@geomak/ui 6.0.1 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -986,15 +986,27 @@ function Tabs({
986
986
  const current = isControlled ? value : internal;
987
987
  const reduced = !!framerMotion.useReducedMotion();
988
988
  const indicatorId = React8.useId();
989
- const handle = (next) => {
989
+ const select = React8.useCallback((next) => {
990
990
  if (!isControlled) setInternal(next);
991
991
  onValueChange?.(next);
992
- };
993
- return /* @__PURE__ */ jsxRuntime.jsx(TabsContext.Provider, { value: { value: current, variant, size, orientation, indicatorId, reduced }, children: /* @__PURE__ */ jsxRuntime.jsx(
992
+ }, [isControlled, onValueChange]);
993
+ const registry = React8.useRef(/* @__PURE__ */ new Map());
994
+ const orderRef = React8.useRef(0);
995
+ const [, bump] = React8.useState(0);
996
+ const registerTab = React8.useCallback((val, meta) => {
997
+ const existing = registry.current.get(val);
998
+ registry.current.set(val, { ...meta, order: existing?.order ?? orderRef.current++ });
999
+ if (!existing) bump((v) => v + 1);
1000
+ }, []);
1001
+ const unregisterTab = React8.useCallback((val) => {
1002
+ if (registry.current.delete(val)) bump((v) => v + 1);
1003
+ }, []);
1004
+ const getTabs = React8.useCallback(() => [...registry.current.entries()].sort((a, b) => a[1].order - b[1].order).map(([val, m]) => ({ value: val, label: m.label, icon: m.icon, disabled: m.disabled })), []);
1005
+ return /* @__PURE__ */ jsxRuntime.jsx(TabsContext.Provider, { value: { value: current, variant, size, orientation, indicatorId, reduced, select, registerTab, unregisterTab, getTabs }, children: /* @__PURE__ */ jsxRuntime.jsx(
994
1006
  TabsPrimitive__namespace.Root,
995
1007
  {
996
1008
  value: current,
997
- onValueChange: handle,
1009
+ onValueChange: select,
998
1010
  orientation,
999
1011
  className: [
1000
1012
  "flex min-w-0",
@@ -1007,7 +1019,7 @@ function Tabs({
1007
1019
  ) });
1008
1020
  }
1009
1021
  function TabsList({ children, "aria-label": ariaLabel, className = "" }) {
1010
- const { variant, orientation, reduced } = useTabsContext();
1022
+ const { variant, orientation, reduced, value } = useTabsContext();
1011
1023
  const horizontal = orientation === "horizontal";
1012
1024
  const scrollRef = React8.useRef(null);
1013
1025
  const [edges, setEdges] = React8.useState({ start: false, end: false });
@@ -1043,6 +1055,14 @@ function TabsList({ children, "aria-label": ariaLabel, className = "" }) {
1043
1055
  const amount = (horizontal ? el.clientWidth : el.clientHeight) * 0.7 * dir;
1044
1056
  el.scrollBy({ [horizontal ? "left" : "top"]: amount, behavior: reduced ? "auto" : "smooth" });
1045
1057
  }, [horizontal, reduced]);
1058
+ React8.useLayoutEffect(() => {
1059
+ const el = scrollRef.current;
1060
+ if (!el || !scrollable) return;
1061
+ const active = el.querySelector("[role=tab][data-state=active]");
1062
+ if (active && typeof active.scrollIntoView === "function") {
1063
+ active.scrollIntoView({ block: "nearest", inline: "nearest", behavior: reduced ? "auto" : "smooth" });
1064
+ }
1065
+ }, [value, scrollable, reduced]);
1046
1066
  const maskStyle = scrollable && (edges.start || edges.end) ? (() => {
1047
1067
  const dir = horizontal ? "to right" : "to bottom";
1048
1068
  const a = edges.start ? "transparent, black 36px" : "black";
@@ -1059,7 +1079,8 @@ function TabsList({ children, "aria-label": ariaLabel, className = "" }) {
1059
1079
  return `flex ${horizontal ? "flex-row" : "flex-col"} ${align} gap-1 ${hairline}`;
1060
1080
  })();
1061
1081
  const scrollClass = scrollable ? horizontal ? "overflow-x-auto overflow-y-hidden hidden-scrollbar" : "overflow-y-auto overflow-x-hidden hidden-scrollbar" : "";
1062
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["relative flex min-w-0", horizontal ? "flex-row items-stretch" : "flex-col items-stretch", className].filter(Boolean).join(" "), children: [
1082
+ const overflowing = scrollable && (edges.start || edges.end);
1083
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: ["relative flex min-w-0 gap-1", horizontal ? "flex-row items-stretch" : "flex-col items-stretch", className].filter(Boolean).join(" "), children: [
1063
1084
  scrollable && edges.start && /* @__PURE__ */ jsxRuntime.jsx(Chevron, { side: "start", orientation, onClick: () => nudge(-1) }),
1064
1085
  /* @__PURE__ */ jsxRuntime.jsx(
1065
1086
  TabsPrimitive__namespace.List,
@@ -1071,37 +1092,131 @@ function TabsList({ children, "aria-label": ariaLabel, className = "" }) {
1071
1092
  children
1072
1093
  }
1073
1094
  ),
1074
- scrollable && edges.end && /* @__PURE__ */ jsxRuntime.jsx(Chevron, { side: "end", orientation, onClick: () => nudge(1) })
1095
+ scrollable && edges.end && /* @__PURE__ */ jsxRuntime.jsx(Chevron, { side: "end", orientation, onClick: () => nudge(1) }),
1096
+ overflowing && /* @__PURE__ */ jsxRuntime.jsx(OverflowMenu, {})
1075
1097
  ] });
1076
1098
  }
1077
1099
  function Chevron({ side, orientation, onClick }) {
1078
1100
  const horizontal = orientation === "horizontal";
1079
1101
  const rotate = horizontal ? side === "start" ? "rotate-180" : "" : side === "start" ? "-rotate-90" : "rotate-90";
1080
- const pos = horizontal ? side === "start" ? "left-0 top-1/2 -translate-y-1/2" : "right-0 top-1/2 -translate-y-1/2" : side === "start" ? "top-0 left-1/2 -translate-x-1/2" : "bottom-0 left-1/2 -translate-x-1/2";
1081
1102
  return /* @__PURE__ */ jsxRuntime.jsx(
1082
1103
  "button",
1083
1104
  {
1084
1105
  type: "button",
1085
1106
  "aria-label": side === "start" ? "Scroll tabs backward" : "Scroll tabs forward",
1086
1107
  onClick,
1087
- className: `absolute z-20 ${pos} flex h-7 w-7 items-center justify-center rounded-full border border-border bg-surface text-foreground-secondary shadow-sm hover:text-foreground hover:bg-surface-raised transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent`,
1108
+ className: "flex-shrink-0 self-center flex h-7 w-7 items-center justify-center rounded-full border border-border bg-surface text-foreground-secondary shadow-sm hover:text-foreground hover:bg-surface-raised transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent",
1088
1109
  children: /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: `h-4 w-4 ${rotate}`, "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9 5l7 7-7 7" }) })
1089
1110
  }
1090
1111
  );
1091
1112
  }
1113
+ function OverflowMenu() {
1114
+ const { getTabs, value, select, orientation } = useTabsContext();
1115
+ const horizontal = orientation === "horizontal";
1116
+ const [open, setOpen] = React8.useState(false);
1117
+ const wrapRef = React8.useRef(null);
1118
+ const timer = React8.useRef(null);
1119
+ const openNow = () => {
1120
+ if (timer.current) clearTimeout(timer.current);
1121
+ setOpen(true);
1122
+ };
1123
+ const closeSoon = () => {
1124
+ timer.current = setTimeout(() => setOpen(false), 160);
1125
+ };
1126
+ React8.useLayoutEffect(() => {
1127
+ if (!open) return;
1128
+ const onDoc = (e) => {
1129
+ if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
1130
+ };
1131
+ const onKey = (e) => {
1132
+ if (e.key === "Escape") setOpen(false);
1133
+ };
1134
+ document.addEventListener("mousedown", onDoc);
1135
+ document.addEventListener("keydown", onKey);
1136
+ return () => {
1137
+ document.removeEventListener("mousedown", onDoc);
1138
+ document.removeEventListener("keydown", onKey);
1139
+ };
1140
+ }, [open]);
1141
+ const tabs = getTabs();
1142
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1143
+ "div",
1144
+ {
1145
+ ref: wrapRef,
1146
+ className: "relative flex-shrink-0 self-center",
1147
+ onMouseEnter: openNow,
1148
+ onMouseLeave: closeSoon,
1149
+ children: [
1150
+ /* @__PURE__ */ jsxRuntime.jsx(
1151
+ "button",
1152
+ {
1153
+ type: "button",
1154
+ "aria-haspopup": "menu",
1155
+ "aria-expanded": open,
1156
+ "aria-label": "Show all tabs",
1157
+ onClick: () => setOpen((o) => !o),
1158
+ className: "flex h-7 w-7 items-center justify-center rounded-full border border-border bg-surface text-foreground-secondary shadow-sm hover:text-foreground hover:bg-surface-raised data-[expanded=true]:text-foreground transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-accent",
1159
+ "data-expanded": open,
1160
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "currentColor", className: "h-4 w-4", "aria-hidden": "true", children: [
1161
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "5", cy: "12", r: "1.6" }),
1162
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "1.6" }),
1163
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "19", cy: "12", r: "1.6" })
1164
+ ] })
1165
+ }
1166
+ ),
1167
+ open && /* @__PURE__ */ jsxRuntime.jsx(
1168
+ "div",
1169
+ {
1170
+ role: "menu",
1171
+ onMouseEnter: openNow,
1172
+ onMouseLeave: closeSoon,
1173
+ className: `absolute z-30 ${horizontal ? "right-0 top-full mt-1.5" : "left-full top-0 ml-1.5"} min-w-[184px] max-h-72 overflow-y-auto hidden-scrollbar rounded-lg border border-border bg-surface p-1 shadow-md animate-in fade-in-0 zoom-in-95`,
1174
+ children: tabs.map((t) => {
1175
+ const isActive = t.value === value;
1176
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1177
+ "button",
1178
+ {
1179
+ type: "button",
1180
+ role: "menuitem",
1181
+ disabled: t.disabled,
1182
+ onClick: () => {
1183
+ select(t.value);
1184
+ setOpen(false);
1185
+ },
1186
+ className: `flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm transition-colors disabled:opacity-40 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-accent ${isActive ? "bg-surface-raised text-accent" : "text-foreground-secondary hover:bg-surface-raised hover:text-foreground"}`,
1187
+ children: [
1188
+ t.icon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-shrink-0 inline-flex h-4 w-4 items-center justify-center", children: t.icon }),
1189
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate", children: t.label }),
1190
+ isActive && /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 20 20", fill: "none", className: "h-4 w-4 flex-shrink-0 text-accent", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 10l4.5 4.5L16 6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
1191
+ ]
1192
+ },
1193
+ t.value
1194
+ );
1195
+ })
1196
+ }
1197
+ )
1198
+ ]
1199
+ }
1200
+ );
1201
+ }
1092
1202
  function TabsTrigger({ value, icon, badge, closeable, onClose, disabled, className = "", children }) {
1093
- const { value: active, variant, size, orientation, indicatorId, reduced } = useTabsContext();
1203
+ const { value: active, variant, size, orientation, indicatorId, reduced, registerTab, unregisterTab } = useTabsContext();
1094
1204
  const isActive = active === value;
1095
1205
  const horizontal = orientation === "horizontal";
1096
1206
  const sz = SIZE[size];
1097
- const base = "group/trigger relative inline-flex items-center justify-center whitespace-nowrap font-medium select-none transition-colors duration-150 focus:outline-none disabled:opacity-40 disabled:cursor-not-allowed flex-shrink-0";
1207
+ React8.useLayoutEffect(() => {
1208
+ registerTab(value, { label: children, icon, disabled });
1209
+ return () => unregisterTab(value);
1210
+ }, [value, children, icon, disabled, registerTab, unregisterTab]);
1211
+ const layoutCls = horizontal ? "justify-center flex-shrink-0" : "justify-start w-full";
1212
+ const base = "group/trigger relative inline-flex items-center whitespace-nowrap font-medium select-none transition-colors duration-150 focus:outline-none disabled:opacity-40 disabled:cursor-not-allowed";
1098
1213
  const variantCls = variant === "segmented" ? `rounded-md ${isActive ? "text-accent" : "text-foreground-secondary hover:text-foreground"} focus-visible:text-accent` : variant === "enclosed" ? `${horizontal ? "rounded-t-md border border-b-0 -mb-px" : "rounded-l-md border border-r-0 -mr-px"} ${isActive ? "bg-surface border-border text-foreground" : "border-transparent text-foreground-secondary hover:text-foreground hover:bg-surface-raised"} focus-visible:text-accent` : `${isActive ? "text-accent" : "text-foreground-secondary hover:text-foreground"} focus-visible:text-accent`;
1099
1214
  const trigger = /* @__PURE__ */ jsxRuntime.jsxs(
1100
1215
  TabsPrimitive__namespace.Trigger,
1101
1216
  {
1102
1217
  value,
1103
1218
  disabled,
1104
- className: [base, sz.trigger, closeable ? "pr-8" : "", variantCls, className].filter(Boolean).join(" "),
1219
+ className: [base, sz.trigger, layoutCls, closeable ? "pr-8" : "", variantCls, className].filter(Boolean).join(" "),
1105
1220
  children: [
1106
1221
  variant === "segmented" && isActive && /* @__PURE__ */ jsxRuntime.jsx(
1107
1222
  framerMotion.motion.span,
@@ -1134,7 +1249,7 @@ function TabsTrigger({ value, icon, badge, closeable, onClose, disabled, classNa
1134
1249
  }
1135
1250
  );
1136
1251
  if (!closeable) return trigger;
1137
- return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "relative inline-flex items-center flex-shrink-0", children: [
1252
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: `relative inline-flex items-center ${horizontal ? "flex-shrink-0" : "w-full"}`, children: [
1138
1253
  trigger,
1139
1254
  /* @__PURE__ */ jsxRuntime.jsx(
1140
1255
  "button",