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
@@ -1,19 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Pencil, Trash2 } from 'lucide-react';
3
3
  import { cn } from 'hazo_ui';
4
+ import { t } from '../../../lib/strings.js';
4
5
  export function ListItemRow({ item, index, columns, render_item, render_item_indicator, on_edit, on_delete, show_checkbox, is_selected, on_toggle_select, }) {
5
6
  // Custom render overrides everything
6
7
  if (render_item) {
7
- return (_jsxs("div", { className: "cls_list_editor_row flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors", children: [show_checkbox && (_jsx("input", { type: "checkbox", checked: is_selected ?? false, onChange: on_toggle_select, className: "shrink-0 w-4 h-4 rounded border-gray-300 text-violet-600 focus:ring-violet-500", "aria-label": "Select item" })), _jsx("div", { className: "flex-1 min-w-0", children: render_item(item, index) }), _jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [_jsx("button", { type: "button", onClick: on_edit, className: "w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors", "aria-label": "Edit item", children: _jsx(Pencil, { className: "w-4 h-4" }) }), _jsx("button", { type: "button", onClick: on_delete, className: "w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors", "aria-label": "Delete item", children: _jsx(Trash2, { className: "w-4 h-4" }) })] })] }));
8
+ return (_jsxs("div", { className: "cls_list_editor_row flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors", children: [show_checkbox && (_jsx("input", { type: "checkbox", checked: is_selected ?? false, onChange: on_toggle_select, className: "shrink-0 w-4 h-4 rounded border-gray-300 text-violet-600 focus:ring-violet-500", "aria-label": t('list_item.select_item_aria') })), _jsx("div", { className: "flex-1 min-w-0", children: render_item(item, index) }), _jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [_jsx("button", { type: "button", onClick: on_edit, className: "w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors", "aria-label": t('list_item.edit_item_aria'), children: _jsx(Pencil, { className: "w-4 h-4" }) }), _jsx("button", { type: "button", onClick: on_delete, className: "w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors", "aria-label": t('list_item.delete_item_aria'), children: _jsx(Trash2, { className: "w-4 h-4" }) })] })] }));
8
9
  }
9
10
  // Default rendering based on column definitions
10
11
  const primary_cols = columns.filter(c => c.list_display === 'primary');
11
12
  const secondary_cols = columns.filter(c => c.list_display === 'secondary');
12
13
  const badge_cols = columns.filter(c => c.list_display === 'badge');
13
- return (_jsxs("div", { className: "cls_list_editor_row flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors", children: [show_checkbox && (_jsx("input", { type: "checkbox", checked: is_selected ?? false, onChange: on_toggle_select, className: "shrink-0 w-4 h-4 rounded border-gray-300 text-violet-600 focus:ring-violet-500", "aria-label": "Select item" })), render_item_indicator && (_jsx("div", { className: "shrink-0", children: render_item_indicator(item) })), _jsxs("div", { className: "flex-1 min-w-0", children: [primary_cols.map((col) => (_jsx("div", { className: "text-sm font-medium text-gray-900 truncate", children: col.format?.(item[col.field], item) ?? String(item[col.field] ?? '') }, col.field))), secondary_cols.map((col) => (_jsx("div", { className: "text-xs text-gray-500 truncate mt-0.5", children: col.format?.(item[col.field], item) ?? String(item[col.field] ?? '') }, col.field)))] }), badge_cols.length > 0 && (_jsx("div", { className: "flex items-center gap-2 shrink-0", children: badge_cols.map((col) => {
14
+ return (_jsxs("div", { className: "cls_list_editor_row flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors", children: [show_checkbox && (_jsx("input", { type: "checkbox", checked: is_selected ?? false, onChange: on_toggle_select, className: "shrink-0 w-4 h-4 rounded border-gray-300 text-violet-600 focus:ring-violet-500", "aria-label": t('list_item.select_item_aria') })), render_item_indicator && (_jsx("div", { className: "shrink-0", children: render_item_indicator(item) })), _jsxs("div", { className: "flex-1 min-w-0", children: [primary_cols.map((col) => (_jsx("div", { className: "text-sm font-medium text-gray-900 truncate", children: col.format?.(item[col.field], item) ?? String(item[col.field] ?? '') }, col.field))), secondary_cols.map((col) => (_jsx("div", { className: "text-xs text-gray-500 truncate mt-0.5", children: col.format?.(item[col.field], item) ?? String(item[col.field] ?? '') }, col.field)))] }), badge_cols.length > 0 && (_jsx("div", { className: "flex items-center gap-2 shrink-0", children: badge_cols.map((col) => {
14
15
  const badge_class = typeof col.badge_class === 'function'
15
16
  ? col.badge_class(item[col.field], item)
16
17
  : col.badge_class;
17
18
  return (_jsx("span", { className: cn('inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-mono', badge_class || 'bg-gray-100 text-gray-600'), children: col.format?.(item[col.field], item) ?? String(item[col.field] ?? '') }, col.field));
18
- }) })), _jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [_jsx("button", { type: "button", onClick: on_edit, className: "w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors", "aria-label": "Edit item", children: _jsx(Pencil, { className: "w-4 h-4" }) }), _jsx("button", { type: "button", onClick: on_delete, className: "w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors", "aria-label": "Delete item", children: _jsx(Trash2, { className: "w-4 h-4" }) })] })] }));
19
+ }) })), _jsxs("div", { className: "flex items-center gap-1 shrink-0", children: [_jsx("button", { type: "button", onClick: on_edit, className: "w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors", "aria-label": t('list_item.edit_item_aria'), children: _jsx(Pencil, { className: "w-4 h-4" }) }), _jsx("button", { type: "button", onClick: on_delete, className: "w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors", "aria-label": t('list_item.delete_item_aria'), children: _jsx(Trash2, { className: "w-4 h-4" }) })] })] }));
19
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"save_status_indicator.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/save_status_indicator.tsx"],"names":[],"mappings":"AASA,UAAU,wBAAwB;IAChC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAA;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,SAAS,GACV,EAAE,wBAAwB,sCA+C1B"}
1
+ {"version":3,"file":"save_status_indicator.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/save_status_indicator.tsx"],"names":[],"mappings":"AAUA,UAAU,wBAAwB;IAChC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAA;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,SAAS,GACV,EAAE,wBAAwB,sCA+C1B"}
@@ -5,6 +5,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
5
5
  import { useEffect, useState } from 'react';
6
6
  import { Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
7
7
  import { cn } from 'hazo_ui';
8
+ import { t } from '../../../lib/strings.js';
8
9
  export function SaveStatusIndicator({ status, className, }) {
9
10
  const [visible, set_visible] = useState(false);
10
11
  useEffect(() => {
@@ -22,5 +23,5 @@ export function SaveStatusIndicator({ status, className, }) {
22
23
  }, [status]);
23
24
  if (!visible && status === 'idle')
24
25
  return null;
25
- return (_jsxs("div", { className: cn('cls_save_status flex items-center gap-1.5 text-xs transition-opacity duration-300', !visible && 'opacity-0', visible && 'opacity-100', className), children: [status === 'saving' && (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "w-3.5 h-3.5 text-gray-500 animate-spin" }), _jsx("span", { className: "text-gray-500", children: "Saving..." })] })), status === 'saved' && (_jsxs(_Fragment, { children: [_jsx(CheckCircle2, { className: "w-3.5 h-3.5 text-green-600" }), _jsx("span", { className: "text-green-600", children: "Saved" })] })), status === 'error' && (_jsxs(_Fragment, { children: [_jsx(AlertCircle, { className: "w-3.5 h-3.5 text-red-600" }), _jsx("span", { className: "text-red-600", children: "Error saving" })] }))] }));
26
+ return (_jsxs("div", { className: cn('cls_save_status flex items-center gap-1.5 text-xs transition-opacity duration-300', !visible && 'opacity-0', visible && 'opacity-100', className), children: [status === 'saving' && (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "w-3.5 h-3.5 text-gray-500 animate-spin" }), _jsx("span", { className: "text-gray-500", children: t('save_status.saving') })] })), status === 'saved' && (_jsxs(_Fragment, { children: [_jsx(CheckCircle2, { className: "w-3.5 h-3.5 text-green-600" }), _jsx("span", { className: "text-green-600", children: t('save_status.saved') })] })), status === 'error' && (_jsxs(_Fragment, { children: [_jsx(AlertCircle, { className: "w-3.5 h-3.5 text-red-600" }), _jsx("span", { className: "text-red-600", children: t('save_status.error') })] }))] }));
26
27
  }
@@ -1 +1 @@
1
- {"version":3,"file":"search_bar.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/search_bar.tsx"],"names":[],"mappings":"AAMA,UAAU,cAAc;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,SAAS,EACT,UAAU,EACV,UAAU,EACV,SAAS,GACV,EAAE,cAAc,+BA6BhB"}
1
+ {"version":3,"file":"search_bar.d.ts","sourceRoot":"","sources":["../../../../src/components/app_config_list_editor/components/search_bar.tsx"],"names":[],"mappings":"AAOA,UAAU,cAAc;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,SAAS,EACT,UAAU,EACV,UAAU,EACV,SAAS,GACV,EAAE,cAAc,+BA6BhB"}
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // Pill-shaped search input with icon, clear button, and item count
4
4
  import { Search, X } from 'lucide-react';
5
5
  import { cn } from 'hazo_ui';
6
+ import { t } from '../../../lib/strings.js';
6
7
  export function SearchBar({ value, on_change, item_count, item_label, className, }) {
7
- return (_jsxs("div", { className: cn('cls_search_bar flex items-center gap-3', className), children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" }), _jsx("input", { type: "text", value: value, onChange: (e) => on_change(e.target.value), placeholder: `Search ${item_label}...`, className: "w-full pl-9 pr-8 py-2 text-sm rounded-full border border-gray-200 bg-white focus:outline-none focus:ring-2 focus:ring-violet-500/20 focus:border-violet-300 transition-colors", "aria-label": `Search ${item_label}` }), value && (_jsx("button", { type: "button", onClick: () => on_change(''), className: "absolute right-2.5 top-1/2 -translate-y-1/2 w-5 h-5 flex items-center justify-center rounded-full text-gray-400 hover:text-gray-600 hover:bg-gray-100", "aria-label": "Clear search", children: _jsx(X, { className: "w-3.5 h-3.5" }) }))] }), _jsxs("span", { className: "text-sm text-gray-500 whitespace-nowrap", children: [item_count, " ", item_label] })] }));
8
+ return (_jsxs("div", { className: cn('cls_search_bar flex items-center gap-3', className), children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" }), _jsx("input", { type: "text", value: value, onChange: (e) => on_change(e.target.value), placeholder: t('search.placeholder', { item_label }), className: "w-full pl-9 pr-8 py-2 text-sm rounded-full border border-gray-200 bg-white focus:outline-none focus:ring-2 focus:ring-violet-500/20 focus:border-violet-300 transition-colors", "aria-label": t('search.aria_label', { item_label }) }), value && (_jsx("button", { type: "button", onClick: () => on_change(''), className: "absolute right-2.5 top-1/2 -translate-y-1/2 w-5 h-5 flex items-center justify-center rounded-full text-gray-400 hover:text-gray-600 hover:bg-gray-100", "aria-label": t('search.clear_aria_label'), children: _jsx(X, { className: "w-3.5 h-3.5" }) }))] }), _jsxs("span", { className: "text-sm text-gray-500 whitespace-nowrap", children: [item_count, " ", item_label] })] }));
8
9
  }
@@ -1 +1 @@
1
- {"version":3,"file":"config_editor.d.ts","sourceRoot":"","sources":["../../src/components/config_editor.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAyC,MAAM,OAAO,CAAA;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAIrD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,eAAe,EAAE,cAAc,CAAA;IAC/B;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IACjC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC3B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC7B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAWD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAgYpD,CAAA"}
1
+ {"version":3,"file":"config_editor.d.ts","sourceRoot":"","sources":["../../src/components/config_editor.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAyC,MAAM,OAAO,CAAA;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAKrD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,eAAe,EAAE,cAAc,CAAA;IAC/B;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IACjC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC3B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC7B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAWD;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAgYpD,CAAA"}
@@ -6,6 +6,7 @@ import React, { useState, useCallback, useMemo } from 'react';
6
6
  import { cn } from 'hazo_ui';
7
7
  import { RefreshCw, Save, Trash2, Eye, EyeOff } from 'lucide-react';
8
8
  import { useConfigSections, is_sensitive_field, mask_value, DEFAULT_SENSITIVE_PATTERNS } from './use_config_sections.js';
9
+ import { t } from '../lib/strings.js';
9
10
  /**
10
11
  * Config editor component
11
12
  * Provides interface for viewing and editing configuration with refresh and save capabilities
@@ -208,9 +209,9 @@ export const ConfigEditor = ({ config_provider, className, on_update, on_error,
208
209
  const current_section_data = useMemo(() => {
209
210
  return selected_section ? sections[selected_section] : null;
210
211
  }, [selected_section, sections]);
211
- return (_jsxs("div", { className: cn('cls_config_editor space-y-4', className), children: [_jsxs("div", { className: "cls_config_editor_header flex items-center justify-between border-b pb-2", children: [_jsxs("h2", { className: "text-xl font-semibold", children: ["Configuration Editor", has_unsaved_changes && (_jsx("span", { className: "ml-2 text-sm text-amber-600", children: "(unsaved changes)" }))] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("button", { onClick: handle_refresh, className: "px-3 py-1 border rounded hover:bg-muted flex items-center gap-2", "aria-label": "Refresh configuration", type: "button", children: [_jsx(RefreshCw, { size: 16 }), "Refresh"] }), _jsxs("button", { onClick: handle_save, className: cn('px-3 py-1 rounded flex items-center gap-2', has_unsaved_changes
212
+ return (_jsxs("div", { className: cn('cls_config_editor space-y-4', className), children: [_jsxs("div", { className: "cls_config_editor_header flex items-center justify-between border-b pb-2", children: [_jsxs("h2", { className: "text-xl font-semibold", children: [t('config_editor.title'), has_unsaved_changes && (_jsx("span", { className: "ml-2 text-sm text-amber-600", children: t('config_editor.unsaved_suffix') }))] }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("button", { onClick: handle_refresh, className: "px-3 py-1 border rounded hover:bg-muted flex items-center gap-2", "aria-label": t('config_editor.refresh_aria'), type: "button", children: [_jsx(RefreshCw, { size: 16 }), t('config_editor.refresh')] }), _jsxs("button", { onClick: handle_save, className: cn('px-3 py-1 rounded flex items-center gap-2', has_unsaved_changes
212
213
  ? 'bg-primary text-primary-foreground hover:bg-primary/90'
213
- : 'bg-muted text-muted-foreground'), "aria-label": "Save configuration", type: "button", disabled: !has_unsaved_changes, children: [_jsx(Save, { size: 16 }), "Save"] })] })] }), _jsxs("div", { className: "cls_config_editor_content grid grid-cols-1 md:grid-cols-3 gap-4", children: [_jsxs("div", { className: "cls_config_sections_list border rounded p-4", children: [_jsx("h3", { className: "font-semibold mb-2", children: "Sections" }), _jsx("div", { className: "space-y-1", role: "listbox", "aria-label": "Configuration sections", children: section_list.map((section_name) => (_jsx("button", { onClick: () => handle_section_select(section_name), className: cn('w-full text-left px-2 py-1 rounded text-sm', selected_section === section_name
214
+ : 'bg-muted text-muted-foreground'), "aria-label": t('config_editor.save_aria'), type: "button", disabled: !has_unsaved_changes, children: [_jsx(Save, { size: 16 }), t('config_editor.save')] })] })] }), _jsxs("div", { className: "cls_config_editor_content grid grid-cols-1 md:grid-cols-3 gap-4", children: [_jsxs("div", { className: "cls_config_sections_list border rounded p-4", children: [_jsx("h3", { className: "font-semibold mb-2", children: t('config_editor.sections_heading') }), _jsx("div", { className: "space-y-1", role: "listbox", "aria-label": t('config_editor.sections_aria'), children: section_list.map((section_name) => (_jsx("button", { onClick: () => handle_section_select(section_name), className: cn('w-full text-left px-2 py-1 rounded text-sm', selected_section === section_name
214
215
  ? 'bg-primary text-primary-foreground'
215
216
  : 'hover:bg-muted'), role: "option", "aria-selected": selected_section === section_name, type: "button", children: section_name }, section_name))) })] }), _jsx("div", { className: "cls_config_section_content md:col-span-2 border rounded p-4", children: selected_section ? (_jsxs(_Fragment, { children: [_jsx("h3", { className: "font-semibold mb-4", children: selected_section }), _jsxs("div", { className: "space-y-3", children: [current_section_data &&
216
217
  Object.entries(current_section_data).map(([key, value]) => {
@@ -218,6 +219,6 @@ export const ConfigEditor = ({ config_provider, className, on_update, on_error,
218
219
  const field_id = `config-${selected_section}-${key}`;
219
220
  const show_value = !is_sensitive || is_revealed(selected_section, key);
220
221
  const display_value = get_field_value(selected_section, key, value);
221
- return (_jsxs("div", { className: "cls_config_item", children: [_jsx("label", { htmlFor: field_id, className: "block text-sm font-medium mb-1", children: key }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { id: field_id, type: is_sensitive && !show_value ? 'password' : 'text', value: is_sensitive && !show_value ? mask_value(display_value) : display_value, onChange: (e) => handle_field_change(selected_section, key, e.target.value), className: "flex-1 px-2 py-1 border rounded", readOnly: is_sensitive && !show_value }), is_sensitive && (_jsx("button", { onClick: () => toggle_reveal(selected_section, key), className: "text-gray-500 hover:text-gray-700", "aria-label": show_value ? 'Hide value' : 'Show value', type: "button", children: show_value ? _jsx(EyeOff, { size: 18 }) : _jsx(Eye, { size: 18 }) })), _jsx("button", { onClick: () => handle_delete_key(selected_section, key), className: "text-red-500 hover:text-red-700", "aria-label": `Delete ${key}`, type: "button", children: _jsx(Trash2, { size: 18 }) })] })] }, key));
222
- }), _jsxs("div", { className: "cls_add_new_key border-t pt-3 mt-3", children: [_jsx("h4", { className: "text-sm font-medium mb-2", children: "Add New Key" }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "new-key-name", className: "sr-only", children: "Key name" }), _jsx("input", { id: "new-key-name", type: "text", placeholder: "Key name", value: new_key, onChange: handle_new_key_change, className: "w-full px-2 py-1 border rounded text-sm" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "new-key-value", className: "sr-only", children: "Value" }), _jsx("input", { id: "new-key-value", type: "text", placeholder: "Value", value: new_value, onChange: handle_new_value_change, className: "w-full px-2 py-1 border rounded text-sm" })] }), _jsx("button", { onClick: handle_add_key, className: "px-3 py-1 bg-primary text-primary-foreground rounded hover:bg-primary/90 text-sm", type: "button", disabled: !new_key || !new_value, children: "Add Key" })] })] })] })] })) : (_jsx("div", { className: "text-center text-muted-foreground py-8", children: "Select a section to view/edit" })) })] })] }));
222
+ return (_jsxs("div", { className: "cls_config_item", children: [_jsx("label", { htmlFor: field_id, className: "block text-sm font-medium mb-1", children: key }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { id: field_id, type: is_sensitive && !show_value ? 'password' : 'text', value: is_sensitive && !show_value ? mask_value(display_value) : display_value, onChange: (e) => handle_field_change(selected_section, key, e.target.value), className: "flex-1 px-2 py-1 border rounded", readOnly: is_sensitive && !show_value }), is_sensitive && (_jsx("button", { onClick: () => toggle_reveal(selected_section, key), className: "text-gray-500 hover:text-gray-700", "aria-label": show_value ? t('common.hide_value') : t('common.show_value'), type: "button", children: show_value ? _jsx(EyeOff, { size: 18 }) : _jsx(Eye, { size: 18 }) })), _jsx("button", { onClick: () => handle_delete_key(selected_section, key), className: "text-red-500 hover:text-red-700", "aria-label": t('config_editor.delete_key_aria', { key }), type: "button", children: _jsx(Trash2, { size: 18 }) })] })] }, key));
223
+ }), _jsxs("div", { className: "cls_add_new_key border-t pt-3 mt-3", children: [_jsx("h4", { className: "text-sm font-medium mb-2", children: t('config_editor.add_new_key_heading') }), _jsxs("div", { className: "space-y-2", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "new-key-name", className: "sr-only", children: t('config_editor.key_name_label') }), _jsx("input", { id: "new-key-name", type: "text", placeholder: t('config_editor.key_name_label'), value: new_key, onChange: handle_new_key_change, className: "w-full px-2 py-1 border rounded text-sm" })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: "new-key-value", className: "sr-only", children: t('config_editor.value_label') }), _jsx("input", { id: "new-key-value", type: "text", placeholder: t('config_editor.value_label'), value: new_value, onChange: handle_new_value_change, className: "w-full px-2 py-1 border rounded text-sm" })] }), _jsx("button", { onClick: handle_add_key, className: "px-3 py-1 bg-primary text-primary-foreground rounded hover:bg-primary/90 text-sm", type: "button", disabled: !new_key || !new_value, children: t('config_editor.add_key') })] })] })] })] })) : (_jsx("div", { className: "text-center text-muted-foreground py-8", children: t('config_editor.select_section_prompt') })) })] })] }));
223
224
  };
@@ -1 +1 @@
1
- {"version":3,"file":"config_viewer.d.ts","sourceRoot":"","sources":["../../src/components/config_viewer.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAyC,MAAM,OAAO,CAAA;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAIrD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,eAAe,EAAE,cAAc,CAAA;IAC/B;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IACjC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC3B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC7B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CA2MpD,CAAA"}
1
+ {"version":3,"file":"config_viewer.d.ts","sourceRoot":"","sources":["../../src/components/config_viewer.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAyC,MAAM,OAAO,CAAA;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAKrD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,eAAe,EAAE,cAAc,CAAA;IAC/B;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB;;OAEG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IACjC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC3B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC7B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CA2MpD,CAAA"}
@@ -6,6 +6,7 @@ import React, { useState, useCallback, useMemo } from 'react';
6
6
  import { cn } from 'hazo_ui';
7
7
  import { Pencil, CheckCircle2, XCircle, Eye, EyeOff } from 'lucide-react';
8
8
  import { useConfigSections, is_sensitive_field, mask_value, DEFAULT_SENSITIVE_PATTERNS } from './use_config_sections.js';
9
+ import { t } from '../lib/strings.js';
9
10
  /**
10
11
  * Config viewer component
11
12
  * Displays configuration sections and allows viewing/editing values
@@ -108,11 +109,11 @@ export const ConfigViewer = ({ config_provider, className, on_update, on_error,
108
109
  * Memoized section entries for rendering
109
110
  */
110
111
  const section_entries = useMemo(() => Object.entries(sections), [sections]);
111
- return (_jsx("div", { className: cn('cls_config_viewer space-y-4', className), children: section_entries.length === 0 ? (_jsx("div", { className: "p-4 text-center text-muted-foreground", children: "No configuration sections found" })) : (section_entries.map(([section_name, section_data]) => (_jsxs("div", { className: "cls_config_section border rounded-lg p-4", children: [_jsx("h3", { className: "text-lg font-semibold mb-3", children: section_name }), _jsx("div", { className: "space-y-2", children: Object.entries(section_data).map(([key, value]) => {
112
+ return (_jsx("div", { className: cn('cls_config_viewer space-y-4', className), children: section_entries.length === 0 ? (_jsx("div", { className: "p-4 text-center text-muted-foreground", children: t('config_viewer.empty') })) : (section_entries.map(([section_name, section_data]) => (_jsxs("div", { className: "cls_config_section border rounded-lg p-4", children: [_jsx("h3", { className: "text-lg font-semibold mb-3", children: section_name }), _jsx("div", { className: "space-y-2", children: Object.entries(section_data).map(([key, value]) => {
112
113
  const is_editing = editing?.section === section_name && editing?.key === key;
113
114
  const is_sensitive = should_mask_field(key);
114
115
  const field_id = `config-${section_name}-${key}`;
115
116
  const show_value = !is_sensitive || is_revealed(section_name, key);
116
- return (_jsx("div", { className: "cls_config_item flex items-center gap-2", children: _jsxs("div", { className: "flex-1", children: [_jsx("label", { htmlFor: field_id, className: "text-sm font-medium text-muted-foreground", children: key }), is_editing ? (_jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx("input", { id: field_id, type: is_sensitive ? 'password' : 'text', value: edit_value, onChange: handle_edit_change, className: "flex-1 px-2 py-1 border rounded text-sm", autoFocus: true }), _jsx("button", { onClick: save_edit, className: "text-green-600 hover:text-green-700", "aria-label": "Save changes", type: "button", children: _jsx(CheckCircle2, { size: 20 }) }), _jsx("button", { onClick: cancel_edit, className: "text-red-600 hover:text-red-700", "aria-label": "Cancel editing", type: "button", children: _jsx(XCircle, { size: 20 }) })] })) : (_jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx("div", { id: field_id, className: "flex-1 px-2 py-1 bg-muted rounded text-sm font-mono", children: show_value ? (value || '(empty)') : mask_value(value) }), is_sensitive && (_jsx("button", { onClick: () => toggle_reveal(section_name, key), className: "text-gray-500 hover:text-gray-700", "aria-label": show_value ? 'Hide value' : 'Show value', type: "button", children: show_value ? _jsx(EyeOff, { size: 18 }) : _jsx(Eye, { size: 18 }) })), _jsx("button", { onClick: () => start_edit(section_name, key), className: "text-blue-600 hover:text-blue-700", "aria-label": `Edit ${key}`, type: "button", children: _jsx(Pencil, { size: 18 }) })] }))] }) }, key));
117
+ return (_jsx("div", { className: "cls_config_item flex items-center gap-2", children: _jsxs("div", { className: "flex-1", children: [_jsx("label", { htmlFor: field_id, className: "text-sm font-medium text-muted-foreground", children: key }), is_editing ? (_jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx("input", { id: field_id, type: is_sensitive ? 'password' : 'text', value: edit_value, onChange: handle_edit_change, className: "flex-1 px-2 py-1 border rounded text-sm", autoFocus: true }), _jsx("button", { onClick: save_edit, className: "text-green-600 hover:text-green-700", "aria-label": t('config_viewer.save_changes_aria'), type: "button", children: _jsx(CheckCircle2, { size: 20 }) }), _jsx("button", { onClick: cancel_edit, className: "text-red-600 hover:text-red-700", "aria-label": t('config_viewer.cancel_editing_aria'), type: "button", children: _jsx(XCircle, { size: 20 }) })] })) : (_jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx("div", { id: field_id, className: "flex-1 px-2 py-1 bg-muted rounded text-sm font-mono", children: show_value ? (value || t('config_viewer.empty_value')) : mask_value(value) }), is_sensitive && (_jsx("button", { onClick: () => toggle_reveal(section_name, key), className: "text-gray-500 hover:text-gray-700", "aria-label": show_value ? t('common.hide_value') : t('common.show_value'), type: "button", children: show_value ? _jsx(EyeOff, { size: 18 }) : _jsx(Eye, { size: 18 }) })), _jsx("button", { onClick: () => start_edit(section_name, key), className: "text-blue-600 hover:text-blue-700", "aria-label": t('config_viewer.edit_key_aria', { key }), type: "button", children: _jsx(Pencil, { size: 18 }) })] }))] }) }, key));
117
118
  }) })] }, section_name)))) }));
118
119
  };
@@ -3,7 +3,7 @@ import { type VariantProps } from "class-variance-authority";
3
3
  declare const buttonVariants: (props?: ({
4
4
  variant?: "default" | "link" | "secondary" | "destructive" | "outline" | "ghost" | null | undefined;
5
5
  size?: "default" | "sm" | "lg" | "icon" | null | undefined;
6
- } & import("class-variance-authority/dist/types").ClassProp) | undefined) => string;
6
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
8
8
  asChild?: boolean;
9
9
  }
@@ -1 +1 @@
1
- {"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../src/components/ui/button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE,QAAA,MAAM,cAAc;;;mFA2BnB,CAAA;AAED,MAAM,WAAW,WACf,SAAQ,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EACnD,YAAY,CAAC,OAAO,cAAc,CAAC;IACrC,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,QAAA,MAAM,MAAM,uFAWX,CAAA;AAGD,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA"}
1
+ {"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../src/components/ui/button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAIjE,QAAA,MAAM,cAAc;;;8EA2BnB,CAAA;AAED,MAAM,WAAW,WACf,SAAQ,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EACnD,YAAY,CAAC,OAAO,cAAc,CAAC;IACrC,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,QAAA,MAAM,MAAM,uFAWX,CAAA;AAGD,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Purpose: Clone a global (platform-default) two-tier config row into a
3
+ * scope-level override row.
4
+ *
5
+ * hazo_connect is an optional peer dependency here (see `hazo_config/server-data`,
6
+ * the entry point that re-exports this). It is loaded via dynamic import()
7
+ * with bundler-ignore hints, matching `config_loader.ts`'s optional
8
+ * `hazo_core/errors` integration: `hazo_connect/server` and
9
+ * `hazo_connect/nextjs/setup` are ESM-only subpath exports, and this
10
+ * package's build uses moduleResolution:"node" (predates subpath-exports
11
+ * resolution), so TS can't resolve them statically even though they load
12
+ * fine at runtime.
13
+ */
14
+ export declare class CloneToOverrideError extends Error {
15
+ constructor(message: string);
16
+ }
17
+ /**
18
+ * Clone the global row `globalRowId` from `table` into a new row scoped to
19
+ * `orgScopeId`. Strips `id`/`created_at`/`updated_at` and stamps
20
+ * `cloned_from_at` on the new row.
21
+ *
22
+ * @throws {CloneToOverrideError} when the source row does not exist, is not
23
+ * itself a global row (`scope_id` must be `null`), or the insert returns no row.
24
+ */
25
+ export declare function cloneToOverride(table: string, globalRowId: string, orgScopeId: string): Promise<Record<string, unknown>>;
26
+ //# sourceMappingURL=clone_to_override.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clone_to_override.d.ts","sourceRoot":"","sources":["../../src/lib/clone_to_override.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA0BH,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAoBlC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Purpose: Clone a global (platform-default) two-tier config row into a
3
+ * scope-level override row.
4
+ *
5
+ * hazo_connect is an optional peer dependency here (see `hazo_config/server-data`,
6
+ * the entry point that re-exports this). It is loaded via dynamic import()
7
+ * with bundler-ignore hints, matching `config_loader.ts`'s optional
8
+ * `hazo_core/errors` integration: `hazo_connect/server` and
9
+ * `hazo_connect/nextjs/setup` are ESM-only subpath exports, and this
10
+ * package's build uses moduleResolution:"node" (predates subpath-exports
11
+ * resolution), so TS can't resolve them statically even though they load
12
+ * fine at runtime.
13
+ */
14
+ async function loadCreateCrudService() {
15
+ // @ts-ignore — hazo_connect/server is a valid subpath export; moduleResolution:"node"
16
+ // predates subpath exports so TS can't resolve it statically, but it loads correctly at runtime.
17
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
18
+ const mod = (await import(/* turbopackIgnore: true */ /* webpackIgnore: true */ 'hazo_connect/server'));
19
+ return mod.createCrudService;
20
+ }
21
+ async function loadGetHazoConnectSingleton() {
22
+ // @ts-ignore — hazo_connect/nextjs/setup is a valid subpath export; see loadCreateCrudService above.
23
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
24
+ const mod = (await import(/* turbopackIgnore: true */ /* webpackIgnore: true */ 'hazo_connect/nextjs/setup'));
25
+ return mod.getHazoConnectSingleton;
26
+ }
27
+ export class CloneToOverrideError extends Error {
28
+ constructor(message) {
29
+ super(message);
30
+ this.name = 'CloneToOverrideError';
31
+ }
32
+ }
33
+ /**
34
+ * Clone the global row `globalRowId` from `table` into a new row scoped to
35
+ * `orgScopeId`. Strips `id`/`created_at`/`updated_at` and stamps
36
+ * `cloned_from_at` on the new row.
37
+ *
38
+ * @throws {CloneToOverrideError} when the source row does not exist, is not
39
+ * itself a global row (`scope_id` must be `null`), or the insert returns no row.
40
+ */
41
+ export async function cloneToOverride(table, globalRowId, orgScopeId) {
42
+ const createCrudService = await loadCreateCrudService();
43
+ const getHazoConnectSingleton = await loadGetHazoConnectSingleton();
44
+ const svc = createCrudService(getHazoConnectSingleton(), table);
45
+ const row = await svc.findById(globalRowId);
46
+ if (!row)
47
+ throw new CloneToOverrideError('Global row not found');
48
+ if (row.scope_id !== null)
49
+ throw new CloneToOverrideError('Source row is not a global (scope_id must be null)');
50
+ // Strip the id and timestamps; set new scope
51
+ const { id: _omit, created_at: _ca, updated_at: _ua, ...rest } = row;
52
+ const inserted = await svc.insert({
53
+ ...rest,
54
+ scope_id: orgScopeId,
55
+ cloned_from_at: new Date().toISOString(),
56
+ });
57
+ if (!inserted[0])
58
+ throw new CloneToOverrideError('Insert returned no row');
59
+ return inserted[0];
60
+ }
@@ -10,6 +10,9 @@
10
10
  */
11
11
  export { MockConfigProvider } from './mock_config_provider.js';
12
12
  export { ConfigErrorCode } from './types.js';
13
+ export { t } from './strings.js';
13
14
  export type { ConfigProvider, HazoConfigOptions, Logger, HazoConfigError, LogData } from './types.js';
14
15
  export type { ConfigType, AppConfigItem, AppConfigContext, AppConfigProps, UseAppConfigResult } from './app_config_types.js';
16
+ export { mergeTwoTier } from './two_tier.js';
17
+ export type { TwoTierRow, MergeOptions } from './two_tier.js';
15
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,MAAM,EACN,eAAe,EACf,OAAO,EACR,MAAM,YAAY,CAAA;AAGnB,YAAY,EACV,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EACnB,MAAM,uBAAuB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAI5C,OAAO,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAChC,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,MAAM,EACN,eAAe,EACf,OAAO,EACR,MAAM,YAAY,CAAA;AAGnB,YAAY,EACV,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,kBAAkB,EACnB,MAAM,uBAAuB,CAAA;AAK9B,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC5C,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA"}
package/dist/lib/index.js CHANGED
@@ -11,3 +11,10 @@
11
11
  // Client-safe exports (no Node.js dependencies)
12
12
  export { MockConfigProvider } from './mock_config_provider.js';
13
13
  export { ConfigErrorCode } from './types.js';
14
+ // i18n — namespaced translator for this package's own baked-in UI strings
15
+ // (config editor/viewer, AppConfig, AppConfigListEditor). See strings.ts.
16
+ export { t } from './strings.js';
17
+ // Two-tier (global default + scope-level override) merge -- pure, no
18
+ // database dependency. See 'hazo_config/server-data' for the hazo_connect-
19
+ // backed clone/override-count helpers that operate on the same shape.
20
+ export { mergeTwoTier } from './two_tier.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Purpose: Count scope-level override rows for a two-tier config key.
3
+ *
4
+ * hazo_connect is an optional peer dependency here (see `hazo_config/server-data`,
5
+ * the entry point that re-exports this) and is loaded via dynamic import()
6
+ * for the same reason as `clone_to_override.ts` -- see that file's header.
7
+ */
8
+ /**
9
+ * Count how many override (non-global) rows exist in `table` for the given
10
+ * `keyColumn`/`keyValue`. A row counts as an override when its `scope_id`
11
+ * is not `null`.
12
+ */
13
+ export declare function getOverrideCount(table: string, keyColumn: string, keyValue: string): Promise<number>;
14
+ //# sourceMappingURL=override_count.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"override_count.d.ts","sourceRoot":"","sources":["../../src/lib/override_count.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA8BH;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAOjB"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Purpose: Count scope-level override rows for a two-tier config key.
3
+ *
4
+ * hazo_connect is an optional peer dependency here (see `hazo_config/server-data`,
5
+ * the entry point that re-exports this) and is loaded via dynamic import()
6
+ * for the same reason as `clone_to_override.ts` -- see that file's header.
7
+ */
8
+ async function loadCreateCrudService() {
9
+ // @ts-ignore — hazo_connect/server is a valid subpath export; moduleResolution:"node"
10
+ // predates subpath exports so TS can't resolve it statically, but it loads correctly at runtime.
11
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
12
+ const mod = (await import(/* turbopackIgnore: true */ /* webpackIgnore: true */ 'hazo_connect/server'));
13
+ return mod.createCrudService;
14
+ }
15
+ async function loadGetHazoConnectSingleton() {
16
+ // @ts-ignore — hazo_connect/nextjs/setup is a valid subpath export; see loadCreateCrudService above.
17
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
18
+ const mod = (await import(/* turbopackIgnore: true */ /* webpackIgnore: true */ 'hazo_connect/nextjs/setup'));
19
+ return mod.getHazoConnectSingleton;
20
+ }
21
+ /**
22
+ * Count how many override (non-global) rows exist in `table` for the given
23
+ * `keyColumn`/`keyValue`. A row counts as an override when its `scope_id`
24
+ * is not `null`.
25
+ */
26
+ export async function getOverrideCount(table, keyColumn, keyValue) {
27
+ const createCrudService = await loadCreateCrudService();
28
+ const getHazoConnectSingleton = await loadGetHazoConnectSingleton();
29
+ const svc = createCrudService(getHazoConnectSingleton(), table);
30
+ const rows = (await svc.list((q) => q.where(keyColumn, 'eq', keyValue).select('id,scope_id'))) ?? [];
31
+ return rows.filter((r) => r.scope_id !== null).length;
32
+ }
@@ -0,0 +1,2 @@
1
+ export declare const t: import("hazo_core/i18n").TFunc;
2
+ //# sourceMappingURL=strings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../../src/lib/strings.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,CAAC,gCAA8D,CAAA"}
@@ -0,0 +1,18 @@
1
+ // hazo_config/src/lib/strings.ts — i18n plumbing for hazo_config's own
2
+ // baked-in UI strings (config editor/viewer, AppConfig, and the
3
+ // AppConfigListEditor family), via hazo_core's canonical bridge
4
+ // (`hazo_core/i18n`).
5
+ //
6
+ // hazo_config's tsconfig.json uses `moduleResolution: "bundler"` (not
7
+ // `Node16`), and `resolveJsonModule: true` is set — so a plain JSON import
8
+ // (no `with { type: 'json' }` attribute) resolves cleanly. tsconfig.build.json
9
+ // previously overrode `moduleResolution` to the classic `"node"` resolver,
10
+ // which cannot follow a package.json `exports` subpath like `hazo_core/i18n`
11
+ // (TS2307) — it was switched to `"bundler"` (matching tsconfig.json) as part
12
+ // of this package's i18n adoption. All of this package's components that use
13
+ // `t()` are `'use client'` components, so a plain import (browser-safe, no
14
+ // `createRequire`) is used rather than the Node-only `createRequire`
15
+ // fallback documented for server-only consumers of this pattern.
16
+ import en from '../locales/en.json';
17
+ import { createPackageT } from 'hazo_core/i18n';
18
+ export const t = createPackageT('hazo_config', en);
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Purpose: Two-tier (global default + scope-level override) config merge.
3
+ *
4
+ * Companion to the AppConfig database-backed configuration story (see
5
+ * `app_config_types.ts`): a global row has `scope_id: null`; an override row
6
+ * has the caller's `scope_id` and shadows the global row that shares its key.
7
+ * Pure and framework-agnostic -- the caller fetches `globals`/`overrides`
8
+ * however it likes (hazo_connect, a plain fetch, a mock) and passes both
9
+ * arrays in.
10
+ */
11
+ /**
12
+ * Minimum shape a two-tier row must have. `scope_id: null` marks a global
13
+ * (platform-default) row; any other value marks a scope-level override.
14
+ */
15
+ export interface TwoTierRow {
16
+ id: string;
17
+ scope_id: string | null;
18
+ enabled: boolean;
19
+ [k: string]: unknown;
20
+ }
21
+ /**
22
+ * Options for {@link mergeTwoTier}.
23
+ */
24
+ export interface MergeOptions<T extends TwoTierRow> {
25
+ /** Platform-default rows (`scope_id: null`). */
26
+ globals: T[];
27
+ /** Scope-level override rows. */
28
+ overrides: T[];
29
+ /** Extracts the key two rows are matched on (e.g. a `code` column, or a composite of several columns). */
30
+ keyFn: (row: T) => string;
31
+ /**
32
+ * When true, skip key-based shadowing entirely and return the union of
33
+ * enabled globals and enabled overrides (both tiers additive, not
34
+ * override-wins-by-key).
35
+ */
36
+ additive?: boolean;
37
+ }
38
+ /**
39
+ * Merge a global/override row pair into the effective list.
40
+ *
41
+ * Default (non-additive) mode: for each key, an override row shadows the
42
+ * global row that shares it; a key present only in one tier passes through
43
+ * as-is. A row (global or override) is included only while `enabled` is
44
+ * true -- an override with `enabled: false` hides an enabled global at the
45
+ * same key, and a disabled global stays hidden unless an override
46
+ * re-enables it.
47
+ *
48
+ * Additive mode (`additive: true`) skips key matching and returns every
49
+ * enabled row from both tiers.
50
+ *
51
+ * @example
52
+ * mergeTwoTier({
53
+ * globals: [{ id: 'g1', scope_id: null, code: 'invoice', enabled: true }],
54
+ * overrides: [{ id: 'o1', scope_id: 'org-A', code: 'invoice', enabled: true }],
55
+ * keyFn: (r) => r.code,
56
+ * })
57
+ * // -> [{ id: 'o1', scope_id: 'org-A', code: 'invoice', enabled: true }]
58
+ */
59
+ export declare function mergeTwoTier<T extends TwoTierRow>(opts: MergeOptions<T>): T[];
60
+ //# sourceMappingURL=two_tier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"two_tier.d.ts","sourceRoot":"","sources":["../../src/lib/two_tier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,OAAO,CAAA;IAChB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,UAAU;IAChD,gDAAgD;IAChD,OAAO,EAAE,CAAC,EAAE,CAAA;IACZ,iCAAiC;IACjC,SAAS,EAAE,CAAC,EAAE,CAAA;IACd,0GAA0G;IAC1G,KAAK,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,UAAU,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAyB7E"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Purpose: Two-tier (global default + scope-level override) config merge.
3
+ *
4
+ * Companion to the AppConfig database-backed configuration story (see
5
+ * `app_config_types.ts`): a global row has `scope_id: null`; an override row
6
+ * has the caller's `scope_id` and shadows the global row that shares its key.
7
+ * Pure and framework-agnostic -- the caller fetches `globals`/`overrides`
8
+ * however it likes (hazo_connect, a plain fetch, a mock) and passes both
9
+ * arrays in.
10
+ */
11
+ /**
12
+ * Merge a global/override row pair into the effective list.
13
+ *
14
+ * Default (non-additive) mode: for each key, an override row shadows the
15
+ * global row that shares it; a key present only in one tier passes through
16
+ * as-is. A row (global or override) is included only while `enabled` is
17
+ * true -- an override with `enabled: false` hides an enabled global at the
18
+ * same key, and a disabled global stays hidden unless an override
19
+ * re-enables it.
20
+ *
21
+ * Additive mode (`additive: true`) skips key matching and returns every
22
+ * enabled row from both tiers.
23
+ *
24
+ * @example
25
+ * mergeTwoTier({
26
+ * globals: [{ id: 'g1', scope_id: null, code: 'invoice', enabled: true }],
27
+ * overrides: [{ id: 'o1', scope_id: 'org-A', code: 'invoice', enabled: true }],
28
+ * keyFn: (r) => r.code,
29
+ * })
30
+ * // -> [{ id: 'o1', scope_id: 'org-A', code: 'invoice', enabled: true }]
31
+ */
32
+ export function mergeTwoTier(opts) {
33
+ const { globals, overrides, keyFn, additive } = opts;
34
+ if (additive) {
35
+ return [...globals, ...overrides].filter((r) => r.enabled);
36
+ }
37
+ const overrideByKey = new Map();
38
+ for (const r of overrides)
39
+ overrideByKey.set(keyFn(r), r);
40
+ const result = [];
41
+ const seenKeys = new Set();
42
+ for (const g of globals) {
43
+ const k = keyFn(g);
44
+ seenKeys.add(k);
45
+ const override = overrideByKey.get(k);
46
+ const effective = override ?? g;
47
+ if (effective.enabled)
48
+ result.push(effective);
49
+ }
50
+ for (const o of overrides) {
51
+ const k = keyFn(o);
52
+ if (!seenKeys.has(k) && o.enabled)
53
+ result.push(o);
54
+ }
55
+ return result;
56
+ }