srcdev-nuxt-components 9.2.1 → 9.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/.vscode/extensions.json +15 -0
  2. package/.vscode/settings.json +45 -0
  3. package/.vscode/srcdev-component-banner-video.code-snippets +59 -0
  4. package/.vscode/srcdev-component-dialog.code-snippets +126 -0
  5. package/.vscode/srcdev-component-display-prompt.code-snippets +66 -0
  6. package/.vscode/srcdev-component-display-toast.code-snippets +78 -0
  7. package/.vscode/srcdev-component-input-copy.code-snippets +72 -0
  8. package/.vscode/srcdev-component-pinia-store.code-snippets +63 -0
  9. package/.vscode/srcdev-component-pricing-card.code-snippets +96 -0
  10. package/.vscode/srcdev-component-services-section.code-snippets +84 -0
  11. package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +143 -0
  12. package/app/components/02.molecules/input-copy/InputCopy.vue +195 -0
  13. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +110 -0
  14. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +181 -0
  15. package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +149 -0
  16. package/app/components/02.molecules/pricing-card/PricingCard.vue +225 -0
  17. package/app/components/02.molecules/pricing-card/stories/PricingCard.stories.ts +160 -0
  18. package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +139 -0
  19. package/nuxt.config.ts +6 -1
  20. package/package.json +2 -1
  21. package/.claude/skills/components/input-copy-core.md +0 -66
  22. package/app/components/05.forms/input-copy/InputCopyCore.vue +0 -132
  23. package/app/components/05.forms/input-copy/stories/InputCopyCore.stories.ts +0 -89
  24. package/app/components/05.forms/input-copy/tests/InputCopyCore.spec.ts +0 -212
  25. package/app/components/05.forms/input-copy/tests/__snapshots__/InputCopyCore.spec.ts.snap +0 -28
@@ -0,0 +1,149 @@
1
+ # PricingCard — Consumer Styling Guide
2
+
3
+ ## Public token API
4
+
5
+ All `--pricing-card-*` tokens are the stable override surface. Set them at any scope (global, page,
6
+ or instance) without touching the component itself.
7
+
8
+ | Token | Default | Controls |
9
+ |---|---|---|
10
+ | `--pricing-card-border` | `1px solid var(--slate-03)` | Card border |
11
+ | `--pricing-card-border-radius` | `0.8rem` | Corner rounding |
12
+ | `--pricing-card-padding` | `2rem` | Internal padding |
13
+ | `--pricing-card-background` | `var(--slate-00)` | Card background colour |
14
+ | `--pricing-card-shadow` | `0 2px 8px oklch(...)` | Box shadow |
15
+ | `--pricing-card-gap` | `1.2rem` | Gap between elements (name, price, features, CTA) |
16
+ | `--pricing-card-highlight-border` | `2px solid var(--teal-06)` | Border when `isHighlighted` |
17
+ | `--pricing-card-highlight-shadow` | `0 8px 24px oklch(...)` | Shadow when highlighted |
18
+ | `--pricing-card-highlight-scale` | `1.05` | Scale transform when highlighted |
19
+ | `--pricing-card-badge-bg` | `var(--teal-06)` | "Most Popular" badge background |
20
+ | `--pricing-card-badge-text` | `var(--teal-00)` | Badge text colour |
21
+ | `--pricing-card-name-font-size` | `1.8rem` | Plan name heading size |
22
+ | `--pricing-card-name-color` | `#1a1a1a` | Plan name text colour |
23
+ | `--pricing-card-amount-font-size` | `3.2rem` | Price amount size |
24
+ | `--pricing-card-amount-color` | `#1a1a1a` | Price text colour |
25
+ | `--pricing-card-period-font-size` | `0.9rem` | Billing period ("one-time", "per month") size |
26
+ | `--pricing-card-period-color` | `#666` | Billing period text colour |
27
+ | `--pricing-card-description-color` | `#555` | Plan description text colour |
28
+ | `--pricing-card-feature-color` | `#333` | Feature list text colour |
29
+ | `--pricing-card-cta-bg` | `var(--theme-button-primary-surface)` | CTA button background |
30
+ | `--pricing-card-cta-bg-hover` | `var(--theme-button-primary-surface-hover)` | CTA button background on hover |
31
+ | `--pricing-card-cta-text` | `var(--theme-button-primary-text)` | CTA button text colour |
32
+ | `--pricing-card-cta-padding` | `1rem 1.6rem` | CTA button padding |
33
+ | `--pricing-card-cta-border-radius` | `0.4rem` | CTA button corner rounding |
34
+
35
+ ---
36
+
37
+ ## Global theming — app-level CSS file
38
+
39
+ Create `assets/styles/setup/07.components/pricing-card.css` in the consuming app and set tokens on
40
+ `:root`. These values apply to every `PricingCard` instance across the site.
41
+
42
+ ```css
43
+ /* assets/styles/setup/07.components/pricing-card.css */
44
+ :root {
45
+ --pricing-card-border-radius: 1rem;
46
+ --pricing-card-padding: 2.4rem;
47
+ --pricing-card-shadow: 0 4px 12px rgb(0 0 0 / 10%);
48
+ --pricing-card-gap: 1.6rem;
49
+ --pricing-card-badge-bg: var(--brand-primary);
50
+ --pricing-card-highlight-border: 2px solid var(--brand-primary);
51
+ --pricing-card-highlight-shadow: 0 12px 32px rgb(0 0 0 / 15%);
52
+ --pricing-card-cta-border-radius: 0.6rem;
53
+ }
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Per-instance overrides via inline styles
59
+
60
+ Override tokens directly on a single card instance:
61
+
62
+ ```vue
63
+ <PricingCard
64
+ planName="Enterprise"
65
+ :price="999"
66
+ style="
67
+ --pricing-card-border-radius: 1.2rem;
68
+ --pricing-card-highlight-shadow: 0 16px 48px rgb(0 0 0 / 20%);
69
+ "
70
+ @select="handleSelect"
71
+ />
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Page-scoped overrides
77
+
78
+ When pricing cards appear in a specific page context, scope overrides under the page wrapper:
79
+
80
+ ```css
81
+ /* In the consuming page's unscoped <style> block */
82
+ .pricing-page {
83
+ .pricing-card {
84
+ --pricing-card-border-radius: 1.2rem;
85
+ --pricing-card-padding: 2.8rem;
86
+ --pricing-card-gap: 1.8rem;
87
+
88
+ &.is-highlighted {
89
+ --pricing-card-highlight-scale: 1.08;
90
+ --pricing-card-highlight-shadow: 0 20px 48px rgb(0 0 0 / 18%);
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Three-card comparison grid
99
+
100
+ When displaying multiple cards side-by-side (common in pricing pages), wrap in a grid container:
101
+
102
+ ```vue
103
+ <div class="pricing-grid">
104
+ <PricingCard v-for="plan in plans" :key="plan.id" v-bind="plan" @select="handleSelect" />
105
+ </div>
106
+ ```
107
+
108
+ ```css
109
+ .pricing-grid {
110
+ display: grid;
111
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
112
+ gap: 2rem;
113
+
114
+ .pricing-card {
115
+ /* Optional per-card overrides */
116
+ }
117
+
118
+ /* Alternate: fixed 3-column layout */
119
+ &.three-col {
120
+ grid-template-columns: repeat(3, 1fr);
121
+ }
122
+ }
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Feature checkmark colour
128
+
129
+ The feature list checkmarks use `--teal-06` by default. Override globally or per-instance:
130
+
131
+ ```css
132
+ .pricing-page {
133
+ .pricing-card__feature::before {
134
+ color: var(--brand-primary);
135
+ }
136
+ }
137
+ ```
138
+
139
+ Or inline:
140
+
141
+ ```vue
142
+ <PricingCard
143
+ v-bind="plan"
144
+ style="--_feature-checkmark-color: var(--brand-accent);"
145
+ @select="handleSelect"
146
+ />
147
+ ```
148
+
149
+ (Note: `--_feature-checkmark-color` is an internal token; consider requesting it as a public token if override is needed frequently.)
@@ -0,0 +1,225 @@
1
+ <template>
2
+ <component :is="tag" class="pricing-card" :class="[elementClasses, { 'is-highlighted': isHighlighted }]">
3
+ <div v-if="isHighlighted" class="pricing-card__badge">Most Popular</div>
4
+
5
+ <h3 class="pricing-card__name">{{ planName }}</h3>
6
+
7
+ <div class="pricing-card__price">
8
+ <span class="pricing-card__amount">${{ price }}</span>
9
+ <span v-if="billingPeriod" class="pricing-card__period">{{ billingPeriod }}</span>
10
+ </div>
11
+
12
+ <p v-if="description" class="pricing-card__description">{{ description }}</p>
13
+
14
+ <ul class="pricing-card__features">
15
+ <slot name="features">
16
+ <li v-for="(feature, index) in features" :key="index" class="pricing-card__feature">
17
+ {{ feature }}
18
+ </li>
19
+ </slot>
20
+ </ul>
21
+
22
+ <div class="pricing-card__cta">
23
+ <slot
24
+ name="cta"
25
+ :cta-text="ctaText"
26
+ :is-disabled="ctaDisabled"
27
+ :plan-name="planName"
28
+ :on-select="handleSelect"
29
+ >
30
+ <InputButtonCore
31
+ :button-text="ctaText"
32
+ :readonly="ctaDisabled"
33
+ @click="handleSelect"
34
+ />
35
+ </slot>
36
+ </div>
37
+ </component>
38
+ </template>
39
+
40
+ <script setup lang="ts">
41
+ interface Props {
42
+ tag?: "div" | "section" | "article";
43
+ planName: string;
44
+ price: number;
45
+ billingPeriod?: string;
46
+ description?: string;
47
+ features?: string[];
48
+ isHighlighted?: boolean;
49
+ ctaText?: string;
50
+ ctaDisabled?: boolean;
51
+ styleClassPassthrough?: string | string[];
52
+ }
53
+
54
+ const props = withDefaults(defineProps<Props>(), {
55
+ tag: "article",
56
+ billingPeriod: "one-time",
57
+ description: undefined,
58
+ features: () => [],
59
+ isHighlighted: false,
60
+ ctaText: "Get started",
61
+ ctaDisabled: false,
62
+ styleClassPassthrough: () => [],
63
+ });
64
+
65
+ const emit = defineEmits<{
66
+ select: [planName: string];
67
+ }>();
68
+
69
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
70
+
71
+ const handleSelect = () => {
72
+ emit("select", props.planName);
73
+ };
74
+
75
+ watch(
76
+ () => props.styleClassPassthrough,
77
+ () => {
78
+ resetElementClasses(props.styleClassPassthrough);
79
+ },
80
+ );
81
+ </script>
82
+
83
+ <style lang="css">
84
+ @layer components {
85
+ .pricing-card {
86
+ --_card-border: var(--pricing-card-border, 1px solid var(--slate-03));
87
+ --_card-border-radius: var(--pricing-card-border-radius, 0.8rem);
88
+ --_card-padding: var(--pricing-card-padding, 2rem);
89
+ --_card-background: var(--pricing-card-background, var(--slate-00));
90
+ --_card-shadow: var(--pricing-card-shadow, 0 2px 8px oklch(from var(--slate-08) l c h / 0.12));
91
+ --_card-gap: var(--pricing-card-gap, 1.2rem);
92
+
93
+ --_highlight-border: var(--pricing-card-highlight-border, 2px solid var(--teal-06));
94
+ --_highlight-shadow: var(--pricing-card-highlight-shadow, 0 8px 24px oklch(from var(--teal-06) l c h / 0.20));
95
+ --_highlight-scale: var(--pricing-card-highlight-scale, 1.05);
96
+
97
+ --_badge-bg: var(--pricing-card-badge-bg, var(--teal-06));
98
+ --_badge-text: var(--pricing-card-badge-text, var(--teal-00));
99
+
100
+ --_name-font-size: var(--pricing-card-name-font-size, 1.8rem);
101
+ --_name-color: var(--pricing-card-name-color, #1a1a1a);
102
+
103
+ --_amount-font-size: var(--pricing-card-amount-font-size, 3.2rem);
104
+ --_amount-color: var(--pricing-card-amount-color, #1a1a1a);
105
+ --_period-font-size: var(--pricing-card-period-font-size, 0.9rem);
106
+ --_period-color: var(--pricing-card-period-color, #666);
107
+
108
+ --_description-color: var(--pricing-card-description-color, #555);
109
+ --_feature-color: var(--pricing-card-feature-color, #333);
110
+
111
+ --_cta-bg: var(--pricing-card-cta-bg, var(--theme-button-primary-surface));
112
+ --_cta-bg-hover: var(--pricing-card-cta-bg-hover, var(--theme-button-primary-surface-hover));
113
+ --_cta-text: var(--pricing-card-cta-text, var(--theme-button-primary-text));
114
+ --_cta-padding: var(--pricing-card-cta-padding, 1rem 1.6rem);
115
+ --_cta-border-radius: var(--pricing-card-cta-border-radius, 0.4rem);
116
+
117
+ display: flex;
118
+ flex-direction: column;
119
+ gap: var(--_card-gap);
120
+ position: relative;
121
+
122
+ padding: var(--_card-padding);
123
+ background-color: var(--_card-background);
124
+ border: var(--_card-border);
125
+ border-radius: var(--_card-border-radius);
126
+ box-shadow: var(--_card-shadow);
127
+
128
+ transition: all 0.3s ease;
129
+
130
+ &.is-highlighted {
131
+ border: var(--_highlight-border);
132
+ box-shadow: var(--_highlight-shadow);
133
+ transform: scale(var(--_highlight-scale));
134
+ }
135
+
136
+ .pricing-card__badge {
137
+ position: absolute;
138
+ top: -0.6rem;
139
+ left: 50%;
140
+ transform: translateX(-50%);
141
+
142
+ display: inline-block;
143
+ padding: 0.4rem 1rem;
144
+ background-color: var(--_badge-bg);
145
+ color: var(--_badge-text);
146
+ border-radius: 2rem;
147
+ font-size: 0.75rem;
148
+ font-weight: 600;
149
+ text-transform: uppercase;
150
+ letter-spacing: 0.05em;
151
+ }
152
+
153
+ .pricing-card__name {
154
+ margin: 0;
155
+ font-size: var(--_name-font-size);
156
+ font-weight: 600;
157
+ color: var(--_name-color);
158
+ }
159
+
160
+ .pricing-card__price {
161
+ display: flex;
162
+ align-items: baseline;
163
+ gap: 0.4rem;
164
+ }
165
+
166
+ .pricing-card__amount {
167
+ font-size: var(--_amount-font-size);
168
+ font-weight: 700;
169
+ color: var(--_amount-color);
170
+ }
171
+
172
+ .pricing-card__period {
173
+ font-size: var(--_period-font-size);
174
+ color: var(--_period-color);
175
+ }
176
+
177
+ .pricing-card__description {
178
+ margin: 0;
179
+ color: var(--_description-color);
180
+ line-height: 1.5;
181
+ }
182
+
183
+ .pricing-card__features {
184
+ list-style: none;
185
+ padding: 0;
186
+ margin: 0;
187
+ display: flex;
188
+ flex-direction: column;
189
+ gap: 0.6rem;
190
+
191
+ flex-grow: 1;
192
+ }
193
+
194
+ .pricing-card__feature {
195
+ color: var(--_feature-color);
196
+ padding-left: 1.6rem;
197
+ position: relative;
198
+
199
+ &::before {
200
+ content: "✓";
201
+ position: absolute;
202
+ left: 0;
203
+ color: var(--teal-06);
204
+ font-weight: bold;
205
+ }
206
+ }
207
+
208
+ .pricing-card__cta {
209
+ align-self: flex-start;
210
+
211
+ :deep(.input-button-core) {
212
+ padding: var(--_cta-padding);
213
+ background-color: var(--_cta-bg);
214
+ color: var(--_cta-text);
215
+ border-radius: var(--_cta-border-radius);
216
+ font-weight: 600;
217
+
218
+ &:hover:not([aria-disabled="true"]) {
219
+ background-color: var(--_cta-bg-hover);
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
225
+ </style>
@@ -0,0 +1,160 @@
1
+ import type { Meta, StoryObj } from "@nuxtjs/storybook";
2
+ import PricingCard from "../PricingCard.vue";
3
+
4
+ const meta: Meta<typeof PricingCard> = {
5
+ title: "Molecules/Pricing Card",
6
+ component: PricingCard,
7
+ tags: ["autodocs"],
8
+ argTypes: {
9
+ planName: { control: "text" },
10
+ price: { control: "number" },
11
+ billingPeriod: { control: "text" },
12
+ description: { control: "text" },
13
+ isHighlighted: { control: "boolean" },
14
+ ctaText: { control: "text" },
15
+ ctaDisabled: { control: "boolean" },
16
+ },
17
+ args: {
18
+ planName: "Single Site",
19
+ price: 99,
20
+ billingPeriod: "one-time",
21
+ description: "Perfect for freelancers and solo practitioners.",
22
+ features: ["Embed on one domain", "Unlimited consultations", "Email support", "Monthly updates"],
23
+ isHighlighted: false,
24
+ ctaText: "Get started",
25
+ ctaDisabled: false,
26
+ },
27
+ parameters: {
28
+ docs: {
29
+ description: {
30
+ component:
31
+ "A SaaS pricing plan card displaying plan name, price, features, and a CTA button. " +
32
+ "Supports a 'Most Popular' highlight state with scaled/emphasized styling. " +
33
+ "Emits a `select` event when the CTA button is clicked. " +
34
+ "Use multiple cards in a grid layout to build a pricing comparison table.",
35
+ },
36
+ },
37
+ },
38
+ };
39
+
40
+ export default meta;
41
+ type Story = StoryObj<typeof meta>;
42
+
43
+ const storyWrapper = (args: Record<string, unknown>, handleSelect: (planName: string) => void) => ({
44
+ components: { PricingCard },
45
+ setup() {
46
+ return { args, handleSelect };
47
+ },
48
+ template: `
49
+ <div style="padding: 2rem; background: #f5f5f5; border-radius: 0.5rem;">
50
+ <PricingCard v-bind="args" @select="handleSelect" />
51
+ </div>
52
+ `,
53
+ });
54
+
55
+ export const Default: Story = {
56
+ render: (args) => {
57
+ const handleSelect = (planName: string) => {
58
+ console.log(`Selected plan: ${planName}`);
59
+ };
60
+ return storyWrapper(args, handleSelect);
61
+ },
62
+ };
63
+
64
+ export const Highlighted: Story = {
65
+ args: {
66
+ planName: "Agency",
67
+ price: 299,
68
+ description: "Best for growing teams managing multiple sites.",
69
+ features: ["Embed on up to 3 domains", "Unlimited consultations", "Priority email support", "Weekly updates"],
70
+ isHighlighted: true,
71
+ },
72
+ render: (args) => {
73
+ const handleSelect = (planName: string) => {
74
+ console.log(`Selected plan: ${planName}`);
75
+ };
76
+ return storyWrapper(args, handleSelect);
77
+ },
78
+ };
79
+
80
+ export const Unlimited: Story = {
81
+ args: {
82
+ planName: "Unlimited",
83
+ price: 799,
84
+ description: "Enterprise plan for unlimited growth.",
85
+ features: [
86
+ "Unlimited domains",
87
+ "Unlimited consultations",
88
+ "24/7 priority support",
89
+ "Real-time updates",
90
+ "Custom integrations",
91
+ ],
92
+ isHighlighted: false,
93
+ },
94
+ render: (args) => {
95
+ const handleSelect = (planName: string) => {
96
+ console.log(`Selected plan: ${planName}`);
97
+ };
98
+ return storyWrapper(args, handleSelect);
99
+ },
100
+ };
101
+
102
+ export const Disabled: Story = {
103
+ args: {
104
+ planName: "Coming Soon",
105
+ price: 199,
106
+ description: "New plan launching soon.",
107
+ features: ["Feature 1", "Feature 2", "Feature 3"],
108
+ ctaDisabled: true,
109
+ ctaText: "Coming soon",
110
+ },
111
+ render: (args) => {
112
+ const handleSelect = (planName: string) => {
113
+ console.log(`Selected plan: ${planName}`);
114
+ };
115
+ return storyWrapper(args, handleSelect);
116
+ },
117
+ };
118
+
119
+ export const ThreeCardComparison: Story = {
120
+ render: () => ({
121
+ components: { PricingCard },
122
+ setup() {
123
+ const handleSelect = (planName: string) => {
124
+ console.log(`Selected plan: ${planName}`);
125
+ };
126
+ return { handleSelect };
127
+ },
128
+ template: `
129
+ <div style="padding: 2rem; background: #f5f5f5; border-radius: 0.5rem;">
130
+ <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem;">
131
+ <PricingCard
132
+ planName="Single Site"
133
+ :price="99"
134
+ billingPeriod="one-time"
135
+ description="Perfect for freelancers and solo practitioners."
136
+ :features="['Embed on one domain', 'Unlimited consultations', 'Email support', 'Monthly updates']"
137
+ @select="handleSelect"
138
+ />
139
+ <PricingCard
140
+ planName="Agency"
141
+ :price="299"
142
+ billingPeriod="one-time"
143
+ description="Best for growing teams managing multiple sites."
144
+ :features="['Embed on up to 3 domains', 'Unlimited consultations', 'Priority email support', 'Weekly updates']"
145
+ :isHighlighted="true"
146
+ @select="handleSelect"
147
+ />
148
+ <PricingCard
149
+ planName="Unlimited"
150
+ :price="799"
151
+ billingPeriod="one-time"
152
+ description="Enterprise plan for unlimited growth."
153
+ :features="['Unlimited domains', 'Unlimited consultations', '24/7 priority support', 'Real-time updates', 'Custom integrations']"
154
+ @select="handleSelect"
155
+ />
156
+ </div>
157
+ </div>
158
+ `,
159
+ }),
160
+ };
@@ -0,0 +1,139 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { mount } from "@vue/test-utils";
3
+ import PricingCard from "../PricingCard.vue";
4
+
5
+ describe("PricingCard", () => {
6
+ it("renders plan name, price, and CTA", () => {
7
+ const wrapper = mount(PricingCard, {
8
+ props: {
9
+ planName: "Single Site",
10
+ price: 99,
11
+ billingPeriod: "one-time",
12
+ ctaText: "Get started",
13
+ },
14
+ });
15
+
16
+ expect(wrapper.text()).toContain("Single Site");
17
+ expect(wrapper.text()).toContain("$99");
18
+ expect(wrapper.text()).toContain("one-time");
19
+ expect(wrapper.text()).toContain("Get started");
20
+ });
21
+
22
+ it("renders features from props", () => {
23
+ const features = ["Feature 1", "Feature 2", "Feature 3"];
24
+ const wrapper = mount(PricingCard, {
25
+ props: {
26
+ planName: "Test Plan",
27
+ price: 100,
28
+ features,
29
+ },
30
+ });
31
+
32
+ features.forEach((feature) => {
33
+ expect(wrapper.text()).toContain(feature);
34
+ });
35
+ });
36
+
37
+ it("renders description when provided", () => {
38
+ const description = "This is a great plan";
39
+ const wrapper = mount(PricingCard, {
40
+ props: {
41
+ planName: "Test Plan",
42
+ price: 100,
43
+ description,
44
+ },
45
+ });
46
+
47
+ expect(wrapper.text()).toContain(description);
48
+ });
49
+
50
+ it("applies highlighted class when isHighlighted is true", () => {
51
+ const wrapper = mount(PricingCard, {
52
+ props: {
53
+ planName: "Test Plan",
54
+ price: 100,
55
+ isHighlighted: true,
56
+ },
57
+ });
58
+
59
+ expect(wrapper.classes()).toContain("is-highlighted");
60
+ });
61
+
62
+ it("displays badge when highlighted", () => {
63
+ const wrapper = mount(PricingCard, {
64
+ props: {
65
+ planName: "Test Plan",
66
+ price: 100,
67
+ isHighlighted: true,
68
+ },
69
+ });
70
+
71
+ expect(wrapper.text()).toContain("Most Popular");
72
+ });
73
+
74
+ it("emits select event with plan name on CTA click", async () => {
75
+ const wrapper = mount(PricingCard, {
76
+ props: {
77
+ planName: "Test Plan",
78
+ price: 100,
79
+ },
80
+ });
81
+
82
+ const button = wrapper.find("button");
83
+ await button.trigger("click");
84
+
85
+ expect(wrapper.emitted("select")).toBeTruthy();
86
+ expect(wrapper.emitted("select")?.[0]).toEqual(["Test Plan"]);
87
+ });
88
+
89
+ it("disables CTA when ctaDisabled is true", () => {
90
+ const wrapper = mount(PricingCard, {
91
+ props: {
92
+ planName: "Test Plan",
93
+ price: 100,
94
+ ctaDisabled: true,
95
+ },
96
+ });
97
+
98
+ const button = wrapper.find("button");
99
+ expect(button.attributes("disabled")).toBeDefined();
100
+ });
101
+
102
+ it("renders custom features via slot", () => {
103
+ const wrapper = mount(PricingCard, {
104
+ props: {
105
+ planName: "Test Plan",
106
+ price: 100,
107
+ },
108
+ slots: {
109
+ features: '<li>Custom Feature 1</li><li>Custom Feature 2</li>',
110
+ },
111
+ });
112
+
113
+ expect(wrapper.text()).toContain("Custom Feature 1");
114
+ expect(wrapper.text()).toContain("Custom Feature 2");
115
+ });
116
+
117
+ it("uses semantic article tag by default", () => {
118
+ const wrapper = mount(PricingCard, {
119
+ props: {
120
+ planName: "Test Plan",
121
+ price: 100,
122
+ },
123
+ });
124
+
125
+ expect(wrapper.element.tagName).toBe("ARTICLE");
126
+ });
127
+
128
+ it("allows custom tag via prop", () => {
129
+ const wrapper = mount(PricingCard, {
130
+ props: {
131
+ planName: "Test Plan",
132
+ price: 100,
133
+ tag: "section",
134
+ },
135
+ });
136
+
137
+ expect(wrapper.element.tagName).toBe("SECTION");
138
+ });
139
+ });
package/nuxt.config.ts CHANGED
@@ -123,7 +123,12 @@ export default defineNuxtConfig({
123
123
  },
124
124
  vite: {
125
125
  optimizeDeps: {
126
- include: ["@oddbird/css-anchor-positioning", "@vue/devtools-core", "@vue/devtools-kit", "vue-qrcode-reader"],
126
+ include: [
127
+ "@oddbird/css-anchor-positioning",
128
+ "@vue/devtools-core",
129
+ "@vue/devtools-kit",
130
+ ...(isStandalone ? ["vue-qrcode-reader"] : []),
131
+ ],
127
132
  },
128
133
  },
129
134
  vue: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.2.1",
4
+ "version": "9.2.3",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "files": [
34
34
  ".claude/",
35
+ ".vscode/",
35
36
  "app/",
36
37
  "modules/",
37
38
  "scripts/",