@exsto/ds 0.0.1-rc.5 → 0.0.1-rc.7

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 (27) hide show
  1. package/README.md +33 -0
  2. package/dist/chunks/toggle-C2c5oDaE.js +629 -0
  3. package/dist/chunks/toggle-C2c5oDaE.js.map +1 -0
  4. package/dist/ds/src/rich-text-editor.d.ts +8 -0
  5. package/dist/index.js +174 -576
  6. package/dist/index.js.map +1 -1
  7. package/dist/rich-text-editor.js +846 -0
  8. package/dist/rich-text-editor.js.map +1 -0
  9. package/dist/styles.css +221 -51
  10. package/dist/ui/src/components/edyn/date-range-picker/date-range-picker.d.ts +7 -1
  11. package/dist/ui/src/components/edyn/multi-select/multi-select.d.ts +10 -2
  12. package/dist/ui/src/components/edyn/rich-text-content/allow-list.d.ts +11 -0
  13. package/dist/ui/src/components/edyn/rich-text-content/index.d.ts +2 -0
  14. package/dist/ui/src/components/edyn/rich-text-content/rich-text-content.d.ts +22 -0
  15. package/dist/ui/src/components/edyn/rich-text-content/rich-text-content.test.d.ts +8 -0
  16. package/dist/ui/src/components/edyn/rich-text-editor/extensions.d.ts +18 -0
  17. package/dist/ui/src/components/edyn/rich-text-editor/index.d.ts +3 -0
  18. package/dist/ui/src/components/edyn/rich-text-editor/mention-list.d.ts +17 -0
  19. package/dist/ui/src/components/edyn/rich-text-editor/rich-text-editor.d.ts +53 -0
  20. package/dist/ui/src/components/edyn/rich-text-editor/rich-text-editor.test.d.ts +6 -0
  21. package/dist/ui/src/components/edyn/rich-text-editor/serialize.d.ts +13 -0
  22. package/dist/ui/src/components/edyn/rich-text-editor/toolbar.d.ts +20 -0
  23. package/dist/ui/src/components/edyn/rich-text-editor/types.d.ts +68 -0
  24. package/dist/ui/src/components/ui/popover/popover.d.ts +1 -1
  25. package/dist/ui/src/components/ui/progress/progress.d.ts +38 -4
  26. package/dist/ui/src/index.d.ts +1 -0
  27. package/package.json +21 -1
package/README.md CHANGED
@@ -80,9 +80,42 @@ query, so a subtree can be dark inside a light page.
80
80
  | Import | What it is |
81
81
  |---|---|
82
82
  | `@exsto/ds` | React components |
83
+ | `@exsto/ds/rich-text-editor` | The rich-text editor, on its own entry so its engine loads only where it is used |
83
84
  | `@exsto/ds/styles.css` | Precompiled tokens and component styles |
84
85
  | `@exsto/ds/theme.css` | Uncompiled tokens, brand layer, and the Tailwind `@theme` mapping |
85
86
 
87
+ ## Rich text
88
+
89
+ `RichTextEditor` is a small rich-text field: paragraphs, bold, italic, underline, bulleted and numbered lists, links and a horizontal line, with optional `@mentions` that your app searches. It lives on its own entry because it carries an editor engine (about 138 KB gzipped); an app that never imports it never pays for it.
90
+
91
+ ```tsx
92
+ import { RichTextContent } from '@exsto/ds';
93
+ import { RichTextEditor } from '@exsto/ds/rich-text-editor';
94
+
95
+ <RichTextEditor
96
+ aria-label="Comment"
97
+ value={html}
98
+ onChange={setHtml} // "" when there is no text
99
+ minRows={3}
100
+ mention={{ search: async (query, { page, signal }) => fetchPeople(query, page, signal) }}
101
+ />;
102
+
103
+ <RichTextContent html={savedHtml} />;
104
+ ```
105
+
106
+ The value is HTML. **The editor emits only these elements and attributes**, whatever is typed or pasted (anything else is dropped on paste and on load, keeping its text), so a server allow-list can be exactly this; the same lists are exported as `RICH_TEXT_ELEMENTS`, `RICH_TEXT_ATTRIBUTES` and `RICH_TEXT_LINK_PROTOCOLS`:
107
+
108
+ | Element | Attributes |
109
+ |---|---|
110
+ | `p`, `br`, `hr`, `strong`, `em`, `u`, `ul`, `ol`, `li` | none (no `class`, no `style`) |
111
+ | `a` | `href`, which must be absolute `http`, `https` or `mailto` |
112
+
113
+ Strong and em, not b and i; no `target` or `rel` on links (add them when you render, as `RichTextContent` does). The editor is a convenience for the writer, not a security boundary: sanitise on the server as well.
114
+
115
+ **Mentions.** Typing `@` opens a list under the caret that calls `mention.search(query, { page, signal })` after a pause (250 ms by default), aborting the previous call. Return an array (one page) or `{ items, hasMore }` (the next page is asked for when the list is scrolled to its end). An item is `{ id, label, secondary? }`: the chip shows `label` while editing, and the HTML carries **plain text, `@${id}` followed by a space, and no element** (`mention.toText` changes that). A value loaded back into the editor shows that text as text, not as a chip. `RichTextContent` marks runs that look like `@person@example.test` and `@assignees` as pills.
116
+
117
+ Also: `ref` gives `insertText(text)` and `focus()` for buttons outside the editor; `fields` adds an "Insert a field" menu that writes `{{id}}`; `maxLength` refuses edits past a number of characters; `onPasteFiles` receives files pasted without any text; `invalid` and `aria-invalid`, `disabled`, `readOnly`, `placeholder`, `labels` (every string) and `toolbar={false}` do what they say. The box is `minRows` lines tall when empty and grows to `maxRows`, then scrolls inside.
118
+
86
119
  ## Status
87
120
 
88
121
  Release candidates (`0.0.1-rc.N`). The API is not stable yet; treat minor versions
@@ -0,0 +1,629 @@
1
+ import { createElement, isValidElement, useCallback, useEffect, useMemo, useRef } from "react";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { Button } from "@base-ui/react/button";
4
+ import { clsx } from "clsx";
5
+ import { twMerge } from "tailwind-merge";
6
+ import { cva } from "class-variance-authority";
7
+ import { IconCheck, IconChevronRight } from "@tabler/icons-react";
8
+ import { Popover } from "@base-ui/react/popover";
9
+ import { ScrollArea } from "@base-ui/react/scroll-area";
10
+ import { Input } from "@base-ui/react/input";
11
+ import { Menu } from "@base-ui/react/menu";
12
+ import { Toggle } from "@base-ui/react/toggle";
13
+ //#region ../ui/src/lib/utils.ts
14
+ var cn = (...inputs) => twMerge(clsx(inputs));
15
+ /**
16
+ * Normalise a size prop to a CSS length.
17
+ *
18
+ * Bare numbers become px, which is also what a bare numeric *string* means: an
19
+ * element attribute arrives as a string, so `width="480"` and `width={480}` have
20
+ * to land on the same value. Anything with a unit, a percentage or a keyword
21
+ * passes through untouched.
22
+ */
23
+ var cssLength = (value) => {
24
+ if (value == null || value === "") return;
25
+ if (typeof value === "number") return Number.isFinite(value) ? `${value}px` : void 0;
26
+ return /^-?\d*\.?\d+$/.test(value.trim()) ? `${value.trim()}px` : value;
27
+ };
28
+ //#endregion
29
+ //#region ../ui/src/components/ui/button/button.tsx
30
+ var buttonVariants = cva("group/button inline-flex shrink-0 cursor-pointer items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all motion-reduce:transition-none motion-reduce:transform-none outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
31
+ variants: {
32
+ variant: {
33
+ default: "bg-primary text-primary-foreground hover:bg-primary/80",
34
+ outline: "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
35
+ secondary: "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
36
+ ghost: "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
37
+ destructive: "bg-destructive/10 text-destructive hover:bg-destructive/15 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/15 dark:hover:bg-destructive/20 dark:focus-visible:ring-destructive/40",
38
+ link: "text-primary underline-offset-4 hover:underline"
39
+ },
40
+ size: {
41
+ default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
42
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
43
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
44
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
45
+ icon: "size-8",
46
+ "icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
47
+ "icon-sm": "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
48
+ "icon-lg": "size-9"
49
+ }
50
+ },
51
+ defaultVariants: {
52
+ variant: "default",
53
+ size: "default"
54
+ }
55
+ });
56
+ /**
57
+ * Whether this Button is rendering a link rather than a button, so it can keep
58
+ * link semantics. Covers `render={<a href="..." />}` and a routing component
59
+ * that takes an `href` (Next's Link). A component with a different prop for its
60
+ * destination (react-router's `to`) cannot be recognised from here: give those
61
+ * `nativeButton={false} role={undefined}` directly.
62
+ */
63
+ var rendersALink = (render) => isValidElement(render) && (render.type === "a" || typeof render.props.href === "string");
64
+ /**
65
+ * A link that looks like a button is still a link, and has to be announced as
66
+ * one: it opens in a new tab, it has an address, Enter follows it and Space
67
+ * scrolls the page.
68
+ *
69
+ * Base UI needs telling, in two steps. Rendering an anchor while `nativeButton`
70
+ * is true (its default) warns in development and puts `type="button"` on the
71
+ * anchor; following the warning and passing `nativeButton={false}` then adds
72
+ * `role="button"`, which is what takes the link semantics away. So this does
73
+ * both: non-native, with the role and the tabindex Base UI would add cleared
74
+ * again. An explicit prop from the caller still wins over either.
75
+ */
76
+ var LINK_PROPS = {
77
+ nativeButton: false,
78
+ role: void 0,
79
+ tabIndex: void 0
80
+ };
81
+ var Button$1 = ({ className, variant = "default", size = "default", ...props }) => {
82
+ return /* @__PURE__ */ jsx(Button, {
83
+ "data-slot": "button",
84
+ ...rendersALink(props.render) ? LINK_PROPS : {},
85
+ className: cn(buttonVariants({
86
+ variant,
87
+ size,
88
+ className
89
+ })),
90
+ ...props
91
+ });
92
+ };
93
+ //#endregion
94
+ //#region ../ui/src/components/ui/popover/popover.tsx
95
+ var Popover$1 = ({ ...props }) => {
96
+ return /* @__PURE__ */ jsx(Popover.Root, {
97
+ "data-slot": "popover",
98
+ ...props
99
+ });
100
+ };
101
+ var PopoverTrigger = ({ ...props }) => {
102
+ return /* @__PURE__ */ jsx(Popover.Trigger, {
103
+ "data-slot": "popover-trigger",
104
+ ...props
105
+ });
106
+ };
107
+ var PopoverContent = ({ className, align = "center", alignOffset = 0, side = "bottom", sideOffset = 4, anchor, ...props }) => {
108
+ return /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
109
+ align,
110
+ alignOffset,
111
+ anchor,
112
+ side,
113
+ sideOffset,
114
+ className: "isolate z-50",
115
+ children: /* @__PURE__ */ jsx(Popover.Popup, {
116
+ "data-slot": "popover-content",
117
+ className: cn("z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 motion-reduce:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className),
118
+ ...props
119
+ })
120
+ }) });
121
+ };
122
+ var PopoverHeader = ({ className, ...props }) => {
123
+ return /* @__PURE__ */ jsx("div", {
124
+ "data-slot": "popover-header",
125
+ className: cn("flex flex-col gap-0.5 text-sm", className),
126
+ ...props
127
+ });
128
+ };
129
+ var PopoverTitle = ({ className, ...props }) => {
130
+ return /* @__PURE__ */ jsx(Popover.Title, {
131
+ "data-slot": "popover-title",
132
+ className: cn("font-medium", className),
133
+ ...props
134
+ });
135
+ };
136
+ var PopoverDescription = ({ className, ...props }) => {
137
+ return /* @__PURE__ */ jsx(Popover.Description, {
138
+ "data-slot": "popover-description",
139
+ className: cn("text-muted-foreground", className),
140
+ ...props
141
+ });
142
+ };
143
+ //#endregion
144
+ //#region ../ui/src/hooks/use-merged-refs.ts
145
+ var assignRef = (ref, value) => {
146
+ if (typeof ref === "function") ref(value);
147
+ else if (ref !== null) ref.current = value;
148
+ };
149
+ var mergeRefs = (...refs) => {
150
+ return (node) => {
151
+ for (const ref of refs) if (ref) assignRef(ref, node);
152
+ };
153
+ };
154
+ var useMergedRefs = (...refs) => {
155
+ return useCallback(mergeRefs(...refs), [...refs]);
156
+ };
157
+ //#endregion
158
+ //#region ../ui/src/components/ui/scroll-area/scroll-area.tsx
159
+ var ScrollBar = ({ className, orientation = "vertical", visibility = "hover", ...props }) => {
160
+ return /* @__PURE__ */ jsx(ScrollArea.Scrollbar, {
161
+ "data-slot": "scroll-area-scrollbar",
162
+ orientation,
163
+ className: cn("peer/scrollbar z-100 flex touch-none select-none motion-safe:transition-opacity motion-safe:duration-150", orientation === "vertical" && "h-(--scroll-bar-length) w-(--scroll-bar-size) me-(--scroll-bar-gap) mt-(--scroll-bar-offset-top) border-l border-l-transparent p-px", orientation === "horizontal" && "h-(--scroll-bar-size) ms-(--scroll-bar-gap) me-(--scroll-bar-gap) mb-(--scroll-bar-gap) flex-col border-t border-t-transparent p-px", visibility !== "always" && "opacity-0 data-scrolling:opacity-100", visibility === "hover" && "data-hovering:opacity-100 group-hover/scroll-area:opacity-100", className),
164
+ ...props,
165
+ children: /* @__PURE__ */ jsx(ScrollArea.Thumb, { className: "relative flex-1 rounded-full bg-muted-foreground/50 transition-colors hover:bg-muted-foreground/70 data-scrolling:bg-muted-foreground/70" })
166
+ });
167
+ };
168
+ /**
169
+ * Which axes fade, published for scroll-fade.css to key its mask off. The
170
+ * stylesheet reaches this element's own viewport through a child combinator, so a
171
+ * ScrollArea nested in another one cannot pick up its ancestor's edges.
172
+ */
173
+ /**
174
+ * Base UI gives the root, the viewport and the content `role="presentation"`. On
175
+ * a div that changes nothing for a screen reader, but it is an EXPLICIT role, and
176
+ * axe treats explicit presentation children as foreign to a parent that owns
177
+ * required children. A ScrollArea inside `role="menu"` or `role="listbox"` then
178
+ * fails as a critical aria-required-children violation, while the same div with
179
+ * no role passes straight through. Clearing it is what makes a ScrollArea legal
180
+ * in a popup list. A spread because the literal `role={undefined}` is flagged by
181
+ * the a11y lint rule, which cannot tell it from an invalid role.
182
+ */
183
+ var NO_ROLE = { role: void 0 };
184
+ /** Base UI makes the viewport a tab stop (0, or -1 with nothing to scroll); undefined takes that away. */
185
+ var NO_TAB_STOP = { tabIndex: void 0 };
186
+ var FADE_AXIS = {
187
+ vertical: "block",
188
+ horizontal: "inline",
189
+ both: "both"
190
+ };
191
+ /**
192
+ * Themed scroll container (Base UI): overflowing content scrolls INSIDE the component
193
+ * with a slim overlay scrollbar instead of exposing a native browser scrollbar. House
194
+ * rule: wide/tall content (tables, lists, code) always goes through this, never a bare
195
+ * overflow-* utility.
196
+ *
197
+ * Cap the height with a plain class on the component itself, e.g.
198
+ * `<ScrollArea className="max-h-88">`. The viewport picks the cap up through
199
+ * `max-h-inherit`, which matters because the viewport is the element that actually
200
+ * scrolls: a cap that lands only on the root leaves tall content spilling out
201
+ * underneath with no scrollbar, and that reads as a working height right up until the
202
+ * content outgrows it.
203
+ *
204
+ * `fade` fades out each edge that is currently hiding content, rather than always
205
+ * fading one: a permanent fade is a lie half the time, dimming the last row that fits
206
+ * and promising more below when the reader is already at the end. It is cut into the
207
+ * content by a mask (@edyn/theme/scroll-fade.css), so it needs no surface colour and
208
+ * reads correctly over a tint, a gradient or a glow. A host that keeps something
209
+ * unscrolled at the top, such as a sticky table header, exempts that band by setting
210
+ * --scroll-fade-offset on the ScrollArea. The static faded-* utilities in
211
+ * @edyn/theme/utilities.css are the other tool, for DECORATIVE edges that do not
212
+ * track scroll position.
213
+ *
214
+ * The viewport publishes its size as --scroll-area-width/--scroll-area-height CSS vars
215
+ * so children can size against the visible area.
216
+ */
217
+ var ScrollArea$1 = ({ className, children, orientation = "vertical", type = "hover", fade = false, fadeFrom = "card", scrollbarClassName, viewportId, viewportRef, viewportClassName, scrollRestorationId, focusable = true, viewPortId, viewPortRef, viewPortClassName, scrollVerticalClassName, scrollHorizontalClassName, ...props }) => {
218
+ const refInner = useRef(null);
219
+ const refsViewport = useMergedRefs(refInner, viewportRef ?? viewPortRef);
220
+ useEffect(() => {
221
+ const el = refInner.current;
222
+ if (!el) return;
223
+ const observer = new ResizeObserver(() => {
224
+ el.style.setProperty("--scroll-area-width", `${el.clientWidth}px`);
225
+ el.style.setProperty("--scroll-area-height", `${el.clientHeight}px`);
226
+ });
227
+ observer.observe(el);
228
+ return () => {
229
+ observer.disconnect();
230
+ };
231
+ }, []);
232
+ return /* @__PURE__ */ jsxs(ScrollArea.Root, {
233
+ "data-slot": "scroll-area",
234
+ "data-focus": "inset",
235
+ className: cn("group/scroll-area relative grid min-w-0 grid-rows-1 overflow-hidden", className),
236
+ "data-fade": fade ? FADE_AXIS[orientation] : void 0,
237
+ ...NO_ROLE,
238
+ ...props,
239
+ children: [
240
+ (orientation === "vertical" || orientation === "both") && /* @__PURE__ */ jsx(ScrollBar, {
241
+ orientation: "vertical",
242
+ visibility: type,
243
+ className: cn(scrollbarClassName, scrollVerticalClassName)
244
+ }),
245
+ (orientation === "horizontal" || orientation === "both") && /* @__PURE__ */ jsx(ScrollBar, {
246
+ orientation: "horizontal",
247
+ visibility: type,
248
+ className: cn(scrollbarClassName, scrollHorizontalClassName)
249
+ }),
250
+ /* @__PURE__ */ jsx(ScrollArea.Corner, {}),
251
+ /* @__PURE__ */ jsx(ScrollArea.Viewport, {
252
+ "data-slot": "scroll-area-viewport",
253
+ id: viewportId ?? viewPortId,
254
+ ref: refsViewport,
255
+ "data-scroll-restoration-id": scrollRestorationId,
256
+ ...NO_ROLE,
257
+ ...focusable ? null : NO_TAB_STOP,
258
+ className: cn("max-h-inherit w-full rounded-[inherit]", orientation === "vertical" && "overflow-x-hidden!", orientation === "horizontal" && "overflow-y-hidden!", viewportClassName ?? viewPortClassName),
259
+ children: /* @__PURE__ */ jsx(ScrollArea.Content, {
260
+ ...NO_ROLE,
261
+ style: orientation === "vertical" ? { minWidth: 0 } : void 0,
262
+ children
263
+ })
264
+ })
265
+ ]
266
+ });
267
+ };
268
+ //#endregion
269
+ //#region ../ui/src/lib/popup-list.ts
270
+ var ITEM_BASE = "relative flex min-h-8 cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-foreground outline-none select-none [&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:text-muted-foreground [&>svg:not([class*='size-'])]:size-4";
271
+ var ITEM_STATES = {
272
+ "base-ui": "data-highlighted:bg-foreground/6 data-highlighted:[&>svg]:text-foreground data-[selected]:font-medium data-disabled:pointer-events-none data-disabled:opacity-50",
273
+ cmdk: "data-[selected=true]:bg-foreground/6 data-[selected=true]:[&>svg]:text-foreground data-[checked=true]:font-medium data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50"
274
+ };
275
+ /** The shared item look. `checkable` leaves room at the end for the check. */
276
+ var popupItemClass = (flavor = "base-ui", checkable = false) => cn(ITEM_BASE, ITEM_STATES[flavor], checkable && "pr-8");
277
+ /**
278
+ * The check's slot, at the end of the item and the same size whether or not the
279
+ * check is in it, so a list does not shift as the choice moves.
280
+ */
281
+ var POPUP_ITEM_INDICATOR_CLASS = "pointer-events-none absolute right-2 flex size-4 items-center justify-center text-foreground";
282
+ /** A group's heading in any of these lists. */
283
+ var POPUP_GROUP_LABEL_CLASS = "px-2 py-1.5 text-xs font-medium text-muted-foreground";
284
+ //#endregion
285
+ //#region ../ui/src/components/edyn/rich-text-content/allow-list.ts
286
+ /**
287
+ * The HTML the rich-text editor emits, and the only HTML RichTextContent renders. An app that
288
+ * sanitises on a server takes its allow-list from here: nothing outside these elements and this
289
+ * attribute ever comes out of the editor, whatever was pasted into it.
290
+ */
291
+ var RICH_TEXT_ELEMENTS = [
292
+ "p",
293
+ "br",
294
+ "hr",
295
+ "strong",
296
+ "em",
297
+ "u",
298
+ "ul",
299
+ "ol",
300
+ "li",
301
+ "a"
302
+ ];
303
+ /** The attributes each element may carry. Every other element carries none (no class, no style). */
304
+ var RICH_TEXT_ATTRIBUTES = { a: ["href"] };
305
+ /** A link must be absolute and use one of these. `javascript:`, `data:` and relative links are refused. */
306
+ var RICH_TEXT_LINK_PROTOCOLS = [
307
+ "http",
308
+ "https",
309
+ "mailto"
310
+ ];
311
+ var isAllowedRichTextHref = (href) => {
312
+ const cleaned = Array.from(href).filter((char) => char.charCodeAt(0) > 32).join("");
313
+ const colon = cleaned.indexOf(":");
314
+ if (colon < 1) return false;
315
+ const protocol = cleaned.slice(0, colon).toLowerCase();
316
+ return RICH_TEXT_LINK_PROTOCOLS.includes(protocol);
317
+ };
318
+ //#endregion
319
+ //#region ../ui/src/components/edyn/rich-text-content/rich-text-content.tsx
320
+ /**
321
+ * How rich text looks, shared by the editor and by RichTextContent so a comment reads the same while it is typed and
322
+ * after it is saved. Descendant selectors rather than classes on the elements, because the HTML that is stored carries
323
+ * no classes.
324
+ */
325
+ var RICH_TEXT_BODY_CLASS = "text-sm leading-6 wrap-break-word [&_a]:text-primary [&_a]:underline [&_a]:underline-offset-2 [&_hr]:my-3 [&_hr]:border-0 [&_hr]:border-t [&_hr]:border-border [&_ol]:list-decimal [&_ol]:ps-5 [&_p+p]:mt-2 [&_ul]:list-disc [&_ul]:ps-5 [&_ul+p]:mt-2 [&_ol+p]:mt-2 [&_p+ul]:mt-2 [&_p+ol]:mt-2";
326
+ var MENTION_PATTERN = /(?<![\w.+-])@(?:assignees\b|[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+)/g;
327
+ var RICH_TEXT_MENTION_CLASS = "rounded-sm bg-primary/10 px-1 py-0.5 font-medium text-primary";
328
+ var TAG_FOR = {
329
+ b: "strong",
330
+ i: "em"
331
+ };
332
+ var KEPT = /* @__PURE__ */ new Set([
333
+ "p",
334
+ "strong",
335
+ "em",
336
+ "u",
337
+ "ul",
338
+ "ol",
339
+ "li",
340
+ "a",
341
+ "br",
342
+ "hr"
343
+ ]);
344
+ var DROPPED_WITH_CONTENT = /* @__PURE__ */ new Set([
345
+ "script",
346
+ "style",
347
+ "template",
348
+ "iframe",
349
+ "object",
350
+ "embed",
351
+ "noscript",
352
+ "svg",
353
+ "math",
354
+ "head",
355
+ "title"
356
+ ]);
357
+ var withMentions = (text, key) => {
358
+ const parts = [];
359
+ let last = 0;
360
+ for (const match of text.matchAll(MENTION_PATTERN)) {
361
+ const start = match.index ?? 0;
362
+ if (start > last) parts.push(text.slice(last, start));
363
+ parts.push(/* @__PURE__ */ jsx("span", {
364
+ "data-slot": "rich-text-mention",
365
+ className: RICH_TEXT_MENTION_CLASS,
366
+ children: match[0]
367
+ }, `${key}-${start}`));
368
+ last = start + match[0].length;
369
+ }
370
+ if (last < text.length) parts.push(text.slice(last));
371
+ return parts;
372
+ };
373
+ var toNodes = (parent, key, insideLink) => {
374
+ const out = [];
375
+ parent.childNodes.forEach((child, index) => {
376
+ const childKey = `${key}.${index}`;
377
+ if (child.nodeType === 3) {
378
+ const text = child.textContent ?? "";
379
+ out.push(...insideLink ? [text] : withMentions(text, childKey));
380
+ return;
381
+ }
382
+ if (child.nodeType !== 1) return;
383
+ const element = child;
384
+ const name = element.tagName.toLowerCase();
385
+ if (DROPPED_WITH_CONTENT.has(name)) return;
386
+ const tag = TAG_FOR[name] ?? name;
387
+ if (!KEPT.has(tag)) {
388
+ out.push(...toNodes(element, childKey, insideLink));
389
+ return;
390
+ }
391
+ if (tag === "br") {
392
+ out.push(/* @__PURE__ */ jsx("br", {}, childKey));
393
+ return;
394
+ }
395
+ if (tag === "hr") {
396
+ out.push(/* @__PURE__ */ jsx("hr", {}, childKey));
397
+ return;
398
+ }
399
+ if (tag === "a") {
400
+ const href = element.getAttribute("href") ?? "";
401
+ const inner = toNodes(element, childKey, true);
402
+ out.push(...isAllowedRichTextHref(href) ? [/* @__PURE__ */ jsx("a", {
403
+ href: href.trim(),
404
+ rel: "noopener noreferrer",
405
+ target: "_blank",
406
+ children: inner
407
+ }, childKey)] : inner);
408
+ return;
409
+ }
410
+ out.push(createElement(tag, { key: childKey }, ...toNodes(element, childKey, insideLink)));
411
+ });
412
+ return out;
413
+ };
414
+ /** The HTML as React elements, keeping only what the editor can produce. Browser only (it needs a DOM parser). */
415
+ var richTextToNodes = (html) => {
416
+ if (typeof DOMParser === "undefined" || html === "") return [];
417
+ return toNodes(new DOMParser().parseFromString(`<body>${html}`, "text/html").body, "r", false);
418
+ };
419
+ /**
420
+ * Shows rich text the editor produced (or any HTML), as the same elements the editor emits and nothing else:
421
+ * `p`, `br`, `hr`, `strong`, `em`, `u`, `ul`, `ol`, `li` and `a` with an `href` (http, https or mailto; it opens in a new tab
422
+ * with `rel="noopener noreferrer"`). Everything else is dropped and its text kept. Built as React elements, not injected
423
+ * as markup, so it is safe on HTML that has not been cleaned. A run like `@name@example.test` or `@assignees` is shown
424
+ * as a chip.
425
+ */
426
+ var RichTextContent = ({ html, className, ...props }) => {
427
+ const nodes = useMemo(() => richTextToNodes(html), [html]);
428
+ return /* @__PURE__ */ jsx("div", {
429
+ "data-slot": "rich-text-content",
430
+ className: cn(RICH_TEXT_BODY_CLASS, className),
431
+ ...props,
432
+ children: nodes
433
+ });
434
+ };
435
+ //#endregion
436
+ //#region ../ui/src/components/ui/input/input.tsx
437
+ var Input$1 = ({ className, type, ...props }) => {
438
+ return /* @__PURE__ */ jsx(Input, {
439
+ type,
440
+ "data-slot": "input",
441
+ className: cn("h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", className),
442
+ ...props
443
+ });
444
+ };
445
+ //#endregion
446
+ //#region ../ui/src/components/ui/dropdown-menu/dropdown-menu.tsx
447
+ var DropdownMenu = ({ ...props }) => {
448
+ return /* @__PURE__ */ jsx(Menu.Root, {
449
+ "data-slot": "dropdown-menu",
450
+ ...props
451
+ });
452
+ };
453
+ var DropdownMenuPortal = ({ ...props }) => {
454
+ return /* @__PURE__ */ jsx(Menu.Portal, {
455
+ "data-slot": "dropdown-menu-portal",
456
+ ...props
457
+ });
458
+ };
459
+ var DropdownMenuTrigger = ({ ...props }) => {
460
+ return /* @__PURE__ */ jsx(Menu.Trigger, {
461
+ "data-slot": "dropdown-menu-trigger",
462
+ ...props
463
+ });
464
+ };
465
+ /**
466
+ * Scrolls inside a DS ScrollArea like the other popup lists, with the bar shown
467
+ * for as long as the menu overflows and a fade on whichever end is cut off.
468
+ *
469
+ * Base UI puts `role="menu"` on this popup itself and offers no inner list part
470
+ * to move it onto, so the ScrollArea sits INSIDE the role. That is only legal
471
+ * because ScrollArea drops the `role="presentation"` Base UI gives its root,
472
+ * viewport and content: a menu may not own presentation wrappers (axe reports
473
+ * a critical aria-required-children violation), but it may own plain divs.
474
+ * e2e/a11y.spec.ts scans the open menu in both themes to keep it that way.
475
+ */
476
+ var DropdownMenuContent = ({ align = "start", alignOffset = 0, side = "bottom", sideOffset = 4, className, children, ...props }) => {
477
+ return /* @__PURE__ */ jsx(Menu.Portal, { children: /* @__PURE__ */ jsx(Menu.Positioner, {
478
+ className: "isolate z-50 outline-none",
479
+ align,
480
+ alignOffset,
481
+ side,
482
+ sideOffset,
483
+ children: /* @__PURE__ */ jsx(Menu.Popup, {
484
+ "data-slot": "dropdown-menu-content",
485
+ className: cn("z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95", "motion-reduce:animate-none", className),
486
+ ...props,
487
+ children: /* @__PURE__ */ jsx(ScrollArea$1, {
488
+ type: "always",
489
+ fade: true,
490
+ focusable: false,
491
+ className: "max-h-inherit",
492
+ children: /* @__PURE__ */ jsx("div", {
493
+ className: "p-1",
494
+ children
495
+ })
496
+ })
497
+ })
498
+ }) });
499
+ };
500
+ var DropdownMenuGroup = ({ ...props }) => {
501
+ return /* @__PURE__ */ jsx(Menu.Group, {
502
+ "data-slot": "dropdown-menu-group",
503
+ ...props
504
+ });
505
+ };
506
+ var DropdownMenuLabel = ({ className, inset, ...props }) => {
507
+ return /* @__PURE__ */ jsx(Menu.GroupLabel, {
508
+ "data-slot": "dropdown-menu-label",
509
+ "data-inset": inset,
510
+ className: cn(POPUP_GROUP_LABEL_CLASS, "data-[inset=true]:pl-7", className),
511
+ ...props
512
+ });
513
+ };
514
+ var DropdownMenuItem = ({ className, inset, variant = "default", ...props }) => {
515
+ return /* @__PURE__ */ jsx(Menu.Item, {
516
+ "data-slot": "dropdown-menu-item",
517
+ "data-inset": inset,
518
+ "data-variant": variant,
519
+ className: cn("group/dropdown-menu-item", popupItemClass(), "data-[inset=true]:pl-7", "data-[variant=destructive]:text-destructive data-[variant=destructive]:[&>svg]:text-destructive", "data-[variant=destructive]:data-highlighted:bg-destructive/10 data-[variant=destructive]:data-highlighted:text-destructive", className),
520
+ ...props
521
+ });
522
+ };
523
+ var DropdownMenuSub = ({ ...props }) => {
524
+ return /* @__PURE__ */ jsx(Menu.SubmenuRoot, {
525
+ "data-slot": "dropdown-menu-sub",
526
+ ...props
527
+ });
528
+ };
529
+ var DropdownMenuSubTrigger = ({ className, inset, children, ...props }) => {
530
+ return /* @__PURE__ */ jsxs(Menu.SubmenuTrigger, {
531
+ "data-slot": "dropdown-menu-sub-trigger",
532
+ "data-inset": inset,
533
+ className: cn(popupItemClass(), "data-[inset=true]:pl-7 data-popup-open:bg-foreground/6 data-popup-open:[&>svg]:text-foreground", className),
534
+ ...props,
535
+ children: [children, /* @__PURE__ */ jsx(IconChevronRight, { className: "ml-auto" })]
536
+ });
537
+ };
538
+ var DropdownMenuSubContent = ({ align = "start", alignOffset = -3, side = "right", sideOffset = 0, className, ...props }) => {
539
+ return /* @__PURE__ */ jsx(DropdownMenuContent, {
540
+ "data-slot": "dropdown-menu-sub-content",
541
+ className: cn("w-auto min-w-[96px] rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", "motion-reduce:animate-none", className),
542
+ align,
543
+ alignOffset,
544
+ side,
545
+ sideOffset,
546
+ ...props
547
+ });
548
+ };
549
+ var DropdownMenuCheckboxItem = ({ className, children, checked, inset, ...props }) => {
550
+ return /* @__PURE__ */ jsxs(Menu.CheckboxItem, {
551
+ "data-slot": "dropdown-menu-checkbox-item",
552
+ "data-inset": inset,
553
+ className: cn(popupItemClass("base-ui", true), "data-checked:font-medium data-[inset=true]:pl-7", className),
554
+ checked,
555
+ ...props,
556
+ children: [/* @__PURE__ */ jsx("span", {
557
+ className: POPUP_ITEM_INDICATOR_CLASS,
558
+ "data-slot": "dropdown-menu-checkbox-item-indicator",
559
+ children: /* @__PURE__ */ jsx(Menu.CheckboxItemIndicator, { children: /* @__PURE__ */ jsx(IconCheck, {}) })
560
+ }), children]
561
+ });
562
+ };
563
+ var DropdownMenuRadioGroup = ({ ...props }) => {
564
+ return /* @__PURE__ */ jsx(Menu.RadioGroup, {
565
+ "data-slot": "dropdown-menu-radio-group",
566
+ ...props
567
+ });
568
+ };
569
+ var DropdownMenuRadioItem = ({ className, children, inset, ...props }) => {
570
+ return /* @__PURE__ */ jsxs(Menu.RadioItem, {
571
+ "data-slot": "dropdown-menu-radio-item",
572
+ "data-inset": inset,
573
+ className: cn(popupItemClass("base-ui", true), "data-checked:font-medium data-[inset=true]:pl-7", className),
574
+ ...props,
575
+ children: [/* @__PURE__ */ jsx("span", {
576
+ className: POPUP_ITEM_INDICATOR_CLASS,
577
+ "data-slot": "dropdown-menu-radio-item-indicator",
578
+ children: /* @__PURE__ */ jsx(Menu.RadioItemIndicator, { children: /* @__PURE__ */ jsx(IconCheck, {}) })
579
+ }), children]
580
+ });
581
+ };
582
+ var DropdownMenuSeparator = ({ className, ...props }) => {
583
+ return /* @__PURE__ */ jsx(Menu.Separator, {
584
+ "data-slot": "dropdown-menu-separator",
585
+ className: cn("-mx-1 my-1 h-px bg-border", className),
586
+ ...props
587
+ });
588
+ };
589
+ var DropdownMenuShortcut = ({ className, ...props }) => {
590
+ return /* @__PURE__ */ jsx("span", {
591
+ "data-slot": "dropdown-menu-shortcut",
592
+ className: cn("ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground", className),
593
+ ...props
594
+ });
595
+ };
596
+ //#endregion
597
+ //#region ../ui/src/components/ui/toggle/toggle.tsx
598
+ var toggleVariants = cva("inline-flex shrink-0 cursor-pointer items-center justify-center gap-1.5 rounded-lg border border-transparent text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 data-pressed:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\"size-\"])]:size-4", {
599
+ variants: {
600
+ variant: {
601
+ default: "bg-transparent hover:bg-muted hover:text-foreground data-pressed:bg-muted",
602
+ outline: "border-border bg-background hover:bg-muted hover:text-foreground data-pressed:bg-muted dark:border-input dark:bg-input/30 dark:hover:bg-input/50"
603
+ },
604
+ size: {
605
+ sm: "h-7 px-2.5 text-[0.8rem]",
606
+ default: "h-8 px-2.5",
607
+ lg: "h-9 px-3"
608
+ }
609
+ },
610
+ defaultVariants: {
611
+ variant: "default",
612
+ size: "default"
613
+ }
614
+ });
615
+ var Toggle$1 = ({ className, variant = "default", size = "default", ...props }) => {
616
+ return /* @__PURE__ */ jsx(Toggle, {
617
+ "data-slot": "toggle",
618
+ className: cn(toggleVariants({
619
+ variant,
620
+ size,
621
+ className
622
+ })),
623
+ ...props
624
+ });
625
+ };
626
+ //#endregion
627
+ export { ScrollBar as A, cn as B, RICH_TEXT_ATTRIBUTES as C, POPUP_ITEM_INDICATOR_CLASS as D, isAllowedRichTextHref as E, PopoverHeader as F, PopoverTitle as I, PopoverTrigger as L, Popover$1 as M, PopoverContent as N, popupItemClass as O, PopoverDescription as P, Button$1 as R, richTextToNodes as S, RICH_TEXT_LINK_PROTOCOLS as T, cssLength as V, DropdownMenuTrigger as _, DropdownMenuContent as a, RICH_TEXT_MENTION_CLASS as b, DropdownMenuLabel as c, DropdownMenuRadioItem as d, DropdownMenuSeparator as f, DropdownMenuSubTrigger as g, DropdownMenuSubContent as h, DropdownMenuCheckboxItem as i, useMergedRefs as j, ScrollArea$1 as k, DropdownMenuPortal as l, DropdownMenuSub as m, toggleVariants as n, DropdownMenuGroup as o, DropdownMenuShortcut as p, DropdownMenu as r, DropdownMenuItem as s, Toggle$1 as t, DropdownMenuRadioGroup as u, Input$1 as v, RICH_TEXT_ELEMENTS as w, RichTextContent as x, RICH_TEXT_BODY_CLASS as y, buttonVariants as z };
628
+
629
+ //# sourceMappingURL=toggle-C2c5oDaE.js.map