@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.
@@ -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";