@happyvertical/smrt-fields 0.40.59 → 0.40.61

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 (184) hide show
  1. package/AGENTS.md +58 -12
  2. package/README.md +90 -2
  3. package/dist/__smrt-register__.d.ts +2 -0
  4. package/dist/__smrt-register__.d.ts.map +1 -0
  5. package/dist/cache.d.ts +18 -0
  6. package/dist/cache.d.ts.map +1 -0
  7. package/dist/collections/FieldPolicyCollection.d.ts +95 -0
  8. package/dist/collections/FieldPolicyCollection.d.ts.map +1 -0
  9. package/dist/field-definitions.d.ts +77 -0
  10. package/dist/field-definitions.d.ts.map +1 -0
  11. package/dist/field-policy-resolver.d.ts +46 -0
  12. package/dist/field-policy-resolver.d.ts.map +1 -0
  13. package/dist/index.d.ts +9 -532
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +752 -55
  16. package/dist/index.js.map +1 -1
  17. package/dist/manifest.json +45 -3
  18. package/dist/models/FieldPolicy.d.ts +163 -0
  19. package/dist/models/FieldPolicy.d.ts.map +1 -0
  20. package/dist/permissions.d.ts +9 -0
  21. package/dist/permissions.d.ts.map +1 -0
  22. package/dist/playground.d.ts +2 -0
  23. package/dist/playground.d.ts.map +1 -0
  24. package/dist/playground.js +39 -0
  25. package/dist/playground.js.map +1 -0
  26. package/dist/settings-catalog.d.ts +73 -0
  27. package/dist/settings-catalog.d.ts.map +1 -0
  28. package/dist/smrt-knowledge.json +50 -7
  29. package/dist/svelte/__tests__/FieldPolicyControlPanel.test.js +223 -0
  30. package/dist/svelte/__tests__/FieldPolicyGear.test.js +503 -0
  31. package/dist/svelte/__tests__/FormHelp.test.js +138 -0
  32. package/dist/svelte/__tests__/ModeUX.test.js +109 -0
  33. package/dist/svelte/__tests__/ObjectForm.test.js +556 -0
  34. package/dist/svelte/__tests__/ObjectFormGeneratedApi.integration.test.js +436 -0
  35. package/dist/svelte/__tests__/PolicyField.test.js +373 -0
  36. package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte +30 -0
  37. package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte.d.ts +9 -0
  38. package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte.d.ts.map +1 -0
  39. package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte +33 -0
  40. package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte.d.ts +10 -0
  41. package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte.d.ts.map +1 -0
  42. package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte +19 -0
  43. package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte.d.ts +10 -0
  44. package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte.d.ts.map +1 -0
  45. package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte +18 -0
  46. package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte.d.ts +11 -0
  47. package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte.d.ts.map +1 -0
  48. package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte +11 -0
  49. package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte.d.ts +19 -0
  50. package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte.d.ts.map +1 -0
  51. package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte +18 -0
  52. package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte.d.ts +8 -0
  53. package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte.d.ts.map +1 -0
  54. package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte +24 -0
  55. package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte.d.ts +13 -0
  56. package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte.d.ts.map +1 -0
  57. package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte +20 -0
  58. package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts +11 -0
  59. package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts.map +1 -0
  60. package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte +17 -0
  61. package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte.d.ts +10 -0
  62. package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte.d.ts.map +1 -0
  63. package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte +25 -0
  64. package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte.d.ts +10 -0
  65. package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte.d.ts.map +1 -0
  66. package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte +34 -0
  67. package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte.d.ts +10 -0
  68. package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte.d.ts.map +1 -0
  69. package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte +29 -0
  70. package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte.d.ts +11 -0
  71. package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte.d.ts.map +1 -0
  72. package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte +33 -0
  73. package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte.d.ts +9 -0
  74. package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte.d.ts.map +1 -0
  75. package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte +40 -0
  76. package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte.d.ts +8 -0
  77. package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte.d.ts.map +1 -0
  78. package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte +26 -0
  79. package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte.d.ts +13 -0
  80. package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte.d.ts.map +1 -0
  81. package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte +39 -0
  82. package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte.d.ts +17 -0
  83. package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte.d.ts.map +1 -0
  84. package/dist/svelte/__tests__/fixtures/RegistryInput.svelte +15 -0
  85. package/dist/svelte/__tests__/fixtures/RegistryInput.svelte.d.ts +5 -0
  86. package/dist/svelte/__tests__/fixtures/RegistryInput.svelte.d.ts.map +1 -0
  87. package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte +29 -0
  88. package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte.d.ts +14 -0
  89. package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte.d.ts.map +1 -0
  90. package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte +24 -0
  91. package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte.d.ts +14 -0
  92. package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte.d.ts.map +1 -0
  93. package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte +28 -0
  94. package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte.d.ts +13 -0
  95. package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte.d.ts.map +1 -0
  96. package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte +14 -0
  97. package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte.d.ts +19 -0
  98. package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte.d.ts.map +1 -0
  99. package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte +25 -0
  100. package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte.d.ts +14 -0
  101. package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte.d.ts.map +1 -0
  102. package/dist/svelte/__tests__/settings-catalog.test.js +161 -0
  103. package/dist/svelte/components/AdvancedFields.svelte +49 -0
  104. package/dist/svelte/components/AdvancedFields.svelte.d.ts +17 -0
  105. package/dist/svelte/components/AdvancedFields.svelte.d.ts.map +1 -0
  106. package/dist/svelte/components/FieldInput.svelte +159 -0
  107. package/dist/svelte/components/FieldInput.svelte.d.ts +5 -0
  108. package/dist/svelte/components/FieldInput.svelte.d.ts.map +1 -0
  109. package/dist/svelte/components/FieldPolicyControlPanel.svelte +261 -0
  110. package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts +26 -0
  111. package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts.map +1 -0
  112. package/dist/svelte/components/FieldPolicyEditor.svelte +417 -0
  113. package/dist/svelte/components/FieldPolicyEditor.svelte.d.ts +18 -0
  114. package/dist/svelte/components/FieldPolicyEditor.svelte.d.ts.map +1 -0
  115. package/dist/svelte/components/FieldPolicyGearButton.svelte +17 -0
  116. package/dist/svelte/components/FieldPolicyGearButton.svelte.d.ts +8 -0
  117. package/dist/svelte/components/FieldPolicyGearButton.svelte.d.ts.map +1 -0
  118. package/dist/svelte/components/FieldPolicyGearProvider.svelte +135 -0
  119. package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts +17 -0
  120. package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts.map +1 -0
  121. package/dist/svelte/components/FieldPolicyProvider.svelte +89 -0
  122. package/dist/svelte/components/FieldPolicyProvider.svelte.d.ts +35 -0
  123. package/dist/svelte/components/FieldPolicyProvider.svelte.d.ts.map +1 -0
  124. package/dist/svelte/components/FormHelp.svelte +157 -0
  125. package/dist/svelte/components/FormHelp.svelte.d.ts +15 -0
  126. package/dist/svelte/components/FormHelp.svelte.d.ts.map +1 -0
  127. package/dist/svelte/components/ModeSwitch.svelte +47 -0
  128. package/dist/svelte/components/ModeSwitch.svelte.d.ts +10 -0
  129. package/dist/svelte/components/ModeSwitch.svelte.d.ts.map +1 -0
  130. package/dist/svelte/components/ObjectForm.svelte +351 -0
  131. package/dist/svelte/components/ObjectForm.svelte.d.ts +40 -0
  132. package/dist/svelte/components/ObjectForm.svelte.d.ts.map +1 -0
  133. package/dist/svelte/components/ObjectFormSourceProvider.svelte +18 -0
  134. package/dist/svelte/components/ObjectFormSourceProvider.svelte.d.ts +11 -0
  135. package/dist/svelte/components/ObjectFormSourceProvider.svelte.d.ts.map +1 -0
  136. package/dist/svelte/components/PolicyField.svelte +338 -0
  137. package/dist/svelte/components/PolicyField.svelte.d.ts +80 -0
  138. package/dist/svelte/components/PolicyField.svelte.d.ts.map +1 -0
  139. package/dist/svelte/context.svelte.d.ts +54 -0
  140. package/dist/svelte/context.svelte.d.ts.map +1 -0
  141. package/dist/svelte/context.svelte.js +55 -0
  142. package/dist/svelte/data-table.d.ts +17 -0
  143. package/dist/svelte/data-table.d.ts.map +1 -0
  144. package/dist/svelte/data-table.js +19 -0
  145. package/dist/svelte/field-policy-editor.d.ts +81 -0
  146. package/dist/svelte/field-policy-editor.d.ts.map +1 -0
  147. package/dist/svelte/field-policy-editor.js +120 -0
  148. package/dist/svelte/gear-context.svelte.d.ts +14 -0
  149. package/dist/svelte/gear-context.svelte.d.ts.map +1 -0
  150. package/dist/svelte/gear-context.svelte.js +15 -0
  151. package/dist/svelte/index.d.ts +58 -0
  152. package/dist/svelte/index.d.ts.map +1 -0
  153. package/dist/svelte/index.js +38 -0
  154. package/dist/svelte/input-registry.d.ts +21 -0
  155. package/dist/svelte/input-registry.d.ts.map +1 -0
  156. package/dist/svelte/input-registry.js +26 -0
  157. package/dist/svelte/object-form-source-context.svelte.d.ts +4 -0
  158. package/dist/svelte/object-form-source-context.svelte.d.ts.map +1 -0
  159. package/dist/svelte/object-form-source-context.svelte.js +8 -0
  160. package/dist/svelte/object-form-source.svelte.d.ts +18 -0
  161. package/dist/svelte/object-form-source.svelte.d.ts.map +1 -0
  162. package/dist/svelte/object-form-source.svelte.js +99 -0
  163. package/dist/svelte/object-form.d.ts +6 -0
  164. package/dist/svelte/object-form.d.ts.map +1 -0
  165. package/dist/svelte/object-form.js +38 -0
  166. package/dist/svelte/playground/FieldPolicyFormPreview.svelte +212 -0
  167. package/dist/svelte/playground/FieldPolicyFormPreview.svelte.d.ts +4 -0
  168. package/dist/svelte/playground/FieldPolicyFormPreview.svelte.d.ts.map +1 -0
  169. package/dist/svelte/playground/ObjectFormPreview.svelte +129 -0
  170. package/dist/svelte/playground/ObjectFormPreview.svelte.d.ts +4 -0
  171. package/dist/svelte/playground/ObjectFormPreview.svelte.d.ts.map +1 -0
  172. package/dist/svelte/playground.d.ts +20 -0
  173. package/dist/svelte/playground.d.ts.map +1 -0
  174. package/dist/svelte/playground.js +38 -0
  175. package/dist/svelte/settings-catalog.d.ts +54 -0
  176. package/dist/svelte/settings-catalog.d.ts.map +1 -0
  177. package/dist/svelte/settings-catalog.js +116 -0
  178. package/dist/svelte/types.d.ts +101 -0
  179. package/dist/svelte/types.d.ts.map +1 -0
  180. package/dist/svelte/types.js +4 -0
  181. package/dist/types.d.ts +276 -203
  182. package/dist/types.d.ts.map +1 -0
  183. package/dist/types.js.map +1 -1
  184. package/package.json +33 -8
@@ -0,0 +1,417 @@
1
+ <script lang="ts">
2
+ /** Accessible, transport-neutral editor for context-derived policy rows. */
3
+
4
+ import { Modal } from '@happyvertical/smrt-ui/feedback';
5
+ import { Input, Select, Toggle } from '@happyvertical/smrt-ui/forms';
6
+ import { Button } from '@happyvertical/smrt-ui/ui';
7
+ import { type Component, untrack } from 'svelte';
8
+ import type {
9
+ FieldPolicyEditorState,
10
+ FieldPolicyScopeType,
11
+ } from '../../types.js';
12
+ import {
13
+ defaultValueSerializationError,
14
+ draftFromRow,
15
+ type FieldPolicyEditorAdapter,
16
+ type FieldPolicyEditorDraft,
17
+ type FieldPolicyEditorTab,
18
+ type FieldPolicyOrganizationScope,
19
+ mutationFromDraft,
20
+ requiredVisibilityIsInvalid,
21
+ rowForScope,
22
+ } from '../field-policy-editor.js';
23
+ import type {
24
+ FieldInputComponent,
25
+ FieldInputRegistry,
26
+ } from '../input-registry.js';
27
+ import type { FieldInputProps, ObjectFormFieldDefinition } from '../types.js';
28
+ import FieldInput from './FieldInput.svelte';
29
+
30
+ interface Props {
31
+ state: FieldPolicyEditorState;
32
+ adapter: FieldPolicyEditorAdapter;
33
+ fields: Readonly<Record<string, ObjectFormFieldDefinition>>;
34
+ inputRegistry?: FieldInputRegistry;
35
+ organizationScope?: FieldPolicyOrganizationScope;
36
+ onclose: () => void;
37
+ onmutated: () => Promise<void>;
38
+ }
39
+
40
+ let {
41
+ state: editorState,
42
+ adapter,
43
+ fields,
44
+ inputRegistry,
45
+ organizationScope = 'tenant',
46
+ onclose,
47
+ onmutated,
48
+ }: Props = $props();
49
+
50
+ let tab = $state<FieldPolicyEditorTab>(
51
+ untrack(() =>
52
+ editorState.capabilities.manage ? 'organization' : 'personal',
53
+ ),
54
+ );
55
+ let drafts = $state<Record<string, FieldPolicyEditorDraft>>({});
56
+ let saving = $state<string | null>(null);
57
+ let error = $state<string | null>(null);
58
+ let invalidDefaults = $state<Set<string>>(new Set());
59
+ const tabInstanceId = $props.id();
60
+
61
+ const scope = $derived<FieldPolicyScopeType>(
62
+ tab === 'organization' ? organizationScope : 'user',
63
+ );
64
+ const availableTabs = $derived<FieldPolicyEditorTab[]>([
65
+ ...(editorState.capabilities.manage ? (['organization'] as const) : []),
66
+ ...(editorState.capabilities.personalize ? (['personal'] as const) : []),
67
+ ]);
68
+ const orderedFields = $derived(
69
+ Object.values(editorState.policy.fields).sort(
70
+ (left, right) =>
71
+ (left.order ?? Number.MAX_SAFE_INTEGER) -
72
+ (right.order ?? Number.MAX_SAFE_INTEGER) ||
73
+ left.fieldName.localeCompare(right.fieldName),
74
+ ),
75
+ );
76
+
77
+ function tabId(tabName: FieldPolicyEditorTab): string {
78
+ return `field-policy-tab-${tabInstanceId}-${tabName}`;
79
+ }
80
+
81
+ function tabPanelId(tabName: FieldPolicyEditorTab): string {
82
+ return `field-policy-panel-${tabInstanceId}-${tabName}`;
83
+ }
84
+
85
+ function selectTab(nextTab: FieldPolicyEditorTab, focus = false): void {
86
+ tab = nextTab;
87
+ if (focus) {
88
+ // The selected button receives tabindex=0 after this reactive update.
89
+ requestAnimationFrame(() => {
90
+ document.getElementById(tabId(nextTab))?.focus();
91
+ });
92
+ }
93
+ }
94
+
95
+ function handleTabKeydown(
96
+ event: KeyboardEvent,
97
+ currentTab: FieldPolicyEditorTab,
98
+ ): void {
99
+ const currentIndex = availableTabs.indexOf(currentTab);
100
+ if (currentIndex === -1 || availableTabs.length < 2) return;
101
+
102
+ let nextIndex: number | null = null;
103
+ switch (event.key) {
104
+ case 'ArrowRight':
105
+ nextIndex = (currentIndex + 1) % availableTabs.length;
106
+ break;
107
+ case 'ArrowLeft':
108
+ nextIndex =
109
+ (currentIndex - 1 + availableTabs.length) % availableTabs.length;
110
+ break;
111
+ case 'Home':
112
+ nextIndex = 0;
113
+ break;
114
+ case 'End':
115
+ nextIndex = availableTabs.length - 1;
116
+ break;
117
+ default:
118
+ return;
119
+ }
120
+
121
+ event.preventDefault();
122
+ const nextTab = availableTabs[nextIndex];
123
+ if (nextTab) selectTab(nextTab, true);
124
+ }
125
+
126
+ function draftFor(fieldName: string): FieldPolicyEditorDraft {
127
+ const key = `${scope}:${fieldName}`;
128
+ return (
129
+ drafts[key] ?? draftFromRow(rowForScope(editorState, fieldName, scope))
130
+ );
131
+ }
132
+
133
+ function setDraft(
134
+ fieldName: string,
135
+ patch: Partial<FieldPolicyEditorDraft>,
136
+ ): void {
137
+ const key = `${scope}:${fieldName}`;
138
+ drafts[key] = { ...draftFor(fieldName), ...patch };
139
+ }
140
+
141
+ function defaultValueFor(fieldName: string): unknown {
142
+ const draft = draftFor(fieldName);
143
+ if (draft.defaultEnabled) return draft.defaultValue;
144
+ const row = rowForScope(editorState, fieldName, scope);
145
+ // Clearing a current row must not count its own resolved default. Replay
146
+ // only lower layers, preserving explicit null as a present (unusable) default.
147
+ if (row?.defaultValue != null) {
148
+ // Personal callers intentionally receive no org rows/layers. The server
149
+ // supplies only whether the hidden code/app/tenant fallback is usable,
150
+ // which is enough to enforce the required-field invariant without
151
+ // disclosing any lower-layer default value.
152
+ if (scope === 'user') {
153
+ return editorState.personalLowerDefaultUsable[fieldName]
154
+ ? true
155
+ : undefined;
156
+ }
157
+ const definition = fields[fieldName];
158
+ let fallback: unknown =
159
+ definition && Object.hasOwn(definition, 'default')
160
+ ? definition.default
161
+ : undefined;
162
+ for (const layer of editorState.policy.layers[fieldName] ?? []) {
163
+ if (scope === 'app' && layer.layer !== 'code') continue;
164
+ if (scope === 'tenant' && layer.layer === 'user') continue;
165
+ if (
166
+ scope === 'tenant' &&
167
+ layer.layer === 'tenant' &&
168
+ layer.tenantId === row.tenantId
169
+ )
170
+ continue;
171
+ if (layer.delta.default) fallback = layer.delta.default.value;
172
+ }
173
+ return fallback;
174
+ }
175
+ return editorState.policy.fields[fieldName].defaultValue;
176
+ }
177
+
178
+ function userLocked(fieldName: string): boolean {
179
+ return scope === 'user' && editorState.policy.fields[fieldName].locked;
180
+ }
181
+
182
+ function invalidRequiredDemotion(fieldName: string): boolean {
183
+ const field = editorState.policy.fields[fieldName];
184
+ const draft = draftFor(fieldName);
185
+ return requiredVisibilityIsInvalid(
186
+ field.required,
187
+ draft.visibility,
188
+ defaultValueFor(fieldName),
189
+ );
190
+ }
191
+
192
+ function defaultSerializationError(fieldName: string): string | null {
193
+ const draft = draftFor(fieldName);
194
+ if (draft.defaultEnabled && invalidDefaults.has(`${scope}:${fieldName}`)) {
195
+ return 'Enter valid JSON before saving this default.';
196
+ }
197
+ return draft.defaultEnabled
198
+ ? defaultValueSerializationError(draft.defaultValue)
199
+ : null;
200
+ }
201
+
202
+ function setDefaultValidity(
203
+ fieldScope: FieldPolicyScopeType,
204
+ fieldName: string,
205
+ valid: boolean,
206
+ ): void {
207
+ const next = new Set(invalidDefaults);
208
+ const key = `${fieldScope}:${fieldName}`;
209
+ if (valid) next.delete(key);
210
+ else next.add(key);
211
+ invalidDefaults = next;
212
+ }
213
+
214
+ function inputFor(fieldName: string): FieldInputComponent {
215
+ const definition = fields[fieldName];
216
+ return (
217
+ inputRegistry?.resolve(
218
+ editorState.policy.objectRef,
219
+ fieldName,
220
+ definition?.type ?? 'text',
221
+ ) ?? (FieldInput as unknown as Component<FieldInputProps>)
222
+ );
223
+ }
224
+
225
+ function editorField(fieldName: string) {
226
+ const definition = fields[fieldName] ?? { type: 'text' };
227
+ const policy = editorState.policy.fields[fieldName];
228
+ return {
229
+ name: fieldName,
230
+ definition,
231
+ group: policy.group,
232
+ order: policy.order,
233
+ };
234
+ }
235
+
236
+ async function save(fieldName: string): Promise<void> {
237
+ if (
238
+ userLocked(fieldName) ||
239
+ invalidRequiredDemotion(fieldName) ||
240
+ defaultSerializationError(fieldName)
241
+ ) {
242
+ return;
243
+ }
244
+ saving = fieldName;
245
+ error = null;
246
+ try {
247
+ const row = rowForScope(editorState, fieldName, scope);
248
+ const mutation = mutationFromDraft(
249
+ editorState.policy.objectRef,
250
+ fieldName,
251
+ scope,
252
+ draftFor(fieldName),
253
+ );
254
+ if (row) await adapter.update({ ...mutation, id: row.id });
255
+ else await adapter.create(mutation);
256
+ drafts = {};
257
+ await onmutated();
258
+ } catch (cause) {
259
+ error =
260
+ cause instanceof Error ? cause.message : 'Unable to save field settings.';
261
+ } finally {
262
+ saving = null;
263
+ }
264
+ }
265
+
266
+ async function reset(fieldName: string): Promise<void> {
267
+ const row = rowForScope(editorState, fieldName, scope);
268
+ if (!row || userLocked(fieldName)) return;
269
+ saving = fieldName;
270
+ error = null;
271
+ try {
272
+ await adapter.delete({ id: row.id });
273
+ drafts = {};
274
+ await onmutated();
275
+ } catch (cause) {
276
+ error =
277
+ cause instanceof Error
278
+ ? cause.message
279
+ : 'Unable to reset field settings.';
280
+ } finally {
281
+ saving = null;
282
+ }
283
+ }
284
+ </script>
285
+
286
+ <Modal open={true} onClose={onclose} title="Field settings" size="xl" ariaDescribedBy="field-policy-editor-description">
287
+ <div class="field-policy-editor">
288
+ <p id="field-policy-editor-description">Overrides are sparse. Reset removes this scope’s row and inherits the effective policy.</p>
289
+
290
+ <div class="field-policy-editor__tabs" role="tablist" aria-label="Field policy scope">
291
+ {#if editorState.capabilities.manage}
292
+ <Button
293
+ type="button"
294
+ role="tab"
295
+ id={tabId('organization')}
296
+ aria-selected={tab === 'organization'}
297
+ aria-controls={tabPanelId('organization')}
298
+ tabindex={tab === 'organization' ? 0 : -1}
299
+ onclick={() => selectTab('organization')}
300
+ onkeydown={(event: KeyboardEvent) => handleTabKeydown(event, 'organization')}
301
+ >
302
+ Organization
303
+ </Button>
304
+ {/if}
305
+ {#if editorState.capabilities.personalize}
306
+ <Button
307
+ type="button"
308
+ role="tab"
309
+ id={tabId('personal')}
310
+ aria-selected={tab === 'personal'}
311
+ aria-controls={tabPanelId('personal')}
312
+ tabindex={tab === 'personal' ? 0 : -1}
313
+ onclick={() => selectTab('personal')}
314
+ onkeydown={(event: KeyboardEvent) => handleTabKeydown(event, 'personal')}
315
+ >
316
+ Just me
317
+ </Button>
318
+ {/if}
319
+ </div>
320
+
321
+ {#if error}
322
+ <p class="field-policy-editor__error" role="alert">{error}</p>
323
+ {/if}
324
+
325
+ <div
326
+ role="tabpanel"
327
+ id={tabPanelId(tab)}
328
+ aria-labelledby={tabId(tab)}
329
+ aria-label={tab === 'organization' ? 'Organization settings' : 'Personal settings'}
330
+ >
331
+ {#each orderedFields as policyField (policyField.fieldName)}
332
+ {@const fieldName = policyField.fieldName}
333
+ {@const defaultScope = scope}
334
+ {@const draft = draftFor(fieldName)}
335
+ {@const InputComponent = inputFor(fieldName)}
336
+ <fieldset class="field-policy-editor__field" disabled={userLocked(fieldName) || saving === fieldName}>
337
+ <legend>{policyField.label ?? fieldName}</legend>
338
+ {#if userLocked(fieldName)}
339
+ <p class="field-policy-editor__notice">This field is locked by your organization and cannot be personalized.</p>
340
+ {/if}
341
+ {#if policyField.required}
342
+ <p id={`${fieldName}-required-invariant`} class="field-policy-editor__notice">
343
+ Required fields without a usable default must remain Basic.
344
+ </p>
345
+ {/if}
346
+ <label>
347
+ Visibility
348
+ <Select
349
+ value={draft.visibility ?? ''}
350
+ aria-describedby={policyField.required ? `${fieldName}-required-invariant` : undefined}
351
+ onchange={(event: Event) => setDraft(fieldName, { visibility: ((event.currentTarget as HTMLSelectElement).value || null) as FieldPolicyEditorDraft['visibility'] })}
352
+ >
353
+ <option value="">Inherit</option>
354
+ <option value="basic">Basic</option>
355
+ <option value="advanced">Advanced</option>
356
+ <option value="hidden">Hidden</option>
357
+ </Select>
358
+ </label>
359
+ {#if invalidRequiredDemotion(fieldName)}
360
+ <p class="field-policy-editor__error" role="status">Add a usable default before making this required field advanced or hidden.</p>
361
+ {/if}
362
+ <Toggle
363
+ checked={draft.defaultEnabled}
364
+ label="Override default value"
365
+ onchange={(value) => {
366
+ setDraft(fieldName, { defaultEnabled: value });
367
+ if (!value) setDefaultValidity(defaultScope, fieldName, true);
368
+ }}
369
+ />
370
+ {#if draft.defaultEnabled}
371
+ <label for={`${fieldName}-default`}>Default value</label>
372
+ {#key `${scope}:${fieldName}`}
373
+ <InputComponent
374
+ id={`${fieldName}-default`}
375
+ name={`${fieldName}-default`}
376
+ label={`Default ${policyField.label ?? fieldName}`}
377
+ field={editorField(fieldName)}
378
+ value={draft.defaultValue}
379
+ required={false}
380
+ disabled={false}
381
+ onvaluechange={(value) => setDraft(fieldName, { defaultValue: value })}
382
+ onvaliditychange={(valid) => setDefaultValidity(defaultScope, fieldName, valid)}
383
+ />
384
+ {/key}
385
+ {#if defaultSerializationError(fieldName)}
386
+ <p class="field-policy-editor__error" role="status">
387
+ {defaultSerializationError(fieldName)}
388
+ </p>
389
+ {/if}
390
+ {/if}
391
+ <label>Label <Input value={draft.label ?? ''} oninput={(event: Event) => setDraft(fieldName, { label: (event.currentTarget as HTMLInputElement).value })} /></label>
392
+ <label>Help <Input value={draft.help ?? ''} oninput={(event: Event) => setDraft(fieldName, { help: (event.currentTarget as HTMLInputElement).value })} /></label>
393
+ <label>Display order <Input type="number" value={draft.displayOrder ?? ''} oninput={(event: Event) => { const value = (event.currentTarget as HTMLInputElement).value; setDraft(fieldName, { displayOrder: value === '' ? null : Number(value) }); }} /></label>
394
+ {#if scope !== 'user'}
395
+ <Toggle checked={draft.locked === true} ariaLabel={`Lock ${policyField.label ?? fieldName}`} onchange={(value) => setDraft(fieldName, { locked: value ? true : null })} />
396
+ <span>Lock personal overrides</span>
397
+ {/if}
398
+ <div class="field-policy-editor__actions">
399
+ <Button type="button" onclick={() => save(fieldName)} disabled={invalidRequiredDemotion(fieldName) || !!defaultSerializationError(fieldName)}>{saving === fieldName ? 'Saving…' : 'Save'}</Button>
400
+ {#if rowForScope(editorState, fieldName, scope)}
401
+ <Button type="button" variant="ghost" onclick={() => reset(fieldName)}>Reset to inherited</Button>
402
+ {/if}
403
+ </div>
404
+ </fieldset>
405
+ {/each}
406
+ </div>
407
+ </div>
408
+ </Modal>
409
+
410
+ <style>
411
+ .field-policy-editor { background: var(--smrt-color-surface, white); color: var(--smrt-color-on-surface, #1f2937); display: grid; gap: var(--smrt-spacing-4, 1rem); max-block-size: min(90vh, 56rem); overflow: auto; padding: var(--smrt-spacing-5, 1.25rem); }
412
+ .field-policy-editor__actions, .field-policy-editor__tabs { align-items: center; display: flex; gap: var(--smrt-spacing-2, .5rem); justify-content: space-between; }
413
+ .field-policy-editor__field { border: 1px solid var(--smrt-color-outline, #d0d5dd); display: grid; gap: var(--smrt-spacing-3, .75rem); margin: 0 0 var(--smrt-spacing-3, .75rem); padding: var(--smrt-spacing-3, .75rem); }
414
+ .field-policy-editor__field label { display: grid; gap: var(--smrt-spacing-1, .25rem); }
415
+ .field-policy-editor__error { color: var(--smrt-color-error, #b42318); }
416
+ .field-policy-editor__notice { margin: 0; }
417
+ </style>
@@ -0,0 +1,18 @@
1
+ import { type Component } from 'svelte';
2
+ import type { FieldPolicyEditorState } from '../../types.js';
3
+ import { type FieldPolicyEditorAdapter, type FieldPolicyOrganizationScope } from '../field-policy-editor.js';
4
+ import type { FieldInputRegistry } from '../input-registry.js';
5
+ import type { ObjectFormFieldDefinition } from '../types.js';
6
+ interface Props {
7
+ state: FieldPolicyEditorState;
8
+ adapter: FieldPolicyEditorAdapter;
9
+ fields: Readonly<Record<string, ObjectFormFieldDefinition>>;
10
+ inputRegistry?: FieldInputRegistry;
11
+ organizationScope?: FieldPolicyOrganizationScope;
12
+ onclose: () => void;
13
+ onmutated: () => Promise<void>;
14
+ }
15
+ declare const FieldPolicyEditor: Component<Props, {}, "">;
16
+ type FieldPolicyEditor = ReturnType<typeof FieldPolicyEditor>;
17
+ export default FieldPolicyEditor;
18
+ //# sourceMappingURL=FieldPolicyEditor.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldPolicyEditor.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/FieldPolicyEditor.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,SAAS,EAAW,MAAM,QAAQ,CAAC;AACjD,OAAO,KAAK,EACV,sBAAsB,EAEvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAGL,KAAK,wBAAwB,EAG7B,KAAK,4BAA4B,EAIlC,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAEV,kBAAkB,EACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAmB,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAI9E,UAAU,KAAK;IACb,KAAK,EAAE,sBAAsB,CAAC;IAC9B,OAAO,EAAE,wBAAwB,CAAC;IAClC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC,CAAC;IAC5D,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,iBAAiB,CAAC,EAAE,4BAA4B,CAAC;IACjD,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAChC;AA4VD,QAAA,MAAM,iBAAiB,0BAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
@@ -0,0 +1,17 @@
1
+ <script lang="ts">
2
+ import { Button } from '@happyvertical/smrt-ui/ui';
3
+ import { getFieldPolicyGearContext } from '../gear-context.svelte.js';
4
+
5
+ let {
6
+ label = 'Field settings',
7
+ class: className = '',
8
+ }: { label?: string; class?: string } = $props();
9
+ const gear = getFieldPolicyGearContext();
10
+ </script>
11
+
12
+ {#if gear.canEdit}
13
+ <Button type="button" variant="ghost" class={className} aria-haspopup="dialog" onclick={() => gear.show()}>
14
+ <span aria-hidden="true">⚙</span>
15
+ <span>{label}</span>
16
+ </Button>
17
+ {/if}
@@ -0,0 +1,8 @@
1
+ type $$ComponentProps = {
2
+ label?: string;
3
+ class?: string;
4
+ };
5
+ declare const FieldPolicyGearButton: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type FieldPolicyGearButton = ReturnType<typeof FieldPolicyGearButton>;
7
+ export default FieldPolicyGearButton;
8
+ //# sourceMappingURL=FieldPolicyGearButton.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldPolicyGearButton.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/FieldPolicyGearButton.svelte.ts"],"names":[],"mappings":"AAMC,KAAK,gBAAgB,GAAI;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAqB7D,QAAA,MAAM,qBAAqB,sDAAwC,CAAC;AACpE,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACtE,eAAe,qBAAqB,CAAC"}
@@ -0,0 +1,135 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { FieldPolicyEditorState } from '../../types.js';
4
+ import {
5
+ editorStateErrorMessage,
6
+ type FieldPolicyEditorAdapter,
7
+ type FieldPolicyOrganizationScope,
8
+ isFieldPolicyEditorState,
9
+ } from '../field-policy-editor.js';
10
+ import {
11
+ type FieldPolicyGearController,
12
+ setFieldPolicyGearContext,
13
+ } from '../gear-context.svelte.js';
14
+ import type { FieldInputRegistry } from '../input-registry.js';
15
+ import type { ObjectFormFieldDefinition } from '../types.js';
16
+ import FieldPolicyEditor from './FieldPolicyEditor.svelte';
17
+
18
+ interface Props {
19
+ objectRef: string;
20
+ fields: Readonly<Record<string, ObjectFormFieldDefinition>>;
21
+ adapter: FieldPolicyEditorAdapter;
22
+ inputRegistry?: FieldInputRegistry;
23
+ /** Hosts in an app-wide context pass app; tenant hosts retain the default. */
24
+ organizationScope?: FieldPolicyOrganizationScope;
25
+ children?: Snippet;
26
+ }
27
+
28
+ let {
29
+ objectRef,
30
+ fields,
31
+ adapter,
32
+ inputRegistry,
33
+ organizationScope = 'tenant',
34
+ children,
35
+ }: Props = $props();
36
+
37
+ let editorState = $state<FieldPolicyEditorState | null>(null);
38
+ let error = $state<string | null>(null);
39
+ let open = $state(false);
40
+ let loading = $state(false);
41
+
42
+ const controller: FieldPolicyGearController = {
43
+ get state() {
44
+ return editorState;
45
+ },
46
+ get error() {
47
+ return error;
48
+ },
49
+ get open() {
50
+ return open;
51
+ },
52
+ get canEdit() {
53
+ return (
54
+ !!editorState &&
55
+ (editorState.capabilities.manage || editorState.capabilities.personalize)
56
+ );
57
+ },
58
+ show() {
59
+ if (this.canEdit) open = true;
60
+ },
61
+ hide() {
62
+ open = false;
63
+ },
64
+ reload,
65
+ };
66
+ setFieldPolicyGearContext(controller);
67
+
68
+ function isPermissionDenied(value: unknown): boolean {
69
+ return (
70
+ !!value &&
71
+ typeof value === 'object' &&
72
+ ((value as { status?: unknown }).status === 403 ||
73
+ (value as { code?: unknown }).code === 'permission_denied' ||
74
+ (value as { response?: { status?: unknown } }).response?.status === 403)
75
+ );
76
+ }
77
+
78
+ async function reload(generation = ++loadGeneration): Promise<void> {
79
+ loading = true;
80
+ error = null;
81
+ try {
82
+ const result = await adapter.load({ objectRef });
83
+ if (generation !== loadGeneration) return;
84
+ if (isFieldPolicyEditorState(result, objectRef)) {
85
+ editorState = result;
86
+ } else {
87
+ editorState = null;
88
+ error = isPermissionDenied(result)
89
+ ? null
90
+ : editorStateErrorMessage(result);
91
+ open = false;
92
+ }
93
+ } catch (cause) {
94
+ if (generation !== loadGeneration) return;
95
+ editorState = null;
96
+ error = isPermissionDenied(cause)
97
+ ? null
98
+ : cause instanceof Error
99
+ ? cause.message
100
+ : 'Unable to load field settings.';
101
+ open = false;
102
+ } finally {
103
+ if (generation === loadGeneration) loading = false;
104
+ }
105
+ }
106
+
107
+ let loadGeneration = 0;
108
+ $effect(() => {
109
+ const generation = ++loadGeneration;
110
+ editorState = null;
111
+ error = null;
112
+ open = false;
113
+ void reload(generation);
114
+ return () => {
115
+ // A delayed transport response must never repopulate a prior object.
116
+ if (loadGeneration === generation) loadGeneration++;
117
+ };
118
+ });
119
+ </script>
120
+
121
+ {@render children?.()}
122
+ {#if error && !open}
123
+ <p class="field-policy-gear__error" role="alert">{error}</p>
124
+ {/if}
125
+ {#if open && editorState}
126
+ <FieldPolicyEditor state={editorState} {adapter} {fields} {inputRegistry} {organizationScope} onclose={() => controller.hide()} onmutated={reload} />
127
+ {/if}
128
+ {#if loading}
129
+ <span class="field-policy-gear__loading" aria-live="polite">Loading field settings…</span>
130
+ {/if}
131
+
132
+ <style>
133
+ .field-policy-gear__error { color: var(--smrt-color-error, #b42318); }
134
+ .field-policy-gear__loading { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }
135
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type FieldPolicyEditorAdapter, type FieldPolicyOrganizationScope } from '../field-policy-editor.js';
3
+ import type { FieldInputRegistry } from '../input-registry.js';
4
+ import type { ObjectFormFieldDefinition } from '../types.js';
5
+ interface Props {
6
+ objectRef: string;
7
+ fields: Readonly<Record<string, ObjectFormFieldDefinition>>;
8
+ adapter: FieldPolicyEditorAdapter;
9
+ inputRegistry?: FieldInputRegistry;
10
+ /** Hosts in an app-wide context pass app; tenant hosts retain the default. */
11
+ organizationScope?: FieldPolicyOrganizationScope;
12
+ children?: Snippet;
13
+ }
14
+ declare const FieldPolicyGearProvider: import("svelte").Component<Props, {}, "">;
15
+ type FieldPolicyGearProvider = ReturnType<typeof FieldPolicyGearProvider>;
16
+ export default FieldPolicyGearProvider;
17
+ //# sourceMappingURL=FieldPolicyGearProvider.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldPolicyGearProvider.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/FieldPolicyGearProvider.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,EAEL,KAAK,wBAAwB,EAC7B,KAAK,4BAA4B,EAElC,MAAM,2BAA2B,CAAC;AAKnC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAI7D,UAAU,KAAK;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC,CAAC;IAC5D,OAAO,EAAE,wBAAwB,CAAC;IAClC,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,8EAA8E;IAC9E,iBAAiB,CAAC,EAAE,4BAA4B,CAAC;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAuHD,QAAA,MAAM,uBAAuB,2CAAwC,CAAC;AACtE,KAAK,uBAAuB,GAAG,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAC1E,eAAe,uBAAuB,CAAC"}