srcdev-nuxt-components 9.1.17 → 9.1.19
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 +7 -1
- package/.claude/skills/component-export-types.md +11 -2
- package/.claude/skills/components/site-navigation.md +10 -0
- package/.claude/skills/components/social-icons-list.md +120 -0
- package/.claude/skills/index.md +3 -1
- package/.claude/skills/release-notes.md +48 -0
- package/app/assets/styles/setup/04.elements/forms/01.field-layout-container-level.css +2 -0
- package/app/components/02.molecules/social-icons-list/SocialIconsList.vue +90 -0
- package/app/components/02.molecules/social-icons-list/stories/SocialIconsList.stories.ts +111 -0
- package/app/components/02.molecules/social-icons-list/tests/SocialIconsList.spec.ts +124 -0
- package/app/components/02.molecules/social-icons-list/tests/__snapshots__/SocialIconsList.spec.ts.snap +9 -0
- package/app/components/forms/ui/FormField.vue +34 -34
- package/app/composables/useWhatsApp.ts +0 -2
- package/app/types/components/index.ts +1 -0
- package/app/types/components/social-icons-list.d.ts +6 -0
- package/modules/icon-sets.ts +1 -0
- package/package.json +6 -1
package/.claude/settings.json
CHANGED
|
@@ -20,7 +20,13 @@
|
|
|
20
20
|
"Bash(npx nuxi:*)",
|
|
21
21
|
"Bash(node -e \"const t = require\\('/Users/simoncornforth/websites/nuxt-components/node_modules/pinia-plugin-persistedstate'\\); console.log\\(Object.keys\\(t\\)\\)\")",
|
|
22
22
|
"Bash(npx vue-tsc:*)",
|
|
23
|
-
"Bash(git -C /Users/simoncornforth/websites/nuxt-components log --oneline -5)"
|
|
23
|
+
"Bash(git -C /Users/simoncornforth/websites/nuxt-components log --oneline -5)",
|
|
24
|
+
"Bash(git -C /Users/simoncornforth/websites/nuxt-components show 16ac4ef --stat)",
|
|
25
|
+
"Bash(git -C /Users/simoncornforth/websites/nuxt-components log --oneline -8)",
|
|
26
|
+
"Bash(git -C /Users/simoncornforth/websites/nuxt-components show 5597a15 --stat)",
|
|
27
|
+
"Bash(git -C /Users/simoncornforth/websites/nuxt-components show 5597a15 -- \"*.vue\" \"*.css\")",
|
|
28
|
+
"Bash(npm install:*)",
|
|
29
|
+
"Bash(npm run:*)"
|
|
24
30
|
],
|
|
25
31
|
"additionalDirectories": [
|
|
26
32
|
"/Users/simoncornforth/websites/nuxt-components/app/components/01.atoms/content-wrappers/content-width",
|
|
@@ -48,10 +48,19 @@ import type { NavItem, NavItemData } from "~/types/components/navigation-horizon
|
|
|
48
48
|
|
|
49
49
|
## Consuming app usage
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
The chain that makes this work:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
app/types/components/<component>.d.ts
|
|
55
|
+
→ app/types/components/index.ts (export * from "./<component>.d")
|
|
56
|
+
→ types.d.ts (export * from "./app/types/components")
|
|
57
|
+
→ consumer import
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`types.d.ts` is the `"types"` entry point in `package.json`, so once a type is added to the barrel it is importable directly from the package name:
|
|
52
61
|
|
|
53
62
|
```ts
|
|
54
|
-
import type { NavItem, NavItemData } from "nuxt-components
|
|
63
|
+
import type { NavItem, NavItemData } from "srcdev-nuxt-components";
|
|
55
64
|
```
|
|
56
65
|
|
|
57
66
|
## Notes
|
|
@@ -20,9 +20,19 @@ Both the horizontal list and the panel include animated active/hover indicator d
|
|
|
20
20
|
|
|
21
21
|
## NavItemData type
|
|
22
22
|
|
|
23
|
+
**Within the layer:**
|
|
24
|
+
|
|
23
25
|
```ts
|
|
24
26
|
import type { NavItemData } from "~/types/components/navigation-horizontal.d";
|
|
27
|
+
```
|
|
25
28
|
|
|
29
|
+
**In a consuming app:**
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import type { NavItemData } from "srcdev-nuxt-components";
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ts
|
|
26
36
|
const navItemData: NavItemData = {
|
|
27
37
|
main: [
|
|
28
38
|
{ text: "Home", href: "/" },
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# SocialIconsList Component
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`SocialIconsList` renders a horizontal list of social network icon links. Each item is data-driven via the `items` prop — the component composes the full href from `baseHref + profileId` at render time. Icons are sourced from the `logos:*` Iconify collection (`@iconify-json/logos`), which uses brand colours baked into the SVG.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Props reference
|
|
10
|
+
|
|
11
|
+
| Prop | Type | Default | Notes |
|
|
12
|
+
|------|------|---------|-------|
|
|
13
|
+
| `items` | `ISocialIcon[]` | — | **Required.** Array of social network items. |
|
|
14
|
+
| `label` | `string` | `"Social media profiles"` | `aria-label` applied to the `<ul>` element. |
|
|
15
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `<ul>`. |
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## ISocialIcon type
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
interface ISocialIcon {
|
|
23
|
+
networkName: string; // Used as aria-label: "{networkName} profile"
|
|
24
|
+
iconName: string; // Iconify icon name, e.g. "logos:instagram-icon"
|
|
25
|
+
baseHref: string; // Base URL including trailing slash, e.g. "https://www.instagram.com/"
|
|
26
|
+
profileId: string; // Profile identifier appended to baseHref
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The rendered `href` is `${baseHref}${profileId}`.
|
|
31
|
+
|
|
32
|
+
### Importing the type
|
|
33
|
+
|
|
34
|
+
**Within the layer** (e.g. in another layer component or composable):
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import type { ISocialIcon } from "~/types/components/social-icons-list.d";
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**In a consuming app** — exported via the package barrel (`types.d.ts` → `app/types/components/index.ts`):
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import type { ISocialIcon } from "srcdev-nuxt-components";
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Standard social network values
|
|
49
|
+
|
|
50
|
+
| Network | `iconName` | `baseHref` |
|
|
51
|
+
|---------|-----------|-----------|
|
|
52
|
+
| Facebook | `logos:facebook` | `https://www.facebook.com/` |
|
|
53
|
+
| X (Twitter) | `logos:x` | `https://x.com/` |
|
|
54
|
+
| Instagram | `logos:instagram-icon` | `https://www.instagram.com/` |
|
|
55
|
+
| YouTube | `logos:youtube-icon` | `https://www.youtube.com/@` |
|
|
56
|
+
| TikTok | `logos:tiktok-icon` | `https://www.tiktok.com/@` |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Usage example
|
|
61
|
+
|
|
62
|
+
```vue
|
|
63
|
+
<SocialIconsList
|
|
64
|
+
:items="[
|
|
65
|
+
{
|
|
66
|
+
networkName: 'Instagram',
|
|
67
|
+
iconName: 'logos:instagram-icon',
|
|
68
|
+
baseHref: 'https://www.instagram.com/',
|
|
69
|
+
profileId: 'yourbrand',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
networkName: 'TikTok',
|
|
73
|
+
iconName: 'logos:tiktok-icon',
|
|
74
|
+
baseHref: 'https://www.tiktok.com/@',
|
|
75
|
+
profileId: 'yourbrand',
|
|
76
|
+
},
|
|
77
|
+
]"
|
|
78
|
+
/>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## CSS custom properties
|
|
84
|
+
|
|
85
|
+
| Property | Default | Notes |
|
|
86
|
+
|----------|---------|-------|
|
|
87
|
+
| `--theme-social-icon-size` | `2.4rem` | Width and height of each icon |
|
|
88
|
+
| `--theme-social-icon-gap` | `1.2rem` | Gap between icons in the flex row |
|
|
89
|
+
|
|
90
|
+
> **Note:** Iconify's `logos:*` styles are injected outside any CSS `@layer`, which means they override layered component styles. Icon sizing is applied via an inline `style` attribute on the `<Icon>` element so it takes precedence.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Local style override scaffold
|
|
95
|
+
|
|
96
|
+
```vue
|
|
97
|
+
<SocialIconsList :style-class-passthrough="['my-social-icons']" :items="items" />
|
|
98
|
+
|
|
99
|
+
<style>
|
|
100
|
+
/* ─── SocialIconsList local overrides ──────────────────────────────
|
|
101
|
+
Geometry and size only — brand colours are baked into logos: SVGs.
|
|
102
|
+
Delete this block if no overrides are needed.
|
|
103
|
+
─────────────────────────────────────────────────────────────────── */
|
|
104
|
+
.social-icons-list {
|
|
105
|
+
&.my-social-icons {
|
|
106
|
+
/* --theme-social-icon-size: 3.2rem; */
|
|
107
|
+
/* --theme-social-icon-gap: 2rem; */
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
</style>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Notes
|
|
116
|
+
|
|
117
|
+
- All links open in a new tab with `rel="noopener noreferrer"`.
|
|
118
|
+
- Each link carries `aria-label="{networkName} profile"` for screen reader accessibility.
|
|
119
|
+
- The `logos:*` Iconify collection requires `@iconify-json/logos` to be installed in the consumer app. Without it, icons will fall back to a CDN fetch (causing FOUC). See [icon-sets.md](../icon-sets.md).
|
|
120
|
+
- Auto-imported in Nuxt — no manual import needed.
|
package/.claude/skills/index.md
CHANGED
|
@@ -36,6 +36,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
36
36
|
├── component-inline-action-button.md — InputButtonCore variant="inline" pattern for buttons embedded in custom input wrappers
|
|
37
37
|
├── icon-sets.md — icon set packages required by layer components, FOUC prevention, component→package map
|
|
38
38
|
├── robots-env-aware.md — @nuxtjs/robots: allow crawling on prod domain only, block on preview/staging via env var
|
|
39
|
+
├── release-notes.md — produce release notes as a fenced markdown block from git log
|
|
39
40
|
├── composable-whatsapp.md — useWhatsApp: open pre-filled wa.me link from form payload; runtime config, security, usage
|
|
40
41
|
├── composable-zod-validation.md — useZodValidation: schema-driven form validation, error binding, submit flow, API error push
|
|
41
42
|
├── composable-colour-scheme.md — useColourScheme: reactive light/dark/auto switching, localStorage persistence, runtime config
|
|
@@ -58,7 +59,8 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
58
59
|
├── glass-panel.md — GlassPanel props, slots, CSS token API (--glass-panel-bg/border-color/shadow/highlight), theming override
|
|
59
60
|
├── navigation-horizontal.md — NavigationHorizontal props, NavItemData type, CSS token API, import path gotcha
|
|
60
61
|
├── input-copy-core.md — InputCopyCore: readonly copy-to-clipboard input; props, emits, slots, CSS classes, usage
|
|
61
|
-
|
|
62
|
+
├── site-navigation.md — SiteNavigation: responsive nav with auto-collapse, burger menu, decorator indicators, CSS token API
|
|
63
|
+
└── social-icons-list.md — SocialIconsList: data-driven social icon links, ISocialIcon type, logos: icon names, CSS tokens
|
|
62
64
|
```
|
|
63
65
|
|
|
64
66
|
## Skill file template
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Release Notes
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
When asked to create release notes, always produce a fenced markdown code block (` ```markdown `) so the content can be copied and pasted directly into a git tag, GitHub release, or changelog.
|
|
6
|
+
|
|
7
|
+
## Steps
|
|
8
|
+
|
|
9
|
+
### 1. Determine the version
|
|
10
|
+
|
|
11
|
+
Run `git log --oneline -8` to find the most recent release commit and the commits since the previous release.
|
|
12
|
+
|
|
13
|
+
### 2. Review the diff
|
|
14
|
+
|
|
15
|
+
Run `git show <commit> --stat` for each commit since the last release to understand what changed.
|
|
16
|
+
|
|
17
|
+
### 3. Produce a fenced markdown block
|
|
18
|
+
|
|
19
|
+
Always wrap the output in a ` ```markdown ` code fence — never render it as plain markdown. This ensures the user can copy the raw text without formatting being stripped.
|
|
20
|
+
|
|
21
|
+
## Format
|
|
22
|
+
|
|
23
|
+
```markdown
|
|
24
|
+
## vX.Y.Z
|
|
25
|
+
|
|
26
|
+
### New
|
|
27
|
+
|
|
28
|
+
- **`ComponentOrComposableName`** — one-line description of what it does and why it exists
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Short description of what was wrong and what was corrected
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- Short description of intentional behaviour or API changes
|
|
37
|
+
|
|
38
|
+
### Documentation
|
|
39
|
+
|
|
40
|
+
- **`skill-name` skill** — new/updated: what it covers
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Notes
|
|
44
|
+
|
|
45
|
+
- Only include sections that have content — omit empty headings
|
|
46
|
+
- Keep each bullet to one line where possible
|
|
47
|
+
- Lead with the most user-facing changes (New, Fixed) before internal ones (Documentation, Tests)
|
|
48
|
+
- Test additions warrant their own section only when substantial; otherwise fold into the relevant New/Fixed bullet
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ul class="social-icons-list" :class="[elementClasses]" :aria-label="label">
|
|
3
|
+
<li v-for="item in items" :key="item.networkName" class="social-icon-item">
|
|
4
|
+
<a
|
|
5
|
+
:href="`${item.baseHref}${item.profileId}`"
|
|
6
|
+
class="social-icon-link"
|
|
7
|
+
:aria-label="`${item.networkName} profile`"
|
|
8
|
+
rel="noopener noreferrer"
|
|
9
|
+
target="_blank"
|
|
10
|
+
>
|
|
11
|
+
<Icon
|
|
12
|
+
:name="item.iconName"
|
|
13
|
+
class="social-icon"
|
|
14
|
+
:style="{ width: 'var(--_icon-size)', height: 'var(--_icon-size)' }"
|
|
15
|
+
/>
|
|
16
|
+
</a>
|
|
17
|
+
</li>
|
|
18
|
+
</ul>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script setup lang="ts">
|
|
22
|
+
import type { ISocialIcon } from "~/types/components/social-icons-list.d";
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
items: ISocialIcon[];
|
|
26
|
+
label?: string;
|
|
27
|
+
styleClassPassthrough?: string | string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
31
|
+
label: "Social media profiles",
|
|
32
|
+
styleClassPassthrough: () => [],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
36
|
+
|
|
37
|
+
watch(
|
|
38
|
+
() => props.styleClassPassthrough,
|
|
39
|
+
() => {
|
|
40
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<style lang="css">
|
|
46
|
+
@layer components {
|
|
47
|
+
.social-icons-list {
|
|
48
|
+
--_icon-size: var(--theme-social-icon-size, 2.4rem);
|
|
49
|
+
--_gap: var(--theme-social-icon-gap, 1.2rem);
|
|
50
|
+
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-wrap: wrap;
|
|
53
|
+
gap: var(--_gap);
|
|
54
|
+
list-style: none;
|
|
55
|
+
padding: 0;
|
|
56
|
+
margin: 0;
|
|
57
|
+
|
|
58
|
+
.social-icon-item {
|
|
59
|
+
display: flex;
|
|
60
|
+
|
|
61
|
+
.social-icon-link {
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
justify-content: center;
|
|
65
|
+
text-decoration: none;
|
|
66
|
+
transition:
|
|
67
|
+
transform 200ms ease,
|
|
68
|
+
opacity 200ms ease;
|
|
69
|
+
|
|
70
|
+
&:hover,
|
|
71
|
+
&:focus-visible {
|
|
72
|
+
transform: scale(1.15);
|
|
73
|
+
opacity: 0.85;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
&:focus-visible {
|
|
77
|
+
outline: 2px solid currentColor;
|
|
78
|
+
outline-offset: 3px;
|
|
79
|
+
border-radius: 2px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.social-icon {
|
|
83
|
+
/* width: var(--_icon-size); */
|
|
84
|
+
/* height: var(--_icon-size); */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
</style>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import SocialIconsList from "../SocialIconsList.vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
import type { ISocialIcon } from "~/types/components/social-icons-list.d";
|
|
4
|
+
|
|
5
|
+
const sampleItems: ISocialIcon[] = [
|
|
6
|
+
{
|
|
7
|
+
networkName: "Facebook",
|
|
8
|
+
iconName: "logos:facebook",
|
|
9
|
+
baseHref: "https://www.facebook.com/",
|
|
10
|
+
profileId: "yourprofile",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
networkName: "X (Twitter)",
|
|
14
|
+
iconName: "logos:x",
|
|
15
|
+
baseHref: "https://x.com/",
|
|
16
|
+
profileId: "yourhandle",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
networkName: "Instagram",
|
|
20
|
+
iconName: "logos:instagram-icon",
|
|
21
|
+
baseHref: "https://www.instagram.com/",
|
|
22
|
+
profileId: "yourprofile",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
networkName: "YouTube",
|
|
26
|
+
iconName: "logos:youtube-icon",
|
|
27
|
+
baseHref: "https://www.youtube.com/@",
|
|
28
|
+
profileId: "yourchannel",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
networkName: "TikTok",
|
|
32
|
+
iconName: "logos:tiktok-icon",
|
|
33
|
+
baseHref: "https://www.tiktok.com/@",
|
|
34
|
+
profileId: "yourprofile",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const meta: Meta<typeof SocialIconsList> = {
|
|
39
|
+
title: "Molecules/SocialIconsList",
|
|
40
|
+
component: SocialIconsList,
|
|
41
|
+
argTypes: {
|
|
42
|
+
items: {
|
|
43
|
+
control: "object",
|
|
44
|
+
description:
|
|
45
|
+
"Array of social network items. Each item requires networkName, iconName (Iconify logos: collection), baseHref, and profileId.",
|
|
46
|
+
},
|
|
47
|
+
label: {
|
|
48
|
+
control: "text",
|
|
49
|
+
description: "aria-label for the list element",
|
|
50
|
+
},
|
|
51
|
+
styleClassPassthrough: {
|
|
52
|
+
control: "object",
|
|
53
|
+
description: "Additional CSS classes applied to the root element",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
args: {
|
|
57
|
+
items: sampleItems,
|
|
58
|
+
label: "Social media profiles",
|
|
59
|
+
styleClassPassthrough: ["m-40"],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default meta;
|
|
64
|
+
type Story = StoryObj<typeof SocialIconsList>;
|
|
65
|
+
|
|
66
|
+
// ─── Stories ─────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/** Default — all five networks at the standard icon size. */
|
|
69
|
+
export const Default: Story = {
|
|
70
|
+
render: (args) => ({
|
|
71
|
+
components: { SocialIconsList },
|
|
72
|
+
setup() {
|
|
73
|
+
return { args };
|
|
74
|
+
},
|
|
75
|
+
template: `<SocialIconsList v-bind="args" />`,
|
|
76
|
+
}),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Large icons — override icon size via CSS token. */
|
|
80
|
+
export const LargeIcons: Story = {
|
|
81
|
+
name: "Large Icons",
|
|
82
|
+
render: (args) => ({
|
|
83
|
+
components: { SocialIconsList },
|
|
84
|
+
setup() {
|
|
85
|
+
return { args };
|
|
86
|
+
},
|
|
87
|
+
template: `<SocialIconsList v-bind="args" style="--theme-social-icon-size: 4rem;" />`,
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Single network — only one item in the list. */
|
|
92
|
+
export const SingleNetwork: Story = {
|
|
93
|
+
name: "Single Network",
|
|
94
|
+
args: {
|
|
95
|
+
items: [
|
|
96
|
+
{
|
|
97
|
+
networkName: "Instagram",
|
|
98
|
+
iconName: "logos:instagram-icon",
|
|
99
|
+
baseHref: "https://www.instagram.com/",
|
|
100
|
+
profileId: "luxurylocs",
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
render: (args) => ({
|
|
105
|
+
components: { SocialIconsList },
|
|
106
|
+
setup() {
|
|
107
|
+
return { args };
|
|
108
|
+
},
|
|
109
|
+
template: `<SocialIconsList v-bind="args" />`,
|
|
110
|
+
}),
|
|
111
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import SocialIconsList from "../SocialIconsList.vue";
|
|
4
|
+
import type { ISocialIcon } from "~/types/components/social-icons-list.d";
|
|
5
|
+
|
|
6
|
+
const defaultItems: ISocialIcon[] = [
|
|
7
|
+
{
|
|
8
|
+
networkName: "Facebook",
|
|
9
|
+
iconName: "logos:facebook",
|
|
10
|
+
baseHref: "https://www.facebook.com/",
|
|
11
|
+
profileId: "testprofile",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
networkName: "Instagram",
|
|
15
|
+
iconName: "logos:instagram-icon",
|
|
16
|
+
baseHref: "https://www.instagram.com/",
|
|
17
|
+
profileId: "testhandle",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
networkName: "YouTube",
|
|
21
|
+
iconName: "logos:youtube-icon",
|
|
22
|
+
baseHref: "https://www.youtube.com/@",
|
|
23
|
+
profileId: "testchannel",
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
describe("SocialIconsList", () => {
|
|
28
|
+
it("mounts without error", async () => {
|
|
29
|
+
const wrapper = await mountSuspended(SocialIconsList, {
|
|
30
|
+
props: { items: defaultItems },
|
|
31
|
+
});
|
|
32
|
+
expect(wrapper.vm).toBeTruthy();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("renders as a ul element", async () => {
|
|
36
|
+
const wrapper = await mountSuspended(SocialIconsList, {
|
|
37
|
+
props: { items: defaultItems },
|
|
38
|
+
});
|
|
39
|
+
expect(wrapper.element.tagName.toLowerCase()).toBe("ul");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("renders the correct number of list items", async () => {
|
|
43
|
+
const wrapper = await mountSuspended(SocialIconsList, {
|
|
44
|
+
props: { items: defaultItems },
|
|
45
|
+
});
|
|
46
|
+
expect(wrapper.findAll(".social-icon-item").length).toBe(defaultItems.length);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("renders the correct href for each item", async () => {
|
|
50
|
+
const wrapper = await mountSuspended(SocialIconsList, {
|
|
51
|
+
props: { items: defaultItems },
|
|
52
|
+
});
|
|
53
|
+
const links = wrapper.findAll(".social-icon-link");
|
|
54
|
+
defaultItems.forEach((item, i) => {
|
|
55
|
+
expect(links[i]!.attributes("href")).toBe(`${item.baseHref}${item.profileId}`);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("renders aria-label on each link using networkName", async () => {
|
|
60
|
+
const wrapper = await mountSuspended(SocialIconsList, {
|
|
61
|
+
props: { items: defaultItems },
|
|
62
|
+
});
|
|
63
|
+
const links = wrapper.findAll(".social-icon-link");
|
|
64
|
+
defaultItems.forEach((item, i) => {
|
|
65
|
+
expect(links[i]!.attributes("aria-label")).toBe(`${item.networkName} profile`);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("adds rel='noopener noreferrer' to each link", async () => {
|
|
70
|
+
const wrapper = await mountSuspended(SocialIconsList, {
|
|
71
|
+
props: { items: defaultItems },
|
|
72
|
+
});
|
|
73
|
+
const links = wrapper.findAll(".social-icon-link");
|
|
74
|
+
links.forEach((link) => {
|
|
75
|
+
expect(link.attributes("rel")).toBe("noopener noreferrer");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("opens each link in a new tab", async () => {
|
|
80
|
+
const wrapper = await mountSuspended(SocialIconsList, {
|
|
81
|
+
props: { items: defaultItems },
|
|
82
|
+
});
|
|
83
|
+
const links = wrapper.findAll(".social-icon-link");
|
|
84
|
+
links.forEach((link) => {
|
|
85
|
+
expect(link.attributes("target")).toBe("_blank");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("uses the default aria-label on the list", async () => {
|
|
90
|
+
const wrapper = await mountSuspended(SocialIconsList, {
|
|
91
|
+
props: { items: defaultItems },
|
|
92
|
+
});
|
|
93
|
+
expect(wrapper.attributes("aria-label")).toBe("Social media profiles");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("uses a custom label prop as aria-label on the list", async () => {
|
|
97
|
+
const wrapper = await mountSuspended(SocialIconsList, {
|
|
98
|
+
props: { items: defaultItems, label: "Follow us" },
|
|
99
|
+
});
|
|
100
|
+
expect(wrapper.attributes("aria-label")).toBe("Follow us");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("renders an empty list when items is empty", async () => {
|
|
104
|
+
const wrapper = await mountSuspended(SocialIconsList, {
|
|
105
|
+
props: { items: [] },
|
|
106
|
+
});
|
|
107
|
+
expect(wrapper.findAll(".social-icon-item").length).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("applies styleClassPassthrough classes to the root element", async () => {
|
|
111
|
+
const wrapper = await mountSuspended(SocialIconsList, {
|
|
112
|
+
props: { items: defaultItems, styleClassPassthrough: ["custom-class", "another-class"] },
|
|
113
|
+
});
|
|
114
|
+
expect(wrapper.classes()).toContain("custom-class");
|
|
115
|
+
expect(wrapper.classes()).toContain("another-class");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("renders the correct HTML structure", async () => {
|
|
119
|
+
const wrapper = await mountSuspended(SocialIconsList, {
|
|
120
|
+
props: { items: defaultItems },
|
|
121
|
+
});
|
|
122
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`SocialIconsList > renders the correct HTML structure 1`] = `
|
|
4
|
+
"<ul class="social-icons-list" aria-label="Social media profiles">
|
|
5
|
+
<li class="social-icon-item"><a href="https://www.facebook.com/testprofile" class="social-icon-link" aria-label="Facebook profile" rel="noopener noreferrer" target="_blank"><span class="iconify i-logos:facebook social-icon" aria-hidden="true" style="width: var(--_icon-size); height: var(--_icon-size);"></span></a></li>
|
|
6
|
+
<li class="social-icon-item"><a href="https://www.instagram.com/testhandle" class="social-icon-link" aria-label="Instagram profile" rel="noopener noreferrer" target="_blank"><span class="iconify i-logos:instagram-icon social-icon" aria-hidden="true" style="width: var(--_icon-size); height: var(--_icon-size);"></span></a></li>
|
|
7
|
+
<li class="social-icon-item"><a href="https://www.youtube.com/@testchannel" class="social-icon-link" aria-label="YouTube profile" rel="noopener noreferrer" target="_blank"><span class="iconify i-logos:youtube-icon social-icon" aria-hidden="true" style="width: var(--_icon-size); height: var(--_icon-size);"></span></a></li>
|
|
8
|
+
</ul>"
|
|
9
|
+
`;
|
|
@@ -34,48 +34,48 @@ defineProps({
|
|
|
34
34
|
|
|
35
35
|
<style lang="css">
|
|
36
36
|
@layer components {
|
|
37
|
-
.form-field {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
.form-field {
|
|
38
|
+
--_gutter-width: 0rem;
|
|
39
|
+
--_max-width: 400px;
|
|
40
|
+
--_background-color: transparent;
|
|
41
|
+
--_border-radius: 0.4rem;
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
background-color: var(--_background-color);
|
|
44
|
+
border-radius: var(--_border-radius);
|
|
45
|
+
margin-inline: auto;
|
|
46
|
+
margin-block: var(--field-margin-block);
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
width: min(100% - calc(2 * var(--_gutter-width)), var(--_max-width));
|
|
49
|
+
outline: 0rem solid var(--slate-05);
|
|
50
|
+
/* overflow-block: hidden; */
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
&:has(.underline) {
|
|
53
|
+
--_background-color: var(--theme-form-input-bg-underlined);
|
|
54
|
+
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
.form-field-inner {
|
|
57
|
+
background-color: var(--_background-color);
|
|
58
|
+
border-radius: var(--_border-radius);
|
|
59
|
+
margin-inline-start: 0rem;
|
|
60
|
+
padding-inline-start: 0rem;
|
|
61
|
+
outline: 0 solid var(--slate-05);
|
|
62
|
+
}
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
&.has-gutter {
|
|
65
|
+
--_gutter-width: 1.6rem;
|
|
66
|
+
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
&.narrow {
|
|
69
|
+
max-width: 400px;
|
|
70
|
+
}
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
&.medium {
|
|
73
|
+
--_max-width: 800px;
|
|
74
|
+
}
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
&.wide {
|
|
77
|
+
--_max-width: 1200px;
|
|
78
|
+
}
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
|
-
}
|
|
81
81
|
</style>
|
package/modules/icon-sets.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "srcdev-nuxt-components",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "9.1.
|
|
4
|
+
"version": "9.1.19",
|
|
5
5
|
"main": "nuxt.config.ts",
|
|
6
6
|
"types": "types.d.ts",
|
|
7
7
|
"license": "MIT",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"@iconify-json/bitcoin-icons": "*",
|
|
46
46
|
"@iconify-json/gravity-ui": "*",
|
|
47
47
|
"@iconify-json/ic": "*",
|
|
48
|
+
"@iconify-json/logos": "*",
|
|
48
49
|
"@iconify-json/lucide": "*",
|
|
49
50
|
"@iconify-json/material-symbols": "*",
|
|
50
51
|
"@iconify-json/mdi": "*",
|
|
@@ -66,6 +67,9 @@
|
|
|
66
67
|
"@iconify-json/ic": {
|
|
67
68
|
"optional": true
|
|
68
69
|
},
|
|
70
|
+
"@iconify-json/logos": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
69
73
|
"@iconify-json/lucide": {
|
|
70
74
|
"optional": true
|
|
71
75
|
},
|
|
@@ -86,6 +90,7 @@
|
|
|
86
90
|
"@iconify-json/bitcoin-icons": "1.2.4",
|
|
87
91
|
"@iconify-json/gravity-ui": "1.2.12",
|
|
88
92
|
"@iconify-json/ic": "*",
|
|
93
|
+
"@iconify-json/logos": "1.2.11",
|
|
89
94
|
"@iconify-json/lucide": "*",
|
|
90
95
|
"@iconify-json/material-symbols": "1.2.64",
|
|
91
96
|
"@iconify-json/mdi": "1.2.3",
|