@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,93 @@
1
+ <script lang="ts">
2
+ import type { ResourceDefinition, MenuItem } from '@svadmin/core';
3
+
4
+ interface Props {
5
+ resources: ResourceDefinition[];
6
+ currentResource?: string;
7
+ brandName?: string;
8
+ userName?: string;
9
+ basePath?: string;
10
+ menu?: MenuItem[];
11
+ }
12
+
13
+ let {
14
+ resources,
15
+ currentResource = '',
16
+ brandName = 'Admin',
17
+ userName = '',
18
+ basePath = '/lite',
19
+ menu,
20
+ }: Props = $props();
21
+
22
+ const menuResources = $derived(
23
+ resources.filter((r: ResourceDefinition) => r.showInMenu !== false)
24
+ );
25
+
26
+ function isActive(href: string | undefined): boolean {
27
+ if (!href) return false;
28
+ return currentResource === href.replace(basePath + '/', '');
29
+ }
30
+
31
+ function hasActiveChild(children?: MenuItem[]): boolean {
32
+ if (!children) return false;
33
+ return children.some(c => isActive(c.href ?? `${basePath}/${c.name}`));
34
+ }
35
+ </script>
36
+
37
+ <nav class="lite-sidebar">
38
+ <div class="lite-sidebar-brand">{brandName}</div>
39
+ {#if menu && menu.length > 0}
40
+ {#each menu as item}
41
+ {#if item.children && item.children.length > 0}
42
+ <details class="lite-menu-group" open={hasActiveChild(item.children)}>
43
+ <summary class="lite-menu-parent">{item.label ?? item.name}</summary>
44
+ {#each item.children as child}
45
+ {#if child.children && child.children.length > 0}
46
+ <details class="lite-menu-group" style="margin-left:12px" open={hasActiveChild(child.children)}>
47
+ <summary class="lite-menu-parent">{child.label ?? child.name}</summary>
48
+ {#each child.children as grandchild}
49
+ <a
50
+ href={grandchild.href ?? `${basePath}/${grandchild.name}`}
51
+ class={isActive(grandchild.href ?? `${basePath}/${grandchild.name}`) ? 'active' : ''}
52
+ target={grandchild.target === '_blank' ? '_blank' : undefined}
53
+ style="padding-left:40px"
54
+ >{grandchild.label ?? grandchild.name}</a>
55
+ {/each}
56
+ </details>
57
+ {:else}
58
+ <a
59
+ href={child.href ?? `${basePath}/${child.name}`}
60
+ class={isActive(child.href ?? `${basePath}/${child.name}`) ? 'active' : ''}
61
+ target={child.target === '_blank' ? '_blank' : undefined}
62
+ style="padding-left:28px"
63
+ >{child.label ?? child.name}</a>
64
+ {/if}
65
+ {/each}
66
+ </details>
67
+ {:else}
68
+ <a
69
+ href={item.href ?? `${basePath}/${item.name}`}
70
+ class={isActive(item.href ?? `${basePath}/${item.name}`) ? 'active' : ''}
71
+ target={item.target === '_blank' ? '_blank' : undefined}
72
+ >{item.label ?? item.name}</a>
73
+ {/if}
74
+ {/each}
75
+ {:else}
76
+ {#each menuResources as res}
77
+ <a
78
+ href="{basePath}/{res.name}"
79
+ class={res.name === currentResource ? 'active' : ''}
80
+ >
81
+ {res.label ?? res.name}
82
+ </a>
83
+ {/each}
84
+ {/if}
85
+ {#if userName}
86
+ <div style="position:absolute;bottom:0;left:0;right:0;padding:12px 16px;border-top:1px solid #334155;font-size:12px;color:#94a3b8;">
87
+ {userName}
88
+ <form method="POST" action="{basePath}/login?/logout" style="display:inline;margin-left:8px;">
89
+ <button type="submit" class="lite-btn lite-btn-sm" style="color:#94a3b8;border-color:#475569;background:transparent;padding:2px 8px;">Logout</button>
90
+ </form>
91
+ </div>
92
+ {/if}
93
+ </nav>
@@ -0,0 +1,8 @@
1
+ export { default as LiteSidebar } from './LiteSidebar.svelte';
2
+ export { default as LiteHeader } from './LiteHeader.svelte';
3
+ export { default as LiteNavigateToResource } from './LiteNavigateToResource.svelte';
4
+ export { default as LiteCatchAllNavigate } from './LiteCatchAllNavigate.svelte';
5
+ export { default as LiteCanAccess } from './LiteCanAccess.svelte';
6
+ export { default as LiteAuthenticated } from './LiteAuthenticated.svelte';
7
+ export { default as LiteErrorComponent } from './LiteErrorComponent.svelte';
8
+ export { default as LiteErrorBoundary } from './LiteErrorBoundary.svelte';
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ import type { ResourceDefinition } from '@svadmin/core';
3
+ import { t } from '@svadmin/core/i18n';
4
+ import LiteForm from '../LiteForm.svelte';
5
+ import LiteListButton from '../buttons/LiteListButton.svelte';
6
+
7
+ interface Props {
8
+ resource: ResourceDefinition;
9
+ errors?: Record<string, string[]>;
10
+ values?: Record<string, unknown>;
11
+ basePath?: string;
12
+ }
13
+
14
+ let {
15
+ resource,
16
+ errors = {},
17
+ values = {},
18
+ basePath = '/lite',
19
+ }: Props = $props();
20
+ </script>
21
+
22
+ <div class="lite-page">
23
+ <div class="lite-page-header">
24
+ <h1 class="lite-page-title">{t('common.create') || 'Create'} {resource.label || resource.name}</h1>
25
+ <div class="lite-page-actions">
26
+ <LiteListButton resource={resource.name} {basePath} />
27
+ </div>
28
+ </div>
29
+
30
+ <LiteForm
31
+ fields={resource.fields}
32
+ mode="create"
33
+ {resource}
34
+ {errors}
35
+ {values}
36
+ action="?/{resource.name}_create"
37
+ cancelUrl="{basePath}/{resource.name}"
38
+ />
39
+ </div>
@@ -0,0 +1,54 @@
1
+ <script lang="ts">
2
+ import type { ResourceDefinition } from '@svadmin/core';
3
+ import { t } from '@svadmin/core/i18n';
4
+ import LiteForm from '../LiteForm.svelte';
5
+ import LiteListButton from '../buttons/LiteListButton.svelte';
6
+ import LiteShowButton from '../buttons/LiteShowButton.svelte';
7
+ import LiteDeleteButton from '../buttons/LiteDeleteButton.svelte';
8
+
9
+ interface Props {
10
+ resource: ResourceDefinition;
11
+ record: Record<string, unknown>;
12
+ errors?: Record<string, string[]>;
13
+ basePath?: string;
14
+ canDelete?: boolean;
15
+ canShow?: boolean;
16
+ }
17
+
18
+ let {
19
+ resource,
20
+ record,
21
+ errors = {},
22
+ basePath = '/lite',
23
+ canDelete = true,
24
+ canShow = true,
25
+ }: Props = $props();
26
+
27
+ const pk = resource.primaryKey ?? 'id';
28
+ const idStr = String(record[pk]);
29
+ </script>
30
+
31
+ <div class="lite-page">
32
+ <div class="lite-page-header">
33
+ <h1 class="lite-page-title">{t('common.edit') || 'Edit'} {resource.label || resource.name} #{idStr}</h1>
34
+ <div class="lite-page-actions">
35
+ {#if canShow}
36
+ <LiteShowButton resource={resource.name} recordItemId={idStr} {basePath} />
37
+ {/if}
38
+ <LiteListButton resource={resource.name} {basePath} />
39
+ {#if canDelete}
40
+ <LiteDeleteButton resource={resource.name} recordItemId={idStr} redirectUrl="{basePath}/{resource.name}" {basePath} />
41
+ {/if}
42
+ </div>
43
+ </div>
44
+
45
+ <LiteForm
46
+ fields={resource.fields}
47
+ mode="edit"
48
+ {resource}
49
+ {errors}
50
+ values={record}
51
+ action="?/{resource.name}_update"
52
+ cancelUrl="{basePath}/{resource.name}"
53
+ />
54
+ </div>
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ title?: string;
4
+ description?: string;
5
+ action?: string;
6
+ loginUrl?: string;
7
+ errors?: string[];
8
+ successMessage?: string;
9
+ }
10
+
11
+ let {
12
+ title = 'Forgot Password',
13
+ description = 'Enter your email to receive a password reset link',
14
+ action = '?/forgot_password',
15
+ loginUrl = '/lite/login',
16
+ errors = [],
17
+ successMessage = '',
18
+ }: Props = $props();
19
+ </script>
20
+
21
+ <div style="min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #f8fafc; padding: 20px;">
22
+ <div class="lite-card" style="width: 100%; max-width: 400px; padding: 32px;">
23
+ <div style="text-align: center; margin-bottom: 24px;">
24
+ <h1 style="font-size: 24px; font-weight: 600; color: #0f172a; margin: 0 0 8px;">{title}</h1>
25
+ <p style="font-size: 14px; color: #64748b; margin: 0;">{description}</p>
26
+ </div>
27
+
28
+ <!-- Native form action -->
29
+ <form method="POST" {action} style="display: flex; flex-direction: column; gap: 16px;">
30
+ <div class="lite-form-group">
31
+ <label for="email">Email</label>
32
+ <input type="email" id="email" name="email" class="lite-input" required />
33
+ </div>
34
+
35
+ {#if errors.length > 0}
36
+ <div style="padding: 12px; background: #fef2f2; border-radius: 6px; color: #ef4444; font-size: 13px;">
37
+ <ul style="margin: 0; padding-left: 20px;">
38
+ {#each errors as err}
39
+ <li>{err}</li>
40
+ {/each}
41
+ </ul>
42
+ </div>
43
+ {/if}
44
+
45
+ {#if successMessage}
46
+ <div style="padding: 12px; background: #ecfdf5; border-radius: 6px; color: #10b981; font-size: 13px;">
47
+ {successMessage}
48
+ </div>
49
+ {/if}
50
+
51
+ <button type="submit" class="lite-btn lite-btn-primary" style="width: 100%; justify-content: center; padding: 10px;">
52
+ Send Reset Link
53
+ </button>
54
+
55
+ <div style="text-align: center; font-size: 13px; color: #64748b; margin-top: 8px;">
56
+ Remember your password? <a href={loginUrl} style="color: #2563eb; text-decoration: none;">Back to Login</a>
57
+ </div>
58
+ </form>
59
+ </div>
60
+ </div>
@@ -0,0 +1,77 @@
1
+ <script lang="ts">
2
+ import type { ResourceDefinition } from '@svadmin/core';
3
+ import { t } from '@svadmin/core/i18n';
4
+ import LiteTable from '../LiteTable.svelte';
5
+ import LitePagination from '../LitePagination.svelte';
6
+ import LiteSearch from '../LiteSearch.svelte';
7
+ import LiteCreateButton from '../buttons/LiteCreateButton.svelte';
8
+ import LiteRefreshButton from '../buttons/LiteRefreshButton.svelte';
9
+
10
+ interface Props {
11
+ resource: ResourceDefinition;
12
+ records: Record<string, unknown>[];
13
+ total: number;
14
+ pagination: { page: number; perPage: number };
15
+ currentSort?: string;
16
+ currentOrder?: 'asc' | 'desc';
17
+ currentSearch?: string;
18
+ basePath?: string;
19
+ canCreate?: boolean;
20
+ canEdit?: boolean;
21
+ canDelete?: boolean;
22
+ }
23
+
24
+ let {
25
+ resource,
26
+ records,
27
+ total,
28
+ pagination,
29
+ currentSort,
30
+ currentOrder = 'asc',
31
+ currentSearch,
32
+ basePath = '/lite',
33
+ canCreate = true,
34
+ canEdit = true,
35
+ canDelete = true,
36
+ }: Props = $props();
37
+ </script>
38
+
39
+ <div class="lite-page">
40
+ <div class="lite-page-header">
41
+ <h1 class="lite-page-title">{resource.label || resource.name} {t('common.list') || 'List'}</h1>
42
+ <div class="lite-page-actions">
43
+ {#if canCreate}
44
+ <LiteCreateButton resource={resource.name} {basePath} />
45
+ {/if}
46
+ <LiteRefreshButton hideText />
47
+ </div>
48
+ </div>
49
+
50
+ <div class="lite-card" style="margin-bottom: 20px;">
51
+ <div style="padding: 16px; border-bottom: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center;">
52
+ <LiteSearch defaultValue={currentSearch} placeholder={t('common.search') || 'Search...'} />
53
+ <span style="font-size: 13px; color: #64748b;">
54
+ {t('common.total') || 'Total'}: {total}
55
+ </span>
56
+ </div>
57
+
58
+ <LiteTable
59
+ {records}
60
+ {resource}
61
+ {currentSort}
62
+ {currentOrder}
63
+ {currentSearch}
64
+ {basePath}
65
+ {canEdit}
66
+ {canDelete}
67
+ />
68
+
69
+ {#if total > pagination.perPage}
70
+ <LitePagination
71
+ page={pagination.page}
72
+ perPage={pagination.perPage}
73
+ {total}
74
+ />
75
+ {/if}
76
+ </div>
77
+ </div>
@@ -0,0 +1,61 @@
1
+ <script lang="ts">
2
+ import { t } from '@svadmin/core/i18n';
3
+
4
+ interface Props {
5
+ user: { id?: string | number; name?: string; email?: string; [key: string]: unknown };
6
+ action?: string;
7
+ errors?: string[];
8
+ successMessage?: string;
9
+ }
10
+
11
+ let {
12
+ user,
13
+ action = '?/update_profile',
14
+ errors = [],
15
+ successMessage = '',
16
+ }: Props = $props();
17
+ </script>
18
+
19
+ <div class="lite-page">
20
+ <div class="lite-page-header">
21
+ <h1 class="lite-page-title">{t('common.profile') || 'User Profile'}</h1>
22
+ </div>
23
+
24
+ <div class="lite-card" style="max-width: 600px; margin: 0 auto; padding: 24px;">
25
+ <form method="POST" {action} style="display: flex; flex-direction: column; gap: 16px;">
26
+ <div class="lite-form-group">
27
+ <label for="name">Name</label>
28
+ <input type="text" id="name" name="name" class="lite-input" value={user?.name ?? ''} />
29
+ </div>
30
+
31
+ <div class="lite-form-group">
32
+ <label for="email">Email</label>
33
+ <input type="email" id="email" name="email" class="lite-input" value={user?.email ?? ''} required />
34
+ </div>
35
+
36
+ <!-- Password update fields can be added here or handled in a separate form -->
37
+
38
+ {#if errors.length > 0}
39
+ <div style="padding: 12px; background: #fef2f2; border-radius: 6px; color: #ef4444; font-size: 13px;">
40
+ <ul style="margin: 0; padding-left: 20px;">
41
+ {#each errors as err}
42
+ <li>{err}</li>
43
+ {/each}
44
+ </ul>
45
+ </div>
46
+ {/if}
47
+
48
+ {#if successMessage}
49
+ <div style="padding: 12px; background: #ecfdf5; border-radius: 6px; color: #10b981; font-size: 13px;">
50
+ {successMessage}
51
+ </div>
52
+ {/if}
53
+
54
+ <div style="display: flex; justify-content: flex-end; margin-top: 8px;">
55
+ <button type="submit" class="lite-btn lite-btn-primary">
56
+ {t('common.save') || 'Save Profile'}
57
+ </button>
58
+ </div>
59
+ </form>
60
+ </div>
61
+ </div>
@@ -0,0 +1,64 @@
1
+ <script lang="ts">
2
+ import { t } from '@svadmin/core/i18n';
3
+
4
+ interface Props {
5
+ title?: string;
6
+ description?: string;
7
+ action?: string;
8
+ loginUrl?: string;
9
+ errors?: string[];
10
+ }
11
+
12
+ let {
13
+ title = 'Register',
14
+ description = 'Create a new account',
15
+ action = '?/register',
16
+ loginUrl = '/lite/login',
17
+ errors = [],
18
+ }: Props = $props();
19
+ </script>
20
+
21
+ <div style="min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #f8fafc; padding: 20px;">
22
+ <div class="lite-card" style="width: 100%; max-width: 400px; padding: 32px;">
23
+ <div style="text-align: center; margin-bottom: 24px;">
24
+ <h1 style="font-size: 24px; font-weight: 600; color: #0f172a; margin: 0 0 8px;">{title}</h1>
25
+ <p style="font-size: 14px; color: #64748b; margin: 0;">{description}</p>
26
+ </div>
27
+
28
+ <!-- We use native form action -->
29
+ <form method="POST" {action} style="display: flex; flex-direction: column; gap: 16px;">
30
+ <div class="lite-form-group">
31
+ <label for="email">Email</label>
32
+ <input type="email" id="email" name="email" class="lite-input" required />
33
+ </div>
34
+
35
+ <div class="lite-form-group">
36
+ <label for="password">Password</label>
37
+ <input type="password" id="password" name="password" class="lite-input" required minlength="6" />
38
+ </div>
39
+
40
+ <div class="lite-form-group">
41
+ <label for="confirmPassword">Confirm Password</label>
42
+ <input type="password" id="confirmPassword" name="confirmPassword" class="lite-input" required minlength="6" />
43
+ </div>
44
+
45
+ {#if errors.length > 0}
46
+ <div style="padding: 12px; background: #fef2f2; border-radius: 6px; color: #ef4444; font-size: 13px;">
47
+ <ul style="margin: 0; padding-left: 20px;">
48
+ {#each errors as err}
49
+ <li>{err}</li>
50
+ {/each}
51
+ </ul>
52
+ </div>
53
+ {/if}
54
+
55
+ <button type="submit" class="lite-btn lite-btn-primary" style="width: 100%; justify-content: center; padding: 10px;">
56
+ Register
57
+ </button>
58
+
59
+ <div style="text-align: center; font-size: 13px; color: #64748b; margin-top: 8px;">
60
+ Already have an account? <a href={loginUrl} style="color: #2563eb; text-decoration: none;">Login</a>
61
+ </div>
62
+ </form>
63
+ </div>
64
+ </div>
@@ -0,0 +1,61 @@
1
+ <script lang="ts">
2
+ import type { ResourceDefinition } from '@svadmin/core';
3
+ import { t } from '@svadmin/core/i18n';
4
+ import LiteShowField from '../LiteShowField.svelte';
5
+ import LiteListButton from '../buttons/LiteListButton.svelte';
6
+ import LiteEditButton from '../buttons/LiteEditButton.svelte';
7
+ import LiteDeleteButton from '../buttons/LiteDeleteButton.svelte';
8
+
9
+ interface Props {
10
+ resource: ResourceDefinition;
11
+ record: Record<string, unknown>;
12
+ basePath?: string;
13
+ canEdit?: boolean;
14
+ canDelete?: boolean;
15
+ }
16
+
17
+ let {
18
+ resource,
19
+ record,
20
+ basePath = '/lite',
21
+ canEdit = true,
22
+ canDelete = true,
23
+ }: Props = $props();
24
+
25
+ const pk = resource.primaryKey ?? 'id';
26
+ const idStr = String(record[pk]);
27
+
28
+ const showFields = $derived(
29
+ resource.fields.filter(f => f.showInShow !== false)
30
+ );
31
+ </script>
32
+
33
+ <div class="lite-page">
34
+ <div class="lite-page-header">
35
+ <h1 class="lite-page-title">{t('common.show') || 'Show'} {resource.label || resource.name} #{idStr}</h1>
36
+ <div class="lite-page-actions">
37
+ {#if canEdit}
38
+ <LiteEditButton resource={resource.name} recordItemId={idStr} {basePath} />
39
+ {/if}
40
+ <LiteListButton resource={resource.name} {basePath} />
41
+ {#if canDelete}
42
+ <LiteDeleteButton resource={resource.name} recordItemId={idStr} redirectUrl="{basePath}/{resource.name}" {basePath} />
43
+ {/if}
44
+ </div>
45
+ </div>
46
+
47
+ <div class="lite-card" style="padding: 24px;">
48
+ <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 24px;">
49
+ {#each showFields as field}
50
+ <div>
51
+ <label style="display: block; font-size: 13px; font-weight: 500; color: #64748b; margin-bottom: 8px;">
52
+ {field.label}
53
+ </label>
54
+ <div style="color: #0f172a; font-size: 14px;">
55
+ <LiteShowField {field} value={record[field.key]} />
56
+ </div>
57
+ </div>
58
+ {/each}
59
+ </div>
60
+ </div>
61
+ </div>
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ title?: string;
4
+ description?: string;
5
+ action?: string;
6
+ errors?: string[];
7
+ }
8
+
9
+ let {
10
+ title = 'Update Password',
11
+ description = 'Enter your new password below.',
12
+ action = '?/update_password',
13
+ errors = [],
14
+ }: Props = $props();
15
+ </script>
16
+
17
+ <div style="min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #f8fafc; padding: 20px;">
18
+ <div class="lite-card" style="width: 100%; max-width: 400px; padding: 32px;">
19
+ <div style="text-align: center; margin-bottom: 24px;">
20
+ <h1 style="font-size: 24px; font-weight: 600; color: #0f172a; margin: 0 0 8px;">{title}</h1>
21
+ <p style="font-size: 14px; color: #64748b; margin: 0;">{description}</p>
22
+ </div>
23
+
24
+ <!-- Native form action -->
25
+ <form method="POST" {action} style="display: flex; flex-direction: column; gap: 16px;">
26
+ <div class="lite-form-group">
27
+ <label for="password">New Password</label>
28
+ <input type="password" id="password" name="password" class="lite-input" required minlength="6" />
29
+ </div>
30
+
31
+ <div class="lite-form-group">
32
+ <label for="confirmPassword">Confirm New Password</label>
33
+ <input type="password" id="confirmPassword" name="confirmPassword" class="lite-input" required minlength="6" />
34
+ </div>
35
+
36
+ {#if errors.length > 0}
37
+ <div style="padding: 12px; background: #fef2f2; border-radius: 6px; color: #ef4444; font-size: 13px;">
38
+ <ul style="margin: 0; padding-left: 20px;">
39
+ {#each errors as err}
40
+ <li>{err}</li>
41
+ {/each}
42
+ </ul>
43
+ </div>
44
+ {/if}
45
+
46
+ <button type="submit" class="lite-btn lite-btn-primary" style="width: 100%; justify-content: center; padding: 10px;">
47
+ Update Password
48
+ </button>
49
+ </form>
50
+ </div>
51
+ </div>
@@ -0,0 +1,8 @@
1
+ export { default as LiteListPage } from './LiteListPage.svelte';
2
+ export { default as LiteCreatePage } from './LiteCreatePage.svelte';
3
+ export { default as LiteEditPage } from './LiteEditPage.svelte';
4
+ export { default as LiteShowPage } from './LiteShowPage.svelte';
5
+ export { default as LiteRegisterPage } from './LiteRegisterPage.svelte';
6
+ export { default as LiteForgotPasswordPage } from './LiteForgotPasswordPage.svelte';
7
+ export { default as LiteUpdatePasswordPage } from './LiteUpdatePasswordPage.svelte';
8
+ export { default as LiteProfilePage } from './LiteProfilePage.svelte';
@@ -0,0 +1,40 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SSR AnomalyBadge — Rendered as a static colored badge.
4
+ * The SPA version has tooltips (requires JS). In lite mode,
5
+ * we use a title attribute for the tooltip fallback.
6
+ */
7
+ interface Props {
8
+ value: number;
9
+ baseline: number;
10
+ threshold?: number;
11
+ formatter?: (val: number) => string;
12
+ lowerIsBetter?: boolean;
13
+ }
14
+
15
+ let {
16
+ value,
17
+ baseline,
18
+ threshold = 0.2,
19
+ formatter = (v: number) => v.toString(),
20
+ lowerIsBetter = false,
21
+ }: Props = $props();
22
+
23
+ const diff = $derived(baseline === 0 ? 0 : (value - baseline) / Math.abs(baseline));
24
+ const isAnomaly = $derived(Math.abs(diff) >= threshold);
25
+ const isGood = $derived(!isAnomaly ? null : (lowerIsBetter ? diff < 0 : diff > 0));
26
+ const percentLabel = $derived(`${Math.abs(diff * 100).toFixed(1)}%`);
27
+ const arrow = $derived(diff > 0 ? '↑' : '↓');
28
+ </script>
29
+
30
+ {#if !isAnomaly}
31
+ <span style="font-size: 14px; color: #64748b;">{formatter(value)}</span>
32
+ {:else}
33
+ <span
34
+ title="Baseline: {formatter(baseline)} ({diff > 0 ? '+' : '-'}{percentLabel})"
35
+ class="lite-badge"
36
+ style="background: {isGood ? '#ecfdf5' : '#fef2f2'}; color: {isGood ? '#065f46' : '#991b1b'}; border: 1px solid {isGood ? '#a7f3d0' : '#fecaca'};"
37
+ >
38
+ {arrow} {formatter(value)}
39
+ </span>
40
+ {/if}
@@ -0,0 +1,45 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SSR BarChart — Rendered as a pure CSS horizontal bar chart.
4
+ * No charting library needed. Data is rendered server-side as
5
+ * colored div bars with percentage widths.
6
+ */
7
+ interface DataPoint {
8
+ label: string;
9
+ value: number;
10
+ color?: string;
11
+ }
12
+
13
+ interface Props {
14
+ data: DataPoint[];
15
+ title?: string;
16
+ }
17
+
18
+ let { data, title }: Props = $props();
19
+
20
+ const maxValue = $derived(Math.max(...data.map(d => d.value), 1));
21
+ </script>
22
+
23
+ <div class="lite-card" style="margin-bottom: 16px;">
24
+ {#if title}
25
+ <div style="padding: 12px 16px; border-bottom: 1px solid #e2e8f0; font-weight: 600; font-size: 14px; color: #0f172a;">
26
+ {title}
27
+ </div>
28
+ {/if}
29
+ <div style="padding: 16px; display: flex; flex-direction: column; gap: 12px;">
30
+ {#each data as point}
31
+ <div>
32
+ <div style="display: flex; justify-content: space-between; margin-bottom: 4px; font-size: 13px;">
33
+ <span style="color: #334155;">{point.label}</span>
34
+ <span style="color: #64748b; font-weight: 500;">{point.value}</span>
35
+ </div>
36
+ <div style="height: 20px; background: #f1f5f9; border-radius: 4px; overflow: hidden;">
37
+ <div style="height: 100%; width: {(point.value / maxValue) * 100}%; background: {point.color ?? '#6366f1'}; border-radius: 4px; transition: width 0.3s;"></div>
38
+ </div>
39
+ </div>
40
+ {/each}
41
+ {#if data.length === 0}
42
+ <p style="color: #94a3b8; font-style: italic; text-align: center;">No data</p>
43
+ {/if}
44
+ </div>
45
+ </div>