frappe-ui 1.0.0-beta.74 → 1.0.0-beta.75
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 +10 -4
- package/src/components/Badge/Badge.vue +1 -1
- package/tailwind/colorPalette.js +61 -166
- package/tailwind/index.d.ts +4 -0
- package/tailwind/lucideIconsPlugin.js +10 -2
- package/tailwind/migrate-tokens-v2.js +2 -2
- package/tailwind/plugin.js +67 -88
- package/tailwind/preset.js +3 -20
- package/tailwind/tokens/build-types.js +281 -0
- package/tailwind/{figma-tokens-to-theme.js → tokens/build.js} +134 -37
- package/tailwind/{colors.json → tokens/colors.js} +4 -1
- package/tailwind/{generated/effects.json → tokens/effects.js} +4 -1
- package/tailwind/tokens/provenance.json +11 -0
- package/tailwind/{generated/radius.json → tokens/radius.js} +4 -1
- package/tailwind/{generated/typography.json → tokens/typography.js} +22 -19
- package/tailwind/tokens.d.ts +714 -0
- package/tailwind/tokens.js +335 -0
- package/tailwind/generated/colors.json +0 -1173
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frappe-ui",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.75",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.19.0"
|
|
@@ -69,7 +69,8 @@
|
|
|
69
69
|
"docs:gen": "tsx docs/scripts/propsgen.ts",
|
|
70
70
|
"docs:gen:all": "yarn docs:gen --all",
|
|
71
71
|
"docs:check": "tsx docs/scripts/propsgen.ts --check",
|
|
72
|
-
"sync-tokens": "node tailwind/
|
|
72
|
+
"sync-tokens": "node tailwind/tokens/build.js",
|
|
73
|
+
"sync-token-types": "node tailwind/tokens/build-types.js"
|
|
73
74
|
},
|
|
74
75
|
"exports": {
|
|
75
76
|
".": {
|
|
@@ -105,6 +106,11 @@
|
|
|
105
106
|
"import": "./tailwind/index.js",
|
|
106
107
|
"default": "./tailwind/index.js"
|
|
107
108
|
},
|
|
109
|
+
"./tailwind/tokens": {
|
|
110
|
+
"types": "./tailwind/tokens.d.ts",
|
|
111
|
+
"import": "./tailwind/tokens.js",
|
|
112
|
+
"default": "./tailwind/tokens.js"
|
|
113
|
+
},
|
|
108
114
|
"./vite": {
|
|
109
115
|
"types": "./vite/index.d.ts",
|
|
110
116
|
"import": "./vite/index.js"
|
|
@@ -240,7 +246,7 @@
|
|
|
240
246
|
"@shikijs/transformers": ">=3.15.0",
|
|
241
247
|
"@vue/compiler-dom": ">=3.5.0",
|
|
242
248
|
"shiki": ">=3.15.0",
|
|
243
|
-
"tailwindcss": ">=3.4.
|
|
249
|
+
"tailwindcss": ">=3.4.2 <4",
|
|
244
250
|
"vite": ">=5",
|
|
245
251
|
"vitepress": ">=2.0.0-alpha.15",
|
|
246
252
|
"vue": ">=3.5.0",
|
|
@@ -326,7 +332,7 @@
|
|
|
326
332
|
"prettier": "^3.3.2",
|
|
327
333
|
"prettier-plugin-tailwindcss": "^0.1.13",
|
|
328
334
|
"shiki": "^3.15.0",
|
|
329
|
-
"tailwindcss": "^3.4.
|
|
335
|
+
"tailwindcss": "^3.4.2",
|
|
330
336
|
"tsx": "^4.20.6",
|
|
331
337
|
"vite": "^7.3.2",
|
|
332
338
|
"vite-plugin-istanbul": "^9.0.0",
|
|
@@ -32,7 +32,7 @@ const props = withDefaults(defineProps<BadgeProps>(), {
|
|
|
32
32
|
variant: 'subtle',
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
// The semantic scale (Figma export → tailwind/
|
|
35
|
+
// The semantic scale (Figma export → tailwind/tokens/colors.js) is
|
|
36
36
|
// consistent across all themes — gray is the only exception (`-7` is its
|
|
37
37
|
// saturated step, since gray needs more headroom). Tailwind's JIT needs
|
|
38
38
|
// literal class names so the per-theme strings are inlined below.
|
package/tailwind/colorPalette.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind-shaped colours.
|
|
3
|
+
*
|
|
4
|
+
* Everything here exists because Tailwind needs a sentinel that is meaningless
|
|
5
|
+
* anywhere else. `<alpha-value>` is how Tailwind v3 threads the `/50` opacity
|
|
6
|
+
* modifier into a colour: it substitutes the modifier at compile time. A bare
|
|
7
|
+
* `oklch(L C H)` has no alpha slot, so without the placeholder `bg-blue-900/30`
|
|
8
|
+
* silently emits an opaque colour.
|
|
9
|
+
*
|
|
10
|
+
* Which is why this is a separate layer. `tokens.js` holds the same values
|
|
11
|
+
* clean, for every consumer that is not Tailwind.
|
|
12
|
+
*/
|
|
1
13
|
import tailwindColors from 'tailwindcss/colors'
|
|
2
|
-
import
|
|
3
|
-
import effectsData from './generated/effects.json'
|
|
14
|
+
import { colors, semanticColors } from './tokens.js'
|
|
4
15
|
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// `<alpha-value>` placeholder into solid oklch values used as theme colors.
|
|
9
|
-
// Values that already carry an alpha channel (overlay tokens) are left intact;
|
|
10
|
-
// colors.json and the raw `--*` CSS variables keep their plain oklch form.
|
|
16
|
+
// Inject the placeholder into solid oklch values. Values that already carry an
|
|
17
|
+
// alpha channel (the overlay ramps) are left intact — they are deliberately
|
|
18
|
+
// translucent, and a modifier on top of that is not a thing Tailwind supports.
|
|
11
19
|
function withAlphaPlaceholder(value) {
|
|
12
20
|
if (typeof value !== 'string') return value
|
|
13
21
|
const solid = value.match(/^oklch\(([^/]+)\)$/)
|
|
@@ -23,6 +31,26 @@ function mapShades(shades) {
|
|
|
23
31
|
)
|
|
24
32
|
}
|
|
25
33
|
|
|
34
|
+
// Declared in the order Tailwind should emit their utilities. Key order in
|
|
35
|
+
// `theme.colors` is CSS source order, so two colour utilities on the same
|
|
36
|
+
// element resolve by it — which makes this list behaviour, not decoration.
|
|
37
|
+
// Families in the export but not named here still land, after the two
|
|
38
|
+
// overlay ramps that follow this list.
|
|
39
|
+
const FAMILY_ORDER = [
|
|
40
|
+
'gray',
|
|
41
|
+
'blue',
|
|
42
|
+
'green',
|
|
43
|
+
'red',
|
|
44
|
+
'orange',
|
|
45
|
+
'yellow',
|
|
46
|
+
'teal',
|
|
47
|
+
'violet',
|
|
48
|
+
'cyan',
|
|
49
|
+
'amber',
|
|
50
|
+
'pink',
|
|
51
|
+
'purple',
|
|
52
|
+
]
|
|
53
|
+
|
|
26
54
|
function generateColorPalette() {
|
|
27
55
|
const colorPalette = {
|
|
28
56
|
inherit: tailwindColors.inherit,
|
|
@@ -30,174 +58,41 @@ function generateColorPalette() {
|
|
|
30
58
|
transparent: tailwindColors.transparent,
|
|
31
59
|
black: tailwindColors.black,
|
|
32
60
|
white: tailwindColors.white,
|
|
33
|
-
gray: {},
|
|
34
|
-
blue: {},
|
|
35
|
-
green: {},
|
|
36
|
-
red: {},
|
|
37
|
-
orange: {},
|
|
38
|
-
yellow: {},
|
|
39
|
-
teal: {},
|
|
40
|
-
violet: {},
|
|
41
|
-
cyan: {},
|
|
42
|
-
amber: {},
|
|
43
|
-
pink: {},
|
|
44
|
-
purple: {},
|
|
45
|
-
'white-overlay': {},
|
|
46
|
-
'black-overlay': {},
|
|
47
61
|
}
|
|
62
|
+
for (const family of FAMILY_ORDER) colorPalette[family] = {}
|
|
63
|
+
colorPalette['white-overlay'] = { ...colors.overlay.white }
|
|
64
|
+
colorPalette['black-overlay'] = { ...colors.overlay.black }
|
|
48
65
|
|
|
49
|
-
Object.
|
|
50
|
-
colorPalette[
|
|
51
|
-
})
|
|
52
|
-
Object.keys(colorsData.darkMode).forEach((color) => {
|
|
53
|
-
colorPalette[`dark-${color}`] = mapShades(colorsData.darkMode[color])
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
Object.keys(colorsData.overlay.white).forEach((shade) => {
|
|
57
|
-
colorPalette['white-overlay'][shade] = colorsData.overlay.white[shade]
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
Object.keys(colorsData.overlay.black).forEach((shade) => {
|
|
61
|
-
colorPalette['black-overlay'][shade] = colorsData.overlay.black[shade]
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
return colorPalette
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function resolveColorReference(reference) {
|
|
68
|
-
const [mode, color, shade] = reference.split('/')
|
|
69
|
-
if (mode === 'lightMode') {
|
|
70
|
-
return colorsData.lightMode[color][shade]
|
|
71
|
-
} else if (mode === 'darkMode') {
|
|
72
|
-
return colorsData.darkMode[color][shade]
|
|
73
|
-
} else if (mode === 'overlay') {
|
|
74
|
-
return colorsData.overlay[color][shade]
|
|
75
|
-
} else if (mode === 'neutral') {
|
|
76
|
-
return colorsData.neutral[color]
|
|
66
|
+
for (const [family, shades] of Object.entries(colors.light)) {
|
|
67
|
+
colorPalette[family] = mapShades(shades)
|
|
77
68
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
':root': {},
|
|
84
|
-
'[data-theme="dark"]': {},
|
|
69
|
+
// Dark ramps are addressable by name (`bg-dark-gray-500`) for the rare case
|
|
70
|
+
// a component needs the other theme's step explicitly. Theme flipping itself
|
|
71
|
+
// goes through the semantic `--<category>-<name>` variables, not these.
|
|
72
|
+
for (const [family, shades] of Object.entries(colors.dark)) {
|
|
73
|
+
colorPalette[`dark-${family}`] = mapShades(shades)
|
|
85
74
|
}
|
|
86
75
|
|
|
87
|
-
|
|
88
|
-
Object.keys(colorsData.themedVariables.light).forEach((category) => {
|
|
89
|
-
Object.keys(colorsData.themedVariables.light[category]).forEach(
|
|
90
|
-
(colorName) => {
|
|
91
|
-
const variableName = `--${category}-${colorName}`
|
|
92
|
-
const reference = colorsData.themedVariables.light[category][colorName]
|
|
93
|
-
const lightValue = resolveColorReference(reference)
|
|
94
|
-
output[':root'][variableName] = lightValue
|
|
95
|
-
},
|
|
96
|
-
)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
// Generate CSS variables for dark mode
|
|
100
|
-
Object.keys(colorsData.themedVariables.dark).forEach((category) => {
|
|
101
|
-
Object.keys(colorsData.themedVariables.dark[category]).forEach(
|
|
102
|
-
(colorName) => {
|
|
103
|
-
const variableName = `--${category}-${colorName}`
|
|
104
|
-
const reference = colorsData.themedVariables.dark[category][colorName]
|
|
105
|
-
const darkValue = resolveColorReference(reference)
|
|
106
|
-
output['[data-theme="dark"]'][variableName] = darkValue
|
|
107
|
-
},
|
|
108
|
-
)
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
// Generate CSS variables for each color shade
|
|
112
|
-
Object.keys(colorsData.lightMode).forEach((color) => {
|
|
113
|
-
Object.keys(colorsData.lightMode[color]).forEach((shade) => {
|
|
114
|
-
const variableName = `--${color}-${shade}`
|
|
115
|
-
const value = colorsData.lightMode[color][shade]
|
|
116
|
-
output[':root'][variableName] = value
|
|
117
|
-
})
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
Object.keys(colorsData.darkMode).forEach((color) => {
|
|
121
|
-
Object.keys(colorsData.darkMode[color]).forEach((shade) => {
|
|
122
|
-
const variableName = `--dark-${color}-${shade}`
|
|
123
|
-
const value = colorsData.darkMode[color][shade]
|
|
124
|
-
output['[data-theme="dark"]'][variableName] = value
|
|
125
|
-
})
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
return output
|
|
76
|
+
return colorPalette
|
|
129
77
|
}
|
|
130
78
|
|
|
79
|
+
// Semantic colours point at their CSS variable rather than a fixed value, so
|
|
80
|
+
// one utility works in both themes. The `color-mix` wrapper is what gives them
|
|
81
|
+
// an alpha slot: a `var(...)` reference has none, so `bg-surface-base/50` would
|
|
82
|
+
// otherwise be ignored. The resolved light value rides along as the var's
|
|
83
|
+
// fallback, so the colour still renders where the stylesheet isn't loaded.
|
|
131
84
|
function generateSemanticColors() {
|
|
132
|
-
|
|
133
|
-
Object.
|
|
85
|
+
return Object.fromEntries(
|
|
86
|
+
Object.entries(semanticColors.light).map(([category, entries]) => [
|
|
134
87
|
category,
|
|
135
|
-
|
|
88
|
+
Object.fromEntries(
|
|
89
|
+
Object.entries(entries).map(([name, lightValue]) => [
|
|
90
|
+
name,
|
|
91
|
+
`color-mix(in srgb, var(--${category}-${name}, ${lightValue}) calc(<alpha-value> * 100%), transparent)`,
|
|
92
|
+
]),
|
|
93
|
+
),
|
|
136
94
|
]),
|
|
137
95
|
)
|
|
138
|
-
|
|
139
|
-
// Generate semantic colors
|
|
140
|
-
Object.keys(colorsData.themedVariables.light).forEach((category) => {
|
|
141
|
-
Object.keys(colorsData.themedVariables.light[category]).forEach(
|
|
142
|
-
(colorName) => {
|
|
143
|
-
const variableName = `${category}-${colorName}`
|
|
144
|
-
const reference = colorsData.themedVariables.light[category][colorName]
|
|
145
|
-
const lightValue = resolveColorReference(reference)
|
|
146
|
-
output[category][colorName] =
|
|
147
|
-
`color-mix(in srgb, var(--${variableName}, ${lightValue}) calc(<alpha-value> * 100%), transparent)`
|
|
148
|
-
},
|
|
149
|
-
)
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
return output
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Emit `--elevation-*` and `--focus-outline-*` CSS variables. Elevation uses
|
|
156
|
-
// the Figma `light/*` values in both modes (matches how Espresso 2.0 actually
|
|
157
|
-
// applies shadows in dark mode — see the dark-mode page in Figma, which
|
|
158
|
-
// references `elevation/light/*` exclusively). Focus rings still mode-swap.
|
|
159
|
-
// Theme-independent entries (e.g. `elevation.custom.status`) land in
|
|
160
|
-
// `:root` only.
|
|
161
|
-
//
|
|
162
|
-
// The focus source values are box-shadows; only the outline form is emitted
|
|
163
|
-
// (ADR-0005). A box-shadow ring collides with `shadow-*` on the same element
|
|
164
|
-
// and forced-colors mode drops it, so nothing in frappe-ui ever read
|
|
165
|
-
// `--focus-<name>`.
|
|
166
|
-
function generateEffectVariables() {
|
|
167
|
-
const output = {
|
|
168
|
-
':root': {},
|
|
169
|
-
'[data-theme="dark"]': {},
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
for (const [step, value] of Object.entries(effectsData.elevation.light)) {
|
|
173
|
-
output[':root'][`--elevation-${step}`] = value
|
|
174
|
-
}
|
|
175
|
-
for (const [name, value] of Object.entries(effectsData.elevation.custom)) {
|
|
176
|
-
output[':root'][`--elevation-${name}`] = value
|
|
177
|
-
}
|
|
178
|
-
for (const [name, value] of Object.entries(effectsData.focus.light)) {
|
|
179
|
-
output[':root'][`--focus-outline-${name}`] = shadowToOutline(value)
|
|
180
|
-
}
|
|
181
|
-
for (const [name, value] of Object.entries(effectsData.focus.dark)) {
|
|
182
|
-
output['[data-theme="dark"]'][`--focus-outline-${name}`] =
|
|
183
|
-
shadowToOutline(value)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return output
|
|
187
96
|
}
|
|
188
97
|
|
|
189
|
-
|
|
190
|
-
// as an `outline` shorthand (`<spread> solid <color>`) so the global focus
|
|
191
|
-
// ring can use outline instead of box-shadow (no collisions with shadow/ring
|
|
192
|
-
// utilities, survives forced-colors mode).
|
|
193
|
-
function shadowToOutline(shadow) {
|
|
194
|
-
const parts = shadow.trim().split(/\s+/)
|
|
195
|
-
return `${parts[3]} solid ${parts.slice(4).join(' ')}`
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export {
|
|
199
|
-
generateColorPalette,
|
|
200
|
-
generateCSSVariables,
|
|
201
|
-
generateSemanticColors,
|
|
202
|
-
generateEffectVariables,
|
|
203
|
-
}
|
|
98
|
+
export { generateColorPalette, generateSemanticColors }
|
package/tailwind/index.d.ts
CHANGED
|
@@ -15,3 +15,7 @@ export default preset
|
|
|
15
15
|
* not merge a preset's `content`, so spread these into your own `content`.
|
|
16
16
|
*/
|
|
17
17
|
export declare const content: string[]
|
|
18
|
+
|
|
19
|
+
// The design tokens live at `frappe-ui/tailwind/tokens`, not here. This entry
|
|
20
|
+
// statically imports `tailwindcss/plugin`, which plain Node does not resolve.
|
|
21
|
+
// The token module imports nothing but its own data modules and loads anywhere.
|
|
@@ -4,9 +4,17 @@ import { iconPackPlugin } from './iconPackPlugin.js'
|
|
|
4
4
|
|
|
5
5
|
// Resolve lucide-static's icons directory once per plugin init. Works whether
|
|
6
6
|
// frappe-ui is consumed locally or installed as a dependency of another app.
|
|
7
|
-
|
|
7
|
+
//
|
|
8
|
+
// Named `nodeRequire`, not `require`. A loader that transpiles this module to
|
|
9
|
+
// CJS with sucrase puts it in a wrapper that already declares a `require`
|
|
10
|
+
// parameter, so a top-level `const require` throws `SyntaxError: Identifier
|
|
11
|
+
// 'require' has already been declared`. tailwindcss 3.4.0 and 3.4.1 load the
|
|
12
|
+
// config that way. Babel renames the binding instead, which is why tailwindcss
|
|
13
|
+
// 3.4.2 and later are unaffected: they route any source with `import.meta`
|
|
14
|
+
// through jiti's babel transform.
|
|
15
|
+
const nodeRequire = createRequire(import.meta.url)
|
|
8
16
|
const ICONS_DIR = path.join(
|
|
9
|
-
path.dirname(
|
|
17
|
+
path.dirname(nodeRequire.resolve('lucide-static/package.json')),
|
|
10
18
|
'icons',
|
|
11
19
|
)
|
|
12
20
|
|
|
@@ -142,7 +142,7 @@ const OUTLINE_ALPHA_RENAMES = {
|
|
|
142
142
|
|
|
143
143
|
// `tiny` and `13xl`-`16xl` were removed as unused vocabulary in #940 — no
|
|
144
144
|
// utility ships for them anymore. Must stay in sync with `DROPPED_SIZES` in
|
|
145
|
-
// `
|
|
145
|
+
// `tokens/build.js`. Filtered out of the shift tables below so the
|
|
146
146
|
// codemod can never *rename a class onto one of these dead sizes*; see the
|
|
147
147
|
// "no destination" handling further down for reporting pre-existing usage.
|
|
148
148
|
const DEAD_SIZES = ['tiny', '13xl', '14xl', '15xl', '16xl']
|
|
@@ -666,7 +666,7 @@ const EXTENSIONS = new Set([
|
|
|
666
666
|
'.vue', '.ts', '.tsx', '.js', '.jsx', '.md', '.css', '.scss', '.html',
|
|
667
667
|
])
|
|
668
668
|
const SKIP_DIRS = new Set([
|
|
669
|
-
'node_modules', '.git', 'dist', 'cache', 'generated',
|
|
669
|
+
'node_modules', '.git', 'dist', 'cache', 'generated',
|
|
670
670
|
])
|
|
671
671
|
|
|
672
672
|
// Symlinks — directories and files alike — are followed only while their real
|
package/tailwind/plugin.js
CHANGED
|
@@ -1,40 +1,48 @@
|
|
|
1
1
|
import plugin from 'tailwindcss/plugin'
|
|
2
|
+
import { generateColorPalette, generateSemanticColors } from './colorPalette.js'
|
|
2
3
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
cssVariables,
|
|
5
|
+
focusRing as focusRingTokens,
|
|
6
|
+
fontSize as fontSizeTokens,
|
|
7
|
+
fontWeight as fontWeightTokens,
|
|
8
|
+
radius as radiusTokens,
|
|
9
|
+
screens as screenTokens,
|
|
10
|
+
shadows as shadowTokens,
|
|
11
|
+
tracking as trackingTokens,
|
|
12
|
+
} from './tokens.js'
|
|
13
|
+
import typographyTokens from './tokens/typography.js'
|
|
11
14
|
import { listColumnRules } from './listColumns.js'
|
|
12
15
|
|
|
16
|
+
// Read straight from the token module. `textTransform` is not a public token
|
|
17
|
+
// export: it is empty, and the one style that filled it went away in #940.
|
|
18
|
+
const textTransformTokens = typographyTokens.textTransform
|
|
19
|
+
|
|
13
20
|
let colorPalette = generateColorPalette()
|
|
14
21
|
let semanticColors = generateSemanticColors()
|
|
15
|
-
let cssVariables = mergeVariableLayers(
|
|
16
|
-
generateCSSVariables(),
|
|
17
|
-
generateEffectVariables(),
|
|
18
|
-
generateRadiusVariables(),
|
|
19
|
-
)
|
|
20
22
|
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// `
|
|
26
|
-
function
|
|
27
|
-
const
|
|
28
|
-
for (const
|
|
29
|
-
|
|
23
|
+
// The `shadow-*` key list is declared once, in tokens.js#shadows, so a new
|
|
24
|
+
// elevation step reaches both the token map and the utility from one edit.
|
|
25
|
+
// The theme values stay `var(--elevation-*)` so a themed page can retune a
|
|
26
|
+
// step at runtime. `none` and `DEFAULT` are Tailwind key names, not tokens:
|
|
27
|
+
// `none` has no variable and `DEFAULT` shares the `base` one.
|
|
28
|
+
function buildBoxShadowConfig() {
|
|
29
|
+
const out = {}
|
|
30
|
+
for (const key of Object.keys(shadowTokens)) {
|
|
31
|
+
if (key === 'none') out[key] = 'none'
|
|
32
|
+
else if (key === 'DEFAULT') out[key] = 'var(--elevation-base)'
|
|
33
|
+
else out[key] = `var(--elevation-${key})`
|
|
30
34
|
}
|
|
31
|
-
return
|
|
35
|
+
return out
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
// `--radius-{key}` is emitted for every token by tokens.js#cssVariables, and
|
|
39
|
+
// `borderRadius` consumes those vars, so `rounded-4` and `--radius-4` stay in
|
|
40
|
+
// sync by construction.
|
|
41
|
+
//
|
|
34
42
|
// Each value carries a trailing `/* {px} */` comment so editor tooling
|
|
35
43
|
// (Tailwind IntelliSense) surfaces the resolved px on hover, instead of
|
|
36
44
|
// the opaque `var(--radius-*)` reference. No `DEFAULT` key: the bare
|
|
37
|
-
// `rounded` utility no longer exists
|
|
45
|
+
// `rounded` utility no longer exists, so write `rounded-4`.
|
|
38
46
|
function buildRadiusConfig() {
|
|
39
47
|
const out = {}
|
|
40
48
|
for (const [key, value] of Object.entries(radiusTokens)) {
|
|
@@ -43,18 +51,6 @@ function buildRadiusConfig() {
|
|
|
43
51
|
return out
|
|
44
52
|
}
|
|
45
53
|
|
|
46
|
-
// Merge two `{ selector: { var: value } }` objects into one, preserving any
|
|
47
|
-
// vars already declared under the same selector.
|
|
48
|
-
function mergeVariableLayers(...layers) {
|
|
49
|
-
const out = {}
|
|
50
|
-
for (const layer of layers) {
|
|
51
|
-
for (const [selector, vars] of Object.entries(layer)) {
|
|
52
|
-
out[selector] = { ...(out[selector] || {}), ...vars }
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return out
|
|
56
|
-
}
|
|
57
|
-
|
|
58
54
|
// Weight variants are exposed as component classes — `text-<size>-<weight>` and
|
|
59
55
|
// the paragraph counterpart `text-p-<size>-<weight>` — one per Figma named
|
|
60
56
|
// style. Figma tracks each weight differently per size and CSS letter-spacing
|
|
@@ -65,28 +61,21 @@ function mergeVariableLayers(...layers) {
|
|
|
65
61
|
// were corrupt export data.
|
|
66
62
|
const WEIGHT_VARIANTS = ['medium', 'semibold', 'bold']
|
|
67
63
|
|
|
64
|
+
// tokens.js holds each style as a plain object — it does not speak Tailwind.
|
|
65
|
+
// Tailwind's `fontSize` theme wants the `[size, meta]` tuple. Convert here.
|
|
66
|
+
// Both families are already present: `base` is the text style, `p-base` the
|
|
67
|
+
// paragraph one (same size, looser line-height, its own letter-spacing).
|
|
68
68
|
function buildFontSize() {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// Paragraph variants (`text-p-<size>`): same size, the paragraph style's
|
|
76
|
-
// looser line-height and its own letter-spacing.
|
|
77
|
-
for (const [key, p] of Object.entries(typographyTokens.paragraph || {})) {
|
|
78
|
-
if (!out[key]) continue
|
|
79
|
-
const [size, meta] = out[key]
|
|
80
|
-
out[`p-${key}`] = [
|
|
81
|
-
size,
|
|
82
|
-
{ ...meta, lineHeight: p.lineHeight, letterSpacing: p.letterSpacing },
|
|
83
|
-
]
|
|
84
|
-
}
|
|
85
|
-
return out
|
|
69
|
+
return Object.fromEntries(
|
|
70
|
+
Object.entries(fontSizeTokens).map(([key, { fontSize, ...meta }]) => [
|
|
71
|
+
key,
|
|
72
|
+
[fontSize, meta],
|
|
73
|
+
]),
|
|
74
|
+
)
|
|
86
75
|
}
|
|
87
76
|
|
|
88
77
|
// Focus ring utilities backed by `--focus-outline-*` CSS vars (theme-flipped
|
|
89
|
-
// in
|
|
78
|
+
// in tokens.js#cssVariables). Implemented as `outline`, not
|
|
90
79
|
// box-shadow, so rings never collide with shadow/ring utilities on the same
|
|
91
80
|
// element and survive forced-colors mode. The default ring is applied
|
|
92
81
|
// globally via `:focus-visible` (see globalStyles); these utilities are for
|
|
@@ -95,7 +84,7 @@ function buildFontSize() {
|
|
|
95
84
|
// Tailwind IntelliSense picks them up.
|
|
96
85
|
function buildFocusRingUtilities() {
|
|
97
86
|
const out = {}
|
|
98
|
-
for (const name of Object.keys(
|
|
87
|
+
for (const name of Object.keys(focusRingTokens.light)) {
|
|
99
88
|
const className = name === 'default' ? '.focus-ring' : `.focus-ring-${name}`
|
|
100
89
|
out[className] = {
|
|
101
90
|
outline: `var(--focus-outline-${name})`,
|
|
@@ -107,41 +96,39 @@ function buildFocusRingUtilities() {
|
|
|
107
96
|
|
|
108
97
|
function buildTextStyleUtilities() {
|
|
109
98
|
const out = {}
|
|
110
|
-
const t = typographyTokens
|
|
111
99
|
const groups = [
|
|
112
100
|
{
|
|
113
101
|
className: (s, w) => `.text-${s}-${w}`,
|
|
114
|
-
tracking:
|
|
115
|
-
|
|
102
|
+
tracking: trackingTokens?.text || {},
|
|
103
|
+
style: (s) => fontSizeTokens[s],
|
|
116
104
|
},
|
|
117
105
|
{
|
|
118
106
|
className: (s, w) => `.text-p-${s}-${w}`,
|
|
119
|
-
tracking:
|
|
120
|
-
|
|
107
|
+
tracking: trackingTokens?.paragraph || {},
|
|
108
|
+
style: (s) => fontSizeTokens[`p-${s}`],
|
|
121
109
|
},
|
|
122
110
|
]
|
|
123
111
|
for (const group of groups) {
|
|
124
112
|
for (const [size, byWeight] of Object.entries(group.tracking)) {
|
|
125
|
-
const
|
|
126
|
-
if (!
|
|
127
|
-
const
|
|
128
|
-
const lineHeight = group.lineHeight(size)
|
|
129
|
-
const transform = t.textTransform?.[size]
|
|
113
|
+
const style = group.style(size)
|
|
114
|
+
if (!style) continue
|
|
115
|
+
const transform = textTransformTokens?.[size]
|
|
130
116
|
for (const weight of WEIGHT_VARIANTS) {
|
|
131
117
|
if (!(weight in byWeight)) continue
|
|
132
118
|
out[group.className(size, weight)] = {
|
|
133
|
-
fontSize,
|
|
134
|
-
lineHeight,
|
|
135
|
-
fontWeight: String(
|
|
119
|
+
fontSize: style.fontSize,
|
|
120
|
+
lineHeight: style.lineHeight,
|
|
121
|
+
fontWeight: String(fontWeightTokens[weight]),
|
|
136
122
|
letterSpacing: byWeight[weight],
|
|
137
123
|
...(transform ? { textTransform: transform } : {}),
|
|
138
124
|
}
|
|
139
125
|
}
|
|
140
126
|
}
|
|
141
127
|
}
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
|
|
128
|
+
// An uppercase eyebrow style needs the text-transform on its bare regular
|
|
129
|
+
// utility too — Tailwind's fontSize tuple can't express one. Empty today:
|
|
130
|
+
// `tiny`, the only such style, was dropped in #940.
|
|
131
|
+
for (const [size, transform] of Object.entries(textTransformTokens || {})) {
|
|
145
132
|
out[`.text-${size}`] = {
|
|
146
133
|
...(out[`.text-${size}`] || {}),
|
|
147
134
|
textTransform: transform,
|
|
@@ -213,7 +200,13 @@ let darkCheckboxStyles = {
|
|
|
213
200
|
|
|
214
201
|
export default plugin(
|
|
215
202
|
function ({ addBase, addComponents, matchUtilities, theme }) {
|
|
216
|
-
|
|
203
|
+
// tokens.js keys the variables by theme; the selector each theme lands on
|
|
204
|
+
// is a Tailwind concern, so it is decided here.
|
|
205
|
+
addBase({
|
|
206
|
+
...globalStyles(theme),
|
|
207
|
+
':root': cssVariables.light,
|
|
208
|
+
'[data-theme="dark"]': cssVariables.dark,
|
|
209
|
+
})
|
|
217
210
|
// Resolves <List :columns="{ base, md, … }"> against this app's own
|
|
218
211
|
// breakpoints, so list tracks and `md:hidden` cells switch at the same
|
|
219
212
|
// width. See tailwind/listColumns.js.
|
|
@@ -237,28 +230,14 @@ export default plugin(
|
|
|
237
230
|
theme: {
|
|
238
231
|
colors: colorPalette,
|
|
239
232
|
borderRadius: buildRadiusConfig(),
|
|
240
|
-
boxShadow:
|
|
241
|
-
none: 'none',
|
|
242
|
-
sm: 'var(--elevation-sm)',
|
|
243
|
-
base: 'var(--elevation-base)',
|
|
244
|
-
DEFAULT: 'var(--elevation-base)',
|
|
245
|
-
md: 'var(--elevation-md)',
|
|
246
|
-
lg: 'var(--elevation-lg)',
|
|
247
|
-
xl: 'var(--elevation-xl)',
|
|
248
|
-
'2xl': 'var(--elevation-2xl)',
|
|
249
|
-
},
|
|
233
|
+
boxShadow: buildBoxShadowConfig(),
|
|
250
234
|
container: {
|
|
251
235
|
padding: {
|
|
252
236
|
xl: '5rem',
|
|
253
237
|
},
|
|
254
238
|
},
|
|
255
239
|
fontSize: buildFontSize(),
|
|
256
|
-
screens:
|
|
257
|
-
sm: '640px',
|
|
258
|
-
md: '768px',
|
|
259
|
-
lg: '1024px',
|
|
260
|
-
xl: '1280px',
|
|
261
|
-
},
|
|
240
|
+
screens: screenTokens,
|
|
262
241
|
extend: {
|
|
263
242
|
textColor: {
|
|
264
243
|
ink: semanticColors.ink,
|
package/tailwind/preset.js
CHANGED
|
@@ -2,6 +2,7 @@ import themePlugin from './plugin.js'
|
|
|
2
2
|
import forms from '@tailwindcss/forms'
|
|
3
3
|
import typography from '@tailwindcss/typography'
|
|
4
4
|
import lucideIconsPlugin from './lucideIconsPlugin.js'
|
|
5
|
+
import { spacing } from './tokens.js'
|
|
5
6
|
|
|
6
7
|
// NOTE: Tailwind v3 does NOT merge `content` from presets into the resolved
|
|
7
8
|
// config (it reads only the top-level config's content.files). So consuming
|
|
@@ -9,26 +10,6 @@ import lucideIconsPlugin from './lucideIconsPlugin.js'
|
|
|
9
10
|
// `content` — declaring them here would be silently ignored. Spread the
|
|
10
11
|
// `content` export from `frappe-ui/tailwind` (see content.js) instead of
|
|
11
12
|
// hand-maintaining the glob list; see docs/content/docs/foundations/tailwind.md.
|
|
12
|
-
// Stock Tailwind's numeric spacing scale has gaps above 12 (13, 15, 17, 18,
|
|
13
|
-
// 19, 21… are undefined), so `h-17` / `size-17` silently don't compile. Fill
|
|
14
|
-
// every integer 1–128 plus every half step 0.5–19.5, all at the canonical
|
|
15
|
-
// 0.25rem step. Values match Tailwind's own formula, so overriding the
|
|
16
|
-
// already-defined keys is a no-op; the win is the in-between steps. Presets DO
|
|
17
|
-
// merge `theme` (unlike `content`), so this reaches consuming apps
|
|
18
|
-
// automatically.
|
|
19
|
-
//
|
|
20
|
-
// This is the ONE place sizing is declared. Tailwind 3.4 reads `theme('spacing')`
|
|
21
|
-
// for width, height, size, minWidth, maxWidth, minHeight and maxHeight, which
|
|
22
|
-
// is why the peer range starts at 3.4 and why the plugin declares no sizing
|
|
23
|
-
// blocks of its own.
|
|
24
|
-
const spacing = Object.fromEntries(
|
|
25
|
-
[
|
|
26
|
-
...Array.from({ length: 20 }, (_, i) => i + 0.5),
|
|
27
|
-
...Array.from({ length: 128 }, (_, i) => i + 1),
|
|
28
|
-
]
|
|
29
|
-
.sort((a, b) => a - b)
|
|
30
|
-
.map((n) => [n, `${n * 0.25}rem`]),
|
|
31
|
-
)
|
|
32
13
|
|
|
33
14
|
/** @type {import('tailwindcss').Config} */
|
|
34
15
|
export default {
|
|
@@ -41,6 +22,8 @@ export default {
|
|
|
41
22
|
// keeps both.
|
|
42
23
|
future: { hoverOnlyWhenSupported: true },
|
|
43
24
|
theme: {
|
|
25
|
+
// Presets DO merge `theme` (unlike `content`), so the spacing scale
|
|
26
|
+
// declared in tokens.js reaches consuming apps from here.
|
|
44
27
|
extend: {
|
|
45
28
|
spacing,
|
|
46
29
|
},
|