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.
Files changed (67) hide show
  1. package/README.md +22 -10
  2. package/package.json +2 -2
  3. package/src/alert/Alert.tsx +1 -1
  4. package/src/app/App.tsx +38 -18
  5. package/src/app/context.ts +32 -0
  6. package/src/button/Button.tsx +2 -2
  7. package/src/calendar/Calendar.tsx +4 -4
  8. package/src/chat/AgentPanel.tsx +15 -6
  9. package/src/chat/common/types.ts +27 -2
  10. package/src/chat/common/useAgentStream.ts +78 -12
  11. package/src/chat/common/utils.ts +13 -0
  12. package/src/chat/index.ts +5 -3
  13. package/src/chat/message-ui/ChatAgentMessage.tsx +1 -1
  14. package/src/chat/message-ui/ChatInput.tsx +1 -1
  15. package/src/chat/message-ui/ChatMarkdown.tsx +6 -6
  16. package/src/chat/message-ui/ChatMarkdownTable.tsx +1 -1
  17. package/src/chat/message-ui/ChatThinking.tsx +4 -3
  18. package/src/chat/message-ui/ChatUserMessage.tsx +0 -8
  19. package/src/chat/message-ui/MermaidBlock.tsx +11 -1
  20. package/src/chat/tool-ui/BackgroundTaskToolUI.tsx +2 -1
  21. package/src/chat/tool-ui/CallAgentToolUI.tsx +3 -2
  22. package/src/chat/tool-ui/ChatToolCall.tsx +3 -2
  23. package/src/chat/tool-ui/GetCurrentTimeToolUI.tsx +2 -1
  24. package/src/chat/tool-ui/ReadSkillToolUI.tsx +3 -2
  25. package/src/chat/tool-ui/RunJsToolUI.tsx +3 -2
  26. package/src/chat/tool-ui/WebFetchToolUI.tsx +3 -2
  27. package/src/chat/tool-ui/registry.ts +24 -12
  28. package/src/checkbox/Checkbox.tsx +5 -4
  29. package/src/codeblock/CodeBlock.tsx +1 -0
  30. package/src/datepicker/DatePicker.tsx +15 -9
  31. package/src/desktop/DesktopHeader.tsx +1 -1
  32. package/src/desktop/DesktopIcon.tsx +2 -4
  33. package/src/desktop/DesktopWindow.tsx +19 -5
  34. package/src/desktop/MeadowDesktop.tsx +2 -2
  35. package/src/drawer/Drawer.tsx +8 -6
  36. package/src/dropdown/ContextMenu.tsx +3 -1
  37. package/src/dropdown/Dropdown.tsx +8 -6
  38. package/src/form/Form.tsx +2 -2
  39. package/src/form/FormItem.tsx +2 -2
  40. package/src/form/partials/FormItemHelps.tsx +2 -2
  41. package/src/form/variants/FieldColor.tsx +1 -1
  42. package/src/form/variants/FieldObject.tsx +2 -2
  43. package/src/form/variants/FieldRepeaterItem.tsx +4 -4
  44. package/src/form-layout/FormLayout.tsx +6 -6
  45. package/src/index.ts +20 -6
  46. package/src/input/Input.tsx +11 -10
  47. package/src/lib/sizes.ts +13 -3
  48. package/src/lib/surface.ts +2 -1
  49. package/src/menu/Menu.tsx +2 -2
  50. package/src/message/message.tsx +35 -2
  51. package/src/modal/Modal.tsx +49 -3
  52. package/src/pagination/Pagination.tsx +6 -5
  53. package/src/popconfirm/Popconfirm.tsx +1 -1
  54. package/src/popover/Popover.tsx +6 -8
  55. package/src/scroll/OverlayScroll.tsx +5 -3
  56. package/src/segmented/Segmented.tsx +6 -5
  57. package/src/select/Select.tsx +9 -6
  58. package/src/shimmer/Shimmer.tsx +17 -0
  59. package/src/skeleton/Skeleton.tsx +3 -3
  60. package/src/spin/Spin.tsx +8 -5
  61. package/src/styles.css +81 -27
  62. package/src/switch/Switch.tsx +6 -5
  63. package/src/table/Table.tsx +6 -7
  64. package/src/tag/Tag.tsx +2 -2
  65. package/src/theme.ts +23 -3
  66. package/src/timepicker/TimePicker.tsx +13 -11
  67. package/src/tooltip/Tooltip.tsx +6 -7
@@ -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 { useAppConfig } from "../app/App";
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 (antd default for Popconfirm). */
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
- /** antd `styles` — `root`/`body` map to content panel; `container` accepted as alias. */
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 app = useAppConfig();
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={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), 700);
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), 700);
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-[scrolling]/scroll:pointer-events-auto group-data-[scrolling]/scroll:opacity-100",
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, normalizeSize } from "../lib/sizes";
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 tok = getSizeTokens(size);
26
+ const resolvedSize = useControlSize(size);
27
+ const tok = getSizeTokens(resolvedSize);
27
28
  const itemPadX = Math.max(tok.paddingInline - 4, 6);
28
- const radius = controlRadiusVar(size);
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={normalizeSize(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(size)} - 4px))`,
53
+ height: `max(22px, calc(${controlHeightVar(resolvedSize)} - 4px))`,
53
54
  paddingLeft: itemPadX,
54
55
  paddingRight: itemPadX,
55
56
  fontSize: tok.fontSize,
@@ -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
- // antd interop: call sites often use (v: string) => void
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
- // Looser antd interop — many call sites type handlers as `(v: string) => void`.
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
- /** antd alias */
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(size)}
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 { type CSSProperties, type ReactNode } from "react";
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((_, i) => (
24
- <div key={i} className={cn("h-3 rounded bg-secondary", pulse, i === rows - 1 ? "w-2/3" : "w-full")} />
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
- {Array.from({ length: 9 }, (_, i) => (
107
- <span key={`c${i % 3}-r${Math.floor(i / 3)}`} className={cn("nonla-spin-matrix-cell", lit.has(i) && "is-on")} />
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={size} variant={variant} indicator={indicator} /> : null}
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={size} variant={variant} indicator={indicator} />
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
- * Light-only. Warm paper + charcoal ink, amber brand.
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: #eb9d29;
24
- /* Ink on brand fills charcoal, not white (white fails WCAG on amber). */
25
- --nonla-solid-fg: var(--nonla-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
- /* ── Derived map (do not set unless you need a one-off exception) ───── */
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: 28px;
243
+ --spacing-control-sm: 24px;
229
244
  --spacing-control-md: 32px;
230
- --spacing-control-lg: 36px;
245
+ --spacing-control-lg: 40px;
231
246
  --spacing-nav-item: 32px;
232
- --spacing-field-sm: 28px;
247
+ --spacing-field-sm: 24px;
233
248
  --spacing-field-md: 32px;
234
- --spacing-field-lg: 36px;
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-chat-shimmer {
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-chat-shimmer {
284
- background-image: linear-gradient(90deg, var(--muted-foreground) 0%, var(--foreground) 45%, var(--muted-foreground) 90%);
285
- background-size: 200% 100%;
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-chat-shimmer 1.8s ease-in-out infinite;
313
+ animation: nonla-shimmer 1.6s ease-in-out infinite;
290
314
  }
291
315
 
292
316
  @media (prefers-reduced-motion: reduce) {
293
- .nonla-chat-shimmer {
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: 10000;
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 — macOS menu chrome: outer hairline + inner white rim */
617
+ /* Frosted overlay — one hairline + lift. No stacked black/white rings. */
593
618
  .nonla-glass {
594
- border: 1px solid rgb(255 255 255 / 0.78);
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: 9990;
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: 9991;
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))));
@@ -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, normalizeSize } from "../lib/sizes";
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
- /** antd-style — receives the next checked boolean (not a DOM event). */
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 (antd-like). */
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 trackH = SWITCH_TRACK_H[normalizeSize(size)];
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(size)} * 0.55))`;
27
+ const squareRadius = `max(3px, calc(${controlRadiusVar(resolvedSize)} * 0.55))`;
27
28
 
28
29
  return (
29
30
  <SwitchPrimitive.Root
@@ -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, normalizeSize } from "../lib/sizes";
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 — Ant Design style `(value, record, index) => ReactNode`. */
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(size: ControlSize | undefined) {
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" onClick={(e) => e.stopPropagation()}>
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"` — kept for antd-ish callers */
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-[22px] max-w-full items-center gap-1.5 rounded-full px-2.5 text-[11px] font-medium tracking-[0.01em]",
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,