@happyvertical/smrt-svelte 0.55.2 → 0.55.4

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/README.md CHANGED
@@ -587,7 +587,7 @@ clears it.
587
587
 
588
588
  ### Components by Category
589
589
 
590
- **Forms**: `AddressInput`, `CheckboxInput`, `DateRangeInput`, `DateTimeInput`, `FileUpload`, `Form`, `FormGroup`, `FormMicButton`, `Input`, `MeasurementInput`, `MoneyInput`, `NumberInput`, `PhoneInput`, `RelationInput`, `SearchInput`, `Select`, `SelectInput`, `Textarea`, `TextareaInput`, `TextInput`, `Toggle`
590
+ **Forms**: `AddressInput`, `CheckboxInput`, `DateRangeInput`, `DateTimeInput`, `FileUpload`, `Form`, `FormGroup`, `FormMicButton`, `Input`, `MeasurementInput`, `MoneyInput`, `NumberInput`, `PhoneInput`, `RelationInput` (deprecated re-export; import from `@happyvertical/smrt-ui/forms`), `SearchInput`, `Select`, `SelectInput`, `Textarea`, `TextareaInput`, `TextInput`, `Toggle`
591
591
 
592
592
  **Layout**: `Container`, `EmptyState`, `Footer`, `Grid`, `Header`, `Masthead`, `PageHeader`, `SummaryCard`
593
593
 
@@ -15,7 +15,7 @@ the top-of-stack, domain-aware pieces:
15
15
  | Category | Components |
16
16
  |----------|------------|
17
17
  | AI | `Provider`, `AILoadingOverlay`, `CapabilityGate`, `DownloadProgress`, `STTTest`, `VoiceInput` |
18
- | Forms (`/forms`) | `TextInput`, `Select`, `MoneyInput`, `DateTimeInput`, `Toggle`, `FileUpload`, `AddressInput`, `RelationInput`, + more (AI-wired inputs use the hooks/browser-ai here) |
18
+ | Forms (`/forms`) | `TextInput`, `Select`, `MoneyInput`, `DateTimeInput`, `Toggle`, `FileUpload`, `AddressInput`, + more (`RelationInput` is a deprecated re-export of smrt-ui's) (AI-wired inputs use the hooks/browser-ai here) |
19
19
  | Module | `ModulePanel` |
20
20
  | Settings (`/settings`) | `SettingsCatalog`, `paginateSettingsCatalog` |
21
21
  | Audit (root export) | `RecordHistory`, `AuditList` — authorized data props; [contract](../../../docs/content/audit-trail.md) |
@@ -50,47 +50,13 @@ migrate domain re-rolls *onto* them rather than build new primitives:
50
50
  flow.
51
51
  - **`Card`** (`./ui`) — the standard surface/container; retire local card CSS.
52
52
 
53
- ### `RelationInput` (`./forms`, #3600)
54
-
55
- Searchable single-select for relation (foreign-key) fields, a thin wrapper over
56
- the smrt-ui `Combobox`. It is presentation only: the caller supplies the data,
57
- so the same lookup an assistant uses to turn "Acme" into a record id backs the
58
- picker. Option shape is `RelationOption` — `{ id, label, detail? }`.
59
-
60
- - **Props.** `name`, `label`, `value` (bindable id, `''` = none), `required`,
61
- `disabled`, `error`, `placeholder`, `description`, `search(query)`, optional
62
- `resolve(id)` and `onCreate(query)`, plus `debounceMs` (250), `interaction`,
63
- `onchange` and overridable text (`createLabel`, `clearLabel`, `loadingText`,
64
- `emptyText`, `errorText`, `resultsText`).
65
- - **`search`** runs with `''` when the list opens and then debounced per
66
- keystroke; a response for a superseded query is dropped. A rejected search
67
- shows `errorText` and the next keystroke retries.
68
- - **`resolve`** supplies the label of a `value` that has not been searched yet
69
- (initial value, parent rebind, form reset). Without it such a value shows an
70
- empty field, never the raw id. A `null` result also leaves the field empty.
71
- - **`onCreate`** adds a "New ..." button; it receives the text most recently
72
- searched for and may return the created `RelationOption`, which is selected.
73
- - **Form integration** is through `Combobox`'s control registration (rich `Form`
74
- provides the registry), not a `FieldDefinition`: agents see a `combobox`
75
- control whose value is the id. The form posts the id under `name`.
76
- - **Known limit.** An agent staging a value through the registered control can
77
- only choose among the options last listed (the registry matches against
78
- `options`; `allowCustom` is off), so it cannot write an id it has not
79
- searched. It fails closed. Writing a resolved id needs an async hook on
80
- `Combobox` and is tracked separately.
81
- - **Stale options.** Typing clears the listed options until the new search
82
- answers, so Enter or a click can never pick a record for an older query.
83
- - **Clear button** shows only when not `required`, not `disabled`, and a value
84
- is set; it returns focus to the field.
85
- - **Accessibility** is the combobox pattern (`role=combobox`, `aria-expanded`,
86
- `aria-activedescendant`, listbox/option, Arrow/Enter/Escape) plus a polite
87
- `role=status` live region announcing searching / "N results" / no matches,
88
- `aria-busy` while fetching, and `aria-invalid` + `aria-describedby` for
89
- `error` and `description`.
90
- - Options render `detail` under the label, so the option's accessible name
91
- includes it. The `Combobox` additions that make this possible (`filter`,
92
- `onquery`, `status`, `busy`, `invalid`, `describedby`, `optionContent`) are
93
- generic and default to the previous behaviour.
53
+ ### `RelationInput` (moved to smrt-ui, #3637)
54
+
55
+ `RelationInput` (#3600) now lives in `@happyvertical/smrt-ui/forms` next to
56
+ `Combobox`; its contract is documented in the smrt-ui README ("RelationInput").
57
+ `@happyvertical/smrt-svelte/forms` still re-exports it (and `RelationOption`),
58
+ marked `@deprecated`, so published consumers keep working. New code imports it
59
+ from smrt-ui.
94
60
 
95
61
  ### Import convention (S10 #1415)
96
62
 
@@ -102,7 +68,7 @@ library. Which barrel for what:
102
68
  | Need | Import from |
103
69
  |------|-------------|
104
70
  | Buttons, cards, badges, avatars, chips, skeletons, tooltips, dropdowns, trees, pagination | `@happyvertical/smrt-ui/ui` |
105
- | Provider-free base inputs — `Input`, `Select`, `Textarea`, `Toggle`, `FormGroup` | `@happyvertical/smrt-ui/forms` (also re-exported from `@happyvertical/smrt-svelte/forms`) |
71
+ | Provider-free base inputs — `Input`, `Select`, `Textarea`, `Toggle`, `FormGroup`, `Combobox`, `RelationInput` | `@happyvertical/smrt-ui/forms` (also re-exported from `@happyvertical/smrt-svelte/forms`) |
106
72
  | Provider-free `Form` (plain `<form>` wrapper) | `@happyvertical/smrt-ui/forms` **only** — `@happyvertical/smrt-svelte/forms` exports the *rich* Provider-backed `Form` under that name, so import the plain one straight from smrt-ui |
107
73
  | Provider-backed inputs — `TextInput`, `NumberInput`, `MoneyInput`, date/measurement/address inputs, `CheckboxInput`, file upload, the rich `Form` | `@happyvertical/smrt-svelte/forms` |
108
74
  | `Modal`, `ConfirmDialog`, `LoadingOverlay`, `ProgressBar` | `@happyvertical/smrt-ui/feedback` |
@@ -45,6 +45,14 @@ App settings, tenant names and environment badges wrap within narrow drawers.
45
45
  system bars never clip wrapped chips. Custom bars must allow this flex child
46
46
  to shrink; use safe alignment if aligning its contents to the end.
47
47
 
48
+ Sidebar-only shells may set `top: false`. In that mode the configured brand is
49
+ shown at the top of the expanded tenant panel as well as in the compact rail.
50
+ Use `tenantRailFooter` for a control pinned below collapsed navigation; it sits
51
+ outside the rail's scroll clip so menus can open over the workspace. Pair it
52
+ with `WorkspaceAccountMenu compact` for an avatar-only trigger that retains the
53
+ full account identity as its accessible name. Keep the ordinary
54
+ `tenantFooter` for the expanded panel.
55
+
48
56
  ### Responsive chrome, viewport defaults, resizable edges
49
57
 
50
58
  The root grid has five rows: `header` · top edge · body · bottom edge · phone
@@ -8,7 +8,12 @@
8
8
  * one-stop barrel for form primitives. The Provider-REQUIRED inputs (the rich
9
9
  * `Form`, `CheckboxInput`, `TextInput`, …) still live in this package.
10
10
  */
11
- export { FormGroup, Input, Select, Textarea, Toggle, } from '@happyvertical/smrt-ui/forms';
11
+ export { FormGroup, Input,
12
+ /**
13
+ * @deprecated `RelationInput` moved to `@happyvertical/smrt-ui/forms`; import it
14
+ * from there. This re-export is kept so published consumers keep working.
15
+ */
16
+ RelationInput, Select, Textarea, Toggle, } from '@happyvertical/smrt-ui/forms';
12
17
  export { default as AddressInput } from './AddressInput.svelte';
13
18
  export { default as CheckboxInput } from './CheckboxInput.svelte';
14
19
  export { default as DateRangeInput } from './DateRangeInput.svelte';
@@ -21,7 +26,6 @@ export { default as MeasurementInput } from './MeasurementInput.svelte';
21
26
  export { default as MoneyInput } from './MoneyInput.svelte';
22
27
  export { default as NumberInput } from './NumberInput.svelte';
23
28
  export { default as PhoneInput } from './PhoneInput.svelte';
24
- export { default as RelationInput } from './RelationInput.svelte';
25
29
  export { default as SearchInput } from './SearchInput.svelte';
26
30
  export { default as SelectInput } from './SelectInput.svelte';
27
31
  export { default as TextareaInput } from './TextareaInput.svelte';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/forms/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EACL,SAAS,EACT,KAAK,EACL,MAAM,EACN,QAAQ,EACR,MAAM,GACP,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAG1D,YAAY,EACV,YAAY,EACZ,cAAc,EACd,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,cAAc,GACf,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/forms/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EACL,SAAS,EACT,KAAK;AACL;;;GAGG;AACH,aAAa,EACb,MAAM,EACN,QAAQ,EACR,MAAM,GACP,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAG1D,YAAY,EACV,YAAY,EACZ,cAAc,EACd,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,cAAc,GACf,MAAM,YAAY,CAAC"}
@@ -9,7 +9,12 @@
9
9
  * `Form`, `CheckboxInput`, `TextInput`, …) still live in this package.
10
10
  */
11
11
  // Provider-free base primitives — relocated to the smrt-ui leaf (#1589).
12
- export { FormGroup, Input, Select, Textarea, Toggle, } from '@happyvertical/smrt-ui/forms';
12
+ export { FormGroup, Input,
13
+ /**
14
+ * @deprecated `RelationInput` moved to `@happyvertical/smrt-ui/forms`; import it
15
+ * from there. This re-export is kept so published consumers keep working.
16
+ */
17
+ RelationInput, Select, Textarea, Toggle, } from '@happyvertical/smrt-ui/forms';
13
18
  // SMRT-enhanced form components with voice support (v2.0 naming)
14
19
  export { default as AddressInput } from './AddressInput.svelte';
15
20
  export { default as CheckboxInput } from './CheckboxInput.svelte';
@@ -23,7 +28,6 @@ export { default as MeasurementInput } from './MeasurementInput.svelte';
23
28
  export { default as MoneyInput } from './MoneyInput.svelte';
24
29
  export { default as NumberInput } from './NumberInput.svelte';
25
30
  export { default as PhoneInput } from './PhoneInput.svelte';
26
- export { default as RelationInput } from './RelationInput.svelte';
27
31
  export { default as SearchInput } from './SearchInput.svelte';
28
32
  export { default as SelectInput } from './SelectInput.svelte';
29
33
  export { default as TextareaInput } from './TextareaInput.svelte';
@@ -10,15 +10,12 @@ export interface SelectOption {
10
10
  value: string;
11
11
  label: string;
12
12
  }
13
- /** A searchable relation target for the RelationInput component */
14
- export interface RelationOption {
15
- /** Record id; this is the value the form posts. */
16
- id: string;
17
- /** Human-readable name shown in the field and the list. */
18
- label: string;
19
- /** Secondary text shown under the label in the list. */
20
- detail?: string;
21
- }
13
+ /**
14
+ * A searchable relation target for the RelationInput component.
15
+ *
16
+ * @deprecated Moved to `@happyvertical/smrt-ui/forms`; import it from there.
17
+ */
18
+ export type { RelationOption } from '@happyvertical/smrt-ui/forms';
22
19
  /** Address value for SMRTAddress component */
23
20
  export interface AddressValue {
24
21
  street: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/forms/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,kCAAkC;AAClC,MAAM,MAAM,cAAc,GAAG,gBAAgB,GAAG,cAAc,GAAG,aAAa,CAAC;AAE/E,wCAAwC;AACxC,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,cAAc,GACd,cAAc,GACd,cAAc,GACd,cAAc,CAAC;AAEnB,sCAAsC;AACtC,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,mEAAmE;AACnE,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,EAAE,EAAE,MAAM,CAAC;IACX,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,8CAA8C;AAC9C,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,mDAAmD;AACnD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,6BAA6B;AAC7B,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAErE,sDAAsD;AACtD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,eAAe,CAAC;CACvB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/forms/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,kCAAkC;AAClC,MAAM,MAAM,cAAc,GAAG,gBAAgB,GAAG,cAAc,GAAG,aAAa,CAAC;AAE/E,wCAAwC;AACxC,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,cAAc,GACd,cAAc,GACd,cAAc,GACd,cAAc,CAAC;AAEnB,sCAAsC;AACtC,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,YAAY,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEnE,8CAA8C;AAC9C,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,mDAAmD;AACnD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,6BAA6B;AAC7B,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAErE,sDAAsD;AACtD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,eAAe,CAAC;CACvB"}
@@ -81,6 +81,8 @@ function buildLayoutStyle(shell: ModuleShellState): string {
81
81
  appPanel?: Snippet;
82
82
  /** Content for the left tenant rail. */
83
83
  tenantRail?: Snippet;
84
+ /** Content pinned below the collapsed tenant rail. */
85
+ tenantRailFooter?: Snippet;
84
86
  /** Content for the left tenant panel. */
85
87
  tenantPanel?: Snippet;
86
88
  /** Content for the tenant panel footer. */
@@ -164,6 +166,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
164
166
  account,
165
167
  appPanel,
166
168
  tenantRail,
169
+ tenantRailFooter,
167
170
  tenantPanel,
168
171
  tenantFooter,
169
172
  focusRail,
@@ -239,8 +242,19 @@ function buildLayoutStyle(shell: ModuleShellState): string {
239
242
  });
240
243
  return () => { cancelled = true; queueMicrotask(() => { if (opener?.isConnected) opener.focus(); }); };
241
244
  });
242
- const showPhoneTop = $derived(isPhone && Boolean(phoneTopBar));
243
- const showHeader = $derived(Boolean(header) && !showPhoneTop);
245
+ const showDefaultPhoneTenantOpener = $derived(
246
+ isPhone &&
247
+ !phoneTopBar &&
248
+ panelState('top') === 'hidden' &&
249
+ shell.isEdgeShown('left') &&
250
+ shell.phonePresentation('left') !== 'hidden',
251
+ );
252
+ const showPhoneTop = $derived(
253
+ isPhone && (Boolean(phoneTopBar) || showDefaultPhoneTenantOpener),
254
+ );
255
+ const showHeader = $derived(
256
+ Boolean(header) && !(isPhone && Boolean(phoneTopBar)),
257
+ );
244
258
  const bottomBar = $derived<BottomBarMode>(
245
259
  isPhone && phoneBottomBar
246
260
  ? bottomBarMode({ formActions, keyboardOpen })
@@ -598,7 +612,12 @@ function buildLayoutStyle(shell: ModuleShellState): string {
598
612
  handle.addEventListener('lostpointercapture', onEnd);
599
613
  }
600
614
 
601
- const footerInHeader = $derived(!account && !!tenantFooter && (!shell.isEdgeShown('left') || !edgeExpanded('left')));
615
+ const footerInHeader = $derived(
616
+ !account &&
617
+ !!tenantFooter &&
618
+ (!shell.isEdgeShown('left') ||
619
+ (!edgeExpanded('left') && !tenantRailFooter)),
620
+ );
602
621
 
603
622
  const layoutStyle = $derived(buildLayoutStyle(shell));
604
623
 
@@ -883,7 +902,9 @@ function buildLayoutStyle(shell: ModuleShellState): string {
883
902
  >
884
903
  {@render resizer('left')}
885
904
  <div class="smrt-admin-shell__rail">
886
- {#if !edgeExpanded('left') && (homeHref || logoSrc || brand)}{@render shellBrand(true)}{/if}
905
+ {#if (panelState('top') === 'hidden' || !edgeExpanded('left')) && (homeHref || logoSrc || brand)}
906
+ {@render shellBrand(!edgeExpanded('left'))}
907
+ {/if}
887
908
  {#if showTenantToggle}{@render edgeToggle('left')}{/if}
888
909
  {#if shownOpen('left') || (tenantPanel && keepsContent('left'))}
889
910
  <div
@@ -904,8 +925,19 @@ function buildLayoutStyle(shell: ModuleShellState): string {
904
925
  {/if}
905
926
  </div>
906
927
  {/if}
907
- {#if !shownOpen('left') && tenantRail}
908
- {@render tenantRail()}
928
+ {#if !shownOpen('left') && (tenantRail || tenantRailFooter)}
929
+ <div class="smrt-admin-shell__tenant-rail-stack">
930
+ {#if tenantRail}
931
+ <div class="smrt-admin-shell__tenant-rail-content">
932
+ {@render tenantRail()}
933
+ </div>
934
+ {/if}
935
+ {#if tenantRailFooter}
936
+ <div class="smrt-admin-shell__tenant-rail-footer">
937
+ {@render tenantRailFooter()}
938
+ </div>
939
+ {/if}
940
+ </div>
909
941
  {/if}
910
942
  </div>
911
943
  </aside>
@@ -922,7 +954,24 @@ function buildLayoutStyle(shell: ModuleShellState): string {
922
954
 
923
955
  {#if showPhoneTop}
924
956
  <div class="smrt-admin-shell__phone-top" data-testid="admin-shell-phone-top">
925
- {@render phoneTopBar?.({ hidden: chromeHidden })}
957
+ {#if phoneTopBar}
958
+ {@render phoneTopBar({ hidden: chromeHidden })}
959
+ {:else if showDefaultPhoneTenantOpener}
960
+ <div class="smrt-admin-shell__phone-default-top">
961
+ <Button
962
+ variant="ghost"
963
+ size="sm"
964
+ aria-label={t(M['ui.admin_shell.menu'])}
965
+ aria-expanded={edgeExpanded('left')}
966
+ aria-controls="smrt-admin-shell-left-panel"
967
+ onclick={() =>
968
+ shell.setPanelState(
969
+ 'left',
970
+ edgeExpanded('left') ? 'collapsed' : 'expanded',
971
+ )}
972
+ >{t(M['ui.admin_shell.menu'])}</Button>
973
+ </div>
974
+ {/if}
926
975
  </div>
927
976
  {/if}
928
977
 
@@ -1424,6 +1473,7 @@ function buildLayoutStyle(shell: ModuleShellState): string {
1424
1473
  display: flex;
1425
1474
  flex-direction: column;
1426
1475
  gap: var(--smrt-spacing-2);
1476
+ overflow: visible;
1427
1477
  }
1428
1478
 
1429
1479
  /* A collapsed `keepMounted` edge keeps its panel in the DOM but hidden;
@@ -1456,6 +1506,30 @@ function buildLayoutStyle(shell: ModuleShellState): string {
1456
1506
  border-block-start: 1px solid var(--smrt-color-outline-variant);
1457
1507
  }
1458
1508
 
1509
+ .smrt-admin-shell__tenant-rail-stack {
1510
+ display: grid;
1511
+ grid-template-rows: minmax(0, 1fr) auto;
1512
+ flex: 1;
1513
+ min-width: 0;
1514
+ min-height: 0;
1515
+ }
1516
+
1517
+ .smrt-admin-shell__tenant-rail-content {
1518
+ min-width: 0;
1519
+ min-height: 0;
1520
+ overflow: auto;
1521
+ }
1522
+
1523
+ .smrt-admin-shell__tenant-rail-footer {
1524
+ position: relative;
1525
+ z-index: 1;
1526
+ grid-row: 2;
1527
+ min-width: 0;
1528
+ padding-block-start: var(--smrt-spacing-2);
1529
+ border-block-start: 1px solid var(--smrt-color-outline-variant);
1530
+ overflow: visible;
1531
+ }
1532
+
1459
1533
  .smrt-admin-shell__edge--right[data-state='expanded']
1460
1534
  .smrt-admin-shell__rail {
1461
1535
  grid-column: 2;
@@ -1643,6 +1717,13 @@ function buildLayoutStyle(shell: ModuleShellState): string {
1643
1717
  var(--smrt-easing-standard, ease);
1644
1718
  }
1645
1719
 
1720
+ .smrt-admin-shell__phone-default-top {
1721
+ display: flex;
1722
+ align-items: center;
1723
+ min-block-size: var(--smrt-admin-shell-phone-top-size);
1724
+ padding-inline: var(--smrt-spacing-3);
1725
+ }
1726
+
1646
1727
  .smrt-admin-shell[data-chrome-hidden] .smrt-admin-shell__phone-top {
1647
1728
  transform: translateY(-100%);
1648
1729
  }
@@ -13,6 +13,8 @@ interface Props extends AdminShellProps {
13
13
  appPanel?: Snippet;
14
14
  /** Content for the left tenant rail. */
15
15
  tenantRail?: Snippet;
16
+ /** Content pinned below the collapsed tenant rail. */
17
+ tenantRailFooter?: Snippet;
16
18
  /** Content for the left tenant panel. */
17
19
  tenantPanel?: Snippet;
18
20
  /** Content for the tenant panel footer. */
@@ -1 +1 @@
1
- {"version":3,"file":"AdminShell.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/workspace/admin-shell/AdminShell.svelte.ts"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAQtC,OAAO,EAAE,KAAK,aAAa,EAAiB,MAAM,mBAAmB,CAAC;AAStE,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EACR,sBAAsB,EACtB,eAAe,EAEf,cAAc,EACd,aAAa,EACd,MAAM,YAAY,CAAC;AAGpB,UAAU,KAAM,SAAQ,eAAe;IACrC,sEAAsE;IACtE,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,mCAAmC;IACnC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qEAAqE;IACrE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wCAAwC;IACxC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,yCAAyC;IACzC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,2CAA2C;IAC3C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wCAAwC;IACxC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,mEAAmE;IACnE,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,IAAI,EAAE,cAAc,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC,CAAC;IACxD;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oCAAoC;IACpC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uCAAuC;IACvC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,wCAAwC;IACxC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,0CAA0C;IAC1C,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,2CAA2C;IAC3C,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,kDAAkD;IAClD,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,QAAQ,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC,CAAC;IAChD;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC,CAAC;IAC7C;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,QAAQ,EAAE,aAAa,CAAC;QAAC,SAAS,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC,CAAC;IAC5E,uEAAuE;IACvE,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;CACnB;AA82BH,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"AdminShell.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/workspace/admin-shell/AdminShell.svelte.ts"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAQtC,OAAO,EAAE,KAAK,aAAa,EAAiB,MAAM,mBAAmB,CAAC;AAStE,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EACR,sBAAsB,EACtB,eAAe,EAEf,cAAc,EACd,aAAa,EACd,MAAM,YAAY,CAAC;AAGpB,UAAU,KAAM,SAAQ,eAAe;IACrC,sEAAsE;IACtE,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,mCAAmC;IACnC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qEAAqE;IACrE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wCAAwC;IACxC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,sDAAsD;IACtD,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,yCAAyC;IACzC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,2CAA2C;IAC3C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wCAAwC;IACxC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,mEAAmE;IACnE,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,IAAI,EAAE,cAAc,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC,CAAC;IACxD;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oCAAoC;IACpC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uCAAuC;IACvC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,wCAAwC;IACxC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,0CAA0C;IAC1C,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,2CAA2C;IAC3C,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,kDAAkD;IAClD,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,QAAQ,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC,CAAC;IAChD;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC,CAAC;IAC7C;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,QAAQ,EAAE,aAAa,CAAC;QAAC,SAAS,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC,CAAC;IAC5E,uEAAuE;IACvE,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,qEAAqE;IACrE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;CACnB;AAs5BH,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -13,6 +13,8 @@ export interface Props {
13
13
  userName: string;
14
14
  /** Target density; omit to inherit the theme density. */
15
15
  density?: 'comfortable' | 'touch';
16
+ /** Show only the avatar while preserving the full accessible account name. */
17
+ compact?: boolean;
16
18
  /** Custom display name for the user, falls back to userName. */
17
19
  userLabel?: string;
18
20
  /** URL for the user's avatar image. */
@@ -38,6 +40,7 @@ export interface Props {
38
40
  let {
39
41
  userName,
40
42
  density,
43
+ compact = false,
41
44
  userLabel = '',
42
45
  avatarUrl,
43
46
  tenantLabel = '',
@@ -103,10 +106,11 @@ function handleSelect(id: string): void {
103
106
  }
104
107
  </script>
105
108
 
106
- <div data-density={density} class="smrt-workspace-account-menu" title={summaryTitle}>
109
+ <div data-density={density} data-compact={compact ? '' : undefined} class="smrt-workspace-account-menu" title={summaryTitle}>
107
110
  <Dropdown
108
111
  {items}
109
112
  {placement}
113
+ triggerLabel={compact ? `${t(M['ui.workspace_account_menu.open'])} ${summaryTitle}` : undefined}
110
114
  onselect={handleSelect}
111
115
  disabled={disabled || items.length === 0}
112
116
  >
@@ -161,6 +165,19 @@ function handleSelect(id: string): void {
161
165
  text-align: start;
162
166
  }
163
167
 
168
+ .smrt-workspace-account-menu[data-compact] :global(.dropdown__trigger) {
169
+ display: grid;
170
+ place-items: center;
171
+ grid-template-columns: minmax(0, 1fr);
172
+ padding: var(--smrt-spacing-1);
173
+ }
174
+
175
+ .smrt-workspace-account-menu[data-compact] .smrt-workspace-account-menu__identity,
176
+ .smrt-workspace-account-menu[data-compact] .smrt-workspace-account-menu__indicator,
177
+ .smrt-workspace-account-menu[data-compact] .smrt-workspace-account-menu__open {
178
+ display: none;
179
+ }
180
+
164
181
  .smrt-workspace-account-menu :global(.dropdown__menu) {
165
182
  box-sizing: border-box;
166
183
  width: 100%;
@@ -5,6 +5,8 @@ export interface Props {
5
5
  userName: string;
6
6
  /** Target density; omit to inherit the theme density. */
7
7
  density?: 'comfortable' | 'touch';
8
+ /** Show only the avatar while preserving the full accessible account name. */
9
+ compact?: boolean;
8
10
  /** Custom display name for the user, falls back to userName. */
9
11
  userLabel?: string;
10
12
  /** URL for the user's avatar image. */
@@ -1 +1 @@
1
- {"version":3,"file":"WorkspaceAccountMenu.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/workspace/admin-shell/WorkspaceAccountMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAY,MAAM,wBAAwB,CAAC;AAI1E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAGzD,MAAM,WAAW,KAAK;IACpB,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC;IAClC,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,OAAO,CAAC,EAAE,sBAAsB,EAAE,CAAC;IACnC,2CAA2C;IAC3C,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,sEAAsE;IACtE,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B,wCAAwC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uDAAuD;IACvD,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AAsGD,QAAA,MAAM,oBAAoB,2CAAwC,CAAC;AACnE,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACpE,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"WorkspaceAccountMenu.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/workspace/admin-shell/WorkspaceAccountMenu.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAY,MAAM,wBAAwB,CAAC;AAI1E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAGzD,MAAM,WAAW,KAAK;IACpB,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC;IAClC,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,OAAO,CAAC,EAAE,sBAAsB,EAAE,CAAC;IACnC,2CAA2C;IAC3C,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,sEAAsE;IACtE,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B,wCAAwC;IACxC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uDAAuD;IACvD,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AAuGD,QAAA,MAAM,oBAAoB,2CAAwC,CAAC;AACnE,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACpE,eAAe,oBAAoB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-svelte",
3
- "version": "0.55.2",
3
+ "version": "0.55.4",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "Svelte 5 components for SMRT user management - auth, users, tenants, roles, permissions, groups",
6
6
  "type": "module",
@@ -148,12 +148,12 @@
148
148
  "access": "public"
149
149
  },
150
150
  "dependencies": {
151
- "@happyvertical/logger": "^0.102.2",
152
- "@happyvertical/smrt-languages": "0.55.2",
153
- "@happyvertical/smrt-mcp-apps": "0.55.2",
154
- "@happyvertical/smrt-types": "0.55.2",
155
- "@happyvertical/smrt-ui": "0.55.2",
156
- "@happyvertical/smrt-web": "0.55.2",
151
+ "@happyvertical/logger": "^0.102.3",
152
+ "@happyvertical/smrt-languages": "0.55.4",
153
+ "@happyvertical/smrt-mcp-apps": "0.55.4",
154
+ "@happyvertical/smrt-types": "0.55.4",
155
+ "@happyvertical/smrt-ui": "0.55.4",
156
+ "@happyvertical/smrt-web": "0.55.4",
157
157
  "@tanstack/db": "^0.6.14",
158
158
  "@tanstack/svelte-db": "^0.1.91",
159
159
  "esm-env": "^1.2.2"
@@ -188,14 +188,14 @@
188
188
  }
189
189
  },
190
190
  "devDependencies": {
191
- "@happyvertical/smrt-agents": "0.55.2",
192
- "@happyvertical/smrt-chat": "0.55.2",
193
- "@happyvertical/smrt-content": "0.55.2",
194
- "@happyvertical/smrt-core": "0.55.2",
195
- "@happyvertical/smrt-reports": "0.55.2",
196
- "@happyvertical/smrt-scanner": "0.55.2",
197
- "@happyvertical/smrt-tenancy": "0.55.2",
198
- "@happyvertical/smrt-users": "0.55.2",
191
+ "@happyvertical/smrt-agents": "0.55.4",
192
+ "@happyvertical/smrt-chat": "0.55.4",
193
+ "@happyvertical/smrt-content": "0.55.4",
194
+ "@happyvertical/smrt-core": "0.55.4",
195
+ "@happyvertical/smrt-reports": "0.55.4",
196
+ "@happyvertical/smrt-scanner": "0.55.4",
197
+ "@happyvertical/smrt-tenancy": "0.55.4",
198
+ "@happyvertical/smrt-users": "0.55.4",
199
199
  "@sveltejs/package": "^2.5.8",
200
200
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
201
201
  "@testing-library/jest-dom": "^7.0.1",
@@ -1,379 +0,0 @@
1
- <script lang="ts">
2
- import { Button } from '@happyvertical/smrt-ui';
3
- import {
4
- Combobox,
5
- type ControlInteractionOptions,
6
- emitControlChange,
7
- recordControlUserEdit,
8
- tryGetControlInteractionContext,
9
- } from '@happyvertical/smrt-ui/forms';
10
- import { untrack } from 'svelte';
11
- import { logger } from '../../internal/logger.js';
12
- import type { RelationOption } from './types.js';
13
-
14
- /**
15
- * RelationInput - searchable single-select for relation (foreign key) fields.
16
- *
17
- * Presentation only: the caller supplies `search` (and optionally `resolve`),
18
- * so the same lookup an assistant uses to turn "Acme" into a record id backs
19
- * the picker. Built on the smrt-ui `Combobox`, which supplies the ARIA 1.2
20
- * combobox pattern (arrow keys, Enter, Escape, `aria-activedescendant`) and
21
- * registers with the rich Form's control registry. The form posts the
22
- * committed id under `name`, never the visible label.
23
- *
24
- * @example
25
- * ```svelte
26
- * <RelationInput
27
- * name="customerId"
28
- * label="Customer"
29
- * bind:value={customerId}
30
- * search={(q) => customers.search(q)}
31
- * resolve={(id) => customers.byId(id)}
32
- * />
33
- * ```
34
- */
35
- export interface Props {
36
- /** Field name; the selected id posts under it. */
37
- name: string;
38
- /** Visible label. */
39
- label: string;
40
- /** Selected record id (bindable). Empty string means no selection. */
41
- value?: string;
42
- /** Required field: hides the clear button and flags an empty value. */
43
- required?: boolean;
44
- /** Disabled state. */
45
- disabled?: boolean;
46
- /** Validation error message, shown below the field. */
47
- error?: string;
48
- /** Placeholder shown when nothing is selected. */
49
- placeholder?: string;
50
- /** Hint announced with the field. */
51
- description?: string;
52
- /** Look up records matching the text; called with '' when the list opens. */
53
- search: (query: string) => Promise<RelationOption[]>;
54
- /** Look up the current value's label when it has not been searched yet. */
55
- resolve?: (id: string) => Promise<{ id: string; label: string } | null>;
56
- /**
57
- * "New ..." action. Receives the text most recently searched for. May
58
- * return the created record, which is then selected.
59
- */
60
- onCreate?: (
61
- query: string,
62
- ) => void | RelationOption | Promise<void | RelationOption>;
63
- /** Milliseconds to wait after typing before searching. */
64
- debounceMs?: number;
65
- /** Combobox interaction options (stable id, subject) for agents. */
66
- interaction?: ControlInteractionOptions | false;
67
- /** Text for the "New ..." button. */
68
- createLabel?: string;
69
- /** Accessible name for the clear button. */
70
- clearLabel?: string;
71
- /** Status text while searching. */
72
- loadingText?: string;
73
- /** Status text when nothing matches. */
74
- emptyText?: string;
75
- /** Status text when the search fails. */
76
- errorText?: string;
77
- /** Status text announcing how many results are listed. */
78
- resultsText?: (count: number) => string;
79
- /** Called when the selected id changes ('' when cleared). */
80
- onchange?: (value: string) => void;
81
- }
82
-
83
- let {
84
- name,
85
- label,
86
- value = $bindable(''),
87
- required = false,
88
- disabled = false,
89
- error,
90
- placeholder,
91
- description,
92
- search,
93
- resolve,
94
- onCreate,
95
- debounceMs = 250,
96
- interaction,
97
- createLabel,
98
- clearLabel,
99
- loadingText = 'Searching…',
100
- emptyText = 'No matches',
101
- errorText = 'Search failed. Try again.',
102
- resultsText = (count: number) =>
103
- `${count} ${count === 1 ? 'result' : 'results'} available`,
104
- onchange,
105
- }: Props = $props();
106
-
107
- const instanceId = $props.id();
108
- const errorId = `${instanceId}-error`;
109
- const hintId = `${instanceId}-hint`;
110
- const interactionContext = tryGetControlInteractionContext();
111
-
112
- let rootEl = $state<HTMLDivElement | null>(null);
113
- let results = $state<RelationOption[]>([]);
114
- let loading = $state(false);
115
- let failed = $state(false);
116
- let selectedLabel = $state('');
117
- let creating = $state(false);
118
-
119
- // Plain bookkeeping, deliberately not reactive.
120
- let labelledId = '';
121
- let resolvingId = '';
122
- let searchSeq = 0;
123
- let lastQuery = '';
124
- let timer: ReturnType<typeof setTimeout> | undefined;
125
-
126
- const options = $derived(
127
- results.map((item) => ({ value: item.id, label: item.label })),
128
- );
129
- const detailById = $derived(
130
- new Map(results.map((item) => [item.id, item.detail])),
131
- );
132
- const status = $derived(
133
- loading
134
- ? loadingText
135
- : failed
136
- ? errorText
137
- : results.length === 0
138
- ? emptyText
139
- : resultsText(results.length),
140
- );
141
- const describedby = $derived(
142
- [error ? errorId : '', description ? hintId : ''].filter(Boolean).join(' ') ||
143
- undefined,
144
- );
145
- const controlId = $derived(
146
- interaction === false ? undefined : (interaction?.id ?? name),
147
- );
148
- const canClear = $derived(!required && !disabled && value !== '');
149
- const newLabel = $derived(createLabel ?? `New ${label.toLowerCase()}`);
150
-
151
- async function runSearch(query: string) {
152
- const seq = ++searchSeq;
153
- loading = true;
154
- failed = false;
155
- try {
156
- const found = await search(query);
157
- if (seq !== searchSeq) return;
158
- results = found;
159
- } catch (err) {
160
- if (seq !== searchSeq) return;
161
- results = [];
162
- failed = true;
163
- logger.warn('[smrt-svelte] RelationInput search failed', {
164
- error: String(err),
165
- });
166
- } finally {
167
- if (seq === searchSeq) loading = false;
168
- }
169
- }
170
-
171
- function handleQuery(query: string) {
172
- clearTimeout(timer);
173
- lastQuery = query;
174
- if (query === '') {
175
- // Opening the list: no typing to wait for.
176
- void runSearch('');
177
- return;
178
- }
179
- // Show "Searching" straight away and drop any in-flight older request. The
180
- // listed options answered an older query, so none stay selectable (Enter or
181
- // a click would otherwise pick a record that no longer matches the text).
182
- searchSeq++;
183
- loading = true;
184
- failed = false;
185
- results = [];
186
- timer = setTimeout(() => void runSearch(query), debounceMs);
187
- }
188
-
189
- function handleValueChange(next: string) {
190
- lastQuery = '';
191
- const hit = results.find((item) => item.id === next);
192
- if (hit) {
193
- selectedLabel = hit.label;
194
- labelledId = next;
195
- }
196
- onchange?.(next);
197
- }
198
-
199
- function focusInput() {
200
- rootEl?.querySelector<HTMLInputElement>('input[role="combobox"]')?.focus();
201
- }
202
-
203
- function clear() {
204
- if (!canClear) return;
205
- value = '';
206
- selectedLabel = '';
207
- labelledId = '';
208
- lastQuery = '';
209
- onchange?.('');
210
- recordControlUserEdit(
211
- interactionContext,
212
- controlId,
213
- interaction === false ? undefined : interaction?.subject,
214
- );
215
- if (rootEl) emitControlChange(rootEl);
216
- focusInput();
217
- }
218
-
219
- async function create() {
220
- if (!onCreate || creating || disabled) return;
221
- // The field has already lost focus (and reset its text) by now, so use the
222
- // text most recently searched for.
223
- const typed = lastQuery;
224
- creating = true;
225
- try {
226
- const made = await onCreate(typed);
227
- if (made && typeof made === 'object') {
228
- results = [made, ...results.filter((item) => item.id !== made.id)];
229
- selectedLabel = made.label;
230
- labelledId = made.id;
231
- value = made.id;
232
- onchange?.(made.id);
233
- recordControlUserEdit(
234
- interactionContext,
235
- controlId,
236
- interaction === false ? undefined : interaction?.subject,
237
- );
238
- if (rootEl) emitControlChange(rootEl);
239
- }
240
- } catch (err) {
241
- logger.warn('[smrt-svelte] RelationInput onCreate failed', {
242
- error: String(err),
243
- });
244
- } finally {
245
- creating = false;
246
- }
247
- }
248
-
249
- // Keep the visible label in step with `value` however it changed (initial
250
- // value, parent rebind, form reset): known from a search, else resolved.
251
- $effect(() => {
252
- const id = value;
253
- const known = results.find((item) => item.id === id);
254
- untrack(() => {
255
- if (!id) {
256
- selectedLabel = '';
257
- labelledId = '';
258
- return;
259
- }
260
- if (id === labelledId) return;
261
- if (known) {
262
- selectedLabel = known.label;
263
- labelledId = id;
264
- return;
265
- }
266
- // A different id than the one labelled: never keep the previous label.
267
- selectedLabel = '';
268
- labelledId = '';
269
- if (!resolve || resolvingId === id) return;
270
- resolvingId = id;
271
- resolve(id)
272
- .then((record) => {
273
- if (value === id && record) {
274
- selectedLabel = record.label;
275
- labelledId = id;
276
- }
277
- })
278
- .catch((err) => {
279
- logger.warn('[smrt-svelte] RelationInput resolve failed', {
280
- error: String(err),
281
- });
282
- })
283
- .finally(() => {
284
- if (resolvingId === id) resolvingId = '';
285
- });
286
- });
287
- });
288
-
289
- $effect(() => () => clearTimeout(timer));
290
- </script>
291
-
292
- <div class="relation-input" bind:this={rootEl}>
293
- <div class="field">
294
- <Combobox
295
- {name}
296
- {label}
297
- {options}
298
- {placeholder}
299
- {disabled}
300
- {required}
301
- {interaction}
302
- {status}
303
- {describedby}
304
- bind:value
305
- valueLabel={selectedLabel}
306
- filter={false}
307
- busy={loading}
308
- invalid={!!error}
309
- onquery={handleQuery}
310
- onvaluechange={handleValueChange}
311
- >
312
- {#snippet optionContent(option)}
313
- <span class="option-label">{option.label}</span>
314
- {#if detailById.get(String(option.value))}
315
- <span class="option-detail">{detailById.get(String(option.value))}</span>
316
- {/if}
317
- {/snippet}
318
- </Combobox>
319
- {#if canClear}
320
- <Button
321
- variant="ghost"
322
- size="sm"
323
- class="clear"
324
- aria-label={clearLabel ?? `Clear ${label}`}
325
- onclick={clear}
326
- >
327
- &times;
328
- </Button>
329
- {/if}
330
- </div>
331
- {#if description}<p id={hintId} class="hint">{description}</p>{/if}
332
- {#if onCreate}
333
- <Button
334
- variant="ghost"
335
- size="sm"
336
- class="create"
337
- disabled={disabled || creating}
338
- onclick={create}
339
- >
340
- + {newLabel}
341
- </Button>
342
- {/if}
343
- {#if error}<p id={errorId} class="error" role="alert">{error}</p>{/if}
344
- </div>
345
-
346
- <style>
347
- .relation-input {
348
- display: grid;
349
- gap: var(--smrt-spacing-1, 4px);
350
- min-width: 0;
351
- }
352
- .field {
353
- position: relative;
354
- }
355
- .field :global(.clear) {
356
- position: absolute;
357
- right: var(--smrt-spacing-1, 4px);
358
- bottom: var(--smrt-spacing-1, 4px);
359
- }
360
- .option-label,
361
- .option-detail {
362
- display: block;
363
- }
364
- .option-detail {
365
- font-size: var(--smrt-typography-body-small-size, 0.75rem);
366
- color: var(--smrt-color-on-surface-variant);
367
- }
368
- .hint,
369
- .error {
370
- margin: 0;
371
- font-size: var(--smrt-typography-body-small-size, 0.75rem);
372
- }
373
- .hint {
374
- color: var(--smrt-color-on-surface-variant);
375
- }
376
- .error {
377
- color: var(--smrt-color-error);
378
- }
379
- </style>
@@ -1,75 +0,0 @@
1
- import { type ControlInteractionOptions } from '@happyvertical/smrt-ui/forms';
2
- import type { RelationOption } from './types.js';
3
- /**
4
- * RelationInput - searchable single-select for relation (foreign key) fields.
5
- *
6
- * Presentation only: the caller supplies `search` (and optionally `resolve`),
7
- * so the same lookup an assistant uses to turn "Acme" into a record id backs
8
- * the picker. Built on the smrt-ui `Combobox`, which supplies the ARIA 1.2
9
- * combobox pattern (arrow keys, Enter, Escape, `aria-activedescendant`) and
10
- * registers with the rich Form's control registry. The form posts the
11
- * committed id under `name`, never the visible label.
12
- *
13
- * @example
14
- * ```svelte
15
- * <RelationInput
16
- * name="customerId"
17
- * label="Customer"
18
- * bind:value={customerId}
19
- * search={(q) => customers.search(q)}
20
- * resolve={(id) => customers.byId(id)}
21
- * />
22
- * ```
23
- */
24
- export interface Props {
25
- /** Field name; the selected id posts under it. */
26
- name: string;
27
- /** Visible label. */
28
- label: string;
29
- /** Selected record id (bindable). Empty string means no selection. */
30
- value?: string;
31
- /** Required field: hides the clear button and flags an empty value. */
32
- required?: boolean;
33
- /** Disabled state. */
34
- disabled?: boolean;
35
- /** Validation error message, shown below the field. */
36
- error?: string;
37
- /** Placeholder shown when nothing is selected. */
38
- placeholder?: string;
39
- /** Hint announced with the field. */
40
- description?: string;
41
- /** Look up records matching the text; called with '' when the list opens. */
42
- search: (query: string) => Promise<RelationOption[]>;
43
- /** Look up the current value's label when it has not been searched yet. */
44
- resolve?: (id: string) => Promise<{
45
- id: string;
46
- label: string;
47
- } | null>;
48
- /**
49
- * "New ..." action. Receives the text most recently searched for. May
50
- * return the created record, which is then selected.
51
- */
52
- onCreate?: (query: string) => void | RelationOption | Promise<void | RelationOption>;
53
- /** Milliseconds to wait after typing before searching. */
54
- debounceMs?: number;
55
- /** Combobox interaction options (stable id, subject) for agents. */
56
- interaction?: ControlInteractionOptions | false;
57
- /** Text for the "New ..." button. */
58
- createLabel?: string;
59
- /** Accessible name for the clear button. */
60
- clearLabel?: string;
61
- /** Status text while searching. */
62
- loadingText?: string;
63
- /** Status text when nothing matches. */
64
- emptyText?: string;
65
- /** Status text when the search fails. */
66
- errorText?: string;
67
- /** Status text announcing how many results are listed. */
68
- resultsText?: (count: number) => string;
69
- /** Called when the selected id changes ('' when cleared). */
70
- onchange?: (value: string) => void;
71
- }
72
- declare const RelationInput: import("svelte").Component<Props, {}, "value">;
73
- type RelationInput = ReturnType<typeof RelationInput>;
74
- export default RelationInput;
75
- //# sourceMappingURL=RelationInput.svelte.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"RelationInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/RelationInput.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,yBAAyB,EAI/B,MAAM,8BAA8B,CAAC;AAGtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGjD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,KAAK;IACpB,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sBAAsB;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IACrD,2EAA2E;IAC3E,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACxE;;;OAGG;IACH,QAAQ,CAAC,EAAE,CACT,KAAK,EAAE,MAAM,KACV,IAAI,GAAG,cAAc,GAAG,OAAO,CAAC,IAAI,GAAG,cAAc,CAAC,CAAC;IAC5D,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oEAAoE;IACpE,WAAW,CAAC,EAAE,yBAAyB,GAAG,KAAK,CAAC;IAChD,qCAAqC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACxC,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAuPD,QAAA,MAAM,aAAa,gDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}