srcdev-nuxt-components 9.1.36 → 9.1.37
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/settings.json +2 -1
- package/.claude/settings.local.json +2 -2
- package/.claude/skills/components/capture-qr-code.md +84 -0
- package/.claude/skills/components/decode-qr-code.md +84 -0
- package/.claude/skills/components/display-qr-code.md +64 -0
- package/.claude/skills/index.md +4 -2
- package/app/components/01.atoms/qr-code/DisplayQrCode.vue +50 -0
- package/app/components/01.atoms/qr-code/stories/DisplayQrCode.stories.ts +206 -0
- package/app/components/01.atoms/qr-code/tests/DisplayQrCode.spec.ts +139 -0
- package/app/components/{qr-code → 02.molecules/qr-code}/CaptureQrCode.vue +18 -59
- package/app/components/{qr-code → 02.molecules/qr-code}/DecodeQrCode.vue +11 -35
- package/app/components/02.molecules/qr-code/stories/QrCode.stories.ts +101 -0
- package/app/components/02.molecules/qr-code/tests/CaptureQrCode.spec.ts +212 -0
- package/app/components/02.molecules/qr-code/tests/DecodeQrCode.spec.ts +145 -0
- package/app/layouts/default.vue +0 -1
- package/app/pages/ui/qr-code/[componentName].vue +3 -3
- package/package.json +1 -1
- package/.claude/skills/components/scroll-parallax-section.md +0 -148
- package/app/components/01.atoms/scroll-parallax-section/ScrollParallaxSection.vue +0 -108
- package/app/components/01.atoms/scroll-parallax-section/stories/ScrollParallaxSection.stories.ts +0 -151
- package/app/components/01.atoms/scroll-parallax-section/tests/ScrollParallaxSection.spec.ts +0 -91
- package/app/components/qr-code/DisplayQrCode.vue +0 -53
- package/app/components/qr-code/stories/QrCode.stories.ts +0 -933
- package/app/pages/ui/scroll-parallax-section.vue +0 -65
package/.claude/settings.json
CHANGED
|
@@ -29,13 +29,13 @@
|
|
|
29
29
|
"additionalDirectories": [
|
|
30
30
|
"/Users/simoncornforth/websites/nuxt-components/app/components/01.atoms/content-wrappers/content-width",
|
|
31
31
|
"/Users/simoncornforth/websites/nuxt-components/.claude/skills",
|
|
32
|
+
"/Users/simoncornforth/websites/nuxt-components/.claude/skills/components",
|
|
32
33
|
"/Users/simoncornforth/websites/nuxt-components/app/components/02.molecules/navigation/site-navigation/tests",
|
|
33
34
|
"/Users/simoncornforth/websites/luxury-locs-by-natasha-nuxt3/app/pages",
|
|
34
35
|
"/Users/simoncornforth/websites/nuxt-components/app/assets/styles/extends-layer",
|
|
35
36
|
"/Users/simoncornforth/websites/nuxt-components/app/components/01.atoms",
|
|
36
37
|
"/private/tmp",
|
|
37
|
-
"/Users/simoncornforth/websites/nuxt-components/app/components/01.atoms/banner-video"
|
|
38
|
-
"/Users/simoncornforth/websites/nuxt-components/app/components/01.atoms/scroll-parallax-section"
|
|
38
|
+
"/Users/simoncornforth/websites/nuxt-components/app/components/01.atoms/banner-video"
|
|
39
39
|
]
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# CaptureQrCode
|
|
2
|
+
|
|
3
|
+
Live camera QR code scanner. Streams the device camera, detects QR codes in real time, and displays the decoded results inline. Handles camera errors, page visibility changes, and route navigation — stopping media streams cleanly in all cases.
|
|
4
|
+
|
|
5
|
+
**File**: `app/components/02.molecules/qr-code/CaptureQrCode.vue`
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
The `nuxt-qrcode` Nuxt module must be registered in the consuming app's `nuxt.config.ts`:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// nuxt.config.ts
|
|
13
|
+
export default defineNuxtConfig({
|
|
14
|
+
modules: ["nuxt-qrcode"],
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Camera access requires a secure context (HTTPS or localhost) and the user granting camera permission.
|
|
19
|
+
|
|
20
|
+
## Props
|
|
21
|
+
|
|
22
|
+
| Prop | Type | Default | Description |
|
|
23
|
+
|------|------|---------|-------------|
|
|
24
|
+
| `style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the root element |
|
|
25
|
+
|
|
26
|
+
## CSS classes
|
|
27
|
+
|
|
28
|
+
| Class | Where | Description |
|
|
29
|
+
|-------|-------|-------------|
|
|
30
|
+
| `.capture-qr-stream` | root | always present |
|
|
31
|
+
| `.camera-stopped` | inner | shown when camera is off but no error |
|
|
32
|
+
| `.camera-error` | inner | shown when an error has occurred |
|
|
33
|
+
| `.scanned-results` | inner | shown when at least one code has been decoded |
|
|
34
|
+
|
|
35
|
+
## Behaviour
|
|
36
|
+
|
|
37
|
+
- Camera starts automatically on mount (`state.cameraOn = true`)
|
|
38
|
+
- When the page becomes hidden (tab switch, minimize), the camera stops and media streams are released
|
|
39
|
+
- On route leave (`onBeforeRouteLeave`), the camera stops and streams are released
|
|
40
|
+
- On `<KeepAlive>` deactivate/activate, camera is stopped/restarted accordingly
|
|
41
|
+
- If the user denies camera permission or another error occurs, the `.camera-error` state is shown with the error message and a **Reset camera** button
|
|
42
|
+
- Detected QR codes replace the previous results — each scan is a fresh result set
|
|
43
|
+
- Results are displayed as a `<ul>` inside `.scanned-results`
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
Drop in where camera scanning is needed. No configuration required beyond the module being present:
|
|
48
|
+
|
|
49
|
+
```vue
|
|
50
|
+
<CaptureQrCode />
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
With styling:
|
|
54
|
+
|
|
55
|
+
```vue
|
|
56
|
+
<CaptureQrCode style-class-passthrough="my-scanner" />
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## CSS override
|
|
60
|
+
|
|
61
|
+
The root element is a square (`aspect-ratio: 1 / 1`) by default. Override in a consuming page or component:
|
|
62
|
+
|
|
63
|
+
```css
|
|
64
|
+
.my-scanner.capture-qr-stream {
|
|
65
|
+
aspect-ratio: auto;
|
|
66
|
+
max-width: 40rem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.my-scanner .camera-error {
|
|
70
|
+
padding: 1.6rem;
|
|
71
|
+
border-radius: 0.8rem;
|
|
72
|
+
background-color: var(--theme-surface-error);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.my-scanner .scanned-results {
|
|
76
|
+
margin-block-start: 1.6rem;
|
|
77
|
+
font-size: 1.4rem;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Notes
|
|
82
|
+
|
|
83
|
+
- The component does not emit the detected values — results are rendered inline. To act on scan results in a parent, extend the component or use `nuxt-qrcode`'s `QrcodeStream` directly with `@detect`.
|
|
84
|
+
- Media streams are stopped via two strategies: via the `QrcodeStream` component ref and via a document-level `querySelectorAll("video")` sweep — the sweep acts as a safety net if the ref is unavailable.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# DecodeQrCode
|
|
2
|
+
|
|
3
|
+
QR code decoder for static images. Accepts images via file picker or drag-and-drop, decodes any QR codes found, and displays the results inline.
|
|
4
|
+
|
|
5
|
+
**File**: `app/components/02.molecules/qr-code/DecodeQrCode.vue`
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
The `nuxt-qrcode` Nuxt module must be registered in the consuming app's `nuxt.config.ts`:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// nuxt.config.ts
|
|
13
|
+
export default defineNuxtConfig({
|
|
14
|
+
modules: ["nuxt-qrcode"],
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
No camera permission is required — this component works entirely with uploaded or dropped image files.
|
|
19
|
+
|
|
20
|
+
## Props
|
|
21
|
+
|
|
22
|
+
| Prop | Type | Default | Description |
|
|
23
|
+
|------|------|---------|-------------|
|
|
24
|
+
| `style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the root element |
|
|
25
|
+
|
|
26
|
+
## CSS classes
|
|
27
|
+
|
|
28
|
+
| Class | Where | Description |
|
|
29
|
+
|-------|-------|-------------|
|
|
30
|
+
| `.decode-qr-code` | root | always present |
|
|
31
|
+
| `.qr-code-capture` | file input | the `QrcodeCapture` file-picker element |
|
|
32
|
+
| `.qr-code-dropzone` | drop zone | the `QrcodeDropZone` drag-and-drop area |
|
|
33
|
+
| `.scanned-results` | inner | shown when at least one code has been decoded |
|
|
34
|
+
|
|
35
|
+
## Behaviour
|
|
36
|
+
|
|
37
|
+
- Two input methods are rendered side by side: a file picker (`.qr-code-capture`) and a drag-and-drop zone (`.qr-code-dropzone`)
|
|
38
|
+
- Both share the same `onDetect` handler — results are displayed in the same `.scanned-results` list regardless of input method
|
|
39
|
+
- Detected QR codes replace the previous results — each decode is a fresh result set
|
|
40
|
+
- If the decoded array is empty, `.scanned-results` is hidden
|
|
41
|
+
- The drop zone has a dashed border and minimum height by default — style with `.qr-code-dropzone` to customise
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
Drop in where image-based QR decoding is needed:
|
|
46
|
+
|
|
47
|
+
```vue
|
|
48
|
+
<DecodeQrCode />
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
With styling:
|
|
52
|
+
|
|
53
|
+
```vue
|
|
54
|
+
<DecodeQrCode style-class-passthrough="my-decoder" />
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## CSS override
|
|
58
|
+
|
|
59
|
+
```css
|
|
60
|
+
.my-decoder.decode-qr-code {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: 1.6rem;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.my-decoder .qr-code-dropzone {
|
|
67
|
+
min-height: 12rem;
|
|
68
|
+
border-radius: 1rem;
|
|
69
|
+
border-color: var(--theme-input-border);
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: center;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.my-decoder .scanned-results {
|
|
76
|
+
margin-block-start: 1.6rem;
|
|
77
|
+
font-size: 1.4rem;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Notes
|
|
82
|
+
|
|
83
|
+
- Like `CaptureQrCode`, results are rendered inline and not emitted. To act on results in a parent, extend the component or use `nuxt-qrcode`'s `QrcodeCapture` / `QrcodeDropZone` primitives directly.
|
|
84
|
+
- The component renders both input methods unconditionally. If only one is needed, use the underlying `QrcodeCapture` or `QrcodeDropZone` primitives directly.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# DisplayQrCode
|
|
2
|
+
|
|
3
|
+
Renders a styled QR code SVG from a string value. Supports colour, size, corner radius, and pixel/marker shape variants.
|
|
4
|
+
|
|
5
|
+
**File**: `app/components/01.atoms/qr-code/DisplayQrCode.vue`
|
|
6
|
+
|
|
7
|
+
## Props
|
|
8
|
+
|
|
9
|
+
| Prop | Type | Default | Description |
|
|
10
|
+
|------|------|---------|-------------|
|
|
11
|
+
| `qr-value` | `string` | required | The string to encode into the QR code |
|
|
12
|
+
| `variant` | `QrCodeVariant` | `{ inner: "default", marker: "default", pixel: "default" }` | Shape variants for inner markers, outer markers, and pixels |
|
|
13
|
+
| `radius` | `number` | `0` | Corner rounding applied to pixels/markers (0–1 scale) |
|
|
14
|
+
| `black-color` | `string` | `"currentColor"` | Colour for the dark modules |
|
|
15
|
+
| `white-color` | `string` | `"transparent"` | Colour for the light modules |
|
|
16
|
+
| `size` | `string` | `"256px"` | Width of the QR code (height matches via `aspect-ratio: 1 / 1`) |
|
|
17
|
+
| `style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the root element |
|
|
18
|
+
|
|
19
|
+
### QrCodeVariant type
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import type { QrCodeVariant } from "srcdev-nuxt-components/types/components";
|
|
23
|
+
|
|
24
|
+
// Each field accepts: "default" | "circle" | "rounded" | "dots" | "classy" | "classy-rounded"
|
|
25
|
+
const variant: QrCodeVariant = {
|
|
26
|
+
inner: "circle",
|
|
27
|
+
marker: "rounded",
|
|
28
|
+
pixel: "dots",
|
|
29
|
+
};
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## CSS classes
|
|
33
|
+
|
|
34
|
+
| Class | Where | Description |
|
|
35
|
+
|-------|-------|-------------|
|
|
36
|
+
| `.display-qr-code` | root | always present |
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
Minimal — encodes a URL with default styling:
|
|
41
|
+
|
|
42
|
+
```vue
|
|
43
|
+
<DisplayQrCode qr-value="https://example.com" />
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Custom size, colour, and rounded pixels:
|
|
47
|
+
|
|
48
|
+
```vue
|
|
49
|
+
<DisplayQrCode
|
|
50
|
+
qr-value="https://example.com"
|
|
51
|
+
size="320px"
|
|
52
|
+
black-color="#1a1a2e"
|
|
53
|
+
white-color="#f5f5f5"
|
|
54
|
+
:radius="0.5"
|
|
55
|
+
:variant="{ inner: 'circle', marker: 'rounded', pixel: 'default' }"
|
|
56
|
+
/>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Notes
|
|
60
|
+
|
|
61
|
+
- The component renders directly as the `Qrcode` element from `nuxt-qrcode` — there is no wrapper `<div>`. The `.display-qr-code` class is applied to the `Qrcode` root, which renders as an `<svg>`.
|
|
62
|
+
- `size` is applied via CSS `v-bind()` — pass any valid CSS length value (`px`, `rem`, `%`, etc.).
|
|
63
|
+
- `black-color` defaults to `currentColor`, so the QR code inherits the surrounding text colour automatically unless overridden.
|
|
64
|
+
- `white-color` defaults to `transparent`, making the background of the QR code see-through by default.
|
package/.claude/skills/index.md
CHANGED
|
@@ -68,11 +68,13 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
68
68
|
├── input-copy-core.md — InputCopyCore: readonly copy-to-clipboard input; props, emits, slots, CSS classes, usage
|
|
69
69
|
├── banner-video.md — BannerVideo: full-width hero video banner, depth tier system, objectFit/objectPosition, reduced-motion fallback, CSS tokens
|
|
70
70
|
├── grid-stack.md — GridStack: CSS Grid z-axis stacking, slot API, z-order rules, sizing, video+overlay and image+text patterns
|
|
71
|
-
├── scroll-parallax-section.md — ScrollParallaxSection: JS-driven parallax background section, parallaxStrength guide, height token, reduced-motion pattern
|
|
72
71
|
├── scroll-reveal-frame.md — ScrollRevealFrame: generic parallax clipping frame, slot API, image grid pattern, CSS tokens, browser support
|
|
73
72
|
├── scroll-reveal-image.md — ScrollRevealImage: single-image parallax reveal, focalX, imgWidth/imgHeight, responsive frame height
|
|
74
73
|
├── site-navigation.md — SiteNavigation: responsive nav with auto-collapse, burger menu, decorator indicators, CSS token API
|
|
75
|
-
|
|
74
|
+
├── social-icons-list.md — SocialIconsList: data-driven social icon links, ISocialIcon type, logos: icon names, CSS tokens
|
|
75
|
+
├── display-qr-code.md — DisplayQrCode: QR code SVG from a string value, colour/size/variant/radius props, currentColor default
|
|
76
|
+
├── capture-qr-code.md — CaptureQrCode: live camera scanner, error state, visibility/route/KeepAlive lifecycle, media stream cleanup
|
|
77
|
+
└── decode-qr-code.md — DecodeQrCode: file picker + drag-and-drop image decoder, shared results list, CSS override points
|
|
76
78
|
```
|
|
77
79
|
|
|
78
80
|
## Skill file template
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Qrcode
|
|
3
|
+
:value="qrValue"
|
|
4
|
+
class="display-qr-code"
|
|
5
|
+
:variant
|
|
6
|
+
:radius
|
|
7
|
+
:black-color
|
|
8
|
+
:white-color
|
|
9
|
+
:class="[elementClasses]"
|
|
10
|
+
/>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
import type { QrCodeVariant } from "~/types/components";
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
qrValue: string;
|
|
18
|
+
variant?: QrCodeVariant;
|
|
19
|
+
radius?: number;
|
|
20
|
+
blackColor?: string;
|
|
21
|
+
whiteColor?: string;
|
|
22
|
+
size?: string;
|
|
23
|
+
styleClassPassthrough?: string | string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
27
|
+
variant: () => ({ inner: "default", marker: "default", pixel: "default" }),
|
|
28
|
+
radius: 0,
|
|
29
|
+
blackColor: "currentColor",
|
|
30
|
+
whiteColor: "transparent",
|
|
31
|
+
size: "256px",
|
|
32
|
+
styleClassPassthrough: () => [],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
36
|
+
|
|
37
|
+
watch(
|
|
38
|
+
() => props.styleClassPassthrough,
|
|
39
|
+
() => resetElementClasses(props.styleClassPassthrough)
|
|
40
|
+
);
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<style lang="css">
|
|
44
|
+
@layer components {
|
|
45
|
+
.display-qr-code {
|
|
46
|
+
aspect-ratio: 1 / 1;
|
|
47
|
+
width: v-bind(size);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
</style>
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import DisplayQrCodeComponent from "../DisplayQrCode.vue";
|
|
3
|
+
import type { QrCodeVariant } from "../../../../types/components";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof DisplayQrCodeComponent> = {
|
|
6
|
+
title: "Atoms/QR Code/DisplayQrCode",
|
|
7
|
+
component: DisplayQrCodeComponent,
|
|
8
|
+
argTypes: {
|
|
9
|
+
qrValue: {
|
|
10
|
+
control: { type: "text" },
|
|
11
|
+
description: "The value/content to encode in the QR code",
|
|
12
|
+
table: { category: "Content" },
|
|
13
|
+
},
|
|
14
|
+
size: {
|
|
15
|
+
control: { type: "text" },
|
|
16
|
+
description: "Size of the QR code (CSS value like '256px', '10rem')",
|
|
17
|
+
table: { category: "Appearance" },
|
|
18
|
+
},
|
|
19
|
+
radius: {
|
|
20
|
+
control: { type: "range", min: 0, max: 20, step: 1 },
|
|
21
|
+
description: "Border radius of the QR code corners",
|
|
22
|
+
table: { category: "Appearance" },
|
|
23
|
+
},
|
|
24
|
+
blackColor: {
|
|
25
|
+
control: { type: "color" },
|
|
26
|
+
description: "Color of the QR code foreground elements",
|
|
27
|
+
table: { category: "Appearance" },
|
|
28
|
+
},
|
|
29
|
+
whiteColor: {
|
|
30
|
+
control: { type: "color" },
|
|
31
|
+
description: "Color of the QR code background",
|
|
32
|
+
table: { category: "Appearance" },
|
|
33
|
+
},
|
|
34
|
+
variant: { table: { disable: true } },
|
|
35
|
+
styleClassPassthrough: { table: { disable: true } },
|
|
36
|
+
},
|
|
37
|
+
args: {
|
|
38
|
+
qrValue: "https://github.com/srcdev/nuxt-components",
|
|
39
|
+
size: "256px",
|
|
40
|
+
radius: 0,
|
|
41
|
+
blackColor: "#000000",
|
|
42
|
+
whiteColor: "transparent",
|
|
43
|
+
styleClassPassthrough: [],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default meta;
|
|
48
|
+
|
|
49
|
+
interface QrStoryArgs {
|
|
50
|
+
qrValue: string;
|
|
51
|
+
size: string;
|
|
52
|
+
radius: number;
|
|
53
|
+
blackColor: string;
|
|
54
|
+
whiteColor: string;
|
|
55
|
+
styleClassPassthrough: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const DisplayTemplate: StoryFn<QrStoryArgs> = (args, { parameters }) => ({
|
|
59
|
+
components: { DisplayQrCodeComponent },
|
|
60
|
+
setup() {
|
|
61
|
+
const variant: QrCodeVariant = parameters?.variant || {
|
|
62
|
+
inner: "default",
|
|
63
|
+
marker: "default",
|
|
64
|
+
pixel: "default",
|
|
65
|
+
};
|
|
66
|
+
return { args, variant };
|
|
67
|
+
},
|
|
68
|
+
template: `
|
|
69
|
+
<div style="padding: 40px; display: flex; align-items: center; justify-content: center; background: #f5f5f5; border-radius: 8px;">
|
|
70
|
+
<DisplayQrCodeComponent
|
|
71
|
+
:qrValue="args.qrValue"
|
|
72
|
+
:variant="variant"
|
|
73
|
+
:radius="args.radius"
|
|
74
|
+
:blackColor="args.blackColor"
|
|
75
|
+
:whiteColor="args.whiteColor"
|
|
76
|
+
:size="args.size"
|
|
77
|
+
:style-class-passthrough="args.styleClassPassthrough"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
`,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const Default = DisplayTemplate.bind({});
|
|
84
|
+
|
|
85
|
+
export const Website = DisplayTemplate.bind({});
|
|
86
|
+
Website.args = { qrValue: "https://nuxt.com", radius: 8, blackColor: "#000000" };
|
|
87
|
+
Website.parameters = { variant: { inner: "circle", marker: "rounded", pixel: "dots" } };
|
|
88
|
+
|
|
89
|
+
export const VariantDefault = DisplayTemplate.bind({});
|
|
90
|
+
VariantDefault.args = { qrValue: "Default variant style", radius: 4 };
|
|
91
|
+
VariantDefault.parameters = { variant: { inner: "default", marker: "default", pixel: "default" } };
|
|
92
|
+
|
|
93
|
+
export const VariantCircle = DisplayTemplate.bind({});
|
|
94
|
+
VariantCircle.args = { qrValue: "Circle variant style", radius: 8, blackColor: "#1e40af" };
|
|
95
|
+
VariantCircle.parameters = { variant: { inner: "circle", marker: "circle", pixel: "circle" } };
|
|
96
|
+
|
|
97
|
+
export const VariantRounded = DisplayTemplate.bind({});
|
|
98
|
+
VariantRounded.args = { qrValue: "Rounded variant style", radius: 12, blackColor: "#059669" };
|
|
99
|
+
VariantRounded.parameters = { variant: { inner: "rounded", marker: "rounded", pixel: "rounded" } };
|
|
100
|
+
|
|
101
|
+
export const VariantDots = DisplayTemplate.bind({});
|
|
102
|
+
VariantDots.args = { qrValue: "Dots variant style", radius: 6, blackColor: "#dc2626" };
|
|
103
|
+
VariantDots.parameters = { variant: { inner: "dots", marker: "dots", pixel: "dots" } };
|
|
104
|
+
|
|
105
|
+
export const VariantMixed = DisplayTemplate.bind({});
|
|
106
|
+
VariantMixed.args = { qrValue: "Mixed variants for creative styling", radius: 10, blackColor: "#7c3aed" };
|
|
107
|
+
VariantMixed.parameters = { variant: { inner: "circle", marker: "rounded", pixel: "dots" } };
|
|
108
|
+
|
|
109
|
+
export const WiFiConnection = DisplayTemplate.bind({});
|
|
110
|
+
WiFiConnection.args = { qrValue: "WIFI:T:WPA;S:MyHomeNetwork;P:mySecurePassword123;H:false;", blackColor: "#1e40af", radius: 4 };
|
|
111
|
+
WiFiConnection.parameters = { variant: { inner: "rounded", marker: "circle", pixel: "default" } };
|
|
112
|
+
|
|
113
|
+
export const VCardContact = DisplayTemplate.bind({});
|
|
114
|
+
VCardContact.args = {
|
|
115
|
+
qrValue: "BEGIN:VCARD\\nVERSION:3.0\\nFN:John Doe\\nORG:Acme Corp\\nTITLE:Software Engineer\\nTEL:+1234567890\\nEMAIL:john.doe@acme.com\\nURL:https://johndoe.dev\\nEND:VCARD",
|
|
116
|
+
blackColor: "#059669",
|
|
117
|
+
radius: 12,
|
|
118
|
+
};
|
|
119
|
+
VCardContact.parameters = { variant: { inner: "dots", marker: "rounded", pixel: "circle" } };
|
|
120
|
+
|
|
121
|
+
export const SmallSize = DisplayTemplate.bind({});
|
|
122
|
+
SmallSize.args = { qrValue: "Small QR for tight spaces", size: "128px", radius: 2, blackColor: "#f59e0b" };
|
|
123
|
+
SmallSize.parameters = { variant: { inner: "dots", marker: "circle", pixel: "rounded" } };
|
|
124
|
+
|
|
125
|
+
export const LargeSize = DisplayTemplate.bind({});
|
|
126
|
+
LargeSize.args = { qrValue: "Large QR for posters and signage", size: "400px", radius: 16, blackColor: "#0ea5e9" };
|
|
127
|
+
LargeSize.parameters = { variant: { inner: "circle", marker: "rounded", pixel: "dots" } };
|
|
128
|
+
|
|
129
|
+
export const HighContrast = DisplayTemplate.bind({});
|
|
130
|
+
HighContrast.args = { qrValue: "High contrast for accessibility", blackColor: "#000000", whiteColor: "#ffffff", radius: 0, size: "256px" };
|
|
131
|
+
HighContrast.parameters = { variant: { inner: "default", marker: "default", pixel: "default" } };
|
|
132
|
+
|
|
133
|
+
const PlaygroundTemplate: StoryFn<QrStoryArgs> = (args) => ({
|
|
134
|
+
components: { DisplayQrCodeComponent },
|
|
135
|
+
setup() {
|
|
136
|
+
const qrValue = ref(args.qrValue);
|
|
137
|
+
const variant = ref<QrCodeVariant>({ inner: "circle", marker: "rounded", pixel: "dots" });
|
|
138
|
+
const radius = ref(args.radius);
|
|
139
|
+
const blackColor = ref(args.blackColor);
|
|
140
|
+
const whiteColor = ref(args.whiteColor);
|
|
141
|
+
const sizeValue = ref(256);
|
|
142
|
+
const size = computed(() => `${sizeValue.value}px`);
|
|
143
|
+
const presetValues = [
|
|
144
|
+
{ label: "Website URL", value: "https://nuxt.com" },
|
|
145
|
+
{ label: "WiFi Network", value: "WIFI:T:WPA;S:MyNetwork;P:password123;" },
|
|
146
|
+
{ label: "Phone Number", value: "tel:+1234567890" },
|
|
147
|
+
{ label: "Email", value: "mailto:hello@example.com" },
|
|
148
|
+
{ label: "SMS", value: "sms:+1234567890?body=Hello!" },
|
|
149
|
+
{ label: "Location", value: "geo:40.7128,-74.0060" },
|
|
150
|
+
];
|
|
151
|
+
const variantOptions = ["default", "circle", "rounded", "dots"];
|
|
152
|
+
return { qrValue, variant, radius, blackColor, whiteColor, sizeValue, size, presetValues, variantOptions };
|
|
153
|
+
},
|
|
154
|
+
template: `
|
|
155
|
+
<div style="padding: 40px;">
|
|
156
|
+
<div style="display: grid; grid-template-columns: 1fr 300px; gap: 40px; align-items: start;">
|
|
157
|
+
<div style="display: flex; align-items: center; justify-content: center; min-height: 400px; background: #f8fafc; border-radius: 8px; padding: 40px;">
|
|
158
|
+
<DisplayQrCodeComponent :qrValue="qrValue" :variant="variant" :radius="radius" :blackColor="blackColor" :whiteColor="whiteColor" :size="size" />
|
|
159
|
+
</div>
|
|
160
|
+
<div style="background: white; padding: 24px; border-radius: 8px; border: 1px solid #e5e7eb;">
|
|
161
|
+
<h3 style="margin: 0 0 20px 0; color: #374151; font-size: 18px;">QR Code Playground</h3>
|
|
162
|
+
<div style="margin-bottom: 20px;">
|
|
163
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Quick Presets:</label>
|
|
164
|
+
<select @change="qrValue = $event.target.value" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px;">
|
|
165
|
+
<option value="">Select a preset...</option>
|
|
166
|
+
<option v-for="preset in presetValues" :key="preset.label" :value="preset.value">{{ preset.label }}</option>
|
|
167
|
+
</select>
|
|
168
|
+
</div>
|
|
169
|
+
<div style="margin-bottom: 20px;">
|
|
170
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Custom Content:</label>
|
|
171
|
+
<textarea v-model="qrValue" rows="3" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; font-family: monospace; font-size: 12px;" placeholder="Enter QR code content..." />
|
|
172
|
+
</div>
|
|
173
|
+
<div style="margin-bottom: 20px;">
|
|
174
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Style Variants:</label>
|
|
175
|
+
<div style="display: grid; gap: 8px;">
|
|
176
|
+
<div><label style="font-size: 12px; color: #6b7280;">Inner:</label><select v-model="variant.inner" style="width: 100%; padding: 4px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 12px;"><option v-for="option in variantOptions" :key="option" :value="option">{{ option }}</option></select></div>
|
|
177
|
+
<div><label style="font-size: 12px; color: #6b7280;">Marker:</label><select v-model="variant.marker" style="width: 100%; padding: 4px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 12px;"><option v-for="option in variantOptions" :key="option" :value="option">{{ option }}</option></select></div>
|
|
178
|
+
<div><label style="font-size: 12px; color: #6b7280;">Pixel:</label><select v-model="variant.pixel" style="width: 100%; padding: 4px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 12px;"><option v-for="option in variantOptions" :key="option" :value="option">{{ option }}</option></select></div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<div style="margin-bottom: 20px;">
|
|
182
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Appearance:</label>
|
|
183
|
+
<div style="display: grid; gap: 12px;">
|
|
184
|
+
<div><label style="font-size: 12px; color: #6b7280;">Size: {{ sizeValue }}px</label><input v-model.number="sizeValue" type="range" min="128" max="400" step="16" style="width: 100%;" /></div>
|
|
185
|
+
<div><label style="font-size: 12px; color: #6b7280;">Radius: {{ radius }}px</label><input v-model.number="radius" type="range" min="0" max="20" step="1" style="width: 100%;" /></div>
|
|
186
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
|
|
187
|
+
<div><label style="font-size: 12px; color: #6b7280;">Foreground:</label><input v-model="blackColor" type="color" style="width: 100%; height: 32px; border: 1px solid #d1d5db; border-radius: 4px;" /></div>
|
|
188
|
+
<div><label style="font-size: 12px; color: #6b7280;">Background:</label><input v-model="whiteColor" type="color" style="width: 100%; height: 32px; border: 1px solid #d1d5db; border-radius: 4px;" /></div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
`,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
export const InteractivePlayground = PlaygroundTemplate.bind({});
|
|
199
|
+
InteractivePlayground.args = {
|
|
200
|
+
qrValue: "https://github.com/srcdev/nuxt-components",
|
|
201
|
+
radius: 8,
|
|
202
|
+
blackColor: "#000000",
|
|
203
|
+
whiteColor: "transparent",
|
|
204
|
+
size: "256px",
|
|
205
|
+
styleClassPassthrough: [],
|
|
206
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import DisplayQrCode from "../DisplayQrCode.vue";
|
|
4
|
+
import type { QrCodeVariant } from "~/types/components";
|
|
5
|
+
|
|
6
|
+
const defaultProps = {
|
|
7
|
+
qrValue: "https://example.com",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
describe("DisplayQrCode", () => {
|
|
11
|
+
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
it("mounts without error", async () => {
|
|
14
|
+
const wrapper = await mountSuspended(DisplayQrCode, { props: defaultProps });
|
|
15
|
+
expect(wrapper.vm).toBeTruthy();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// ─── Root element ─────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
it("renders a single root element", async () => {
|
|
21
|
+
const wrapper = await mountSuspended(DisplayQrCode, { props: defaultProps });
|
|
22
|
+
expect(wrapper.element).toBeTruthy();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("always has the display-qr-code class", async () => {
|
|
26
|
+
const wrapper = await mountSuspended(DisplayQrCode, { props: defaultProps });
|
|
27
|
+
expect(wrapper.classes()).toContain("display-qr-code");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// ─── Default props ────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
it("defaults radius to 0", async () => {
|
|
33
|
+
const wrapper = await mountSuspended(DisplayQrCode, { props: defaultProps });
|
|
34
|
+
interface VM { radius: number }
|
|
35
|
+
expect((wrapper.vm as unknown as VM).radius).toBe(0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("defaults blackColor to currentColor", async () => {
|
|
39
|
+
const wrapper = await mountSuspended(DisplayQrCode, { props: defaultProps });
|
|
40
|
+
interface VM { blackColor: string }
|
|
41
|
+
expect((wrapper.vm as unknown as VM).blackColor).toBe("currentColor");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("defaults whiteColor to transparent", async () => {
|
|
45
|
+
const wrapper = await mountSuspended(DisplayQrCode, { props: defaultProps });
|
|
46
|
+
interface VM { whiteColor: string }
|
|
47
|
+
expect((wrapper.vm as unknown as VM).whiteColor).toBe("transparent");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("defaults size to 256px", async () => {
|
|
51
|
+
const wrapper = await mountSuspended(DisplayQrCode, { props: defaultProps });
|
|
52
|
+
interface VM { size: string }
|
|
53
|
+
expect((wrapper.vm as unknown as VM).size).toBe("256px");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("defaults variant to all-default", async () => {
|
|
57
|
+
const wrapper = await mountSuspended(DisplayQrCode, { props: defaultProps });
|
|
58
|
+
interface VM { variant: QrCodeVariant }
|
|
59
|
+
expect((wrapper.vm as unknown as VM).variant).toEqual({ inner: "default", marker: "default", pixel: "default" });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ─── Custom props ─────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
it("reflects a custom radius prop", async () => {
|
|
65
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
66
|
+
props: { ...defaultProps, radius: 12 },
|
|
67
|
+
});
|
|
68
|
+
interface VM { radius: number }
|
|
69
|
+
expect((wrapper.vm as unknown as VM).radius).toBe(12);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("reflects a custom blackColor prop", async () => {
|
|
73
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
74
|
+
props: { ...defaultProps, blackColor: "#ff0000" },
|
|
75
|
+
});
|
|
76
|
+
interface VM { blackColor: string }
|
|
77
|
+
expect((wrapper.vm as unknown as VM).blackColor).toBe("#ff0000");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("reflects a custom whiteColor prop", async () => {
|
|
81
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
82
|
+
props: { ...defaultProps, whiteColor: "#ffffff" },
|
|
83
|
+
});
|
|
84
|
+
interface VM { whiteColor: string }
|
|
85
|
+
expect((wrapper.vm as unknown as VM).whiteColor).toBe("#ffffff");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("reflects a custom size prop", async () => {
|
|
89
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
90
|
+
props: { ...defaultProps, size: "128px" },
|
|
91
|
+
});
|
|
92
|
+
interface VM { size: string }
|
|
93
|
+
expect((wrapper.vm as unknown as VM).size).toBe("128px");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("reflects a custom variant prop", async () => {
|
|
97
|
+
const variant: QrCodeVariant = { inner: "circle", marker: "rounded", pixel: "dots" };
|
|
98
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
99
|
+
props: { ...defaultProps, variant },
|
|
100
|
+
});
|
|
101
|
+
interface VM { variant: QrCodeVariant }
|
|
102
|
+
expect((wrapper.vm as unknown as VM).variant).toEqual(variant);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ─── styleClassPassthrough ────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
it("applies a single styleClassPassthrough string", async () => {
|
|
108
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
109
|
+
props: { ...defaultProps, styleClassPassthrough: "custom-class" },
|
|
110
|
+
});
|
|
111
|
+
expect(wrapper.classes()).toContain("custom-class");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
115
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
116
|
+
props: { ...defaultProps, styleClassPassthrough: ["class-a", "class-b"] },
|
|
117
|
+
});
|
|
118
|
+
expect(wrapper.classes()).toContain("class-a");
|
|
119
|
+
expect(wrapper.classes()).toContain("class-b");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("retains display-qr-code class alongside styleClassPassthrough", async () => {
|
|
123
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
124
|
+
props: { ...defaultProps, styleClassPassthrough: "extra" },
|
|
125
|
+
});
|
|
126
|
+
expect(wrapper.classes()).toContain("display-qr-code");
|
|
127
|
+
expect(wrapper.classes()).toContain("extra");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("updates classes when styleClassPassthrough prop changes", async () => {
|
|
131
|
+
const wrapper = await mountSuspended(DisplayQrCode, {
|
|
132
|
+
props: { ...defaultProps, styleClassPassthrough: ["original"] },
|
|
133
|
+
});
|
|
134
|
+
expect(wrapper.classes()).toContain("original");
|
|
135
|
+
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
136
|
+
expect(wrapper.classes()).not.toContain("original");
|
|
137
|
+
expect(wrapper.classes()).toContain("updated");
|
|
138
|
+
});
|
|
139
|
+
});
|