sveltekit-admin 0.8.1 → 0.9.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.
- package/dist/server/adapters/drizzle/dataAdapter.js +76 -7
- package/dist/server/adapters/prisma/dataAdapter.js +19 -1
- package/dist/server/adapters/types.d.ts +33 -1
- package/dist/server/data.d.ts +18 -2
- package/dist/server/data.js +39 -8
- package/dist/server/handler.d.ts +35 -0
- package/dist/server/handler.js +67 -15
- package/dist/server/introspection/parser.d.ts +15 -0
- package/dist/server/introspection/parser.js +17 -0
- package/dist/server/mutations.d.ts +8 -4
- package/dist/server/mutations.js +203 -22
- package/dist/server/query/listColumns.d.ts +19 -0
- package/dist/server/query/listColumns.js +43 -0
- package/dist/server/query/pageSize.d.ts +19 -0
- package/dist/server/query/pageSize.js +26 -0
- package/dist/server/query/sortQuery.d.ts +31 -0
- package/dist/server/query/sortQuery.js +33 -0
- package/dist/server/query/urls.js +9 -1
- package/dist/server/runtime.d.ts +5 -0
- package/dist/server/runtime.js +54 -1
- package/dist/server/submitted.d.ts +20 -0
- package/dist/server/submitted.js +54 -0
- package/dist/server/views/FieldInput.svelte +114 -8
- package/dist/server/views/FieldInput.svelte.d.ts +7 -0
- package/dist/server/views/Form.svelte +71 -4
- package/dist/server/views/Form.svelte.d.ts +17 -0
- package/dist/server/views/Layout.svelte +5 -2
- package/dist/server/views/List.svelte +128 -23
- package/dist/server/views/List.svelte.d.ts +5 -0
- package/dist/server/views/RelationCheckboxes.svelte +26 -5
- package/dist/server/views/RelationCheckboxes.svelte.d.ts +9 -0
- package/dist/server/views/RelationSelect.svelte +14 -3
- package/dist/server/views/RelationSelect.svelte.d.ts +2 -0
- package/dist/server/views/html.d.ts +19 -0
- package/dist/server/views/html.js +25 -0
- package/dist/server/views/pagination.d.ts +9 -0
- package/dist/server/views/pagination.js +31 -0
- package/dist/server/views/theme.js +147 -3
- package/dist/server/views/types.d.ts +7 -0
- package/package.json +2 -1
|
@@ -2,6 +2,7 @@ import type { AdminHandlerConfig } from '../handler.js';
|
|
|
2
2
|
import type { ViewModel, ListRecordAction, FkFilterMeta } from './types.js';
|
|
3
3
|
import type { ListQuery } from '../query/listQuery.js';
|
|
4
4
|
import type { ResolvedFilterField } from '../query/filterDetection.js';
|
|
5
|
+
import type { SortState } from '../query/sortQuery.js';
|
|
5
6
|
type $$ComponentProps = {
|
|
6
7
|
model: ViewModel;
|
|
7
8
|
items: any[];
|
|
@@ -20,6 +21,10 @@ type $$ComponentProps = {
|
|
|
20
21
|
listFilters?: ResolvedFilterField[];
|
|
21
22
|
/** Métadonnées async (options scopées + label actif) pour les filtres FK configurés. */
|
|
22
23
|
fkFilterMeta?: Map<string, FkFilterMeta>;
|
|
24
|
+
/** Tri actif + drapeau « colonne refusée ». Absent = en-têtes non cliquables. */
|
|
25
|
+
sort?: SortState;
|
|
26
|
+
/** Tailles de page proposées. Vide/absent = pas de sélecteur. */
|
|
27
|
+
pageSizes?: number[];
|
|
23
28
|
recordActions?: ListRecordAction[];
|
|
24
29
|
};
|
|
25
30
|
declare const List: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { RelationEdge } from '../introspection/relations.js';
|
|
3
3
|
import type { RelationMeta } from './types.js';
|
|
4
|
-
import { toLabel } from './html.js';
|
|
4
|
+
import { toLabel, fieldErrorAttrs } from './html.js';
|
|
5
5
|
|
|
6
6
|
let {
|
|
7
7
|
edge,
|
|
8
|
-
meta
|
|
8
|
+
meta,
|
|
9
|
+
submittedIds,
|
|
10
|
+
errorMessage
|
|
9
11
|
}: {
|
|
10
12
|
edge: RelationEdge;
|
|
11
13
|
meta: RelationMeta;
|
|
14
|
+
/**
|
|
15
|
+
* IDs cochés au dernier POST, quand la mutation a échoué. Un tableau vide
|
|
16
|
+
* dit « tout décoché » et doit gagner sur `meta.selectedIds` ; `undefined`
|
|
17
|
+
* dit « pas un re-rendu d'erreur ». C'est la même distinction que le
|
|
18
|
+
* sentinelle `__rel_present__` côté écriture.
|
|
19
|
+
*/
|
|
20
|
+
submittedIds?: string[];
|
|
21
|
+
/** Défini seulement si c'est cette relation que l'erreur désigne. */
|
|
22
|
+
errorMessage?: string;
|
|
12
23
|
} = $props();
|
|
13
24
|
|
|
14
25
|
const label = $derived(toLabel(edge.field));
|
|
15
26
|
const inputName = $derived(`__rel__${edge.field}`);
|
|
16
|
-
const selected = $derived(
|
|
27
|
+
const selected = $derived(
|
|
28
|
+
new Set((submittedIds ?? meta.selectedIds ?? []).map(String))
|
|
29
|
+
);
|
|
30
|
+
const err = $derived(fieldErrorAttrs(inputName, errorMessage));
|
|
17
31
|
</script>
|
|
18
32
|
|
|
19
33
|
<!--
|
|
@@ -29,13 +43,18 @@
|
|
|
29
43
|
<div class="ska-field">
|
|
30
44
|
<label class="ska-label" for={inputName}>{label} (IDs séparés par des virgules)</label>
|
|
31
45
|
<input type="hidden" name="__rel_present__{edge.field}" value="1" />
|
|
32
|
-
<input id={inputName} type="text" name={inputName} value={[...selected].join(',')} class="ska-input" />
|
|
46
|
+
<input id={inputName} type="text" name={inputName} value={[...selected].join(',')} class="ska-input" aria-invalid={err.ariaInvalid} aria-describedby={err.describedBy} />
|
|
47
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- fieldErrorAttrs échappe l'id et le message -->
|
|
48
|
+
{@html err.html}
|
|
33
49
|
</div>
|
|
34
50
|
{:else}
|
|
35
51
|
<div class="ska-field">
|
|
36
52
|
<label class="ska-label" for={inputName}>{label}</label>
|
|
37
53
|
<input type="hidden" name="__rel_present__{edge.field}" value="1" />
|
|
38
|
-
|
|
54
|
+
<!-- Pas d'`aria-invalid` ici : l'attribut n'est pas permis sur le rôle
|
|
55
|
+
`group` implicite d'un `<fieldset>`. Le rattachement du message se fait
|
|
56
|
+
par `aria-describedby` seul, qui l'est. -->
|
|
57
|
+
<fieldset id={inputName} class="ska-checkbox-group" aria-describedby={err.describedBy}>
|
|
39
58
|
{#each meta.options as opt (opt.id)}
|
|
40
59
|
<label class="ska-checkbox-wrap">
|
|
41
60
|
<input
|
|
@@ -49,5 +68,7 @@
|
|
|
49
68
|
</label>
|
|
50
69
|
{/each}
|
|
51
70
|
</fieldset>
|
|
71
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- fieldErrorAttrs échappe l'id et le message -->
|
|
72
|
+
{@html err.html}
|
|
52
73
|
</div>
|
|
53
74
|
{/if}
|
|
@@ -3,6 +3,15 @@ import type { RelationMeta } from './types.js';
|
|
|
3
3
|
type $$ComponentProps = {
|
|
4
4
|
edge: RelationEdge;
|
|
5
5
|
meta: RelationMeta;
|
|
6
|
+
/**
|
|
7
|
+
* IDs cochés au dernier POST, quand la mutation a échoué. Un tableau vide
|
|
8
|
+
* dit « tout décoché » et doit gagner sur `meta.selectedIds` ; `undefined`
|
|
9
|
+
* dit « pas un re-rendu d'erreur ». C'est la même distinction que le
|
|
10
|
+
* sentinelle `__rel_present__` côté écriture.
|
|
11
|
+
*/
|
|
12
|
+
submittedIds?: string[];
|
|
13
|
+
/** Défini seulement si c'est cette relation que l'erreur désigne. */
|
|
14
|
+
errorMessage?: string;
|
|
6
15
|
};
|
|
7
16
|
declare const RelationCheckboxes: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
17
|
type RelationCheckboxes = ReturnType<typeof RelationCheckboxes>;
|
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
import type { AdminHandlerConfig } from '../handler.js';
|
|
3
3
|
import type { RelationEdge } from '../introspection/relations.js';
|
|
4
4
|
import type { RelationMeta } from './types.js';
|
|
5
|
-
import { toLabel } from './html.js';
|
|
5
|
+
import { toLabel, fieldErrorAttrs } from './html.js';
|
|
6
6
|
|
|
7
7
|
let {
|
|
8
8
|
edge,
|
|
9
9
|
meta,
|
|
10
10
|
currentValue,
|
|
11
|
-
config
|
|
11
|
+
config,
|
|
12
|
+
errorMessage
|
|
12
13
|
}: {
|
|
13
14
|
edge: RelationEdge;
|
|
14
15
|
meta: RelationMeta;
|
|
15
16
|
currentValue: unknown;
|
|
16
17
|
config: AdminHandlerConfig;
|
|
18
|
+
/** Défini seulement si c'est cette relation que l'erreur désigne. */
|
|
19
|
+
errorMessage?: string;
|
|
17
20
|
} = $props();
|
|
18
21
|
|
|
19
22
|
const relConfig = $derived(config.models?.[edge.model]?.relations?.[edge.field]);
|
|
@@ -23,6 +26,8 @@
|
|
|
23
26
|
// Toujours défini pour une arête to-one-owning : c'est ce qui la distingue
|
|
24
27
|
// des autres kinds dans le graphe (voir buildRelationGraph).
|
|
25
28
|
const scalarName = $derived(edge.scalarFields[0]);
|
|
29
|
+
// Rattaché au scalaire : c'est lui qui porte l'`id` et le `name` du widget.
|
|
30
|
+
const err = $derived(fieldErrorAttrs(scalarName, errorMessage));
|
|
26
31
|
</script>
|
|
27
32
|
|
|
28
33
|
{#if meta.tooMany}
|
|
@@ -36,12 +41,16 @@
|
|
|
36
41
|
value={currentValue ?? ''}
|
|
37
42
|
class="ska-input"
|
|
38
43
|
required={required}
|
|
44
|
+
aria-invalid={err.ariaInvalid}
|
|
45
|
+
aria-describedby={err.describedBy}
|
|
39
46
|
/>
|
|
47
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- fieldErrorAttrs échappe l'id et le message -->
|
|
48
|
+
{@html err.html}
|
|
40
49
|
</div>
|
|
41
50
|
{:else}
|
|
42
51
|
<div class="ska-field">
|
|
43
52
|
<label class="ska-label" for={scalarName}>{label}{required ? ' *' : ''}</label>
|
|
44
|
-
<select id={scalarName} name={scalarName} class="ska-input" required={required}>
|
|
53
|
+
<select id={scalarName} name={scalarName} class="ska-input" required={required} aria-invalid={err.ariaInvalid} aria-describedby={err.describedBy}>
|
|
45
54
|
{#if !required}
|
|
46
55
|
<option value="">{nullLabel}</option>
|
|
47
56
|
{/if}
|
|
@@ -49,5 +58,7 @@
|
|
|
49
58
|
<option value={opt.id} selected={String(opt.id) === String(currentValue ?? '')}>{opt.label}</option>
|
|
50
59
|
{/each}
|
|
51
60
|
</select>
|
|
61
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -- fieldErrorAttrs échappe l'id et le message -->
|
|
62
|
+
{@html err.html}
|
|
52
63
|
</div>
|
|
53
64
|
{/if}
|
|
@@ -6,6 +6,8 @@ type $$ComponentProps = {
|
|
|
6
6
|
meta: RelationMeta;
|
|
7
7
|
currentValue: unknown;
|
|
8
8
|
config: AdminHandlerConfig;
|
|
9
|
+
/** Défini seulement si c'est cette relation que l'erreur désigne. */
|
|
10
|
+
errorMessage?: string;
|
|
9
11
|
};
|
|
10
12
|
declare const RelationSelect: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
13
|
type RelationSelect = ReturnType<typeof RelationSelect>;
|
|
@@ -3,3 +3,22 @@ export declare function escapeHtml(str: string): string;
|
|
|
3
3
|
export declare function toLabel(name: string): string;
|
|
4
4
|
export declare function adjustColor(hex: string, percent: number): string;
|
|
5
5
|
export declare function formatValue(value: any, type: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Markup du message d'erreur d'un champ, et les attributs qui l'y rattachent.
|
|
8
|
+
*
|
|
9
|
+
* Rendu comme une chaîne passée à `{@html}` plutôt qu'en `{#if}` : la SSR de
|
|
10
|
+
* Svelte 5 pose de toute façon un commentaire de frontière autour des deux, et
|
|
11
|
+
* les deux coûtent la même chose sur un formulaire sans erreur (mesuré : 18
|
|
12
|
+
* octets par champ pour `{@html}`, 19 pour `{#if}`). Le `{@html}` est retenu
|
|
13
|
+
* pour la cohérence avec `recordActionsHtml` dans `Form.svelte`, qui documente
|
|
14
|
+
* déjà ce compromis, et parce qu'il garde l'échappement explicite ici plutôt
|
|
15
|
+
* que réparti dans quatre branches de gabarit.
|
|
16
|
+
*
|
|
17
|
+
* `inputId` doit être l'`id` du champ décrit : c'est lui qui fabrique la cible
|
|
18
|
+
* d'`aria-describedby`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function fieldErrorAttrs(inputId: string, message: string | undefined): {
|
|
21
|
+
ariaInvalid: 'true' | undefined;
|
|
22
|
+
describedBy: string | undefined;
|
|
23
|
+
html: string;
|
|
24
|
+
};
|
|
@@ -39,3 +39,28 @@ export function formatValue(value, type) {
|
|
|
39
39
|
}
|
|
40
40
|
return escapeHtml(str);
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Markup du message d'erreur d'un champ, et les attributs qui l'y rattachent.
|
|
44
|
+
*
|
|
45
|
+
* Rendu comme une chaîne passée à `{@html}` plutôt qu'en `{#if}` : la SSR de
|
|
46
|
+
* Svelte 5 pose de toute façon un commentaire de frontière autour des deux, et
|
|
47
|
+
* les deux coûtent la même chose sur un formulaire sans erreur (mesuré : 18
|
|
48
|
+
* octets par champ pour `{@html}`, 19 pour `{#if}`). Le `{@html}` est retenu
|
|
49
|
+
* pour la cohérence avec `recordActionsHtml` dans `Form.svelte`, qui documente
|
|
50
|
+
* déjà ce compromis, et parce qu'il garde l'échappement explicite ici plutôt
|
|
51
|
+
* que réparti dans quatre branches de gabarit.
|
|
52
|
+
*
|
|
53
|
+
* `inputId` doit être l'`id` du champ décrit : c'est lui qui fabrique la cible
|
|
54
|
+
* d'`aria-describedby`.
|
|
55
|
+
*/
|
|
56
|
+
export function fieldErrorAttrs(inputId, message) {
|
|
57
|
+
if (message === undefined) {
|
|
58
|
+
return { ariaInvalid: undefined, describedBy: undefined, html: '' };
|
|
59
|
+
}
|
|
60
|
+
const errorId = `${inputId}-error`;
|
|
61
|
+
return {
|
|
62
|
+
ariaInvalid: 'true',
|
|
63
|
+
describedBy: errorId,
|
|
64
|
+
html: `<p class="ska-field__error" id="${escapeHtml(errorId)}">${escapeHtml(message)}</p>`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fenêtre de numéros de page : « 1 … 8 9 [10] 11 12 … 20 ».
|
|
3
|
+
*
|
|
4
|
+
* Pure, et volontairement hors du composant : les cas limites (premiers et
|
|
5
|
+
* derniers écrans, trous d'une seule page) se testent sur des nombres plutôt
|
|
6
|
+
* qu'en fouillant du HTML.
|
|
7
|
+
*/
|
|
8
|
+
export type PageEntry = number | 'gap';
|
|
9
|
+
export declare function paginationWindow(page: number, totalPages: number): PageEntry[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fenêtre de numéros de page : « 1 … 8 9 [10] 11 12 … 20 ».
|
|
3
|
+
*
|
|
4
|
+
* Pure, et volontairement hors du composant : les cas limites (premiers et
|
|
5
|
+
* derniers écrans, trous d'une seule page) se testent sur des nombres plutôt
|
|
6
|
+
* qu'en fouillant du HTML.
|
|
7
|
+
*/
|
|
8
|
+
/** Nombre de pages affichées de part et d'autre de la page courante. */
|
|
9
|
+
const SPAN = 2;
|
|
10
|
+
export function paginationWindow(page, totalPages) {
|
|
11
|
+
if (totalPages <= 1)
|
|
12
|
+
return [];
|
|
13
|
+
const first = 1;
|
|
14
|
+
const last = totalPages;
|
|
15
|
+
const from = Math.max(first, page - SPAN);
|
|
16
|
+
const to = Math.min(last, page + SPAN);
|
|
17
|
+
const entries = [];
|
|
18
|
+
for (let n = from; n <= to; n++)
|
|
19
|
+
entries.push(n);
|
|
20
|
+
// Un trou qui ne cacherait qu'une seule page ne gagne aucune place et coûte
|
|
21
|
+
// un clic : on montre la page à la place. D'où la comparaison à 1 et non 0.
|
|
22
|
+
if (from - first > 1)
|
|
23
|
+
entries.unshift(first, 'gap');
|
|
24
|
+
else if (from > first)
|
|
25
|
+
entries.unshift(first);
|
|
26
|
+
if (last - to > 1)
|
|
27
|
+
entries.push('gap', last);
|
|
28
|
+
else if (to < last)
|
|
29
|
+
entries.push(last);
|
|
30
|
+
return entries;
|
|
31
|
+
}
|
|
@@ -182,6 +182,45 @@ export function styles(primaryColor) {
|
|
|
182
182
|
letter-spacing: 0.05em;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
.ska-pagination__pages {
|
|
186
|
+
display: flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
gap: 0.25rem;
|
|
189
|
+
flex-wrap: wrap;
|
|
190
|
+
}
|
|
191
|
+
.ska-pagination__gap { padding: 0 0.25rem; color: #64748b; }
|
|
192
|
+
.ska-bulk { margin-bottom: 0.75rem; }
|
|
193
|
+
.ska-table__select { width: 2.5rem; text-align: center; }
|
|
194
|
+
.ska-alert--success {
|
|
195
|
+
background: #f0fdf4;
|
|
196
|
+
border-color: #bbf7d0;
|
|
197
|
+
color: #166534;
|
|
198
|
+
}
|
|
199
|
+
.ska-pagination__sizes {
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
gap: 0.25rem;
|
|
203
|
+
flex-wrap: wrap;
|
|
204
|
+
}
|
|
205
|
+
.ska-pagination__current {
|
|
206
|
+
background: var(--ska-primary);
|
|
207
|
+
color: #fff;
|
|
208
|
+
border-color: var(--ska-primary);
|
|
209
|
+
cursor: default;
|
|
210
|
+
}
|
|
211
|
+
.ska-table th a.ska-th-sort {
|
|
212
|
+
color: inherit;
|
|
213
|
+
text-decoration: none;
|
|
214
|
+
display: inline-flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
gap: 0.25rem;
|
|
217
|
+
}
|
|
218
|
+
.ska-table th a.ska-th-sort:hover { text-decoration: underline; }
|
|
219
|
+
.ska-table th a.ska-th-sort:focus-visible {
|
|
220
|
+
outline: 2px solid var(--ska-primary);
|
|
221
|
+
outline-offset: 2px;
|
|
222
|
+
border-radius: 2px;
|
|
223
|
+
}
|
|
185
224
|
.ska-table td {
|
|
186
225
|
padding: 0.75rem 1rem;
|
|
187
226
|
border-bottom: 1px solid #e2e8f0;
|
|
@@ -220,10 +259,15 @@ export function styles(primaryColor) {
|
|
|
220
259
|
transition: all 0.15s;
|
|
221
260
|
}
|
|
222
261
|
|
|
223
|
-
|
|
224
|
-
|
|
262
|
+
/* 'outline: none' remplacé par un box-shadow à 10 % d'opacité donnait un
|
|
263
|
+
contraste d'environ 1,1:1 contre le fond blanc — soit, en pratique, aucun
|
|
264
|
+
indicateur de focus. Le contour reprend la couleur de marque, qui est
|
|
265
|
+
elle contrastée, et le box-shadow ne fait plus que l'accompagner. */
|
|
266
|
+
.ska-input:focus-visible {
|
|
267
|
+
outline: 2px solid var(--ska-primary);
|
|
268
|
+
outline-offset: 1px;
|
|
225
269
|
border-color: var(--ska-primary);
|
|
226
|
-
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.
|
|
270
|
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
|
|
227
271
|
}
|
|
228
272
|
|
|
229
273
|
.ska-input[readonly] {
|
|
@@ -231,6 +275,25 @@ export function styles(primaryColor) {
|
|
|
231
275
|
color: #6b7280;
|
|
232
276
|
}
|
|
233
277
|
|
|
278
|
+
/* Sélecteur d'attribut et non une classe : aria-invalid est déjà la source
|
|
279
|
+
de vérité pour les lecteurs d'écran, une classe parallèle finirait par en
|
|
280
|
+
diverger. */
|
|
281
|
+
.ska-input[aria-invalid='true'] {
|
|
282
|
+
border-color: #dc2626;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.ska-input[aria-invalid='true']:focus-visible {
|
|
286
|
+
outline-color: #dc2626;
|
|
287
|
+
border-color: #dc2626;
|
|
288
|
+
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.ska-field__error {
|
|
292
|
+
margin-top: 0.375rem;
|
|
293
|
+
font-size: 0.8125rem;
|
|
294
|
+
color: #b91c1c;
|
|
295
|
+
}
|
|
296
|
+
|
|
234
297
|
.ska-checkbox-wrap {
|
|
235
298
|
display: flex;
|
|
236
299
|
align-items: center;
|
|
@@ -533,5 +596,86 @@ export function styles(primaryColor) {
|
|
|
533
596
|
color: #16a34a;
|
|
534
597
|
border: 1px solid #bbf7d0;
|
|
535
598
|
}
|
|
599
|
+
|
|
600
|
+
/* Hors flux tant qu'il n'est pas ciblé : 'display: none' le retirerait de
|
|
601
|
+
l'ordre de tabulation, ce qui le rendrait inatteignable — donc inutile. */
|
|
602
|
+
.ska-skip {
|
|
603
|
+
position: absolute;
|
|
604
|
+
left: -9999px;
|
|
605
|
+
top: 0;
|
|
606
|
+
background: var(--ska-primary);
|
|
607
|
+
color: #fff;
|
|
608
|
+
padding: 0.625rem 1rem;
|
|
609
|
+
border-radius: 0 0 0.375rem 0;
|
|
610
|
+
z-index: 100;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.ska-skip:focus {
|
|
614
|
+
left: 0;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/* Un seul contour, de la couleur de marque, sur tout ce qui prend le focus.
|
|
618
|
+
'focus-visible' et non 'focus' : le contour apparaît au clavier sans
|
|
619
|
+
s'afficher au clic à la souris. La cible du lien d'évitement porte
|
|
620
|
+
tabindex="-1" pour être focusable par programme, mais elle ne doit pas
|
|
621
|
+
dessiner de contour pour autant. */
|
|
622
|
+
.ska-btn:focus-visible,
|
|
623
|
+
.ska-nav__link:focus-visible,
|
|
624
|
+
.ska-logo:focus-visible,
|
|
625
|
+
.ska-back:focus-visible,
|
|
626
|
+
.ska-checkbox:focus-visible,
|
|
627
|
+
.ska-logout__btn:focus-visible,
|
|
628
|
+
.ska-skip:focus-visible {
|
|
629
|
+
outline: 2px solid var(--ska-primary);
|
|
630
|
+
outline-offset: 2px;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.ska-main:focus-visible { outline: none; }
|
|
634
|
+
|
|
635
|
+
/* La sidebar était fixe à 260px et le contenu décalé d'autant : sur un
|
|
636
|
+
écran de 375px il restait 115px utilisables. Elle repasse dans le flux et
|
|
637
|
+
la navigation devient horizontale. */
|
|
638
|
+
@media (max-width: 900px) {
|
|
639
|
+
.ska-layout { flex-direction: column; }
|
|
640
|
+
|
|
641
|
+
.ska-sidebar {
|
|
642
|
+
position: static;
|
|
643
|
+
width: auto;
|
|
644
|
+
height: auto;
|
|
645
|
+
border-right: none;
|
|
646
|
+
border-bottom: 1px solid #e2e8f0;
|
|
647
|
+
padding: 1rem;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.ska-nav {
|
|
651
|
+
display: flex;
|
|
652
|
+
flex-wrap: wrap;
|
|
653
|
+
gap: 0.25rem;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.ska-nav__item { margin-bottom: 0; }
|
|
657
|
+
|
|
658
|
+
.ska-main {
|
|
659
|
+
margin-left: 0;
|
|
660
|
+
padding: 1rem;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.ska-header {
|
|
664
|
+
flex-direction: column;
|
|
665
|
+
gap: 0.75rem;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.ska-form { max-width: none; }
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/* Cinq 'transition: all' vivent dans cette feuille. Le réglage système est
|
|
672
|
+
une demande explicite, pas une préférence esthétique. */
|
|
673
|
+
@media (prefers-reduced-motion: reduce) {
|
|
674
|
+
* {
|
|
675
|
+
transition-duration: 0.01ms !important;
|
|
676
|
+
animation-duration: 0.01ms !important;
|
|
677
|
+
scroll-behavior: auto !important;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
536
680
|
`;
|
|
537
681
|
}
|
|
@@ -16,6 +16,13 @@ export interface ViewModel {
|
|
|
16
16
|
label: string;
|
|
17
17
|
fields: PrismaField[];
|
|
18
18
|
primaryKey: string;
|
|
19
|
+
/**
|
|
20
|
+
* Valeurs déclarées de chaque enum du schéma, indexées par nom de type.
|
|
21
|
+
* Portées par le ViewModel plutôt que passées à part comme le fait
|
|
22
|
+
* `filterDetection` : le formulaire résout un enum champ par champ, et un
|
|
23
|
+
* second chemin d'acheminement vers les mêmes valeurs finirait par diverger.
|
|
24
|
+
*/
|
|
25
|
+
enums?: Map<string, string[]>;
|
|
19
26
|
relationGraph?: RelationGraph;
|
|
20
27
|
/** Options résolues pour chaque arête to-one-owning, indexées par "Model.field" */
|
|
21
28
|
relationOptions?: Map<string, RelationMeta>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sveltekit-admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Django-like admin panel for SvelteKit + Prisma",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"svelte": "./dist/index.js",
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
"build": "pnpm run package",
|
|
90
90
|
"package": "svelte-kit sync && svelte-package -o dist",
|
|
91
91
|
"size": "pnpm run package && node scripts/package-size.mjs",
|
|
92
|
+
"smoke:packaged": "node scripts/consumer-smoke.mjs",
|
|
92
93
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
93
94
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
94
95
|
"test:gen": "prisma generate --schema tests/fixtures/prisma/schema.prisma",
|