@burtson-labs/ui 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/DEPENDENCIES.txt +1 -1
- package/README.md +15 -11
- package/dist/components/accordion.d.ts +3 -3
- package/dist/components/accordion.js +13 -10
- package/dist/components/accordion.js.map +1 -1
- package/dist/components/agent-run.d.ts +22 -0
- package/dist/components/agent-run.js +131 -0
- package/dist/components/agent-run.js.map +1 -0
- package/dist/components/alert-dialog.d.ts +12 -10
- package/dist/components/alert-dialog.js +29 -20
- package/dist/components/alert-dialog.js.map +1 -1
- package/dist/components/alert.d.ts +5 -3
- package/dist/components/alert.js +11 -7
- package/dist/components/alert.js.map +1 -1
- package/dist/components/app-shell.d.ts +7 -6
- package/dist/components/app-shell.js +21 -14
- package/dist/components/app-shell.js.map +1 -1
- package/dist/components/attachment.d.ts +3 -3
- package/dist/components/attachment.js +10 -7
- package/dist/components/attachment.js.map +1 -1
- package/dist/components/audio-player.d.ts +2 -2
- package/dist/components/audio-player.js +8 -6
- package/dist/components/audio-player.js.map +1 -1
- package/dist/components/avatar-upload.d.ts +19 -0
- package/dist/components/avatar-upload.js +359 -0
- package/dist/components/avatar-upload.js.map +1 -0
- package/dist/components/avatar.d.ts +3 -3
- package/dist/components/avatar.js +10 -7
- package/dist/components/avatar.js.map +1 -1
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +12 -10
- package/dist/components/badge.js.map +1 -1
- package/dist/components/breadcrumb.d.ts +8 -8
- package/dist/components/breadcrumb.js +24 -17
- package/dist/components/breadcrumb.js.map +1 -1
- package/dist/components/button.d.ts +8 -1
- package/dist/components/button.js +12 -3
- package/dist/components/button.js.map +1 -1
- package/dist/components/card.d.ts +8 -8
- package/dist/components/card.js +32 -23
- package/dist/components/card.js.map +1 -1
- package/dist/components/chat-history.d.ts +1 -1
- package/dist/components/chat-history.js +10 -9
- package/dist/components/chat-history.js.map +1 -1
- package/dist/components/chat-layout.d.ts +1 -1
- package/dist/components/chat-layout.js +7 -3
- package/dist/components/chat-layout.js.map +1 -1
- package/dist/components/checkbox-card.d.ts +2 -2
- package/dist/components/checkbox-card.js +9 -7
- package/dist/components/checkbox-card.js.map +1 -1
- package/dist/components/checkbox.d.ts +23 -2
- package/dist/components/checkbox.js +48 -6
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/collapsible.d.ts +2 -2
- package/dist/components/collapsible.js +7 -5
- package/dist/components/collapsible.js.map +1 -1
- package/dist/components/combobox.d.ts +14 -4
- package/dist/components/combobox.js +18 -8
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/command.d.ts +68 -12
- package/dist/components/command.js +38 -25
- package/dist/components/command.js.map +1 -1
- package/dist/components/composer.d.ts +2 -2
- package/dist/components/composer.js +9 -7
- package/dist/components/composer.js.map +1 -1
- package/dist/components/connection-status.d.ts +3 -3
- package/dist/components/connection-status.js +18 -14
- package/dist/components/connection-status.js.map +1 -1
- package/dist/components/context-menu.d.ts +14 -13
- package/dist/components/context-menu.js +33 -23
- package/dist/components/context-menu.js.map +1 -1
- package/dist/components/conversation.d.ts +1 -1
- package/dist/components/conversation.js +6 -5
- package/dist/components/conversation.js.map +1 -1
- package/dist/components/copy-button.d.ts +1 -1
- package/dist/components/copy-button.js +3 -2
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table.d.ts +7 -2
- package/dist/components/data-table.js +16 -42
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/dialog.d.ts +18 -9
- package/dist/components/dialog.js +38 -22
- package/dist/components/dialog.js.map +1 -1
- package/dist/components/dropdown-menu.d.ts +14 -13
- package/dist/components/dropdown-menu.js +33 -23
- package/dist/components/dropdown-menu.js.map +1 -1
- package/dist/components/editor-tabs.d.ts +1 -1
- package/dist/components/editor-tabs.js +6 -5
- package/dist/components/editor-tabs.js.map +1 -1
- package/dist/components/empty-state.d.ts +5 -5
- package/dist/components/empty-state.js +16 -11
- package/dist/components/empty-state.js.map +1 -1
- package/dist/components/error-summary.d.ts +21 -0
- package/dist/components/error-summary.js +62 -0
- package/dist/components/error-summary.js.map +1 -0
- package/dist/components/field.d.ts +66 -7
- package/dist/components/field.js +151 -20
- package/dist/components/field.js.map +1 -1
- package/dist/components/form-actions.d.ts +18 -0
- package/dist/components/form-actions.js +30 -0
- package/dist/components/form-actions.js.map +1 -0
- package/dist/components/icon-button.d.ts +3 -1
- package/dist/components/icon-button.js +5 -3
- package/dist/components/icon-button.js.map +1 -1
- package/dist/components/input.d.ts +42 -7
- package/dist/components/input.js +53 -10
- package/dist/components/input.js.map +1 -1
- package/dist/components/kbd.d.ts +2 -2
- package/dist/components/kbd.js +7 -5
- package/dist/components/kbd.js.map +1 -1
- package/dist/components/label.d.ts +6 -1
- package/dist/components/label.js +10 -4
- package/dist/components/label.js.map +1 -1
- package/dist/components/markdown.d.ts +4 -3
- package/dist/components/markdown.js +7 -6
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/menubar.d.ts +16 -14
- package/dist/components/menubar.js +39 -27
- package/dist/components/menubar.js.map +1 -1
- package/dist/components/message-actions.d.ts +3 -3
- package/dist/components/message-actions.js +15 -12
- package/dist/components/message-actions.js.map +1 -1
- package/dist/components/message.d.ts +1 -1
- package/dist/components/message.js +5 -3
- package/dist/components/message.js.map +1 -1
- package/dist/components/mobile-nav.d.ts +1 -1
- package/dist/components/mobile-nav.js +6 -5
- package/dist/components/mobile-nav.js.map +1 -1
- package/dist/components/native-select.d.ts +16 -0
- package/dist/components/native-select.js +33 -0
- package/dist/components/native-select.js.map +1 -0
- package/dist/components/navigation-menu.d.ts +12 -11
- package/dist/components/navigation-menu.js +32 -23
- package/dist/components/navigation-menu.js.map +1 -1
- package/dist/components/number-input.d.ts +23 -0
- package/dist/components/number-input.js +45 -0
- package/dist/components/number-input.js.map +1 -0
- package/dist/components/onboarding-checklist.d.ts +1 -1
- package/dist/components/onboarding-checklist.js +4 -3
- package/dist/components/onboarding-checklist.js.map +1 -1
- package/dist/components/page-header.d.ts +7 -6
- package/dist/components/page-header.js +22 -15
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/pagination.d.ts +1 -1
- package/dist/components/pagination.js +3 -2
- package/dist/components/pagination.js.map +1 -1
- package/dist/components/popover.d.ts +13 -10
- package/dist/components/popover.js +23 -17
- package/dist/components/popover.js.map +1 -1
- package/dist/components/progress.d.ts +1 -1
- package/dist/components/progress.js +6 -5
- package/dist/components/progress.js.map +1 -1
- package/dist/components/radio-group.d.ts +22 -3
- package/dist/components/radio-group.js +51 -10
- package/dist/components/radio-group.js.map +1 -1
- package/dist/components/reasoning.d.ts +3 -3
- package/dist/components/reasoning.js +9 -7
- package/dist/components/reasoning.js.map +1 -1
- package/dist/components/scroll-area.d.ts +3 -2
- package/dist/components/scroll-area.js +10 -7
- package/dist/components/scroll-area.js.map +1 -1
- package/dist/components/secret-input.d.ts +3 -2
- package/dist/components/secret-input.js +6 -4
- package/dist/components/secret-input.js.map +1 -1
- package/dist/components/select.d.ts +11 -6
- package/dist/components/select.js +24 -18
- package/dist/components/select.js.map +1 -1
- package/dist/components/separator.d.ts +1 -1
- package/dist/components/separator.js +4 -3
- package/dist/components/separator.js.map +1 -1
- package/dist/components/sheet.d.ts +10 -8
- package/dist/components/sheet.js +31 -20
- package/dist/components/sheet.js.map +1 -1
- package/dist/components/skeleton.d.ts +2 -1
- package/dist/components/skeleton.js +6 -4
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/slider.d.ts +10 -6
- package/dist/components/slider.js +15 -7
- package/dist/components/slider.js.map +1 -1
- package/dist/components/source.d.ts +2 -2
- package/dist/components/source.js +9 -7
- package/dist/components/source.js.map +1 -1
- package/dist/components/spinner.d.ts +4 -2
- package/dist/components/spinner.js +4 -3
- package/dist/components/spinner.js.map +1 -1
- package/dist/components/stat-card.d.ts +27 -2
- package/dist/components/stat-card.js +69 -9
- package/dist/components/stat-card.js.map +1 -1
- package/dist/components/status.d.ts +7 -6
- package/dist/components/status.js +11 -5
- package/dist/components/status.js.map +1 -1
- package/dist/components/steps.d.ts +1 -1
- package/dist/components/steps.js +4 -3
- package/dist/components/steps.js.map +1 -1
- package/dist/components/switch.d.ts +46 -2
- package/dist/components/switch.js +85 -8
- package/dist/components/switch.js.map +1 -1
- package/dist/components/table.d.ts +29 -12
- package/dist/components/table.js +79 -34
- package/dist/components/table.js.map +1 -1
- package/dist/components/tabs.d.ts +9 -4
- package/dist/components/tabs.js +22 -13
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/textarea.d.ts +6 -1
- package/dist/components/textarea.js +3 -3
- package/dist/components/textarea.js.map +1 -1
- package/dist/components/toast.d.ts +10 -6
- package/dist/components/toast.js +27 -18
- package/dist/components/toast.js.map +1 -1
- package/dist/components/toaster.d.ts +4 -2
- package/dist/components/toaster.js +6 -2
- package/dist/components/toaster.js.map +1 -1
- package/dist/components/tool-call.d.ts +3 -3
- package/dist/components/tool-call.js +16 -12
- package/dist/components/tool-call.js.map +1 -1
- package/dist/components/toolbar.d.ts +9 -4
- package/dist/components/toolbar.js +14 -9
- package/dist/components/toolbar.js.map +1 -1
- package/dist/components/tooltip.d.ts +3 -2
- package/dist/components/tooltip.js +7 -4
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/tour.js +2 -2
- package/dist/components/tour.js.map +1 -1
- package/dist/components/tree-view.d.ts +1 -1
- package/dist/components/tree-view.js +9 -5
- package/dist/components/tree-view.js.map +1 -1
- package/dist/components/voice-recorder.d.ts +1 -1
- package/dist/components/voice-recorder.js +4 -2
- package/dist/components/voice-recorder.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +19 -12
- package/dist/lib/media.d.ts +18 -0
- package/dist/lib/media.js +34 -0
- package/dist/lib/media.js.map +1 -0
- package/dist/lib/utils.d.ts +32 -0
- package/dist/lib/utils.js +33 -1
- package/dist/lib/utils.js.map +1 -1
- package/dist/mui/index.js +20 -8
- package/dist/mui/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/theme.css +36 -17
- package/dist/tokens.d.ts +78 -36
- package/dist/tokens.js +63 -32
- package/dist/tokens.js.map +1 -1
- package/package.json +5 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tree-view.js","names":[],"sources":["../../src/components/tree-view.tsx"],"sourcesContent":["import ChevronRight from '@burtson-labs/icons/react/chevron-right';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { Spinner } from './spinner';\n\nexport interface TreeNode {\n /** Stable identity. Keep it across renders (a path, a database id). */\n id: string;\n /** Plain text: the accessible name and the typeahead key. */\n label: string;\n icon?: React.ReactNode;\n /** Undefined: a leaf, unless `hasChildren` says the children load later. */\n children?: TreeNode[];\n /** Expandable before its children are known; expanding calls onLoadChildren. */\n hasChildren?: boolean;\n /** Children are being fetched; shows a loading row while expanded. */\n loading?: boolean;\n disabled?: boolean;\n /** Trailing detail, such as a git status letter or a count. */\n meta?: React.ReactNode;\n /** Extra classes on the row, for app states such as an ignored file or a highlight. */\n className?: string;\n}\n\nexport interface TreeViewProps extends Omit<\n React.ComponentProps<'div'>,\n 'children' | 'onSelect' | 'aria-label'\n> {\n nodes: TreeNode[];\n /** Required: what the tree holds, e.g. \"Files\". */\n 'aria-label': string;\n expanded: string[];\n onExpandedChange: (ids: string[]) => void;\n selected: string[];\n onSelectedChange: (ids: string[]) => void;\n /** `multiple`: Shift extends a range, Ctrl/⌘ toggles, Space toggles. */\n selectionMode?: 'single' | 'multiple';\n /** Move selection with focus on arrow keys (a file explorer); default off. */\n selectionFollowsFocus?: boolean;\n /** Enter or double-click: open the file, run the item. */\n onAction?: (id: string) => void;\n /**\n * A plain click on a row without children also runs `onAction`, the way a\n * file explorer opens a file on one click. Default off.\n */\n actionOnClick?: boolean;\n /**\n * A plain click on an expandable row toggles it. Defaults to on for single\n * selection; turn it on for a multi-select explorer, where Shift and Ctrl/⌘\n * clicks still only select.\n */\n expandOnClick?: boolean;\n /** Draw a line per ancestor level while the pointer is over the tree. */\n indentGuides?: boolean;\n /** Called once when a node with `hasChildren` and no children expands. */\n onLoadChildren?: (id: string) => void;\n /** Replace the label, e.g. with a rename input. Keys typed in an input never move focus. */\n renderLabel?: (node: TreeNode) => React.ReactNode;\n /** `compact` 24px rows for desktop explorers, `default` 28px. */\n density?: 'compact' | 'default';\n /** Shown when `nodes` is empty. */\n empty?: React.ReactNode;\n /** Right-click or the context-menu key on a row. */\n onRowContextMenu?: (id: string, event: React.MouseEvent | React.KeyboardEvent) => void;\n}\n\ninterface Row {\n node: TreeNode;\n level: number;\n parentId: string | null;\n setSize: number;\n posInSet: number;\n expandable: boolean;\n expanded: boolean;\n}\n\nconst isExpandable = (n: TreeNode) => (n.children?.length ?? 0) > 0 || Boolean(n.hasChildren);\n\n/** The rows a person can see, in order, with the ARIA position data for each. */\nexport function flattenTree(nodes: TreeNode[], expanded: ReadonlySet<string>): Row[] {\n const rows: Row[] = [];\n const walk = (list: TreeNode[], level: number, parentId: string | null) => {\n list.forEach((node, i) => {\n const expandable = isExpandable(node);\n const open = expandable && expanded.has(node.id);\n rows.push({\n node,\n level,\n parentId,\n setSize: list.length,\n posInSet: i + 1,\n expandable,\n expanded: open,\n });\n if (open && node.children) walk(node.children, level + 1, node.id);\n });\n };\n walk(nodes, 1, null);\n return rows;\n}\n\nconst isTypingTarget = (target: EventTarget | null) =>\n target instanceof HTMLElement &&\n (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName));\n\n/**\n * A file-explorer style tree. Everything is controlled: the app owns which\n * nodes are expanded and selected, and file operations are callbacks. Rows\n * render flat (role=\"treeitem\" with aria-level), one tab stop for the tree,\n * arrows to move, Right/Left to open and close, Home/End, typeahead.\n */\nfunction TreeView({\n nodes,\n expanded,\n onExpandedChange,\n selected,\n onSelectedChange,\n selectionMode = 'single',\n selectionFollowsFocus = false,\n onAction,\n actionOnClick = false,\n expandOnClick,\n indentGuides = false,\n onLoadChildren,\n renderLabel,\n density = 'default',\n empty,\n onRowContextMenu,\n className,\n ...props\n}: TreeViewProps) {\n const expandedSet = React.useMemo(() => new Set(expanded), [expanded]);\n const selectedSet = React.useMemo(() => new Set(selected), [selected]);\n const rows = React.useMemo(() => flattenTree(nodes, expandedSet), [nodes, expandedSet]);\n const index = React.useMemo(() => new Map(rows.map((r, i) => [r.node.id, i])), [rows]);\n\n const [focusedId, setFocusedId] = React.useState<string | null>(null);\n const anchorId = React.useRef<string | null>(null);\n const rowRefs = React.useRef(new Map<string, HTMLDivElement>());\n const treeRef = React.useRef<HTMLDivElement>(null);\n const hadFocus = React.useRef(false);\n const lastIds = React.useRef<string[]>([]);\n const lastParents = React.useRef(new Map<string, string | null>());\n const typeahead = React.useRef({ text: '', at: 0 });\n\n // The tab stop: the focused row if it is still visible, else the first\n // selected visible row, else the first row.\n const tabStopId =\n (focusedId && index.has(focusedId) ? focusedId : null) ??\n rows.find((r) => selectedSet.has(r.node.id))?.node.id ??\n rows[0]?.node.id ??\n null;\n\n const focusRow = React.useCallback((id: string) => {\n setFocusedId(id);\n rowRefs.current.get(id)?.focus();\n rowRefs.current.get(id)?.scrollIntoView?.({ block: 'nearest' });\n }, []);\n\n // Focus recovery: when the focused row disappears (deleted, or a parent\n // collapsed), move to its nearest visible ancestor, else the row now at its\n // old position. Only takes DOM focus if the tree had it.\n React.useLayoutEffect(() => {\n if (focusedId && !index.has(focusedId)) {\n let next: string | null = null;\n let parent = lastParents.current.get(focusedId) ?? null;\n while (parent && !index.has(parent)) parent = lastParents.current.get(parent) ?? null;\n const was = Math.max(0, lastIds.current.indexOf(focusedId));\n next = parent ?? rows[Math.min(was, rows.length - 1)]?.node.id ?? null;\n setFocusedId(next);\n const active = document.activeElement;\n if (next && hadFocus.current && (!active || active === document.body))\n rowRefs.current.get(next)?.focus();\n }\n lastIds.current = rows.map((r) => r.node.id);\n lastParents.current = new Map(rows.map((r) => [r.node.id, r.parentId]));\n }, [rows, index, focusedId]);\n\n const setExpanded = (row: Row, open: boolean) => {\n if (!row.expandable || row.expanded === open) return;\n const id = row.node.id;\n onExpandedChange(open ? [...expanded, id] : expanded.filter((e) => e !== id));\n if (open && !row.node.children && row.node.hasChildren) onLoadChildren?.(id);\n };\n\n const selectRange = (toId: string) => {\n const from = index.get(anchorId.current ?? toId) ?? 0;\n const to = index.get(toId) ?? 0;\n const [a, b] = from < to ? [from, to] : [to, from];\n onSelectedChange(\n rows\n .slice(a, b + 1)\n .filter((r) => !r.node.disabled)\n .map((r) => r.node.id),\n );\n };\n\n const select = (id: string, mode: 'replace' | 'toggle' | 'range') => {\n if (rows[index.get(id) ?? -1]?.node.disabled) return;\n if (selectionMode === 'single' || mode === 'replace') {\n anchorId.current = id;\n onSelectedChange([id]);\n } else if (mode === 'toggle') {\n anchorId.current = id;\n onSelectedChange(selectedSet.has(id) ? selected.filter((s) => s !== id) : [...selected, id]);\n } else selectRange(id);\n };\n\n const moveTo = (i: number, extend: boolean) => {\n const row = rows[Math.max(0, Math.min(rows.length - 1, i))];\n if (!row) return;\n focusRow(row.node.id);\n if (selectionMode === 'multiple' && extend) selectRange(row.node.id);\n else if (selectionFollowsFocus && !row.node.disabled) select(row.node.id, 'replace');\n };\n\n const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n props.onKeyDown?.(event);\n if (event.defaultPrevented || isTypingTarget(event.target)) return;\n const id = tabStopId;\n if (!id) return;\n const i = index.get(id) ?? 0;\n const row = rows[i];\n if (!row) return;\n const mod = event.metaKey || event.ctrlKey;\n let handled = true;\n switch (event.key) {\n case 'ArrowDown':\n moveTo(i + 1, event.shiftKey);\n break;\n case 'ArrowUp':\n moveTo(i - 1, event.shiftKey);\n break;\n case 'Home':\n moveTo(0, event.shiftKey);\n break;\n case 'End':\n moveTo(rows.length - 1, event.shiftKey);\n break;\n case 'ArrowRight':\n if (row.expandable && !row.expanded) setExpanded(row, true);\n else if (row.expanded && rows[i + 1]?.parentId === row.node.id)\n moveTo(i + 1, event.shiftKey);\n break;\n case 'ArrowLeft':\n if (row.expanded) setExpanded(row, false);\n else if (row.parentId) moveTo(index.get(row.parentId) ?? i, event.shiftKey);\n break;\n case 'Enter':\n if (row.node.disabled) break;\n select(row.node.id, 'replace');\n onAction?.(row.node.id);\n break;\n case ' ':\n select(\n row.node.id,\n selectionMode === 'multiple' ? (event.shiftKey ? 'range' : 'toggle') : 'replace',\n );\n break;\n case 'a':\n if (mod && selectionMode === 'multiple') {\n onSelectedChange(rows.filter((r) => !r.node.disabled).map((r) => r.node.id));\n } else handled = false;\n break;\n case 'ContextMenu':\n onRowContextMenu?.(row.node.id, event);\n break;\n default:\n handled = false;\n }\n if (!handled && event.key.length === 1 && !mod && !event.altKey) {\n // Typeahead: jump to the next row whose label starts with what was typed.\n const now = event.timeStamp;\n const t = typeahead.current;\n t.text = now - t.at > 600 ? event.key : t.text + event.key;\n t.at = now;\n const query = t.text.toLowerCase();\n const start = t.text.length === 1 ? i + 1 : i;\n for (let k = 0; k < rows.length; k++) {\n const r = rows[(start + k) % rows.length];\n if (r && r.node.label.toLowerCase().startsWith(query)) {\n moveTo(index.get(r.node.id) ?? 0, false);\n break;\n }\n }\n handled = true;\n }\n if (handled) {\n event.preventDefault();\n event.stopPropagation();\n }\n };\n\n if (rows.length === 0 && empty) {\n return (\n <div data-slot=\"tree-view\" data-empty className={cn('text-[13px]', className)} {...props}>\n {empty}\n </div>\n );\n }\n\n return (\n <div\n ref={treeRef}\n role=\"tree\"\n tabIndex={-1}\n data-slot=\"tree-view\"\n data-density={density}\n aria-multiselectable={selectionMode === 'multiple' || undefined}\n className={cn('group/tree flex flex-col py-1 text-[13px] select-none', className)}\n {...props}\n onKeyDown={onKeyDown}\n onFocus={(event) => {\n hadFocus.current = true;\n // Focus landing on the tree itself (a click on empty space) goes to the tab stop.\n if (event.target === event.currentTarget && tabStopId)\n rowRefs.current.get(tabStopId)?.focus();\n props.onFocus?.(event);\n }}\n onBlur={(event) => {\n if (!treeRef.current?.contains(event.relatedTarget)) hadFocus.current = false;\n props.onBlur?.(event);\n }}\n >\n {rows.map((row) => {\n const { node } = row;\n const isSelected = selectedSet.has(node.id);\n return (\n <React.Fragment key={node.id}>\n {/* Keys are handled once on the tree, for whichever row has focus. */}\n {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}\n <div\n ref={(el) => {\n if (el) rowRefs.current.set(node.id, el);\n else rowRefs.current.delete(node.id);\n }}\n role=\"treeitem\"\n data-slot=\"tree-item\"\n data-id={node.id}\n aria-level={row.level}\n aria-setsize={row.setSize}\n aria-posinset={row.posInSet}\n aria-expanded={row.expandable ? row.expanded : undefined}\n aria-selected={isSelected}\n aria-disabled={node.disabled || undefined}\n aria-busy={(row.expanded && node.loading) || undefined}\n aria-label={node.label}\n tabIndex={node.id === tabStopId ? 0 : -1}\n data-state={isSelected ? 'selected' : undefined}\n style={{ paddingInlineStart: `${(row.level - 1) * 12 + 4}px` }}\n className={cn(\n 'relative flex h-7 shrink-0 cursor-default items-center gap-1 rounded-sm pe-2 outline-none group-data-[density=compact]/tree:h-6',\n 'hover:bg-muted/70 data-[state=selected]:bg-brand-soft/70 data-[state=selected]:text-foreground',\n 'focus-visible:shadow-[inset_0_0_0_1px_var(--ring)] aria-disabled:opacity-50',\n node.className,\n )}\n onFocus={() => setFocusedId(node.id)}\n onClick={(event) => {\n if (isTypingTarget(event.target)) return;\n select(\n node.id,\n event.shiftKey ? 'range' : event.metaKey || event.ctrlKey ? 'toggle' : 'replace',\n );\n const plain = !event.shiftKey && !event.metaKey && !event.ctrlKey;\n if (plain && row.expandable && (expandOnClick ?? selectionMode === 'single'))\n setExpanded(row, !row.expanded);\n if (plain && !row.expandable && actionOnClick && !node.disabled)\n onAction?.(node.id);\n }}\n onDoubleClick={(event) => {\n if (!isTypingTarget(event.target) && !node.disabled) onAction?.(node.id);\n }}\n onContextMenu={\n onRowContextMenu ? (event) => onRowContextMenu(node.id, event) : undefined\n }\n >\n {indentGuides &&\n Array.from({ length: row.level - 1 }, (_, level) => (\n <span\n key={level}\n aria-hidden\n data-slot=\"tree-guide\"\n style={{ insetInlineStart: `${level * 12 + 12}px` }}\n className=\"pointer-events-none absolute inset-y-0 w-px bg-border opacity-0 transition-opacity group-hover/tree:opacity-100\"\n />\n ))}\n {/* A pointer shortcut only: Right and Left expand and collapse from the keyboard. */}\n <span\n aria-hidden\n className=\"flex size-4 shrink-0 items-center justify-center text-muted-foreground\"\n onClick={(event) => {\n if (!row.expandable) return;\n event.stopPropagation();\n setExpanded(row, !row.expanded);\n }}\n >\n {row.expandable && (\n <ChevronRight\n className={cn(\n 'size-3.5 motion-safe:transition-transform',\n row.expanded && 'rotate-90',\n )}\n />\n )}\n </span>\n {node.icon && (\n <span aria-hidden className=\"flex shrink-0 text-muted-foreground [&_svg]:size-4\">\n {node.icon}\n </span>\n )}\n <span className=\"min-w-0 flex-1 truncate\">\n {renderLabel ? renderLabel(node) : node.label}\n </span>\n {node.meta && (\n <span className=\"shrink-0 text-xs text-muted-foreground\">{node.meta}</span>\n )}\n </div>\n {row.expanded && node.loading && !node.children?.length && (\n <div\n role=\"presentation\"\n data-slot=\"tree-loading\"\n style={{ paddingInlineStart: `${row.level * 12 + 8}px` }}\n className=\"flex h-7 items-center gap-2 text-muted-foreground group-data-[density=compact]/tree:h-6\"\n >\n <Spinner className=\"size-3.5\" label={`Loading ${node.label}`} />\n <span aria-hidden>Loading…</span>\n </div>\n )}\n </React.Fragment>\n );\n })}\n </div>\n );\n}\n\nexport { TreeView };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tree-view.js","names":[],"sources":["../../src/components/tree-view.tsx"],"sourcesContent":["import ChevronRight from '@burtson-labs/icons/react/chevron-right';\nimport * as React from 'react';\n\nimport { cn, focusRingClasses } from '../lib/utils';\n\nimport { Spinner } from './spinner';\n\nexport interface TreeNode {\n /** Stable identity. Keep it across renders (a path, a database id). */\n id: string;\n /** Plain text: the accessible name and the typeahead key. */\n label: string;\n icon?: React.ReactNode;\n /** Undefined: a leaf, unless `hasChildren` says the children load later. */\n children?: TreeNode[];\n /** Expandable before its children are known; expanding calls onLoadChildren. */\n hasChildren?: boolean;\n /** Children are being fetched; shows a loading row while expanded. */\n loading?: boolean;\n disabled?: boolean;\n /** Trailing detail, such as a git status letter or a count. */\n meta?: React.ReactNode;\n /** Extra classes on the row, for app states such as an ignored file or a highlight. */\n className?: string;\n}\n\nexport interface TreeViewProps extends Omit<\n React.ComponentProps<'div'>,\n 'children' | 'onSelect' | 'aria-label'\n> {\n nodes: TreeNode[];\n /** Required: what the tree holds, e.g. \"Files\". */\n 'aria-label': string;\n expanded: string[];\n onExpandedChange: (ids: string[]) => void;\n selected: string[];\n onSelectedChange: (ids: string[]) => void;\n /** `multiple`: Shift extends a range, Ctrl/⌘ toggles, Space toggles. */\n selectionMode?: 'single' | 'multiple';\n /** Move selection with focus on arrow keys (a file explorer); default off. */\n selectionFollowsFocus?: boolean;\n /** Enter or double-click: open the file, run the item. */\n onAction?: (id: string) => void;\n /**\n * A plain click on a row without children also runs `onAction`, the way a\n * file explorer opens a file on one click. Default off.\n */\n actionOnClick?: boolean;\n /**\n * A plain click on an expandable row toggles it. Defaults to on for single\n * selection; turn it on for a multi-select explorer, where Shift and Ctrl/⌘\n * clicks still only select.\n */\n expandOnClick?: boolean;\n /** Draw a line per ancestor level while the pointer is over the tree. */\n indentGuides?: boolean;\n /** Called once when a node with `hasChildren` and no children expands. */\n onLoadChildren?: (id: string) => void;\n /** Replace the label, e.g. with a rename input. Keys typed in an input never move focus. */\n renderLabel?: (node: TreeNode) => React.ReactNode;\n /** `compact` 24px rows for desktop explorers, `default` 28px. */\n density?: 'compact' | 'default';\n /** Shown when `nodes` is empty. */\n empty?: React.ReactNode;\n /** Right-click or the context-menu key on a row. */\n onRowContextMenu?: (id: string, event: React.MouseEvent | React.KeyboardEvent) => void;\n}\n\ninterface Row {\n node: TreeNode;\n level: number;\n parentId: string | null;\n setSize: number;\n posInSet: number;\n expandable: boolean;\n expanded: boolean;\n}\n\nconst isExpandable = (n: TreeNode) => (n.children?.length ?? 0) > 0 || Boolean(n.hasChildren);\n\n/** The rows a person can see, in order, with the ARIA position data for each. */\nexport function flattenTree(nodes: TreeNode[], expanded: ReadonlySet<string>): Row[] {\n const rows: Row[] = [];\n const walk = (list: TreeNode[], level: number, parentId: string | null) => {\n list.forEach((node, i) => {\n const expandable = isExpandable(node);\n const open = expandable && expanded.has(node.id);\n rows.push({\n node,\n level,\n parentId,\n setSize: list.length,\n posInSet: i + 1,\n expandable,\n expanded: open,\n });\n if (open && node.children) walk(node.children, level + 1, node.id);\n });\n };\n walk(nodes, 1, null);\n return rows;\n}\n\nconst isTypingTarget = (target: EventTarget | null) =>\n target instanceof HTMLElement &&\n (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName));\n\n/**\n * A file-explorer style tree. Everything is controlled: the app owns which\n * nodes are expanded and selected, and file operations are callbacks. Rows\n * render flat (role=\"treeitem\" with aria-level), one tab stop for the tree,\n * arrows to move, Right/Left to open and close, Home/End, typeahead.\n */\nconst TreeView = React.forwardRef<HTMLDivElement, TreeViewProps>(function TreeView(\n {\n nodes,\n expanded,\n onExpandedChange,\n selected,\n onSelectedChange,\n selectionMode = 'single',\n selectionFollowsFocus = false,\n onAction,\n actionOnClick = false,\n expandOnClick,\n indentGuides = false,\n onLoadChildren,\n renderLabel,\n density = 'default',\n empty,\n onRowContextMenu,\n className,\n ...props\n },\n ref,\n) {\n const expandedSet = React.useMemo(() => new Set(expanded), [expanded]);\n const selectedSet = React.useMemo(() => new Set(selected), [selected]);\n const rows = React.useMemo(() => flattenTree(nodes, expandedSet), [nodes, expandedSet]);\n const index = React.useMemo(() => new Map(rows.map((r, i) => [r.node.id, i])), [rows]);\n\n const [focusedId, setFocusedId] = React.useState<string | null>(null);\n const anchorId = React.useRef<string | null>(null);\n const rowRefs = React.useRef(new Map<string, HTMLDivElement>());\n const treeRef = React.useRef<HTMLDivElement>(null);\n const hadFocus = React.useRef(false);\n const lastIds = React.useRef<string[]>([]);\n const lastParents = React.useRef(new Map<string, string | null>());\n const typeahead = React.useRef({ text: '', at: 0 });\n\n // The tab stop: the focused row if it is still visible, else the first\n // selected visible row, else the first row.\n const tabStopId =\n (focusedId && index.has(focusedId) ? focusedId : null) ??\n rows.find((r) => selectedSet.has(r.node.id))?.node.id ??\n rows[0]?.node.id ??\n null;\n\n const focusRow = React.useCallback((id: string) => {\n setFocusedId(id);\n rowRefs.current.get(id)?.focus();\n rowRefs.current.get(id)?.scrollIntoView?.({ block: 'nearest' });\n }, []);\n\n // Focus recovery: when the focused row disappears (deleted, or a parent\n // collapsed), move to its nearest visible ancestor, else the row now at its\n // old position. Only takes DOM focus if the tree had it.\n React.useLayoutEffect(() => {\n if (focusedId && !index.has(focusedId)) {\n let next: string | null = null;\n let parent = lastParents.current.get(focusedId) ?? null;\n while (parent && !index.has(parent)) parent = lastParents.current.get(parent) ?? null;\n const was = Math.max(0, lastIds.current.indexOf(focusedId));\n next = parent ?? rows[Math.min(was, rows.length - 1)]?.node.id ?? null;\n setFocusedId(next);\n const active = document.activeElement;\n if (next && hadFocus.current && (!active || active === document.body))\n rowRefs.current.get(next)?.focus();\n }\n lastIds.current = rows.map((r) => r.node.id);\n lastParents.current = new Map(rows.map((r) => [r.node.id, r.parentId]));\n }, [rows, index, focusedId]);\n\n const setExpanded = (row: Row, open: boolean) => {\n if (!row.expandable || row.expanded === open) return;\n const id = row.node.id;\n onExpandedChange(open ? [...expanded, id] : expanded.filter((e) => e !== id));\n if (open && !row.node.children && row.node.hasChildren) onLoadChildren?.(id);\n };\n\n const selectRange = (toId: string) => {\n const from = index.get(anchorId.current ?? toId) ?? 0;\n const to = index.get(toId) ?? 0;\n const [a, b] = from < to ? [from, to] : [to, from];\n onSelectedChange(\n rows\n .slice(a, b + 1)\n .filter((r) => !r.node.disabled)\n .map((r) => r.node.id),\n );\n };\n\n const select = (id: string, mode: 'replace' | 'toggle' | 'range') => {\n if (rows[index.get(id) ?? -1]?.node.disabled) return;\n if (selectionMode === 'single' || mode === 'replace') {\n anchorId.current = id;\n onSelectedChange([id]);\n } else if (mode === 'toggle') {\n anchorId.current = id;\n onSelectedChange(selectedSet.has(id) ? selected.filter((s) => s !== id) : [...selected, id]);\n } else selectRange(id);\n };\n\n const moveTo = (i: number, extend: boolean) => {\n const row = rows[Math.max(0, Math.min(rows.length - 1, i))];\n if (!row) return;\n focusRow(row.node.id);\n if (selectionMode === 'multiple' && extend) selectRange(row.node.id);\n else if (selectionFollowsFocus && !row.node.disabled) select(row.node.id, 'replace');\n };\n\n const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n props.onKeyDown?.(event);\n if (event.defaultPrevented || isTypingTarget(event.target)) return;\n const id = tabStopId;\n if (!id) return;\n const i = index.get(id) ?? 0;\n const row = rows[i];\n if (!row) return;\n const mod = event.metaKey || event.ctrlKey;\n let handled = true;\n switch (event.key) {\n case 'ArrowDown':\n moveTo(i + 1, event.shiftKey);\n break;\n case 'ArrowUp':\n moveTo(i - 1, event.shiftKey);\n break;\n case 'Home':\n moveTo(0, event.shiftKey);\n break;\n case 'End':\n moveTo(rows.length - 1, event.shiftKey);\n break;\n case 'ArrowRight':\n if (row.expandable && !row.expanded) setExpanded(row, true);\n else if (row.expanded && rows[i + 1]?.parentId === row.node.id)\n moveTo(i + 1, event.shiftKey);\n break;\n case 'ArrowLeft':\n if (row.expanded) setExpanded(row, false);\n else if (row.parentId) moveTo(index.get(row.parentId) ?? i, event.shiftKey);\n break;\n case 'Enter':\n if (row.node.disabled) break;\n select(row.node.id, 'replace');\n onAction?.(row.node.id);\n break;\n case ' ':\n select(\n row.node.id,\n selectionMode === 'multiple' ? (event.shiftKey ? 'range' : 'toggle') : 'replace',\n );\n break;\n case 'a':\n if (mod && selectionMode === 'multiple') {\n onSelectedChange(rows.filter((r) => !r.node.disabled).map((r) => r.node.id));\n } else handled = false;\n break;\n case 'ContextMenu':\n onRowContextMenu?.(row.node.id, event);\n break;\n default:\n handled = false;\n }\n if (!handled && event.key.length === 1 && !mod && !event.altKey) {\n // Typeahead: jump to the next row whose label starts with what was typed.\n const now = event.timeStamp;\n const t = typeahead.current;\n t.text = now - t.at > 600 ? event.key : t.text + event.key;\n t.at = now;\n const query = t.text.toLowerCase();\n const start = t.text.length === 1 ? i + 1 : i;\n for (let k = 0; k < rows.length; k++) {\n const r = rows[(start + k) % rows.length];\n if (r && r.node.label.toLowerCase().startsWith(query)) {\n moveTo(index.get(r.node.id) ?? 0, false);\n break;\n }\n }\n handled = true;\n }\n if (handled) {\n event.preventDefault();\n event.stopPropagation();\n }\n };\n\n if (rows.length === 0 && empty) {\n return (\n <div data-slot=\"tree-view\" data-empty className={cn('text-[13px]', className)} {...props}>\n {empty}\n </div>\n );\n }\n\n return (\n <div\n ref={(node) => {\n treeRef.current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) ref.current = node;\n }}\n role=\"tree\"\n tabIndex={-1}\n data-slot=\"tree-view\"\n data-density={density}\n aria-multiselectable={selectionMode === 'multiple' || undefined}\n className={cn('group/tree flex flex-col py-1 text-[13px] select-none', className)}\n {...props}\n onKeyDown={onKeyDown}\n onFocus={(event) => {\n hadFocus.current = true;\n // Focus landing on the tree itself (a click on empty space) goes to the tab stop.\n if (event.target === event.currentTarget && tabStopId)\n rowRefs.current.get(tabStopId)?.focus();\n props.onFocus?.(event);\n }}\n onBlur={(event) => {\n if (!treeRef.current?.contains(event.relatedTarget)) hadFocus.current = false;\n props.onBlur?.(event);\n }}\n >\n {rows.map((row) => {\n const { node } = row;\n const isSelected = selectedSet.has(node.id);\n return (\n <React.Fragment key={node.id}>\n {/* Keys are handled once on the tree, for whichever row has focus. */}\n {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}\n <div\n ref={(el) => {\n if (el) rowRefs.current.set(node.id, el);\n else rowRefs.current.delete(node.id);\n }}\n role=\"treeitem\"\n data-slot=\"tree-item\"\n data-id={node.id}\n aria-level={row.level}\n aria-setsize={row.setSize}\n aria-posinset={row.posInSet}\n aria-expanded={row.expandable ? row.expanded : undefined}\n aria-selected={isSelected}\n aria-disabled={node.disabled || undefined}\n aria-busy={(row.expanded && node.loading) || undefined}\n aria-label={node.label}\n tabIndex={node.id === tabStopId ? 0 : -1}\n data-state={isSelected ? 'selected' : undefined}\n style={{ paddingInlineStart: `${(row.level - 1) * 12 + 4}px` }}\n className={cn(\n 'relative flex h-7 shrink-0 cursor-default items-center gap-1 rounded-sm pe-2 group-data-[density=compact]/tree:h-6 pointer-coarse:h-11',\n 'hover:bg-muted/70 data-[state=selected]:bg-brand-soft/70 data-[state=selected]:text-foreground',\n focusRingClasses,\n 'focus-visible:outline-offset-[-2px] aria-disabled:opacity-50',\n node.className,\n )}\n onFocus={() => setFocusedId(node.id)}\n onClick={(event) => {\n if (isTypingTarget(event.target)) return;\n select(\n node.id,\n event.shiftKey ? 'range' : event.metaKey || event.ctrlKey ? 'toggle' : 'replace',\n );\n const plain = !event.shiftKey && !event.metaKey && !event.ctrlKey;\n if (plain && row.expandable && (expandOnClick ?? selectionMode === 'single'))\n setExpanded(row, !row.expanded);\n if (plain && !row.expandable && actionOnClick && !node.disabled)\n onAction?.(node.id);\n }}\n onDoubleClick={(event) => {\n if (!isTypingTarget(event.target) && !node.disabled) onAction?.(node.id);\n }}\n onContextMenu={\n onRowContextMenu ? (event) => onRowContextMenu(node.id, event) : undefined\n }\n >\n {indentGuides &&\n Array.from({ length: row.level - 1 }, (_, level) => (\n <span\n key={level}\n aria-hidden\n data-slot=\"tree-guide\"\n style={{ insetInlineStart: `${level * 12 + 12}px` }}\n className=\"pointer-events-none absolute inset-y-0 w-px bg-border opacity-0 transition-opacity group-hover/tree:opacity-100\"\n />\n ))}\n {/* A pointer shortcut only: Right and Left expand and collapse from the keyboard. */}\n <span\n aria-hidden\n className=\"flex size-4 shrink-0 items-center justify-center text-muted-foreground\"\n onClick={(event) => {\n if (!row.expandable) return;\n event.stopPropagation();\n setExpanded(row, !row.expanded);\n }}\n >\n {row.expandable && (\n <ChevronRight\n className={cn(\n 'size-3.5 motion-safe:transition-transform',\n row.expanded && 'rotate-90',\n )}\n />\n )}\n </span>\n {node.icon && (\n <span aria-hidden className=\"flex shrink-0 text-muted-foreground [&_svg]:size-4\">\n {node.icon}\n </span>\n )}\n <span className=\"min-w-0 flex-1 truncate\">\n {renderLabel ? renderLabel(node) : node.label}\n </span>\n {node.meta && (\n <span className=\"shrink-0 text-xs text-muted-foreground\">{node.meta}</span>\n )}\n </div>\n {row.expanded && node.loading && !node.children?.length && (\n <div\n role=\"presentation\"\n data-slot=\"tree-loading\"\n style={{ paddingInlineStart: `${row.level * 12 + 8}px` }}\n className=\"flex h-7 items-center gap-2 text-muted-foreground group-data-[density=compact]/tree:h-6\"\n >\n <Spinner className=\"size-3.5\" label={`Loading ${node.label}`} />\n <span aria-hidden>Loading…</span>\n </div>\n )}\n </React.Fragment>\n );\n })}\n </div>\n );\n});\n\nexport { TreeView };\n"],"mappings":";;;;;;;AA8EA,IAAM,gBAAgB,OAAiB,EAAE,UAAU,UAAU,KAAK,KAAK,QAAQ,EAAE,WAAW;;AAG5F,SAAgB,YAAY,OAAmB,UAAsC;CACnF,MAAM,OAAc,CAAC;CACrB,MAAM,QAAQ,MAAkB,OAAe,aAA4B;EACzE,KAAK,SAAS,MAAM,MAAM;GACxB,MAAM,aAAa,aAAa,IAAI;GACpC,MAAM,OAAO,cAAc,SAAS,IAAI,KAAK,EAAE;GAC/C,KAAK,KAAK;IACR;IACA;IACA;IACA,SAAS,KAAK;IACd,UAAU,IAAI;IACd;IACA,UAAU;GACZ,CAAC;GACD,IAAI,QAAQ,KAAK,UAAU,KAAK,KAAK,UAAU,QAAQ,GAAG,KAAK,EAAE;EACnE,CAAC;CACH;CACA,KAAK,OAAO,GAAG,IAAI;CACnB,OAAO;AACT;AAEA,IAAM,kBAAkB,WACtB,kBAAkB,gBACjB,OAAO,qBAAqB,4BAA4B,KAAK,OAAO,OAAO;;;;;;;AAQ9E,IAAM,WAAW,MAAM,WAA0C,SAAS,SACxE,EACE,OACA,UACA,kBACA,UACA,kBACA,gBAAgB,UAChB,wBAAwB,OACxB,UACA,gBAAgB,OAChB,eACA,eAAe,OACf,gBACA,aACA,UAAU,WACV,OACA,kBACA,WACA,GAAG,SAEL,KACA;CACA,MAAM,cAAc,MAAM,cAAc,IAAI,IAAI,QAAQ,GAAG,CAAC,QAAQ,CAAC;CACrE,MAAM,cAAc,MAAM,cAAc,IAAI,IAAI,QAAQ,GAAG,CAAC,QAAQ,CAAC;CACrE,MAAM,OAAO,MAAM,cAAc,YAAY,OAAO,WAAW,GAAG,CAAC,OAAO,WAAW,CAAC;CACtF,MAAM,QAAQ,MAAM,cAAc,IAAI,IAAI,KAAK,KAAK,GAAG,MAAM,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;CAErF,MAAM,CAAC,WAAW,gBAAgB,MAAM,SAAwB,IAAI;CACpE,MAAM,WAAW,MAAM,OAAsB,IAAI;CACjD,MAAM,UAAU,MAAM,uBAAO,IAAI,IAA4B,CAAC;CAC9D,MAAM,UAAU,MAAM,OAAuB,IAAI;CACjD,MAAM,WAAW,MAAM,OAAO,KAAK;CACnC,MAAM,UAAU,MAAM,OAAiB,CAAC,CAAC;CACzC,MAAM,cAAc,MAAM,uBAAO,IAAI,IAA2B,CAAC;CACjE,MAAM,YAAY,MAAM,OAAO;EAAE,MAAM;EAAI,IAAI;CAAE,CAAC;CAIlD,MAAM,aACH,aAAa,MAAM,IAAI,SAAS,IAAI,YAAY,SACjD,KAAK,MAAM,MAAM,YAAY,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,KAAK,MACnD,KAAK,EAAE,EAAE,KAAK,MACd;CAEF,MAAM,WAAW,MAAM,aAAa,OAAe;EACjD,aAAa,EAAE;EACf,QAAQ,QAAQ,IAAI,EAAE,CAAC,EAAE,MAAM;EAC/B,QAAQ,QAAQ,IAAI,EAAE,CAAC,EAAE,iBAAiB,EAAE,OAAO,UAAU,CAAC;CAChE,GAAG,CAAC,CAAC;CAKL,MAAM,sBAAsB;EAC1B,IAAI,aAAa,CAAC,MAAM,IAAI,SAAS,GAAG;GACtC,IAAI,OAAsB;GAC1B,IAAI,SAAS,YAAY,QAAQ,IAAI,SAAS,KAAK;GACnD,OAAO,UAAU,CAAC,MAAM,IAAI,MAAM,GAAG,SAAS,YAAY,QAAQ,IAAI,MAAM,KAAK;GACjF,MAAM,MAAM,KAAK,IAAI,GAAG,QAAQ,QAAQ,QAAQ,SAAS,CAAC;GAC1D,OAAO,UAAU,KAAK,KAAK,IAAI,KAAK,KAAK,SAAS,CAAC,EAAE,EAAE,KAAK,MAAM;GAClE,aAAa,IAAI;GACjB,MAAM,SAAS,SAAS;GACxB,IAAI,QAAQ,SAAS,YAAY,CAAC,UAAU,WAAW,SAAS,OAC9D,QAAQ,QAAQ,IAAI,IAAI,CAAC,EAAE,MAAM;EACrC;EACA,QAAQ,UAAU,KAAK,KAAK,MAAM,EAAE,KAAK,EAAE;EAC3C,YAAY,UAAU,IAAI,IAAI,KAAK,KAAK,MAAM,CAAC,EAAE,KAAK,IAAI,EAAE,QAAQ,CAAC,CAAC;CACxE,GAAG;EAAC;EAAM;EAAO;CAAS,CAAC;CAE3B,MAAM,eAAe,KAAU,SAAkB;EAC/C,IAAI,CAAC,IAAI,cAAc,IAAI,aAAa,MAAM;EAC9C,MAAM,KAAK,IAAI,KAAK;EACpB,iBAAiB,OAAO,CAAC,GAAG,UAAU,EAAE,IAAI,SAAS,QAAQ,MAAM,MAAM,EAAE,CAAC;EAC5E,IAAI,QAAQ,CAAC,IAAI,KAAK,YAAY,IAAI,KAAK,aAAa,iBAAiB,EAAE;CAC7E;CAEA,MAAM,eAAe,SAAiB;EACpC,MAAM,OAAO,MAAM,IAAI,SAAS,WAAW,IAAI,KAAK;EACpD,MAAM,KAAK,MAAM,IAAI,IAAI,KAAK;EAC9B,MAAM,CAAC,GAAG,KAAK,OAAO,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,IAAI;EACjD,iBACE,KACG,MAAM,GAAG,IAAI,CAAC,CAAC,CACf,QAAQ,MAAM,CAAC,EAAE,KAAK,QAAQ,CAAC,CAC/B,KAAK,MAAM,EAAE,KAAK,EAAE,CACzB;CACF;CAEA,MAAM,UAAU,IAAY,SAAyC;EACnE,IAAI,KAAK,MAAM,IAAI,EAAE,KAAK,GAAG,EAAE,KAAK,UAAU;EAC9C,IAAI,kBAAkB,YAAY,SAAS,WAAW;GACpD,SAAS,UAAU;GACnB,iBAAiB,CAAC,EAAE,CAAC;EACvB,OAAO,IAAI,SAAS,UAAU;GAC5B,SAAS,UAAU;GACnB,iBAAiB,YAAY,IAAI,EAAE,IAAI,SAAS,QAAQ,MAAM,MAAM,EAAE,IAAI,CAAC,GAAG,UAAU,EAAE,CAAC;EAC7F,OAAO,YAAY,EAAE;CACvB;CAEA,MAAM,UAAU,GAAW,WAAoB;EAC7C,MAAM,MAAM,KAAK,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,SAAS,GAAG,CAAC,CAAC;EACzD,IAAI,CAAC,KAAK;EACV,SAAS,IAAI,KAAK,EAAE;EACpB,IAAI,kBAAkB,cAAc,QAAQ,YAAY,IAAI,KAAK,EAAE;OAC9D,IAAI,yBAAyB,CAAC,IAAI,KAAK,UAAU,OAAO,IAAI,KAAK,IAAI,SAAS;CACrF;CAEA,MAAM,aAAa,UAA+C;EAChE,MAAM,YAAY,KAAK;EACvB,IAAI,MAAM,oBAAoB,eAAe,MAAM,MAAM,GAAG;EAC5D,MAAM,KAAK;EACX,IAAI,CAAC,IAAI;EACT,MAAM,IAAI,MAAM,IAAI,EAAE,KAAK;EAC3B,MAAM,MAAM,KAAK;EACjB,IAAI,CAAC,KAAK;EACV,MAAM,MAAM,MAAM,WAAW,MAAM;EACnC,IAAI,UAAU;EACd,QAAQ,MAAM,KAAd;GACE,KAAK;IACH,OAAO,IAAI,GAAG,MAAM,QAAQ;IAC5B;GACF,KAAK;IACH,OAAO,IAAI,GAAG,MAAM,QAAQ;IAC5B;GACF,KAAK;IACH,OAAO,GAAG,MAAM,QAAQ;IACxB;GACF,KAAK;IACH,OAAO,KAAK,SAAS,GAAG,MAAM,QAAQ;IACtC;GACF,KAAK;IACH,IAAI,IAAI,cAAc,CAAC,IAAI,UAAU,YAAY,KAAK,IAAI;SACrD,IAAI,IAAI,YAAY,KAAK,IAAI,EAAE,EAAE,aAAa,IAAI,KAAK,IAC1D,OAAO,IAAI,GAAG,MAAM,QAAQ;IAC9B;GACF,KAAK;IACH,IAAI,IAAI,UAAU,YAAY,KAAK,KAAK;SACnC,IAAI,IAAI,UAAU,OAAO,MAAM,IAAI,IAAI,QAAQ,KAAK,GAAG,MAAM,QAAQ;IAC1E;GACF,KAAK;IACH,IAAI,IAAI,KAAK,UAAU;IACvB,OAAO,IAAI,KAAK,IAAI,SAAS;IAC7B,WAAW,IAAI,KAAK,EAAE;IACtB;GACF,KAAK;IACH,OACE,IAAI,KAAK,IACT,kBAAkB,aAAc,MAAM,WAAW,UAAU,WAAY,SACzE;IACA;GACF,KAAK;IACH,IAAI,OAAO,kBAAkB,YAC3B,iBAAiB,KAAK,QAAQ,MAAM,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,EAAE,CAAC;SACtE,UAAU;IACjB;GACF,KAAK;IACH,mBAAmB,IAAI,KAAK,IAAI,KAAK;IACrC;GACF,SACE,UAAU;EACd;EACA,IAAI,CAAC,WAAW,MAAM,IAAI,WAAW,KAAK,CAAC,OAAO,CAAC,MAAM,QAAQ;GAE/D,MAAM,MAAM,MAAM;GAClB,MAAM,IAAI,UAAU;GACpB,EAAE,OAAO,MAAM,EAAE,KAAK,MAAM,MAAM,MAAM,EAAE,OAAO,MAAM;GACvD,EAAE,KAAK;GACP,MAAM,QAAQ,EAAE,KAAK,YAAY;GACjC,MAAM,QAAQ,EAAE,KAAK,WAAW,IAAI,IAAI,IAAI;GAC5C,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;IACpC,MAAM,IAAI,MAAM,QAAQ,KAAK,KAAK;IAClC,IAAI,KAAK,EAAE,KAAK,MAAM,YAAY,CAAC,CAAC,WAAW,KAAK,GAAG;KACrD,OAAO,MAAM,IAAI,EAAE,KAAK,EAAE,KAAK,GAAG,KAAK;KACvC;IACF;GACF;GACA,UAAU;EACZ;EACA,IAAI,SAAS;GACX,MAAM,eAAe;GACrB,MAAM,gBAAgB;EACxB;CACF;CAEA,IAAI,KAAK,WAAW,KAAK,OACvB,OACE,oBAAC,OAAD;EAAK,aAAU;EAAY,cAAA;EAAW,WAAW,GAAG,eAAe,SAAS;EAAG,GAAI;EAChF,UAAA;CACE,CAAA;CAIT,OACE,oBAAC,OAAD;EACE,MAAM,SAAS;GACb,QAAQ,UAAU;GAClB,IAAI,OAAO,QAAQ,YAAY,IAAI,IAAI;QAClC,IAAI,KAAK,IAAI,UAAU;EAC9B;EACA,MAAK;EACL,UAAU;EACV,aAAU;EACV,gBAAc;EACd,wBAAsB,kBAAkB,cAAc,KAAA;EACtD,WAAW,GAAG,yDAAyD,SAAS;EAChF,GAAI;EACO;EACX,UAAU,UAAU;GAClB,SAAS,UAAU;GAEnB,IAAI,MAAM,WAAW,MAAM,iBAAiB,WAC1C,QAAQ,QAAQ,IAAI,SAAS,CAAC,EAAE,MAAM;GACxC,MAAM,UAAU,KAAK;EACvB;EACA,SAAS,UAAU;GACjB,IAAI,CAAC,QAAQ,SAAS,SAAS,MAAM,aAAa,GAAG,SAAS,UAAU;GACxE,MAAM,SAAS,KAAK;EACtB;EAEC,UAAA,KAAK,KAAK,QAAQ;GACjB,MAAM,EAAE,SAAS;GACjB,MAAM,aAAa,YAAY,IAAI,KAAK,EAAE;GAC1C,OACE,qBAAC,MAAM,UAAP,EAAA,UAAA,CAGE,qBAAC,OAAD;IACE,MAAM,OAAO;KACX,IAAI,IAAI,QAAQ,QAAQ,IAAI,KAAK,IAAI,EAAE;UAClC,QAAQ,QAAQ,OAAO,KAAK,EAAE;IACrC;IACA,MAAK;IACL,aAAU;IACV,WAAS,KAAK;IACd,cAAY,IAAI;IAChB,gBAAc,IAAI;IAClB,iBAAe,IAAI;IACnB,iBAAe,IAAI,aAAa,IAAI,WAAW,KAAA;IAC/C,iBAAe;IACf,iBAAe,KAAK,YAAY,KAAA;IAChC,aAAY,IAAI,YAAY,KAAK,WAAY,KAAA;IAC7C,cAAY,KAAK;IACjB,UAAU,KAAK,OAAO,YAAY,IAAI;IACtC,cAAY,aAAa,aAAa,KAAA;IACtC,OAAO,EAAE,oBAAoB,IAAI,IAAI,QAAQ,KAAK,KAAK,EAAE,IAAI;IAC7D,WAAW,GACT,0IACA,kGACA,kBACA,gEACA,KAAK,SACP;IACA,eAAe,aAAa,KAAK,EAAE;IACnC,UAAU,UAAU;KAClB,IAAI,eAAe,MAAM,MAAM,GAAG;KAClC,OACE,KAAK,IACL,MAAM,WAAW,UAAU,MAAM,WAAW,MAAM,UAAU,WAAW,SACzE;KACA,MAAM,QAAQ,CAAC,MAAM,YAAY,CAAC,MAAM,WAAW,CAAC,MAAM;KAC1D,IAAI,SAAS,IAAI,eAAe,iBAAiB,kBAAkB,WACjE,YAAY,KAAK,CAAC,IAAI,QAAQ;KAChC,IAAI,SAAS,CAAC,IAAI,cAAc,iBAAiB,CAAC,KAAK,UACrD,WAAW,KAAK,EAAE;IACtB;IACA,gBAAgB,UAAU;KACxB,IAAI,CAAC,eAAe,MAAM,MAAM,KAAK,CAAC,KAAK,UAAU,WAAW,KAAK,EAAE;IACzE;IACA,eACE,oBAAoB,UAAU,iBAAiB,KAAK,IAAI,KAAK,IAAI,KAAA;IA3CrE,UAAA;KA8CG,gBACC,MAAM,KAAK,EAAE,QAAQ,IAAI,QAAQ,EAAE,IAAI,GAAG,UACxC,oBAAC,QAAD;MAEE,eAAA;MACA,aAAU;MACV,OAAO,EAAE,kBAAkB,GAAG,QAAQ,KAAK,GAAG,IAAI;MAClD,WAAU;KACX,GALM,KAKN,CACF;KAEH,oBAAC,QAAD;MACE,eAAA;MACA,WAAU;MACV,UAAU,UAAU;OAClB,IAAI,CAAC,IAAI,YAAY;OACrB,MAAM,gBAAgB;OACtB,YAAY,KAAK,CAAC,IAAI,QAAQ;MAChC;MAEC,UAAA,IAAI,cACH,oBAAC,cAAD,EACE,WAAW,GACT,6CACA,IAAI,YAAY,WAClB,EACD,CAAA;KAEC,CAAA;KACL,KAAK,QACJ,oBAAC,QAAD;MAAM,eAAA;MAAY,WAAU;MACzB,UAAA,KAAK;KACF,CAAA;KAER,oBAAC,QAAD;MAAM,WAAU;MACb,UAAA,cAAc,YAAY,IAAI,IAAI,KAAK;KACpC,CAAA;KACL,KAAK,QACJ,oBAAC,QAAD;MAAM,WAAU;MAA0C,UAAA,KAAK;KAAW,CAAA;IAEzE;GACJ,CAAA,GAAA,IAAI,YAAY,KAAK,WAAW,CAAC,KAAK,UAAU,UAC/C,qBAAC,OAAD;IACE,MAAK;IACL,aAAU;IACV,OAAO,EAAE,oBAAoB,GAAG,IAAI,QAAQ,KAAK,EAAE,IAAI;IACvD,WAAU;IAJZ,UAAA,CAME,oBAAC,SAAD;KAAS,WAAU;KAAW,OAAO,WAAW,KAAK;IAAU,CAAA,GAC/D,oBAAC,QAAD;KAAM,eAAA;KAAY,UAAA;IAAc,CAAA,CAC7B;GAEO,CAAA,CAAA,EAAA,GArGK,KAAK,EAqGV;EAEpB,CAAC;CACE,CAAA;AAET,CAAC"}
|
|
@@ -24,5 +24,5 @@ export interface VoiceRecorderProps extends React.ComponentProps<'div'> {
|
|
|
24
24
|
* When the browser can't record or the microphone is blocked it says so and
|
|
25
25
|
* how to fix it.
|
|
26
26
|
*/
|
|
27
|
-
declare
|
|
27
|
+
declare const VoiceRecorder: React.ForwardRefExoticComponent<Omit<VoiceRecorderProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
28
28
|
export { VoiceRecorder };
|
|
@@ -21,7 +21,7 @@ function recordingSupported() {
|
|
|
21
21
|
* When the browser can't record or the microphone is blocked it says so and
|
|
22
22
|
* how to fix it.
|
|
23
23
|
*/
|
|
24
|
-
function VoiceRecorder({ onRecorded, onCancel, onStateChange, maxDurationMs = 3e5, mimeType, label = "Record a voice message", className, ...props }) {
|
|
24
|
+
var VoiceRecorder = React.forwardRef(function VoiceRecorder({ onRecorded, onCancel, onStateChange, maxDurationMs = 3e5, mimeType, label = "Record a voice message", className, ...props }, ref) {
|
|
25
25
|
const [state, setStateRaw] = React.useState("idle");
|
|
26
26
|
const [elapsed, setElapsed] = React.useState(0);
|
|
27
27
|
const [levels, setLevels] = React.useState(() => Array.from({ length: LEVEL_BARS }, () => 0));
|
|
@@ -159,6 +159,7 @@ function VoiceRecorder({ onRecorded, onCancel, onStateChange, maxDurationMs = 3e
|
|
|
159
159
|
if (!(state === "recording" || state === "paused")) {
|
|
160
160
|
const note = state === "denied" ? "Microphone access is blocked. Allow it in your browser’s site settings, then try again." : state === "unsupported" ? "This browser can’t record audio. Try a current Chrome, Edge, Firefox or Safari." : state === "error" ? "The microphone could not start. Check that one is connected and not in use." : null;
|
|
161
161
|
return /* @__PURE__ */ jsxs("div", {
|
|
162
|
+
ref,
|
|
162
163
|
"data-slot": "voice-recorder",
|
|
163
164
|
"data-state": state,
|
|
164
165
|
className: cn("inline-flex items-center gap-2", className),
|
|
@@ -181,6 +182,7 @@ function VoiceRecorder({ onRecorded, onCancel, onStateChange, maxDurationMs = 3e
|
|
|
181
182
|
});
|
|
182
183
|
}
|
|
183
184
|
return /* @__PURE__ */ jsxs("div", {
|
|
185
|
+
ref,
|
|
184
186
|
"data-slot": "voice-recorder",
|
|
185
187
|
"data-state": state,
|
|
186
188
|
role: "group",
|
|
@@ -234,7 +236,7 @@ function VoiceRecorder({ onRecorded, onCancel, onStateChange, maxDurationMs = 3e
|
|
|
234
236
|
})
|
|
235
237
|
]
|
|
236
238
|
});
|
|
237
|
-
}
|
|
239
|
+
});
|
|
238
240
|
//#endregion
|
|
239
241
|
export { VoiceRecorder };
|
|
240
242
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voice-recorder.js","names":[],"sources":["../../src/components/voice-recorder.tsx"],"sourcesContent":["import Mic from '@burtson-labs/icons/react/mic';\nimport Pause from '@burtson-labs/icons/react/pause';\nimport Play from '@burtson-labs/icons/react/play';\nimport Square from '@burtson-labs/icons/react/square';\nimport Trash from '@burtson-labs/icons/react/trash';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { formatDuration } from './audio-player';\nimport { Button } from './button';\n\nexport type VoiceRecorderState =\n 'idle' | 'requesting' | 'recording' | 'paused' | 'denied' | 'unsupported' | 'error';\n\nexport interface VoiceRecording {\n blob: Blob;\n mimeType: string;\n durationMs: number;\n}\n\nexport interface VoiceRecorderProps extends React.ComponentProps<'div'> {\n /** The finished recording, after Stop. */\n onRecorded: (recording: VoiceRecording) => void;\n onCancel?: () => void;\n onStateChange?: (state: VoiceRecorderState) => void;\n /** Stops by itself at this length. Default 5 minutes. */\n maxDurationMs?: number;\n /** Preferred container, used when the browser supports it (e.g. \"audio/webm\"). */\n mimeType?: string;\n /** Accessible name of the record button. */\n label?: string;\n}\n\nconst LEVEL_BARS = 12;\n\nfunction recordingSupported() {\n return (\n typeof navigator !== 'undefined' &&\n typeof navigator.mediaDevices?.getUserMedia === 'function' &&\n typeof (globalThis as { MediaRecorder?: unknown }).MediaRecorder === 'function'\n );\n}\n\n/**\n * A microphone control for a composer: tap to record, then pause, stop to\n * keep the clip or discard it. While recording it shows a live level meter\n * and the elapsed time, in the recording colour (a state, not an error).\n * When the browser can't record or the microphone is blocked it says so and\n * how to fix it.\n */\nfunction VoiceRecorder({\n onRecorded,\n onCancel,\n onStateChange,\n maxDurationMs = 5 * 60 * 1000,\n mimeType,\n label = 'Record a voice message',\n className,\n ...props\n}: VoiceRecorderProps) {\n const [state, setStateRaw] = React.useState<VoiceRecorderState>('idle');\n const [elapsed, setElapsed] = React.useState(0);\n const [levels, setLevels] = React.useState<number[]>(() =>\n Array.from({ length: LEVEL_BARS }, () => 0),\n );\n const recorder = React.useRef<MediaRecorder | null>(null);\n const stream = React.useRef<MediaStream | null>(null);\n const chunks = React.useRef<Blob[]>([]);\n const started = React.useRef(0);\n const pausedTotal = React.useRef(0);\n const pausedAt = React.useRef(0);\n const discard = React.useRef(false);\n const frame = React.useRef(0);\n const audioCtx = React.useRef<AudioContext | null>(null);\n const tick = React.useRef<ReturnType<typeof setInterval> | undefined>(undefined);\n\n const setState = React.useCallback(\n (s: VoiceRecorderState) => {\n setStateRaw(s);\n onStateChange?.(s);\n },\n [onStateChange],\n );\n\n const cleanup = React.useCallback(() => {\n cancelAnimationFrame(frame.current);\n clearInterval(tick.current);\n stream.current?.getTracks().forEach((t) => t.stop());\n stream.current = null;\n void audioCtx.current?.close().catch(() => undefined);\n audioCtx.current = null;\n setLevels(Array.from({ length: LEVEL_BARS }, () => 0));\n }, []);\n\n React.useEffect(() => cleanup, [cleanup]);\n\n const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());\n const recordedMs = () =>\n now() -\n started.current -\n pausedTotal.current -\n (pausedAt.current ? now() - pausedAt.current : 0);\n\n const meter = (s: MediaStream) => {\n const Ctx =\n globalThis.AudioContext ??\n (globalThis as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Ctx) return;\n try {\n const ctx = new Ctx();\n audioCtx.current = ctx;\n const analyser = ctx.createAnalyser();\n analyser.fftSize = 256;\n ctx.createMediaStreamSource(s).connect(analyser);\n const data = new Uint8Array(analyser.frequencyBinCount);\n const draw = () => {\n analyser.getByteFrequencyData(data);\n const per = Math.floor(data.length / LEVEL_BARS) || 1;\n setLevels(\n Array.from({ length: LEVEL_BARS }, (_, i) => {\n let sum = 0;\n for (let j = i * per; j < (i + 1) * per; j++) sum += data[j] ?? 0;\n return Math.min(1, sum / per / 180);\n }),\n );\n frame.current = requestAnimationFrame(draw);\n };\n draw();\n } catch {\n // No meter is fine; recording still works. Drop the half-made context.\n void audioCtx.current?.close().catch(() => undefined);\n audioCtx.current = null;\n }\n };\n\n const start = async () => {\n if (!recordingSupported()) {\n setState('unsupported');\n return;\n }\n setState('requesting');\n let s: MediaStream;\n try {\n s = await navigator.mediaDevices.getUserMedia({ audio: true });\n } catch (err) {\n const name = (err as { name?: string })?.name;\n setState(name === 'NotAllowedError' || name === 'SecurityError' ? 'denied' : 'error');\n return;\n }\n stream.current = s;\n const type = mimeType && MediaRecorder.isTypeSupported?.(mimeType) ? mimeType : undefined;\n let rec: MediaRecorder;\n try {\n rec = new MediaRecorder(s, type ? { mimeType: type } : undefined);\n } catch {\n cleanup();\n setState('error');\n return;\n }\n recorder.current = rec;\n chunks.current = [];\n discard.current = false;\n rec.ondataavailable = (e) => {\n if (e.data && e.data.size > 0) chunks.current.push(e.data);\n };\n rec.onstop = () => {\n const durationMs = Math.round(recordedMs());\n const mime = rec.mimeType || type || 'audio/webm';\n cleanup();\n setElapsed(0);\n setState('idle');\n if (!discard.current)\n onRecorded({ blob: new Blob(chunks.current, { type: mime }), mimeType: mime, durationMs });\n chunks.current = [];\n };\n started.current = now();\n pausedTotal.current = 0;\n pausedAt.current = 0;\n rec.start(250);\n setState('recording');\n meter(s);\n tick.current = setInterval(() => {\n const ms = recordedMs();\n setElapsed(ms);\n if (ms >= maxDurationMs && rec.state !== 'inactive') rec.stop();\n }, 200);\n };\n\n const pause = () => {\n const rec = recorder.current;\n if (!rec) return;\n if (rec.state === 'recording') {\n rec.pause();\n pausedAt.current = now();\n setState('paused');\n } else if (rec.state === 'paused') {\n rec.resume();\n pausedTotal.current += now() - pausedAt.current;\n pausedAt.current = 0;\n setState('recording');\n }\n };\n\n const stop = (keep: boolean) => {\n const rec = recorder.current;\n discard.current = !keep;\n if (rec && rec.state !== 'inactive') rec.stop();\n else {\n cleanup();\n setState('idle');\n }\n if (!keep) onCancel?.();\n };\n\n const live = state === 'recording' || state === 'paused';\n\n if (!live) {\n const note =\n state === 'denied'\n ? 'Microphone access is blocked. Allow it in your browser’s site settings, then try again.'\n : state === 'unsupported'\n ? 'This browser can’t record audio. Try a current Chrome, Edge, Firefox or Safari.'\n : state === 'error'\n ? 'The microphone could not start. Check that one is connected and not in use.'\n : null;\n return (\n <div\n data-slot=\"voice-recorder\"\n data-state={state}\n className={cn('inline-flex items-center gap-2', className)}\n {...props}\n >\n <Button\n type=\"button\"\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={label}\n title={label}\n disabled={state === 'requesting' || state === 'unsupported'}\n onClick={() => void start()}\n className=\"pointer-coarse:size-11\"\n >\n <Mic />\n </Button>\n {note && (\n <p role=\"alert\" className=\"max-w-xs text-xs text-muted-foreground\">\n {note}\n </p>\n )}\n </div>\n );\n }\n\n return (\n <div\n data-slot=\"voice-recorder\"\n data-state={state}\n role=\"group\"\n aria-label=\"Recording\"\n className={cn(\n 'inline-flex items-center gap-2 rounded-full border border-recording/30 bg-recording/8 py-1 pr-1 pl-3',\n className,\n )}\n {...props}\n >\n <span\n aria-hidden\n className={cn(\n 'size-2 shrink-0 rounded-full bg-recording',\n state === 'recording' && 'motion-safe:animate-pulse',\n )}\n />\n <span aria-hidden className=\"flex h-5 items-center gap-[2px]\">\n {levels.map((l, i) => (\n <span\n key={i}\n className=\"w-[3px] rounded-full bg-recording/80 transition-[height] duration-[var(--duration-fast)] ease-[var(--ease-standard)]\"\n style={{ height: `${Math.max(15, Math.round(l * 100))}%` }}\n />\n ))}\n </span>\n <span\n role=\"timer\"\n aria-label={`${state === 'paused' ? 'Paused at' : 'Recording'} ${formatDuration(elapsed / 1000)}`}\n className=\"font-mono text-xs text-foreground tabular-nums\"\n >\n {formatDuration(elapsed / 1000)}\n </span>\n <Button\n type=\"button\"\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={state === 'paused' ? 'Resume recording' : 'Pause recording'}\n onClick={pause}\n className=\"rounded-full pointer-coarse:size-11\"\n >\n {state === 'paused' ? (\n <Play className=\"fill-current\" />\n ) : (\n <Pause className=\"fill-current\" />\n )}\n </Button>\n <Button\n type=\"button\"\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label=\"Discard recording\"\n onClick={() => stop(false)}\n className=\"rounded-full pointer-coarse:size-11\"\n >\n <Trash />\n </Button>\n <Button\n type=\"button\"\n size=\"icon-sm\"\n aria-label=\"Stop and attach recording\"\n onClick={() => stop(true)}\n className=\"rounded-full bg-recording text-recording-foreground hover:bg-recording/90 pointer-coarse:size-11\"\n >\n <Square className=\"fill-current\" />\n </Button>\n </div>\n );\n}\n\nexport { VoiceRecorder };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"voice-recorder.js","names":[],"sources":["../../src/components/voice-recorder.tsx"],"sourcesContent":["import Mic from '@burtson-labs/icons/react/mic';\nimport Pause from '@burtson-labs/icons/react/pause';\nimport Play from '@burtson-labs/icons/react/play';\nimport Square from '@burtson-labs/icons/react/square';\nimport Trash from '@burtson-labs/icons/react/trash';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { formatDuration } from './audio-player';\nimport { Button } from './button';\n\nexport type VoiceRecorderState =\n 'idle' | 'requesting' | 'recording' | 'paused' | 'denied' | 'unsupported' | 'error';\n\nexport interface VoiceRecording {\n blob: Blob;\n mimeType: string;\n durationMs: number;\n}\n\nexport interface VoiceRecorderProps extends React.ComponentProps<'div'> {\n /** The finished recording, after Stop. */\n onRecorded: (recording: VoiceRecording) => void;\n onCancel?: () => void;\n onStateChange?: (state: VoiceRecorderState) => void;\n /** Stops by itself at this length. Default 5 minutes. */\n maxDurationMs?: number;\n /** Preferred container, used when the browser supports it (e.g. \"audio/webm\"). */\n mimeType?: string;\n /** Accessible name of the record button. */\n label?: string;\n}\n\nconst LEVEL_BARS = 12;\n\nfunction recordingSupported() {\n return (\n typeof navigator !== 'undefined' &&\n typeof navigator.mediaDevices?.getUserMedia === 'function' &&\n typeof (globalThis as { MediaRecorder?: unknown }).MediaRecorder === 'function'\n );\n}\n\n/**\n * A microphone control for a composer: tap to record, then pause, stop to\n * keep the clip or discard it. While recording it shows a live level meter\n * and the elapsed time, in the recording colour (a state, not an error).\n * When the browser can't record or the microphone is blocked it says so and\n * how to fix it.\n */\nconst VoiceRecorder = React.forwardRef<HTMLDivElement, VoiceRecorderProps>(function VoiceRecorder(\n {\n onRecorded,\n onCancel,\n onStateChange,\n maxDurationMs = 5 * 60 * 1000,\n mimeType,\n label = 'Record a voice message',\n className,\n ...props\n },\n ref,\n) {\n const [state, setStateRaw] = React.useState<VoiceRecorderState>('idle');\n const [elapsed, setElapsed] = React.useState(0);\n const [levels, setLevels] = React.useState<number[]>(() =>\n Array.from({ length: LEVEL_BARS }, () => 0),\n );\n const recorder = React.useRef<MediaRecorder | null>(null);\n const stream = React.useRef<MediaStream | null>(null);\n const chunks = React.useRef<Blob[]>([]);\n const started = React.useRef(0);\n const pausedTotal = React.useRef(0);\n const pausedAt = React.useRef(0);\n const discard = React.useRef(false);\n const frame = React.useRef(0);\n const audioCtx = React.useRef<AudioContext | null>(null);\n const tick = React.useRef<ReturnType<typeof setInterval> | undefined>(undefined);\n\n const setState = React.useCallback(\n (s: VoiceRecorderState) => {\n setStateRaw(s);\n onStateChange?.(s);\n },\n [onStateChange],\n );\n\n const cleanup = React.useCallback(() => {\n cancelAnimationFrame(frame.current);\n clearInterval(tick.current);\n stream.current?.getTracks().forEach((t) => t.stop());\n stream.current = null;\n void audioCtx.current?.close().catch(() => undefined);\n audioCtx.current = null;\n setLevels(Array.from({ length: LEVEL_BARS }, () => 0));\n }, []);\n\n React.useEffect(() => cleanup, [cleanup]);\n\n const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());\n const recordedMs = () =>\n now() -\n started.current -\n pausedTotal.current -\n (pausedAt.current ? now() - pausedAt.current : 0);\n\n const meter = (s: MediaStream) => {\n const Ctx =\n globalThis.AudioContext ??\n (globalThis as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;\n if (!Ctx) return;\n try {\n const ctx = new Ctx();\n audioCtx.current = ctx;\n const analyser = ctx.createAnalyser();\n analyser.fftSize = 256;\n ctx.createMediaStreamSource(s).connect(analyser);\n const data = new Uint8Array(analyser.frequencyBinCount);\n const draw = () => {\n analyser.getByteFrequencyData(data);\n const per = Math.floor(data.length / LEVEL_BARS) || 1;\n setLevels(\n Array.from({ length: LEVEL_BARS }, (_, i) => {\n let sum = 0;\n for (let j = i * per; j < (i + 1) * per; j++) sum += data[j] ?? 0;\n return Math.min(1, sum / per / 180);\n }),\n );\n frame.current = requestAnimationFrame(draw);\n };\n draw();\n } catch {\n // No meter is fine; recording still works. Drop the half-made context.\n void audioCtx.current?.close().catch(() => undefined);\n audioCtx.current = null;\n }\n };\n\n const start = async () => {\n if (!recordingSupported()) {\n setState('unsupported');\n return;\n }\n setState('requesting');\n let s: MediaStream;\n try {\n s = await navigator.mediaDevices.getUserMedia({ audio: true });\n } catch (err) {\n const name = (err as { name?: string })?.name;\n setState(name === 'NotAllowedError' || name === 'SecurityError' ? 'denied' : 'error');\n return;\n }\n stream.current = s;\n const type = mimeType && MediaRecorder.isTypeSupported?.(mimeType) ? mimeType : undefined;\n let rec: MediaRecorder;\n try {\n rec = new MediaRecorder(s, type ? { mimeType: type } : undefined);\n } catch {\n cleanup();\n setState('error');\n return;\n }\n recorder.current = rec;\n chunks.current = [];\n discard.current = false;\n rec.ondataavailable = (e) => {\n if (e.data && e.data.size > 0) chunks.current.push(e.data);\n };\n rec.onstop = () => {\n const durationMs = Math.round(recordedMs());\n const mime = rec.mimeType || type || 'audio/webm';\n cleanup();\n setElapsed(0);\n setState('idle');\n if (!discard.current)\n onRecorded({ blob: new Blob(chunks.current, { type: mime }), mimeType: mime, durationMs });\n chunks.current = [];\n };\n started.current = now();\n pausedTotal.current = 0;\n pausedAt.current = 0;\n rec.start(250);\n setState('recording');\n meter(s);\n tick.current = setInterval(() => {\n const ms = recordedMs();\n setElapsed(ms);\n if (ms >= maxDurationMs && rec.state !== 'inactive') rec.stop();\n }, 200);\n };\n\n const pause = () => {\n const rec = recorder.current;\n if (!rec) return;\n if (rec.state === 'recording') {\n rec.pause();\n pausedAt.current = now();\n setState('paused');\n } else if (rec.state === 'paused') {\n rec.resume();\n pausedTotal.current += now() - pausedAt.current;\n pausedAt.current = 0;\n setState('recording');\n }\n };\n\n const stop = (keep: boolean) => {\n const rec = recorder.current;\n discard.current = !keep;\n if (rec && rec.state !== 'inactive') rec.stop();\n else {\n cleanup();\n setState('idle');\n }\n if (!keep) onCancel?.();\n };\n\n const live = state === 'recording' || state === 'paused';\n\n if (!live) {\n const note =\n state === 'denied'\n ? 'Microphone access is blocked. Allow it in your browser’s site settings, then try again.'\n : state === 'unsupported'\n ? 'This browser can’t record audio. Try a current Chrome, Edge, Firefox or Safari.'\n : state === 'error'\n ? 'The microphone could not start. Check that one is connected and not in use.'\n : null;\n return (\n <div\n ref={ref}\n data-slot=\"voice-recorder\"\n data-state={state}\n className={cn('inline-flex items-center gap-2', className)}\n {...props}\n >\n <Button\n type=\"button\"\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={label}\n title={label}\n disabled={state === 'requesting' || state === 'unsupported'}\n onClick={() => void start()}\n className=\"pointer-coarse:size-11\"\n >\n <Mic />\n </Button>\n {note && (\n <p role=\"alert\" className=\"max-w-xs text-xs text-muted-foreground\">\n {note}\n </p>\n )}\n </div>\n );\n }\n\n return (\n <div\n ref={ref}\n data-slot=\"voice-recorder\"\n data-state={state}\n role=\"group\"\n aria-label=\"Recording\"\n className={cn(\n 'inline-flex items-center gap-2 rounded-full border border-recording/30 bg-recording/8 py-1 pr-1 pl-3',\n className,\n )}\n {...props}\n >\n <span\n aria-hidden\n className={cn(\n 'size-2 shrink-0 rounded-full bg-recording',\n state === 'recording' && 'motion-safe:animate-pulse',\n )}\n />\n <span aria-hidden className=\"flex h-5 items-center gap-[2px]\">\n {levels.map((l, i) => (\n <span\n key={i}\n className=\"w-[3px] rounded-full bg-recording/80 transition-[height] duration-[var(--duration-fast)] ease-[var(--ease-standard)]\"\n style={{ height: `${Math.max(15, Math.round(l * 100))}%` }}\n />\n ))}\n </span>\n <span\n role=\"timer\"\n aria-label={`${state === 'paused' ? 'Paused at' : 'Recording'} ${formatDuration(elapsed / 1000)}`}\n className=\"font-mono text-xs text-foreground tabular-nums\"\n >\n {formatDuration(elapsed / 1000)}\n </span>\n <Button\n type=\"button\"\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={state === 'paused' ? 'Resume recording' : 'Pause recording'}\n onClick={pause}\n className=\"rounded-full pointer-coarse:size-11\"\n >\n {state === 'paused' ? (\n <Play className=\"fill-current\" />\n ) : (\n <Pause className=\"fill-current\" />\n )}\n </Button>\n <Button\n type=\"button\"\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label=\"Discard recording\"\n onClick={() => stop(false)}\n className=\"rounded-full pointer-coarse:size-11\"\n >\n <Trash />\n </Button>\n <Button\n type=\"button\"\n size=\"icon-sm\"\n aria-label=\"Stop and attach recording\"\n onClick={() => stop(true)}\n className=\"rounded-full bg-recording text-recording-foreground hover:bg-recording/90 pointer-coarse:size-11\"\n >\n <Square className=\"fill-current\" />\n </Button>\n </div>\n );\n});\n\nexport { VoiceRecorder };\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { cn } from './lib/utils';
|
|
1
|
+
export { cn, focusRingClasses, noOutlineClasses, touchTargetClasses, touchTargetRowClasses, } from './lib/utils';
|
|
2
|
+
export { breakpoints, type Breakpoint, useMediaQuery, useNarrowerThan } from './lib/media';
|
|
2
3
|
export { isApplePlatform, modKeyLabel, shortcutLabel } from './lib/platform';
|
|
3
4
|
export * from './tokens';
|
|
4
5
|
export * from './components/accordion';
|
|
@@ -29,7 +30,9 @@ export * from './components/dialog';
|
|
|
29
30
|
export * from './components/dropdown-menu';
|
|
30
31
|
export * from './components/editor-tabs';
|
|
31
32
|
export * from './components/empty-state';
|
|
33
|
+
export * from './components/error-summary';
|
|
32
34
|
export * from './components/field';
|
|
35
|
+
export * from './components/form-actions';
|
|
33
36
|
export * from './components/icon-button';
|
|
34
37
|
export * from './components/input';
|
|
35
38
|
export * from './components/kbd';
|
|
@@ -39,7 +42,9 @@ export * from './components/menubar';
|
|
|
39
42
|
export * from './components/message';
|
|
40
43
|
export * from './components/message-actions';
|
|
41
44
|
export * from './components/mobile-nav';
|
|
45
|
+
export * from './components/native-select';
|
|
42
46
|
export * from './components/navigation-menu';
|
|
47
|
+
export * from './components/number-input';
|
|
43
48
|
export * from './components/onboarding-checklist';
|
|
44
49
|
export * from './components/page-header';
|
|
45
50
|
export * from './components/pagination';
|
|
@@ -72,3 +77,5 @@ export * from './components/tooltip';
|
|
|
72
77
|
export * from './components/tour';
|
|
73
78
|
export * from './components/tree-view';
|
|
74
79
|
export * from './components/voice-recorder';
|
|
80
|
+
export * from './components/avatar-upload';
|
|
81
|
+
export * from './components/agent-run';
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { cn } from "./lib/utils.js";
|
|
1
|
+
import { cn, focusRingClasses, noOutlineClasses, touchTargetClasses, touchTargetRowClasses } from "./lib/utils.js";
|
|
2
|
+
import { breakpoints, useMediaQuery, useNarrowerThan } from "./lib/media.js";
|
|
2
3
|
import { isApplePlatform, modKeyLabel, shortcutLabel } from "./lib/platform.js";
|
|
3
|
-
import tokens, { accentTokens, brand, contrast, dark, duration, easing, elevation, fontMono, fontSans, light, mix, motion, radius, shadow } from "./tokens.js";
|
|
4
|
+
import tokens, { accentTokens, brand, contrast, dark, duration, easing, elevation, fontMono, fontSans, layer, light, mix, motion, radius, shadow } from "./tokens.js";
|
|
4
5
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./components/accordion.js";
|
|
5
6
|
import { Alert, AlertDescription, AlertTitle, alertVariants } from "./components/alert.js";
|
|
6
7
|
import { Button, buttonVariants } from "./components/button.js";
|
|
8
|
+
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, overlayClasses, overlayCloseClasses } from "./components/dialog.js";
|
|
7
9
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "./components/alert-dialog.js";
|
|
8
10
|
import { AppShell, AppShellBody, AppShellContent, AppShellHeader, AppShellMain, AppShellSidebar } from "./components/app-shell.js";
|
|
9
11
|
import { Progress } from "./components/progress.js";
|
|
@@ -14,18 +16,17 @@ import { Avatar, AvatarFallback, AvatarImage } from "./components/avatar.js";
|
|
|
14
16
|
import { Badge, badgeVariants } from "./components/badge.js";
|
|
15
17
|
import { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "./components/breadcrumb.js";
|
|
16
18
|
import { Card, CardAction, CardContent, CardDescription, CardEyebrow, CardFooter, CardHeader, CardTitle, cardVariants } from "./components/card.js";
|
|
17
|
-
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger, menuItemClasses, surfaceClasses } from "./components/popover.js";
|
|
19
|
+
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger, menuItemClasses, menuLabelClasses, surfaceClasses } from "./components/popover.js";
|
|
18
20
|
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger } from "./components/dropdown-menu.js";
|
|
19
|
-
import { Input, fieldClasses, fieldFocusClasses } from "./components/input.js";
|
|
21
|
+
import { Input, SearchInput, fieldClasses, fieldFocusClasses, fieldWidthClasses } from "./components/input.js";
|
|
20
22
|
import { ChatHistory, groupConversations, matchesConversation } from "./components/chat-history.js";
|
|
21
23
|
import { IconButton } from "./components/icon-button.js";
|
|
22
24
|
import { ResizablePanel, ResizablePanelGroup, ResizeHandle, useGroupRef, usePanelRef } from "./components/resizable.js";
|
|
23
25
|
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "./components/sheet.js";
|
|
24
26
|
import { ChatLayout, useFullscreen } from "./components/chat-layout.js";
|
|
25
|
-
import { Checkbox } from "./components/checkbox.js";
|
|
27
|
+
import { Checkbox, CheckboxRow } from "./components/checkbox.js";
|
|
26
28
|
import { CheckboxCard, CheckboxCardGroup } from "./components/checkbox-card.js";
|
|
27
29
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./components/collapsible.js";
|
|
28
|
-
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from "./components/dialog.js";
|
|
29
30
|
import { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut } from "./components/command.js";
|
|
30
31
|
import { Combobox } from "./components/combobox.js";
|
|
31
32
|
import { Composer, Suggestions, acceptsFile } from "./components/composer.js";
|
|
@@ -40,11 +41,13 @@ import { CopyButton } from "./components/copy-button.js";
|
|
|
40
41
|
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue } from "./components/select.js";
|
|
41
42
|
import { DEFAULT_PAGE_SIZES, Pagination, clampPage, pageCountFor, pageWindow, paginate, paginationRange, paginationSummary, usePagination } from "./components/pagination.js";
|
|
42
43
|
import { Skeleton } from "./components/skeleton.js";
|
|
43
|
-
import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "./components/table.js";
|
|
44
|
+
import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, overflowState } from "./components/table.js";
|
|
44
45
|
import { DataTable } from "./components/data-table.js";
|
|
45
46
|
import { EditorTabs } from "./components/editor-tabs.js";
|
|
46
47
|
import { EmptyState, EmptyStateActions, EmptyStateDescription, EmptyStateIcon, EmptyStateTitle } from "./components/empty-state.js";
|
|
47
|
-
import {
|
|
48
|
+
import { ErrorSummary } from "./components/error-summary.js";
|
|
49
|
+
import { Field, FieldDescription, FieldError, FieldGrid, FieldHeader, FieldHint, FieldLabel, FieldSet, useField } from "./components/field.js";
|
|
50
|
+
import { FormActions } from "./components/form-actions.js";
|
|
48
51
|
import { Kbd, KbdGroup } from "./components/kbd.js";
|
|
49
52
|
import { Label } from "./components/label.js";
|
|
50
53
|
import { CodeBlock, Markdown, parseMarkdown } from "./components/markdown.js";
|
|
@@ -52,19 +55,21 @@ import { Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem
|
|
|
52
55
|
import { Message } from "./components/message.js";
|
|
53
56
|
import { MessageActions, MessageAttachments, MessageEditor } from "./components/message-actions.js";
|
|
54
57
|
import { MobileNav } from "./components/mobile-nav.js";
|
|
58
|
+
import { NativeSelect } from "./components/native-select.js";
|
|
55
59
|
import { NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport, navigationMenuTriggerStyle } from "./components/navigation-menu.js";
|
|
60
|
+
import { NumberInput } from "./components/number-input.js";
|
|
56
61
|
import { OnboardingChecklist } from "./components/onboarding-checklist.js";
|
|
57
62
|
import { PageHeader, PageHeaderActions, PageHeaderDescription, PageHeaderEyebrow, PageHeaderMain, PageHeaderTitle } from "./components/page-header.js";
|
|
58
|
-
import { RadioGroup, RadioGroupItem } from "./components/radio-group.js";
|
|
63
|
+
import { RadioCard, RadioGroup, RadioGroupItem } from "./components/radio-group.js";
|
|
59
64
|
import { Reasoning, StreamingIndicator } from "./components/reasoning.js";
|
|
60
65
|
import { ScrollArea, ScrollBar } from "./components/scroll-area.js";
|
|
61
66
|
import { SecretInput } from "./components/secret-input.js";
|
|
62
67
|
import { Separator } from "./components/separator.js";
|
|
63
68
|
import { Slider } from "./components/slider.js";
|
|
64
69
|
import { SourceCitation, SourceList, safeSourceHref } from "./components/source.js";
|
|
65
|
-
import { StatCard } from "./components/stat-card.js";
|
|
70
|
+
import { StatCard, StatStrip } from "./components/stat-card.js";
|
|
66
71
|
import { Steps } from "./components/steps.js";
|
|
67
|
-
import { Switch } from "./components/switch.js";
|
|
72
|
+
import { InlineSwitch, Switch, SwitchList, SwitchRow } from "./components/switch.js";
|
|
68
73
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/tabs.js";
|
|
69
74
|
import { Textarea } from "./components/textarea.js";
|
|
70
75
|
import { ToolApproval, ToolCall } from "./components/tool-call.js";
|
|
@@ -72,4 +77,6 @@ import { Toolbar, ToolbarGroup, ToolbarSeparator, ToolbarSpacer } from "./compon
|
|
|
72
77
|
import { Spotlight, Tour, TourAnchor, findTourTarget, tourTarget, waitForTourTarget } from "./components/tour.js";
|
|
73
78
|
import { TreeView, flattenTree } from "./components/tree-view.js";
|
|
74
79
|
import { VoiceRecorder } from "./components/voice-recorder.js";
|
|
75
|
-
|
|
80
|
+
import { AvatarUpload } from "./components/avatar-upload.js";
|
|
81
|
+
import { AgentRun } from "./components/agent-run.js";
|
|
82
|
+
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, AgentRun, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, AlertTitle, AppShell, AppShellBody, AppShellContent, AppShellHeader, AppShellMain, AppShellSidebar, AttachmentItem, AttachmentTray, AudioPlayer, Avatar, AvatarFallback, AvatarImage, AvatarUpload, Badge, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, Card, CardAction, CardContent, CardDescription, CardEyebrow, CardFooter, CardHeader, CardTitle, ChatHistory, ChatLayout, Checkbox, CheckboxCard, CheckboxCardGroup, CheckboxRow, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, Combobox, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, Composer, ConnectionBanner, ConnectionStatus, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, Conversation, CopyButton, DEFAULT_PAGE_SIZES, DataTable, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EditorTabs, EmptyState, EmptyStateActions, EmptyStateDescription, EmptyStateIcon, EmptyStateTitle, ErrorSummary, Field, FieldDescription, FieldError, FieldGrid, FieldHeader, FieldHint, FieldLabel, FieldSet, FormActions, IconButton, InlineSwitch, Input, Kbd, KbdGroup, Label, Markdown, Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger, Message, MessageActions, MessageAttachments, MessageEditor, MobileNav, NativeSelect, NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport, NumberInput, OnboardingChecklist, PageHeader, PageHeaderActions, PageHeaderDescription, PageHeaderEyebrow, PageHeaderMain, PageHeaderTitle, Pagination, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Progress, RadioCard, RadioGroup, RadioGroupItem, Reasoning, ResizablePanel, ResizablePanelGroup, ResizeHandle, ScrollArea, ScrollBar, SearchInput, SecretInput, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, Skeleton, Slider, SourceCitation, SourceList, Spinner, Spotlight, StatCard, StatStrip, Status, StatusDot, Steps, StreamingIndicator, Suggestions, Switch, SwitchList, SwitchRow, SyncStatus, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, Toast, ToastAction, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport, Toaster, ToolApproval, ToolCall, Toolbar, ToolbarGroup, ToolbarSeparator, ToolbarSpacer, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, Tour, TourAnchor, TreeView, UploadQueue, VoiceMessage, VoiceRecorder, accentTokens, acceptsFile, alertVariants, badgeVariants, brand, breakpoints, buttonVariants, cardVariants, clampPage, cn, computePeaks, contrast, dark, duration, easing, elevation, fieldClasses, fieldFocusClasses, fieldWidthClasses, findTourTarget, flattenTree, focusRingClasses, fontMono, fontSans, formatBytes, formatDuration, groupConversations, isApplePlatform, layer, light, matchesConversation, menuItemClasses, menuLabelClasses, mix, modKeyLabel, motion, navigationMenuTriggerStyle, noOutlineClasses, overflowState, overlayClasses, overlayCloseClasses, pageCountFor, pageWindow, paginate, paginationRange, paginationSummary, parseMarkdown, peaksFromChannelData, radius, safeSourceHref, shadow, shortcutLabel, statusDotVariants, surfaceClasses, toast, toastVariants, tokens, touchTargetClasses, touchTargetRowClasses, tourTarget, useField, useFullscreen, useGroupRef, useMediaQuery, useNarrowerThan, usePagination, usePanelRef, waitForTourTarget };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Tailwind's breakpoints, for layout that has to change in script, not only in CSS. */
|
|
2
|
+
export declare const breakpoints: {
|
|
3
|
+
readonly sm: 640;
|
|
4
|
+
readonly md: 768;
|
|
5
|
+
readonly lg: 1024;
|
|
6
|
+
readonly xl: 1280;
|
|
7
|
+
};
|
|
8
|
+
export type Breakpoint = keyof typeof breakpoints;
|
|
9
|
+
/**
|
|
10
|
+
* True while the media query matches. Server-side and without `matchMedia`
|
|
11
|
+
* (old browsers, jsdom) it is false, so ask the question whose "false" is the
|
|
12
|
+
* desktop layout: `useMediaQuery('(max-width: 767px)')` rather than
|
|
13
|
+
* `(min-width: 768px)`. The value is read with useSyncExternalStore, so it is
|
|
14
|
+
* right on the first client render and never flashes the wrong layout.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useMediaQuery(query: string | null): boolean;
|
|
17
|
+
/** True while the viewport is narrower than the breakpoint; false with no `matchMedia`. */
|
|
18
|
+
export declare function useNarrowerThan(breakpoint: Breakpoint | false): boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
//#region src/lib/media.ts
|
|
3
|
+
/** Tailwind's breakpoints, for layout that has to change in script, not only in CSS. */
|
|
4
|
+
var breakpoints = {
|
|
5
|
+
sm: 640,
|
|
6
|
+
md: 768,
|
|
7
|
+
lg: 1024,
|
|
8
|
+
xl: 1280
|
|
9
|
+
};
|
|
10
|
+
var noop = () => void 0;
|
|
11
|
+
/**
|
|
12
|
+
* True while the media query matches. Server-side and without `matchMedia`
|
|
13
|
+
* (old browsers, jsdom) it is false, so ask the question whose "false" is the
|
|
14
|
+
* desktop layout: `useMediaQuery('(max-width: 767px)')` rather than
|
|
15
|
+
* `(min-width: 768px)`. The value is read with useSyncExternalStore, so it is
|
|
16
|
+
* right on the first client render and never flashes the wrong layout.
|
|
17
|
+
*/
|
|
18
|
+
function useMediaQuery(query) {
|
|
19
|
+
const subscribe = React.useCallback((onChange) => {
|
|
20
|
+
if (!query || typeof matchMedia !== "function") return noop;
|
|
21
|
+
const mq = matchMedia(query);
|
|
22
|
+
mq.addEventListener?.("change", onChange);
|
|
23
|
+
return () => mq.removeEventListener?.("change", onChange);
|
|
24
|
+
}, [query]);
|
|
25
|
+
return React.useSyncExternalStore(subscribe, () => Boolean(query && typeof matchMedia === "function" && matchMedia(query).matches), () => false);
|
|
26
|
+
}
|
|
27
|
+
/** True while the viewport is narrower than the breakpoint; false with no `matchMedia`. */
|
|
28
|
+
function useNarrowerThan(breakpoint) {
|
|
29
|
+
return useMediaQuery(breakpoint ? `(max-width: ${breakpoints[breakpoint] - .02}px)` : null);
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { breakpoints, useMediaQuery, useNarrowerThan };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=media.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media.js","names":[],"sources":["../../src/lib/media.ts"],"sourcesContent":["import * as React from 'react';\n\n/** Tailwind's breakpoints, for layout that has to change in script, not only in CSS. */\nexport const breakpoints = { sm: 640, md: 768, lg: 1024, xl: 1280 } as const;\n\nexport type Breakpoint = keyof typeof breakpoints;\n\nconst noop = () => undefined;\n\n/**\n * True while the media query matches. Server-side and without `matchMedia`\n * (old browsers, jsdom) it is false, so ask the question whose \"false\" is the\n * desktop layout: `useMediaQuery('(max-width: 767px)')` rather than\n * `(min-width: 768px)`. The value is read with useSyncExternalStore, so it is\n * right on the first client render and never flashes the wrong layout.\n */\nexport function useMediaQuery(query: string | null): boolean {\n const subscribe = React.useCallback(\n (onChange: () => void) => {\n if (!query || typeof matchMedia !== 'function') return noop;\n const mq = matchMedia(query);\n mq.addEventListener?.('change', onChange);\n return () => mq.removeEventListener?.('change', onChange);\n },\n [query],\n );\n return React.useSyncExternalStore(\n subscribe,\n () => Boolean(query && typeof matchMedia === 'function' && matchMedia(query).matches),\n () => false,\n );\n}\n\n/** True while the viewport is narrower than the breakpoint; false with no `matchMedia`. */\nexport function useNarrowerThan(breakpoint: Breakpoint | false): boolean {\n return useMediaQuery(breakpoint ? `(max-width: ${breakpoints[breakpoint] - 0.02}px)` : null);\n}\n"],"mappings":";;;AAGA,IAAa,cAAc;CAAE,IAAI;CAAK,IAAI;CAAK,IAAI;CAAM,IAAI;AAAK;AAIlE,IAAM,aAAa,KAAA;;;;;;;;AASnB,SAAgB,cAAc,OAA+B;CAC3D,MAAM,YAAY,MAAM,aACrB,aAAyB;EACxB,IAAI,CAAC,SAAS,OAAO,eAAe,YAAY,OAAO;EACvD,MAAM,KAAK,WAAW,KAAK;EAC3B,GAAG,mBAAmB,UAAU,QAAQ;EACxC,aAAa,GAAG,sBAAsB,UAAU,QAAQ;CAC1D,GACA,CAAC,KAAK,CACR;CACA,OAAO,MAAM,qBACX,iBACM,QAAQ,SAAS,OAAO,eAAe,cAAc,WAAW,KAAK,CAAC,CAAC,OAAO,SAC9E,KACR;AACF;;AAGA,SAAgB,gBAAgB,YAAyC;CACvE,OAAO,cAAc,aAAa,eAAe,YAAY,cAAc,IAAK,OAAO,IAAI;AAC7F"}
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
1
|
import { type ClassValue } from 'clsx';
|
|
2
2
|
/** Joins class names and lets later Tailwind utilities override earlier ones. */
|
|
3
3
|
export declare function cn(...inputs: ClassValue[]): string;
|
|
4
|
+
/**
|
|
5
|
+
* Keyboard focus for buttons, toggles, tabs, links and every other control
|
|
6
|
+
* that is not a text field: a 2px outline in the ring colour, 2px outside the
|
|
7
|
+
* box, so it follows the control's radius and reads at 3:1 on any surface.
|
|
8
|
+
* (Fields use `fieldFocusClasses`: the border and a 1px inset ring. Menu and
|
|
9
|
+
* list rows use a fill.) The same outline theme.css draws for any `[data-slot]`
|
|
10
|
+
* by default; spelling it out here keeps it when a host page sets
|
|
11
|
+
* `outline: none` on buttons. (`outline-solid` matters: Tailwind's
|
|
12
|
+
* `outline-none` sets the shared outline-style variable to none, and
|
|
13
|
+
* `outline-2` alone would read it back.)
|
|
14
|
+
*/
|
|
15
|
+
export declare const focusRingClasses = "outline-none focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring";
|
|
16
|
+
/**
|
|
17
|
+
* Suppresses the outline outright (a control that draws focus another way, or
|
|
18
|
+
* whose box is not the focus target), with `!important` so a host page's
|
|
19
|
+
* unlayered `:focus-visible { outline }` cannot draw a second box. In forced-
|
|
20
|
+
* colors mode (Windows High Contrast) no ring or border colour survives, so
|
|
21
|
+
* keyboard focus gets a system-colour outline there and nothing at rest.
|
|
22
|
+
*/
|
|
23
|
+
export declare const noOutlineClasses = "outline-none! forced-colors:focus-visible:[outline:2px_solid_Highlight]! forced-colors:focus-visible:outline-offset-2";
|
|
24
|
+
/**
|
|
25
|
+
* A 44px hit area on touch screens for a small control (a 16px checkbox, a
|
|
26
|
+
* 20px switch), drawn as an invisible square centred on it. The layout does
|
|
27
|
+
* not change; the control just becomes easier to tap. The control needs
|
|
28
|
+
* `relative` (included).
|
|
29
|
+
*/
|
|
30
|
+
export declare const touchTargetClasses = "relative pointer-coarse:before:absolute pointer-coarse:before:top-1/2 pointer-coarse:before:left-1/2 pointer-coarse:before:size-11 pointer-coarse:before:-translate-1/2 pointer-coarse:before:content-['']";
|
|
31
|
+
/**
|
|
32
|
+
* The same for a control that already spans its row (a tab, a menubar
|
|
33
|
+
* trigger): the hit area keeps the control's width and grows to 44px tall.
|
|
34
|
+
*/
|
|
35
|
+
export declare const touchTargetRowClasses = "relative pointer-coarse:before:absolute pointer-coarse:before:inset-x-0 pointer-coarse:before:top-1/2 pointer-coarse:before:h-11 pointer-coarse:before:-translate-y-1/2 pointer-coarse:before:content-['']";
|
package/dist/lib/utils.js
CHANGED
|
@@ -5,7 +5,39 @@ import { twMerge } from "tailwind-merge";
|
|
|
5
5
|
function cn(...inputs) {
|
|
6
6
|
return twMerge(clsx(inputs));
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Keyboard focus for buttons, toggles, tabs, links and every other control
|
|
10
|
+
* that is not a text field: a 2px outline in the ring colour, 2px outside the
|
|
11
|
+
* box, so it follows the control's radius and reads at 3:1 on any surface.
|
|
12
|
+
* (Fields use `fieldFocusClasses`: the border and a 1px inset ring. Menu and
|
|
13
|
+
* list rows use a fill.) The same outline theme.css draws for any `[data-slot]`
|
|
14
|
+
* by default; spelling it out here keeps it when a host page sets
|
|
15
|
+
* `outline: none` on buttons. (`outline-solid` matters: Tailwind's
|
|
16
|
+
* `outline-none` sets the shared outline-style variable to none, and
|
|
17
|
+
* `outline-2` alone would read it back.)
|
|
18
|
+
*/
|
|
19
|
+
var focusRingClasses = "outline-none focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring";
|
|
20
|
+
/**
|
|
21
|
+
* Suppresses the outline outright (a control that draws focus another way, or
|
|
22
|
+
* whose box is not the focus target), with `!important` so a host page's
|
|
23
|
+
* unlayered `:focus-visible { outline }` cannot draw a second box. In forced-
|
|
24
|
+
* colors mode (Windows High Contrast) no ring or border colour survives, so
|
|
25
|
+
* keyboard focus gets a system-colour outline there and nothing at rest.
|
|
26
|
+
*/
|
|
27
|
+
var noOutlineClasses = "outline-none! forced-colors:focus-visible:[outline:2px_solid_Highlight]! forced-colors:focus-visible:outline-offset-2";
|
|
28
|
+
/**
|
|
29
|
+
* A 44px hit area on touch screens for a small control (a 16px checkbox, a
|
|
30
|
+
* 20px switch), drawn as an invisible square centred on it. The layout does
|
|
31
|
+
* not change; the control just becomes easier to tap. The control needs
|
|
32
|
+
* `relative` (included).
|
|
33
|
+
*/
|
|
34
|
+
var touchTargetClasses = "relative pointer-coarse:before:absolute pointer-coarse:before:top-1/2 pointer-coarse:before:left-1/2 pointer-coarse:before:size-11 pointer-coarse:before:-translate-1/2 pointer-coarse:before:content-['']";
|
|
35
|
+
/**
|
|
36
|
+
* The same for a control that already spans its row (a tab, a menubar
|
|
37
|
+
* trigger): the hit area keeps the control's width and grows to 44px tall.
|
|
38
|
+
*/
|
|
39
|
+
var touchTargetRowClasses = "relative pointer-coarse:before:absolute pointer-coarse:before:inset-x-0 pointer-coarse:before:top-1/2 pointer-coarse:before:h-11 pointer-coarse:before:-translate-y-1/2 pointer-coarse:before:content-['']";
|
|
8
40
|
//#endregion
|
|
9
|
-
export { cn };
|
|
41
|
+
export { cn, focusRingClasses, noOutlineClasses, touchTargetClasses, touchTargetRowClasses };
|
|
10
42
|
|
|
11
43
|
//# sourceMappingURL=utils.js.map
|
package/dist/lib/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","names":[],"sources":["../../src/lib/utils.ts"],"sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/** Joins class names and lets later Tailwind utilities override earlier ones. */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n"],"mappings":";;;;AAIA,SAAgB,GAAG,GAAG,QAA8B;CAClD,OAAO,QAAQ,KAAK,MAAM,CAAC;AAC7B"}
|
|
1
|
+
{"version":3,"file":"utils.js","names":[],"sources":["../../src/lib/utils.ts"],"sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/** Joins class names and lets later Tailwind utilities override earlier ones. */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n\n/**\n * Keyboard focus for buttons, toggles, tabs, links and every other control\n * that is not a text field: a 2px outline in the ring colour, 2px outside the\n * box, so it follows the control's radius and reads at 3:1 on any surface.\n * (Fields use `fieldFocusClasses`: the border and a 1px inset ring. Menu and\n * list rows use a fill.) The same outline theme.css draws for any `[data-slot]`\n * by default; spelling it out here keeps it when a host page sets\n * `outline: none` on buttons. (`outline-solid` matters: Tailwind's\n * `outline-none` sets the shared outline-style variable to none, and\n * `outline-2` alone would read it back.)\n */\nexport const focusRingClasses =\n 'outline-none focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring';\n\n/**\n * Suppresses the outline outright (a control that draws focus another way, or\n * whose box is not the focus target), with `!important` so a host page's\n * unlayered `:focus-visible { outline }` cannot draw a second box. In forced-\n * colors mode (Windows High Contrast) no ring or border colour survives, so\n * keyboard focus gets a system-colour outline there and nothing at rest.\n */\nexport const noOutlineClasses =\n 'outline-none! forced-colors:focus-visible:[outline:2px_solid_Highlight]! forced-colors:focus-visible:outline-offset-2';\n\n/**\n * A 44px hit area on touch screens for a small control (a 16px checkbox, a\n * 20px switch), drawn as an invisible square centred on it. The layout does\n * not change; the control just becomes easier to tap. The control needs\n * `relative` (included).\n */\nexport const touchTargetClasses =\n \"relative pointer-coarse:before:absolute pointer-coarse:before:top-1/2 pointer-coarse:before:left-1/2 pointer-coarse:before:size-11 pointer-coarse:before:-translate-1/2 pointer-coarse:before:content-['']\";\n\n/**\n * The same for a control that already spans its row (a tab, a menubar\n * trigger): the hit area keeps the control's width and grows to 44px tall.\n */\nexport const touchTargetRowClasses =\n \"relative pointer-coarse:before:absolute pointer-coarse:before:inset-x-0 pointer-coarse:before:top-1/2 pointer-coarse:before:h-11 pointer-coarse:before:-translate-y-1/2 pointer-coarse:before:content-['']\";\n"],"mappings":";;;;AAIA,SAAgB,GAAG,GAAG,QAA8B;CAClD,OAAO,QAAQ,KAAK,MAAM,CAAC;AAC7B;;;;;;;;;;;;AAaA,IAAa,mBACX;;;;;;;;AASF,IAAa,mBACX;;;;;;;AAQF,IAAa,qBACX;;;;;AAMF,IAAa,wBACX"}
|