@motir/design-system 0.14.0 → 0.17.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/dist/components/theme/StyleVignette.js +3 -3
- package/dist/components/theme/StyleVignette.js.map +1 -1
- package/dist/components/ui/Combobox.js +27 -14
- package/dist/components/ui/Combobox.js.map +1 -1
- package/dist/components/ui/Modal.d.ts +3 -1
- package/dist/components/ui/Modal.js +28 -13
- package/dist/components/ui/Modal.js.map +1 -1
- package/dist/components/ui/MultiSelectPicker.js +26 -13
- package/dist/components/ui/MultiSelectPicker.js.map +1 -1
- package/dist/components/ui/Popover.js +36 -14
- package/dist/components/ui/Popover.js.map +1 -1
- package/dist/components/ui/Tooltip.js +28 -13
- package/dist/components/ui/Tooltip.js.map +1 -1
- package/dist/contexts/theme-context.js +25 -1
- package/dist/contexts/theme-context.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/mock/index.js +159 -1
- package/dist/mock/index.js.map +1 -1
- package/dist/mock/pruneUnmatchedRules.d.ts +16 -0
- package/dist/mock/pruneUnmatchedRules.js +160 -0
- package/dist/mock/pruneUnmatchedRules.js.map +1 -0
- package/dist/mock/renderMock.js +159 -1
- package/dist/mock/renderMock.js.map +1 -1
- package/dist/specimen/TokensSpecimen.js +3 -3
- package/dist/specimen/TokensSpecimen.js.map +1 -1
- package/dist/theme/fontSets.d.ts +38 -5
- package/dist/theme/fontSets.js +33 -3
- package/dist/theme/fontSets.js.map +1 -1
- package/dist/utils/fullscreen.d.ts +23 -4
- package/dist/utils/fullscreen.js +25 -2
- package/dist/utils/fullscreen.js.map +1 -1
- package/package.json +1 -1
- package/theme.css +107 -0
package/dist/theme/fontSets.d.ts
CHANGED
|
@@ -24,8 +24,16 @@
|
|
|
24
24
|
*/
|
|
25
25
|
/** The eleven locales app.motir.co ships in. A closed product decision. */
|
|
26
26
|
declare const FONT_SET_LOCALES: readonly ["en", "zh", "ja", "ko", "de", "fr", "es", "it", "nl", "pl", "pt"];
|
|
27
|
-
/**
|
|
27
|
+
/** One of the eleven app locales. */
|
|
28
28
|
type FontSetLocale = (typeof FONT_SET_LOCALES)[number];
|
|
29
|
+
/**
|
|
30
|
+
* Languages with a font set that only motir.co ships (MOTIR-8937 · MOTIR-9239).
|
|
31
|
+
* Kept apart from `FONT_SET_LOCALES`, which is app.motir.co's own locale set and
|
|
32
|
+
* is closed over its stored font picks (one `fontPick<Locale>` column each).
|
|
33
|
+
*/
|
|
34
|
+
declare const FONT_SET_EXTRA_LOCALES: readonly ["zh-tw"];
|
|
35
|
+
/** A language the font-set registry knows: an app locale or an extra one. */
|
|
36
|
+
type FontSetLanguage = FontSetLocale | (typeof FONT_SET_EXTRA_LOCALES)[number];
|
|
29
37
|
/** The three type roles every set covers: the same roles `--font-*` carries. */
|
|
30
38
|
declare const FONT_SET_ROLES: readonly ["sans", "serif", "mono"];
|
|
31
39
|
/** One of the three type roles. */
|
|
@@ -128,6 +136,26 @@ declare const FONT_SET_REGISTRY: {
|
|
|
128
136
|
};
|
|
129
137
|
};
|
|
130
138
|
};
|
|
139
|
+
readonly 'zh-Hant': {
|
|
140
|
+
readonly id: "zh-Hant";
|
|
141
|
+
readonly label: "Traditional Chinese";
|
|
142
|
+
readonly lang: "zh-TW";
|
|
143
|
+
readonly cjk: true;
|
|
144
|
+
readonly roles: {
|
|
145
|
+
readonly sans: {
|
|
146
|
+
readonly default: "noto-sans-tc";
|
|
147
|
+
readonly members: readonly [FontSetMember];
|
|
148
|
+
};
|
|
149
|
+
readonly serif: {
|
|
150
|
+
readonly default: "noto-serif-tc";
|
|
151
|
+
readonly members: readonly [FontSetMember, FontSetMember];
|
|
152
|
+
};
|
|
153
|
+
readonly mono: {
|
|
154
|
+
readonly default: "noto-sans-tc";
|
|
155
|
+
readonly members: readonly [FontSetMember];
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
};
|
|
131
159
|
readonly ja: {
|
|
132
160
|
readonly id: "ja";
|
|
133
161
|
readonly label: "Japanese";
|
|
@@ -188,12 +216,17 @@ declare const LOCALE_FONT_SET: {
|
|
|
188
216
|
readonly nl: "latin";
|
|
189
217
|
readonly pl: "latin";
|
|
190
218
|
readonly pt: "latin";
|
|
219
|
+
readonly 'zh-tw': "zh-Hant";
|
|
191
220
|
};
|
|
192
221
|
/** Narrowing guard: is an arbitrary value one of the eleven locales? */
|
|
193
222
|
declare function isFontSetLocale(value: unknown): value is FontSetLocale;
|
|
223
|
+
/** Narrowing guard: is an arbitrary value an app locale or an extra language? */
|
|
224
|
+
declare function isFontSetLanguage(value: unknown): value is FontSetLanguage;
|
|
194
225
|
/**
|
|
195
|
-
* The set for an `<html lang>` value.
|
|
196
|
-
*
|
|
226
|
+
* The set for an `<html lang>` value. Tries the whole tag first, case-insensitively
|
|
227
|
+
* and reading `_` as `-` (`zh-TW` / `zh_tw` → zh-Hant), then the primary subtag
|
|
228
|
+
* (`ja-JP` → ja, `PT-br` → pt, `zh-Hans` → zh). No script or region heuristics:
|
|
229
|
+
* `zh-Hant` and `zh-HK` reach zh-Hant only through a page whose `lang` is `zh-TW`.
|
|
197
230
|
*
|
|
198
231
|
* The fallback is explicit: an empty or unknown tag returns the Latin set,
|
|
199
232
|
* because the app renders an unknown language in English. It never falls back
|
|
@@ -232,6 +265,6 @@ type FontSetPickAttribute = `data-font-set-${FontSetRole}`;
|
|
|
232
265
|
* first byte share, so a pick sets identical attributes on both sides — which
|
|
233
266
|
* is why it lives here, free of any `'use client'` boundary.
|
|
234
267
|
*/
|
|
235
|
-
declare function fontSetPickAttributes(locale:
|
|
268
|
+
declare function fontSetPickAttributes(locale: FontSetLanguage, memberId: string | null): Partial<Record<FontSetPickAttribute, string>>;
|
|
236
269
|
|
|
237
|
-
export { DEFAULT_FONT_SET_ID, FONT_SET_IDS, FONT_SET_LOCALES, FONT_SET_REGISTRY, FONT_SET_ROLES, type FontSet, type FontSetId, type FontSetLocale, type FontSetMember, type FontSetMemberSource, type FontSetPickAttribute, type FontSetRole, type FontSetRoleMembers, LOCALE_FONT_SET, fontSetMemberVar, fontSetPickAttributes, isFontSetLocale, resolveFontSet, resolveFontSetMember };
|
|
270
|
+
export { DEFAULT_FONT_SET_ID, FONT_SET_EXTRA_LOCALES, FONT_SET_IDS, FONT_SET_LOCALES, FONT_SET_REGISTRY, FONT_SET_ROLES, type FontSet, type FontSetId, type FontSetLanguage, type FontSetLocale, type FontSetMember, type FontSetMemberSource, type FontSetPickAttribute, type FontSetRole, type FontSetRoleMembers, LOCALE_FONT_SET, fontSetMemberVar, fontSetPickAttributes, isFontSetLanguage, isFontSetLocale, resolveFontSet, resolveFontSetMember };
|
package/dist/theme/fontSets.js
CHANGED
|
@@ -12,6 +12,7 @@ var FONT_SET_LOCALES = [
|
|
|
12
12
|
"pl",
|
|
13
13
|
"pt"
|
|
14
14
|
];
|
|
15
|
+
var FONT_SET_EXTRA_LOCALES = ["zh-tw"];
|
|
15
16
|
var FONT_SET_ROLES = ["sans", "serif", "mono"];
|
|
16
17
|
var TYPE_PAIRING = {
|
|
17
18
|
id: "type-pairing",
|
|
@@ -64,6 +65,29 @@ var FONT_SET_REGISTRY = {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
},
|
|
68
|
+
// Not in dooooWeb: Traditional Chinese (Taiwan) for motir.co's `/zh-tw`
|
|
69
|
+
// (MOTIR-8937). The zh-Hans set with its Traditional-form faces.
|
|
70
|
+
"zh-Hant": {
|
|
71
|
+
id: "zh-Hant",
|
|
72
|
+
label: "Traditional Chinese",
|
|
73
|
+
lang: "zh-TW",
|
|
74
|
+
cjk: true,
|
|
75
|
+
roles: {
|
|
76
|
+
sans: { default: "noto-sans-tc", members: [google("noto-sans-tc", "Noto Sans TC", false)] },
|
|
77
|
+
serif: {
|
|
78
|
+
default: "noto-serif-tc",
|
|
79
|
+
members: [
|
|
80
|
+
google("noto-serif-tc", "Noto Serif TC", false),
|
|
81
|
+
// A handwritten-style face, so a pick rather than the default.
|
|
82
|
+
google("lxgw-wenkai-tc", "LXGW WenKai TC", false)
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
mono: {
|
|
86
|
+
default: "noto-sans-tc",
|
|
87
|
+
members: [google("noto-sans-tc", "Noto Sans TC", false, "sans")]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
67
91
|
// dooooWeb: `japanese`.
|
|
68
92
|
ja: {
|
|
69
93
|
id: "ja",
|
|
@@ -126,13 +150,19 @@ var LOCALE_FONT_SET = {
|
|
|
126
150
|
it: "latin",
|
|
127
151
|
nl: "latin",
|
|
128
152
|
pl: "latin",
|
|
129
|
-
pt: "latin"
|
|
153
|
+
pt: "latin",
|
|
154
|
+
"zh-tw": "zh-Hant"
|
|
130
155
|
};
|
|
131
156
|
function isFontSetLocale(value) {
|
|
132
157
|
return typeof value === "string" && FONT_SET_LOCALES.includes(value);
|
|
133
158
|
}
|
|
159
|
+
function isFontSetLanguage(value) {
|
|
160
|
+
return isFontSetLocale(value) || typeof value === "string" && FONT_SET_EXTRA_LOCALES.includes(value);
|
|
161
|
+
}
|
|
134
162
|
function resolveFontSet(lang) {
|
|
135
|
-
const
|
|
163
|
+
const tag = lang.trim().toLowerCase().replace(/_/g, "-");
|
|
164
|
+
if (isFontSetLanguage(tag)) return FONT_SET_REGISTRY[LOCALE_FONT_SET[tag]];
|
|
165
|
+
const primary = tag.split("-")[0] ?? "";
|
|
136
166
|
const id = isFontSetLocale(primary) ? LOCALE_FONT_SET[primary] : DEFAULT_FONT_SET_ID;
|
|
137
167
|
return FONT_SET_REGISTRY[id];
|
|
138
168
|
}
|
|
@@ -162,6 +192,6 @@ function fontSetPickAttributes(locale, memberId) {
|
|
|
162
192
|
return attrs;
|
|
163
193
|
}
|
|
164
194
|
|
|
165
|
-
export { DEFAULT_FONT_SET_ID, FONT_SET_IDS, FONT_SET_LOCALES, FONT_SET_REGISTRY, FONT_SET_ROLES, LOCALE_FONT_SET, fontSetMemberVar, fontSetPickAttributes, isFontSetLocale, resolveFontSet, resolveFontSetMember };
|
|
195
|
+
export { DEFAULT_FONT_SET_ID, FONT_SET_EXTRA_LOCALES, FONT_SET_IDS, FONT_SET_LOCALES, FONT_SET_REGISTRY, FONT_SET_ROLES, LOCALE_FONT_SET, fontSetMemberVar, fontSetPickAttributes, isFontSetLanguage, isFontSetLocale, resolveFontSet, resolveFontSetMember };
|
|
166
196
|
//# sourceMappingURL=fontSets.js.map
|
|
167
197
|
//# sourceMappingURL=fontSets.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/theme/fontSets.ts"],"names":[],"mappings":"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","file":"fontSets.js","sourcesContent":["/**\n * The Motir FONT-SET REGISTRY: which faces each locale's script is drawn in\n * (Story MOTIR-7733 · Subtask MOTIR-7843).\n *\n * A Type pairing (`./typography.ts`) decides the LATIN faces of the three roles\n * (sans / serif / mono). A font set decides the SCRIPT face that sits BEHIND\n * them, so a ja page draws its Latin in the pairing's face and its kana and\n * kanji in the ja set's face, under every pairing. The two axes are\n * independent: a set never changes which pairing is active, and a pairing never\n * changes which set a locale uses.\n *\n * ── Where the rows come from ────────────────────────────────────────────\n * Every set, member and default below is taken from\n * `docs/typography/font-sets.md` (MOTIR-7841), which read dooooWeb's mapping at\n * `225af77` and filled the roles dooooWeb leaves empty from cited references.\n * `inDooooWeb` carries that document's `in dooooWeb` / `added (not in dooooWeb)`\n * tag. Change a row here only together with that document; the package test\n * holds the two equal.\n *\n * ── What this module is NOT ─────────────────────────────────────────────\n * Data and pure resolvers only. It names CSS custom properties (the contract\n * between the app's `next/font` loader and `theme.css`) but sets none, loads no\n * face, and writes no CSS. Loading stays in the consuming app.\n */\n\n/** The eleven locales app.motir.co ships in. A closed product decision. */\nexport const FONT_SET_LOCALES = [\n 'en',\n 'zh',\n 'ja',\n 'ko',\n 'de',\n 'fr',\n 'es',\n 'it',\n 'nl',\n 'pl',\n 'pt',\n] as const;\n\n/** A locale the font-set registry knows. */\nexport type FontSetLocale = (typeof FONT_SET_LOCALES)[number];\n\n/** The three type roles every set covers: the same roles `--font-*` carries. */\nexport const FONT_SET_ROLES = ['sans', 'serif', 'mono'] as const;\n\n/** One of the three type roles. */\nexport type FontSetRole = (typeof FONT_SET_ROLES)[number];\n\n/**\n * Where a member's face comes from.\n *\n * - `type-pairing`: the active Type pairing's own face for the role. The Latin\n * set's only member; it loads nothing and has no CSS variable.\n * - `next/font/google`: a Google Fonts face the app loads with `next/font`;\n * `googleFamily` is the loader's export name (`Noto_Sans_JP`).\n */\nexport type FontSetMemberSource =\n | { readonly kind: 'type-pairing' }\n | { readonly kind: 'next/font/google'; readonly googleFamily: string };\n\n/** One selectable face in one role of one set. */\nexport interface FontSetMember {\n /** Stable kebab-case slug, unique within its role. The value a pick stores. */\n readonly id: string;\n /** The display name, or `null` for the `type-pairing` member. */\n readonly family: string | null;\n readonly source: FontSetMemberSource;\n /** `docs/typography/font-sets.md`'s tag: `in dooooWeb` (true) or `added` (false). */\n readonly inDooooWeb: boolean;\n /**\n * The role whose member of the SAME id this member re-uses the face of. A\n * CJK set's mono role draws its Han glyphs from the set's sans face (the Noto\n * Sans Mono CJK practice, see the doc), so the face is loaded once and both\n * roles read one variable.\n */\n readonly sameFaceAs?: FontSetRole;\n}\n\n/** One role of one set: its members, and which one a page uses by default. */\nexport interface FontSetRoleMembers {\n /** The id of the member used when nobody has picked one. */\n readonly default: string;\n readonly members: readonly FontSetMember[];\n}\n\n/** A locale group's font set. */\nexport interface FontSet {\n readonly id: string;\n /** Display name for `/tokens` and the picker. */\n readonly label: string;\n /**\n * The BCP 47 tag the set's `:lang()` rule matches. It is the tag the app\n * renders on `<html lang>` (`zh`, not `zh-Hans`), because `:lang(zh)`\n * matches `zh` and `zh-Hans` while `:lang(zh-Hans)` does not match `zh`.\n * `null` for the Latin set, which is matched by its locales instead.\n */\n readonly lang: string | null;\n /** True for a CJK set: its faces are unicode-range sliced and never preloaded. */\n readonly cjk: boolean;\n readonly roles: Readonly<Record<FontSetRole, FontSetRoleMembers>>;\n}\n\nconst TYPE_PAIRING: FontSetMember = {\n id: 'type-pairing',\n family: null,\n source: { kind: 'type-pairing' },\n inDooooWeb: false,\n};\n\nfunction google(\n id: string,\n family: string,\n inDooooWeb: boolean,\n sameFaceAs?: FontSetRole,\n): FontSetMember {\n return {\n id,\n family,\n source: { kind: 'next/font/google', googleFamily: family.replace(/ /g, '_') },\n inDooooWeb,\n ...(sameFaceAs ? { sameFaceAs } : {}),\n };\n}\n\n/**\n * The registry, one entry per set. Ids are the doc's registry ids; dooooWeb's\n * own group names are noted beside each.\n */\nexport const FONT_SET_REGISTRY = {\n // dooooWeb: `latin`. Its Latin picker faces are deliberately not members:\n // behind the pairing's face they would never draw a glyph (font-sets.md § latin).\n latin: {\n id: 'latin',\n label: 'Latin',\n lang: null,\n cjk: false,\n roles: {\n sans: { default: 'type-pairing', members: [TYPE_PAIRING] },\n serif: { default: 'type-pairing', members: [TYPE_PAIRING] },\n mono: { default: 'type-pairing', members: [TYPE_PAIRING] },\n },\n },\n // dooooWeb: `chinese-simplified`.\n 'zh-Hans': {\n id: 'zh-Hans',\n label: 'Simplified Chinese',\n lang: 'zh',\n cjk: true,\n roles: {\n sans: { default: 'noto-sans-sc', members: [google('noto-sans-sc', 'Noto Sans SC', false)] },\n serif: {\n default: 'noto-serif-sc',\n members: [\n google('noto-serif-sc', 'Noto Serif SC', true),\n // Draws Traditional / inherited forms, so never the default (font-sets.md § Region forms).\n google('lxgw-wenkai-tc', 'LXGW WenKai TC', true),\n ],\n },\n mono: {\n default: 'noto-sans-sc',\n members: [google('noto-sans-sc', 'Noto Sans SC', false, 'sans')],\n },\n },\n },\n // dooooWeb: `japanese`.\n ja: {\n id: 'ja',\n label: 'Japanese',\n lang: 'ja',\n cjk: true,\n roles: {\n sans: {\n default: 'noto-sans-jp',\n members: [\n google('noto-sans-jp', 'Noto Sans JP', true),\n google('m-plus-rounded-1c', 'M PLUS Rounded 1c', true),\n ],\n },\n serif: {\n default: 'noto-serif-jp',\n members: [google('noto-serif-jp', 'Noto Serif JP', false)],\n },\n mono: {\n default: 'noto-sans-jp',\n members: [google('noto-sans-jp', 'Noto Sans JP', false, 'sans')],\n },\n },\n },\n // dooooWeb: `korean`.\n ko: {\n id: 'ko',\n label: 'Korean',\n lang: 'ko',\n cjk: true,\n roles: {\n sans: {\n default: 'noto-sans-kr',\n members: [\n google('noto-sans-kr', 'Noto Sans KR', true),\n google('nanum-gothic', 'Nanum Gothic', true),\n ],\n },\n serif: {\n default: 'noto-serif-kr',\n members: [google('noto-serif-kr', 'Noto Serif KR', false)],\n },\n mono: {\n default: 'noto-sans-kr',\n members: [google('noto-sans-kr', 'Noto Sans KR', false, 'sans')],\n },\n },\n },\n} as const satisfies Record<string, FontSet>;\n\n/** The id of every registered set. */\nexport type FontSetId = keyof typeof FONT_SET_REGISTRY;\n\n/** All set ids, in registry order. */\nexport const FONT_SET_IDS = Object.keys(FONT_SET_REGISTRY) as FontSetId[];\n\n/** The set an unknown or empty page language falls back to: the app renders it in English. */\nexport const DEFAULT_FONT_SET_ID: FontSetId = 'latin';\n\n/** Which set each locale is drawn in (font-sets.md § Locale → set). */\nexport const LOCALE_FONT_SET = {\n en: 'latin',\n zh: 'zh-Hans',\n ja: 'ja',\n ko: 'ko',\n de: 'latin',\n fr: 'latin',\n es: 'latin',\n it: 'latin',\n nl: 'latin',\n pl: 'latin',\n pt: 'latin',\n} as const satisfies Record<FontSetLocale, FontSetId>;\n\n/** Narrowing guard: is an arbitrary value one of the eleven locales? */\nexport function isFontSetLocale(value: unknown): value is FontSetLocale {\n return typeof value === 'string' && (FONT_SET_LOCALES as readonly string[]).includes(value);\n}\n\n/**\n * The set for an `<html lang>` value. Matches the primary subtag\n * case-insensitively (`ja-JP` → ja, `PT-br` → pt, `zh-Hans` → zh).\n *\n * The fallback is explicit: an empty or unknown tag returns the Latin set,\n * because the app renders an unknown language in English. It never falls back\n * to \"the nearest CJK set\". Use `isFontSetLocale` on the primary subtag to tell\n * a fallback from a match.\n */\nexport function resolveFontSet(lang: string): FontSet {\n const primary = lang.trim().split(/[-_]/)[0]?.toLowerCase() ?? '';\n const id = isFontSetLocale(primary) ? LOCALE_FONT_SET[primary] : DEFAULT_FONT_SET_ID;\n return FONT_SET_REGISTRY[id];\n}\n\n/**\n * A member of one role of one set, by id. A missing or unknown id (a stored\n * pick naming a member that has since been removed) returns that role's\n * default, never another set's face.\n */\nexport function resolveFontSetMember(\n setId: FontSetId,\n role: FontSetRole,\n memberId?: string,\n): FontSetMember {\n const r: FontSetRoleMembers = FONT_SET_REGISTRY[setId].roles[role];\n const named = memberId === undefined ? undefined : r.members.find((m) => m.id === memberId);\n return named ?? r.members.find((m) => m.id === r.default)!;\n}\n\n/**\n * The CSS custom property the app's `next/font` loader assigns to a member's\n * face and `theme.css` reads: `--font-set-<set>-<role>-<member>`. This name is\n * the contract between the registry, the loader and the stylesheet, so it is\n * built here and nowhere else.\n *\n * A member that re-uses another role's face (`sameFaceAs`) returns THAT role's\n * variable, so the face is declared once. The `type-pairing` member, and an id\n * the role does not have, return `null`.\n */\nexport function fontSetMemberVar(\n setId: FontSetId,\n role: FontSetRole,\n memberId: string,\n): string | null {\n const member = FONT_SET_REGISTRY[setId].roles[role].members.find(\n (m: FontSetMember) => m.id === memberId,\n );\n if (!member || member.source.kind === 'type-pairing') return null;\n const faceRole = member.sameFaceAs ?? role;\n return `--font-set-${setId}-${faceRole}-${member.id}`;\n}\n\n/** The `<html>` attribute that names a picked member for one role. */\nexport type FontSetPickAttribute = `data-font-set-${FontSetRole}`;\n\n/**\n * The `data-font-set-<role>` attributes a person's pick for `locale` puts on\n * `<html>` (Story MOTIR-7736 · MOTIR-7897). One attribute per role of the\n * locale's set that has a member with this id, unless that member is the\n * `type-pairing` placeholder or the role's default: `theme.css` carries a rule\n * only for a non-default member, so a default needs no attribute.\n *\n * `null`, an unknown id, a default id and every Latin locale return `{}`. This\n * is the ONE derivation the client (`setFontPick`) and the server-rendered\n * first byte share, so a pick sets identical attributes on both sides — which\n * is why it lives here, free of any `'use client'` boundary.\n */\nexport function fontSetPickAttributes(\n locale: FontSetLocale,\n memberId: string | null,\n): Partial<Record<FontSetPickAttribute, string>> {\n if (memberId === null) return {};\n const set: FontSet = FONT_SET_REGISTRY[LOCALE_FONT_SET[locale]];\n const attrs: Partial<Record<FontSetPickAttribute, string>> = {};\n for (const role of FONT_SET_ROLES) {\n const r: FontSetRoleMembers = set.roles[role];\n if (memberId === r.default) continue;\n const member = r.members.find((m: FontSetMember) => m.id === memberId);\n if (member && member.source.kind !== 'type-pairing') attrs[`data-font-set-${role}`] = memberId;\n }\n return attrs;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/theme/fontSets.ts"],"names":[],"mappings":"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","file":"fontSets.js","sourcesContent":["/**\n * The Motir FONT-SET REGISTRY: which faces each locale's script is drawn in\n * (Story MOTIR-7733 · Subtask MOTIR-7843).\n *\n * A Type pairing (`./typography.ts`) decides the LATIN faces of the three roles\n * (sans / serif / mono). A font set decides the SCRIPT face that sits BEHIND\n * them, so a ja page draws its Latin in the pairing's face and its kana and\n * kanji in the ja set's face, under every pairing. The two axes are\n * independent: a set never changes which pairing is active, and a pairing never\n * changes which set a locale uses.\n *\n * ── Where the rows come from ────────────────────────────────────────────\n * Every set, member and default below is taken from\n * `docs/typography/font-sets.md` (MOTIR-7841), which read dooooWeb's mapping at\n * `225af77` and filled the roles dooooWeb leaves empty from cited references.\n * `inDooooWeb` carries that document's `in dooooWeb` / `added (not in dooooWeb)`\n * tag. Change a row here only together with that document; the package test\n * holds the two equal.\n *\n * ── What this module is NOT ─────────────────────────────────────────────\n * Data and pure resolvers only. It names CSS custom properties (the contract\n * between the app's `next/font` loader and `theme.css`) but sets none, loads no\n * face, and writes no CSS. Loading stays in the consuming app.\n */\n\n/** The eleven locales app.motir.co ships in. A closed product decision. */\nexport const FONT_SET_LOCALES = [\n 'en',\n 'zh',\n 'ja',\n 'ko',\n 'de',\n 'fr',\n 'es',\n 'it',\n 'nl',\n 'pl',\n 'pt',\n] as const;\n\n/** One of the eleven app locales. */\nexport type FontSetLocale = (typeof FONT_SET_LOCALES)[number];\n\n/**\n * Languages with a font set that only motir.co ships (MOTIR-8937 · MOTIR-9239).\n * Kept apart from `FONT_SET_LOCALES`, which is app.motir.co's own locale set and\n * is closed over its stored font picks (one `fontPick<Locale>` column each).\n */\nexport const FONT_SET_EXTRA_LOCALES = ['zh-tw'] as const;\n\n/** A language the font-set registry knows: an app locale or an extra one. */\nexport type FontSetLanguage = FontSetLocale | (typeof FONT_SET_EXTRA_LOCALES)[number];\n\n/** The three type roles every set covers: the same roles `--font-*` carries. */\nexport const FONT_SET_ROLES = ['sans', 'serif', 'mono'] as const;\n\n/** One of the three type roles. */\nexport type FontSetRole = (typeof FONT_SET_ROLES)[number];\n\n/**\n * Where a member's face comes from.\n *\n * - `type-pairing`: the active Type pairing's own face for the role. The Latin\n * set's only member; it loads nothing and has no CSS variable.\n * - `next/font/google`: a Google Fonts face the app loads with `next/font`;\n * `googleFamily` is the loader's export name (`Noto_Sans_JP`).\n */\nexport type FontSetMemberSource =\n | { readonly kind: 'type-pairing' }\n | { readonly kind: 'next/font/google'; readonly googleFamily: string };\n\n/** One selectable face in one role of one set. */\nexport interface FontSetMember {\n /** Stable kebab-case slug, unique within its role. The value a pick stores. */\n readonly id: string;\n /** The display name, or `null` for the `type-pairing` member. */\n readonly family: string | null;\n readonly source: FontSetMemberSource;\n /** `docs/typography/font-sets.md`'s tag: `in dooooWeb` (true) or `added` (false). */\n readonly inDooooWeb: boolean;\n /**\n * The role whose member of the SAME id this member re-uses the face of. A\n * CJK set's mono role draws its Han glyphs from the set's sans face (the Noto\n * Sans Mono CJK practice, see the doc), so the face is loaded once and both\n * roles read one variable.\n */\n readonly sameFaceAs?: FontSetRole;\n}\n\n/** One role of one set: its members, and which one a page uses by default. */\nexport interface FontSetRoleMembers {\n /** The id of the member used when nobody has picked one. */\n readonly default: string;\n readonly members: readonly FontSetMember[];\n}\n\n/** A locale group's font set. */\nexport interface FontSet {\n readonly id: string;\n /** Display name for `/tokens` and the picker. */\n readonly label: string;\n /**\n * The BCP 47 tag the set's `:lang()` rule matches. It is the tag the app\n * renders on `<html lang>` (`zh`, not `zh-Hans`), because `:lang(zh)`\n * matches `zh` and `zh-Hans` while `:lang(zh-Hans)` does not match `zh`.\n * `null` for the Latin set, which is matched by its locales instead.\n */\n readonly lang: string | null;\n /** True for a CJK set: its faces are unicode-range sliced and never preloaded. */\n readonly cjk: boolean;\n readonly roles: Readonly<Record<FontSetRole, FontSetRoleMembers>>;\n}\n\nconst TYPE_PAIRING: FontSetMember = {\n id: 'type-pairing',\n family: null,\n source: { kind: 'type-pairing' },\n inDooooWeb: false,\n};\n\nfunction google(\n id: string,\n family: string,\n inDooooWeb: boolean,\n sameFaceAs?: FontSetRole,\n): FontSetMember {\n return {\n id,\n family,\n source: { kind: 'next/font/google', googleFamily: family.replace(/ /g, '_') },\n inDooooWeb,\n ...(sameFaceAs ? { sameFaceAs } : {}),\n };\n}\n\n/**\n * The registry, one entry per set. Ids are the doc's registry ids; dooooWeb's\n * own group names are noted beside each.\n */\nexport const FONT_SET_REGISTRY = {\n // dooooWeb: `latin`. Its Latin picker faces are deliberately not members:\n // behind the pairing's face they would never draw a glyph (font-sets.md § latin).\n latin: {\n id: 'latin',\n label: 'Latin',\n lang: null,\n cjk: false,\n roles: {\n sans: { default: 'type-pairing', members: [TYPE_PAIRING] },\n serif: { default: 'type-pairing', members: [TYPE_PAIRING] },\n mono: { default: 'type-pairing', members: [TYPE_PAIRING] },\n },\n },\n // dooooWeb: `chinese-simplified`.\n 'zh-Hans': {\n id: 'zh-Hans',\n label: 'Simplified Chinese',\n lang: 'zh',\n cjk: true,\n roles: {\n sans: { default: 'noto-sans-sc', members: [google('noto-sans-sc', 'Noto Sans SC', false)] },\n serif: {\n default: 'noto-serif-sc',\n members: [\n google('noto-serif-sc', 'Noto Serif SC', true),\n // Draws Traditional / inherited forms, so never the default (font-sets.md § Region forms).\n google('lxgw-wenkai-tc', 'LXGW WenKai TC', true),\n ],\n },\n mono: {\n default: 'noto-sans-sc',\n members: [google('noto-sans-sc', 'Noto Sans SC', false, 'sans')],\n },\n },\n },\n // Not in dooooWeb: Traditional Chinese (Taiwan) for motir.co's `/zh-tw`\n // (MOTIR-8937). The zh-Hans set with its Traditional-form faces.\n 'zh-Hant': {\n id: 'zh-Hant',\n label: 'Traditional Chinese',\n lang: 'zh-TW',\n cjk: true,\n roles: {\n sans: { default: 'noto-sans-tc', members: [google('noto-sans-tc', 'Noto Sans TC', false)] },\n serif: {\n default: 'noto-serif-tc',\n members: [\n google('noto-serif-tc', 'Noto Serif TC', false),\n // A handwritten-style face, so a pick rather than the default.\n google('lxgw-wenkai-tc', 'LXGW WenKai TC', false),\n ],\n },\n mono: {\n default: 'noto-sans-tc',\n members: [google('noto-sans-tc', 'Noto Sans TC', false, 'sans')],\n },\n },\n },\n // dooooWeb: `japanese`.\n ja: {\n id: 'ja',\n label: 'Japanese',\n lang: 'ja',\n cjk: true,\n roles: {\n sans: {\n default: 'noto-sans-jp',\n members: [\n google('noto-sans-jp', 'Noto Sans JP', true),\n google('m-plus-rounded-1c', 'M PLUS Rounded 1c', true),\n ],\n },\n serif: {\n default: 'noto-serif-jp',\n members: [google('noto-serif-jp', 'Noto Serif JP', false)],\n },\n mono: {\n default: 'noto-sans-jp',\n members: [google('noto-sans-jp', 'Noto Sans JP', false, 'sans')],\n },\n },\n },\n // dooooWeb: `korean`.\n ko: {\n id: 'ko',\n label: 'Korean',\n lang: 'ko',\n cjk: true,\n roles: {\n sans: {\n default: 'noto-sans-kr',\n members: [\n google('noto-sans-kr', 'Noto Sans KR', true),\n google('nanum-gothic', 'Nanum Gothic', true),\n ],\n },\n serif: {\n default: 'noto-serif-kr',\n members: [google('noto-serif-kr', 'Noto Serif KR', false)],\n },\n mono: {\n default: 'noto-sans-kr',\n members: [google('noto-sans-kr', 'Noto Sans KR', false, 'sans')],\n },\n },\n },\n} as const satisfies Record<string, FontSet>;\n\n/** The id of every registered set. */\nexport type FontSetId = keyof typeof FONT_SET_REGISTRY;\n\n/** All set ids, in registry order. */\nexport const FONT_SET_IDS = Object.keys(FONT_SET_REGISTRY) as FontSetId[];\n\n/** The set an unknown or empty page language falls back to: the app renders it in English. */\nexport const DEFAULT_FONT_SET_ID: FontSetId = 'latin';\n\n/** Which set each locale is drawn in (font-sets.md § Locale → set). */\nexport const LOCALE_FONT_SET = {\n en: 'latin',\n zh: 'zh-Hans',\n ja: 'ja',\n ko: 'ko',\n de: 'latin',\n fr: 'latin',\n es: 'latin',\n it: 'latin',\n nl: 'latin',\n pl: 'latin',\n pt: 'latin',\n 'zh-tw': 'zh-Hant',\n} as const satisfies Record<FontSetLanguage, FontSetId>;\n\n/** Narrowing guard: is an arbitrary value one of the eleven locales? */\nexport function isFontSetLocale(value: unknown): value is FontSetLocale {\n return typeof value === 'string' && (FONT_SET_LOCALES as readonly string[]).includes(value);\n}\n\n/** Narrowing guard: is an arbitrary value an app locale or an extra language? */\nexport function isFontSetLanguage(value: unknown): value is FontSetLanguage {\n return (\n isFontSetLocale(value) ||\n (typeof value === 'string' && (FONT_SET_EXTRA_LOCALES as readonly string[]).includes(value))\n );\n}\n\n/**\n * The set for an `<html lang>` value. Tries the whole tag first, case-insensitively\n * and reading `_` as `-` (`zh-TW` / `zh_tw` → zh-Hant), then the primary subtag\n * (`ja-JP` → ja, `PT-br` → pt, `zh-Hans` → zh). No script or region heuristics:\n * `zh-Hant` and `zh-HK` reach zh-Hant only through a page whose `lang` is `zh-TW`.\n *\n * The fallback is explicit: an empty or unknown tag returns the Latin set,\n * because the app renders an unknown language in English. It never falls back\n * to \"the nearest CJK set\". Use `isFontSetLocale` on the primary subtag to tell\n * a fallback from a match.\n */\nexport function resolveFontSet(lang: string): FontSet {\n const tag = lang.trim().toLowerCase().replace(/_/g, '-');\n if (isFontSetLanguage(tag)) return FONT_SET_REGISTRY[LOCALE_FONT_SET[tag]];\n const primary = tag.split('-')[0] ?? '';\n const id = isFontSetLocale(primary) ? LOCALE_FONT_SET[primary] : DEFAULT_FONT_SET_ID;\n return FONT_SET_REGISTRY[id];\n}\n\n/**\n * A member of one role of one set, by id. A missing or unknown id (a stored\n * pick naming a member that has since been removed) returns that role's\n * default, never another set's face.\n */\nexport function resolveFontSetMember(\n setId: FontSetId,\n role: FontSetRole,\n memberId?: string,\n): FontSetMember {\n const r: FontSetRoleMembers = FONT_SET_REGISTRY[setId].roles[role];\n const named = memberId === undefined ? undefined : r.members.find((m) => m.id === memberId);\n return named ?? r.members.find((m) => m.id === r.default)!;\n}\n\n/**\n * The CSS custom property the app's `next/font` loader assigns to a member's\n * face and `theme.css` reads: `--font-set-<set>-<role>-<member>`. This name is\n * the contract between the registry, the loader and the stylesheet, so it is\n * built here and nowhere else.\n *\n * A member that re-uses another role's face (`sameFaceAs`) returns THAT role's\n * variable, so the face is declared once. The `type-pairing` member, and an id\n * the role does not have, return `null`.\n */\nexport function fontSetMemberVar(\n setId: FontSetId,\n role: FontSetRole,\n memberId: string,\n): string | null {\n const member = FONT_SET_REGISTRY[setId].roles[role].members.find(\n (m: FontSetMember) => m.id === memberId,\n );\n if (!member || member.source.kind === 'type-pairing') return null;\n const faceRole = member.sameFaceAs ?? role;\n return `--font-set-${setId}-${faceRole}-${member.id}`;\n}\n\n/** The `<html>` attribute that names a picked member for one role. */\nexport type FontSetPickAttribute = `data-font-set-${FontSetRole}`;\n\n/**\n * The `data-font-set-<role>` attributes a person's pick for `locale` puts on\n * `<html>` (Story MOTIR-7736 · MOTIR-7897). One attribute per role of the\n * locale's set that has a member with this id, unless that member is the\n * `type-pairing` placeholder or the role's default: `theme.css` carries a rule\n * only for a non-default member, so a default needs no attribute.\n *\n * `null`, an unknown id, a default id and every Latin locale return `{}`. This\n * is the ONE derivation the client (`setFontPick`) and the server-rendered\n * first byte share, so a pick sets identical attributes on both sides — which\n * is why it lives here, free of any `'use client'` boundary.\n */\nexport function fontSetPickAttributes(\n locale: FontSetLanguage,\n memberId: string | null,\n): Partial<Record<FontSetPickAttribute, string>> {\n if (memberId === null) return {};\n const set: FontSet = FONT_SET_REGISTRY[LOCALE_FONT_SET[locale]];\n const attrs: Partial<Record<FontSetPickAttribute, string>> = {};\n for (const role of FONT_SET_ROLES) {\n const r: FontSetRoleMembers = set.roles[role];\n if (memberId === r.default) continue;\n const member = r.members.find((m: FontSetMember) => m.id === memberId);\n if (member && member.source.kind !== 'type-pairing') attrs[`data-font-set-${role}`] = memberId;\n }\n return attrs;\n}\n"]}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The element currently in NATIVE full screen (`document.fullscreenElement`), or
|
|
3
5
|
* `null` — re-read on every `fullscreenchange`.
|
|
@@ -6,12 +8,29 @@
|
|
|
6
8
|
* element's subtree, in the top layer. Anything portalled to `document.body` —
|
|
7
9
|
* a dialog, a popover, a picker's menu — is not shown at all, whatever its
|
|
8
10
|
* z-index (MOTIR-7658: the roadmap canvas's quick view opened invisibly and
|
|
9
|
-
* held focus and scroll lock until Esc).
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* held focus and scroll lock until Esc).
|
|
12
|
+
*
|
|
13
|
+
* A portalling primitive does not use this directly: it uses
|
|
14
|
+
* `usePortalContainer`, which also refuses to follow full screen into the
|
|
15
|
+
* primitive's own subtree.
|
|
12
16
|
*
|
|
13
17
|
* `null` on the server and in a document with no Fullscreen API.
|
|
14
18
|
*/
|
|
15
19
|
declare function useFullscreenElement(): Element | null;
|
|
20
|
+
/**
|
|
21
|
+
* The element a portalling surface mounts into: the element in native full
|
|
22
|
+
* screen while there is one (MOTIR-7658), else `null` — which the caller turns
|
|
23
|
+
* into `document.body`.
|
|
24
|
+
*
|
|
25
|
+
* `own` is the surface's portalled root (a dialog's content, a menu). When the
|
|
26
|
+
* element that goes full screen is INSIDE it — the native full-screen button of
|
|
27
|
+
* a `<video>` in a dialog — the container stays where it was (MOTIR-8560).
|
|
28
|
+
* Following it would re-portal the surface into its own descendant: React
|
|
29
|
+
* unmounts the subtree to move it, the full-screen element leaves the document,
|
|
30
|
+
* the browser exits full screen, and the surface snaps back to a fresh copy.
|
|
31
|
+
*
|
|
32
|
+
* `null` on the server and in a document with no Fullscreen API.
|
|
33
|
+
*/
|
|
34
|
+
declare function usePortalContainer(own: RefObject<Element | null>): Element | null;
|
|
16
35
|
|
|
17
|
-
export { useFullscreenElement };
|
|
36
|
+
export { useFullscreenElement, usePortalContainer };
|
package/dist/utils/fullscreen.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { useSyncExternalStore } from 'react';
|
|
2
|
+
import { useSyncExternalStore, useState } from 'react';
|
|
3
3
|
|
|
4
4
|
function useFullscreenElement() {
|
|
5
5
|
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
@@ -14,7 +14,30 @@ function getSnapshot() {
|
|
|
14
14
|
function getServerSnapshot() {
|
|
15
15
|
return null;
|
|
16
16
|
}
|
|
17
|
+
function usePortalContainer(own) {
|
|
18
|
+
const [store] = useState(() => createPortalContainerStore(own));
|
|
19
|
+
return useSyncExternalStore(store.subscribe, store.getSnapshot, getServerSnapshot);
|
|
20
|
+
}
|
|
21
|
+
function createPortalContainerStore(own) {
|
|
22
|
+
let container = typeof document === "undefined" ? null : document.fullscreenElement ?? null;
|
|
23
|
+
return {
|
|
24
|
+
subscribe(onChange) {
|
|
25
|
+
const sync = () => {
|
|
26
|
+
const next = document.fullscreenElement ?? null;
|
|
27
|
+
const root = own.current;
|
|
28
|
+
if (next !== null && root !== null && root.contains(next)) return;
|
|
29
|
+
if (next === container) return;
|
|
30
|
+
container = next;
|
|
31
|
+
onChange();
|
|
32
|
+
};
|
|
33
|
+
sync();
|
|
34
|
+
document.addEventListener("fullscreenchange", sync);
|
|
35
|
+
return () => document.removeEventListener("fullscreenchange", sync);
|
|
36
|
+
},
|
|
37
|
+
getSnapshot: () => container
|
|
38
|
+
};
|
|
39
|
+
}
|
|
17
40
|
|
|
18
|
-
export { useFullscreenElement };
|
|
41
|
+
export { useFullscreenElement, usePortalContainer };
|
|
19
42
|
//# sourceMappingURL=fullscreen.js.map
|
|
20
43
|
//# sourceMappingURL=fullscreen.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/fullscreen.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"sources":["../../src/utils/fullscreen.ts"],"names":[],"mappings":";;AAoBO,SAAS,oBAAA,GAAuC;AACrD,EAAA,OAAO,oBAAA,CAAqB,SAAA,EAAW,WAAA,EAAa,iBAAiB,CAAA;AACvE;AAEA,SAAS,UAAU,QAAA,EAAkC;AACnD,EAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAoB,QAAQ,CAAA;AACtD,EAAA,OAAO,MAAM,QAAA,CAAS,mBAAA,CAAoB,kBAAA,EAAoB,QAAQ,CAAA;AACxE;AAEA,SAAS,WAAA,GAA8B;AACrC,EAAA,OAAO,SAAS,iBAAA,IAAqB,IAAA;AACvC;AAEA,SAAS,iBAAA,GAA0B;AACjC,EAAA,OAAO,IAAA;AACT;AAgBO,SAAS,mBAAmB,GAAA,EAAgD;AACjF,EAAA,MAAM,CAAC,KAAK,CAAA,GAAI,SAAS,MAAM,0BAAA,CAA2B,GAAG,CAAC,CAAA;AAC9D,EAAA,OAAO,oBAAA,CAAqB,KAAA,CAAM,SAAA,EAAW,KAAA,CAAM,aAAa,iBAAiB,CAAA;AACnF;AAEA,SAAS,2BAA2B,GAAA,EAAgC;AAClE,EAAA,IAAI,YACF,OAAO,QAAA,KAAa,WAAA,GAAc,IAAA,GAAQ,SAAS,iBAAA,IAAqB,IAAA;AAC1E,EAAA,OAAO;AAAA,IACL,UAAU,QAAA,EAAkC;AAC1C,MAAA,MAAM,OAAO,MAAM;AACjB,QAAA,MAAM,IAAA,GAAO,SAAS,iBAAA,IAAqB,IAAA;AAC3C,QAAA,MAAM,OAAO,GAAA,CAAI,OAAA;AAEjB,QAAA,IAAI,SAAS,IAAA,IAAQ,IAAA,KAAS,QAAQ,IAAA,CAAK,QAAA,CAAS,IAAI,CAAA,EAAG;AAC3D,QAAA,IAAI,SAAS,SAAA,EAAW;AACxB,QAAA,SAAA,GAAY,IAAA;AACZ,QAAA,QAAA,EAAS;AAAA,MACX,CAAA;AAEA,MAAA,IAAA,EAAK;AACL,MAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAoB,IAAI,CAAA;AAClD,MAAA,OAAO,MAAM,QAAA,CAAS,mBAAA,CAAoB,kBAAA,EAAoB,IAAI,CAAA;AAAA,IACpE,CAAA;AAAA,IACA,aAAa,MAAsB;AAAA,GACrC;AACF","file":"fullscreen.js","sourcesContent":["'use client';\n\nimport { useState, useSyncExternalStore, type RefObject } from 'react';\n\n/**\n * The element currently in NATIVE full screen (`document.fullscreenElement`), or\n * `null` — re-read on every `fullscreenchange`.\n *\n * While an element is in native full screen the browser paints ONLY that\n * element's subtree, in the top layer. Anything portalled to `document.body` —\n * a dialog, a popover, a picker's menu — is not shown at all, whatever its\n * z-index (MOTIR-7658: the roadmap canvas's quick view opened invisibly and\n * held focus and scroll lock until Esc).\n *\n * A portalling primitive does not use this directly: it uses\n * `usePortalContainer`, which also refuses to follow full screen into the\n * primitive's own subtree.\n *\n * `null` on the server and in a document with no Fullscreen API.\n */\nexport function useFullscreenElement(): Element | null {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n\nfunction subscribe(onChange: () => void): () => void {\n document.addEventListener('fullscreenchange', onChange);\n return () => document.removeEventListener('fullscreenchange', onChange);\n}\n\nfunction getSnapshot(): Element | null {\n return document.fullscreenElement ?? null;\n}\n\nfunction getServerSnapshot(): null {\n return null;\n}\n\n/**\n * The element a portalling surface mounts into: the element in native full\n * screen while there is one (MOTIR-7658), else `null` — which the caller turns\n * into `document.body`.\n *\n * `own` is the surface's portalled root (a dialog's content, a menu). When the\n * element that goes full screen is INSIDE it — the native full-screen button of\n * a `<video>` in a dialog — the container stays where it was (MOTIR-8560).\n * Following it would re-portal the surface into its own descendant: React\n * unmounts the subtree to move it, the full-screen element leaves the document,\n * the browser exits full screen, and the surface snaps back to a fresh copy.\n *\n * `null` on the server and in a document with no Fullscreen API.\n */\nexport function usePortalContainer(own: RefObject<Element | null>): Element | null {\n const [store] = useState(() => createPortalContainerStore(own));\n return useSyncExternalStore(store.subscribe, store.getSnapshot, getServerSnapshot);\n}\n\nfunction createPortalContainerStore(own: RefObject<Element | null>) {\n let container: Element | null =\n typeof document === 'undefined' ? null : (document.fullscreenElement ?? null);\n return {\n subscribe(onChange: () => void): () => void {\n const sync = () => {\n const next = document.fullscreenElement ?? null;\n const root = own.current;\n // The surface's own descendant went full screen: stay put.\n if (next !== null && root !== null && root.contains(next)) return;\n if (next === container) return;\n container = next;\n onChange();\n };\n // A change between creating the store and subscribing to it.\n sync();\n document.addEventListener('fullscreenchange', sync);\n return () => document.removeEventListener('fullscreenchange', sync);\n },\n getSnapshot: (): Element | null => container,\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motir/design-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Motir's 3-axis design system (colour · style · type): distributable token CSS, the axis registries, the theme-apply API, and framework-agnostic React primitives. Shared by motir-core and planner-scaffolded products.",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"repository": {
|
package/theme.css
CHANGED
|
@@ -287,6 +287,57 @@
|
|
|
287
287
|
--font-mono-tail: monospace;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
/* The DENSITY STEPS (MOTIR-8901; the vocabulary is MOTIR-2336's approved
|
|
291
|
+
decision, Option 1). A surface's own padding / height is a SCALE, not one
|
|
292
|
+
value, so each step is DERIVED from the base token it scales: the name is
|
|
293
|
+
the step's pixel value in the reference style (warm-editorial, whose bases
|
|
294
|
+
are the @theme ones above), and the value is that base multiplied by
|
|
295
|
+
N / <reference px> and rounded to a whole pixel. Under warm-editorial every
|
|
296
|
+
step is exactly N px; a denser or roomier style moves the base and every step
|
|
297
|
+
follows it, with no per-style declaration to keep in sync.
|
|
298
|
+
|
|
299
|
+
⚠️ TWO SELECTORS, ON PURPOSE. A custom property is substituted where it is
|
|
300
|
+
DECLARED, not where it is used. A rule on `:root` alone would resolve
|
|
301
|
+
`var(--spacing-control-x)` once, against the root's value, and hand that
|
|
302
|
+
frozen length to a nested `[data-style]` subtree (a scoped /tokens tile, a
|
|
303
|
+
style preview) that overrides the base — the same hazard
|
|
304
|
+
`--width-rail-chrome` records above. Listing `[data-style]` beside `:root`
|
|
305
|
+
re-declares every step on each scope that carries its own bases, so it
|
|
306
|
+
re-resolves there.
|
|
307
|
+
|
|
308
|
+
The `[data-style]` blocks are deliberately NOT edited: a step is never
|
|
309
|
+
written per style. A step is added by ONE line here, and only when at least
|
|
310
|
+
three real sites need it. Button, input, icon-button, kbd and tooltip tokens
|
|
311
|
+
carry no steps. */
|
|
312
|
+
:root,
|
|
313
|
+
[data-style] {
|
|
314
|
+
--spacing-card-padding-16: round(nearest, calc(var(--spacing-card-padding) * 16 / 24), 1px);
|
|
315
|
+
--spacing-card-padding-12: round(nearest, calc(var(--spacing-card-padding) * 12 / 24), 1px);
|
|
316
|
+
--spacing-card-padding-8: round(nearest, calc(var(--spacing-card-padding) * 8 / 24), 1px);
|
|
317
|
+
--spacing-card-padding-4: round(nearest, calc(var(--spacing-card-padding) * 4 / 24), 1px);
|
|
318
|
+
|
|
319
|
+
--spacing-control-x-16: round(nearest, calc(var(--spacing-control-x) * 16 / 10), 1px);
|
|
320
|
+
--spacing-control-x-12: round(nearest, calc(var(--spacing-control-x) * 12 / 10), 1px);
|
|
321
|
+
--spacing-control-x-8: round(nearest, calc(var(--spacing-control-x) * 8 / 10), 1px);
|
|
322
|
+
--spacing-control-x-6: round(nearest, calc(var(--spacing-control-x) * 6 / 10), 1px);
|
|
323
|
+
--spacing-control-x-4: round(nearest, calc(var(--spacing-control-x) * 4 / 10), 1px);
|
|
324
|
+
--spacing-control-x-2: round(nearest, calc(var(--spacing-control-x) * 2 / 10), 1px);
|
|
325
|
+
|
|
326
|
+
--spacing-control-y-8: round(nearest, calc(var(--spacing-control-y) * 8 / 6), 1px);
|
|
327
|
+
--spacing-control-y-4: round(nearest, calc(var(--spacing-control-y) * 4 / 6), 1px);
|
|
328
|
+
--spacing-control-y-2: round(nearest, calc(var(--spacing-control-y) * 2 / 6), 1px);
|
|
329
|
+
|
|
330
|
+
--spacing-chip-x-8: round(nearest, calc(var(--spacing-chip-x) * 8 / 10), 1px);
|
|
331
|
+
--spacing-chip-x-6: round(nearest, calc(var(--spacing-chip-x) * 6 / 10), 1px);
|
|
332
|
+
--spacing-chip-x-4: round(nearest, calc(var(--spacing-chip-x) * 4 / 10), 1px);
|
|
333
|
+
--spacing-chip-x-2: round(nearest, calc(var(--spacing-chip-x) * 2 / 10), 1px);
|
|
334
|
+
|
|
335
|
+
--height-control-28: round(nearest, calc(var(--height-control) * 28 / 36), 1px);
|
|
336
|
+
--height-control-32: round(nearest, calc(var(--height-control) * 32 / 36), 1px);
|
|
337
|
+
--height-control-40: round(nearest, calc(var(--height-control) * 40 / 36), 1px);
|
|
338
|
+
--height-control-48: round(nearest, calc(var(--height-control) * 48 / 36), 1px);
|
|
339
|
+
}
|
|
340
|
+
|
|
290
341
|
/* ═══════════════════════════════════════════════════════════════════════
|
|
291
342
|
TIER 1 — Dark mode color overrides
|
|
292
343
|
Activated when [data-theme="dark"] is set on <html> (by the init
|
|
@@ -3366,6 +3417,11 @@
|
|
|
3366
3417
|
(The planning node's done/total meter is NOT this role: it means DONE and
|
|
3367
3418
|
stays --el-success.) */
|
|
3368
3419
|
--el-progress-fill: var(--color-primary-fill);
|
|
3420
|
+
/* THE DRAFTING CUE (MOTIR-8414 · MOTIR-8416) — the card Motir is writing right now:
|
|
3421
|
+
its ring, chip edge and pen (`--el-drafting`) and the wash on the card
|
|
3422
|
+
(`--el-drafting-surface`). Base keeps the in-progress hue; Motir warms it. */
|
|
3423
|
+
--el-drafting: var(--el-status-in-progress);
|
|
3424
|
+
--el-drafting-surface: var(--el-tint-sky);
|
|
3369
3425
|
/* The WARM primary CTA (MOTIR-8251 Revision 5 · built by MOTIR-8254) — the
|
|
3370
3426
|
onboarding screens' one warm element: the palette's decorative highlight as
|
|
3371
3427
|
the FILL, ink as the LABEL. The label is dark in BOTH themes, because white
|
|
@@ -3851,6 +3907,10 @@
|
|
|
3851
3907
|
--el-chart-surplus: var(--color-success); /* fill where resolved > created */
|
|
3852
3908
|
--el-chart-age: var(--color-warning); /* average-age bar (8.8.13 — aging backlog) */
|
|
3853
3909
|
--el-chart-resolution: var(--color-info); /* resolution-time bar (8.8.13 — throughput) */
|
|
3910
|
+
--el-chart-workload-assigned: var(--color-primary); /* workload: assigned segment (MOTIR-8925) */
|
|
3911
|
+
--el-chart-workload-unassigned: var(
|
|
3912
|
+
--color-accent-teal
|
|
3913
|
+
); /* workload: unassigned-by-reporter segment */
|
|
3854
3914
|
/* Sprint CYCLE GRAPH series (Story 8.14 — Linear scope-vs-completed). A
|
|
3855
3915
|
collision-free --el-chart-cycle-* family: the bare --el-chart-completed /
|
|
3856
3916
|
-started / -target names are taken (velocity / old burndown), so reusing
|
|
@@ -4040,6 +4100,16 @@
|
|
|
4040
4100
|
--el-showcase-wave-1: var(--color-primary);
|
|
4041
4101
|
--el-showcase-wave-2: var(--color-accent-teal);
|
|
4042
4102
|
--el-showcase-wave-3: var(--color-accent);
|
|
4103
|
+
/* The Motir mark AT WORK (MOTIR-9212 · MOTIR-9213): the three hues the mark
|
|
4104
|
+
cycles through while it flows on the ink circle (`--el-accent`). Each wave
|
|
4105
|
+
hue pulled toward the ink already on the circle, the shape of
|
|
4106
|
+
--el-danger-on-surface: the raw hues fall to 1.00:1 on the circle in the
|
|
4107
|
+
palettes whose accent IS a wave hue. At 35% every palette and theme keeps
|
|
4108
|
+
3:1 (WCAG 1.4.11, a graphic) for each role and every blend between them;
|
|
4109
|
+
the default Motir palette keeps 60% (its own blocks, below). */
|
|
4110
|
+
--el-mark-wave-1: color-mix(in srgb, var(--el-showcase-wave-1) 35%, var(--el-accent-text));
|
|
4111
|
+
--el-mark-wave-2: color-mix(in srgb, var(--el-showcase-wave-2) 35%, var(--el-accent-text));
|
|
4112
|
+
--el-mark-wave-3: color-mix(in srgb, var(--el-showcase-wave-3) 35%, var(--el-accent-text));
|
|
4043
4113
|
/* motir.co's live link ("Motir builds itself" in the header): its square
|
|
4044
4114
|
and its words walk the wave hues. The mark is each hue as is; the text
|
|
4045
4115
|
is each hue mixed 60/40 with the primary ink, because the raw teal and
|
|
@@ -4311,6 +4381,10 @@
|
|
|
4311
4381
|
--el-editor-focus: var(--color-accent);
|
|
4312
4382
|
--el-priority-high: var(--color-warning);
|
|
4313
4383
|
--el-progress-fill: var(--color-primary-fill);
|
|
4384
|
+
/* MOTIR-8416 — carried here because `motir` OVERRIDES both (the warm orange
|
|
4385
|
+
drafting cue); a scoped amethyst tile keeps the base's in-progress hue. */
|
|
4386
|
+
--el-drafting: var(--el-status-in-progress);
|
|
4387
|
+
--el-drafting-surface: var(--el-tint-sky);
|
|
4314
4388
|
--el-sidebar-item-bg-hover: #eeede9;
|
|
4315
4389
|
/* MOTIR-5139 — carried here because `evergreen` OVERRIDES it. A token this
|
|
4316
4390
|
block omits leaks from the ANCESTOR into a scoped `motir` tile, so an
|
|
@@ -4331,6 +4405,9 @@
|
|
|
4331
4405
|
palettes override, restated so a scoped amethyst tile keeps the base. */
|
|
4332
4406
|
--el-logo-mark: var(--color-primary-fill);
|
|
4333
4407
|
--el-logo-tile: color-mix(in srgb, var(--color-primary-fill) 14%, var(--color-background));
|
|
4408
|
+
--el-mark-wave-1: color-mix(in srgb, var(--el-showcase-wave-1) 35%, var(--el-accent-text));
|
|
4409
|
+
--el-mark-wave-2: color-mix(in srgb, var(--el-showcase-wave-2) 35%, var(--el-accent-text));
|
|
4410
|
+
--el-mark-wave-3: color-mix(in srgb, var(--el-showcase-wave-3) 35%, var(--el-accent-text));
|
|
4334
4411
|
--el-showcase-highlight: color-mix(in srgb, var(--color-accent) 55%, var(--color-background));
|
|
4335
4412
|
--el-showcase-decision: color-mix(in srgb, var(--color-accent) 70%, var(--color-background));
|
|
4336
4413
|
}
|
|
@@ -4594,6 +4671,11 @@
|
|
|
4594
4671
|
ink CTA, not its identity hue) on a 28% wash of that blue. */
|
|
4595
4672
|
--el-logo-mark: var(--color-primary);
|
|
4596
4673
|
--el-logo-tile: color-mix(in srgb, var(--color-primary) 28%, var(--color-background));
|
|
4674
|
+
/* The mark at work keeps more of its hue on the Motir circle (MOTIR-9212):
|
|
4675
|
+
its lowest pair is 4.40:1 at 60%, and the hues stay the prototype's. */
|
|
4676
|
+
--el-mark-wave-1: color-mix(in srgb, var(--el-showcase-wave-1) 60%, var(--el-accent-text));
|
|
4677
|
+
--el-mark-wave-2: color-mix(in srgb, var(--el-showcase-wave-2) 60%, var(--el-accent-text));
|
|
4678
|
+
--el-mark-wave-3: color-mix(in srgb, var(--el-showcase-wave-3) 60%, var(--el-accent-text));
|
|
4597
4679
|
/* brand & primary — the high-contrast INK CTA + the single cool-blue accent.
|
|
4598
4680
|
--color-primary is the accent AS text/icon/focus on a surface (the cool
|
|
4599
4681
|
blue, AA on white & the slate surface); --color-primary-fill is the CTA
|
|
@@ -4715,6 +4797,9 @@
|
|
|
4715
4797
|
--el-showcase-decision: var(--color-accent);
|
|
4716
4798
|
/* progress bars take the warm accent instead of the ink CTA fill */
|
|
4717
4799
|
--el-progress-fill: var(--color-accent);
|
|
4800
|
+
/* the drafting cue takes the warm accent and the peach wash (MOTIR-8414) */
|
|
4801
|
+
--el-drafting: var(--color-accent);
|
|
4802
|
+
--el-drafting-surface: var(--color-tint-peach);
|
|
4718
4803
|
/* the editor's focus border / rings stay the blue focus hue — the accent they
|
|
4719
4804
|
borrowed is decorative-only here now */
|
|
4720
4805
|
--el-editor-focus: var(--color-primary);
|
|
@@ -4728,6 +4813,9 @@
|
|
|
4728
4813
|
[data-palette='motir'][data-theme='dark'] {
|
|
4729
4814
|
--el-logo-mark: var(--color-primary); /* parity with the light block */
|
|
4730
4815
|
--el-logo-tile: color-mix(in srgb, var(--color-primary) 28%, var(--color-background));
|
|
4816
|
+
--el-mark-wave-1: color-mix(in srgb, var(--el-showcase-wave-1) 60%, var(--el-accent-text));
|
|
4817
|
+
--el-mark-wave-2: color-mix(in srgb, var(--el-showcase-wave-2) 60%, var(--el-accent-text));
|
|
4818
|
+
--el-mark-wave-3: color-mix(in srgb, var(--el-showcase-wave-3) 60%, var(--el-accent-text));
|
|
4731
4819
|
--color-background: #0c0d0f; /* stark cool near-black canvas */
|
|
4732
4820
|
--color-foreground: #edeef0;
|
|
4733
4821
|
--color-surface: #18191c; /* cool slate section */
|
|
@@ -4809,6 +4897,8 @@
|
|
|
4809
4897
|
--el-showcase-highlight: #ffd02f; /* see the light block */
|
|
4810
4898
|
--el-showcase-decision: var(--color-accent);
|
|
4811
4899
|
--el-progress-fill: var(--color-accent);
|
|
4900
|
+
--el-drafting: var(--color-accent);
|
|
4901
|
+
--el-drafting-surface: var(--color-tint-peach);
|
|
4812
4902
|
--el-editor-focus: var(--color-primary);
|
|
4813
4903
|
}
|
|
4814
4904
|
|
|
@@ -6210,6 +6300,23 @@
|
|
|
6210
6300
|
--font-script-serif: var(--font-set-zh-Hans-serif-lxgw-wenkai-tc, serif);
|
|
6211
6301
|
}
|
|
6212
6302
|
|
|
6303
|
+
/* Traditional Chinese, Taiwan (zh-Hant · MOTIR-8937): each role's default
|
|
6304
|
+
member. `:lang(zh)` also matches zh-TW, so these blocks come AFTER both zh
|
|
6305
|
+
blocks above: the specificity is equal and source order hands zh-TW its own
|
|
6306
|
+
faces, including over a zh by-name pick of the same attribute. */
|
|
6307
|
+
[lang]:lang(zh-TW) {
|
|
6308
|
+
--font-script-sans: var(--font-set-zh-Hant-sans-noto-sans-tc, sans-serif);
|
|
6309
|
+
--font-script-serif: var(--font-set-zh-Hant-serif-noto-serif-tc, serif);
|
|
6310
|
+
--font-script-mono: var(--font-set-zh-Hant-sans-noto-sans-tc, monospace);
|
|
6311
|
+
}
|
|
6312
|
+
|
|
6313
|
+
/* A member picked by name. Comes after the set's block, so on an element
|
|
6314
|
+
carrying both a lang and the attribute this one wins the specificity tie.
|
|
6315
|
+
An id the registry does not list matches nothing and keeps the default. */
|
|
6316
|
+
:lang(zh-TW)[data-font-set-serif='lxgw-wenkai-tc'] {
|
|
6317
|
+
--font-script-serif: var(--font-set-zh-Hant-serif-lxgw-wenkai-tc, serif);
|
|
6318
|
+
}
|
|
6319
|
+
|
|
6213
6320
|
/* Japanese (ja): each role's default member. A CJK mono role
|
|
6214
6321
|
draws its Han glyphs from the set's sans face (fontSets.ts sameFaceAs), so
|
|
6215
6322
|
the block stays total over the three roles while loading no extra face. */
|