@motir/design-system 0.15.1 → 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.
@@ -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
- /** A locale the font-set registry knows. */
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. Matches the primary subtag
196
- * case-insensitively (`ja-JP` → ja, `PT-br` → pt, `zh-Hans` → zh).
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: FontSetLocale, memberId: string | null): Partial<Record<FontSetPickAttribute, string>>;
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 };
@@ -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 primary = lang.trim().split(/[-_]/)[0]?.toLowerCase() ?? "";
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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motir/design-system",
3
- "version": "0.15.1",
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
@@ -3856,6 +3907,10 @@
3856
3907
  --el-chart-surplus: var(--color-success); /* fill where resolved > created */
3857
3908
  --el-chart-age: var(--color-warning); /* average-age bar (8.8.13 — aging backlog) */
3858
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 */
3859
3914
  /* Sprint CYCLE GRAPH series (Story 8.14 — Linear scope-vs-completed). A
3860
3915
  collision-free --el-chart-cycle-* family: the bare --el-chart-completed /
3861
3916
  -started / -target names are taken (velocity / old burndown), so reusing
@@ -4045,6 +4100,16 @@
4045
4100
  --el-showcase-wave-1: var(--color-primary);
4046
4101
  --el-showcase-wave-2: var(--color-accent-teal);
4047
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));
4048
4113
  /* motir.co's live link ("Motir builds itself" in the header): its square
4049
4114
  and its words walk the wave hues. The mark is each hue as is; the text
4050
4115
  is each hue mixed 60/40 with the primary ink, because the raw teal and
@@ -4340,6 +4405,9 @@
4340
4405
  palettes override, restated so a scoped amethyst tile keeps the base. */
4341
4406
  --el-logo-mark: var(--color-primary-fill);
4342
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));
4343
4411
  --el-showcase-highlight: color-mix(in srgb, var(--color-accent) 55%, var(--color-background));
4344
4412
  --el-showcase-decision: color-mix(in srgb, var(--color-accent) 70%, var(--color-background));
4345
4413
  }
@@ -4603,6 +4671,11 @@
4603
4671
  ink CTA, not its identity hue) on a 28% wash of that blue. */
4604
4672
  --el-logo-mark: var(--color-primary);
4605
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));
4606
4679
  /* brand & primary — the high-contrast INK CTA + the single cool-blue accent.
4607
4680
  --color-primary is the accent AS text/icon/focus on a surface (the cool
4608
4681
  blue, AA on white & the slate surface); --color-primary-fill is the CTA
@@ -4740,6 +4813,9 @@
4740
4813
  [data-palette='motir'][data-theme='dark'] {
4741
4814
  --el-logo-mark: var(--color-primary); /* parity with the light block */
4742
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));
4743
4819
  --color-background: #0c0d0f; /* stark cool near-black canvas */
4744
4820
  --color-foreground: #edeef0;
4745
4821
  --color-surface: #18191c; /* cool slate section */
@@ -6224,6 +6300,23 @@
6224
6300
  --font-script-serif: var(--font-set-zh-Hans-serif-lxgw-wenkai-tc, serif);
6225
6301
  }
6226
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
+
6227
6320
  /* Japanese (ja): each role's default member. A CJK mono role
6228
6321
  draws its Han glyphs from the set's sans face (fontSets.ts sameFaceAs), so
6229
6322
  the block stays total over the three roles while loading no extra face. */