@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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { colors_default } from './chunk-GKXP6OJJ.js';
2
2
  export { colors_default as COLORS, PALETTE as palette, semanticTokens, vars } from './chunk-GKXP6OJJ.js';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
- import React8, { createContext, useState, useEffect, useMemo, useId, useCallback, useContext, useRef, useLayoutEffect, useSyncExternalStore } from 'react';
4
+ import React8, { createContext, useState, useEffect, useMemo, useId, useCallback, useRef, useContext, useLayoutEffect, useSyncExternalStore } from 'react';
5
5
  import { createPortal } from 'react-dom';
6
6
  import * as AvatarPrimitive from '@radix-ui/react-avatar';
7
7
  import * as Dialog from '@radix-ui/react-dialog';
@@ -951,15 +951,27 @@ function Tabs({
951
951
  const current = isControlled ? value : internal;
952
952
  const reduced = !!useReducedMotion();
953
953
  const indicatorId = useId();
954
- const handle = (next) => {
954
+ const select = useCallback((next) => {
955
955
  if (!isControlled) setInternal(next);
956
956
  onValueChange?.(next);
957
- };
958
- return /* @__PURE__ */ jsx(TabsContext.Provider, { value: { value: current, variant, size, orientation, indicatorId, reduced }, children: /* @__PURE__ */ jsx(
957
+ }, [isControlled, onValueChange]);
958
+ const registry = useRef(/* @__PURE__ */ new Map());
959
+ const orderRef = useRef(0);
960
+ const [, bump] = useState(0);
961
+ const registerTab = useCallback((val, meta) => {
962
+ const existing = registry.current.get(val);
963
+ registry.current.set(val, { ...meta, order: existing?.order ?? orderRef.current++ });
964
+ if (!existing) bump((v) => v + 1);
965
+ }, []);
966
+ const unregisterTab = useCallback((val) => {
967
+ if (registry.current.delete(val)) bump((v) => v + 1);
968
+ }, []);
969
+ const getTabs = 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 })), []);
970
+ return /* @__PURE__ */ jsx(TabsContext.Provider, { value: { value: current, variant, size, orientation, indicatorId, reduced, select, registerTab, unregisterTab, getTabs }, children: /* @__PURE__ */ jsx(
959
971
  TabsPrimitive.Root,
960
972
  {
961
973
  value: current,
962
- onValueChange: handle,
974
+ onValueChange: select,
963
975
  orientation,
964
976
  className: [
965
977
  "flex min-w-0",
@@ -972,7 +984,7 @@ function Tabs({
972
984
  ) });
973
985
  }
974
986
  function TabsList({ children, "aria-label": ariaLabel, className = "" }) {
975
- const { variant, orientation, reduced } = useTabsContext();
987
+ const { variant, orientation, reduced, value } = useTabsContext();
976
988
  const horizontal = orientation === "horizontal";
977
989
  const scrollRef = useRef(null);
978
990
  const [edges, setEdges] = useState({ start: false, end: false });
@@ -1008,6 +1020,14 @@ function TabsList({ children, "aria-label": ariaLabel, className = "" }) {
1008
1020
  const amount = (horizontal ? el.clientWidth : el.clientHeight) * 0.7 * dir;
1009
1021
  el.scrollBy({ [horizontal ? "left" : "top"]: amount, behavior: reduced ? "auto" : "smooth" });
1010
1022
  }, [horizontal, reduced]);
1023
+ useLayoutEffect(() => {
1024
+ const el = scrollRef.current;
1025
+ if (!el || !scrollable) return;
1026
+ const active = el.querySelector("[role=tab][data-state=active]");
1027
+ if (active && typeof active.scrollIntoView === "function") {
1028
+ active.scrollIntoView({ block: "nearest", inline: "nearest", behavior: reduced ? "auto" : "smooth" });
1029
+ }
1030
+ }, [value, scrollable, reduced]);
1011
1031
  const maskStyle = scrollable && (edges.start || edges.end) ? (() => {
1012
1032
  const dir = horizontal ? "to right" : "to bottom";
1013
1033
  const a = edges.start ? "transparent, black 36px" : "black";
@@ -1024,7 +1044,8 @@ function TabsList({ children, "aria-label": ariaLabel, className = "" }) {
1024
1044
  return `flex ${horizontal ? "flex-row" : "flex-col"} ${align} gap-1 ${hairline}`;
1025
1045
  })();
1026
1046
  const scrollClass = scrollable ? horizontal ? "overflow-x-auto overflow-y-hidden hidden-scrollbar" : "overflow-y-auto overflow-x-hidden hidden-scrollbar" : "";
1027
- return /* @__PURE__ */ jsxs("div", { className: ["relative flex min-w-0", horizontal ? "flex-row items-stretch" : "flex-col items-stretch", className].filter(Boolean).join(" "), children: [
1047
+ const overflowing = scrollable && (edges.start || edges.end);
1048
+ return /* @__PURE__ */ jsxs("div", { className: ["relative flex min-w-0 gap-1", horizontal ? "flex-row items-stretch" : "flex-col items-stretch", className].filter(Boolean).join(" "), children: [
1028
1049
  scrollable && edges.start && /* @__PURE__ */ jsx(Chevron, { side: "start", orientation, onClick: () => nudge(-1) }),
1029
1050
  /* @__PURE__ */ jsx(
1030
1051
  TabsPrimitive.List,
@@ -1036,37 +1057,131 @@ function TabsList({ children, "aria-label": ariaLabel, className = "" }) {
1036
1057
  children
1037
1058
  }
1038
1059
  ),
1039
- scrollable && edges.end && /* @__PURE__ */ jsx(Chevron, { side: "end", orientation, onClick: () => nudge(1) })
1060
+ scrollable && edges.end && /* @__PURE__ */ jsx(Chevron, { side: "end", orientation, onClick: () => nudge(1) }),
1061
+ overflowing && /* @__PURE__ */ jsx(OverflowMenu, {})
1040
1062
  ] });
1041
1063
  }
1042
1064
  function Chevron({ side, orientation, onClick }) {
1043
1065
  const horizontal = orientation === "horizontal";
1044
1066
  const rotate = horizontal ? side === "start" ? "rotate-180" : "" : side === "start" ? "-rotate-90" : "rotate-90";
1045
- 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";
1046
1067
  return /* @__PURE__ */ jsx(
1047
1068
  "button",
1048
1069
  {
1049
1070
  type: "button",
1050
1071
  "aria-label": side === "start" ? "Scroll tabs backward" : "Scroll tabs forward",
1051
1072
  onClick,
1052
- 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`,
1073
+ 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",
1053
1074
  children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, className: `h-4 w-4 ${rotate}`, "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9 5l7 7-7 7" }) })
1054
1075
  }
1055
1076
  );
1056
1077
  }
1078
+ function OverflowMenu() {
1079
+ const { getTabs, value, select, orientation } = useTabsContext();
1080
+ const horizontal = orientation === "horizontal";
1081
+ const [open, setOpen] = useState(false);
1082
+ const wrapRef = useRef(null);
1083
+ const timer = useRef(null);
1084
+ const openNow = () => {
1085
+ if (timer.current) clearTimeout(timer.current);
1086
+ setOpen(true);
1087
+ };
1088
+ const closeSoon = () => {
1089
+ timer.current = setTimeout(() => setOpen(false), 160);
1090
+ };
1091
+ useLayoutEffect(() => {
1092
+ if (!open) return;
1093
+ const onDoc = (e) => {
1094
+ if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
1095
+ };
1096
+ const onKey = (e) => {
1097
+ if (e.key === "Escape") setOpen(false);
1098
+ };
1099
+ document.addEventListener("mousedown", onDoc);
1100
+ document.addEventListener("keydown", onKey);
1101
+ return () => {
1102
+ document.removeEventListener("mousedown", onDoc);
1103
+ document.removeEventListener("keydown", onKey);
1104
+ };
1105
+ }, [open]);
1106
+ const tabs = getTabs();
1107
+ return /* @__PURE__ */ jsxs(
1108
+ "div",
1109
+ {
1110
+ ref: wrapRef,
1111
+ className: "relative flex-shrink-0 self-center",
1112
+ onMouseEnter: openNow,
1113
+ onMouseLeave: closeSoon,
1114
+ children: [
1115
+ /* @__PURE__ */ jsx(
1116
+ "button",
1117
+ {
1118
+ type: "button",
1119
+ "aria-haspopup": "menu",
1120
+ "aria-expanded": open,
1121
+ "aria-label": "Show all tabs",
1122
+ onClick: () => setOpen((o) => !o),
1123
+ 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",
1124
+ "data-expanded": open,
1125
+ children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "currentColor", className: "h-4 w-4", "aria-hidden": "true", children: [
1126
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "12", r: "1.6" }),
1127
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1.6" }),
1128
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "12", r: "1.6" })
1129
+ ] })
1130
+ }
1131
+ ),
1132
+ open && /* @__PURE__ */ jsx(
1133
+ "div",
1134
+ {
1135
+ role: "menu",
1136
+ onMouseEnter: openNow,
1137
+ onMouseLeave: closeSoon,
1138
+ 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`,
1139
+ children: tabs.map((t) => {
1140
+ const isActive = t.value === value;
1141
+ return /* @__PURE__ */ jsxs(
1142
+ "button",
1143
+ {
1144
+ type: "button",
1145
+ role: "menuitem",
1146
+ disabled: t.disabled,
1147
+ onClick: () => {
1148
+ select(t.value);
1149
+ setOpen(false);
1150
+ },
1151
+ 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"}`,
1152
+ children: [
1153
+ t.icon && /* @__PURE__ */ jsx("span", { className: "flex-shrink-0 inline-flex h-4 w-4 items-center justify-center", children: t.icon }),
1154
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: t.label }),
1155
+ isActive && /* @__PURE__ */ jsx("svg", { viewBox: "0 0 20 20", fill: "none", className: "h-4 w-4 flex-shrink-0 text-accent", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 10l4.5 4.5L16 6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) })
1156
+ ]
1157
+ },
1158
+ t.value
1159
+ );
1160
+ })
1161
+ }
1162
+ )
1163
+ ]
1164
+ }
1165
+ );
1166
+ }
1057
1167
  function TabsTrigger({ value, icon, badge, closeable, onClose, disabled, className = "", children }) {
1058
- const { value: active, variant, size, orientation, indicatorId, reduced } = useTabsContext();
1168
+ const { value: active, variant, size, orientation, indicatorId, reduced, registerTab, unregisterTab } = useTabsContext();
1059
1169
  const isActive = active === value;
1060
1170
  const horizontal = orientation === "horizontal";
1061
1171
  const sz = SIZE[size];
1062
- 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";
1172
+ useLayoutEffect(() => {
1173
+ registerTab(value, { label: children, icon, disabled });
1174
+ return () => unregisterTab(value);
1175
+ }, [value, children, icon, disabled, registerTab, unregisterTab]);
1176
+ const layoutCls = horizontal ? "justify-center flex-shrink-0" : "justify-start w-full";
1177
+ 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";
1063
1178
  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`;
1064
1179
  const trigger = /* @__PURE__ */ jsxs(
1065
1180
  TabsPrimitive.Trigger,
1066
1181
  {
1067
1182
  value,
1068
1183
  disabled,
1069
- className: [base, sz.trigger, closeable ? "pr-8" : "", variantCls, className].filter(Boolean).join(" "),
1184
+ className: [base, sz.trigger, layoutCls, closeable ? "pr-8" : "", variantCls, className].filter(Boolean).join(" "),
1070
1185
  children: [
1071
1186
  variant === "segmented" && isActive && /* @__PURE__ */ jsx(
1072
1187
  motion.span,
@@ -1099,7 +1214,7 @@ function TabsTrigger({ value, icon, badge, closeable, onClose, disabled, classNa
1099
1214
  }
1100
1215
  );
1101
1216
  if (!closeable) return trigger;
1102
- return /* @__PURE__ */ jsxs("span", { className: "relative inline-flex items-center flex-shrink-0", children: [
1217
+ return /* @__PURE__ */ jsxs("span", { className: `relative inline-flex items-center ${horizontal ? "flex-shrink-0" : "w-full"}`, children: [
1103
1218
  trigger,
1104
1219
  /* @__PURE__ */ jsx(
1105
1220
  "button",