firstly 0.6.2 → 0.7.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/CHANGELOG.md +20 -0
- package/esm/changeLog/index.d.ts +1 -0
- package/esm/svelte/FF_Config.svelte +2 -2
- package/esm/svelte/FF_Config.svelte.js +3 -0
- package/esm/svelte/grid/DefaultInput.svelte +42 -0
- package/esm/svelte/grid/DefaultInput.svelte.d.ts +9 -0
- package/esm/svelte/grid/FF_Cell.svelte +98 -0
- package/esm/svelte/grid/FF_Cell.svelte.d.ts +10 -0
- package/esm/svelte/grid/FF_CellValue.svelte +24 -0
- package/esm/svelte/grid/FF_CellValue.svelte.d.ts +28 -0
- package/esm/svelte/grid/FF_Cell_Content.svelte +37 -0
- package/esm/svelte/grid/FF_Cell_Content.svelte.d.ts +10 -0
- package/esm/svelte/grid/FF_Cell_Error.svelte +26 -0
- package/esm/svelte/grid/FF_Cell_Error.svelte.d.ts +8 -0
- package/esm/svelte/grid/FF_Cell_Hint.svelte +26 -0
- package/esm/svelte/grid/FF_Cell_Hint.svelte.d.ts +8 -0
- package/esm/svelte/grid/FF_Cell_Label.svelte +34 -0
- package/esm/svelte/grid/FF_Cell_Label.svelte.d.ts +8 -0
- package/esm/svelte/grid/FF_Grid.svelte +385 -0
- package/esm/svelte/grid/FF_Grid.svelte.d.ts +47 -0
- package/esm/svelte/grid/GroupFields.svelte +141 -0
- package/esm/svelte/grid/GroupFields.svelte.d.ts +45 -0
- package/esm/svelte/grid/_test/FF_Cell_ContextWrapper.svelte +9 -0
- package/esm/svelte/grid/_test/FF_Cell_ContextWrapper.svelte.d.ts +18 -0
- package/esm/svelte/grid/buildCells.d.ts +16 -0
- package/esm/svelte/grid/buildCells.js +79 -0
- package/esm/svelte/grid/cellComponent.d.ts +9 -0
- package/esm/svelte/grid/cellComponent.js +31 -0
- package/esm/svelte/grid/cellConfig.d.ts +13 -0
- package/esm/svelte/grid/cellConfig.js +48 -0
- package/esm/svelte/grid/cellTypes.d.ts +168 -0
- package/esm/svelte/grid/cellTypes.js +1 -0
- package/esm/svelte/grid/index.d.ts +10 -0
- package/esm/svelte/grid/index.js +12 -0
- package/esm/svelte/grid/metaKind.d.ts +27 -0
- package/esm/svelte/grid/metaKind.js +34 -0
- package/esm/svelte/index.d.ts +3 -2
- package/esm/svelte/index.js +3 -2
- package/package.json +4 -3
- package/esm/svelte/DemoForm.svelte +0 -121
- package/esm/svelte/DemoForm.svelte.d.ts +0 -42
- package/esm/svelte/DemoGrid.svelte +0 -258
- 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
|
+
}
|
package/esm/svelte/index.d.ts
CHANGED
|
@@ -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,7 @@ 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 {
|
|
19
|
-
export {
|
|
19
|
+
export { FF_Cell, FF_CellValue, FF_Grid, GroupFields, DefaultInput, buildCells, displayCell, resolveCellComponent, getFieldMetaType, getInputType, getStyle, getCellElementConfig, defaultConfig, } from './grid/index.js';
|
|
20
|
+
export type { Cell, CellInput, CellComponent, CellUI, MetaKind, CellConfig, CellElementConfig, CellProps, CellContentProps, CellElementProps, CellMode, HubConfig, ActionConfig, } from './grid/index.js';
|
|
20
21
|
export { default as Icon } from './ui/Icon.svelte';
|
|
21
22
|
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';
|
package/esm/svelte/index.js
CHANGED
|
@@ -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,7 @@ 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
|
-
|
|
12
|
-
export {
|
|
12
|
+
// Headless cell primitives (FF_Grid / FF_Group are boutique-only — copy from src/boutique/grid).
|
|
13
|
+
export { FF_Cell, FF_CellValue, FF_Grid, GroupFields, DefaultInput, buildCells, displayCell, resolveCellComponent, getFieldMetaType, getInputType, getStyle, getCellElementConfig, defaultConfig, } from './grid/index.js';
|
|
13
14
|
export { default as Icon } from './ui/Icon.svelte';
|
|
14
15
|
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';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firstly",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Firstly, an opinionated Remult setup!",
|
|
6
6
|
"funding": "https://github.com/sponsors/jycouet",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@kitql/helpers": "0.8.15",
|
|
34
35
|
"@layerstack/utils": "1.0.0",
|
|
35
36
|
"@mdi/js": "7.4.47",
|
|
36
37
|
"@types/nodemailer": "8.0.0",
|
|
@@ -39,8 +40,8 @@
|
|
|
39
40
|
"esm-env": "1.2.2",
|
|
40
41
|
"nodemailer": "8.0.5",
|
|
41
42
|
"svelte-sonner": "1.1.1",
|
|
42
|
-
"tailwind-merge": "3.
|
|
43
|
-
"tailwindcss": "4.
|
|
43
|
+
"tailwind-merge": "3.6.0",
|
|
44
|
+
"tailwindcss": "4.3.0",
|
|
44
45
|
"vite-plugin-kit-routes": "1.0.6",
|
|
45
46
|
"vite-plugin-stripper": "0.10.4"
|
|
46
47
|
},
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
<script lang="ts" generics="T extends { id: string }">
|
|
2
|
-
import type { ClassType, EntityFilter } from 'remult'
|
|
3
|
-
|
|
4
|
-
import { ff } from './ff.svelte.js'
|
|
5
|
-
|
|
6
|
-
type Props = {
|
|
7
|
-
/** The remult entity class. */
|
|
8
|
-
entity: ClassType<T>
|
|
9
|
-
/** Fields to edit. Labels come from each field's `caption`. */
|
|
10
|
-
fields: (keyof T & string)[]
|
|
11
|
-
/** Which record (remult `EntityFilter`). Default: findFirst by `defaultOrderBy` (the latest). */
|
|
12
|
-
where?: EntityFilter<T>
|
|
13
|
-
}
|
|
14
|
-
let { entity, fields, where }: Props = $props()
|
|
15
|
-
|
|
16
|
-
// A single bound record: `where` (or findFirst by the entity's defaultOrderBy - the latest row).
|
|
17
|
-
const r = ff(entity).one(() => ({ where }))
|
|
18
|
-
|
|
19
|
-
const get = (row: T, f: keyof T & string) => (row as Record<string, unknown>)[f]
|
|
20
|
-
function setField(f: keyof T & string, value: string) {
|
|
21
|
-
if (r.item) (r.item as Record<string, unknown>)[f] = value
|
|
22
|
-
}
|
|
23
|
-
</script>
|
|
24
|
-
|
|
25
|
-
<div class="form">
|
|
26
|
-
{#if r.loading.init}
|
|
27
|
-
<p class="muted">Loading…</p>
|
|
28
|
-
{:else if r.item}
|
|
29
|
-
{#each fields as f (f)}
|
|
30
|
-
<label>
|
|
31
|
-
<span>{r.meta.fields.find(f)?.caption ?? f}</span>
|
|
32
|
-
<input
|
|
33
|
-
value={String(get(r.item, f) ?? '')}
|
|
34
|
-
oninput={(e) => setField(f, e.currentTarget.value)}
|
|
35
|
-
/>
|
|
36
|
-
</label>
|
|
37
|
-
{/each}
|
|
38
|
-
<div class="actions">
|
|
39
|
-
<button
|
|
40
|
-
disabled={r.isWriting ||
|
|
41
|
-
(r.item.id ? !r.meta.apiUpdateAllowed(r.item) : !r.meta.apiInsertAllowed(r.item))}
|
|
42
|
-
onclick={() => r.save()}
|
|
43
|
-
>
|
|
44
|
-
Save
|
|
45
|
-
</button>
|
|
46
|
-
{#if r.isWriting}<span class="busy">saving…</span>{/if}
|
|
47
|
-
</div>
|
|
48
|
-
{#if r.error}<p class="err">{r.error}</p>{/if}
|
|
49
|
-
{:else}
|
|
50
|
-
<p class="muted">No record yet.</p>
|
|
51
|
-
<button disabled={!r.meta.apiInsertAllowed()} onclick={() => r.create()}>+ New</button>
|
|
52
|
-
{/if}
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
<style>
|
|
56
|
-
.form {
|
|
57
|
-
display: flex;
|
|
58
|
-
flex-direction: column;
|
|
59
|
-
gap: 12px;
|
|
60
|
-
align-items: stretch;
|
|
61
|
-
font-size: 14px;
|
|
62
|
-
}
|
|
63
|
-
label {
|
|
64
|
-
display: flex;
|
|
65
|
-
flex-direction: column;
|
|
66
|
-
gap: 4px;
|
|
67
|
-
}
|
|
68
|
-
label span {
|
|
69
|
-
font-size: 12px;
|
|
70
|
-
opacity: 0.65;
|
|
71
|
-
}
|
|
72
|
-
input {
|
|
73
|
-
width: 100%;
|
|
74
|
-
box-sizing: border-box;
|
|
75
|
-
padding: 7px 10px;
|
|
76
|
-
font: inherit;
|
|
77
|
-
color: inherit;
|
|
78
|
-
background: transparent;
|
|
79
|
-
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
|
|
80
|
-
border-radius: 8px;
|
|
81
|
-
}
|
|
82
|
-
input:focus-visible {
|
|
83
|
-
outline: none;
|
|
84
|
-
border-color: color-mix(in srgb, currentColor 55%, transparent);
|
|
85
|
-
}
|
|
86
|
-
.actions {
|
|
87
|
-
display: flex;
|
|
88
|
-
gap: 10px;
|
|
89
|
-
align-items: center;
|
|
90
|
-
}
|
|
91
|
-
.busy {
|
|
92
|
-
color: #d97706;
|
|
93
|
-
font-weight: 600;
|
|
94
|
-
font-size: 12px;
|
|
95
|
-
}
|
|
96
|
-
.err {
|
|
97
|
-
color: #dc2626;
|
|
98
|
-
margin: 0;
|
|
99
|
-
font-size: 13px;
|
|
100
|
-
}
|
|
101
|
-
button {
|
|
102
|
-
cursor: pointer;
|
|
103
|
-
font: inherit;
|
|
104
|
-
padding: 6px 14px;
|
|
105
|
-
color: inherit;
|
|
106
|
-
background: color-mix(in srgb, currentColor 8%, transparent);
|
|
107
|
-
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
|
|
108
|
-
border-radius: 8px;
|
|
109
|
-
transition: background 0.14s ease;
|
|
110
|
-
}
|
|
111
|
-
button:hover {
|
|
112
|
-
background: color-mix(in srgb, currentColor 15%, transparent);
|
|
113
|
-
}
|
|
114
|
-
button:disabled {
|
|
115
|
-
opacity: 0.45;
|
|
116
|
-
cursor: not-allowed;
|
|
117
|
-
}
|
|
118
|
-
.muted {
|
|
119
|
-
opacity: 0.6;
|
|
120
|
-
}
|
|
121
|
-
</style>
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { ClassType, EntityFilter } from 'remult';
|
|
2
|
-
declare function $$render<T extends {
|
|
3
|
-
id: string;
|
|
4
|
-
}>(): {
|
|
5
|
-
props: {
|
|
6
|
-
/** The remult entity class. */
|
|
7
|
-
entity: ClassType<T>;
|
|
8
|
-
/** Fields to edit. Labels come from each field's `caption`. */
|
|
9
|
-
fields: (keyof T & string)[];
|
|
10
|
-
/** Which record (remult `EntityFilter`). Default: findFirst by `defaultOrderBy` (the latest). */
|
|
11
|
-
where?: EntityFilter<T>;
|
|
12
|
-
};
|
|
13
|
-
exports: {};
|
|
14
|
-
bindings: "";
|
|
15
|
-
slots: {};
|
|
16
|
-
events: {};
|
|
17
|
-
};
|
|
18
|
-
declare class __sveltets_Render<T extends {
|
|
19
|
-
id: string;
|
|
20
|
-
}> {
|
|
21
|
-
props(): ReturnType<typeof $$render<T>>['props'];
|
|
22
|
-
events(): ReturnType<typeof $$render<T>>['events'];
|
|
23
|
-
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
24
|
-
bindings(): "";
|
|
25
|
-
exports(): {};
|
|
26
|
-
}
|
|
27
|
-
interface $$IsomorphicComponent {
|
|
28
|
-
new <T extends {
|
|
29
|
-
id: string;
|
|
30
|
-
}>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
31
|
-
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
32
|
-
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
33
|
-
<T extends {
|
|
34
|
-
id: string;
|
|
35
|
-
}>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
36
|
-
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
37
|
-
}
|
|
38
|
-
declare const DemoForm: $$IsomorphicComponent;
|
|
39
|
-
type DemoForm<T extends {
|
|
40
|
-
id: string;
|
|
41
|
-
}> = InstanceType<typeof DemoForm<T>>;
|
|
42
|
-
export default DemoForm;
|