@godxjp/ui 30.7.0 → 30.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.
- package/README.md +16 -3
- package/agent/START-HERE.md +1 -1
- package/agent/components/Affix.json +1 -1
- package/agent/components/Anchor.json +2 -2
- package/agent/components/RecordPicker.json +1 -1
- package/agent/components.json +4 -4
- package/agent/index.json +2 -2
- package/agent/llms.txt +3 -3
- package/agent/rules.json +1 -1
- package/dist/app/app-provider.js +1 -5
- package/dist/app/timezones.d.ts +15 -0
- package/dist/app/timezones.js +5 -0
- 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/data-entry/record-picker.d.ts +26 -0
- package/dist/components/data-entry/record-picker.js +16 -12
- 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/datetime/sync.d.ts +1 -1
- package/dist/lib/datetime/sync.js +8 -7
- package/dist/lib/hooks.d.ts +7 -0
- package/dist/lib/hooks.js +6 -0
- package/dist/props/components/data-entry.prop.d.ts +9 -1
- package/dist/props/components/layout.prop.d.ts +3 -2
- package/dist/props/components/navigation.prop.d.ts +5 -3
- package/dist/styles/navigation-layout.css +10 -0
- package/dist/styles/table-layout.css +47 -33
- 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 +2 -2
- package/scripts/explain-token.mjs +55 -2
package/README.md
CHANGED
|
@@ -160,9 +160,22 @@ The framework ships colors, the type scale, the wa-iro palette, and (opt-in) bun
|
|
|
160
160
|
@source "../views";
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
-
###
|
|
164
|
-
|
|
165
|
-
`@godxjp/ui/styles` is the zero-config all-in-one
|
|
163
|
+
### Where the CSS weight actually is — the font faces, not the components
|
|
164
|
+
|
|
165
|
+
`@godxjp/ui/styles` is the zero-config all-in-one: every component's layers **plus 729 `@font-face`
|
|
166
|
+
declarations** for the bundled CJK faces. Those declarations are render-blocking CSS on every page,
|
|
167
|
+
separate from the font FILES (which load lazily, per glyph range). Measured on a fresh Vite 8 +
|
|
168
|
+
Tailwind v4 app using 15 components (gh#971):
|
|
169
|
+
|
|
170
|
+
| entry | CSS emitted | gzip |
|
|
171
|
+
| ------------------------ | ----------- | ---------- |
|
|
172
|
+
| `@godxjp/ui/styles` | 1,303,803 B | **367 KB** |
|
|
173
|
+
| `@godxjp/ui/styles/core` | 581,210 B | **86 KB** |
|
|
174
|
+
|
|
175
|
+
So if CSS weight matters, **the one change that moves it is the import line** — 77% of it is the face
|
|
176
|
+
declarations. `core` is not a per-component slice: it still carries every component's layers (about
|
|
177
|
+
64 KB gzip for all ~165), it only drops the faces. Load fonts another way (next/font, a system stack,
|
|
178
|
+
your own `@font-face`) and take the same layers without them:
|
|
166
179
|
|
|
167
180
|
```css
|
|
168
181
|
@import "@godxjp/ui/styles/core"; /* every component layer, no @font-face */
|
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.8.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
|
},
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"type": "(SearchSelectOptionProp | SelectOptionGroupProp)[]"
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
|
-
"description": "Server fetcher, debounced, and it runs on BOTH branches — a picker whose `count` is small still fetches from the server rather than showing an empty dropdown (gh#942). `filters` carries YOUR OWN vocabulary back (the `name` of each declared filter), so the server reads the keys it already understands instead of a shape this component invented; the dropdown branch draws no filters, so it passes an empty object. Return `nextCursor` to page: the dialog shows a Load more button and APPENDS, so nothing the user already scrolled past is lost.",
|
|
36
|
+
"description": "Server fetcher, debounced, and it runs on BOTH branches — a picker whose `count` is small still fetches from the server rather than showing an empty dropdown (gh#942). `filters` carries YOUR OWN vocabulary back (the `name` of each declared filter), so the server reads the keys it already understands instead of a shape this component invented; the dropdown branch draws no filters, so it passes an empty object. Return `nextCursor` to page: the dialog shows a Load more button and APPENDS, so nothing the user already scrolled past is lost. `cursor` is EXACTLY the `nextCursor` your previous response returned for the same `query` (undefined on the first page) — the same in all three shapes, dropdown included; an offset or an opaque token both work. Before gh#964 the dropdown sent the page NUMBER instead, so an offset server read \"2\" as offset 2 and returned duplicate rows; if you worked around that, remove the workaround.",
|
|
37
37
|
"name": "loadOptions",
|
|
38
38
|
"type": "(params: { query: string; filters: Record<string, string>; cursor?: string }) => Promise<{ options: SearchSelectOptionProp[]; count?: number; nextCursor?: string }>"
|
|
39
39
|
},
|
package/agent/components.json
CHANGED
|
@@ -6019,7 +6019,7 @@
|
|
|
6019
6019
|
"type": "(SearchSelectOptionProp | SelectOptionGroupProp)[]"
|
|
6020
6020
|
},
|
|
6021
6021
|
{
|
|
6022
|
-
"description": "Server fetcher, debounced, and it runs on BOTH branches — a picker whose `count` is small still fetches from the server rather than showing an empty dropdown (gh#942). `filters` carries YOUR OWN vocabulary back (the `name` of each declared filter), so the server reads the keys it already understands instead of a shape this component invented; the dropdown branch draws no filters, so it passes an empty object. Return `nextCursor` to page: the dialog shows a Load more button and APPENDS, so nothing the user already scrolled past is lost.",
|
|
6022
|
+
"description": "Server fetcher, debounced, and it runs on BOTH branches — a picker whose `count` is small still fetches from the server rather than showing an empty dropdown (gh#942). `filters` carries YOUR OWN vocabulary back (the `name` of each declared filter), so the server reads the keys it already understands instead of a shape this component invented; the dropdown branch draws no filters, so it passes an empty object. Return `nextCursor` to page: the dialog shows a Load more button and APPENDS, so nothing the user already scrolled past is lost. `cursor` is EXACTLY the `nextCursor` your previous response returned for the same `query` (undefined on the first page) — the same in all three shapes, dropdown included; an offset or an opaque token both work. Before gh#964 the dropdown sent the page NUMBER instead, so an offset server read \"2\" as offset 2 and returned duplicate rows; if you worked around that, remove the workaround.",
|
|
6023
6023
|
"name": "loadOptions",
|
|
6024
6024
|
"type": "(params: { query: string; filters: Record<string, string>; cursor?: string }) => Promise<{ options: SearchSelectOptionProp[]; count?: number; nextCursor?: string }>"
|
|
6025
6025
|
},
|
|
@@ -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.8.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.8.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.8.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.8.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.8.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.
|
package/agent/rules.json
CHANGED
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
"title": "Long text columns get an explicit width"
|
|
196
196
|
},
|
|
197
197
|
{
|
|
198
|
-
"body": "Author and verify every page at 320–390px FIRST. Page sections are spaced by `PageContainer` itself (`--page-body-gap` between its direct children). Inside a section spacing comes only from `Flex` `gap` (vertical rhythm = `Flex direction=\"col\"`, control rows = the default `direction=\"row\"`) + `ResponsiveGrid columns={2|3|4}` (which collapse to a single column on narrow screens) — never raw `p-*` / `gap-*` / `space-*` utilities for page layout (the static audit rejects them). Styles are loaded whole: `@godxjp/ui/styles` or `styles/core`, never a hand-picked set of layers.",
|
|
198
|
+
"body": "Author and verify every page at 320–390px FIRST. Page sections are spaced by `PageContainer` itself (`--page-body-gap` between its direct children). Inside a section spacing comes only from `Flex` `gap` (vertical rhythm = `Flex direction=\"col\"`, control rows = the default `direction=\"row\"`) + `ResponsiveGrid columns={2|3|4}` (which collapse to a single column on narrow screens) — never raw `p-*` / `gap-*` / `space-*` utilities for page layout (the static audit rejects them). Styles are loaded whole: `@godxjp/ui/styles` or `styles/core`, never a hand-picked set of layers. They differ ONLY in the 729 bundled CJK `@font-face` declarations, and that difference is most of the CSS: 367 KB vs 86 KB gzip for a 15-component app (gh#971) — pick `styles/core` whenever the app loads fonts another way.",
|
|
199
199
|
"number": 40,
|
|
200
200
|
"title": "Pages are mobile-first"
|
|
201
201
|
},
|
package/dist/app/app-provider.js
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
PREFERS_DARK_SCHEME_QUERY
|
|
28
28
|
} from "./theme-axes.js";
|
|
29
29
|
import { resolveDefaultTimeFormat } from "./time-format-labels.js";
|
|
30
|
-
import { resolveDefaultTimezone } from "./timezones.js";
|
|
30
|
+
import { resolveDefaultTimezone, resolveHydrationSafeTimezone } from "./timezones.js";
|
|
31
31
|
import {
|
|
32
32
|
APP_REQUEST_HEADER_DATE_FORMAT,
|
|
33
33
|
APP_REQUEST_HEADER_LOCALE,
|
|
@@ -53,10 +53,6 @@ function buildRequestHeaders(locale, timezone, timeFormat, dateFormat) {
|
|
|
53
53
|
[APP_REQUEST_HEADER_DATE_FORMAT]: dateFormat
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
|
-
function resolveHydrationSafeTimezone(defaultTimezone, systemTimezone) {
|
|
57
|
-
if (defaultTimezone === "browser") return systemTimezone ?? "UTC";
|
|
58
|
-
return resolveDefaultTimezone(defaultTimezone, systemTimezone);
|
|
59
|
-
}
|
|
60
56
|
function AppProvider({
|
|
61
57
|
children,
|
|
62
58
|
defaultLocale = "vi",
|
package/dist/app/timezones.d.ts
CHANGED
|
@@ -24,5 +24,20 @@ export declare function getTimezoneOffsetLabel(timezone: string, locale?: AppLoc
|
|
|
24
24
|
export declare function formatTimezoneDisplayLabel(timezone: string, locale?: AppLocale): string;
|
|
25
25
|
export declare function getTimezoneLabel(timezone: string, locale: AppLocale, fallbackLocale?: AppLocale): string;
|
|
26
26
|
export declare function resolveDefaultTimezone(defaultTimezone: "browser" | "system" | (string & {}), systemTimezone?: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* The timezone to use BEFORE the browser's is known — the first render, and anything that formats
|
|
29
|
+
* before AppProvider has synced (gh#968).
|
|
30
|
+
*
|
|
31
|
+
* `"browser"` answers UTC here, not the browser's zone: a server cannot know the browser's zone, so
|
|
32
|
+
* resolving it on the first render would render one time on the server and another on the client.
|
|
33
|
+
* AppProvider settles to the real browser zone in an effect afterwards.
|
|
34
|
+
*
|
|
35
|
+
* It lives here, not inside AppProvider, so the module-level datetime fallback (`lib/datetime/sync`)
|
|
36
|
+
* can call the SAME function. Before gh#968 that fallback hard-coded `Asia/Ho_Chi_Minh` while this
|
|
37
|
+
* answered UTC — two different answers to "nothing is configured yet". The Vietnamese one was the
|
|
38
|
+
* worse of the two because it looks right: a JST attendance timestamp read 2 hours off, and nothing
|
|
39
|
+
* about `01/05/2026 14:30` says so. UTC is at least visibly not the user's local time.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveHydrationSafeTimezone(defaultTimezone: "browser" | "system" | (string & {}), systemTimezone?: string): string;
|
|
27
42
|
/** Vitest only — reset cached IANA list between cases. */
|
|
28
43
|
export declare function resetIanaTimezoneCacheForTests(): void;
|
package/dist/app/timezones.js
CHANGED
|
@@ -141,6 +141,10 @@ function resolveDefaultTimezone(defaultTimezone, systemTimezone) {
|
|
|
141
141
|
if (defaultTimezone === "system") return systemTimezone ?? "UTC";
|
|
142
142
|
return defaultTimezone;
|
|
143
143
|
}
|
|
144
|
+
function resolveHydrationSafeTimezone(defaultTimezone, systemTimezone) {
|
|
145
|
+
if (defaultTimezone === "browser") return systemTimezone ?? "UTC";
|
|
146
|
+
return resolveDefaultTimezone(defaultTimezone, systemTimezone);
|
|
147
|
+
}
|
|
144
148
|
function resetIanaTimezoneCacheForTests() {
|
|
145
149
|
cachedAllTimezones = null;
|
|
146
150
|
cachedTimezoneSet = null;
|
|
@@ -157,6 +161,7 @@ export {
|
|
|
157
161
|
isValidIanaTimezone,
|
|
158
162
|
resetIanaTimezoneCacheForTests,
|
|
159
163
|
resolveDefaultTimezone,
|
|
164
|
+
resolveHydrationSafeTimezone,
|
|
160
165
|
resolveTimezoneForIntl,
|
|
161
166
|
resolveTimezonePickerOptions
|
|
162
167
|
};
|
|
@@ -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
|
{
|
|
@@ -33,6 +33,32 @@ export type { RecordPickerProp, RecordPickerProp as RecordPickerProps };
|
|
|
33
33
|
* hands over the labels it already has, and it is merged ahead of whatever loads later, so a chip
|
|
34
34
|
* never renders as a raw id and never disappears when the query changes.
|
|
35
35
|
*/
|
|
36
|
+
/**
|
|
37
|
+
* Adapt RecordPicker's cursor contract to Select's page-numbered loader (gh#964).
|
|
38
|
+
*
|
|
39
|
+
* `loadOptions` is typed to receive the `nextCursor` the server returned last time — an offset, an
|
|
40
|
+
* opaque token, whatever the server minted. The inline and dialog shapes pass it straight back.
|
|
41
|
+
* This adapter used to pass `String(page)` instead, so page 2 reached the server as "2" and a
|
|
42
|
+
* consumer implementing the type as written read it as offset 2: duplicated and missing rows, with
|
|
43
|
+
* no error. The server's `nextCursor` was used only as a has-more flag.
|
|
44
|
+
*
|
|
45
|
+
* Select asks for pages by number, so the adapter remembers, per query, which cursor fetches which
|
|
46
|
+
* page: page 1 has none, and page N+1 is whatever page N's response said. A new query starts its
|
|
47
|
+
* own chain, so one query's cursor can never be sent with another's text — a user typing,
|
|
48
|
+
* deleting and retyping can make Select ask for page 2 of an earlier query after a later one began.
|
|
49
|
+
*
|
|
50
|
+
* The chains live in this closure, so they are discarded whenever `loadOptions` itself changes
|
|
51
|
+
* (the caller memoises on it): a cursor minted by one data source is meaningless to the next.
|
|
52
|
+
*
|
|
53
|
+
* @internal Exported for tests only; the package barrel re-exports RecordPicker by name, not this.
|
|
54
|
+
*/
|
|
55
|
+
export declare function selectLoadOptionsFor(loadOptions: RecordPickerProp["loadOptions"]): (({ query, page }: {
|
|
56
|
+
query: string;
|
|
57
|
+
page: number;
|
|
58
|
+
}) => Promise<{
|
|
59
|
+
options: SearchSelectOptionProp[];
|
|
60
|
+
hasMore: boolean;
|
|
61
|
+
}>) | undefined;
|
|
36
62
|
export declare const RecordPicker: React.ForwardRefExoticComponent<Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "defaultValue" | "onChange" | "value"> & {
|
|
37
63
|
mode?: "single" | "multiple";
|
|
38
64
|
shape?: "auto" | "inline";
|
|
@@ -21,6 +21,19 @@ import {
|
|
|
21
21
|
import { Command, CommandGroup } from "./command.js";
|
|
22
22
|
import { Input } from "./input.js";
|
|
23
23
|
import { Select } from "./select.js";
|
|
24
|
+
function selectLoadOptionsFor(loadOptions) {
|
|
25
|
+
if (!loadOptions) return void 0;
|
|
26
|
+
const chains = /* @__PURE__ */ new Map();
|
|
27
|
+
return async ({ query, page }) => {
|
|
28
|
+
if (page === 1) chains.set(query, /* @__PURE__ */ new Map());
|
|
29
|
+
const chain = chains.get(query);
|
|
30
|
+
const cursor = page === 1 ? void 0 : chain?.get(page);
|
|
31
|
+
if (page > 1 && cursor === void 0) return { options: [], hasMore: false };
|
|
32
|
+
const r = await loadOptions({ query, filters: {}, cursor });
|
|
33
|
+
if (r.nextCursor !== void 0) chains.get(query)?.set(page + 1, r.nextCursor);
|
|
34
|
+
return { options: r.options, hasMore: r.nextCursor !== void 0 };
|
|
35
|
+
};
|
|
36
|
+
}
|
|
24
37
|
const RecordPicker = React.forwardRef(
|
|
25
38
|
function RecordPicker2({
|
|
26
39
|
mode = "single",
|
|
@@ -57,17 +70,7 @@ const RecordPicker = React.forwardRef(
|
|
|
57
70
|
},
|
|
58
71
|
[controlled, isMultiple, onValueChange]
|
|
59
72
|
);
|
|
60
|
-
const selectLoadOptions = React.useMemo(
|
|
61
|
-
() => loadOptions ? async ({ query, page }) => {
|
|
62
|
-
const r = await loadOptions({
|
|
63
|
-
query,
|
|
64
|
-
filters: {},
|
|
65
|
-
cursor: page > 1 ? String(page) : void 0
|
|
66
|
-
});
|
|
67
|
-
return { options: r.options, hasMore: r.nextCursor !== void 0 };
|
|
68
|
-
} : void 0,
|
|
69
|
-
[loadOptions]
|
|
70
|
-
);
|
|
73
|
+
const selectLoadOptions = React.useMemo(() => selectLoadOptionsFor(loadOptions), [loadOptions]);
|
|
71
74
|
const staticOptions = React.useMemo(
|
|
72
75
|
() => options ? normalizeSelectOptions(options) : [],
|
|
73
76
|
[options]
|
|
@@ -542,5 +545,6 @@ const DialogPicker = React.forwardRef(function DialogPicker2({
|
|
|
542
545
|
] });
|
|
543
546
|
});
|
|
544
547
|
export {
|
|
545
|
-
RecordPicker
|
|
548
|
+
RecordPicker,
|
|
549
|
+
selectLoadOptionsFor
|
|
546
550
|
};
|
|
@@ -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.8.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,
|
|
@@ -8,7 +8,7 @@ export type DatetimeContext = {
|
|
|
8
8
|
dateFormat: AppDateFormat;
|
|
9
9
|
};
|
|
10
10
|
/** Sync module-level datetime prefs from AppProvider (mirrors syncI18nLocale). */
|
|
11
|
-
export declare function syncDatetimeContext(
|
|
11
|
+
export declare function syncDatetimeContext(prefs: Pick<DatetimeContext, "locale" | "timezone" | "timeFormat" | "dateFormat"> & {
|
|
12
12
|
dateFnsLocale?: Locale;
|
|
13
13
|
}): void;
|
|
14
14
|
export declare function getDatetimeContext(): Readonly<DatetimeContext>;
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { getDateFnsLocale } from "../../app/locales.js";
|
|
2
2
|
import { resolveDefaultDateFormat } from "../../app/date-format-labels.js";
|
|
3
|
+
import { resolveHydrationSafeTimezone } from "../../app/timezones.js";
|
|
3
4
|
const DEFAULT_LOCALE = "vi";
|
|
4
5
|
const defaultContext = () => ({
|
|
5
6
|
locale: DEFAULT_LOCALE,
|
|
6
7
|
dateFnsLocale: getDateFnsLocale(DEFAULT_LOCALE),
|
|
7
|
-
timezone: "
|
|
8
|
+
timezone: resolveHydrationSafeTimezone("browser"),
|
|
8
9
|
timeFormat: "24h",
|
|
9
10
|
dateFormat: resolveDefaultDateFormat(DEFAULT_LOCALE)
|
|
10
11
|
});
|
|
11
12
|
let syncedContext = defaultContext();
|
|
12
13
|
let liveRelativeFormattingEnabled = true;
|
|
13
|
-
function syncDatetimeContext(
|
|
14
|
+
function syncDatetimeContext(prefs) {
|
|
14
15
|
syncedContext = {
|
|
15
|
-
locale:
|
|
16
|
-
timezone:
|
|
17
|
-
timeFormat:
|
|
18
|
-
dateFormat:
|
|
19
|
-
dateFnsLocale:
|
|
16
|
+
locale: prefs.locale,
|
|
17
|
+
timezone: prefs.timezone,
|
|
18
|
+
timeFormat: prefs.timeFormat,
|
|
19
|
+
dateFormat: prefs.dateFormat,
|
|
20
|
+
dateFnsLocale: prefs.dateFnsLocale ?? getDateFnsLocale(prefs.locale)
|
|
20
21
|
};
|
|
21
22
|
}
|
|
22
23
|
function getDatetimeContext() {
|
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,
|
|
@@ -1170,7 +1170,15 @@ export type RecordPickerProp = Omit<React.ButtonHTMLAttributes<HTMLButtonElement
|
|
|
1170
1170
|
onValueChange?: (value: string | string[] | null) => void;
|
|
1171
1171
|
/** Static rows, client-filtered. Provide this OR `loadOptions`. */
|
|
1172
1172
|
options?: (SearchSelectOptionProp | SelectOptionGroupProp)[];
|
|
1173
|
-
/**
|
|
1173
|
+
/**
|
|
1174
|
+
* Server fetcher. `filters` carries the consumer's own filter vocabulary.
|
|
1175
|
+
*
|
|
1176
|
+
* `cursor` is EXACTLY the `nextCursor` your previous response returned for the same `query` —
|
|
1177
|
+
* an offset, an opaque token, whatever your server minted — and `undefined` for the first page.
|
|
1178
|
+
* The same in every shape: under `threshold` (dropdown), `inline`, and the dialog. Return no
|
|
1179
|
+
* `nextCursor` when the list is finished; that is what ends paging. (Before gh#964 the dropdown
|
|
1180
|
+
* sent the page NUMBER here instead, so an offset-based server read "2" as offset 2.)
|
|
1181
|
+
*/
|
|
1174
1182
|
loadOptions?: (params: {
|
|
1175
1183
|
query: string;
|
|
1176
1184
|
filters: Record<string, string>;
|
|
@@ -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.
|
|
@@ -52,6 +52,16 @@
|
|
|
52
52
|
border-width: var(--stroke-hairline);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
[data-slot="tabs-list"][data-variant="line"] {
|
|
56
|
+
position: relative;
|
|
57
|
+
z-index: 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
[data-slot="tabs"]:has([data-slot="tabs-list"][data-variant="line"])
|
|
61
|
+
> :is([data-slot="tabs-panel"], [data-slot="tabs-content"]) {
|
|
62
|
+
isolation: isolate;
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
[data-slot="tabs-list"][data-variant="line"] [data-slot="tabs-trigger"]::after {
|
|
56
66
|
content: "";
|
|
57
67
|
position: absolute;
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
.ui-table-row[data-tone] {
|
|
63
|
-
|
|
63
|
+
|
|
64
|
+
--table-row-tone-layer: linear-gradient(
|
|
64
65
|
var(
|
|
65
66
|
--table-row-tone-surface,
|
|
66
67
|
hsl(var(--table-row-tone-color) / var(--table-row-tone-wash-alpha))
|
|
@@ -70,6 +71,7 @@
|
|
|
70
71
|
hsl(var(--table-row-tone-color) / var(--table-row-tone-wash-alpha))
|
|
71
72
|
)
|
|
72
73
|
);
|
|
74
|
+
background-image: var(--table-row-tone-layer);
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
:where([data-slot="table"] > tbody > tr:nth-child(even of :not([data-expanded-row]))) {
|
|
@@ -440,48 +442,60 @@
|
|
|
440
442
|
box-shadow: 6px 0 6px -5px hsl(var(--foreground) / 0.12);
|
|
441
443
|
}
|
|
442
444
|
|
|
445
|
+
:where(.ui-table-row[data-tone]) > :is(.ui-data-table-pin-end, .ui-data-table-pin-start) {
|
|
446
|
+
background-image: var(--table-row-tone-layer);
|
|
447
|
+
}
|
|
448
|
+
|
|
443
449
|
:where([data-slot="table"] > tbody > tr:nth-child(even of :not([data-expanded-row])))
|
|
444
450
|
> :is(.ui-data-table-pin-end, .ui-data-table-pin-start) {
|
|
445
|
-
background-image:
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
451
|
+
background-image:
|
|
452
|
+
var(--table-row-tone-layer, none),
|
|
453
|
+
linear-gradient(
|
|
454
|
+
color-mix(
|
|
455
|
+
in srgb,
|
|
456
|
+
var(--table-row-striped-background, hsl(var(--muted) / 0.8))
|
|
457
|
+
var(--table-row-striped-alpha),
|
|
458
|
+
transparent
|
|
459
|
+
),
|
|
460
|
+
color-mix(
|
|
461
|
+
in srgb,
|
|
462
|
+
var(--table-row-striped-background, hsl(var(--muted) / 0.8))
|
|
463
|
+
var(--table-row-striped-alpha),
|
|
464
|
+
transparent
|
|
465
|
+
)
|
|
466
|
+
);
|
|
457
467
|
}
|
|
458
468
|
|
|
459
469
|
.ui-data-table-surface tbody tr:hover > .ui-data-table-pin-end,
|
|
460
470
|
.ui-data-table-surface tbody tr:hover > .ui-data-table-pin-start {
|
|
461
|
-
background-image:
|
|
462
|
-
var(
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
+
background-image:
|
|
472
|
+
var(--table-row-tone-layer, none),
|
|
473
|
+
linear-gradient(
|
|
474
|
+
var(
|
|
475
|
+
--table-row-hover-background,
|
|
476
|
+
hsl(var(--accent) / var(--table-row-hover-background-alpha, 0.7))
|
|
477
|
+
),
|
|
478
|
+
var(
|
|
479
|
+
--table-row-hover-background,
|
|
480
|
+
hsl(var(--accent) / var(--table-row-hover-background-alpha, 0.7))
|
|
481
|
+
)
|
|
482
|
+
);
|
|
471
483
|
}
|
|
472
484
|
|
|
473
485
|
.ui-data-table-surface tbody tr[data-state="selected"] > .ui-data-table-pin-end,
|
|
474
486
|
.ui-data-table-surface tbody tr[data-state="selected"] > .ui-data-table-pin-start {
|
|
475
|
-
background-image:
|
|
476
|
-
var(
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
487
|
+
background-image:
|
|
488
|
+
var(--table-row-tone-layer, none),
|
|
489
|
+
linear-gradient(
|
|
490
|
+
var(
|
|
491
|
+
--table-row-selected-background,
|
|
492
|
+
hsl(var(--muted) / var(--table-row-selected-background-alpha, 0.3))
|
|
493
|
+
),
|
|
494
|
+
var(
|
|
495
|
+
--table-row-selected-background,
|
|
496
|
+
hsl(var(--muted) / var(--table-row-selected-background-alpha, 0.3))
|
|
497
|
+
)
|
|
498
|
+
);
|
|
485
499
|
}
|
|
486
500
|
|
|
487
501
|
.ui-data-table-ellipsis {
|
package/docs/TOKEN-RESOLUTION.md
CHANGED
|
@@ -109,6 +109,14 @@ selector, its value, and whether it is a freeze; then every read and whether tha
|
|
|
109
109
|
call-site fallback. If two components move together, run it on the token they share and the shared
|
|
110
110
|
declaration is on the screen.
|
|
111
111
|
|
|
112
|
+
**Before you rely on a token, read its `published:` line.** `yes` is the contract; `NO` is an
|
|
113
|
+
internal variable that can change or vanish in any release — `--text-xs` looks like the obvious
|
|
114
|
+
name for the small size, but it is Tailwind's inlined `0.75rem` (12px), while the published
|
|
115
|
+
`--font-size-xs` resolves to 12.4699px. A consumer who patched with the first shipped a 0.47px shift
|
|
116
|
+
no gate caught (gh#988). For an `initial` knob the tool also prints `read it as: var(--x, …)` — the
|
|
117
|
+
fallback the package itself uses at its read sites. Copy that line; a bare `var(--x)` on an
|
|
118
|
+
`initial` knob is unset.
|
|
119
|
+
|
|
112
120
|
**It does not compute a winner, and says so.** The first version ranked selectors into four
|
|
113
121
|
"cascade" buckets by regex and printed them strongest-last. Codex found that `@theme inline` and
|
|
114
122
|
`[dir="rtl"] .ui-actions[data-fade-in-inline]` both scored top precedence on the substring
|
package/docs/data-entry/form.tsx
CHANGED
|
@@ -356,39 +356,43 @@ export default function Demo() {
|
|
|
356
356
|
<CardContent>
|
|
357
357
|
<Form layout="horizontal" labelWidth="9rem">
|
|
358
358
|
<FormField id="cmp-tel" label="電話番号" helper="市外局番から">
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
359
|
+
{/* The three parts never split across lines — "03 -" alone on a row reads as a
|
|
360
|
+
whole number. The extension is a separate value, so it is what wraps on a phone. */}
|
|
361
|
+
<Flex direction="row" gap="sm" align="center" wrap>
|
|
362
|
+
<Flex direction="row" gap="sm" align="center" shrink={false}>
|
|
363
|
+
<Flex width="4rem" shrink={false}>
|
|
364
|
+
<Input
|
|
365
|
+
id="cmp-tel-area"
|
|
366
|
+
aria-label="市外局番"
|
|
367
|
+
inputMode="numeric"
|
|
368
|
+
maxLength={4}
|
|
369
|
+
defaultValue="03"
|
|
370
|
+
/>
|
|
371
|
+
</Flex>
|
|
372
|
+
<Text aria-hidden tone="muted">
|
|
373
|
+
-
|
|
374
|
+
</Text>
|
|
375
|
+
<Flex width="4rem" shrink={false}>
|
|
376
|
+
<Input
|
|
377
|
+
id="cmp-tel-city"
|
|
378
|
+
aria-label="市内局番"
|
|
379
|
+
inputMode="numeric"
|
|
380
|
+
maxLength={4}
|
|
381
|
+
defaultValue="6273"
|
|
382
|
+
/>
|
|
383
|
+
</Flex>
|
|
384
|
+
<Text aria-hidden tone="muted">
|
|
385
|
+
-
|
|
386
|
+
</Text>
|
|
387
|
+
<Flex width="4rem" shrink={false}>
|
|
388
|
+
<Input
|
|
389
|
+
id="cmp-tel-line"
|
|
390
|
+
aria-label="加入者番号"
|
|
391
|
+
inputMode="numeric"
|
|
392
|
+
maxLength={4}
|
|
393
|
+
defaultValue="0001"
|
|
394
|
+
/>
|
|
395
|
+
</Flex>
|
|
392
396
|
</Flex>
|
|
393
397
|
<Flex width="6rem" shrink={false}>
|
|
394
398
|
<NumberInput
|
|
@@ -499,19 +503,19 @@ export default function Demo() {
|
|
|
499
503
|
</FormField>
|
|
500
504
|
<FormField id="cmp-ok-tel" label="電話番号" validateStatus="success" hasFeedback>
|
|
501
505
|
<Flex direction="row" gap="sm" align="center">
|
|
502
|
-
<Flex width="
|
|
506
|
+
<Flex width="4rem" shrink={false}>
|
|
503
507
|
<Input id="cmp-ok-tel-area" aria-label="市外局番" defaultValue="03" />
|
|
504
508
|
</Flex>
|
|
505
509
|
<Text aria-hidden tone="muted">
|
|
506
510
|
-
|
|
507
511
|
</Text>
|
|
508
|
-
<Flex width="
|
|
512
|
+
<Flex width="4rem" shrink={false}>
|
|
509
513
|
<Input id="cmp-ok-tel-city" aria-label="市内局番" defaultValue="6273" />
|
|
510
514
|
</Flex>
|
|
511
515
|
<Text aria-hidden tone="muted">
|
|
512
516
|
-
|
|
513
517
|
</Text>
|
|
514
|
-
<Flex width="
|
|
518
|
+
<Flex width="4rem" shrink={false}>
|
|
515
519
|
<Input id="cmp-ok-tel-line" aria-label="加入者番号" defaultValue="0001" />
|
|
516
520
|
</Flex>
|
|
517
521
|
</Flex>
|
|
@@ -1111,7 +1115,7 @@ export default function Demo() {
|
|
|
1111
1115
|
</CardHeader>
|
|
1112
1116
|
<CardContent>
|
|
1113
1117
|
<Flex direction="col" gap="lg">
|
|
1114
|
-
<Form layout="horizontal" labelWidth="14rem" labelAlign="end"
|
|
1118
|
+
<Form layout="horizontal" labelWidth="14rem" labelAlign="end">
|
|
1115
1119
|
<FormField id="len-e-name" label="氏名" required>
|
|
1116
1120
|
<Input id="len-e-name" defaultValue="山田 太郎" />
|
|
1117
1121
|
</FormField>
|
|
@@ -1127,7 +1131,7 @@ export default function Demo() {
|
|
|
1127
1131
|
<Input id="len-e-note" defaultValue="ゴドー商事" />
|
|
1128
1132
|
</FormField>
|
|
1129
1133
|
</Form>
|
|
1130
|
-
<Form layout="horizontal" labelWidth="14rem" labelAlign="start"
|
|
1134
|
+
<Form layout="horizontal" labelWidth="14rem" labelAlign="start">
|
|
1131
1135
|
<FormField id="len-s-name" label="氏名" required>
|
|
1132
1136
|
<Input id="len-s-name" defaultValue="山田 太郎" />
|
|
1133
1137
|
</FormField>
|
package/docs/layout/nav-list.tsx
CHANGED
|
@@ -395,8 +395,8 @@ export default function Demo() {
|
|
|
395
395
|
</CardDescription>
|
|
396
396
|
</CardHeader>
|
|
397
397
|
<CardContent>
|
|
398
|
-
<Flex direction={{ base: "col", md: "row" }} gap="xl"
|
|
399
|
-
<Flex direction="col" gap="sm"
|
|
398
|
+
<Flex direction={{ base: "col", md: "row" }} gap="xl">
|
|
399
|
+
<Flex direction="col" gap="sm" fill>
|
|
400
400
|
<Text size="sm" weight="medium">
|
|
401
401
|
レール幅(compact · 18.75rem)
|
|
402
402
|
</Text>
|
|
@@ -182,8 +182,10 @@ export default function Demo() {
|
|
|
182
182
|
</CardDescription>
|
|
183
183
|
</CardHeader>
|
|
184
184
|
<CardContent>
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
{/* Side by side from md; on a phone the rail stacks above the text it indexes. A 256px
|
|
186
|
+
rail beside a 244px card left the chapters a column one glyph wide (gh#965). */}
|
|
187
|
+
<Flex direction={{ base: "col", md: "row" }} gap="lg" align="start">
|
|
188
|
+
<ScrollArea className="h-48 w-full shrink-0 md:h-96 md:w-64" label="Contents">
|
|
187
189
|
<Anchor
|
|
188
190
|
affix={false}
|
|
189
191
|
showInkInFixed
|
|
@@ -192,7 +194,11 @@ export default function Demo() {
|
|
|
192
194
|
getContainer={() => chaptersRef.current ?? window}
|
|
193
195
|
/>
|
|
194
196
|
</ScrollArea>
|
|
195
|
-
<ScrollArea
|
|
197
|
+
<ScrollArea
|
|
198
|
+
viewportRef={chaptersRef}
|
|
199
|
+
className="h-96 w-full md:w-auto md:flex-1"
|
|
200
|
+
label="Agreement"
|
|
201
|
+
>
|
|
196
202
|
<Flex direction="col" gap="md">
|
|
197
203
|
{CHAPTERS.map((chapter) => (
|
|
198
204
|
<Chapter
|
|
@@ -251,24 +257,28 @@ export default function Demo() {
|
|
|
251
257
|
</CardHeader>
|
|
252
258
|
<CardContent>
|
|
253
259
|
<Flex gap="lg" align="start">
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
{
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
href: "#page-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
260
|
+
{/* A pinned side rail has no room on a phone: at 320px it was squeezed to 58px and
|
|
261
|
+
broke "Gamma" mid-word. Documentation sites drop the in-page rail below md. */}
|
|
262
|
+
<Flex hideBelow="md">
|
|
263
|
+
<Anchor
|
|
264
|
+
offsetBlockStart={16}
|
|
265
|
+
targetOffsetBlockStart={24}
|
|
266
|
+
label="On this page"
|
|
267
|
+
items={[
|
|
268
|
+
{ key: "page-a", href: "#page-alpha", title: "Alpha · 第一章" },
|
|
269
|
+
{
|
|
270
|
+
key: "page-b",
|
|
271
|
+
href: "#page-beta",
|
|
272
|
+
title: "Beta · 第二章",
|
|
273
|
+
children: [
|
|
274
|
+
{ key: "page-b1", href: "#page-beta-one", title: "Beta, part one" },
|
|
275
|
+
{ key: "page-b2", href: "#page-beta-two", title: "Beta, part two" },
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
{ key: "page-c", href: "#page-gamma", title: "Gamma · 第三章" },
|
|
279
|
+
]}
|
|
280
|
+
/>
|
|
281
|
+
</Flex>
|
|
272
282
|
<Flex direction="col" gap="md" className="flex-1">
|
|
273
283
|
<Chapter id="page-alpha" title="Alpha · 第一章" lines={6} />
|
|
274
284
|
<Chapter id="page-beta" title="Beta · 第二章" lines={2} />
|
package/package.json
CHANGED
|
@@ -39,9 +39,15 @@
|
|
|
39
39
|
* node scripts/explain-token.mjs --json <name> machine-readable, for a gate
|
|
40
40
|
*/
|
|
41
41
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
42
|
-
import { join, relative } from "node:path";
|
|
42
|
+
import { dirname, join, relative } from "node:path";
|
|
43
|
+
import { fileURLToPath } from "node:url";
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
/* The PACKAGE root, from this file's own location — never `process.cwd()` (gh#980). The documented
|
|
46
|
+
* call is `node node_modules/@godxjp/ui/scripts/explain-token.mjs` from the consumer's directory, and
|
|
47
|
+
* with cwd as the root every read below looked in the consumer's tree: no catalog, no CSS, and a
|
|
48
|
+
* confident "no token matches" for every token that exists. In this checkout the two roots coincide,
|
|
49
|
+
* which is why the defect was invisible here. */
|
|
50
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
45
51
|
|
|
46
52
|
/** Source of truth for what a CONSUMER can set: the published catalog, not the stylesheets. */
|
|
47
53
|
function publishedTokens() {
|
|
@@ -139,11 +145,37 @@ function parse(file) {
|
|
|
139
145
|
.slice(m.index, m.index + 400)
|
|
140
146
|
.replace(/\s+/g, " ")
|
|
141
147
|
.includes(`${m[1]},`),
|
|
148
|
+
fallback: fallbackAt(blank, m.index),
|
|
142
149
|
});
|
|
143
150
|
}
|
|
144
151
|
return { decls, reads };
|
|
145
152
|
}
|
|
146
153
|
|
|
154
|
+
/**
|
|
155
|
+
* The fallback text of the `var(` that opens at `at`, or null. Balanced on parentheses, because the
|
|
156
|
+
* fallbacks this package writes are formulas — `calc(var(--font-size-base) / var(--font-size-ratio))`
|
|
157
|
+
* — and stopping at the first `)` would print half of one.
|
|
158
|
+
*/
|
|
159
|
+
function fallbackAt(blank, at) {
|
|
160
|
+
let depth = 0;
|
|
161
|
+
let comma = -1;
|
|
162
|
+
for (let i = at; i < blank.length; i += 1) {
|
|
163
|
+
const ch = blank[i];
|
|
164
|
+
if (ch === "(") depth += 1;
|
|
165
|
+
else if (ch === ")") {
|
|
166
|
+
depth -= 1;
|
|
167
|
+
if (depth === 0)
|
|
168
|
+
return comma < 0
|
|
169
|
+
? null
|
|
170
|
+
: blank
|
|
171
|
+
.slice(comma + 1, i)
|
|
172
|
+
.trim()
|
|
173
|
+
.replace(/\s+/g, " ");
|
|
174
|
+
} else if (ch === "," && depth === 1 && comma < 0) comma = i;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
147
179
|
/** The selector chain enclosing a byte offset, outermost first. */
|
|
148
180
|
function contextAt(blank, at) {
|
|
149
181
|
const chain = [];
|
|
@@ -282,6 +314,27 @@ function trace(name, { decls, reads }, published, scopedNames) {
|
|
|
282
314
|
);
|
|
283
315
|
}
|
|
284
316
|
if (myReads.length > 12) console.log(` … and ${myReads.length - 12} more`);
|
|
317
|
+
|
|
318
|
+
/* THE PATCH SHAPE, printed rather than left to be inferred (gh#988). An `initial` knob read bare
|
|
319
|
+
* is unset, so a consumer migrating a rule has to copy the package's own fallback — and without
|
|
320
|
+
* this line they went looking for a shorter token instead, picked `--text-xs` (unpublished,
|
|
321
|
+
* Tailwind-inlined to 12px) over `--font-size-xs` (12.4699px), and shipped a 0.47px shift no
|
|
322
|
+
* gate caught. The most common fallback wins; the count says how settled it is. */
|
|
323
|
+
if (mine.some((d) => d.value === "initial")) {
|
|
324
|
+
const counts = new Map();
|
|
325
|
+
for (const r of myReads)
|
|
326
|
+
if (r.fallback) counts.set(r.fallback, (counts.get(r.fallback) ?? 0) + 1);
|
|
327
|
+
const [best, n] = [...counts].sort((a, b) => b[1] - a[1])[0] ?? [];
|
|
328
|
+
if (best) {
|
|
329
|
+
console.log(` read it as: var(${name}, ${best})`);
|
|
330
|
+
console.log(
|
|
331
|
+
` ${n} of ${myReads.length} read(s) use this fallback — the knob is \`initial\`, so a bare read is unset.`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!tier && myReads.length === 0) {
|
|
336
|
+
console.log(" → not part of the contract: read a published token instead.");
|
|
337
|
+
}
|
|
285
338
|
return { name, tier, decls: mine, reads: myReads };
|
|
286
339
|
}
|
|
287
340
|
|