From a24708466be929f2f162e888262e386aa09e7b4d Mon Sep 17 00:00:00 2001 From: Alessandro Bellesia Date: Wed, 7 Oct 2026 18:48:51 +0200 Subject: [PATCH 01/12] fix(sonar): the outlined preset page and tsconfig.json parse again SonarCloud gave up on docs/components/PresetOutlinedFields.vue with "Bad syntax reference: ". The stylelint it runs on the styles of a component validates `@else` as the at-rule of CSS Conditional 5, and @csstools/css-syntax-patches-for-csstree 1.1.12 gives that at-rule a prelude of `?` without defining the type, so the match throws. Reproduced with stylelint 17.16 and at-rule-prelude-no-invalid. -changed() now records whether a key changed in a variable and tests it with two @if, and the Sass it compiles to is identical byte for byte. The comment that said the dark theme sets the valid and invalid colours, which it no longer does, is corrected. tsconfig.json drops its comments, which SonarCloud could not parse (tsconfig.json:22:1); it is plain JSON now, with the same options. Co-Authored-By: Claude Opus 5.5 --- docs/components/PresetOutlinedFields.vue | 22 +++++++++++++--------- tsconfig.json | 5 ----- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/components/PresetOutlinedFields.vue b/docs/components/PresetOutlinedFields.vue index 7205ce1..6315f8c 100644 --- a/docs/components/PresetOutlinedFields.vue +++ b/docs/components/PresetOutlinedFields.vue @@ -23,21 +23,25 @@ @use '@/layers'; // The keys of `$after` whose value is not the one in `$before`, nested - // maps compared key by key + // maps compared key by key. Written without `@else`: stylelint, which + // SonarCloud runs on the styles of a component, validates `@else` as the + // at-rule of CSS Conditional 5, whose syntax refers to an undefined + // ``, and the whole file fails to parse. @function -changed($before, $after) { $result: (); @each $key, $value in $after { $old: map.get($before, $key); + $change: $value; + $changed: $value != $old; @if meta.type-of($value) == 'map' and meta.type-of($old) == 'map' { - $nested: -changed($old, $value); + $change: -changed($old, $value); + $changed: list.length($change) > 0; + } - @if list.length($nested) > 0 { - $result: map.set($result, $key, $nested); - } - } @else if $value != $old { - $result: map.set($result, $key, $value); + @if $changed { + $result: map.set($result, $key, $change); } } @@ -51,8 +55,8 @@ .docs-preset-outlined-fields { // The `--input-*` tokens the preset changes, declared on this element // instead of the root. The ones it leaves alone, such as the colours - // the dark theme sets for `valid` and `invalid`, keep coming from the - // root, as they do in a build with the preset. + // of `valid` and `invalid`, keep coming from the root, as they do in a + // build with the preset. @include ctx.layer(props, ctx.$use-css-layers, ctx.$layer-prefix) { @include ctx.spread-map-into-props( $map: -changed($-default-input, ctx.$input), diff --git a/tsconfig.json b/tsconfig.json index 9547e5f..afda278 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,18 +16,13 @@ "alwaysStrict": true, "noImplicitAny": true, "noImplicitReturns": true, - // `"noImplicitThis": true` is part of `strict` - // Added again here in case some users decide to disable `strict`. - // This enables stricter inference for data properties on `this`. "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, "declaration": true, "outDir": "dist", - // Recommended "esModuleInterop": true, "forceConsistentCasingInFileNames": true, - // `"verbatimModuleSyntax": true` replaces isolatedModules, preserveValueImports and importsNotUsedAsValues "verbatimModuleSyntax": true, "skipLibCheck": true }, From fd1a59eebaa9ffdb7e0877782349c94ec3129af3 Mon Sep 17 00:00:00 2001 From: Alessandro Bellesia Date: Thu, 8 Oct 2026 10:02:27 +0200 Subject: [PATCH 02/12] fix(utilities, dropdown): five defects found by the docs review - `.container` was 0px wide below 360px: `--breakpoint-value` is 0 at `xxs`. The token keeps its value, and the container takes the whole width up to the next breakpoint through a media query. - The sizing classes recursed into the dynamic spacing map and emitted `.w-xs-xxs` and the like, reading undeclared tokens with a unitless fallback, plus duplicates for every breakpoint. They now read `--spacing-{xs..xl}` with one class per size, like spacing, gap and inset; volver.css drops from 1425 kB to 1071 kB. - The duration, delay, shadow, opacity, filter, backdrop blur, transition property and ease classes read their tokens with the value as fallback, so durations and delays follow prefers-reduced-motion and a token set on the page reaches the classes. The default backdrop blur is named `.backdrop-blur`, and `.backdrop-blur-blur` is kept. - The `vv-dropdown` list alias read `[role="list-box"]`, not an ARIA role: it reads `[role="listbox"]`. - `scheme` no longer emits `.bg-scheme` and the other color classes, which wrote `light` as a color. dist/ moves in those rules only, measured rule by rule against a snapshot and in headless Chrome. The docs pages, the bundled skill and the changelog follow, and TableUtility loses the `literal` prop that no page needs any more. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 18 ++++++++++++++++++ docs/components/TableUtility.vue | 10 +--------- docs/contents/utilities/effects/box-shadow.md | 2 +- docs/contents/utilities/effects/opacity.md | 2 +- .../utilities/filters/backdrop-blur.md | 9 +++++++-- docs/contents/utilities/filters/blur.md | 2 +- docs/contents/utilities/filters/brightness.md | 2 +- docs/contents/utilities/filters/contrast.md | 2 +- docs/contents/utilities/filters/saturation.md | 2 +- docs/contents/utilities/layout/container.md | 2 +- docs/contents/utilities/sizing/height.md | 2 +- docs/contents/utilities/sizing/max-height.md | 2 +- docs/contents/utilities/sizing/max-width.md | 2 +- docs/contents/utilities/sizing/min-height.md | 2 +- docs/contents/utilities/sizing/min-width.md | 2 +- docs/contents/utilities/sizing/width.md | 2 +- .../utilities/transitions/transition-delay.md | 2 +- .../transitions/transition-duration.md | 2 +- .../transitions/transition-property.md | 2 +- .../transitions/transition-timing-function.md | 2 +- skills/volverjs-style/SKILL.md | 2 +- skills/volverjs-style/references/components.md | 2 +- skills/volverjs-style/references/utilities.md | 13 ++++++++----- src/settings/components/_vv-dropdown.scss | 2 +- src/utilities/colors.scss | 7 ++++--- src/utilities/effects.scss | 5 +++++ src/utilities/filters.scss | 17 +++++++++++++++++ src/utilities/layout.scss | 10 ++++++++++ src/utilities/sizing.scss | 4 ++++ src/utilities/transitions.scss | 5 +++++ 30 files changed, 99 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db038cc..b5c93b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to this project will be documented in this file. +## [Unreleased] + +### Fixed + +* `.container` was 0px wide on a viewport narrower than 360px, with its content spilling out of it. Its `max-width` reads `--breakpoint-value`, which the props set at each breakpoint to the width it starts at, and the first breakpoint, `xxs`, starts at 0. The token keeps that value, which is true of the breakpoint and is what a script reading it gets. Instead, the container rule now takes the whole width up to the next breakpoint, through a media query for every breakpoint that starts at 0. In `dist/` the `.container` rule gains `@media (width<=359.98px)` with `max-width: 100%`, and nothing else moves. +* The sizing classes walked into the dynamic spacing map, which holds the value of each size at each breakpoint, and emitted a class per entry: `.w-xs-xxs`, `.h-md-sm`, `.max-w-xl-lg` and so on, for `w-`, `h-`, `max-w-`, `max-h-`, `min-w-` and `min-h-`. Each read a token nothing declares, with a unitless fallback (`var(--spacing-xs-xxs, 6)`), which is invalid, so the class reset the property instead of setting it. Only `sm` and `lg` came out with a bare name, because they are also the names of two breakpoints: `.w-sm` and `.w-lg` read the right token, still with the unitless fallback. The recursion also ran the loop over the breakpoints again for every breakpoint, so about half of the rules were duplicates. The classes now read the dynamic tokens the way the spacing, gap and inset utilities do, with one class per size, from `w-xs` to `w-xl` for each of the six prefixes and their responsive variants, and the token resolves the size for the viewport. `max-w-md` is therefore 12 to 16px, not the container width that Tailwind gives the name: the skill says so. In `dist/`, 624 class names that did nothing go away and 144 come in. `utilities/sizing.css` drops from 529 kB to 173 kB, and `volver.css` from 1425 kB to 1071 kB, 120.3 kB to 113.2 kB gzipped. +* The `duration-` and `delay-` classes wrote their value, `.duration-300` as `transition-duration: .3s`, while the duration tokens are `0s` unless the user allows motion, so a transition set by a class kept moving for a user who had asked for less motion. The classes now read the tokens, with the value as a fallback: `var(--duration-300, .3s)`. A delay reads the same duration tokens and drops to 0s with the preference, as the transitions of the components do. The shadow, opacity, blur, brightness, contrast, saturate, backdrop blur, `transition-property-` and `ease-` classes read their tokens the same way, so a token set on the page, `--shadow-md` or `--opacity-50`, reaches the classes as it already reached the components. + + A shadow token is written with `var(--color-shadow)`, which resolves where the token is declared, on the root and on every `.theme`. A `--color-shadow` set on another element no longer recolours the shadow classes inside it, which is how the shadows of the components already behaved. + + Reading the token also renames the default backdrop blur, because the generator gives the bare class name to the key that matches the token. `.backdrop-blur-blur` is now `.backdrop-blur`, like `.blur`, and the old name is kept on the same rule. + + In `dist/` the declarations of these classes change and no other rule moves. Every token they read is declared in `props.css`. +* The alias that styles a plain `div` as the list of `vv-dropdown` matched `[role="list-box"]`, which is not an ARIA role, so a `
` without the `vv-dropdown__list` class was left unstyled. It reads `[role="listbox"]` now. `VvDropdown` of `@volverjs/ui-vue` puts the class on its list and renders as before. In `dist/` the 12 rules of the list change their selector and none of their declarations. +* `scheme`, the `color-scheme` of the theme, is in the map of the colors, and came out as `.bg-scheme`, `.text-scheme`, `.border-scheme` and `.decoration-scheme`, which wrote `light` as a color. The color utilities leave it out, as they leave out `tint`, and `--color-scheme` stays declared. In `dist/` the four rules go away. + +`design-tokens.json` and the `exports` map do not change. + ## [0.1.30] - 2026-10-07 ### Added diff --git a/docs/components/TableUtility.vue b/docs/components/TableUtility.vue index 206180c..0a61391 100644 --- a/docs/components/TableUtility.vue +++ b/docs/components/TableUtility.vue @@ -44,14 +44,6 @@ type: Array, default: () => [], }, - /** - * The classes declare the value itself rather than reading the custom - * property: show the value even when the custom property column is on. - */ - literal: { - type: Boolean, - default: false, - }, }) const items = computed(() => { @@ -121,7 +113,7 @@ {{ `${attribute || property}: ${ - customProperty !== undefined && !literal + customProperty !== undefined ? `var(${getCustomProperty(key)})` : value };` diff --git a/docs/contents/utilities/effects/box-shadow.md b/docs/contents/utilities/effects/box-shadow.md index 89ccf6f..5c30d86 100644 --- a/docs/contents/utilities/effects/box-shadow.md +++ b/docs/contents/utilities/effects/box-shadow.md @@ -3,7 +3,7 @@ title: Box Shadow description: Utilities for controlling the box shadow of an element. customProperties: true --- - +
diff --git a/docs/contents/utilities/effects/opacity.md b/docs/contents/utilities/effects/opacity.md index 62a1f33..c09918b 100644 --- a/docs/contents/utilities/effects/opacity.md +++ b/docs/contents/utilities/effects/opacity.md @@ -4,7 +4,7 @@ description: Utilities for controlling the opacity of an element. customProperties: true --- - +
diff --git a/docs/contents/utilities/filters/backdrop-blur.md b/docs/contents/utilities/filters/backdrop-blur.md index 130fa93..a5b0e66 100644 --- a/docs/contents/utilities/filters/backdrop-blur.md +++ b/docs/contents/utilities/filters/backdrop-blur.md @@ -1,9 +1,14 @@ --- title: Backdrop Blur description: Utilities for blurring what is behind an element. +customProperties: true --- -The element needs a background that lets what is behind it show through, such as a translucent surface. The class for the default 8px is `.backdrop-blur-blur`; there is no bare `.backdrop-blur`. +The element needs a background that lets what is behind it show through, such as a translucent surface. The class for the default 8px is `.backdrop-blur`, like `.blur`; `.backdrop-blur-blur`, its earlier name, still works. The classes read the blur tokens, the same as the [blur](/style/utilities/filters/blur) utilities.
- + + +
diff --git a/docs/contents/utilities/filters/blur.md b/docs/contents/utilities/filters/blur.md index 7ccbc02..578bcfa 100644 --- a/docs/contents/utilities/filters/blur.md +++ b/docs/contents/utilities/filters/blur.md @@ -4,5 +4,5 @@ description: Utilities for controlling the blur of an element. customProperties: true ---
- +
\ No newline at end of file diff --git a/docs/contents/utilities/filters/brightness.md b/docs/contents/utilities/filters/brightness.md index 744e0fb..54138fa 100644 --- a/docs/contents/utilities/filters/brightness.md +++ b/docs/contents/utilities/filters/brightness.md @@ -4,5 +4,5 @@ description: Utilities for controlling the brightness of an element. customProperties: true ---
- +
\ No newline at end of file diff --git a/docs/contents/utilities/filters/contrast.md b/docs/contents/utilities/filters/contrast.md index 3a49fe5..4531e51 100644 --- a/docs/contents/utilities/filters/contrast.md +++ b/docs/contents/utilities/filters/contrast.md @@ -4,5 +4,5 @@ description: Utilities for applying contrast filters to an element. customProperties: true ---
- +
\ No newline at end of file diff --git a/docs/contents/utilities/filters/saturation.md b/docs/contents/utilities/filters/saturation.md index 4bf7a4f..e953815 100644 --- a/docs/contents/utilities/filters/saturation.md +++ b/docs/contents/utilities/filters/saturation.md @@ -4,5 +4,5 @@ description: Utilities for controlling the saturation of an element. customProperties: true ---
- +
\ No newline at end of file diff --git a/docs/contents/utilities/layout/container.md b/docs/contents/utilities/layout/container.md index 105b08b..803a4cc 100644 --- a/docs/contents/utilities/layout/container.md +++ b/docs/contents/utilities/layout/container.md @@ -3,7 +3,7 @@ title: Container description: Utilities for fixing an element's width to the current breakpoint. --- -`.container` sets `width: 100%` and `max-width: var(--breakpoint-value, 100%)`. `--breakpoint-value` holds the value of the breakpoint the viewport has reached, so the container snaps to the width of the current breakpoint, as the table shows. +`.container` sets `width: 100%` and `max-width: var(--breakpoint-value, 100%)`. `--breakpoint-value` holds the value of the breakpoint the viewport has reached, so the container snaps to the width of the current breakpoint, as the table shows. Below `xs`, where the first breakpoint starts at 0, the container takes the whole width.
diff --git a/docs/contents/utilities/sizing/height.md b/docs/contents/utilities/sizing/height.md index 6a03a1e..f23b87f 100644 --- a/docs/contents/utilities/sizing/height.md +++ b/docs/contents/utilities/sizing/height.md @@ -9,7 +9,7 @@ spacing: true ### Spacing scale -Height can also be set using the static spacing scale, for example `h-96`. +Height can also be set using the spacing scale: a static step, for example `h-96`, or a dynamic one from `h-xs` to `h-xl`, which grows with the viewport.
diff --git a/docs/contents/utilities/sizing/max-height.md b/docs/contents/utilities/sizing/max-height.md index b82dd07..1a0718c 100644 --- a/docs/contents/utilities/sizing/max-height.md +++ b/docs/contents/utilities/sizing/max-height.md @@ -9,7 +9,7 @@ spacing: true ### Spacing scale -Max height can also be set using the static spacing scale, for example `max-h-288`. +Max height can also be set using the spacing scale: a static step, for example `max-h-288`, or a dynamic one from `max-h-xs` to `max-h-xl`, which grows with the viewport.
diff --git a/docs/contents/utilities/sizing/max-width.md b/docs/contents/utilities/sizing/max-width.md index 778ff1c..2109b78 100644 --- a/docs/contents/utilities/sizing/max-width.md +++ b/docs/contents/utilities/sizing/max-width.md @@ -9,7 +9,7 @@ spacing: true ### Spacing scale -Max width can also be set using the static spacing scale, for example `max-w-384`. +Max width can also be set using the spacing scale: a static step, for example `max-w-384`, or a dynamic one from `max-w-xs` to `max-w-xl`, which grows with the viewport.
diff --git a/docs/contents/utilities/sizing/min-height.md b/docs/contents/utilities/sizing/min-height.md index bdc5808..cb12e2d 100644 --- a/docs/contents/utilities/sizing/min-height.md +++ b/docs/contents/utilities/sizing/min-height.md @@ -9,7 +9,7 @@ spacing: true ### Spacing scale -Min height can also be set using the static spacing scale, for example `min-h-96`. +Min height can also be set using the spacing scale: a static step, for example `min-h-96`, or a dynamic one from `min-h-xs` to `min-h-xl`, which grows with the viewport.
diff --git a/docs/contents/utilities/sizing/min-width.md b/docs/contents/utilities/sizing/min-width.md index 4e87a40..f34c971 100644 --- a/docs/contents/utilities/sizing/min-width.md +++ b/docs/contents/utilities/sizing/min-width.md @@ -9,7 +9,7 @@ spacing: true ### Spacing scale -Min width can also be set using the static spacing scale, for example `min-w-96`. +Min width can also be set using the spacing scale: a static step, for example `min-w-96`, or a dynamic one from `min-w-xs` to `min-w-xl`, which grows with the viewport.
diff --git a/docs/contents/utilities/sizing/width.md b/docs/contents/utilities/sizing/width.md index 667eee0..91d28d7 100644 --- a/docs/contents/utilities/sizing/width.md +++ b/docs/contents/utilities/sizing/width.md @@ -8,7 +8,7 @@ spacing: true ### Spacing scale -Width can also be set using the static spacing scale, for example `w-96`. +Width can also be set using the spacing scale: a static step, for example `w-96`, or a dynamic one from `w-xs` to `w-xl`, which grows with the viewport.
diff --git a/docs/contents/utilities/transitions/transition-delay.md b/docs/contents/utilities/transitions/transition-delay.md index 270332c..61742a6 100644 --- a/docs/contents/utilities/transitions/transition-delay.md +++ b/docs/contents/utilities/transitions/transition-delay.md @@ -4,5 +4,5 @@ description: Utilities for controlling the delay of CSS transitions. customProperties: true ---
- +
\ No newline at end of file diff --git a/docs/contents/utilities/transitions/transition-duration.md b/docs/contents/utilities/transitions/transition-duration.md index 06898a0..dff243c 100644 --- a/docs/contents/utilities/transitions/transition-duration.md +++ b/docs/contents/utilities/transitions/transition-duration.md @@ -4,5 +4,5 @@ description: Utilities for controlling the duration of CSS transitions. customProperties: true ---
- +
\ No newline at end of file diff --git a/docs/contents/utilities/transitions/transition-property.md b/docs/contents/utilities/transitions/transition-property.md index ec6019a..f3d92eb 100644 --- a/docs/contents/utilities/transitions/transition-property.md +++ b/docs/contents/utilities/transitions/transition-property.md @@ -4,5 +4,5 @@ description: Utilities for controlling which CSS properties transition. customProperties: true ---
- +
\ No newline at end of file diff --git a/docs/contents/utilities/transitions/transition-timing-function.md b/docs/contents/utilities/transitions/transition-timing-function.md index 1488f53..d1929fd 100644 --- a/docs/contents/utilities/transitions/transition-timing-function.md +++ b/docs/contents/utilities/transitions/transition-timing-function.md @@ -4,5 +4,5 @@ description: Utilities for controlling the easing of CSS transitions. customProperties: true ---
- +
\ No newline at end of file diff --git a/skills/volverjs-style/SKILL.md b/skills/volverjs-style/SKILL.md index 417418d..fc96782 100644 --- a/skills/volverjs-style/SKILL.md +++ b/skills/volverjs-style/SKILL.md @@ -59,7 +59,7 @@ These are the mistakes that recur. Each row is verified against the compiled CSS | `md:p-16`, `lg:gap-8` | Spacing and gap have **no responsive variants**. Use the dynamic tokens `xs sm md lg xl` (`p-md`, `gap-lg`), which grow with the viewport by themselves | | `xxs:` prefix, `2xl:` | Prefixes are `xs: sm: md: lg: xl: xxl: xxxl:` (360, 576, 992, 1024, 1280, 1440, 1536px) and only display, position, visibility, flex/grid, sizing, text size/align, border width/style, aspect and columns accept them | | `hover:bg-…`, `dark:text-…`, `focus:` | No state or theme variants at all. Use component states, tokens, or your own CSS | -| `max-w-md`, `max-w-2xl` | `max-w-screen-md` … `max-w-screen-xxxl`, `max-w-prose`, or `container` | +| `max-w-md`, `max-w-2xl` | `max-w-screen-md` … `max-w-screen-xxxl`, `max-w-prose`, or `container`. `max-w-md` exists, but it is the dynamic spacing step (12 to 16px) | | `z-10` … `z-50` | Semantic: `z-sticky` `z-fixed` `z-dropdown` `z-modal-backdrop` `z-modal` `z-popover` `z-tooltip` `z-toast` (plus `z-1`, `z-auto`) | | `aspect-video` | `aspect-wide` (16/9); also `square` `photo` `tv` `ultrawide` | | `rounded-2xl`, `rounded-3xl` | `rounded-xxl`, `rounded-xxxl` (shadows do use `shadow-2xl`) | diff --git a/skills/volverjs-style/references/components.md b/skills/volverjs-style/references/components.md index 826dafa..3be6a5f 100644 --- a/skills/volverjs-style/references/components.md +++ b/skills/volverjs-style/references/components.md @@ -423,7 +423,7 @@ The `` block is the backdrop; the panel is the mandatory `
` chi ## vv-dropdown family `vv-dropdown` is a popover container (`[popover]`) positioned by script in -`@volverjs/ui-vue`. Its list is a `div[role=menu]` or `div[role=list-box]` (a `
    ` needs +`@volverjs/ui-vue`. Its list is a `div[role=menu]` or `div[role=listbox]` (a `
      ` needs the `__list` class). Items are `vv-dropdown-action` (buttons) or `vv-dropdown-option` (selectable, `aria-selected`), grouped by `vv-dropdown-optgroup`. diff --git a/skills/volverjs-style/references/utilities.md b/skills/volverjs-style/references/utilities.md index f740077..75a6b79 100644 --- a/skills/volverjs-style/references/utilities.md +++ b/skills/volverjs-style/references/utilities.md @@ -82,7 +82,7 @@ white black transparent shadow backdrop | `z-` | `1` `sticky` `fixed` `dropdown` `modal-backdrop` `modal` `confirm-backdrop` `confirm` `popover` `tooltip` `toast` `notification-alert` `auto` | no | | `overflow-` `overflow-x-` `overflow-y-` | `visible` `hidden` `scroll` `auto` | no | | `scrollbar-` | `thin` (thin, `word-5` thumb on a transparent track) `none` (hidden, still scrolls) | no | -| `container` | | width 100%, max-width follows the current breakpoint; no size suffixes | +| `container` | | width 100%, max-width follows the current breakpoint, the whole width below `xs` (360px); no size suffixes | | `aspect-` | `auto` `square` `photo` (3/2) `tv` (4/3) `wide` (16/9) `ultrawide` (21/9) | yes | | `object-` (fit) | `fill` `contain` `cover` `none` `scale-down` | no | | `object-` (position) | `top` `bottom` `left` `right` `center` `left-top` `left-bottom` `right-top` `right-bottom` | no | @@ -139,8 +139,9 @@ All grid utilities are responsive. | `min-w-` | spacing scale, `0` `full` `min` `max` `fit` | | `min-h-` | spacing scale, `0` `full` `screen` `min` `max` `fit` | -`screen` uses `100dvw`/`100dvh`. The Tailwind container idiom `max-w-md` does not -exist here (the dynamic spacing names are not valid sizing values): use +`screen` uses `100dvw`/`100dvh`. The spacing scale includes the dynamic steps +(`w-md`, `h-lg`), which grow with the viewport like the spacing does. So the Tailwind +container idiom `max-w-md` is 12 to 16px here, not a container width: use `max-w-screen-md` or `container`. Fractions are written literally in the markup (`class="w-1/2"`). @@ -234,7 +235,7 @@ The radius scale is `xxl`/`xxxl`, not `2xl`/`3xl`. | `opacity-` | `0` `5` `10` … `95` `100` (every multiple of 5) | | `mix-blend-` `bg-blend-` | CSS blend mode names (`multiply` `screen` `overlay` …) | | `blur` `blur-` | `none` `sm` (4px) `md` (12px) `lg` (16px) `xl` (24px) `xxl` (40px) `xxxl` (64px); bare `blur` = 8px | -| `backdrop-blur-` | `none` `sm` `blur` (8px) `md` `lg` `xl` `xxl` `xxxl`; no bare `backdrop-blur` | +| `backdrop-blur` `backdrop-blur-` | `none` `sm` `md` `lg` `xl` `xxl` `xxxl`; bare `backdrop-blur` = 8px (`backdrop-blur-blur` is kept as an alias) | | `brightness-` | `0` `50` `75` `90` `95` `100` `105` `110` `125` `150` `200` `250` `500` `1000` | | `contrast-` | `0` `50` `75` `100` `125` `150` `200` `300` | | `saturate-` | `0` `50` `100` `150` `200` | @@ -245,7 +246,9 @@ The radius scale is `xxl`/`xxxl`, not `2xl`/`3xl`. Transition and duration tokens are `0s` unless the user has `prefers-reduced-motion: no-preference`, so motion switches itself off for users -who asked for it. There are no `animate-*` utilities; animation keyframes exist only +who asked for it, and the `duration-` and `delay-` classes read them. The shadow, +opacity, filter, `transition-property-` and `ease-` classes read their tokens too, with +the value as a fallback, so a token set on the page reaches them. There are no `animate-*` utilities; animation keyframes exist only as `--animation-*` custom properties. ## Transforms (not responsive) diff --git a/src/settings/components/_vv-dropdown.scss b/src/settings/components/_vv-dropdown.scss index 1bdee3c..a3e9d12 100644 --- a/src/settings/components/_vv-dropdown.scss +++ b/src/settings/components/_vv-dropdown.scss @@ -23,7 +23,7 @@ $vv-dropdown: ( ), element: ( list: ( - _alias: 'div:is([role="menu"],[role="list-box"])', + _alias: 'div:is([role="menu"],[role="listbox"])', [border-radius]: var(--dropdown-border-radius), background-color: var(--color-surface-1), overflow: auto, diff --git a/src/utilities/colors.scss b/src/utilities/colors.scss index 034be6d..d7f3a2a 100644 --- a/src/utilities/colors.scss +++ b/src/utilities/colors.scss @@ -39,9 +39,10 @@ @return $result; } -// --color-tint is only the source the neutrals take their hue from, so it -// gets no utilities -$-colors: map.remove($colors, tint); +// --color-tint is only the source the neutrals take their hue from, and +// --color-scheme is a value of `color-scheme`, not a color: neither gets +// utilities +$-colors: map.remove($colors, tint, scheme); $-texts: -utility-colors($-colors, $text: true); $-fills: -utility-colors($-colors, $text: false); diff --git a/src/utilities/effects.scss b/src/utilities/effects.scss index f4b2fbb..2384c4d 100644 --- a/src/utilities/effects.scss +++ b/src/utilities/effects.scss @@ -1,11 +1,15 @@ @use '../context' as *; @include layer(utilities, $use-css-layers, $layer-prefix) { +// The shadows and the opacities read their tokens, with the value as the +// fallback, so a token set on the page reaches the classes too + // box shadow @include spread-map-into-utilities( $map: $box-shadow, $class: shadow, $attribute: box-shadow, + $prop: shadow, $zero-specificity: $zero-specificity-for-utilities ); @@ -14,6 +18,7 @@ $map: $opacity, $class: opacity, $attribute: opacity, + $prop: opacity, $zero-specificity: $zero-specificity-for-utilities ); diff --git a/src/utilities/filters.scss b/src/utilities/filters.scss index c2587d4..b129891 100644 --- a/src/utilities/filters.scss +++ b/src/utilities/filters.scss @@ -1,11 +1,15 @@ @use '../context' as *; @include layer(utilities, $use-css-layers, $layer-prefix) { +// The filters read their tokens, with the value as the fallback, so a +// token set on the page reaches the classes too + // blur @include spread-map-into-utilities( $map: $blur, $class: blur, $attribute: filter, + $prop: blur, $zero-specificity: $zero-specificity-for-utilities ); @@ -14,6 +18,7 @@ $map: $brightness, $class: brightness, $attribute: filter, + $prop: brightness, $zero-specificity: $zero-specificity-for-utilities ); @@ -22,6 +27,7 @@ $map: $contrast, $class: contrast, $attribute: filter, + $prop: contrast, $zero-specificity: $zero-specificity-for-utilities ); @@ -30,6 +36,7 @@ $map: $saturation, $class: saturate, $attribute: filter, + $prop: saturate, $zero-specificity: $zero-specificity-for-utilities ); @@ -38,7 +45,17 @@ $map: $blur, $class: backdrop-blur, $attribute: backdrop-filter, + $prop: blur, $zero-specificity: $zero-specificity-for-utilities ); +// The default blur was `.backdrop-blur-blur` before the classes read the +// tokens, which names it `.backdrop-blur` like `.blur`: the old name stays +@include wrap-with-where( + $selector: '.backdrop-blur-blur', + $enabled: $zero-specificity-for-utilities +) { + @extend %backdrop-blur !optional; +} + } diff --git a/src/utilities/layout.scss b/src/utilities/layout.scss index 8ce4c2c..d65b56d 100644 --- a/src/utilities/layout.scss +++ b/src/utilities/layout.scss @@ -14,9 +14,19 @@ ); // container +// The first breakpoint starts at 0, a value that caps nothing: up to the +// next one the container takes the whole width instead of collapsing %container { width: 100%; max-width: var(--breakpoint-value, 100%); + + @each $breakpoint, $breakpoint-value in $breakpoints { + @if $breakpoint-value == 0 { + @include media-breakpoint-down($breakpoint, $breakpoints) { + max-width: 100%; + } + } + } } @include wrap-with-where( diff --git a/src/utilities/sizing.scss b/src/utilities/sizing.scss index 40786e0..c389180 100644 --- a/src/utilities/sizing.scss +++ b/src/utilities/sizing.scss @@ -14,10 +14,14 @@ min-h: min-height ) { + // A dynamic size is a map of its value at each breakpoint, which + // --spacing-{size} already resolves: one class reads that token, as + // the spacing, gap and inset utilities do, instead of a class per entry @include spread-map-into-utilities( $map: map.merge($spacing, $spacing-dynamic), $class: $class, $attribute: $attribute, + $deep: false, $bps: $breakpoints, $prop: spacing, $zero-specificity: $zero-specificity-for-utilities diff --git a/src/utilities/transitions.scss b/src/utilities/transitions.scss index 09e5842..65739b4 100644 --- a/src/utilities/transitions.scss +++ b/src/utilities/transitions.scss @@ -7,10 +7,13 @@ $map: $transition-property, $class: transition-property, $attribute: transition-property, + $prop: transition-property, $zero-specificity: $zero-specificity-for-utilities ); // transition duration and transition delay +// Both read the duration tokens, which are 0s under prefers-reduced-motion, +// so a class that sets a duration or a delay follows the preference @each $class, $attribute in (duration: transition-duration, delay: transition-delay) { @@ -18,6 +21,7 @@ $map: $transition-duration-delay, $class: $class, $attribute: $attribute, + $prop: duration, $zero-specificity: $zero-specificity-for-utilities ); } @@ -27,6 +31,7 @@ $map: $transition-timing-function, $class: ease, $attribute: transition-timing-function, + $prop: ease, $zero-specificity: $zero-specificity-for-utilities ); From f26e2d502f099fff306dd923a6f226585d600f6c Mon Sep 17 00:00:00 2001 From: Alessandro Bellesia Date: Mon, 12 Oct 2026 15:53:38 +0200 Subject: [PATCH 03/12] fix(utilities): a nested map takes the current breakpoint alone spread-map-into-utilities walked a nested map with every breakpoint under the prefix of the current one: at the first, of 0, it emitted the prefixed classes with no media query, and it emitted every rule of the nested map once per breakpoint. The recursion now receives the current breakpoint only. No map of the library reaches the branch since the sizing classes stopped walking the dynamic spacing map, so dist/ does not move. Before that it gave 630 sizing classes such as `.xs:w-sm` that applied at every width, and 4,410 identical copies of the others; a nested map of an application still did. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 ++ src/tools/mixin-modules/_utilities.scss | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5c93b7..ffaa64e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ All notable changes to this project will be documented in this file. * The alias that styles a plain `div` as the list of `vv-dropdown` matched `[role="list-box"]`, which is not an ARIA role, so a `
      ` without the `vv-dropdown__list` class was left unstyled. It reads `[role="listbox"]` now. `VvDropdown` of `@volverjs/ui-vue` puts the class on its list and renders as before. In `dist/` the 12 rules of the list change their selector and none of their declarations. * `scheme`, the `color-scheme` of the theme, is in the map of the colors, and came out as `.bg-scheme`, `.text-scheme`, `.border-scheme` and `.decoration-scheme`, which wrote `light` as a color. The color utilities leave it out, as they leave out `tint`, and `--color-scheme` stays declared. In `dist/` the four rules go away. +* A nested map in `spread-map-into-utilities` was walked with every breakpoint under the prefix of the current one: at the first, of 0, it emitted the prefixed classes with no media query, and it emitted every rule of the nested map once per breakpoint. The recursion now takes the current breakpoint alone. No map of the library reaches the branch since the sizing classes stopped walking the dynamic spacing map, above, so `dist/` does not move; before that the sizing classes gave 630 classes such as `.xs:w-sm` that applied at every width and 4,410 identical copies of the others, and an application that spreads a nested map of its own with breakpoints still did. + `design-tokens.json` and the `exports` map do not change. ## [0.1.30] - 2026-10-07 diff --git a/src/tools/mixin-modules/_utilities.scss b/src/tools/mixin-modules/_utilities.scss index 98b9d2f..97602f6 100644 --- a/src/tools/mixin-modules/_utilities.scss +++ b/src/tools/mixin-modules/_utilities.scss @@ -42,13 +42,16 @@ } @if meta.type-of($value) == 'map' and $deep { + // The nested map takes the current step alone: given every + // step, it walked them again under the prefix of this one, + // and emitted `md:` classes with no media query at its step 0 @include spread-map-into-utilities( $map: $value, $class: $class, $attribute: $attribute, $prop: $prop, $name: $key, - $bps: $bps, + $bps: ($breakpoint: $breakpoint-value), $prefix: $prefix, $zero-specificity: $zero-specificity ); From 1c5d64bc2a1a4b2902b8ad0534b7a67dea5aac2a Mon Sep 17 00:00:00 2001 From: Alessandro Bellesia Date: Mon, 12 Oct 2026 15:53:39 +0200 Subject: [PATCH 04/12] feat(utilities): container queries The `@container` class sets `container-type: inline-size`, and the utilities that lay a block out take a container prefix, `@md:flex-row`, which reads the width of the nearest `@container` instead of the viewport. The steps are the ones of Tailwind CSS, from `@xs:` (20rem) to `@xxxl:` (48rem), set by `$container-breakpoints`. They reach display, flex direction and wrap, justify and align of content and items, grid-cols, col and text align: 70 classes per step, 491 rules, 2 KB of volver.css gzipped. spread-map-into-utilities takes the steps in `$cqs`, appended after its other arguments, and emits them after the responsive classes of the same utility, so a container class wins when both apply. `cq-up` wraps a rule in the query of a step; an unknown step, or one that is not above 0, stops the compilation. The breakpoints page also stops claiming that a map out of order or without a key of 0 is an error: it is a warning, and the CSS is wrong. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++ docs/contents/settings/breakpoints.md | 48 +++++++++++++++++- .../utilities/flex-grid/align-content.md | 1 + .../utilities/flex-grid/align-items.md | 1 + .../utilities/flex-grid/flex-direction.md | 1 + .../contents/utilities/flex-grid/flex-wrap.md | 1 + .../flex-grid/grid-column-start-end.md | 3 ++ .../flex-grid/grid-template-columns.md | 1 + .../utilities/flex-grid/justify-content.md | 1 + .../utilities/flex-grid/justify-items.md | 1 + docs/contents/utilities/layout/display.md | 3 +- .../utilities/typography/text-align.md | 1 + docs/pages/utilities/[group]/[name].vue | 16 ++++++ skills/volverjs-style/SKILL.md | 5 +- skills/volverjs-style/references/scss.md | 4 +- skills/volverjs-style/references/utilities.md | 11 +++++ src/export.scss | 5 ++ src/settings/_breakpoints.scss | 18 +++++++ src/tools/_breakpoints.scss | 28 +++++++++++ src/tools/mixin-modules/_utilities.scss | 49 ++++++++++++++++++- src/utilities/flexbox.scss | 4 ++ src/utilities/grid.scss | 2 + src/utilities/layout.scss | 13 +++++ src/utilities/typography.scss | 3 +- 24 files changed, 216 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffaa64e..9bfe64e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added + +* Container queries. The `@container` class sets `container-type: inline-size`, and the utilities that lay a block out take a container prefix, the name of a step after an `@`: `@md:flex-row` applies when the nearest `@container` above the element is at least 28rem wide, and never when there is none. The steps are the ones of Tailwind CSS, `@xs:` 20rem, `@sm:` 24rem, `@md:` 28rem, `@lg:` 32rem, `@xl:` 36rem, `@xxl:` 42rem and `@xxxl:` 48rem, set by `$container-breakpoints`, and they reach display, flex direction and wrap, the justify and align utilities of content and items, `grid-cols-`, `col-` and text align: 70 classes per step, 2 KB of `volver.css` once gzipped, where giving them to every responsive utility would have added about 570 KB before compression. A container class is emitted after the responsive classes of its utility, so it wins when both apply. `spread-map-into-utilities` takes the steps in a `$cqs` argument, `$container-breakpoints: ()` emits none, and `cq-up`, a shorthand for `container-breakpoint-up`, wraps any rule in the container query of a step; an unknown step, or one of 0, which would emit the base classes a second time after the responsive ones, stops the compilation. Containing the inline size stops an element from taking its width from its content, so `@container` belongs on a block that fills its parent, a card or a column, and collapses an `inline-flex` or a flex item with no basis to nothing. For the same reason no component turns itself into a container. + ### Fixed * `.container` was 0px wide on a viewport narrower than 360px, with its content spilling out of it. Its `max-width` reads `--breakpoint-value`, which the props set at each breakpoint to the width it starts at, and the first breakpoint, `xxs`, starts at 0. The token keeps that value, which is true of the breakpoint and is what a script reading it gets. Instead, the container rule now takes the whole width up to the next breakpoint, through a media query for every breakpoint that starts at 0. In `dist/` the `.container` rule gains `@media (width<=359.98px)` with `max-width: 100%`, and nothing else moves. diff --git a/docs/contents/settings/breakpoints.md b/docs/contents/settings/breakpoints.md index 999395a..63c6c06 100644 --- a/docs/contents/settings/breakpoints.md +++ b/docs/contents/settings/breakpoints.md @@ -29,6 +29,28 @@ Many utility classes can be used with a breakpoint prefix. Responsive classes have the same zero specificity as the others: they are emitted after the base classes, so at their breakpoint and above they override them. +### Container Queries +A breakpoint prefix reads the width of the viewport. A container prefix, the same name after an `@`, reads the width of the nearest ancestor with the `@container` class, so a component lays itself out by the room it is given: a card in a sidebar and the same card in the main column. + +```html +
      +
      …
      +
      +``` + + + + + + +The steps are the ones of Tailwind CSS, smaller than the viewport breakpoints because a container is narrower than the page. They are available on the utilities that lay a block out: display, flex direction and wrap, the justify and align utilities of content and items, grid template columns, the `col-*` column spans and text align. A container class is emitted after the responsive classes of its utility, so it wins when both apply. With no `@container` above it, a container class never applies. + +`@container` sets `container-type: inline-size`, which stops the element from taking its width from its content. Put it on a block that fills its parent (a card, a column, a panel), not on one that shrinks to what it holds, such as an `inline-flex` or a flex item with no basis: that one collapses to a width of 0. + ### Current Breakpoint The current breakpoint can be accessed using the `--breakpoint-key` and `--breakpoint-value` CSS Custom Properties. @@ -68,8 +90,17 @@ Volver Style includes a set of SCSS mixins that can be used to create responsive Each is a shorthand for the `media-breakpoint-*` mixin of the same name with `$breakpoints` as the last argument. +The container steps have their own mixin, a shorthand for `container-breakpoint-up` with `$container-breakpoints`. An unknown step, or a step of 0, stops the compilation with an error. + +```scss +// nearest @container from md up: (min-width: 28rem) +@include cq-up('md') { + // ... +} +``` + ### Customization -You can create your own breakpoints overriding the `$breakpoints` SCSS variable. The map must be in ascending order and start with a key set to `0`, or the compilation stops with an error. +You can create your own breakpoints overriding the `$breakpoints` SCSS variable. The map must be in ascending order and start with a key set to `0`: the compilation only prints a warning otherwise, and the CSS is wrong: without a key set to `0` the classes with no prefix are not emitted, and out of order a smaller breakpoint overrides a larger one. ```scss @use '@volverjs/style/scss/context' with ( @@ -83,4 +114,17 @@ You can create your own breakpoints overriding the `$breakpoints` SCSS variable. ) ); @use '@volverjs/style/scss'; -``` \ No newline at end of file +``` + +The container steps are the `$container-breakpoints` SCSS variable: an ascending map of widths above 0. A step of 0 stops the compilation, a map out of order prints a warning, and an empty map emits no container class. + +```scss +@use '@volverjs/style/scss/context' with ( + $container-breakpoints: ( + sm: 20rem, + md: 30rem, + lg: 40rem, + ) +); +@use '@volverjs/style/scss'; +``` diff --git a/docs/contents/utilities/flex-grid/align-content.md b/docs/contents/utilities/flex-grid/align-content.md index f28607c..c393cf6 100644 --- a/docs/contents/utilities/flex-grid/align-content.md +++ b/docs/contents/utilities/flex-grid/align-content.md @@ -2,6 +2,7 @@ title: Align Content description: Utilities for controlling how rows are positioned in multi-row flex and grid containers. breakpoints: true +containerQueries: true --- ### Align Content The `align-content` property aligns a flex container's lines within when there is extra space in the cross-axis, similar to how `justify-content` aligns individual items within the main-axis. diff --git a/docs/contents/utilities/flex-grid/align-items.md b/docs/contents/utilities/flex-grid/align-items.md index 060351e..e6be9cc 100644 --- a/docs/contents/utilities/flex-grid/align-items.md +++ b/docs/contents/utilities/flex-grid/align-items.md @@ -2,6 +2,7 @@ title: Align Items description: Utilities for controlling how flex and grid items are positioned along a container's cross axis. breakpoints: true +containerQueries: true --- ### Align Items The `align-items` property sets the default `align-self` of every item, placing it on the cross axis of its line or of its grid area. diff --git a/docs/contents/utilities/flex-grid/flex-direction.md b/docs/contents/utilities/flex-grid/flex-direction.md index b7f7d7f..826240b 100644 --- a/docs/contents/utilities/flex-grid/flex-direction.md +++ b/docs/contents/utilities/flex-grid/flex-direction.md @@ -2,6 +2,7 @@ title: Flex Direction description: Utilities for controlling the direction of flex items. breakpoints: true +containerQueries: true --- Flex direction utilities are used to control the direction of flex items. They can be used to change the direction of flex items from row to column, or vice versa. diff --git a/docs/contents/utilities/flex-grid/flex-wrap.md b/docs/contents/utilities/flex-grid/flex-wrap.md index a033a85..d9e7e0e 100644 --- a/docs/contents/utilities/flex-grid/flex-wrap.md +++ b/docs/contents/utilities/flex-grid/flex-wrap.md @@ -2,6 +2,7 @@ title: Flex Wrap description: Utilities for controlling how flex items wrap. breakpoints: true +containerQueries: true --- Flex wrap utilities are used to control how flex items wrap. They can be used to change the wrapping behavior of flex items from wrapping to not wrapping, or vice versa. diff --git a/docs/contents/utilities/flex-grid/grid-column-start-end.md b/docs/contents/utilities/flex-grid/grid-column-start-end.md index 5135ca9..fb675de 100644 --- a/docs/contents/utilities/flex-grid/grid-column-start-end.md +++ b/docs/contents/utilities/flex-grid/grid-column-start-end.md @@ -2,11 +2,14 @@ title: Grid Column Start / End description: Utilities for controlling how elements are sized and placed across grid columns. breakpoints: true +containerQueries: true --- Column start and end utilities are used to control how elements are sized and placed across grid columns. They can be used to change the size and placement of elements from the default to a specific size and placement, or vice versa. +The `col-*` classes above also take a container prefix, `@md:col-span-6`. The `col-start-*` and `col-end-*` classes below take the breakpoint prefix only. + ### Column Start The column start utilities are used to control the starting column of an element. They can be used to change the starting column of an element from the default to a specific column, or vice versa. diff --git a/docs/contents/utilities/flex-grid/grid-template-columns.md b/docs/contents/utilities/flex-grid/grid-template-columns.md index 57240cb..8319809 100644 --- a/docs/contents/utilities/flex-grid/grid-template-columns.md +++ b/docs/contents/utilities/flex-grid/grid-template-columns.md @@ -2,6 +2,7 @@ title: Grid Template Columns description: Utilities for specifying the columns in a grid layout. breakpoints: true +containerQueries: true --- Grid template columns utilities are used to specify the columns in a grid layout. They can be used to change the number of columns from the default to a specific number. diff --git a/docs/contents/utilities/flex-grid/justify-content.md b/docs/contents/utilities/flex-grid/justify-content.md index 3de5be3..e5927df 100644 --- a/docs/contents/utilities/flex-grid/justify-content.md +++ b/docs/contents/utilities/flex-grid/justify-content.md @@ -2,6 +2,7 @@ title: Justify Content description: Utilities for controlling how flex and grid items are positioned along a container's main axis. breakpoints: true +containerQueries: true --- The `justify-content` property aligns flex items along the main axis of the current line of the flex container. It helps distribute extra free space left over when either all the flex items on a line are inflexible, or are flexible but have reached their maximum size. It also exerts some control over the alignment of items when they overflow the line. diff --git a/docs/contents/utilities/flex-grid/justify-items.md b/docs/contents/utilities/flex-grid/justify-items.md index 64cea32..50fa76d 100644 --- a/docs/contents/utilities/flex-grid/justify-items.md +++ b/docs/contents/utilities/flex-grid/justify-items.md @@ -2,6 +2,7 @@ title: Justify Items description: Utilities for controlling how grid items are aligned along their inline axis. breakpoints: true +containerQueries: true --- The `justify-items` property sets the default `justify-self` of every grid item, placing it on the inline axis of its grid area. Flex containers ignore it. diff --git a/docs/contents/utilities/layout/display.md b/docs/contents/utilities/layout/display.md index 84d7727..485d3d3 100644 --- a/docs/contents/utilities/layout/display.md +++ b/docs/contents/utilities/layout/display.md @@ -2,10 +2,11 @@ title: Display description: Utilities for controlling the display box type of an element. breakpoints: true +containerQueries: true --- ### Display without breakpoints -The following utilities can not be used with breakpoint prefix. +The following utilities can not be used with a breakpoint or a container prefix. \ No newline at end of file diff --git a/docs/contents/utilities/typography/text-align.md b/docs/contents/utilities/typography/text-align.md index 7964662..3c8a5f8 100644 --- a/docs/contents/utilities/typography/text-align.md +++ b/docs/contents/utilities/typography/text-align.md @@ -2,6 +2,7 @@ title: Text Align description: Utilities for controlling the alignment of text. breakpoints: true +containerQueries: true --- diff --git a/docs/pages/utilities/[group]/[name].vue b/docs/pages/utilities/[group]/[name].vue index 877ce92..2990851 100644 --- a/docs/pages/utilities/[group]/[name].vue +++ b/docs/pages/utilities/[group]/[name].vue @@ -8,6 +8,7 @@ const title = ref('') const description = ref('') const breakpoints = ref(false) + const containerQueries = ref(false) const colors = ref(false) const customProperties = ref(false) const spacing = ref(false) @@ -25,6 +26,7 @@ title.value = attributes.title description.value = attributes.description breakpoints.value = attributes.breakpoints + containerQueries.value = attributes.containerQueries colors.value = attributes.colors customProperties.value = attributes.customProperties spacing.value = attributes.spacing @@ -67,6 +69,11 @@ class="vv-badge vv-badge--sm"> breakpoints + + container queries + @@ -114,6 +121,15 @@ prefix.

      .{breakpoint}:{utility} +
      ` styles the whole block | diff --git a/skills/volverjs-style/references/utilities.md b/skills/volverjs-style/references/utilities.md index 75a6b79..7dd5802 100644 --- a/skills/volverjs-style/references/utilities.md +++ b/skills/volverjs-style/references/utilities.md @@ -19,6 +19,16 @@ before using it. |---|---|---|---|---|---|---|---| | min-width | 360px | 576px | 992px | 1024px | 1280px | 1440px | 1536px | +- Container variant: `@{step}:{class}` (`@md:flex-row`), the width of the nearest ancestor + with the `@container` class (`container-type: inline-size`); with none it never applies. + Only for display, `flex-` direction and wrap, `justify-` `content-` `justify-items-` + `items-`, `grid-cols-`, `col-` (span, auto, full) and `text-` align. Emitted after the + responsive classes of the same utility, so it wins when both apply. + +| Container step | @xs | @sm | @md | @lg | @xl | @xxl | @xxxl | +|---|---|---|---|---|---|---|---| +| min-width | 20rem | 24rem | 28rem | 32rem | 36rem | 42rem | 48rem | + ## Scales shared by several groups **Static spacing scale** (used by margin, padding, gap, inset, sizing): @@ -83,6 +93,7 @@ white black transparent shadow backdrop | `overflow-` `overflow-x-` `overflow-y-` | `visible` `hidden` `scroll` `auto` | no | | `scrollbar-` | `thin` (thin, `word-5` thumb on a transparent track) `none` (hidden, still scrolls) | no | | `container` | | width 100%, max-width follows the current breakpoint, the whole width below `xs` (360px); no size suffixes | +| `@container` | | `container-type: inline-size`, the box the `@` variants measure; it stops taking its width from its content, so not on an `inline-flex` or a flex item with no basis; no prefixes | | `aspect-` | `auto` `square` `photo` (3/2) `tv` (4/3) `wide` (16/9) `ultrawide` (21/9) | yes | | `object-` (fit) | `fill` `contain` `cover` `none` `scale-down` | no | | `object-` (position) | `top` `bottom` `left` `right` `center` `left-top` `left-bottom` `right-top` `right-bottom` | no | diff --git a/src/export.scss b/src/export.scss index dff3413..2aa0890 100644 --- a/src/export.scss +++ b/src/export.scss @@ -75,6 +75,11 @@ #{'breakpoints__' + $key}: $value; } + // container breakpoints + @each $key, $value in $container-breakpoints { + #{'container-breakpoints__' + $key}: $value; + } + // columns @each $index in $columns { #{'columns__' + $index}: $index; diff --git a/src/settings/_breakpoints.scss b/src/settings/_breakpoints.scss index 03dd590..186cad7 100644 --- a/src/settings/_breakpoints.scss +++ b/src/settings/_breakpoints.scss @@ -13,3 +13,21 @@ $breakpoints: ( @include assert-ascending($breakpoints, '$breakpoints'); @include assert-starts-at-zero($breakpoints, '$breakpoints'); + +// The widths of a container for the `@` prefixed utilities (`@md:flex-row`), +// which query the nearest ancestor marked with the `@container` class rather +// than the viewport. The steps are the ones of Tailwind CSS, a component +// being narrower than the page: every step above 0, since a step of 0 would +// emit the base classes a second time after the responsive ones. An empty map +// emits no container variant at all. +$container-breakpoints: ( + xs: 20rem, + sm: 24rem, + md: 28rem, + lg: 32rem, + xl: 36rem, + xxl: 42rem, + xxxl: 48rem, +) !default; + +@include assert-ascending($container-breakpoints, '$container-breakpoints'); diff --git a/src/tools/_breakpoints.scss b/src/tools/_breakpoints.scss index 3416c32..9da29c7 100644 --- a/src/tools/_breakpoints.scss +++ b/src/tools/_breakpoints.scss @@ -179,3 +179,31 @@ @content; } } + +// Container of at least the width of the given step. The query reads the +// nearest ancestor that declares `container-type`, the `@container` class, and +// matches nothing where there is none. +// >> container-breakpoint-up(md, (sm: 24rem, md: 28rem)) +// @container (min-width: 28rem) +@mixin container-breakpoint-up($name, $cqs: $container-breakpoints) { + @if not map.has-key($cqs, $name) { + @error "Unknown container breakpoint `#{$name}`, expected one of: #{map.keys($cqs)}."; + } + + $min: map.get($cqs, $name); + + // Not `== 0`: Sass tells `0rem` and `0px` apart from `0` + @if not ($min > 0) { + @error "The container breakpoint `#{$name}` is #{$min}: a container query needs a width above 0."; + } + + @container (min-width: #{$min}) { + @content; + } +} + +@mixin cq-up($name, $cqs: $container-breakpoints) { + @include container-breakpoint-up($name, $cqs) { + @content; + } +} diff --git a/src/tools/mixin-modules/_utilities.scss b/src/tools/mixin-modules/_utilities.scss index 97602f6..2e91366 100644 --- a/src/tools/mixin-modules/_utilities.scss +++ b/src/tools/mixin-modules/_utilities.scss @@ -1,3 +1,4 @@ +@use 'sass:list'; @use 'sass:meta'; @use 'sass:string'; @use '../functions' as *; @@ -9,6 +10,20 @@ // Generates utility classes from maps // ============================================ +// The query of a step: a media query on the viewport, or a container query +// on the nearest ancestor that declares `container-type` +@mixin -query-up($query, $name, $bps) { + @if $query == container { + @include container-breakpoint-up($name, $bps) { + @content; + } + } @else { + @include media-breakpoint-up($name, $bps) { + @content; + } + } +} + // Transform a map into a list of utility classes // @param {Map|List} $map - The map or list to transform // @param {String} $class - Base class name @@ -19,6 +34,13 @@ // @param {Map} $bps - Breakpoints map // @param {String} $prefix - Class prefix for breakpoints // @param {Bool} $zero-specificity - Whether to wrap in :where() +// @param {Map} $cqs - Container breakpoints map: the classes are emitted again +// behind a container query, prefixed with `@` (`@md:flex`), after the +// responsive ones, so that a container variant wins when both apply +// @param {String} $query - `media` or `container`, the query of the steps of +// `$bps`: set by the mixin itself when it emits the container variants +// The last two come after the others, so a call that passes them by position +// keeps its meaning. @mixin spread-map-into-utilities( $map: (), $class: '', @@ -28,12 +50,18 @@ $deep: true, $bps: (xxs: 0), $prefix: '', - $zero-specificity: false + $zero-specificity: false, + $cqs: (), + $query: media ) { @if (meta.type-of($map) == 'map' or meta.type-of($map) == 'list') and $attribute != '' { @each $breakpoint, $breakpoint-value in $bps { @if $breakpoint-value > 0 { $prefix: $breakpoint + '\\:'; + + @if $query == container { + $prefix: '\\@' + $prefix; + } } @each $key, $value in $map { @@ -52,6 +80,7 @@ $prop: $prop, $name: $key, $bps: ($breakpoint: $breakpoint-value), + $query: $query, $prefix: $prefix, $zero-specificity: $zero-specificity ); @@ -92,7 +121,7 @@ } %#{$prefix}#{$class-name} { - @include media-breakpoint-up($breakpoint, $bps) { + @include -query-up($query, $breakpoint, $bps) { @if meta.type-of($attribute) != 'list' { $attribute: ($attribute); } @@ -120,5 +149,21 @@ } } } + + // The container variants: a nested call walks the map again with the + // container steps, and passes no `$cqs` of its own + @if list.length($cqs) > 0 { + @include spread-map-into-utilities( + $map: $map, + $class: $class, + $attribute: $attribute, + $prop: $prop, + $name: $name, + $deep: $deep, + $bps: $cqs, + $query: container, + $zero-specificity: $zero-specificity + ); + } } } diff --git a/src/utilities/flexbox.scss b/src/utilities/flexbox.scss index e84b12e..17339a2 100644 --- a/src/utilities/flexbox.scss +++ b/src/utilities/flexbox.scss @@ -9,6 +9,7 @@ $class: flex, $attribute: flex-direction, $bps: $breakpoints, + $cqs: $container-breakpoints, $zero-specificity: $zero-specificity-for-utilities ); @@ -18,6 +19,7 @@ $class: flex, $attribute: flex-wrap, $bps: $breakpoints, + $cqs: $container-breakpoints, $zero-specificity: $zero-specificity-for-utilities ); @@ -110,6 +112,7 @@ $class: $class, $attribute: $attribute, $bps: $breakpoints, + $cqs: $container-breakpoints, $zero-specificity: $zero-specificity-for-utilities ); } @@ -121,6 +124,7 @@ $class: $class, $attribute: $attribute, $bps: $breakpoints, + $cqs: $container-breakpoints, $zero-specificity: $zero-specificity-for-utilities ); } diff --git a/src/utilities/grid.scss b/src/utilities/grid.scss index fcef730..64da98c 100644 --- a/src/utilities/grid.scss +++ b/src/utilities/grid.scss @@ -8,6 +8,7 @@ $class: grid-cols, $attribute: grid-template-columns, $bps: $breakpoints, + $cqs: $container-breakpoints, $zero-specificity: $zero-specificity-for-utilities ); @@ -17,6 +18,7 @@ $class: col, $attribute: grid-column, $bps: $breakpoints, + $cqs: $container-breakpoints, $zero-specificity: $zero-specificity-for-utilities ); diff --git a/src/utilities/layout.scss b/src/utilities/layout.scss index d65b56d..9e3efc0 100644 --- a/src/utilities/layout.scss +++ b/src/utilities/layout.scss @@ -36,6 +36,18 @@ @extend %container; } +// container query: the element whose width the `@` prefixed utilities of its +// descendants read. Its inline size is contained, so it no longer grows with +// its content: it suits a block that fills its parent (a card, a column, a +// panel), not one that takes the width of what it holds (an inline-flex, an +// item of a flex row with no basis), which collapses to nothing. +@include wrap-with-where( + $selector: '.\\@container', + $enabled: $zero-specificity-for-utilities +) { + container-type: inline-size; +} + // columns @include spread-map-into-utilities( $map: $columns, @@ -79,6 +91,7 @@ $map: $display-with-breakpoints, $attribute: display, $bps: $breakpoints, + $cqs: $container-breakpoints, $zero-specificity: $zero-specificity-for-utilities ); diff --git a/src/utilities/typography.scss b/src/utilities/typography.scss index 36097c4..92607a6 100644 --- a/src/utilities/typography.scss +++ b/src/utilities/typography.scss @@ -126,7 +126,8 @@ $class: text, $attribute: text-align, $zero-specificity: $zero-specificity-for-utilities, - $bps: $breakpoints + $bps: $breakpoints, + $cqs: $container-breakpoints ); // text-decorator-line From 232cc6089299f4c3b265fdd6e46b79a1520b2fc0 Mon Sep 17 00:00:00 2001 From: Alessandro Bellesia Date: Mon, 12 Oct 2026 15:53:40 +0200 Subject: [PATCH 05/12] feat(components): open and close transitions for accordion, popover and dialog The content of vv-accordion grows from 0 to its height and back through `::details-content`, `interpolate-size` and `content-visibility` with `allow-discrete`, where the browser has them (Chrome). The height is a plain declaration, so an accordion nested in an open one keeps its own. vv-popover with `popover` fades in from a `@starting-style` and out with `display` and `overlay` held by `allow-discrete`. The rules sit in the component entry, before the block so that the map can override them, on `[popover]` alone, and the closed opacity on `[popover]:not(:popover-open)`, which Safari 16 drops instead of hiding an element it cannot open. vv-dialog fades in when it opens and closes at once: a fade out needs `allow-discrete` on the closed dialog, which would keep a dialog that @volverjs/ui-vue hides with `display: none` on the page, invisible and in the way of the pointer. Its Vue transitions run as before. The generator learns `details-content`, which it dropped while still declaring its custom properties, with no `content: ''` of its own, and no longer takes a map of the block for a declaration: a `transition` set by a state reassigned a `--vv-dialog-transition` that nothing reads. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 ++ docs/contents/components/accordion/index.md | 2 + docs/contents/components/dialog/index.md | 2 + docs/contents/components/popover/index.md | 2 + .../volverjs-style/references/components.md | 9 +++- src/components/vv-dialog.scss | 13 ++++++ src/components/vv-popover.scss | 43 +++++++++++++++++++ src/settings/components/_vv-accordion.scss | 25 +++++++++++ src/settings/components/_vv-dialog.scss | 7 +++ src/settings/components/_vv-popover.scss | 2 + src/tools/mixin-modules/_attrs.scss | 19 ++++++-- 11 files changed, 122 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bfe64e..89a7fb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Added * Container queries. The `@container` class sets `container-type: inline-size`, and the utilities that lay a block out take a container prefix, the name of a step after an `@`: `@md:flex-row` applies when the nearest `@container` above the element is at least 28rem wide, and never when there is none. The steps are the ones of Tailwind CSS, `@xs:` 20rem, `@sm:` 24rem, `@md:` 28rem, `@lg:` 32rem, `@xl:` 36rem, `@xxl:` 42rem and `@xxxl:` 48rem, set by `$container-breakpoints`, and they reach display, flex direction and wrap, the justify and align utilities of content and items, `grid-cols-`, `col-` and text align: 70 classes per step, 2 KB of `volver.css` once gzipped, where giving them to every responsive utility would have added about 570 KB before compression. A container class is emitted after the responsive classes of its utility, so it wins when both apply. `spread-map-into-utilities` takes the steps in a `$cqs` argument, `$container-breakpoints: ()` emits none, and `cq-up`, a shorthand for `container-breakpoint-up`, wraps any rule in the container query of a step; an unknown step, or one of 0, which would emit the base classes a second time after the responsive ones, stops the compilation. Containing the inline size stops an element from taking its width from its content, so `@container` belongs on a block that fills its parent, a card or a column, and collapses an `inline-flex` or a flex item with no basis to nothing. For the same reason no component turns itself into a container. +* Open and close transitions on the native elements, with no script. The content of `vv-accordion` grows from 0 to its height and back: its `::details-content` transitions `block-size` to `auto` through `interpolate-size`, and `content-visibility` with `allow-discrete`, so the content stays painted until the closing ends. A browser without them, Safari and Firefox today, opens it at once as before. A `vv-popover` with `popover` fades in from a `@starting-style` and fades out with `display` and `overlay` held by `allow-discrete` until the fade ends, where the browser holds them: Safari 17.6 fades it in and hides it at once, as before. One shown without `popover` does not fade. The closed panel takes its opacity of 0 from `[popover]:not(:popover-open)`, which a browser without the Popover API (Safari 16) drops: there the element is shown as it is, and an opacity on `[popover]` alone would have left it invisible and still in the way of the pointer. A `vv-dialog` opened with `showModal()` fades in and closes at once. A fade out would need `allow-discrete` on the closed dialog, and `@volverjs/ui-vue` hides its dialog with an inline `display: none` and closes it in the same task: the dialog would stay on the page for 300ms after every close, invisible and in the way of the pointer. The Vue transitions of the dialog run as before. Every duration reads the duration tokens, which are 0s under reduced motion. +* `details-content` is a pseudo-element a component map can style. It was missing from the list the generator checks, which dropped the rule and still declared its custom properties, and it gets no `content: ''` of its own: it wraps content of the page instead of generating a box. ### Fixed @@ -23,6 +25,7 @@ All notable changes to this project will be documented in this file. * `scheme`, the `color-scheme` of the theme, is in the map of the colors, and came out as `.bg-scheme`, `.text-scheme`, `.border-scheme` and `.decoration-scheme`, which wrote `light` as a color. The color utilities leave it out, as they leave out `tint`, and `--color-scheme` stays declared. In `dist/` the four rules go away. * A nested map in `spread-map-into-utilities` was walked with every breakpoint under the prefix of the current one: at the first, of 0, it emitted the prefixed classes with no media query, and it emitted every rule of the nested map once per breakpoint. The recursion now takes the current breakpoint alone. No map of the library reaches the branch since the sizing classes stopped walking the dynamic spacing map, above, so `dist/` does not move; before that the sizing classes gave 630 classes such as `.xs:w-sm` that applied at every width and 4,410 identical copies of the others, and an application that spreads a nested map of its own with breakpoints still did. +* A `transition` declared by a state or a modifier of a block that has Vue transitions shares its name with the `transition` map of the block, which the generator took for a declaration: it reassigned a `--vv-*-transition` that no rule reads, and the transition never applied. A map in the block is no longer taken for a declaration. No component reached the branch before the fade in of `vv-dialog`. `design-tokens.json` and the `exports` map do not change. diff --git a/docs/contents/components/accordion/index.md b/docs/contents/components/accordion/index.md index 3dc5a0f..603d501 100644 --- a/docs/contents/components/accordion/index.md +++ b/docs/contents/components/accordion/index.md @@ -6,6 +6,8 @@ uiVue: true An accordion is a native `
      ` with a `summary` and a `vv-accordion__content`: the browser opens and closes it and tells assistive technology its state, so no `aria-expanded` is needed. A `vv-accordion-group` stacks several of them. +The content opens and closes with a transition of its height where the browser can animate a height to `auto`, through `::details-content` and `interpolate-size` (Chrome); elsewhere, and under reduced motion, it opens at once. + ### Disabled diff --git a/docs/contents/components/dialog/index.md b/docs/contents/components/dialog/index.md index 2714a1f..01ba3de 100644 --- a/docs/contents/components/dialog/index.md +++ b/docs/contents/components/dialog/index.md @@ -6,6 +6,8 @@ uiVue: true A `` is the backdrop, and the panel is the `article.vv-dialog__wrapper` inside it, which the dialog needs: a `__header` with the title and a `__close` button, a `__content` and a `__footer`. Name the dialog with `aria-labelledby` pointing to its title. +A dialog opened with `showModal()` fades in from the `@starting-style` of the block and closes at once: a fade out would keep a dialog that `@volverjs/ui-vue` hides on the page, invisible and in the way of the pointer, until the fade ends. With Vue, the transitions below animate both ways. + `vv-dialog--small` and `vv-dialog--fullscreen` change the size. The drawers dock the panel to a side of the viewport: `vv-dialog--drawer` at the inline end, `--drawer-start`, `--drawer-top` and `--drawer-bottom`. The `fade-*`, `scale` and `slide-*` classes are the enter and leave classes of a Vue `` of the same name: pair each drawer with the slide from its side, `slide-inline-end` for `drawer`, `slide-inline-start` for `drawer-start`, `slide-block-start` for `drawer-top` and `slide-block-end` for `drawer-bottom`. diff --git a/docs/contents/components/popover/index.md b/docs/contents/components/popover/index.md index c440866..538e35c 100644 --- a/docs/contents/components/popover/index.md +++ b/docs/contents/components/popover/index.md @@ -6,4 +6,6 @@ isNew: true The element carries `popover`, and a button with `popovertarget` opens it: the browser moves it to the top layer and shows it while it is open. The block puts back the fixed position the reset of the library unsets, at the top of the stacking order, with a `__title` and a `__footer` for actions. Placing it next to its trigger is left to the script that opens it, or to anchor positioning where the browser has it, which set its inset. Without either the panel opens at the top left corner of the viewport; `vv-popover--center` centers it, as the example does. A panel that opens on hover is the same block, with the delay in the script; menus and lists of options stay a [Dropdown](/style/components/dropdown). +A panel with `popover` fades in and out: `@starting-style` gives the fade in its first frame, and `transition-behavior: allow-discrete` keeps the panel on the page until the fade out ends. Where the browser lacks one of them it skips that half (Safari 17 fades the panel in and hides it at once), and under reduced motion it shows and hides at once. A panel shown without `popover` does not fade. + diff --git a/skills/volverjs-style/references/components.md b/skills/volverjs-style/references/components.md index 3be6a5f..95b0bbf 100644 --- a/skills/volverjs-style/references/components.md +++ b/skills/volverjs-style/references/components.md @@ -396,7 +396,9 @@ Modifiers: `fixed` `absolute` `reverse` `stack` `full-bleed`, position `top-star ## vv-dialog The `` block is the backdrop; the panel is the mandatory `
      ` child -(`__wrapper`). The header holds the title text directly (no `__title`). +(`__wrapper`). The header holds the title text directly (no `__title`). Opened with +`showModal()` it fades in and closes at once; the `fade-*`, `scale` and `slide-*` modifiers +are the classes of a Vue ``. ```html @@ -470,6 +472,9 @@ Panels toggle on `:target` or the `target` class. ## vv-accordion +The height of the content animates where the browser has `::details-content` and +`interpolate-size` (Chrome); elsewhere the accordion opens at once. + ```html
      Shipping @@ -747,7 +752,7 @@ questionnaire are `vv-radio--card` / `vv-checkbox--card` or a text field. | component | elements | modifiers / states | |---|---|---| | vv-kbd, vv-kbd-group | | sized in `em` | -| vv-popover | title, footer | `center`; on `[popover]`, placed by a script or anchor positioning, top left without one | +| vv-popover | title, footer | `center`; on `[popover]`, placed by a script or anchor positioning, top left without one; fades in and out (`@starting-style`, `allow-discrete`; Safari 17 hides it at once) | | vv-command | search (focus-within), input, list, heading, option, shortcut, empty | option: `aria-selected="true"`, disabled; groups are `role="group"` labelled by a heading | | vv-input-otp | group, slot, separator | `invalid` | | vv-sidebar | header, content, footer, label | `collapsed` (hides `label` from sight, not from AT, and centres the icons of header, content and footer on the rail: wrap every text of the nav in `label`); `--sidebar-width`, `--sidebar-collapsed-width` | diff --git a/src/components/vv-dialog.scss b/src/components/vv-dialog.scss index abc736e..eaf17b6 100644 --- a/src/components/vv-dialog.scss +++ b/src/components/vv-dialog.scss @@ -4,6 +4,7 @@ @include layer(components, $use-css-layers, $layer-prefix) { $component-name: map.get($components-names, vv-dialog); $component-map: $vv-dialog; +$component-selector: '.#{ $component-name}'; @include spread-map-into-bem( $map: $component-map, @@ -13,4 +14,16 @@ $component-map: $vv-dialog; $bps: $breakpoints ); +// The frame before the dialog opens, which the fade in of the `open` state +// starts from. A component map has no key for `@starting-style`, and the +// rule has to follow the block it overrides. +@starting-style { + @include wrap-with-where( + $selector: '#{$component-selector}[open]', + $enabled: $zero-specificity-for-components + ) { + opacity: var(--opacity-0); + } +} + } diff --git a/src/components/vv-popover.scss b/src/components/vv-popover.scss index 788301b..f9eec14 100644 --- a/src/components/vv-popover.scss +++ b/src/components/vv-popover.scss @@ -6,6 +6,37 @@ $component-name: map.get($components-names, vv-popover); $component-map: $vv-popover; $component-selector: '.#{ $component-name}'; +// A panel on the Popover API fades in and out. `display` and `overlay` keep it +// on the page and in the top layer until the fade has ended. A browser without +// `@starting-style` shows it at once, and one that does not hold `display` +// through the transition (Safari 17) hides it at once, as before; so does +// reduced motion, where the duration tokens are 0s. The rules are written here +// rather than as a state of the map, which would also give them to the +// `--popover` and `.popover` classes: hidden with `display: none`, those would +// stay on the page for the length of the transition. They come before the +// block, so a state or a modifier of the map overrides them at the same zero +// specificity (`transition: none` for a panel that must not fade). +@include wrap-with-where( + $selector: '#{$component-selector}[popover]', + $enabled: $zero-specificity-for-components +) { + transition: + opacity var(--duration-150) var(--ease-out), + display var(--duration-150) allow-discrete, + overlay var(--duration-150) allow-discrete; +} + +// The closed panel, which the fade out runs to. Not on `[popover]` alone: a +// browser without the Popover API (Safari 16) shows the element as it is, and +// would keep it invisible and still in the way of the pointer, while it drops +// this selector for its unknown pseudo-class. +@include wrap-with-where( + $selector: '#{$component-selector}[popover]:not(:popover-open)', + $enabled: $zero-specificity-for-components +) { + opacity: var(--opacity-0); +} + @include spread-map-into-bem( $map: $component-map, $block: $component-name, @@ -14,4 +45,16 @@ $component-selector: '.#{ $component-name}'; $bps: $breakpoints ); +// The frame before the panel opens, which the fade in starts from. After the +// block, so it also starts from 0 a panel whose modifier sets an opacity. A +// component map has no key for `@starting-style`. +@starting-style { + @include wrap-with-where( + $selector: '#{$component-selector}:popover-open', + $enabled: $zero-specificity-for-components + ) { + opacity: var(--opacity-0); + } +} + } diff --git a/src/settings/components/_vv-accordion.scss b/src/settings/components/_vv-accordion.scss index e16f7fd..0f9e351 100644 --- a/src/settings/components/_vv-accordion.scss +++ b/src/settings/components/_vv-accordion.scss @@ -8,8 +8,33 @@ $vv-accordion: ( --accordion-padding-bottom: var(--spacing-14), --accordion-padding-left: var(--spacing-14), overflow: hidden, + // The box the browser wraps around everything but the summary: its height + // runs from 0 to `auto`, which `interpolate-size` lets a transition reach, + // and `content-visibility` flips at the end of the closing rather than at + // its start. The `overflow` of the block clips what the box does not hold + // yet. A browser without `::details-content` drops the rule and opens the + // accordion at once, one without `interpolate-size` (Safari, Firefox) as + // well. The duration tokens are 0s under reduced motion. The height is a + // plain declaration: through a `--vv-accordion-*` property, the `auto` of + // an open accordion would be inherited by an accordion nested in it, + // which would then open and close with no transition. + pseudo: ( + details-content: ( + [block-size]: 0, + interpolate-size: allow-keywords, + transition: ( + block-size var(--duration-300) var(--ease-in-out), + content-visibility var(--duration-300) allow-discrete, + ), + ), + ), state: ( open: ( + pseudo: ( + details-content: ( + [block-size]: auto, + ), + ), element: ( summary: ( background-color: var(--color-surface-2), diff --git a/src/settings/components/_vv-dialog.scss b/src/settings/components/_vv-dialog.scss index a09b18b..1eddb21 100644 --- a/src/settings/components/_vv-dialog.scss +++ b/src/settings/components/_vv-dialog.scss @@ -56,8 +56,15 @@ $vv-dialog: ( close: ( display: none, ), + // A dialog opened with `showModal()` fades in from the + // `@starting-style` of the component entry. It closes at once: a fade + // out needs `allow-discrete` on the closed dialog, which would keep a + // dialog that `@volverjs/ui-vue` hides with an inline `display: none` + // on the page, invisible and in the way of the pointer, for the + // length of the transition. The Vue transitions below run as before. open: ( overflow: hidden, + transition: var(--transition-opacity), ), ), element: ( diff --git a/src/settings/components/_vv-popover.scss b/src/settings/components/_vv-popover.scss index 9b6e14b..b89666a 100644 --- a/src/settings/components/_vv-popover.scss +++ b/src/settings/components/_vv-popover.scss @@ -25,6 +25,8 @@ $vv-popover: ( color: var(--color-word-2), font-size: var(--text-14), line-height: var(--leading-normal), + // The fade of a panel on the Popover API is written in the component + // entry, on the `popover` attribute alone. element: ( title: ( margin: 0 0 var(--spacing-4), diff --git a/src/tools/mixin-modules/_attrs.scss b/src/tools/mixin-modules/_attrs.scss index e121e83..921afed 100644 --- a/src/tools/mixin-modules/_attrs.scss +++ b/src/tools/mixin-modules/_attrs.scss @@ -49,7 +49,7 @@ $-pseudo-elements: ( inner-spin-button, outer-spin-button, progress-bar, progress-value, inner-element, marker, datetime-edit, datetime-edit-fields-wrapper, datetime-edit-day-field, - backdrop, file-selector-button, + backdrop, file-selector-button, details-content, -webkit-progress-bar, -webkit-progress-value, -webkit-inner-element, -moz-progress-bar, -moz-progress-value, -moz-inner-element, -webkit-details-marker, -webkit-scrollbar, @@ -58,6 +58,10 @@ $-pseudo-elements: ( -moz-range-track, -moz-range-progress, -moz-range-thumb ); +// Pseudo-elements that wrap content of the page rather than generate a box of +// their own: an empty `content` means nothing on them, so none is added +$-element-backed-pseudo-elements: (details-content); + // Transform a map into a list of CSS attributes // @param {Map} $original - Original map for reference // @param {Map} $map - The map to transform @@ -114,7 +118,9 @@ $-pseudo-elements: ( @include pseudo-element-placeholder($pseudo-element) { @if not $pseudo-original { - @if not map.has-key($pseudo-map, 'content') { + @if not map.has-key($pseudo-map, 'content') and + not list.index($-element-backed-pseudo-elements, $pseudo-element) + { content: ''; } @@ -169,7 +175,14 @@ $-pseudo-elements: ( $prop-to-replace: #{$prop-to-replace}-#{$key}; $prop-to-replace-with: #{$prop-to-replace-with}-#{$key}; - @if map.get($original, $key) { + // A map in the block is a nested structure, not a + // declaration with a custom property to reassign: a + // `transition` set by a state or a modifier shares its + // name with the map of the Vue transitions, and + // reassigned a `--vv-*-transition` that nothing reads + $original-value: map.get($original, $key); + + @if $original-value and meta.type-of($original-value) != 'map' { #{$prop-to-replace}: var(#{$prop-to-replace-with}); } @else { #{$key}: var(#{$prop-to-replace-with}); From b549dca792d985faafc3272b594be54a890ac5b2 Mon Sep 17 00:00:00 2001 From: Alessandro Bellesia Date: Mon, 12 Oct 2026 15:53:40 +0200 Subject: [PATCH 06/12] feat(props): the radius scale follows --rounded `--rounded-sm` to `--rounded-xxxl` are declared as `calc(var(--rounded) * )`, the ratio taken from `$border-radius` at compile time, so the computed values do not move while one `--rounded` rescales every corner of the utilities and the components. `none`, `full`, the steps in `$border-radius-fixed` and the steps in another unit than `rounded` keep their value, and so does every step when the base is 0. The token reference also says that an override written on `:root` alone does not reach inside a `.theme`, which declares every token again. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++ .../utilities/borders/border-radius.md | 28 ++++++++++++- skills/volverjs-style/SKILL.md | 3 +- skills/volverjs-style/references/scss.md | 1 + skills/volverjs-style/references/tokens.md | 15 +++---- src/props/borders.scss | 41 ++++++++++++++++++- src/settings/_borders.scss | 6 +++ 7 files changed, 88 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89a7fb3..148c00a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ All notable changes to this project will be documented in this file. * Open and close transitions on the native elements, with no script. The content of `vv-accordion` grows from 0 to its height and back: its `::details-content` transitions `block-size` to `auto` through `interpolate-size`, and `content-visibility` with `allow-discrete`, so the content stays painted until the closing ends. A browser without them, Safari and Firefox today, opens it at once as before. A `vv-popover` with `popover` fades in from a `@starting-style` and fades out with `display` and `overlay` held by `allow-discrete` until the fade ends, where the browser holds them: Safari 17.6 fades it in and hides it at once, as before. One shown without `popover` does not fade. The closed panel takes its opacity of 0 from `[popover]:not(:popover-open)`, which a browser without the Popover API (Safari 16) drops: there the element is shown as it is, and an opacity on `[popover]` alone would have left it invisible and still in the way of the pointer. A `vv-dialog` opened with `showModal()` fades in and closes at once. A fade out would need `allow-discrete` on the closed dialog, and `@volverjs/ui-vue` hides its dialog with an inline `display: none` and closes it in the same task: the dialog would stay on the page for 300ms after every close, invisible and in the way of the pointer. The Vue transitions of the dialog run as before. Every duration reads the duration tokens, which are 0s under reduced motion. * `details-content` is a pseudo-element a component map can style. It was missing from the list the generator checks, which dropped the rule and still declared its custom properties, and it gets no `content: ''` of its own: it wraps content of the page instead of generating a box. +### Changed + +* `--rounded` is the base of the radius scale. `--rounded-sm` to `--rounded-xxxl` are declared as `calc(var(--rounded) * )`, the ratio taken from `$border-radius` at compile time, so the computed values do not move while `:root, :host, .theme { --rounded: 0.375rem }` now rescales every corner of the utilities and of the components, where before it changed the `rounded` step alone. The steps are computed where they are declared, on the root and on every `.theme`, so a section with the `theme` class can carry a scale of its own. `none` and `full` keep their value, as does any step listed in the new `$border-radius-fixed` or whose unit differs from the one of `rounded`; a pill in px over a base in px has to be listed there. A script that reads `--rounded-lg` through `getPropertyValue` gets the `calc()` instead of `0.5rem`. + ### Fixed * `.container` was 0px wide on a viewport narrower than 360px, with its content spilling out of it. Its `max-width` reads `--breakpoint-value`, which the props set at each breakpoint to the width it starts at, and the first breakpoint, `xxs`, starts at 0. The token keeps that value, which is true of the breakpoint and is what a script reading it gets. Instead, the container rule now takes the whole width up to the next breakpoint, through a media query for every breakpoint that starts at 0. In `dist/` the `.container` rule gains `@media (width<=359.98px)` with `max-width: 100%`, and nothing else moves. diff --git a/docs/contents/utilities/borders/border-radius.md b/docs/contents/utilities/borders/border-radius.md index 0a62ee5..510faff 100644 --- a/docs/contents/utilities/borders/border-radius.md +++ b/docs/contents/utilities/borders/border-radius.md @@ -73,4 +73,30 @@ Border radius utilities are available for individual corners, as well as for all
-
\ No newline at end of file +
+ +### Rescale the corners +`--rounded` is the base of the scale: the other steps are declared as a multiple of it, so one declaration rescales every corner of the utilities and of the components. `none` and `full` keep their value. + +```css +:root, :host, .theme { + --rounded: 0.375rem; /* sm 0.1875rem, lg 0.75rem, xxl 1.5rem */ +} +``` + +The steps are computed where they are declared, on the root and on every `.theme` element, so a section gets its own scale with the `theme` class: + +```html +
+ +
+``` + +In SCSS the ratios come from `$border-radius` at compile time. A step that is not a size, like a pill, goes in `$border-radius-fixed` to keep its own value: + +```scss +@use '@volverjs/style/scss/context' with ( + $border-radius: (none: 0px, sm: 2px, rounded: 4px, lg: 8px, pill: 9999px, full: 9999px), + $border-radius-fixed: (none, pill, full) +); +``` diff --git a/skills/volverjs-style/SKILL.md b/skills/volverjs-style/SKILL.md index a679e4a..2b709ec 100644 --- a/skills/volverjs-style/SKILL.md +++ b/skills/volverjs-style/SKILL.md @@ -208,7 +208,8 @@ with the block: `--vv-button-background`, `--vv-button-state-hover-background`, :root, :host, .theme { --color-brand: #45cb85; --color-tint: #45cb85; --rounded: 0.5rem; } ``` -`--color-tint` gives the brand hue to gray, word and surface too; leave it out to keep the +`--rounded` is the base of the radius scale: `--rounded-sm` … `--rounded-xxxl` are multiples +of it, so it rescales every corner (`none` and `full` stay). `--color-tint` gives the brand hue to gray, word and surface too; leave it out to keep the neutrals. The readable, contrast and graphic roles follow the new brand by themselves. A new button variant goes through the SCSS map (see scss.md) or, in plain CSS, names the hover state itself, since a class beats the zero specificity of the library's state diff --git a/skills/volverjs-style/references/scss.md b/skills/volverjs-style/references/scss.md index 78f0231..69c0b2d 100644 --- a/skills/volverjs-style/references/scss.md +++ b/skills/volverjs-style/references/scss.md @@ -64,6 +64,7 @@ compile error, because the comma separates configuration entries. | `$font-family-sans` `$font-family-serif` `$font-family-mono` | system stacks | one quoted string each | | `$font-size` `$font-size-dynamic` `$font-weight` `$spacing` `$spacing-dynamic` `$breakpoints` `$colors` | maps | override the scales themselves | | `$container-breakpoints` | `xs` 20rem … `xxxl` 48rem | steps of the `@` container variants, all above 0; `()` emits none | +| `$border-radius-fixed` | `(none, full)` | radius steps that keep their value instead of following `--rounded` | | `$vv-button` `$vv-card` … one per component | maps | see "Extending a component" | | `$components-prefix` | `vv` | class prefix for every component | | `$components-names` | map | rename a component, classes and `--vv-*` properties alike, except in `.preflight`, which extends and leaves out the default `vv-` names: a renamed component loses what preflight gives its bare tags, and one renamed outside the prefix is dressed by it. It cannot drop a component, `scss` emits them all, so cherry-pick the modules to leave some out | diff --git a/skills/volverjs-style/references/tokens.md b/skills/volverjs-style/references/tokens.md index 55ed555..77146f3 100644 --- a/skills/volverjs-style/references/tokens.md +++ b/skills/volverjs-style/references/tokens.md @@ -1,8 +1,9 @@ # Design tokens, CSS-only customization, dark theme All tokens are CSS custom properties declared on `:where(:host, :root, .theme)`, which -has zero specificity: a plain `:root { … }` in your stylesheet overrides any of them -without `!important`. The machine-readable copy is +has zero specificity: a plain `:root, :host, .theme { … }` in your stylesheet overrides +any of them without `!important`. `:root` alone does not reach inside an element with the +`theme` class, which declares every token again. The machine-readable copy is `node_modules/@volverjs/style/design-tokens.json` (keys `volver` and `volver-dark`, entries shaped `{ value, type }`; read it by path, it has no `exports` entry). @@ -24,7 +25,7 @@ entries shaped `{ value, type }`; read it by path, it has no `exports` entry). | Font weight | `--font-{thin,extralight,light,normal,medium,semibold,bold,extrabold,black}` | | Font size | `--text-{6..128 static steps}`, `--text-smaller`, `--text-larger`, dynamic `--text-{xs,sm,md,lg,xl}` | | Line height, tracking | `--leading-{none,tight,snug,normal,relaxed,loose}`, `--tracking-{tighter,tight,normal,wide,wider,widest}` | -| Borders | `--border` (1px), `--border-{0,2,4,8}`, `--rounded`, `--rounded-{none,sm,md,lg,xl,xxl,xxxl,full}` | +| Borders | `--border` (1px), `--border-{0,2,4,8}`, `--rounded`, `--rounded-{none,sm,md,lg,xl,xxl,xxxl,full}`; `sm` … `xxxl` are `calc(var(--rounded) * n)`, so `--rounded` rescales them, declared on the root or on a `.theme` | | Breakpoints | `--breakpoint-{xs,sm,md,lg,xl,xxl,xxxl}` (no xxs); `--breakpoint-key` and `--breakpoint-value` are re-declared inside each media query and always hold the current breakpoint | | Layout | `--z-{1,sticky,fixed,dropdown,modal-backdrop,modal,confirm-backdrop,confirm,popover,tooltip,toast,notification-alert,auto}`, `--aspect-*`, `--w-*`, `--h-*` | | Direction | `--direction`: `1` where the inline axis runs left to right, `-1` where it runs the other way | @@ -64,16 +65,16 @@ when the library is compiled with `$use-color-mix: false`, where the contrast to chosen at compile time and `--color-tint` does not exist). ```css -:root { +:root, :host, .theme { --color-brand: #45cb85; --color-tint: #45cb85; /* gray, word, surface, shadow, backdrop follow its hue */ --color-accent: #e040fb; --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif; - --rounded: 0.5rem; + --rounded: 0.5rem; /* the whole radius scale but none and full */ } /* one component, everywhere */ -:root { +:root, :host, .theme { --vv-button-border-radius: var(--rounded-full); --vv-button-font-weight: var(--font-medium); } @@ -95,7 +96,7 @@ chosen at compile time and `--color-tint` does not exist). } /* all form fields at once */ -:root { +:root, :host, .theme { --input-min-height: var(--spacing-44); --input-background-color: var(--color-surface); } diff --git a/src/props/borders.scss b/src/props/borders.scss index 07651c2..556970e 100644 --- a/src/props/borders.scss +++ b/src/props/borders.scss @@ -1,8 +1,47 @@ +@use 'sass:list'; +@use 'sass:map'; +@use 'sass:math'; +@use 'sass:meta'; +@use 'sass:string'; @use '../context' as *; +// The radius scale as custom properties: each step but `rounded` and the fixed +// ones becomes `calc(var(--rounded) * )`, the ratio taken from the map +// at compile time, so the default values do not move. A step written in a unit +// other than the one of `rounded`, or in none, keeps its own value: across +// units the ratio would carry a `px` per `in`, and a unitless `0` would divide +// into a number with a unit. A base of 0 has no ratio either: `$base > 0` rather than `!= 0`, which is +// true for `0rem`. `var()` is substituted where the step is declared, which +// is the root and every `.theme`: a new `--rounded` rescales the steps on +// those elements only. +@function -radius-scale($map, $fixed) { + $base: map.get($map, rounded); + $scale: (); + + @each $key, $value in $map { + @if $key != rounded and + not list.index($fixed, $key) and + meta.type-of($base) == 'number' and + meta.type-of($value) == 'number' and + math.unit($base) == math.unit($value) and + $base > 0 + { + $ratio: math.div($value, $base); + $value: string.unquote('calc(var(--rounded) * #{$ratio})'); + } + + $scale: map.set($scale, $key, $value); + } + + @return $scale; +} + @include layer(props, $use-css-layers, $layer-prefix) { :where(:host, :root, .theme) { - @include spread-map-into-props($border-radius, $prefix: rounded); + @include spread-map-into-props( + -radius-scale($border-radius, $border-radius-fixed), + $prefix: rounded + ); @include spread-map-into-props($border-width, $prefix: border); } } diff --git a/src/settings/_borders.scss b/src/settings/_borders.scss index b8b71dc..77ea026 100644 --- a/src/settings/_borders.scss +++ b/src/settings/_borders.scss @@ -11,6 +11,12 @@ $border-radius: ( full: 9999px, ) !default; +// The other steps of the scale are declared as a multiple of `--rounded`, +// with the ratio they have in `$border-radius`, so redeclaring that one token +// rescales every corner of the library. The steps listed here keep the value +// of the map: no radius at all and a pill are not sizes. +$border-radius-fixed: (none, full) !default; + // border-width $border-width: ( 0: 0px, From bd035e23f8985911edd6352e6ddb9d311f6d0bfd Mon Sep 17 00:00:00 2001 From: Alessandro Bellesia Date: Mon, 12 Oct 2026 15:53:41 +0200 Subject: [PATCH 07/12] docs: llms.txt and llms-full.txt, and a sitemap that keeps /style The styleguide build writes llms.txt, an index of the agent skill and of every page with its description, and llms-full.txt, the skill and its four references in one file, and publishes the skill next to them. The menu and the script read the sections from docs/settings/menuSections.js. The sitemap listed every page without the `/style` of the site, because its routes are absolute and replaced the path of the hostname: `basePath: 'style'` puts it back. Co-Authored-By: Claude Opus 5.5 --- .eslintrc-auto-import.json | 2 + CHANGELOG.md | 2 + README.md | 2 + auto-imports.d.ts | 4 + docs/settings/menuSections.js | 23 ++++ docs/settings/navigation.js | 70 ++----------- scripts/generateLlms.js | 192 ++++++++++++++++++++++++++++++++++ vite.config.ts | 8 +- 8 files changed, 242 insertions(+), 61 deletions(-) create mode 100644 docs/settings/menuSections.js create mode 100644 scripts/generateLlms.js diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json index 0d1db51..e1c5c8a 100644 --- a/.eslintrc-auto-import.json +++ b/.eslintrc-auto-import.json @@ -20,6 +20,7 @@ "WritableComputedRef": true, "asyncComputed": true, "autoResetRef": true, + "componentSections": true, "computed": true, "computedAsync": true, "computedEager": true, @@ -301,6 +302,7 @@ "useWindowScroll": true, "useWindowSize": true, "utilityColors": true, + "utilityGroups": true, "watch": true, "watchArray": true, "watchAtMost": true, diff --git a/CHANGELOG.md b/CHANGELOG.md index 148c00a..197d571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. * Container queries. The `@container` class sets `container-type: inline-size`, and the utilities that lay a block out take a container prefix, the name of a step after an `@`: `@md:flex-row` applies when the nearest `@container` above the element is at least 28rem wide, and never when there is none. The steps are the ones of Tailwind CSS, `@xs:` 20rem, `@sm:` 24rem, `@md:` 28rem, `@lg:` 32rem, `@xl:` 36rem, `@xxl:` 42rem and `@xxxl:` 48rem, set by `$container-breakpoints`, and they reach display, flex direction and wrap, the justify and align utilities of content and items, `grid-cols-`, `col-` and text align: 70 classes per step, 2 KB of `volver.css` once gzipped, where giving them to every responsive utility would have added about 570 KB before compression. A container class is emitted after the responsive classes of its utility, so it wins when both apply. `spread-map-into-utilities` takes the steps in a `$cqs` argument, `$container-breakpoints: ()` emits none, and `cq-up`, a shorthand for `container-breakpoint-up`, wraps any rule in the container query of a step; an unknown step, or one of 0, which would emit the base classes a second time after the responsive ones, stops the compilation. Containing the inline size stops an element from taking its width from its content, so `@container` belongs on a block that fills its parent, a card or a column, and collapses an `inline-flex` or a flex item with no basis to nothing. For the same reason no component turns itself into a container. * Open and close transitions on the native elements, with no script. The content of `vv-accordion` grows from 0 to its height and back: its `::details-content` transitions `block-size` to `auto` through `interpolate-size`, and `content-visibility` with `allow-discrete`, so the content stays painted until the closing ends. A browser without them, Safari and Firefox today, opens it at once as before. A `vv-popover` with `popover` fades in from a `@starting-style` and fades out with `display` and `overlay` held by `allow-discrete` until the fade ends, where the browser holds them: Safari 17.6 fades it in and hides it at once, as before. One shown without `popover` does not fade. The closed panel takes its opacity of 0 from `[popover]:not(:popover-open)`, which a browser without the Popover API (Safari 16) drops: there the element is shown as it is, and an opacity on `[popover]` alone would have left it invisible and still in the way of the pointer. A `vv-dialog` opened with `showModal()` fades in and closes at once. A fade out would need `allow-discrete` on the closed dialog, and `@volverjs/ui-vue` hides its dialog with an inline `display: none` and closes it in the same task: the dialog would stay on the page for 300ms after every close, invisible and in the way of the pointer. The Vue transitions of the dialog run as before. Every duration reads the duration tokens, which are 0s under reduced motion. * `details-content` is a pseudo-element a component map can style. It was missing from the list the generator checks, which dropped the rule and still declared its custom properties, and it gets no `content: ''` of its own: it wraps content of the page instead of generating a box. +* `llms.txt` and `llms-full.txt` on the documentation site, in the format of llmstxt.org. The index links the agent skill and every page of the styleguide with its description; the full file is the skill, the overview and its four references, which is written for a model and checked against the compiled CSS, while the pages are built from live examples that read as noise once flattened. The skill is published next to them, under `skills/volverjs-style/`, so the links between its files keep working. ### Changed @@ -30,6 +31,7 @@ All notable changes to this project will be documented in this file. * A nested map in `spread-map-into-utilities` was walked with every breakpoint under the prefix of the current one: at the first, of 0, it emitted the prefixed classes with no media query, and it emitted every rule of the nested map once per breakpoint. The recursion now takes the current breakpoint alone. No map of the library reaches the branch since the sizing classes stopped walking the dynamic spacing map, above, so `dist/` does not move; before that the sizing classes gave 630 classes such as `.xs:w-sm` that applied at every width and 4,410 identical copies of the others, and an application that spreads a nested map of its own with breakpoints still did. * A `transition` declared by a state or a modifier of a block that has Vue transitions shares its name with the `transition` map of the block, which the generator took for a declaration: it reassigned a `--vv-*-transition` that no rule reads, and the transition never applied. A map in the block is no longer taken for a declaration. No component reached the branch before the fade in of `vv-dialog`. +* The sitemap of the documentation listed every page without the `/style` of the site, `https://volverjs.github.io/components/accordion` instead of `https://volverjs.github.io/style/components/accordion`: the routes are absolute, so they replaced the path of the hostname. The sitemap is now generated with `basePath: 'style'`, and `robots.txt` still points to `/style/sitemap.xml`. `design-tokens.json` and the `exports` map do not change. diff --git a/README.md b/README.md index 796e74e..ca926de 100644 --- a/README.md +++ b/README.md @@ -192,5 +192,7 @@ context.$vv-button: map.deep-merge( ## Documentation To learn more about `@volverjs/style`, check [its documentation](https://volverjs.github.io/style). +For coding agents, the documentation publishes [`llms.txt`](https://volverjs.github.io/style/llms.txt), an index of its pages, and [`llms-full.txt`](https://volverjs.github.io/style/llms-full.txt), the whole reference of the [agent skill](skills/volverjs-style) in one file. The skill installs with `npx skills add volverjs/style`. + ## License [MIT](http://opensource.org/licenses/MIT) diff --git a/auto-imports.d.ts b/auto-imports.d.ts index 4496fcd..2089552 100644 --- a/auto-imports.d.ts +++ b/auto-imports.d.ts @@ -11,6 +11,7 @@ declare global { const EffectScope: typeof import('vue').EffectScope const asyncComputed: typeof import('@vueuse/core').asyncComputed const autoResetRef: typeof import('@vueuse/core').autoResetRef + const componentSections: typeof import('./docs/settings/menuSections.js').componentSections const computed: typeof import('vue').computed const computedAsync: typeof import('@vueuse/core').computedAsync const computedEager: typeof import('@vueuse/core').computedEager @@ -295,6 +296,7 @@ declare global { const useWindowScroll: typeof import('@vueuse/core').useWindowScroll const useWindowSize: typeof import('@vueuse/core').useWindowSize const utilityColors: typeof import('./docs/utils/utilityColors.js').utilityColors + const utilityGroups: typeof import('./docs/settings/menuSections.js').utilityGroups const watch: typeof import('vue').watch const watchArray: typeof import('@vueuse/core').watchArray const watchAtMost: typeof import('@vueuse/core').watchAtMost @@ -327,6 +329,7 @@ declare module 'vue' { readonly EffectScope: UnwrapRef readonly asyncComputed: UnwrapRef readonly autoResetRef: UnwrapRef + readonly componentSections: UnwrapRef readonly computed: UnwrapRef readonly computedAsync: UnwrapRef readonly computedEager: UnwrapRef @@ -608,6 +611,7 @@ declare module 'vue' { readonly useWindowScroll: UnwrapRef readonly useWindowSize: UnwrapRef readonly utilityColors: UnwrapRef + readonly utilityGroups: UnwrapRef readonly watch: UnwrapRef readonly watchArray: UnwrapRef readonly watchAtMost: UnwrapRef diff --git a/docs/settings/menuSections.js b/docs/settings/menuSections.js new file mode 100644 index 0000000..628f07f --- /dev/null +++ b/docs/settings/menuSections.js @@ -0,0 +1,23 @@ +// The sections of the menu, in order. A plain module, so that +// `scripts/generateLlms.js`, which runs outside Vite, lists the same pages. + +// The sections of the component pages: a page names its section in the +// `section` attribute, and a page that names none belongs to Components +export const componentSections = ['Components', 'AI'] + +// The groups of the utility pages, each a folder of `docs/contents/utilities` +export const utilityGroups = [ + { name: 'Layout', group: 'layout' }, + { name: 'Flexbox & Grid', group: 'flex-grid' }, + { name: 'Spacing', group: 'spacing' }, + { name: 'Sizing', group: 'sizing' }, + { name: 'Typography', group: 'typography' }, + { name: 'Backgrounds', group: 'backgrounds' }, + { name: 'Borders', group: 'borders' }, + { name: 'Effects', group: 'effects' }, + { name: 'Filters', group: 'filters' }, + { name: 'Tables', group: 'tables' }, + { name: 'Transitions & Animation', group: 'transitions' }, + { name: 'Transforms', group: 'transforms' }, + { name: 'Interactivity', group: 'interactivity' }, +] diff --git a/docs/settings/navigation.js b/docs/settings/navigation.js index aabf968..82bf10d 100644 --- a/docs/settings/navigation.js +++ b/docs/settings/navigation.js @@ -1,3 +1,5 @@ +import { componentSections, utilityGroups } from './menuSections.js' + const getStarted = import.meta.glob('../contents/get-started/*.md', { eager: true, import: 'attributes', @@ -93,64 +95,12 @@ export const mainMenu = [ }) .sort((a, b) => a.index - b.index), }, - { - name: 'Components', - children: getComponentsSection('Components'), - }, - { - name: 'AI', - children: getComponentsSection('AI'), - }, - { - name: 'Layout', - children: getUtilitiesGroup('layout'), - }, - { - name: 'Flexbox & Grid', - children: getUtilitiesGroup('flex-grid'), - }, - { - name: 'Spacing', - children: getUtilitiesGroup('spacing'), - }, - { - name: 'Sizing', - children: getUtilitiesGroup('sizing'), - }, - { - name: 'Typography', - children: getUtilitiesGroup('typography'), - }, - { - name: 'Backgrounds', - children: getUtilitiesGroup('backgrounds'), - }, - { - name: 'Borders', - children: getUtilitiesGroup('borders'), - }, - { - name: 'Effects', - children: getUtilitiesGroup('effects'), - }, - { - name: 'Filters', - children: getUtilitiesGroup('filters'), - }, - { - name: 'Tables', - children: getUtilitiesGroup('tables'), - }, - { - name: 'Transitions & Animation', - children: getUtilitiesGroup('transitions'), - }, - { - name: 'Transforms', - children: getUtilitiesGroup('transforms'), - }, - { - name: 'Interactivity', - children: getUtilitiesGroup('interactivity'), - }, + ...componentSections.map((name) => ({ + name, + children: getComponentsSection(name), + })), + ...utilityGroups.map(({ name, group }) => ({ + name, + children: getUtilitiesGroup(group), + })), ] diff --git a/scripts/generateLlms.js b/scripts/generateLlms.js new file mode 100644 index 0000000..699b6b5 --- /dev/null +++ b/scripts/generateLlms.js @@ -0,0 +1,192 @@ +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' +import { + componentSections, + utilityGroups, +} from '../docs/settings/menuSections.js' + +// `llms.txt` and `llms-full.txt` for the styleguide, in the format of +// https://llmstxt.org. The index links every page of the documentation and +// the agent skill of the repository. The full file is the skill alone: its +// references are written for a model and checked against the compiled CSS, +// while the pages of the styleguide are built from Vue components (live +// examples, tables filled at runtime) that read as noise once flattened. + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const contents = path.join(root, 'docs/contents') +const skill = path.join(root, 'skills/volverjs-style') +const references = [ + { + file: 'utilities.md', + label: 'Utilities', + description: + 'every utility class, its values and its breakpoint prefixes', + }, + { + file: 'components.md', + label: 'Components', + description: + 'markup, elements, modifiers and states of every component', + }, + { + file: 'tokens.md', + label: 'Tokens', + description: 'custom properties, overrides written in CSS, dark theme', + }, + { + file: 'scss.md', + label: 'SCSS', + description: + 'import paths, configuration, extending a component map, mixins, layers', + }, +] + +const frontMatter = /^---\n([\s\S]*?)\n---\n?/ + +// The front matter of a page: flat `key: value` lines, quotes stripped +function readAttributes(file) { + const attributes = {} + const [, block = ''] = frontMatter.exec(fs.readFileSync(file, 'utf8')) ?? [] + for (const line of block.split('\n')) { + const [, key, value] = /^(\w+):\s*(.*)$/.exec(line) ?? [] + if (key) { + attributes[key] = value + .replace(/^'(.*)'$/, (_, text) => text.replaceAll("''", "'")) + .replace(/^"(.*)"$/, '$1') + } + } + return attributes +} + +// The pages of a folder of `docs/contents`, drafts left out, by name and then +// by their `index`: `readdirSync` keeps the order of the file system, which +// is not alphabetical everywhere. A page is a markdown file, or a folder with +// an `index.md` for a component: the other folders hold the examples a page +// embeds. +function listPages(folder, label) { + return fs + .readdirSync(path.join(contents, folder), { withFileTypes: true }) + .sort((a, b) => a.name.localeCompare(b.name)) + .filter((entry) => + entry.isDirectory() + ? fs.existsSync( + path.join(contents, folder, entry.name, 'index.md'), + ) + : entry.name.endsWith('.md'), + ) + .map((entry) => { + const name = path.basename(entry.name, '.md') + const attributes = readAttributes( + entry.isDirectory() + ? path.join(contents, folder, name, 'index.md') + : path.join(contents, folder, entry.name), + ) + return { + attributes, + label: attributes[label], + route: `${folder}/${name}`, + description: attributes.description, + } + }) + .filter(({ attributes }) => attributes.isDraft !== 'true') + .sort((a, b) => (a.attributes.index ?? 0) - (b.attributes.index ?? 0)) +} + +function link(hostname, { label, route, description }) { + return `- [${label}](${hostname}/${route})${description ? `: ${description}` : ''}` +} + +function linkSection(hostname, title, pages) { + return ['', `## ${title}`, '', ...pages.map((page) => link(hostname, page))] +} + +export function generateLlms({ hostname, outDir }) { + const components = listPages('components', 'title') + const section = (name) => + components.filter( + ({ attributes }) => (attributes.section ?? 'Components') === name, + ) + + const skillSource = fs + .readFileSync(path.join(skill, 'SKILL.md'), 'utf8') + .replace(frontMatter, '') + .trim() + const summary = skillSource + .split('\n\n') + .find((paragraph) => !paragraph.startsWith('#')) + .replaceAll('\n', ' ') + + const index = [ + '# @volverjs/style', + '', + `> ${summary}`, + '', + 'The vocabulary looks like Tailwind and differs from it in many places, and a class that does not exist fails silently. Read the reference before writing a class name: it is checked against the compiled CSS, while the pages of the styleguide are built around live examples.', + ...linkSection(hostname, 'Reference', [ + { + label: 'Overview', + route: 'skills/volverjs-style/SKILL.md', + description: + 'workflow, where the vocabulary differs from Tailwind, layout recipes, the essentials of components and tokens', + }, + ...references.map(({ file, label, description }) => ({ + label, + route: `skills/volverjs-style/references/${file}`, + description, + })), + ]), + ...linkSection( + hostname, + 'Get started', + listPages('get-started', 'label'), + ), + ...linkSection(hostname, 'Settings', listPages('settings', 'label')), + ...componentSections.flatMap((name) => + linkSection(hostname, name, section(name)), + ), + ...utilityGroups.flatMap(({ name, group }) => + linkSection( + hostname, + `Utilities: ${name}`, + listPages(`utilities/${group}`, 'title'), + ), + ), + ...linkSection(hostname, 'Optional', [ + { + label: 'Full reference', + route: 'llms-full.txt', + description: 'the overview and the four references in one file', + }, + ]), + ] + + const full = [ + skillSource, + ...references.map(({ file }) => + fs + .readFileSync(path.join(skill, 'references', file), 'utf8') + .trim(), + ), + ] + + // The skill is published next to the index, so the relative links between + // its files keep working + const skillOut = path.join(outDir, 'skills/volverjs-style') + fs.mkdirSync(path.join(skillOut, 'references'), { recursive: true }) + fs.copyFileSync( + path.join(skill, 'SKILL.md'), + path.join(skillOut, 'SKILL.md'), + ) + for (const { file } of references) { + fs.copyFileSync( + path.join(skill, 'references', file), + path.join(skillOut, 'references', file), + ) + } + fs.writeFileSync(path.join(outDir, 'llms.txt'), `${index.join('\n')}\n`) + fs.writeFileSync( + path.join(outDir, 'llms-full.txt'), + `${full.join('\n\n---\n\n')}\n`, + ) +} diff --git a/vite.config.ts b/vite.config.ts index 842ecd1..76d0033 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -15,8 +15,10 @@ import Components from 'unplugin-vue-components/vite' import generateSitemap from 'vite-ssg-sitemap' import packageJson from './package.json' with { type: 'json' } import { fileURLToPath } from 'url' +import { generateLlms } from './scripts/generateLlms.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const hostname = 'https://volverjs.github.io/style' const markdownIt = MarkdownIt({ html: true, linkify: true }) .use(MarkdownItPrism) @@ -103,10 +105,14 @@ export default defineConfig(({ mode }) => ({ script: 'async', formatting: 'minify', onFinished() { + // The routes are absolute, so without `basePath` the URL of every + // page lost the `/style` of the hostname generateSitemap({ - hostname: 'https://volverjs.github.io/style', + hostname, + basePath: 'style', outDir: 'styleguide', }) + generateLlms({ hostname, outDir: 'styleguide' }) }, }, define: { From 19b8255fc30da9e697472b153aaf5d53dc3d97b9 Mon Sep 17 00:00:00 2001 From: Alessandro Bellesia Date: Mon, 12 Oct 2026 16:37:34 +0200 Subject: [PATCH 08/12] refactor(scripts): node: imports, and the llms script without the Sonar issues SonarCloud flagged five issues on the llms script of this branch: three built-in modules imported without the `node:` prefix, a front matter regex whose `\s*(.*)` overlaps on whitespace, and a nested template literal. The regex now captures everything after the colon and trims it, which also drops trailing whitespace as YAML does, and `link()` builds the item before appending its description. build.js, generateDesignTokens.js and vite.config.ts import their built-ins with `node:` too, as checkColors.js already did. llms.txt, llms-full.txt, dist/ and design-tokens.json are identical before and after. Co-Authored-By: Claude Opus 5.5 --- scripts/build.js | 2 +- scripts/generateDesignTokens.js | 2 +- scripts/generateLlms.js | 12 +++++++----- vite.config.ts | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/scripts/build.js b/scripts/build.js index d29f212..9394de2 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -1,4 +1,4 @@ -import fs from 'fs' +import fs from 'node:fs' import { glob } from 'glob' import { build } from 'vite' import stylelint from 'vite-plugin-stylelint' diff --git a/scripts/generateDesignTokens.js b/scripts/generateDesignTokens.js index 8fc2572..ef99cc2 100644 --- a/scripts/generateDesignTokens.js +++ b/scripts/generateDesignTokens.js @@ -1,5 +1,5 @@ import { extractICSS } from 'icss-utils' -import fs from 'fs' +import fs from 'node:fs' import postcss from 'postcss' import { compile } from 'sass-embedded' import calc from 'reduce-css-calc' diff --git a/scripts/generateLlms.js b/scripts/generateLlms.js index 699b6b5..6beeb26 100644 --- a/scripts/generateLlms.js +++ b/scripts/generateLlms.js @@ -1,6 +1,6 @@ -import fs from 'fs' -import path from 'path' -import { fileURLToPath } from 'url' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' import { componentSections, utilityGroups, @@ -49,9 +49,10 @@ function readAttributes(file) { const attributes = {} const [, block = ''] = frontMatter.exec(fs.readFileSync(file, 'utf8')) ?? [] for (const line of block.split('\n')) { - const [, key, value] = /^(\w+):\s*(.*)$/.exec(line) ?? [] + const [, key, value] = /^(\w+):(.*)$/.exec(line) ?? [] if (key) { attributes[key] = value + .trim() .replace(/^'(.*)'$/, (_, text) => text.replaceAll("''", "'")) .replace(/^"(.*)"$/, '$1') } @@ -94,7 +95,8 @@ function listPages(folder, label) { } function link(hostname, { label, route, description }) { - return `- [${label}](${hostname}/${route})${description ? `: ${description}` : ''}` + const item = `- [${label}](${hostname}/${route})` + return description ? `${item}: ${description}` : item } function linkSection(hostname, title, pages) { diff --git a/vite.config.ts b/vite.config.ts index 76d0033..f565ed0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,4 @@ -import path from 'path' +import path from 'node:path' import { defineConfig } from 'vite' import Vue from '@vitejs/plugin-vue' import CSSExports from 'vite-plugin-css-export' @@ -14,7 +14,7 @@ import Pages from 'vite-plugin-pages' import Components from 'unplugin-vue-components/vite' import generateSitemap from 'vite-ssg-sitemap' import packageJson from './package.json' with { type: 'json' } -import { fileURLToPath } from 'url' +import { fileURLToPath } from 'node:url' import { generateLlms } from './scripts/generateLlms.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) From acfe18082c9808a92a482274ec99ace0f241ad80 Mon Sep 17 00:00:00 2001 From: Alessandro Bellesia Date: Mon, 12 Oct 2026 16:56:06 +0200 Subject: [PATCH 09/12] docs(utilities): the callout of a utility page shows container queries on their own The callout of a utility page sat behind `breakpoints` and showed the container prefix only inside it, while the badges read the two flags separately: a page with `containerQueries` alone would have shown the badge and no callout. Every page with container queries also has breakpoints today, so the site does not change. The callout now renders for either flag, keeps the breakpoint text under `breakpoints`, and with container queries alone takes their title and links to their section. Reported by CodeRabbit on #128. Co-Authored-By: Claude Opus 5.5 --- docs/pages/utilities/[group]/[name].vue | 35 +++++++++++++++++-------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/docs/pages/utilities/[group]/[name].vue b/docs/pages/utilities/[group]/[name].vue index 2990851..3106499 100644 --- a/docs/pages/utilities/[group]/[name].vue +++ b/docs/pages/utilities/[group]/[name].vue @@ -109,22 +109,33 @@