@flanksource/clicky-ui 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/components/Combobox.cjs +89 -49
  2. package/dist/components/Combobox.cjs.map +1 -1
  3. package/dist/components/Combobox.d.ts +3 -0
  4. package/dist/components/Combobox.d.ts.map +1 -1
  5. package/dist/components/Combobox.js +91 -51
  6. package/dist/components/Combobox.js.map +1 -1
  7. package/dist/components/FilterBar.cjs +42 -44
  8. package/dist/components/FilterBar.cjs.map +1 -1
  9. package/dist/components/FilterBar.d.ts +19 -0
  10. package/dist/components/FilterBar.d.ts.map +1 -1
  11. package/dist/components/FilterBar.js +43 -45
  12. package/dist/components/FilterBar.js.map +1 -1
  13. package/dist/components/JsonSchemaForm.cjs +48 -0
  14. package/dist/components/JsonSchemaForm.cjs.map +1 -0
  15. package/dist/components/JsonSchemaForm.d.ts +3 -0
  16. package/dist/components/JsonSchemaForm.d.ts.map +1 -0
  17. package/dist/components/JsonSchemaForm.js +48 -0
  18. package/dist/components/JsonSchemaForm.js.map +1 -0
  19. package/dist/components/json-schema-form-array.cjs +169 -0
  20. package/dist/components/json-schema-form-array.cjs.map +1 -0
  21. package/dist/components/json-schema-form-array.d.ts +8 -0
  22. package/dist/components/json-schema-form-array.d.ts.map +1 -0
  23. package/dist/components/json-schema-form-array.js +169 -0
  24. package/dist/components/json-schema-form-array.js.map +1 -0
  25. package/dist/components/json-schema-form-fields.cjs +374 -0
  26. package/dist/components/json-schema-form-fields.cjs.map +1 -0
  27. package/dist/components/json-schema-form-fields.d.ts +19 -0
  28. package/dist/components/json-schema-form-fields.d.ts.map +1 -0
  29. package/dist/components/json-schema-form-fields.js +374 -0
  30. package/dist/components/json-schema-form-fields.js.map +1 -0
  31. package/dist/components/json-schema-form-render.cjs +154 -0
  32. package/dist/components/json-schema-form-render.cjs.map +1 -0
  33. package/dist/components/json-schema-form-render.d.ts +40 -0
  34. package/dist/components/json-schema-form-render.d.ts.map +1 -0
  35. package/dist/components/json-schema-form-render.js +154 -0
  36. package/dist/components/json-schema-form-render.js.map +1 -0
  37. package/dist/components/json-schema-form-resolve.cjs +121 -0
  38. package/dist/components/json-schema-form-resolve.cjs.map +1 -0
  39. package/dist/components/json-schema-form-resolve.d.ts +19 -0
  40. package/dist/components/json-schema-form-resolve.d.ts.map +1 -0
  41. package/dist/components/json-schema-form-resolve.js +121 -0
  42. package/dist/components/json-schema-form-resolve.js.map +1 -0
  43. package/dist/components/json-schema-form-types.d.ts +124 -0
  44. package/dist/components/json-schema-form-types.d.ts.map +1 -0
  45. package/dist/components.cjs +7 -0
  46. package/dist/components.cjs.map +1 -1
  47. package/dist/components.d.ts +3 -0
  48. package/dist/components.d.ts.map +1 -1
  49. package/dist/components.js +8 -1
  50. package/dist/components.js.map +1 -1
  51. package/dist/data/Clicky.cjs +1 -1
  52. package/dist/data/Clicky.cjs.map +1 -1
  53. package/dist/data/Clicky.js +1 -1
  54. package/dist/data/Clicky.js.map +1 -1
  55. package/dist/data/DataTable.cjs +16 -15
  56. package/dist/data/DataTable.cjs.map +1 -1
  57. package/dist/data/DataTable.d.ts +2 -1
  58. package/dist/data/DataTable.d.ts.map +1 -1
  59. package/dist/data/DataTable.js +16 -15
  60. package/dist/data/DataTable.js.map +1 -1
  61. package/dist/data/Icon.cjs +11 -0
  62. package/dist/data/Icon.cjs.map +1 -1
  63. package/dist/data/Icon.d.ts +12 -1
  64. package/dist/data/Icon.d.ts.map +1 -1
  65. package/dist/data/Icon.js +12 -1
  66. package/dist/data/Icon.js.map +1 -1
  67. package/dist/data/LogDetails.cjs +277 -0
  68. package/dist/data/LogDetails.cjs.map +1 -0
  69. package/dist/data/LogDetails.d.ts +5 -0
  70. package/dist/data/LogDetails.d.ts.map +1 -0
  71. package/dist/data/LogDetails.js +277 -0
  72. package/dist/data/LogDetails.js.map +1 -0
  73. package/dist/data/LogsTable.cjs +6 -202
  74. package/dist/data/LogsTable.cjs.map +1 -1
  75. package/dist/data/LogsTable.d.ts.map +1 -1
  76. package/dist/data/LogsTable.js +4 -200
  77. package/dist/data/LogsTable.js.map +1 -1
  78. package/dist/data/Properties.cjs +2 -2
  79. package/dist/data/Properties.cjs.map +1 -1
  80. package/dist/data/Properties.js +2 -2
  81. package/dist/data/Properties.js.map +1 -1
  82. package/dist/data/cells/TagList.cjs +19 -3
  83. package/dist/data/cells/TagList.cjs.map +1 -1
  84. package/dist/data/cells/TagList.d.ts +11 -1
  85. package/dist/data/cells/TagList.d.ts.map +1 -1
  86. package/dist/data/cells/TagList.js +19 -3
  87. package/dist/data/cells/TagList.js.map +1 -1
  88. package/dist/data/cells/Timestamp.cjs +17 -2
  89. package/dist/data/cells/Timestamp.cjs.map +1 -1
  90. package/dist/data/cells/Timestamp.d.ts +2 -0
  91. package/dist/data/cells/Timestamp.d.ts.map +1 -1
  92. package/dist/data/cells/Timestamp.js +17 -2
  93. package/dist/data/cells/Timestamp.js.map +1 -1
  94. package/dist/data/log-utils.cjs +7 -0
  95. package/dist/data/log-utils.cjs.map +1 -0
  96. package/dist/data/log-utils.d.ts +2 -0
  97. package/dist/data/log-utils.d.ts.map +1 -0
  98. package/dist/data/log-utils.js +7 -0
  99. package/dist/data/log-utils.js.map +1 -0
  100. package/dist/data.cjs +3 -0
  101. package/dist/data.cjs.map +1 -1
  102. package/dist/data.d.ts +2 -2
  103. package/dist/data.d.ts.map +1 -1
  104. package/dist/data.js +5 -2
  105. package/dist/index.cjs +13 -0
  106. package/dist/index.cjs.map +1 -1
  107. package/dist/index.js +15 -2
  108. package/dist/index.js.map +1 -1
  109. package/dist/rpc/EntityExplorerApp.cjs +3 -2
  110. package/dist/rpc/EntityExplorerApp.cjs.map +1 -1
  111. package/dist/rpc/EntityExplorerApp.js +3 -2
  112. package/dist/rpc/EntityExplorerApp.js.map +1 -1
  113. package/dist/rpc/OperationActionDialog.cjs +1 -1
  114. package/dist/rpc/OperationActionDialog.cjs.map +1 -1
  115. package/dist/rpc/OperationActionDialog.js +1 -1
  116. package/dist/rpc/OperationActionDialog.js.map +1 -1
  117. package/dist/rpc/OperationCommandPage.cjs +21 -14
  118. package/dist/rpc/OperationCommandPage.cjs.map +1 -1
  119. package/dist/rpc/OperationCommandPage.d.ts.map +1 -1
  120. package/dist/rpc/OperationCommandPage.js +21 -14
  121. package/dist/rpc/OperationCommandPage.js.map +1 -1
  122. package/dist/rpc/apiClient.cjs +316 -0
  123. package/dist/rpc/apiClient.cjs.map +1 -0
  124. package/dist/rpc/apiClient.d.ts +44 -0
  125. package/dist/rpc/apiClient.d.ts.map +1 -0
  126. package/dist/rpc/apiClient.js +316 -0
  127. package/dist/rpc/apiClient.js.map +1 -0
  128. package/dist/rpc/useOperations.cjs.map +1 -1
  129. package/dist/rpc/useOperations.d.ts +2 -1
  130. package/dist/rpc/useOperations.d.ts.map +1 -1
  131. package/dist/rpc/useOperations.js.map +1 -1
  132. package/dist/rpc.cjs +3 -0
  133. package/dist/rpc.cjs.map +1 -1
  134. package/dist/rpc.d.ts +1 -0
  135. package/dist/rpc.d.ts.map +1 -1
  136. package/dist/rpc.js +3 -0
  137. package/dist/rpc.js.map +1 -1
  138. package/dist/styles.css +1 -1
  139. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"Combobox.js","sources":["../../src/components/Combobox.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { UiChevronDown, UiClose, UiCheck } from \"../icons\";\n\nexport type ComboboxOption = {\n value: string;\n label: string;\n disabled?: boolean;\n};\n\ntype ComboboxBaseProps = {\n /** Available options, rendered in the order provided. */\n options: ComboboxOption[];\n /** Ghost text when no value is set. */\n placeholder?: string;\n /** Optional label rendered inline inside the control, before the input. */\n label?: ReactNode;\n /** Disables the input. */\n disabled?: boolean;\n /** When true, the value cannot be cleared (the clear button is hidden). */\n required?: boolean;\n /**\n * When false, the value is restricted to the provided options — typed text\n * that does not match an option is discarded instead of committed. Defaults\n * to true (freeform entry allowed).\n */\n allowCustomValue?: boolean;\n /** HTML id for the input element. */\n id?: string;\n /** Classes applied to the root wrapper. */\n className?: string;\n /** Shows a loading indicator when options are being fetched. */\n loading?: boolean;\n /**\n * Optional async search invoked (debounced ~250ms) as the user types. The\n * consumer fetches and feeds matching options back via the `options` prop\n * (setting `loading` while in flight). When set, the component renders the\n * provided `options` as-is instead of filtering them client-side — the\n * server has already filtered. When absent, typing filters `options`\n * client-side as before.\n */\n onSearch?: (query: string) => void;\n /** Called when a key is pressed in the input. */\n onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void;\n};\n\nexport type ComboboxSingleProps = ComboboxBaseProps & {\n multiple?: false;\n /** Controlled selected value. */\n value: string;\n /** Called when the selected value changes (from list or freeform input). */\n onChange: (value: string) => void;\n};\n\nexport type ComboboxMultiProps = ComboboxBaseProps & {\n multiple: true;\n /** Controlled selected values. */\n value: string[];\n /** Called with the complete next value array after each toggle. */\n onChange: (value: string[]) => void;\n};\n\nexport type ComboboxProps = ComboboxSingleProps | ComboboxMultiProps;\n\nexport function Combobox(props: ComboboxProps) {\n const {\n options,\n placeholder,\n label,\n disabled,\n required,\n allowCustomValue = true,\n id,\n className,\n loading,\n onSearch,\n onKeyDown: onKeyDownProp,\n } = props;\n const multiple = props.multiple === true;\n const selectedValues = useMemo<string[]>(\n () => (multiple ? props.value : props.value ? [props.value] : []),\n [multiple, props.value],\n );\n\n const rootRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n const [open, setOpen] = useState(false);\n // `query` is the type-ahead filter text, kept separate from the committed\n // value. It is empty unless the user is actively typing, so opening the\n // dropdown shows every option rather than only the selected one.\n const [query, setQuery] = useState(\"\");\n const [highlighted, setHighlighted] = useState(-1);\n\n const isSelected = (optValue: string) => selectedValues.includes(optValue);\n\n // Closed-state input text. Single: the selected option's label (or its raw\n // value as a fallback). Multi: a summary mirroring MultiSelect.\n const closedLabel = useMemo(() => {\n if (multiple) {\n const labels = options\n .filter((o) => selectedValues.includes(o.value))\n .map((o) => o.label);\n if (labels.length === 0) return \"\";\n if (labels.length <= 2) return labels.join(\", \");\n return `${labels.length} selected`;\n }\n const single = selectedValues[0] ?? \"\";\n return options.find((o) => o.value === single)?.label ?? single;\n }, [multiple, options, selectedValues]);\n\n // While open the input mirrors the query; while closed it shows the\n // committed selection's label/summary.\n const displayValue = open ? query : closedLabel;\n\n const filtered = useMemo(() => {\n // With onSearch the server already filtered. The matches replace the list,\n // but any already-selected value not in the matches is pinned so the user's\n // selection stays visible and toggleable. Selected values carry no label\n // object here, so synthesize {value, label: value} (the typeahead's value is\n // its own human label).\n if (onSearch) return withSelectedOptions(options, selectedValues);\n const q = query.toLowerCase().trim();\n if (!q) return options;\n return options.filter(\n (o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q),\n );\n }, [onSearch, options, query, selectedValues]);\n\n // Debounced server-side search: fires onSearch ~250ms after the query settles\n // while the menu is open. The empty-query call lets the consumer reset its\n // result set. Only active when onSearch is provided.\n useEffect(() => {\n if (!onSearch || !open) return;\n const handle = setTimeout(() => onSearch(query.trim()), 250);\n return () => clearTimeout(handle);\n }, [onSearch, open, query]);\n\n useEffect(() => {\n setHighlighted(-1);\n }, [filtered.length]);\n\n useEffect(() => {\n if (!open) return;\n const onPointerDown = (e: MouseEvent) => {\n if (!rootRef.current?.contains(e.target as Node)) {\n commitAndClose();\n }\n };\n document.addEventListener(\"mousedown\", onPointerDown);\n return () => document.removeEventListener(\"mousedown\", onPointerDown);\n });\n\n function emit(next: string[]) {\n if (multiple) {\n props.onChange(next);\n } else {\n props.onChange(next[0] ?? \"\");\n }\n }\n\n function openMenu() {\n setQuery(\"\");\n setHighlighted(-1);\n setOpen(true);\n }\n\n function commitAndClose() {\n // Freeform commit (single, allowCustomValue only): a non-empty query that\n // doesn't match an option becomes the value. An empty query keeps the\n // existing selection. Multi and strict modes never commit freeform text.\n const trimmed = query.trim();\n if (!multiple && allowCustomValue && trimmed && trimmed !== selectedValues[0]) {\n emit([trimmed]);\n }\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n }\n\n function selectOption(opt: ComboboxOption) {\n if (multiple) {\n const next = isSelected(opt.value)\n ? selectedValues.filter((v) => v !== opt.value)\n : [...selectedValues, opt.value];\n emit(next);\n setQuery(\"\");\n setHighlighted(-1);\n inputRef.current?.focus();\n return;\n }\n emit([opt.value]);\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n inputRef.current?.focus();\n }\n\n function clear() {\n if (selectedValues.length > 0) emit([]);\n setQuery(\"\");\n setHighlighted(-1);\n inputRef.current?.focus();\n }\n\n function scrollToHighlighted(index: number) {\n const list = listRef.current;\n if (!list) return;\n const item = list.children[index] as HTMLElement | undefined;\n item?.scrollIntoView({ block: \"nearest\" });\n }\n\n function onKeyDown(e: KeyboardEvent<HTMLInputElement>) {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n if (!open) {\n openMenu();\n return;\n }\n const next = highlighted < filtered.length - 1 ? highlighted + 1 : 0;\n setHighlighted(next);\n scrollToHighlighted(next);\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n if (!open) return;\n const next = highlighted > 0 ? highlighted - 1 : filtered.length - 1;\n setHighlighted(next);\n scrollToHighlighted(next);\n } else if (e.key === \"Enter\") {\n if (open && highlighted >= 0 && filtered[highlighted] && !filtered[highlighted].disabled) {\n e.preventDefault();\n selectOption(filtered[highlighted]);\n return;\n }\n commitAndClose();\n onKeyDownProp?.(e);\n } else if (e.key === \"Escape\") {\n if (open) {\n e.preventDefault();\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n }\n } else {\n onKeyDownProp?.(e);\n }\n }\n\n const listId = id ? `${id}-listbox` : undefined;\n const ariaLabel = typeof label === \"string\" ? label : undefined;\n const showClear = !required && !loading && !disabled && selectedValues.length > 0;\n\n return (\n <div ref={rootRef} className={cn(\"relative\", className)}>\n <div className=\"relative flex items-center\">\n {label != null && (\n <span className=\"pointer-events-none absolute left-2 z-10 whitespace-nowrap font-medium uppercase tracking-wide text-muted-foreground text-[10px]\">\n {label}\n </span>\n )}\n <input\n ref={inputRef}\n id={id}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listId}\n aria-activedescendant={highlighted >= 0 ? `${listId}-${highlighted}` : undefined}\n aria-label={ariaLabel}\n autoComplete=\"off\"\n disabled={disabled}\n placeholder={placeholder}\n value={displayValue}\n onChange={(e) => {\n setQuery(e.target.value);\n if (!open) setOpen(true);\n }}\n onFocus={openMenu}\n onKeyDown={onKeyDown}\n className={cn(\n \"h-control-h w-full rounded-md border border-input bg-background px-control-px text-sm text-foreground\",\n showClear ? \"pr-14\" : \"pr-8\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n )}\n style={label != null ? labelPadding(label) : undefined}\n />\n {showClear && (\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label=\"Clear\"\n onClick={clear}\n className=\"absolute right-7 flex h-full items-center px-1 text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiClose} className=\"text-xs\" />\n </button>\n )}\n {loading ? (\n <span className=\"pointer-events-none absolute right-2 top-1/2 -translate-y-1/2\">\n <svg className=\"h-3.5 w-3.5 animate-spin text-muted-foreground\" viewBox=\"0 0 16 16\" fill=\"none\">\n <circle cx=\"8\" cy=\"8\" r=\"6.5\" stroke=\"currentColor\" strokeWidth=\"2\" opacity=\"0.25\" />\n <path d=\"M14.5 8a6.5 6.5 0 0 0-6.5-6.5\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n </svg>\n </span>\n ) : (\n <button\n type=\"button\"\n tabIndex={-1}\n disabled={disabled}\n aria-label=\"Toggle options\"\n onClick={() => {\n if (open) {\n commitAndClose();\n } else {\n openMenu();\n inputRef.current?.focus();\n }\n }}\n className=\"pointer-events-auto absolute right-0 flex h-full items-center px-2 text-muted-foreground\"\n >\n <Icon icon={UiChevronDown} className=\"text-xs\" />\n </button>\n )}\n </div>\n {open && (\n <div\n id={listId}\n ref={listRef}\n role=\"listbox\"\n aria-multiselectable={multiple || undefined}\n className=\"absolute left-0 top-[calc(100%+0.25rem)] z-50 w-full min-w-[10rem] max-h-64 overflow-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg shadow-black/5\"\n >\n {loading && filtered.length === 0 && (\n <div className=\"px-2 py-4 text-center text-sm text-muted-foreground\">Loading…</div>\n )}\n {!loading && filtered.length === 0 && (\n <div className=\"px-2 py-4 text-center text-sm text-muted-foreground\">No results</div>\n )}\n {filtered.map((opt, i) => {\n const selected = isSelected(opt.value);\n return (\n <div\n key={opt.value}\n id={listId ? `${listId}-${i}` : undefined}\n role=\"option\"\n aria-selected={selected}\n aria-disabled={opt.disabled}\n onMouseDown={(e) => {\n e.preventDefault();\n if (!opt.disabled) selectOption(opt);\n }}\n onMouseEnter={() => setHighlighted(i)}\n className={cn(\n \"flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm\",\n i === highlighted && \"bg-accent\",\n selected && \"font-medium\",\n opt.disabled && \"cursor-not-allowed opacity-50\",\n )}\n >\n <Icon\n icon={UiCheck}\n className={cn(\"text-xs shrink-0\", selected ? \"opacity-100\" : \"opacity-0\")}\n />\n <span className=\"min-w-0 truncate\">{opt.label}</span>\n </div>\n );\n })}\n </div>\n )}\n </div>\n );\n}\n\n// Reserve room on the left of the input for the inline label so typed text\n// does not overlap it. Roughly 0.6rem per character plus padding.\nfunction labelPadding(label: ReactNode) {\n const length = typeof label === \"string\" ? label.length : 6;\n return { paddingLeft: `${Math.min(length * 0.62 + 0.75, 9)}rem` };\n}\n\n// withSelectedOptions returns the option list plus any selected value that isn't\n// already present, synthesized as {value, label: value}. Used by the server\n// (onSearch) typeahead so an already-selected value missing from the current\n// matches still renders as a checked row. Selected-but-absent values come first\n// so they don't get scrolled past by a long match list.\nfunction withSelectedOptions(\n options: ComboboxOption[],\n selectedValues: string[],\n): ComboboxOption[] {\n const present = new Set(options.map((o) => o.value));\n const pinned = selectedValues\n .filter((v) => !present.has(v))\n .map((v) => ({ value: v, label: v }));\n return pinned.length === 0 ? options : [...pinned, ...options];\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Combobox.js","sources":["../../src/components/Combobox.tsx"],"sourcesContent":["import {\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n type KeyboardEvent,\n type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/utils\";\nimport { Icon, LabelIcon, type LabelIconSpec } from \"../data/Icon\";\nimport { UiChevronDown, UiClose, UiCheck } from \"../icons\";\n\n// Upper bound the open dropdown grows to in order to show full option labels\n// before they truncate. The menu's minimum is always the input's width.\nconst MENU_MAX_WIDTH_PX = 400;\n\nexport type ComboboxOption = {\n value: string;\n label: string;\n disabled?: boolean;\n /** Leading glyph for the option: a runtime icon name or a rendered node. */\n icon?: LabelIconSpec;\n};\n\ntype ComboboxBaseProps = {\n /** Available options, rendered in the order provided. */\n options: ComboboxOption[];\n /** Ghost text when no value is set. */\n placeholder?: string;\n /** Optional label rendered inline inside the control, before the input. */\n label?: ReactNode;\n /** Disables the input. */\n disabled?: boolean;\n /** When true, the value cannot be cleared (the clear button is hidden). */\n required?: boolean;\n /**\n * When false, the value is restricted to the provided options — typed text\n * that does not match an option is discarded instead of committed. Defaults\n * to true (freeform entry allowed).\n */\n allowCustomValue?: boolean;\n /** HTML id for the input element. */\n id?: string;\n /** Classes applied to the root wrapper. */\n className?: string;\n /** Shows a loading indicator when options are being fetched. */\n loading?: boolean;\n /**\n * Optional async search invoked (debounced ~250ms) as the user types. The\n * consumer fetches and feeds matching options back via the `options` prop\n * (setting `loading` while in flight). When set, the component renders the\n * provided `options` as-is instead of filtering them client-side — the\n * server has already filtered. When absent, typing filters `options`\n * client-side as before.\n */\n onSearch?: (query: string) => void;\n /** Called when a key is pressed in the input. */\n onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void;\n};\n\nexport type ComboboxSingleProps = ComboboxBaseProps & {\n multiple?: false;\n /** Controlled selected value. */\n value: string;\n /** Called when the selected value changes (from list or freeform input). */\n onChange: (value: string) => void;\n};\n\nexport type ComboboxMultiProps = ComboboxBaseProps & {\n multiple: true;\n /** Controlled selected values. */\n value: string[];\n /** Called with the complete next value array after each toggle. */\n onChange: (value: string[]) => void;\n};\n\nexport type ComboboxProps = ComboboxSingleProps | ComboboxMultiProps;\n\nexport function Combobox(props: ComboboxProps) {\n const {\n options,\n placeholder,\n label,\n disabled,\n required,\n allowCustomValue = true,\n id,\n className,\n loading,\n onSearch,\n onKeyDown: onKeyDownProp,\n } = props;\n const multiple = props.multiple === true;\n const selectedValues = useMemo<string[]>(\n () => (multiple ? props.value : props.value ? [props.value] : []),\n [multiple, props.value],\n );\n\n const rootRef = useRef<HTMLDivElement>(null);\n const anchorRef = useRef<HTMLDivElement>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n const listRef = useRef<HTMLDivElement>(null);\n const [open, setOpen] = useState(false);\n // Fixed-position coordinates for the portaled listbox, measured from the\n // input row. `width` is the input width (the menu's minimum); `maxWidth` caps\n // content growth at 400px, further bounded by the viewport. Null until first\n // measured so we never render at (0,0).\n const [menuPos, setMenuPos] = useState<{\n top: number;\n left: number;\n width: number;\n maxWidth: number;\n } | null>(null);\n // `query` is the type-ahead filter text, kept separate from the committed\n // value. It is empty unless the user is actively typing, so opening the\n // dropdown shows every option rather than only the selected one.\n const [query, setQuery] = useState(\"\");\n const [highlighted, setHighlighted] = useState(-1);\n\n const isSelected = (optValue: string) => selectedValues.includes(optValue);\n\n // Closed-state input text. Single: the selected option's label (or its raw\n // value as a fallback). Multi: a summary mirroring MultiSelect.\n const closedLabel = useMemo(() => {\n if (multiple) {\n const labels = options\n .filter((o) => selectedValues.includes(o.value))\n .map((o) => o.label);\n if (labels.length === 0) return \"\";\n if (labels.length <= 2) return labels.join(\", \");\n return `${labels.length} selected`;\n }\n const single = selectedValues[0] ?? \"\";\n return options.find((o) => o.value === single)?.label ?? single;\n }, [multiple, options, selectedValues]);\n\n // While open the input mirrors the query; while closed it shows the\n // committed selection's label/summary.\n const displayValue = open ? query : closedLabel;\n\n const filtered = useMemo(() => {\n // With onSearch the server already filtered. The matches replace the list,\n // but any already-selected value not in the matches is pinned so the user's\n // selection stays visible and toggleable. Selected values carry no label\n // object here, so synthesize {value, label: value} (the typeahead's value is\n // its own human label).\n if (onSearch) return withSelectedOptions(options, selectedValues);\n const q = query.toLowerCase().trim();\n if (!q) return options;\n return options.filter(\n (o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q),\n );\n }, [onSearch, options, query, selectedValues]);\n\n // Debounced server-side search: fires onSearch ~250ms after the query settles\n // while the menu is open. The empty-query call lets the consumer reset its\n // result set. Only active when onSearch is provided.\n useEffect(() => {\n if (!onSearch || !open) return;\n const handle = setTimeout(() => onSearch(query.trim()), 250);\n return () => clearTimeout(handle);\n }, [onSearch, open, query]);\n\n useEffect(() => {\n setHighlighted(-1);\n }, [filtered.length]);\n\n useEffect(() => {\n if (!open) return;\n const onPointerDown = (e: MouseEvent) => {\n const target = e.target as Node;\n // The listbox is portaled out of rootRef, so a click inside it would\n // otherwise read as \"outside\" and close the menu before selection.\n if (!rootRef.current?.contains(target) && !listRef.current?.contains(target)) {\n commitAndClose();\n }\n };\n document.addEventListener(\"mousedown\", onPointerDown);\n return () => document.removeEventListener(\"mousedown\", onPointerDown);\n });\n\n // Position the portaled listbox with fixed coordinates measured from the\n // input row, so it escapes any overflow-hidden / scroll ancestor (e.g. a\n // Modal body) that would otherwise clip it. Mirrors HoverCard's approach.\n useLayoutEffect(() => {\n if (!open) {\n setMenuPos(null);\n return;\n }\n const update = () => {\n const anchor = anchorRef.current;\n if (!anchor) return;\n const rect = anchor.getBoundingClientRect();\n // The menu may grow rightward up to 400px, but not past the viewport edge\n // (leaving an 8px gutter). Never let the cap fall below the input width.\n const viewportCap = window.innerWidth - rect.left - 8;\n const maxWidth = Math.max(rect.width, Math.min(MENU_MAX_WIDTH_PX, viewportCap));\n setMenuPos({ top: rect.bottom + 4, left: rect.left, width: rect.width, maxWidth });\n };\n update();\n window.addEventListener(\"scroll\", update, true);\n window.addEventListener(\"resize\", update);\n return () => {\n window.removeEventListener(\"scroll\", update, true);\n window.removeEventListener(\"resize\", update);\n };\n }, [open]);\n\n function emit(next: string[]) {\n if (multiple) {\n props.onChange(next);\n } else {\n props.onChange(next[0] ?? \"\");\n }\n }\n\n function openMenu() {\n setQuery(\"\");\n setHighlighted(-1);\n setOpen(true);\n }\n\n function commitAndClose() {\n // Freeform commit (single, allowCustomValue only): a non-empty query that\n // doesn't match an option becomes the value. An empty query keeps the\n // existing selection. Multi and strict modes never commit freeform text.\n const trimmed = query.trim();\n if (!multiple && allowCustomValue && trimmed && trimmed !== selectedValues[0]) {\n emit([trimmed]);\n }\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n }\n\n function selectOption(opt: ComboboxOption) {\n if (multiple) {\n const next = isSelected(opt.value)\n ? selectedValues.filter((v) => v !== opt.value)\n : [...selectedValues, opt.value];\n emit(next);\n setQuery(\"\");\n setHighlighted(-1);\n inputRef.current?.focus();\n return;\n }\n emit([opt.value]);\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n inputRef.current?.focus();\n }\n\n function clear() {\n if (selectedValues.length > 0) emit([]);\n setQuery(\"\");\n setHighlighted(-1);\n inputRef.current?.focus();\n }\n\n function scrollToHighlighted(index: number) {\n const list = listRef.current;\n if (!list) return;\n const item = list.children[index] as HTMLElement | undefined;\n item?.scrollIntoView({ block: \"nearest\" });\n }\n\n function onKeyDown(e: KeyboardEvent<HTMLInputElement>) {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n if (!open) {\n openMenu();\n return;\n }\n const next = highlighted < filtered.length - 1 ? highlighted + 1 : 0;\n setHighlighted(next);\n scrollToHighlighted(next);\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n if (!open) return;\n const next = highlighted > 0 ? highlighted - 1 : filtered.length - 1;\n setHighlighted(next);\n scrollToHighlighted(next);\n } else if (e.key === \"Enter\") {\n if (open && highlighted >= 0 && filtered[highlighted] && !filtered[highlighted].disabled) {\n e.preventDefault();\n selectOption(filtered[highlighted]);\n return;\n }\n commitAndClose();\n onKeyDownProp?.(e);\n } else if (e.key === \"Escape\") {\n if (open) {\n e.preventDefault();\n setQuery(\"\");\n setOpen(false);\n setHighlighted(-1);\n }\n } else {\n onKeyDownProp?.(e);\n }\n }\n\n const listId = id ? `${id}-listbox` : undefined;\n const ariaLabel = typeof label === \"string\" ? label : undefined;\n const showClear = !required && !loading && !disabled && selectedValues.length > 0;\n\n return (\n <div ref={rootRef} className={cn(\"relative\", className)}>\n <div ref={anchorRef} className=\"relative flex items-center\">\n {label != null && (\n <span className=\"pointer-events-none absolute left-2 z-10 whitespace-nowrap font-medium uppercase tracking-wide text-muted-foreground text-[10px]\">\n {label}\n </span>\n )}\n <input\n ref={inputRef}\n id={id}\n type=\"text\"\n role=\"combobox\"\n aria-expanded={open}\n aria-controls={listId}\n aria-activedescendant={highlighted >= 0 ? `${listId}-${highlighted}` : undefined}\n aria-label={ariaLabel}\n autoComplete=\"off\"\n disabled={disabled}\n placeholder={placeholder}\n value={displayValue}\n onChange={(e) => {\n setQuery(e.target.value);\n if (!open) setOpen(true);\n }}\n onFocus={openMenu}\n onKeyDown={onKeyDown}\n className={cn(\n \"h-control-h w-full rounded-md border border-input bg-background px-control-px text-sm text-foreground\",\n showClear ? \"pr-14\" : \"pr-8\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n )}\n style={label != null ? labelPadding(label) : undefined}\n />\n {showClear && (\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label=\"Clear\"\n onClick={clear}\n className=\"absolute right-7 flex h-full items-center px-1 text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiClose} className=\"text-xs\" />\n </button>\n )}\n {loading ? (\n <span className=\"pointer-events-none absolute right-2 top-1/2 -translate-y-1/2\">\n <svg className=\"h-3.5 w-3.5 animate-spin text-muted-foreground\" viewBox=\"0 0 16 16\" fill=\"none\">\n <circle cx=\"8\" cy=\"8\" r=\"6.5\" stroke=\"currentColor\" strokeWidth=\"2\" opacity=\"0.25\" />\n <path d=\"M14.5 8a6.5 6.5 0 0 0-6.5-6.5\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n </svg>\n </span>\n ) : (\n <button\n type=\"button\"\n tabIndex={-1}\n disabled={disabled}\n aria-label=\"Toggle options\"\n onClick={() => {\n if (open) {\n commitAndClose();\n } else {\n openMenu();\n inputRef.current?.focus();\n }\n }}\n className=\"pointer-events-auto absolute right-0 flex h-full items-center px-2 text-muted-foreground\"\n >\n <Icon icon={UiChevronDown} className=\"text-xs\" />\n </button>\n )}\n </div>\n {open && menuPos && typeof document !== \"undefined\" &&\n createPortal(\n <div\n id={listId}\n ref={listRef}\n role=\"listbox\"\n aria-multiselectable={multiple || undefined}\n style={{\n position: \"fixed\",\n top: menuPos.top,\n left: menuPos.left,\n // Grow to fit the widest option, but never narrower than the input\n // nor wider than 400px (beyond which option labels truncate). The\n // cap is also bounded by the viewport so the menu can't overflow.\n minWidth: menuPos.width,\n maxWidth: menuPos.maxWidth,\n }}\n className=\"z-[9999] max-h-64 w-max overflow-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg shadow-black/5\"\n >\n {loading && filtered.length === 0 && (\n <div className=\"px-2 py-4 text-center text-sm text-muted-foreground\">Loading…</div>\n )}\n {!loading && filtered.length === 0 && (\n <div className=\"px-2 py-4 text-center text-sm text-muted-foreground\">No results</div>\n )}\n {filtered.map((opt, i) => {\n const selected = isSelected(opt.value);\n return (\n <div\n key={opt.value}\n id={listId ? `${listId}-${i}` : undefined}\n role=\"option\"\n aria-selected={selected}\n aria-disabled={opt.disabled}\n onMouseDown={(e) => {\n e.preventDefault();\n if (!opt.disabled) selectOption(opt);\n }}\n onMouseEnter={() => setHighlighted(i)}\n className={cn(\n \"flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm\",\n i === highlighted && \"bg-accent\",\n selected && \"font-medium\",\n opt.disabled && \"cursor-not-allowed opacity-50\",\n )}\n >\n <Icon\n icon={UiCheck}\n className={cn(\"text-xs shrink-0\", selected ? \"opacity-100\" : \"opacity-0\")}\n />\n <LabelIcon icon={opt.icon} className=\"text-sm text-muted-foreground\" />\n <span className=\"min-w-0 truncate\">{opt.label}</span>\n </div>\n );\n })}\n </div>,\n document.body,\n )}\n </div>\n );\n}\n\n// Reserve room on the left of the input for the inline label so typed text\n// does not overlap it. Roughly 0.6rem per character plus padding.\nfunction labelPadding(label: ReactNode) {\n const length = typeof label === \"string\" ? label.length : 6;\n return { paddingLeft: `${Math.min(length * 0.62 + 0.75, 9)}rem` };\n}\n\n// withSelectedOptions returns the option list plus any selected value that isn't\n// already present, synthesized as {value, label: value}. Used by the server\n// (onSearch) typeahead so an already-selected value missing from the current\n// matches still renders as a checked row. Selected-but-absent values come first\n// so they don't get scrolled past by a long match list.\nfunction withSelectedOptions(\n options: ComboboxOption[],\n selectedValues: string[],\n): ComboboxOption[] {\n const present = new Set(options.map((o) => o.value));\n const pinned = selectedValues\n .filter((v) => !present.has(v))\n .map((v) => ({ value: v, label: v }));\n return pinned.length === 0 ? options : [...pinned, ...options];\n}\n"],"names":[],"mappings":"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"}
@@ -4,13 +4,13 @@ const jsxRuntime = require("react/jsx-runtime");
4
4
  const react = require("react");
5
5
  const FilterPill = require("../data/FilterPill.cjs");
6
6
  const Icon = require("../data/Icon.cjs");
7
+ const Timestamp = require("../data/cells/Timestamp.cjs");
7
8
  const utils = require("../lib/utils.cjs");
8
9
  const button = require("./button.cjs");
9
10
  const Combobox = require("./Combobox.cjs");
10
11
  const DateTimePicker = require("./DateTimePicker.cjs");
11
12
  const MultiSelect = require("./MultiSelect.cjs");
12
13
  const RangeSlider = require("./RangeSlider.cjs");
13
- const select = require("./select.cjs");
14
14
  const TimeRange = require("./TimeRange.cjs");
15
15
  const UiSearch = require("../icons/components/UiSearch.cjs");
16
16
  const UiFilter = require("../icons/components/UiFilter.cjs");
@@ -357,13 +357,16 @@ function OverflowFiltersMenu({
357
357
  "data-overflow-filter-row": filter.key,
358
358
  className: "grid h-12 grid-cols-[minmax(7rem,10rem)_auto_minmax(0,1fr)_auto] items-center gap-2 overflow-visible p-2",
359
359
  children: [
360
- /* @__PURE__ */ jsxRuntime.jsx(
360
+ /* @__PURE__ */ jsxRuntime.jsxs(
361
361
  "label",
362
362
  {
363
363
  htmlFor: filterInputId(filter),
364
- className: "min-w-0 truncate text-[10px] font-medium uppercase tracking-wide text-muted-foreground",
364
+ className: "flex min-w-0 items-center gap-1 truncate text-[10px] font-medium uppercase tracking-wide text-muted-foreground",
365
365
  title: filter.description ?? filter.label,
366
- children: filter.label
366
+ children: [
367
+ /* @__PURE__ */ jsxRuntime.jsx(Icon.LabelIcon, { icon: filter.icon, className: "text-[12px] normal-case" }),
368
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: filter.label })
369
+ ]
367
370
  }
368
371
  ),
369
372
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-muted-foreground", children: "=" }),
@@ -542,21 +545,24 @@ function LookupMultiFilterValueControl({ filter }) {
542
545
  }
543
546
  );
544
547
  }
548
+ function enumOptionsToCombobox(options) {
549
+ return options.map((option) => ({
550
+ value: option.value,
551
+ label: option.label ?? option.value
552
+ }));
553
+ }
545
554
  function EnumFilterValueControl({ filter }) {
546
555
  return /* @__PURE__ */ jsxRuntime.jsx(
547
- select.Select,
556
+ Combobox.Combobox,
548
557
  {
549
558
  id: filterInputId(filter),
550
- "aria-label": filter.label,
551
- className: "h-8 w-full rounded-md border border-input bg-background px-2 text-sm shadow-none",
559
+ options: enumOptionsToCombobox(filter.options),
552
560
  value: filter.value,
561
+ onChange: filter.onChange,
562
+ allowCustomValue: false,
553
563
  placeholder: filter.placeholder ?? `Any ${filter.label.toLowerCase()}`,
554
- disabled: filter.disabled,
555
- onChange: (event) => filter.onChange(event.target.value),
556
- options: filter.options.map((option) => ({
557
- value: option.value,
558
- label: option.label ?? option.value
559
- }))
564
+ className: "w-full",
565
+ ...filter.disabled !== void 0 ? { disabled: filter.disabled } : {}
560
566
  }
561
567
  );
562
568
  }
@@ -623,34 +629,17 @@ function FilterBarRangePanel({
623
629
  ) });
624
630
  }
625
631
  function EnumFilterField({ filter, grow }) {
626
- return /* @__PURE__ */ jsxRuntime.jsxs(
627
- "label",
632
+ return /* @__PURE__ */ jsxRuntime.jsx(
633
+ Combobox.Combobox,
628
634
  {
629
- title: filter.description,
630
- className: utils.cn(
631
- "flex h-8 items-center gap-2 rounded-md border border-input bg-muted/30 pl-2 pr-1 text-xs",
632
- grow ? "min-w-[12rem] max-w-[18rem] flex-1" : "min-w-[11rem] max-w-[15rem] shrink-0",
633
- filter.disabled && "opacity-60",
634
- filter.className
635
- ),
636
- children: [
637
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "whitespace-nowrap font-medium uppercase tracking-wide text-muted-foreground", children: filter.label }),
638
- /* @__PURE__ */ jsxRuntime.jsx(
639
- select.Select,
640
- {
641
- "aria-label": filter.label,
642
- className: "h-6 border-0 bg-transparent px-1 text-xs shadow-none focus-visible:ring-0",
643
- value: filter.value,
644
- placeholder: filter.placeholder ?? `Any ${filter.label.toLowerCase()}`,
645
- disabled: filter.disabled,
646
- onChange: (event) => filter.onChange(event.target.value),
647
- options: filter.options.map((option) => ({
648
- value: option.value,
649
- label: option.label ?? option.value
650
- }))
651
- }
652
- )
653
- ]
635
+ label: filter.label,
636
+ options: enumOptionsToCombobox(filter.options),
637
+ value: filter.value,
638
+ onChange: filter.onChange,
639
+ allowCustomValue: false,
640
+ placeholder: filter.placeholder ?? `Any ${filter.label.toLowerCase()}`,
641
+ className: utils.cn(lookupFieldWidthClass(grow), filter.className),
642
+ ...filter.disabled !== void 0 ? { disabled: filter.disabled } : {}
654
643
  }
655
644
  );
656
645
  }
@@ -676,7 +665,7 @@ function BooleanFilterField({ filter }) {
676
665
  onChange: (event) => filter.onChange(event.target.checked)
677
666
  }
678
667
  ),
679
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "whitespace-nowrap font-medium uppercase tracking-wide text-muted-foreground", children: filter.label })
668
+ /* @__PURE__ */ jsxRuntime.jsx(FilterFieldLabel, { icon: filter.icon, label: filter.label })
680
669
  ]
681
670
  }
682
671
  );
@@ -720,7 +709,7 @@ function TextFilterField({ filter, grow }) {
720
709
  filter.className
721
710
  ),
722
711
  children: [
723
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "whitespace-nowrap font-medium uppercase tracking-wide text-muted-foreground", children: filter.label }),
712
+ /* @__PURE__ */ jsxRuntime.jsx(FilterFieldLabel, { icon: filter.icon, label: filter.label }),
724
713
  /* @__PURE__ */ jsxRuntime.jsx(
725
714
  "input",
726
715
  {
@@ -737,16 +726,23 @@ function TextFilterField({ filter, grow }) {
737
726
  }
738
727
  );
739
728
  }
729
+ function FilterFieldLabel({ icon, label }) {
730
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1 whitespace-nowrap font-medium uppercase tracking-wide text-muted-foreground", children: [
731
+ /* @__PURE__ */ jsxRuntime.jsx(Icon.LabelIcon, { icon, className: "text-[13px] normal-case" }),
732
+ label
733
+ ] });
734
+ }
740
735
  function lookupFieldWidthClass(grow) {
741
736
  return grow ? "min-w-[12rem] max-w-[18rem] flex-1" : "min-w-[11rem] max-w-[15rem] shrink-0";
742
737
  }
743
738
  function LookupFilterField({ filter, grow }) {
744
739
  const [draft, setDraft] = useDebouncedTextDraft(filter.value, filter.onChange);
745
740
  if (filter.inputType === "date" || filter.inputType === "number") {
741
+ const dateTitle = filter.inputType === "date" && draft ? Timestamp.formatDateTimeRelative(draft) : void 0;
746
742
  return /* @__PURE__ */ jsxRuntime.jsxs(
747
743
  "label",
748
744
  {
749
- title: filter.description,
745
+ title: dateTitle ?? filter.description,
750
746
  className: utils.cn(
751
747
  "flex h-8 items-center gap-2 rounded-md border border-input bg-muted/30 pl-2 pr-2 text-xs",
752
748
  lookupFieldWidthClass(grow),
@@ -754,7 +750,7 @@ function LookupFilterField({ filter, grow }) {
754
750
  filter.className
755
751
  ),
756
752
  children: [
757
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "whitespace-nowrap font-medium uppercase tracking-wide text-muted-foreground", children: filter.label }),
753
+ /* @__PURE__ */ jsxRuntime.jsx(FilterFieldLabel, { icon: filter.icon, label: filter.label }),
758
754
  filter.inputType === "date" ? /* @__PURE__ */ jsxRuntime.jsx(
759
755
  DateTimePicker.DateTimePicker,
760
756
  {
@@ -860,6 +856,7 @@ function NumberFilterField({ filter, grow }) {
860
856
  summary === filter.label && "text-muted-foreground"
861
857
  ),
862
858
  children: [
859
+ /* @__PURE__ */ jsxRuntime.jsx(Icon.LabelIcon, { icon: filter.icon, className: "text-[14px] text-muted-foreground" }),
863
860
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: summary }),
864
861
  /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { icon: open ? UiChevronUp.UiChevronUp : UiChevronDown.UiChevronDown, className: "text-muted-foreground" })
865
862
  ]
@@ -1129,6 +1126,7 @@ function MultiFilterField({ filter, grow }) {
1129
1126
  summary === filter.label && "text-muted-foreground"
1130
1127
  ),
1131
1128
  children: [
1129
+ /* @__PURE__ */ jsxRuntime.jsx(Icon.LabelIcon, { icon: filter.icon, className: "text-[14px] text-muted-foreground" }),
1132
1130
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: summary }),
1133
1131
  /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { icon: open ? UiChevronUp.UiChevronUp : UiChevronDown.UiChevronDown, className: "text-muted-foreground" })
1134
1132
  ]