@exsto/ds 0.0.1-rc.18 → 0.0.1-rc.19
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.
- package/README.md +22 -1
- package/dist/index.js +3 -2
- package/dist/source-hash.txt +1 -1
- package/dist/styles.css +54 -55
- package/dist/theme.css +6 -1
- package/dist/ui/src/components/edyn/file-input/file-input.js +2 -2
- package/dist/ui/src/components/edyn/file-input/file-input.js.map +1 -1
- package/dist/ui/src/components/edyn/multi-select/multi-select.js +6 -6
- package/dist/ui/src/components/edyn/multi-select/multi-select.js.map +1 -1
- package/dist/ui/src/components/edyn/rich-text-editor/mention-list.js +1 -1
- package/dist/ui/src/components/edyn/rich-text-editor/rich-text-editor.js +1 -1
- package/dist/ui/src/components/edyn/rich-text-editor/rich-text-editor.js.map +1 -1
- package/dist/ui/src/components/edyn/rich-text-editor/toolbar.js +1 -1
- package/dist/ui/src/components/edyn/rich-text-editor/toolbar.js.map +1 -1
- package/dist/ui/src/components/edyn/time-field/time-field.js +8 -1
- package/dist/ui/src/components/edyn/time-field/time-field.js.map +1 -1
- package/dist/ui/src/components/ui/breadcrumb/breadcrumb.js +1 -1
- package/dist/ui/src/components/ui/breadcrumb/breadcrumb.js.map +1 -1
- package/dist/ui/src/components/ui/button/button.js +2 -2
- package/dist/ui/src/components/ui/button/button.js.map +1 -1
- package/dist/ui/src/components/ui/card/card.js +1 -1
- package/dist/ui/src/components/ui/card/card.js.map +1 -1
- package/dist/ui/src/components/ui/checkbox/checkbox.js +1 -1
- package/dist/ui/src/components/ui/checkbox/checkbox.js.map +1 -1
- package/dist/ui/src/components/ui/combobox/combobox.d.ts +5 -1
- package/dist/ui/src/components/ui/combobox/combobox.js +12 -18
- package/dist/ui/src/components/ui/combobox/combobox.js.map +1 -1
- package/dist/ui/src/components/ui/command/command.js +2 -2
- package/dist/ui/src/components/ui/command/command.js.map +1 -1
- package/dist/ui/src/components/ui/context-menu/context-menu.js +5 -9
- package/dist/ui/src/components/ui/context-menu/context-menu.js.map +1 -1
- package/dist/ui/src/components/ui/dropdown-menu/dropdown-menu.js +5 -9
- package/dist/ui/src/components/ui/dropdown-menu/dropdown-menu.js.map +1 -1
- package/dist/ui/src/components/ui/empty/empty.js +1 -1
- package/dist/ui/src/components/ui/empty/empty.js.map +1 -1
- package/dist/ui/src/components/ui/field/field.js +2 -2
- package/dist/ui/src/components/ui/field/field.js.map +1 -1
- package/dist/ui/src/components/ui/hover-card/hover-card.js +2 -1
- package/dist/ui/src/components/ui/hover-card/hover-card.js.map +1 -1
- package/dist/ui/src/components/ui/input/input.js +1 -1
- package/dist/ui/src/components/ui/input/input.js.map +1 -1
- package/dist/ui/src/components/ui/input-group/input-group.js +1 -1
- package/dist/ui/src/components/ui/input-group/input-group.js.map +1 -1
- package/dist/ui/src/components/ui/input-otp/input-otp.js +1 -1
- package/dist/ui/src/components/ui/input-otp/input-otp.js.map +1 -1
- package/dist/ui/src/components/ui/label/label.js +1 -1
- package/dist/ui/src/components/ui/label/label.js.map +1 -1
- package/dist/ui/src/components/ui/menubar/menubar.js +7 -4
- package/dist/ui/src/components/ui/menubar/menubar.js.map +1 -1
- package/dist/ui/src/components/ui/native-select/native-select.js +1 -1
- package/dist/ui/src/components/ui/native-select/native-select.js.map +1 -1
- package/dist/ui/src/components/ui/navigation-menu/navigation-menu.js +2 -1
- package/dist/ui/src/components/ui/navigation-menu/navigation-menu.js.map +1 -1
- package/dist/ui/src/components/ui/popover/popover.js +2 -1
- package/dist/ui/src/components/ui/popover/popover.js.map +1 -1
- package/dist/ui/src/components/ui/questionnaire/questionnaire.js +4 -4
- package/dist/ui/src/components/ui/questionnaire/questionnaire.js.map +1 -1
- package/dist/ui/src/components/ui/radio-group/radio-group.js +1 -1
- package/dist/ui/src/components/ui/radio-group/radio-group.js.map +1 -1
- package/dist/ui/src/components/ui/scroll-area/index.d.ts +1 -0
- package/dist/ui/src/components/ui/scroll-area/popup-list-scroll.d.ts +13 -0
- package/dist/ui/src/components/ui/scroll-area/popup-list-scroll.js +69 -0
- package/dist/ui/src/components/ui/scroll-area/popup-list-scroll.js.map +1 -0
- package/dist/ui/src/components/ui/scroll-area/scroll-area.d.ts +2 -4
- package/dist/ui/src/components/ui/scroll-area/scroll-area.js +27 -4
- package/dist/ui/src/components/ui/scroll-area/scroll-area.js.map +1 -1
- package/dist/ui/src/components/ui/select/select.js +9 -14
- package/dist/ui/src/components/ui/select/select.js.map +1 -1
- package/dist/ui/src/components/ui/tabs/tabs.js +1 -1
- package/dist/ui/src/components/ui/tabs/tabs.js.map +1 -1
- package/dist/ui/src/components/ui/textarea/textarea.js +1 -1
- package/dist/ui/src/components/ui/textarea/textarea.js.map +1 -1
- package/dist/ui/src/components/ui/toggle/toggle.js +2 -2
- package/dist/ui/src/components/ui/toggle/toggle.js.map +1 -1
- package/dist/ui/src/components/ui/tooltip/tooltip.js +2 -1
- package/dist/ui/src/components/ui/tooltip/tooltip.js.map +1 -1
- package/dist/ui/src/index.d.ts +1 -1
- package/dist/ui/src/lib/popup-list.d.ts +7 -10
- package/dist/ui/src/lib/popup-list.js +8 -11
- package/dist/ui/src/lib/popup-list.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +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 { useExposedFocusGuards } from '@edyn/ui/lib/focus-guards';\nimport { POPUP_FRAME_CLASS, POPUP_LIST_HEIGHT_CLASS, POPUP_LIST_WIDTH_CLASS, 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\n/** The number of options up to which a MultiSelect does not show its search field (see `searchFrom`). */\nexport const SEARCH_FROM = 8;\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 /**\n * How many options a list must have before it shows its search field: it is there for a list too long to scan, and over a short\n * one it is a box in the way. Above this many (default 8, the rows a popup shows at once) the field shows; at or under, it\n * does not. In async mode it always shows, because the field is how the caller is told what to fetch. `0` always shows it.\n *\n * Hidden, the field is still in the popup, out of sight and not typable, because it is what Base UI moves the highlight\n * from: the arrow keys, Home, End and Enter work as ever, and a screen reader still finds it by its label.\n */\n searchFrom?: number;\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 = cn(\n 'flex max-h-(--available-height) origin-(--transform-origin) flex-col overflow-hidden duration-100',\n POPUP_FRAME_CLASS,\n POPUP_LIST_WIDTH_CLASS,\n '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);\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 {/* Muted grey is 4.2:1 on the highlighted row (6% of the foreground), under the 4.5:1 small text needs, so the count turns to the foreground with its row, as a CommandShortcut does. */}\n {option.count !== undefined && (\n <span className=\"text-xs text-muted-foreground tabular-nums group-data-highlighted:text-foreground\">{option.count}</span>\n )}\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/** Rendered inside the portal, so it exists only while the list is open: the focus guards around the list are not aria-hidden. */\nconst ExposedFocusGuards = () => {\n useExposedFocusGuards();\n return null;\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 searchFrom = SEARCH_FROM,\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 // Over a short list the search is a box in the way; the list still counts what was offered, so a chosen option the page lacks\n // does not hide the field.\n const searchable = isAsync || items.length > searchFrom;\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 <ExposedFocusGuards />\n <Combobox.Positioner sideOffset={4} align=\"start\" className=\"z-50\">\n <Combobox.Popup data-slot=\"multi-select-content\" className={POPUP}>\n <div className={searchable ? 'flex shrink-0 items-center gap-2 border-b border-border px-3' : 'sr-only'}>\n <IconSearch aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n <Combobox.Input\n ref={refSearch}\n readOnly={!searchable}\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={POPUP_LIST_HEIGHT_CLASS}>\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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"}
|
|
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 { useExposedFocusGuards } from '@edyn/ui/lib/focus-guards';\nimport { POPUP_FRAME_CLASS, POPUP_LAYER_CLASS, POPUP_LIST_HEIGHT_CLASS, POPUP_LIST_WIDTH_CLASS, 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\n/** The number of options up to which a MultiSelect does not show its search field (see `searchFrom`). */\nexport const SEARCH_FROM = 8;\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 /**\n * How many options a list must have before it shows its search field: it is there for a list too long to scan, and over a short\n * one it is a box in the way. Above this many (default 8, the rows a popup shows at once) the field shows; at or under, it\n * does not. In async mode it always shows, because the field is how the caller is told what to fetch. `0` always shows it.\n *\n * Hidden, the field is still in the popup, out of sight and not typable, because it is what Base UI moves the highlight\n * from: the arrow keys, Home, End and Enter work as ever, and a screen reader still finds it by its label.\n */\n searchFrom?: number;\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 = cn(\n 'flex max-h-(--available-height) origin-(--transform-origin) flex-col overflow-hidden duration-100',\n POPUP_FRAME_CLASS,\n POPUP_LIST_WIDTH_CLASS,\n '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);\n\nconst TRIGGER =\n 'flex h-8 w-full min-w-0 cursor-pointer items-center justify-between gap-2 rounded-lg border border-control 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-control 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 {/* Muted grey is 4.2:1 on the highlighted row (6% of the foreground), under the 4.5:1 small text needs, so the count turns to the foreground with its row, as a CommandShortcut does. */}\n {option.count !== undefined && (\n <span className=\"text-xs text-muted-foreground tabular-nums group-data-highlighted:text-foreground\">{option.count}</span>\n )}\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=\"px-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 mx-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/** Rendered inside the portal, so it exists only while the list is open: the focus guards around the list are not aria-hidden. */\nconst ExposedFocusGuards = () => {\n useExposedFocusGuards();\n return null;\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 searchFrom = SEARCH_FROM,\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 // Over a short list the search is a box in the way; the list still counts what was offered, so a chosen option the page lacks\n // does not hide the field.\n const searchable = isAsync || items.length > searchFrom;\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 <ExposedFocusGuards />\n <Combobox.Positioner sideOffset={4} align=\"start\" className={POPUP_LAYER_CLASS}>\n <Combobox.Popup data-slot=\"multi-select-content\" className={POPUP}>\n <div className={searchable ? 'flex shrink-0 items-center gap-2 border-b border-border px-3' : 'sr-only'}>\n <IconSearch aria-hidden=\"true\" className=\"size-4 shrink-0 text-muted-foreground\" />\n <Combobox.Input\n ref={refSearch}\n readOnly={!searchable}\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={POPUP_LIST_HEIGHT_CLASS}>\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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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cn } from "../../../lib/utils.js";
|
|
2
|
+
import { popupItemClass } from "../../../lib/popup-list.js";
|
|
2
3
|
import { Popover, PopoverContent } from "../../ui/popover/popover.js";
|
|
3
4
|
import { ScrollArea } from "../../ui/scroll-area/scroll-area.js";
|
|
4
|
-
import { popupItemClass } from "../../../lib/popup-list.js";
|
|
5
5
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
6
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
import { IconLoader2 } from "@tabler/icons-react";
|
|
@@ -194,7 +194,7 @@ var RichTextEditor = ({ value, defaultValue = "", onChange, placeholder, disable
|
|
|
194
194
|
"data-invalid": isInvalid ? "" : void 0,
|
|
195
195
|
"data-disabled": disabled ? "" : void 0,
|
|
196
196
|
"data-readonly": readOnly ? "" : void 0,
|
|
197
|
-
className: cn("flex w-full min-w-0 flex-col rounded-lg border border-
|
|
197
|
+
className: cn("flex w-full min-w-0 flex-col rounded-lg border border-control bg-transparent text-base transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:bg-input/50 data-disabled:opacity-50 data-invalid:border-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:data-disabled:bg-input/80 dark:data-invalid:ring-destructive/40", className),
|
|
198
198
|
...props,
|
|
199
199
|
children: [
|
|
200
200
|
showToolbar ? /* @__PURE__ */ jsx(RichTextToolbar, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rich-text-editor.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/rich-text-editor/rich-text-editor.tsx"],"sourcesContent":["import { RICH_TEXT_BODY_CLASS } from '@edyn/ui/components/edyn/rich-text-content/rich-text-content';\nimport { ScrollArea } from '@edyn/ui/components/ui/scroll-area';\nimport { cn } from '@edyn/ui/lib/utils';\nimport type { Editor } from '@tiptap/core';\nimport { EditorContent, useEditor, useEditorState } from '@tiptap/react';\nimport type { SuggestionProps } from '@tiptap/suggestion';\nimport {\n type ComponentPropsWithoutRef,\n type Ref,\n useCallback,\n useEffect,\n useId,\n useImperativeHandle,\n useMemo,\n useReducer,\n useRef,\n useState,\n} from 'react';\nimport { buildExtensions, type IEditorHost } from './extensions';\nimport { MentionList } from './mention-list';\nimport { defaultMentionToText, isDocumentEmpty, serializeDocument } from './serialize';\nimport { isLive, syncEditable, syncValue } from './sync';\nimport { type ILinkRange, RichTextToolbar } from './toolbar';\nimport {\n DEFAULT_RICH_TEXT_LABELS,\n type IRichTextEditorHandle,\n type IRichTextField,\n type IRichTextLabels,\n type IRichTextMention,\n type IRichTextMentionItem,\n} from './types';\n\nconst LINE = 1.5;\n\ninterface ISuggestionState {\n query: string;\n rect: (() => DOMRect | null) | null;\n command: (item: IRichTextMentionItem) => void;\n}\n\nexport interface IRichTextEditorProps extends Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'defaultValue' | 'onChange' | 'id'> {\n /** The HTML, when the parent owns it. Pass back what `onChange` gave. An empty editor is `\"\"`. */\n value?: string;\n /** The HTML to start from when the editor owns its own value. */\n defaultValue?: string;\n /** Called with the HTML after every edit. Only the elements in `RICH_TEXT_ELEMENTS` can appear, and `\"\"` when there is no text. */\n onChange?: (html: string) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Shows the content and keeps it selectable and copyable, with no toolbar. */\n readOnly?: boolean;\n /** Draws the error border and sets `aria-invalid`. The message is the form's to draw: point `aria-describedby` at it. */\n invalid?: boolean;\n /**\n * Lines the box is tall when empty.\n * @default 6\n */\n minRows?: number;\n /**\n * Lines it grows to before it scrolls inside.\n * @default 20\n */\n maxRows?: number;\n /** The most characters of text (a mention counts as the text it is written as). An edit past it is refused. Off by default. */\n maxLength?: number;\n /** Turns @ into a search the app answers. Without it, @ is an ordinary character. */\n mention?: IRichTextMention;\n /** Adds an \"Insert a field\" menu that writes `{{id}}` at the caret. */\n fields?: IRichTextField[];\n /**\n * Whether the formatting toolbar shows.\n * @default true\n */\n toolbar?: boolean;\n /** Replaces any of the strings the editor shows (the defaults are English). */\n labels?: Partial<IRichTextLabels>;\n /** On the editable element, so a form can name it. */\n id?: string;\n autoFocus?: boolean;\n /** Called with the files when the writer pastes only files (a screenshot). Without it, such a paste does nothing. */\n onPasteFiles?: (files: File[]) => void;\n /** Gives `insertText` and `focus`, for buttons outside the editor. */\n ref?: Ref<IRichTextEditorHandle>;\n}\n\n/**\n * A small rich-text field: paragraphs, bold, italic, underline, bulleted and numbered lists and links, with optional\n * @mentions the app answers asynchronously. The value is HTML in and out, and only the elements in `RICH_TEXT_ELEMENTS`\n * can come out (no class, no style), whatever is pasted. A mention is a chip while editing and plain text in the HTML.\n * The server should still sanitise: this is a convenience for the writer, not a security boundary.\n */\nexport const RichTextEditor = ({\n value,\n defaultValue = '',\n onChange,\n placeholder,\n disabled = false,\n readOnly = false,\n invalid = false,\n minRows = 6,\n maxRows = 20,\n maxLength,\n mention,\n fields,\n toolbar = true,\n labels,\n id,\n autoFocus = false,\n onPasteFiles,\n ref,\n className,\n 'aria-invalid': ariaInvalid,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n 'aria-describedby': ariaDescribedBy,\n ...props\n}: IRichTextEditorProps) => {\n const isInvalid = invalid || ariaInvalid === true || ariaInvalid === 'true';\n const text = useMemo(() => ({ ...DEFAULT_RICH_TEXT_LABELS, ...labels }), [labels]);\n const listId = useId();\n const [suggestion, setSuggestion] = useState<ISuggestionState | null>(null);\n const [activeOption, setActiveOption] = useState<string | undefined>(undefined);\n const [linkOpen, setLinkOpen] = useState(false);\n const refLinkRange = useRef<ILinkRange | null>(null);\n const refKeys = useRef<((event: KeyboardEvent) => boolean) | null>(null);\n // What the editor and the parent agree on. It starts as the value the editor is built from, so a controlled value\n // that has not changed asks for nothing at mount.\n const refKnown = useRef<string | undefined>(value ?? defaultValue);\n const refPending = useRef<Array<(live: Editor) => void>>([]);\n const [, wake] = useReducer((count: number) => count + 1, 0);\n const editable = !disabled && !readOnly;\n const showToolbar = toolbar && !readOnly;\n\n const latest = useRef({ mention, maxLength, onChange, onPasteFiles, toolbar: showToolbar });\n latest.current = { mention, maxLength, onChange, onPasteFiles, toolbar: showToolbar };\n\n const host = useMemo<IEditorHost>(\n () => ({\n mentionEnabled: () => latest.current.mention !== undefined,\n maxLength: () => latest.current.maxLength,\n toText: () => latest.current.mention?.toText ?? defaultMentionToText,\n onSuggestion: (props: SuggestionProps<IRichTextMentionItem, IRichTextMentionItem> | null) => {\n if (props === null) {\n setActiveOption(undefined);\n }\n setSuggestion(props === null ? null : { query: props.query, rect: props.clientRect ?? null, command: props.command });\n },\n onSuggestionKey: ({ event }) => refKeys.current?.(event) ?? false,\n }),\n [],\n );\n const extensions = useMemo(() => buildExtensions(host), [host]);\n\n const emit = useCallback(\n (current: Editor): string => {\n const html = isDocumentEmpty(current.state.doc) ? '' : serializeDocument(current.state.doc, host.toText());\n refKnown.current = html;\n return html;\n },\n [host],\n );\n\n const attributes = useMemo(() => {\n const entries: Record<string, string | undefined> = {\n role: 'textbox',\n 'aria-multiline': 'true',\n 'aria-autocomplete': mention ? 'list' : undefined,\n 'aria-controls': suggestion ? listId : undefined,\n 'aria-activedescendant': suggestion ? activeOption : undefined,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n 'aria-describedby': ariaDescribedBy,\n 'aria-invalid': isInvalid ? 'true' : undefined,\n 'aria-readonly': readOnly ? 'true' : undefined,\n 'aria-disabled': disabled ? 'true' : undefined,\n 'aria-placeholder': placeholder,\n tabindex: readOnly && !disabled ? '0' : undefined,\n id,\n 'data-slot': 'rich-text-editor-content',\n class: cn(RICH_TEXT_BODY_CLASS, 'min-h-(--rich-text-min) px-2.5 py-2 outline-none'),\n };\n return Object.fromEntries(Object.entries(entries).filter((entry): entry is [string, string] => entry[1] !== undefined));\n }, [activeOption, ariaDescribedBy, ariaLabel, ariaLabelledBy, disabled, id, isInvalid, listId, mention, placeholder, readOnly, suggestion]);\n\n const editor = useEditor({\n extensions,\n content: value ?? defaultValue,\n editable,\n autofocus: autoFocus ? 'end' : false,\n editorProps: {\n attributes,\n handlePaste: (_view, event) => {\n const data = event.clipboardData;\n const onlyFiles = data !== null && data.files.length > 0 && data.getData('text/plain') === '' && data.getData('text/html') === '';\n if (!onlyFiles || !latest.current.onPasteFiles) {\n return false;\n }\n latest.current.onPasteFiles(Array.from(data.files));\n return true;\n },\n handleKeyDown: (view, event) => {\n if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 'k' && latest.current.toolbar) {\n event.preventDefault();\n refLinkRange.current = { from: view.state.selection.from, to: view.state.selection.to };\n setLinkOpen(true);\n return true;\n }\n return false;\n },\n },\n onBlur: () => {\n setSuggestion(null);\n setActiveOption(undefined);\n },\n onUpdate: ({ editor: current }) => {\n latest.current.onChange?.(emit(current));\n },\n });\n\n // useEditor can replace the editor (it destroys one that was not mounted in time, and in development a second render\n // makes a second), so readiness is read off the editor in hand: wake up when this one finishes initialising.\n const ready = isLive(editor);\n useEffect(() => {\n editor.on('create', wake);\n wake();\n return () => {\n editor.off('create', wake);\n };\n }, [editor]);\n\n const whenLive = useCallback(\n (action: (live: Editor) => void) => {\n if (isLive(editor)) {\n action(editor);\n } else {\n refPending.current.push(action);\n }\n },\n [editor],\n );\n\n useImperativeHandle(\n ref,\n () => ({\n insertText: (text) => {\n whenLive((live) => {\n live.chain().focus().insertContent({ type: 'text', text }).run();\n });\n },\n focus: () => {\n whenLive((live) => {\n live.commands.focus();\n });\n },\n }),\n [whenLive],\n );\n\n useEffect(() => {\n if (!ready || !isLive(editor)) {\n return;\n }\n for (const action of refPending.current.splice(0)) {\n action(editor);\n }\n }, [editor, ready]);\n\n useEffect(() => {\n if (ready) {\n syncEditable(editor, editable);\n }\n }, [editable, editor, ready]);\n\n useEffect(() => {\n if (ready) {\n syncValue(editor, value, refKnown);\n }\n }, [editor, ready, value]);\n\n const empty = useEditorState({ editor, selector: ({ editor: current }) => isDocumentEmpty(current.state.doc) });\n\n const pick = useCallback(\n (item: IRichTextMentionItem) => {\n suggestion?.command(item);\n },\n [suggestion],\n );\n\n return (\n <div\n data-slot=\"rich-text-editor\"\n data-invalid={isInvalid ? '' : undefined}\n data-disabled={disabled ? '' : undefined}\n data-readonly={readOnly ? '' : undefined}\n className={cn(\n 'flex w-full min-w-0 flex-col rounded-lg border border-input bg-transparent text-base transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:bg-input/50 data-disabled:opacity-50 data-invalid:border-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:data-disabled:bg-input/80 dark:data-invalid:ring-destructive/40',\n className,\n )}\n {...props}\n >\n {showToolbar ? (\n <RichTextToolbar\n editor={editor}\n labels={text}\n fields={fields}\n disabled={disabled}\n linkOpen={linkOpen}\n linkRange={refLinkRange}\n onLinkOpenChange={(next) => {\n if (next && !linkOpen) {\n refLinkRange.current = { from: editor.state.selection.from, to: editor.state.selection.to };\n }\n setLinkOpen(next);\n }}\n />\n ) : null}\n <ScrollArea\n className=\"relative\"\n style={{ maxHeight: `${maxRows * LINE + 1}rem`, ['--rich-text-min' as string]: `${minRows * LINE + 1}rem` }}\n focusable={false}\n >\n {empty && placeholder ? (\n <p aria-hidden className=\"pointer-events-none absolute start-2.5 top-2 text-muted-foreground\">\n {placeholder}\n </p>\n ) : null}\n <EditorContent editor={editor} />\n </ScrollArea>\n {suggestion && mention ? (\n <MentionList\n query={suggestion.query}\n rect={suggestion.rect}\n config={mention}\n labels={text}\n listId={listId}\n onPick={pick}\n keyHandler={refKeys}\n onActiveChange={setActiveOption}\n />\n ) : null}\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;AAgCA,IAAM,OAAO;;;;;;;AA2Db,IAAa,kBAAkB,EAC7B,OACA,eAAe,IACf,UACA,aACA,WAAW,OACX,WAAW,OACX,UAAU,OACV,UAAU,GACV,UAAU,IACV,WACA,SACA,QACA,UAAU,MACV,QACA,IACA,YAAY,OACZ,cACA,KACA,WACA,gBAAgB,aAChB,cAAc,WACd,mBAAmB,gBACnB,oBAAoB,iBACpB,GAAG,YACuB;CAC1B,MAAM,YAAY,WAAW,gBAAgB,QAAQ,gBAAgB;CACrE,MAAM,OAAO,eAAe;EAAE,GAAG;EAA0B,GAAG;CAAO,IAAI,CAAC,MAAM,CAAC;CACjF,MAAM,SAAS,MAAM;CACrB,MAAM,CAAC,YAAY,iBAAiB,SAAkC,IAAI;CAC1E,MAAM,CAAC,cAAc,mBAAmB,SAA6B,KAAA,CAAS;CAC9E,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAC9C,MAAM,eAAe,OAA0B,IAAI;CACnD,MAAM,UAAU,OAAmD,IAAI;CAGvE,MAAM,WAAW,OAA2B,SAAS,YAAY;CACjE,MAAM,aAAa,OAAsC,CAAC,CAAC;CAC3D,MAAM,GAAG,QAAQ,YAAY,UAAkB,QAAQ,GAAG,CAAC;CAC3D,MAAM,WAAW,CAAC,YAAY,CAAC;CAC/B,MAAM,cAAc,WAAW,CAAC;CAEhC,MAAM,SAAS,OAAO;EAAE;EAAS;EAAW;EAAU;EAAc,SAAS;CAAY,CAAC;CAC1F,OAAO,UAAU;EAAE;EAAS;EAAW;EAAU;EAAc,SAAS;CAAY;CAEpF,MAAM,OAAO,eACJ;EACL,sBAAsB,OAAO,QAAQ,YAAY,KAAA;EACjD,iBAAiB,OAAO,QAAQ;EAChC,cAAc,OAAO,QAAQ,SAAS,UAAU;EAChD,eAAe,UAA8E;GAC3F,IAAI,UAAU,MACZ,gBAAgB,KAAA,CAAS;GAE3B,cAAc,UAAU,OAAO,OAAO;IAAE,OAAO,MAAM;IAAO,MAAM,MAAM,cAAc;IAAM,SAAS,MAAM;GAAQ,CAAC;EACtH;EACA,kBAAkB,EAAE,YAAY,QAAQ,UAAU,KAAK,KAAK;CAC9D,IACA,CAAC,CACH;CACA,MAAM,aAAa,cAAc,gBAAgB,IAAI,GAAG,CAAC,IAAI,CAAC;CAE9D,MAAM,OAAO,aACV,YAA4B;EAC3B,MAAM,OAAO,gBAAgB,QAAQ,MAAM,GAAG,IAAI,KAAK,kBAAkB,QAAQ,MAAM,KAAK,KAAK,OAAO,CAAC;EACzG,SAAS,UAAU;EACnB,OAAO;CACT,GACA,CAAC,IAAI,CACP;CAEA,MAAM,aAAa,cAAc;EAC/B,MAAM,UAA8C;GAClD,MAAM;GACN,kBAAkB;GAClB,qBAAqB,UAAU,SAAS,KAAA;GACxC,iBAAiB,aAAa,SAAS,KAAA;GACvC,yBAAyB,aAAa,eAAe,KAAA;GACrD,cAAc;GACd,mBAAmB;GACnB,oBAAoB;GACpB,gBAAgB,YAAY,SAAS,KAAA;GACrC,iBAAiB,WAAW,SAAS,KAAA;GACrC,iBAAiB,WAAW,SAAS,KAAA;GACrC,oBAAoB;GACpB,UAAU,YAAY,CAAC,WAAW,MAAM,KAAA;GACxC;GACA,aAAa;GACb,OAAO,GAAG,sBAAsB,kDAAkD;EACpF;EACA,OAAO,OAAO,YAAY,OAAO,QAAQ,OAAO,CAAC,CAAC,QAAQ,UAAqC,MAAM,OAAO,KAAA,CAAS,CAAC;CACxH,GAAG;EAAC;EAAc;EAAiB;EAAW;EAAgB;EAAU;EAAI;EAAW;EAAQ;EAAS;EAAa;EAAU;CAAU,CAAC;CAE1I,MAAM,SAAS,UAAU;EACvB;EACA,SAAS,SAAS;EAClB;EACA,WAAW,YAAY,QAAQ;EAC/B,aAAa;GACX;GACA,cAAc,OAAO,UAAU;IAC7B,MAAM,OAAO,MAAM;IAEnB,IAAI,EADc,SAAS,QAAQ,KAAK,MAAM,SAAS,KAAK,KAAK,QAAQ,YAAY,MAAM,MAAM,KAAK,QAAQ,WAAW,MAAM,OAC7G,CAAC,OAAO,QAAQ,cAChC,OAAO;IAET,OAAO,QAAQ,aAAa,MAAM,KAAK,KAAK,KAAK,CAAC;IAClD,OAAO;GACT;GACA,gBAAgB,MAAM,UAAU;IAC9B,KAAK,MAAM,WAAW,MAAM,YAAY,CAAC,MAAM,UAAU,CAAC,MAAM,YAAY,MAAM,IAAI,YAAY,MAAM,OAAO,OAAO,QAAQ,SAAS;KACrI,MAAM,eAAe;KACrB,aAAa,UAAU;MAAE,MAAM,KAAK,MAAM,UAAU;MAAM,IAAI,KAAK,MAAM,UAAU;KAAG;KACtF,YAAY,IAAI;KAChB,OAAO;IACT;IACA,OAAO;GACT;EACF;EACA,cAAc;GACZ,cAAc,IAAI;GAClB,gBAAgB,KAAA,CAAS;EAC3B;EACA,WAAW,EAAE,QAAQ,cAAc;GACjC,OAAO,QAAQ,WAAW,KAAK,OAAO,CAAC;EACzC;CACF,CAAC;CAID,MAAM,QAAQ,OAAO,MAAM;CAC3B,gBAAgB;EACd,OAAO,GAAG,UAAU,IAAI;EACxB,KAAK;EACL,aAAa;GACX,OAAO,IAAI,UAAU,IAAI;EAC3B;CACF,GAAG,CAAC,MAAM,CAAC;CAEX,MAAM,WAAW,aACd,WAAmC;EAClC,IAAI,OAAO,MAAM,GACf,OAAO,MAAM;OAEb,WAAW,QAAQ,KAAK,MAAM;CAElC,GACA,CAAC,MAAM,CACT;CAEA,oBACE,YACO;EACL,aAAa,SAAS;GACpB,UAAU,SAAS;IACjB,KAAK,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,cAAc;KAAE,MAAM;KAAQ;IAAK,CAAC,CAAC,CAAC,IAAI;GACjE,CAAC;EACH;EACA,aAAa;GACX,UAAU,SAAS;IACjB,KAAK,SAAS,MAAM;GACtB,CAAC;EACH;CACF,IACA,CAAC,QAAQ,CACX;CAEA,gBAAgB;EACd,IAAI,CAAC,SAAS,CAAC,OAAO,MAAM,GAC1B;EAEF,KAAK,MAAM,UAAU,WAAW,QAAQ,OAAO,CAAC,GAC9C,OAAO,MAAM;CAEjB,GAAG,CAAC,QAAQ,KAAK,CAAC;CAElB,gBAAgB;EACd,IAAI,OACF,aAAa,QAAQ,QAAQ;CAEjC,GAAG;EAAC;EAAU;EAAQ;CAAK,CAAC;CAE5B,gBAAgB;EACd,IAAI,OACF,UAAU,QAAQ,OAAO,QAAQ;CAErC,GAAG;EAAC;EAAQ;EAAO;CAAK,CAAC;CAEzB,MAAM,QAAQ,eAAe;EAAE;EAAQ,WAAW,EAAE,QAAQ,cAAc,gBAAgB,QAAQ,MAAM,GAAG;CAAE,CAAC;CAE9G,MAAM,OAAO,aACV,SAA+B;EAC9B,YAAY,QAAQ,IAAI;CAC1B,GACA,CAAC,UAAU,CACb;CAEA,OACE,qBAAC,OAAD;EACE,aAAU;EACV,gBAAc,YAAY,KAAK,KAAA;EAC/B,iBAAe,WAAW,KAAK,KAAA;EAC/B,iBAAe,WAAW,KAAK,KAAA;EAC/B,WAAW,GACT,6dACA,SACF;EACA,GAAI;EATN,UAAA;GAWG,cACC,oBAAC,iBAAD;IACU;IACR,QAAQ;IACA;IACE;IACA;IACV,WAAW;IACX,mBAAmB,SAAS;KAC1B,IAAI,QAAQ,CAAC,UACX,aAAa,UAAU;MAAE,MAAM,OAAO,MAAM,UAAU;MAAM,IAAI,OAAO,MAAM,UAAU;KAAG;KAE5F,YAAY,IAAI;IAClB;GACD,CAAA,IACC;GACJ,qBAAC,YAAD;IACE,WAAU;IACV,OAAO;KAAE,WAAW,GAAG,UAAU,OAAO,EAAE;MAAO,oBAA8B,GAAG,UAAU,OAAO,EAAE;IAAK;IAC1G,WAAW;IAHb,UAAA,CAKG,SAAS,cACR,oBAAC,KAAD;KAAG,eAAA;KAAY,WAAU;KACtB,UAAA;IACA,CAAA,IACD,MACJ,oBAAC,eAAD,EAAuB,OAAS,CAAA,CACtB;;GACX,cAAc,UACb,oBAAC,aAAD;IACE,OAAO,WAAW;IAClB,MAAM,WAAW;IACjB,QAAQ;IACR,QAAQ;IACA;IACR,QAAQ;IACR,YAAY;IACZ,gBAAgB;GACjB,CAAA,IACC;EACD;;AAET"}
|
|
1
|
+
{"version":3,"file":"rich-text-editor.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/rich-text-editor/rich-text-editor.tsx"],"sourcesContent":["import { RICH_TEXT_BODY_CLASS } from '@edyn/ui/components/edyn/rich-text-content/rich-text-content';\nimport { ScrollArea } from '@edyn/ui/components/ui/scroll-area';\nimport { cn } from '@edyn/ui/lib/utils';\nimport type { Editor } from '@tiptap/core';\nimport { EditorContent, useEditor, useEditorState } from '@tiptap/react';\nimport type { SuggestionProps } from '@tiptap/suggestion';\nimport {\n type ComponentPropsWithoutRef,\n type Ref,\n useCallback,\n useEffect,\n useId,\n useImperativeHandle,\n useMemo,\n useReducer,\n useRef,\n useState,\n} from 'react';\nimport { buildExtensions, type IEditorHost } from './extensions';\nimport { MentionList } from './mention-list';\nimport { defaultMentionToText, isDocumentEmpty, serializeDocument } from './serialize';\nimport { isLive, syncEditable, syncValue } from './sync';\nimport { type ILinkRange, RichTextToolbar } from './toolbar';\nimport {\n DEFAULT_RICH_TEXT_LABELS,\n type IRichTextEditorHandle,\n type IRichTextField,\n type IRichTextLabels,\n type IRichTextMention,\n type IRichTextMentionItem,\n} from './types';\n\nconst LINE = 1.5;\n\ninterface ISuggestionState {\n query: string;\n rect: (() => DOMRect | null) | null;\n command: (item: IRichTextMentionItem) => void;\n}\n\nexport interface IRichTextEditorProps extends Omit<ComponentPropsWithoutRef<'div'>, 'children' | 'defaultValue' | 'onChange' | 'id'> {\n /** The HTML, when the parent owns it. Pass back what `onChange` gave. An empty editor is `\"\"`. */\n value?: string;\n /** The HTML to start from when the editor owns its own value. */\n defaultValue?: string;\n /** Called with the HTML after every edit. Only the elements in `RICH_TEXT_ELEMENTS` can appear, and `\"\"` when there is no text. */\n onChange?: (html: string) => void;\n placeholder?: string;\n disabled?: boolean;\n /** Shows the content and keeps it selectable and copyable, with no toolbar. */\n readOnly?: boolean;\n /** Draws the error border and sets `aria-invalid`. The message is the form's to draw: point `aria-describedby` at it. */\n invalid?: boolean;\n /**\n * Lines the box is tall when empty.\n * @default 6\n */\n minRows?: number;\n /**\n * Lines it grows to before it scrolls inside.\n * @default 20\n */\n maxRows?: number;\n /** The most characters of text (a mention counts as the text it is written as). An edit past it is refused. Off by default. */\n maxLength?: number;\n /** Turns @ into a search the app answers. Without it, @ is an ordinary character. */\n mention?: IRichTextMention;\n /** Adds an \"Insert a field\" menu that writes `{{id}}` at the caret. */\n fields?: IRichTextField[];\n /**\n * Whether the formatting toolbar shows.\n * @default true\n */\n toolbar?: boolean;\n /** Replaces any of the strings the editor shows (the defaults are English). */\n labels?: Partial<IRichTextLabels>;\n /** On the editable element, so a form can name it. */\n id?: string;\n autoFocus?: boolean;\n /** Called with the files when the writer pastes only files (a screenshot). Without it, such a paste does nothing. */\n onPasteFiles?: (files: File[]) => void;\n /** Gives `insertText` and `focus`, for buttons outside the editor. */\n ref?: Ref<IRichTextEditorHandle>;\n}\n\n/**\n * A small rich-text field: paragraphs, bold, italic, underline, bulleted and numbered lists and links, with optional\n * @mentions the app answers asynchronously. The value is HTML in and out, and only the elements in `RICH_TEXT_ELEMENTS`\n * can come out (no class, no style), whatever is pasted. A mention is a chip while editing and plain text in the HTML.\n * The server should still sanitise: this is a convenience for the writer, not a security boundary.\n */\nexport const RichTextEditor = ({\n value,\n defaultValue = '',\n onChange,\n placeholder,\n disabled = false,\n readOnly = false,\n invalid = false,\n minRows = 6,\n maxRows = 20,\n maxLength,\n mention,\n fields,\n toolbar = true,\n labels,\n id,\n autoFocus = false,\n onPasteFiles,\n ref,\n className,\n 'aria-invalid': ariaInvalid,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n 'aria-describedby': ariaDescribedBy,\n ...props\n}: IRichTextEditorProps) => {\n const isInvalid = invalid || ariaInvalid === true || ariaInvalid === 'true';\n const text = useMemo(() => ({ ...DEFAULT_RICH_TEXT_LABELS, ...labels }), [labels]);\n const listId = useId();\n const [suggestion, setSuggestion] = useState<ISuggestionState | null>(null);\n const [activeOption, setActiveOption] = useState<string | undefined>(undefined);\n const [linkOpen, setLinkOpen] = useState(false);\n const refLinkRange = useRef<ILinkRange | null>(null);\n const refKeys = useRef<((event: KeyboardEvent) => boolean) | null>(null);\n // What the editor and the parent agree on. It starts as the value the editor is built from, so a controlled value\n // that has not changed asks for nothing at mount.\n const refKnown = useRef<string | undefined>(value ?? defaultValue);\n const refPending = useRef<Array<(live: Editor) => void>>([]);\n const [, wake] = useReducer((count: number) => count + 1, 0);\n const editable = !disabled && !readOnly;\n const showToolbar = toolbar && !readOnly;\n\n const latest = useRef({ mention, maxLength, onChange, onPasteFiles, toolbar: showToolbar });\n latest.current = { mention, maxLength, onChange, onPasteFiles, toolbar: showToolbar };\n\n const host = useMemo<IEditorHost>(\n () => ({\n mentionEnabled: () => latest.current.mention !== undefined,\n maxLength: () => latest.current.maxLength,\n toText: () => latest.current.mention?.toText ?? defaultMentionToText,\n onSuggestion: (props: SuggestionProps<IRichTextMentionItem, IRichTextMentionItem> | null) => {\n if (props === null) {\n setActiveOption(undefined);\n }\n setSuggestion(props === null ? null : { query: props.query, rect: props.clientRect ?? null, command: props.command });\n },\n onSuggestionKey: ({ event }) => refKeys.current?.(event) ?? false,\n }),\n [],\n );\n const extensions = useMemo(() => buildExtensions(host), [host]);\n\n const emit = useCallback(\n (current: Editor): string => {\n const html = isDocumentEmpty(current.state.doc) ? '' : serializeDocument(current.state.doc, host.toText());\n refKnown.current = html;\n return html;\n },\n [host],\n );\n\n const attributes = useMemo(() => {\n const entries: Record<string, string | undefined> = {\n role: 'textbox',\n 'aria-multiline': 'true',\n 'aria-autocomplete': mention ? 'list' : undefined,\n 'aria-controls': suggestion ? listId : undefined,\n 'aria-activedescendant': suggestion ? activeOption : undefined,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n 'aria-describedby': ariaDescribedBy,\n 'aria-invalid': isInvalid ? 'true' : undefined,\n 'aria-readonly': readOnly ? 'true' : undefined,\n 'aria-disabled': disabled ? 'true' : undefined,\n 'aria-placeholder': placeholder,\n tabindex: readOnly && !disabled ? '0' : undefined,\n id,\n 'data-slot': 'rich-text-editor-content',\n class: cn(RICH_TEXT_BODY_CLASS, 'min-h-(--rich-text-min) px-2.5 py-2 outline-none'),\n };\n return Object.fromEntries(Object.entries(entries).filter((entry): entry is [string, string] => entry[1] !== undefined));\n }, [activeOption, ariaDescribedBy, ariaLabel, ariaLabelledBy, disabled, id, isInvalid, listId, mention, placeholder, readOnly, suggestion]);\n\n const editor = useEditor({\n extensions,\n content: value ?? defaultValue,\n editable,\n autofocus: autoFocus ? 'end' : false,\n editorProps: {\n attributes,\n handlePaste: (_view, event) => {\n const data = event.clipboardData;\n const onlyFiles = data !== null && data.files.length > 0 && data.getData('text/plain') === '' && data.getData('text/html') === '';\n if (!onlyFiles || !latest.current.onPasteFiles) {\n return false;\n }\n latest.current.onPasteFiles(Array.from(data.files));\n return true;\n },\n handleKeyDown: (view, event) => {\n if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 'k' && latest.current.toolbar) {\n event.preventDefault();\n refLinkRange.current = { from: view.state.selection.from, to: view.state.selection.to };\n setLinkOpen(true);\n return true;\n }\n return false;\n },\n },\n onBlur: () => {\n setSuggestion(null);\n setActiveOption(undefined);\n },\n onUpdate: ({ editor: current }) => {\n latest.current.onChange?.(emit(current));\n },\n });\n\n // useEditor can replace the editor (it destroys one that was not mounted in time, and in development a second render\n // makes a second), so readiness is read off the editor in hand: wake up when this one finishes initialising.\n const ready = isLive(editor);\n useEffect(() => {\n editor.on('create', wake);\n wake();\n return () => {\n editor.off('create', wake);\n };\n }, [editor]);\n\n const whenLive = useCallback(\n (action: (live: Editor) => void) => {\n if (isLive(editor)) {\n action(editor);\n } else {\n refPending.current.push(action);\n }\n },\n [editor],\n );\n\n useImperativeHandle(\n ref,\n () => ({\n insertText: (text) => {\n whenLive((live) => {\n live.chain().focus().insertContent({ type: 'text', text }).run();\n });\n },\n focus: () => {\n whenLive((live) => {\n live.commands.focus();\n });\n },\n }),\n [whenLive],\n );\n\n useEffect(() => {\n if (!ready || !isLive(editor)) {\n return;\n }\n for (const action of refPending.current.splice(0)) {\n action(editor);\n }\n }, [editor, ready]);\n\n useEffect(() => {\n if (ready) {\n syncEditable(editor, editable);\n }\n }, [editable, editor, ready]);\n\n useEffect(() => {\n if (ready) {\n syncValue(editor, value, refKnown);\n }\n }, [editor, ready, value]);\n\n const empty = useEditorState({ editor, selector: ({ editor: current }) => isDocumentEmpty(current.state.doc) });\n\n const pick = useCallback(\n (item: IRichTextMentionItem) => {\n suggestion?.command(item);\n },\n [suggestion],\n );\n\n return (\n <div\n data-slot=\"rich-text-editor\"\n data-invalid={isInvalid ? '' : undefined}\n data-disabled={disabled ? '' : undefined}\n data-readonly={readOnly ? '' : undefined}\n className={cn(\n 'flex w-full min-w-0 flex-col rounded-lg border border-control bg-transparent text-base transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:bg-input/50 data-disabled:opacity-50 data-invalid:border-destructive data-invalid:ring-3 data-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:data-disabled:bg-input/80 dark:data-invalid:ring-destructive/40',\n className,\n )}\n {...props}\n >\n {showToolbar ? (\n <RichTextToolbar\n editor={editor}\n labels={text}\n fields={fields}\n disabled={disabled}\n linkOpen={linkOpen}\n linkRange={refLinkRange}\n onLinkOpenChange={(next) => {\n if (next && !linkOpen) {\n refLinkRange.current = { from: editor.state.selection.from, to: editor.state.selection.to };\n }\n setLinkOpen(next);\n }}\n />\n ) : null}\n <ScrollArea\n className=\"relative\"\n style={{ maxHeight: `${maxRows * LINE + 1}rem`, ['--rich-text-min' as string]: `${minRows * LINE + 1}rem` }}\n focusable={false}\n >\n {empty && placeholder ? (\n <p aria-hidden className=\"pointer-events-none absolute start-2.5 top-2 text-muted-foreground\">\n {placeholder}\n </p>\n ) : null}\n <EditorContent editor={editor} />\n </ScrollArea>\n {suggestion && mention ? (\n <MentionList\n query={suggestion.query}\n rect={suggestion.rect}\n config={mention}\n labels={text}\n listId={listId}\n onPick={pick}\n keyHandler={refKeys}\n onActiveChange={setActiveOption}\n />\n ) : null}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -153,7 +153,7 @@ var RichTextToolbar = ({ linkRange, editor, labels, fields, disabled, linkOpen,
|
|
|
153
153
|
return /* @__PURE__ */ jsxs(Toolbar.Root, {
|
|
154
154
|
"data-slot": "rich-text-toolbar",
|
|
155
155
|
"aria-label": labels.toolbar,
|
|
156
|
-
className: "flex flex-wrap items-center gap-0.5 border-b border-
|
|
156
|
+
className: "flex flex-wrap items-center gap-0.5 border-b border-control p-1",
|
|
157
157
|
children: [
|
|
158
158
|
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
159
159
|
label: labels.bold,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolbar.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/rich-text-editor/toolbar.tsx"],"sourcesContent":["import { Toolbar as ToolbarPrimitive } from '@base-ui/react/toolbar';\nimport { isAllowedRichTextHref } from '@edyn/ui/components/edyn/rich-text-content/allow-list';\nimport { Button } from '@edyn/ui/components/ui/button';\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@edyn/ui/components/ui/dropdown-menu';\nimport { Input } from '@edyn/ui/components/ui/input';\nimport { Popover, PopoverContent, PopoverTrigger } from '@edyn/ui/components/ui/popover';\nimport { toggleVariants } from '@edyn/ui/components/ui/toggle';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconBold, IconItalic, IconLink, IconList, IconListNumbers, IconSeparatorHorizontal, IconUnderline, IconVariable } from '@tabler/icons-react';\nimport type { Editor } from '@tiptap/core';\nimport { useEditorState } from '@tiptap/react';\nimport { type FormEvent, type ReactNode, type RefObject, useEffect, useState } from 'react';\nimport type { IRichTextField, IRichTextLabels } from './types';\n\nconst BUTTON_CLASS = cn(toggleVariants({ variant: 'default', size: 'sm' }), 'size-7 px-0');\n\n/** A typed address becomes a link: \"name@example.test\" a mailto, \"example.test\" an https address. */\nexport const normalizeHref = (raw: string): string => {\n const value = raw.trim();\n if (value === '' || /^[a-z][a-z0-9+.-]*:/i.test(value)) {\n return value;\n }\n return /^[^\\s@/]+@[^\\s@/]+\\.[^\\s@/]+$/.test(value) ? `mailto:${value}` : `https://${value}`;\n};\n\ninterface IToolbarButtonProps {\n label: string;\n pressed?: boolean;\n disabled?: boolean;\n onClick?: () => void;\n children: ReactNode;\n}\n\nconst ToolbarButton = ({ label, pressed, disabled, onClick, children }: IToolbarButtonProps) => (\n <ToolbarPrimitive.Button\n type=\"button\"\n aria-label={label}\n title={label}\n aria-pressed={pressed}\n data-pressed={pressed ? '' : undefined}\n disabled={disabled}\n className={BUTTON_CLASS}\n onMouseDown={(event) => event.preventDefault()}\n onClick={onClick}\n >\n {children}\n </ToolbarPrimitive.Button>\n);\n\n/** Where the writer's selection was when the link box opened: moving focus into the box must not lose it. */\nexport interface ILinkRange {\n from: number;\n to: number;\n}\n\nconst hrefAt = (editor: Editor, range: ILinkRange): string => {\n const { doc } = editor.state;\n const marks = range.from < range.to ? (doc.nodeAt(range.from)?.marks ?? []) : doc.resolve(range.from).marks();\n return String(marks.find((mark) => mark.type.name === 'link')?.attrs.href ?? '');\n};\n\ninterface ILinkControlProps {\n range: RefObject<ILinkRange | null>;\n editor: Editor;\n labels: IRichTextLabels;\n active: boolean;\n disabled: boolean;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n}\n\nconst LinkControl = ({ range, editor, labels, active, disabled, open, onOpenChange }: ILinkControlProps) => {\n const [address, setAddress] = useState('');\n const [invalid, setInvalid] = useState(false);\n\n useEffect(() => {\n if (open) {\n setAddress(range.current ? hrefAt(editor, range.current) : '');\n setInvalid(false);\n }\n }, [editor, open, range]);\n\n const apply = (event: FormEvent) => {\n event.preventDefault();\n event.stopPropagation();\n const href = normalizeHref(address);\n if (!isAllowedRichTextHref(href)) {\n setInvalid(true);\n return;\n }\n const saved = range.current ?? { from: editor.state.selection.from, to: editor.state.selection.to };\n const chain = editor.chain().focus().setTextSelection(saved);\n if (saved.from === saved.to && !hrefAt(editor, saved)) {\n chain.insertContent({ type: 'text', text: href, marks: [{ type: 'link', attrs: { href } }] }).run();\n } else {\n chain.extendMarkRange('link').setLink({ href }).run();\n }\n onOpenChange(false);\n };\n\n const remove = () => {\n const saved = range.current ?? { from: editor.state.selection.from, to: editor.state.selection.to };\n editor.chain().focus().setTextSelection(saved).extendMarkRange('link').unsetLink().run();\n onOpenChange(false);\n };\n\n return (\n <Popover open={open} onOpenChange={onOpenChange}>\n <PopoverTrigger\n render={\n <ToolbarPrimitive.Button\n type=\"button\"\n aria-label={labels.link}\n title={labels.link}\n aria-pressed={active}\n data-pressed={active ? '' : undefined}\n disabled={disabled}\n className={BUTTON_CLASS}\n />\n }\n >\n <IconLink aria-hidden />\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-80\">\n <form className=\"flex flex-col gap-2\" onSubmit={apply}>\n <Input\n type=\"text\"\n inputMode=\"url\"\n aria-label={labels.linkAddress}\n aria-invalid={invalid || undefined}\n value={address}\n onChange={(event) => {\n setAddress(event.target.value);\n setInvalid(false);\n }}\n />\n {invalid ? <p className=\"text-xs text-destructive\">{labels.linkInvalid}</p> : null}\n <div className=\"flex justify-end gap-2\">\n {active ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={remove}>\n {labels.linkRemove}\n </Button>\n ) : null}\n <Button type=\"submit\" size=\"sm\">\n {labels.linkApply}\n </Button>\n </div>\n </form>\n </PopoverContent>\n </Popover>\n );\n};\n\nexport interface IRichTextToolbarProps {\n linkRange: RefObject<ILinkRange | null>;\n editor: Editor;\n labels: IRichTextLabels;\n fields: IRichTextField[] | undefined;\n disabled: boolean;\n linkOpen: boolean;\n onLinkOpenChange: (open: boolean) => void;\n}\n\nexport const RichTextToolbar = ({ linkRange, editor, labels, fields, disabled, linkOpen, onLinkOpenChange }: IRichTextToolbarProps) => {\n const state = useEditorState({\n editor,\n selector: ({ editor: current }) => ({\n bold: current.isActive('bold'),\n italic: current.isActive('italic'),\n underline: current.isActive('underline'),\n bullets: current.isActive('bulletList'),\n numbers: current.isActive('orderedList'),\n link: current.isActive('link'),\n }),\n });\n\n return (\n <ToolbarPrimitive.Root\n data-slot=\"rich-text-toolbar\"\n aria-label={labels.toolbar}\n className=\"flex flex-wrap items-center gap-0.5 border-b border-input p-1\"\n >\n <ToolbarButton label={labels.bold} pressed={state.bold} disabled={disabled} onClick={() => editor.chain().focus().toggleBold().run()}>\n <IconBold aria-hidden />\n </ToolbarButton>\n <ToolbarButton label={labels.italic} pressed={state.italic} disabled={disabled} onClick={() => editor.chain().focus().toggleItalic().run()}>\n <IconItalic aria-hidden />\n </ToolbarButton>\n <ToolbarButton\n label={labels.underline}\n pressed={state.underline}\n disabled={disabled}\n onClick={() => editor.chain().focus().toggleUnderline().run()}\n >\n <IconUnderline aria-hidden />\n </ToolbarButton>\n <ToolbarPrimitive.Separator className=\"mx-1 h-4 w-px bg-border\" />\n <ToolbarButton\n label={labels.bulletList}\n pressed={state.bullets}\n disabled={disabled}\n onClick={() => editor.chain().focus().toggleBulletList().run()}\n >\n <IconList aria-hidden />\n </ToolbarButton>\n <ToolbarButton\n label={labels.numberedList}\n pressed={state.numbers}\n disabled={disabled}\n onClick={() => editor.chain().focus().toggleOrderedList().run()}\n >\n <IconListNumbers aria-hidden />\n </ToolbarButton>\n <LinkControl\n range={linkRange}\n editor={editor}\n labels={labels}\n active={state.link}\n disabled={disabled}\n open={linkOpen}\n onOpenChange={onLinkOpenChange}\n />\n <ToolbarButton label={labels.horizontalRule} disabled={disabled} onClick={() => editor.chain().focus().setHorizontalRule().run()}>\n <IconSeparatorHorizontal aria-hidden />\n </ToolbarButton>\n {fields && fields.length > 0 ? (\n <>\n <ToolbarPrimitive.Separator className=\"mx-1 h-4 w-px bg-border\" />\n <DropdownMenu>\n <DropdownMenuTrigger\n render={\n <ToolbarPrimitive.Button\n type=\"button\"\n aria-label={labels.insertField}\n title={labels.insertField}\n disabled={disabled}\n className={BUTTON_CLASS}\n />\n }\n >\n <IconVariable aria-hidden />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-56\">\n {fields.map((field) => (\n <DropdownMenuItem\n key={field.id}\n onClick={() =>\n editor\n .chain()\n .focus()\n .insertContent({ type: 'text', text: `{{${field.id}}}` })\n .run()\n }\n >\n {field.label}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n </>\n ) : null}\n </ToolbarPrimitive.Root>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"toolbar.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/rich-text-editor/toolbar.tsx"],"sourcesContent":["import { Toolbar as ToolbarPrimitive } from '@base-ui/react/toolbar';\nimport { isAllowedRichTextHref } from '@edyn/ui/components/edyn/rich-text-content/allow-list';\nimport { Button } from '@edyn/ui/components/ui/button';\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@edyn/ui/components/ui/dropdown-menu';\nimport { Input } from '@edyn/ui/components/ui/input';\nimport { Popover, PopoverContent, PopoverTrigger } from '@edyn/ui/components/ui/popover';\nimport { toggleVariants } from '@edyn/ui/components/ui/toggle';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconBold, IconItalic, IconLink, IconList, IconListNumbers, IconSeparatorHorizontal, IconUnderline, IconVariable } from '@tabler/icons-react';\nimport type { Editor } from '@tiptap/core';\nimport { useEditorState } from '@tiptap/react';\nimport { type FormEvent, type ReactNode, type RefObject, useEffect, useState } from 'react';\nimport type { IRichTextField, IRichTextLabels } from './types';\n\nconst BUTTON_CLASS = cn(toggleVariants({ variant: 'default', size: 'sm' }), 'size-7 px-0');\n\n/** A typed address becomes a link: \"name@example.test\" a mailto, \"example.test\" an https address. */\nexport const normalizeHref = (raw: string): string => {\n const value = raw.trim();\n if (value === '' || /^[a-z][a-z0-9+.-]*:/i.test(value)) {\n return value;\n }\n return /^[^\\s@/]+@[^\\s@/]+\\.[^\\s@/]+$/.test(value) ? `mailto:${value}` : `https://${value}`;\n};\n\ninterface IToolbarButtonProps {\n label: string;\n pressed?: boolean;\n disabled?: boolean;\n onClick?: () => void;\n children: ReactNode;\n}\n\nconst ToolbarButton = ({ label, pressed, disabled, onClick, children }: IToolbarButtonProps) => (\n <ToolbarPrimitive.Button\n type=\"button\"\n aria-label={label}\n title={label}\n aria-pressed={pressed}\n data-pressed={pressed ? '' : undefined}\n disabled={disabled}\n className={BUTTON_CLASS}\n onMouseDown={(event) => event.preventDefault()}\n onClick={onClick}\n >\n {children}\n </ToolbarPrimitive.Button>\n);\n\n/** Where the writer's selection was when the link box opened: moving focus into the box must not lose it. */\nexport interface ILinkRange {\n from: number;\n to: number;\n}\n\nconst hrefAt = (editor: Editor, range: ILinkRange): string => {\n const { doc } = editor.state;\n const marks = range.from < range.to ? (doc.nodeAt(range.from)?.marks ?? []) : doc.resolve(range.from).marks();\n return String(marks.find((mark) => mark.type.name === 'link')?.attrs.href ?? '');\n};\n\ninterface ILinkControlProps {\n range: RefObject<ILinkRange | null>;\n editor: Editor;\n labels: IRichTextLabels;\n active: boolean;\n disabled: boolean;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n}\n\nconst LinkControl = ({ range, editor, labels, active, disabled, open, onOpenChange }: ILinkControlProps) => {\n const [address, setAddress] = useState('');\n const [invalid, setInvalid] = useState(false);\n\n useEffect(() => {\n if (open) {\n setAddress(range.current ? hrefAt(editor, range.current) : '');\n setInvalid(false);\n }\n }, [editor, open, range]);\n\n const apply = (event: FormEvent) => {\n event.preventDefault();\n event.stopPropagation();\n const href = normalizeHref(address);\n if (!isAllowedRichTextHref(href)) {\n setInvalid(true);\n return;\n }\n const saved = range.current ?? { from: editor.state.selection.from, to: editor.state.selection.to };\n const chain = editor.chain().focus().setTextSelection(saved);\n if (saved.from === saved.to && !hrefAt(editor, saved)) {\n chain.insertContent({ type: 'text', text: href, marks: [{ type: 'link', attrs: { href } }] }).run();\n } else {\n chain.extendMarkRange('link').setLink({ href }).run();\n }\n onOpenChange(false);\n };\n\n const remove = () => {\n const saved = range.current ?? { from: editor.state.selection.from, to: editor.state.selection.to };\n editor.chain().focus().setTextSelection(saved).extendMarkRange('link').unsetLink().run();\n onOpenChange(false);\n };\n\n return (\n <Popover open={open} onOpenChange={onOpenChange}>\n <PopoverTrigger\n render={\n <ToolbarPrimitive.Button\n type=\"button\"\n aria-label={labels.link}\n title={labels.link}\n aria-pressed={active}\n data-pressed={active ? '' : undefined}\n disabled={disabled}\n className={BUTTON_CLASS}\n />\n }\n >\n <IconLink aria-hidden />\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-80\">\n <form className=\"flex flex-col gap-2\" onSubmit={apply}>\n <Input\n type=\"text\"\n inputMode=\"url\"\n aria-label={labels.linkAddress}\n aria-invalid={invalid || undefined}\n value={address}\n onChange={(event) => {\n setAddress(event.target.value);\n setInvalid(false);\n }}\n />\n {invalid ? <p className=\"text-xs text-destructive\">{labels.linkInvalid}</p> : null}\n <div className=\"flex justify-end gap-2\">\n {active ? (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={remove}>\n {labels.linkRemove}\n </Button>\n ) : null}\n <Button type=\"submit\" size=\"sm\">\n {labels.linkApply}\n </Button>\n </div>\n </form>\n </PopoverContent>\n </Popover>\n );\n};\n\nexport interface IRichTextToolbarProps {\n linkRange: RefObject<ILinkRange | null>;\n editor: Editor;\n labels: IRichTextLabels;\n fields: IRichTextField[] | undefined;\n disabled: boolean;\n linkOpen: boolean;\n onLinkOpenChange: (open: boolean) => void;\n}\n\nexport const RichTextToolbar = ({ linkRange, editor, labels, fields, disabled, linkOpen, onLinkOpenChange }: IRichTextToolbarProps) => {\n const state = useEditorState({\n editor,\n selector: ({ editor: current }) => ({\n bold: current.isActive('bold'),\n italic: current.isActive('italic'),\n underline: current.isActive('underline'),\n bullets: current.isActive('bulletList'),\n numbers: current.isActive('orderedList'),\n link: current.isActive('link'),\n }),\n });\n\n return (\n <ToolbarPrimitive.Root\n data-slot=\"rich-text-toolbar\"\n aria-label={labels.toolbar}\n className=\"flex flex-wrap items-center gap-0.5 border-b border-control p-1\"\n >\n <ToolbarButton label={labels.bold} pressed={state.bold} disabled={disabled} onClick={() => editor.chain().focus().toggleBold().run()}>\n <IconBold aria-hidden />\n </ToolbarButton>\n <ToolbarButton label={labels.italic} pressed={state.italic} disabled={disabled} onClick={() => editor.chain().focus().toggleItalic().run()}>\n <IconItalic aria-hidden />\n </ToolbarButton>\n <ToolbarButton\n label={labels.underline}\n pressed={state.underline}\n disabled={disabled}\n onClick={() => editor.chain().focus().toggleUnderline().run()}\n >\n <IconUnderline aria-hidden />\n </ToolbarButton>\n <ToolbarPrimitive.Separator className=\"mx-1 h-4 w-px bg-border\" />\n <ToolbarButton\n label={labels.bulletList}\n pressed={state.bullets}\n disabled={disabled}\n onClick={() => editor.chain().focus().toggleBulletList().run()}\n >\n <IconList aria-hidden />\n </ToolbarButton>\n <ToolbarButton\n label={labels.numberedList}\n pressed={state.numbers}\n disabled={disabled}\n onClick={() => editor.chain().focus().toggleOrderedList().run()}\n >\n <IconListNumbers aria-hidden />\n </ToolbarButton>\n <LinkControl\n range={linkRange}\n editor={editor}\n labels={labels}\n active={state.link}\n disabled={disabled}\n open={linkOpen}\n onOpenChange={onLinkOpenChange}\n />\n <ToolbarButton label={labels.horizontalRule} disabled={disabled} onClick={() => editor.chain().focus().setHorizontalRule().run()}>\n <IconSeparatorHorizontal aria-hidden />\n </ToolbarButton>\n {fields && fields.length > 0 ? (\n <>\n <ToolbarPrimitive.Separator className=\"mx-1 h-4 w-px bg-border\" />\n <DropdownMenu>\n <DropdownMenuTrigger\n render={\n <ToolbarPrimitive.Button\n type=\"button\"\n aria-label={labels.insertField}\n title={labels.insertField}\n disabled={disabled}\n className={BUTTON_CLASS}\n />\n }\n >\n <IconVariable aria-hidden />\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-56\">\n {fields.map((field) => (\n <DropdownMenuItem\n key={field.id}\n onClick={() =>\n editor\n .chain()\n .focus()\n .insertContent({ type: 'text', text: `{{${field.id}}}` })\n .run()\n }\n >\n {field.label}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n </>\n ) : null}\n </ToolbarPrimitive.Root>\n );\n};\n"],"mappings":"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"}
|
|
@@ -17,6 +17,13 @@ var DEFAULT_TIME_FIELD_LABELS = {
|
|
|
17
17
|
period: "AM or PM",
|
|
18
18
|
empty: "Empty"
|
|
19
19
|
};
|
|
20
|
+
/** What an empty segment shows. Letters, as a DatePicker's pattern is, never a dash. */
|
|
21
|
+
var SEGMENT_PLACEHOLDER = {
|
|
22
|
+
hour: "HH",
|
|
23
|
+
minute: "MM",
|
|
24
|
+
second: "SS",
|
|
25
|
+
period: "AM"
|
|
26
|
+
};
|
|
20
27
|
var TIME = /^(\d{2}):(\d{2})(?::(\d{2}))?$/;
|
|
21
28
|
var two = (value) => String(value).padStart(2, "0");
|
|
22
29
|
/** 12 where the reader's locale writes AM and PM, 24 where it does not. */
|
|
@@ -296,7 +303,7 @@ var TimeField = ({ value, defaultValue = "", onValueChange, granularity = "minut
|
|
|
296
303
|
autoCorrect: "off",
|
|
297
304
|
spellCheck: false,
|
|
298
305
|
value: display(segment),
|
|
299
|
-
placeholder:
|
|
306
|
+
placeholder: SEGMENT_PLACEHOLDER[segment],
|
|
300
307
|
disabled,
|
|
301
308
|
readOnly,
|
|
302
309
|
"aria-label": labels[segment],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-field.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/time-field/time-field.tsx"],"sourcesContent":["import { InputGroup, InputGroupAddon } from '@edyn/ui/components/ui/input-group';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconClock } from '@tabler/icons-react';\nimport { type ChangeEvent, type ComponentPropsWithoutRef, type KeyboardEvent, useMemo, useRef, useState } from 'react';\n\nexport type TTimeFieldGranularity = 'minute' | 'second';\n\nexport type TTimeFieldHourCycle = 12 | 24;\n\nexport type TTimeFieldSegment = 'hour' | 'minute' | 'second' | 'period';\n\nexport type TTimeFieldPeriod = 'AM' | 'PM';\n\n/** The segments as they are shown: the hour in the field's own cycle (0 to 23, or 1 to 12 beside a period), each empty until entered. */\nexport interface ITimeParts {\n hour: number | null;\n minute: number | null;\n second: number | null;\n period: TTimeFieldPeriod | null;\n}\n\nexport const EMPTY_TIME: ITimeParts = { hour: null, minute: null, second: null, period: null };\n\nexport interface ITimeFieldLabels {\n hour: string;\n minute: string;\n second: string;\n /** The AM/PM segment. */\n period: string;\n /** Read for a segment with nothing in it. */\n empty: string;\n}\n\nexport const DEFAULT_TIME_FIELD_LABELS: ITimeFieldLabels = {\n hour: 'Hours',\n minute: 'Minutes',\n second: 'Seconds',\n period: 'AM or PM',\n empty: 'Empty',\n};\n\nconst TIME = /^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/;\n\nconst two = (value: number): string => String(value).padStart(2, '0');\n\n/** 12 where the reader's locale writes AM and PM, 24 where it does not. */\nexport const localeHourCycle = (): TTimeFieldHourCycle =>\n new Intl.DateTimeFormat(undefined, { hour: 'numeric' }).resolvedOptions().hour12 ? 12 : 24;\n\n/** `HH:mm` or `HH:mm:ss` (24-hour, as `<input type=\"time\">` writes it) to the segments; anything else is no time. */\nexport const parseTime = (value: string, hourCycle: TTimeFieldHourCycle): ITimeParts => {\n const match = TIME.exec(value);\n if (match === null) {\n return EMPTY_TIME;\n }\n const hour24 = Number(match[1]);\n const minute = Number(match[2]);\n const second = match[3] === undefined ? 0 : Number(match[3]);\n if (hour24 > 23 || minute > 59 || second > 59) {\n return EMPTY_TIME;\n }\n if (hourCycle === 24) {\n return { hour: hour24, minute, second, period: null };\n }\n return { hour: hour24 % 12 === 0 ? 12 : hour24 % 12, minute, second, period: hour24 >= 12 ? 'PM' : 'AM' };\n};\n\n/** The segments to `HH:mm` or `HH:mm:ss`, or '' until every segment the field shows is in. */\nexport const formatTime = (parts: ITimeParts, hourCycle: TTimeFieldHourCycle, granularity: TTimeFieldGranularity): string => {\n const { hour, minute, second, period } = parts;\n if (hour === null || minute === null || (granularity === 'second' && second === null)) {\n return '';\n }\n let hour24 = hour;\n if (hourCycle === 12) {\n // 0 is only ever a first digit still waiting for its second.\n if (period === null || hour === 0) {\n return '';\n }\n hour24 = (hour % 12) + (period === 'PM' ? 12 : 0);\n }\n return granularity === 'second' ? `${two(hour24)}:${two(minute)}:${two(second ?? 0)}` : `${two(hour24)}:${two(minute)}`;\n};\n\nexport interface IRange {\n min: number;\n max: number;\n}\n\nexport const rangeOf = (segment: Exclude<TTimeFieldSegment, 'period'>, hourCycle: TTimeFieldHourCycle): IRange => {\n if (segment === 'hour') {\n return hourCycle === 12 ? { min: 1, max: 12 } : { min: 0, max: 23 };\n }\n return { min: 0, max: 59 };\n};\n\n/** One step up or down inside a range, wrapping at the ends. A segment with nothing in it starts at the end the step is heading away from. */\nexport const stepValue = (current: number | null, delta: number, { min, max }: IRange): number => {\n if (current === null) {\n return delta > 0 ? min : max;\n }\n const size = max - min + 1;\n return ((((current - min + delta) % size) + size) % size) + min;\n};\n\n/**\n * Types a digit into a segment. The second digit joins the first when it makes a number the segment can hold; otherwise it starts\n * over. The segment is done, and focus moves on, at two digits or as soon as no second digit could fit (a 3 in the hours, a 6 in the\n * minutes).\n */\nexport const typeDigit = (buffer: string, digit: string, { min, max }: IRange): { value: number; buffer: string; done: boolean } => {\n let text = buffer + digit;\n if (text.length > 2 || Number(text) > max) {\n text = digit;\n }\n const value = Number(text);\n const done = text.length === 2 || value * 10 > max;\n return { value: done && value < min ? min : value, buffer: done ? '' : text, done };\n};\n\nconst PAGE_STEP: Record<Exclude<TTimeFieldSegment, 'period'>, number> = { hour: 3, minute: 10, second: 10 };\n\nexport interface IEdit {\n parts: ITimeParts;\n /** The digits typed so far into this segment and not yet complete. */\n buffer: string;\n /** Move to the next segment: this one is finished. */\n advance: boolean;\n}\n\nconst withSegment = (parts: ITimeParts, segment: Exclude<TTimeFieldSegment, 'period'>, value: number | null): ITimeParts => ({\n ...parts,\n [segment]: value,\n});\n\n/** What a key does to a segment, or null when the key is not one the segment handles (Tab, a letter in the hours...). */\nexport const editSegment = (\n parts: ITimeParts,\n segment: TTimeFieldSegment,\n key: string,\n hourCycle: TTimeFieldHourCycle,\n buffer: string,\n): IEdit | null => {\n if (segment === 'period') {\n const current = parts.period;\n const set = (period: TTimeFieldPeriod | null): IEdit => ({ parts: { ...parts, period }, buffer: '', advance: false });\n switch (key) {\n case 'a':\n case 'A':\n return { ...set('AM'), advance: true };\n case 'p':\n case 'P':\n return { ...set('PM'), advance: true };\n case 'ArrowUp':\n case 'ArrowDown':\n return set(current === null ? (key === 'ArrowUp' ? 'AM' : 'PM') : current === 'AM' ? 'PM' : 'AM');\n case 'Home':\n return set('AM');\n case 'End':\n return set('PM');\n case 'Backspace':\n case 'Delete':\n return set(null);\n default:\n return null;\n }\n }\n\n const range = rangeOf(segment, hourCycle);\n const current = parts[segment];\n const set = (value: number | null, advance = false): IEdit => ({ parts: withSegment(parts, segment, value), buffer: '', advance });\n\n if (/^[0-9]$/.test(key)) {\n const typed = typeDigit(buffer, key, range);\n return { parts: withSegment(parts, segment, typed.value), buffer: typed.buffer, advance: typed.done };\n }\n switch (key) {\n case 'ArrowUp':\n return set(stepValue(current, 1, range));\n case 'ArrowDown':\n return set(stepValue(current, -1, range));\n case 'PageUp':\n return set(stepValue(current, PAGE_STEP[segment], range));\n case 'PageDown':\n return set(stepValue(current, -PAGE_STEP[segment], range));\n case 'Home':\n return set(range.min);\n case 'End':\n return set(range.max);\n case 'Backspace':\n case 'Delete':\n return set(null);\n default:\n return null;\n }\n};\n\nexport interface ITimeFieldProps extends Omit<ComponentPropsWithoutRef<'div'>, 'defaultValue' | 'onChange' | 'children' | 'role'> {\n /** The time as `HH:mm` or `HH:mm:ss`, 24-hour whatever the field shows, as `<input type=\"time\">` writes it. '' is no time. */\n value?: string;\n defaultValue?: string;\n /** Called with the time as soon as every segment is in, and with '' when one of a full time is cleared. */\n onValueChange?: (value: string) => void;\n /** `second` adds a seconds segment and a `HH:mm:ss` value. */\n granularity?: TTimeFieldGranularity;\n /** 12 shows an AM/PM segment, 24 does not. The default is what the reader's locale writes. */\n hourCycle?: TTimeFieldHourCycle;\n /** The earliest and the latest time, `HH:mm`. A time outside them is marked invalid; it is not refused. */\n min?: string;\n max?: string;\n disabled?: boolean;\n /** Shown, focusable and readable, not changeable. */\n readOnly?: boolean;\n invalid?: boolean;\n /** Submits the time with a form, under this name, as `HH:mm` or `HH:mm:ss`. */\n name?: string;\n labels?: Partial<ITimeFieldLabels>;\n}\n\nconst SEGMENT_CLASS =\n 'h-6 rounded-sm bg-transparent text-center text-sm tabular-nums caret-transparent outline-none selection:bg-transparent placeholder:text-muted-foreground focus:bg-foreground/10 disabled:cursor-not-allowed';\n\n/**\n * A time of day typed in segments, in the kit's field: hours, minutes, optional seconds, and AM/PM where the reader's locale uses it.\n * It exists so an app does not fall back to `<input type=\"time\">`, whose look differs between browsers and from the rest of the kit.\n *\n * Each segment is a spinbutton: type digits (the focus moves on by itself), ArrowUp and ArrowDown step with wrap-around, PageUp and\n * PageDown step by more, Home and End jump to the ends, Backspace empties, ArrowLeft and ArrowRight move between segments, and A or P\n * set the period. The group is named by `aria-label` or `aria-labelledby`; a screen reader reads each segment by its own name and value.\n */\nexport const TimeField = ({\n value,\n defaultValue = '',\n onValueChange,\n granularity = 'minute',\n hourCycle: hourCycleProp,\n min,\n max,\n disabled = false,\n readOnly = false,\n invalid = false,\n name,\n labels: labelOverrides,\n className,\n ...rest\n}: ITimeFieldProps) => {\n const labels = useMemo(() => ({ ...DEFAULT_TIME_FIELD_LABELS, ...labelOverrides }), [labelOverrides]);\n const hourCycle = useMemo(() => hourCycleProp ?? localeHourCycle(), [hourCycleProp]);\n const [parts, setParts] = useState<ITimeParts>(() => parseTime(value ?? defaultValue, hourCycle));\n const [seen, setSeen] = useState(value);\n const refSegments = useRef<Array<HTMLInputElement | null>>([]);\n const refBuffer = useRef<{ segment: TTimeFieldSegment; text: string }>({ segment: 'hour', text: '' });\n const formatted = formatTime(parts, hourCycle, granularity);\n // What the parent was last told, and the segments as the last key left them (state is a render behind inside one event).\n const refReported = useRef(formatted);\n const refParts = useRef(parts);\n refParts.current = parts;\n // A time handed in from outside replaces what is shown, unless it is the very time that is shown: clearing one segment of a full\n // time reports '', and taking that back as a new value would wipe the segments still being edited.\n if (value !== seen) {\n setSeen(value);\n if (value !== undefined && value !== formatted) {\n setParts(parseTime(value, hourCycle));\n refReported.current = value;\n }\n }\n\n const segments: TTimeFieldSegment[] = [\n 'hour',\n 'minute',\n ...(granularity === 'second' ? (['second'] as const) : []),\n ...(hourCycle === 12 ? (['period'] as const) : []),\n ];\n\n const focusSegment = (index: number): void => {\n const target = refSegments.current[index];\n if (target !== null && target !== undefined) {\n target.focus();\n target.select();\n }\n };\n\n const report = (next: ITimeParts): void => {\n const text = formatTime(next, hourCycle, granularity);\n if (text !== refReported.current) {\n refReported.current = text;\n onValueChange?.(text);\n }\n };\n\n const edit = (segment: TTimeFieldSegment, key: string): boolean => {\n const buffer = refBuffer.current.segment === segment ? refBuffer.current.text : '';\n const result = editSegment(refParts.current, segment, key, hourCycle, buffer);\n if (result === null) {\n return false;\n }\n refBuffer.current = { segment, text: result.buffer };\n refParts.current = result.parts;\n setParts(result.parts);\n // A segment still waiting for its second digit is not a time yet worth reporting (a 3 may become 30); it is reported when it is\n // done, or when the focus leaves it.\n if (result.buffer === '') {\n report(result.parts);\n }\n if (result.advance) {\n focusSegment(segments.indexOf(segment) + 1);\n }\n return true;\n };\n\n const onKeyDown = (segment: TTimeFieldSegment, event: KeyboardEvent<HTMLInputElement>): void => {\n if (event.altKey || event.ctrlKey || event.metaKey) {\n return;\n }\n const index = segments.indexOf(segment);\n if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {\n event.preventDefault();\n focusSegment(index + (event.key === 'ArrowRight' ? 1 : -1));\n return;\n }\n if (disabled || readOnly) {\n return;\n }\n if (edit(segment, event.key)) {\n event.preventDefault();\n }\n };\n\n // Leaving a segment ends what was being typed into it: coming back and typing a 2 must not make 12 of an old 1. A 0 left alone in a\n // 12-hour hour is the start of a number that never came, so it is emptied.\n const onBlur = (segment: TTimeFieldSegment): void => {\n const unfinished = refBuffer.current.segment === segment && refBuffer.current.text !== '';\n refBuffer.current = { segment, text: '' };\n let settled = refParts.current;\n if (segment === 'hour' && hourCycle === 12 && settled.hour === 0) {\n settled = { ...settled, hour: null };\n refParts.current = settled;\n setParts(settled);\n }\n if (unfinished) {\n report(settled);\n }\n };\n\n // A phone's keyboard does not send key events a page can use; what it types arrives as a change to the input.\n const onChange = (segment: TTimeFieldSegment, event: ChangeEvent<HTMLInputElement>): void => {\n if (disabled || readOnly) {\n return;\n }\n const typed = event.target.value.slice(-1);\n edit(segment, typed);\n };\n\n const outOfRange =\n formatted !== '' && ((min !== undefined && formatted.slice(0, min.length) < min) || (max !== undefined && formatted.slice(0, max.length) > max));\n const flagged = invalid || outOfRange;\n\n const display = (segment: TTimeFieldSegment): string => {\n if (segment === 'period') {\n return parts.period ?? '';\n }\n const current = parts[segment];\n return current === null ? '' : two(current);\n };\n\n return (\n <InputGroup role=\"group\" className={cn('w-fit', className)} {...rest}>\n <InputGroupAddon>\n <IconClock aria-hidden=\"true\" />\n </InputGroupAddon>\n <div className=\"flex items-center gap-0.5 pr-2\">\n {segments.map((segment, index) => {\n const numeric = segment !== 'period';\n const range = numeric ? rangeOf(segment, hourCycle) : { min: 0, max: 1 };\n const current = numeric ? parts[segment] : parts.period === null ? null : parts.period === 'AM' ? 0 : 1;\n return (\n <span key={segment} className=\"flex items-center gap-0.5\">\n {index > 0 && segment !== 'period' && (\n <span aria-hidden=\"true\" className=\"text-muted-foreground\">\n :\n </span>\n )}\n <input\n ref={(node) => {\n refSegments.current[index] = node;\n }}\n data-slot=\"input-group-control\"\n data-segment={segment}\n role=\"spinbutton\"\n inputMode={numeric ? 'numeric' : 'text'}\n autoComplete=\"off\"\n autoCorrect=\"off\"\n spellCheck={false}\n value={display(segment)}\n placeholder=\"––\"\n disabled={disabled}\n readOnly={readOnly}\n aria-label={labels[segment]}\n aria-readonly={readOnly || undefined}\n aria-invalid={flagged || undefined}\n aria-valuemin={range.min}\n aria-valuemax={range.max}\n aria-valuenow={current ?? undefined}\n aria-valuetext={current === null ? labels.empty : segment === 'period' ? (parts.period ?? labels.empty) : two(current)}\n className={cn(SEGMENT_CLASS, segment === 'period' ? 'w-7' : 'w-5')}\n onKeyDown={(event) => onKeyDown(segment, event)}\n onChange={(event) => onChange(segment, event)}\n onFocus={(event) => event.currentTarget.select()}\n onBlur={() => onBlur(segment)}\n />\n </span>\n );\n })}\n </div>\n {name !== undefined && <input type=\"hidden\" name={name} value={formatted} disabled={disabled} />}\n </InputGroup>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"time-field.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/time-field/time-field.tsx"],"sourcesContent":["import { InputGroup, InputGroupAddon } from '@edyn/ui/components/ui/input-group';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconClock } from '@tabler/icons-react';\nimport { type ChangeEvent, type ComponentPropsWithoutRef, type KeyboardEvent, useMemo, useRef, useState } from 'react';\n\nexport type TTimeFieldGranularity = 'minute' | 'second';\n\nexport type TTimeFieldHourCycle = 12 | 24;\n\nexport type TTimeFieldSegment = 'hour' | 'minute' | 'second' | 'period';\n\nexport type TTimeFieldPeriod = 'AM' | 'PM';\n\n/** The segments as they are shown: the hour in the field's own cycle (0 to 23, or 1 to 12 beside a period), each empty until entered. */\nexport interface ITimeParts {\n hour: number | null;\n minute: number | null;\n second: number | null;\n period: TTimeFieldPeriod | null;\n}\n\nexport const EMPTY_TIME: ITimeParts = { hour: null, minute: null, second: null, period: null };\n\nexport interface ITimeFieldLabels {\n hour: string;\n minute: string;\n second: string;\n /** The AM/PM segment. */\n period: string;\n /** Read for a segment with nothing in it. */\n empty: string;\n}\n\nexport const DEFAULT_TIME_FIELD_LABELS: ITimeFieldLabels = {\n hour: 'Hours',\n minute: 'Minutes',\n second: 'Seconds',\n period: 'AM or PM',\n empty: 'Empty',\n};\n\n/** What an empty segment shows. Letters, as a DatePicker's pattern is, never a dash. */\nconst SEGMENT_PLACEHOLDER: Record<TTimeFieldSegment, string> = { hour: 'HH', minute: 'MM', second: 'SS', period: 'AM' };\n\nconst TIME = /^(\\d{2}):(\\d{2})(?::(\\d{2}))?$/;\n\nconst two = (value: number): string => String(value).padStart(2, '0');\n\n/** 12 where the reader's locale writes AM and PM, 24 where it does not. */\nexport const localeHourCycle = (): TTimeFieldHourCycle =>\n new Intl.DateTimeFormat(undefined, { hour: 'numeric' }).resolvedOptions().hour12 ? 12 : 24;\n\n/** `HH:mm` or `HH:mm:ss` (24-hour, as `<input type=\"time\">` writes it) to the segments; anything else is no time. */\nexport const parseTime = (value: string, hourCycle: TTimeFieldHourCycle): ITimeParts => {\n const match = TIME.exec(value);\n if (match === null) {\n return EMPTY_TIME;\n }\n const hour24 = Number(match[1]);\n const minute = Number(match[2]);\n const second = match[3] === undefined ? 0 : Number(match[3]);\n if (hour24 > 23 || minute > 59 || second > 59) {\n return EMPTY_TIME;\n }\n if (hourCycle === 24) {\n return { hour: hour24, minute, second, period: null };\n }\n return { hour: hour24 % 12 === 0 ? 12 : hour24 % 12, minute, second, period: hour24 >= 12 ? 'PM' : 'AM' };\n};\n\n/** The segments to `HH:mm` or `HH:mm:ss`, or '' until every segment the field shows is in. */\nexport const formatTime = (parts: ITimeParts, hourCycle: TTimeFieldHourCycle, granularity: TTimeFieldGranularity): string => {\n const { hour, minute, second, period } = parts;\n if (hour === null || minute === null || (granularity === 'second' && second === null)) {\n return '';\n }\n let hour24 = hour;\n if (hourCycle === 12) {\n // 0 is only ever a first digit still waiting for its second.\n if (period === null || hour === 0) {\n return '';\n }\n hour24 = (hour % 12) + (period === 'PM' ? 12 : 0);\n }\n return granularity === 'second' ? `${two(hour24)}:${two(minute)}:${two(second ?? 0)}` : `${two(hour24)}:${two(minute)}`;\n};\n\nexport interface IRange {\n min: number;\n max: number;\n}\n\nexport const rangeOf = (segment: Exclude<TTimeFieldSegment, 'period'>, hourCycle: TTimeFieldHourCycle): IRange => {\n if (segment === 'hour') {\n return hourCycle === 12 ? { min: 1, max: 12 } : { min: 0, max: 23 };\n }\n return { min: 0, max: 59 };\n};\n\n/** One step up or down inside a range, wrapping at the ends. A segment with nothing in it starts at the end the step is heading away from. */\nexport const stepValue = (current: number | null, delta: number, { min, max }: IRange): number => {\n if (current === null) {\n return delta > 0 ? min : max;\n }\n const size = max - min + 1;\n return ((((current - min + delta) % size) + size) % size) + min;\n};\n\n/**\n * Types a digit into a segment. The second digit joins the first when it makes a number the segment can hold; otherwise it starts\n * over. The segment is done, and focus moves on, at two digits or as soon as no second digit could fit (a 3 in the hours, a 6 in the\n * minutes).\n */\nexport const typeDigit = (buffer: string, digit: string, { min, max }: IRange): { value: number; buffer: string; done: boolean } => {\n let text = buffer + digit;\n if (text.length > 2 || Number(text) > max) {\n text = digit;\n }\n const value = Number(text);\n const done = text.length === 2 || value * 10 > max;\n return { value: done && value < min ? min : value, buffer: done ? '' : text, done };\n};\n\nconst PAGE_STEP: Record<Exclude<TTimeFieldSegment, 'period'>, number> = { hour: 3, minute: 10, second: 10 };\n\nexport interface IEdit {\n parts: ITimeParts;\n /** The digits typed so far into this segment and not yet complete. */\n buffer: string;\n /** Move to the next segment: this one is finished. */\n advance: boolean;\n}\n\nconst withSegment = (parts: ITimeParts, segment: Exclude<TTimeFieldSegment, 'period'>, value: number | null): ITimeParts => ({\n ...parts,\n [segment]: value,\n});\n\n/** What a key does to a segment, or null when the key is not one the segment handles (Tab, a letter in the hours...). */\nexport const editSegment = (\n parts: ITimeParts,\n segment: TTimeFieldSegment,\n key: string,\n hourCycle: TTimeFieldHourCycle,\n buffer: string,\n): IEdit | null => {\n if (segment === 'period') {\n const current = parts.period;\n const set = (period: TTimeFieldPeriod | null): IEdit => ({ parts: { ...parts, period }, buffer: '', advance: false });\n switch (key) {\n case 'a':\n case 'A':\n return { ...set('AM'), advance: true };\n case 'p':\n case 'P':\n return { ...set('PM'), advance: true };\n case 'ArrowUp':\n case 'ArrowDown':\n return set(current === null ? (key === 'ArrowUp' ? 'AM' : 'PM') : current === 'AM' ? 'PM' : 'AM');\n case 'Home':\n return set('AM');\n case 'End':\n return set('PM');\n case 'Backspace':\n case 'Delete':\n return set(null);\n default:\n return null;\n }\n }\n\n const range = rangeOf(segment, hourCycle);\n const current = parts[segment];\n const set = (value: number | null, advance = false): IEdit => ({ parts: withSegment(parts, segment, value), buffer: '', advance });\n\n if (/^[0-9]$/.test(key)) {\n const typed = typeDigit(buffer, key, range);\n return { parts: withSegment(parts, segment, typed.value), buffer: typed.buffer, advance: typed.done };\n }\n switch (key) {\n case 'ArrowUp':\n return set(stepValue(current, 1, range));\n case 'ArrowDown':\n return set(stepValue(current, -1, range));\n case 'PageUp':\n return set(stepValue(current, PAGE_STEP[segment], range));\n case 'PageDown':\n return set(stepValue(current, -PAGE_STEP[segment], range));\n case 'Home':\n return set(range.min);\n case 'End':\n return set(range.max);\n case 'Backspace':\n case 'Delete':\n return set(null);\n default:\n return null;\n }\n};\n\nexport interface ITimeFieldProps extends Omit<ComponentPropsWithoutRef<'div'>, 'defaultValue' | 'onChange' | 'children' | 'role'> {\n /** The time as `HH:mm` or `HH:mm:ss`, 24-hour whatever the field shows, as `<input type=\"time\">` writes it. '' is no time. */\n value?: string;\n defaultValue?: string;\n /** Called with the time as soon as every segment is in, and with '' when one of a full time is cleared. */\n onValueChange?: (value: string) => void;\n /** `second` adds a seconds segment and a `HH:mm:ss` value. */\n granularity?: TTimeFieldGranularity;\n /** 12 shows an AM/PM segment, 24 does not. The default is what the reader's locale writes. */\n hourCycle?: TTimeFieldHourCycle;\n /** The earliest and the latest time, `HH:mm`. A time outside them is marked invalid; it is not refused. */\n min?: string;\n max?: string;\n disabled?: boolean;\n /** Shown, focusable and readable, not changeable. */\n readOnly?: boolean;\n invalid?: boolean;\n /** Submits the time with a form, under this name, as `HH:mm` or `HH:mm:ss`. */\n name?: string;\n labels?: Partial<ITimeFieldLabels>;\n}\n\nconst SEGMENT_CLASS =\n 'h-6 rounded-sm bg-transparent text-center text-sm tabular-nums caret-transparent outline-none selection:bg-transparent placeholder:text-muted-foreground focus:bg-foreground/10 disabled:cursor-not-allowed';\n\n/**\n * A time of day typed in segments, in the kit's field: hours, minutes, optional seconds, and AM/PM where the reader's locale uses it.\n * It exists so an app does not fall back to `<input type=\"time\">`, whose look differs between browsers and from the rest of the kit.\n *\n * Each segment is a spinbutton: type digits (the focus moves on by itself), ArrowUp and ArrowDown step with wrap-around, PageUp and\n * PageDown step by more, Home and End jump to the ends, Backspace empties, ArrowLeft and ArrowRight move between segments, and A or P\n * set the period. The group is named by `aria-label` or `aria-labelledby`; a screen reader reads each segment by its own name and value.\n */\nexport const TimeField = ({\n value,\n defaultValue = '',\n onValueChange,\n granularity = 'minute',\n hourCycle: hourCycleProp,\n min,\n max,\n disabled = false,\n readOnly = false,\n invalid = false,\n name,\n labels: labelOverrides,\n className,\n ...rest\n}: ITimeFieldProps) => {\n const labels = useMemo(() => ({ ...DEFAULT_TIME_FIELD_LABELS, ...labelOverrides }), [labelOverrides]);\n const hourCycle = useMemo(() => hourCycleProp ?? localeHourCycle(), [hourCycleProp]);\n const [parts, setParts] = useState<ITimeParts>(() => parseTime(value ?? defaultValue, hourCycle));\n const [seen, setSeen] = useState(value);\n const refSegments = useRef<Array<HTMLInputElement | null>>([]);\n const refBuffer = useRef<{ segment: TTimeFieldSegment; text: string }>({ segment: 'hour', text: '' });\n const formatted = formatTime(parts, hourCycle, granularity);\n // What the parent was last told, and the segments as the last key left them (state is a render behind inside one event).\n const refReported = useRef(formatted);\n const refParts = useRef(parts);\n refParts.current = parts;\n // A time handed in from outside replaces what is shown, unless it is the very time that is shown: clearing one segment of a full\n // time reports '', and taking that back as a new value would wipe the segments still being edited.\n if (value !== seen) {\n setSeen(value);\n if (value !== undefined && value !== formatted) {\n setParts(parseTime(value, hourCycle));\n refReported.current = value;\n }\n }\n\n const segments: TTimeFieldSegment[] = [\n 'hour',\n 'minute',\n ...(granularity === 'second' ? (['second'] as const) : []),\n ...(hourCycle === 12 ? (['period'] as const) : []),\n ];\n\n const focusSegment = (index: number): void => {\n const target = refSegments.current[index];\n if (target !== null && target !== undefined) {\n target.focus();\n target.select();\n }\n };\n\n const report = (next: ITimeParts): void => {\n const text = formatTime(next, hourCycle, granularity);\n if (text !== refReported.current) {\n refReported.current = text;\n onValueChange?.(text);\n }\n };\n\n const edit = (segment: TTimeFieldSegment, key: string): boolean => {\n const buffer = refBuffer.current.segment === segment ? refBuffer.current.text : '';\n const result = editSegment(refParts.current, segment, key, hourCycle, buffer);\n if (result === null) {\n return false;\n }\n refBuffer.current = { segment, text: result.buffer };\n refParts.current = result.parts;\n setParts(result.parts);\n // A segment still waiting for its second digit is not a time yet worth reporting (a 3 may become 30); it is reported when it is\n // done, or when the focus leaves it.\n if (result.buffer === '') {\n report(result.parts);\n }\n if (result.advance) {\n focusSegment(segments.indexOf(segment) + 1);\n }\n return true;\n };\n\n const onKeyDown = (segment: TTimeFieldSegment, event: KeyboardEvent<HTMLInputElement>): void => {\n if (event.altKey || event.ctrlKey || event.metaKey) {\n return;\n }\n const index = segments.indexOf(segment);\n if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {\n event.preventDefault();\n focusSegment(index + (event.key === 'ArrowRight' ? 1 : -1));\n return;\n }\n if (disabled || readOnly) {\n return;\n }\n if (edit(segment, event.key)) {\n event.preventDefault();\n }\n };\n\n // Leaving a segment ends what was being typed into it: coming back and typing a 2 must not make 12 of an old 1. A 0 left alone in a\n // 12-hour hour is the start of a number that never came, so it is emptied.\n const onBlur = (segment: TTimeFieldSegment): void => {\n const unfinished = refBuffer.current.segment === segment && refBuffer.current.text !== '';\n refBuffer.current = { segment, text: '' };\n let settled = refParts.current;\n if (segment === 'hour' && hourCycle === 12 && settled.hour === 0) {\n settled = { ...settled, hour: null };\n refParts.current = settled;\n setParts(settled);\n }\n if (unfinished) {\n report(settled);\n }\n };\n\n // A phone's keyboard does not send key events a page can use; what it types arrives as a change to the input.\n const onChange = (segment: TTimeFieldSegment, event: ChangeEvent<HTMLInputElement>): void => {\n if (disabled || readOnly) {\n return;\n }\n const typed = event.target.value.slice(-1);\n edit(segment, typed);\n };\n\n const outOfRange =\n formatted !== '' && ((min !== undefined && formatted.slice(0, min.length) < min) || (max !== undefined && formatted.slice(0, max.length) > max));\n const flagged = invalid || outOfRange;\n\n const display = (segment: TTimeFieldSegment): string => {\n if (segment === 'period') {\n return parts.period ?? '';\n }\n const current = parts[segment];\n return current === null ? '' : two(current);\n };\n\n return (\n <InputGroup role=\"group\" className={cn('w-fit', className)} {...rest}>\n <InputGroupAddon>\n <IconClock aria-hidden=\"true\" />\n </InputGroupAddon>\n <div className=\"flex items-center gap-0.5 pr-2\">\n {segments.map((segment, index) => {\n const numeric = segment !== 'period';\n const range = numeric ? rangeOf(segment, hourCycle) : { min: 0, max: 1 };\n const current = numeric ? parts[segment] : parts.period === null ? null : parts.period === 'AM' ? 0 : 1;\n return (\n <span key={segment} className=\"flex items-center gap-0.5\">\n {index > 0 && segment !== 'period' && (\n <span aria-hidden=\"true\" className=\"text-muted-foreground\">\n :\n </span>\n )}\n <input\n ref={(node) => {\n refSegments.current[index] = node;\n }}\n data-slot=\"input-group-control\"\n data-segment={segment}\n role=\"spinbutton\"\n inputMode={numeric ? 'numeric' : 'text'}\n autoComplete=\"off\"\n autoCorrect=\"off\"\n spellCheck={false}\n value={display(segment)}\n placeholder={SEGMENT_PLACEHOLDER[segment]}\n disabled={disabled}\n readOnly={readOnly}\n aria-label={labels[segment]}\n aria-readonly={readOnly || undefined}\n aria-invalid={flagged || undefined}\n aria-valuemin={range.min}\n aria-valuemax={range.max}\n aria-valuenow={current ?? undefined}\n aria-valuetext={current === null ? labels.empty : segment === 'period' ? (parts.period ?? labels.empty) : two(current)}\n className={cn(SEGMENT_CLASS, segment === 'period' ? 'w-7' : 'w-5')}\n onKeyDown={(event) => onKeyDown(segment, event)}\n onChange={(event) => onChange(segment, event)}\n onFocus={(event) => event.currentTarget.select()}\n onBlur={() => onBlur(segment)}\n />\n </span>\n );\n })}\n </div>\n {name !== undefined && <input type=\"hidden\" name={name} value={formatted} disabled={disabled} />}\n </InputGroup>\n );\n};\n"],"mappings":"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"}
|
|
@@ -29,7 +29,7 @@ var BreadcrumbItem = ({ className, ...props }) => {
|
|
|
29
29
|
var BreadcrumbLink = ({ className, render, ...props }) => {
|
|
30
30
|
return useRender({
|
|
31
31
|
defaultTagName: "a",
|
|
32
|
-
props: mergeProps({ className: cn("transition-colors hover:text-foreground", className) }, props),
|
|
32
|
+
props: mergeProps({ className: cn("inline-flex h-6 items-center transition-colors hover:text-foreground", className) }, props),
|
|
33
33
|
render,
|
|
34
34
|
state: { slot: "breadcrumb-link" }
|
|
35
35
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"breadcrumb.js","names":[],"sources":["../../../../../../../ui/src/components/ui/breadcrumb/breadcrumb.tsx"],"sourcesContent":["import { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconChevronRight, IconDots } from '@tabler/icons-react';\nimport type * as React from 'react';\n\nexport const Breadcrumb = ({ className, ...props }: React.ComponentProps<'nav'>) => {\n return <nav aria-label=\"breadcrumb\" data-slot=\"breadcrumb\" className={cn(className)} {...props} />;\n};\n\nexport const BreadcrumbList = ({ className, ...props }: React.ComponentProps<'ol'>) => {\n return (\n <ol\n data-slot=\"breadcrumb-list\"\n className={cn('flex flex-wrap items-center gap-1.5 text-sm wrap-break-word text-muted-foreground', className)}\n {...props}\n />\n );\n};\n\nexport const BreadcrumbItem = ({ className, ...props }: React.ComponentProps<'li'>) => {\n return <li data-slot=\"breadcrumb-item\" className={cn('inline-flex items-center gap-1', className)} {...props} />;\n};\n\nexport const BreadcrumbLink = ({ className, render, ...props }: useRender.ComponentProps<'a'>) => {\n return useRender({\n defaultTagName: 'a',\n props: mergeProps<'a'>(\n {\n className: cn('transition-colors hover:text-foreground', className),\n },\n props,\n ),\n render,\n state: {\n slot: 'breadcrumb-link',\n },\n });\n};\n\nexport const BreadcrumbPage = ({ className, ...props }: React.ComponentProps<'span'>) => {\n return (\n // biome-ignore lint/a11y/useFocusableInteractive: matches shadcn pattern; the current page is not an interactive target\n // biome-ignore lint/a11y/useSemanticElements: <span> is intentional; role=\"link\" here only conveys visual/semantic context, not an actual navigable link\n <span\n data-slot=\"breadcrumb-page\"\n role=\"link\"\n aria-disabled=\"true\"\n aria-current=\"page\"\n className={cn('font-normal text-foreground', className)}\n {...props}\n />\n );\n};\n\nexport const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<'li'>) => {\n return (\n <li data-slot=\"breadcrumb-separator\" role=\"presentation\" aria-hidden=\"true\" className={cn('[&>svg]:size-3.5', className)} {...props}>\n {children ?? <IconChevronRight />}\n </li>\n );\n};\n\nexport const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => {\n return (\n <span\n data-slot=\"breadcrumb-ellipsis\"\n role=\"presentation\"\n aria-hidden=\"true\"\n className={cn('flex size-5 items-center justify-center [&>svg]:size-4', className)}\n {...props}\n >\n <IconDots />\n <span className=\"sr-only\">More</span>\n </span>\n );\n};\n"],"mappings":";;;;;;AAMA,IAAa,cAAc,EAAE,WAAW,GAAG,YAAyC;CAClF,OAAO,oBAAC,OAAD;EAAK,cAAW;EAAa,aAAU;EAAa,WAAW,GAAG,SAAS;EAAG,GAAI;CAAQ,CAAA;AACnG;AAEA,IAAa,kBAAkB,EAAE,WAAW,GAAG,YAAwC;CACrF,OACE,oBAAC,MAAD;EACE,aAAU;EACV,WAAW,GAAG,qFAAqF,SAAS;EAC5G,GAAI;CACL,CAAA;AAEL;AAEA,IAAa,kBAAkB,EAAE,WAAW,GAAG,YAAwC;CACrF,OAAO,oBAAC,MAAD;EAAI,aAAU;EAAkB,WAAW,GAAG,kCAAkC,SAAS;EAAG,GAAI;CAAQ,CAAA;AACjH;AAEA,IAAa,kBAAkB,EAAE,WAAW,QAAQ,GAAG,YAA2C;CAChG,OAAO,UAAU;EACf,gBAAgB;EAChB,OAAO,WACL,EACE,WAAW,GAAG,
|
|
1
|
+
{"version":3,"file":"breadcrumb.js","names":[],"sources":["../../../../../../../ui/src/components/ui/breadcrumb/breadcrumb.tsx"],"sourcesContent":["import { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconChevronRight, IconDots } from '@tabler/icons-react';\nimport type * as React from 'react';\n\nexport const Breadcrumb = ({ className, ...props }: React.ComponentProps<'nav'>) => {\n return <nav aria-label=\"breadcrumb\" data-slot=\"breadcrumb\" className={cn(className)} {...props} />;\n};\n\nexport const BreadcrumbList = ({ className, ...props }: React.ComponentProps<'ol'>) => {\n return (\n <ol\n data-slot=\"breadcrumb-list\"\n className={cn('flex flex-wrap items-center gap-1.5 text-sm wrap-break-word text-muted-foreground', className)}\n {...props}\n />\n );\n};\n\nexport const BreadcrumbItem = ({ className, ...props }: React.ComponentProps<'li'>) => {\n return <li data-slot=\"breadcrumb-item\" className={cn('inline-flex items-center gap-1', className)} {...props} />;\n};\n\nexport const BreadcrumbLink = ({ className, render, ...props }: useRender.ComponentProps<'a'>) => {\n return useRender({\n defaultTagName: 'a',\n props: mergeProps<'a'>(\n {\n className: cn('inline-flex h-6 items-center transition-colors hover:text-foreground', className),\n },\n props,\n ),\n render,\n state: {\n slot: 'breadcrumb-link',\n },\n });\n};\n\nexport const BreadcrumbPage = ({ className, ...props }: React.ComponentProps<'span'>) => {\n return (\n // biome-ignore lint/a11y/useFocusableInteractive: matches shadcn pattern; the current page is not an interactive target\n // biome-ignore lint/a11y/useSemanticElements: <span> is intentional; role=\"link\" here only conveys visual/semantic context, not an actual navigable link\n <span\n data-slot=\"breadcrumb-page\"\n role=\"link\"\n aria-disabled=\"true\"\n aria-current=\"page\"\n className={cn('font-normal text-foreground', className)}\n {...props}\n />\n );\n};\n\nexport const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<'li'>) => {\n return (\n <li data-slot=\"breadcrumb-separator\" role=\"presentation\" aria-hidden=\"true\" className={cn('[&>svg]:size-3.5', className)} {...props}>\n {children ?? <IconChevronRight />}\n </li>\n );\n};\n\nexport const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => {\n return (\n <span\n data-slot=\"breadcrumb-ellipsis\"\n role=\"presentation\"\n aria-hidden=\"true\"\n className={cn('flex size-5 items-center justify-center [&>svg]:size-4', className)}\n {...props}\n >\n <IconDots />\n <span className=\"sr-only\">More</span>\n </span>\n );\n};\n"],"mappings":";;;;;;AAMA,IAAa,cAAc,EAAE,WAAW,GAAG,YAAyC;CAClF,OAAO,oBAAC,OAAD;EAAK,cAAW;EAAa,aAAU;EAAa,WAAW,GAAG,SAAS;EAAG,GAAI;CAAQ,CAAA;AACnG;AAEA,IAAa,kBAAkB,EAAE,WAAW,GAAG,YAAwC;CACrF,OACE,oBAAC,MAAD;EACE,aAAU;EACV,WAAW,GAAG,qFAAqF,SAAS;EAC5G,GAAI;CACL,CAAA;AAEL;AAEA,IAAa,kBAAkB,EAAE,WAAW,GAAG,YAAwC;CACrF,OAAO,oBAAC,MAAD;EAAI,aAAU;EAAkB,WAAW,GAAG,kCAAkC,SAAS;EAAG,GAAI;CAAQ,CAAA;AACjH;AAEA,IAAa,kBAAkB,EAAE,WAAW,QAAQ,GAAG,YAA2C;CAChG,OAAO,UAAU;EACf,gBAAgB;EAChB,OAAO,WACL,EACE,WAAW,GAAG,wEAAwE,SAAS,EACjG,GACA,KACF;EACA;EACA,OAAO,EACL,MAAM,kBACR;CACF,CAAC;AACH;AAEA,IAAa,kBAAkB,EAAE,WAAW,GAAG,YAA0C;CACvF,OAGE,oBAAC,QAAD;EACE,aAAU;EACV,MAAK;EACL,iBAAc;EACd,gBAAa;EACb,WAAW,GAAG,+BAA+B,SAAS;EACtD,GAAI;CACL,CAAA;AAEL;AAEA,IAAa,uBAAuB,EAAE,UAAU,WAAW,GAAG,YAAwC;CACpG,OACE,oBAAC,MAAD;EAAI,aAAU;EAAuB,MAAK;EAAe,eAAY;EAAO,WAAW,GAAG,oBAAoB,SAAS;EAAG,GAAI;EAC3H,UAAA,YAAY,oBAAC,kBAAD,CAAmB,CAAA;CAC9B,CAAA;AAER;AAEA,IAAa,sBAAsB,EAAE,WAAW,GAAG,YAA0C;CAC3F,OACE,qBAAC,QAAD;EACE,aAAU;EACV,MAAK;EACL,eAAY;EACZ,WAAW,GAAG,0DAA0D,SAAS;EACjF,GAAI;EALN,UAAA,CAOE,oBAAC,UAAD,CAAW,CAAA,GACX,oBAAC,QAAD;GAAM,WAAU;GAAU,UAAA;EAAU,CAAA,CAChC;;AAEV"}
|
|
@@ -14,7 +14,7 @@ var buttonVariants = cva("group/button inline-flex shrink-0 cursor-pointer items
|
|
|
14
14
|
variants: {
|
|
15
15
|
variant: {
|
|
16
16
|
default: `bg-primary text-primary-foreground hover:bg-primary/80 ${DISABLED_FILL}`,
|
|
17
|
-
outline: `border-
|
|
17
|
+
outline: `border-control bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-control dark:bg-input/30 dark:hover:bg-input/50 ${DISABLED_FILL}`,
|
|
18
18
|
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 ${DISABLED_FILL}`,
|
|
19
19
|
ghost: "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 aria-disabled:hover:bg-transparent dark:aria-disabled:hover:bg-transparent",
|
|
20
20
|
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 ${DISABLED_FILL}`,
|
|
@@ -23,7 +23,7 @@ var buttonVariants = cva("group/button inline-flex shrink-0 cursor-pointer items
|
|
|
23
23
|
size: {
|
|
24
24
|
default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
25
25
|
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",
|
|
26
|
-
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-
|
|
26
|
+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 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.5",
|
|
27
27
|
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
28
28
|
icon: "size-8",
|
|
29
29
|
"icon-xs": "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|