import 'package:flutter/material.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; class SuperportShadSelect extends StatefulWidget { final String label; final String? placeholder; final List items; final T? value; final String Function(T) itemLabel; final ValueChanged? onChanged; final bool enabled; final bool searchable; final String? errorText; final String? helperText; final Widget? prefixIcon; final VoidCallback? onClear; final Future> Function(String)? onSearch; final bool required; const SuperportShadSelect({ super.key, required this.label, required this.items, required this.itemLabel, this.placeholder, this.value, this.onChanged, this.enabled = true, this.searchable = false, this.errorText, this.helperText, this.prefixIcon, this.onClear, this.onSearch, this.required = false, }); @override State> createState() => _SuperportShadSelectState(); } class _SuperportShadSelectState extends State> { final TextEditingController _searchController = TextEditingController(); List _filteredItems = []; bool _isSearching = false; @override void initState() { super.initState(); _filteredItems = widget.items; } @override void didUpdateWidget(SuperportShadSelect oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.items != widget.items) { _filteredItems = widget.items; } } @override void dispose() { _searchController.dispose(); super.dispose(); } void _handleSearch(String query) async { if (query.isEmpty) { setState(() { _filteredItems = widget.items; _isSearching = false; }); return; } setState(() { _isSearching = true; }); if (widget.onSearch != null) { final results = await widget.onSearch!(query); setState(() { _filteredItems = results; _isSearching = false; }); } else { setState(() { _filteredItems = widget.items.where((item) { return widget.itemLabel(item) .toLowerCase() .contains(query.toLowerCase()); }).toList(); _isSearching = false; }); } } @override Widget build(BuildContext context) { final theme = ShadTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( widget.label, style: theme.textTheme.small.copyWith( fontWeight: FontWeight.w600, fontSize: 14, ), ), if (widget.required) Text( ' *', style: theme.textTheme.small.copyWith( color: theme.colorScheme.destructive, fontWeight: FontWeight.w600, ), ), ], ), const SizedBox(height: 8), ShadSelect( placeholder: widget.placeholder != null ? Text(widget.placeholder!) : const Text('선택하세요'), selectedOptionBuilder: (context, value) { if (value == null) return const Text('선택하세요'); return Text(widget.itemLabel(value)); }, enabled: widget.enabled, options: _filteredItems.map((item) { return ShadOption( value: item, child: Row( children: [ if (widget.value == item) const Padding( padding: EdgeInsets.only(right: 8), child: Icon( Icons.check, size: 16, ), ), Expanded( child: Text( widget.itemLabel(item), style: theme.textTheme.small.copyWith( fontWeight: widget.value == item ? FontWeight.w600 : FontWeight.w400, ), ), ), ], ), ); }).toList(), onChanged: widget.onChanged, ), if (widget.errorText != null) ...[ const SizedBox(height: 4), Text( widget.errorText!, style: theme.textTheme.small.copyWith( color: theme.colorScheme.destructive, fontSize: 12, ), ), ], if (widget.helperText != null && widget.errorText == null) ...[ const SizedBox(height: 4), Text( widget.helperText!, style: theme.textTheme.small.copyWith( color: theme.colorScheme.mutedForeground, fontSize: 12, ), ), ], ], ); } } class CascadeSelect extends StatefulWidget { final String parentLabel; final String childLabel; final List parentItems; final String Function(T) parentItemLabel; final Future> Function(T) getChildItems; final String Function(U) childItemLabel; final T? parentValue; final U? childValue; final ValueChanged? onParentChanged; final ValueChanged? onChildChanged; final bool enabled; final bool searchable; final bool required; const CascadeSelect({ super.key, required this.parentLabel, required this.childLabel, required this.parentItems, required this.parentItemLabel, required this.getChildItems, required this.childItemLabel, this.parentValue, this.childValue, this.onParentChanged, this.onChildChanged, this.enabled = true, this.searchable = true, this.required = false, }); @override State> createState() => _CascadeSelectState(); } class _CascadeSelectState extends State> { List _childItems = []; bool _isLoadingChildren = false; @override void initState() { super.initState(); if (widget.parentValue != null) { _loadChildItems(widget.parentValue as T); } } @override void didUpdateWidget(CascadeSelect oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.parentValue != widget.parentValue && widget.parentValue != null) { _loadChildItems(widget.parentValue as T); } } Future _loadChildItems(T parentValue) async { setState(() { _isLoadingChildren = true; }); try { final items = await widget.getChildItems(parentValue); if (mounted) { setState(() { _childItems = items; _isLoadingChildren = false; }); } } catch (e) { if (mounted) { setState(() { _childItems = []; _isLoadingChildren = false; }); } } } @override Widget build(BuildContext context) { return Column( children: [ SuperportShadSelect( label: widget.parentLabel, items: widget.parentItems, itemLabel: widget.parentItemLabel, value: widget.parentValue, onChanged: (value) { widget.onParentChanged?.call(value); widget.onChildChanged?.call(null); if (value != null) { _loadChildItems(value); } else { setState(() { _childItems = []; }); } }, enabled: widget.enabled, searchable: widget.searchable, required: widget.required, placeholder: '${widget.parentLabel}을(를) 선택하세요', ), const SizedBox(height: 16), SuperportShadSelect( label: widget.childLabel, items: _childItems, itemLabel: widget.childItemLabel, value: widget.childValue, onChanged: widget.onChildChanged, enabled: widget.enabled && widget.parentValue != null && !_isLoadingChildren, searchable: widget.searchable, required: widget.required, placeholder: _isLoadingChildren ? '로딩 중...' : widget.parentValue == null ? '먼저 ${widget.parentLabel}을(를) 선택하세요' : '${widget.childLabel}을(를) 선택하세요', ), ], ); } }