devnonla-ui 0.1.0 → 0.2.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/README.md +22 -10
- package/package.json +2 -2
- package/src/alert/Alert.tsx +1 -1
- package/src/app/App.tsx +38 -18
- package/src/app/context.ts +32 -0
- package/src/button/Button.tsx +2 -2
- package/src/calendar/Calendar.tsx +4 -4
- package/src/chat/AgentPanel.tsx +15 -6
- package/src/chat/common/types.ts +27 -2
- package/src/chat/common/useAgentStream.ts +78 -12
- package/src/chat/common/utils.ts +13 -0
- package/src/chat/index.ts +5 -3
- package/src/chat/message-ui/ChatAgentMessage.tsx +1 -1
- package/src/chat/message-ui/ChatInput.tsx +1 -1
- package/src/chat/message-ui/ChatMarkdown.tsx +6 -6
- package/src/chat/message-ui/ChatMarkdownTable.tsx +1 -1
- package/src/chat/message-ui/ChatThinking.tsx +4 -3
- package/src/chat/message-ui/ChatUserMessage.tsx +0 -8
- package/src/chat/message-ui/MermaidBlock.tsx +11 -1
- package/src/chat/tool-ui/BackgroundTaskToolUI.tsx +2 -1
- package/src/chat/tool-ui/CallAgentToolUI.tsx +3 -2
- package/src/chat/tool-ui/ChatToolCall.tsx +3 -2
- package/src/chat/tool-ui/GetCurrentTimeToolUI.tsx +2 -1
- package/src/chat/tool-ui/ReadSkillToolUI.tsx +3 -2
- package/src/chat/tool-ui/RunJsToolUI.tsx +3 -2
- package/src/chat/tool-ui/WebFetchToolUI.tsx +3 -2
- package/src/chat/tool-ui/registry.ts +24 -12
- package/src/checkbox/Checkbox.tsx +5 -4
- package/src/codeblock/CodeBlock.tsx +1 -0
- package/src/datepicker/DatePicker.tsx +15 -9
- package/src/desktop/DesktopHeader.tsx +1 -1
- package/src/desktop/DesktopIcon.tsx +2 -4
- package/src/desktop/DesktopWindow.tsx +19 -5
- package/src/desktop/MeadowDesktop.tsx +2 -2
- package/src/drawer/Drawer.tsx +8 -6
- package/src/dropdown/ContextMenu.tsx +3 -1
- package/src/dropdown/Dropdown.tsx +8 -6
- package/src/form/Form.tsx +2 -2
- package/src/form/FormItem.tsx +2 -2
- package/src/form/partials/FormItemHelps.tsx +2 -2
- package/src/form/variants/FieldColor.tsx +1 -1
- package/src/form/variants/FieldObject.tsx +2 -2
- package/src/form/variants/FieldRepeaterItem.tsx +4 -4
- package/src/form-layout/FormLayout.tsx +6 -6
- package/src/index.ts +20 -6
- package/src/input/Input.tsx +11 -10
- package/src/lib/sizes.ts +13 -3
- package/src/lib/surface.ts +2 -1
- package/src/menu/Menu.tsx +2 -2
- package/src/message/message.tsx +35 -2
- package/src/modal/Modal.tsx +49 -3
- package/src/pagination/Pagination.tsx +6 -5
- package/src/popconfirm/Popconfirm.tsx +1 -1
- package/src/popover/Popover.tsx +6 -8
- package/src/scroll/OverlayScroll.tsx +5 -3
- package/src/segmented/Segmented.tsx +6 -5
- package/src/select/Select.tsx +9 -6
- package/src/shimmer/Shimmer.tsx +17 -0
- package/src/skeleton/Skeleton.tsx +3 -3
- package/src/spin/Spin.tsx +8 -5
- package/src/styles.css +81 -27
- package/src/switch/Switch.tsx +6 -5
- package/src/table/Table.tsx +6 -7
- package/src/tag/Tag.tsx +2 -2
- package/src/theme.ts +23 -3
- package/src/timepicker/TimePicker.tsx +13 -11
- package/src/tooltip/Tooltip.tsx +6 -7
package/src/popover/Popover.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
2
2
|
import { type CSSProperties, type ReactNode, useEffect, useRef, useState } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { usePopupContainer } from "../app/context";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { type PopperPlacement, placementToRadix } from "../lib/placement";
|
|
6
6
|
import { glassOverlayClass } from "../lib/surface";
|
|
@@ -17,14 +17,14 @@ export type PopoverProps = {
|
|
|
17
17
|
mouseEnterDelay?: number;
|
|
18
18
|
mouseLeaveDelay?: number;
|
|
19
19
|
getPopupContainer?: () => HTMLElement;
|
|
20
|
-
/** Show arrow pointing at the trigger (
|
|
20
|
+
/** Show arrow pointing at the trigger (Popconfirm default). */
|
|
21
21
|
arrow?: boolean | { pointAtCenter?: boolean };
|
|
22
22
|
className?: string;
|
|
23
23
|
overlayClassName?: string;
|
|
24
24
|
/** Extra class on the content panel (padding / width). */
|
|
25
25
|
contentClassName?: string;
|
|
26
26
|
style?: CSSProperties;
|
|
27
|
-
/**
|
|
27
|
+
/** `styles` — `root`/`body` map to content panel; `container` accepted as alias. */
|
|
28
28
|
styles?: {
|
|
29
29
|
root?: CSSProperties;
|
|
30
30
|
body?: CSSProperties;
|
|
@@ -52,11 +52,11 @@ export function Popover({
|
|
|
52
52
|
style,
|
|
53
53
|
styles,
|
|
54
54
|
}: PopoverProps) {
|
|
55
|
+
const hover = trigger === "hover";
|
|
55
56
|
const showArrow = Boolean(arrow);
|
|
56
57
|
const panelStyle = { ...style, ...styles?.root, ...styles?.content, ...styles?.container, ...styles?.body };
|
|
57
|
-
const
|
|
58
|
+
const getContainer = usePopupContainer(getPopupContainer);
|
|
58
59
|
const { side, align } = placementToRadix(placement);
|
|
59
|
-
const hover = trigger === "hover";
|
|
60
60
|
const [innerOpen, setInnerOpen] = useState(defaultOpen ?? false);
|
|
61
61
|
const open = openProp ?? innerOpen;
|
|
62
62
|
const enterTimer = useRef<number>(0);
|
|
@@ -83,8 +83,6 @@ export function Popover({
|
|
|
83
83
|
leaveTimer.current = window.setTimeout(() => setOpen(false), mouseLeaveDelay * 1000);
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
-
const container = getPopupContainer ?? app.getPopupContainer;
|
|
87
|
-
|
|
88
86
|
return (
|
|
89
87
|
<PopoverPrimitive.Root
|
|
90
88
|
open={open}
|
|
@@ -96,7 +94,7 @@ export function Popover({
|
|
|
96
94
|
<PopoverPrimitive.Trigger asChild onMouseEnter={onEnter} onMouseLeave={onLeave}>
|
|
97
95
|
{children}
|
|
98
96
|
</PopoverPrimitive.Trigger>
|
|
99
|
-
<PopoverPrimitive.Portal container={
|
|
97
|
+
<PopoverPrimitive.Portal container={getContainer()}>
|
|
100
98
|
<PopoverPrimitive.Content
|
|
101
99
|
side={side}
|
|
102
100
|
align={align}
|
|
@@ -3,6 +3,8 @@ import { cn } from "../lib/cn";
|
|
|
3
3
|
|
|
4
4
|
export type OverlayScrollVisibility = "hover" | "always";
|
|
5
5
|
|
|
6
|
+
const HIDE_MS = 1000;
|
|
7
|
+
|
|
6
8
|
export type OverlayScrollProps = {
|
|
7
9
|
/** `hover` shows the thumb on hover / while scrolling. `always` keeps it visible. */
|
|
8
10
|
visibility?: OverlayScrollVisibility;
|
|
@@ -75,7 +77,7 @@ export function OverlayScroll({ visibility = "hover", className, innerClassName,
|
|
|
75
77
|
if (visibility === "hover" && !dragging) {
|
|
76
78
|
setScrolling(true);
|
|
77
79
|
if (hideTimer.current) clearTimeout(hideTimer.current);
|
|
78
|
-
hideTimer.current = setTimeout(() => setScrolling(false),
|
|
80
|
+
hideTimer.current = setTimeout(() => setScrolling(false), HIDE_MS);
|
|
79
81
|
}
|
|
80
82
|
onScroll?.(e);
|
|
81
83
|
};
|
|
@@ -109,7 +111,7 @@ export function OverlayScroll({ visibility = "hover", className, innerClassName,
|
|
|
109
111
|
window.removeEventListener("pointercancel", onUp, true);
|
|
110
112
|
setDragging(false);
|
|
111
113
|
if (hideTimer.current) clearTimeout(hideTimer.current);
|
|
112
|
-
hideTimer.current = setTimeout(() => setScrolling(false),
|
|
114
|
+
hideTimer.current = setTimeout(() => setScrolling(false), HIDE_MS);
|
|
113
115
|
};
|
|
114
116
|
window.addEventListener("pointermove", onMove, true);
|
|
115
117
|
window.addEventListener("pointerup", onUp, true);
|
|
@@ -131,7 +133,7 @@ export function OverlayScroll({ visibility = "hover", className, innerClassName,
|
|
|
131
133
|
"nonla-overlay-thumb absolute z-20 rounded-full transition-opacity duration-150",
|
|
132
134
|
visibility === "always" || dragging
|
|
133
135
|
? "pointer-events-auto opacity-100"
|
|
134
|
-
: "pointer-events-none opacity-0 group-hover/scroll:pointer-events-auto group-hover/scroll:opacity-100 group-data-
|
|
136
|
+
: "pointer-events-none opacity-0 group-hover/scroll:pointer-events-auto group-hover/scroll:opacity-100 group-data-scrolling/scroll:pointer-events-auto group-data-scrolling/scroll:opacity-100",
|
|
135
137
|
)}
|
|
136
138
|
style={{ right: 2, width: "var(--nonla-scrollbar-size)", height: thumb.height, transform: `translateY(${thumb.top}px)` }}
|
|
137
139
|
/>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { cn } from "../lib/cn";
|
|
3
|
-
import { type ControlSize, controlHeightVar, controlRadiusVar, getSizeTokens,
|
|
3
|
+
import { type ControlSize, controlHeightVar, controlRadiusVar, getSizeTokens, useControlSize } from "../lib/sizes";
|
|
4
4
|
|
|
5
5
|
export type SegmentedOption<V extends string | number = string | number> = V | { label: ReactNode; value: V; disabled?: boolean; icon?: ReactNode };
|
|
6
6
|
|
|
@@ -23,16 +23,17 @@ function norm<V extends string | number>(opt: SegmentedOption<V>): { label: Reac
|
|
|
23
23
|
export function Segmented<V extends string | number = string | number>({ options, value, defaultValue, onChange, size, block, disabled, className }: SegmentedProps<V>) {
|
|
24
24
|
const items = options.map((o) => norm(o));
|
|
25
25
|
const current = value ?? defaultValue ?? items[0]?.value;
|
|
26
|
-
const
|
|
26
|
+
const resolvedSize = useControlSize(size);
|
|
27
|
+
const tok = getSizeTokens(resolvedSize);
|
|
27
28
|
const itemPadX = Math.max(tok.paddingInline - 4, 6);
|
|
28
|
-
const radius = controlRadiusVar(
|
|
29
|
+
const radius = controlRadiusVar(resolvedSize);
|
|
29
30
|
|
|
30
31
|
return (
|
|
31
32
|
<div
|
|
32
33
|
className={cn("inline-flex w-fit max-w-full shrink-0 items-center gap-0.5 rounded-lg bg-brand-50 p-0.5", block && "flex w-full", className)}
|
|
33
34
|
role="tablist"
|
|
34
35
|
style={{ borderRadius: radius }}
|
|
35
|
-
data-size={
|
|
36
|
+
data-size={resolvedSize}
|
|
36
37
|
>
|
|
37
38
|
{items.map((item) => {
|
|
38
39
|
const active = item.value === current;
|
|
@@ -49,7 +50,7 @@ export function Segmented<V extends string | number = string | number>({ options
|
|
|
49
50
|
active ? "bg-brand-200 text-foreground" : "bg-transparent text-foreground hover:bg-brand-100",
|
|
50
51
|
)}
|
|
51
52
|
style={{
|
|
52
|
-
height: `max(22px, calc(${controlHeightVar(
|
|
53
|
+
height: `max(22px, calc(${controlHeightVar(resolvedSize)} - 4px))`,
|
|
53
54
|
paddingLeft: itemPadX,
|
|
54
55
|
paddingRight: itemPadX,
|
|
55
56
|
fontSize: tok.fontSize,
|
package/src/select/Select.tsx
CHANGED
|
@@ -11,8 +11,9 @@ import {
|
|
|
11
11
|
useRef,
|
|
12
12
|
useState,
|
|
13
13
|
} from "react";
|
|
14
|
+
import { usePopupContainer } from "../app/context";
|
|
14
15
|
import { cn } from "../lib/cn";
|
|
15
|
-
import { type ControlSize, controlFieldFocusBorder, controlFieldStyle, controlFieldSurface, controlFieldTransition, controlStatusClass } from "../lib/sizes";
|
|
16
|
+
import { type ControlSize, controlFieldFocusBorder, controlFieldStyle, controlFieldSurface, controlFieldTransition, controlStatusClass, useControlSize } from "../lib/sizes";
|
|
16
17
|
import { glassOverlayClass } from "../lib/surface";
|
|
17
18
|
|
|
18
19
|
export type SelectValue = string | number;
|
|
@@ -26,10 +27,10 @@ export type SelectOptionConfig = {
|
|
|
26
27
|
export type SelectProps = {
|
|
27
28
|
value?: SelectValue | null;
|
|
28
29
|
defaultValue?: SelectValue | null;
|
|
29
|
-
//
|
|
30
|
+
// Call sites often use (v: string) => void
|
|
30
31
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
32
|
onChange?: (value: any) => void;
|
|
32
|
-
//
|
|
33
|
+
// Many call sites type handlers as `(v: string) => void`.
|
|
33
34
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
35
|
options?: SelectOptionConfig[];
|
|
35
36
|
placeholder?: string;
|
|
@@ -42,7 +43,7 @@ export type SelectProps = {
|
|
|
42
43
|
className?: string;
|
|
43
44
|
popupClassName?: string;
|
|
44
45
|
children?: ReactNode;
|
|
45
|
-
/**
|
|
46
|
+
/** Alias of `onChange` for a single pick. */
|
|
46
47
|
onSelect?: (value: SelectValue) => void;
|
|
47
48
|
};
|
|
48
49
|
|
|
@@ -104,6 +105,8 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
104
105
|
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
|
105
106
|
const searchRef = useRef<HTMLInputElement>(null);
|
|
106
107
|
const listRef = useRef<HTMLDivElement>(null);
|
|
108
|
+
const resolvedSize = useControlSize(size);
|
|
109
|
+
const portal = usePopupContainer()?.();
|
|
107
110
|
|
|
108
111
|
const searchable = Boolean(showSearch);
|
|
109
112
|
const filterProp =
|
|
@@ -219,7 +222,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
219
222
|
controlStatusClass(status),
|
|
220
223
|
className,
|
|
221
224
|
)}
|
|
222
|
-
style={controlFieldStyle(
|
|
225
|
+
style={controlFieldStyle(resolvedSize)}
|
|
223
226
|
onKeyDown={onTriggerKey}
|
|
224
227
|
>
|
|
225
228
|
<span className={cn("min-w-0 flex-1 truncate", selected == null || selected === "" ? "text-quaternary-foreground" : "")}>
|
|
@@ -244,7 +247,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
244
247
|
<Chevron />
|
|
245
248
|
</button>
|
|
246
249
|
</PopoverPrimitive.Trigger>
|
|
247
|
-
<PopoverPrimitive.Portal>
|
|
250
|
+
<PopoverPrimitive.Portal container={portal}>
|
|
248
251
|
<PopoverPrimitive.Content
|
|
249
252
|
align="start"
|
|
250
253
|
sideOffset={4}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type ShimmerProps = HTMLAttributes<HTMLSpanElement> & {
|
|
5
|
+
/** When false, children render as static text. Default true. */
|
|
6
|
+
active?: boolean;
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Sweep highlight on live status text (Thinking, Fetching, …). */
|
|
11
|
+
export function Shimmer({ active = true, className, children, ...rest }: ShimmerProps) {
|
|
12
|
+
return (
|
|
13
|
+
<span className={cn(active && "nonla-shimmer", className)} aria-busy={active || undefined} {...rest}>
|
|
14
|
+
{children}
|
|
15
|
+
</span>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
2
|
import { cn } from "../lib/cn";
|
|
3
3
|
|
|
4
4
|
export type SkeletonProps = {
|
|
@@ -20,8 +20,8 @@ function SkeletonRoot({ active = true, loading = true, paragraph = true, title =
|
|
|
20
20
|
{avatar ? <div className={cn("size-10 shrink-0 rounded-full bg-secondary", pulse)} /> : null}
|
|
21
21
|
<div className="flex-1 space-y-2">
|
|
22
22
|
{title ? <div className={cn("h-4 w-1/3 rounded bg-secondary", pulse)} /> : null}
|
|
23
|
-
{Array.from({ length: rows }).map((
|
|
24
|
-
<div key={
|
|
23
|
+
{Array.from({ length: rows }, (_, i) => `sk-${i}`).map((key, i) => (
|
|
24
|
+
<div key={key} className={cn("h-3 rounded bg-secondary", pulse, i === rows - 1 ? "w-2/3" : "w-full")} />
|
|
25
25
|
))}
|
|
26
26
|
</div>
|
|
27
27
|
</div>
|
package/src/spin/Spin.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactNode, useEffect, useState } from "react";
|
|
2
2
|
import { cn } from "../lib/cn";
|
|
3
|
-
import { type ControlSize, getSizeTokens } from "../lib/sizes";
|
|
3
|
+
import { type ControlSize, getSizeTokens, useControlSize } from "../lib/sizes";
|
|
4
4
|
|
|
5
5
|
export type SpinVariant = "default" | "agent" | "subAgent";
|
|
6
6
|
|
|
@@ -35,6 +35,8 @@ type Cell = number; // 0..8 on a 3×3 grid
|
|
|
35
35
|
/** Snake = [head, mid, tail] — always 3 orthogonally adjacent cells. */
|
|
36
36
|
type Snake = [Cell, Cell, Cell];
|
|
37
37
|
|
|
38
|
+
const MATRIX_CELLS = [0, 1, 2, 3, 4, 5, 6, 7, 8] as const;
|
|
39
|
+
|
|
38
40
|
const NEIGHBORS: Record<number, number[]> = {
|
|
39
41
|
0: [1, 3],
|
|
40
42
|
1: [0, 2, 4],
|
|
@@ -103,8 +105,8 @@ function MatrixSnake({ size, tickMs = 220 }: { size: ControlSize | undefined; ti
|
|
|
103
105
|
}}
|
|
104
106
|
aria-hidden
|
|
105
107
|
>
|
|
106
|
-
{
|
|
107
|
-
<span key={
|
|
108
|
+
{MATRIX_CELLS.map((cell) => (
|
|
109
|
+
<span key={cell} className={cn("nonla-spin-matrix-cell", lit.has(cell) && "is-on")} />
|
|
108
110
|
))}
|
|
109
111
|
</span>
|
|
110
112
|
);
|
|
@@ -138,10 +140,11 @@ function Indicator({
|
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
export function Spin({ spinning = true, tip, size, variant = "default", indicator, children, className }: SpinProps) {
|
|
143
|
+
const resolvedSize = useControlSize(size);
|
|
141
144
|
if (children == null) {
|
|
142
145
|
return (
|
|
143
146
|
<div className={cn("inline-flex flex-col items-center gap-2", className)} role={spinning ? "status" : undefined} aria-live={spinning ? "polite" : undefined}>
|
|
144
|
-
{spinning ? <Indicator size={
|
|
147
|
+
{spinning ? <Indicator size={resolvedSize} variant={variant} indicator={indicator} /> : null}
|
|
145
148
|
{tip ? <span className="text-xs text-muted-foreground">{tip}</span> : null}
|
|
146
149
|
</div>
|
|
147
150
|
);
|
|
@@ -151,7 +154,7 @@ export function Spin({ spinning = true, tip, size, variant = "default", indicato
|
|
|
151
154
|
{children}
|
|
152
155
|
{spinning ? (
|
|
153
156
|
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-2 bg-background/50" role="status" aria-live="polite">
|
|
154
|
-
<Indicator size={
|
|
157
|
+
<Indicator size={resolvedSize} variant={variant} indicator={indicator} />
|
|
155
158
|
{tip ? <span className="text-xs text-muted-foreground">{tip}</span> : null}
|
|
156
159
|
</div>
|
|
157
160
|
) : null}
|
package/src/styles.css
CHANGED
|
@@ -3,26 +3,30 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* NonlaUI theme — single source of color.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Layers:
|
|
7
|
+
* Seed — `--nonla-*` knobs in the first block. Consumer overrides these.
|
|
8
|
+
* Map — derived (brand-50…800, radius-sm/lg, z-popup). Do not set unless exception.
|
|
9
|
+
* Alias — `--background`, `--brand`, `--destructive` (shadcn + Nonla names).
|
|
10
|
+
*
|
|
11
|
+
* Light-only defaults. `.dark` is not an alias of this sheet.
|
|
7
12
|
* Override `--nonla-*` knobs in the consuming app (CSS or `<App theme>`).
|
|
8
13
|
* Components read only these tokens — never edit Button/Tag/… to retheme.
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
16
|
:root,
|
|
12
|
-
.dark,
|
|
13
17
|
.nonla-ui {
|
|
14
18
|
color-scheme: light;
|
|
15
19
|
|
|
16
|
-
/* ── Knobs (override these)
|
|
20
|
+
/* ── Knobs / Seed (override these) ──────────────────────────────────── */
|
|
17
21
|
--nonla-bg: #fffcf1;
|
|
18
22
|
--nonla-fg: #1f1f1e;
|
|
19
23
|
/* Text rank on cream — same ink, contrast steps ~13.6 / 7 / 4.5 / 3 (WCAG AAA body → AA body → AA UI). */
|
|
20
24
|
--nonla-fg-muted: color-mix(in oklab, var(--nonla-fg) 78%, var(--nonla-bg));
|
|
21
25
|
--nonla-fg-tertiary: color-mix(in oklab, var(--nonla-fg) 64%, var(--nonla-bg));
|
|
22
26
|
--nonla-fg-quaternary: color-mix(in oklab, var(--nonla-fg) 50%, var(--nonla-bg));
|
|
23
|
-
--nonla-brand: #
|
|
24
|
-
/*
|
|
25
|
-
--nonla-solid-fg:
|
|
27
|
+
--nonla-brand: #f18d00;
|
|
28
|
+
/* Label on brand fills (Button primary, Tag solid, Checkbox). Override via `colors.solidFg`. */
|
|
29
|
+
--nonla-solid-fg: #ffffff;
|
|
26
30
|
--nonla-danger: #c0392b;
|
|
27
31
|
--nonla-success: #1f9d55;
|
|
28
32
|
--nonla-warn: #c47d12;
|
|
@@ -53,6 +57,11 @@
|
|
|
53
57
|
--nonla-ink-active: color-mix(in srgb, var(--nonla-ink) 8%, transparent);
|
|
54
58
|
--nonla-ink-line: color-mix(in srgb, var(--nonla-ink) 14%, transparent);
|
|
55
59
|
--nonla-desktop-bar-height: 42px;
|
|
60
|
+
--nonla-z-base: 0;
|
|
61
|
+
--nonla-z-desktop: 20;
|
|
62
|
+
--nonla-z-window: 30;
|
|
63
|
+
--nonla-z-header: 40;
|
|
64
|
+
--nonla-z-popup-base: 1000;
|
|
56
65
|
|
|
57
66
|
/* Preset accents — Button color="blue" / Tag color="purple" / … */
|
|
58
67
|
--nonla-blue: #1677ff;
|
|
@@ -69,9 +78,13 @@
|
|
|
69
78
|
--nonla-lime: #a0d911;
|
|
70
79
|
--nonla-gold: #faad14;
|
|
71
80
|
|
|
72
|
-
/* ──
|
|
81
|
+
/* ── Map + Alias (do not set unless you need a one-off exception) ───── */
|
|
73
82
|
--nonla-radius-sm: max(0px, calc(var(--nonla-radius) - 2px));
|
|
74
83
|
--nonla-radius-lg: calc(var(--nonla-radius) + 2px);
|
|
84
|
+
--nonla-z-drawer: var(--nonla-z-popup-base);
|
|
85
|
+
--nonla-z-modal: var(--nonla-z-popup-base);
|
|
86
|
+
--nonla-z-message: calc(var(--nonla-z-popup-base) + 10);
|
|
87
|
+
--nonla-z-popup: calc(var(--nonla-z-popup-base) + 50);
|
|
75
88
|
--radius: var(--nonla-radius);
|
|
76
89
|
--background: var(--nonla-bg);
|
|
77
90
|
--foreground: var(--nonla-fg);
|
|
@@ -106,6 +119,7 @@
|
|
|
106
119
|
--brand-800: color-mix(in oklab, var(--nonla-brand) 48%, black);
|
|
107
120
|
--brand: var(--brand-500);
|
|
108
121
|
--brand-text: var(--brand-700);
|
|
122
|
+
--solid-fg: var(--nonla-solid-fg);
|
|
109
123
|
--link: var(--nonla-link);
|
|
110
124
|
--success: var(--nonla-success);
|
|
111
125
|
--warn: var(--nonla-warn);
|
|
@@ -202,6 +216,7 @@
|
|
|
202
216
|
--color-brand-700: var(--brand-700);
|
|
203
217
|
--color-brand-800: var(--brand-800);
|
|
204
218
|
--color-brand-text: var(--brand-text);
|
|
219
|
+
--color-solid-fg: var(--solid-fg);
|
|
205
220
|
--color-link: var(--link);
|
|
206
221
|
--color-success: var(--success);
|
|
207
222
|
--color-warn: var(--warn);
|
|
@@ -225,13 +240,13 @@
|
|
|
225
240
|
--shadow-panel: 0 0 0 1px var(--border), 0 4px 16px color-mix(in oklab, var(--nonla-fg) 10%, transparent);
|
|
226
241
|
|
|
227
242
|
--spacing-control-xs: 24px;
|
|
228
|
-
--spacing-control-sm:
|
|
243
|
+
--spacing-control-sm: 24px;
|
|
229
244
|
--spacing-control-md: 32px;
|
|
230
|
-
--spacing-control-lg:
|
|
245
|
+
--spacing-control-lg: 40px;
|
|
231
246
|
--spacing-nav-item: 32px;
|
|
232
|
-
--spacing-field-sm:
|
|
247
|
+
--spacing-field-sm: 24px;
|
|
233
248
|
--spacing-field-md: 32px;
|
|
234
|
-
--spacing-field-lg:
|
|
249
|
+
--spacing-field-lg: 40px;
|
|
235
250
|
--spacing-field-nav: 32px;
|
|
236
251
|
--spacing-desktop-bar: var(--nonla-desktop-bar-height);
|
|
237
252
|
|
|
@@ -271,7 +286,7 @@
|
|
|
271
286
|
}
|
|
272
287
|
}
|
|
273
288
|
|
|
274
|
-
@keyframes nonla-
|
|
289
|
+
@keyframes nonla-shimmer {
|
|
275
290
|
0% {
|
|
276
291
|
background-position: 100% 0;
|
|
277
292
|
}
|
|
@@ -280,21 +295,31 @@
|
|
|
280
295
|
}
|
|
281
296
|
}
|
|
282
297
|
|
|
283
|
-
.nonla-
|
|
284
|
-
|
|
285
|
-
background-
|
|
298
|
+
.nonla-shimmer {
|
|
299
|
+
display: inline-block;
|
|
300
|
+
background-image: linear-gradient(
|
|
301
|
+
90deg,
|
|
302
|
+
var(--tertiary-foreground) 0%,
|
|
303
|
+
var(--foreground) 40%,
|
|
304
|
+
color-mix(in oklab, var(--foreground) 25%, white) 50%,
|
|
305
|
+
var(--foreground) 60%,
|
|
306
|
+
var(--tertiary-foreground) 100%
|
|
307
|
+
);
|
|
308
|
+
background-size: 220% 100%;
|
|
286
309
|
background-clip: text;
|
|
287
310
|
-webkit-background-clip: text;
|
|
311
|
+
-webkit-text-fill-color: transparent;
|
|
288
312
|
color: transparent;
|
|
289
|
-
animation: nonla-
|
|
313
|
+
animation: nonla-shimmer 1.6s ease-in-out infinite;
|
|
290
314
|
}
|
|
291
315
|
|
|
292
316
|
@media (prefers-reduced-motion: reduce) {
|
|
293
|
-
.nonla-
|
|
317
|
+
.nonla-shimmer {
|
|
294
318
|
animation: none;
|
|
295
319
|
background-image: none;
|
|
296
320
|
background-clip: border-box;
|
|
297
321
|
-webkit-background-clip: border-box;
|
|
322
|
+
-webkit-text-fill-color: unset;
|
|
298
323
|
color: var(--muted-foreground);
|
|
299
324
|
}
|
|
300
325
|
}
|
|
@@ -305,7 +330,7 @@
|
|
|
305
330
|
top: 16px;
|
|
306
331
|
left: 50%;
|
|
307
332
|
transform: translateX(-50%);
|
|
308
|
-
z-index:
|
|
333
|
+
z-index: var(--nonla-z-message);
|
|
309
334
|
display: flex;
|
|
310
335
|
flex-direction: column;
|
|
311
336
|
align-items: center;
|
|
@@ -589,20 +614,31 @@
|
|
|
589
614
|
}
|
|
590
615
|
}
|
|
591
616
|
|
|
592
|
-
/* Frosted overlay —
|
|
617
|
+
/* Frosted overlay — one hairline + lift. No stacked black/white rings. */
|
|
593
618
|
.nonla-glass {
|
|
594
|
-
border: 1px solid
|
|
619
|
+
border: 1px solid var(--glass-border);
|
|
595
620
|
background: var(--glass);
|
|
596
621
|
color: var(--foreground);
|
|
597
|
-
box-shadow:
|
|
598
|
-
0 0 0 0.5px color-mix(in srgb, black 22%, transparent),
|
|
599
|
-
0 10px 28px color-mix(in srgb, black 14%, transparent),
|
|
600
|
-
inset 0 0 0 0.5px rgb(255 255 255 / 0.55),
|
|
601
|
-
inset 0 1px 0 rgb(255 255 255 / 0.92);
|
|
622
|
+
box-shadow: 0 10px 28px color-mix(in srgb, black 12%, transparent);
|
|
602
623
|
-webkit-backdrop-filter: blur(24px) saturate(160%);
|
|
603
624
|
backdrop-filter: blur(24px) saturate(160%);
|
|
604
625
|
}
|
|
605
626
|
|
|
627
|
+
/* Desktop window — cream wash + less saturate so meadow doesn't tint the panel. */
|
|
628
|
+
.nonla-window-glass {
|
|
629
|
+
background: color-mix(in srgb, var(--nonla-bg) 81%, transparent);
|
|
630
|
+
-webkit-backdrop-filter: blur(40px) saturate(108%);
|
|
631
|
+
backdrop-filter: blur(40px) saturate(108%);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/* Fluent Color desktop tile — same frost as the window, macOS squircle. */
|
|
635
|
+
.nonla-desktop-icon-plate {
|
|
636
|
+
overflow: hidden;
|
|
637
|
+
border: 0;
|
|
638
|
+
border-radius: 22%;
|
|
639
|
+
box-shadow: none;
|
|
640
|
+
}
|
|
641
|
+
|
|
606
642
|
.nonla-btn-glass {
|
|
607
643
|
border-color: var(--glass-border);
|
|
608
644
|
background: var(--glass);
|
|
@@ -646,6 +682,7 @@
|
|
|
646
682
|
.nonla-overlay {
|
|
647
683
|
position: fixed;
|
|
648
684
|
inset: 0;
|
|
685
|
+
z-index: var(--nonla-z-drawer);
|
|
649
686
|
background: var(--mask);
|
|
650
687
|
}
|
|
651
688
|
.nonla-overlay[data-state="open"] {
|
|
@@ -658,7 +695,7 @@
|
|
|
658
695
|
.nonla-modal-overlay {
|
|
659
696
|
position: fixed;
|
|
660
697
|
inset: 0;
|
|
661
|
-
z-index:
|
|
698
|
+
z-index: var(--nonla-z-modal);
|
|
662
699
|
background: var(--mask);
|
|
663
700
|
}
|
|
664
701
|
.nonla-modal-overlay[data-state="open"] {
|
|
@@ -673,7 +710,7 @@
|
|
|
673
710
|
position: fixed;
|
|
674
711
|
left: 50%;
|
|
675
712
|
top: 60px;
|
|
676
|
-
z-index:
|
|
713
|
+
z-index: calc(var(--nonla-z-modal) + 1);
|
|
677
714
|
transform: translate(-50%, 0);
|
|
678
715
|
display: flex;
|
|
679
716
|
flex-direction: column;
|
|
@@ -761,6 +798,7 @@
|
|
|
761
798
|
|
|
762
799
|
.nonla-drawer-panel {
|
|
763
800
|
will-change: transform;
|
|
801
|
+
z-index: calc(var(--nonla-z-drawer) + 1);
|
|
764
802
|
}
|
|
765
803
|
.nonla-drawer-panel[data-side="right"][data-state="open"] {
|
|
766
804
|
animation: nonla-drawer-in-right var(--nonla-dur-slow) var(--nonla-ease-out);
|
|
@@ -787,6 +825,22 @@
|
|
|
787
825
|
animation: nonla-drawer-out-top var(--nonla-dur) var(--nonla-ease-in);
|
|
788
826
|
}
|
|
789
827
|
|
|
828
|
+
.nonla-popup-layer {
|
|
829
|
+
z-index: var(--nonla-z-popup, 1050);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.nonla-desktop-layer {
|
|
833
|
+
z-index: var(--nonla-z-desktop, 20);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
.nonla-window-layer {
|
|
837
|
+
z-index: var(--nonla-z-window, 30);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
.nonla-header-layer {
|
|
841
|
+
z-index: var(--nonla-z-header, 40);
|
|
842
|
+
}
|
|
843
|
+
|
|
790
844
|
.nonla-popper {
|
|
791
845
|
will-change: transform, opacity;
|
|
792
846
|
transform-origin: var(--radix-popover-content-transform-origin, var(--radix-dropdown-menu-content-transform-origin, var(--radix-select-content-transform-origin, var(--radix-tooltip-content-transform-origin, center))));
|
package/src/switch/Switch.tsx
CHANGED
|
@@ -1,29 +1,30 @@
|
|
|
1
1
|
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
|
2
2
|
import { type CSSProperties, type ComponentPropsWithoutRef, forwardRef } from "react";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
|
-
import { type ControlSize, controlRadiusVar,
|
|
4
|
+
import { type ControlSize, controlRadiusVar, useControlSize } from "../lib/sizes";
|
|
5
5
|
|
|
6
6
|
export type SwitchVariant = "default" | "square";
|
|
7
7
|
|
|
8
8
|
export type SwitchProps = Omit<ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>, "onCheckedChange" | "checked" | "onChange"> & {
|
|
9
9
|
checked?: boolean;
|
|
10
10
|
defaultChecked?: boolean;
|
|
11
|
-
/**
|
|
11
|
+
/** Receives the next checked boolean (not a DOM event). */
|
|
12
12
|
onChange?: (checked: boolean) => void;
|
|
13
13
|
size?: ControlSize;
|
|
14
14
|
variant?: SwitchVariant;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
/** Switch track heights — shorter than Button/Input
|
|
17
|
+
/** Switch track heights — shorter than Button/Input. */
|
|
18
18
|
const SWITCH_TRACK_H = { small: 16, default: 22, large: 28 } as const;
|
|
19
19
|
|
|
20
20
|
export const Switch = forwardRef<HTMLButtonElement, SwitchProps>(function Switch({ className, checked, defaultChecked, onChange, size, variant = "default", disabled, style, ...rest }, ref) {
|
|
21
|
-
const
|
|
21
|
+
const resolvedSize = useControlSize(size);
|
|
22
|
+
const trackH = SWITCH_TRACK_H[resolvedSize];
|
|
22
23
|
const trackW = Math.round(trackH * 1.8);
|
|
23
24
|
const thumb = trackH - 4;
|
|
24
25
|
const travel = trackW - thumb - 3;
|
|
25
26
|
const square = variant === "square";
|
|
26
|
-
const squareRadius = `max(3px, calc(${controlRadiusVar(
|
|
27
|
+
const squareRadius = `max(3px, calc(${controlRadiusVar(resolvedSize)} * 0.55))`;
|
|
27
28
|
|
|
28
29
|
return (
|
|
29
30
|
<SwitchPrimitive.Root
|
package/src/table/Table.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { type CSSProperties, type Key, type ReactNode, useMemo, useState } from
|
|
|
2
2
|
import { Checkbox } from "../checkbox/Checkbox";
|
|
3
3
|
import { Empty } from "../empty/Empty";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
-
import { type ControlSize,
|
|
5
|
+
import { type CanonicalSize, type ControlSize, useControlSize } from "../lib/sizes";
|
|
6
6
|
import { Pagination } from "../pagination/Pagination";
|
|
7
7
|
import { Spin } from "../spin/Spin";
|
|
8
8
|
|
|
@@ -15,7 +15,7 @@ export type ColumnType<T> = {
|
|
|
15
15
|
width?: number | string;
|
|
16
16
|
minWidth?: number | string;
|
|
17
17
|
align?: "left" | "center" | "right";
|
|
18
|
-
/** Custom cell —
|
|
18
|
+
/** Custom cell — `(value, record, index) => ReactNode`. */
|
|
19
19
|
render?: (value: any, record: T, index: number) => ReactNode;
|
|
20
20
|
className?: string;
|
|
21
21
|
ellipsis?: boolean;
|
|
@@ -127,8 +127,7 @@ function SortIcon({ order }: { order: SortOrder | undefined }) {
|
|
|
127
127
|
);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
function sizeClasses(
|
|
131
|
-
const s = normalizeSize(size);
|
|
130
|
+
function sizeClasses(s: CanonicalSize) {
|
|
132
131
|
if (s === "small") {
|
|
133
132
|
return {
|
|
134
133
|
head: "h-9 px-2 text-xs",
|
|
@@ -152,7 +151,7 @@ function sizeClasses(size: ControlSize | undefined) {
|
|
|
152
151
|
}
|
|
153
152
|
|
|
154
153
|
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);
|
|
154
|
+
const sz = sizeClasses(useControlSize(size));
|
|
156
155
|
|
|
157
156
|
const selectionControlled = rowSelection?.selectedRowKeys !== undefined;
|
|
158
157
|
const [innerSelected, setInnerSelected] = useState<Key[]>(() => rowSelection?.defaultSelectedRowKeys ?? []);
|
|
@@ -324,8 +323,8 @@ export function Table<T extends object = Record<string, unknown>>({ columns = []
|
|
|
324
323
|
return (
|
|
325
324
|
<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
325
|
{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"
|
|
326
|
+
<td data-slot="table-cell" className={cn(sz.cell, sz.check, "align-middle")} style={selectionColWidth != null ? { width: selectionColWidth } : undefined} onClick={(e) => e.stopPropagation()}>
|
|
327
|
+
<div className="flex items-center justify-center">
|
|
329
328
|
{rowSelection.type === "radio" ? (
|
|
330
329
|
<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
330
|
) : (
|
package/src/tag/Tag.tsx
CHANGED
|
@@ -6,7 +6,7 @@ export type TagVariant = "soft" | "solid";
|
|
|
6
6
|
export type TagProps = {
|
|
7
7
|
children?: ReactNode;
|
|
8
8
|
color?: string;
|
|
9
|
-
/** @deprecated prefer `variant="soft"`
|
|
9
|
+
/** @deprecated prefer `variant="soft"` */
|
|
10
10
|
bordered?: boolean;
|
|
11
11
|
/**
|
|
12
12
|
* `soft` — quiet wash (default).
|
|
@@ -78,7 +78,7 @@ export function Tag({
|
|
|
78
78
|
return (
|
|
79
79
|
<span
|
|
80
80
|
className={cn(
|
|
81
|
-
"inline-flex h-
|
|
81
|
+
"inline-flex h-5.5 max-w-full items-center gap-1.5 rounded-full px-2.5 text-[11px] font-medium tracking-[0.01em]",
|
|
82
82
|
isDefault && (solid ? "bg-foreground/10 text-(--nonla-ink)" : "bg-muted text-muted-foreground"),
|
|
83
83
|
!isDefault && (solid ? SOLID : SOFT),
|
|
84
84
|
className,
|