@versini/ui-tabs 1.1.0 → 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Arno Versini
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -32,6 +32,7 @@ export declare type Size = "small" | "medium" | "large";
32
32
  * orphan TabsTrigger (no matching panel) renders without `aria-controls`.
33
33
  * 9. DOM-order traversal — triggers register in DOM order (compareDocumentPosition)
34
34
  * so keyboard navigation matches visual order.
35
+ *
35
36
  */
36
37
  export declare function Tabs<Value extends string = string>({ children, value, defaultValue, onValueChange, orientation, activationMode, size, mode, className, }: TabsProps<Value>): JSX.Element;
37
38
 
@@ -120,8 +121,8 @@ export declare type TabsProps<Value extends string = string> = {
120
121
  size?: Size;
121
122
  /**
122
123
  * How tab/panel text adapts to its surface. Use `"light"`/`"dark"` to force
123
- * colors for a surface whose lightness differs from the ambient theme (e.g.
124
- * a light card inside a dark-mode page); `"system"` follows the ambient theme,
124
+ * colors for a surface whose lightness differs from the ambient theme (e.g. a
125
+ * light card inside a dark-mode page); `"system"` follows the ambient theme,
125
126
  * `"alt-system"` inverts it.
126
127
  * @default "system"
127
128
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- @versini/ui-tabs v1.1.0
2
+ @versini/ui-tabs v1.1.1
3
3
  © 2026 gizmette.com
4
4
  */
5
5
 
@@ -81,19 +81,21 @@ const getTabsTriggerSizeClasses = ({ size })=>{
81
81
  };
82
82
  const getTabsTriggerClasses = ({ size, orientation, active, mode })=>{
83
83
  const borderSide = orientation === "vertical" ? "border-l-2" : "border-b-2";
84
- return clsx(TABS_TRIGGER_CLASSNAME, "relative cursor-pointer bg-transparent transition-colors outline-none", // High-contrast text that matches the surface (via mode). The active state
85
- // is conveyed by the indicator border + bold weight, never by dimming the
86
- // resting text. Matches clubroad's full-contrast inactive tabs.
87
- getModeTextClasses({
84
+ return clsx(TABS_TRIGGER_CLASSNAME, "relative cursor-pointer bg-transparent transition-colors outline-none", /**
85
+ * High-contrast text that matches the surface (via mode). The active state is
86
+ * conveyed by the indicator border + bold weight, never by dimming the
87
+ * resting text. Matches clubroad's full-contrast inactive tabs.
88
+ */ getModeTextClasses({
88
89
  mode
89
90
  }), "focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus-dark", "disabled:cursor-not-allowed disabled:opacity-50", getTabsTriggerSizeClasses({
90
91
  size
91
92
  }), borderSide, active ? "border-action-light" : "border-transparent");
92
93
  };
93
94
  const getTabsContentClasses = ({ className, mode })=>{
94
- return clsx(TABS_CONTENT_CLASSNAME, // Theme-aware text so panel content stays readable on the actual surface
95
- // (consumers can still override via className).
96
- getModeTextClasses({
95
+ return clsx(TABS_CONTENT_CLASSNAME, /**
96
+ * Theme-aware text so panel content stays readable on the actual surface
97
+ * (consumers can still override via className).
98
+ */ getModeTextClasses({
97
99
  mode
98
100
  }), "outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus-dark", className);
99
101
  };
@@ -105,7 +107,10 @@ const getTabsContentClasses = ({ className, mode })=>{
105
107
 
106
108
 
107
109
 
108
- /** Nearest enabled trigger to `fromIndex`: scan forward (next), then backward (previous). */ const findNearestEnabledIndex = (triggers, fromIndex)=>{
110
+ /**
111
+ * Nearest enabled trigger to `fromIndex`: scan forward (next), then backward
112
+ * (previous).
113
+ */ const findNearestEnabledIndex = (triggers, fromIndex)=>{
109
114
  const count = triggers.length;
110
115
  const start = Math.min(Math.max(fromIndex, 0), count);
111
116
  for(let i = start; i < count; i++){
@@ -144,6 +149,7 @@ const getTabsContentClasses = ({ className, mode })=>{
144
149
  * orphan TabsTrigger (no matching panel) renders without `aria-controls`.
145
150
  * 9. DOM-order traversal — triggers register in DOM order (compareDocumentPosition)
146
151
  * so keyboard navigation matches visual order.
152
+ *
147
153
  */ function Tabs({ children, value, defaultValue, onValueChange, orientation = "horizontal", activationMode = "automatic", size = "medium", mode = "system", className }) {
148
154
  const [activeValue, setActiveValue] = useUncontrolled({
149
155
  value,
@@ -154,19 +160,22 @@ const getTabsContentClasses = ({ className, mode })=>{
154
160
  const triggersRef = useRef([]);
155
161
  const panelsRef = useRef(new Set());
156
162
  const focusWithinRef = useRef(false);
157
- // Last known index of the active trigger, used to recover to the *nearest*
158
- // trigger when the active one is removed (its value is gone from the registry,
159
- // but this ref still holds the slot it occupied).
160
- const lastActiveIndexRef = useRef(0);
161
- // Bumped whenever a trigger/panel registers or unregisters; it is a dependency
162
- // of the context memo so consumers re-render and recompute roving tabIndex /
163
- // aria-controls from the (mutable) registries.
164
- const [registrationVersion, setRegistrationVersion] = useState(0);
163
+ /**
164
+ * Last known index of the active trigger, used to recover to the *nearest*
165
+ * trigger when the active one is removed (its value is gone from the registry,
166
+ * but this ref still holds the slot it occupied).
167
+ */ const lastActiveIndexRef = useRef(0);
168
+ /**
169
+ * Bumped whenever a trigger/panel registers or unregisters; it is a dependency
170
+ * of the context memo so consumers re-render and recompute roving tabIndex /
171
+ * aria-controls from the (mutable) registries.
172
+ */ const [registrationVersion, setRegistrationVersion] = useState(0);
165
173
  const bumpVersion = useCallback(()=>setRegistrationVersion((version)=>version + 1), []);
166
- // Stable activation setter (useUncontrolled returns a fresh setter each render
167
- // in uncontrolled mode; a ref keeps `setValue`'s identity stable so the context
168
- // memo is not invalidated on every render).
169
- const setActiveRef = useRef(setActiveValue);
174
+ /**
175
+ * Stable activation setter (useUncontrolled returns a fresh setter each render
176
+ * in uncontrolled mode; a ref keeps `setValue`'s identity stable so the
177
+ * context memo is not invalidated on every render).
178
+ */ const setActiveRef = useRef(setActiveValue);
170
179
  setActiveRef.current = setActiveValue;
171
180
  const setValue = useCallback((next)=>{
172
181
  setActiveRef.current(next);
@@ -216,16 +225,18 @@ const getTabsContentClasses = ({ className, mode })=>{
216
225
  focusWithinRef.current = true;
217
226
  }, []);
218
227
  const handleRootBlur = useCallback((event)=>{
219
- // Keep the flag when focus is lost to <body> (e.g. the focused trigger
220
- // unmounts) so recovery can restore it; only clear when focus genuinely
221
- // moves to an element outside this Tabs.
222
- if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) {
228
+ /**
229
+ * Keep the flag when focus is lost to <body> (e.g. the focused trigger
230
+ * unmounts) so recovery can restore it; only clear when focus genuinely moves
231
+ * to an element outside this Tabs.
232
+ */ if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) {
223
233
  focusWithinRef.current = false;
224
234
  }
225
235
  }, []);
226
- // Default resolution (Invariant 3) + dynamic recovery (Invariant 7), both
227
- // uncontrolled-only (controlled mode delegates recovery to the consumer).
228
- useIsomorphicLayoutEffect(()=>{
236
+ /**
237
+ * Default resolution (Invariant 3) + dynamic recovery (Invariant 7), both
238
+ * uncontrolled-only (controlled mode delegates recovery to the consumer).
239
+ */ useIsomorphicLayoutEffect(()=>{
229
240
  if (value !== undefined) {
230
241
  return;
231
242
  }
@@ -238,9 +249,10 @@ const getTabsContentClasses = ({ className, mode })=>{
238
249
  if (recoveredIndex !== -1) {
239
250
  const recovered = triggers[recoveredIndex];
240
251
  setActiveValue(recovered.value);
241
- // Restore keyboard position if focus was inside the tablist (e.g. the
242
- // focused active trigger was just removed and focus fell to <body>).
243
- if (focusWithinRef.current) {
252
+ /**
253
+ * Restore keyboard position if focus was inside the tablist (e.g. the
254
+ * focused active trigger was just removed and focus fell to <body>).
255
+ */ if (focusWithinRef.current) {
244
256
  recovered.element.focus();
245
257
  }
246
258
  }
@@ -250,10 +262,11 @@ const getTabsContentClasses = ({ className, mode })=>{
250
262
  if (activeIndex !== -1) {
251
263
  lastActiveIndexRef.current = activeIndex;
252
264
  }
253
- // Exactly one trigger carries tabIndex=0 (Invariant 2): the active trigger if
254
- // it matches one (even when disabled, Invariant 5), else the first enabled,
255
- // else the first.
256
- let tabbableValue;
265
+ /**
266
+ * Exactly one trigger carries tabIndex=0 (Invariant 2): the active trigger if
267
+ * it matches one (even when disabled, Invariant 5), else the first enabled,
268
+ * else the first.
269
+ */ let tabbableValue;
257
270
  if (activeIndex !== -1) {
258
271
  tabbableValue = activeValue;
259
272
  } else {
@@ -278,9 +291,10 @@ const getTabsContentClasses = ({ className, mode })=>{
278
291
  }), [
279
292
  activeValue,
280
293
  tabbableValue,
281
- // registrationVersion drives recompute of roving tabIndex / aria-controls
282
- // from the registries when a trigger/panel (un)registers.
283
- registrationVersion,
294
+ /**
295
+ * registrationVersion drives recompute of roving tabIndex / aria-controls
296
+ * from the registries when a trigger/panel (un)registers.
297
+ */ registrationVersion,
284
298
  setValue,
285
299
  orientation,
286
300
  activationMode,
@@ -326,10 +340,11 @@ function TabsContent({ value, children, className, ...rest }) {
326
340
  registerPanel,
327
341
  unregisterPanel
328
342
  ]);
329
- // Only the active panel renders, and only when a matching trigger exists
330
- // (an orphan TabsContent never renders). Check active first so inactive
331
- // panels skip the trigger-registry scan.
332
- if (activeValue !== value) {
343
+ /**
344
+ * Only the active panel renders, and only when a matching trigger exists (an
345
+ * orphan TabsContent never renders). Check active first so inactive panels
346
+ * skip the trigger-registry scan.
347
+ */ if (activeValue !== value) {
333
348
  return null;
334
349
  }
335
350
  const hasMatchingTrigger = getTriggers().some((trigger)=>trigger.value === value);
@@ -356,9 +371,10 @@ TabsContent.displayName = "TabsContent";
356
371
 
357
372
  const getNextEnabledIndex = (triggers, currentIndex, direction)=>{
358
373
  const count = triggers.length;
359
- // Seed the sentinel per direction so the first forward step lands on 0 and the
360
- // first backward step lands on count-1 (correct wrap when nothing is current).
361
- let index = currentIndex < 0 ? direction === 1 ? -1 : count : currentIndex;
374
+ /**
375
+ * Seed the sentinel per direction so the first forward step lands on 0 and the
376
+ * first backward step lands on count-1 (correct wrap when nothing is current).
377
+ */ let index = currentIndex < 0 ? direction === 1 ? -1 : count : currentIndex;
362
378
  for(let i = 0; i < count; i++){
363
379
  index = (index + direction + count) % count;
364
380
  if (!triggers[index].disabled) {
@@ -436,10 +452,11 @@ const getLastEnabledIndex = (triggers)=>{
436
452
  case "Enter":
437
453
  case " ":
438
454
  {
439
- // Prevent the native <button> click (and Space-scroll) so we own
440
- // activation. In manual mode, activate the focused trigger; in
441
- // automatic mode the tab is already active, so this is a no-op.
442
- event.preventDefault();
455
+ /**
456
+ * Prevent the native <button> click (and Space-scroll) so we own
457
+ * activation. In manual mode, activate the focused trigger; in automatic
458
+ * mode the tab is already active, so this is a no-op.
459
+ */ event.preventDefault();
443
460
  if (activationMode === "manual" && currentIndex >= 0 && !triggers[currentIndex].disabled) {
444
461
  setValue(triggers[currentIndex].value);
445
462
  }
@@ -510,9 +527,10 @@ TabsList.displayName = "TabsList";
510
527
  function TabsTrigger({ value, children, disabled = false, className, onClick, ...rest }) {
511
528
  const { value: activeValue, tabbableValue, setValue, orientation, size, mode, baseId, registerTrigger, unregisterTrigger, hasPanel } = useContext(TabsContext);
512
529
  const buttonRef = useRef(null);
513
- // Register in a layout effect (before paint) so the panel's first render sees
514
- // a populated trigger registry avoids a one-frame empty/flicker state.
515
- useIsomorphicLayoutEffect(()=>{
530
+ /**
531
+ * Register in a layout effect (before paint) so the panel's first render sees
532
+ * a populated trigger registry — avoids a one-frame empty/flicker state.
533
+ */ useIsomorphicLayoutEffect(()=>{
516
534
  const element = buttonRef.current;
517
535
  /* v8 ignore start - ref is always set after mount */ if (!element) {
518
536
  return;
@@ -534,9 +552,10 @@ function TabsTrigger({ value, children, disabled = false, className, onClick, ..
534
552
  const active = activeValue === value;
535
553
  const tabId = getTabId(baseId, value);
536
554
  const panelId = getPanelId(baseId, value);
537
- // Emit aria-controls only on the active trigger and only when its panel is
538
- // registered (avoids a dangling IDREF; supports trigger-only/navigation use).
539
- const controlsPanel = active && hasPanel(value);
555
+ /**
556
+ * Emit aria-controls only on the active trigger and only when its panel is
557
+ * registered (avoids a dangling IDREF; supports trigger-only/navigation use).
558
+ */ const controlsPanel = active && hasPanel(value);
540
559
  return /*#__PURE__*/ jsx("button", {
541
560
  ...rest,
542
561
  ref: buttonRef,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versini/ui-tabs",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "license": "MIT",
5
5
  "author": "Arno Versini",
6
6
  "publishConfig": {
@@ -41,11 +41,12 @@
41
41
  "test:visual:ui": "playwright test -c playwright-ct.config.ts --ui"
42
42
  },
43
43
  "dependencies": {
44
- "@versini/ui-hooks": "workspace:*",
44
+ "@versini/ui-hooks": "6.1.1",
45
45
  "clsx": "2.1.1",
46
46
  "tailwindcss": "4.3.0"
47
47
  },
48
48
  "sideEffects": [
49
49
  "**/*.css"
50
- ]
50
+ ],
51
+ "gitHead": "4d011eb94af9a5685d315e7c965fdf85241635c1"
51
52
  }