@svadmin/ui 0.70.0 → 0.72.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 (158) hide show
  1. package/dist/actions.js +1 -1
  2. package/dist/app.css +102 -63
  3. package/dist/app.theme.css +102 -63
  4. package/dist/component-registry.svelte.d.ts +33 -19
  5. package/dist/component-registry.svelte.js +6 -4
  6. package/dist/components/AdminApp.svelte +55 -53
  7. package/dist/components/AdminApp.svelte.d.ts +2 -2
  8. package/dist/components/ApiSettings.svelte +19 -1
  9. package/dist/components/AuditLogDrawer.svelte +14 -3
  10. package/dist/components/AutoForm.svelte +146 -48
  11. package/dist/components/AutoForm.svelte.d.ts +2 -0
  12. package/dist/components/AutoTable.svelte +358 -258
  13. package/dist/components/AutoTable.svelte.d.ts +1 -0
  14. package/dist/components/BoundRecordDetailDrawer.svelte +166 -0
  15. package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +9 -0
  16. package/dist/components/Breadcrumbs.svelte +7 -4
  17. package/dist/components/CancelTaskButton.svelte +9 -5
  18. package/dist/components/ColumnHeaderFilter.svelte +4 -3
  19. package/dist/components/ColumnSettings.svelte +1 -0
  20. package/dist/components/ComboboxField.svelte +90 -30
  21. package/dist/components/ComboboxField.svelte.d.ts +2 -2
  22. package/dist/components/CreatePage.svelte +3 -1
  23. package/dist/components/DetailDrawer.svelte +4 -0
  24. package/dist/components/DetailDrawer.svelte.d.ts +1 -0
  25. package/dist/components/DevTools.svelte +6 -19
  26. package/dist/components/DraggableGrid.svelte +1 -0
  27. package/dist/components/DraggableHeader.svelte +5 -9
  28. package/dist/components/DraggableHeader.svelte.d.ts +43 -18
  29. package/dist/components/DraggableRowTable.svelte +2 -2
  30. package/dist/components/DynamicFormList.svelte +17 -17
  31. package/dist/components/DynamicFormList.svelte.d.ts +2 -1
  32. package/dist/components/EditPage.svelte +3 -1
  33. package/dist/components/EditableTable.svelte +4 -0
  34. package/dist/components/ErrorPage.svelte +3 -1
  35. package/dist/components/FieldDisplay.svelte +14 -0
  36. package/dist/components/FieldDisplay.svelte.d.ts +7 -0
  37. package/dist/components/FieldRenderer.svelte +9 -5
  38. package/dist/components/FilterBuilder.svelte +19 -8
  39. package/dist/components/FilterDropdown.svelte.d.ts +1 -1
  40. package/dist/components/Header.svelte +5 -6
  41. package/dist/components/ImportWizard.svelte +3 -2
  42. package/dist/components/InfiniteList.svelte +22 -8
  43. package/dist/components/InfiniteList.svelte.d.ts +5 -3
  44. package/dist/components/InlineEdit.svelte +114 -44
  45. package/dist/components/JsonSchemaForm.svelte +19 -14
  46. package/dist/components/Layout.svelte +19 -15
  47. package/dist/components/LazyTaskQueueDrawer.svelte +26 -0
  48. package/dist/components/LazyTaskQueueDrawer.svelte.d.ts +8 -0
  49. package/dist/components/ListPage.svelte +7 -6
  50. package/dist/components/MasterDetailView.svelte +3 -3
  51. package/dist/components/MediaLibraryModal.svelte +5 -2
  52. package/dist/components/MentionsInput.svelte +4 -3
  53. package/dist/components/NavigateToResource.svelte +3 -2
  54. package/dist/components/PermissionMatrix.svelte +4 -3
  55. package/dist/components/ProfilePage.svelte +2 -2
  56. package/dist/components/QuickEditDrawer.svelte +39 -0
  57. package/dist/components/QuickEditDrawer.svelte.d.ts +8 -0
  58. package/dist/components/RecordDetailDrawer.svelte +14 -108
  59. package/dist/components/RecordRowActions.svelte +41 -0
  60. package/dist/components/RecordRowActions.svelte.d.ts +14 -0
  61. package/dist/components/ResourceAccessGuard.svelte +7 -5
  62. package/dist/components/RetryTaskButton.svelte +9 -5
  63. package/dist/components/RowActions.svelte +5 -3
  64. package/dist/components/SensitiveDataMask.svelte +3 -3
  65. package/dist/components/SettingsPage.svelte +1 -1
  66. package/dist/components/ShowPage.svelte +44 -38
  67. package/dist/components/Sidebar.svelte +10 -8
  68. package/dist/components/SplitPaneLayout.svelte +4 -2
  69. package/dist/components/StepsForm.svelte +161 -44
  70. package/dist/components/StepsForm.svelte.d.ts +1 -0
  71. package/dist/components/TaskDetails.svelte +20 -7
  72. package/dist/components/TaskList.svelte +19 -7
  73. package/dist/components/TaskQueueDrawer.svelte +21 -22
  74. package/dist/components/Toast.svelte +4 -2
  75. package/dist/components/TooltipButton.svelte +1 -1
  76. package/dist/components/TreeSelect.svelte +4 -2
  77. package/dist/components/ValidatedField.svelte +27 -0
  78. package/dist/components/ValidatedField.svelte.d.ts +35 -0
  79. package/dist/components/account/ImportMembersPage.svelte +2 -2
  80. package/dist/components/account/SecurityLogPage.svelte +3 -1
  81. package/dist/components/account/SettingsEnterprisePage.svelte +7 -5
  82. package/dist/components/account/TeamMembersPage.svelte +1 -1
  83. package/dist/components/buttons/CloneButton.svelte +6 -4
  84. package/dist/components/buttons/DeleteButton.svelte +95 -29
  85. package/dist/components/buttons/DeleteButton.svelte.d.ts +2 -0
  86. package/dist/components/buttons/EditButton.svelte +6 -4
  87. package/dist/components/buttons/ExportButton.svelte +22 -4
  88. package/dist/components/buttons/ImportButton.svelte +83 -38
  89. package/dist/components/buttons/ImportButton.svelte.d.ts +6 -10
  90. package/dist/components/buttons/RefreshButton.svelte +48 -8
  91. package/dist/components/buttons/ShowButton.svelte +6 -4
  92. package/dist/components/content/ApiKeyList.svelte +3 -1
  93. package/dist/components/content/ContentPageShell.svelte +3 -1
  94. package/dist/components/content/FileList.svelte +3 -1
  95. package/dist/components/content/MemberList.svelte +3 -1
  96. package/dist/components/content/NetworkTable.svelte +3 -1
  97. package/dist/components/content/SecurityEventTable.svelte +3 -1
  98. package/dist/components/content/StatusTabs.svelte +2 -2
  99. package/dist/components/fieldComponentMap.d.ts +18 -13
  100. package/dist/components/fieldComponentMap.js +95 -47
  101. package/dist/components/fields/AvatarField.svelte +1 -1
  102. package/dist/components/fields/ChoiceField.svelte +16 -0
  103. package/dist/components/fields/ChoiceField.svelte.d.ts +10 -0
  104. package/dist/components/fields/CodeField.svelte +11 -12
  105. package/dist/components/fields/CodeField.svelte.d.ts +1 -1
  106. package/dist/components/fields/DateRangeField.svelte +2 -4
  107. package/dist/components/fields/ImageField.svelte +3 -1
  108. package/dist/components/fields/ImagesField.svelte +23 -0
  109. package/dist/components/fields/ImagesField.svelte.d.ts +6 -0
  110. package/dist/components/fields/JsonField.svelte +19 -13
  111. package/dist/components/fields/PasswordField.svelte +5 -0
  112. package/dist/components/fields/PasswordField.svelte.d.ts +6 -0
  113. package/dist/components/fields/json-value.d.ts +11 -0
  114. package/dist/components/fields/json-value.js +28 -0
  115. package/dist/components/profile/ActivityTimeline.svelte +2 -2
  116. package/dist/components/record-detail.svelte.d.ts +16 -0
  117. package/dist/components/record-detail.svelte.js +28 -0
  118. package/dist/components/saved-list-views.js +29 -29
  119. package/dist/components/table-contract.d.ts +13 -0
  120. package/dist/components/table-contract.js +42 -0
  121. package/dist/components/task-utils.js +4 -2
  122. package/dist/components/ui/alert/alert-variants.d.ts +2 -3
  123. package/dist/components/ui/alert/alert-variants.js +1 -2
  124. package/dist/components/ui/alert/alert.svelte +4 -5
  125. package/dist/components/ui/alert/alert.svelte.d.ts +3 -8
  126. package/dist/components/ui/alert/index.js +0 -2
  127. package/dist/components/ui/avatar/avatar-variants.d.ts +2 -3
  128. package/dist/components/ui/avatar/avatar-variants.js +1 -2
  129. package/dist/components/ui/avatar/avatar.svelte +5 -6
  130. package/dist/components/ui/avatar/avatar.svelte.d.ts +3 -8
  131. package/dist/components/ui/badge/badge-variants.d.ts +2 -3
  132. package/dist/components/ui/badge/badge-variants.js +1 -2
  133. package/dist/components/ui/badge/badge.svelte +4 -5
  134. package/dist/components/ui/badge/badge.svelte.d.ts +3 -8
  135. package/dist/components/ui/badge/index.js +0 -2
  136. package/dist/components/ui/button/button-variants.d.ts +2 -3
  137. package/dist/components/ui/button/button-variants.js +1 -2
  138. package/dist/components/ui/button/button.svelte +100 -44
  139. package/dist/components/ui/button/button.svelte.d.ts +8 -12
  140. package/dist/components/ui/button/index.js +0 -2
  141. package/dist/components/ui/command/command-dialog.svelte +3 -1
  142. package/dist/components/ui/command/command-dialog.svelte.d.ts +1 -1
  143. package/dist/components/ui/command/command-group.svelte +4 -2
  144. package/dist/components/ui/command/command-item.svelte +4 -2
  145. package/dist/components/ui/command/command-separator.svelte +3 -1
  146. package/dist/components/ui/command/index.d.ts +5 -5
  147. package/dist/components/ui/input/input.svelte +40 -20
  148. package/dist/components/ui/input/input.svelte.d.ts +5 -4
  149. package/dist/components/ui/sheet/sheet.svelte +9 -3
  150. package/dist/components/ui/sheet/sheet.svelte.d.ts +1 -0
  151. package/dist/components/ui/tooltip/tooltip.svelte +4 -2
  152. package/dist/index.d.ts +1 -1
  153. package/dist/inline-value.d.ts +5 -0
  154. package/dist/inline-value.js +24 -0
  155. package/dist/numeric-input.d.ts +2 -0
  156. package/dist/numeric-input.js +9 -0
  157. package/dist/types.d.ts +3 -3
  158. package/package.json +11 -5
@@ -1,7 +1,12 @@
1
1
  <script lang="ts">
2
- import { useUpdate } from '@svadmin/core';
2
+ import { useUpdate, useResourceContract, captureAdminContext, captureAuthSession, useCan, HttpError, useTranslation } from '@svadmin/core';
3
3
  import type { FieldDefinition } from '@svadmin/core';
4
- import { Loader2 } from '@lucide/svelte';
4
+ import { definedReactiveOptions } from '@svadmin/core/options';
5
+ import { untrack } from 'svelte';
6
+ import { Check, X, Loader2 } from '@lucide/svelte';
7
+ import { Button } from './ui/button/index.js';
8
+ import FieldDisplay from './FieldDisplay.svelte';
9
+ import { inlineDisplayValue, parseInlineValue } from '../inline-value';
5
10
 
6
11
  interface Props {
7
12
  resourceName: string;
@@ -14,53 +19,103 @@
14
19
 
15
20
  let { resourceName, recordId, field, value, onSave }: Props = $props();
16
21
 
22
+ const context = captureAdminContext();
23
+ const binding = useResourceContract(() => resourceName);
24
+ const i18n = useTranslation();
25
+ const permission = useCan(() => ({ resource: resourceName, action: 'edit', id: recordId }));
26
+ const { mutation } = useUpdate(definedReactiveOptions({
27
+ get resource() { return binding.resource; },
28
+ get id() { return recordId; },
29
+ }));
30
+ const display = $derived(inlineDisplayValue(field.type, value));
31
+ const session = $derived(captureAuthSession(context.authProvider));
32
+ const editable = $derived(session.available && context.getResource(resourceName).canEdit !== false && field.showInEdit !== false &&
33
+ field.key !== 'id' && permission.allowed === true && display !== undefined);
34
+ const scope = $derived({
35
+ contract: binding.resource, id: recordId, field: field.key, type: field.type, editable,
36
+ provider: context.providers?.[binding.dataProviderName], meta: binding.meta,
37
+ tenant: context.tenantCacheKey?.__svadminTenant,
38
+ auth: context.authProvider, session,
39
+ });
17
40
  let editing = $state(false);
18
41
  let editValue = $state('');
19
42
  let inputRef = $state<HTMLInputElement | null>(null);
43
+ let editorRef = $state<HTMLDivElement | null>(null);
20
44
  let saving = $state(false);
21
- let savePending = false;
22
- const displayValue = $derived(String(value ?? ''));
45
+ let failure = $state<'input' | 'request' | null>(null);
46
+ let active: object | undefined;
47
+ let editingScope = $state.raw<typeof scope>();
23
48
  const fieldLabel = $derived(field.label || field.key);
24
49
 
25
- const { mutation } = useUpdate({ get resource() { return resourceName; } });
50
+ $effect(() => {
51
+ void scope;
52
+ editing = false;
53
+ editValue = '';
54
+ saving = false;
55
+ failure = null;
56
+ active = undefined;
57
+ editingScope = undefined;
58
+ });
59
+ $effect(() => () => { active = undefined; });
60
+ $effect(() => {
61
+ const node = inputRef;
62
+ if (editing && node) untrack(() => node.focus());
63
+ });
26
64
 
27
65
  function startEdit() {
28
- if (field.type === 'number' || field.type === 'text' || field.type === 'url' || field.type === 'email') {
29
- editing = true;
30
- editValue = displayValue;
31
- // Focus input on next tick
32
- setTimeout(() => inputRef?.focus(), 0);
33
- }
66
+ if (!editable || !display || saving || !session.isCurrent()) return;
67
+ editingScope = scope;
68
+ failure = null;
69
+ editValue = display.text;
70
+ editing = true;
34
71
  }
35
72
 
36
73
  async function save() {
37
- if (!editing || saving || savePending) return;
38
- const newValue = field.type === 'number' ? (editValue.trim() === '' ? null : (() => { const n = Number(editValue); return isNaN(n) ? value : n; })()) : editValue;
39
- if (newValue === value) {
74
+ if (!editing || saving || !editable || editingScope !== scope || !session.isCurrent()) return;
75
+ let newValue: string | number | null;
76
+ try { newValue = parseInlineValue(field.type, editValue); }
77
+ catch { failure = 'input'; return; }
78
+ if (newValue === display?.value) {
40
79
  editing = false;
41
80
  return;
42
81
  }
43
- savePending = true;
82
+ const current = scope;
83
+ const fieldKey = field.key;
84
+ const callback = onSave;
85
+ const token = {};
86
+ active = token;
44
87
  saving = true;
88
+ failure = null;
89
+ let succeeded = false;
90
+ let savedValue: unknown;
45
91
  try {
46
- await mutation.mutateAsync({
47
- id: recordId,
48
- resource: resourceName,
49
- variables: { [field.key]: newValue },
92
+ const result = await mutation.mutateAsync({
93
+ variables: { [fieldKey]: newValue }, dataProviderName: binding.dataProviderName,
50
94
  });
51
- editing = false;
52
- onSave?.(newValue);
53
- } catch {
54
- // Keep editing on error
95
+ if (active === token && scope === current && current.session.isCurrent()) {
96
+ editing = false;
97
+ succeeded = true;
98
+ savedValue = result.data[fieldKey];
99
+ }
100
+ } catch (error) {
101
+ if (active === token && scope === current && current.session.isCurrent()) {
102
+ failure = error instanceof HttpError && error.code === 'INVALID_RESOURCE_INPUT' ? 'input' : 'request';
103
+ }
55
104
  } finally {
56
- saving = false;
57
- savePending = false;
105
+ if (active === token && scope === current && current.session.isCurrent()) {
106
+ active = undefined;
107
+ saving = false;
108
+ }
58
109
  }
110
+ if (succeeded && scope === current && current.session.isCurrent()) callback?.(savedValue);
59
111
  }
60
112
 
61
113
  function cancel() {
114
+ if (saving) return;
62
115
  editing = false;
63
- editValue = displayValue;
116
+ failure = null;
117
+ editingScope = undefined;
118
+ editValue = display?.text ?? '';
64
119
  }
65
120
 
66
121
  function handleKeydown(e: KeyboardEvent) {
@@ -71,38 +126,53 @@
71
126
  if (e.key === 'Escape') cancel();
72
127
  }
73
128
 
74
- function handleBlur() {
75
- if (!savePending) save();
129
+ function handleBlur(event: FocusEvent) {
130
+ if (event.relatedTarget instanceof Node && editorRef?.contains(event.relatedTarget)) return;
131
+ if (!saving) void save();
76
132
  }
77
133
  </script>
78
134
 
79
- {#if editing}
80
- <div class="svadmin-u-d89972fe17d6 svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-6da6a3c3f741">
135
+ {#if editing && editingScope === scope && session.isCurrent()}
136
+ <div bind:this={editorRef} class="svadmin-u-d89972fe17d6 svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-6da6a3c3f741">
81
137
  <input
82
138
  bind:this={inputRef}
83
- type={field.type === 'number' ? 'number' : 'text'}
139
+ type="text"
140
+ inputmode={field.type === 'number' ? 'decimal' : field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text'}
84
141
  bind:value={editValue}
85
142
  onkeydown={handleKeydown}
86
143
  onblur={handleBlur}
87
144
  disabled={saving}
88
145
  aria-label={fieldLabel}
89
- class="svadmin-u-d0a52b312f7d svadmin-u-6da6a3c3f741 svadmin-u-07389a777c1f svadmin-u-ca6bcd4b6f3f svadmin-u-e6f9e383a762 svadmin-u-d5eab218aa34 svadmin-u-fc7473ca09eb svadmin-u-df37b1fd9495 svadmin-u-3daca9af0861 svadmin-u-f42e9fee68a1 svadmin-u-608dd26cd5ba svadmin-u-1a5f9520d7fa svadmin-u-0fe7d7d814d0 svadmin-u-d463b664011d"
146
+ aria-invalid={failure !== null}
147
+ class="svadmin-u-d0a52b312f7d svadmin-u-7e0b7cdf1a94 svadmin-u-36e579c0b41c svadmin-u-07389a777c1f svadmin-u-ca6bcd4b6f3f svadmin-u-e6f9e383a762 svadmin-u-d5eab218aa34 svadmin-u-fc7473ca09eb svadmin-u-df37b1fd9495 svadmin-u-3daca9af0861 svadmin-u-f42e9fee68a1 svadmin-u-608dd26cd5ba svadmin-u-1a5f9520d7fa svadmin-u-0fe7d7d814d0 svadmin-u-d463b664011d"
90
148
  />
91
- {#if saving}
92
- <span class="svadmin-u-da4dbfbc4fdc svadmin-u-7b2d63937d23 svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-a4326536b8f5" role="status" aria-label="Saving...">
93
- <Loader2 class="svadmin-u-783b0d9d1e2c svadmin-u-afbdd13a380e svadmin-u-bfa603190748" aria-hidden="true" />
94
- </span>
95
- {/if}
149
+ <Button type="button" variant="ghost" size="icon-sm" disabled={saving} title={i18n.t('common.save')}
150
+ aria-label={i18n.t('common.save')} aria-busy={saving} onclick={save}>
151
+ {#if saving}
152
+ <Loader2 class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3 svadmin-u-afbdd13a380e" role="status" aria-label="Saving..." />
153
+ {:else}
154
+ <Check class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
155
+ {/if}
156
+ </Button>
157
+ <Button type="button" variant="ghost" size="icon-sm" disabled={saving} title={i18n.t('common.cancel')}
158
+ aria-label={i18n.t('common.cancel')} onclick={cancel}>
159
+ <X class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
160
+ </Button>
96
161
  </div>
97
- {:else}
98
- <span
162
+ {#if failure}
163
+ <p role="alert">{i18n.t(failure === 'input' ? 'validation.invalidFormat' : 'common.operationFailed')}</p>
164
+ {/if}
165
+ {:else if editable && display}
166
+ <button type="button"
99
167
  class="svadmin-u-52083e7da442 svadmin-u-3960ffc248d9 svadmin-u-34516836730d svadmin-u-07389a777c1f svadmin-u-45d828117213 svadmin-u-465609a240a8 svadmin-u-76610325273f svadmin-u-fc7473ca09eb svadmin-u-d4108abe6359 svadmin-u-2a6233dc87a9 svadmin-u-f10f771f87e9 svadmin-u-793c80e97ffb svadmin-u-9c1295a6914a svadmin-u-ce4edccf4cbb svadmin-u-ceb69a6b0e5f"
100
168
  ondblclick={startEdit}
101
- title={`Double-click to edit ${fieldLabel}`}
102
- role="button"
103
- tabindex="0"
169
+ onclick={startEdit}
170
+ title={`${i18n.t('common.edit')} ${fieldLabel}`}
171
+ aria-label={`${i18n.t('common.edit')} ${fieldLabel}`}
104
172
  onkeydown={(e) => { if (e.key === 'Enter' || e.key === 'F2') { e.preventDefault(); startEdit(); } }}
105
173
  >
106
- {value != null && value !== '' ? String(value) : '—'}
107
- </span>
174
+ {display.text || '—'}
175
+ </button>
176
+ {:else}
177
+ <FieldDisplay type={field.type} {value} options={field.options} resourceName={field.resource} />
108
178
  {/if}
@@ -1,4 +1,7 @@
1
1
  <script lang="ts">
2
+ import { definedOptions } from '@svadmin/core/options';
3
+ import { useTranslation } from '@svadmin/core/i18n';
4
+
2
5
  import { Button } from './ui/button/index.js';
3
6
  import { Loader2 } from '@lucide/svelte';
4
7
  import { cn } from '../utils.js';
@@ -19,6 +22,8 @@
19
22
  class: className = '',
20
23
  }: Props = $props();
21
24
 
25
+ const i18n = useTranslation();
26
+
22
27
  let isSubmitting = $state(false);
23
28
 
24
29
  interface FieldMeta {
@@ -32,18 +37,18 @@
32
37
  }
33
38
 
34
39
  const properties = $derived.by<FieldMeta[]>(() => {
35
- const propsObj = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;
36
- const requiredKeys = Array.isArray(schema.required) ? (schema.required as string[]) : [];
40
+ const propsObj = (schema['properties'] ?? {}) as Record<string, Record<string, unknown>>;
41
+ const requiredKeys = Array.isArray(schema['required']) ? (schema['required'] as string[]) : [];
37
42
 
38
- return Object.entries(propsObj).map(([key, propDef]) => ({
43
+ return Object.entries(propsObj).map(([key, propDef]) => (definedOptions({
39
44
  key,
40
- title: String(propDef.title ?? key),
41
- description: propDef.description ? String(propDef.description) : undefined,
42
- type: String(propDef.type ?? 'string'),
43
- enum: Array.isArray(propDef.enum) ? (propDef.enum as Array<string | number>) : undefined,
45
+ title: String(propDef['title'] ?? key),
46
+ description: propDef['description'] ? String(propDef['description']) : undefined,
47
+ type: String(propDef['type'] ?? 'string'),
48
+ enum: Array.isArray(propDef['enum']) ? (propDef['enum'] as Array<string | number>) : undefined,
44
49
  required: requiredKeys.includes(key),
45
- default: propDef.default,
46
- }));
50
+ default: propDef['default'],
51
+ })));
47
52
  });
48
53
 
49
54
  async function handleSubmit(e: SubmitEvent) {
@@ -68,11 +73,11 @@
68
73
  onsubmit={handleSubmit}
69
74
  class={cn('svadmin-u-3e7ce58d64fa svadmin-u-a217b4eaa918 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-cd0ad9a56558 svadmin-u-0478c89a150f svadmin-u-cef5b893cf23 svadmin-u-359090c2d529', className)}
70
75
  >
71
- {#if schema.title}
76
+ {#if schema['title']}
72
77
  <div class="svadmin-u-f4cc511ff0c1 svadmin-u-65fdbade2025 svadmin-u-05faf5c801ff">
73
- <h3 class="svadmin-u-fc7473ca09eb svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{schema.title}</h3>
74
- {#if schema.description}
75
- <p class="svadmin-u-359090c2d529 svadmin-u-bfa603190748 svadmin-u-15e1b1f444fe">{schema.description}</p>
78
+ <h3 class="svadmin-u-fc7473ca09eb svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{schema['title']}</h3>
79
+ {#if schema['description']}
80
+ <p class="svadmin-u-359090c2d529 svadmin-u-bfa603190748 svadmin-u-15e1b1f444fe">{schema['description']}</p>
76
81
  {/if}
77
82
  </div>
78
83
  {/if}
@@ -99,7 +104,7 @@
99
104
  onchange={(e) => updateValue(field.key, e.currentTarget.value)}
100
105
  class="svadmin-u-ed8a5df7b2fb svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-d5eab218aa34 svadmin-u-359090c2d529 svadmin-u-f10f771f87e9 svadmin-u-3e94a98e1466 svadmin-u-9c1295a6914a"
101
106
  >
102
- <option value="">Select an option...</option>
107
+ <option value="">{i18n.t('common.selectOption')}</option>
103
108
  {#each field.enum as option (option)}
104
109
  <option value={String(option)}>{option}</option>
105
110
  {/each}
@@ -7,6 +7,8 @@
7
7
  </script>
8
8
 
9
9
  <script lang="ts">
10
+ import { definedOptions } from '@svadmin/core/options';
11
+
10
12
  /* eslint-disable svelte/no-useless-children-snippet */
11
13
  import type { Snippet } from 'svelte';
12
14
  import { fade, fly } from 'svelte/transition';
@@ -18,7 +20,7 @@
18
20
  import { useTranslation } from '@svadmin/core/i18n';
19
21
 
20
22
  import { captureAdminContext } from '@svadmin/core';
21
- import type { Identity, MenuItem, TaskProvider, TaskRecord } from '@svadmin/core';
23
+ import type { Identity, MenuItem } from '@svadmin/core';
22
24
  import { getPath } from '../router-state.svelte.js';
23
25
  import { Skeleton } from './ui/skeleton/index.js';
24
26
  import * as Sheet from './ui/sheet/index.js';
@@ -42,7 +44,7 @@
42
44
  const auth = $derived(adminContext.authProvider);
43
45
  let loading = $state(true);
44
46
  let identity = $state<Identity | null>(null);
45
- const taskProvider = $derived(adminContext.taskProvider as TaskProvider<TaskRecord> | undefined);
47
+ const taskProvider = $derived(adminContext.taskProvider ?? undefined);
46
48
  const TaskQueueComponent = getComponentRegistry()?.TaskQueueDrawer;
47
49
 
48
50
  $effect(() => {
@@ -94,10 +96,10 @@
94
96
  let touchEndX = $state(0);
95
97
 
96
98
  function ownsLayoutEvent(event: Event): boolean {
97
- const target = event.target instanceof HTMLElement ? event.target : document.activeElement as HTMLElement | null;
99
+ const target = event.target instanceof Element ? event.target : document.activeElement;
98
100
  const owner = target
99
101
  ?.closest<HTMLElement>('[data-svadmin-layout-scope]')
100
- ?.dataset.svadminLayoutScope;
102
+ ?.dataset['svadminLayoutScope'];
101
103
  if (owner) return owner === layoutScope;
102
104
  return document.querySelectorAll('[data-svadmin-layout-scope]').length === 1;
103
105
  }
@@ -108,7 +110,7 @@
108
110
  e.preventDefault();
109
111
  commandOpen = true;
110
112
  }
111
- if (e.key === '?' && !['INPUT', 'TEXTAREA', 'SELECT'].includes((e.target as HTMLElement)?.tagName)) {
113
+ if (e.key === '?' && !(e.target instanceof Element && ['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName))) {
112
114
  e.preventDefault();
113
115
  shortcutsOpen = true;
114
116
  }
@@ -116,16 +118,18 @@
116
118
 
117
119
  function handleTouchStart(e: TouchEvent) {
118
120
  if (!ownsLayoutEvent(e)) return;
119
- if (e.touches.length === 1) {
120
- touchStartX = e.touches[0].clientX;
121
- touchEndX = e.touches[0].clientX;
121
+ const touch = e.touches.item(0);
122
+ if (e.touches.length === 1 && touch) {
123
+ touchStartX = touch.clientX;
124
+ touchEndX = touch.clientX;
122
125
  }
123
126
  }
124
127
 
125
128
  function handleTouchMove(e: TouchEvent) {
126
129
  if (!ownsLayoutEvent(e)) return;
127
- if (e.touches.length === 1) {
128
- touchEndX = e.touches[0].clientX;
130
+ const touch = e.touches.item(0);
131
+ if (e.touches.length === 1 && touch) {
132
+ touchEndX = touch.clientX;
129
133
  }
130
134
  }
131
135
 
@@ -180,7 +184,7 @@
180
184
 
181
185
  <!-- Desktop sidebar -->
182
186
  <div class="svadmin-u-99d72c7fc3e2 svadmin-u-9d60be3a6d80">
183
- <Sidebar {collapsed} {identity} {title} {menu} {routeMode} onToggle={() => collapsed = !collapsed} onLogout={handleLogout} />
187
+ <Sidebar {collapsed} {identity} {title} {...definedOptions({ "menu": menu })} {routeMode} onToggle={() => collapsed = !collapsed} onLogout={handleLogout} />
184
188
  </div>
185
189
 
186
190
  <!-- Mobile sidebar via Sheet -->
@@ -193,7 +197,7 @@
193
197
  >
194
198
  <Sheet.Title id={`${layoutScope}-mobile-navigation-title`} class="svadmin-u-2daa8e5e2f2e">{title}</Sheet.Title>
195
199
  <div class="svadmin-u-e477a6af4cb6">
196
- <Sidebar collapsed={false} {identity} {title} {menu} {routeMode} onToggle={() => { mobileMenuOpen = false; }} onLogout={handleLogout} />
200
+ <Sidebar collapsed={false} {identity} {title} {...definedOptions({ "menu": menu })} {routeMode} onToggle={() => { mobileMenuOpen = false; }} onLogout={handleLogout} />
197
201
  </div>
198
202
  </Sheet.Root>
199
203
 
@@ -206,8 +210,8 @@
206
210
  >
207
211
  <!-- Header with mobile hamburger -->
208
212
  <Header
209
- {siteUrl}
210
- {menu}
213
+ {...definedOptions({ "siteUrl": siteUrl })}
214
+ {...definedOptions({ "menu": menu })}
211
215
  showSearch={true}
212
216
  showThemeToggle={true}
213
217
  onSearchClick={() => { commandOpen = true; }}
@@ -255,7 +259,7 @@
255
259
  </div>
256
260
  <CommandPalette
257
261
  bind:open={commandOpen}
258
- onAskAI={aiAssistant ? handleAskAI : undefined}
262
+ {...definedOptions({ "onAskAI": aiAssistant ? handleAskAI : undefined })}
259
263
  />
260
264
  <KeyboardShortcuts bind:open={shortcutsOpen} />
261
265
  {/if}
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ import type { ComponentProps } from 'svelte';
3
+ import type TaskQueueDrawer from './TaskQueueDrawer.svelte';
4
+ import { definedOptions } from '@svadmin/core/options';
5
+ import { useTranslation } from '@svadmin/core/i18n';
6
+ import { ListTodo, RotateCcw } from '@lucide/svelte';
7
+ import { Button } from './ui/button/index.js';
8
+
9
+ let { open = $bindable(false), taskProvider, title, initialTab }: ComponentProps<typeof TaskQueueDrawer> = $props();
10
+ const i18n = useTranslation();
11
+ const load = () => import('./TaskQueueDrawer.svelte');
12
+ let module = $state(load());
13
+ const resolvedTitle = $derived(title ?? i18n.t('task.drawerTitle'));
14
+ </script>
15
+
16
+ {#await module}
17
+ <Button variant="ghost" size="icon" disabled aria-busy="true" aria-label={resolvedTitle}>
18
+ <ListTodo class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
19
+ </Button>
20
+ {:then { default: Drawer }}
21
+ <Drawer bind:open {...definedOptions({ taskProvider, title, initialTab })} />
22
+ {:catch}
23
+ <Button variant="ghost" size="icon" aria-label={`${resolvedTitle}: ${i18n.t('common.retry')}`} onclick={() => module = load()}>
24
+ <RotateCcw class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" />
25
+ </Button>
26
+ {/await}
@@ -0,0 +1,8 @@
1
+ declare const LazyTaskQueueDrawer: import("svelte").Component<{
2
+ open?: boolean;
3
+ taskProvider?: import("@svadmin/core").TaskProvider<import("@svadmin/core").TaskRecord>;
4
+ title?: string;
5
+ initialTab?: "dlq" | "tasks";
6
+ }, {}, "open">;
7
+ type LazyTaskQueueDrawer = ReturnType<typeof LazyTaskQueueDrawer>;
8
+ export default LazyTaskQueueDrawer;
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { definedOptions } from '@svadmin/core/options';
3
+
2
4
  import { getResource } from '@svadmin/core';
3
5
 
4
6
  import type { Snippet } from 'svelte';
@@ -82,11 +84,10 @@
82
84
  {resourceName}
83
85
  showHeader={false}
84
86
  {density}
85
- {selectable}
86
- {batchActions}
87
- defaultCellRenderer={cellRenderer}
88
- {rowActions}
89
- {emptyState}
90
- {expandedRowRender}
87
+ {...definedOptions({ "selectable": selectable })}
88
+ {...definedOptions({ batchActions })}
89
+ {...definedOptions({ "defaultCellRenderer": cellRenderer })}
90
+ {...definedOptions({ "rowActions": rowActions })}
91
+ {...definedOptions({ emptyState, expandedRowRender })}
91
92
  />
92
93
  </div>
@@ -18,7 +18,7 @@
18
18
 
19
19
  let {
20
20
  items = [],
21
- selectedId = $bindable(items[0]?.id as (string | number | undefined)),
21
+ selectedId = $bindable(items[0]?.['id'] as (string | number | undefined)),
22
22
  idKey = 'id',
23
23
  titleKey = 'title',
24
24
  subtitleKey = 'subtitle',
@@ -80,7 +80,7 @@
80
80
  {@render itemSnippet(item, isSelected)}
81
81
  {:else}
82
82
  <div class={cn('svadmin-u-e83a7042bc91 svadmin-u-f283ea9bea0e', isSelected ? 'svadmin-u-20aaf08a7ed1' : 'svadmin-u-d4108abe6359')}>
83
- {item[titleKey] ?? item.name ?? `Item #${item[idKey]}`}
83
+ {item[titleKey] ?? item['name'] ?? `Item #${item[idKey]}`}
84
84
  </div>
85
85
  {#if item[subtitleKey]}
86
86
  <div class="svadmin-u-d058ca6de60f svadmin-u-bfa603190748 svadmin-u-f283ea9bea0e svadmin-u-15e1b1f444fe">
@@ -108,7 +108,7 @@
108
108
  <div class="svadmin-u-3e7ce58d64fa">
109
109
  <div class="svadmin-u-7fcf9124b5df svadmin-u-65fdbade2025 svadmin-u-05faf5c801ff">
110
110
  <h3 class="svadmin-u-4ee734926ff6 svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">
111
- {activeItem[titleKey] ?? activeItem.name ?? `Item #${activeItem[idKey]}`}
111
+ {activeItem[titleKey] ?? activeItem['name'] ?? `Item #${activeItem[idKey]}`}
112
112
  </h3>
113
113
  {#if activeItem[subtitleKey]}
114
114
  <p class="svadmin-u-359090c2d529 svadmin-u-bfa603190748 svadmin-u-15e1b1f444fe">{activeItem[subtitleKey]}</p>
@@ -3,6 +3,7 @@
3
3
  import { Button } from './ui/button/index.js';
4
4
  import { Badge } from './ui/badge/index.js';
5
5
  import { Upload, Image as ImageIcon, Check, Search, Folder } from '@lucide/svelte';
6
+ import { useTranslation } from '@svadmin/core/i18n';
6
7
  import { cn } from '../utils.js';
7
8
 
8
9
  export interface MediaItem {
@@ -38,6 +39,8 @@
38
39
  class: className = '',
39
40
  }: Props = $props();
40
41
 
42
+ const i18n = useTranslation();
43
+
41
44
  let activeCategory = $state('All');
42
45
  let searchQuery = $state('');
43
46
  let localSelected = $state<string[]>([...selectedUrls]);
@@ -111,7 +114,7 @@
111
114
  <input
112
115
  type="text"
113
116
  bind:value={searchQuery}
114
- placeholder="Search media..."
117
+ placeholder={i18n.t('common.search')}
115
118
  class="svadmin-u-d1c57777d8b6 svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-d2d2d97dd0c8 svadmin-u-aa2c13a5e1b4 svadmin-u-359090c2d529 svadmin-u-f10f771f87e9 svadmin-u-3e94a98e1466 svadmin-u-9c1295a6914a"
116
119
  />
117
120
  </div>
@@ -120,7 +123,7 @@
120
123
  <input type="file" multiple class="svadmin-u-99d72c7fc3e2" onchange={handleFileInput} />
121
124
  <span class="svadmin-u-52083e7da442 svadmin-u-3960ffc248d9 svadmin-u-44ee8ba0a421 svadmin-u-d1c57777d8b6 svadmin-u-0b91436debbd svadmin-u-421ac2be5045 svadmin-u-ba939ea82d8b svadmin-u-5064267f78e3 svadmin-u-359090c2d529 svadmin-u-2689f3958069 svadmin-u-35b680bc2a46 svadmin-u-34516836730d">
122
125
  <Upload class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
123
- Upload
126
+ {i18n.t('common.upload')}
124
127
  </span>
125
128
  </label>
126
129
  </div>
@@ -65,7 +65,7 @@
65
65
 
66
66
  for (const trig of triggers) {
67
67
  const idx = textBeforeCursor.lastIndexOf(trig);
68
- if (idx !== -1 && (idx === 0 || /\s/.test(textBeforeCursor[idx - 1]))) {
68
+ if (idx !== -1 && (idx === 0 || /\s/.test(textBeforeCursor.charAt(idx - 1)))) {
69
69
  const query = textBeforeCursor.slice(idx + 1);
70
70
  if (!/\s/.test(query)) {
71
71
  matchTrigger = trig;
@@ -117,8 +117,9 @@
117
117
  selectedIndex = (selectedIndex - 1 + activeOptions.length) % activeOptions.length;
118
118
  } else if (e.key === 'Enter' || e.key === 'Tab') {
119
119
  e.preventDefault();
120
- if (activeOptions[selectedIndex]) {
121
- insertMention(activeOptions[selectedIndex]);
120
+ const selectedOption = activeOptions[selectedIndex];
121
+ if (selectedOption) {
122
+ insertMention(selectedOption);
122
123
  }
123
124
  } else if (e.key === 'Escape') {
124
125
  showDropdown = false;
@@ -7,9 +7,10 @@
7
7
  let navigated = false;
8
8
  $effect(() => {
9
9
  // Navigate to first resource ONCE — avoid infinite re-triggers
10
- if (!navigated && menuItems.length > 0) {
10
+ const firstMenuItem = menuItems[0];
11
+ if (!navigated && firstMenuItem) {
11
12
  navigated = true;
12
- adminContext.navigate(menuItems[0].route);
13
+ adminContext.navigate(firstMenuItem.route);
13
14
  }
14
15
  });
15
16
  </script>
@@ -48,8 +48,9 @@
48
48
  } = $props();
49
49
 
50
50
  $effect(() => {
51
- if (!selectedRole && roles.length > 0) {
52
- selectedRole = roles[0].code;
51
+ const firstRole = roles[0];
52
+ if (!selectedRole && firstRole) {
53
+ selectedRole = firstRole.code;
53
54
  }
54
55
  });
55
56
  </script>
@@ -163,7 +164,7 @@
163
164
  {/if}
164
165
  {#each resources as resource, i (i)}
165
166
  <!-- Section Grouping Header -->
166
- {#if resource.section && (i === 0 || resource.section !== resources[i-1].section)}
167
+ {#if resource.section && (i === 0 || resource.section !== resources[i-1]?.section)}
167
168
  <tr class="svadmin-u-989c466fdbe7">
168
169
  <td colspan="{actions.length + 1}" class="svadmin-u-f0faeb26d656 svadmin-u-03b4dd7f172b svadmin-u-359090c2d529 svadmin-u-69450ef1487e svadmin-u-20aaf08a7ed1">{resource.section}</td>
169
170
  </tr>
@@ -232,14 +232,14 @@
232
232
  <p class="svadmin-u-42536e69e639 svadmin-u-e83a7042bc91 svadmin-u-f283ea9bea0e">{identity.data.name ?? '—'}</p>
233
233
  </div>
234
234
 
235
- {#if identity.data.email || identity.data.username}
235
+ {#if identity.data.email || identity.data['username']}
236
236
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4 svadmin-u-fc7473ca09eb svadmin-u-bfa603190748">
237
237
  {#if identity.data.email}
238
238
  <Mail class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3 svadmin-u-012fbd121f37" />
239
239
  {:else}
240
240
  <User class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3 svadmin-u-012fbd121f37" />
241
241
  {/if}
242
- <span class="svadmin-u-f283ea9bea0e">{identity.data.email || identity.data.username}</span>
242
+ <span class="svadmin-u-f283ea9bea0e">{identity.data.email || identity.data['username']}</span>
243
243
  </div>
244
244
  {/if}
245
245
 
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ import { captureAdminContext } from '@svadmin/core';
3
+ import { useTranslation } from '@svadmin/core/i18n';
4
+ import AutoForm from './AutoForm.svelte';
5
+ import DetailDrawer from './DetailDrawer.svelte';
6
+
7
+ let { resourceName, recordId, onClose }: {
8
+ resourceName: string;
9
+ recordId: string | number;
10
+ onClose: () => void;
11
+ } = $props();
12
+ const context = captureAdminContext();
13
+ const i18n = useTranslation();
14
+ const resource = $derived(context.getResource(resourceName));
15
+ let open = $state(true);
16
+ let guardClose = $state<(close: () => void) => void>(() => {});
17
+ </script>
18
+
19
+ <DetailDrawer
20
+ bind:open
21
+ title={`${i18n.t('common.edit')} ${resource.label}`}
22
+ description={`#${recordId}`}
23
+ closeLabel={i18n.t('common.close')}
24
+ {onClose}
25
+ onCloseRequest={(close) => guardClose(close)}
26
+ data-svadmin-quick-edit
27
+ >
28
+ <AutoForm
29
+ {resourceName}
30
+ id={recordId}
31
+ mode="edit"
32
+ density="compact"
33
+ showHeader={false}
34
+ redirect={false}
35
+ onSuccess={onClose}
36
+ onCancel={onClose}
37
+ onNavigationGuardReady={(guard) => guardClose = guard}
38
+ />
39
+ </DetailDrawer>
@@ -0,0 +1,8 @@
1
+ type $$ComponentProps = {
2
+ resourceName: string;
3
+ recordId: string | number;
4
+ onClose: () => void;
5
+ };
6
+ declare const QuickEditDrawer: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type QuickEditDrawer = ReturnType<typeof QuickEditDrawer>;
8
+ export default QuickEditDrawer;