@godxjp/ui 23.4.3 → 23.4.4
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 -2
- package/dist/components/data-entry/branch-scope-picker.js +1 -1
- package/dist/components/data-entry/form-field.js +1 -1
- package/dist/components/data-entry/upload.js +8 -8
- package/dist/styles/core-with-jis-level1.css +3 -0
- package/dist/styles/dialog-layout.css +1 -1
- package/dist/styles/fonts/noto-sans-jp-jis-level1-400.woff2 +0 -0
- package/dist/styles/fonts/noto-sans-jp-jis-level1-500.woff2 +0 -0
- package/dist/styles/fonts/noto-sans-jp-jis-level1-700.woff2 +0 -0
- package/dist/styles/jis-level1-fonts.css +24 -0
- package/docs/CONSUMER-RULES.md +1 -1
- package/docs/CUSTOMER-THEMING.md +45 -1
- package/docs/TOKENS.md +19 -1
- package/package.json +4 -2
- package/scripts/consumer-rule.md +24 -5
- package/scripts/ui-audit.mjs +17 -1
package/README.md
CHANGED
|
@@ -125,11 +125,25 @@ Then name the family directly after your own face:
|
|
|
125
125
|
node_modules/@godxjp/ui/dist/styles/core.css` → `0` is the promise, so the fallbacks got their own
|
|
126
126
|
entry rather than being folded in.
|
|
127
127
|
|
|
128
|
+
Japanese app that wants the bundled face without the per-screen round-trips? The fourth entry
|
|
129
|
+
replaces the 729 sliced faces with one merged file per weight, JIS X 0208 level 1:
|
|
130
|
+
|
|
131
|
+
```css
|
|
132
|
+
@import "@godxjp/ui/styles/core-with-jis-level1"; /* core + fallbacks + 3 merged faces */
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**3 requests, ~1.53 MB, all on first paint** — against 150 requests and 1,772,728 bytes for a
|
|
136
|
+
694-character Japanese screen on `styles`, spread across screens. It does not cover JIS level 2
|
|
137
|
+
(rare-surname kanji fall through to the platform face, so name one after ours) and it loses to the
|
|
138
|
+
slices below roughly 620 distinct characters. Full table and reasoning in
|
|
139
|
+
[docs/CUSTOMER-THEMING.md](docs/CUSTOMER-THEMING.md).
|
|
140
|
+
|
|
128
141
|
> **Do not cherry-pick `*-layout.css` files.** Layers depend on each other (a
|
|
129
142
|
> Select's rows, a menu's surface, a form's rhythm live in shared rules) and a
|
|
130
143
|
> missing layer fails silently: menus render with no background, rows with no
|
|
131
|
-
> height. `styles`, `styles/core
|
|
132
|
-
>
|
|
144
|
+
> height. `styles`, `styles/core`, `styles/core-with-fallbacks` and
|
|
145
|
+
> `styles/core-with-jis-level1` are the four supported entries; the runtime `visual-audit` flags a
|
|
146
|
+
> page whose layers are incomplete (`css-layers-missing`).
|
|
133
147
|
|
|
134
148
|
## Golden ratio (φ ≈ 1.618)
|
|
135
149
|
|
|
@@ -188,7 +188,7 @@ const BranchScopePicker = React.forwardRef(
|
|
|
188
188
|
),
|
|
189
189
|
/* @__PURE__ */ jsx(Text, { size: "xs", tone: "muted", "aria-live": "polite", children: t("dataEntry.branchScope.selectedCount", { count: branchIds.length }) })
|
|
190
190
|
] }),
|
|
191
|
-
invalid && /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "text-
|
|
191
|
+
invalid && /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "text-error-strong text-xs", children: error })
|
|
192
192
|
] })
|
|
193
193
|
);
|
|
194
194
|
}
|
|
@@ -175,7 +175,7 @@ function FormField({
|
|
|
175
175
|
t(`dataEntry.form.${validationStatus}`)
|
|
176
176
|
] }) : null,
|
|
177
177
|
helperPlacement === "after" ? helperNode : null,
|
|
178
|
-
error ? /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "ui-form-field-note text-
|
|
178
|
+
error ? /* @__PURE__ */ jsx("p", { id: errorId, role: "alert", className: "ui-form-field-note text-error-strong text-xs", children: error }) : null
|
|
179
179
|
] })
|
|
180
180
|
]
|
|
181
181
|
}
|
|
@@ -434,7 +434,7 @@ function Upload({
|
|
|
434
434
|
return /* @__PURE__ */ jsxs("div", { ...pasteProps, className: cn("ui-stack-sm", className), children: [
|
|
435
435
|
hiddenInput,
|
|
436
436
|
liveRegion,
|
|
437
|
-
rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-
|
|
437
|
+
rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-error-strong", children: rejection }),
|
|
438
438
|
/* @__PURE__ */ jsxs(
|
|
439
439
|
"div",
|
|
440
440
|
{
|
|
@@ -483,7 +483,7 @@ function Upload({
|
|
|
483
483
|
return /* @__PURE__ */ jsxs("div", { ...pasteProps, className: cn("ui-stack-sm", className), children: [
|
|
484
484
|
hiddenInput,
|
|
485
485
|
liveRegion,
|
|
486
|
-
rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-
|
|
486
|
+
rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-error-strong", children: rejection }),
|
|
487
487
|
/* @__PURE__ */ jsxs(
|
|
488
488
|
Button,
|
|
489
489
|
{
|
|
@@ -514,7 +514,7 @@ function Upload({
|
|
|
514
514
|
return /* @__PURE__ */ jsxs("div", { ...pasteProps, className: cn("ui-upload-grid", className), children: [
|
|
515
515
|
hiddenInput,
|
|
516
516
|
liveRegion,
|
|
517
|
-
rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-
|
|
517
|
+
rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-error-strong", children: rejection }),
|
|
518
518
|
(showUploadList ? items : []).map((item2) => /* @__PURE__ */ jsx(
|
|
519
519
|
UploadPictureCard,
|
|
520
520
|
{
|
|
@@ -548,7 +548,7 @@ function Upload({
|
|
|
548
548
|
return /* @__PURE__ */ jsxs("div", { ...pasteProps, className: cn("ui-stack-sm", className), children: [
|
|
549
549
|
hiddenInput,
|
|
550
550
|
liveRegion,
|
|
551
|
-
rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-
|
|
551
|
+
rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-error-strong", children: rejection }),
|
|
552
552
|
/* @__PURE__ */ jsx(Button, { type: "button", disabled, variant: triggerVariant, onClick: openPicker, children: children ?? t("dataEntry.upload.addImage") }),
|
|
553
553
|
list
|
|
554
554
|
] });
|
|
@@ -558,7 +558,7 @@ function Upload({
|
|
|
558
558
|
return /* @__PURE__ */ jsxs("div", { ...pasteProps, className: cn("ui-stack-sm ui-upload-picture", className), children: [
|
|
559
559
|
hiddenInput,
|
|
560
560
|
liveRegion,
|
|
561
|
-
rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-
|
|
561
|
+
rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-error-strong", children: rejection }),
|
|
562
562
|
item2?.previewUrl && !item2.pendingDelete ? /* @__PURE__ */ jsxs("div", { className: "ui-upload-picture-frame", children: [
|
|
563
563
|
/* @__PURE__ */ jsx("img", { src: item2.previewUrl, alt: "", className: "ui-upload-picture-img" }),
|
|
564
564
|
item2.status === "uploading" && /* @__PURE__ */ jsx("div", { className: "ui-upload-overlay ui-upload-picture-overlay", children: t("dataEntry.upload.uploading") }),
|
|
@@ -650,7 +650,7 @@ function UploadDraftActions({
|
|
|
650
650
|
const { state, undoRemove, undoReplace } = draft;
|
|
651
651
|
if (state.canUndoRemove) {
|
|
652
652
|
return /* @__PURE__ */ jsxs("div", { className: "ui-upload-draft-undo", children: [
|
|
653
|
-
/* @__PURE__ */ jsx("span", { className: "text-
|
|
653
|
+
/* @__PURE__ */ jsx("span", { className: "text-error-strong", children: t("dataEntry.upload.markedForDelete") }),
|
|
654
654
|
/* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", variant: "outline", disabled, onClick: undoRemove, children: [
|
|
655
655
|
/* @__PURE__ */ jsx(RotateCcw, { className: "ui-upload-draft-icon", "aria-hidden": "true" }),
|
|
656
656
|
t("dataEntry.upload.undo")
|
|
@@ -690,7 +690,7 @@ function UploadPictureCard({
|
|
|
690
690
|
}
|
|
691
691
|
)
|
|
692
692
|
] }),
|
|
693
|
-
item.status === "error" && /* @__PURE__ */ jsx("span", { role: "alert", className: "text-
|
|
693
|
+
item.status === "error" && /* @__PURE__ */ jsx("span", { role: "alert", className: "text-error-strong", children: item.error }),
|
|
694
694
|
/* @__PURE__ */ jsxs("div", { className: "ui-inline-xs", children: [
|
|
695
695
|
onCancel && item.status === "uploading" && /* @__PURE__ */ jsx(
|
|
696
696
|
Button,
|
|
@@ -760,7 +760,7 @@ function UploadFileList({
|
|
|
760
760
|
/* @__PURE__ */ jsx("div", { className: "truncate font-medium", children: item.name }),
|
|
761
761
|
/* @__PURE__ */ jsxs("div", { className: "text-muted-foreground text-xs", children: [
|
|
762
762
|
formatBytes(item.size),
|
|
763
|
-
item.status === "error" && item.error && /* @__PURE__ */ jsxs("span", { role: "alert", className: "text-
|
|
763
|
+
item.status === "error" && item.error && /* @__PURE__ */ jsxs("span", { role: "alert", className: "text-error-strong", children: [
|
|
764
764
|
" ",
|
|
765
765
|
"\xB7 ",
|
|
766
766
|
item.error
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
@font-face {
|
|
3
|
+
font-family: "Noto Sans JP";
|
|
4
|
+
font-style: normal;
|
|
5
|
+
font-weight: 400;
|
|
6
|
+
font-display: swap;
|
|
7
|
+
src: url("./fonts/noto-sans-jp-jis-level1-400.woff2") format("woff2");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@font-face {
|
|
11
|
+
font-family: "Noto Sans JP";
|
|
12
|
+
font-style: normal;
|
|
13
|
+
font-weight: 500;
|
|
14
|
+
font-display: swap;
|
|
15
|
+
src: url("./fonts/noto-sans-jp-jis-level1-500.woff2") format("woff2");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@font-face {
|
|
19
|
+
font-family: "Noto Sans JP";
|
|
20
|
+
font-style: normal;
|
|
21
|
+
font-weight: 700;
|
|
22
|
+
font-display: swap;
|
|
23
|
+
src: url("./fonts/noto-sans-jp-jis-level1-700.woff2") format("woff2");
|
|
24
|
+
}
|
package/docs/CONSUMER-RULES.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Read this once; the audit enforces it. Everything else in `docs/` is for contributors.
|
|
4
4
|
|
|
5
|
-
1. Load styles with `@import "@godxjp/ui/styles"` (fonts bundled), `@import "@godxjp/ui/styles/core"` (no `@font-face` at all)
|
|
5
|
+
1. Load styles with `@import "@godxjp/ui/styles"` (fonts bundled, 729 sliced woff2 faces, ~11.7 MB), `@import "@godxjp/ui/styles/core"` (no `@font-face` at all), `@import "@godxjp/ui/styles/core-with-fallbacks"` (`core` + six `local()`-only metric-matched faces, still zero network bytes — for when you supply Noto Sans JP yourself) or `@import "@godxjp/ui/styles/core-with-jis-level1"` (`core-with-fallbacks` + Noto Sans JP merged to JIS X 0208 level 1: **3 requests, ~1.53 MB, once** instead of ~8 font requests on every navigation — for a Japanese app that wants the bundled face). Never cherry-pick `*-layout.css`.
|
|
6
6
|
2. Every page is `<PageContainer title subtitle extra footer>`; its sections are spaced by the page. Group items inside a section with `<Flex direction="col" gap>` or `<ResponsiveGrid>`.
|
|
7
7
|
3. No Tailwind layout on your own elements: no `flex`, `grid`, `gap-*`, `p-*`, `m-*`, `space-*`. Rows are `<Flex>` (default row), stacks are `<Flex direction="col">`, grids are `<ResponsiveGrid>`.
|
|
8
8
|
4. No hand-rolled surfaces: no `rounded-* border bg-*` divs. A box is `Card`, a pill is `Badge`, a person is `Avatar`, a row is `ListRow`, a label/value pair is `Descriptions`, an empty area is `EmptyState`, a read-only sample of a colour the USER chose is `Swatch`.
|
package/docs/CUSTOMER-THEMING.md
CHANGED
|
@@ -16,7 +16,7 @@ That single import ships everything: colors, the bundled fonts (**Noto Sans JP**
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
## CSS entries — `styles`, `styles/core`, `styles/core-with-fallbacks`, nothing smaller
|
|
19
|
+
## CSS entries — `styles`, `styles/core`, `styles/core-with-fallbacks`, `styles/core-with-jis-level1`, nothing smaller
|
|
20
20
|
|
|
21
21
|
`@godxjp/ui/styles` bundles every component's CSS plus the fonts: **729 woff2 subsets, ~11.7 MB on disk** at the current @fontsource versions (issue #535 measured 737 files / 13 MB in a real consumer build). When you manage fonts yourself (next/font, a system stack, a browser extension that must not ship font files), load the same layers without the faces:
|
|
22
22
|
|
|
@@ -38,6 +38,49 @@ It declares the faces and nothing else — name the family yourself, directly af
|
|
|
38
38
|
}
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
### A Japanese app that wants the bundled face anyway
|
|
42
|
+
|
|
43
|
+
The slicing is what costs the round-trips: a browser cannot know which of the 729 faces it needs
|
|
44
|
+
until it has laid out and measured the text, so every new screen discovers a new handful. The
|
|
45
|
+
fourth entry replaces them with **one merged file per weight**, JIS X 0208 level 1 — 2965 kanji
|
|
46
|
+
plus kana, symbols, Cyrillic, Latin and Vietnamese, 3861 code points:
|
|
47
|
+
|
|
48
|
+
```css
|
|
49
|
+
@import "@godxjp/ui/styles/core-with-jis-level1"; /* core + fallbacks + 3 merged faces */
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Measured against the sliced entry, Noto Sans JP only, weights 400/500/700:
|
|
53
|
+
|
|
54
|
+
| distinct Japanese characters on screen | `styles` | `core-with-jis-level1` |
|
|
55
|
+
| --------------------------------------------- | -------------------------: | -----------------------: |
|
|
56
|
+
| 448 — this package's own `ja` labels, no data | 99 requests · 1,051,268 B | 3 requests · 1,534,636 B |
|
|
57
|
+
| 694 — labels plus names, addresses, prose | 150 requests · 1,772,728 B | 3 requests · 1,534,636 B |
|
|
58
|
+
| 772 — a little more prose | 216 requests · 3,491,840 B | 3 requests · 1,534,636 B |
|
|
59
|
+
|
|
60
|
+
The left column grows with your content and is paid again on every screen that renders a character
|
|
61
|
+
no earlier screen did; the right column does not move. **Below roughly 620 distinct characters the
|
|
62
|
+
slices are fewer bytes** (in ~100 requests), so an app that renders less Japanese than this
|
|
63
|
+
package's own menu labels should stay on `styles`.
|
|
64
|
+
|
|
65
|
+
What is deliberately not in it: **JIS level 2** (rows 48–84), which would roughly double the bytes
|
|
66
|
+
to cover kanji that appear in rare surnames — those resolve from the platform Japanese face, so
|
|
67
|
+
name one after ours. **M PLUS 2**, which sits behind Noto Sans JP in every stack this package ships
|
|
68
|
+
and is therefore never downloaded today either; merging it would add 1,089,676 bytes for nothing.
|
|
69
|
+
And **no `unicode-range`** on the merged faces, because per-range discovery is the mechanism the
|
|
70
|
+
entry exists to remove — the cost of that is a Latin-only screen downloading its weight's ~500 KB
|
|
71
|
+
rather than the ~25 KB of Latin inside it.
|
|
72
|
+
|
|
73
|
+
Like `core-with-fallbacks`, it declares faces and does not set `--font-sans-base`; name a platform
|
|
74
|
+
Japanese face after ours so level 2 kanji have somewhere to land:
|
|
75
|
+
|
|
76
|
+
```css
|
|
77
|
+
:root {
|
|
78
|
+
--font-sans-base:
|
|
79
|
+
"Noto Sans JP", "Noto Sans JP Fallback", "Hiragino Sans", "Yu Gothic Medium", Meiryo, system-ui,
|
|
80
|
+
sans-serif;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
41
84
|
The per-layer files (`control`, `card-layout`, `navigation-layout`, …) are the package's internal structure, **not a public menu**. Layers share rules — a Select's rows and a menu's surface, a form's rhythm, a card's header type — so a page that loads a subset renders naked menus and unsized rows with no error. The runtime `visual-audit` reports it as `css-layers-missing`.
|
|
42
85
|
|
|
43
86
|
---
|
|
@@ -190,3 +233,4 @@ Scope the overrides under a tenant attribute instead of `:root`. The colour util
|
|
|
190
233
|
Set `data-tenant` on the app root. Two CSS-inheritance caveats for the **scoped** case (a single `:root` brand theme is unaffected — there, overriding just `--radius` / `--shadow-color` cascades):
|
|
191
234
|
|
|
192
235
|
- **Radius & shadow-tint don't cascade from a scoped anchor.** `--radius-{xs…2xl}`, `--card-radius`, `--control-radius` and the `--shadow-{xs…2xl}` ramp are computed at their declaring element, so a scoped `--radius` / `--shadow-color` override won't reach them. For a scoped re-theme, re-declare the derived tokens you need (e.g. `--card-radius: var(--radius)`, or set `--card-shadow` to a literal value).
|
|
236
|
+
````
|
package/docs/TOKENS.md
CHANGED
|
@@ -53,7 +53,7 @@ in the source.
|
|
|
53
53
|
| Tier | Tokens | What it paints | Contrast bar |
|
|
54
54
|
| -------- | ------------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------- |
|
|
55
55
|
| **FILL** | `--success`, `--warning`, `--info`, `--destructive` | A solid chip, band or bar with a label ON it | AA **4.5:1** against its own `*-foreground` label |
|
|
56
|
-
| **TEXT** | `--text-success`, `--text-warning`, `--text-info`, `--text-error` | Small coloured type — a StatCard delta, an outline badge label
|
|
56
|
+
| **TEXT** | `--text-success`, `--text-warning`, `--text-info`, `--text-error` | Small coloured type — a StatCard delta, an outline badge label, a field's error line | AA **4.5:1** against the surface BEHIND it |
|
|
57
57
|
| **MARK** | `--mark-success`, `--mark-warning`, `--mark-info`, `--mark-destructive`, `--mark-primary`, `--mark-attention` | A thin shape carrying meaning with nothing written on it — a `Card accent` rail, a `DataTable rowTone` rail | SC 1.4.11 **3:1** against the surface it sits on |
|
|
58
58
|
|
|
59
59
|
The MARK tier exists because both rails were reading FILL, and two of them were effectively
|
|
@@ -94,6 +94,24 @@ Worst case anywhere on the two routes after the move: **4.52:1**. `.ui-progress-
|
|
|
94
94
|
over-capacity) reads the same tokens as the slice, so a `tone="warning"` meter and a `warning`
|
|
95
95
|
slice on one screen stay the same colour.
|
|
96
96
|
|
|
97
|
+
**ERROR PROSE WAS THE THIRD PLACE THE SAME MISTAKE LIVED (gh#610).** `Alert`, `Text` and
|
|
98
|
+
`Heading` resolved their destructive ink through TEXT from the start, but the `role="alert"` line
|
|
99
|
+
the data-entry primitives render — `<FormField error>`, `Upload`'s rejections, the `Dialog` step-up
|
|
100
|
+
failure — was on `text-destructive`, the FILL utility. So the same `tone` was readable inside an
|
|
101
|
+
`Alert` and near-invisible one line below it, in the field that caused it. Measured in Chromium on
|
|
102
|
+
`/isolate/layout-auth-recovery-examples-mfa-challenge`:
|
|
103
|
+
|
|
104
|
+
| surface | ground | FILL (before) | TEXT (after) |
|
|
105
|
+
| --- | --- | --- | --- |
|
|
106
|
+
| `.ui-form-field-note[role="alert"]` | dark card | **2.95** | **5.52** |
|
|
107
|
+
| `.ui-form-field-note[role="alert"]` | light card | 6.16 | **7.21** |
|
|
108
|
+
|
|
109
|
+
Only the DARK branch failed, and the light one passing is why it survived: the fill is tuned for a
|
|
110
|
+
white label ON it, so it darkens on light grounds and lightens on dark ones — the opposite of what
|
|
111
|
+
ink needs. The guard is `error-text-tier.test.ts` (token ratios on `--background`/`--card`/
|
|
112
|
+
`--muted` in both themes, plus a source scan for the fill utility), and `check:contrast` gained the
|
|
113
|
+
two `ui-auth-shell` routes it had never loaded.
|
|
114
|
+
|
|
97
115
|
**A theme that repoints `--secondary` owes `--progress-track-background`.** The track defaults to
|
|
98
116
|
`hsl(var(--secondary))`, which is a pale neutral in the stock palette. `docs/showcase/acme-portal`
|
|
99
117
|
repurposes `--secondary` as a navy *button* colour, so its bars were drawn on a near-black track
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/ui",
|
|
3
|
-
"version": "23.4.
|
|
4
|
-
"godxUiMcp": "23.4.
|
|
3
|
+
"version": "23.4.4",
|
|
4
|
+
"godxUiMcp": "23.4.4",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"./styles": "./dist/styles/index.css",
|
|
49
49
|
"./styles/core": "./dist/styles/core.css",
|
|
50
50
|
"./styles/core-with-fallbacks": "./dist/styles/core-with-fallbacks.css",
|
|
51
|
+
"./styles/core-with-jis-level1": "./dist/styles/core-with-jis-level1.css",
|
|
51
52
|
"./styles/*": "./dist/styles/*.css",
|
|
52
53
|
"./theme/*": "./dist/theme/*",
|
|
53
54
|
"./tokens": "./dist/tokens/base.css",
|
|
@@ -380,6 +381,7 @@
|
|
|
380
381
|
"audit": "node scripts/ui-audit.mjs",
|
|
381
382
|
"audit:examples": "node scripts/ui-audit.mjs docs",
|
|
382
383
|
"build": "tsup && tsc -p tsconfig.build.json && node scripts/copy-styles.mjs && node scripts/fix-esm-extensions.mjs && node scripts/add-use-client.mjs",
|
|
384
|
+
"build:jis-level1-fonts": "node scripts/build-jis-level1-fonts.mjs",
|
|
383
385
|
"dev": "node scripts/dev.mjs",
|
|
384
386
|
"prepublishOnly": "pnpm run build",
|
|
385
387
|
"check:audit-sync": "node scripts/check-audit-sync.mjs",
|
package/scripts/consumer-rule.md
CHANGED
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
`docs/CONSUMER-RULES.md` (10 luật) và, với kho chuột bạch, ở
|
|
13
13
|
`.claude/skills/godx-ui-guinea-pig/SKILL.md`.
|
|
14
14
|
|
|
15
|
-
## Nạp style:
|
|
15
|
+
## Nạp style: BỐN lối vào, và ba lối sau đều không chở 729 lát font
|
|
16
16
|
|
|
17
17
|
```css
|
|
18
|
-
@import "@godxjp/ui/styles"; /* mọi layer +
|
|
18
|
+
@import "@godxjp/ui/styles"; /* mọi layer + 729 lát woff2 cắt theo unicode-range */
|
|
19
19
|
@import "@godxjp/ui/styles/core"; /* CÙNG các layer ấy, KHÔNG một @font-face nào */
|
|
20
20
|
@import "@godxjp/ui/styles/core-with-fallbacks"; /* core + 6 khối local()-only, vẫn 0 byte mạng */
|
|
21
|
+
@import "@godxjp/ui/styles/core-with-jis-level1"; /* + Noto Sans JP gộp JIS mức 1: 3 request, ~1,53 MB */
|
|
21
22
|
```
|
|
22
23
|
|
|
23
24
|
Chọn `core` khi kho tự lo mặt chữ, hoặc khi không muốn chở font: `@fontsource` cắt
|
|
@@ -30,13 +31,31 @@ sổ swap không đội hình: nó chở đúng 6 `@font-face` metric-matched, `
|
|
|
30
31
|
nên **không tải byte nào**. Nhớ tự xếp tên họ chữ ngay sau mặt chữ của bạn:
|
|
31
32
|
`--font-sans-base: "Noto Sans JP", "Noto Sans JP Fallback", system-ui, sans-serif;`
|
|
32
33
|
|
|
34
|
+
Lối thứ tư dành cho **app tiếng Nhật vẫn muốn font kèm gói**, nhưng không muốn trả số
|
|
35
|
+
vòng tải mỗi lần chuyển màn: nó thay 729 lát bằng **một tệp gộp cho mỗi weight**, phủ
|
|
36
|
+
JIS X 0208 mức 1 (2965 kanji + kana + ký hiệu + Latin + tiếng Việt, 3861 code point).
|
|
37
|
+
Đo trên ba weight, chỉ Noto Sans JP:
|
|
38
|
+
|
|
39
|
+
| số ký tự Nhật khác nhau trên màn | `styles` | `core-with-jis-level1` |
|
|
40
|
+
| -------------------------------- | ---------------------------- | -------------------------- |
|
|
41
|
+
| 448 (nhãn `ja` của chính gói) | 99 request · 1.051.268 byte | 3 request · 1.534.636 byte |
|
|
42
|
+
| 694 (nhãn + tên, địa chỉ, câu) | 150 request · 1.772.728 byte | 3 request · 1.534.636 byte |
|
|
43
|
+
| 772 (thêm chút văn xuôi) | 216 request · 3.491.840 byte | 3 request · 1.534.636 byte |
|
|
44
|
+
|
|
45
|
+
Cột trái phình theo NỘI DUNG và phải trả lại ở mỗi màn có ký tự mới; cột phải không đổi.
|
|
46
|
+
**Dưới khoảng 620 ký tự khác nhau thì cắt lát ít byte hơn** (đổi bằng ~100 request), nên
|
|
47
|
+
app nào render ít chữ Nhật hơn cả menu của gói này thì cứ ở `styles`. Lối này KHÔNG chở
|
|
48
|
+
JIS mức 2 — kanji trong họ tên hiếm sẽ rơi xuống mặt chữ của hệ điều hành, nên hãy xếp
|
|
49
|
+
một mặt chữ Nhật của hệ ngay sau mặt chữ của gói. Nó cũng không đụng `--font-sans-base`,
|
|
50
|
+
y như lối thứ ba.
|
|
51
|
+
|
|
33
52
|
`core` giữ `@font-face` = **0** và đó là lời hứa đo được —
|
|
34
53
|
`grep -c '@font-face' node_modules/@godxjp/ui/dist/styles/core.css` → `0`. Vì vậy các
|
|
35
54
|
fallback nằm ở entry riêng chứ không nhét vào `core`.
|
|
36
55
|
|
|
37
|
-
Không lối nào trong
|
|
38
|
-
vẹn. Cherry-pick từng layer riêng lẻ thì vẫn cấm — đó là thứ làm vỡ hợp đồng thứ
|
|
39
|
-
không phải việc chọn lối vào.
|
|
56
|
+
Không lối nào trong bốn là cherry-pick: cả bốn đều được hỗ trợ và thứ tự layer vẫn
|
|
57
|
+
nguyên vẹn. Cherry-pick từng layer riêng lẻ thì vẫn cấm — đó là thứ làm vỡ hợp đồng thứ
|
|
58
|
+
tự, không phải việc chọn lối vào.
|
|
40
59
|
|
|
41
60
|
## Bố cục chuẩn của platform: BA CỘT, và ba cột là BA PHẠM VI
|
|
42
61
|
|
package/scripts/ui-audit.mjs
CHANGED
|
@@ -902,8 +902,24 @@ function jsxOpeningEnd(source, start) {
|
|
|
902
902
|
* filters AND a table is a mixed body, where the padding is correct — the reporter hit that trap
|
|
903
903
|
* with a `childElementCount === 1` test.
|
|
904
904
|
*/
|
|
905
|
+
/*
|
|
906
|
+
* The alternation is DERIVED from the stylesheet, not guessed. `SkeletonTable` was missing and the
|
|
907
|
+
* same mistake went through silently (gh#611) — which is the worse half, because `SkeletonTable`
|
|
908
|
+
* is the package's OWN stand-in for `DataTable`: a consumer writes the skeleton and the table side
|
|
909
|
+
* by side, one is flagged and the other is not.
|
|
910
|
+
*
|
|
911
|
+
* The list to match is exactly what `[data-slot="card-content"][data-flush]` special-cases in CSS,
|
|
912
|
+
* because a per-pair exception there IS the statement that the pair matters:
|
|
913
|
+
*
|
|
914
|
+
* .ui-data-table-root · .ui-data-table-scroll · .ui-data-table-toolbar → DataTable
|
|
915
|
+
* .ui-skeleton-table → SkeletonTable
|
|
916
|
+
*
|
|
917
|
+
* `Table` stays because it is the primitive `DataTable` is built from and a consumer can place it
|
|
918
|
+
* directly. Adding a component to that CSS block without adding it here reopens this issue, so the
|
|
919
|
+
* two are checked against each other in the rule's test.
|
|
920
|
+
*/
|
|
905
921
|
const CARD_TABLE_FLUSH = new RegExp(
|
|
906
|
-
`<CardContent(?![^>]*\\bflush\\b)(?:\\s${ATTRS})?>\\s*<(?:DataTable|Table)\\b`,
|
|
922
|
+
`<CardContent(?![^>]*\\bflush\\b)(?:\\s${ATTRS})?>\\s*<(?:DataTable|SkeletonTable|Table)\\b`,
|
|
907
923
|
"g",
|
|
908
924
|
);
|
|
909
925
|
|