srcdev-nuxt-components 9.5.6 → 9.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/component-compliance-checklist.md +6 -2
- package/.claude/skills/components/breadcrumb.md +5 -0
- package/.claude/skills/components/display-pill.md +2 -0
- package/.claude/skills/components/input-button.md +3 -0
- package/.claude/skills/components/navigation-items.md +1 -0
- package/.claude/skills/components/opening-hours.md +12 -0
- package/.claude/skills/components/price-list.md +10 -1
- package/.claude/skills/components/responsive-header.md +74 -0
- package/.claude/skills/components/service-detail.md +32 -0
- package/.claude/skills/components/service-summary.md +8 -0
- package/.claude/skills/components/services-card-grid.md +5 -3
- package/.claude/skills/components/services-card.md +22 -12
- package/.claude/skills/components/site-header.md +2 -0
- package/.claude/skills/components/slider-gallery.md +10 -4
- package/.claude/skills/components/tab-navigation.md +25 -9
- package/.claude/skills/index.md +1 -2
- package/.claude/skills/storybook-add-story.md +14 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +13 -1
- package/.vscode/srcdev-component-service-detail.code-snippets +1 -0
- package/.vscode/srcdev-component-service-summary.code-snippets +3 -1
- package/.vscode/srcdev-component-services-card.code-snippets +38 -36
- package/.vscode/srcdev-component-services-grids.code-snippets +2 -1
- package/.vscode/srcdev-component-site-header.code-snippets +3 -1
- package/.vscode/srcdev-component-tab-navigation.code-snippets +1 -1
- package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +3 -0
- package/app/components/01.atoms/display-pill/DisplayPill.vue +5 -2
- package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +16 -4
- package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +28 -5
- package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +94 -0
- package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +27 -0
- package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +52 -11
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +55 -23
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +176 -257
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +42 -2
- package/app/components/02.molecules/opening-hours/CONSUMER-STYLING.md +11 -1
- package/app/components/02.molecules/opening-hours/OpeningHours.vue +71 -43
- package/app/components/02.molecules/opening-hours/stories/OpeningHours.stories.ts +138 -6
- package/app/components/02.molecules/opening-hours/tests/OpeningHours.spec.ts +53 -0
- package/app/components/02.molecules/opening-hours/tests/__snapshots__/OpeningHours.spec.ts.snap +1 -1
- package/app/components/02.molecules/opening-hours/tests/buildOpeningHoursSpecification.spec.ts +37 -1
- package/app/components/02.molecules/price-list/CONSUMER-STYLING.md +17 -5
- package/app/components/02.molecules/price-list/PriceList.vue +32 -9
- package/app/components/02.molecules/price-list/stories/PriceList.stories.ts +125 -29
- package/app/components/02.molecules/price-list/tests/PriceList.spec.ts +20 -0
- package/app/components/03.organisms/google-reviews/stories/GoogleReviews.stories.ts +9 -4
- package/app/components/03.organisms/image-galleries/slider-gallery/CONSUMER-STYLING.md +19 -1
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +65 -7
- package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +201 -63
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +41 -2
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +6 -6
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +18 -0
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +56 -18
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +106 -31
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +201 -18
- package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +21 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +66 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +30 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +1 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +35 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +23 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +1 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +71 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +386 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +11 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +79 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +17 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +64 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +29 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +32 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +10 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
- package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +36 -0
- package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +131 -6
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +9 -9
- package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +1 -0
- package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +92 -41
- package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +198 -73
- package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +157 -9
- package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +58 -0
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +4 -3
- package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +30 -0
- package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +32 -8
- package/app/components/03.organisms/services/service-summary/stories/ServiceSummary.stories.ts +147 -0
- package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +26 -0
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
- package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +104 -63
- package/app/components/03.organisms/services/services-card/ServicesCard.vue +92 -66
- package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +194 -27
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +34 -8
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +8 -8
- package/app/components/03.organisms/services/services-grids/CONSUMER-STYLING.md +9 -0
- package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +2 -1
- package/app/components/03.organisms/services/services-grids/stories/ServiceSummaryGrid.stories.ts +46 -6
- package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +89 -11
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +8 -0
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
- package/app/components/03.organisms/site-header/SiteHeader.vue +8 -0
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +8 -0
- package/app/components/05.forms/input-button/InputButton.vue +3 -1
- package/app/composables/tests/useAnchorScroll.spec.ts +8 -0
- package/app/composables/useAnchorScroll.ts +1 -1
- package/app/utils/opening-hours.ts +23 -2
- package/nuxt.config.ts +1 -1
- package/package.json +1 -1
- package/.claude/skills/components/site-navigation.md +0 -145
- package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
- package/app/components/02.molecules/navigation/site-navigation/CONSUMER-STYLING.md +0 -109
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +0 -751
- package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +0 -356
- package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +0 -367
- package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +0 -30
package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts
CHANGED
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
import { ref, reactive, computed } from "vue";
|
|
2
|
-
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import type { Meta, StoryFn, StoryObj } from "@nuxtjs/storybook";
|
|
3
3
|
import TabNavigationComponent from "../TabNavigation.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
4
5
|
import type { NavItemData } from "~/types/components/nav-item.d";
|
|
6
|
+
import type { MediaCanvas } from "~/types/components";
|
|
5
7
|
|
|
6
8
|
const meta: Meta<typeof TabNavigationComponent> = {
|
|
7
9
|
title: "Molecules/TabNavigation",
|
|
8
10
|
component: TabNavigationComponent,
|
|
11
|
+
parameters: {
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component:
|
|
15
|
+
"**Deprecated: use ResponsiveHeader.** It handles the same route and `#anchor` links (with " +
|
|
16
|
+
"`anchorScrollOffset`) and collapses item by item rather than all-or-nothing. See \"Migrating from " +
|
|
17
|
+
"TabNavigation\" in the ResponsiveHeader skill doc. TabNavigation stays until existing consumers have moved.",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
9
21
|
argTypes: {
|
|
10
22
|
navAlign: {
|
|
11
23
|
control: { type: "select" },
|
|
12
24
|
options: ["left", "center", "right"],
|
|
13
25
|
description: "Horizontal alignment of the nav list",
|
|
14
26
|
},
|
|
27
|
+
ariaLabel: { control: "text", description: "aria-label on the nav landmark; override for localisation" },
|
|
28
|
+
openMenuLabel: { control: "text", description: "Burger button label while the menu is closed" },
|
|
29
|
+
closeMenuLabel: { control: "text", description: "Burger button label while the menu is open" },
|
|
15
30
|
navItemData: { table: { disable: true } },
|
|
16
31
|
styleClassPassthrough: { table: { disable: true } },
|
|
17
32
|
},
|
|
18
33
|
args: {
|
|
19
34
|
navAlign: "left",
|
|
35
|
+
ariaLabel: "Site navigation",
|
|
36
|
+
openMenuLabel: "Open navigation menu",
|
|
37
|
+
closeMenuLabel: "Close navigation menu",
|
|
20
38
|
},
|
|
21
39
|
};
|
|
22
40
|
|
|
@@ -32,85 +50,47 @@ const navItemData: NavItemData = {
|
|
|
32
50
|
],
|
|
33
51
|
};
|
|
34
52
|
|
|
35
|
-
|
|
53
|
+
// Playground: every control starts unset, so the nav shows the component's own (light) defaults.
|
|
54
|
+
// Picking a colour sets that token as an override; Reset removes it again.
|
|
55
|
+
const tokenControls = [
|
|
56
|
+
{ group: "Indicator", label: "Indicator colour", token: "--tab-nav-decorator-indicator-color" },
|
|
57
|
+
{ group: "Horizontal nav links", label: "Link colour", token: "--tab-nav-link-color" },
|
|
58
|
+
{ group: "Horizontal nav links", label: "Hover colour", token: "--tab-nav-link-hover-color" },
|
|
59
|
+
{ group: "Horizontal nav links", label: "Active colour", token: "--tab-nav-link-active-color" },
|
|
60
|
+
{ group: "Horizontal nav links", label: "Focus ring colour", token: "--tab-nav-focus-ring-colour" },
|
|
61
|
+
{ group: "Mobile panel", label: "Panel background", token: "--tab-nav-panel-bg" },
|
|
62
|
+
{ group: "Mobile panel", label: "Panel link colour", token: "--tab-nav-panel-link-color" },
|
|
63
|
+
{ group: "Burger", label: "Burger colour", token: "--tab-nav-burger-color" },
|
|
64
|
+
] as const;
|
|
36
65
|
|
|
37
|
-
|
|
38
|
-
Theme,
|
|
39
|
-
{
|
|
40
|
-
indicatorColor: string;
|
|
41
|
-
linkColor: string;
|
|
42
|
-
linkHoverColor: string;
|
|
43
|
-
linkActiveColor: string;
|
|
44
|
-
panelBg: string;
|
|
45
|
-
panelLinkColor: string;
|
|
46
|
-
burgerColor: string;
|
|
47
|
-
pageBg: string;
|
|
48
|
-
}
|
|
49
|
-
> = {
|
|
50
|
-
dark: {
|
|
51
|
-
indicatorColor: "#c0847a",
|
|
52
|
-
linkColor: "#f0ece8",
|
|
53
|
-
linkHoverColor: "#e09890",
|
|
54
|
-
linkActiveColor: "#c0847a",
|
|
55
|
-
panelBg: "#1a1614",
|
|
56
|
-
panelLinkColor: "#f0ece8",
|
|
57
|
-
burgerColor: "#f0ece8",
|
|
58
|
-
pageBg: "#1a1614",
|
|
59
|
-
},
|
|
60
|
-
light: {
|
|
61
|
-
indicatorColor: "#b05a50",
|
|
62
|
-
linkColor: "#1a1614",
|
|
63
|
-
linkHoverColor: "#b05a50",
|
|
64
|
-
linkActiveColor: "#8c3a30",
|
|
65
|
-
panelBg: "#f5f2f0",
|
|
66
|
-
panelLinkColor: "#1a1614",
|
|
67
|
-
burgerColor: "#1a1614",
|
|
68
|
-
pageBg: "#f5f2f0",
|
|
69
|
-
},
|
|
70
|
-
};
|
|
66
|
+
type Token = (typeof tokenControls)[number]["token"];
|
|
71
67
|
|
|
72
68
|
const DefaultTemplate: StoryFn<typeof TabNavigationComponent> = (args) => ({
|
|
73
69
|
components: { TabNavigationComponent },
|
|
74
70
|
setup() {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const setTheme = (newTheme: Theme) => {
|
|
80
|
-
theme.value = newTheme;
|
|
81
|
-
Object.assign(controls, themeDefaults[newTheme]);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const navStyle = computed(() => ({
|
|
85
|
-
"--tab-nav-decorator-indicator-color": controls.indicatorColor,
|
|
86
|
-
"--tab-nav-link-color": controls.linkColor,
|
|
87
|
-
"--tab-nav-link-hover-color": controls.linkHoverColor,
|
|
88
|
-
"--tab-nav-link-active-color": controls.linkActiveColor,
|
|
89
|
-
"--tab-nav-panel-bg": controls.panelBg,
|
|
90
|
-
"--tab-nav-panel-link-color": controls.panelLinkColor,
|
|
91
|
-
"--tab-nav-burger-color": controls.burgerColor,
|
|
92
|
-
"--page-bg": controls.pageBg,
|
|
93
|
-
"margin-block": "3.6rem",
|
|
71
|
+
const overrides = reactive<Partial<Record<Token, string>>>({});
|
|
72
|
+
const groups = [...new Set(tokenControls.map((control) => control.group))].map((group) => ({
|
|
73
|
+
group,
|
|
74
|
+
controls: tokenControls.filter((control) => control.group === group),
|
|
94
75
|
}));
|
|
95
76
|
|
|
96
|
-
const
|
|
97
|
-
(
|
|
98
|
-
|
|
99
|
-
|
|
77
|
+
const setOverride = (token: Token, event: Event) => {
|
|
78
|
+
overrides[token] = (event.target as HTMLInputElement).value;
|
|
79
|
+
};
|
|
80
|
+
const resetOverride = (token: Token) => {
|
|
81
|
+
overrides[token] = undefined;
|
|
82
|
+
};
|
|
100
83
|
|
|
101
|
-
|
|
102
|
-
--tab-nav-link-color: ${controls.linkColor};
|
|
103
|
-
--tab-nav-link-hover-color: ${controls.linkHoverColor};
|
|
104
|
-
--tab-nav-link-active-color: ${controls.linkActiveColor};
|
|
84
|
+
const activeOverrides = computed(() => Object.entries(overrides).filter(([, value]) => value));
|
|
105
85
|
|
|
106
|
-
|
|
107
|
-
--tab-nav-panel-bg: ${controls.panelBg};
|
|
108
|
-
--tab-nav-panel-link-color: ${controls.panelLinkColor};
|
|
86
|
+
const navStyle = computed(() => Object.fromEntries(activeOverrides.value));
|
|
109
87
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
88
|
+
const cssSnippet = computed(() => {
|
|
89
|
+
const lines = activeOverrides.value.map(([token, value]) => ` ${token}: ${value};`);
|
|
90
|
+
return lines.length
|
|
91
|
+
? `.your-selector {\n${lines.join("\n")}\n}`
|
|
92
|
+
: "/* No overrides: the nav is using its default (light theme) colours. */";
|
|
93
|
+
});
|
|
114
94
|
|
|
115
95
|
const copied = ref(false);
|
|
116
96
|
const copySnippet = async () => {
|
|
@@ -121,7 +101,7 @@ const DefaultTemplate: StoryFn<typeof TabNavigationComponent> = (args) => ({
|
|
|
121
101
|
}, 2000);
|
|
122
102
|
};
|
|
123
103
|
|
|
124
|
-
return { args, navItemData,
|
|
104
|
+
return { args, navItemData, groups, overrides, setOverride, resetOverride, navStyle, cssSnippet, copied, copySnippet };
|
|
125
105
|
},
|
|
126
106
|
template: `
|
|
127
107
|
<div class="sb-tabnav-story">
|
|
@@ -130,211 +110,150 @@ const DefaultTemplate: StoryFn<typeof TabNavigationComponent> = (args) => ({
|
|
|
130
110
|
Resize the browser window to see the navigation collapse into a burger menu.
|
|
131
111
|
The active indicator uses CSS <code>anchor-name</code> / <code>position-anchor</code> —
|
|
132
112
|
no JavaScript required. Unsupported browsers fall back to link colour changes only.
|
|
113
|
+
The colours below start at the component defaults (light theme); dark mode is the consumer app's
|
|
114
|
+
call, e.g. with <code>light-dark()</code> in its own token overrides.
|
|
133
115
|
</div>
|
|
134
|
-
|
|
116
|
+
|
|
117
|
+
<div class="sb-tabnav-header" :style="navStyle">
|
|
135
118
|
<div class="sb-tabnav-logo">LOGO</div>
|
|
136
|
-
<TabNavigationComponent
|
|
137
|
-
:nav-item-data="navItemData"
|
|
138
|
-
:nav-align="args.navAlign"
|
|
139
|
-
/>
|
|
119
|
+
<TabNavigationComponent v-bind="args" :nav-item-data="navItemData" />
|
|
140
120
|
</div>
|
|
141
121
|
|
|
142
|
-
<!-- Controls -->
|
|
143
122
|
<div class="sb-tabnav-playground">
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
<fieldset>
|
|
165
|
-
<legend>Horizontal nav links</legend>
|
|
166
|
-
<div class="sb-control-row">
|
|
167
|
-
<label for="sb-link-color">Link colour</label>
|
|
168
|
-
<input id="sb-link-color" v-model="controls.linkColor" type="color" />
|
|
169
|
-
</div>
|
|
170
|
-
<div class="sb-control-row">
|
|
171
|
-
<label for="sb-link-hover-color">Hover colour</label>
|
|
172
|
-
<input id="sb-link-hover-color" v-model="controls.linkHoverColor" type="color" />
|
|
173
|
-
</div>
|
|
174
|
-
<div class="sb-control-row">
|
|
175
|
-
<label for="sb-link-active-color">Active colour</label>
|
|
176
|
-
<input id="sb-link-active-color" v-model="controls.linkActiveColor" type="color" />
|
|
123
|
+
<fieldset v-for="{ group, controls } in groups" :key="group">
|
|
124
|
+
<legend>{{ group }}</legend>
|
|
125
|
+
<div v-for="control in controls" :key="control.token" class="sb-control-row">
|
|
126
|
+
<label :for="control.token">{{ control.label }}</label>
|
|
127
|
+
<input
|
|
128
|
+
:id="control.token"
|
|
129
|
+
type="color"
|
|
130
|
+
:value="overrides[control.token] ?? '#000000'"
|
|
131
|
+
@input="setOverride(control.token, $event)"
|
|
132
|
+
/>
|
|
133
|
+
<button
|
|
134
|
+
v-if="overrides[control.token]"
|
|
135
|
+
type="button"
|
|
136
|
+
class="sb-reset-btn"
|
|
137
|
+
@click="resetOverride(control.token)"
|
|
138
|
+
>
|
|
139
|
+
Reset
|
|
140
|
+
</button>
|
|
141
|
+
<span v-else class="sb-default-badge">default</span>
|
|
177
142
|
</div>
|
|
178
143
|
</fieldset>
|
|
179
|
-
|
|
180
|
-
<fieldset>
|
|
181
|
-
<legend>Mobile panel</legend>
|
|
182
|
-
<div class="sb-control-row">
|
|
183
|
-
<label for="sb-panel-bg">Panel background</label>
|
|
184
|
-
<input id="sb-panel-bg" v-model="controls.panelBg" type="color" />
|
|
185
|
-
</div>
|
|
186
|
-
<div class="sb-control-row">
|
|
187
|
-
<label for="sb-panel-link-color">Panel link colour</label>
|
|
188
|
-
<input id="sb-panel-link-color" v-model="controls.panelLinkColor" type="color" />
|
|
189
|
-
</div>
|
|
190
|
-
</fieldset>
|
|
191
|
-
|
|
192
|
-
<fieldset>
|
|
193
|
-
<legend>Burger</legend>
|
|
194
|
-
<div class="sb-control-row">
|
|
195
|
-
<label for="sb-burger-color">Burger colour</label>
|
|
196
|
-
<input id="sb-burger-color" v-model="controls.burgerColor" type="color" />
|
|
197
|
-
</div>
|
|
198
|
-
</fieldset>
|
|
199
|
-
|
|
200
144
|
</div>
|
|
201
145
|
|
|
202
|
-
<!-- CSS snippet -->
|
|
203
146
|
<div class="sb-css-snippet">
|
|
204
147
|
<div class="sb-css-snippet-header">
|
|
205
148
|
<strong>CSS Token Snippet</strong>
|
|
206
|
-
<button class="sb-copy-btn" @click="copySnippet">{{ copied ? 'Copied!' : 'Copy' }}</button>
|
|
149
|
+
<button class="sb-copy-btn" type="button" @click="copySnippet">{{ copied ? 'Copied!' : 'Copy' }}</button>
|
|
207
150
|
</div>
|
|
208
151
|
<pre class="sb-css-snippet-code">{{ cssSnippet }}</pre>
|
|
209
152
|
</div>
|
|
210
153
|
|
|
154
|
+
<component is="style">
|
|
155
|
+
.sb-tabnav-story { font-size: 1.4rem; display: grid; gap: 2.4rem; padding: 2.4rem; }
|
|
156
|
+
.sb-tabnav-note { font-size: 1.3rem; opacity: 0.7; font-style: italic; }
|
|
157
|
+
.sb-tabnav-note code { font-family: monospace; background: rgba(128,128,128,0.15); padding: 0.1em 0.4em; border-radius: 0.2rem; }
|
|
158
|
+
.sb-tabnav-header {
|
|
159
|
+
display: flex; align-items: center; gap: 3.2rem; padding: 1.2rem 2.4rem;
|
|
160
|
+
position: relative; min-height: 6rem; border-radius: 0.4rem;
|
|
161
|
+
border: 1px solid rgba(128,128,128,0.3);
|
|
162
|
+
}
|
|
163
|
+
.sb-tabnav-logo { font-size: 1.8rem; font-weight: 700; letter-spacing: 0.1em; flex-shrink: 0; opacity: 0.5; }
|
|
164
|
+
.sb-tabnav-header .tab-navigation { flex: 1; min-width: 0; }
|
|
165
|
+
.sb-tabnav-playground { display: grid; grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr)); gap: 1.6rem; }
|
|
166
|
+
.sb-tabnav-playground fieldset { border: 1px solid #ccc; border-radius: 0.4rem; padding: 1.6rem; }
|
|
167
|
+
.sb-tabnav-playground legend { font-weight: bold; padding: 0 0.8rem; font-size: 1.4rem; }
|
|
168
|
+
.sb-control-row { display: grid; grid-template-columns: 1fr 8rem 6rem; gap: 1rem; align-items: center; padding: 0.5rem 0; font-size: 1.3rem; }
|
|
169
|
+
.sb-control-row input[type="color"] { height: 3.2rem; width: 100%; padding: 0.2rem; border: 1px solid #ccc; border-radius: 0.2rem; cursor: pointer; background: transparent; }
|
|
170
|
+
.sb-default-badge { font-size: 1.1rem; opacity: 0.6; text-align: center; }
|
|
171
|
+
.sb-css-snippet { border: 1px solid #ccc; border-radius: 0.4rem; overflow: hidden; }
|
|
172
|
+
.sb-css-snippet-header { display: flex; justify-content: space-between; align-items: center; padding: 1rem 1.6rem; border-bottom: 1px solid #ccc; font-size: 1.4rem; }
|
|
173
|
+
.sb-css-snippet-code { margin: 0; padding: 1.6rem; font-family: monospace; font-size: 1.3rem; line-height: 1.6; overflow-x: auto; white-space: pre; }
|
|
174
|
+
.sb-copy-btn, .sb-reset-btn { padding: 0.4rem 1.2rem; border: 1px solid #ccc; border-radius: 0.2rem; cursor: pointer; background: transparent; font-size: 1.3rem; color: inherit; }
|
|
175
|
+
.sb-copy-btn:hover, .sb-reset-btn:hover { background: #e0e0e0; }
|
|
176
|
+
</component>
|
|
211
177
|
</div>
|
|
212
|
-
|
|
213
|
-
<style>
|
|
214
|
-
.sb-tabnav-story {
|
|
215
|
-
font-size: 1.4rem;
|
|
216
|
-
display: grid;
|
|
217
|
-
gap: 2.4rem;
|
|
218
|
-
padding: 2.4rem;
|
|
219
|
-
}
|
|
220
|
-
.sb-tabnav-note {
|
|
221
|
-
font-size: 1.3rem;
|
|
222
|
-
color: #888;
|
|
223
|
-
font-style: italic;
|
|
224
|
-
}
|
|
225
|
-
.sb-tabnav-note code {
|
|
226
|
-
font-family: monospace;
|
|
227
|
-
background: rgba(128,128,128,0.15);
|
|
228
|
-
padding: 0.1em 0.4em;
|
|
229
|
-
border-radius: 0.2rem;
|
|
230
|
-
}
|
|
231
|
-
.sb-tabnav-header {
|
|
232
|
-
display: flex;
|
|
233
|
-
align-items: center;
|
|
234
|
-
gap: 3.2rem;
|
|
235
|
-
padding: 1.2rem 2.4rem;
|
|
236
|
-
transition: background 300ms;
|
|
237
|
-
position: relative;
|
|
238
|
-
min-height: 6rem;
|
|
239
|
-
border-radius: 0.4rem;
|
|
240
|
-
}
|
|
241
|
-
.sb-tabnav-header.theme-dark {
|
|
242
|
-
background: var(--page-bg, #1a1614);
|
|
243
|
-
color-scheme: dark;
|
|
244
|
-
}
|
|
245
|
-
.sb-tabnav-header.theme-light {
|
|
246
|
-
background: var(--page-bg, #f5f2f0);
|
|
247
|
-
color-scheme: light;
|
|
248
|
-
}
|
|
249
|
-
.sb-tabnav-logo {
|
|
250
|
-
font-size: 1.8rem;
|
|
251
|
-
font-weight: 700;
|
|
252
|
-
letter-spacing: 0.1em;
|
|
253
|
-
flex-shrink: 0;
|
|
254
|
-
opacity: 0.5;
|
|
255
|
-
}
|
|
256
|
-
.sb-tabnav-header .tab-navigation {
|
|
257
|
-
flex: 1;
|
|
258
|
-
min-width: 0;
|
|
259
|
-
}
|
|
260
|
-
.sb-tabnav-playground {
|
|
261
|
-
display: grid;
|
|
262
|
-
grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr));
|
|
263
|
-
gap: 1.6rem;
|
|
264
|
-
}
|
|
265
|
-
.sb-tabnav-playground fieldset {
|
|
266
|
-
border: 1px solid #ccc;
|
|
267
|
-
border-radius: 0.4rem;
|
|
268
|
-
padding: 1.6rem;
|
|
269
|
-
}
|
|
270
|
-
.sb-tabnav-playground legend {
|
|
271
|
-
font-weight: bold;
|
|
272
|
-
padding: 0 0.8rem;
|
|
273
|
-
font-size: 1.4rem;
|
|
274
|
-
}
|
|
275
|
-
.sb-control-row {
|
|
276
|
-
display: grid;
|
|
277
|
-
grid-template-columns: 1fr 14rem;
|
|
278
|
-
gap: 1rem;
|
|
279
|
-
align-items: center;
|
|
280
|
-
padding: 0.5rem 0;
|
|
281
|
-
font-size: 1.3rem;
|
|
282
|
-
}
|
|
283
|
-
.sb-control-row input[type="color"] {
|
|
284
|
-
height: 3.2rem;
|
|
285
|
-
width: 100%;
|
|
286
|
-
padding: 0.2rem;
|
|
287
|
-
border: 1px solid #ccc;
|
|
288
|
-
border-radius: 0.2rem;
|
|
289
|
-
cursor: pointer;
|
|
290
|
-
background: transparent;
|
|
291
|
-
}
|
|
292
|
-
.sb-theme-toggle { display: flex; gap: 0.4rem; }
|
|
293
|
-
.sb-theme-toggle button {
|
|
294
|
-
padding: 0.4rem 1.6rem;
|
|
295
|
-
border: 1px solid #ccc;
|
|
296
|
-
border-radius: 0.2rem;
|
|
297
|
-
cursor: pointer;
|
|
298
|
-
background: transparent;
|
|
299
|
-
font-size: 1.3rem;
|
|
300
|
-
color: inherit;
|
|
301
|
-
}
|
|
302
|
-
.sb-theme-toggle button.active { background: #e0e0e0; font-weight: bold; }
|
|
303
|
-
.sb-css-snippet {
|
|
304
|
-
border: 1px solid #ccc;
|
|
305
|
-
border-radius: 0.4rem;
|
|
306
|
-
overflow: hidden;
|
|
307
|
-
}
|
|
308
|
-
.sb-css-snippet-header {
|
|
309
|
-
display: flex;
|
|
310
|
-
justify-content: space-between;
|
|
311
|
-
align-items: center;
|
|
312
|
-
padding: 1rem 1.6rem;
|
|
313
|
-
border-bottom: 1px solid #ccc;
|
|
314
|
-
font-size: 1.4rem;
|
|
315
|
-
}
|
|
316
|
-
.sb-css-snippet-code {
|
|
317
|
-
margin: 0;
|
|
318
|
-
padding: 1.6rem;
|
|
319
|
-
font-family: monospace;
|
|
320
|
-
font-size: 1.3rem;
|
|
321
|
-
line-height: 1.6;
|
|
322
|
-
overflow-x: auto;
|
|
323
|
-
white-space: pre;
|
|
324
|
-
}
|
|
325
|
-
.sb-copy-btn {
|
|
326
|
-
padding: 0.4rem 1.2rem;
|
|
327
|
-
border: 1px solid #ccc;
|
|
328
|
-
border-radius: 0.2rem;
|
|
329
|
-
cursor: pointer;
|
|
330
|
-
background: transparent;
|
|
331
|
-
font-size: 1.3rem;
|
|
332
|
-
color: inherit;
|
|
333
|
-
}
|
|
334
|
-
.sb-copy-btn:hover { background: #e0e0e0; }
|
|
335
|
-
</style>
|
|
336
178
|
`,
|
|
337
179
|
});
|
|
338
180
|
|
|
339
181
|
export const Default = DefaultTemplate.bind({});
|
|
340
182
|
Default.args = { navAlign: "left" };
|
|
183
|
+
|
|
184
|
+
// ─── Stress test ──────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
const longGerman = "Haarverlängerungsbehandlungsberatungsterminvereinbarung";
|
|
187
|
+
|
|
188
|
+
const stressNavItemData: NavItemData = {
|
|
189
|
+
main: [
|
|
190
|
+
{ text: "Startseite", href: "#start" },
|
|
191
|
+
{ text: `${longGerman} und mehr`, href: "#lang" },
|
|
192
|
+
{ text: "", href: "#blank" },
|
|
193
|
+
{ text: " ", href: "#spaces" },
|
|
194
|
+
{ text: "😀 Emoji first", href: "#emoji", iconName: "this-icon:does-not-exist" },
|
|
195
|
+
{ text: "<b>not bold</b>", href: "#html" },
|
|
196
|
+
{ text: "تسريحات شعر جديدة", href: "#rtl" },
|
|
197
|
+
{ text: "https://example.com/a/very/long/url/that/never/breaks/at/all", href: "https://example.com", isExternal: true },
|
|
198
|
+
{ text: "Same href one", href: "#same" },
|
|
199
|
+
{ text: "Same href two", href: "#same" },
|
|
200
|
+
{ text: "No href" },
|
|
201
|
+
{ text: "A", href: "#a" },
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export const StressTest: StoryObj<typeof TabNavigationComponent> = {
|
|
206
|
+
name: "Stress Test (Worst-Case Data)",
|
|
207
|
+
args: {
|
|
208
|
+
ariaLabel: "Hauptnavigation",
|
|
209
|
+
openMenuLabel: "Navigationsmenü öffnen",
|
|
210
|
+
closeMenuLabel: "Navigationsmenü schließen",
|
|
211
|
+
},
|
|
212
|
+
render: (args) => ({
|
|
213
|
+
components: { TabNavigationComponent },
|
|
214
|
+
setup() {
|
|
215
|
+
return { args, stressNavItemData };
|
|
216
|
+
},
|
|
217
|
+
template: `
|
|
218
|
+
<div
|
|
219
|
+
style="position: relative; display: flex; align-items: center; gap: 2.4rem; padding: 1.2rem 2.4rem; min-height: 6rem;"
|
|
220
|
+
>
|
|
221
|
+
<strong style="flex-shrink: 0;">LOGO</strong>
|
|
222
|
+
<TabNavigationComponent v-bind="args" :nav-item-data="stressNavItemData" style="flex: 1; min-inline-size: 0;" />
|
|
223
|
+
</div>
|
|
224
|
+
`,
|
|
225
|
+
}),
|
|
226
|
+
decorators: [
|
|
227
|
+
(story, context) => ({
|
|
228
|
+
components: { story, CanvasSwitcher },
|
|
229
|
+
setup() {
|
|
230
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
231
|
+
return { canvasName };
|
|
232
|
+
},
|
|
233
|
+
template: `
|
|
234
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
235
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
236
|
+
</div>
|
|
237
|
+
<div :class="canvasName" style="margin-inline: auto; padding-block-end: 40rem; outline: 1px dashed currentColor;">
|
|
238
|
+
<story />
|
|
239
|
+
</div>
|
|
240
|
+
`,
|
|
241
|
+
}),
|
|
242
|
+
],
|
|
243
|
+
parameters: {
|
|
244
|
+
layout: "fullscreen",
|
|
245
|
+
initialCanvas: "fullWidthCanvas",
|
|
246
|
+
docs: {
|
|
247
|
+
description: {
|
|
248
|
+
story:
|
|
249
|
+
"Deliberately hostile data to find breakage: twelve items including a long German compound word, an unbroken " +
|
|
250
|
+
"URL, emoji (with a broken icon name), HTML-like text (must render as text), right-to-left Arabic, a single " +
|
|
251
|
+
"character, two items sharing an href, one with no href, and two blank labels (skipped: a link with no text " +
|
|
252
|
+
"has no accessible name), plus German landmark and burger labels." +
|
|
253
|
+
" Check at every canvas width: the bar " +
|
|
254
|
+
"collapses to the burger as soon as the items don\x27t fit rather than overflowing; in the open panel every " +
|
|
255
|
+
"label wraps inside the panel; Tab shows a focus ring on each link; and the burger\x27s label switches language.",
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
};
|
|
@@ -331,9 +331,49 @@ describe("TabNavigation", () => {
|
|
|
331
331
|
expect(wrapper.find(".tab-nav-panel").attributes("inert")).toBeUndefined();
|
|
332
332
|
});
|
|
333
333
|
|
|
334
|
-
it("
|
|
334
|
+
it("burger aria-controls points at the panel's own unique id", async () => {
|
|
335
335
|
const { wrapper } = await mountCollapsed();
|
|
336
|
-
|
|
336
|
+
const panelId = wrapper.find(".tab-nav-panel").attributes("id");
|
|
337
|
+
expect(panelId).toBeTruthy();
|
|
338
|
+
expect(panelId).not.toBe("tab-nav-panel");
|
|
339
|
+
expect(wrapper.find(".tab-nav-burger").attributes("aria-controls")).toBe(panelId);
|
|
337
340
|
});
|
|
341
|
+
|
|
342
|
+
it("uses openMenuLabel / closeMenuLabel for the burger", async () => {
|
|
343
|
+
const wrapper = await mountSuspended(TabNavigation, {
|
|
344
|
+
props: { navItemData: defaultNavItemData, openMenuLabel: "Menü öffnen", closeMenuLabel: "Menü schließen" },
|
|
345
|
+
});
|
|
346
|
+
const setup = (wrapper.vm as unknown as { $: { setupState: TabNavSetup } }).$.setupState;
|
|
347
|
+
setup.isCollapsed = true;
|
|
348
|
+
await nextTick();
|
|
349
|
+
expect(wrapper.find(".tab-nav-burger").text()).toContain("Menü öffnen");
|
|
350
|
+
await wrapper.find(".tab-nav-burger").trigger("click");
|
|
351
|
+
expect(wrapper.find(".tab-nav-burger").text()).toContain("Menü schließen");
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// ─── Worst-case data ─────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
it("skips items with blank text", async () => {
|
|
358
|
+
const wrapper = await mountSuspended(TabNavigation, {
|
|
359
|
+
props: {
|
|
360
|
+
navItemData: { main: [{ text: "Home", href: "/" }, { text: " ", href: "/blank" }, { text: "", href: "/empty" }] },
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
expect(wrapper.findAll(".tab-nav-link")).toHaveLength(1);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("renders duplicate and missing hrefs without collapsing items", async () => {
|
|
367
|
+
const wrapper = await mountSuspended(TabNavigation, {
|
|
368
|
+
props: {
|
|
369
|
+
navItemData: { main: [{ text: "One", href: "/same" }, { text: "Two", href: "/same" }, { text: "Three" }, { text: "Four" }] },
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
expect(wrapper.findAll(".tab-nav-link")).toHaveLength(4);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("renders no nav landmark when there are no items", async () => {
|
|
376
|
+
const wrapper = await mountSuspended(TabNavigation, { props: { navItemData: { main: [] } } });
|
|
377
|
+
expect(wrapper.find("nav").exists()).toBe(false);
|
|
338
378
|
});
|
|
339
379
|
});
|
|
@@ -25,6 +25,7 @@ deliberately matches `PriceList`.
|
|
|
25
25
|
| `--opening-hours-day-font-size` | `1.4rem` | Day name / exception label size |
|
|
26
26
|
| `--opening-hours-day-colour` | `inherit` | Day name / exception label colour |
|
|
27
27
|
| `--opening-hours-hours-font-size` | `1.4rem` | Times and status text size |
|
|
28
|
+
| `--opening-hours-hours-max-inline-size` | `60%` | Most of the row the hours column can take. Longer content (long session labels, notes, status labels) wraps inside it |
|
|
28
29
|
| `--opening-hours-closed-colour` | `inherit` | Hours colour on closed rows |
|
|
29
30
|
|
|
30
31
|
### Sessions and notes
|
|
@@ -36,6 +37,9 @@ deliberately matches `PriceList`.
|
|
|
36
37
|
| `--opening-hours-session-label-colour` | `inherit` | Session label colour |
|
|
37
38
|
| `--opening-hours-note-font-size` | `1.2rem` | Note size |
|
|
38
39
|
| `--opening-hours-note-colour` | `inherit` | Note colour |
|
|
40
|
+
| `--opening-hours-note-line-clamp` | `none` | Cap notes at this many lines, with an ellipsis on the last. `1` is single-line ellipsis, `none` shows everything |
|
|
41
|
+
|
|
42
|
+
Each session's times stay on one line; a long session label wraps before them.
|
|
39
43
|
|
|
40
44
|
### Today
|
|
41
45
|
|
|
@@ -54,8 +58,13 @@ deliberately matches `PriceList`.
|
|
|
54
58
|
| `--opening-hours-exceptions-heading-margin-block-end` | `1.2rem` | Space below the exceptions heading |
|
|
55
59
|
| `--opening-hours-date-font-size` | `1.2rem` | Exception date size |
|
|
56
60
|
| `--opening-hours-date-colour` | `inherit` | Exception date colour |
|
|
61
|
+
| `--opening-hours-exception-label-line-clamp` | `none` | Cap exception labels at this many lines. `1` is single-line ellipsis, `none` shows everything |
|
|
62
|
+
|
|
63
|
+
There are no private `--_` tokens. Long unbroken text anywhere in the component wraps rather than overflowing.
|
|
57
64
|
|
|
58
|
-
|
|
65
|
+
> **Changed 2026-10-05**: rows are a grid instead of a flex row, so the day column can shrink and
|
|
66
|
+
> the hours column is capped by `--opening-hours-hours-max-inline-size`. `.opening-hours__session`
|
|
67
|
+
> is no longer `nowrap`; the new `.opening-hours__session-times` wrapper is.
|
|
59
68
|
|
|
60
69
|
---
|
|
61
70
|
|
|
@@ -77,6 +86,7 @@ Inner element classes, safe to target:
|
|
|
77
86
|
| `.opening-hours__hours` | `<dd>` |
|
|
78
87
|
| `.opening-hours__session` | One opening session |
|
|
79
88
|
| `.opening-hours__session-label` | Session label ("Lunch") |
|
|
89
|
+
| `.opening-hours__session-times` | The opens–closes pair, kept on one line |
|
|
80
90
|
| `.opening-hours__status` | "Closed" / "Open 24 hours" / "By appointment only" |
|
|
81
91
|
| `.opening-hours__note` | Per-day or per-exception note |
|
|
82
92
|
| `.opening-hours__exceptions` | Exceptions wrapper |
|