@webround/core 0.1.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/base-css.d.ts +8 -0
  2. package/dist/base-css.js +19 -0
  3. package/dist/components/WrAltcha.d.ts +5 -0
  4. package/dist/components/WrAltcha.js +7 -0
  5. package/dist/components/WrButton.js +19 -0
  6. package/dist/components/WrForm.d.ts +7 -0
  7. package/dist/components/WrForm.js +15 -0
  8. package/dist/components/WrFormPrivacy.d.ts +4 -0
  9. package/dist/components/WrFormPrivacy.js +6 -0
  10. package/dist/components/WrFormSuccess.d.ts +4 -0
  11. package/dist/components/WrFormSuccess.js +6 -0
  12. package/dist/components/WrIframe.d.ts +8 -0
  13. package/dist/components/WrIframe.js +13 -0
  14. package/dist/components/WrImage.d.ts +9 -0
  15. package/dist/components/WrImage.js +13 -0
  16. package/dist/components/WrInput.d.ts +8 -0
  17. package/dist/components/WrInput.js +10 -0
  18. package/dist/components/WrLangSelector.d.ts +17 -0
  19. package/dist/components/WrLangSelector.js +27 -0
  20. package/dist/components/WrLangSync.d.ts +6 -0
  21. package/dist/components/WrLangSync.js +12 -0
  22. package/dist/components/WrLink.js +23 -0
  23. package/dist/components/WrNav.d.ts +26 -0
  24. package/dist/components/WrNav.js +15 -0
  25. package/dist/{WrSection.d.ts → components/WrSection.d.ts} +3 -3
  26. package/dist/components/WrShared.d.ts +22 -0
  27. package/dist/components/WrShared.js +18 -0
  28. package/dist/components/WrText.d.ts +11 -0
  29. package/dist/components/WrText.js +14 -0
  30. package/dist/components/WrThemeToggle.d.ts +9 -0
  31. package/dist/components/WrThemeToggle.js +43 -0
  32. package/dist/components/WrVideo.js +61 -0
  33. package/dist/context/WrAppContext.d.ts +40 -0
  34. package/dist/context/WrAppContext.js +95 -0
  35. package/dist/context/WrLayout.d.ts +23 -0
  36. package/dist/context/WrLayout.js +18 -0
  37. package/dist/context/WrPageContext.d.ts +10 -0
  38. package/dist/context/WrPageContext.js +13 -0
  39. package/dist/form-react.d.ts +82 -0
  40. package/dist/form-react.js +165 -0
  41. package/dist/form.d.ts +83 -0
  42. package/dist/form.js +175 -0
  43. package/dist/index.css +651 -0
  44. package/dist/index.d.ts +20 -8
  45. package/dist/index.js +20 -8
  46. package/dist/kinds.d.ts +109 -0
  47. package/dist/kinds.js +35 -0
  48. package/dist/lang-codes.d.ts +366 -0
  49. package/dist/lang-codes.js +166 -0
  50. package/dist/localized.d.ts +36 -0
  51. package/dist/localized.js +71 -0
  52. package/dist/not-found.d.ts +9 -0
  53. package/dist/not-found.js +13 -0
  54. package/dist/routing.d.ts +122 -0
  55. package/dist/routing.js +198 -0
  56. package/dist/seo-react.d.ts +20 -0
  57. package/dist/seo-react.js +37 -0
  58. package/dist/seo.d.ts +167 -0
  59. package/dist/seo.js +221 -0
  60. package/dist/site-env.d.ts +25 -0
  61. package/dist/site-env.js +37 -0
  62. package/dist/style.d.ts +456 -0
  63. package/dist/style.js +1081 -0
  64. package/dist/text-react.d.ts +11 -0
  65. package/dist/text-react.js +5 -0
  66. package/dist/theme.d.ts +22 -0
  67. package/dist/theme.js +45 -0
  68. package/dist/types.d.ts +37 -21
  69. package/package.json +93 -7
  70. package/dist/WrButton.d.ts.map +0 -1
  71. package/dist/WrButton.js +0 -7
  72. package/dist/WrButton.js.map +0 -1
  73. package/dist/WrImage.d.ts +0 -8
  74. package/dist/WrImage.d.ts.map +0 -1
  75. package/dist/WrImage.js +0 -7
  76. package/dist/WrImage.js.map +0 -1
  77. package/dist/WrLink.d.ts.map +0 -1
  78. package/dist/WrLink.js +0 -15
  79. package/dist/WrLink.js.map +0 -1
  80. package/dist/WrPageContext.d.ts +0 -8
  81. package/dist/WrPageContext.d.ts.map +0 -1
  82. package/dist/WrPageContext.js +0 -11
  83. package/dist/WrPageContext.js.map +0 -1
  84. package/dist/WrSection.d.ts.map +0 -1
  85. package/dist/WrSection.js.map +0 -1
  86. package/dist/WrText.d.ts +0 -11
  87. package/dist/WrText.d.ts.map +0 -1
  88. package/dist/WrText.js +0 -7
  89. package/dist/WrText.js.map +0 -1
  90. package/dist/WrVideo.d.ts.map +0 -1
  91. package/dist/WrVideo.js +0 -42
  92. package/dist/WrVideo.js.map +0 -1
  93. package/dist/bridge.d.ts +0 -2
  94. package/dist/bridge.d.ts.map +0 -1
  95. package/dist/bridge.js +0 -110
  96. package/dist/bridge.js.map +0 -1
  97. package/dist/index.d.ts.map +0 -1
  98. package/dist/index.js.map +0 -1
  99. package/dist/types.d.ts.map +0 -1
  100. package/dist/types.js.map +0 -1
  101. /package/dist/{WrButton.d.ts → components/WrButton.d.ts} +0 -0
  102. /package/dist/{WrLink.d.ts → components/WrLink.d.ts} +0 -0
  103. /package/dist/{WrSection.js → components/WrSection.js} +0 -0
  104. /package/dist/{WrVideo.d.ts → components/WrVideo.d.ts} +0 -0
package/dist/style.js ADDED
@@ -0,0 +1,1081 @@
1
+ // The style of an element, as a person, an AI and a tool outside all understand it: a PROPERTY (padding, text color, width…), at a BREAKPOINT
2
+ // (base, md…), with a VALUE that has a name (4, accent, center). The code says it with Tailwind classes (`px-4 md:px-6 text-accent`); this file is the
3
+ // ONE place that knows how one becomes the other, in both directions. Pure: no React, no Node, no I/O. The panel, the operations (style-get, style-set,
4
+ // style-catalog: so `wr`, MCP and the API) and the tests read the same table, and nothing else lists a property, a value or a class.
5
+ //
6
+ // What it promises when it writes a className:
7
+ // - only whole classes that are in the table (Tailwind finds a class by reading the code: nothing is ever built from pieces);
8
+ // - what it does not know stays exactly where it was (hover:, dark:, !important, arbitrary values on other properties, the project's own classes);
9
+ // - only the classes of the property and the breakpoint it was asked about change (and the narrower ones a wider one replaces: p replaces px, pt…).
10
+ /** Mobile first: a value at a breakpoint holds from that width up, until a wider breakpoint says otherwise. */
11
+ export const WR_BREAKPOINTS = [
12
+ { id: 'base', variant: '', minWidth: 0, label: 'All screens', description: 'Every screen, phones included. Wider breakpoints can change it.' },
13
+ { id: 'mobile', variant: 'max-md', minWidth: 0, label: 'Mobile', description: 'Phones only: below 768 px wide. It overrides "All screens" there and on no other screen.' },
14
+ { id: 'sm', variant: 'sm', minWidth: 640, label: 'Large phones and up', description: 'From 640 px wide.' },
15
+ { id: 'md', variant: 'md', minWidth: 768, label: 'Tablet and up', description: 'From 768 px wide.' },
16
+ { id: 'lg', variant: 'lg', minWidth: 1024, label: 'Desktop and up', description: 'From 1024 px wide.' },
17
+ { id: 'xl', variant: 'xl', minWidth: 1280, label: 'Wide desktop and up', description: 'From 1280 px wide.' },
18
+ ];
19
+ export const WR_BREAKPOINT_IDS = WR_BREAKPOINTS.map(b => b.id);
20
+ /**
21
+ * What a breakpoint inherits from, own first: mobile first, so a width counts the narrower ones (md: base, sm, md). Mobile is the exception: it holds BELOW md (`max-md:`), so it only
22
+ * has the base to fall back on, and no other width counts it.
23
+ */
24
+ export function wrStyleCascade(breakpoint) {
25
+ if (breakpoint === 'mobile')
26
+ return ['mobile', 'base'];
27
+ const at = WR_BREAKPOINTS.findIndex(b => b.id === breakpoint);
28
+ return WR_BREAKPOINTS.slice(0, at + 1).filter(b => b.id !== 'mobile').map(b => b.id).reverse();
29
+ }
30
+ export const WR_STYLE_GROUPS = {
31
+ spacing: 'Spacing', size: 'Size', text: 'Text', color: 'Color', border: 'Border', effects: 'Effects', motion: 'Motion', layout: 'Layout',
32
+ };
33
+ export const WR_STATES = [
34
+ { id: 'hover', variant: 'hover', label: 'Hover', description: 'While the pointer is over it (where there is a pointer: a phone does not hover).' },
35
+ { id: 'focus', variant: 'focus', label: 'Focus', description: 'While it has the focus (reached with the keyboard, or clicked into): what shows a person where they are.' },
36
+ { id: 'active', variant: 'active', label: 'Pressed', description: 'While it is being pressed.' },
37
+ ];
38
+ export const WR_STATE_IDS = WR_STATES.map(s => s.id);
39
+ // ── The table ────────────────────────────────────────────────────────────────
40
+ /** The semantic colors of the starter site's theme (index.css `@theme`): the project's own list replaces it (see `WrStyleOptions`). */
41
+ export const WR_DEFAULT_COLOR_TOKENS = [
42
+ 'page', 'fg', 'muted', 'subtle', 'surface', 'surface-2', 'line', 'accent', 'accent-hover', 'accent-text', 'accent-soft', 'on-accent', 'inverse', 'on-inverse',
43
+ ];
44
+ const SPACING_STEPS = ['0', 'px', '0.5', '1', '1.5', '2', '2.5', '3', '3.5', '4', '5', '6', '7', '8', '9', '10', '11', '12', '14', '16', '20', '24', '28', '32', '36', '40', '44', '48', '52', '56', '60', '64', '72', '80', '96'];
45
+ /** Tailwind's scale: a step is a quarter of a rem. */
46
+ function stepCss(step) {
47
+ if (step === 'px')
48
+ return '1px';
49
+ const n = Number(step);
50
+ return n === 0 ? '0' : `${n / 4}rem (${n * 4}px)`;
51
+ }
52
+ const spacingValues = (prefix, extra = []) => [
53
+ ...SPACING_STEPS.map(step => ({ value: step, class: `${prefix}-${step}`, label: step, css: stepCss(step) })),
54
+ ...extra,
55
+ ];
56
+ const FRACTIONS = ['1/2', '1/3', '2/3', '1/4', '3/4', '1/5', '2/5', '3/5', '4/5'];
57
+ const sizeValues = (prefix) => [
58
+ { value: 'auto', class: `${prefix}-auto`, label: 'Auto', css: 'auto' },
59
+ { value: 'full', class: `${prefix}-full`, label: 'Full', css: '100% of the parent' },
60
+ { value: 'screen', class: `${prefix}-screen`, label: 'Screen', css: '100% of the screen' },
61
+ { value: 'fit', class: `${prefix}-fit`, label: 'Fit content', css: 'as wide as its content' },
62
+ { value: 'min', class: `${prefix}-min`, label: 'Min content', css: 'min-content' },
63
+ { value: 'max', class: `${prefix}-max`, label: 'Max content', css: 'max-content' },
64
+ ...FRACTIONS.map(f => ({ value: f, class: `${prefix}-${f}`, label: f, css: `${Math.round((Number(f.split('/')[0]) / Number(f.split('/')[1])) * 10000) / 100}% of the parent` })),
65
+ ...spacingValues(prefix),
66
+ ];
67
+ const option = (value, className, label, css) => ({ value, class: className, label, ...(css ? { css } : {}) });
68
+ const STEP_PATTERN = /^\d{1,4}(?:\.\d{1,2})?$/;
69
+ const INTEGER_PATTERN = /^\d{1,3}$/;
70
+ const FRACTION_PATTERN = /^(\d{1,3})\/(\d{1,3})$/;
71
+ /** A number someone typed (or a class carries) as a value of a free property: its name, class and meaning; null when it is not one. */
72
+ function freeValue(free, text) {
73
+ const negative = text.startsWith('-');
74
+ const body = negative ? text.slice(1) : text;
75
+ if (negative && !free.negative)
76
+ return null;
77
+ const classOf = (amount) => `${negative ? '-' : ''}${free.prefix}-${amount}`;
78
+ if (free.kind === 'step' && STEP_PATTERN.test(body) && Number(body) <= free.max) {
79
+ const n = Number(body);
80
+ const value = `${negative && n !== 0 ? '-' : ''}${n}`;
81
+ return { value, class: classOf(String(n)), label: value, css: `${negative && n !== 0 ? '-' : ''}${stepCss(String(n))}` };
82
+ }
83
+ if (free.kind === 'integer' && INTEGER_PATTERN.test(body) && Number(body) <= free.max) {
84
+ const n = Number(body);
85
+ return { value: String(n), class: classOf(String(n)), label: String(n), css: `${n}px` };
86
+ }
87
+ const fraction = free.fractions ? FRACTION_PATTERN.exec(body) : null;
88
+ if (fraction && !negative && Number(fraction[1]) > 0 && Number(fraction[2]) > 0) {
89
+ const value = `${Number(fraction[1])}/${Number(fraction[2])}`;
90
+ return { value, class: classOf(value), label: value, css: `${Math.round((Number(fraction[1]) / Number(fraction[2])) * 10000) / 100}% of the parent` };
91
+ }
92
+ return null;
93
+ }
94
+ /** What a value of a property is: one of its list, or (when the property is free) any number it accepts. null when it is neither. */
95
+ export function wrStyleParseValue(property, text) {
96
+ const trimmed = text.trim();
97
+ return property.values.find(v => v.value === trimmed) ?? (property.free ? freeValue(property.free, trimmed) : null)
98
+ ?? (typeof property.hex === 'string' ? hexValue(property.hex, trimmed) ?? colorFunctionValue(property, property.hex, trimmed) : null);
99
+ }
100
+ const RGB_COLOR = /^rgba?\(\s*(\d{1,3}(?:\.\d+)?)(?:\s*,\s*|\s+)(\d{1,3}(?:\.\d+)?)(?:\s*,\s*|\s+)(\d{1,3}(?:\.\d+)?)(?:\s*(?:,|\/)\s*(\d*\.?\d+%?))?\s*\)$/i;
101
+ /**
102
+ * A color said the way the browser says it (`rgb(34, 197, 94)`, `rgba(50, 34, 18, 0.5)`: what `getComputedStyle` gives, what a person copies from the dev tools) as the hex color it is. Fully
103
+ * transparent is `transparent` when the property has it; a half-transparent color is a hex with its alpha. null when it is not one of these.
104
+ */
105
+ function colorFunctionValue(property, prefix, text) {
106
+ const match = RGB_COLOR.exec(text);
107
+ if (!match)
108
+ return null;
109
+ const [r, g, b] = match.slice(1, 4).map(Number);
110
+ if ([r, g, b].some(n => n > 255))
111
+ return null;
112
+ const alpha = match[4] === undefined ? 1 : match[4].endsWith('%') ? parseFloat(match[4]) / 100 : parseFloat(match[4]);
113
+ if (!(alpha >= 0 && alpha <= 1))
114
+ return null;
115
+ if (alpha === 0)
116
+ return property.values.find(v => v.value === 'transparent') ?? null;
117
+ const hex = (n) => Math.round(n).toString(16).padStart(2, '0');
118
+ return hexValue(prefix, `#${hex(r)}${hex(g)}${hex(b)}${alpha < 1 ? hex(alpha * 255) : ''}`);
119
+ }
120
+ const HEX_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
121
+ /** A color picked on the fly (`#22c55e`): written as an arbitrary value (`bg-[#22c55e]`), so it is outside the theme and does not follow it. null when it is not a hex color. */
122
+ function hexValue(prefix, text) {
123
+ if (!HEX_COLOR.test(text))
124
+ return null;
125
+ const value = text.toLowerCase();
126
+ return { value, class: `${prefix}-[${value}]`, label: value, css: 'not the theme\'s: it does not follow it', swatch: value };
127
+ }
128
+ /** What a free property accepts, in words: for the field that takes it and for an error that refuses it. */
129
+ export function wrStyleFreeHint(free) {
130
+ const what = free.kind === 'step' ? `any number from 0 to ${free.max} (each step is a quarter of a rem, 4px: 27 is 6.75rem, 108px)` : `a whole number of pixels, from 0 to ${free.max}`;
131
+ return `${what}${free.negative ? ', negative too (-4)' : ''}${free.fractions ? ', or a fraction of the parent (3/7)' : ''}`;
132
+ }
133
+ const SIDES = [
134
+ { suffix: '', label: 'all sides' }, { suffix: 'x', label: 'left and right' }, { suffix: 'y', label: 'top and bottom' },
135
+ { suffix: 't', label: 'top' }, { suffix: 'r', label: 'right' }, { suffix: 'b', label: 'bottom' }, { suffix: 'l', label: 'left' },
136
+ ];
137
+ const SIDE_NAME = { '': '', x: '-x', y: '-y', t: '-top', r: '-right', b: '-bottom', l: '-left' };
138
+ const INCLUDED = { '': ['x', 'y', 't', 'r', 'b', 'l'], x: ['l', 'r'], y: ['t', 'b'] };
139
+ function boxProperties(kind, letter) {
140
+ const text = kind === 'padding' ? 'Space inside the element, between its edge and its content' : 'Space outside the element, between it and its neighbours';
141
+ return SIDES.map(({ suffix, label }) => ({
142
+ id: `${kind}${SIDE_NAME[suffix]}`,
143
+ label: `${kind === 'padding' ? 'Padding' : 'Margin'}${suffix ? ` (${label})` : ''}`,
144
+ description: `${text}: ${label}.`,
145
+ group: 'spacing',
146
+ control: 'scale',
147
+ values: spacingValues(`${letter}${suffix}`, kind === 'margin' ? [option('auto', `${letter}${suffix}-auto`, 'Auto', 'takes the free space')] : []),
148
+ ...(INCLUDED[suffix] ? { includes: INCLUDED[suffix].map(s => `${kind}${SIDE_NAME[s]}`) } : {}),
149
+ customPrefix: `${letter}${suffix}`,
150
+ free: { kind: 'step', prefix: `${letter}${suffix}`, max: 1000, ...(kind === 'margin' ? { negative: true } : {}) },
151
+ compound: kind,
152
+ }));
153
+ }
154
+ function gapProperties() {
155
+ return [
156
+ { id: 'gap', label: 'Gap', description: 'Space between the children of a flex or grid element, both ways.', prefix: 'gap', includes: ['gap-x', 'gap-y'] },
157
+ { id: 'gap-x', label: 'Gap (columns)', description: 'Space between the children of a flex or grid element, side by side.', prefix: 'gap-x' },
158
+ { id: 'gap-y', label: 'Gap (rows)', description: 'Space between the children of a flex or grid element, one above the other.', prefix: 'gap-y' },
159
+ ].map(({ prefix, ...rest }) => ({ ...rest, group: 'spacing', control: 'scale', values: spacingValues(prefix), customPrefix: prefix, free: { kind: 'step', prefix, max: 1000 }, compound: 'gap' }));
160
+ }
161
+ const BORDER_WIDTHS = [['0', '0'], ['1', '1px'], ['2', '2px'], ['4', '4px'], ['8', '8px']];
162
+ /** The border's thickness, as a whole and side by side (`border`, `border-x`, `border-t-2`…): a model like padding's, the sides are the same table. */
163
+ function borderWidthProperties() {
164
+ return SIDES.map(({ suffix, label }) => {
165
+ const prefix = suffix ? `border-${suffix}` : 'border';
166
+ return {
167
+ id: `border-width${SIDE_NAME[suffix]}`,
168
+ label: `Border width${suffix ? ` (${label})` : ''}`,
169
+ description: `How thick the border is (it needs a color to be seen): ${label}.`,
170
+ group: 'border',
171
+ control: 'options',
172
+ values: BORDER_WIDTHS.map(([value, css]) => option(value, value === '1' ? prefix : `${prefix}-${value}`, value === '0' ? 'None' : value, css)),
173
+ ...(INCLUDED[suffix] ? { includes: INCLUDED[suffix].map(s => `border-width${SIDE_NAME[s]}`) } : {}),
174
+ free: { kind: 'integer', prefix, max: 100 },
175
+ compound: 'border-width',
176
+ };
177
+ });
178
+ }
179
+ const RADII = [
180
+ ['none', 'None', '0'], ['sm', 'Small', '0.25rem (4px)'], ['default', 'Default', '0.25rem (4px)'], ['md', 'Medium', '0.375rem (6px)'], ['lg', 'Large', '0.5rem (8px)'],
181
+ ['xl', 'Extra large', '0.75rem (12px)'], ['2xl', '2xl', '1rem (16px)'], ['3xl', '3xl', '1.5rem (24px)'], ['full', 'Full', 'a pill or a circle'],
182
+ ];
183
+ const CORNERS = [{ suffix: '', label: 'all corners' }, { suffix: 'tl', label: 'top left' }, { suffix: 'tr', label: 'top right' }, { suffix: 'bl', label: 'bottom left' }, { suffix: 'br', label: 'bottom right' }];
184
+ /** How round the corners are, as a whole and corner by corner (`rounded-xl`, `rounded-tl-lg`). */
185
+ function borderRadiusProperties() {
186
+ return CORNERS.map(({ suffix, label }) => {
187
+ const prefix = suffix ? `rounded-${suffix}` : 'rounded';
188
+ return {
189
+ id: suffix ? `border-radius-${suffix}` : 'border-radius',
190
+ label: suffix ? `Corners (${label})` : 'Corners',
191
+ description: `How round the corners are: ${label}.`,
192
+ group: 'border',
193
+ control: 'options',
194
+ values: RADII.map(([value, name, css]) => option(value, value === 'default' ? prefix : `${prefix}-${value}`, name, css)),
195
+ ...(suffix ? {} : { includes: CORNERS.filter(c => c.suffix).map(c => `border-radius-${c.suffix}`) }),
196
+ compound: 'border-radius',
197
+ };
198
+ });
199
+ }
200
+ const FONT_SIZES = [
201
+ ['xs', '0.75rem (12px)'], ['sm', '0.875rem (14px)'], ['base', '1rem (16px)'], ['lg', '1.125rem (18px)'], ['xl', '1.25rem (20px)'], ['2xl', '1.5rem (24px)'],
202
+ ['3xl', '1.875rem (30px)'], ['4xl', '2.25rem (36px)'], ['5xl', '3rem (48px)'], ['6xl', '3.75rem (60px)'], ['7xl', '4.5rem (72px)'], ['8xl', '6rem (96px)'], ['9xl', '8rem (128px)'],
203
+ ];
204
+ const FONT_WEIGHTS = [
205
+ ['thin', 'Thin', '100'], ['extralight', 'Extra light', '200'], ['light', 'Light', '300'], ['normal', 'Normal', '400'], ['medium', 'Medium', '500'],
206
+ ['semibold', 'Semibold', '600'], ['bold', 'Bold', '700'], ['extrabold', 'Extra bold', '800'], ['black', 'Black', '900'],
207
+ ];
208
+ const colorValues = (prefix, tokens, withTransparent, swatches = {}) => [
209
+ ...tokens.map(token => ({ value: token, class: `${prefix}-${token}`, label: token, css: `the theme's ${token} color`, ...(swatches[token] ? { swatch: swatches[token] } : {}) })),
210
+ ...(withTransparent ? [option('transparent', `${prefix}-transparent`, 'Transparent', 'no color')] : []),
211
+ ];
212
+ /**
213
+ * The entry animations (fade in, slide up, zoom in) are not Tailwind's own: they need their keyframes in the theme (`--animate-*` in an `@theme`, which Tailwind 4 turns into the class
214
+ * `animate-fade-in`). This is the ONE text of them: the operations write it into `theme.css` the first time one is used, and the tests compile it. The three built in (pulse, bounce, spin,
215
+ * ping) need nothing.
216
+ */
217
+ export const WR_ENTRY_ANIMATIONS = ['fade-in', 'slide-up', 'zoom-in'];
218
+ export const WR_ENTRY_ANIMATIONS_CSS = {
219
+ 'fade-in': { value: 'wr-fade-in 0.6s ease-out both', keyframes: 'from { opacity: 0; } to { opacity: 1; }' },
220
+ 'slide-up': { value: 'wr-slide-up 0.6s ease-out both', keyframes: 'from { opacity: 0; transform: translateY(1rem); } to { opacity: 1; transform: translateY(0); }' },
221
+ 'zoom-in': { value: 'wr-zoom-in 0.5s ease-out both', keyframes: 'from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); }' },
222
+ };
223
+ /** Every property the panel, the operations and the API speak of, in the order they are shown. */
224
+ export function wrStyleProperties(options = {}) {
225
+ const tokens = options.colorTokens ?? WR_DEFAULT_COLOR_TOKENS;
226
+ const swatches = options.colorSwatches;
227
+ return [
228
+ ...boxProperties('padding', 'p'),
229
+ ...boxProperties('margin', 'm'),
230
+ ...gapProperties(),
231
+ { id: 'width', label: 'Width', description: 'How wide the element is.', group: 'size', control: 'scale', values: sizeValues('w'), customPrefix: 'w', free: { kind: 'step', prefix: 'w', max: 1000, fractions: true }, shortcuts: ['auto', 'fit', 'full', '1/2', '1/3', '1/4'] },
232
+ { id: 'height', label: 'Height', description: 'How tall the element is.', group: 'size', control: 'scale', values: sizeValues('h'), customPrefix: 'h', free: { kind: 'step', prefix: 'h', max: 1000, fractions: true }, shortcuts: ['auto', 'fit', 'full', 'screen'] },
233
+ {
234
+ id: 'max-width', label: 'Max width', description: 'The width the element never goes past.', group: 'size', control: 'options', customPrefix: 'max-w',
235
+ values: [
236
+ option('none', 'max-w-none', 'None', 'no limit'),
237
+ ...['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl', '7xl'].map((s, i) => option(s, `max-w-${s}`, s, `${[20, 24, 28, 32, 36, 42, 48, 56, 64, 72, 80][i]}rem`)),
238
+ option('full', 'max-w-full', 'Full', '100% of the parent'),
239
+ option('prose', 'max-w-prose', 'Prose', '65 characters: a line of text that is comfortable to read'),
240
+ ],
241
+ },
242
+ {
243
+ id: 'font-size', label: 'Text size', description: 'How big the text is.', group: 'text', control: 'options',
244
+ values: FONT_SIZES.map(([size, css]) => option(size, `text-${size}`, size, css)),
245
+ },
246
+ {
247
+ id: 'font-weight', label: 'Text weight', description: 'How bold the text is.', group: 'text', control: 'options',
248
+ values: FONT_WEIGHTS.map(([weight, label, css]) => option(weight, `font-${weight}`, label, css)),
249
+ },
250
+ {
251
+ id: 'text-align', label: 'Text alignment', description: 'Where the text lines up.', group: 'text', control: 'options',
252
+ values: [option('left', 'text-left', 'Left'), option('center', 'text-center', 'Center'), option('right', 'text-right', 'Right'), option('justify', 'text-justify', 'Justify')],
253
+ },
254
+ {
255
+ id: 'text-decoration', label: 'Underline', description: 'Whether the text is underlined (or struck through).', group: 'text', control: 'options', states: true,
256
+ values: [option('underline', 'underline', 'Underline'), option('line-through', 'line-through', 'Strike through'), option('none', 'no-underline', 'No underline')],
257
+ },
258
+ { id: 'text-color', label: 'Text color', description: 'The color of the text, from the site\'s theme.', group: 'color', control: 'color', states: true, hex: 'text', values: colorValues('text', tokens, false, swatches) },
259
+ { id: 'background-color', label: 'Background color', description: 'The color behind the element, from the site\'s theme.', group: 'color', control: 'color', states: true, hex: 'bg', values: colorValues('bg', tokens, true, swatches) },
260
+ {
261
+ id: 'gradient-direction', label: 'Gradient', description: 'Makes the background a gradient that runs in this direction. Then choose the colors it goes through.', group: 'color', control: 'options',
262
+ values: [['to-t', 'Up'], ['to-tr', 'Up right'], ['to-r', 'Right'], ['to-br', 'Down right'], ['to-b', 'Down'], ['to-bl', 'Down left'], ['to-l', 'Left'], ['to-tl', 'Up left']].map(([v, label]) => option(v, `bg-linear-${v}`, label)),
263
+ },
264
+ { id: 'gradient-from', label: 'Gradient start', description: 'The color the gradient starts with.', group: 'color', control: 'color', hex: 'from', values: colorValues('from', tokens, true, swatches) },
265
+ { id: 'gradient-via', label: 'Gradient middle', description: 'A color the gradient goes through on the way (optional).', group: 'color', control: 'color', hex: 'via', values: colorValues('via', tokens, true, swatches) },
266
+ { id: 'gradient-to', label: 'Gradient end', description: 'The color the gradient ends with.', group: 'color', control: 'color', hex: 'to', values: colorValues('to', tokens, true, swatches) },
267
+ { id: 'border-color', label: 'Border color', description: 'The color of the border, from the site\'s theme.', group: 'color', control: 'color', states: true, hex: 'border', values: colorValues('border', tokens, true, swatches) },
268
+ ...borderWidthProperties(),
269
+ ...borderRadiusProperties(),
270
+ {
271
+ id: 'shadow', label: 'Shadow', description: 'The shadow the element casts.', group: 'effects', control: 'options', states: true,
272
+ values: [
273
+ option('none', 'shadow-none', 'None'), option('2xs', 'shadow-2xs', '2xs'), option('xs', 'shadow-xs', 'xs'), option('sm', 'shadow-sm', 'Small'), option('default', 'shadow', 'Default'),
274
+ option('md', 'shadow-md', 'Medium'), option('lg', 'shadow-lg', 'Large'), option('xl', 'shadow-xl', 'Extra large'), option('2xl', 'shadow-2xl', '2xl'),
275
+ ],
276
+ },
277
+ {
278
+ id: 'opacity', label: 'Opacity', description: 'How see-through the element is: 100% is solid, 0% is invisible.', group: 'effects', control: 'options', states: true,
279
+ values: [0, 5, 10, 20, 25, 30, 40, 50, 60, 70, 75, 80, 90, 95, 100].map(n => option(String(n), `opacity-${n}`, `${n}%`, n === 0 ? 'invisible' : n === 100 ? 'solid' : `${n}% solid`)),
280
+ },
281
+ {
282
+ id: 'scale', label: 'Size change', description: 'Makes the element look bigger or smaller without moving the others (100% is as it is).', group: 'effects', control: 'options', states: true,
283
+ values: [50, 75, 90, 95, 100, 105, 110, 125, 150].map(n => option(String(n), `scale-${n}`, `${n}%`, n === 100 ? 'as it is' : `${n}% of its size`)),
284
+ },
285
+ {
286
+ id: 'translate-y', label: 'Move up or down', description: 'Moves the element up or down without moving the others (a "lift" on hover).', group: 'effects', control: 'options', states: true,
287
+ values: [['-4', 'Up a lot', '-1rem (-16px)'], ['-2', 'Up', '-0.5rem (-8px)'], ['-1', 'Up a little', '-0.25rem (-4px)'], ['0', 'Not moved', 'as it is'], ['1', 'Down a little', '0.25rem (4px)'], ['2', 'Down', '0.5rem (8px)'], ['4', 'Down a lot', '1rem (16px)']]
288
+ .map(([value, label, css]) => option(value, value.startsWith('-') ? `-translate-y-${value.slice(1)}` : `translate-y-${value}`, label, css)),
289
+ },
290
+ {
291
+ id: 'transition', label: 'Smooth change', description: 'What changes gradually instead of at once when the element goes from one state to another (hover, focus, pressed).', group: 'motion', control: 'options',
292
+ values: [
293
+ option('none', 'transition-none', 'Nothing: at once'), option('colors', 'transition-colors', 'Colors', 'text, background and border colors'), option('opacity', 'transition-opacity', 'Opacity'),
294
+ option('shadow', 'transition-shadow', 'Shadow'), option('transform', 'transition-transform', 'Movement and size', 'move, resize'), option('all', 'transition-all', 'Everything'),
295
+ ],
296
+ },
297
+ {
298
+ id: 'duration', label: 'Speed of the change', description: 'How long the gradual change takes.', group: 'motion', control: 'options',
299
+ values: [75, 100, 150, 200, 300, 500, 700, 1000].map(n => option(String(n), `duration-${n}`, `${n} ms`, n <= 150 ? 'quick' : n <= 300 ? 'comfortable' : 'slow')),
300
+ },
301
+ {
302
+ id: 'ease', label: 'Easing', description: 'How the change speeds up and slows down.', group: 'motion', control: 'options',
303
+ values: [option('linear', 'ease-linear', 'Steady'), option('in', 'ease-in', 'Starts slow'), option('out', 'ease-out', 'Ends slow'), option('in-out', 'ease-in-out', 'Slow at both ends')],
304
+ },
305
+ {
306
+ id: 'animation', label: 'Animation', description: 'A ready-made animation. It does not run for people who asked their system for less motion.', group: 'motion', control: 'options', variants: ['motion-safe'],
307
+ values: [
308
+ option('pulse', 'animate-pulse', 'Pulse', 'fades in and out, to say something is loading'), option('bounce', 'animate-bounce', 'Bounce', 'jumps up and down, to call for attention'),
309
+ option('spin', 'animate-spin', 'Spin', 'turns around, as a loader'), option('ping', 'animate-ping', 'Ping', 'a ring that grows and fades, as a notification'),
310
+ option('fade-in', 'animate-fade-in', 'Fade in', 'appears gradually when the page opens'), option('slide-up', 'animate-slide-up', 'Slide up', 'rises into place when the page opens'),
311
+ option('zoom-in', 'animate-zoom-in', 'Zoom in', 'grows into place when the page opens'),
312
+ ],
313
+ },
314
+ {
315
+ id: 'display', label: 'Display', description: 'How the element takes part in the layout; `none` hides it at that breakpoint.', group: 'layout', control: 'options',
316
+ values: [
317
+ option('block', 'block', 'Block', 'on its own line'), option('inline-block', 'inline-block', 'Inline block', 'in the line, with a size'), option('inline', 'inline', 'Inline', 'in the line of text'),
318
+ option('flex', 'flex', 'Flex', 'children in a row or column'), option('inline-flex', 'inline-flex', 'Inline flex'), option('grid', 'grid', 'Grid', 'children in a grid'),
319
+ option('none', 'hidden', 'None', 'hidden'),
320
+ ],
321
+ },
322
+ {
323
+ id: 'flex-direction', label: 'Direction', description: 'For a flex element: whether the children go in a row or a column.', group: 'layout', control: 'options',
324
+ values: [option('row', 'flex-row', 'Row'), option('col', 'flex-col', 'Column'), option('row-reverse', 'flex-row-reverse', 'Row, reversed'), option('col-reverse', 'flex-col-reverse', 'Column, reversed')],
325
+ },
326
+ {
327
+ id: 'align-items', label: 'Align items', description: 'For a flex or grid element: where the children sit across the direction.', group: 'layout', control: 'options',
328
+ values: [option('start', 'items-start', 'Start'), option('center', 'items-center', 'Center'), option('end', 'items-end', 'End'), option('stretch', 'items-stretch', 'Stretch'), option('baseline', 'items-baseline', 'Baseline')],
329
+ },
330
+ {
331
+ id: 'justify-content', label: 'Justify content', description: 'For a flex or grid element: how the children are spread along the direction.', group: 'layout', control: 'options',
332
+ values: [
333
+ option('start', 'justify-start', 'Start'), option('center', 'justify-center', 'Center'), option('end', 'justify-end', 'End'),
334
+ option('between', 'justify-between', 'Space between'), option('around', 'justify-around', 'Space around'), option('evenly', 'justify-evenly', 'Space evenly'),
335
+ ],
336
+ },
337
+ {
338
+ id: 'grid-columns', label: 'Columns', description: 'For a grid element: how many columns it has.', group: 'layout', control: 'options',
339
+ values: [1, 2, 3, 4, 5, 6].map(n => option(String(n), `grid-cols-${n}`, String(n))),
340
+ },
341
+ {
342
+ id: 'flex-wrap', label: 'Wrap', description: 'For a flex element: whether the children go to a new line when they do not fit.', group: 'layout', control: 'options',
343
+ values: [option('nowrap', 'flex-nowrap', 'No wrap'), option('wrap', 'flex-wrap', 'Wrap'), option('wrap-reverse', 'flex-wrap-reverse', 'Wrap, reversed')],
344
+ },
345
+ ];
346
+ }
347
+ /** Splits a className into its classes; a `:` inside `[…]` is part of a value (`bg-[url(http://x)]`), not a variant. */
348
+ export function wrParseClassName(className) {
349
+ return className.split(/\s+/).filter(Boolean).map((raw) => {
350
+ const parts = [];
351
+ let depth = 0;
352
+ let start = 0;
353
+ for (let i = 0; i < raw.length; i++) {
354
+ const c = raw[i];
355
+ if (c === '[' || c === '(')
356
+ depth++;
357
+ else if (c === ']' || c === ')')
358
+ depth = Math.max(0, depth - 1);
359
+ else if (c === ':' && depth === 0) {
360
+ parts.push(raw.slice(start, i));
361
+ start = i + 1;
362
+ }
363
+ }
364
+ return { raw, variants: parts, utility: raw.slice(start) };
365
+ });
366
+ }
367
+ const BASE_BREAKPOINT = WR_BREAKPOINTS[0];
368
+ const variantOf = (breakpoint) => (breakpoint.variant ? [breakpoint.variant] : []);
369
+ const stateById = (id) => (id ? (WR_STATES.find(s => s.id === id) ?? null) : null);
370
+ /** The variants a class of this property carries at a breakpoint and in a state: the screen first, then the state, then the property's own (`md:hover:bg-accent`, `motion-safe:animate-pulse`). */
371
+ const variantsOf = (breakpoint, state, property) => [...variantOf(breakpoint), ...(state ? [state.variant] : []), ...(property.variants ?? [])];
372
+ /**
373
+ * Where a class of a property holds, when it says it the way the table does: the breakpoint and the state, and the variants of the property itself last. Anything else (`dark:`, an order the
374
+ * table does not write) is not a class of the table: null.
375
+ */
376
+ function contextOf(variants, property) {
377
+ const fixed = property.variants ?? [];
378
+ let rest = variants;
379
+ if (fixed.length > 0) {
380
+ if (rest.length < fixed.length || !fixed.every((v, i) => rest[rest.length - fixed.length + i] === v))
381
+ return null;
382
+ rest = rest.slice(0, rest.length - fixed.length);
383
+ }
384
+ let state = null;
385
+ if (property.states && rest.length > 0) {
386
+ const found = WR_STATES.find(st => st.variant === rest[rest.length - 1]);
387
+ if (found) {
388
+ state = found;
389
+ rest = rest.slice(0, -1);
390
+ }
391
+ }
392
+ const breakpoint = WR_BREAKPOINTS.find(b => sameVariants(rest, variantOf(b)));
393
+ return breakpoint ? { breakpoint, state } : null;
394
+ }
395
+ const sameVariants = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
396
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
397
+ /** The arbitrary value of a property (`w-[320px]`): outside the table, so it is read and replaced but never offered. */
398
+ function isCustom(property, utility) {
399
+ return property.customPrefix !== undefined && new RegExp(`^-?${escapeRegExp(property.customPrefix)}-\\[.+\\]$`).test(utility);
400
+ }
401
+ /** A free number (`px-27`, `-mt-4`, `w-3/7`) as the class says it: its value; null when the class is not one of the property's. */
402
+ function freeOf(property, utility) {
403
+ const free = property.free;
404
+ if (!free || !utility.startsWith(`${utility.startsWith('-') ? '-' : ''}${free.prefix}-`))
405
+ return null;
406
+ const negative = utility.startsWith('-');
407
+ return freeValue(free, `${negative ? '-' : ''}${utility.slice(free.prefix.length + (negative ? 2 : 1))}`);
408
+ }
409
+ const byId = (properties) => new Map(properties.map(p => [p.id, p]));
410
+ const breakpointOf = (id) => WR_BREAKPOINTS.find(b => b.id === id);
411
+ /** What a class says about one property: the value of the table, or `custom` for what is outside it. */
412
+ function matchOf(property, utility) {
413
+ const known = property.values.find(v => v.class === utility);
414
+ if (known)
415
+ return { value: known.value, custom: false };
416
+ const free = freeOf(property, utility);
417
+ if (free)
418
+ return { value: free.value, custom: false };
419
+ if (property.hex && utility.startsWith(`${property.hex}-[`) && utility.endsWith(']') && HEX_COLOR.test(utility.slice(property.hex.length + 2, -1)))
420
+ return { value: utility.slice(property.hex.length + 2, -1).toLowerCase(), custom: false };
421
+ if (isCustom(property, utility))
422
+ return { value: null, custom: true };
423
+ return null;
424
+ }
425
+ /**
426
+ * Everything a className says about the properties of the table, at the breakpoints it names. Several classes for one property: the last one is what is read. `state`: what it says for that
427
+ * state (`hover:bg-accent`); without it, only what holds always (the classes of a state are not mixed in).
428
+ */
429
+ export function wrStyleRead(className, options = {}, state) {
430
+ const properties = wrStyleProperties(options);
431
+ const entries = new Map();
432
+ for (const token of wrParseClassName(className)) {
433
+ for (const property of properties) {
434
+ const match = matchOf(property, token.utility);
435
+ if (!match)
436
+ continue;
437
+ const context = contextOf(token.variants, property);
438
+ if (context && (context.state?.id ?? undefined) === state) {
439
+ entries.set(`${property.id}@${context.breakpoint.id}`, {
440
+ property: property.id, breakpoint: context.breakpoint.id, value: match.value, class: token.raw, ...(match.custom ? { custom: true } : {}), ...(context.state ? { state: context.state.id } : {}),
441
+ });
442
+ }
443
+ break;
444
+ }
445
+ }
446
+ return [...entries.values()];
447
+ }
448
+ /** The classes the table does not read: `dark:` and other variants (what the table reads of `hover:`, `focus:` and `active:` is the state of a property that can have one), `!important`, what is not about a property of the table, the project's own classes. They are never touched. */
449
+ export function wrStyleUnmanaged(className, options = {}) {
450
+ const properties = wrStyleProperties(options);
451
+ return wrParseClassName(className)
452
+ .filter(token => !properties.some(p => matchOf(p, token.utility) !== null && contextOf(token.variants, p) !== null))
453
+ .map(token => token.raw);
454
+ }
455
+ /** What a property is at a breakpoint once the narrower screens are counted: its own value, or the nearest smaller breakpoint's. */
456
+ export function wrStyleEffective(entries, property, breakpoint) {
457
+ for (const id of wrStyleCascade(breakpoint)) {
458
+ const entry = entries.find(e => e.property === property && e.breakpoint === id);
459
+ if (entry)
460
+ return { value: entry.value, from: entry.breakpoint, ...(entry.custom ? { custom: true } : {}) };
461
+ }
462
+ return null;
463
+ }
464
+ export class WrStyleError extends Error {
465
+ constructor(code, message) {
466
+ super(message);
467
+ this.code = code;
468
+ }
469
+ }
470
+ /** The state a change is for: none (always), or a state the property can have. */
471
+ function checkedState(id, property, properties) {
472
+ if (id === undefined || id === null)
473
+ return null;
474
+ const state = stateById(id);
475
+ if (!state)
476
+ throw new WrStyleError('unknown-state', `no state "${id}" (there is: ${WR_STATE_IDS.join(', ')})`);
477
+ if (!property.states)
478
+ throw new WrStyleError('invalid-state', `${property.id} cannot change on ${state.label.toLowerCase()} (the ones that can: ${list(properties.filter(p => p.states).map(p => p.id))})`);
479
+ return state;
480
+ }
481
+ const list = (items, max = 40) => (items.length > max ? `${items.slice(0, max).join(', ')}…` : items.join(', '));
482
+ /** The className once every change is made, in order. A value that is not the property's, a property or a breakpoint that does not exist: a `WrStyleError` that says what there is. */
483
+ export function wrStyleApply(className, changes, options = {}) {
484
+ const properties = wrStyleProperties(options);
485
+ const lookup = byId(properties);
486
+ let tokens = wrParseClassName(className);
487
+ for (const change of changes) {
488
+ const property = lookup.get(change.property);
489
+ if (!property)
490
+ throw new WrStyleError('unknown-property', `no style property "${change.property}" (there is: ${list(properties.map(p => p.id), 60)})`);
491
+ const breakpoint = breakpointOf(change.breakpoint);
492
+ if (!breakpoint)
493
+ throw new WrStyleError('unknown-breakpoint', `no breakpoint "${change.breakpoint}" (there is: ${WR_BREAKPOINT_IDS.join(', ')})`);
494
+ const state = checkedState(change.state, property, properties);
495
+ const wanted = change.value === null ? null : wrStyleParseValue(property, change.value);
496
+ if (change.value !== null && !wanted) {
497
+ throw new WrStyleError('invalid-value', `${property.id} cannot be "${change.value}" (it can be: ${list(property.values.map(v => v.value))}${property.free ? `, or ${wrStyleFreeHint(property.free)}` : ''}${property.hex ? ', or a color like #22c55e' : ''})`);
498
+ }
499
+ const variants = variantsOf(breakpoint, state, property);
500
+ // What this replaces: the property's own classes at this breakpoint (the table's, and the values outside it), and the narrower properties it includes
501
+ const replaced = [property, ...(change.value === null ? [] : includedOf(property, lookup))];
502
+ const written = wanted ? { raw: [...variants, wanted.class].join(':'), variants, utility: wanted.class } : null;
503
+ tokens = place(tokens, property, replaced, variants, written);
504
+ }
505
+ return tokens.map(t => t.raw).join(' ');
506
+ }
507
+ /** The properties a property replaces when it is set (padding: padding-x, padding-top…), all the way down. */
508
+ function includedOf(property, lookup) {
509
+ const found = new Set();
510
+ const visit = (p) => (p.includes ?? []).forEach((id) => {
511
+ const inner = lookup.get(id);
512
+ if (inner && !found.has(inner)) {
513
+ found.add(inner);
514
+ visit(inner);
515
+ }
516
+ });
517
+ visit(property);
518
+ return [...found];
519
+ }
520
+ /**
521
+ * Puts `written` (or nothing) where the property's class was at these variants, taking out what it replaces. Where it goes: where the property's own class was, or else where the first
522
+ * class it replaces was, or else last. The ONE step that writing a value and copying a class both take.
523
+ */
524
+ function place(tokens, property, replaced, variants, written) {
525
+ const isReplaced = (token) => sameVariants(token.variants, variants) && replaced.some(p => matchOf(p, token.utility) !== null);
526
+ const own = tokens.findIndex(token => sameVariants(token.variants, variants) && matchOf(property, token.utility) !== null);
527
+ const spot = own !== -1 ? own : tokens.findIndex(isReplaced);
528
+ return spot === -1
529
+ ? [...tokens, ...(written ? [written] : [])]
530
+ : tokens.flatMap((token, index) => (index === spot ? (written ? [written] : []) : isReplaced(token) ? [] : [token]));
531
+ }
532
+ function managedClasses(tokens, properties) {
533
+ const managed = new Map();
534
+ for (const token of tokens) {
535
+ for (const property of properties) {
536
+ if (matchOf(property, token.utility) === null)
537
+ continue;
538
+ const context = contextOf(token.variants, property);
539
+ if (context)
540
+ managed.set(token, { token, property, breakpoint: context.breakpoint, state: context.state });
541
+ break;
542
+ }
543
+ }
544
+ return managed;
545
+ }
546
+ /**
547
+ * The classes of one screen put on another (`to` a breakpoint), or on every screen at once (`to: 'all'`). The classes are copied as they are written, so what the table does not offer
548
+ * (`w-[320px]`) goes too; only the style classes move: hover:, dark: and the project's own stay where they are.
549
+ * - to a breakpoint: for each property `from` says, `to` says the same; the same properties are replaced (a wider one takes out the narrower it includes: p replaces px), the rest of
550
+ * `to` stays. What `from` inherits from narrower screens is not copied: it is not written there.
551
+ * - to 'all': one clean list. The look `from` has once the narrower screens are counted becomes what every screen has; every class of another breakpoint is taken away. From "All screens"
552
+ * that is only taking the overrides away.
553
+ * `only`: just these properties (and the ones they include or are included by: padding and padding-x say the same thing); every other class stays as it is. `state`: the classes of that state
554
+ * (hover…) and no others; without it, the ones that hold always. Pure; an unknown breakpoint or state is a `WrStyleError`.
555
+ */
556
+ export function wrStyleCopyTo(className, from, to, options = {}, only, state) {
557
+ const source = breakpointOf(from);
558
+ const target = to === 'all' ? undefined : breakpointOf(to);
559
+ if (!source)
560
+ throw new WrStyleError('unknown-breakpoint', `no breakpoint "${from}" (there is: ${WR_BREAKPOINT_IDS.join(', ')})`);
561
+ if (to !== 'all' && !target)
562
+ throw new WrStyleError('unknown-breakpoint', `no breakpoint "${to}" (there is: all, ${WR_BREAKPOINT_IDS.join(', ')})`);
563
+ const properties = wrStyleProperties(options);
564
+ const lookup = byId(properties);
565
+ let tokens = wrParseClassName(className);
566
+ const managed = managedClasses(tokens, properties);
567
+ if (state !== undefined && !stateById(state))
568
+ throw new WrStyleError('unknown-state', `no state "${state}" (there is: ${WR_STATE_IDS.join(', ')})`);
569
+ const scope = only ? relatedProperties(only, properties, lookup) : null;
570
+ const inScope = (m) => (m.state?.id ?? undefined) === state && (!scope || scope.has(m.property.id));
571
+ if (target) {
572
+ if (target.id === source.id)
573
+ return className;
574
+ // The last class of a property at the source is what it says (as when reading)
575
+ const says = new Map();
576
+ for (const m of managed.values())
577
+ if (m.breakpoint.id === source.id && inScope(m))
578
+ says.set(m.property.id, m);
579
+ for (const { token, property, state: held } of says.values()) {
580
+ const variants = variantsOf(target, held, property);
581
+ tokens = place(tokens, property, [property, ...includedOf(property, lookup)], variants, { raw: [...variants, token.utility].join(':'), variants, utility: token.utility });
582
+ }
583
+ return tokens.map(t => t.raw).join(' ');
584
+ }
585
+ // To every screen: what each property is where `from` stands (the nearest breakpoint that says it, narrower first)…
586
+ const cascade = wrStyleCascade(source.id);
587
+ const effective = new Map();
588
+ for (const m of managed.values()) {
589
+ const rank = cascade.indexOf(m.breakpoint.id);
590
+ if (rank === -1 || !inScope(m))
591
+ continue;
592
+ const have = effective.get(m.property.id);
593
+ // Smaller rank = closer to `from`; at the same breakpoint the last class is what is read
594
+ if (!have || rank < have.rank || (rank === have.rank && tokens.indexOf(m.token) > tokens.indexOf(have.m.token)))
595
+ effective.set(m.property.id, { m, rank });
596
+ }
597
+ // …except a narrower property a wider one, said closer to `from`, has taken over (md:p-8 over px-4: at md the padding is 8 all round)
598
+ for (const [id, mine] of effective) {
599
+ const covered = [...effective].some(([otherId, other]) => otherId !== id && other.rank < mine.rank && includedOf(other.m.property, lookup).some(p => p.id === id));
600
+ if (covered)
601
+ effective.delete(id);
602
+ }
603
+ const kept = new Set([...effective.values()].map(e => e.m.token));
604
+ return tokens.flatMap((token) => {
605
+ const m = managed.get(token);
606
+ if (!m || !inScope(m))
607
+ return [token.raw];
608
+ // What is kept goes to the base: no screen, the state and the property's own variants stay
609
+ return kept.has(token) ? [[...variantsOf(BASE_BREAKPOINT, m.state, m.property), token.utility].join(':')] : [];
610
+ }).join(' ');
611
+ }
612
+ /** The properties that say the same thing as these: themselves, what they include (padding: padding-x, padding-top…), and what includes them. */
613
+ function relatedProperties(ids, properties, lookup) {
614
+ const related = new Set();
615
+ for (const id of ids) {
616
+ const property = lookup.get(id);
617
+ if (!property)
618
+ continue;
619
+ related.add(id);
620
+ includedOf(property, lookup).forEach(p => related.add(p.id));
621
+ for (const other of properties)
622
+ if (includedOf(other, lookup).some(p => p.id === id))
623
+ related.add(other.id);
624
+ }
625
+ return related;
626
+ }
627
+ /**
628
+ * Makes changes hold on every screen: each is made at the breakpoint it names, and then what the element looks like there, for those properties only, becomes what every screen has (the
629
+ * overrides of the other screens for them are taken away). A change that takes a value away (`null`) takes it away from every screen. What the changes do not touch keeps its overrides.
630
+ * This is what "All screens" does in the editor: you set it where you are looking, and it holds everywhere. Pure.
631
+ */
632
+ export function wrStyleApplyEverywhere(className, changes, options = {}) {
633
+ let next = wrStyleApply(className, changes, options);
634
+ const removed = changes.filter(c => c.value === null);
635
+ if (removed.length > 0) {
636
+ next = wrStyleApply(next, removed.flatMap(c => WR_BREAKPOINT_IDS.map(breakpoint => ({ property: c.property, breakpoint, value: null, ...(c.state ? { state: c.state } : {}) }))), options);
637
+ }
638
+ // What was made is held for each screen and state it was made at
639
+ const held = new Map();
640
+ for (const c of changes) {
641
+ if (c.value === null)
642
+ continue;
643
+ const key = `${c.breakpoint}|${c.state ?? ''}`;
644
+ const entry = held.get(key) ?? { breakpoint: c.breakpoint, ...(c.state ? { state: c.state } : {}), ids: [] };
645
+ entry.ids.push(c.property);
646
+ held.set(key, entry);
647
+ }
648
+ for (const { breakpoint, state, ids } of held.values())
649
+ next = wrStyleCopyTo(next, breakpoint, 'all', options, ids, state);
650
+ return next;
651
+ }
652
+ // ── Layering classes ─────────────────────────────────────────────────────────
653
+ /**
654
+ * Two groups of classes put together, `over` winning where both say something: what an element looks like when a shared style (`under`) is written out on it and the element has classes
655
+ * of its own (`over`). A class of `under` is left out when `over` says the same property at the same breakpoint, or a wider property that includes it there (p over px); everything else
656
+ * of `under` stays, then all of `over`. Pure.
657
+ */
658
+ export function wrStyleLayer(under, over, options = {}) {
659
+ const properties = wrStyleProperties(options);
660
+ const lookup = byId(properties);
661
+ const overTokens = wrParseClassName(over);
662
+ const overSays = new Set();
663
+ for (const m of managedClasses(overTokens, properties).values()) {
664
+ for (const property of [m.property, ...includedOf(m.property, lookup)])
665
+ overSays.add(`${property.id}@${m.breakpoint.id}@${m.state?.id ?? ''}`);
666
+ }
667
+ const raws = new Set(overTokens.map(t => t.raw));
668
+ const underTokens = wrParseClassName(under);
669
+ const underManaged = managedClasses(underTokens, properties);
670
+ const kept = underTokens.filter((token) => {
671
+ const m = underManaged.get(token);
672
+ return !raws.has(token.raw) && !(m && overSays.has(`${m.property.id}@${m.breakpoint.id}@${m.state?.id ?? ''}`));
673
+ });
674
+ return [...kept, ...overTokens].map(t => t.raw).join(' ');
675
+ }
676
+ export function wrStyleDescribe(entries, options = {}) {
677
+ const lookup = byId(wrStyleProperties(options));
678
+ return entries.flatMap((entry) => {
679
+ const property = lookup.get(entry.property);
680
+ const breakpoint = breakpointOf(entry.breakpoint);
681
+ if (!property || !breakpoint)
682
+ return [];
683
+ const known = entry.value === null ? undefined : wrStyleParseValue(property, entry.value);
684
+ const valueLabel = known?.label ?? `custom (${entry.class.split(':').pop()})`;
685
+ const css = known?.css;
686
+ const state = stateById(entry.state);
687
+ return [{
688
+ property: property.id, propertyLabel: property.label, breakpoint: breakpoint.id, breakpointLabel: breakpoint.label, value: entry.value, valueLabel, ...(css ? { css } : {}),
689
+ class: entry.class, ...(entry.custom ? { custom: true } : {}), ...(state ? { state: state.id, stateLabel: state.label } : {}),
690
+ sentence: `${property.label} is ${valueLabel}${css && css !== valueLabel ? ` (${css})` : ''}${state ? `, on ${state.label.toLowerCase()}` : ''}${breakpoint.id === 'base' ? '' : `, ${breakpoint.label.toLowerCase()}`}.`,
691
+ }];
692
+ });
693
+ }
694
+ const sidesCompound = (kind, label, description, group) => {
695
+ const top = `${kind}-top`, right = `${kind}-right`, bottom = `${kind}-bottom`, left = `${kind}-left`;
696
+ return {
697
+ id: kind, label, description, group,
698
+ modes: [
699
+ { id: 'all', label: 'All', description: 'One value for the four sides.', parts: [{ property: kind, label: 'All sides', covers: [top, right, bottom, left] }] },
700
+ {
701
+ id: 'axis', label: 'Pairs', description: 'One value for top and bottom, one for left and right.',
702
+ parts: [{ property: `${kind}-y`, label: 'Top and bottom', covers: [top, bottom] }, { property: `${kind}-x`, label: 'Left and right', covers: [left, right] }],
703
+ },
704
+ {
705
+ id: 'sides', label: 'Each', description: 'A value for each side.',
706
+ parts: [{ property: top, label: 'Top', covers: [top] }, { property: right, label: 'Right', covers: [right] }, { property: bottom, label: 'Bottom', covers: [bottom] }, { property: left, label: 'Left', covers: [left] }],
707
+ },
708
+ ],
709
+ };
710
+ };
711
+ export const WR_STYLE_COMPOUNDS = [
712
+ sidesCompound('padding', 'Padding', 'Space inside the element, between its edge and its content.', 'spacing'),
713
+ sidesCompound('margin', 'Margin', 'Space outside the element, between it and its neighbours.', 'spacing'),
714
+ {
715
+ id: 'gap', label: 'Gap', description: 'Space between the children of a flex or grid element.', group: 'spacing',
716
+ modes: [
717
+ { id: 'all', label: 'All', description: 'One value both ways.', parts: [{ property: 'gap', label: 'Both ways', covers: ['gap-x', 'gap-y'] }] },
718
+ { id: 'axis', label: 'Each', description: 'A value for the columns and one for the rows.', parts: [{ property: 'gap-x', label: 'Columns', covers: ['gap-x'] }, { property: 'gap-y', label: 'Rows', covers: ['gap-y'] }] },
719
+ ],
720
+ },
721
+ sidesCompound('border-width', 'Border width', 'How thick the border is, all round, top and bottom against left and right, or on each side. It needs a color to be seen.', 'border'),
722
+ {
723
+ id: 'border-radius', label: 'Corners', description: 'How round the corners are: all together, or each corner by itself.', group: 'border',
724
+ modes: [
725
+ { id: 'all', label: 'All', description: 'One value for the four corners.', parts: [{ property: 'border-radius', label: 'All corners', covers: ['border-radius-tl', 'border-radius-tr', 'border-radius-bl', 'border-radius-br'] }] },
726
+ {
727
+ id: 'sides', label: 'Each', description: 'A value for each corner.',
728
+ parts: [
729
+ { property: 'border-radius-tl', label: 'Top left', covers: ['border-radius-tl'] }, { property: 'border-radius-tr', label: 'Top right', covers: ['border-radius-tr'] },
730
+ { property: 'border-radius-bl', label: 'Bottom left', covers: ['border-radius-bl'] }, { property: 'border-radius-br', label: 'Bottom right', covers: ['border-radius-br'] },
731
+ ],
732
+ },
733
+ ],
734
+ },
735
+ ];
736
+ const ownAt = (entries, property, breakpoint) => entries.find(e => e.property === property && e.breakpoint === breakpoint);
737
+ /** The mode that shows what an element has at a breakpoint: the finest one with something set in it (the panel opens there). */
738
+ export function wrStyleCompoundMode(compound, entries, breakpoint) {
739
+ for (const mode of [...compound.modes].reverse()) {
740
+ if (mode.parts.some(part => ownAt(entries, part.property, breakpoint)))
741
+ return mode.id;
742
+ }
743
+ return compound.modes[0].id;
744
+ }
745
+ /**
746
+ * What a part of a multi-side model comes to at a breakpoint once everything that says something about it is counted: its own value, the coarser part that covers it (`padding`
747
+ * for `padding-top`), and the narrower screens (mobile first). So in "each side" mode a side that has no class of its own shows what the whole says.
748
+ */
749
+ export function wrStyleCompoundValue(compound, entries, breakpoint, part) {
750
+ const parts = compound.modes.flatMap(m => m.parts);
751
+ const own = ownAt(entries, part.property, breakpoint);
752
+ for (const bp of wrStyleCascade(breakpoint)) {
753
+ const values = new Set();
754
+ let said = false;
755
+ for (const slot of part.covers) {
756
+ const says = parts.filter(p => p.covers.includes(slot) && ownAt(entries, p.property, bp)).sort((a, b) => a.covers.length - b.covers.length)[0];
757
+ if (says)
758
+ said = true;
759
+ values.add(says ? ownAt(entries, says.property, bp).value : null);
760
+ }
761
+ if (said)
762
+ return { value: values.size === 1 ? [...values][0] : null, own: bp === breakpoint && own !== undefined, from: bp, mixed: values.size > 1 };
763
+ }
764
+ return { value: null, own: false, from: breakpoint, mixed: false };
765
+ }
766
+ /**
767
+ * What changing the mode of a multi-side property asks for, so that the element looks the same: going to a finer mode copies the value to the parts (and takes the
768
+ * coarser ones away); going to a coarser one needs the parts it joins to agree (to be the same value, or all none), and says why when they do not. A value outside the table
769
+ * (`px-[3px]`) cannot be copied: it is said, not lost.
770
+ */
771
+ export function wrStyleCompoundSwitch(compound, entries, breakpoint, to) {
772
+ const target = compound.modes.find(m => m.id === to);
773
+ if (!target)
774
+ return { ok: false, reason: `no mode "${to}"` };
775
+ // Already there: nothing to move
776
+ if (wrStyleCompoundMode(compound, entries, breakpoint) === to)
777
+ return { ok: true, changes: [] };
778
+ const slots = compound.modes[compound.modes.length - 1].parts.map(p => p.property);
779
+ const parts = compound.modes.flatMap(m => m.parts);
780
+ const used = parts.filter(p => ownAt(entries, p.property, breakpoint));
781
+ if (used.some(p => ownAt(entries, p.property, breakpoint)?.value === null))
782
+ return { ok: false, reason: 'a value outside the options (written by hand in the code) cannot be moved to another mode' };
783
+ // What each finest slot comes to: the most specific part that says something about it (as the CSS cascade has it)
784
+ const slotValue = new Map();
785
+ for (const slot of slots) {
786
+ const says = used.filter(p => p.covers.includes(slot)).sort((a, b) => a.covers.length - b.covers.length)[0];
787
+ slotValue.set(slot, says ? ownAt(entries, says.property, breakpoint).value : null);
788
+ }
789
+ const changes = [];
790
+ for (const part of used)
791
+ if (!target.parts.some(t => t.property === part.property))
792
+ changes.push({ property: part.property, breakpoint, value: null });
793
+ for (const part of target.parts) {
794
+ const values = new Set(part.covers.map(slot => slotValue.get(slot) ?? null));
795
+ if (values.size > 1)
796
+ return { ok: false, reason: `${part.label.toLowerCase()} are not the same value` };
797
+ const value = [...values][0] ?? null;
798
+ const now = ownAt(entries, part.property, breakpoint)?.value ?? null;
799
+ if (value !== now)
800
+ changes.push({ property: part.property, breakpoint, value });
801
+ }
802
+ return { ok: true, changes };
803
+ }
804
+ export function wrStyleCatalog(options = {}) {
805
+ const colorTokens = [...(options.colorTokens ?? WR_DEFAULT_COLOR_TOKENS)];
806
+ const colorSwatches = options.colorSwatches;
807
+ return {
808
+ breakpoints: WR_BREAKPOINTS.map(b => ({ id: b.id, label: b.label, description: b.description, minWidth: b.minWidth })),
809
+ note: 'Mobile first: a value set at a breakpoint holds from that width up, until a wider breakpoint sets another. "base" is every screen. A property marked `states` can also be set for hover, focus or pressed.',
810
+ groups: WR_STYLE_GROUPS,
811
+ states: WR_STATES.map(({ id, label, description }) => ({ id, label, description })),
812
+ colorTokens,
813
+ properties: wrStyleProperties({ colorTokens, colorSwatches }).map(p => ({
814
+ id: p.id, label: p.label, description: p.description, group: p.group, control: p.control,
815
+ ...(p.includes ? { includes: [...p.includes], includesNote: 'Setting it replaces these at the same breakpoint.' } : {}),
816
+ ...(p.free ? { free: { ...p.free, hint: wrStyleFreeHint(p.free) } } : {}),
817
+ ...(p.compound ? { compound: p.compound } : {}),
818
+ ...(p.states ? { states: true } : {}),
819
+ ...(p.hex ? { hex: true } : {}),
820
+ ...(p.shortcuts ? { shortcuts: [...p.shortcuts] } : {}),
821
+ values: p.values.map(v => ({ value: v.value, label: v.label, ...(v.css ? { css: v.css } : {}), class: v.class, ...(v.swatch ? { swatch: v.swatch } : {}) })),
822
+ })),
823
+ compounds: [...WR_STYLE_COMPOUNDS],
824
+ };
825
+ }
826
+ // ── What a panel offers ──────────────────────────────────────────────────────
827
+ /**
828
+ * What the panel offers: the minimum that is needed, and nothing else. Space inside and outside, the width and the height, the border and the shadow, the text (its size and weight, and the
829
+ * color), the background. Layout and alignment (flex, justify…) are not offered here (alignment has its own grid): they are the controls that confuse (flex and alignments often do not do what is expected) and the ones that make a site fragile. This is
830
+ * ONLY the panel's choice: the catalog, the operations, the API and the MCP take every property, so an AI or a tool can still set any of them, and adding one back to the
831
+ * panel is a word in this list.
832
+ */
833
+ const OFFERED_ITEMS = new Set([
834
+ 'padding', 'margin', 'gap', 'width', 'height', 'max-width', 'border-width', 'border-color', 'border-radius', 'shadow', 'font-size', 'font-weight', 'text-align', 'text-color', 'background-color',
835
+ 'display', 'flex-direction', 'flex-wrap', 'align-items', 'justify-content',
836
+ 'opacity', 'scale', 'translate-y', 'text-decoration', 'transition', 'duration', 'ease', 'animation',
837
+ 'gradient-direction', 'gradient-from', 'gradient-via', 'gradient-to',
838
+ ]);
839
+ /** The text properties: a text written in the editor (WrText) has its own toolbar (and its content is HTML in the JSON, which wins over a class on the element; but the alignment, see `wrStyleOffered`), and an image or a video has no text. */
840
+ const TEXT_ITEMS = new Set(['font-size', 'font-weight', 'text-align', 'text-color', 'text-decoration']);
841
+ /** The properties of a box that lays out what it holds: only a box (a section) has them. */
842
+ const LAYOUT_ITEMS = new Set(['gap', 'display', 'flex-direction', 'flex-wrap', 'align-items', 'justify-content']);
843
+ const NO_TEXT_KINDS = new Set(['text', 'image', 'video', 'iframe']);
844
+ /** Whether the panel offers a control for this kind of element (`text`, `section`, `image`…). `item` is a property id, or a model's (`padding`). */
845
+ export function wrStyleOffered(item, kind) {
846
+ if (!OFFERED_ITEMS.has(item))
847
+ return false;
848
+ if (LAYOUT_ITEMS.has(item) && kind !== undefined && kind !== 'section')
849
+ return false;
850
+ // The alignment of a text is the one thing of its own the panel offers: the paragraphs hold none, so they follow the element's, at the breakpoint the panel is on
851
+ if (kind === 'text' && item === 'text-align')
852
+ return true;
853
+ return !(kind && NO_TEXT_KINDS.has(kind) && TEXT_ITEMS.has(item));
854
+ }
855
+ const WIDTH_HANDLE = { property: 'width', edge: 'right', label: 'Width' };
856
+ const HEIGHT_HANDLE = { property: 'height', edge: 'bottom', label: 'Height' };
857
+ /** Which handles each kind of element has: one table, so what can be dragged is said for each component and nowhere else. */
858
+ const HANDLES = {
859
+ section: [WIDTH_HANDLE, HEIGHT_HANDLE],
860
+ image: [WIDTH_HANDLE, HEIGHT_HANDLE],
861
+ video: [WIDTH_HANDLE, HEIGHT_HANDLE],
862
+ iframe: [WIDTH_HANDLE, HEIGHT_HANDLE],
863
+ text: [WIDTH_HANDLE],
864
+ link: [WIDTH_HANDLE],
865
+ button: [WIDTH_HANDLE],
866
+ };
867
+ export const wrStyleHandles = (kind) => (kind ? HANDLES[kind] ?? [] : []);
868
+ const QUICK = {
869
+ section: ['width', 'height', 'padding', 'margin', 'gap', 'background-color', 'align', 'duplicate', 'delete'],
870
+ image: ['width', 'height', 'margin', 'duplicate', 'delete'],
871
+ video: ['width', 'height', 'margin', 'duplicate', 'delete'],
872
+ iframe: ['width', 'height', 'margin', 'duplicate', 'delete'],
873
+ text: ['width', 'margin', 'duplicate', 'delete'],
874
+ link: ['width', 'padding', 'background-color', 'duplicate', 'delete'],
875
+ button: ['width', 'padding', 'background-color', 'duplicate', 'delete'],
876
+ };
877
+ export const wrStyleQuick = (kind) => (kind ? QUICK[kind] ?? ['duplicate', 'delete'] : []);
878
+ const SIDE_LABEL = { top: 'top', right: 'right', bottom: 'bottom', left: 'left' };
879
+ const spaceHandles = (kind) => ['top', 'right', 'bottom', 'left'].map(side => ({
880
+ property: `${kind}-${side}`, kind, side, all: kind, label: `${kind === 'padding' ? 'Padding' : 'Margin'} (${SIDE_LABEL[side]})`,
881
+ }));
882
+ /** Which sides each kind of element can have dragged on the page: a box has room inside and outside, what is not a box (an image) only outside. */
883
+ const SPACE_HANDLES = {
884
+ section: [...spaceHandles('padding'), ...spaceHandles('margin')],
885
+ link: [...spaceHandles('padding'), ...spaceHandles('margin')],
886
+ button: [...spaceHandles('padding'), ...spaceHandles('margin')],
887
+ image: spaceHandles('margin'), video: spaceHandles('margin'), iframe: spaceHandles('margin'), text: spaceHandles('margin'),
888
+ };
889
+ export const wrStyleSpaceHandles = (kind) => (kind ? SPACE_HANDLES[kind] ?? [] : []);
890
+ /**
891
+ * What a padding or a margin dragged to `px` comes to: a whole step of the spacing scale (a quarter of a rem, 4px: nothing finer is asked of a hand), never negative (a drag does not make a
892
+ * negative margin). `property` is the side (`padding-top`) or all of them (`padding`). null for a property that is not one of these. Pure, like `wrStyleDrag`.
893
+ */
894
+ export function wrStyleSpaceDrag(property, px, rem = 16) {
895
+ const spec = wrStyleProperties().find(p => p.id === property);
896
+ if (!spec || !/^(padding|margin)(-(top|right|bottom|left))?$/.test(property) || !spec.free)
897
+ return null;
898
+ const step = Math.max(0, Math.round(px / (rem / 4)));
899
+ const parsed = wrStyleParseValue(spec, String(step));
900
+ const css = wrStylePreviewCss(property, String(step));
901
+ if (!parsed || !css)
902
+ return null;
903
+ return { value: parsed.value, class: parsed.class, label: `${parsed.class} · ${Math.round(step * (rem / 4))}px`, css: Object.values(css)[0], px: step * (rem / 4) };
904
+ }
905
+ /** The smallest step number (`w-N`) that is not under `floor` px: the number a field takes as its minimum. 0 when there is no floor. */
906
+ export const wrStyleFloorStep = (floor, rem = 16) => floor && floor > 0 ? Math.ceil(floor / (rem / 4)) : 0;
907
+ /** How near to a fraction of the parent (as a part of the parent) a width has to be to snap to it. */
908
+ const SNAP_TO_FRACTION = 0.02;
909
+ const FRACTION_VALUES = [...FRACTIONS.map(f => ({ value: f, ratio: Number(f.split('/')[0]) / Number(f.split('/')[1]) })), { value: 'full', ratio: 1 }];
910
+ /**
911
+ * What a size dragged to `px` comes to: a whole step of the spacing scale (a quarter of a rem, 4px: nothing finer is asked of a hand), or, for a width, a fraction of the parent when
912
+ * it comes close to one. null for a property that is not a size. Pure, so the same answer is given for the person dragging and for a test.
913
+ */
914
+ export function wrStyleDrag(property, px, context = {}) {
915
+ const spec = wrStyleProperties().find(p => p.id === property);
916
+ if (!spec?.free || !Number.isFinite(px))
917
+ return null;
918
+ const unit = (context.rem ?? 16) / 4;
919
+ const parent = context.parent ?? 0;
920
+ const floor = context.floor ?? 0;
921
+ const near = property === 'width' && parent > 0 && context.fractions !== false
922
+ ? FRACTION_VALUES.map(f => ({ ...f, distance: Math.abs(px / parent - f.ratio) })).filter(f => f.distance <= SNAP_TO_FRACTION && f.ratio * parent >= floor).sort((a, b) => a.distance - b.distance)[0]
923
+ : undefined;
924
+ const step = Math.min(spec.free.max, Math.max(1, wrStyleFloorStep(floor, context.rem), Math.round(px / unit)));
925
+ const value = near ? near.value : String(step);
926
+ const known = wrStyleParseValue(spec, value);
927
+ if (!known)
928
+ return null;
929
+ const shown = near ? Math.round(near.ratio * parent) : step * unit;
930
+ return { value, class: known.class, label: `${known.class} · ${near ? `${Math.round(near.ratio * 10000) / 100}% · ` : ''}${shown}px`, css: near ? `${Math.round(near.ratio * 10000) / 100}%` : `${step / 4}rem`, px: shown };
931
+ }
932
+ export const WR_STYLE_ALIGN_SIDES = ['start', 'center', 'end'];
933
+ /** The kinds of element whose children can be aligned like this: the boxes (a text, a button, an image have no children to place). */
934
+ export const wrStyleCanAlign = (kind) => kind === 'section';
935
+ const effectiveOf = (entries, property, breakpoint) => {
936
+ for (const id of wrStyleCascade(breakpoint)) {
937
+ const entry = ownAt(entries, property, id);
938
+ if (entry)
939
+ return entry.value;
940
+ }
941
+ return null;
942
+ };
943
+ /**
944
+ * How the box lays out its children at a breakpoint, for aligning them: a plain box (it becomes a column of flex when it is aligned: its children stack as they did), a flex row, a
945
+ * flex column; or why it cannot be aligned this way (a grid places by cells, a reversed direction swaps the sides, a hidden box has nothing to align).
946
+ */
947
+ function layoutOf(entries, breakpoint) {
948
+ const display = effectiveOf(entries, 'display', breakpoint);
949
+ const direction = effectiveOf(entries, 'flex-direction', breakpoint);
950
+ if (display === 'grid')
951
+ return { reason: 'a grid places its children by cells, not by alignment' };
952
+ if (display === 'none')
953
+ return { reason: 'it is hidden at this width' };
954
+ if (display === 'flex' || display === 'inline-flex') {
955
+ if (direction === 'row-reverse' || direction === 'col-reverse')
956
+ return { reason: 'its direction is reversed, which swaps the sides' };
957
+ return { kind: direction === 'col' ? 'col' : 'row' };
958
+ }
959
+ return { kind: 'block' };
960
+ }
961
+ const sideOf = (value) => (value === 'start' || value === 'center' || value === 'end' ? value : null);
962
+ /** Where the children sit now (the cell of the grid that is on), or why this box cannot be aligned. */
963
+ export function wrStyleAlignRead(entries, breakpoint) {
964
+ const layout = layoutOf(entries, breakpoint);
965
+ if ('reason' in layout)
966
+ return { ok: false, reason: layout.reason };
967
+ const items = sideOf(effectiveOf(entries, 'align-items', breakpoint));
968
+ const justify = sideOf(effectiveOf(entries, 'justify-content', breakpoint));
969
+ const height = effectiveOf(entries, 'height', breakpoint);
970
+ const needsHeight = height === null || ['auto', 'fit', 'min', 'max'].includes(height);
971
+ if (layout.kind === 'block')
972
+ return { ok: true, horizontal: null, vertical: null, needsHeight };
973
+ return layout.kind === 'row' ? { ok: true, horizontal: justify, vertical: items, needsHeight } : { ok: true, horizontal: items, vertical: justify, needsHeight };
974
+ }
975
+ /**
976
+ * What to write to put the children at a place of the grid (horizontal × vertical), ALL at once: the layout that makes it work (a plain box becomes a flex column: its
977
+ * children stack as before) and both alignments, whichever axis each is on. Written separately these pieces do nothing (`items-center` on a block box does not move anything), which
978
+ * is why they are one change.
979
+ */
980
+ export function wrStyleAlignSet(entries, breakpoint, horizontal, vertical) {
981
+ const layout = layoutOf(entries, breakpoint);
982
+ if ('reason' in layout)
983
+ return { ok: false, reason: layout.reason };
984
+ const change = (property, value) => ({ property, breakpoint, value });
985
+ const [items, justify] = layout.kind === 'row' ? [vertical, horizontal] : [horizontal, vertical];
986
+ return {
987
+ ok: true,
988
+ changes: [...(layout.kind === 'block' ? [change('display', 'flex'), change('flex-direction', 'col')] : []), change('align-items', items), change('justify-content', justify)],
989
+ };
990
+ }
991
+ /** What to write to take the alignment away at a breakpoint (the box stays as it is laid out). */
992
+ export const wrStyleAlignClear = (breakpoint) => [{ property: 'align-items', breakpoint, value: null }, { property: 'justify-content', breakpoint, value: null }];
993
+ const layoutRecipe = (...entries) => entries.map(([property, breakpoint, value]) => ({ property, breakpoint, value }));
994
+ /** The layouts, in the order they are offered: the one table that the panel, the quick menu, the API and the tools read. */
995
+ export const WR_STYLE_LAYOUTS = [
996
+ { id: 'block', label: 'Plain', description: 'Children one under the other, as they come.', recipe: [], gap: null },
997
+ { id: 'stack', label: 'Stack', description: 'Children in a column, with a gap.', recipe: layoutRecipe(['display', 'base', 'flex'], ['flex-direction', 'base', 'col']), gap: '4' },
998
+ { id: 'row', label: 'Row', description: 'Children side by side from tablet up, one under the other on a phone.', recipe: layoutRecipe(['display', 'base', 'flex'], ['flex-direction', 'base', 'col'], ['flex-direction', 'md', 'row']), gap: '4' },
999
+ { id: 'columns-2', label: '2 columns', description: 'Two columns from tablet up, one on a phone.', recipe: layoutRecipe(['display', 'base', 'grid'], ['grid-columns', 'base', '1'], ['grid-columns', 'md', '2']), gap: '6' },
1000
+ { id: 'columns-3', label: '3 columns', description: 'Three columns on desktop, two on a tablet, one on a phone.', recipe: layoutRecipe(['display', 'base', 'grid'], ['grid-columns', 'base', '1'], ['grid-columns', 'md', '2'], ['grid-columns', 'lg', '3']), gap: '6' },
1001
+ { id: 'grid', label: 'Grid', description: 'Four columns on desktop, two on a tablet and a phone.', recipe: layoutRecipe(['display', 'base', 'grid'], ['grid-columns', 'base', '2'], ['grid-columns', 'lg', '4']), gap: '6' },
1002
+ ];
1003
+ /** The properties a layout is made of (what a new layout takes away before it writes its own). */
1004
+ const LAYOUT_PROPERTIES = ['display', 'flex-direction', 'flex-wrap', 'grid-columns'];
1005
+ /** The kinds of element that can change layout: the boxes (they hold other elements). */
1006
+ export const wrStyleCanLayout = (kind) => kind === 'section';
1007
+ /**
1008
+ * Which layout a box has: the id of the one it matches exactly (only its layout properties count, not the gap or the alignment), `null` when it has none of those set, `'custom'`
1009
+ * when it has something that is none of the layouts.
1010
+ */
1011
+ export function wrStyleLayoutRead(entries) {
1012
+ const own = entries.filter(e => LAYOUT_PROPERTIES.includes(e.property) && e.value !== null && e.value !== undefined);
1013
+ if (own.length === 0)
1014
+ return 'block';
1015
+ const key = (list) => list.map(e => `${e.property}@${e.breakpoint}=${e.value}`).sort().join(' ');
1016
+ const mine = key(own);
1017
+ return WR_STYLE_LAYOUTS.find(l => key(l.recipe) === mine)?.id ?? 'custom';
1018
+ }
1019
+ /**
1020
+ * What to write to turn a box into a layout, ALL at once and at every screen width: what the old layout set is taken away (a row's `md:flex-row` must not survive into columns), the
1021
+ * new one is written, and a gap is given if the box had none (columns glued together are not columns). One change list: one undo.
1022
+ */
1023
+ export function wrStyleLayoutSet(entries, layoutId) {
1024
+ const layout = WR_STYLE_LAYOUTS.find(l => l.id === layoutId);
1025
+ if (!layout)
1026
+ return { ok: false, reason: `no layout "${layoutId}" (there is: ${WR_STYLE_LAYOUTS.map(l => l.id).join(', ')})` };
1027
+ const taken = entries.filter(e => LAYOUT_PROPERTIES.includes(e.property) && e.value !== null && e.value !== undefined
1028
+ && !layout.recipe.some(r => r.property === e.property && r.breakpoint === e.breakpoint));
1029
+ const hasGap = entries.some(e => (e.property === 'gap' || e.property === 'gap-x' || e.property === 'gap-y') && e.value !== null && e.value !== undefined);
1030
+ return {
1031
+ ok: true,
1032
+ changes: [
1033
+ ...taken.map(e => ({ property: e.property, breakpoint: e.breakpoint, value: null })),
1034
+ ...layout.recipe.map(r => ({ property: r.property, breakpoint: r.breakpoint, value: r.value })),
1035
+ ...(layout.gap && !hasGap ? [{ property: 'gap', breakpoint: 'base', value: layout.gap }] : []),
1036
+ ],
1037
+ };
1038
+ }
1039
+ // ── Showing a value before it is written ─────────────────────────────────────
1040
+ /** The CSS properties a number can be shown on while it is dragged, for each property of the table (a padding of both sides is two). */
1041
+ const PREVIEW_PROPERTIES = {
1042
+ padding: ['padding'], 'padding-x': ['padding-left', 'padding-right'], 'padding-y': ['padding-top', 'padding-bottom'],
1043
+ 'padding-top': ['padding-top'], 'padding-right': ['padding-right'], 'padding-bottom': ['padding-bottom'], 'padding-left': ['padding-left'],
1044
+ margin: ['margin'], 'margin-x': ['margin-left', 'margin-right'], 'margin-y': ['margin-top', 'margin-bottom'],
1045
+ 'margin-top': ['margin-top'], 'margin-right': ['margin-right'], 'margin-bottom': ['margin-bottom'], 'margin-left': ['margin-left'],
1046
+ gap: ['gap'], 'gap-x': ['column-gap'], 'gap-y': ['row-gap'],
1047
+ width: ['width'], height: ['height'],
1048
+ 'border-width': ['border-width'], 'border-width-x': ['border-left-width', 'border-right-width'], 'border-width-y': ['border-top-width', 'border-bottom-width'],
1049
+ 'border-width-top': ['border-top-width'], 'border-width-right': ['border-right-width'], 'border-width-bottom': ['border-bottom-width'], 'border-width-left': ['border-left-width'],
1050
+ };
1051
+ /** Every CSS property a preview may touch: what a page is asked to show is limited to these, whatever is asked. */
1052
+ export const WR_STYLE_PREVIEW_CSS = new Set(Object.values(PREVIEW_PROPERTIES).flat());
1053
+ const KEYWORD_CSS = {
1054
+ width: { auto: 'auto', full: '100%', screen: '100vw', fit: 'fit-content', min: 'min-content', max: 'max-content' },
1055
+ height: { auto: 'auto', full: '100%', screen: '100vh', fit: 'fit-content', min: 'min-content', max: 'max-content' },
1056
+ };
1057
+ /**
1058
+ * The CSS that shows a value of a number property now (`padding-top` at 6 is `padding-top: 1.5rem`), for a page to wear while the value is dragged: nothing is written, and the
1059
+ * page gives it back. null when it cannot be shown that way (the value is written without a preview). The same table as the classes: a step is a quarter of a rem.
1060
+ */
1061
+ export function wrStylePreviewCss(property, value) {
1062
+ const properties = PREVIEW_PROPERTIES[property];
1063
+ const spec = wrStyleProperties().find(p => p.id === property);
1064
+ if (!properties || !spec)
1065
+ return null;
1066
+ const parsed = wrStyleParseValue(spec, value);
1067
+ if (!parsed)
1068
+ return null;
1069
+ const v = parsed.value;
1070
+ let css;
1071
+ if (v === 'px')
1072
+ css = '1px';
1073
+ else if (/^-?\d+(\.\d+)?$/.test(v))
1074
+ css = spec.free?.kind === 'integer' ? `${v}px` : `${Number(v) / 4}rem`;
1075
+ else if (/^\d+\/\d+$/.test(v))
1076
+ css = `${Math.round((Number(v.split('/')[0]) / Number(v.split('/')[1])) * 10000) / 100}%`;
1077
+ else
1078
+ css = KEYWORD_CSS[property]?.[v] ?? (v === 'auto' ? 'auto' : undefined);
1079
+ return css === undefined ? null : Object.fromEntries(properties.map(p => [p, css]));
1080
+ }
1081
+ //# sourceMappingURL=style.js.map