srcdev-nuxt-components 9.0.18 → 9.1.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.
Files changed (27) hide show
  1. package/.claude/settings.json +2 -1
  2. package/.claude/skills/component-inline-action-button.md +79 -0
  3. package/.claude/skills/components/input-copy-core.md +66 -0
  4. package/.claude/skills/components/treatment-consultant.md +128 -0
  5. package/.claude/skills/icon-sets.md +45 -0
  6. package/.claude/skills/index.md +7 -1
  7. package/.claude/skills/performance-review.md +105 -0
  8. package/.claude/skills/robots-env-aware.md +69 -0
  9. package/app/assets/styles/extends-layer/srcdev-forms/setup/themes/_error.css +1 -1
  10. package/app/assets/styles/setup/02.colours/_amber.css +2 -2
  11. package/app/assets/styles/setup/03.theming/default/_dark.css +20 -2
  12. package/app/assets/styles/setup/03.theming/default/_light.css +11 -1
  13. package/app/assets/styles/setup/03.theming/error/_dark.css +1 -1
  14. package/app/components/01.atoms/text-blocks/eyebrow-text/EyebrowText.vue +15 -12
  15. package/app/components/01.atoms/text-blocks/hero-text/HeroText.vue +3 -1
  16. package/app/components/forms/form-errors/InputError.vue +104 -103
  17. package/app/components/forms/input-copy/InputCopyCore.vue +132 -0
  18. package/app/components/forms/input-copy/stories/InputCopyCore.stories.ts +89 -0
  19. package/app/components/forms/input-copy/tests/InputCopyCore.spec.ts +212 -0
  20. package/app/components/forms/input-copy/tests/__snapshots__/InputCopyCore.spec.ts.snap +28 -0
  21. package/app/pages/index.vue +0 -5
  22. package/modules/icon-sets.ts +53 -0
  23. package/nuxt.config.ts +1 -0
  24. package/package.json +44 -1
  25. package/app/components/03.organisms/treatment-consultant/TreatmentConsultant.vue +0 -2204
  26. package/app/components/03.organisms/treatment-consultant/stories/TreatmentConsultant.stories.ts +0 -38
  27. package/app/pages/ui/services/treatment-consultant.vue +0 -39
@@ -15,7 +15,8 @@
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",
@@ -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
+ ```
@@ -0,0 +1,128 @@
1
+ # TreatmentConsultant Component
2
+
3
+ ## Overview
4
+
5
+ `TreatmentConsultant` is a self-contained 5-step hair consultation wizard (`03.organisms`). It collects a user's hair profile across four steps (hair type, natural colour, dream colour, style & treatments) and renders a personalised recommendation on the final step.
6
+
7
+ All data — hair types, colour swatches, treatments, and the recommendation matrix — is hard-coded inside the component. There are no external data dependencies.
8
+
9
+ ## Props
10
+
11
+ | Prop | Type | Default | Purpose |
12
+ |------|------|---------|---------|
13
+ | `autoAdvance` | `boolean` | `false` | Automatically advances to the next step immediately after each selection (steps 0–2). On step 3, a "View Results" button still appears when `allowMultipleTreatments` is also `true`. |
14
+ | `allowMultipleTreatments` | `boolean` | `false` | Enables multi-select on the treatments step. When `false`, selecting a treatment deselects any previous choice. |
15
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Standard passthrough prop for HOC styling. |
16
+
17
+ ## Basic usage
18
+
19
+ ```vue
20
+ <TreatmentConsultant :auto-advance="true" :allow-multiple-treatments="true" />
21
+ ```
22
+
23
+ ## Steps
24
+
25
+ | Index | Label | Behaviour |
26
+ |-------|-------|-----------|
27
+ | 0 | Hair Type | Single select — straight, wavy, curly, coily |
28
+ | 1 | Your Colour | Single select — 7 natural colour swatches |
29
+ | 2 | Dream Colour | Single select — 8 desired colours (incl. "none") |
30
+ | 3 | Style & Treatments | Single or multi-select (see `allowMultipleTreatments`) |
31
+ | 4 | Results | Read-only — recommendation, treatment cards, summary, CTA |
32
+
33
+ ## Navigation rules
34
+
35
+ - Completed steps (index < current) are clickable to go back.
36
+ - Future steps are disabled via `aria-disabled` + `tabindex="-1"`.
37
+ - Steps 0–2 require a selection before proceeding (`canProceed`).
38
+ - Step 3 is always passable — treatments are optional.
39
+ - `reset()` clears all state and returns to step 0.
40
+
41
+ ## Treatment selection logic
42
+
43
+ - Selecting `"none"` clears all other treatments (and vice versa).
44
+ - In multi-select mode, selecting a treatment automatically removes any conflicting ones (defined by each treatment's `excludes` array).
45
+ - Conflicting treatments are visually marked with a `lucide:ban` icon and a "Conflicts with X" label.
46
+
47
+ ## Treatment–Colour compatibility
48
+
49
+ Some treatments clash with same-day colour services: `keratin-smoothing`, `perm`, `relaxer`, `japanese-straightening`. These show a "Not same-day as colour" badge in the results view when a colour change was also selected.
50
+
51
+ ## Recommendation matrix
52
+
53
+ `getColourRecommendation(naturalColour, desiredColour, hairType)` maps the combination to:
54
+
55
+ ```ts
56
+ interface Recommendation {
57
+ suitability: "great" | "possible" | "difficult" | "not-recommended";
58
+ method: string; // e.g. "Semi-Permanent", "Bleach Required"
59
+ notes: string; // one-line summary
60
+ details: string[]; // bullet points
61
+ }
62
+ ```
63
+
64
+ Curly or coily hair type appends an extra conditioning note to `details`. Unmapped combinations fall back to `suitability: "possible"` with a consultation prompt.
65
+
66
+ ## Image assets
67
+
68
+ Swatch JPEGs live at `public/images/treatment-consultant/`:
69
+ - `swatch-{light-blonde,dark-blonde,light-brown,dark-brown,red,black,grey-white}.jpeg` (natural colours)
70
+ - `swatch-dream-{blonde,brown,red,black,grey-silver,vivid,balayage}.jpeg` (desired colours)
71
+
72
+ All `NuxtImg` usages include explicit `width` and `height` (128×128 in options, 96×96 in results summary).
73
+
74
+ ## Styling
75
+
76
+ Amber-toned dark theme. Tokens scoped to `.treatment-consultant`:
77
+
78
+ ```css
79
+ .treatment-consultant {
80
+ --_canvas-color: var(--amber-09);
81
+ --_canvas-text: var(--amber-02);
82
+ --_surface-active: var(--amber-10);
83
+ --_surface-checked: var(--green-09);
84
+ --_surface-excluded: color-mix(in srgb, var(--red-07) 10%, transparent);
85
+ /* … */
86
+ }
87
+ ```
88
+
89
+ Fonts: Inter (body), Playfair Display (accent).
90
+ Step transitions: `<Transition name="slide" mode="out-in">`.
91
+ Results cards use `v-motion` with staggered enter animations.
92
+
93
+ ## Local style override scaffold
94
+
95
+ ```vue
96
+ <TreatmentConsultant :style-class-passthrough="['my-consultant']" />
97
+
98
+ <style>
99
+ /* ─── TreatmentConsultant local overrides ──────────────────────────
100
+ Colours, borders, geometry only — do not override behaviour.
101
+ Delete this block if no overrides are needed.
102
+ ─────────────────────────────────────────────────────────────────── */
103
+ .treatment-consultant {
104
+ &.my-consultant {
105
+ /* Canvas background */
106
+ /* --_canvas-color: var(--brand-surface); */
107
+
108
+ /* Option selected state */
109
+ /* --_surface-checked: var(--brand-accent); */
110
+ /* --_border-checked: var(--brand-accent-border); */
111
+ }
112
+ }
113
+ </style>
114
+ ```
115
+
116
+ See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
117
+
118
+ ## CTA (results step)
119
+
120
+ - "Book Consultation" links to `/#contact` (hardcoded anchor).
121
+ - "Start Again" calls `reset()`.
122
+
123
+ ## Notes
124
+
125
+ - Component is auto-imported in Nuxt — no import needed.
126
+ - No slots — entirely self-contained UI and data.
127
+ - `styleClassPassthrough` is accepted but not currently wired to `useStyleClassPassthrough` inside the component.
128
+ - Storybook story at `Organisms/TreatmentConsultant` — controls for both props.
@@ -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.
@@ -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
- └── navigation-horizontal.md — NavigationHorizontal props, NavItemData type, CSS token API, import path gotcha
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
+ └── treatment-consultant.md — TreatmentConsultant 5-step wizard: props, step flow, treatment exclusion logic, recommendation matrix, image paths
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-10);
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
 
@@ -7,6 +7,6 @@
7
7
  --amber-06: oklch(0.501 0.166 75);
8
8
  --amber-07: oklch(0.433 0.144 75);
9
9
  --amber-08: oklch(0.383 0.123 75);
10
- --amber-09: oklch(0.338 0.1 75);
11
- --amber-10: oklch(0.288 0.072 75);
10
+ --amber-09: oklch(0.238 0.1 75);
11
+ --amber-10: oklch(0.188 0.072 75);
12
12
  }
@@ -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(--blue-03);
70
- --treatment-consultant-border-colour: var(--blue-03);
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);
@@ -30,6 +30,16 @@
30
30
  --glass-panel-shadow: 0 8px 32px rgba(0, 0, 0, 0.08), 0 2px 8px rgba(0, 0, 0, 0.04);
31
31
  --glass-panel-highlight: rgba(255, 255, 255, 0.9);
32
32
 
33
+ /* ===========================================
34
+ HERO TEXT VARIABLES
35
+ =========================================== */
36
+ --hero-text-bg-img: linear-gradient(135deg, #c2a770, #b4747e, #d1bd94);
37
+
38
+ /* ===========================================
39
+ EYEBROW TEXT VARIABLES
40
+ =========================================== */
41
+ --eyebrow-text-bg-img: linear-gradient(135deg, #c2a770, #b4747e, #d1bd94);
42
+
33
43
  /* ===========================================
34
44
  STEPPER LIST VARIABLES
35
45
  =========================================== */
@@ -64,7 +74,7 @@
64
74
  /* ===========================================
65
75
  TREATMENT CONSULTANT VARIABLES
66
76
  =========================================== */
67
- --treatment-consultant-primary-colour: var(--blue-03);
77
+ --treatment-consultant-primary-colour: var(--amber-05);
68
78
  --treatment-consultant-border-colour: var(--blue-02);
69
79
 
70
80
  --treatment-consultant-checked-surface-colour: var(--green-03);
@@ -26,7 +26,7 @@
26
26
  FORM INPUT VARIABLES
27
27
  Styling for form inputs and controls
28
28
  =========================================== */
29
- --theme-input-border: var(--red-05);
29
+ --theme-input-border: var(--red-06);
30
30
  --theme-input-border-focus: var(--red-04);
31
31
 
32
32
  /* Input outlines and focus */
@@ -22,21 +22,24 @@ const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough)
22
22
 
23
23
  <style lang="css">
24
24
  @layer components {
25
- .eyebrow-text {
26
- text-transform: uppercase;
27
- color: var(--colour-text-eyebrow);
25
+ .eyebrow-text {
26
+ text-transform: uppercase;
27
+ background-clip: text;
28
+ background-image: var(--eyebrow-text-bg-img);
29
+ font-style: italic;
30
+ color: transparent;
28
31
 
29
- &.large {
30
- font-size: var(--eyebrow-text-large);
31
- }
32
+ &.large {
33
+ font-size: var(--eyebrow-text-large);
34
+ }
32
35
 
33
- &.medium {
34
- font-size: var(--eyebrow-text-medium);
35
- }
36
+ &.medium {
37
+ font-size: var(--eyebrow-text-medium);
38
+ }
36
39
 
37
- &.small {
38
- font-size: var(--eyebrow-text-small);
40
+ &.small {
41
+ font-size: var(--eyebrow-text-small);
42
+ }
39
43
  }
40
44
  }
41
- }
42
45
  </style>