hazo_config 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +62 -0
  2. package/dist/components/app_config_list_editor/app_config_list_editor.d.ts +7 -0
  3. package/dist/components/app_config_list_editor/app_config_list_editor.d.ts.map +1 -0
  4. package/dist/components/app_config_list_editor/app_config_list_editor.js +129 -0
  5. package/dist/components/app_config_list_editor/components/color_swatch_picker.d.ts +9 -0
  6. package/dist/components/app_config_list_editor/components/color_swatch_picker.d.ts.map +1 -0
  7. package/dist/components/app_config_list_editor/components/color_swatch_picker.js +15 -0
  8. package/dist/components/app_config_list_editor/components/delete_dialog.d.ts +10 -0
  9. package/dist/components/app_config_list_editor/components/delete_dialog.d.ts.map +1 -0
  10. package/dist/components/app_config_list_editor/components/delete_dialog.js +9 -0
  11. package/dist/components/app_config_list_editor/components/edit_modal.d.ts +19 -0
  12. package/dist/components/app_config_list_editor/components/edit_modal.d.ts.map +1 -0
  13. package/dist/components/app_config_list_editor/components/edit_modal.js +97 -0
  14. package/dist/components/app_config_list_editor/components/empty_state.d.ts +8 -0
  15. package/dist/components/app_config_list_editor/components/empty_state.d.ts.map +1 -0
  16. package/dist/components/app_config_list_editor/components/empty_state.js +8 -0
  17. package/dist/components/app_config_list_editor/components/import_export_buttons.d.ts +12 -0
  18. package/dist/components/app_config_list_editor/components/import_export_buttons.d.ts.map +1 -0
  19. package/dist/components/app_config_list_editor/components/import_export_buttons.js +74 -0
  20. package/dist/components/app_config_list_editor/components/list_item_row.d.ts +14 -0
  21. package/dist/components/app_config_list_editor/components/list_item_row.d.ts.map +1 -0
  22. package/dist/components/app_config_list_editor/components/list_item_row.js +14 -0
  23. package/dist/components/app_config_list_editor/components/save_status_indicator.d.ts +7 -0
  24. package/dist/components/app_config_list_editor/components/save_status_indicator.d.ts.map +1 -0
  25. package/dist/components/app_config_list_editor/components/save_status_indicator.js +25 -0
  26. package/dist/components/app_config_list_editor/components/search_bar.d.ts +10 -0
  27. package/dist/components/app_config_list_editor/components/search_bar.d.ts.map +1 -0
  28. package/dist/components/app_config_list_editor/components/search_bar.js +8 -0
  29. package/dist/components/app_config_list_editor/index.d.ts +4 -0
  30. package/dist/components/app_config_list_editor/index.d.ts.map +1 -0
  31. package/dist/components/app_config_list_editor/index.js +3 -0
  32. package/dist/components/app_config_list_editor/types.d.ts +102 -0
  33. package/dist/components/app_config_list_editor/types.d.ts.map +1 -0
  34. package/dist/components/app_config_list_editor/types.js +14 -0
  35. package/dist/components/app_config_list_editor/utils/json_import_export.d.ts +26 -0
  36. package/dist/components/app_config_list_editor/utils/json_import_export.d.ts.map +1 -0
  37. package/dist/components/app_config_list_editor/utils/json_import_export.js +112 -0
  38. package/dist/components/index.d.ts +3 -0
  39. package/dist/components/index.d.ts.map +1 -1
  40. package/dist/components/index.js +3 -0
  41. package/dist/components/ui/alert-dialog.d.ts +21 -0
  42. package/dist/components/ui/alert-dialog.d.ts.map +1 -0
  43. package/dist/components/ui/alert-dialog.js +26 -0
  44. package/dist/components/ui/button.d.ts +12 -0
  45. package/dist/components/ui/button.d.ts.map +1 -0
  46. package/dist/components/ui/button.js +33 -0
  47. package/dist/components/ui/dialog.d.ts +20 -0
  48. package/dist/components/ui/dialog.d.ts.map +1 -0
  49. package/dist/components/ui/dialog.js +22 -0
  50. package/dist/components/ui/input.d.ts +4 -0
  51. package/dist/components/ui/input.d.ts.map +1 -0
  52. package/dist/components/ui/input.js +8 -0
  53. package/package.json +7 -1
  54. package/MIGRATION_V2.md +0 -531
package/README.md CHANGED
@@ -223,6 +223,66 @@ The AppConfig component supports:
223
223
  - Section-based organization
224
224
  - Type conversion between general and JSON
225
225
 
226
+ ### Example: Generic List Editor (v2.0.2+)
227
+
228
+ ```typescript
229
+ import { AppConfigListEditor } from 'hazo_config'
230
+ import type { ColumnDef } from 'hazo_config'
231
+
232
+ interface Tag {
233
+ [key: string]: unknown
234
+ tag_id: string
235
+ tag_label: string
236
+ color: string
237
+ description: string
238
+ }
239
+
240
+ const TAG_COLUMNS: ColumnDef<Tag>[] = [
241
+ { field: 'tag_label', label: 'Label', type: 'text', required: true, list_display: 'primary' },
242
+ { field: 'tag_id', label: 'Tag ID', type: 'text', required: true, list_display: 'badge' },
243
+ { field: 'color', label: 'Color', type: 'color_swatch', color_options: ['bg-blue-100 text-blue-800', 'bg-green-100 text-green-800'] },
244
+ { field: 'description', label: 'Description', type: 'textarea', list_display: 'secondary' },
245
+ ]
246
+
247
+ export function TagManager({ tags, onTagsChange }) {
248
+ return (
249
+ <AppConfigListEditor<Tag>
250
+ items={tags}
251
+ on_items_change={onTagsChange}
252
+ columns={TAG_COLUMNS}
253
+ id_field="tag_id"
254
+ auto_id_from="tag_label"
255
+ title="Classification Tags"
256
+ description="Manage tags for document categorization."
257
+ enable_search={true}
258
+ delete_confirmation={(tag) => `Delete "${tag.tag_label}"?`}
259
+ />
260
+ )
261
+ }
262
+ ```
263
+
264
+ The `AppConfigListEditor` is a generic, reusable CRUD list editor for arrays of structured objects. It supports:
265
+ - Dynamic form fields (text, textarea, number, select, color_swatch, toggle)
266
+ - Auto-ID generation from a source field
267
+ - Search/filter for long lists
268
+ - Delete confirmation dialogs
269
+ - Color swatch picker
270
+ - Save status indicators (saving/saved/error)
271
+ - Custom item rendering (indicator, preview, full row override)
272
+ - Field validation with inline error messages
273
+ - **JSON import/export** — export items as `.json` file, import from file with validation and duplicate skipping
274
+
275
+ #### JSON Import/Export
276
+
277
+ The list editor includes built-in Export and Import buttons in the section bar. Export downloads the current items as a formatted JSON file. Import opens a file picker, validates the JSON against column definitions, and merges new items into the list (skipping duplicates by ID).
278
+
279
+ Utility functions are also exported for programmatic use:
280
+
281
+ ```typescript
282
+ import { validate_import_data, merge_items, export_items_to_json, generate_export_filename } from 'hazo_config'
283
+ import type { ImportResult } from 'hazo_config'
284
+ ```
285
+
226
286
  ## Development
227
287
 
228
288
  ### Local Setup
@@ -266,6 +326,8 @@ hazo_config/
266
326
  │ │ ├── config_viewer.tsx # INI config viewer
267
327
  │ │ ├── config_editor.tsx # INI config editor
268
328
  │ │ ├── app_config.tsx # Database-backed config (v2.0+)
329
+ │ │ ├── app_config_list_editor/ # Generic CRUD list editor (v2.0.2+)
330
+ │ │ ├── ui/ # shadcn/ui primitives (Dialog, Button, etc.)
269
331
  │ │ ├── use_app_config.ts # Hook for app config
270
332
  │ │ └── *.stories.tsx
271
333
  │ ├── lib/ # Core config management
@@ -0,0 +1,7 @@
1
+ import type { AppConfigListEditorProps } from './types.js';
2
+ /**
3
+ * AppConfigListEditor - A polished CRUD list editor for config item arrays.
4
+ * Purely callback-based: no API calls, no database access.
5
+ */
6
+ export declare function AppConfigListEditor<T extends Record<string, unknown>>({ items, on_items_change, columns, id_field, auto_id_from, title, description, enable_search, search_threshold, render_item, render_item_indicator, render_preview, delete_confirmation, max_items, id_editable_after_create, className, save_status, }: AppConfigListEditorProps<T>): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=app_config_list_editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app_config_list_editor.d.ts","sourceRoot":"","sources":["../../../src/components/app_config_list_editor/app_config_list_editor.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,wBAAwB,EAAqC,MAAM,YAAY,CAAA;AAS7F;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACrE,KAAK,EACL,eAAe,EACf,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,KAAK,EACL,WAAW,EACX,aAAqB,EACrB,gBAAoB,EACpB,WAAW,EACX,qBAAqB,EACrB,cAAc,EACd,mBAAmB,EACnB,SAAS,EACT,wBAAgC,EAChC,SAAS,EACT,WAAoB,GACrB,EAAE,wBAAwB,CAAC,CAAC,CAAC,2CAgQ7B"}
@@ -0,0 +1,129 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // AppConfigListEditor - Main component
3
+ // Generic CRUD list editor for arrays of structured objects stored as JSON config
4
+ import { useState, useCallback, useMemo } from 'react';
5
+ import { Plus } from 'lucide-react';
6
+ import { cn } from '../../lib/utils.js';
7
+ import { ListItemRow } from './components/list_item_row.js';
8
+ import { EditModal } from './components/edit_modal.js';
9
+ import { SearchBar } from './components/search_bar.js';
10
+ import { EmptyState } from './components/empty_state.js';
11
+ import { DeleteDialog } from './components/delete_dialog.js';
12
+ import { SaveStatusIndicator } from './components/save_status_indicator.js';
13
+ import { ImportExportButtons } from './components/import_export_buttons.js';
14
+ /**
15
+ * AppConfigListEditor - A polished CRUD list editor for config item arrays.
16
+ * Purely callback-based: no API calls, no database access.
17
+ */
18
+ export function AppConfigListEditor({ items, on_items_change, columns, id_field, auto_id_from, title, description, enable_search = false, search_threshold = 8, render_item, render_item_indicator, render_preview, delete_confirmation, max_items, id_editable_after_create = false, className, save_status = 'idle', }) {
19
+ const [search_term, set_search_term] = useState('');
20
+ const [edit_state, set_edit_state] = useState(null);
21
+ const [delete_state, set_delete_state] = useState(null);
22
+ // Derive a friendly item label from the title (e.g., "Classification Tags" -> "tags")
23
+ const item_label = useMemo(() => {
24
+ if (!title)
25
+ return 'items';
26
+ const words = title.toLowerCase().split(/\s+/);
27
+ return words[words.length - 1] || 'items';
28
+ }, [title]);
29
+ // Derive singular item type label (e.g., "tags" -> "Tag")
30
+ const item_type_label = useMemo(() => {
31
+ const singular = item_label.replace(/s$/, '');
32
+ return singular.charAt(0).toUpperCase() + singular.slice(1);
33
+ }, [item_label]);
34
+ // Derive section header label (uppercase, e.g., "TAGS")
35
+ const section_label = useMemo(() => item_label.toUpperCase(), [item_label]);
36
+ // Get all existing IDs for duplicate checking
37
+ const existing_ids = useMemo(() => items.map((item) => String(item[id_field])), [items, id_field]);
38
+ // Filter items by search term
39
+ const filtered_items = useMemo(() => {
40
+ if (!search_term.trim())
41
+ return items;
42
+ const term = search_term.toLowerCase();
43
+ return items.filter((item) => columns.some((col) => {
44
+ if (col.list_display === 'hidden' && col.show_in_list === false)
45
+ return false;
46
+ const val = item[col.field];
47
+ return val !== undefined && val !== null && String(val).toLowerCase().includes(term);
48
+ }));
49
+ }, [items, search_term, columns]);
50
+ const show_search = enable_search && items.length > search_threshold;
51
+ const is_max_reached = max_items !== undefined && max_items !== null && items.length >= max_items;
52
+ // CRUD handlers
53
+ const handle_add = useCallback(() => {
54
+ const empty_item = {};
55
+ // Initialize with defaults for toggle fields
56
+ for (const col of columns) {
57
+ if (col.type === 'toggle') {
58
+ empty_item[col.field] = false;
59
+ }
60
+ }
61
+ set_edit_state({
62
+ mode: 'create',
63
+ item: empty_item,
64
+ errors: new Map(),
65
+ user_touched_id: false,
66
+ });
67
+ }, [columns]);
68
+ const handle_edit = useCallback((item) => {
69
+ set_edit_state({
70
+ mode: 'edit',
71
+ item: { ...item },
72
+ original_item: item,
73
+ errors: new Map(),
74
+ user_touched_id: true,
75
+ });
76
+ }, []);
77
+ const handle_delete_request = useCallback((item) => {
78
+ if (!delete_confirmation) {
79
+ // No confirmation needed, delete immediately
80
+ on_items_change(items.filter((i) => i[id_field] !== item[id_field]));
81
+ return;
82
+ }
83
+ set_delete_state({ item });
84
+ }, [delete_confirmation, items, id_field, on_items_change]);
85
+ const handle_delete_confirm = useCallback(() => {
86
+ if (!delete_state)
87
+ return;
88
+ on_items_change(items.filter((i) => i[id_field] !== delete_state.item[id_field]));
89
+ set_delete_state(null);
90
+ }, [delete_state, items, id_field, on_items_change]);
91
+ const handle_save = useCallback((saved_item) => {
92
+ if (edit_state?.mode === 'create') {
93
+ on_items_change([...items, saved_item]);
94
+ }
95
+ else if (edit_state?.mode === 'edit') {
96
+ const original_id = edit_state.original_item?.[id_field];
97
+ on_items_change(items.map((i) => (i[id_field] === original_id ? saved_item : i)));
98
+ }
99
+ set_edit_state(null);
100
+ }, [edit_state, items, id_field, on_items_change]);
101
+ const handle_cancel_edit = useCallback(() => {
102
+ set_edit_state(null);
103
+ }, []);
104
+ const handle_cancel_delete = useCallback(() => {
105
+ set_delete_state(null);
106
+ }, []);
107
+ // Get delete confirmation message
108
+ const delete_message = useMemo(() => {
109
+ if (!delete_state || !delete_confirmation)
110
+ return '';
111
+ if (typeof delete_confirmation === 'function') {
112
+ return delete_confirmation(delete_state.item);
113
+ }
114
+ return delete_confirmation;
115
+ }, [delete_state, delete_confirmation]);
116
+ // Get delete item name for dialog title
117
+ const delete_item_name = useMemo(() => {
118
+ if (!delete_state)
119
+ return '';
120
+ const primary_col = columns.find((c) => c.list_display === 'primary');
121
+ if (primary_col) {
122
+ return String(delete_state.item[primary_col.field] ?? '');
123
+ }
124
+ return String(delete_state.item[id_field] ?? '');
125
+ }, [delete_state, columns, id_field]);
126
+ return (_jsxs("div", { className: cn('cls_list_editor', className), children: [(title || description) && (_jsxs("div", { className: "cls_list_editor_header mb-6", children: [title && (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("h2", { className: "text-2xl font-bold text-gray-900", children: title }), _jsx(SaveStatusIndicator, { status: save_status })] })), description && (_jsx("p", { className: "text-sm text-gray-500 mt-1", children: description }))] })), _jsxs("div", { className: "cls_list_editor_section_bar flex items-center justify-between mb-3", children: [_jsx("div", { className: "flex items-center gap-2", children: _jsx("span", { className: "text-xs font-semibold uppercase tracking-wider text-gray-500", children: section_label }) }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(ImportExportButtons, { items: items, on_items_change: on_items_change, columns: columns, id_field: id_field, title: title, max_items: max_items }), _jsxs("button", { type: "button", onClick: handle_add, disabled: is_max_reached, className: cn('inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-full transition-colors', is_max_reached
127
+ ? 'bg-gray-100 text-gray-400 cursor-not-allowed'
128
+ : 'bg-violet-600 hover:bg-violet-700 text-white'), title: is_max_reached ? `Maximum of ${max_items} items reached` : undefined, children: [_jsx(Plus, { className: "w-4 h-4" }), "Add"] })] })] }), show_search && (_jsx("div", { className: "mb-3", children: _jsx(SearchBar, { value: search_term, on_change: set_search_term, item_count: filtered_items.length, item_label: item_label }) })), _jsx("div", { className: "cls_list_editor_card rounded-xl border border-gray-200 bg-white overflow-hidden", children: items.length === 0 ? (_jsx(EmptyState, { item_label: item_label, on_add: handle_add })) : filtered_items.length === 0 ? (_jsxs("div", { className: "py-8 text-center text-sm text-gray-500", children: ["No ", item_label, " matching \u201C", search_term, "\u201D"] })) : (_jsx("div", { className: "divide-y divide-gray-100", children: filtered_items.map((item, index) => (_jsx(ListItemRow, { item: item, index: index, columns: columns, render_item: render_item, render_item_indicator: render_item_indicator, on_edit: () => handle_edit(item), on_delete: () => handle_delete_request(item) }, String(item[id_field])))) })) }), is_max_reached && (_jsxs("p", { className: "text-xs text-gray-400 mt-2", children: ["Maximum of ", max_items, " ", item_label, " reached."] })), edit_state && (_jsx(EditModal, { open: true, mode: edit_state.mode, item: edit_state.item, columns: columns, id_field: id_field, auto_id_from: auto_id_from, id_editable_after_create: id_editable_after_create, existing_ids: existing_ids, item_type_label: item_type_label, render_preview: render_preview, on_save: handle_save, on_cancel: handle_cancel_edit })), delete_state && (_jsx(DeleteDialog, { open: true, item_name: delete_item_name, message: delete_message, on_confirm: handle_delete_confirm, on_cancel: handle_cancel_delete }))] }));
129
+ }
@@ -0,0 +1,9 @@
1
+ interface ColorSwatchPickerProps {
2
+ value: string;
3
+ options: string[];
4
+ on_change: (color: string) => void;
5
+ className?: string;
6
+ }
7
+ export declare function ColorSwatchPicker({ value, options, on_change, className, }: ColorSwatchPickerProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=color_swatch_picker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"color_swatch_picker.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/color_swatch_picker.tsx"],"names":[],"mappings":"AAMA,UAAU,sBAAsB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,OAAO,EACP,SAAS,EACT,SAAS,GACV,EAAE,sBAAsB,2CA+BxB"}
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // Color swatch picker component
3
+ // Displays a row of circular color swatches for selection
4
+ import { Check } from 'lucide-react';
5
+ import { cn } from '../../../lib/utils.js';
6
+ export function ColorSwatchPicker({ value, options, on_change, className, }) {
7
+ return (_jsx("div", { className: cn('cls_color_swatch_picker flex flex-wrap gap-2', className), children: options.map((color) => {
8
+ const is_selected = value === color;
9
+ // Extract the bg class for the swatch display
10
+ const bg_class = color.split(' ').find(c => c.startsWith('bg-')) || 'bg-gray-200';
11
+ return (_jsx("button", { type: "button", onClick: () => on_change(color), className: cn('cls_color_swatch w-7 h-7 rounded-full flex items-center justify-center transition-transform hover:scale-110', bg_class, is_selected
12
+ ? 'ring-2 ring-violet-600 ring-offset-2'
13
+ : 'ring-1 ring-gray-200'), "aria-label": `Select color ${color}`, "aria-pressed": is_selected, children: is_selected && (_jsx(Check, { className: "w-3.5 h-3.5 text-current opacity-80" })) }, color));
14
+ }) }));
15
+ }
@@ -0,0 +1,10 @@
1
+ interface DeleteDialogProps {
2
+ open: boolean;
3
+ item_name: string;
4
+ message: string;
5
+ on_confirm: () => void;
6
+ on_cancel: () => void;
7
+ }
8
+ export declare function DeleteDialog({ open, item_name, message, on_confirm, on_cancel, }: DeleteDialogProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
10
+ //# sourceMappingURL=delete_dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delete_dialog.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/delete_dialog.tsx"],"names":[],"mappings":"AAeA,UAAU,iBAAiB;IACzB,IAAI,EAAE,OAAO,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,wBAAgB,YAAY,CAAC,EAC3B,IAAI,EACJ,SAAS,EACT,OAAO,EACP,UAAU,EACV,SAAS,GACV,EAAE,iBAAiB,2CAgCnB"}
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Delete confirmation dialog component
3
+ // Uses shadcn AlertDialog with red warning styling
4
+ import { AlertTriangle } from 'lucide-react';
5
+ import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, } from '../../ui/alert-dialog.js';
6
+ export function DeleteDialog({ open, item_name, message, on_confirm, on_cancel, }) {
7
+ return (_jsx(AlertDialog, { open: open, onOpenChange: (is_open) => { if (!is_open)
8
+ on_cancel(); }, children: _jsxs(AlertDialogContent, { className: "cls_delete_dialog max-w-sm rounded-2xl", children: [_jsxs(AlertDialogHeader, { className: "items-center sm:items-center", children: [_jsx("div", { className: "w-12 h-12 rounded-full bg-red-100 flex items-center justify-center mb-2", children: _jsx(AlertTriangle, { className: "w-6 h-6 text-red-600" }) }), _jsxs(AlertDialogTitle, { className: "text-center", children: ["Delete \u201C", item_name, "\u201D?"] }), _jsx(AlertDialogDescription, { className: "text-center", children: message })] }), _jsxs(AlertDialogFooter, { className: "sm:justify-center gap-2", children: [_jsx(AlertDialogCancel, { onClick: on_cancel, className: "rounded-full", children: "Cancel" }), _jsx(AlertDialogAction, { onClick: on_confirm, className: "rounded-full bg-red-600 text-white hover:bg-red-700", children: "Delete" })] })] }) }));
9
+ }
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import type { ColumnDef } from '../types.js';
3
+ interface EditModalProps<T extends Record<string, unknown>> {
4
+ open: boolean;
5
+ mode: 'create' | 'edit';
6
+ item: Partial<T>;
7
+ columns: ColumnDef<T>[];
8
+ id_field: keyof T & string;
9
+ auto_id_from?: keyof T & string;
10
+ id_editable_after_create?: boolean;
11
+ existing_ids: string[];
12
+ item_type_label: string;
13
+ render_preview?: (item: T) => React.ReactNode;
14
+ on_save: (item: T) => void;
15
+ on_cancel: () => void;
16
+ }
17
+ export declare function EditModal<T extends Record<string, unknown>>({ open, mode, item: initial_item, columns, id_field, auto_id_from, id_editable_after_create, existing_ids, item_type_label, render_preview, on_save, on_cancel, }: EditModalProps<T>): import("react/jsx-runtime").JSX.Element;
18
+ export {};
19
+ //# sourceMappingURL=edit_modal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edit_modal.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/edit_modal.tsx"],"names":[],"mappings":"AAGA,OAAO,KAA2C,MAAM,OAAO,CAAA;AAY/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAG5C,UAAU,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACxD,IAAI,EAAE,OAAO,CAAA;IACb,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAA;IACvB,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IAChB,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IAC/B,wBAAwB,CAAC,EAAE,OAAO,CAAA;IAClC,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,eAAe,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,KAAK,CAAC,SAAS,CAAA;IAC7C,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAA;IAC1B,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC3D,IAAI,EACJ,IAAI,EACJ,IAAI,EAAE,YAAY,EAClB,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,wBAAwB,EACxB,YAAY,EACZ,eAAe,EACf,cAAc,EACd,OAAO,EACP,SAAS,GACV,EAAE,cAAc,CAAC,CAAC,CAAC,2CA2PnB"}
@@ -0,0 +1,97 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Edit modal component
3
+ // Centered dialog for creating/editing items with dynamic form fields
4
+ import { useState, useCallback, useEffect } from 'react';
5
+ import { cn } from '../../../lib/utils.js';
6
+ import { Input } from '../../ui/input.js';
7
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '../../ui/dialog.js';
8
+ import { Button } from '../../ui/button.js';
9
+ import { ColorSwatchPicker } from './color_swatch_picker.js';
10
+ import { slugify } from '../types.js';
11
+ export function EditModal({ open, mode, item: initial_item, columns, id_field, auto_id_from, id_editable_after_create, existing_ids, item_type_label, render_preview, on_save, on_cancel, }) {
12
+ const [form_data, set_form_data] = useState({});
13
+ const [errors, set_errors] = useState(new Map());
14
+ const [user_touched_id, set_user_touched_id] = useState(false);
15
+ // Reset form when modal opens
16
+ useEffect(() => {
17
+ if (open) {
18
+ set_form_data({ ...initial_item });
19
+ set_errors(new Map());
20
+ set_user_touched_id(mode === 'edit');
21
+ }
22
+ }, [open, initial_item, mode]);
23
+ const update_field = useCallback((field, value) => {
24
+ set_form_data((prev) => {
25
+ const updated = { ...prev, [field]: value };
26
+ // Auto-generate ID from source field when creating
27
+ if (auto_id_from &&
28
+ field === auto_id_from &&
29
+ !user_touched_id &&
30
+ mode === 'create') {
31
+ updated[id_field] = slugify(String(value ?? ''));
32
+ }
33
+ return updated;
34
+ });
35
+ // Clear error for this field on change
36
+ set_errors((prev) => {
37
+ const next = new Map(prev);
38
+ next.delete(field);
39
+ return next;
40
+ });
41
+ }, [auto_id_from, id_field, mode, user_touched_id]);
42
+ const handle_id_change = useCallback((value) => {
43
+ set_user_touched_id(true);
44
+ update_field(id_field, value);
45
+ }, [id_field, update_field]);
46
+ const validate_form = useCallback(() => {
47
+ const new_errors = new Map();
48
+ for (const col of columns) {
49
+ const value = form_data[col.field];
50
+ // Required check
51
+ if (col.required && (value === undefined || value === null || value === '')) {
52
+ new_errors.set(col.field, `${col.label} is required`);
53
+ continue;
54
+ }
55
+ // Custom validation
56
+ if (col.validate && value !== undefined && value !== null && value !== '') {
57
+ const error = col.validate(value, form_data);
58
+ if (error) {
59
+ new_errors.set(col.field, error);
60
+ }
61
+ }
62
+ }
63
+ // Check ID field is filled
64
+ const id_value = form_data[id_field];
65
+ if (!id_value || String(id_value).trim() === '') {
66
+ new_errors.set(id_field, 'ID is required');
67
+ }
68
+ // Check for duplicate ID
69
+ if (id_value) {
70
+ const id_str = String(id_value);
71
+ const is_duplicate = mode === 'create'
72
+ ? existing_ids.includes(id_str)
73
+ : existing_ids.filter(id => id !== String(initial_item[id_field])).includes(id_str);
74
+ if (is_duplicate) {
75
+ new_errors.set(id_field, 'This ID already exists');
76
+ }
77
+ }
78
+ set_errors(new_errors);
79
+ return new_errors.size === 0;
80
+ }, [columns, form_data, id_field, existing_ids, mode, initial_item]);
81
+ const handle_save = useCallback(() => {
82
+ if (validate_form()) {
83
+ on_save(form_data);
84
+ }
85
+ }, [validate_form, form_data, on_save]);
86
+ const render_field = (col) => {
87
+ const value = form_data[col.field];
88
+ const error = errors.get(col.field);
89
+ const is_id_field = col.field === id_field;
90
+ const is_disabled = is_id_field && mode === 'edit' && !id_editable_after_create;
91
+ return (_jsxs("div", { className: "cls_edit_field space-y-1.5", children: [_jsxs("label", { className: "text-sm font-medium text-gray-700", children: [col.label, col.required && _jsx("span", { className: "text-red-500 ml-0.5", children: "*" })] }), col.type === 'text' && (_jsx(Input, { type: "text", value: String(value ?? ''), onChange: (e) => is_id_field
92
+ ? handle_id_change(e.target.value)
93
+ : update_field(col.field, e.target.value), placeholder: col.placeholder, disabled: is_disabled, className: cn('rounded-lg focus-visible:ring-violet-500/20 focus-visible:ring-offset-0', is_disabled && 'bg-gray-50 text-gray-500', error && 'border-red-400 focus-visible:ring-red-500/20') })), col.type === 'number' && (_jsx(Input, { type: "number", value: value !== undefined && value !== null ? String(value) : '', onChange: (e) => update_field(col.field, e.target.value === '' ? '' : Number(e.target.value)), placeholder: col.placeholder, className: cn('rounded-lg focus-visible:ring-violet-500/20 focus-visible:ring-offset-0', error && 'border-red-400 focus-visible:ring-red-500/20') })), col.type === 'textarea' && (_jsx("textarea", { value: String(value ?? ''), onChange: (e) => update_field(col.field, e.target.value), placeholder: col.placeholder, rows: 3, className: cn('flex w-full rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500/20 focus-visible:border-violet-300 disabled:cursor-not-allowed disabled:opacity-50', error && 'border-red-400 focus-visible:ring-red-500/20') })), col.type === 'select' && col.options && (_jsxs("select", { value: String(value ?? ''), onChange: (e) => update_field(col.field, e.target.value), className: cn('flex h-10 w-full rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500/20 focus-visible:border-violet-300', error && 'border-red-400 focus-visible:ring-red-500/20'), children: [_jsx("option", { value: "", children: col.placeholder || 'Select...' }), col.options.map((opt) => (_jsx("option", { value: opt.value, children: opt.label }, opt.value)))] })), col.type === 'color_swatch' && col.color_options && (_jsx(ColorSwatchPicker, { value: String(value ?? ''), options: col.color_options, on_change: (color) => update_field(col.field, color) })), col.type === 'toggle' && (_jsx("button", { type: "button", role: "switch", "aria-checked": Boolean(value), onClick: () => update_field(col.field, !value), className: cn('relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors', value ? 'bg-violet-600' : 'bg-gray-200'), children: _jsx("span", { className: cn('pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-lg ring-0 transition-transform', value ? 'translate-x-5' : 'translate-x-0') }) })), is_id_field && auto_id_from && mode === 'create' && !user_touched_id && (_jsxs("p", { className: "text-xs text-gray-400", children: ["Auto-generated from ", columns.find(c => c.field === auto_id_from)?.label?.toLowerCase() || auto_id_from] })), error && (_jsx("p", { className: "text-xs text-red-600", children: error }))] }, col.field));
94
+ };
95
+ return (_jsx(Dialog, { open: open, onOpenChange: (is_open) => { if (!is_open)
96
+ on_cancel(); }, children: _jsxs(DialogContent, { className: "cls_edit_modal max-w-[460px] rounded-2xl p-0 gap-0 overflow-hidden", children: [_jsx(DialogHeader, { className: "px-6 pt-6 pb-4", children: _jsx(DialogTitle, { children: mode === 'create' ? `New ${item_type_label}` : `Edit ${item_type_label}` }) }), _jsxs("div", { className: "px-6 pb-4 space-y-4", children: [render_preview && Object.keys(form_data).length > 0 && (_jsx("div", { className: "p-3 bg-gray-50 rounded-lg border border-gray-100", children: render_preview(form_data) })), columns.map((col) => render_field(col))] }), _jsxs(DialogFooter, { className: "bg-gray-50 px-6 py-4 border-t border-gray-100 sm:justify-between", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: on_cancel, className: "rounded-full", children: "Cancel" }), _jsx(Button, { type: "button", onClick: handle_save, className: "rounded-full bg-violet-600 hover:bg-violet-700 text-white", children: mode === 'create' ? `Add ${item_type_label}` : 'Save Changes' })] })] }) }));
97
+ }
@@ -0,0 +1,8 @@
1
+ interface EmptyStateProps {
2
+ item_label: string;
3
+ on_add: () => void;
4
+ className?: string;
5
+ }
6
+ export declare function EmptyState({ item_label, on_add, className, }: EmptyStateProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
8
+ //# sourceMappingURL=empty_state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"empty_state.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/empty_state.tsx"],"names":[],"mappings":"AAMA,UAAU,eAAe;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,UAAU,CAAC,EACzB,UAAU,EACV,MAAM,EACN,SAAS,GACV,EAAE,eAAe,2CAsBjB"}
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Empty state component
3
+ // Displayed when the list has no items
4
+ import { Inbox, Plus } from 'lucide-react';
5
+ import { cn } from '../../../lib/utils.js';
6
+ export function EmptyState({ item_label, on_add, className, }) {
7
+ return (_jsxs("div", { className: cn('cls_empty_state flex flex-col items-center justify-center py-12 px-4', className), children: [_jsx("div", { className: "w-12 h-12 rounded-full bg-gray-100 flex items-center justify-center mb-3", children: _jsx(Inbox, { className: "w-6 h-6 text-gray-400" }) }), _jsxs("p", { className: "text-sm font-medium text-gray-900 mb-1", children: ["No ", item_label, " yet"] }), _jsxs("p", { className: "text-sm text-gray-500 mb-4", children: ["Get started by adding your first ", item_label.replace(/s$/, ''), "."] }), _jsxs("button", { type: "button", onClick: on_add, className: "inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-white bg-violet-600 hover:bg-violet-700 rounded-full transition-colors", children: [_jsx(Plus, { className: "w-4 h-4" }), "Add your first ", item_label.replace(/s$/, '')] })] }));
8
+ }
@@ -0,0 +1,12 @@
1
+ import type { ColumnDef } from '../types.js';
2
+ interface ImportExportButtonsProps<T extends Record<string, unknown>> {
3
+ items: T[];
4
+ on_items_change: (items: T[]) => void;
5
+ columns: ColumnDef<T>[];
6
+ id_field: keyof T & string;
7
+ title?: string;
8
+ max_items?: number;
9
+ }
10
+ export declare function ImportExportButtons<T extends Record<string, unknown>>({ items, on_items_change, columns, id_field, title, max_items, }: ImportExportButtonsProps<T>): import("react/jsx-runtime").JSX.Element;
11
+ export {};
12
+ //# sourceMappingURL=import_export_buttons.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import_export_buttons.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/import_export_buttons.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAQ5C,UAAU,wBAAwB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAClE,KAAK,EAAE,CAAC,EAAE,CAAA;IACV,eAAe,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,IAAI,CAAA;IACrC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAA;IACvB,QAAQ,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACrE,KAAK,EACL,eAAe,EACf,OAAO,EACP,QAAQ,EACR,KAAK,EACL,SAAS,GACV,EAAE,wBAAwB,CAAC,CAAC,CAAC,2CA0H7B"}
@@ -0,0 +1,74 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Import/Export buttons for AppConfigListEditor
3
+ // Provides JSON export (download) and import (file picker) with transient status messages
4
+ import { useRef, useState, useCallback, useEffect } from 'react';
5
+ import { Download, Upload } from 'lucide-react';
6
+ import { cn } from '../../../lib/utils.js';
7
+ import { validate_import_data, merge_items, export_items_to_json, generate_export_filename, } from '../utils/json_import_export.js';
8
+ export function ImportExportButtons({ items, on_items_change, columns, id_field, title, max_items, }) {
9
+ const file_input_ref = useRef(null);
10
+ const [status_message, set_status_message] = useState(null);
11
+ // Auto-fade status message after 3 seconds
12
+ useEffect(() => {
13
+ if (!status_message)
14
+ return;
15
+ const timer = setTimeout(() => set_status_message(null), 3000);
16
+ return () => clearTimeout(timer);
17
+ }, [status_message]);
18
+ const handle_export = useCallback(() => {
19
+ const filename = generate_export_filename(title);
20
+ export_items_to_json(items, filename);
21
+ }, [items, title]);
22
+ const handle_import_click = useCallback(() => {
23
+ // Reset input so the same file can be re-imported
24
+ if (file_input_ref.current) {
25
+ file_input_ref.current.value = '';
26
+ }
27
+ file_input_ref.current?.click();
28
+ }, []);
29
+ const handle_file_selected = useCallback((event) => {
30
+ const file = event.target.files?.[0];
31
+ if (!file)
32
+ return;
33
+ const reader = new FileReader();
34
+ reader.onload = (e) => {
35
+ const text = e.target?.result;
36
+ if (typeof text !== 'string') {
37
+ set_status_message({ text: 'Failed to read file', is_error: true });
38
+ return;
39
+ }
40
+ let data;
41
+ try {
42
+ data = JSON.parse(text);
43
+ }
44
+ catch {
45
+ set_status_message({ text: 'Invalid JSON file', is_error: true });
46
+ return;
47
+ }
48
+ const { valid_items, errors } = validate_import_data(data, columns, id_field);
49
+ if (valid_items.length === 0) {
50
+ const msg = errors.length > 0 ? errors[0] : 'No valid items found';
51
+ set_status_message({ text: msg, is_error: true });
52
+ return;
53
+ }
54
+ const result = merge_items(items, valid_items, id_field, max_items);
55
+ on_items_change(result.merged_items);
56
+ const parts = [];
57
+ if (result.added_count > 0) {
58
+ parts.push(`Added ${result.added_count}`);
59
+ }
60
+ if (result.skipped_count > 0) {
61
+ parts.push(`${result.skipped_count} skipped`);
62
+ }
63
+ if (errors.length > 0) {
64
+ parts.push(`${errors.length} invalid`);
65
+ }
66
+ set_status_message({
67
+ text: parts.join(', '),
68
+ is_error: result.added_count === 0,
69
+ });
70
+ };
71
+ reader.readAsText(file);
72
+ }, [items, columns, id_field, max_items, on_items_change]);
73
+ return (_jsxs("div", { className: "cls_import_export flex items-center gap-1.5", children: [status_message && (_jsx("span", { className: cn('cls_import_status text-xs transition-opacity duration-300', status_message.is_error ? 'text-red-600' : 'text-green-600'), children: status_message.text })), _jsxs("button", { type: "button", onClick: handle_export, className: "inline-flex items-center gap-1 px-2 py-1.5 text-xs font-medium text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full transition-colors", title: "Export as JSON", children: [_jsx(Download, { className: "w-3.5 h-3.5" }), "Export"] }), _jsxs("button", { type: "button", onClick: handle_import_click, className: "inline-flex items-center gap-1 px-2 py-1.5 text-xs font-medium text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full transition-colors", title: "Import from JSON", children: [_jsx(Upload, { className: "w-3.5 h-3.5" }), "Import"] }), _jsx("input", { ref: file_input_ref, type: "file", accept: ".json", onChange: handle_file_selected, className: "hidden" })] }));
74
+ }
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import type { ColumnDef } from '../types.js';
3
+ interface ListItemRowProps<T extends Record<string, unknown>> {
4
+ item: T;
5
+ index: number;
6
+ columns: ColumnDef<T>[];
7
+ render_item?: (item: T, index: number) => React.ReactNode;
8
+ render_item_indicator?: (item: T) => React.ReactNode;
9
+ on_edit: () => void;
10
+ on_delete: () => void;
11
+ }
12
+ export declare function ListItemRow<T extends Record<string, unknown>>({ item, index, columns, render_item, render_item_indicator, on_edit, on_delete, }: ListItemRowProps<T>): import("react/jsx-runtime").JSX.Element;
13
+ export {};
14
+ //# sourceMappingURL=list_item_row.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list_item_row.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/list_item_row.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C,UAAU,gBAAgB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC1D,IAAI,EAAE,CAAC,CAAA;IACP,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAA;IACvB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAA;IACzD,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,KAAK,CAAC,SAAS,CAAA;IACpD,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC7D,IAAI,EACJ,KAAK,EACL,OAAO,EACP,WAAW,EACX,qBAAqB,EACrB,OAAO,EACP,SAAS,GACV,EAAE,gBAAgB,CAAC,CAAC,CAAC,2CAgGrB"}
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Pencil, Trash2 } from 'lucide-react';
3
+ import { cn } from '../../../lib/utils.js';
4
+ export function ListItemRow({ item, index, columns, render_item, render_item_indicator, on_edit, on_delete, }) {
5
+ // Custom render overrides everything
6
+ if (render_item) {
7
+ return (_jsxs("div", { className: "cls_list_editor_row flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors", children: [_jsx("div", { className: "flex-1 min-w-0", children: render_item(item, index) }), _jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [_jsx("button", { type: "button", onClick: on_edit, className: "w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors", "aria-label": "Edit item", children: _jsx(Pencil, { className: "w-4 h-4" }) }), _jsx("button", { type: "button", onClick: on_delete, className: "w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors", "aria-label": "Delete item", children: _jsx(Trash2, { className: "w-4 h-4" }) })] })] }));
8
+ }
9
+ // Default rendering based on column definitions
10
+ const primary_cols = columns.filter(c => c.list_display === 'primary');
11
+ const secondary_cols = columns.filter(c => c.list_display === 'secondary');
12
+ const badge_cols = columns.filter(c => c.list_display === 'badge');
13
+ return (_jsxs("div", { className: "cls_list_editor_row flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors", children: [render_item_indicator && (_jsx("div", { className: "shrink-0", children: render_item_indicator(item) })), _jsxs("div", { className: "flex-1 min-w-0", children: [primary_cols.map((col) => (_jsx("div", { className: "text-sm font-medium text-gray-900 truncate", children: String(item[col.field] ?? '') }, col.field))), secondary_cols.map((col) => (_jsx("div", { className: "text-xs text-gray-500 truncate mt-0.5", children: String(item[col.field] ?? '') }, col.field)))] }), badge_cols.length > 0 && (_jsx("div", { className: "flex items-center gap-2 shrink-0", children: badge_cols.map((col) => (_jsx("span", { className: cn('inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-mono', 'bg-gray-100 text-gray-600'), children: String(item[col.field] ?? '') }, col.field))) })), _jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [_jsx("button", { type: "button", onClick: on_edit, className: "w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors", "aria-label": "Edit item", children: _jsx(Pencil, { className: "w-4 h-4" }) }), _jsx("button", { type: "button", onClick: on_delete, className: "w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors", "aria-label": "Delete item", children: _jsx(Trash2, { className: "w-4 h-4" }) })] })] }));
14
+ }
@@ -0,0 +1,7 @@
1
+ interface SaveStatusIndicatorProps {
2
+ status: 'idle' | 'saving' | 'saved' | 'error';
3
+ className?: string;
4
+ }
5
+ export declare function SaveStatusIndicator({ status, className, }: SaveStatusIndicatorProps): import("react/jsx-runtime").JSX.Element | null;
6
+ export {};
7
+ //# sourceMappingURL=save_status_indicator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"save_status_indicator.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/save_status_indicator.tsx"],"names":[],"mappings":"AAOA,UAAU,wBAAwB;IAChC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAA;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,SAAS,GACV,EAAE,wBAAwB,kDA+C1B"}
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Save status indicator component
3
+ // Shows saving/saved/error states inline
4
+ import { useEffect, useState } from 'react';
5
+ import { Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
6
+ import { cn } from '../../../lib/utils.js';
7
+ export function SaveStatusIndicator({ status, className, }) {
8
+ const [visible, set_visible] = useState(false);
9
+ useEffect(() => {
10
+ if (status === 'saved') {
11
+ set_visible(true);
12
+ const timer = setTimeout(() => set_visible(false), 2000);
13
+ return () => clearTimeout(timer);
14
+ }
15
+ if (status === 'saving' || status === 'error') {
16
+ set_visible(true);
17
+ }
18
+ else {
19
+ set_visible(false);
20
+ }
21
+ }, [status]);
22
+ if (!visible && status === 'idle')
23
+ return null;
24
+ return (_jsxs("div", { className: cn('cls_save_status flex items-center gap-1.5 text-xs transition-opacity duration-300', !visible && 'opacity-0', visible && 'opacity-100', className), children: [status === 'saving' && (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "w-3.5 h-3.5 text-gray-500 animate-spin" }), _jsx("span", { className: "text-gray-500", children: "Saving..." })] })), status === 'saved' && (_jsxs(_Fragment, { children: [_jsx(CheckCircle2, { className: "w-3.5 h-3.5 text-green-600" }), _jsx("span", { className: "text-green-600", children: "Saved" })] })), status === 'error' && (_jsxs(_Fragment, { children: [_jsx(AlertCircle, { className: "w-3.5 h-3.5 text-red-600" }), _jsx("span", { className: "text-red-600", children: "Error saving" })] }))] }));
25
+ }
@@ -0,0 +1,10 @@
1
+ interface SearchBarProps {
2
+ value: string;
3
+ on_change: (value: string) => void;
4
+ item_count: number;
5
+ item_label: string;
6
+ className?: string;
7
+ }
8
+ export declare function SearchBar({ value, on_change, item_count, item_label, className, }: SearchBarProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
10
+ //# sourceMappingURL=search_bar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search_bar.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/search_bar.tsx"],"names":[],"mappings":"AAMA,UAAU,cAAc;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,SAAS,EACT,UAAU,EACV,UAAU,EACV,SAAS,GACV,EAAE,cAAc,2CA6BhB"}