firstly 0.6.3 → 0.7.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 (47) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/esm/changeLog/index.d.ts +1 -0
  3. package/esm/svelte/FF_Config.svelte +2 -2
  4. package/esm/svelte/FF_Config.svelte.js +3 -0
  5. package/esm/svelte/grid/DefaultInput.svelte +42 -0
  6. package/esm/svelte/grid/DefaultInput.svelte.d.ts +9 -0
  7. package/esm/svelte/grid/FF_Cell.svelte +98 -0
  8. package/esm/svelte/grid/FF_Cell.svelte.d.ts +10 -0
  9. package/esm/svelte/grid/FF_CellValue.svelte +24 -0
  10. package/esm/svelte/grid/FF_CellValue.svelte.d.ts +28 -0
  11. package/esm/svelte/grid/FF_Cell_Content.svelte +37 -0
  12. package/esm/svelte/grid/FF_Cell_Content.svelte.d.ts +10 -0
  13. package/esm/svelte/grid/FF_Cell_Error.svelte +26 -0
  14. package/esm/svelte/grid/FF_Cell_Error.svelte.d.ts +8 -0
  15. package/esm/svelte/grid/FF_Cell_Hint.svelte +26 -0
  16. package/esm/svelte/grid/FF_Cell_Hint.svelte.d.ts +8 -0
  17. package/esm/svelte/grid/FF_Cell_Label.svelte +34 -0
  18. package/esm/svelte/grid/FF_Cell_Label.svelte.d.ts +8 -0
  19. package/esm/svelte/grid/FF_Grid.svelte +385 -0
  20. package/esm/svelte/grid/FF_Grid.svelte.d.ts +47 -0
  21. package/esm/svelte/grid/GroupFields.svelte +141 -0
  22. package/esm/svelte/grid/GroupFields.svelte.d.ts +45 -0
  23. package/esm/svelte/grid/_test/FF_Cell_ContextWrapper.svelte +9 -0
  24. package/esm/svelte/grid/_test/FF_Cell_ContextWrapper.svelte.d.ts +18 -0
  25. package/esm/svelte/grid/buildCells.d.ts +16 -0
  26. package/esm/svelte/grid/buildCells.js +79 -0
  27. package/esm/svelte/grid/cellComponent.d.ts +9 -0
  28. package/esm/svelte/grid/cellComponent.js +31 -0
  29. package/esm/svelte/grid/cellConfig.d.ts +13 -0
  30. package/esm/svelte/grid/cellConfig.js +48 -0
  31. package/esm/svelte/grid/cellTypes.d.ts +168 -0
  32. package/esm/svelte/grid/cellTypes.js +1 -0
  33. package/esm/svelte/grid/index.d.ts +10 -0
  34. package/esm/svelte/grid/index.js +12 -0
  35. package/esm/svelte/grid/metaKind.d.ts +27 -0
  36. package/esm/svelte/grid/metaKind.js +34 -0
  37. package/esm/svelte/index.d.ts +4 -2
  38. package/esm/svelte/index.js +4 -2
  39. package/esm/svelte/remultApiLoad.d.ts +19 -0
  40. package/esm/svelte/remultApiLoad.js +31 -0
  41. package/esm/svelte/server/index.d.ts +12 -0
  42. package/esm/svelte/server/index.js +27 -0
  43. package/package.json +6 -2
  44. package/esm/svelte/DemoForm.svelte +0 -121
  45. package/esm/svelte/DemoForm.svelte.d.ts +0 -42
  46. package/esm/svelte/DemoGrid.svelte +0 -258
  47. package/esm/svelte/DemoGrid.svelte.d.ts +0 -49
@@ -0,0 +1,79 @@
1
+ import { getInputType } from './cellConfig.js';
2
+ import { getFieldMetaType } from './metaKind.js';
3
+ const HIDE_BY_DEFAULT = new Set(['id', 'createdAt', 'updatedAt', 'deletedAt']);
4
+ /** Default column set when `cells` is omitted: visible fields minus id/timestamps. */
5
+ function defaultCells(meta) {
6
+ return meta.fields
7
+ .toArray()
8
+ .filter((f) => !HIDE_BY_DEFAULT.has(f.key) && !f.dbReadOnly && !f.isServerExpression)
9
+ .map((f) => f.key);
10
+ }
11
+ function alignFor(field, ui) {
12
+ if (ui.align)
13
+ return ui.align;
14
+ if (field?.inputType === 'number')
15
+ return 'right';
16
+ return 'left';
17
+ }
18
+ /** Resolve the render kind: explicit > href(field_link) > metaKind(relation/enum) > field. */
19
+ function resolveKind(field, explicit) {
20
+ if (explicit)
21
+ return explicit;
22
+ if (!field)
23
+ return 'spacer';
24
+ if (field.options.href)
25
+ return 'field_link';
26
+ const mk = getFieldMetaType(field);
27
+ if (mk.kind === 'relation')
28
+ return 'relation';
29
+ if (mk.kind === 'enum')
30
+ return mk.subKind === 'multi' ? 'enum_multi' : 'enum';
31
+ return 'field';
32
+ }
33
+ /**
34
+ * Build headless cell descriptors from entity metadata.
35
+ * `cells` is a terse list of field keys and/or config objects; omit it to auto-build
36
+ * from visible fields. Per-cell config overrides the field's `ui` option (escape the SSoT).
37
+ */
38
+ export function buildCells(meta, cells, opts) {
39
+ const input = cells ?? defaultCells(meta);
40
+ return input.map((item) => {
41
+ const isObj = typeof item === 'object';
42
+ const colRaw = isObj ? item.col : item;
43
+ const spacer = colRaw === '_spacer';
44
+ const field = !spacer
45
+ ? meta.fields.find(colRaw)
46
+ : undefined;
47
+ const fieldUI = field?.options.ui ?? {};
48
+ const ui = { ...fieldUI, ...(isObj ? item.ui : undefined) };
49
+ const kind = spacer ? 'spacer' : resolveKind(field, isObj ? item.kind : undefined);
50
+ // per-cell `sortable` wins; else the configured default (FF_Config / hub via opts), else on.
51
+ const sortable = isObj && item.sortable !== undefined ? item.sortable : (opts?.defaultSortable ?? true);
52
+ return {
53
+ col: spacer ? undefined : colRaw,
54
+ field,
55
+ kind,
56
+ caption: (isObj && item.caption) || field?.caption || '',
57
+ ui,
58
+ inputType: field ? getInputType(field, ui) : 'text',
59
+ align: alignFor(field, ui),
60
+ sortable,
61
+ class: isObj ? item.class : undefined,
62
+ cellSnippet: isObj ? item.cellSnippet : undefined,
63
+ component: isObj ? item.component : undefined,
64
+ props: isObj ? item.props : undefined,
65
+ rowToProps: isObj ? item.rowToProps : undefined,
66
+ };
67
+ });
68
+ }
69
+ /**
70
+ * Formatted display string for a cell + row. Uses remult `field.displayValue` (which already
71
+ * formats enums/dates/value-lists). Relations/multi-enum that need richer rendering should use
72
+ * a `cellSnippet` escape hatch (Stage 0 does not auto-resolve the related row's caption).
73
+ */
74
+ export function displayCell(cell, row) {
75
+ if (!cell.field)
76
+ return '';
77
+ const v = cell.field.displayValue(row);
78
+ return v ?? '';
79
+ }
@@ -0,0 +1,9 @@
1
+ import type { Component } from 'svelte';
2
+ import type { CellComponent } from './cellTypes.js';
3
+ /**
4
+ * Resolve a {@link CellComponent} thunk to a Component.
5
+ * - eager `() => Comp` → returns the Component **synchronously** (no render flash)
6
+ * - lazy `() => import('./X.svelte')` → returns a Promise (the module's `default` is unwrapped)
7
+ * Both are cached/deduped per thunk.
8
+ */
9
+ export declare function resolveCellComponent(thunk: CellComponent): Component | Promise<Component>;
@@ -0,0 +1,31 @@
1
+ // Cache resolved components (and in-flight loads) per thunk, so a lazy `() => import(...)` fires once
2
+ // across every row/cell that shares it, and eager `() => Comp` thunks resolve synchronously.
3
+ const resolved = new WeakMap();
4
+ const loading = new WeakMap();
5
+ /**
6
+ * Resolve a {@link CellComponent} thunk to a Component.
7
+ * - eager `() => Comp` → returns the Component **synchronously** (no render flash)
8
+ * - lazy `() => import('./X.svelte')` → returns a Promise (the module's `default` is unwrapped)
9
+ * Both are cached/deduped per thunk.
10
+ */
11
+ export function resolveCellComponent(thunk) {
12
+ const hit = resolved.get(thunk);
13
+ if (hit)
14
+ return hit;
15
+ const inFlight = loading.get(thunk);
16
+ if (inFlight)
17
+ return inFlight;
18
+ const r = thunk();
19
+ if (r instanceof Promise) {
20
+ const p = r.then((m) => {
21
+ const c = (m && typeof m === 'object' && 'default' in m ? m.default : m);
22
+ resolved.set(thunk, c);
23
+ loading.delete(thunk);
24
+ return c;
25
+ });
26
+ loading.set(thunk, p);
27
+ return p;
28
+ }
29
+ resolved.set(thunk, r);
30
+ return r;
31
+ }
@@ -0,0 +1,13 @@
1
+ import type { FieldMetadata } from 'remult';
2
+ import type { CellConfig, CellElementConfig, CellUI } from './cellTypes.js';
3
+ /** firstly's built-in geometry: label/error share a row, content + hint full width. */
4
+ export declare const defaultConfig: CellConfig;
5
+ /** Build an inline style string for one sub-element from its CellElementConfig. */
6
+ export declare function getStyle(config: CellElementConfig): string;
7
+ /**
8
+ * Resolve a sub-element's geometry: firstly default <- app-level FF_Config `cell.config`.
9
+ * MUST be called during component init (it reads context via `ffConfig()`).
10
+ */
11
+ export declare function getCellElementConfig(element: keyof CellConfig): CellElementConfig;
12
+ /** Resolved input type: merged `ui.inputType` > remult resolved `inputType` > 'text'. */
13
+ export declare function getInputType(field: FieldMetadata, ui?: CellUI): string;
@@ -0,0 +1,48 @@
1
+ import { ffConfig } from '../FF_Config.svelte.js';
2
+ /** firstly's built-in geometry: label/error share a row, content + hint full width. */
3
+ export const defaultConfig = {
4
+ label: { width: 50, order: 1, align: 'MiddleLeft' },
5
+ error: { width: 50, order: 2, align: 'MiddleRight' },
6
+ content: { width: 100, order: 3, align: 'MiddleLeft' },
7
+ hint: { width: 100, order: 4, align: 'MiddleLeft' },
8
+ };
9
+ /** Build an inline style string for one sub-element from its CellElementConfig. */
10
+ export function getStyle(config) {
11
+ const justify = config.align?.includes('Left')
12
+ ? 'flex-start'
13
+ : config.align?.includes('Right')
14
+ ? 'flex-end'
15
+ : config.align?.includes('Center')
16
+ ? 'center'
17
+ : undefined;
18
+ const items = config.align?.includes('Top')
19
+ ? 'start'
20
+ : config.align?.includes('Bottom')
21
+ ? 'end'
22
+ : config.align?.includes('Middle') || config.align?.includes('Center')
23
+ ? 'center'
24
+ : undefined;
25
+ return [
26
+ config.width != null ? `width: ${config.width}%` : '',
27
+ config.width != null ? `flex: 0 0 ${config.width}%` : '',
28
+ config.order != null ? `order: ${config.order}` : '',
29
+ `display: flex`,
30
+ justify ? `justify-content: ${justify}` : '',
31
+ items ? `align-items: ${items}` : '',
32
+ config.style ?? '',
33
+ ]
34
+ .filter(Boolean)
35
+ .join('; ');
36
+ }
37
+ /**
38
+ * Resolve a sub-element's geometry: firstly default <- app-level FF_Config `cell.config`.
39
+ * MUST be called during component init (it reads context via `ffConfig()`).
40
+ */
41
+ export function getCellElementConfig(element) {
42
+ const appConfig = ffConfig().cell?.config ?? {};
43
+ return { ...defaultConfig[element], ...appConfig[element] };
44
+ }
45
+ /** Resolved input type: merged `ui.inputType` > remult resolved `inputType` > 'text'. */
46
+ export function getInputType(field, ui) {
47
+ return ui?.inputType ?? field.inputType ?? 'text';
48
+ }
@@ -0,0 +1,168 @@
1
+ import type { Component, Snippet } from 'svelte';
2
+ import type { ClassType, EntityFilter, EntityOrderBy, FieldMetadata } from 'remult';
3
+ /**
4
+ * A component to render in a cell or as a form input. Always a THUNK, so an isomorphic entity `hub`
5
+ * never statically pulls UI into the server graph:
6
+ * eager (in .svelte): `() => Badge`
7
+ * lazy (server-safe): `() => import('./Badge.svelte')`
8
+ * The renderer resolves it once (cached) and unwraps a `{ default }` module.
9
+ */
10
+ export type CellComponent = () => Component | Promise<Component> | Promise<{
11
+ default: Component;
12
+ }>;
13
+ /** Per-field UI hints. width/margins are PERCENTAGES of the parent row. */
14
+ export interface CellUI {
15
+ /** Override the resolved input type ('text'|'number'|'date'|'checkbox'|'select'|'multiSelect'|...). */
16
+ inputType?: string;
17
+ width?: number;
18
+ marginLeft?: number;
19
+ marginRight?: number;
20
+ /** Independent geometry for screens <= 40rem. */
21
+ mobile?: {
22
+ width?: number;
23
+ marginLeft?: number;
24
+ marginRight?: number;
25
+ };
26
+ /** Column alignment in a grid. */
27
+ align?: 'left' | 'center' | 'right';
28
+ order?: number;
29
+ }
30
+ /** How a resolved cell renders. */
31
+ export type MetaKind = 'field' | 'field_link' | 'relation' | 'enum' | 'enum_multi' | 'slot' | 'component' | 'header' | 'spacer';
32
+ /** Resolved, headless cell descriptor consumed by FF_Grid / FF_Form. */
33
+ export interface Cell<E = any> {
34
+ col?: keyof E & string;
35
+ field?: FieldMetadata<unknown, E>;
36
+ kind: MetaKind;
37
+ caption: string;
38
+ ui: CellUI;
39
+ /** Resolved input type for edit (getInputType). */
40
+ inputType: string;
41
+ align: 'left' | 'center' | 'right';
42
+ /** Whether this column is sortable by header click (resolved; opt in via `sortable: true`). */
43
+ sortable: boolean;
44
+ /** Tailwind/CSS passthrough (e.g. 'col-span-2'). */
45
+ class?: string;
46
+ cellSnippet?: Snippet<[{
47
+ row: E;
48
+ cell: Cell<E>;
49
+ }]>;
50
+ /** Render a component for this cell. Static `props` + per-row `rowToProps()` are merged into it. */
51
+ component?: CellComponent;
52
+ props?: Record<string, unknown>;
53
+ rowToProps?: (row: E) => Record<string, unknown>;
54
+ }
55
+ /** Terse author input: a bare field key, or a config object, or '_spacer'. */
56
+ export type CellInput<E> = (keyof E & string) | {
57
+ col: (keyof E & string) | '_spacer';
58
+ kind?: MetaKind;
59
+ caption?: string;
60
+ ui?: CellUI;
61
+ align?: 'left' | 'center' | 'right';
62
+ class?: string;
63
+ /** Set true to make this column sortable by header click (default: not sortable). */
64
+ sortable?: boolean;
65
+ cellSnippet?: Snippet<[{
66
+ row: E;
67
+ cell: Cell<E>;
68
+ }]>;
69
+ /** Render a component for this cell (thunk). `props` + `rowToProps()` are merged into it. */
70
+ component?: CellComponent;
71
+ props?: Record<string, unknown>;
72
+ rowToProps?: (row: E) => Record<string, unknown>;
73
+ };
74
+ /** Geometry config for one of a cell's four sub-elements. */
75
+ export interface CellElementConfig {
76
+ width?: number;
77
+ order?: number;
78
+ align?: 'TopLeft' | 'TopCenter' | 'TopRight' | 'MiddleLeft' | 'MiddleCenter' | 'MiddleRight' | 'BottomLeft' | 'BottomCenter' | 'BottomRight';
79
+ class?: string;
80
+ style?: string;
81
+ }
82
+ /** App-level default geometry for the label/error/content/hint sub-elements. */
83
+ export interface CellConfig {
84
+ label?: CellElementConfig;
85
+ error?: CellElementConfig;
86
+ content?: CellElementConfig;
87
+ hint?: CellElementConfig;
88
+ }
89
+ declare module 'remult' {
90
+ interface FieldOptions<entityType, valueType> {
91
+ /** firstly cell UI hints (width %, inputType, mobile, align). */
92
+ ui?: CellUI;
93
+ /** Input placeholder. */
94
+ placeholder?: string;
95
+ /** For multiSelect value-lists: the element value-list class. */
96
+ valueTypeArray?: ClassType<valueType>;
97
+ /** When set, the field renders as a link (field_link kind). */
98
+ href?: (row: entityType) => string;
99
+ }
100
+ }
101
+ /** Per-action (create/edit/delete) config. Omit `cells` to inherit the list `cells`. */
102
+ export interface ActionConfig<E = any> {
103
+ /** Fields shown in this action's form. Omit = inherit the list `cells`. */
104
+ cells?: CellInput<E>[];
105
+ /** Dialog title (e.g. from the row being edited). */
106
+ title?: (row: E) => string;
107
+ /** Override the action's button icon (mdi path string). */
108
+ icon?: string;
109
+ /** Escape: render a custom dialog component instead of the generated form. */
110
+ component?: CellComponent;
111
+ props?: Record<string, unknown>;
112
+ rowToProps?: (row: E) => Record<string, unknown>;
113
+ }
114
+ /**
115
+ * Entity-level grid/form config — the SSoT. Declared on the entity (`@FF_Entity('x', { hub: {...} })`)
116
+ * and read by FF_Grid / a boutique App_Grid as DEFAULTS; every prop overrides it. Keep it a plain,
117
+ * cheap object: `cells` (strings), `where`, `orderBy`, `sortable`, action toggles + `title` fns are all
118
+ * server-safe. Any UI `component` must be a lazy {@link CellComponent} thunk so the isomorphic entity
119
+ * file never statically pulls Svelte into the server graph.
120
+ */
121
+ export interface HubConfig<E = any> {
122
+ /** Entity icon (mdi path string). */
123
+ icon?: string;
124
+ /** Grid columns + the default fields for the create/edit forms. */
125
+ cells?: CellInput<E>[];
126
+ /** Default for column sorting on this entity (a per-cell `sortable` overrides). Default true. */
127
+ defaultSortable?: boolean;
128
+ where?: EntityFilter<E>;
129
+ orderBy?: EntityOrderBy<E>;
130
+ strategy?: 'paginate' | 'listen' | 'load';
131
+ pageSize?: number;
132
+ /** Create action; `false` to disable. Omit = on, using the list `cells`. */
133
+ insert?: ActionConfig<E> | false;
134
+ /** Edit action; `false` to disable. */
135
+ update?: ActionConfig<E> | false;
136
+ /** Delete action; `false` to disable. */
137
+ delete?: ActionConfig<E> | false;
138
+ }
139
+ declare module 'remult' {
140
+ interface EntityOptions<entityType> {
141
+ /** firstly grid/form config — read as defaults by FF_Grid / App_Grid, overridable per call. */
142
+ hub?: HubConfig<entityType>;
143
+ }
144
+ }
145
+ export interface CellContentProps {
146
+ component?: Component;
147
+ componentReadonly?: Component;
148
+ props?: Record<string, unknown>;
149
+ children?: string | Component;
150
+ config?: CellElementConfig;
151
+ }
152
+ export interface CellElementProps {
153
+ html?: string;
154
+ config?: CellElementConfig;
155
+ }
156
+ /** Edit/readonly mode for a cell, group, or grid. Extensible — a future `'filter'` member (the cell
157
+ * renders a filter input instead of a record input) slots in here. */
158
+ export type CellMode = 'edit' | 'readonly';
159
+ export type CellProps = {
160
+ key?: string;
161
+ mode?: CellMode;
162
+ label?: CellElementProps;
163
+ error?: CellElementProps;
164
+ hint?: CellElementProps;
165
+ content?: CellContentProps;
166
+ value?: unknown;
167
+ ui?: CellUI;
168
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ export { buildCells, displayCell } from './buildCells.js';
2
+ export { resolveCellComponent } from './cellComponent.js';
3
+ export { getFieldMetaType } from './metaKind.js';
4
+ export { getStyle, getCellElementConfig, getInputType, defaultConfig } from './cellConfig.js';
5
+ export type { Cell, CellInput, CellComponent, CellUI, MetaKind, CellConfig, CellElementConfig, CellProps, CellContentProps, CellElementProps, CellMode, HubConfig, ActionConfig, } from './cellTypes.js';
6
+ export { default as FF_Cell } from './FF_Cell.svelte';
7
+ export { default as FF_CellValue } from './FF_CellValue.svelte';
8
+ export { default as GroupFields } from './GroupFields.svelte';
9
+ export { default as DefaultInput } from './DefaultInput.svelte';
10
+ export { default as FF_Grid } from './FF_Grid.svelte';
@@ -0,0 +1,12 @@
1
+ export { buildCells, displayCell } from './buildCells.js';
2
+ export { resolveCellComponent } from './cellComponent.js';
3
+ export { getFieldMetaType } from './metaKind.js';
4
+ export { getStyle, getCellElementConfig, getInputType, defaultConfig } from './cellConfig.js';
5
+ export { default as FF_Cell } from './FF_Cell.svelte';
6
+ export { default as FF_CellValue } from './FF_CellValue.svelte';
7
+ export { default as GroupFields } from './GroupFields.svelte';
8
+ export { default as DefaultInput } from './DefaultInput.svelte';
9
+ // FF_Grid = the batteries-included demo grid (default skin + input). For a fully-owned grid, copy the
10
+ // boutique App_Grid (src/boutique/grid) instead.
11
+ export { default as FF_Grid } from './FF_Grid.svelte';
12
+ // App_Grid / App_Group are the copy-own boutique shells (src/boutique/grid) — NOT published.
@@ -0,0 +1,27 @@
1
+ import type { FieldMetadata, Repository } from 'remult';
2
+ import type { BaseEnum } from '../../core/BaseEnum.js';
3
+ export type FieldMetaType = {
4
+ kind: 'relation';
5
+ subKind: 'reference' | 'toOne' | 'toMany';
6
+ repoTarget: Repository<unknown>;
7
+ field: FieldMetadata;
8
+ } | {
9
+ kind: 'enum';
10
+ subKind: 'single' | 'multi';
11
+ values: BaseEnum[];
12
+ field: FieldMetadata;
13
+ } | {
14
+ kind: 'primitive';
15
+ subKind: string;
16
+ field: FieldMetadata;
17
+ } | {
18
+ kind: 'slot';
19
+ subKind: 'unknown';
20
+ };
21
+ /**
22
+ * Derive a render "kind" from a remult FieldMetadata.
23
+ * Mirrors my-minion's old_ff getFieldMetaType (the renderer brain), minus the daisyUI bits.
24
+ * Note: value-lists are read off `options.valueConverter.values` directly - `getValueList(field)`
25
+ * throws "ValueType not yet initialized" here, so don't use it.
26
+ */
27
+ export declare function getFieldMetaType(field?: FieldMetadata, withHidden?: boolean): FieldMetaType;
@@ -0,0 +1,34 @@
1
+ import { getRelationFieldInfo } from 'remult/internals';
2
+ /**
3
+ * Derive a render "kind" from a remult FieldMetadata.
4
+ * Mirrors my-minion's old_ff getFieldMetaType (the renderer brain), minus the daisyUI bits.
5
+ * Note: value-lists are read off `options.valueConverter.values` directly - `getValueList(field)`
6
+ * throws "ValueType not yet initialized" here, so don't use it.
7
+ */
8
+ export function getFieldMetaType(field, withHidden = false) {
9
+ if (field === undefined)
10
+ return { kind: 'slot', subKind: 'unknown' };
11
+ const rel = getRelationFieldInfo(field);
12
+ if (rel) {
13
+ return {
14
+ kind: 'relation',
15
+ subKind: rel.type,
16
+ repoTarget: rel.toRepo,
17
+ field,
18
+ };
19
+ }
20
+ const opts = field.options;
21
+ if (opts?.inputType === 'selectArrayEnum') {
22
+ return { kind: 'enum', subKind: 'multi', values: opts.valueConverter?.values ?? [], field };
23
+ }
24
+ if (opts?.valueConverter?.values) {
25
+ const values = opts.valueConverter.values;
26
+ return {
27
+ kind: 'enum',
28
+ subKind: 'single',
29
+ values: withHidden ? values : values.filter((v) => !v.hide),
30
+ field,
31
+ };
32
+ }
33
+ return { kind: 'primitive', subKind: field.inputType ?? 'text', field };
34
+ }
@@ -1,3 +1,4 @@
1
+ export { errorMessage, isError } from '../core/helper.js';
1
2
  export { ff } from './ff.svelte.js';
2
3
  export type { FF_Many, FF_One, FF_Builder, FF_RepoOptions, FF_OneOptions, FF_RepoLoading, FF_Issue, ManyStrategy, AggregateOptions, QueryOptionsHelper, } from './ff.svelte.js';
3
4
  export { infiniteScroll } from './infiniteScroll.js';
@@ -15,7 +16,8 @@ export { default as FF_ToastManager } from './FF_ToastManager.svelte';
15
16
  export { SP } from './class/SP.svelte';
16
17
  export type { ParamDefinition } from './class/SP.svelte';
17
18
  export { initRemultSvelteReactivity } from './initRemultSvelteReactivity';
18
- export { default as DemoGrid } from './DemoGrid.svelte';
19
- export { default as DemoForm } from './DemoForm.svelte';
19
+ export { remultApiUniversalLoad } from './remultApiLoad.js';
20
+ export { FF_Cell, FF_CellValue, FF_Grid, GroupFields, DefaultInput, buildCells, displayCell, resolveCellComponent, getFieldMetaType, getInputType, getStyle, getCellElementConfig, defaultConfig, } from './grid/index.js';
21
+ export type { Cell, CellInput, CellComponent, CellUI, MetaKind, CellConfig, CellElementConfig, CellProps, CellContentProps, CellElementProps, CellMode, HubConfig, ActionConfig, } from './grid/index.js';
20
22
  export { default as Icon } from './ui/Icon.svelte';
21
23
  export { LibIcon_Empty, LibIcon_Forbidden, LibIcon_ChevronDown, LibIcon_ChevronUp, LibIcon_ChevronLeft, LibIcon_ChevronRight, LibIcon_Search, LibIcon_Check, LibIcon_MultiCheck, LibIcon_Add, LibIcon_MultiAdd, LibIcon_Edit, LibIcon_Eye, LibIcon_EyeOff, LibIcon_Delete, LibIcon_Cross, LibIcon_Save, LibIcon_Man, LibIcon_Woman, LibIcon_Send, LibIcon_Load, LibIcon_Settings, LibIcon_Sort, LibIcon_SortAsc, LibIcon_SortDesc, } from './ui/LibIcon.js';
@@ -1,3 +1,4 @@
1
+ export { errorMessage, isError } from '../core/helper.js';
1
2
  export { ff } from './ff.svelte.js';
2
3
  export { infiniteScroll } from './infiniteScroll.js';
3
4
  export { dialog, ffAutofocus, resolveMessage } from './dialog.svelte.js';
@@ -8,7 +9,8 @@ export { toast } from './toast.js';
8
9
  export { default as FF_ToastManager } from './FF_ToastManager.svelte';
9
10
  export { SP } from './class/SP.svelte';
10
11
  export { initRemultSvelteReactivity } from './initRemultSvelteReactivity';
11
- export { default as DemoGrid } from './DemoGrid.svelte';
12
- export { default as DemoForm } from './DemoForm.svelte';
12
+ export { remultApiUniversalLoad } from './remultApiLoad.js';
13
+ // Headless cell primitives (FF_Grid / FF_Group are boutique-only — copy from src/boutique/grid).
14
+ export { FF_Cell, FF_CellValue, FF_Grid, GroupFields, DefaultInput, buildCells, displayCell, resolveCellComponent, getFieldMetaType, getInputType, getStyle, getCellElementConfig, defaultConfig, } from './grid/index.js';
13
15
  export { default as Icon } from './ui/Icon.svelte';
14
16
  export { LibIcon_Empty, LibIcon_Forbidden, LibIcon_ChevronDown, LibIcon_ChevronUp, LibIcon_ChevronLeft, LibIcon_ChevronRight, LibIcon_Search, LibIcon_Check, LibIcon_MultiCheck, LibIcon_Add, LibIcon_MultiAdd, LibIcon_Edit, LibIcon_Eye, LibIcon_EyeOff, LibIcon_Delete, LibIcon_Cross, LibIcon_Save, LibIcon_Man, LibIcon_Woman, LibIcon_Send, LibIcon_Load, LibIcon_Settings, LibIcon_Sort, LibIcon_SortAsc, LibIcon_SortDesc, } from './ui/LibIcon.js';
@@ -0,0 +1,19 @@
1
+ import type { LoadEvent } from '@sveltejs/kit';
2
+ /**
3
+ * Wrap a SvelteKit UNIVERSAL load (`+page.ts` / `+layout.ts`) so plain global
4
+ * `repo()` / `ff()` reads through the API (so `allowApiRead` / `apiPrefilter`
5
+ * apply), on both SSR and CSR.
6
+ *
7
+ * - SSR: runs the body in a nested `withRemult` bound to `event.fetch` - gated, and
8
+ * scoped so a concurrent `+page.server.ts` keeps its own provider. SvelteKit
9
+ * inlines the response. `remult.user` is carried in so the body can read it.
10
+ * - CSR / hydration: points the client `remult` at this page's `event.fetch` so the
11
+ * inlined SSR response is reused, then runs the body.
12
+ *
13
+ * (A universal load runs on the client too, so it must use `event.fetch`. For a
14
+ * server-only load see `remultApiServerLoad` from `firstly/svelte/server`.)
15
+ *
16
+ * TODO(remult): the SSR branch is framework-agnostic (only needs `event.fetch`) -
17
+ * candidate to live in remult (e.g. `withRemultFetch(fetch, body)`).
18
+ */
19
+ export declare function remultApiUniversalLoad<T>(body: (event: LoadEvent) => Promise<T>): (event: LoadEvent) => Promise<T>;
@@ -0,0 +1,31 @@
1
+ import { isBackend, remult, RestDataProvider, withRemult } from 'remult';
2
+ /**
3
+ * Wrap a SvelteKit UNIVERSAL load (`+page.ts` / `+layout.ts`) so plain global
4
+ * `repo()` / `ff()` reads through the API (so `allowApiRead` / `apiPrefilter`
5
+ * apply), on both SSR and CSR.
6
+ *
7
+ * - SSR: runs the body in a nested `withRemult` bound to `event.fetch` - gated, and
8
+ * scoped so a concurrent `+page.server.ts` keeps its own provider. SvelteKit
9
+ * inlines the response. `remult.user` is carried in so the body can read it.
10
+ * - CSR / hydration: points the client `remult` at this page's `event.fetch` so the
11
+ * inlined SSR response is reused, then runs the body.
12
+ *
13
+ * (A universal load runs on the client too, so it must use `event.fetch`. For a
14
+ * server-only load see `remultApiServerLoad` from `firstly/svelte/server`.)
15
+ *
16
+ * TODO(remult): the SSR branch is framework-agnostic (only needs `event.fetch`) -
17
+ * candidate to live in remult (e.g. `withRemultFetch(fetch, body)`).
18
+ */
19
+ export function remultApiUniversalLoad(body) {
20
+ return (event) => {
21
+ if (!isBackend()) {
22
+ remult.apiClient.httpClient = event.fetch;
23
+ return body(event);
24
+ }
25
+ const user = remult.user;
26
+ return withRemult(async () => {
27
+ remult.user = user;
28
+ return body(event);
29
+ }, { dataProvider: new RestDataProvider(() => ({ httpClient: event.fetch })) });
30
+ };
31
+ }
@@ -0,0 +1,12 @@
1
+ import type { ServerLoadEvent } from '@sveltejs/kit';
2
+ /**
3
+ * Wrap a SvelteKit SERVER load (`+page.server.ts`) so plain global `repo()` reads
4
+ * apply the API gate (as the current user) instead of the privileged in-process
5
+ * provider. Use it only when a server load should see exactly what the API exposes;
6
+ * otherwise a server load keeps the DB provider (gate rows with `backendPrefilter`).
7
+ * Gates direct `repo()` reads; BackendMethods keep their own `allowed` gate.
8
+ *
9
+ * TODO(remult): framework-agnostic (event passed through; provider read from
10
+ * `remult`) - candidate to live in remult directly.
11
+ */
12
+ export declare function remultApiServerLoad<T>(body: (event: ServerLoadEvent) => Promise<T>): (event: ServerLoadEvent) => Promise<T>;
@@ -0,0 +1,27 @@
1
+ import { remult, withRemult } from 'remult';
2
+ import { TestApiDataProvider } from 'remult/server';
3
+ // SERVER-ONLY (imports `remult/server`). Never import from client-reachable code.
4
+ // Lazily wrap the app's dataProvider once. TestApiDataProvider dispatches repo()
5
+ // through the API pipeline in-process (so allowApiRead / apiPrefilter apply) and
6
+ // carries the current user - no event.fetch, no `/api` round-trip.
7
+ let apiDataProvider;
8
+ /**
9
+ * Wrap a SvelteKit SERVER load (`+page.server.ts`) so plain global `repo()` reads
10
+ * apply the API gate (as the current user) instead of the privileged in-process
11
+ * provider. Use it only when a server load should see exactly what the API exposes;
12
+ * otherwise a server load keeps the DB provider (gate rows with `backendPrefilter`).
13
+ * Gates direct `repo()` reads; BackendMethods keep their own `allowed` gate.
14
+ *
15
+ * TODO(remult): framework-agnostic (event passed through; provider read from
16
+ * `remult`) - candidate to live in remult directly.
17
+ */
18
+ export function remultApiServerLoad(body) {
19
+ return (event) => {
20
+ apiDataProvider ??= TestApiDataProvider({ dataProvider: remult.dataProvider });
21
+ const user = remult.user;
22
+ return withRemult(async () => {
23
+ remult.user = user;
24
+ return body(event);
25
+ }, { dataProvider: apiDataProvider });
26
+ };
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firstly",
3
- "version": "0.6.3",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "description": "Firstly, an opinionated Remult setup!",
6
6
  "funding": "https://github.com/sponsors/jycouet",
@@ -41,7 +41,7 @@
41
41
  "nodemailer": "8.0.5",
42
42
  "svelte-sonner": "1.1.1",
43
43
  "tailwind-merge": "3.6.0",
44
- "tailwindcss": "4.2.2",
44
+ "tailwindcss": "4.3.0",
45
45
  "vite-plugin-kit-routes": "1.0.6",
46
46
  "vite-plugin-stripper": "0.10.4"
47
47
  },
@@ -57,6 +57,10 @@
57
57
  "svelte": "./esm/svelte/index.js",
58
58
  "default": "./esm/svelte/index.js"
59
59
  },
60
+ "./svelte/server": {
61
+ "types": "./esm/svelte/server/index.d.ts",
62
+ "default": "./esm/svelte/server/index.js"
63
+ },
60
64
  "./vite": {
61
65
  "types": "./esm/vite/index.d.ts",
62
66
  "default": "./esm/vite/index.js"