srcdev-nuxt-components 9.0.18 → 9.1.1
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 +4 -2
- package/.claude/skills/component-inline-action-button.md +79 -0
- package/.claude/skills/components/input-copy-core.md +66 -0
- package/.claude/skills/components/page-hero-highlights.md +60 -0
- package/.claude/skills/components/site-navigation.md +120 -0
- package/.claude/skills/icon-sets.md +45 -0
- package/.claude/skills/index.md +7 -1
- package/.claude/skills/performance-review.md +105 -0
- package/.claude/skills/robots-env-aware.md +69 -0
- package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_error.css +1 -1
- package/app/assets/styles/setup/02.colours/_amber.css +2 -2
- package/app/assets/styles/setup/03.theming/default/_dark.css +20 -2
- package/app/assets/styles/setup/03.theming/default/_light.css +11 -1
- package/app/assets/styles/setup/03.theming/error/_dark.css +1 -1
- package/app/components/01.atoms/text-blocks/eyebrow-text/EyebrowText.vue +15 -12
- package/app/components/01.atoms/text-blocks/hero-text/HeroText.vue +3 -1
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +780 -0
- package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +335 -0
- package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +328 -0
- package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +30 -0
- package/app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue +36 -21
- package/app/components/04.templates/page-hero-highlights/PageHeroHighlightsHeader.vue +66 -0
- package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts +50 -3
- package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlightsHeader.stories.ts +77 -0
- package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts +15 -7
- package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlightsHeader.spec.ts +51 -0
- package/app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap +1 -1
- package/app/components/forms/form-errors/InputError.vue +104 -103
- package/app/components/forms/input-copy/InputCopyCore.vue +132 -0
- package/app/components/forms/input-copy/stories/InputCopyCore.stories.ts +89 -0
- package/app/components/forms/input-copy/tests/InputCopyCore.spec.ts +212 -0
- package/app/components/forms/input-copy/tests/__snapshots__/InputCopyCore.spec.ts.snap +28 -0
- package/app/layouts/default.vue +1 -0
- package/app/pages/index.vue +0 -5
- package/app/pages/page-hero-highlights.vue +15 -11
- package/modules/icon-sets.ts +53 -0
- package/nuxt.config.ts +8 -0
- package/package.json +49 -6
- package/app/components/03.organisms/treatment-consultant/TreatmentConsultant.vue +0 -2204
- package/app/components/03.organisms/treatment-consultant/stories/TreatmentConsultant.stories.ts +0 -38
- package/app/pages/ui/services/treatment-consultant.vue +0 -39
package/.claude/settings.json
CHANGED
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
"Bash(git mv:*)",
|
|
16
16
|
"Bash(npx tsc:*)",
|
|
17
17
|
"Bash(npx vitest:*)",
|
|
18
|
-
"Bash(git show:*)"
|
|
18
|
+
"Bash(git show:*)",
|
|
19
|
+
"Edit(/.claude/skills/components/**)"
|
|
19
20
|
],
|
|
20
21
|
"additionalDirectories": [
|
|
21
22
|
"/Users/simoncornforth/websites/nuxt-components/app/components/01.atoms/content-wrappers/content-width",
|
|
22
|
-
"/Users/simoncornforth/websites/nuxt-components/.claude/skills"
|
|
23
|
+
"/Users/simoncornforth/websites/nuxt-components/.claude/skills",
|
|
24
|
+
"/Users/simoncornforth/websites/nuxt-components/app/components/02.molecules/navigation/site-navigation/tests"
|
|
23
25
|
]
|
|
24
26
|
}
|
|
25
27
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Inline Action Button in a Custom Input Wrapper
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
When a component needs an action button visually attached to an input-like wrapper (e.g. copy-to-clipboard, search-submit), use `InputButtonCore variant="inline"` rather than a raw `<button>`. The `inline` variant applies no built-in styles intentionally — the parent component's CSS provides all context. This keeps the button consistent with the rest of the input system (focus rings, hover tokens, transition timing) without duplicating the button style system.
|
|
6
|
+
|
|
7
|
+
## Pattern
|
|
8
|
+
|
|
9
|
+
```vue
|
|
10
|
+
<template>
|
|
11
|
+
<div class="my-wrapper" :class="{ 'some-state': isActive }">
|
|
12
|
+
<input class="my-field" ... />
|
|
13
|
+
<InputButtonCore
|
|
14
|
+
type="button"
|
|
15
|
+
variant="inline"
|
|
16
|
+
class="my-action-button"
|
|
17
|
+
:button-text="label"
|
|
18
|
+
:aria-label="label"
|
|
19
|
+
@click="handleAction"
|
|
20
|
+
>
|
|
21
|
+
<template v-if="slots.icon" #left>
|
|
22
|
+
<slot name="icon"></slot>
|
|
23
|
+
</template>
|
|
24
|
+
</InputButtonCore>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`aria-label` is not a declared prop on InputButtonCore — it falls through to the root element via Vue's default `inheritAttrs: true`.
|
|
30
|
+
|
|
31
|
+
## CSS
|
|
32
|
+
|
|
33
|
+
Target `.my-action-button.input-button-core` inside the wrapper to override InputButtonCore's defaults. Use theme tokens — not private `--_` tokens — for colours that already have theme equivalents:
|
|
34
|
+
|
|
35
|
+
```css
|
|
36
|
+
@layer components {
|
|
37
|
+
.my-wrapper {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: stretch;
|
|
40
|
+
border: var(--form-element-border-width) solid var(--theme-input-border);
|
|
41
|
+
border-radius: var(--form-input-border-radius);
|
|
42
|
+
background-color: var(--theme-input-surface);
|
|
43
|
+
|
|
44
|
+
.my-action-button.input-button-core {
|
|
45
|
+
border-radius: 0;
|
|
46
|
+
border-inline-start: var(--form-element-border-width) solid var(--theme-input-border);
|
|
47
|
+
padding-inline: var(--input-padding-inline);
|
|
48
|
+
min-width: var(--input-min-height);
|
|
49
|
+
background-color: var(--theme-button-secondary-surface);
|
|
50
|
+
color: var(--theme-button-secondary-text);
|
|
51
|
+
|
|
52
|
+
&:hover {
|
|
53
|
+
background-color: var(--theme-button-primary-surface);
|
|
54
|
+
color: var(--theme-button-primary-text);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
&:focus-visible {
|
|
58
|
+
outline: var(--form-element-outline-width-focus) solid var(--theme-input-outline-focus);
|
|
59
|
+
outline-offset: -4px;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Use private tokens only for new semantic states with no theme equivalent */
|
|
64
|
+
&.some-state {
|
|
65
|
+
--_state-surface: light-dark(var(--green-01), var(--green-09));
|
|
66
|
+
--_state-text: light-dark(var(--green-08), var(--green-01));
|
|
67
|
+
|
|
68
|
+
.my-action-button.input-button-core {
|
|
69
|
+
background-color: var(--_state-surface);
|
|
70
|
+
color: var(--_state-text);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Real example
|
|
78
|
+
|
|
79
|
+
`InputCopyCore` — `app/components/forms/input-copy/InputCopyCore.vue`
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# InputCopyCore
|
|
2
|
+
|
|
3
|
+
Readonly input that displays a value with a one-click copy-to-clipboard button. Intended for license keys, API tokens, share URLs, etc.
|
|
4
|
+
|
|
5
|
+
**File**: `app/components/forms/input-copy/InputCopyCore.vue`
|
|
6
|
+
|
|
7
|
+
## Props
|
|
8
|
+
|
|
9
|
+
| Prop | Type | Default | Description |
|
|
10
|
+
|------|------|---------|-------------|
|
|
11
|
+
| `id` | `string` | required | `id` attribute on the `<input>` |
|
|
12
|
+
| `value` | `string` | required | Text to display and copy |
|
|
13
|
+
| `copy-label` | `string` | `"Copy"` | Button label before copying |
|
|
14
|
+
| `copied-label` | `string` | `"Copied!"` | Button label after a successful copy |
|
|
15
|
+
| `feedback-duration` | `number` | `2000` | ms to show the copied state before resetting |
|
|
16
|
+
| `style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the root element |
|
|
17
|
+
|
|
18
|
+
## Emits
|
|
19
|
+
|
|
20
|
+
| Event | Payload | Description |
|
|
21
|
+
|-------|---------|-------------|
|
|
22
|
+
| `copy` | `value: string` | Fires on successful clipboard write |
|
|
23
|
+
|
|
24
|
+
## Slots
|
|
25
|
+
|
|
26
|
+
| Slot | Description |
|
|
27
|
+
|------|-------------|
|
|
28
|
+
| `icon` | Optional icon shown to the left of the button label (passes through to InputButtonCore `#left`) |
|
|
29
|
+
|
|
30
|
+
## Behaviour
|
|
31
|
+
|
|
32
|
+
- Calls `navigator.clipboard.writeText(value)` on button click
|
|
33
|
+
- On success: adds `.copied` class to root, shows `copiedLabel`, emits `copy`
|
|
34
|
+
- After `feedbackDuration` ms: removes `.copied` class, reverts button label
|
|
35
|
+
- Clipboard failure is caught silently — no `.copied` state is set
|
|
36
|
+
|
|
37
|
+
## CSS classes
|
|
38
|
+
|
|
39
|
+
| Class | Where | Description |
|
|
40
|
+
|-------|-------|-------------|
|
|
41
|
+
| `.input-copy-core` | root | always present |
|
|
42
|
+
| `.copied` | root | present during feedback window |
|
|
43
|
+
| `.input-copy-field` | `<input>` | the readonly text field |
|
|
44
|
+
| `.input-copy-button` | button | extra class added to InputButtonCore root |
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
```vue
|
|
49
|
+
<InputCopyCore
|
|
50
|
+
id="license-key-input"
|
|
51
|
+
value="sk_live_abc123def456"
|
|
52
|
+
copy-label="Copy key"
|
|
53
|
+
copied-label="Copied!"
|
|
54
|
+
:feedback-duration="2000"
|
|
55
|
+
/>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
With icon slot (e.g. using a Radix icon):
|
|
59
|
+
|
|
60
|
+
```vue
|
|
61
|
+
<InputCopyCore id="api-key" value="sk_live_abc123def456">
|
|
62
|
+
<template #icon>
|
|
63
|
+
<Icon name="radix-icons:clipboard" class="icon" />
|
|
64
|
+
</template>
|
|
65
|
+
</InputCopyCore>
|
|
66
|
+
```
|
|
@@ -19,6 +19,8 @@ The layout uses a 4-row CSS Grid with `subgrid` — no `translate`, negative mar
|
|
|
19
19
|
| `highlightsJustify` | `"start" \| "center" \| "end" \| "space-between" \| "space-around"` | `"start"` | Alignment of highlight items along the main axis |
|
|
20
20
|
| `maxWidth` | `string` | `undefined` | Cap the central content column (e.g. `"1064px"`). Gutters grow to enforce the constraint; below this width they hold at `16px`. |
|
|
21
21
|
| `contentAlign` | `"start" \| "center"` | `"center"` | When `maxWidth` is set: `"center"` grows gutters equally; `"start"` pins content to the left with a fixed `16px` left gutter. |
|
|
22
|
+
| `contentPanel` | `boolean` | `true` | When `true`, renders a decorative panel behind the content slot and offsets the highlights strip. Set to `false` for a flat layout with no backdrop. |
|
|
23
|
+
| `highlightTitleBaseline`| `boolean` | `false` | When `true`, fixes the highlight title row to a set height so titles align at a common baseline. Override `--highlight-title-height` to tune. |
|
|
22
24
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root element |
|
|
23
25
|
|
|
24
26
|
## Slots
|
|
@@ -83,6 +85,64 @@ Example with `HeroText` in the header slot:
|
|
|
83
85
|
</PageHeroHighlights>
|
|
84
86
|
```
|
|
85
87
|
|
|
88
|
+
## PageHeroHighlightsHeader companion component
|
|
89
|
+
|
|
90
|
+
A co-located companion component for laying out the `#header` slot. Provides a responsive two-area layout: `#start` (title/description) and `#end` (action buttons), stacking vertically on mobile and sitting side-by-side on wider viewports.
|
|
91
|
+
|
|
92
|
+
Located at: `app/components/04.templates/page-hero-highlights/PageHeroHighlightsHeader.vue`
|
|
93
|
+
|
|
94
|
+
### PageHeroHighlightsHeader props
|
|
95
|
+
|
|
96
|
+
| Prop | Type | Default | Description |
|
|
97
|
+
| ----------------------- | -------------------- | ------- | --------------------------------- |
|
|
98
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root element |
|
|
99
|
+
|
|
100
|
+
### PageHeroHighlightsHeader slots
|
|
101
|
+
|
|
102
|
+
| Slot | Purpose |
|
|
103
|
+
| ------- | --------------------------------------------------------- |
|
|
104
|
+
| `start` | Title and description — always rendered, fills full width when `#end` is absent |
|
|
105
|
+
| `end` | Action buttons — `.phh-end` is only mounted when this slot is provided |
|
|
106
|
+
|
|
107
|
+
### CSS tokens
|
|
108
|
+
|
|
109
|
+
| Token | Default | Description |
|
|
110
|
+
| ------------------ | -------- | ---------------------------------------- |
|
|
111
|
+
| `--phh-padding-block` | `1.6rem` | Block padding on the header |
|
|
112
|
+
| `--phh-gap` | `1.6rem` | Gap between `#start` and `#end` areas |
|
|
113
|
+
| `--phh-end-gap` | `0.8rem` | Gap between items within `#end` |
|
|
114
|
+
|
|
115
|
+
### Usage
|
|
116
|
+
|
|
117
|
+
```vue
|
|
118
|
+
<PageHeroHighlights tag="section">
|
|
119
|
+
<template #header="{ headingId }">
|
|
120
|
+
<PageHeroHighlightsHeader>
|
|
121
|
+
<template #start>
|
|
122
|
+
<h1 :id="headingId" class="page-heading-1">Surplus needs</h1>
|
|
123
|
+
<p class="page-body-normal">Let us know what you need help with.</p>
|
|
124
|
+
</template>
|
|
125
|
+
<template #end>
|
|
126
|
+
<HelpButton />
|
|
127
|
+
<Button>Create new need</Button>
|
|
128
|
+
</template>
|
|
129
|
+
</PageHeroHighlightsHeader>
|
|
130
|
+
</template>
|
|
131
|
+
...
|
|
132
|
+
</PageHeroHighlights>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Omit `#end` for a single-element header — `#start` fills full width with no layout change needed:
|
|
136
|
+
|
|
137
|
+
```vue
|
|
138
|
+
<PageHeroHighlightsHeader>
|
|
139
|
+
<template #start>
|
|
140
|
+
<h1 :id="headingId">Dashboard</h1>
|
|
141
|
+
<p>Overview of your account activity.</p>
|
|
142
|
+
</template>
|
|
143
|
+
</PageHeroHighlightsHeader>
|
|
144
|
+
```
|
|
145
|
+
|
|
86
146
|
## With aria-labelledby (section tag)
|
|
87
147
|
|
|
88
148
|
When `tag="section"`, `aria-labelledby` is set automatically. Wire the heading id via the scoped slot prop:
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# SiteNavigation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`SiteNavigation` is a responsive site-wide navigation component. It renders a horizontal link list on wide viewports and automatically collapses to a burger-menu + slide-down panel on narrow viewports. Collapse is driven by a `ResizeObserver` that compares the list's natural `scrollWidth` against the nav container's `clientWidth` — no breakpoint prop needed.
|
|
6
|
+
|
|
7
|
+
Both the horizontal list and the panel include animated active/hover indicator decorators (underline indicator + background highlight) that snap into position using CSS custom properties set via JavaScript.
|
|
8
|
+
|
|
9
|
+
## Component location
|
|
10
|
+
|
|
11
|
+
`app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue`
|
|
12
|
+
|
|
13
|
+
## Props
|
|
14
|
+
|
|
15
|
+
| Prop | Type | Default | Description |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| `navItemData` | `NavItemData` | — (required) | Navigation items — see type below |
|
|
18
|
+
| `navAlign` | `"left" \| "center" \| "right"` | `"left"` | Alignment of the horizontal nav list |
|
|
19
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root `<nav>` |
|
|
20
|
+
|
|
21
|
+
## NavItemData type
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import type { NavItemData } from "~/types/components/navigation-horizontal.d";
|
|
25
|
+
|
|
26
|
+
const navItemData: NavItemData = {
|
|
27
|
+
main: [
|
|
28
|
+
{ text: "Home", href: "/" },
|
|
29
|
+
{ text: "About", href: "/about" },
|
|
30
|
+
{ text: "Services", href: "/services", cssName: "is-featured" },
|
|
31
|
+
{ text: "Contact", href: "/contact", iconName: "heroicons:envelope", isExternal: false },
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`NavItem` fields:
|
|
37
|
+
|
|
38
|
+
| Field | Type | Description |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `text` | `string` | Link label |
|
|
41
|
+
| `href` | `string` | Link destination |
|
|
42
|
+
| `isExternal` | `boolean?` | Passed to NuxtLink `:external` — opens in new tab |
|
|
43
|
+
| `iconName` | `string?` | Iconify icon name rendered before the label |
|
|
44
|
+
| `cssName` | `string?` | CSS class applied to the `<li>` element |
|
|
45
|
+
|
|
46
|
+
## Basic usage
|
|
47
|
+
|
|
48
|
+
```vue
|
|
49
|
+
<SiteNavigation :nav-item-data="navItemData" nav-align="left" />
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Always use hyphenated prop names in templates (ESLint enforces this).
|
|
53
|
+
|
|
54
|
+
## CSS token API
|
|
55
|
+
|
|
56
|
+
Set these tokens on a parent element (e.g. your `<header>`) to theme the navigation.
|
|
57
|
+
|
|
58
|
+
```css
|
|
59
|
+
.your-header {
|
|
60
|
+
/* ── Decorators ────────────────────────────────────── */
|
|
61
|
+
--site-nav-decorator-indicator-color: var(--rose-05); /* underline bar */
|
|
62
|
+
|
|
63
|
+
/* ── Horizontal nav links ──────────────────────────── */
|
|
64
|
+
--site-nav-link-color: var(--warm-01);
|
|
65
|
+
--site-nav-link-hover-color: var(--rose-04);
|
|
66
|
+
--site-nav-link-active-color: var(--rose-05);
|
|
67
|
+
--site-nav-link-size: 1.6rem;
|
|
68
|
+
--site-nav-link-weight: 400;
|
|
69
|
+
--site-nav-link-tracking: 0.06em;
|
|
70
|
+
--site-nav-gap: 2.2rem;
|
|
71
|
+
--site-nav-transition: 250ms ease;
|
|
72
|
+
|
|
73
|
+
/* ── Mobile panel ──────────────────────────────────── */
|
|
74
|
+
--site-nav-panel-bg: var(--page-bg, #1a1614);
|
|
75
|
+
--site-nav-panel-border-color: color-mix(in oklch, var(--rose-05) 35%, transparent);
|
|
76
|
+
--site-nav-panel-link-color: var(--warm-01);
|
|
77
|
+
--site-nav-panel-link-hover-color: var(--rose-04);
|
|
78
|
+
--site-nav-panel-link-active-color: var(--rose-05);
|
|
79
|
+
--site-nav-panel-padding-block: 1.4rem;
|
|
80
|
+
--site-nav-panel-padding-inline: 1.5rem;
|
|
81
|
+
--site-nav-panel-slide-duration: 350ms;
|
|
82
|
+
--site-nav-panel-slide-easing: cubic-bezier(0.4, 0, 0.2, 1);
|
|
83
|
+
--site-nav-panel-decorator-indicator-color: var(--rose-05);
|
|
84
|
+
--site-nav-panel-indicator-left: 0; /* position the panel indicator bar */
|
|
85
|
+
--site-nav-panel-indicator-right: auto;
|
|
86
|
+
|
|
87
|
+
/* ── Burger button ─────────────────────────────────── */
|
|
88
|
+
--site-nav-burger-color: var(--warm-01);
|
|
89
|
+
--site-nav-burger-width: 22px;
|
|
90
|
+
--site-nav-burger-height: 1.5px;
|
|
91
|
+
--site-nav-burger-gap: 5px;
|
|
92
|
+
--site-nav-burger-transition: 300ms ease;
|
|
93
|
+
|
|
94
|
+
/* ── Backdrop (teleported to <body>) ───────────────── */
|
|
95
|
+
--site-nav-backdrop-bg: oklch(0% 0 0 / 55%);
|
|
96
|
+
--site-nav-backdrop-blur: 3px;
|
|
97
|
+
--site-nav-backdrop-duration: 350ms;
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Behaviour notes
|
|
102
|
+
|
|
103
|
+
- **Collapse detection**: `ResizeObserver` fires on every container resize. The list's `scrollWidth` is cached whenever the list is in the DOM; that cached value is compared against the container's `clientWidth` to set `isCollapsed`.
|
|
104
|
+
- **isLoaded state**: The component uses `useState("site-nav-loaded")` — a Nuxt shared state — to gate visibility until the first measurement is complete, preventing a flash of the wrong nav state on load. The nav renders `opacity: 0` until `is-loaded` is applied.
|
|
105
|
+
- **Teleport**: The backdrop overlay is teleported to `<body>` via `<Teleport>` and is only mounted when `isCollapsed && isLoaded`.
|
|
106
|
+
- **Panel inert**: The `#site-nav-panel` div receives `:inert="!isMenuOpen ? true : undefined"` — it is inert (keyboard/pointer-inaccessible) when closed.
|
|
107
|
+
- **Decorator init**: `initNavDecorators()` and `initPanelDecorators()` inject `<li>` elements with CSS-driven indicator `<div>`s. They query `[data-nav-item]` / `[data-panel-nav-item]` to find links, and look for `router-link-active` to set the initial active position.
|
|
108
|
+
|
|
109
|
+
## Accessibility
|
|
110
|
+
|
|
111
|
+
- Root `<nav>` has `aria-label="Site navigation"`.
|
|
112
|
+
- Burger button uses `aria-expanded` (string `"true"/"false"`) and `aria-controls="site-nav-panel"`.
|
|
113
|
+
- Backdrop and indicator `<li>` elements are `aria-hidden="true"`.
|
|
114
|
+
- Panel div is `inert` when closed.
|
|
115
|
+
|
|
116
|
+
## Related files
|
|
117
|
+
|
|
118
|
+
- Type: `app/types/components/navigation-horizontal.d.ts`
|
|
119
|
+
- Tests: `app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts`
|
|
120
|
+
- Story: `app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts`
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Icon Sets
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`@nuxt/icon` can serve icons from two sources:
|
|
6
|
+
|
|
7
|
+
1. **Local package** (`@iconify-json/*` installed in the project) — SVG is inlined at build/SSR time, zero runtime cost, no flash.
|
|
8
|
+
2. **Iconify CDN** (`api.iconify.design`) — icon is fetched client-side after JS loads, causing a visible flash of content (FOUC) on first page load.
|
|
9
|
+
|
|
10
|
+
The layer uses several icon sets across its components. Because these are in the layer's `devDependencies` (not `dependencies`), they are **not automatically installed** in consumer apps. Any missing set falls back to the CDN and will flash.
|
|
11
|
+
|
|
12
|
+
## How the layer signals missing sets
|
|
13
|
+
|
|
14
|
+
The layer's `modules/icon-sets.ts` runs at dev/build time and logs an info message listing any icon sets that are used by layer components but not found in the consumer's project. Check the terminal output when running `nuxt dev` or `nuxt build`.
|
|
15
|
+
|
|
16
|
+
## Component → icon set mapping
|
|
17
|
+
|
|
18
|
+
| Icon set | Package | Used by |
|
|
19
|
+
|----------|---------|---------|
|
|
20
|
+
| `akar-icons` | `@iconify-json/akar-icons` | DisplayToast, display-prompt variants |
|
|
21
|
+
| `bi` | `@iconify-json/bi` | NavigationItems (overflow caret) |
|
|
22
|
+
| `bitcoin-icons` | `@iconify-json/bitcoin-icons` | Display components |
|
|
23
|
+
| `gravity-ui` | `@iconify-json/gravity-ui` | NavigationItems (burger/ellipsis overflow) |
|
|
24
|
+
| `ic` | `@iconify-json/ic` | CarouselBasic, CarouselFlip, CarouselInfinite, SliderGallery, CanvasSwitcher |
|
|
25
|
+
| `lucide` | `@iconify-json/lucide` | ColourFinder, TreatmentConsultant |
|
|
26
|
+
| `material-symbols` | `@iconify-json/material-symbols` | ServicesSection, form components |
|
|
27
|
+
| `mdi` | `@iconify-json/mdi` | NavigationHorizontal, form components, ServicesCard |
|
|
28
|
+
| `radix-icons` | `@iconify-json/radix-icons` | InputPasswordWithLabel, InputError, DisplayThemeSwitch |
|
|
29
|
+
|
|
30
|
+
## Fix: install missing packages
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @iconify-json/akar-icons @iconify-json/bi @iconify-json/bitcoin-icons \
|
|
34
|
+
@iconify-json/gravity-ui @iconify-json/ic @iconify-json/lucide \
|
|
35
|
+
@iconify-json/material-symbols @iconify-json/mdi @iconify-json/radix-icons
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Only install the sets you actually need — unused ones cost nothing either way, but the install above covers everything the layer ships.
|
|
39
|
+
|
|
40
|
+
## Notes
|
|
41
|
+
|
|
42
|
+
- The layer's build-time info message only lists sets missing from the **consumer's own project**. It is not a hard error.
|
|
43
|
+
- A consumer app can install any additional icon sets it needs for its own components — these won't conflict.
|
|
44
|
+
- If you're not using a particular layer component (e.g. `ColourFinder`), missing `@iconify-json/lucide` won't cause any visible problem.
|
|
45
|
+
- The `peerDependencies` + `peerDependenciesMeta (optional: true)` in the layer's `package.json` is the npm-standard signal — package managers like npm 7+ will show a notice for missing optional peers during install.
|
package/.claude/skills/index.md
CHANGED
|
@@ -19,6 +19,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
19
19
|
```text
|
|
20
20
|
.claude/skills/
|
|
21
21
|
├── index.md — this file
|
|
22
|
+
├── performance-review.md — spawn a subagent to inspect recently written code for Vue/Nuxt performance issues before dev handoff
|
|
22
23
|
├── storybook-add-story.md — create a Storybook story for a component
|
|
23
24
|
├── storybook-add-font.md — add a new font to Storybook
|
|
24
25
|
├── testing-add-unit-test.md — create a Vitest unit test with snapshots
|
|
@@ -31,6 +32,9 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
31
32
|
├── css-grid-max-width-gutters.md — cap a centre grid column width by growing gutters, with start/center alignment variants
|
|
32
33
|
├── component-aria-landmark.md — useAriaLabelledById composable: aria-labelledby for section/main/article/aside tags
|
|
33
34
|
├── component-export-types.md — move inline component types to app/types/components/ barrel for consumer imports
|
|
35
|
+
├── component-inline-action-button.md — InputButtonCore variant="inline" pattern for buttons embedded in custom input wrappers
|
|
36
|
+
├── icon-sets.md — icon set packages required by layer components, FOUC prevention, component→package map
|
|
37
|
+
├── robots-env-aware.md — @nuxtjs/robots: allow crawling on prod domain only, block on preview/staging via env var
|
|
34
38
|
└── components/
|
|
35
39
|
├── accordian-core.md — AccordianCore indexed dynamic slots (accordian-{n}-summary/icon/content), exclusive-open grouping
|
|
36
40
|
├── eyebrow-text.md — EyebrowText props, usage patterns, styling
|
|
@@ -44,7 +48,9 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
44
48
|
├── contact-section.md — ContactSection props (stepperIndicatorSize pass-through), 3-item info+form layout, slot API
|
|
45
49
|
├── stepper-list.md — StepperList dynamic slots (item-{n}/indicator-{n}), props, connector behaviour
|
|
46
50
|
├── expanding-panel.md — ExpandingPanel v-model, forceOpened, slots (summary/icon/content), ARIA wiring
|
|
47
|
-
|
|
51
|
+
├── navigation-horizontal.md — NavigationHorizontal props, NavItemData type, CSS token API, import path gotcha
|
|
52
|
+
├── input-copy-core.md — InputCopyCore: readonly copy-to-clipboard input; props, emits, slots, CSS classes, usage
|
|
53
|
+
└── site-navigation.md — SiteNavigation: responsive nav with auto-collapse, burger menu, decorator indicators, CSS token API
|
|
48
54
|
```
|
|
49
55
|
|
|
50
56
|
## Skill file template
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Performance Review
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Spawn a subagent to inspect recently written or modified code for performance issues and report findings back before handoff to dev. Run this after code is written and ready for review.
|
|
6
|
+
|
|
7
|
+
## When to invoke
|
|
8
|
+
|
|
9
|
+
Invoke this skill (`/performance-review`) after completing any of:
|
|
10
|
+
- A new component
|
|
11
|
+
- A significant edit to an existing component
|
|
12
|
+
- A new composable
|
|
13
|
+
- A test file that exercises heavy setup
|
|
14
|
+
|
|
15
|
+
## Steps
|
|
16
|
+
|
|
17
|
+
### 1. Identify files to review
|
|
18
|
+
|
|
19
|
+
Collect the list of files changed in this conversation. If unclear, run `git diff --name-only HEAD` to get recently modified files.
|
|
20
|
+
|
|
21
|
+
### 2. Spawn the performance review subagent
|
|
22
|
+
|
|
23
|
+
Use the Agent tool with `subagent_type: "general-purpose"` and the following prompt, substituting `{FILE_LIST}` with the actual file paths:
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
**Subagent prompt template:**
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
You are a Vue 3 / Nuxt performance reviewer. Inspect the following files for performance issues and produce a concise report.
|
|
31
|
+
|
|
32
|
+
Files to review:
|
|
33
|
+
{FILE_LIST}
|
|
34
|
+
|
|
35
|
+
Read each file in full, then check for the following issues. Report only issues that are actually present — do not flag hypothetical problems.
|
|
36
|
+
|
|
37
|
+
─── Vue / Nuxt Reactivity ───────────────────────────────────────
|
|
38
|
+
□ Expensive operations inside computed properties (should be pure/cheap)
|
|
39
|
+
□ `watch` with no `{ immediate }` where it could cause double-execution on mount
|
|
40
|
+
□ Large objects stored in `ref()` where `shallowRef()` would suffice
|
|
41
|
+
□ Reactive state that is never mutated (should be a plain const)
|
|
42
|
+
□ Missing `watchEffect` cleanup / `onUnmounted` teardown for subscriptions or timers
|
|
43
|
+
|
|
44
|
+
─── Template Rendering ──────────────────────────────────────────
|
|
45
|
+
□ `v-for` without a stable `:key` (or using array index as key on mutable lists)
|
|
46
|
+
□ Heavy inline expressions in templates (should be computed properties)
|
|
47
|
+
□ `v-if` + `v-for` on the same element (use a wrapping element or computed filter)
|
|
48
|
+
□ Components mounted unconditionally that could be `v-if`-deferred until needed
|
|
49
|
+
□ Missing `v-once` on truly static subtrees
|
|
50
|
+
□ Transitions on elements that trigger layout (width/height) rather than transform/opacity
|
|
51
|
+
|
|
52
|
+
─── CSS / Styling ───────────────────────────────────────────────
|
|
53
|
+
□ CSS custom properties defined but never consumed (dead tokens)
|
|
54
|
+
□ Expensive selectors: deep descendant chains, universal selectors inside scoped blocks
|
|
55
|
+
□ `transition: all` (transitions every property — prefer explicit property list)
|
|
56
|
+
□ Animations that animate layout properties (top/left/width/height) instead of transform
|
|
57
|
+
□ `v-bind()` in CSS used for values that never change (should be a static custom property)
|
|
58
|
+
|
|
59
|
+
─── Images / Assets ─────────────────────────────────────────────
|
|
60
|
+
□ `NuxtImg` without explicit `width` and `height` (causes layout shift + Vercel width fallback)
|
|
61
|
+
□ Images without `loading="lazy"` where above-the-fold loading is not required
|
|
62
|
+
□ Large inline SVGs that could be icon components
|
|
63
|
+
|
|
64
|
+
─── Bundle / Imports ────────────────────────────────────────────
|
|
65
|
+
□ Manual imports of Vue internals (`ref`, `computed`, etc.) — these are auto-imported in Nuxt
|
|
66
|
+
□ Entire libraries imported where only specific named exports are needed
|
|
67
|
+
□ Unused imports left in the file
|
|
68
|
+
|
|
69
|
+
─── Data & Logic ────────────────────────────────────────────────
|
|
70
|
+
□ Large static data arrays defined inside `<script setup>` (re-created on every HMR — move outside the component or to a separate module)
|
|
71
|
+
□ Nested loops or O(n²) logic in computed properties or render functions
|
|
72
|
+
□ Repeated `.find()` / `.filter()` calls on the same array within a single render — consolidate into one computed
|
|
73
|
+
□ `setTimeout` / `setInterval` not cleared in `onUnmounted`
|
|
74
|
+
|
|
75
|
+
─── Report format ───────────────────────────────────────────────
|
|
76
|
+
For each issue found, output:
|
|
77
|
+
|
|
78
|
+
**File:** `path/to/file.vue` (line N)
|
|
79
|
+
**Issue:** One-sentence description of the problem
|
|
80
|
+
**Impact:** Low / Medium / High
|
|
81
|
+
**Fix:** Concrete code change or approach to resolve it
|
|
82
|
+
|
|
83
|
+
Group findings by file. If no issues are found in a file, skip it.
|
|
84
|
+
End the report with a one-line summary: total issues found, breakdown by impact level.
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### 3. Review the report
|
|
90
|
+
|
|
91
|
+
Read the subagent's findings. For each **High** or **Medium** impact issue:
|
|
92
|
+
- Apply the fix immediately if it is straightforward and does not change behaviour
|
|
93
|
+
- Flag it to the user with the suggested fix if it requires a design decision
|
|
94
|
+
|
|
95
|
+
For **Low** impact issues, present them as a list for the user to decide on.
|
|
96
|
+
|
|
97
|
+
### 4. Confirm completion
|
|
98
|
+
|
|
99
|
+
After applying any fixes, re-run the subagent on the modified files to confirm no new issues were introduced.
|
|
100
|
+
|
|
101
|
+
## Notes
|
|
102
|
+
|
|
103
|
+
- This skill is read-only when used via the subagent — it does not edit files directly. Edits are made by the main agent after reviewing the report.
|
|
104
|
+
- Focus on issues that are actually present in the code — do not pre-emptively flag patterns that might become a problem.
|
|
105
|
+
- The subagent uses `subagent_type: "general-purpose"` so it has access to all tools (Read, Grep, Glob) needed to inspect the files.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Environment-Aware robots.txt and Meta Robots
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
SSR apps often run on multiple domains — a preview/staging URL (e.g. Vercel's auto-generated domain) and the real production domain. This skill sets up `@nuxtjs/robots` so that crawling is allowed only on the production host, and blocked everywhere else — covering both `robots.txt` and `<meta name="robots">` injection.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- SSR deployment (e.g. Vercel)
|
|
10
|
+
- `@nuxtjs/robots` installed: `npm install @nuxtjs/robots`
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
### 1. Derive `isProduction` at the top of `nuxt.config.ts`
|
|
15
|
+
|
|
16
|
+
Before `defineNuxtConfig`, read the canonical host from an env var and compare it to the known production domain:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
const PROD_HOST = "yourdomain.com"
|
|
20
|
+
const canonicalHost = process.env.NUXT_PUBLIC_CANONICAL_HOST ?? "your-preview.vercel.app"
|
|
21
|
+
const isProduction = canonicalHost === PROD_HOST
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Using `??` means local dev and Vercel preview deployments default to the non-production host, so no special local config is needed.
|
|
25
|
+
|
|
26
|
+
### 2. Wire `canonicalHost` into `runtimeConfig` and add the robots module
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
export default defineNuxtConfig({
|
|
30
|
+
runtimeConfig: {
|
|
31
|
+
public: {
|
|
32
|
+
canonicalHost, // shorthand — value comes from the const above
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
modules: ["@nuxtjs/robots"],
|
|
36
|
+
robots: {
|
|
37
|
+
enabled: isProduction,
|
|
38
|
+
groups: [
|
|
39
|
+
{
|
|
40
|
+
userAgent: ["*"],
|
|
41
|
+
allow: ["/"],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
sitemap: [`https://${PROD_HOST}/sitemap.xml`],
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
When `enabled: false` the module:
|
|
50
|
+
- Serves `robots.txt` with `Disallow: /` for all user agents
|
|
51
|
+
- Injects `<meta name="robots" content="noindex, nofollow">` on every page
|
|
52
|
+
|
|
53
|
+
When `enabled: true` it serves the `groups` config and no blocking meta tag.
|
|
54
|
+
|
|
55
|
+
### 3. Set the env var in Vercel
|
|
56
|
+
|
|
57
|
+
In the Vercel dashboard → Project → Settings → Environment Variables:
|
|
58
|
+
|
|
59
|
+
| Name | Value | Environment |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `NUXT_PUBLIC_CANONICAL_HOST` | `yourdomain.com` | **Production** only |
|
|
62
|
+
|
|
63
|
+
Leave it unset for Preview and Development — the `??` fallback in step 1 handles those.
|
|
64
|
+
|
|
65
|
+
## Notes
|
|
66
|
+
|
|
67
|
+
- The `sitemap` URL always points to the production host (hardcoded via `PROD_HOST`) so it is never wrong regardless of which environment serves the file
|
|
68
|
+
- `NUXT_PUBLIC_CANONICAL_HOST` follows Nuxt's standard automatic env var mapping for `runtimeConfig.public.canonicalHost`
|
|
69
|
+
- No `sitemap.xml` is required immediately — remove the `sitemap` line until one is generated
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
--theme-input-surface-hover: light-dark(var(--slate-01), var(--slate-10));
|
|
15
15
|
--theme-input-surface-focus: light-dark(var(--slate-01), var(--slate-10));
|
|
16
16
|
|
|
17
|
-
--theme-input-border: var(--red-
|
|
17
|
+
--theme-input-border: var(--red-06);
|
|
18
18
|
--theme-input-border-hover: light-dark(var(--slate-10), var(--slate-05));
|
|
19
19
|
--theme-input-border-focus: light-dark(var(--slate-10), var(--slate-05));
|
|
20
20
|
|
|
@@ -32,6 +32,16 @@
|
|
|
32
32
|
--glass-panel-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
33
33
|
--glass-panel-highlight: rgba(255, 255, 255, 0.04);
|
|
34
34
|
|
|
35
|
+
/* ===========================================
|
|
36
|
+
HERO TEXT VARIABLES
|
|
37
|
+
=========================================== */
|
|
38
|
+
--hero-text-bg-img: linear-gradient(135deg, #c2a770, #b4747e, #d1bd94);
|
|
39
|
+
|
|
40
|
+
/* ===========================================
|
|
41
|
+
EYEBROW TEXT VARIABLES
|
|
42
|
+
=========================================== */
|
|
43
|
+
--eyebrow-text-bg-img: linear-gradient(135deg, #c2a770, #b4747e, #d1bd94);
|
|
44
|
+
|
|
35
45
|
/* ===========================================
|
|
36
46
|
STEPPER LIST VARIABLES
|
|
37
47
|
=========================================== */
|
|
@@ -66,8 +76,16 @@
|
|
|
66
76
|
/* ===========================================
|
|
67
77
|
TREATMENT CONSULTANT VARIABLES
|
|
68
78
|
=========================================== */
|
|
69
|
-
--treatment-consultant-primary-colour: var(--
|
|
70
|
-
--treatment-consultant-
|
|
79
|
+
--treatment-consultant-primary-colour: var(--amber-02);
|
|
80
|
+
--treatment-consultant-primary-foreground: var(--amber-02);
|
|
81
|
+
|
|
82
|
+
--treatment-consultant-muted-colour: var(--amber-05);
|
|
83
|
+
--treatment-consultant-muted-foreground: var(--amber-05);
|
|
84
|
+
|
|
85
|
+
--treatment-consultant-background: var(--amber-10);
|
|
86
|
+
--treatment-consultant-foreground: var(--amber-02);
|
|
87
|
+
|
|
88
|
+
--treatment-consultant-border-colour: var(--amber-03);
|
|
71
89
|
|
|
72
90
|
--treatment-consultant-checked-surface-colour: var(--green-10);
|
|
73
91
|
--treatment-consultant-checked-stroke-colour: var(--green-03);
|