@kahitsan/ksui 0.31.1 → 0.33.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/package.json +1 -1
- package/src/components/composite/AccountRadioPicker.tsx +120 -0
- package/src/components/composite/FormAdvancedSection.tsx +366 -0
- package/src/components/composite/SalesBodyEditor.tsx +418 -0
- package/src/components/composite/TransactionForm.tsx +1053 -0
- package/src/components/composite/TransferAccountsPicker.tsx +291 -0
- package/src/components/composite/TransferFeeChip.tsx +34 -0
- package/src/components/composite/resource/form-spec.ts +141 -0
- package/src/components/composite/resource/route-adapter.ts +63 -0
- package/src/components/composite/resource/route-spec.test.ts +125 -0
- package/src/components/composite/resource/route-spec.ts +158 -0
- package/src/index.ts +37 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Pure builder layer for declarative resource routes. defineRoute() composes a
|
|
2
|
+
// frozen RouteSpec out of small helpers; the adapter (./route-adapter) folds it
|
|
3
|
+
// DOWN to the existing ResourceUiSpec, so ResourcePage stays the one render
|
|
4
|
+
// engine. No solid-js — this module is data only, like ./spec.ts.
|
|
5
|
+
import type { ColumnTone, UiColumnRender, UiFilter, UiDetailRow } from "./spec.js";
|
|
6
|
+
import type { FormSpec } from "./form-spec.js";
|
|
7
|
+
|
|
8
|
+
// ---- columns ---------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
/** A column descriptor; `render` is the EXISTING UiColumnRender union, so
|
|
11
|
+
* no new cell-rendering path is introduced. */
|
|
12
|
+
export interface RouteColumn {
|
|
13
|
+
readonly key: string;
|
|
14
|
+
readonly title: string;
|
|
15
|
+
readonly orderable?: boolean;
|
|
16
|
+
readonly render: UiColumnRender;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Cell.* helpers emit the existing UiColumnRender union — the title field is the
|
|
20
|
+
// column key, supplied at col() time, so these only carry the render shape.
|
|
21
|
+
export const Cell = {
|
|
22
|
+
Title: { type: "title" } as const satisfies UiColumnRender,
|
|
23
|
+
Enum: (labels: Readonly<Record<string, string>>): UiColumnRender => ({ type: "enum", labels }),
|
|
24
|
+
Status: (opts: {
|
|
25
|
+
active: { label: string; tone: ColumnTone };
|
|
26
|
+
inactive: { label: string; tone: ColumnTone };
|
|
27
|
+
}): UiColumnRender => ({ type: "status", active: opts.active, inactive: opts.inactive }),
|
|
28
|
+
Text: (opts?: { muted?: boolean }): UiColumnRender =>
|
|
29
|
+
opts?.muted === undefined ? { type: "text" } : { type: "text", muted: opts.muted },
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
/** Build a column. `orderable` is omitted (not set false) when not requested so
|
|
33
|
+
* the lowered spec matches a hand-authored one that left it out. */
|
|
34
|
+
export function col(
|
|
35
|
+
key: string,
|
|
36
|
+
opts: { title: string; orderable?: boolean; render: UiColumnRender },
|
|
37
|
+
): RouteColumn {
|
|
38
|
+
const c: RouteColumn =
|
|
39
|
+
opts.orderable === undefined
|
|
40
|
+
? { key, title: opts.title, render: opts.render }
|
|
41
|
+
: { key, title: opts.title, orderable: opts.orderable, render: opts.render };
|
|
42
|
+
return Object.freeze(c);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---- body views ------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/** A `table` body. `kind` keys a future body-renderer registry; today only
|
|
48
|
+
* `table` is wired (the adapter THROWS on anything else). `cards` is reserved
|
|
49
|
+
* in the union but the builder for it is intentionally absent so an unwired
|
|
50
|
+
* view is a tsc error at the call site, not a runtime hole. */
|
|
51
|
+
export interface TableView {
|
|
52
|
+
readonly kind: "table";
|
|
53
|
+
readonly columns: readonly RouteColumn[];
|
|
54
|
+
}
|
|
55
|
+
export interface CardsView {
|
|
56
|
+
readonly kind: "cards";
|
|
57
|
+
readonly item: unknown;
|
|
58
|
+
}
|
|
59
|
+
export type BodyView = TableView | CardsView;
|
|
60
|
+
|
|
61
|
+
/** The only wired body builder. */
|
|
62
|
+
export function table(opts: { columns: readonly RouteColumn[] }): TableView {
|
|
63
|
+
return Object.freeze({ kind: "table", columns: opts.columns });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---- header actions ---------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/** A header action. `flow` is a STRING id (never an inline fn) so a flow
|
|
69
|
+
* runtime can slot in behind a host-provided runner without recontracting. */
|
|
70
|
+
export interface RouteAction {
|
|
71
|
+
readonly id: string;
|
|
72
|
+
readonly label: string;
|
|
73
|
+
readonly icon?: string;
|
|
74
|
+
readonly variant?: string;
|
|
75
|
+
readonly flow: string;
|
|
76
|
+
}
|
|
77
|
+
export function action(
|
|
78
|
+
id: string,
|
|
79
|
+
opts: { label: string; icon?: string; variant?: string; flow: string },
|
|
80
|
+
): RouteAction {
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
id,
|
|
83
|
+
label: opts.label,
|
|
84
|
+
flow: opts.flow,
|
|
85
|
+
...(opts.icon ? { icon: opts.icon } : {}),
|
|
86
|
+
...(opts.variant ? { variant: opts.variant } : {}),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---- the route spec ---------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
export interface RoutePermissions {
|
|
93
|
+
readonly view: string;
|
|
94
|
+
readonly edit: readonly string[];
|
|
95
|
+
readonly delete: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Labels carried straight onto the lowered ResourceUiSpec.labels. */
|
|
99
|
+
export type RouteLabels = {
|
|
100
|
+
readonly add: string;
|
|
101
|
+
readonly createTitle: string;
|
|
102
|
+
readonly createSubmit: string;
|
|
103
|
+
readonly editTitle: string;
|
|
104
|
+
readonly editSubmit: string;
|
|
105
|
+
readonly titleField: string;
|
|
106
|
+
readonly searchPlaceholder: string;
|
|
107
|
+
readonly empty: string;
|
|
108
|
+
readonly noResults: string;
|
|
109
|
+
readonly createErrorFallback: string;
|
|
110
|
+
readonly updateErrorFallback: string;
|
|
111
|
+
readonly networkError: string;
|
|
112
|
+
readonly archiveTitle: string;
|
|
113
|
+
readonly archiveMessage: string;
|
|
114
|
+
readonly archiveConfirm: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** A route-load setting. Reserved for a host settings store; the adapter does
|
|
118
|
+
* not lower it (it is host-resolved later), so it is presentation metadata
|
|
119
|
+
* only for now. */
|
|
120
|
+
export interface SettingDecl {
|
|
121
|
+
readonly kind: "number";
|
|
122
|
+
readonly default: number;
|
|
123
|
+
}
|
|
124
|
+
export const setting = {
|
|
125
|
+
number: (opts: { default: number }): SettingDecl =>
|
|
126
|
+
Object.freeze({ kind: "number", default: opts.default }),
|
|
127
|
+
} as const;
|
|
128
|
+
|
|
129
|
+
export interface RouteSpec {
|
|
130
|
+
readonly path?: string;
|
|
131
|
+
readonly title: string;
|
|
132
|
+
readonly subtitle?: string;
|
|
133
|
+
readonly icon?: string;
|
|
134
|
+
readonly basePath: string;
|
|
135
|
+
readonly softDeleteField: string;
|
|
136
|
+
readonly testIdPrefix: string;
|
|
137
|
+
readonly permissions: RoutePermissions;
|
|
138
|
+
readonly header?: { readonly actions: readonly RouteAction[] };
|
|
139
|
+
readonly toolbar?: {
|
|
140
|
+
readonly search?: { readonly placeholder: string; readonly fields: readonly string[] };
|
|
141
|
+
readonly filters?: readonly UiFilter[];
|
|
142
|
+
};
|
|
143
|
+
readonly view:
|
|
144
|
+
| BodyView
|
|
145
|
+
| { readonly views: Readonly<Record<string, BodyView>>; readonly default: string };
|
|
146
|
+
readonly form?: FormSpec;
|
|
147
|
+
readonly detail: readonly UiDetailRow[];
|
|
148
|
+
readonly settings?: Readonly<Record<string, SettingDecl>>;
|
|
149
|
+
// Sticky-footer slot — reserved in the contract, not rendered yet (needs an
|
|
150
|
+
// additive page-shell `footer?` prop on the consumer side).
|
|
151
|
+
readonly footer?: unknown;
|
|
152
|
+
readonly labels: RouteLabels;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** defineRoute composes a frozen RouteSpec. Pure — no rendering, no side effects. */
|
|
156
|
+
export function defineRoute(cfg: RouteSpec): RouteSpec {
|
|
157
|
+
return Object.freeze(cfg);
|
|
158
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -109,6 +109,35 @@ export {
|
|
|
109
109
|
export { default as PaymentAccountPicker } from "./components/composite/PaymentAccountPicker";
|
|
110
110
|
export type { PaymentAccountOption } from "./components/composite/PaymentAccountPicker";
|
|
111
111
|
|
|
112
|
+
// AccountRadioPicker: a static grid/radiogroup of account buttons fed by an
|
|
113
|
+
// already-fetched accounts prop (vs. PaymentAccountPicker's self-fetching
|
|
114
|
+
// dropdown) -- the payment-source picker used inside TransactionForm.
|
|
115
|
+
export { default as AccountRadioPicker } from "./components/composite/AccountRadioPicker";
|
|
116
|
+
export type { AccountRadioPickerProps } from "./components/composite/AccountRadioPicker";
|
|
117
|
+
|
|
118
|
+
// TransactionForm: the full transaction create/edit form (category picker,
|
|
119
|
+
// SalesBodyEditor, amount/date/payee/attachments, advanced tax/EWT/sharing
|
|
120
|
+
// fields). `simpleMode` hides the Type picker + advanced toggle for a caller
|
|
121
|
+
// that locks `category` to one value. Lifted verbatim from kplugin_finance so
|
|
122
|
+
// any plugin recording a transaction against the shared /api/transactions
|
|
123
|
+
// endpoint can reuse the same form instead of forking it.
|
|
124
|
+
export { default as TransactionForm } from "./components/composite/TransactionForm";
|
|
125
|
+
export type {
|
|
126
|
+
TransactionFormProps,
|
|
127
|
+
TransactionAccount,
|
|
128
|
+
TransactionOrgMember,
|
|
129
|
+
TransactionShareableRole,
|
|
130
|
+
TransactionAttachment,
|
|
131
|
+
} from "./components/composite/TransactionForm";
|
|
132
|
+
export { default as FormAdvancedSection } from "./components/composite/FormAdvancedSection";
|
|
133
|
+
export type { FormAdvancedSectionProps } from "./components/composite/FormAdvancedSection";
|
|
134
|
+
export { default as SalesBodyEditor } from "./components/composite/SalesBodyEditor";
|
|
135
|
+
export type { SalesLine, SalesBodyEditorProps } from "./components/composite/SalesBodyEditor";
|
|
136
|
+
export { default as TransferFeeChip } from "./components/composite/TransferFeeChip";
|
|
137
|
+
export type { TransferFeeChipProps } from "./components/composite/TransferFeeChip";
|
|
138
|
+
export { default as TransferAccountsPicker } from "./components/composite/TransferAccountsPicker";
|
|
139
|
+
export type { TransferAccountsPickerProps } from "./components/composite/TransferAccountsPicker";
|
|
140
|
+
|
|
112
141
|
// LiveTimer wraps ProgressBar with timer state and elapsed-time display.
|
|
113
142
|
export { default as LiveTimer, type LiveTimerProps } from "./components/composite/LiveTimer";
|
|
114
143
|
|
|
@@ -162,6 +191,14 @@ export type {
|
|
|
162
191
|
} from "./components/composite/resource/ResourcePage";
|
|
163
192
|
export * from "./components/composite/resource/spec";
|
|
164
193
|
|
|
194
|
+
// Builder layer over ResourceUiSpec: defineRoute/defineForm compose a frozen
|
|
195
|
+
// RouteSpec/FormSpec out of small helpers (col, Cell.*, table, action, field.*,
|
|
196
|
+
// setting.*), and routeToResourceSpec lowers it to the exact ResourceUiSpec a
|
|
197
|
+
// hand-authored spec would produce — ResourcePage stays the one render engine.
|
|
198
|
+
export * from "./components/composite/resource/route-spec";
|
|
199
|
+
export * from "./components/composite/resource/form-spec";
|
|
200
|
+
export { routeToResourceSpec } from "./components/composite/resource/route-adapter";
|
|
201
|
+
|
|
165
202
|
// U6 — declarative file/media field for the spec-driven form runtime. Value is an
|
|
166
203
|
// opaque asset handle; host injects onUpload + presignUrl (storage-agnostic).
|
|
167
204
|
export { default as FileField, type FileFieldProps, type AssetHandle, type FileFieldStatus } from "./components/composite/FileField";
|