@godxjp/ui 25.1.0 → 25.3.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.
@@ -22,7 +22,7 @@ function Calendar({
22
22
  onClose,
23
23
  footer,
24
24
  width = "auto",
25
- bordered = false,
25
+ bordered = true,
26
26
  cellRender,
27
27
  month: monthProp,
28
28
  onMonthChange,
@@ -49,6 +49,7 @@ function DatePicker(props) {
49
49
  toDate,
50
50
  disabledDate,
51
51
  cellRender,
52
+ bordered,
52
53
  allowClear,
53
54
  triggerLabel,
54
55
  format: formatProp,
@@ -381,6 +382,7 @@ function DatePicker(props) {
381
382
  onMonthChange: setViewAnchor,
382
383
  locale: dayPickerLocale,
383
384
  cellRender,
385
+ bordered,
384
386
  showWeekNumber: showWeek,
385
387
  startMonth: minimum,
386
388
  endMonth: maximum,
@@ -221,7 +221,7 @@ const SelectValue = React.forwardRef(function SelectValue2({ placeholder, childr
221
221
  className: className ?? "",
222
222
  style,
223
223
  ...props,
224
- children: ({ isPlaceholder, defaultChildren }) => isPlaceholder ? placeholder ?? null : children ?? defaultChildren
224
+ children: ({ isPlaceholder, defaultChildren }) => isPlaceholder ? placeholder ?? "" : children ?? defaultChildren
225
225
  }
226
226
  );
227
227
  });
@@ -17,7 +17,12 @@ const buttonVariants = cva("ui-button", {
17
17
  // (`--primary-hover`) could never take effect while `hover:bg-primary/90` was emitted here.
18
18
  default: "ui-button--default",
19
19
  destructive: "ui-button--destructive bg-destructive text-destructive-foreground",
20
- outline: "ui-button--outline border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
20
+ // `shadow-xs` từng nằm ở đây và mắc ĐÚNG lỗi đã nêu ngay trên cho `default`:
21
+ // một utility thì out-rank tầng components, nên bóng của nút outline đi vòng
22
+ // qua tầng token — consumer đặt `--shadow-sm: none` thì `default` sạch bóng còn
23
+ // `outline` trơ ra. Nay bóng khai ở `.ui-button--outline` và đọc `var(--shadow-sm)`
24
+ // như mọi biến thể khác, nên nó tắt/đổi được qua theme. (gh#662)
25
+ outline: "ui-button--outline border bg-background hover:bg-accent hover:text-accent-foreground",
21
26
  dashed: "ui-button--dashed border border-dashed bg-background hover:bg-accent hover:text-accent-foreground",
22
27
  secondary: "ui-button--secondary bg-secondary text-secondary-foreground hover:bg-secondary/80",
23
28
  ghost: "ui-button--ghost hover:bg-accent hover:text-accent-foreground",
@@ -139,7 +139,8 @@ type DropdownMenuContentProps = React.PropsWithChildren<DropdownMenuContentProps
139
139
  * phần tử mang `role="menu"`. `data-slot` và lớp `ui-dropdown-menu-content` ở lại trên HỘP, cùng
140
140
  * chỗ với `data-state` / `data-side` mà các utility animate đang đọc.
141
141
  *
142
- * `useInertHiddenBackground` đã được gỡ ở đây: RAC gọi `ariaHideOutside(..., {shouldUseInert:
142
+ * `useInertHiddenBackground` đã được gỡ ở đây, và nay đã rời hẳn `src/` sang
143
+ * `data-entry/__tests__/inert-background.fixture.ts` (gh#643): RAC gọi `ariaHideOutside(..., {shouldUseInert:
143
144
  * true})` — nền thành `inert` thật chứ không chỉ `aria-hidden`, nên axe `aria-hidden-focus` không
144
145
  * còn cửa để nổ. Và hiệu ứng đó khoá theo `state.isOpen`, không theo unmount, nên `inert` được nhả
145
146
  * NGAY ở ý định đóng: đúng lớp lỗi gh#385, đã sửa ở thượng nguồn. Phép đo là
@@ -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": "25.1.0",
3
+ "version": "25.3.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,
@@ -725,8 +725,10 @@ export type CalendarProp = DayPickerProps & CalendarFooterProp & {
725
725
  * What a month grid needs is the ruling BETWEEN days, so a week reads as a row of cells the
726
726
  * eye can track across.
727
727
  *
728
- * Default `false`, because a picker popover wants the opposite: floating day buttons with no
729
- * ruling, so the selected day is the only shape in the panel.
728
+ * Default `true` — the owner's call, from a real screen: with no ruling the month read as a
729
+ * cloud of numbers and was "very hard to read". Pass `bordered={false}` for the floating-button
730
+ * look. The line colour is `--calendar-grid-border-color` (default `--input`, the ≥3:1 tier;
731
+ * the decorative `--border` measured 1.15:1 and was invisible as a 1px ruling).
730
732
  */
731
733
  bordered?: boolean;
732
734
  /** Decorate a day cell — 祝日, a booked day, a deadline. @see CalendarCellRenderProp */
@@ -861,6 +863,12 @@ export type DatePickerBaseProp = FieldA11yProps & PickerChromeProp & {
861
863
  toDate?: Date;
862
864
  /** Decorate a day cell — 祝日, a booked day, a deadline. @see CalendarCellRenderProp */
863
865
  cellRender?: CalendarCellRenderProp;
866
+ /**
867
+ * Rule the day grid in the panel — forwarded to `Calendar bordered`, and defaults to `true`
868
+ * like it. Applies to the DAY grid (`picker="date"` / `"week"`, single, `multiple` and `range`);
869
+ * the month / quarter / year period grid has no day cells to rule and ignores it.
870
+ */
871
+ bordered?: boolean;
864
872
  /**
865
873
  * Forbid individual dates by predicate — the rule `minDate`/`maxDate` cannot express, because a
866
874
  * business calendar is rarely one contiguous range: 土日, a closed accounting period, a 祝日, a
@@ -132,6 +132,8 @@
132
132
  border: 1px solid hsl(var(--input));
133
133
  background: hsl(var(--background));
134
134
  color: hsl(var(--foreground));
135
+
136
+ box-shadow: var(--shadow-sm);
135
137
  }
136
138
 
137
139
  .ui-button--ghost {
@@ -2178,13 +2180,13 @@
2178
2180
 
2179
2181
  .ui-calendar[data-bordered="true"] .ui-calendar-day,
2180
2182
  .ui-calendar[data-bordered="true"] .ui-calendar-weekday {
2181
- border-inline-start: 1px solid hsl(var(--border));
2182
- border-block-start: 1px solid hsl(var(--border));
2183
+ border-inline-start: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
2184
+ border-block-start: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
2183
2185
  }
2184
2186
 
2185
2187
  .ui-calendar[data-bordered="true"] .ui-calendar-week,
2186
2188
  .ui-calendar[data-bordered="true"] .ui-calendar-weekdays {
2187
- border-inline-end: 1px solid hsl(var(--border));
2189
+ border-inline-end: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
2188
2190
  }
2189
2191
 
2190
2192
  .ui-calendar[data-bordered="true"] .ui-calendar-weekday {
@@ -2196,7 +2198,7 @@
2196
2198
  }
2197
2199
 
2198
2200
  .ui-calendar[data-bordered="true"] .ui-calendar-grid {
2199
- border-block-end: 1px solid hsl(var(--border));
2201
+ border-block-end: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
2200
2202
  }
2201
2203
 
2202
2204
  .ui-calendar[data-bordered="true"] .ui-calendar-day.invisible {
@@ -2217,8 +2219,12 @@
2217
2219
 
2218
2220
  .ui-calendar[data-bordered="true"] .ui-calendar-week-number,
2219
2221
  .ui-calendar[data-bordered="true"] .ui-calendar-week-number-header {
2220
- border-inline-start: 1px solid hsl(var(--border));
2221
- border-block-start: 1px solid hsl(var(--border));
2222
+ border-inline-start: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
2223
+ border-block-start: 1px solid var(--calendar-grid-border-color, hsl(var(--input)));
2224
+ }
2225
+
2226
+ .ui-calendar[data-bordered="true"] .ui-calendar-day {
2227
+ inline-size: var(--control-height);
2222
2228
  }
2223
2229
 
2224
2230
  .ui-calendar[data-bordered="true"] .ui-calendar-day-button {
@@ -120,10 +120,14 @@
120
120
  }
121
121
 
122
122
  [data-slot="text"][data-link] {
123
- text-decoration-line: none;
123
+ text-decoration-line: var(--text-link-decoration-line);
124
124
  text-underline-offset: var(--text-link-underline-offset);
125
125
  text-decoration-thickness: var(--text-link-underline-width);
126
126
  }
127
+
128
+ [data-slot="text"][data-link][data-tone="primary"]:not([data-disabled]) {
129
+ color: hsl(var(--text-link));
130
+ }
127
131
  [data-slot="text"][data-link]:hover,
128
132
  [data-slot="text"][data-link]:focus-visible {
129
133
  text-decoration-line: underline;
@@ -290,6 +290,8 @@
290
290
 
291
291
  --calendar-bordered-weekday-padding-block: var(--space-1);
292
292
 
293
+ --calendar-grid-border-color: initial;
294
+
293
295
  --transfer-pane-min-height: 14rem;
294
296
  --transfer-pane-radius: var(--radius-md);
295
297
  --transfer-panes-space-gap: var(--space-3);
@@ -1,9 +1,11 @@
1
1
 
2
2
  :root {
3
3
 
4
- --text-link-underline-offset: 0.2em;
4
+ --text-link-underline-offset: 0.18em;
5
5
 
6
6
  --text-link-underline-width: auto;
7
+
8
+ --text-link-decoration-line: underline;
7
9
  }
8
10
 
9
11
  :root {
@@ -39,7 +39,8 @@
39
39
 
40
40
  --text-tertiary: 44 5% 55%;
41
41
  --text-disabled: 40 6% 68%;
42
- --text-link: 204 100% 39%;
42
+
43
+ --text-link: 268.3 100% 41.6%;
43
44
  --text-brand: 268.3 100% 41.6%;
44
45
 
45
46
  --text-primary: 268 100% 34.5%;
@@ -351,7 +352,7 @@
351
352
 
352
353
  --text-tertiary: 44 6% 56%;
353
354
  --text-disabled: 45 6% 40%;
354
- --text-link: 204 90% 66%;
355
+ --text-link: 268.7 100% 86.9%;
355
356
  --text-brand: 268.7 100% 86.9%;
356
357
 
357
358
  --text-primary: 262.7 100% 92.7%;
@@ -122,7 +122,7 @@ pnpm test # FULL suite — CI only, never from an agent loop
122
122
  pnpm preview:build # integration test: examples + docs must build — at most once, pre-PR
123
123
  pnpm audit # godxjp-ui-audit — 0 errors for touched files
124
124
  pnpm check:mcp-sync # MCP registry ↔ library export drift guard
125
- pnpm check:frame-axe # WCAG 2.2 AA over every frame — see FRAME-A11Y-CI.md
125
+ pnpm check:frame-axe # WCAG 2.2 AA over every frame — LOCAL ONLY, never in CI (FRAME-A11Y-CI.md)
126
126
  ```
127
127
 
128
128
  `pnpm verify` and `pnpm verify:release` run these together (verify:release also builds) — **both include the full suite, so both belong to CI.** Locally, run them at most once immediately before opening a PR, never inside an edit loop and never while other agents are working on the same machine. It needs `pnpm exec playwright install chromium` once locally; see [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) for how to run/scope it, read the evidence, and regenerate its baseline after an accessibility fix.
@@ -190,5 +190,5 @@ npm versions are immutable, so you can never "fix" a bad published version in pl
190
190
  - [README](../README.md) — overview, component groups, consumer setup.
191
191
  - [CONSUMER-RULES.md](./CONSUMER-RULES.md) — the ten rules every consumer follows (the audit enforces them).
192
192
  - `docs/TOKENS.md`, `docs/SPACING.md`, `docs/PROPS-VOCABULARY.md`, `docs/PROPS-REGISTRY.md`.
193
- - [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) — per-frame axe a11y + geometry + coverage CI gates.
193
+ - [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) — per-frame axe a11y (**local only, never in CI**) + geometry + coverage gates.
194
194
  - **`godx-ui-component-placement`** skill — decide whether a component belongs here.
@@ -1,6 +1,13 @@
1
- # Per-frame accessibility CI — `check:frame-axe`
1
+ # Per-frame accessibility — `check:frame-axe` (LOCAL ONLY)
2
2
 
3
- One ruler, run in the repository that owns the CSS.
3
+ One ruler, run in the repository that owns the CSS — **on a developer machine, never in CI/CD.**
4
+
5
+ > **Owner's standing rule (2026-09-17): axe must not run in any GitHub Actions workflow.** It is a
6
+ > local measurement only. gh#643 had wired it into `ci-browser.yml` (four shards on every merge) and
7
+ > `ci-browser-full.yml` (nightly); both jobs are removed. `check:gate-coverage` lists
8
+ > `check:frame-axe` as EXEMPT with this reason, so its absence from the workflows is a declaration,
9
+ > not a dead gate. **Do not re-wire it into a workflow.** The file keeps its historical name so
10
+ > existing links still resolve.
4
11
 
5
12
  ## Why it exists (gh#643)
6
13
 
@@ -234,41 +241,45 @@ which changes what `color-contrast` resolves a background to). Two sweeps after
234
241
 
235
242
  ## Cost
236
243
 
237
- | | |
238
- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
239
- | full sweep | **3m20s** locally · **12m01s** on the self-hosted runner — 215 routes × 3 viewports |
240
- | sequential | ~55 min — the three viewport passes run concurrently, which is the whole difference |
241
- | showcase only (`--scope=showcase`) | 30s |
242
- | full sweep **with the overlay scope** | **2m14s** on the machine gh#643 item 4 was built on, same 215 × 3 — the runner number has not been re-measured |
244
+ | | |
245
+ | ------------------------------------- | ----------------------------------------------------------------------------------- |
246
+ | full sweep | **2m14s** locally, with the overlay scope — 215 routes × 3 viewports |
247
+ | sequential | ~55 min — the three viewport passes run concurrently, which is the whole difference |
248
+ | showcase only (`--scope=showcase`) | ~30s |
249
+ | one route (`pnpm check:frame-axe /isolate/…`) | seconds — the loop to use while fixing |
243
250
 
244
251
  The overlay pass is bounded by declaration: only the 7 declaring routes pay it (21 extra scans plus
245
- their open steps), and no `/showcase/**` route declares one, so `--scope=showcase` — the merge lane —
246
- is unchanged.
252
+ their open steps), and no `/showcase/**` route declares one, so `--scope=showcase` is unchanged by it.
253
+
254
+ ## Where it runs: your machine
247
255
 
248
- `--shard=i/n` is in the script for the day the sweep outgrows the lane. Using it adds check-run
249
- names, which costs nothing here because none of them is in `REQUIRED_CI_CHECK_RUNS`.
256
+ **Never in CI/CD** — see the rule at the top. The procedure:
250
257
 
251
- ## Where it runs
258
+ ```bash
259
+ pnpm build && pnpm preview:build # the STATIC preview — not `pnpm dev` / `pnpm preview`
260
+ pnpm check:frame-axe # full sweep; must end with 0 baselined entries, 0 nodes
261
+ pnpm check:frame-axe /isolate/<frame> # one route, while fixing
262
+ ```
252
263
 
253
- Two lanes, split on a **measurement taken on the runner, not on a laptop**:
264
+ Run it before a PR that touches markup, ARIA, focus behaviour or colour, and commit what it writes —
265
+ `audit-evidence/frame-axe/results.json` and, only when a full sweep genuinely changed it,
266
+ `frame-axe-baseline.json`.
254
267
 
255
- | lane | what | when | measured |
256
- | ----------------------------------------------------------------- | ----------------------------------------- | -------------------------------------------------------------- | -------------------------------------- |
257
- | `ci-browser.yml` · `Per-frame axe (showcases, WCAG 2.2 AA)` | `--scope=showcase` — 30 whole-page frames | every push to `main` | 30s locally |
258
- | `ci-browser-full.yml` · `Per-frame axe (all frames, WCAG 2.2 AA)` | the full 215-route sweep | nightly, `workflow_dispatch`, or a `run-browser` label on a PR | **12m01s on the pool** (3m20s locally) |
268
+ **The static preview, not the dev server.** Against the vite dev server the sweep produces 1–3
269
+ spurious `route-did-not-render` / `axe-did-not-run` rows on different routes each run — 30s
270
+ `page.goto` timeouts while three viewport contexts hammer an on-demand-transforming server. Each is
271
+ clean alone. The static build does not do this.
259
272
 
260
- The full sweep went into the merge lane first, on the local 3m20s. On the pool's runner the same
261
- job took **12m01s** — three times the local wall clock and more than twice CONTRACT.md L4's
262
- five-minute budget for that whole lane. So the merge lane keeps the showcases, which are the only
263
- frames here shaped like the screens a consumer ships, and the wide matrix moved to the lane this
264
- repository already reserves for wide matrices.
273
+ **`--shard=i/n` and `--scope=` are for splitting a LOCAL run**, e.g. across two terminals. Two guards
274
+ hold for both: `--update` refuses under either (a partial run would rewrite the ledger from a fraction
275
+ of the frames and delete the rest), and the "no longer fires" report is suppressed on a partial run
276
+ (every other partition's rows would read as fixed). Regenerate the baseline from a full sweep only.
265
277
 
266
- **Not** the PR lane: that file's own header lists axe among what it deliberately excludes, at a
267
- measured 653–745s, and that decision is not reopened here.
278
+ ### What leaving CI costs, stated rather than discovered
268
279
 
269
- The job is not in `REQUIRED_CI_CHECK_RUNS` (#492 removed it from the release proof map and this does
270
- not put it back). It still protects a release through `assertCiProvenance`'s collateral rule — **any**
271
- red check run on the SHA being published refuses the publish.
280
+ A regression is now caught when someone runs this, not on merge. Nothing in the pipeline will turn
281
+ red on a new `aria-*` defect, a nameless button or a sub-24px target. That is the trade the rule
282
+ makes, and the only thing that closes it is the local run above actually happening before a PR.
272
283
 
273
284
  ## Related
274
285
 
package/docs/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
  | [SPACING.md](./SPACING.md) | Golden ratio (φ) macro spacing — PageContainer sections, Flex gap, Card rhythm |
13
13
  | [FORMS.md](./FORMS.md) | **Mandatory** react-hook-form + Zod 4 |
14
14
  | [TESTING.md](./TESTING.md) | **Mandatory** Vitest per component |
15
- | [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) | Per-frame axe a11y + geometry + coverage CI gates — how to run/regenerate locally |
15
+ | [FRAME-A11Y-CI.md](./FRAME-A11Y-CI.md) | Per-frame axe a11y (LOCAL ONLY, never CI) + geometry + coverage gates — how to run/regenerate |
16
16
  | [../README.md](../README.md) | Setup, workspace wiring, theme |
17
17
 
18
18
  ## Preview
@@ -29,6 +29,10 @@ export default function Demo() {
29
29
  from: new Date(2026, 3, 1),
30
30
  to: new Date(2026, 3, 30),
31
31
  });
32
+ const [ruledRange, setRuledRange] = useState<DateRange | undefined>({
33
+ from: new Date(2026, 3, 13),
34
+ to: new Date(2026, 3, 22),
35
+ });
32
36
  const [shiftDays, setShiftDays] = useState<Date[] | undefined>([
33
37
  new Date(2026, 3, 6),
34
38
  new Date(2026, 3, 13),
@@ -67,6 +71,48 @@ export default function Demo() {
67
71
  </CardContent>
68
72
  </Card>
69
73
 
74
+ <Card>
75
+ <CardHeader>
76
+ <CardTitle level={2}>
77
+ 罫線 (bordered) · 既定で表示 / bordered={"{false}"} で解除
78
+ </CardTitle>
79
+ <CardDescription>
80
+ 日と日の間に罫線を引くのが既定。線色は --calendar-grid-border-color (既定
81
+ --input、背景に対し 3:1
82
+ 以上)。選択日・今日・範囲の始点/中間/終点・無効日・前後月の日が
83
+ 罫線の中でも判別できる。罫線なしの浮いたボタン表示にしたい場合のみ bordered=
84
+ {"{false}"} を渡す。
85
+ </CardDescription>
86
+ </CardHeader>
87
+ <CardContent>
88
+ <Flex gap="lg" wrap>
89
+ <Calendar
90
+ mode="range"
91
+ selected={ruledRange}
92
+ onSelect={setRuledRange}
93
+ defaultMonth={new Date(2026, 3, 1)}
94
+ today={new Date(2026, 3, 8)}
95
+ locale={ja}
96
+ disabled={[new Date(2026, 3, 5), new Date(2026, 3, 27)]}
97
+ aria-label="罫線あり (既定) の期間カレンダー"
98
+ data-testid="calendar-ruled"
99
+ />
100
+ <Calendar
101
+ mode="range"
102
+ selected={ruledRange}
103
+ onSelect={setRuledRange}
104
+ defaultMonth={new Date(2026, 3, 1)}
105
+ today={new Date(2026, 3, 8)}
106
+ locale={ja}
107
+ disabled={[new Date(2026, 3, 5), new Date(2026, 3, 27)]}
108
+ bordered={false}
109
+ aria-label="罫線なし (bordered=false) の期間カレンダー"
110
+ data-testid="calendar-unruled"
111
+ />
112
+ </Flex>
113
+ </CardContent>
114
+ </Card>
115
+
70
116
  <Card>
71
117
  <CardHeader>
72
118
  <CardTitle level={2}>範囲選択 · Popover 内</CardTitle>
@@ -55,6 +55,36 @@ export default function Demo() {
55
55
  </CardContent>
56
56
  </Card>
57
57
 
58
+ <Card>
59
+ <CardHeader>
60
+ <CardTitle level={2}>カレンダーの罫線 (bordered)</CardTitle>
61
+ <CardDescription>
62
+ ポップアップのカレンダーは既定で日ごとに罫線を引く (Calendar bordered を転送)。
63
+ bordered={"{false}"} で罫線なしに戻せる。range / multiple / week
64
+ でも同じ。月・四半期・年の グリッドには日のセルがないため対象外。
65
+ </CardDescription>
66
+ </CardHeader>
67
+ <CardContent>
68
+ <Flex gap="lg" wrap>
69
+ <FormField id="ruled-date" label="罫線あり (既定)">
70
+ <DatePicker
71
+ id="ruled-date"
72
+ name="ruled_date"
73
+ defaultValue={new Date(2026, 0, 15)}
74
+ />
75
+ </FormField>
76
+ <FormField id="unruled-date" label="罫線なし (bordered={false})">
77
+ <DatePicker
78
+ id="unruled-date"
79
+ name="unruled_date"
80
+ defaultValue={new Date(2026, 0, 15)}
81
+ bordered={false}
82
+ />
83
+ </FormField>
84
+ </Flex>
85
+ </CardContent>
86
+ </Card>
87
+
58
88
  <Card>
59
89
  <CardHeader>
60
90
  <CardTitle level={2}>fromDate で過去日を無効化</CardTitle>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "25.1.0",
4
- "godxUiMcp": "25.1.0",
3
+ "version": "25.3.0",
4
+ "godxUiMcp": "25.3.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -1,10 +0,0 @@
1
- import * as React from "react";
2
- /**
3
- * Call from an overlay's content wrapper, and spread the returned ref onto the Radix content
4
- * element itself — that element is what carries `data-state`, and registering it is what lets the
5
- * background be released on close intent rather than on unmount (gh#385).
6
- *
7
- * The caller's own forwarded ref is composed in, so a content component still hands its node to
8
- * the consumer. Idempotent and refcounted: one observer serves every overlay on the page.
9
- */
10
- export declare function useInertHiddenBackground<T extends HTMLElement>(forwarded?: React.Ref<T>): (node: T | null) => void;
@@ -1,73 +0,0 @@
1
- "use client";
2
- import * as React from "react";
3
- const TABBABLE = 'a[href], area[href], button, input, select, textarea, iframe, object, embed, details, audio[controls], video[controls], [contenteditable="true"], [tabindex]:not([tabindex^="-"])';
4
- const owned = /* @__PURE__ */ new Set();
5
- const anchors = /* @__PURE__ */ new Set();
6
- let observer = null;
7
- let mounted = 0;
8
- function anyOverlayOpen() {
9
- for (const anchor of anchors) {
10
- if (anchor.isConnected && anchor.getAttribute("data-state") !== "closed") return true;
11
- }
12
- return false;
13
- }
14
- function sync() {
15
- if (!anyOverlayOpen()) {
16
- release();
17
- return;
18
- }
19
- for (const el of document.querySelectorAll("[data-aria-hidden]")) {
20
- if (owned.has(el) || el.hasAttribute("inert")) continue;
21
- if (el.matches("[data-radix-focus-guard]")) continue;
22
- if (el.matches(TABBABLE) || el.querySelector(TABBABLE)) {
23
- el.setAttribute("inert", "");
24
- owned.add(el);
25
- }
26
- }
27
- for (const el of [...owned]) {
28
- if (el.isConnected && el.hasAttribute("data-aria-hidden")) continue;
29
- el.removeAttribute("inert");
30
- owned.delete(el);
31
- }
32
- }
33
- function release() {
34
- for (const el of owned) el.removeAttribute("inert");
35
- owned.clear();
36
- }
37
- function useInertHiddenBackground(forwarded) {
38
- React.useEffect(() => {
39
- mounted += 1;
40
- if (!observer) {
41
- observer = new MutationObserver(sync);
42
- observer.observe(document.body, {
43
- subtree: true,
44
- attributes: true,
45
- // `data-state` is the close-intent signal; `data-aria-hidden` is the what-to-hide signal.
46
- attributeFilter: ["data-aria-hidden", "data-state"]
47
- });
48
- }
49
- sync();
50
- return () => {
51
- mounted -= 1;
52
- if (mounted > 0) return;
53
- observer?.disconnect();
54
- observer = null;
55
- release();
56
- };
57
- }, []);
58
- const registered = React.useRef(null);
59
- const forwardedRef = React.useRef(forwarded);
60
- forwardedRef.current = forwarded;
61
- return React.useCallback((node) => {
62
- if (registered.current) anchors.delete(registered.current);
63
- registered.current = node;
64
- if (node) anchors.add(node);
65
- sync();
66
- const target = forwardedRef.current;
67
- if (typeof target === "function") target(node);
68
- else if (target) target.current = node;
69
- }, []);
70
- }
71
- export {
72
- useInertHiddenBackground
73
- };