@kahitsan/ksui 0.32.0 → 0.34.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 +4 -1
- package/src/components/base/AccountAvatar.tsx +1 -1
- package/src/components/base/AddAttachmentTile.tsx +4 -4
- package/src/components/base/BadgeSelect.tsx +10 -10
- package/src/components/base/Button.tsx +70 -38
- package/src/components/base/CameraCapture.tsx +8 -8
- package/src/components/base/ChartLegend.tsx +3 -3
- package/src/components/base/CopyButton.tsx +1 -1
- package/src/components/base/DataTable.tsx +68 -72
- package/src/components/base/DatePicker.tsx +77 -82
- package/src/components/base/DateTile.tsx +7 -7
- package/src/components/base/DetailRow.tsx +2 -2
- package/src/components/base/Dropdown.tsx +15 -15
- package/src/components/base/ExistingAttachmentTile.tsx +6 -6
- package/src/components/base/EyebrowBadge.tsx +15 -15
- package/src/components/base/FormErrorBanner.tsx +1 -1
- package/src/components/base/FormField.tsx +2 -2
- package/src/components/base/ImageCropper.tsx +19 -19
- package/src/components/base/ImageViewer.tsx +7 -11
- package/src/components/base/KpiCard.tsx +7 -7
- package/src/components/base/Modal.tsx +10 -14
- package/src/components/base/ProgressBar.tsx +63 -59
- package/src/components/base/RadioCardGroup.tsx +13 -6
- package/src/components/base/SectionHeading.tsx +4 -4
- package/src/components/base/SegmentedFilter.tsx +60 -4
- package/src/components/base/SocialLinksBar.tsx +3 -15
- package/src/components/base/StatusIndicator.tsx +11 -11
- package/src/components/base/StatusPill.tsx +15 -15
- package/src/components/base/TagPill.tsx +1 -1
- package/src/components/base/ThemeToggle.tsx +12 -20
- package/src/components/base/Tooltip.tsx +2 -2
- package/src/components/composite/AccountRadioPicker.tsx +8 -4
- package/src/components/composite/ComboBox.tsx +51 -44
- package/src/components/composite/CustomRenderer.tsx +6 -13
- package/src/components/composite/FileField.tsx +10 -17
- package/src/components/composite/FlowGraph.tsx +29 -36
- package/src/components/composite/FlowRunner.tsx +11 -18
- package/src/components/composite/FormAdvancedSection.tsx +31 -31
- package/src/components/composite/LiveTimer.tsx +3 -3
- package/src/components/composite/MarkdownNotes.tsx +11 -11
- package/src/components/composite/MentionTextarea.tsx +10 -10
- package/src/components/composite/NotFound.tsx +7 -7
- package/src/components/composite/PaymentAccountPicker.tsx +19 -19
- package/src/components/composite/SalesBodyEditor.tsx +26 -26
- package/src/components/composite/SearchableSelect.tsx +14 -14
- package/src/components/composite/SecretReveal.tsx +5 -5
- package/src/components/composite/TransactionAccountFields.tsx +78 -0
- package/src/components/composite/TransactionForm.tsx +83 -169
- package/src/components/composite/TransactionPayableFields.tsx +97 -0
- package/src/components/composite/TransferAccountsPicker.tsx +20 -20
- package/src/components/composite/TransferFeeChip.tsx +2 -2
- package/src/components/composite/VoucherPicker.tsx +25 -25
- package/src/components/composite/resource/ResourceForm.tsx +1 -1
- package/src/components/composite/resource/ResourcePage.tsx +8 -8
- package/src/components/composite/resource/cells.tsx +6 -3
- 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 +8 -0
- package/src/utils/INPUT_CLASS.ts +6 -3
- package/src/utils/account-icons.ts +29 -7
- package/src/utils/highlight.tsx +5 -6
- package/src/utils/inject-css.ts +13 -0
- package/tailwind.js +11 -7
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Unit tests for the RouteSpec builder layer + its lowering. They pin the
|
|
2
|
+
// contract that a built RouteSpec lowers to the EXACT ResourceUiSpec a
|
|
3
|
+
// hand-authored spec would produce, and that unrepresentable config fails
|
|
4
|
+
// loudly instead of being silently dropped.
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { Cell, action, col, defineRoute, setting, table } from "./route-spec";
|
|
7
|
+
import { defineForm, field, fieldToUiField } from "./form-spec";
|
|
8
|
+
import { routeToResourceSpec } from "./route-adapter";
|
|
9
|
+
import type { ResourceUiSpec } from "./spec";
|
|
10
|
+
|
|
11
|
+
const LABELS = {
|
|
12
|
+
add: "Add Vendor",
|
|
13
|
+
createTitle: "New Vendor",
|
|
14
|
+
createSubmit: "Create",
|
|
15
|
+
editTitle: "Edit Vendor",
|
|
16
|
+
editSubmit: "Save",
|
|
17
|
+
titleField: "name",
|
|
18
|
+
searchPlaceholder: "Search vendors",
|
|
19
|
+
empty: "No vendors yet.",
|
|
20
|
+
noResults: "No vendors match.",
|
|
21
|
+
createErrorFallback: "Could not create the vendor.",
|
|
22
|
+
updateErrorFallback: "Could not update the vendor.",
|
|
23
|
+
networkError: "Network error — try again.",
|
|
24
|
+
archiveTitle: "Archive vendor?",
|
|
25
|
+
archiveMessage: "It can be restored later.",
|
|
26
|
+
archiveConfirm: "Archive",
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
function vendorRoute() {
|
|
30
|
+
return defineRoute({
|
|
31
|
+
title: "Vendors",
|
|
32
|
+
basePath: "/api/vendors",
|
|
33
|
+
softDeleteField: "is_active",
|
|
34
|
+
testIdPrefix: "vendor",
|
|
35
|
+
permissions: { view: "vendors.view", edit: ["vendors.edit"], delete: "vendors.delete" },
|
|
36
|
+
header: {
|
|
37
|
+
actions: [action("add", { label: "Add Vendor", flow: "vendors.create" })],
|
|
38
|
+
},
|
|
39
|
+
view: table({
|
|
40
|
+
columns: [
|
|
41
|
+
col("name", { title: "Name", orderable: true, render: Cell.Title }),
|
|
42
|
+
col("kind", { title: "Kind", render: Cell.Enum({ a: "A", b: "B" }) }),
|
|
43
|
+
col("notes", { title: "Notes", render: Cell.Text({ muted: true }) }),
|
|
44
|
+
],
|
|
45
|
+
}),
|
|
46
|
+
form: defineForm({
|
|
47
|
+
fields: {
|
|
48
|
+
name: field.text({ label: "Name", required: true, transform: "trim" }),
|
|
49
|
+
notes: field.textarea({ label: "Notes", transform: "trimOrNull", rows: 3 }),
|
|
50
|
+
kind: field.select({
|
|
51
|
+
label: "Kind",
|
|
52
|
+
default: "a",
|
|
53
|
+
options: [
|
|
54
|
+
{ value: "a", label: "A" },
|
|
55
|
+
{ value: "b", label: "B" },
|
|
56
|
+
],
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
submit: { create: "vendors.create", update: "vendors.update" },
|
|
60
|
+
}),
|
|
61
|
+
detail: [{ label: "Name", value: { type: "field", key: "name" } }],
|
|
62
|
+
settings: { pageSize: setting.number({ default: 25 }) },
|
|
63
|
+
labels: LABELS,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe("routeToResourceSpec lowering", () => {
|
|
68
|
+
it("lowers a built route to the hand-authored ResourceUiSpec shape", () => {
|
|
69
|
+
const lowered = routeToResourceSpec(vendorRoute());
|
|
70
|
+
const hand: ResourceUiSpec = {
|
|
71
|
+
basePath: "/api/vendors",
|
|
72
|
+
title: "Vendors",
|
|
73
|
+
permissions: { view: "vendors.view", edit: ["vendors.edit"], delete: "vendors.delete" },
|
|
74
|
+
softDeleteField: "is_active",
|
|
75
|
+
testIdPrefix: "vendor",
|
|
76
|
+
columns: [
|
|
77
|
+
{ key: "name", title: "Name", orderable: true, render: { type: "title" } },
|
|
78
|
+
{ key: "kind", title: "Kind", render: { type: "enum", labels: { a: "A", b: "B" } } },
|
|
79
|
+
{ key: "notes", title: "Notes", render: { type: "text", muted: true } },
|
|
80
|
+
],
|
|
81
|
+
fields: [
|
|
82
|
+
{ key: "name", label: "Name", type: "text", required: true, transform: "trim" },
|
|
83
|
+
{ key: "notes", label: "Notes", type: "textarea", transform: "trimOrNull", rows: 3 },
|
|
84
|
+
{
|
|
85
|
+
key: "kind",
|
|
86
|
+
label: "Kind",
|
|
87
|
+
type: "select",
|
|
88
|
+
default: "a",
|
|
89
|
+
options: [
|
|
90
|
+
{ value: "a", label: "A" },
|
|
91
|
+
{ value: "b", label: "B" },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
detail: [{ label: "Name", value: { type: "field", key: "name" } }],
|
|
96
|
+
labels: LABELS,
|
|
97
|
+
};
|
|
98
|
+
expect(lowered).toEqual(hand);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("omits `orderable` (does not emit false) when col() left it out", () => {
|
|
102
|
+
const lowered = routeToResourceSpec(vendorRoute());
|
|
103
|
+
expect("orderable" in lowered.columns[1]).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("throws on a cards body (unwired view)", () => {
|
|
107
|
+
const route = defineRoute({
|
|
108
|
+
...vendorRoute(),
|
|
109
|
+
view: { kind: "cards", item: {} },
|
|
110
|
+
});
|
|
111
|
+
expect(() => routeToResourceSpec(route)).toThrow(/not wired/);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("throws on a multi-view route", () => {
|
|
115
|
+
const route = defineRoute({
|
|
116
|
+
...vendorRoute(),
|
|
117
|
+
view: { views: { t: table({ columns: [] }) }, default: "t" },
|
|
118
|
+
});
|
|
119
|
+
expect(() => routeToResourceSpec(route)).toThrow(/multi-view/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("throws on an unwired field kind", () => {
|
|
123
|
+
expect(() => fieldToUiField("due", { kind: "date", label: "Due" })).toThrow(/unwired kind/);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -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
|
@@ -191,6 +191,14 @@ export type {
|
|
|
191
191
|
} from "./components/composite/resource/ResourcePage";
|
|
192
192
|
export * from "./components/composite/resource/spec";
|
|
193
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
|
+
|
|
194
202
|
// U6 — declarative file/media field for the spec-driven form runtime. Value is an
|
|
195
203
|
// opaque asset handle; host injects onUpload + presignUrl (storage-agnostic).
|
|
196
204
|
export { default as FileField, type FileFieldProps, type AssetHandle, type FileFieldStatus } from "./components/composite/FileField";
|
package/src/utils/INPUT_CLASS.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Shared Tailwind class string for text inputs across plugin forms: a
|
|
2
|
-
// full-width rounded input with the
|
|
2
|
+
// full-width rounded input with the themed input surface and the amber focus
|
|
3
3
|
// ring. Copied verbatim from the packages remote UI atoms so the shared kit
|
|
4
4
|
// owns the single source of truth. Pure string constant, no dependencies.
|
|
5
|
-
|
|
5
|
+
//
|
|
6
|
+
// bg reads --ks-input-bg (not --ks-border, THEME-SPEC §1.2's dedicated input
|
|
7
|
+
// surface token) — the border token happens to be a plausible dark literal so
|
|
8
|
+
// this silently looked right in the `dark` theme and only broke elsewhere.
|
|
6
9
|
export const INPUT_CLASS =
|
|
7
|
-
"w-full rounded-lg border border-
|
|
10
|
+
"w-full rounded-lg border border-[var(--ks-input-border,#3f3f46)] bg-[var(--ks-input-bg,#18181b)] px-3 py-2 text-sm text-[var(--ks-fg,#ffffff)] focus:border-[color-mix(in_srgb,var(--ks-focus-ring,#c9a961)_50%,transparent)] focus:outline-none";
|
|
@@ -74,17 +74,39 @@ export function getAccountIcon(account: { icon?: string | null; type: string }):
|
|
|
74
74
|
return DEFAULT_BY_TYPE[account.type] ?? Banknote;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// bank has no exact-hue token (blue-400/500 family isn't in §1.2), so it
|
|
78
|
+
// drifts to the nearest semantic bucket (--ks-info, sky). e_wallet/capital's
|
|
79
|
+
// bg (amber-500) and border (amber-400) are each an EXACT hex match to
|
|
80
|
+
// --ks-warning and --ks-accent respectively, so those keep zero-regression
|
|
81
|
+
// color-mix rather than sharing one base. Same for cash: bg is an exact
|
|
82
|
+
// --ks-success match, border an exact --ks-success-fg match.
|
|
77
83
|
const DEFAULT_TONE_BY_TYPE: Record<string, { text: string; bg: string; border: string }> = {
|
|
78
|
-
bank: {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
bank: {
|
|
85
|
+
text: "text-[var(--ks-info,#38bdf8)]",
|
|
86
|
+
bg: "bg-[color-mix(in_srgb,var(--ks-info,#38bdf8)_10%,transparent)]",
|
|
87
|
+
border: "border-[color-mix(in_srgb,var(--ks-info,#38bdf8)_40%,transparent)]",
|
|
88
|
+
},
|
|
89
|
+
e_wallet: {
|
|
90
|
+
text: "text-[var(--ks-accent,#fbbf24)]",
|
|
91
|
+
bg: "bg-[color-mix(in_srgb,var(--ks-warning,#f59e0b)_10%,transparent)]",
|
|
92
|
+
border: "border-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_40%,transparent)]",
|
|
93
|
+
},
|
|
94
|
+
cash: {
|
|
95
|
+
text: "text-[var(--ks-success-fg,#34d399)]",
|
|
96
|
+
bg: "bg-[color-mix(in_srgb,var(--ks-success,#10b981)_10%,transparent)]",
|
|
97
|
+
border: "border-[color-mix(in_srgb,var(--ks-success-fg,#34d399)_40%,transparent)]",
|
|
98
|
+
},
|
|
99
|
+
capital: {
|
|
100
|
+
text: "text-[var(--ks-accent,#fbbf24)]",
|
|
101
|
+
bg: "bg-[color-mix(in_srgb,var(--ks-warning,#f59e0b)_10%,transparent)]",
|
|
102
|
+
border: "border-[color-mix(in_srgb,var(--ks-accent,#fbbf24)_40%,transparent)]",
|
|
103
|
+
},
|
|
82
104
|
};
|
|
83
105
|
|
|
84
106
|
const FALLBACK_TONE = {
|
|
85
|
-
text: "text-
|
|
86
|
-
bg: "bg-
|
|
87
|
-
border: "border-
|
|
107
|
+
text: "text-[var(--ks-fg-muted,#a1a1aa)]",
|
|
108
|
+
bg: "bg-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_30%,transparent)]",
|
|
109
|
+
border: "border-[color-mix(in_srgb,var(--ks-border-strong,#3f3f46)_60%,transparent)]",
|
|
88
110
|
};
|
|
89
111
|
|
|
90
112
|
// Per-type accent tone for an account chip, or the account's own custom color.
|
package/src/utils/highlight.tsx
CHANGED
|
@@ -9,16 +9,15 @@
|
|
|
9
9
|
// still pass their own `markClass` to override.
|
|
10
10
|
|
|
11
11
|
import { type JSX } from "solid-js";
|
|
12
|
+
import { injectCSS } from "./inject-css";
|
|
12
13
|
|
|
13
14
|
const MARK_STYLE_ID = "ksui-mark-style";
|
|
14
15
|
|
|
15
16
|
function ensureMarkStyle(): void {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
style.textContent = `.ksui-mark{background-color:rgba(245,158,11,0.3);color:inherit;border-radius:2px;}`;
|
|
21
|
-
document.head.appendChild(style);
|
|
17
|
+
injectCSS(
|
|
18
|
+
MARK_STYLE_ID,
|
|
19
|
+
`.ksui-mark{background-color:color-mix(in srgb, var(--ks-warning, #f59e0b) 30%, transparent);color:inherit;border-radius:2px;}`,
|
|
20
|
+
);
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
/** Case-insensitive substring test. Empty query matches everything. */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Shared dedup so a component's CSS-in-JS string is appended to <head> once
|
|
2
|
+
// per id, no matter how many instances mount — replaces the copy-pasted
|
|
3
|
+
// createElement('style') pattern across ~14 components.
|
|
4
|
+
const injected = new Set<string>();
|
|
5
|
+
|
|
6
|
+
export function injectCSS(id: string, css: string): void {
|
|
7
|
+
if (injected.has(id) || typeof document === "undefined") return;
|
|
8
|
+
injected.add(id);
|
|
9
|
+
const style = document.createElement("style");
|
|
10
|
+
style.id = id;
|
|
11
|
+
style.textContent = css;
|
|
12
|
+
document.head.appendChild(style);
|
|
13
|
+
}
|
package/tailwind.js
CHANGED
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
/** @type {import('tailwindcss').Plugin} */
|
|
20
20
|
export default function ksuiTailwindPlugin({ addUtilities, addComponents }) {
|
|
21
21
|
// ─── Button intents ────────────────────────────────────────────────
|
|
22
|
+
// Amber/red/slate intent palette mirrors Button.tsx's own documented
|
|
23
|
+
// exception (component-authored tone system, not a 1:1 §1.2 surface/
|
|
24
|
+
// text/border mapping) — none of these rgba alpha values byte-match a
|
|
25
|
+
// §1.2 dark default, so left literal rather than guessed.
|
|
22
26
|
// Primary (amber)
|
|
23
27
|
addUtilities({
|
|
24
28
|
".ks-btn-primary": {
|
|
@@ -134,7 +138,7 @@ export default function ksuiTailwindPlugin({ addUtilities, addComponents }) {
|
|
|
134
138
|
"align-items": "center",
|
|
135
139
|
gap: "0.5rem",
|
|
136
140
|
padding: "0.375rem 0.75rem",
|
|
137
|
-
"background-color": "rgb(24 24 27)",
|
|
141
|
+
"background-color": "var(--ks-input-bg, rgb(24 24 27))",
|
|
138
142
|
border: "1px solid rgb(63 63 70 / 0.6)",
|
|
139
143
|
"border-radius": "0.25rem",
|
|
140
144
|
color: "rgb(228 228 231)",
|
|
@@ -152,7 +156,7 @@ export default function ksuiTailwindPlugin({ addUtilities, addComponents }) {
|
|
|
152
156
|
padding: "0.5rem 1rem",
|
|
153
157
|
"text-align": "left",
|
|
154
158
|
"font-weight": 500,
|
|
155
|
-
color: "rgb(161 161 170)",
|
|
159
|
+
color: "var(--ks-fg-muted, rgb(161 161 170))",
|
|
156
160
|
"font-size": "0.75rem",
|
|
157
161
|
"text-transform": "uppercase",
|
|
158
162
|
"letter-spacing": "0.05em",
|
|
@@ -168,7 +172,7 @@ export default function ksuiTailwindPlugin({ addUtilities, addComponents }) {
|
|
|
168
172
|
".ks-dt-empty": {
|
|
169
173
|
padding: "2rem",
|
|
170
174
|
"text-align": "center",
|
|
171
|
-
color: "rgb(113 113 122)",
|
|
175
|
+
color: "var(--ks-fg-subtle, rgb(113 113 122))",
|
|
172
176
|
},
|
|
173
177
|
".ks-dt-pager": {
|
|
174
178
|
display: "flex",
|
|
@@ -177,7 +181,7 @@ export default function ksuiTailwindPlugin({ addUtilities, addComponents }) {
|
|
|
177
181
|
padding: "0.75rem 1rem",
|
|
178
182
|
"border-top": "1px solid rgb(39 39 42 / 0.6)",
|
|
179
183
|
"font-size": "0.8125rem",
|
|
180
|
-
color: "rgb(161 161 170)",
|
|
184
|
+
color: "var(--ks-fg-muted, rgb(161 161 170))",
|
|
181
185
|
},
|
|
182
186
|
});
|
|
183
187
|
|
|
@@ -194,7 +198,7 @@ export default function ksuiTailwindPlugin({ addUtilities, addComponents }) {
|
|
|
194
198
|
padding: "1rem",
|
|
195
199
|
},
|
|
196
200
|
".ks-modal-card": {
|
|
197
|
-
"background-color": "rgb(24 24 27)",
|
|
201
|
+
"background-color": "var(--ks-overlay-surface, rgb(24 24 27))",
|
|
198
202
|
border: "1px solid rgb(63 63 70 / 0.6)",
|
|
199
203
|
"max-height": "calc(100vh - 2rem)",
|
|
200
204
|
overflow: "auto",
|
|
@@ -246,7 +250,7 @@ export default function ksuiTailwindPlugin({ addUtilities, addComponents }) {
|
|
|
246
250
|
"background-color": "rgba(59 130 246 / 0.1)",
|
|
247
251
|
},
|
|
248
252
|
".ks-pill-neutral": {
|
|
249
|
-
color: "rgb(161 161 170)",
|
|
253
|
+
color: "var(--ks-fg-muted, rgb(161 161 170))",
|
|
250
254
|
"background-color": "rgb(63 63 70 / 0.3)",
|
|
251
255
|
},
|
|
252
256
|
});
|
|
@@ -259,7 +263,7 @@ export default function ksuiTailwindPlugin({ addUtilities, addComponents }) {
|
|
|
259
263
|
gap: "0.25rem",
|
|
260
264
|
padding: "0.25rem 0.5rem",
|
|
261
265
|
"font-size": "0.75rem",
|
|
262
|
-
color: "rgb(161 161 170)",
|
|
266
|
+
color: "var(--ks-fg-muted, rgb(161 161 170))",
|
|
263
267
|
"background-color": "transparent",
|
|
264
268
|
border: "1px solid rgb(63 63 70 / 0.6)",
|
|
265
269
|
"border-radius": "0.25rem",
|