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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-ui",
3
- "version": "1.0.0-beta.74",
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/figma-tokens-to-theme.js"
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.0 <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.0",
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/generated/colors.json) is
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.
@@ -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 colorsData from './colors.json'
3
- import effectsData from './generated/effects.json'
14
+ import { colors, semanticColors } from './tokens.js'
4
15
 
5
- // Tailwind v3 can only apply the `/<opacity>` modifier (e.g. `bg-blue-900/30`)
6
- // when the color value exposes an alpha slot. Hex did this implicitly; a bare
7
- // `oklch(L C H)` string does not, so the modifier silently fails. Inject the
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.keys(colorsData.lightMode).forEach((color) => {
50
- colorPalette[color] = mapShades(colorsData.lightMode[color])
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
- return null
79
- }
80
-
81
- function generateCSSVariables() {
82
- const output = {
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
- // Generate CSS variables for light mode
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
- const output = Object.fromEntries(
133
- Object.keys(colorsData.themedVariables.light).map((category) => [
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
- // Focus tokens are single-layer `0 0 0 <spread> <color>` shadows — re-express
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 }
@@ -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
- const require = createRequire(import.meta.url)
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(require.resolve('lucide-static/package.json')),
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
- // `figma-tokens-to-theme.js`. Filtered out of the shift tables below so the
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', 'espresso-v2-design-tokens',
669
+ 'node_modules', '.git', 'dist', 'cache', 'generated',
670
670
  ])
671
671
 
672
672
  // Symlinks — directories and files alike — are followed only while their real
@@ -1,40 +1,48 @@
1
1
  import plugin from 'tailwindcss/plugin'
2
+ import { generateColorPalette, generateSemanticColors } from './colorPalette.js'
2
3
  import {
3
- generateColorPalette,
4
- generateSemanticColors,
5
- generateCSSVariables,
6
- generateEffectVariables,
7
- } from './colorPalette.js'
8
- import radiusTokens from './generated/radius.json'
9
- import typographyTokens from './generated/typography.json'
10
- import effectsData from './generated/effects.json'
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
- // Emit `--radius-{key}` for every radius token (the numbered scale plus the
22
- // kept `none` / `full` names — the deprecated size aliases were removed in
23
- // 1.0.0 per ADR-0006, #998) so the values are inspectable as real CSS
24
- // variables. `borderRadius` is rewired below to consume these vars, so
25
- // `rounded-4` and `--radius-4` stay in sync.
26
- function generateRadiusVariables() {
27
- const vars = {}
28
- for (const [key, value] of Object.entries(radiusTokens)) {
29
- vars[`--radius-${key}`] = value
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 { ':root': vars }
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 — use `rounded-4`.
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
- const out = {}
70
- // Each size's regular variant already carries lineHeight, letterSpacing and
71
- // fontWeight from the text-styles export (see figma-tokens-to-theme.js).
72
- for (const [key, [size, meta]] of Object.entries(typographyTokens.fontSize)) {
73
- out[key] = [size, { ...meta }]
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 colorPalette.js#generateEffectVariables). Implemented as `outline`, not
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(effectsData.focus.light)) {
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: t.tracking?.text || {},
115
- lineHeight: (s) => t.fontSize[s]?.[1].lineHeight,
102
+ tracking: trackingTokens?.text || {},
103
+ style: (s) => fontSizeTokens[s],
116
104
  },
117
105
  {
118
106
  className: (s, w) => `.text-p-${s}-${w}`,
119
- tracking: t.tracking?.paragraph || {},
120
- lineHeight: (s) => t.paragraph?.[s]?.lineHeight,
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 entry = t.fontSize[size]
126
- if (!entry) continue
127
- const [fontSize] = entry
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(t.fontWeight[weight]),
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
- // `tiny` is an uppercase eyebrow style; the bare regular utility needs the
143
- // text-transform too (Tailwind's fontSize tuple can't express it).
144
- for (const [size, transform] of Object.entries(t.textTransform || {})) {
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
- addBase({ ...globalStyles(theme), ...cssVariables })
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,
@@ -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
  },