srcdev-nuxt-components 9.2.5 → 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/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/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/tests/PricingCard.spec.ts +2 -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,91 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { nextTick, ref } from "vue";
|
|
3
|
+
import { useContainerBreakpoints } from "../useContainerBreakpoints";
|
|
4
|
+
|
|
5
|
+
let resizeCallback: ResizeObserverCallback | null = null;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
resizeCallback = null;
|
|
9
|
+
vi.stubGlobal(
|
|
10
|
+
"ResizeObserver",
|
|
11
|
+
vi.fn((callback: ResizeObserverCallback) => {
|
|
12
|
+
resizeCallback = callback;
|
|
13
|
+
return {
|
|
14
|
+
observe: vi.fn(),
|
|
15
|
+
unobserve: vi.fn(),
|
|
16
|
+
disconnect: vi.fn(),
|
|
17
|
+
};
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function triggerResize(el: HTMLElement, width: number) {
|
|
23
|
+
Object.defineProperty(el, "offsetWidth", { value: width, configurable: true });
|
|
24
|
+
resizeCallback?.(
|
|
25
|
+
[{ contentRect: { width, height: 0 } } as unknown as ResizeObserverEntry],
|
|
26
|
+
{} as ResizeObserver
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("useContainerBreakpoints", () => {
|
|
31
|
+
it("defaults active to base when narrower than the smallest breakpoint", () => {
|
|
32
|
+
const el = ref<HTMLElement | null>(document.createElement("div"));
|
|
33
|
+
const { active } = useContainerBreakpoints(undefined, el);
|
|
34
|
+
expect(active.value).toBe("base");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("updates active as the observed element's width crosses breakpoints", async () => {
|
|
38
|
+
const el = ref<HTMLElement | null>(document.createElement("div"));
|
|
39
|
+
const { active } = useContainerBreakpoints(undefined, el);
|
|
40
|
+
|
|
41
|
+
triggerResize(el.value as HTMLElement, 700);
|
|
42
|
+
await nextTick();
|
|
43
|
+
expect(active.value).toBe("sm");
|
|
44
|
+
|
|
45
|
+
triggerResize(el.value as HTMLElement, 1300);
|
|
46
|
+
await nextTick();
|
|
47
|
+
expect(active.value).toBe("xl");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("greaterOrEqual/smaller reflect the current width against a named breakpoint", async () => {
|
|
51
|
+
const el = ref<HTMLElement | null>(document.createElement("div"));
|
|
52
|
+
const { greaterOrEqual, smaller } = useContainerBreakpoints(undefined, el);
|
|
53
|
+
const isLg = greaterOrEqual("lg");
|
|
54
|
+
const isSmallerThanMd = smaller("md");
|
|
55
|
+
|
|
56
|
+
expect(isLg.value).toBe(false);
|
|
57
|
+
expect(isSmallerThanMd.value).toBe(true);
|
|
58
|
+
|
|
59
|
+
triggerResize(el.value as HTMLElement, 1100);
|
|
60
|
+
await nextTick();
|
|
61
|
+
expect(isLg.value).toBe(true);
|
|
62
|
+
expect(isSmallerThanMd.value).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("between returns true only within the [min, max) range", async () => {
|
|
66
|
+
const el = ref<HTMLElement | null>(document.createElement("div"));
|
|
67
|
+
const { between } = useContainerBreakpoints(undefined, el);
|
|
68
|
+
const isTablet = between("sm", "lg");
|
|
69
|
+
|
|
70
|
+
triggerResize(el.value as HTMLElement, 800);
|
|
71
|
+
await nextTick();
|
|
72
|
+
expect(isTablet.value).toBe(true);
|
|
73
|
+
|
|
74
|
+
triggerResize(el.value as HTMLElement, 1024);
|
|
75
|
+
await nextTick();
|
|
76
|
+
expect(isTablet.value).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("supports a custom breakpoints map", async () => {
|
|
80
|
+
const el = ref<HTMLElement | null>(document.createElement("div"));
|
|
81
|
+
const { active } = useContainerBreakpoints({ narrow: 400, wide: 900 }, el);
|
|
82
|
+
|
|
83
|
+
triggerResize(el.value as HTMLElement, 500);
|
|
84
|
+
await nextTick();
|
|
85
|
+
expect(active.value).toBe("narrow");
|
|
86
|
+
|
|
87
|
+
triggerResize(el.value as HTMLElement, 950);
|
|
88
|
+
await nextTick();
|
|
89
|
+
expect(active.value).toBe("wide");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Ref } from "vue";
|
|
2
|
+
import { useElementSize } from "@vueuse/core";
|
|
3
|
+
|
|
4
|
+
export const containerBreakpointsDefault = {
|
|
5
|
+
sm: 640,
|
|
6
|
+
md: 768,
|
|
7
|
+
lg: 1024,
|
|
8
|
+
xl: 1280,
|
|
9
|
+
"2xl": 1536,
|
|
10
|
+
"4k": 2560,
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
export type ContainerBreakpointName = keyof typeof containerBreakpointsDefault;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* useContainerBreakpoints composable
|
|
17
|
+
* Container-query equivalent of VueUse's useBreakpoints — tracks an element's
|
|
18
|
+
* own width via ResizeObserver instead of the viewport, for layouts where
|
|
19
|
+
* page decoration (nav, sidebars) means viewport width != available width.
|
|
20
|
+
* @param breakpoints - map of breakpoint name to min-width in px (default: containerBreakpointsDefault)
|
|
21
|
+
* @param target - optional existing element ref to observe; creates its own if omitted
|
|
22
|
+
* @returns { el, width, active, greater, greaterOrEqual, smaller, smallerOrEqual, between }
|
|
23
|
+
*/
|
|
24
|
+
export function useContainerBreakpoints<K extends string = ContainerBreakpointName>(
|
|
25
|
+
breakpoints: Record<K, number> = containerBreakpointsDefault as unknown as Record<K, number>,
|
|
26
|
+
target?: Ref<HTMLElement | null>
|
|
27
|
+
) {
|
|
28
|
+
const el = target ?? ref<HTMLElement | null>(null);
|
|
29
|
+
const { width } = useElementSize(el);
|
|
30
|
+
|
|
31
|
+
const sortedEntries = (Object.entries(breakpoints) as [K, number][]).sort((a, b) => a[1] - b[1]);
|
|
32
|
+
|
|
33
|
+
function greaterOrEqual(name: K) {
|
|
34
|
+
return computed(() => width.value >= breakpoints[name]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function greater(name: K) {
|
|
38
|
+
return computed(() => width.value > breakpoints[name]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function smaller(name: K) {
|
|
42
|
+
return computed(() => width.value < breakpoints[name]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function smallerOrEqual(name: K) {
|
|
46
|
+
return computed(() => width.value <= breakpoints[name]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function between(min: K, max: K) {
|
|
50
|
+
return computed(() => width.value >= breakpoints[min] && width.value < breakpoints[max]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const active = computed<K | "base">(() => {
|
|
54
|
+
let current: K | "base" = "base";
|
|
55
|
+
for (const [name, minWidth] of sortedEntries) {
|
|
56
|
+
if (width.value >= minWidth) current = name;
|
|
57
|
+
}
|
|
58
|
+
return current;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
el,
|
|
63
|
+
width,
|
|
64
|
+
active,
|
|
65
|
+
greater,
|
|
66
|
+
greaterOrEqual,
|
|
67
|
+
smaller,
|
|
68
|
+
smallerOrEqual,
|
|
69
|
+
between,
|
|
70
|
+
};
|
|
71
|
+
}
|
package/app/layouts/default.vue
CHANGED
|
@@ -110,6 +110,7 @@ const responsiveNavLinks = {
|
|
|
110
110
|
{ name: "Layout Grid A", path: "/ui/layout-grid-a" },
|
|
111
111
|
{ name: "Layout Grid B", path: "/ui/layout-grid-b" },
|
|
112
112
|
{ name: "Simple Grid", path: "/ui/simple-grid" },
|
|
113
|
+
{ name: "Layout Content Docs", path: "/ui/layout-content-docs" },
|
|
113
114
|
{ name: "Masonry Grid Simple", path: "/ui/masonry-grid" },
|
|
114
115
|
{ name: "Masonry Grid Sorted", path: "/ui/masonry-grid-sorted" },
|
|
115
116
|
{ name: "Masonry Grid Ordered", path: "/ui/masonry-grid-ordered" },
|
|
@@ -2,134 +2,142 @@
|
|
|
2
2
|
<div>
|
|
3
3
|
<NuxtLayout name="default">
|
|
4
4
|
<template #layout-content>
|
|
5
|
-
<PageRow tag="div" variant="
|
|
6
|
-
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
</template>
|
|
34
|
-
<template #content>
|
|
35
|
-
<div>
|
|
36
|
-
<p class="mt-0">Details content</p>
|
|
37
|
-
<p>Details content</p>
|
|
38
|
-
<p>Details content</p>
|
|
39
|
-
<p>Details content</p>
|
|
40
|
-
<p>Details content</p>
|
|
41
|
-
</div>
|
|
42
|
-
</template>
|
|
43
|
-
</ExpandingPanel>
|
|
44
|
-
</PageRow>
|
|
5
|
+
<PageRow tag="div" variant="content" :style-class-passthrough="['expanding-panel-section', 'mb-20']">
|
|
6
|
+
<!-- ── QA Panel ─────────────────────────────────────────────── -->
|
|
7
|
+
<div class="qa-panel">
|
|
8
|
+
<details class="qa-panel__details" open>
|
|
9
|
+
<summary class="qa-panel__summary">
|
|
10
|
+
<span class="qa-panel__title">QA — ExpandingPanel</span>
|
|
11
|
+
<code class="qa-panel__status">
|
|
12
|
+
duration:{{ qaAnimationDuration }}ms · forceOpened:{{ qaForceOpened }} · contentIsOnTop:{{
|
|
13
|
+
qaContentIsOnTop
|
|
14
|
+
}}
|
|
15
|
+
· linked:{{ qaLinked }}
|
|
16
|
+
</code>
|
|
17
|
+
</summary>
|
|
18
|
+
<div class="qa-panel__body">
|
|
19
|
+
<div class="qa-panel__group">
|
|
20
|
+
<span class="qa-panel__label">Animation Duration</span>
|
|
21
|
+
<div class="qa-panel__chips">
|
|
22
|
+
<button
|
|
23
|
+
v-for="d in animationDurations"
|
|
24
|
+
:key="d"
|
|
25
|
+
class="qa-panel__chip"
|
|
26
|
+
:class="{ 'is-active': qaAnimationDuration === d }"
|
|
27
|
+
@click="qaAnimationDuration = d"
|
|
28
|
+
>
|
|
29
|
+
{{ d }}ms
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
45
33
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
34
|
+
<div class="qa-panel__group">
|
|
35
|
+
<span class="qa-panel__label">Force Opened</span>
|
|
36
|
+
<div class="qa-panel__chips">
|
|
37
|
+
<button
|
|
38
|
+
class="qa-panel__chip"
|
|
39
|
+
:class="{ 'is-active': !qaForceOpened }"
|
|
40
|
+
@click="qaForceOpened = false"
|
|
41
|
+
>
|
|
42
|
+
off
|
|
43
|
+
</button>
|
|
44
|
+
<button
|
|
45
|
+
class="qa-panel__chip"
|
|
46
|
+
:class="{ 'is-active': qaForceOpened }"
|
|
47
|
+
@click="qaForceOpened = true"
|
|
48
|
+
>
|
|
49
|
+
on
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div class="qa-panel__group">
|
|
55
|
+
<span class="qa-panel__label">Content Is On Top</span>
|
|
56
|
+
<div class="qa-panel__chips">
|
|
57
|
+
<button
|
|
58
|
+
class="qa-panel__chip"
|
|
59
|
+
:class="{ 'is-active': !qaContentIsOnTop }"
|
|
60
|
+
@click="qaContentIsOnTop = false"
|
|
61
|
+
>
|
|
62
|
+
off
|
|
63
|
+
</button>
|
|
64
|
+
<button
|
|
65
|
+
class="qa-panel__chip"
|
|
66
|
+
:class="{ 'is-active': qaContentIsOnTop }"
|
|
67
|
+
@click="qaContentIsOnTop = true"
|
|
68
|
+
>
|
|
69
|
+
on
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="qa-panel__group">
|
|
75
|
+
<span class="qa-panel__label">Linked (shared name)</span>
|
|
76
|
+
<div class="qa-panel__chips">
|
|
77
|
+
<button class="qa-panel__chip" :class="{ 'is-active': !qaLinked }" @click="qaLinked = false">
|
|
78
|
+
off
|
|
79
|
+
</button>
|
|
80
|
+
<button class="qa-panel__chip" :class="{ 'is-active': qaLinked }" @click="qaLinked = true">
|
|
81
|
+
on
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
92
85
|
</div>
|
|
93
|
-
</
|
|
94
|
-
</
|
|
86
|
+
</details>
|
|
87
|
+
</div>
|
|
95
88
|
</PageRow>
|
|
96
89
|
|
|
97
|
-
<PageRow tag="div" variant="
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
</
|
|
121
|
-
</
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
90
|
+
<PageRow tag="div" variant="content" :style-class-passthrough="['qa-panel__preview']">
|
|
91
|
+
<section class="qa-panel__preview">
|
|
92
|
+
<h2 class="page-heading-3">Interactive preview</h2>
|
|
93
|
+
<div style="position: relative">
|
|
94
|
+
<ExpandingPanel
|
|
95
|
+
v-model="qaOpen1"
|
|
96
|
+
:name="qaPanel1Name"
|
|
97
|
+
:animation-duration="qaAnimationDuration"
|
|
98
|
+
:force-opened="qaForceOpened"
|
|
99
|
+
:content-is-on-top="qaContentIsOnTop"
|
|
100
|
+
:style-class-passthrough="['qa-preview-panel']"
|
|
101
|
+
>
|
|
102
|
+
<template #summary>
|
|
103
|
+
<h3 class="page-heading-3 mb-2">Interactive preview panel 1</h3>
|
|
104
|
+
</template>
|
|
105
|
+
<template #content>
|
|
106
|
+
<!-- Wrapper INSIDE the slot carries visual styling — never .inner itself,
|
|
107
|
+
see .claude/skills/components/expanding-panel.md for why -->
|
|
108
|
+
<div class="qa-preview-panel-body">
|
|
109
|
+
<p class="mt-0">Content driven by the controls above.</p>
|
|
110
|
+
<p class="mb-0">
|
|
111
|
+
Toggle "Content Is On Top" to see it overlay the text below instead of pushing it down.
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
</template>
|
|
115
|
+
</ExpandingPanel>
|
|
116
|
+
|
|
117
|
+
<!-- When "Linked" is on, this shares its name with panel 1 above, grouping them
|
|
118
|
+
into a native accordion (only one open at a time). -->
|
|
119
|
+
<ExpandingPanel
|
|
120
|
+
v-model="qaOpen2"
|
|
121
|
+
:name="qaPanel2Name"
|
|
122
|
+
:animation-duration="qaAnimationDuration"
|
|
123
|
+
:force-opened="qaForceOpened"
|
|
124
|
+
:content-is-on-top="qaContentIsOnTop"
|
|
125
|
+
:style-class-passthrough="['qa-preview-panel']"
|
|
126
|
+
>
|
|
127
|
+
<template #summary>
|
|
128
|
+
<h3 class="page-heading-3 mb-2">Interactive preview panel 2</h3>
|
|
129
|
+
</template>
|
|
130
|
+
<template #content>
|
|
131
|
+
<div class="qa-preview-panel-body">
|
|
132
|
+
<p class="mt-0">When "Linked" is on, opening this panel closes panel 1, and vice versa.</p>
|
|
133
|
+
<p class="mb-0">Also driven by the controls above.</p>
|
|
134
|
+
</div>
|
|
135
|
+
</template>
|
|
136
|
+
</ExpandingPanel>
|
|
137
|
+
|
|
138
|
+
<p class="mbs-16">Page content below the preview panel — stays in place when contentIsOnTop is on.</p>
|
|
139
|
+
</div>
|
|
140
|
+
</section>
|
|
133
141
|
</PageRow>
|
|
134
142
|
</template>
|
|
135
143
|
</NuxtLayout>
|
|
@@ -149,14 +157,32 @@ useHead({
|
|
|
149
157
|
content: "Meta description content",
|
|
150
158
|
},
|
|
151
159
|
],
|
|
160
|
+
bodyAttrs: {
|
|
161
|
+
class: "ui-expanding-panel-page",
|
|
162
|
+
},
|
|
152
163
|
});
|
|
153
164
|
|
|
154
|
-
const
|
|
155
|
-
|
|
165
|
+
const animationDurations = [0, 300, 800] as const;
|
|
166
|
+
type AnimationDuration = (typeof animationDurations)[number];
|
|
156
167
|
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
const qaAnimationDuration = ref<AnimationDuration>(300);
|
|
169
|
+
const qaForceOpened = ref(false);
|
|
170
|
+
const qaContentIsOnTop = ref(false);
|
|
171
|
+
const qaLinked = ref(false);
|
|
172
|
+
const qaOpen1 = ref(false);
|
|
173
|
+
const qaOpen2 = ref(false);
|
|
174
|
+
|
|
175
|
+
const qaPanel1Name = "qa-preview-1";
|
|
176
|
+
const qaPanel2Name = computed(() => (qaLinked.value ? qaPanel1Name : "qa-preview-2"));
|
|
177
|
+
|
|
178
|
+
// forceOpened only forces panels *open*; a panel's own isPanelOpen state is left untouched
|
|
179
|
+
// by the component (see expanding-panel skill notes), so toggling forceOpened off can leave
|
|
180
|
+
// a panel open or closed depending on hidden prior clicks. Reset both panels to a known,
|
|
181
|
+
// visible state on every toggle so the QA control always produces an obvious response.
|
|
182
|
+
watch(qaForceOpened, () => {
|
|
183
|
+
qaOpen1.value = false;
|
|
184
|
+
qaOpen2.value = false;
|
|
185
|
+
});
|
|
160
186
|
</script>
|
|
161
187
|
|
|
162
188
|
<style lang="css">
|
|
@@ -186,4 +212,115 @@ const closePanel = () => {
|
|
|
186
212
|
}
|
|
187
213
|
}
|
|
188
214
|
}
|
|
215
|
+
|
|
216
|
+
.ui-expanding-panel-page {
|
|
217
|
+
.qa-panel {
|
|
218
|
+
background: oklch(15% 0 0);
|
|
219
|
+
color: white;
|
|
220
|
+
font-size: 1.3rem;
|
|
221
|
+
margin-block-end: 2rem;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.qa-panel__details {
|
|
225
|
+
padding: 1rem 2rem;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.qa-panel__summary {
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
display: flex;
|
|
231
|
+
align-items: center;
|
|
232
|
+
gap: 1.6rem;
|
|
233
|
+
list-style: none;
|
|
234
|
+
user-select: none;
|
|
235
|
+
|
|
236
|
+
&::-webkit-details-marker {
|
|
237
|
+
display: none;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.qa-panel__title {
|
|
242
|
+
font-weight: 600;
|
|
243
|
+
font-size: 1.1rem;
|
|
244
|
+
text-transform: uppercase;
|
|
245
|
+
letter-spacing: 0.08em;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.qa-panel__status {
|
|
249
|
+
font-family: monospace;
|
|
250
|
+
font-size: 1.2rem;
|
|
251
|
+
background: oklch(0% 0 0 / 0.3);
|
|
252
|
+
padding: 0.2rem 0.8rem;
|
|
253
|
+
border-radius: 0.4rem;
|
|
254
|
+
user-select: text;
|
|
255
|
+
cursor: text;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.qa-panel__body {
|
|
259
|
+
display: flex;
|
|
260
|
+
flex-wrap: wrap;
|
|
261
|
+
gap: 2.4rem;
|
|
262
|
+
padding-block: 1.2rem 0.4rem;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.qa-panel__group {
|
|
266
|
+
display: flex;
|
|
267
|
+
flex-direction: column;
|
|
268
|
+
gap: 0.6rem;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.qa-panel__label {
|
|
272
|
+
font-size: 1.1rem;
|
|
273
|
+
text-transform: uppercase;
|
|
274
|
+
letter-spacing: 0.08em;
|
|
275
|
+
opacity: 0.55;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.qa-panel__chips {
|
|
279
|
+
display: flex;
|
|
280
|
+
flex-wrap: wrap;
|
|
281
|
+
gap: 0.4rem;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.qa-panel__chip {
|
|
285
|
+
font-family: monospace;
|
|
286
|
+
font-size: 1.2rem;
|
|
287
|
+
color: white;
|
|
288
|
+
background: oklch(0% 0 0 / 0.25);
|
|
289
|
+
border: 1px solid oklch(100% 0 0 / 0.18);
|
|
290
|
+
padding: 0.3rem 1rem;
|
|
291
|
+
border-radius: 0.4rem;
|
|
292
|
+
cursor: pointer;
|
|
293
|
+
transition: background 0.15s;
|
|
294
|
+
|
|
295
|
+
&:hover {
|
|
296
|
+
background: oklch(0% 0 0 / 0.4);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
&.is-active {
|
|
300
|
+
background: oklch(55% 0.18 240);
|
|
301
|
+
border-color: oklch(55% 0.18 240);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.qa-panel__preview {
|
|
306
|
+
padding: 0 2rem 2rem;
|
|
307
|
+
|
|
308
|
+
.expanding-panel.qa-preview-panel {
|
|
309
|
+
--expanding-panel-content-z-index: 20;
|
|
310
|
+
|
|
311
|
+
+ .expanding-panel.qa-preview-panel {
|
|
312
|
+
margin-block-start: 1rem;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.qa-preview-panel-body {
|
|
317
|
+
background-color: white;
|
|
318
|
+
color: black;
|
|
319
|
+
border: 1px solid oklch(0% 0 0 / 0.15);
|
|
320
|
+
border-radius: 0.4rem;
|
|
321
|
+
padding: 1rem;
|
|
322
|
+
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
189
326
|
</style>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<NuxtLayout name="default">
|
|
4
|
+
<template #layout-content>
|
|
5
|
+
<PageRow tag="div" variant="content" :style-class-passthrough="['mbe-20']">
|
|
6
|
+
<h1 class="page-heading-2">Layout Content Docs</h1>
|
|
7
|
+
|
|
8
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
9
|
+
</PageRow>
|
|
10
|
+
|
|
11
|
+
<PageRow tag="div" variant="content" :style-class-passthrough="['mbe-20']">
|
|
12
|
+
<div :class="[canvasName]">
|
|
13
|
+
<ContentDocs
|
|
14
|
+
v-model:active-nav-item="activeNavItem"
|
|
15
|
+
v-model:active-page-nav-item="activePageNavItem"
|
|
16
|
+
:docs-nav-items="docsNavItems"
|
|
17
|
+
:docs-page-nav-items="docsPageNavItems"
|
|
18
|
+
>
|
|
19
|
+
<template #docsContent>
|
|
20
|
+
<h3 class="page-heading-3">Docs Content</h3>
|
|
21
|
+
<p>
|
|
22
|
+
Mi nibh quisque taciti porta curabitur nostra volutpat. Habitant sodales arcu habitasse mi duis
|
|
23
|
+
conubia leo lacinia. Montes torquent sodales adipiscing; proin semper feugiat morbi ullamcorper
|
|
24
|
+
praesent. Arcu luctus tempor quam ligula vestibulum sapien faucibus ridiculus. Cursus consequat
|
|
25
|
+
ultricies consectetur class suscipit quisque convallis eget? Dignissim mattis luctus enim habitant
|
|
26
|
+
porta pretium litora. Parturient montes imperdiet massa; sollicitudin varius hac aptent. Eleifend
|
|
27
|
+
parturient mattis tellus nisi a montes.
|
|
28
|
+
</p>
|
|
29
|
+
</template>
|
|
30
|
+
</ContentDocs>
|
|
31
|
+
</div>
|
|
32
|
+
</PageRow>
|
|
33
|
+
</template>
|
|
34
|
+
</NuxtLayout>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script setup lang="ts">
|
|
39
|
+
import type { MediaCanvas, DocsNavItem } from "~/types/components";
|
|
40
|
+
definePageMeta({
|
|
41
|
+
layout: false,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
useHead({
|
|
45
|
+
title: "UI Layout Content Docs",
|
|
46
|
+
meta: [{ name: "description", content: "Examples of UI Component Layout Content Docs" }],
|
|
47
|
+
bodyAttrs: {
|
|
48
|
+
class: "content-docs-demo-page",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const canvasName = ref<MediaCanvas>("mobileCanvas");
|
|
53
|
+
|
|
54
|
+
const docsNavItems: DocsNavItem[] = [
|
|
55
|
+
{ label: "Getting started", to: "/ui/layout-content-docs", icon: "lucide:rocket" },
|
|
56
|
+
{ label: "Installation", to: "/ui/layout-content-docs#installation", icon: "lucide:download" },
|
|
57
|
+
{ label: "Configuration", to: "/ui/layout-content-docs#configuration" },
|
|
58
|
+
{ label: "Theming", to: "/ui/layout-content-docs#theming", icon: "lucide:palette" },
|
|
59
|
+
{ label: "Accessibility", to: "/ui/layout-content-docs#accessibility" },
|
|
60
|
+
{ label: "Troubleshooting", to: "/ui/layout-content-docs#troubleshooting", icon: "lucide:life-buoy" },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const docsPageNavItems: DocsNavItem[] = [
|
|
64
|
+
{ label: "Overview", to: "/ui/layout-content-docs#overview", icon: "lucide:eye" },
|
|
65
|
+
{ label: "Examples", to: "/ui/layout-content-docs#examples" },
|
|
66
|
+
{ label: "Props", to: "/ui/layout-content-docs#props", icon: "lucide:settings-2" },
|
|
67
|
+
{ label: "Slots", to: "/ui/layout-content-docs#slots" },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const activeNavItem = ref<string | undefined>(docsNavItems[0]?.to);
|
|
71
|
+
const activePageNavItem = ref<string | undefined>(undefined);
|
|
72
|
+
</script>
|