@uniflowed/stylex 0.0.0-alpha.10
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 +158 -0
- package/package.json +26 -0
- package/preset.js +486 -0
- package/props.js +147 -0
- package/theme.js +112 -0
- package/tokens.stylex.js +114 -0
package/index.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/stylex`: uf's style engine, and the preset it ships.
|
|
4
|
+
//
|
|
5
|
+
// Almost all of StyleX happens at compile time. `uf transform` rewrites every
|
|
6
|
+
// `stylex.create({ … })` into a plain object of class names, turns every
|
|
7
|
+
// `stylex.defineVars({ … })` into CSS custom properties, turns every
|
|
8
|
+
// `stylex.createTheme(tokens, { … })` into the classes that override them, and
|
|
9
|
+
// collects the rules into a stylesheet. By the time this module is loaded there
|
|
10
|
+
// are no style values left — only names.
|
|
11
|
+
//
|
|
12
|
+
// This module is the front door. The merge lives in `./props.js` beside its
|
|
13
|
+
// compile-time twin; the preset lives in `./tokens.stylex.js`, `./preset.js`
|
|
14
|
+
// and `./theme.js`. What is here is the merge re-exported, and the three
|
|
15
|
+
// functions that must never run.
|
|
16
|
+
//
|
|
17
|
+
// # What "preset" means here
|
|
18
|
+
//
|
|
19
|
+
// A framework's styling default is worth something only if it removes work
|
|
20
|
+
// without removing control, so uf's preset is three things and stops there:
|
|
21
|
+
//
|
|
22
|
+
// 1. **A token set that already exists.** `@uniflowed/stylex/tokens.stylex.js`
|
|
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
|
|
25
|
+
// without authoring one, and gets it as `:root` custom properties the build
|
|
26
|
+
// inlined rather than as anything computed in a browser.
|
|
27
|
+
// 2. **A base layer over those tokens.** `@uniflowed/stylex/preset` is
|
|
28
|
+
// `stylex.create` namespaces for the surfaces a real application has, behind
|
|
29
|
+
// small functions that answer with `{ className }`. A project gets a default
|
|
30
|
+
// look by spreading them and nothing else.
|
|
31
|
+
// 3. **A way to replace any of it.** `stylex.createTheme(ufTokens, { … })`
|
|
32
|
+
// compiles to one class per overridden token; put that class on an ancestor
|
|
33
|
+
// and every `var(--…)` beneath it resolves differently. `./theme.js` ships
|
|
34
|
+
// two, and a project writes its own the same way.
|
|
35
|
+
//
|
|
36
|
+
// It is deliberately *not*: a global reset (uf emits no rule a project did not
|
|
37
|
+
// ask for), a component library (`@uniflowed/ui` ships behaviour and no
|
|
38
|
+
// styles, and must keep doing so — nothing here appears in its types), or a
|
|
39
|
+
// configuration flag. The preset is reached by importing it, so a project that
|
|
40
|
+
// does not import it pays nothing, and one that imports half of it keeps half.
|
|
41
|
+
//
|
|
42
|
+
// # Where the token values come from
|
|
43
|
+
//
|
|
44
|
+
// `@uniflowed/brand` owns uf's visual identity — the palette, the type,
|
|
45
|
+
// spacing and radius scales — and it stays there. It cannot own the token
|
|
46
|
+
// module: a StyleX token's name is computed by the compiler from the binding
|
|
47
|
+
// and key it was declared under, `defineVars` accepts only literals, and
|
|
48
|
+
// brand's `--uf-*` names are hand-written for a different consumer. So brand
|
|
49
|
+
// holds the identity values and `./tokens.stylex.js` is their StyleX-shaped
|
|
50
|
+
// projection into semantic roles — `accent`, `ink`, `canvas` — which is the
|
|
51
|
+
// layer a design system needs and an identity does not have.
|
|
52
|
+
//
|
|
53
|
+
// # Readiness
|
|
54
|
+
//
|
|
55
|
+
// **Implemented.** `props`, `create`, `defineVars`, `createTheme`, pseudo-class
|
|
56
|
+
// and pseudo-element conditions, at-rule conditions (`@media`, `@supports`),
|
|
57
|
+
// shorthand-versus-longhand ordering, unit inference, the preset tokens, the
|
|
58
|
+
// base layer, and the shipped themes. Every one of them is compiled: the
|
|
59
|
+
// runtime half of this package is `props` and three functions that throw.
|
|
60
|
+
//
|
|
61
|
+
// **Experimental.** The preset's own visual choices. The token *names* are the
|
|
62
|
+
// contract and are meant to be stable; the values behind them are uf's opinion
|
|
63
|
+
// and may change within the alpha series. A project that wants them pinned
|
|
64
|
+
// should say so with a theme.
|
|
65
|
+
//
|
|
66
|
+
// **Not there.** `keyframes`, `firstThatWorks` and `positionTry`. Each needs a
|
|
67
|
+
// rule shape the sheet does not have — a named `@keyframes` block, several
|
|
68
|
+
// declarations of one property in one rule, and an `@position-try` block
|
|
69
|
+
// respectively — and the sheet's whole guarantee is that a rule is one class,
|
|
70
|
+
// one declaration, one state, ordered by what it is. Widening that is a change
|
|
71
|
+
// to the ordering model, not an addition to it, so none of the three is
|
|
72
|
+
// half-present: writing any of them is a compile error today, not a call that
|
|
73
|
+
// silently does nothing. A preset does not need them, which is why they are
|
|
74
|
+
// not in the way.
|
|
75
|
+
|
|
76
|
+
import { nativeRuntimeRequired } from "@uniflowed/core/native";
|
|
77
|
+
|
|
78
|
+
import type { CompiledStyle } from "./props.js";
|
|
79
|
+
import { props } from "./props.js";
|
|
80
|
+
|
|
81
|
+
const MODULE = "@uniflowed/stylex";
|
|
82
|
+
|
|
83
|
+
export type { CompiledClasses, CompiledStyle, StyleArgument, StyleProps } from "./props.js";
|
|
84
|
+
export { props } from "./props.js";
|
|
85
|
+
|
|
86
|
+
/** A value a token may hold, and therefore a value a theme may give it. */
|
|
87
|
+
export type ThemeValue = string | number;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* What `createTheme` accepts for one token set.
|
|
91
|
+
*
|
|
92
|
+
* Every key is optional — a theme that changes one colour is a theme — and no
|
|
93
|
+
* key outside the token set is allowed, so a typo is a Flow error rather than a
|
|
94
|
+
* custom property nothing reads. A value may carry states, which is how a theme
|
|
95
|
+
* follows `@media (prefers-color-scheme: dark)` without a second theme.
|
|
96
|
+
*/
|
|
97
|
+
export type ThemeOverrides<Tokens extends { readonly [string]: ThemeValue }> = Partial<{
|
|
98
|
+
[Key in keyof Tokens]: ThemeValue | { readonly [state: string]: ThemeValue },
|
|
99
|
+
}>;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Declare a set of style namespaces.
|
|
103
|
+
*
|
|
104
|
+
* Never runs. `uf transform` replaces the whole call with the object it
|
|
105
|
+
* computed, so reaching this means the module was loaded without going through
|
|
106
|
+
* uf — a bundler configured by hand, a plain `node` invocation — and the styles
|
|
107
|
+
* it declares are in no stylesheet. Throwing says so; returning the input would
|
|
108
|
+
* render an application with no styles and no explanation.
|
|
109
|
+
*/
|
|
110
|
+
export function create<T extends { readonly [string]: mixed }>(styles: T): T {
|
|
111
|
+
return nativeRuntimeRequired(MODULE, "stylex.create");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Declare design tokens, and hand back the `var(--…)` references to them.
|
|
116
|
+
*
|
|
117
|
+
* Compile-time, for the same reason as `create`.
|
|
118
|
+
*/
|
|
119
|
+
export function defineVars<T extends { readonly [string]: ThemeValue }>(tokens: T): T {
|
|
120
|
+
return nativeRuntimeRequired(MODULE, "stylex.defineVars");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Override a set of tokens, and hand back the class that applies the override.
|
|
125
|
+
*
|
|
126
|
+
* The result is a compiled namespace like any other, so it is applied by
|
|
127
|
+
* spreading it — `<div {...props(ufDarkTheme)}>` — and it composes: a second
|
|
128
|
+
* theme merged after the first replaces the tokens it names and leaves the rest
|
|
129
|
+
* alone, because the merge's unit is the property and a token is a property.
|
|
130
|
+
*
|
|
131
|
+
* Compile-time, for the same reason as `create`.
|
|
132
|
+
*/
|
|
133
|
+
export function createTheme<
|
|
134
|
+
Tokens extends { readonly [string]: ThemeValue },
|
|
135
|
+
Overrides extends ThemeOverrides<Tokens>,
|
|
136
|
+
>(tokens: Tokens, overrides: Overrides): CompiledStyle {
|
|
137
|
+
return nativeRuntimeRequired(MODULE, "stylex.createTheme");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The namespace form, so `stylex.create` and `stylex.props` read the way
|
|
142
|
+
* StyleX documents them.
|
|
143
|
+
*
|
|
144
|
+
* The named exports are the ones a bundler can drop individually; this object
|
|
145
|
+
* is for call sites that prefer the qualified spelling, and the compiler
|
|
146
|
+
* recognises both.
|
|
147
|
+
*/
|
|
148
|
+
export const stylex: {
|
|
149
|
+
readonly create: typeof create,
|
|
150
|
+
readonly props: typeof props,
|
|
151
|
+
readonly defineVars: typeof defineVars,
|
|
152
|
+
readonly createTheme: typeof createTheme,
|
|
153
|
+
} = {
|
|
154
|
+
create,
|
|
155
|
+
props,
|
|
156
|
+
defineVars,
|
|
157
|
+
createTheme,
|
|
158
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uniflowed/stylex",
|
|
3
|
+
"version": "0.0.0-alpha.10",
|
|
4
|
+
"description": "Flow declarations for @uniflowed/stylex, part of the Unified Toolchain for Flow.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/ubugeeei-prod/uf.git",
|
|
11
|
+
"directory": "packages/stylex"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./index.js",
|
|
15
|
+
"./props": "./props.js",
|
|
16
|
+
"./preset": "./preset.js",
|
|
17
|
+
"./theme": "./theme.js",
|
|
18
|
+
"./tokens.stylex.js": "./tokens.stylex.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"*.js"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@uniflowed/core": "0.0.0-alpha.10"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/preset.js
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/stylex/preset`: the default look, as props you spread.
|
|
4
|
+
//
|
|
5
|
+
// import { buttonStyles, cardStyles } from "@uniflowed/stylex/preset";
|
|
6
|
+
// import { Dialog } from "@uniflowed/ui";
|
|
7
|
+
//
|
|
8
|
+
// <div {...cardStyles()}>
|
|
9
|
+
// <button {...buttonStyles({ tone: "primary" })}>Save</button>
|
|
10
|
+
// </div>
|
|
11
|
+
//
|
|
12
|
+
// This is the second of the three things uf's preset is: a base layer over
|
|
13
|
+
// `./tokens.stylex.js`, so a project has a coherent default look without
|
|
14
|
+
// authoring a declaration. Every function answers with `{ className }` and
|
|
15
|
+
// nothing else, which is why a `@uniflowed/ui` primitive takes them without
|
|
16
|
+
// knowing they exist — `className` is a DOM prop, not a StyleX type, and
|
|
17
|
+
// keeping it that way is what lets the headless package stay headless.
|
|
18
|
+
//
|
|
19
|
+
// # Why this is a separate module
|
|
20
|
+
//
|
|
21
|
+
// It is the only module in the package that has opinions. `props` is a merge,
|
|
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.
|
|
26
|
+
//
|
|
27
|
+
// # Why functions rather than exported namespaces
|
|
28
|
+
//
|
|
29
|
+
// A namespace would make the caller responsible for the merge order —
|
|
30
|
+
// `props(button.base, button.primary, button.md)` — and getting that order
|
|
31
|
+
// wrong is silent. The functions own it, and they take the *variants* a caller
|
|
32
|
+
// actually has an opinion about. `match` makes each mapping exhaustive, so a
|
|
33
|
+
// tone added to the type without a rule is a Flow error rather than an
|
|
34
|
+
// unstyled button.
|
|
35
|
+
//
|
|
36
|
+
// # What the merge order here has to get right
|
|
37
|
+
//
|
|
38
|
+
// Every recipe puts `base` first and the variant after it, because the merge's
|
|
39
|
+
// unit is the property and the later argument wins. Two of them rely on that
|
|
40
|
+
// deliberately: `menuItemStyles({ active: true })` replaces the item's
|
|
41
|
+
// `backgroundColor` *including* its `:hover` state, which is what a selected
|
|
42
|
+
// row should do, and `buttonStyles({ disabled: true })` replaces the tone's
|
|
43
|
+
// cursor. Neither works if the arguments are swapped, and the compile-time
|
|
44
|
+
// model in `crates/uf_stylex/src/props.rs` is where that is pinned down.
|
|
45
|
+
|
|
46
|
+
// `stylex` comes from the package's own name because that is the specifier the
|
|
47
|
+
// compiler resolves a StyleX binding from; `props` and its types come from the
|
|
48
|
+
// module that owns them, which is the same module either way.
|
|
49
|
+
import { stylex } from "@uniflowed/stylex";
|
|
50
|
+
|
|
51
|
+
import type { StyleProps } from "./props.js";
|
|
52
|
+
import { props } from "./props.js";
|
|
53
|
+
import { ufTokens } from "./tokens.stylex.js";
|
|
54
|
+
|
|
55
|
+
/** How loud a control is, and therefore what it is for. */
|
|
56
|
+
export type Tone = "primary" | "neutral" | "ghost" | "danger";
|
|
57
|
+
|
|
58
|
+
/** The three sizes every control in the preset comes in. */
|
|
59
|
+
export type Size = "sm" | "md" | "lg";
|
|
60
|
+
|
|
61
|
+
/** How much a surface is lifted off the page. */
|
|
62
|
+
export type SurfaceKind = "page" | "card" | "panel" | "sunken";
|
|
63
|
+
|
|
64
|
+
/** Which of the type scale's steps a piece of text sits on. */
|
|
65
|
+
export type TextSize = "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
|
66
|
+
|
|
67
|
+
/** What a piece of text is: body copy, a secondary note, or an error. */
|
|
68
|
+
export type TextTone = "ink" | "muted" | "danger";
|
|
69
|
+
|
|
70
|
+
const surfaces = stylex.create({
|
|
71
|
+
base: {
|
|
72
|
+
color: ufTokens.ink,
|
|
73
|
+
fontFamily: ufTokens.fontSans,
|
|
74
|
+
fontSize: ufTokens.textMd,
|
|
75
|
+
lineHeight: ufTokens.leadingBase,
|
|
76
|
+
},
|
|
77
|
+
page: {
|
|
78
|
+
backgroundColor: ufTokens.canvas,
|
|
79
|
+
},
|
|
80
|
+
card: {
|
|
81
|
+
backgroundColor: ufTokens.surface,
|
|
82
|
+
borderWidth: "1px",
|
|
83
|
+
borderStyle: "solid",
|
|
84
|
+
borderColor: ufTokens.border,
|
|
85
|
+
borderRadius: ufTokens.radiusLg,
|
|
86
|
+
padding: ufTokens.space6,
|
|
87
|
+
boxShadow: ufTokens.shadowCard,
|
|
88
|
+
},
|
|
89
|
+
panel: {
|
|
90
|
+
backgroundColor: ufTokens.surface,
|
|
91
|
+
borderRadius: ufTokens.radiusXl,
|
|
92
|
+
padding: ufTokens.space6,
|
|
93
|
+
boxShadow: ufTokens.shadowPanel,
|
|
94
|
+
},
|
|
95
|
+
sunken: {
|
|
96
|
+
backgroundColor: ufTokens.sunken,
|
|
97
|
+
borderRadius: ufTokens.radiusMd,
|
|
98
|
+
padding: ufTokens.space4,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const texts = stylex.create({
|
|
103
|
+
base: {
|
|
104
|
+
fontFamily: ufTokens.fontSans,
|
|
105
|
+
lineHeight: ufTokens.leadingBase,
|
|
106
|
+
margin: 0,
|
|
107
|
+
},
|
|
108
|
+
xs: { fontSize: ufTokens.textXs },
|
|
109
|
+
sm: { fontSize: ufTokens.textSm },
|
|
110
|
+
md: { fontSize: ufTokens.textMd },
|
|
111
|
+
lg: { fontSize: ufTokens.textLg, lineHeight: ufTokens.leadingTight },
|
|
112
|
+
xl: { fontSize: ufTokens.textXl, lineHeight: ufTokens.leadingTight },
|
|
113
|
+
xxl: { fontSize: ufTokens.text2Xl, lineHeight: ufTokens.leadingTight },
|
|
114
|
+
ink: { color: ufTokens.ink },
|
|
115
|
+
muted: { color: ufTokens.muted },
|
|
116
|
+
danger: { color: ufTokens.danger },
|
|
117
|
+
strong: { fontWeight: ufTokens.weightBold },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const buttons = stylex.create({
|
|
121
|
+
base: {
|
|
122
|
+
display: "inline-flex",
|
|
123
|
+
alignItems: "center",
|
|
124
|
+
justifyContent: "center",
|
|
125
|
+
gap: ufTokens.space2,
|
|
126
|
+
fontFamily: ufTokens.fontSans,
|
|
127
|
+
fontWeight: ufTokens.weightMedium,
|
|
128
|
+
lineHeight: ufTokens.leadingTight,
|
|
129
|
+
borderRadius: ufTokens.radiusMd,
|
|
130
|
+
borderWidth: "1px",
|
|
131
|
+
borderStyle: "solid",
|
|
132
|
+
borderColor: "transparent",
|
|
133
|
+
cursor: "pointer",
|
|
134
|
+
transitionProperty: "background-color, border-color, color",
|
|
135
|
+
transitionDuration: ufTokens.durationFast,
|
|
136
|
+
transitionTimingFunction: ufTokens.easing,
|
|
137
|
+
// The ring is drawn only for a keyboard focus, which is the whole reason
|
|
138
|
+
// `:focus-visible` exists: a mouse click should not light the control up.
|
|
139
|
+
outlineWidth: { default: "0", ":focus-visible": "2px" },
|
|
140
|
+
outlineStyle: "solid",
|
|
141
|
+
outlineColor: ufTokens.focus,
|
|
142
|
+
outlineOffset: "2px",
|
|
143
|
+
},
|
|
144
|
+
primary: {
|
|
145
|
+
backgroundColor: { default: ufTokens.accent, ":hover": ufTokens.accentHover },
|
|
146
|
+
color: ufTokens.accentInk,
|
|
147
|
+
},
|
|
148
|
+
neutral: {
|
|
149
|
+
backgroundColor: { default: ufTokens.surface, ":hover": ufTokens.surfaceHover },
|
|
150
|
+
borderColor: ufTokens.border,
|
|
151
|
+
color: ufTokens.ink,
|
|
152
|
+
},
|
|
153
|
+
ghost: {
|
|
154
|
+
backgroundColor: { default: "transparent", ":hover": ufTokens.surfaceHover },
|
|
155
|
+
color: ufTokens.ink,
|
|
156
|
+
},
|
|
157
|
+
danger: {
|
|
158
|
+
backgroundColor: { default: ufTokens.danger, ":hover": ufTokens.dangerHover },
|
|
159
|
+
color: ufTokens.dangerInk,
|
|
160
|
+
},
|
|
161
|
+
sm: {
|
|
162
|
+
fontSize: ufTokens.textSm,
|
|
163
|
+
paddingBlock: ufTokens.space1,
|
|
164
|
+
paddingInline: ufTokens.space3,
|
|
165
|
+
},
|
|
166
|
+
md: {
|
|
167
|
+
fontSize: ufTokens.textSm,
|
|
168
|
+
paddingBlock: ufTokens.space2,
|
|
169
|
+
paddingInline: ufTokens.space4,
|
|
170
|
+
},
|
|
171
|
+
lg: {
|
|
172
|
+
fontSize: ufTokens.textMd,
|
|
173
|
+
paddingBlock: ufTokens.space3,
|
|
174
|
+
paddingInline: ufTokens.space6,
|
|
175
|
+
},
|
|
176
|
+
disabled: {
|
|
177
|
+
opacity: 0.55,
|
|
178
|
+
cursor: "not-allowed",
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const fields = stylex.create({
|
|
183
|
+
base: {
|
|
184
|
+
display: "block",
|
|
185
|
+
width: "100%",
|
|
186
|
+
fontFamily: ufTokens.fontSans,
|
|
187
|
+
fontSize: ufTokens.textSm,
|
|
188
|
+
lineHeight: ufTokens.leadingBase,
|
|
189
|
+
color: ufTokens.ink,
|
|
190
|
+
backgroundColor: ufTokens.surface,
|
|
191
|
+
borderWidth: "1px",
|
|
192
|
+
borderStyle: "solid",
|
|
193
|
+
borderColor: ufTokens.border,
|
|
194
|
+
borderRadius: ufTokens.radiusSm,
|
|
195
|
+
paddingBlock: ufTokens.space2,
|
|
196
|
+
paddingInline: ufTokens.space3,
|
|
197
|
+
outlineWidth: { default: "0", ":focus-visible": "2px" },
|
|
198
|
+
outlineStyle: "solid",
|
|
199
|
+
outlineColor: ufTokens.focus,
|
|
200
|
+
outlineOffset: "1px",
|
|
201
|
+
},
|
|
202
|
+
invalid: {
|
|
203
|
+
borderColor: ufTokens.danger,
|
|
204
|
+
backgroundColor: ufTokens.dangerSoft,
|
|
205
|
+
},
|
|
206
|
+
disabled: {
|
|
207
|
+
backgroundColor: ufTokens.sunken,
|
|
208
|
+
color: ufTokens.muted,
|
|
209
|
+
cursor: "not-allowed",
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const overlays = stylex.create({
|
|
214
|
+
backdrop: {
|
|
215
|
+
position: "fixed",
|
|
216
|
+
inset: 0,
|
|
217
|
+
backgroundColor: ufTokens.scrim,
|
|
218
|
+
},
|
|
219
|
+
panel: {
|
|
220
|
+
position: "fixed",
|
|
221
|
+
top: "50%",
|
|
222
|
+
left: "50%",
|
|
223
|
+
transform: "translate(-50%, -50%)",
|
|
224
|
+
width: "calc(100% - 32px)",
|
|
225
|
+
maxWidth: "32rem",
|
|
226
|
+
backgroundColor: ufTokens.surface,
|
|
227
|
+
color: ufTokens.ink,
|
|
228
|
+
fontFamily: ufTokens.fontSans,
|
|
229
|
+
borderRadius: ufTokens.radiusXl,
|
|
230
|
+
padding: ufTokens.space6,
|
|
231
|
+
boxShadow: ufTokens.shadowPanel,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const menus = stylex.create({
|
|
236
|
+
list: {
|
|
237
|
+
minWidth: "12rem",
|
|
238
|
+
margin: 0,
|
|
239
|
+
padding: ufTokens.space1,
|
|
240
|
+
listStyle: "none",
|
|
241
|
+
backgroundColor: ufTokens.surface,
|
|
242
|
+
borderWidth: "1px",
|
|
243
|
+
borderStyle: "solid",
|
|
244
|
+
borderColor: ufTokens.border,
|
|
245
|
+
borderRadius: ufTokens.radiusMd,
|
|
246
|
+
boxShadow: ufTokens.shadowPanel,
|
|
247
|
+
},
|
|
248
|
+
item: {
|
|
249
|
+
display: "flex",
|
|
250
|
+
alignItems: "center",
|
|
251
|
+
gap: ufTokens.space2,
|
|
252
|
+
width: "100%",
|
|
253
|
+
paddingBlock: ufTokens.space2,
|
|
254
|
+
paddingInline: ufTokens.space3,
|
|
255
|
+
borderRadius: ufTokens.radiusSm,
|
|
256
|
+
fontFamily: ufTokens.fontSans,
|
|
257
|
+
fontSize: ufTokens.textSm,
|
|
258
|
+
textAlign: "start",
|
|
259
|
+
color: ufTokens.ink,
|
|
260
|
+
backgroundColor: { default: "transparent", ":hover": ufTokens.surfaceHover },
|
|
261
|
+
cursor: "pointer",
|
|
262
|
+
},
|
|
263
|
+
active: {
|
|
264
|
+
backgroundColor: ufTokens.accentSoft,
|
|
265
|
+
color: ufTokens.accent,
|
|
266
|
+
},
|
|
267
|
+
disabled: {
|
|
268
|
+
color: ufTokens.muted,
|
|
269
|
+
cursor: "not-allowed",
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const tabs = stylex.create({
|
|
274
|
+
list: {
|
|
275
|
+
display: "flex",
|
|
276
|
+
gap: ufTokens.space1,
|
|
277
|
+
borderBottomWidth: "1px",
|
|
278
|
+
borderBottomStyle: "solid",
|
|
279
|
+
borderBottomColor: ufTokens.border,
|
|
280
|
+
},
|
|
281
|
+
tab: {
|
|
282
|
+
// `borderWidth: 0` before `borderBottomWidth: "2px"` is the case the sheet's
|
|
283
|
+
// ordering exists for: both are one class selector, and the longhand only
|
|
284
|
+
// survives because the shorthand is emitted before it.
|
|
285
|
+
borderWidth: 0,
|
|
286
|
+
borderStyle: "solid",
|
|
287
|
+
borderColor: "transparent",
|
|
288
|
+
borderBottomWidth: "2px",
|
|
289
|
+
backgroundColor: "transparent",
|
|
290
|
+
paddingBlock: ufTokens.space2,
|
|
291
|
+
paddingInline: ufTokens.space3,
|
|
292
|
+
fontFamily: ufTokens.fontSans,
|
|
293
|
+
fontSize: ufTokens.textSm,
|
|
294
|
+
fontWeight: ufTokens.weightMedium,
|
|
295
|
+
color: { default: ufTokens.muted, ":hover": ufTokens.ink },
|
|
296
|
+
cursor: "pointer",
|
|
297
|
+
},
|
|
298
|
+
selected: {
|
|
299
|
+
color: ufTokens.ink,
|
|
300
|
+
borderBottomColor: ufTokens.accent,
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const controls = stylex.create({
|
|
305
|
+
base: {
|
|
306
|
+
display: "inline-flex",
|
|
307
|
+
alignItems: "center",
|
|
308
|
+
justifyContent: "center",
|
|
309
|
+
flexShrink: 0,
|
|
310
|
+
padding: 0,
|
|
311
|
+
borderWidth: "1px",
|
|
312
|
+
borderStyle: "solid",
|
|
313
|
+
borderColor: ufTokens.border,
|
|
314
|
+
backgroundColor: ufTokens.surface,
|
|
315
|
+
color: ufTokens.accentInk,
|
|
316
|
+
cursor: "pointer",
|
|
317
|
+
transitionProperty: "background-color, border-color",
|
|
318
|
+
transitionDuration: ufTokens.durationFast,
|
|
319
|
+
transitionTimingFunction: ufTokens.easing,
|
|
320
|
+
outlineWidth: { default: "0", ":focus-visible": "2px" },
|
|
321
|
+
outlineStyle: "solid",
|
|
322
|
+
outlineColor: ufTokens.focus,
|
|
323
|
+
outlineOffset: "2px",
|
|
324
|
+
},
|
|
325
|
+
box: {
|
|
326
|
+
width: ufTokens.sizeControl,
|
|
327
|
+
height: ufTokens.sizeControl,
|
|
328
|
+
borderRadius: "5px",
|
|
329
|
+
},
|
|
330
|
+
// A switch's track is the one piece of geometry here that is not a token:
|
|
331
|
+
// nothing but a switch reads it, and a theme that changed it would leave the
|
|
332
|
+
// thumb somewhere else.
|
|
333
|
+
track: {
|
|
334
|
+
width: "36px",
|
|
335
|
+
height: "20px",
|
|
336
|
+
justifyContent: "flex-start",
|
|
337
|
+
borderRadius: ufTokens.radiusPill,
|
|
338
|
+
},
|
|
339
|
+
on: {
|
|
340
|
+
backgroundColor: ufTokens.accent,
|
|
341
|
+
borderColor: ufTokens.accent,
|
|
342
|
+
},
|
|
343
|
+
disabled: {
|
|
344
|
+
backgroundColor: ufTokens.sunken,
|
|
345
|
+
cursor: "not-allowed",
|
|
346
|
+
opacity: 0.6,
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
/** A page or a card, with the preset's type already on it. */
|
|
351
|
+
export function surfaceStyles(options?: { readonly kind?: SurfaceKind }): StyleProps {
|
|
352
|
+
const kind = options?.kind ?? "card";
|
|
353
|
+
return props(
|
|
354
|
+
surfaces.base,
|
|
355
|
+
match (kind) {
|
|
356
|
+
"page" => surfaces.page,
|
|
357
|
+
"card" => surfaces.card,
|
|
358
|
+
"panel" => surfaces.panel,
|
|
359
|
+
"sunken" => surfaces.sunken,
|
|
360
|
+
},
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** A card: the surface most application chrome is made of. */
|
|
365
|
+
export function cardStyles(): StyleProps {
|
|
366
|
+
return surfaceStyles({ kind: "card" });
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** One step of the type scale, in one of the three text roles. */
|
|
370
|
+
export function textStyles(options?: {
|
|
371
|
+
readonly size?: TextSize,
|
|
372
|
+
readonly tone?: TextTone,
|
|
373
|
+
readonly strong?: boolean,
|
|
374
|
+
}): StyleProps {
|
|
375
|
+
const size = options?.size ?? "md";
|
|
376
|
+
const tone = options?.tone ?? "ink";
|
|
377
|
+
return props(
|
|
378
|
+
texts.base,
|
|
379
|
+
match (size) {
|
|
380
|
+
"xs" => texts.xs,
|
|
381
|
+
"sm" => texts.sm,
|
|
382
|
+
"md" => texts.md,
|
|
383
|
+
"lg" => texts.lg,
|
|
384
|
+
"xl" => texts.xl,
|
|
385
|
+
"2xl" => texts.xxl,
|
|
386
|
+
},
|
|
387
|
+
match (tone) {
|
|
388
|
+
"ink" => texts.ink,
|
|
389
|
+
"muted" => texts.muted,
|
|
390
|
+
"danger" => texts.danger,
|
|
391
|
+
},
|
|
392
|
+
options?.strong === true && texts.strong,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** A button, in one of four tones and three sizes. */
|
|
397
|
+
export function buttonStyles(options?: {
|
|
398
|
+
readonly tone?: Tone,
|
|
399
|
+
readonly size?: Size,
|
|
400
|
+
readonly disabled?: boolean,
|
|
401
|
+
}): StyleProps {
|
|
402
|
+
const tone = options?.tone ?? "neutral";
|
|
403
|
+
const size = options?.size ?? "md";
|
|
404
|
+
return props(
|
|
405
|
+
buttons.base,
|
|
406
|
+
match (tone) {
|
|
407
|
+
"primary" => buttons.primary,
|
|
408
|
+
"neutral" => buttons.neutral,
|
|
409
|
+
"ghost" => buttons.ghost,
|
|
410
|
+
"danger" => buttons.danger,
|
|
411
|
+
},
|
|
412
|
+
match (size) {
|
|
413
|
+
"sm" => buttons.sm,
|
|
414
|
+
"md" => buttons.md,
|
|
415
|
+
"lg" => buttons.lg,
|
|
416
|
+
},
|
|
417
|
+
options?.disabled === true && buttons.disabled,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** A text input, a select, or anything else that takes typing. */
|
|
422
|
+
export function fieldStyles(options?: {
|
|
423
|
+
readonly invalid?: boolean,
|
|
424
|
+
readonly disabled?: boolean,
|
|
425
|
+
}): StyleProps {
|
|
426
|
+
return props(
|
|
427
|
+
fields.base,
|
|
428
|
+
options?.invalid === true && fields.invalid,
|
|
429
|
+
options?.disabled === true && fields.disabled,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** The wash behind a modal surface. */
|
|
434
|
+
export function backdropStyles(): StyleProps {
|
|
435
|
+
return props(overlays.backdrop);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** A centred modal panel. */
|
|
439
|
+
export function dialogStyles(): StyleProps {
|
|
440
|
+
return props(overlays.panel);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** The box a menu's options sit in. */
|
|
444
|
+
export function menuStyles(): StyleProps {
|
|
445
|
+
return props(menus.list);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** One option in a menu. */
|
|
449
|
+
export function menuItemStyles(options?: {
|
|
450
|
+
readonly active?: boolean,
|
|
451
|
+
readonly disabled?: boolean,
|
|
452
|
+
}): StyleProps {
|
|
453
|
+
return props(
|
|
454
|
+
menus.item,
|
|
455
|
+
options?.active === true && menus.active,
|
|
456
|
+
options?.disabled === true && menus.disabled,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** The row a set of tabs sits in. */
|
|
461
|
+
export function tabListStyles(): StyleProps {
|
|
462
|
+
return props(tabs.list);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** One tab, selected or not. */
|
|
466
|
+
export function tabStyles(options?: { readonly selected?: boolean }): StyleProps {
|
|
467
|
+
return props(tabs.tab, options?.selected === true && tabs.selected);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** A checkbox's box or a switch's track. */
|
|
471
|
+
export function controlStyles(options?: {
|
|
472
|
+
readonly shape?: "box" | "track",
|
|
473
|
+
readonly on?: boolean,
|
|
474
|
+
readonly disabled?: boolean,
|
|
475
|
+
}): StyleProps {
|
|
476
|
+
const shape = options?.shape ?? "box";
|
|
477
|
+
return props(
|
|
478
|
+
controls.base,
|
|
479
|
+
match (shape) {
|
|
480
|
+
"box" => controls.box,
|
|
481
|
+
"track" => controls.track,
|
|
482
|
+
},
|
|
483
|
+
options?.on === true && controls.on,
|
|
484
|
+
options?.disabled === true && controls.disabled,
|
|
485
|
+
);
|
|
486
|
+
}
|
package/props.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/stylex/props`: the merge, and the only part of StyleX that runs.
|
|
4
|
+
//
|
|
5
|
+
// `uf transform` rewrites every `stylex.create({ … })` and
|
|
6
|
+
// `stylex.createTheme(tokens, { … })` into a plain object of class names, so by
|
|
7
|
+
// the time this module is loaded there are no style values left. What is left
|
|
8
|
+
// is the merge, and it is here because it cannot be anywhere else: a call site
|
|
9
|
+
// writes `active && styles.on`, and a compiler cannot fold a value it does not
|
|
10
|
+
// know.
|
|
11
|
+
//
|
|
12
|
+
// # This file has a twin
|
|
13
|
+
//
|
|
14
|
+
// The same merge is modelled in `crates/uf_stylex/src/props.rs`, at compile
|
|
15
|
+
// time, which is what lets its ordering be tested against the sheet the
|
|
16
|
+
// compiler emits. The two have to agree, and they agree on the two things that
|
|
17
|
+
// are observable:
|
|
18
|
+
//
|
|
19
|
+
// * **which classes survive.** The *property* is the unit of merging — a later
|
|
20
|
+
// namespace that sets `color` replaces everything an earlier one said about
|
|
21
|
+
// `color`, its `:hover` value included. That is what a later `color:` in a
|
|
22
|
+
// stylesheet does, and it is why a later namespace cannot leave a stray
|
|
23
|
+
// hover state behind.
|
|
24
|
+
// * **that every class of a surviving property survives.** A property written
|
|
25
|
+
// with states compiles to a *map* — `{ "default": "x1", ":hover": "x2" }` —
|
|
26
|
+
// and both classes belong in the class attribute. A runtime that kept only
|
|
27
|
+
// the first, or skipped the map because it is not a string, would silently
|
|
28
|
+
// drop every conditional style in an application while every test that used
|
|
29
|
+
// plain values kept passing.
|
|
30
|
+
//
|
|
31
|
+
// They deliberately do not agree on the *order* of the class attribute, and
|
|
32
|
+
// nothing depends on it: the compiler sorts globally by sheet position because
|
|
33
|
+
// it has the priorities to hand, this file emits in the order properties were
|
|
34
|
+
// first claimed because it does not. Which rule wins is decided by the sheet,
|
|
35
|
+
// never by the attribute, so the two orders render identically. Within one
|
|
36
|
+
// property the orders do match, because the compiler writes that property's
|
|
37
|
+
// states in sheet order and this file reads them in the order it finds them.
|
|
38
|
+
|
|
39
|
+
/** The class names one property sets, keyed by the state each applies in. */
|
|
40
|
+
export type CompiledClasses = { readonly [state: string]: string | null };
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A compiled style namespace.
|
|
44
|
+
*
|
|
45
|
+
* `$$css` marks an object the compiler produced. Every other key is a CSS
|
|
46
|
+
* property — or a custom property, for a theme — mapped to the class name that
|
|
47
|
+
* sets it, to a map of class names when the property has states, or to `null`,
|
|
48
|
+
* which is how a namespace says it deliberately unsets that property.
|
|
49
|
+
*/
|
|
50
|
+
export type CompiledStyle = {
|
|
51
|
+
readonly $$css: true,
|
|
52
|
+
readonly [property: string]: string | null | true | CompiledClasses,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** What a call site may pass: a namespace, something falsy, or a list. */
|
|
56
|
+
export type StyleArgument = mixed;
|
|
57
|
+
|
|
58
|
+
/** What `props` hands to an element. */
|
|
59
|
+
export type StyleProps = { readonly className?: string };
|
|
60
|
+
|
|
61
|
+
/** What one property contributed, once the merge has picked a winner. */
|
|
62
|
+
type Winner = string | null | CompiledClasses;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Merge compiled namespaces into a `className`, left to right.
|
|
66
|
+
*
|
|
67
|
+
* Falsy arguments are skipped, because `active && styles.on` is the idiom this
|
|
68
|
+
* function exists for, and arrays are flattened so a list built elsewhere can
|
|
69
|
+
* be passed without spreading it.
|
|
70
|
+
*
|
|
71
|
+
* Returns an object rather than a string so the call site stays
|
|
72
|
+
* `<div {...stylex.props(a, b)} />` — the same shape whether or not anything
|
|
73
|
+
* survived.
|
|
74
|
+
*/
|
|
75
|
+
export function props(...styles: $ReadOnlyArray<StyleArgument>): StyleProps {
|
|
76
|
+
const winners: { [string]: Winner } = {};
|
|
77
|
+
collect(styles, winners);
|
|
78
|
+
|
|
79
|
+
let className = "";
|
|
80
|
+
for (const property of Object.keys(winners)) {
|
|
81
|
+
const winner = winners[property];
|
|
82
|
+
// `null` is a deliberate unset: the property has an owner, and that owner
|
|
83
|
+
// said there should be no class for it.
|
|
84
|
+
if (winner == null) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (typeof winner === "string") {
|
|
88
|
+
className = join(className, winner);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// A property with states. Every one of its classes belongs in the
|
|
92
|
+
// attribute: the base rule and the `:hover` rule are separate rules in the
|
|
93
|
+
// sheet, and dropping either is a style that never applies.
|
|
94
|
+
for (const state of Object.keys(winner)) {
|
|
95
|
+
const name = winner[state];
|
|
96
|
+
if (name != null) {
|
|
97
|
+
className = join(className, name);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return className === "" ? {} : { className };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Fold arguments into `winners`, flattening arrays.
|
|
107
|
+
*
|
|
108
|
+
* Insertion order is the order a property was *first* claimed, and assigning
|
|
109
|
+
* over an existing key does not move it — so two namespaces that both set
|
|
110
|
+
* `color` produce one entry in the position the first one had. The class list
|
|
111
|
+
* is a function of the properties involved, not of how many namespaces
|
|
112
|
+
* mentioned them.
|
|
113
|
+
*/
|
|
114
|
+
function collect(styles: $ReadOnlyArray<StyleArgument>, winners: { [string]: Winner }): void {
|
|
115
|
+
for (const style of styles) {
|
|
116
|
+
if (style == null || style === false || style === true) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(style)) {
|
|
120
|
+
collect(style, winners);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (typeof style !== "object") {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// The one place an untyped value enters. This object was written by `uf
|
|
127
|
+
// transform`, and its shape is the compiler's promise rather than something
|
|
128
|
+
// Flow can see from a call site; everything below reads it through
|
|
129
|
+
// `CompiledStyle`, so the trust boundary is this line and no wider.
|
|
130
|
+
const namespace: CompiledStyle = style as $FlowFixMe;
|
|
131
|
+
for (const property of Object.keys(namespace)) {
|
|
132
|
+
const value = namespace[property];
|
|
133
|
+
// `$$css` is the marker, not a property. Skipping it by value as well as
|
|
134
|
+
// by name means a plain object that happens to carry `true` somewhere
|
|
135
|
+
// cannot put `true` into a class attribute.
|
|
136
|
+
if (property === "$$css" || value === true) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
winners[property] = value;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Append one class name to a space-separated attribute. */
|
|
145
|
+
function join(className: string, name: string): string {
|
|
146
|
+
return className === "" ? name : className + " " + name;
|
|
147
|
+
}
|
package/theme.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/stylex/theme`: the shipped themes, and how to replace them.
|
|
4
|
+
//
|
|
5
|
+
// import { props } from "@uniflowed/stylex";
|
|
6
|
+
// import { ufAutoTheme } from "@uniflowed/stylex/theme";
|
|
7
|
+
//
|
|
8
|
+
// <body {...props(ufAutoTheme)}>…</body>
|
|
9
|
+
//
|
|
10
|
+
// This is the third of the three things uf's preset is, and the one that makes
|
|
11
|
+
// the other two a default rather than a decoration. A theme is a set of
|
|
12
|
+
// overrides for `./tokens.stylex.js`; `uf transform` turns each override into
|
|
13
|
+
// one class whose rule writes that token's custom property, so putting the
|
|
14
|
+
// theme's class on an ancestor changes what every `var(--…)` beneath it
|
|
15
|
+
// resolves to. No rule in `preset.js` knows a theme happened.
|
|
16
|
+
//
|
|
17
|
+
// # Two themes, because there are two questions
|
|
18
|
+
//
|
|
19
|
+
// [`ufAutoTheme`] answers "follow the reader's operating system". It overrides
|
|
20
|
+
// nothing at all in light mode — every entry is written *only* under
|
|
21
|
+
// `@media (prefers-color-scheme: dark)`, so outside dark mode its class matches
|
|
22
|
+
// no rule and the tokens keep their declared values. That is why it costs one
|
|
23
|
+
// media block rather than two full sets of custom properties.
|
|
24
|
+
//
|
|
25
|
+
// [`ufDarkTheme`] answers "this subtree is dark, whatever the system says": a
|
|
26
|
+
// preview pane, a code surface, a reader who chose. Its overrides are
|
|
27
|
+
// unconditional, so it also works as the thing a stored preference applies.
|
|
28
|
+
//
|
|
29
|
+
// Both can be composed, and the merge decides the same way it decides anything
|
|
30
|
+
// else: `props(ufAutoTheme, ufDarkTheme)` is dark, because the later argument
|
|
31
|
+
// wins token by token.
|
|
32
|
+
//
|
|
33
|
+
// # Writing your own
|
|
34
|
+
//
|
|
35
|
+
// `createTheme` takes the token set and a subset of its keys, and Flow rejects
|
|
36
|
+
// a key the set does not declare — so a renamed token is a type error rather
|
|
37
|
+
// than a custom property nothing reads:
|
|
38
|
+
//
|
|
39
|
+
// export const brandTheme = stylex.createTheme(ufTokens, {
|
|
40
|
+
// accent: "#0f766e",
|
|
41
|
+
// accentHover: "#115e59",
|
|
42
|
+
// });
|
|
43
|
+
//
|
|
44
|
+
// Overriding four colours is a complete theme; there is no requirement to
|
|
45
|
+
// restate the set. Two themes that give the same token the same value compile
|
|
46
|
+
// to the same class and one rule, wherever they were written.
|
|
47
|
+
//
|
|
48
|
+
// # Contrast
|
|
49
|
+
//
|
|
50
|
+
// The dark values below meet WCAG AA at 4.5:1 for every pair the preset
|
|
51
|
+
// actually pairs, and `crates/uf_stylex/src/tests/preset.rs` computes those
|
|
52
|
+
// ratios from the compiled stylesheet.
|
|
53
|
+
|
|
54
|
+
import { stylex } from "@uniflowed/stylex";
|
|
55
|
+
|
|
56
|
+
import { ufTokens } from "./tokens.stylex.js";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The preset, following the reader's operating system.
|
|
60
|
+
*
|
|
61
|
+
* Every entry is conditional, so this theme is inert in light mode and costs
|
|
62
|
+
* exactly one `@media (prefers-color-scheme: dark)` block.
|
|
63
|
+
*/
|
|
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)" },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The preset in dark, unconditionally.
|
|
88
|
+
*
|
|
89
|
+
* For a subtree that is dark whatever the system says, and for the stored
|
|
90
|
+
* preference a reader chose.
|
|
91
|
+
*/
|
|
92
|
+
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)",
|
|
112
|
+
});
|
package/tokens.stylex.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/stylex/tokens.stylex.js`: the tokens a project gets for free.
|
|
4
|
+
//
|
|
5
|
+
// This is an ordinary `stylex.defineVars` module — the compiler binds it by its
|
|
6
|
+
// `.stylex.js` suffix like any other — and it is the first of the three things
|
|
7
|
+
// uf's preset is. Import it and every name below is a `var(--…)` the build
|
|
8
|
+
// already declared on `:root`; nothing here is computed in a browser, and a
|
|
9
|
+
// project that never imports it ships none of it.
|
|
10
|
+
//
|
|
11
|
+
// # Roles, not colours
|
|
12
|
+
//
|
|
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.
|
|
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.
|
|
26
|
+
//
|
|
27
|
+
// # What is not a token
|
|
28
|
+
//
|
|
29
|
+
// Geometry that exactly one control uses. A switch's track is 36×20 because
|
|
30
|
+
// that is what a switch is, and promoting it to a token would invite a theme to
|
|
31
|
+
// change it into something the thumb no longer fits. A token is a decision more
|
|
32
|
+
// than one rule reads.
|
|
33
|
+
//
|
|
34
|
+
// # Contrast
|
|
35
|
+
//
|
|
36
|
+
// Every foreground/background pair the preset actually pairs meets WCAG AA at
|
|
37
|
+
// 4.5:1, in both the light default and the shipped dark theme, and
|
|
38
|
+
// `crates/uf_stylex/src/tests/preset.rs` computes the ratios from the compiled
|
|
39
|
+
// stylesheet rather than trusting this comment.
|
|
40
|
+
|
|
41
|
+
import { stylex } from "@uniflowed/stylex";
|
|
42
|
+
|
|
43
|
+
// Deliberately unannotated: `defineVars` hands back exactly what it was given,
|
|
44
|
+
// so the inferred type is the token set itself — which is what makes
|
|
45
|
+
// `ThemeOverrides<typeof ufTokens>` reject a token this module does not declare.
|
|
46
|
+
export const ufTokens = stylex.defineVars({
|
|
47
|
+
// Surfaces, from furthest back to nearest front.
|
|
48
|
+
canvas: "#f8fafc",
|
|
49
|
+
sunken: "#eef2f7",
|
|
50
|
+
surface: "#ffffff",
|
|
51
|
+
surfaceHover: "#f1f5f9",
|
|
52
|
+
border: "#dbe3ec",
|
|
53
|
+
|
|
54
|
+
// Text.
|
|
55
|
+
ink: "#0f172a",
|
|
56
|
+
muted: "#475569",
|
|
57
|
+
|
|
58
|
+
// The colour a primary action wears, and what is legible on it.
|
|
59
|
+
accent: "#5c49ff",
|
|
60
|
+
accentHover: "#4a37f0",
|
|
61
|
+
accentInk: "#ffffff",
|
|
62
|
+
accentSoft: "#eeecff",
|
|
63
|
+
|
|
64
|
+
// The colour a destructive action wears.
|
|
65
|
+
danger: "#b42318",
|
|
66
|
+
dangerHover: "#9a1c12",
|
|
67
|
+
dangerInk: "#ffffff",
|
|
68
|
+
dangerSoft: "#fef3f2",
|
|
69
|
+
|
|
70
|
+
// Focus ring, and the wash behind a modal surface.
|
|
71
|
+
focus: "#2677ff",
|
|
72
|
+
scrim: "rgba(15, 23, 42, 0.48)",
|
|
73
|
+
|
|
74
|
+
// Type.
|
|
75
|
+
fontSans: "ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
|
|
76
|
+
fontMono: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
77
|
+
textXs: "12px",
|
|
78
|
+
textSm: "14px",
|
|
79
|
+
textMd: "16px",
|
|
80
|
+
textLg: "20px",
|
|
81
|
+
textXl: "28px",
|
|
82
|
+
text2Xl: "40px",
|
|
83
|
+
leadingTight: 1.25,
|
|
84
|
+
leadingBase: 1.55,
|
|
85
|
+
weightRegular: 400,
|
|
86
|
+
weightMedium: 500,
|
|
87
|
+
weightBold: 700,
|
|
88
|
+
|
|
89
|
+
// Space.
|
|
90
|
+
space1: "4px",
|
|
91
|
+
space2: "8px",
|
|
92
|
+
space3: "12px",
|
|
93
|
+
space4: "16px",
|
|
94
|
+
space6: "24px",
|
|
95
|
+
space8: "32px",
|
|
96
|
+
space12: "48px",
|
|
97
|
+
|
|
98
|
+
// Shape.
|
|
99
|
+
radiusSm: "8px",
|
|
100
|
+
radiusMd: "12px",
|
|
101
|
+
radiusLg: "16px",
|
|
102
|
+
radiusXl: "24px",
|
|
103
|
+
radiusPill: "999px",
|
|
104
|
+
|
|
105
|
+
// The size of a control a finger or a pointer aims at.
|
|
106
|
+
sizeControl: "18px",
|
|
107
|
+
|
|
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)",
|
|
111
|
+
durationFast: "120ms",
|
|
112
|
+
durationBase: "200ms",
|
|
113
|
+
easing: "cubic-bezier(0.2, 0, 0, 1)",
|
|
114
|
+
});
|