devnonla-ui 0.1.0 → 0.3.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 (71) 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/colorpicker/ColorPicker.tsx +694 -30
  31. package/src/colorpicker/color.ts +246 -0
  32. package/src/datepicker/DatePicker.tsx +15 -9
  33. package/src/desktop/DesktopHeader.tsx +1 -1
  34. package/src/desktop/DesktopIcon.tsx +2 -4
  35. package/src/desktop/DesktopWindow.tsx +19 -5
  36. package/src/desktop/MeadowDesktop.tsx +2 -2
  37. package/src/drawer/Drawer.tsx +8 -6
  38. package/src/dropdown/ContextMenu.tsx +3 -1
  39. package/src/dropdown/Dropdown.tsx +8 -6
  40. package/src/form/Form.tsx +2 -2
  41. package/src/form/FormItem.tsx +2 -2
  42. package/src/form/partials/FormItemHelps.tsx +2 -2
  43. package/src/form/variants/FieldColor.tsx +16 -51
  44. package/src/form/variants/FieldObject.tsx +2 -2
  45. package/src/form/variants/FieldRepeaterItem.tsx +4 -4
  46. package/src/form-layout/FormLayout.tsx +6 -6
  47. package/src/index.ts +25 -8
  48. package/src/input/Input.tsx +11 -10
  49. package/src/lib/sizes.ts +13 -3
  50. package/src/lib/surface.ts +2 -1
  51. package/src/menu/Menu.tsx +2 -2
  52. package/src/message/message.tsx +35 -2
  53. package/src/modal/Modal.tsx +49 -3
  54. package/src/pagination/Pagination.tsx +6 -5
  55. package/src/popconfirm/Popconfirm.tsx +1 -1
  56. package/src/popover/Popover.tsx +6 -8
  57. package/src/scroll/OverlayScroll.tsx +5 -3
  58. package/src/segmented/Segmented.tsx +6 -5
  59. package/src/select/Select.tsx +9 -6
  60. package/src/shimmer/Shimmer.tsx +17 -0
  61. package/src/skeleton/Skeleton.tsx +3 -3
  62. package/src/spin/Spin.tsx +8 -5
  63. package/src/splitter/Splitter.tsx +330 -0
  64. package/src/splitter/sizes.ts +67 -0
  65. package/src/styles.css +96 -27
  66. package/src/switch/Switch.tsx +6 -5
  67. package/src/table/Table.tsx +6 -7
  68. package/src/tag/Tag.tsx +2 -2
  69. package/src/theme.ts +23 -3
  70. package/src/timepicker/TimePicker.tsx +13 -11
  71. package/src/tooltip/Tooltip.tsx +6 -7
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))));
@@ -895,3 +949,18 @@
895
949
  .group\/scroll:hover .nonla-overlay-thumb {
896
950
  background: var(--nonla-scrollbar-thumb-hover);
897
951
  }
952
+
953
+ .nonla-color-checkered {
954
+ background-color: #fff;
955
+ background-image: conic-gradient(#d9d9d9 0.25turn, #fff 0.25turn 0.5turn, #d9d9d9 0.5turn 0.75turn, #fff 0.75turn);
956
+ background-size: 8px 8px;
957
+ }
958
+
959
+ .nonla-color-cleared {
960
+ background: linear-gradient(
961
+ to bottom left,
962
+ transparent calc(50% - 1px),
963
+ var(--destructive) calc(50% - 1px) calc(50% + 1px),
964
+ transparent calc(50% + 1px)
965
+ );
966
+ }
@@ -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,
package/src/theme.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Theme knobs for NonlaUI.
3
3
  *
4
- * Defaults live in `styles.css` on `:root` / `.dark` / `.nonla-ui`.
4
+ * Defaults live in `styles.css` on `:root` / `.nonla-ui`.
5
5
  * `--brand-50`…`--brand-800` follow `--nonla-brand` (500 = the knob).
6
6
  * Consumers retheme without touching components:
7
7
  * 1. CSS: `:root { --nonla-brand: #3b82f6; }`
@@ -43,6 +43,11 @@ export const NONLA_THEME_KEYS = {
43
43
  inkActive: "--nonla-ink-active",
44
44
  inkLine: "--nonla-ink-line",
45
45
  desktopBarHeight: "--nonla-desktop-bar-height",
46
+ zBase: "--nonla-z-base",
47
+ zDesktop: "--nonla-z-desktop",
48
+ zWindow: "--nonla-z-window",
49
+ zHeader: "--nonla-z-header",
50
+ zPopupBase: "--nonla-z-popup-base",
46
51
  blue: "--nonla-blue",
47
52
  purple: "--nonla-purple",
48
53
  cyan: "--nonla-cyan",
@@ -61,9 +66,12 @@ export const NONLA_THEME_KEYS = {
61
66
  export type NonlaThemeKnobName = keyof typeof NONLA_THEME_KEYS;
62
67
  export type NonlaThemeKnob = (typeof NONLA_THEME_KEYS)[NonlaThemeKnobName];
63
68
 
64
- const METRIC_KEYS = new Set<NonlaThemeKnobName>(["radius", "height", "heightSm", "heightLg", "desktopBarHeight"]);
69
+ const METRIC_KEYS = new Set<NonlaThemeKnobName>(["radius", "height", "heightSm", "heightLg", "desktopBarHeight", "zBase", "zDesktop", "zWindow", "zHeader", "zPopupBase"]);
65
70
 
66
- export type NonlaThemeColorName = Exclude<NonlaThemeKnobName, "radius" | "height" | "heightSm" | "heightLg">;
71
+ export type NonlaThemeColorName = Exclude<
72
+ NonlaThemeKnobName,
73
+ "radius" | "height" | "heightSm" | "heightLg" | "desktopBarHeight" | "zBase" | "zDesktop" | "zWindow" | "zHeader" | "zPopupBase"
74
+ >;
67
75
  export type NonlaThemeColors = { [K in NonlaThemeColorName]?: string };
68
76
 
69
77
  /** Flat aliases → knob name (`colorBorder` == `colors.border`). */
@@ -117,6 +125,18 @@ function entriesOf(theme: NonlaThemeConfig): [string, string][] {
117
125
  return [...map.entries()];
118
126
  }
119
127
 
128
+ export type NonlaTokenSnapshot = { [K in NonlaThemeKnobName]: string };
129
+
130
+ /** Read computed `--nonla-*` knobs (`getDesignToken`). */
131
+ export function getDesignToken(target?: HTMLElement): NonlaTokenSnapshot {
132
+ const el = target ?? (typeof document === "undefined" ? null : document.documentElement);
133
+ const token = {} as NonlaTokenSnapshot;
134
+ for (const name of Object.keys(NONLA_THEME_KEYS) as NonlaThemeKnobName[]) {
135
+ token[name] = el ? getComputedStyle(el).getPropertyValue(NONLA_THEME_KEYS[name]).trim() : "";
136
+ }
137
+ return token;
138
+ }
139
+
120
140
  /** Apply knobs on an element (defaults to `:root`). Returns a restore function. */
121
141
  export function applyNonlaTheme(theme: NonlaThemeConfig, target: HTMLElement = document.documentElement): () => void {
122
142
  const entries = entriesOf(theme);
@@ -12,10 +12,10 @@ import {
12
12
  useRef,
13
13
  useState,
14
14
  } from "react";
15
- import { useAppConfig } from "../app/App";
15
+ import { usePopupContainer } from "../app/context";
16
16
  import { cn } from "../lib/cn";
17
17
  import { type PopperPlacement, placementToRadix } from "../lib/placement";
18
- import { type ControlSize, controlFieldFocusBorder, controlFieldStyle, controlFieldSurface, controlFieldTransition, controlStatusClass, getSizeTokens } from "../lib/sizes";
18
+ import { type ControlSize, controlFieldFocusBorder, controlFieldStyle, controlFieldSurface, controlFieldTransition, controlStatusClass, getSizeTokens, useControlSize } from "../lib/sizes";
19
19
  import { glassOverlayClass } from "../lib/surface";
20
20
 
21
21
  export type TimeValue = { hours: number; minutes: number; seconds: number };
@@ -244,7 +244,7 @@ function StepperButton({
244
244
  tabIndex={-1}
245
245
  aria-label={label}
246
246
  className={cn(
247
- "flex h-10 items-center justify-center rounded-[6px] text-muted-foreground transition-colors",
247
+ "flex h-10 items-center justify-center rounded-md text-muted-foreground transition-colors",
248
248
  "hover:bg-foreground/5 hover:text-foreground active:bg-foreground/8",
249
249
  )}
250
250
  style={{ width: size }}
@@ -370,7 +370,7 @@ function StepperBoard({
370
370
  tabIndex={-1}
371
371
  aria-label={`Choose ${col.label}`}
372
372
  className={cn(
373
- "flex cursor-pointer items-center justify-center rounded-[6px] font-semibold leading-none tracking-tight transition-colors",
373
+ "flex cursor-pointer items-center justify-center rounded-md font-semibold leading-none tracking-tight transition-colors",
374
374
  col.wide ? "text-[23px]" : "tabular-nums",
375
375
  col.active ? "bg-foreground/6 text-foreground" : "text-foreground hover:bg-foreground/5",
376
376
  )}
@@ -414,7 +414,7 @@ export const TimePicker = forwardRef<HTMLInputElement, TimePickerProps>(function
414
414
  },
415
415
  ref,
416
416
  ) {
417
- const app = useAppConfig();
417
+ const appPortal = usePopupContainer();
418
418
  const showSeconds = format === "HH:mm:ss";
419
419
  const controlled = value !== undefined;
420
420
  const [inner, setInner] = useState<TimeValue | null>(() => parseTime(defaultValue));
@@ -428,8 +428,9 @@ export const TimePicker = forwardRef<HTMLInputElement, TimePickerProps>(function
428
428
  const [fallback, setFallback] = useState<TimeValue>(nowParts);
429
429
  const inputRef = useRef<HTMLInputElement | null>(null);
430
430
  const fieldRef = useRef<HTMLDivElement | null>(null);
431
- const tok = getSizeTokens(size);
432
- const fieldStyle = controlFieldStyle(size);
431
+ const resolvedSize = useControlSize(size);
432
+ const tok = getSizeTokens(resolvedSize);
433
+ const fieldStyle = controlFieldStyle(resolvedSize);
433
434
  const { side, align } = placementToRadix(placement);
434
435
 
435
436
  useEffect(() => {
@@ -593,6 +594,7 @@ export const TimePicker = forwardRef<HTMLInputElement, TimePickerProps>(function
593
594
  <span className="inline-flex shrink-0 text-muted-foreground">
594
595
  <ClockIcon size={tok.icon} />
595
596
  </span>
597
+ {/* biome-ignore lint/a11y/useAriaPropsSupportedByRole: native time field exposes popup state */}
596
598
  <input
597
599
  ref={setRefs}
598
600
  id={id}
@@ -647,7 +649,7 @@ export const TimePicker = forwardRef<HTMLInputElement, TimePickerProps>(function
647
649
  ) : null}
648
650
  </div>
649
651
  </PopoverPrimitive.Anchor>
650
- <PopoverPrimitive.Portal container={app.getPopupContainer?.()}>
652
+ <PopoverPrimitive.Portal container={appPortal?.()}>
651
653
  <PopoverPrimitive.Content
652
654
  side={side}
653
655
  align={align}
@@ -668,14 +670,14 @@ export const TimePicker = forwardRef<HTMLInputElement, TimePickerProps>(function
668
670
  if (fieldRef.current?.contains(e.target as Node)) e.preventDefault();
669
671
  }}
670
672
  className={cn(
671
- "w-[300px] p-0",
673
+ "w-75 p-0",
672
674
  glassOverlayClass,
673
675
  popupClassName,
674
676
  )}
675
677
  >
676
678
  <div className="relative overflow-hidden rounded-[inherit]">
677
679
  {picking && pickOptions ? (
678
- <div className="relative flex h-[220px] items-center px-3 pt-8 pb-3">
680
+ <div className="relative flex h-55 items-center px-3 pt-8 pb-3">
679
681
  <button
680
682
  type="button"
681
683
  tabIndex={-1}
@@ -690,7 +692,7 @@ export const TimePicker = forwardRef<HTMLInputElement, TimePickerProps>(function
690
692
  </div>
691
693
  </div>
692
694
  ) : (
693
- <div className="flex h-[220px] flex-col items-center justify-center px-4">
695
+ <div className="flex h-55 flex-col items-center justify-center px-4">
694
696
  <StepperBoard
695
697
  columns={[
696
698
  {
@@ -1,6 +1,6 @@
1
1
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
2
- import { type ReactNode } from "react";
3
- import { useAppConfig } from "../app/App";
2
+ import type { ReactNode } from "react";
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";
@@ -16,10 +16,10 @@ export type TooltipProps = {
16
16
  onOpenChange?: (open: boolean) => void;
17
17
  mouseEnterDelay?: number;
18
18
  className?: string;
19
- /** antd alias */
19
+ /** Extra class on the overlay panel. */
20
20
  overlayClassName?: string;
21
21
  getPopupContainer?: () => HTMLElement;
22
- /** antd accepts `boolean | { pointAtCenter }` — we only honor truthiness. */
22
+ /** `boolean | { pointAtCenter }` — we only honor truthiness. */
23
23
  arrow?: boolean | { pointAtCenter?: boolean };
24
24
  };
25
25
 
@@ -35,15 +35,14 @@ export function Tooltip({
35
35
  overlayClassName,
36
36
  getPopupContainer,
37
37
  }: TooltipProps) {
38
- const app = useAppConfig();
38
+ const getContainer = usePopupContainer(getPopupContainer);
39
39
  const { side, align } = placementToRadix(placement);
40
- const container = getPopupContainer ?? app.getPopupContainer;
41
40
  if (title == null || title === false) return <>{children}</>;
42
41
  return (
43
42
  <TooltipPrimitive.Provider delayDuration={Math.round(mouseEnterDelay * 1000)}>
44
43
  <TooltipPrimitive.Root open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}>
45
44
  <TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
46
- <TooltipPrimitive.Portal container={container?.()}>
45
+ <TooltipPrimitive.Portal container={getContainer()}>
47
46
  <TooltipPrimitive.Content
48
47
  side={side}
49
48
  align={align}