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.
@@ -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.
@@ -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
  ```
@@ -2,7 +2,7 @@ import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
2
  import StorybookComponent from "../DisplayAvatar.vue";
3
3
 
4
4
  export default {
5
- title: "Components/UI/DisplayAvatar",
5
+ title: "Atoms/DisplayAvatar",
6
6
  component: StorybookComponent,
7
7
  argTypes: {
8
8
  size: {
@@ -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>"`;
@@ -20,7 +20,7 @@ interface ChipStoryArgs {
20
20
  }
21
21
 
22
22
  export default {
23
- title: "Components/UI/DisplayChip",
23
+ title: "Molecules/DisplayChip",
24
24
  component: StorybookComponent,
25
25
  argTypes: {
26
26
  // Basic Configuration
@@ -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: swap;
175
+ font-display: optional;
176
176
  }
177
177
 
178
- .samaritan-prompt {
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
- row-gap: 0.6rem;
191
- font-family: var(--_font-family);
192
- font-size: var(--_font-size);
193
- letter-spacing: var(--_letter-spacing);
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
- width: 100%;
201
- transition: opacity v-bind(fadeDurationCss) ease;
198
+ font-family: var(--_font-family);
199
+ font-size: var(--_font-size);
200
+ letter-spacing: var(--_letter-spacing);
202
201
 
203
- .samaritan-prompt__stage {
202
+ .samaritan-prompt__content {
204
203
  display: flex;
205
- justify-content: center;
206
- min-height: 1.2em;
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
- .samaritan-prompt__text {
209
- color: var(--_color-text);
210
- white-space: nowrap;
211
- text-transform: uppercase;
215
+ .samaritan-prompt__text {
216
+ color: var(--_color-text);
217
+ white-space: nowrap;
218
+ text-transform: uppercase;
219
+ }
212
220
  }
213
- }
214
221
 
215
- .samaritan-prompt__underline {
216
- width: 100%;
217
- min-width: 4ch;
218
- height: 0.15rem;
219
- background: var(--_color-underline);
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
- .samaritan-prompt__cursor {
224
- color: var(--_color-cursor);
225
- font-size: 2.4rem;
226
- line-height: 1;
227
- animation: samaritan-pulse 2.5s ease-in-out infinite;
228
- transition: opacity 400ms ease;
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
- .samaritan-prompt__sr-text {
232
- position: absolute;
233
- width: 1px;
234
- height: 1px;
235
- padding: 0;
236
- margin: -1px;
237
- overflow: hidden;
238
- clip: rect(0, 0, 0, 0);
239
- white-space: nowrap;
240
- border: 0;
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
 
@@ -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
  },
@@ -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
+ >&lt;{{ t }}&gt;</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 &amp; 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>&nbsp;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>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.1.44",
4
+ "version": "9.1.46",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",