hazo_config 2.4.3 → 2.6.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 (49) hide show
  1. package/CHANGE_LOG.md +73 -0
  2. package/dist/components/app_config.d.ts.map +1 -1
  3. package/dist/components/app_config.js +14 -13
  4. package/dist/components/app_config_list_editor/app_config_list_editor.d.ts.map +1 -1
  5. package/dist/components/app_config_list_editor/app_config_list_editor.js +8 -4
  6. package/dist/components/app_config_list_editor/components/bulk_edit_modal.d.ts.map +1 -1
  7. package/dist/components/app_config_list_editor/components/bulk_edit_modal.js +5 -4
  8. package/dist/components/app_config_list_editor/components/color_swatch_picker.d.ts.map +1 -1
  9. package/dist/components/app_config_list_editor/components/color_swatch_picker.js +2 -1
  10. package/dist/components/app_config_list_editor/components/delete_dialog.d.ts.map +1 -1
  11. package/dist/components/app_config_list_editor/components/delete_dialog.js +2 -1
  12. package/dist/components/app_config_list_editor/components/edit_modal.d.ts.map +1 -1
  13. package/dist/components/app_config_list_editor/components/edit_modal.js +14 -7
  14. package/dist/components/app_config_list_editor/components/empty_state.d.ts.map +1 -1
  15. package/dist/components/app_config_list_editor/components/empty_state.js +2 -1
  16. package/dist/components/app_config_list_editor/components/import_export_buttons.d.ts.map +1 -1
  17. package/dist/components/app_config_list_editor/components/import_export_buttons.js +8 -7
  18. package/dist/components/app_config_list_editor/components/list_item_row.d.ts.map +1 -1
  19. package/dist/components/app_config_list_editor/components/list_item_row.js +4 -3
  20. package/dist/components/app_config_list_editor/components/save_status_indicator.d.ts.map +1 -1
  21. package/dist/components/app_config_list_editor/components/save_status_indicator.js +2 -1
  22. package/dist/components/app_config_list_editor/components/search_bar.d.ts.map +1 -1
  23. package/dist/components/app_config_list_editor/components/search_bar.js +2 -1
  24. package/dist/components/config_editor.d.ts.map +1 -1
  25. package/dist/components/config_editor.js +5 -4
  26. package/dist/components/config_viewer.d.ts.map +1 -1
  27. package/dist/components/config_viewer.js +3 -2
  28. package/dist/components/ui/button.d.ts +1 -1
  29. package/dist/components/ui/button.d.ts.map +1 -1
  30. package/dist/lib/clone_to_override.d.ts +26 -0
  31. package/dist/lib/clone_to_override.d.ts.map +1 -0
  32. package/dist/lib/clone_to_override.js +60 -0
  33. package/dist/lib/index.d.ts +3 -0
  34. package/dist/lib/index.d.ts.map +1 -1
  35. package/dist/lib/index.js +7 -0
  36. package/dist/lib/override_count.d.ts +14 -0
  37. package/dist/lib/override_count.d.ts.map +1 -0
  38. package/dist/lib/override_count.js +32 -0
  39. package/dist/lib/strings.d.ts +2 -0
  40. package/dist/lib/strings.d.ts.map +1 -0
  41. package/dist/lib/strings.js +18 -0
  42. package/dist/lib/two_tier.d.ts +60 -0
  43. package/dist/lib/two_tier.d.ts.map +1 -0
  44. package/dist/lib/two_tier.js +56 -0
  45. package/dist/locales/en.json +104 -0
  46. package/dist/server-data/index.d.ts +17 -0
  47. package/dist/server-data/index.d.ts.map +1 -0
  48. package/dist/server-data/index.js +16 -0
  49. package/package.json +20 -5
package/CHANGE_LOG.md CHANGED
@@ -1,5 +1,78 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.6.0 — 2026-09-21
4
+
5
+ ### Added
6
+
7
+ Full adoption of the canonical `hazo_core/i18n` bridge (`createPackageT`) for
8
+ this package's own baked-in UI strings — hazo_config previously had no i18n
9
+ bridge at all.
10
+
11
+ - `src/locales/en.json` — a new flat, literal-dotted fallback catalog (102
12
+ keys) covering every `'use client'` component's user-facing text: the
13
+ `ConfigEditor`/`ConfigViewer` screens, `AppConfig` (+ its add-key/add-section
14
+ `KeyDialog`), and the whole `AppConfigListEditor` family (`SearchBar`,
15
+ `EmptyState`, `ImportExportButtons`, `ListItemRow`, `EditModal`,
16
+ `BulkEditModal`, `DeleteDialog`, `ColorSwatchPicker`,
17
+ `SaveStatusIndicator`). Namespaced by component (`config_editor.*`,
18
+ `config_viewer.*`, `app_config.*`, `list_editor.*`, `bulk_edit.*`,
19
+ `edit_modal.*`, `search.*`, `empty_state.*`, `import_export.*`,
20
+ `list_item.*`, `save_status.*`, `color_swatch.*`), with a small `common.*`
21
+ namespace (`cancel`, `delete`, `select_placeholder`, `hide_value`,
22
+ `show_value`) for strings that were byte-identical across components.
23
+ `{{var}}` mustache interpolation used for all dynamic copy (item counts,
24
+ labels, key names).
25
+ - `src/lib/strings.ts` — `export const t = createPackageT('hazo_config', en)`,
26
+ re-exported from `hazo_config/lib`. A plain JSON import (no
27
+ `with { type: 'json' }` attribute) is used since every consuming component
28
+ is client-bundled and this package's tsconfig already has
29
+ `moduleResolution: "bundler"` + `resolveJsonModule: true`.
30
+ - Every hardcoded UI string literal across the components above now routes
31
+ through `t()` — labels, buttons, aria-labels, placeholders, validation
32
+ messages, and toast/status text. Original estimate was ~30 strings; actual
33
+ extraction came to 102 catalog keys (the `AppConfigListEditor` family alone
34
+ accounts for over half).
35
+ - `hazo_ui/test-harness` autotest group `hazo_config_i18n` (new
36
+ `test-app/app/autotest/` route + sidebar entry) — catalog-loads,
37
+ all-sampled-keys-resolve, interpolation, and coverage-threshold cases,
38
+ matching the pattern used by `hazo_ui`'s own i18n autotest scenarios.
39
+ - `hazo_i18n` added as an **optional** peer dependency (`^0.5.0`); `hazo_core`
40
+ peer range bumped `^2.3.0` → `^2.4.0` (first version shipping
41
+ `hazo_core/i18n`).
42
+
43
+ ### Fixed
44
+
45
+ `tsconfig.build.json`'s `moduleResolution` was hardcoded to the classic
46
+ `"node"` resolver, which cannot follow a `package.json` `exports` subpath
47
+ (e.g. `hazo_core/i18n`) — TS2307. Switched to `"bundler"`, matching the
48
+ package's own `tsconfig.json`.
49
+
50
+ ## 2.5.0 — 2026-09-06
51
+
52
+ ### Added
53
+
54
+ Ecosystem-first promotion of taxclientdocs' two-tier (global default +
55
+ scope-level override) config helpers, previously duplicated in
56
+ `lib/config/{two-tier,clone-to-override,override-count}.ts`.
57
+
58
+ - `mergeTwoTier<T>(opts): T[]` (`src/lib/two_tier.ts`, exported from the main
59
+ entry and `hazo_config/lib`) — pure, zero-dep merge of a `globals[]` +
60
+ `overrides[]` row pair (each shaped `{ id, scope_id, enabled, ... }`) into
61
+ the effective list. Override wins by key (via `keyFn`); an `additive` mode
62
+ returns the union of both tiers instead. `TwoTierRow`/`MergeOptions` types
63
+ exported alongside. No database dependency — callers fetch `globals`/
64
+ `overrides` however they like, matching the existing `AppConfig`
65
+ callback-persistence pattern.
66
+ - New `hazo_config/server-data` entry point: `cloneToOverride(table,
67
+ globalRowId, orgScopeId)` / `CloneToOverrideError` and
68
+ `getOverrideCount(table, keyColumn, keyValue)` (`src/lib/clone_to_override.ts`,
69
+ `src/lib/override_count.ts`) — hazo_connect-backed CRUD helpers for cloning
70
+ a global row into a scope-level override and counting existing overrides
71
+ for a key. Kept in a separate entry point (not `/server` or `/lib`) so a
72
+ consumer of the fs-based `HazoConfig` or the pure `mergeTwoTier` is never
73
+ required to install `hazo_connect`. `hazo_connect` added as an **optional**
74
+ peer dependency (`^4.1.0`) accordingly.
75
+
3
76
  ## 2.4.2 — 2026-08-04
4
77
 
5
78
  ### Changed
@@ -1 +1 @@
1
- {"version":3,"file":"app_config.d.ts","sourceRoot":"","sources":["../../src/components/app_config.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAyC,MAAM,OAAO,CAAA;AAK7D,OAAO,KAAK,EAAE,cAAc,EAA6B,MAAM,4BAA4B,CAAA;AA0P3F;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CA6a9C,CAAA"}
1
+ {"version":3,"file":"app_config.d.ts","sourceRoot":"","sources":["../../src/components/app_config.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAyC,MAAM,OAAO,CAAA;AAM7D,OAAO,KAAK,EAAE,cAAc,EAA6B,MAAM,4BAA4B,CAAA;AA0P3F;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CA6a9C,CAAA"}
@@ -7,6 +7,7 @@ import { cn } from 'hazo_ui';
7
7
  import { RefreshCw, Save, Trash2, Eye, EyeOff, Plus, X } from 'lucide-react';
8
8
  import { useAppConfig } from './use_app_config.js';
9
9
  import { is_sensitive_field, mask_value, DEFAULT_SENSITIVE_PATTERNS } from './use_config_sections.js';
10
+ import { t } from '../lib/strings.js';
10
11
  const JsonEditor = ({ value, onChange, onValidationChange, placeholder, className }) => {
11
12
  const [text_value, set_text_value] = useState(() => JSON.stringify(value, null, 2));
12
13
  const [validation_error, set_validation_error] = useState();
@@ -20,7 +21,7 @@ const JsonEditor = ({ value, onChange, onValidationChange, placeholder, classNam
20
21
  onValidationChange?.(true);
21
22
  }
22
23
  catch (err) {
23
- const error_msg = err instanceof Error ? err.message : 'Invalid JSON format';
24
+ const error_msg = err instanceof Error ? err.message : t('app_config.invalid_json');
24
25
  set_validation_error(error_msg);
25
26
  onValidationChange?.(false, error_msg);
26
27
  }
@@ -67,7 +68,7 @@ const KeyDialog = ({ mode, section_name, on_close, on_save }) => {
67
68
  : new_key && new_value && is_json_valid;
68
69
  if (!mode)
69
70
  return null;
70
- return (_jsx("div", { className: "fixed inset-0 bg-black/50 flex items-center justify-center z-50", children: _jsxs("div", { className: "bg-white rounded-lg shadow-xl max-w-md w-full mx-4", children: [_jsxs("div", { className: "flex items-center justify-between p-6 border-b", children: [_jsx("h3", { className: "text-lg font-semibold", children: mode === 'add-section' ? 'Add New Section' : 'Add New Key' }), _jsx("button", { onClick: on_close, className: "text-muted-foreground hover:text-foreground", type: "button", children: _jsx(X, { className: "h-5 w-5" }) })] }), _jsxs("div", { className: "p-6 space-y-4", children: [mode === 'add-section' && (_jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-sm font-medium", children: "Section Name" }), _jsx("input", { type: "text", placeholder: "e.g., integrations, notifications", value: new_section, onChange: (e) => set_new_section(e.target.value), className: "w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary", autoFocus: true })] })), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-sm font-medium", children: "Key Name" }), _jsx("input", { type: "text", placeholder: "e.g., api_key, webhook_url", value: new_key, onChange: (e) => set_new_key(e.target.value), className: "w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary", autoFocus: mode !== 'add-section' })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-sm font-medium", children: "Type" }), _jsxs("div", { className: "flex gap-4", children: [_jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [_jsx("input", { type: "radio", name: "type", value: "general", checked: new_type === 'general', onChange: (e) => set_new_type(e.target.value), className: "w-4 h-4 text-primary" }), _jsx("span", { className: "text-sm", children: "General (Text)" })] }), _jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [_jsx("input", { type: "radio", name: "type", value: "json", checked: new_type === 'json', onChange: (e) => set_new_type(e.target.value), className: "w-4 h-4 text-primary" }), _jsx("span", { className: "text-sm", children: "JSON (Object)" })] })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-sm font-medium", children: "Value" }), new_type === 'json' ? (_jsx(JsonEditor, { value: new_value ? (() => {
71
+ return (_jsx("div", { className: "fixed inset-0 bg-black/50 flex items-center justify-center z-50", children: _jsxs("div", { className: "bg-white rounded-lg shadow-xl max-w-md w-full mx-4", children: [_jsxs("div", { className: "flex items-center justify-between p-6 border-b", children: [_jsx("h3", { className: "text-lg font-semibold", children: mode === 'add-section' ? t('app_config.dialog_title_add_section') : t('app_config.dialog_title_add_key') }), _jsx("button", { onClick: on_close, className: "text-muted-foreground hover:text-foreground", type: "button", children: _jsx(X, { className: "h-5 w-5" }) })] }), _jsxs("div", { className: "p-6 space-y-4", children: [mode === 'add-section' && (_jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-sm font-medium", children: t('app_config.section_name_label') }), _jsx("input", { type: "text", placeholder: t('app_config.section_name_placeholder'), value: new_section, onChange: (e) => set_new_section(e.target.value), className: "w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary", autoFocus: true })] })), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-sm font-medium", children: t('app_config.key_name_label') }), _jsx("input", { type: "text", placeholder: t('app_config.key_name_placeholder'), value: new_key, onChange: (e) => set_new_key(e.target.value), className: "w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary", autoFocus: mode !== 'add-section' })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-sm font-medium", children: t('app_config.type_label') }), _jsxs("div", { className: "flex gap-4", children: [_jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [_jsx("input", { type: "radio", name: "type", value: "general", checked: new_type === 'general', onChange: (e) => set_new_type(e.target.value), className: "w-4 h-4 text-primary" }), _jsx("span", { className: "text-sm", children: t('app_config.type_general') })] }), _jsxs("label", { className: "flex items-center gap-2 cursor-pointer", children: [_jsx("input", { type: "radio", name: "type", value: "json", checked: new_type === 'json', onChange: (e) => set_new_type(e.target.value), className: "w-4 h-4 text-primary" }), _jsx("span", { className: "text-sm", children: t('app_config.type_json') })] })] })] }), _jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-sm font-medium", children: t('app_config.value_label') }), new_type === 'json' ? (_jsx(JsonEditor, { value: new_value ? (() => {
71
72
  try {
72
73
  return JSON.parse(new_value);
73
74
  }
@@ -76,9 +77,9 @@ const KeyDialog = ({ mode, section_name, on_close, on_save }) => {
76
77
  }
77
78
  })() : {}, onChange: (val) => set_new_value(JSON.stringify(val, null, 2)), onValidationChange: (isValid) => {
78
79
  set_is_json_valid(isValid);
79
- }, placeholder: '{"key": "value"}' })) : (_jsx("input", { type: "text", placeholder: "Enter value", value: new_value, onChange: (e) => set_new_value(e.target.value), className: "w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary" }))] })] }), _jsxs("div", { className: "flex items-center justify-end gap-3 p-6 border-t bg-muted/30", children: [_jsx("button", { onClick: on_close, className: "px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground", type: "button", children: "Cancel" }), _jsx("button", { onClick: handle_save, disabled: !can_save || is_saving, className: cn('px-4 py-2 text-sm font-medium rounded-md', can_save && !is_saving
80
+ }, placeholder: '{"key": "value"}' })) : (_jsx("input", { type: "text", placeholder: t('app_config.value_placeholder'), value: new_value, onChange: (e) => set_new_value(e.target.value), className: "w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary" }))] })] }), _jsxs("div", { className: "flex items-center justify-end gap-3 p-6 border-t bg-muted/30", children: [_jsx("button", { onClick: on_close, className: "px-4 py-2 text-sm font-medium text-muted-foreground hover:text-foreground", type: "button", children: t('common.cancel') }), _jsx("button", { onClick: handle_save, disabled: !can_save || is_saving, className: cn('px-4 py-2 text-sm font-medium rounded-md', can_save && !is_saving
80
81
  ? 'bg-primary text-primary-foreground hover:bg-primary/90'
81
- : 'bg-muted text-muted-foreground cursor-not-allowed'), type: "button", children: is_saving ? 'Saving...' : mode === 'add-section' ? 'Create Section' : 'Add Key' })] })] }) }));
82
+ : 'bg-muted text-muted-foreground cursor-not-allowed'), type: "button", children: is_saving ? t('app_config.saving') : mode === 'add-section' ? t('app_config.create_section') : t('app_config.add_key') })] })] }) }));
82
83
  };
83
84
  /**
84
85
  * AppConfig component
@@ -87,7 +88,7 @@ const KeyDialog = ({ mode, section_name, on_close, on_save }) => {
87
88
  * @param props - Component props
88
89
  * @returns React component
89
90
  */
90
- export const AppConfig = ({ context, fetch_config, save_config, delete_config, className, on_update, on_error, sensitive_fields, sensitive_patterns = DEFAULT_SENSITIVE_PATTERNS, disable_auto_mask = false, title = 'Configuration', }) => {
91
+ export const AppConfig = ({ context, fetch_config, save_config, delete_config, className, on_update, on_error, sensitive_fields, sensitive_patterns = DEFAULT_SENSITIVE_PATTERNS, disable_auto_mask = false, title = t('app_config.title_default'), }) => {
91
92
  const { sections, is_loading, error, reload, set_value, delete_value, } = useAppConfig(context, fetch_config, save_config, delete_config);
92
93
  const [selected_section, set_selected_section] = useState(null);
93
94
  const [editing_fields, set_editing_fields] = useState(new Map());
@@ -217,7 +218,7 @@ export const AppConfig = ({ context, fetch_config, save_config, delete_config, c
217
218
  * Handle delete key
218
219
  */
219
220
  const handle_delete_key = useCallback(async (section, key) => {
220
- if (!window.confirm(`Delete key "${key}"?`))
221
+ if (!window.confirm(t('app_config.delete_key_confirm', { key })))
221
222
  return;
222
223
  try {
223
224
  await delete_value(section, key);
@@ -270,13 +271,13 @@ export const AppConfig = ({ context, fetch_config, save_config, delete_config, c
270
271
  return selected_section ? sections[selected_section] : null;
271
272
  }, [selected_section, sections]);
272
273
  if (is_loading) {
273
- return (_jsx("div", { className: cn('cls_app_config flex items-center justify-center p-8', className), children: _jsx("div", { className: "text-muted-foreground", children: "Loading configuration..." }) }));
274
+ return (_jsx("div", { className: cn('cls_app_config flex items-center justify-center p-8', className), children: _jsx("div", { className: "text-muted-foreground", children: t('app_config.loading') }) }));
274
275
  }
275
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: cn('cls_app_config space-y-6', className), children: [_jsxs("div", { className: "cls_app_config_header flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-2xl font-bold", children: title }), has_unsaved_changes && (_jsx("p", { className: "text-sm text-amber-600 mt-1", children: "You have unsaved changes" }))] }), _jsxs("div", { className: "flex gap-3", children: [_jsxs("button", { onClick: handle_refresh, className: "px-4 py-2 border border-input rounded-md hover:bg-accent hover:text-accent-foreground flex items-center gap-2 transition-colors", type: "button", disabled: is_saving, children: [_jsx(RefreshCw, { className: "h-4 w-4" }), "Refresh"] }), _jsxs("button", { onClick: handle_save, className: cn('px-4 py-2 rounded-md flex items-center gap-2 transition-colors', has_unsaved_changes
276
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: cn('cls_app_config space-y-6', className), children: [_jsxs("div", { className: "cls_app_config_header flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-2xl font-bold", children: title }), has_unsaved_changes && (_jsx("p", { className: "text-sm text-amber-600 mt-1", children: t('app_config.unsaved_changes') }))] }), _jsxs("div", { className: "flex gap-3", children: [_jsxs("button", { onClick: handle_refresh, className: "px-4 py-2 border border-input rounded-md hover:bg-accent hover:text-accent-foreground flex items-center gap-2 transition-colors", type: "button", disabled: is_saving, children: [_jsx(RefreshCw, { className: "h-4 w-4" }), t('app_config.refresh')] }), _jsxs("button", { onClick: handle_save, className: cn('px-4 py-2 rounded-md flex items-center gap-2 transition-colors', has_unsaved_changes
276
277
  ? 'bg-primary text-primary-foreground hover:bg-primary/90'
277
- : 'bg-muted text-muted-foreground cursor-not-allowed'), type: "button", disabled: !has_unsaved_changes || is_saving, children: [_jsx(Save, { className: "h-4 w-4" }), is_saving ? 'Saving...' : 'Save'] })] })] }), _jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-4 gap-6", children: [_jsx("div", { className: "lg:col-span-1", children: _jsxs("div", { className: "bg-card border border-border rounded-lg overflow-hidden", children: [_jsx("div", { className: "p-4 border-b bg-muted/30", children: _jsxs("div", { className: "flex items-center justify-between mb-3", children: [_jsx("h3", { className: "font-semibold", children: "Sections" }), _jsx("button", { onClick: () => set_dialog_mode('add-section'), className: "text-primary hover:text-primary/80 transition-colors", type: "button", children: _jsx(Plus, { className: "h-5 w-5" }) })] }) }), _jsx("div", { className: "p-2", children: section_list.length === 0 ? (_jsx("div", { className: "text-sm text-muted-foreground text-center py-8 px-4", children: "No sections yet. Click + to add your first section." })) : (_jsx("div", { className: "space-y-1", children: section_list.map((section_name) => (_jsx("button", { onClick: () => set_selected_section(section_name), className: cn('w-full text-left px-3 py-2 rounded-md text-sm font-medium transition-colors', selected_section === section_name
278
+ : 'bg-muted text-muted-foreground cursor-not-allowed'), type: "button", disabled: !has_unsaved_changes || is_saving, children: [_jsx(Save, { className: "h-4 w-4" }), is_saving ? t('app_config.saving') : t('app_config.save')] })] })] }), _jsxs("div", { className: "grid grid-cols-1 lg:grid-cols-4 gap-6", children: [_jsx("div", { className: "lg:col-span-1", children: _jsxs("div", { className: "bg-card border border-border rounded-lg overflow-hidden", children: [_jsx("div", { className: "p-4 border-b bg-muted/30", children: _jsxs("div", { className: "flex items-center justify-between mb-3", children: [_jsx("h3", { className: "font-semibold", children: t('app_config.sections_heading') }), _jsx("button", { onClick: () => set_dialog_mode('add-section'), className: "text-primary hover:text-primary/80 transition-colors", type: "button", children: _jsx(Plus, { className: "h-5 w-5" }) })] }) }), _jsx("div", { className: "p-2", children: section_list.length === 0 ? (_jsx("div", { className: "text-sm text-muted-foreground text-center py-8 px-4", children: t('app_config.no_sections') })) : (_jsx("div", { className: "space-y-1", children: section_list.map((section_name) => (_jsx("button", { onClick: () => set_selected_section(section_name), className: cn('w-full text-left px-3 py-2 rounded-md text-sm font-medium transition-colors', selected_section === section_name
278
279
  ? 'bg-primary text-primary-foreground'
279
- : 'hover:bg-muted'), type: "button", children: section_name }, section_name))) })) })] }) }), _jsx("div", { className: "lg:col-span-3", children: selected_section && current_section_data ? (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h3", { className: "text-xl font-semibold", children: selected_section }), _jsxs("button", { onClick: () => set_dialog_mode('add'), className: "px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 flex items-center gap-2 transition-colors", type: "button", children: [_jsx(Plus, { className: "h-4 w-4" }), "Add Key"] })] }), _jsxs("div", { className: "space-y-3", children: [Object.entries(current_section_data).map(([key, item]) => {
280
+ : 'hover:bg-muted'), type: "button", children: section_name }, section_name))) })) })] }) }), _jsx("div", { className: "lg:col-span-3", children: selected_section && current_section_data ? (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h3", { className: "text-xl font-semibold", children: selected_section }), _jsxs("button", { onClick: () => set_dialog_mode('add'), className: "px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 flex items-center gap-2 transition-colors", type: "button", children: [_jsx(Plus, { className: "h-4 w-4" }), t('app_config.add_key')] })] }), _jsxs("div", { className: "space-y-3", children: [Object.entries(current_section_data).map(([key, item]) => {
280
281
  const is_sensitive = should_mask_field(key);
281
282
  const show_value = !is_sensitive || is_revealed(selected_section, key);
282
283
  const display_value = get_field_value(selected_section, key, item);
@@ -285,7 +286,7 @@ export const AppConfig = ({ context, fetch_config, save_config, delete_config, c
285
286
  : 'bg-gray-100 text-gray-700'), children: item.config_type })] }) }), _jsxs("div", { className: "flex items-center gap-2", children: [is_sensitive && (_jsx("button", { onClick: () => toggle_reveal(selected_section, key), className: "text-muted-foreground hover:text-foreground transition-colors", type: "button", children: show_value ? _jsx(EyeOff, { className: "h-4 w-4" }) : _jsx(Eye, { className: "h-4 w-4" }) })), _jsx("button", { onClick: () => handle_delete_key(selected_section, key), className: "text-red-500 hover:text-red-700 transition-colors", type: "button", children: _jsx(Trash2, { className: "h-4 w-4" }) })] })] }), item.config_type === 'json' ? (_jsx(JsonEditor, { value: typeof display_value === 'object' ? display_value : {}, onChange: (val) => handle_field_change(selected_section, key, val), className: "bg-muted/30" })) : (_jsx("input", { type: is_sensitive && !show_value ? 'password' : 'text', value: is_sensitive && !show_value
286
287
  ? mask_value(String(display_value))
287
288
  : String(display_value), onChange: (e) => handle_field_change(selected_section, key, e.target.value), className: "w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-primary", readOnly: is_sensitive && !show_value }))] }, key));
288
- }), Object.keys(current_section_data).length === 0 && (_jsx("div", { className: "text-center py-12 text-muted-foreground", children: "No keys in this section yet. Click \"Add Key\" to get started." }))] })] })) : (_jsx("div", { className: "flex items-center justify-center h-64 bg-muted/30 border border-dashed border-border rounded-lg", children: _jsx("div", { className: "text-center text-muted-foreground", children: section_list.length > 0
289
- ? 'Select a section to view and edit keys'
290
- : 'Click + to create your first configuration section' }) })) })] })] }), _jsx(KeyDialog, { mode: dialog_mode, section_name: selected_section || undefined, on_close: () => set_dialog_mode(null), on_save: handle_dialog_save })] }));
289
+ }), Object.keys(current_section_data).length === 0 && (_jsx("div", { className: "text-center py-12 text-muted-foreground", children: t('app_config.no_keys') }))] })] })) : (_jsx("div", { className: "flex items-center justify-center h-64 bg-muted/30 border border-dashed border-border rounded-lg", children: _jsx("div", { className: "text-center text-muted-foreground", children: section_list.length > 0
290
+ ? t('app_config.select_section_prompt')
291
+ : t('app_config.create_first_section_prompt') }) })) })] })] }), _jsx(KeyDialog, { mode: dialog_mode, section_name: selected_section || undefined, on_close: () => set_dialog_mode(null), on_save: handle_dialog_save })] }));
291
292
  };
@@ -1 +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":"AAQA,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,EACpB,oBAAoB,EAEpB,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,cAAc,EAEd,cAAc,EACd,cAAc,EACd,cAAc,EACd,SAAS,GACV,EAAE,wBAAwB,CAAC,CAAC,CAAC,+BAuc7B"}
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":"AASA,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,EACpB,oBAAoB,EAEpB,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,cAAc,EAEd,cAAc,EACd,cAAc,EACd,cAAc,EACd,SAAS,GACV,EAAE,wBAAwB,CAAC,CAAC,CAAC,+BA0c7B"}
@@ -5,6 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  import { useState, useCallback, useMemo } from 'react';
6
6
  import { Plus, Trash2 } from 'lucide-react';
7
7
  import { cn, HazoUiConfirmDialog } from 'hazo_ui';
8
+ import { t } from '../../lib/strings.js';
8
9
  import { ListItemRow } from './components/list_item_row.js';
9
10
  import { EditModal } from './components/edit_modal.js';
10
11
  import { SearchBar } from './components/search_bar.js';
@@ -248,9 +249,12 @@ on_item_create, on_item_update, on_item_delete, on_reload, }) {
248
249
  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: [_jsxs("div", { className: "flex items-center gap-2", children: [enable_selection && filtered_items.length > 0 && (_jsx("input", { type: "checkbox", checked: all_filtered_selected, ref: (el) => {
249
250
  if (el)
250
251
  el.indeterminate = !all_filtered_selected && some_filtered_selected;
251
- }, onChange: toggle_select_all, className: "w-4 h-4 rounded border-gray-300 text-violet-600 focus:ring-violet-500", "aria-label": "Select all" })), _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
252
+ }, onChange: toggle_select_all, className: "w-4 h-4 rounded border-gray-300 text-violet-600 focus:ring-violet-500", "aria-label": t('list_editor.select_all_aria') })), _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
252
253
  ? 'bg-gray-100 text-gray-400 cursor-not-allowed'
253
- : '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"] })] })] }), enable_selection && selection_count > 0 && (_jsxs("div", { className: "cls_bulk_action_bar flex items-center gap-3 mb-3 px-4 py-2.5 bg-violet-50 border border-violet-200 rounded-xl", children: [_jsxs("span", { className: "text-sm font-medium text-violet-800", children: [selection_count, " selected"] }), _jsx("button", { type: "button", onClick: clear_selection, className: "text-xs text-violet-600 hover:text-violet-800 underline", children: "Clear" }), _jsx("div", { className: "flex-1" }), bulk_edit_fields && bulk_edit_fields.length > 0 && on_bulk_update && (_jsx("button", { type: "button", onClick: () => set_bulk_edit_open(true), className: "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-full bg-violet-600 hover:bg-violet-700 text-white transition-colors", children: "Edit selected" })), on_bulk_delete && (_jsxs("button", { type: "button", onClick: handle_bulk_delete, className: "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-full bg-red-600 hover:bg-red-700 text-white transition-colors", children: [_jsx(Trash2, { className: "w-3.5 h-3.5" }), "Delete selected"] }))] })), 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), show_checkbox: enable_selection, is_selected: selected_ids.has(String(item[id_field])), on_toggle_select: () => toggle_row(String(item[id_field])) }, 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, max_width_class: edit_modal_max_width, is_saving: is_saving })), delete_state && (_jsx(HazoUiConfirmDialog, { open: true, onOpenChange: (is_open) => { if (!is_open)
254
- handle_cancel_delete(); }, title: `Delete “${delete_item_name}”?`, description: delete_message, variant: "destructive", confirmLabel: "Delete", onConfirm: handle_delete_confirm, onCancel: handle_cancel_delete })), on_bulk_delete && (_jsx(HazoUiConfirmDialog, { open: bulk_delete_open, onOpenChange: (is_open) => { if (!is_open)
255
- set_bulk_delete_open(false); }, title: `Delete ${selection_count} selected ${selection_count === 1 ? item_type_label.toLowerCase() : item_label}?`, description: "This action cannot be undone.", variant: "destructive", confirmLabel: `Delete ${selection_count}`, onConfirm: handle_bulk_delete_confirm, onCancel: () => set_bulk_delete_open(false) })), bulk_edit_fields && bulk_edit_fields.length > 0 && (_jsx(BulkEditModal, { open: bulk_edit_open, selected_count: selection_count, columns: columns, bulk_edit_fields: bulk_edit_fields, on_confirm: handle_bulk_edit_confirm, on_cancel: () => set_bulk_edit_open(false) }))] }));
254
+ : 'bg-violet-600 hover:bg-violet-700 text-white'), title: is_max_reached ? t('list_editor.max_items_title', { max: max_items ?? 0 }) : undefined, children: [_jsx(Plus, { className: "w-4 h-4" }), t('list_editor.add')] })] })] }), enable_selection && selection_count > 0 && (_jsxs("div", { className: "cls_bulk_action_bar flex items-center gap-3 mb-3 px-4 py-2.5 bg-violet-50 border border-violet-200 rounded-xl", children: [_jsx("span", { className: "text-sm font-medium text-violet-800", children: t('list_editor.selected_count', { count: selection_count }) }), _jsx("button", { type: "button", onClick: clear_selection, className: "text-xs text-violet-600 hover:text-violet-800 underline", children: t('list_editor.clear') }), _jsx("div", { className: "flex-1" }), bulk_edit_fields && bulk_edit_fields.length > 0 && on_bulk_update && (_jsx("button", { type: "button", onClick: () => set_bulk_edit_open(true), className: "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-full bg-violet-600 hover:bg-violet-700 text-white transition-colors", children: t('list_editor.edit_selected') })), on_bulk_delete && (_jsxs("button", { type: "button", onClick: handle_bulk_delete, className: "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-full bg-red-600 hover:bg-red-700 text-white transition-colors", children: [_jsx(Trash2, { className: "w-3.5 h-3.5" }), t('list_editor.delete_selected')] }))] })), 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 ? (_jsx("div", { className: "py-8 text-center text-sm text-gray-500", children: t('list_editor.no_items_matching', { item_label, term: search_term }) })) : (_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), show_checkbox: enable_selection, is_selected: selected_ids.has(String(item[id_field])), on_toggle_select: () => toggle_row(String(item[id_field])) }, String(item[id_field])))) })) }), is_max_reached && (_jsx("p", { className: "text-xs text-gray-400 mt-2", children: t('list_editor.max_reached_note', { max: max_items ?? 0, item_label }) })), 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, max_width_class: edit_modal_max_width, is_saving: is_saving })), delete_state && (_jsx(HazoUiConfirmDialog, { open: true, onOpenChange: (is_open) => { if (!is_open)
255
+ handle_cancel_delete(); }, title: t('list_editor.delete_confirm_title', { name: delete_item_name }), description: delete_message, variant: "destructive", confirmLabel: t('common.delete'), onConfirm: handle_delete_confirm, onCancel: handle_cancel_delete })), on_bulk_delete && (_jsx(HazoUiConfirmDialog, { open: bulk_delete_open, onOpenChange: (is_open) => { if (!is_open)
256
+ set_bulk_delete_open(false); }, title: t('list_editor.bulk_delete_title', {
257
+ count: selection_count,
258
+ label: selection_count === 1 ? item_type_label.toLowerCase() : item_label,
259
+ }), description: t('list_editor.bulk_delete_description'), variant: "destructive", confirmLabel: t('list_editor.bulk_delete_confirm_label', { count: selection_count }), onConfirm: handle_bulk_delete_confirm, onCancel: () => set_bulk_delete_open(false) })), bulk_edit_fields && bulk_edit_fields.length > 0 && (_jsx(BulkEditModal, { open: bulk_edit_open, selected_count: selection_count, columns: columns, bulk_edit_fields: bulk_edit_fields, on_confirm: handle_bulk_edit_confirm, on_cancel: () => set_bulk_edit_open(false) }))] }));
256
260
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bulk_edit_modal.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/bulk_edit_modal.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACnE,IAAI,EAAE,OAAO,CAAA;IACb,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAA;IACvB,gBAAgB,EAAE,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,EAAE,CAAA;IACtC,UAAU,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IACvC,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC/D,IAAI,EACJ,cAAc,EACd,OAAO,EACP,gBAAgB,EAChB,UAAU,EACV,SAAS,GACV,EAAE,kBAAkB,CAAC,CAAC,CAAC,+BAuOvB"}
1
+ {"version":3,"file":"bulk_edit_modal.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/bulk_edit_modal.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACnE,IAAI,EAAE,OAAO,CAAA;IACb,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAA;IACvB,gBAAgB,EAAE,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,EAAE,CAAA;IACtC,UAAU,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,CAAA;IACvC,SAAS,EAAE,MAAM,IAAI,CAAA;CACtB;AAED,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAC/D,IAAI,EACJ,cAAc,EACd,OAAO,EACP,gBAAgB,EAChB,UAAU,EACV,SAAS,GACV,EAAE,kBAAkB,CAAC,CAAC,CAAC,+BAuOvB"}
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // Allows editing shared fields across multiple selected items
5
5
  import { useState, useCallback, useEffect } from 'react';
6
6
  import { cn } from 'hazo_ui';
7
+ import { t } from '../../../lib/strings.js';
7
8
  import { Input } from '../../ui/input.js';
8
9
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '../../ui/dialog.js';
9
10
  import { Button } from '../../ui/button.js';
@@ -64,7 +65,7 @@ export function BulkEditModal({ open, selected_count, columns, bulk_edit_fields,
64
65
  return (_jsx("button", { type: "button", role: "switch", "aria-checked": Boolean(value), disabled: !is_enabled, onClick: () => update_field(col.field, !value), className: cn('relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors', value ? 'bg-primary' : 'bg-gray-200', !is_enabled && 'opacity-40 cursor-not-allowed'), children: _jsx("span", { className: cn('pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition-transform', value ? 'translate-x-4' : 'translate-x-0') }) }));
65
66
  }
66
67
  if (col.type === 'select' && col.options) {
67
- return (_jsxs("select", { value: String(value ?? ''), disabled: !is_enabled, 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', !is_enabled && 'opacity-40 cursor-not-allowed bg-gray-50'), children: [_jsx("option", { value: "", children: col.placeholder || 'Select...' }), col.options.map((opt) => (_jsx("option", { value: opt.value, children: opt.label }, opt.value)))] }));
68
+ return (_jsxs("select", { value: String(value ?? ''), disabled: !is_enabled, 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', !is_enabled && 'opacity-40 cursor-not-allowed bg-gray-50'), children: [_jsx("option", { value: "", children: col.placeholder || t('common.select_placeholder') }), col.options.map((opt) => (_jsx("option", { value: opt.value, children: opt.label }, opt.value)))] }));
68
69
  }
69
70
  if (col.type === 'textarea') {
70
71
  return (_jsx("textarea", { value: String(value ?? ''), disabled: !is_enabled, 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', !is_enabled && 'opacity-40 bg-gray-50') }));
@@ -76,8 +77,8 @@ export function BulkEditModal({ open, selected_count, columns, bulk_edit_fields,
76
77
  return (_jsx(Input, { type: "text", value: String(value ?? ''), disabled: !is_enabled, onChange: (e) => update_field(col.field, e.target.value), placeholder: col.placeholder, className: cn('rounded-lg focus-visible:ring-violet-500/20 focus-visible:ring-offset-0', !is_enabled && 'opacity-40 bg-gray-50') }));
77
78
  };
78
79
  return (_jsx(Dialog, { open: open, onOpenChange: (is_open) => { if (!is_open)
79
- on_cancel(); }, children: _jsxs(DialogContent, { className: "cls_bulk_edit_modal w-[95vw] rounded-2xl p-0 gap-0 overflow-hidden max-h-[90vh] flex flex-col sm:max-w-lg", children: [_jsxs(DialogHeader, { className: "px-6 pt-6 pb-4 shrink-0", children: [_jsxs(DialogTitle, { children: ["Edit ", selected_count, " selected items"] }), _jsx("p", { className: "text-sm text-gray-500 mt-1", children: "Enable a field below to apply a new value to all selected items. Disabled fields are left unchanged." })] }), _jsxs("div", { className: "px-6 pb-4 space-y-3 overflow-y-auto flex-1 min-h-0", children: [editable_cols.map((col) => {
80
+ on_cancel(); }, children: _jsxs(DialogContent, { className: "cls_bulk_edit_modal w-[95vw] rounded-2xl p-0 gap-0 overflow-hidden max-h-[90vh] flex flex-col sm:max-w-lg", children: [_jsxs(DialogHeader, { className: "px-6 pt-6 pb-4 shrink-0", children: [_jsx(DialogTitle, { children: t('bulk_edit.title', { count: selected_count }) }), _jsx("p", { className: "text-sm text-gray-500 mt-1", children: t('bulk_edit.description') })] }), _jsxs("div", { className: "px-6 pb-4 space-y-3 overflow-y-auto flex-1 min-h-0", children: [editable_cols.map((col) => {
80
81
  const is_enabled = enabled_fields.has(col.field);
81
- return (_jsxs("div", { className: "cls_bulk_edit_field", children: [_jsxs("div", { className: "flex items-center gap-2 mb-1.5", children: [_jsx("input", { type: "checkbox", id: `bulk_enable_${col.field}`, checked: is_enabled, onChange: () => toggle_field_enabled(col.field), className: "w-4 h-4 rounded border-gray-300 text-violet-600 focus:ring-violet-500" }), _jsxs("label", { htmlFor: `bulk_enable_${col.field}`, className: cn('text-sm font-medium cursor-pointer select-none', is_enabled ? 'text-gray-700' : 'text-gray-400'), children: [col.label, !is_enabled && (_jsx("span", { className: "ml-2 text-xs font-normal text-gray-400", children: "Leave unchanged" }))] })] }), _jsx("div", { className: cn('transition-opacity', !is_enabled && 'opacity-50'), children: col.type === 'toggle' ? (_jsxs("div", { className: "flex items-center justify-between py-1 pl-6", children: [_jsx("span", { className: "text-sm text-gray-500", children: col.label }), render_field_control(col)] })) : (_jsx("div", { className: "pl-6", children: render_field_control(col) })) })] }, col.field));
82
- }), editable_cols.length === 0 && (_jsx("p", { className: "text-sm text-gray-400 py-4 text-center", children: "No editable fields configured." }))] }), _jsxs(DialogFooter, { className: "bg-gray-50 px-6 py-4 border-t border-gray-100 sm:justify-between shrink-0", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: on_cancel, className: "rounded-full", children: "Cancel" }), _jsxs(Button, { type: "button", onClick: handle_confirm, disabled: enabled_fields.size === 0, className: "rounded-full bg-violet-600 hover:bg-violet-700 text-white disabled:opacity-50 disabled:cursor-not-allowed", children: ["Apply to ", selected_count, " items"] })] })] }) }));
82
+ return (_jsxs("div", { className: "cls_bulk_edit_field", children: [_jsxs("div", { className: "flex items-center gap-2 mb-1.5", children: [_jsx("input", { type: "checkbox", id: `bulk_enable_${col.field}`, checked: is_enabled, onChange: () => toggle_field_enabled(col.field), className: "w-4 h-4 rounded border-gray-300 text-violet-600 focus:ring-violet-500" }), _jsxs("label", { htmlFor: `bulk_enable_${col.field}`, className: cn('text-sm font-medium cursor-pointer select-none', is_enabled ? 'text-gray-700' : 'text-gray-400'), children: [col.label, !is_enabled && (_jsx("span", { className: "ml-2 text-xs font-normal text-gray-400", children: t('bulk_edit.leave_unchanged') }))] })] }), _jsx("div", { className: cn('transition-opacity', !is_enabled && 'opacity-50'), children: col.type === 'toggle' ? (_jsxs("div", { className: "flex items-center justify-between py-1 pl-6", children: [_jsx("span", { className: "text-sm text-gray-500", children: col.label }), render_field_control(col)] })) : (_jsx("div", { className: "pl-6", children: render_field_control(col) })) })] }, col.field));
83
+ }), editable_cols.length === 0 && (_jsx("p", { className: "text-sm text-gray-400 py-4 text-center", children: t('bulk_edit.no_fields') }))] }), _jsxs(DialogFooter, { className: "bg-gray-50 px-6 py-4 border-t border-gray-100 sm:justify-between shrink-0", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: on_cancel, className: "rounded-full", children: t('common.cancel') }), _jsx(Button, { type: "button", onClick: handle_confirm, disabled: enabled_fields.size === 0, className: "rounded-full bg-violet-600 hover:bg-violet-700 text-white disabled:opacity-50 disabled:cursor-not-allowed", children: t('bulk_edit.apply', { count: selected_count }) })] })] }) }));
83
84
  }
@@ -1 +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,+BA+BxB"}
1
+ {"version":3,"file":"color_swatch_picker.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/color_swatch_picker.tsx"],"names":[],"mappings":"AAOA,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,+BA+BxB"}
@@ -3,6 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  // Displays a row of circular color swatches for selection
4
4
  import { Check } from 'lucide-react';
5
5
  import { cn } from 'hazo_ui';
6
+ import { t } from '../../../lib/strings.js';
6
7
  export function ColorSwatchPicker({ value, options, on_change, className, }) {
7
8
  return (_jsx("div", { className: cn('cls_color_swatch_picker flex flex-wrap gap-2', className), children: options.map((color) => {
8
9
  const is_selected = value === color;
@@ -10,6 +11,6 @@ export function ColorSwatchPicker({ value, options, on_change, className, }) {
10
11
  const bg_class = color.split(' ').find(c => c.startsWith('bg-')) || 'bg-gray-200';
11
12
  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
13
  ? '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
+ : 'ring-1 ring-gray-200'), "aria-label": t('color_swatch.select_aria', { color }), "aria-pressed": is_selected, children: is_selected && (_jsx(Check, { className: "w-3.5 h-3.5 text-current opacity-80" })) }, color));
14
15
  }) }));
15
16
  }
@@ -1 +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,+BAgCnB"}
1
+ {"version":3,"file":"delete_dialog.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/delete_dialog.tsx"],"names":[],"mappings":"AAgBA,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,+BAgCnB"}
@@ -2,8 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // Delete confirmation dialog component
3
3
  // Uses shadcn AlertDialog with red warning styling
4
4
  import { AlertTriangle } from 'lucide-react';
5
+ import { t } from '../../../lib/strings.js';
5
6
  import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, } from '../../ui/alert-dialog.js';
6
7
  export function DeleteDialog({ open, item_name, message, on_confirm, on_cancel, }) {
7
8
  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
+ 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: t('common.cancel') }), _jsx(AlertDialogAction, { onClick: on_confirm, className: "rounded-full bg-red-600 text-white hover:bg-red-700", children: t('common.delete') })] })] }) }));
9
10
  }
@@ -1 +1 @@
1
- {"version":3,"file":"edit_modal.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/edit_modal.tsx"],"names":[],"mappings":"AAKA,OAAO,KAA2C,MAAM,OAAO,CAAA;AAc/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAQ5C,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;IACrB,6DAA6D;IAC7D,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,sEAAsE;IACtE,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;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,EACT,eAAgC,EAChC,SAAiB,GAClB,EAAE,cAAc,CAAC,CAAC,CAAC,qBAqUnB"}
1
+ {"version":3,"file":"edit_modal.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/edit_modal.tsx"],"names":[],"mappings":"AAKA,OAAO,KAA2C,MAAM,OAAO,CAAA;AAe/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAQ5C,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;IACrB,6DAA6D;IAC7D,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,sEAAsE;IACtE,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;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,EACT,eAAgC,EAChC,SAAiB,GAClB,EAAE,cAAc,CAAC,CAAC,CAAC,qBA2UnB"}
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // Centered dialog for creating/editing items with dynamic form fields
5
5
  import React, { useState, useCallback, useEffect } from 'react';
6
6
  import { cn } from 'hazo_ui';
7
+ import { t } from '../../../lib/strings.js';
7
8
  import { Input } from '../../ui/input.js';
8
9
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '../../ui/dialog.js';
9
10
  import { Button } from '../../ui/button.js';
@@ -56,7 +57,7 @@ export function EditModal({ open, mode, item: initial_item, columns, id_field, a
56
57
  const value = form_data[col.field];
57
58
  // Required check
58
59
  if (col.required && (value === undefined || value === null || value === '')) {
59
- new_errors.set(col.field, `${col.label} is required`);
60
+ new_errors.set(col.field, t('edit_modal.field_required', { label: col.label }));
60
61
  continue;
61
62
  }
62
63
  // Custom validation
@@ -70,7 +71,7 @@ export function EditModal({ open, mode, item: initial_item, columns, id_field, a
70
71
  // Check ID field is filled
71
72
  const id_value = form_data[id_field];
72
73
  if (!id_value || String(id_value).trim() === '') {
73
- new_errors.set(id_field, 'ID is required');
74
+ new_errors.set(id_field, t('edit_modal.id_required'));
74
75
  }
75
76
  // Check for duplicate ID
76
77
  if (id_value) {
@@ -79,7 +80,7 @@ export function EditModal({ open, mode, item: initial_item, columns, id_field, a
79
80
  ? existing_ids.includes(id_str)
80
81
  : existing_ids.filter(id => id !== String(initial_item[id_field])).includes(id_str);
81
82
  if (is_duplicate) {
82
- new_errors.set(id_field, 'This ID already exists');
83
+ new_errors.set(id_field, t('edit_modal.id_duplicate'));
83
84
  }
84
85
  }
85
86
  set_errors(new_errors);
@@ -119,11 +120,17 @@ export function EditModal({ open, mode, item: initial_item, columns, id_field, a
119
120
  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
120
121
  ? handle_id_change(e.target.value)
121
122
  : 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 && ((col.searchable || col.options.length > 10)
122
- ? (_jsx(Combobox, { value: String(value ?? ''), onChange: (v) => update_field(col.field, v), options: [{ value: '', label: col.placeholder || 'Select...' }, ...col.options], placeholder: col.placeholder || 'Select...', error: !!error }))
123
- : (_jsxs(Select, { value: String(value ?? ''), onValueChange: (v) => update_field(col.field, v === SELECT_CLEAR_VALUE ? '' : v), children: [_jsx(SelectTrigger, { className: cn(error && 'border-red-400'), children: _jsx(SelectValue, { placeholder: col.placeholder || 'Select...' }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: SELECT_CLEAR_VALUE, children: col.placeholder || 'Select...' }), col.options.map((opt) => (_jsx(SelectItem, { 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) })), 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));
123
+ ? (_jsx(Combobox, { value: String(value ?? ''), onChange: (v) => update_field(col.field, v), options: [{ value: '', label: col.placeholder || t('common.select_placeholder') }, ...col.options], placeholder: col.placeholder || t('common.select_placeholder'), error: !!error }))
124
+ : (_jsxs(Select, { value: String(value ?? ''), onValueChange: (v) => update_field(col.field, v === SELECT_CLEAR_VALUE ? '' : v), children: [_jsx(SelectTrigger, { className: cn(error && 'border-red-400'), children: _jsx(SelectValue, { placeholder: col.placeholder || t('common.select_placeholder') }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: SELECT_CLEAR_VALUE, children: col.placeholder || t('common.select_placeholder') }), col.options.map((opt) => (_jsx(SelectItem, { 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) })), is_id_field && auto_id_from && mode === 'create' && !user_touched_id && (_jsx("p", { className: "text-xs text-gray-400", children: t('edit_modal.auto_generated_hint', { source: 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));
124
125
  };
125
126
  return (_jsx(Dialog, { open: open, onOpenChange: (is_open) => { if (!is_open)
126
- on_cancel(); }, children: _jsxs(DialogContent, { className: cn('cls_edit_modal w-[95vw] rounded-2xl p-0 gap-0 overflow-hidden max-h-[90vh] flex flex-col', max_width_class), children: [_jsx(DialogHeader, { className: "px-6 pt-6 pb-4 shrink-0", children: _jsx(DialogTitle, { children: mode === 'create' ? `New ${item_type_label}` : `Edit ${item_type_label}` }) }), _jsxs("div", { className: "px-6 pb-4 space-y-4 overflow-y-auto flex-1 min-h-0", 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 shrink-0", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: on_cancel, className: "rounded-full", children: "Cancel" }), _jsx(Button, { type: "button", onClick: handle_save, disabled: is_saving, className: "rounded-full bg-violet-600 hover:bg-violet-700 text-white disabled:opacity-50 disabled:cursor-not-allowed", children: is_saving ? 'Saving…' : mode === 'create' ? `Add ${item_type_label}` : 'Save Changes' })] })] }) }));
127
+ on_cancel(); }, children: _jsxs(DialogContent, { className: cn('cls_edit_modal w-[95vw] rounded-2xl p-0 gap-0 overflow-hidden max-h-[90vh] flex flex-col', max_width_class), children: [_jsx(DialogHeader, { className: "px-6 pt-6 pb-4 shrink-0", children: _jsx(DialogTitle, { children: mode === 'create'
128
+ ? t('edit_modal.title_new', { item_type: item_type_label })
129
+ : t('edit_modal.title_edit', { item_type: item_type_label }) }) }), _jsxs("div", { className: "px-6 pb-4 space-y-4 overflow-y-auto flex-1 min-h-0", 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 shrink-0", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: on_cancel, className: "rounded-full", children: t('common.cancel') }), _jsx(Button, { type: "button", onClick: handle_save, disabled: is_saving, className: "rounded-full bg-violet-600 hover:bg-violet-700 text-white disabled:opacity-50 disabled:cursor-not-allowed", children: is_saving
130
+ ? t('edit_modal.saving')
131
+ : mode === 'create'
132
+ ? t('edit_modal.add_button', { item_type: item_type_label })
133
+ : t('edit_modal.save_changes') })] })] }) }));
127
134
  }
128
135
  /* ── Tag Picker Field ── */
129
136
  function TagPickerField({ value, options, on_change }) {
@@ -151,5 +158,5 @@ function TagPickerField({ value, options, on_change }) {
151
158
  on_change(value.filter(v => v !== tag_value));
152
159
  };
153
160
  const label_for = (v) => options.find(o => o.value === v)?.label ?? v;
154
- return (_jsxs("div", { ref: ref, className: "relative", children: [_jsxs("div", { className: cn('flex flex-wrap gap-1.5 min-h-[38px] p-1.5 border rounded-lg bg-white cursor-text', open && 'ring-2 ring-violet-500/20 border-violet-400'), onClick: () => set_open(true), children: [value.map(v => (_jsxs("span", { className: "inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-violet-100 text-violet-800 text-xs font-medium", children: [label_for(v), _jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); remove_tag(v); }, className: "text-violet-500 hover:text-violet-800 ml-0.5", children: "\u00D7" })] }, v))), _jsx("input", { type: "text", value: search, onChange: e => { set_search(e.target.value); set_open(true); }, onFocus: () => set_open(true), placeholder: value.length === 0 ? 'Type to search...' : '', className: "flex-1 min-w-[80px] text-sm outline-none bg-transparent px-1 py-0.5" })] }), open && filtered.length > 0 && (_jsx("div", { className: "absolute z-50 mt-1 w-full max-h-48 overflow-y-auto rounded-lg border bg-white shadow-lg", children: filtered.map(opt => (_jsxs("button", { type: "button", onClick: () => add_tag(opt.value), className: "w-full text-left px-3 py-2 text-sm hover:bg-violet-50 transition-colors flex items-center justify-between", children: [_jsx("span", { children: opt.label }), _jsx("span", { className: "text-xs text-gray-400 font-mono", children: opt.value })] }, opt.value))) })), open && filtered.length === 0 && search && (_jsx("div", { className: "absolute z-50 mt-1 w-full rounded-lg border bg-white shadow-lg px-3 py-2 text-sm text-gray-400", children: "No matching options" }))] }));
161
+ return (_jsxs("div", { ref: ref, className: "relative", children: [_jsxs("div", { className: cn('flex flex-wrap gap-1.5 min-h-[38px] p-1.5 border rounded-lg bg-white cursor-text', open && 'ring-2 ring-violet-500/20 border-violet-400'), onClick: () => set_open(true), children: [value.map(v => (_jsxs("span", { className: "inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-violet-100 text-violet-800 text-xs font-medium", children: [label_for(v), _jsx("button", { type: "button", onClick: (e) => { e.stopPropagation(); remove_tag(v); }, className: "text-violet-500 hover:text-violet-800 ml-0.5", children: "\u00D7" })] }, v))), _jsx("input", { type: "text", value: search, onChange: e => { set_search(e.target.value); set_open(true); }, onFocus: () => set_open(true), placeholder: value.length === 0 ? t('edit_modal.tag_search_placeholder') : '', className: "flex-1 min-w-[80px] text-sm outline-none bg-transparent px-1 py-0.5" })] }), open && filtered.length > 0 && (_jsx("div", { className: "absolute z-50 mt-1 w-full max-h-48 overflow-y-auto rounded-lg border bg-white shadow-lg", children: filtered.map(opt => (_jsxs("button", { type: "button", onClick: () => add_tag(opt.value), className: "w-full text-left px-3 py-2 text-sm hover:bg-violet-50 transition-colors flex items-center justify-between", children: [_jsx("span", { children: opt.label }), _jsx("span", { className: "text-xs text-gray-400 font-mono", children: opt.value })] }, opt.value))) })), open && filtered.length === 0 && search && (_jsx("div", { className: "absolute z-50 mt-1 w-full rounded-lg border bg-white shadow-lg px-3 py-2 text-sm text-gray-400", children: t('edit_modal.no_matching_options') }))] }));
155
162
  }
@@ -1 +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,+BAsBjB"}
1
+ {"version":3,"file":"empty_state.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/empty_state.tsx"],"names":[],"mappings":"AAOA,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,+BAsBjB"}
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // Displayed when the list has no items
4
4
  import { Inbox, Plus } from 'lucide-react';
5
5
  import { cn } from 'hazo_ui';
6
+ import { t } from '../../../lib/strings.js';
6
7
  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
+ 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" }) }), _jsx("p", { className: "text-sm font-medium text-gray-900 mb-1", children: t('empty_state.title', { item_label }) }), _jsx("p", { className: "text-sm text-gray-500 mb-4", children: t('empty_state.description', { item: 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" }), t('empty_state.cta', { item: item_label.replace(/s$/, '') })] })] }));
8
9
  }
@@ -1 +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":"AAQA,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,+BA0H7B"}
1
+ {"version":3,"file":"import_export_buttons.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/import_export_buttons.tsx"],"names":[],"mappings":"AASA,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,+BA0H7B"}
@@ -5,6 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  import { useRef, useState, useCallback, useEffect } from 'react';
6
6
  import { Download, Upload } from 'lucide-react';
7
7
  import { cn } from 'hazo_ui';
8
+ import { t } from '../../../lib/strings.js';
8
9
  import { validate_import_data, merge_items, export_items_to_json, generate_export_filename, } from '../utils/json_import_export.js';
9
10
  export function ImportExportButtons({ items, on_items_change, columns, id_field, title, max_items, }) {
10
11
  const file_input_ref = useRef(null);
@@ -35,7 +36,7 @@ export function ImportExportButtons({ items, on_items_change, columns, id_field,
35
36
  reader.onload = (e) => {
36
37
  const text = e.target?.result;
37
38
  if (typeof text !== 'string') {
38
- set_status_message({ text: 'Failed to read file', is_error: true });
39
+ set_status_message({ text: t('import_export.error_read_file'), is_error: true });
39
40
  return;
40
41
  }
41
42
  let data;
@@ -43,12 +44,12 @@ export function ImportExportButtons({ items, on_items_change, columns, id_field,
43
44
  data = JSON.parse(text);
44
45
  }
45
46
  catch {
46
- set_status_message({ text: 'Invalid JSON file', is_error: true });
47
+ set_status_message({ text: t('import_export.error_invalid_json'), is_error: true });
47
48
  return;
48
49
  }
49
50
  const { valid_items, errors } = validate_import_data(data, columns, id_field);
50
51
  if (valid_items.length === 0) {
51
- const msg = errors.length > 0 ? errors[0] : 'No valid items found';
52
+ const msg = errors.length > 0 ? errors[0] : t('import_export.error_no_valid_items');
52
53
  set_status_message({ text: msg, is_error: true });
53
54
  return;
54
55
  }
@@ -56,13 +57,13 @@ export function ImportExportButtons({ items, on_items_change, columns, id_field,
56
57
  on_items_change(result.merged_items);
57
58
  const parts = [];
58
59
  if (result.added_count > 0) {
59
- parts.push(`Added ${result.added_count}`);
60
+ parts.push(t('import_export.added', { count: result.added_count }));
60
61
  }
61
62
  if (result.skipped_count > 0) {
62
- parts.push(`${result.skipped_count} skipped`);
63
+ parts.push(t('import_export.skipped', { count: result.skipped_count }));
63
64
  }
64
65
  if (errors.length > 0) {
65
- parts.push(`${errors.length} invalid`);
66
+ parts.push(t('import_export.invalid', { count: errors.length }));
66
67
  }
67
68
  set_status_message({
68
69
  text: parts.join(', '),
@@ -71,5 +72,5 @@ export function ImportExportButtons({ items, on_items_change, columns, id_field,
71
72
  };
72
73
  reader.readAsText(file);
73
74
  }, [items, columns, id_field, max_items, on_items_change]);
74
- 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" })] }));
75
+ 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: t('import_export.export_title'), children: [_jsx(Download, { className: "w-3.5 h-3.5" }), t('import_export.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: t('import_export.import_title'), children: [_jsx(Upload, { className: "w-3.5 h-3.5" }), t('import_export.import')] }), _jsx("input", { ref: file_input_ref, type: "file", accept: ".json", onChange: handle_file_selected, className: "hidden" })] }));
75
76
  }
@@ -1 +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;IACrB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAA;CAC9B;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,EACT,aAAa,EACb,WAAW,EACX,gBAAgB,GACjB,EAAE,gBAAgB,CAAC,CAAC,CAAC,qBA0HrB"}
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;AAIzB,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;IACrB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAA;CAC9B;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,EACT,aAAa,EACb,WAAW,EACX,gBAAgB,GACjB,EAAE,gBAAgB,CAAC,CAAC,CAAC,qBA0HrB"}