@flanksource/clicky-ui 0.3.18 → 0.3.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/dist/components/ComboboxMenu.cjs +5 -1
  2. package/dist/components/ComboboxMenu.cjs.map +1 -1
  3. package/dist/components/ComboboxMenu.d.ts.map +1 -1
  4. package/dist/components/ComboboxMenu.js +5 -1
  5. package/dist/components/ComboboxMenu.js.map +1 -1
  6. package/dist/components/EndpointSelector.model.cjs +96 -0
  7. package/dist/components/EndpointSelector.model.cjs.map +1 -0
  8. package/dist/components/EndpointSelector.model.d.ts +2 -0
  9. package/dist/components/EndpointSelector.model.d.ts.map +1 -1
  10. package/dist/components/EndpointSelector.model.js +96 -0
  11. package/dist/components/EndpointSelector.model.js.map +1 -0
  12. package/dist/components/FilterBar.cjs +52 -2
  13. package/dist/components/FilterBar.cjs.map +1 -1
  14. package/dist/components/FilterBar.d.ts +20 -2
  15. package/dist/components/FilterBar.d.ts.map +1 -1
  16. package/dist/components/FilterBar.js +52 -2
  17. package/dist/components/FilterBar.js.map +1 -1
  18. package/dist/components/JSONPathField.cjs +87 -66
  19. package/dist/components/JSONPathField.cjs.map +1 -1
  20. package/dist/components/JSONPathField.d.ts +26 -9
  21. package/dist/components/JSONPathField.d.ts.map +1 -1
  22. package/dist/components/JSONPathField.js +88 -67
  23. package/dist/components/JSONPathField.js.map +1 -1
  24. package/dist/components/JSONPathPlayground.cjs +225 -0
  25. package/dist/components/JSONPathPlayground.cjs.map +1 -0
  26. package/dist/components/JSONPathPlayground.d.ts +59 -0
  27. package/dist/components/JSONPathPlayground.d.ts.map +1 -0
  28. package/dist/components/JSONPathPlayground.js +225 -0
  29. package/dist/components/JSONPathPlayground.js.map +1 -0
  30. package/dist/components/OnePasswordSelector.cjs +178 -0
  31. package/dist/components/OnePasswordSelector.cjs.map +1 -0
  32. package/dist/components/OnePasswordSelector.d.ts +8 -0
  33. package/dist/components/OnePasswordSelector.d.ts.map +1 -0
  34. package/dist/components/OnePasswordSelector.js +178 -0
  35. package/dist/components/OnePasswordSelector.js.map +1 -0
  36. package/dist/components/OnePasswordSelector.model.cjs +31 -0
  37. package/dist/components/OnePasswordSelector.model.cjs.map +1 -0
  38. package/dist/components/OnePasswordSelector.model.d.ts +27 -0
  39. package/dist/components/OnePasswordSelector.model.d.ts.map +1 -0
  40. package/dist/components/OnePasswordSelector.model.js +31 -0
  41. package/dist/components/OnePasswordSelector.model.js.map +1 -0
  42. package/dist/components/SecretFormField.cjs +142 -0
  43. package/dist/components/SecretFormField.cjs.map +1 -0
  44. package/dist/components/SecretFormField.d.ts +9 -0
  45. package/dist/components/SecretFormField.d.ts.map +1 -0
  46. package/dist/components/SecretFormField.js +142 -0
  47. package/dist/components/SecretFormField.js.map +1 -0
  48. package/dist/components/SecretKeySelector.cjs +20 -14
  49. package/dist/components/SecretKeySelector.cjs.map +1 -1
  50. package/dist/components/SecretKeySelector.d.ts +4 -1
  51. package/dist/components/SecretKeySelector.d.ts.map +1 -1
  52. package/dist/components/SecretKeySelector.js +20 -14
  53. package/dist/components/SecretKeySelector.js.map +1 -1
  54. package/dist/components/TreePickerField.cjs +55 -29
  55. package/dist/components/TreePickerField.cjs.map +1 -1
  56. package/dist/components/TreePickerField.d.ts +10 -1
  57. package/dist/components/TreePickerField.d.ts.map +1 -1
  58. package/dist/components/TreePickerField.js +56 -30
  59. package/dist/components/TreePickerField.js.map +1 -1
  60. package/dist/components/combobox-types.d.ts +9 -0
  61. package/dist/components/combobox-types.d.ts.map +1 -1
  62. package/dist/components/filter-bar-utils.cjs +7 -0
  63. package/dist/components/filter-bar-utils.cjs.map +1 -1
  64. package/dist/components/filter-bar-utils.d.ts.map +1 -1
  65. package/dist/components/filter-bar-utils.js +7 -0
  66. package/dist/components/filter-bar-utils.js.map +1 -1
  67. package/dist/components/jotai-bindings-core.cjs.map +1 -1
  68. package/dist/components/jotai-bindings-core.d.ts +4 -1
  69. package/dist/components/jotai-bindings-core.d.ts.map +1 -1
  70. package/dist/components/jotai-bindings-core.js.map +1 -1
  71. package/dist/components/json-schema-form-render.cjs +2 -1
  72. package/dist/components/json-schema-form-render.cjs.map +1 -1
  73. package/dist/components/json-schema-form-render.d.ts.map +1 -1
  74. package/dist/components/json-schema-form-render.js +2 -1
  75. package/dist/components/json-schema-form-render.js.map +1 -1
  76. package/dist/components/json-schema-form-types.d.ts +9 -0
  77. package/dist/components/json-schema-form-types.d.ts.map +1 -1
  78. package/dist/components/jsonPathTree.cjs +189 -0
  79. package/dist/components/jsonPathTree.cjs.map +1 -0
  80. package/dist/components/jsonPathTree.d.ts +92 -0
  81. package/dist/components/jsonPathTree.d.ts.map +1 -0
  82. package/dist/components/jsonPathTree.js +189 -0
  83. package/dist/components/jsonPathTree.js.map +1 -0
  84. package/dist/components/secret-form-extension.cjs +30 -0
  85. package/dist/components/secret-form-extension.cjs.map +1 -0
  86. package/dist/components/secret-form-extension.d.ts +23 -0
  87. package/dist/components/secret-form-extension.d.ts.map +1 -0
  88. package/dist/components/secret-form-extension.js +30 -0
  89. package/dist/components/secret-form-extension.js.map +1 -0
  90. package/dist/components.cjs +16 -0
  91. package/dist/components.cjs.map +1 -1
  92. package/dist/components.d.ts +6 -0
  93. package/dist/components.d.ts.map +1 -1
  94. package/dist/components.js +16 -0
  95. package/dist/components.js.map +1 -1
  96. package/dist/data/TaskExecDetails.cjs +40 -0
  97. package/dist/data/TaskExecDetails.cjs.map +1 -0
  98. package/dist/data/TaskExecDetails.d.ts +5 -0
  99. package/dist/data/TaskExecDetails.d.ts.map +1 -0
  100. package/dist/data/TaskExecDetails.js +40 -0
  101. package/dist/data/TaskExecDetails.js.map +1 -0
  102. package/dist/data/TaskProgress.cjs +9 -3
  103. package/dist/data/TaskProgress.cjs.map +1 -1
  104. package/dist/data/TaskProgress.d.ts.map +1 -1
  105. package/dist/data/TaskProgress.js +9 -3
  106. package/dist/data/TaskProgress.js.map +1 -1
  107. package/dist/data/TaskSnapshot.cjs.map +1 -1
  108. package/dist/data/TaskSnapshot.d.ts +12 -1
  109. package/dist/data/TaskSnapshot.d.ts.map +1 -1
  110. package/dist/data/TaskSnapshot.js.map +1 -1
  111. package/dist/data/TreeNode.cjs +3 -0
  112. package/dist/data/TreeNode.cjs.map +1 -1
  113. package/dist/data/TreeNode.d.ts.map +1 -1
  114. package/dist/data/TreeNode.js +3 -0
  115. package/dist/data/TreeNode.js.map +1 -1
  116. package/dist/data/data-table-filter-values.cjs +20 -0
  117. package/dist/data/data-table-filter-values.cjs.map +1 -1
  118. package/dist/data/data-table-filter-values.d.ts +16 -0
  119. package/dist/data/data-table-filter-values.d.ts.map +1 -1
  120. package/dist/data/data-table-filter-values.js +20 -0
  121. package/dist/data/data-table-filter-values.js.map +1 -1
  122. package/dist/data/data-table-server-filters.cjs +23 -29
  123. package/dist/data/data-table-server-filters.cjs.map +1 -1
  124. package/dist/data/data-table-server-filters.d.ts.map +1 -1
  125. package/dist/data/data-table-server-filters.js +21 -27
  126. package/dist/data/data-table-server-filters.js.map +1 -1
  127. package/dist/data/query-browser/QueryBrowser.cjs +129 -157
  128. package/dist/data/query-browser/QueryBrowser.cjs.map +1 -1
  129. package/dist/data/query-browser/QueryBrowser.d.ts +1 -71
  130. package/dist/data/query-browser/QueryBrowser.d.ts.map +1 -1
  131. package/dist/data/query-browser/QueryBrowser.editor.cjs +88 -0
  132. package/dist/data/query-browser/QueryBrowser.editor.cjs.map +1 -0
  133. package/dist/data/query-browser/QueryBrowser.editor.d.ts +12 -0
  134. package/dist/data/query-browser/QueryBrowser.editor.d.ts.map +1 -0
  135. package/dist/data/query-browser/QueryBrowser.editor.js +88 -0
  136. package/dist/data/query-browser/QueryBrowser.editor.js.map +1 -0
  137. package/dist/data/query-browser/QueryBrowser.js +130 -158
  138. package/dist/data/query-browser/QueryBrowser.js.map +1 -1
  139. package/dist/data/query-browser/QueryBrowser.types.cjs +11 -0
  140. package/dist/data/query-browser/QueryBrowser.types.cjs.map +1 -0
  141. package/dist/data/query-browser/QueryBrowser.types.d.ts +92 -0
  142. package/dist/data/query-browser/QueryBrowser.types.d.ts.map +1 -0
  143. package/dist/data/query-browser/QueryBrowser.types.js +11 -0
  144. package/dist/data/query-browser/QueryBrowser.types.js.map +1 -0
  145. package/dist/data/query-browser/QueryBrowserDiagnosticsPanel.cjs +56 -0
  146. package/dist/data/query-browser/QueryBrowserDiagnosticsPanel.cjs.map +1 -0
  147. package/dist/data/query-browser/QueryBrowserDiagnosticsPanel.d.ts +5 -0
  148. package/dist/data/query-browser/QueryBrowserDiagnosticsPanel.d.ts.map +1 -0
  149. package/dist/data/query-browser/QueryBrowserDiagnosticsPanel.js +56 -0
  150. package/dist/data/query-browser/QueryBrowserDiagnosticsPanel.js.map +1 -0
  151. package/dist/data/query-browser/QueryBrowserResults.cjs +80 -0
  152. package/dist/data/query-browser/QueryBrowserResults.cjs.map +1 -0
  153. package/dist/data/query-browser/QueryBrowserResults.d.ts +16 -0
  154. package/dist/data/query-browser/QueryBrowserResults.d.ts.map +1 -0
  155. package/dist/data/query-browser/QueryBrowserResults.js +80 -0
  156. package/dist/data/query-browser/QueryBrowserResults.js.map +1 -0
  157. package/dist/data/query-browser/index.d.ts +2 -1
  158. package/dist/data/query-browser/index.d.ts.map +1 -1
  159. package/dist/data/task-exec-details.cjs +9 -0
  160. package/dist/data/task-exec-details.cjs.map +1 -0
  161. package/dist/data/task-exec-details.d.ts +3 -0
  162. package/dist/data/task-exec-details.d.ts.map +1 -0
  163. package/dist/data/task-exec-details.js +9 -0
  164. package/dist/data/task-exec-details.js.map +1 -0
  165. package/dist/data/version-info.cjs +3 -3
  166. package/dist/data/version-info.js +3 -3
  167. package/dist/data.cjs +2 -0
  168. package/dist/data.cjs.map +1 -1
  169. package/dist/data.d.ts +1 -1
  170. package/dist/data.d.ts.map +1 -1
  171. package/dist/data.js +2 -0
  172. package/dist/data.js.map +1 -1
  173. package/dist/index.cjs +18 -0
  174. package/dist/index.cjs.map +1 -1
  175. package/dist/index.js +18 -0
  176. package/dist/index.js.map +1 -1
  177. package/dist/rpc/FilterForm.cjs +20 -0
  178. package/dist/rpc/FilterForm.cjs.map +1 -1
  179. package/dist/rpc/FilterForm.js +20 -0
  180. package/dist/rpc/FilterForm.js.map +1 -1
  181. package/dist/rpc/formMetadata.cjs +34 -0
  182. package/dist/rpc/formMetadata.cjs.map +1 -1
  183. package/dist/rpc/formMetadata.d.ts.map +1 -1
  184. package/dist/rpc/formMetadata.js +35 -1
  185. package/dist/rpc/formMetadata.js.map +1 -1
  186. package/dist/rpc/types.cjs.map +1 -1
  187. package/dist/rpc/types.d.ts +9 -1
  188. package/dist/rpc/types.d.ts.map +1 -1
  189. package/dist/rpc/types.js.map +1 -1
  190. package/dist/styles.css +1 -1
  191. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"SecretKeySelector.js","sources":["../../src/components/SecretKeySelector.tsx"],"sourcesContent":["import { createElement, useEffect, useMemo, useState } from \"react\";\nimport {\n K8SSecret,\n K8SConfigmap,\n Helm,\n K8SServiceaccount,\n Vault,\n} from \"@flanksource/icons/mi\";\nimport { type StaticIconComponent } from \"../data/Icon\";\nimport { cn } from \"../lib/utils\";\nimport { UiEdit } from \"../icons\";\nimport { Combobox, type ComboboxOption } from \"./Combobox\";\nimport { IconMenuPicker, type IconMenuOption } from \"./icon-menu-picker\";\n\n// SecretKeySelector picks how a credential is sourced and lowers the choice into\n// a single reference string the consumer persists. It supports Kubernetes\n// Secrets and ConfigMaps (name + key), Helm release values (name + jsonpath\n// key), a service-account token (name only), a 1Password reference\n// (op://vault/item/field), and a static inline \"Value\". It is presentational:\n// it fetches nothing — the consumer supplies async `loadResources` /\n// `loadKeyPreview` (for keyed kinds) and optional `loadServiceAccounts` getters.\n\n/** Kinds that reference a named resource plus one of its keys. */\nexport type SecretKind = \"secret\" | \"configmap\" | \"helm\";\n\n/** Every source the picker can emit, including the non-keyed ones. */\nexport type SecretValueSource =\n | SecretKind\n | \"serviceaccount\"\n | \"onepassword\"\n | \"value\";\n\n/** One key's mid-masked preview. `value` is already masked by the consumer. */\nexport type KeyPreview = { key: string; value: string };\n\n/**\n * The selector's value: a {kind,name,key} reference into a Secret / ConfigMap /\n * Helm release, a service-account token by name, a 1Password op:// reference, or\n * a static inline literal.\n */\nexport type SecretKeyValue =\n | { kind: SecretKind; name: string; key: string }\n | { kind: \"serviceaccount\"; name: string }\n | { kind: \"onepassword\"; ref: string }\n | { kind: \"value\"; value: string };\n\n/** A named secret/configmap/helm-release and its data key names (values never returned). */\nexport type SecretResource = { name: string; keys?: string[] };\n\nexport type SecretKeySelectorProps = {\n value: SecretKeyValue | undefined;\n onChange: (next: SecretKeyValue | undefined) => void;\n /** Loads the named keyed kind's resources (name + data key names). */\n loadResources: (kind: SecretKind) => Promise<SecretResource[]>;\n /** Loads mid-masked previews for the named keyed resource's keys. */\n loadKeyPreview: (kind: SecretKind, name: string) => Promise<KeyPreview[]>;\n /**\n * Loads the service-account names (no keys) for the \"serviceaccount\" source.\n * When omitted, that source's name field accepts freeform entry only.\n */\n loadServiceAccounts?: () => Promise<SecretResource[]>;\n /**\n * The sources offered in the picker, in order. Defaults to\n * `[\"secret\", \"configmap\", \"value\"]` (or drops \"value\" when `allowLiteral` is\n * false). Pass an explicit list to offer helm / serviceaccount / onepassword.\n */\n sources?: SecretValueSource[];\n /**\n * Shorthand honoured only when `sources` is not supplied: `false` drops the\n * inline \"Value\" source, leaving Secret/ConfigMap. Ignored when `sources` is\n * given.\n */\n allowLiteral?: boolean;\n /**\n * When true, a chosen name absent from the loaded resources, or a key absent\n * from the chosen keyed resource's keys, is flagged invalid (once the load\n * settles). The onepassword and literal sources are never strict-flagged.\n */\n strict?: boolean;\n className?: string;\n};\n\nconst DEFAULT_SOURCES: SecretValueSource[] = [\"secret\", \"configmap\", \"value\"];\n\nconst SHRINKABLE_FIELD = \"max-w-full min-w-0 shrink\";\nconst SOURCE_FIELD = `${SHRINKABLE_FIELD} basis-40 grow-0`;\nconst RESOURCE_FIELD = `${SHRINKABLE_FIELD} basis-40 grow-0`;\nconst KEY_FIELD = `${SHRINKABLE_FIELD} basis-72 grow`;\nconst VALUE_FIELD = `${SHRINKABLE_FIELD} basis-64 grow`;\n\nexport function SecretKeySelector({\n value,\n onChange,\n loadResources,\n loadKeyPreview,\n loadServiceAccounts,\n sources,\n allowLiteral = true,\n strict = false,\n className,\n}: SecretKeySelectorProps) {\n const sourceList = useMemo<SecretValueSource[]>(\n () => sources ?? (allowLiteral ? DEFAULT_SOURCES : [\"secret\", \"configmap\"]),\n [sources, allowLiteral],\n );\n\n const source: SecretValueSource = value?.kind ?? sourceList[0] ?? \"secret\";\n const isKeyed =\n source === \"secret\" || source === \"configmap\" || source === \"helm\";\n const isServiceAccount = source === \"serviceaccount\";\n const isOnePassword = source === \"onepassword\";\n const isLiteral = source === \"value\";\n // A keyed source's refKind drives the loaders; other sources default to \"secret\".\n const refKind: SecretKind = isKeyed ? (source as SecretKind) : \"secret\";\n\n const selectedName =\n value && (value.kind === \"serviceaccount\" || isRefKind(value))\n ? value.name\n : \"\";\n const selectedKey = value && isRefKind(value) ? value.key : \"\";\n const literalValue = value && value.kind === \"value\" ? value.value : \"\";\n const opRef = value && value.kind === \"onepassword\" ? value.ref : \"\";\n\n const [resources, setResources] = useState<SecretResource[]>([]);\n const [resourcesLoading, setResourcesLoading] = useState(false);\n const [previews, setPreviews] = useState<KeyPreview[]>([]);\n const [previewLoading, setPreviewLoading] = useState(false);\n\n // Load the name list for keyed kinds (secret/configmap/helm) and, when a\n // loader is supplied, for the serviceaccount source. Other sources list nothing.\n useEffect(() => {\n const loader = isKeyed\n ? () => loadResources(refKind)\n : isServiceAccount && loadServiceAccounts\n ? loadServiceAccounts\n : null;\n if (!loader) {\n setResources([]);\n return;\n }\n let cancelled = false;\n setResourcesLoading(true);\n loader()\n .then((res) => !cancelled && setResources(res))\n .finally(() => !cancelled && setResourcesLoading(false));\n return () => {\n cancelled = true;\n };\n }, [loadResources, loadServiceAccounts, refKind, isKeyed, isServiceAccount]);\n\n useEffect(() => {\n if (!isKeyed || !selectedName) {\n setPreviews([]);\n return;\n }\n let cancelled = false;\n setPreviewLoading(true);\n loadKeyPreview(refKind, selectedName)\n .then((res) => !cancelled && setPreviews(res))\n .catch(() => !cancelled && setPreviews([]))\n .finally(() => !cancelled && setPreviewLoading(false));\n return () => {\n cancelled = true;\n };\n }, [loadKeyPreview, refKind, selectedName, isKeyed]);\n\n const nameOptions = useMemo<ComboboxOption[]>(\n () => resources.map((r) => ({ value: r.name, label: r.name })),\n [resources],\n );\n const selectedResource = useMemo(\n () => resources.find((r) => r.name === selectedName),\n [resources, selectedName],\n );\n const keyOptions = useMemo(\n () => buildKeyOptions(selectedResource?.keys ?? [], previews, selectedKey),\n [selectedResource, selectedKey, previews],\n );\n\n // Strict validity: the name is invalid when it names no loaded resource; the\n // key is invalid when, with a resolved resource, it isn't one of that\n // resource's keys. Only names from a listed source are checked (a\n // serviceaccount source with no loader lists nothing, so it never flags).\n const listsNames = isKeyed || (isServiceAccount && !!loadServiceAccounts);\n const nameInvalid =\n strict &&\n listsNames &&\n !!selectedName &&\n !resourcesLoading &&\n !selectedResource;\n const keyInvalid =\n strict &&\n isKeyed &&\n !!selectedKey &&\n !!selectedResource &&\n !resourcesLoading &&\n !(selectedResource.keys ?? []).includes(selectedKey);\n\n const setSource = (next: SecretValueSource) => {\n switch (next) {\n case \"value\":\n return onChange({ kind: \"value\", value: \"\" });\n case \"onepassword\":\n return onChange({ kind: \"onepassword\", ref: \"\" });\n case \"serviceaccount\":\n return onChange({ kind: \"serviceaccount\", name: \"\" });\n default:\n return onChange({ kind: next, name: \"\", key: \"\" });\n }\n };\n const setName = (name: string) => {\n if (isServiceAccount)\n return onChange(name ? { kind: \"serviceaccount\", name } : undefined);\n return onChange(\n name ? { kind: refKind, name, key: selectedKey } : undefined,\n );\n };\n const setKey = (key: string) =>\n selectedName\n ? onChange({ kind: refKind, name: selectedName, key })\n : undefined;\n const setLiteral = (v: string) => onChange({ kind: \"value\", value: v });\n const setOpRef = (v: string) => onChange({ kind: \"onepassword\", ref: v });\n\n const sourceOptions = useMemo<ComboboxOption[]>(\n () =>\n sourceList.map((k) => ({\n value: k,\n label: SOURCE_LABEL[k],\n icon: createElement(SOURCE_ICON[k], { className: \"size-4\" }),\n })),\n [sourceList],\n );\n const sourceMenuOptions = useMemo<IconMenuOption<SecretValueSource>[]>(\n () =>\n sourceList.map((kind) => ({\n value: kind,\n label: SOURCE_LABEL[kind],\n icon: SOURCE_ICON[kind],\n })),\n [sourceList],\n );\n\n return (\n <div\n data-slot=\"secret-key-selector\"\n className={cn(\"@container w-full min-w-0\", className)}\n >\n <div\n className=\"flex w-full max-w-full min-w-0 flex-nowrap items-center gap-2\"\n data-slot=\"secret-fields\"\n >\n <div\n className={cn(SOURCE_FIELD, \"@max-md:hidden\")}\n data-slot=\"secret-source-combobox\"\n >\n <Combobox\n ariaLabel=\"Secret value source\"\n options={sourceOptions}\n value={source}\n onChange={(next) => setSource(next as SecretValueSource)}\n allowCustomValue={false}\n required\n />\n </div>\n <div\n className=\"hidden shrink-0 @max-md:!block\"\n data-slot=\"secret-source-menu\"\n >\n <IconMenuPicker\n value={source}\n onChange={setSource}\n options={sourceMenuOptions}\n ariaLabel=\"Secret value source\"\n triggerClassName=\"size-control-h rounded-md border border-input bg-background\"\n />\n </div>\n {isLiteral ? (\n <TextField\n value={literalValue}\n onChange={setLiteral}\n placeholder=\"Static value…\"\n ariaLabel=\"Static value\"\n />\n ) : isOnePassword ? (\n <TextField\n value={opRef}\n onChange={setOpRef}\n placeholder=\"op://vault/item/field\"\n ariaLabel=\"1Password reference\"\n mono\n />\n ) : isServiceAccount ? (\n <div\n className={VALUE_FIELD}\n data-slot=\"secret-serviceaccount-field\"\n >\n <Combobox\n options={nameOptions}\n value={selectedName}\n onChange={setName}\n allowCustomValue\n loading={resourcesLoading}\n invalid={nameInvalid}\n placeholder=\"Service account…\"\n />\n </div>\n ) : (\n <div\n className={cn(\n \"flex flex-nowrap items-center gap-2\",\n SHRINKABLE_FIELD,\n \"grow\",\n )}\n data-slot=\"secret-reference-fields\"\n >\n <div\n className={RESOURCE_FIELD}\n data-slot=\"secret-resource-field\"\n >\n <Combobox\n options={nameOptions}\n value={selectedName}\n onChange={setName}\n allowCustomValue\n loading={resourcesLoading}\n invalid={nameInvalid}\n placeholder={`Select ${refKind}…`}\n />\n </div>\n <div className={KEY_FIELD} data-slot=\"secret-key-field\">\n <Combobox\n options={keyOptions}\n value={selectedKey}\n onChange={setKey}\n allowCustomValue\n loading={previewLoading}\n invalid={keyInvalid}\n placeholder={selectedName ? keyPlaceholder(refKind) : \"—\"}\n />\n </div>\n </div>\n )}\n </div>\n </div>\n );\n}\n\n// TextField is the shared single-input surface for the literal and 1Password\n// sources (no resource list backs either).\nfunction TextField({\n value,\n onChange,\n placeholder,\n ariaLabel,\n mono,\n}: {\n value: string;\n onChange: (v: string) => void;\n placeholder: string;\n ariaLabel: string;\n mono?: boolean;\n}) {\n return (\n <div className={VALUE_FIELD} data-slot=\"secret-value-fields\">\n <input\n type=\"text\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n placeholder={placeholder}\n aria-label={ariaLabel}\n className={cn(\n \"h-control-h w-full min-w-0 rounded border border-input bg-background px-control-px text-sm\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n mono && \"font-mono\",\n )}\n />\n </div>\n );\n}\n\n// isRefKind narrows to the keyed variants that carry both a name and a key.\nfunction isRefKind(\n v: SecretKeyValue,\n): v is { kind: SecretKind; name: string; key: string } {\n return v.kind === \"secret\" || v.kind === \"configmap\" || v.kind === \"helm\";\n}\n\n// keyPlaceholder hints the key field: helm keys are jsonpath expressions into\n// the merged release values, the others are literal data keys.\nfunction keyPlaceholder(kind: SecretKind): string {\n return kind === \"helm\" ? \"jsonpath…\" : \"Key…\";\n}\n\n// Each source's leading glyph. The flanksource FCs carry static metadata that\n// doesn't unify with ComponentType, so type the slot as a plain render function.\nconst SOURCE_ICON: Record<SecretValueSource, StaticIconComponent> = {\n secret: K8SSecret,\n configmap: K8SConfigmap,\n helm: Helm,\n serviceaccount: K8SServiceaccount,\n onepassword: Vault,\n value: UiEdit,\n};\n\nconst SOURCE_LABEL: Record<SecretValueSource, string> = {\n secret: \"Secret\",\n configmap: \"ConfigMap\",\n helm: \"Helm\",\n serviceaccount: \"Service Account\",\n onepassword: \"1Password\",\n value: \"Value\",\n};\n\n// buildKeyOptions labels each key with its mid-masked preview value when one is\n// available. A selected key absent from the list is pinned so the selection\n// stays visible.\nfunction buildKeyOptions(\n keys: string[],\n previews: KeyPreview[],\n selectedKey: string,\n): ComboboxOption[] {\n const masked = new Map(previews.map((p) => [p.key, p.value]));\n const base = keys.map((k) => ({\n value: k,\n label: masked.has(k) ? `${k} — ${masked.get(k)}` : k,\n }));\n if (selectedKey && !base.some((o) => o.value === selectedKey)) {\n return [{ value: selectedKey, label: selectedKey }, ...base];\n }\n return base;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SecretKeySelector.js","sources":["../../src/components/SecretKeySelector.tsx"],"sourcesContent":["import { createElement, useEffect, useMemo, useState } from \"react\";\nimport {\n K8SSecret,\n K8SConfigmap,\n Helm,\n K8SServiceaccount,\n Vault,\n} from \"@flanksource/icons/mi\";\nimport { type StaticIconComponent } from \"../data/Icon\";\nimport { cn } from \"../lib/utils\";\nimport { UiEdit } from \"../icons\";\nimport { Combobox, type ComboboxOption } from \"./Combobox\";\nimport { IconMenuPicker, type IconMenuOption } from \"./icon-menu-picker\";\nimport { OnePasswordSelector } from \"./OnePasswordSelector\";\nimport type { OnePasswordLoaders } from \"./OnePasswordSelector.model\";\n\n// SecretKeySelector picks how a credential is sourced and lowers the choice into\n// a single reference string the consumer persists. It supports Kubernetes\n// Secrets and ConfigMaps (name + key), Helm release values (name + jsonpath\n// key), a service-account token (name only), a 1Password reference\n// (op://vault/item/field), and a static inline \"Value\". It is presentational:\n// it fetches nothing — the consumer supplies async `loadResources` /\n// `loadKeyPreview` (for keyed kinds) and optional `loadServiceAccounts` getters.\n\n/** Kinds that reference a named resource plus one of its keys. */\nexport type SecretKind = \"secret\" | \"configmap\" | \"helm\";\n\n/** Every source the picker can emit, including the non-keyed ones. */\nexport type SecretValueSource =\n | SecretKind\n | \"serviceaccount\"\n | \"onepassword\"\n | \"value\";\n\n/** One key's mid-masked preview. `value` is already masked by the consumer. */\nexport type KeyPreview = { key: string; value: string };\n\n/**\n * The selector's value: a {kind,name,key} reference into a Secret / ConfigMap /\n * Helm release, a service-account token by name, a 1Password op:// reference, or\n * a static inline literal.\n */\nexport type SecretKeyValue =\n | { kind: SecretKind; name: string; key: string }\n | { kind: \"serviceaccount\"; name: string }\n | { kind: \"onepassword\"; ref: string }\n | { kind: \"value\"; value: string };\n\n/** A named secret/configmap/helm-release and its data key names (values never returned). */\nexport type SecretResource = { name: string; keys?: string[] };\n\nexport type SecretKeySelectorProps = {\n value: SecretKeyValue | undefined;\n onChange: (next: SecretKeyValue | undefined) => void;\n /** Loads the named keyed kind's resources (name + data key names). */\n loadResources: (kind: SecretKind) => Promise<SecretResource[]>;\n /** Loads mid-masked previews for the named keyed resource's keys. */\n loadKeyPreview: (kind: SecretKind, name: string) => Promise<KeyPreview[]>;\n /**\n * Loads the service-account names (no keys) for the \"serviceaccount\" source.\n * When omitted, that source's name field accepts freeform entry only.\n */\n loadServiceAccounts?: () => Promise<SecretResource[]>;\n /** Loads metadata for the cascading 1Password vault, item, and field pickers. */\n onePassword?: OnePasswordLoaders;\n /**\n * The sources offered in the picker, in order. Defaults to\n * `[\"secret\", \"configmap\", \"value\"]` (or drops \"value\" when `allowLiteral` is\n * false). Pass an explicit list to offer helm / serviceaccount / onepassword.\n */\n sources?: SecretValueSource[];\n /**\n * Shorthand honoured only when `sources` is not supplied: `false` drops the\n * inline \"Value\" source, leaving Secret/ConfigMap. Ignored when `sources` is\n * given.\n */\n allowLiteral?: boolean;\n /**\n * When true, a chosen name absent from the loaded resources, or a key absent\n * from the chosen keyed resource's keys, is flagged invalid (once the load\n * settles). The onepassword and literal sources are never strict-flagged.\n */\n strict?: boolean;\n className?: string;\n};\n\nconst DEFAULT_SOURCES: SecretValueSource[] = [\"secret\", \"configmap\", \"value\"];\n\nconst SHRINKABLE_FIELD = \"max-w-full min-w-0 shrink\";\nconst SOURCE_FIELD = `${SHRINKABLE_FIELD} basis-40 grow-0`;\nconst RESOURCE_FIELD = `${SHRINKABLE_FIELD} basis-40 grow-0`;\nconst KEY_FIELD = `${SHRINKABLE_FIELD} basis-72 grow`;\nconst VALUE_FIELD = `${SHRINKABLE_FIELD} basis-64 grow`;\n\nexport function SecretKeySelector({\n value,\n onChange,\n loadResources,\n loadKeyPreview,\n loadServiceAccounts,\n onePassword,\n sources,\n allowLiteral = true,\n strict = false,\n className,\n}: SecretKeySelectorProps) {\n const sourceList = useMemo<SecretValueSource[]>(\n () => sources ?? (allowLiteral ? DEFAULT_SOURCES : [\"secret\", \"configmap\"]),\n [sources, allowLiteral],\n );\n\n const source: SecretValueSource = value?.kind ?? sourceList[0] ?? \"secret\";\n const isKeyed =\n source === \"secret\" || source === \"configmap\" || source === \"helm\";\n const isServiceAccount = source === \"serviceaccount\";\n const isOnePassword = source === \"onepassword\";\n const isLiteral = source === \"value\";\n // A keyed source's refKind drives the loaders; other sources default to \"secret\".\n const refKind: SecretKind = isKeyed ? (source as SecretKind) : \"secret\";\n\n const selectedName =\n value && (value.kind === \"serviceaccount\" || isRefKind(value))\n ? value.name\n : \"\";\n const selectedKey = value && isRefKind(value) ? value.key : \"\";\n const literalValue = value && value.kind === \"value\" ? value.value : \"\";\n const opRef = value && value.kind === \"onepassword\" ? value.ref : \"\";\n\n const [resources, setResources] = useState<SecretResource[]>([]);\n const [resourcesLoading, setResourcesLoading] = useState(false);\n const [previews, setPreviews] = useState<KeyPreview[]>([]);\n const [previewLoading, setPreviewLoading] = useState(false);\n\n // Load the name list for keyed kinds (secret/configmap/helm) and, when a\n // loader is supplied, for the serviceaccount source. Other sources list nothing.\n useEffect(() => {\n const loader = isKeyed\n ? () => loadResources(refKind)\n : isServiceAccount && loadServiceAccounts\n ? loadServiceAccounts\n : null;\n if (!loader) {\n setResources([]);\n return;\n }\n let cancelled = false;\n setResourcesLoading(true);\n loader()\n .then((res) => !cancelled && setResources(res))\n .finally(() => !cancelled && setResourcesLoading(false));\n return () => {\n cancelled = true;\n };\n }, [loadResources, loadServiceAccounts, refKind, isKeyed, isServiceAccount]);\n\n useEffect(() => {\n if (!isKeyed || !selectedName) {\n setPreviews([]);\n return;\n }\n let cancelled = false;\n setPreviewLoading(true);\n loadKeyPreview(refKind, selectedName)\n .then((res) => !cancelled && setPreviews(res))\n .catch(() => !cancelled && setPreviews([]))\n .finally(() => !cancelled && setPreviewLoading(false));\n return () => {\n cancelled = true;\n };\n }, [loadKeyPreview, refKind, selectedName, isKeyed]);\n\n const nameOptions = useMemo<ComboboxOption[]>(\n () => resources.map((r) => ({ value: r.name, label: r.name })),\n [resources],\n );\n const selectedResource = useMemo(\n () => resources.find((r) => r.name === selectedName),\n [resources, selectedName],\n );\n const keyOptions = useMemo(\n () => buildKeyOptions(selectedResource?.keys ?? [], previews, selectedKey),\n [selectedResource, selectedKey, previews],\n );\n\n // Strict validity: the name is invalid when it names no loaded resource; the\n // key is invalid when, with a resolved resource, it isn't one of that\n // resource's keys. Only names from a listed source are checked (a\n // serviceaccount source with no loader lists nothing, so it never flags).\n const listsNames = isKeyed || (isServiceAccount && !!loadServiceAccounts);\n const nameInvalid =\n strict &&\n listsNames &&\n !!selectedName &&\n !resourcesLoading &&\n !selectedResource;\n const keyInvalid =\n strict &&\n isKeyed &&\n !!selectedKey &&\n !!selectedResource &&\n !resourcesLoading &&\n !(selectedResource.keys ?? []).includes(selectedKey);\n\n const setSource = (next: SecretValueSource) => {\n switch (next) {\n case \"value\":\n return onChange({ kind: \"value\", value: \"\" });\n case \"onepassword\":\n return onChange({ kind: \"onepassword\", ref: \"\" });\n case \"serviceaccount\":\n return onChange({ kind: \"serviceaccount\", name: \"\" });\n default:\n return onChange({ kind: next, name: \"\", key: \"\" });\n }\n };\n const setName = (name: string) => {\n if (isServiceAccount)\n return onChange(name ? { kind: \"serviceaccount\", name } : undefined);\n return onChange(\n name ? { kind: refKind, name, key: selectedKey } : undefined,\n );\n };\n const setKey = (key: string) =>\n selectedName\n ? onChange({ kind: refKind, name: selectedName, key })\n : undefined;\n const setLiteral = (v: string) => onChange({ kind: \"value\", value: v });\n const setOpRef = (ref: string) => onChange({ kind: \"onepassword\", ref });\n\n const sourceOptions = useMemo<ComboboxOption[]>(\n () =>\n sourceList.map((k) => ({\n value: k,\n label: SOURCE_LABEL[k],\n icon: createElement(SOURCE_ICON[k], { className: \"size-4\" }),\n })),\n [sourceList],\n );\n const sourceMenuOptions = useMemo<IconMenuOption<SecretValueSource>[]>(\n () =>\n sourceList.map((kind) => ({\n value: kind,\n label: SOURCE_LABEL[kind],\n icon: SOURCE_ICON[kind],\n })),\n [sourceList],\n );\n\n return (\n <div\n data-slot=\"secret-key-selector\"\n className={cn(\"@container w-full min-w-0\", className)}\n >\n <div\n className=\"flex w-full max-w-full min-w-0 flex-nowrap items-center gap-2\"\n data-slot=\"secret-fields\"\n >\n <div\n className={cn(SOURCE_FIELD, \"@max-md:hidden\")}\n data-slot=\"secret-source-combobox\"\n >\n <Combobox\n ariaLabel=\"Secret value source\"\n options={sourceOptions}\n value={source}\n onChange={(next) => setSource(next as SecretValueSource)}\n allowCustomValue={false}\n required\n />\n </div>\n <div\n className=\"hidden shrink-0 @max-md:!block\"\n data-slot=\"secret-source-menu\"\n >\n <IconMenuPicker\n value={source}\n onChange={setSource}\n options={sourceMenuOptions}\n ariaLabel=\"Secret value source\"\n triggerClassName=\"size-control-h rounded-md border border-input bg-background\"\n />\n </div>\n {isLiteral ? (\n <TextField\n value={literalValue}\n onChange={setLiteral}\n placeholder=\"Static value…\"\n ariaLabel=\"Static value\"\n />\n ) : isOnePassword ? (\n (() => {\n if (!onePassword) {\n throw new Error(\n \"SecretKeySelector onepassword source requires onePassword loaders\",\n );\n }\n return (\n <div className={VALUE_FIELD} data-slot=\"secret-onepassword-field\">\n <OnePasswordSelector\n value={opRef}\n onChange={setOpRef}\n {...onePassword}\n allowCustomValue\n />\n </div>\n );\n })()\n ) : isServiceAccount ? (\n <div\n className={VALUE_FIELD}\n data-slot=\"secret-serviceaccount-field\"\n >\n <Combobox\n options={nameOptions}\n value={selectedName}\n onChange={setName}\n allowCustomValue\n loading={resourcesLoading}\n invalid={nameInvalid}\n placeholder=\"Service account…\"\n />\n </div>\n ) : (\n <div\n className={cn(\n \"flex flex-nowrap items-center gap-2\",\n SHRINKABLE_FIELD,\n \"grow\",\n )}\n data-slot=\"secret-reference-fields\"\n >\n <div\n className={RESOURCE_FIELD}\n data-slot=\"secret-resource-field\"\n >\n <Combobox\n options={nameOptions}\n value={selectedName}\n onChange={setName}\n allowCustomValue\n loading={resourcesLoading}\n invalid={nameInvalid}\n placeholder={`Select ${refKind}…`}\n />\n </div>\n <div className={KEY_FIELD} data-slot=\"secret-key-field\">\n <Combobox\n options={keyOptions}\n value={selectedKey}\n onChange={setKey}\n allowCustomValue\n loading={previewLoading}\n invalid={keyInvalid}\n placeholder={selectedName ? keyPlaceholder(refKind) : \"—\"}\n />\n </div>\n </div>\n )}\n </div>\n </div>\n );\n}\n\nfunction TextField({\n value,\n onChange,\n placeholder,\n ariaLabel,\n}: {\n value: string;\n onChange: (v: string) => void;\n placeholder: string;\n ariaLabel: string;\n}) {\n return (\n <div className={VALUE_FIELD} data-slot=\"secret-value-fields\">\n <input\n type=\"text\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n placeholder={placeholder}\n aria-label={ariaLabel}\n className={cn(\n \"h-control-h w-full min-w-0 rounded border border-input bg-background px-control-px text-sm\",\n \"outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n )}\n />\n </div>\n );\n}\n\n// isRefKind narrows to the keyed variants that carry both a name and a key.\nfunction isRefKind(\n v: SecretKeyValue,\n): v is { kind: SecretKind; name: string; key: string } {\n return v.kind === \"secret\" || v.kind === \"configmap\" || v.kind === \"helm\";\n}\n\n// keyPlaceholder hints the key field: helm keys are jsonpath expressions into\n// the merged release values, the others are literal data keys.\nfunction keyPlaceholder(kind: SecretKind): string {\n return kind === \"helm\" ? \"jsonpath…\" : \"Key…\";\n}\n\n// Each source's leading glyph. The flanksource FCs carry static metadata that\n// doesn't unify with ComponentType, so type the slot as a plain render function.\nconst SOURCE_ICON: Record<SecretValueSource, StaticIconComponent> = {\n secret: K8SSecret,\n configmap: K8SConfigmap,\n helm: Helm,\n serviceaccount: K8SServiceaccount,\n onepassword: Vault,\n value: UiEdit,\n};\n\nconst SOURCE_LABEL: Record<SecretValueSource, string> = {\n secret: \"Secret\",\n configmap: \"ConfigMap\",\n helm: \"Helm\",\n serviceaccount: \"Service Account\",\n onepassword: \"1Password\",\n value: \"Value\",\n};\n\n// buildKeyOptions labels each key with its mid-masked preview value when one is\n// available. A selected key absent from the list is pinned so the selection\n// stays visible.\nfunction buildKeyOptions(\n keys: string[],\n previews: KeyPreview[],\n selectedKey: string,\n): ComboboxOption[] {\n const masked = new Map(previews.map((p) => [p.key, p.value]));\n const base = keys.map((k) => ({\n value: k,\n label: masked.has(k) ? `${k} — ${masked.get(k)}` : k,\n }));\n if (selectedKey && !base.some((o) => o.value === selectedKey)) {\n return [{ value: selectedKey, label: selectedKey }, ...base];\n }\n return base;\n}\n"],"names":[],"mappings":"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"}
@@ -10,6 +10,9 @@ const modalStack = require("../overlay/modalStack.cjs");
10
10
  const jsonSchemaFormSize = require("./json-schema-form-size.cjs");
11
11
  const UiChevronDown = require("../icons/components/UiChevronDown.cjs");
12
12
  const PANEL_MAX_WIDTH_PX = 480;
13
+ const PANEL_MAX_HEIGHT_PX = 480;
14
+ const PANEL_MIN_HEIGHT_PX = 200;
15
+ const VIEWPORT_MARGIN_PX = 8;
13
16
  function TreePickerField({
14
17
  roots,
15
18
  getKey,
@@ -31,12 +34,12 @@ function TreePickerField({
31
34
  className,
32
35
  triggerClassName,
33
36
  panelClassName,
34
- renderTrigger
37
+ renderTrigger,
38
+ renderFooter
35
39
  }) {
36
40
  const [open, setOpen] = react.useState(false);
37
- const [pos, setPos] = react.useState(
38
- null
39
- );
41
+ const floatingZ = modalStack.useFloatingZIndex();
42
+ const [pos, setPos] = react.useState(null);
40
43
  const rootRef = react.useRef(null);
41
44
  const anchorRef = react.useRef(null);
42
45
  const panelRef = react.useRef(null);
@@ -54,9 +57,20 @@ function TreePickerField({
54
57
  const anchor = rootRef.current;
55
58
  if (!anchor) return;
56
59
  const rect = anchor.getBoundingClientRect();
57
- const viewportCap = window.innerWidth - rect.left - 8;
60
+ const viewportCap = window.innerWidth - rect.left - VIEWPORT_MARGIN_PX;
58
61
  const maxWidth = Math.max(rect.width, Math.min(PANEL_MAX_WIDTH_PX, viewportCap));
59
- setPos({ top: rect.bottom + 4, left: rect.left, width: rect.width, maxWidth });
62
+ const below = window.innerHeight - rect.bottom - VIEWPORT_MARGIN_PX - 4;
63
+ const above = rect.top - VIEWPORT_MARGIN_PX - 4;
64
+ const flip = below < PANEL_MIN_HEIGHT_PX && above > below;
65
+ const available = Math.max(flip ? above : below, PANEL_MIN_HEIGHT_PX);
66
+ const maxHeight = Math.min(PANEL_MAX_HEIGHT_PX, available);
67
+ setPos({
68
+ top: flip ? Math.max(VIEWPORT_MARGIN_PX, rect.top - 4 - maxHeight) : rect.bottom + 4,
69
+ left: rect.left,
70
+ width: rect.width,
71
+ maxWidth,
72
+ maxHeight
73
+ });
60
74
  };
61
75
  update();
62
76
  window.addEventListener("scroll", update, true);
@@ -110,7 +124,7 @@ function TreePickerField({
110
124
  }
111
125
  ),
112
126
  open && pos && reactDom.createPortal(
113
- /* @__PURE__ */ jsxRuntime.jsx(
127
+ /* @__PURE__ */ jsxRuntime.jsxs(
114
128
  "div",
115
129
  {
116
130
  ref: panelRef,
@@ -119,39 +133,51 @@ function TreePickerField({
119
133
  position: "fixed",
120
134
  top: pos.top,
121
135
  left: pos.left,
136
+ // Computed rather than constant: the popup has to clear whatever
137
+ // modal is open, and a modal sits at zIndex.modal (10000). A
138
+ // hardcoded z-50 renders the tree behind any dialog it is opened
139
+ // in — see ../overlay/zIndex.
140
+ zIndex: floatingZ,
122
141
  // Grow to fit the widest row/toolbar, but never narrower than the
123
142
  // trigger nor wider than maxWidth — so labels show in full and the
124
143
  // toolbar's intrinsic-width search input never forces a scrollbar.
125
144
  minWidth: pos.width,
126
145
  width: "max-content",
127
- maxWidth: pos.maxWidth
146
+ maxWidth: pos.maxWidth,
147
+ maxHeight: pos.maxHeight
128
148
  },
129
149
  className: utils.cn(
130
- "z-50 flex max-h-[60vh] flex-col overflow-hidden rounded-md border border-border bg-popover shadow-md",
150
+ // Height comes from the measured maxHeight above, not a viewport
151
+ // fraction: 60vh is the same number wherever the trigger sits, and
152
+ // a trigger low on the page needs a shorter panel, not an equal one.
153
+ "flex flex-col overflow-hidden rounded-md border border-border bg-popover shadow-md",
131
154
  panelClassName
132
155
  ),
133
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: /* @__PURE__ */ jsxRuntime.jsx(
134
- Tree.Tree,
135
- {
136
- roots,
137
- getKey,
138
- getChildren,
139
- renderRow,
140
- ...getSearchText ? { getSearchText } : {},
141
- ...defaultOpen ? { defaultOpen } : {},
142
- ...selected !== void 0 ? { selected } : {},
143
- ...revealSelected !== void 0 ? { revealSelected } : {},
144
- ...showControls !== void 0 ? { showControls } : {},
145
- ...ariaLabel !== void 0 ? { ariaLabel } : {},
146
- ...empty !== void 0 ? { empty } : {},
147
- onSelect: (node) => {
148
- if (!isSelectable || isSelectable(node)) {
149
- onSelect(node);
150
- setOpen(false);
156
+ children: [
157
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: /* @__PURE__ */ jsxRuntime.jsx(
158
+ Tree.Tree,
159
+ {
160
+ roots,
161
+ getKey,
162
+ getChildren,
163
+ renderRow,
164
+ ...getSearchText ? { getSearchText } : {},
165
+ ...defaultOpen ? { defaultOpen } : {},
166
+ ...selected !== void 0 ? { selected } : {},
167
+ ...revealSelected !== void 0 ? { revealSelected } : {},
168
+ ...showControls !== void 0 ? { showControls } : {},
169
+ ...ariaLabel !== void 0 ? { ariaLabel } : {},
170
+ ...empty !== void 0 ? { empty } : {},
171
+ onSelect: (node) => {
172
+ if (!isSelectable || isSelectable(node)) {
173
+ onSelect(node);
174
+ setOpen(false);
175
+ }
151
176
  }
152
177
  }
153
- }
154
- ) })
178
+ ) }),
179
+ renderFooter && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "shrink-0 border-t border-border bg-popover", children: renderFooter({ close: () => setOpen(false) }) })
180
+ ]
155
181
  }
156
182
  ),
157
183
  document.body
@@ -1 +1 @@
1
- {"version":3,"file":"TreePickerField.cjs","sources":["../../src/components/TreePickerField.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState, type ReactNode, type Ref } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { UiChevronDown } from \"../icons\";\nimport { Tree, type TreeProps } from \"../data/Tree\";\nimport { useEscapeLayer } from \"../overlay/modalStack\";\nimport { inputSizeClass, type FormSize } from \"./json-schema-form-size\";\n\n// Upper bound the open panel grows to so wide trees show full labels before\n// truncating; the panel is never narrower than the trigger.\nconst PANEL_MAX_WIDTH_PX = 480;\n\nexport interface TreePickerFieldProps<T> {\n /** Root nodes of the tree to browse. */\n roots: T[];\n getKey: (node: T) => string | number;\n getChildren: (node: T) => T[] | undefined;\n renderRow: TreeProps<T>[\"renderRow\"];\n getSearchText?: (node: T) => string;\n defaultOpen?: (node: T, depth: number) => boolean;\n /**\n * Decides which nodes commit a selection when clicked. A click on a\n * non-selectable node still toggles its expansion (it never closes the\n * picker). Defaults to every node selectable.\n */\n isSelectable?: (node: T) => boolean;\n /** Fires for a selectable node, then closes the dropdown. */\n onSelect: (node: T) => void;\n /** Highlights the committed node inside the open tree. */\n selected?: T | null;\n /**\n * Force-opens the `selected` node's ancestors so the current value is always\n * visible when the panel opens, however deep its branch. Requires `selected`.\n */\n revealSelected?: boolean;\n /** Renders the tree's expand/collapse + filter toolbar. */\n showControls?: boolean;\n /** Accessible name for the tree inside the panel. */\n ariaLabel?: string;\n /** Empty-state content when `roots` is empty. */\n empty?: ReactNode;\n /** Current-selection display in the trigger; falls back to `placeholder`. */\n label?: ReactNode;\n placeholder?: string;\n disabled?: boolean;\n size?: FormSize;\n className?: string;\n triggerClassName?: string;\n panelClassName?: string;\n renderTrigger?: (props: TreePickerTriggerProps) => ReactNode;\n}\n\nexport interface TreePickerTriggerProps {\n open: boolean;\n disabled: boolean;\n triggerRef: Ref<HTMLButtonElement>;\n toggle: () => void;\n}\n\n// TreePickerField is a form-field-styled trigger (matching Combobox's closed\n// input) that opens a portal-anchored dropdown holding a Tree. The dropdown is\n// positioned with fixed coordinates measured from the trigger so it escapes any\n// overflow-hidden / scroll ancestor (e.g. a modal body). A click on a selectable\n// node commits and closes; a non-selectable node only toggles expansion.\nexport function TreePickerField<T>({\n roots,\n getKey,\n getChildren,\n renderRow,\n getSearchText,\n defaultOpen,\n isSelectable,\n onSelect,\n selected,\n revealSelected,\n showControls,\n ariaLabel,\n empty,\n label,\n placeholder = \"Select…\",\n disabled,\n size,\n className,\n triggerClassName,\n panelClassName,\n renderTrigger,\n}: TreePickerFieldProps<T>) {\n const [open, setOpen] = useState(false);\n const [pos, setPos] = useState<{ top: number; left: number; width: number; maxWidth: number } | null>(\n null,\n );\n const rootRef = useRef<HTMLDivElement>(null);\n const anchorRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n\n useEscapeLayer(open, () => {\n setOpen(false);\n anchorRef.current?.focus();\n });\n\n // Position the portaled panel from the trigger rect, repositioning while open.\n useLayoutEffect(() => {\n if (!open) {\n setPos(null);\n return;\n }\n const update = () => {\n const anchor = rootRef.current;\n if (!anchor) return;\n const rect = anchor.getBoundingClientRect();\n const viewportCap = window.innerWidth - rect.left - 8;\n const maxWidth = Math.max(rect.width, Math.min(PANEL_MAX_WIDTH_PX, viewportCap));\n setPos({ 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 // Dismiss on an outside pointer-down (the trigger and panel are exempt).\n useEffect(() => {\n if (!open) return;\n const onPointerDown = (e: MouseEvent) => {\n const target = e.target as Node;\n if (!rootRef.current?.contains(target) && !panelRef.current?.contains(target)) {\n setOpen(false);\n }\n };\n document.addEventListener(\"mousedown\", onPointerDown);\n return () => document.removeEventListener(\"mousedown\", onPointerDown);\n }, [open]);\n\n return (\n <div ref={rootRef} className={cn(\"relative\", className)}>\n {renderTrigger ? (\n renderTrigger({\n open,\n disabled: Boolean(disabled),\n triggerRef: anchorRef,\n toggle: () => {\n if (!disabled) setOpen((current) => !current);\n },\n })\n ) : (\n <button\n ref={anchorRef}\n type=\"button\"\n disabled={disabled}\n aria-haspopup=\"tree\"\n aria-expanded={open}\n onClick={() => setOpen((current) => !current)}\n className={cn(\n \"relative flex w-full items-center rounded-md border border-input bg-background text-left text-foreground\",\n size ? inputSizeClass[size] : \"h-control-h px-control-px text-sm\",\n \"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 triggerClassName,\n )}\n >\n <span className={cn(\"min-w-0 flex-1 truncate\", label == null && \"text-muted-foreground\")}>\n {label ?? placeholder}\n </span>\n <span className=\"pointer-events-none absolute right-0 flex h-full items-center px-2 text-muted-foreground\">\n <Icon icon={UiChevronDown} className=\"text-xs\" />\n </span>\n </button>\n )}\n\n {open &&\n pos &&\n createPortal(\n <div\n ref={panelRef}\n data-slot=\"tree-picker-popup\"\n style={{\n position: \"fixed\",\n top: pos.top,\n left: pos.left,\n // Grow to fit the widest row/toolbar, but never narrower than the\n // trigger nor wider than maxWidth — so labels show in full and the\n // toolbar's intrinsic-width search input never forces a scrollbar.\n minWidth: pos.width,\n width: \"max-content\",\n maxWidth: pos.maxWidth,\n }}\n className={cn(\n \"z-50 flex max-h-[60vh] flex-col overflow-hidden rounded-md border border-border bg-popover shadow-md\",\n panelClassName,\n )}\n >\n <div className=\"min-h-0 flex-1 overflow-auto\">\n <Tree<T>\n roots={roots}\n getKey={getKey}\n getChildren={getChildren}\n renderRow={renderRow}\n {...(getSearchText ? { getSearchText } : {})}\n {...(defaultOpen ? { defaultOpen } : {})}\n {...(selected !== undefined ? { selected } : {})}\n {...(revealSelected !== undefined ? { revealSelected } : {})}\n {...(showControls !== undefined ? { showControls } : {})}\n {...(ariaLabel !== undefined ? { ariaLabel } : {})}\n {...(empty !== undefined ? { empty } : {})}\n onSelect={(node) => {\n if (!isSelectable || isSelectable(node)) {\n onSelect(node);\n setOpen(false);\n }\n }}\n />\n </div>\n </div>,\n document.body,\n )}\n </div>\n );\n}\n"],"names":["useState","useRef","useEscapeLayer","useLayoutEffect","useEffect","jsxs","cn","inputSizeClass","jsx","Icon","UiChevronDown","createPortal","Tree"],"mappings":"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"}
1
+ {"version":3,"file":"TreePickerField.cjs","sources":["../../src/components/TreePickerField.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState, type ReactNode, type Ref } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { UiChevronDown } from \"../icons\";\nimport { Tree, type TreeProps } from \"../data/Tree\";\nimport { useEscapeLayer, useFloatingZIndex } from \"../overlay/modalStack\";\nimport { inputSizeClass, type FormSize } from \"./json-schema-form-size\";\n\n// Upper bound the open panel grows to so wide trees show full labels before\n// truncating; the panel is never narrower than the trigger.\nconst PANEL_MAX_WIDTH_PX = 480;\n// Tallest the panel gets when the viewport allows it.\nconst PANEL_MAX_HEIGHT_PX = 480;\n// Below this there is not enough room to be worth opening downward, so the panel\n// flips above the trigger instead.\nconst PANEL_MIN_HEIGHT_PX = 200;\n// Breathing room kept between the panel and the viewport edge.\nconst VIEWPORT_MARGIN_PX = 8;\n\nexport interface TreePickerFieldProps<T> {\n /** Root nodes of the tree to browse. */\n roots: T[];\n getKey: (node: T) => string | number;\n getChildren: (node: T) => T[] | undefined;\n renderRow: TreeProps<T>[\"renderRow\"];\n getSearchText?: (node: T) => string;\n defaultOpen?: (node: T, depth: number) => boolean;\n /**\n * Decides which nodes commit a selection when clicked. A click on a\n * non-selectable node still toggles its expansion (it never closes the\n * picker). Defaults to every node selectable.\n */\n isSelectable?: (node: T) => boolean;\n /** Fires for a selectable node, then closes the dropdown. */\n onSelect: (node: T) => void;\n /** Highlights the committed node inside the open tree. */\n selected?: T | null;\n /**\n * Force-opens the `selected` node's ancestors so the current value is always\n * visible when the panel opens, however deep its branch. Requires `selected`.\n */\n revealSelected?: boolean;\n /** Renders the tree's expand/collapse + filter toolbar. */\n showControls?: boolean;\n /** Accessible name for the tree inside the panel. */\n ariaLabel?: string;\n /** Empty-state content when `roots` is empty. */\n empty?: ReactNode;\n /** Current-selection display in the trigger; falls back to `placeholder`. */\n label?: ReactNode;\n placeholder?: string;\n disabled?: boolean;\n size?: FormSize;\n className?: string;\n triggerClassName?: string;\n panelClassName?: string;\n renderTrigger?: (props: TreePickerTriggerProps) => ReactNode;\n /**\n * Sticky row pinned below the tree inside the open panel — an escape hatch\n * from a dropdown that has run out of room (a fuller browser, a \"create new\")\n * rather than another node. It is handed `close` because acting on it almost\n * always means leaving the dropdown behind.\n */\n renderFooter?: (props: { close: () => void }) => ReactNode;\n}\n\nexport interface TreePickerTriggerProps {\n open: boolean;\n disabled: boolean;\n triggerRef: Ref<HTMLButtonElement>;\n toggle: () => void;\n}\n\n// TreePickerField is a form-field-styled trigger (matching Combobox's closed\n// input) that opens a portal-anchored dropdown holding a Tree. The dropdown is\n// positioned with fixed coordinates measured from the trigger so it escapes any\n// overflow-hidden / scroll ancestor (e.g. a modal body). A click on a selectable\n// node commits and closes; a non-selectable node only toggles expansion.\nexport function TreePickerField<T>({\n roots,\n getKey,\n getChildren,\n renderRow,\n getSearchText,\n defaultOpen,\n isSelectable,\n onSelect,\n selected,\n revealSelected,\n showControls,\n ariaLabel,\n empty,\n label,\n placeholder = \"Select…\",\n disabled,\n size,\n className,\n triggerClassName,\n panelClassName,\n renderTrigger,\n renderFooter,\n}: TreePickerFieldProps<T>) {\n const [open, setOpen] = useState(false);\n const floatingZ = useFloatingZIndex();\n const [pos, setPos] = useState<\n { top: number; left: number; width: number; maxWidth: number; maxHeight: number } | null\n >(null);\n const rootRef = useRef<HTMLDivElement>(null);\n const anchorRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n\n useEscapeLayer(open, () => {\n setOpen(false);\n anchorRef.current?.focus();\n });\n\n // Position the portaled panel from the trigger rect, repositioning while open.\n useLayoutEffect(() => {\n if (!open) {\n setPos(null);\n return;\n }\n const update = () => {\n const anchor = rootRef.current;\n if (!anchor) return;\n const rect = anchor.getBoundingClientRect();\n const viewportCap = window.innerWidth - rect.left - VIEWPORT_MARGIN_PX;\n const maxWidth = Math.max(rect.width, Math.min(PANEL_MAX_WIDTH_PX, viewportCap));\n // Fit the panel to the room the trigger actually has, and flip it above\n // when there is more room up there. Without this the panel keeps its full\n // height wherever the trigger sits, so a trigger low on the page pushes\n // the panel's bottom past the viewport — taking the sticky footer, and\n // any escape hatch pinned to it, out of reach entirely.\n const below = window.innerHeight - rect.bottom - VIEWPORT_MARGIN_PX - 4;\n const above = rect.top - VIEWPORT_MARGIN_PX - 4;\n const flip = below < PANEL_MIN_HEIGHT_PX && above > below;\n const available = Math.max(flip ? above : below, PANEL_MIN_HEIGHT_PX);\n const maxHeight = Math.min(PANEL_MAX_HEIGHT_PX, available);\n setPos({\n top: flip ? Math.max(VIEWPORT_MARGIN_PX, rect.top - 4 - maxHeight) : rect.bottom + 4,\n left: rect.left,\n width: rect.width,\n maxWidth,\n maxHeight,\n });\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 // Dismiss on an outside pointer-down (the trigger and panel are exempt).\n useEffect(() => {\n if (!open) return;\n const onPointerDown = (e: MouseEvent) => {\n const target = e.target as Node;\n if (!rootRef.current?.contains(target) && !panelRef.current?.contains(target)) {\n setOpen(false);\n }\n };\n document.addEventListener(\"mousedown\", onPointerDown);\n return () => document.removeEventListener(\"mousedown\", onPointerDown);\n }, [open]);\n\n return (\n <div ref={rootRef} className={cn(\"relative\", className)}>\n {renderTrigger ? (\n renderTrigger({\n open,\n disabled: Boolean(disabled),\n triggerRef: anchorRef,\n toggle: () => {\n if (!disabled) setOpen((current) => !current);\n },\n })\n ) : (\n <button\n ref={anchorRef}\n type=\"button\"\n disabled={disabled}\n aria-haspopup=\"tree\"\n aria-expanded={open}\n onClick={() => setOpen((current) => !current)}\n className={cn(\n \"relative flex w-full items-center rounded-md border border-input bg-background text-left text-foreground\",\n size ? inputSizeClass[size] : \"h-control-h px-control-px text-sm\",\n \"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 triggerClassName,\n )}\n >\n <span className={cn(\"min-w-0 flex-1 truncate\", label == null && \"text-muted-foreground\")}>\n {label ?? placeholder}\n </span>\n <span className=\"pointer-events-none absolute right-0 flex h-full items-center px-2 text-muted-foreground\">\n <Icon icon={UiChevronDown} className=\"text-xs\" />\n </span>\n </button>\n )}\n\n {open &&\n pos &&\n createPortal(\n <div\n ref={panelRef}\n data-slot=\"tree-picker-popup\"\n style={{\n position: \"fixed\",\n top: pos.top,\n left: pos.left,\n // Computed rather than constant: the popup has to clear whatever\n // modal is open, and a modal sits at zIndex.modal (10000). A\n // hardcoded z-50 renders the tree behind any dialog it is opened\n // in — see ../overlay/zIndex.\n zIndex: floatingZ,\n // Grow to fit the widest row/toolbar, but never narrower than the\n // trigger nor wider than maxWidth — so labels show in full and the\n // toolbar's intrinsic-width search input never forces a scrollbar.\n minWidth: pos.width,\n width: \"max-content\",\n maxWidth: pos.maxWidth,\n maxHeight: pos.maxHeight,\n }}\n className={cn(\n // Height comes from the measured maxHeight above, not a viewport\n // fraction: 60vh is the same number wherever the trigger sits, and\n // a trigger low on the page needs a shorter panel, not an equal one.\n \"flex flex-col overflow-hidden rounded-md border border-border bg-popover shadow-md\",\n panelClassName,\n )}\n >\n <div className=\"min-h-0 flex-1 overflow-auto\">\n <Tree<T>\n roots={roots}\n getKey={getKey}\n getChildren={getChildren}\n renderRow={renderRow}\n {...(getSearchText ? { getSearchText } : {})}\n {...(defaultOpen ? { defaultOpen } : {})}\n {...(selected !== undefined ? { selected } : {})}\n {...(revealSelected !== undefined ? { revealSelected } : {})}\n {...(showControls !== undefined ? { showControls } : {})}\n {...(ariaLabel !== undefined ? { ariaLabel } : {})}\n {...(empty !== undefined ? { empty } : {})}\n onSelect={(node) => {\n if (!isSelectable || isSelectable(node)) {\n onSelect(node);\n setOpen(false);\n }\n }}\n />\n </div>\n {renderFooter && (\n <div className=\"shrink-0 border-t border-border bg-popover\">\n {renderFooter({ close: () => setOpen(false) })}\n </div>\n )}\n </div>,\n document.body,\n )}\n </div>\n );\n}\n"],"names":["useState","useFloatingZIndex","useRef","useEscapeLayer","useLayoutEffect","useEffect","jsxs","cn","inputSizeClass","jsx","Icon","UiChevronDown","createPortal","Tree"],"mappings":"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"}
@@ -39,6 +39,15 @@ export interface TreePickerFieldProps<T> {
39
39
  triggerClassName?: string;
40
40
  panelClassName?: string;
41
41
  renderTrigger?: (props: TreePickerTriggerProps) => ReactNode;
42
+ /**
43
+ * Sticky row pinned below the tree inside the open panel — an escape hatch
44
+ * from a dropdown that has run out of room (a fuller browser, a "create new")
45
+ * rather than another node. It is handed `close` because acting on it almost
46
+ * always means leaving the dropdown behind.
47
+ */
48
+ renderFooter?: (props: {
49
+ close: () => void;
50
+ }) => ReactNode;
42
51
  }
43
52
  export interface TreePickerTriggerProps {
44
53
  open: boolean;
@@ -46,5 +55,5 @@ export interface TreePickerTriggerProps {
46
55
  triggerRef: Ref<HTMLButtonElement>;
47
56
  toggle: () => void;
48
57
  }
49
- export declare function TreePickerField<T>({ roots, getKey, getChildren, renderRow, getSearchText, defaultOpen, isSelectable, onSelect, selected, revealSelected, showControls, ariaLabel, empty, label, placeholder, disabled, size, className, triggerClassName, panelClassName, renderTrigger, }: TreePickerFieldProps<T>): import("react/jsx-runtime").JSX.Element;
58
+ export declare function TreePickerField<T>({ roots, getKey, getChildren, renderRow, getSearchText, defaultOpen, isSelectable, onSelect, selected, revealSelected, showControls, ariaLabel, empty, label, placeholder, disabled, size, className, triggerClassName, panelClassName, renderTrigger, renderFooter, }: TreePickerFieldProps<T>): import("react/jsx-runtime").JSX.Element;
50
59
  //# sourceMappingURL=TreePickerField.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TreePickerField.d.ts","sourceRoot":"","sources":["../../src/components/TreePickerField.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAgD,KAAK,SAAS,EAAE,KAAK,GAAG,EAAE,MAAM,OAAO,CAAC;AAK/F,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAMxE,MAAM,WAAW,oBAAoB,CAAC,CAAC;IACrC,wCAAwC;IACxC,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,MAAM,CAAC;IACrC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,EAAE,GAAG,SAAS,CAAC;IAC1C,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IACrC,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IACpC,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IAClD;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC;IACpC,6DAA6D;IAC7D,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;IACpB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,SAAS,CAAC;CAC9D;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAOD,wBAAgB,eAAe,CAAC,CAAC,EAAE,EACjC,KAAK,EACL,MAAM,EACN,WAAW,EACX,SAAS,EACT,aAAa,EACb,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,SAAS,EACT,KAAK,EACL,KAAK,EACL,WAAuB,EACvB,QAAQ,EACR,IAAI,EACJ,SAAS,EACT,gBAAgB,EAChB,cAAc,EACd,aAAa,GACd,EAAE,oBAAoB,CAAC,CAAC,CAAC,2CAuIzB"}
1
+ {"version":3,"file":"TreePickerField.d.ts","sourceRoot":"","sources":["../../src/components/TreePickerField.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAgD,KAAK,SAAS,EAAE,KAAK,GAAG,EAAE,MAAM,OAAO,CAAC;AAK/F,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAaxE,MAAM,WAAW,oBAAoB,CAAC,CAAC;IACrC,wCAAwC;IACxC,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,MAAM,CAAC;IACrC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,EAAE,GAAG,SAAS,CAAC;IAC1C,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IACrC,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IACpC,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IAClD;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC;IACpC,6DAA6D;IAC7D,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;IACpB;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,SAAS,CAAC;IAC7D;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,IAAI,CAAA;KAAE,KAAK,SAAS,CAAC;CAC5D;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAOD,wBAAgB,eAAe,CAAC,CAAC,EAAE,EACjC,KAAK,EACL,MAAM,EACN,WAAW,EACX,SAAS,EACT,aAAa,EACb,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,SAAS,EACT,KAAK,EACL,KAAK,EACL,WAAuB,EACvB,QAAQ,EACR,IAAI,EACJ,SAAS,EACT,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,YAAY,GACb,EAAE,oBAAoB,CAAC,CAAC,CAAC,2CAsKzB"}
@@ -4,10 +4,13 @@ import { createPortal } from "react-dom";
4
4
  import { cn } from "../lib/utils.js";
5
5
  import { Icon } from "../data/Icon.js";
6
6
  import { Tree } from "../data/Tree.js";
7
- import { useEscapeLayer } from "../overlay/modalStack.js";
7
+ import { useFloatingZIndex, useEscapeLayer } from "../overlay/modalStack.js";
8
8
  import { inputSizeClass } from "./json-schema-form-size.js";
9
9
  import { UiChevronDown } from "../icons/components/UiChevronDown.js";
10
10
  const PANEL_MAX_WIDTH_PX = 480;
11
+ const PANEL_MAX_HEIGHT_PX = 480;
12
+ const PANEL_MIN_HEIGHT_PX = 200;
13
+ const VIEWPORT_MARGIN_PX = 8;
11
14
  function TreePickerField({
12
15
  roots,
13
16
  getKey,
@@ -29,12 +32,12 @@ function TreePickerField({
29
32
  className,
30
33
  triggerClassName,
31
34
  panelClassName,
32
- renderTrigger
35
+ renderTrigger,
36
+ renderFooter
33
37
  }) {
34
38
  const [open, setOpen] = useState(false);
35
- const [pos, setPos] = useState(
36
- null
37
- );
39
+ const floatingZ = useFloatingZIndex();
40
+ const [pos, setPos] = useState(null);
38
41
  const rootRef = useRef(null);
39
42
  const anchorRef = useRef(null);
40
43
  const panelRef = useRef(null);
@@ -52,9 +55,20 @@ function TreePickerField({
52
55
  const anchor = rootRef.current;
53
56
  if (!anchor) return;
54
57
  const rect = anchor.getBoundingClientRect();
55
- const viewportCap = window.innerWidth - rect.left - 8;
58
+ const viewportCap = window.innerWidth - rect.left - VIEWPORT_MARGIN_PX;
56
59
  const maxWidth = Math.max(rect.width, Math.min(PANEL_MAX_WIDTH_PX, viewportCap));
57
- setPos({ top: rect.bottom + 4, left: rect.left, width: rect.width, maxWidth });
60
+ const below = window.innerHeight - rect.bottom - VIEWPORT_MARGIN_PX - 4;
61
+ const above = rect.top - VIEWPORT_MARGIN_PX - 4;
62
+ const flip = below < PANEL_MIN_HEIGHT_PX && above > below;
63
+ const available = Math.max(flip ? above : below, PANEL_MIN_HEIGHT_PX);
64
+ const maxHeight = Math.min(PANEL_MAX_HEIGHT_PX, available);
65
+ setPos({
66
+ top: flip ? Math.max(VIEWPORT_MARGIN_PX, rect.top - 4 - maxHeight) : rect.bottom + 4,
67
+ left: rect.left,
68
+ width: rect.width,
69
+ maxWidth,
70
+ maxHeight
71
+ });
58
72
  };
59
73
  update();
60
74
  window.addEventListener("scroll", update, true);
@@ -108,7 +122,7 @@ function TreePickerField({
108
122
  }
109
123
  ),
110
124
  open && pos && createPortal(
111
- /* @__PURE__ */ jsx(
125
+ /* @__PURE__ */ jsxs(
112
126
  "div",
113
127
  {
114
128
  ref: panelRef,
@@ -117,39 +131,51 @@ function TreePickerField({
117
131
  position: "fixed",
118
132
  top: pos.top,
119
133
  left: pos.left,
134
+ // Computed rather than constant: the popup has to clear whatever
135
+ // modal is open, and a modal sits at zIndex.modal (10000). A
136
+ // hardcoded z-50 renders the tree behind any dialog it is opened
137
+ // in — see ../overlay/zIndex.
138
+ zIndex: floatingZ,
120
139
  // Grow to fit the widest row/toolbar, but never narrower than the
121
140
  // trigger nor wider than maxWidth — so labels show in full and the
122
141
  // toolbar's intrinsic-width search input never forces a scrollbar.
123
142
  minWidth: pos.width,
124
143
  width: "max-content",
125
- maxWidth: pos.maxWidth
144
+ maxWidth: pos.maxWidth,
145
+ maxHeight: pos.maxHeight
126
146
  },
127
147
  className: cn(
128
- "z-50 flex max-h-[60vh] flex-col overflow-hidden rounded-md border border-border bg-popover shadow-md",
148
+ // Height comes from the measured maxHeight above, not a viewport
149
+ // fraction: 60vh is the same number wherever the trigger sits, and
150
+ // a trigger low on the page needs a shorter panel, not an equal one.
151
+ "flex flex-col overflow-hidden rounded-md border border-border bg-popover shadow-md",
129
152
  panelClassName
130
153
  ),
131
- children: /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: /* @__PURE__ */ jsx(
132
- Tree,
133
- {
134
- roots,
135
- getKey,
136
- getChildren,
137
- renderRow,
138
- ...getSearchText ? { getSearchText } : {},
139
- ...defaultOpen ? { defaultOpen } : {},
140
- ...selected !== void 0 ? { selected } : {},
141
- ...revealSelected !== void 0 ? { revealSelected } : {},
142
- ...showControls !== void 0 ? { showControls } : {},
143
- ...ariaLabel !== void 0 ? { ariaLabel } : {},
144
- ...empty !== void 0 ? { empty } : {},
145
- onSelect: (node) => {
146
- if (!isSelectable || isSelectable(node)) {
147
- onSelect(node);
148
- setOpen(false);
154
+ children: [
155
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: /* @__PURE__ */ jsx(
156
+ Tree,
157
+ {
158
+ roots,
159
+ getKey,
160
+ getChildren,
161
+ renderRow,
162
+ ...getSearchText ? { getSearchText } : {},
163
+ ...defaultOpen ? { defaultOpen } : {},
164
+ ...selected !== void 0 ? { selected } : {},
165
+ ...revealSelected !== void 0 ? { revealSelected } : {},
166
+ ...showControls !== void 0 ? { showControls } : {},
167
+ ...ariaLabel !== void 0 ? { ariaLabel } : {},
168
+ ...empty !== void 0 ? { empty } : {},
169
+ onSelect: (node) => {
170
+ if (!isSelectable || isSelectable(node)) {
171
+ onSelect(node);
172
+ setOpen(false);
173
+ }
149
174
  }
150
175
  }
151
- }
152
- ) })
176
+ ) }),
177
+ renderFooter && /* @__PURE__ */ jsx("div", { className: "shrink-0 border-t border-border bg-popover", children: renderFooter({ close: () => setOpen(false) }) })
178
+ ]
153
179
  }
154
180
  ),
155
181
  document.body
@@ -1 +1 @@
1
- {"version":3,"file":"TreePickerField.js","sources":["../../src/components/TreePickerField.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState, type ReactNode, type Ref } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { UiChevronDown } from \"../icons\";\nimport { Tree, type TreeProps } from \"../data/Tree\";\nimport { useEscapeLayer } from \"../overlay/modalStack\";\nimport { inputSizeClass, type FormSize } from \"./json-schema-form-size\";\n\n// Upper bound the open panel grows to so wide trees show full labels before\n// truncating; the panel is never narrower than the trigger.\nconst PANEL_MAX_WIDTH_PX = 480;\n\nexport interface TreePickerFieldProps<T> {\n /** Root nodes of the tree to browse. */\n roots: T[];\n getKey: (node: T) => string | number;\n getChildren: (node: T) => T[] | undefined;\n renderRow: TreeProps<T>[\"renderRow\"];\n getSearchText?: (node: T) => string;\n defaultOpen?: (node: T, depth: number) => boolean;\n /**\n * Decides which nodes commit a selection when clicked. A click on a\n * non-selectable node still toggles its expansion (it never closes the\n * picker). Defaults to every node selectable.\n */\n isSelectable?: (node: T) => boolean;\n /** Fires for a selectable node, then closes the dropdown. */\n onSelect: (node: T) => void;\n /** Highlights the committed node inside the open tree. */\n selected?: T | null;\n /**\n * Force-opens the `selected` node's ancestors so the current value is always\n * visible when the panel opens, however deep its branch. Requires `selected`.\n */\n revealSelected?: boolean;\n /** Renders the tree's expand/collapse + filter toolbar. */\n showControls?: boolean;\n /** Accessible name for the tree inside the panel. */\n ariaLabel?: string;\n /** Empty-state content when `roots` is empty. */\n empty?: ReactNode;\n /** Current-selection display in the trigger; falls back to `placeholder`. */\n label?: ReactNode;\n placeholder?: string;\n disabled?: boolean;\n size?: FormSize;\n className?: string;\n triggerClassName?: string;\n panelClassName?: string;\n renderTrigger?: (props: TreePickerTriggerProps) => ReactNode;\n}\n\nexport interface TreePickerTriggerProps {\n open: boolean;\n disabled: boolean;\n triggerRef: Ref<HTMLButtonElement>;\n toggle: () => void;\n}\n\n// TreePickerField is a form-field-styled trigger (matching Combobox's closed\n// input) that opens a portal-anchored dropdown holding a Tree. The dropdown is\n// positioned with fixed coordinates measured from the trigger so it escapes any\n// overflow-hidden / scroll ancestor (e.g. a modal body). A click on a selectable\n// node commits and closes; a non-selectable node only toggles expansion.\nexport function TreePickerField<T>({\n roots,\n getKey,\n getChildren,\n renderRow,\n getSearchText,\n defaultOpen,\n isSelectable,\n onSelect,\n selected,\n revealSelected,\n showControls,\n ariaLabel,\n empty,\n label,\n placeholder = \"Select…\",\n disabled,\n size,\n className,\n triggerClassName,\n panelClassName,\n renderTrigger,\n}: TreePickerFieldProps<T>) {\n const [open, setOpen] = useState(false);\n const [pos, setPos] = useState<{ top: number; left: number; width: number; maxWidth: number } | null>(\n null,\n );\n const rootRef = useRef<HTMLDivElement>(null);\n const anchorRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n\n useEscapeLayer(open, () => {\n setOpen(false);\n anchorRef.current?.focus();\n });\n\n // Position the portaled panel from the trigger rect, repositioning while open.\n useLayoutEffect(() => {\n if (!open) {\n setPos(null);\n return;\n }\n const update = () => {\n const anchor = rootRef.current;\n if (!anchor) return;\n const rect = anchor.getBoundingClientRect();\n const viewportCap = window.innerWidth - rect.left - 8;\n const maxWidth = Math.max(rect.width, Math.min(PANEL_MAX_WIDTH_PX, viewportCap));\n setPos({ 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 // Dismiss on an outside pointer-down (the trigger and panel are exempt).\n useEffect(() => {\n if (!open) return;\n const onPointerDown = (e: MouseEvent) => {\n const target = e.target as Node;\n if (!rootRef.current?.contains(target) && !panelRef.current?.contains(target)) {\n setOpen(false);\n }\n };\n document.addEventListener(\"mousedown\", onPointerDown);\n return () => document.removeEventListener(\"mousedown\", onPointerDown);\n }, [open]);\n\n return (\n <div ref={rootRef} className={cn(\"relative\", className)}>\n {renderTrigger ? (\n renderTrigger({\n open,\n disabled: Boolean(disabled),\n triggerRef: anchorRef,\n toggle: () => {\n if (!disabled) setOpen((current) => !current);\n },\n })\n ) : (\n <button\n ref={anchorRef}\n type=\"button\"\n disabled={disabled}\n aria-haspopup=\"tree\"\n aria-expanded={open}\n onClick={() => setOpen((current) => !current)}\n className={cn(\n \"relative flex w-full items-center rounded-md border border-input bg-background text-left text-foreground\",\n size ? inputSizeClass[size] : \"h-control-h px-control-px text-sm\",\n \"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 triggerClassName,\n )}\n >\n <span className={cn(\"min-w-0 flex-1 truncate\", label == null && \"text-muted-foreground\")}>\n {label ?? placeholder}\n </span>\n <span className=\"pointer-events-none absolute right-0 flex h-full items-center px-2 text-muted-foreground\">\n <Icon icon={UiChevronDown} className=\"text-xs\" />\n </span>\n </button>\n )}\n\n {open &&\n pos &&\n createPortal(\n <div\n ref={panelRef}\n data-slot=\"tree-picker-popup\"\n style={{\n position: \"fixed\",\n top: pos.top,\n left: pos.left,\n // Grow to fit the widest row/toolbar, but never narrower than the\n // trigger nor wider than maxWidth — so labels show in full and the\n // toolbar's intrinsic-width search input never forces a scrollbar.\n minWidth: pos.width,\n width: \"max-content\",\n maxWidth: pos.maxWidth,\n }}\n className={cn(\n \"z-50 flex max-h-[60vh] flex-col overflow-hidden rounded-md border border-border bg-popover shadow-md\",\n panelClassName,\n )}\n >\n <div className=\"min-h-0 flex-1 overflow-auto\">\n <Tree<T>\n roots={roots}\n getKey={getKey}\n getChildren={getChildren}\n renderRow={renderRow}\n {...(getSearchText ? { getSearchText } : {})}\n {...(defaultOpen ? { defaultOpen } : {})}\n {...(selected !== undefined ? { selected } : {})}\n {...(revealSelected !== undefined ? { revealSelected } : {})}\n {...(showControls !== undefined ? { showControls } : {})}\n {...(ariaLabel !== undefined ? { ariaLabel } : {})}\n {...(empty !== undefined ? { empty } : {})}\n onSelect={(node) => {\n if (!isSelectable || isSelectable(node)) {\n onSelect(node);\n setOpen(false);\n }\n }}\n />\n </div>\n </div>,\n document.body,\n )}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TreePickerField.js","sources":["../../src/components/TreePickerField.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState, type ReactNode, type Ref } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"../lib/utils\";\nimport { Icon } from \"../data/Icon\";\nimport { UiChevronDown } from \"../icons\";\nimport { Tree, type TreeProps } from \"../data/Tree\";\nimport { useEscapeLayer, useFloatingZIndex } from \"../overlay/modalStack\";\nimport { inputSizeClass, type FormSize } from \"./json-schema-form-size\";\n\n// Upper bound the open panel grows to so wide trees show full labels before\n// truncating; the panel is never narrower than the trigger.\nconst PANEL_MAX_WIDTH_PX = 480;\n// Tallest the panel gets when the viewport allows it.\nconst PANEL_MAX_HEIGHT_PX = 480;\n// Below this there is not enough room to be worth opening downward, so the panel\n// flips above the trigger instead.\nconst PANEL_MIN_HEIGHT_PX = 200;\n// Breathing room kept between the panel and the viewport edge.\nconst VIEWPORT_MARGIN_PX = 8;\n\nexport interface TreePickerFieldProps<T> {\n /** Root nodes of the tree to browse. */\n roots: T[];\n getKey: (node: T) => string | number;\n getChildren: (node: T) => T[] | undefined;\n renderRow: TreeProps<T>[\"renderRow\"];\n getSearchText?: (node: T) => string;\n defaultOpen?: (node: T, depth: number) => boolean;\n /**\n * Decides which nodes commit a selection when clicked. A click on a\n * non-selectable node still toggles its expansion (it never closes the\n * picker). Defaults to every node selectable.\n */\n isSelectable?: (node: T) => boolean;\n /** Fires for a selectable node, then closes the dropdown. */\n onSelect: (node: T) => void;\n /** Highlights the committed node inside the open tree. */\n selected?: T | null;\n /**\n * Force-opens the `selected` node's ancestors so the current value is always\n * visible when the panel opens, however deep its branch. Requires `selected`.\n */\n revealSelected?: boolean;\n /** Renders the tree's expand/collapse + filter toolbar. */\n showControls?: boolean;\n /** Accessible name for the tree inside the panel. */\n ariaLabel?: string;\n /** Empty-state content when `roots` is empty. */\n empty?: ReactNode;\n /** Current-selection display in the trigger; falls back to `placeholder`. */\n label?: ReactNode;\n placeholder?: string;\n disabled?: boolean;\n size?: FormSize;\n className?: string;\n triggerClassName?: string;\n panelClassName?: string;\n renderTrigger?: (props: TreePickerTriggerProps) => ReactNode;\n /**\n * Sticky row pinned below the tree inside the open panel — an escape hatch\n * from a dropdown that has run out of room (a fuller browser, a \"create new\")\n * rather than another node. It is handed `close` because acting on it almost\n * always means leaving the dropdown behind.\n */\n renderFooter?: (props: { close: () => void }) => ReactNode;\n}\n\nexport interface TreePickerTriggerProps {\n open: boolean;\n disabled: boolean;\n triggerRef: Ref<HTMLButtonElement>;\n toggle: () => void;\n}\n\n// TreePickerField is a form-field-styled trigger (matching Combobox's closed\n// input) that opens a portal-anchored dropdown holding a Tree. The dropdown is\n// positioned with fixed coordinates measured from the trigger so it escapes any\n// overflow-hidden / scroll ancestor (e.g. a modal body). A click on a selectable\n// node commits and closes; a non-selectable node only toggles expansion.\nexport function TreePickerField<T>({\n roots,\n getKey,\n getChildren,\n renderRow,\n getSearchText,\n defaultOpen,\n isSelectable,\n onSelect,\n selected,\n revealSelected,\n showControls,\n ariaLabel,\n empty,\n label,\n placeholder = \"Select…\",\n disabled,\n size,\n className,\n triggerClassName,\n panelClassName,\n renderTrigger,\n renderFooter,\n}: TreePickerFieldProps<T>) {\n const [open, setOpen] = useState(false);\n const floatingZ = useFloatingZIndex();\n const [pos, setPos] = useState<\n { top: number; left: number; width: number; maxWidth: number; maxHeight: number } | null\n >(null);\n const rootRef = useRef<HTMLDivElement>(null);\n const anchorRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n\n useEscapeLayer(open, () => {\n setOpen(false);\n anchorRef.current?.focus();\n });\n\n // Position the portaled panel from the trigger rect, repositioning while open.\n useLayoutEffect(() => {\n if (!open) {\n setPos(null);\n return;\n }\n const update = () => {\n const anchor = rootRef.current;\n if (!anchor) return;\n const rect = anchor.getBoundingClientRect();\n const viewportCap = window.innerWidth - rect.left - VIEWPORT_MARGIN_PX;\n const maxWidth = Math.max(rect.width, Math.min(PANEL_MAX_WIDTH_PX, viewportCap));\n // Fit the panel to the room the trigger actually has, and flip it above\n // when there is more room up there. Without this the panel keeps its full\n // height wherever the trigger sits, so a trigger low on the page pushes\n // the panel's bottom past the viewport — taking the sticky footer, and\n // any escape hatch pinned to it, out of reach entirely.\n const below = window.innerHeight - rect.bottom - VIEWPORT_MARGIN_PX - 4;\n const above = rect.top - VIEWPORT_MARGIN_PX - 4;\n const flip = below < PANEL_MIN_HEIGHT_PX && above > below;\n const available = Math.max(flip ? above : below, PANEL_MIN_HEIGHT_PX);\n const maxHeight = Math.min(PANEL_MAX_HEIGHT_PX, available);\n setPos({\n top: flip ? Math.max(VIEWPORT_MARGIN_PX, rect.top - 4 - maxHeight) : rect.bottom + 4,\n left: rect.left,\n width: rect.width,\n maxWidth,\n maxHeight,\n });\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 // Dismiss on an outside pointer-down (the trigger and panel are exempt).\n useEffect(() => {\n if (!open) return;\n const onPointerDown = (e: MouseEvent) => {\n const target = e.target as Node;\n if (!rootRef.current?.contains(target) && !panelRef.current?.contains(target)) {\n setOpen(false);\n }\n };\n document.addEventListener(\"mousedown\", onPointerDown);\n return () => document.removeEventListener(\"mousedown\", onPointerDown);\n }, [open]);\n\n return (\n <div ref={rootRef} className={cn(\"relative\", className)}>\n {renderTrigger ? (\n renderTrigger({\n open,\n disabled: Boolean(disabled),\n triggerRef: anchorRef,\n toggle: () => {\n if (!disabled) setOpen((current) => !current);\n },\n })\n ) : (\n <button\n ref={anchorRef}\n type=\"button\"\n disabled={disabled}\n aria-haspopup=\"tree\"\n aria-expanded={open}\n onClick={() => setOpen((current) => !current)}\n className={cn(\n \"relative flex w-full items-center rounded-md border border-input bg-background text-left text-foreground\",\n size ? inputSizeClass[size] : \"h-control-h px-control-px text-sm\",\n \"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 triggerClassName,\n )}\n >\n <span className={cn(\"min-w-0 flex-1 truncate\", label == null && \"text-muted-foreground\")}>\n {label ?? placeholder}\n </span>\n <span className=\"pointer-events-none absolute right-0 flex h-full items-center px-2 text-muted-foreground\">\n <Icon icon={UiChevronDown} className=\"text-xs\" />\n </span>\n </button>\n )}\n\n {open &&\n pos &&\n createPortal(\n <div\n ref={panelRef}\n data-slot=\"tree-picker-popup\"\n style={{\n position: \"fixed\",\n top: pos.top,\n left: pos.left,\n // Computed rather than constant: the popup has to clear whatever\n // modal is open, and a modal sits at zIndex.modal (10000). A\n // hardcoded z-50 renders the tree behind any dialog it is opened\n // in — see ../overlay/zIndex.\n zIndex: floatingZ,\n // Grow to fit the widest row/toolbar, but never narrower than the\n // trigger nor wider than maxWidth — so labels show in full and the\n // toolbar's intrinsic-width search input never forces a scrollbar.\n minWidth: pos.width,\n width: \"max-content\",\n maxWidth: pos.maxWidth,\n maxHeight: pos.maxHeight,\n }}\n className={cn(\n // Height comes from the measured maxHeight above, not a viewport\n // fraction: 60vh is the same number wherever the trigger sits, and\n // a trigger low on the page needs a shorter panel, not an equal one.\n \"flex flex-col overflow-hidden rounded-md border border-border bg-popover shadow-md\",\n panelClassName,\n )}\n >\n <div className=\"min-h-0 flex-1 overflow-auto\">\n <Tree<T>\n roots={roots}\n getKey={getKey}\n getChildren={getChildren}\n renderRow={renderRow}\n {...(getSearchText ? { getSearchText } : {})}\n {...(defaultOpen ? { defaultOpen } : {})}\n {...(selected !== undefined ? { selected } : {})}\n {...(revealSelected !== undefined ? { revealSelected } : {})}\n {...(showControls !== undefined ? { showControls } : {})}\n {...(ariaLabel !== undefined ? { ariaLabel } : {})}\n {...(empty !== undefined ? { empty } : {})}\n onSelect={(node) => {\n if (!isSelectable || isSelectable(node)) {\n onSelect(node);\n setOpen(false);\n }\n }}\n />\n </div>\n {renderFooter && (\n <div className=\"shrink-0 border-t border-border bg-popover\">\n {renderFooter({ close: () => setOpen(false) })}\n </div>\n )}\n </div>,\n document.body,\n )}\n </div>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -7,6 +7,15 @@ export type ComboboxOption = {
7
7
  label: string;
8
8
  /** Secondary text rendered below the label and included in local filtering. */
9
9
  description?: string;
10
+ /**
11
+ * Node pinned to the right edge of the option row — a rendered preview of
12
+ * what the choice produces, a count, a badge. Keeps the row single-line, so
13
+ * prefer it over `description` when the secondary content is a value rather
14
+ * than a sentence. Not searchable (use `label`, `description` or `title` for
15
+ * anything that must match a query), and ignored in tristate mode, whose row
16
+ * is a full-width pill.
17
+ */
18
+ trailing?: ReactNode;
10
19
  /** Optional compact label used in the closed selection or a selected tag. */
11
20
  selectedLabel?: string;
12
21
  disabled?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"combobox-types.d.ts","sourceRoot":"","sources":["../../src/components/combobox-types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"combobox-types.d.ts","sourceRoot":"","sources":["../../src/components/combobox-types.ts"],"names":[],"mappings":"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"}
@@ -25,9 +25,16 @@ function clearFilterBarFilter(filter) {
25
25
  filter.onChange(void 0);
26
26
  return;
27
27
  }
28
+ if (filter.kind === "date-range") {
29
+ filter.onApply("", "");
30
+ return;
31
+ }
28
32
  filter.onChange({});
29
33
  }
30
34
  function isFilterBarFilterActive(filter) {
35
+ if (filter.kind === "date-range") {
36
+ return String(filter.from ?? "").trim() !== "" || String(filter.to ?? "").trim() !== "";
37
+ }
31
38
  if (filter.kind === "text" || filter.kind === "lookup" || filter.kind === "enum") {
32
39
  return String(filter.value ?? "").trim() !== "";
33
40
  }