@godxjp/ui 30.7.1 → 30.9.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 +23 -1
- package/agent/START-HERE.md +1 -1
- package/agent/components/Affix.json +1 -1
- package/agent/components/Anchor.json +2 -2
- package/agent/components.json +3 -3
- package/agent/index.json +2 -2
- package/agent/llms.txt +3 -3
- package/dist/components/data-display/marquee-reveal.d.ts +49 -0
- package/dist/components/data-display/marquee-reveal.js +55 -0
- package/dist/components/data-display/marquee.js +37 -19
- package/dist/components/layout/affix.js +14 -6
- package/dist/components/navigation/anchor.js +2 -2
- package/dist/contracts/measurement.json +1 -1
- package/dist/lib/hooks.d.ts +7 -0
- package/dist/lib/hooks.js +6 -0
- package/dist/props/components/layout.prop.d.ts +3 -2
- package/dist/props/components/navigation.prop.d.ts +5 -3
- package/dist/styles/layers.json +1182 -0
- package/dist/styles/vendor-day-picker.css +2 -0
- package/dist/styles/vendor-sonner.css +2 -0
- package/docs/TOKEN-RESOLUTION.md +8 -0
- package/docs/data-entry/form.tsx +42 -38
- package/docs/layout/nav-list.tsx +2 -2
- package/docs/navigation/anchor.tsx +31 -21
- package/package.json +6 -3
- package/scripts/cli.mjs +15 -0
- package/scripts/explain-token.mjs +55 -2
- package/scripts/prune-css.mjs +221 -0
package/README.md
CHANGED
|
@@ -214,7 +214,29 @@ slices below roughly 620 distinct characters. Full table and reasoning in
|
|
|
214
214
|
> missing layer fails silently: menus render with no background, rows with no
|
|
215
215
|
> height. `styles`, `styles/core`, `styles/core-with-fallbacks` and
|
|
216
216
|
> `styles/core-with-jis-level1` are the four supported entries; the runtime `visual-audit` flags a
|
|
217
|
-
> page whose layers are incomplete (`css-layers-missing`).
|
|
217
|
+
> page whose layers are incomplete (`css-layers-missing`). The one supported way to ship LESS
|
|
218
|
+
> than `core` is `prune-css` below — the tool slices along the dependency graph the package
|
|
219
|
+
> ships, so it cannot forget a layer the way a hand-picked list does.
|
|
220
|
+
|
|
221
|
+
### prune-css — ship only the component layers you use (gh#971)
|
|
222
|
+
|
|
223
|
+
`core` still carries every component's layers (~64 KB gzip for all ~165). If that remainder
|
|
224
|
+
matters, let the package slice it:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
npx @godxjp/ui prune-css resources/js --out resources/css/godx-ui.css # --fonts for the bundled faces
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
It scans your sources for `@godxjp/ui` imports, resolves the CSS layer dependency closure from
|
|
231
|
+
the graph shipped in `dist/styles/layers.json` (what each component's internals render is part
|
|
232
|
+
of the graph — a `DataTable` still gets its dropdown and pagination surfaces), and emits a file
|
|
233
|
+
that imports the foundation plus only the needed layers, in `styles/index.css`'s exact order.
|
|
234
|
+
Import that file INSTEAD of `@godxjp/ui/styles`. Defaults mirror `styles/core` (no
|
|
235
|
+
`@font-face`); the sonner / react-day-picker vendor sheets come along only when a used
|
|
236
|
+
component renders them. **Re-run it whenever the set of components you use changes and after
|
|
237
|
+
every upgrade** — the emitted header says so, and the tool refuses to run against a manifest
|
|
238
|
+
from a different package version. Measured on gh#971's 15-component app: 368 KB gzip
|
|
239
|
+
(`styles`) → 86 KB (`core`) → 75 KB pruned; a small 8-component app lands at 55 KB.
|
|
218
240
|
|
|
219
241
|
## Golden ratio (φ ≈ 1.618)
|
|
220
242
|
|
package/agent/START-HERE.md
CHANGED
|
@@ -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` 30.
|
|
6
|
+
**This catalog describes `@godxjp/ui` 30.9.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.
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"type": "number"
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
|
-
"description": "antd `target
|
|
24
|
+
"description": "antd `target` — the scroll box to pin against, which need not be the nearest scrolling ancestor. Omitted, it IS the nearest block-axis scroller (`position: sticky`'s rule), else the viewport (gh#984); `() => window` is the viewport. Same lazy-getter shape `FloatButton.BackTop.target` uses here, and for the same reason: the element does not exist on the render that declares it.",
|
|
25
25
|
"name": "target",
|
|
26
26
|
"type": "() => Window | HTMLElement | null"
|
|
27
27
|
},
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"type": "number"
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
|
-
"description": "gh#890. The scroll box the sections are measured in AND `Affix` pins the nav against — one function, both halves. `Affix`'s own name and shape (`AffixTargetProp`), the same lazy getter `FloatButton.BackTop.target` already spells here. `null
|
|
49
|
+
"description": "gh#890. The scroll box the sections are measured in AND `Affix` pins the nav against — one function, both halves. `Affix`'s own name and shape (`AffixTargetProp`), the same lazy getter `FloatButton.BackTop.target` already spells here. `null` means the viewport; absent (with no `getContainer`), the nearest block-axis scroller, else the viewport — `Affix`'s default (gh#984). Wins over `getContainer` when both are given.",
|
|
50
50
|
"name": "target",
|
|
51
51
|
"type": "() => Window | HTMLElement | null"
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
|
-
"description": "antd `getContainer
|
|
54
|
+
"description": "antd `getContainer` — the scroll box holding the sections; omitted → the nearest block-axis scroller, else the viewport (gh#984). Superseded by `target` (gh#890), which mirrors `Affix`'s own spelling for the identical idea; kept live for a call site written before `target` existed.",
|
|
55
55
|
"name": "getContainer",
|
|
56
56
|
"type": "() => HTMLElement | Window"
|
|
57
57
|
},
|
package/agent/components.json
CHANGED
|
@@ -15858,7 +15858,7 @@
|
|
|
15858
15858
|
"type": "number"
|
|
15859
15859
|
},
|
|
15860
15860
|
{
|
|
15861
|
-
"description": "antd `target
|
|
15861
|
+
"description": "antd `target` — the scroll box to pin against, which need not be the nearest scrolling ancestor. Omitted, it IS the nearest block-axis scroller (`position: sticky`'s rule), else the viewport (gh#984); `() => window` is the viewport. Same lazy-getter shape `FloatButton.BackTop.target` uses here, and for the same reason: the element does not exist on the render that declares it.",
|
|
15862
15862
|
"name": "target",
|
|
15863
15863
|
"type": "() => Window | HTMLElement | null"
|
|
15864
15864
|
},
|
|
@@ -15961,12 +15961,12 @@
|
|
|
15961
15961
|
"type": "number"
|
|
15962
15962
|
},
|
|
15963
15963
|
{
|
|
15964
|
-
"description": "gh#890. The scroll box the sections are measured in AND `Affix` pins the nav against — one function, both halves. `Affix`'s own name and shape (`AffixTargetProp`), the same lazy getter `FloatButton.BackTop.target` already spells here. `null
|
|
15964
|
+
"description": "gh#890. The scroll box the sections are measured in AND `Affix` pins the nav against — one function, both halves. `Affix`'s own name and shape (`AffixTargetProp`), the same lazy getter `FloatButton.BackTop.target` already spells here. `null` means the viewport; absent (with no `getContainer`), the nearest block-axis scroller, else the viewport — `Affix`'s default (gh#984). Wins over `getContainer` when both are given.",
|
|
15965
15965
|
"name": "target",
|
|
15966
15966
|
"type": "() => Window | HTMLElement | null"
|
|
15967
15967
|
},
|
|
15968
15968
|
{
|
|
15969
|
-
"description": "antd `getContainer
|
|
15969
|
+
"description": "antd `getContainer` — the scroll box holding the sections; omitted → the nearest block-axis scroller, else the viewport (gh#984). Superseded by `target` (gh#890), which mirrors `Affix`'s own spelling for the identical idea; kept live for a call site written before `target` existed.",
|
|
15970
15970
|
"name": "getContainer",
|
|
15971
15971
|
"type": "() => HTMLElement | Window"
|
|
15972
15972
|
},
|
package/agent/index.json
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"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.",
|
|
49
49
|
"read": {
|
|
50
50
|
"live": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json",
|
|
51
|
-
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v30.
|
|
51
|
+
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v30.9.0/agent/index.json"
|
|
52
52
|
},
|
|
53
53
|
"source": "mcp/src/data — the same data @godxjp/ui-mcp serves — plus the foundation and semantic token tiers, read from src/tokens/*.css",
|
|
54
54
|
"start": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md",
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
"foundation": "the seeds a consumer is invited to set — --primary, --background, --radius",
|
|
63
63
|
"semantic": "named roles that follow the seeds — --ring, --text-link, --overlay-background"
|
|
64
64
|
},
|
|
65
|
-
"version": "30.
|
|
65
|
+
"version": "30.9.0"
|
|
66
66
|
}
|
package/agent/llms.txt
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @godxjp/ui
|
|
2
2
|
|
|
3
3
|
> A Japanese-enterprise React design system: 175 components, 2074 design tokens,
|
|
4
|
-
> 50 cardinal rules. This file is the entry point for AI agents. Catalog version 30.
|
|
4
|
+
> 50 cardinal rules. This file is the entry point for AI agents. Catalog version 30.9.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@30.
|
|
7
|
+
(`npx @godxjp/ui-mcp@30.9.0`). It is searchable and version-locked. These files exist for agents
|
|
8
8
|
that can only fetch URLs.
|
|
9
9
|
|
|
10
10
|
## Start
|
|
@@ -26,7 +26,7 @@ that can only fetch URLs.
|
|
|
26
26
|
## Pinning
|
|
27
27
|
|
|
28
28
|
Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
|
|
29
|
-
the tag: `.../godx-jp/godxjp-ui/v30.
|
|
29
|
+
the tag: `.../godx-jp/godxjp-ui/v30.9.0/agent/...`. A catalog that does not match the installed
|
|
30
30
|
package describes props that are absent, or hides props that are present, and says nothing either way.
|
|
31
31
|
|
|
32
32
|
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.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bring a focused item of a Marquee track back into view by SEEKING the lap, not by scrolling
|
|
3
|
+
* (gh#983).
|
|
4
|
+
*
|
|
5
|
+
* The track travels by `transform` towards the inline start, so an item that has left the viewport
|
|
6
|
+
* on that side sits in NEGATIVE overflow — which is not scrollable overflow. The browser's own
|
|
7
|
+
* "scroll the focused element into view" therefore does nothing for it, and Tab through the track
|
|
8
|
+
* landed on links that were partly or wholly invisible: measured 8 of 18 tab stops at 375px, one
|
|
9
|
+
* at -149..-65 against a 75..1321 viewport at 1440px (WCAG 2.2 SC 2.4.11).
|
|
10
|
+
*
|
|
11
|
+
* The lap is seamless by construction, so any offset within it is a legitimate position: moving the
|
|
12
|
+
* animation's `currentTime` moves the whole strip to where the item is visible, and resuming later
|
|
13
|
+
* continues from there exactly as a pause would (G4).
|
|
14
|
+
*/
|
|
15
|
+
/** Inline-axis interval, in viewport pixels. */
|
|
16
|
+
export type MarqueeSpan = {
|
|
17
|
+
start: number;
|
|
18
|
+
end: number;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The animation time at which the item sits fully inside the box, or `null` when it already does
|
|
22
|
+
* (so tabbing across visible items never jolts the strip).
|
|
23
|
+
*
|
|
24
|
+
* It aims to CENTRE the item — clear of the `fade` mask at both edges — and settles for any fully
|
|
25
|
+
* visible position when the centre is out of reach.
|
|
26
|
+
*
|
|
27
|
+
* It NEVER wraps into another lap. The copies look identical, but only the first holds the real,
|
|
28
|
+
* focusable item; the clones are `inert`. Seeking by a whole lap therefore moves the focused item a
|
|
29
|
+
* whole copy away while the strip looks unchanged — measured, the first attempt at this fix put
|
|
30
|
+
* every item at exactly `centre − lap` (-139 against a 698 centre, lap 837) at 1440px. So the
|
|
31
|
+
* answer stays inside the iteration the animation is in.
|
|
32
|
+
*
|
|
33
|
+
* `step` is how far the item moved when time advanced by a hundredth of a lap — measured rather
|
|
34
|
+
* than derived, so `direction="end"` (a reversed animation) and the RTL keyframes need no case of their
|
|
35
|
+
* own. A probe that crossed an iteration boundary shows a jump of nearly a whole copy the OTHER
|
|
36
|
+
* way; `lap` (one copy, in px) is what tells the two apart. Only its SIGN is used. It is a hundredth
|
|
37
|
+
* of a lap and not a millisecond because a slow track (99.6s a lap on the logo wall at 320px) moves
|
|
38
|
+
* 0.008px in a millisecond, which measured as 0 — and a zero step meant no seek at all.
|
|
39
|
+
*/
|
|
40
|
+
export declare function marqueeRevealTime({ item, box, time, duration, lap, step, }: {
|
|
41
|
+
item: MarqueeSpan;
|
|
42
|
+
box: MarqueeSpan;
|
|
43
|
+
time: number;
|
|
44
|
+
duration: number;
|
|
45
|
+
lap: number;
|
|
46
|
+
step: number;
|
|
47
|
+
}): number | null;
|
|
48
|
+
/** Wire it to the live DOM. A no-op where there is no running animation (reduced motion, jsdom). */
|
|
49
|
+
export declare function revealInMarquee(viewport: HTMLElement, track: HTMLElement, item: Element): void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function marqueeRevealTime({
|
|
2
|
+
item,
|
|
3
|
+
box,
|
|
4
|
+
time,
|
|
5
|
+
duration,
|
|
6
|
+
lap,
|
|
7
|
+
step
|
|
8
|
+
}) {
|
|
9
|
+
if (item.start >= box.start - 1 && item.end <= box.end + 1) return null;
|
|
10
|
+
if (!(duration > 0) || !(lap > 0) || step === 0) return null;
|
|
11
|
+
const sign = Math.abs(step) < lap / 2 ? Math.sign(step) : -Math.sign(step);
|
|
12
|
+
const pxPerMs = sign * lap / duration;
|
|
13
|
+
const local = (time % duration + duration) % duration;
|
|
14
|
+
const iteration = time - local;
|
|
15
|
+
const low = box.start - item.start;
|
|
16
|
+
const high = box.end - item.end;
|
|
17
|
+
const centre = (box.start + box.end) / 2 - (item.start + item.end) / 2;
|
|
18
|
+
const at = (d) => local + d / pxPerMs;
|
|
19
|
+
const [a, b] = [at(low), at(high)].sort((x, y) => x - y);
|
|
20
|
+
const from = Math.max(0, low <= high ? a : at(centre));
|
|
21
|
+
const to = Math.min(duration, low <= high ? b : at(centre));
|
|
22
|
+
const clamp = (t, lo, hi) => Math.min(hi, Math.max(lo, t));
|
|
23
|
+
const chosen = from <= to ? clamp(at(centre), from, to) : clamp(at(centre), 0, duration);
|
|
24
|
+
return iteration + chosen;
|
|
25
|
+
}
|
|
26
|
+
function revealInMarquee(viewport, track, item) {
|
|
27
|
+
const animation = track.getAnimations?.()[0];
|
|
28
|
+
const copy = track.firstElementChild;
|
|
29
|
+
if (!animation || !copy) return;
|
|
30
|
+
viewport.scrollLeft = 0;
|
|
31
|
+
const span = () => {
|
|
32
|
+
const r = item.getBoundingClientRect();
|
|
33
|
+
return { start: r.left, end: r.right };
|
|
34
|
+
};
|
|
35
|
+
const box = viewport.getBoundingClientRect();
|
|
36
|
+
const time = Number(animation.currentTime ?? 0);
|
|
37
|
+
const duration = Number(animation.effect?.getComputedTiming().duration ?? 0);
|
|
38
|
+
const before = span();
|
|
39
|
+
animation.currentTime = time + duration / 100;
|
|
40
|
+
const step = span().start - before.start;
|
|
41
|
+
animation.currentTime = time;
|
|
42
|
+
const next = marqueeRevealTime({
|
|
43
|
+
item: before,
|
|
44
|
+
box: { start: box.left, end: box.right },
|
|
45
|
+
time,
|
|
46
|
+
duration,
|
|
47
|
+
lap: copy.offsetWidth,
|
|
48
|
+
step
|
|
49
|
+
});
|
|
50
|
+
if (next !== null) animation.currentTime = next;
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
marqueeRevealTime,
|
|
54
|
+
revealInMarquee
|
|
55
|
+
};
|
|
@@ -6,6 +6,7 @@ import { useTranslation } from "../../i18n/use-translation.js";
|
|
|
6
6
|
import { useMediaQuery } from "../../lib/hooks.js";
|
|
7
7
|
import { cn } from "../../lib/utils.js";
|
|
8
8
|
import { Button } from "../general/button.js";
|
|
9
|
+
import { revealInMarquee } from "./marquee-reveal.js";
|
|
9
10
|
import { ScrollArea } from "./scroll-area.js";
|
|
10
11
|
function gapToken(step) {
|
|
11
12
|
if (step === "none" || step === 0) return "0px";
|
|
@@ -33,6 +34,7 @@ const Marquee = React.forwardRef(function Marquee2({
|
|
|
33
34
|
const trackId = React.useId();
|
|
34
35
|
const viewportRef = React.useRef(null);
|
|
35
36
|
const copyRef = React.useRef(null);
|
|
37
|
+
const trackRef = React.useRef(null);
|
|
36
38
|
const [uncontrolledPlay, setUncontrolledPlay] = React.useState(defaultPlay);
|
|
37
39
|
const playing = play ?? uncontrolledPlay;
|
|
38
40
|
const [measurement, setMeasurement] = React.useState({ copies: 2, cycleScale: 1 });
|
|
@@ -111,25 +113,41 @@ const Marquee = React.forwardRef(function Marquee2({
|
|
|
111
113
|
style: animatedStyle,
|
|
112
114
|
...props,
|
|
113
115
|
children: [
|
|
114
|
-
/* @__PURE__ */ jsx(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
{
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
116
|
+
/* @__PURE__ */ jsx(
|
|
117
|
+
"div",
|
|
118
|
+
{
|
|
119
|
+
ref: viewportRef,
|
|
120
|
+
"data-slot": "marquee-viewport",
|
|
121
|
+
className: "ui-marquee-viewport",
|
|
122
|
+
onFocus: (event) => {
|
|
123
|
+
const item = event.target;
|
|
124
|
+
requestAnimationFrame(() => {
|
|
125
|
+
if (viewportRef.current && trackRef.current) {
|
|
126
|
+
revealInMarquee(viewportRef.current, trackRef.current, item);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
children: /* @__PURE__ */ jsxs("div", { ref: trackRef, id: trackId, "data-slot": "marquee-track", className: "ui-marquee-track", children: [
|
|
131
|
+
/* @__PURE__ */ jsx("div", { ref: copyRef, "data-slot": "marquee-copy", className: "ui-marquee-copy", children }),
|
|
132
|
+
Array.from({ length: measurement.copies - 1 }, (_, index) => (
|
|
133
|
+
// Decorative by construction: `aria-hidden` keeps the duplicate out of the
|
|
134
|
+
// accessibility tree and `inert` keeps it out of the tab order. The prior art has
|
|
135
|
+
// neither, which is why its content is announced once per screenful of width.
|
|
136
|
+
/* @__PURE__ */ jsx(
|
|
137
|
+
"div",
|
|
138
|
+
{
|
|
139
|
+
"data-slot": "marquee-clone",
|
|
140
|
+
className: "ui-marquee-copy",
|
|
141
|
+
"aria-hidden": "true",
|
|
142
|
+
inert: true,
|
|
143
|
+
children
|
|
144
|
+
},
|
|
145
|
+
index
|
|
146
|
+
)
|
|
147
|
+
))
|
|
148
|
+
] })
|
|
149
|
+
}
|
|
150
|
+
),
|
|
133
151
|
/* @__PURE__ */ jsx(
|
|
134
152
|
Button,
|
|
135
153
|
{
|
|
@@ -2,17 +2,23 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { isDevelopment } from "../../lib/dev.js";
|
|
5
|
-
import { useInView } from "../../lib/hooks.js";
|
|
5
|
+
import { scrollBoxOf, useInView } from "../../lib/hooks.js";
|
|
6
6
|
import { cn } from "../../lib/utils.js";
|
|
7
7
|
const OVERSHOOT_MARGIN = "1000000px";
|
|
8
8
|
const ROOT_MARGIN_BLOCK_START = `0px 0px ${OVERSHOOT_MARGIN} 0px`;
|
|
9
9
|
const ROOT_MARGIN_BLOCK_END = `${OVERSHOOT_MARGIN} 0px 0px 0px`;
|
|
10
|
-
function resolveTarget(target) {
|
|
11
|
-
if (!target) return
|
|
10
|
+
function resolveTarget(target, from) {
|
|
11
|
+
if (!target) return scrollBoxOf(from);
|
|
12
12
|
const node = target();
|
|
13
13
|
if (!node || typeof window === "undefined" || node === window) return null;
|
|
14
14
|
return node;
|
|
15
15
|
}
|
|
16
|
+
function isRendered(node) {
|
|
17
|
+
for (let p = node; p; p = p.parentElement) {
|
|
18
|
+
if (window.getComputedStyle(p).display === "none") return false;
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
16
22
|
function fixedContainingBlock(node) {
|
|
17
23
|
if (typeof window === "undefined") return null;
|
|
18
24
|
for (let n = node?.parentElement ?? null; n && n !== document.documentElement; n = n.parentElement) {
|
|
@@ -53,20 +59,22 @@ const Affix = React.forwardRef(function Affix2({
|
|
|
53
59
|
const pinToEnd = offsetBlockStart === void 0 && offsetBlockEnd !== void 0;
|
|
54
60
|
const [targetElement, setTargetElement] = React.useState(null);
|
|
55
61
|
React.useEffect(() => {
|
|
56
|
-
setTargetElement(resolveTarget(target));
|
|
62
|
+
setTargetElement(resolveTarget(target, rootRef.current));
|
|
57
63
|
}, [target]);
|
|
58
64
|
const inView = useInView(sentinelRef, {
|
|
59
65
|
root: targetElement,
|
|
60
66
|
rootMargin: pinToEnd ? ROOT_MARGIN_BLOCK_END : ROOT_MARGIN_BLOCK_START,
|
|
61
67
|
assumeInView: true
|
|
62
68
|
});
|
|
63
|
-
const
|
|
69
|
+
const [rendered, setRendered] = React.useState(true);
|
|
70
|
+
const affixed = !inView && rendered;
|
|
64
71
|
const [box, setBox] = React.useState(null);
|
|
65
72
|
const [targetInset, setTargetInset] = React.useState(0);
|
|
66
73
|
const measure = React.useCallback(() => {
|
|
67
74
|
const root = rootRef.current;
|
|
68
75
|
const content = contentRef.current;
|
|
69
76
|
if (!root || !content) return;
|
|
77
|
+
setRendered(isRendered(root));
|
|
70
78
|
const rootRect = root.getBoundingClientRect();
|
|
71
79
|
const contentRect = content.getBoundingClientRect();
|
|
72
80
|
setBox(
|
|
@@ -177,7 +185,7 @@ const Affix = React.forwardRef(function Affix2({
|
|
|
177
185
|
"data-slot": "affix-placeholder",
|
|
178
186
|
"aria-hidden": "true",
|
|
179
187
|
className: "ui-affix-placeholder",
|
|
180
|
-
style: { blockSize: box.block }
|
|
188
|
+
style: { blockSize: box.block, inlineSize: box.inline }
|
|
181
189
|
}
|
|
182
190
|
) : null,
|
|
183
191
|
/* @__PURE__ */ jsx(
|
|
@@ -3,7 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
5
5
|
import { isDevelopment } from "../../lib/dev.js";
|
|
6
|
-
import { useControlledLatch } from "../../lib/hooks.js";
|
|
6
|
+
import { scrollBoxOf, useControlledLatch } from "../../lib/hooks.js";
|
|
7
7
|
import { cn, prefersReducedMotion } from "../../lib/utils.js";
|
|
8
8
|
import { Affix } from "../layout/affix.js";
|
|
9
9
|
const SHARP_MATCHER = /#([^\t\r\n\f\v]+)$/;
|
|
@@ -120,7 +120,7 @@ function Anchor({
|
|
|
120
120
|
}, []);
|
|
121
121
|
const container = React.useCallback(() => {
|
|
122
122
|
if (target) return target() ?? window;
|
|
123
|
-
return getContainer?.() ?? window;
|
|
123
|
+
return getContainer?.() ?? scrollBoxOf(navRef.current) ?? window;
|
|
124
124
|
}, [target, getContainer]);
|
|
125
125
|
const line = targetOffsetBlockStart ?? offsetBlockStart ?? 0;
|
|
126
126
|
const resolveFromScroll = React.useCallback(() => {
|
|
@@ -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": "30.
|
|
3
|
+
"version": "30.9.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,
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -70,6 +70,13 @@ export declare function useScrollsHorizontally(ref: RefObject<HTMLElement | null
|
|
|
70
70
|
* them would otherwise write this walk again.
|
|
71
71
|
*/
|
|
72
72
|
export declare function scrollParent(el: HTMLElement | null): HTMLElement | null;
|
|
73
|
+
/**
|
|
74
|
+
* `scrollParent` for a caller that hands the answer to `IntersectionObserver` or a scroll listener:
|
|
75
|
+
* the root and body elements are the VIEWPORT by another name (a reset writing
|
|
76
|
+
* `html { overflow-y: scroll }` makes the root "scroll"), and the answer for them is `null`.
|
|
77
|
+
* Affix and Anchor use it as their default scroll box (gh#984).
|
|
78
|
+
*/
|
|
79
|
+
export declare function scrollBoxOf(el: HTMLElement | null): HTMLElement | null;
|
|
73
80
|
/**
|
|
74
81
|
* Has this element entered the viewport (or `root`) yet?
|
|
75
82
|
*
|
package/dist/lib/hooks.js
CHANGED
|
@@ -151,6 +151,11 @@ function scrollParent(el) {
|
|
|
151
151
|
}
|
|
152
152
|
return null;
|
|
153
153
|
}
|
|
154
|
+
function scrollBoxOf(el) {
|
|
155
|
+
if (typeof document === "undefined") return null;
|
|
156
|
+
const box = scrollParent(el);
|
|
157
|
+
return box === document.documentElement || box === document.body ? null : box;
|
|
158
|
+
}
|
|
154
159
|
function useInView(ref, {
|
|
155
160
|
enabled = true,
|
|
156
161
|
once = false,
|
|
@@ -192,6 +197,7 @@ function useInView(ref, {
|
|
|
192
197
|
return !enabled || inView;
|
|
193
198
|
}
|
|
194
199
|
export {
|
|
200
|
+
scrollBoxOf,
|
|
195
201
|
scrollParent,
|
|
196
202
|
useControlledLatch,
|
|
197
203
|
useDebouncedValue,
|
|
@@ -1876,7 +1876,8 @@ export type MasonryProp<TData = unknown> = {
|
|
|
1876
1876
|
*
|
|
1877
1877
|
* It is a FUNCTION and not an element because the element does not exist on the render that
|
|
1878
1878
|
* declares it: the caller writes `target={() => scrollRef.current}` and the component calls it
|
|
1879
|
-
* after mount. `window`
|
|
1879
|
+
* after mount. `() => window` means the document viewport. Omitted, it is the nearest ancestor
|
|
1880
|
+
* that scrolls on the block axis (`position: sticky`'s rule), and the viewport only when none does.
|
|
1880
1881
|
* @see Affix
|
|
1881
1882
|
*/
|
|
1882
1883
|
export type AffixTargetProp = () => Window | HTMLElement | null;
|
|
@@ -1931,7 +1932,7 @@ export type AffixProp = {
|
|
|
1931
1932
|
* @deprecated Ant Design spells this `offsetBottom`; in `@godxjp/ui` it is `offsetBlockEnd`.
|
|
1932
1933
|
*/
|
|
1933
1934
|
offsetBottom?: never;
|
|
1934
|
-
/** The scroll box to pin against. Ant Design `target
|
|
1935
|
+
/** The scroll box to pin against. Ant Design `target`; omitted → the nearest block-axis scroller, else the viewport. */
|
|
1935
1936
|
target?: AffixTargetProp;
|
|
1936
1937
|
/**
|
|
1937
1938
|
* Fires when the pinned state FLIPS, and only then — never on a scroll frame that did not
|
|
@@ -867,12 +867,14 @@ export type AnchorProp = {
|
|
|
867
867
|
* The scroll box the sections are measured in AND the box `Affix` pins the nav against — one
|
|
868
868
|
* function, both halves (gh#890). `AffixTargetProp`, the same lazy-getter shape and the same
|
|
869
869
|
* name `Affix.target` / `FloatButton.BackTop.target` already spell here, so a consumer who has
|
|
870
|
-
* scoped one scrolling component already knows this one. `null`
|
|
871
|
-
*
|
|
870
|
+
* scoped one scrolling component already knows this one. `null` means the viewport. Absent (with
|
|
871
|
+
* no `getContainer`), it is the nearest ancestor that scrolls on the block axis, else the
|
|
872
|
+
* viewport — `Affix`'s own default (gh#984). Wins over `getContainer` when both are given.
|
|
872
873
|
*/
|
|
873
874
|
target?: AffixTargetProp;
|
|
874
875
|
/**
|
|
875
|
-
* The scroll box holding the sections. Ant Design `getContainer
|
|
876
|
+
* The scroll box holding the sections. Ant Design `getContainer`; omitted → the nearest
|
|
877
|
+
* block-axis scroller, else the viewport (gh#984).
|
|
876
878
|
*
|
|
877
879
|
* Superseded by `target`, which mirrors `Affix`'s own spelling for the identical idea; kept,
|
|
878
880
|
* still live, for a call site written before `target` existed.
|