@uniflowed/stylex 0.1.0 → 0.3.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/index.js CHANGED
@@ -21,7 +21,7 @@
21
21
  //
22
22
  // 1. **A token set that already exists.** `@uniflowed/stylex/tokens.stylex.js`
23
23
  // is a real `stylex.defineVars` module — colour roles, a type scale,
24
- // spacing, radii, elevation and motion — so a project has a coherent palette
24
+ // spacing, radii and motion — so a project has a coherent palette
25
25
  // without authoring one, and gets it as `:root` custom properties the build
26
26
  // inlined rather than as anything computed in a browser.
27
27
  // 2. **A base layer over those tokens.** `@uniflowed/stylex/preset` is
@@ -67,14 +67,17 @@
67
67
  //
68
68
  // # Where the token values come from
69
69
  //
70
- // `@uniflowed/brand` owns uf's visual identity — the palette, the type,
71
- // spacing and radius scales — and it stays there. It cannot own the token
72
- // module: a StyleX token's name is computed by the compiler from the binding
73
- // and key it was declared under, `defineVars` accepts only literals, and
74
- // brand's `--uf-*` names are hand-written for a different consumer. So brand
75
- // holds the identity values and `./tokens.stylex.js` is their StyleX-shaped
76
- // projection into semantic roles — `accent`, `ink`, `canvas` — which is the
77
- // layer a design system needs and an identity does not have.
70
+ // `@uniflowed/brand` owns uf's visual identity — the logo, its spectrum, the
71
+ // type and spacing scales — and it stays there. The interface borrows brand's
72
+ // type and spacing, and chooses its own colours and corners: a UI dressed in
73
+ // the logo's cyan-to-magenta spectrum and round corners looks generated, so
74
+ // `./tokens.stylex.js` holds a neutral-first palette of its own
75
+ // (`docs/ui-visual-language.md` says why). Brand could not own the token
76
+ // module anyway: a StyleX token's name is computed by the compiler from the
77
+ // binding and key it was declared under, and `defineVars` accepts only
78
+ // literals. So `./tokens.stylex.js` is where the semantic roles — `accent`,
79
+ // `ink`, `canvas` — get their values, which is the layer a design system needs
80
+ // and an identity does not have.
78
81
  //
79
82
  // # Readiness
80
83
  //
@@ -129,13 +132,22 @@ export type ThemeOverrides<Tokens extends { readonly [string]: ThemeValue }> = P
129
132
  /**
130
133
  * Declare a set of style namespaces.
131
134
  *
135
+ * Typed as what the compiler hands back, not what was written: every key of
136
+ * `styles` becomes a {@link CompiledStyle}, a map of property to class name
137
+ * marked with `$$css`. Typing the result as the input (`T`) said a namespace
138
+ * held `{ color: "black" }` when at run time it holds `{ $$css: true, color:
139
+ * "x1e2nbdu" }`, so code reading the compiled marker, and `props`'s own
140
+ * tests, were type errors against the truth.
141
+ *
132
142
  * Never runs. `uf transform` replaces the whole call with the object it
133
143
  * computed, so reaching this means the module was loaded without going through
134
144
  * uf — a bundler configured by hand, a plain `node` invocation — and the styles
135
145
  * it declares are in no stylesheet. Throwing says so; returning the input would
136
146
  * render an application with no styles and no explanation.
137
147
  */
138
- export function create<T extends { readonly [string]: mixed }>(styles: T): T {
148
+ export function create<T extends { readonly [string]: mixed }>(
149
+ styles: T,
150
+ ): { readonly [Key in keyof T]: CompiledStyle } {
139
151
  return nativeRuntimeRequired(MODULE, "stylex.create");
140
152
  }
141
153
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/stylex",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Flow declarations for @uniflowed/stylex, part of the Unified Toolchain for Flow.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,7 +23,7 @@
23
23
  "!*.test.js"
24
24
  ],
25
25
  "dependencies": {
26
- "@uniflowed/core": "0.1.0"
26
+ "@uniflowed/core": "0.3.0"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "react-native": ">=0.86.0"
package/preset.js CHANGED
@@ -20,9 +20,9 @@
20
20
  //
21
21
  // It is the only module in the package that has opinions. `props` is a merge,
22
22
  // the tokens are names, the themes are values; this is where uf says a card has
23
- // a 16px radius. A project that wants its own look drops this import and keeps
24
- // everything else, and a bundler drops the rules with it because nothing else
25
- // references them.
23
+ // a 6px radius and a 1px border rather than a shadow. A project that wants its
24
+ // own look drops this import and keeps everything else, and a bundler drops
25
+ // the rules with it because nothing else references them.
26
26
  //
27
27
  // # Why functions rather than exported namespaces
28
28
  //
@@ -84,13 +84,14 @@ const surfaces = stylex.create({
84
84
  borderColor: ufTokens.border,
85
85
  borderRadius: ufTokens.radiusLg,
86
86
  padding: ufTokens.space6,
87
- boxShadow: ufTokens.shadowCard,
88
87
  },
89
88
  panel: {
90
89
  backgroundColor: ufTokens.surface,
91
- borderRadius: ufTokens.radiusXl,
90
+ borderWidth: "1px",
91
+ borderStyle: "solid",
92
+ borderColor: ufTokens.border,
93
+ borderRadius: ufTokens.radiusLg,
92
94
  padding: ufTokens.space6,
93
- boxShadow: ufTokens.shadowPanel,
94
95
  },
95
96
  sunken: {
96
97
  backgroundColor: ufTokens.sunken,
@@ -131,7 +132,20 @@ const buttons = stylex.create({
131
132
  borderStyle: "solid",
132
133
  borderColor: "transparent",
133
134
  cursor: "pointer",
134
- transitionProperty: "background-color, border-color, color",
135
+ // Press: the button gives a little under the pointer, to 97% of its size
136
+ // in `durationFast`, and comes back on release without passing 1. Under
137
+ // reduced motion it does not move; only the colours still change.
138
+ transform: {
139
+ default: "none",
140
+ ":active": "scale(0.97)",
141
+ "@media (prefers-reduced-motion: reduce)": "none",
142
+ },
143
+ // The focus ring draws itself outward from the edge (`outline-width`
144
+ // 0 → 2px) rather than blinking on, in `durationFast`.
145
+ transitionProperty: {
146
+ default: "background-color, border-color, color, outline-width, transform",
147
+ "@media (prefers-reduced-motion: reduce)": "background-color, border-color, color",
148
+ },
135
149
  transitionDuration: ufTokens.durationFast,
136
150
  transitionTimingFunction: ufTokens.easing,
137
151
  // The ring is drawn only for a keyboard focus, which is the whole reason
@@ -191,7 +205,7 @@ const fields = stylex.create({
191
205
  borderWidth: "1px",
192
206
  borderStyle: "solid",
193
207
  borderColor: ufTokens.border,
194
- borderRadius: ufTokens.radiusSm,
208
+ borderRadius: ufTokens.radiusMd,
195
209
  paddingBlock: ufTokens.space2,
196
210
  paddingInline: ufTokens.space3,
197
211
  outlineWidth: { default: "0", ":focus-visible": "2px" },
@@ -226,9 +240,11 @@ const overlays = stylex.create({
226
240
  backgroundColor: ufTokens.surface,
227
241
  color: ufTokens.ink,
228
242
  fontFamily: ufTokens.fontSans,
229
- borderRadius: ufTokens.radiusXl,
243
+ borderWidth: "1px",
244
+ borderStyle: "solid",
245
+ borderColor: ufTokens.border,
246
+ borderRadius: ufTokens.radiusLg,
230
247
  padding: ufTokens.space6,
231
- boxShadow: ufTokens.shadowPanel,
232
248
  },
233
249
  });
234
250
 
@@ -243,7 +259,6 @@ const menus = stylex.create({
243
259
  borderStyle: "solid",
244
260
  borderColor: ufTokens.border,
245
261
  borderRadius: ufTokens.radiusMd,
246
- boxShadow: ufTokens.shadowPanel,
247
262
  },
248
263
  item: {
249
264
  display: "flex",
@@ -314,7 +329,12 @@ const controls = stylex.create({
314
329
  backgroundColor: ufTokens.surface,
315
330
  color: ufTokens.accentInk,
316
331
  cursor: "pointer",
317
- transitionProperty: "background-color, border-color",
332
+ // Colours, and the focus ring drawing outward; only the colours stay
333
+ // under reduced motion.
334
+ transitionProperty: {
335
+ default: "background-color, border-color, outline-width",
336
+ "@media (prefers-reduced-motion: reduce)": "background-color, border-color",
337
+ },
318
338
  transitionDuration: ufTokens.durationFast,
319
339
  transitionTimingFunction: ufTokens.easing,
320
340
  outlineWidth: { default: "0", ":focus-visible": "2px" },
@@ -325,7 +345,7 @@ const controls = stylex.create({
325
345
  box: {
326
346
  width: ufTokens.sizeControl,
327
347
  height: ufTokens.sizeControl,
328
- borderRadius: "5px",
348
+ borderRadius: ufTokens.radiusSm,
329
349
  },
330
350
  // A switch's track is the one piece of geometry here that is not a token:
331
351
  // nothing but a switch reads it, and a theme that changed it would leave the
package/theme.js CHANGED
@@ -62,25 +62,23 @@ import { ufTokens } from "./tokens.stylex.js";
62
62
  * exactly one `@media (prefers-color-scheme: dark)` block.
63
63
  */
64
64
  export const ufAutoTheme = stylex.createTheme(ufTokens, {
65
- canvas: { "@media (prefers-color-scheme: dark)": "#0b1220" },
66
- sunken: { "@media (prefers-color-scheme: dark)": "#0f1726" },
67
- surface: { "@media (prefers-color-scheme: dark)": "#131c2e" },
68
- surfaceHover: { "@media (prefers-color-scheme: dark)": "#1b2540" },
69
- border: { "@media (prefers-color-scheme: dark)": "#26324a" },
70
- ink: { "@media (prefers-color-scheme: dark)": "#e8eefc" },
71
- muted: { "@media (prefers-color-scheme: dark)": "#9fb0cc" },
72
- accent: { "@media (prefers-color-scheme: dark)": "#8f8bff" },
73
- accentHover: { "@media (prefers-color-scheme: dark)": "#a49fff" },
74
- accentInk: { "@media (prefers-color-scheme: dark)": "#0b1220" },
75
- accentSoft: { "@media (prefers-color-scheme: dark)": "#1e2547" },
76
- danger: { "@media (prefers-color-scheme: dark)": "#ff6b5e" },
77
- dangerHover: { "@media (prefers-color-scheme: dark)": "#ff8378" },
78
- dangerInk: { "@media (prefers-color-scheme: dark)": "#2a0b08" },
79
- dangerSoft: { "@media (prefers-color-scheme: dark)": "#2a1512" },
80
- focus: { "@media (prefers-color-scheme: dark)": "#7fb0ff" },
81
- scrim: { "@media (prefers-color-scheme: dark)": "rgba(2, 6, 16, 0.62)" },
82
- shadowCard: { "@media (prefers-color-scheme: dark)": "0 1px 2px rgba(0, 0, 0, 0.40)" },
83
- shadowPanel: { "@media (prefers-color-scheme: dark)": "0 16px 48px rgba(0, 0, 0, 0.55)" },
65
+ canvas: { "@media (prefers-color-scheme: dark)": "#141413" },
66
+ sunken: { "@media (prefers-color-scheme: dark)": "#0f0f0e" },
67
+ surface: { "@media (prefers-color-scheme: dark)": "#1c1c1a" },
68
+ surfaceHover: { "@media (prefers-color-scheme: dark)": "#262623" },
69
+ border: { "@media (prefers-color-scheme: dark)": "#363632" },
70
+ ink: { "@media (prefers-color-scheme: dark)": "#ecece7" },
71
+ muted: { "@media (prefers-color-scheme: dark)": "#a5a59e" },
72
+ accent: { "@media (prefers-color-scheme: dark)": "#8eaee8" },
73
+ accentHover: { "@media (prefers-color-scheme: dark)": "#a8c1ee" },
74
+ accentInk: { "@media (prefers-color-scheme: dark)": "#101318" },
75
+ accentSoft: { "@media (prefers-color-scheme: dark)": "#1f2838" },
76
+ danger: { "@media (prefers-color-scheme: dark)": "#ee8a80" },
77
+ dangerHover: { "@media (prefers-color-scheme: dark)": "#f3a49c" },
78
+ dangerInk: { "@media (prefers-color-scheme: dark)": "#1d0f0d" },
79
+ dangerSoft: { "@media (prefers-color-scheme: dark)": "#35201d" },
80
+ focus: { "@media (prefers-color-scheme: dark)": "#8eaee8" },
81
+ scrim: { "@media (prefers-color-scheme: dark)": "rgba(0, 0, 0, 0.6)" },
84
82
  });
85
83
 
86
84
  /**
@@ -90,23 +88,21 @@ export const ufAutoTheme = stylex.createTheme(ufTokens, {
90
88
  * preference a reader chose.
91
89
  */
92
90
  export const ufDarkTheme = stylex.createTheme(ufTokens, {
93
- canvas: "#0b1220",
94
- sunken: "#0f1726",
95
- surface: "#131c2e",
96
- surfaceHover: "#1b2540",
97
- border: "#26324a",
98
- ink: "#e8eefc",
99
- muted: "#9fb0cc",
100
- accent: "#8f8bff",
101
- accentHover: "#a49fff",
102
- accentInk: "#0b1220",
103
- accentSoft: "#1e2547",
104
- danger: "#ff6b5e",
105
- dangerHover: "#ff8378",
106
- dangerInk: "#2a0b08",
107
- dangerSoft: "#2a1512",
108
- focus: "#7fb0ff",
109
- scrim: "rgba(2, 6, 16, 0.62)",
110
- shadowCard: "0 1px 2px rgba(0, 0, 0, 0.40)",
111
- shadowPanel: "0 16px 48px rgba(0, 0, 0, 0.55)",
91
+ canvas: "#141413",
92
+ sunken: "#0f0f0e",
93
+ surface: "#1c1c1a",
94
+ surfaceHover: "#262623",
95
+ border: "#363632",
96
+ ink: "#ecece7",
97
+ muted: "#a5a59e",
98
+ accent: "#8eaee8",
99
+ accentHover: "#a8c1ee",
100
+ accentInk: "#101318",
101
+ accentSoft: "#1f2838",
102
+ danger: "#ee8a80",
103
+ dangerHover: "#f3a49c",
104
+ dangerInk: "#1d0f0d",
105
+ dangerSoft: "#35201d",
106
+ focus: "#8eaee8",
107
+ scrim: "rgba(0, 0, 0, 0.6)",
112
108
  });
package/tokens.stylex.js CHANGED
@@ -11,18 +11,37 @@
11
11
  // # Roles, not colours
12
12
  //
13
13
  // The names are what a token is *for*, never what it looks like. `accent` is
14
- // the colour a primary action wears; it is indigo today and a theme can make it
15
- // anything, and no rule in `preset.js` has to change when it does. A token
16
- // named `indigo500` would have made the preset unthemeable the moment someone
17
- // wanted a green product.
14
+ // the colour a primary action wears; it is a quiet cobalt today and a theme
15
+ // can make it anything, and no rule in `preset.js` has to change when it does.
16
+ // A token named `blue600` would have made the preset unthemeable the moment
17
+ // someone wanted a green product.
18
18
  //
19
- // The values are uf's identity, and `@uniflowed/brand` is where that identity
20
- // is decided: `accent`, `ink`, `canvas` and `muted` are brand's Indigo, Ink,
21
- // Mist and Slate, and the type, spacing and radius steps are brand's scales.
22
- // Brand cannot hold the token module itself — a StyleX token's name is computed
23
- // by the compiler, and `defineVars` takes literals, not an imported array — so
24
- // this module is brand's projection into the semantic roles a design system
25
- // needs and an identity does not have.
19
+ // # The palette
20
+ //
21
+ // `docs/ui-visual-language.md` explains it; in short: the interface is
22
+ // neutral first — warm greys with no blue cast, near-black ink on off-white
23
+ // paper — and colour is spent in one place, a single accent for what is
24
+ // selected, current or primary. Danger is a brick red rather than a signal
25
+ // red. Dark is designed as its own palette (charcoal, not navy; a lighter,
26
+ // desaturated accent that does not glow), not the light one inverted.
27
+ //
28
+ // The type and spacing steps are `@uniflowed/brand`'s scales. The colours and
29
+ // the corners are not: brand's cyan-to-magenta spectrum and its round corners
30
+ // are uf's logo and marketing identity, and an interface built from them looks
31
+ // like every other generated one, so this module's values are its own. Brand
32
+ // cannot hold the token module itself anyway — a StyleX token's name is
33
+ // computed by the compiler, and `defineVars` takes literals, not an imported
34
+ // array.
35
+ //
36
+ // # No shadows, no gradients
37
+ //
38
+ // The defaults draw depth with a rule and a change of surface, and nothing
39
+ // else. A shadow is the first thing a theme has to fight when the product's
40
+ // look is flat, and a gradient is a decision about a brand this module does
41
+ // not have, so neither is a token and neither appears in a default style.
42
+ // `crates/uf_stylex/src/tests/defaults.rs` compiles every default style —
43
+ // this module, `preset.js`, `theme.js` and every file in `registry/ui/` — and
44
+ // fails on a `box-shadow`, a `text-shadow`, a `drop-shadow()` or a gradient.
26
45
  //
27
46
  // # What is not a token
28
47
  //
@@ -45,31 +64,31 @@ import { stylex } from "@uniflowed/stylex";
45
64
  // `ThemeOverrides<typeof ufTokens>` reject a token this module does not declare.
46
65
  export const ufTokens = stylex.defineVars({
47
66
  // Surfaces, from furthest back to nearest front.
48
- canvas: "#f8fafc",
49
- sunken: "#eef2f7",
67
+ canvas: "#f6f6f4",
68
+ sunken: "#eeeeeb",
50
69
  surface: "#ffffff",
51
- surfaceHover: "#f1f5f9",
52
- border: "#dbe3ec",
70
+ surfaceHover: "#f1f1ee",
71
+ border: "#d9d9d4",
53
72
 
54
73
  // Text.
55
- ink: "#0f172a",
56
- muted: "#475569",
74
+ ink: "#1b1b19",
75
+ muted: "#5c5c57",
57
76
 
58
77
  // The colour a primary action wears, and what is legible on it.
59
- accent: "#5c49ff",
60
- accentHover: "#4a37f0",
78
+ accent: "#2b58b5",
79
+ accentHover: "#234996",
61
80
  accentInk: "#ffffff",
62
- accentSoft: "#eeecff",
81
+ accentSoft: "#e8eef9",
63
82
 
64
83
  // The colour a destructive action wears.
65
- danger: "#b42318",
66
- dangerHover: "#9a1c12",
84
+ danger: "#b1271d",
85
+ dangerHover: "#931f17",
67
86
  dangerInk: "#ffffff",
68
- dangerSoft: "#fef3f2",
87
+ dangerSoft: "#fbedeb",
69
88
 
70
89
  // Focus ring, and the wash behind a modal surface.
71
- focus: "#2677ff",
72
- scrim: "rgba(15, 23, 42, 0.48)",
90
+ focus: "#2b58b5",
91
+ scrim: "rgba(27, 27, 25, 0.45)",
73
92
 
74
93
  // Type.
75
94
  fontSans: "ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
@@ -95,20 +114,77 @@ export const ufTokens = stylex.defineVars({
95
114
  space8: "32px",
96
115
  space12: "48px",
97
116
 
98
- // Shape.
99
- radiusSm: "8px",
100
- radiusMd: "12px",
101
- radiusLg: "16px",
102
- radiusXl: "24px",
117
+ // Shape. Three small steps and a pill, and nothing rounder: a control is
118
+ // 2px, a button, field group or popover 4px, and the largest surface — a
119
+ // card, a dialog — 6px. `radiusPill` is for the few things that are round
120
+ // by what they are, such as a switch's track or a radio's dot, and not a
121
+ // way to soften a rectangle.
122
+ radiusSm: "2px",
123
+ radiusMd: "4px",
124
+ radiusLg: "6px",
103
125
  radiusPill: "999px",
104
126
 
105
127
  // The size of a control a finger or a pointer aims at.
106
128
  sizeControl: "18px",
107
129
 
108
- // Elevation and motion.
109
- shadowCard: "0 1px 2px rgba(15, 23, 42, 0.06), 0 1px 3px rgba(15, 23, 42, 0.10)",
110
- shadowPanel: "0 16px 48px rgba(15, 23, 42, 0.24)",
130
+ // There is no elevation: a surface in front of another is told apart by a
131
+ // 1px `border` and its own background, never by a shadow.
132
+
133
+ // # Motion
134
+ //
135
+ // Three durations and three easings, and every default transition is one of
136
+ // each. `docs/ui-visual-language.md` has the table of what moves how; the
137
+ // values are argued here, where a theme that wants to change them will look.
138
+ //
139
+ // **Durations, by how far the eye has to follow.**
140
+ //
141
+ // * `durationFast`, 120ms: a change *in place* — a colour, a pressed
142
+ // button giving way, a focus ring drawing itself, a chevron turning.
143
+ // Nothing travels, so anything longer reads as lag between the press
144
+ // and the answer; anything under ~100ms is not seen at all, which is
145
+ // how #1414's defaults came to look like they had no motion.
146
+ // * `durationBase`, 200ms: something small *arriving* or *travelling* — a
147
+ // popover, menu or listbox leaving its trigger, a switch's thumb, a
148
+ // check being drawn. Long enough to see where it came from, short enough
149
+ // that a reader who opens a menu to act in it is never waiting for it.
150
+ // * `durationSlow`, 280ms: something *large* — a dialog, a sheet, a
151
+ // drawer, a toast, a progress bar's fill. A surface that covers a third
152
+ // of the screen and moves as fast as a menu looks thrown. It stops short
153
+ // of the ~300ms where an interface starts to feel like it is performing.
154
+ //
155
+ // **Easings, by where the motion starts and ends.** All three are
156
+ // cubic-béziers with both y values inside 0..1, so nothing overshoots its
157
+ // destination and nothing bounces; `defaults.rs` holds them to that.
158
+ //
159
+ // * `easing`, `cubic-bezier(0.2, 0, 0, 1)`: the standard curve, for
160
+ // something moving between two places it rests at — a switch's thumb, a
161
+ // progress fill, a drawer between snap points, a colour. It eases out of
162
+ // rest a little and settles for a long time, so it never stops abruptly.
163
+ // * `easingEnter`, `cubic-bezier(0, 0, 0.2, 1)`: a decelerate, for
164
+ // something *arriving*. It leaves at full speed and spends its second
165
+ // half settling: half the distance is covered by a fifth of the time,
166
+ // so the content is legible almost at once, while the settle is long
167
+ // enough to be seen. A stronger curve (Material's emphasised
168
+ // `0.05, 0.7, 0.1, 1`) was tried and rejected: it is 78% there by a
169
+ // fifth of the time, which on a 200ms popover is the "almost no motion"
170
+ // this replaces.
171
+ // * `easingExit`, `cubic-bezier(0.4, 0, 1, 1)`: an accelerate, for
172
+ // something *leaving*. It starts slowly, so the eye registers that it is
173
+ // going, then gets out of the way at full speed. Exits pair it with a
174
+ // shorter duration than the matching enter, because nobody watches a
175
+ // thing leave. (Exit transitions need the behaviour layer to keep a
176
+ // closing part mounted until they finish; until it does, this token is
177
+ // declared and not yet read — see `docs/ui-visual-language.md`.)
178
+ //
179
+ // A transition names the properties it moves, never `all`. Under
180
+ // `prefers-reduced-motion: reduce` nothing travels, grows or is drawn:
181
+ // either the duration is `0s`, or only opacity and colour still transition,
182
+ // so an overlay still fades rather than cutting. `defaults.rs` checks every
183
+ // default style for each of these.
111
184
  durationFast: "120ms",
112
185
  durationBase: "200ms",
186
+ durationSlow: "280ms",
113
187
  easing: "cubic-bezier(0.2, 0, 0, 1)",
188
+ easingEnter: "cubic-bezier(0, 0, 0.2, 1)",
189
+ easingExit: "cubic-bezier(0.4, 0, 1, 1)",
114
190
  });