@milktop/react-ui 0.0.0-stage → 0.1.2

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 (250) hide show
  1. package/README.md +173 -2
  2. package/dist/accordion/Accordion.d.ts +65 -0
  3. package/dist/accordion/Accordion.js +61 -0
  4. package/dist/accordion/accordion.css +17 -0
  5. package/dist/action-bar/ActionBar.d.ts +33 -0
  6. package/dist/action-bar/ActionBar.js +48 -0
  7. package/dist/action-bar/action-bar.css +22 -0
  8. package/dist/alert/Alert.d.ts +28 -0
  9. package/dist/alert/Alert.js +12 -0
  10. package/dist/alert/alert.css +32 -0
  11. package/dist/app-shell/AppShell.d.ts +38 -0
  12. package/dist/app-shell/AppShell.js +92 -0
  13. package/dist/app-shell/Topbar.d.ts +20 -0
  14. package/dist/app-shell/Topbar.js +13 -0
  15. package/dist/app-shell/app-shell.css +41 -0
  16. package/dist/app-shell/shell.d.ts +9 -0
  17. package/dist/app-shell/shell.js +3 -0
  18. package/dist/attachments/Attachments.d.ts +84 -0
  19. package/dist/attachments/Attachments.js +235 -0
  20. package/dist/attachments/attachments.css +86 -0
  21. package/dist/avatar/Avatar.d.ts +31 -0
  22. package/dist/avatar/Avatar.js +41 -0
  23. package/dist/avatar/AvatarGroup.d.ts +41 -0
  24. package/dist/avatar/AvatarGroup.js +17 -0
  25. package/dist/avatar/avatar.css +73 -0
  26. package/dist/badge/Badge.d.ts +26 -0
  27. package/dist/badge/Badge.js +10 -0
  28. package/dist/badge/badge.css +24 -0
  29. package/dist/banner/Banner.d.ts +33 -0
  30. package/dist/banner/Banner.js +14 -0
  31. package/dist/banner/banner.css +25 -0
  32. package/dist/breadcrumbs/Breadcrumbs.d.ts +39 -0
  33. package/dist/breadcrumbs/Breadcrumbs.js +28 -0
  34. package/dist/breadcrumbs/breadcrumbs.css +18 -0
  35. package/dist/button/Button.d.ts +40 -0
  36. package/dist/button/Button.js +19 -0
  37. package/dist/button/button.css +63 -0
  38. package/dist/card/Card.d.ts +355 -0
  39. package/dist/card/Card.js +56 -0
  40. package/dist/card/card.css +43 -0
  41. package/dist/chart/AreaChart.d.ts +19 -0
  42. package/dist/chart/AreaChart.js +61 -0
  43. package/dist/chart/BarChart.d.ts +3 -0
  44. package/dist/chart/BarChart.js +60 -0
  45. package/dist/chart/ChartFrame.d.ts +25 -0
  46. package/dist/chart/ChartFrame.js +22 -0
  47. package/dist/chart/DonutChart.d.ts +20 -0
  48. package/dist/chart/DonutChart.js +41 -0
  49. package/dist/chart/Sparkline.d.ts +26 -0
  50. package/dist/chart/Sparkline.js +36 -0
  51. package/dist/chart/chart.css +51 -0
  52. package/dist/chart/chart.d.ts +23 -0
  53. package/dist/chart/chart.js +19 -0
  54. package/dist/checkbox/Checkbox.d.ts +50 -0
  55. package/dist/checkbox/Checkbox.js +19 -0
  56. package/dist/checkbox/CheckboxGroup.d.ts +44 -0
  57. package/dist/checkbox/CheckboxGroup.js +80 -0
  58. package/dist/checkbox/checkbox.css +49 -0
  59. package/dist/collapsible/Collapsible.d.ts +41 -0
  60. package/dist/collapsible/Collapsible.js +15 -0
  61. package/dist/collapsible/collapsible.css +20 -0
  62. package/dist/color-scheme.d.ts +18 -0
  63. package/dist/color-scheme.js +84 -0
  64. package/dist/command/Command.d.ts +64 -0
  65. package/dist/command/Command.js +162 -0
  66. package/dist/command/command.css +47 -0
  67. package/dist/confirm/confirm.css +5 -0
  68. package/dist/confirm/confirm.d.ts +42 -0
  69. package/dist/confirm/confirm.js +115 -0
  70. package/dist/copy-button/CopyButton.d.ts +30 -0
  71. package/dist/copy-button/CopyButton.js +23 -0
  72. package/dist/copy-button/copy-button.css +12 -0
  73. package/dist/css.d.ts +2 -0
  74. package/dist/data-list/DataList.d.ts +55 -0
  75. package/dist/data-list/DataList.js +43 -0
  76. package/dist/data-list/data-list.css +19 -0
  77. package/dist/date-picker/DatePicker.d.ts +95 -0
  78. package/dist/date-picker/DatePicker.js +237 -0
  79. package/dist/date-picker/date-picker.css +53 -0
  80. package/dist/dialog/Dialog.d.ts +98 -0
  81. package/dist/dialog/Dialog.js +92 -0
  82. package/dist/dialog/dialog.css +31 -0
  83. package/dist/divider/Divider.d.ts +27 -0
  84. package/dist/divider/Divider.js +8 -0
  85. package/dist/divider/divider.css +12 -0
  86. package/dist/editable/Editable.d.ts +47 -0
  87. package/dist/editable/Editable.js +51 -0
  88. package/dist/editable/editable.css +25 -0
  89. package/dist/editor/Editor.d.ts +59 -0
  90. package/dist/editor/Editor.js +220 -0
  91. package/dist/editor/editor.css +42 -0
  92. package/dist/editor/image.d.ts +1 -0
  93. package/dist/editor/image.js +23 -0
  94. package/dist/empty-state/EmptyState.d.ts +22 -0
  95. package/dist/empty-state/EmptyState.js +9 -0
  96. package/dist/empty-state/empty-state.css +14 -0
  97. package/dist/event-calendar/EventCalendar.d.ts +213 -0
  98. package/dist/event-calendar/EventCalendar.js +686 -0
  99. package/dist/event-calendar/dates.d.ts +8 -0
  100. package/dist/event-calendar/dates.js +21 -0
  101. package/dist/event-calendar/event-calendar.css +157 -0
  102. package/dist/field/Field.d.ts +66 -0
  103. package/dist/field/Field.js +23 -0
  104. package/dist/field/field.css +26 -0
  105. package/dist/file-upload/FileUpload.d.ts +60 -0
  106. package/dist/file-upload/FileUpload.js +102 -0
  107. package/dist/file-upload/file-upload.css +32 -0
  108. package/dist/format/FormatNumber.d.ts +42 -0
  109. package/dist/format/FormatNumber.js +59 -0
  110. package/dist/format/LocaleProvider.d.ts +33 -0
  111. package/dist/format/LocaleProvider.js +48 -0
  112. package/dist/format/format.d.ts +37 -0
  113. package/dist/format/format.js +74 -0
  114. package/dist/format/messages.d.ts +211 -0
  115. package/dist/format/messages.js +202 -0
  116. package/dist/highlight/Highlight.d.ts +11 -0
  117. package/dist/highlight/Highlight.js +14 -0
  118. package/dist/highlight/highlight.css +17 -0
  119. package/dist/hover-card/HoverCard.d.ts +47 -0
  120. package/dist/hover-card/HoverCard.js +18 -0
  121. package/dist/hover-card/hover-card.css +19 -0
  122. package/dist/index.d.ts +84 -0
  123. package/dist/index.js +85 -0
  124. package/dist/input/Input.d.ts +41 -0
  125. package/dist/input/Input.js +48 -0
  126. package/dist/input/input.css +25 -0
  127. package/dist/kbd/Kbd.d.ts +19 -0
  128. package/dist/kbd/Kbd.js +20 -0
  129. package/dist/kbd/kbd.css +8 -0
  130. package/dist/menu/Menu.d.ts +223 -0
  131. package/dist/menu/Menu.js +275 -0
  132. package/dist/menu/menu.css +38 -0
  133. package/dist/native-select/NativeSelect.d.ts +36 -0
  134. package/dist/native-select/NativeSelect.js +51 -0
  135. package/dist/native-select/native-select.css +16 -0
  136. package/dist/number-input/NumberInput.d.ts +56 -0
  137. package/dist/number-input/NumberInput.js +67 -0
  138. package/dist/number-input/number-input.css +21 -0
  139. package/dist/page/Page.d.ts +30 -0
  140. package/dist/page/Page.js +9 -0
  141. package/dist/page/page.css +17 -0
  142. package/dist/pagination/Pagination.d.ts +31 -0
  143. package/dist/pagination/Pagination.js +58 -0
  144. package/dist/pagination/pagination.css +15 -0
  145. package/dist/password-input/PasswordInput.d.ts +47 -0
  146. package/dist/password-input/PasswordInput.js +35 -0
  147. package/dist/password-input/password-input.css +18 -0
  148. package/dist/pin-input/PinInput.d.ts +45 -0
  149. package/dist/pin-input/PinInput.js +22 -0
  150. package/dist/pin-input/pin-input.css +13 -0
  151. package/dist/popover/Popover.d.ts +65 -0
  152. package/dist/popover/Popover.js +26 -0
  153. package/dist/popover/popover.css +16 -0
  154. package/dist/progress/Progress.d.ts +56 -0
  155. package/dist/progress/Progress.js +28 -0
  156. package/dist/progress/progress.css +24 -0
  157. package/dist/prose/Prose.d.ts +23 -0
  158. package/dist/prose/Prose.js +10 -0
  159. package/dist/prose/prose.css +29 -0
  160. package/dist/radio-group/RadioGroup.d.ts +59 -0
  161. package/dist/radio-group/RadioGroup.js +37 -0
  162. package/dist/radio-group/radio-group.css +30 -0
  163. package/dist/rolling-number/RollingNumber.d.ts +27 -0
  164. package/dist/rolling-number/RollingNumber.js +18 -0
  165. package/dist/rolling-number/rolling-number.css +10 -0
  166. package/dist/segment-group/Segmented.d.ts +68 -0
  167. package/dist/segment-group/Segmented.js +79 -0
  168. package/dist/segment-group/segment-group.css +25 -0
  169. package/dist/select/ListSelect.d.ts +5 -0
  170. package/dist/select/ListSelect.js +82 -0
  171. package/dist/select/SearchSelect.d.ts +43 -0
  172. package/dist/select/SearchSelect.js +219 -0
  173. package/dist/select/Select.d.ts +147 -0
  174. package/dist/select/Select.js +32 -0
  175. package/dist/select/items.d.ts +17 -0
  176. package/dist/select/items.js +14 -0
  177. package/dist/select/search-select.css +45 -0
  178. package/dist/select/select.css +52 -0
  179. package/dist/sheet/Sheet.d.ts +88 -0
  180. package/dist/sheet/Sheet.js +66 -0
  181. package/dist/sheet/sheet.css +44 -0
  182. package/dist/sidebar/NavGroup.d.ts +29 -0
  183. package/dist/sidebar/NavGroup.js +78 -0
  184. package/dist/sidebar/NavItem.d.ts +33 -0
  185. package/dist/sidebar/NavItem.js +50 -0
  186. package/dist/sidebar/NavSection.d.ts +20 -0
  187. package/dist/sidebar/NavSection.js +44 -0
  188. package/dist/sidebar/Sidebar.d.ts +65 -0
  189. package/dist/sidebar/Sidebar.js +59 -0
  190. package/dist/sidebar/SidebarUser.d.ts +31 -0
  191. package/dist/sidebar/SidebarUser.js +14 -0
  192. package/dist/sidebar/context.d.ts +13 -0
  193. package/dist/sidebar/context.js +6 -0
  194. package/dist/sidebar/sidebar.css +109 -0
  195. package/dist/skeleton/Skeleton.d.ts +25 -0
  196. package/dist/skeleton/Skeleton.js +8 -0
  197. package/dist/skeleton/skeleton.css +13 -0
  198. package/dist/slider/Slider.d.ts +70 -0
  199. package/dist/slider/Slider.js +65 -0
  200. package/dist/slider/slider.css +17 -0
  201. package/dist/sortable-list/SortableList.d.ts +46 -0
  202. package/dist/sortable-list/SortableList.js +85 -0
  203. package/dist/sortable-list/sortable-list.css +25 -0
  204. package/dist/spinner/Spinner.d.ts +18 -0
  205. package/dist/spinner/Spinner.js +11 -0
  206. package/dist/spinner/spinner.css +9 -0
  207. package/dist/stat/Stat.d.ts +42 -0
  208. package/dist/stat/Stat.js +21 -0
  209. package/dist/stat/Stats.d.ts +23 -0
  210. package/dist/stat/Stats.js +8 -0
  211. package/dist/stat/stat.css +34 -0
  212. package/dist/swatch-picker/SwatchPicker.d.ts +45 -0
  213. package/dist/swatch-picker/SwatchPicker.js +54 -0
  214. package/dist/swatch-picker/swatch-picker.css +38 -0
  215. package/dist/switch/Switch.d.ts +39 -0
  216. package/dist/switch/Switch.js +17 -0
  217. package/dist/switch/switch.css +13 -0
  218. package/dist/table/Table.d.ts +90 -0
  219. package/dist/table/Table.js +145 -0
  220. package/dist/table/table.css +44 -0
  221. package/dist/tabs/Tabs.d.ts +87 -0
  222. package/dist/tabs/Tabs.js +90 -0
  223. package/dist/tabs/tabs.css +53 -0
  224. package/dist/tags-input/TagsInput.d.ts +69 -0
  225. package/dist/tags-input/TagsInput.js +95 -0
  226. package/dist/tags-input/tags-input.css +37 -0
  227. package/dist/textarea/Textarea.d.ts +24 -0
  228. package/dist/textarea/Textarea.js +51 -0
  229. package/dist/textarea/textarea.css +14 -0
  230. package/dist/theme-toggle/ThemeToggle.d.ts +49 -0
  231. package/dist/theme-toggle/ThemeToggle.js +71 -0
  232. package/dist/theme-toggle/theme-toggle.css +23 -0
  233. package/dist/theme.css +121 -0
  234. package/dist/time-input/TimeInput.d.ts +25 -0
  235. package/dist/time-input/TimeInput.js +20 -0
  236. package/dist/timeline/Timeline.d.ts +50 -0
  237. package/dist/timeline/Timeline.js +15 -0
  238. package/dist/timeline/timeline.css +29 -0
  239. package/dist/toast/Toaster.d.ts +36 -0
  240. package/dist/toast/Toaster.js +25 -0
  241. package/dist/toast/toast.css +33 -0
  242. package/dist/tooltip/Tooltip.d.ts +55 -0
  243. package/dist/tooltip/Tooltip.js +48 -0
  244. package/dist/tooltip/tooltip.css +18 -0
  245. package/dist/tree-view/TreeView.d.ts +86 -0
  246. package/dist/tree-view/TreeView.js +74 -0
  247. package/dist/tree-view/tree-view.css +29 -0
  248. package/dist/utils.d.ts +19 -0
  249. package/dist/utils.js +59 -0
  250. package/package.json +99 -4
@@ -0,0 +1,92 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * A modal dialog, rendered at the end of <body> so no ancestor's stacking or
4
+ * overflow can trap it.
5
+ *
6
+ * <Dialog heading="Delete lesson?" description="This cannot be undone."
7
+ * trigger={<Button>Delete</Button>}
8
+ * footer={<Button onClick={…}>Cancel</Button>}>
9
+ * …body…
10
+ * </Dialog>
11
+ *
12
+ * Focus moves in and is trapped while it is open, the page behind doesn't
13
+ * scroll, Escape or a click on the backdrop closes it, and focus returns to
14
+ * the trigger.
15
+ *
16
+ * - `open`, `defaultOpen`; `onOpenChange`: called with the new open state;
17
+ * `onExitComplete`: called once it has animated out
18
+ * - `trigger`: the element that opens it (optional; it gets the trigger's props,
19
+ * so a component must pass them and its ref on, as Button does)
20
+ * - `heading`, `description`: label and describe it for assistive tech
21
+ * - `footer`: buttons along the bottom
22
+ * - `alert`: an alertdialog, for confirmations; the backdrop doesn't close it
23
+ * - `size`: 'sm', 'md' (default) or 'lg'
24
+ * - `closable`: false hides the close button
25
+ *
26
+ * Or composed, with the overlay still done for you (backdrop, positioning,
27
+ * close button), from Header, Body and Footer; a Dialog.Trigger child opens it:
28
+ *
29
+ * <Dialog>
30
+ * <Dialog.Trigger asChild><Button>Edit</Button></Dialog.Trigger>
31
+ * <Dialog.Header heading="Edit student" description="Saved to their profile." />
32
+ * <Dialog.Body>…</Dialog.Body>
33
+ * <Dialog.Footer><Dialog.CloseTrigger asChild><Button>Cancel</Button></Dialog.CloseTrigger>…</Dialog.Footer>
34
+ * </Dialog>
35
+ *
36
+ * Fully by hand, with Ark's parts (and Header, Body and Footer for the layout):
37
+ *
38
+ * <Dialog.Root>
39
+ * <Dialog.Trigger asChild><Button>Open</Button></Dialog.Trigger>
40
+ * <Portal>
41
+ * <Dialog.Backdrop />
42
+ * <Dialog.Positioner>
43
+ * <Dialog.Content>
44
+ * <Dialog.Header><Dialog.Title>Title</Dialog.Title></Dialog.Header>
45
+ * <Dialog.Body>…</Dialog.Body>
46
+ * <Dialog.Footer><Dialog.CloseTrigger asChild><Button>Close</Button></Dialog.CloseTrigger></Dialog.Footer>
47
+ * </Dialog.Content>
48
+ * </Dialog.Positioner>
49
+ * </Portal>
50
+ * </Dialog.Root>
51
+ */
52
+ import { Children, isValidElement } from 'react';
53
+ import { Dialog as Ark } from '@ark-ui/react/dialog';
54
+ import { Portal } from '@ark-ui/react/portal';
55
+ import 'iconify-icon';
56
+ import { cx, on, partOf, partsOf, withParts } from '../utils.js';
57
+ import { useMessages } from '../format/LocaleProvider.js';
58
+ import '../theme.css';
59
+ import './dialog.css';
60
+ function Header({ heading = null, description = null, children, ...rest }) {
61
+ return (_jsxs("div", { "data-scope": "dialog", "data-part": "header", ...rest, children: [heading && _jsx(Ark.Title, { children: heading }), description && _jsx(Ark.Description, { children: description }), children] }));
62
+ }
63
+ const Body = partOf('dialog', 'body');
64
+ const Footer = partOf('dialog', 'footer');
65
+ const layout = [Header, Body, Footer];
66
+ function DialogShortcut({ open, defaultOpen, onOpenChange, onExitComplete, heading = null, description = null, alert = false, size = 'md', closable = true, trigger = null, footer = null, className, classes, children, ...rest }) {
67
+ const t = useMessages();
68
+ const part = partsOf('dialog', classes);
69
+ // Composed from Dialog.Header, Body and Footer: they go straight into the
70
+ // dialog (not wrapped as its body), and a Dialog.Trigger goes before it.
71
+ const nodes = Children.toArray(children);
72
+ const composed = nodes.some((node) => isValidElement(node) && layout.includes(node.type));
73
+ const triggers = nodes.filter((node) => isValidElement(node) && node.type === Ark.Trigger);
74
+ const content = composed ? nodes.filter((node) => !triggers.includes(node)) : children;
75
+ // Left out when not given: Zag reads undefined as a value.
76
+ const state = { ...(open !== undefined && { open }), ...(defaultOpen !== undefined && { defaultOpen }) };
77
+ return (_jsxs(Ark.Root, { ...state, ...on('onOpenChange', onOpenChange, (details) => details.open), ...(onExitComplete && { onExitComplete }), role: alert ? 'alertdialog' : 'dialog', closeOnInteractOutside: !alert, lazyMount: true, unmountOnExit: true, children: [trigger && _jsx(Ark.Trigger, { asChild: true, children: trigger }), composed && triggers, _jsxs(Portal, { children: [_jsx(Ark.Backdrop, { ...part('backdrop') }), _jsx(Ark.Positioner, { ...part('positioner'), children: _jsxs(Ark.Content, { ...rest, ...part('content'), className: cx(className, classes?.content), "data-size": size, children: [closable && _jsx(Ark.CloseTrigger, { ...part('close-trigger'), "aria-label": t.common.close, children: _jsx("iconify-icon", { icon: "lucide:x" }) }), (heading || description) && (_jsxs("div", { ...part('header'), children: [heading && _jsx(Ark.Title, { ...part('title'), children: heading }), description && _jsx(Ark.Description, { ...part('description'), children: description })] })), composed ? content : children != null && _jsx("div", { ...part('body'), children: children }), footer && _jsx("div", { ...part('footer'), children: footer })] }) })] })] }));
78
+ }
79
+ export const Dialog = withParts(DialogShortcut, {
80
+ Root: Ark.Root,
81
+ Trigger: Ark.Trigger,
82
+ Backdrop: Ark.Backdrop,
83
+ Positioner: Ark.Positioner,
84
+ Content: Ark.Content,
85
+ CloseTrigger: Ark.CloseTrigger,
86
+ Title: Ark.Title,
87
+ Description: Ark.Description,
88
+ Context: Ark.Context,
89
+ Header,
90
+ Body,
91
+ Footer,
92
+ });
@@ -0,0 +1,31 @@
1
+ @layer ui {
2
+ [data-scope=dialog][data-part=backdrop] { position: fixed; inset: 0; z-index: 100; background: rgba(0, 0, 0, 0.45);
3
+ &[data-state=open] { animation: ui-fade-in 150ms ease-out; }
4
+ &[data-state=closed] { animation: ui-fade-out 120ms ease-in forwards; } }
5
+ /* Covers the screen to centre the panel, but lets clicks through to the backdrop. */
6
+ [data-scope=dialog][data-part=positioner] { position: fixed; inset: 0; z-index: 100; display: grid; place-items: center; padding: 1rem; pointer-events: none; }
7
+ [data-scope=dialog][data-part=content] {
8
+ position: relative; display: flex; flex-direction: column; width: 100%; max-width: 32rem; max-height: calc(100vh - 32px); box-sizing: border-box;
9
+ background: var(--ui-surface); color: var(--ui-text); font-family: var(--ui-font); font-size: 14px;
10
+ border: 1px solid var(--ui-border); border-radius: calc(var(--ui-radius) + 4px); box-shadow: var(--ui-shadow); outline: none; pointer-events: auto;
11
+ &[data-size=sm] { max-width: 24rem; }
12
+ &[data-size=lg] { max-width: 44rem; }
13
+ &[data-state=open] { animation: ui-dialog-in 180ms ease-out; }
14
+ &[data-state=closed] { animation: ui-dialog-out 120ms ease-in forwards; }
15
+ }
16
+ [data-scope=dialog][data-part=header] { padding: 1.5rem 3rem 0 1.5rem; }
17
+ [data-scope=dialog][data-part=title] { margin: 0; font-size: 18px; font-weight: 600; }
18
+ [data-scope=dialog][data-part=description] { margin: 0.375rem 0 0; color: var(--ui-muted); }
19
+ [data-scope=dialog][data-part=body] { padding: 1rem 1.5rem; overflow-y: auto;
20
+ &:first-child, [data-part=close-trigger] + & { padding-top: 1.5rem; } }
21
+ [data-scope=dialog][data-part=footer] { display: flex; justify-content: flex-end; gap: 0.5rem; flex-wrap: wrap; padding: 0 1.5rem 1.5rem;
22
+ [data-part=header] + & { padding-top: 1.5rem; } }
23
+ [data-scope=dialog][data-part=close-trigger] { position: absolute; top: 1rem; right: 1rem; display: grid; place-items: center; width: 2rem; height: 2rem;
24
+ border: none; background: transparent; border-radius: 6px; color: var(--ui-muted); cursor: pointer; font-size: 16px;
25
+ &:hover { background: var(--ui-hover); color: var(--ui-text); }
26
+ &:focus-visible { outline: 2px solid var(--ui-ring-soft); } }
27
+ @keyframes ui-fade-in { from { opacity: 0; } }
28
+ @keyframes ui-fade-out { to { opacity: 0; } }
29
+ @keyframes ui-dialog-in { from { opacity: 0; transform: translateY(8px) scale(0.97); } }
30
+ @keyframes ui-dialog-out { to { opacity: 0; transform: translateY(8px) scale(0.97); } }
31
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * A line between things: across (between sections) or up (between toolbar
3
+ * buttons), with an optional label in the middle ('or', 'Today').
4
+ *
5
+ * <Divider />
6
+ * <Divider label="or" />
7
+ * <Divider orientation="vertical" />
8
+ *
9
+ * - `label`: text (or anything) in the line; `align`: 'center' (default),
10
+ * 'start' or 'end'
11
+ * - `orientation`: 'horizontal' (default) or 'vertical' (as tall as its row)
12
+ * - `variant`: 'solid' (default), 'dashed' or 'dotted'
13
+ * - `spacing`: the room either side, a CSS length (0.75rem across, 0.5rem up)
14
+ */
15
+ import type { HTMLAttributes, ReactNode } from 'react';
16
+ import { type Classes } from '../utils.js';
17
+ import '../theme.css';
18
+ import './divider.css';
19
+ export type DividerProps = HTMLAttributes<HTMLDivElement> & {
20
+ label?: ReactNode;
21
+ align?: 'start' | 'center' | 'end';
22
+ orientation?: 'horizontal' | 'vertical';
23
+ variant?: 'solid' | 'dashed' | 'dotted';
24
+ spacing?: string | null;
25
+ classes?: Classes<'root' | 'label'>;
26
+ };
27
+ export declare function Divider({ label, align, orientation, variant, spacing, className, classes, style, ...rest }: DividerProps): import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx, partsOf } from '../utils.js';
3
+ import '../theme.css';
4
+ import './divider.css';
5
+ export function Divider({ label = null, align = 'center', orientation = 'horizontal', variant = 'solid', spacing = null, className, classes, style, ...rest }) {
6
+ const part = partsOf('divider', classes);
7
+ return (_jsx("div", { ...rest, ...part('root'), className: cx(className, classes?.root), role: "separator", "aria-orientation": orientation, "data-orientation": orientation, "data-variant": variant, "data-align": align, "data-labelled": label ? '' : undefined, style: { ...style, ...(spacing && { '--spacing': spacing }) }, children: label && _jsx("span", { ...part('label'), children: label }) }));
8
+ }
@@ -0,0 +1,12 @@
1
+ @layer ui {
2
+ [data-scope=divider][data-part=root] { --line: 1px var(--style, solid) var(--ui-border); color: var(--ui-muted); font-family: var(--ui-font);
3
+ &[data-variant=dashed] { --style: dashed; }
4
+ &[data-variant=dotted] { --style: dotted; }
5
+ &[data-orientation=horizontal] { margin: var(--spacing, 0.75rem) 0; border-top: var(--line);
6
+ &[data-labelled] { display: flex; align-items: center; gap: 0.75rem; border-top: none;
7
+ &::before, &::after { content: ''; flex: 1; border-top: var(--line); }
8
+ &[data-align=start]::before { flex: 0 0 1rem; }
9
+ &[data-align=end]::after { flex: 0 0 1rem; } } }
10
+ &[data-orientation=vertical] { align-self: stretch; width: 0; min-height: 1em; margin: 0 var(--spacing, 0.5rem); border-left: var(--line); } }
11
+ [data-scope=divider][data-part=label] { flex-shrink: 0; font-size: 12px; font-weight: 500; white-space: nowrap; }
12
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Text you click to edit in place, e.g. a pet's name or a price on a details
3
+ * page: it reads as text until clicked, then Enter saves and Escape cancels.
4
+ *
5
+ * <Editable value={pet.name} onValueChange={(name) => save({ name })} />
6
+ *
7
+ * - `value`, `defaultValue`: the saved text; `onValueChange`: called with it
8
+ * when an edit is saved (not on each key)
9
+ * - `placeholder`: shown when it's empty
10
+ * - `label`: a label above it (skipped inside a Field)
11
+ * - `activationMode`: 'click' (default), 'dblclick' or 'focus'
12
+ * - `controls`: ✓ and ✕ buttons while editing (true); `submitMode`: 'enter'
13
+ * (default), 'blur', 'both' or 'none' saves on those
14
+ * - `disabled`, `readOnly`
15
+ *
16
+ * By hand, Ark's parts are here too: Editable.Root, Area, Input, Preview,
17
+ * Control, EditTrigger, SubmitTrigger, CancelTrigger, Label.
18
+ */
19
+ import { type ReactNode } from 'react';
20
+ import { Editable as Ark } from '@ark-ui/react/editable';
21
+ import 'iconify-icon';
22
+ import { type Classes } from '../utils.js';
23
+ import '../theme.css';
24
+ import './editable.css';
25
+ export type EditableProps = Omit<Ark.RootProps, 'value' | 'defaultValue' | 'onValueChange' | 'onValueCommit' | 'onValueRevert'> & {
26
+ value?: string;
27
+ defaultValue?: string;
28
+ onValueChange?: (value: string) => void;
29
+ label?: ReactNode;
30
+ controls?: boolean;
31
+ classes?: Classes<'root' | 'label' | 'area' | 'input' | 'preview' | 'control' | 'editTrigger' | 'submitTrigger' | 'cancelTrigger'>;
32
+ };
33
+ declare function EditableShortcut({ value, defaultValue, onValueChange, label, controls, activationMode, placeholder, className, classes, ...rest }: EditableProps): import("react").JSX.Element;
34
+ export declare const Editable: typeof EditableShortcut & {
35
+ Root: import("react").ForwardRefExoticComponent<Ark.RootProps & import("react").RefAttributes<HTMLDivElement>>;
36
+ RootProvider: import("react").ForwardRefExoticComponent<Ark.RootProviderProps & import("react").RefAttributes<HTMLDivElement>>;
37
+ Area: import("react").ForwardRefExoticComponent<Ark.AreaProps & import("react").RefAttributes<HTMLDivElement>>;
38
+ Input: import("react").ForwardRefExoticComponent<Ark.InputProps & import("react").RefAttributes<HTMLInputElement>>;
39
+ Preview: import("react").ForwardRefExoticComponent<Ark.PreviewProps & import("react").RefAttributes<HTMLSpanElement>>;
40
+ Control: import("react").ForwardRefExoticComponent<Ark.ControlProps & import("react").RefAttributes<HTMLDivElement>>;
41
+ EditTrigger: import("react").ForwardRefExoticComponent<Ark.EditTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
42
+ SubmitTrigger: import("react").ForwardRefExoticComponent<Ark.SubmitTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
43
+ CancelTrigger: import("react").ForwardRefExoticComponent<Ark.CancelTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
44
+ Label: import("react").ForwardRefExoticComponent<Ark.LabelProps & import("react").RefAttributes<HTMLLabelElement>>;
45
+ Context: (props: Ark.ContextProps) => ReactNode;
46
+ };
47
+ export {};
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Text you click to edit in place, e.g. a pet's name or a price on a details
4
+ * page: it reads as text until clicked, then Enter saves and Escape cancels.
5
+ *
6
+ * <Editable value={pet.name} onValueChange={(name) => save({ name })} />
7
+ *
8
+ * - `value`, `defaultValue`: the saved text; `onValueChange`: called with it
9
+ * when an edit is saved (not on each key)
10
+ * - `placeholder`: shown when it's empty
11
+ * - `label`: a label above it (skipped inside a Field)
12
+ * - `activationMode`: 'click' (default), 'dblclick' or 'focus'
13
+ * - `controls`: ✓ and ✕ buttons while editing (true); `submitMode`: 'enter'
14
+ * (default), 'blur', 'both' or 'none' saves on those
15
+ * - `disabled`, `readOnly`
16
+ *
17
+ * By hand, Ark's parts are here too: Editable.Root, Area, Input, Preview,
18
+ * Control, EditTrigger, SubmitTrigger, CancelTrigger, Label.
19
+ */
20
+ import { useState } from 'react';
21
+ import { Editable as Ark } from '@ark-ui/react/editable';
22
+ import { useFieldContext } from '@ark-ui/react/field';
23
+ import 'iconify-icon';
24
+ import { cx, partsOf, withParts } from '../utils.js';
25
+ import { useMessages } from '../format/LocaleProvider.js';
26
+ import '../theme.css';
27
+ import './editable.css';
28
+ function EditableShortcut({ value, defaultValue = '', onValueChange, label = null, controls = true, activationMode = 'click', placeholder, className, classes, ...rest }) {
29
+ const t = useMessages();
30
+ const field = useFieldContext();
31
+ const part = partsOf('editable', classes);
32
+ // The saved text (ours, or the caller's), and the draft being typed.
33
+ const [own, setOwn] = useState(defaultValue);
34
+ const saved = value ?? own;
35
+ const [draft, setDraft] = useState(saved);
36
+ const [editing, setEditing] = useState(false);
37
+ // A new saved value from outside shows when not editing.
38
+ const [seen, setSeen] = useState(saved);
39
+ if (saved !== seen) {
40
+ setSeen(saved);
41
+ if (!editing)
42
+ setDraft(saved);
43
+ }
44
+ return (_jsxs(Ark.Root, { ...(t.zag.editable && { translations: t.zag.editable }), ...rest, ...part('root'), className: cx(className, classes?.root), activationMode: activationMode, ...(placeholder !== undefined && { placeholder }), value: draft, onValueChange: (d) => setDraft(d.value), edit: editing, onEditChange: (d) => setEditing(d.edit), onValueCommit: (d) => { if (value === undefined)
45
+ setOwn(d.value); if (d.value !== saved)
46
+ onValueChange?.(d.value); }, onValueRevert: () => setDraft(saved), children: [label && !field && _jsx(Ark.Label, { ...part('label'), children: label }), _jsxs("div", { ...part('row'), children: [_jsxs(Ark.Area, { ...part('area'), children: [_jsx(Ark.Input, { ...part('input') }), _jsx(Ark.Preview, { ...part('preview') })] }), controls && (_jsx(Ark.Control, { ...part('control'), children: editing ? _jsxs(_Fragment, { children: [_jsx(Ark.SubmitTrigger, { ...part('submit-trigger'), "aria-label": t.common.save, children: _jsx("iconify-icon", { icon: "lucide:check" }) }), _jsx(Ark.CancelTrigger, { ...part('cancel-trigger'), "aria-label": t.common.cancel, children: _jsx("iconify-icon", { icon: "lucide:x" }) })] }) : _jsx(Ark.EditTrigger, { ...part('edit-trigger'), "aria-label": t.common.edit, children: _jsx("iconify-icon", { icon: "lucide:pencil" }) }) }))] })] }));
47
+ }
48
+ export const Editable = withParts(EditableShortcut, {
49
+ Root: Ark.Root, RootProvider: Ark.RootProvider, Area: Ark.Area, Input: Ark.Input, Preview: Ark.Preview, Control: Ark.Control,
50
+ EditTrigger: Ark.EditTrigger, SubmitTrigger: Ark.SubmitTrigger, CancelTrigger: Ark.CancelTrigger, Label: Ark.Label, Context: Ark.Context,
51
+ });
@@ -0,0 +1,25 @@
1
+ @layer ui {
2
+ [data-scope=editable][data-part=root] { display: block; min-width: 0; color: var(--ui-text); font-family: var(--ui-font); font-size: 14px; }
3
+ [data-scope=editable][data-part=label] { display: block; margin-bottom: 0.25rem; font-size: 14px; font-weight: 500; }
4
+ [data-scope=editable][data-part=row] { display: flex; align-items: center; gap: 0.25rem; min-width: 0; }
5
+ [data-scope=editable][data-part=area] { flex: 1; min-width: 0; }
6
+ /* As text until clicked: a faint box on hover says it can be edited. */
7
+ [data-scope=editable][data-part=preview] { display: block; margin: 0 -0.375rem; padding: 0.25rem 0.375rem; border-radius: calc(var(--ui-radius) - 2px);
8
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: text;
9
+ &:hover { background: var(--ui-hover); }
10
+ &[data-placeholder-shown] { color: var(--ui-muted); }
11
+ [data-disabled] & { cursor: default; background: transparent; } }
12
+ [data-scope=editable][data-part=input] { width: 100%; box-sizing: border-box; margin: 0 -0.375rem; padding: 0.25rem 0.375rem;
13
+ border: 1px solid var(--ui-ring); border-radius: calc(var(--ui-radius) - 2px); outline: 2px solid var(--ui-ring-soft);
14
+ background: var(--ui-surface); color: inherit; font: inherit; }
15
+ [data-scope=editable][data-part=control] { display: flex; gap: 0.125rem; flex-shrink: 0; }
16
+ [data-scope=editable]:is([data-part=edit-trigger], [data-part=submit-trigger], [data-part=cancel-trigger]) {
17
+ display: grid; place-items: center; width: 1.75rem; height: 1.75rem; padding: 0; border: none; border-radius: calc(var(--ui-radius) - 2px);
18
+ background: transparent; color: var(--ui-muted); font-size: 14px; cursor: pointer;
19
+ &:hover { background: var(--ui-hover); color: var(--ui-text); }
20
+ &:focus-visible { outline: 2px solid var(--ui-ring-soft); } }
21
+ /* The pencil shows on hover (and when focused), to keep the text clean. */
22
+ [data-scope=editable][data-part=edit-trigger] { opacity: 0;
23
+ [data-part=root]:hover &, &:focus-visible { opacity: 1; } }
24
+ [data-scope=editable][data-part=submit-trigger] { color: var(--ui-accent); }
25
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * A rich text editor on TipTap (ProseMirror): a toolbar over an editable area
3
+ * whose value is HTML.
4
+ *
5
+ * <Editor label="Notes" placeholder="Lesson notes…" value={notes} onValueChange={setNotes} />
6
+ *
7
+ * Inside a Field it takes the field's label, hint and error.
8
+ *
9
+ * - `value`, `defaultValue`: the HTML ('' when empty); `onValueChange`
10
+ * - `label`: a label above it (skipped inside a Field)
11
+ * - `tools`: which toolbar buttons, in order ('|' for a divider): bold,
12
+ * italic, underline, strike, code, h2, h3, bulletList, orderedList,
13
+ * blockquote, link, image, undo, redo
14
+ * - `toolbar`: false hides the toolbar
15
+ * - `placeholder`: shown while it's empty
16
+ * - `maxLength`: a character limit, with a counter
17
+ * - `minHeight`, `maxHeight`: CSS lengths; it scrolls past maxHeight
18
+ * - `disabled`: read only
19
+ * - `uploadImage`: an async function taking a File and returning its URL, or
20
+ * `{ src, alt, title, data-* }` (data-* kept on the <img>, e.g. a signed id
21
+ * for your server). With it, images can be dropped, pasted or picked with
22
+ * the image button, and are inserted once uploaded; without it, dropped
23
+ * and pasted files are ignored. A failed upload calls `onError`
24
+ * - `editorRef`: a ref given the TipTap editor, for anything more
25
+ *
26
+ * Keyboard shortcuts are TipTap's (⌘B, ⌘I, ⌘U, ⌘⇧7/8 for lists, ⌘Z…).
27
+ */
28
+ import { type HTMLAttributes, type ReactNode, type RefObject } from 'react';
29
+ import { Editor as TipTap } from '@tiptap/core';
30
+ import 'iconify-icon';
31
+ import { type Classes } from '../utils.js';
32
+ import '../theme.css';
33
+ import './editor.css';
34
+ export type EditorTool = 'bold' | 'italic' | 'underline' | 'strike' | 'code' | 'h2' | 'h3' | 'bulletList' | 'orderedList' | 'blockquote' | 'link' | 'image' | 'undo' | 'redo' | '|';
35
+ export type UploadedImage = string | {
36
+ src: string;
37
+ alt?: string;
38
+ title?: string;
39
+ [data: `data-${string}`]: string;
40
+ };
41
+ export declare const defaultTools: EditorTool[];
42
+ export type EditorProps = Omit<HTMLAttributes<HTMLDivElement>, 'defaultValue' | 'onError'> & {
43
+ value?: string;
44
+ defaultValue?: string;
45
+ onValueChange?: (html: string) => void;
46
+ label?: ReactNode;
47
+ placeholder?: string;
48
+ tools?: EditorTool[];
49
+ toolbar?: boolean;
50
+ maxLength?: number | null;
51
+ minHeight?: string;
52
+ maxHeight?: string | null;
53
+ disabled?: boolean;
54
+ uploadImage?: ((file: File) => Promise<UploadedImage | null | undefined>) | null;
55
+ onError?: (error: unknown) => void;
56
+ editorRef?: RefObject<TipTap | null>;
57
+ classes?: Classes<'label' | 'root' | 'toolbar' | 'divider' | 'tool' | 'imageInput' | 'content' | 'uploading' | 'count' | 'linkForm' | 'linkActions'>;
58
+ };
59
+ export declare function Editor({ value, defaultValue, onValueChange, label, placeholder, tools, toolbar, maxLength, minHeight, maxHeight, disabled, uploadImage, onError, editorRef, className, classes, ...rest }: EditorProps): import("react").JSX.Element;
@@ -0,0 +1,220 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * A rich text editor on TipTap (ProseMirror): a toolbar over an editable area
4
+ * whose value is HTML.
5
+ *
6
+ * <Editor label="Notes" placeholder="Lesson notes…" value={notes} onValueChange={setNotes} />
7
+ *
8
+ * Inside a Field it takes the field's label, hint and error.
9
+ *
10
+ * - `value`, `defaultValue`: the HTML ('' when empty); `onValueChange`
11
+ * - `label`: a label above it (skipped inside a Field)
12
+ * - `tools`: which toolbar buttons, in order ('|' for a divider): bold,
13
+ * italic, underline, strike, code, h2, h3, bulletList, orderedList,
14
+ * blockquote, link, image, undo, redo
15
+ * - `toolbar`: false hides the toolbar
16
+ * - `placeholder`: shown while it's empty
17
+ * - `maxLength`: a character limit, with a counter
18
+ * - `minHeight`, `maxHeight`: CSS lengths; it scrolls past maxHeight
19
+ * - `disabled`: read only
20
+ * - `uploadImage`: an async function taking a File and returning its URL, or
21
+ * `{ src, alt, title, data-* }` (data-* kept on the <img>, e.g. a signed id
22
+ * for your server). With it, images can be dropped, pasted or picked with
23
+ * the image button, and are inserted once uploaded; without it, dropped
24
+ * and pasted files are ignored. A failed upload calls `onError`
25
+ * - `editorRef`: a ref given the TipTap editor, for anything more
26
+ *
27
+ * Keyboard shortcuts are TipTap's (⌘B, ⌘I, ⌘U, ⌘⇧7/8 for lists, ⌘Z…).
28
+ */
29
+ import { useEffect, useId, useReducer, useRef, useState } from 'react';
30
+ import { Editor as TipTap } from '@tiptap/core';
31
+ import StarterKit from '@tiptap/starter-kit';
32
+ import { Placeholder, CharacterCount } from '@tiptap/extensions';
33
+ import { useFieldContext } from '@ark-ui/react/field';
34
+ import 'iconify-icon';
35
+ import { RichImage } from './image.js';
36
+ import { Popover } from '../popover/Popover.js';
37
+ import { Input } from '../input/Input.js';
38
+ import { Button } from '../button/Button.js';
39
+ import { cx, partsOf, useControllable } from '../utils.js';
40
+ import { useMessages } from '../format/LocaleProvider.js';
41
+ import '../theme.css';
42
+ import './editor.css';
43
+ export const defaultTools = ['bold', 'italic', 'underline', 'strike', '|', 'h2', 'h3', '|', 'bulletList', 'orderedList', 'blockquote', '|', 'link', 'image', '|', 'undo', 'redo'];
44
+ // Each tool: its icon and label, how it runs, and when it's active.
45
+ const toolDefs = {
46
+ bold: { icon: 'lucide:bold', label: 'Bold', run: (c) => c.toggleBold(), active: ['bold'] },
47
+ italic: { icon: 'lucide:italic', label: 'Italic', run: (c) => c.toggleItalic(), active: ['italic'] },
48
+ underline: { icon: 'lucide:underline', label: 'Underline', run: (c) => c.toggleUnderline(), active: ['underline'] },
49
+ strike: { icon: 'lucide:strikethrough', label: 'Strikethrough', run: (c) => c.toggleStrike(), active: ['strike'] },
50
+ code: { icon: 'lucide:code', label: 'Code', run: (c) => c.toggleCode(), active: ['code'] },
51
+ h2: { icon: 'lucide:heading-2', label: 'Heading', run: (c) => c.toggleHeading({ level: 2 }), active: ['heading', { level: 2 }] },
52
+ h3: { icon: 'lucide:heading-3', label: 'Subheading', run: (c) => c.toggleHeading({ level: 3 }), active: ['heading', { level: 3 }] },
53
+ bulletList: { icon: 'lucide:list', label: 'Bulleted list', run: (c) => c.toggleBulletList(), active: ['bulletList'] },
54
+ orderedList: { icon: 'lucide:list-ordered', label: 'Numbered list', run: (c) => c.toggleOrderedList(), active: ['orderedList'] },
55
+ blockquote: { icon: 'lucide:text-quote', label: 'Quote', run: (c) => c.toggleBlockquote(), active: ['blockquote'] },
56
+ undo: { icon: 'lucide:undo-2', label: 'Undo', run: (c) => c.undo(), can: 'undo' },
57
+ redo: { icon: 'lucide:redo-2', label: 'Redo', run: (c) => c.redo(), can: 'redo' },
58
+ };
59
+ // A URL, or { src, alt, title, data-*… }, as the image node's attributes.
60
+ function imageAttrs(result, file) {
61
+ if (!result)
62
+ return null;
63
+ if (typeof result === 'string')
64
+ return { src: result, alt: file.name };
65
+ const data = Object.fromEntries(Object.entries(result).filter(([key]) => key.startsWith('data-')));
66
+ return { src: result.src, alt: result.alt ?? file.name, title: result.title ?? null, data: Object.keys(data).length ? data : null };
67
+ }
68
+ export function Editor({ value, defaultValue = '', onValueChange, label = null, placeholder = '', tools = defaultTools, toolbar = true, maxLength = null, minHeight = '8rem', maxHeight = null, disabled = false, uploadImage = null, onError, editorRef, className, classes, ...rest }) {
69
+ const t = useMessages();
70
+ const field = useFieldContext();
71
+ const part = partsOf('editor', classes);
72
+ const id = useId();
73
+ const labelId = field ? field.ids.label : label ? `${id}-label` : undefined;
74
+ const [current, setCurrent] = useControllable(value, defaultValue, onValueChange);
75
+ const contentEl = useRef(null);
76
+ const [editor, setEditor] = useState(null);
77
+ const [focused, setFocused] = useState(false);
78
+ const [uploading, setUploading] = useState(0);
79
+ // Bumped on every transaction, so the toolbar's states re-read the editor.
80
+ const [, bump] = useReducer((n) => n + 1, 0);
81
+ // The HTML the editor last had, to tell its own changes from new values.
82
+ const html = useRef(current ?? '');
83
+ // The latest props, for the editor's callbacks (it's made once).
84
+ const latest = useRef({ setCurrent, placeholder, uploadImage, onError, labelId, field });
85
+ useEffect(() => { latest.current = { setCurrent, placeholder, uploadImage, onError, labelId, field }; });
86
+ // Uploads the images among `files` and inserts each at `pos` (or the cursor) as it arrives.
87
+ async function insertImages(instance, files, pos = null) {
88
+ const upload = latest.current.uploadImage;
89
+ if (!upload)
90
+ return;
91
+ for (const file of files.filter((f) => (f.type ?? '').startsWith('image/'))) {
92
+ setUploading((n) => n + 1);
93
+ try {
94
+ const attrs = imageAttrs(await upload(file), file);
95
+ if (attrs && !instance.isDestroyed)
96
+ instance.chain().focus().insertContentAt(pos ?? instance.state.selection.to, { type: 'image', attrs }).run();
97
+ }
98
+ catch (error) {
99
+ latest.current.onError?.(error);
100
+ }
101
+ setUploading((n) => n - 1);
102
+ }
103
+ }
104
+ const insert = useRef(insertImages);
105
+ useEffect(() => { insert.current = insertImages; });
106
+ // Made once; the effects below keep it in step with the props.
107
+ useEffect(() => {
108
+ const instance = new TipTap({
109
+ element: contentEl.current,
110
+ content: html.current,
111
+ editable: !disabled,
112
+ extensions: [
113
+ StarterKit.configure({ heading: { levels: [2, 3] }, link: { openOnClick: false, autolink: true } }),
114
+ Placeholder.configure({ placeholder: () => latest.current.placeholder }),
115
+ CharacterCount.configure({ limit: maxLength }),
116
+ RichImage,
117
+ ],
118
+ editorProps: {
119
+ // Files dropped or pasted: images go to `uploadImage`; anything else
120
+ // is ignored, so the browser doesn't open the file in the tab.
121
+ handleDrop(view, event, _slice, moved) {
122
+ const files = Array.from(event.dataTransfer?.files ?? []);
123
+ if (moved || !files.length)
124
+ return false;
125
+ event.preventDefault();
126
+ insert.current(instance, files, view.posAtCoords({ left: event.clientX, top: event.clientY })?.pos ?? null);
127
+ return true;
128
+ },
129
+ handlePaste(_view, event) {
130
+ const files = Array.from(event.clipboardData?.files ?? []);
131
+ if (!files.length)
132
+ return false;
133
+ insert.current(instance, files);
134
+ return true;
135
+ },
136
+ // Read on every update, so the field's label and messages follow.
137
+ attributes: () => ({
138
+ role: 'textbox', 'aria-multiline': 'true',
139
+ 'aria-labelledby': latest.current.labelId ?? '', 'aria-describedby': latest.current.field?.ariaDescribedby ?? '',
140
+ 'aria-invalid': latest.current.field?.invalid ? 'true' : 'false',
141
+ }),
142
+ },
143
+ onCreate: () => setEditor(instance),
144
+ onUpdate: ({ editor }) => {
145
+ html.current = editor.isEmpty ? '' : editor.getHTML();
146
+ latest.current.setCurrent(html.current);
147
+ },
148
+ onTransaction: bump,
149
+ onFocus: () => setFocused(true),
150
+ onBlur: () => setFocused(false),
151
+ });
152
+ if (editorRef)
153
+ editorRef.current = instance;
154
+ return () => {
155
+ instance.destroy();
156
+ if (editorRef)
157
+ editorRef.current = null;
158
+ setEditor(null);
159
+ };
160
+ // Made once: maxLength and the rest are read when it's created.
161
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
162
+ // A new value from outside (not the editor's own) replaces the content.
163
+ useEffect(() => {
164
+ if (editor && (current ?? '') !== html.current) {
165
+ html.current = current ?? '';
166
+ editor.commands.setContent(html.current, { emitUpdate: false });
167
+ }
168
+ }, [editor, current]);
169
+ useEffect(() => { if (editor && editor.isEditable === disabled)
170
+ editor.setEditable(!disabled); }, [editor, disabled]);
171
+ // ProseMirror reads its attributes on updates; refresh them when the field changes.
172
+ useEffect(() => { editor?.view.setProps({}); }, [editor, labelId, field?.ariaDescribedby, field?.invalid]);
173
+ const isActive = (tool) => !!editor && !!tool.active && editor.isActive(...tool.active);
174
+ const canRun = (tool) => !!editor && !disabled && (tool.can ? editor.can()[tool.can]() : true);
175
+ const run = (tool) => editor && tool.run(editor.chain().focus()).run();
176
+ const characters = editor ? editor.storage.characterCount.characters() : 0;
177
+ // The link popover: the current link's address, applied or removed.
178
+ const [linkOpen, setLinkOpen] = useState(false);
179
+ const [linkUrl, setLinkUrl] = useState('');
180
+ const linkActive = !!editor?.isActive('link');
181
+ function openLink(open) {
182
+ if (open)
183
+ setLinkUrl(editor?.getAttributes('link')?.href ?? '');
184
+ setLinkOpen(open);
185
+ }
186
+ function applyLink(e) {
187
+ e.preventDefault();
188
+ if (!editor)
189
+ return;
190
+ const url = linkUrl.trim();
191
+ const chain = editor.chain().focus().extendMarkRange('link');
192
+ if (url)
193
+ chain.setLink({ href: /^[a-z]+:/i.test(url) ? url : `https://${url}` }).run();
194
+ else
195
+ chain.unsetLink().run();
196
+ setLinkOpen(false);
197
+ }
198
+ function removeLink() {
199
+ editor?.chain().focus().extendMarkRange('link').unsetLink().run();
200
+ setLinkOpen(false);
201
+ }
202
+ function pickedImages(e) {
203
+ const files = Array.from(e.target.files ?? []);
204
+ e.target.value = '';
205
+ if (editor)
206
+ insertImages(editor, files);
207
+ }
208
+ // Toolbar buttons keep the editor's selection (no focus on mousedown).
209
+ const keepFocus = (e) => e.preventDefault();
210
+ return _jsxs(_Fragment, { children: [label && !field && _jsx("span", { ...part('label'), id: labelId, children: label }), _jsxs("div", { ...rest, ...part('root'), className: cx(className, classes?.root), "data-focused": focused || undefined, "data-disabled": disabled || undefined, "data-invalid": field?.invalid || undefined, children: [toolbar && !disabled && (_jsx("div", { ...part('toolbar'), role: "toolbar", "aria-label": t.editor.toolbar, children: tools.map((name, i) => {
211
+ if (name === '|')
212
+ return _jsx("span", { ...part('divider') }, `d${i}`);
213
+ if (name === 'link')
214
+ return (_jsx(Popover, { placement: "bottom-start", open: linkOpen, onOpenChange: openLink, trigger: _jsx("button", { ...part('tool'), type: "button", "aria-label": t.editor.link, "aria-pressed": linkActive, onMouseDown: keepFocus, children: _jsx("iconify-icon", { icon: "lucide:link", "aria-hidden": "true" }) }), children: _jsxs("form", { ...part('link-form'), onSubmit: applyLink, children: [_jsx(Input, { size: "sm", icon: "lucide:link", placeholder: t.editor.linkPlaceholder, value: linkUrl, onValueChange: setLinkUrl }), _jsxs("div", { ...part('link-actions'), children: [linkActive && _jsx(Button, { size: "sm", variant: "ghost", onClick: removeLink, children: t.editor.remove }), _jsx(Button, { size: "sm", variant: "primary", type: "submit", children: t.editor.apply })] })] }) }, name));
215
+ if (name === 'image')
216
+ return uploadImage && (_jsxs("label", { ...part('tool'), "aria-label": t.editor.image, onMouseDown: keepFocus, children: [_jsx("input", { ...part('image-input'), type: "file", accept: "image/*", multiple: true, onChange: pickedImages }), _jsx("iconify-icon", { icon: "lucide:image", "aria-hidden": "true" })] }, name));
217
+ const tool = toolDefs[name];
218
+ return tool && (_jsx("button", { ...part('tool'), type: "button", "aria-label": t.editor.tools[name] ?? tool.label, "aria-pressed": tool.active ? isActive(tool) : undefined, disabled: !canRun(tool), onMouseDown: keepFocus, onClick: () => run(tool), children: _jsx("iconify-icon", { icon: tool.icon, "aria-hidden": "true" }) }, name));
219
+ }) })), _jsx("div", { ...part('content'), ref: contentEl, style: { '--min-height': minHeight, '--max-height': maxHeight ?? 'none' } }), uploading > 0 && _jsx("div", { ...part('uploading'), children: t.editor.uploading }), maxLength && _jsxs("div", { ...part('count'), "data-over": characters >= maxLength || undefined, children: [characters, " / ", maxLength] })] })] });
220
+ }
@@ -0,0 +1,42 @@
1
+ @layer ui {
2
+ [data-scope=editor][data-part=label] { display: block; margin-bottom: 0.375rem; font-size: 14px; font-weight: 500; color: var(--ui-text); font-family: var(--ui-font); }
3
+ [data-scope=editor][data-part=root] { display: block; position: relative; width: 100%; min-width: 0; box-sizing: border-box;
4
+ background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius);
5
+ color: var(--ui-text); font-family: var(--ui-font); font-size: 14px;
6
+ &[data-focused] { border-color: var(--ui-ring); outline: 2px solid var(--ui-ring-soft); }
7
+ &[data-invalid] { border-color: var(--ui-danger); }
8
+ &[data-disabled] { background: var(--ui-hover); } }
9
+ [data-scope=editor][data-part=toolbar] { display: flex; align-items: center; flex-wrap: wrap; gap: 0.125rem; padding: 0.25rem; border-bottom: 1px solid var(--ui-border); }
10
+ [data-scope=editor][data-part=tool] { display: grid; place-items: center; width: 2rem; height: 2rem; padding: 0; border: none;
11
+ border-radius: calc(var(--ui-radius) - 2px); background: transparent; color: var(--ui-muted); font-size: 16px; cursor: pointer;
12
+ &:hover { color: var(--ui-text); background: var(--ui-hover); }
13
+ &:focus-visible, &:focus-within:has(:focus-visible) { outline: 2px solid var(--ui-ring-soft); }
14
+ &:disabled { opacity: 0.35; cursor: default; background: transparent; }
15
+ &[aria-pressed=true] { color: var(--ui-text); background: var(--ui-hover); } }
16
+ [data-scope=editor][data-part=divider] { width: 1px; height: 1.25rem; margin: 0 0.25rem; background: var(--ui-border); }
17
+ [data-scope=editor][data-part=image-input] { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
18
+ [data-scope=editor][data-part=uploading] { position: absolute; left: 0.75rem; bottom: 0.375rem; font-size: 12px; color: var(--ui-muted); }
19
+ [data-scope=editor][data-part=link-form] { display: flex; flex-direction: column; gap: 0.5rem; width: 16rem; }
20
+ [data-scope=editor][data-part=link-actions] { display: flex; justify-content: flex-end; gap: 0.5rem; }
21
+ /* The editable area (ProseMirror renders it). */
22
+ [data-scope=editor][data-part=content] {
23
+ & .ProseMirror { min-height: var(--min-height); max-height: var(--max-height); overflow-y: auto; padding: 0.625rem 0.75rem; outline: none;
24
+ line-height: 1.6; overflow-wrap: anywhere; }
25
+ & :is(p, ul, ol, blockquote, h2, h3) { margin: 0 0 0.5rem; }
26
+ & .ProseMirror > :last-child { margin-bottom: 0; }
27
+ & h2 { font-size: 18px; font-weight: 600; line-height: 1.3; margin-top: 0.75rem; }
28
+ & h3 { font-size: 16px; font-weight: 600; line-height: 1.3; margin-top: 0.625rem; }
29
+ & :is(ul, ol) { padding-left: 1.25rem; }
30
+ & ul { list-style: disc; }
31
+ & ol { list-style: decimal; }
32
+ & li > p { margin: 0; }
33
+ & blockquote { padding-left: 0.75rem; border-left: 3px solid var(--ui-border); color: var(--ui-muted); }
34
+ & code { padding: 0.125rem 0.25rem; border-radius: 4px; background: var(--ui-hover); font-family: ui-monospace, monospace; font-size: 0.9em; }
35
+ & a { color: var(--ui-accent); text-decoration: underline; text-underline-offset: 2px; }
36
+ & img { display: block; max-width: 100%; height: auto; margin: 0.5rem 0; border-radius: var(--ui-radius); }
37
+ & img.ProseMirror-selectednode { outline: 2px solid var(--ui-ring); }
38
+ /* TipTap's placeholder: on the first empty paragraph. */
39
+ & p.is-editor-empty:first-child::before { content: attr(data-placeholder); float: left; height: 0; color: var(--ui-muted); pointer-events: none; } }
40
+ [data-scope=editor][data-part=count] { position: absolute; right: 0.625rem; bottom: 0.375rem; font-size: 12px; color: var(--ui-muted); pointer-events: none;
41
+ &[data-over] { color: var(--ui-danger); } }
42
+ }
@@ -0,0 +1 @@
1
+ export declare const RichImage: import("@tiptap/core").Node<import("@tiptap/extension-image").ImageOptions, any>;