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,169 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ControllerFieldState, ControllerRenderProps, FieldValues, RegisterOptions, Validate } from "react-hook-form";
3
+ import type { EFormItemType } from "./enum";
4
+
5
+ export type TFormItemRenderCtx = {
6
+ field: ControllerRenderProps<FieldValues, string>;
7
+ fieldState: ControllerFieldState;
8
+ status?: "error" | "warning";
9
+ };
10
+
11
+ type TFormItemCondition = "==" | "!=" | ">=" | "<=" | ">" | "<";
12
+
13
+ type TFormItemConditions = [string, TFormItemCondition, unknown];
14
+
15
+ export interface ISelectItemProps {
16
+ label: string;
17
+ value: string | number;
18
+ }
19
+
20
+ export interface IFormItemHelpProps {
21
+ text: string;
22
+ className?: string;
23
+ iconClassName?: string;
24
+ }
25
+
26
+ /** JSON-serializable rule value with message (RHF min/max/minLength/maxLength shape). */
27
+ export type TRuleValueMessage<T = number> = {
28
+ value: T;
29
+ message?: string;
30
+ };
31
+
32
+ /**
33
+ * Serializable rules for BE → FE forms.
34
+ * `pattern.value` is a string in JSON; hydrated to RegExp via `hydrateRules`.
35
+ * Client-only `validate` may be a function when composing schema in TS.
36
+ */
37
+ export type TFormRule = {
38
+ required?: string | boolean;
39
+ min?: number | TRuleValueMessage;
40
+ max?: number | TRuleValueMessage;
41
+ minLength?: number | TRuleValueMessage;
42
+ maxLength?: number | TRuleValueMessage;
43
+ pattern?: { value: string | RegExp; message?: string };
44
+ validate?: Validate<unknown, unknown> | Record<string, Validate<unknown, unknown>>;
45
+ };
46
+
47
+ export type TFormItemBase = {
48
+ type: EFormItemType;
49
+ label?: string;
50
+ collapsable?: boolean;
51
+ name: string | string[] | number[];
52
+ defaultValue?: unknown;
53
+ rules?: TFormRule;
54
+ helps?: IFormItemHelpProps[];
55
+ conditions?: Array<Array<TFormItemConditions>>;
56
+ options?: Record<string, unknown>;
57
+ colSpan?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
58
+ className?: string;
59
+ labelClassName?: string;
60
+ innerClassName?: string;
61
+ };
62
+
63
+ export type TFormItemInput = TFormItemBase & {
64
+ type: EFormItemType.Input;
65
+ };
66
+
67
+ export type TFormItemTextarea = TFormItemBase & {
68
+ type: EFormItemType.Textarea;
69
+ };
70
+
71
+ export type TFormItemNumber = TFormItemBase & {
72
+ type: EFormItemType.Number;
73
+ };
74
+
75
+ export type TFormItemColor = TFormItemBase & {
76
+ type: EFormItemType.Color;
77
+ };
78
+
79
+ export type TFormItemRadio = TFormItemBase & {
80
+ type: EFormItemType.Radio;
81
+ choices: ISelectItemProps[];
82
+ };
83
+
84
+ export type TFormItemSelect = TFormItemBase & {
85
+ type: EFormItemType.Select;
86
+ choices: ISelectItemProps[];
87
+ };
88
+
89
+ export type TFormItemSelectMultiple = TFormItemBase & {
90
+ type: EFormItemType.SelectMultiple;
91
+ choices: ISelectItemProps[];
92
+ };
93
+
94
+ export type TFormItemSelectRemote = TFormItemBase & {
95
+ type: EFormItemType.SelectRemote;
96
+ endpoint: string;
97
+ keyProp: string;
98
+ labelProp: string;
99
+ dataProp?: string;
100
+ };
101
+
102
+ export type TFormItemDateTime = TFormItemBase & {
103
+ type: EFormItemType.DateTime;
104
+ };
105
+
106
+ export type TFormItemTime = TFormItemBase & {
107
+ type: EFormItemType.Time;
108
+ };
109
+
110
+ export type TFormItemRepeater = TFormItemBase & {
111
+ type: EFormItemType.Repeater;
112
+ childItems?: TFormItemProps[];
113
+ };
114
+
115
+ export type TFormItemObject = TFormItemBase & {
116
+ type: EFormItemType.Object;
117
+ childItems?: TFormItemProps[];
118
+ };
119
+
120
+ export type TFormItemHidden = TFormItemBase & {
121
+ type: EFormItemType.Hidden;
122
+ };
123
+
124
+ export type TFormItemSwitch = TFormItemBase & {
125
+ type: EFormItemType.Switch;
126
+ };
127
+
128
+ export type TFormItemCheckbox = TFormItemBase & {
129
+ type: EFormItemType.Checkbox;
130
+ };
131
+
132
+ export type TFormItemJson = TFormItemBase & {
133
+ type: EFormItemType.JSON;
134
+ };
135
+
136
+ export type TFormItemEditor = TFormItemBase & {
137
+ type: EFormItemType.Editor;
138
+ };
139
+
140
+ export type TFormItemCustom = TFormItemBase & {
141
+ type: EFormItemType.Custom;
142
+ render: (ctx: TFormItemRenderCtx) => ReactNode;
143
+ };
144
+
145
+ export type TFormItemProps =
146
+ | TFormItemInput
147
+ | TFormItemTextarea
148
+ | TFormItemNumber
149
+ | TFormItemRepeater
150
+ | TFormItemSelect
151
+ | TFormItemSelectMultiple
152
+ | TFormItemDateTime
153
+ | TFormItemTime
154
+ | TFormItemSelectRemote
155
+ | TFormItemObject
156
+ | TFormItemColor
157
+ | TFormItemRadio
158
+ | TFormItemHidden
159
+ | TFormItemSwitch
160
+ | TFormItemCheckbox
161
+ | TFormItemJson
162
+ | TFormItemEditor
163
+ | TFormItemCustom
164
+ | TFormItemBase;
165
+
166
+ export type TForm = TFormItemProps[];
167
+
168
+ /** After hydrate — RHF RegisterOptions (pattern is RegExp). */
169
+ export type THydratedRules = RegisterOptions;
@@ -0,0 +1,35 @@
1
+ /** Schema-driven form (react-hook-form). Prefer this name over layout `Form`. */
2
+ export { Form as SchemaForm } from "./Form";
3
+ export type { FormProps as SchemaFormProps } from "./Form";
4
+ /** @deprecated use SchemaForm — kept so existing demo imports keep typechecking during migrate */
5
+ export { Form as FormSchema } from "./Form";
6
+ export { FormItem } from "./FormItem";
7
+ export type { FormItemProps } from "./FormItem";
8
+ export { EFormItemType } from "./common/enum";
9
+ export { hydrateRules, fieldNameOf, isRuleRequired } from "./common/rules";
10
+ export type { FormFetcher } from "./common/context";
11
+ export type {
12
+ TForm,
13
+ TFormItemProps,
14
+ TFormRule,
15
+ TRuleValueMessage,
16
+ ISelectItemProps,
17
+ IFormItemHelpProps,
18
+ TFormItemInput,
19
+ TFormItemTextarea,
20
+ TFormItemNumber,
21
+ TFormItemSelect,
22
+ TFormItemSelectMultiple,
23
+ TFormItemSelectRemote,
24
+ TFormItemDateTime,
25
+ TFormItemTime,
26
+ TFormItemRepeater,
27
+ TFormItemObject,
28
+ TFormItemRadio,
29
+ TFormItemColor,
30
+ TFormItemHidden,
31
+ TFormItemSwitch,
32
+ TFormItemCheckbox,
33
+ TFormItemCustom,
34
+ TFormItemRenderCtx,
35
+ } from "./common/types";
@@ -0,0 +1,31 @@
1
+ import { cn } from "../../lib/cn";
2
+ import type { IFormItemHelpProps } from "../common/types";
3
+
4
+ function FormItemHelp({ text, className }: IFormItemHelpProps) {
5
+ return (
6
+ <div className={cn("flex flex-row items-start gap-1.5 text-xs text-muted-foreground", className)}>
7
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden className="mt-0.5 shrink-0 text-warning">
8
+ <path d="M6 1.5L10.5 10H1.5L6 1.5Z" stroke="currentColor" strokeWidth="1.2" strokeLinejoin="round" />
9
+ <path d="M6 5V7.2" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
10
+ <circle cx="6" cy="8.6" r="0.6" fill="currentColor" />
11
+ </svg>
12
+ <div>{text}</div>
13
+ </div>
14
+ );
15
+ }
16
+
17
+ export type FormItemHelpsProps = {
18
+ className?: string;
19
+ items?: IFormItemHelpProps[];
20
+ };
21
+
22
+ export function FormItemHelps({ items, className }: FormItemHelpsProps) {
23
+ if (!items?.length) return null;
24
+ return (
25
+ <div className={cn("mt-1 flex flex-col gap-1 pl-2.75", className)}>
26
+ {items.map((item, index) => (
27
+ <FormItemHelp key={`${index}-${item.text}`} text={item.text} className={item.className} iconClassName={item.iconClassName} />
28
+ ))}
29
+ </div>
30
+ );
31
+ }
@@ -0,0 +1,20 @@
1
+ import type { ControllerRenderProps, FieldValues } from "react-hook-form";
2
+ import { Checkbox } from "../../checkbox/Checkbox";
3
+
4
+ type Props = {
5
+ field: ControllerRenderProps<FieldValues, string>;
6
+ options?: Record<string, unknown>;
7
+ };
8
+
9
+ export function FieldCheckbox({ field, options }: Props) {
10
+ const checkboxLabel = options?.checkboxLabel as string | undefined;
11
+ return (
12
+ <Checkbox
13
+ checked={Boolean(field.value)}
14
+ onChange={(checked) => field.onChange(checked)}
15
+ disabled={options?.disabled as boolean | undefined}
16
+ >
17
+ {checkboxLabel}
18
+ </Checkbox>
19
+ );
20
+ }
@@ -0,0 +1,63 @@
1
+ import { useState } from "react";
2
+ import type { ControllerRenderProps, FieldValues } from "react-hook-form";
3
+ import { Input } from "../../input/Input";
4
+ import { Popover } from "../../popover/Popover";
5
+
6
+ type Props = {
7
+ field: ControllerRenderProps<FieldValues, string>;
8
+ options?: Record<string, unknown>;
9
+ status?: "error" | "warning";
10
+ };
11
+
12
+ const FALLBACK = "#000000";
13
+
14
+ export function FieldColor({ field, options, status }: Props) {
15
+ const raw = typeof field.value === "string" ? field.value : field.value ? String(field.value) : "";
16
+ const hex = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(raw) ? raw : FALLBACK;
17
+ const [open, setOpen] = useState(false);
18
+
19
+ return (
20
+ <div className="flex items-center gap-2">
21
+ <Popover
22
+ open={open}
23
+ onOpenChange={setOpen}
24
+ trigger="click"
25
+ placement="bottom"
26
+ content={
27
+ <div className="flex flex-col gap-2 p-1 min-w-[180px]">
28
+ <input
29
+ type="color"
30
+ value={hex}
31
+ onChange={(e) => field.onChange(e.target.value)}
32
+ className="h-24 w-full cursor-pointer border-0 bg-transparent p-0"
33
+ aria-label="Pick color"
34
+ />
35
+ <Input
36
+ value={raw || hex}
37
+ onChange={(e) => field.onChange(e.target.value)}
38
+ placeholder="#000000"
39
+ status={status}
40
+ className="font-mono text-sm"
41
+ {...(options as object)}
42
+ />
43
+ </div>
44
+ }
45
+ >
46
+ <button
47
+ type="button"
48
+ className="size-9 shrink-0 cursor-pointer rounded border border-input p-0"
49
+ style={{ backgroundColor: raw || hex }}
50
+ aria-label="Color picker"
51
+ />
52
+ </Popover>
53
+ <Input
54
+ className="flex-1 font-mono text-sm"
55
+ value={raw}
56
+ onChange={(e) => field.onChange(e.target.value)}
57
+ onBlur={field.onBlur}
58
+ placeholder="#000000"
59
+ status={status}
60
+ />
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,25 @@
1
+ import type { ControllerRenderProps, FieldValues } from "react-hook-form";
2
+ import { DatePicker } from "../../datepicker/DatePicker";
3
+
4
+ type Props = {
5
+ field: ControllerRenderProps<FieldValues, string>;
6
+ options?: Record<string, unknown>;
7
+ status?: "error" | "warning";
8
+ };
9
+
10
+ export function FieldDateTime({ field, options, status }: Props) {
11
+ const opts = options ?? {};
12
+ return (
13
+ <DatePicker
14
+ className="w-full"
15
+ value={(field.value as Date | string | null) ?? null}
16
+ onChange={(date) => field.onChange(date)}
17
+ placeholder={(opts.placeholder as string) ?? "Select date"}
18
+ status={status}
19
+ showTime={opts.showTime as boolean | undefined}
20
+ format={opts.format as string | undefined}
21
+ allowClear={opts.allowClear !== false}
22
+ disabled={opts.disabled as boolean | undefined}
23
+ />
24
+ );
25
+ }
@@ -0,0 +1,9 @@
1
+ import type { ControllerRenderProps, FieldValues } from "react-hook-form";
2
+
3
+ type Props = {
4
+ field: ControllerRenderProps<FieldValues, string>;
5
+ };
6
+
7
+ export function FieldHidden({ field }: Props) {
8
+ return <input type="hidden" name={field.name} value={field.value == null ? "" : String(field.value)} onChange={field.onChange} ref={field.ref} />;
9
+ }
@@ -0,0 +1,24 @@
1
+ import type { ControllerRenderProps, FieldValues } from "react-hook-form";
2
+ import { Input } from "../../input/Input";
3
+
4
+ type Props = {
5
+ field: ControllerRenderProps<FieldValues, string>;
6
+ options?: Record<string, unknown>;
7
+ status?: "error" | "warning";
8
+ };
9
+
10
+ export function FieldInput({ field, options, status }: Props) {
11
+ const { value, onChange, onBlur, name, ref } = field;
12
+ return (
13
+ <Input
14
+ className="w-full"
15
+ name={name}
16
+ ref={ref}
17
+ onBlur={onBlur}
18
+ value={value == null ? "" : String(value)}
19
+ onChange={(e) => onChange(e.target.value)}
20
+ status={status}
21
+ {...(options as object)}
22
+ />
23
+ );
24
+ }
@@ -0,0 +1,28 @@
1
+ import type { ControllerRenderProps, FieldValues } from "react-hook-form";
2
+ import { InputNumber } from "../../input/Input";
3
+
4
+ type Props = {
5
+ field: ControllerRenderProps<FieldValues, string>;
6
+ options?: Record<string, unknown>;
7
+ status?: "error" | "warning";
8
+ };
9
+
10
+ export function FieldNumber({ field, options, status }: Props) {
11
+ const { value, onChange, onBlur, name, ref } = field;
12
+ const opts = options ?? {};
13
+ return (
14
+ <InputNumber
15
+ className="w-full"
16
+ name={name}
17
+ ref={ref}
18
+ onBlur={onBlur}
19
+ value={value == null || value === "" ? null : Number(value)}
20
+ onChange={(v) => onChange(v)}
21
+ status={status}
22
+ min={opts.min as number | undefined}
23
+ max={opts.max as number | undefined}
24
+ step={opts.step as number | undefined}
25
+ {...(opts as object)}
26
+ />
27
+ );
28
+ }
@@ -0,0 +1,29 @@
1
+ import type { Control, FieldValues } from "react-hook-form";
2
+ import { fieldNameOf } from "../common/rules";
3
+ import type { TFormItemProps } from "../common/types";
4
+ import { FormItem } from "../FormItem";
5
+
6
+ type Props = {
7
+ control: Control<FieldValues>;
8
+ name: string | string[] | number[];
9
+ childItems?: TFormItemProps[];
10
+ };
11
+
12
+ export function FieldObject({ control, name, childItems }: Props) {
13
+ const prefix = fieldNameOf(name);
14
+ return (
15
+ <div className="grid grid-cols-12 gap-x-4">
16
+ {(childItems ?? []).map((child, index) => {
17
+ const childName = fieldNameOf(child.name);
18
+ return (
19
+ <FormItem
20
+ key={`${prefix}.${childName}-${index}`}
21
+ {...child}
22
+ name={`${prefix}.${childName}`}
23
+ control={control}
24
+ />
25
+ );
26
+ })}
27
+ </div>
28
+ );
29
+ }
@@ -0,0 +1,40 @@
1
+ import type { ControllerRenderProps, FieldValues } from "react-hook-form";
2
+ import type { ISelectItemProps } from "../common/types";
3
+
4
+ type Props = {
5
+ field: ControllerRenderProps<FieldValues, string>;
6
+ choices?: ISelectItemProps[];
7
+ };
8
+
9
+ export function FieldRadio({ field, choices }: Props) {
10
+ return (
11
+ <div className="flex flex-col gap-2" role="radiogroup">
12
+ {(choices ?? []).map((option) => {
13
+ const id = `${field.name}-${option.value}`;
14
+ const checked = String(field.value) === String(option.value);
15
+ return (
16
+ <label key={String(option.value)} htmlFor={id} className="inline-flex cursor-pointer items-center gap-2 text-sm text-foreground">
17
+ <span className="relative inline-flex size-4 shrink-0">
18
+ <input
19
+ id={id}
20
+ type="radio"
21
+ name={field.name}
22
+ value={String(option.value)}
23
+ checked={checked}
24
+ onChange={() => field.onChange(option.value)}
25
+ onBlur={field.onBlur}
26
+ className="peer absolute inset-0 z-1 m-0 size-4 cursor-pointer appearance-none rounded-full opacity-0"
27
+ />
28
+ <span
29
+ aria-hidden
30
+ className="pointer-events-none size-4 rounded-full border border-solid border-input bg-(--control-bg) peer-checked:border-brand"
31
+ />
32
+ <span aria-hidden className="pointer-events-none absolute inset-[3.5px] rounded-full bg-brand opacity-0 peer-checked:opacity-100" />
33
+ </span>
34
+ <span>{option.label}</span>
35
+ </label>
36
+ );
37
+ })}
38
+ </div>
39
+ );
40
+ }
@@ -0,0 +1,62 @@
1
+ import type { Control, ControllerRenderProps, FieldValues } from "react-hook-form";
2
+ import { useFieldArray } from "react-hook-form";
3
+ import { Button } from "../../button/Button";
4
+ import { fieldNameOf } from "../common/rules";
5
+ import type { TFormItemProps } from "../common/types";
6
+ import { FieldRepeaterItem } from "./FieldRepeaterItem";
7
+
8
+ function PlusIcon() {
9
+ return (
10
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden>
11
+ <path d="M6 2.5V9.5M2.5 6H9.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
12
+ </svg>
13
+ );
14
+ }
15
+
16
+ type Props = {
17
+ field: ControllerRenderProps<FieldValues, string>;
18
+ name: string | string[] | number[];
19
+ control: Control<FieldValues>;
20
+ childItems?: TFormItemProps[];
21
+ };
22
+
23
+ export function FieldRepeater({ name, control, childItems }: Props) {
24
+ const arrayName = fieldNameOf(name);
25
+ const { fields, append, remove } = useFieldArray({
26
+ control,
27
+ name: arrayName,
28
+ });
29
+
30
+ const handleAdd = () => {
31
+ const defaults: Record<string, unknown> = {};
32
+ for (const child of childItems ?? []) {
33
+ const key = fieldNameOf(child.name);
34
+ if (child.defaultValue !== undefined) {
35
+ defaults[key] = child.defaultValue;
36
+ }
37
+ }
38
+ append(defaults);
39
+ };
40
+
41
+ return (
42
+ <div className="flex flex-col gap-5">
43
+ {fields.map((item, index) => (
44
+ <FieldRepeaterItem
45
+ key={item.id}
46
+ index={index}
47
+ remove={remove}
48
+ namePrefix={`${arrayName}.${index}`}
49
+ childItems={childItems}
50
+ control={control}
51
+ />
52
+ ))}
53
+
54
+ <div className="relative flex flex-row items-center">
55
+ <div className="h-px w-4 bg-border" />
56
+ <Button type="default" htmlType="button" size="small" icon={<PlusIcon />} onClick={handleAdd}>
57
+ Add item
58
+ </Button>
59
+ </div>
60
+ </div>
61
+ );
62
+ }
@@ -0,0 +1,64 @@
1
+ import { useState } from "react";
2
+ import type { Control, FieldValues } from "react-hook-form";
3
+ import { fieldNameOf } from "../common/rules";
4
+ import type { TFormItemProps } from "../common/types";
5
+ import { FormItem } from "../FormItem";
6
+
7
+ type Props = {
8
+ index: number;
9
+ remove: (index: number) => void;
10
+ namePrefix: string;
11
+ childItems?: TFormItemProps[];
12
+ control: Control<FieldValues>;
13
+ };
14
+
15
+ export function FieldRepeaterItem({ index, remove, namePrefix, childItems, control }: Props) {
16
+ const [collapsed, setCollapsed] = useState(false);
17
+
18
+ return (
19
+ <div className="relative">
20
+ <div className={`absolute bottom-0 left-0 border-l border-dashed border-border ${index === 0 ? "top-0" : "top-[-20px]"}`} />
21
+
22
+ <div className="flex items-center gap-2">
23
+ <div className="flex flex-1 items-center gap-2">
24
+ <div className="h-px w-4 bg-border" />
25
+ <button
26
+ type="button"
27
+ aria-label={collapsed ? "Expand item" : "Collapse item"}
28
+ className="inline-flex size-4 items-center justify-center rounded-[3px] border-0 bg-muted text-muted-foreground cursor-pointer p-0"
29
+ onClick={() => setCollapsed((v) => !v)}
30
+ >
31
+ <svg width="10" height="10" viewBox="0 0 12 12" fill="none" aria-hidden className={collapsed ? "-rotate-90" : undefined}>
32
+ <path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
33
+ </svg>
34
+ </button>
35
+ <span className="text-sm font-medium text-foreground">{`Item ${index + 1}`}</span>
36
+ </div>
37
+ <button
38
+ type="button"
39
+ aria-label="Remove item"
40
+ className="inline-flex size-5 items-center justify-center rounded border-0 bg-muted text-muted-foreground cursor-pointer hover:bg-destructive hover:text-[var(--destructive-foreground)]"
41
+ onClick={() => remove(index)}
42
+ >
43
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden>
44
+ <path d="M3 3L9 9M9 3L3 9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
45
+ </svg>
46
+ </button>
47
+ </div>
48
+
49
+ <div className={`mt-2 grid grid-cols-12 gap-x-4 overflow-hidden pl-8 ${collapsed ? "max-h-0" : ""}`}>
50
+ {(childItems ?? []).map((child, childIndex) => {
51
+ const childName = fieldNameOf(child.name);
52
+ return (
53
+ <FormItem
54
+ key={`${namePrefix}.${childName}-${childIndex}`}
55
+ {...child}
56
+ name={`${namePrefix}.${childName}`}
57
+ control={control}
58
+ />
59
+ );
60
+ })}
61
+ </div>
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,27 @@
1
+ import type { ControllerRenderProps, FieldValues } from "react-hook-form";
2
+ import { Select } from "../../select/Select";
3
+ import type { ISelectItemProps } from "../common/types";
4
+
5
+ type Props = {
6
+ field: ControllerRenderProps<FieldValues, string>;
7
+ choices?: ISelectItemProps[];
8
+ options?: Record<string, unknown>;
9
+ status?: "error" | "warning";
10
+ };
11
+
12
+ export function FieldSelect({ field, choices, options, status }: Props) {
13
+ const opts = options ?? {};
14
+ return (
15
+ <Select
16
+ className="w-full"
17
+ value={field.value == null || field.value === "" ? null : (field.value as string | number)}
18
+ onChange={(v) => field.onChange(v)}
19
+ placeholder={(opts.placeholder as string) ?? "Select..."}
20
+ disabled={opts.disabled as boolean | undefined}
21
+ showSearch={opts.searchable === true}
22
+ allowClear={opts.allowClear as boolean | undefined}
23
+ status={status}
24
+ options={(choices ?? []).map((c) => ({ label: c.label, value: c.value }))}
25
+ />
26
+ );
27
+ }
@@ -0,0 +1,32 @@
1
+ import type { ControllerRenderProps, FieldValues } from "react-hook-form";
2
+ import { Checkbox } from "../../checkbox/Checkbox";
3
+ import type { ISelectItemProps } from "../common/types";
4
+
5
+ type Props = {
6
+ field: ControllerRenderProps<FieldValues, string>;
7
+ choices?: ISelectItemProps[];
8
+ };
9
+
10
+ export function FieldSelectMultiple({ field, choices }: Props) {
11
+ const value: Array<string | number> = Array.isArray(field.value) ? field.value : [];
12
+
13
+ return (
14
+ <div className="flex flex-col gap-2">
15
+ {(choices ?? []).map((item) => (
16
+ <Checkbox
17
+ key={String(item.value)}
18
+ checked={value.includes(item.value)}
19
+ onChange={(checked) => {
20
+ if (checked) {
21
+ field.onChange([...value, item.value]);
22
+ } else {
23
+ field.onChange(value.filter((v) => v !== item.value));
24
+ }
25
+ }}
26
+ >
27
+ {item.label}
28
+ </Checkbox>
29
+ ))}
30
+ </div>
31
+ );
32
+ }