srcdev-nuxt-components 9.3.0 → 9.3.2
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/.claude/hooks/check-component-update.sh +66 -0
- package/.claude/settings.json +1 -1
- package/.claude/skills/component-local-style-override.md +45 -0
- package/.claude/skills/components/accordian-core.md +26 -5
- package/.claude/skills/components/expanding-panel-classic.md +221 -0
- package/.claude/skills/components/expanding-panel.md +8 -8
- package/.claude/skills/components/navigation-items.md +51 -0
- package/.claude/skills/components/responsive-header.md +161 -0
- package/.claude/skills/components/site-header.md +98 -0
- package/.claude/skills/index.md +6 -1
- package/.claude/skills/page-transitions.md +116 -0
- package/.claude/skills/theming-form-geometry-tokens.md +12 -0
- package/.claude/skills/theming-override-default.md +14 -0
- package/.claude/skills/theming-partial-override.md +17 -0
- package/.vscode/srcdev-component-accordian-core.code-snippets +74 -0
- package/.vscode/srcdev-component-expanding-panel-classic.code-snippets +121 -0
- package/.vscode/srcdev-component-expanding-panel.code-snippets +1 -3
- package/.vscode/srcdev-component-responsive-header.code-snippets +46 -0
- package/.vscode/srcdev-component-site-header.code-snippets +74 -0
- package/app/components/02.molecules/expandable/accordian/AccordianCore.vue +19 -11
- package/app/components/02.molecules/expandable/accordian/CONSUMER-STYLING.md +93 -0
- package/app/components/02.molecules/expandable/accordian/stories/AccordianCore.stories.ts +31 -0
- package/app/components/02.molecules/expandable/accordian/tests/AccordianCore.spec.ts +31 -0
- package/app/components/02.molecules/expandable/accordian/tests/__snapshots__/AccordianCore.spec.ts.snap +7 -21
- package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +10 -10
- package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +35 -37
- package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +5 -6
- package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +5 -13
- package/app/components/02.molecules/expandable/expanding-panel-classic/CONSUMER-STYLING.md +103 -0
- package/app/components/02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue +191 -0
- package/app/components/02.molecules/expandable/expanding-panel-classic/stories/ExpandingPanelClassic.stories.ts +293 -0
- package/app/components/02.molecules/expandable/expanding-panel-classic/tests/ExpandingPanelClassic.spec.ts +514 -0
- package/app/components/02.molecules/expandable/expanding-panel-classic/tests/__snapshots__/ExpandingPanelClassic.spec.ts.snap +59 -0
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +66 -0
- package/app/components/{responsive-header → 03.organisms/responsive-header}/NavigationItems.vue +21 -28
- package/app/components/{responsive-header → 03.organisms/responsive-header}/ResponsiveHeader.vue +141 -43
- package/app/components/03.organisms/responsive-header/stories/NavigationItems.stories.ts +80 -0
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +122 -0
- package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +155 -0
- package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +319 -0
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/NavigationItems.spec.ts.snap +34 -0
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +72 -0
- package/app/components/03.organisms/site-header/CONSUMER-STYLING.md +60 -0
- package/app/components/03.organisms/site-header/SiteHeader.vue +96 -0
- package/app/components/03.organisms/site-header/stories/SiteHeader.stories.ts +135 -0
- package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +103 -0
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +15 -0
- package/app/components/05.forms/input-select/CONSUMER-STYLING.md +42 -0
- package/app/components/05.forms/input-select/InputSelectCore.vue +21 -9
- package/app/components/05.forms/input-text/CONSUMER-STYLING.md +60 -0
- package/app/components/05.forms/input-text/InputTextCore.vue +17 -8
- package/app/layouts/default.vue +64 -67
- package/package.json +4 -1
|
@@ -17,17 +17,15 @@
|
|
|
17
17
|
</slot>
|
|
18
18
|
</div>
|
|
19
19
|
</summary>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
>
|
|
27
|
-
<div class="inner">
|
|
20
|
+
<div
|
|
21
|
+
:id="`id-${name}-content`"
|
|
22
|
+
class="expanding-panel-content"
|
|
23
|
+
:aria-labelledby="`id-${name}-trigger`"
|
|
24
|
+
role="region"
|
|
25
|
+
>
|
|
28
26
|
<slot name="content"></slot>
|
|
29
27
|
</div>
|
|
30
|
-
</
|
|
28
|
+
</details>
|
|
31
29
|
</div>
|
|
32
30
|
</template>
|
|
33
31
|
|
|
@@ -58,10 +56,6 @@ const open = computed(() => props.forceOpened || isPanelOpen.value);
|
|
|
58
56
|
|
|
59
57
|
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
60
58
|
|
|
61
|
-
// contentIsOnTop panels behave like a dropdown/overlay (e.g. ContentDocs' mobile nav) rather
|
|
62
|
-
// than an inline accordion — clicking outside is the expected way to dismiss those, matching
|
|
63
|
-
// native <select>/menu behaviour. Not wanted for ordinary accordions (forceOpened is always
|
|
64
|
-
// false here too — a forceOpened panel isn't meant to be dismissible by any interaction).
|
|
65
59
|
const rootEl = ref<HTMLElement | null>(null);
|
|
66
60
|
onClickOutside(rootEl, () => {
|
|
67
61
|
if (props.contentIsOnTop && !props.forceOpened && isPanelOpen.value) {
|
|
@@ -94,9 +88,6 @@ const handleToggle = (event: Event) => {
|
|
|
94
88
|
};
|
|
95
89
|
|
|
96
90
|
const onDetailsToggle = (event: Event) => {
|
|
97
|
-
// forceOpened drives the native `open` attribute directly, which fires its own
|
|
98
|
-
// 'toggle' event — ignore it here or it leaks into isPanelOpen and gets stuck
|
|
99
|
-
// once forceOpened later reverts to false.
|
|
100
91
|
if (props.forceOpened) return;
|
|
101
92
|
isPanelOpen.value = (event.target as HTMLDetailsElement).open;
|
|
102
93
|
};
|
|
@@ -129,16 +120,20 @@ const onDetailsToggle = (event: Event) => {
|
|
|
129
120
|
.label-wrapper {
|
|
130
121
|
display: inline-block;
|
|
131
122
|
}
|
|
123
|
+
/* inline-size is reserved explicitly — the grid's align-items:center
|
|
124
|
+
doesn't stretch this item to row height, so with only aspect-ratio:1
|
|
125
|
+
and no content yet (icon SVG not loaded), the wrapper has nothing to
|
|
126
|
+
size itself against and collapses to 0 until the icon arrives, then
|
|
127
|
+
pops in and shifts the "auto" grid column's width. */
|
|
132
128
|
.icon-wrapper {
|
|
133
129
|
display: flex;
|
|
134
130
|
align-items: center;
|
|
135
131
|
justify-content: space-between;
|
|
136
132
|
|
|
133
|
+
inline-size: var(--expanding-panel-icon-size, 1.2rem);
|
|
137
134
|
aspect-ratio: 1;
|
|
138
135
|
overflow: hidden;
|
|
139
136
|
|
|
140
|
-
/* Space is always reserved (not v-if'd away) so a forceOpened panel's summary
|
|
141
|
-
row stays the same height as a toggleable sibling's — only the glyph hides. */
|
|
142
137
|
&.icon-wrapper--hidden {
|
|
143
138
|
visibility: hidden;
|
|
144
139
|
}
|
|
@@ -161,32 +156,35 @@ const onDetailsToggle = (event: Event) => {
|
|
|
161
156
|
}
|
|
162
157
|
}
|
|
163
158
|
}
|
|
164
|
-
+ .expanding-panel-content {
|
|
165
|
-
grid-template-rows: 1fr;
|
|
166
|
-
}
|
|
167
159
|
}
|
|
168
|
-
}
|
|
169
160
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
161
|
+
&::details-content {
|
|
162
|
+
overflow: clip;
|
|
163
|
+
height: 0;
|
|
164
|
+
transition:
|
|
165
|
+
height v-bind(animationDurationStr) ease-in-out,
|
|
166
|
+
content-visibility v-bind(animationDurationStr) ease-in-out allow-discrete;
|
|
167
|
+
}
|
|
175
168
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
169
|
+
@supports (interpolate-size: allow-keywords) {
|
|
170
|
+
interpolate-size: allow-keywords;
|
|
171
|
+
|
|
172
|
+
&[open]::details-content {
|
|
173
|
+
height: auto;
|
|
174
|
+
}
|
|
179
175
|
}
|
|
180
176
|
}
|
|
181
177
|
|
|
182
178
|
&.content-is-on-top {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
179
|
+
.expanding-panel-details {
|
|
180
|
+
position: relative;
|
|
181
|
+
|
|
182
|
+
&::details-content {
|
|
183
|
+
position: absolute;
|
|
184
|
+
top: calc(100% + var(--expanding-panel-content-gap, 0px));
|
|
185
|
+
inset-inline: 0;
|
|
186
|
+
z-index: var(--expanding-panel-content-z-index, 10);
|
|
187
|
+
}
|
|
190
188
|
}
|
|
191
189
|
}
|
|
192
190
|
}
|
package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts
CHANGED
|
@@ -242,12 +242,11 @@ export const ContentIsOnTop: Story = {
|
|
|
242
242
|
</template>
|
|
243
243
|
<template #content>
|
|
244
244
|
<!--
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
a wrapper *inside* the slot keeps it clipped correctly by .inner's overflow:hidden.
|
|
245
|
+
Background/border/shadow can go directly on .expanding-panel-content (or, as
|
|
246
|
+
here, a wrapper inside the slot — either works). ::details-content (the native
|
|
247
|
+
anonymous box wrapping this content) is what clips to 0 height when collapsed,
|
|
248
|
+
via overflow:clip on the parent — so this element's own box, including padding
|
|
249
|
+
and background, is clipped correctly regardless of where the styling lives.
|
|
251
250
|
-->
|
|
252
251
|
<div style="color: #808080; background-color:#fff;border:1px solid #ddd;border-radius:0.4rem;padding:1rem;box-shadow:0 4px 12px rgb(0 0 0 / 15%)">
|
|
253
252
|
<p style="margin:0">
|
|
@@ -7,10 +7,8 @@ exports[`ExpandingPanel > renders correct HTML structure with all props set 1`]
|
|
|
7
7
|
<div class="label-wrapper"></div>
|
|
8
8
|
<div class="icon-wrapper icon-wrapper--hidden"><span class="iconify i-bi:caret-down-fill icon mi-12" aria-hidden="true"></span></div>
|
|
9
9
|
</summary>
|
|
10
|
+
<div id="id-snap-all-content" class="expanding-panel-content" aria-labelledby="id-snap-all-trigger" role="region"></div>
|
|
10
11
|
</details>
|
|
11
|
-
<div id="id-snap-all-content" class="expanding-panel-content" aria-labelledby="id-snap-all-trigger" role="region">
|
|
12
|
-
<div class="inner"></div>
|
|
13
|
-
</div>
|
|
14
12
|
</div>"
|
|
15
13
|
`;
|
|
16
14
|
|
|
@@ -21,10 +19,8 @@ exports[`ExpandingPanel > renders correct HTML structure with contentIsOnTop tru
|
|
|
21
19
|
<div class="label-wrapper"></div>
|
|
22
20
|
<div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon mi-12" aria-hidden="true"></span></div>
|
|
23
21
|
</summary>
|
|
22
|
+
<div id="id-snap-content-on-top-content" class="expanding-panel-content" aria-labelledby="id-snap-content-on-top-trigger" role="region"></div>
|
|
24
23
|
</details>
|
|
25
|
-
<div id="id-snap-content-on-top-content" class="expanding-panel-content" aria-labelledby="id-snap-content-on-top-trigger" role="region">
|
|
26
|
-
<div class="inner"></div>
|
|
27
|
-
</div>
|
|
28
24
|
</div>"
|
|
29
25
|
`;
|
|
30
26
|
|
|
@@ -35,10 +31,8 @@ exports[`ExpandingPanel > renders correct HTML structure with default props 1`]
|
|
|
35
31
|
<div class="label-wrapper"></div>
|
|
36
32
|
<div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon mi-12" aria-hidden="true"></span></div>
|
|
37
33
|
</summary>
|
|
34
|
+
<div id="id-snap-default-content" class="expanding-panel-content" aria-labelledby="id-snap-default-trigger" role="region"></div>
|
|
38
35
|
</details>
|
|
39
|
-
<div id="id-snap-default-content" class="expanding-panel-content" aria-labelledby="id-snap-default-trigger" role="region">
|
|
40
|
-
<div class="inner"></div>
|
|
41
|
-
</div>
|
|
42
36
|
</div>"
|
|
43
37
|
`;
|
|
44
38
|
|
|
@@ -49,11 +43,9 @@ exports[`ExpandingPanel > renders correct HTML structure with populated slots 1`
|
|
|
49
43
|
<div class="label-wrapper"><span>Summary text</span></div>
|
|
50
44
|
<div class="icon-wrapper"><span>▸</span></div>
|
|
51
45
|
</summary>
|
|
52
|
-
|
|
53
|
-
<div id="id-snap-slots-content" class="expanding-panel-content" aria-labelledby="id-snap-slots-trigger" role="region">
|
|
54
|
-
<div class="inner">
|
|
46
|
+
<div id="id-snap-slots-content" class="expanding-panel-content" aria-labelledby="id-snap-slots-trigger" role="region">
|
|
55
47
|
<p>Content text</p>
|
|
56
48
|
</div>
|
|
57
|
-
</
|
|
49
|
+
</details>
|
|
58
50
|
</div>"
|
|
59
51
|
`;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# ExpandingPanelClassic — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Public token API
|
|
4
|
+
|
|
5
|
+
All `--expanding-panel-classic-*` tokens are the stable override surface. Set them at any scope
|
|
6
|
+
(global, page, or instance) without touching the component itself.
|
|
7
|
+
|
|
8
|
+
| Token | Default | Controls |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| `--expanding-panel-classic-summary-gap` | `1rem` | Gap between the summary label and the toggle icon |
|
|
11
|
+
| `--expanding-panel-classic-summary-padding-block` | `0.5rem` | Vertical padding on the clickable summary row |
|
|
12
|
+
| `--expanding-panel-classic-icon-size` | `1.2rem` | Toggle icon `font-size` |
|
|
13
|
+
| `--expanding-panel-classic-content-z-index` | `10` | Stacking order of the content region when `contentIsOnTop` is `true` |
|
|
14
|
+
| `--expanding-panel-classic-content-gap` | `0px` | Space between the summary and the content region when `contentIsOnTop` is `true` |
|
|
15
|
+
|
|
16
|
+
Note: `background-color`, `padding`, and shadow are **not** tokenised, and must never be applied
|
|
17
|
+
to `.inner` itself — see [expanding-panel-classic.md](../../../../.claude/skills/components/expanding-panel-classic.md#styling-the-content-when-contentisontop)
|
|
18
|
+
for why, and style a wrapper *inside* the `#content` slot instead.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Global theming — recommended approach
|
|
23
|
+
|
|
24
|
+
Create `assets/styles/setup/07.components/expanding-panel-classic.css` in the consuming app and set
|
|
25
|
+
tokens on `:root`. This applies to every `ExpandingPanelClassic` across the site.
|
|
26
|
+
|
|
27
|
+
```css
|
|
28
|
+
/* assets/styles/setup/07.components/expanding-panel-classic.css */
|
|
29
|
+
:root {
|
|
30
|
+
--expanding-panel-classic-summary-gap: 1.6rem;
|
|
31
|
+
--expanding-panel-classic-summary-padding-block: 1.2rem;
|
|
32
|
+
--expanding-panel-classic-icon-size: 1.4rem;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Page-scoped overrides
|
|
39
|
+
|
|
40
|
+
Override tokens for a specific section by scoping them under the page or layout wrapper.
|
|
41
|
+
No `:deep()` is required (component styles are unscoped).
|
|
42
|
+
|
|
43
|
+
```css
|
|
44
|
+
/* In the consuming page's unscoped <style> block */
|
|
45
|
+
.faq-section {
|
|
46
|
+
.expanding-panel-classic {
|
|
47
|
+
--expanding-panel-classic-summary-padding-block: 1.6rem;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Per-instance overrides via styleClassPassthrough
|
|
55
|
+
|
|
56
|
+
Use sparingly — prefer global or page-scoped CSS. When a single instance needs a distinct
|
|
57
|
+
visual style, pass a modifier class:
|
|
58
|
+
|
|
59
|
+
```vue
|
|
60
|
+
<ExpandingPanelClassic
|
|
61
|
+
name="promo"
|
|
62
|
+
:content-is-on-top="true"
|
|
63
|
+
:style-class-passthrough="['promo-panel']"
|
|
64
|
+
>
|
|
65
|
+
<template #summary>...</template>
|
|
66
|
+
<template #content>
|
|
67
|
+
<!-- Wrapper INSIDE the slot carries the visual styling — never .inner itself -->
|
|
68
|
+
<div class="promo-panel-body">...</div>
|
|
69
|
+
</template>
|
|
70
|
+
</ExpandingPanelClassic>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```css
|
|
74
|
+
.expanding-panel-classic.promo-panel {
|
|
75
|
+
--expanding-panel-classic-content-gap: 0.4rem;
|
|
76
|
+
--expanding-panel-classic-content-z-index: 20;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.promo-panel-body {
|
|
80
|
+
background-color: white;
|
|
81
|
+
padding: 1rem;
|
|
82
|
+
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Notes
|
|
89
|
+
|
|
90
|
+
- `--expanding-panel-classic-content-gap` and `--expanding-panel-classic-content-z-index` only take effect when
|
|
91
|
+
`contentIsOnTop` is `true` — they're no-ops for the default (in-flow) layout.
|
|
92
|
+
- Never set `background-color`, `padding`, `border`, or a shadow directly on `.inner`. `.inner`
|
|
93
|
+
has no explicit height when collapsed (`grid-template-rows: 0fr`) and relies on
|
|
94
|
+
`overflow: hidden` to clip its *children* to 0px — but padding/border/background on `.inner`'s
|
|
95
|
+
own box are not "overflow content", so they'd still render as a visible gap under the summary
|
|
96
|
+
while closed. Always style a wrapper element placed *inside* the `#content` slot instead; that
|
|
97
|
+
wrapper is a child of `.inner` and gets clipped correctly.
|
|
98
|
+
- Don't stack multiple `contentIsOnTop` panels as direct siblings (linked via a shared `name` or
|
|
99
|
+
not). The overlay is absolutely positioned so it doesn't push the next element down — which
|
|
100
|
+
means a sibling `ExpandingPanelClassic` placed right after it sits exactly where the overlay renders,
|
|
101
|
+
and gets visually covered when the first panel opens. `contentIsOnTop` is for a single panel
|
|
102
|
+
overlaying unrelated trailing page content, not for grouped/stacked accordion panels — use the
|
|
103
|
+
default in-flow layout for those.
|
package/app/components/02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="rootEl" class="expanding-panel-classic" :class="[elementClasses, { 'content-is-on-top': contentIsOnTop }]">
|
|
3
|
+
<details class="expanding-panel-classic-details" :name :open @toggle="onDetailsToggle">
|
|
4
|
+
<summary
|
|
5
|
+
:id="`id-${name}-trigger`"
|
|
6
|
+
class="expanding-panel-classic-summary"
|
|
7
|
+
:aria-controls="`id-${name}-content`"
|
|
8
|
+
:aria-expanded="open"
|
|
9
|
+
@click.prevent.stop="handleToggle"
|
|
10
|
+
>
|
|
11
|
+
<div class="label-wrapper">
|
|
12
|
+
<slot name="summary"></slot>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="icon-wrapper" :class="{ 'icon-wrapper--hidden': forceOpened }">
|
|
15
|
+
<slot name="icon">
|
|
16
|
+
<Icon name="bi:caret-down-fill" class="icon mi-12" />
|
|
17
|
+
</slot>
|
|
18
|
+
</div>
|
|
19
|
+
</summary>
|
|
20
|
+
</details>
|
|
21
|
+
<div
|
|
22
|
+
:id="`id-${name}-content`"
|
|
23
|
+
class="expanding-panel-classic-content"
|
|
24
|
+
:aria-labelledby="`id-${name}-trigger`"
|
|
25
|
+
role="region"
|
|
26
|
+
>
|
|
27
|
+
<div class="inner">
|
|
28
|
+
<slot name="content"></slot>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script setup lang="ts">
|
|
35
|
+
import { onClickOutside } from "@vueuse/core";
|
|
36
|
+
|
|
37
|
+
interface Props {
|
|
38
|
+
name?: string;
|
|
39
|
+
animationDuration?: number;
|
|
40
|
+
forceOpened?: boolean;
|
|
41
|
+
contentIsOnTop?: boolean;
|
|
42
|
+
styleClassPassthrough?: string | string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
46
|
+
name: undefined,
|
|
47
|
+
animationDuration: 400,
|
|
48
|
+
forceOpened: false,
|
|
49
|
+
contentIsOnTop: false,
|
|
50
|
+
styleClassPassthrough: () => [],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const fallbackName = useId();
|
|
54
|
+
const name = computed(() => props.name || fallbackName);
|
|
55
|
+
const isPanelOpen = defineModel<boolean>({ default: false });
|
|
56
|
+
const animationDurationStr = computed(() => `${props.animationDuration}ms`);
|
|
57
|
+
const open = computed(() => props.forceOpened || isPanelOpen.value);
|
|
58
|
+
|
|
59
|
+
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
60
|
+
|
|
61
|
+
const rootEl = ref<HTMLElement | null>(null);
|
|
62
|
+
onClickOutside(rootEl, () => {
|
|
63
|
+
if (props.contentIsOnTop && !props.forceOpened && isPanelOpen.value) {
|
|
64
|
+
isPanelOpen.value = false;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (import.meta.dev) {
|
|
69
|
+
watch(
|
|
70
|
+
() => props.contentIsOnTop,
|
|
71
|
+
(value) => {
|
|
72
|
+
if (!value) return;
|
|
73
|
+
console.warn(
|
|
74
|
+
`ExpandingPanelClassic${props.name ? ` "${props.name}"` : ""}: contentIsOnTop is enabled. ` +
|
|
75
|
+
"The content overlay is absolutely positioned and does not reserve space, so a sibling " +
|
|
76
|
+
"element (e.g. another ExpandingPanelClassic) placed directly after this one will be visually " +
|
|
77
|
+
"covered when this panel opens. Intended for a single panel overlaying unrelated trailing " +
|
|
78
|
+
"page content — avoid stacking multiple contentIsOnTop panels as direct siblings."
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
{ immediate: true }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const handleToggle = (event: Event) => {
|
|
86
|
+
if (props.forceOpened) {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
}
|
|
89
|
+
isPanelOpen.value = !isPanelOpen.value;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const onDetailsToggle = (event: Event) => {
|
|
93
|
+
if (props.forceOpened) return;
|
|
94
|
+
isPanelOpen.value = (event.target as HTMLDetailsElement).open;
|
|
95
|
+
};
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<style lang="css">
|
|
99
|
+
@layer components {
|
|
100
|
+
.expanding-panel-classic {
|
|
101
|
+
.expanding-panel-classic-details {
|
|
102
|
+
&:hover {
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
}
|
|
105
|
+
.expanding-panel-classic-summary {
|
|
106
|
+
display: grid;
|
|
107
|
+
align-items: center;
|
|
108
|
+
grid-template-columns: 1fr auto;
|
|
109
|
+
gap: var(--expanding-panel-classic-summary-gap, 1rem);
|
|
110
|
+
list-style: none;
|
|
111
|
+
user-select: none;
|
|
112
|
+
|
|
113
|
+
padding-block: var(--expanding-panel-classic-summary-padding-block, 0.5rem);
|
|
114
|
+
|
|
115
|
+
&::-webkit-details-marker,
|
|
116
|
+
&::marker {
|
|
117
|
+
display: none;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
|
|
122
|
+
.label-wrapper {
|
|
123
|
+
display: inline-block;
|
|
124
|
+
}
|
|
125
|
+
/* inline-size is reserved explicitly — the grid's align-items:center
|
|
126
|
+
doesn't stretch this item to row height, so with only aspect-ratio:1
|
|
127
|
+
and no content yet (icon SVG not loaded), the wrapper has nothing to
|
|
128
|
+
size itself against and collapses to 0 until the icon arrives, then
|
|
129
|
+
pops in and shifts the "auto" grid column's width. */
|
|
130
|
+
.icon-wrapper {
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: space-between;
|
|
134
|
+
|
|
135
|
+
inline-size: var(--expanding-panel-classic-icon-size, 1.2rem);
|
|
136
|
+
aspect-ratio: 1;
|
|
137
|
+
overflow: hidden;
|
|
138
|
+
|
|
139
|
+
&.icon-wrapper--hidden {
|
|
140
|
+
visibility: hidden;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.icon {
|
|
144
|
+
display: block;
|
|
145
|
+
transform: scaleY(1);
|
|
146
|
+
transition: transform v-bind(animationDurationStr) ease-in-out;
|
|
147
|
+
font-size: var(--expanding-panel-classic-icon-size, 1.2rem);
|
|
148
|
+
will-change: transform;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
&[open] {
|
|
154
|
+
.expanding-panel-classic-summary {
|
|
155
|
+
.icon-wrapper {
|
|
156
|
+
.icon {
|
|
157
|
+
transform: scaleY(-1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
+ .expanding-panel-classic-content {
|
|
162
|
+
grid-template-rows: 1fr;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.expanding-panel-classic-content {
|
|
168
|
+
display: grid;
|
|
169
|
+
grid-template-rows: 0fr;
|
|
170
|
+
transition: grid-template-rows v-bind(animationDurationStr) ease-in-out;
|
|
171
|
+
will-change: grid-template-rows;
|
|
172
|
+
|
|
173
|
+
.inner {
|
|
174
|
+
overflow: hidden;
|
|
175
|
+
margin-top: 0;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
&.content-is-on-top {
|
|
180
|
+
position: relative;
|
|
181
|
+
|
|
182
|
+
.expanding-panel-classic-content {
|
|
183
|
+
position: absolute;
|
|
184
|
+
top: calc(100% + var(--expanding-panel-classic-content-gap, 0px));
|
|
185
|
+
inset-inline: 0;
|
|
186
|
+
z-index: var(--expanding-panel-classic-content-z-index, 10);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
</style>
|