@godxjp/ui 27.12.1 → 28.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/components/data-display/card.js +5 -0
- package/dist/components/navigation/tabs.d.ts +1 -1
- package/dist/components/navigation/tabs.js +54 -6
- package/dist/components/ui/toggle-group.d.ts +22 -0
- package/dist/components/ui/toggle-group.js +59 -3
- package/dist/components/ui/toggle.d.ts +11 -2
- package/dist/components/ui/toggle.js +3 -2
- package/dist/contracts/measurement.json +1 -1
- package/dist/props/components/data-display.prop.d.ts +15 -0
- package/dist/props/components/navigation.prop.d.ts +50 -0
- package/dist/props/registry.d.ts +18 -2
- package/dist/props/registry.js +28 -2
- package/dist/styles/card-layout.css +3 -4
- package/dist/styles/navigation-layout.css +109 -0
- package/dist/tokens/components/navigation.css +18 -0
- package/docs/DESIGN-AUTHORITY.md +33 -0
- package/docs/data-entry/toggle-group.tsx +86 -6
- package/docs/navigation/card-tabs.tsx +344 -0
- package/package.json +2 -2
- package/scripts/ui-audit.mjs +120 -4
package/scripts/ui-audit.mjs
CHANGED
|
@@ -158,6 +158,63 @@ const ATTRS = String.raw`(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|=>|[^>"'])*`;
|
|
|
158
158
|
*/
|
|
159
159
|
const rawTag = (tag) => new RegExp(`<${tag}(?=[\\s/>])[^\\n]*`, "g");
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Primitives whose `asChild` hands over THE VISUAL BOX, so the borrowed element cannot be a
|
|
163
|
+
* `<Button>` — wrapping it would paint a button where a card belongs.
|
|
164
|
+
*
|
|
165
|
+
* This set is deliberately tiny and deliberately NOT "anything with asChild". The first cut of
|
|
166
|
+
* this exemption was the broad version, and measuring it against a real consumer showed what that
|
|
167
|
+
* costs: of 39 `no-raw-button` findings across its three SPAs, it silenced 11 — and
|
|
168
|
+
* every single one was a BEHAVIOUR trigger, not a box:
|
|
169
|
+
*
|
|
170
|
+
* DropdownMenuTrigger · SheetTrigger · CollapsibleTrigger · PopoverTrigger · TooltipTrigger
|
|
171
|
+
*
|
|
172
|
+
* A Radix trigger supplies behaviour and no styling, so `<DropdownMenuTrigger asChild><Button>`
|
|
173
|
+
* is not only legal, it is the better answer — the package's button, correctly styled, keyboard
|
|
174
|
+
* and screen-reader shaped. Exempting those threw away 11 legitimate nudges to fix 0 real
|
|
175
|
+
* findings. Adding a name here means asserting that primitive paints the box itself.
|
|
176
|
+
*/
|
|
177
|
+
const ASCHILD_BOX_PRIMITIVES = new Set(["Card"]);
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Is the raw tag at `index` the slot child of a primitive that hands over its BOX?
|
|
181
|
+
*
|
|
182
|
+
* `<Card asChild hoverable><button onClick={…}>` is not a violation — it is the shape this
|
|
183
|
+
* package PRESCRIBES. `Card.asChild`'s own docblock says so in as many words:
|
|
184
|
+
*
|
|
185
|
+
* "a card rendered as an `<a>` or a `<button>` measures byte for byte like the `div` it
|
|
186
|
+
* replaces and stays a SINGLE tab stop"
|
|
187
|
+
*
|
|
188
|
+
* and `hoverable`'s docblock sends the reader there: "the whole card rendered as one via
|
|
189
|
+
* {@link CardProps.asChild}".
|
|
190
|
+
*
|
|
191
|
+
* Reported as gh#740 and closed by shipping `Card asChild` — but the audit was never taught about
|
|
192
|
+
* it, so a consumer following the fix still got `no-raw-button: error` on the documented answer
|
|
193
|
+
* (godx-jp/shoots-gemba#8). A package that forbids its own prescription leaves the consumer no
|
|
194
|
+
* legal move at all, which is exactly what #740 was opened about.
|
|
195
|
+
*
|
|
196
|
+
* Walks BACKWARDS to the nearest preceding `>` and asks whether the tag it closes is one of
|
|
197
|
+
* ASCHILD_BOX_PRIMITIVES carrying `asChild`. That covers both spellings prettier produces —
|
|
198
|
+
* `<Card asChild><button` on one line, and the attribute wrapped onto its own line — while
|
|
199
|
+
* `</Card>` or `<div>` before the tag exempts nothing, because neither starts a component, and
|
|
200
|
+
* `<DropdownMenuTrigger asChild>` exempts nothing either, because a trigger paints no box.
|
|
201
|
+
*/
|
|
202
|
+
const isAsChildSlot = (content, index) => {
|
|
203
|
+
const before = content.slice(0, index).replace(/\s+$/, "");
|
|
204
|
+
if (!before.endsWith(">")) return false;
|
|
205
|
+
const openStart = before.lastIndexOf("<", before.length - 1);
|
|
206
|
+
if (openStart === -1) return false;
|
|
207
|
+
const openTag = before.slice(openStart);
|
|
208
|
+
// A component, not `</Card>` and not a lowercase host element…
|
|
209
|
+
const name = /^<([A-Z][\w.]*)\b/.exec(openTag)?.[1];
|
|
210
|
+
// …and one that hands over its BOX, not merely its behaviour. See ASCHILD_BOX_PRIMITIVES.
|
|
211
|
+
return name !== undefined && ASCHILD_BOX_PRIMITIVES.has(name) && /\basChild\b/.test(openTag);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** `rawTag`, minus the matches that are an `asChild` primitive's borrowed element. */
|
|
215
|
+
const rawTagOutsideAsChildSlot = (tag) => (content) =>
|
|
216
|
+
[...content.matchAll(rawTag(tag))].filter((match) => !isAsChildSlot(content, match.index));
|
|
217
|
+
|
|
161
218
|
/**
|
|
162
219
|
* @type {{id:string, severity:'error'|'warn', test:RegExp, message:string, standard?:string,
|
|
163
220
|
* exempt?:RegExp, classOnly?:boolean}[]}
|
|
@@ -409,6 +466,12 @@ const RULES = [
|
|
|
409
466
|
severity: "error",
|
|
410
467
|
spansElement: true,
|
|
411
468
|
test: new RegExp(`<input\\b(?!${ATTRS}\\btype=["']hidden["'])${ATTRS}>`, "g"),
|
|
469
|
+
matches: (content) =>
|
|
470
|
+
[
|
|
471
|
+
...content.matchAll(
|
|
472
|
+
new RegExp(`<input\\b(?!${ATTRS}\\btype=["']hidden["'])${ATTRS}>`, "g"),
|
|
473
|
+
),
|
|
474
|
+
].filter((match) => !isAsChildSlot(content, match.index)),
|
|
412
475
|
message: "Use <Input> from @godxjp/ui, not a raw <input> (rules §3).",
|
|
413
476
|
},
|
|
414
477
|
{
|
|
@@ -418,6 +481,7 @@ const RULES = [
|
|
|
418
481
|
severity: "error",
|
|
419
482
|
spansElement: true,
|
|
420
483
|
test: rawTag("button"),
|
|
484
|
+
matches: rawTagOutsideAsChildSlot("button"),
|
|
421
485
|
message: "Use <Button> from @godxjp/ui, not a raw <button> (rules §3).",
|
|
422
486
|
},
|
|
423
487
|
{
|
|
@@ -601,10 +665,19 @@ const RULES = [
|
|
|
601
665
|
/*
|
|
602
666
|
* A lucide element rendered where NOTHING will size it.
|
|
603
667
|
*
|
|
604
|
-
* A lucide component ships `width="24" height="24"`, and
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
668
|
+
* A lucide component ships `width="24" height="24"`, and this library overrides that in
|
|
669
|
+
* **31** selectors (counted 2026-09-19 across `src/styles/*.css`; this comment used to say
|
|
670
|
+
* "exactly four" and name them, which had drifted far enough to cause gh#755). Most are
|
|
671
|
+
* component-internal — a consumer cannot put a glyph inside `.ui-rating-star` — so the number
|
|
672
|
+
* is not the exemption list and never was.
|
|
673
|
+
*
|
|
674
|
+
* What a consumer CAN reach is a SLOT, and that is what `iconSizingRanges` keys on: the slot's
|
|
675
|
+
* NAME at the call site, not the selector that ends up sizing it. That is why the list there
|
|
676
|
+
* is prop names rather than CSS, and why it must cover both spellings a slot takes —
|
|
677
|
+
* `icon={<Users />}` and `icon: <Users />` inside an items array (gh#755).
|
|
678
|
+
*
|
|
679
|
+
* Outside a slot — in a `Text`, a table cell, an `<a>`, a `Flex` — the glyph draws at 24px
|
|
680
|
+
* beside 14px type. It is the one defect class that is
|
|
608
681
|
* INVISIBLE in review: the JSX is correct, the import is correct, and only the screen is
|
|
609
682
|
* wrong. A consumer swept one app and found 38 (gh#712).
|
|
610
683
|
*
|
|
@@ -1051,9 +1124,52 @@ function iconSizingRanges(source) {
|
|
|
1051
1124
|
const close = matchBracket(source, open);
|
|
1052
1125
|
ranges.push([open, close < 0 ? source.length : close + 1]);
|
|
1053
1126
|
}
|
|
1127
|
+
/*
|
|
1128
|
+
* The SAME slots, written as an object property instead of a JSX prop (gh#755).
|
|
1129
|
+
*
|
|
1130
|
+
* <Tabs items={saved.map((s) => ({ icon: s.shared ? <Users /> : undefined }))} />
|
|
1131
|
+
*
|
|
1132
|
+
* `Tabs` wraps `items[].icon` in `<span class="ui-tabs-trigger-icon">`, and
|
|
1133
|
+
* `navigation-layout.css` sizes `.ui-tabs-trigger-icon svg` — so the glyph IS measured, by the
|
|
1134
|
+
* same component that owns the slot. But the prop form above requires `=`, so `icon:` matched
|
|
1135
|
+
* nothing and the call site was told to size a box the design system owns — the opposite of
|
|
1136
|
+
* what CONSUMER-RULES asks for.
|
|
1137
|
+
*
|
|
1138
|
+
* A property value is not bracketed like `={...}`, so the range runs to the `,` or `}` that
|
|
1139
|
+
* closes it at depth zero. Strings and nested brackets are skipped so a comma inside
|
|
1140
|
+
* `style={{a: 1, b: 2}}` or inside a string does not end it early.
|
|
1141
|
+
*/
|
|
1142
|
+
for (const m of source.matchAll(
|
|
1143
|
+
/\b(?:icon|leading|trailing|mark|indicator|avatar|prefix|suffix|addonBefore|addonAfter)\s*:/g,
|
|
1144
|
+
)) {
|
|
1145
|
+
const from = m.index + m[0].length;
|
|
1146
|
+
ranges.push([from, objectPropertyValueEnd(source, from)]);
|
|
1147
|
+
}
|
|
1054
1148
|
return ranges;
|
|
1055
1149
|
}
|
|
1056
1150
|
|
|
1151
|
+
/** Where an object property's value ends: the `,` or `}` closing it at depth zero. */
|
|
1152
|
+
function objectPropertyValueEnd(source, from) {
|
|
1153
|
+
let depth = 0;
|
|
1154
|
+
for (let i = from; i < source.length; i += 1) {
|
|
1155
|
+
const c = source[i];
|
|
1156
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
1157
|
+
// Skip to the closing quote of the same kind, honouring backslash escapes.
|
|
1158
|
+
const quote = c;
|
|
1159
|
+
i += 1;
|
|
1160
|
+
while (i < source.length && source[i] !== quote) i += source[i] === "\\" ? 2 : 1;
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
if (c === "(" || c === "[" || c === "{") depth += 1;
|
|
1164
|
+
else if (c === ")" || c === "]") depth -= 1;
|
|
1165
|
+
else if (c === "}") {
|
|
1166
|
+
if (depth === 0) return i;
|
|
1167
|
+
depth -= 1;
|
|
1168
|
+
} else if (c === "," && depth === 0) return i;
|
|
1169
|
+
}
|
|
1170
|
+
return source.length;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1057
1173
|
/** A lucide element that no rule, no slot and no author-supplied size will ever measure. */
|
|
1058
1174
|
function* lucideGlyphMatches(source) {
|
|
1059
1175
|
const names = lucideLocalNames(source);
|