devnonla-ui 0.1.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/LICENSE +21 -0
- package/README.md +140 -0
- package/package.json +66 -0
- package/src/alert/Alert.tsx +109 -0
- package/src/app/App.tsx +36 -0
- package/src/button/Button.tsx +278 -0
- package/src/button/ButtonCopy.tsx +60 -0
- package/src/calendar/Calendar.tsx +133 -0
- package/src/chat/AgentPanel.tsx +203 -0
- package/src/chat/common/bgTasks.ts +41 -0
- package/src/chat/common/sse.ts +156 -0
- package/src/chat/common/types.ts +43 -0
- package/src/chat/common/useAgentStream.ts +351 -0
- package/src/chat/common/utils.ts +73 -0
- package/src/chat/index.ts +56 -0
- package/src/chat/message-ui/ChatAgentMessage.tsx +29 -0
- package/src/chat/message-ui/ChatError.tsx +24 -0
- package/src/chat/message-ui/ChatInput.tsx +162 -0
- package/src/chat/message-ui/ChatMarkdown.tsx +128 -0
- package/src/chat/message-ui/ChatMarkdownTable.tsx +45 -0
- package/src/chat/message-ui/ChatSpinner.tsx +5 -0
- package/src/chat/message-ui/ChatThinking.tsx +61 -0
- package/src/chat/message-ui/ChatUserMessage.tsx +72 -0
- package/src/chat/message-ui/ChatWelcome.tsx +70 -0
- package/src/chat/message-ui/MermaidBlock.tsx +266 -0
- package/src/chat/message-ui/mermaidFence.ts +55 -0
- package/src/chat/message-ui/sanitizeMermaid.ts +135 -0
- package/src/chat/tool-ui/BackgroundTaskToolUI.tsx +36 -0
- package/src/chat/tool-ui/BackgroundTasksBar.tsx +152 -0
- package/src/chat/tool-ui/CallAgentToolUI.tsx +168 -0
- package/src/chat/tool-ui/ChatToolCall.tsx +77 -0
- package/src/chat/tool-ui/GetCurrentTimeToolUI.tsx +49 -0
- package/src/chat/tool-ui/ReadSkillToolUI.tsx +80 -0
- package/src/chat/tool-ui/RunJsToolUI.tsx +106 -0
- package/src/chat/tool-ui/ToolUiBadge.tsx +10 -0
- package/src/chat/tool-ui/ToolUiTrailing.tsx +24 -0
- package/src/chat/tool-ui/WebFetchToolUI.tsx +53 -0
- package/src/chat/tool-ui/registry.ts +29 -0
- package/src/chat/tool-ui/types.ts +22 -0
- package/src/checkbox/Checkbox.tsx +50 -0
- package/src/codeblock/CodeBlock.tsx +76 -0
- package/src/codeblock/highlight.ts +159 -0
- package/src/colorpicker/ColorPicker.tsx +45 -0
- package/src/datepicker/DatePicker.tsx +481 -0
- package/src/desktop/DesktopHeader.tsx +92 -0
- package/src/desktop/DesktopIcon.tsx +34 -0
- package/src/desktop/DesktopStage.tsx +6 -0
- package/src/desktop/DesktopWindow.tsx +504 -0
- package/src/desktop/MeadowDesktop.tsx +15 -0
- package/src/desktop/MeadowShell.tsx +12 -0
- package/src/desktop/MeadowWallpaper.tsx +5 -0
- package/src/desktop/assets/desktop-meadow.jpg +0 -0
- package/src/desktop/assets.d.ts +9 -0
- package/src/desktop/surface.ts +1 -0
- package/src/drawer/Drawer.tsx +125 -0
- package/src/dropdown/ContextMenu.tsx +97 -0
- package/src/dropdown/Dropdown.tsx +139 -0
- package/src/dropdown/menuClasses.ts +9 -0
- package/src/empty/Empty.tsx +47 -0
- package/src/form/Form.tsx +95 -0
- package/src/form/FormItem.tsx +145 -0
- package/src/form/common/context.ts +13 -0
- package/src/form/common/enum.ts +20 -0
- package/src/form/common/rules.ts +68 -0
- package/src/form/common/types.ts +169 -0
- package/src/form/index.ts +35 -0
- package/src/form/partials/FormItemHelps.tsx +31 -0
- package/src/form/variants/FieldCheckbox.tsx +20 -0
- package/src/form/variants/FieldColor.tsx +63 -0
- package/src/form/variants/FieldDateTime.tsx +25 -0
- package/src/form/variants/FieldHidden.tsx +9 -0
- package/src/form/variants/FieldInput.tsx +24 -0
- package/src/form/variants/FieldNumber.tsx +28 -0
- package/src/form/variants/FieldObject.tsx +29 -0
- package/src/form/variants/FieldRadio.tsx +40 -0
- package/src/form/variants/FieldRepeater.tsx +62 -0
- package/src/form/variants/FieldRepeaterItem.tsx +64 -0
- package/src/form/variants/FieldSelect.tsx +27 -0
- package/src/form/variants/FieldSelectMultiple.tsx +32 -0
- package/src/form/variants/FieldSelectRemote.tsx +66 -0
- package/src/form/variants/FieldSwitch.tsx +17 -0
- package/src/form/variants/FieldTextarea.tsx +25 -0
- package/src/form/variants/FieldTime.tsx +25 -0
- package/src/form-layout/FormLayout.tsx +74 -0
- package/src/icon/FluentIcon.tsx +28 -0
- package/src/icon/fluent.ts +75 -0
- package/src/index.ts +214 -0
- package/src/input/Input.tsx +371 -0
- package/src/input/SearchInput.tsx +73 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/placement.ts +44 -0
- package/src/lib/sizes.ts +117 -0
- package/src/lib/surface.ts +8 -0
- package/src/menu/Menu.tsx +98 -0
- package/src/message/message.tsx +214 -0
- package/src/modal/Modal.tsx +277 -0
- package/src/pagination/Pagination.tsx +103 -0
- package/src/popconfirm/Popconfirm.tsx +134 -0
- package/src/popover/Popover.tsx +116 -0
- package/src/scroll/OverlayScroll.tsx +141 -0
- package/src/segmented/Segmented.tsx +67 -0
- package/src/select/Select.tsx +319 -0
- package/src/skeleton/Skeleton.tsx +47 -0
- package/src/spin/Spin.tsx +160 -0
- package/src/styles.css +897 -0
- package/src/switch/Switch.tsx +58 -0
- package/src/table/Table.tsx +387 -0
- package/src/tag/Tag.tsx +109 -0
- package/src/theme.ts +131 -0
- package/src/timepicker/TimePicker.tsx +751 -0
- package/src/tooltip/Tooltip.tsx +59 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
|
2
|
+
import { type CSSProperties, type ComponentPropsWithoutRef, forwardRef } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { type ControlSize, controlRadiusVar, normalizeSize } from "../lib/sizes";
|
|
5
|
+
|
|
6
|
+
export type SwitchVariant = "default" | "square";
|
|
7
|
+
|
|
8
|
+
export type SwitchProps = Omit<ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>, "onCheckedChange" | "checked" | "onChange"> & {
|
|
9
|
+
checked?: boolean;
|
|
10
|
+
defaultChecked?: boolean;
|
|
11
|
+
/** antd-style — receives the next checked boolean (not a DOM event). */
|
|
12
|
+
onChange?: (checked: boolean) => void;
|
|
13
|
+
size?: ControlSize;
|
|
14
|
+
variant?: SwitchVariant;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Switch track heights — shorter than Button/Input (antd-like). */
|
|
18
|
+
const SWITCH_TRACK_H = { small: 16, default: 22, large: 28 } as const;
|
|
19
|
+
|
|
20
|
+
export const Switch = forwardRef<HTMLButtonElement, SwitchProps>(function Switch({ className, checked, defaultChecked, onChange, size, variant = "default", disabled, style, ...rest }, ref) {
|
|
21
|
+
const trackH = SWITCH_TRACK_H[normalizeSize(size)];
|
|
22
|
+
const trackW = Math.round(trackH * 1.8);
|
|
23
|
+
const thumb = trackH - 4;
|
|
24
|
+
const travel = trackW - thumb - 3;
|
|
25
|
+
const square = variant === "square";
|
|
26
|
+
const squareRadius = `max(3px, calc(${controlRadiusVar(size)} * 0.55))`;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<SwitchPrimitive.Root
|
|
30
|
+
ref={ref}
|
|
31
|
+
checked={checked}
|
|
32
|
+
defaultChecked={defaultChecked}
|
|
33
|
+
disabled={disabled}
|
|
34
|
+
onCheckedChange={onChange}
|
|
35
|
+
className={cn(
|
|
36
|
+
"peer inline-flex shrink-0 cursor-pointer items-center border transition-colors duration-200 ease-[cubic-bezier(0.16,1,0.3,1)]",
|
|
37
|
+
square ? undefined : "rounded-full",
|
|
38
|
+
"focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-45",
|
|
39
|
+
"data-[state=checked]:border-transparent data-[state=checked]:bg-brand data-[state=unchecked]:border-[color-mix(in_oklab,var(--foreground)_40%,var(--background))] data-[state=unchecked]:bg-[color-mix(in_oklab,var(--foreground)_22%,var(--background))]",
|
|
40
|
+
className,
|
|
41
|
+
)}
|
|
42
|
+
style={{ width: trackW, height: trackH, borderRadius: square ? squareRadius : undefined, ...style }}
|
|
43
|
+
{...rest}
|
|
44
|
+
>
|
|
45
|
+
<SwitchPrimitive.Thumb
|
|
46
|
+
className={cn("pointer-events-none block bg-(--nonla-surface) shadow-sm transition-transform duration-200 ease-[cubic-bezier(0.16,1,0.3,1)] data-[state=unchecked]:translate-x-0.5 data-[state=checked]:translate-x-(--nonla-switch-travel)", square ? undefined : "rounded-full")}
|
|
47
|
+
style={
|
|
48
|
+
{
|
|
49
|
+
width: thumb,
|
|
50
|
+
height: thumb,
|
|
51
|
+
borderRadius: square ? `max(1px, calc(${squareRadius} - 1px))` : undefined,
|
|
52
|
+
["--nonla-switch-travel" as string]: `${travel}px`,
|
|
53
|
+
} as CSSProperties
|
|
54
|
+
}
|
|
55
|
+
/>
|
|
56
|
+
</SwitchPrimitive.Root>
|
|
57
|
+
);
|
|
58
|
+
});
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { type CSSProperties, type Key, type ReactNode, useMemo, useState } from "react";
|
|
2
|
+
import { Checkbox } from "../checkbox/Checkbox";
|
|
3
|
+
import { Empty } from "../empty/Empty";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { type ControlSize, normalizeSize } from "../lib/sizes";
|
|
6
|
+
import { Pagination } from "../pagination/Pagination";
|
|
7
|
+
import { Spin } from "../spin/Spin";
|
|
8
|
+
|
|
9
|
+
export type SortOrder = "ascend" | "descend" | null;
|
|
10
|
+
|
|
11
|
+
export type ColumnType<T> = {
|
|
12
|
+
title?: ReactNode;
|
|
13
|
+
dataIndex?: keyof T | string | (string | number)[];
|
|
14
|
+
key?: string;
|
|
15
|
+
width?: number | string;
|
|
16
|
+
minWidth?: number | string;
|
|
17
|
+
align?: "left" | "center" | "right";
|
|
18
|
+
/** Custom cell — Ant Design style `(value, record, index) => ReactNode`. */
|
|
19
|
+
render?: (value: any, record: T, index: number) => ReactNode;
|
|
20
|
+
className?: string;
|
|
21
|
+
ellipsis?: boolean;
|
|
22
|
+
fixed?: "left" | "right";
|
|
23
|
+
/** `true` = default compare on `dataIndex`; or pass a compare fn. */
|
|
24
|
+
sorter?: boolean | ((a: T, b: T) => number);
|
|
25
|
+
sortOrder?: SortOrder;
|
|
26
|
+
defaultSortOrder?: SortOrder;
|
|
27
|
+
onHeaderCell?: () => { className?: string; style?: CSSProperties };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ColumnsType<T> = ColumnType<T>[];
|
|
31
|
+
|
|
32
|
+
export type TablePaginationConfig = {
|
|
33
|
+
current?: number;
|
|
34
|
+
pageSize?: number;
|
|
35
|
+
total?: number;
|
|
36
|
+
onChange?: (page: number, pageSize: number) => void;
|
|
37
|
+
hideOnSinglePage?: boolean;
|
|
38
|
+
className?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type TableRowSelection<T> = {
|
|
42
|
+
selectedRowKeys?: Key[];
|
|
43
|
+
defaultSelectedRowKeys?: Key[];
|
|
44
|
+
onChange?: (selectedRowKeys: Key[], selectedRows: T[]) => void;
|
|
45
|
+
getCheckboxProps?: (record: T) => { disabled?: boolean };
|
|
46
|
+
columnWidth?: number | string;
|
|
47
|
+
type?: "checkbox" | "radio";
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type TableProps<T extends object = Record<string, unknown>> = {
|
|
51
|
+
columns?: ColumnType<T>[];
|
|
52
|
+
dataSource?: T[];
|
|
53
|
+
rowKey?: keyof T | ((record: T) => Key);
|
|
54
|
+
loading?: boolean;
|
|
55
|
+
/** `false` disables; object enables (client-side slice when `total` omitted). */
|
|
56
|
+
pagination?: false | TablePaginationConfig;
|
|
57
|
+
className?: string;
|
|
58
|
+
size?: ControlSize;
|
|
59
|
+
bordered?: boolean;
|
|
60
|
+
scroll?: { x?: number | string; y?: number | string };
|
|
61
|
+
onRow?: (
|
|
62
|
+
record: T,
|
|
63
|
+
index: number,
|
|
64
|
+
) => {
|
|
65
|
+
onClick?: () => void;
|
|
66
|
+
className?: string;
|
|
67
|
+
style?: CSSProperties;
|
|
68
|
+
};
|
|
69
|
+
locale?: { emptyText?: ReactNode };
|
|
70
|
+
showHeader?: boolean;
|
|
71
|
+
title?: ReactNode | ((data: readonly T[]) => ReactNode);
|
|
72
|
+
footer?: ReactNode | ((data: readonly T[]) => ReactNode);
|
|
73
|
+
rowSelection?: TableRowSelection<T>;
|
|
74
|
+
rowClassName?: string | ((record: T, index: number) => string);
|
|
75
|
+
onChange?: (pagination: TablePaginationConfig | false, _filters: Record<string, unknown>, sorter: { columnKey?: string; field?: string; order?: SortOrder }) => void;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function getRowKey<T extends object>(record: T, index: number, rowKey?: TableProps<T>["rowKey"]): Key {
|
|
79
|
+
if (typeof rowKey === "function") return rowKey(record);
|
|
80
|
+
if (typeof rowKey === "string" || typeof rowKey === "number") {
|
|
81
|
+
return String(record[rowKey as keyof T] ?? index);
|
|
82
|
+
}
|
|
83
|
+
if ("id" in record) return String((record as { id: unknown }).id);
|
|
84
|
+
if ("key" in record) return String((record as { key: unknown }).key);
|
|
85
|
+
return index;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getCellValue<T>(record: T, dataIndex?: ColumnType<T>["dataIndex"]): unknown {
|
|
89
|
+
if (dataIndex == null) return undefined;
|
|
90
|
+
if (Array.isArray(dataIndex)) {
|
|
91
|
+
let cur: unknown = record;
|
|
92
|
+
for (const part of dataIndex) {
|
|
93
|
+
if (cur == null || typeof cur !== "object") return undefined;
|
|
94
|
+
cur = (cur as Record<string | number, unknown>)[part];
|
|
95
|
+
}
|
|
96
|
+
return cur;
|
|
97
|
+
}
|
|
98
|
+
return (record as Record<string, unknown>)[dataIndex as string];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getColumnKey<T>(col: ColumnType<T>, index: number): string {
|
|
102
|
+
if (col.key != null) return col.key;
|
|
103
|
+
if (Array.isArray(col.dataIndex)) return col.dataIndex.join(".");
|
|
104
|
+
if (col.dataIndex != null) return String(col.dataIndex);
|
|
105
|
+
return String(index);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function defaultCompare(a: unknown, b: unknown): number {
|
|
109
|
+
if (a == null && b == null) return 0;
|
|
110
|
+
if (a == null) return -1;
|
|
111
|
+
if (b == null) return 1;
|
|
112
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
113
|
+
return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: "base" });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Dual filled carets with a clear gap (stroke chevrons merge into a ◇). */
|
|
117
|
+
function SortIcon({ order }: { order: SortOrder | undefined }) {
|
|
118
|
+
return (
|
|
119
|
+
<span className="ml-1.5 inline-flex shrink-0 flex-col items-center gap-0.5" aria-hidden>
|
|
120
|
+
<svg width="7" height="4" viewBox="0 0 7 4" className={cn(order === "ascend" ? "text-foreground" : "text-muted-foreground/45")}>
|
|
121
|
+
<path d="M3.5 0 7 4H0z" fill="currentColor" />
|
|
122
|
+
</svg>
|
|
123
|
+
<svg width="7" height="4" viewBox="0 0 7 4" className={cn(order === "descend" ? "text-foreground" : "text-muted-foreground/45")}>
|
|
124
|
+
<path d="M3.5 4 0 0h7z" fill="currentColor" />
|
|
125
|
+
</svg>
|
|
126
|
+
</span>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sizeClasses(size: ControlSize | undefined) {
|
|
131
|
+
const s = normalizeSize(size);
|
|
132
|
+
if (s === "small") {
|
|
133
|
+
return {
|
|
134
|
+
head: "h-9 px-2 text-xs",
|
|
135
|
+
cell: "px-2 py-1.5 text-xs",
|
|
136
|
+
check: "w-9 px-2",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (s === "large") {
|
|
140
|
+
return {
|
|
141
|
+
head: "h-12 px-3 text-sm",
|
|
142
|
+
cell: "px-3 py-3 text-sm",
|
|
143
|
+
check: "w-12 px-3",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// default — matches shadcn TableHead h-10 / TableCell p-2 / text-sm
|
|
147
|
+
return {
|
|
148
|
+
head: "h-10 px-2 text-sm",
|
|
149
|
+
cell: "p-2 text-sm",
|
|
150
|
+
check: "w-10 px-2",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function Table<T extends object = Record<string, unknown>>({ columns = [], dataSource = [], rowKey, loading, pagination, className, size, bordered, scroll, onRow, locale, showHeader = true, title, footer, rowSelection, rowClassName, onChange }: TableProps<T>) {
|
|
155
|
+
const sz = sizeClasses(size);
|
|
156
|
+
|
|
157
|
+
const selectionControlled = rowSelection?.selectedRowKeys !== undefined;
|
|
158
|
+
const [innerSelected, setInnerSelected] = useState<Key[]>(() => rowSelection?.defaultSelectedRowKeys ?? []);
|
|
159
|
+
const selectedKeys = selectionControlled ? (rowSelection?.selectedRowKeys ?? []) : innerSelected;
|
|
160
|
+
|
|
161
|
+
const [innerPage, setInnerPage] = useState(1);
|
|
162
|
+
const [innerPageSize, setInnerPageSize] = useState(10);
|
|
163
|
+
|
|
164
|
+
const initialSort = useMemo(() => {
|
|
165
|
+
const col = columns.find((c) => c.defaultSortOrder);
|
|
166
|
+
if (!col) return { key: null as string | null, order: null as SortOrder };
|
|
167
|
+
return { key: getColumnKey(col, columns.indexOf(col)), order: col.defaultSortOrder ?? null };
|
|
168
|
+
}, [columns]);
|
|
169
|
+
|
|
170
|
+
const [innerSort, setInnerSort] = useState(initialSort);
|
|
171
|
+
|
|
172
|
+
const controlledSortCol = columns.find((c) => c.sortOrder !== undefined);
|
|
173
|
+
const sortState = controlledSortCol
|
|
174
|
+
? {
|
|
175
|
+
key: getColumnKey(controlledSortCol, columns.indexOf(controlledSortCol)),
|
|
176
|
+
order: controlledSortCol.sortOrder ?? null,
|
|
177
|
+
}
|
|
178
|
+
: innerSort;
|
|
179
|
+
|
|
180
|
+
const paginationOff = pagination === false;
|
|
181
|
+
const pageConfig: TablePaginationConfig = paginationOff ? {} : (pagination ?? {});
|
|
182
|
+
const pageControlled = pageConfig.current !== undefined;
|
|
183
|
+
const current = pageControlled ? (pageConfig.current ?? 1) : innerPage;
|
|
184
|
+
const pageSize = pageConfig.pageSize ?? (pageControlled ? 10 : innerPageSize);
|
|
185
|
+
const serverSide = pageConfig.total !== undefined;
|
|
186
|
+
|
|
187
|
+
const sortedData = useMemo(() => {
|
|
188
|
+
if (!sortState.key || !sortState.order) return dataSource;
|
|
189
|
+
const colIndex = columns.findIndex((c, i) => getColumnKey(c, i) === sortState.key);
|
|
190
|
+
const col = colIndex >= 0 ? columns[colIndex] : undefined;
|
|
191
|
+
if (!col?.sorter) return dataSource;
|
|
192
|
+
const dir = sortState.order === "ascend" ? 1 : -1;
|
|
193
|
+
const cmp = typeof col.sorter === "function" ? col.sorter : (a: T, b: T) => defaultCompare(getCellValue(a, col.dataIndex), getCellValue(b, col.dataIndex));
|
|
194
|
+
return [...dataSource].sort((a, b) => cmp(a, b) * dir);
|
|
195
|
+
}, [columns, dataSource, sortState.key, sortState.order]);
|
|
196
|
+
|
|
197
|
+
const total = serverSide ? (pageConfig.total ?? sortedData.length) : sortedData.length;
|
|
198
|
+
|
|
199
|
+
const pageData = useMemo(() => {
|
|
200
|
+
if (paginationOff || serverSide) return sortedData;
|
|
201
|
+
const start = (current - 1) * pageSize;
|
|
202
|
+
return sortedData.slice(start, start + pageSize);
|
|
203
|
+
}, [sortedData, paginationOff, serverSide, current, pageSize]);
|
|
204
|
+
|
|
205
|
+
const showPagination = !paginationOff && !(pageConfig.hideOnSinglePage && total <= pageSize) && (total > 0 || serverSide);
|
|
206
|
+
|
|
207
|
+
const emitSelection = (keys: Key[]) => {
|
|
208
|
+
if (!selectionControlled) setInnerSelected(keys);
|
|
209
|
+
const rows = dataSource.filter((r, i) => keys.includes(getRowKey(r, i, rowKey)));
|
|
210
|
+
rowSelection?.onChange?.(keys, rows);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/** Two modes only: ascend ↔ descend. */
|
|
214
|
+
const toggleSort = (col: ColumnType<T>, index: number) => {
|
|
215
|
+
if (!col.sorter) return;
|
|
216
|
+
const key = getColumnKey(col, index);
|
|
217
|
+
const next: SortOrder = sortState.key === key && sortState.order === "ascend" ? "descend" : "ascend";
|
|
218
|
+
if (!controlledSortCol) setInnerSort({ key, order: next });
|
|
219
|
+
onChange?.(
|
|
220
|
+
paginationOff ? false : { current, pageSize, total },
|
|
221
|
+
{},
|
|
222
|
+
{
|
|
223
|
+
columnKey: key,
|
|
224
|
+
field: Array.isArray(col.dataIndex) ? col.dataIndex.join(".") : String(col.dataIndex ?? key),
|
|
225
|
+
order: next,
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const handlePageChange = (page: number, nextSize: number) => {
|
|
231
|
+
if (!pageControlled) {
|
|
232
|
+
setInnerPage(page);
|
|
233
|
+
setInnerPageSize(nextSize);
|
|
234
|
+
}
|
|
235
|
+
pageConfig.onChange?.(page, nextSize);
|
|
236
|
+
onChange?.({ current: page, pageSize: nextSize, total }, {}, { columnKey: sortState.key ?? undefined, order: sortState.order });
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const pageKeys = pageData.map((r, i) => getRowKey(r, (current - 1) * pageSize + i, rowKey));
|
|
240
|
+
const enabledPageKeys = pageData
|
|
241
|
+
.map((r, i) => ({ key: pageKeys[i]!, disabled: rowSelection?.getCheckboxProps?.(r)?.disabled }))
|
|
242
|
+
.filter((x) => !x.disabled)
|
|
243
|
+
.map((x) => x.key);
|
|
244
|
+
const allPageSelected = enabledPageKeys.length > 0 && enabledPageKeys.every((k) => selectedKeys.includes(k));
|
|
245
|
+
const somePageSelected = enabledPageKeys.some((k) => selectedKeys.includes(k));
|
|
246
|
+
|
|
247
|
+
const titleNode = typeof title === "function" ? title(pageData) : title;
|
|
248
|
+
const footerNode = typeof footer === "function" ? footer(pageData) : footer;
|
|
249
|
+
const selectionColWidth = rowSelection?.columnWidth;
|
|
250
|
+
const colCount = (columns.length || 1) + (rowSelection ? 1 : 0);
|
|
251
|
+
|
|
252
|
+
const alignClass = (align?: "left" | "center" | "right") => (align === "center" ? "text-center" : align === "right" ? "text-right" : "text-left");
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<div className={cn("relative w-full", className)}>
|
|
256
|
+
{titleNode != null ? <div className="mb-3 text-sm font-medium text-foreground">{titleNode}</div> : null}
|
|
257
|
+
|
|
258
|
+
<Spin spinning={Boolean(loading)}>
|
|
259
|
+
<div data-slot="table-container" className={cn("relative w-full overflow-x-auto", bordered && "rounded-md border border-border")} style={scroll?.x != null ? { overflowX: "auto" } : undefined}>
|
|
260
|
+
<div style={scroll?.y != null ? { maxHeight: scroll.y, overflow: "auto" } : undefined}>
|
|
261
|
+
<table data-slot="table" className="w-full caption-bottom border-collapse text-sm">
|
|
262
|
+
{showHeader ? (
|
|
263
|
+
<thead data-slot="table-header" className="[&_tr]:border-b [&_tr]:border-border">
|
|
264
|
+
<tr data-slot="table-row" className="border-b border-border transition-colors hover:bg-transparent">
|
|
265
|
+
{rowSelection ? (
|
|
266
|
+
<th data-slot="table-head" className={cn(sz.head, sz.check, "align-middle font-medium text-foreground", scroll?.y && "sticky top-0 z-10 bg-background", bordered && "border-b border-border")} style={selectionColWidth != null ? { width: selectionColWidth } : undefined}>
|
|
267
|
+
<div className="flex items-center justify-center">
|
|
268
|
+
{rowSelection.type === "radio" ? null : (
|
|
269
|
+
<Checkbox
|
|
270
|
+
checked={allPageSelected}
|
|
271
|
+
indeterminate={somePageSelected && !allPageSelected}
|
|
272
|
+
onChange={(checked) => {
|
|
273
|
+
if (checked) emitSelection(Array.from(new Set([...selectedKeys, ...enabledPageKeys])));
|
|
274
|
+
else emitSelection(selectedKeys.filter((k: Key) => !enabledPageKeys.includes(k)));
|
|
275
|
+
}}
|
|
276
|
+
aria-label="Select all"
|
|
277
|
+
/>
|
|
278
|
+
)}
|
|
279
|
+
</div>
|
|
280
|
+
</th>
|
|
281
|
+
) : null}
|
|
282
|
+
{columns.map((col, i) => {
|
|
283
|
+
const key = getColumnKey(col, i);
|
|
284
|
+
const order = sortState.key === key ? sortState.order : undefined;
|
|
285
|
+
const headerExtra = col.onHeaderCell?.();
|
|
286
|
+
const sortable = Boolean(col.sorter);
|
|
287
|
+
return (
|
|
288
|
+
<th
|
|
289
|
+
key={key}
|
|
290
|
+
data-slot="table-head"
|
|
291
|
+
className={cn(sz.head, "align-middle font-medium whitespace-nowrap text-foreground", alignClass(col.align), scroll?.y && "sticky top-0 z-10 bg-background", bordered && "border-b border-border", sortable && "cursor-pointer select-none", col.className, headerExtra?.className)}
|
|
292
|
+
style={{
|
|
293
|
+
width: col.width,
|
|
294
|
+
minWidth: col.minWidth,
|
|
295
|
+
...headerExtra?.style,
|
|
296
|
+
}}
|
|
297
|
+
onClick={sortable ? () => toggleSort(col, i) : undefined}
|
|
298
|
+
>
|
|
299
|
+
<span className={cn("inline-flex max-w-full items-center", col.align === "center" && "justify-center", col.align === "right" && "w-full justify-end")}>
|
|
300
|
+
<span className="min-w-0 truncate">{col.title}</span>
|
|
301
|
+
{sortable ? <SortIcon order={order ?? undefined} /> : null}
|
|
302
|
+
</span>
|
|
303
|
+
</th>
|
|
304
|
+
);
|
|
305
|
+
})}
|
|
306
|
+
</tr>
|
|
307
|
+
</thead>
|
|
308
|
+
) : null}
|
|
309
|
+
|
|
310
|
+
<tbody data-slot="table-body" className="[&_tr:last-child]:border-0">
|
|
311
|
+
{pageData.length === 0 ? (
|
|
312
|
+
<tr data-slot="table-row" className="border-b border-border">
|
|
313
|
+
<td data-slot="table-cell" colSpan={colCount} className={cn(sz.cell, "text-center align-middle")}>
|
|
314
|
+
{locale?.emptyText ?? <Empty description="No data" className="py-10" />}
|
|
315
|
+
</td>
|
|
316
|
+
</tr>
|
|
317
|
+
) : (
|
|
318
|
+
pageData.map((record, index) => {
|
|
319
|
+
const absoluteIndex = paginationOff || serverSide ? index : (current - 1) * pageSize + index;
|
|
320
|
+
const key = getRowKey(record, absoluteIndex, rowKey);
|
|
321
|
+
const rowProps = onRow?.(record, absoluteIndex);
|
|
322
|
+
const extraClass = typeof rowClassName === "function" ? rowClassName(record, absoluteIndex) : rowClassName;
|
|
323
|
+
const selected = selectedKeys.includes(key);
|
|
324
|
+
return (
|
|
325
|
+
<tr key={key} data-slot="table-row" data-state={selected ? "selected" : undefined} className={cn("border-b border-border transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", rowProps?.className, extraClass)} style={rowProps?.style} onClick={rowProps?.onClick}>
|
|
326
|
+
{rowSelection ? (
|
|
327
|
+
<td data-slot="table-cell" className={cn(sz.cell, sz.check, "align-middle")} style={selectionColWidth != null ? { width: selectionColWidth } : undefined}>
|
|
328
|
+
<div className="flex items-center justify-center" onClick={(e) => e.stopPropagation()}>
|
|
329
|
+
{rowSelection.type === "radio" ? (
|
|
330
|
+
<input type="radio" name="nonla-table-row-select" checked={selected} disabled={rowSelection.getCheckboxProps?.(record)?.disabled} aria-label="Select row" className="size-3.5 accent-foreground" onChange={() => emitSelection([key])} />
|
|
331
|
+
) : (
|
|
332
|
+
<Checkbox
|
|
333
|
+
checked={selected}
|
|
334
|
+
disabled={rowSelection.getCheckboxProps?.(record)?.disabled}
|
|
335
|
+
aria-label="Select row"
|
|
336
|
+
onChange={(next) => {
|
|
337
|
+
if (next) emitSelection([...selectedKeys, key]);
|
|
338
|
+
else emitSelection(selectedKeys.filter((k: Key) => k !== key));
|
|
339
|
+
}}
|
|
340
|
+
/>
|
|
341
|
+
)}
|
|
342
|
+
</div>
|
|
343
|
+
</td>
|
|
344
|
+
) : null}
|
|
345
|
+
{columns.map((col, i) => {
|
|
346
|
+
const raw = getCellValue(record, col.dataIndex);
|
|
347
|
+
const content = col.render ? col.render(raw, record, absoluteIndex) : (raw as ReactNode);
|
|
348
|
+
return (
|
|
349
|
+
<td
|
|
350
|
+
key={getColumnKey(col, i)}
|
|
351
|
+
data-slot="table-cell"
|
|
352
|
+
className={cn(sz.cell, "align-middle", alignClass(col.align), col.ellipsis ? "max-w-0 truncate" : "whitespace-nowrap", col.className)}
|
|
353
|
+
style={{ width: col.width, minWidth: col.minWidth }}
|
|
354
|
+
title={col.ellipsis && (typeof content === "string" || typeof content === "number") ? String(content) : undefined}
|
|
355
|
+
>
|
|
356
|
+
{content}
|
|
357
|
+
</td>
|
|
358
|
+
);
|
|
359
|
+
})}
|
|
360
|
+
</tr>
|
|
361
|
+
);
|
|
362
|
+
})
|
|
363
|
+
)}
|
|
364
|
+
</tbody>
|
|
365
|
+
|
|
366
|
+
{footerNode != null ? (
|
|
367
|
+
<tfoot data-slot="table-footer" className="border-t border-border bg-muted/50 font-medium [&>tr]:last:border-b-0">
|
|
368
|
+
<tr>
|
|
369
|
+
<td colSpan={colCount} className={cn(sz.cell, "align-middle text-muted-foreground")}>
|
|
370
|
+
{footerNode}
|
|
371
|
+
</td>
|
|
372
|
+
</tr>
|
|
373
|
+
</tfoot>
|
|
374
|
+
) : null}
|
|
375
|
+
</table>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
</Spin>
|
|
379
|
+
|
|
380
|
+
{showPagination ? (
|
|
381
|
+
<div className={cn("mt-4 flex justify-end", pageConfig.className)}>
|
|
382
|
+
<Pagination current={current} pageSize={pageSize} total={total} size={size} onChange={handlePageChange} />
|
|
383
|
+
</div>
|
|
384
|
+
) : null}
|
|
385
|
+
</div>
|
|
386
|
+
);
|
|
387
|
+
}
|
package/src/tag/Tag.tsx
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type TagVariant = "soft" | "solid";
|
|
5
|
+
|
|
6
|
+
export type TagProps = {
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
color?: string;
|
|
9
|
+
/** @deprecated prefer `variant="soft"` — kept for antd-ish callers */
|
|
10
|
+
bordered?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* `soft` — quiet wash (default).
|
|
13
|
+
* `solid` — saturated chip.
|
|
14
|
+
* `filled` / `outlined` map to `soft` for backwards compat.
|
|
15
|
+
*/
|
|
16
|
+
variant?: TagVariant | "filled" | "outlined";
|
|
17
|
+
className?: string;
|
|
18
|
+
onClose?: () => void;
|
|
19
|
+
closable?: boolean;
|
|
20
|
+
/** Hide the leading status dot on soft colored tags. */
|
|
21
|
+
dot?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const NAMED_VAR: Record<string, string> = {
|
|
25
|
+
success: "var(--success)",
|
|
26
|
+
error: "var(--destructive)",
|
|
27
|
+
warning: "var(--warn)",
|
|
28
|
+
processing: "var(--link)",
|
|
29
|
+
brand: "var(--brand)",
|
|
30
|
+
blue: "var(--nonla-blue)",
|
|
31
|
+
purple: "var(--nonla-purple)",
|
|
32
|
+
cyan: "var(--nonla-cyan)",
|
|
33
|
+
green: "var(--nonla-green)",
|
|
34
|
+
magenta: "var(--nonla-magenta)",
|
|
35
|
+
pink: "var(--nonla-pink)",
|
|
36
|
+
orange: "var(--nonla-orange)",
|
|
37
|
+
gold: "var(--nonla-gold)",
|
|
38
|
+
red: "var(--nonla-red)",
|
|
39
|
+
yellow: "var(--nonla-yellow)",
|
|
40
|
+
volcano: "var(--nonla-volcano)",
|
|
41
|
+
geekblue: "var(--nonla-geekblue)",
|
|
42
|
+
lime: "var(--nonla-lime)",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const SOFT = "bg-[color-mix(in_oklab,var(--nonla-tag)_12%,transparent)] text-[color-mix(in_oklab,var(--nonla-tag)_72%,black)]";
|
|
46
|
+
const SOLID = "bg-(--nonla-tag) text-[var(--nonla-solid-fg)]";
|
|
47
|
+
|
|
48
|
+
function resolveVariant(variant: TagProps["variant"]): TagVariant {
|
|
49
|
+
if (variant === "solid") return "solid";
|
|
50
|
+
return "soft";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function CloseIcon() {
|
|
54
|
+
return (
|
|
55
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden>
|
|
56
|
+
<path d="M2.2 2.2L7.8 7.8M7.8 2.2L2.2 7.8" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
|
|
57
|
+
</svg>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function Tag({
|
|
62
|
+
children,
|
|
63
|
+
color = "default",
|
|
64
|
+
bordered: _bordered,
|
|
65
|
+
variant = "soft",
|
|
66
|
+
className,
|
|
67
|
+
closable,
|
|
68
|
+
onClose,
|
|
69
|
+
dot,
|
|
70
|
+
}: TagProps) {
|
|
71
|
+
const named = NAMED_VAR[color];
|
|
72
|
+
const isDefault = color === "default";
|
|
73
|
+
const token = named ?? (isDefault ? undefined : color);
|
|
74
|
+
const mode = resolveVariant(variant);
|
|
75
|
+
const solid = mode === "solid";
|
|
76
|
+
const showDot = (dot ?? (!solid && !isDefault)) && Boolean(token || isDefault);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<span
|
|
80
|
+
className={cn(
|
|
81
|
+
"inline-flex h-[22px] max-w-full items-center gap-1.5 rounded-full px-2.5 text-[11px] font-medium tracking-[0.01em]",
|
|
82
|
+
isDefault && (solid ? "bg-foreground/10 text-(--nonla-ink)" : "bg-muted text-muted-foreground"),
|
|
83
|
+
!isDefault && (solid ? SOLID : SOFT),
|
|
84
|
+
className,
|
|
85
|
+
)}
|
|
86
|
+
style={token ? ({ "--nonla-tag": token } as CSSProperties) : undefined}
|
|
87
|
+
>
|
|
88
|
+
{showDot ? (
|
|
89
|
+
<span
|
|
90
|
+
className={cn("size-1.5 shrink-0 rounded-full", isDefault ? "bg-(--nonla-fg-muted)" : "bg-(--nonla-tag)")}
|
|
91
|
+
aria-hidden
|
|
92
|
+
/>
|
|
93
|
+
) : null}
|
|
94
|
+
<span className="min-w-0 overflow-clip text-ellipsis whitespace-nowrap [overflow-clip-margin:4px] [text-box:trim-both_cap_alphabetic]">
|
|
95
|
+
{children}
|
|
96
|
+
</span>
|
|
97
|
+
{closable ? (
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
className="-mr-0.5 inline-flex size-3.5 shrink-0 items-center justify-center rounded-full text-current/55 transition-colors hover:bg-foreground/8 hover:text-current"
|
|
101
|
+
aria-label="Remove"
|
|
102
|
+
onClick={onClose}
|
|
103
|
+
>
|
|
104
|
+
<CloseIcon />
|
|
105
|
+
</button>
|
|
106
|
+
) : null}
|
|
107
|
+
</span>
|
|
108
|
+
);
|
|
109
|
+
}
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme knobs for NonlaUI.
|
|
3
|
+
*
|
|
4
|
+
* Defaults live in `styles.css` on `:root` / `.dark` / `.nonla-ui`.
|
|
5
|
+
* `--brand-50`…`--brand-800` follow `--nonla-brand` (500 = the knob).
|
|
6
|
+
* Consumers retheme without touching components:
|
|
7
|
+
* 1. CSS: `:root { --nonla-brand: #3b82f6; }`
|
|
8
|
+
* 2. JS: `<App theme={{ colors: { brand: "#3b82f6" } }} />` or `applyNonlaTheme({ brand: "#3b82f6" })`
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const NONLA_THEME_KEYS = {
|
|
12
|
+
bg: "--nonla-bg",
|
|
13
|
+
fg: "--nonla-fg",
|
|
14
|
+
fgMuted: "--nonla-fg-muted",
|
|
15
|
+
fgTertiary: "--nonla-fg-tertiary",
|
|
16
|
+
fgQuaternary: "--nonla-fg-quaternary",
|
|
17
|
+
brand: "--nonla-brand",
|
|
18
|
+
solidFg: "--nonla-solid-fg",
|
|
19
|
+
danger: "--nonla-danger",
|
|
20
|
+
success: "--nonla-success",
|
|
21
|
+
warn: "--nonla-warn",
|
|
22
|
+
link: "--nonla-link",
|
|
23
|
+
radius: "--nonla-radius",
|
|
24
|
+
height: "--nonla-height",
|
|
25
|
+
heightSm: "--nonla-height-sm",
|
|
26
|
+
heightLg: "--nonla-height-lg",
|
|
27
|
+
ink: "--nonla-ink",
|
|
28
|
+
surface: "--nonla-surface",
|
|
29
|
+
elevated: "--nonla-elevated",
|
|
30
|
+
chip: "--nonla-chip",
|
|
31
|
+
chipHover: "--nonla-chip-hover",
|
|
32
|
+
border: "--nonla-border",
|
|
33
|
+
borderInput: "--nonla-input",
|
|
34
|
+
composer: "--nonla-composer",
|
|
35
|
+
chatBg: "--nonla-chat-bg",
|
|
36
|
+
meadow: "--nonla-meadow",
|
|
37
|
+
glass: "--nonla-glass",
|
|
38
|
+
glassBar: "--nonla-glass-bar",
|
|
39
|
+
glassMenu: "--nonla-glass-menu",
|
|
40
|
+
glassBorder: "--nonla-glass-border",
|
|
41
|
+
glassHighlight: "--nonla-glass-highlight",
|
|
42
|
+
inkHover: "--nonla-ink-hover",
|
|
43
|
+
inkActive: "--nonla-ink-active",
|
|
44
|
+
inkLine: "--nonla-ink-line",
|
|
45
|
+
desktopBarHeight: "--nonla-desktop-bar-height",
|
|
46
|
+
blue: "--nonla-blue",
|
|
47
|
+
purple: "--nonla-purple",
|
|
48
|
+
cyan: "--nonla-cyan",
|
|
49
|
+
green: "--nonla-green",
|
|
50
|
+
magenta: "--nonla-magenta",
|
|
51
|
+
pink: "--nonla-pink",
|
|
52
|
+
red: "--nonla-red",
|
|
53
|
+
orange: "--nonla-orange",
|
|
54
|
+
yellow: "--nonla-yellow",
|
|
55
|
+
volcano: "--nonla-volcano",
|
|
56
|
+
geekblue: "--nonla-geekblue",
|
|
57
|
+
lime: "--nonla-lime",
|
|
58
|
+
gold: "--nonla-gold",
|
|
59
|
+
} as const;
|
|
60
|
+
|
|
61
|
+
export type NonlaThemeKnobName = keyof typeof NONLA_THEME_KEYS;
|
|
62
|
+
export type NonlaThemeKnob = (typeof NONLA_THEME_KEYS)[NonlaThemeKnobName];
|
|
63
|
+
|
|
64
|
+
const METRIC_KEYS = new Set<NonlaThemeKnobName>(["radius", "height", "heightSm", "heightLg", "desktopBarHeight"]);
|
|
65
|
+
|
|
66
|
+
export type NonlaThemeColorName = Exclude<NonlaThemeKnobName, "radius" | "height" | "heightSm" | "heightLg">;
|
|
67
|
+
export type NonlaThemeColors = { [K in NonlaThemeColorName]?: string };
|
|
68
|
+
|
|
69
|
+
/** Flat aliases → knob name (`colorBorder` == `colors.border`). */
|
|
70
|
+
const COLOR_ALIASES = {
|
|
71
|
+
colorBorder: "border",
|
|
72
|
+
colorBorderInput: "borderInput",
|
|
73
|
+
/** @deprecated use `borderInput` / `colorBorderInput` */
|
|
74
|
+
input: "borderInput",
|
|
75
|
+
} as const satisfies Record<string, NonlaThemeColorName>;
|
|
76
|
+
|
|
77
|
+
export const NONLA_THEME_KNOBS = Object.values(NONLA_THEME_KEYS);
|
|
78
|
+
|
|
79
|
+
export type NonlaThemeConfig = {
|
|
80
|
+
[K in NonlaThemeKnobName]?: string;
|
|
81
|
+
} & {
|
|
82
|
+
/** Grouped color knobs — same names as the flat keys (`colors.border`, `colors.borderInput`, …). */
|
|
83
|
+
colors?: NonlaThemeColors;
|
|
84
|
+
/** Alias of `colors.border` / `border`. */
|
|
85
|
+
colorBorder?: string;
|
|
86
|
+
/** Alias of `colors.borderInput` / `borderInput`. */
|
|
87
|
+
colorBorderInput?: string;
|
|
88
|
+
/** Extra CSS custom properties (include the leading `--`). */
|
|
89
|
+
vars?: Record<string, string>;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
function entriesOf(theme: NonlaThemeConfig): [string, string][] {
|
|
93
|
+
const map = new Map<string, string>();
|
|
94
|
+
const set = (key: string, value: unknown) => {
|
|
95
|
+
if (typeof value !== "string" || !value) return;
|
|
96
|
+
const knob = (COLOR_ALIASES as Record<string, NonlaThemeKnobName>)[key] ?? (key as NonlaThemeKnobName);
|
|
97
|
+
const prop = NONLA_THEME_KEYS[knob];
|
|
98
|
+
if (prop) map.set(prop, value);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (theme.colors) {
|
|
102
|
+
for (const [key, value] of Object.entries(theme.colors)) {
|
|
103
|
+
if (METRIC_KEYS.has(key as NonlaThemeKnobName)) continue;
|
|
104
|
+
set(key, value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
set("colorBorder", theme.colorBorder);
|
|
108
|
+
set("colorBorderInput", theme.colorBorderInput);
|
|
109
|
+
for (const key of Object.keys(NONLA_THEME_KEYS) as NonlaThemeKnobName[]) {
|
|
110
|
+
set(key, theme[key]);
|
|
111
|
+
}
|
|
112
|
+
if (theme.vars) {
|
|
113
|
+
for (const [prop, value] of Object.entries(theme.vars)) {
|
|
114
|
+
if (value) map.set(prop, value);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return [...map.entries()];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Apply knobs on an element (defaults to `:root`). Returns a restore function. */
|
|
121
|
+
export function applyNonlaTheme(theme: NonlaThemeConfig, target: HTMLElement = document.documentElement): () => void {
|
|
122
|
+
const entries = entriesOf(theme);
|
|
123
|
+
const prev = entries.map(([prop]) => [prop, target.style.getPropertyValue(prop)] as const);
|
|
124
|
+
for (const [prop, value] of entries) target.style.setProperty(prop, value);
|
|
125
|
+
return () => {
|
|
126
|
+
for (const [prop, value] of prev) {
|
|
127
|
+
if (value) target.style.setProperty(prop, value);
|
|
128
|
+
else target.style.removeProperty(prop);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|