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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<PageRow tag="div" :variant="pageRowVariant" :style-class-passthrough="styleClassPassthrough">
|
|
3
|
+
<template #default>
|
|
4
|
+
<header class="site-header">
|
|
5
|
+
<nav class="home-navigation" aria-label="Home Navigation">
|
|
6
|
+
<SkipLinks>
|
|
7
|
+
<template #homeLink>
|
|
8
|
+
<slot name="branding"></slot>
|
|
9
|
+
</template>
|
|
10
|
+
</SkipLinks>
|
|
11
|
+
</nav>
|
|
12
|
+
<ResponsiveHeader
|
|
13
|
+
:responsive-nav-links="responsiveNavLinks"
|
|
14
|
+
:gap-between-first-and-second-nav="gapBetweenFirstAndSecondNav"
|
|
15
|
+
:overflow-details-summary-icons="overflowDetailsSummaryIcons"
|
|
16
|
+
:collapse-breakpoint="collapseBreakpoint"
|
|
17
|
+
:collapse-at-main-nav-intersection="collapseAtMainNavIntersection"
|
|
18
|
+
:allow-expand-on-gesture="allowExpandOnGesture"
|
|
19
|
+
:style-class-passthrough="navStyleClassPassthrough"
|
|
20
|
+
>
|
|
21
|
+
<template v-if="slots.secondaryNavigation" #secondaryNavigation>
|
|
22
|
+
<slot name="secondaryNavigation"></slot>
|
|
23
|
+
</template>
|
|
24
|
+
</ResponsiveHeader>
|
|
25
|
+
</header>
|
|
26
|
+
</template>
|
|
27
|
+
</PageRow>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import type { ResponsiveHeaderProp } from "../../../types/components";
|
|
32
|
+
import PageRow from "../../01.atoms/page-row/PageRow.vue";
|
|
33
|
+
import SkipLinks from "../../skip-links/SkipLinks.vue";
|
|
34
|
+
import ResponsiveHeader from "../responsive-header/ResponsiveHeader.vue";
|
|
35
|
+
|
|
36
|
+
interface Props {
|
|
37
|
+
responsiveNavLinks?: ResponsiveHeaderProp;
|
|
38
|
+
gapBetweenFirstAndSecondNav?: number;
|
|
39
|
+
overflowDetailsSummaryIcons?: Record<string, string>;
|
|
40
|
+
collapseBreakpoint?: number | null;
|
|
41
|
+
collapseAtMainNavIntersection?: boolean;
|
|
42
|
+
allowExpandOnGesture?: boolean;
|
|
43
|
+
pageRowVariant?: "full" | "popout" | "content" | "inset-content";
|
|
44
|
+
styleClassPassthrough?: string | string[];
|
|
45
|
+
navStyleClassPassthrough?: string | string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
withDefaults(defineProps<Props>(), {
|
|
49
|
+
responsiveNavLinks: () => ({}),
|
|
50
|
+
gapBetweenFirstAndSecondNav: 12,
|
|
51
|
+
overflowDetailsSummaryIcons: () => ({
|
|
52
|
+
more: "gravity-ui:ellipsis",
|
|
53
|
+
burger: "gravity-ui:bars",
|
|
54
|
+
}),
|
|
55
|
+
collapseBreakpoint: null,
|
|
56
|
+
collapseAtMainNavIntersection: false,
|
|
57
|
+
allowExpandOnGesture: true,
|
|
58
|
+
pageRowVariant: "content",
|
|
59
|
+
styleClassPassthrough: () => [],
|
|
60
|
+
navStyleClassPassthrough: () => [],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const slots = useSlots();
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<style lang="css">
|
|
67
|
+
@layer components {
|
|
68
|
+
/* ─── Public CSS tokens ─────────────────────────────────────────────────
|
|
69
|
+
Override on the consumer's scope class (via styleClassPassthrough).
|
|
70
|
+
Nested --responsive-header-* / --overflow-nav-* tokens still apply —
|
|
71
|
+
forward a scope class via navStyleClassPassthrough to reach them.
|
|
72
|
+
|
|
73
|
+
--site-header-gap (default: 2.4rem)
|
|
74
|
+
--site-header-padding-inline (default: 2.4rem)
|
|
75
|
+
--site-header-padding-block (default: 0px)
|
|
76
|
+
--site-header-bg (default: transparent)
|
|
77
|
+
--site-header-position (default: static)
|
|
78
|
+
--site-header-sticky-offset (default: 0px)
|
|
79
|
+
--site-header-z-index (default: 9)
|
|
80
|
+
──────────────────────────────────────────────────────────────────────── */
|
|
81
|
+
|
|
82
|
+
.site-header {
|
|
83
|
+
display: grid;
|
|
84
|
+
grid-template-columns: auto 1fr;
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: var(--site-header-gap, 2.4rem);
|
|
87
|
+
padding-block: var(--site-header-padding-block, 0px);
|
|
88
|
+
padding-inline: var(--site-header-padding-inline, 2.4rem);
|
|
89
|
+
background-color: var(--site-header-bg, transparent);
|
|
90
|
+
position: var(--site-header-position, static);
|
|
91
|
+
top: var(--site-header-sticky-offset, 0px);
|
|
92
|
+
z-index: var(--site-header-z-index, 9);
|
|
93
|
+
width: 100%;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
</style>
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import SiteHeader from "../SiteHeader.vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof SiteHeader> = {
|
|
5
|
+
title: "Organisms/Site Header",
|
|
6
|
+
component: SiteHeader,
|
|
7
|
+
argTypes: {
|
|
8
|
+
responsiveNavLinks: {
|
|
9
|
+
control: "object",
|
|
10
|
+
description: "Nav groups keyed by name — each item is either a link (`path`) or a dropdown (`childLinks`)",
|
|
11
|
+
},
|
|
12
|
+
gapBetweenFirstAndSecondNav: {
|
|
13
|
+
control: { type: "number", min: 0, step: 4 },
|
|
14
|
+
description: "Gap in pixels between the first and second nav groups",
|
|
15
|
+
},
|
|
16
|
+
collapseAtMainNavIntersection: {
|
|
17
|
+
control: "boolean",
|
|
18
|
+
description: "Collapse the main nav into the overflow burger menu once it no longer fits",
|
|
19
|
+
},
|
|
20
|
+
allowExpandOnGesture: {
|
|
21
|
+
control: "boolean",
|
|
22
|
+
description: "Allow the collapsed nav to expand on touch/gesture",
|
|
23
|
+
},
|
|
24
|
+
pageRowVariant: {
|
|
25
|
+
control: { type: "radio" },
|
|
26
|
+
options: ["full", "popout", "content", "inset-content"],
|
|
27
|
+
description: "Variant forwarded to the root PageRow",
|
|
28
|
+
},
|
|
29
|
+
styleClassPassthrough: {
|
|
30
|
+
control: "object",
|
|
31
|
+
description: "Extra CSS classes applied to the root PageRow (targets --site-header-* tokens)",
|
|
32
|
+
},
|
|
33
|
+
navStyleClassPassthrough: {
|
|
34
|
+
control: "object",
|
|
35
|
+
description: "Extra CSS classes applied to the nested ResponsiveHeader (targets its own token API)",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
args: {
|
|
39
|
+
pageRowVariant: "content",
|
|
40
|
+
gapBetweenFirstAndSecondNav: 12,
|
|
41
|
+
collapseAtMainNavIntersection: false,
|
|
42
|
+
allowExpandOnGesture: true,
|
|
43
|
+
styleClassPassthrough: [],
|
|
44
|
+
navStyleClassPassthrough: [],
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default meta;
|
|
49
|
+
type Story = StoryObj<typeof SiteHeader>;
|
|
50
|
+
|
|
51
|
+
export const Default: Story = {
|
|
52
|
+
args: {
|
|
53
|
+
responsiveNavLinks: {
|
|
54
|
+
firstNav: [
|
|
55
|
+
{
|
|
56
|
+
name: "Components",
|
|
57
|
+
childLinksTitle: "UI Components",
|
|
58
|
+
childLinks: [
|
|
59
|
+
{ name: "Buttons", path: "/forms/examples/buttons" },
|
|
60
|
+
{ name: "Tabs", path: "/ui/tabs" },
|
|
61
|
+
{ name: "Carousels", path: "/ui/carousel-basic" },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{ name: "Typography", path: "/typography/page-heading" },
|
|
65
|
+
],
|
|
66
|
+
secondNav: [{ name: "Contact", path: "#" }],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
render: (args) => ({
|
|
70
|
+
components: { SiteHeader },
|
|
71
|
+
setup() {
|
|
72
|
+
return { args };
|
|
73
|
+
},
|
|
74
|
+
template: `
|
|
75
|
+
<SiteHeader v-bind="args">
|
|
76
|
+
<template #branding>
|
|
77
|
+
<a href="/" style="color:inherit;text-decoration:none;font-weight:600">Brand</a>
|
|
78
|
+
</template>
|
|
79
|
+
</SiteHeader>
|
|
80
|
+
`,
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** A single flat nav group with no dropdowns — the shape a lighter, few-page site would use. */
|
|
85
|
+
export const SimpleFlatNav: Story = {
|
|
86
|
+
name: "Simple Flat Nav (no dropdowns)",
|
|
87
|
+
args: {
|
|
88
|
+
responsiveNavLinks: {
|
|
89
|
+
main: [
|
|
90
|
+
{ name: "Home", path: "/" },
|
|
91
|
+
{ name: "About", path: "/about" },
|
|
92
|
+
{ name: "Services", path: "/services" },
|
|
93
|
+
{ name: "Contact", path: "/contact" },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
render: (args) => ({
|
|
98
|
+
components: { SiteHeader },
|
|
99
|
+
setup() {
|
|
100
|
+
return { args };
|
|
101
|
+
},
|
|
102
|
+
template: `
|
|
103
|
+
<SiteHeader v-bind="args">
|
|
104
|
+
<template #branding>
|
|
105
|
+
<a href="/" style="color:inherit;text-decoration:none;font-weight:600">Brand</a>
|
|
106
|
+
</template>
|
|
107
|
+
</SiteHeader>
|
|
108
|
+
`,
|
|
109
|
+
}),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const WithSecondaryNavigationSlot: Story = {
|
|
113
|
+
name: "With secondaryNavigation slot",
|
|
114
|
+
args: {
|
|
115
|
+
responsiveNavLinks: {
|
|
116
|
+
firstNav: [{ name: "Home", path: "/" }],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
render: (args) => ({
|
|
120
|
+
components: { SiteHeader },
|
|
121
|
+
setup() {
|
|
122
|
+
return { args };
|
|
123
|
+
},
|
|
124
|
+
template: `
|
|
125
|
+
<SiteHeader v-bind="args">
|
|
126
|
+
<template #branding>
|
|
127
|
+
<a href="/" style="color:inherit;text-decoration:none;font-weight:600">Brand</a>
|
|
128
|
+
</template>
|
|
129
|
+
<template #secondaryNavigation>
|
|
130
|
+
<a href="/settings" aria-label="Settings">⚙</a>
|
|
131
|
+
</template>
|
|
132
|
+
</SiteHeader>
|
|
133
|
+
`,
|
|
134
|
+
}),
|
|
135
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import SiteHeader from "../SiteHeader.vue";
|
|
4
|
+
import type { ResponsiveHeaderProp } from "../../../../types/components";
|
|
5
|
+
|
|
6
|
+
const navLinks: ResponsiveHeaderProp = {
|
|
7
|
+
firstNav: [{ name: "Home", path: "/" }],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const responsiveHeaderStub = {
|
|
11
|
+
name: "ResponsiveHeader",
|
|
12
|
+
props: [
|
|
13
|
+
"responsiveNavLinks",
|
|
14
|
+
"gapBetweenFirstAndSecondNav",
|
|
15
|
+
"overflowDetailsSummaryIcons",
|
|
16
|
+
"collapseBreakpoint",
|
|
17
|
+
"collapseAtMainNavIntersection",
|
|
18
|
+
"allowExpandOnGesture",
|
|
19
|
+
"styleClassPassthrough",
|
|
20
|
+
],
|
|
21
|
+
template: `<div class="responsive-header-stub"><slot name="secondaryNavigation"></slot></div>`,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe("SiteHeader", () => {
|
|
25
|
+
it("mounts without error", async () => {
|
|
26
|
+
const wrapper = await mountSuspended(SiteHeader, {
|
|
27
|
+
global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
|
|
28
|
+
});
|
|
29
|
+
expect(wrapper.vm).toBeTruthy();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("renders the branding slot inside SkipLinks' homeLink slot", async () => {
|
|
33
|
+
const wrapper = await mountSuspended(SiteHeader, {
|
|
34
|
+
slots: { branding: "<a href='/' class='brand-link'>Brand</a>" },
|
|
35
|
+
global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
|
|
36
|
+
});
|
|
37
|
+
expect(wrapper.find(".home-link .brand-link").exists()).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("does not render the secondaryNavigation slot when not provided", async () => {
|
|
41
|
+
const wrapper = await mountSuspended(SiteHeader, {
|
|
42
|
+
global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
|
|
43
|
+
});
|
|
44
|
+
expect(wrapper.text()).not.toContain("Extra nav");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("forwards the secondaryNavigation slot through to ResponsiveHeader", async () => {
|
|
48
|
+
const wrapper = await mountSuspended(SiteHeader, {
|
|
49
|
+
slots: { secondaryNavigation: "<span>Extra nav</span>" },
|
|
50
|
+
global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
|
|
51
|
+
});
|
|
52
|
+
expect(wrapper.text()).toContain("Extra nav");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("forwards responsiveNavLinks and other nav props to ResponsiveHeader", async () => {
|
|
56
|
+
const wrapper = await mountSuspended(SiteHeader, {
|
|
57
|
+
props: {
|
|
58
|
+
responsiveNavLinks: navLinks,
|
|
59
|
+
gapBetweenFirstAndSecondNav: 24,
|
|
60
|
+
collapseAtMainNavIntersection: true,
|
|
61
|
+
allowExpandOnGesture: false,
|
|
62
|
+
navStyleClassPassthrough: ["site-header-nav"],
|
|
63
|
+
},
|
|
64
|
+
global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
|
|
65
|
+
});
|
|
66
|
+
const responsiveHeader = wrapper.findComponent(responsiveHeaderStub);
|
|
67
|
+
expect(responsiveHeader.props("responsiveNavLinks")).toEqual(navLinks);
|
|
68
|
+
expect(responsiveHeader.props("gapBetweenFirstAndSecondNav")).toBe(24);
|
|
69
|
+
expect(responsiveHeader.props("collapseAtMainNavIntersection")).toBe(true);
|
|
70
|
+
expect(responsiveHeader.props("allowExpandOnGesture")).toBe(false);
|
|
71
|
+
expect(responsiveHeader.props("styleClassPassthrough")).toEqual(["site-header-nav"]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("passes pageRowVariant through to the root PageRow", async () => {
|
|
75
|
+
const wrapper = await mountSuspended(SiteHeader, {
|
|
76
|
+
props: { pageRowVariant: "full" },
|
|
77
|
+
global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
|
|
78
|
+
});
|
|
79
|
+
expect(wrapper.find(".page-row.full").exists()).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("defaults pageRowVariant to content", async () => {
|
|
83
|
+
const wrapper = await mountSuspended(SiteHeader, {
|
|
84
|
+
global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
|
|
85
|
+
});
|
|
86
|
+
expect(wrapper.find(".page-row.content").exists()).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("applies styleClassPassthrough to the root PageRow", async () => {
|
|
90
|
+
const wrapper = await mountSuspended(SiteHeader, {
|
|
91
|
+
props: { styleClassPassthrough: ["header"] },
|
|
92
|
+
global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
|
|
93
|
+
});
|
|
94
|
+
expect(wrapper.find(".page-row.header").exists()).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("renders correct HTML structure with default props", async () => {
|
|
98
|
+
const wrapper = await mountSuspended(SiteHeader, {
|
|
99
|
+
global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
|
|
100
|
+
});
|
|
101
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`SiteHeader > renders correct HTML structure with default props 1`] = `
|
|
4
|
+
"<div class="page-row content">
|
|
5
|
+
<header class="site-header">
|
|
6
|
+
<nav class="home-navigation" aria-label="Home Navigation">
|
|
7
|
+
<div class="home-link-navigation">
|
|
8
|
+
<div class="home-link"></div>
|
|
9
|
+
<nav class="skip-links-nav" aria-label="Skip navigation"><a href="#main-content" class="skip-link">Skip to main content</a><a href="#footer-content" class="skip-link">Skip to footer</a></nav>
|
|
10
|
+
</div>
|
|
11
|
+
</nav>
|
|
12
|
+
<div class="responsive-header-stub"></div>
|
|
13
|
+
</header>
|
|
14
|
+
</div>"
|
|
15
|
+
`;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# InputSelectCore — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Two override paths
|
|
4
|
+
|
|
5
|
+
Same public/private split as `InputTextCore` (see that component's `CONSUMER-STYLING.md` for the
|
|
6
|
+
full rationale) — `.input-select-wrapper` now exposes local tokens, one indirection step below the
|
|
7
|
+
global `--theme-*` ones they default from:
|
|
8
|
+
|
|
9
|
+
```css
|
|
10
|
+
--_input-select-surface: var(--theme-input-surface);
|
|
11
|
+
--_input-select-surface-hover: var(--theme-input-surface-hover);
|
|
12
|
+
--_input-select-border: var(--theme-border);
|
|
13
|
+
--_input-select-border-focus: var(--theme-border-focus);
|
|
14
|
+
--_input-select-outline-color: var(--_input-select-border-focus);
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- **Global override**: redeclare `--theme-input-surface` / `--theme-border` / `--theme-border-focus`
|
|
18
|
+
/ `--theme-input-surface-hover` wherever you'd normally set theme tokens.
|
|
19
|
+
- **Local override** (this component only, guaranteed to win):
|
|
20
|
+
|
|
21
|
+
```css
|
|
22
|
+
.input-select-wrapper {
|
|
23
|
+
--_input-select-border: var(--slate-06);
|
|
24
|
+
--_input-select-border-focus: var(--slate-04);
|
|
25
|
+
--_input-select-surface: var(--slate-01);
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Note on `--_input-select-outline-color`
|
|
30
|
+
|
|
31
|
+
Before 2026-08-22 this was referenced (on the open-picker outline) but never declared anywhere —
|
|
32
|
+
a dangling private token that silently resolved to the property's initial value (same failure mode
|
|
33
|
+
as the `--theme-button-surface` typo documented in the layer's `CLAUDE.md`, pitfall list). It's now
|
|
34
|
+
properly defined, defaulting to `--_input-select-border-focus`.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Other tokens
|
|
39
|
+
|
|
40
|
+
Generic form-geometry tokens (`--input-padding-inline`, `--input-min-height`, `--input-font-size`,
|
|
41
|
+
`--form-element-border-width`, `--form-input-border-radius`, etc.) are shared across every
|
|
42
|
+
`05.forms` component and declared in `setup/04.elements/forms/*.css` — not component-local.
|
|
@@ -73,7 +73,17 @@ const fieldData = defineModel("fieldData") as Ref<IFormMultipleOptions>;
|
|
|
73
73
|
<style lang="css">
|
|
74
74
|
@layer components {
|
|
75
75
|
.input-select-wrapper {
|
|
76
|
-
|
|
76
|
+
/* Local overrides, one indirection step below the global --theme-* tokens they default from —
|
|
77
|
+
a consumer can target `.input-select-wrapper { --_input-select-border: ...; }` directly for
|
|
78
|
+
a guaranteed-specific override, instead of relying solely on redefining the global token.
|
|
79
|
+
See CONSUMER-STYLING.md. */
|
|
80
|
+
--_input-select-surface: var(--theme-input-surface);
|
|
81
|
+
--_input-select-surface-hover: var(--theme-input-surface-hover);
|
|
82
|
+
--_input-select-border: var(--theme-border);
|
|
83
|
+
--_input-select-border-focus: var(--theme-border-focus);
|
|
84
|
+
--_input-select-outline-color: var(--_input-select-border-focus);
|
|
85
|
+
|
|
86
|
+
background-color: var(--_input-select-surface);
|
|
77
87
|
overflow: hidden;
|
|
78
88
|
|
|
79
89
|
z-index: 2;
|
|
@@ -81,31 +91,33 @@ const fieldData = defineModel("fieldData") as Ref<IFormMultipleOptions>;
|
|
|
81
91
|
transition: all var(--theme-form-transition-duration) ease-in-out;
|
|
82
92
|
|
|
83
93
|
&.normal {
|
|
84
|
-
border: var(--form-element-border-width) solid var(--
|
|
94
|
+
border: var(--form-element-border-width) solid var(--_input-select-border);
|
|
85
95
|
border-radius: var(--form-input-border-radius);
|
|
86
96
|
outline: var(--form-element-outline-width) solid transparent;
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
&.underlined {
|
|
90
|
-
border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--
|
|
91
|
-
background-color: var(--
|
|
100
|
+
border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--_input-select-border);
|
|
101
|
+
background-color: var(--_input-select-surface);
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
&:has(select:focus-visible, :hover) {
|
|
95
|
-
outline: var(--form-element-outline-width-focus) solid var(--
|
|
105
|
+
outline: var(--form-element-outline-width-focus) solid var(--_input-select-border-focus);
|
|
96
106
|
outline-offset: var(--form-element-outline-offset-focus);
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
.input-select-core {
|
|
100
110
|
appearance: none;
|
|
101
111
|
background-color: transparent;
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
102
114
|
|
|
103
115
|
/* For legacy support - eg, Safari */
|
|
104
116
|
/* &::after {
|
|
105
117
|
content: '';
|
|
106
118
|
width: 0.8em;
|
|
107
119
|
height: 0.5em;
|
|
108
|
-
background-color: var(--
|
|
120
|
+
background-color: var(--_input-select-border);
|
|
109
121
|
clip-path: polygon(100% 0%, 0 0%, 50% 100%);
|
|
110
122
|
} */
|
|
111
123
|
|
|
@@ -128,7 +140,7 @@ const fieldData = defineModel("fieldData") as Ref<IFormMultipleOptions>;
|
|
|
128
140
|
|
|
129
141
|
&:open::picker(select) {
|
|
130
142
|
opacity: 1;
|
|
131
|
-
border: var(--form-element-border-width) solid var(--
|
|
143
|
+
border: var(--form-element-border-width) solid var(--_input-select-border);
|
|
132
144
|
outline: var(--form-element-outline-width) solid var(--_input-select-outline-color);
|
|
133
145
|
|
|
134
146
|
@starting-style {
|
|
@@ -145,7 +157,7 @@ const fieldData = defineModel("fieldData") as Ref<IFormMultipleOptions>;
|
|
|
145
157
|
|
|
146
158
|
font-family: var(--font-family);
|
|
147
159
|
font-size: var(--input-font-size);
|
|
148
|
-
line-height: var(--input-element-line-height);
|
|
160
|
+
/* line-height: var(--input-element-line-height); */
|
|
149
161
|
padding-block: var(--input-padding-block);
|
|
150
162
|
padding-inline: var(--input-padding-inline);
|
|
151
163
|
min-height: var(--input-min-height);
|
|
@@ -156,7 +168,7 @@ const fieldData = defineModel("fieldData") as Ref<IFormMultipleOptions>;
|
|
|
156
168
|
transition: all var(--theme-form-transition-duration) ease-in-out;
|
|
157
169
|
|
|
158
170
|
&:hover {
|
|
159
|
-
background-color: var(--
|
|
171
|
+
background-color: var(--_input-select-surface-hover);
|
|
160
172
|
}
|
|
161
173
|
|
|
162
174
|
.input-select-core-option-decorator-icon {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# InputTextCore — Consumer Styling Guide
|
|
2
|
+
|
|
3
|
+
## Two override paths
|
|
4
|
+
|
|
5
|
+
Before 2026-08-22, `.input-text-wrapper` read the global `--theme-*` tokens directly
|
|
6
|
+
(`--theme-input-surface`, `--theme-border`, `--theme-border-focus`, `--theme-input-surface-hover`).
|
|
7
|
+
Overriding those globally (e.g. in a consumer app's unlayered `:root` block) should always have
|
|
8
|
+
worked on pure CSS-cascade grounds — an unlayered `:root` declaration (specificity `0,1,0`) beats
|
|
9
|
+
the layer's own `:where(html, [data-theme], [data-invalid])` declaration (specificity `0`)
|
|
10
|
+
regardless of source/import order — but a consumer app hit a case where a global override wasn't
|
|
11
|
+
visibly landing and the root cause wasn't pinned down (see `nuxt-components`'s own git history
|
|
12
|
+
around the same date for a *different*, confirmed bug in a sibling app: wrapping consumer CSS in
|
|
13
|
+
`@layer consumer` broke in production because SSR-inlined per-chunk `<style>` tags fix layer
|
|
14
|
+
priority by first-reference document order, not the app's intended `@layer` statement order — sic
|
|
15
|
+
that isn't what was happening here, since the consumer app's CSS was already unlayered, but it's
|
|
16
|
+
the same class of "cascade behaves differently than a clean mental model predicts" issue).
|
|
17
|
+
|
|
18
|
+
Rather than leave global-token overrides as the only lever, `.input-text-wrapper` now also
|
|
19
|
+
exposes its own local tokens, one indirection step below the global ones:
|
|
20
|
+
|
|
21
|
+
```css
|
|
22
|
+
--_input-text-surface: var(--theme-input-surface);
|
|
23
|
+
--_input-text-surface-hover: var(--theme-input-surface-hover);
|
|
24
|
+
--_input-text-border: var(--theme-border);
|
|
25
|
+
--_input-text-border-focus: var(--theme-border-focus);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- **Global override** (affects every themed component, not just this one): redeclare
|
|
29
|
+
`--theme-input-surface` / `--theme-border` / `--theme-border-focus` / `--theme-input-surface-hover`
|
|
30
|
+
at whatever scope you'd normally set theme tokens (`:root`, `[data-theme]`, a page wrapper).
|
|
31
|
+
- **Local override** (this component only, guaranteed to win — it's the last declaration before
|
|
32
|
+
the property that consumes it, no global-token indirection to reason about):
|
|
33
|
+
|
|
34
|
+
```css
|
|
35
|
+
/* Unscoped consumer CSS, no :deep() needed */
|
|
36
|
+
.input-text-wrapper {
|
|
37
|
+
--_input-text-border: var(--slate-06);
|
|
38
|
+
--_input-text-border-focus: var(--slate-04);
|
|
39
|
+
--_input-text-surface: var(--slate-01);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Prefer the local override when you only want to change this one component's look, or when you've
|
|
44
|
+
hit a case (like the one above) where a global-token change isn't landing and you don't have time
|
|
45
|
+
to dig into why — the local path is a strictly simpler cascade to reason about.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Other tokens
|
|
50
|
+
|
|
51
|
+
- `--input-icon-slot-gap`, `--input-padding-inline`, `--input-padding-block`, `--input-min-height`,
|
|
52
|
+
`--input-font-size`, `--form-element-border-width`, `--form-input-border-radius`,
|
|
53
|
+
`--form-element-border-bottom-width-underlined`, `--form-element-outline-width(-focus)`,
|
|
54
|
+
`--form-element-outline-offset-focus` — all generic form-geometry tokens shared across every
|
|
55
|
+
`05.forms` component, declared in `setup/04.elements/forms/*.css`. Not component-local, so
|
|
56
|
+
overriding one affects every form element, not just `InputTextCore`.
|
|
57
|
+
- `--theme-input-text-color-normal`, `--theme-input-placeholder`,
|
|
58
|
+
`--theme-input-placeholder-font-size` — still read directly from the global theme tokens, no
|
|
59
|
+
local indirection yet. Follow the same pattern above (`--_input-text-*`) if a future consumer
|
|
60
|
+
needs a local override for these.
|
|
@@ -127,10 +127,19 @@ onMounted(() => {
|
|
|
127
127
|
<style lang="css">
|
|
128
128
|
@layer components {
|
|
129
129
|
.input-text-wrapper {
|
|
130
|
+
/* Local overrides, one indirection step below the global --theme-* tokens they default from
|
|
131
|
+
— a consumer can target `.input-text-wrapper { --_input-text-border: ...; }` directly for
|
|
132
|
+
a guaranteed-specific override, instead of relying solely on redefining the global token.
|
|
133
|
+
See CONSUMER-STYLING.md. */
|
|
134
|
+
--_input-text-surface: var(--theme-input-surface);
|
|
135
|
+
--_input-text-surface-hover: var(--theme-input-surface-hover);
|
|
136
|
+
--_input-text-border: var(--theme-border);
|
|
137
|
+
--_input-text-border-focus: var(--theme-border-focus);
|
|
138
|
+
|
|
130
139
|
display: flex;
|
|
131
140
|
align-items: center;
|
|
132
141
|
gap: var(--input-icon-slot-gap);
|
|
133
|
-
background-color: var(--
|
|
142
|
+
background-color: var(--_input-text-surface);
|
|
134
143
|
overflow: hidden;
|
|
135
144
|
transition: all var(--theme-form-transition-duration) ease-in-out;
|
|
136
145
|
|
|
@@ -138,25 +147,25 @@ onMounted(() => {
|
|
|
138
147
|
position: relative;
|
|
139
148
|
|
|
140
149
|
&.normal {
|
|
141
|
-
border: var(--form-element-border-width) solid var(--
|
|
150
|
+
border: var(--form-element-border-width) solid var(--_input-text-border);
|
|
142
151
|
border-radius: var(--form-input-border-radius);
|
|
143
152
|
outline: var(--form-element-outline-width) solid transparent;
|
|
144
153
|
|
|
145
154
|
padding-inline: var(--input-padding-inline);
|
|
146
155
|
|
|
147
156
|
&:has(input:is(:hover), button:is(:hover)) {
|
|
148
|
-
outline: var(--form-element-outline-width-focus) solid var(--
|
|
157
|
+
outline: var(--form-element-outline-width-focus) solid var(--_input-text-border-focus);
|
|
149
158
|
outline-offset: var(--form-element-outline-offset-focus);
|
|
150
159
|
}
|
|
151
160
|
|
|
152
161
|
&:has(input:focus-visible, button:focus-visible) {
|
|
153
|
-
outline: var(--form-element-outline-width-focus) solid var(--
|
|
162
|
+
outline: var(--form-element-outline-width-focus) solid var(--_input-text-border-focus);
|
|
154
163
|
outline-offset: var(--form-element-outline-offset-focus);
|
|
155
164
|
}
|
|
156
165
|
}
|
|
157
166
|
|
|
158
167
|
&.underlined {
|
|
159
|
-
border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--
|
|
168
|
+
border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--_input-text-border);
|
|
160
169
|
padding-inline: var(--input-padding-inline);
|
|
161
170
|
}
|
|
162
171
|
|
|
@@ -171,17 +180,17 @@ onMounted(() => {
|
|
|
171
180
|
box-sizing: content-box;
|
|
172
181
|
|
|
173
182
|
.input-button-core {
|
|
174
|
-
background-color: var(--
|
|
183
|
+
background-color: var(--_input-text-surface);
|
|
175
184
|
aspect-ratio: 1;
|
|
176
185
|
border-radius: 0;
|
|
177
186
|
width: var(--input-min-height);
|
|
178
187
|
|
|
179
188
|
&:hover {
|
|
180
|
-
background-color: var(--
|
|
189
|
+
background-color: var(--_input-text-surface-hover);
|
|
181
190
|
}
|
|
182
191
|
|
|
183
192
|
&:is(:focus-visible) {
|
|
184
|
-
outline: var(--form-element-outline-width-focus) solid var(--
|
|
193
|
+
outline: var(--form-element-outline-width-focus) solid var(--_input-text-border-focus);
|
|
185
194
|
outline-offset: -4px;
|
|
186
195
|
}
|
|
187
196
|
|