srcdev-nuxt-components 9.1.41 → 9.1.43
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 +3 -1
- package/.claude/skills/components/{data-grid.md → auto-grid.md} +36 -34
- package/.claude/skills/components/samaritan-prompt-mixed.md +139 -0
- package/.claude/skills/css-animation-utilities.md +87 -0
- package/.claude/skills/index.md +4 -2
- package/app/assets/styles/setup/06.utility-classes/animations/_animation-scroller-x.css +21 -0
- package/app/assets/styles/setup/06.utility-classes/animations/_auto-rotate.css +5 -3
- package/app/assets/styles/setup/06.utility-classes/animations/_entry-exit-blur.css +5 -3
- package/app/assets/styles/setup/06.utility-classes/animations/_entry-zoom-reveal.css +5 -3
- package/app/assets/styles/setup/06.utility-classes/animations/index.css +5 -4
- package/app/components/01.atoms/grids/data-grid/AutoGrid.vue +57 -0
- package/app/components/01.atoms/grids/data-grid/stories/{DataGrid.stories.ts → AutoGrid.stories.ts} +95 -39
- package/app/components/01.atoms/grids/data-grid/tests/{DataGrid.spec.ts → AutoGrid.spec.ts} +42 -22
- package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/AutoGrid.spec.ts.snap +11 -0
- package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/DataGrid.spec.ts.snap +3 -3
- package/app/components/02.molecules/samaritan-prompt/SamaritanPromptMixed.vue +31 -1
- package/app/pages/auto-grid.vue +311 -0
- package/app/pages/index.vue +5 -0
- package/package.json +1 -1
- package/app/components/01.atoms/grids/data-grid/DataGrid.vue +0 -39
package/.claude/settings.json
CHANGED
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
"Bash(node -e ':*)",
|
|
27
27
|
"Bash(git ls-tree *)",
|
|
28
28
|
"Bash(node -p \"require\\('./package.json'\\).version\")",
|
|
29
|
-
"Bash(git status *)"
|
|
29
|
+
"Bash(git status *)",
|
|
30
|
+
"Bash(cp /Users/simoncornforth/websites/nuxt-components/.claude/skills/components/data-grid.md /Users/simoncornforth/websites/nuxt-components/.claude/skills/components/auto-grid.md)",
|
|
31
|
+
"Bash(rm /Users/simoncornforth/websites/nuxt-components/.claude/skills/components/data-grid.md)"
|
|
30
32
|
],
|
|
31
33
|
"additionalDirectories": []
|
|
32
34
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AutoGrid Component
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
`
|
|
5
|
+
`AutoGrid` is a responsive auto-fit CSS grid wrapper. It renders whatever named slots the consumer provides, auto-fitting columns to a minimum of `250px` each. Column count and gap are controlled via CSS custom properties, making layout adjustments a single-line style override rather than a prop change.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -11,21 +11,21 @@
|
|
|
11
11
|
Pass any number of named slots — the component renders each one in document order inside the grid.
|
|
12
12
|
|
|
13
13
|
```vue
|
|
14
|
-
<
|
|
14
|
+
<AutoGrid>
|
|
15
15
|
<template #item-1><StatCard label="Revenue" value="£24,500" /></template>
|
|
16
16
|
<template #item-2><StatCard label="Clients" value="142" /></template>
|
|
17
17
|
<template #item-3><StatCard label="Bookings" value="38" /></template>
|
|
18
|
-
</
|
|
18
|
+
</AutoGrid>
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
When filling from a data array, use a dynamic slot name in a `v-for`:
|
|
22
22
|
|
|
23
23
|
```vue
|
|
24
|
-
<
|
|
24
|
+
<AutoGrid>
|
|
25
25
|
<template v-for="(item, i) in stats" #[`item-${i}`] :key="i">
|
|
26
26
|
<StatCard :label="item.label" :value="item.value" />
|
|
27
27
|
</template>
|
|
28
|
-
</
|
|
28
|
+
</AutoGrid>
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
---
|
|
@@ -34,10 +34,10 @@ When filling from a data array, use a dynamic slot name in a `v-for`:
|
|
|
34
34
|
|
|
35
35
|
> **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:style-class-passthrough`.
|
|
36
36
|
|
|
37
|
-
| Prop (template form)
|
|
38
|
-
|
|
39
|
-
| `tag`
|
|
40
|
-
| `:style-class-passthrough` | `string \| string[]`
|
|
37
|
+
| Prop (template form) | Type | Default | Notes |
|
|
38
|
+
| -------------------------- | ------------------------------------------- | ------- | --------------------------------------------- |
|
|
39
|
+
| `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | Use a semantic tag for page landmark regions. |
|
|
40
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes on the root element. |
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
@@ -45,25 +45,27 @@ When filling from a data array, use a dynamic slot name in a `v-for`:
|
|
|
45
45
|
|
|
46
46
|
Override these via `style` attribute or a `styleClassPassthrough` class in a consuming `<style>` block.
|
|
47
47
|
|
|
48
|
-
| Property
|
|
49
|
-
|
|
50
|
-
| `--
|
|
51
|
-
| `--
|
|
48
|
+
| Property | Default | Notes |
|
|
49
|
+
| ------------------------- | -------- | ---------------------------------------------------------------------------------------- |
|
|
50
|
+
| `--auto-grid-min-col-size` | `250px` | Minimum column width; browser auto-fits as many columns as will fit. |
|
|
51
|
+
| `--auto-grid-gap` | `1rem` | Grid gap between items. |
|
|
52
52
|
|
|
53
53
|
### Fixed column count
|
|
54
54
|
|
|
55
|
+
Override `grid-template-columns` directly — there is no single token for this:
|
|
56
|
+
|
|
55
57
|
```vue
|
|
56
|
-
<
|
|
58
|
+
<AutoGrid style="grid-template-columns: repeat(3, 1fr); --auto-grid-gap: 2.4rem;">
|
|
57
59
|
...
|
|
58
|
-
</
|
|
60
|
+
</AutoGrid>
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
### Narrower minimum item width
|
|
62
64
|
|
|
63
65
|
```vue
|
|
64
|
-
<
|
|
66
|
+
<AutoGrid style="--auto-grid-min-col-size: 180px;">
|
|
65
67
|
...
|
|
66
|
-
</
|
|
68
|
+
</AutoGrid>
|
|
67
69
|
```
|
|
68
70
|
|
|
69
71
|
---
|
|
@@ -73,7 +75,7 @@ Override these via `style` attribute or a `styleClassPassthrough` class in a con
|
|
|
73
75
|
### Stat cards (default auto-fit)
|
|
74
76
|
|
|
75
77
|
```vue
|
|
76
|
-
<
|
|
78
|
+
<AutoGrid>
|
|
77
79
|
<template #revenue>
|
|
78
80
|
<div class="stat-card">
|
|
79
81
|
<span class="stat-card-label">Revenue</span>
|
|
@@ -86,17 +88,17 @@ Override these via `style` attribute or a `styleClassPassthrough` class in a con
|
|
|
86
88
|
<span class="stat-card-value">142</span>
|
|
87
89
|
</div>
|
|
88
90
|
</template>
|
|
89
|
-
</
|
|
91
|
+
</AutoGrid>
|
|
90
92
|
```
|
|
91
93
|
|
|
92
94
|
### Semantic section with auto aria-labelledby
|
|
93
95
|
|
|
94
96
|
```vue
|
|
95
|
-
<
|
|
97
|
+
<AutoGrid tag="section">
|
|
96
98
|
<!-- aria-labelledby is wired automatically via useAriaLabelledById -->
|
|
97
99
|
<template #item-1><div>Item 1</div></template>
|
|
98
100
|
<template #item-2><div>Item 2</div></template>
|
|
99
|
-
</
|
|
101
|
+
</AutoGrid>
|
|
100
102
|
```
|
|
101
103
|
|
|
102
104
|
### Data-driven grid
|
|
@@ -105,20 +107,20 @@ Override these via `style` attribute or a `styleClassPassthrough` class in a con
|
|
|
105
107
|
<script setup lang="ts">
|
|
106
108
|
const stats = [
|
|
107
109
|
{ id: "revenue", label: "Revenue", value: "£24,500" },
|
|
108
|
-
{ id: "clients", label: "Clients",
|
|
110
|
+
{ id: "clients", label: "Clients", value: "142" },
|
|
109
111
|
{ id: "bookings", label: "Bookings", value: "38" },
|
|
110
112
|
];
|
|
111
113
|
</script>
|
|
112
114
|
|
|
113
115
|
<template>
|
|
114
|
-
<
|
|
116
|
+
<AutoGrid>
|
|
115
117
|
<template v-for="stat in stats" #[stat.id] :key="stat.id">
|
|
116
118
|
<div class="stat-card">
|
|
117
119
|
<span class="stat-card-label">{{ stat.label }}</span>
|
|
118
120
|
<span class="stat-card-value">{{ stat.value }}</span>
|
|
119
121
|
</div>
|
|
120
122
|
</template>
|
|
121
|
-
</
|
|
123
|
+
</AutoGrid>
|
|
122
124
|
</template>
|
|
123
125
|
```
|
|
124
126
|
|
|
@@ -137,19 +139,19 @@ See [component-aria-landmark.md](../component-aria-landmark.md) for the full lan
|
|
|
137
139
|
## Local style override scaffold
|
|
138
140
|
|
|
139
141
|
```vue
|
|
140
|
-
<
|
|
142
|
+
<AutoGrid :style-class-passthrough="['my-auto-grid']">
|
|
141
143
|
...
|
|
142
|
-
</
|
|
144
|
+
</AutoGrid>
|
|
143
145
|
|
|
144
146
|
<style>
|
|
145
|
-
/* ───
|
|
147
|
+
/* ─── AutoGrid local overrides ──────────────────────────────────────
|
|
146
148
|
Use CSS custom properties for layout, not utility classes.
|
|
147
149
|
Delete this block if no overrides are needed.
|
|
148
150
|
─────────────────────────────────────────────────────────────────── */
|
|
149
|
-
.
|
|
150
|
-
&.my-
|
|
151
|
-
--
|
|
152
|
-
--
|
|
151
|
+
.auto-grid {
|
|
152
|
+
&.my-auto-grid {
|
|
153
|
+
--auto-grid-min-col-size: 200px;
|
|
154
|
+
--auto-grid-gap: 2rem;
|
|
153
155
|
}
|
|
154
156
|
}
|
|
155
157
|
</style>
|
|
@@ -163,5 +165,5 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
|
|
|
163
165
|
|
|
164
166
|
- Auto-imported in Nuxt — no manual import needed.
|
|
165
167
|
- Slot names can be anything — semantic (`#revenue`) or indexed (`#item-0`). Document order determines render order.
|
|
166
|
-
- `--
|
|
167
|
-
-
|
|
168
|
+
- `--auto-grid-min-col-size` controls the minimum column width; `auto-fit` fills as many columns as will fit.
|
|
169
|
+
- To fix the column count, override `grid-template-columns` directly (e.g. `style="grid-template-columns: repeat(3, 1fr)"`) — there is no single token for this.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: SamaritanPromptMixed
|
|
3
|
+
description: SamaritanPromptMixed animated text prompt — props, MessageConfig API, typewriter/word-pulse effects, accessibility (aria-live), CSS tokens, consumer styling
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# SamaritanPromptMixed
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
`SamaritanPromptMixed` cycles through a list of messages using animated text effects. Each message can independently use either a `typewriter` effect (characters typed and deleted one-by-one) or a `word-pulse` effect (full text fades in, holds, then fades out). Effects and timing can be set globally via props and overridden per-message via `MessageConfig`.
|
|
12
|
+
|
|
13
|
+
The component uses `useCancellableTimer` for all async timing — the loop exits cleanly on `onUnmounted` without dangling promises.
|
|
14
|
+
|
|
15
|
+
### Accessibility
|
|
16
|
+
|
|
17
|
+
- The visual animated content (`.samaritan-prompt__content`) carries `aria-hidden="true"` — screen readers never hear incremental characters.
|
|
18
|
+
- A visually hidden `aria-live="polite"` `aria-atomic="true"` span announces the complete message at the moment it is fully displayed:
|
|
19
|
+
- **Typewriter**: announced after all characters are typed (before the hold/delete phase).
|
|
20
|
+
- **Word-pulse**: announced when `textOpacity` reaches `1` (fade-in complete).
|
|
21
|
+
- `announcedText` is cleared to `""` before each deletion/fade-out, so the same message announced again in the next cycle fires a new announcement (value changes `"" → text`).
|
|
22
|
+
- `prefers-reduced-motion: reduce` disables the content fade transition and the cursor pulse animation via a CSS media query.
|
|
23
|
+
|
|
24
|
+
## Props
|
|
25
|
+
|
|
26
|
+
| Prop | Type | Default | Description |
|
|
27
|
+
|------|------|---------|-------------|
|
|
28
|
+
| `messageConfigs` | `MessageConfig[]` | — | Array of message objects. **Required.** |
|
|
29
|
+
| `effect` | `"typewriter" \| "word-pulse"` | `"typewriter"` | Default effect for all messages. |
|
|
30
|
+
| `typeSpeed` | `number` | `80` | ms per character typed. |
|
|
31
|
+
| `deleteSpeed` | `number` | `40` | ms per character deleted. |
|
|
32
|
+
| `holdDuration` | `number` | `7000` | ms to hold the fully typed text before deleting. |
|
|
33
|
+
| `pauseDuration` | `number` | `1000` | ms pause after a message completes, before the next begins. |
|
|
34
|
+
| `wordDuration` | `number` | `1200` | ms the word-pulse text stays fully visible. |
|
|
35
|
+
| `fadeDuration` | `number` | `400` | ms for the word-pulse fade-in / fade-out CSS transition. |
|
|
36
|
+
| `introDelay` | `number` | `2000` | ms delay before the first message starts on mount. Set to `0` to start immediately. |
|
|
37
|
+
| `hideCursorInCycle` | `boolean` | `true` | Hide the cursor during text animation; show it during pause/hold. Per-message override available. |
|
|
38
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
|
|
39
|
+
|
|
40
|
+
## MessageConfig
|
|
41
|
+
|
|
42
|
+
Each entry in `messageConfigs` accepts:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
interface MessageConfig {
|
|
46
|
+
text: string;
|
|
47
|
+
effect?: "typewriter" | "word-pulse"; // overrides global prop
|
|
48
|
+
typeSpeed?: number;
|
|
49
|
+
deleteSpeed?: number;
|
|
50
|
+
holdDuration?: number;
|
|
51
|
+
pauseDuration?: number;
|
|
52
|
+
wordDuration?: number;
|
|
53
|
+
fadeDuration?: number;
|
|
54
|
+
hideCursorInCycle?: boolean;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Any field omitted falls back to the corresponding component prop.
|
|
59
|
+
|
|
60
|
+
## Basic usage
|
|
61
|
+
|
|
62
|
+
```vue
|
|
63
|
+
<SamaritanPromptMixed
|
|
64
|
+
:message-configs="[
|
|
65
|
+
{ text: 'INITIALISING SYSTEM' },
|
|
66
|
+
{ text: 'LOADING PROFILE' },
|
|
67
|
+
{ text: 'ACCESS GRANTED' },
|
|
68
|
+
]"
|
|
69
|
+
/>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Mixed effects
|
|
73
|
+
|
|
74
|
+
```vue
|
|
75
|
+
<SamaritanPromptMixed
|
|
76
|
+
:message-configs="[
|
|
77
|
+
{ text: 'INITIALISING SYSTEM', effect: 'typewriter', hold-duration: 3000 },
|
|
78
|
+
{ text: 'STAND BY', effect: 'word-pulse', word-duration: 2000 },
|
|
79
|
+
{ text: 'CONNECTED', effect: 'typewriter' },
|
|
80
|
+
]"
|
|
81
|
+
/>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Fast intro, no delay
|
|
85
|
+
|
|
86
|
+
```vue
|
|
87
|
+
<SamaritanPromptMixed
|
|
88
|
+
:intro-delay="0"
|
|
89
|
+
:type-speed="50"
|
|
90
|
+
:message-configs="[{ text: 'READY' }]"
|
|
91
|
+
/>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## CSS custom properties
|
|
95
|
+
|
|
96
|
+
| Token | Default | Description |
|
|
97
|
+
|-------|---------|-------------|
|
|
98
|
+
| `--samaritan-font-size` | `2rem` | Text size. |
|
|
99
|
+
| `--samaritan-color-text` | `#ffffff` | Text colour. |
|
|
100
|
+
| `--samaritan-color-underline` | `#ffffff` | Underline bar colour. |
|
|
101
|
+
| `--samaritan-color-cursor` | `#cc0000` | Cursor colour (peak of pulse). |
|
|
102
|
+
| `--samaritan-color-cursor-off` | `transparent` | Cursor colour (trough of pulse). |
|
|
103
|
+
| `--samaritan-font-family` | `"Mono MMM 5", "Nova Mono", "Courier New", monospace` | Font stack. |
|
|
104
|
+
| `--samaritan-letter-spacing` | `0.08em` | Letter spacing. |
|
|
105
|
+
|
|
106
|
+
The custom font `Mono MMM 5` is loaded via `@font-face` inside the component's unscoped `<style>` block from `/fonts/monoMMM_5.ttf`.
|
|
107
|
+
|
|
108
|
+
## Consumer styling
|
|
109
|
+
|
|
110
|
+
Use an unscoped style block scoped by a page or section wrapper. No `:deep()` needed.
|
|
111
|
+
|
|
112
|
+
```vue
|
|
113
|
+
<style>
|
|
114
|
+
.my-page .samaritan-prompt {
|
|
115
|
+
--samaritan-font-size: 3.2rem;
|
|
116
|
+
--samaritan-color-text: #00ff88;
|
|
117
|
+
--samaritan-color-cursor: #ff4400;
|
|
118
|
+
}
|
|
119
|
+
</style>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## CSS classes
|
|
123
|
+
|
|
124
|
+
| Class | Notes |
|
|
125
|
+
|-------|-------|
|
|
126
|
+
| `.samaritan-prompt` | Root element. |
|
|
127
|
+
| `.samaritan-prompt__content` | Wraps text + underline. `aria-hidden="true"`. Opacity transitions on word-pulse effect. |
|
|
128
|
+
| `.samaritan-prompt__stage` | Centers the text span; min-height prevents layout shift. |
|
|
129
|
+
| `.samaritan-prompt__text` | The visible animated text. `text-transform: uppercase`. |
|
|
130
|
+
| `.samaritan-prompt__underline` | Horizontal rule below text. |
|
|
131
|
+
| `.samaritan-prompt__cursor` | `▲` glyph; pulses via `samaritan-pulse` keyframe. `aria-hidden="true"`. |
|
|
132
|
+
| `.samaritan-prompt__sr-text` | Visually hidden `aria-live` region for screen reader announcements. |
|
|
133
|
+
|
|
134
|
+
## Notes
|
|
135
|
+
|
|
136
|
+
- The `introDelay` only applies at the start of the loop, not between messages. Between messages, `pauseDuration` applies.
|
|
137
|
+
- The cursor pulse animation uses the `samaritan-pulse` keyframe (defined in the component's global style). The cursor hides/shows during cycles via `cursorVisible` + opacity transition, not `display`.
|
|
138
|
+
- `prefers-reduced-motion: reduce` suppresses the opacity transition on `.samaritan-prompt__content` and the `animation` on `.samaritan-prompt__cursor`. The `aria-live` announcements continue to fire regardless.
|
|
139
|
+
- The `MessageConfig` type is exported from the component: `import type { MessageConfig } from 'srcdev-nuxt-components/components/02.molecules/samaritan-prompt/SamaritanPromptMixed.vue'`.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# CSS Animation Utilities
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Scroll-driven animation utility classes bundled with the layer. Apply a class to any element to get a CSS-only, scroll-linked animation — no JavaScript required. All utilities use the [CSS Scroll-Driven Animations](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timeline) spec (`animation-timeline: view()`).
|
|
6
|
+
|
|
7
|
+
**Browser support**: Chrome 115+, Firefox 110+, Safari 18+. No polyfill exists — use `@supports` for graceful degradation where needed.
|
|
8
|
+
|
|
9
|
+
All utilities are wrapped in `@media (prefers-reduced-motion: no-preference)` — animations are automatically disabled for users who prefer reduced motion.
|
|
10
|
+
|
|
11
|
+
## Available classes
|
|
12
|
+
|
|
13
|
+
### `.animation-scroller-x`
|
|
14
|
+
|
|
15
|
+
Scales and fades items relative to their position in a **horizontal scroll container** (carousel, horizontal list). Items at the edges are small and faint; items at the centre are full size and opaque.
|
|
16
|
+
|
|
17
|
+
```css
|
|
18
|
+
animation-timeline: view(x);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| Position | `opacity` | `scale` |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| Edges (0 %, 100 %) | 0.25 | 0.5 |
|
|
24
|
+
| Centre (35 %–65 %) | 1 | 1 |
|
|
25
|
+
|
|
26
|
+
#### Usage
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<!-- Scrollable container — overflow-x must be auto or scroll -->
|
|
30
|
+
<div class="carousel-track" style="overflow-x: auto; display: flex;">
|
|
31
|
+
<div class="animation-scroller-x">Item 1</div>
|
|
32
|
+
<div class="animation-scroller-x">Item 2</div>
|
|
33
|
+
<div class="animation-scroller-x">Item 3</div>
|
|
34
|
+
</div>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The animation is driven by the element's position inside its nearest scrollport on the x-axis. Add `.animation-scroller-x` to each **child** — not the container.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
### `.entry-zoom-reveal`
|
|
42
|
+
|
|
43
|
+
Fades and zooms an element in as it scrolls into the vertical viewport (bottom 30 % inset to top 5 %).
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<div class="entry-zoom-reveal">Content revealed on scroll</div>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- `fill: both` — element stays hidden before entry, visible after exit
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### `.entry-slide-in`
|
|
54
|
+
|
|
55
|
+
Slides an element up from 200 px below as it scrolls into view.
|
|
56
|
+
|
|
57
|
+
```html
|
|
58
|
+
<div class="entry-slide-in">Slides up on scroll</div>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
### `.entry-exit-blur`
|
|
64
|
+
|
|
65
|
+
Blurs an element as it enters and exits the vertical viewport; sharp in the centre (45 %–55 %).
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<div class="entry-exit-blur">Sharp in view, blurred at edges</div>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### `.auto-rotate`
|
|
74
|
+
|
|
75
|
+
Rotates an element 0 → 360 ° as it scrolls through the vertical viewport.
|
|
76
|
+
|
|
77
|
+
```html
|
|
78
|
+
<div class="auto-rotate">
|
|
79
|
+
<img src="/logo.svg" alt="" />
|
|
80
|
+
</div>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Notes
|
|
84
|
+
|
|
85
|
+
- All classes use `animation-timeline: view()` (vertical) **except** `.animation-scroller-x` which uses `view(x)`.
|
|
86
|
+
- The `scroller` keyframe name used by `.animation-scroller-x` is global — avoid re-declaring `@keyframes scroller` in your own CSS.
|
|
87
|
+
- Utility classes are included automatically when you extend the layer — no explicit import needed in your app.
|
package/.claude/skills/index.md
CHANGED
|
@@ -34,6 +34,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
34
34
|
├── component-local-style-override.md — styleClassPassthrough + scoped style block for per-usage visual customisation
|
|
35
35
|
├── component-prop-driven-container-layout.md — vary CSS grid layout inside @container queries using data-* attribute selectors
|
|
36
36
|
├── css-grid-max-width-gutters.md — cap a centre grid column width by growing gutters, with start/center alignment variants
|
|
37
|
+
├── css-animation-utilities.md — scroll-driven animation utility classes: scroller-x (carousel), entry-zoom-reveal, entry-slide-in, entry-exit-blur, auto-rotate
|
|
37
38
|
├── component-aria-landmark.md — useAriaLabelledById composable: aria-labelledby for section/main/article/aside tags
|
|
38
39
|
├── component-export-types.md — move inline component types to app/types/components/ barrel for consumer imports
|
|
39
40
|
├── component-inline-action-button.md — InputButtonCore variant="inline" pattern for buttons embedded in custom input wrappers
|
|
@@ -76,8 +77,9 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
76
77
|
├── display-qr-code.md — DisplayQrCode: QR code SVG from a string value, colour/size/variant/radius props, currentColor default
|
|
77
78
|
├── capture-qr-code.md — CaptureQrCode: live camera scanner, error state, visibility/route/KeepAlive lifecycle, media stream cleanup
|
|
78
79
|
├── decode-qr-code.md — DecodeQrCode: file picker + drag-and-drop image decoder, shared results list, CSS override points
|
|
79
|
-
├──
|
|
80
|
-
|
|
80
|
+
├── auto-grid.md — AutoGrid: auto-fit responsive grid, $slots iteration, --auto-grid-min-col-size/gap tokens, semantic tag + aria
|
|
81
|
+
├── carousel-flip.md — CarouselFlip: FLIP-animated carousel, carouselDataIds slot API, buttonLayout variants (sides/controls-flanking/controls-grouped-right/overlay), CSS tokens
|
|
82
|
+
└── samaritan-prompt-mixed.md — SamaritanPromptMixed: animated text prompt, typewriter/word-pulse effects, MessageConfig API, aria-live accessibility, CSS tokens
|
|
81
83
|
```
|
|
82
84
|
|
|
83
85
|
## Skill file template
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
@keyframes scroller {
|
|
2
|
+
0%,
|
|
3
|
+
100% {
|
|
4
|
+
opacity: 0.25;
|
|
5
|
+
scale: 0.5;
|
|
6
|
+
}
|
|
7
|
+
35%,
|
|
8
|
+
65% {
|
|
9
|
+
opacity: 1;
|
|
10
|
+
scale: 1;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Used to scroll scale elements when scrolling x-axis like a carousel. */
|
|
15
|
+
|
|
16
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
17
|
+
.animation-scroller-x {
|
|
18
|
+
animation: scroller linear both;
|
|
19
|
+
animation-timeline: view(x);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
2
|
+
.entry-exit-blur {
|
|
3
|
+
animation: entryExitBlurAnimation linear both;
|
|
4
|
+
animation-timeline: view();
|
|
5
|
+
}
|
|
4
6
|
}
|
|
5
7
|
@keyframes entryExitBlurAnimation {
|
|
6
8
|
0% {
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
13
|
+
.entry-zoom-reveal {
|
|
14
|
+
animation: entryZoomRevealAnimation both;
|
|
15
|
+
animation-timeline: view(70% 5%);
|
|
16
|
+
}
|
|
15
17
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
@import
|
|
2
|
-
@import
|
|
3
|
-
@import
|
|
4
|
-
@import
|
|
1
|
+
@import "./_entry-zoom-reveal";
|
|
2
|
+
@import "./_entry-slide-in";
|
|
3
|
+
@import "./_entry-exit-blur";
|
|
4
|
+
@import "./_auto-rotate";
|
|
5
|
+
@import "./_animation-scroller-x.css";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component :is="tag" class="auto-grid" :class="[elementClasses, { 'is-responsive': isResponsive }]" :aria-labelledby="ariaLabelledby">
|
|
3
|
+
<slot v-for="(_, name) in $slots" :key="name" :name="name"></slot>
|
|
4
|
+
</component>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
interface Props {
|
|
9
|
+
tag?: "div" | "section" | "article" | "main";
|
|
10
|
+
isResponsive?: boolean;
|
|
11
|
+
styleClassPassthrough?: string | string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
15
|
+
tag: "div",
|
|
16
|
+
isResponsive: false,
|
|
17
|
+
styleClassPassthrough: () => [],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
21
|
+
const { ariaLabelledby } = useAriaLabelledById(props.tag);
|
|
22
|
+
|
|
23
|
+
watch(
|
|
24
|
+
() => props.styleClassPassthrough,
|
|
25
|
+
() => resetElementClasses(props.styleClassPassthrough)
|
|
26
|
+
);
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<style lang="css">
|
|
30
|
+
@layer components {
|
|
31
|
+
.auto-grid {
|
|
32
|
+
--auto-grid-min-col-size-small: 250px;
|
|
33
|
+
--auto-grid-min-col-size-default: 300px;
|
|
34
|
+
--auto-grid-min-col-size-large: 350px;
|
|
35
|
+
--auto-grid-gap: 1rem;
|
|
36
|
+
|
|
37
|
+
display: grid;
|
|
38
|
+
gap: var(--auto-grid-gap);
|
|
39
|
+
|
|
40
|
+
&:not(.is-responsive) {
|
|
41
|
+
grid-template-columns: repeat(auto-fit, minmax(min(var(--auto-grid-min-col-size-default), 100%), 1fr));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
&.is-responsive {
|
|
45
|
+
grid-template-columns: repeat(auto-fit, minmax(min(var(--auto-grid-min-col-size-small), 100%), 1fr));
|
|
46
|
+
|
|
47
|
+
@container (width >= 768px) {
|
|
48
|
+
grid-template-columns: repeat(auto-fit, minmax(min(var(--auto-grid-min-col-size-default), 100%), 1fr));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@container (width >= 1024px) {
|
|
52
|
+
grid-template-columns: repeat(auto-fit, minmax(min(var(--auto-grid-min-col-size-large), 100%), 1fr));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
</style>
|