sveltekit-admin 0.2.0 → 0.5.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 (72) hide show
  1. package/README.md +80 -2
  2. package/dist/index.d.ts +2 -7
  3. package/dist/index.js +2 -13
  4. package/dist/server/auth.d.ts +7 -0
  5. package/dist/server/auth.js +19 -0
  6. package/dist/server/data.d.ts +38 -0
  7. package/dist/server/data.js +116 -0
  8. package/dist/server/handler.d.ts +123 -4
  9. package/dist/server/handler.js +590 -819
  10. package/dist/server/introspection/parser.d.ts +19 -12
  11. package/dist/server/introspection/parser.js +71 -61
  12. package/dist/server/introspection/relations.d.ts +49 -0
  13. package/dist/server/introspection/relations.js +128 -0
  14. package/dist/server/query/filterDetection.d.ts +71 -0
  15. package/dist/server/query/filterDetection.js +153 -0
  16. package/dist/server/query/listQuery.d.ts +89 -0
  17. package/dist/server/query/listQuery.js +428 -0
  18. package/dist/server/query/urls.d.ts +33 -0
  19. package/dist/server/query/urls.js +58 -0
  20. package/dist/server/router.d.ts +6 -0
  21. package/dist/server/router.js +27 -0
  22. package/dist/server/views/Dashboard.svelte +29 -0
  23. package/dist/server/views/Dashboard.svelte.d.ts +15 -0
  24. package/dist/server/views/FieldInput.svelte +63 -0
  25. package/dist/server/views/FieldInput.svelte.d.ts +9 -0
  26. package/dist/server/views/Form.svelte +127 -0
  27. package/dist/server/views/Form.svelte.d.ts +12 -0
  28. package/dist/server/views/Layout.svelte +78 -0
  29. package/dist/server/views/Layout.svelte.d.ts +13 -0
  30. package/dist/server/views/List.svelte +240 -0
  31. package/dist/server/views/List.svelte.d.ts +27 -0
  32. package/dist/server/views/ListFilters.svelte +257 -0
  33. package/dist/server/views/ListFilters.svelte.d.ts +18 -0
  34. package/dist/server/views/ModelCard.svelte +11 -0
  35. package/dist/server/views/ModelCard.svelte.d.ts +8 -0
  36. package/dist/server/views/NotFound.svelte +7 -0
  37. package/dist/server/views/NotFound.svelte.d.ts +7 -0
  38. package/dist/server/views/RelatedBlock.svelte +74 -0
  39. package/dist/server/views/RelatedBlock.svelte.d.ts +11 -0
  40. package/dist/server/views/RelationCheckboxes.svelte +53 -0
  41. package/dist/server/views/RelationCheckboxes.svelte.d.ts +9 -0
  42. package/dist/server/views/RelationSelect.svelte +53 -0
  43. package/dist/server/views/RelationSelect.svelte.d.ts +12 -0
  44. package/dist/server/views/StatCard.svelte +18 -0
  45. package/dist/server/views/StatCard.svelte.d.ts +8 -0
  46. package/dist/server/views/html.d.ts +5 -0
  47. package/dist/server/views/html.js +41 -0
  48. package/dist/server/views/theme.d.ts +1 -0
  49. package/dist/server/views/theme.js +484 -0
  50. package/dist/server/views/types.d.ts +57 -0
  51. package/dist/server/views/types.js +1 -0
  52. package/package.json +24 -26
  53. package/dist/admin.d.ts +0 -227
  54. package/dist/admin.js +0 -369
  55. package/dist/components/AdminForm.svelte +0 -423
  56. package/dist/components/AdminForm.svelte.d.ts +0 -30
  57. package/dist/components/AdminLayout.svelte +0 -328
  58. package/dist/components/AdminLayout.svelte.d.ts +0 -20
  59. package/dist/components/DataTable.svelte +0 -573
  60. package/dist/components/DataTable.svelte.d.ts +0 -25
  61. package/dist/components/index.d.ts +0 -3
  62. package/dist/components/index.js +0 -3
  63. package/dist/server/auth/guard.d.ts +0 -36
  64. package/dist/server/auth/guard.js +0 -38
  65. package/dist/server/auth/index.d.ts +0 -1
  66. package/dist/server/auth/index.js +0 -1
  67. package/dist/server/crud/index.d.ts +0 -1
  68. package/dist/server/crud/index.js +0 -1
  69. package/dist/server/crud/operations.d.ts +0 -87
  70. package/dist/server/crud/operations.js +0 -276
  71. package/dist/server/introspection/index.d.ts +0 -1
  72. package/dist/server/introspection/index.js +0 -1
@@ -0,0 +1,257 @@
1
+ <script lang="ts">
2
+ import type { ResolvedFilterField } from '../query/filterDetection.js';
3
+ import type { FkFilterMeta } from './types.js';
4
+ import { buildListUrl, hiddenParams } from '../query/urls.js';
5
+
6
+ /**
7
+ * Sidebar de filtres façon Django admin. Boolean/enum/datetime rendus en
8
+ * liens (§3.1 : zéro JS, un clic = un état). Les plages numériques et les
9
+ * FK à forte cardinalité utilisent un `<form method="GET">` avec bouton
10
+ * explicite — la seule exception documentée au rendu en liens, qui reste
11
+ * fonctionnelle sans JavaScript (§3.2).
12
+ */
13
+ let {
14
+ filters,
15
+ activeValues,
16
+ activeRangeValues,
17
+ fkFilterMeta,
18
+ currentUrl
19
+ }: {
20
+ filters: ResolvedFilterField[];
21
+ /** Valeur brute active par champ (Boolean/enum/datetime/FK), telle qu'apparue dans l'URL. */
22
+ activeValues: Map<string, string>;
23
+ /** Bornes actives par champ range, ex. { views: { gte: '10', lte: '100' } }. */
24
+ activeRangeValues: Map<string, { gte?: string; lte?: string }>;
25
+ /** Résolution async, scopée, des FK configurées. */
26
+ fkFilterMeta: Map<string, FkFilterMeta>;
27
+ currentUrl: URL;
28
+ } = $props();
29
+
30
+ interface Option {
31
+ label: string;
32
+ href: string;
33
+ active: boolean;
34
+ /** Valeur brute envoyée dans `f.<field>` ; null représente l'option All. */
35
+ value: string | null;
36
+ }
37
+
38
+ interface LinkGroup {
39
+ kind: 'links';
40
+ field: string;
41
+ label: string;
42
+ options: Option[];
43
+ }
44
+
45
+ interface RangeGroup {
46
+ kind: 'range';
47
+ field: string;
48
+ label: string;
49
+ gteParam: string;
50
+ lteParam: string;
51
+ gteValue: string;
52
+ lteValue: string;
53
+ hidden: { name: string; value: string }[];
54
+ }
55
+
56
+ interface FkGroup {
57
+ kind: 'fk';
58
+ field: string;
59
+ label: string;
60
+ mode: 'links' | 'select' | 'raw-id';
61
+ options: Option[];
62
+ inputValue: string;
63
+ hidden: { name: string; value: string }[];
64
+ activeLabel?: string;
65
+ activeHref?: string;
66
+ clearHref: string;
67
+ }
68
+
69
+ const PRESET_LABELS: Record<string, string> = {
70
+ today: 'Today',
71
+ '7d': 'Last 7 days',
72
+ month: 'This month',
73
+ year: 'This year'
74
+ };
75
+
76
+ const groups = $derived.by((): (LinkGroup | RangeGroup | FkGroup)[] => {
77
+ return filters.flatMap((f): (LinkGroup | RangeGroup | FkGroup)[] => {
78
+ const paramName = `f.${f.field}`;
79
+
80
+ if (f.kind === 'fk') {
81
+ const meta = fkFilterMeta.get(f.field);
82
+ // Résolution async indisponible (client Prisma incomplet, modèle
83
+ // cible absent...) : ne pas afficher un filtre mort. Le filtre URL
84
+ // reste accepté par parseListQuery, mais l'UI ne l'invente pas.
85
+ if (!meta) return [];
86
+ const current = activeValues.get(f.field);
87
+ const allOption: Option = {
88
+ label: 'All',
89
+ href: buildListUrl(currentUrl, { [paramName]: null }),
90
+ active: current === undefined,
91
+ value: null
92
+ };
93
+ const options = meta.options.map((o) => ({
94
+ label: o.label,
95
+ href: buildListUrl(currentUrl, { [paramName]: String(o.id) }),
96
+ active: current === String(o.id),
97
+ value: String(o.id)
98
+ }));
99
+ return [{
100
+ kind: 'fk',
101
+ field: f.field,
102
+ label: f.label,
103
+ mode: meta.mode,
104
+ options: [allOption, ...options],
105
+ inputValue: current ?? '',
106
+ hidden: hiddenParams(currentUrl, [paramName]),
107
+ // Une valeur forgée hors scope a activeLabel undefined : afficher
108
+ // l'ID brut dans le chip, jamais un label qui fuit un autre tenant.
109
+ activeLabel: current === undefined ? undefined : meta.activeLabel ?? current,
110
+ activeHref: meta.activeHref,
111
+ clearHref: buildListUrl(currentUrl, { [paramName]: null })
112
+ }];
113
+ }
114
+
115
+ if (f.kind === 'range') {
116
+ const gteParam = `f.${f.field}__gte`;
117
+ const lteParam = `f.${f.field}__lte`;
118
+ const active = activeRangeValues.get(f.field);
119
+ return [{
120
+ kind: 'range',
121
+ field: f.field,
122
+ label: f.label,
123
+ gteParam,
124
+ lteParam,
125
+ gteValue: active?.gte ?? '',
126
+ lteValue: active?.lte ?? '',
127
+ hidden: hiddenParams(currentUrl, [gteParam, lteParam])
128
+ }];
129
+ }
130
+
131
+ const current = activeValues.get(f.field);
132
+ const allOption: Option = {
133
+ label: 'All',
134
+ href: buildListUrl(currentUrl, { [paramName]: null }),
135
+ active: current === undefined,
136
+ value: null
137
+ };
138
+
139
+ let valueOptions: Option[];
140
+ if (f.kind === 'boolean') {
141
+ valueOptions = [
142
+ { value: 'true', label: 'Yes' },
143
+ { value: 'false', label: 'No' }
144
+ ].map((o) => ({
145
+ label: o.label,
146
+ href: buildListUrl(currentUrl, { [paramName]: o.value }),
147
+ active: current === o.value,
148
+ value: o.value
149
+ }));
150
+ } else if (f.kind === 'datetime') {
151
+ valueOptions = (f.presets ?? []).map((p) => ({
152
+ label: PRESET_LABELS[p] ?? p,
153
+ href: buildListUrl(currentUrl, { [paramName]: p }),
154
+ active: current === p,
155
+ value: p
156
+ }));
157
+ } else {
158
+ valueOptions = (f.enumValues ?? []).map((v) => ({
159
+ label: v,
160
+ href: buildListUrl(currentUrl, { [paramName]: v }),
161
+ active: current === v,
162
+ value: v
163
+ }));
164
+ }
165
+
166
+ return [{ kind: 'links', field: f.field, label: f.label, options: [allOption, ...valueOptions] }];
167
+ });
168
+ });
169
+ </script>
170
+
171
+ {#if groups.length > 0}
172
+ <div class="ska-card ska-filters">
173
+ {#each groups as group (group.field)}
174
+ <div class="ska-filters__group">
175
+ <h3 class="ska-filters__title">{group.label}</h3>
176
+
177
+ {#if group.kind === 'range'}
178
+ <form method="GET" class="ska-filters__range">
179
+ {#each group.hidden as p (p.name)}
180
+ <input type="hidden" name={p.name} value={p.value} />
181
+ {/each}
182
+ <input
183
+ type="number"
184
+ name={group.gteParam}
185
+ value={group.gteValue}
186
+ placeholder="Min"
187
+ class="ska-filters__range-input"
188
+ />
189
+ <input
190
+ type="number"
191
+ name={group.lteParam}
192
+ value={group.lteValue}
193
+ placeholder="Max"
194
+ class="ska-filters__range-input"
195
+ />
196
+ <button type="submit" class="ska-btn ska-btn--secondary ska-btn--sm">Apply</button>
197
+ </form>
198
+
199
+ {:else if group.kind === 'fk' && group.activeLabel}
200
+ <p class="ska-filters__chip">
201
+ {#if group.activeHref}
202
+ <a href={group.activeHref}>{group.activeLabel}</a>
203
+ {:else}
204
+ <span>{group.activeLabel}</span>
205
+ {/if}
206
+ <a href={group.clearHref} class="ska-filters__chip-clear" aria-label={`Clear ${group.label}`}>×</a>
207
+ </p>
208
+ {/if}
209
+
210
+ {#if group.kind === 'fk' && group.mode === 'select'}
211
+ <form method="GET" class="ska-filters__select">
212
+ {#each group.hidden as p (p.name)}
213
+ <input type="hidden" name={p.name} value={p.value} />
214
+ {/each}
215
+ <select name={`f.${group.field}`} class="ska-filters__select-input">
216
+ {#each group.options as option (option.value ?? 'all')}
217
+ <option value={option.value ?? ''} selected={option.active}>{option.label}</option>
218
+ {/each}
219
+ </select>
220
+ <button type="submit" class="ska-btn ska-btn--secondary ska-btn--sm">Apply</button>
221
+ </form>
222
+
223
+ {:else if group.kind === 'fk' && group.mode === 'raw-id'}
224
+ <form method="GET" class="ska-filters__range">
225
+ {#each group.hidden as p (p.name)}
226
+ <input type="hidden" name={p.name} value={p.value} />
227
+ {/each}
228
+ <input
229
+ type="text"
230
+ name={`f.${group.field}`}
231
+ value={group.inputValue}
232
+ placeholder="ID"
233
+ class="ska-filters__range-input"
234
+ />
235
+ <button type="submit" class="ska-btn ska-btn--secondary ska-btn--sm">Apply</button>
236
+ </form>
237
+
238
+ {:else if group.kind === 'links' || (group.kind === 'fk' && group.mode === 'links')}
239
+ <ul class="ska-filters__list">
240
+ {#each group.options as option (option.value ?? 'all')}
241
+ <li>
242
+ <a
243
+ href={option.href}
244
+ class="ska-filters__link"
245
+ class:ska-filters__link--active={option.active}
246
+ aria-current={option.active ? 'page' : undefined}
247
+ >
248
+ {option.label}
249
+ </a>
250
+ </li>
251
+ {/each}
252
+ </ul>
253
+ {/if}
254
+ </div>
255
+ {/each}
256
+ </div>
257
+ {/if}
@@ -0,0 +1,18 @@
1
+ import type { ResolvedFilterField } from '../query/filterDetection.js';
2
+ import type { FkFilterMeta } from './types.js';
3
+ type $$ComponentProps = {
4
+ filters: ResolvedFilterField[];
5
+ /** Valeur brute active par champ (Boolean/enum/datetime/FK), telle qu'apparue dans l'URL. */
6
+ activeValues: Map<string, string>;
7
+ /** Bornes actives par champ range, ex. { views: { gte: '10', lte: '100' } }. */
8
+ activeRangeValues: Map<string, {
9
+ gte?: string;
10
+ lte?: string;
11
+ }>;
12
+ /** Résolution async, scopée, des FK configurées. */
13
+ fkFilterMeta: Map<string, FkFilterMeta>;
14
+ currentUrl: URL;
15
+ };
16
+ declare const ListFilters: import("svelte").Component<$$ComponentProps, {}, "">;
17
+ type ListFilters = ReturnType<typeof ListFilters>;
18
+ export default ListFilters;
@@ -0,0 +1,11 @@
1
+ <script lang="ts">
2
+ let { href, label, count }: { href: string; label: string; count: number } = $props();
3
+ </script>
4
+
5
+ <a {href} class="ska-model-card">
6
+ <div>
7
+ <div class="ska-model-card__name">{label}</div>
8
+ <div class="ska-model-card__count">{count} records</div>
9
+ </div>
10
+ <div class="ska-model-card__footer">Manage →</div>
11
+ </a>
@@ -0,0 +1,8 @@
1
+ type $$ComponentProps = {
2
+ href: string;
3
+ label: string;
4
+ count: number;
5
+ };
6
+ declare const ModelCard: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type ModelCard = ReturnType<typeof ModelCard>;
8
+ export default ModelCard;
@@ -0,0 +1,7 @@
1
+ <script lang="ts">
2
+ let { message, basePath }: { message: string; basePath: string } = $props();
3
+ </script>
4
+
5
+ <h1>Not Found</h1>
6
+ <p class="ska-subtitle">{message}</p>
7
+ <a href={basePath} class="ska-btn ska-btn--secondary">← Back to Dashboard</a>
@@ -0,0 +1,7 @@
1
+ type $$ComponentProps = {
2
+ message: string;
3
+ basePath: string;
4
+ };
5
+ declare const NotFound: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type NotFound = ReturnType<typeof NotFound>;
7
+ export default NotFound;
@@ -0,0 +1,74 @@
1
+ <script lang="ts">
2
+ import type { RelationEdge, RelationGraph } from '../introspection/relations.js';
3
+ import { toLabel } from './html.js';
4
+
5
+ /**
6
+ * Bloc read-only pour les relations inverses (1-N inverse, 1-1 inverse) :
7
+ * compteur + lien "voir tout" filtré + lien "ajouter" pré-rempli avec la FK.
8
+ * Jamais éditable depuis ce formulaire — éditer une liste d'enfants depuis
9
+ * le parent, c'est un inline formset (non-objectif, voir
10
+ * docs/design/relations.md §2.5/§3.4).
11
+ */
12
+ let {
13
+ edges,
14
+ graph,
15
+ counts,
16
+ currentId,
17
+ basePath
18
+ }: {
19
+ edges: RelationEdge[];
20
+ graph: RelationGraph;
21
+ counts: Map<string, number>;
22
+ currentId: string | number;
23
+ basePath: string;
24
+ } = $props();
25
+
26
+ interface Row {
27
+ field: string;
28
+ label: string;
29
+ count: number;
30
+ viewAllHref: string;
31
+ addHref: string;
32
+ }
33
+
34
+ const rows = $derived.by((): Row[] => {
35
+ const out: Row[] = [];
36
+ for (const e of edges) {
37
+ // Le champ FK vit sur la contrepartie owning (ex: `User.posts` inverse
38
+ // ↔ `Post.author` owning, qui porte `authorId`), pas sur `e` lui-même.
39
+ const owning = [...graph.edges.values()].find(
40
+ (o) => o.model === e.target && o.kind === 'to-one-owning' && o.relationName === e.relationName
41
+ );
42
+ if (!owning || owning.unsupported) continue;
43
+
44
+ const targetPath = `${basePath}/${e.target.toLowerCase()}`;
45
+ const scalarName = owning.scalarFields[0];
46
+ out.push({
47
+ field: e.field,
48
+ label: toLabel(e.field),
49
+ count: counts.get(`${e.model}.${e.field}`)!,
50
+ // `f.<field>=<value>` — même pipeline sécurisé que la recherche/
51
+ // filtre (whitelist + coercion, docs/design/list-search-filters.md).
52
+ // L'ancien `?filter=field:value` reste supporté en lecture pour la
53
+ // rétrocompatibilité, mais ce composant n'en émet plus.
54
+ viewAllHref: `${targetPath}?f.${encodeURIComponent(scalarName)}=${encodeURIComponent(String(currentId))}`,
55
+ addHref: `${targetPath}/new?${encodeURIComponent(scalarName)}=${encodeURIComponent(String(currentId))}`
56
+ });
57
+ }
58
+ return out;
59
+ });
60
+ </script>
61
+
62
+ {#if rows.length > 0}
63
+ <div class="ska-card ska-related">
64
+ <h2 class="ska-subtitle">Liaisons</h2>
65
+ {#each rows as row (row.field)}
66
+ <div class="ska-related-row">
67
+ <span class="ska-label">{row.label}</span>
68
+ <span class="ska-subtitle">{row.count} record{row.count === 1 ? '' : 's'}</span>
69
+ <a href={row.viewAllHref} class="ska-btn ska-btn--secondary ska-btn--sm">Voir tout</a>
70
+ <a href={row.addHref} class="ska-btn ska-btn--secondary ska-btn--sm">Ajouter</a>
71
+ </div>
72
+ {/each}
73
+ </div>
74
+ {/if}
@@ -0,0 +1,11 @@
1
+ import type { RelationEdge, RelationGraph } from '../introspection/relations.js';
2
+ type $$ComponentProps = {
3
+ edges: RelationEdge[];
4
+ graph: RelationGraph;
5
+ counts: Map<string, number>;
6
+ currentId: string | number;
7
+ basePath: string;
8
+ };
9
+ declare const RelatedBlock: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type RelatedBlock = ReturnType<typeof RelatedBlock>;
11
+ export default RelatedBlock;
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ import type { RelationEdge } from '../introspection/relations.js';
3
+ import type { RelationMeta } from './types.js';
4
+ import { toLabel } from './html.js';
5
+
6
+ let {
7
+ edge,
8
+ meta
9
+ }: {
10
+ edge: RelationEdge;
11
+ meta: RelationMeta;
12
+ } = $props();
13
+
14
+ const label = $derived(toLabel(edge.field));
15
+ const inputName = $derived(`__rel__${edge.field}`);
16
+ const selected = $derived(new Set((meta.selectedIds ?? []).map(String)));
17
+ </script>
18
+
19
+ <!--
20
+ Fieldset de checkboxes pour une arête m2m-implicite.
21
+
22
+ Nommage `__rel__<field>` pour les valeurs cochées et un hidden sentinelle
23
+ `__rel_present__<field>` toujours émis : en HTML, zéro checkbox cochée
24
+ retire la clé du POST, donc impossible de distinguer "tout décoché" de
25
+ "widget absent" sans ce sentinelle. Voir docs/design/relations.md §3.1.
26
+ -->
27
+ {#if meta.tooMany}
28
+ <!-- Au-delà du seuil : liste d'IDs texte, jamais des centaines de checkboxes. -->
29
+ <div class="ska-field">
30
+ <label class="ska-label" for={inputName}>{label} (IDs séparés par des virgules)</label>
31
+ <input type="hidden" name="__rel_present__{edge.field}" value="1" />
32
+ <input id={inputName} type="text" name={inputName} value={[...selected].join(',')} class="ska-input" />
33
+ </div>
34
+ {:else}
35
+ <div class="ska-field">
36
+ <label class="ska-label" for={inputName}>{label}</label>
37
+ <input type="hidden" name="__rel_present__{edge.field}" value="1" />
38
+ <fieldset id={inputName} class="ska-checkbox-group">
39
+ {#each meta.options as opt (opt.id)}
40
+ <label class="ska-checkbox-wrap">
41
+ <input
42
+ type="checkbox"
43
+ name={inputName}
44
+ value={opt.id}
45
+ class="ska-checkbox"
46
+ checked={selected.has(String(opt.id))}
47
+ />
48
+ <span class="ska-label">{opt.label}</span>
49
+ </label>
50
+ {/each}
51
+ </fieldset>
52
+ </div>
53
+ {/if}
@@ -0,0 +1,9 @@
1
+ import type { RelationEdge } from '../introspection/relations.js';
2
+ import type { RelationMeta } from './types.js';
3
+ type $$ComponentProps = {
4
+ edge: RelationEdge;
5
+ meta: RelationMeta;
6
+ };
7
+ declare const RelationCheckboxes: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type RelationCheckboxes = ReturnType<typeof RelationCheckboxes>;
9
+ export default RelationCheckboxes;
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ import type { AdminHandlerConfig } from '../handler.js';
3
+ import type { RelationEdge } from '../introspection/relations.js';
4
+ import type { RelationMeta } from './types.js';
5
+ import { toLabel } from './html.js';
6
+
7
+ let {
8
+ edge,
9
+ meta,
10
+ currentValue,
11
+ config
12
+ }: {
13
+ edge: RelationEdge;
14
+ meta: RelationMeta;
15
+ currentValue: unknown;
16
+ config: AdminHandlerConfig;
17
+ } = $props();
18
+
19
+ const relConfig = $derived(config.models?.[edge.model]?.relations?.[edge.field]);
20
+ const label = $derived(toLabel(edge.field));
21
+ const required = $derived(edge.isRequired);
22
+ const nullLabel = $derived(relConfig?.nullLabel ?? '— aucun —');
23
+ // Toujours défini pour une arête to-one-owning : c'est ce qui la distingue
24
+ // des autres kinds dans le graphe (voir buildRelationGraph).
25
+ const scalarName = $derived(edge.scalarFields[0]);
26
+ </script>
27
+
28
+ {#if meta.tooMany}
29
+ <!-- Au-delà du seuil : raw-id, jamais un select de 10k lignes. -->
30
+ <div class="ska-field">
31
+ <label class="ska-label" for={scalarName}>{label} (ID){required ? ' *' : ''}</label>
32
+ <input
33
+ id={scalarName}
34
+ type="text"
35
+ name={scalarName}
36
+ value={currentValue ?? ''}
37
+ class="ska-input"
38
+ required={required}
39
+ />
40
+ </div>
41
+ {:else}
42
+ <div class="ska-field">
43
+ <label class="ska-label" for={scalarName}>{label}{required ? ' *' : ''}</label>
44
+ <select id={scalarName} name={scalarName} class="ska-input" required={required}>
45
+ {#if !required}
46
+ <option value="">{nullLabel}</option>
47
+ {/if}
48
+ {#each meta.options as opt (opt.id)}
49
+ <option value={opt.id} selected={String(opt.id) === String(currentValue ?? '')}>{opt.label}</option>
50
+ {/each}
51
+ </select>
52
+ </div>
53
+ {/if}
@@ -0,0 +1,12 @@
1
+ import type { AdminHandlerConfig } from '../handler.js';
2
+ import type { RelationEdge } from '../introspection/relations.js';
3
+ import type { RelationMeta } from './types.js';
4
+ type $$ComponentProps = {
5
+ edge: RelationEdge;
6
+ meta: RelationMeta;
7
+ currentValue: unknown;
8
+ config: AdminHandlerConfig;
9
+ };
10
+ declare const RelationSelect: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type RelationSelect = ReturnType<typeof RelationSelect>;
12
+ export default RelationSelect;
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ let { value, label, icon }: { value: number; label: string; icon: 'models' | 'records' } =
3
+ $props();
4
+ </script>
5
+
6
+ <div class="ska-stat">
7
+ <div class="ska-stat__icon">
8
+ {#if icon === 'models'}
9
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/></svg>
10
+ {:else}
11
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5V19A9 3 0 0 0 21 19V5"/><path d="M3 12A9 3 0 0 0 21 12"/></svg>
12
+ {/if}
13
+ </div>
14
+ <div>
15
+ <div class="ska-stat__value">{value}</div>
16
+ <div class="ska-stat__label">{label}</div>
17
+ </div>
18
+ </div>
@@ -0,0 +1,8 @@
1
+ type $$ComponentProps = {
2
+ value: number;
3
+ label: string;
4
+ icon: 'models' | 'records';
5
+ };
6
+ declare const StatCard: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type StatCard = ReturnType<typeof StatCard>;
8
+ export default StatCard;
@@ -0,0 +1,5 @@
1
+ /** Primitives de rendu partagées par toutes les vues. */
2
+ export declare function escapeHtml(str: string): string;
3
+ export declare function toLabel(name: string): string;
4
+ export declare function adjustColor(hex: string, percent: number): string;
5
+ export declare function formatValue(value: any, type: string): string;
@@ -0,0 +1,41 @@
1
+ /** Primitives de rendu partagées par toutes les vues. */
2
+ export function escapeHtml(str) {
3
+ return str
4
+ .replace(/&/g, '&amp;')
5
+ .replace(/</g, '&lt;')
6
+ .replace(/>/g, '&gt;')
7
+ .replace(/"/g, '&quot;')
8
+ .replace(/'/g, '&#39;');
9
+ }
10
+ export function toLabel(name) {
11
+ return name.replace(/([A-Z])/g, ' $1').trim();
12
+ }
13
+ /** Couleur de repli, alignée sur le `primaryColor` par défaut du layout. */
14
+ const DEFAULT_PRIMARY = '#6366f1';
15
+ export function adjustColor(hex, percent) {
16
+ // Sans ce garde-fou, un hex invalide part en `parseInt(…, 16)` → NaN, que les
17
+ // décalages de bits ramènent à 0 : la fonction rendait '#000000' sans rien signaler.
18
+ if (!/^#?[0-9a-f]{6}$/i.test(hex))
19
+ return DEFAULT_PRIMARY;
20
+ const num = parseInt(hex.replace('#', ''), 16);
21
+ const amt = Math.round(2.55 * percent);
22
+ const R = Math.max(0, Math.min(255, (num >> 16) + amt));
23
+ const G = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amt));
24
+ const B = Math.max(0, Math.min(255, (num & 0x0000FF) + amt));
25
+ return `#${(0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1)}`;
26
+ }
27
+ export function formatValue(value, type) {
28
+ if (value === null || value === undefined)
29
+ return '<span style="color:#94a3b8">—</span>';
30
+ if (type === 'DateTime') {
31
+ return new Date(value).toLocaleString();
32
+ }
33
+ if (type === 'Boolean') {
34
+ return value ? '✓' : '✗';
35
+ }
36
+ const str = String(value);
37
+ if (str.length > 50) {
38
+ return escapeHtml(str.slice(0, 50)) + '...';
39
+ }
40
+ return escapeHtml(str);
41
+ }
@@ -0,0 +1 @@
1
+ export declare function styles(primaryColor: string): string;