@flanksource/clicky-ui 0.2.12 → 0.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/dist/components/Combobox.cjs +47 -33
  2. package/dist/components/Combobox.cjs.map +1 -1
  3. package/dist/components/Combobox.d.ts +12 -0
  4. package/dist/components/Combobox.d.ts.map +1 -1
  5. package/dist/components/Combobox.js +48 -34
  6. package/dist/components/Combobox.js.map +1 -1
  7. package/dist/components/IconButton.cjs +26 -0
  8. package/dist/components/IconButton.cjs.map +1 -0
  9. package/dist/components/IconButton.d.ts +25 -0
  10. package/dist/components/IconButton.d.ts.map +1 -0
  11. package/dist/components/IconButton.js +26 -0
  12. package/dist/components/IconButton.js.map +1 -0
  13. package/dist/components/SecretKeySelector.cjs +151 -0
  14. package/dist/components/SecretKeySelector.cjs.map +1 -0
  15. package/dist/components/SecretKeySelector.d.ts +51 -0
  16. package/dist/components/SecretKeySelector.d.ts.map +1 -0
  17. package/dist/components/SecretKeySelector.js +151 -0
  18. package/dist/components/SecretKeySelector.js.map +1 -0
  19. package/dist/components/WorkloadPicker.cjs +144 -0
  20. package/dist/components/WorkloadPicker.cjs.map +1 -0
  21. package/dist/components/WorkloadPicker.d.ts +52 -0
  22. package/dist/components/WorkloadPicker.d.ts.map +1 -0
  23. package/dist/components/WorkloadPicker.js +144 -0
  24. package/dist/components/WorkloadPicker.js.map +1 -0
  25. package/dist/components/icon-menu-picker.cjs +83 -112
  26. package/dist/components/icon-menu-picker.cjs.map +1 -1
  27. package/dist/components/icon-menu-picker.d.ts.map +1 -1
  28. package/dist/components/icon-menu-picker.js +85 -114
  29. package/dist/components/icon-menu-picker.js.map +1 -1
  30. package/dist/components.cjs +10 -0
  31. package/dist/components.cjs.map +1 -1
  32. package/dist/components.d.ts +3 -0
  33. package/dist/components.d.ts.map +1 -1
  34. package/dist/components.js +11 -1
  35. package/dist/components.js.map +1 -1
  36. package/dist/data/Clicky.cjs +1 -1
  37. package/dist/data/Clicky.cjs.map +1 -1
  38. package/dist/data/Clicky.js +1 -1
  39. package/dist/data/Clicky.js.map +1 -1
  40. package/dist/data/TimeseriesCoreBars.cjs +8 -1
  41. package/dist/data/TimeseriesCoreBars.cjs.map +1 -1
  42. package/dist/data/TimeseriesCoreBars.d.ts.map +1 -1
  43. package/dist/data/TimeseriesCoreBars.js +8 -1
  44. package/dist/data/TimeseriesCoreBars.js.map +1 -1
  45. package/dist/data/Version.cjs +42 -0
  46. package/dist/data/Version.cjs.map +1 -0
  47. package/dist/data/Version.d.ts +36 -0
  48. package/dist/data/Version.d.ts.map +1 -0
  49. package/dist/data/Version.js +42 -0
  50. package/dist/data/Version.js.map +1 -0
  51. package/dist/data/test-runner/TestDetailPanel.cjs +165 -0
  52. package/dist/data/test-runner/TestDetailPanel.cjs.map +1 -0
  53. package/dist/data/test-runner/TestDetailPanel.d.ts +7 -0
  54. package/dist/data/test-runner/TestDetailPanel.d.ts.map +1 -0
  55. package/dist/data/test-runner/TestDetailPanel.js +165 -0
  56. package/dist/data/test-runner/TestDetailPanel.js.map +1 -0
  57. package/dist/data/test-runner/TestFailureDetail.cjs +78 -0
  58. package/dist/data/test-runner/TestFailureDetail.cjs.map +1 -0
  59. package/dist/data/test-runner/TestFailureDetail.d.ts +14 -0
  60. package/dist/data/test-runner/TestFailureDetail.d.ts.map +1 -0
  61. package/dist/data/test-runner/TestFailureDetail.js +78 -0
  62. package/dist/data/test-runner/TestFailureDetail.js.map +1 -0
  63. package/dist/data/test-runner/TestFilterBar.cjs +108 -0
  64. package/dist/data/test-runner/TestFilterBar.cjs.map +1 -0
  65. package/dist/data/test-runner/TestFilterBar.d.ts +15 -0
  66. package/dist/data/test-runner/TestFilterBar.d.ts.map +1 -0
  67. package/dist/data/test-runner/TestFilterBar.js +108 -0
  68. package/dist/data/test-runner/TestFilterBar.js.map +1 -0
  69. package/dist/data/test-runner/TestRunSummary.cjs +81 -0
  70. package/dist/data/test-runner/TestRunSummary.cjs.map +1 -0
  71. package/dist/data/test-runner/TestRunSummary.d.ts +19 -0
  72. package/dist/data/test-runner/TestRunSummary.d.ts.map +1 -0
  73. package/dist/data/test-runner/TestRunSummary.js +81 -0
  74. package/dist/data/test-runner/TestRunSummary.js.map +1 -0
  75. package/dist/data/test-runner/TestRunner.cjs +143 -0
  76. package/dist/data/test-runner/TestRunner.cjs.map +1 -0
  77. package/dist/data/test-runner/TestRunner.d.ts +42 -0
  78. package/dist/data/test-runner/TestRunner.d.ts.map +1 -0
  79. package/dist/data/test-runner/TestRunner.fixtures.d.ts +26 -0
  80. package/dist/data/test-runner/TestRunner.fixtures.d.ts.map +1 -0
  81. package/dist/data/test-runner/TestRunner.js +143 -0
  82. package/dist/data/test-runner/TestRunner.js.map +1 -0
  83. package/dist/data/test-runner/TestTree.cjs +42 -0
  84. package/dist/data/test-runner/TestTree.cjs.map +1 -0
  85. package/dist/data/test-runner/TestTree.d.ts +9 -0
  86. package/dist/data/test-runner/TestTree.d.ts.map +1 -0
  87. package/dist/data/test-runner/TestTree.js +42 -0
  88. package/dist/data/test-runner/TestTree.js.map +1 -0
  89. package/dist/data/test-runner/TestTreeNode.cjs +99 -0
  90. package/dist/data/test-runner/TestTreeNode.cjs.map +1 -0
  91. package/dist/data/test-runner/TestTreeNode.d.ts +12 -0
  92. package/dist/data/test-runner/TestTreeNode.d.ts.map +1 -0
  93. package/dist/data/test-runner/TestTreeNode.js +99 -0
  94. package/dist/data/test-runner/TestTreeNode.js.map +1 -0
  95. package/dist/data/test-runner/adapter.cjs +57 -0
  96. package/dist/data/test-runner/adapter.cjs.map +1 -0
  97. package/dist/data/test-runner/adapter.d.ts +60 -0
  98. package/dist/data/test-runner/adapter.d.ts.map +1 -0
  99. package/dist/data/test-runner/adapter.js +57 -0
  100. package/dist/data/test-runner/adapter.js.map +1 -0
  101. package/dist/data/test-runner/context.cjs +15 -0
  102. package/dist/data/test-runner/context.cjs.map +1 -0
  103. package/dist/data/test-runner/context.d.ts +40 -0
  104. package/dist/data/test-runner/context.d.ts.map +1 -0
  105. package/dist/data/test-runner/context.js +15 -0
  106. package/dist/data/test-runner/context.js.map +1 -0
  107. package/dist/data/test-runner/filterState.cjs +47 -0
  108. package/dist/data/test-runner/filterState.cjs.map +1 -0
  109. package/dist/data/test-runner/filterState.d.ts +14 -0
  110. package/dist/data/test-runner/filterState.d.ts.map +1 -0
  111. package/dist/data/test-runner/filterState.js +47 -0
  112. package/dist/data/test-runner/filterState.js.map +1 -0
  113. package/dist/data/test-runner/frameworkIcon.cjs +27 -0
  114. package/dist/data/test-runner/frameworkIcon.cjs.map +1 -0
  115. package/dist/data/test-runner/frameworkIcon.d.ts +3 -0
  116. package/dist/data/test-runner/frameworkIcon.d.ts.map +1 -0
  117. package/dist/data/test-runner/frameworkIcon.js +27 -0
  118. package/dist/data/test-runner/frameworkIcon.js.map +1 -0
  119. package/dist/data/test-runner/index.d.ts +14 -0
  120. package/dist/data/test-runner/index.d.ts.map +1 -0
  121. package/dist/data/test-runner/status.cjs +234 -0
  122. package/dist/data/test-runner/status.cjs.map +1 -0
  123. package/dist/data/test-runner/status.d.ts +52 -0
  124. package/dist/data/test-runner/status.d.ts.map +1 -0
  125. package/dist/data/test-runner/status.js +234 -0
  126. package/dist/data/test-runner/status.js.map +1 -0
  127. package/dist/data/test-runner/types.d.ts +148 -0
  128. package/dist/data/test-runner/types.d.ts.map +1 -0
  129. package/dist/data/version-info.cjs +26 -0
  130. package/dist/data/version-info.cjs.map +1 -0
  131. package/dist/data/version-info.d.ts +11 -0
  132. package/dist/data/version-info.d.ts.map +1 -0
  133. package/dist/data/version-info.js +26 -0
  134. package/dist/data/version-info.js.map +1 -0
  135. package/dist/data.cjs +49 -0
  136. package/dist/data.cjs.map +1 -1
  137. package/dist/data.d.ts +3 -0
  138. package/dist/data.d.ts.map +1 -1
  139. package/dist/data.js +50 -1
  140. package/dist/data.js.map +1 -1
  141. package/dist/index.cjs +59 -0
  142. package/dist/index.cjs.map +1 -1
  143. package/dist/index.js +60 -1
  144. package/dist/index.js.map +1 -1
  145. package/dist/overlay/DropdownMenu.cjs +57 -30
  146. package/dist/overlay/DropdownMenu.cjs.map +1 -1
  147. package/dist/overlay/DropdownMenu.d.ts.map +1 -1
  148. package/dist/overlay/DropdownMenu.js +57 -30
  149. package/dist/overlay/DropdownMenu.js.map +1 -1
  150. package/dist/overlay/Modal.cjs +62 -6
  151. package/dist/overlay/Modal.cjs.map +1 -1
  152. package/dist/overlay/Modal.d.ts +19 -1
  153. package/dist/overlay/Modal.d.ts.map +1 -1
  154. package/dist/overlay/Modal.js +62 -6
  155. package/dist/overlay/Modal.js.map +1 -1
  156. package/dist/rpc/CommandForm.cjs +2 -2
  157. package/dist/rpc/CommandForm.cjs.map +1 -1
  158. package/dist/rpc/CommandForm.d.ts.map +1 -1
  159. package/dist/rpc/CommandForm.js +3 -3
  160. package/dist/rpc/CommandForm.js.map +1 -1
  161. package/dist/rpc/formMetadata.cjs +5 -4
  162. package/dist/rpc/formMetadata.cjs.map +1 -1
  163. package/dist/rpc/formMetadata.d.ts.map +1 -1
  164. package/dist/rpc/formMetadata.js +6 -5
  165. package/dist/rpc/formMetadata.js.map +1 -1
  166. package/dist/rpc/types.cjs +4 -0
  167. package/dist/rpc/types.cjs.map +1 -1
  168. package/dist/rpc/types.d.ts +10 -0
  169. package/dist/rpc/types.d.ts.map +1 -1
  170. package/dist/rpc/types.js +5 -1
  171. package/dist/rpc/types.js.map +1 -1
  172. package/dist/styles.css +1 -1
  173. package/package.json +3 -1
@@ -0,0 +1,52 @@
1
+ import { ComboboxOption } from './Combobox';
2
+ export type WorkloadKind = "service" | "ingress" | "deployment" | "statefulset";
3
+ /** One workload returned by the loader. Ingresses may carry rule hostnames. */
4
+ export type WorkloadResource = {
5
+ name: string;
6
+ hosts?: string[];
7
+ };
8
+ export type WorkloadPickerProps = {
9
+ /**
10
+ * Controlled selected value — a `[namespace/]kind/name` key (see
11
+ * {@link workloadKey} / {@link parseWorkloadKey}).
12
+ */
13
+ value: string;
14
+ /** Called with the chosen `[namespace/]kind/name` key (or "" when cleared). */
15
+ onChange: (value: string) => void;
16
+ /**
17
+ * Async getter the component calls to load the requested kinds' workloads.
18
+ * Returns a map keyed by kind. The consumer owns fetching/caching.
19
+ */
20
+ loadWorkloads: (kinds: WorkloadKind[]) => Promise<Record<WorkloadKind, WorkloadResource[]>>;
21
+ /**
22
+ * Namespace the workloads live in. When set it prefixes the emitted key
23
+ * (`namespace/kind/name`), so values stay distinct across namespaces.
24
+ */
25
+ namespace?: string;
26
+ /** Kinds to offer, in display order. Defaults to all four. */
27
+ kinds?: WorkloadKind[];
28
+ /**
29
+ * When true, a non-empty value that does not match any loaded workload (once
30
+ * loading settles) is flagged invalid — the picked workload doesn't exist in
31
+ * the namespace. The value is still shown (pinned) so the user can see and
32
+ * fix it; freeform entry is not rejected, only marked.
33
+ */
34
+ strict?: boolean;
35
+ placeholder?: string;
36
+ className?: string;
37
+ };
38
+ export declare function WorkloadPicker({ value, onChange, loadWorkloads, namespace, kinds, strict, placeholder, className, }: WorkloadPickerProps): import("react/jsx-runtime").JSX.Element;
39
+ export type ParsedWorkloadKey = {
40
+ namespace?: string;
41
+ kind?: WorkloadKind;
42
+ name: string;
43
+ };
44
+ export declare function workloadKey(namespace: string | undefined, kind: WorkloadKind, r: WorkloadResource): string;
45
+ export declare function parseWorkloadKey(value: string): ParsedWorkloadKey;
46
+ export declare function loadedWorkloads(namespace: string | undefined, byKind: Partial<Record<WorkloadKind, WorkloadResource[]>>, kinds: WorkloadKind[]): {
47
+ keys: Set<string>;
48
+ names: Set<string>;
49
+ };
50
+ export declare function kindForValue(kinds: WorkloadKind[], value: string): WorkloadKind;
51
+ export declare function buildWorkloadOptions(namespace: string | undefined, byKind: Partial<Record<WorkloadKind, WorkloadResource[]>>, kinds: WorkloadKind[], value: string): ComboboxOption[];
52
+ //# sourceMappingURL=WorkloadPicker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkloadPicker.d.ts","sourceRoot":"","sources":["../../src/components/WorkloadPicker.tsx"],"names":[],"mappings":"AAQA,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAgB3D,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,SAAS,GAAG,YAAY,GAAG,aAAa,CAAC;AAEhF,+EAA+E;AAC/E,MAAM,MAAM,gBAAgB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAgBlE,MAAM,MAAM,mBAAmB,GAAG;IAChC;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC;;;OAGG;IACH,aAAa,EAAE,CAAC,KAAK,EAAE,YAAY,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC;IAC5F;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;IACvB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,QAAQ,EACR,aAAa,EACb,SAAS,EACT,KAAiB,EACjB,MAAc,EACd,WAA0C,EAC1C,SAAS,GACV,EAAE,mBAAmB,2CAkErB;AAeD,MAAM,MAAM,iBAAiB,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAO1F,wBAAgB,WAAW,CACzB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,IAAI,EAAE,YAAY,EAClB,CAAC,EAAE,gBAAgB,GAClB,MAAM,CAGR;AAOD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAWjE;AAOD,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,gBAAgB,EAAE,CAAC,CAAC,EACzD,KAAK,EAAE,YAAY,EAAE,GACpB;IAAE,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAAC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CAU3C;AAKD,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,CAG/E;AAMD,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,gBAAgB,EAAE,CAAC,CAAC,EACzD,KAAK,EAAE,YAAY,EAAE,EACrB,KAAK,EAAE,MAAM,GACZ,cAAc,EAAE,CAuBlB"}
@@ -0,0 +1,144 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useState, useEffect, useMemo } from "react";
3
+ import { K8SStatefulset, K8SDeployment, K8SIngress, K8SService } from "@flanksource/icons/mi";
4
+ import { cn } from "../lib/utils.js";
5
+ import { Combobox } from "./Combobox.js";
6
+ const WORKLOAD_META = {
7
+ service: { label: "Service", Icon: K8SService },
8
+ ingress: { label: "Ingress", Icon: K8SIngress },
9
+ deployment: { label: "Deployment", Icon: K8SDeployment },
10
+ statefulset: { label: "StatefulSet", Icon: K8SStatefulset }
11
+ };
12
+ const ALL_KINDS = ["service", "ingress", "deployment", "statefulset"];
13
+ function WorkloadPicker({
14
+ value,
15
+ onChange,
16
+ loadWorkloads,
17
+ namespace,
18
+ kinds = ALL_KINDS,
19
+ strict = false,
20
+ placeholder = "Select workload / service…",
21
+ className
22
+ }) {
23
+ const [byKind, setByKind] = useState({});
24
+ const [loading, setLoading] = useState(false);
25
+ const kindsKey = kinds.join(",");
26
+ useEffect(() => {
27
+ let cancelled = false;
28
+ setLoading(true);
29
+ loadWorkloads(kinds).then((res) => {
30
+ if (!cancelled) setByKind(res);
31
+ }).finally(() => {
32
+ if (!cancelled) setLoading(false);
33
+ });
34
+ return () => {
35
+ cancelled = true;
36
+ };
37
+ }, [loadWorkloads, kindsKey]);
38
+ const options = useMemo(
39
+ () => buildWorkloadOptions(namespace, byKind, kinds, value),
40
+ [namespace, byKind, kinds, value]
41
+ );
42
+ const invalid = useMemo(() => {
43
+ if (!strict || !value || loading) return false;
44
+ const { keys, names } = loadedWorkloads(namespace, byKind, kinds);
45
+ return !keys.has(value) && !names.has(value);
46
+ }, [strict, value, loading, namespace, byKind, kinds]);
47
+ const selectedKind = kindForValue(kinds, value);
48
+ const leadMeta = WORKLOAD_META[selectedKind];
49
+ const LeadIcon = leadMeta.Icon;
50
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2", className), children: [
51
+ /* @__PURE__ */ jsx(
52
+ LeadIcon,
53
+ {
54
+ className: "h-4 w-4 shrink-0 text-muted-foreground",
55
+ title: leadMeta.label,
56
+ "aria-label": leadMeta.label
57
+ }
58
+ ),
59
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(
60
+ Combobox,
61
+ {
62
+ options,
63
+ value,
64
+ onChange,
65
+ allowCustomValue: true,
66
+ loading,
67
+ invalid,
68
+ placeholder
69
+ }
70
+ ) })
71
+ ] });
72
+ }
73
+ function ingressHost(kind, r) {
74
+ var _a;
75
+ return kind === "ingress" ? (_a = r.hosts) == null ? void 0 : _a[0] : void 0;
76
+ }
77
+ function workloadName(kind, r) {
78
+ return ingressHost(kind, r) ?? r.name;
79
+ }
80
+ const KNOWN_KINDS = /* @__PURE__ */ new Set(["service", "ingress", "deployment", "statefulset"]);
81
+ function workloadKey(namespace, kind, r) {
82
+ const name = workloadName(kind, r);
83
+ return namespace ? `${namespace}/${kind}/${name}` : `${kind}/${name}`;
84
+ }
85
+ function parseWorkloadKey(value) {
86
+ const parts = value.split("/");
87
+ const name = parts.at(-1);
88
+ const maybeKind = parts.at(-2);
89
+ if (name != null && maybeKind != null && KNOWN_KINDS.has(maybeKind)) {
90
+ const namespace = parts.slice(0, -2).join("/");
91
+ const parsed = { kind: maybeKind, name };
92
+ if (namespace) parsed.namespace = namespace;
93
+ return parsed;
94
+ }
95
+ return { name: value };
96
+ }
97
+ function loadedWorkloads(namespace, byKind, kinds) {
98
+ const keys = /* @__PURE__ */ new Set();
99
+ const names = /* @__PURE__ */ new Set();
100
+ for (const kind of kinds) {
101
+ for (const r of byKind[kind] ?? []) {
102
+ keys.add(workloadKey(namespace, kind, r));
103
+ names.add(workloadName(kind, r));
104
+ }
105
+ }
106
+ return { keys, names };
107
+ }
108
+ function kindForValue(kinds, value) {
109
+ const kind = value ? parseWorkloadKey(value).kind : void 0;
110
+ return kind ?? kinds[0] ?? "service";
111
+ }
112
+ function buildWorkloadOptions(namespace, byKind, kinds, value) {
113
+ const opts = [];
114
+ for (const kind of kinds) {
115
+ const meta = WORKLOAD_META[kind];
116
+ const Icon = meta.Icon;
117
+ for (const r of byKind[kind] ?? []) {
118
+ const host = ingressHost(kind, r);
119
+ opts.push({
120
+ value: workloadKey(namespace, kind, r),
121
+ // The label is the human name; an ingress pairs its host with the
122
+ // ingress name for context.
123
+ label: host ? `${host} (${r.name})` : r.name,
124
+ group: meta.label,
125
+ icon: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" })
126
+ });
127
+ }
128
+ }
129
+ if (value && !opts.some((o) => o.value === value)) {
130
+ const parsed = parseWorkloadKey(value);
131
+ const Icon = WORKLOAD_META[parsed.kind ?? kinds[0] ?? "service"].Icon;
132
+ return [{ value, label: parsed.name, icon: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" }) }, ...opts];
133
+ }
134
+ return opts;
135
+ }
136
+ export {
137
+ WorkloadPicker,
138
+ buildWorkloadOptions,
139
+ kindForValue,
140
+ loadedWorkloads,
141
+ parseWorkloadKey,
142
+ workloadKey
143
+ };
144
+ //# sourceMappingURL=WorkloadPicker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkloadPicker.js","sources":["../../src/components/WorkloadPicker.tsx"],"sourcesContent":["import { useEffect, useMemo, useState, type ComponentType } from \"react\";\nimport {\n K8SService,\n K8SIngress,\n K8SDeployment,\n K8SStatefulset,\n} from \"@flanksource/icons/mi\";\nimport { cn } from \"../lib/utils\";\nimport { Combobox, type ComboboxOption } from \"./Combobox\";\n\n// WorkloadPicker selects a backing Kubernetes workload (Service / Ingress /\n// Deployment / StatefulSet) for an endpoint. Options from every kind are merged\n// into one Combobox, grouped by kind via the Combobox group-header support and\n// labelled with the kind's icon.\n//\n// The emitted value is a `[namespace/]kind/name` key (see workloadKey /\n// parseWorkloadKey) so two workloads of different kinds that share a name don't\n// collide. An ingress's name-part is its first host (the routable address),\n// labelled with the ingress name for context; every other kind uses its name.\n//\n// The component is presentational: it owns the kind→group/icon mapping and the\n// key encoding but fetches nothing — the consumer supplies an async\n// `loadWorkloads` getter.\n\nexport type WorkloadKind = \"service\" | \"ingress\" | \"deployment\" | \"statefulset\";\n\n/** One workload returned by the loader. Ingresses may carry rule hostnames. */\nexport type WorkloadResource = { name: string; hosts?: string[] };\n\ntype KindIconProps = { className?: string; title?: string; \"aria-label\"?: string };\ntype KindMeta = { label: string; Icon: ComponentType<KindIconProps> };\n\n// WORKLOAD_META maps each kind to its group header label and icon, in display\n// order. The order of the keys drives the order options/groups appear in.\nconst WORKLOAD_META: Record<WorkloadKind, KindMeta> = {\n service: { label: \"Service\", Icon: K8SService },\n ingress: { label: \"Ingress\", Icon: K8SIngress },\n deployment: { label: \"Deployment\", Icon: K8SDeployment },\n statefulset: { label: \"StatefulSet\", Icon: K8SStatefulset },\n};\n\nconst ALL_KINDS: WorkloadKind[] = [\"service\", \"ingress\", \"deployment\", \"statefulset\"];\n\nexport type WorkloadPickerProps = {\n /**\n * Controlled selected value — a `[namespace/]kind/name` key (see\n * {@link workloadKey} / {@link parseWorkloadKey}).\n */\n value: string;\n /** Called with the chosen `[namespace/]kind/name` key (or \"\" when cleared). */\n onChange: (value: string) => void;\n /**\n * Async getter the component calls to load the requested kinds' workloads.\n * Returns a map keyed by kind. The consumer owns fetching/caching.\n */\n loadWorkloads: (kinds: WorkloadKind[]) => Promise<Record<WorkloadKind, WorkloadResource[]>>;\n /**\n * Namespace the workloads live in. When set it prefixes the emitted key\n * (`namespace/kind/name`), so values stay distinct across namespaces.\n */\n namespace?: string;\n /** Kinds to offer, in display order. Defaults to all four. */\n kinds?: WorkloadKind[];\n /**\n * When true, a non-empty value that does not match any loaded workload (once\n * loading settles) is flagged invalid — the picked workload doesn't exist in\n * the namespace. The value is still shown (pinned) so the user can see and\n * fix it; freeform entry is not rejected, only marked.\n */\n strict?: boolean;\n placeholder?: string;\n className?: string;\n};\n\nexport function WorkloadPicker({\n value,\n onChange,\n loadWorkloads,\n namespace,\n kinds = ALL_KINDS,\n strict = false,\n placeholder = \"Select workload / service…\",\n className,\n}: WorkloadPickerProps) {\n const [byKind, setByKind] = useState<Partial<Record<WorkloadKind, WorkloadResource[]>>>({});\n const [loading, setLoading] = useState(false);\n\n const kindsKey = kinds.join(\",\");\n useEffect(() => {\n let cancelled = false;\n setLoading(true);\n loadWorkloads(kinds)\n .then((res) => {\n if (!cancelled) setByKind(res);\n })\n .finally(() => {\n if (!cancelled) setLoading(false);\n });\n return () => {\n cancelled = true;\n };\n // loadWorkloads is expected to be stable (memoized by the consumer); kindsKey\n // is the joined `kinds` so a changed selection reloads without depending on\n // the array's identity.\n }, [loadWorkloads, kindsKey]);\n\n const options = useMemo(\n () => buildWorkloadOptions(namespace, byKind, kinds, value),\n [namespace, byKind, kinds, value],\n );\n\n // In strict mode a non-empty value is invalid once loading settles unless it\n // matches a loaded workload — either by its full `[namespace/]kind/name` key\n // (what the picker emits) or by its bare name-part (what consumers that\n // persist only the resolved name, e.g. a Service name or ingress host, feed\n // back in). buildWorkloadOptions pins an unmatched value first, so test\n // membership against the loaded set, not `options`.\n const invalid = useMemo(() => {\n if (!strict || !value || loading) return false;\n const { keys, names } = loadedWorkloads(namespace, byKind, kinds);\n return !keys.has(value) && !names.has(value);\n }, [strict, value, loading, namespace, byKind, kinds]);\n\n // The lead icon reflects the selected workload's kind (read from the value's\n // key), falling back to the first offered kind when nothing is selected or the\n // value carries no recognised kind (e.g. freeform input).\n const selectedKind = kindForValue(kinds, value);\n const leadMeta = WORKLOAD_META[selectedKind];\n const LeadIcon = leadMeta.Icon;\n return (\n <div className={cn(\"flex items-center gap-2\", className)}>\n <LeadIcon\n className=\"h-4 w-4 shrink-0 text-muted-foreground\"\n title={leadMeta.label}\n aria-label={leadMeta.label}\n />\n <div className=\"min-w-0 flex-1\">\n <Combobox\n options={options}\n value={value}\n onChange={onChange}\n allowCustomValue\n loading={loading}\n invalid={invalid}\n placeholder={placeholder}\n />\n </div>\n </div>\n );\n}\n\n// ingressHost is the first host an ingress routes, or undefined for non-ingress\n// kinds and host-less ingresses. An ingress's name-part is its host.\nfunction ingressHost(kind: WorkloadKind, r: WorkloadResource): string | undefined {\n return kind === \"ingress\" ? r.hosts?.[0] : undefined;\n}\n\n// workloadName is the name-part of a resource's key: an ingress's first host\n// (the routable address callers want), else the resource's own name.\nfunction workloadName(kind: WorkloadKind, r: WorkloadResource): string {\n return ingressHost(kind, r) ?? r.name;\n}\n\n// ParsedWorkloadKey is the structured form of a `[namespace/]kind/name` value.\nexport type ParsedWorkloadKey = { namespace?: string; kind?: WorkloadKind; name: string };\n\nconst KNOWN_KINDS = new Set<string>([\"service\", \"ingress\", \"deployment\", \"statefulset\"]);\n\n// workloadKey is the value a resource contributes (what onChange emits): a\n// `[namespace/]kind/name` composite so two workloads of different kinds sharing\n// a name don't collide. namespace is omitted when not supplied.\nexport function workloadKey(\n namespace: string | undefined,\n kind: WorkloadKind,\n r: WorkloadResource,\n): string {\n const name = workloadName(kind, r);\n return namespace ? `${namespace}/${kind}/${name}` : `${kind}/${name}`;\n}\n\n// parseWorkloadKey splits a `[namespace/]kind/name` value back into its parts.\n// The trailing segment is always the name (a host may itself contain no slash);\n// the segment before it is the kind when it is a known workload kind, and any\n// remaining leading segment is the namespace. A bare value (no recognised\n// kind/name shape) is returned as just its name so freeform input survives.\nexport function parseWorkloadKey(value: string): ParsedWorkloadKey {\n const parts = value.split(\"/\");\n const name = parts.at(-1);\n const maybeKind = parts.at(-2);\n if (name != null && maybeKind != null && KNOWN_KINDS.has(maybeKind)) {\n const namespace = parts.slice(0, -2).join(\"/\");\n const parsed: ParsedWorkloadKey = { kind: maybeKind as WorkloadKind, name };\n if (namespace) parsed.namespace = namespace;\n return parsed;\n }\n return { name: value };\n}\n\n// loadedWorkloads collapses the loaded resources into the two membership sets a\n// strict picker validates a value against: `keys` are the full\n// `[namespace/]kind/name` keys the picker emits, `names` are the bare name-parts\n// (service name / ingress host) a consumer may persist instead. A value in\n// neither set names a workload that doesn't exist in the namespace.\nexport function loadedWorkloads(\n namespace: string | undefined,\n byKind: Partial<Record<WorkloadKind, WorkloadResource[]>>,\n kinds: WorkloadKind[],\n): { keys: Set<string>; names: Set<string> } {\n const keys = new Set<string>();\n const names = new Set<string>();\n for (const kind of kinds) {\n for (const r of byKind[kind] ?? []) {\n keys.add(workloadKey(namespace, kind, r));\n names.add(workloadName(kind, r));\n }\n }\n return { keys, names };\n}\n\n// kindForValue returns the kind encoded in the selected value, so the lead icon\n// can mirror it without consulting the loaded resources. Falls back to the first\n// offered kind when nothing is selected or the value carries no recognised kind.\nexport function kindForValue(kinds: WorkloadKind[], value: string): WorkloadKind {\n const kind = value ? parseWorkloadKey(value).kind : undefined;\n return kind ?? kinds[0] ?? \"service\";\n}\n\n// buildWorkloadOptions flattens the per-kind resources into grouped Combobox\n// options in kind order. Each option's value is its `[namespace/]kind/name` key\n// and its label the human name; a current value not present in any kind is\n// pinned first (labelled with just its name-part) so the selection stays visible.\nexport function buildWorkloadOptions(\n namespace: string | undefined,\n byKind: Partial<Record<WorkloadKind, WorkloadResource[]>>,\n kinds: WorkloadKind[],\n value: string,\n): ComboboxOption[] {\n const opts: ComboboxOption[] = [];\n for (const kind of kinds) {\n const meta = WORKLOAD_META[kind];\n const Icon = meta.Icon;\n for (const r of byKind[kind] ?? []) {\n const host = ingressHost(kind, r);\n opts.push({\n value: workloadKey(namespace, kind, r),\n // The label is the human name; an ingress pairs its host with the\n // ingress name for context.\n label: host ? `${host} (${r.name})` : r.name,\n group: meta.label,\n icon: <Icon className=\"h-4 w-4\" />,\n });\n }\n }\n if (value && !opts.some((o) => o.value === value)) {\n const parsed = parseWorkloadKey(value);\n const Icon = WORKLOAD_META[parsed.kind ?? kinds[0] ?? \"service\"].Icon;\n return [{ value, label: parsed.name, icon: <Icon className=\"h-4 w-4\" /> }, ...opts];\n }\n return opts;\n}\n"],"names":[],"mappings":"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"}
@@ -2,9 +2,11 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const jsxRuntime = require("react/jsx-runtime");
4
4
  const react = require("react");
5
+ const react$1 = require("@floating-ui/react");
5
6
  const utils = require("../lib/utils.cjs");
6
7
  const Icon = require("../data/Icon.cjs");
7
8
  const button = require("./button.cjs");
9
+ const IconButton = require("./IconButton.cjs");
8
10
  const UiChevronDown = require("../icons/components/UiChevronDown.cjs");
9
11
  const UiCheck = require("../icons/components/UiCheck.cjs");
10
12
  function IconMenuPickerInner({
@@ -18,137 +20,104 @@ function IconMenuPickerInner({
18
20
  className,
19
21
  triggerClassName,
20
22
  menuClassName
21
- }, ref) {
22
- const rootRef = react.useRef(null);
23
- const triggerRef = react.useRef(null);
24
- const itemRefs = react.useRef([]);
23
+ }, forwardedRef) {
25
24
  const [open, setOpen] = react.useState(false);
26
- react.useImperativeHandle(ref, () => rootRef.current);
27
- const activeIndex = react.useMemo(
28
- () => Math.max(
29
- 0,
30
- options.findIndex((option) => option.value === value)
31
- ),
25
+ const [activeIndex, setActiveIndex] = react.useState(null);
26
+ const listRef = react.useRef([]);
27
+ const labelsRef = react.useRef(options.map((o) => o.label));
28
+ labelsRef.current = options.map((o) => o.label);
29
+ const selectedIndex = react.useMemo(
30
+ () => options.findIndex((option) => option.value === value),
32
31
  [options, value]
33
32
  );
34
- const close = react.useCallback((returnFocus) => {
35
- var _a;
33
+ const { refs, floatingStyles, context } = react$1.useFloating({
34
+ open,
35
+ onOpenChange: setOpen,
36
+ placement: "bottom-start",
37
+ whileElementsMounted: react$1.autoUpdate,
38
+ // offset(6) reproduces the previous 0.375rem gap; flip opens the menu
39
+ // upward when the trigger is near the bottom edge; shift slides it back
40
+ // on-screen at the left/right edges. The 8px padding keeps a gutter.
41
+ middleware: [react$1.offset(6), react$1.flip({ padding: 8 }), react$1.shift({ padding: 8 })]
42
+ });
43
+ const click = react$1.useClick(context);
44
+ const dismiss = react$1.useDismiss(context);
45
+ const role = react$1.useRole(context, { role: "menu" });
46
+ const listNav = react$1.useListNavigation(context, {
47
+ listRef,
48
+ activeIndex,
49
+ selectedIndex,
50
+ onNavigate: setActiveIndex,
51
+ loop: true
52
+ });
53
+ const typeahead = react$1.useTypeahead(context, {
54
+ listRef: labelsRef,
55
+ activeIndex,
56
+ onMatch: setActiveIndex
57
+ });
58
+ const { getReferenceProps, getFloatingProps, getItemProps } = react$1.useInteractions([
59
+ click,
60
+ dismiss,
61
+ role,
62
+ listNav,
63
+ typeahead
64
+ ]);
65
+ const referenceRef = react$1.useMergeRefs([refs.setReference, forwardedRef]);
66
+ const select = (next) => {
67
+ onChange(next);
36
68
  setOpen(false);
37
- if (returnFocus) (_a = triggerRef.current) == null ? void 0 : _a.focus();
38
- }, []);
39
- const select = react.useCallback(
40
- (next) => {
41
- onChange(next);
42
- close(true);
43
- },
44
- [close, onChange]
45
- );
46
- react.useEffect(() => {
47
- if (!open) return;
48
- const onPointerDown = (event) => {
49
- var _a;
50
- if (event.button !== 0) return;
51
- if (!((_a = rootRef.current) == null ? void 0 : _a.contains(event.target))) {
52
- close(false);
53
- }
54
- };
55
- const onKeyDown = (event) => {
56
- if (event.key === "Escape") {
57
- event.preventDefault();
58
- close(true);
59
- }
60
- };
61
- document.addEventListener("pointerdown", onPointerDown);
62
- document.addEventListener("keydown", onKeyDown);
63
- return () => {
64
- document.removeEventListener("pointerdown", onPointerDown);
65
- document.removeEventListener("keydown", onKeyDown);
66
- };
67
- }, [close, open]);
68
- react.useEffect(() => {
69
- if (!open) return;
70
- const node = itemRefs.current[activeIndex];
71
- node == null ? void 0 : node.focus();
72
- }, [activeIndex, open]);
73
- const focusItem = (index) => {
74
- var _a;
75
- const wrapped = (index + options.length) % options.length;
76
- (_a = itemRefs.current[wrapped]) == null ? void 0 : _a.focus();
77
69
  };
78
- const onMenuKeyDown = (event) => {
79
- const focusedIndex = itemRefs.current.findIndex((node) => node === document.activeElement);
80
- if (event.key === "ArrowDown") {
81
- event.preventDefault();
82
- focusItem((focusedIndex < 0 ? activeIndex : focusedIndex) + 1);
83
- return;
84
- }
85
- if (event.key === "ArrowUp") {
86
- event.preventDefault();
87
- focusItem((focusedIndex < 0 ? activeIndex : focusedIndex) - 1);
88
- return;
89
- }
90
- if (event.key === "Home") {
91
- event.preventDefault();
92
- focusItem(0);
93
- return;
94
- }
95
- if (event.key === "End") {
96
- event.preventDefault();
97
- focusItem(options.length - 1);
98
- return;
99
- }
100
- if (event.key === "Tab") {
101
- close(false);
102
- }
103
- };
104
- const onTriggerKeyDown = (event) => {
105
- if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
106
- event.preventDefault();
107
- setOpen(true);
108
- }
109
- };
110
- const selected = options[activeIndex];
70
+ const selected = options[selectedIndex];
111
71
  if (!selected) {
112
72
  throw new Error(`IconMenuPicker: value "${value}" not found in options for ${ariaLabel}`);
113
73
  }
114
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: rootRef, className: utils.cn("relative inline-flex", showLabel && "w-full", className), children: [
115
- /* @__PURE__ */ jsxRuntime.jsxs(
74
+ const triggerTitleText = triggerTitle ?? `${ariaLabel}: ${selected.label}`;
75
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: referenceRef, className: utils.cn("relative inline-flex", showLabel && "w-full", className), children: [
76
+ showLabel ? /* @__PURE__ */ jsxRuntime.jsxs(
116
77
  button.Button,
117
78
  {
118
- ref: triggerRef,
119
79
  type: "button",
120
80
  variant: "ghost",
121
- size: showLabel ? "default" : "icon",
81
+ size: "default",
122
82
  "aria-label": ariaLabel,
123
83
  "aria-haspopup": "menu",
124
84
  "aria-expanded": open,
125
- title: triggerTitle ?? `${ariaLabel}: ${selected.label}`,
126
- onClick: () => setOpen((current) => !current),
127
- onKeyDown: onTriggerKeyDown,
85
+ title: triggerTitleText,
128
86
  className: utils.cn(
129
- "text-muted-foreground hover:text-foreground",
130
- showLabel && "w-full justify-start",
87
+ "w-full justify-start text-muted-foreground hover:text-foreground",
131
88
  triggerClassName
132
89
  ),
90
+ ...getReferenceProps(),
133
91
  children: [
134
- /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { icon: selected.icon, ...showLabel ? { className: "shrink-0 text-foreground" } : {} }),
135
- showLabel && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
136
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate text-left capitalize", children: `${ariaLabel}: ${selected.label}` }),
137
- /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { icon: UiChevronDown.UiChevronDown, className: "shrink-0 text-muted-foreground" })
138
- ] })
92
+ /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { icon: selected.icon, className: "shrink-0 text-foreground" }),
93
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate text-left capitalize", children: `${ariaLabel}: ${selected.label}` }),
94
+ /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { icon: UiChevronDown.UiChevronDown, className: "shrink-0 text-muted-foreground" })
139
95
  ]
140
96
  }
97
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
98
+ IconButton.IconButton,
99
+ {
100
+ icon: selected.icon,
101
+ label: ariaLabel,
102
+ title: triggerTitleText,
103
+ "aria-haspopup": "menu",
104
+ "aria-expanded": open,
105
+ className: triggerClassName,
106
+ ...getReferenceProps()
107
+ }
141
108
  ),
142
- open && /* @__PURE__ */ jsxRuntime.jsxs(
109
+ open && /* @__PURE__ */ jsxRuntime.jsx(react$1.FloatingPortal, { children: /* @__PURE__ */ jsxRuntime.jsx(react$1.FloatingFocusManager, { context, modal: false, initialFocus: selectedIndex, children: /* @__PURE__ */ jsxRuntime.jsxs(
143
110
  "div",
144
111
  {
112
+ ref: refs.setFloating,
145
113
  role: "menu",
146
114
  "aria-label": ariaLabel,
147
- onKeyDown: onMenuKeyDown,
115
+ style: floatingStyles,
148
116
  className: utils.cn(
149
- "absolute left-0 top-[calc(100%+0.375rem)] z-50 min-w-[12rem] rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg shadow-black/5",
117
+ "z-[9999] min-w-[12rem] rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg shadow-black/5",
150
118
  menuClassName
151
119
  ),
120
+ ...getFloatingProps(),
152
121
  children: [
153
122
  options.map((option, index) => {
154
123
  const active = option.value === value;
@@ -156,24 +125,26 @@ function IconMenuPickerInner({
156
125
  "button",
157
126
  {
158
127
  ref: (node) => {
159
- itemRefs.current[index] = node;
128
+ listRef.current[index] = node;
160
129
  },
161
130
  type: "button",
162
131
  role: "menuitemradio",
163
132
  "aria-checked": active,
164
- tabIndex: active ? 0 : -1,
165
- onClick: () => select(option.value),
166
- onKeyDown: (event) => {
167
- if (event.key === "Enter" || event.key === " ") {
168
- event.preventDefault();
169
- select(option.value);
170
- }
171
- },
133
+ tabIndex: index === activeIndex ? 0 : -1,
172
134
  className: utils.cn(
173
135
  "flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm transition-colors",
174
136
  "hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none",
175
137
  active && "text-foreground"
176
138
  ),
139
+ ...getItemProps({
140
+ onClick: () => select(option.value),
141
+ onKeyDown: (event) => {
142
+ if (event.key === "Enter" || event.key === " ") {
143
+ event.preventDefault();
144
+ select(option.value);
145
+ }
146
+ }
147
+ }),
177
148
  children: [
178
149
  /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { icon: option.icon, className: "shrink-0 text-muted-foreground" }),
179
150
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 truncate capitalize", children: option.label }),
@@ -186,7 +157,7 @@ function IconMenuPickerInner({
186
157
  footer ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1 border-t border-border/60 px-2 py-1.5 text-[11px] text-muted-foreground", children: footer }) : null
187
158
  ]
188
159
  }
189
- )
160
+ ) }) })
190
161
  ] });
191
162
  }
192
163
  const IconMenuPicker = react.forwardRef(IconMenuPickerInner);
@@ -1 +1 @@
1
- {"version":3,"file":"icon-menu-picker.cjs","sources":["../../src/components/icon-menu-picker.tsx"],"sourcesContent":["import {\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n type KeyboardEvent as ReactKeyboardEvent,\n type ReactNode,\n} from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon, type StaticIconComponent } from \"../data/Icon\";\nimport { UiCheck, UiChevronDown } from \"../icons\";\nimport { Button } from \"./button\";\n\nexport type IconMenuOption<T extends string> = {\n value: T;\n icon: StaticIconComponent;\n label: string;\n description?: string;\n};\n\nexport type IconMenuPickerProps<T extends string> = {\n value: T;\n onChange: (next: T) => void;\n options: IconMenuOption<T>[];\n ariaLabel: string;\n triggerTitle?: string | undefined;\n footer?: ReactNode | undefined;\n showLabel?: boolean | undefined;\n className?: string | undefined;\n triggerClassName?: string | undefined;\n menuClassName?: string | undefined;\n};\n\nfunction IconMenuPickerInner<T extends string>(\n {\n value,\n onChange,\n options,\n ariaLabel,\n triggerTitle,\n footer,\n showLabel,\n className,\n triggerClassName,\n menuClassName,\n }: IconMenuPickerProps<T>,\n ref: React.Ref<HTMLDivElement>,\n) {\n const rootRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLButtonElement>(null);\n const itemRefs = useRef<(HTMLButtonElement | null)[]>([]);\n const [open, setOpen] = useState(false);\n\n useImperativeHandle(ref, () => rootRef.current as HTMLDivElement);\n\n const activeIndex = useMemo(\n () =>\n Math.max(\n 0,\n options.findIndex((option) => option.value === value),\n ),\n [options, value],\n );\n\n const close = useCallback((returnFocus: boolean) => {\n setOpen(false);\n if (returnFocus) triggerRef.current?.focus();\n }, []);\n\n const select = useCallback(\n (next: T) => {\n onChange(next);\n close(true);\n },\n [close, onChange],\n );\n\n useEffect(() => {\n if (!open) return;\n\n const onPointerDown = (event: PointerEvent) => {\n if (event.button !== 0) return;\n if (!rootRef.current?.contains(event.target as Node)) {\n close(false);\n }\n };\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n close(true);\n }\n };\n\n document.addEventListener(\"pointerdown\", onPointerDown);\n document.addEventListener(\"keydown\", onKeyDown);\n return () => {\n document.removeEventListener(\"pointerdown\", onPointerDown);\n document.removeEventListener(\"keydown\", onKeyDown);\n };\n }, [close, open]);\n\n useEffect(() => {\n if (!open) return;\n const node = itemRefs.current[activeIndex];\n node?.focus();\n }, [activeIndex, open]);\n\n const focusItem = (index: number) => {\n const wrapped = (index + options.length) % options.length;\n itemRefs.current[wrapped]?.focus();\n };\n\n const onMenuKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {\n const focusedIndex = itemRefs.current.findIndex((node) => node === document.activeElement);\n if (event.key === \"ArrowDown\") {\n event.preventDefault();\n focusItem((focusedIndex < 0 ? activeIndex : focusedIndex) + 1);\n return;\n }\n if (event.key === \"ArrowUp\") {\n event.preventDefault();\n focusItem((focusedIndex < 0 ? activeIndex : focusedIndex) - 1);\n return;\n }\n if (event.key === \"Home\") {\n event.preventDefault();\n focusItem(0);\n return;\n }\n if (event.key === \"End\") {\n event.preventDefault();\n focusItem(options.length - 1);\n return;\n }\n if (event.key === \"Tab\") {\n close(false);\n }\n };\n\n const onTriggerKeyDown = (event: ReactKeyboardEvent<HTMLButtonElement>) => {\n if (event.key === \"ArrowDown\" || event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n setOpen(true);\n }\n };\n\n const selected = options[activeIndex];\n if (!selected) {\n throw new Error(`IconMenuPicker: value \"${value}\" not found in options for ${ariaLabel}`);\n }\n\n return (\n <div ref={rootRef} className={cn(\"relative inline-flex\", showLabel && \"w-full\", className)}>\n <Button\n ref={triggerRef}\n type=\"button\"\n variant=\"ghost\"\n size={showLabel ? \"default\" : \"icon\"}\n aria-label={ariaLabel}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n title={triggerTitle ?? `${ariaLabel}: ${selected.label}`}\n onClick={() => setOpen((current) => !current)}\n onKeyDown={onTriggerKeyDown}\n className={cn(\n \"text-muted-foreground hover:text-foreground\",\n showLabel && \"w-full justify-start\",\n triggerClassName,\n )}\n >\n <Icon icon={selected.icon} {...(showLabel ? { className: \"shrink-0 text-foreground\" } : {})} />\n {showLabel && (\n <>\n <span className=\"min-w-0 flex-1 truncate text-left capitalize\">\n {`${ariaLabel}: ${selected.label}`}\n </span>\n <Icon icon={UiChevronDown} className=\"shrink-0 text-muted-foreground\" />\n </>\n )}\n </Button>\n {open && (\n <div\n role=\"menu\"\n aria-label={ariaLabel}\n onKeyDown={onMenuKeyDown}\n className={cn(\n \"absolute left-0 top-[calc(100%+0.375rem)] z-50 min-w-[12rem] rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg shadow-black/5\",\n menuClassName,\n )}\n >\n {options.map((option, index) => {\n const active = option.value === value;\n return (\n <button\n key={option.value}\n ref={(node) => {\n itemRefs.current[index] = node;\n }}\n type=\"button\"\n role=\"menuitemradio\"\n aria-checked={active}\n tabIndex={active ? 0 : -1}\n onClick={() => select(option.value)}\n onKeyDown={(event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n select(option.value);\n }\n }}\n className={cn(\n \"flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm transition-colors\",\n \"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none\",\n active && \"text-foreground\",\n )}\n >\n <Icon icon={option.icon} className=\"shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 flex-1 truncate capitalize\">{option.label}</span>\n {active ? (\n <Icon icon={UiCheck} className=\"shrink-0 text-foreground\" />\n ) : (\n <span className=\"inline-block size-4 shrink-0\" aria-hidden />\n )}\n </button>\n );\n })}\n {footer ? (\n <div className=\"mt-1 border-t border-border/60 px-2 py-1.5 text-[11px] text-muted-foreground\">\n {footer}\n </div>\n ) : null}\n </div>\n )}\n </div>\n );\n}\n\nexport const IconMenuPicker = forwardRef(IconMenuPickerInner) as <T extends string>(\n props: IconMenuPickerProps<T> & { ref?: React.Ref<HTMLDivElement> },\n) => JSX.Element;\n"],"names":["useRef","useState","useImperativeHandle","useMemo","useCallback","useEffect","jsxs","cn","Button","jsx","Icon","Fragment","UiChevronDown","UiCheck","forwardRef"],"mappings":"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"}
1
+ {"version":3,"file":"icon-menu-picker.cjs","sources":["../../src/components/icon-menu-picker.tsx"],"sourcesContent":["import {\n forwardRef,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport {\n autoUpdate,\n flip,\n FloatingFocusManager,\n FloatingPortal,\n offset,\n shift,\n useClick,\n useDismiss,\n useFloating,\n useInteractions,\n useListNavigation,\n useMergeRefs,\n useRole,\n useTypeahead,\n} from \"@floating-ui/react\";\nimport { cn } from \"../lib/utils\";\nimport { Icon, type StaticIconComponent } from \"../data/Icon\";\nimport { UiCheck, UiChevronDown } from \"../icons\";\nimport { Button } from \"./button\";\nimport { IconButton } from \"./IconButton\";\n\nexport type IconMenuOption<T extends string> = {\n value: T;\n icon: StaticIconComponent;\n label: string;\n description?: string;\n};\n\nexport type IconMenuPickerProps<T extends string> = {\n value: T;\n onChange: (next: T) => void;\n options: IconMenuOption<T>[];\n ariaLabel: string;\n triggerTitle?: string | undefined;\n footer?: ReactNode | undefined;\n showLabel?: boolean | undefined;\n className?: string | undefined;\n triggerClassName?: string | undefined;\n menuClassName?: string | undefined;\n};\n\nfunction IconMenuPickerInner<T extends string>(\n {\n value,\n onChange,\n options,\n ariaLabel,\n triggerTitle,\n footer,\n showLabel,\n className,\n triggerClassName,\n menuClassName,\n }: IconMenuPickerProps<T>,\n forwardedRef: React.Ref<HTMLDivElement>,\n) {\n const [open, setOpen] = useState(false);\n // Index of the item highlighted by keyboard navigation. Null until the user\n // arrows/typeaheads, so a pointer-opened menu starts with the selected row\n // focused (selectedIndex below) rather than the first.\n const [activeIndex, setActiveIndex] = useState<number | null>(null);\n const listRef = useRef<(HTMLElement | null)[]>([]);\n const labelsRef = useRef<string[]>(options.map((o) => o.label));\n labelsRef.current = options.map((o) => o.label);\n\n const selectedIndex = useMemo(\n () => options.findIndex((option) => option.value === value),\n [options, value],\n );\n\n const { refs, floatingStyles, context } = useFloating<HTMLDivElement>({\n open,\n onOpenChange: setOpen,\n placement: \"bottom-start\",\n whileElementsMounted: autoUpdate,\n // offset(6) reproduces the previous 0.375rem gap; flip opens the menu\n // upward when the trigger is near the bottom edge; shift slides it back\n // on-screen at the left/right edges. The 8px padding keeps a gutter.\n middleware: [offset(6), flip({ padding: 8 }), shift({ padding: 8 })],\n });\n\n const click = useClick(context);\n const dismiss = useDismiss(context);\n const role = useRole(context, { role: \"menu\" });\n const listNav = useListNavigation(context, {\n listRef,\n activeIndex,\n selectedIndex,\n onNavigate: setActiveIndex,\n loop: true,\n });\n const typeahead = useTypeahead(context, {\n listRef: labelsRef,\n activeIndex,\n onMatch: setActiveIndex,\n });\n\n const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([\n click,\n dismiss,\n role,\n listNav,\n typeahead,\n ]);\n\n // Keep the trigger wrapper available both to floating-ui (as the positioning\n // reference) and to the consumer's forwarded ref.\n const referenceRef = useMergeRefs([refs.setReference, forwardedRef]);\n\n const select = (next: T) => {\n onChange(next);\n setOpen(false);\n };\n\n const selected = options[selectedIndex];\n if (!selected) {\n throw new Error(`IconMenuPicker: value \"${value}\" not found in options for ${ariaLabel}`);\n }\n\n const triggerTitleText = triggerTitle ?? `${ariaLabel}: ${selected.label}`;\n\n return (\n <div ref={referenceRef} className={cn(\"relative inline-flex\", showLabel && \"w-full\", className)}>\n {showLabel ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"default\"\n aria-label={ariaLabel}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n title={triggerTitleText}\n className={cn(\n \"w-full justify-start text-muted-foreground hover:text-foreground\",\n triggerClassName,\n )}\n {...getReferenceProps()}\n >\n <Icon icon={selected.icon} className=\"shrink-0 text-foreground\" />\n <span className=\"min-w-0 flex-1 truncate text-left capitalize\">\n {`${ariaLabel}: ${selected.label}`}\n </span>\n <Icon icon={UiChevronDown} className=\"shrink-0 text-muted-foreground\" />\n </Button>\n ) : (\n <IconButton\n icon={selected.icon}\n label={ariaLabel}\n title={triggerTitleText}\n aria-haspopup=\"menu\"\n aria-expanded={open}\n className={triggerClassName}\n {...getReferenceProps()}\n />\n )}\n {open && (\n <FloatingPortal>\n <FloatingFocusManager context={context} modal={false} initialFocus={selectedIndex}>\n <div\n ref={refs.setFloating}\n role=\"menu\"\n aria-label={ariaLabel}\n style={floatingStyles}\n className={cn(\n \"z-[9999] min-w-[12rem] rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg shadow-black/5\",\n menuClassName,\n )}\n {...getFloatingProps()}\n >\n {options.map((option, index) => {\n const active = option.value === value;\n return (\n <button\n key={option.value}\n ref={(node) => {\n listRef.current[index] = node;\n }}\n type=\"button\"\n role=\"menuitemradio\"\n aria-checked={active}\n tabIndex={index === activeIndex ? 0 : -1}\n className={cn(\n \"flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm transition-colors\",\n \"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none\",\n active && \"text-foreground\",\n )}\n {...getItemProps({\n onClick: () => select(option.value),\n onKeyDown: (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n select(option.value);\n }\n },\n })}\n >\n <Icon icon={option.icon} className=\"shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 flex-1 truncate capitalize\">{option.label}</span>\n {active ? (\n <Icon icon={UiCheck} className=\"shrink-0 text-foreground\" />\n ) : (\n <span className=\"inline-block size-4 shrink-0\" aria-hidden />\n )}\n </button>\n );\n })}\n {footer ? (\n <div className=\"mt-1 border-t border-border/60 px-2 py-1.5 text-[11px] text-muted-foreground\">\n {footer}\n </div>\n ) : null}\n </div>\n </FloatingFocusManager>\n </FloatingPortal>\n )}\n </div>\n );\n}\n\nexport const IconMenuPicker = forwardRef(IconMenuPickerInner) as <T extends string>(\n props: IconMenuPickerProps<T> & { ref?: React.Ref<HTMLDivElement> },\n) => JSX.Element;\n"],"names":["useState","useRef","useMemo","useFloating","autoUpdate","offset","flip","shift","useClick","useDismiss","useRole","useListNavigation","useTypeahead","useInteractions","useMergeRefs","jsxs","cn","Button","jsx","Icon","UiChevronDown","IconButton","FloatingPortal","FloatingFocusManager","UiCheck","forwardRef"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"icon-menu-picker.d.ts","sourceRoot":"","sources":["../../src/components/icon-menu-picker.tsx"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,EAAQ,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAI9D,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,MAAM,IAAI;IAC7C,KAAK,EAAE,CAAC,CAAC;IACT,IAAI,EAAE,mBAAmB,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,MAAM,IAAI;IAClD,KAAK,EAAE,CAAC,CAAC;IACT,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC/B,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC,CAAC;AA8MF,eAAO,MAAM,cAAc,EAAsC,CAAC,CAAC,SAAS,MAAM,EAChF,KAAK,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;CAAE,KAChE,GAAG,CAAC,OAAO,CAAC"}
1
+ {"version":3,"file":"icon-menu-picker.d.ts","sourceRoot":"","sources":["../../src/components/icon-menu-picker.tsx"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAkBf,OAAO,EAAQ,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAK9D,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,MAAM,IAAI;IAC7C,KAAK,EAAE,CAAC,CAAC;IACT,IAAI,EAAE,mBAAmB,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,MAAM,IAAI;IAClD,KAAK,EAAE,CAAC,CAAC;IACT,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC/B,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC,CAAC;AAoLF,eAAO,MAAM,cAAc,EAAsC,CAAC,CAAC,SAAS,MAAM,EAChF,KAAK,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;CAAE,KAChE,GAAG,CAAC,OAAO,CAAC"}