@godxjp/ui 18.7.0 → 18.8.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.
@@ -3,9 +3,21 @@ import { type VariantProps } from "class-variance-authority";
3
3
  import type { LucideIcon } from "lucide-react";
4
4
  import type { HeadingLevelProp } from "../../props/vocabulary/index.js";
5
5
  type CardSize = "md" | "compact";
6
- /** Semantic leading-edge accent stripe (border-inline-start; width via the
7
- * --card-accent-rail-width token, default 6px). */
6
+ /** Semantic accent tone. `accentPlacement` decides WHERE it is drawn — a leading-edge
7
+ * stripe (default) or the full perimeter. */
8
8
  type CardAccent = "primary" | "success" | "warning" | "info" | "attention" | "destructive";
9
+ /**
10
+ * Where the semantic `accent` tone is drawn.
11
+ *
12
+ * - `"edge"` (default) — the leading-edge stripe on `border-inline-start` only, at the
13
+ * `--card-accent-rail-width` measure (6px). The classic "this row needs a look" rail.
14
+ * - `"perimeter"` — a full attention border around the WHOLE card in the same semantic
15
+ * tone, measured by `--card-accent-perimeter-width` + `--card-accent-perimeter-ring-width`.
16
+ * This is what `variant="featured"` does, except the tone is yours instead of `--primary`,
17
+ * so a card can shout "action required" (`accent="attention"`) or "this failed"
18
+ * (`accent="destructive"`) without borrowing the brand colour.
19
+ */
20
+ type CardAccentPlacement = "edge" | "perimeter";
9
21
  /** Surface fill — plain card, muted band, borderless outline, or emphasized featured ring. */
10
22
  type CardVariant = "default" | "muted" | "outline" | "featured";
11
23
  /** Padding density — base 16px · tight 12px · cozy 20px. */
@@ -16,6 +28,11 @@ declare const cardVariants: (props?: ({
16
28
  export type CardProps = React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof cardVariants> & {
17
29
  size?: CardSize;
18
30
  accent?: CardAccent;
31
+ /**
32
+ * Where `accent` is drawn — `"edge"` (default, the leading-edge stripe) or `"perimeter"`
33
+ * (a full attention border in the accent tone). Inert without `accent`.
34
+ */
35
+ accentPlacement?: CardAccentPlacement;
19
36
  variant?: CardVariant;
20
37
  density?: CardDensity;
21
38
  };
@@ -24,6 +41,11 @@ export declare const Card: React.ForwardRefExoticComponent<React.HTMLAttributes<
24
41
  } & import("class-variance-authority/types").ClassProp) | undefined) => string> & {
25
42
  size?: CardSize;
26
43
  accent?: CardAccent;
44
+ /**
45
+ * Where `accent` is drawn — `"edge"` (default, the leading-edge stripe) or `"perimeter"`
46
+ * (a full attention border in the accent tone). Inert without `accent`.
47
+ */
48
+ accentPlacement?: CardAccentPlacement;
27
49
  variant?: CardVariant;
28
50
  density?: CardDensity;
29
51
  } & React.RefAttributes<HTMLDivElement>>;
@@ -136,9 +158,11 @@ export type StatCardProps = React.HTMLAttributes<HTMLDivElement> & VariantProps<
136
158
  inverse?: boolean;
137
159
  /** Semantic leading-edge rail (Card accent) — flags a KPI needing attention. */
138
160
  accent?: CardAccent;
161
+ /** Where `accent` is drawn — `"edge"` (default rail) or `"perimeter"` (full attention border). */
162
+ accentPlacement?: CardAccentPlacement;
139
163
  };
140
164
  /** KPI / stat tile — token-driven layout aligned to dashboard KPI cards. */
141
- export declare function StatCard({ label, value, hint, icon: Icon, delta, layout, align, inverse, accent, className, size, ...props }: StatCardProps): React.JSX.Element;
165
+ export declare function StatCard({ label, value, hint, icon: Icon, delta, layout, align, inverse, accent, accentPlacement, className, size, ...props }: StatCardProps): React.JSX.Element;
142
166
  /** Header actions slot — pair with `CardHeader className="flex flex-row …"`. */
143
167
  export declare const CardAction: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
144
168
  export {};
@@ -12,7 +12,7 @@ const cardVariants = cva("group/card", {
12
12
  defaultVariants: { size: "md" }
13
13
  });
14
14
  const Card = React.forwardRef(
15
- ({ className, size = "md", accent, variant, density, ...props }, ref) => /* @__PURE__ */ jsx(
15
+ ({ className, size = "md", accent, accentPlacement, variant, density, ...props }, ref) => /* @__PURE__ */ jsx(
16
16
  "div",
17
17
  {
18
18
  ref,
@@ -20,6 +20,7 @@ const Card = React.forwardRef(
20
20
  "data-slot": "card",
21
21
  "data-size": size === "compact" ? "compact" : void 0,
22
22
  "data-accent": accent,
23
+ "data-accent-placement": accentPlacement === "perimeter" ? "perimeter" : void 0,
23
24
  "data-variant": variant && variant !== "default" ? variant : void 0,
24
25
  "data-density": density,
25
26
  ...props
@@ -108,6 +109,7 @@ function StatCard({
108
109
  align = "start",
109
110
  inverse = false,
110
111
  accent,
112
+ accentPlacement,
111
113
  className,
112
114
  size = "compact",
113
115
  ...props
@@ -118,6 +120,7 @@ function StatCard({
118
120
  {
119
121
  size: size ?? "compact",
120
122
  accent,
123
+ accentPlacement,
121
124
  className: cn("ui-stat-card", className),
122
125
  "data-stat-card": "",
123
126
  "data-stat-layout": layout,
@@ -1,2 +1,2 @@
1
1
  export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from "../ui/input-otp.js";
2
- export type { InputOTPGroupAppearanceProp, InputOTPGroupProp, InputOTPGroupProps, } from "../ui/input-otp.js";
2
+ export type { InputOTPAlignProp, InputOTPGroupAppearanceProp, InputOTPGroupProp, InputOTPGroupProps, } from "../ui/input-otp.js";
@@ -1,4 +1,4 @@
1
1
  import * as React from "react";
2
2
  import type { StepsProp } from "../../props/components/navigation.prop.js";
3
- export type { StepsProp, StepsProp as StepsProps, StepItemProp, StepStatusProp, } from "../../props/components/navigation.prop.js";
4
- export declare function Steps({ items, value: current, defaultValue, status: currentStatus, orientation, type, size, titlePlacement, onValueChange, className, }: StepsProp): React.JSX.Element;
3
+ export type { StepsProp, StepsProp as StepsProps, StepItemProp, StepStatusProp, StepsSeparatorProp, } from "../../props/components/navigation.prop.js";
4
+ export declare function Steps({ items, value: current, defaultValue, status: currentStatus, orientation, type, size, titlePlacement, separator, onValueChange, className, }: StepsProp): React.JSX.Element;
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { Check, ChevronRight, Circle, Loader2, X } from "lucide-react";
3
+ import { ArrowRight, Check, ChevronRight, Circle, Loader2, X } from "lucide-react";
4
4
  import { useTranslation } from "../../i18n/use-translation.js";
5
5
  import { cn } from "../../lib/utils.js";
6
6
  import { controlIconClass } from "../../lib/control-styles.js";
@@ -61,6 +61,7 @@ function Steps({
61
61
  type = "default",
62
62
  size = "md",
63
63
  titlePlacement = "horizontal",
64
+ separator = "chevron",
64
65
  onValueChange,
65
66
  className
66
67
  }) {
@@ -69,6 +70,7 @@ function Steps({
69
70
  const isVertical = orientation === "vertical";
70
71
  const compact = size === "sm";
71
72
  const inline = type === "inline";
73
+ const SeparatorIcon = separator === "arrow" ? ArrowRight : ChevronRight;
72
74
  return /* @__PURE__ */ jsx(
73
75
  "ol",
74
76
  {
@@ -151,7 +153,7 @@ function Steps({
151
153
  }
152
154
  ) : /* @__PURE__ */ jsx("span", { className: stepClassName, children: stepInner }),
153
155
  inline && index < items.length - 1 ? /* @__PURE__ */ jsx(
154
- ChevronRight,
156
+ SeparatorIcon,
155
157
  {
156
158
  className: "ui-steps-inline-separator rtl:rotate-180",
157
159
  "aria-hidden": "true"
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import * as AvatarPrimitive from "@radix-ui/react-avatar";
3
3
  import type { AvatarProp } from "../../props/components/data-display.prop.js";
4
- export declare const Avatar: React.ForwardRefExoticComponent<Omit<AvatarPrimitive.AvatarProps & React.RefAttributes<HTMLSpanElement>, "ref"> & Pick<AvatarProp, "shape"> & React.RefAttributes<HTMLSpanElement>>;
4
+ export declare const Avatar: React.ForwardRefExoticComponent<Omit<AvatarPrimitive.AvatarProps & React.RefAttributes<HTMLSpanElement>, "ref"> & Pick<AvatarProp, "shape" | "appearance"> & React.RefAttributes<HTMLSpanElement>>;
5
5
  export declare const AvatarImage: React.ForwardRefExoticComponent<Omit<AvatarPrimitive.AvatarImageProps & React.RefAttributes<HTMLImageElement>, "ref"> & React.RefAttributes<HTMLImageElement>>;
6
6
  export declare const AvatarFallback: React.ForwardRefExoticComponent<Omit<AvatarPrimitive.AvatarFallbackProps & React.RefAttributes<HTMLSpanElement>, "ref"> & React.RefAttributes<HTMLSpanElement>>;
@@ -3,12 +3,13 @@ import { jsx } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import * as AvatarPrimitive from "@radix-ui/react-avatar";
5
5
  import { cn } from "../../lib/utils.js";
6
- const Avatar = React.forwardRef(({ className, shape = "circle", ...props }, ref) => /* @__PURE__ */ jsx(
6
+ const Avatar = React.forwardRef(({ className, shape = "circle", appearance = "default", ...props }, ref) => /* @__PURE__ */ jsx(
7
7
  AvatarPrimitive.Root,
8
8
  {
9
9
  ref,
10
10
  "data-slot": "avatar",
11
11
  "data-shape": shape === "square" ? "square" : void 0,
12
+ "data-appearance": appearance === "tinted" ? "tinted" : void 0,
12
13
  className: cn("ui-avatar", className),
13
14
  ...props
14
15
  }
@@ -1,5 +1,15 @@
1
1
  import * as React from "react";
2
- export declare const InputOTP: React.ForwardRefExoticComponent<(Omit<Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange" | "value" | "maxLength" | "textAlign" | "onComplete" | "pushPasswordManagerStrategy" | "pasteTransformer" | "containerClassName" | "noScriptCSSFallback"> & {
2
+ import type { InputOTPAlignProp } from "../../props/components/data-entry.prop.js";
3
+ /**
4
+ * InputOTP — the code-challenge field.
5
+ *
6
+ * `align` positions the whole row (groups + separators) on the main axis: `start` (default,
7
+ * unchanged), `center` — the canonical auth challenge — or `end`. The attribute lands on the
8
+ * hidden input because `input-otp` owns the container element; `.ui-otp-container` reads it back
9
+ * through `:has()`, exactly as it already does for the invalid and disabled states. A service that
10
+ * wants every code field centred sets `--otp-container-align` once instead.
11
+ */
12
+ export declare const InputOTP: React.ForwardRefExoticComponent<((Omit<Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange" | "value" | "maxLength" | "textAlign" | "onComplete" | "pushPasswordManagerStrategy" | "pasteTransformer" | "containerClassName" | "noScriptCSSFallback"> & {
3
13
  value?: string;
4
14
  onChange?: (newValue: string) => unknown;
5
15
  maxLength: number;
@@ -25,8 +35,10 @@ export declare const InputOTP: React.ForwardRefExoticComponent<(Omit<Omit<React.
25
35
  } & {
26
36
  render?: never;
27
37
  children: React.ReactNode;
28
- } & React.RefAttributes<HTMLInputElement>, "ref">) & React.RefAttributes<HTMLInputElement>>;
29
- export type { InputOTPGroupAppearanceProp, InputOTPGroupProp, InputOTPGroupProp as InputOTPGroupProps, } from "../../props/components/data-entry.prop.js";
38
+ } & React.RefAttributes<HTMLInputElement>, "ref">) & {
39
+ align?: InputOTPAlignProp;
40
+ }) & React.RefAttributes<HTMLInputElement>>;
41
+ export type { InputOTPAlignProp, InputOTPGroupAppearanceProp, InputOTPGroupProp, InputOTPGroupProp as InputOTPGroupProps, } from "../../props/components/data-entry.prop.js";
30
42
  export declare const InputOTPGroup: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & {
31
43
  appearance?: import("./input-otp.js").InputOTPGroupAppearanceProp;
32
44
  } & React.RefAttributes<HTMLDivElement>>;
@@ -4,11 +4,12 @@ import * as React from "react";
4
4
  import { OTPInput, OTPInputContext } from "input-otp";
5
5
  import { Minus } from "lucide-react";
6
6
  import { cn } from "../../lib/utils.js";
7
- const InputOTP = React.forwardRef(({ className, containerClassName, ...props }, ref) => /* @__PURE__ */ jsx(
7
+ const InputOTP = React.forwardRef(({ className, containerClassName, align, ...props }, ref) => /* @__PURE__ */ jsx(
8
8
  OTPInput,
9
9
  {
10
10
  ref,
11
11
  "data-slot": "input-otp",
12
+ "data-align": align && align !== "start" ? align : void 0,
12
13
  containerClassName: cn("ui-otp-container", containerClassName),
13
14
  className: cn("ui-otp-input", className),
14
15
  ...props
@@ -146,8 +146,8 @@ export declare const EMAIL_GEOMETRY_SOURCE: {
146
146
  readonly "--email-heading-font-size": "17px";
147
147
  readonly "--email-heading-line-height": "1.7";
148
148
  readonly "--email-heading-font-weight": "500";
149
- readonly "--email-cta-height": "36px";
150
- readonly "--email-cta-line-height": "36px";
149
+ readonly "--email-cta-height": "44px";
150
+ readonly "--email-cta-line-height": "44px";
151
151
  readonly "--email-cta-padding-x": "16px";
152
152
  readonly "--email-cta-radius": "6px";
153
153
  readonly "--email-cta-font-size": "14px";
@@ -136,8 +136,8 @@ const EMAIL_GEOMETRY_SOURCE = {
136
136
  "--email-heading-font-size": "17px",
137
137
  "--email-heading-line-height": "1.7",
138
138
  "--email-heading-font-weight": "500",
139
- "--email-cta-height": "36px",
140
- "--email-cta-line-height": "36px",
139
+ "--email-cta-height": "44px",
140
+ "--email-cta-line-height": "44px",
141
141
  "--email-cta-padding-x": "16px",
142
142
  "--email-cta-radius": "6px",
143
143
  "--email-cta-font-size": "14px",
@@ -62,9 +62,25 @@ export type DescriptionsItemProp = {
62
62
  */
63
63
  export type AvatarProp = React.ComponentPropsWithoutRef<"span"> & {
64
64
  shape?: AvatarShapeProp;
65
+ appearance?: AvatarAppearanceProp;
65
66
  className?: ClassNameProp;
66
67
  children?: ChildrenProp;
67
68
  };
69
+ /**
70
+ * Avatar fill treatment.
71
+ *
72
+ * - `"default"` — the identity fill: `--muted` for a person, the solid brand mark for
73
+ * `shape="square"`.
74
+ * - `"tinted"` — the **capability medallion**: a soft role wash behind a role-coloured glyph.
75
+ * This is the plate a capability/feature icon sits on (`shape="square" appearance="tinted"` for
76
+ * the canonical rounded square). It exists because the medallion is a composition — `Avatar` +
77
+ * a Lucide glyph, per docs/COMPOSITION-VS-COMPONENT.md — but its *tint* was not reachable from
78
+ * a token, so consumers were re-deriving `hsl(var(--primary) / 0.1)` in page CSS or giving up
79
+ * and rendering a bare glyph. Retune with `--avatar-tinted-*`.
80
+ *
81
+ * @see Avatar
82
+ */
83
+ export type AvatarAppearanceProp = "default" | "tinted";
68
84
  /** @see Badge */
69
85
  export type BadgeProp = {
70
86
  variant?: "default" | "secondary" | "outline";
@@ -16,6 +16,13 @@ export type InputOTPGroupAppearanceProp = "slots" | "grouped";
16
16
  export type InputOTPGroupProp = React.HTMLAttributes<HTMLDivElement> & {
17
17
  appearance?: InputOTPGroupAppearanceProp;
18
18
  };
19
+ /**
20
+ * Main-axis alignment of the whole code row (groups + separators) inside its container.
21
+ * `start` is the historical default. A centred challenge is the common auth case and used to
22
+ * force every consumer to wrap `.ui-otp-container` in their own flex-centring div.
23
+ * @see InputOTP
24
+ */
25
+ export type InputOTPAlignProp = "start" | "center" | "end";
19
26
  /** @see Input */
20
27
  export type InputProp = React.InputHTMLAttributes<HTMLInputElement> & {
21
28
  /** Show an inline ✕ that clears the field while it holds text (default false). */
@@ -144,9 +144,18 @@ export type StepsProp = {
144
144
  type?: "default" | "dot" | "inline";
145
145
  size?: "md" | "sm";
146
146
  titlePlacement?: "horizontal" | "vertical";
147
+ /**
148
+ * The glyph between inline steps (`type="inline"` only). `chevron` (default, `›`) is the
149
+ * breadcrumb-flavoured original. `arrow` (`→`) is the canonical hosted-identity progression
150
+ * marker: a chevron reads as "drill into", an arrow reads as "then" — which is what a step row
151
+ * means. Ignored by every other `type`.
152
+ */
153
+ separator?: StepsSeparatorProp;
147
154
  onValueChange?: (value: number) => void;
148
155
  className?: ClassNameProp;
149
156
  };
157
+ /** @see Steps — inline separator glyph. */
158
+ export type StepsSeparatorProp = "chevron" | "arrow";
150
159
  /** Tab pane — Ant Design `items` entry. */
151
160
  export type TabItemProp = {
152
161
  value: string;
@@ -1063,6 +1063,11 @@ export declare const COMPONENT_PROP_REGISTRY: {
1063
1063
  readonly file: "components/data-display.prop.ts";
1064
1064
  readonly vocabulary: readonly ["AvatarShapeProp", "ChildrenProp", "ClassNameProp"];
1065
1065
  };
1066
+ readonly AvatarAppearanceProp: {
1067
+ readonly group: "data-display";
1068
+ readonly file: "components/data-display.prop.ts";
1069
+ readonly vocabulary: readonly [];
1070
+ };
1066
1071
  readonly BadgeProp: {
1067
1072
  readonly group: "data-display";
1068
1073
  readonly file: "components/data-display.prop.ts";
@@ -1474,6 +1479,11 @@ export declare const COMPONENT_PROP_REGISTRY: {
1474
1479
  readonly file: "components/data-entry.prop.ts";
1475
1480
  readonly vocabulary: readonly [];
1476
1481
  };
1482
+ readonly InputOTPAlignProp: {
1483
+ readonly group: "data-entry";
1484
+ readonly file: "components/data-entry.prop.ts";
1485
+ readonly vocabulary: readonly [];
1486
+ };
1477
1487
  readonly StepItemProp: {
1478
1488
  readonly group: "navigation";
1479
1489
  readonly file: "components/navigation.prop.ts";
@@ -1484,6 +1494,11 @@ export declare const COMPONENT_PROP_REGISTRY: {
1484
1494
  readonly file: "components/navigation.prop.ts";
1485
1495
  readonly vocabulary: readonly [];
1486
1496
  };
1497
+ readonly StepsSeparatorProp: {
1498
+ readonly group: "navigation";
1499
+ readonly file: "components/navigation.prop.ts";
1500
+ readonly vocabulary: readonly [];
1501
+ };
1487
1502
  readonly TabsProp: {
1488
1503
  readonly group: "navigation";
1489
1504
  readonly file: "components/navigation.prop.ts";
@@ -1224,6 +1224,11 @@ const COMPONENT_PROP_REGISTRY = {
1224
1224
  file: "components/data-display.prop.ts",
1225
1225
  vocabulary: ["AvatarShapeProp", "ChildrenProp", "ClassNameProp"]
1226
1226
  },
1227
+ AvatarAppearanceProp: {
1228
+ group: "data-display",
1229
+ file: "components/data-display.prop.ts",
1230
+ vocabulary: []
1231
+ },
1227
1232
  BadgeProp: {
1228
1233
  group: "data-display",
1229
1234
  file: "components/data-display.prop.ts",
@@ -1626,12 +1631,22 @@ const COMPONENT_PROP_REGISTRY = {
1626
1631
  file: "components/data-entry.prop.ts",
1627
1632
  vocabulary: []
1628
1633
  },
1634
+ InputOTPAlignProp: {
1635
+ group: "data-entry",
1636
+ file: "components/data-entry.prop.ts",
1637
+ vocabulary: []
1638
+ },
1629
1639
  StepItemProp: {
1630
1640
  group: "navigation",
1631
1641
  file: "components/navigation.prop.ts",
1632
1642
  vocabulary: ["TitleProp", "SubtitleProp", "DescriptionProp", "IconProp", "DisabledProp"]
1633
1643
  },
1634
1644
  StepStatusProp: { group: "navigation", file: "components/navigation.prop.ts", vocabulary: [] },
1645
+ StepsSeparatorProp: {
1646
+ group: "navigation",
1647
+ file: "components/navigation.prop.ts",
1648
+ vocabulary: []
1649
+ },
1635
1650
  TabsProp: {
1636
1651
  group: "navigation",
1637
1652
  file: "components/navigation.prop.ts",
@@ -51,9 +51,16 @@
51
51
  background: transparent;
52
52
  }
53
53
 
54
+ /* `featured` = the brand-toned perimeter. Its colour is a role-mirror knob (rule #45): declared
55
+ * `initial` in the token layer so the --primary default resolves HERE, at the call site, and a
56
+ * scoped [data-tenant]/.dark override of --primary still reaches it. A service that wants the
57
+ * featured ring in a non-brand tone overrides --card-featured-border-color; a card that wants a
58
+ * SEMANTIC perimeter (attention / destructive / success …) uses
59
+ * `accent="…" accentPlacement="perimeter"` instead — same geometry, tone-owned. */
54
60
  [data-slot="card"][data-variant="featured"] {
55
- border-color: hsl(var(--primary));
56
- box-shadow: 0 0 0 1px hsl(var(--primary));
61
+ border-color: var(--card-featured-border-color, hsl(var(--primary)));
62
+ box-shadow: 0 0 0 var(--card-featured-ring-width)
63
+ var(--card-featured-border-color, hsl(var(--primary)));
57
64
  }
58
65
 
59
66
  /* ── Density — override the shell inset/body tokens ──────────────────────
@@ -72,33 +79,39 @@
72
79
  --card-space-body-y: var(--space-5);
73
80
  }
74
81
 
75
- /* ── Accent edgesemantic left stripe (token width), content on shell ─ */
76
- [data-slot="card"][data-accent] {
77
- border-inline-start-width: var(--card-accent-rail-width);
78
- }
79
-
82
+ /* ── Accent toneONE resolved colour per semantic accent ─────────────
83
+ * `--card-accent-color` is RESOLVED from the card's own `data-accent`, so both placements read
84
+ * the same value and a new tone is one line. It is deliberately NOT a service knob: it is set on
85
+ * the element, so a :root / [data-tenant] override could never reach it. Retint the ROLE
86
+ * (--attention, --success, …) — that is what a role is for. */
80
87
  [data-slot="card"][data-accent="primary"] {
81
- border-inline-start-color: hsl(var(--primary));
88
+ --card-accent-color: hsl(var(--primary));
82
89
  }
83
90
 
84
91
  [data-slot="card"][data-accent="success"] {
85
- border-inline-start-color: hsl(var(--success));
92
+ --card-accent-color: hsl(var(--success));
86
93
  }
87
94
 
88
95
  [data-slot="card"][data-accent="warning"] {
89
- border-inline-start-color: hsl(var(--warning));
96
+ --card-accent-color: hsl(var(--warning));
90
97
  }
91
98
 
92
99
  [data-slot="card"][data-accent="info"] {
93
- border-inline-start-color: hsl(var(--info));
100
+ --card-accent-color: hsl(var(--info));
94
101
  }
95
102
 
96
103
  [data-slot="card"][data-accent="attention"] {
97
- border-inline-start-color: hsl(var(--attention));
104
+ --card-accent-color: hsl(var(--attention));
98
105
  }
99
106
 
100
107
  [data-slot="card"][data-accent="destructive"] {
101
- border-inline-start-color: hsl(var(--destructive));
108
+ --card-accent-color: hsl(var(--destructive));
109
+ }
110
+
111
+ /* ── Accent placement `edge` (DEFAULT) — semantic left stripe (token width), content on shell ─ */
112
+ [data-slot="card"][data-accent] {
113
+ border-inline-start-width: var(--card-accent-rail-width);
114
+ border-inline-start-color: var(--card-accent-color);
102
115
  }
103
116
 
104
117
  [data-slot="card"][data-accent] > [data-slot="card-header"],
@@ -107,6 +120,29 @@
107
120
  padding-inline-start: calc(var(--card-space-inset) - var(--card-accent-rail-width));
108
121
  }
109
122
 
123
+ /* ── Accent placement `perimeter` — the full attention border (gh#12) ───
124
+ * The whole edge carries the semantic tone instead of one rail, so a card can read as
125
+ * "action required" / "failed" at a glance without borrowing `--primary` the way
126
+ * `variant="featured"` does. Same optical weight as `featured` (border + 1px ring), tone-owned.
127
+ * Because the rail is gone, the slot padding compensation above is undone — content returns to
128
+ * the normal shell column, so switching placement never shifts the text. */
129
+ [data-slot="card"][data-accent][data-accent-placement="perimeter"] {
130
+ border-width: var(--card-accent-perimeter-width);
131
+ border-color: var(--card-accent-color);
132
+ box-shadow:
133
+ 0 0 0 var(--card-accent-perimeter-ring-width) var(--card-accent-color),
134
+ var(--card-shadow),
135
+ var(--card-glow);
136
+ }
137
+
138
+ [data-slot="card"][data-accent][data-accent-placement="perimeter"] > [data-slot="card-header"],
139
+ [data-slot="card"][data-accent][data-accent-placement="perimeter"]
140
+ > [data-slot="card-content"]:not([data-flush]),
141
+ [data-slot="card"][data-accent][data-accent-placement="perimeter"]
142
+ > [data-slot="card-footer"]:not([data-flush]) {
143
+ padding-inline-start: var(--card-space-inset);
144
+ }
145
+
110
146
  /* ── Header ──────────────────────────────────────────────────────────── */
111
147
  [data-slot="card-header"] {
112
148
  display: flex;
@@ -714,8 +714,18 @@
714
714
  .ui-otp-container {
715
715
  display: flex;
716
716
  align-items: center;
717
+ justify-content: var(--otp-container-align, flex-start);
717
718
  gap: var(--space-2);
718
719
  }
720
+ /* `align` on InputOTP lands on the hidden <input> (input-otp owns the container element), so the
721
+ * container reads it through :has() — the same mechanism this file already uses for the invalid
722
+ * and disabled states. Specificity puts the per-instance prop above the theme token. */
723
+ .ui-otp-container:has(.ui-otp-input[data-align="center"]) {
724
+ justify-content: center;
725
+ }
726
+ .ui-otp-container:has(.ui-otp-input[data-align="end"]) {
727
+ justify-content: flex-end;
728
+ }
719
729
  .ui-otp-input:disabled {
720
730
  cursor: not-allowed;
721
731
  }
@@ -726,8 +736,8 @@
726
736
  .ui-otp-slot {
727
737
  position: relative;
728
738
  display: flex;
729
- width: var(--otp-slot-size, var(--control-height));
730
- height: var(--otp-slot-size, var(--control-height));
739
+ width: var(--otp-slot-inline-size, var(--otp-slot-size, var(--control-height)));
740
+ height: var(--otp-slot-block-size, var(--otp-slot-size, var(--control-height)));
731
741
  align-items: center;
732
742
  justify-content: center;
733
743
  font-size: var(--font-size-base);
@@ -30,6 +30,22 @@
30
30
  color: var(--avatar-square-foreground, hsl(var(--primary-foreground)));
31
31
  }
32
32
 
33
+ /* CAPABILITY MEDALLION — `<Avatar appearance="tinted">` (gh#12). The tinted companion of the
34
+ * solid entity mark: same box, same radius, a soft role wash instead of a solid fill and the
35
+ * glyph in the role itself. Declared AFTER the square rule (equal specificity) so it retints
36
+ * either shape. The glyph rule is scoped to this appearance ON PURPOSE — a global
37
+ * `.ui-avatar svg` would outrank the per-call-site icon classes existing avatars already use. */
38
+ .ui-avatar[data-appearance="tinted"] {
39
+ --avatar-background: var(--avatar-tinted-background, hsl(var(--primary) / 0.1));
40
+ color: var(--avatar-tinted-foreground, hsl(var(--primary)));
41
+ }
42
+
43
+ .ui-avatar[data-appearance="tinted"] svg {
44
+ inline-size: var(--avatar-tinted-glyph-size);
45
+ block-size: var(--avatar-tinted-glyph-size);
46
+ flex: none;
47
+ }
48
+
33
49
  .ui-avatar-image {
34
50
  width: 100%;
35
51
  height: 100%;
@@ -394,10 +394,15 @@
394
394
  box-shadow: 0 0 0 var(--focus-ring-width) hsl(var(--focus-ring-color, var(--ring)));
395
395
  }
396
396
 
397
+ /* The step NUMBER carries the emphasis. Both knobs exist because the canonical hosted-identity
398
+ * row marks the step with an accent TINT at normal weight, while the original marked it with
399
+ * bold at the inherited colour — a service picks its own emphasis instead of forking this rule
400
+ * (rule #45). Defaults are the original bold + inherited colour, so nothing moves. */
397
401
  .ui-steps-inline-index {
398
402
  flex: none;
399
403
  font-variant-numeric: tabular-nums;
400
- font-weight: var(--font-weight-bold);
404
+ font-weight: var(--steps-inline-index-font-weight);
405
+ color: var(--steps-inline-index-color, currentColor);
401
406
  }
402
407
 
403
408
  .ui-steps-inline-title {
@@ -418,8 +423,7 @@
418
423
  color: hsl(var(--destructive));
419
424
  }
420
425
 
421
- .ui-steps-inline-item[data-status="wait"] .ui-steps-inline-control,
422
- .ui-steps-inline-separator {
426
+ .ui-steps-inline-item[data-status="wait"] .ui-steps-inline-control {
423
427
  color: hsl(var(--muted-foreground));
424
428
  }
425
429
 
@@ -427,5 +431,6 @@
427
431
  width: var(--steps-inline-separator-size);
428
432
  height: var(--steps-inline-separator-size);
429
433
  flex: none;
434
+ color: var(--steps-inline-separator-color, hsl(var(--muted-foreground)));
430
435
  }
431
436
  }
@@ -192,9 +192,14 @@
192
192
  text-align: center;
193
193
  }
194
194
 
195
+ /* Device-authorization also owns its CODE FIELD (gh#12): the preset's subject is the code, so
196
+ * the slot box comes from the preset instead of the generic square control tier. Scoped on the
197
+ * shell so every OTP inside the device flow inherits it; a per-instance override still wins. */
195
198
  .ui-auth-shell[data-preset="device-authorization"] {
196
199
  --auth-shell-card-max-width: var(--auth-shell-device-card-max-width);
197
200
  --auth-shell-main-padding: var(--auth-shell-device-main-padding);
201
+ --otp-slot-inline-size: var(--auth-shell-device-otp-slot-inline-size);
202
+ --otp-slot-block-size: var(--auth-shell-device-otp-slot-block-size);
198
203
  }
199
204
 
200
205
  .ui-auth-shell[data-preset="context-selection"] {
@@ -519,7 +524,11 @@
519
524
  }
520
525
 
521
526
  /* AppShell-owned mobile drawer trigger — hidden on desktop, shown once the docked sidebar
522
- * collapses out below `lg` (gh#165). The drawer nav itself is a focus-trapped Sheet. */
527
+ * collapses out at 900px, THE ONE AppShell breakpoint (gh#165, gh#259). NOT Tailwind's `lg`
528
+ * (1024px): this comment said "below lg" for six minors while the shipped rule below and the
529
+ * hamburger's own `max-[900px]:inline-flex` both fired at 900 — see the block comment on the
530
+ * `@media (width <= 56.25rem)` rule for why 900 is the canonical value. The drawer nav itself is
531
+ * a focus-trapped Sheet. */
523
532
  .app-mobile-nav-trigger {
524
533
  display: none;
525
534
  flex: 0 0 auto;
@@ -74,6 +74,18 @@
74
74
  * The slot padding compensation in card-layout.css subtracts the same token,
75
75
  * so content stays aligned on the shell whatever the rail width. */
76
76
  --card-accent-rail-width: 6px;
77
+ /* Accent placement `perimeter` (gh#12) — the FULL attention border. Two knobs so a service can
78
+ * tune the edge weight and the outer ring independently; the defaults reproduce the optical
79
+ * weight of `variant="featured"` (1px border + 1px ring) in the card's own semantic accent tone
80
+ * instead of --primary. Structural 1px literals, like the base card hairline. */
81
+ --card-accent-perimeter-width: 1px;
82
+ --card-accent-perimeter-ring-width: 1px;
83
+ /* `variant="featured"` edge — role-mirror knob (docs/TOKENS.md). `initial` so the --primary
84
+ * default resolves at the CALL SITE and a scoped [data-tenant]/.dark override of --primary
85
+ * reaches it; a service points it anywhere (e.g. var(--attention)) to retint every featured card
86
+ * at once. Default = hsl(var(--primary)). */
87
+ --card-featured-border-color: initial;
88
+ --card-featured-ring-width: 1px;
77
89
  --stat-card-label-font-size: var(--font-size-xs);
78
90
  --stat-card-label-font-weight: var(--font-weight-medium);
79
91
  --stat-card-label-letter-spacing: 0.04em;
@@ -67,6 +67,22 @@
67
67
  * re-scopes --control-height to 36px, would silently shrink. Verified in Chromium: 36px before
68
68
  * and after. A service opts in with a NAMED tier (`var(--control-height-lg)`), never a calc. */
69
69
  --otp-slot-size: initial; /* default = var(--control-height) at the call site */
70
+ /* Per-AXIS overrides of the slot box (gh#12). --otp-slot-size stays the square shorthand; these
71
+ * two win over it when set, so a code field can be WIDER-than-tall or TALLER-than-wide without
72
+ * abandoning the token. A canonical device-grant code field is 27.5x52 per slot (a 4-slot
73
+ * `appearance="grouped"` box of 112x54 with its 1px group border) — not expressible with one
74
+ * square knob, which is why grouped OTP measured 146x38 against that artboard. `initial` for the
75
+ * same tier-mirror reason as --otp-slot-size: the whole fallback chain
76
+ * (axis → square → --control-height) must resolve at the CALL SITE so a shell that re-scopes
77
+ * --control-height still reaches an OTP row that sets neither axis. */
78
+ --otp-slot-inline-size: initial; /* default = var(--otp-slot-size, var(--control-height)) */
79
+ --otp-slot-block-size: initial; /* default = var(--otp-slot-size, var(--control-height)) */
80
+ /* Main-axis alignment of the whole OTP row (rule #44/#45 — chrome is a token, default quiet).
81
+ * `flex-start` is the historical behaviour, so an existing field is byte-identical; a centred
82
+ * code field is `align="center"` on InputOTP (which wins over this knob) or this token set once
83
+ * in a service theme. It exists because EVERY consumer was wrapping .ui-otp-container in a
84
+ * flex-centring div to get a centred challenge. */
85
+ --otp-container-align: flex-start;
70
86
 
71
87
  /* Checked/on/active fills — `initial` so the --primary default re-resolves at the call site
72
88
  * under a scoped theme (a :root binding to var(--primary) freezes at the :root value and a scoped
@@ -28,6 +28,18 @@
28
28
  --avatar-square-background: initial;
29
29
  --avatar-square-foreground: initial;
30
30
 
31
+ /* Capability medallion — `<Avatar appearance="tinted">` (gh#12): the tinted plate a capability /
32
+ feature glyph sits on, as opposed to the SOLID entity mark above. Pair it with shape="square"
33
+ for the canonical rounded square. --avatar-tinted-background / --avatar-tinted-foreground are
34
+ role-mirror knobs (`initial`, docs/TOKENS.md) so the --primary defaults resolve at the CALL
35
+ SITE and a scoped [data-tenant]/.dark override of --primary reaches them — the exact reason
36
+ this is a token and not the `hsl(var(--primary) / 0.1)` literal consumers were writing in page
37
+ CSS. Defaults = hsl(var(--primary) / 0.1) wash · hsl(var(--primary)) glyph ·
38
+ --control-icon-size glyph box. */
39
+ --avatar-tinted-background: initial;
40
+ --avatar-tinted-foreground: initial;
41
+ --avatar-tinted-glyph-size: var(--control-icon-size);
42
+
31
43
  /* Progress track + fill — `initial` so the role defaults re-resolve under a scoped theme.
32
44
  Track reads --secondary, fill reads --success; a service re-tones once.
33
45
  Defaults = hsl(var(--secondary)) track · hsl(var(--success)) fill. */
@@ -63,8 +63,14 @@
63
63
  --email-heading-font-weight: 500; /* mirrors --font-weight-medium — an email title is calm, not bold */
64
64
 
65
65
  /* ── Primary CTA — the one action button ────────────────────────────────────────────────── */
66
- --email-cta-height: 36px; /* mirrors --control-height-lg (2.25rem) the canonical CTA box */
67
- --email-cta-line-height: 36px; /* equal to the height: bulletproof vertical centring in Outlook */
66
+ /* TOUCH TARGET, deliberately DECOUPLED from --control-height-lg (36px). Do not "restore" the
67
+ * mirror: email is a mobile-first, touch-only medium. There is no hover state, no precise
68
+ * pointer, and mail clients do not reliably offer zoom or a focus affordance — so the web's AA
69
+ * floor (SC 2.5.8, 24x24, which 36px clears) is the wrong bar here. 44px is the AAA target size
70
+ * (SC 2.5.5, 44x44) and the Apple HIG 44pt / Material 48dp convention. A service that must keep
71
+ * the old box sets --email-cta-height + --email-cta-line-height in its own theme. */
72
+ --email-cta-height: 44px;
73
+ --email-cta-line-height: 44px; /* equal to the height: bulletproof vertical centring in Outlook */
68
74
  --email-cta-padding-x: 16px; /* mirrors --space-4 */
69
75
  --email-cta-radius: 6px; /* mirrors the --radius base (0.375rem) */
70
76
  --email-cta-font-size: 14px; /* mirrors --font-size-base */
@@ -14,6 +14,15 @@
14
14
  --steps-inline-item-gap: var(--space-1);
15
15
  --steps-inline-font-size: var(--font-size-xs);
16
16
  --steps-inline-separator-size: var(--control-icon-size-sm);
17
+ /* Inline-step emphasis (gh#12). The step number's weight and tint are separate knobs so a
18
+ * service can express progression the canonical way — an accent TINT at normal weight — instead
19
+ * of the original bold. --steps-inline-index-color / --steps-inline-separator-color are
20
+ * role-mirror knobs (docs/TOKENS.md): `initial` so their defaults (the inherited step colour and
21
+ * the --muted-foreground role) resolve at the CALL SITE and a scoped [data-tenant]/.dark
22
+ * override still reaches them. Defaults reproduce today's row byte for byte. */
23
+ --steps-inline-index-font-weight: var(--font-weight-bold);
24
+ --steps-inline-index-color: initial; /* default = currentColor at the call site */
25
+ --steps-inline-separator-color: initial; /* default = hsl(var(--muted-foreground)) */
17
26
 
18
27
  /* AppSettingPicker `compact` (gh#217) — the small, content-hugging labelled trigger used in an
19
28
  * auth/legal footer, where the square icon-only default reads as a stray button and the full
@@ -81,10 +81,21 @@
81
81
  --topbar-height: auto;
82
82
  --topbar-inset: 0px;
83
83
  --topbar-gap: var(--space-2);
84
- /* At compact desktop/tablet widths the docked sidebar leaves too little inline room for three
85
- * intrinsically-sized clusters. The package hides the optional center slot before it can cover
86
- * the breadcrumb/title or end utilities. Consumers may opt back into another display strategy
87
- * (for example `flex`) once their center content has its own compact presentation. */
84
+ /* THIS KNOB DELETES CONTENT AT 1100px AND BELOW read before you ship a center slot.
85
+ * At compact desktop/tablet widths the docked sidebar leaves too little inline room for three
86
+ * intrinsically-sized clusters, so the package hides the optional center slot before it can
87
+ * cover the breadcrumb/title or end utilities (gh#244). The default is `none`, which means a
88
+ * global search trigger placed in `center` is INVISIBLE from 1100px down — including on every
89
+ * phone — unless the consumer opts back in. That default arrived in 18.6.0 and removed the slot
90
+ * for consumers who never changed a line of their own code (gh#12); it stays because the
91
+ * overlap it prevents is a real defect and flipping a shipped default twice is worse than
92
+ * documenting it once, but it is a DECISION, not an accident:
93
+ *
94
+ * :root { --topbar-center-compact-display: flex; } ← restore the slot at every width
95
+ *
96
+ * Opt back in only once the center content has a compact presentation of its own (an icon-only
97
+ * search trigger, a collapsing field). A page-local media query is the anti-pattern this knob
98
+ * replaces. */
88
99
  --topbar-center-compact-display: none;
89
100
  --org-switcher-trigger-height: 2.75rem;
90
101
  --org-switcher-trigger-padding-x: var(--space-2);
@@ -181,6 +192,18 @@
181
192
  --auth-shell-device-card-max-width: 23.75rem;
182
193
  --auth-shell-device-main-padding: 1rem;
183
194
  --auth-shell-device-main-padding-mobile: 0.9375rem 0.3125rem;
195
+ /* CODE FIELD measure — the preset owns it (gh#12). A device grant is the one canonical flow whose
196
+ * PRIMARY control is the code field, so leaving it on the generic square --otp-slot-size made the
197
+ * preset own the page but not its own subject: two 4-slot `appearance="grouped"` boxes rendered
198
+ * 146x38 (4 x the canonical 36px control tier + the 1px group border) against a 112x54 artboard.
199
+ * These are the per-slot boxes that ADD UP to that artboard group:
200
+ * inline 27.5px x 4 + 2 x 1px group border = 112px
201
+ * block 52px + 2 x 1px group border = 54px
202
+ * Literal artboard lengths, like every other preset measure here — they must not drift with
203
+ * --scaling. A service that groups its code differently overrides --otp-slot-inline-size /
204
+ * --otp-slot-block-size on its own field; the generic square knob is untouched everywhere else. */
205
+ --auth-shell-device-otp-slot-inline-size: 1.71875rem;
206
+ --auth-shell-device-otp-slot-block-size: 3.25rem;
184
207
 
185
208
  /* Context-selection preset — 25rem card measure on desktop/tablet, edge-to-edge on mobile
186
209
  * (0 inline gutter) with a 16px rhythm between the intro, the card and the remember row. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "18.7.0",
4
- "godxUiMcp": "18.7.0",
3
+ "version": "18.8.0",
4
+ "godxUiMcp": "18.8.0",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.29.1",
7
7
  "pnpm": {