hazo_config 2.1.1 → 2.1.3

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.
package/CHANGE_LOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.3
4
+ - chore: upgrade to Tailwind CSS v4 (`tailwindcss`, `@tailwindcss/postcss`); switch PostCSS pipeline to `@tailwindcss/postcss` and drop `autoprefixer`
5
+ - chore: update `globals.css` to v4 syntax (`@import "tailwindcss"`, `@custom-variant dark`, `@config` reference)
6
+ - chore: bump `peerDependencies.tailwindcss` to `^4.0.0`
7
+ - chore: align canonical workspace dep versions (`clsx ^2.1.1`, `tailwind-merge ^3.5.0`, `@types/node ^20.14.10`, `@types/react ^18.3.3`, `@types/react-dom ^18.3.0`, `postcss ^8.4.49`, `typescript ^5.7.2`, `hazo_connect ^2.4.0`)
8
+ - docs: README and SETUP_CHECKLIST refreshed for Tailwind v4 setup
9
+
3
10
  ## 2.1.1
4
11
  - chore: add `engines` field (`node >= 18.0.0`) and `prepublishOnly` script
5
12
  - chore: widen React peer deps to support React 18 and 19
package/README.md CHANGED
@@ -36,7 +36,7 @@ npm install hazo_config
36
36
  }
37
37
  ```
38
38
 
39
- 3. If using Tailwind v4, add the `@source` directive to `globals.css`:
39
+ 3. Add the `@source` directive to `globals.css` (Tailwind v4 is the supported peer):
40
40
  ```css
41
41
  @import "tailwindcss";
42
42
 
@@ -104,38 +104,39 @@ Add these CSS custom properties to your `globals.css` or main CSS file. These ar
104
104
  }
105
105
  ```
106
106
 
107
- #### Tailwind v3 Setup
107
+ #### Tailwind v4 Setup (Required)
108
108
 
109
- If using Tailwind v3, ensure your `tailwind.config.js` includes:
109
+ This package is built and tested against **Tailwind CSS v4** (declared as the only supported peer). Configure PostCSS to use the v4 plugin and import Tailwind from your CSS:
110
110
 
111
+ `postcss.config.cjs`:
111
112
  ```js
112
113
  module.exports = {
113
- content: [
114
- './app/**/*.{js,ts,jsx,tsx}',
115
- './components/**/*.{js,ts,jsx,tsx}',
116
- ],
117
- // ... rest of config
118
- }
114
+ plugins: {
115
+ "@tailwindcss/postcss": {},
116
+ },
117
+ };
119
118
  ```
120
119
 
121
- #### Tailwind v4 Setup (Critical)
122
-
123
- Tailwind v4 uses JIT compilation and only generates CSS for classes found in scanned files. By default, it only scans your project's files, NOT files in `node_modules/`. This causes all Tailwind classes in this package to have NO CSS generated.
124
-
125
- Add the following to your `globals.css` or main CSS file AFTER the tailwindcss import:
126
-
120
+ `globals.css`:
127
121
  ```css
128
122
  @import "tailwindcss";
129
123
 
130
124
  /* Required: Enable Tailwind to scan hazo_config package classes */
131
125
  @source "../node_modules/hazo_config/dist";
126
+
127
+ /* Required: register the dark variant used by this package */
128
+ @custom-variant dark (&:where(.dark, .dark *));
132
129
  ```
133
130
 
134
- Without this directive, the components will have:
131
+ Without the `@source` directive, the components will have:
135
132
  - Missing hover states (transparent/invisible)
136
133
  - Missing colors, spacing, and typography
137
134
  - Broken layouts
138
135
 
136
+ > **Note:** `@tailwindcss/postcss` ships its own Lightning CSS-based prefixer, so you no longer need `autoprefixer` in your PostCSS pipeline.
137
+
138
+ > **Tailwind v3 consumers:** the `peerDependencies` constraint is `tailwindcss ^4.0.0`. v3 apps can install with `--legacy-peer-deps`, but the package's dist CSS is authored against v4 conventions and is not actively tested on v3.
139
+
139
140
  ### Import Paths (Server/Client Separation)
140
141
 
141
142
  This package provides separate entry points for server and client code to prevent Next.js bundling errors:
@@ -223,6 +224,61 @@ The AppConfig component supports:
223
224
  - Section-based organization
224
225
  - Type conversion between general and JSON
225
226
 
227
+ #### Required Database Schema
228
+
229
+ `AppConfig` and the `useAppConfig` hook expect a single table named `hazo_app_config`. Create it before mounting the component. The file-based `HazoConfig` class does **not** need any database tables — this only applies to the database-backed `AppConfig` flow.
230
+
231
+ **PostgreSQL**
232
+ ```sql
233
+ CREATE TABLE IF NOT EXISTS hazo_app_config (
234
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
235
+ scope_id UUID,
236
+ user_id UUID,
237
+ config_section TEXT NOT NULL,
238
+ config_name TEXT NOT NULL,
239
+ config_value_text TEXT,
240
+ config_value_json JSONB,
241
+ config_type TEXT NOT NULL,
242
+ created_at TIMESTAMPTZ DEFAULT NOW(),
243
+ changed_at TIMESTAMPTZ
244
+ );
245
+
246
+ CREATE INDEX IF NOT EXISTS idx_hazo_app_config_scope ON hazo_app_config(scope_id);
247
+ CREATE INDEX IF NOT EXISTS idx_hazo_app_config_user ON hazo_app_config(user_id);
248
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_hazo_app_config_unique
249
+ ON hazo_app_config(scope_id, user_id, config_section, config_name);
250
+ ```
251
+
252
+ **SQLite**
253
+ ```sql
254
+ CREATE TABLE IF NOT EXISTS hazo_app_config (
255
+ id TEXT PRIMARY KEY,
256
+ scope_id TEXT,
257
+ user_id TEXT,
258
+ config_section TEXT NOT NULL,
259
+ config_name TEXT NOT NULL,
260
+ config_value_text TEXT,
261
+ config_value_json TEXT,
262
+ config_type TEXT NOT NULL,
263
+ created_at TEXT DEFAULT (datetime('now')),
264
+ changed_at TEXT
265
+ );
266
+
267
+ CREATE INDEX IF NOT EXISTS idx_hazo_app_config_scope ON hazo_app_config(scope_id);
268
+ CREATE INDEX IF NOT EXISTS idx_hazo_app_config_user ON hazo_app_config(user_id);
269
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_hazo_app_config_unique
270
+ ON hazo_app_config(scope_id, user_id, config_section, config_name);
271
+ ```
272
+
273
+ Column meanings:
274
+ - `scope_id` — org/tenant id. `NULL` for global rows.
275
+ - `user_id` — per-user override. `NULL` for scope-level rows.
276
+ - `config_section` / `config_name` — the section + key as shown in the UI.
277
+ - `config_type` — `'general'` (use `config_value_text`) or `'json'` (use `config_value_json`).
278
+ - The unique index `(scope_id, user_id, config_section, config_name)` guarantees one row per scope/user/section/name. On PostgreSQL 15+, append `NULLS NOT DISTINCT` if you want `NULL` scope/user values to count as equal for uniqueness.
279
+
280
+ See [SETUP_CHECKLIST.md](./SETUP_CHECKLIST.md#6-database-config-if-using-appconfig) for full details, including a SQLite UUID-default snippet.
281
+
226
282
  ### Example: Generic List Editor (v2.0.2+)
227
283
 
228
284
  ```typescript
@@ -10,16 +10,37 @@ npm install hazo_config
10
10
 
11
11
  ## 2. Tailwind CSS Setup
12
12
 
13
- ### Tailwind v4
14
- Add `@source` directive in your globals.css to ensure Tailwind compiles classes from the package:
13
+ This package supports **Tailwind v4** as its only declared peer (`tailwindcss ^4.0.0`).
14
+
15
+ ### PostCSS plugin
16
+
17
+ Use the v4 PostCSS plugin (it handles vendor prefixing internally — `autoprefixer` is no longer needed):
18
+
19
+ ```js
20
+ // postcss.config.cjs
21
+ module.exports = {
22
+ plugins: {
23
+ "@tailwindcss/postcss": {},
24
+ },
25
+ };
26
+ ```
27
+
28
+ ### globals.css
15
29
 
16
30
  ```css
17
31
  @import "tailwindcss";
32
+
33
+ /* Tailwind v4 only scans your project files by default. This directive
34
+ enables it to compile classes used inside the hazo_config package. */
18
35
  @source "../node_modules/hazo_config/dist";
36
+
37
+ /* Register the dark variant the package's components use. */
38
+ @custom-variant dark (&:where(.dark, .dark *));
19
39
  ```
20
40
 
21
- ### Tailwind v3
22
- Add the dist directory to your `tailwind.config.js` content array:
41
+ ### Tailwind v3 (legacy)
42
+
43
+ The package no longer declares v3 in `peerDependencies`. v3 consumers may install with `--legacy-peer-deps` and add the dist directory to `tailwind.config.js`:
23
44
 
24
45
  ```js
25
46
  module.exports = {
@@ -30,6 +51,8 @@ module.exports = {
30
51
  }
31
52
  ```
32
53
 
54
+ This path is unsupported and not part of the test matrix.
55
+
33
56
  ## 3. CSS Custom Properties (Optional)
34
57
 
35
58
  Add these CSS variables if you want themed styling:
@@ -81,7 +104,22 @@ const config = new HazoConfig({
81
104
 
82
105
  ## 6. Database Config (if using AppConfig)
83
106
 
84
- Create the `hazo_app_config` table in your database:
107
+ The `AppConfig` component reads/writes from a single table named `hazo_app_config`. Create it (and its indexes) in your database before mounting the component.
108
+
109
+ ### Columns
110
+
111
+ | Column | Type / SQLite | Type / PostgreSQL | Notes |
112
+ |---------------------|-----------------------------------|-----------------------------------------|--------------------------------------------------------|
113
+ | `id` | `TEXT PRIMARY KEY` | `UUID PRIMARY KEY DEFAULT gen_random_uuid()` | Row id. SQLite generates a UUID-like string. |
114
+ | `scope_id` | `TEXT` | `UUID` | Org/tenant scope. Nullable for global rows. |
115
+ | `user_id` | `TEXT` | `UUID` | Per-user override. Nullable for scope-level rows. |
116
+ | `config_section` | `TEXT NOT NULL` | `TEXT NOT NULL` | Section grouping (e.g. `database`, `app`). |
117
+ | `config_name` | `TEXT NOT NULL` | `TEXT NOT NULL` | Key within the section. |
118
+ | `config_value_text` | `TEXT` | `TEXT` | Value when `config_type = 'general'`. |
119
+ | `config_value_json` | `TEXT` | `JSONB` | Value when `config_type = 'json'`. |
120
+ | `config_type` | `TEXT NOT NULL` | `TEXT NOT NULL` | One of `'general'` or `'json'`. |
121
+ | `created_at` | `TEXT DEFAULT (datetime('now'))` | `TIMESTAMPTZ DEFAULT NOW()` | Insert timestamp. |
122
+ | `changed_at` | `TEXT` | `TIMESTAMPTZ` | Last-update timestamp (set by the app on update). |
85
123
 
86
124
  ### PostgreSQL
87
125
  ```sql
@@ -91,14 +129,21 @@ CREATE TABLE IF NOT EXISTS hazo_app_config (
91
129
  user_id UUID,
92
130
  config_section TEXT NOT NULL,
93
131
  config_name TEXT NOT NULL,
94
- config_value_text TEXT DEFAULT '',
95
- config_value_json JSONB DEFAULT '{}',
96
- config_type TEXT NOT NULL DEFAULT 'general',
132
+ config_value_text TEXT,
133
+ config_value_json JSONB,
134
+ config_type TEXT NOT NULL,
97
135
  created_at TIMESTAMPTZ DEFAULT NOW(),
98
- changed_at TIMESTAMPTZ DEFAULT NOW()
136
+ changed_at TIMESTAMPTZ
99
137
  );
138
+
139
+ CREATE INDEX IF NOT EXISTS idx_hazo_app_config_scope ON hazo_app_config(scope_id);
140
+ CREATE INDEX IF NOT EXISTS idx_hazo_app_config_user ON hazo_app_config(user_id);
141
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_hazo_app_config_unique
142
+ ON hazo_app_config(scope_id, user_id, config_section, config_name);
100
143
  ```
101
144
 
145
+ > **PostgreSQL NULL note:** by default PostgreSQL treats `NULL` values in a unique index as distinct, so two rows where `user_id IS NULL` (or `scope_id IS NULL`) for the same section/name will *not* conflict. On PostgreSQL 15+ you can add `NULLS NOT DISTINCT` to the unique index if you want the uniqueness check to treat `NULL` as equal.
146
+
102
147
  ### SQLite
103
148
  ```sql
104
149
  CREATE TABLE IF NOT EXISTS hazo_app_config (
@@ -107,14 +152,35 @@ CREATE TABLE IF NOT EXISTS hazo_app_config (
107
152
  user_id TEXT,
108
153
  config_section TEXT NOT NULL,
109
154
  config_name TEXT NOT NULL,
110
- config_value_text TEXT DEFAULT '',
111
- config_value_json TEXT DEFAULT '{}',
112
- config_type TEXT NOT NULL DEFAULT 'general',
155
+ config_value_text TEXT,
156
+ config_value_json TEXT,
157
+ config_type TEXT NOT NULL,
113
158
  created_at TEXT DEFAULT (datetime('now')),
114
- changed_at TEXT DEFAULT (datetime('now'))
159
+ changed_at TEXT
115
160
  );
161
+
162
+ CREATE INDEX IF NOT EXISTS idx_hazo_app_config_scope ON hazo_app_config(scope_id);
163
+ CREATE INDEX IF NOT EXISTS idx_hazo_app_config_user ON hazo_app_config(user_id);
164
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_hazo_app_config_unique
165
+ ON hazo_app_config(scope_id, user_id, config_section, config_name);
116
166
  ```
117
167
 
168
+ > **SQLite id generation:** SQLite has no built-in UUID generator. Either generate the `id` in application code before insert, or define the column with a UUID-like default such as the one used in this package's test-app:
169
+ > ```sql
170
+ > id TEXT PRIMARY KEY DEFAULT (
171
+ > lower(hex(randomblob(4))) || '-' ||
172
+ > lower(hex(randomblob(2))) || '-4' ||
173
+ > substr(lower(hex(randomblob(2))), 2) || '-' ||
174
+ > substr('89ab', abs(random()) % 4 + 1, 1) ||
175
+ > substr(lower(hex(randomblob(2))), 2) || '-' ||
176
+ > lower(hex(randomblob(6)))
177
+ > )
178
+ > ```
179
+
180
+ ### Note: `INI`-based `HazoConfig` does not need any tables
181
+
182
+ The `hazo_app_config` table is **only** required if you use the database-backed `AppConfig` component or the `useAppConfig` hook. The file-based `HazoConfig` class (Section 5) reads and writes an INI file directly and does not touch the database.
183
+
118
184
  ## 7. Next.js Configuration
119
185
 
120
186
  Add to `next.config.js`:
@@ -3,5 +3,5 @@ import type { AppConfigListEditorProps } from './types.js';
3
3
  * AppConfigListEditor - A polished CRUD list editor for config item arrays.
4
4
  * Purely callback-based: no API calls, no database access.
5
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;
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, edit_modal_max_width, }: AppConfigListEditorProps<T>): import("react/jsx-runtime").JSX.Element;
7
7
  //# sourceMappingURL=app_config_list_editor.d.ts.map
@@ -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":"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"}
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,EACpB,oBAAoB,GACrB,EAAE,wBAAwB,CAAC,CAAC,CAAC,2CAoQ7B"}
@@ -15,7 +15,7 @@ import { ImportExportButtons } from './components/import_export_buttons.js';
15
15
  * AppConfigListEditor - A polished CRUD list editor for config item arrays.
16
16
  * Purely callback-based: no API calls, no database access.
17
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', }) {
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', edit_modal_max_width, }) {
19
19
  const [search_term, set_search_term] = useState('');
20
20
  const [edit_state, set_edit_state] = useState(null);
21
21
  const [delete_state, set_delete_state] = useState(null);
@@ -57,6 +57,9 @@ export function AppConfigListEditor({ items, on_items_change, columns, id_field,
57
57
  if (col.type === 'toggle') {
58
58
  empty_item[col.field] = false;
59
59
  }
60
+ if (col.type === 'tag_picker') {
61
+ empty_item[col.field] = [];
62
+ }
60
63
  }
61
64
  set_edit_state({
62
65
  mode: 'create',
@@ -125,5 +128,5 @@ export function AppConfigListEditor({ items, on_items_change, columns, id_field,
125
128
  }, [delete_state, columns, id_field]);
126
129
  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
130
  ? '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 }))] }));
131
+ : '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, max_width_class: edit_modal_max_width })), 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
132
  }
@@ -13,7 +13,9 @@ interface EditModalProps<T extends Record<string, unknown>> {
13
13
  render_preview?: (item: T) => React.ReactNode;
14
14
  on_save: (item: T) => void;
15
15
  on_cancel: () => void;
16
+ /** Tailwind max-width class(es). Default: `sm:max-w-2xl`. */
17
+ max_width_class?: string;
16
18
  }
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;
19
+ 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, max_width_class, }: EditModalProps<T>): import("react/jsx-runtime").JSX.Element;
18
20
  export {};
19
21
  //# sourceMappingURL=edit_modal.d.ts.map
@@ -1 +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"}
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;IACrB,6DAA6D;IAC7D,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;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,GACjC,EAAE,cAAc,CAAC,CAAC,CAAC,2CAqRnB"}
@@ -1,14 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // Edit modal component
3
3
  // Centered dialog for creating/editing items with dynamic form fields
4
- import { useState, useCallback, useEffect } from 'react';
4
+ import React, { useState, useCallback, useEffect } from 'react';
5
5
  import { cn } from '../../../lib/utils.js';
6
6
  import { Input } from '../../ui/input.js';
7
7
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '../../ui/dialog.js';
8
8
  import { Button } from '../../ui/button.js';
9
9
  import { ColorSwatchPicker } from './color_swatch_picker.js';
10
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, }) {
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, max_width_class = 'sm:max-w-2xl', }) {
12
12
  const [form_data, set_form_data] = useState({});
13
13
  const [errors, set_errors] = useState(new Map());
14
14
  const [user_touched_id, set_user_touched_id] = useState(false);
@@ -88,10 +88,46 @@ export function EditModal({ open, mode, item: initial_item, columns, id_field, a
88
88
  const error = errors.get(col.field);
89
89
  const is_id_field = col.field === id_field;
90
90
  const is_disabled = is_id_field && mode === 'edit' && !id_editable_after_create;
91
+ // Tag picker fields — render with dedicated component
92
+ if (col.type === 'tag_picker') {
93
+ 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: "*" })] }), _jsx(TagPickerField, { value: Array.isArray(value) ? value : (typeof value === 'string' && value ? value.split(',').map(s => s.trim()).filter(Boolean) : []), options: col.tag_options ?? [], on_change: (tags) => update_field(col.field, tags) }), error && _jsx("p", { className: "text-xs text-red-600", children: error })] }, col.field));
94
+ }
95
+ // Toggle fields render as horizontal row: label left, switch right
96
+ if (col.type === 'toggle') {
97
+ return (_jsxs("div", { className: "cls_edit_field flex items-center justify-between py-2 border-b border-gray-100 last:border-b-0", children: [_jsx("label", { className: "text-sm font-medium text-gray-700", children: col.label }), _jsx("button", { type: "button", role: "switch", "aria-checked": Boolean(value), 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'), 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') }) })] }, col.field));
98
+ }
91
99
  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
100
  ? 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));
101
+ : 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) })), 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
102
  };
95
103
  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' })] })] }) }));
104
+ 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, className: "rounded-full bg-violet-600 hover:bg-violet-700 text-white", children: mode === 'create' ? `Add ${item_type_label}` : 'Save Changes' })] })] }) }));
105
+ }
106
+ /* ── Tag Picker Field ── */
107
+ function TagPickerField({ value, options, on_change }) {
108
+ const [search, set_search] = React.useState('');
109
+ const [open, set_open] = React.useState(false);
110
+ const ref = React.useRef(null);
111
+ // Close on outside click
112
+ React.useEffect(() => {
113
+ if (!open)
114
+ return;
115
+ const handler = (e) => {
116
+ if (ref.current && !ref.current.contains(e.target))
117
+ set_open(false);
118
+ };
119
+ document.addEventListener('mousedown', handler);
120
+ return () => document.removeEventListener('mousedown', handler);
121
+ }, [open]);
122
+ const selected = new Set(value);
123
+ const filtered = options.filter(o => !selected.has(o.value) && o.label.toLowerCase().includes(search.toLowerCase()));
124
+ const add_tag = (tag_value) => {
125
+ on_change([...value, tag_value]);
126
+ set_search('');
127
+ };
128
+ const remove_tag = (tag_value) => {
129
+ on_change(value.filter(v => v !== tag_value));
130
+ };
131
+ const label_for = (v) => options.find(o => o.value === v)?.label ?? v;
132
+ 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" }))] }));
97
133
  }
@@ -8,7 +8,7 @@ export interface ColumnDef<T> {
8
8
  /** Display label for the form */
9
9
  label: string;
10
10
  /** Column type determines the input control */
11
- type: 'text' | 'textarea' | 'color_swatch' | 'select' | 'number' | 'toggle';
11
+ type: 'text' | 'textarea' | 'color_swatch' | 'select' | 'number' | 'toggle' | 'tag_picker';
12
12
  /** Placeholder text */
13
13
  placeholder?: string;
14
14
  /** Whether this field is required */
@@ -22,6 +22,11 @@ export interface ColumnDef<T> {
22
22
  value: string;
23
23
  label: string;
24
24
  }[];
25
+ /** Options for 'tag_picker' type — value is stored, label is displayed */
26
+ tag_options?: {
27
+ value: string;
28
+ label: string;
29
+ }[];
25
30
  /** Options for 'color_swatch' type */
26
31
  color_options?: string[];
27
32
  /** Validation function - returns error message or null */
@@ -67,6 +72,11 @@ export interface AppConfigListEditorProps<T extends Record<string, unknown>> {
67
72
  className?: string;
68
73
  /** Save status indicator */
69
74
  save_status?: 'idle' | 'saving' | 'saved' | 'error';
75
+ /**
76
+ * Tailwind class(es) that control the edit modal's max width. Overrides the
77
+ * default `sm:max-w-2xl`. Use e.g. `sm:max-w-4xl` or `sm:max-w-[900px]`.
78
+ */
79
+ edit_modal_max_width?: string;
70
80
  }
71
81
  /**
72
82
  * Internal state for the edit modal
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/app_config_list_editor/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/app_config_list_editor/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"json_import_export.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/utils/json_import_export.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE1D,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,CAAA;AAEvC;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpE,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,EACvB,QAAQ,EAAE,MAAM,CAAC,GAAG,MAAM,GACzB;IAAE,WAAW,EAAE,CAAC,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAwDxC;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,QAAQ,EAAE,CAAC,EAAE,EACb,QAAQ,EAAE,CAAC,EAAE,EACb,QAAQ,EAAE,MAAM,CAAC,GAAG,MAAM,EAC1B,SAAS,CAAC,EAAE,MAAM,GACjB,YAAY,CAAC,CAAC,CAAC,CA0BjB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAW1E;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ/D"}
1
+ {"version":3,"file":"json_import_export.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/utils/json_import_export.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE1D,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,CAAA;AAEvC;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpE,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,EACvB,QAAQ,EAAE,MAAM,CAAC,GAAG,MAAM,GACzB;IAAE,WAAW,EAAE,CAAC,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CA2DxC;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,QAAQ,EAAE,CAAC,EAAE,EACb,QAAQ,EAAE,CAAC,EAAE,EACb,QAAQ,EAAE,MAAM,CAAC,GAAG,MAAM,EAC1B,SAAS,CAAC,EAAE,MAAM,GACjB,YAAY,CAAC,CAAC,CAAC,CA0BjB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAW1E;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ/D"}
@@ -41,6 +41,9 @@ export function validate_import_data(data, columns, id_field) {
41
41
  if (col.type === 'toggle' && typeof val !== 'boolean') {
42
42
  item_errors.push(`"${col.field}" should be a boolean`);
43
43
  }
44
+ if (col.type === 'tag_picker' && !Array.isArray(val)) {
45
+ item_errors.push(`"${col.field}" should be an array`);
46
+ }
44
47
  }
45
48
  if (item_errors.length > 0) {
46
49
  errors.push(`Item ${i + 1}: ${item_errors.join(', ')}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_config",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "Config wrapper with error handling",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -65,11 +65,11 @@
65
65
  "@radix-ui/react-dialog": "^1.1.15",
66
66
  "@radix-ui/react-slot": "^1.2.4",
67
67
  "class-variance-authority": "^0.7.0",
68
- "clsx": "^2.1.0",
68
+ "clsx": "^2.1.1",
69
69
  "ini": "^4.1.0",
70
70
  "lucide-react": "^0.553.0",
71
71
  "server-only": "^0.0.1",
72
- "tailwind-merge": "^2.2.0"
72
+ "tailwind-merge": "^3.5.0"
73
73
  },
74
74
  "devDependencies": {
75
75
  "@storybook/addon-essentials": "^8.0.0",
@@ -81,26 +81,26 @@
81
81
  "@storybook/test": "^8.0.0",
82
82
  "@testing-library/react": "^14.3.1",
83
83
  "@types/ini": "^4.1.0",
84
- "@types/node": "^20.11.0",
85
- "@types/react": "^18.2.0",
86
- "@types/react-dom": "^18.2.0",
84
+ "@types/node": "^20.14.10",
85
+ "@types/react": "^18.3.3",
86
+ "@types/react-dom": "^18.3.0",
87
+ "@tailwindcss/postcss": "^4.2.4",
87
88
  "@vitejs/plugin-react": "^4.2.0",
88
- "autoprefixer": "^10.4.17",
89
- "hazo_connect": "^2.3.3",
89
+ "hazo_connect": "^2.4.0",
90
90
  "jsdom": "^24.1.3",
91
- "postcss": "^8.4.33",
91
+ "postcss": "^8.4.49",
92
92
  "react": "^18.2.0",
93
93
  "react-dom": "^18.2.0",
94
94
  "storybook": "^8.0.0",
95
- "tailwindcss": "^3.4.1",
96
- "typescript": "^5.3.3",
95
+ "tailwindcss": "^4.2.4",
96
+ "typescript": "^5.7.2",
97
97
  "vite": "^6.0.0",
98
98
  "vitest": "^1.6.1"
99
99
  },
100
100
  "peerDependencies": {
101
101
  "react": "^18.0.0 || ^19.0.0",
102
102
  "react-dom": "^18.0.0 || ^19.0.0",
103
- "tailwindcss": "^3.0.0"
103
+ "tailwindcss": "^4.0.0"
104
104
  },
105
105
  "peerDependenciesMeta": {
106
106
  "tailwindcss": {