@wix/origami-transforms 0.1.28 → 0.1.30
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/dist/cssPlugins/componentPosition.d.ts +6 -2
- package/dist/cssPlugins/customEnums.d.ts +5 -2
- package/dist/cssPlugins/customMedia.d.ts +7 -2
- package/dist/cssPlugins/fonts.d.ts +6 -2
- package/dist/cssPlugins/index.d.ts +2 -2
- package/dist/cssPlugins/layout/index.d.ts +7 -2
- package/dist/cssPlugins/mixins.d.ts +7 -2
- package/dist/cssPlugins/presets.d.ts +6 -2
- package/dist/cssPlugins/shapeDividers.d.ts +6 -2
- package/dist/cssPlugins/spx/calls.d.ts +3 -0
- package/dist/cssPlugins/spx/index.d.ts +7 -2
- package/dist/cssPlugins/spxThemeVariables/index.d.ts +6 -0
- package/dist/cssPlugins/spxThemeVariables/utils/breakpointUtils.d.ts +23 -0
- package/dist/cssPlugins/spxThemeVariables/utils/copyPlanUtils.d.ts +14 -0
- package/dist/cssPlugins/spxThemeVariables/utils/cssTreeUtils.d.ts +10 -0
- package/dist/cssPlugins/spxThemeVariables/utils/restateUtils.d.ts +9 -0
- package/dist/cssPlugins/spxThemeVariables/utils/sectionUtils.d.ts +6 -0
- package/dist/cssPlugins/spxThemeVariables/utils/stylesheetUtils.d.ts +10 -0
- package/dist/cssPlugins/spxThemeVariables/utils/themeUtils.d.ts +6 -0
- package/dist/cssPlugins/spxThemeVariables/utils/wrappingAtRuleUtils.d.ts +10 -0
- package/dist/cssPlugins/spxThemeVariables/utils/writeUtils.d.ts +17 -0
- package/dist/cssPlugins/stackChildren.d.ts +5 -2
- package/dist/cssPlugins/wixVectorUrls.d.ts +6 -2
- package/dist/jsxPlugins/index.d.ts +2 -2
- package/dist/jsxPlugins/mediaValidation/index.d.ts +5 -2
- package/dist/jsxPlugins/origamiCompatibility/index.d.ts +6 -2
- package/dist/jsxPlugins/pageSlots.d.ts +7 -6
- package/dist/jsxPlugins/validation/checks.d.ts +7 -7
- package/dist/jsxPlugins/validation/index.d.ts +5 -2
- package/dist/jsxPlugins/validation/rules.d.ts +25 -0
- package/dist/transform/bi.d.ts +9 -0
- package/dist/transform/layoutError.d.ts +7 -4
- package/dist/transform/pipeline.d.ts +2 -2
- package/dist/transform/types.d.ts +11 -11
- package/package.json +3 -3
- package/src/cssPlugins/componentPosition.js +5 -1
- package/src/cssPlugins/customEnums.js +17 -11
- package/src/cssPlugins/customMedia.js +10 -3
- package/src/cssPlugins/fonts.js +5 -1
- package/src/cssPlugins/index.js +3 -1
- package/src/cssPlugins/layout/index.js +8 -1
- package/src/cssPlugins/layout/parser.js +15 -14
- package/src/cssPlugins/mixins.js +13 -6
- package/src/cssPlugins/presets.js +5 -1
- package/src/cssPlugins/shapeDividers.js +5 -1
- package/src/cssPlugins/spx/README.md +12 -3
- package/src/cssPlugins/spx/calls.js +28 -1
- package/src/cssPlugins/spx/cascade.js +26 -6
- package/src/cssPlugins/spx/index.js +8 -1
- package/src/cssPlugins/spxThemeVariables/README.md +14 -0
- package/src/cssPlugins/spxThemeVariables/index.js +36 -0
- package/src/cssPlugins/spxThemeVariables/utils/breakpointUtils.js +126 -0
- package/src/cssPlugins/spxThemeVariables/utils/copyPlanUtils.js +162 -0
- package/src/cssPlugins/spxThemeVariables/utils/cssTreeUtils.js +58 -0
- package/src/cssPlugins/spxThemeVariables/utils/restateUtils.js +134 -0
- package/src/cssPlugins/spxThemeVariables/utils/sectionUtils.js +32 -0
- package/src/cssPlugins/spxThemeVariables/utils/stylesheetUtils.js +98 -0
- package/src/cssPlugins/spxThemeVariables/utils/themeUtils.js +90 -0
- package/src/cssPlugins/spxThemeVariables/utils/wrappingAtRuleUtils.js +68 -0
- package/src/cssPlugins/spxThemeVariables/utils/writeUtils.js +98 -0
- package/src/cssPlugins/stackChildren.js +6 -2
- package/src/cssPlugins/wixVectorUrls.js +5 -1
- package/src/jsxPlugins/index.js +1 -1
- package/src/jsxPlugins/mediaValidation/index.js +10 -5
- package/src/jsxPlugins/origamiCompatibility/index.js +5 -1
- package/src/jsxPlugins/pageSlots.js +6 -1
- package/src/jsxPlugins/validation/checks.js +15 -8
- package/src/jsxPlugins/validation/index.js +9 -3
- package/src/jsxPlugins/validation/rules.js +14 -0
- package/src/transform/bi.js +30 -0
- package/src/transform/css/runPlugins.js +5 -7
- package/src/transform/jsx/runPlugins.js +3 -6
- package/src/transform/layoutError.js +7 -3
- package/src/transform/pipeline.js +2 -2
- package/src/transform/types.js +28 -6
package/src/cssPlugins/fonts.js
CHANGED
|
@@ -49,8 +49,12 @@ const fontIdsOf = declaration => {
|
|
|
49
49
|
return family.split(',').map(name => name.trim().replace(/^(['"])(.*)\1$/, '$2').toLowerCase())
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/** @type {
|
|
52
|
+
const checks = /** @type {const} */ ([])
|
|
53
|
+
/** @typedef {typeof checks[number]} Check */
|
|
54
|
+
|
|
55
|
+
/** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
|
|
53
56
|
export const fontsPlugin = {
|
|
57
|
+
checks,
|
|
54
58
|
name: 'fonts',
|
|
55
59
|
// Reads the declarations as authored, before spx rewrites the sizes the family follows.
|
|
56
60
|
order: 5,
|
package/src/cssPlugins/index.js
CHANGED
|
@@ -26,10 +26,11 @@ import { presetsPlugin } from './presets.js'
|
|
|
26
26
|
import { shapeDividersPlugin } from './shapeDividers.js'
|
|
27
27
|
import { layoutPlugin } from './layout/index.js'
|
|
28
28
|
import { spxPlugin } from './spx/index.js'
|
|
29
|
+
import { spxThemeVariablesPlugin } from './spxThemeVariables/index.js'
|
|
29
30
|
import { stackChildrenPlugin } from './stackChildren.js'
|
|
30
31
|
import { wixVectorUrlsPlugin } from './wixVectorUrls.js'
|
|
31
32
|
|
|
32
|
-
/** @type {readonly import('../transform/types.js').CssTransformPlugin[]} */
|
|
33
|
+
/** @type {readonly import('../transform/types.js').CssTransformPlugin<string>[]} */
|
|
33
34
|
export const cssPlugins = Object.freeze([
|
|
34
35
|
customMediaPlugin,
|
|
35
36
|
fontsPlugin,
|
|
@@ -39,6 +40,7 @@ export const cssPlugins = Object.freeze([
|
|
|
39
40
|
presetsPlugin,
|
|
40
41
|
componentPositionPlugin,
|
|
41
42
|
layoutPlugin,
|
|
43
|
+
spxThemeVariablesPlugin,
|
|
42
44
|
spxPlugin,
|
|
43
45
|
shapeDividersPlugin,
|
|
44
46
|
wixVectorUrlsPlugin,
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { compileContainers } from './compiler.js'
|
|
2
2
|
import { parseTargets } from './parser.js'
|
|
3
3
|
|
|
4
|
-
/** @type {
|
|
4
|
+
export const checks = /** @type {const} */ (['unknown_target', 'target_requires_component_id', 'invalid_target_envelope', 'target_not_site_layout', 'duplicate_target', 'target_requires_declarations', 'duplicate_target_property', 'property_wrong_target', 'content_position_not_relative', 'unsupported_overflow', 'unsupported_content_alignment', 'inconsistent_container_display', 'duplicate_root_property', 'property_requires_target'])
|
|
5
|
+
/** @typedef {typeof checks[number]} Check */
|
|
6
|
+
|
|
7
|
+
/** @template {Error} E @param {Check} check @param {E} error */
|
|
8
|
+
export const fail = (check, error) => Object.assign(error, { check })
|
|
9
|
+
|
|
10
|
+
/** @type {import('../../transform/types.js').CssTransformPlugin<Check>} */
|
|
5
11
|
export const layoutPlugin = {
|
|
12
|
+
checks,
|
|
6
13
|
name: 'layout',
|
|
7
14
|
order: 15,
|
|
8
15
|
validate: (root, context) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fail } from './index.js'
|
|
1
2
|
import { basename } from 'node:path'
|
|
2
3
|
import { isValidContainerId } from '@wix/origami-system-components/layout-contract'
|
|
3
4
|
import { relativeToSite } from '../../transform/relativeToSite.js'
|
|
@@ -20,7 +21,7 @@ export const parseTargets = (root, context) => {
|
|
|
20
21
|
const owners = new Map()
|
|
21
22
|
root.walkAtRules('origami-target', target => {
|
|
22
23
|
const name = target.params.trim()
|
|
23
|
-
if (!Object.hasOwn(targets, name)) throw target.error(`Unknown @origami-target ${name}`)
|
|
24
|
+
if (!Object.hasOwn(targets, name)) throw fail('unknown_target', target.error(`Unknown @origami-target ${name}`))
|
|
24
25
|
const owner = target.parent
|
|
25
26
|
// DM writes component rules as .compId; #compId is still accepted.
|
|
26
27
|
if (
|
|
@@ -28,34 +29,34 @@ export const parseTargets = (root, context) => {
|
|
|
28
29
|
!['.', '#'].includes(owner.selector[0]) ||
|
|
29
30
|
!isValidContainerId(owner.selector.slice(1))
|
|
30
31
|
) {
|
|
31
|
-
throw target.error('@origami-target requires a single component ID rule')
|
|
32
|
+
throw fail('target_requires_component_id', target.error('@origami-target requires a single component ID rule'))
|
|
32
33
|
}
|
|
33
34
|
for (let ancestor = owner.parent; ancestor && ancestor.type !== 'root'; ancestor = ancestor.parent) {
|
|
34
35
|
if (
|
|
35
36
|
ancestor.type !== 'atrule' ||
|
|
36
37
|
!['scope', 'media', 'layer', 'supports', 'container'].includes(ancestor.name)
|
|
37
38
|
) {
|
|
38
|
-
throw target.error('@origami-target is only allowed inside layout rule envelopes')
|
|
39
|
+
throw fail('invalid_target_envelope', target.error('@origami-target is only allowed inside layout rule envelopes'))
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
if (!isLayoutSource(target, context))
|
|
42
|
-
throw target.error('@origami-target must originate in site-owned layout.css')
|
|
43
|
+
throw fail('target_not_site_layout', target.error('@origami-target must originate in site-owned layout.css'))
|
|
43
44
|
const siblings = owners.get(owner) ?? []
|
|
44
|
-
if (siblings.some(sibling => sibling.params.trim() === name)) throw target.error(`Duplicate target ${name}`)
|
|
45
|
+
if (siblings.some(sibling => sibling.params.trim() === name)) throw fail('duplicate_target', target.error(`Duplicate target ${name}`))
|
|
45
46
|
const seen = new Set()
|
|
46
47
|
for (const node of target.nodes ?? []) {
|
|
47
48
|
if (node.type === 'comment') continue
|
|
48
|
-
if (node.type !== 'decl') throw node.error('Target bodies must contain declarations only')
|
|
49
|
-
if (seen.has(node.prop)) throw node.error(`Duplicate target property ${node.prop}`)
|
|
49
|
+
if (node.type !== 'decl') throw fail('target_requires_declarations', node.error('Target bodies must contain declarations only'))
|
|
50
|
+
if (seen.has(node.prop)) throw fail('duplicate_target_property', node.error(`Duplicate target property ${node.prop}`))
|
|
50
51
|
seen.add(node.prop)
|
|
51
|
-
if (!targets[name].has(node.prop)) throw node.error(`${node.prop} does not belong to ${name}`)
|
|
52
|
+
if (!targets[name].has(node.prop)) throw fail('property_wrong_target', node.error(`${node.prop} does not belong to ${name}`))
|
|
52
53
|
if (node.prop === 'position' && node.value !== 'relative')
|
|
53
|
-
throw node.error('Content position must be relative')
|
|
54
|
+
throw fail('content_position_not_relative', node.error('Content position must be relative'))
|
|
54
55
|
if (name === OVERFLOW && !['auto', 'scroll', 'hidden', 'visible', 'clip'].includes(node.value)) {
|
|
55
|
-
throw node.error(`Unsupported overflow value ${node.value}`)
|
|
56
|
+
throw fail('unsupported_overflow', node.error(`Unsupported overflow value ${node.value}`))
|
|
56
57
|
}
|
|
57
58
|
if (node.prop === '--content-max-width-align' && !['left', 'center', 'right'].includes(node.value)) {
|
|
58
|
-
throw node.error(`Unsupported content alignment ${node.value}`)
|
|
59
|
+
throw fail('unsupported_content_alignment', node.error(`Unsupported content alignment ${node.value}`))
|
|
59
60
|
}
|
|
60
61
|
if (node.prop === 'display') {
|
|
61
62
|
const type = owner.nodes.find(item => item.type === 'decl' && item.prop === '--container-layout-type')
|
|
@@ -64,7 +65,7 @@ export const parseTargets = (root, context) => {
|
|
|
64
65
|
(type?.type === 'decl' &&
|
|
65
66
|
(!Object.hasOwn(displays, type.value) || displays[type.value] !== node.value))
|
|
66
67
|
) {
|
|
67
|
-
throw node.error('Container type and content display are inconsistent')
|
|
68
|
+
throw fail('inconsistent_container_display', node.error('Container type and content display are inconsistent'))
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
}
|
|
@@ -75,10 +76,10 @@ export const parseTargets = (root, context) => {
|
|
|
75
76
|
const seen = new Set()
|
|
76
77
|
for (const node of owner.nodes) {
|
|
77
78
|
if (node.type !== 'decl') continue
|
|
78
|
-
if (seen.has(node.prop)) throw node.error(`Duplicate root property ${node.prop}`)
|
|
79
|
+
if (seen.has(node.prop)) throw fail('duplicate_root_property', node.error(`Duplicate root property ${node.prop}`))
|
|
79
80
|
seen.add(node.prop)
|
|
80
81
|
if ((targets[CONTENT].has(node.prop) && node.prop !== 'position') || targets[OVERFLOW].has(node.prop)) {
|
|
81
|
-
throw node.error(`${node.prop} belongs inside @origami-target`)
|
|
82
|
+
throw fail('property_requires_target', node.error(`${node.prop} belongs inside @origami-target`))
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
}
|
package/src/cssPlugins/mixins.js
CHANGED
|
@@ -7,8 +7,15 @@ const MAX_CLONED_BYTES = 10 * 1024 * 1024
|
|
|
7
7
|
/** @param {import('postcss').AtRule} atRule */
|
|
8
8
|
const mixinName = atRule => atRule.params.trim().replace(/\(\)$/, '')
|
|
9
9
|
|
|
10
|
-
/** @type {
|
|
10
|
+
export const checks = /** @type {const} */ (['missing_mixin', 'cyclic_apply', 'expansion_depth_exceeded', 'cloned_node_budget_exceeded', 'cloned_css_budget_exceeded'])
|
|
11
|
+
/** @typedef {typeof checks[number]} Check */
|
|
12
|
+
|
|
13
|
+
/** @template {Error} E @param {Check} check @param {E} error */
|
|
14
|
+
export const fail = (check, error) => Object.assign(error, { check })
|
|
15
|
+
|
|
16
|
+
/** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
|
|
11
17
|
export const mixinsPlugin = {
|
|
18
|
+
checks,
|
|
12
19
|
name: 'mixins',
|
|
13
20
|
order: 10,
|
|
14
21
|
transform(root) {
|
|
@@ -30,16 +37,16 @@ export const mixinsPlugin = {
|
|
|
30
37
|
container.walkAtRules('apply', atRule => {
|
|
31
38
|
const name = mixinName(atRule)
|
|
32
39
|
const body = bodies.get(name)
|
|
33
|
-
if (!body) throw new Error(`@apply ${atRule.params} has no @mixin definition`)
|
|
34
|
-
if (stack.includes(name)) throw new Error(`Cyclic @apply: ${[...stack, name].join(' -> ')}`)
|
|
40
|
+
if (!body) throw fail('missing_mixin', new Error(`@apply ${atRule.params} has no @mixin definition`))
|
|
41
|
+
if (stack.includes(name)) throw fail('cyclic_apply', new Error(`Cyclic @apply: ${[...stack, name].join(' -> ')}`))
|
|
35
42
|
if (stack.length >= MAX_EXPANSION_DEPTH) {
|
|
36
|
-
throw new Error(`@apply ${name} exceeds maximum expansion depth (${MAX_EXPANSION_DEPTH})`)
|
|
43
|
+
throw fail('expansion_depth_exceeded', new Error(`@apply ${name} exceeds maximum expansion depth (${MAX_EXPANSION_DEPTH})`))
|
|
37
44
|
}
|
|
38
45
|
const pending = [...body]
|
|
39
46
|
while (pending.length) {
|
|
40
47
|
const node = pending.pop()
|
|
41
48
|
if (++clonedNodes > MAX_CLONED_NODES) {
|
|
42
|
-
throw new Error(`@apply ${name} exceeds stylesheet cloned-node budget (${MAX_CLONED_NODES})`)
|
|
49
|
+
throw fail('cloned_node_budget_exceeded', new Error(`@apply ${name} exceeds stylesheet cloned-node budget (${MAX_CLONED_NODES})`))
|
|
43
50
|
}
|
|
44
51
|
if (node && 'nodes' in node && node.nodes) {
|
|
45
52
|
for (const child of node.nodes) pending.push(child)
|
|
@@ -48,7 +55,7 @@ export const mixinsPlugin = {
|
|
|
48
55
|
for (const node of body) {
|
|
49
56
|
clonedBytes += Buffer.byteLength((node.raws.before ?? '') + node.toString(), 'utf8')
|
|
50
57
|
if (clonedBytes > MAX_CLONED_BYTES) {
|
|
51
|
-
throw new Error(`@apply ${name} exceeds stylesheet cloned-CSS byte budget (${MAX_CLONED_BYTES})`)
|
|
58
|
+
throw fail('cloned_css_budget_exceeded', new Error(`@apply ${name} exceeds stylesheet cloned-CSS byte budget (${MAX_CLONED_BYTES})`))
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
61
|
// PostCSS does not revisit nodes inserted in place of the current at-rule.
|
|
@@ -23,8 +23,12 @@ const target = (selector, key) => {
|
|
|
23
23
|
return selector === first ? `.${id}${WRAPPER}` : null
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/** @type {
|
|
26
|
+
const checks = /** @type {const} */ ([])
|
|
27
|
+
/** @typedef {typeof checks[number]} Check */
|
|
28
|
+
|
|
29
|
+
/** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
|
|
27
30
|
export const presetsPlugin = {
|
|
31
|
+
checks,
|
|
28
32
|
name: 'presets',
|
|
29
33
|
order: 13,
|
|
30
34
|
transform(root, context) {
|
|
@@ -101,8 +101,12 @@ const layerDeclarations = (compId, vars) => {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
/** @type {
|
|
104
|
+
const checks = /** @type {const} */ ([])
|
|
105
|
+
/** @typedef {typeof checks[number]} Check */
|
|
106
|
+
|
|
107
|
+
/** @type {import('../transform/types.js').CssTransformPlugin<Check>} */
|
|
105
108
|
export const shapeDividersPlugin = {
|
|
109
|
+
checks,
|
|
106
110
|
name: 'shape-dividers',
|
|
107
111
|
// Last, with wix-vector-urls: it only adds rules, which no other plugin needs to see, and it reads only `--height` as
|
|
108
112
|
// `var(--height)`, so spx converts that var's `--spx()` where it is declared.
|
|
@@ -64,17 +64,26 @@ When settings are absent, expressions fall back to reference width 1280, scale m
|
|
|
64
64
|
viewport width 100vw, scrollbar width 0px, stoppers 0px / 99999px and no cap. Nested components inherit settings unless
|
|
65
65
|
they override them. As with any CSS custom property, an authored variable containing an
|
|
66
66
|
SPX expression resolves where the variable is declared; inheriting the variable does not
|
|
67
|
-
re-evaluate it using a child's settings.
|
|
67
|
+
re-evaluate it using a child's settings. So `../spxThemeVariables/` copies the SPX-dependent theme
|
|
68
|
+
variables onto capped containers.
|
|
69
|
+
|
|
70
|
+
## Theme calls
|
|
71
|
+
|
|
72
|
+
`../spxThemeVariables/` rewrites each theme `--spx(N)` as `--theme-spx(N, W)`, W being the theme's
|
|
73
|
+
reference width; authors never write it. It compiles like `--spx(N)` but divides by the literal W
|
|
74
|
+
instead of `var(--spx-ref-width, 1280)`, so a copied theme value keeps its theme size while still
|
|
75
|
+
following the section's cap and scaling factor. N must be a literal number and W a positive one.
|
|
68
76
|
|
|
69
77
|
## Files and boundaries
|
|
70
78
|
|
|
71
79
|
- `index.js` removes the SPX function definition and visits declarations.
|
|
72
80
|
- `cascade.js` translates resolver settings and generates browser-evaluated expressions.
|
|
73
|
-
- `calls.js` parses literal numeric
|
|
81
|
+
- `calls.js` parses literal numeric `--spx()` and `--theme-spx()` calls and rejects unsupported arguments.
|
|
74
82
|
|
|
75
83
|
Reference widths must be positive numbers and resolver modes must be `scale` or `fixed`.
|
|
76
84
|
Stoppers and `--content-max-width` are not validated.
|
|
77
85
|
|
|
78
86
|
`test/spx.browser.mjs` checks computed styles for the button example, nested overrides,
|
|
79
|
-
settings in separate stylesheets, cascade priority, fixed fractions, signs, zero, stoppers
|
|
87
|
+
settings in separate stylesheets, cascade priority, fixed fractions, signs, zero, stoppers, cap
|
|
88
|
+
and theme copies on capped sections.
|
|
80
89
|
Run it together with the container browser checks using the package's `test:browser` script.
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { fail } from './index.js'
|
|
1
2
|
const CALL = /--spx\(\s*(-?(?:\d+\.?\d*|\.\d+))\s*\)/g
|
|
3
|
+
const THEME_CALL = /--theme-spx\(\s*(-?(?:\d+\.?\d*|\.\d+))\s*,\s*(\d+\.?\d*|\.\d+)\s*\)/g
|
|
4
|
+
|
|
5
|
+
export const THEME_SPX = '--theme-spx('
|
|
2
6
|
|
|
3
7
|
/** Replace literal SPX calls, reporting unsupported arguments at their declaration.
|
|
4
8
|
* @param {import('postcss').Declaration} declaration
|
|
@@ -8,5 +12,28 @@ const CALL = /--spx\(\s*(-?(?:\d+\.?\d*|\.\d+))\s*\)/g
|
|
|
8
12
|
export const lowerCalls = (declaration, resolve, invalidCall) => {
|
|
9
13
|
if (!declaration.value.includes('--spx(')) return
|
|
10
14
|
declaration.value = declaration.value.replace(CALL, (_, literal) => resolve(Number(literal)))
|
|
11
|
-
if (declaration.value.includes('--spx(')) throw invalidCall()
|
|
15
|
+
if (declaration.value.includes('--spx(')) throw fail('non_literal_spx_argument', invalidCall())
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {import('postcss').Declaration} declaration
|
|
20
|
+
* @param {(value: number, themeRefWidth: number) => string} resolve
|
|
21
|
+
* @param {() => Error} invalidCall
|
|
22
|
+
*/
|
|
23
|
+
export const lowerThemeCalls = (declaration, resolve, invalidCall) => {
|
|
24
|
+
if (!declaration.value.includes(THEME_SPX)) return
|
|
25
|
+
declaration.value = declaration.value.replace(THEME_CALL, (call, literal, themeRefWidthLiteral) => {
|
|
26
|
+
const themeRefWidth = Number(themeRefWidthLiteral)
|
|
27
|
+
if (themeRefWidth <= 0) return call
|
|
28
|
+
const number = Number(literal)
|
|
29
|
+
return resolve(number, themeRefWidth)
|
|
30
|
+
})
|
|
31
|
+
if (declaration.value.includes(THEME_SPX)) throw invalidCall()
|
|
12
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} value
|
|
36
|
+
* @param {number} themeRefWidth
|
|
37
|
+
*/
|
|
38
|
+
export const toThemeCalls = (value, themeRefWidth) =>
|
|
39
|
+
value.replace(CALL, (_, literal) => `--theme-spx(${literal}, ${themeRefWidth})`)
|
|
@@ -1,22 +1,41 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fail } from './index.js'
|
|
2
|
+
import { lowerCalls, lowerThemeCalls } from './calls.js'
|
|
2
3
|
|
|
3
4
|
// Keep arithmetic at the call site: an inherited derived custom property would already
|
|
4
5
|
// be computed against its parent's settings. Only the numeric resolver switch inherits.
|
|
5
6
|
const SCALE = 'var(--origami-spx-scale, 1)'
|
|
6
7
|
const FLOOR = `calc(0.5px * ${SCALE})`
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} cap
|
|
10
|
+
* @param {string} [divisor]
|
|
11
|
+
*/
|
|
12
|
+
const unitCappedAt = (cap, divisor = 'var(--spx-ref-width, 1280)') =>
|
|
13
|
+
`(${SCALE} * (clamp(var(--spx-stopper-min, 0px), var(--scaling-factor, 100vw), min(var(--spx-stopper-max, 99999px), ${cap})) - var(--scrollbar-width, 0px)) / ${divisor} + (1 - ${SCALE}) * 1px)`
|
|
10
14
|
const UNIT = unitCappedAt('var(--content-max-width, 99999px)')
|
|
11
15
|
const UNCAPPED_UNIT = unitCappedAt('99999px')
|
|
12
16
|
|
|
17
|
+
/** @param {import('postcss').Declaration} declaration */
|
|
18
|
+
const compileThemeCalls = declaration => {
|
|
19
|
+
const cap = declaration.prop === '--content-max-width' ? '99999px' : 'var(--content-max-width, 99999px)'
|
|
20
|
+
lowerThemeCalls(
|
|
21
|
+
declaration,
|
|
22
|
+
(number, themeRefWidth) => {
|
|
23
|
+
if (number === 0) return '0px'
|
|
24
|
+
const divisor = String(themeRefWidth)
|
|
25
|
+
const unit = unitCappedAt(cap, divisor)
|
|
26
|
+
return number < 0 ? `min(calc(-1 * ${FLOOR}), ${number} * ${unit})` : `max(${FLOOR}, ${number} * ${unit})`
|
|
27
|
+
},
|
|
28
|
+
() => declaration.error('SPX calls --theme-spx() without a literal number and a positive literal theme reference width')
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
/** Compile settings and calls in place, retaining their cascade and source locations.
|
|
14
33
|
* @param {import('postcss').Declaration} declaration
|
|
15
34
|
*/
|
|
16
35
|
export const compileCascadeDeclaration = declaration => {
|
|
17
36
|
if (declaration.prop === '--spx-resolver-type') {
|
|
18
37
|
const resolver = declaration.value.trim()
|
|
19
|
-
if (!['scale', 'fixed'].includes(resolver)) throw declaration.error('Unsupported SPX resolver')
|
|
38
|
+
if (!['scale', 'fixed'].includes(resolver)) throw fail('unsupported_spx_resolver', declaration.error('Unsupported SPX resolver'))
|
|
20
39
|
const value = resolver === 'scale' ? '1' : '0'
|
|
21
40
|
const previous = declaration.prev()
|
|
22
41
|
if (
|
|
@@ -32,8 +51,9 @@ export const compileCascadeDeclaration = declaration => {
|
|
|
32
51
|
declaration.prop === '--spx-ref-width' &&
|
|
33
52
|
(!Number.isFinite(Number(declaration.value)) || Number(declaration.value) <= 0)
|
|
34
53
|
) {
|
|
35
|
-
throw declaration.error('SPX reference width must be positive')
|
|
54
|
+
throw fail('invalid_spx_reference_width', declaration.error('SPX reference width must be positive'))
|
|
36
55
|
}
|
|
56
|
+
compileThemeCalls(declaration)
|
|
37
57
|
const unit = declaration.prop === '--content-max-width' ? UNCAPPED_UNIT : UNIT
|
|
38
58
|
lowerCalls(
|
|
39
59
|
declaration,
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { compileCascadeDeclaration } from './cascade.js'
|
|
2
2
|
|
|
3
|
-
/** @type {
|
|
3
|
+
export const checks = /** @type {const} */ (['unsupported_spx_resolver', 'invalid_spx_reference_width', 'non_literal_spx_argument'])
|
|
4
|
+
/** @typedef {typeof checks[number]} Check */
|
|
5
|
+
|
|
6
|
+
/** @template {Error} E @param {Check} check @param {E} error */
|
|
7
|
+
export const fail = (check, error) => Object.assign(error, { check })
|
|
8
|
+
|
|
9
|
+
/** @type {import('../../transform/types.js').CssTransformPlugin<Check>} */
|
|
4
10
|
export const spxPlugin = {
|
|
11
|
+
checks,
|
|
5
12
|
name: 'spx',
|
|
6
13
|
order: 20,
|
|
7
14
|
transform(root) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Theme SPX variables follow each section's settings
|
|
2
|
+
|
|
3
|
+
A custom property resolves `--spx()` where it is declared, so theme variables on `:root` ignore a section's `--content-max-width`. Like Thunderbolt, this plugin re-declares them on each capped section.
|
|
4
|
+
|
|
5
|
+
- **Theme:** the `:root` rules from `src/global/themes/theme.css`, at the top level or under one `(max-width: Npx)` `@media`. Theme variables are their custom properties except the SPX settings. Without a site folder in the context, the plugin does nothing.
|
|
6
|
+
- **Cascade:** at a breakpoint, the theme value of a variable is the one CSS gives `:root` there: among desktop, that breakpoint and every wider one, the declaration that comes last in the stylesheet wins, whatever the widths. So a wider block written after a narrower one overrides it.
|
|
7
|
+
- **Restate:** a theme variable holding `--spx(` that a breakpoint inherits, through the cascade, from a breakpoint with another `--spx-ref-width` is re-declared on that breakpoint's last theme `:root`, widest breakpoint first, so it is tagged with the breakpoint's own reference width, like the `:root` value it had before. When a narrower breakpoint that sets the variable comes before that `:root`, the restatement goes in a new `:root` block of that breakpoint placed just before the narrower one, so the narrower value still wins. `var()` readers are not restated, and neither is a value written after that last `:root`, since the restatement would lose to it.
|
|
8
|
+
- **Tag:** each theme `--spx(N)` becomes `--theme-spx(N, W)`, W being that breakpoint's `--spx-ref-width`, so a section's own reference width never changes it.
|
|
9
|
+
- **Capped:** a section whose desktop rule sets `--content-max-width`.
|
|
10
|
+
- **Copied:** desktop copies every SPX-dependent variable (it holds `--theme-spx(` or reads one through `var()`, at any depth), except `--content-max-width`. Each breakpoint copies the variables whose cascade winner it sets that are SPX-dependent or override a wider copy, plus inherited ones that first become SPX-dependent there. Copies are written widest first, so each breakpoint copy holds the value `:root` has there.
|
|
11
|
+
- **Written:** one `@layer origami-theme-copies` at the top, after any `@charset` and `@import`, so it is the lowest layer: any section rule wins, and a copy still beats the value inherited from `:root`. Inside it, one `:where(#a, #b, …)` rule for desktop, then one `@media` per breakpoint, widest first. Sections inside `@scope`, `@supports` or `@container` get their own list, wrapped in a copy of those wrapping at-rules, so the copies keep the section's condition. Copies drop `!important`. Nothing is written without copies or capped sections.
|
|
12
|
+
- **Order:** 16, after layout (15) and before SPX (20), which compiles `--theme-spx()`.
|
|
13
|
+
|
|
14
|
+
Code: `index.js` runs `stylesheetUtils.js` (scan), `themeUtils.js` (read), `copyPlanUtils.js` (breakpoint order, shared by restate and plan), `restateUtils.js`, `themeUtils.js` (tag), `sectionUtils.js` (with `wrappingAtRuleUtils.js`), `copyPlanUtils.js`, then `writeUtils.js`. Tests: `test/spxThemeVariables.spec.js`.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { planBreakpoints, planThemeCopies } from './utils/copyPlanUtils.js'
|
|
2
|
+
import { readSectionRules } from './utils/sectionUtils.js'
|
|
3
|
+
import { restateThemeVariables } from './utils/restateUtils.js'
|
|
4
|
+
import { scanStylesheet } from './utils/stylesheetUtils.js'
|
|
5
|
+
import { readThemeVariables, tagThemeVariables } from './utils/themeUtils.js'
|
|
6
|
+
import { writeCopies } from './utils/writeUtils.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {import('postcss').Root} root
|
|
10
|
+
* @param {string} siteFolder
|
|
11
|
+
*/
|
|
12
|
+
const copyThemeVariables = (root, siteFolder) => {
|
|
13
|
+
const { themeRules, layoutRules } = scanStylesheet(root, siteFolder)
|
|
14
|
+
const theme = readThemeVariables(themeRules)
|
|
15
|
+
const breakpointPlan = planBreakpoints(theme)
|
|
16
|
+
restateThemeVariables(theme, breakpointPlan)
|
|
17
|
+
tagThemeVariables(theme.rules.map(({ scanned }) => scanned), theme)
|
|
18
|
+
const sections = readSectionRules(layoutRules)
|
|
19
|
+
const themeCopies = planThemeCopies(theme, breakpointPlan)
|
|
20
|
+
writeCopies(root, themeCopies, sections)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const checks = /** @type {const} */ ([])
|
|
24
|
+
/** @typedef {typeof checks[number]} Check */
|
|
25
|
+
|
|
26
|
+
/** @type {import('../../transform/types.js').CssTransformPlugin<Check>} */
|
|
27
|
+
export const spxThemeVariablesPlugin = {
|
|
28
|
+
checks,
|
|
29
|
+
name: 'spx-theme-variables',
|
|
30
|
+
order: 16,
|
|
31
|
+
transform(root, context) {
|
|
32
|
+
const siteFolder = context?.layout?.root
|
|
33
|
+
if (siteFolder === undefined) return
|
|
34
|
+
copyThemeVariables(root, siteFolder)
|
|
35
|
+
},
|
|
36
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { DESKTOP, collapseWhitespace, isMedia } from './cssTreeUtils.js'
|
|
2
|
+
|
|
3
|
+
/** @typedef {Map<string, import('postcss').Declaration>} VariableMap */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {object} ThemeRule
|
|
7
|
+
* @property {import('./stylesheetUtils.js').ScannedRule} scanned
|
|
8
|
+
* @property {VariableMap} variables
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} ThemeVariables
|
|
13
|
+
* @property {ThemeRule[]} rules
|
|
14
|
+
* @property {string[]} breakpoints
|
|
15
|
+
* @property {Map<string, number>} refWidthByBreakpoint
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} CascadedVariable
|
|
20
|
+
* @property {import('postcss').Declaration} declaration
|
|
21
|
+
* @property {string} breakpoint
|
|
22
|
+
* @property {number} index
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const DEFAULT_REF_WIDTH = 1280
|
|
26
|
+
|
|
27
|
+
const MAX_WIDTH = /^\(\s*max-width\s*:\s*(\d+(?:\.\d+)?)px\s*\)$/i
|
|
28
|
+
|
|
29
|
+
/** @param {string} query */
|
|
30
|
+
export const maxWidthOf = query => {
|
|
31
|
+
const trimmed = query.trim()
|
|
32
|
+
const match = MAX_WIDTH.exec(trimmed)
|
|
33
|
+
return match === null ? undefined : Number(match[1])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @param {string} query */
|
|
37
|
+
const breakpointKeyOf = query => {
|
|
38
|
+
const maxWidth = maxWidthOf(query)
|
|
39
|
+
return maxWidth === undefined ? collapseWhitespace(query) : `(max-width: ${maxWidth}px)`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @param {readonly import('postcss').Node[]} ancestors */
|
|
43
|
+
export const breakpointOf = ancestors => {
|
|
44
|
+
const media = ancestors.find(isMedia)
|
|
45
|
+
return media === undefined ? DESKTOP : breakpointKeyOf(media.params)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {readonly string[]} breakpoints */
|
|
49
|
+
export const widestFirst = breakpoints =>
|
|
50
|
+
[...breakpoints].sort((left, right) => (maxWidthOf(right) ?? 0) - (maxWidthOf(left) ?? 0))
|
|
51
|
+
|
|
52
|
+
/** @param {ThemeVariables} theme */
|
|
53
|
+
export const themeBreakpointsOf = ({ breakpoints }) => breakpoints
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {readonly string[]} breakpoints
|
|
57
|
+
* @param {string} breakpoint
|
|
58
|
+
*/
|
|
59
|
+
const enclosingBreakpointsOf = (breakpoints, breakpoint) => {
|
|
60
|
+
const maxWidth = maxWidthOf(breakpoint)
|
|
61
|
+
if (maxWidth === undefined) return [breakpoint]
|
|
62
|
+
return breakpoints.filter(candidate => (maxWidthOf(candidate) ?? -1) >= maxWidth)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {ThemeVariables} theme
|
|
67
|
+
* @param {string} breakpoint
|
|
68
|
+
*/
|
|
69
|
+
export const inheritedBreakpointsOf = ({ breakpoints }, breakpoint) => {
|
|
70
|
+
if (breakpoint === DESKTOP) return []
|
|
71
|
+
const enclosing = enclosingBreakpointsOf(breakpoints, breakpoint)
|
|
72
|
+
const outer = enclosing.filter(candidate => candidate !== breakpoint)
|
|
73
|
+
return [DESKTOP, ...outer]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {ThemeRule} rule
|
|
78
|
+
* @param {number} index
|
|
79
|
+
* @returns {[string, CascadedVariable][]}
|
|
80
|
+
*/
|
|
81
|
+
const cascadedVariablesOf = ({ scanned, variables }, index) => {
|
|
82
|
+
const { breakpoint } = scanned
|
|
83
|
+
return [...variables].map(([prop, declaration]) => [prop, { declaration, breakpoint, index }])
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {ThemeVariables} theme
|
|
88
|
+
* @param {readonly string[]} cascadeBreakpoints
|
|
89
|
+
* @returns {Map<string, CascadedVariable>}
|
|
90
|
+
*/
|
|
91
|
+
export const cascadeAt = ({ rules }, cascadeBreakpoints) => {
|
|
92
|
+
const matching = new Set(cascadeBreakpoints)
|
|
93
|
+
const entries = rules.flatMap((rule, index) => (matching.has(rule.scanned.breakpoint) ? cascadedVariablesOf(rule, index) : []))
|
|
94
|
+
return new Map(entries)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {readonly string[]} breakpoints
|
|
99
|
+
* @param {Map<string, number>} ownRefWidths
|
|
100
|
+
* @param {string} breakpoint
|
|
101
|
+
*/
|
|
102
|
+
const resolveRefWidth = (breakpoints, ownRefWidths, breakpoint) => {
|
|
103
|
+
const own = ownRefWidths.get(breakpoint)
|
|
104
|
+
if (own !== undefined) return own
|
|
105
|
+
const enclosing = enclosingBreakpointsOf(breakpoints, breakpoint)
|
|
106
|
+
const defined = enclosing.flatMap(candidate => ownRefWidths.get(candidate) ?? [])
|
|
107
|
+
return defined.at(-1) ?? ownRefWidths.get(DESKTOP) ?? DEFAULT_REF_WIDTH
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {readonly string[]} breakpoints
|
|
112
|
+
* @param {Map<string, number>} ownRefWidths
|
|
113
|
+
*/
|
|
114
|
+
export const resolveRefWidths = (breakpoints, ownRefWidths) => {
|
|
115
|
+
const entries = [DESKTOP, ...breakpoints].map(breakpoint => {
|
|
116
|
+
const refWidth = resolveRefWidth(breakpoints, ownRefWidths, breakpoint)
|
|
117
|
+
return /** @type {const} */ ([breakpoint, refWidth])
|
|
118
|
+
})
|
|
119
|
+
return new Map(entries)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {ThemeVariables} theme
|
|
124
|
+
* @param {string} breakpoint
|
|
125
|
+
*/
|
|
126
|
+
export const refWidthAt = ({ refWidthByBreakpoint }, breakpoint) => refWidthByBreakpoint.get(breakpoint) ?? DEFAULT_REF_WIDTH
|