@godxjp/ui 27.3.1 → 27.4.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/dist/components/general/icon.d.ts +31 -0
- package/dist/components/general/icon.js +23 -0
- package/dist/components/general/index.d.ts +2 -0
- package/dist/components/general/index.js +2 -0
- package/dist/components/layout/flex.d.ts +2 -2
- package/dist/components/layout/flex.js +7 -1
- package/dist/components/layout/topbar-item.d.ts +1 -0
- package/dist/components/layout/topbar-item.js +3 -1
- package/dist/contracts/measurement.json +1 -1
- package/dist/props/components/general.prop.d.ts +42 -1
- package/dist/props/components/layout.prop.d.ts +42 -0
- package/dist/props/registry.d.ts +31 -0
- package/dist/props/registry.js +37 -0
- package/dist/props/vocabulary/index.d.ts +1 -1
- package/dist/props/vocabulary/interaction.prop.d.ts +11 -0
- package/dist/styles/core.css +2 -0
- package/dist/styles/icon-layout.css +37 -0
- package/dist/styles/index.css +2 -0
- package/dist/styles/shell-layout.css +10 -0
- package/dist/styles/text-layout.css +16 -8
- package/docs/CONSUMER-RULES.md +2 -2
- package/docs/FRAME-COVERAGE-REPORT.md +3 -2
- package/docs/general/icon.tsx +215 -0
- package/docs/layout/flex.tsx +59 -0
- package/docs/layout/topbar-item.tsx +29 -1
- package/package.json +2 -2
- package/scripts/consumer-rule.md +18 -17
- package/scripts/ui-audit.mjs +128 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type { IconGlyphProp, IconGlyphProp as IconProps, } from "../../props/components/general.prop.js";
|
|
3
|
+
/**
|
|
4
|
+
* Icon — a glyph on the `--icon-size-*` scale, and the only supported way to draw a standalone one.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS (gh#712). A lucide component ships `width="24" height="24"`, and exactly four
|
|
7
|
+
* rules in this library ever override that: `.ui-button svg`, `.ui-dropdown-menu-item > svg`,
|
|
8
|
+
* `.ui-topbar-item > svg` and `[data-slot="list-row-leading"] > svg`. Anywhere else — in a `Text`,
|
|
9
|
+
* in a table cell, in an `<a>` — the glyph draws at 24px beside 14px type, 1.71× the text it
|
|
10
|
+
* annotates. A consumer cannot fix that themselves: `size-4` and `w-[16px]` are what
|
|
11
|
+
* docs/CONSUMER-RULES.md §3/§8 forbid, and `size={16}` hard-codes a number the theme owns. One
|
|
12
|
+
* consumer measured 38 such glyphs in a single app.
|
|
13
|
+
*
|
|
14
|
+
* RENDERS ONTO THE GLYPH, NOT AROUND IT. `as={Lock}` is invoked with the class, the size and the
|
|
15
|
+
* a11y wiring, so the element that carries the metric IS the `<svg>`. A wrapper `<span>` would add
|
|
16
|
+
* a box to every flex row that holds an icon, and would break `Button`'s own `svg` rule by putting
|
|
17
|
+
* an element between the two.
|
|
18
|
+
*
|
|
19
|
+
* DECORATIVE BY DEFAULT. Without `label` the glyph is `aria-hidden` — a glyph beside a visible
|
|
20
|
+
* label is decoration, and announcing it twice is the usual defect (WCAG 2.2 SC 1.1.1). Pass
|
|
21
|
+
* `label` only when the glyph is the ONLY thing saying what it says; it then renders as
|
|
22
|
+
* `role="img"` + that name, which is how a graphic gets a name in WAI-ARIA 1.2. The string is the
|
|
23
|
+
* consumer's to localize — the library ships no copy for it.
|
|
24
|
+
*/
|
|
25
|
+
export declare const Icon: React.ForwardRefExoticComponent<Omit<React.SVGProps<SVGSVGElement>, "children" | "ref"> & {
|
|
26
|
+
as: React.ComponentType<React.SVGProps<SVGSVGElement> & React.RefAttributes<SVGSVGElement>>;
|
|
27
|
+
size?: import("../../props/index.js").IconSizeProp;
|
|
28
|
+
tone?: import("../../props/index.js").TextToneProp;
|
|
29
|
+
label?: string;
|
|
30
|
+
className?: import("../../props/index.js").ClassNameProp;
|
|
31
|
+
} & React.RefAttributes<SVGSVGElement>>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
const Icon = React.forwardRef(function Icon2({ as: Glyph, size = "md", tone, label, className, ...props }, ref) {
|
|
5
|
+
const named = label !== void 0 && label !== "";
|
|
6
|
+
return /* @__PURE__ */ jsx(
|
|
7
|
+
Glyph,
|
|
8
|
+
{
|
|
9
|
+
ref,
|
|
10
|
+
"data-slot": "icon",
|
|
11
|
+
"data-size": size,
|
|
12
|
+
"data-tone": tone,
|
|
13
|
+
className: cn("ui-icon", className),
|
|
14
|
+
role: named ? "img" : void 0,
|
|
15
|
+
"aria-label": named ? label : void 0,
|
|
16
|
+
"aria-hidden": named ? void 0 : true,
|
|
17
|
+
...props
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
export {
|
|
22
|
+
Icon
|
|
23
|
+
};
|
|
@@ -2,6 +2,8 @@ export { Button, buttonVariants } from "./button.js";
|
|
|
2
2
|
export type { ButtonProps } from "./button.js";
|
|
3
3
|
export { Text, Heading, Typography, Title, Paragraph, Link } from "./typography.js";
|
|
4
4
|
export type { TextProps, HeadingProps, TextProp, HeadingProp, TypographyProp, TypographyProps, TypographyBlockProp, TypographyTitleProp, TitleProps, ParagraphProp, ParagraphProps, LinkProp, LinkProps, } from "./typography.js";
|
|
5
|
+
export { Icon } from "./icon.js";
|
|
6
|
+
export type { IconGlyphProp, IconProps } from "./icon.js";
|
|
5
7
|
export { Logo } from "./logo.js";
|
|
6
8
|
export type { LogoMark, LogoProps, LogoSize, LogoTone } from "./logo.js";
|
|
7
9
|
export { Reveal } from "./reveal.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Button, buttonVariants } from "./button.js";
|
|
2
2
|
import { Text, Heading, Typography, Title, Paragraph, Link } from "./typography.js";
|
|
3
|
+
import { Icon } from "./icon.js";
|
|
3
4
|
import { Logo } from "./logo.js";
|
|
4
5
|
import { Reveal } from "./reveal.js";
|
|
5
6
|
import { FloatButton } from "./float-button.js";
|
|
@@ -15,6 +16,7 @@ export {
|
|
|
15
16
|
Button,
|
|
16
17
|
FloatButton,
|
|
17
18
|
Heading,
|
|
19
|
+
Icon,
|
|
18
20
|
Link,
|
|
19
21
|
Logo,
|
|
20
22
|
Paragraph,
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { FlexProp } from "../../props/components/layout.prop.js";
|
|
2
|
-
export type { FlexAlignProp, FlexDirectionProp, FlexJustifyProp, FlexProp, FlexProp as FlexProps, } from "../../props/components/layout.prop.js";
|
|
3
|
-
export declare function Flex({ as: Element, direction, grow, shrink, surface, bleed, reveal, gap, gapRaw, pad, padRaw, align, justify, wrap, hideBelow, hideFrom, hideBelowRaw, hideFromRaw, fill, width, className, style, children, ...props }: FlexProp): import("react").JSX.Element;
|
|
2
|
+
export type { FlexAlignProp, FlexDirectionProp, FlexJustifyProp, FlexMarkerProp, FlexProp, FlexProp as FlexProps, } from "../../props/components/layout.prop.js";
|
|
3
|
+
export declare function Flex({ as: Element, direction, marker, grow, shrink, surface, bleed, reveal, gap, gapRaw, pad, padRaw, align, justify, wrap, hideBelow, hideFrom, hideBelowRaw, hideFromRaw, fill, width, className, style, children, ...props }: FlexProp): import("react").JSX.Element;
|
|
@@ -6,9 +6,15 @@ import { flexGapClass, padStyle, padStepToken } from "../../lib/variants.js";
|
|
|
6
6
|
const toCssLength = (value) => typeof value === "number" ? `${value}px` : value;
|
|
7
7
|
const rawBreakpoint = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
8
8
|
const rawBreakpointRule = (axis, px) => `@media (width ${axis === "below" ? "<" : ">="} ${px}px){.ui-flex[data-hide-${axis}-raw="${px}"]{display:none}}`;
|
|
9
|
+
const listMarker = (element, marker) => {
|
|
10
|
+
if (element !== "ul" && element !== "ol") return void 0;
|
|
11
|
+
const resolved = marker ?? (element === "ul" ? "disc" : "decimal");
|
|
12
|
+
return resolved === "none" ? void 0 : resolved;
|
|
13
|
+
};
|
|
9
14
|
function Flex({
|
|
10
15
|
as: Element = "div",
|
|
11
16
|
direction = "row",
|
|
17
|
+
marker,
|
|
12
18
|
grow,
|
|
13
19
|
shrink,
|
|
14
20
|
surface,
|
|
@@ -65,7 +71,7 @@ function Flex({
|
|
|
65
71
|
"data-shrink": shrink === false ? "false" : void 0,
|
|
66
72
|
"data-surface": surface,
|
|
67
73
|
"data-reveal": reveal,
|
|
68
|
-
"data-list": Element
|
|
74
|
+
"data-list": listMarker(Element, marker),
|
|
69
75
|
"data-align": align,
|
|
70
76
|
"data-justify": justify,
|
|
71
77
|
"data-wrap": wrap ? "true" : void 0,
|
|
@@ -12,6 +12,7 @@ export type { TopbarItemProp, TopbarItemProp as TopbarItemProps, } from "../../p
|
|
|
12
12
|
*/
|
|
13
13
|
export declare const TopbarItem: React.ForwardRefExoticComponent<Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> & {
|
|
14
14
|
hideBelow?: import("../../props/index.js").BreakpointProp;
|
|
15
|
+
icon?: React.ReactNode;
|
|
15
16
|
badge?: React.ReactNode;
|
|
16
17
|
badgeTone?: import("../../props/components/layout.prop.js").SidebarBadgeToneProp;
|
|
17
18
|
asChild?: boolean;
|
|
@@ -2,9 +2,10 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { Slot } from "../../lib/slot.js";
|
|
4
4
|
import { cn } from "../../lib/utils.js";
|
|
5
|
-
const TopbarItem = React.forwardRef(function TopbarItem2({ asChild = false, className, type, hideBelow, badge, badgeTone, children, ...props }, ref) {
|
|
5
|
+
const TopbarItem = React.forwardRef(function TopbarItem2({ asChild = false, className, type, hideBelow, icon, badge, badgeTone, children, ...props }, ref) {
|
|
6
6
|
const Comp = asChild ? Slot : "button";
|
|
7
7
|
const showBadge = !asChild && badge !== void 0 && badge !== "";
|
|
8
|
+
const iconNode = !asChild && icon !== void 0 && icon !== null && icon !== false ? /* @__PURE__ */ jsx("span", { "data-slot": "topbar-item-icon", className: "ui-topbar-item-icon", children: icon }) : null;
|
|
8
9
|
const badgeNode = showBadge ? /* @__PURE__ */ jsx(
|
|
9
10
|
"span",
|
|
10
11
|
{
|
|
@@ -24,6 +25,7 @@ const TopbarItem = React.forwardRef(function TopbarItem2({ asChild = false, clas
|
|
|
24
25
|
type: asChild ? void 0 : type ?? "button",
|
|
25
26
|
...props,
|
|
26
27
|
children: asChild ? children : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
28
|
+
iconNode,
|
|
27
29
|
children,
|
|
28
30
|
badgeNode
|
|
29
31
|
] })
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
|
|
3
|
-
"version": "27.
|
|
3
|
+
"version": "27.4.1",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Foundation component prop types — @see docs/COMPONENTS.md#foundation */
|
|
2
2
|
import type * as React from "react";
|
|
3
|
-
import type { ActivityAnnounceProp, ActivityVariantProp, AsChildProp, ButtonSizeProp, ButtonVariantProp, ChildrenProp, ClassNameProp, DescriptionProp, DisabledProp, FontWeightProp, HeadingLevelProp, IdProp, LabelProp, OnClickProp, OnOpenChangeProp, OpenProp, PendingProp, RevealDelayProp, ShapeProp, SizeProp, TextAlignProp, TextSizeProp, TextToneProp, TextWhitespaceProp, TitleLevelProp, TypographyActionsConfigProp, TypographyCopyConfigProp, TypographyEditConfigProp, TypographyEllipsisConfigProp, TypographyTypeProp } from "../vocabulary/index.js";
|
|
3
|
+
import type { ActivityAnnounceProp, ActivityVariantProp, AsChildProp, ButtonSizeProp, ButtonVariantProp, ChildrenProp, ClassNameProp, DescriptionProp, DisabledProp, FontWeightProp, HeadingLevelProp, IconSizeProp, IdProp, LabelProp, OnClickProp, OnOpenChangeProp, OpenProp, PendingProp, RevealDelayProp, ShapeProp, SizeProp, TextAlignProp, TextSizeProp, TextToneProp, TextWhitespaceProp, TitleLevelProp, TypographyActionsConfigProp, TypographyCopyConfigProp, TypographyEditConfigProp, TypographyEllipsisConfigProp, TypographyTypeProp } from "../vocabulary/index.js";
|
|
4
4
|
/**
|
|
5
5
|
* @see Text — typographic primitive; replaces hand-rolled `<span className="text-[13px] …">`.
|
|
6
6
|
*
|
|
@@ -336,6 +336,47 @@ export type ActivityProp = Omit<React.HTMLAttributes<HTMLSpanElement>, "color">
|
|
|
336
336
|
announce?: ActivityAnnounceProp;
|
|
337
337
|
className?: ClassNameProp;
|
|
338
338
|
};
|
|
339
|
+
/**
|
|
340
|
+
* @see Icon — the ONE supported way to put a standalone glyph on the `--icon-size-*` scale.
|
|
341
|
+
*
|
|
342
|
+
* A lucide component carries `width="24" height="24"` of its own, and only four rules in this
|
|
343
|
+
* library ever re-size a glyph (`.ui-button svg`, the menu row, the topbar cell, the ListRow
|
|
344
|
+
* leading slot) — three of them direct-child rules. Outside those four a glyph renders at 24px
|
|
345
|
+
* beside 14px text, and a consumer has no supported way to fix it: `docs/CONSUMER-RULES.md` §8
|
|
346
|
+
* forbids `size-4`/`w-[16px]`, and `size={16}` re-derives a scale the theme owns. Measured in a
|
|
347
|
+
* consumer at 38 such glyphs (gh#712).
|
|
348
|
+
*
|
|
349
|
+
* Icon renders ONTO the glyph rather than wrapping it — the sized element IS the `<svg>` — so it
|
|
350
|
+
* composes anywhere a bare glyph does (inside `Text`, in a `Button`, in a table cell, in an `<a>`)
|
|
351
|
+
* and adds no box to the layout.
|
|
352
|
+
*/
|
|
353
|
+
export type IconGlyphProp = Omit<React.SVGProps<SVGSVGElement>, "ref" | "children"> & {
|
|
354
|
+
/**
|
|
355
|
+
* The glyph COMPONENT — `as={Lock}`, not `<Lock />`. Any component that forwards SVG props and a
|
|
356
|
+
* ref qualifies (every `lucide-react` icon does); Icon hands it the class, the size attributes
|
|
357
|
+
* and the a11y wiring.
|
|
358
|
+
*/
|
|
359
|
+
as: React.ComponentType<React.SVGProps<SVGSVGElement> & React.RefAttributes<SVGSVGElement>>;
|
|
360
|
+
/**
|
|
361
|
+
* Step of the nine-step icon scale. Default `md` (16px — "the default icon step" in
|
|
362
|
+
* docs/TOKENS.md). A glyph beside `Text size="sm"` wants `sm`; an empty-state mark wants `4xl`.
|
|
363
|
+
*/
|
|
364
|
+
size?: IconSizeProp;
|
|
365
|
+
/**
|
|
366
|
+
* Semantic colour intent, the same vocabulary `Text` uses. Omitted by default, and that default
|
|
367
|
+
* is load-bearing: a glyph inherits `currentColor`, so an Icon inside a Button, a Badge or a
|
|
368
|
+
* toned `Text` paints in that surface's own ink without being told.
|
|
369
|
+
*/
|
|
370
|
+
tone?: TextToneProp;
|
|
371
|
+
/**
|
|
372
|
+
* Accessible NAME, when the glyph carries meaning nothing else on screen says (a lock in a
|
|
373
|
+
* status cell with no text beside it). Localized by the consumer — the library ships no copy.
|
|
374
|
+
* Supplying it switches the glyph from `aria-hidden` to `role="img"` + that name; leave it off
|
|
375
|
+
* (the default) for a decorative glyph beside a visible label, which is the common case.
|
|
376
|
+
*/
|
|
377
|
+
label?: string;
|
|
378
|
+
className?: ClassNameProp;
|
|
379
|
+
};
|
|
339
380
|
/**
|
|
340
381
|
* FloatButton `type` — the two fills Ant Design gives the corner mark.
|
|
341
382
|
*
|
|
@@ -100,6 +100,11 @@ export type PageContainerProp = {
|
|
|
100
100
|
className?: ClassNameProp;
|
|
101
101
|
};
|
|
102
102
|
export type FlexDirectionProp = "row" | "col";
|
|
103
|
+
/**
|
|
104
|
+
* List marker for `as="ul"`/`as="ol"` — the CSS `list-style-type` keywords the layer implements,
|
|
105
|
+
* plus `none` for a list that is SEMANTIC ONLY (gh#714).
|
|
106
|
+
*/
|
|
107
|
+
export type FlexMarkerProp = "disc" | "decimal" | "none";
|
|
103
108
|
export type FlexAlignProp = "start" | "center" | "end" | "stretch" | "baseline";
|
|
104
109
|
export type FlexJustifyProp = "start" | "center" | "end" | "between" | "around" | "evenly";
|
|
105
110
|
/** @see Flex */
|
|
@@ -112,6 +117,26 @@ export type FlexProp = React.HTMLAttributes<HTMLElement> & {
|
|
|
112
117
|
* `.ui-flex` rules carry `display: flex`, so the box is identical either way.
|
|
113
118
|
*/
|
|
114
119
|
as?: "div" | "span" | "ul" | "ol" | "li";
|
|
120
|
+
/**
|
|
121
|
+
* Marker for a LIST element (`as="ul"` → `disc`, `as="ol"` → `decimal` by default); ignored by
|
|
122
|
+
* every other tag.
|
|
123
|
+
*
|
|
124
|
+
* `marker="none"` emits no `data-list` at all, so the list keeps its element, its `<li>`
|
|
125
|
+
* semantics and its `gap`, and loses the bullet AND the `--space-5` indent that come with it.
|
|
126
|
+
*
|
|
127
|
+
* ## Why the axis exists
|
|
128
|
+
*
|
|
129
|
+
* The bullet was unconditional, and the catalog's own idiom — `<ListRow as="li">` inside a list
|
|
130
|
+
* — could not live with it: `.ui-flex[data-list] > li { display: list-item }` outranks
|
|
131
|
+
* `[data-slot="list-row"] { display: flex }`, so every row fell out of flex layout (measured on
|
|
132
|
+
* this page: row height 94.58px → 41.2px once the attribute is gone). The only move left was a
|
|
133
|
+
* RAW `<ul>`, which carries no gap token and which the consumer rules forbid in spirit — one
|
|
134
|
+
* consumer kept three `Flex role="list"` divs for exactly that reason (gh#714).
|
|
135
|
+
*
|
|
136
|
+
* `disc`/`decimal` stay available so an `<ol>` can be bulleted (or a `<ul>` numbered) without a
|
|
137
|
+
* `list-style-type` in a className.
|
|
138
|
+
*/
|
|
139
|
+
marker?: FlexMarkerProp;
|
|
115
140
|
/** Lightweight surfaces for rows and notices; no Card elevation by default. */
|
|
116
141
|
surface?: "muted" | "popover" | "warning";
|
|
117
142
|
/** Negative inline inset, using the same spacing scale as pad. */
|
|
@@ -1314,6 +1339,23 @@ export type TopbarItemProp = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
|
1314
1339
|
*/
|
|
1315
1340
|
/** Hide below a shared responsive breakpoint, without changing cell height. */
|
|
1316
1341
|
hideBelow?: BreakpointProp;
|
|
1342
|
+
/**
|
|
1343
|
+
* The cell's leading glyph, placed in a slot the CELL sizes (`--topbar-icon-size`).
|
|
1344
|
+
*
|
|
1345
|
+
* Passing the glyph as a child works too — but only while it stays a DIRECT child, because
|
|
1346
|
+
* `.ui-topbar-item > svg` is the rule that sizes it. The moment a cell wraps its glyph (a
|
|
1347
|
+
* `<Flex hideBelow>` so the icon drops at narrow widths) the rule can no longer reach it and the
|
|
1348
|
+
* glyph renders at lucide's intrinsic 24px: measured at 390px on an org-switcher cell, 1.5x the
|
|
1349
|
+
* cell's own step (gh#712). This slot survives that, because the wrapper is the one the cell
|
|
1350
|
+
* placed. The alternative — widening the rule to a descendant — was measured to shrink or grow
|
|
1351
|
+
* OTHER components' glyphs inside the cell (a Badge's 12px glyph became 16px), so it is not one.
|
|
1352
|
+
*
|
|
1353
|
+
* Decorative by convention: the cell's accessible name comes from its own `aria-label` or its
|
|
1354
|
+
* visible label, not from the glyph. For a glyph that must carry a name, or one that needs a
|
|
1355
|
+
* step other than the bar's, pass `<Icon as={…} size=… label=… />` — as this prop's value or as
|
|
1356
|
+
* a child — and it keeps its own metric and its own semantics.
|
|
1357
|
+
*/
|
|
1358
|
+
icon?: ReactNode;
|
|
1317
1359
|
/**
|
|
1318
1360
|
* Unread count OVERLAID on the cell's glyph — the notification-bell affordance the cell's own use
|
|
1319
1361
|
* cases name (gh#398). Pass the CONTENT ONLY, exactly like `SidebarItemProp.badge`: a number, a
|
package/dist/props/registry.d.ts
CHANGED
|
@@ -434,6 +434,11 @@ export declare const VOCABULARY_REGISTRY: {
|
|
|
434
434
|
readonly category: "interaction";
|
|
435
435
|
readonly description: "Text size — golden-ratio type-scale steps (2xs…2xl), never an arbitrary px";
|
|
436
436
|
};
|
|
437
|
+
readonly IconSizeProp: {
|
|
438
|
+
readonly file: "vocabulary/interaction.prop.ts";
|
|
439
|
+
readonly category: "interaction";
|
|
440
|
+
readonly description: "Icon size — the nine --icon-size-* scale steps (2xs…4xl), never an arbitrary px or a size- utility";
|
|
441
|
+
};
|
|
437
442
|
readonly TextToneProp: {
|
|
438
443
|
readonly file: "vocabulary/interaction.prop.ts";
|
|
439
444
|
readonly category: "interaction";
|
|
@@ -746,6 +751,11 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
746
751
|
readonly file: "components/layout.prop.ts";
|
|
747
752
|
readonly vocabulary: readonly [];
|
|
748
753
|
};
|
|
754
|
+
readonly FlexMarkerProp: {
|
|
755
|
+
readonly group: "layout";
|
|
756
|
+
readonly file: "components/layout.prop.ts";
|
|
757
|
+
readonly vocabulary: readonly [];
|
|
758
|
+
};
|
|
749
759
|
readonly FlexAlignProp: {
|
|
750
760
|
readonly group: "layout";
|
|
751
761
|
readonly file: "components/layout.prop.ts";
|
|
@@ -767,6 +777,10 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
767
777
|
readonly field: "direction";
|
|
768
778
|
readonly local: true;
|
|
769
779
|
readonly reason: "Flex-specific axis control.";
|
|
780
|
+
}, {
|
|
781
|
+
readonly field: "marker";
|
|
782
|
+
readonly local: true;
|
|
783
|
+
readonly reason: 'List-marker seam for as="ul"/"ol" (disc | decimal | none). marker="none" emits no data-list, so a list keeps its element, its <li> semantics and its gap token while losing the bullet and the --space-5 indent — the home <ListRow as="li"> had none of, which forced a raw <ul> (gh#714).';
|
|
770
784
|
}, "GapProp", {
|
|
771
785
|
readonly field: "align";
|
|
772
786
|
readonly local: true;
|
|
@@ -1203,6 +1217,23 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
1203
1217
|
readonly reason: "Ambient live-region opt-in — ActivityAnnounceProp vocabulary; deliberately NOT a boolean so the value names the politeness level.";
|
|
1204
1218
|
}];
|
|
1205
1219
|
};
|
|
1220
|
+
readonly IconGlyphProp: {
|
|
1221
|
+
readonly group: "general";
|
|
1222
|
+
readonly file: "components/general.prop.ts";
|
|
1223
|
+
readonly vocabulary: readonly ["IconProp", "IconSizeProp", "TextToneProp", "LabelProp", "ClassNameProp", {
|
|
1224
|
+
readonly field: "label";
|
|
1225
|
+
readonly local: true;
|
|
1226
|
+
readonly reason: "LabelProp narrowed to string: this becomes the glyph's aria-label, and an accessible name is TEXT — a ReactNode cannot be one.";
|
|
1227
|
+
}, {
|
|
1228
|
+
readonly field: "as";
|
|
1229
|
+
readonly local: true;
|
|
1230
|
+
readonly reason: "The glyph COMPONENT to render — the IconProp concept, widened to a component that also forwards SVG props and a ref, because Icon renders ONTO the glyph (the sized element is the <svg> itself) rather than wrapping it.";
|
|
1231
|
+
}, {
|
|
1232
|
+
readonly field: "size";
|
|
1233
|
+
readonly local: true;
|
|
1234
|
+
readonly reason: "IconSizeProp, the nine-step icon scale — deliberately NOT the four-step control SizeProp: a glyph beside 2xs caption text and a 48px empty-state mark are the same primitive.";
|
|
1235
|
+
}];
|
|
1236
|
+
};
|
|
1206
1237
|
readonly FloatButtonTypeProp: {
|
|
1207
1238
|
readonly group: "general";
|
|
1208
1239
|
readonly file: "components/general.prop.ts";
|
package/dist/props/registry.js
CHANGED
|
@@ -433,6 +433,11 @@ const VOCABULARY_REGISTRY = {
|
|
|
433
433
|
category: "interaction",
|
|
434
434
|
description: "Text size \u2014 golden-ratio type-scale steps (2xs\u20262xl), never an arbitrary px"
|
|
435
435
|
},
|
|
436
|
+
IconSizeProp: {
|
|
437
|
+
file: "vocabulary/interaction.prop.ts",
|
|
438
|
+
category: "interaction",
|
|
439
|
+
description: "Icon size \u2014 the nine --icon-size-* scale steps (2xs\u20264xl), never an arbitrary px or a size- utility"
|
|
440
|
+
},
|
|
436
441
|
TextToneProp: {
|
|
437
442
|
file: "vocabulary/interaction.prop.ts",
|
|
438
443
|
category: "interaction",
|
|
@@ -767,6 +772,7 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
767
772
|
]
|
|
768
773
|
},
|
|
769
774
|
FlexDirectionProp: { group: "layout", file: "components/layout.prop.ts", vocabulary: [] },
|
|
775
|
+
FlexMarkerProp: { group: "layout", file: "components/layout.prop.ts", vocabulary: [] },
|
|
770
776
|
FlexAlignProp: { group: "layout", file: "components/layout.prop.ts", vocabulary: [] },
|
|
771
777
|
FlexJustifyProp: { group: "layout", file: "components/layout.prop.ts", vocabulary: [] },
|
|
772
778
|
FlexProp: {
|
|
@@ -779,6 +785,11 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
779
785
|
reason: "Closed tag seam (div | span) so a Flex stays valid HTML inside a phrasing-only parent such as the <button> a TabsTrigger renders \u2014 same shape as ListRow's `as` (gh#354)."
|
|
780
786
|
},
|
|
781
787
|
{ field: "direction", local: true, reason: "Flex-specific axis control." },
|
|
788
|
+
{
|
|
789
|
+
field: "marker",
|
|
790
|
+
local: true,
|
|
791
|
+
reason: 'List-marker seam for as="ul"/"ol" (disc | decimal | none). marker="none" emits no data-list, so a list keeps its element, its <li> semantics and its gap token while losing the bullet and the --space-5 indent \u2014 the home <ListRow as="li"> had none of, which forced a raw <ul> (gh#714).'
|
|
792
|
+
},
|
|
782
793
|
"GapProp",
|
|
783
794
|
{ field: "align", local: true, reason: "Flex-specific align-items keyword subset." },
|
|
784
795
|
{ field: "justify", local: true, reason: "Flex-specific justify-content keyword subset." },
|
|
@@ -1328,6 +1339,32 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
1328
1339
|
}
|
|
1329
1340
|
]
|
|
1330
1341
|
},
|
|
1342
|
+
IconGlyphProp: {
|
|
1343
|
+
group: "general",
|
|
1344
|
+
file: "components/general.prop.ts",
|
|
1345
|
+
vocabulary: [
|
|
1346
|
+
"IconProp",
|
|
1347
|
+
"IconSizeProp",
|
|
1348
|
+
"TextToneProp",
|
|
1349
|
+
"LabelProp",
|
|
1350
|
+
"ClassNameProp",
|
|
1351
|
+
{
|
|
1352
|
+
field: "label",
|
|
1353
|
+
local: true,
|
|
1354
|
+
reason: "LabelProp narrowed to string: this becomes the glyph's aria-label, and an accessible name is TEXT \u2014 a ReactNode cannot be one."
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
field: "as",
|
|
1358
|
+
local: true,
|
|
1359
|
+
reason: "The glyph COMPONENT to render \u2014 the IconProp concept, widened to a component that also forwards SVG props and a ref, because Icon renders ONTO the glyph (the sized element is the <svg> itself) rather than wrapping it."
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
field: "size",
|
|
1363
|
+
local: true,
|
|
1364
|
+
reason: "IconSizeProp, the nine-step icon scale \u2014 deliberately NOT the four-step control SizeProp: a glyph beside 2xs caption text and a 48px empty-state mark are the same primitive."
|
|
1365
|
+
}
|
|
1366
|
+
]
|
|
1367
|
+
},
|
|
1331
1368
|
FloatButtonTypeProp: {
|
|
1332
1369
|
group: "general",
|
|
1333
1370
|
file: "components/general.prop.ts",
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
export type { ClassNameProp, ChildrenProp, IdProp, OpenProp, DefaultOpenProp, OnOpenChangeProp, HandlerProp, PendingProp, RequiredProp, DisabledProp, LabelProp, HelperProp, ErrorProp, ErrorBagProp, PlaceholderProp, NameProp, ValueProp, DefaultValueProp, OnValueChangeProp, OnChangeProp, OnClickProp, AsChildProp, FlushProp, WidthProp, ControlWidthProp, AllowClearProp, MaxTagCountProp, MaxTagPlaceholderProp, NotFoundContentProp, PopupMatchWidthProp, } from "./shared.prop.js";
|
|
3
3
|
export type { TitleProp, SubtitleProp, StatusProp, DescriptionProp, ExtraProp, FooterProp, ActionProp, IconProp, ConfirmLabelProp, CancelLabelProp, ActionsProp, EmptyMessageProp, } from "./content.prop.js";
|
|
4
4
|
export type { PageDensityProp, PageContainerVariantProp, CenteredShellWidthProp, CenteredShellAlignProp, CenteredShellPresetProp, ErrorSurfaceModeProp, ErrorSurfaceStatusProp, AuthShellPresetProp, MobileShellHeightProp, MobileShellWidthProp, OrientationProp, TableDensityProp, DensityProp, GapNameProp, PadProp, PadRawProp, PadSides, GapProp, GapStepProp, } from "./layout.prop.js";
|
|
5
|
-
export type { ButtonVariantProp, ButtonSizeProp, BadgeVariantProp, AppSettingPickerAppearanceProp, AppSettingToggleAppearanceProp, ShapeProp, AvatarShapeProp, TextSizeProp, TextToneProp, FontWeightProp, HeadingLevelProp, TextAlignProp, TextWhitespaceProp, TypographyTypeProp, TitleLevelProp, TypographyCopyConfigProp, TypographyEditConfigProp, TypographyEllipsisConfigProp, TypographyActionsConfigProp, SizeProp, ControlStatusProp, ControlVariantProp, FormLayoutProp, DescriptionsLayoutProp, BreakpointProp, ConfirmVariantProp, ToneProp, AlertVariantProp, SortDirectionProp, ColumnAlignProp, SortStateProp, RevealDelayProp, ActivityVariantProp, ActivityAnnounceProp, } from "./interaction.prop.js";
|
|
5
|
+
export type { ButtonVariantProp, ButtonSizeProp, BadgeVariantProp, AppSettingPickerAppearanceProp, AppSettingToggleAppearanceProp, ShapeProp, AvatarShapeProp, TextSizeProp, TextToneProp, FontWeightProp, HeadingLevelProp, TextAlignProp, TextWhitespaceProp, TypographyTypeProp, TitleLevelProp, TypographyCopyConfigProp, TypographyEditConfigProp, TypographyEllipsisConfigProp, TypographyActionsConfigProp, SizeProp, ControlStatusProp, ControlVariantProp, FormLayoutProp, DescriptionsLayoutProp, BreakpointProp, ConfirmVariantProp, ToneProp, AlertVariantProp, SortDirectionProp, ColumnAlignProp, SortStateProp, RevealDelayProp, ActivityVariantProp, ActivityAnnounceProp, IconSizeProp, } from "./interaction.prop.js";
|
|
6
6
|
export type { BreadcrumbItemProp, BreadcrumbProp, BreadcrumbItemMenuProp, BreadcrumbItemMenuEntryProp, BreadcrumbSeparatorProp, BreadcrumbItemRenderProp, } from "./navigation.prop.js";
|
|
7
7
|
export type { GetRowIdProp, GetRowLabelProp, OnRowClickProp, ColumnDefProp, TablePresetProp, TableColumnPriorityProp, TableCellIndentProp, SelectedIdsProp, OnSelectChangeProp, OnTableDensityChangeProp, TableRowToneProp, RowToneProp, OnSortChangeProp, OnSearchChangeProp, OnClearFiltersProp, HasActiveFiltersProp, StickyProp, ColumnFixedProp, ColumnFilterValueProp, ColumnFilterItemProp, ColumnFilterStateProp, OnColumnFilterChangeProp, ColumnCompareProp, ColumnSorterProp, TableSelectionItemProp, TableRowSelectionProp, TableExpandableProp, TableSummaryProp, TableScrollProp, TableStickyProp, OnRowProp, TablePaginationProp, TablePaginationPositionProp, DescriptionsColumnProp, DescriptionsSpanProp, DescriptionsItemsProp, } from "./data.prop.js";
|
|
@@ -31,6 +31,17 @@ export type ShapeProp = "default" | "pill" | "sharp";
|
|
|
31
31
|
export type AvatarShapeProp = "circle" | "square";
|
|
32
32
|
/** Text size — steps of the golden-ratio type scale (NEVER an arbitrary px). `sm` = base. */
|
|
33
33
|
export type TextSizeProp = "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
|
34
|
+
/**
|
|
35
|
+
* Icon size — the NINE steps of the `--icon-size-*` scale (docs/TOKENS.md · gh#326), not the four
|
|
36
|
+
* of the control `SizeProp`.
|
|
37
|
+
*
|
|
38
|
+
* The icon axis already had a vocabulary before it had a name, and it is a FIXED LIST rather than
|
|
39
|
+
* a ratio: 14/16 = 0.875 but 20/16 = 1.25, because a 1px-stroke glyph cannot sit on half a pixel.
|
|
40
|
+
* Naming the whole list here — as `TextSizeProp` does for type — is what lets a consumer put a
|
|
41
|
+
* standalone glyph on the scale (`<Icon as={Lock} size="sm" />`) instead of reaching for a
|
|
42
|
+
* `className="size-4"` the consumer rules forbid.
|
|
43
|
+
*/
|
|
44
|
+
export type IconSizeProp = "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl";
|
|
34
45
|
/** Text colour intent — maps to semantic foreground tokens (no raw palette). */
|
|
35
46
|
/**
|
|
36
47
|
* `inherit` is the one that is NOT a colour: it takes whatever the surface already set (gh#643).
|
package/dist/styles/core.css
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
|
|
2
|
+
@layer components {
|
|
3
|
+
svg.ui-icon[data-slot="icon"] {
|
|
4
|
+
inline-size: var(--icon-glyph-size);
|
|
5
|
+
block-size: var(--icon-glyph-size);
|
|
6
|
+
|
|
7
|
+
flex: 0 0 auto;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.ui-icon[data-size="2xs"] {
|
|
11
|
+
--icon-glyph-size: var(--icon-size-2xs);
|
|
12
|
+
}
|
|
13
|
+
.ui-icon[data-size="xs"] {
|
|
14
|
+
--icon-glyph-size: var(--icon-size-xs);
|
|
15
|
+
}
|
|
16
|
+
.ui-icon[data-size="sm"] {
|
|
17
|
+
--icon-glyph-size: var(--icon-size-sm);
|
|
18
|
+
}
|
|
19
|
+
.ui-icon[data-size="md"] {
|
|
20
|
+
--icon-glyph-size: var(--icon-size-md);
|
|
21
|
+
}
|
|
22
|
+
.ui-icon[data-size="lg"] {
|
|
23
|
+
--icon-glyph-size: var(--icon-size-lg);
|
|
24
|
+
}
|
|
25
|
+
.ui-icon[data-size="xl"] {
|
|
26
|
+
--icon-glyph-size: var(--icon-size-xl);
|
|
27
|
+
}
|
|
28
|
+
.ui-icon[data-size="2xl"] {
|
|
29
|
+
--icon-glyph-size: var(--icon-size-2xl);
|
|
30
|
+
}
|
|
31
|
+
.ui-icon[data-size="3xl"] {
|
|
32
|
+
--icon-glyph-size: var(--icon-size-3xl);
|
|
33
|
+
}
|
|
34
|
+
.ui-icon[data-size="4xl"] {
|
|
35
|
+
--icon-glyph-size: var(--icon-size-4xl);
|
|
36
|
+
}
|
|
37
|
+
}
|
package/dist/styles/index.css
CHANGED
|
@@ -2083,6 +2083,16 @@
|
|
|
2083
2083
|
flex: 0 0 auto;
|
|
2084
2084
|
}
|
|
2085
2085
|
|
|
2086
|
+
.ui-topbar-item-icon {
|
|
2087
|
+
display: inline-flex;
|
|
2088
|
+
flex: 0 0 auto;
|
|
2089
|
+
align-items: center;
|
|
2090
|
+
}
|
|
2091
|
+
.ui-topbar-item-icon svg {
|
|
2092
|
+
inline-size: var(--topbar-icon-size);
|
|
2093
|
+
block-size: var(--topbar-icon-size);
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2086
2096
|
@media (width <= 68.75rem) {
|
|
2087
2097
|
.ui-topbar-center {
|
|
2088
2098
|
display: var(--topbar-center-compact-display);
|
|
@@ -51,37 +51,45 @@
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
[data-slot="text"][data-tone="default"],
|
|
54
|
-
[data-slot="heading"][data-tone="default"]
|
|
54
|
+
[data-slot="heading"][data-tone="default"],
|
|
55
|
+
[data-slot="icon"][data-tone="default"] {
|
|
55
56
|
color: hsl(var(--foreground));
|
|
56
57
|
}
|
|
57
58
|
[data-slot="text"][data-tone="muted"],
|
|
58
|
-
[data-slot="heading"][data-tone="muted"]
|
|
59
|
+
[data-slot="heading"][data-tone="muted"],
|
|
60
|
+
[data-slot="icon"][data-tone="muted"] {
|
|
59
61
|
color: hsl(var(--muted-foreground));
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
[data-slot="text"][data-tone="inherit"],
|
|
63
|
-
[data-slot="heading"][data-tone="inherit"]
|
|
65
|
+
[data-slot="heading"][data-tone="inherit"],
|
|
66
|
+
[data-slot="icon"][data-tone="inherit"] {
|
|
64
67
|
color: inherit;
|
|
65
68
|
}
|
|
66
69
|
[data-slot="text"][data-tone="primary"],
|
|
67
|
-
[data-slot="heading"][data-tone="primary"]
|
|
70
|
+
[data-slot="heading"][data-tone="primary"],
|
|
71
|
+
[data-slot="icon"][data-tone="primary"] {
|
|
68
72
|
color: hsl(var(--primary));
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
[data-slot="text"][data-tone="success"],
|
|
72
|
-
[data-slot="heading"][data-tone="success"]
|
|
76
|
+
[data-slot="heading"][data-tone="success"],
|
|
77
|
+
[data-slot="icon"][data-tone="success"] {
|
|
73
78
|
color: hsl(var(--text-success));
|
|
74
79
|
}
|
|
75
80
|
[data-slot="text"][data-tone="warning"],
|
|
76
|
-
[data-slot="heading"][data-tone="warning"]
|
|
81
|
+
[data-slot="heading"][data-tone="warning"],
|
|
82
|
+
[data-slot="icon"][data-tone="warning"] {
|
|
77
83
|
color: hsl(var(--text-warning));
|
|
78
84
|
}
|
|
79
85
|
[data-slot="text"][data-tone="destructive"],
|
|
80
|
-
[data-slot="heading"][data-tone="destructive"]
|
|
86
|
+
[data-slot="heading"][data-tone="destructive"],
|
|
87
|
+
[data-slot="icon"][data-tone="destructive"] {
|
|
81
88
|
color: hsl(var(--text-error));
|
|
82
89
|
}
|
|
83
90
|
[data-slot="text"][data-tone="info"],
|
|
84
|
-
[data-slot="heading"][data-tone="info"]
|
|
91
|
+
[data-slot="heading"][data-tone="info"],
|
|
92
|
+
[data-slot="icon"][data-tone="info"] {
|
|
85
93
|
color: hsl(var(--text-info));
|
|
86
94
|
}
|
|
87
95
|
|
package/docs/CONSUMER-RULES.md
CHANGED
|
@@ -5,9 +5,9 @@ Read this once; the audit enforces it. Everything else in `docs/` is for contrib
|
|
|
5
5
|
1. Load styles with `@import "@godxjp/ui/styles"` (fonts bundled, 729 sliced woff2 faces, ~11.7 MB), `@import "@godxjp/ui/styles/core"` (no `@font-face` at all), `@import "@godxjp/ui/styles/core-with-fallbacks"` (`core` + six `local()`-only metric-matched faces, still zero network bytes — for when you supply Noto Sans JP yourself) or `@import "@godxjp/ui/styles/core-with-jis-level1"` (`core-with-fallbacks` + Noto Sans JP merged to JIS X 0208 level 1: **3 requests, ~1.53 MB, once** instead of ~8 font requests on every navigation — for a Japanese app that wants the bundled face). Never cherry-pick `*-layout.css`.
|
|
6
6
|
2. Every page is `<PageContainer title subtitle extra footer>`; its sections are spaced by the page. Group items inside a section with `<Flex direction="col" gap>` or `<ResponsiveGrid>`.
|
|
7
7
|
3. No Tailwind layout on your own elements: no `flex`, `grid`, `gap-*`, `p-*`, `m-*`, `space-*`. Rows are `<Flex>` (default row), stacks are `<Flex direction="col">`, grids are `<ResponsiveGrid>`.
|
|
8
|
-
4. No hand-rolled surfaces: no `rounded-* border bg-*` divs. A box is `Card`, a pill is `Badge`, a person is `Avatar`, a row is `ListRow`, a label/value pair is `Descriptions`, an empty area is `EmptyState`, a read-only sample of a colour the USER chose is `Swatch`.
|
|
8
|
+
4. No hand-rolled surfaces: no `rounded-* border bg-*` divs. A box is `Card`, a pill is `Badge`, a person is `Avatar`, a row is `ListRow`, a label/value pair is `Descriptions`, an empty area is `EmptyState`, a read-only sample of a colour the USER chose is `Swatch`. A LIST of those rows is `<Flex as="ul" marker="none" direction="col" gap="none">` with `<ListRow as="li">` children — `marker="none"` keeps the `<ul>`, the `<li>` semantics and the gap token while dropping the bullet and the indent. Never a raw `<ul>`/`<ol>` (it carries no gap token), never `<div role="list">` + `<div role="listitem">`, and never a wrapper around each row: the divider is `:not(:last-child)` among siblings, so a row alone in its own wrapper is always the last one and EVERY divider disappears silently. A bulleted prose list is the same `<Flex as="ul">` without `marker`.
|
|
9
9
|
5. Real controls only: `Button`, `Input`, `Select`, `Textarea`, `Checkbox`… never raw `<button>`/`<input>`; a labelled control lives in `<FormField label>`. A Select outside a form takes `width="auto"`. **A disabled control's reason is visible text, never a tooltip** — see below.
|
|
10
|
-
6. Text is `<Text>` / `<Heading>` with `tone`, `size`, `weight`, `truncate`, `mono` — not `className="text-muted-foreground font-semibold"`.
|
|
10
|
+
6. Text is `<Text>` / `<Heading>` with `tone`, `size`, `weight`, `truncate`, `mono` — not `className="text-muted-foreground font-semibold"`. A STANDALONE glyph is `<Icon as={Lock} size="sm" tone="muted" />` from `@godxjp/ui/general`: a bare lucide icon carries its own `width="24" height="24"` and only four places re-size it for you (`Button`, a `DropdownMenuItem`, a `TopbarItem`, a `ListRow` leading slot), so everywhere else — in a `Text`, a table cell, an `<a>` — it draws at 24px beside 14px type. `size-4` and `w-[16px]` are closed to you by rules 3 and 8, and `size={16}` hard-codes a number the theme owns; `Icon` puts the glyph on the `--icon-size-*` scale (`2xs…4xl`) instead, and is `aria-hidden` unless you give it a `label` (then it is `role="img"` with that name). The audit rule is `lucide-icon-needs-size`.
|
|
11
11
|
7. Colours are semantic tokens (`tone="destructive"`, `bg-primary`), never palette names, hex, or `bg-black` / `text-white`.
|
|
12
12
|
8. Sizes come from props (`size`, `width`, `columns`), never `w-[240px]` / `max-h-[420px]`.
|
|
13
13
|
9. Logical directions (`ms-`, `me-`, `start-`, `end-`) when a utility is unavoidable; never `ml-` / `left-`.
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
> Generated by `scripts/frame-coverage.mjs` (issue #163). Do not edit by hand — run `pnpm check:frame-coverage`.
|
|
4
4
|
> Standard: [FRAME-COVERAGE-STANDARD.md](./FRAME-COVERAGE-STANDARD.md). A missing dimension is **UNTESTED**, never a pass.
|
|
5
5
|
|
|
6
|
-
- Public components: **
|
|
7
|
-
- With ≥1 frame: **
|
|
6
|
+
- Public components: **164**
|
|
7
|
+
- With ≥1 frame: **143** · zero-frame: **21**
|
|
8
8
|
- Fully covered (every contract axis declared): **0**
|
|
9
9
|
|
|
10
10
|
## UNTESTED — no `/frame/**` route
|
|
@@ -74,6 +74,7 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
|
|
|
74
74
|
| Logo | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
|
|
75
75
|
| Reveal | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
76
76
|
| Activity | ✓ | · | N/A | N/A | · | · | · | · | · | · |
|
|
77
|
+
| Icon | ✓ | · | N/A | N/A | · | · | · | · | · | · |
|
|
77
78
|
| DataTable | ✓ | ✓ | · | · | · | · | · | · | · | · |
|
|
78
79
|
| Card | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
|
|
79
80
|
| CardContent | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrowUpRight,
|
|
3
|
+
Check,
|
|
4
|
+
Download,
|
|
5
|
+
FileWarning,
|
|
6
|
+
Globe,
|
|
7
|
+
Lock,
|
|
8
|
+
ShieldCheck,
|
|
9
|
+
Smartphone,
|
|
10
|
+
Unlock,
|
|
11
|
+
} from "lucide-react";
|
|
12
|
+
|
|
13
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@godxjp/ui/data-display";
|
|
14
|
+
import { Button, Heading, Icon, Text } from "@godxjp/ui/general";
|
|
15
|
+
import { Flex, PageContainer, Separator } from "@godxjp/ui/layout";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Icon · a glyph on the `--icon-size-*` scale, and the only supported way to draw a standalone one.
|
|
19
|
+
*
|
|
20
|
+
* A lucide component ships `width="24" height="24"`. Four rules in this library override that
|
|
21
|
+
* (`.ui-button svg`, the menu row, the topbar cell, the ListRow leading slot) and nothing else
|
|
22
|
+
* does · so a glyph in a `Text`, a table cell or an `<a>` draws at 24px beside 14px type, 1.7× the
|
|
23
|
+
* text it annotates. `docs/CONSUMER-RULES.md` §3/§8 forbid the `size-4` / `w-[16px]` escape, which
|
|
24
|
+
* left a consumer with 38 oversized glyphs and no supported fix (gh#712).
|
|
25
|
+
*
|
|
26
|
+
* `Icon` renders ONTO the glyph · the sized element is the `<svg>` itself · so it composes
|
|
27
|
+
* anywhere a bare glyph does and adds no box to the row. Decorative (`aria-hidden`) by default;
|
|
28
|
+
* `label` switches it to `role="img"` with that accessible name.
|
|
29
|
+
*
|
|
30
|
+
* Composed only from real @godxjp/ui components. All copy is consumer-owned and localized.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** The nine steps, with the px each resolves to at a 16px root · docs/TOKENS.md. */
|
|
34
|
+
const SIZES = [
|
|
35
|
+
{ size: "2xs", px: "10px", note: "アバターの在席ドット" },
|
|
36
|
+
{ size: "xs", px: "12px", note: "表の並べ替えカーソル" },
|
|
37
|
+
{ size: "sm", px: "14px", note: 'size="sm" のコントロール' },
|
|
38
|
+
{ size: "md", px: "16px", note: "既定のアイコン段" },
|
|
39
|
+
{ size: "lg", px: "20px", note: "アラート・ランチャー" },
|
|
40
|
+
{ size: "xl", px: "24px", note: "アップロードタイル" },
|
|
41
|
+
{ size: "2xl", px: "36px", note: "StatCard のメダリオン" },
|
|
42
|
+
{ size: "3xl", px: "40px", note: "ドロップゾーン" },
|
|
43
|
+
{ size: "4xl", px: "48px", note: "空状態の枠" },
|
|
44
|
+
] as const;
|
|
45
|
+
|
|
46
|
+
/** `tone` shares Text's vocabulary · one table for the glyph and the label beside it. */
|
|
47
|
+
const TONES = [
|
|
48
|
+
{ tone: "default", label: "default · 前景色" },
|
|
49
|
+
{ tone: "muted", label: "muted · 補足" },
|
|
50
|
+
{ tone: "primary", label: "primary · 主要導線" },
|
|
51
|
+
{ tone: "success", label: "success · 有効" },
|
|
52
|
+
{ tone: "warning", label: "warning · 要確認" },
|
|
53
|
+
{ tone: "destructive", label: "destructive · 失敗" },
|
|
54
|
+
{ tone: "info", label: "info · 情報" },
|
|
55
|
+
{ tone: "inherit", label: "inherit · 面の色を継承" },
|
|
56
|
+
] as const;
|
|
57
|
+
|
|
58
|
+
/** The real screen: a security settings list whose status is carried by a glyph beside 14px text. */
|
|
59
|
+
const SESSIONS = [
|
|
60
|
+
{
|
|
61
|
+
id: "s1",
|
|
62
|
+
device: "MacBook Pro · 東京",
|
|
63
|
+
detail: "2026-09-17 09:12 · 192.0.2.24",
|
|
64
|
+
secure: true,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "s2",
|
|
68
|
+
device: "iPhone 15 · 大阪",
|
|
69
|
+
detail: "2026-09-16 21:40 · 198.51.100.7",
|
|
70
|
+
secure: true,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "s3",
|
|
74
|
+
device: "Windows PC · 未確認の拠点",
|
|
75
|
+
detail: "2026-09-15 02:05 · 203.0.113.19",
|
|
76
|
+
secure: false,
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
export default function Demo() {
|
|
81
|
+
return (
|
|
82
|
+
<PageContainer
|
|
83
|
+
title="Icon"
|
|
84
|
+
subtitle='--icon-size-* の9段にグリフを載せる唯一の方法 · 既定は装飾(aria-hidden)· label で role="img"'
|
|
85
|
+
>
|
|
86
|
+
<Flex direction="col" gap="lg">
|
|
87
|
+
{/* ── The real screen · the defect this primitive exists for: a status glyph inline with
|
|
88
|
+
14px body text, where no context rule would ever have sized it. ── */}
|
|
89
|
+
<Card>
|
|
90
|
+
<CardHeader>
|
|
91
|
+
<CardTitle level={2}>サインイン中の端末</CardTitle>
|
|
92
|
+
</CardHeader>
|
|
93
|
+
<CardContent>
|
|
94
|
+
<Flex direction="col" gap="md">
|
|
95
|
+
{SESSIONS.map((session) => (
|
|
96
|
+
<Flex key={session.id} direction="row" gap="sm" align="center" justify="between">
|
|
97
|
+
<Flex direction="row" gap="sm" align="center">
|
|
98
|
+
<Icon
|
|
99
|
+
as={session.secure ? ShieldCheck : FileWarning}
|
|
100
|
+
size="lg"
|
|
101
|
+
tone={session.secure ? "success" : "warning"}
|
|
102
|
+
label={session.secure ? "保護された接続" : "確認が必要な接続"}
|
|
103
|
+
/>
|
|
104
|
+
<Flex direction="col">
|
|
105
|
+
<Text weight="medium">{session.device}</Text>
|
|
106
|
+
{/* A decorative glyph INSIDE 14px text · `sm` puts it on the type's own step. */}
|
|
107
|
+
<Text size="xs" tone="muted">
|
|
108
|
+
<Icon as={Globe} size="xs" /> {session.detail}
|
|
109
|
+
</Text>
|
|
110
|
+
</Flex>
|
|
111
|
+
</Flex>
|
|
112
|
+
<Button variant="outline" size="sm">
|
|
113
|
+
<Icon as={session.secure ? Lock : Unlock} size="sm" />
|
|
114
|
+
サインアウト
|
|
115
|
+
</Button>
|
|
116
|
+
</Flex>
|
|
117
|
+
))}
|
|
118
|
+
</Flex>
|
|
119
|
+
</CardContent>
|
|
120
|
+
</Card>
|
|
121
|
+
|
|
122
|
+
{/* ── Every step of the scale ── */}
|
|
123
|
+
<Card>
|
|
124
|
+
<CardHeader>
|
|
125
|
+
<CardTitle level={2}>size · 9段のアイコンスケール</CardTitle>
|
|
126
|
+
</CardHeader>
|
|
127
|
+
<CardContent>
|
|
128
|
+
<Flex direction="col" gap="sm">
|
|
129
|
+
{SIZES.map((step) => (
|
|
130
|
+
<Flex key={step.size} direction="row" gap="md" align="center">
|
|
131
|
+
<Flex direction="row" gap="sm" align="center">
|
|
132
|
+
<Icon as={Smartphone} size={step.size} />
|
|
133
|
+
</Flex>
|
|
134
|
+
<Text size="sm" mono>
|
|
135
|
+
{step.size}
|
|
136
|
+
</Text>
|
|
137
|
+
<Text size="sm" tone="muted" mono>
|
|
138
|
+
{step.px}
|
|
139
|
+
</Text>
|
|
140
|
+
<Text size="sm" tone="muted">
|
|
141
|
+
{step.note}
|
|
142
|
+
</Text>
|
|
143
|
+
</Flex>
|
|
144
|
+
))}
|
|
145
|
+
</Flex>
|
|
146
|
+
</CardContent>
|
|
147
|
+
</Card>
|
|
148
|
+
|
|
149
|
+
{/* ── Every tone, beside the Text that shares the vocabulary ── */}
|
|
150
|
+
<Card>
|
|
151
|
+
<CardHeader>
|
|
152
|
+
<CardTitle level={2}>tone · Text と同じ意味の語彙</CardTitle>
|
|
153
|
+
</CardHeader>
|
|
154
|
+
<CardContent>
|
|
155
|
+
<Flex direction="col" gap="sm">
|
|
156
|
+
{TONES.map((entry) => (
|
|
157
|
+
<Flex key={entry.tone} direction="row" gap="sm" align="center">
|
|
158
|
+
<Icon as={Check} size="md" tone={entry.tone} />
|
|
159
|
+
<Text size="sm" tone={entry.tone}>
|
|
160
|
+
{entry.label}
|
|
161
|
+
</Text>
|
|
162
|
+
</Flex>
|
|
163
|
+
))}
|
|
164
|
+
</Flex>
|
|
165
|
+
</CardContent>
|
|
166
|
+
</Card>
|
|
167
|
+
|
|
168
|
+
{/* ── Where it composes · inside Text, inside a Button, standalone in a link ── */}
|
|
169
|
+
<Card>
|
|
170
|
+
<CardHeader>
|
|
171
|
+
<CardTitle level={2}>合成 · Text の中・Button の中・単独</CardTitle>
|
|
172
|
+
</CardHeader>
|
|
173
|
+
<CardContent>
|
|
174
|
+
<Flex direction="col" gap="md">
|
|
175
|
+
<Flex direction="col" gap="xs">
|
|
176
|
+
<Heading level={3}>Text の中</Heading>
|
|
177
|
+
<Text size="sm">
|
|
178
|
+
請求書は <Icon as={Lock} size="sm" tone="muted" /> 暗号化されて保管されます。
|
|
179
|
+
</Text>
|
|
180
|
+
</Flex>
|
|
181
|
+
|
|
182
|
+
<Separator />
|
|
183
|
+
|
|
184
|
+
<Flex direction="col" gap="xs">
|
|
185
|
+
<Heading level={3}>Button の中 · 明示した段が Button の既定に勝つ</Heading>
|
|
186
|
+
<Flex direction="row" gap="sm" align="center">
|
|
187
|
+
<Button size="sm">
|
|
188
|
+
<Icon as={Download} size="sm" />
|
|
189
|
+
書き出す
|
|
190
|
+
</Button>
|
|
191
|
+
<Button variant="outline">
|
|
192
|
+
<Icon as={Download} size="lg" />
|
|
193
|
+
大きめのグリフ
|
|
194
|
+
</Button>
|
|
195
|
+
</Flex>
|
|
196
|
+
</Flex>
|
|
197
|
+
|
|
198
|
+
<Separator />
|
|
199
|
+
|
|
200
|
+
<Flex direction="col" gap="xs">
|
|
201
|
+
<Heading level={3}>単独 · 名前を持つグリフ</Heading>
|
|
202
|
+
<Flex direction="row" gap="sm" align="center">
|
|
203
|
+
<Text as="a" href="#icon-standalone" size="sm" tone="primary">
|
|
204
|
+
監査ログを開く <Icon as={ArrowUpRight} size="sm" />
|
|
205
|
+
</Text>
|
|
206
|
+
<Icon as={ShieldCheck} size="md" tone="success" label="二要素認証は有効です" />
|
|
207
|
+
</Flex>
|
|
208
|
+
</Flex>
|
|
209
|
+
</Flex>
|
|
210
|
+
</CardContent>
|
|
211
|
+
</Card>
|
|
212
|
+
</Flex>
|
|
213
|
+
</PageContainer>
|
|
214
|
+
);
|
|
215
|
+
}
|
package/docs/layout/flex.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
CardDescription,
|
|
6
6
|
CardHeader,
|
|
7
7
|
CardTitle,
|
|
8
|
+
ListRow,
|
|
8
9
|
} from "@godxjp/ui/data-display";
|
|
9
10
|
import { Button, Text } from "@godxjp/ui/general";
|
|
10
11
|
import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
@@ -309,6 +310,64 @@ export default function Demo() {
|
|
|
309
310
|
</Flex>
|
|
310
311
|
</CardContent>
|
|
311
312
|
</Card>
|
|
313
|
+
<Card>
|
|
314
|
+
<CardHeader>
|
|
315
|
+
<CardTitle level={2}>marker="none" · 装飾のない意味的リスト</CardTitle>
|
|
316
|
+
<CardDescription>
|
|
317
|
+
as="ul" は既定で中黒と字下げが付きます。設定メニューのような「行の一覧」では
|
|
318
|
+
marker="none" を指定すると、ul と li の意味づけと gap トークンは残したまま、
|
|
319
|
+
中黒も字下げも消えます。行は ListRow as="li"
|
|
320
|
+
で、区切り線は行が自分で描きます。
|
|
321
|
+
</CardDescription>
|
|
322
|
+
</CardHeader>
|
|
323
|
+
<CardContent flush>
|
|
324
|
+
<Flex id="semantic-list" as="ul" marker="none" direction="col" gap="none">
|
|
325
|
+
<ListRow
|
|
326
|
+
as="li"
|
|
327
|
+
title="二要素認証"
|
|
328
|
+
description="認証アプリ · 有効"
|
|
329
|
+
trailing={
|
|
330
|
+
<Button size="xs" variant="outline">
|
|
331
|
+
管理
|
|
332
|
+
</Button>
|
|
333
|
+
}
|
|
334
|
+
/>
|
|
335
|
+
<ListRow
|
|
336
|
+
as="li"
|
|
337
|
+
title="パスキー"
|
|
338
|
+
description="2 件登録済み"
|
|
339
|
+
trailing={
|
|
340
|
+
<Button size="xs" variant="outline">
|
|
341
|
+
管理
|
|
342
|
+
</Button>
|
|
343
|
+
}
|
|
344
|
+
/>
|
|
345
|
+
<ListRow
|
|
346
|
+
as="li"
|
|
347
|
+
title="回復コード"
|
|
348
|
+
description="未発行"
|
|
349
|
+
trailing={
|
|
350
|
+
<Button size="xs" variant="outline">
|
|
351
|
+
発行
|
|
352
|
+
</Button>
|
|
353
|
+
}
|
|
354
|
+
/>
|
|
355
|
+
</Flex>
|
|
356
|
+
</CardContent>
|
|
357
|
+
<CardContent>
|
|
358
|
+
<Flex id="semantic-list-gap" as="ul" marker="none" gap="sm" wrap>
|
|
359
|
+
<Flex as="li">
|
|
360
|
+
<Badge variant="outline">監査ログ</Badge>
|
|
361
|
+
</Flex>
|
|
362
|
+
<Flex as="li">
|
|
363
|
+
<Badge variant="outline">IP 制限</Badge>
|
|
364
|
+
</Flex>
|
|
365
|
+
<Flex as="li">
|
|
366
|
+
<Badge variant="outline">SSO</Badge>
|
|
367
|
+
</Flex>
|
|
368
|
+
</Flex>
|
|
369
|
+
</CardContent>
|
|
370
|
+
</Card>
|
|
312
371
|
<Card>
|
|
313
372
|
<CardHeader>
|
|
314
373
|
<CardTitle level={2}>軽い強調と行アクション</CardTitle>
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
CardHeader,
|
|
10
10
|
CardTitle,
|
|
11
11
|
} from "@godxjp/ui/data-display";
|
|
12
|
-
import { Button, Text } from "@godxjp/ui/general";
|
|
12
|
+
import { Button, Icon, Text } from "@godxjp/ui/general";
|
|
13
13
|
import { AppShell, Flex, PageContainer, Sidebar, Topbar, TopbarItem } from "@godxjp/ui/layout";
|
|
14
14
|
import type { SidebarSectionProp } from "@godxjp/ui/layout";
|
|
15
15
|
import {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "@godxjp/ui/navigation";
|
|
23
23
|
import {
|
|
24
24
|
Bell,
|
|
25
|
+
Building2,
|
|
25
26
|
FileText,
|
|
26
27
|
LayoutDashboard,
|
|
27
28
|
LogOut,
|
|
@@ -150,6 +151,33 @@ export default function Demo() {
|
|
|
150
151
|
/>
|
|
151
152
|
</CardContent>
|
|
152
153
|
</Card>
|
|
154
|
+
|
|
155
|
+
<Card>
|
|
156
|
+
<CardHeader>
|
|
157
|
+
<CardTitle level={2}>包んだグリフ(icon スロットと Icon)</CardTitle>
|
|
158
|
+
<CardDescription>
|
|
159
|
+
`.ui-topbar-item > svg` は直接の子だけを見ます。ブレークポイントで隠すために
|
|
160
|
+
グリフを包むと、その規則は届かず lucide 既定の 24px で描かれます(390px
|
|
161
|
+
のセルで実測)。 セル自身が持つ `icon` スロットに渡すか、包むなら `Icon`
|
|
162
|
+
を使えば、どちらもバーの段 (--topbar-icon-size)に載ります。
|
|
163
|
+
</CardDescription>
|
|
164
|
+
</CardHeader>
|
|
165
|
+
<CardContent>
|
|
166
|
+
<Topbar
|
|
167
|
+
start={
|
|
168
|
+
<>
|
|
169
|
+
<TopbarItem icon={<Building2 aria-hidden="true" />}>経理部</TopbarItem>
|
|
170
|
+
<TopbarItem aria-label="拠点を切り替える">
|
|
171
|
+
<Flex hideBelow="sm">
|
|
172
|
+
<Icon as={Building2} size="md" />
|
|
173
|
+
</Flex>
|
|
174
|
+
東京本社
|
|
175
|
+
</TopbarItem>
|
|
176
|
+
</>
|
|
177
|
+
}
|
|
178
|
+
/>
|
|
179
|
+
</CardContent>
|
|
180
|
+
</Card>
|
|
153
181
|
</Flex>
|
|
154
182
|
</PageContainer>
|
|
155
183
|
</AppShell>
|
package/package.json
CHANGED
package/scripts/consumer-rule.md
CHANGED
|
@@ -98,23 +98,24 @@ Slack thì đặt lại **một dòng**, không fork `.app-nav-rail`.
|
|
|
98
98
|
Hỏi MCP `godxjp-ui` (`search_components`, `get_component`). Đo được trong một
|
|
99
99
|
ngày: năm thứ cần đều ĐÃ CÓ và vẫn bị dựng lại bằng thứ khác —
|
|
100
100
|
|
|
101
|
-
| Cần
|
|
102
|
-
|
|
|
103
|
-
| đường kẻ chạm mép Card
|
|
104
|
-
| header có kẻ khi thân là danh sách flush
|
|
105
|
-
| một hàng LÀ liên kết (thay cho nút rời)
|
|
106
|
-
| kẻ ô từng ngày trong lịch
|
|
107
|
-
| dải giữa hai vùng, tự kẻ theo VỊ TRÍ
|
|
108
|
-
| chip "điều kiện đang bật" có dấu × để bỏ
|
|
109
|
-
| dải tab nằm TRONG đầu Card
|
|
110
|
-
| tiêu đề / đoạn văn / liên kết trong văn bản
|
|
111
|
-
| nút nổi góc màn (quay lên đầu, hành động nhanh)
|
|
112
|
-
| khung xương của một FORM khi đang tải
|
|
113
|
-
| ô màu chỉ để xem, màu do NGƯỜI DÙNG chọn
|
|
114
|
-
| ảnh đại diện của một tệp / bản xem trước
|
|
115
|
-
| danh sách "có gì trong gói này"
|
|
116
|
-
| panel kéo giãn được (chia đôi màn)
|
|
117
|
-
| màn hội thoại AI
|
|
101
|
+
| Cần | Đã có |
|
|
102
|
+
| ---------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
103
|
+
| đường kẻ chạm mép Card | `<CardContent flush>` |
|
|
104
|
+
| header có kẻ khi thân là danh sách flush | `<CardHeader banded>` |
|
|
105
|
+
| một hàng LÀ liên kết (thay cho nút rời) | `<ListRow asChild>` |
|
|
106
|
+
| kẻ ô từng ngày trong lịch | `<Calendar bordered>` |
|
|
107
|
+
| dải giữa hai vùng, tự kẻ theo VỊ TRÍ | `<CardBar>` (`border` để ép khi xếp chồng) |
|
|
108
|
+
| chip "điều kiện đang bật" có dấu × để bỏ | `<Badge onRemove>` — KHÔNG phải `TagInput` |
|
|
109
|
+
| dải tab nằm TRONG đầu Card | `<Card tabList activeTabKey onTabChange>` |
|
|
110
|
+
| tiêu đề / đoạn văn / liên kết trong văn bản | `<Title>` · `<Paragraph>` · `<Link>` (`Text` 33 prop) |
|
|
111
|
+
| nút nổi góc màn (quay lên đầu, hành động nhanh) | `<FloatButton>` |
|
|
112
|
+
| khung xương của một FORM khi đang tải | `<SkeletonForm>` |
|
|
113
|
+
| ô màu chỉ để xem, màu do NGƯỜI DÙNG chọn | `<Swatch>` |
|
|
114
|
+
| ảnh đại diện của một tệp / bản xem trước | `<Thumbnail>` |
|
|
115
|
+
| danh sách "có gì trong gói này" | `<FeatureList>` |
|
|
116
|
+
| panel kéo giãn được (chia đôi màn) | `<DraggablePanel>` |
|
|
117
|
+
| màn hội thoại AI | `<Welcome>` · `<Conversations>` · `<Attachments>` · `<ThoughtChain>` · `<Actions>` |
|
|
118
|
+
| một glyph lucide đứng một mình (trong `Text`, ô bảng, thẻ `<a>`) | `<Icon as={Lock} size="sm">` — KHÔNG phải `size-4` hay `size={16}` |
|
|
118
119
|
|
|
119
120
|
Lỗi không phải "đoán sai tên prop" mà là **cho rằng nó không tồn tại nên không
|
|
120
121
|
hỏi**.
|
package/scripts/ui-audit.mjs
CHANGED
|
@@ -590,6 +590,68 @@ const RULES = [
|
|
|
590
590
|
message:
|
|
591
591
|
"No em-dash (—) in product copy. Use a middot `·` for JP/EN label pairs, or restructure into two calm sentences. Keep copy factual and quiet.",
|
|
592
592
|
},
|
|
593
|
+
{
|
|
594
|
+
id: "lucide-icon-needs-size",
|
|
595
|
+
replacement: '<Icon as={Glyph} size="sm" />',
|
|
596
|
+
// CONSUMER scope, like the other rules about what an app may write: inside this package a bare
|
|
597
|
+
// glyph is normal — Tree, Pagination, Attachments and a dozen others size their own svg from
|
|
598
|
+
// their own stylesheet, which is exactly the capability a consumer does not have. Measured:
|
|
599
|
+
// 90 findings here with the scope off, every one of them a component sizing its own glyph.
|
|
600
|
+
scope: "consumer",
|
|
601
|
+
matches: lucideGlyphMatches,
|
|
602
|
+
spansElement: true,
|
|
603
|
+
severity: "warn",
|
|
604
|
+
/*
|
|
605
|
+
* A lucide element rendered where NOTHING will size it.
|
|
606
|
+
*
|
|
607
|
+
* A lucide component ships `width="24" height="24"`, and exactly four rules in this library
|
|
608
|
+
* override that: `.ui-button svg`, `.ui-dropdown-menu-item > svg`, `.ui-topbar-item svg` and
|
|
609
|
+
* `[data-slot="list-row-leading"] > svg`. Outside them — in a `Text`, a table cell, an `<a>`,
|
|
610
|
+
* a `Flex` — the glyph draws at 24px beside 14px type. It is the one defect class that is
|
|
611
|
+
* INVISIBLE in review: the JSX is correct, the import is correct, and only the screen is
|
|
612
|
+
* wrong. A consumer swept one app and found 38 (gh#712).
|
|
613
|
+
*
|
|
614
|
+
* `warn`, not `error`, and deliberately so: the exemptions below are lexical, so a glyph
|
|
615
|
+
* inside a consumer's own wrapper component that happens to size it would be reported. The
|
|
616
|
+
* fix is cheap (`<Icon as={Lock} size="sm" />`) and the finding is worth reading even then.
|
|
617
|
+
*/
|
|
618
|
+
standard: "WCAG 2.2 SC 1.4.4 · @godxjp/ui icon scale (docs/TOKENS.md, --icon-size-*)",
|
|
619
|
+
message:
|
|
620
|
+
'A lucide glyph with nothing to size it renders at its intrinsic 24px — 1.7× a 14px label. Put it on the scale with the primitive: <Icon as={Lock} size="sm" tone="muted" />, which is aria-hidden by default. A size-4 / w-[16px] utility is what docs/CONSUMER-RULES.md §3/§8 forbid, and size={16} hard-codes a number the theme owns.',
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
id: "no-hand-rolled-list",
|
|
624
|
+
replacement: 'Flex as="ul" marker="none" + ListRow as="li"',
|
|
625
|
+
scope: "consumer",
|
|
626
|
+
severity: "warn",
|
|
627
|
+
spansElement: true,
|
|
628
|
+
/*
|
|
629
|
+
* A list built by hand instead of by the primitives, in the three shapes a consumer reaches for:
|
|
630
|
+
*
|
|
631
|
+
* 1. a raw `<ul>`/`<ol>` — it cannot carry the gap token, so the spacing goes back into
|
|
632
|
+
* utilities, and until `marker="none"` (gh#714) it was the only way to get a list without
|
|
633
|
+
* a bullet and a --space-5 indent. It no longer is.
|
|
634
|
+
* 2. `role="list"` / `role="listitem"` on a div — ARIA re-describing markup that HTML already
|
|
635
|
+
* has a element for (WAI-ARIA 1.2 §Using ARIA in HTML: prefer the native element).
|
|
636
|
+
* 3. a `<li>` (or any wrapper) around a library ROW instead of BEING it. This is the one that
|
|
637
|
+
* cost real money: `[data-slot="list-row"]:not(:last-child)` draws the divider, so a row
|
|
638
|
+
* alone in a wrapper of its own is always `:last-child` and the rule never matches — a
|
|
639
|
+
* consumer's settings menu and dashboard lost EVERY divider, silently, and code review saw
|
|
640
|
+
* nothing. `ListRow` has `as="li"` (and `as` + `asChild` for a row of links) precisely so
|
|
641
|
+
* the rows stay siblings.
|
|
642
|
+
*
|
|
643
|
+
* WARN, not error, and the measurement says why: of 15 `role="list"` wrappers one consumer
|
|
644
|
+
* audited, 3 survived as deliberate exceptions (a drag-and-drop Kanban column of Cards, two
|
|
645
|
+
* evidence lists inside a TableCell). A rule wrong one time in five is a rule that gets
|
|
646
|
+
* silenced wholesale at `error`; at `warn` the three exceptions take an
|
|
647
|
+
* `ui-audit-disable-line` that states the reason and the other twelve still get fixed.
|
|
648
|
+
*/
|
|
649
|
+
test: /<(?:ul|ol)(?=[\s/>])[^\n]*|\brole=["'](?:list|listitem)["']|<li(?=[\s>])(?:(?!<\/li>)[\s\S]){0,400}?<(?:ListRow|Card)(?=[\s/>])/g,
|
|
650
|
+
standard:
|
|
651
|
+
"WAI-ARIA 1.2 (list / listitem) · HTML Living Standard (ul/ol/li) · WCAG 2.2 SC 1.3.1",
|
|
652
|
+
message:
|
|
653
|
+
'Hand-rolled list — a raw <ul>/<ol>, an ARIA role="list"/"listitem", or a <li> wrapped around a library row. Build it from <Flex as="ul" marker="none"> (keeps the element, the <li> semantics and the gap token; no bullet, no indent) with <ListRow as="li"> as the rows. A row inside a wrapper of its own is an only child, so its :not(:last-child) divider never matches and every divider disappears silently (docs/CONSUMER-RULES.md §4, gh#714).',
|
|
654
|
+
},
|
|
593
655
|
];
|
|
594
656
|
|
|
595
657
|
/**
|
|
@@ -946,6 +1008,72 @@ function insideStringLiteral(source, index) {
|
|
|
946
1008
|
return quote !== "";
|
|
947
1009
|
}
|
|
948
1010
|
|
|
1011
|
+
/**
|
|
1012
|
+
* The LOCAL names bound to `lucide-react` icons in this file — `import { Lock, Pencil as Edit }`
|
|
1013
|
+
* gives `Lock` and `Edit`. Reading the import rather than matching "any PascalCase tag" is what
|
|
1014
|
+
* keeps the rule from reporting every component in the file.
|
|
1015
|
+
*/
|
|
1016
|
+
function lucideLocalNames(source) {
|
|
1017
|
+
const names = new Set();
|
|
1018
|
+
for (const imp of source.matchAll(
|
|
1019
|
+
/import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*["']lucide-react["']/g,
|
|
1020
|
+
)) {
|
|
1021
|
+
for (const spec of imp[1].split(",")) {
|
|
1022
|
+
const local =
|
|
1023
|
+
spec
|
|
1024
|
+
.trim()
|
|
1025
|
+
.split(/\s+as\s+/)
|
|
1026
|
+
.pop()
|
|
1027
|
+
?.trim() ?? "";
|
|
1028
|
+
if (/^[A-Z][A-Za-z0-9]*$/.test(local)) names.add(local);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return names;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Character ranges in which a glyph is SOMEBODY ELSE'S to size, so a glyph inside one is quiet:
|
|
1036
|
+
*
|
|
1037
|
+
* 1. the body of a component whose CSS sizes its own `svg` — the four rules named on the rule
|
|
1038
|
+
* below, which are the only four that exist;
|
|
1039
|
+
* 2. the value of a slot PROP (`icon={<Lock />}`, `leading={<Lock />}`). A component handed a
|
|
1040
|
+
* glyph through a named slot owns the metric for that slot — that is what a slot IS — and
|
|
1041
|
+
* reporting the call site would push consumers away from the slot and toward a bare glyph.
|
|
1042
|
+
*/
|
|
1043
|
+
function iconSizingRanges(source) {
|
|
1044
|
+
const ranges = [];
|
|
1045
|
+
for (const m of source.matchAll(
|
|
1046
|
+
/<(Button|DropdownMenuItem|DropdownMenuCheckboxItem|DropdownMenuRadioItem|TopbarItem)\b[\s\S]*?<\/\1>/g,
|
|
1047
|
+
)) {
|
|
1048
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
1049
|
+
}
|
|
1050
|
+
for (const m of source.matchAll(
|
|
1051
|
+
/\b(?:icon|leading|trailing|mark|indicator|avatar|prefix|suffix|addonBefore|addonAfter)\s*=\s*\{/g,
|
|
1052
|
+
)) {
|
|
1053
|
+
const open = m.index + m[0].length - 1;
|
|
1054
|
+
const close = matchBracket(source, open);
|
|
1055
|
+
ranges.push([open, close < 0 ? source.length : close + 1]);
|
|
1056
|
+
}
|
|
1057
|
+
return ranges;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/** A lucide element that no rule, no slot and no author-supplied size will ever measure. */
|
|
1061
|
+
function* lucideGlyphMatches(source) {
|
|
1062
|
+
const names = lucideLocalNames(source);
|
|
1063
|
+
if (names.size === 0) return;
|
|
1064
|
+
const ranges = iconSizingRanges(source);
|
|
1065
|
+
for (const m of source.matchAll(new RegExp(`<(?:${[...names].join("|")})(?=[\\s/>])`, "g"))) {
|
|
1066
|
+
if (insideStringLiteral(source, m.index)) continue;
|
|
1067
|
+
if (ranges.some(([from, to]) => m.index > from && m.index < to)) continue;
|
|
1068
|
+
const end = jsxOpeningEnd(source, m.index);
|
|
1069
|
+
if (end >= source.length) continue;
|
|
1070
|
+
// An explicit size, or ANY className — a class list is the author saying something about this
|
|
1071
|
+
// element's box, and second-guessing which utility sizes it is how a warning becomes noise.
|
|
1072
|
+
if (/\s(?:size|width|height|className|style)\s*=/.test(source.slice(m.index, end))) continue;
|
|
1073
|
+
yield { 0: source.slice(m.index, end + 1), index: m.index };
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
949
1077
|
/** Match real JSX text after a balanced opening tag, including props with comparisons. */
|
|
950
1078
|
function* currencyMatches(source) {
|
|
951
1079
|
for (const opening of source.matchAll(/<(?:[A-Za-z][\w.:]*\b|(?=>))/g)) {
|