srcdev-nuxt-components 9.1.43 → 9.1.44

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.
@@ -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).
@@ -78,6 +78,8 @@ 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
81
83
  ├── carousel-flip.md — CarouselFlip: FLIP-animated carousel, carouselDataIds slot API, buttonLayout variants (sides/controls-flanking/controls-grouped-right/overlay), CSS tokens
82
84
  └── samaritan-prompt-mixed.md — SamaritanPromptMixed: animated text prompt, typewriter/word-pulse effects, MessageConfig API, aria-live accessibility, CSS tokens
83
85
  ```
@@ -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>
@@ -5,6 +5,14 @@ export default {
5
5
  title: "Components/UI/DisplayAvatar",
6
6
  component: StorybookComponent,
7
7
  argTypes: {
8
+ size: {
9
+ control: { type: "inline-radio" },
10
+ options: ["xs", "s", "md", "lg", "xl"],
11
+ description: "Avatar size",
12
+ table: {
13
+ category: "Avatar",
14
+ },
15
+ },
8
16
  src: {
9
17
  control: { type: "text" },
10
18
  description: "Avatar image source URL",
@@ -61,6 +69,7 @@ export default {
61
69
  },
62
70
  },
63
71
  args: {
72
+ size: "md",
64
73
  src: "https://github.com/srcdev.png",
65
74
  alt: "SrcDev Avatar",
66
75
  chipSize: 12,
@@ -77,7 +86,9 @@ const Template: StoryFn<typeof StorybookComponent> = (args) => ({
77
86
  return { args };
78
87
  },
79
88
  template: `
89
+ <div style="display: flex; align-items: center; justify-content: center; height: 100vh;">
80
90
  <StorybookComponent
91
+ :size="args.size"
81
92
  :src="args.src"
82
93
  :alt="args.alt"
83
94
  :chip="{
@@ -88,6 +99,7 @@ const Template: StoryFn<typeof StorybookComponent> = (args) => ({
88
99
  }"
89
100
  :style-class-passthrough="args.styleClassPassthrough"
90
101
  />
102
+ </div>
91
103
  `,
92
104
  });
93
105