devnonla-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -0
  3. package/package.json +66 -0
  4. package/src/alert/Alert.tsx +109 -0
  5. package/src/app/App.tsx +36 -0
  6. package/src/button/Button.tsx +278 -0
  7. package/src/button/ButtonCopy.tsx +60 -0
  8. package/src/calendar/Calendar.tsx +133 -0
  9. package/src/chat/AgentPanel.tsx +203 -0
  10. package/src/chat/common/bgTasks.ts +41 -0
  11. package/src/chat/common/sse.ts +156 -0
  12. package/src/chat/common/types.ts +43 -0
  13. package/src/chat/common/useAgentStream.ts +351 -0
  14. package/src/chat/common/utils.ts +73 -0
  15. package/src/chat/index.ts +56 -0
  16. package/src/chat/message-ui/ChatAgentMessage.tsx +29 -0
  17. package/src/chat/message-ui/ChatError.tsx +24 -0
  18. package/src/chat/message-ui/ChatInput.tsx +162 -0
  19. package/src/chat/message-ui/ChatMarkdown.tsx +128 -0
  20. package/src/chat/message-ui/ChatMarkdownTable.tsx +45 -0
  21. package/src/chat/message-ui/ChatSpinner.tsx +5 -0
  22. package/src/chat/message-ui/ChatThinking.tsx +61 -0
  23. package/src/chat/message-ui/ChatUserMessage.tsx +72 -0
  24. package/src/chat/message-ui/ChatWelcome.tsx +70 -0
  25. package/src/chat/message-ui/MermaidBlock.tsx +266 -0
  26. package/src/chat/message-ui/mermaidFence.ts +55 -0
  27. package/src/chat/message-ui/sanitizeMermaid.ts +135 -0
  28. package/src/chat/tool-ui/BackgroundTaskToolUI.tsx +36 -0
  29. package/src/chat/tool-ui/BackgroundTasksBar.tsx +152 -0
  30. package/src/chat/tool-ui/CallAgentToolUI.tsx +168 -0
  31. package/src/chat/tool-ui/ChatToolCall.tsx +77 -0
  32. package/src/chat/tool-ui/GetCurrentTimeToolUI.tsx +49 -0
  33. package/src/chat/tool-ui/ReadSkillToolUI.tsx +80 -0
  34. package/src/chat/tool-ui/RunJsToolUI.tsx +106 -0
  35. package/src/chat/tool-ui/ToolUiBadge.tsx +10 -0
  36. package/src/chat/tool-ui/ToolUiTrailing.tsx +24 -0
  37. package/src/chat/tool-ui/WebFetchToolUI.tsx +53 -0
  38. package/src/chat/tool-ui/registry.ts +29 -0
  39. package/src/chat/tool-ui/types.ts +22 -0
  40. package/src/checkbox/Checkbox.tsx +50 -0
  41. package/src/codeblock/CodeBlock.tsx +76 -0
  42. package/src/codeblock/highlight.ts +159 -0
  43. package/src/colorpicker/ColorPicker.tsx +45 -0
  44. package/src/datepicker/DatePicker.tsx +481 -0
  45. package/src/desktop/DesktopHeader.tsx +92 -0
  46. package/src/desktop/DesktopIcon.tsx +34 -0
  47. package/src/desktop/DesktopStage.tsx +6 -0
  48. package/src/desktop/DesktopWindow.tsx +504 -0
  49. package/src/desktop/MeadowDesktop.tsx +15 -0
  50. package/src/desktop/MeadowShell.tsx +12 -0
  51. package/src/desktop/MeadowWallpaper.tsx +5 -0
  52. package/src/desktop/assets/desktop-meadow.jpg +0 -0
  53. package/src/desktop/assets.d.ts +9 -0
  54. package/src/desktop/surface.ts +1 -0
  55. package/src/drawer/Drawer.tsx +125 -0
  56. package/src/dropdown/ContextMenu.tsx +97 -0
  57. package/src/dropdown/Dropdown.tsx +139 -0
  58. package/src/dropdown/menuClasses.ts +9 -0
  59. package/src/empty/Empty.tsx +47 -0
  60. package/src/form/Form.tsx +95 -0
  61. package/src/form/FormItem.tsx +145 -0
  62. package/src/form/common/context.ts +13 -0
  63. package/src/form/common/enum.ts +20 -0
  64. package/src/form/common/rules.ts +68 -0
  65. package/src/form/common/types.ts +169 -0
  66. package/src/form/index.ts +35 -0
  67. package/src/form/partials/FormItemHelps.tsx +31 -0
  68. package/src/form/variants/FieldCheckbox.tsx +20 -0
  69. package/src/form/variants/FieldColor.tsx +63 -0
  70. package/src/form/variants/FieldDateTime.tsx +25 -0
  71. package/src/form/variants/FieldHidden.tsx +9 -0
  72. package/src/form/variants/FieldInput.tsx +24 -0
  73. package/src/form/variants/FieldNumber.tsx +28 -0
  74. package/src/form/variants/FieldObject.tsx +29 -0
  75. package/src/form/variants/FieldRadio.tsx +40 -0
  76. package/src/form/variants/FieldRepeater.tsx +62 -0
  77. package/src/form/variants/FieldRepeaterItem.tsx +64 -0
  78. package/src/form/variants/FieldSelect.tsx +27 -0
  79. package/src/form/variants/FieldSelectMultiple.tsx +32 -0
  80. package/src/form/variants/FieldSelectRemote.tsx +66 -0
  81. package/src/form/variants/FieldSwitch.tsx +17 -0
  82. package/src/form/variants/FieldTextarea.tsx +25 -0
  83. package/src/form/variants/FieldTime.tsx +25 -0
  84. package/src/form-layout/FormLayout.tsx +74 -0
  85. package/src/icon/FluentIcon.tsx +28 -0
  86. package/src/icon/fluent.ts +75 -0
  87. package/src/index.ts +214 -0
  88. package/src/input/Input.tsx +371 -0
  89. package/src/input/SearchInput.tsx +73 -0
  90. package/src/lib/cn.ts +6 -0
  91. package/src/lib/placement.ts +44 -0
  92. package/src/lib/sizes.ts +117 -0
  93. package/src/lib/surface.ts +8 -0
  94. package/src/menu/Menu.tsx +98 -0
  95. package/src/message/message.tsx +214 -0
  96. package/src/modal/Modal.tsx +277 -0
  97. package/src/pagination/Pagination.tsx +103 -0
  98. package/src/popconfirm/Popconfirm.tsx +134 -0
  99. package/src/popover/Popover.tsx +116 -0
  100. package/src/scroll/OverlayScroll.tsx +141 -0
  101. package/src/segmented/Segmented.tsx +67 -0
  102. package/src/select/Select.tsx +319 -0
  103. package/src/skeleton/Skeleton.tsx +47 -0
  104. package/src/spin/Spin.tsx +160 -0
  105. package/src/styles.css +897 -0
  106. package/src/switch/Switch.tsx +58 -0
  107. package/src/table/Table.tsx +387 -0
  108. package/src/tag/Tag.tsx +109 -0
  109. package/src/theme.ts +131 -0
  110. package/src/timepicker/TimePicker.tsx +751 -0
  111. package/src/tooltip/Tooltip.tsx +59 -0
@@ -0,0 +1,481 @@
1
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
2
+ import { format as formatDate, isValid, parseISO } from "date-fns";
3
+ import {
4
+ type ButtonHTMLAttributes,
5
+ type CSSProperties,
6
+ type ForwardRefExoticComponent,
7
+ type MouseEvent,
8
+ type ReactElement,
9
+ type ReactNode,
10
+ type RefAttributes,
11
+ forwardRef,
12
+ useEffect,
13
+ useMemo,
14
+ useState,
15
+ } from "react";
16
+ import type { DateRange } from "react-day-picker";
17
+ import { Calendar } from "../calendar/Calendar";
18
+ import { Button } from "../button/Button";
19
+ import { cn } from "../lib/cn";
20
+ import { type ControlSize, controlFieldFocusBorder, controlFieldStyle, controlFieldSurface, controlFieldTransition, controlStatusClass, getSizeTokens } from "../lib/sizes";
21
+ import { glassOverlayClass } from "../lib/surface";
22
+
23
+ export type DatePickerProps = {
24
+ value?: Date | string | null;
25
+ defaultValue?: Date | string | null;
26
+ onChange?: (date: Date | null, dateString: string) => void;
27
+ size?: ControlSize;
28
+ status?: "error" | "warning";
29
+ allowClear?: boolean;
30
+ format?: string;
31
+ showTime?: boolean;
32
+ /** Keep the panel open until OK (antd). */
33
+ needConfirm?: boolean;
34
+ placeholder?: string;
35
+ disabled?: boolean;
36
+ className?: string;
37
+ style?: CSSProperties;
38
+ popupClassName?: string;
39
+ autoFocus?: boolean;
40
+ open?: boolean;
41
+ onOpenChange?: (open: boolean) => void;
42
+ renderExtraFooter?: () => ReactNode;
43
+ };
44
+
45
+ export type RangeValue = [Date | null, Date | null] | null;
46
+
47
+ export type RangePickerProps = {
48
+ value?: RangeValue;
49
+ defaultValue?: RangeValue;
50
+ onChange?: (dates: RangeValue, dateStrings: [string, string]) => void;
51
+ size?: ControlSize;
52
+ status?: "error" | "warning";
53
+ allowClear?: boolean;
54
+ format?: string;
55
+ placeholder?: [string, string] | string;
56
+ disabled?: boolean;
57
+ className?: string;
58
+ style?: CSSProperties;
59
+ popupClassName?: string;
60
+ /** Show two months side by side (default true). */
61
+ numberOfMonths?: number;
62
+ separator?: string;
63
+ };
64
+
65
+ function parseValue(value: Date | string | null | undefined): Date | undefined {
66
+ if (value == null || value === "") return undefined;
67
+ if (value instanceof Date) return isValid(value) ? value : undefined;
68
+ const iso = parseISO(value);
69
+ if (isValid(iso)) return iso;
70
+ const d = new Date(value);
71
+ return isValid(d) ? d : undefined;
72
+ }
73
+
74
+ function toDateString(date: Date | null | undefined, showTime?: boolean, fmt?: string): string {
75
+ if (!date || !isValid(date)) return "";
76
+ if (fmt) return formatDate(date, fmt);
77
+ return formatDate(date, showTime ? "MMM d, yyyy HH:mm" : "MMM d, yyyy");
78
+ }
79
+
80
+ function parseRange(value: RangeValue | undefined): DateRange | undefined {
81
+ if (!value) return undefined;
82
+ const from = parseValue(value[0]);
83
+ const to = parseValue(value[1]);
84
+ if (!from && !to) return undefined;
85
+ return { from, to };
86
+ }
87
+
88
+ function rangeToValue(range: DateRange | undefined): RangeValue {
89
+ if (!range?.from && !range?.to) return null;
90
+ return [range?.from ?? null, range?.to ?? null];
91
+ }
92
+
93
+ function CalendarIcon({ size }: { size: number }) {
94
+ return (
95
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden className="shrink-0 opacity-55">
96
+ <rect x="3" y="5" width="18" height="16" rx="2" stroke="currentColor" strokeWidth="1.75" />
97
+ <path d="M3 10H21" stroke="currentColor" strokeWidth="1.75" />
98
+ <path d="M8 3V7M16 3V7" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" />
99
+ </svg>
100
+ );
101
+ }
102
+
103
+ function ClearIcon() {
104
+ return (
105
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden>
106
+ <path d="M3 3L9 9M9 3L3 9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
107
+ </svg>
108
+ );
109
+ }
110
+
111
+ function mergeDateTime(base: Date, hours: number, minutes: number): Date {
112
+ const next = new Date(base);
113
+ next.setHours(hours, minutes, 0, 0);
114
+ return next;
115
+ }
116
+
117
+ const TriggerChrome = forwardRef<
118
+ HTMLButtonElement,
119
+ {
120
+ size?: ControlSize;
121
+ status?: "error" | "warning";
122
+ disabled?: boolean;
123
+ className?: string;
124
+ style?: CSSProperties;
125
+ icon: ReactElement;
126
+ clear?: ReactElement | null;
127
+ children: ReactNode;
128
+ } & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children" | "style">
129
+ >(function TriggerChrome({ size, status, disabled, className, style, icon, clear, children, ...rest }, ref) {
130
+ return (
131
+ <button
132
+ ref={ref}
133
+ type="button"
134
+ disabled={disabled}
135
+ className={cn(
136
+ "group/datepicker inline-flex w-full cursor-pointer items-center gap-2 text-left",
137
+ controlFieldSurface,
138
+ controlFieldTransition,
139
+ controlFieldFocusBorder,
140
+ "focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-45",
141
+ controlStatusClass(status),
142
+ className,
143
+ )}
144
+ style={{ ...controlFieldStyle(size), ...style }}
145
+ {...rest}
146
+ >
147
+ {icon}
148
+ <span className="min-w-0 flex-1 truncate">{children}</span>
149
+ {clear}
150
+ </button>
151
+ );
152
+ });
153
+
154
+ const DatePickerRoot = forwardRef<HTMLButtonElement, DatePickerProps>(function DatePicker(
155
+ {
156
+ value,
157
+ defaultValue,
158
+ onChange,
159
+ size,
160
+ status,
161
+ allowClear = true,
162
+ format,
163
+ showTime,
164
+ needConfirm,
165
+ placeholder = "Pick a date",
166
+ disabled,
167
+ className,
168
+ style,
169
+ popupClassName,
170
+ autoFocus,
171
+ open: openProp,
172
+ onOpenChange,
173
+ renderExtraFooter,
174
+ },
175
+ ref,
176
+ ) {
177
+ const controlled = value !== undefined;
178
+ const [inner, setInner] = useState<Date | undefined>(() => parseValue(defaultValue));
179
+ const selected = controlled ? parseValue(value) : inner;
180
+ const [innerOpen, setInnerOpen] = useState(false);
181
+ const open = openProp ?? innerOpen;
182
+ const setOpen = (v: boolean) => {
183
+ if (openProp === undefined) setInnerOpen(v);
184
+ onOpenChange?.(v);
185
+ };
186
+ const [month, setMonth] = useState<Date>(() => selected ?? new Date());
187
+ const [draft, setDraft] = useState<Date | undefined>(selected);
188
+ const tok = getSizeTokens(size);
189
+ const confirm = Boolean(needConfirm);
190
+
191
+ useEffect(() => {
192
+ if (autoFocus) {
193
+ // focus trigger after mount
194
+ const id = window.requestAnimationFrame(() => (ref as React.RefObject<HTMLButtonElement> | null)?.current?.focus());
195
+ return () => window.cancelAnimationFrame(id);
196
+ }
197
+ }, [autoFocus, ref]);
198
+
199
+ useEffect(() => {
200
+ if (selected) setMonth(selected);
201
+ }, [selected?.getTime()]);
202
+
203
+ useEffect(() => {
204
+ if (open) setDraft(selected);
205
+ }, [open, selected?.getTime()]);
206
+
207
+ const panelDate = confirm ? draft : selected;
208
+ const label = useMemo(() => toDateString(selected, showTime, format), [selected, showTime, format]);
209
+
210
+ const commit = (next: Date | null) => {
211
+ if (!controlled) setInner(next ?? undefined);
212
+ onChange?.(next, toDateString(next, showTime, format));
213
+ };
214
+
215
+ const onSelect = (day: Date | undefined) => {
216
+ if (!day) {
217
+ if (confirm) setDraft(undefined);
218
+ else commit(null);
219
+ return;
220
+ }
221
+ const base = confirm ? draft ?? selected : selected;
222
+ const withTime = showTime && base ? mergeDateTime(day, base.getHours(), base.getMinutes()) : day;
223
+ if (confirm) {
224
+ setDraft(withTime);
225
+ return;
226
+ }
227
+ commit(withTime);
228
+ if (!showTime) setOpen(false);
229
+ };
230
+
231
+ const onTimeChange = (part: "h" | "m", raw: string) => {
232
+ const base = confirm ? draft ?? selected : selected;
233
+ if (!base) return;
234
+ const n = Number(raw);
235
+ if (!Number.isFinite(n)) return;
236
+ const hours = part === "h" ? Math.min(23, Math.max(0, n)) : base.getHours();
237
+ const minutes = part === "m" ? Math.min(59, Math.max(0, n)) : base.getMinutes();
238
+ const next = mergeDateTime(base, hours, minutes);
239
+ if (confirm) setDraft(next);
240
+ else commit(next);
241
+ };
242
+
243
+ const clear = (e: MouseEvent) => {
244
+ e.preventDefault();
245
+ e.stopPropagation();
246
+ commit(null);
247
+ };
248
+
249
+ const pad = (n: number) => String(n).padStart(2, "0");
250
+
251
+ return (
252
+ <PopoverPrimitive.Root open={open} onOpenChange={(v) => !disabled && setOpen(v)}>
253
+ <PopoverPrimitive.Trigger asChild>
254
+ <TriggerChrome
255
+ ref={ref}
256
+ size={size}
257
+ status={status}
258
+ disabled={disabled}
259
+ className={className}
260
+ style={style}
261
+ icon={<CalendarIcon size={tok.icon} />}
262
+ clear={
263
+ allowClear && selected && !disabled ? (
264
+ <span
265
+ role="button"
266
+ tabIndex={-1}
267
+ aria-label="Clear"
268
+ className="inline-flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-full text-muted-foreground opacity-0 transition-opacity hover:bg-foreground/6 hover:text-foreground group-hover/datepicker:opacity-100"
269
+ onClick={clear}
270
+ onPointerDown={(e) => e.preventDefault()}
271
+ >
272
+ <ClearIcon />
273
+ </span>
274
+ ) : null
275
+ }
276
+ >
277
+ <span className={cn(!selected && "text-quaternary-foreground")}>{selected ? label : placeholder}</span>
278
+ </TriggerChrome>
279
+ </PopoverPrimitive.Trigger>
280
+ <PopoverPrimitive.Portal>
281
+ <PopoverPrimitive.Content
282
+ align="start"
283
+ sideOffset={6}
284
+ className={cn(
285
+ "w-auto overflow-hidden p-3",
286
+ glassOverlayClass,
287
+ popupClassName,
288
+ )}
289
+ >
290
+ {renderExtraFooter?.()}
291
+ <Calendar mode="single" selected={panelDate} onSelect={onSelect} month={month} onMonthChange={setMonth} />
292
+ {showTime ? (
293
+ <div className="mt-3 flex items-center gap-2 border-t border-border pt-3">
294
+ <span className="text-xs text-muted-foreground">Time</span>
295
+ <TimeField value={panelDate ? pad(panelDate.getHours()) : "00"} disabled={!panelDate} onChange={(v) => onTimeChange("h", v)} max={23} />
296
+ <span className="text-muted-foreground">:</span>
297
+ <TimeField value={panelDate ? pad(panelDate.getMinutes()) : "00"} disabled={!panelDate} onChange={(v) => onTimeChange("m", v)} max={59} />
298
+ </div>
299
+ ) : null}
300
+ {confirm ? (
301
+ <div className="mt-3 flex justify-end gap-2 border-t border-border pt-3">
302
+ <Button
303
+ size="small"
304
+ onClick={() => {
305
+ setDraft(selected);
306
+ setOpen(false);
307
+ }}
308
+ >
309
+ Cancel
310
+ </Button>
311
+ <Button
312
+ size="small"
313
+ type="primary"
314
+ disabled={!draft}
315
+ onClick={() => {
316
+ commit(draft ?? null);
317
+ setOpen(false);
318
+ }}
319
+ >
320
+ OK
321
+ </Button>
322
+ </div>
323
+ ) : null}
324
+ </PopoverPrimitive.Content>
325
+ </PopoverPrimitive.Portal>
326
+ </PopoverPrimitive.Root>
327
+ );
328
+ });
329
+
330
+ const RangePicker = forwardRef<HTMLButtonElement, RangePickerProps>(function RangePicker(
331
+ {
332
+ value,
333
+ defaultValue,
334
+ onChange,
335
+ size,
336
+ status,
337
+ allowClear = true,
338
+ format,
339
+ placeholder = ["Start date", "End date"],
340
+ disabled,
341
+ className,
342
+ style,
343
+ popupClassName,
344
+ numberOfMonths = 2,
345
+ separator = "–",
346
+ },
347
+ ref,
348
+ ) {
349
+ const controlled = value !== undefined;
350
+ const [inner, setInner] = useState<DateRange | undefined>(() => parseRange(defaultValue));
351
+ const selected = controlled ? parseRange(value) : inner;
352
+ const [open, setOpen] = useState(false);
353
+ const [month, setMonth] = useState<Date>(() => selected?.from ?? new Date());
354
+ const tok = getSizeTokens(size);
355
+
356
+ useEffect(() => {
357
+ if (selected?.from) setMonth(selected.from);
358
+ }, [selected?.from?.getTime()]);
359
+
360
+ const placeholders = Array.isArray(placeholder) ? placeholder : [placeholder, placeholder];
361
+ const startLabel = toDateString(selected?.from, false, format);
362
+ const rangeComplete = Boolean(
363
+ selected?.from && selected?.to && selected.from.getTime() !== selected.to.getTime(),
364
+ );
365
+ const endLabel = rangeComplete ? toDateString(selected?.to, false, format) : "";
366
+ const hasValue = Boolean(selected?.from || selected?.to);
367
+
368
+ const commit = (next: DateRange | undefined) => {
369
+ if (!controlled) setInner(next);
370
+ const dates = rangeToValue(next);
371
+ onChange?.(dates, [toDateString(dates?.[0], false, format), toDateString(dates?.[1], false, format)]);
372
+ };
373
+
374
+ const onSelect = (range: DateRange | undefined) => {
375
+ commit(range);
376
+ // DayPicker sets from===to on the first click; close only once the range has a real end.
377
+ if (range?.from && range?.to && range.from.getTime() !== range.to.getTime()) {
378
+ setOpen(false);
379
+ }
380
+ };
381
+
382
+ const clear = (e: MouseEvent) => {
383
+ e.preventDefault();
384
+ e.stopPropagation();
385
+ commit(undefined);
386
+ };
387
+
388
+ return (
389
+ <PopoverPrimitive.Root open={open} onOpenChange={(v) => !disabled && setOpen(v)}>
390
+ <PopoverPrimitive.Trigger asChild>
391
+ <TriggerChrome
392
+ ref={ref}
393
+ size={size}
394
+ status={status}
395
+ disabled={disabled}
396
+ className={className}
397
+ style={style}
398
+ icon={<CalendarIcon size={tok.icon} />}
399
+ clear={
400
+ allowClear && hasValue && !disabled ? (
401
+ <span
402
+ role="button"
403
+ tabIndex={-1}
404
+ aria-label="Clear"
405
+ className="inline-flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-full text-muted-foreground opacity-0 transition-opacity hover:bg-foreground/6 hover:text-foreground group-hover/datepicker:opacity-100"
406
+ onClick={clear}
407
+ onPointerDown={(e) => e.preventDefault()}
408
+ >
409
+ <ClearIcon />
410
+ </span>
411
+ ) : null
412
+ }
413
+ >
414
+ <span className="inline-flex min-w-0 items-center gap-1.5">
415
+ <span className={cn("truncate", !selected?.from && "text-quaternary-foreground")}>{selected?.from ? startLabel : placeholders[0]}</span>
416
+ <span className="shrink-0 text-muted-foreground">{separator}</span>
417
+ <span className={cn("truncate", !rangeComplete && "text-quaternary-foreground")}>{rangeComplete ? endLabel : placeholders[1]}</span>
418
+ </span>
419
+ </TriggerChrome>
420
+ </PopoverPrimitive.Trigger>
421
+ <PopoverPrimitive.Portal>
422
+ <PopoverPrimitive.Content
423
+ align="start"
424
+ sideOffset={6}
425
+ className={cn(
426
+ "w-auto overflow-hidden p-3",
427
+ glassOverlayClass,
428
+ popupClassName,
429
+ )}
430
+ >
431
+ <Calendar
432
+ mode="range"
433
+ selected={selected}
434
+ onSelect={onSelect}
435
+ month={month}
436
+ onMonthChange={setMonth}
437
+ numberOfMonths={numberOfMonths}
438
+ defaultMonth={selected?.from}
439
+ />
440
+ </PopoverPrimitive.Content>
441
+ </PopoverPrimitive.Portal>
442
+ </PopoverPrimitive.Root>
443
+ );
444
+ });
445
+
446
+ function TimeField({
447
+ value,
448
+ onChange,
449
+ disabled,
450
+ max,
451
+ }: {
452
+ value: string;
453
+ onChange: (v: string) => void;
454
+ disabled?: boolean;
455
+ max: number;
456
+ }) {
457
+ return (
458
+ <input
459
+ type="number"
460
+ inputMode="numeric"
461
+ min={0}
462
+ max={max}
463
+ disabled={disabled}
464
+ value={value}
465
+ onChange={(e) => onChange(e.target.value)}
466
+ className={cn(
467
+ "h-8 w-12 rounded-md border border-solid border-input bg-transparent px-1.5 text-center text-sm text-foreground",
468
+ "focus-visible:outline-none disabled:opacity-40 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none",
469
+ )}
470
+ />
471
+ );
472
+ }
473
+
474
+ type DatePickerComponent = ForwardRefExoticComponent<DatePickerProps & RefAttributes<HTMLButtonElement>> & {
475
+ RangePicker: typeof RangePicker;
476
+ };
477
+
478
+ export const DatePicker = DatePickerRoot as DatePickerComponent;
479
+ DatePicker.RangePicker = RangePicker;
480
+
481
+ export { RangePicker };
@@ -0,0 +1,92 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from "react";
2
+ import { cn } from "../lib/cn";
3
+ import { Tooltip } from "../tooltip/Tooltip";
4
+
5
+ export function DesktopBarButton({
6
+ label,
7
+ active,
8
+ onClick,
9
+ children,
10
+ }: {
11
+ label: string;
12
+ active?: boolean;
13
+ onClick?: () => void;
14
+ children: ReactNode;
15
+ }) {
16
+ return (
17
+ <Tooltip title={label} placement="bottom">
18
+ <button
19
+ type="button"
20
+ aria-label={label}
21
+ aria-current={active ? "true" : undefined}
22
+ onClick={onClick}
23
+ className={cn(
24
+ "inline-flex size-8 items-center justify-center rounded-full border-0 bg-transparent cursor-pointer",
25
+ active ? "bg-ink-active text-foreground" : "text-foreground/65 hover:bg-ink-hover hover:text-foreground",
26
+ )}
27
+ >
28
+ {children}
29
+ </button>
30
+ </Tooltip>
31
+ );
32
+ }
33
+
34
+ export function DesktopBarItem({
35
+ label,
36
+ active,
37
+ onClick,
38
+ className,
39
+ children,
40
+ ...rest
41
+ }: {
42
+ label?: ReactNode;
43
+ active?: boolean;
44
+ onClick?: () => void;
45
+ className?: string;
46
+ children?: ReactNode;
47
+ } & ButtonHTMLAttributes<HTMLButtonElement>) {
48
+ return (
49
+ <button
50
+ type="button"
51
+ aria-current={active ? "true" : undefined}
52
+ onClick={onClick}
53
+ className={cn(
54
+ "inline-flex h-7 items-center rounded-md border-0 bg-transparent px-2.5 text-sm font-medium leading-5 cursor-pointer",
55
+ active ? "bg-ink-active text-foreground" : "text-foreground/85 hover:bg-ink-hover hover:text-foreground",
56
+ className,
57
+ )}
58
+ {...rest}
59
+ >
60
+ {children ?? label}
61
+ </button>
62
+ );
63
+ }
64
+
65
+ export function DesktopBarDivider({ className }: { className?: string }) {
66
+ return <span className={cn("mx-2 h-4 w-px bg-ink-line", className)} aria-hidden />;
67
+ }
68
+
69
+ export function DesktopHeader({
70
+ logo,
71
+ leading,
72
+ trailing,
73
+ profile,
74
+ }: {
75
+ logo?: ReactNode;
76
+ leading?: ReactNode;
77
+ trailing?: ReactNode;
78
+ profile?: ReactNode;
79
+ }) {
80
+ return (
81
+ <header className="fixed inset-x-0 top-0 z-40 flex h-desktop-bar items-center justify-between gap-3 border-0 bg-glass-bar px-3 backdrop-blur-lg">
82
+ <div className="flex min-w-0 items-center">
83
+ {logo}
84
+ {leading}
85
+ </div>
86
+ <div className="flex min-w-0 shrink-0 items-center">
87
+ {trailing}
88
+ {profile}
89
+ </div>
90
+ </header>
91
+ );
92
+ }
@@ -0,0 +1,34 @@
1
+ import { type ReactNode, forwardRef } from "react";
2
+ import { FluentIcon } from "../icon/FluentIcon";
3
+
4
+ const iconButtonClass =
5
+ "group w-[96px] flex flex-col items-center gap-2 py-2 px-0 rounded-md border-none cursor-pointer select-none outline-none bg-transparent focus-visible:outline-2 focus-visible:outline-brand motion-safe:transition-transform motion-safe:hover:-translate-y-0.5 motion-safe:active:scale-[0.98]";
6
+
7
+ const labelClass = "text-xs leading-[18px] font-semibold text-center line-clamp-2 w-full";
8
+
9
+ function plateClass(active: boolean) {
10
+ return `w-11 h-11 flex items-center justify-center filter-[drop-shadow(0_1px_2px_rgba(0,0,0,0.45))] ${active ? "ring-2 ring-brand rounded-[10px]" : ""}`;
11
+ }
12
+
13
+ export const DesktopIcon = forwardRef<
14
+ HTMLButtonElement,
15
+ {
16
+ label: string;
17
+ icon?: string;
18
+ media?: ReactNode;
19
+ active?: boolean;
20
+ onCard?: boolean;
21
+ onClick?: () => void;
22
+ }
23
+ >(function DesktopIcon({ label, icon, media, active = false, onCard = false, onClick }, ref) {
24
+ return (
25
+ <button ref={ref} type="button" title={label} aria-label={label} aria-current={active ? "true" : undefined} dir="ltr" onClick={onClick} className={iconButtonClass}>
26
+ {media ?? (
27
+ <span className={plateClass(active)}>
28
+ <FluentIcon name={icon ?? "sparkle-24"} size={32} />
29
+ </span>
30
+ )}
31
+ <span className={`${labelClass} ${onCard ? "text-foreground" : "text-white [text-shadow:0_1px_2px_rgba(0,0,0,0.85)]"}`}>{label}</span>
32
+ </button>
33
+ );
34
+ });
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from "react";
2
+ import { cn } from "../lib/cn";
3
+
4
+ export function DesktopStage({ children, className }: { children?: ReactNode; className?: string }) {
5
+ return <div className={cn("relative h-dvh w-screen overflow-hidden bg-meadow", className)}>{children}</div>;
6
+ }