srcdev-nuxt-components 9.2.4 → 9.2.6
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/settings.json +35 -1
- package/.claude/skills/components/content-docs.md +165 -0
- package/.claude/skills/components/expanding-panel.md +55 -0
- package/.claude/skills/index.md +4 -2
- package/.claude/skills/qa-panel.md +5 -6
- package/.claude/skills/setup-postinstall.md +11 -5
- package/.claude/skills/theming-typography-tokens.md +109 -0
- package/.vscode/srcdev-component-content-docs.code-snippets +129 -0
- package/.vscode/srcdev-component-expanding-panel.code-snippets +123 -0
- package/README.md +10 -4
- package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +410 -0
- package/app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts +53 -0
- package/app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts-snapshots/default-chromium-darwin.png +0 -0
- package/app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts-snapshots/state-desktop-chromium-darwin.png +0 -0
- package/app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts-snapshots/state-mobile-chromium-darwin.png +0 -0
- package/app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts-snapshots/state-tablet-chromium-darwin.png +0 -0
- package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +172 -0
- package/app/components/01.atoms/content-wrappers/docs-pages/tests/ContentDocs.spec.ts +218 -0
- package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +103 -0
- package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +97 -56
- package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +50 -2
- package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +79 -2
- package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +25 -4
- package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +4 -0
- package/app/components/02.molecules/pricing-card/PricingCard.vue +5 -2
- package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +15 -1
- package/app/components/layout-grids/LayoutGridA.vue +59 -59
- package/app/composables/tests/useContainerBreakpoints.spec.ts +91 -0
- package/app/composables/useContainerBreakpoints.ts +71 -0
- package/app/layouts/default.vue +1 -0
- package/app/pages/ui/expanding-panel.vue +266 -129
- package/app/pages/ui/layout-content-docs.vue +72 -0
- package/app/pages/ui/layout-grid-a.vue +5 -58
- package/app/types/components/content-docs.d.ts +5 -0
- package/app/types/components/index.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
{
|
|
2
|
+
"SRCDEV ExpandingPanel Basic": {
|
|
3
|
+
"description": "Basic uncontrolled ExpandingPanel with summary and content slots",
|
|
4
|
+
"scope": "vue,html",
|
|
5
|
+
"body": [
|
|
6
|
+
"<ExpandingPanel name=\"$1delivery\">",
|
|
7
|
+
" <template #summary>",
|
|
8
|
+
" <span>$2Delivery & Returns</span>",
|
|
9
|
+
" </template>",
|
|
10
|
+
" <template #content>",
|
|
11
|
+
" <p>$3Free standard delivery on orders over £50.</p>",
|
|
12
|
+
" </template>",
|
|
13
|
+
"</ExpandingPanel>"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"SRCDEV ExpandingPanel Controlled": {
|
|
17
|
+
"description": "ExpandingPanel controlled via v-model with external toggle",
|
|
18
|
+
"scope": "vue,html",
|
|
19
|
+
"body": [
|
|
20
|
+
"<ExpandingPanel name=\"$1faq-1\" v-model=\"$2isOpen\">",
|
|
21
|
+
" <template #summary>",
|
|
22
|
+
" <span>$3What is your returns policy?</span>",
|
|
23
|
+
" </template>",
|
|
24
|
+
" <template #content>",
|
|
25
|
+
" <p>$4You can return any item within 30 days.</p>",
|
|
26
|
+
" </template>",
|
|
27
|
+
"</ExpandingPanel>"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"SRCDEV ExpandingPanel Accordion Group": {
|
|
31
|
+
"description": "Multiple ExpandingPanels grouped into a native accordion (shared name, only one open at a time)",
|
|
32
|
+
"scope": "vue,html",
|
|
33
|
+
"body": [
|
|
34
|
+
"<ExpandingPanel name=\"$1faq\">",
|
|
35
|
+
" <template #summary>",
|
|
36
|
+
" <span>$2Question one?</span>",
|
|
37
|
+
" </template>",
|
|
38
|
+
" <template #content>",
|
|
39
|
+
" <p>$3Answer one.</p>",
|
|
40
|
+
" </template>",
|
|
41
|
+
"</ExpandingPanel>",
|
|
42
|
+
"<ExpandingPanel name=\"$1faq\">",
|
|
43
|
+
" <template #summary>",
|
|
44
|
+
" <span>$4Question two?</span>",
|
|
45
|
+
" </template>",
|
|
46
|
+
" <template #content>",
|
|
47
|
+
" <p>$5Answer two.</p>",
|
|
48
|
+
" </template>",
|
|
49
|
+
"</ExpandingPanel>"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"SRCDEV ExpandingPanel Force Opened": {
|
|
53
|
+
"description": "ExpandingPanel locked open, no toggle icon, cannot be collapsed",
|
|
54
|
+
"scope": "vue,html",
|
|
55
|
+
"body": [
|
|
56
|
+
"<ExpandingPanel name=\"$1notice\" :force-opened=\"true\">",
|
|
57
|
+
" <template #summary>",
|
|
58
|
+
" <strong>$2Important notice</strong>",
|
|
59
|
+
" </template>",
|
|
60
|
+
" <template #content>",
|
|
61
|
+
" <p>$3This panel cannot be collapsed.</p>",
|
|
62
|
+
" </template>",
|
|
63
|
+
"</ExpandingPanel>"
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
"SRCDEV ExpandingPanel Content Is On Top": {
|
|
67
|
+
"description": "ExpandingPanel with content overlaying page content instead of pushing layout down — styling must go on a wrapper INSIDE #content, never on .inner",
|
|
68
|
+
"scope": "vue,html",
|
|
69
|
+
"body": [
|
|
70
|
+
"<ExpandingPanel",
|
|
71
|
+
" name=\"$1overlay\"",
|
|
72
|
+
" :content-is-on-top=\"true\"",
|
|
73
|
+
" :style-class-passthrough=\"['$2my-overlay-panel']\"",
|
|
74
|
+
">",
|
|
75
|
+
" <template #summary>",
|
|
76
|
+
" <span>$3Open me — content overlays what's below</span>",
|
|
77
|
+
" </template>",
|
|
78
|
+
" <template #content>",
|
|
79
|
+
" <!-- Wrapper INSIDE the slot carries background/padding/shadow — never .inner itself,",
|
|
80
|
+
" which must stay visually unstyled or it renders while the panel is closed. -->",
|
|
81
|
+
" <div class=\"$2my-overlay-panel-body\">",
|
|
82
|
+
" <p>$4Positioned absolutely below the summary, doesn't push page content down.</p>",
|
|
83
|
+
" </div>",
|
|
84
|
+
" </template>",
|
|
85
|
+
"</ExpandingPanel>"
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
"SRCDEV ExpandingPanel Custom Icon": {
|
|
89
|
+
"description": "ExpandingPanel with a custom toggle icon via #icon slot",
|
|
90
|
+
"scope": "vue,html",
|
|
91
|
+
"body": [
|
|
92
|
+
"<ExpandingPanel name=\"$1custom-icon\">",
|
|
93
|
+
" <template #summary>",
|
|
94
|
+
" <span>$2Section title</span>",
|
|
95
|
+
" </template>",
|
|
96
|
+
" <template #icon>",
|
|
97
|
+
" <span style=\"font-size:1.2rem\">$3+</span>",
|
|
98
|
+
" </template>",
|
|
99
|
+
" <template #content>",
|
|
100
|
+
" <p>$4Content here.</p>",
|
|
101
|
+
" </template>",
|
|
102
|
+
"</ExpandingPanel>"
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
"SRCDEV ExpandingPanel CSS Override": {
|
|
106
|
+
"description": "CSS override scaffold for ExpandingPanel tokens",
|
|
107
|
+
"scope": "css",
|
|
108
|
+
"body": [
|
|
109
|
+
".$1my-panel {",
|
|
110
|
+
" .expanding-panel {",
|
|
111
|
+
" --expanding-panel-summary-gap: $2;",
|
|
112
|
+
" --expanding-panel-summary-padding-block: $3;",
|
|
113
|
+
" --expanding-panel-icon-size: $4;",
|
|
114
|
+
"",
|
|
115
|
+
" &.content-is-on-top {",
|
|
116
|
+
" --expanding-panel-content-gap: $5;",
|
|
117
|
+
" --expanding-panel-content-z-index: $6;",
|
|
118
|
+
" }",
|
|
119
|
+
" }",
|
|
120
|
+
"}"
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
}
|
package/README.md
CHANGED
|
@@ -43,16 +43,22 @@ Skills are copied into `.claude/skills/srcdev-nuxt-components/` so they never co
|
|
|
43
43
|
|
|
44
44
|
### Automate with postinstall (recommended)
|
|
45
45
|
|
|
46
|
-
To ensure skills are always up to date and `nuxt prepare` is never forgotten,
|
|
46
|
+
To ensure skills are always up to date, VSCode snippets are installed, and `nuxt prepare` is never forgotten, add a `postinstall` script. npm runs this automatically after every `npm install`:
|
|
47
47
|
|
|
48
48
|
```json
|
|
49
49
|
"scripts": {
|
|
50
|
-
"setup:claude": "cp -r node_modules/srcdev-nuxt-components/.claude/skills .claude/skills/srcdev-nuxt-components",
|
|
51
|
-
"postinstall": "nuxt prepare && npm run setup:claude"
|
|
50
|
+
"setup:claude": "mkdir -p .claude/skills/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/skills/. .claude/skills/srcdev-nuxt-components",
|
|
51
|
+
"postinstall": "node node_modules/srcdev-nuxt-components/scripts/copy-snippets.mjs && nuxt prepare && npm run setup:claude"
|
|
52
52
|
}
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
**What this does:**
|
|
56
|
+
|
|
57
|
+
1. `copy-snippets.mjs` — copies VSCode snippets (`.code-snippets` files) from the layer to your `.vscode/` folder
|
|
58
|
+
2. `nuxt prepare` — generates Nuxt type declarations
|
|
59
|
+
3. `setup:claude` — copies skills into `.claude/skills/srcdev-nuxt-components/`
|
|
60
|
+
|
|
61
|
+
> **Note**: The snippet copy must run from your consumer app's postinstall, not the layer's. npm doesn't invoke postinstall hooks for dependencies, so each consumer app is responsible for copying snippets into its own `.vscode/` folder. If your app uses a standalone env flag for `nuxt prepare` (e.g. `NUXT_STANDALONE=true`), add it before `nuxt prepare`. This is project-specific — check your own `nuxt.config.ts` to confirm whether it is needed.
|
|
56
62
|
|
|
57
63
|
---
|
|
58
64
|
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component :is="tag" ref="rootEl" class="content-docs" :class="[elementClasses]">
|
|
3
|
+
<div class="content-docs-inner">
|
|
4
|
+
<div v-if="hasDocsNav" class="docs-nav">
|
|
5
|
+
<ExpandingPanel
|
|
6
|
+
:name="docsNavPanelName"
|
|
7
|
+
:animation-duration="200"
|
|
8
|
+
:force-opened="docsNavForceOpen"
|
|
9
|
+
:content-is-on-top="docsNavOnTop"
|
|
10
|
+
>
|
|
11
|
+
<template #summary>
|
|
12
|
+
<h3 class="docs-nav-heading">{{ docsNavLabel }}</h3>
|
|
13
|
+
</template>
|
|
14
|
+
<template #content>
|
|
15
|
+
<nav class="docs-nav-list" aria-label="Docs navigation">
|
|
16
|
+
<ul>
|
|
17
|
+
<li v-for="item in docsNavItems" :key="item.to">
|
|
18
|
+
<component
|
|
19
|
+
:is="NuxtLink"
|
|
20
|
+
:to="item.to"
|
|
21
|
+
class="docs-nav-link"
|
|
22
|
+
:class="{ 'is-active': item.to === activeNavItem }"
|
|
23
|
+
:aria-current="item.to === activeNavItem ? 'page' : undefined"
|
|
24
|
+
@click="activeNavItem = item.to"
|
|
25
|
+
>
|
|
26
|
+
<Icon v-if="item.icon" :name="item.icon" class="docs-nav-link-icon" aria-hidden="true" />
|
|
27
|
+
<span class="docs-nav-link-label">{{ item.label }}</span>
|
|
28
|
+
</component>
|
|
29
|
+
</li>
|
|
30
|
+
</ul>
|
|
31
|
+
</nav>
|
|
32
|
+
</template>
|
|
33
|
+
</ExpandingPanel>
|
|
34
|
+
</div>
|
|
35
|
+
<div v-if="hasDocsContent" class="docs-content">
|
|
36
|
+
<slot name="docsContent"></slot>
|
|
37
|
+
</div>
|
|
38
|
+
<div v-if="hasDocsPageNav" class="docs-page-nav">
|
|
39
|
+
<ExpandingPanel
|
|
40
|
+
:name="docsPageNavPanelName"
|
|
41
|
+
:animation-duration="200"
|
|
42
|
+
:force-opened="docsPageNavForceOpen"
|
|
43
|
+
:content-is-on-top="docsPageNavOnTop"
|
|
44
|
+
>
|
|
45
|
+
<template #summary>
|
|
46
|
+
<h3 class="docs-page-nav-heading">{{ docsPageNavLabel }}</h3>
|
|
47
|
+
</template>
|
|
48
|
+
<template #content>
|
|
49
|
+
<nav class="docs-page-nav-list" aria-label="On this page">
|
|
50
|
+
<ul>
|
|
51
|
+
<li v-for="item in docsPageNavItems" :key="item.to">
|
|
52
|
+
<component
|
|
53
|
+
:is="NuxtLink"
|
|
54
|
+
:to="item.to"
|
|
55
|
+
class="docs-page-nav-link"
|
|
56
|
+
:class="{ 'is-active': item.to === activePageNavItem }"
|
|
57
|
+
:aria-current="item.to === activePageNavItem ? 'page' : undefined"
|
|
58
|
+
@click="activePageNavItem = item.to"
|
|
59
|
+
>
|
|
60
|
+
<Icon v-if="item.icon" :name="item.icon" class="docs-page-nav-link-icon" aria-hidden="true" />
|
|
61
|
+
<span class="docs-page-nav-link-label">{{ item.label }}</span>
|
|
62
|
+
</component>
|
|
63
|
+
</li>
|
|
64
|
+
</ul>
|
|
65
|
+
</nav>
|
|
66
|
+
</template>
|
|
67
|
+
</ExpandingPanel>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</component>
|
|
71
|
+
</template>
|
|
72
|
+
|
|
73
|
+
<script setup lang="ts">
|
|
74
|
+
import type { DocsNavItem } from "~/types/components";
|
|
75
|
+
|
|
76
|
+
interface Props {
|
|
77
|
+
tag?: "div" | "section" | "article" | "main";
|
|
78
|
+
docsNavItems?: DocsNavItem[];
|
|
79
|
+
docsPageNavItems?: DocsNavItem[];
|
|
80
|
+
docsNavLabel?: string;
|
|
81
|
+
docsPageNavLabel?: string;
|
|
82
|
+
styleClassPassthrough?: string | string[];
|
|
83
|
+
}
|
|
84
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
85
|
+
tag: "div",
|
|
86
|
+
docsNavItems: () => [],
|
|
87
|
+
docsPageNavItems: () => [],
|
|
88
|
+
docsNavLabel: "Navigation",
|
|
89
|
+
docsPageNavLabel: "On this page",
|
|
90
|
+
styleClassPassthrough: () => [],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const activeNavItem = defineModel<string | undefined>("activeNavItem");
|
|
94
|
+
const activePageNavItem = defineModel<string | undefined>("activePageNavItem");
|
|
95
|
+
|
|
96
|
+
const NuxtLink = resolveComponent("NuxtLink");
|
|
97
|
+
|
|
98
|
+
const slots = useSlots();
|
|
99
|
+
const hasDocsNav = computed(() => props.docsNavItems.length > 0);
|
|
100
|
+
const hasDocsContent = computed(() => !!slots.docsContent);
|
|
101
|
+
const hasDocsPageNav = computed(() => props.docsPageNavItems.length > 0);
|
|
102
|
+
|
|
103
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
104
|
+
|
|
105
|
+
watch(
|
|
106
|
+
() => props.styleClassPassthrough,
|
|
107
|
+
() => {
|
|
108
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Breakpoints match this component's own @container queries below —
|
|
113
|
+
// keep both in sync if either changes.
|
|
114
|
+
const rootEl = ref<HTMLElement | null>(null);
|
|
115
|
+
const { greaterOrEqual, smaller } = useContainerBreakpoints({ tablet: 768, desktop: 1024 }, rootEl);
|
|
116
|
+
const isDesktop = greaterOrEqual("desktop");
|
|
117
|
+
const isMobile = smaller("tablet");
|
|
118
|
+
|
|
119
|
+
const docsNavForceOpen = computed(() => isDesktop.value);
|
|
120
|
+
const docsNavOnTop = computed(() => !isDesktop.value);
|
|
121
|
+
const docsPageNavForceOpen = computed(() => !isMobile.value);
|
|
122
|
+
const docsPageNavOnTop = computed(() => isMobile.value);
|
|
123
|
+
|
|
124
|
+
// Shared `name` groups the two <details> into a mutually-exclusive native accordion —
|
|
125
|
+
// wanted on mobile (neither is forceOpened), wrong on tablet/desktop where both are
|
|
126
|
+
// forceOpened simultaneously: a shared name would make the browser force-close one.
|
|
127
|
+
const panelGroupId = useId();
|
|
128
|
+
const docsNavPanelName = computed(() => (isMobile.value ? panelGroupId : `${panelGroupId}-docsNav`));
|
|
129
|
+
const docsPageNavPanelName = computed(() => (isMobile.value ? panelGroupId : `${panelGroupId}-docsPageNav`));
|
|
130
|
+
</script>
|
|
131
|
+
|
|
132
|
+
<style lang="css">
|
|
133
|
+
@layer components {
|
|
134
|
+
.content-docs {
|
|
135
|
+
/* Shared defaults — theme both docsNav and docsPageNav at once */
|
|
136
|
+
--_heading-font-size: var(--content-docs-heading-font-size, 1.4rem);
|
|
137
|
+
--_heading-font-weight: var(--content-docs-heading-font-weight, 700);
|
|
138
|
+
--_heading-color: var(--content-docs-heading-color, light-dark(var(--slate-10), var(--slate-01)));
|
|
139
|
+
--_heading-bg: var(--content-docs-heading-bg, transparent);
|
|
140
|
+
--_heading-margin: var(--content-docs-heading-margin, 0);
|
|
141
|
+
--_heading-padding-block: var(--content-docs-heading-padding-block, 0.4rem 0.8rem);
|
|
142
|
+
--_heading-padding-inline: var(--content-docs-heading-padding-inline, 0);
|
|
143
|
+
|
|
144
|
+
--_panel-bg: var(--content-docs-panel-bg, light-dark(var(--slate-00), var(--slate-10)));
|
|
145
|
+
--_panel-padding-block: var(--content-docs-panel-padding-block, 0.8rem);
|
|
146
|
+
--_panel-padding-inline: var(--content-docs-panel-padding-inline, 0.8rem);
|
|
147
|
+
--_panel-border-radius: var(--content-docs-panel-border-radius, 0.5rem);
|
|
148
|
+
|
|
149
|
+
/* Fixed-width grid tracks — the docsNav/docsPageNav columns at tablet/desktop.
|
|
150
|
+
docsPageNav gets a narrower track at tablet since docsNav is full-width there. */
|
|
151
|
+
--_nav-column-width: var(--content-docs-nav-column-width, 23rem);
|
|
152
|
+
--_page-nav-column-width: var(--content-docs-page-nav-column-width, 22rem);
|
|
153
|
+
--_page-nav-column-width-tablet: var(--content-docs-page-nav-column-width-tablet, 20rem);
|
|
154
|
+
|
|
155
|
+
--_link-font-size: var(--content-docs-link-font-size, 1.4rem);
|
|
156
|
+
--_link-padding-block: var(--content-docs-link-padding-block, 0.6rem);
|
|
157
|
+
--_link-padding-inline: var(--content-docs-link-padding-inline, 0.8rem);
|
|
158
|
+
--_link-margin-block: var(--content-docs-link-margin-block, 0);
|
|
159
|
+
--_link-border-radius: var(--content-docs-link-border-radius, 0.4rem);
|
|
160
|
+
--_link-color: var(--content-docs-link-color, light-dark(var(--slate-09), var(--slate-01)));
|
|
161
|
+
--_link-bg: var(--content-docs-link-bg, transparent);
|
|
162
|
+
--_link-hover-bg: var(--content-docs-link-hover-bg, light-dark(var(--slate-01), var(--slate-09)));
|
|
163
|
+
--_link-hover-color: var(--content-docs-link-hover-color, var(--_link-color));
|
|
164
|
+
--_link-active-bg: var(--content-docs-link-active-bg, light-dark(var(--green-01), var(--green-09)));
|
|
165
|
+
--_link-active-color: var(--content-docs-link-active-color, light-dark(var(--green-10), var(--green-04)));
|
|
166
|
+
|
|
167
|
+
/* Per-side overrides — each falls back to the shared token above */
|
|
168
|
+
--_nav-heading-font-size: var(--content-docs-nav-heading-font-size, var(--_heading-font-size));
|
|
169
|
+
--_nav-heading-font-weight: var(--content-docs-nav-heading-font-weight, var(--_heading-font-weight));
|
|
170
|
+
--_nav-heading-color: var(--content-docs-nav-heading-color, var(--_heading-color));
|
|
171
|
+
--_nav-heading-bg: var(--content-docs-nav-heading-bg, var(--_heading-bg));
|
|
172
|
+
--_nav-heading-margin: var(--content-docs-nav-heading-margin, var(--_heading-margin));
|
|
173
|
+
--_nav-heading-padding-block: var(--content-docs-nav-heading-padding-block, var(--_heading-padding-block));
|
|
174
|
+
--_nav-heading-padding-inline: var(--content-docs-nav-heading-padding-inline, var(--_heading-padding-inline));
|
|
175
|
+
|
|
176
|
+
--_page-nav-heading-font-size: var(--content-docs-page-nav-heading-font-size, var(--_heading-font-size));
|
|
177
|
+
--_page-nav-heading-font-weight: var(--content-docs-page-nav-heading-font-weight, var(--_heading-font-weight));
|
|
178
|
+
--_page-nav-heading-color: var(--content-docs-page-nav-heading-color, var(--_heading-color));
|
|
179
|
+
--_page-nav-heading-bg: var(--content-docs-page-nav-heading-bg, var(--_heading-bg));
|
|
180
|
+
--_page-nav-heading-margin: var(--content-docs-page-nav-heading-margin, var(--_heading-margin));
|
|
181
|
+
--_page-nav-heading-padding-block: var(
|
|
182
|
+
--content-docs-page-nav-heading-padding-block,
|
|
183
|
+
var(--_heading-padding-block)
|
|
184
|
+
);
|
|
185
|
+
--_page-nav-heading-padding-inline: var(
|
|
186
|
+
--content-docs-page-nav-heading-padding-inline,
|
|
187
|
+
var(--_heading-padding-inline)
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
--_nav-panel-bg: var(--content-docs-nav-panel-bg, var(--_panel-bg));
|
|
191
|
+
--_page-nav-panel-bg: var(--content-docs-page-nav-panel-bg, var(--_panel-bg));
|
|
192
|
+
|
|
193
|
+
--_nav-link-color: var(--content-docs-nav-link-color, var(--_link-color));
|
|
194
|
+
--_nav-link-hover-bg: var(--content-docs-nav-link-hover-bg, var(--_link-hover-bg));
|
|
195
|
+
--_nav-link-hover-color: var(--content-docs-nav-link-hover-color, var(--_link-hover-color));
|
|
196
|
+
--_nav-link-active-bg: var(--content-docs-nav-link-active-bg, var(--_link-active-bg));
|
|
197
|
+
--_nav-link-active-color: var(--content-docs-nav-link-active-color, var(--_link-active-color));
|
|
198
|
+
|
|
199
|
+
--_page-nav-link-color: var(--content-docs-page-nav-link-color, var(--_link-color));
|
|
200
|
+
--_page-nav-link-hover-bg: var(--content-docs-page-nav-link-hover-bg, var(--_link-hover-bg));
|
|
201
|
+
--_page-nav-link-hover-color: var(--content-docs-page-nav-link-hover-color, var(--_link-hover-color));
|
|
202
|
+
--_page-nav-link-active-bg: var(--content-docs-page-nav-link-active-bg, var(--_link-active-bg));
|
|
203
|
+
--_page-nav-link-active-color: var(--content-docs-page-nav-link-active-color, var(--_link-active-color));
|
|
204
|
+
|
|
205
|
+
container-type: inline-size;
|
|
206
|
+
container-name: contentDocs;
|
|
207
|
+
|
|
208
|
+
.content-docs-inner {
|
|
209
|
+
display: grid;
|
|
210
|
+
grid-template-areas:
|
|
211
|
+
"docsNav"
|
|
212
|
+
"docsPageNav"
|
|
213
|
+
"docsContent";
|
|
214
|
+
gap: 1.6rem;
|
|
215
|
+
align-items: start;
|
|
216
|
+
|
|
217
|
+
@container contentDocs (width >= 768px) {
|
|
218
|
+
grid-template-areas:
|
|
219
|
+
"docsNav docsPageNav"
|
|
220
|
+
"docsContent docsPageNav";
|
|
221
|
+
gap: 1.6rem;
|
|
222
|
+
grid-template-columns: 1fr var(--_page-nav-column-width-tablet);
|
|
223
|
+
grid-template-rows: auto 1fr;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@container contentDocs (width >= 1024px) {
|
|
227
|
+
grid-template-areas: "docsNav docsContent docsPageNav";
|
|
228
|
+
gap: 1.6rem;
|
|
229
|
+
grid-template-columns: var(--_nav-column-width) 1fr var(--_page-nav-column-width);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.docs-nav {
|
|
233
|
+
grid-area: docsNav;
|
|
234
|
+
align-items: start;
|
|
235
|
+
|
|
236
|
+
@container contentDocs (width >= 1024px) {
|
|
237
|
+
.expanding-panel {
|
|
238
|
+
& .expanding-panel-details {
|
|
239
|
+
.expanding-panel-summary {
|
|
240
|
+
cursor: default;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
.docs-content {
|
|
247
|
+
grid-area: docsContent;
|
|
248
|
+
align-items: start;
|
|
249
|
+
}
|
|
250
|
+
.docs-page-nav {
|
|
251
|
+
grid-area: docsPageNav;
|
|
252
|
+
align-items: start;
|
|
253
|
+
|
|
254
|
+
@container contentDocs (width >= 768px) {
|
|
255
|
+
.expanding-panel {
|
|
256
|
+
& .expanding-panel-details {
|
|
257
|
+
.expanding-panel-summary {
|
|
258
|
+
cursor: default;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.docs-nav-heading {
|
|
267
|
+
font-size: var(--_nav-heading-font-size);
|
|
268
|
+
font-weight: var(--_nav-heading-font-weight);
|
|
269
|
+
color: var(--_nav-heading-color);
|
|
270
|
+
background-color: var(--_nav-heading-bg);
|
|
271
|
+
margin: var(--_nav-heading-margin);
|
|
272
|
+
padding-block: var(--_nav-heading-padding-block);
|
|
273
|
+
padding-inline: var(--_nav-heading-padding-inline);
|
|
274
|
+
}
|
|
275
|
+
.docs-page-nav-heading {
|
|
276
|
+
font-size: var(--_page-nav-heading-font-size);
|
|
277
|
+
font-weight: var(--_page-nav-heading-font-weight);
|
|
278
|
+
color: var(--_page-nav-heading-color);
|
|
279
|
+
background-color: var(--_page-nav-heading-bg);
|
|
280
|
+
margin: var(--_page-nav-heading-margin);
|
|
281
|
+
padding-block: var(--_page-nav-heading-padding-block);
|
|
282
|
+
padding-inline: var(--_page-nav-heading-padding-inline);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.docs-nav-list {
|
|
286
|
+
border-radius: var(--_panel-border-radius);
|
|
287
|
+
padding-block: var(--_panel-padding-block);
|
|
288
|
+
padding-inline: var(--_panel-padding-inline);
|
|
289
|
+
background-color: var(--_nav-panel-bg);
|
|
290
|
+
|
|
291
|
+
ul {
|
|
292
|
+
list-style: none;
|
|
293
|
+
margin: 0;
|
|
294
|
+
padding: 0;
|
|
295
|
+
display: flex;
|
|
296
|
+
flex-direction: column;
|
|
297
|
+
gap: 0.4rem;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
.docs-page-nav-list {
|
|
301
|
+
border-radius: var(--_panel-border-radius);
|
|
302
|
+
padding-block: var(--_panel-padding-block);
|
|
303
|
+
padding-inline: var(--_panel-padding-inline);
|
|
304
|
+
background-color: var(--_page-nav-panel-bg);
|
|
305
|
+
|
|
306
|
+
ul {
|
|
307
|
+
list-style: none;
|
|
308
|
+
margin: 0;
|
|
309
|
+
padding: 0;
|
|
310
|
+
display: flex;
|
|
311
|
+
flex-direction: column;
|
|
312
|
+
gap: 0.4rem;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/* Grid (not flex) so the label column stays aligned at a fixed offset whether or
|
|
317
|
+
not an item has an icon — an icon-less item's label still starts in column 2,
|
|
318
|
+
instead of collapsing back to column 1 like it would with flex.
|
|
319
|
+
|
|
320
|
+
Icon at the end: set --docs-nav-link-icon-order to rtl. This mirrors which
|
|
321
|
+
physical side each grid column renders on without touching column sizing —
|
|
322
|
+
swapping grid-column values directly would leave the label squeezed into the
|
|
323
|
+
icon-sized track instead. The label/icon direction resets further down undo
|
|
324
|
+
the mirroring for their own content so text and icon glyphs don't visually flip. */
|
|
325
|
+
.docs-nav-link {
|
|
326
|
+
display: grid;
|
|
327
|
+
grid-template-columns: var(--docs-nav-link-icon-size, 1.6rem) 1fr;
|
|
328
|
+
align-items: center;
|
|
329
|
+
gap: var(--docs-nav-link-icon-gap, 0.6rem);
|
|
330
|
+
border-radius: var(--_link-border-radius);
|
|
331
|
+
font-size: var(--_link-font-size);
|
|
332
|
+
padding-block: var(--_link-padding-block);
|
|
333
|
+
padding-inline: var(--_link-padding-inline);
|
|
334
|
+
margin-block: var(--_link-margin-block);
|
|
335
|
+
background-color: var(--_link-bg);
|
|
336
|
+
text-decoration: none;
|
|
337
|
+
direction: var(--docs-nav-link-icon-order, ltr);
|
|
338
|
+
color: var(--_nav-link-color);
|
|
339
|
+
transition:
|
|
340
|
+
background-color var(--control-transition-duration, 200ms) var(--control-transition-ease, ease),
|
|
341
|
+
color var(--control-transition-duration, 200ms) var(--control-transition-ease, ease);
|
|
342
|
+
|
|
343
|
+
&:focus-visible {
|
|
344
|
+
outline: 0.2rem solid var(--theme-ring, currentcolor);
|
|
345
|
+
outline-offset: -0.2rem;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
&:hover {
|
|
349
|
+
background-color: var(--_nav-link-hover-bg);
|
|
350
|
+
color: var(--_nav-link-hover-color);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
&.is-active {
|
|
354
|
+
background-color: var(--_nav-link-active-bg);
|
|
355
|
+
color: var(--_nav-link-active-color);
|
|
356
|
+
font-weight: 600;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
.docs-page-nav-link {
|
|
360
|
+
display: grid;
|
|
361
|
+
grid-template-columns: var(--docs-nav-link-icon-size, 1.6rem) 1fr;
|
|
362
|
+
align-items: center;
|
|
363
|
+
gap: var(--docs-nav-link-icon-gap, 0.6rem);
|
|
364
|
+
border-radius: var(--_link-border-radius);
|
|
365
|
+
font-size: var(--_link-font-size);
|
|
366
|
+
padding-block: var(--_link-padding-block);
|
|
367
|
+
padding-inline: var(--_link-padding-inline);
|
|
368
|
+
margin-block: var(--_link-margin-block);
|
|
369
|
+
background-color: var(--_link-bg);
|
|
370
|
+
text-decoration: none;
|
|
371
|
+
direction: var(--docs-page-nav-link-icon-order, ltr);
|
|
372
|
+
color: var(--_page-nav-link-color);
|
|
373
|
+
transition:
|
|
374
|
+
background-color var(--control-transition-duration, 200ms) var(--control-transition-ease, ease),
|
|
375
|
+
color var(--control-transition-duration, 200ms) var(--control-transition-ease, ease);
|
|
376
|
+
|
|
377
|
+
&:focus-visible {
|
|
378
|
+
outline: 0.2rem solid var(--theme-ring, currentcolor);
|
|
379
|
+
outline-offset: -0.2rem;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
&:hover {
|
|
383
|
+
background-color: var(--_page-nav-link-hover-bg);
|
|
384
|
+
color: var(--_page-nav-link-hover-color);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
&.is-active {
|
|
388
|
+
background-color: var(--_page-nav-link-active-bg);
|
|
389
|
+
color: var(--_page-nav-link-active-color);
|
|
390
|
+
font-weight: 600;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.docs-nav-link-icon,
|
|
395
|
+
.docs-page-nav-link-icon {
|
|
396
|
+
grid-column: 1;
|
|
397
|
+
direction: ltr;
|
|
398
|
+
flex-shrink: 0;
|
|
399
|
+
width: var(--docs-nav-link-icon-size, 1.6rem);
|
|
400
|
+
height: var(--docs-nav-link-icon-size, 1.6rem);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.docs-nav-link-label,
|
|
404
|
+
.docs-page-nav-link-label {
|
|
405
|
+
grid-column: 2;
|
|
406
|
+
direction: ltr;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
</style>
|
package/app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { test, expect, type Page } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
// --- Config ---
|
|
4
|
+
const STORYBOOK_URL = "http://127.0.0.1:6006";
|
|
5
|
+
const STORY_BASE = "atoms-content-wrappers-contentdocs";
|
|
6
|
+
const ELEMENT_TIMEOUT = 15_000;
|
|
7
|
+
|
|
8
|
+
// --- Helpers ---
|
|
9
|
+
const getStoryUrl = (story: string): string => `${STORYBOOK_URL}/?path=/story/${STORY_BASE}--${story}`;
|
|
10
|
+
|
|
11
|
+
const getElement = async (page: Page, story: string, viewport?: { width: number; height: number }) => {
|
|
12
|
+
if (viewport) await page.setViewportSize(viewport);
|
|
13
|
+
await page.goto(getStoryUrl(story), { waitUntil: "load" });
|
|
14
|
+
const frame = page.frameLocator("#storybook-preview-iframe");
|
|
15
|
+
await frame.locator("#storybook-root > *").waitFor({ state: "visible", timeout: ELEMENT_TIMEOUT });
|
|
16
|
+
const el = frame.locator(".content-docs");
|
|
17
|
+
await el.waitFor({ state: "visible", timeout: ELEMENT_TIMEOUT });
|
|
18
|
+
return el;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// -------------------------
|
|
22
|
+
// Baseline (default story, default args)
|
|
23
|
+
// -------------------------
|
|
24
|
+
test.describe("ContentDocs — baseline", () => {
|
|
25
|
+
test("default matches snapshot", async ({ page }) => {
|
|
26
|
+
const el = await getElement(page, "default");
|
|
27
|
+
await expect(el).toHaveScreenshot("default.png");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// -------------------------
|
|
32
|
+
// Breakpoint states — docsNav (toggleable) and docsPageNav (forceOpened) summary
|
|
33
|
+
// rows must render at the same height so their content columns start aligned.
|
|
34
|
+
// See ExpandingPanel's icon-wrapper--hidden fix: the icon-wrapper is always kept
|
|
35
|
+
// in the DOM (not v-if'd away) so a forceOpened panel's row doesn't shrink relative
|
|
36
|
+
// to a toggleable sibling's.
|
|
37
|
+
// -------------------------
|
|
38
|
+
test.describe("ContentDocs — breakpoint states", () => {
|
|
39
|
+
test("tablet: docsNav toggleable + docsPageNav forceOpened stay row-aligned", async ({ page }) => {
|
|
40
|
+
const el = await getElement(page, "default", { width: 1300, height: 700 });
|
|
41
|
+
await expect(el).toHaveScreenshot("state-tablet.png");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("desktop: both panels forceOpened", async ({ page }) => {
|
|
45
|
+
const el = await getElement(page, "default", { width: 1800, height: 700 });
|
|
46
|
+
await expect(el).toHaveScreenshot("state-desktop.png");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("mobile: both panels toggleable, collapsed by default", async ({ page }) => {
|
|
50
|
+
const el = await getElement(page, "default", { width: 500, height: 900 });
|
|
51
|
+
await expect(el).toHaveScreenshot("state-mobile.png");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|