@godxjp/ui 33.3.0 → 33.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,7 +18,7 @@ npm i @godxjp/ui
18
18
 
19
19
  <!-- agent-catalog-counts:start -->
20
20
 
21
- The catalog — 166 components, 2230 tokens, 44 cardinal rules — is published in two forms from one
21
+ The catalog — 166 components, 2232 tokens, 44 cardinal rules — is published in two forms from one
22
22
  source. **Entry point for either: [`AGENTS.md`](AGENTS.md).**
23
23
  <!-- agent-catalog-counts:end -->
24
24
 
@@ -56,7 +56,7 @@ static files straight from this public repo — no hosting, no deploy step:
56
56
  | [`agent/components-index.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json) | 50 KB | all 166 as name + group + tagline — **fetch this first** |
57
57
  | [`agent/components/<Name>.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json) | 1 KB–37 KB | one file per component, each with its `importPath` — **this is the route to take** |
58
58
  | [`agent/components.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json) | 1.3 MB | all 166 entries in one file — most URL fetchers truncate this silently; prefer the per-component files |
59
- | [`agent/tokens.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json) | 857 KB | all 2230 design tokens, their values and why they exist |
59
+ | [`agent/tokens.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json) | 858 KB | all 2232 design tokens, their values and why they exist |
60
60
  | [`agent/vocabulary.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/vocabulary.json) | 7 KB | the controlled prop vocabulary |
61
61
  | [`agent/rules.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json) | 23 KB | the 44 cardinal rules |
62
62
  <!-- prettier-ignore-end -->
@@ -3,7 +3,7 @@
3
3
  You are about to write code against a design system you did not author. This file is the whole
4
4
  contract. Read it before you write JSX.
5
5
 
6
- **This catalog describes `@godxjp/ui` 33.3.0.** If the project you are editing has a different
6
+ **This catalog describes `@godxjp/ui` 33.4.0.** If the project you are editing has a different
7
7
  version in its `package.json`, read the pinned catalog for THAT version instead
8
8
  (`…/v<their-version>/agent/…`). A catalog newer than the installed package describes props that do
9
9
  not exist yet; older, and it hides props that do. Neither failure announces itself.
@@ -61,10 +61,10 @@ screen anatomy, the visual target the rules below do not give you. Then fetch in
61
61
  its `importPath`, and its examples. Fetch only the handful you picked in step 1.
62
62
  3. `rules.json` — 44 cardinal rules. The ones about raw HTML and hardcoded colour are not
63
63
  style advice.
64
- 4. `tokens.json` — 2230 design tokens, each tagged with its `tier`. **If you were handed a
64
+ 4. `tokens.json` — 2232 design tokens, each tagged with its `tier`. **If you were handed a
65
65
  brand, read the 216 `foundation` entries first** — `--primary`, `--background`,
66
66
  `--radius`, `--font-size-base` are the handful everything else derives from. The
67
- 1901 `component` entries are per-part knobs; reach for one only when a role is
67
+ 1903 `component` entries are per-part knobs; reach for one only when a role is
68
68
  right everywhere except one component.
69
69
  5. `anti-ai-tells.json` — 35 shapes that make generated UI look generated, each with the
70
70
  fix. Read before you reach for a gradient hero or a wall of coloured chips.
@@ -150,7 +150,7 @@ has stopped following the brand.
150
150
  |---|---|---|---|
151
151
  | `foundation` | 216 | the seeds — `--primary`, `--background`, `--foreground`, `--radius`, `--font-size-base`, `--shadow-color`. Everything below is derived from these | **yes — this is the main road.** The default `--primary` is a neutral placeholder: set it (`:root { --primary: <H> <S>% <L>%; }`, HSL components, no `hsl()` wrapper) or import a theme preset such as `@godxjp/ui/themes/godx.css` |
152
152
  | `semantic` | 113 | named roles that follow the seeds — `--ring`, `--text-link`, `--primary-hover`, `--overlay-background` | only when the seed is right and ONE role must differ. That role then stops following a later brand change |
153
- | `component` | 1901 | per-part knobs, `--{component}-{part}-{property}` | rarely. Most are declared `initial` with the real default at the call site — deliberate, so a scoped override re-resolves instead of freezing at `:root` |
153
+ | `component` | 1903 | per-part knobs, `--{component}-{part}-{property}` | rarely. Most are declared `initial` with the real default at the call site — deliberate, so a scoped override re-resolves instead of freezing at `:root` |
154
154
 
155
155
  A token whose `value` is `initial` is not empty and not broken: `initial` is the guaranteed-invalid
156
156
  value, so the real default is computed where the element paints it. Set it and yours wins.
@@ -20,7 +20,7 @@
20
20
  "type": "BreakpointProp"
21
21
  },
22
22
  {
23
- "description": "Overlaid count; does not change the bar cell width. Rendered under asChild too (gh#1171): the pill goes inside the child element after its children, so `<TopbarItem asChild badge={3}><a href=\"/notifications\">…</a></TopbarItem>` keeps its count.",
23
+ "description": "Overlaid count; does not change the bar cell width. Rendered under asChild too (gh#1171): the pill goes inside the child element after its children, so `<TopbarItem asChild badge={3}><a href=\"/notifications\">…</a></TopbarItem>` keeps its count. Nothing renders for `undefined`, `\"\"` or the number `0` (gh#1341) — pass the count as it is, never `count === 0 ? undefined : count`; pass the string `\"0\"` to show a zero on purpose.",
24
24
  "name": "badge",
25
25
  "type": "ReactNode"
26
26
  },
@@ -1791,7 +1791,7 @@
1791
1791
  "type": "BreakpointProp"
1792
1792
  },
1793
1793
  {
1794
- "description": "Overlaid count; does not change the bar cell width. Rendered under asChild too (gh#1171): the pill goes inside the child element after its children, so `<TopbarItem asChild badge={3}><a href=\"/notifications\">…</a></TopbarItem>` keeps its count.",
1794
+ "description": "Overlaid count; does not change the bar cell width. Rendered under asChild too (gh#1171): the pill goes inside the child element after its children, so `<TopbarItem asChild badge={3}><a href=\"/notifications\">…</a></TopbarItem>` keeps its count. Nothing renders for `undefined`, `\"\"` or the number `0` (gh#1341) — pass the count as it is, never `count === 0 ? undefined : count`; pass the string `\"0\"` to show a zero on purpose.",
1795
1795
  "name": "badge",
1796
1796
  "type": "ReactNode"
1797
1797
  },
package/agent/index.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "components": 166,
5
5
  "patterns": 28,
6
6
  "rules": 44,
7
- "tokens": 2230,
7
+ "tokens": 2232,
8
8
  "vocabulary": 14
9
9
  },
10
10
  "design": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/DESIGN.md",
@@ -54,19 +54,19 @@
54
54
  "note": "Pin to the tag that matches the @godxjp/ui version you installed. A catalog newer than your package describes props you do not have; older, and it hides props you do.",
55
55
  "read": {
56
56
  "live": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json",
57
- "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v33.3.0/agent/index.json"
57
+ "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v33.4.0/agent/index.json"
58
58
  },
59
59
  "source": "mcp/src/data — the same data @godxjp/ui-mcp serves — plus the foundation and semantic token tiers, read from src/tokens/*.css",
60
60
  "start": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md",
61
61
  "tokenTiers": {
62
62
  "component": "per-part knobs, --{component}-{part}-{property}; usually leave these alone",
63
63
  "counts": {
64
- "component": 1901,
64
+ "component": 1903,
65
65
  "foundation": 216,
66
66
  "semantic": 113
67
67
  },
68
68
  "foundation": "the seeds a consumer is invited to set — --primary, --background, --radius",
69
69
  "semantic": "named roles that follow the seeds — --ring, --text-link, --overlay-background"
70
70
  },
71
- "version": "33.3.0"
71
+ "version": "33.4.0"
72
72
  }
package/agent/llms.txt CHANGED
@@ -1,10 +1,10 @@
1
1
  # @godxjp/ui
2
2
 
3
- > A Japanese-enterprise React design system: 166 components, 2230 design tokens,
4
- > 44 cardinal rules. This file is the entry point for AI agents. Catalog version 33.3.0.
3
+ > A Japanese-enterprise React design system: 166 components, 2232 design tokens,
4
+ > 44 cardinal rules. This file is the entry point for AI agents. Catalog version 33.4.0.
5
5
 
6
6
  If your client can run a process, do not read these files — run the MCP server instead
7
- (`npx @godxjp/ui-mcp@33.3.0`). It is searchable and version-locked. These files exist for agents
7
+ (`npx @godxjp/ui-mcp@33.4.0`). It is searchable and version-locked. These files exist for agents
8
8
  that can only fetch URLs.
9
9
 
10
10
  ## Start
@@ -19,7 +19,7 @@ that can only fetch URLs.
19
19
  - [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 50 KB — all 166 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
20
20
  - [components/&lt;Name&gt;.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json): one file per component (1 KB–37 KB, median 6 KB). Read the index, then fetch only the ones you chose — this is the selective route, and the reason you do not need the blob.
21
21
  - [components.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json): 1.3 MB — every entry in one file. Most URL fetchers truncate a response this size without saying so; prefer the per-component files.
22
- - [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 216 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 113 `semantic` roles, 1901 `component` knobs.
22
+ - [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 216 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 113 `semantic` roles, 1903 `component` knobs.
23
23
  - [vocabulary.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/vocabulary.json): the controlled prop vocabulary — which prop name means what, across every component.
24
24
  - [rules.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json): 44 cardinal rules.
25
25
  - [anti-ai-tells.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/anti-ai-tells.json): 35 shapes that make generated UI look generated, each with its fix.
@@ -27,7 +27,7 @@ that can only fetch URLs.
27
27
  ## Pinning
28
28
 
29
29
  Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
30
- the tag: `.../godx-jp/godxjp-ui/v33.3.0/agent/...`. A catalog that does not match the installed
30
+ the tag: `.../godx-jp/godxjp-ui/v33.4.0/agent/...`. A catalog that does not match the installed
31
31
  package describes props that are absent, or hides props that are present, and says nothing either way.
32
32
 
33
33
  Pinned catalogs only exist for releases whose tag actually contains `agent/`. If `…/v<version>/agent/index.json` returns 404, that release predates this catalog: read `…/main/…` instead and compare `index.json` → `version` against the package you have, so you at least know which way it drifted.
package/agent/tokens.json CHANGED
@@ -10968,16 +10968,16 @@
10968
10968
  "value": "var(--secondary)"
10969
10969
  },
10970
10970
  {
10971
- "description": "COUNT PILL overlaid on the cell's glyph (gh#398) — the unread affordance the bell needs. The offsets are what a call site used to hand-write as `relative` + `absolute -end-0.5 -top-0.5`, i.e. a position the cell owns and no theme could retune; they are measured from the GLYPH box (the pill is centred on the cell, then pushed out by these), so a bar that retunes --topbar-icon-size keeps the pill on the glyph's corner. Colour mirrors `.sb-badge`: quiet by default, `badgeTone=\"destructive\"` for a count addressed to the user. The two role-mirror knobs stay `initial` so a scoped [data-tenant]/.dark override of --secondary/--destructive still reaches the pill (docs/TOKENS.md). Defaults = hsl(var(--secondary)) · hsl(var(--muted-foreground)) · hsl(var(--destructive)) · hsl(var(--destructive-foreground)).",
10971
+ "description": "How far a one-digit pill's top edge (block) and end edge (inline) sit OUTSIDE the glyph's. Fractions of the pill, not a flat length (gh#1341 — Material 3 badges / Apple HIG): half a pill inline centres it on the glyph's corner, three quarters block leaves a quarter of it on the glyph, so at most a quarter of the glyph is covered; a longer count stays inside the cell and grows back across the glyph, still covering at most a quarter of it. They used to be a flat 4px, which left a 16px pill covering 56% of a 16px bell. Declared `initial` with the default resolved at the call site, so a scoped --topbar-item-badge-size still moves them (the freeze rule). The ring is the surface colour drawn round the pill so it reads cut out of the glyph; role-mirror knob, default = hsl(var(--card)) (the bar) at the call site, and the nav rail sets it to its own surface. default = calc(var(--topbar-item-badge-size) * 3 / 4)",
10972
10972
  "name": "--topbar-item-badge-offset-block",
10973
10973
  "tier": "component",
10974
- "value": "var(--space-1)"
10974
+ "value": "initial"
10975
10975
  },
10976
10976
  {
10977
- "description": "Shell (sidebar / topbar / kbd) component tokens — small-by-design text knobs (rule #45/#46). A service re-tunes chrome text without moving the global scale.",
10977
+ "description": "default = calc(var(--topbar-item-badge-size) / 2)",
10978
10978
  "name": "--topbar-item-badge-offset-inline",
10979
10979
  "tier": "component",
10980
- "value": "var(--space-1)"
10980
+ "value": "initial"
10981
10981
  },
10982
10982
  {
10983
10983
  "description": "Shell (sidebar / topbar / kbd) component tokens — small-by-design text knobs (rule #45/#46). A service re-tunes chrome text without moving the global scale.",
@@ -10985,6 +10985,18 @@
10985
10985
  "tier": "component",
10986
10986
  "value": "1rem"
10987
10987
  },
10988
+ {
10989
+ "description": "Shell (sidebar / topbar / kbd) component tokens — small-by-design text knobs (rule #45/#46). A service re-tunes chrome text without moving the global scale.",
10990
+ "name": "--topbar-item-badge-ring-width",
10991
+ "tier": "component",
10992
+ "value": "var(--stroke-md)"
10993
+ },
10994
+ {
10995
+ "description": "Shell (sidebar / topbar / kbd) component tokens — small-by-design text knobs (rule #45/#46). A service re-tunes chrome text without moving the global scale.",
10996
+ "name": "--topbar-item-badge-ring-color",
10997
+ "tier": "component",
10998
+ "value": "initial"
10999
+ },
10988
11000
  {
10989
11001
  "description": "Shell (sidebar / topbar / kbd) component tokens — small-by-design text knobs (rule #45/#46). A service re-tunes chrome text without moving the global scale.",
10990
11002
  "name": "--topbar-item-badge-padding-inline",
@@ -5,6 +5,7 @@ import * as React from "react";
5
5
  import { chain, mergeRefs } from "@react-aria/utils";
6
6
  import { Popover as AriaPopover } from "react-aria-components";
7
7
  import { cn } from "../../lib/utils.js";
8
+ import { getActiveElement } from "../../lib/active-element.js";
8
9
  import { Slot } from "../../lib/slot.js";
9
10
  import { useOverlayCloseFocus } from "../feedback/overlay-close-focus.js";
10
11
  function toPlacement(side, align) {
@@ -278,7 +279,7 @@ function ClickContent({
278
279
  if (!event.defaultPrevented) {
279
280
  const target = firstTabbable(node);
280
281
  target?.focus({ preventScroll: true });
281
- if (!node.contains(document.activeElement)) {
282
+ if (!node.contains(getActiveElement(node))) {
282
283
  node.focus({ preventScroll: true });
283
284
  }
284
285
  }
@@ -287,11 +288,11 @@ function ClickContent({
287
288
  React.useEffect(() => {
288
289
  if (!open) return;
289
290
  const onPointerDown = (event) => {
290
- const target = event.target;
291
- if (!target) return;
292
- if (contentRef.current?.contains(target)) return;
293
- if (triggerRef.current?.contains(target)) return;
294
- if (anchored && anchorRef.current?.contains(target)) return;
291
+ const path = event.composedPath();
292
+ const within = (node) => node !== null && path.includes(node);
293
+ if (within(contentRef.current)) return;
294
+ if (within(triggerRef.current)) return;
295
+ if (anchored && within(anchorRef.current)) return;
295
296
  setOpen(false);
296
297
  };
297
298
  document.addEventListener("pointerdown", onPointerDown);
@@ -305,6 +306,7 @@ function ClickContent({
305
306
  isOpen: root.open,
306
307
  onOpenChange: onAriaOpenChange,
307
308
  isNonModal: root.isNonModal,
309
+ shouldCloseOnInteractOutside: (element) => !triggerRef.current?.contains(element),
308
310
  triggerRef: root.anchored ? root.anchorRef : root.triggerRef,
309
311
  placement: toPlacement(side, align),
310
312
  offset: sideOffset,
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
+ import { getActiveElement } from "../../lib/active-element.js";
3
4
  function useOverlayCloseFocus(isOpen, onCloseAutoFocus, contentRef) {
4
5
  const nodeToRestore = React.useRef(null);
5
6
  const renderedOpen = React.useRef(false);
@@ -7,7 +8,7 @@ function useOverlayCloseFocus(isOpen, onCloseAutoFocus, contentRef) {
7
8
  if (isOpen !== renderedOpen.current) {
8
9
  renderedOpen.current = isOpen;
9
10
  if (isOpen && typeof document !== "undefined") {
10
- const active = document.activeElement;
11
+ const active = getActiveElement();
11
12
  nodeToRestore.current = active === document.body ? null : active;
12
13
  }
13
14
  }
@@ -17,7 +18,7 @@ function useOverlayCloseFocus(isOpen, onCloseAutoFocus, contentRef) {
17
18
  onCloseAutoFocus?.(event);
18
19
  const node = nodeToRestore.current;
19
20
  nodeToRestore.current = null;
20
- const active = document.activeElement;
21
+ const active = getActiveElement();
21
22
  const focusMovedAway = contentRef != null && active != null && active !== document.body && active.isConnected && contentRef.current?.contains(active) !== true;
22
23
  if (!event.defaultPrevented && !focusMovedAway && node?.isConnected === true) {
23
24
  node.focus();
@@ -10,6 +10,7 @@ import {
10
10
  TooltipTriggerStateContext
11
11
  } from "react-aria-components";
12
12
  import { cn } from "../../lib/utils.js";
13
+ import { getActiveElement } from "../../lib/active-element.js";
13
14
  import { Slot } from "../../lib/slot.js";
14
15
  const DEFAULT_DELAY_DURATION = 200;
15
16
  const TooltipAnchorContext = React.createContext(
@@ -166,7 +167,7 @@ function EllipsisTooltip({
166
167
  show(DEFAULT_DELAY_DURATION);
167
168
  };
168
169
  const onFocusIn = () => {
169
- const active = document.activeElement;
170
+ const active = getActiveElement(node);
170
171
  if (active && (active === host || node.contains(active)) && isFocusVisible(active)) show(0);
171
172
  };
172
173
  node.addEventListener("pointerenter", onPointerEnter);
@@ -16,7 +16,7 @@ const TopbarItem = React.forwardRef(function TopbarItem2({
16
16
  ...props
17
17
  }, ref) {
18
18
  const Comp = asChild ? Slot : "button";
19
- const showBadge = badge !== void 0 && badge !== "";
19
+ const showBadge = badge !== void 0 && badge !== "" && badge !== 0;
20
20
  const iconNode = icon !== void 0 && icon !== null && icon !== false ? /* @__PURE__ */ jsx(
21
21
  "span",
22
22
  {
@@ -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": "33.3.0",
3
+ "version": "33.4.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,
@@ -3,6 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { ChevronDown } from "lucide-react";
5
5
  import { cn } from "../lib/utils.js";
6
+ import { getActiveElement } from "../lib/active-element.js";
6
7
  import { useTranslation } from "../i18n/use-translation.js";
7
8
  const OPEN_DELAY_MS = 0;
8
9
  const CLOSE_DELAY_MS = 100;
@@ -216,7 +217,7 @@ const MegaMenu = React.forwardRef(function MegaMenu2({
216
217
  if (openKey === null) return;
217
218
  const onPointerDown = (event) => {
218
219
  const node = rootRef.current;
219
- if (node && event.target instanceof Node && !node.contains(event.target)) close();
220
+ if (node && !event.composedPath().includes(node)) close();
220
221
  };
221
222
  document.addEventListener("pointerdown", onPointerDown);
222
223
  return () => document.removeEventListener("pointerdown", onPointerDown);
@@ -381,7 +382,7 @@ const MegaMenu = React.forwardRef(function MegaMenu2({
381
382
  closeTimer.current = setTimeout(() => {
382
383
  closeTimer.current = null;
383
384
  const node = rootRef.current;
384
- if (node && node.contains(document.activeElement)) return;
385
+ if (node && node.contains(getActiveElement(node))) return;
385
386
  commitOpen(null);
386
387
  }, closeDelay);
387
388
  };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The element that REALLY has focus, through shadow roots (gh#1340).
3
+ *
4
+ * `document.activeElement` stops at the shadow HOST: with the app mounted in a shadow root (GoDX
5
+ * Dock, `OverlayPortalProvider`), every `node.contains(document.activeElement)` in this library
6
+ * read "focus is not in here" while focus sat on an input inside the panel.
7
+ *
8
+ * With `scope`, the walk starts at that node's own root (`getRootNode().activeElement`), which also
9
+ * sees into a CLOSED shadow root the node lives in; it returns `null` when focus is not inside that
10
+ * root at all. Without it, the walk starts at `document` and descends through open shadow roots.
11
+ */
12
+ export declare function getActiveElement(scope?: Node | null): Element | null;
@@ -0,0 +1,10 @@
1
+ function getActiveElement(scope) {
2
+ if (typeof document === "undefined") return null;
3
+ const root = scope?.getRootNode();
4
+ let active = root instanceof Document || root instanceof ShadowRoot ? root.activeElement : document.activeElement;
5
+ while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
6
+ return active;
7
+ }
8
+ export {
9
+ getActiveElement
10
+ };
@@ -53,9 +53,10 @@ function isEditableTarget(target) {
53
53
  }
54
54
  function shouldHandleShortcut(spec, event, owner) {
55
55
  if (isImeComposing(event) || !shortcutMatches(spec, event)) return false;
56
- if (inForeignLayer(event.target, owner)) return false;
56
+ const target = event.composedPath()[0] ?? event.target;
57
+ if (inForeignLayer(target, owner)) return false;
57
58
  const commandModifier = spec.mod || spec.meta || spec.ctrl || spec.alt;
58
- return commandModifier || !isEditableTarget(event.target);
59
+ return commandModifier || !isEditableTarget(target);
59
60
  }
60
61
  const SHORTCUT_LAYER = '[role="dialog"], [role="alertdialog"], [aria-modal="true"], [role="menu"], [role="listbox"]';
61
62
  function inForeignLayer(target, owner) {
@@ -1656,6 +1656,9 @@ export type TopbarItemProp = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>,
1656
1656
  * Drawn under `asChild` too (gh#1171): the pill goes inside the child element, after its own
1657
1657
  * children, so a bell that is a link (`<TopbarItem asChild badge={3}><a …/></TopbarItem>`) keeps
1658
1658
  * its count.
1659
+ *
1660
+ * Nothing renders for `undefined`, `""` or the NUMBER `0` (gh#1341), so pass the count as it is.
1661
+ * To show a zero deliberately, pass the string `"0"`.
1659
1662
  */
1660
1663
  badge?: ReactNode;
1661
1664
  /**
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "33.3.0",
2
+ "version": "33.4.0",
3
3
  "generatedBy": "scripts/gen-style-layers.mjs — do not edit by hand; run `pnpm gen:style-layers`",
4
4
  "base": "base.css",
5
5
  "fonts": "fonts.css",
@@ -203,6 +203,14 @@
203
203
  --control-height-sm: var(--app-shell-nav-rail-item-size);
204
204
  }
205
205
 
206
+ .app-nav-rail {
207
+ --topbar-item-badge-ring-color: var(--app-shell-nav-rail-background, hsl(var(--muted)));
208
+ }
209
+
210
+ .app-mobile-nav-rail {
211
+ --topbar-item-badge-ring-color: var(--sheet-surface-background, hsl(var(--background)));
212
+ }
213
+
206
214
  .app-nav-rail[data-edge="end"] {
207
215
  border-inline-start: var(--stroke-hairline) solid hsl(var(--border));
208
216
  border-inline-end: 0;
@@ -836,6 +844,8 @@
836
844
  flex-direction: column;
837
845
  border-inline-end: var(--stroke-hairline) solid hsl(var(--border));
838
846
  inline-size: var(--app-shell-nav-rail-width);
847
+
848
+ padding-block: var(--app-shell-nav-rail-inset);
839
849
  overflow-y: auto;
840
850
  overscroll-behavior: contain;
841
851
  }
@@ -2315,10 +2325,13 @@
2315
2325
  position: absolute;
2316
2326
 
2317
2327
  inset-block-start: calc(
2318
- 50% - var(--topbar-icon-size) / 2 - var(--topbar-item-badge-offset-block)
2328
+ 50% - var(--topbar-icon-size) / 2 -
2329
+ var(--topbar-item-badge-offset-block, var(--topbar-item-badge-size) * 3 / 4)
2319
2330
  );
2320
- inset-inline-end: calc(
2321
- 50% - var(--topbar-icon-size) / 2 - var(--topbar-item-badge-offset-inline)
2331
+ inset-inline-end: max(
2332
+ var(--topbar-item-badge-ring-width),
2333
+ 50% - var(--topbar-icon-size) / 2 -
2334
+ var(--topbar-item-badge-offset-inline, var(--topbar-item-badge-size) / 2)
2322
2335
  );
2323
2336
  display: inline-flex;
2324
2337
  min-inline-size: var(--topbar-item-badge-size);
@@ -2333,6 +2346,9 @@
2333
2346
  font-weight: var(--font-weight-medium);
2334
2347
  line-height: 1;
2335
2348
 
2349
+ box-shadow: 0 0 0 var(--topbar-item-badge-ring-width)
2350
+ var(--topbar-item-badge-ring-color, hsl(var(--card)));
2351
+
2336
2352
  pointer-events: none;
2337
2353
  }
2338
2354