@getvitops/generator 1.0.0 → 2.0.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.
- package/CHANGELOG.md +743 -0
- package/README.md +8 -1
- package/assets/bricks/elements/icon.php +102 -0
- package/assets/bricks/elements/split.php +8 -5
- package/assets/bricks/load.php +91 -0
- package/assets/css/animation.css +145 -34
- package/assets/css/global.css +19 -3
- package/assets/css/index.css +8 -0
- package/assets/css/layout-utilities.css +764 -0
- package/assets/css/layout.css +50 -584
- package/assets/css/patterns/consent.css +117 -0
- package/assets/css/patterns/details.css +1 -1
- package/assets/css/patterns/reveal.css +92 -0
- package/assets/css/patterns/subgrid.css +7 -2
- package/assets/css/patterns/theme-editor.css +37 -0
- package/assets/css/utilities.css +19 -98
- package/assets/js/consent.js +1 -0
- package/assets/js/deferred.js +1 -1
- package/assets/js/editor.js +1 -1
- package/dist/index.d.mts +668 -252
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +956 -133
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -3
- package/schema.json +3 -3
- package/site.schema.json +554 -506
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ This is the library that powers
|
|
|
10
10
|
import { generate, validate, defaultConfig } from '@getvitops/generator';
|
|
11
11
|
|
|
12
12
|
await generate({
|
|
13
|
-
input: './design-system.json', // path or a config
|
|
13
|
+
input: './design-system.json', // path or object; a design-system.json OR a site config
|
|
14
14
|
format: 'tailwind', // 'tailwind' | 'bricks' | 'design' | 'css'
|
|
15
15
|
outDir: './src/styles',
|
|
16
16
|
});
|
|
@@ -19,6 +19,13 @@ await generate({
|
|
|
19
19
|
## API
|
|
20
20
|
|
|
21
21
|
- `generate({ input, format, outDir })` → writes the chosen format into `outDir`.
|
|
22
|
+
`input` may be a `design-system.json` **or** the larger site config that embeds one — told apart
|
|
23
|
+
by shape, not by filename. For a site config the design system is `designSystem.themes[theme]`
|
|
24
|
+
with its `extends` chain resolved (`theme`, else the config's `defaultTheme`, else `default`), and
|
|
25
|
+
the site config also supplies `site`, so the colour scheme, legal documents and icon sprite it
|
|
26
|
+
asks for need no second path.
|
|
27
|
+
- `resolveInput(raw, { theme, siteEnv })` / `isSiteConfig(raw)` → the discriminator itself, for
|
|
28
|
+
tools that need to do the same routing.
|
|
22
29
|
- `validate(configOrObject)` → `{ ok, errors }` against the schema.
|
|
23
30
|
- `defaultConfig()` → a complete starter `DesignSystem`.
|
|
24
31
|
- `DesignSystemSchema` / `jsonSchema` / `SCHEMA_URL` — the `zod/mini` schema, its JSON Schema,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
/**
|
|
3
|
+
* Vitops — Icon element (Bricks Builder).
|
|
4
|
+
*
|
|
5
|
+
* Renders one icon from the generated sprite (dist/icons.svg) as
|
|
6
|
+
* `<span class="icon"><svg><use href="…#id"></svg></span>`. Pure markup: no
|
|
7
|
+
* JavaScript, no request to an icon API, and the glyph inherits `currentColor`,
|
|
8
|
+
* so a colour utility on this element or an ancestor just works.
|
|
9
|
+
*
|
|
10
|
+
* The sprite only exists when the site config sets `icons.sprite` — without it
|
|
11
|
+
* this renders an empty box rather than failing, which is the same thing a
|
|
12
|
+
* missing icon name does.
|
|
13
|
+
*
|
|
14
|
+
* Two id forms resolve: a semantic name ('menu'), which the build also emits as
|
|
15
|
+
* a set-independent alias so the page survives an icon-set change, and a
|
|
16
|
+
* qualified one ('ph:list' / 'ph--list'). Rendering is delegated to
|
|
17
|
+
* vitops_icon() in load.php so the element, the shortcode and any theme PHP all
|
|
18
|
+
* emit identical markup.
|
|
19
|
+
*
|
|
20
|
+
* Non-nestable: PHP render() runs in the builder canvas too, so the icon appears
|
|
21
|
+
* live while editing.
|
|
22
|
+
*
|
|
23
|
+
* Owned by the framework repo (vitops: bricks/elements/); copied into the theme's
|
|
24
|
+
* dist/bricks/ on build — do not hand-edit in the theme.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
if ( ! defined( 'ABSPATH' ) ) {
|
|
28
|
+
exit;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class Vitops_Element_Icon extends \Bricks\Element {
|
|
32
|
+
public $category = 'vitops';
|
|
33
|
+
public $name = 'vitops-icon';
|
|
34
|
+
public $icon = 'ti-star';
|
|
35
|
+
public $css_selector = '.icon';
|
|
36
|
+
|
|
37
|
+
public function get_label() {
|
|
38
|
+
return esc_html__( 'Icon', 'bricks' );
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public function get_keywords() {
|
|
42
|
+
return array( 'icon', 'svg', 'sprite', 'symbol', 'vitops' );
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public function set_controls() {
|
|
46
|
+
$this->controls['name'] = array(
|
|
47
|
+
'tab' => 'content',
|
|
48
|
+
'label' => esc_html__( 'Icon name', 'bricks' ),
|
|
49
|
+
'type' => 'text',
|
|
50
|
+
'placeholder' => 'menu',
|
|
51
|
+
'description' => esc_html__(
|
|
52
|
+
'A semantic name (menu, close, arrow-right) or a set-specific one (ph:list). Only icons listed in the site config are in the sprite.',
|
|
53
|
+
'bricks'
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
$this->controls['size'] = array(
|
|
58
|
+
'tab' => 'content',
|
|
59
|
+
'label' => esc_html__( 'Size', 'bricks' ),
|
|
60
|
+
'type' => 'text',
|
|
61
|
+
'placeholder' => '1.25em',
|
|
62
|
+
'description' => esc_html__(
|
|
63
|
+
'Any CSS length. Sets --icon-size; em units track the surrounding text.',
|
|
64
|
+
'bricks'
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
$this->controls['label'] = array(
|
|
69
|
+
'tab' => 'content',
|
|
70
|
+
'label' => esc_html__( 'Accessible label', 'bricks' ),
|
|
71
|
+
'type' => 'text',
|
|
72
|
+
'description' => esc_html__(
|
|
73
|
+
'Leave empty for decorative icons — they are then hidden from screen readers. Fill it in only when the icon carries meaning on its own.',
|
|
74
|
+
'bricks'
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public function render() {
|
|
80
|
+
$settings = $this->settings;
|
|
81
|
+
$name = isset( $settings['name'] ) ? $settings['name'] : '';
|
|
82
|
+
|
|
83
|
+
if ( ! function_exists( 'vitops_icon' ) ) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The root attributes carry Bricks' own classes and any the author added,
|
|
88
|
+
// so they are merged onto the same wrapper vitops_icon() emits rather than
|
|
89
|
+
// wrapping it in a second element.
|
|
90
|
+
$this->set_attribute( '_root', 'class', 'icon' );
|
|
91
|
+
|
|
92
|
+
echo '<div ' . $this->render_attributes( '_root' ) . '>'
|
|
93
|
+
. vitops_icon(
|
|
94
|
+
$name,
|
|
95
|
+
array(
|
|
96
|
+
'size' => isset( $settings['size'] ) ? $settings['size'] : '',
|
|
97
|
+
'label' => isset( $settings['label'] ) ? $settings['label'] : '',
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
. '</div>';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Vitops — Split layout element (Bricks Builder).
|
|
4
4
|
*
|
|
5
|
-
* A nestable flex row rendering the framework's `.split` (
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
5
|
+
* A nestable flex row rendering the framework's `.split` (layout.css) — equal columns
|
|
6
|
+
* by default. Set a ratio by adding a class in the CSS-classes field: `split-1-2` /
|
|
7
|
+
* `2-1` / `1-3` / `3-1` / `1-4` / `4-1` / `2-3` / `3-2`, each with an optional
|
|
8
|
+
* `sm-`/`md-`/`lg-`/`xl-` PREFIX to engage the ratio from a container breakpoint.
|
|
9
|
+
* `flex-col` stacks the columns below that breakpoint; `split-reverse` (also
|
|
10
|
+
* breakpoint-prefixable) swaps the two panels. Reversing puts visual order out of
|
|
11
|
+
* step with DOM order, so keep focusable content in only one of the two columns.
|
|
9
12
|
*
|
|
10
13
|
* The `.split` base rides on the built-in "CSS classes" setting (defaulted below) so it
|
|
11
14
|
* shows in the builder canvas (assembled from settings) and the frontend.
|
|
@@ -57,7 +60,7 @@ class Vitops_Element_Split extends \Bricks\Element {
|
|
|
57
60
|
$this->controls['ratioInfo'] = [
|
|
58
61
|
'tab' => 'content',
|
|
59
62
|
'type' => 'info',
|
|
60
|
-
'content' => esc_html__( 'Equal columns by default. Add a class in "CSS classes" for a ratio: split-1-2, 2-1, 1-3, 3-1, 1-4, 4-1, 2-3, 3-2 (
|
|
63
|
+
'content' => esc_html__( 'Equal columns by default. Add a class in "CSS classes" for a ratio: split-1-2, 2-1, 1-3, 3-1, 1-4, 4-1, 2-3, 3-2 (prefix with sm-/md-/lg-/xl- to engage it from a breakpoint). Add flex-col to stack the columns below that breakpoint, and split-reverse to swap the two panels — when reversing, keep focusable content in only one column so the tab order stays linear.', 'bricks' ),
|
|
61
64
|
];
|
|
62
65
|
|
|
63
66
|
// Base flex class, defaulted onto the built-in "CSS classes" setting so it
|
package/assets/bricks/load.php
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* 3. Registers a "Vitops" builder category so the elements group together.
|
|
16
16
|
* 4. Defaults the design system to dark mode.
|
|
17
17
|
* 5. Registers [vitops_legal] for the generated legal documents.
|
|
18
|
+
* 6. Registers vitops_icon() + [vitops_icon] for the generated SVG sprite.
|
|
18
19
|
*
|
|
19
20
|
* Owned by the framework repo (vitops: bricks/); copied into the theme's
|
|
20
21
|
* dist/bricks/ on build — do not hand-edit in the theme.
|
|
@@ -172,3 +173,93 @@ add_shortcode(
|
|
|
172
173
|
return file_get_contents( $path );
|
|
173
174
|
}
|
|
174
175
|
);
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 6. vitops_icon() / [vitops_icon name="menu"] — render an icon from the sprite.
|
|
179
|
+
*
|
|
180
|
+
* The build emits dist/icons.svg (a hidden <svg> of <symbol>s) when the site
|
|
181
|
+
* config sets `icons.sprite`. Referencing one is a plain `<use>`: no JavaScript,
|
|
182
|
+
* no request to an icon API, and it inherits `currentColor` like any other glyph.
|
|
183
|
+
*
|
|
184
|
+
* Two ids resolve. A qualified icon is `ph--caret-down` (the `prefix:name`
|
|
185
|
+
* separator becomes `--`, since `:` is not valid in a fragment identifier), and
|
|
186
|
+
* a semantic name also gets a set-independent `icon-menu` alias — so markup
|
|
187
|
+
* written here survives changing icon sets, the same guarantee the Astro
|
|
188
|
+
* components get from resolveIcon().
|
|
189
|
+
*
|
|
190
|
+
* IMPORTANT: an external-file <use> is same-origin only, and dead under file://.
|
|
191
|
+
* That is fine for a theme-relative dist/, but if you ever serve dist/ from a
|
|
192
|
+
* CDN on another origin the icons vanish silently — no console error, just
|
|
193
|
+
* empty boxes. Inline the sprite into the page instead if that day comes.
|
|
194
|
+
*
|
|
195
|
+
* `$name` is charset-gated rather than allowlisted (unlike [vitops_legal], whose
|
|
196
|
+
* value lands in a filesystem read): here it only ever reaches an HTML
|
|
197
|
+
* attribute, so restricting it to id-safe characters plus escaping is the
|
|
198
|
+
* matching level of care.
|
|
199
|
+
*/
|
|
200
|
+
if ( ! defined( 'VITOPS_SPRITE_URI' ) ) {
|
|
201
|
+
define( 'VITOPS_SPRITE_URI', get_stylesheet_directory_uri() . '/dist/icons.svg' );
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param string $name Sprite id — 'menu' (semantic) or 'ph--list' (qualified).
|
|
206
|
+
* @param array $args 'size' => any CSS length, 'label' => accessible name,
|
|
207
|
+
* 'class' => extra classes on the wrapper.
|
|
208
|
+
* @return string HTML, or '' when the name is unusable.
|
|
209
|
+
*/
|
|
210
|
+
function vitops_icon( $name, $args = array() ) {
|
|
211
|
+
$name = is_string( $name ) ? strtolower( trim( $name ) ) : '';
|
|
212
|
+
|
|
213
|
+
// Sprite ids are ASCII word characters, hyphens and colons only. Anything
|
|
214
|
+
// else is a mistake, and echoing it would put author input into markup.
|
|
215
|
+
if ( '' === $name || ! preg_match( '/^[a-z0-9:_-]+$/', $name ) ) {
|
|
216
|
+
return '';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Accept a qualified `prefix:name` too, and normalise it to the sprite's id.
|
|
220
|
+
$id = str_replace( ':', '--', $name );
|
|
221
|
+
|
|
222
|
+
$defaults = array(
|
|
223
|
+
'size' => '',
|
|
224
|
+
'label' => '',
|
|
225
|
+
'class' => '',
|
|
226
|
+
);
|
|
227
|
+
$args = array_merge( $defaults, is_array( $args ) ? $args : array() );
|
|
228
|
+
|
|
229
|
+
$classes = trim( 'icon ' . $args['class'] );
|
|
230
|
+
$style = '' !== $args['size'] ? ' style="--icon-size:' . esc_attr( $args['size'] ) . '"' : '';
|
|
231
|
+
|
|
232
|
+
// Decorative by default — an icon beside a label must not be announced twice.
|
|
233
|
+
$a11y = '' !== $args['label']
|
|
234
|
+
? ' role="img" aria-label="' . esc_attr( $args['label'] ) . '"'
|
|
235
|
+
: ' aria-hidden="true"';
|
|
236
|
+
|
|
237
|
+
return '<span class="' . esc_attr( $classes ) . '"' . $style . $a11y . '>'
|
|
238
|
+
. '<svg><use href="' . esc_url( VITOPS_SPRITE_URI ) . '#' . esc_attr( $id ) . '"></use></svg>'
|
|
239
|
+
. '</span>';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
add_shortcode(
|
|
243
|
+
'vitops_icon',
|
|
244
|
+
function ( $atts ) {
|
|
245
|
+
$atts = shortcode_atts(
|
|
246
|
+
array(
|
|
247
|
+
'name' => '',
|
|
248
|
+
'size' => '',
|
|
249
|
+
'label' => '',
|
|
250
|
+
'class' => '',
|
|
251
|
+
),
|
|
252
|
+
$atts,
|
|
253
|
+
'vitops_icon'
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
return vitops_icon(
|
|
257
|
+
$atts['name'],
|
|
258
|
+
array(
|
|
259
|
+
'size' => $atts['size'],
|
|
260
|
+
'label' => $atts['label'],
|
|
261
|
+
'class' => $atts['class'],
|
|
262
|
+
)
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
);
|
package/assets/css/animation.css
CHANGED
|
@@ -11,15 +11,18 @@
|
|
|
11
11
|
* · animate-view / animate-scroll — keyframe linked to a scroll timeline
|
|
12
12
|
* · animate-trigger — keyframe, time-based; plays once when
|
|
13
13
|
* .is-active / [data-active] is set
|
|
14
|
-
* · transition
|
|
15
|
-
*
|
|
14
|
+
* · transition + hover-/focus-/ — TRANSITION (not keyframe) over the same
|
|
15
|
+
* active-<fx> from/to vars, so it reverses on state flip.
|
|
16
|
+
* Each state matches the element OR its
|
|
17
|
+
* direct parent, so an effect that clips
|
|
18
|
+
* itself away (reveal-*) is still reachable.
|
|
16
19
|
* Both driver families read the same `--<prop>-*` vars, so one effect
|
|
17
20
|
* (e.g. .fade-in) works in every mode with no redefinition.
|
|
18
21
|
*
|
|
19
22
|
* Effects by rendering stage (cheapest → most expensive):
|
|
20
23
|
* composite: fade-in/out, slide-up/down/left/right, scale-up/down, rotate-cw/ccw
|
|
21
24
|
* paint: blur-in/out, elevate-up/down, reveal-left/right/up/down
|
|
22
|
-
* layout: size-grow, size-shrink (
|
|
25
|
+
* layout: size-grow, size-shrink (auto height; needs interpolate-size)
|
|
23
26
|
* journey: fade-journey, slide-journey, … (multi-phase; view timeline)
|
|
24
27
|
* One keyframe per element: same-stage effects compose via vars (fade-in +
|
|
25
28
|
* scale-up); cross-stage (fade + blur) needs nested elements.
|
|
@@ -31,8 +34,8 @@
|
|
|
31
34
|
* Usage:
|
|
32
35
|
* <div class="fade-in slide-up animate-view"> <!-- scroll-into-view -->
|
|
33
36
|
* <div class="fade-in scale-up animate-trigger"> <!-- play once on enter -->
|
|
34
|
-
* <button class="scale-up
|
|
35
|
-
* <div class="fade-in
|
|
37
|
+
* <button class="transition hover-scale-up"> <!-- grow on hover -->
|
|
38
|
+
* <div class="transition active-fade-in"> <!-- flip via .is-active -->
|
|
36
39
|
*/
|
|
37
40
|
|
|
38
41
|
/* ===========================================
|
|
@@ -220,11 +223,20 @@
|
|
|
220
223
|
/* ===========================================
|
|
221
224
|
* Trigger Play-State
|
|
222
225
|
* Paused until activated. deferred.ts adds .is-active via IntersectionObserver,
|
|
223
|
-
* but [data-active] / .is-active can also be toggled by any state logic.
|
|
224
|
-
*
|
|
226
|
+
* but [data-active] / .is-active can also be toggled by any state logic.
|
|
227
|
+
*
|
|
228
|
+
* Gated on `scripting: enabled` because pausing is only safe when something can
|
|
229
|
+
* un-pause it: with no JS, .is-active is never added, and a paused entrance
|
|
230
|
+
* animation is `opacity: 0` — i.e. invisible content, permanently. Note which way
|
|
231
|
+
* this fails. An engine that doesn't know the media feature drops the whole block
|
|
232
|
+
* and the animation simply runs on load: an enhancement lost, nothing hidden.
|
|
233
|
+
* That is the right direction, and it is why this is a media query rather than
|
|
234
|
+
* the `:root:not(.no-js)` class it replaced — that spelling needed a class in the
|
|
235
|
+
* consumer's own <html>, which the Astro integration cannot put there, so every
|
|
236
|
+
* Astro site was silently failing the other way.
|
|
225
237
|
* =========================================== */
|
|
226
238
|
|
|
227
|
-
|
|
239
|
+
@media (scripting: enabled) {
|
|
228
240
|
.animate-trigger {
|
|
229
241
|
animation-play-state: paused;
|
|
230
242
|
}
|
|
@@ -261,6 +273,21 @@
|
|
|
261
273
|
* Stagger Support
|
|
262
274
|
* =========================================== */
|
|
263
275
|
|
|
276
|
+
/* Both are per-element positions, never something a child should inherit — an
|
|
277
|
+
unregistered custom property would leak the parent's index into every
|
|
278
|
+
descendant that happens to carry a driver. */
|
|
279
|
+
@property --_stagger-index {
|
|
280
|
+
syntax: '<number>';
|
|
281
|
+
inherits: false;
|
|
282
|
+
initial-value: 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
@property --_anim-range-offset {
|
|
286
|
+
syntax: '<length-percentage>';
|
|
287
|
+
inherits: false;
|
|
288
|
+
initial-value: 0%;
|
|
289
|
+
}
|
|
290
|
+
|
|
264
291
|
.stagger {
|
|
265
292
|
--_stagger-amount: var(--stagger-amount, 0.1s);
|
|
266
293
|
}
|
|
@@ -270,15 +297,36 @@
|
|
|
270
297
|
--_stagger-index inline, which wins. Where sibling-index() is supported
|
|
271
298
|
it provides a JS-free fallback via the @supports block below. */
|
|
272
299
|
--_stagger-index: 0;
|
|
300
|
+
|
|
301
|
+
/* Time-based drivers (.animate-trigger, .float) stagger on delay. */
|
|
273
302
|
animation-delay: calc(var(--_stagger-index) * var(--_stagger-amount));
|
|
274
|
-
}
|
|
275
303
|
|
|
276
|
-
/*
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
304
|
+
/* Scroll-driven drivers do NOT: animation-delay is time-based and is ignored
|
|
305
|
+
outright on a progress-based timeline, which is why `.stagger > .animate-view`
|
|
306
|
+
used to arrive all at once. Offsetting the view RANGE by the same index is the
|
|
307
|
+
equivalent move — each child starts its entrance a little further into its own
|
|
308
|
+
entry phase. .animate-view folds this into its default range, so one .stagger
|
|
309
|
+
works under both driver families. Journeys set an explicit animation-range and
|
|
310
|
+
opt out by construction.
|
|
311
|
+
|
|
312
|
+
The step is a VIEWPORT length, matching the window it shifts: .animate-view
|
|
313
|
+
now spans 10vh → 25vh of the element's midpoint travel, so a step expressed
|
|
314
|
+
as a percentage of the element's own height (which is what a bare `%` means
|
|
315
|
+
inside the entry range) would be a few pixels against an ~80px window — a
|
|
316
|
+
stagger too small to read, and one that changed meaning with tile size. */
|
|
317
|
+
--_anim-range-offset: calc(var(--_stagger-index) * var(--stagger-range-step, 5vh));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* sibling-index() is near-baseline (lacking only Firefox as of 2026). The test
|
|
321
|
+
must be a REAL property — `@supports (--x: sibling-index())` is always true,
|
|
322
|
+
because any token stream is a valid custom-property value, so it guarded
|
|
323
|
+
nothing. Where it is unsupported the declaration is skipped and the index stays
|
|
324
|
+
0 (items animate together) rather than producing an invalid delay/range.
|
|
325
|
+
`- 1` because applyRowStagger writes a 0-based index and sibling-index() is
|
|
326
|
+
1-based; the two must agree or the JS and CSS paths differ by one step. */
|
|
327
|
+
@supports (animation-delay: calc(sibling-index() * 1s)) {
|
|
280
328
|
.stagger > * {
|
|
281
|
-
--_stagger-index: sibling-index();
|
|
329
|
+
--_stagger-index: calc(sibling-index() - 1);
|
|
282
330
|
}
|
|
283
331
|
}
|
|
284
332
|
|
|
@@ -355,11 +403,34 @@
|
|
|
355
403
|
.animate-view {
|
|
356
404
|
animation-name: var(--_anim);
|
|
357
405
|
animation-timeline: view();
|
|
358
|
-
/*
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
406
|
+
/* Timed off the element's MIDPOINT, measured into the viewport — the only
|
|
407
|
+
framing that behaves the same for a 4rem card and a full-bleed section.
|
|
408
|
+
|
|
409
|
+
The pivot is `entry 50%`. The entry phase runs from "leading edge at the
|
|
410
|
+
viewport's trailing edge" to "trailing edge at the viewport's trailing
|
|
411
|
+
edge", a scroll of exactly one element height — so at its halfway point the
|
|
412
|
+
element's midpoint sits precisely ON the viewport edge, whatever the element
|
|
413
|
+
measures. Everything after that is stated in viewport units: `+ 10vh` is
|
|
414
|
+
"midpoint 10% of the viewport further in".
|
|
415
|
+
|
|
416
|
+
So the default plays from midpoint 15% in to midpoint 25% in. Both stops are
|
|
417
|
+
the ELEMENT's position on screen, not a fraction of its own height, which is
|
|
418
|
+
what the previous defaults got wrong in both directions: `entry 20%` was ~17px
|
|
419
|
+
of scroll for a small tile (over before it appeared), and `entry 100%` still
|
|
420
|
+
finished with the element flush against the bottom edge.
|
|
421
|
+
|
|
422
|
+
Percentages here resolve against the entry range (the element's height) and
|
|
423
|
+
lengths against the scrollport, so the two mix in one calc() by design.
|
|
424
|
+
|
|
425
|
+
--anim-start / --anim-end move the window without restating the pivot;
|
|
426
|
+
--anim-range replaces it outright; --_anim-range-offset shifts both stops
|
|
427
|
+
together (that's how .stagger offsets scroll-driven children — see Stagger
|
|
428
|
+
Support). Journeys declare their own range and ignore all of it. */
|
|
429
|
+
animation-range: var(
|
|
430
|
+
--anim-range,
|
|
431
|
+
entry calc(50% + var(--anim-start, 10vh) + var(--_anim-range-offset, 0%)) entry
|
|
432
|
+
calc(50% + var(--anim-end, 25vh) + var(--_anim-range-offset, 0%))
|
|
433
|
+
);
|
|
363
434
|
}
|
|
364
435
|
|
|
365
436
|
.animate-scroll {
|
|
@@ -372,13 +443,21 @@
|
|
|
372
443
|
animation-duration: var(--trigger-animation-duration);
|
|
373
444
|
}
|
|
374
445
|
|
|
375
|
-
/* Fallback:
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
446
|
+
/* Fallback: without a scroll-driven timeline, view()/scroll() animations never
|
|
447
|
+
advance and their elements stick at the `from` keyframe (opacity:0, clipped,
|
|
448
|
+
blurred) — an entrance animation that never runs is invisible content. Cancel
|
|
449
|
+
the animation so they render in their natural, fully-visible state. Trigger
|
|
450
|
+
animations are unaffected (JS-driven, see Trigger Play-State above).
|
|
451
|
+
|
|
452
|
+
Same probe as the `.text-reveal` block above, in its negative form — no JS, so
|
|
453
|
+
it holds with scripting disabled, which the `.no-scroll-timeline` class this
|
|
454
|
+
replaced could not (a class only JS can add cannot help a page with no JS).
|
|
455
|
+
`.animate-scroll` uses scroll() rather than view(), but no engine has shipped
|
|
456
|
+
one without the other, so one probe covers both. */
|
|
457
|
+
@supports not (animation-timeline: view()) {
|
|
458
|
+
:where(.animate-view, .animate-scroll) {
|
|
459
|
+
animation: none;
|
|
460
|
+
}
|
|
382
461
|
}
|
|
383
462
|
|
|
384
463
|
/* ===========================================
|
|
@@ -388,7 +467,8 @@
|
|
|
388
467
|
* active-<fx> in animation-effects.css) set those vars per state, so different
|
|
389
468
|
* effects bind to different states on one element:
|
|
390
469
|
* <div class="transition hover-slide-up active-elevate-up">
|
|
391
|
-
* Covers compositor
|
|
470
|
+
* Covers compositor, the full paint chain, and — where `interpolate-size` is
|
|
471
|
+
* supported — the layout stage (auto height) too.
|
|
392
472
|
* =========================================== */
|
|
393
473
|
|
|
394
474
|
.transition {
|
|
@@ -409,13 +489,44 @@
|
|
|
409
489
|
saturate(var(--t-saturate, var(--saturate-from, 1))) sepia(var(--t-sepia, var(--sepia-from, 0)))
|
|
410
490
|
hue-rotate(var(--t-hue-rotate, var(--hue-rotate-from, 0deg)));
|
|
411
491
|
clip-path: var(--t-clip, var(--clip-from, none));
|
|
412
|
-
transition:
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
492
|
+
/* Longhands rather than the `transition` shorthand: every property here shares
|
|
493
|
+
one duration and one easing, so listing them once lets the @supports block
|
|
494
|
+
below APPEND height without restating the whole list (a shorthand would
|
|
495
|
+
replace it, and transition-property has no additive form). */
|
|
496
|
+
transition-property: opacity, translate, scale, rotate, filter, clip-path;
|
|
497
|
+
transition-duration: var(--animation-duration);
|
|
498
|
+
transition-timing-function: var(--custom-ease-out);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/* Layout stage — `height`, the only one that reflows, and the only one that
|
|
502
|
+
needs a feature gate.
|
|
503
|
+
|
|
504
|
+
`height: 0 → auto` is not interpolable on its own: `auto` isn't a length, so
|
|
505
|
+
the pair falls back to discrete and the transition doesn't run.
|
|
506
|
+
`interpolate-size: allow-keywords` (set on :root in utilities.css) is what
|
|
507
|
+
makes `auto` a real endpoint — NOT `transition-behavior: allow-discrete`,
|
|
508
|
+
which only permits genuinely discrete properties (display,
|
|
509
|
+
content-visibility, overlay) to transition and would at best snap at the
|
|
510
|
+
midpoint here. Gated so engines without it keep no height transition rather
|
|
511
|
+
than that snap. The same dependency applies to the `layout` KEYFRAME, so this
|
|
512
|
+
is not a weaker path than a driver — it is the same requirement, stated once.
|
|
513
|
+
|
|
514
|
+
Declaring `height: auto` on every .transition element is safe because auto is
|
|
515
|
+
the initial value and animation.css is imported second in index.css — before
|
|
516
|
+
layout.css, every pattern and utilities.css — so anything later at equal
|
|
517
|
+
specificity still wins.
|
|
518
|
+
|
|
519
|
+
Caveat worth keeping: patterns/details.css records that transitioning
|
|
520
|
+
block-size to `auto` on ::details-content DEADLOCKS in Chrome 149
|
|
521
|
+
(content-visibility: hidden → no layout → `auto` resolves to 0 → nothing
|
|
522
|
+
runs). That is specific to ::details-content, but the lesson generalises — a
|
|
523
|
+
passing @supports query is not evidence the interaction works. Verify a real
|
|
524
|
+
hover before trusting a change here. */
|
|
525
|
+
@supports (interpolate-size: allow-keywords) {
|
|
526
|
+
.transition {
|
|
527
|
+
height: var(--t-height, var(--height-from, auto));
|
|
528
|
+
transition-property: opacity, translate, scale, rotate, filter, clip-path, height;
|
|
529
|
+
}
|
|
419
530
|
}
|
|
420
531
|
|
|
421
532
|
/* Respect reduced-motion: drop keyframes, transitions, and continuous floats so
|
package/assets/css/global.css
CHANGED
|
@@ -8,12 +8,28 @@
|
|
|
8
8
|
font-size: 100% !important;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/* Border-box sizing, everywhere.
|
|
12
|
+
The framework used to refuse this on the grounds that a global reset would
|
|
13
|
+
silently reflow consumer layouts. Two things changed. It is now LAYERED (this
|
|
14
|
+
file is `vitops.base`, the lowest of the three), so it loses to any unlayered
|
|
15
|
+
consumer rule — the opt-out is `*, *::before, *::after { box-sizing:
|
|
16
|
+
content-box }` in your own stylesheet, and a consumer who already ships a
|
|
17
|
+
border-box reset (Tailwind preflight, normalize, almost any modern base) sees
|
|
18
|
+
no change at all. And several patterns are only CORRECT under border-box: the
|
|
19
|
+
`.split` ratio is a flex basis, which sizes the border box, so under
|
|
20
|
+
content-box a padded column came out wider than its sibling by exactly its
|
|
21
|
+
padding. Stating the assumption beats each pattern re-asserting it and the
|
|
22
|
+
ones that forget being quietly wrong. */
|
|
23
|
+
*,
|
|
24
|
+
*::before,
|
|
25
|
+
*::after {
|
|
26
|
+
box-sizing: border-box;
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
/* Drop the UA's 8px body margin. The framework owns page gutters via `.centered`
|
|
12
30
|
(--gutter), so the UA default just offsets every full-bleed surface — sticky
|
|
13
31
|
headers and `bg-*` bands end up inset by 8px with a sliver of canvas showing
|
|
14
|
-
around them.
|
|
15
|
-
does NOT ship a general reset (no global box-sizing change), because that
|
|
16
|
-
would silently reflow existing consumer layouts. */
|
|
32
|
+
around them. */
|
|
17
33
|
body {
|
|
18
34
|
margin: 0;
|
|
19
35
|
}
|
package/assets/css/index.css
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
@import './generated/tokens.css';
|
|
8
8
|
@import './generated/typography.css';
|
|
9
9
|
@import './layout.css';
|
|
10
|
+
/* Must precede utilities.css: both are `vitops.utilities`, and utilities.css
|
|
11
|
+
re-emits `.<bp>-flex` AFTER its own `.hidden` so `class="hidden md-flex"`
|
|
12
|
+
reveals. That re-emit is only load-bearing because this file's `.md-flex`
|
|
13
|
+
comes first. */
|
|
14
|
+
@import './layout-utilities.css';
|
|
10
15
|
/* was layout-extra.css */
|
|
11
16
|
@import './patterns/cluster.css';
|
|
12
17
|
@import './patterns/grouped.css';
|
|
@@ -17,7 +22,9 @@
|
|
|
17
22
|
@import './patterns/sticky.css';
|
|
18
23
|
@import './patterns/stack.css';
|
|
19
24
|
@import './patterns/splitter.css';
|
|
25
|
+
@import './patterns/reveal.css';
|
|
20
26
|
@import './utilities.css';
|
|
27
|
+
@import './generated/spacing.css';
|
|
21
28
|
@import './generated/patterns.css';
|
|
22
29
|
/* was overlays.css */
|
|
23
30
|
@import './patterns/dialog.css';
|
|
@@ -38,6 +45,7 @@
|
|
|
38
45
|
@import './patterns/details.css';
|
|
39
46
|
@import './patterns/comment.css';
|
|
40
47
|
@import './patterns/banner.css';
|
|
48
|
+
@import './patterns/consent.css';
|
|
41
49
|
@import './patterns/list.css';
|
|
42
50
|
@import './patterns/tree.css';
|
|
43
51
|
/* was typographic.css */
|