srcdev-nuxt-components 9.1.43 → 9.1.45

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 (22) hide show
  1. package/.claude/skills/components/display-avatar.md +187 -0
  2. package/.claude/skills/components/display-chip.md +213 -0
  3. package/.claude/skills/components/display-pill.md +162 -0
  4. package/.claude/skills/index.md +3 -0
  5. package/app/components/01.atoms/display-avatar/DisplayAvatar.vue +130 -0
  6. package/app/components/{display-avatar → 01.atoms/display-avatar}/stories/DisplayAvatar.stories.ts +13 -1
  7. package/app/components/01.atoms/display-avatar/tests/DisplayAvatar.spec.ts +208 -0
  8. package/app/components/01.atoms/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +11 -0
  9. package/app/components/01.atoms/display-pill/DisplayPill.vue +134 -0
  10. package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +88 -0
  11. package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +159 -0
  12. package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +7 -0
  13. package/app/components/02.molecules/display-chip/DisplayChip.vue +189 -0
  14. package/app/components/{display-chip → 02.molecules/display-chip}/stories/DisplayChip.stories.ts +38 -54
  15. package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +191 -0
  16. package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +12 -0
  17. package/app/layouts/default.vue +1 -0
  18. package/app/pages/ui/display-chip.vue +201 -66
  19. package/app/pages/ui/display-pill.vue +402 -0
  20. package/package.json +1 -1
  21. package/app/components/display-avatar/DisplayAvatar.vue +0 -148
  22. package/app/components/display-chip/DisplayChip.vue +0 -187
@@ -0,0 +1,187 @@
1
+ # DisplayAvatar Component
2
+
3
+ ## Overview
4
+
5
+ `DisplayAvatar` renders a circular avatar — either an image (via `NuxtImg`) or a text fallback showing initials derived from the `alt` prop. Optionally wraps in a `DisplayChip` to show a status indicator badge.
6
+
7
+ ---
8
+
9
+ ## Props reference
10
+
11
+ > **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:style-class-passthrough`.
12
+
13
+ | Prop (template form) | Type | Default | Notes |
14
+ | -------------------------- | ----------------------------------------- | -------- | ------------------------------------------------------------------ |
15
+ | `as` | `string \| object` | `"span"` | Root element tag. Ignored when `chip` is set. |
16
+ | `src` | `string` | — | Image URL. Renders `NuxtImg` when set; fallback text otherwise. |
17
+ | `alt` | `string` | — | Alt text for the image; also used to derive initials. |
18
+ | `text` | `string` | — | Override the auto-derived initials with an explicit string. |
19
+ | `size` | `"xs" \| "s" \| "md" \| "lg" \| "xl"` | `"md"` | Controls width, height, and font-size. |
20
+ | `chip` | `boolean \| DisplayChipConfig` | — | Add a status chip. `true` uses defaults; pass a config object to customise. |
21
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes on the root element. |
22
+
23
+ ### Size dimensions
24
+
25
+ | Size | Diameter | Font size |
26
+ | ---- | -------- | --------- |
27
+ | `xs` | 24px | 0.75rem |
28
+ | `s` | 32px | 0.875rem |
29
+ | `md` | 40px | 1rem |
30
+ | `lg` | 48px | 1.125rem |
31
+ | `xl` | 56px | 1.25rem |
32
+
33
+ ---
34
+
35
+ ## Slots
36
+
37
+ | Slot | Purpose |
38
+ | --------- | -------------------------------------------------------------------- |
39
+ | `default` | Replaces the auto image/fallback content entirely. |
40
+ | `icon` | Appended inside the avatar (e.g. an icon overlay over the image). |
41
+
42
+ ---
43
+
44
+ ## Fallback text logic
45
+
46
+ When `src` is not set, a `<span>` renders the fallback value:
47
+
48
+ 1. `text` prop — used as-is if provided.
49
+ 2. `alt` initials — first character of each word, capped at two characters.
50
+ 3. Empty string — if neither is set.
51
+
52
+ ```
53
+ alt="John Doe" → "JD"
54
+ alt="Alice" → "A"
55
+ alt="Alice Bob C" → "AB"
56
+ text="?" → "?"
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Usage examples
62
+
63
+ ### Image avatar
64
+
65
+ ```vue
66
+ <DisplayAvatar
67
+ src="/images/profile.jpg"
68
+ alt="Jane Smith"
69
+ size="lg"
70
+ />
71
+ ```
72
+
73
+ ### Initials fallback
74
+
75
+ ```vue
76
+ <DisplayAvatar alt="Jane Smith" size="md" />
77
+ <!-- renders: "JS" -->
78
+ ```
79
+
80
+ ### Custom text fallback
81
+
82
+ ```vue
83
+ <DisplayAvatar text="?" size="xs" />
84
+ ```
85
+
86
+ ### Custom root element
87
+
88
+ ```vue
89
+ <DisplayAvatar as="div" alt="Jane Smith" />
90
+ ```
91
+
92
+ ### With a status chip (default config)
93
+
94
+ ```vue
95
+ <DisplayAvatar
96
+ src="/images/profile.jpg"
97
+ alt="Jane Smith"
98
+ :chip="true"
99
+ />
100
+ ```
101
+
102
+ Default chip config: `{ size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg" }`.
103
+
104
+ ### With a custom chip
105
+
106
+ ```vue
107
+ <DisplayAvatar
108
+ src="/images/profile.jpg"
109
+ alt="Jane Smith"
110
+ :chip="{
111
+ size: '16px',
112
+ maskWidth: '2px',
113
+ offset: '4px',
114
+ angle: '45deg'
115
+ }"
116
+ />
117
+ ```
118
+
119
+ Full `DisplayChipConfig` shape (pass directly as the `chip` value):
120
+
121
+ ```ts
122
+ interface DisplayChipConfig {
123
+ size: string // chip diameter, e.g. "12px"
124
+ maskWidth: string // cutout ring width, e.g. "4px"
125
+ offset: string // distance from avatar edge, e.g. "0px"
126
+ angle: string // position around avatar (0–360deg), e.g. "45deg"
127
+ icon?: string // Iconify icon name
128
+ label?: string // short text (max 3 characters)
129
+ }
130
+ ```
131
+
132
+ ### Default slot override
133
+
134
+ ```vue
135
+ <DisplayAvatar size="xl">
136
+ <template #default>
137
+ <img src="/images/profile.jpg" alt="Jane Smith" class="avatar-image" />
138
+ </template>
139
+ </DisplayAvatar>
140
+ ```
141
+
142
+ ### Icon slot
143
+
144
+ ```vue
145
+ <DisplayAvatar alt="Jane Smith">
146
+ <template #icon>
147
+ <Icon name="bi:check-circle-fill" class="avatar-icon" />
148
+ </template>
149
+ </DisplayAvatar>
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Local style override scaffold
155
+
156
+ ```vue
157
+ <DisplayAvatar
158
+ alt="Jane Smith"
159
+ :style-class-passthrough="['profile-avatar']"
160
+ />
161
+
162
+ <style>
163
+ /* ─── DisplayAvatar local overrides ────────────────────────────────
164
+ Scope by your wrapper class, then nest .display-avatar directly.
165
+ No :deep() needed (component styles are unscoped).
166
+ Delete this block if no overrides are needed.
167
+ ─────────────────────────────────────────────────────────────────── */
168
+ .my-page-section {
169
+ .display-avatar {
170
+ &.profile-avatar {
171
+ /* custom overrides */
172
+ }
173
+ }
174
+ }
175
+ </style>
176
+ ```
177
+
178
+ See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
179
+
180
+ ---
181
+
182
+ ## Notes
183
+
184
+ - Auto-imported in Nuxt — no manual import needed.
185
+ - When `chip` is set, the root element becomes `DisplayChip` and the `as` prop is ignored.
186
+ - `class` and `style` are **not** declared as explicit props — they fall through to the root element automatically via Vue's attribute inheritance (`inheritAttrs: true`). Do not re-add them as props; doing so pulls them out of `$attrs` and breaks automatic inheritance.
187
+ - `NuxtImg` is used for the image, so `@nuxt/image` must be installed in the consuming app.
@@ -0,0 +1,213 @@
1
+ # DisplayChip Component
2
+
3
+ ## Overview
4
+
5
+ `DisplayChip` renders a small status indicator dot (or icon/label badge) that is absolutely positioned on a parent element using CSS trigonometric functions. It works by applying a radial-gradient mask to the parent's content, creating a clean cutout behind the chip. Supports circle and square parent shapes.
6
+
7
+ Used directly for standalone chip overlays, and internally by `DisplayAvatar` when its `chip` prop is set.
8
+
9
+ ---
10
+
11
+ ## Props reference
12
+
13
+ > **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:style-class-passthrough`.
14
+
15
+ | Prop (template form) | Type | Default | Notes |
16
+ | -------------------------- | -------------------------- | ---------- | -------------------------------------------------- |
17
+ | `tag` | `"div" \| "span"` | `"span"` | Root element tag. |
18
+ | `shape` | `"circle" \| "square"` | `"circle"` | Affects position maths — must match the parent shape. |
19
+ | `:config` | `DisplayChipConfig` | see below | Controls chip geometry and optional content. |
20
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes — use for status colour variants. |
21
+
22
+ ### DisplayChipConfig
23
+
24
+ ```ts
25
+ interface DisplayChipConfig {
26
+ size: string // chip dot diameter, e.g. "12px"
27
+ maskWidth: string // cutout ring width around the chip, e.g. "4px"
28
+ offset: string // extra distance from the parent edge, e.g. "0px"
29
+ angle: string // position around the parent (0–360deg), e.g. "45deg"
30
+ icon?: string // Iconify icon name rendered inside the chip
31
+ label?: string // short text rendered inside the chip (max 3 characters)
32
+ }
33
+ ```
34
+
35
+ Default config: `{ size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg" }`.
36
+
37
+ ### Angle reference
38
+
39
+ | Angle | Position |
40
+ | -------- | ------------ |
41
+ | `0deg` | Top |
42
+ | `45deg` | Top-right |
43
+ | `90deg` | Right |
44
+ | `135deg` | Bottom-right |
45
+ | `180deg` | Bottom |
46
+ | `225deg` | Bottom-left |
47
+ | `270deg` | Left |
48
+ | `315deg` | Top-left |
49
+
50
+ ---
51
+
52
+ ## Status colours
53
+
54
+ Apply status via `styleClassPassthrough` — the component has built-in colour variants:
55
+
56
+ | Class | Colour |
57
+ | ---------- | ----------------------- |
58
+ | (none) | `slategrey` (offline) |
59
+ | `online` | `rgb(0, 255, 135)` |
60
+ | `idle` | `rgb(255, 185, 51)` |
61
+ | `dnd` | `rgb(255, 40, 80)` |
62
+
63
+ ```vue
64
+ <DisplayChip :style-class-passthrough="['online']">...</DisplayChip>
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Label constraints
70
+
71
+ - Max 3 characters. Longer values are silently truncated with a `console.warn`.
72
+ - Font-size scales automatically with chip size via `--_font-size-adjust`:
73
+ - 1 char → `0.7 × size`
74
+ - 2 chars → `0.6 × size`
75
+ - 3 chars → `0.5 × size`
76
+
77
+ ---
78
+
79
+ ## Slots
80
+
81
+ | Slot | Purpose |
82
+ | --------- | ------------------------------------------ |
83
+ | `default` | The host element the chip is positioned on. Must be a single block element. |
84
+
85
+ ---
86
+
87
+ ## Usage examples
88
+
89
+ ### Simple status dot on a circular avatar
90
+
91
+ ```vue
92
+ <DisplayChip
93
+ shape="circle"
94
+ :config="{ size: '12px', maskWidth: '4px', offset: '0px', angle: '45deg' }"
95
+ :style-class-passthrough="['online']"
96
+ >
97
+ <div class="avatar">SRC</div>
98
+ </DisplayChip>
99
+ ```
100
+
101
+ ### Status dot on a square card thumbnail
102
+
103
+ ```vue
104
+ <DisplayChip
105
+ shape="square"
106
+ :config="{ size: '10px', maskWidth: '3px', offset: '2px', angle: '135deg' }"
107
+ :style-class-passthrough="['idle']"
108
+ >
109
+ <img src="/thumbnail.jpg" alt="Card thumbnail" />
110
+ </DisplayChip>
111
+ ```
112
+
113
+ ### With an icon inside the chip
114
+
115
+ ```vue
116
+ <DisplayChip
117
+ :config="{ size: '16px', maskWidth: '4px', offset: '0px', angle: '45deg', icon: 'bi:check-circle-fill' }"
118
+ :style-class-passthrough="['online']"
119
+ >
120
+ <div class="avatar">SRC</div>
121
+ </DisplayChip>
122
+ ```
123
+
124
+ ### With a label inside the chip
125
+
126
+ ```vue
127
+ <!-- 1–3 characters only; longer values are truncated with a warning -->
128
+ <DisplayChip
129
+ :config="{ size: '16px', maskWidth: '4px', offset: '0px', angle: '45deg', label: '+2' }"
130
+ :style-class-passthrough="['dnd']"
131
+ >
132
+ <div class="avatar">SRC</div>
133
+ </DisplayChip>
134
+ ```
135
+
136
+ ### Reactive config (QA panel / form pattern)
137
+
138
+ ```vue
139
+ <script setup lang="ts">
140
+ import type { DisplayChipConfig } from 'srcdev-nuxt-components/types/components'
141
+
142
+ const size = ref(12)
143
+ const angle = ref(45)
144
+
145
+ const chipConfig = computed((): DisplayChipConfig => ({
146
+ size: `${size.value}px`,
147
+ maskWidth: '4px',
148
+ offset: '0px',
149
+ angle: `${angle.value}deg`,
150
+ }))
151
+ </script>
152
+
153
+ <template>
154
+ <DisplayChip shape="circle" :config="chipConfig" :style-class-passthrough="['online']">
155
+ <div class="avatar">SRC</div>
156
+ </DisplayChip>
157
+ </template>
158
+ ```
159
+
160
+ ### Via DisplayAvatar (recommended for avatar use cases)
161
+
162
+ Prefer `DisplayAvatar` with its `chip` prop over wiring `DisplayChip` directly:
163
+
164
+ ```vue
165
+ <DisplayAvatar
166
+ src="/images/profile.jpg"
167
+ alt="Jane Smith"
168
+ :chip="{ size: '12px', maskWidth: '4px', offset: '0px', angle: '45deg' }"
169
+ :style-class-passthrough="['online']"
170
+ />
171
+ ```
172
+
173
+ See [display-avatar.md](./display-avatar.md) for the full API.
174
+
175
+ ---
176
+
177
+ ## Local style override scaffold
178
+
179
+ ```vue
180
+ <DisplayChip
181
+ :config="chipConfig"
182
+ :style-class-passthrough="['my-chip']"
183
+ >
184
+ <div class="avatar">SRC</div>
185
+ </DisplayChip>
186
+
187
+ <style>
188
+ /* ─── DisplayChip local overrides ──────────────────────────────────
189
+ Scope by your wrapper class, then nest .display-chip-core directly.
190
+ No :deep() needed (component styles are unscoped).
191
+ Delete this block if no overrides are needed.
192
+ ─────────────────────────────────────────────────────────────────── */
193
+ .my-page-section {
194
+ .display-chip-core {
195
+ &.my-chip {
196
+ /* override colour vars, e.g. */
197
+ --color-online: hotpink;
198
+ }
199
+ }
200
+ }
201
+ </style>
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Notes
207
+
208
+ - Auto-imported in Nuxt — no manual import needed.
209
+ - `shape` must match the actual shape of the slot content — the position maths differs between `circle` (radius-based) and `square` (clamped corner-aware).
210
+ - `config` values are geometric inputs to CSS `calc(cos())` / `calc(sin())` expressions. Pass them as strings with units (`"12px"`, `"45deg"`), not plain numbers.
211
+ - The chip dot is rendered via `::after` pseudo-element; icon and label sit above it at `z-index: 2`.
212
+ - The mask cutout is applied to all direct children of `.display-chip-core` except `.chip-icon` and `.chip-label` — ensure the host element is a direct child.
213
+ - `DisplayChipConfig` and `DisplayChipProps` are both exported from the layer types. Use `DisplayChipConfig` when passing geometry values (the `config` prop). Use `DisplayChipProps` only if you need to pass the full component prop set (e.g. when building a wrapper component).
@@ -0,0 +1,162 @@
1
+ # DisplayPill
2
+
3
+ ## Overview
4
+
5
+ A classic pill/badge component for displaying status labels, tags, or metadata. Supports an icon slot and a text label, with reversible order, six colour variants, three sizes, and a full CSS custom property token API for consumer theming.
6
+
7
+ ## Props
8
+
9
+ | Prop | Type | Default | Description |
10
+ |------|------|---------|-------------|
11
+ | `tag` | `"span" \| "div" \| "button" \| "a"` | `"span"` | Root element. Use `"button"` or `"a"` for interactive pills — cursor auto-switches to pointer |
12
+ | `label` | `string` | `undefined` | Pill text. When set, renders inside `.pill-label` and suppresses the default slot |
13
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Size variant |
14
+ | `variant` | `"default" \| "primary" \| "success" \| "warning" \| "danger" \| "neutral"` | `"default"` | Colour variant |
15
+ | `reversed` | `boolean` | `false` | Reverses icon/label order (`flex-direction: row-reverse`) |
16
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes merged onto the root element |
17
+
18
+ ## Slots
19
+
20
+ | Slot | Description |
21
+ |------|-------------|
22
+ | `icon` | Icon content — rendered before the label by default, after when `reversed=true` |
23
+ | `default` | Custom label content — only used when the `label` prop is not set |
24
+
25
+ ## Basic usage
26
+
27
+ ```vue
28
+ <DisplayPill label="Active" variant="success">
29
+ <template #icon>
30
+ <Icon name="material-symbols:circle" />
31
+ </template>
32
+ </DisplayPill>
33
+ ```
34
+
35
+ ## Reversed order (text → icon)
36
+
37
+ ```vue
38
+ <DisplayPill label="Draft" variant="warning" :reversed="true">
39
+ <template #icon>
40
+ <Icon name="material-symbols:edit-outline" />
41
+ </template>
42
+ </DisplayPill>
43
+ ```
44
+
45
+ ## Icon only
46
+
47
+ ```vue
48
+ <DisplayPill variant="danger" aria-label="Error">
49
+ <template #icon>
50
+ <Icon name="material-symbols:error-outline" />
51
+ </template>
52
+ </DisplayPill>
53
+ ```
54
+
55
+ ## Custom slot content
56
+
57
+ When `label` is not set, the default slot is rendered instead:
58
+
59
+ ```vue
60
+ <DisplayPill variant="primary">
61
+ <template #icon>
62
+ <Icon name="material-symbols:star" />
63
+ </template>
64
+ <strong>Pro</strong> plan
65
+ </DisplayPill>
66
+ ```
67
+
68
+ ## CSS token API
69
+
70
+ All tokens follow the `--theme-pill-*` convention and fall back to project colour tokens.
71
+
72
+ ### Colour
73
+
74
+ | Token | Default |
75
+ |-------|---------|
76
+ | `--theme-pill-bg` | `var(--slate-01)` |
77
+ | `--theme-pill-color` | `var(--slate-09)` |
78
+ | `--theme-pill-primary-bg` | `var(--blue-01)` |
79
+ | `--theme-pill-primary-color` | `var(--blue-09)` |
80
+ | `--theme-pill-success-bg` | `var(--green-01)` |
81
+ | `--theme-pill-success-color` | `var(--green-10)` |
82
+ | `--theme-pill-warning-bg` | `var(--yellow-08, #fef9c3)` |
83
+ | `--theme-pill-warning-color` | `var(--yellow-03, #a16207)` |
84
+ | `--theme-pill-danger-bg` | `var(--red-01)` |
85
+ | `--theme-pill-danger-color` | `var(--red-10)` |
86
+ | `--theme-pill-neutral-bg` | `var(--slate-08)` |
87
+ | `--theme-pill-neutral-color` | `var(--slate-03)` |
88
+
89
+ ### Border
90
+
91
+ | Token | Default |
92
+ |-------|---------|
93
+ | `--theme-pill-border-color` | `transparent` |
94
+ | `--theme-pill-border-width` | `1px` |
95
+ | `--theme-pill-border-style` | `solid` |
96
+ | `--theme-pill-border-radius` | `100vw` |
97
+
98
+ ### Outline
99
+
100
+ | Token | Default |
101
+ |-------|---------|
102
+ | `--theme-pill-outline` | `none` |
103
+ | `--theme-pill-outline-offset` | `0px` |
104
+
105
+ ### Typography & spacing
106
+
107
+ | Token | Default |
108
+ |-------|---------|
109
+ | `--theme-pill-font-size` | `1.2rem` |
110
+ | `--theme-pill-font-size-sm` | `1rem` |
111
+ | `--theme-pill-font-size-lg` | `1.4rem` |
112
+ | `--theme-pill-font-weight` | `500` |
113
+ | `--theme-pill-padding-x` | `1rem` |
114
+ | `--theme-pill-padding-y` | `0.4rem` |
115
+ | `--theme-pill-gap` | `0.5rem` |
116
+ | `--theme-pill-icon-size` | `1.4rem` |
117
+
118
+ ## Theming via styleClassPassthrough
119
+
120
+ Override tokens by targeting `.your-class.display-pill` — no `:deep()` needed:
121
+
122
+ ```vue
123
+ <DisplayPill label="New" variant="primary" style-class-passthrough="pill-new" />
124
+ ```
125
+
126
+ ```css
127
+ .pill-new.display-pill {
128
+ --theme-pill-primary-bg: var(--green-01);
129
+ --theme-pill-primary-color: var(--green-10);
130
+ --theme-pill-border-color: currentColor;
131
+ --theme-pill-border-width: 1.5px;
132
+ }
133
+ ```
134
+
135
+ ## Border and outline examples
136
+
137
+ ```css
138
+ /* Solid border */
139
+ .bordered.display-pill {
140
+ --theme-pill-border-color: currentColor;
141
+ --theme-pill-border-width: 1.5px;
142
+ }
143
+
144
+ /* Dashed border */
145
+ .dashed.display-pill {
146
+ --theme-pill-border-color: currentColor;
147
+ --theme-pill-border-width: 1.5px;
148
+ --theme-pill-border-style: dashed;
149
+ }
150
+
151
+ /* Focus-ring style outline */
152
+ .outlined.display-pill {
153
+ --theme-pill-outline: 2px solid currentColor;
154
+ --theme-pill-outline-offset: 3px;
155
+ }
156
+ ```
157
+
158
+ ## Notes
159
+
160
+ - `border-radius` defaults to `100vw` — this always collapses to the tightest possible radius on the shorter axis, making it geometry-independent (no magic number needed).
161
+ - `cursor: default` and `user-select: none` are applied unconditionally. `cursor: pointer` is applied automatically via `:is(button, a)` when `tag` is interactive.
162
+ - All variant colour pairs use a light-tint background (`--color-01`) with a dark text colour (`--color-09`/`--color-10`) to ensure WCAG AA contrast. The project colour scale runs `00` (lightest) → `10` (darkest) — the opposite of Tailwind.
@@ -78,6 +78,9 @@ Each skill is a single markdown file named `<area>-<task>.md`.
78
78
  ├── capture-qr-code.md — CaptureQrCode: live camera scanner, error state, visibility/route/KeepAlive lifecycle, media stream cleanup
79
79
  ├── decode-qr-code.md — DecodeQrCode: file picker + drag-and-drop image decoder, shared results list, CSS override points
80
80
  ├── auto-grid.md — AutoGrid: auto-fit responsive grid, $slots iteration, --auto-grid-min-col-size/gap tokens, semantic tag + aria
81
+ ├── display-avatar.md — DisplayAvatar: circular avatar with image/initials fallback, size variants, chip badge, icon slot, styleClassPassthrough
82
+ ├── display-chip.md — DisplayChip: status indicator chip overlay, CSS trig positioning, circle/square shapes, status colours, icon/label content
83
+ ├── display-pill.md — DisplayPill: pill/badge label with icon slot, 6 variants, 3 sizes, reversible order, full CSS token API for border/outline/colour
81
84
  ├── carousel-flip.md — CarouselFlip: FLIP-animated carousel, carouselDataIds slot API, buttonLayout variants (sides/controls-flanking/controls-grouped-right/overlay), CSS tokens
82
85
  └── samaritan-prompt-mixed.md — SamaritanPromptMixed: animated text prompt, typewriter/word-pulse effects, MessageConfig API, aria-live accessibility, CSS tokens
83
86
  ```
@@ -0,0 +1,130 @@
1
+ <template>
2
+ <component
3
+ :is="props.chip ? DisplayChip : as"
4
+ v-bind="
5
+ props.chip
6
+ ? typeof props.chip === 'object'
7
+ ? { tag: chipTag, config: props.chip }
8
+ : { tag: chipTag, config: chipDefaultConfig }
9
+ : {}
10
+ "
11
+ class="display-avatar"
12
+ :class="[size, elementClasses]"
13
+ :style-class-passthrough="elementClasses"
14
+ >
15
+ <slot name="default">
16
+ <NuxtImg v-if="src" :src :alt="alt || 'Avatar'" width="100%" height="100%" class="avatar-image" />
17
+ <span v-else>{{ fallback }}</span>
18
+ </slot>
19
+ <slot name="icon"></slot>
20
+ </component>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ import DisplayChip from "../../02.molecules/display-chip/DisplayChip.vue";
25
+ import type { DisplayChipConfig } from "~/types/components";
26
+
27
+ interface Props {
28
+ as?: string | object;
29
+ src?: string;
30
+ alt?: string;
31
+ text?: string;
32
+ size?: "xs" | "s" | "md" | "lg" | "xl" | string;
33
+ chip?: boolean | DisplayChipConfig;
34
+ styleClassPassthrough?: string | string[];
35
+ }
36
+
37
+ const props = withDefaults(defineProps<Props>(), {
38
+ as: "span",
39
+ src: undefined,
40
+ alt: undefined,
41
+ text: undefined,
42
+ size: "md",
43
+ chip: undefined,
44
+ styleClassPassthrough: () => [],
45
+ });
46
+
47
+ const { elementClasses, resetElementClasses, updateElementClasses } = useStyleClassPassthrough(
48
+ props.styleClassPassthrough
49
+ );
50
+
51
+ if (props.chip && typeof props.chip === "object" && !("styleClassPassthrough" in props.chip)) {
52
+ updateElementClasses(["display-avatar", props.size]);
53
+ }
54
+
55
+ const fallback = computed(
56
+ () =>
57
+ props.text ||
58
+ (props.alt || "")
59
+ .split(" ")
60
+ .map((word) => word.charAt(0))
61
+ .join("")
62
+ .substring(0, 2)
63
+ );
64
+
65
+ const chipDefaultConfig: DisplayChipConfig = {
66
+ size: "12px",
67
+ maskWidth: "4px",
68
+ offset: "0px",
69
+ angle: "90deg",
70
+ };
71
+
72
+ const chipTag = computed((): "div" | "span" => (props.as === "div" || props.as === "span" ? props.as : "span"));
73
+
74
+ watch(
75
+ () => props.styleClassPassthrough,
76
+ () => {
77
+ resetElementClasses(props.styleClassPassthrough);
78
+ }
79
+ );
80
+ </script>
81
+
82
+ <style lang="css">
83
+ @layer components {
84
+ .display-avatar {
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ border-radius: 50%;
89
+ color: var(--slate-03);
90
+
91
+ isolation: isolate;
92
+
93
+ &.xs {
94
+ width: 24px;
95
+ height: 24px;
96
+ font-size: 0.75rem;
97
+ }
98
+ &.s {
99
+ width: 32px;
100
+ height: 32px;
101
+ font-size: 0.875rem;
102
+ }
103
+ &.md {
104
+ width: 40px;
105
+ height: 40px;
106
+ font-size: 1rem;
107
+ }
108
+ &.lg {
109
+ width: 48px;
110
+ height: 48px;
111
+ font-size: 1.125rem;
112
+ }
113
+ &.xl {
114
+ width: 56px;
115
+ height: 56px;
116
+ font-size: 1.25rem;
117
+ }
118
+
119
+ .avatar-image {
120
+ width: 100%;
121
+ border-radius: 50%;
122
+ object-fit: cover;
123
+ }
124
+
125
+ .avatar-icon {
126
+ font-size: 24px;
127
+ }
128
+ }
129
+ }
130
+ </style>