@geomak/ui 6.26.1 → 6.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { colors_default } from './chunk-I2P4JJDB.js';
2
2
  export { colors_default as COLORS, PALETTE as palette, semanticTokens, vars } from './chunk-I2P4JJDB.js';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
- import React29, { createContext, useState, useEffect, useMemo, useId, useCallback, useRef, useContext, useSyncExternalStore, useLayoutEffect } from 'react';
4
+ import React28, { createContext, useState, useEffect, useMemo, useId, useCallback, useRef, useContext, useSyncExternalStore, useLayoutEffect } from 'react';
5
5
  import { createPortal } from 'react-dom';
6
6
  import * as AvatarPrimitive from '@radix-ui/react-avatar';
7
7
  import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
@@ -693,7 +693,7 @@ var SIZE_CLASSES = {
693
693
  md: "h-9 px-4 text-sm gap-1.5 rounded-lg",
694
694
  lg: "h-11 px-5 text-sm gap-2 rounded-xl"
695
695
  };
696
- var Button = React29.forwardRef(function Button2({
696
+ var Button = React28.forwardRef(function Button2({
697
697
  content,
698
698
  variant = "primary",
699
699
  size = "md",
@@ -801,7 +801,7 @@ function MenuButton({
801
801
  "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
802
802
  "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95"
803
803
  ].join(" "),
804
- children: items.map((item) => /* @__PURE__ */ jsxs(React29.Fragment, { children: [
804
+ children: items.map((item) => /* @__PURE__ */ jsxs(React28.Fragment, { children: [
805
805
  item.separatorBefore && /* @__PURE__ */ jsx(DropdownMenu.Separator, { className: "my-1 h-px bg-border" }),
806
806
  /* @__PURE__ */ jsxs(
807
807
  DropdownMenu.Item,
@@ -1900,7 +1900,7 @@ function Kbd({
1900
1900
  style
1901
1901
  }) {
1902
1902
  if (keys && keys.length > 0) {
1903
- return /* @__PURE__ */ jsx("span", { className: ["inline-flex items-center gap-1", className].filter(Boolean).join(" "), style, children: keys.map((k, i) => /* @__PURE__ */ jsxs(React29.Fragment, { children: [
1903
+ return /* @__PURE__ */ jsx("span", { className: ["inline-flex items-center gap-1", className].filter(Boolean).join(" "), style, children: keys.map((k, i) => /* @__PURE__ */ jsxs(React28.Fragment, { children: [
1904
1904
  i > 0 && /* @__PURE__ */ jsx("span", { className: "text-foreground-muted text-xs select-none", children: separator }),
1905
1905
  /* @__PURE__ */ jsx("kbd", { className: [cap, SIZE3[size]].join(" "), children: k })
1906
1906
  ] }, `${k}-${i}`)) });
@@ -1992,7 +1992,7 @@ function FlatCarousel({
1992
1992
  style
1993
1993
  }) {
1994
1994
  const scrollerRef = useRef(null);
1995
- const slides = React29.Children.toArray(children);
1995
+ const slides = React28.Children.toArray(children);
1996
1996
  const [active, setActive] = useState(0);
1997
1997
  const [atStart, setAtStart] = useState(true);
1998
1998
  const [atEnd, setAtEnd] = useState(false);
@@ -2047,7 +2047,7 @@ function RotatingCarousel({
2047
2047
  className = "",
2048
2048
  style
2049
2049
  }) {
2050
- const slides = React29.Children.toArray(children);
2050
+ const slides = React28.Children.toArray(children);
2051
2051
  const count = slides.length;
2052
2052
  const [active, setActive] = useState(0);
2053
2053
  const reduced = useReducedMotion();
@@ -2910,6 +2910,7 @@ function Scheduler({
2910
2910
  onSelectSlot,
2911
2911
  onSelectEvent,
2912
2912
  onNewEvent,
2913
+ onError,
2913
2914
  className = "",
2914
2915
  style
2915
2916
  }) {
@@ -2918,9 +2919,11 @@ function Scheduler({
2918
2919
  const [cursor, setCursor] = useState(() => defaultDate ?? /* @__PURE__ */ new Date());
2919
2920
  const [loaded, setLoaded] = useState([]);
2920
2921
  const [loading, setLoading] = useState(false);
2922
+ const [error, setError] = useState(null);
2923
+ const [reloadKey, setReloadKey] = useState(0);
2921
2924
  const [dir, setDir] = useState(0);
2922
- const loaderRef = useRef(loadEvents);
2923
- loaderRef.current = loadEvents;
2925
+ const cbRef = useRef({ loadEvents, onError });
2926
+ cbRef.current = { loadEvents, onError };
2924
2927
  const range = useMemo(
2925
2928
  () => view === "month" ? monthRange(cursor) : weekRange(cursor, weekStartsOn),
2926
2929
  [view, cursor, weekStartsOn]
@@ -2928,21 +2931,26 @@ function Scheduler({
2928
2931
  const fromKey = range.from.getTime();
2929
2932
  const toKey = range.to.getTime();
2930
2933
  useEffect(() => {
2931
- const loader = loaderRef.current;
2934
+ const { loadEvents: loader, onError: onErr } = cbRef.current;
2932
2935
  if (!loader) return;
2933
2936
  let cancelled = false;
2934
2937
  setLoading(true);
2938
+ setError(null);
2935
2939
  Promise.resolve(loader({ from: new Date(fromKey), to: new Date(toKey) }, view)).then((evts) => {
2936
2940
  if (!cancelled) setLoaded(evts);
2937
- }).catch(() => {
2938
- if (!cancelled) setLoaded([]);
2941
+ }).catch((err) => {
2942
+ if (!cancelled) {
2943
+ setError(err ?? new Error("Failed to load events"));
2944
+ onErr?.(err);
2945
+ }
2939
2946
  }).finally(() => {
2940
2947
  if (!cancelled) setLoading(false);
2941
2948
  });
2942
2949
  return () => {
2943
2950
  cancelled = true;
2944
2951
  };
2945
- }, [fromKey, toKey, view]);
2952
+ }, [fromKey, toKey, view, reloadKey]);
2953
+ const retry = useCallback(() => setReloadKey((k) => k + 1), []);
2946
2954
  const events = useMemo(
2947
2955
  () => (controlledEvents ?? loaded).map(normalize),
2948
2956
  [controlledEvents, loaded]
@@ -2995,7 +3003,7 @@ function Scheduler({
2995
3003
  onNewEvent && /* @__PURE__ */ jsx(Button_default, { size: "sm", icon: /* @__PURE__ */ jsx(Plus, {}), content: "New event", onClick: onNewEvent })
2996
3004
  ] })
2997
3005
  ] }),
2998
- /* @__PURE__ */ jsx("div", { className: "relative flex-1 overflow-hidden", children: /* @__PURE__ */ jsx(
3006
+ /* @__PURE__ */ jsx("div", { className: "relative flex-1 overflow-hidden", children: error ? /* @__PURE__ */ jsx(SchedulerError, { onRetry: retry }) : loadEvents && loading && events.length === 0 ? /* @__PURE__ */ jsx(SchedulerSkeleton, { view }) : /* @__PURE__ */ jsx(
2999
3007
  motion.div,
3000
3008
  {
3001
3009
  initial: { opacity: 0, x: reduced ? 0 : dir * 24 },
@@ -3030,6 +3038,36 @@ function Scheduler({
3030
3038
  }
3031
3039
  );
3032
3040
  }
3041
+ function SchedulerSkeleton({ view }) {
3042
+ const bar = "rounded bg-background animate-pulse";
3043
+ if (view === "week") {
3044
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col p-3", "aria-hidden": "true", children: [
3045
+ /* @__PURE__ */ jsxs("div", { className: "mb-3 grid gap-2", style: { gridTemplateColumns: "3.5rem repeat(7, 1fr)" }, children: [
3046
+ /* @__PURE__ */ jsx("span", {}),
3047
+ Array.from({ length: 7 }, (_, i) => /* @__PURE__ */ jsx("span", { className: `${bar} mx-auto h-8 w-8 rounded-full` }, i))
3048
+ ] }),
3049
+ /* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col gap-3", children: Array.from({ length: 8 }, (_, i) => /* @__PURE__ */ jsx("span", { className: `${bar} h-6`, style: { width: `${60 + i * 13 % 35}%` } }, i)) })
3050
+ ] });
3051
+ }
3052
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col p-3", "aria-hidden": "true", children: [
3053
+ /* @__PURE__ */ jsx("div", { className: "mb-3 grid grid-cols-7 gap-2", children: Array.from({ length: 7 }, (_, i) => /* @__PURE__ */ jsx("span", { className: `${bar} h-2.5` }, i)) }),
3054
+ /* @__PURE__ */ jsx("div", { className: "grid flex-1 grid-cols-7 grid-rows-5 gap-2", children: Array.from({ length: 35 }, (_, i) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
3055
+ /* @__PURE__ */ jsx("span", { className: `${bar} h-5 w-5 rounded-full` }),
3056
+ i % 3 === 0 && /* @__PURE__ */ jsx("span", { className: `${bar} h-3` }),
3057
+ i % 5 === 0 && /* @__PURE__ */ jsx("span", { className: `${bar} h-3`, style: { width: "70%" } })
3058
+ ] }, i)) })
3059
+ ] });
3060
+ }
3061
+ function SchedulerError({ onRetry }) {
3062
+ return /* @__PURE__ */ jsxs("div", { role: "alert", className: "flex h-full flex-col items-center justify-center gap-3 p-8 text-center", children: [
3063
+ /* @__PURE__ */ jsx("span", { className: "flex h-10 w-10 items-center justify-center rounded-full text-status-error", style: { backgroundColor: "color-mix(in oklab, var(--color-error) 12%, var(--color-surface))" }, children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", className: "h-5 w-5", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M12 9v4m0 4h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h16.94a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z" }) }) }),
3064
+ /* @__PURE__ */ jsxs("div", { children: [
3065
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-semibold text-foreground", children: "Couldn\u2019t load events" }),
3066
+ /* @__PURE__ */ jsx("div", { className: "mt-0.5 text-xs text-foreground-muted", children: "Something went wrong fetching this range." })
3067
+ ] }),
3068
+ /* @__PURE__ */ jsx(Button_default, { size: "sm", variant: "secondary", content: "Retry", onClick: onRetry })
3069
+ ] });
3070
+ }
3033
3071
  function MonthYearPicker({ label, cursor, onPick }) {
3034
3072
  const [open, setOpen] = useState(false);
3035
3073
  const [viewYear, setViewYear] = useState(cursor.getFullYear());
@@ -5164,7 +5202,7 @@ function Wizard({
5164
5202
  ] });
5165
5203
  }
5166
5204
  var SearchIcon = /* @__PURE__ */ jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "currentColor", className: "w-4 h-4", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { fillRule: "evenodd", d: "M10.5 3.75a6.75 6.75 0 100 13.5 6.75 6.75 0 000-13.5zM2.25 10.5a8.25 8.25 0 1114.59 5.28l4.69 4.69a.75.75 0 11-1.06 1.06l-4.69-4.69A8.25 8.25 0 012.25 10.5z", clipRule: "evenodd" }) });
5167
- var SearchInput = React29.forwardRef(function SearchInput2({ value, onChange, disabled, label, htmlFor, placeholder, name, inputStyle, style, layout = "vertical", size = "md", icon, helperText, className }, ref) {
5205
+ var SearchInput = React28.forwardRef(function SearchInput2({ value, onChange, disabled, label, htmlFor, placeholder, name, inputStyle, style, layout = "vertical", size = "md", icon, helperText, className }, ref) {
5168
5206
  return /* @__PURE__ */ jsx(Field, { className, label, htmlFor, layout, helperText, children: /* @__PURE__ */ jsxs(
5169
5207
  "div",
5170
5208
  {
@@ -5193,438 +5231,136 @@ var SearchInput = React29.forwardRef(function SearchInput2({ value, onChange, di
5193
5231
  ) });
5194
5232
  });
5195
5233
  var SearchInput_default = SearchInput;
5196
- function Tag({ children, onRemove, removeLabel, disabled }) {
5197
- return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 rounded-md border border-border bg-surface-raised text-foreground text-xs pl-2 pr-1 py-0.5 max-w-full", children: [
5198
- /* @__PURE__ */ jsx("span", { className: "truncate", children }),
5199
- onRemove && /* @__PURE__ */ jsx(
5200
- "button",
5201
- {
5202
- type: "button",
5203
- disabled,
5204
- onClick: (e) => {
5205
- e.stopPropagation();
5206
- onRemove();
5207
- },
5208
- "aria-label": removeLabel ?? "Remove",
5209
- className: "inline-flex items-center justify-center w-4 h-4 flex-shrink-0 rounded text-foreground-muted hover:text-status-error hover:bg-surface transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-accent disabled:cursor-not-allowed",
5210
- children: /* @__PURE__ */ jsx("svg", { width: "10", height: "10", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M15 5L5 15M5 5l10 10", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) })
5234
+ var SHIMMER = "oxy-skeleton rounded-sm bg-surface-raised";
5235
+ function SkeletonBox({ width, height = 16, radius, className = "", style }) {
5236
+ return /* @__PURE__ */ jsx(
5237
+ "span",
5238
+ {
5239
+ role: "presentation",
5240
+ "aria-hidden": "true",
5241
+ className: `block ${SHIMMER} ${className}`,
5242
+ style: {
5243
+ width: width ?? "100%",
5244
+ height,
5245
+ borderRadius: radius ?? "var(--radius-md)",
5246
+ ...style
5211
5247
  }
5212
- )
5213
- ] });
5248
+ }
5249
+ );
5214
5250
  }
5215
- function MultiTagRow({
5216
- values,
5217
- disabled,
5218
- labelFor,
5219
- onRemove
5251
+ function SkeletonText({
5252
+ lines = 3,
5253
+ lastLineWidth = 60,
5254
+ lineHeight = 14,
5255
+ gap = 8,
5256
+ className = "",
5257
+ style
5220
5258
  }) {
5221
- const wrapRef = useRef(null);
5222
- const measureRef = useRef(null);
5223
- const [visibleCount, setVisibleCount] = useState(values.length);
5224
- const key = values.map(String).join("|");
5225
- useLayoutEffect(() => {
5226
- const wrap = wrapRef.current;
5227
- const measure = measureRef.current;
5228
- if (!wrap || !measure) return;
5229
- const GAP = 6;
5230
- const recompute = () => {
5231
- const avail = wrap.clientWidth;
5232
- const tagEls = Array.from(measure.querySelectorAll("[data-mt]"));
5233
- const moreEl = measure.querySelector("[data-mm]");
5234
- const widths = tagEls.map((e) => e.offsetWidth);
5235
- const moreW = moreEl ? moreEl.offsetWidth : 0;
5236
- if (widths.length === 0) {
5237
- setVisibleCount(0);
5238
- return;
5239
- }
5240
- let used = 0;
5241
- let count = 0;
5242
- for (let i = 0; i < widths.length; i++) {
5243
- const w = widths[i] + (i > 0 ? GAP : 0);
5244
- if (used + w <= avail) {
5245
- used += w;
5246
- count++;
5247
- } else break;
5248
- }
5249
- if (count < widths.length) {
5250
- while (count > 0) {
5251
- let t = 0;
5252
- for (let i = 0; i < count; i++) t += widths[i] + (i > 0 ? GAP : 0);
5253
- t += GAP + moreW;
5254
- if (t <= avail) break;
5255
- count--;
5256
- }
5257
- }
5258
- setVisibleCount(count);
5259
- };
5260
- recompute();
5261
- const ro = new ResizeObserver(recompute);
5262
- ro.observe(wrap);
5263
- return () => ro.disconnect();
5264
- }, [key]);
5265
- const hidden = values.length - visibleCount;
5266
- const moreChip = (n) => /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center flex-shrink-0 rounded-md border border-border bg-surface-raised text-foreground-secondary text-xs px-2 py-0.5", children: [
5267
- "+",
5268
- n,
5269
- " more"
5270
- ] });
5271
- return /* @__PURE__ */ jsxs("div", { ref: wrapRef, className: "relative flex-1 min-w-0 flex flex-nowrap items-center gap-1.5 overflow-hidden", children: [
5272
- /* @__PURE__ */ jsxs(
5273
- "div",
5274
- {
5275
- ref: measureRef,
5276
- "aria-hidden": "true",
5277
- className: "absolute invisible pointer-events-none flex flex-nowrap items-center gap-1.5",
5278
- style: { left: -9999, top: -9999 },
5279
- children: [
5280
- values.map((val) => /* @__PURE__ */ jsx("span", { "data-mt": true, children: /* @__PURE__ */ jsx(Tag, { removeLabel: "x", onRemove: () => {
5281
- }, children: labelFor(val) }) }, `m-${val}`)),
5282
- /* @__PURE__ */ jsx("span", { "data-mm": true, children: moreChip(values.length) })
5283
- ]
5259
+ return /* @__PURE__ */ jsx(
5260
+ "div",
5261
+ {
5262
+ role: "presentation",
5263
+ "aria-hidden": "true",
5264
+ className: `flex flex-col ${className}`,
5265
+ style: { gap, ...style },
5266
+ children: Array.from({ length: lines }).map((_, i) => {
5267
+ const isLast = i === lines - 1;
5268
+ const width = isLast && lines > 1 ? `${lastLineWidth}%` : "100%";
5269
+ return /* @__PURE__ */ jsx(
5270
+ "span",
5271
+ {
5272
+ className: `block ${SHIMMER}`,
5273
+ style: { height: lineHeight, width, borderRadius: "var(--radius-sm)" }
5274
+ },
5275
+ i
5276
+ );
5277
+ })
5278
+ }
5279
+ );
5280
+ }
5281
+ function SkeletonCircle({ size = 40, className = "", style }) {
5282
+ return /* @__PURE__ */ jsx(
5283
+ "span",
5284
+ {
5285
+ role: "presentation",
5286
+ "aria-hidden": "true",
5287
+ className: `block flex-shrink-0 ${SHIMMER} ${className}`,
5288
+ style: {
5289
+ width: size,
5290
+ height: size,
5291
+ borderRadius: "50%",
5292
+ ...style
5284
5293
  }
5285
- ),
5286
- values.slice(0, visibleCount).map((val) => /* @__PURE__ */ jsx(
5287
- Tag,
5294
+ }
5295
+ );
5296
+ }
5297
+ function SkeletonCard({ hasAvatar = true, lines = 3, className = "", style }) {
5298
+ return /* @__PURE__ */ jsxs(
5299
+ "div",
5300
+ {
5301
+ role: "presentation",
5302
+ "aria-hidden": "true",
5303
+ className: `rounded-lg border border-border bg-surface p-4 ${className}`,
5304
+ style,
5305
+ children: [
5306
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-4", children: [
5307
+ hasAvatar && /* @__PURE__ */ jsx(SkeletonCircle, { size: 36 }),
5308
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col gap-2", children: [
5309
+ /* @__PURE__ */ jsx(SkeletonBox, { height: 12, width: "55%" }),
5310
+ /* @__PURE__ */ jsx(SkeletonBox, { height: 10, width: "35%" })
5311
+ ] })
5312
+ ] }),
5313
+ /* @__PURE__ */ jsx(SkeletonText, { lines, lastLineWidth: 55 }),
5314
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 flex gap-2", children: [
5315
+ /* @__PURE__ */ jsx(SkeletonBox, { height: 28, width: 72 }),
5316
+ /* @__PURE__ */ jsx(SkeletonBox, { height: 28, width: 56 })
5317
+ ] })
5318
+ ]
5319
+ }
5320
+ );
5321
+ }
5322
+ var DEFAULT_PICKER = [
5323
+ { key: 1, value: 5, label: 5 },
5324
+ { key: 2, value: 10, label: 10 },
5325
+ { key: 3, value: 15, label: 15 },
5326
+ { key: 4, value: 20, label: 20 }
5327
+ ];
5328
+ var DEFAULT_PAGINATION = {
5329
+ enabled: true,
5330
+ perPage: 15,
5331
+ withPicker: true,
5332
+ pickerOptions: DEFAULT_PICKER
5333
+ };
5334
+ var DEFAULT_EXPAND = {
5335
+ enabled: false
5336
+ };
5337
+ function createDatasets(rows, perPage) {
5338
+ if (!perPage) return [rows.slice()];
5339
+ const all = [];
5340
+ for (let i = 0; i < rows.length; i += perPage) {
5341
+ all.push(rows.slice(i, i + perPage));
5342
+ }
5343
+ return all;
5344
+ }
5345
+ var defaultGetRowKey = (_row, index) => index;
5346
+ var cellAlign = (align) => align === "left" ? "text-left" : align === "right" ? "text-right" : "text-center";
5347
+ function TableHeader({
5348
+ columns,
5349
+ hasExpand
5350
+ }) {
5351
+ return /* @__PURE__ */ jsx("thead", { className: "bg-surface-raised border-b border-border", children: /* @__PURE__ */ jsxs("tr", { children: [
5352
+ hasExpand && /* @__PURE__ */ jsx("th", { "aria-hidden": "true", className: "w-9" }),
5353
+ columns.map((col) => /* @__PURE__ */ jsx(
5354
+ "th",
5288
5355
  {
5289
- disabled,
5290
- removeLabel: `Remove ${labelFor(val)}`,
5291
- onRemove: () => onRemove(val),
5292
- children: labelFor(val)
5356
+ scope: "col",
5357
+ className: `${cellAlign(col.align)} text-sm font-semibold text-foreground py-3 px-3`,
5358
+ style: col.width != null ? { width: col.width } : void 0,
5359
+ children: col.label
5293
5360
  },
5294
- String(val)
5295
- )),
5296
- hidden > 0 && moreChip(hidden)
5297
- ] });
5298
- }
5299
- function Dropdown({
5300
- isMultiselect = false,
5301
- hasSearch = true,
5302
- label,
5303
- name,
5304
- value,
5305
- onChange,
5306
- disabled,
5307
- layout = "horizontal",
5308
- helperText,
5309
- required,
5310
- errorMessage,
5311
- style = {},
5312
- htmlFor,
5313
- items = [],
5314
- labelStyle = {},
5315
- placeholder,
5316
- size = "md",
5317
- className = ""
5318
- }) {
5319
- const [open, setOpen] = useState(false);
5320
- const [selectedItems, setSelectedItems] = useState([]);
5321
- const [searchTerm, setSearchTerm] = useState("");
5322
- const [innerItems, setInnerItems] = useState([]);
5323
- const errorId = useId();
5324
- const hasError = errorMessage != null;
5325
- useEffect(() => {
5326
- setInnerItems(items);
5327
- }, [items]);
5328
- useEffect(() => {
5329
- if (isMultiselect && Array.isArray(value)) {
5330
- setSelectedItems(value);
5331
- }
5332
- }, [isMultiselect, value]);
5333
- const selectItem = (key) => {
5334
- if (isMultiselect) {
5335
- const next = selectedItems.includes(key) ? selectedItems.filter((it) => it !== key) : [...selectedItems, key];
5336
- setSelectedItems(next);
5337
- onChange?.({ target: { value: next, id: htmlFor, name } });
5338
- } else {
5339
- setSelectedItems([key]);
5340
- onChange?.({ target: { value: key, id: htmlFor, name } });
5341
- setOpen(false);
5342
- }
5343
- };
5344
- const removeSelected = (key) => {
5345
- if (isMultiselect) {
5346
- const next = selectedItems.filter((it) => it !== key);
5347
- setSelectedItems(next);
5348
- onChange?.({ target: { value: next, id: htmlFor, name } });
5349
- } else {
5350
- setSelectedItems([]);
5351
- onChange?.({ target: { value: "", id: htmlFor, name } });
5352
- }
5353
- };
5354
- const labelFor = (key) => innerItems.find((it) => it.key === key)?.label ?? String(key);
5355
- const onSearchChange = (e) => {
5356
- const term = e.target.value;
5357
- setSearchTerm(term);
5358
- setInnerItems(
5359
- term.trim() === "" ? items : items.filter(
5360
- (it) => String(it.label).toLowerCase().includes(term.toLowerCase())
5361
- )
5362
- );
5363
- };
5364
- const isSelected = (key) => Array.isArray(value) ? value.includes(key) : value === key;
5365
- return /* @__PURE__ */ jsxs("div", { className: className || void 0, children: [
5366
- /* @__PURE__ */ jsxs(
5367
- "div",
5368
- {
5369
- className: `flex ${layout === "vertical" ? "flex-col gap-1.5" : "flex-row items-start gap-3"}`,
5370
- children: [
5371
- /* @__PURE__ */ jsx(
5372
- FieldLabel,
5373
- {
5374
- label,
5375
- htmlFor,
5376
- required,
5377
- helperText,
5378
- horizontal: layout === "horizontal",
5379
- style: labelStyle
5380
- }
5381
- ),
5382
- /* @__PURE__ */ jsxs(Popover.Root, { open: open && !disabled, onOpenChange: (o) => !disabled && setOpen(o), children: [
5383
- /* @__PURE__ */ jsx(Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
5384
- "div",
5385
- {
5386
- id: htmlFor,
5387
- role: "combobox",
5388
- "aria-expanded": open,
5389
- "aria-haspopup": "listbox",
5390
- "aria-invalid": hasError || void 0,
5391
- "aria-describedby": hasError ? errorId : void 0,
5392
- style: { width: 240, ...style },
5393
- className: `flex items-center justify-between gap-2 cursor-pointer select-none min-h-[36px] px-3 py-1.5 ${fieldShell({ size, hasError, disabled, sized: false })}`,
5394
- tabIndex: disabled ? -1 : 0,
5395
- onKeyDown: (e) => {
5396
- if (disabled) return;
5397
- if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown" || e.key === "ArrowUp") {
5398
- e.preventDefault();
5399
- setOpen(true);
5400
- }
5401
- },
5402
- children: [
5403
- !value || Array.isArray(value) && value.length === 0 ? /* @__PURE__ */ jsx("span", { className: "flex-1 min-w-0 truncate text-foreground-muted text-sm", children: placeholder }) : Array.isArray(value) ? /* @__PURE__ */ jsx(
5404
- MultiTagRow,
5405
- {
5406
- values: value,
5407
- disabled,
5408
- labelFor,
5409
- onRemove: removeSelected
5410
- }
5411
- ) : /* @__PURE__ */ jsx("div", { className: "flex-1 min-w-0 flex items-center overflow-hidden", children: /* @__PURE__ */ jsx(
5412
- Tag,
5413
- {
5414
- disabled,
5415
- removeLabel: `Remove ${labelFor(value)}`,
5416
- onRemove: () => removeSelected(value),
5417
- children: labelFor(value)
5418
- }
5419
- ) }),
5420
- /* @__PURE__ */ jsx("div", { className: `flex-shrink-0 text-foreground-muted transition-transform duration-200 ${open ? "rotate-180" : "rotate-0"}`, "aria-hidden": "true", children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) }) })
5421
- ]
5422
- }
5423
- ) }),
5424
- /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsxs(
5425
- Popover.Content,
5426
- {
5427
- align: "start",
5428
- sideOffset: 4,
5429
- style: { width: style?.width || 240 },
5430
- className: "bg-surface text-foreground border border-border rounded-lg shadow-md z-50 p-2 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
5431
- onInteractOutside: () => setOpen(false),
5432
- children: [
5433
- hasSearch && /* @__PURE__ */ jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsx(
5434
- SearchInput_default,
5435
- {
5436
- style: { width: "100%" },
5437
- inputStyle: { width: "100%" },
5438
- value: searchTerm,
5439
- onChange: onSearchChange,
5440
- placeholder: "Search..."
5441
- }
5442
- ) }),
5443
- /* @__PURE__ */ jsx("div", { role: "listbox", "aria-multiselectable": isMultiselect, className: "max-h-40 overflow-y-auto", children: innerItems.map((item) => (
5444
- // aria-rowindex was previously set here but
5445
- // it's invalid ARIA on role="option" (it
5446
- // belongs on rows of a grid/treegrid). Dropped.
5447
- // tabIndex={0} + Enter/Space handler makes the
5448
- // option keyboard-activatable; the full
5449
- // combobox roving-tabindex pattern is deferred
5450
- // until the planned Phase-5 rewrite.
5451
- /* @__PURE__ */ jsxs(
5452
- "div",
5453
- {
5454
- role: "option",
5455
- "aria-selected": isSelected(item.key),
5456
- tabIndex: 0,
5457
- className: `flex items-center justify-between p-2 hover:bg-accent hover:text-accent-fg transition-colors duration-150 text-sm rounded-lg cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-accent ${selectedItems.includes(item.key) ? "bg-surface-raised text-foreground" : "text-foreground"}`,
5458
- onClick: () => selectItem(item.key),
5459
- onKeyDown: (e) => {
5460
- if (e.key === "Enter" || e.key === " ") {
5461
- e.preventDefault();
5462
- selectItem(item.key);
5463
- }
5464
- },
5465
- children: [
5466
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs", children: [
5467
- item.icon && /* @__PURE__ */ jsx("div", { children: item.icon }),
5468
- item.label
5469
- ] }),
5470
- isSelected(item.key) && // currentColor — checkmark follows
5471
- // the item's text colour, which
5472
- // flips automatically on hover.
5473
- /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
5474
- "path",
5475
- {
5476
- d: "M4 10l4.5 4.5L16 6",
5477
- stroke: "currentColor",
5478
- strokeWidth: "2",
5479
- strokeLinecap: "round",
5480
- strokeLinejoin: "round"
5481
- }
5482
- ) })
5483
- ]
5484
- },
5485
- item.key
5486
- )
5487
- )) })
5488
- ]
5489
- }
5490
- ) })
5491
- ] })
5492
- ]
5493
- }
5494
- ),
5495
- hasError && /* @__PURE__ */ jsx("div", { id: errorId, className: "text-status-error text-xs mt-1", children: errorMessage })
5496
- ] });
5497
- }
5498
- var SHIMMER = "oxy-skeleton rounded-sm bg-surface-raised";
5499
- function SkeletonBox({ width, height = 16, radius, className = "", style }) {
5500
- return /* @__PURE__ */ jsx(
5501
- "span",
5502
- {
5503
- role: "presentation",
5504
- "aria-hidden": "true",
5505
- className: `block ${SHIMMER} ${className}`,
5506
- style: {
5507
- width: width ?? "100%",
5508
- height,
5509
- borderRadius: radius ?? "var(--radius-md)",
5510
- ...style
5511
- }
5512
- }
5513
- );
5514
- }
5515
- function SkeletonText({
5516
- lines = 3,
5517
- lastLineWidth = 60,
5518
- lineHeight = 14,
5519
- gap = 8,
5520
- className = "",
5521
- style
5522
- }) {
5523
- return /* @__PURE__ */ jsx(
5524
- "div",
5525
- {
5526
- role: "presentation",
5527
- "aria-hidden": "true",
5528
- className: `flex flex-col ${className}`,
5529
- style: { gap, ...style },
5530
- children: Array.from({ length: lines }).map((_, i) => {
5531
- const isLast = i === lines - 1;
5532
- const width = isLast && lines > 1 ? `${lastLineWidth}%` : "100%";
5533
- return /* @__PURE__ */ jsx(
5534
- "span",
5535
- {
5536
- className: `block ${SHIMMER}`,
5537
- style: { height: lineHeight, width, borderRadius: "var(--radius-sm)" }
5538
- },
5539
- i
5540
- );
5541
- })
5542
- }
5543
- );
5544
- }
5545
- function SkeletonCircle({ size = 40, className = "", style }) {
5546
- return /* @__PURE__ */ jsx(
5547
- "span",
5548
- {
5549
- role: "presentation",
5550
- "aria-hidden": "true",
5551
- className: `block flex-shrink-0 ${SHIMMER} ${className}`,
5552
- style: {
5553
- width: size,
5554
- height: size,
5555
- borderRadius: "50%",
5556
- ...style
5557
- }
5558
- }
5559
- );
5560
- }
5561
- function SkeletonCard({ hasAvatar = true, lines = 3, className = "", style }) {
5562
- return /* @__PURE__ */ jsxs(
5563
- "div",
5564
- {
5565
- role: "presentation",
5566
- "aria-hidden": "true",
5567
- className: `rounded-lg border border-border bg-surface p-4 ${className}`,
5568
- style,
5569
- children: [
5570
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-4", children: [
5571
- hasAvatar && /* @__PURE__ */ jsx(SkeletonCircle, { size: 36 }),
5572
- /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col gap-2", children: [
5573
- /* @__PURE__ */ jsx(SkeletonBox, { height: 12, width: "55%" }),
5574
- /* @__PURE__ */ jsx(SkeletonBox, { height: 10, width: "35%" })
5575
- ] })
5576
- ] }),
5577
- /* @__PURE__ */ jsx(SkeletonText, { lines, lastLineWidth: 55 }),
5578
- /* @__PURE__ */ jsxs("div", { className: "mt-4 flex gap-2", children: [
5579
- /* @__PURE__ */ jsx(SkeletonBox, { height: 28, width: 72 }),
5580
- /* @__PURE__ */ jsx(SkeletonBox, { height: 28, width: 56 })
5581
- ] })
5582
- ]
5583
- }
5584
- );
5585
- }
5586
- var DEFAULT_PICKER = [
5587
- { key: 1, value: 5, label: 5 },
5588
- { key: 2, value: 10, label: 10 },
5589
- { key: 3, value: 15, label: 15 },
5590
- { key: 4, value: 20, label: 20 }
5591
- ];
5592
- var DEFAULT_PAGINATION = {
5593
- enabled: true,
5594
- perPage: 15,
5595
- withPicker: true,
5596
- pickerOptions: DEFAULT_PICKER
5597
- };
5598
- var DEFAULT_EXPAND = {
5599
- enabled: false
5600
- };
5601
- function createDatasets(rows, perPage) {
5602
- if (!perPage) return [rows.slice()];
5603
- const all = [];
5604
- for (let i = 0; i < rows.length; i += perPage) {
5605
- all.push(rows.slice(i, i + perPage));
5606
- }
5607
- return all;
5608
- }
5609
- var defaultGetRowKey = (_row, index) => index;
5610
- var cellAlign = (align) => align === "left" ? "text-left" : align === "right" ? "text-right" : "text-center";
5611
- function TableHeader({
5612
- columns,
5613
- hasExpand
5614
- }) {
5615
- return /* @__PURE__ */ jsx("thead", { className: "bg-surface-raised border-b border-border", children: /* @__PURE__ */ jsxs("tr", { children: [
5616
- hasExpand && /* @__PURE__ */ jsx("th", { "aria-hidden": "true", className: "w-9" }),
5617
- columns.map((col) => /* @__PURE__ */ jsx(
5618
- "th",
5619
- {
5620
- scope: "col",
5621
- className: `${cellAlign(col.align)} text-sm font-semibold text-foreground py-3 px-3`,
5622
- style: col.width != null ? { width: col.width } : void 0,
5623
- children: col.label
5624
- },
5625
- col.key
5626
- ))
5627
- ] }) });
5361
+ col.key
5362
+ ))
5363
+ ] }) });
5628
5364
  }
5629
5365
  var DefaultExpandIcon = /* @__PURE__ */ jsx(
5630
5366
  "svg",
@@ -5665,7 +5401,7 @@ function TableBody({
5665
5401
  return /* @__PURE__ */ jsx("tbody", { children: rows.map((row, i) => {
5666
5402
  const rowKey = getRowKey(row, i);
5667
5403
  const isExpanded = expanded.has(rowKey);
5668
- return /* @__PURE__ */ jsxs(React29.Fragment, { children: [
5404
+ return /* @__PURE__ */ jsxs(React28.Fragment, { children: [
5669
5405
  /* @__PURE__ */ jsxs(
5670
5406
  "tr",
5671
5407
  {
@@ -5729,6 +5465,8 @@ function Pagination({
5729
5465
  if (next) setPerPageKey(next.key);
5730
5466
  }
5731
5467
  }, [serverSide, options.perPage, picker]);
5468
+ const currentOpt = picker.find((o) => o.key === displayPerPageKey);
5469
+ const currentPerPageLabel = currentOpt?.label ?? currentOpt?.value ?? options.perPage ?? "";
5732
5470
  const navBtn = (icon, disabled, onClick, title) => /* @__PURE__ */ jsx(IconButton, { type: "bordered", size: "sm", disabled, onClick, icon, title });
5733
5471
  const chevronRight = /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9 5l7 7-7 7" }) });
5734
5472
  const doubleChevronRight = /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M13 5l7 7-7 7M5 5l7 7-7 7" }) });
@@ -5736,21 +5474,20 @@ function Pagination({
5736
5474
  options.withPicker && /* @__PURE__ */ jsxs("div", { className: "mr-auto flex items-center gap-2", children: [
5737
5475
  /* @__PURE__ */ jsx("span", { className: "whitespace-nowrap text-xs text-foreground-muted", children: "Rows per page" }),
5738
5476
  /* @__PURE__ */ jsx(
5739
- Dropdown,
5477
+ MenuButton,
5740
5478
  {
5479
+ variant: "secondary",
5741
5480
  size: "sm",
5742
- style: { width: 76 },
5743
- hasSearch: false,
5744
- items: picker,
5745
- isMultiselect: false,
5746
- value: displayPerPageKey,
5747
- onChange: ({ target: { value } }) => {
5748
- if (Array.isArray(value)) return;
5749
- const numKey = typeof value === "number" ? value : Number(value);
5750
- if (!serverSide) setPerPageKey(numKey);
5751
- const opt = picker.find((o) => o.key === numKey);
5752
- onPerPageChange(opt?.label ?? opt?.value ?? numKey);
5753
- }
5481
+ side: "top",
5482
+ label: String(currentPerPageLabel),
5483
+ items: picker.map((o) => ({
5484
+ key: o.key,
5485
+ label: String(o.label ?? o.value ?? o.key),
5486
+ onSelect: () => {
5487
+ if (!serverSide) setPerPageKey(o.key);
5488
+ onPerPageChange(o.label ?? o.value ?? o.key);
5489
+ }
5490
+ }))
5754
5491
  }
5755
5492
  )
5756
5493
  ] }),
@@ -6206,8 +5943,8 @@ function MegaMenuLink({ href, icon, description, active, onClick, children, clas
6206
5943
  function MegaMenuFeatured({ children, className = "" }) {
6207
5944
  return /* @__PURE__ */ jsx("div", { className: ["min-w-0 rounded-lg bg-surface-raised border border-border p-4 flex flex-col", className].filter(Boolean).join(" "), children });
6208
5945
  }
6209
- var elementsOfType = (children, type) => React29.Children.toArray(children).filter(
6210
- (c) => React29.isValidElement(c) && c.type === type
5946
+ var elementsOfType = (children, type) => React28.Children.toArray(children).filter(
5947
+ (c) => React28.isValidElement(c) && c.type === type
6211
5948
  );
6212
5949
  var MOBILE_CHEVRON = /* @__PURE__ */ jsx(
6213
5950
  "svg",
@@ -6244,9 +5981,9 @@ function MobileLinkRow({ link, onNavigate }) {
6244
5981
  );
6245
5982
  }
6246
5983
  function MobilePanel({ panel, onNavigate }) {
6247
- const nodes = React29.Children.toArray(panel.props.children);
5984
+ const nodes = React28.Children.toArray(panel.props.children);
6248
5985
  return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-4 px-2 pb-3 pt-1", children: nodes.map((node, i) => {
6249
- if (!React29.isValidElement(node)) return null;
5986
+ if (!React28.isValidElement(node)) return null;
6250
5987
  const el = node;
6251
5988
  if (el.type === MegaMenuSection) {
6252
5989
  const { title, children } = el.props;
@@ -6655,7 +6392,7 @@ function ThemeProvider({
6655
6392
  className = "",
6656
6393
  style
6657
6394
  }) {
6658
- const id = React29.useId().replace(/:/g, "");
6395
+ const id = React28.useId().replace(/:/g, "");
6659
6396
  const scopeClass = `geo-th-${id}`;
6660
6397
  const divRef = useRef(null);
6661
6398
  useEffect(() => {
@@ -7113,77 +6850,379 @@ function RadioGroup({
7113
6850
  }
7114
6851
  );
7115
6852
  }
7116
- function Switch({
7117
- checked,
7118
- defaultChecked = false,
7119
- onChange,
7120
- checkedIcon,
7121
- uncheckedIcon,
6853
+ function Switch({
6854
+ checked,
6855
+ defaultChecked = false,
6856
+ onChange,
6857
+ checkedIcon,
6858
+ uncheckedIcon,
6859
+ label,
6860
+ layout = "horizontal",
6861
+ helperText,
6862
+ className,
6863
+ offLabel,
6864
+ onLabel,
6865
+ name,
6866
+ required,
6867
+ disabled,
6868
+ errorMessage
6869
+ }) {
6870
+ const id = useId();
6871
+ const errorId = useId();
6872
+ const hasError = errorMessage != null;
6873
+ const isControlled = checked !== void 0;
6874
+ const [internal, setInternal] = useState(defaultChecked);
6875
+ const isOn = isControlled ? checked : internal;
6876
+ const handle = (c) => {
6877
+ if (!isControlled) setInternal(c);
6878
+ onChange?.({ target: { checked: c, name } });
6879
+ };
6880
+ const stateLabel = (active) => [
6881
+ "text-sm select-none transition-colors",
6882
+ active ? "text-foreground font-medium" : "text-foreground-muted",
6883
+ disabled ? "opacity-50" : "cursor-pointer"
6884
+ ].filter(Boolean).join(" ");
6885
+ return /* @__PURE__ */ jsx(
6886
+ Field,
6887
+ {
6888
+ className,
6889
+ label,
6890
+ htmlFor: id,
6891
+ errorId,
6892
+ errorMessage,
6893
+ layout,
6894
+ required,
6895
+ helperText,
6896
+ labelAlign: "center",
6897
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5", children: [
6898
+ offLabel != null && /* @__PURE__ */ jsx("label", { htmlFor: id, className: stateLabel(!isOn), children: offLabel }),
6899
+ /* @__PURE__ */ jsx(
6900
+ SwitchPrimitive.Root,
6901
+ {
6902
+ id,
6903
+ name,
6904
+ checked: isOn,
6905
+ onCheckedChange: handle,
6906
+ disabled,
6907
+ required,
6908
+ "aria-invalid": hasError || void 0,
6909
+ "aria-describedby": hasError ? errorId : void 0,
6910
+ className: "relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full bg-foreground-secondary data-[state=checked]:bg-accent transition-colors focus:outline-none focus-visible:ring-[3px] focus-visible:ring-focus-ring disabled:opacity-50 disabled:cursor-not-allowed",
6911
+ children: /* @__PURE__ */ jsx(
6912
+ SwitchPrimitive.Thumb,
6913
+ {
6914
+ className: "pointer-events-none flex h-5 w-5 items-center justify-center rounded-full bg-background text-foreground shadow transition-transform duration-200 data-[state=checked]:translate-x-[22px] data-[state=unchecked]:translate-x-[2px]",
6915
+ children: checkedIcon && uncheckedIcon ? /* @__PURE__ */ jsx("span", { className: "flex items-center justify-center w-3 h-3", children: isOn ? checkedIcon : uncheckedIcon }) : null
6916
+ }
6917
+ )
6918
+ }
6919
+ ),
6920
+ onLabel != null && /* @__PURE__ */ jsx("label", { htmlFor: id, className: stateLabel(isOn), children: onLabel })
6921
+ ] })
6922
+ }
6923
+ );
6924
+ }
6925
+ function Tag({ children, onRemove, removeLabel, disabled }) {
6926
+ return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 rounded-md border border-border bg-surface-raised text-foreground text-xs pl-2 pr-1 py-0.5 max-w-full", children: [
6927
+ /* @__PURE__ */ jsx("span", { className: "truncate", children }),
6928
+ onRemove && /* @__PURE__ */ jsx(
6929
+ "button",
6930
+ {
6931
+ type: "button",
6932
+ disabled,
6933
+ onClick: (e) => {
6934
+ e.stopPropagation();
6935
+ onRemove();
6936
+ },
6937
+ "aria-label": removeLabel ?? "Remove",
6938
+ className: "inline-flex items-center justify-center w-4 h-4 flex-shrink-0 rounded text-foreground-muted hover:text-status-error hover:bg-surface transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-accent disabled:cursor-not-allowed",
6939
+ children: /* @__PURE__ */ jsx("svg", { width: "10", height: "10", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M15 5L5 15M5 5l10 10", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) })
6940
+ }
6941
+ )
6942
+ ] });
6943
+ }
6944
+ function MultiTagRow({
6945
+ values,
6946
+ disabled,
6947
+ labelFor,
6948
+ onRemove
6949
+ }) {
6950
+ const wrapRef = useRef(null);
6951
+ const measureRef = useRef(null);
6952
+ const [visibleCount, setVisibleCount] = useState(values.length);
6953
+ const key = values.map(String).join("|");
6954
+ useLayoutEffect(() => {
6955
+ const wrap = wrapRef.current;
6956
+ const measure = measureRef.current;
6957
+ if (!wrap || !measure) return;
6958
+ const GAP = 6;
6959
+ const recompute = () => {
6960
+ const avail = wrap.clientWidth;
6961
+ const tagEls = Array.from(measure.querySelectorAll("[data-mt]"));
6962
+ const moreEl = measure.querySelector("[data-mm]");
6963
+ const widths = tagEls.map((e) => e.offsetWidth);
6964
+ const moreW = moreEl ? moreEl.offsetWidth : 0;
6965
+ if (widths.length === 0) {
6966
+ setVisibleCount(0);
6967
+ return;
6968
+ }
6969
+ let used = 0;
6970
+ let count = 0;
6971
+ for (let i = 0; i < widths.length; i++) {
6972
+ const w = widths[i] + (i > 0 ? GAP : 0);
6973
+ if (used + w <= avail) {
6974
+ used += w;
6975
+ count++;
6976
+ } else break;
6977
+ }
6978
+ if (count < widths.length) {
6979
+ while (count > 0) {
6980
+ let t = 0;
6981
+ for (let i = 0; i < count; i++) t += widths[i] + (i > 0 ? GAP : 0);
6982
+ t += GAP + moreW;
6983
+ if (t <= avail) break;
6984
+ count--;
6985
+ }
6986
+ }
6987
+ setVisibleCount(count);
6988
+ };
6989
+ recompute();
6990
+ const ro = new ResizeObserver(recompute);
6991
+ ro.observe(wrap);
6992
+ return () => ro.disconnect();
6993
+ }, [key]);
6994
+ const hidden = values.length - visibleCount;
6995
+ const moreChip = (n) => /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center flex-shrink-0 rounded-md border border-border bg-surface-raised text-foreground-secondary text-xs px-2 py-0.5", children: [
6996
+ "+",
6997
+ n,
6998
+ " more"
6999
+ ] });
7000
+ return /* @__PURE__ */ jsxs("div", { ref: wrapRef, className: "relative flex-1 min-w-0 flex flex-nowrap items-center gap-1.5 overflow-hidden", children: [
7001
+ /* @__PURE__ */ jsxs(
7002
+ "div",
7003
+ {
7004
+ ref: measureRef,
7005
+ "aria-hidden": "true",
7006
+ className: "absolute invisible pointer-events-none flex flex-nowrap items-center gap-1.5",
7007
+ style: { left: -9999, top: -9999 },
7008
+ children: [
7009
+ values.map((val) => /* @__PURE__ */ jsx("span", { "data-mt": true, children: /* @__PURE__ */ jsx(Tag, { removeLabel: "x", onRemove: () => {
7010
+ }, children: labelFor(val) }) }, `m-${val}`)),
7011
+ /* @__PURE__ */ jsx("span", { "data-mm": true, children: moreChip(values.length) })
7012
+ ]
7013
+ }
7014
+ ),
7015
+ values.slice(0, visibleCount).map((val) => /* @__PURE__ */ jsx(
7016
+ Tag,
7017
+ {
7018
+ disabled,
7019
+ removeLabel: `Remove ${labelFor(val)}`,
7020
+ onRemove: () => onRemove(val),
7021
+ children: labelFor(val)
7022
+ },
7023
+ String(val)
7024
+ )),
7025
+ hidden > 0 && moreChip(hidden)
7026
+ ] });
7027
+ }
7028
+ function Dropdown({
7029
+ isMultiselect = false,
7030
+ hasSearch = true,
7122
7031
  label,
7032
+ name,
7033
+ value,
7034
+ onChange,
7035
+ disabled,
7123
7036
  layout = "horizontal",
7124
7037
  helperText,
7125
- className,
7126
- offLabel,
7127
- onLabel,
7128
- name,
7129
7038
  required,
7130
- disabled,
7131
- errorMessage
7039
+ errorMessage,
7040
+ style = {},
7041
+ htmlFor,
7042
+ items = [],
7043
+ labelStyle = {},
7044
+ placeholder,
7045
+ size = "md",
7046
+ className = ""
7132
7047
  }) {
7133
- const id = useId();
7048
+ const [open, setOpen] = useState(false);
7049
+ const [selectedItems, setSelectedItems] = useState([]);
7050
+ const [searchTerm, setSearchTerm] = useState("");
7051
+ const [innerItems, setInnerItems] = useState([]);
7134
7052
  const errorId = useId();
7135
7053
  const hasError = errorMessage != null;
7136
- const isControlled = checked !== void 0;
7137
- const [internal, setInternal] = useState(defaultChecked);
7138
- const isOn = isControlled ? checked : internal;
7139
- const handle = (c) => {
7140
- if (!isControlled) setInternal(c);
7141
- onChange?.({ target: { checked: c, name } });
7054
+ useEffect(() => {
7055
+ setInnerItems(items);
7056
+ }, [items]);
7057
+ useEffect(() => {
7058
+ if (isMultiselect && Array.isArray(value)) {
7059
+ setSelectedItems(value);
7060
+ }
7061
+ }, [isMultiselect, value]);
7062
+ const selectItem = (key) => {
7063
+ if (isMultiselect) {
7064
+ const next = selectedItems.includes(key) ? selectedItems.filter((it) => it !== key) : [...selectedItems, key];
7065
+ setSelectedItems(next);
7066
+ onChange?.({ target: { value: next, id: htmlFor, name } });
7067
+ } else {
7068
+ setSelectedItems([key]);
7069
+ onChange?.({ target: { value: key, id: htmlFor, name } });
7070
+ setOpen(false);
7071
+ }
7142
7072
  };
7143
- const stateLabel = (active) => [
7144
- "text-sm select-none transition-colors",
7145
- active ? "text-foreground font-medium" : "text-foreground-muted",
7146
- disabled ? "opacity-50" : "cursor-pointer"
7147
- ].filter(Boolean).join(" ");
7148
- return /* @__PURE__ */ jsx(
7149
- Field,
7150
- {
7151
- className,
7152
- label,
7153
- htmlFor: id,
7154
- errorId,
7155
- errorMessage,
7156
- layout,
7157
- required,
7158
- helperText,
7159
- labelAlign: "center",
7160
- children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5", children: [
7161
- offLabel != null && /* @__PURE__ */ jsx("label", { htmlFor: id, className: stateLabel(!isOn), children: offLabel }),
7162
- /* @__PURE__ */ jsx(
7163
- SwitchPrimitive.Root,
7164
- {
7165
- id,
7166
- name,
7167
- checked: isOn,
7168
- onCheckedChange: handle,
7169
- disabled,
7170
- required,
7171
- "aria-invalid": hasError || void 0,
7172
- "aria-describedby": hasError ? errorId : void 0,
7173
- className: "relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full bg-foreground-secondary data-[state=checked]:bg-accent transition-colors focus:outline-none focus-visible:ring-[3px] focus-visible:ring-focus-ring disabled:opacity-50 disabled:cursor-not-allowed",
7174
- children: /* @__PURE__ */ jsx(
7175
- SwitchPrimitive.Thumb,
7073
+ const removeSelected = (key) => {
7074
+ if (isMultiselect) {
7075
+ const next = selectedItems.filter((it) => it !== key);
7076
+ setSelectedItems(next);
7077
+ onChange?.({ target: { value: next, id: htmlFor, name } });
7078
+ } else {
7079
+ setSelectedItems([]);
7080
+ onChange?.({ target: { value: "", id: htmlFor, name } });
7081
+ }
7082
+ };
7083
+ const labelFor = (key) => innerItems.find((it) => it.key === key)?.label ?? String(key);
7084
+ const onSearchChange = (e) => {
7085
+ const term = e.target.value;
7086
+ setSearchTerm(term);
7087
+ setInnerItems(
7088
+ term.trim() === "" ? items : items.filter(
7089
+ (it) => String(it.label).toLowerCase().includes(term.toLowerCase())
7090
+ )
7091
+ );
7092
+ };
7093
+ const isSelected = (key) => Array.isArray(value) ? value.includes(key) : value === key;
7094
+ return /* @__PURE__ */ jsxs("div", { className: className || void 0, children: [
7095
+ /* @__PURE__ */ jsxs(
7096
+ "div",
7097
+ {
7098
+ className: `flex ${layout === "vertical" ? "flex-col gap-1.5" : "flex-row items-start gap-3"}`,
7099
+ children: [
7100
+ /* @__PURE__ */ jsx(
7101
+ FieldLabel,
7102
+ {
7103
+ label,
7104
+ htmlFor,
7105
+ required,
7106
+ helperText,
7107
+ horizontal: layout === "horizontal",
7108
+ style: labelStyle
7109
+ }
7110
+ ),
7111
+ /* @__PURE__ */ jsxs(Popover.Root, { open: open && !disabled, onOpenChange: (o) => !disabled && setOpen(o), children: [
7112
+ /* @__PURE__ */ jsx(Popover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
7113
+ "div",
7176
7114
  {
7177
- className: "pointer-events-none flex h-5 w-5 items-center justify-center rounded-full bg-background text-foreground shadow transition-transform duration-200 data-[state=checked]:translate-x-[22px] data-[state=unchecked]:translate-x-[2px]",
7178
- children: checkedIcon && uncheckedIcon ? /* @__PURE__ */ jsx("span", { className: "flex items-center justify-center w-3 h-3", children: isOn ? checkedIcon : uncheckedIcon }) : null
7115
+ id: htmlFor,
7116
+ role: "combobox",
7117
+ "aria-expanded": open,
7118
+ "aria-haspopup": "listbox",
7119
+ "aria-invalid": hasError || void 0,
7120
+ "aria-describedby": hasError ? errorId : void 0,
7121
+ style: { width: 240, ...style },
7122
+ className: `flex items-center justify-between gap-2 cursor-pointer select-none min-h-[36px] px-3 py-1.5 ${fieldShell({ size, hasError, disabled, sized: false })}`,
7123
+ tabIndex: disabled ? -1 : 0,
7124
+ onKeyDown: (e) => {
7125
+ if (disabled) return;
7126
+ if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown" || e.key === "ArrowUp") {
7127
+ e.preventDefault();
7128
+ setOpen(true);
7129
+ }
7130
+ },
7131
+ children: [
7132
+ !value || Array.isArray(value) && value.length === 0 ? /* @__PURE__ */ jsx("span", { className: "flex-1 min-w-0 truncate text-foreground-muted text-sm", children: placeholder }) : Array.isArray(value) ? /* @__PURE__ */ jsx(
7133
+ MultiTagRow,
7134
+ {
7135
+ values: value,
7136
+ disabled,
7137
+ labelFor,
7138
+ onRemove: removeSelected
7139
+ }
7140
+ ) : /* @__PURE__ */ jsx("div", { className: "flex-1 min-w-0 flex items-center overflow-hidden", children: /* @__PURE__ */ jsx(
7141
+ Tag,
7142
+ {
7143
+ disabled,
7144
+ removeLabel: `Remove ${labelFor(value)}`,
7145
+ onRemove: () => removeSelected(value),
7146
+ children: labelFor(value)
7147
+ }
7148
+ ) }),
7149
+ /* @__PURE__ */ jsx("div", { className: `flex-shrink-0 text-foreground-muted transition-transform duration-200 ${open ? "rotate-180" : "rotate-0"}`, "aria-hidden": "true", children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: "h-4 w-4", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) }) })
7150
+ ]
7179
7151
  }
7180
- )
7181
- }
7182
- ),
7183
- onLabel != null && /* @__PURE__ */ jsx("label", { htmlFor: id, className: stateLabel(isOn), children: onLabel })
7184
- ] })
7185
- }
7186
- );
7152
+ ) }),
7153
+ /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsxs(
7154
+ Popover.Content,
7155
+ {
7156
+ align: "start",
7157
+ sideOffset: 4,
7158
+ style: { width: style?.width || 240 },
7159
+ className: "bg-surface text-foreground border border-border rounded-lg shadow-md z-50 p-2 animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
7160
+ onInteractOutside: () => setOpen(false),
7161
+ children: [
7162
+ hasSearch && /* @__PURE__ */ jsx("div", { className: "mb-2", children: /* @__PURE__ */ jsx(
7163
+ SearchInput_default,
7164
+ {
7165
+ style: { width: "100%" },
7166
+ inputStyle: { width: "100%" },
7167
+ value: searchTerm,
7168
+ onChange: onSearchChange,
7169
+ placeholder: "Search..."
7170
+ }
7171
+ ) }),
7172
+ /* @__PURE__ */ jsx("div", { role: "listbox", "aria-multiselectable": isMultiselect, className: "max-h-40 overflow-y-auto", children: innerItems.map((item) => (
7173
+ // aria-rowindex was previously set here but
7174
+ // it's invalid ARIA on role="option" (it
7175
+ // belongs on rows of a grid/treegrid). Dropped.
7176
+ // tabIndex={0} + Enter/Space handler makes the
7177
+ // option keyboard-activatable; the full
7178
+ // combobox roving-tabindex pattern is deferred
7179
+ // until the planned Phase-5 rewrite.
7180
+ /* @__PURE__ */ jsxs(
7181
+ "div",
7182
+ {
7183
+ role: "option",
7184
+ "aria-selected": isSelected(item.key),
7185
+ tabIndex: 0,
7186
+ className: `flex items-center justify-between p-2 hover:bg-accent hover:text-accent-fg transition-colors duration-150 text-sm rounded-lg cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-accent ${selectedItems.includes(item.key) ? "bg-surface-raised text-foreground" : "text-foreground"}`,
7187
+ onClick: () => selectItem(item.key),
7188
+ onKeyDown: (e) => {
7189
+ if (e.key === "Enter" || e.key === " ") {
7190
+ e.preventDefault();
7191
+ selectItem(item.key);
7192
+ }
7193
+ },
7194
+ children: [
7195
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs", children: [
7196
+ item.icon && /* @__PURE__ */ jsx("div", { children: item.icon }),
7197
+ item.label
7198
+ ] }),
7199
+ isSelected(item.key) && // currentColor — checkmark follows
7200
+ // the item's text colour, which
7201
+ // flips automatically on hover.
7202
+ /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
7203
+ "path",
7204
+ {
7205
+ d: "M4 10l4.5 4.5L16 6",
7206
+ stroke: "currentColor",
7207
+ strokeWidth: "2",
7208
+ strokeLinecap: "round",
7209
+ strokeLinejoin: "round"
7210
+ }
7211
+ ) })
7212
+ ]
7213
+ },
7214
+ item.key
7215
+ )
7216
+ )) })
7217
+ ]
7218
+ }
7219
+ ) })
7220
+ ] })
7221
+ ]
7222
+ }
7223
+ ),
7224
+ hasError && /* @__PURE__ */ jsx("div", { id: errorId, className: "text-status-error text-xs mt-1", children: errorMessage })
7225
+ ] });
7187
7226
  }
7188
7227
  function AutoComplete({
7189
7228
  disabled,
@@ -8495,7 +8534,7 @@ function OtpInput({
8495
8534
  emit(valid.join(""));
8496
8535
  focusBox(valid.length);
8497
8536
  };
8498
- return /* @__PURE__ */ jsx(Field, { className, label, htmlFor, errorId, errorMessage, required, layout, helperText, children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2", role: "group", "aria-label": typeof label === "string" ? label : "One-time code", children: chars.map((char, idx) => /* @__PURE__ */ jsxs(React29.Fragment, { children: [
8537
+ return /* @__PURE__ */ jsx(Field, { className, label, htmlFor, errorId, errorMessage, required, layout, helperText, children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2", role: "group", "aria-label": typeof label === "string" ? label : "One-time code", children: chars.map((char, idx) => /* @__PURE__ */ jsxs(React28.Fragment, { children: [
8499
8538
  /* @__PURE__ */ jsx(
8500
8539
  "input",
8501
8540
  {