@getvitops/generator 6.0.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +432 -22
- package/assets/css/animation.css +9 -0
- package/assets/css/global.css +28 -0
- package/assets/css/index.css +4 -0
- package/assets/css/patterns/counter.css +50 -0
- package/assets/css/patterns/forms.css +1 -1
- package/assets/css/patterns/gallery.css +64 -0
- package/assets/css/patterns/lightbox.css +97 -98
- package/assets/css/patterns/list.css +1 -1
- package/assets/css/patterns/review.css +91 -0
- package/assets/css/patterns/tag.css +1 -2
- package/assets/css/patterns/tree.css +111 -8
- package/assets/css/utilities.css +17 -0
- package/assets/js/elements.js +7 -844
- package/assets/js/polyfills/invokers-polyfill.js +1 -0
- package/assets/js/polyfills/template-shadowroot.js +1 -14
- package/assets/js/polyfills.js +1 -1
- package/config.schema.json +169 -1
- package/dist/index.d.mts +138 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +218 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -4
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gallery — thumbnail grid whose triggers open a `.lightbox-dialog` (patterns/lightbox.css).
|
|
3
|
+
* Consumes the generated token cascade (src/css/generated/tokens.css).
|
|
4
|
+
*
|
|
5
|
+
* Split out of what used to be `.lightbox`'s grid half. The two are separate
|
|
6
|
+
* concerns — "a grid of images" and "view one full-screen" — and only the dialog
|
|
7
|
+
* half needs the `lightbox` name; the grid never was one.
|
|
8
|
+
*
|
|
9
|
+
* `<wc-gallery>` wraps this and the single `.lightbox-dialog` it opens, running the
|
|
10
|
+
* thumbnail → full-image view-transition morph; stepping between images is a
|
|
11
|
+
* `<wc-carousel>` scroll inside that dialog, not a second dialog. With no JS each
|
|
12
|
+
* `.gallery__trigger` is a real `<a href="#slide-id">` into the carousel's own
|
|
13
|
+
* `<li id="slide-id">`, and `:has(:target)` in lightbox.css is what reveals the
|
|
14
|
+
* dialog — so the grid + dialog already form a working modal lightbox, with
|
|
15
|
+
* prev/next, before any script runs.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
wc-gallery {
|
|
19
|
+
/* Unregistered custom elements default to `display: inline`, which forms an
|
|
20
|
+
inline box around this grid's block content until `elements.js` upgrades it. */
|
|
21
|
+
display: block;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.gallery {
|
|
25
|
+
--_br: var(--br-gallery, var(--br-gallery-group, var(--br-default)));
|
|
26
|
+
|
|
27
|
+
display: grid;
|
|
28
|
+
grid-template-columns: repeat(auto-fill, minmax(var(--gallery-thumb-size, 150px), 1fr));
|
|
29
|
+
gap: var(--gallery-gap, 0.5rem);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.gallery__item {
|
|
33
|
+
aspect-ratio: var(--gallery-aspect, 1);
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
border-radius: var(--_br);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.gallery__trigger {
|
|
39
|
+
display: block;
|
|
40
|
+
inline-size: 100%;
|
|
41
|
+
block-size: 100%;
|
|
42
|
+
margin: 0;
|
|
43
|
+
padding: 0;
|
|
44
|
+
background: none;
|
|
45
|
+
border: none;
|
|
46
|
+
text-decoration: none;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
|
|
49
|
+
&:focus-visible {
|
|
50
|
+
outline: 2px solid var(--gallery-focus-color, currentColor);
|
|
51
|
+
outline-offset: 2px;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.gallery__thumb {
|
|
56
|
+
inline-size: 100%;
|
|
57
|
+
block-size: 100%;
|
|
58
|
+
object-fit: cover;
|
|
59
|
+
transition: scale 0.3s ease;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.gallery__trigger:hover .gallery__thumb {
|
|
63
|
+
scale: 1.05;
|
|
64
|
+
}
|
|
@@ -1,42 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Lightbox —
|
|
2
|
+
* Lightbox — the full-screen `<dialog>` a gallery trigger opens.
|
|
3
3
|
* Consumes the generated token cascade (src/css/generated/tokens.css).
|
|
4
|
+
*
|
|
5
|
+
* The thumbnail grid that drives this lives in `patterns/gallery.css`
|
|
6
|
+
* (`.gallery` / `.gallery__item` / `.gallery__trigger` / `.gallery__thumb`) — this
|
|
7
|
+
* partial owns only the dialog a trigger opens. `<Gallery />` emits both; this
|
|
8
|
+
* file has no opinion on what triggers it.
|
|
9
|
+
*
|
|
10
|
+
* **One dialog, one carousel** — not one dialog per image. Stepping between
|
|
11
|
+
* images is a scroll inside an already-open dialog (`.carousel`, patterns/
|
|
12
|
+
* carousel.css), so there is no close-one/open-another sequence for the top-layer
|
|
13
|
+
* driver in animation.css to double-animate. `<wc-gallery>` only opens/closes;
|
|
14
|
+
* `<wc-carousel>` (nested inside, `<Gallery />` emits both tags) owns prev/next,
|
|
15
|
+
* dots, arrow keys and swipe — reused rather than re-implemented.
|
|
4
16
|
*/
|
|
5
17
|
|
|
6
|
-
/* ── Lightbox —
|
|
7
|
-
.lightbox {
|
|
8
|
-
--_br: var(--br-lightbox, var(--br-lightbox-group, var(--br-default)));
|
|
9
|
-
|
|
10
|
-
display: grid;
|
|
11
|
-
grid-template-columns: repeat(auto-fill, minmax(var(--lightbox-thumb-size, 150px), 1fr));
|
|
12
|
-
gap: var(--lightbox-gap, 0.5rem);
|
|
13
|
-
|
|
14
|
-
.lightbox__item {
|
|
15
|
-
position: relative;
|
|
16
|
-
aspect-ratio: var(--lightbox-aspect, 1);
|
|
17
|
-
overflow: hidden;
|
|
18
|
-
border-radius: var(--_br);
|
|
19
|
-
cursor: pointer;
|
|
20
|
-
|
|
21
|
-
&:focus-visible {
|
|
22
|
-
outline: 2px solid var(--lightbox-focus-color, currentColor);
|
|
23
|
-
outline-offset: 2px;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.lightbox__thumb {
|
|
28
|
-
inline-size: 100%;
|
|
29
|
-
block-size: 100%;
|
|
30
|
-
object-fit: cover;
|
|
31
|
-
transition: scale 0.3s ease;
|
|
32
|
-
view-transition-name: match-element;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.lightbox__item:hover .lightbox__thumb {
|
|
36
|
-
scale: 1.05;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
18
|
+
/* ── Lightbox dialog — full-screen, w/ view transitions ─────────────────────── */
|
|
40
19
|
dialog.lightbox-dialog {
|
|
41
20
|
position: fixed;
|
|
42
21
|
inset: 0;
|
|
@@ -50,30 +29,98 @@ dialog.lightbox-dialog {
|
|
|
50
29
|
background: var(--lightbox-dialog-bg, oklch(0 0 0 / 0.92));
|
|
51
30
|
color: #fff;
|
|
52
31
|
display: grid;
|
|
53
|
-
place-items:
|
|
32
|
+
place-items: stretch;
|
|
54
33
|
|
|
55
34
|
/* No scrim. Stated through popover.css's token rather than overriding its
|
|
56
35
|
::backdrop rule, so it keeps the shared fade timing. */
|
|
57
36
|
--scrim: transparent;
|
|
58
37
|
|
|
38
|
+
/* The entry, as effect vars rather than a keyframe — same approach as
|
|
39
|
+
dialog.css, and for the same reason: the top-layer driver in animation.css
|
|
40
|
+
then animates this OUT too, which an `animation:` on `[open]` never does
|
|
41
|
+
(it only ever plays once, on open). Runs on real open/close ONLY — stepping
|
|
42
|
+
between slides never opens or closes this dialog, so it can never fire here. */
|
|
43
|
+
--opacity-from: 0;
|
|
44
|
+
--scale-x-from: 0.95;
|
|
45
|
+
--scale-y-from: 0.95;
|
|
46
|
+
|
|
59
47
|
&[open] {
|
|
60
48
|
display: grid;
|
|
61
49
|
}
|
|
62
50
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
51
|
+
/* ── No-JS fallback: `:target` + `:has()` ──────────────────────────────────
|
|
52
|
+
A `.gallery__trigger` is a real `<a href="#slide-id">`. Following it makes
|
|
53
|
+
that slide's `<li>` match `:target`, which this rule alone is what reveals
|
|
54
|
+
the dialog — no Invoker Commands, no polyfill, in every engine that has
|
|
55
|
+
`:has()` (Safari 15.4+, Chromium 105+, Firefox 121+). The browser's own
|
|
56
|
+
fragment-navigation scroll then lands the carousel on the right slide,
|
|
57
|
+
because the target lives inside `.carousel__track`.
|
|
58
|
+
|
|
59
|
+
`:not([open])` is load-bearing: once `<wc-gallery>` opens this for real via
|
|
60
|
+
`showModal()`, `[open]` is present and this rule must yield to it — a stale
|
|
61
|
+
`:target` from a previous no-JS visit must not fight the real modal, and
|
|
62
|
+
must not keep the dialog "open" by this rule alone once JS is stepping
|
|
63
|
+
between slides (which never changes the URL fragment). */
|
|
64
|
+
&:not([open]):has(:target) {
|
|
65
|
+
display: grid;
|
|
66
|
+
position: fixed;
|
|
67
|
+
inset: 0;
|
|
68
|
+
z-index: var(--lightbox-z, 100);
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
.lightbox-
|
|
71
|
+
.lightbox-dialog__track {
|
|
73
72
|
max-inline-size: 100%;
|
|
74
|
-
max-block-size:
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
max-block-size: 100%;
|
|
74
|
+
|
|
75
|
+
/* `.carousel__media`'s own rule reads this for its `object-fit` (default
|
|
76
|
+
`cover`, right for a thumbnail strip, wrong for a full image — nothing
|
|
77
|
+
here should be cropped). Nesting under `dialog.lightbox-dialog` gives
|
|
78
|
+
this whole block higher specificity than carousel.css's bare
|
|
79
|
+
`.carousel__media` rule regardless of import order, so it always wins. */
|
|
80
|
+
--carousel-media-fit: contain;
|
|
81
|
+
|
|
82
|
+
.carousel__track {
|
|
83
|
+
max-block-size: 100%;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Breathing room against the edge-to-edge dialog, on the SLIDE rather than
|
|
87
|
+
the media box — a slide is the grid item `align-items: stretch` fills to
|
|
88
|
+
the track's full height, so padding here is what keeps the image off the
|
|
89
|
+
glass instead of the media box's aspect-ratio doing it. */
|
|
90
|
+
.carousel__slide {
|
|
91
|
+
box-sizing: border-box;
|
|
92
|
+
padding: 5vh 5vw;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.carousel__media {
|
|
96
|
+
/* Full images vary wildly in ratio; let each fill its slide instead of
|
|
97
|
+
carousel.css's fixed 16/9 box (right for a thumbnail strip, wrong
|
|
98
|
+
here). */
|
|
99
|
+
aspect-ratio: unset;
|
|
100
|
+
block-size: 100%;
|
|
101
|
+
|
|
102
|
+
/* Painted the instant a slide is visible, whether or not the full image
|
|
103
|
+
has loaded yet — the morph target is never an empty box. Set per-slide
|
|
104
|
+
by `<Gallery />` from the thumbnail URL; declared as a var rather than
|
|
105
|
+
a bare property so a slide that omits a thumb (no placeholder) simply
|
|
106
|
+
paints nothing extra. */
|
|
107
|
+
background-image: var(--lightbox-placeholder, none);
|
|
108
|
+
background-size: contain;
|
|
109
|
+
background-position: center;
|
|
110
|
+
background-repeat: no-repeat;
|
|
111
|
+
|
|
112
|
+
/* `<wc-gallery>` assigns `view-transition-name: lightbox-active` to the
|
|
113
|
+
slide's `<img>` and the originating thumbnail at the moment it opens,
|
|
114
|
+
then clears it on close — that shared, runtime-assigned name is what
|
|
115
|
+
morphs one into the other. `match-element` (the auto-generated
|
|
116
|
+
per-element name) does NOT do this: it names each element uniquely,
|
|
117
|
+
so it can never morph a thumbnail into a different `<img>`. Do not put
|
|
118
|
+
a `view-transition-name` on the image here statically. */
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.carousel__caption {
|
|
122
|
+
color: inherit;
|
|
123
|
+
}
|
|
77
124
|
}
|
|
78
125
|
|
|
79
126
|
.lightbox-dialog__close {
|
|
@@ -91,6 +138,9 @@ dialog.lightbox-dialog {
|
|
|
91
138
|
display: flex;
|
|
92
139
|
align-items: center;
|
|
93
140
|
justify-content: center;
|
|
141
|
+
text-decoration: none;
|
|
142
|
+
font-size: 1.5rem;
|
|
143
|
+
line-height: 1;
|
|
94
144
|
transition: background-color 0.15s ease;
|
|
95
145
|
z-index: 10;
|
|
96
146
|
|
|
@@ -102,55 +152,4 @@ dialog.lightbox-dialog {
|
|
|
102
152
|
outline-offset: 2px;
|
|
103
153
|
}
|
|
104
154
|
}
|
|
105
|
-
|
|
106
|
-
.lightbox-dialog__nav {
|
|
107
|
-
position: absolute;
|
|
108
|
-
inset-block: 0;
|
|
109
|
-
display: flex;
|
|
110
|
-
align-items: center;
|
|
111
|
-
padding: 1rem;
|
|
112
|
-
|
|
113
|
-
&[data-direction='prev'] {
|
|
114
|
-
inset-inline-start: 0;
|
|
115
|
-
}
|
|
116
|
-
&[data-direction='next'] {
|
|
117
|
-
inset-inline-end: 0;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
.lightbox-dialog__nav-button {
|
|
122
|
-
inline-size: 3rem;
|
|
123
|
-
block-size: 3rem;
|
|
124
|
-
padding: 0;
|
|
125
|
-
background: var(--lightbox-nav-bg, transparent);
|
|
126
|
-
border: none;
|
|
127
|
-
border-radius: 50%;
|
|
128
|
-
color: inherit;
|
|
129
|
-
cursor: pointer;
|
|
130
|
-
display: flex;
|
|
131
|
-
align-items: center;
|
|
132
|
-
justify-content: center;
|
|
133
|
-
transition: background-color 0.15s ease;
|
|
134
|
-
|
|
135
|
-
&:hover {
|
|
136
|
-
background: var(--lightbox-nav-hover-bg, oklch(1 0 0 / 0.15));
|
|
137
|
-
}
|
|
138
|
-
&:disabled {
|
|
139
|
-
opacity: 0.3;
|
|
140
|
-
cursor: not-allowed;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
.lightbox-dialog__caption {
|
|
145
|
-
position: absolute;
|
|
146
|
-
inset-block-end: 0;
|
|
147
|
-
inset-inline: 0;
|
|
148
|
-
padding: 1rem 2rem;
|
|
149
|
-
text-align: center;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
::view-transition-old(*),
|
|
154
|
-
::view-transition-new(*) {
|
|
155
|
-
animation-duration: 0.3s;
|
|
156
155
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review — the `.review` card's structure only.
|
|
3
|
+
*
|
|
4
|
+
* Box chrome (border/padding/background/shadow) is CONFIG-GENERATED
|
|
5
|
+
* (`patterns.items.review` in design-system.json), the same split `card.css`
|
|
6
|
+
* documents. This partial holds what the framework owns: the internal layout —
|
|
7
|
+
* rating, identity, body, reply, source link.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
.review {
|
|
11
|
+
.review__header {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-wrap: wrap;
|
|
14
|
+
align-items: center;
|
|
15
|
+
gap: 0.75rem;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.review__avatar {
|
|
19
|
+
inline-size: 2.5rem;
|
|
20
|
+
block-size: 2.5rem;
|
|
21
|
+
border-radius: 50%;
|
|
22
|
+
object-fit: cover;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Shown instead of `.review__avatar` when `avatars` is off (the default) — see
|
|
26
|
+
the config-generated pattern's rationale in AGENTS.md's Reviews section. */
|
|
27
|
+
.review__initials {
|
|
28
|
+
display: inline-flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
inline-size: 2.5rem;
|
|
32
|
+
block-size: 2.5rem;
|
|
33
|
+
border-radius: 50%;
|
|
34
|
+
background: var(--color-bg-neutral-muted);
|
|
35
|
+
color: var(--color-text-neutral);
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
font-size: 0.875em;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.review__identity {
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
gap: 0.125rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.review__author {
|
|
47
|
+
font-weight: 600;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.review__date {
|
|
51
|
+
font-size: 0.875em;
|
|
52
|
+
opacity: 0.7;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.review__stars {
|
|
56
|
+
display: inline-flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
gap: 0.125rem;
|
|
59
|
+
margin-inline-start: auto;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.review__star--empty {
|
|
63
|
+
opacity: 0.35;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.review__body {
|
|
67
|
+
margin: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.review__reply {
|
|
71
|
+
margin-inline-start: 1.5rem;
|
|
72
|
+
padding-inline-start: 1rem;
|
|
73
|
+
border-inline-start: 2px solid var(--color-bg-surface-muted);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.review__reply-author {
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
font-size: 0.875em;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.review__source {
|
|
82
|
+
font-size: 0.875em;
|
|
83
|
+
align-self: start;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.review-list {
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
gap: 1.5rem;
|
|
91
|
+
}
|
|
@@ -29,7 +29,6 @@
|
|
|
29
29
|
/* ── Badge indicator — numeric notification bubble (distinct from .badge) ──── */
|
|
30
30
|
.badge-indicator {
|
|
31
31
|
--_size: var(--badge-size, 1.25rem);
|
|
32
|
-
--_br: var(--br-badge, var(--br-badge-group, var(--br-default)));
|
|
33
32
|
|
|
34
33
|
display: inline-flex;
|
|
35
34
|
align-items: center;
|
|
@@ -40,7 +39,7 @@
|
|
|
40
39
|
font-size: var(--fs-badge, var(--fs-badge-group, var(--fs-default)));
|
|
41
40
|
font-weight: 600;
|
|
42
41
|
line-height: 1;
|
|
43
|
-
border-radius: var(--br-badge-indicator,
|
|
42
|
+
border-radius: var(--br-badge-indicator, 999px);
|
|
44
43
|
background: var(--color-text-danger);
|
|
45
44
|
color: #fff;
|
|
46
45
|
|
|
@@ -44,10 +44,16 @@ wc-tree {
|
|
|
44
44
|
position: relative;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/* Nested list indent. Two shapes are supported
|
|
48
|
-
`<details>`, or be an `<li>` wrapping one — the latter is
|
|
49
|
-
emits, because `<ul>`/`<li>` is what gives assistive tech
|
|
50
|
-
|
|
47
|
+
/* Nested list indent. Two shapes are supported IN THE NO-JS FALLBACK: the
|
|
48
|
+
item may BE the `<details>`, or be an `<li>` wrapping one — the latter is
|
|
49
|
+
what `<Tree />` emits, because `<ul>`/`<li>` is what gives assistive tech
|
|
50
|
+
list position and depth. `<wc-tree>` unwraps both shapes to one structure
|
|
51
|
+
on upgrade (`.tree__item` > `.tree__summary`/`.tree__content` + optional
|
|
52
|
+
nested `.tree`), which is why the first selector below already matches
|
|
53
|
+
the ENHANCED state for both former shapes with no changes — only the
|
|
54
|
+
second (`.tree__item > details > .tree`) is fallback-only now, kept for
|
|
55
|
+
the no-JS list shape. Same split applies to every other
|
|
56
|
+
`details`/`summary`-keyed selector in this file below.
|
|
51
57
|
|
|
52
58
|
Indent is ONE `--tree-indent` per level, not two. This used to set both
|
|
53
59
|
`margin-inline-start` and `padding-inline-start` to it, putting the guide
|
|
@@ -85,6 +91,7 @@ wc-tree {
|
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
.tree__content,
|
|
94
|
+
.tree__summary,
|
|
88
95
|
details.tree__item > summary,
|
|
89
96
|
.tree__item > details > summary {
|
|
90
97
|
display: flex;
|
|
@@ -100,6 +107,26 @@ wc-tree {
|
|
|
100
107
|
padding-inline-start: var(--_leaf-indent);
|
|
101
108
|
}
|
|
102
109
|
|
|
110
|
+
/* `.tree__summary` — the enhanced-state row `<wc-tree>` builds on upgrade,
|
|
111
|
+
replacing `<summary>`. Not `.tree__content`: that class's
|
|
112
|
+
`padding-inline-start: var(--_leaf-indent)` exists specifically to make a
|
|
113
|
+
TOGGLE-LESS row line up with one that has a toggle — applying it to a row
|
|
114
|
+
that DOES have a toggle would double-indent every branch, the mirror
|
|
115
|
+
image of the 24px-misalignment bug the comment above this block
|
|
116
|
+
documents fixing. */
|
|
117
|
+
.tree__summary {
|
|
118
|
+
cursor: pointer;
|
|
119
|
+
/* Only source of this in the enhanced state — `<summary>`'s own
|
|
120
|
+
`user-select: none` came from `details.css`'s bare `details` selector,
|
|
121
|
+
which no longer matches once `<details>` is gone. Without it, a
|
|
122
|
+
drag-select across a label also fires a `click` and toggles the row. */
|
|
123
|
+
user-select: none;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Fallback-only from here: no-JS renders `<details>`/`<summary>`, which
|
|
127
|
+
`<wc-tree>` unwraps on upgrade (see the class docblock in `WCTree.ts`).
|
|
128
|
+
Kept for the two SSR'd shapes; nothing in the enhanced state matches
|
|
129
|
+
these. */
|
|
103
130
|
details.tree__item > summary,
|
|
104
131
|
.tree__item > details > summary {
|
|
105
132
|
cursor: pointer;
|
|
@@ -127,6 +154,13 @@ wc-tree {
|
|
|
127
154
|
transition: transform 0.2s ease;
|
|
128
155
|
}
|
|
129
156
|
|
|
157
|
+
/* Enhanced state: `aria-expanded` is the mirror `#setExpanded` writes
|
|
158
|
+
alongside the group's `hidden`, kept in sync with it — see WCTree.ts. */
|
|
159
|
+
.tree__item[aria-expanded='true'] > .tree__summary .tree__toggle {
|
|
160
|
+
transform: rotate(90deg);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* Fallback-only — see the note above `details.tree__item > summary`. */
|
|
130
164
|
details.tree__item[open] > summary .tree__toggle,
|
|
131
165
|
.tree__item > details[open] > summary .tree__toggle {
|
|
132
166
|
transform: rotate(90deg);
|
|
@@ -165,8 +199,12 @@ wc-tree {
|
|
|
165
199
|
transition: opacity 0.15s ease;
|
|
166
200
|
}
|
|
167
201
|
|
|
168
|
-
|
|
169
|
-
|
|
202
|
+
/* `>`, not a descendant selector: `.tree__item` now carries the roving
|
|
203
|
+
tabindex directly, so arrow-keying to a deep item would otherwise light
|
|
204
|
+
up every ANCESTOR branch's own actions row too, not just the row hovered
|
|
205
|
+
or focused. */
|
|
206
|
+
.tree__item:hover > .tree__actions,
|
|
207
|
+
.tree__item:focus-within > .tree__actions {
|
|
170
208
|
opacity: 1;
|
|
171
209
|
}
|
|
172
210
|
|
|
@@ -174,6 +212,65 @@ wc-tree {
|
|
|
174
212
|
opacity: 0.5;
|
|
175
213
|
}
|
|
176
214
|
|
|
215
|
+
/* Focus now lands on `.tree__item` itself (the roving tabindex target), a
|
|
216
|
+
block box that — for an expanded branch — wraps its entire subtree. An
|
|
217
|
+
outline on it would ring every descendant, so it's suppressed there and
|
|
218
|
+
drawn on the ROW instead. */
|
|
219
|
+
.tree__item:focus {
|
|
220
|
+
outline: none;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* `:focus-visible` alone under-covers a real case: script-moved focus only
|
|
224
|
+
matches `:focus-visible` if the PREVIOUSLY focused element did, so a
|
|
225
|
+
mouse click (which doesn't match) followed immediately by an arrow key
|
|
226
|
+
shows no ring at all in Chrome/Safari until the visitor tabs fully out
|
|
227
|
+
and back in. `[data-tree-keyboard]` is `<wc-tree>`'s own signal — set on
|
|
228
|
+
any handled navigation keydown, cleared on `pointerdown` — and covers
|
|
229
|
+
exactly that gap; `happy-dom` implements neither `:focus-visible` nor
|
|
230
|
+
layout, so this needs a manual browser check, not a unit test. */
|
|
231
|
+
.tree__item:focus-visible > .tree__summary,
|
|
232
|
+
.tree__item:focus-visible > .tree__content,
|
|
233
|
+
&[data-tree-keyboard] .tree__item:focus > .tree__summary,
|
|
234
|
+
&[data-tree-keyboard] .tree__item:focus > .tree__content {
|
|
235
|
+
outline: 2px solid var(--tree-focus-color, currentColor);
|
|
236
|
+
/* Inset, not the pattern's usual positive offset: the nested group below
|
|
237
|
+
clips its content (`overflow: clip`, next rule), so a positive offset
|
|
238
|
+
on the first or last row of an expanded subtree would be cut off by
|
|
239
|
+
its own ancestor. */
|
|
240
|
+
outline-offset: -2px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* Animated collapse — the enhanced-state replacement for `details.css`'s
|
|
244
|
+
`::details-content` transition, which no longer applies once `<details>`
|
|
245
|
+
is gone. Keyed on `[hidden]` itself, NOT on `[aria-expanded]`: the
|
|
246
|
+
browser's own find-in-page / fragment-navigation reveal removes `hidden`
|
|
247
|
+
directly or CSS anchored to a JS-set `aria-expanded` mirror would leave a
|
|
248
|
+
UA-revealed branch visually still collapsed until a `beforematch` handler
|
|
249
|
+
caught up (see WCTree.ts). Same idiom `details.css` verified working, and
|
|
250
|
+
the same warning applies: the tell of a regression is a branch that
|
|
251
|
+
doesn't open on the FIRST click, not a syntax error — check a real
|
|
252
|
+
interaction. */
|
|
253
|
+
.tree__item > .tree {
|
|
254
|
+
/* `clip` + a small margin, not `hidden`: an `overflow: hidden` group
|
|
255
|
+
would clip the focus ring (`outline-offset: -2px` above still needs a
|
|
256
|
+
few px of headroom) of its own first/last child row. */
|
|
257
|
+
overflow: clip;
|
|
258
|
+
overflow-clip-margin: 3px;
|
|
259
|
+
|
|
260
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
261
|
+
block-size: auto;
|
|
262
|
+
transition:
|
|
263
|
+
block-size var(--animation-duration) var(--custom-ease-out),
|
|
264
|
+
content-visibility var(--animation-duration) allow-discrete;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
269
|
+
.tree__item > .tree[hidden] {
|
|
270
|
+
block-size: 0;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
177
274
|
&.tree--lines .tree__item {
|
|
178
275
|
position: relative;
|
|
179
276
|
|
|
@@ -195,10 +292,16 @@ wc-tree {
|
|
|
195
292
|
|
|
196
293
|
/* A node's own prose. `.tree__label` is `nowrap` + ellipsis for file-tree-style
|
|
197
294
|
single-line names, so a description can't live in it — it sits below the
|
|
198
|
-
summary row and wraps. Indented to the label, past the toggle.
|
|
295
|
+
summary row and wraps. Indented to the label, past the toggle.
|
|
296
|
+
|
|
297
|
+
`margin-inline-end` is set here rather than left to come from an
|
|
298
|
+
ancestor: a BRANCH description used to get it incidentally from
|
|
299
|
+
`details.css`'s `<details>` margins (now gone — see the class docblock in
|
|
300
|
+
`WCTree.ts`), while a LEAF description, never inside a `<details>`, never
|
|
301
|
+
got one at all. Self-contained now, and consistent between the two. */
|
|
199
302
|
.tree__desc {
|
|
200
303
|
margin-block: 0.125rem 0.25rem;
|
|
201
|
-
margin-inline
|
|
304
|
+
margin-inline: var(--_leaf-indent) 0.25rem;
|
|
202
305
|
color: var(--color-text-surface-muted);
|
|
203
306
|
text-wrap: pretty;
|
|
204
307
|
}
|
package/assets/css/utilities.css
CHANGED
|
@@ -284,3 +284,20 @@
|
|
|
284
284
|
.relative {
|
|
285
285
|
position: relative;
|
|
286
286
|
}
|
|
287
|
+
|
|
288
|
+
/* ── Shape — the two geometric absolutes, Tailwind-named ──
|
|
289
|
+
Everything between "square" and "fully round" stays owned by the token
|
|
290
|
+
cascade (`patterns.radii`, group `br`, per-pattern `--br-<name>`) — these two
|
|
291
|
+
are the only radii with no design-token meaning of their own, which is why
|
|
292
|
+
they're utilities and not tokens. No responsive (`md-rounded-full`) variants:
|
|
293
|
+
a responsive radius is rare enough that Tailwind's own `@md:rounded-full`
|
|
294
|
+
covers it on that side.
|
|
295
|
+
|
|
296
|
+
Names + values are Tailwind-exact; Tailwind ships them natively, so they are
|
|
297
|
+
in TW_CLASH and the tailwind format drops these copies. */
|
|
298
|
+
.rounded-none {
|
|
299
|
+
border-radius: 0;
|
|
300
|
+
}
|
|
301
|
+
.rounded-full {
|
|
302
|
+
border-radius: 999px;
|
|
303
|
+
}
|