srcdev-nuxt-components 9.1.44 → 9.1.46
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/components/display-pill.md +162 -0
- package/.claude/skills/index.md +1 -0
- package/app/components/01.atoms/display-avatar/stories/DisplayAvatar.stories.ts +1 -1
- package/app/components/01.atoms/display-pill/DisplayPill.vue +134 -0
- package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +88 -0
- package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +159 -0
- package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +7 -0
- package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +1 -1
- package/app/components/02.molecules/samaritan-prompt/SamaritanPromptMixed.vue +57 -49
- package/app/layouts/default.vue +1 -0
- package/app/pages/samaritan.vue +0 -5
- package/app/pages/ui/display-pill.vue +402 -0
- package/package.json +1 -1
|
@@ -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.
|
package/.claude/skills/index.md
CHANGED
|
@@ -80,6 +80,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
80
80
|
├── auto-grid.md — AutoGrid: auto-fit responsive grid, $slots iteration, --auto-grid-min-col-size/gap tokens, semantic tag + aria
|
|
81
81
|
├── display-avatar.md — DisplayAvatar: circular avatar with image/initials fallback, size variants, chip badge, icon slot, styleClassPassthrough
|
|
82
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
|
|
83
84
|
├── carousel-flip.md — CarouselFlip: FLIP-animated carousel, carouselDataIds slot API, buttonLayout variants (sides/controls-flanking/controls-grouped-right/overlay), CSS tokens
|
|
84
85
|
└── samaritan-prompt-mixed.md — SamaritanPromptMixed: animated text prompt, typewriter/word-pulse effects, MessageConfig API, aria-live accessibility, CSS tokens
|
|
85
86
|
```
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="tag"
|
|
4
|
+
class="display-pill"
|
|
5
|
+
:class="[size, variant, { 'is-reversed': reversed }, elementClasses]"
|
|
6
|
+
>
|
|
7
|
+
<slot name="icon"></slot>
|
|
8
|
+
<span v-if="label" class="pill-label">{{ label }}</span>
|
|
9
|
+
<slot v-else name="default"></slot>
|
|
10
|
+
</component>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
interface Props {
|
|
15
|
+
tag?: "span" | "div" | "button" | "a";
|
|
16
|
+
label?: string;
|
|
17
|
+
size?: "sm" | "md" | "lg";
|
|
18
|
+
variant?: "default" | "primary" | "success" | "warning" | "danger" | "neutral";
|
|
19
|
+
reversed?: boolean;
|
|
20
|
+
styleClassPassthrough?: string | string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
24
|
+
tag: "span",
|
|
25
|
+
label: undefined,
|
|
26
|
+
size: "md",
|
|
27
|
+
variant: "default",
|
|
28
|
+
reversed: false,
|
|
29
|
+
styleClassPassthrough: () => [],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
33
|
+
|
|
34
|
+
watch(
|
|
35
|
+
() => props.styleClassPassthrough,
|
|
36
|
+
() => {
|
|
37
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<style lang="css">
|
|
43
|
+
@layer components {
|
|
44
|
+
.display-pill {
|
|
45
|
+
--_bg: var(--theme-pill-bg, var(--slate-01));
|
|
46
|
+
--_color: var(--theme-pill-color, var(--slate-09));
|
|
47
|
+
--_border-color: var(--theme-pill-border-color, transparent);
|
|
48
|
+
--_border-width: var(--theme-pill-border-width, 1px);
|
|
49
|
+
--_border-style: var(--theme-pill-border-style, solid);
|
|
50
|
+
/* 100vw always resolves to the correct pill radius regardless of element size */
|
|
51
|
+
--_border-radius: var(--theme-pill-border-radius, 100vw);
|
|
52
|
+
--_outline: var(--theme-pill-outline, none);
|
|
53
|
+
--_outline-offset: var(--theme-pill-outline-offset, 0px);
|
|
54
|
+
--_gap: var(--theme-pill-gap, 0.5rem);
|
|
55
|
+
--_font-size: var(--theme-pill-font-size, 1.2rem);
|
|
56
|
+
--_font-weight: var(--theme-pill-font-weight, 500);
|
|
57
|
+
--_padding-x: var(--theme-pill-padding-x, 1rem);
|
|
58
|
+
--_padding-y: var(--theme-pill-padding-y, 0.4rem);
|
|
59
|
+
--_icon-size: var(--theme-pill-icon-size, 1.4rem);
|
|
60
|
+
|
|
61
|
+
display: inline-flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
gap: var(--_gap);
|
|
64
|
+
padding: var(--_padding-y) var(--_padding-x);
|
|
65
|
+
border-radius: var(--_border-radius);
|
|
66
|
+
border: var(--_border-width) var(--_border-style) var(--_border-color);
|
|
67
|
+
outline: var(--_outline);
|
|
68
|
+
outline-offset: var(--_outline-offset);
|
|
69
|
+
background-color: var(--_bg);
|
|
70
|
+
color: var(--_color);
|
|
71
|
+
font-size: var(--_font-size);
|
|
72
|
+
font-weight: var(--_font-weight);
|
|
73
|
+
line-height: 1;
|
|
74
|
+
white-space: nowrap;
|
|
75
|
+
width: fit-content;
|
|
76
|
+
cursor: default;
|
|
77
|
+
user-select: none;
|
|
78
|
+
|
|
79
|
+
&:is(button, a) {
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
&.is-reversed {
|
|
84
|
+
flex-direction: row-reverse;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.pill-label {
|
|
88
|
+
display: inline-flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Sizes */
|
|
93
|
+
&.sm {
|
|
94
|
+
--_font-size: var(--theme-pill-font-size-sm, 1rem);
|
|
95
|
+
--_padding-x: var(--theme-pill-padding-x-sm, 0.8rem);
|
|
96
|
+
--_padding-y: var(--theme-pill-padding-y-sm, 0.3rem);
|
|
97
|
+
--_icon-size: var(--theme-pill-icon-size-sm, 1.2rem);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
&.lg {
|
|
101
|
+
--_font-size: var(--theme-pill-font-size-lg, 1.4rem);
|
|
102
|
+
--_padding-x: var(--theme-pill-padding-x-lg, 1.2rem);
|
|
103
|
+
--_padding-y: var(--theme-pill-padding-y-lg, 0.6rem);
|
|
104
|
+
--_icon-size: var(--theme-pill-icon-size-lg, 1.6rem);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Variants */
|
|
108
|
+
&.primary {
|
|
109
|
+
--_bg: var(--theme-pill-primary-bg, var(--blue-01));
|
|
110
|
+
--_color: var(--theme-pill-primary-color, var(--blue-09));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
&.success {
|
|
114
|
+
--_bg: var(--theme-pill-success-bg, var(--green-01));
|
|
115
|
+
--_color: var(--theme-pill-success-color, var(--green-10));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
&.warning {
|
|
119
|
+
--_bg: var(--theme-pill-warning-bg, var(--yellow-08, #fef9c3));
|
|
120
|
+
--_color: var(--theme-pill-warning-color, var(--yellow-03, #a16207));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
&.danger {
|
|
124
|
+
--_bg: var(--theme-pill-danger-bg, var(--red-01));
|
|
125
|
+
--_color: var(--theme-pill-danger-color, var(--red-10));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
&.neutral {
|
|
129
|
+
--_bg: var(--theme-pill-neutral-bg, var(--slate-08));
|
|
130
|
+
--_color: var(--theme-pill-neutral-color, var(--slate-03));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
</style>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import StorybookComponent from "../DisplayPill.vue";
|
|
3
|
+
|
|
4
|
+
type StoryArgs = {
|
|
5
|
+
tag: "span" | "div" | "button" | "a";
|
|
6
|
+
label: string;
|
|
7
|
+
size: "sm" | "md" | "lg";
|
|
8
|
+
variant: "default" | "primary" | "success" | "warning" | "danger" | "neutral";
|
|
9
|
+
reversed: boolean;
|
|
10
|
+
showIcon: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
title: "Atoms/DisplayPill",
|
|
15
|
+
component: StorybookComponent,
|
|
16
|
+
argTypes: {
|
|
17
|
+
tag: {
|
|
18
|
+
control: { type: "inline-radio" },
|
|
19
|
+
options: ["span", "div", "button", "a"],
|
|
20
|
+
description: "Root element tag",
|
|
21
|
+
},
|
|
22
|
+
label: {
|
|
23
|
+
control: { type: "text" },
|
|
24
|
+
description: "Pill label text",
|
|
25
|
+
},
|
|
26
|
+
size: {
|
|
27
|
+
control: { type: "inline-radio" },
|
|
28
|
+
options: ["sm", "md", "lg"],
|
|
29
|
+
description: "Pill size",
|
|
30
|
+
},
|
|
31
|
+
variant: {
|
|
32
|
+
control: { type: "inline-radio" },
|
|
33
|
+
options: ["default", "primary", "success", "warning", "danger", "neutral"],
|
|
34
|
+
description: "Colour variant",
|
|
35
|
+
},
|
|
36
|
+
reversed: {
|
|
37
|
+
control: { type: "boolean" },
|
|
38
|
+
description: "Swap icon and label order",
|
|
39
|
+
},
|
|
40
|
+
showIcon: {
|
|
41
|
+
control: { type: "boolean" },
|
|
42
|
+
description: "Show icon slot",
|
|
43
|
+
},
|
|
44
|
+
styleClassPassthrough: {
|
|
45
|
+
table: { disable: true },
|
|
46
|
+
},
|
|
47
|
+
class: {
|
|
48
|
+
table: { disable: true },
|
|
49
|
+
},
|
|
50
|
+
style: {
|
|
51
|
+
table: { disable: true },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
args: {
|
|
55
|
+
tag: "span",
|
|
56
|
+
label: "Status",
|
|
57
|
+
size: "md",
|
|
58
|
+
variant: "default",
|
|
59
|
+
reversed: false,
|
|
60
|
+
showIcon: true,
|
|
61
|
+
},
|
|
62
|
+
} as Meta<StoryArgs>;
|
|
63
|
+
|
|
64
|
+
const Template: StoryFn<StoryArgs> = (args) => ({
|
|
65
|
+
components: { StorybookComponent },
|
|
66
|
+
setup() {
|
|
67
|
+
return { args };
|
|
68
|
+
},
|
|
69
|
+
template: `
|
|
70
|
+
<div style="display: flex; align-items: center; justify-content: center; min-height: 100vh; gap: 1.6rem; flex-wrap: wrap;">
|
|
71
|
+
<StorybookComponent
|
|
72
|
+
:tag="args.tag"
|
|
73
|
+
:label="args.label"
|
|
74
|
+
:size="args.size"
|
|
75
|
+
:variant="args.variant"
|
|
76
|
+
:reversed="args.reversed"
|
|
77
|
+
>
|
|
78
|
+
<template v-if="args.showIcon" #icon>
|
|
79
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
80
|
+
<circle cx="12" cy="12" r="5" />
|
|
81
|
+
</svg>
|
|
82
|
+
</template>
|
|
83
|
+
</StorybookComponent>
|
|
84
|
+
</div>
|
|
85
|
+
`,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const Default = Template.bind({});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import DisplayPill from "../DisplayPill.vue";
|
|
4
|
+
|
|
5
|
+
describe("DisplayPill", () => {
|
|
6
|
+
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
it("mounts without error", async () => {
|
|
9
|
+
const wrapper = await mountSuspended(DisplayPill);
|
|
10
|
+
expect(wrapper.vm).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// ─── Snapshots ───────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
it("renders correct HTML structure (default)", async () => {
|
|
16
|
+
const wrapper = await mountSuspended(DisplayPill, {
|
|
17
|
+
props: { label: "Status" },
|
|
18
|
+
});
|
|
19
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("renders correct HTML structure (with icon slot)", async () => {
|
|
23
|
+
const wrapper = await mountSuspended(DisplayPill, {
|
|
24
|
+
props: { label: "Status", variant: "success", size: "lg" },
|
|
25
|
+
slots: { icon: "<span class='icon'>✓</span>" },
|
|
26
|
+
});
|
|
27
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("renders correct HTML structure (reversed)", async () => {
|
|
31
|
+
const wrapper = await mountSuspended(DisplayPill, {
|
|
32
|
+
props: { label: "Status", reversed: true },
|
|
33
|
+
slots: { icon: "<span class='icon'>✓</span>" },
|
|
34
|
+
});
|
|
35
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ─── Root element ─────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
it("renders as <span> by default", async () => {
|
|
41
|
+
const wrapper = await mountSuspended(DisplayPill);
|
|
42
|
+
expect(wrapper.element.tagName).toBe("SPAN");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it.each(["span", "div", "button", "a"] as const)("renders as <%s> when tag='%s'", async (tag) => {
|
|
46
|
+
const wrapper = await mountSuspended(DisplayPill, { props: { tag } });
|
|
47
|
+
expect(wrapper.element.tagName).toBe(tag.toUpperCase());
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ─── Base class ───────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
it("always has the display-pill class", async () => {
|
|
53
|
+
const wrapper = await mountSuspended(DisplayPill);
|
|
54
|
+
expect(wrapper.classes()).toContain("display-pill");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ─── Label ────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
it("renders label text inside .pill-label", async () => {
|
|
60
|
+
const wrapper = await mountSuspended(DisplayPill, {
|
|
61
|
+
props: { label: "Active" },
|
|
62
|
+
});
|
|
63
|
+
expect(wrapper.find(".pill-label").text()).toBe("Active");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("renders default slot when no label prop is set", async () => {
|
|
67
|
+
const wrapper = await mountSuspended(DisplayPill, {
|
|
68
|
+
slots: { default: "<strong class='custom'>Custom</strong>" },
|
|
69
|
+
});
|
|
70
|
+
expect(wrapper.find(".custom").exists()).toBe(true);
|
|
71
|
+
expect(wrapper.find(".pill-label").exists()).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("does not render default slot when label prop is set", async () => {
|
|
75
|
+
const wrapper = await mountSuspended(DisplayPill, {
|
|
76
|
+
props: { label: "Active" },
|
|
77
|
+
slots: { default: "<strong class='custom'>Custom</strong>" },
|
|
78
|
+
});
|
|
79
|
+
expect(wrapper.find(".pill-label").exists()).toBe(true);
|
|
80
|
+
expect(wrapper.find(".custom").exists()).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ─── Icon slot ────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
it("renders icon slot content", async () => {
|
|
86
|
+
const wrapper = await mountSuspended(DisplayPill, {
|
|
87
|
+
slots: { icon: "<span class='test-icon'>★</span>" },
|
|
88
|
+
});
|
|
89
|
+
expect(wrapper.find(".test-icon").exists()).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ─── Size ─────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
it("applies md size class by default", async () => {
|
|
95
|
+
const wrapper = await mountSuspended(DisplayPill);
|
|
96
|
+
expect(wrapper.classes()).toContain("md");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it.each(["sm", "md", "lg"] as const)("applies %s size class when size='%s'", async (size) => {
|
|
100
|
+
const wrapper = await mountSuspended(DisplayPill, { props: { size } });
|
|
101
|
+
expect(wrapper.classes()).toContain(size);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ─── Variant ──────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
it("applies default variant class by default", async () => {
|
|
107
|
+
const wrapper = await mountSuspended(DisplayPill);
|
|
108
|
+
expect(wrapper.classes()).toContain("default");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it.each(["default", "primary", "success", "warning", "danger", "neutral"] as const)(
|
|
112
|
+
"applies %s variant class when variant='%s'",
|
|
113
|
+
async (variant) => {
|
|
114
|
+
const wrapper = await mountSuspended(DisplayPill, { props: { variant } });
|
|
115
|
+
expect(wrapper.classes()).toContain(variant);
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// ─── Reversed ─────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
it("does not apply is-reversed class by default", async () => {
|
|
122
|
+
const wrapper = await mountSuspended(DisplayPill);
|
|
123
|
+
expect(wrapper.classes()).not.toContain("is-reversed");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("applies is-reversed class when reversed=true", async () => {
|
|
127
|
+
const wrapper = await mountSuspended(DisplayPill, {
|
|
128
|
+
props: { reversed: true },
|
|
129
|
+
});
|
|
130
|
+
expect(wrapper.classes()).toContain("is-reversed");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ─── styleClassPassthrough ────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
it("applies a single styleClassPassthrough string", async () => {
|
|
136
|
+
const wrapper = await mountSuspended(DisplayPill, {
|
|
137
|
+
props: { styleClassPassthrough: "featured" },
|
|
138
|
+
});
|
|
139
|
+
expect(wrapper.classes()).toContain("featured");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
143
|
+
const wrapper = await mountSuspended(DisplayPill, {
|
|
144
|
+
props: { styleClassPassthrough: ["featured", "highlighted"] },
|
|
145
|
+
});
|
|
146
|
+
expect(wrapper.classes()).toContain("featured");
|
|
147
|
+
expect(wrapper.classes()).toContain("highlighted");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("updates classes when styleClassPassthrough prop changes", async () => {
|
|
151
|
+
const wrapper = await mountSuspended(DisplayPill, {
|
|
152
|
+
props: { styleClassPassthrough: ["featured"] },
|
|
153
|
+
});
|
|
154
|
+
expect(wrapper.classes()).toContain("featured");
|
|
155
|
+
await wrapper.setProps({ styleClassPassthrough: ["highlighted"] });
|
|
156
|
+
expect(wrapper.classes()).not.toContain("featured");
|
|
157
|
+
expect(wrapper.classes()).toContain("highlighted");
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`DisplayPill > renders correct HTML structure (default) 1`] = `"<span class="display-pill md default"><span class="pill-label">Status</span></span>"`;
|
|
4
|
+
|
|
5
|
+
exports[`DisplayPill > renders correct HTML structure (reversed) 1`] = `"<span class="display-pill md default is-reversed"><span class="icon">✓</span><span class="pill-label">Status</span></span>"`;
|
|
6
|
+
|
|
7
|
+
exports[`DisplayPill > renders correct HTML structure (with icon slot) 1`] = `"<span class="display-pill lg success"><span class="icon">✓</span><span class="pill-label">Status</span></span>"`;
|
|
@@ -172,72 +172,80 @@ onUnmounted(stop);
|
|
|
172
172
|
src: url("/fonts/monoMMM_5.ttf") format("truetype");
|
|
173
173
|
font-weight: normal;
|
|
174
174
|
font-style: normal;
|
|
175
|
-
font-display:
|
|
175
|
+
font-display: optional;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
.samaritan-
|
|
179
|
-
--_font-size: var(--samaritan-font-size, 2rem);
|
|
180
|
-
--_color-text: var(--samaritan-color-text, #ffffff);
|
|
181
|
-
--_color-underline: var(--samaritan-color-underline, #ffffff);
|
|
182
|
-
--_color-cursor: var(--samaritan-color-cursor, #cc0000);
|
|
183
|
-
--_color-cursor-off: var(--samaritan-color-cursor-off, transparent);
|
|
184
|
-
--_font-family: var(--samaritan-font-family, "Mono MMM 5", "Nova Mono", "Courier New", monospace);
|
|
185
|
-
--_letter-spacing: var(--samaritan-letter-spacing, 0.08em);
|
|
186
|
-
|
|
178
|
+
.samaritan-stage {
|
|
187
179
|
display: flex;
|
|
188
|
-
flex-direction: column;
|
|
189
180
|
align-items: center;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
181
|
+
justify-content: center;
|
|
182
|
+
min-block-size: 100dvh;
|
|
183
|
+
width: 100%;
|
|
184
|
+
|
|
185
|
+
.samaritan-prompt {
|
|
186
|
+
--_font-size: var(--samaritan-font-size, 2rem);
|
|
187
|
+
--_color-text: var(--samaritan-color-text, #ffffff);
|
|
188
|
+
--_color-underline: var(--samaritan-color-underline, #ffffff);
|
|
189
|
+
--_color-cursor: var(--samaritan-color-cursor, #cc0000);
|
|
190
|
+
--_color-cursor-off: var(--samaritan-color-cursor-off, transparent);
|
|
191
|
+
--_font-family: var(--samaritan-font-family, "Mono MMM 5", "Nova Mono", "Courier New", monospace);
|
|
192
|
+
--_letter-spacing: var(--samaritan-letter-spacing, 0.08em);
|
|
194
193
|
|
|
195
|
-
.samaritan-prompt__content {
|
|
196
194
|
display: flex;
|
|
197
195
|
flex-direction: column;
|
|
198
196
|
align-items: center;
|
|
199
197
|
row-gap: 0.6rem;
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
font-family: var(--_font-family);
|
|
199
|
+
font-size: var(--_font-size);
|
|
200
|
+
letter-spacing: var(--_letter-spacing);
|
|
202
201
|
|
|
203
|
-
.samaritan-
|
|
202
|
+
.samaritan-prompt__content {
|
|
204
203
|
display: flex;
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
flex-direction: column;
|
|
205
|
+
align-items: center;
|
|
206
|
+
row-gap: 0.6rem;
|
|
207
|
+
width: 100%;
|
|
208
|
+
transition: opacity v-bind(fadeDurationCss) ease;
|
|
209
|
+
|
|
210
|
+
.samaritan-prompt__stage {
|
|
211
|
+
display: flex;
|
|
212
|
+
justify-content: center;
|
|
213
|
+
min-height: 1.2em;
|
|
207
214
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
215
|
+
.samaritan-prompt__text {
|
|
216
|
+
color: var(--_color-text);
|
|
217
|
+
white-space: nowrap;
|
|
218
|
+
text-transform: uppercase;
|
|
219
|
+
}
|
|
212
220
|
}
|
|
213
|
-
}
|
|
214
221
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
.samaritan-prompt__underline {
|
|
223
|
+
width: 100%;
|
|
224
|
+
min-width: 4ch;
|
|
225
|
+
height: 0.15rem;
|
|
226
|
+
background: var(--_color-underline);
|
|
227
|
+
}
|
|
220
228
|
}
|
|
221
|
-
}
|
|
222
229
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
+
.samaritan-prompt__cursor {
|
|
231
|
+
color: var(--_color-cursor);
|
|
232
|
+
font-size: 2.4rem;
|
|
233
|
+
line-height: 1;
|
|
234
|
+
animation: samaritan-pulse 2.5s ease-in-out infinite;
|
|
235
|
+
transition: opacity 400ms ease;
|
|
236
|
+
}
|
|
230
237
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
.samaritan-prompt__sr-text {
|
|
239
|
+
position: absolute;
|
|
240
|
+
width: 1px;
|
|
241
|
+
height: 1px;
|
|
242
|
+
padding: 0;
|
|
243
|
+
margin: -1px;
|
|
244
|
+
overflow: hidden;
|
|
245
|
+
clip: rect(0, 0, 0, 0);
|
|
246
|
+
white-space: nowrap;
|
|
247
|
+
border: 0;
|
|
248
|
+
}
|
|
241
249
|
}
|
|
242
250
|
}
|
|
243
251
|
|
package/app/layouts/default.vue
CHANGED
|
@@ -89,6 +89,7 @@ const responsiveNavLinks = {
|
|
|
89
89
|
{ name: "Clipped Panels", path: "/ui/clipped-panels" },
|
|
90
90
|
{ name: "Display Chip", path: "/ui/display-chip" },
|
|
91
91
|
{ name: "Display Avatar", path: "/ui/display-avatar" },
|
|
92
|
+
{ name: "Display Pill", path: "/ui/display-pill" },
|
|
92
93
|
{ name: "Qr Codes", path: "/ui/qr-code/display" },
|
|
93
94
|
],
|
|
94
95
|
},
|
package/app/pages/samaritan.vue
CHANGED
|
@@ -230,11 +230,6 @@ const qaHideCursorInCycle = ref(true);
|
|
|
230
230
|
--samaritan-letter-spacing: 0.14em;
|
|
231
231
|
|
|
232
232
|
.samaritan-stage {
|
|
233
|
-
display: flex;
|
|
234
|
-
align-items: center;
|
|
235
|
-
justify-content: center;
|
|
236
|
-
min-block-size: 100dvh;
|
|
237
|
-
|
|
238
233
|
.samaritan-stage--mixed {
|
|
239
234
|
border-top: 1px solid oklch(100% 0 0 / 0.08);
|
|
240
235
|
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<NuxtLayout name="default">
|
|
4
|
+
<template #layout-content>
|
|
5
|
+
<LayoutRow tag="div" variant="popout">
|
|
6
|
+
<h1 class="page-heading-2">Display Pill</h1>
|
|
7
|
+
|
|
8
|
+
<!-- ── QA Panel (dev only) ───────────────────────────────── -->
|
|
9
|
+
<div v-if="isDev" class="qa-panel">
|
|
10
|
+
<details class="qa-panel__details" open>
|
|
11
|
+
<summary class="qa-panel__summary">
|
|
12
|
+
<span class="qa-panel__title">QA — DisplayPill</span>
|
|
13
|
+
<code class="qa-panel__status">
|
|
14
|
+
variant:{{ qaVariant }} · size:{{ qaSize }} · reversed:{{ qaReversed }}
|
|
15
|
+
</code>
|
|
16
|
+
</summary>
|
|
17
|
+
<div class="qa-panel__body">
|
|
18
|
+
|
|
19
|
+
<div class="qa-panel__group">
|
|
20
|
+
<span class="qa-panel__label">Variant</span>
|
|
21
|
+
<div class="qa-panel__chips">
|
|
22
|
+
<button
|
|
23
|
+
v-for="v in variants"
|
|
24
|
+
:key="v"
|
|
25
|
+
class="qa-panel__chip"
|
|
26
|
+
:class="{ 'is-active': qaVariant === v }"
|
|
27
|
+
@click="qaVariant = v"
|
|
28
|
+
>{{ v }}</button>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="qa-panel__group">
|
|
33
|
+
<span class="qa-panel__label">Size</span>
|
|
34
|
+
<div class="qa-panel__chips">
|
|
35
|
+
<button
|
|
36
|
+
v-for="s in sizes"
|
|
37
|
+
:key="s"
|
|
38
|
+
class="qa-panel__chip"
|
|
39
|
+
:class="{ 'is-active': qaSize === s }"
|
|
40
|
+
@click="qaSize = s"
|
|
41
|
+
>{{ s }}</button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="qa-panel__group">
|
|
46
|
+
<span class="qa-panel__label">Reversed</span>
|
|
47
|
+
<div class="qa-panel__chips">
|
|
48
|
+
<button
|
|
49
|
+
class="qa-panel__chip"
|
|
50
|
+
:class="{ 'is-active': !qaReversed }"
|
|
51
|
+
@click="qaReversed = false"
|
|
52
|
+
>icon → text</button>
|
|
53
|
+
<button
|
|
54
|
+
class="qa-panel__chip"
|
|
55
|
+
:class="{ 'is-active': qaReversed }"
|
|
56
|
+
@click="qaReversed = true"
|
|
57
|
+
>text → icon</button>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="qa-panel__group">
|
|
62
|
+
<span class="qa-panel__label">Tag</span>
|
|
63
|
+
<div class="qa-panel__chips">
|
|
64
|
+
<button
|
|
65
|
+
v-for="t in tags"
|
|
66
|
+
:key="t"
|
|
67
|
+
class="qa-panel__chip"
|
|
68
|
+
:class="{ 'is-active': qaTag === t }"
|
|
69
|
+
@click="qaTag = t"
|
|
70
|
+
><{{ t }}></button>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
</div>
|
|
75
|
+
</details>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<!-- ── Demos ─────────────────────────────────────────────── -->
|
|
79
|
+
<section>
|
|
80
|
+
<h2 class="page-heading-3">Interactive preview</h2>
|
|
81
|
+
<div class="demo-row">
|
|
82
|
+
<DisplayPill
|
|
83
|
+
:tag="qaTag"
|
|
84
|
+
label="Label text"
|
|
85
|
+
:variant="qaVariant"
|
|
86
|
+
:size="qaSize"
|
|
87
|
+
:reversed="qaReversed"
|
|
88
|
+
>
|
|
89
|
+
<template #icon>
|
|
90
|
+
<Icon name="material-symbols:star-outline-rounded" class="demo-icon" aria-hidden="true" />
|
|
91
|
+
</template>
|
|
92
|
+
</DisplayPill>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<h2 class="page-heading-3">All variants</h2>
|
|
96
|
+
<div class="dl">
|
|
97
|
+
<template v-for="v in variants" :key="v">
|
|
98
|
+
<div class="dt">{{ v }}</div>
|
|
99
|
+
<div class="dd">
|
|
100
|
+
<DisplayPill :variant="v" label="Status" :size="qaSize" :reversed="qaReversed">
|
|
101
|
+
<template #icon>
|
|
102
|
+
<Icon name="material-symbols:circle" class="demo-icon" aria-hidden="true" />
|
|
103
|
+
</template>
|
|
104
|
+
</DisplayPill>
|
|
105
|
+
</div>
|
|
106
|
+
</template>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<h2 class="page-heading-3">All sizes</h2>
|
|
110
|
+
<div class="dl">
|
|
111
|
+
<template v-for="s in sizes" :key="s">
|
|
112
|
+
<div class="dt">{{ s }}</div>
|
|
113
|
+
<div class="dd">
|
|
114
|
+
<DisplayPill :size="s" :variant="qaVariant" label="Label" :reversed="qaReversed">
|
|
115
|
+
<template #icon>
|
|
116
|
+
<Icon name="material-symbols:bolt-rounded" class="demo-icon" aria-hidden="true" />
|
|
117
|
+
</template>
|
|
118
|
+
</DisplayPill>
|
|
119
|
+
</div>
|
|
120
|
+
</template>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<h2 class="page-heading-3">Border & outline</h2>
|
|
124
|
+
<div class="dl">
|
|
125
|
+
<div class="dt">Solid border</div>
|
|
126
|
+
<div class="dd">
|
|
127
|
+
<DisplayPill
|
|
128
|
+
label="Bordered"
|
|
129
|
+
:variant="qaVariant"
|
|
130
|
+
:size="qaSize"
|
|
131
|
+
style-class-passthrough="demo-bordered"
|
|
132
|
+
>
|
|
133
|
+
<template #icon>
|
|
134
|
+
<Icon name="material-symbols:circle-outline" class="demo-icon" aria-hidden="true" />
|
|
135
|
+
</template>
|
|
136
|
+
</DisplayPill>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div class="dt">Dashed border</div>
|
|
140
|
+
<div class="dd">
|
|
141
|
+
<DisplayPill
|
|
142
|
+
label="Dashed"
|
|
143
|
+
:variant="qaVariant"
|
|
144
|
+
:size="qaSize"
|
|
145
|
+
style-class-passthrough="demo-dashed"
|
|
146
|
+
>
|
|
147
|
+
<template #icon>
|
|
148
|
+
<Icon name="material-symbols:circle-outline" class="demo-icon" aria-hidden="true" />
|
|
149
|
+
</template>
|
|
150
|
+
</DisplayPill>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div class="dt">Outline (focus-ring style)</div>
|
|
154
|
+
<div class="dd">
|
|
155
|
+
<DisplayPill
|
|
156
|
+
label="Outlined"
|
|
157
|
+
:variant="qaVariant"
|
|
158
|
+
:size="qaSize"
|
|
159
|
+
style-class-passthrough="demo-outlined"
|
|
160
|
+
>
|
|
161
|
+
<template #icon>
|
|
162
|
+
<Icon name="material-symbols:circle-outline" class="demo-icon" aria-hidden="true" />
|
|
163
|
+
</template>
|
|
164
|
+
</DisplayPill>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div class="dt">Border + outline</div>
|
|
168
|
+
<div class="dd">
|
|
169
|
+
<DisplayPill
|
|
170
|
+
label="Both"
|
|
171
|
+
:variant="qaVariant"
|
|
172
|
+
:size="qaSize"
|
|
173
|
+
style-class-passthrough="demo-border-and-outline"
|
|
174
|
+
>
|
|
175
|
+
<template #icon>
|
|
176
|
+
<Icon name="material-symbols:circle-outline" class="demo-icon" aria-hidden="true" />
|
|
177
|
+
</template>
|
|
178
|
+
</DisplayPill>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<h2 class="page-heading-3">Icon only / label only</h2>
|
|
183
|
+
<div class="dl">
|
|
184
|
+
<div class="dt">Icon + label (default)</div>
|
|
185
|
+
<div class="dd">
|
|
186
|
+
<DisplayPill :variant="qaVariant" label="With icon" :size="qaSize">
|
|
187
|
+
<template #icon>
|
|
188
|
+
<Icon name="material-symbols:check-circle-outline-rounded" class="demo-icon" aria-hidden="true" />
|
|
189
|
+
</template>
|
|
190
|
+
</DisplayPill>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div class="dt">Label only (no icon)</div>
|
|
194
|
+
<div class="dd">
|
|
195
|
+
<DisplayPill :variant="qaVariant" label="No icon" :size="qaSize" />
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div class="dt">Icon only (no label)</div>
|
|
199
|
+
<div class="dd">
|
|
200
|
+
<DisplayPill :variant="qaVariant" :size="qaSize" aria-label="Status icon">
|
|
201
|
+
<template #icon>
|
|
202
|
+
<Icon name="material-symbols:check-circle-outline-rounded" class="demo-icon" aria-hidden="true" />
|
|
203
|
+
</template>
|
|
204
|
+
</DisplayPill>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div class="dt">Default slot (custom content)</div>
|
|
208
|
+
<div class="dd">
|
|
209
|
+
<DisplayPill :variant="qaVariant" :size="qaSize">
|
|
210
|
+
<template #icon>
|
|
211
|
+
<Icon name="material-symbols:info-outline-rounded" class="demo-icon" aria-hidden="true" />
|
|
212
|
+
</template>
|
|
213
|
+
<strong>Custom</strong> slot
|
|
214
|
+
</DisplayPill>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</section>
|
|
218
|
+
</LayoutRow>
|
|
219
|
+
</template>
|
|
220
|
+
</NuxtLayout>
|
|
221
|
+
</div>
|
|
222
|
+
</template>
|
|
223
|
+
|
|
224
|
+
<script setup lang="ts">
|
|
225
|
+
definePageMeta({
|
|
226
|
+
layout: false,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
useHead({
|
|
230
|
+
title: "UI Display Pill",
|
|
231
|
+
meta: [{ name: "description", content: "Examples of UI Display Pill" }],
|
|
232
|
+
bodyAttrs: {
|
|
233
|
+
class: "ui-display-pill-page",
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const isDev = import.meta.dev;
|
|
238
|
+
|
|
239
|
+
const variants = ["default", "primary", "success", "warning", "danger", "neutral"] as const;
|
|
240
|
+
const sizes = ["sm", "md", "lg"] as const;
|
|
241
|
+
const tags = ["span", "div", "button", "a"] as const;
|
|
242
|
+
|
|
243
|
+
type Variant = (typeof variants)[number];
|
|
244
|
+
type Size = (typeof sizes)[number];
|
|
245
|
+
type Tag = (typeof tags)[number];
|
|
246
|
+
|
|
247
|
+
const qaVariant = ref<Variant>("default");
|
|
248
|
+
const qaSize = ref<Size>("md");
|
|
249
|
+
const qaReversed = ref(false);
|
|
250
|
+
const qaTag = ref<Tag>("span");
|
|
251
|
+
</script>
|
|
252
|
+
|
|
253
|
+
<style lang="css">
|
|
254
|
+
.ui-display-pill-page {
|
|
255
|
+
.qa-panel {
|
|
256
|
+
background: oklch(15% 0 0);
|
|
257
|
+
color: white;
|
|
258
|
+
font-size: 1.3rem;
|
|
259
|
+
margin-block: 2rem;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.qa-panel__details {
|
|
263
|
+
padding: 1rem 2rem;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.qa-panel__summary {
|
|
267
|
+
cursor: pointer;
|
|
268
|
+
display: flex;
|
|
269
|
+
align-items: center;
|
|
270
|
+
gap: 1.6rem;
|
|
271
|
+
list-style: none;
|
|
272
|
+
user-select: none;
|
|
273
|
+
|
|
274
|
+
&::-webkit-details-marker { display: none; }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.qa-panel__title {
|
|
278
|
+
font-weight: 600;
|
|
279
|
+
font-size: 1.1rem;
|
|
280
|
+
text-transform: uppercase;
|
|
281
|
+
letter-spacing: 0.08em;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.qa-panel__status {
|
|
285
|
+
font-family: monospace;
|
|
286
|
+
font-size: 1.2rem;
|
|
287
|
+
background: oklch(0% 0 0 / 0.3);
|
|
288
|
+
padding: 0.2rem 0.8rem;
|
|
289
|
+
border-radius: 0.4rem;
|
|
290
|
+
user-select: text;
|
|
291
|
+
cursor: text;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.qa-panel__body {
|
|
295
|
+
display: flex;
|
|
296
|
+
flex-wrap: wrap;
|
|
297
|
+
gap: 2.4rem;
|
|
298
|
+
padding-block: 1.2rem 0.4rem;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.qa-panel__group {
|
|
302
|
+
display: flex;
|
|
303
|
+
flex-direction: column;
|
|
304
|
+
gap: 0.6rem;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.qa-panel__label {
|
|
308
|
+
font-size: 1.1rem;
|
|
309
|
+
text-transform: uppercase;
|
|
310
|
+
letter-spacing: 0.08em;
|
|
311
|
+
opacity: 0.55;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.qa-panel__chips {
|
|
315
|
+
display: flex;
|
|
316
|
+
flex-wrap: wrap;
|
|
317
|
+
gap: 0.4rem;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.qa-panel__chip {
|
|
321
|
+
font-family: monospace;
|
|
322
|
+
font-size: 1.2rem;
|
|
323
|
+
color: white;
|
|
324
|
+
background: oklch(0% 0 0 / 0.25);
|
|
325
|
+
border: 1px solid oklch(100% 0 0 / 0.18);
|
|
326
|
+
padding: 0.3rem 1rem;
|
|
327
|
+
border-radius: 0.4rem;
|
|
328
|
+
cursor: pointer;
|
|
329
|
+
transition: background 0.15s;
|
|
330
|
+
|
|
331
|
+
&:hover { background: oklch(0% 0 0 / 0.4); }
|
|
332
|
+
|
|
333
|
+
&.is-active {
|
|
334
|
+
background: oklch(55% 0.18 240);
|
|
335
|
+
border-color: oklch(55% 0.18 240);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* ── Demo sections ─────────────────────────────────────────────── */
|
|
340
|
+
|
|
341
|
+
section {
|
|
342
|
+
margin-top: 2rem;
|
|
343
|
+
|
|
344
|
+
.demo-row {
|
|
345
|
+
display: flex;
|
|
346
|
+
align-items: center;
|
|
347
|
+
gap: 1.6rem;
|
|
348
|
+
flex-wrap: wrap;
|
|
349
|
+
padding-block: 2rem;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.dl {
|
|
353
|
+
display: grid;
|
|
354
|
+
grid-template-columns: auto auto;
|
|
355
|
+
gap: 1.6rem;
|
|
356
|
+
align-items: center;
|
|
357
|
+
justify-content: start;
|
|
358
|
+
margin-block: 1.6rem 2.4rem;
|
|
359
|
+
|
|
360
|
+
.dt {
|
|
361
|
+
font-weight: bold;
|
|
362
|
+
font-size: 1.3rem;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.dd {
|
|
366
|
+
margin: 0;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.demo-icon {
|
|
371
|
+
width: 1em;
|
|
372
|
+
height: 1em;
|
|
373
|
+
font-size: var(--theme-pill-icon-size, 1.4rem);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* ── Border / outline demo overrides ──────────────────────────── */
|
|
377
|
+
|
|
378
|
+
.demo-bordered.display-pill {
|
|
379
|
+
--theme-pill-border-color: currentColor;
|
|
380
|
+
--theme-pill-border-width: 1.5px;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.demo-dashed.display-pill {
|
|
384
|
+
--theme-pill-border-color: currentColor;
|
|
385
|
+
--theme-pill-border-width: 1.5px;
|
|
386
|
+
--theme-pill-border-style: dashed;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.demo-outlined.display-pill {
|
|
390
|
+
--theme-pill-outline: 2px solid currentColor;
|
|
391
|
+
--theme-pill-outline-offset: 3px;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.demo-border-and-outline.display-pill {
|
|
395
|
+
--theme-pill-border-color: currentColor;
|
|
396
|
+
--theme-pill-border-width: 1.5px;
|
|
397
|
+
--theme-pill-outline: 2px solid currentColor;
|
|
398
|
+
--theme-pill-outline-offset: 3px;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
</style>
|