@svadmin/lite 0.1.0 → 0.2.1

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 (76) hide show
  1. package/README.md +49 -6
  2. package/package.json +5 -1
  3. package/src/components/LiteArrayField.svelte +112 -0
  4. package/src/components/LiteAuditLog.svelte +104 -0
  5. package/src/components/LiteBreadcrumbs.svelte +39 -0
  6. package/src/components/LiteChatDialog.svelte +101 -0
  7. package/src/components/LiteConfirmDialog.svelte +60 -0
  8. package/src/components/LiteEmptyState.svelte +39 -0
  9. package/src/components/LiteLayout.svelte +58 -10
  10. package/src/components/LitePermissionMatrix.svelte +147 -0
  11. package/src/components/LiteShow.svelte +3 -38
  12. package/src/components/LiteShowField.svelte +51 -0
  13. package/src/components/LiteStatsCard.svelte +70 -0
  14. package/src/components/LiteTabs.svelte +57 -0
  15. package/src/components/advanced/LiteAutoSaveIndicator.svelte +26 -0
  16. package/src/components/advanced/LiteDraggableHeader.svelte +33 -0
  17. package/src/components/advanced/LiteDrawerForm.svelte +42 -0
  18. package/src/components/advanced/LiteInlineEdit.svelte +32 -0
  19. package/src/components/advanced/LiteModalForm.svelte +44 -0
  20. package/src/components/advanced/LiteToast.svelte +34 -0
  21. package/src/components/advanced/LiteUndoableNotification.svelte +25 -0
  22. package/src/components/advanced/LiteVirtualTable.svelte +44 -0
  23. package/src/components/advanced/index.ts +8 -0
  24. package/src/components/buttons/LiteCloneButton.svelte +33 -0
  25. package/src/components/buttons/LiteCreateButton.svelte +31 -0
  26. package/src/components/buttons/LiteDeleteButton.svelte +57 -0
  27. package/src/components/buttons/LiteEditButton.svelte +33 -0
  28. package/src/components/buttons/LiteExportButton.svelte +31 -0
  29. package/src/components/buttons/LiteImportButton.svelte +50 -0
  30. package/src/components/buttons/LiteListButton.svelte +31 -0
  31. package/src/components/buttons/LiteRefreshButton.svelte +27 -0
  32. package/src/components/buttons/LiteSaveButton.svelte +27 -0
  33. package/src/components/buttons/LiteShowButton.svelte +33 -0
  34. package/src/components/buttons/index.ts +10 -0
  35. package/src/components/fields/LiteBooleanField.svelte +41 -0
  36. package/src/components/fields/LiteDateField.svelte +51 -0
  37. package/src/components/fields/LiteEmailField.svelte +40 -0
  38. package/src/components/fields/LiteFileField.svelte +53 -0
  39. package/src/components/fields/LiteImageField.svelte +57 -0
  40. package/src/components/fields/LiteJsonField.svelte +43 -0
  41. package/src/components/fields/LiteMarkdownField.svelte +33 -0
  42. package/src/components/fields/LiteMultiSelectField.svelte +57 -0
  43. package/src/components/fields/LiteNumberField.svelte +34 -0
  44. package/src/components/fields/LiteRelationField.svelte +66 -0
  45. package/src/components/fields/LiteRichTextField.svelte +45 -0
  46. package/src/components/fields/LiteSelectField.svelte +47 -0
  47. package/src/components/fields/LiteTagField.svelte +44 -0
  48. package/src/components/fields/LiteTextField.svelte +34 -0
  49. package/src/components/fields/LiteUrlField.svelte +40 -0
  50. package/src/components/fields/index.ts +15 -0
  51. package/src/components/layout/LiteAuthenticated.svelte +23 -0
  52. package/src/components/layout/LiteCanAccess.svelte +27 -0
  53. package/src/components/layout/LiteCatchAllNavigate.svelte +20 -0
  54. package/src/components/layout/LiteErrorBoundary.svelte +20 -0
  55. package/src/components/layout/LiteErrorComponent.svelte +37 -0
  56. package/src/components/layout/LiteHeader.svelte +19 -0
  57. package/src/components/layout/LiteNavigateToResource.svelte +25 -0
  58. package/src/components/layout/LiteSidebar.svelte +93 -0
  59. package/src/components/layout/index.ts +8 -0
  60. package/src/components/pages/LiteCreatePage.svelte +39 -0
  61. package/src/components/pages/LiteEditPage.svelte +54 -0
  62. package/src/components/pages/LiteForgotPasswordPage.svelte +60 -0
  63. package/src/components/pages/LiteListPage.svelte +77 -0
  64. package/src/components/pages/LiteProfilePage.svelte +61 -0
  65. package/src/components/pages/LiteRegisterPage.svelte +64 -0
  66. package/src/components/pages/LiteShowPage.svelte +61 -0
  67. package/src/components/pages/LiteUpdatePasswordPage.svelte +51 -0
  68. package/src/components/pages/index.ts +8 -0
  69. package/src/components/widgets/LiteAnomalyBadge.svelte +40 -0
  70. package/src/components/widgets/LiteBarChart.svelte +45 -0
  71. package/src/components/widgets/LiteInsightCard.svelte +28 -0
  72. package/src/components/widgets/LiteLineChart.svelte +48 -0
  73. package/src/components/widgets/LitePieChart.svelte +44 -0
  74. package/src/components/widgets/index.ts +5 -0
  75. package/src/index.ts +28 -0
  76. package/src/lite.css +372 -124
@@ -0,0 +1,147 @@
1
+ <script lang="ts">
2
+ /**
3
+ * LitePermissionMatrix — SSR-compatible permission matrix.
4
+ * Pure HTML table with checkbox inputs. No client-side JS required.
5
+ * Works with form POST actions for state changes.
6
+ */
7
+
8
+ interface LiteRole {
9
+ code: string;
10
+ name: string;
11
+ }
12
+
13
+ interface LiteResource {
14
+ code: string;
15
+ name: string;
16
+ section?: string;
17
+ }
18
+
19
+ interface LiteAction {
20
+ code: string;
21
+ name: string;
22
+ }
23
+
24
+ interface Props {
25
+ roles: LiteRole[];
26
+ resources: LiteResource[];
27
+ actions: LiteAction[];
28
+ /** Current permissions map: { "resource:action": true } */
29
+ permissions: Record<string, boolean>;
30
+ selectedRole?: string;
31
+ /** Form POST action URL */
32
+ actionUrl?: string;
33
+ disabled?: boolean;
34
+ }
35
+
36
+ let {
37
+ roles,
38
+ resources,
39
+ actions,
40
+ permissions,
41
+ selectedRole = roles[0]?.code || '',
42
+ actionUrl = '?/updatePermissions',
43
+ disabled = false,
44
+ }: Props = $props();
45
+
46
+ function isGranted(resource: string, action: string): boolean {
47
+ return permissions[`${resource}:${action}`] === true;
48
+ }
49
+ </script>
50
+
51
+ <div class="lite-permission-matrix">
52
+ <!-- Role Tabs -->
53
+ <div class="lite-role-tabs">
54
+ {#each roles as role}
55
+ <a
56
+ href="?role={role.code}"
57
+ class="lite-role-tab {selectedRole === role.code ? 'active' : ''}"
58
+ >
59
+ {role.name}
60
+ </a>
61
+ {/each}
62
+ </div>
63
+
64
+ <!-- Matrix Table -->
65
+ <form method="POST" action={actionUrl}>
66
+ <input type="hidden" name="role" value={selectedRole} />
67
+
68
+ <table class="lite-table" style="margin-top:0;">
69
+ <thead>
70
+ <tr>
71
+ <th style="width:200px;">Resource</th>
72
+ {#each actions as action}
73
+ <th style="text-align:center;">{action.name}</th>
74
+ {/each}
75
+ </tr>
76
+ </thead>
77
+ <tbody>
78
+ {#each resources as resource, i}
79
+ {#if resource.section && (i === 0 || resource.section !== resources[i-1].section)}
80
+ <tr>
81
+ <td colspan={actions.length + 1} style="background:#f1f5f9;font-weight:700;font-size:12px;text-transform:uppercase;letter-spacing:0.05em;color:#475569;">
82
+ {resource.section}
83
+ </td>
84
+ </tr>
85
+ {/if}
86
+ <tr>
87
+ <td><strong>{resource.name}</strong><br/><small style="color:#94a3b8;">{resource.code}</small></td>
88
+ {#each actions as action}
89
+ <td style="text-align:center;">
90
+ <input
91
+ type="checkbox"
92
+ name="perm_{resource.code}_{action.code}"
93
+ checked={isGranted(resource.code, action.code)}
94
+ {disabled}
95
+ style="width:18px;height:18px;cursor:{disabled ? 'not-allowed' : 'pointer'};"
96
+ />
97
+ </td>
98
+ {/each}
99
+ </tr>
100
+ {/each}
101
+ {#if resources.length === 0}
102
+ <tr>
103
+ <td colspan={actions.length + 1} style="text-align:center;padding:24px;color:#94a3b8;">
104
+ No resources configured.
105
+ </td>
106
+ </tr>
107
+ {/if}
108
+ </tbody>
109
+ </table>
110
+
111
+ {#if !disabled}
112
+ <div style="margin-top:12px;text-align:right;">
113
+ <button type="submit" class="lite-btn lite-btn-primary">Save Permissions</button>
114
+ </div>
115
+ {/if}
116
+ </form>
117
+ </div>
118
+
119
+ <style>
120
+ .lite-role-tabs {
121
+ display: flex;
122
+ flex-wrap: wrap;
123
+ margin-bottom: 16px;
124
+ border-bottom: 2px solid #e2e8f0;
125
+ padding-bottom: 0;
126
+ }
127
+ .lite-role-tab {
128
+ padding: 8px 16px;
129
+ margin-right: 4px;
130
+ text-decoration: none;
131
+ color: #64748b;
132
+ font-size: 14px;
133
+ font-weight: 500;
134
+ border-bottom: 2px solid transparent;
135
+ margin-bottom: -2px;
136
+ transition: color 0.15s ease, border-color 0.15s ease;
137
+ }
138
+ .lite-role-tab:hover {
139
+ color: #0f172a;
140
+ text-decoration: none;
141
+ }
142
+ .lite-role-tab.active {
143
+ color: #4f46e5;
144
+ border-bottom-color: #4f46e5;
145
+ font-weight: 600;
146
+ }
147
+ </style>
@@ -3,8 +3,9 @@
3
3
  * LiteShow — Detail/view page for a single record.
4
4
  * Renders field labels and values in a key-value layout.
5
5
  */
6
- import type { ResourceDefinition, FieldDefinition } from '@svadmin/core';
6
+ import type { ResourceDefinition } from '@svadmin/core';
7
7
  import { t } from '@svadmin/core/i18n';
8
+ import LiteShowField from './LiteShowField.svelte';
8
9
 
9
10
  interface Props {
10
11
  record: Record<string, unknown>;
@@ -25,25 +26,6 @@
25
26
  const showFields = $derived(
26
27
  resource.fields.filter(f => f.showInShow !== false && f.showInList !== false)
27
28
  );
28
-
29
- function formatValue(value: unknown, field: FieldDefinition): string {
30
- if (value == null) return '—';
31
- if (field.type === 'boolean') return value ? '✓' : '✗';
32
- if (field.type === 'date') {
33
- try { return new Date(value as string).toLocaleString(); } catch { return String(value); }
34
- }
35
- if (field.type === 'select' && field.options) {
36
- const opt = field.options.find(o => o.value === value);
37
- return opt?.label ?? String(value);
38
- }
39
- if (field.type === 'url') return String(value);
40
- if (field.type === 'email') return String(value);
41
- if (Array.isArray(value)) return value.join(', ');
42
- if (typeof value === 'object') {
43
- try { return JSON.stringify(value, null, 2); } catch { return String(value); }
44
- }
45
- return String(value);
46
- }
47
29
  </script>
48
30
 
49
31
  <div class="lite-card">
@@ -70,24 +52,7 @@
70
52
  {field.label}
71
53
  </td>
72
54
  <td>
73
- {#if field.type === 'boolean'}
74
- <span class="lite-bool {value ? 'lite-bool-true' : ''}"></span>
75
- {value ? '✓ Yes' : '✗ No'}
76
- {:else if field.type === 'url' && value}
77
- <a href={String(value)} target="_blank" rel="noopener">{value}</a>
78
- {:else if field.type === 'email' && value}
79
- <a href="mailto:{value}">{value}</a>
80
- {:else if field.type === 'image' && value}
81
- <img src={String(value)} alt={field.label} style="max-width:300px;max-height:200px;border-radius:4px;" />
82
- {:else if field.type === 'tags' && Array.isArray(value)}
83
- {#each value as tag}
84
- <span class="lite-badge">{tag}</span>
85
- {/each}
86
- {:else if field.type === 'json' && value}
87
- <pre style="margin:0;font-size:12px;background:#f8fafc;padding:8px;border-radius:4px;overflow-x:auto;">{typeof value === 'string' ? value : JSON.stringify(value, null, 2)}</pre>
88
- {:else}
89
- {formatValue(value, field)}
90
- {/if}
55
+ <LiteShowField {field} {value} />
91
56
  </td>
92
57
  </tr>
93
58
  {/each}
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ /**
3
+ * LiteShowField — SSR-compatible field renderer for detail views.
4
+ * Renders a single field value based on its type definition.
5
+ */
6
+ import type { FieldDefinition } from '@svadmin/core';
7
+
8
+ interface Props {
9
+ field: FieldDefinition;
10
+ value: unknown;
11
+ }
12
+
13
+ let { field, value }: Props = $props();
14
+
15
+ function formatValue(v: unknown, f: FieldDefinition): string {
16
+ if (v == null) return '—';
17
+ if (f.type === 'date') {
18
+ try { return new Date(v as string).toLocaleString(); } catch { return String(v); }
19
+ }
20
+ if (f.type === 'select' && f.options) {
21
+ const opt = f.options.find(o => o.value === v);
22
+ return opt?.label ?? String(v);
23
+ }
24
+ if (f.type === 'url') return String(v);
25
+ if (f.type === 'email') return String(v);
26
+ if (Array.isArray(v)) return v.join(', ');
27
+ if (typeof v === 'object') {
28
+ try { return JSON.stringify(v, null, 2); } catch { return String(v); }
29
+ }
30
+ return String(v);
31
+ }
32
+ </script>
33
+
34
+ {#if field.type === 'boolean'}
35
+ <span class="lite-bool {value ? 'lite-bool-true' : ''}"></span>
36
+ {value ? '✓ Yes' : '✗ No'}
37
+ {:else if field.type === 'url' && value}
38
+ <a href={String(value)} target="_blank" rel="noopener">{value}</a>
39
+ {:else if field.type === 'email' && value}
40
+ <a href="mailto:{value}">{value}</a>
41
+ {:else if field.type === 'image' && value}
42
+ <img src={String(value)} alt={field.label} style="max-width:300px;max-height:200px;border-radius:6px;border:1px solid #e2e8f0;" />
43
+ {:else if field.type === 'tags' && Array.isArray(value)}
44
+ {#each value as tag}
45
+ <span class="lite-badge">{tag}</span>
46
+ {/each}
47
+ {:else if field.type === 'json' && value}
48
+ <pre style="margin:0;font-size:12px;background:#f8fafc;padding:12px;border-radius:6px;border:1px solid #e2e8f0;overflow-x:auto;">{typeof value === 'string' ? value : JSON.stringify(value, null, 2)}</pre>
49
+ {:else}
50
+ {formatValue(value, field)}
51
+ {/if}
@@ -0,0 +1,70 @@
1
+ <script lang="ts">
2
+ /**
3
+ * LiteStatsCard — SSR-compatible KPI statistics card.
4
+ * Pure HTML card with number, label, and optional trend indicator.
5
+ * No client-side JS required.
6
+ */
7
+
8
+ interface Props {
9
+ /** The main number/value to display */
10
+ value: string | number;
11
+ /** Label/title for the stat */
12
+ label: string;
13
+ /** Optional description text below the label */
14
+ description?: string;
15
+ /** Trend direction: 'up' shows green arrow, 'down' shows red arrow */
16
+ trend?: 'up' | 'down' | 'neutral';
17
+ /** Trend value text (e.g. "+12%", "-3%") */
18
+ trendValue?: string;
19
+ /** Optional icon character or HTML entity to display */
20
+ icon?: string;
21
+ /** Link to detail page */
22
+ href?: string;
23
+ }
24
+
25
+ let {
26
+ value,
27
+ label,
28
+ description,
29
+ trend,
30
+ trendValue,
31
+ icon,
32
+ href,
33
+ }: Props = $props();
34
+ </script>
35
+
36
+ {#if href}
37
+ <a href={href} class="lite-stats-card lite-card" style="text-decoration:none;display:block;">
38
+ {#if icon}
39
+ <div class="lite-stats-icon">{icon}</div>
40
+ {/if}
41
+ <div class="lite-stats-value">{value}</div>
42
+ <div class="lite-stats-label">{label}</div>
43
+ {#if trendValue}
44
+ <div class="lite-stats-trend lite-stats-trend-{trend || 'neutral'}">
45
+ {#if trend === 'up'}&#9650;{:else if trend === 'down'}&#9660;{:else}&#9644;{/if}
46
+ {trendValue}
47
+ </div>
48
+ {/if}
49
+ {#if description}
50
+ <div class="lite-stats-desc">{description}</div>
51
+ {/if}
52
+ </a>
53
+ {:else}
54
+ <div class="lite-stats-card lite-card">
55
+ {#if icon}
56
+ <div class="lite-stats-icon">{icon}</div>
57
+ {/if}
58
+ <div class="lite-stats-value">{value}</div>
59
+ <div class="lite-stats-label">{label}</div>
60
+ {#if trendValue}
61
+ <div class="lite-stats-trend lite-stats-trend-{trend || 'neutral'}">
62
+ {#if trend === 'up'}&#9650;{:else if trend === 'down'}&#9660;{:else}&#9644;{/if}
63
+ {trendValue}
64
+ </div>
65
+ {/if}
66
+ {#if description}
67
+ <div class="lite-stats-desc">{description}</div>
68
+ {/if}
69
+ </div>
70
+ {/if}
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ /**
3
+ * LiteTabs — SSR-compatible tab navigation.
4
+ * Uses simple <a> tags and flexbox. No JS required.
5
+ */
6
+
7
+ interface TabItem {
8
+ label: string;
9
+ value: string;
10
+ href: string;
11
+ }
12
+
13
+ interface Props {
14
+ items: TabItem[];
15
+ activeValue: string;
16
+ }
17
+
18
+ let { items, activeValue }: Props = $props();
19
+ </script>
20
+
21
+ <div class="lite-tabs">
22
+ {#each items as item}
23
+ <a href={item.href} class="lite-tab {activeValue === item.value ? 'active' : ''}">
24
+ {item.label}
25
+ </a>
26
+ {/each}
27
+ </div>
28
+
29
+ <style>
30
+ .lite-tabs {
31
+ display: flex;
32
+ flex-wrap: wrap;
33
+ border-bottom: 2px solid #e2e8f0;
34
+ margin-bottom: 24px;
35
+ padding-bottom: 0;
36
+ }
37
+ .lite-tab {
38
+ padding: 8px 16px;
39
+ margin-right: 4px;
40
+ font-size: 14px;
41
+ font-weight: 500;
42
+ color: #64748b;
43
+ border-bottom: 2px solid transparent;
44
+ margin-bottom: -2px;
45
+ text-decoration: none;
46
+ transition: color 0.15s ease, border-color 0.15s ease;
47
+ }
48
+ .lite-tab:hover {
49
+ color: #0f172a;
50
+ text-decoration: none;
51
+ }
52
+ .lite-tab.active {
53
+ color: #4f46e5;
54
+ border-bottom-color: #4f46e5;
55
+ font-weight: 600;
56
+ }
57
+ </style>
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SSR AutoSaveIndicator — Degraded to a static status label.
4
+ * Without JS, there is no auto-save. This component simply renders
5
+ * the last known save status passed from the server.
6
+ */
7
+ interface Props {
8
+ status?: 'idle' | 'saved' | 'error';
9
+ message?: string;
10
+ }
11
+
12
+ let { status = 'idle', message }: Props = $props();
13
+
14
+ const statusConfig: Record<string, { icon: string; color: string; text: string }> = {
15
+ idle: { icon: '☁', color: '#64748b', text: 'All changes saved' },
16
+ saved: { icon: '✓', color: '#10b981', text: 'Saved' },
17
+ error: { icon: '✕', color: '#ef4444', text: 'Save failed' },
18
+ };
19
+
20
+ const cfg = $derived(statusConfig[status] ?? statusConfig.idle);
21
+ </script>
22
+
23
+ <span style="display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: {cfg.color};">
24
+ <span>{cfg.icon}</span>
25
+ <span>{message ?? cfg.text}</span>
26
+ </span>
@@ -0,0 +1,33 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SSR DraggableHeader — Degraded to a static table header.
4
+ * Column dragging/resizing requires JS mouse/touch events.
5
+ * In lite mode, columns are rendered at natural widths.
6
+ * Sort is achieved via URL query parameters as in LiteTable.
7
+ */
8
+ import type { FieldDefinition } from '@svadmin/core';
9
+
10
+ interface Props {
11
+ fields: FieldDefinition[];
12
+ currentSort?: string;
13
+ currentOrder?: 'asc' | 'desc';
14
+ }
15
+
16
+ let { fields, currentSort, currentOrder = 'asc' }: Props = $props();
17
+ </script>
18
+
19
+ <tr>
20
+ {#each fields as field}
21
+ <th style="padding: 10px 12px; text-align: left; font-weight: 600; font-size: 13px; color: #475569; white-space: nowrap;">
22
+ {#if currentSort === field.key}
23
+ <a href="?sort={field.key}&order={currentOrder === 'asc' ? 'desc' : 'asc'}" style="color: #0f172a; text-decoration: none;">
24
+ {field.label} {currentOrder === 'asc' ? '↑' : '↓'}
25
+ </a>
26
+ {:else}
27
+ <a href="?sort={field.key}&order=asc" style="color: inherit; text-decoration: none;">
28
+ {field.label}
29
+ </a>
30
+ {/if}
31
+ </th>
32
+ {/each}
33
+ </tr>
@@ -0,0 +1,42 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SSR DrawerForm — Degraded to a dedicated page redirect.
4
+ * Side drawers require JS for sliding panel rendering. In lite mode,
5
+ * this component works identically to LiteModalForm: a plain link.
6
+ */
7
+ import type { ResourceDefinition } from '@svadmin/core';
8
+ import { t } from '@svadmin/core/i18n';
9
+
10
+ interface Props {
11
+ resource: ResourceDefinition;
12
+ mode?: 'create' | 'edit';
13
+ recordId?: string | number;
14
+ basePath?: string;
15
+ label?: string;
16
+ }
17
+
18
+ let {
19
+ resource,
20
+ mode = 'create',
21
+ recordId,
22
+ basePath = '/lite',
23
+ label,
24
+ }: Props = $props();
25
+
26
+ const href = $derived(
27
+ mode === 'create'
28
+ ? `${basePath}/${resource.name}/create`
29
+ : `${basePath}/${resource.name}/${recordId}/edit`
30
+ );
31
+
32
+ const buttonLabel = $derived(
33
+ label ??
34
+ (mode === 'create'
35
+ ? `${t('common.create') || 'Create'} ${resource.label || resource.name}`
36
+ : `${t('common.edit') || 'Edit'} ${resource.label || resource.name}`)
37
+ );
38
+ </script>
39
+
40
+ <a href={href} class="lite-btn lite-btn-outline">
41
+ {buttonLabel}
42
+ </a>
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SSR InlineEdit — Degraded to a show-value + edit-link pattern.
4
+ * True inline editing requires JS for focus management, key events,
5
+ * and async save. In lite mode, we display the value as static text
6
+ * and provide a small "edit" link that navigates to the edit page.
7
+ */
8
+ import type { FieldDefinition } from '@svadmin/core';
9
+
10
+ interface Props {
11
+ field: FieldDefinition;
12
+ value: unknown;
13
+ editUrl?: string;
14
+ }
15
+
16
+ let { field, value, editUrl }: Props = $props();
17
+
18
+ const displayValue = $derived(
19
+ value == null ? '—' : String(value)
20
+ );
21
+ </script>
22
+
23
+ <span style="display: inline-flex; align-items: center; gap: 6px;">
24
+ <span>{displayValue}</span>
25
+ {#if editUrl}
26
+ <a
27
+ href={editUrl}
28
+ style="font-size: 11px; color: #6366f1; text-decoration: none;"
29
+ title="Edit {field.label}"
30
+ >✎</a>
31
+ {/if}
32
+ </span>
@@ -0,0 +1,44 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SSR ModalForm — Degraded to a dedicated page redirect.
4
+ * True modals require JS for overlay rendering. In lite mode,
5
+ * this component simply renders a link that navigates to the
6
+ * create/edit page for the resource (a full page load).
7
+ */
8
+ import type { ResourceDefinition } from '@svadmin/core';
9
+ import { t } from '@svadmin/core/i18n';
10
+
11
+ interface Props {
12
+ resource: ResourceDefinition;
13
+ mode?: 'create' | 'edit';
14
+ recordId?: string | number;
15
+ basePath?: string;
16
+ label?: string;
17
+ }
18
+
19
+ let {
20
+ resource,
21
+ mode = 'create',
22
+ recordId,
23
+ basePath = '/lite',
24
+ label,
25
+ }: Props = $props();
26
+
27
+ const href = $derived(
28
+ mode === 'create'
29
+ ? `${basePath}/${resource.name}/create`
30
+ : `${basePath}/${resource.name}/${recordId}/edit`
31
+ );
32
+
33
+ const buttonLabel = $derived(
34
+ label ??
35
+ (mode === 'create'
36
+ ? `${t('common.create') || 'Create'} ${resource.label || resource.name}`
37
+ : `${t('common.edit') || 'Edit'} ${resource.label || resource.name}`)
38
+ );
39
+ </script>
40
+
41
+ <!-- In lite mode, modals degrade to a full-page navigation -->
42
+ <a href={href} class="lite-btn lite-btn-primary">
43
+ {buttonLabel}
44
+ </a>
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SSR Toast — Degraded to a static alert banner.
4
+ * In SSR-only mode, toast notifications are rendered as inline banners
5
+ * by the server and included in the page HTML. They persist until the
6
+ * next full-page navigation (no auto-dismiss without JS).
7
+ */
8
+ interface Props {
9
+ message: string;
10
+ type?: 'success' | 'error' | 'warning' | 'info';
11
+ dismissUrl?: string;
12
+ }
13
+
14
+ let { message, type = 'info', dismissUrl }: Props = $props();
15
+
16
+ const colors: Record<string, { bg: string; border: string; text: string }> = {
17
+ success: { bg: '#ecfdf5', border: '#a7f3d0', text: '#065f46' },
18
+ error: { bg: '#fef2f2', border: '#fecaca', text: '#991b1b' },
19
+ warning: { bg: '#fffbeb', border: '#fed7aa', text: '#92400e' },
20
+ info: { bg: '#eff6ff', border: '#bfdbfe', text: '#1e40af' },
21
+ };
22
+
23
+ const style = $derived(colors[type] ?? colors.info);
24
+ </script>
25
+
26
+ <div
27
+ role="alert"
28
+ style="padding: 12px 16px; background: {style.bg}; border: 1px solid {style.border}; color: {style.text}; border-radius: 6px; margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center; font-size: 14px;"
29
+ >
30
+ <span>{message}</span>
31
+ {#if dismissUrl}
32
+ <a href={dismissUrl} style="color: {style.text}; text-decoration: none; font-weight: 600; margin-left: 12px;">✕</a>
33
+ {/if}
34
+ </div>
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SSR UndoableNotification — Degraded to a static banner with an undo link.
4
+ * Without JS there's no countdown timer. Instead, the server provides an
5
+ * undo URL that performs the reversal action. If the user navigates away,
6
+ * the action is considered confirmed.
7
+ */
8
+ interface Props {
9
+ message: string;
10
+ undoUrl: string;
11
+ }
12
+
13
+ let { message, undoUrl }: Props = $props();
14
+ </script>
15
+
16
+ <div
17
+ role="alert"
18
+ style="padding: 12px 16px; background: #fefce8; border: 1px solid #fde68a; color: #713f12; border-radius: 6px; margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center; font-size: 14px;"
19
+ >
20
+ <span>{message}</span>
21
+ <a
22
+ href={undoUrl}
23
+ style="color: #b45309; font-weight: 600; text-decoration: underline; margin-left: 16px; white-space: nowrap;"
24
+ >Undo</a>
25
+ </div>
@@ -0,0 +1,44 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SSR VirtualTable — Identical to LiteTable.
4
+ * Virtual scrolling (rendering only visible rows) requires JS.
5
+ * In lite mode, all rows are rendered server-side. For large datasets,
6
+ * rely on server-side pagination instead.
7
+ */
8
+ import type { ResourceDefinition } from '@svadmin/core';
9
+ import LiteTable from '../LiteTable.svelte';
10
+
11
+ interface Props {
12
+ records: Record<string, unknown>[];
13
+ resource: ResourceDefinition;
14
+ currentSort?: string;
15
+ currentOrder?: 'asc' | 'desc';
16
+ currentSearch?: string;
17
+ basePath?: string;
18
+ canEdit?: boolean;
19
+ canDelete?: boolean;
20
+ }
21
+
22
+ let {
23
+ records,
24
+ resource,
25
+ currentSort,
26
+ currentOrder = 'asc',
27
+ currentSearch,
28
+ basePath = '/lite',
29
+ canEdit = true,
30
+ canDelete = true,
31
+ }: Props = $props();
32
+ </script>
33
+
34
+ <!-- Virtual scrolling is not available without JS. All rows are rendered. -->
35
+ <LiteTable
36
+ {records}
37
+ {resource}
38
+ {currentSort}
39
+ {currentOrder}
40
+ {currentSearch}
41
+ {basePath}
42
+ {canEdit}
43
+ {canDelete}
44
+ />
@@ -0,0 +1,8 @@
1
+ export { default as LiteToast } from './LiteToast.svelte';
2
+ export { default as LiteUndoableNotification } from './LiteUndoableNotification.svelte';
3
+ export { default as LiteModalForm } from './LiteModalForm.svelte';
4
+ export { default as LiteDrawerForm } from './LiteDrawerForm.svelte';
5
+ export { default as LiteInlineEdit } from './LiteInlineEdit.svelte';
6
+ export { default as LiteAutoSaveIndicator } from './LiteAutoSaveIndicator.svelte';
7
+ export { default as LiteVirtualTable } from './LiteVirtualTable.svelte';
8
+ export { default as LiteDraggableHeader } from './LiteDraggableHeader.svelte';