@godxjp/ui 27.3.1 → 27.5.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.
Files changed (53) hide show
  1. package/dist/components/data-entry/upload-file-kind.d.ts +25 -0
  2. package/dist/components/data-entry/upload-file-kind.js +88 -0
  3. package/dist/components/data-entry/upload.d.ts +2 -2
  4. package/dist/components/data-entry/upload.js +10 -6
  5. package/dist/components/general/icon.d.ts +31 -0
  6. package/dist/components/general/icon.js +23 -0
  7. package/dist/components/general/index.d.ts +2 -0
  8. package/dist/components/general/index.js +2 -0
  9. package/dist/components/layout/flex.d.ts +2 -2
  10. package/dist/components/layout/flex.js +7 -1
  11. package/dist/components/layout/topbar-item.d.ts +1 -0
  12. package/dist/components/layout/topbar-item.js +3 -1
  13. package/dist/components/ui/avatar.d.ts +1 -1
  14. package/dist/components/ui/avatar.js +2 -0
  15. package/dist/components/ui/segmented.d.ts +7 -2
  16. package/dist/components/ui/toggle.d.ts +2 -2
  17. package/dist/components/ui/toggle.js +4 -0
  18. package/dist/contracts/measurement.json +1 -1
  19. package/dist/props/components/data-display.prop.d.ts +14 -0
  20. package/dist/props/components/data-entry.prop.d.ts +25 -0
  21. package/dist/props/components/general.prop.d.ts +42 -1
  22. package/dist/props/components/layout.prop.d.ts +42 -0
  23. package/dist/props/registry.d.ts +37 -1
  24. package/dist/props/registry.js +43 -1
  25. package/dist/props/vocabulary/index.d.ts +1 -1
  26. package/dist/props/vocabulary/interaction.prop.d.ts +11 -0
  27. package/dist/styles/control.css +12 -0
  28. package/dist/styles/core.css +2 -0
  29. package/dist/styles/data-display-layout.css +35 -5
  30. package/dist/styles/data-entry-layout.css +22 -0
  31. package/dist/styles/icon-layout.css +37 -0
  32. package/dist/styles/index.css +2 -0
  33. package/dist/styles/shell-layout.css +10 -0
  34. package/dist/styles/text-layout.css +16 -8
  35. package/dist/tokens/components/control.css +1 -0
  36. package/dist/tokens/components/data-display.css +26 -0
  37. package/dist/tokens/components/segmented.css +4 -0
  38. package/dist/tokens/components/upload.css +4 -0
  39. package/docs/CONSUMER-RULES.md +2 -2
  40. package/docs/CUSTOMER-THEMING.md +26 -0
  41. package/docs/FRAME-COVERAGE-REPORT.md +3 -2
  42. package/docs/data-display/avatar.tsx +128 -11
  43. package/docs/data-display/prose.tsx +64 -0
  44. package/docs/data-entry/segmented.tsx +55 -5
  45. package/docs/data-entry/toggle-group.tsx +15 -3
  46. package/docs/data-entry/toggle.tsx +39 -1
  47. package/docs/data-entry/upload.tsx +38 -0
  48. package/docs/general/icon.tsx +215 -0
  49. package/docs/layout/flex.tsx +59 -0
  50. package/docs/layout/topbar-item.tsx +29 -1
  51. package/package.json +2 -2
  52. package/scripts/consumer-rule.md +18 -17
  53. package/scripts/ui-audit.mjs +128 -0
@@ -0,0 +1,25 @@
1
+ /**
2
+ * What a listed file IS, at the coarseness a row can actually draw (gh#720).
3
+ *
4
+ * A picture list shows a thumbnail for an image and a glyph for everything else, so the buckets
5
+ * are exactly the marks we have: five, not one per MIME type. The kind is also written onto the
6
+ * row as `data-file-kind`, so a service can retint a bucket without forking the component.
7
+ */
8
+ export type UploadFileKind = "image" | "pdf" | "archive" | "text" | "file";
9
+ /** The kind of one listed file, from its MIME type when it has one and its extension otherwise. */
10
+ export declare function uploadFileKind(item: {
11
+ name?: string;
12
+ mimeType?: string;
13
+ }): UploadFileKind;
14
+ /**
15
+ * The mark per kind. PDF takes the prose-lines document (`FileText`) because lucide ships no PDF
16
+ * glyph, which is also why plain/structured text takes the `</>` mark rather than the same one: a
17
+ * `.pdf` and a `.json` on adjacent rows must not draw identically.
18
+ */
19
+ export declare const UPLOAD_FILE_KIND_GLYPHS: {
20
+ readonly image: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
21
+ readonly pdf: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
22
+ readonly archive: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
23
+ readonly text: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
24
+ readonly file: import("react").ForwardRefExoticComponent<Omit<import("lucide-react").LucideProps, "ref"> & import("react").RefAttributes<SVGSVGElement>>;
25
+ };
@@ -0,0 +1,88 @@
1
+ import { File as FileGlyph, FileArchive, FileCode, FileImage, FileText } from "lucide-react";
2
+ const MIME_KINDS = {
3
+ "application/pdf": "pdf",
4
+ "application/x-pdf": "pdf",
5
+ "application/zip": "archive",
6
+ "application/x-zip-compressed": "archive",
7
+ "application/x-7z-compressed": "archive",
8
+ "application/vnd.rar": "archive",
9
+ "application/x-rar-compressed": "archive",
10
+ "application/gzip": "archive",
11
+ "application/x-tar": "archive",
12
+ "application/x-bzip": "archive",
13
+ "application/x-bzip2": "archive",
14
+ "application/json": "text",
15
+ "application/ld+json": "text",
16
+ "application/xml": "text",
17
+ "application/yaml": "text",
18
+ "application/x-yaml": "text"
19
+ };
20
+ const EXTENSION_KINDS = {
21
+ png: "image",
22
+ jpg: "image",
23
+ jpeg: "image",
24
+ gif: "image",
25
+ webp: "image",
26
+ avif: "image",
27
+ svg: "image",
28
+ bmp: "image",
29
+ ico: "image",
30
+ heic: "image",
31
+ heif: "image",
32
+ tif: "image",
33
+ tiff: "image",
34
+ pdf: "pdf",
35
+ zip: "archive",
36
+ tar: "archive",
37
+ gz: "archive",
38
+ tgz: "archive",
39
+ rar: "archive",
40
+ "7z": "archive",
41
+ bz2: "archive",
42
+ xz: "archive",
43
+ zst: "archive",
44
+ txt: "text",
45
+ md: "text",
46
+ markdown: "text",
47
+ json: "text",
48
+ jsonl: "text",
49
+ csv: "text",
50
+ tsv: "text",
51
+ log: "text",
52
+ xml: "text",
53
+ yml: "text",
54
+ yaml: "text",
55
+ html: "text",
56
+ htm: "text",
57
+ css: "text",
58
+ js: "text",
59
+ ts: "text"
60
+ };
61
+ function uploadFileKind(item) {
62
+ const mime = item.mimeType?.trim().toLowerCase();
63
+ if (mime) {
64
+ if (mime.startsWith("image/")) return "image";
65
+ if (mime.startsWith("text/")) return "text";
66
+ const known = MIME_KINDS[mime];
67
+ if (known) return known;
68
+ }
69
+ const name = item.name ?? "";
70
+ const dot = name.lastIndexOf(".");
71
+ if (dot > 0) {
72
+ const extension = name.slice(dot + 1).toLowerCase();
73
+ const known = EXTENSION_KINDS[extension];
74
+ if (known) return known;
75
+ }
76
+ return "file";
77
+ }
78
+ const UPLOAD_FILE_KIND_GLYPHS = {
79
+ image: FileImage,
80
+ pdf: FileText,
81
+ archive: FileArchive,
82
+ text: FileCode,
83
+ file: FileGlyph
84
+ };
85
+ export {
86
+ UPLOAD_FILE_KIND_GLYPHS,
87
+ uploadFileKind
88
+ };
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  import type { UploadProp } from "../../props/components/data-entry.prop.js";
3
- export type { UploadProp, UploadProp as UploadProps, UploadFileItemProp, UploadVariantProp, } from "../../props/components/data-entry.prop.js";
3
+ export type { UploadProp, UploadProp as UploadProps, UploadFileItemProp, UploadVariantProp, UploadListTypeProp, } from "../../props/components/data-entry.prop.js";
4
4
  export type { UploadFileItem, UploadVariant, UploadCommitAction } from "./upload-types.js";
5
5
  export { collectUploadCommitActions, createUploadItem, UPLOAD_LIST_IGNORE } from "./upload-types.js";
6
6
  export { useUploadDraft } from "./use-upload-draft.js";
7
- export declare function Upload({ variant, triggerSize, triggerVariant, value, defaultValue, onValueChange, accept: acceptProp, multiple: multipleProp, maxCount: maxCountProp, maxSizeBytes, disabled: disabledProp, readOnly, directory, pastable, openFileDialogOnClick, name, action, method, headers, data, withCredentials, beforeUpload, onReject, onRemove, onPreview, onDownload, previewFile, onDrop, showUploadList, itemRender, removable, onUpload, id, className, children, ...ariaProps }: UploadProp): React.JSX.Element;
7
+ export declare function Upload({ variant, listType, triggerSize, triggerVariant, value, defaultValue, onValueChange, accept: acceptProp, multiple: multipleProp, maxCount: maxCountProp, maxSizeBytes, disabled: disabledProp, readOnly, directory, pastable, openFileDialogOnClick, name, action, method, headers, data, withCredentials, beforeUpload, onReject, onRemove, onPreview, onDownload, previewFile, onDrop, showUploadList, itemRender, removable, onUpload, id, className, children, ...ariaProps }: UploadProp): React.JSX.Element;
@@ -15,9 +15,10 @@ import { useTranslation } from "../../i18n/use-translation.js";
15
15
  import { formatBytes } from "../../lib/format.js";
16
16
  import { cn } from "../../lib/utils.js";
17
17
  import { resolveFieldA11y } from "../../lib/field-a11y.js";
18
- import { controlIconClass } from "../../lib/control-styles.js";
19
18
  import { Button } from "../general/button.js";
19
+ import { Icon } from "../general/icon.js";
20
20
  import { UploadCropDialog } from "./upload-crop-dialog.js";
21
+ import { UPLOAD_FILE_KIND_GLYPHS, uploadFileKind } from "./upload-file-kind.js";
21
22
  import {
22
23
  UPLOAD_LIST_IGNORE,
23
24
  createUploadItem,
@@ -66,6 +67,7 @@ function useUploadList(controlled, defaultValue, onValueChange) {
66
67
  }
67
68
  function Upload({
68
69
  variant = "dropzone",
70
+ listType,
69
71
  triggerSize,
70
72
  triggerVariant = "outline",
71
73
  value,
@@ -109,6 +111,7 @@ function Upload({
109
111
  triggerOwnsAriaLabel ? { ...ariaProps, "aria-label": void 0 } : ariaProps,
110
112
  t("dataEntry.upload.inputLabel")
111
113
  );
114
+ const resolvedListType = listType ?? (variant === "picture" ? "picture" : "text");
112
115
  const accept = acceptProp ?? defaultAcceptForVariant(variant);
113
116
  const maxCount = maxCountProp ?? defaultMaxCount(variant);
114
117
  const multiple = multipleProp ?? (maxCount === 1 ? false : true);
@@ -380,7 +383,7 @@ function Upload({
380
383
  hasCustomPreview: Boolean(onPreview),
381
384
  onDownload,
382
385
  itemRender,
383
- showThumbnails: variant === "picture"
386
+ listType: resolvedListType
384
387
  }
385
388
  ) : null;
386
389
  const hiddenInput = /* @__PURE__ */ jsx(
@@ -571,7 +574,7 @@ function Upload({
571
574
  onClick: openPicker,
572
575
  className: "ui-upload-picture-empty",
573
576
  children: [
574
- /* @__PURE__ */ jsx(ImagePlus, { className: controlIconClass, "aria-hidden": "true" }),
577
+ /* @__PURE__ */ jsx(ImagePlus, { className: "ui-upload-picture-empty-icon", "aria-hidden": "true" }),
575
578
  /* @__PURE__ */ jsx("span", { className: "ui-upload-picture-empty-label", children: t("dataEntry.upload.addImage") })
576
579
  ]
577
580
  }
@@ -621,7 +624,7 @@ function Upload({
621
624
  className: "ui-upload-avatar ui-focus-ring",
622
625
  "aria-label": t("dataEntry.upload.avatarLabel"),
623
626
  children: [
624
- !showPlaceholder && item?.previewUrl ? /* @__PURE__ */ jsx("img", { src: item.previewUrl, alt: "", className: "ui-upload-avatar-image" }) : /* @__PURE__ */ jsx("span", { className: "ui-upload-avatar-placeholder", children: /* @__PURE__ */ jsx(Camera, { className: controlIconClass, "aria-hidden": "true" }) }),
627
+ !showPlaceholder && item?.previewUrl ? /* @__PURE__ */ jsx("img", { src: item.previewUrl, alt: "", className: "ui-upload-avatar-image" }) : /* @__PURE__ */ jsx("span", { className: "ui-upload-avatar-placeholder", children: /* @__PURE__ */ jsx(Camera, { className: "ui-upload-avatar-icon", "aria-hidden": "true" }) }),
625
628
  !disabled && /* @__PURE__ */ jsx("span", { className: "ui-upload-avatar-change", children: t("dataEntry.upload.change") })
626
629
  ]
627
630
  }
@@ -750,12 +753,13 @@ function UploadFileList({
750
753
  hasCustomPreview,
751
754
  onDownload,
752
755
  itemRender,
753
- showThumbnails
756
+ listType
754
757
  }) {
755
758
  const { t } = useTranslation();
756
759
  return /* @__PURE__ */ jsx("ul", { className: "ui-stack-xs", children: items.map((item) => {
760
+ const kind = uploadFileKind(item);
757
761
  const node = /* @__PURE__ */ jsxs("div", { className: "ui-upload-row", children: [
758
- showThumbnails && item.previewUrl && /* @__PURE__ */ jsx("img", { src: item.previewUrl, alt: "", className: "ui-upload-list-thumb" }),
762
+ listType === "picture" && (item.previewUrl ? /* @__PURE__ */ jsx("img", { src: item.previewUrl, alt: "", className: "ui-upload-list-thumb" }) : /* @__PURE__ */ jsx("span", { className: "ui-upload-list-glyph", "data-file-kind": kind, children: /* @__PURE__ */ jsx(Icon, { as: UPLOAD_FILE_KIND_GLYPHS[kind], size: "lg" }) })),
759
763
  /* @__PURE__ */ jsxs("div", { className: "ui-upload-row-main", children: [
760
764
  /* @__PURE__ */ jsx("div", { className: "truncate font-medium", children: item.name }),
761
765
  /* @__PURE__ */ jsxs("div", { className: "text-muted-foreground text-xs", children: [
@@ -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 === "ul" ? "disc" : Element === "ol" ? "decimal" : void 0,
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
  ] })
@@ -19,7 +19,7 @@ import type { AvatarProp } from "../../props/components/data-display.prop.js";
19
19
  type ImageLoadingStatus = "idle" | "loading" | "loaded" | "error";
20
20
  export declare const Avatar: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & {
21
21
  asChild?: boolean;
22
- } & Pick<AvatarProp, "appearance" | "presence" | "presenceLabel" | "shape"> & React.RefAttributes<HTMLSpanElement>>;
22
+ } & Pick<AvatarProp, "appearance" | "presence" | "presenceLabel" | "shape" | "size"> & React.RefAttributes<HTMLSpanElement>>;
23
23
  export declare const AvatarImage: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>, "ref"> & {
24
24
  asChild?: boolean;
25
25
  onLoadingStatusChange?: (status: ImageLoadingStatus) => void;
@@ -53,6 +53,7 @@ const Avatar = React.forwardRef(
53
53
  ({
54
54
  className,
55
55
  shape = "circle",
56
+ size = "md",
56
57
  appearance = "default",
57
58
  presence,
58
59
  presenceLabel,
@@ -73,6 +74,7 @@ const Avatar = React.forwardRef(
73
74
  ref,
74
75
  "data-slot": "avatar",
75
76
  "data-shape": shape === "square" ? "square" : void 0,
77
+ "data-size": size === "md" ? void 0 : size,
76
78
  "data-appearance": appearance === "tinted" ? "tinted" : void 0,
77
79
  "data-presence": presence,
78
80
  className: cn("ui-avatar", className),
@@ -42,8 +42,13 @@ export type SegmentedProp = {
42
42
  block?: boolean;
43
43
  /** antd `vertical` — stack the choices in a column. Arrow keys follow the axis. */
44
44
  vertical?: boolean;
45
- /** Control height tier: `md` (default), `sm` or `lg` — the same tiers as every other control. */
46
- size?: "sm" | "md" | "lg";
45
+ /**
46
+ * Control height tier: `md` (default), `xs`, `sm` or `lg` — the same `--control-height-*` tier
47
+ * every other control reads. `xs` is the fourth step (gh#719, the one Toggle/ToggleGroup took in
48
+ * gh#716): a 24px-dense row can carry a real segmented control instead of a hand-rolled row of
49
+ * Buttons.
50
+ */
51
+ size?: "xs" | "sm" | "md" | "lg";
47
52
  /** Disable the whole group. */
48
53
  disabled?: boolean;
49
54
  /** Form field name — submits the selected value with the form. */
@@ -2,7 +2,7 @@ import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  declare const toggleVariants: (props?: ({
4
4
  variant?: "default" | "outline" | null | undefined;
5
- size?: "lg" | "md" | "sm" | null | undefined;
5
+ size?: "lg" | "md" | "sm" | "xs" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  /**
8
8
  * Not exported on its own — it is folded into `ToggleProp` and `ToggleGroupItemProp` so there is
@@ -69,7 +69,7 @@ export type ToggleProp = React.ComponentPropsWithoutRef<"button"> & TogglePresse
69
69
  export type ToggleProps = ToggleProp;
70
70
  export declare const Toggle: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref"> & TogglePressedFields & VariantProps<(props?: ({
71
71
  variant?: "default" | "outline" | null | undefined;
72
- size?: "lg" | "md" | "sm" | null | undefined;
72
+ size?: "lg" | "md" | "sm" | "xs" | null | undefined;
73
73
  } & import("class-variance-authority/types").ClassProp) | undefined) => string> & ToggleCountFields & React.RefAttributes<HTMLButtonElement>>;
74
74
  export { toggleVariants, useCounterPill, restoreDomProps };
75
75
  export type { ToggleCountFields, TogglePressedFields };
@@ -13,6 +13,10 @@ const toggleVariants = cva("ui-toggle", {
13
13
  outline: "ui-toggle-outline"
14
14
  },
15
15
  size: {
16
+ // The fourth step of the shared control ladder (gh#716): 24px, `--control-height-xs`, the
17
+ // same tier `Button size="xs"` reads — so a 24px-dense row can carry a segmented control
18
+ // instead of someone hand-rolling one out of Buttons.
19
+ xs: "ui-toggle-xs",
16
20
  sm: "ui-toggle-sm",
17
21
  md: "ui-toggle-default-size",
18
22
  lg: "ui-toggle-lg"
@@ -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.1",
3
+ "version": "27.5.0",
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,
@@ -208,6 +208,20 @@ export type DescriptionsItemProp = {
208
208
  */
209
209
  export type AvatarProp = React.ComponentPropsWithoutRef<"span"> & {
210
210
  shape?: AvatarShapeProp;
211
+ /**
212
+ * Box size on the shared control ladder — `md` (default) is `--control-height`, and `xs`/`sm`/
213
+ * `lg` are `--control-height-xs/-sm/-lg`, the SAME tier Button and Input read. State it when the
214
+ * mark has to sit in a row the row's height already decided: inside an `icon-sm` trigger, in a
215
+ * 24px dense table row, beside a `size="sm"` Button. Before gh#716 the box was welded to
216
+ * `--control-height`, so a 32px mark inside a 28px trigger overflowed it and the only legal move
217
+ * left was raising the whole row.
218
+ *
219
+ * The initials' type step and a glyph's box move with the box — one step of the type scale and
220
+ * one step of the `--icon-size-*` scale per step — so a small mark is small, not clipped.
221
+ * `shape="square"` rides the identical ladder. Beyond the ladder (a 96px profile mark) is a
222
+ * `className` size utility, exactly as before; the ladder is for the control row.
223
+ */
224
+ size?: SizeProp;
211
225
  appearance?: AvatarAppearanceProp;
212
226
  /**
213
227
  * Presence — WHO is reachable right now, drawn as an indicator at the block-end/inline-end
@@ -1464,9 +1464,34 @@ export type SelectDataProp = SearchSelectBaseProp & {
1464
1464
  export type UploadFileItemProp = UploadFileItem;
1465
1465
  /** @see Upload */
1466
1466
  export type UploadVariantProp = "dropzone" | "button" | "picture-card" | "picture" | "avatar" | "avatar-crop";
1467
+ /**
1468
+ * Upload `listType` — HOW THE CHOSEN FILES ARE LISTED, ported from antd (gh#720).
1469
+ *
1470
+ * antd separates two axes and this component had only one: `variant` says how a file is PICKED
1471
+ * (drag area, button, tile grid, avatar), `listType` says how the picked ones are DRAWN. Welding
1472
+ * the second onto the first meant a `variant="dropzone"` could not show a thumbnail even for an
1473
+ * item that already carried a `previewUrl`, and its rows carried no per-type mark at all — a
1474
+ * drawer listing a `.png` beside a `.json` and a `.txt` could draw only three identical
1475
+ * name-and-size rows.
1476
+ *
1477
+ * - `text` — name, size, actions. The row every dropzone/button list has always drawn.
1478
+ * - `picture` — a leading box on every row: the thumbnail when the item has a `previewUrl`, and
1479
+ * otherwise the glyph for its file kind (image / pdf / archive / text / generic). Both boxes are
1480
+ * the same size, so a mixed list keeps one row height.
1481
+ *
1482
+ * antd's third value, `picture-card`, is `variant="picture-card"` here: in that variant the tile
1483
+ * grid IS the picker (the add tile sits inside the grid), so it is not a listing that can be put
1484
+ * under a different trigger. It is deliberately not offered as a `listType`.
1485
+ */
1486
+ export type UploadListTypeProp = "text" | "picture";
1467
1487
  /** @see Upload — presentational; wire `onUpload` to media-service in app api.ts */
1468
1488
  export type UploadProp = FieldA11yProps & {
1469
1489
  variant?: UploadVariantProp;
1490
+ /**
1491
+ * @see UploadListTypeProp — an axis of its own, independent of `variant`. Defaults to the
1492
+ * listing each variant already drew: `picture` for `variant="picture"`, `text` everywhere else.
1493
+ */
1494
+ listType?: UploadListTypeProp;
1470
1495
  value?: ValueProp<UploadFileItemProp[]>;
1471
1496
  defaultValue?: DefaultValueProp<UploadFileItemProp[]>;
1472
1497
  onValueChange?: OnValueChangeProp<UploadFileItemProp[]>;
@@ -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