@exsto/ds 0.0.1-rc.13 → 0.0.1-rc.14

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 (185) hide show
  1. package/dist/date-picker.js +1 -344
  2. package/dist/index.js +75 -4513
  3. package/dist/rich-text-editor.js +3 -904
  4. package/dist/source-hash.txt +1 -1
  5. package/dist/styles.css +5 -19
  6. package/dist/theme.css +2 -1
  7. package/dist/ui/src/components/edyn/animated-number/animated-number.js +108 -0
  8. package/dist/ui/src/components/edyn/animated-number/animated-number.js.map +1 -0
  9. package/dist/ui/src/components/edyn/audio-player/audio-player.js +86 -0
  10. package/dist/ui/src/components/edyn/audio-player/audio-player.js.map +1 -0
  11. package/dist/ui/src/components/edyn/date-picker/date-picker.js +250 -0
  12. package/dist/ui/src/components/edyn/date-picker/date-picker.js.map +1 -0
  13. package/dist/ui/src/components/edyn/date-picker/date-text.js +105 -0
  14. package/dist/ui/src/components/edyn/date-picker/date-text.js.map +1 -0
  15. package/dist/ui/src/components/edyn/date-range-picker/date-range-picker.js +59 -0
  16. package/dist/ui/src/components/edyn/date-range-picker/date-range-picker.js.map +1 -0
  17. package/dist/ui/src/components/edyn/initials-avatar/initials-avatar.js +86 -0
  18. package/dist/ui/src/components/edyn/initials-avatar/initials-avatar.js.map +1 -0
  19. package/dist/ui/src/components/edyn/multi-select/multi-select.js +332 -0
  20. package/dist/ui/src/components/edyn/multi-select/multi-select.js.map +1 -0
  21. package/dist/ui/src/components/edyn/progress-line/progress-line.js +100 -0
  22. package/dist/ui/src/components/edyn/progress-line/progress-line.js.map +1 -0
  23. package/dist/ui/src/components/edyn/rich-text-content/allow-list.js +37 -0
  24. package/dist/ui/src/components/edyn/rich-text-content/allow-list.js.map +1 -0
  25. package/dist/ui/src/components/edyn/rich-text-content/rich-text-content.js +124 -0
  26. package/dist/ui/src/components/edyn/rich-text-content/rich-text-content.js.map +1 -0
  27. package/dist/ui/src/components/edyn/rich-text-editor/extensions.js +124 -0
  28. package/dist/ui/src/components/edyn/rich-text-editor/extensions.js.map +1 -0
  29. package/dist/ui/src/components/edyn/rich-text-editor/mention-list.js +232 -0
  30. package/dist/ui/src/components/edyn/rich-text-editor/mention-list.js.map +1 -0
  31. package/dist/ui/src/components/edyn/rich-text-editor/rich-text-editor.js +244 -0
  32. package/dist/ui/src/components/edyn/rich-text-editor/rich-text-editor.js.map +1 -0
  33. package/dist/ui/src/components/edyn/rich-text-editor/serialize.js +55 -0
  34. package/dist/ui/src/components/edyn/rich-text-editor/serialize.js.map +1 -0
  35. package/dist/ui/src/components/edyn/rich-text-editor/sync.js +34 -0
  36. package/dist/ui/src/components/edyn/rich-text-editor/sync.js.map +1 -0
  37. package/dist/ui/src/components/edyn/rich-text-editor/toolbar.js +235 -0
  38. package/dist/ui/src/components/edyn/rich-text-editor/toolbar.js.map +1 -0
  39. package/dist/ui/src/components/edyn/rich-text-editor/types.js +26 -0
  40. package/dist/ui/src/components/edyn/rich-text-editor/types.js.map +1 -0
  41. package/dist/ui/src/components/edyn/video-player/video-player.js +132 -0
  42. package/dist/ui/src/components/edyn/video-player/video-player.js.map +1 -0
  43. package/dist/ui/src/components/ui/accordion/accordion.js +83 -0
  44. package/dist/ui/src/components/ui/accordion/accordion.js.map +1 -0
  45. package/dist/ui/src/components/ui/alert/alert.js +47 -0
  46. package/dist/ui/src/components/ui/alert/alert.js.map +1 -0
  47. package/dist/ui/src/components/ui/alert-dialog/alert-dialog.js +95 -0
  48. package/dist/ui/src/components/ui/alert-dialog/alert-dialog.js.map +1 -0
  49. package/dist/ui/src/components/ui/aspect-ratio/aspect-ratio.js +15 -0
  50. package/dist/ui/src/components/ui/aspect-ratio/aspect-ratio.js.map +1 -0
  51. package/dist/ui/src/components/ui/attachment/attachment.js +105 -0
  52. package/dist/ui/src/components/ui/attachment/attachment.js.map +1 -0
  53. package/dist/ui/src/components/ui/avatar/avatar.js +51 -0
  54. package/dist/ui/src/components/ui/avatar/avatar.js.map +1 -0
  55. package/dist/ui/src/components/ui/badge/badge.js +47 -0
  56. package/dist/ui/src/components/ui/badge/badge.js.map +1 -0
  57. package/dist/ui/src/components/ui/breadcrumb/breadcrumb.js +73 -0
  58. package/dist/ui/src/components/ui/breadcrumb/breadcrumb.js.map +1 -0
  59. package/dist/ui/src/components/ui/bubble/bubble.js +74 -0
  60. package/dist/ui/src/components/ui/bubble/bubble.js.map +1 -0
  61. package/dist/{chunks/input-syWxjY2U.js → ui/src/components/ui/button/button.js} +3 -82
  62. package/dist/ui/src/components/ui/button/button.js.map +1 -0
  63. package/dist/ui/src/components/ui/button-group/button-group.js +37 -0
  64. package/dist/ui/src/components/ui/button-group/button-group.js.map +1 -0
  65. package/dist/{chunks/date-range-picker-CWVcHgy2.js → ui/src/components/ui/calendar/calendar.js} +6 -100
  66. package/dist/ui/src/components/ui/calendar/calendar.js.map +1 -0
  67. package/dist/ui/src/components/ui/card/card.js +57 -0
  68. package/dist/ui/src/components/ui/card/card.js.map +1 -0
  69. package/dist/ui/src/components/ui/carousel/carousel.js +136 -0
  70. package/dist/ui/src/components/ui/carousel/carousel.js.map +1 -0
  71. package/dist/ui/src/components/ui/chart/chart.js +168 -0
  72. package/dist/ui/src/components/ui/chart/chart.js.map +1 -0
  73. package/dist/ui/src/components/ui/checkbox/checkbox.js +21 -0
  74. package/dist/ui/src/components/ui/checkbox/checkbox.js.map +1 -0
  75. package/dist/ui/src/components/ui/collapsible/collapsible.js +26 -0
  76. package/dist/ui/src/components/ui/collapsible/collapsible.js.map +1 -0
  77. package/dist/ui/src/components/ui/combobox/combobox.js +85 -0
  78. package/dist/ui/src/components/ui/combobox/combobox.js.map +1 -0
  79. package/dist/ui/src/components/ui/command/command.js +118 -0
  80. package/dist/ui/src/components/ui/command/command.js.map +1 -0
  81. package/dist/ui/src/components/ui/context-menu/context-menu.js +134 -0
  82. package/dist/ui/src/components/ui/context-menu/context-menu.js.map +1 -0
  83. package/dist/ui/src/components/ui/dialog/dialog.js +92 -0
  84. package/dist/ui/src/components/ui/dialog/dialog.js.map +1 -0
  85. package/dist/ui/src/components/ui/direction/direction.js +2 -0
  86. package/dist/ui/src/components/ui/drawer/drawer.js +86 -0
  87. package/dist/ui/src/components/ui/drawer/drawer.js.map +1 -0
  88. package/dist/ui/src/components/ui/dropdown-menu/dropdown-menu.js +160 -0
  89. package/dist/ui/src/components/ui/dropdown-menu/dropdown-menu.js.map +1 -0
  90. package/dist/ui/src/components/ui/empty/empty.js +61 -0
  91. package/dist/ui/src/components/ui/empty/empty.js.map +1 -0
  92. package/dist/ui/src/components/ui/field/field.js +36 -0
  93. package/dist/ui/src/components/ui/field/field.js.map +1 -0
  94. package/dist/ui/src/components/ui/hover-card/hover-card.js +38 -0
  95. package/dist/ui/src/components/ui/hover-card/hover-card.js.map +1 -0
  96. package/dist/ui/src/components/ui/indicator/indicator.js +79 -0
  97. package/dist/ui/src/components/ui/indicator/indicator.js.map +1 -0
  98. package/dist/ui/src/components/ui/input/input.js +16 -0
  99. package/dist/ui/src/components/ui/input/input.js.map +1 -0
  100. package/dist/ui/src/components/ui/input-group/input-group.js +77 -0
  101. package/dist/ui/src/components/ui/input-group/input-group.js.map +1 -0
  102. package/dist/ui/src/components/ui/input-otp/input-otp.js +48 -0
  103. package/dist/ui/src/components/ui/input-otp/input-otp.js.map +1 -0
  104. package/dist/ui/src/components/ui/item/item.js +122 -0
  105. package/dist/ui/src/components/ui/item/item.js.map +1 -0
  106. package/dist/ui/src/components/ui/kbd/kbd.js +21 -0
  107. package/dist/ui/src/components/ui/kbd/kbd.js.map +1 -0
  108. package/dist/ui/src/components/ui/label/label.js +14 -0
  109. package/dist/ui/src/components/ui/label/label.js.map +1 -0
  110. package/dist/ui/src/components/ui/marker/marker.js +44 -0
  111. package/dist/ui/src/components/ui/marker/marker.js.map +1 -0
  112. package/dist/ui/src/components/ui/menubar/menubar.js +137 -0
  113. package/dist/ui/src/components/ui/menubar/menubar.js.map +1 -0
  114. package/dist/ui/src/components/ui/message/message.js +50 -0
  115. package/dist/ui/src/components/ui/message/message.js.map +1 -0
  116. package/dist/ui/src/components/ui/message-scroller/message-scroller.js +61 -0
  117. package/dist/ui/src/components/ui/message-scroller/message-scroller.js.map +1 -0
  118. package/dist/ui/src/components/ui/native-select/native-select.js +39 -0
  119. package/dist/ui/src/components/ui/native-select/native-select.js.map +1 -0
  120. package/dist/ui/src/components/ui/navigation-menu/navigation-menu.js +84 -0
  121. package/dist/ui/src/components/ui/navigation-menu/navigation-menu.js.map +1 -0
  122. package/dist/ui/src/components/ui/pagination/pagination.js +104 -0
  123. package/dist/ui/src/components/ui/pagination/pagination.js.map +1 -0
  124. package/dist/ui/src/components/ui/popover/popover.js +56 -0
  125. package/dist/ui/src/components/ui/popover/popover.js.map +1 -0
  126. package/dist/ui/src/components/ui/progress/progress.js +70 -0
  127. package/dist/ui/src/components/ui/progress/progress.js.map +1 -0
  128. package/dist/ui/src/components/ui/questionnaire/questionnaire.js +171 -0
  129. package/dist/ui/src/components/ui/questionnaire/questionnaire.js.map +1 -0
  130. package/dist/ui/src/components/ui/radio-group/radio-group.js +29 -0
  131. package/dist/ui/src/components/ui/radio-group/radio-group.js.map +1 -0
  132. package/dist/ui/src/components/ui/resizable/resizable.js +29 -0
  133. package/dist/ui/src/components/ui/resizable/resizable.js.map +1 -0
  134. package/dist/ui/src/components/ui/scroll-area/scroll-area.js +119 -0
  135. package/dist/ui/src/components/ui/scroll-area/scroll-area.js.map +1 -0
  136. package/dist/ui/src/components/ui/select/select.js +99 -0
  137. package/dist/ui/src/components/ui/select/select.js.map +1 -0
  138. package/dist/ui/src/components/ui/separator/separator.js +30 -0
  139. package/dist/ui/src/components/ui/separator/separator.js.map +1 -0
  140. package/dist/ui/src/components/ui/sheet/sheet.js +89 -0
  141. package/dist/ui/src/components/ui/sheet/sheet.js.map +1 -0
  142. package/dist/ui/src/components/ui/sidebar/sidebar.js +383 -0
  143. package/dist/ui/src/components/ui/sidebar/sidebar.js.map +1 -0
  144. package/dist/ui/src/components/ui/skeleton/skeleton.js +14 -0
  145. package/dist/ui/src/components/ui/skeleton/skeleton.js.map +1 -0
  146. package/dist/ui/src/components/ui/slider/slider.js +49 -0
  147. package/dist/ui/src/components/ui/slider/slider.js.map +1 -0
  148. package/dist/ui/src/components/ui/spinner/spinner.js +28 -0
  149. package/dist/ui/src/components/ui/spinner/spinner.js.map +1 -0
  150. package/dist/ui/src/components/ui/switch/switch.js +20 -0
  151. package/dist/ui/src/components/ui/switch/switch.js.map +1 -0
  152. package/dist/ui/src/components/ui/table/table.js +68 -0
  153. package/dist/ui/src/components/ui/table/table.js.map +1 -0
  154. package/dist/ui/src/components/ui/tabs/tabs.js +50 -0
  155. package/dist/ui/src/components/ui/tabs/tabs.js.map +1 -0
  156. package/dist/ui/src/components/ui/textarea/textarea.js +14 -0
  157. package/dist/ui/src/components/ui/textarea/textarea.js.map +1 -0
  158. package/dist/ui/src/components/ui/toast/toast.js +124 -0
  159. package/dist/ui/src/components/ui/toast/toast.js.map +1 -0
  160. package/dist/ui/src/components/ui/toggle/toggle.js +37 -0
  161. package/dist/ui/src/components/ui/toggle/toggle.js.map +1 -0
  162. package/dist/ui/src/components/ui/toggle-group/toggle-group.js +56 -0
  163. package/dist/ui/src/components/ui/toggle-group/toggle-group.js.map +1 -0
  164. package/dist/ui/src/components/ui/tooltip/tooltip.js +42 -0
  165. package/dist/ui/src/components/ui/tooltip/tooltip.js.map +1 -0
  166. package/dist/ui/src/hooks/use-media-player.js +140 -0
  167. package/dist/ui/src/hooks/use-media-player.js.map +1 -0
  168. package/dist/ui/src/hooks/use-merged-refs.js +18 -0
  169. package/dist/ui/src/hooks/use-merged-refs.js.map +1 -0
  170. package/dist/ui/src/hooks/use-mobile.js +20 -0
  171. package/dist/ui/src/hooks/use-mobile.js.map +1 -0
  172. package/dist/ui/src/lib/first-weekday.js +34 -0
  173. package/dist/ui/src/lib/first-weekday.js.map +1 -0
  174. package/dist/ui/src/lib/popup-list.js +26 -0
  175. package/dist/ui/src/lib/popup-list.js.map +1 -0
  176. package/dist/ui/src/lib/utils.js +21 -0
  177. package/dist/ui/src/lib/utils.js.map +1 -0
  178. package/package.json +1 -1
  179. package/dist/chunks/date-range-picker-CWVcHgy2.js.map +0 -1
  180. package/dist/chunks/input-syWxjY2U.js.map +0 -1
  181. package/dist/chunks/toggle-Bxg8P-61.js +0 -490
  182. package/dist/chunks/toggle-Bxg8P-61.js.map +0 -1
  183. package/dist/date-picker.js.map +0 -1
  184. package/dist/index.js.map +0 -1
  185. package/dist/rich-text-editor.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multi-select.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/multi-select/multi-select.tsx"],"sourcesContent":["import { Combobox } from '@base-ui/react/combobox';\nimport { ScrollArea } from '@edyn/ui/components/ui/scroll-area';\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@edyn/ui/components/ui/tooltip';\nimport { popupItemClass } from '@edyn/ui/lib/popup-list';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconCheck, IconChevronDown, IconSearch } from '@tabler/icons-react';\nimport { useVirtualizer } from '@tanstack/react-virtual';\nimport {\n type ComponentPropsWithoutRef,\n type CSSProperties,\n type KeyboardEvent as ReactKeyboardEvent,\n type ReactNode,\n type RefObject,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\n\n/** An option: `leading` goes before its label (an avatar, a status dot), `count` after it (how many it would show). */\nexport type TMultiSelectOption = { value: string; label: string; count?: number; leading?: ReactNode };\n\n/** Every word the component prints, so a product can translate it. */\nexport interface IMultiSelectLabels {\n noMatches: string;\n loading: string;\n /** Shown when `error` is `true`; a string `error` replaces it. */\n error: string;\n retry: string;\n loadMore: string;\n loadingMore: string;\n clear: string;\n selectedCount: (count: number) => string;\n}\n\nexport const DEFAULT_MULTI_SELECT_LABELS: IMultiSelectLabels = {\n noMatches: 'No matches.',\n loading: 'Loading…',\n error: 'Could not load the options.',\n retry: 'Try again',\n loadMore: 'Load more',\n loadingMore: 'Loading…',\n clear: 'Clear',\n selectedCount: (count) => `${count} selected`,\n};\n\n/** Above this many options a list draws only the rows on screen. */\nexport const VIRTUALIZE_FROM = 100;\n\nconst ITEM_HEIGHT = 32;\n\n/** Anything else a button takes (data-*, aria-*, title) goes to the trigger. */\nexport interface IMultiSelectProps\n extends Omit<\n ComponentPropsWithoutRef<'button'>,\n 'placeholder' | 'value' | 'defaultValue' | 'id' | 'disabled' | 'className' | 'children' | 'onChange'\n > {\n /** The trigger's text with nothing picked, which means no filter (\"All dealer groups\"). */\n placeholder: string;\n /** The search field's prompt (\"Search dealer groups…\"). */\n searchPlaceholder: string;\n /** The options to show now: all of a short fixed list, or in async mode the ones for the typed text. */\n options: readonly TMultiSelectOption[];\n value: readonly string[];\n onValueChange: (value: string[]) => void;\n /**\n * What the trigger says once something is chosen. `selection` (default) names the one chosen or counts several;\n * `label` keeps the label and only counts, so a value the page shows elsewhere (as a removable chip, say) is\n * not repeated here: nothing chosen reads `placeholder`, anything chosen reads `labels.selectedCount(n)`.\n */\n summary?: 'selection' | 'label';\n /** For a Label's htmlFor. */\n id?: string;\n 'aria-label'?: string;\n /**\n * Marks the trigger invalid: the kit's destructive border, and a screen reader says so. For a form that does not use\n * Field; inside one, its invalid state already does this.\n */\n 'aria-invalid'?: boolean | 'true' | 'false';\n /**\n * The id of the text that explains it (the error). Added to what a Field names already, never replacing it.\n */\n 'aria-describedby'?: string;\n /** Shown but not changeable (a read-only form). */\n disabled?: boolean;\n /** Layout only (a width); the look is the kit's. */\n className?: string;\n /**\n * Passing this turns on ASYNC MODE, for lists that grow (people, dealer groups) and must\n * be searched by the API rather than loaded whole and filtered in the browser. The\n * component then never filters: it reports the typed text (after `searchDelay`) and shows\n * whatever `options` the caller supplies for it. It is also called with '' each time the\n * list opens, so the first page is fetched on demand, not at page load.\n */\n onSearchChange?: (text: string) => void;\n /** How long typing must pause before `onSearchChange` is called. Async mode only. */\n searchDelay?: number;\n /**\n * The options behind the chosen values, for the ones not in the current `options`. A\n * search that no longer returns a chosen value must not make it vanish from the trigger\n * or the count, so the caller says what it is called. Without a label a value shows as\n * itself.\n */\n selectedOptions?: readonly TMultiSelectOption[];\n /** The options for the typed text are on their way. The old ones stay until they arrive. */\n loading?: boolean;\n /** The request failed: `true` for the default message, or the message itself. */\n error?: boolean | string;\n onRetry?: () => void;\n /** There is another page after `options`. */\n hasMore?: boolean;\n onLoadMore?: () => void;\n /** The next page is on its way. */\n loadingMore?: boolean;\n labels?: Partial<IMultiSelectLabels>;\n}\n\n/**\n * The options behind the chosen values, in the order they were chosen. A value with no\n * known option is kept and shown as itself rather than dropped: a selection must never\n * change just because a list did.\n */\nexport const resolveChosen = (\n value: readonly string[],\n options: readonly TMultiSelectOption[],\n selectedOptions: readonly TMultiSelectOption[] = [],\n): TMultiSelectOption[] => {\n const known = new Map<string, TMultiSelectOption>();\n for (const option of [...selectedOptions, ...options]) {\n known.set(option.value, option);\n }\n return value.map((entry) => known.get(entry) ?? { value: entry, label: entry });\n};\n\n/** What the trigger says: the placeholder, the one picked, or a count. */\nexport const summarize = (\n chosen: readonly TMultiSelectOption[],\n placeholder: string,\n labels: IMultiSelectLabels,\n summary: 'selection' | 'label' = 'selection',\n): string => {\n if (chosen.length === 0) {\n return placeholder;\n }\n if (summary === 'label') {\n return labels.selectedCount(chosen.length);\n }\n return chosen.length === 1 ? (chosen[0]?.label ?? '') : labels.selectedCount(chosen.length);\n};\n\n/** Every chosen name, for a tooltip on a trigger whose text is cut. With `label` the names are not shown, so it is the text itself. */\nexport const fullSelection = (\n chosen: readonly TMultiSelectOption[],\n placeholder: string,\n labels: IMultiSelectLabels,\n summary: 'selection' | 'label' = 'selection',\n): string =>\n summary === 'selection' && chosen.length > 0 ? chosen.map((option) => option.label).join(', ') : summarize(chosen, placeholder, labels, summary);\n\nconst POPUP =\n 'flex max-h-(--available-height) w-(--anchor-width) min-w-64 max-w-[max(24rem,var(--anchor-width))] origin-(--transform-origin) flex-col overflow-hidden rounded-lg border border-border bg-popover text-popover-foreground shadow-md outline-none duration-100 motion-reduce:animate-none 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';\n\nconst TRIGGER =\n 'flex h-8 w-full min-w-0 cursor-pointer items-center justify-between gap-2 rounded-lg border border-input bg-transparent px-2.5 text-sm text-foreground transition-colors outline-none focus-visible:border-ring disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive data-popup-open:border-ring dark:bg-input/30';\n\nconst Option = ({\n option,\n index,\n style,\n measure,\n highlighted,\n}: {\n option: TMultiSelectOption;\n index: number;\n style?: CSSProperties;\n measure?: (node: Element | null) => void;\n highlighted: boolean;\n}) => {\n const [cut, setCut] = useState(false);\n // A callback ref, so measuring starts when the label mounts and stops when it goes; the key on the label below mounts a fresh one\n // when the text changes, which a resize observer alone would miss at an unchanged width.\n const measureLabel = useCallback((element: HTMLSpanElement | null) => {\n if (!element) {\n return;\n }\n const measureCut = (): void => setCut(element.scrollWidth > element.clientWidth);\n measureCut();\n if (typeof ResizeObserver === 'undefined') {\n return;\n }\n const observer = new ResizeObserver(measureCut);\n observer.observe(element);\n return () => observer.disconnect();\n }, []);\n\n return (\n <Combobox.Item\n value={option}\n index={index}\n ref={measure}\n data-index={index}\n style={style}\n data-slot=\"multi-select-item\"\n className={cn(popupItemClass('base-ui'), 'group')}\n >\n {/* A checkbox, filled when chosen: a set of toggles reads as one. */}\n <span\n data-slot=\"multi-select-item-indicator\"\n aria-hidden=\"true\"\n className=\"flex size-4 shrink-0 items-center justify-center rounded-sm border border-input text-primary-foreground transition-colors group-data-[selected]:border-primary group-data-[selected]:bg-primary\"\n >\n <IconCheck className=\"size-3 opacity-0 group-data-[selected]:opacity-100\" stroke={3} />\n </span>\n {option.leading}\n {/* The option is the highlighted one under the pointer and under the keyboard alike, so one controlled tooltip serves both: the full label while the text is cut. */}\n <Tooltip open={highlighted && cut}>\n <TooltipTrigger\n key={option.label}\n data-slot=\"multi-select-item-label\"\n data-overflowing={cut ? '' : undefined}\n render={<span ref={measureLabel} className=\"min-w-0 flex-1 truncate\" />}\n >\n {option.label}\n </TooltipTrigger>\n <TooltipContent side=\"right\">{option.label}</TooltipContent>\n </Tooltip>\n {option.count !== undefined && <span className=\"text-xs text-muted-foreground tabular-nums\">{option.count}</span>}\n </Combobox.Item>\n );\n};\n\n// The options on offer, every one drawn, or past VIRTUALIZE_FROM only those on screen. The\n// list is the listbox itself (the options its direct children), sized to the whole list\n// when virtualized; the ScrollArea wraps it.\nconst Options = ({\n scroller,\n scrollTo,\n busy,\n highlightedValue,\n}: {\n scroller: RefObject<HTMLDivElement | null>;\n scrollTo: RefObject<(index: number) => void>;\n busy: boolean;\n highlightedValue: string | null;\n}) => {\n const items = Combobox.useFilteredItems<TMultiSelectOption>();\n const virtual = items.length > VIRTUALIZE_FROM;\n // The scroller is an ancestor, so its ref is set only after this list's own effects:\n // one more render once mounted gives the virtualizer its element.\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n const virtualizer = useVirtualizer({\n count: items.length,\n getScrollElement: () => scroller.current,\n estimateSize: () => ITEM_HEIGHT,\n overscan: 12,\n enabled: virtual && mounted,\n });\n useEffect(() => {\n scrollTo.current = virtual ? (index) => virtualizer.scrollToIndex(index, { align: 'auto' }) : () => undefined;\n }, [scrollTo, virtual, virtualizer]);\n if (!virtual) {\n return (\n <Combobox.List aria-busy={busy || undefined} className=\"p-1 outline-none\">\n {items.map((option, index) => (\n <Option key={option.value} option={option} index={index} highlighted={highlightedValue === option.value} />\n ))}\n </Combobox.List>\n );\n }\n return (\n <Combobox.List\n data-virtualized=\"\"\n aria-busy={busy || undefined}\n className=\"relative m-1 outline-none\"\n style={{ height: virtualizer.getTotalSize() }}\n >\n {virtualizer.getVirtualItems().map((row) => {\n const option = items[row.index];\n return option === undefined ? null : (\n <Option\n key={option.value}\n option={option}\n index={row.index}\n highlighted={highlightedValue === option.value}\n measure={virtualizer.measureElement}\n style={{ position: 'absolute', top: 0, left: 0, right: 0, transform: `translateY(${row.start}px)` }}\n />\n );\n })}\n </Combobox.List>\n );\n};\n\n/**\n * A choice of several from a list: a trigger shaped like a Select that says what is picked\n * (the placeholder, the one picked, or a count), and a popup with a search field and checkbox\n * options, with the shared popup list style, the ScrollArea's bar and fades, and virtualization\n * past VIRTUALIZE_FROM options. Nothing chosen means no filter.\n *\n * For a short fixed list pass `options` and nothing else: the component filters them as the\n * user types. For a list that grows, pass `onSearchChange` and it becomes a window onto a\n * server: the caller fetches the options for the typed text, and says when they are loading,\n * failed, or have another page.\n */\nexport const MultiSelect = ({\n placeholder,\n searchPlaceholder,\n options,\n value,\n onValueChange,\n id,\n disabled = false,\n className,\n onSearchChange,\n searchDelay = 250,\n selectedOptions,\n loading = false,\n error = false,\n onRetry,\n hasMore = false,\n onLoadMore,\n loadingMore = false,\n labels: labelOverrides,\n summary = 'selection',\n ...rest\n}: IMultiSelectProps) => {\n const labels = useMemo(() => ({ ...DEFAULT_MULTI_SELECT_LABELS, ...labelOverrides }), [labelOverrides]);\n const isAsync = onSearchChange !== undefined;\n const chosen = useMemo(() => resolveChosen(value, options, selectedOptions), [value, options, selectedOptions]);\n // Chosen options the current page does not contain stay on offer at the top, so they can\n // still be unchecked after a search has moved on.\n const items = useMemo(() => {\n const offered = new Set(options.map((option) => option.value));\n return [...chosen.filter((option) => !offered.has(option.value)), ...options];\n }, [chosen, options]);\n const refScroller = useRef<HTMLDivElement | null>(null);\n const refScrollTo = useRef<(index: number) => void>(() => undefined);\n const refTimer = useRef<number | undefined>(undefined);\n const refTriggerText = useRef<HTMLSpanElement | null>(null);\n const refSearch = useRef<HTMLInputElement | null>(null);\n // The popup and the trigger's tooltip are two hover surfaces on one element, so the tooltip stands down while the popup is open.\n const [popupOpen, setPopupOpen] = useState(false);\n // The option under the pointer or the keyboard, whichever moved last, for the tooltip that shows a cut label in full.\n const [highlightedValue, setHighlightedValue] = useState<string | null>(null);\n // The text last reported through onSearchChange, and whether the popup is open. Base UI\n // reports the input as empty when the popup opens and again when it closes; reporting\n // those would fetch the first page twice on every open, and again while closed.\n const refReported = useRef<string | undefined>(undefined);\n const refOpen = useRef(false);\n const errorMessage = typeof error === 'string' ? error : labels.error;\n const failed = error !== false;\n\n useEffect(() => () => window.clearTimeout(refTimer.current), []);\n\n // Base UI gives the search field a key handler that closes the popup on Escape and keeps the key from the page, but not\n // the trigger: with focus back on the trigger and the popup still open (a click that landed while a Sheet slid in,\n // `focus()`, Shift+Tab) Escape reached the document, where an enclosing Sheet or Dialog closed along with the popup.\n // So the trigger hands Escape to the search field, the path that already works, and the key goes no further.\n const closeFromTrigger = (event: ReactKeyboardEvent<HTMLElement>) => {\n if (event.key !== 'Escape' || !popupOpen || refSearch.current === null) {\n return;\n }\n event.stopPropagation();\n event.preventDefault();\n refSearch.current.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }));\n };\n\n return (\n <Combobox.Root\n multiple\n disabled={disabled}\n items={items}\n filter={isAsync ? null : undefined}\n value={chosen}\n onValueChange={(next: TMultiSelectOption[]) => onValueChange(next.map((option) => option.value))}\n onInputValueChange={(text) => {\n if (onSearchChange === undefined || !refOpen.current) {\n return;\n }\n window.clearTimeout(refTimer.current);\n if (text === refReported.current) {\n return;\n }\n refTimer.current = window.setTimeout(() => {\n refReported.current = text;\n onSearchChange(text);\n }, searchDelay);\n }}\n onOpenChange={(open) => {\n window.clearTimeout(refTimer.current);\n setPopupOpen(open);\n if (!open) {\n setHighlightedValue(null);\n }\n refOpen.current = open;\n refReported.current = open ? '' : undefined;\n if (open && onSearchChange !== undefined) {\n onSearchChange('');\n }\n }}\n itemToStringLabel={(option: TMultiSelectOption) => option.label}\n isItemEqualToValue={(a: TMultiSelectOption, b: TMultiSelectOption) => a.value === b.value}\n virtualized={items.length > VIRTUALIZE_FROM}\n onItemHighlighted={(item, details) => {\n setHighlightedValue(item?.value ?? null);\n if (details.reason === 'keyboard' && details.index >= 0) {\n refScrollTo.current(details.index);\n }\n }}\n >\n {/* A tooltip only when the text is really cut, which is known when it is about to open. */}\n <Tooltip\n disabled={popupOpen}\n onOpenChange={(open, details) => {\n const element = refTriggerText.current;\n if (open && !(element && element.scrollWidth > element.clientWidth)) {\n details.cancel();\n }\n }}\n >\n <TooltipTrigger\n data-slot=\"multi-select-trigger\"\n render={\n <Combobox.Trigger\n {...rest}\n id={id}\n data-set={chosen.length > 0 ? '' : undefined}\n onKeyDown={closeFromTrigger}\n className={cn(TRIGGER, className)}\n />\n }\n >\n <span ref={refTriggerText} className={cn('min-w-0 flex-1 truncate text-left', chosen.length === 0 && 'text-muted-foreground')}>\n {summarize(chosen, placeholder, labels, summary)}\n </span>\n <IconChevronDown aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n </TooltipTrigger>\n <TooltipContent>{fullSelection(chosen, placeholder, labels, summary)}</TooltipContent>\n </Tooltip>\n <Combobox.Portal>\n <Combobox.Positioner sideOffset={4} align=\"start\" className=\"z-50\">\n <Combobox.Popup data-slot=\"multi-select-content\" className={POPUP}>\n <div className=\"flex shrink-0 items-center gap-2 border-b border-border px-3\">\n <IconSearch aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n <Combobox.Input\n ref={refSearch}\n placeholder={searchPlaceholder}\n aria-label={searchPlaceholder.replace(/…$/, '')}\n className=\"h-10 w-full min-w-0 bg-transparent text-sm outline-none placeholder:text-muted-foreground\"\n />\n </div>\n {/* The live region stays mounted and only its words change, which is what makes a\n screen reader announce them. */}\n <Combobox.Status data-slot=\"multi-select-status\" className=\"px-3 py-2 text-sm text-muted-foreground empty:p-0\">\n {failed ? (\n <span className=\"flex items-center justify-between gap-2 text-destructive\">\n {errorMessage}\n {onRetry !== undefined && (\n <button type=\"button\" onClick={onRetry} className=\"rounded-md px-2 py-1 font-medium text-foreground hover:bg-foreground/6\">\n {labels.retry}\n </button>\n )}\n </span>\n ) : loading ? (\n labels.loading\n ) : null}\n </Combobox.Status>\n <Combobox.Empty className=\"px-3 py-6 text-center text-sm text-muted-foreground empty:p-0\">\n {loading || failed ? null : labels.noMatches}\n </Combobox.Empty>\n <ScrollArea type=\"always\" fade viewportRef={refScroller} className=\"max-h-72 min-h-0\">\n <Options scroller={refScroller} scrollTo={refScrollTo} busy={loading} highlightedValue={highlightedValue} />\n </ScrollArea>\n {hasMore && onLoadMore !== undefined && (\n <div className=\"shrink-0 border-t border-border p-1\">\n <button\n type=\"button\"\n disabled={loadingMore}\n onClick={onLoadMore}\n className=\"h-8 w-full rounded-md px-2 text-sm font-medium text-foreground hover:bg-foreground/6 disabled:pointer-events-none disabled:opacity-50\"\n >\n {loadingMore ? labels.loadingMore : labels.loadMore}\n </button>\n </div>\n )}\n {chosen.length > 0 && (\n <div className=\"flex shrink-0 items-center justify-between gap-2 border-t border-border py-1 pr-1 pl-3 text-xs text-muted-foreground\">\n <span className=\"tabular-nums\">{labels.selectedCount(chosen.length)}</span>\n <button\n type=\"button\"\n onClick={() => onValueChange([])}\n className=\"rounded-md px-2 py-1 font-medium text-foreground hover:bg-foreground/6\"\n >\n {labels.clear}\n </button>\n </div>\n )}\n </Combobox.Popup>\n </Combobox.Positioner>\n </Combobox.Portal>\n </Combobox.Root>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ import { cn } from "../../../lib/utils.js";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ //#region ../ui/src/components/edyn/progress-line/progress-line.tsx
5
+ /** Work quicker than this shows no line at all. */
6
+ var START_DELAY = 150;
7
+ var FINISH = 150;
8
+ var FADE = 200;
9
+ /** How far an ordinary run eases towards the end without reaching it. */
10
+ var RUN_TARGET = .85;
11
+ /**
12
+ * The line's colour: an app sets --progress-line (on :root, or on any element above the
13
+ * line) to match its accent. The fallback is read where the line is, so it follows the
14
+ * theme, which a default declared once on :root would not.
15
+ */
16
+ var PROGRESS_LINE_COLOR = "var(--progress-line, var(--primary))";
17
+ var reducedMotion = () => window.matchMedia("(prefers-reduced-motion: reduce)").matches;
18
+ var scaleOf = (line) => {
19
+ const transform = line === null ? "none" : getComputedStyle(line).transform;
20
+ return transform === "none" ? 0 : new DOMMatrixReadOnly(transform).a;
21
+ };
22
+ /**
23
+ * A thin line in the accent (--progress-line, by default --primary) that runs along the top of what is loading (a table's rows
24
+ * under its header, a page under the top bar). It waits a moment before it shows, so
25
+ * quick work never flashes it; it eases towards the end without reaching it while the
26
+ * work runs; when the work is done it runs to the end and only then fades. More work
27
+ * before it has gone carries on from where it is. With reduced motion it is a
28
+ * full-width line that fades.
29
+ */
30
+ var ProgressLine = ({ active, label = "Loading", className, style, ...props }) => {
31
+ const [phase, setPhase] = useState("idle");
32
+ const [target, setTarget] = useState(RUN_TARGET);
33
+ const refPhase = useRef("idle");
34
+ const refLine = useRef(null);
35
+ useEffect(() => {
36
+ let timer;
37
+ const go = (next) => {
38
+ refPhase.current = next;
39
+ setPhase(next);
40
+ };
41
+ const later = (milliseconds, run) => {
42
+ timer = window.setTimeout(run, milliseconds);
43
+ };
44
+ const fade = () => {
45
+ go("fading");
46
+ later(FADE, () => go("idle"));
47
+ };
48
+ const phaseNow = refPhase.current;
49
+ if (active) {
50
+ if (phaseNow === "idle" || phaseNow === "waiting") {
51
+ go("waiting");
52
+ later(START_DELAY, () => {
53
+ setTarget(RUN_TARGET);
54
+ go("running");
55
+ });
56
+ } else if (phaseNow === "finishing" || phaseNow === "fading") {
57
+ const reached = scaleOf(refLine.current);
58
+ setTarget(reached > .8 ? Math.min(.97, reached + .04) : RUN_TARGET);
59
+ go("running");
60
+ }
61
+ } else if (phaseNow === "waiting") go("idle");
62
+ else if (phaseNow === "running" || phaseNow === "finishing") {
63
+ if (reducedMotion()) fade();
64
+ else {
65
+ go("finishing");
66
+ later(FINISH, fade);
67
+ }
68
+ } else if (phaseNow === "fading") later(FADE, () => go("idle"));
69
+ return () => window.clearTimeout(timer);
70
+ }, [active]);
71
+ const reduced = phase !== "idle" && reducedMotion();
72
+ const scale = phase === "idle" || phase === "waiting" ? 0 : phase === "running" && !reduced ? target : 1;
73
+ const transition = phase === "running" && !reduced ? "transform 8s cubic-bezier(0.1, 0.7, 0.15, 1), opacity 120ms ease-out" : phase === "finishing" ? `transform ${FINISH}ms ease-out` : phase === "fading" ? `opacity ${FADE}ms ease-out` : "none";
74
+ const shown = phase === "running" || phase === "finishing" || phase === "fading";
75
+ return /* @__PURE__ */ jsx("div", {
76
+ "data-slot": "progress-line",
77
+ role: "progressbar",
78
+ "aria-label": label,
79
+ ...props,
80
+ "aria-hidden": shown && phase !== "fading" ? void 0 : true,
81
+ "data-phase": phase,
82
+ className: cn("pointer-events-none h-0.5 overflow-hidden", className),
83
+ style,
84
+ children: /* @__PURE__ */ jsx("div", {
85
+ ref: refLine,
86
+ "data-slot": "progress-line-bar",
87
+ className: "h-full origin-left",
88
+ style: {
89
+ backgroundColor: PROGRESS_LINE_COLOR,
90
+ transform: `scaleX(${scale})`,
91
+ opacity: phase === "fading" || !shown ? 0 : 1,
92
+ transition
93
+ }
94
+ })
95
+ });
96
+ };
97
+ //#endregion
98
+ export { PROGRESS_LINE_COLOR, ProgressLine };
99
+
100
+ //# sourceMappingURL=progress-line.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress-line.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/progress-line/progress-line.tsx"],"sourcesContent":["import { cn } from '@edyn/ui/lib/utils';\nimport { type ComponentPropsWithoutRef, type CSSProperties, useEffect, useRef, useState } from 'react';\n\ntype TPhase = 'idle' | 'waiting' | 'running' | 'finishing' | 'fading';\n\n/** Work quicker than this shows no line at all. */\nconst START_DELAY = 150;\nconst FINISH = 150;\nconst FADE = 200;\n/** How far an ordinary run eases towards the end without reaching it. */\nconst RUN_TARGET = 0.85;\n\n/**\n * The line's colour: an app sets --progress-line (on :root, or on any element above the\n * line) to match its accent. The fallback is read where the line is, so it follows the\n * theme, which a default declared once on :root would not.\n */\nexport const PROGRESS_LINE_COLOR = 'var(--progress-line, var(--primary))';\n\nconst reducedMotion = (): boolean => window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n// How far the line has got: its scale along x, read from the transform it is drawn with right now.\nconst scaleOf = (line: HTMLElement | null): number => {\n const transform = line === null ? 'none' : getComputedStyle(line).transform;\n return transform === 'none' ? 0 : new DOMMatrixReadOnly(transform).a;\n};\n\nexport interface IProgressLineProps extends Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'className' | 'style' | 'role'> {\n /** Work is under way. */\n active: boolean;\n label?: string;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * A thin line in the accent (--progress-line, by default --primary) that runs along the top of what is loading (a table's rows\n * under its header, a page under the top bar). It waits a moment before it shows, so\n * quick work never flashes it; it eases towards the end without reaching it while the\n * work runs; when the work is done it runs to the end and only then fades. More work\n * before it has gone carries on from where it is. With reduced motion it is a\n * full-width line that fades.\n */\nexport const ProgressLine = ({ active, label = 'Loading', className, style, ...props }: IProgressLineProps) => {\n const [phase, setPhase] = useState<TPhase>('idle');\n const [target, setTarget] = useState(RUN_TARGET);\n const refPhase = useRef<TPhase>('idle');\n const refLine = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n let timer: number | undefined;\n const go = (next: TPhase): void => {\n refPhase.current = next;\n setPhase(next);\n };\n const later = (milliseconds: number, run: () => void): void => {\n timer = window.setTimeout(run, milliseconds);\n };\n const fade = (): void => {\n go('fading');\n later(FADE, () => go('idle'));\n };\n const phaseNow = refPhase.current;\n if (active) {\n if (phaseNow === 'idle' || phaseNow === 'waiting') {\n go('waiting');\n later(START_DELAY, () => {\n setTarget(RUN_TARGET);\n go('running');\n });\n } else if (phaseNow === 'finishing' || phaseNow === 'fading') {\n // More work before the line has gone: on from where it is, never back to the start.\n const reached = scaleOf(refLine.current);\n setTarget(reached > 0.8 ? Math.min(0.97, reached + 0.04) : RUN_TARGET);\n go('running');\n }\n } else if (phaseNow === 'waiting') {\n go('idle');\n } else if (phaseNow === 'running' || phaseNow === 'finishing') {\n if (reducedMotion()) {\n fade();\n } else {\n go('finishing');\n later(FINISH, fade);\n }\n } else if (phaseNow === 'fading') {\n later(FADE, () => go('idle'));\n }\n return () => window.clearTimeout(timer);\n }, [active]);\n\n const reduced = phase !== 'idle' && reducedMotion();\n const scale = phase === 'idle' || phase === 'waiting' ? 0 : phase === 'running' && !reduced ? target : 1;\n const transition =\n phase === 'running' && !reduced\n ? 'transform 8s cubic-bezier(0.1, 0.7, 0.15, 1), opacity 120ms ease-out'\n : phase === 'finishing'\n ? `transform ${FINISH}ms ease-out`\n : phase === 'fading'\n ? `opacity ${FADE}ms ease-out`\n : 'none';\n const shown = phase === 'running' || phase === 'finishing' || phase === 'fading';\n return (\n <div\n data-slot=\"progress-line\"\n role=\"progressbar\"\n aria-label={label}\n {...props}\n aria-hidden={shown && phase !== 'fading' ? undefined : true}\n data-phase={phase}\n className={cn('pointer-events-none h-0.5 overflow-hidden', className)}\n style={style}\n >\n <div\n ref={refLine}\n data-slot=\"progress-line-bar\"\n className=\"h-full origin-left\"\n style={{\n backgroundColor: PROGRESS_LINE_COLOR,\n transform: `scaleX(${scale})`,\n opacity: phase === 'fading' || !shown ? 0 : 1,\n transition,\n }}\n />\n </div>\n );\n};\n"],"mappings":";;;;;AAMA,IAAM,cAAc;AACpB,IAAM,SAAS;AACf,IAAM,OAAO;;AAEb,IAAM,aAAa;;;;;;AAOnB,IAAa,sBAAsB;AAEnC,IAAM,sBAA+B,OAAO,WAAW,kCAAkC,CAAC,CAAC;AAG3F,IAAM,WAAW,SAAqC;CACpD,MAAM,YAAY,SAAS,OAAO,SAAS,iBAAiB,IAAI,CAAC,CAAC;CAClE,OAAO,cAAc,SAAS,IAAI,IAAI,kBAAkB,SAAS,CAAC,CAAC;AACrE;;;;;;;;;AAkBA,IAAa,gBAAgB,EAAE,QAAQ,QAAQ,WAAW,WAAW,OAAO,GAAG,YAAgC;CAC7G,MAAM,CAAC,OAAO,YAAY,SAAiB,MAAM;CACjD,MAAM,CAAC,QAAQ,aAAa,SAAS,UAAU;CAC/C,MAAM,WAAW,OAAe,MAAM;CACtC,MAAM,UAAU,OAAuB,IAAI;CAE3C,gBAAgB;EACd,IAAI;EACJ,MAAM,MAAM,SAAuB;GACjC,SAAS,UAAU;GACnB,SAAS,IAAI;EACf;EACA,MAAM,SAAS,cAAsB,QAA0B;GAC7D,QAAQ,OAAO,WAAW,KAAK,YAAY;EAC7C;EACA,MAAM,aAAmB;GACvB,GAAG,QAAQ;GACX,MAAM,YAAY,GAAG,MAAM,CAAC;EAC9B;EACA,MAAM,WAAW,SAAS;EAC1B,IAAI,QAAQ;GACV,IAAI,aAAa,UAAU,aAAa,WAAW;IACjD,GAAG,SAAS;IACZ,MAAM,mBAAmB;KACvB,UAAU,UAAU;KACpB,GAAG,SAAS;IACd,CAAC;GACH,OAAO,IAAI,aAAa,eAAe,aAAa,UAAU;IAE5D,MAAM,UAAU,QAAQ,QAAQ,OAAO;IACvC,UAAU,UAAU,KAAM,KAAK,IAAI,KAAM,UAAU,GAAI,IAAI,UAAU;IACrE,GAAG,SAAS;GACd;EACF,OAAO,IAAI,aAAa,WACtB,GAAG,MAAM;OACJ,IAAI,aAAa,aAAa,aAAa,aAAa;GAC7D,IAAI,cAAc,GAChB,KAAK;QACA;IACL,GAAG,WAAW;IACd,MAAM,QAAQ,IAAI;GACpB;EACF,OAAO,IAAI,aAAa,UACtB,MAAM,YAAY,GAAG,MAAM,CAAC;EAE9B,aAAa,OAAO,aAAa,KAAK;CACxC,GAAG,CAAC,MAAM,CAAC;CAEX,MAAM,UAAU,UAAU,UAAU,cAAc;CAClD,MAAM,QAAQ,UAAU,UAAU,UAAU,YAAY,IAAI,UAAU,aAAa,CAAC,UAAU,SAAS;CACvG,MAAM,aACJ,UAAU,aAAa,CAAC,UACpB,yEACA,UAAU,cACR,aAAa,OAAO,eACpB,UAAU,WACR,WAAW,KAAK,eAChB;CACV,MAAM,QAAQ,UAAU,aAAa,UAAU,eAAe,UAAU;CACxE,OACE,oBAAC,OAAD;EACE,aAAU;EACV,MAAK;EACL,cAAY;EACZ,GAAI;EACJ,eAAa,SAAS,UAAU,WAAW,KAAA,IAAY;EACvD,cAAY;EACZ,WAAW,GAAG,6CAA6C,SAAS;EAC7D;EAEP,UAAA,oBAAC,OAAD;GACE,KAAK;GACL,aAAU;GACV,WAAU;GACV,OAAO;IACL,iBAAiB;IACjB,WAAW,UAAU,MAAM;IAC3B,SAAS,UAAU,YAAY,CAAC,QAAQ,IAAI;IAC5C;GACF;EACD,CAAA;CACE,CAAA;AAET"}
@@ -0,0 +1,37 @@
1
+ //#region ../ui/src/components/edyn/rich-text-content/allow-list.ts
2
+ /**
3
+ * The HTML the rich-text editor emits, and the only HTML RichTextContent renders. An app that
4
+ * sanitises on a server takes its allow-list from here: nothing outside these elements and this
5
+ * attribute ever comes out of the editor, whatever was pasted into it.
6
+ */
7
+ var RICH_TEXT_ELEMENTS = [
8
+ "p",
9
+ "br",
10
+ "hr",
11
+ "strong",
12
+ "em",
13
+ "u",
14
+ "ul",
15
+ "ol",
16
+ "li",
17
+ "a"
18
+ ];
19
+ /** The attributes each element may carry. Every other element carries none (no class, no style). */
20
+ var RICH_TEXT_ATTRIBUTES = { a: ["href"] };
21
+ /** A link must be absolute and use one of these. `javascript:`, `data:` and relative links are refused. */
22
+ var RICH_TEXT_LINK_PROTOCOLS = [
23
+ "http",
24
+ "https",
25
+ "mailto"
26
+ ];
27
+ var isAllowedRichTextHref = (href) => {
28
+ const cleaned = Array.from(href).filter((char) => char.charCodeAt(0) > 32).join("");
29
+ const colon = cleaned.indexOf(":");
30
+ if (colon < 1) return false;
31
+ const protocol = cleaned.slice(0, colon).toLowerCase();
32
+ return RICH_TEXT_LINK_PROTOCOLS.includes(protocol);
33
+ };
34
+ //#endregion
35
+ export { RICH_TEXT_ATTRIBUTES, RICH_TEXT_ELEMENTS, RICH_TEXT_LINK_PROTOCOLS, isAllowedRichTextHref };
36
+
37
+ //# sourceMappingURL=allow-list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"allow-list.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/rich-text-content/allow-list.ts"],"sourcesContent":["/**\n * The HTML the rich-text editor emits, and the only HTML RichTextContent renders. An app that\n * sanitises on a server takes its allow-list from here: nothing outside these elements and this\n * attribute ever comes out of the editor, whatever was pasted into it.\n */\nexport const RICH_TEXT_ELEMENTS = ['p', 'br', 'hr', 'strong', 'em', 'u', 'ul', 'ol', 'li', 'a'] as const;\n\n/** The attributes each element may carry. Every other element carries none (no class, no style). */\nexport const RICH_TEXT_ATTRIBUTES: Readonly<Record<string, readonly string[]>> = { a: ['href'] };\n\n/** A link must be absolute and use one of these. `javascript:`, `data:` and relative links are refused. */\nexport const RICH_TEXT_LINK_PROTOCOLS = ['http', 'https', 'mailto'] as const;\n\nexport const isAllowedRichTextHref = (href: string): boolean => {\n const cleaned = Array.from(href)\n .filter((char) => char.charCodeAt(0) > 32)\n .join('');\n const colon = cleaned.indexOf(':');\n if (colon < 1) {\n return false;\n }\n const protocol = cleaned.slice(0, colon).toLowerCase();\n return (RICH_TEXT_LINK_PROTOCOLS as readonly string[]).includes(protocol);\n};\n"],"mappings":";;;;;;AAKA,IAAa,qBAAqB;CAAC;CAAK;CAAM;CAAM;CAAU;CAAM;CAAK;CAAM;CAAM;CAAM;AAAG;;AAG9F,IAAa,uBAAoE,EAAE,GAAG,CAAC,MAAM,EAAE;;AAG/F,IAAa,2BAA2B;CAAC;CAAQ;CAAS;AAAQ;AAElE,IAAa,yBAAyB,SAA0B;CAC9D,MAAM,UAAU,MAAM,KAAK,IAAI,CAAC,CAC7B,QAAQ,SAAS,KAAK,WAAW,CAAC,IAAI,EAAE,CAAC,CACzC,KAAK,EAAE;CACV,MAAM,QAAQ,QAAQ,QAAQ,GAAG;CACjC,IAAI,QAAQ,GACV,OAAO;CAET,MAAM,WAAW,QAAQ,MAAM,GAAG,KAAK,CAAC,CAAC,YAAY;CACrD,OAAQ,yBAA+C,SAAS,QAAQ;AAC1E"}
@@ -0,0 +1,124 @@
1
+ import { cn } from "../../../lib/utils.js";
2
+ import { isAllowedRichTextHref } from "./allow-list.js";
3
+ import { createElement, useMemo } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region ../ui/src/components/edyn/rich-text-content/rich-text-content.tsx
6
+ /**
7
+ * How rich text looks, shared by the editor and by RichTextContent so a comment reads the same while it is typed and
8
+ * after it is saved. Descendant selectors rather than classes on the elements, because the HTML that is stored carries
9
+ * no classes.
10
+ */
11
+ 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";
12
+ var MENTION_PATTERN = /(?<![\w.+-])@(?:assignees\b|[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+)/g;
13
+ var RICH_TEXT_MENTION_CLASS = "rounded-sm bg-primary/10 px-1 py-0.5 font-medium text-primary";
14
+ var TAG_FOR = {
15
+ b: "strong",
16
+ i: "em"
17
+ };
18
+ var KEPT = /* @__PURE__ */ new Set([
19
+ "p",
20
+ "strong",
21
+ "em",
22
+ "u",
23
+ "ul",
24
+ "ol",
25
+ "li",
26
+ "a",
27
+ "br",
28
+ "hr"
29
+ ]);
30
+ var DROPPED_WITH_CONTENT = /* @__PURE__ */ new Set([
31
+ "script",
32
+ "style",
33
+ "template",
34
+ "iframe",
35
+ "object",
36
+ "embed",
37
+ "noscript",
38
+ "svg",
39
+ "math",
40
+ "head",
41
+ "title"
42
+ ]);
43
+ var withMentions = (text, key) => {
44
+ const parts = [];
45
+ let last = 0;
46
+ for (const match of text.matchAll(MENTION_PATTERN)) {
47
+ const start = match.index ?? 0;
48
+ if (start > last) parts.push(text.slice(last, start));
49
+ parts.push(/* @__PURE__ */ jsx("span", {
50
+ "data-slot": "rich-text-mention",
51
+ className: RICH_TEXT_MENTION_CLASS,
52
+ children: match[0]
53
+ }, `${key}-${start}`));
54
+ last = start + match[0].length;
55
+ }
56
+ if (last < text.length) parts.push(text.slice(last));
57
+ return parts;
58
+ };
59
+ var toNodes = (parent, key, insideLink) => {
60
+ const out = [];
61
+ parent.childNodes.forEach((child, index) => {
62
+ const childKey = `${key}.${index}`;
63
+ if (child.nodeType === 3) {
64
+ const text = child.textContent ?? "";
65
+ out.push(...insideLink ? [text] : withMentions(text, childKey));
66
+ return;
67
+ }
68
+ if (child.nodeType !== 1) return;
69
+ const element = child;
70
+ const name = element.tagName.toLowerCase();
71
+ if (DROPPED_WITH_CONTENT.has(name)) return;
72
+ const tag = TAG_FOR[name] ?? name;
73
+ if (!KEPT.has(tag)) {
74
+ out.push(...toNodes(element, childKey, insideLink));
75
+ return;
76
+ }
77
+ if (tag === "br") {
78
+ out.push(/* @__PURE__ */ jsx("br", {}, childKey));
79
+ return;
80
+ }
81
+ if (tag === "hr") {
82
+ out.push(/* @__PURE__ */ jsx("hr", {}, childKey));
83
+ return;
84
+ }
85
+ if (tag === "a") {
86
+ const href = element.getAttribute("href") ?? "";
87
+ const inner = toNodes(element, childKey, true);
88
+ out.push(...isAllowedRichTextHref(href) ? [/* @__PURE__ */ jsx("a", {
89
+ href: href.trim(),
90
+ rel: "noopener noreferrer",
91
+ target: "_blank",
92
+ children: inner
93
+ }, childKey)] : inner);
94
+ return;
95
+ }
96
+ out.push(createElement(tag, { key: childKey }, ...toNodes(element, childKey, insideLink)));
97
+ });
98
+ return out;
99
+ };
100
+ /** The HTML as React elements, keeping only what the editor can produce. Browser only (it needs a DOM parser). */
101
+ var richTextToNodes = (html) => {
102
+ if (typeof DOMParser === "undefined" || html === "") return [];
103
+ return toNodes(new DOMParser().parseFromString(`<body>${html}`, "text/html").body, "r", false);
104
+ };
105
+ /**
106
+ * Shows rich text the editor produced (or any HTML), as the same elements the editor emits and nothing else:
107
+ * `p`, `br`, `hr`, `strong`, `em`, `u`, `ul`, `ol`, `li` and `a` with an `href` (http, https or mailto; it opens in a new tab
108
+ * with `rel="noopener noreferrer"`). Everything else is dropped and its text kept. Built as React elements, not injected
109
+ * as markup, so it is safe on HTML that has not been cleaned. A run like `@name@example.test` or `@assignees` is shown
110
+ * as a chip.
111
+ */
112
+ var RichTextContent = ({ html, className, ...props }) => {
113
+ const nodes = useMemo(() => richTextToNodes(html), [html]);
114
+ return /* @__PURE__ */ jsx("div", {
115
+ "data-slot": "rich-text-content",
116
+ className: cn(RICH_TEXT_BODY_CLASS, className),
117
+ ...props,
118
+ children: nodes
119
+ });
120
+ };
121
+ //#endregion
122
+ export { RICH_TEXT_BODY_CLASS, RICH_TEXT_MENTION_CLASS, RichTextContent, richTextToNodes };
123
+
124
+ //# sourceMappingURL=rich-text-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rich-text-content.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/rich-text-content/rich-text-content.tsx"],"sourcesContent":["import { cn } from '@edyn/ui/lib/utils';\nimport { createElement, type HTMLAttributes, type ReactNode, useMemo } from 'react';\nimport { isAllowedRichTextHref } from './allow-list';\n\n/**\n * How rich text looks, shared by the editor and by RichTextContent so a comment reads the same while it is typed and\n * after it is saved. Descendant selectors rather than classes on the elements, because the HTML that is stored carries\n * no classes.\n */\nexport const RICH_TEXT_BODY_CLASS =\n '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';\n\nconst MENTION_PATTERN = /(?<![\\w.+-])@(?:assignees\\b|[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)+)/g;\n\nexport const RICH_TEXT_MENTION_CLASS = 'rounded-sm bg-primary/10 px-1 py-0.5 font-medium text-primary';\n\nconst TAG_FOR: Readonly<Record<string, string>> = { b: 'strong', i: 'em' };\nconst KEPT = new Set(['p', 'strong', 'em', 'u', 'ul', 'ol', 'li', 'a', 'br', 'hr']);\nconst DROPPED_WITH_CONTENT = new Set(['script', 'style', 'template', 'iframe', 'object', 'embed', 'noscript', 'svg', 'math', 'head', 'title']);\n\nconst withMentions = (text: string, key: string): ReactNode[] => {\n const parts: ReactNode[] = [];\n let last = 0;\n for (const match of text.matchAll(MENTION_PATTERN)) {\n const start = match.index ?? 0;\n if (start > last) {\n parts.push(text.slice(last, start));\n }\n parts.push(\n <span key={`${key}-${start}`} data-slot=\"rich-text-mention\" className={RICH_TEXT_MENTION_CLASS}>\n {match[0]}\n </span>,\n );\n last = start + match[0].length;\n }\n if (last < text.length) {\n parts.push(text.slice(last));\n }\n return parts;\n};\n\nconst toNodes = (parent: Node, key: string, insideLink: boolean): ReactNode[] => {\n const out: ReactNode[] = [];\n parent.childNodes.forEach((child, index) => {\n const childKey = `${key}.${index}`;\n if (child.nodeType === 3) {\n const text = child.textContent ?? '';\n out.push(...(insideLink ? [text] : withMentions(text, childKey)));\n return;\n }\n if (child.nodeType !== 1) {\n return;\n }\n const element = child as Element;\n const name = element.tagName.toLowerCase();\n if (DROPPED_WITH_CONTENT.has(name)) {\n return;\n }\n const tag = TAG_FOR[name] ?? name;\n if (!KEPT.has(tag)) {\n out.push(...toNodes(element, childKey, insideLink));\n return;\n }\n if (tag === 'br') {\n out.push(<br key={childKey} />);\n return;\n }\n if (tag === 'hr') {\n out.push(<hr key={childKey} />);\n return;\n }\n if (tag === 'a') {\n const href = element.getAttribute('href') ?? '';\n const inner = toNodes(element, childKey, true);\n out.push(\n ...(isAllowedRichTextHref(href)\n ? [\n <a key={childKey} href={href.trim()} rel=\"noopener noreferrer\" target=\"_blank\">\n {inner}\n </a>,\n ]\n : inner),\n );\n return;\n }\n out.push(createElement(tag, { key: childKey }, ...toNodes(element, childKey, insideLink)));\n });\n return out;\n};\n\n/** The HTML as React elements, keeping only what the editor can produce. Browser only (it needs a DOM parser). */\nexport const richTextToNodes = (html: string): ReactNode[] => {\n if (typeof DOMParser === 'undefined' || html === '') {\n return [];\n }\n const parsed = new DOMParser().parseFromString(`<body>${html}`, 'text/html');\n return toNodes(parsed.body, 'r', false);\n};\n\nexport interface IRichTextContentProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'dangerouslySetInnerHTML'> {\n /** The HTML to show. Only the editor's elements are rendered, whatever else it holds. */\n html: string;\n}\n\n/**\n * Shows rich text the editor produced (or any HTML), as the same elements the editor emits and nothing else:\n * `p`, `br`, `hr`, `strong`, `em`, `u`, `ul`, `ol`, `li` and `a` with an `href` (http, https or mailto; it opens in a new tab\n * with `rel=\"noopener noreferrer\"`). Everything else is dropped and its text kept. Built as React elements, not injected\n * as markup, so it is safe on HTML that has not been cleaned. A run like `@name@example.test` or `@assignees` is shown\n * as a chip.\n */\nexport const RichTextContent = ({ html, className, ...props }: IRichTextContentProps) => {\n const nodes = useMemo(() => richTextToNodes(html), [html]);\n return (\n <div data-slot=\"rich-text-content\" className={cn(RICH_TEXT_BODY_CLASS, className)} {...props}>\n {nodes}\n </div>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ import { isAllowedRichTextHref } from "../rich-text-content/allow-list.js";
2
+ import { countCharacters } from "./serialize.js";
3
+ import { Extension, mergeAttributes } from "@tiptap/core";
4
+ import { Bold } from "@tiptap/extension-bold";
5
+ import { Document } from "@tiptap/extension-document";
6
+ import { HardBreak } from "@tiptap/extension-hard-break";
7
+ import { HorizontalRule } from "@tiptap/extension-horizontal-rule";
8
+ import { Italic } from "@tiptap/extension-italic";
9
+ import { Link } from "@tiptap/extension-link";
10
+ import { BulletList, ListItem, OrderedList } from "@tiptap/extension-list";
11
+ import { Mention } from "@tiptap/extension-mention";
12
+ import { Paragraph } from "@tiptap/extension-paragraph";
13
+ import { Text } from "@tiptap/extension-text";
14
+ import { Underline } from "@tiptap/extension-underline";
15
+ import { UndoRedo } from "@tiptap/extensions";
16
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
17
+ //#region ../ui/src/components/edyn/rich-text-editor/extensions.ts
18
+ var MENTION_CHIP_CLASS = "rounded-sm bg-primary/10 px-1 py-0.5 font-medium text-primary";
19
+ var limitKey = new PluginKey("richTextMaxLength");
20
+ /** Refuses an edit that would take the text over the limit (a paste included) and lets one that shortens it through. */
21
+ var maxLengthExtension = (host) => Extension.create({
22
+ name: "maxLength",
23
+ addProseMirrorPlugins() {
24
+ return [new Plugin({
25
+ key: limitKey,
26
+ filterTransaction: (tr, state) => {
27
+ const limit = host.maxLength();
28
+ if (!tr.docChanged || limit === void 0) return true;
29
+ const next = countCharacters(tr.doc, host.toText());
30
+ return next <= limit || next <= countCharacters(state.doc, host.toText());
31
+ }
32
+ })];
33
+ }
34
+ });
35
+ /**
36
+ * The editor's whole schema: paragraph, line break, bold, italic, underline, bullet and numbered lists, link, and the
37
+ * mention chip. Whatever else arrives (a paste, `value`) is not in the schema, so ProseMirror drops the markup and keeps
38
+ * the text.
39
+ */
40
+ var buildExtensions = (host) => [
41
+ Document,
42
+ Paragraph,
43
+ Text,
44
+ HardBreak,
45
+ Bold,
46
+ Italic,
47
+ Underline,
48
+ BulletList,
49
+ OrderedList.extend({ addAttributes() {
50
+ return {};
51
+ } }),
52
+ ListItem,
53
+ HorizontalRule,
54
+ Link.extend({ addAttributes() {
55
+ return { href: {
56
+ default: null,
57
+ parseHTML: (element) => element.getAttribute("href"),
58
+ renderHTML: (attributes) => attributes.href ? { href: attributes.href } : {}
59
+ } };
60
+ } }).configure({
61
+ openOnClick: false,
62
+ autolink: true,
63
+ linkOnPaste: true,
64
+ defaultProtocol: "https",
65
+ protocols: ["mailto"],
66
+ isAllowedUri: (url) => isAllowedRichTextHref(url),
67
+ HTMLAttributes: {
68
+ rel: null,
69
+ target: null,
70
+ class: null
71
+ }
72
+ }),
73
+ UndoRedo,
74
+ maxLengthExtension(host),
75
+ Mention.extend({ addAttributes() {
76
+ return {
77
+ id: {
78
+ default: null,
79
+ parseHTML: (element) => element.getAttribute("data-id"),
80
+ renderHTML: (attributes) => attributes.id ? { "data-id": attributes.id } : {}
81
+ },
82
+ label: {
83
+ default: null,
84
+ parseHTML: (element) => element.getAttribute("data-label"),
85
+ renderHTML: (attributes) => attributes.label ? { "data-label": attributes.label } : {}
86
+ }
87
+ };
88
+ } }).configure({
89
+ HTMLAttributes: { class: MENTION_CHIP_CLASS },
90
+ renderText: ({ node }) => `@${node.attrs.label ?? node.attrs.id}`,
91
+ renderHTML: ({ options, node }) => [
92
+ "span",
93
+ mergeAttributes(options.HTMLAttributes),
94
+ `@${node.attrs.label ?? node.attrs.id}`
95
+ ],
96
+ suggestion: {
97
+ char: "@",
98
+ allow: () => host.mentionEnabled(),
99
+ items: () => [],
100
+ command: ({ editor, range, props }) => {
101
+ editor.chain().focus().insertContentAt(range, [{
102
+ type: "mention",
103
+ attrs: {
104
+ id: props.id,
105
+ label: props.label
106
+ }
107
+ }, {
108
+ type: "text",
109
+ text: " "
110
+ }]).run();
111
+ },
112
+ render: () => ({
113
+ onStart: (props) => host.onSuggestion(props),
114
+ onUpdate: (props) => host.onSuggestion(props),
115
+ onKeyDown: (props) => host.onSuggestionKey(props),
116
+ onExit: () => host.onSuggestion(null)
117
+ })
118
+ }
119
+ })
120
+ ];
121
+ //#endregion
122
+ export { buildExtensions };
123
+
124
+ //# sourceMappingURL=extensions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extensions.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/rich-text-editor/extensions.ts"],"sourcesContent":["import { isAllowedRichTextHref } from '@edyn/ui/components/edyn/rich-text-content/allow-list';\nimport { Extension, mergeAttributes } from '@tiptap/core';\nimport { Bold } from '@tiptap/extension-bold';\nimport { Document } from '@tiptap/extension-document';\nimport { HardBreak } from '@tiptap/extension-hard-break';\nimport { HorizontalRule } from '@tiptap/extension-horizontal-rule';\nimport { Italic } from '@tiptap/extension-italic';\nimport { Link } from '@tiptap/extension-link';\nimport { BulletList, ListItem, OrderedList } from '@tiptap/extension-list';\nimport { Mention } from '@tiptap/extension-mention';\nimport { Paragraph } from '@tiptap/extension-paragraph';\nimport { Text } from '@tiptap/extension-text';\nimport { Underline } from '@tiptap/extension-underline';\nimport { UndoRedo } from '@tiptap/extensions';\nimport { Plugin, PluginKey } from '@tiptap/pm/state';\nimport type { SuggestionKeyDownProps, SuggestionProps } from '@tiptap/suggestion';\nimport { countCharacters, type TMentionToText } from './serialize';\nimport type { IRichTextMentionItem } from './types';\n\n/** What the editor needs from the component while it runs. Held in a ref, so the extensions are built once. */\nexport interface IEditorHost {\n mentionEnabled: () => boolean;\n maxLength: () => number | undefined;\n toText: () => TMentionToText;\n onSuggestion: (props: SuggestionProps<IRichTextMentionItem, IRichTextMentionItem> | null) => void;\n onSuggestionKey: (props: SuggestionKeyDownProps) => boolean;\n}\n\nconst MENTION_CHIP_CLASS = 'rounded-sm bg-primary/10 px-1 py-0.5 font-medium text-primary';\n\nconst limitKey = new PluginKey('richTextMaxLength');\n\n/** Refuses an edit that would take the text over the limit (a paste included) and lets one that shortens it through. */\nconst maxLengthExtension = (host: IEditorHost) =>\n Extension.create({\n name: 'maxLength',\n addProseMirrorPlugins() {\n return [\n new Plugin({\n key: limitKey,\n filterTransaction: (tr, state) => {\n const limit = host.maxLength();\n if (!tr.docChanged || limit === undefined) {\n return true;\n }\n const next = countCharacters(tr.doc, host.toText());\n return next <= limit || next <= countCharacters(state.doc, host.toText());\n },\n }),\n ];\n },\n });\n\n/**\n * The editor's whole schema: paragraph, line break, bold, italic, underline, bullet and numbered lists, link, and the\n * mention chip. Whatever else arrives (a paste, `value`) is not in the schema, so ProseMirror drops the markup and keeps\n * the text.\n */\nexport const buildExtensions = (host: IEditorHost) => [\n Document,\n Paragraph,\n Text,\n HardBreak,\n Bold,\n Italic,\n Underline,\n BulletList,\n // A list is numbered from 1; the `start` it would otherwise keep is an attribute outside the allow-list.\n OrderedList.extend({\n addAttributes() {\n return {};\n },\n }),\n ListItem,\n HorizontalRule,\n Link.extend({\n addAttributes() {\n return {\n href: {\n default: null,\n parseHTML: (element) => element.getAttribute('href'),\n renderHTML: (attributes) => (attributes.href ? { href: attributes.href } : {}),\n },\n };\n },\n }).configure({\n openOnClick: false,\n autolink: true,\n linkOnPaste: true,\n defaultProtocol: 'https',\n protocols: ['mailto'],\n isAllowedUri: (url) => isAllowedRichTextHref(url),\n HTMLAttributes: { rel: null, target: null, class: null },\n }),\n UndoRedo,\n maxLengthExtension(host),\n Mention.extend({\n addAttributes() {\n return {\n id: {\n default: null,\n parseHTML: (element) => element.getAttribute('data-id'),\n renderHTML: (attributes) => (attributes.id ? { 'data-id': attributes.id } : {}),\n },\n label: {\n default: null,\n parseHTML: (element) => element.getAttribute('data-label'),\n renderHTML: (attributes) => (attributes.label ? { 'data-label': attributes.label } : {}),\n },\n };\n },\n }).configure({\n HTMLAttributes: { class: MENTION_CHIP_CLASS },\n renderText: ({ node }) => `@${node.attrs.label ?? node.attrs.id}`,\n renderHTML: ({ options, node }) => ['span', mergeAttributes(options.HTMLAttributes), `@${node.attrs.label ?? node.attrs.id}`],\n suggestion: {\n char: '@',\n allow: () => host.mentionEnabled(),\n items: () => [],\n command: ({ editor, range, props }) => {\n editor\n .chain()\n .focus()\n .insertContentAt(range, [\n { type: 'mention', attrs: { id: props.id, label: props.label } },\n { type: 'text', text: ' ' },\n ])\n .run();\n },\n render: () => ({\n onStart: (props) => host.onSuggestion(props),\n onUpdate: (props) => host.onSuggestion(props),\n onKeyDown: (props) => host.onSuggestionKey(props),\n onExit: () => host.onSuggestion(null),\n }),\n },\n }),\n];\n"],"mappings":";;;;;;;;;;;;;;;;;AA4BA,IAAM,qBAAqB;AAE3B,IAAM,WAAW,IAAI,UAAU,mBAAmB;;AAGlD,IAAM,sBAAsB,SAC1B,UAAU,OAAO;CACf,MAAM;CACN,wBAAwB;EACtB,OAAO,CACL,IAAI,OAAO;GACT,KAAK;GACL,oBAAoB,IAAI,UAAU;IAChC,MAAM,QAAQ,KAAK,UAAU;IAC7B,IAAI,CAAC,GAAG,cAAc,UAAU,KAAA,GAC9B,OAAO;IAET,MAAM,OAAO,gBAAgB,GAAG,KAAK,KAAK,OAAO,CAAC;IAClD,OAAO,QAAQ,SAAS,QAAQ,gBAAgB,MAAM,KAAK,KAAK,OAAO,CAAC;GAC1E;EACF,CAAC,CACH;CACF;AACF,CAAC;;;;;;AAOH,IAAa,mBAAmB,SAAsB;CACpD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA,YAAY,OAAO,EACjB,gBAAgB;EACd,OAAO,CAAC;CACV,EACF,CAAC;CACD;CACA;CACA,KAAK,OAAO,EACV,gBAAgB;EACd,OAAO,EACL,MAAM;GACJ,SAAS;GACT,YAAY,YAAY,QAAQ,aAAa,MAAM;GACnD,aAAa,eAAgB,WAAW,OAAO,EAAE,MAAM,WAAW,KAAK,IAAI,CAAC;EAC9E,EACF;CACF,EACF,CAAC,CAAC,CAAC,UAAU;EACX,aAAa;EACb,UAAU;EACV,aAAa;EACb,iBAAiB;EACjB,WAAW,CAAC,QAAQ;EACpB,eAAe,QAAQ,sBAAsB,GAAG;EAChD,gBAAgB;GAAE,KAAK;GAAM,QAAQ;GAAM,OAAO;EAAK;CACzD,CAAC;CACD;CACA,mBAAmB,IAAI;CACvB,QAAQ,OAAO,EACb,gBAAgB;EACd,OAAO;GACL,IAAI;IACF,SAAS;IACT,YAAY,YAAY,QAAQ,aAAa,SAAS;IACtD,aAAa,eAAgB,WAAW,KAAK,EAAE,WAAW,WAAW,GAAG,IAAI,CAAC;GAC/E;GACA,OAAO;IACL,SAAS;IACT,YAAY,YAAY,QAAQ,aAAa,YAAY;IACzD,aAAa,eAAgB,WAAW,QAAQ,EAAE,cAAc,WAAW,MAAM,IAAI,CAAC;GACxF;EACF;CACF,EACF,CAAC,CAAC,CAAC,UAAU;EACX,gBAAgB,EAAE,OAAO,mBAAmB;EAC5C,aAAa,EAAE,WAAW,IAAI,KAAK,MAAM,SAAS,KAAK,MAAM;EAC7D,aAAa,EAAE,SAAS,WAAW;GAAC;GAAQ,gBAAgB,QAAQ,cAAc;GAAG,IAAI,KAAK,MAAM,SAAS,KAAK,MAAM;EAAI;EAC5H,YAAY;GACV,MAAM;GACN,aAAa,KAAK,eAAe;GACjC,aAAa,CAAC;GACd,UAAU,EAAE,QAAQ,OAAO,YAAY;IACrC,OACG,MAAM,CAAC,CACP,MAAM,CAAC,CACP,gBAAgB,OAAO,CACtB;KAAE,MAAM;KAAW,OAAO;MAAE,IAAI,MAAM;MAAI,OAAO,MAAM;KAAM;IAAE,GAC/D;KAAE,MAAM;KAAQ,MAAM;IAAI,CAC5B,CAAC,CAAC,CACD,IAAI;GACT;GACA,eAAe;IACb,UAAU,UAAU,KAAK,aAAa,KAAK;IAC3C,WAAW,UAAU,KAAK,aAAa,KAAK;IAC5C,YAAY,UAAU,KAAK,gBAAgB,KAAK;IAChD,cAAc,KAAK,aAAa,IAAI;GACtC;EACF;CACF,CAAC;AACH"}