srcdev-nuxt-components 9.5.2 → 9.5.4
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/colour-scheme-disable.md +1 -1
- package/.claude/skills/component-compliance-checklist.md +214 -0
- package/.claude/skills/component-inline-action-button.md +2 -2
- package/.claude/skills/components/canvas-switcher.md +5 -1
- package/.claude/skills/components/carousel-flip.md +3 -3
- package/.claude/skills/components/clipped-panel.md +2 -2
- package/.claude/skills/components/dashboard-quad-grid.md +2 -2
- package/.claude/skills/components/dashboard-stats-grid.md +1 -1
- package/.claude/skills/components/deep-expanding-menu-classic.md +1 -1
- package/.claude/skills/components/deep-expanding-menu.md +1 -1
- package/.claude/skills/components/eyebrow-text.md +6 -4
- package/.claude/skills/components/google-reviews.md +193 -0
- package/.claude/skills/components/input-number.md +4 -0
- package/.claude/skills/components/input-otp.md +120 -0
- package/.claude/skills/components/section-parallax.md +1 -1
- package/.claude/skills/components/select-menu.md +9 -5
- package/.claude/skills/components/services-card.md +25 -2
- package/.claude/skills/composable-colour-scheme.md +2 -0
- package/.claude/skills/index.md +5 -1
- package/.claude/skills/new-app-scaffold.md +1 -1
- package/.claude/skills/storybook-add-story.md +5 -0
- package/.claude/skills/theming-colour-ramps.md +6 -33
- package/.claude/skills/theming-component-token-pattern.md +30 -0
- package/.claude/skills/theming-dark-mode.md +243 -0
- package/.claude/skills/theming-override-default.md +6 -7
- package/.claude/skills/theming-partial-override.md +11 -11
- package/.vscode/srcdev-component-eyebrow-text.code-snippets +4 -0
- package/.vscode/srcdev-component-google-reviews.code-snippets +78 -0
- package/.vscode/srcdev-component-input-otp.code-snippets +47 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +13 -0
- package/.vscode/srcdev-component-services-card.code-snippets +38 -7
- package/README.md +81 -41
- package/app/assets/styles/setup/01.config/_head.css +0 -9
- package/app/assets/styles/setup/03.theming/_default.css +29 -29
- package/app/assets/styles/setup/03.theming/_error.css +3 -3
- package/app/assets/styles/setup/03.theming/_theme-slots.css +13 -16
- package/app/assets/styles/setup/a11y/_variables.css +2 -2
- package/app/components/01.atoms/page-row/stories/PageRow.stories.ts +47 -13
- package/app/components/01.atoms/text-blocks/eyebrow-text/CONSUMER-STYLING.md +5 -1
- package/app/components/01.atoms/text-blocks/eyebrow-text/EyebrowText.vue +4 -1
- package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +7 -7
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +10 -0
- package/app/components/02.molecules/select-menu/SelectMenu.vue +15 -1
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +35 -0
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +9 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +3 -3
- package/app/components/03.organisms/google-reviews/CONSUMER-STYLING.md +110 -0
- package/app/components/03.organisms/google-reviews/GoogleReviewCard.vue +161 -0
- package/app/components/03.organisms/google-reviews/GoogleReviews.vue +275 -0
- package/app/components/03.organisms/google-reviews/stories/GoogleReviews.stories.ts +323 -0
- package/app/components/03.organisms/google-reviews/tests/GoogleReviewCard.spec.ts +104 -0
- package/app/components/03.organisms/google-reviews/tests/GoogleReviews.spec.ts +232 -0
- package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviewCard.spec.ts.snap +9 -0
- package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviews.spec.ts.snap +43 -0
- package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +28 -3
- package/app/components/03.organisms/services/services-card/ServicesCard.vue +111 -18
- package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +122 -0
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +4 -1
- package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +100 -19
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +12 -3
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +1 -1
- package/app/components/05.forms/input-number/InputNumber.vue +2 -2
- package/app/components/05.forms/input-otp/CONSUMER-STYLING.md +59 -0
- package/app/components/05.forms/input-otp/InputOtp.vue +254 -0
- package/app/components/05.forms/input-otp/InputOtpField.vue +106 -0
- package/app/components/05.forms/input-otp/stories/InputOtp.stories.ts +100 -0
- package/app/components/05.forms/input-otp/stories/InputOtpField.stories.ts +163 -0
- package/app/components/05.forms/input-otp/tests/InputOtp.spec.ts +275 -0
- package/app/components/05.forms/input-otp/tests/InputOtpField.spec.ts +76 -0
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +151 -33
- package/app/composables/tests/useAriaDescribedById.spec.ts +5 -5
- package/app/composables/useGoogleReviews.ts +4 -0
- package/app/types/components/google-reviews.d.ts +18 -0
- package/app/types/components/index.ts +1 -0
- package/nuxt.config.ts +9 -9
- package/package.json +9 -8
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
This layer includes light/dark/auto colour scheme
|
|
5
|
+
This layer includes light/dark/auto colour scheme switching (an attribute, not `light-dark()`, for older iPad Safari). The layer ships light colour values only; dark values are the app's to add (`theming-dark-mode.md`). By default it:
|
|
6
6
|
|
|
7
7
|
1. Injects a synchronous `<head>` script to apply the saved scheme before first paint (FOUC prevention)
|
|
8
8
|
2. Provides `useColourScheme()` composable to read/write the active scheme via localStorage and `document.documentElement.dataset.colorScheme`
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Component compliance checklist
|
|
2
|
+
|
|
3
|
+
The definition of done for every component in this library. It is shared by two commands:
|
|
4
|
+
|
|
5
|
+
- `/new-component` uses it as the build spec for a component that doesn't exist yet.
|
|
6
|
+
- `/migrate-component` uses it to bring an existing component up to standard.
|
|
7
|
+
|
|
8
|
+
Item numbers are stable and are referenced elsewhere ("checklist item 4", the ledger columns, memory
|
|
9
|
+
notes), so add new items at the end rather than renumbering.
|
|
10
|
+
|
|
11
|
+
Go through every item. Skip one only when it genuinely doesn't apply, and say why briefly; don't
|
|
12
|
+
skip silently. Notes marked **Existing component** only apply when migrating.
|
|
13
|
+
|
|
14
|
+
A component passes when its Component Ledger row (`node .claude/component-ledger/build.mjs`, then
|
|
15
|
+
`.claude/component-ledger/audit.json`) shows `score: 5`, a real tier, and `variants`, `legacy_props`,
|
|
16
|
+
`story_args_bug`, `eslint_issues`, `redundant_priv_tokens` and `styling_doc_outdated` all `false`.
|
|
17
|
+
The ledger can't see items 9, 10, 11 and 14, so check those by hand.
|
|
18
|
+
|
|
19
|
+
## The checklist
|
|
20
|
+
|
|
21
|
+
1. **Tier folder** — the component lives in one of `01.atoms`–`05.forms` (see
|
|
22
|
+
`project_component_tiers` in memory for what each tier means), in its own kebab-case folder:
|
|
23
|
+
`app/components/<tier>/<family>/`. If none fits cleanly, ask the user rather than guessing.
|
|
24
|
+
**Existing component:** if it lives outside a tier, move it with `git mv`.
|
|
25
|
+
2. **Props pattern** — `interface Props` + `withDefaults(defineProps<Props>(), {...})`, never
|
|
26
|
+
options-style `defineProps({...})`. Include `styleClassPassthrough` and wire it through
|
|
27
|
+
`useStyleClassPassthrough()`. Consumer-facing data-shape types go in
|
|
28
|
+
`app/types/components/<component-name>.d.ts` and are exported from `app/types/components/index.ts`
|
|
29
|
+
(see `component-export-types.md`), not left inline in the `.vue` file.
|
|
30
|
+
3. **CSS tokens** — audit every custom property in the component's `<style>` block against the
|
|
31
|
+
rule in CLAUDE.md's "Public token pattern" (consumer relevance, not reuse count):
|
|
32
|
+
- A value a consumer could plausibly want to override — including a per-state variant like a
|
|
33
|
+
hover-only width or a dark-theme colour — must be a **public** token (`--component-name-x`),
|
|
34
|
+
consumed directly at its point of use, even if used only once.
|
|
35
|
+
- A value with no plausible consumer relevance stays **private** (`--_x`), even if used only
|
|
36
|
+
once — don't strip the prefix just because reuse is low.
|
|
37
|
+
- Fix the actual bug class this catches: a consumer-relevant value that's currently private
|
|
38
|
+
with no public fallback. That's the thing to promote, not every `--_` var you find.
|
|
39
|
+
- Remove redundant private tokens (the other half of pitfall #20): a `--_` token declared but
|
|
40
|
+
never read, or one whose whole value is a single `var(--public-token, fallback)` and which is
|
|
41
|
+
read exactly once, is pure duplication. Delete the unread ones; inline the public token at
|
|
42
|
+
the single point of use for the rest. Keep a private token that's read in more than one
|
|
43
|
+
place, is composed (`calc()`, `v-bind()`, several `var()`s), or is re-declared by a
|
|
44
|
+
state/variant selector. `node .claude/component-ledger/fix-private-tokens.mjs <file.vue>`
|
|
45
|
+
lists them (dry run); `--write` applies the ones marked `fix`. A `REVIEW` item is declared
|
|
46
|
+
somewhere other than the root rule or read outside its subtree: check the element that
|
|
47
|
+
reads it sits inside the one that declares it before adding `--include-review`, or fix it by
|
|
48
|
+
hand. Afterwards, update any CONSUMER-STYLING.md or skill doc line that names the removed
|
|
49
|
+
tokens, and delete comments left describing them.
|
|
50
|
+
- Colours default from `--theme-*` tokens through the component's own public tokens, never by
|
|
51
|
+
reading a global `--theme-*` token bare at the point of use (pitfall #14). Pair a dark/bold
|
|
52
|
+
surface with `--theme-on-surface`, not `--theme-text` (pitfall #13). For a card or panel
|
|
53
|
+
background default to `--theme-surface-subtle` (the surface `--theme-text` sits on);
|
|
54
|
+
`--theme-surface` and `--theme-border` are bold steps meant for buttons and focus, and make a
|
|
55
|
+
card a solid block of theme colour.
|
|
56
|
+
- Style rules sit in `@layer components`, with child selectors nested inside the root block
|
|
57
|
+
using native nesting (`& .block__child`), never Sass `&__child` concatenation
|
|
58
|
+
(`css-nesting-conventions.md`).
|
|
59
|
+
4. **CONSUMER-STYLING.md** — create or update so every public token the component exposes is
|
|
60
|
+
documented, with its default. Skip only if the component genuinely has no override surface at
|
|
61
|
+
all (no CSS custom properties, no class passthrough). Use this fixed layout, in this order:
|
|
62
|
+
- `## Public token API` — token tables (split into `###` groups by element when there are
|
|
63
|
+
several), each token with its default; list any private `--_` tokens as "not public API", or
|
|
64
|
+
say there are none.
|
|
65
|
+
- `## State hooks` — `data-*` attributes / state selectors and the inner element class names.
|
|
66
|
+
Omit only if the component has neither.
|
|
67
|
+
- Component-specific reference sections (motion, sizing model, etc.) may follow here.
|
|
68
|
+
- `## Global theming` — optional. Include a `:where(html) { ... }` block only when there's
|
|
69
|
+
something worth showing (e.g. a common site-wide tweak); don't pad it out.
|
|
70
|
+
- `## Local overrides` — required heading (the ledger's `styling_doc_outdated` check looks for
|
|
71
|
+
it). The mechanics are the same for every component and live in
|
|
72
|
+
`.claude/skills/component-local-style-override.md`, so don't write bespoke examples by
|
|
73
|
+
default. Write one standard paragraph (copy it from any migrated doc, e.g.
|
|
74
|
+
`05.forms/form-field/CONSUMER-STYLING.md`): set tokens on an element you own (page/section
|
|
75
|
+
class, or a `:style-class-passthrough` class, or a plain `class` that falls through when there's
|
|
76
|
+
no passthrough prop); keep the block unlayered; from a **consumer's** `<style scoped>` file,
|
|
77
|
+
tokens on their own element still work but selectors reaching inside need `:deep()` (this
|
|
78
|
+
library's own styles are never scoped); link the guide. Then add a **Caveat** only where this
|
|
79
|
+
component breaks that model, most often a token it re-declares on its own elements (so an
|
|
80
|
+
ancestor value never lands, e.g. `DisplayThemeSwitch`'s sizing tokens, `ServiceSummary`'s
|
|
81
|
+
`--display-pill-*`, `InputError`'s `data-theme`), or a wrapper that doesn't forward
|
|
82
|
+
passthrough. Check with a grep for `^\s*--<name>-[\w-]+\s*:` in the component's `<style>`.
|
|
83
|
+
Existing examples can stay as `###` subsections (`Page or section`, `One instance`).
|
|
84
|
+
- `## Recipe: <name>` — optional, for component-specific patterns that need several tokens
|
|
85
|
+
working together (e.g. InputDescription's panel/callout).
|
|
86
|
+
- `## Class passthrough` — what `style-class-passthrough` targets and whether it's actually
|
|
87
|
+
reachable in normal use.
|
|
88
|
+
- `## Notes` — optional, last.
|
|
89
|
+
|
|
90
|
+
Dated "Changed YYYY-MM-DD" migration notes go as blockquotes inside the relevant section.
|
|
91
|
+
5. **Tests** — create or bring up to date in `tests/`, following the Testing Requirements section
|
|
92
|
+
of CLAUDE.md (mountSuspended, fake-timer rules, etc.). Skip only for a trivial presentational
|
|
93
|
+
component with no logic worth testing — say so explicitly if you skip. If anything is derived
|
|
94
|
+
from slots, test it by toggling a `v-if`'d slot in a host component after mount (pitfall #25).
|
|
95
|
+
6. **Storybook story** — create or update `stories/*.stories.ts` with controls for props/slots
|
|
96
|
+
that warrant them (see `.claude/skills/storybook-add-story.md`). The story title uses the tier
|
|
97
|
+
name directly. A component whose layout responds to its container gets the `CanvasSwitcher`
|
|
98
|
+
decorator (see `PageRow.stories.ts` or `GoogleReviews.stories.ts`, which also shows a per-story
|
|
99
|
+
starting canvas via `parameters.initialCanvas`) rather than a hand-sized wrapper or a width arg.
|
|
100
|
+
6a. **Storybook Controls-panel reactivity** — check every story `Template`/`render` function in
|
|
101
|
+
the component's `stories/*.stories.ts` files for this bug: `@storybook/vue3` mounts the story
|
|
102
|
+
component **once** and, on every Controls-panel change, mutates the same reactive `args`
|
|
103
|
+
object in place — it never re-runs `setup()`. A story that destructures or spreads `args` into
|
|
104
|
+
local variables/a ref at setup-time (the tell-tale shape: `const { modelValue, ...otherArgs }
|
|
105
|
+
= args;`, whether or not it's then fed into a `ref()`) takes a one-time snapshot, so most
|
|
106
|
+
Controls silently stop updating the rendered story after first render — often alongside a
|
|
107
|
+
prop-name typo in `argTypes` that compounds it (e.g. a control literally named differently
|
|
108
|
+
from the real component prop it's meant to drive — check that too). Fix by binding the
|
|
109
|
+
template directly to the live object (`v-model="args.modelValue"`, `v-bind="componentArgs"`
|
|
110
|
+
where `componentArgs` is a `computed()` that strips only genuinely non-prop extra args) rather
|
|
111
|
+
than copying values out in `setup()` — a destructure/spread from `args` is only safe when it's
|
|
112
|
+
inside a `computed()`. The ledger's `story_args_bug` column flags this automatically (any
|
|
113
|
+
`{...} = args;` spread not wrapped in a `computed()`); still eyeball each story file yourself,
|
|
114
|
+
since a differently-shaped variant of the same mistake may not match that exact heuristic.
|
|
115
|
+
6b. **Stress-test story** — add a `StressTest` story ("Stress Test (Worst-Case Data)") that
|
|
116
|
+
feeds the component the most hostile data a QA tester could think of, and fix whatever it
|
|
117
|
+
breaks. Go through every prop, slot and data field and ask what would break it, for example:
|
|
118
|
+
- very long text, and long **unbroken** strings (no spaces, a long URL) in every text field,
|
|
119
|
+
including labels and copy props (long translated copy, e.g. German)
|
|
120
|
+
- empty strings, missing optional fields, a single character, a single item, the maximum
|
|
121
|
+
number of items
|
|
122
|
+
- emoji (including at the start of anything that takes initials or first letters), HTML-like
|
|
123
|
+
text (must render as text), right-to-left text
|
|
124
|
+
- out-of-range or odd numbers (0, negative, above the maximum, fractional, very large)
|
|
125
|
+
- broken image or link URLs
|
|
126
|
+
Describe what to check in the story's docs description, and look at it at every
|
|
127
|
+
`CanvasSwitcher` width. Fixes usually mean `overflow-wrap: anywhere`, `min-inline-size: 0` on
|
|
128
|
+
flex/grid children, `Array.from(str)` instead of `str[0]`, and `clamp()` on values
|
|
129
|
+
derived from data. Give each text element a consumer might want to shorten (names, titles,
|
|
130
|
+
descriptions, body text) a line-clamp token, `--<component>-<element>-line-clamp`, which
|
|
131
|
+
covers ellipsis too (`1` is single-line ellipsis). Default it to `none` unless the layout
|
|
132
|
+
clearly needs a cap, and never clamp text that must stay readable (legal or attribution text,
|
|
133
|
+
link text that names its destination). Shape and rules: `theming-component-token-pattern.md`,
|
|
134
|
+
"Line-clamp tokens". Add a unit test for any logic fix. A component that takes no data or copy
|
|
135
|
+
(e.g. a pure layout wrapper) still gets one with oversized slot content.
|
|
136
|
+
7. **Skill doc** — create or update `.claude/skills/components/<component-name>.md`, following the
|
|
137
|
+
pattern of an existing one. A labelled `<Name>Field` wrapper (or any other wrapper) is documented
|
|
138
|
+
in its control's doc under a "Variants" section, not in a doc of its own
|
|
139
|
+
(`feedback_variants_deprecated_as_own_skill_docs`). **Existing component:** if you find a
|
|
140
|
+
`variants/` subfolder, flatten it into the parent folder (`git mv` the `.vue` files plus their
|
|
141
|
+
stories/tests up one level, fix relative imports); `pathPrefix: false` keeps the auto-import
|
|
142
|
+
names unchanged. All existing `variants/` folders were flattened 2026-09-25.
|
|
143
|
+
8. **VS Code snippet** — create or update `.vscode/srcdev-component-{family}.code-snippets`,
|
|
144
|
+
covering the control and its Field wrapper in one file.
|
|
145
|
+
9. **Accessibility** — check for gaps beyond what already-passing tests would catch:
|
|
146
|
+
- Interactive controls (buttons, toggles, custom form-like widgets) have an accessible name —
|
|
147
|
+
visible text, `aria-label`, or `aria-labelledby`.
|
|
148
|
+
- Keyboard behaviour matches what's actually implemented — no keydown case that only calls
|
|
149
|
+
`event.preventDefault()` with a comment like "could add this later", and no screen-reader
|
|
150
|
+
copy (visible or `sr-only`) that describes an interaction the component doesn't actually
|
|
151
|
+
perform. Cross-check every claim in the copy against the handler that's supposed to back it.
|
|
152
|
+
- Focus is visible (`:focus-visible`, not a suppressed outline) on anything focusable.
|
|
153
|
+
- Live-updating or auto-advancing content (carousels, marquees, auto-dismissing toasts) has a
|
|
154
|
+
way to pause it, per WCAG 2.2.2 — a visible control, or pause-on-hover/focus at minimum.
|
|
155
|
+
- `prefers-reduced-motion` is respected for any animation that isn't purely decorative.
|
|
156
|
+
- A component with a `tag` prop that can render a landmark uses `useAriaLabelledById`
|
|
157
|
+
(`component-aria-landmark.md`), unless it falls back to `aria-label` when no heading is
|
|
158
|
+
given: the composable can't do that and warns whenever a landmark has no heading. Then label
|
|
159
|
+
it directly with `useId()` (`aria-labelledby` when the heading slot is filled, `aria-label`
|
|
160
|
+
otherwise, both decided by a function called from the template, per pitfall #25), as
|
|
161
|
+
`GoogleReviews` does.
|
|
162
|
+
10. **Localisation (no hardcoded consumer-facing text)** — grep the template and script for
|
|
163
|
+
user-visible string literals (button copy, `aria-label`/`aria-description` text, placeholder
|
|
164
|
+
text, empty-state messages, etc.) that aren't already props. This library has no i18n
|
|
165
|
+
framework dependency (see `MarqueeScroller`'s `playLabel`/`pauseLabel`/`ariaLabel`/
|
|
166
|
+
`ariaDescription` for the pattern) — promote any hardcoded copy to a string prop with the
|
|
167
|
+
existing English text as its default, so a consumer can pass translated strings from their own
|
|
168
|
+
i18n solution. Icon-only controls should also get an icon-override prop or slot (see
|
|
169
|
+
`playIcon`/`pauseIcon`/`toggle-icon`) alongside the label prop, not just the label. Default
|
|
170
|
+
copy follows the em dash ban for visitor-facing text.
|
|
171
|
+
11. **No `light-dark()` CSS function in the component's own values** — grep the `<style>` block
|
|
172
|
+
(and any `v-bind()`-fed script constants) for `light-dark(`. Older iPads/Safari versions don't
|
|
173
|
+
support it, so this library's own default values must never rely on it — use the existing
|
|
174
|
+
`--theme-*` token convention (pitfall #14) where one already fits. Where none fits and you're
|
|
175
|
+
replacing a literal `light-dark(light-value, dark-value)` call directly, keep the **light**
|
|
176
|
+
value only — don't invent a dark-mode fallback scheme, and don't ask which of the two to keep.
|
|
177
|
+
The mechanism stays available for consumers: a consumer app is free to define its own public
|
|
178
|
+
token overrides using `light-dark()` inside its own CSS, since that's their own browser-support
|
|
179
|
+
decision to make, not this library's default. The global token layer (`03.theming`, `a11y/_variables.css`) is light-only too since 2026-10-04, so `--theme-*` tokens are safe to consume.
|
|
180
|
+
12. **Add the component to the Migrated Fields Form story** — once the component reaches 5/5,
|
|
181
|
+
add one field for it to
|
|
182
|
+
`app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts` (import the
|
|
183
|
+
component, add a field to the demo form, wire minimal state/error handling matching the
|
|
184
|
+
existing fields). This story is a living migration-progress tracker, not just a demo — its own
|
|
185
|
+
top-of-file comment lists which components are currently included and must be updated too
|
|
186
|
+
(add the new component to the list, bump the "As of `<date>`" note). Skip only if the
|
|
187
|
+
component's shape genuinely doesn't fit a form field (anything outside `05.forms`, or a
|
|
188
|
+
non-input `05.forms` helper component) — state why if you skip.
|
|
189
|
+
13. **Naming** — bare control `<Name>`, labelled wrapper `<Name>Field`, root DOM class following
|
|
190
|
+
the name (`.input-number`, `.input-number-field`), per `.claude/skills/component-naming.md`.
|
|
191
|
+
Multi-word, never a native or plausibly-future HTML element name, and not a bare common word
|
|
192
|
+
a consuming app is likely to define itself (the Nuxt-layer collision caveat).
|
|
193
|
+
**Existing component:** if it (or its wrappers) still uses a `Core`, `Default` or `WithLabel`
|
|
194
|
+
suffix, rename it. The doc's backlog table gives the proposed name and which consumer repos
|
|
195
|
+
use the old one. Rows marked **decide** (collision-prone generic names like `Card`/`Tabs`)
|
|
196
|
+
need an `AskUserQuestion` before renaming. Follow the doc's rename procedure (including
|
|
197
|
+
updating consumer repos) and tick the row off. Do this as the last checklist step, so the
|
|
198
|
+
earlier steps' edits don't have to track a mid-flight rename.
|
|
199
|
+
14. **Known pitfalls** — check the component against the CLAUDE.md pitfalls that recur in new
|
|
200
|
+
code (the ledger can't see any of these):
|
|
201
|
+
- Every structural class name is prefixed with the component name (`.display-dialog-header`,
|
|
202
|
+
not `.header`), since the component renders inside the consumer's DOM (pitfall #16).
|
|
203
|
+
- A `position: fixed`/`sticky` overlay exposes a public z-index token defaulting to `999999`
|
|
204
|
+
(pitfall #17).
|
|
205
|
+
- No utility class from `06.utility-classes` in the component's own markup unless it's used
|
|
206
|
+
for what that class actually means (pitfall #18, `mi-12`).
|
|
207
|
+
- Nothing load-bearing relies on a CSS feature WebKit lacks (pitfall #19).
|
|
208
|
+
- No `computed()` over `useSlots()`; use a function called from the template (pitfall #25).
|
|
209
|
+
- Values derived from props are `computed()`, not plain consts frozen at mount.
|
|
210
|
+
- Explicit closing tags (no self-closing non-void elements), hyphenated props in templates, no
|
|
211
|
+
manual Vue imports in components.
|
|
212
|
+
- `NuxtImg` gets explicit `width` and `height`; a `<video>` uses a `<source :src>` child with
|
|
213
|
+
`:key` and `.load()` (pitfall #12).
|
|
214
|
+
- No `#components` import (breaks Storybook); use `resolveComponent()`.
|
|
@@ -62,8 +62,8 @@ Target `.my-action-button.input-button` inside the wrapper to override InputButt
|
|
|
62
62
|
|
|
63
63
|
/* Use private tokens only for new semantic states with no theme equivalent */
|
|
64
64
|
&.some-state {
|
|
65
|
-
--_state-surface:
|
|
66
|
-
--_state-text:
|
|
65
|
+
--_state-surface: var(--green-01);
|
|
66
|
+
--_state-text: var(--green-08);
|
|
67
67
|
|
|
68
68
|
.my-action-button.input-button {
|
|
69
69
|
background-color: var(--_state-surface);
|
|
@@ -47,7 +47,11 @@ const canvasName = ref<MediaCanvas>("desktopCanvas");
|
|
|
47
47
|
The selected value doubles as a utility class name: `CanvasSwitcher`'s stylesheet also declares
|
|
48
48
|
`.mobileCanvas`/`.tabletCanvas`/`.laptopCanvas`/`.desktopCanvas`/`.fullWidthCanvas` (max-width
|
|
49
49
|
utilities, not scoped to `.canvas-switcher`), so binding `:class="canvasName"` directly onto a
|
|
50
|
-
preview wrapper constrains it to match
|
|
50
|
+
preview wrapper constrains it to match. `DashboardQuadGrid`/`DashboardStatsGrid` stories use the
|
|
51
|
+
classes alone (driven by a Controls select); `PageRow.stories.ts` renders the switcher itself in a
|
|
52
|
+
meta-level decorator, so every story gets an in-canvas width toggle. Copy that decorator for any
|
|
53
|
+
component that sizes itself from its container (`%`, container queries) rather than from
|
|
54
|
+
`@media`: a constrained wrapper cannot trigger a viewport media query.
|
|
51
55
|
These classes only ship in the bundle once `CanvasSwitcher` (or another importer of them) is
|
|
52
56
|
actually imported somewhere, since Nuxt only bundles a component's SFC styles when it's used.
|
|
53
57
|
|
|
@@ -90,10 +90,10 @@ See [CONSUMER-STYLING.md](../../app/components/03.organisms/image-galleries/caro
|
|
|
90
90
|
| `--carousel-flip-item-max-width` | `80rem` | Max width of a single item, before subtracting the edge-preview width |
|
|
91
91
|
| `--carousel-flip-edge-preview-width` | `4rem` | How much of the neighbouring items peek in at each edge |
|
|
92
92
|
| `--carousel-flip-marker-gap` | `1rem` | Gap between marker (dot) buttons |
|
|
93
|
-
| `--carousel-flip-marker-active-colour` | `
|
|
94
|
-
| `--carousel-flip-button-background-colour` | `
|
|
93
|
+
| `--carousel-flip-marker-active-colour` | `var(--slate-10)` | Active marker background colour |
|
|
94
|
+
| `--carousel-flip-button-background-colour` | `white` | Prev/next button background colour |
|
|
95
95
|
| `--carousel-flip-button-border-width` | `0.1rem` | Prev/next button border width |
|
|
96
|
-
| `--carousel-flip-button-border-colour` | `
|
|
96
|
+
| `--carousel-flip-button-border-colour` | `hsl(0, 29%, 3%)` | Prev/next button border colour |
|
|
97
97
|
| `--carousel-flip-button-border-radius` | `100vw` | Prev/next button corner radius |
|
|
98
98
|
| `--carousel-flip-button-padding` | `0.8rem` | Prev/next button padding |
|
|
99
99
|
| `--carousel-flip-button-icon-size` | `2.4rem` | Prev/next button icon glyph size |
|
|
@@ -46,8 +46,8 @@ see CONSUMER-STYLING.md for why these can't be resized via CSS.
|
|
|
46
46
|
|
|
47
47
|
| Property | Default | Controls |
|
|
48
48
|
|----------|---------|----------|
|
|
49
|
-
| `--clipped-panel-background-colour` | `
|
|
50
|
-
| `--clipped-panel-outline-colour` | `
|
|
49
|
+
| `--clipped-panel-background-colour` | `hsl(0, 0%, 96%)` | Panel background |
|
|
50
|
+
| `--clipped-panel-outline-colour` | `hsl(0, 29%, 3%)` | Outline colour and text/icon colour |
|
|
51
51
|
| `--clipped-panel-outline-width` | `1px` | Outline width |
|
|
52
52
|
|
|
53
53
|
## Notes
|
|
@@ -59,10 +59,10 @@ See [CONSUMER-STYLING.md](../../app/components/01.atoms/content-wrappers/layout-
|
|
|
59
59
|
| `--dashboard-quad-grid-gap` | `2rem` | Gap between the four slot panels |
|
|
60
60
|
| `--dashboard-quad-grid-padding` | `2rem` | Padding inside each slot panel |
|
|
61
61
|
| `--dashboard-quad-grid-outline-width` | `0.1rem` | Slot panel outline width |
|
|
62
|
-
| `--dashboard-quad-grid-outline-colour` | `
|
|
62
|
+
| `--dashboard-quad-grid-outline-colour` | `black` | Slot panel outline colour |
|
|
63
63
|
| `--dashboard-quad-grid-outline-offset` | `0` | Slot panel outline offset |
|
|
64
64
|
| `--dashboard-quad-grid-border-width` | `0` | Slot panel border width (off by default — the panel edge is drawn by the outline) |
|
|
65
|
-
| `--dashboard-quad-grid-border-colour` | `
|
|
65
|
+
| `--dashboard-quad-grid-border-colour` | `black` | Slot panel border colour, only visible once `--dashboard-quad-grid-border-width` is set |
|
|
66
66
|
| `--dashboard-quad-grid-border-radius` | `0.5rem` | Slot panel corner radius |
|
|
67
67
|
| `--dashboard-quad-grid-rule-width` | `0.1rem` | Width of the grid gap rule line (CSS Gap Decorations, limited browser support) |
|
|
68
68
|
| `--dashboard-quad-grid-rule-colour` | `transparent` | Colour of the grid gap rule line, invisible by default |
|
|
@@ -67,7 +67,7 @@ See [CONSUMER-STYLING.md](../../app/components/01.atoms/content-wrappers/layout-
|
|
|
67
67
|
| `--dashboard-stats-grid-gap` | `1.2rem` | Gap between panels, in every row |
|
|
68
68
|
| `--dashboard-stats-grid-panel-padding` | `1.2rem` | Padding inside each panel |
|
|
69
69
|
| `--dashboard-stats-grid-border-width` | `0.1rem` | Panel border width |
|
|
70
|
-
| `--dashboard-stats-grid-border-colour` | `
|
|
70
|
+
| `--dashboard-stats-grid-border-colour` | `hsl(0, 29%, 3%)` | Panel border colour |
|
|
71
71
|
| `--dashboard-stats-grid-border-radius` | `1.2rem` | Panel corner radius |
|
|
72
72
|
| `--dashboard-stats-grid-rule-width` | `0.1rem` | Width of the grid gap rule line (CSS Gap Decorations, limited browser support) |
|
|
73
73
|
| `--dashboard-stats-grid-rule-colour` | `transparent` | Colour of the grid gap rule line, invisible by default |
|
|
@@ -59,7 +59,7 @@ const navLinks: ResponsiveHeaderNavItem[] = [
|
|
|
59
59
|
| `--deep-expanding-menu-classic-item-gap` | `2.4rem` | Gap between top-level items |
|
|
60
60
|
| `--deep-expanding-menu-classic-link-border-width` | `0.2rem` | Bottom border width on links/toggles |
|
|
61
61
|
| `--deep-expanding-menu-classic-link-padding-block` | `0.8rem` | Vertical padding on links/toggles |
|
|
62
|
-
| `--deep-expanding-menu-classic-link-border-colour-hover` | `
|
|
62
|
+
| `--deep-expanding-menu-classic-link-border-colour-hover` | `var(--blue-10)` | Bottom border colour on hover/focus |
|
|
63
63
|
| `--deep-expanding-menu-classic-icon-size` | `1.2rem` | Caret icon size |
|
|
64
64
|
| `--deep-expanding-menu-classic-panel-offset-top` | `4rem` | Panel offset from the toggle |
|
|
65
65
|
| `--deep-expanding-menu-classic-panel-width` | `20rem` | Panel width below `768px` |
|
|
@@ -61,7 +61,7 @@ const navLinks: ResponsiveHeaderNavItem[] = [
|
|
|
61
61
|
| `--deep-expanding-menu-gap` | `2.4rem` | Gap between top-level items |
|
|
62
62
|
| `--deep-expanding-menu-link-border-width` | `0.2rem` | Bottom border width on links/toggles |
|
|
63
63
|
| `--deep-expanding-menu-link-padding-block` | `0.8rem` | Vertical padding on links/toggles |
|
|
64
|
-
| `--deep-expanding-menu-link-border-colour-hover` | `
|
|
64
|
+
| `--deep-expanding-menu-link-border-colour-hover` | `var(--blue-10)` | Bottom border colour on hover/focus |
|
|
65
65
|
| `--deep-expanding-menu-icon-size` | `1.2rem` | Caret icon size |
|
|
66
66
|
| `--deep-expanding-menu-panel-width` | `min(100%, 50vw)` | Popover panel width |
|
|
67
67
|
| `--deep-expanding-menu-panel-background-colour` | `white` | Popover panel background |
|
|
@@ -71,9 +71,10 @@ reference: `app/components/01.atoms/text-blocks/eyebrow-text/CONSUMER-STYLING.md
|
|
|
71
71
|
|
|
72
72
|
## Styling
|
|
73
73
|
|
|
74
|
-
Key CSS custom properties:
|
|
74
|
+
Key CSS custom properties (full list in `CONSUMER-STYLING.md`):
|
|
75
75
|
|
|
76
|
-
- `--
|
|
76
|
+
- `--eyebrow-text-bg-img` — text fill (gradient via `background-clip: text`; use a single-colour gradient for a solid colour)
|
|
77
|
+
- `--eyebrow-text-font-family`, `--eyebrow-text-font-style` (default `italic`), `--eyebrow-text-font-weight`, `--eyebrow-text-letter-spacing` — typography
|
|
77
78
|
|
|
78
79
|
Text is always `text-transform: uppercase` — do not pass pre-uppercased strings, as this makes content harder to edit and search.
|
|
79
80
|
|
|
@@ -95,8 +96,9 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
|
|
|
95
96
|
─────────────────────────────────────────────────────────────────── */
|
|
96
97
|
.eyebrow-text {
|
|
97
98
|
&.my-eyebrow {
|
|
98
|
-
/*
|
|
99
|
-
/* --
|
|
99
|
+
/* --eyebrow-text-bg-img: linear-gradient(var(--brand-accent), var(--brand-accent)); */
|
|
100
|
+
/* --eyebrow-text-font-style: normal; */
|
|
101
|
+
/* --eyebrow-text-letter-spacing: 0.2em; */
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
</style>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: GoogleReviews
|
|
3
|
+
description: GoogleReviews + GoogleReviewCard — Google Places reviews in a single scrolling row; server route, env vars, useGoogleReviews composable, props, slots, tokens, Google terms
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# GoogleReviews
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`GoogleReviews` (`03.organisms/google-reviews`) shows a place's Google reviews as a single
|
|
12
|
+
horizontally scrolling row of cards, with an overall rating summary, prev/next buttons and the
|
|
13
|
+
Google Maps attribution. It retrieves nothing itself: the layer's server route fetches the data
|
|
14
|
+
with a server-only API key, `useGoogleReviews()` reads that route, and the component displays
|
|
15
|
+
what it's given.
|
|
16
|
+
|
|
17
|
+
Google returns **at most 5 reviews** per place. Showing more needs the Business Profile API with
|
|
18
|
+
the owner signed in, which this doesn't support.
|
|
19
|
+
|
|
20
|
+
## Setup (consumer app)
|
|
21
|
+
|
|
22
|
+
1. In Google Cloud, enable **Places API (New)**, create an API key and restrict it to that API.
|
|
23
|
+
The `reviews` field is billed at the Place Details Enterprise + Atmosphere rate; the route
|
|
24
|
+
caches responses to limit calls.
|
|
25
|
+
2. Set env vars (server-only, never sent to the browser):
|
|
26
|
+
|
|
27
|
+
| Env var | Purpose |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `NUXT_GOOGLE_REVIEWS_API_KEY` | Places API key (required) |
|
|
30
|
+
| `NUXT_GOOGLE_REVIEWS_PLACE_ID` | The business's place ID (required; find it with Google's Place ID Finder) |
|
|
31
|
+
| `NUXT_GOOGLE_REVIEWS_LANGUAGE_CODE` | Optional, e.g. `en-GB` |
|
|
32
|
+
| `NUXT_GOOGLE_REVIEWS_CACHE_MAX_AGE` | Seconds to cache the response, default `3600` |
|
|
33
|
+
|
|
34
|
+
3. Use it:
|
|
35
|
+
|
|
36
|
+
```vue
|
|
37
|
+
<script setup lang="ts">
|
|
38
|
+
const { data } = await useGoogleReviews();
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<GoogleReviews :data="data" />
|
|
43
|
+
</template>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The place ID is read only from server config, never from the request, so the route can't be used
|
|
47
|
+
to look up other places on your key. The layer adds `lh3.googleusercontent.com` (reviewer photos)
|
|
48
|
+
to `image.domains`.
|
|
49
|
+
|
|
50
|
+
## Server route and composable
|
|
51
|
+
|
|
52
|
+
- `GET /api/google-reviews` (`server/api/google-reviews.get.ts`) returns `GoogleReviewsData`,
|
|
53
|
+
cached with Nitro's `defineCachedEventHandler` for `cacheMaxAge` seconds (stale-while-revalidate).
|
|
54
|
+
- Missing key or place ID: `500` whose message names the missing env var. Google request fails:
|
|
55
|
+
`502`.
|
|
56
|
+
- `useGoogleReviews()` is `useFetch<GoogleReviewsData>("/api/google-reviews")`, so it returns
|
|
57
|
+
`{ data, status, error, refresh }`. Use `status`/`error` for your own fallback; the component
|
|
58
|
+
renders nothing without data.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import type { GoogleReviewsData, GoogleReview } from "srcdev-nuxt-components";
|
|
62
|
+
|
|
63
|
+
interface GoogleReview {
|
|
64
|
+
authorName: string;
|
|
65
|
+
authorUri?: string;
|
|
66
|
+
authorPhotoUri?: string;
|
|
67
|
+
rating: number;
|
|
68
|
+
text: string;
|
|
69
|
+
relativeTime: string; // "2 weeks ago", in the configured language
|
|
70
|
+
publishTime: string; // ISO timestamp
|
|
71
|
+
reviewUri?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface GoogleReviewsData {
|
|
75
|
+
placeName: string;
|
|
76
|
+
rating: number;
|
|
77
|
+
totalReviews: number;
|
|
78
|
+
mapsUri: string;
|
|
79
|
+
reviews: GoogleReview[];
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Props
|
|
84
|
+
|
|
85
|
+
| Prop | Type | Default | Notes |
|
|
86
|
+
|---|---|---|---|
|
|
87
|
+
| `data` | `GoogleReviewsData \| null` | required | Usually `data` from `useGoogleReviews()` |
|
|
88
|
+
| `minRating` | `number` (0–5) | `0` | Hides lower-rated reviews and shows `filterNotice` |
|
|
89
|
+
| `showSummary` | `boolean` | `true` | Overall rating, stars and total count linked to the Maps listing |
|
|
90
|
+
| `tag` | `"section" \| "div"` | `"section"` | Root element |
|
|
91
|
+
| `ariaLabel` | `string` | `"Google reviews"` | Root label when there's no `heading` slot |
|
|
92
|
+
| `prevLabel` / `nextLabel` | `string` | `"Previous reviews"` / `"Next reviews"` | Button accessible names |
|
|
93
|
+
| `prevIcon` / `nextIcon` | `string` | `ic:outline-keyboard-arrow-left/right` | Button icons |
|
|
94
|
+
| `ratingLabel` | `string` | `"Rated {rating} out of 5"` | Screen-reader rating text |
|
|
95
|
+
| `totalLabel` | `string` | `"{count} reviews on Google"` | Summary link text |
|
|
96
|
+
| `readMoreLabel` | `string` | `"Read on Google"` | Per-card link text |
|
|
97
|
+
| `filterNotice` | `string` | `"Showing reviews rated {rating} stars and above, in Google's order."` | Shown when `minRating > 0` |
|
|
98
|
+
| `attributionText` | `string` | `"Reviews from Google Maps"` | Keep "Google Maps" when translating |
|
|
99
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Classes on the root |
|
|
100
|
+
|
|
101
|
+
All copy props are there for localisation; pass translated strings. `{rating}` and `{count}` are
|
|
102
|
+
replaced.
|
|
103
|
+
|
|
104
|
+
## Slots
|
|
105
|
+
|
|
106
|
+
| Slot | Props | Notes |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `heading` | `{ headingId }` | Heading above the summary. Bind `headingId` to it: the root is then labelled by the heading instead of `ariaLabel` |
|
|
109
|
+
| `card` | `{ review }` | Replaces each card. The list, scrolling and attribution stay. Keep the author attribution (name, photo, profile link) if you replace it |
|
|
110
|
+
|
|
111
|
+
## Behaviour
|
|
112
|
+
|
|
113
|
+
- Renders nothing when `data` is null or no reviews are left after filtering.
|
|
114
|
+
- The summary shows the overall rating (one decimal), stars, and the total count linked to the
|
|
115
|
+
Google Maps listing.
|
|
116
|
+
- Each card shows the reviewer's avatar (photo, or initials), name linked to their Google profile,
|
|
117
|
+
star rating, relative date, and the review text clamped to 5 lines, with a "Read on Google" link
|
|
118
|
+
to the full review.
|
|
119
|
+
- The Google Maps attribution is always shown.
|
|
120
|
+
- Cards sit in a single row that never wraps and scrolls horizontally with snapping, by touch,
|
|
121
|
+
trackpad or keyboard (the row is focusable; arrow keys scroll it, and tabbing to a link scrolls
|
|
122
|
+
its card into view).
|
|
123
|
+
- Prev/next scroll by one card, are disabled (`aria-disabled`, still focusable) at either end, and
|
|
124
|
+
are hidden when every card fits. Direction follows `dir="rtl"`.
|
|
125
|
+
- Card sections line up across the row (name, date, stars, text and link each share a row, via
|
|
126
|
+
subgrid), even when one reviewer's name or date wraps further than the others.
|
|
127
|
+
- Smooth scrolling is turned off under `prefers-reduced-motion`.
|
|
128
|
+
- The root is a labelled region; reviews are a `ul`/`li` list. Stars are hidden from screen
|
|
129
|
+
readers and replaced by `ratingLabel` text; the avatar is hidden too, since the name is read out.
|
|
130
|
+
Each "Read on Google" link is described by its author's name.
|
|
131
|
+
- `minRating` hides lower-rated reviews and shows the filter notice; no notice without a filter.
|
|
132
|
+
- Survives hostile data (see the `StressTest` story): long and unbroken text wraps inside its card,
|
|
133
|
+
reviewer names are cut to 2 lines (`--google-reviews-author-line-clamp`),
|
|
134
|
+
initials take whole characters (emoji-safe), star fill is clamped to 0–5, review text renders as
|
|
135
|
+
plain text. A broken photo URL still shows the browser's broken-image icon, since
|
|
136
|
+
`DisplayAvatar` has no image-error fallback.
|
|
137
|
+
|
|
138
|
+
## Variants
|
|
139
|
+
|
|
140
|
+
### GoogleReviewCard
|
|
141
|
+
|
|
142
|
+
One review, used by `GoogleReviews` and usable on its own (e.g. in a custom layout via the `card`
|
|
143
|
+
slot).
|
|
144
|
+
|
|
145
|
+
| Prop | Type | Default |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| `review` | `GoogleReview` | required |
|
|
148
|
+
| `tag` | `"article" \| "div"` | `"article"` |
|
|
149
|
+
| `ratingLabel` | `string` | `"Rated {rating} out of 5"` |
|
|
150
|
+
| `readMoreLabel` | `string` | `"Read on Google"` |
|
|
151
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` |
|
|
152
|
+
|
|
153
|
+
```vue
|
|
154
|
+
<GoogleReviews :data="data">
|
|
155
|
+
<template #card="{ review }">
|
|
156
|
+
<GoogleReviewCard :review="review" tag="div" :style-class-passthrough="['my-review']" />
|
|
157
|
+
</template>
|
|
158
|
+
</GoogleReviews>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## CSS custom properties
|
|
162
|
+
|
|
163
|
+
All `--google-reviews-*`, shared by both components. Full list with defaults in
|
|
164
|
+
`app/components/03.organisms/google-reviews/CONSUMER-STYLING.md`. Most used:
|
|
165
|
+
|
|
166
|
+
```css
|
|
167
|
+
.reviews-section {
|
|
168
|
+
--google-reviews-card-width: min(28rem, 80%);
|
|
169
|
+
--google-reviews-card-surface: var(--slate-01);
|
|
170
|
+
--google-reviews-card-border-radius: 1.6rem;
|
|
171
|
+
--google-reviews-star-colour: var(--amber-06);
|
|
172
|
+
--google-reviews-text-line-clamp: 4;
|
|
173
|
+
--google-reviews-author-line-clamp: 1;
|
|
174
|
+
--google-reviews-scroll-padding: 1.6rem;
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Google's terms
|
|
179
|
+
|
|
180
|
+
- Credit each review's author: the name, photo and profile link are rendered by default; keep them
|
|
181
|
+
if you use the `card` slot.
|
|
182
|
+
- Keep the Google Maps attribution visible and readable.
|
|
183
|
+
- Any filtering needs a visible notice describing it (`filterNotice`, shown automatically with
|
|
184
|
+
`minRating`).
|
|
185
|
+
- Don't store review content long-term. The route's cache is short-lived; check Google's current
|
|
186
|
+
Places API policies before raising `NUXT_GOOGLE_REVIEWS_CACHE_MAX_AGE` a lot.
|
|
187
|
+
|
|
188
|
+
## Notes
|
|
189
|
+
|
|
190
|
+
- Out of scope: more than 5 reviews, multiple places per site, sorting other than Google's
|
|
191
|
+
relevance order, auto-advance, review structured data (Google doesn't give review rich results
|
|
192
|
+
for a business's own reviews).
|
|
193
|
+
- Storybook can't reach the server route; stories use mock `GoogleReviewsData`.
|
|
@@ -156,6 +156,10 @@ to mirror `InputTextCore` directly:
|
|
|
156
156
|
`var(--input-number-surface-hover, var(--theme-input-surface-hover))`) rather than kept as
|
|
157
157
|
private wrappers. `--_border-focus` stayed private since it's now genuinely reused (the
|
|
158
158
|
wrapper's own focus outline *and* the embedded button's focus outline both read it).
|
|
159
|
+
- Fixed 2026-10-04: the wrapper's hover and focus outlines only matched `input:hover` /
|
|
160
|
+
`input:focus-visible`, so hovering or tabbing to a step button showed no outer ring, unlike
|
|
161
|
+
`InputTextCore` (and so `InputTextAsNumberWithLabel`), which matches `button` too. Both
|
|
162
|
+
selectors are now `:has(input:…, button:…)`, mirroring `InputTextCore`.
|
|
159
163
|
|
|
160
164
|
**Separately found in the same investigation** (not an `InputNumber` bug, but what the visual
|
|
161
165
|
comparison was originally checking against): `InputTextAsNumberWithLabel.vue` had its own,
|