@svadmin/lite 0.3.10 → 0.3.13

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 (55) hide show
  1. package/README.md +30 -5
  2. package/dist/components/LiteArrayItem.svelte +31 -10
  3. package/dist/components/LiteAuditLog.svelte +8 -17
  4. package/dist/components/LiteAuditLog.svelte.d.ts +5 -13
  5. package/dist/components/LiteForm.svelte +37 -7
  6. package/dist/components/LiteForm.svelte.d.ts +1 -1
  7. package/dist/components/LiteLayout.svelte +12 -7
  8. package/dist/components/LiteLayout.svelte.d.ts +2 -0
  9. package/dist/components/LitePermissionMatrix.svelte +48 -15
  10. package/dist/components/LitePermissionMatrix.svelte.d.ts +10 -9
  11. package/dist/components/LiteShow.svelte +3 -2
  12. package/dist/components/LiteShowField.svelte +5 -3
  13. package/dist/components/LiteTable.svelte +19 -9
  14. package/dist/components/LiteTable.svelte.d.ts +1 -0
  15. package/dist/components/advanced/LiteVirtualTable.svelte +5 -2
  16. package/dist/components/advanced/LiteVirtualTable.svelte.d.ts +1 -0
  17. package/dist/components/buttons/LiteCloneButton.svelte +2 -2
  18. package/dist/components/buttons/LiteCreateButton.svelte +2 -2
  19. package/dist/components/buttons/LiteDeleteButton.svelte +2 -2
  20. package/dist/components/buttons/LiteEditButton.svelte +2 -2
  21. package/dist/components/buttons/LiteExportButton.svelte +2 -2
  22. package/dist/components/buttons/LiteImportButton.svelte +2 -2
  23. package/dist/components/buttons/LiteListButton.svelte +2 -2
  24. package/dist/components/buttons/LiteRefreshButton.svelte +7 -11
  25. package/dist/components/buttons/LiteSaveButton.svelte +2 -2
  26. package/dist/components/buttons/LiteShowButton.svelte +2 -2
  27. package/dist/components/fields/LiteBooleanField.svelte +9 -8
  28. package/dist/components/fields/LiteDateField.svelte +2 -2
  29. package/dist/components/fields/LiteEmailField.svelte +1 -2
  30. package/dist/components/fields/LiteImageField.svelte +20 -11
  31. package/dist/components/fields/LiteNumberField.svelte +1 -2
  32. package/dist/components/fields/LiteRelationField.svelte +15 -7
  33. package/dist/components/fields/LiteSelectField.svelte +2 -1
  34. package/dist/components/fields/LiteTextField.svelte +1 -2
  35. package/dist/components/fields/LiteUrlField.svelte +1 -5
  36. package/dist/components/layout/LiteCatchAllNavigate.svelte +3 -9
  37. package/dist/components/layout/LiteNavigateToResource.svelte +5 -16
  38. package/dist/components/layout/LiteSidebar.svelte +9 -5
  39. package/dist/components/layout/LiteSidebar.svelte.d.ts +1 -0
  40. package/dist/components/pages/LiteEditPage.svelte +6 -4
  41. package/dist/components/pages/LiteListPage.svelte +14 -6
  42. package/dist/components/pages/LiteListPage.svelte.d.ts +1 -0
  43. package/dist/components/pages/LiteShowPage.svelte +6 -4
  44. package/dist/components/widgets/LiteAnomalyBadge.svelte +5 -1
  45. package/dist/index.js +1 -1
  46. package/dist/lite.css +2 -1
  47. package/dist/menu-visibility.d.ts +4 -0
  48. package/dist/menu-visibility.js +34 -0
  49. package/dist/schema-generator.d.ts +1 -1
  50. package/dist/schema-generator.js +112 -51
  51. package/dist/server-adapter.d.ts +53 -4
  52. package/dist/server-adapter.js +235 -67
  53. package/dist/value-normalization.d.ts +2 -0
  54. package/dist/value-normalization.js +21 -0
  55. package/package.json +14 -22
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import type { ResourceDefinition, FieldDefinition } from '@svadmin/core';
7
7
  import { t } from '@svadmin/core/i18n';
8
+ import { isExplicitBooleanTrue } from '../value-normalization';
8
9
 
9
10
  interface Props {
10
11
  records: Record<string, unknown>[];
@@ -14,6 +15,7 @@
14
15
  currentSearch?: string;
15
16
  basePath?: string;
16
17
  /** Show edit/delete action buttons */
18
+ canShow?: boolean;
17
19
  canEdit?: boolean;
18
20
  canDelete?: boolean;
19
21
  }
@@ -25,11 +27,15 @@
25
27
  currentOrder = 'asc',
26
28
  currentSearch,
27
29
  basePath = '/lite',
28
- canEdit = true,
29
- canDelete = true,
30
+ canShow,
31
+ canEdit,
32
+ canDelete,
30
33
  }: Props = $props();
31
34
 
32
35
  let pk = $derived(resource.primaryKey ?? 'id');
36
+ const showView = $derived(canShow ?? resource.canShow !== false);
37
+ const showEdit = $derived(canEdit ?? resource.canEdit !== false);
38
+ const showDelete = $derived(canDelete ?? resource.canDelete !== false);
33
39
  const listFields = $derived(
34
40
  resource.fields.filter(f => f.showInList !== false)
35
41
  );
@@ -53,12 +59,13 @@
53
59
  try { return new Date(value as string).toLocaleDateString(); } catch { return String(value); }
54
60
  }
55
61
  if (field.type === 'select' && field.options) {
56
- const opt = field.options.find(o => o.value === value);
62
+ const opt = field.options.find(o => String(o.value) === String(value));
57
63
  return opt?.label ?? String(value);
58
64
  }
59
65
  if (Array.isArray(value)) return value.join(', ');
60
66
  return String(value);
61
67
  }
68
+
62
69
  </script>
63
70
 
64
71
  <table class="lite-table">
@@ -76,7 +83,7 @@
76
83
  {/if}
77
84
  </th>
78
85
  {/each}
79
- {#if canEdit || canDelete}
86
+ {#if showView || showEdit || showDelete}
80
87
  <th style="text-align:right;">{t('common.actions') || 'Actions'}</th>
81
88
  {/if}
82
89
  </tr>
@@ -88,7 +95,7 @@
88
95
  {#each listFields as field, _i (_i)}
89
96
  <td>
90
97
  {#if field.type === 'boolean'}
91
- <span class="lite-bool {record[field.key] ? 'lite-bool-true' : ''}"></span>
98
+ <span class="lite-bool {isExplicitBooleanTrue(record[field.key]) ? 'lite-bool-true' : ''}"></span>
92
99
  {:else if field.type === 'tags' && Array.isArray(record[field.key])}
93
100
  {#each (record[field.key] as string[]).slice(0, 3) as tag, _i (_i)}
94
101
  <span class="lite-badge">{tag}</span>
@@ -100,12 +107,15 @@
100
107
  {/if}
101
108
  </td>
102
109
  {/each}
103
- {#if canEdit || canDelete}
110
+ {#if showView || showEdit || showDelete}
104
111
  <td class="actions">
105
- {#if canEdit}
112
+ {#if showView}
113
+ <a href={`${basePath}/${resource.name}/show/${id}`} class="lite-btn lite-btn-sm">{t('common.show') || 'Show'}</a>
114
+ {/if}
115
+ {#if showEdit}
106
116
  <a href={`${basePath}/${resource.name}/edit/${id}`} class="lite-btn lite-btn-sm">{t('common.edit') || 'Edit'}</a>
107
117
  {/if}
108
- {#if canDelete}
118
+ {#if showDelete}
109
119
  <!-- Delete uses <details> for no-JS confirmation -->
110
120
  <details class="lite-confirm-details">
111
121
  <summary class="lite-btn lite-btn-sm lite-btn-danger">{t('common.delete') || 'Delete'}</summary>
@@ -123,7 +133,7 @@
123
133
  </tr>
124
134
  {:else}
125
135
  <tr>
126
- <td colspan={listFields.length + (canEdit || canDelete ? 1 : 0)} style="text-align:center;padding:40px;color:#9ca3af;">
136
+ <td colspan={listFields.length + (showView || showEdit || showDelete ? 1 : 0)} style="text-align:center;padding:40px;color:#9ca3af;">
127
137
  {t('common.noData') || 'No records found.'}
128
138
  </td>
129
139
  </tr>
@@ -11,6 +11,7 @@ interface Props {
11
11
  currentSearch?: string;
12
12
  basePath?: string;
13
13
  /** Show edit/delete action buttons */
14
+ canShow?: boolean;
14
15
  canEdit?: boolean;
15
16
  canDelete?: boolean;
16
17
  }
@@ -15,6 +15,7 @@
15
15
  currentOrder?: 'asc' | 'desc';
16
16
  currentSearch?: string;
17
17
  basePath?: string;
18
+ canShow?: boolean;
18
19
  canEdit?: boolean;
19
20
  canDelete?: boolean;
20
21
  }
@@ -26,8 +27,9 @@
26
27
  currentOrder = 'asc',
27
28
  currentSearch,
28
29
  basePath = '/lite',
29
- canEdit = true,
30
- canDelete = true,
30
+ canShow,
31
+ canEdit,
32
+ canDelete,
31
33
  }: Props = $props();
32
34
  </script>
33
35
 
@@ -39,6 +41,7 @@
39
41
  {currentOrder}
40
42
  {currentSearch}
41
43
  {basePath}
44
+ {canShow}
42
45
  {canEdit}
43
46
  {canDelete}
44
47
  />
@@ -12,6 +12,7 @@ interface Props {
12
12
  currentOrder?: 'asc' | 'desc';
13
13
  currentSearch?: string;
14
14
  basePath?: string;
15
+ canShow?: boolean;
15
16
  canEdit?: boolean;
16
17
  canDelete?: boolean;
17
18
  }
@@ -26,8 +26,8 @@
26
26
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
27
27
  title={t('common.clone') || 'Clone'}
28
28
  >
29
- <Copy class="h-4 w-4" />
29
+ <Copy size={16} />
30
30
  {#if !hideText}
31
- <span style="marginLeft: 4px">{t('common.clone') || 'Clone'}</span>
31
+ <span style="margin-left: 4px">{t('common.clone') || 'Clone'}</span>
32
32
  {/if}
33
33
  </a>
@@ -24,8 +24,8 @@
24
24
  class="lite-btn lite-btn-primary {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
25
25
  title={t('common.create') || 'Create'}
26
26
  >
27
- <Plus class="h-4 w-4" />
27
+ <Plus size={16} />
28
28
  {#if !hideText}
29
- <span style="marginLeft: 4px">{t('common.create') || 'Create'}</span>
29
+ <span style="margin-left: 4px">{t('common.create') || 'Create'}</span>
30
30
  {/if}
31
31
  </a>
@@ -28,9 +28,9 @@
28
28
  class="lite-btn lite-btn-danger {size === 'sm' ? 'lite-btn-sm' : ''}"
29
29
  title={t('common.delete') || 'Delete'}
30
30
  >
31
- <Trash2 class="h-4 w-4" />
31
+ <Trash2 size={16} />
32
32
  {#if !hideText}
33
- <span style="marginLeft: 4px">{t('common.delete') || 'Delete'}</span>
33
+ <span style="margin-left: 4px">{t('common.delete') || 'Delete'}</span>
34
34
  {/if}
35
35
  </summary>
36
36
  <div class="lite-confirm-panel" style="right: 0; left: auto;">
@@ -26,8 +26,8 @@
26
26
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
27
27
  title={t('common.edit') || 'Edit'}
28
28
  >
29
- <Pencil class="h-4 w-4" />
29
+ <Pencil size={16} />
30
30
  {#if !hideText}
31
- <span style="marginLeft: 4px">{t('common.edit') || 'Edit'}</span>
31
+ <span style="margin-left: 4px">{t('common.edit') || 'Edit'}</span>
32
32
  {/if}
33
33
  </a>
@@ -24,8 +24,8 @@
24
24
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
25
25
  title={t('common.export') || 'Export'}
26
26
  >
27
- <Download class="h-4 w-4" />
27
+ <Download size={16} />
28
28
  {#if !hideText}
29
- <span style="marginLeft: 4px">{t('common.export') || 'Export'}</span>
29
+ <span style="margin-left: 4px">{t('common.export') || 'Export'}</span>
30
30
  {/if}
31
31
  </a>
@@ -24,9 +24,9 @@
24
24
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''}"
25
25
  title={t('common.import') || 'Import'}
26
26
  >
27
- <Upload class="h-4 w-4" />
27
+ <Upload size={16} />
28
28
  {#if !hideText}
29
- <span style="marginLeft: 4px">{t('common.import') || 'Import'}</span>
29
+ <span style="margin-left: 4px">{t('common.import') || 'Import'}</span>
30
30
  {/if}
31
31
  </summary>
32
32
  <div class="lite-confirm-panel">
@@ -24,8 +24,8 @@
24
24
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
25
25
  title={t('common.list') || 'List'}
26
26
  >
27
- <List class="h-4 w-4" />
27
+ <List size={16} />
28
28
  {#if !hideText}
29
- <span style="marginLeft: 4px">{t('common.list') || 'List'}</span>
29
+ <span style="margin-left: 4px">{t('common.list') || 'List'}</span>
30
30
  {/if}
31
31
  </a>
@@ -14,21 +14,17 @@
14
14
  size = 'default'
15
15
  }: Props = $props();
16
16
 
17
- function refreshPage() {
18
- if (typeof window !== 'undefined') {
19
- window.location.reload();
20
- }
21
- }
22
17
  </script>
23
18
 
24
- <button
25
- type="button"
19
+ <!-- The empty URL intentionally reloads the exact current document, including its query string. -->
20
+ <!-- svelte-ignore a11y_invalid_attribute -->
21
+ <a
22
+ href=""
26
23
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
27
24
  title={t('common.refresh') || 'Refresh'}
28
- onclick={refreshPage}
29
25
  >
30
- <RefreshCw class="h-4 w-4" />
26
+ <RefreshCw size={16} />
31
27
  {#if !hideText}
32
- <span style="marginLeft: 4px">{t('common.refresh') || 'Refresh'}</span>
28
+ <span style="margin-left: 4px">{t('common.refresh') || 'Refresh'}</span>
33
29
  {/if}
34
- </button>
30
+ </a>
@@ -20,8 +20,8 @@
20
20
  class="lite-btn lite-btn-primary {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
21
21
  title={t('common.save') || 'Save'}
22
22
  >
23
- <Save class="h-4 w-4" />
23
+ <Save size={16} />
24
24
  {#if !hideText}
25
- <span style="marginLeft: 4px">{t('common.save') || 'Save'}</span>
25
+ <span style="margin-left: 4px">{t('common.save') || 'Save'}</span>
26
26
  {/if}
27
27
  </button>
@@ -26,8 +26,8 @@
26
26
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
27
27
  title={t('common.show') || 'Show'}
28
28
  >
29
- <Eye class="h-4 w-4" />
29
+ <Eye size={16} />
30
30
  {#if !hideText}
31
- <span style="marginLeft: 4px">{t('common.show') || 'Show'}</span>
31
+ <span style="margin-left: 4px">{t('common.show') || 'Show'}</span>
32
32
  {/if}
33
33
  </a>
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
3
2
  import type { FieldDefinition } from '@svadmin/core';
3
+ import { isExplicitBooleanTrue } from '../../value-normalization';
4
4
 
5
5
  interface Props {
6
6
  field: FieldDefinition;
@@ -11,12 +11,14 @@
11
11
 
12
12
  let { field, value, error = [], mode = 'show' }: Props = $props();
13
13
  let hasError = $derived(error.length > 0);
14
+
15
+ const checked = $derived(isExplicitBooleanTrue(value));
14
16
  </script>
15
17
 
16
18
  {#if mode === 'show'}
17
19
  <div>
18
- <span class="lite-bool {value ? 'lite-bool-true' : ''}"></span>
19
- {value ? '✓ Yes' : '✗ No'}
20
+ <span class="lite-bool {checked ? 'lite-bool-true' : ''}"></span>
21
+ {checked ? '✓ Yes' : '✗ No'}
20
22
  </div>
21
23
  {:else}
22
24
  <div class="lite-checkbox-group">
@@ -24,15 +26,14 @@
24
26
  type="checkbox"
25
27
  name={field.key}
26
28
  id={field.key}
27
- checked={!!value}
29
+ checked={checked}
28
30
  value="true"
29
31
  />
30
- <!-- Fallback hidden input so unchecked boxes submit "false" -->
31
- <input type="hidden" name={field.key} value="false" />
32
-
33
32
  <label for={field.key}>
34
- {(field as any).placeholder ?? (field.label || 'Yes')}
33
+ {field.label || 'Yes'}
35
34
  </label>
35
+ <!-- Keep the label adjacent for the CSS checked/focus selector. -->
36
+ <input type="hidden" name={field.key} value="false" />
36
37
  {#if hasError}
37
38
  {#each error as err, _i (_i)}
38
39
  <div class="lite-error-text">{err}</div>
@@ -14,7 +14,7 @@
14
14
  function formatDate(v: unknown): string {
15
15
  if (!v) return '';
16
16
  try {
17
- return new Date(v as string).toISOString().substring(0, 16); // Formats for html datetime-local
17
+ return new Date(v as string).toISOString().substring(0, 16);
18
18
  } catch {
19
19
  return '';
20
20
  }
@@ -23,7 +23,7 @@
23
23
  function displayDate(v: unknown): string {
24
24
  if (!v) return '—';
25
25
  try {
26
- return new Date(v as string).toLocaleString();
26
+ return new Date(v as string).toLocaleDateString();
27
27
  } catch {
28
28
  return String(v);
29
29
  }
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
3
2
  import type { FieldDefinition } from '@svadmin/core';
4
3
 
5
4
  interface Props {
@@ -29,7 +28,7 @@
29
28
  id={field.key}
30
29
  value={String(value ?? '')}
31
30
  class="lite-input {hasError ? 'lite-input-error' : ''}"
32
- placeholder={(field as any).placeholder ?? field.label}
31
+ placeholder={field.label}
33
32
  {...field.required ? { required: true } : {}}
34
33
  />
35
34
  {#if hasError}
@@ -31,23 +31,32 @@
31
31
  {:else}
32
32
  {@const urls = getUrls(value)}
33
33
  <div>
34
- {#if urls.length > 0 && mode === 'edit'}
34
+ {#if urls.length > 0}
35
35
  <div style="margin-bottom: 8px; display:flex; gap: 8px;">
36
36
  {#each urls as url, _i (_i)}
37
37
  <img src={url} alt="Current" style="height: 60px; border-radius: 4px; border: 1px solid #e5e7eb; opacity: 0.6;" />
38
38
  {/each}
39
39
  </div>
40
- <p style="font-size: 11px; color:#6b7280; margin: 0 0 4px;">Uploading new ones will overwrite.</p>
41
40
  {/if}
42
- <input
43
- type="file"
44
- name={field.key}
45
- id={field.key}
46
- accept="image/*"
47
- class="lite-input {hasError ? 'lite-input-error' : ''}"
48
- {...(field.type as string) === 'images' ? { multiple: true } : {}}
49
- {...field.required && !urls.length ? { required: true } : {}}
50
- />
41
+ {#if field.type === 'images'}
42
+ <textarea
43
+ name={field.key}
44
+ id={field.key}
45
+ class="lite-input {hasError ? 'lite-input-error' : ''}"
46
+ placeholder="One image URL per line"
47
+ required={field.required}
48
+ >{urls.join('\n')}</textarea>
49
+ {:else}
50
+ <input
51
+ type="text"
52
+ name={field.key}
53
+ id={field.key}
54
+ value={urls[0] ?? ''}
55
+ placeholder="https://example.com/image.jpg"
56
+ class="lite-input {hasError ? 'lite-input-error' : ''}"
57
+ required={field.required}
58
+ />
59
+ {/if}
51
60
  {#if hasError}
52
61
  {#each error as err, _i (_i)}
53
62
  <div class="lite-error-text">{err}</div>
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
3
2
  import type { FieldDefinition } from '@svadmin/core';
4
3
 
5
4
  interface Props {
@@ -23,7 +22,7 @@
23
22
  id={field.key}
24
23
  value={value == null ? '' : Number(value)}
25
24
  class="lite-input {hasError ? 'lite-input-error' : ''}"
26
- placeholder={(field as any).placeholder ?? field.label}
25
+ placeholder={field.label}
27
26
  {...field.required ? { required: true } : {}}
28
27
  />
29
28
  {#if hasError}
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
3
2
  import type { FieldDefinition } from '@svadmin/core';
4
3
  import { t } from '@svadmin/core/i18n';
5
4
 
@@ -17,10 +16,13 @@
17
16
  // passed through options, or just standard inputs requesting ID.
18
17
  function displayRelation(v: unknown): string {
19
18
  if (v == null) return '—';
20
- if (typeof v === 'object' && v !== null && 'id' in v) {
21
- // It's a populated relation object
22
- const labelField = (field as any).relation?.labelField || 'name';
23
- return String((v as Record<string, unknown>)[labelField] || (v as Record<string, unknown>).id);
19
+ if (typeof v === 'object' && v !== null) {
20
+ const record = v as Record<string, unknown>;
21
+ const idField = field.optionValue ?? 'id';
22
+ const labelField = field.optionLabel ?? 'name';
23
+ if (record[idField] != null) {
24
+ return String(record[labelField] ?? record[idField]);
25
+ }
24
26
  }
25
27
  // Try options
26
28
  const opt = field.options?.find(o => String(o.value) === String(v));
@@ -38,10 +40,14 @@
38
40
  name={field.key}
39
41
  id={field.key}
40
42
  class="lite-select {hasError ? 'lite-input-error' : ''}"
43
+ value={typeof value === 'object' && value
44
+ ? String((value as Record<string, unknown>)[field.optionValue ?? 'id'] ?? '')
45
+ : String(value ?? '')}
46
+ required={field.required}
41
47
  >
42
48
  <option value="">-- {t('common.select') || 'Select'} Reference --</option>
43
49
  {#each field.options as opt, _i (_i)}
44
- <option value={String(opt.value)} selected={String(value) === String(opt.value)}>
50
+ <option value={String(opt.value)}>
45
51
  {opt.label}
46
52
  </option>
47
53
  {/each}
@@ -52,7 +58,9 @@
52
58
  type="text"
53
59
  name={field.key}
54
60
  id={field.key}
55
- value={typeof value === 'object' && value ? String((value as Record<string, unknown>).id || '') : String(value ?? '')}
61
+ value={typeof value === 'object' && value
62
+ ? String((value as Record<string, unknown>)[field.optionValue ?? 'id'] ?? '')
63
+ : String(value ?? '')}
56
64
  class="lite-input {hasError ? 'lite-input-error' : ''}"
57
65
  placeholder="Enter reference ID"
58
66
  {...field.required ? { required: true } : {}}
@@ -27,12 +27,13 @@
27
27
  name={field.key}
28
28
  id={field.key}
29
29
  class="lite-select {hasError ? 'lite-input-error' : ''}"
30
+ value={String(value ?? '')}
30
31
  {...field.required ? { required: true } : {}}
31
32
  >
32
33
  <option value="">-- {t('common.select') || 'Select'} --</option>
33
34
  {#if field.options}
34
35
  {#each field.options as opt, _i (_i)}
35
- <option value={String(opt.value)} selected={String(value) === String(opt.value)}>
36
+ <option value={String(opt.value)}>
36
37
  {opt.label}
37
38
  </option>
38
39
  {/each}
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
3
2
  import type { FieldDefinition } from '@svadmin/core';
4
3
 
5
4
  interface Props {
@@ -23,7 +22,7 @@
23
22
  id={field.key}
24
23
  value={String(value ?? '')}
25
24
  class="lite-input {hasError ? 'lite-input-error' : ''}"
26
- placeholder={(field as any).placeholder ?? field.label}
25
+ placeholder={field.label}
27
26
  {...field.required ? { required: true } : {}}
28
27
  />
29
28
  {#if hasError}
@@ -13,11 +13,7 @@
13
13
  let hasError = $derived(error.length > 0);
14
14
  let displayValue = $derived(toSafeText(value));
15
15
  let safeHref = $derived(toSafeHref(value));
16
- const placeholder = $derived(
17
- 'placeholder' in field && typeof field.placeholder === 'string'
18
- ? field.placeholder
19
- : field.label
20
- );
16
+ const placeholder = $derived(field.label);
21
17
  </script>
22
18
 
23
19
  {#if mode === 'show'}
@@ -1,6 +1,4 @@
1
1
  <script lang="ts">
2
- import { onMount } from 'svelte';
3
-
4
2
  interface Props {
5
3
  basePath?: string;
6
4
  defaultResource?: string;
@@ -8,13 +6,9 @@
8
6
 
9
7
  let { basePath = '/lite', defaultResource = '' }: Props = $props();
10
8
  const target = $derived(`${basePath}${defaultResource ? '/' + defaultResource : ''}`);
11
-
12
- onMount(() => {
13
- window.location.href = target;
14
- });
15
9
  </script>
16
10
 
17
- <noscript>
11
+ <svelte:head>
18
12
  <meta http-equiv="refresh" content={`0; url=${target}`} />
19
- </noscript>
20
- <p>Redirecting...</p>
13
+ </svelte:head>
14
+ <p>Redirecting to <a href={target}>{target}</a>...</p>
@@ -1,25 +1,14 @@
1
1
  <script lang="ts">
2
- /**
3
- * SSR redirection helper component
4
- * Normally redirection is done via +page.server.ts load() function in SvelteKit.
5
- * This component will do a client-side fallback redirect if rendered.
6
- */
7
- import { onMount } from 'svelte';
8
-
9
2
  interface Props {
10
3
  resource: string;
11
4
  basePath?: string;
12
5
  }
13
6
 
14
7
  let { resource, basePath = '/lite' }: Props = $props();
15
-
16
- onMount(() => {
17
- // Only fires if JS somehow runs, otherwise server should have redirected.
18
- window.location.href = `${basePath}/${resource}`;
19
- });
8
+ const target = $derived(`${basePath}/${resource}`);
20
9
  </script>
21
10
 
22
- <noscript>
23
- <meta http-equiv="refresh" content={`0; url=${basePath}/${resource}`} />
24
- </noscript>
25
- <p>Redirecting to {resource}...</p>
11
+ <svelte:head>
12
+ <meta http-equiv="refresh" content={`0; url=${target}`} />
13
+ </svelte:head>
14
+ <p>Redirecting to <a href={target}>{resource}</a>...</p>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { ResourceDefinition, MenuItem } from '@svadmin/core';
3
+ import { filterVisibleMenu, filterVisibleResources } from '../../menu-visibility';
3
4
 
4
5
  interface Props {
5
6
  resources: ResourceDefinition[];
@@ -8,6 +9,7 @@
8
9
  userName?: string;
9
10
  basePath?: string;
10
11
  menu?: MenuItem[];
12
+ canAccess?: (resource: string, action: string) => boolean;
11
13
  }
12
14
 
13
15
  let {
@@ -17,11 +19,13 @@
17
19
  userName = '',
18
20
  basePath = '/lite',
19
21
  menu,
22
+ canAccess,
20
23
  }: Props = $props();
21
24
 
22
- const menuResources = $derived(
23
- resources.filter((r: ResourceDefinition) => r.showInMenu !== false)
24
- );
25
+ const menuResources = $derived(filterVisibleResources(resources, canAccess));
26
+
27
+ const hasCustomMenu = $derived(menu !== undefined);
28
+ const visibleMenu = $derived(menu ? filterVisibleMenu(menu, canAccess) : []);
25
29
 
26
30
  function isActive(href: string | undefined): boolean {
27
31
  if (!href) return false;
@@ -36,8 +40,8 @@
36
40
 
37
41
  <nav class="lite-sidebar">
38
42
  <div class="lite-sidebar-brand">{brandName}</div>
39
- {#if menu && menu.length > 0}
40
- {#each menu as item, _i (_i)}
43
+ {#if hasCustomMenu}
44
+ {#each visibleMenu as item, _i (_i)}
41
45
  {#if item.children && item.children.length > 0}
42
46
  <details class="lite-menu-group" open={hasActiveChild(item.children)}>
43
47
  <summary class="lite-menu-parent">{item.label ?? item.name}</summary>
@@ -6,6 +6,7 @@ interface Props {
6
6
  userName?: string;
7
7
  basePath?: string;
8
8
  menu?: MenuItem[];
9
+ canAccess?: (resource: string, action: string) => boolean;
9
10
  }
10
11
  declare const LiteSidebar: import("svelte").Component<Props, {}, "">;
11
12
  type LiteSidebar = ReturnType<typeof LiteSidebar>;