@svadmin/ui 0.88.1 → 0.92.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 (172) hide show
  1. package/dist/app.css +4 -0
  2. package/dist/app.theme.css +4 -0
  3. package/dist/component-registry.svelte.d.ts +2 -2
  4. package/dist/components/AboutSettings.svelte +23 -4
  5. package/dist/components/AdminApp.svelte +6 -4
  6. package/dist/components/AdminApp.svelte.d.ts +3 -3
  7. package/dist/components/ApiSettings.svelte +43 -27
  8. package/dist/components/ApprovalActionCard.svelte +8 -9
  9. package/dist/components/ApprovalCenter.svelte +24 -5
  10. package/dist/components/AuditLogViewer.svelte +2 -2
  11. package/dist/components/AutoComplete.svelte +610 -0
  12. package/dist/components/AutoComplete.svelte.d.ts +25 -0
  13. package/dist/components/AutoForm.svelte +265 -18
  14. package/dist/components/AutoForm.svelte.d.ts +19 -2
  15. package/dist/components/AutoTable.svelte +141 -36
  16. package/dist/components/AutoTable.svelte.d.ts +8 -0
  17. package/dist/components/BatchActionBar.svelte +42 -14
  18. package/dist/components/BatchActionBar.svelte.d.ts +5 -0
  19. package/dist/components/BoundRecordDetailDrawer.svelte +5 -3
  20. package/dist/components/CancelTaskButton.svelte +14 -1
  21. package/dist/components/Cascader.svelte +412 -53
  22. package/dist/components/Cascader.svelte.d.ts +7 -1
  23. package/dist/components/ColorPicker.svelte +1 -1
  24. package/dist/components/ColorPicker.svelte.d.ts +1 -1
  25. package/dist/components/ComboboxField.svelte +270 -34
  26. package/dist/components/ComboboxField.svelte.d.ts +4 -0
  27. package/dist/components/CommandPalette.svelte +4 -4
  28. package/dist/components/DashboardView.svelte +73 -7
  29. package/dist/components/DashboardView.svelte.d.ts +3 -1
  30. package/dist/components/DateRangeInput.svelte +8 -1
  31. package/dist/components/DetailWorkspaceAsyncSection.svelte +107 -0
  32. package/dist/components/DetailWorkspaceAsyncSection.svelte.d.ts +36 -0
  33. package/dist/components/DetailWorkspaceSections.svelte +233 -0
  34. package/dist/components/DetailWorkspaceSections.svelte.d.ts +9 -0
  35. package/dist/components/DrawerForm.svelte +78 -9
  36. package/dist/components/DrawerForm.svelte.d.ts +3 -3
  37. package/dist/components/ErrorBoundary.svelte +1 -1
  38. package/dist/components/ErrorComponent.svelte +19 -6
  39. package/dist/components/ErrorComponent.svelte.d.ts +1 -0
  40. package/dist/components/ErrorSummary.svelte +9 -4
  41. package/dist/components/FieldRenderer.svelte +203 -90
  42. package/dist/components/FieldRenderer.svelte.d.ts +16 -1
  43. package/dist/components/FileUpload.svelte +116 -51
  44. package/dist/components/FileUpload.svelte.d.ts +7 -27
  45. package/dist/components/FileUploadField.svelte +170 -0
  46. package/dist/components/FileUploadField.svelte.d.ts +21 -0
  47. package/dist/components/FilterBuilder.svelte +13 -7
  48. package/dist/components/FilterCollectionInput.svelte +9 -7
  49. package/dist/components/FilterCollectionInput.svelte.d.ts +3 -0
  50. package/dist/components/ForgotPasswordPage.svelte +1 -1
  51. package/dist/components/Header.svelte +1 -1
  52. package/dist/components/ImportWizard.svelte +197 -28
  53. package/dist/components/ImportWizard.svelte.d.ts +5 -1
  54. package/dist/components/InferencerPanel.svelte +4 -2
  55. package/dist/components/InfiniteList.svelte +52 -9
  56. package/dist/components/JsonSchemaForm.svelte +237 -42
  57. package/dist/components/JsonSchemaForm.svelte.d.ts +10 -3
  58. package/dist/components/KanbanBoard.svelte +43 -7
  59. package/dist/components/KanbanBoard.svelte.d.ts +2 -2
  60. package/dist/components/Layout.svelte +1 -1
  61. package/dist/components/LoginPage.svelte +1 -1
  62. package/dist/components/MetricStrip.svelte +8 -3
  63. package/dist/components/MetricStrip.svelte.d.ts +1 -0
  64. package/dist/components/ModalForm.svelte +85 -9
  65. package/dist/components/ModalForm.svelte.d.ts +3 -3
  66. package/dist/components/MoneyInput.svelte +5 -3
  67. package/dist/components/MoneyInput.svelte.d.ts +4 -2
  68. package/dist/components/MultiTabKeepAlive.svelte +502 -43
  69. package/dist/components/MultiTabKeepAlive.svelte.d.ts +40 -6
  70. package/dist/components/NotificationsSettings.svelte +10 -10
  71. package/dist/components/NumberInput.svelte +5 -3
  72. package/dist/components/NumberInput.svelte.d.ts +4 -2
  73. package/dist/components/NumericInput.svelte +277 -25
  74. package/dist/components/NumericInput.svelte.d.ts +3 -1
  75. package/dist/components/PercentInput.svelte +5 -3
  76. package/dist/components/PercentInput.svelte.d.ts +4 -2
  77. package/dist/components/PermissionMatrix.svelte +44 -8
  78. package/dist/components/PermissionMatrix.svelte.d.ts +1 -0
  79. package/dist/components/ProfilePage.svelte +38 -13
  80. package/dist/components/RadioGroup.svelte +203 -4
  81. package/dist/components/RadioGroup.svelte.d.ts +1 -1
  82. package/dist/components/Rate.svelte +3 -3
  83. package/dist/components/Rate.svelte.d.ts +2 -2
  84. package/dist/components/RecordDetailDrawer.svelte +1 -1
  85. package/dist/components/RecordDetailDrawer.svelte.d.ts +1 -1
  86. package/dist/components/RegisterPage.svelte +1 -1
  87. package/dist/components/RelationPicker.svelte +39 -11
  88. package/dist/components/ResourceAccessGuard.svelte +7 -2
  89. package/dist/components/RetryTaskButton.svelte +14 -1
  90. package/dist/components/RolesSettings.svelte +74 -28
  91. package/dist/components/SecuritySettings.svelte +66 -51
  92. package/dist/components/SegmentedControl.svelte +4 -2
  93. package/dist/components/SegmentedControl.svelte.d.ts +1 -1
  94. package/dist/components/Sidebar.svelte +1 -1
  95. package/dist/components/StepsForm.svelte +62 -10
  96. package/dist/components/StepsForm.svelte.d.ts +4 -2
  97. package/dist/components/SvadminLogo.svelte +1 -0
  98. package/dist/components/SvarDataGrid.svelte +42 -24
  99. package/dist/components/SvarDataGrid.svelte.d.ts +10 -10
  100. package/dist/components/SvarResourceRead.svelte +2 -0
  101. package/dist/components/SvarResourceTableView.svelte +4 -2
  102. package/dist/components/TagsInput.svelte +3 -3
  103. package/dist/components/TagsInput.svelte.d.ts +2 -2
  104. package/dist/components/TaskDetails.svelte +22 -16
  105. package/dist/components/TaskDetails.svelte.d.ts +1 -1
  106. package/dist/components/TaskList.svelte +57 -15
  107. package/dist/components/TaskQueueDrawer.svelte +54 -34
  108. package/dist/components/Toast.svelte +13 -2
  109. package/dist/components/Transfer.svelte +67 -5
  110. package/dist/components/Transfer.svelte.d.ts +4 -0
  111. package/dist/components/TreeSelect.svelte +337 -77
  112. package/dist/components/TreeSelect.svelte.d.ts +4 -0
  113. package/dist/components/TreeTable.svelte +6 -1
  114. package/dist/components/TwoFactorAuthPage.svelte +8 -4
  115. package/dist/components/UpdatePasswordPage.svelte +1 -1
  116. package/dist/components/VersionDiffViewer.svelte +9 -7
  117. package/dist/components/account/CompanyProfilePage.svelte +13 -12
  118. package/dist/components/account/MemberDirectory.svelte +14 -10
  119. package/dist/components/account/SettingsEnterprisePage.svelte +18 -18
  120. package/dist/components/content/AuditTimeline.svelte +8 -6
  121. package/dist/components/content/DataState.svelte +1 -1
  122. package/dist/components/content/FileList.svelte +44 -13
  123. package/dist/components/content/FileList.svelte.d.ts +4 -2
  124. package/dist/components/content/FileListRow.svelte +87 -0
  125. package/dist/components/content/FileListRow.svelte.d.ts +11 -0
  126. package/dist/components/content/ImportDropzone.svelte +109 -12
  127. package/dist/components/content/ImportDropzone.svelte.d.ts +1 -0
  128. package/dist/components/content/IntegrationCard.svelte +3 -3
  129. package/dist/components/content/IntegrationCard.svelte.d.ts +2 -0
  130. package/dist/components/content/MediaThumbnail.svelte +6 -2
  131. package/dist/components/detail-workspace.d.ts +60 -0
  132. package/dist/components/detail-workspace.js +3 -0
  133. package/dist/components/enterprise/json-schema-form.d.ts +1 -1
  134. package/dist/components/enterprise/recursive-schema-form.d.ts +15 -1
  135. package/dist/components/enterprise/recursive-schema-form.js +288 -14
  136. package/dist/components/enterprise/schema-form-references.d.ts +14 -0
  137. package/dist/components/enterprise/schema-form-references.js +316 -0
  138. package/dist/components/fields/ArrayField.svelte +178 -14
  139. package/dist/components/fields/ArrayField.svelte.d.ts +17 -3
  140. package/dist/components/fields/array-field-metadata.d.ts +4 -0
  141. package/dist/components/fields/array-field-metadata.js +36 -0
  142. package/dist/components/file-upload-reference.d.ts +11 -0
  143. package/dist/components/file-upload-reference.js +50 -0
  144. package/dist/components/file-upload-types.d.ts +32 -0
  145. package/dist/components/file-upload-types.js +1 -0
  146. package/dist/components/form-contract.upload-type-fixture.svelte +108 -0
  147. package/dist/components/form-contract.upload-type-fixture.svelte.d.ts +18 -0
  148. package/dist/components/json-schema-form-state.d.ts +19 -0
  149. package/dist/components/list-deletion.svelte.js +6 -1
  150. package/dist/components/list-state.svelte.d.ts +12 -1
  151. package/dist/components/list-state.svelte.js +56 -16
  152. package/dist/components/numeric-format.d.ts +44 -0
  153. package/dist/components/numeric-format.js +198 -0
  154. package/dist/components/record-detail.svelte.js +2 -0
  155. package/dist/components/saved-list-views.d.ts +9 -2
  156. package/dist/components/saved-list-views.js +42 -11
  157. package/dist/components/saved-list-views.svelte.d.ts +3 -0
  158. package/dist/components/saved-list-views.svelte.js +56 -7
  159. package/dist/components/task-action.svelte.d.ts +1 -0
  160. package/dist/components/task-action.svelte.js +55 -3
  161. package/dist/components/task-utils.d.ts +1 -0
  162. package/dist/components/task-utils.js +18 -0
  163. package/dist/components/ui/input/input.svelte +1 -1
  164. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  165. package/dist/components/ui/sheet/sheet.svelte +4 -1
  166. package/dist/components/version-history.d.ts +14 -0
  167. package/dist/components/version-history.js +32 -0
  168. package/dist/components/workspace-tab-state.d.ts +78 -0
  169. package/dist/components/workspace-tab-state.js +587 -0
  170. package/dist/index.d.ts +13 -1
  171. package/dist/index.js +4 -0
  172. package/package.json +5 -5
@@ -33,13 +33,13 @@
33
33
  let loading = $state(false);
34
34
  let saving = $state(false);
35
35
  let loaded = $state(false);
36
- let loadError = $state('');
37
- let saveError = $state('');
36
+ let loadError = $state(false);
37
+ let saveError = $state(false);
38
38
  let epoch = 0;
39
39
  async function load() {
40
40
  const provider = preferencesProvider;
41
41
  const request = ++epoch;
42
- loadError = ''; saveError = ''; loaded = false; saving = false;
42
+ loadError = false; saveError = false; loaded = false; saving = false;
43
43
  loading = Boolean(provider);
44
44
  if (!provider) return;
45
45
  try {
@@ -47,8 +47,8 @@
47
47
  if (request !== epoch) return;
48
48
  emailAlerts = { ...result.email }; pushAlerts = { ...result.push }; smsAlerts = { ...result.sms };
49
49
  loaded = true;
50
- } catch (caught) {
51
- if (request === epoch) loadError = caught instanceof Error ? caught.message : String(caught);
50
+ } catch {
51
+ if (request === epoch) loadError = true;
52
52
  } finally {
53
53
  if (request === epoch) loading = false;
54
54
  }
@@ -62,12 +62,12 @@
62
62
  const provider = preferencesProvider;
63
63
  if (!provider || !loaded || saving) return;
64
64
  const request = epoch;
65
- saving = true; saveError = '';
65
+ saving = true; saveError = false;
66
66
  try {
67
67
  await provider.save({ email: { ...emailAlerts }, push: { ...pushAlerts }, sms: { ...smsAlerts } });
68
68
  if (request === epoch) notification.success(i18n.t('common.updateSuccess'), 3000);
69
- } catch (caught) {
70
- if (request === epoch) saveError = caught instanceof Error ? caught.message : String(caught);
69
+ } catch {
70
+ if (request === epoch) saveError = true;
71
71
  } finally {
72
72
  if (request === epoch) saving = false;
73
73
  }
@@ -89,9 +89,9 @@
89
89
  {#if loading}
90
90
  <DataState state="loading" />
91
91
  {:else if loadError}
92
- <DataState state="error" description={loadError} retry={load} />
92
+ <DataState state="error" description={i18n.t('common.operationFailed')} retry={load} />
93
93
  {:else if saveError}
94
- <FeedbackNotice tone="danger" message={saveError} />
94
+ <FeedbackNotice tone="danger" message={i18n.t('common.operationFailed')} />
95
95
  {/if}
96
96
  {#if loaded || !preferencesProvider}
97
97
  <fieldset disabled={!preferencesProvider || saving} class="svadmin-u-b3542e058833">
@@ -1,9 +1,11 @@
1
1
  <script lang="ts">
2
2
  import NumericInput from './NumericInput.svelte';
3
+ import type { NumericInputLocale } from './numeric-format.js';
3
4
  import type { NumericInputMode } from './NumericInput.svelte';
4
5
 
5
6
  interface Props {
6
7
  value?: number | null;
8
+ locale?: NumericInputLocale | undefined;
7
9
  min?: number;
8
10
  max?: number;
9
11
  step?: number;
@@ -14,12 +16,12 @@
14
16
  disabled?: boolean;
15
17
  required?: boolean;
16
18
  invalid?: boolean;
17
- describedby?: string;
19
+ describedby?: string | undefined;
18
20
  class?: string;
19
21
  onchange?: (value: number | null) => void;
20
22
  }
21
23
 
22
- let props: Props = $props();
24
+ let { value = $bindable(null), ...props }: Props = $props();
23
25
  </script>
24
26
 
25
- <NumericInput {...props} mode={'number' satisfies NumericInputMode} />
27
+ <NumericInput {...props} bind:value mode={'number' satisfies NumericInputMode} />
@@ -1,5 +1,7 @@
1
+ import type { NumericInputLocale } from './numeric-format.js';
1
2
  interface Props {
2
3
  value?: number | null;
4
+ locale?: NumericInputLocale | undefined;
3
5
  min?: number;
4
6
  max?: number;
5
7
  step?: number;
@@ -10,10 +12,10 @@ interface Props {
10
12
  disabled?: boolean;
11
13
  required?: boolean;
12
14
  invalid?: boolean;
13
- describedby?: string;
15
+ describedby?: string | undefined;
14
16
  class?: string;
15
17
  onchange?: (value: number | null) => void;
16
18
  }
17
- declare const NumberInput: import("svelte").Component<Props, {}, "">;
19
+ declare const NumberInput: import("svelte").Component<Props, {}, "value">;
18
20
  type NumberInput = ReturnType<typeof NumberInput>;
19
21
  export default NumberInput;
@@ -1,6 +1,15 @@
1
1
  <script lang="ts">
2
2
  import { Input } from './ui/input/index.js';
3
3
  import { cn } from '../utils.js';
4
+ import { useTranslation } from '@svadmin/core/i18n';
5
+ import {
6
+ addNumericStep,
7
+ formatNumericEditingText,
8
+ formatNumericValue,
9
+ isNumericInputLocale,
10
+ parseNumericInput,
11
+ type NumericInputLocale,
12
+ } from './numeric-format.js';
4
13
 
5
14
  export type NumericInputMode = 'number' | 'currency' | 'percent';
6
15
 
@@ -9,6 +18,7 @@
9
18
  mode?: NumericInputMode;
10
19
  currency?: string;
11
20
  scale?: '100' | '1';
21
+ locale?: NumericInputLocale | undefined;
12
22
  min?: number;
13
23
  max?: number;
14
24
  step?: number;
@@ -19,7 +29,7 @@
19
29
  disabled?: boolean;
20
30
  required?: boolean;
21
31
  invalid?: boolean;
22
- describedby?: string;
32
+ describedby?: string | undefined;
23
33
  class?: string;
24
34
  onchange?: (value: number | null) => void;
25
35
  }
@@ -29,6 +39,7 @@
29
39
  mode = 'number',
30
40
  currency,
31
41
  scale = '100',
42
+ locale,
32
43
  min,
33
44
  max,
34
45
  step,
@@ -50,27 +61,138 @@
50
61
  : Math.min(20, Math.max(0, Math.trunc(precision))),
51
62
  );
52
63
 
64
+ const i18n = useTranslation();
65
+ const localeValue = $derived(isNumericInputLocale(locale) ? locale : undefined);
66
+ const localeRequested = $derived(locale !== undefined);
67
+ const localeUnsupported = $derived(localeRequested && localeValue === undefined);
68
+
69
+ let localeFocused = $state(false);
70
+ let localeDraft = $state<string | null>(null);
71
+ let localeInvalid = $state(false);
72
+ let observedValue = $state<number | null | undefined>(undefined);
73
+ let observedLocale = $state<string | NumericInputLocale | undefined>(undefined);
74
+ let localeInputElement = $state<HTMLInputElement | null>(null);
75
+ let localeComposing = $state(false);
76
+ let composedText: string | null = null;
77
+
78
+ const localeBoundaryInvalid = $derived.by(() => {
79
+ if (!localeRequested || value == null) return false;
80
+ if (!Number.isFinite(value) || !Number.isFinite(displayNumber)) return true;
81
+ const lower = min != null && Number.isFinite(min) ? min : undefined;
82
+ const upper = max != null && Number.isFinite(max) ? max : undefined;
83
+ return (lower !== undefined && value < lower) || (upper !== undefined && value > upper);
84
+ });
85
+
86
+ const localeErrorMessage = $derived(
87
+ localeUnsupported
88
+ ? i18n.t('validation.invalidFormat')
89
+ : localeInvalid || localeBoundaryInvalid || localeComposing
90
+ ? i18n.t('validation.invalidNumber')
91
+ : '',
92
+ );
93
+
94
+ const displayNumber = $derived.by(() => {
95
+ if (value == null || !Number.isFinite(value)) return null;
96
+ const scaled = mode === 'percent' && scale === '1' ? value * 100 : value;
97
+ return Number.isFinite(scaled) ? scaled : null;
98
+ });
99
+
53
100
  const displayValue = $derived.by(() => {
54
- if (value == null) return '';
55
- const displayed = mode === 'percent' && scale === '1' ? value * 100 : value;
56
- return normalizedPrecision === undefined ? String(displayed) : displayed.toFixed(normalizedPrecision);
101
+ if (displayNumber == null) return '';
102
+ return normalizedPrecision === undefined
103
+ ? String(displayNumber)
104
+ : displayNumber.toFixed(normalizedPrecision);
105
+ });
106
+
107
+ const localizedDisplay = $derived(
108
+ localeValue ? formatNumericValue(localeValue, displayNumber, normalizedPrecision) : displayValue,
109
+ );
110
+ const localizedEditing = $derived(
111
+ localeValue
112
+ ? formatNumericEditingText(localeValue, displayNumber, normalizedPrecision)
113
+ : displayValue,
114
+ );
115
+
116
+ const inputValue = $derived(
117
+ localeRequested
118
+ ? localeDraft ?? (localeFocused && localeValue ? localizedEditing : localeValue ? localizedDisplay : displayValue)
119
+ : displayValue,
120
+ );
121
+
122
+ const submittedValue = $derived(
123
+ value == null || !Number.isFinite(value) ? '' : String(value),
124
+ );
125
+ const hiddenDisabled = $derived(
126
+ disabled || localeInvalid || localeUnsupported || localeBoundaryInvalid || localeComposing,
127
+ );
128
+
129
+ $effect(() => {
130
+ if (!localeRequested) return;
131
+ const current = value;
132
+ const finite = current == null || Number.isFinite(current);
133
+ const display = displayNumber;
134
+ if (!finite || display == null) {
135
+ if (!Object.is(current, observedValue)) {
136
+ observedValue = current;
137
+ localeInvalid = false;
138
+ }
139
+ localeDraft = null;
140
+ return;
141
+ }
142
+ if (Object.is(current, observedValue)) return;
143
+ observedValue = current;
144
+ localeInvalid = false;
145
+ localeDraft = localeFocused && localeValue
146
+ ? formatNumericEditingText(localeValue, display, normalizedPrecision)
147
+ : null;
148
+ });
149
+
150
+ $effect(() => {
151
+ const nextLocale = locale;
152
+ if (Object.is(nextLocale, observedLocale)) return;
153
+ observedLocale = nextLocale;
154
+ localeFocused = false;
155
+ localeDraft = null;
156
+ localeInvalid = false;
157
+ localeComposing = false;
158
+ composedText = null;
159
+ observedValue = value;
160
+ });
161
+
162
+ $effect(() => {
163
+ localeInputElement?.setCustomValidity(localeErrorMessage);
57
164
  });
58
165
 
59
166
  const displayMin = $derived(mode === 'percent' && scale === '1' && min != null ? min * 100 : min);
60
167
  const displayMax = $derived(mode === 'percent' && scale === '1' && max != null ? max * 100 : max);
61
- const resolvedStep = $derived(
62
- mode === 'percent' && scale === '1'
63
- ? step == null ? 1 : step * 100
64
- : step ?? (normalizedPrecision === undefined ? 'any' : 10 ** -normalizedPrecision),
168
+ const explicitDisplayStep = $derived.by(() => {
169
+ if (step == null || !Number.isFinite(step) || step <= 0) return undefined;
170
+ const scaled = mode === 'percent' && scale === '1' ? step * 100 : step;
171
+ return Number.isFinite(scaled) && scaled > 0 ? scaled : undefined;
172
+ });
173
+ const displayStep = $derived(
174
+ explicitDisplayStep
175
+ ?? (mode === 'percent' && scale === '1'
176
+ ? 1
177
+ : normalizedPrecision === undefined
178
+ ? undefined
179
+ : 10 ** -normalizedPrecision),
65
180
  );
181
+ const resolvedStep = $derived(displayStep ?? 'any');
66
182
 
67
183
  function clamp(next: number): number {
68
184
  return Math.min(max ?? next, Math.max(min ?? next, next));
69
185
  }
70
186
 
71
- function handleInput(event: Event): void {
187
+ function commit(next: number | null): void {
188
+ observedValue = next;
189
+ value = next;
190
+ onchange?.(next);
191
+ }
192
+
193
+ function handleLegacyInput(event: Event): void {
72
194
  const input = event.currentTarget;
73
- if (!(input instanceof HTMLInputElement)) return;
195
+ if (!(input instanceof HTMLInputElement) || disabled) return;
74
196
  if (input.value === '') {
75
197
  value = null;
76
198
  onchange?.(null);
@@ -83,27 +205,157 @@
83
205
  value = next;
84
206
  onchange?.(next);
85
207
  }
208
+
209
+ function applyLocaleText(text: string): void {
210
+ if (!localeValue) return;
211
+ localeDraft = text;
212
+ const parsed = parseNumericInput(localeValue, text);
213
+ if (!parsed.ok) {
214
+ localeInvalid = true;
215
+ localeInputElement?.setCustomValidity(i18n.t('validation.invalidNumber'));
216
+ return;
217
+ }
218
+ localeInvalid = false;
219
+ localeInputElement?.setCustomValidity('');
220
+ if (parsed.value === null) {
221
+ commit(null);
222
+ return;
223
+ }
224
+ const stored = mode === 'percent' && scale === '1' ? parsed.value / 100 : parsed.value;
225
+ commit(clamp(stored));
226
+ }
227
+
228
+ function handleLocaleInput(event: Event): void {
229
+ const input = event.currentTarget;
230
+ if (!(input instanceof HTMLInputElement) || disabled) return;
231
+ const text = input.value;
232
+ if (localeUnsupported) {
233
+ localeDraft = text;
234
+ return;
235
+ }
236
+ if (!localeValue) return;
237
+ localeDraft = text;
238
+ if (localeComposing || (typeof InputEvent !== 'undefined' && event instanceof InputEvent && event.isComposing)) {
239
+ return;
240
+ }
241
+ if (composedText !== null && text === composedText) {
242
+ composedText = null;
243
+ return;
244
+ }
245
+ composedText = null;
246
+ applyLocaleText(text);
247
+ }
248
+
249
+ function handleCompositionStart(): void {
250
+ if (disabled || localeUnsupported || !localeValue) return;
251
+ localeComposing = true;
252
+ }
253
+
254
+ function handleCompositionEnd(event: Event): void {
255
+ const input = event.currentTarget;
256
+ if (!(input instanceof HTMLInputElement) || disabled || localeUnsupported || !localeValue) return;
257
+ localeComposing = false;
258
+ composedText = input.value;
259
+ applyLocaleText(input.value);
260
+ }
261
+
262
+ function handleLocaleFocus(): void {
263
+ if (disabled || localeUnsupported || !localeValue) return;
264
+ localeFocused = true;
265
+ observedValue = value;
266
+ if (!localeInvalid) localeDraft = localizedEditing;
267
+ }
268
+
269
+ function handleLocaleBlur(): void {
270
+ localeFocused = false;
271
+ if (!localeInvalid) localeDraft = null;
272
+ }
273
+
274
+ function handleLocaleKeyDown(event: KeyboardEvent): void {
275
+ if (disabled || localeUnsupported || !localeValue) return;
276
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;
277
+ event.preventDefault();
278
+ if (localeComposing || event.isComposing) return;
279
+ const increment = displayStep !== undefined && Number.isFinite(displayStep) && displayStep > 0
280
+ ? displayStep
281
+ : 1;
282
+ const base = value ?? min ?? 0;
283
+ const displayedBase = mode === 'percent' && scale === '1' ? base * 100 : base;
284
+ const nextDisplayed = addNumericStep(
285
+ displayedBase,
286
+ increment,
287
+ event.key === 'ArrowUp' ? 1 : -1,
288
+ );
289
+ const stored = mode === 'percent' && scale === '1' ? nextDisplayed / 100 : nextDisplayed;
290
+ const next = clamp(stored);
291
+ localeInvalid = false;
292
+ localeInputElement?.setCustomValidity('');
293
+ localeDraft = formatNumericEditingText(
294
+ localeValue,
295
+ mode === 'percent' && scale === '1' ? next * 100 : next,
296
+ normalizedPrecision,
297
+ );
298
+ commit(next);
299
+ }
86
300
  </script>
87
301
 
88
302
  <div class={cn('svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc', className)} data-numeric-input={mode}>
89
303
  {#if mode === 'currency' && currency}
90
304
  <span class="svadmin-u-bfa603190748 svadmin-u-012fbd121f37" aria-hidden="true">{currency}</span>
91
305
  {/if}
92
- <Input
93
- {id}
94
- {name}
95
- type="number"
96
- value={displayValue}
97
- min={displayMin}
98
- max={displayMax}
99
- step={resolvedStep}
100
- {placeholder}
101
- {disabled}
102
- {required}
103
- aria-invalid={invalid || undefined}
104
- aria-describedby={describedby}
105
- oninput={handleInput}
106
- />
306
+ {#if localeRequested}
307
+ {#if name}
308
+ <input
309
+ type="hidden"
310
+ {name}
311
+ value={submittedValue}
312
+ disabled={hiddenDisabled}
313
+ data-numeric-submit-value
314
+ />
315
+ {/if}
316
+ <Input
317
+ bind:ref={localeInputElement}
318
+ {id}
319
+ type="text"
320
+ inputmode="decimal"
321
+ role="spinbutton"
322
+ lang={localeValue}
323
+ value={inputValue}
324
+ min={displayMin}
325
+ max={displayMax}
326
+ step={resolvedStep}
327
+ aria-valuemin={displayMin}
328
+ aria-valuemax={displayMax}
329
+ aria-valuenow={displayNumber ?? undefined}
330
+ {placeholder}
331
+ {disabled}
332
+ {required}
333
+ aria-invalid={invalid || localeInvalid || localeBoundaryInvalid || localeUnsupported || undefined}
334
+ aria-describedby={describedby}
335
+ onfocus={handleLocaleFocus}
336
+ onblur={handleLocaleBlur}
337
+ onkeydown={handleLocaleKeyDown}
338
+ oninput={handleLocaleInput}
339
+ oncompositionstart={handleCompositionStart}
340
+ oncompositionend={handleCompositionEnd}
341
+ />
342
+ {:else}
343
+ <Input
344
+ {id}
345
+ {name}
346
+ type="number"
347
+ value={displayValue}
348
+ min={displayMin}
349
+ max={displayMax}
350
+ step={resolvedStep}
351
+ {placeholder}
352
+ {disabled}
353
+ {required}
354
+ aria-invalid={invalid || undefined}
355
+ aria-describedby={describedby}
356
+ oninput={handleLegacyInput}
357
+ />
358
+ {/if}
107
359
  {#if mode === 'percent'}
108
360
  <span class="svadmin-u-bfa603190748 svadmin-u-012fbd121f37" aria-hidden="true">%</span>
109
361
  {/if}
@@ -1,9 +1,11 @@
1
+ import { type NumericInputLocale } from './numeric-format.js';
1
2
  export type NumericInputMode = 'number' | 'currency' | 'percent';
2
3
  interface Props {
3
4
  value?: number | null;
4
5
  mode?: NumericInputMode;
5
6
  currency?: string;
6
7
  scale?: '100' | '1';
8
+ locale?: NumericInputLocale | undefined;
7
9
  min?: number;
8
10
  max?: number;
9
11
  step?: number;
@@ -14,7 +16,7 @@ interface Props {
14
16
  disabled?: boolean;
15
17
  required?: boolean;
16
18
  invalid?: boolean;
17
- describedby?: string;
19
+ describedby?: string | undefined;
18
20
  class?: string;
19
21
  onchange?: (value: number | null) => void;
20
22
  }
@@ -1,8 +1,10 @@
1
1
  <script lang="ts">
2
2
  import NumericInput from './NumericInput.svelte';
3
+ import type { NumericInputLocale } from './numeric-format.js';
3
4
 
4
5
  interface Props {
5
6
  value?: number | null;
7
+ locale?: NumericInputLocale | undefined;
6
8
  scale?: '100' | '1';
7
9
  min?: number;
8
10
  max?: number;
@@ -14,12 +16,12 @@
14
16
  disabled?: boolean;
15
17
  required?: boolean;
16
18
  invalid?: boolean;
17
- describedby?: string;
19
+ describedby?: string | undefined;
18
20
  class?: string;
19
21
  onchange?: (value: number | null) => void;
20
22
  }
21
23
 
22
- let props: Props = $props();
24
+ let { value = $bindable(null), ...props }: Props = $props();
23
25
  </script>
24
26
 
25
- <NumericInput {...props} mode="percent" />
27
+ <NumericInput {...props} bind:value mode="percent" />
@@ -1,5 +1,7 @@
1
+ import type { NumericInputLocale } from './numeric-format.js';
1
2
  interface Props {
2
3
  value?: number | null;
4
+ locale?: NumericInputLocale | undefined;
3
5
  scale?: '100' | '1';
4
6
  min?: number;
5
7
  max?: number;
@@ -11,10 +13,10 @@ interface Props {
11
13
  disabled?: boolean;
12
14
  required?: boolean;
13
15
  invalid?: boolean;
14
- describedby?: string;
16
+ describedby?: string | undefined;
15
17
  class?: string;
16
18
  onchange?: (value: number | null) => void;
17
19
  }
18
- declare const PercentInput: import("svelte").Component<Props, {}, "">;
20
+ declare const PercentInput: import("svelte").Component<Props, {}, "value">;
19
21
  type PercentInput = ReturnType<typeof PercentInput>;
20
22
  export default PercentInput;
@@ -24,6 +24,7 @@
24
24
  return value === key && labels ? labels[translation.locale === 'zh-CN' ? 1 : 0] : value;
25
25
  },
26
26
  };
27
+ const instanceId = $props.id();
27
28
 
28
29
 
29
30
  let {
@@ -48,6 +49,7 @@
48
49
  // Custom slots for injecting multi-tenant switchers or security filters
49
50
  sidebarExtra,
50
51
  headerExtra,
52
+ tableContent,
51
53
 
52
54
  // Callback event
53
55
  onToggle
@@ -63,6 +65,7 @@
63
65
  conflictCount?: number;
64
66
  sidebarExtra?: Snippet;
65
67
  headerExtra?: Snippet;
68
+ tableContent?: Snippet | undefined;
66
69
  onToggle: (role: string, resource: string, action: string, grant: boolean) => void;
67
70
  } = $props();
68
71
 
@@ -72,6 +75,20 @@
72
75
  selectedRole = firstRole.code;
73
76
  }
74
77
  });
78
+
79
+ type PermissionDecision = 'granted' | 'denied' | 'unknown';
80
+
81
+ function resolveDecision(role: string, resource: string, action: string): PermissionDecision {
82
+ if (!role) return 'unknown';
83
+ try {
84
+ const value: unknown = isGranted(role, resource, action);
85
+ if (value === true) return 'granted';
86
+ if (value === false) return 'denied';
87
+ return 'unknown';
88
+ } catch {
89
+ return 'unknown';
90
+ }
91
+ }
75
92
  </script>
76
93
 
77
94
  <div class="svadmin-u-668b21aa5409 svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed svadmin-u-f46b61a9b310">
@@ -87,7 +104,11 @@
87
104
 
88
105
  <!-- Toast Feedback -->
89
106
  {#if message}
90
- <div class={`svadmin-u-f0faeb26d656 svadmin-u-03b4dd7f172b svadmin-u-5f22e64f2282 svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-438b2237b8d6 svadmin-u-0fe7d7d814d0 svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4 ${messageType === 'success' ? 'svadmin-u-17a9f7af2265 svadmin-u-76747e5e02ff svadmin-u-3daca9af0861 svadmin-u-be62e835b692' : 'svadmin-u-43928fcc832f svadmin-u-811148b13d1e svadmin-u-3daca9af0861 svadmin-u-5b92fd698365'}`}>
107
+ <div
108
+ role={messageType === 'error' ? 'alert' : 'status'}
109
+ aria-live={messageType === 'error' ? 'assertive' : 'polite'}
110
+ class={`svadmin-u-f0faeb26d656 svadmin-u-03b4dd7f172b svadmin-u-5f22e64f2282 svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-438b2237b8d6 svadmin-u-0fe7d7d814d0 svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4 ${messageType === 'success' ? 'svadmin-u-17a9f7af2265 svadmin-u-76747e5e02ff svadmin-u-3daca9af0861 svadmin-u-be62e835b692' : 'svadmin-u-43928fcc832f svadmin-u-811148b13d1e svadmin-u-3daca9af0861 svadmin-u-5b92fd698365'}`}
111
+ >
91
112
  {#if messageType === 'success'}
92
113
  <Check class="svadmin-u-dc7972ebf3f3 svadmin-u-11e59c6d5f6b" />
93
114
  {:else}
@@ -100,8 +121,8 @@
100
121
 
101
122
  <div class="svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed svadmin-u-2adea12c41f3 svadmin-u-0d304f904cb0 svadmin-u-36e579c0b41c svadmin-u-243d62579c55">
102
123
  <div class="svadmin-u-6da6a3c3f741 svadmin-u-5f22e64f2282 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-cd0ad9a56558 svadmin-u-8e63407b5ceb svadmin-u-438b2237b8d6 svadmin-u-a327049cac5b">
103
- <label for="permission-role-select" class="svadmin-u-a77ed4d908c0 svadmin-u-0214b4b355d1 svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-d4108abe6359">{i18n.t('permissions.roles') || 'Roles'}</label>
104
- <select id="permission-role-select" bind:value={selectedRole} class="svadmin-u-e7a768f922d2 svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-0e17f2bd9074 svadmin-u-fc7473ca09eb svadmin-u-582e6ef4b245 svadmin-u-55d048ebfb1c svadmin-u-608dd26cd5ba svadmin-u-80b9d0ae125f svadmin-u-6b22a22a9752" disabled={roles.length === 0}>
124
+ <label for={`${instanceId}-permission-role-select`} class="svadmin-u-a77ed4d908c0 svadmin-u-0214b4b355d1 svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-d4108abe6359">{i18n.t('permissions.roles') || 'Roles'}</label>
125
+ <select id={`${instanceId}-permission-role-select`} value={selectedRole} onchange={(event) => { selectedRole = event.currentTarget.value; }} class="svadmin-u-e7a768f922d2 svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-0e17f2bd9074 svadmin-u-fc7473ca09eb svadmin-u-582e6ef4b245 svadmin-u-55d048ebfb1c svadmin-u-608dd26cd5ba svadmin-u-80b9d0ae125f svadmin-u-6b22a22a9752" disabled={roles.length === 0}>
105
126
  {#each roles as role (role.code)}<option value={role.code}>{role.name}</option>{/each}
106
127
  </select>
107
128
  {#if roles.length === 0}<p class="svadmin-u-50d0d216a2f8 svadmin-u-fc7473ca09eb svadmin-u-bfa603190748">{i18n.t('permissions.noRoles') || 'No roles available'}</p>{/if}
@@ -114,7 +135,9 @@
114
135
  <div class="svadmin-u-da7c36cd8867 svadmin-u-36e579c0b41c svadmin-u-92bf82f493b1">
115
136
  {#each roles as role, _i (_i)}
116
137
  <button
138
+ type="button"
117
139
  onclick={() => selectedRole = role.code}
140
+ aria-pressed={selectedRole === role.code}
118
141
  class="svadmin-u-6da6a3c3f741 svadmin-u-2eba0d65d059 svadmin-u-0e17f2bd9074 svadmin-u-e7ee55ac7ffe svadmin-u-5f22e64f2282 svadmin-u-fc7473ca09eb svadmin-u-0fe7d7d814d0 svadmin-u-625a4c3fbeb2 {selectedRole === role.code ? 'svadmin-u-375dc44df6e9 svadmin-u-20aaf08a7ed1 svadmin-u-2689f3958069 svadmin-u-438b2237b8d6 svadmin-u-3daca9af0861 svadmin-u-2b6f77ad4036' : 'svadmin-u-d4108abe6359 svadmin-u-8e551981c8d7'}"
119
142
  >
120
143
  {role.name}
@@ -161,6 +184,9 @@
161
184
 
162
185
  <!-- Table Area -->
163
186
  <div class="svadmin-u-36e579c0b41c svadmin-u-73fc3fb18ceb svadmin-u-8e63407b5ceb">
187
+ {#if tableContent}
188
+ {@render tableContent()}
189
+ {:else}
164
190
  <table class="svadmin-u-a1e7a8069878 svadmin-u-5f22e64f2282 svadmin-u-2cd02d11d1af svadmin-u-438b2237b8d6 svadmin-u-3daca9af0861 svadmin-u-a10fdd7667ee">
165
191
  <thead class="svadmin-u-358af0b65a31">
166
192
  <tr>
@@ -197,19 +223,28 @@
197
223
 
198
224
  <!-- Action Checkboxes -->
199
225
  {#each actions as action, _i (_i)}
200
- {@const granted = selectedRole ? isGranted(selectedRole, resource.code, action.code) : false}
226
+ {@const decision = selectedRole ? resolveDecision(selectedRole, resource.code, action.code) : 'unknown'}
227
+ {@const granted = decision === 'granted'}
228
+ {@const actionable = !loading && !!selectedRole && decision !== 'unknown'}
201
229
  <td class="svadmin-u-e82ae8be04aa svadmin-u-0e17f2bd9074 svadmin-u-1b2d54a3fd12 svadmin-u-ca6bf63030aa">
202
230
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227">
203
231
  <button
204
- disabled={loading || !selectedRole}
205
- aria-label="Toggle {action.name}"
206
- onclick={() => onToggle(selectedRole as string, resource.code, action.code, !granted)}
207
- class="svadmin-u-7ec10f86d9b1 svadmin-u-f6fe902450dc svadmin-u-07389a777c1f svadmin-u-ca6bcd4b6f3f svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-0fe7d7d814d0 svadmin-u-55d048ebfb1c svadmin-u-608dd26cd5ba svadmin-u-094cedcfedfd {granted ? 'svadmin-u-75b1bec3ea0e svadmin-u-6cbc84dd9e1a svadmin-u-30ca335ae9c2' : 'svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-0557b88819cd'} {loading || !selectedRole ? 'svadmin-u-0b8c506a0596 svadmin-u-29b733e4c162' : 'svadmin-u-34516836730d'}"
232
+ type="button"
233
+ disabled={!actionable}
234
+ aria-label={translation.t('permissions.toggleAction').replaceAll('{action}', () => action.name)}
235
+ aria-describedby={`${instanceId}-permission-${resource.code}-${action.code}`}
236
+ aria-pressed={granted}
237
+ onclick={() => { if (actionable) onToggle(selectedRole as string, resource.code, action.code, !granted); }}
238
+ class="svadmin-u-7ec10f86d9b1 svadmin-u-f6fe902450dc svadmin-u-07389a777c1f svadmin-u-ca6bcd4b6f3f svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-0fe7d7d814d0 svadmin-u-55d048ebfb1c svadmin-u-608dd26cd5ba svadmin-u-094cedcfedfd {granted ? 'svadmin-u-75b1bec3ea0e svadmin-u-6cbc84dd9e1a svadmin-u-30ca335ae9c2' : 'svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-0557b88819cd'} {!actionable ? 'svadmin-u-0b8c506a0596 svadmin-u-29b733e4c162' : 'svadmin-u-34516836730d'}"
208
239
  >
209
240
  <svg class="svadmin-u-bf600f8e029c svadmin-u-7fc7f732bf7e {granted ? 'svadmin-u-0214b4b355d1' : 'svadmin-u-99d72c7fc3e2'}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
210
241
  <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
211
242
  </svg>
212
243
  </button>
244
+ <span id={`${instanceId}-permission-${resource.code}-${action.code}`} class="svadmin-sr-only">
245
+ {roles.find(role => role.code === selectedRole)?.name || selectedRole} {selectedRole ? `(${selectedRole})` : ''}
246
+ / {resource.name} ({resource.code}) / {action.name} ({action.code})
247
+ </span>
213
248
  </div>
214
249
  </td>
215
250
  {/each}
@@ -217,6 +252,7 @@
217
252
  {/each}
218
253
  </tbody>
219
254
  </table>
255
+ {/if}
220
256
  </div>
221
257
  </div>
222
258
  </div>
@@ -12,6 +12,7 @@ type $$ComponentProps = {
12
12
  conflictCount?: number;
13
13
  sidebarExtra?: Snippet;
14
14
  headerExtra?: Snippet;
15
+ tableContent?: Snippet | undefined;
15
16
  onToggle: (role: string, resource: string, action: string, grant: boolean) => void;
16
17
  };
17
18
  declare const PermissionMatrix: import("svelte").Component<$$ComponentProps, {}, "selectedRole">;