@webjsdev/ui 0.3.9 → 0.3.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/ui",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "type": "module",
5
5
  "description": "An AI-first component library - class-helper functions for visuals, custom elements only where state matters. Source-copied into your repo, you own it. Works with any Tailwind v4 project.",
6
6
  "bin": {
@@ -241,7 +241,13 @@ export class UiTabsTrigger extends WebComponent({
241
241
  const tabs = this._tabs;
242
242
  if (!tabs) return;
243
243
  const orientation = tabs.orientation;
244
- const triggers = Array.from(tabs.querySelectorAll<UiTabsTrigger>('ui-tabs-trigger'));
244
+ // Scope to triggers belonging to THIS group: querySelectorAll crosses into
245
+ // a nested <ui-tabs> inside a panel, and arrow nav at the boundary would
246
+ // jump into the inner group and set this group's value to an inner-only
247
+ // value (hiding every panel). Same scoping dropdown-menu applies to items.
248
+ const triggers = Array.from(tabs.querySelectorAll<UiTabsTrigger>('ui-tabs-trigger')).filter(
249
+ (t) => t._tabs === tabs,
250
+ );
245
251
  const idx = triggers.indexOf(this);
246
252
  const nextKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
247
253
  const prevKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
@@ -257,7 +263,13 @@ export class UiTabsTrigger extends WebComponent({
257
263
  e.preventDefault();
258
264
  const v = target.value;
259
265
  if (v) tabs.setAttribute('value', v);
260
- target.focus();
266
+ // Focus the inner <button role="tab">, NOT the <ui-tabs-trigger> host.
267
+ // The host carries no tabindex so it is not focusable; calling focus()
268
+ // on it is a no-op that leaves the focus ring stranded on the previous
269
+ // trigger while a different panel shows. The roving tabindex lives on
270
+ // the inner button, so that is what APG focus-follows-selection targets.
271
+ const btn = target.querySelector<HTMLButtonElement>('[role="tab"]');
272
+ (btn ?? target).focus();
261
273
  }
262
274
  };
263
275
  }
@@ -73,22 +73,48 @@ const GROUPS: Array<[RegExp, string]> = [
73
73
  [/^grid(-|$)/, 'grid'],
74
74
  ];
75
75
 
76
+ // Directional shorthand conflicts (the tailwind-merge model). A shorthand
77
+ // utility invalidates the axis / side utilities it SUBSUMES, because Tailwind's
78
+ // cascade makes the shorthand win: `p-0` after `px-4 py-2` drops both, but
79
+ // `px-4` after `p-2` refines only the x-axis so BOTH survive (the conflict is
80
+ // directional, not symmetric). Keyed by the group a NEWLY-seen token belongs to;
81
+ // the value lists the EARLIER groups whose survivors it removes (its own group
82
+ // is always removed too, handled below).
83
+ const CONFLICTS: Record<string, string[]> = {
84
+ p: ['px', 'py', 'pt', 'pr', 'pb', 'pl'],
85
+ px: ['pl', 'pr'],
86
+ py: ['pt', 'pb'],
87
+ m: ['mx', 'my', 'mt', 'mr', 'mb', 'ml'],
88
+ mx: ['ml', 'mr'],
89
+ my: ['mt', 'mb'],
90
+ size: ['w', 'h'],
91
+ };
92
+
76
93
  function dedupeUtilities(input: string): string {
77
94
  const tokens = input.split(/\s+/).filter(Boolean);
78
- const seen = new Map<string, number>();
95
+ // `${prefix}::${group}` -> index of the last SURVIVING token in that group.
96
+ const lastByKey = new Map<string, number>();
79
97
  const result: Array<string | null> = [];
80
98
 
81
99
  for (const token of tokens) {
82
- let key: string | null = null;
83
- // Strip variant prefix (`hover:`, `dark:md:`, …) before testing each
84
- // dedupe regex so `hover:bg-red-500` still matches the `bg-color` group.
100
+ // Strip variant prefix (`hover:`, `dark:md:`, …) before testing each dedupe
101
+ // regex so `hover:bg-red-500` still matches the `bg-color` group. Conflicts
102
+ // only apply WITHIN the same variant (`px-4 hover:p-0` keeps both).
85
103
  const prefix = variantPrefix(token);
86
104
  const bare = prefix ? token.slice(prefix.length) : token;
87
- for (const [re, gk] of GROUPS) {
88
- if (re.test(bare)) { key = `${prefix}::${gk}`; break; }
105
+ let gk: string | null = null;
106
+ for (const [re, g] of GROUPS) {
107
+ if (re.test(bare)) { gk = g; break; }
108
+ }
109
+ if (gk) {
110
+ // Remove earlier survivors in this group AND in every group it subsumes.
111
+ for (const g of [gk, ...(CONFLICTS[gk] ?? [])]) {
112
+ const k = `${prefix}::${g}`;
113
+ const idx = lastByKey.get(k);
114
+ if (idx !== undefined) { result[idx] = null; lastByKey.delete(k); }
115
+ }
116
+ lastByKey.set(`${prefix}::${gk}`, result.length);
89
117
  }
90
- if (key && seen.has(key)) result[seen.get(key)!] = null;
91
- if (key) seen.set(key, result.length);
92
118
  result.push(token);
93
119
  }
94
120
  return result.filter(Boolean).join(' ');
@@ -135,7 +135,18 @@
135
135
  }
136
136
 
137
137
  @layer base {
138
+ /* Focus: a themed, keyboard-only (:focus-visible), never-removed outline, in
139
+ the --ring colour, applied globally. `outline-ring/50` themes only the
140
+ COLOUR and leaves outline-style: auto, which the browser draws as its OWN
141
+ ring (it can look thick/white and ignore the colour). So force a SOLID
142
+ outline on focus, at a consistent width + offset, so the themed --ring/50
143
+ colour actually renders the same on every element. A shadow-DOM component
144
+ sets its own focus (a document rule cannot cross the shadow boundary). */
138
145
  * {
139
146
  @apply border-border outline-ring/50;
140
147
  }
148
+ :focus-visible {
149
+ outline: 2px solid color-mix(in oklab, var(--color-ring) 50%, transparent);
150
+ outline-offset: 2px;
151
+ }
141
152
  }
@@ -19,13 +19,7 @@
19
19
  */
20
20
 
21
21
  import { loadRegistryItem, loadRegistryIndex, isCustomElementSource } from './local.js';
22
- import { extractExample, stripExample } from './example.js';
23
-
24
- // Re-export the lean-copy primitives so the scaffold generator (via
25
- // `packages/cli/lib/lean-copy.js`) has one import surface for the shared
26
- // example-strip. (The CLI's own `add` imports them directly from local.js /
27
- // example.js.)
28
- export { isCustomElementSource, stripExample };
22
+ import { extractExample } from './example.js';
29
23
 
30
24
  /**
31
25
  * Extract the exported class-helper signatures from a Tier-1 source, e.g.