@flyos/design-system 3.2.0 → 3.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flyos/design-system",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "FlyOS design system — shared components, directives, pipes, services, and models for Business App developers.",
5
5
  "keywords": [
6
6
  "flyos",
@@ -3,6 +3,11 @@
3
3
  // stylePreprocessorOptions.includePaths → design-system src folder
4
4
  // @use 'scss/fly-theme' or @use 'fly-theme' when scss is on the path.
5
5
  @use 'tokens';
6
+
7
+ // The Arabic face the RTL half of `--font-family` names. Emits an @font-face and
8
+ // nothing else, and must be @used from here alone — see the partial's header for
9
+ // why it is a design-system concern rather than a shell one.
10
+ @use 'sf-arabic-font';
6
11
  @use 'theme-light';
7
12
  @use 'theme-dark';
8
13
 
@@ -110,6 +110,19 @@ html.light-theme {
110
110
  --sys-green: #1B7355;
111
111
  --sys-purple: #6A3FC0;
112
112
 
113
+ // ── Greeting gradient ──────────────────────────────────────────────────────
114
+ // The three stops the Home greeting cycles through (`.mb-ctx__greeting`, via the
115
+ // `greetShift` keyframe; design §7 "Header context / greeting"). Ported from the
116
+ // design's own per-theme stop sets rather than re-pointed at --accent/--sys-*:
117
+ // those are a BRAND knob and STATUS hues, and borrowing them drew a saturated
118
+ // violet→cyan run where the design draws a deeper violet→teal on light glass.
119
+ // Deliberately NOT accent-derived, so a tenant `applyAccent` re-skin does not
120
+ // carry the greeting — that is the trade the design's fixed stops ask for.
121
+ // Tune HERE, never at the call site.
122
+ --greet-grad-1: #5B3FA8;
123
+ --greet-grad-2: #0A6E8F;
124
+ --greet-grad-3: #8A3FB0;
125
+
113
126
  // ── System TINTS ───────────────────────────────────────────────────────────
114
127
  // The status-wash step the hue family was missing. Before these existed, 75
115
128
  // feature-app call sites minted their own Tailwind-palette literal at a fixed
@@ -332,6 +345,15 @@ html.dark-theme {
332
345
  --sys-green: #4dd6a8;
333
346
  --sys-purple: #a48ce0;
334
347
 
348
+ // ── Greeting gradient ──────────────────────────────────────────────────────
349
+ // Same role as the light block (see its comment for the full rationale). The
350
+ // design draws the dark greeting as a pale lavender → cyan → mauve wash: these
351
+ // stops sit near white on a near-black plate, which is why they are nowhere
352
+ // near the saturated hues the status family carries at this position.
353
+ --greet-grad-1: #E4DBFF;
354
+ --greet-grad-2: #BFF0FF;
355
+ --greet-grad-3: #F3DEFF;
356
+
335
357
  // ── System TINTS ───────────────────────────────────────────────────────────
336
358
  // Same two-step law as the light block (see its comment for the full
337
359
  // rationale). Dark strengths are HIGHER on purpose: these hues are bright
@@ -22,12 +22,18 @@
22
22
 
23
23
  // ── The Display/Text split ───────────────────────────────────────────────────
24
24
  // The design distinguishes a Display face (headings ≥ ~13.5px bold) from Text
25
- // (everything else), but D8 (skill §7, ratified) forbids ever shipping a
26
- // second face for it no SF fonts, ever. Geist is ONE family for both; the
27
- // split rides WEIGHT and LETTER-SPACING only, never `font-family`. Proof
28
- // point: `--nova-font-menu-item` and `--nova-font-section-title` share the
29
- // exact same 13.5px SIZE and sit on opposite sides of the split (500/normal
30
- // Text vs 700/-0.01em Display) that pairing IS the split in action.
25
+ // (everything else). Since the UI-owner's 2026-08-18 one-system ruling that
26
+ // split rides `font-family` as well as weight and tracking: the six title roles
27
+ // below resolve `var(--font-family-display)`, every other role resolves
28
+ // `var(--font-family)`. Those are the platform's only two families and both are
29
+ // device-resident system faces, so honouring the split still ships no font file
30
+ // which is what the earlier D8 prohibition was actually protecting.
31
+ // ·
32
+ // Before that ruling the split was carried by WEIGHT and LETTER-SPACING alone,
33
+ // because D8 forbade a second face outright. The proof point that pairing left
34
+ // behind still reads: `--nova-font-menu-item` and `--nova-font-section-title`
35
+ // share the exact same 13.5px SIZE and sit on opposite sides (500/normal Text vs
36
+ // 700/-0.01em Display) — now they differ in family too.
31
37
 
32
38
  // ── Snap vs mint ─────────────────────────────────────────────────────────────
33
39
  // Each role is annotated SNAP (aliases an existing `--font-*` ramp step from
@@ -54,30 +60,30 @@
54
60
  // compact hero headline read visibly looser than the design. Pair with the
55
61
  // `greetShift` keyframe (`_nova-motion.scss`) for the animated gradient
56
62
  // fill; the fill itself is the caller's concern, not baked in here.
57
- --nova-font-page-headline: 700 28px/1.1 var(--font-family);
63
+ --nova-font-page-headline: 700 28px/1.1 var(--font-family-display);
58
64
  --nova-tracking-page-headline: -0.022em;
59
65
 
60
66
  // MINT. Size coincides with --font-title1 (24px), but that step's 32/24 ≈
61
67
  // 1.333 line-height ratio is a third looser than this role's 1.15 — the
62
68
  // assistant's hero greeting would visibly loosen on snap.
63
- --nova-font-hero-greeting: 700 24px/1.15 var(--font-family);
69
+ --nova-font-hero-greeting: 700 24px/1.15 var(--font-family-display);
64
70
  --nova-tracking-hero-greeting: -0.018em;
65
71
 
66
72
  // MINT. No ramp step sits at 26px, and none pairs a semibold (600) weight
67
73
  // with a sub-30px size. Colour/text-shadow (the D1 extraction notes
68
74
  // "#fff + text-shadow") are the caller's concern, not part of the type role.
69
- --nova-font-hero-card-name: 600 26px/1.14 var(--font-family);
75
+ --nova-font-hero-card-name: 600 26px/1.14 var(--font-family-display);
70
76
  --nova-tracking-hero-card-name: -0.01em;
71
77
 
72
78
  // ── Chrome / structural headings (Display) ────────────────────────────────
73
79
  // MINT. --font-headline (17px/700) is the nearest ramp step; its 22/17 ≈
74
80
  // 1.294 line-height ratio is far looser than this role's 1.1, and the ramp
75
81
  // carries no letter-spacing vocabulary at all.
76
- --nova-font-drawer-title: 700 16px/1.1 var(--font-family);
82
+ --nova-font-drawer-title: 700 16px/1.1 var(--font-family-display);
77
83
  --nova-tracking-drawer-title: -0.016em;
78
84
 
79
85
  // MINT. Nothing in the ramp sits near 14.5px at weight 700.
80
- --nova-font-dialog-title: 700 14.5px/1.1 var(--font-family);
86
+ --nova-font-dialog-title: 700 14.5px/1.1 var(--font-family-display);
81
87
  --nova-tracking-dialog-title: -0.014em;
82
88
 
83
89
  // MINT. The design gives a 500-600 WEIGHT RANGE at 14px; --font-footnote
@@ -95,7 +101,7 @@
95
101
  // ramp has no vocabulary for at all. Shares its 13.5px SIZE with
96
102
  // --nova-font-menu-item below on the opposite side of the Display/Text
97
103
  // split — see the file header.
98
- --nova-font-section-title: 700 13.5px/1.2 var(--font-family);
104
+ --nova-font-section-title: 700 13.5px/1.2 var(--font-family-display);
99
105
  --nova-tracking-section-title: -0.01em;
100
106
 
101
107
  // ── Workhorse text — repeated everywhere, snapped where fidelity allows ───
@@ -159,6 +165,39 @@
159
165
  --nova-tracking-timestamp: normal;
160
166
  }
161
167
 
168
+ // ── RTL: tracking off, everywhere, once ─────────────────────────────────────
169
+ // Latin display type is tightened (down to -0.022em on the page headline) because
170
+ // negative tracking is how a large Latin heading stops looking loose. Applied to
171
+ // Arabic the same declaration is not a tightening at all: `letter-spacing` inserts
172
+ // its value BETWEEN glyphs, and Arabic glyphs are joined — so any non-zero value,
173
+ // negative or positive, forces the shaper to break the cursive connections and the
174
+ // word visibly falls apart. That is why the eyebrow's POSITIVE 0.03em is reset here
175
+ // too; "reduce the negative ones" would be the wrong reading of the rule.
176
+ // ·
177
+ // Every role is listed, including the ones already `normal`, for the same reason the
178
+ // block above gives every role a tracking token at all: a caller writes one pattern
179
+ // regardless of role, and a reader can see at a glance that the reset is total
180
+ // rather than having to diff two lists. `nova-type.spec.ts` pins that completeness.
181
+ // ·
182
+ // Unlayered, like the `:root` it mirrors — see the RTL note in `_tokens.scss` for
183
+ // why a layered copy of this would silently never apply.
184
+ html[dir='rtl'] {
185
+ --nova-tracking-page-headline: normal;
186
+ --nova-tracking-hero-greeting: normal;
187
+ --nova-tracking-hero-card-name: normal;
188
+ --nova-tracking-drawer-title: normal;
189
+ --nova-tracking-dialog-title: normal;
190
+ --nova-tracking-header-context: normal;
191
+ --nova-tracking-section-title: normal;
192
+ --nova-tracking-menu-item: normal;
193
+ --nova-tracking-body: normal;
194
+ --nova-tracking-row-label: normal;
195
+ --nova-tracking-field-label: normal;
196
+ --nova-tracking-tooltip: normal;
197
+ --nova-tracking-eyebrow: normal;
198
+ --nova-tracking-timestamp: normal;
199
+ }
200
+
162
201
  // ── Rule A: tabular numerals (skill §7, mechanical) ─────────────────────────
163
202
  // "font-variant-numeric: tabular-nums on every count, timestamp and pagination
164
203
  // number." `_app-surface-utilities.scss` already ships `.mono` bundling this
@@ -0,0 +1,32 @@
1
+ // ─── 'SF Arabic' — the Arabic face, declared ONCE for the whole estate ───────
2
+ // ·
3
+ // This is a `local()`-ONLY @font-face: it names faces already installed on the
4
+ // device and downloads nothing. That is what keeps it inside the platform's
5
+ // "self-hosted only, no CDN" rule — the rule bans *fetching* a font from
6
+ // someone else's server, and this fetches nothing at all.
7
+ // ·
8
+ // It lives in the design system rather than in the shell's `src/styles/` on
9
+ // purpose: External App remotes (Circles, Thoughts, PPM) consume `fly-theme` and
10
+ // would otherwise inherit `--font-family`'s reference to a family no @font-face
11
+ // in their document ever declared — the exact dev/prod skew the Kufi partial's
12
+ // header records for `page-header.component.scss`. `_fly-theme.scss` is the one
13
+ // entry point that `@use`s this, so the block is emitted exactly once.
14
+ // ·
15
+ // WHERE IT DOES NOT RESOLVE — say it plainly, because it is half the estate:
16
+ // 'SF Arabic' and 'Geeza Pro' ship with macOS / iOS. On Windows and Linux none
17
+ // of the three `local()` names match, the @font-face contributes nothing, and
18
+ // Arabic falls through the stack to `system-ui` (Segoe UI on Windows). That is a
19
+ // deliberate consequence of the UI-owner's 2026-08-18 stack ruling, which retired
20
+ // the self-hosted Noto Kufi Arabic that had covered every OS equally.
21
+ // ·
22
+ // `font-weight: 400 700` declares a RANGE over the local faces so a 600 title
23
+ // and a 400 body both resolve here instead of falling out of the family at the
24
+ // first weight the single named face cannot supply.
25
+
26
+ @font-face {
27
+ font-family: 'SF Arabic';
28
+ src: local('SF Arabic'), local('SFArabic-Regular'), local('Geeza Pro');
29
+ font-weight: 400 700;
30
+ font-style: normal;
31
+ font-display: swap;
32
+ }
package/scss/_tokens.scss CHANGED
@@ -22,27 +22,42 @@
22
22
  --status-pending: var(--system-yellow);
23
23
 
24
24
  // ── FlyOS Typography ──
25
- // Geist is the primary face (self-hosted, see styles/_geist-fonts.scss). Inter and the
26
- // system stack are the fallbacks. 'Noto Kufi Arabic' (self-hosted, styles/_kufi-fonts.scss,
27
- // post-G7) carries the Arabic script for ar/ur: its @font-face is unicode-range-scoped to
28
- // Arabic blocks, so per-GLYPH fallback gives Kufi to Arabic text and never touches Latin —
29
- // Geist stays the Latin face in every locale, and no per-locale restatement is needed.
30
- // In a consumer without that @font-face the name simply skips (pre-Kufi behaviour: system
31
- // Arabic face) External Apps declare their own faces per the estate convention.
32
- // ---
33
- // 2.0.0 removed 'SF Pro Display' / 'SF Pro' D8 (no SF fonts, ever, for licensing). The
34
- // repo had been SHIPPING them: 9 self-hosted woff2 faces, preloaded from index.html.
35
- // Removing them changed NOTHING for ar/ur: SF Pro carries no unicode-range and no Arabic
36
- // glyphs, so before Kufi those scripts always resolved to the system Arabic face.
37
- --font-family: 'Geist', 'Inter', 'Noto Kufi Arabic', -apple-system, blinkmacsystemfont, 'Segoe UI', sans-serif;
25
+ // ONE typographic system, expressed as TWO stacks: Text carries body/UI, Display
26
+ // carries titles. The split rides `font-family` (UI-owner ruling 2026-08-18) — it
27
+ // used to ride weight + letter-spacing alone, because D8 forbade shipping a second
28
+ // face. Both stacks are resolved from faces already ON the device: nothing is
29
+ // downloaded, so "no CDN / self-hosted only" is satisfied by shipping no font file
30
+ // at all rather than by vendoring one. That is what makes naming SF here legal
31
+ // where the 2.0.0 removal of the SHIPPED SF Pro woff2 set was not.
32
+ // ·
33
+ // These two are the ONLY family declarations in the platform. Everything inherits
34
+ // from the root rule (`styles/_reset.scss`); the title stack reaches type through
35
+ // the `--font-*` title roles below and the `--nova-font-*` roles in `_nova-type.scss`.
36
+ // A component must never restate a stack inheritance or a role token, never both.
37
+ // ·
38
+ // RTL: the `html[dir='rtl']` block after this one re-points BOTH names at their
39
+ // Arabic-leading variants. It sits unlayered, in this same cascade tier, because a
40
+ // layered override would lose to this `:root` — unlayered beats every `@layer`.
41
+ // 'SF Arabic' is declared `local()`-only in `styles/_sf-arabic-font.scss`; on a
42
+ // device without it (Windows, Linux) the stack falls through to `system-ui`.
43
+ --font-family: -apple-system, blinkmacsystemfont, 'SF Pro Text', system-ui, sans-serif;
44
+ --font-family-display: -apple-system, blinkmacsystemfont, 'SF Pro Display', system-ui, sans-serif;
45
+
46
+ // Monospace survives the one-system rule because it is a FUNCTION, not a voice:
47
+ // code blocks, diffs, ids and log output are unreadable proportionally spaced.
48
+ // 'Geist Mono' stays self-hosted (`styles/_geist-fonts.scss`) — the Geist SANS half
49
+ // is what the ruling above retires.
38
50
  --font-family-mono: 'Geist Mono', ui-monospace, 'SF Mono', menlo, monospace;
39
- --font-xl-title1: 700 48px/56px var(--font-family);
40
- --font-xl-title2: 700 38px/46px var(--font-family);
41
- --font-large-title: 700 29px/38px var(--font-family);
42
- --font-title1: 700 24px/32px var(--font-family);
43
- --font-title2: 700 22px/28px var(--font-family);
44
- --font-title3: 700 19px/24px var(--font-family);
45
- --font-headline: 700 17px/22px var(--font-family);
51
+
52
+ // Title roles → Display. Body roles → Text. The boundary is `--font-headline`:
53
+ // everything at or above it is a heading treatment, everything below is copy.
54
+ --font-xl-title1: 700 48px/56px var(--font-family-display);
55
+ --font-xl-title2: 700 38px/46px var(--font-family-display);
56
+ --font-large-title: 700 29px/38px var(--font-family-display);
57
+ --font-title1: 700 24px/32px var(--font-family-display);
58
+ --font-title2: 700 22px/28px var(--font-family-display);
59
+ --font-title3: 700 19px/24px var(--font-family-display);
60
+ --font-headline: 700 17px/22px var(--font-family-display);
46
61
  --font-body: 510 17px/22px var(--font-family);
47
62
  --font-callout: 590 15px/20px var(--font-family);
48
63
  --font-subheadline: 400 15px/20px var(--font-family);
@@ -126,3 +141,24 @@
126
141
  --link-height: 26px;
127
142
  --link-font-size: 13px;
128
143
  }
144
+
145
+ // ─── RTL: the whole typographic system, swapped once ─────────────────────────
146
+ // The ONLY direction-aware font rule in the platform. `dir="rtl"` is set on <html>
147
+ // by `I18nService` and nowhere else, so re-pointing the two family names here
148
+ // re-faces every surface — no per-component `[dir=rtl]` font rule exists or may be
149
+ // added.
150
+ // ·
151
+ // UNLAYERED, deliberately, and this is the trap that makes it non-obvious: the shell
152
+ // keeps its RTL overrides in `styles-rtl.scss` inside `@layer shell`, and a custom
153
+ // property redeclared there would LOSE to the `:root` block above — under Cascade
154
+ // Level 5 an unlayered declaration beats every layered one regardless of specificity.
155
+ // So the swap has to live in the token layer, beside the values it swaps.
156
+ // `styles.scss` already sets the RTL precedent for `--shell-screen-slide` the same way.
157
+ // ·
158
+ // Tracking is neutralised rather than merely reduced: negative letter-spacing pulls
159
+ // Arabic glyphs out of their joined forms, which is a legibility failure, not a
160
+ // tightening. `_nova-type.scss` carries the companion `--nova-tracking-*` reset.
161
+ html[dir='rtl'] {
162
+ --font-family: 'SF Arabic', -apple-system, blinkmacsystemfont, 'SF Pro Text', system-ui, sans-serif;
163
+ --font-family-display: 'SF Arabic', -apple-system, blinkmacsystemfont, 'SF Pro Display', system-ui, sans-serif;
164
+ }