rastack 0.0.25 → 0.0.27
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 +4 -0
- package/components/auto-form/AutoForm.tsx +254 -0
- package/components/auto-form/fields.ts +108 -0
- package/components/auto-form/use-auto-form.ts +89 -0
- package/components/data-table/DataTable.tsx +267 -0
- package/components/data-table/columns.ts +117 -0
- package/components/data-table/use-data-table.ts +141 -0
- package/components/index.ts +77 -0
- package/components/primitives/controls.tsx +291 -0
- package/components/primitives/field.tsx +117 -0
- package/components/primitives/index.ts +11 -0
- package/components/primitives/misc.tsx +173 -0
- package/components/primitives/table.tsx +147 -0
- package/components/style.ts +195 -0
- package/components/types.ts +98 -0
- package/components/util.ts +14 -0
- package/dist/rastack-wasm-build.d.ts +1 -1
- package/dist/rastack-wasm-build.js +3 -3
- package/hooks/form/form.ts +10 -4
- package/hooks/form/interfaces.ts +40 -1
- package/hooks/form/structure.ts +83 -26
- package/hooks/query/delete.ts +3 -1
- package/package.json +1 -1
- package/src/rastack-wasm-build.ts +3 -3
- package/test/components.spec.ts +386 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { tokens, mergeStyles } from "../style";
|
|
3
|
+
import type { CellAlign } from "../util";
|
|
4
|
+
|
|
5
|
+
export type { CellAlign };
|
|
6
|
+
|
|
7
|
+
export interface TableProps extends React.TableHTMLAttributes<HTMLTableElement> {}
|
|
8
|
+
|
|
9
|
+
/** A token-styled `<table>`. Compose with the head/body/row/cell primitives, or drive it with `<DataTable>`. */
|
|
10
|
+
export function Table({
|
|
11
|
+
style,
|
|
12
|
+
children,
|
|
13
|
+
...rest
|
|
14
|
+
}: TableProps): React.JSX.Element {
|
|
15
|
+
const base: React.CSSProperties = {
|
|
16
|
+
width: "100%",
|
|
17
|
+
borderCollapse: "collapse",
|
|
18
|
+
fontFamily: tokens.font.sans,
|
|
19
|
+
fontSize: "0.875rem",
|
|
20
|
+
color: tokens.color.text,
|
|
21
|
+
textAlign: "left",
|
|
22
|
+
};
|
|
23
|
+
return (
|
|
24
|
+
<table style={mergeStyles(base, style)} {...rest}>
|
|
25
|
+
{children}
|
|
26
|
+
</table>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function THead({
|
|
31
|
+
style,
|
|
32
|
+
children,
|
|
33
|
+
...rest
|
|
34
|
+
}: React.HTMLAttributes<HTMLTableSectionElement>): React.JSX.Element {
|
|
35
|
+
return (
|
|
36
|
+
<thead style={style} {...rest}>
|
|
37
|
+
{children}
|
|
38
|
+
</thead>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function TBody({
|
|
43
|
+
style,
|
|
44
|
+
children,
|
|
45
|
+
...rest
|
|
46
|
+
}: React.HTMLAttributes<HTMLTableSectionElement>): React.JSX.Element {
|
|
47
|
+
return (
|
|
48
|
+
<tbody style={style} {...rest}>
|
|
49
|
+
{children}
|
|
50
|
+
</tbody>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface TrProps extends React.HTMLAttributes<HTMLTableRowElement> {}
|
|
55
|
+
|
|
56
|
+
export function Tr({ style, children, ...rest }: TrProps): React.JSX.Element {
|
|
57
|
+
return (
|
|
58
|
+
<tr style={style} {...rest}>
|
|
59
|
+
{children}
|
|
60
|
+
</tr>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ThProps extends React.ThHTMLAttributes<HTMLTableCellElement> {
|
|
65
|
+
align?: CellAlign;
|
|
66
|
+
sortable?: boolean;
|
|
67
|
+
/** The current sort on this column, for the indicator. */
|
|
68
|
+
sortDirection?: "asc" | "desc";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A header cell. When `sortable`, shows a direction indicator and a pointer cursor. */
|
|
72
|
+
export function Th({
|
|
73
|
+
align = "left",
|
|
74
|
+
sortable = false,
|
|
75
|
+
sortDirection,
|
|
76
|
+
style,
|
|
77
|
+
children,
|
|
78
|
+
...rest
|
|
79
|
+
}: ThProps): React.JSX.Element {
|
|
80
|
+
const base: React.CSSProperties = {
|
|
81
|
+
padding: `${tokens.space.sm} ${tokens.space.md}`,
|
|
82
|
+
textAlign: align,
|
|
83
|
+
fontSize: "0.75rem",
|
|
84
|
+
fontWeight: tokens.font.weightMedium as unknown as number,
|
|
85
|
+
letterSpacing: "0.02em",
|
|
86
|
+
textTransform: "uppercase",
|
|
87
|
+
color: tokens.color.textMuted,
|
|
88
|
+
borderBottom: `1px solid ${tokens.color.border}`,
|
|
89
|
+
background: tokens.color.surfaceAlt,
|
|
90
|
+
whiteSpace: "nowrap",
|
|
91
|
+
cursor: sortable ? "pointer" : undefined,
|
|
92
|
+
userSelect: sortable ? "none" : undefined,
|
|
93
|
+
};
|
|
94
|
+
const indicator = sortable
|
|
95
|
+
? sortDirection === "asc"
|
|
96
|
+
? " ▲"
|
|
97
|
+
: sortDirection === "desc"
|
|
98
|
+
? " ▼"
|
|
99
|
+
: " ↕"
|
|
100
|
+
: null;
|
|
101
|
+
return (
|
|
102
|
+
<th
|
|
103
|
+
style={mergeStyles(base, style)}
|
|
104
|
+
aria-sort={
|
|
105
|
+
sortDirection === "asc"
|
|
106
|
+
? "ascending"
|
|
107
|
+
: sortDirection === "desc"
|
|
108
|
+
? "descending"
|
|
109
|
+
: undefined
|
|
110
|
+
}
|
|
111
|
+
{...rest}
|
|
112
|
+
>
|
|
113
|
+
{children}
|
|
114
|
+
{indicator ? (
|
|
115
|
+
<span
|
|
116
|
+
style={{ opacity: sortDirection ? 0.9 : 0.35, fontSize: "0.7em" }}
|
|
117
|
+
>
|
|
118
|
+
{indicator}
|
|
119
|
+
</span>
|
|
120
|
+
) : null}
|
|
121
|
+
</th>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface TdProps extends React.TdHTMLAttributes<HTMLTableCellElement> {
|
|
126
|
+
align?: CellAlign;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function Td({
|
|
130
|
+
align = "left",
|
|
131
|
+
style,
|
|
132
|
+
children,
|
|
133
|
+
...rest
|
|
134
|
+
}: TdProps): React.JSX.Element {
|
|
135
|
+
const base: React.CSSProperties = {
|
|
136
|
+
padding: `${tokens.space.sm} ${tokens.space.md}`,
|
|
137
|
+
textAlign: align,
|
|
138
|
+
borderBottom: `1px solid ${tokens.color.border}`,
|
|
139
|
+
color: tokens.color.text,
|
|
140
|
+
verticalAlign: "middle",
|
|
141
|
+
};
|
|
142
|
+
return (
|
|
143
|
+
<td style={mergeStyles(base, style)} {...rest}>
|
|
144
|
+
{children}
|
|
145
|
+
</td>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `rastack/components` — the styling primitives.
|
|
3
|
+
*
|
|
4
|
+
* Every component in this package is painted **entirely from design tokens**:
|
|
5
|
+
* there is not a single hard-coded colour or dimension in the render tree, only
|
|
6
|
+
* `var(--…)` references produced here. That is what makes the kit "use the
|
|
7
|
+
* design system tokens as inputs" — drop the components inside a
|
|
8
|
+
* `<ThemeProvider>` (or link a `rastack tokens`-generated `tokens.css`) and they
|
|
9
|
+
* inherit the app's brand, spacing, radius and light/dark modes for free, with
|
|
10
|
+
* mode switches recascading through CSS with no re-render (exactly like
|
|
11
|
+
* `rastack/theme`).
|
|
12
|
+
*
|
|
13
|
+
* This module is pure (no React, no DOM), so the token-name mapping is unit
|
|
14
|
+
* tested the same way the token engine is.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type React from "react";
|
|
18
|
+
import { cssVarName } from "rastack/tokens";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A design-token path (`"color.surface"`, `"space.md"`, `"radius.lg"`) rendered
|
|
22
|
+
* as a CSS custom-property reference. Reuses the token engine's `cssVarName`, so
|
|
23
|
+
* the variable names are guaranteed identical to what `rastack tokens` emits and
|
|
24
|
+
* `<ThemeProvider>` injects.
|
|
25
|
+
*
|
|
26
|
+
* An optional `fallback` becomes the second argument of `var()`, so a component
|
|
27
|
+
* still renders sensibly when a token set omits an optional token (e.g. a
|
|
28
|
+
* hand-authored theme without `color.danger`) or before any theme is wired up.
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* cssVar("color.surface") // "var(--color-surface)"
|
|
32
|
+
* cssVar("color.danger", "#E5484D") // "var(--color-danger, #E5484D)"
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function cssVar(path: string, fallback?: string): string {
|
|
36
|
+
const name = cssVarName(path);
|
|
37
|
+
return fallback ? `var(${name}, ${fallback})` : `var(${name})`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The semantic tokens the kit paints from, each already a `var(--…)` string.
|
|
42
|
+
* Colours carry a literal fallback that mirrors `defineTheme`'s defaults, so the
|
|
43
|
+
* components degrade gracefully with partial token sets and are legible even
|
|
44
|
+
* with no `<ThemeProvider>` at all — while a real theme still wins.
|
|
45
|
+
*/
|
|
46
|
+
export const tokens = {
|
|
47
|
+
color: {
|
|
48
|
+
bg: cssVar("color.bg", "#ffffff"),
|
|
49
|
+
surface: cssVar("color.surface", "#ffffff"),
|
|
50
|
+
surfaceAlt: cssVar("color.surface-alt", "#f2f5fb"),
|
|
51
|
+
border: cssVar("color.border", "#e6eaf2"),
|
|
52
|
+
text: cssVar("color.text", "#0b1020"),
|
|
53
|
+
textMuted: cssVar("color.text-muted", "#4b5670"),
|
|
54
|
+
accent: cssVar("color.accent", "#4f6bff"),
|
|
55
|
+
accentStrong: cssVar("color.accent-strong", "#3b54e8"),
|
|
56
|
+
onAccent: cssVar("color.on-accent", "#ffffff"),
|
|
57
|
+
success: cssVar("color.success", "#1fa971"),
|
|
58
|
+
danger: cssVar("color.danger", "#e5484d"),
|
|
59
|
+
warning: cssVar("color.warning", "#f5a623"),
|
|
60
|
+
},
|
|
61
|
+
space: {
|
|
62
|
+
xs: cssVar("space.xs", "4px"),
|
|
63
|
+
sm: cssVar("space.sm", "8px"),
|
|
64
|
+
md: cssVar("space.md", "16px"),
|
|
65
|
+
lg: cssVar("space.lg", "24px"),
|
|
66
|
+
xl: cssVar("space.xl", "40px"),
|
|
67
|
+
},
|
|
68
|
+
radius: {
|
|
69
|
+
sm: cssVar("radius.sm", "6px"),
|
|
70
|
+
md: cssVar("radius.md", "10px"),
|
|
71
|
+
lg: cssVar("radius.lg", "16px"),
|
|
72
|
+
xl: cssVar("radius.xl", "24px"),
|
|
73
|
+
pill: cssVar("radius.pill", "999px"),
|
|
74
|
+
},
|
|
75
|
+
font: {
|
|
76
|
+
sans: cssVar("font.family.sans", "system-ui, sans-serif"),
|
|
77
|
+
mono: cssVar("font.family.mono", "ui-monospace, monospace"),
|
|
78
|
+
weightRegular: cssVar("font.weight.regular", "400"),
|
|
79
|
+
weightMedium: cssVar("font.weight.medium", "500"),
|
|
80
|
+
weightBold: cssVar("font.weight.bold", "700"),
|
|
81
|
+
},
|
|
82
|
+
shadow: {
|
|
83
|
+
sm: cssVar("shadow.sm", "0 1px 2px rgba(11,16,32,0.08)"),
|
|
84
|
+
md: cssVar(
|
|
85
|
+
"shadow.md",
|
|
86
|
+
"0 2px 4px rgba(11,16,32,0.12), 0 8px 24px rgba(11,16,32,0.10)",
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
duration: {
|
|
90
|
+
fast: cssVar("duration.fast", "150ms"),
|
|
91
|
+
base: cssVar("duration.base", "200ms"),
|
|
92
|
+
},
|
|
93
|
+
} as const;
|
|
94
|
+
|
|
95
|
+
/** Shallow-merge a base style with a caller override (override wins, `undefined` ignored). */
|
|
96
|
+
export function mergeStyles(
|
|
97
|
+
base: React.CSSProperties,
|
|
98
|
+
override?: React.CSSProperties,
|
|
99
|
+
): React.CSSProperties {
|
|
100
|
+
return override ? { ...base, ...override } : base;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// --- shared control tokens (kept in one place so inputs/selects/buttons align) --
|
|
104
|
+
|
|
105
|
+
/** Sizes shared by the interactive controls. */
|
|
106
|
+
export type ControlSize = "sm" | "md" | "lg";
|
|
107
|
+
|
|
108
|
+
/** Per-size padding / font-size / height, in token units. */
|
|
109
|
+
export const controlSizing: Record<
|
|
110
|
+
ControlSize,
|
|
111
|
+
{ paddingY: string; paddingX: string; fontSize: string; height: string }
|
|
112
|
+
> = {
|
|
113
|
+
sm: {
|
|
114
|
+
paddingY: tokens.space.xs,
|
|
115
|
+
paddingX: tokens.space.sm,
|
|
116
|
+
fontSize: "0.8125rem",
|
|
117
|
+
height: "2rem",
|
|
118
|
+
},
|
|
119
|
+
md: {
|
|
120
|
+
paddingY: tokens.space.sm,
|
|
121
|
+
paddingX: tokens.space.md,
|
|
122
|
+
fontSize: "0.875rem",
|
|
123
|
+
height: "2.5rem",
|
|
124
|
+
},
|
|
125
|
+
lg: {
|
|
126
|
+
paddingY: tokens.space.sm,
|
|
127
|
+
paddingX: tokens.space.lg,
|
|
128
|
+
fontSize: "1rem",
|
|
129
|
+
height: "3rem",
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** The base look every text-like control shares (input, textarea, select). */
|
|
134
|
+
export function controlBaseStyle(size: ControlSize): React.CSSProperties {
|
|
135
|
+
const s = controlSizing[size];
|
|
136
|
+
return {
|
|
137
|
+
display: "block",
|
|
138
|
+
width: "100%",
|
|
139
|
+
boxSizing: "border-box",
|
|
140
|
+
padding: `${s.paddingY} ${s.paddingX}`,
|
|
141
|
+
fontFamily: tokens.font.sans,
|
|
142
|
+
fontSize: s.fontSize,
|
|
143
|
+
color: tokens.color.text,
|
|
144
|
+
background: tokens.color.surface,
|
|
145
|
+
border: `1px solid ${tokens.color.border}`,
|
|
146
|
+
borderRadius: tokens.radius.md,
|
|
147
|
+
outline: "none",
|
|
148
|
+
transition: `border-color ${tokens.duration.fast} ease, box-shadow ${tokens.duration.fast} ease`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The focus ring shared by controls, as inline style to spread on focus. */
|
|
153
|
+
export const focusRingStyle: React.CSSProperties = {
|
|
154
|
+
borderColor: tokens.color.accent,
|
|
155
|
+
boxShadow: `0 0 0 3px ${cssVar("color.accent", "#4f6bff")}33`,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** Style applied to a control flagged invalid. */
|
|
159
|
+
export const invalidControlStyle: React.CSSProperties = {
|
|
160
|
+
borderColor: tokens.color.danger,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// --- button variants ----------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
export type ButtonVariant =
|
|
166
|
+
"primary" | "secondary" | "outline" | "ghost" | "danger";
|
|
167
|
+
|
|
168
|
+
/** Colour treatment per button variant (structure/sizing handled by the component). */
|
|
169
|
+
export const buttonVariantStyle: Record<ButtonVariant, React.CSSProperties> = {
|
|
170
|
+
primary: {
|
|
171
|
+
background: tokens.color.accent,
|
|
172
|
+
color: tokens.color.onAccent,
|
|
173
|
+
border: "1px solid transparent",
|
|
174
|
+
},
|
|
175
|
+
secondary: {
|
|
176
|
+
background: tokens.color.surfaceAlt,
|
|
177
|
+
color: tokens.color.text,
|
|
178
|
+
border: `1px solid ${tokens.color.border}`,
|
|
179
|
+
},
|
|
180
|
+
outline: {
|
|
181
|
+
background: "transparent",
|
|
182
|
+
color: tokens.color.text,
|
|
183
|
+
border: `1px solid ${tokens.color.border}`,
|
|
184
|
+
},
|
|
185
|
+
ghost: {
|
|
186
|
+
background: "transparent",
|
|
187
|
+
color: tokens.color.text,
|
|
188
|
+
border: "1px solid transparent",
|
|
189
|
+
},
|
|
190
|
+
danger: {
|
|
191
|
+
background: tokens.color.danger,
|
|
192
|
+
color: "#ffffff",
|
|
193
|
+
border: "1px solid transparent",
|
|
194
|
+
},
|
|
195
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural contracts the data-connected components accept.
|
|
3
|
+
*
|
|
4
|
+
* The framework has **two** hook families (see `docs/local-sync-engine.md`):
|
|
5
|
+
* the REST list hook returns a rich `IListQuery` (`data` + `header` +
|
|
6
|
+
* `pagination`/`sorting`/`searching`/`filtering` + the react-query flags), while
|
|
7
|
+
* a `sync: { mode: "local" }` entity returns only `{ data, isLoading, isReady }`.
|
|
8
|
+
* Rather than couple to either, the components accept the **structural
|
|
9
|
+
* lowest-common-denominator** below and *feature-detect* the richer parts — a
|
|
10
|
+
* `<DataTable>` renders a pager only when `pagination` is present, sortable
|
|
11
|
+
* headers only when `sorting` is present, and so on. Both hook families satisfy
|
|
12
|
+
* `TableQuery`, so the same `<DataTable query={…} />` works with either.
|
|
13
|
+
*
|
|
14
|
+
* These are `import type`-only re-exports of the real hook interfaces, so this
|
|
15
|
+
* file adds no runtime dependency and the shapes never drift from the hooks.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
IPaginationActions,
|
|
20
|
+
ISearchingActions,
|
|
21
|
+
ISortingActions,
|
|
22
|
+
IFilterActions,
|
|
23
|
+
ReturnedHeader,
|
|
24
|
+
} from "../hooks/query/interfaces";
|
|
25
|
+
import type { FormFieldStructure } from "../hooks/form/interfaces";
|
|
26
|
+
|
|
27
|
+
export type { ReturnedHeader } from "../hooks/query/interfaces";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Anything a `<DataTable>` / `useDataTable` can drive. The only required members
|
|
31
|
+
* are `data` (the rows) and, in practice, a loading flag; everything else is
|
|
32
|
+
* optional and lit up when present.
|
|
33
|
+
*/
|
|
34
|
+
export interface TableQuery<TRow = Record<string, unknown>> {
|
|
35
|
+
/** The rows. `IListQuery.data` (`.results`) or a sync list's `data`. */
|
|
36
|
+
data: TRow[] | undefined;
|
|
37
|
+
/** react-query's flag (REST) — absent on sync hooks, treated as `false`. */
|
|
38
|
+
isLoading?: boolean;
|
|
39
|
+
/** react-query's flag (REST). */
|
|
40
|
+
isError?: boolean;
|
|
41
|
+
/** react-query's error (REST). */
|
|
42
|
+
error?: unknown;
|
|
43
|
+
/** Column descriptors from the server envelope, when the API supplies them. */
|
|
44
|
+
header?: ReturnedHeader[];
|
|
45
|
+
/** Present on REST list hooks — enables the pager. */
|
|
46
|
+
pagination?: IPaginationActions;
|
|
47
|
+
/** Present on REST list hooks — enables the search box. */
|
|
48
|
+
searching?: ISearchingActions<Record<string, unknown>>;
|
|
49
|
+
/** Present on REST list hooks — enables sortable headers. */
|
|
50
|
+
sorting?: ISortingActions<Record<string, unknown>>;
|
|
51
|
+
/** Present on REST list hooks. */
|
|
52
|
+
filtering?: IFilterActions<Record<string, unknown>>;
|
|
53
|
+
/** Sync hooks expose readiness instead of react-query's `isLoading`. */
|
|
54
|
+
isReady?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A single field as `useForm().getFormField(name)` returns it, with `value`
|
|
59
|
+
* widened to `unknown` and `setValue` to `(value: any)`. `FormFieldStructure<T>`
|
|
60
|
+
* is invariant in `T` (it both reads `value: T` and writes `setValue(v: T)`), so
|
|
61
|
+
* these two overrides are what let a `FormFieldStructure<SomeField>` assign here
|
|
62
|
+
* regardless of the field's concrete type.
|
|
63
|
+
*/
|
|
64
|
+
export type AutoFormField = Omit<
|
|
65
|
+
FormFieldStructure<unknown>,
|
|
66
|
+
"value" | "setValue"
|
|
67
|
+
> & {
|
|
68
|
+
value: unknown;
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
setValue: (value: any) => void;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Anything an `<AutoForm>` / `useAutoForm` can drive — the subset of
|
|
75
|
+
* `useForm()`'s `FormStructure` the auto-form reads. `getFormField`/`onSubmit`/
|
|
76
|
+
* `resetForm` are declared as **methods** (bivariant params) so the generic
|
|
77
|
+
* `FormStructure` assigns without a cast.
|
|
78
|
+
*/
|
|
79
|
+
export interface AutoFormLike {
|
|
80
|
+
/**
|
|
81
|
+
* Current values; their keys are the default field list/order. Typed as
|
|
82
|
+
* `object` (not `Record<string, unknown>`) so a `useForm<Interface>()` result —
|
|
83
|
+
* whose values are a plain interface with no index signature — assigns cleanly.
|
|
84
|
+
*/
|
|
85
|
+
formValues: object | undefined;
|
|
86
|
+
/** Per-field structure, including the (enriched) datatype used to pick a control. */
|
|
87
|
+
getFormField(name: string): AutoFormField;
|
|
88
|
+
/** Validate + fire the wired mutation. */
|
|
89
|
+
onSubmit(): void;
|
|
90
|
+
/** Reset back to the initial values. */
|
|
91
|
+
resetForm(): void;
|
|
92
|
+
/** Whether submission is currently blocked (invalid). */
|
|
93
|
+
isSubmitDisabled: boolean;
|
|
94
|
+
/** Whether a submit mutation is in flight. */
|
|
95
|
+
isSubmitting: boolean;
|
|
96
|
+
/** Formik's error bag, keyed by field name. */
|
|
97
|
+
errors?: Record<string, unknown> | unknown;
|
|
98
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Small pure helpers + shared types across the components. React- and DOM-free. */
|
|
2
|
+
|
|
3
|
+
/** Text alignment shared by table cells and (potentially) other layout. */
|
|
4
|
+
export type CellAlign = "left" | "center" | "right";
|
|
5
|
+
|
|
6
|
+
/** `"first_name"` / `"firstName"` / `"created-at"` → `"First name"`. */
|
|
7
|
+
export function humanizeKey(key: string): string {
|
|
8
|
+
const spaced = key
|
|
9
|
+
.replace(/[_\-.]+/g, " ")
|
|
10
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
11
|
+
.trim();
|
|
12
|
+
if (!spaced) return key;
|
|
13
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
|
|
14
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* `rastack wasm` — compile the Rust API to WebAssembly for in-browser local dev.
|
|
4
4
|
*
|
|
5
5
|
* Pipeline:
|
|
6
|
-
* 1. cargo build -p
|
|
6
|
+
* 1. cargo build -p rastack-wasm --release --target wasm32-unknown-unknown
|
|
7
7
|
* 2. wasm-bindgen --target web → stack/wasm/rastack_wasm.js + rastack_wasm_bg.wasm
|
|
8
8
|
* 3. wasm-opt -Oz (if available) to shrink the binary
|
|
9
9
|
*
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `rastack wasm` — compile the Rust API to WebAssembly for in-browser local dev.
|
|
5
5
|
*
|
|
6
6
|
* Pipeline:
|
|
7
|
-
* 1. cargo build -p
|
|
7
|
+
* 1. cargo build -p rastack-wasm --release --target wasm32-unknown-unknown
|
|
8
8
|
* 2. wasm-bindgen --target web → stack/wasm/rastack_wasm.js + rastack_wasm_bg.wasm
|
|
9
9
|
* 3. wasm-opt -Oz (if available) to shrink the binary
|
|
10
10
|
*
|
|
@@ -49,11 +49,11 @@ function has(cmd) {
|
|
|
49
49
|
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
50
50
|
return (0, child_process_1.spawnSync)(cmd, ["--version"], { stdio: "ignore" }).status === 0;
|
|
51
51
|
}
|
|
52
|
-
console.log("→ cargo build -p
|
|
52
|
+
console.log("→ cargo build -p rastack-wasm --release (wasm32-unknown-unknown)");
|
|
53
53
|
run("cargo", [
|
|
54
54
|
"build",
|
|
55
55
|
"-p",
|
|
56
|
-
"
|
|
56
|
+
"rastack-wasm",
|
|
57
57
|
"--release",
|
|
58
58
|
"--target",
|
|
59
59
|
"wasm32-unknown-unknown",
|
package/hooks/form/form.ts
CHANGED
|
@@ -160,10 +160,11 @@ export function useForm<
|
|
|
160
160
|
const { setValue: setFormValue, setTouched } =
|
|
161
161
|
formik.getFieldHelpers(fieldName);
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
const { type, isRequired, maxLength, options, relation, nullable, format } =
|
|
164
|
+
getFieldStructure(
|
|
165
|
+
mutations ? mutations[updateMethod]?.validation : undefined,
|
|
166
|
+
fieldName,
|
|
167
|
+
);
|
|
167
168
|
|
|
168
169
|
const error = errorMessage ?? "";
|
|
169
170
|
const isError = errorMessage !== undefined;
|
|
@@ -189,6 +190,11 @@ export function useForm<
|
|
|
189
190
|
isRequired,
|
|
190
191
|
setValue,
|
|
191
192
|
setTouched,
|
|
193
|
+
maxLength,
|
|
194
|
+
options,
|
|
195
|
+
relation,
|
|
196
|
+
nullable,
|
|
197
|
+
format,
|
|
192
198
|
};
|
|
193
199
|
};
|
|
194
200
|
|
package/hooks/form/interfaces.ts
CHANGED
|
@@ -3,6 +3,18 @@ import { SchemaObject } from "ajv";
|
|
|
3
3
|
import { IUpdateQueryResult } from "../../hooks";
|
|
4
4
|
import { FormikValues } from "formik";
|
|
5
5
|
|
|
6
|
+
/** A choice for an `enum`-backed (`select`) field. */
|
|
7
|
+
export interface FieldOption {
|
|
8
|
+
label: string;
|
|
9
|
+
value: string | number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** The `{ app, model }` a foreign-key field points at (`x-rastack-relation`). */
|
|
13
|
+
export interface RelationTarget {
|
|
14
|
+
app: string;
|
|
15
|
+
model: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
export interface FormFieldStructure<TValue> {
|
|
7
19
|
value: TValue;
|
|
8
20
|
error: string;
|
|
@@ -13,6 +25,16 @@ export interface FormFieldStructure<TValue> {
|
|
|
13
25
|
isRequired: boolean;
|
|
14
26
|
setValue: (value: TValue) => void;
|
|
15
27
|
setTouched: (isTouched: boolean) => void;
|
|
28
|
+
/** Max string length, when the schema constrains it. Drives a character counter. */
|
|
29
|
+
maxLength?: number;
|
|
30
|
+
/** Choices for a `select` field, derived from the schema's `enum`. */
|
|
31
|
+
options?: FieldOption[];
|
|
32
|
+
/** The FK target, when this field is a foreign key. Lets a caller render a relation picker. */
|
|
33
|
+
relation?: RelationTarget;
|
|
34
|
+
/** Whether the schema allows `null`. */
|
|
35
|
+
nullable?: boolean;
|
|
36
|
+
/** The schema `format` (`date-time`, `email`, `uuid`, …), for finer control choices. */
|
|
37
|
+
format?: string;
|
|
16
38
|
}
|
|
17
39
|
|
|
18
40
|
interface FormMutation<
|
|
@@ -118,7 +140,24 @@ export interface FormStructure<TFormValues, TResponse> {
|
|
|
118
140
|
) => FormFieldStructure<FlattenedRecord<TFormValues>[TFieldName]>;
|
|
119
141
|
}
|
|
120
142
|
|
|
121
|
-
|
|
143
|
+
/**
|
|
144
|
+
* The control kind a field maps to. Derived from the write JSON Schema by
|
|
145
|
+
* {@link getFieldStructure}: the base scalar types (`text`/`number`/`array`)
|
|
146
|
+
* plus the richer distinctions a form UI needs — `boolean` (checkbox), `date`
|
|
147
|
+
* / `datetime` (date pickers), `select` (an `enum`), and the typed-text formats
|
|
148
|
+
* `email` / `url`. The list is a superset of the original three, so existing
|
|
149
|
+
* `type === "number"` / `"text"` / `"array"` checks keep working.
|
|
150
|
+
*/
|
|
151
|
+
export type FormType =
|
|
152
|
+
| "text"
|
|
153
|
+
| "number"
|
|
154
|
+
| "array"
|
|
155
|
+
| "boolean"
|
|
156
|
+
| "date"
|
|
157
|
+
| "datetime"
|
|
158
|
+
| "select"
|
|
159
|
+
| "email"
|
|
160
|
+
| "url";
|
|
122
161
|
|
|
123
162
|
export interface SetupUpdateEndpointReturn<TParams, TPayload, TResult> {
|
|
124
163
|
mutation: IUpdateQueryResult<TParams, TPayload, TResult>;
|