@dillingerstaffing/strand-vue 0.13.0 → 0.15.0
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/dist/components/CodeBlock/CodeBlock.vue.d.ts +7 -0
- package/dist/components/CodeBlock/CodeBlock.vue.d.ts.map +1 -1
- package/dist/components/InstrumentViewport/InstrumentViewport.vue.d.ts +5 -0
- package/dist/components/InstrumentViewport/InstrumentViewport.vue.d.ts.map +1 -1
- package/dist/components/Link/Link.vue.d.ts +3 -0
- package/dist/components/Link/Link.vue.d.ts.map +1 -1
- package/dist/components/Nav/Nav.vue.d.ts +3 -0
- package/dist/components/Nav/Nav.vue.d.ts.map +1 -1
- package/dist/components/Section/Section.vue.d.ts +5 -2
- package/dist/components/Section/Section.vue.d.ts.map +1 -1
- package/dist/css/strand-ui.css +2796 -222
- package/dist/index.js +417 -354
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/CodeBlock/CodeBlock.vue +78 -5
- package/src/components/InstrumentViewport/InstrumentViewport.test.ts +19 -0
- package/src/components/InstrumentViewport/InstrumentViewport.vue +6 -0
- package/src/components/Link/Link.test.ts +12 -0
- package/src/components/Link/Link.vue +4 -1
- package/src/components/Nav/Nav.test.ts +14 -0
- package/src/components/Nav/Nav.vue +4 -1
- package/src/components/Section/Section.test.ts +10 -0
- package/src/components/Section/Section.vue +5 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<!--! Strand Vue | MIT License | dillingerstaffing.com -->
|
|
2
2
|
<script setup lang="ts">
|
|
3
|
-
import { computed } from 'vue'
|
|
3
|
+
import { computed, onBeforeUnmount, ref } from 'vue'
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
/** The code content to display */
|
|
@@ -9,22 +9,95 @@ interface Props {
|
|
|
9
9
|
language?: string
|
|
10
10
|
/** Additional CSS class */
|
|
11
11
|
className?: string
|
|
12
|
+
/**
|
|
13
|
+
* Render the one-click copy-to-clipboard button. Defaults to true so
|
|
14
|
+
* every CodeBlock is copyable out of the box; pass false to opt out
|
|
15
|
+
* for blocks that should not advertise a copy affordance.
|
|
16
|
+
*/
|
|
17
|
+
copyable?: boolean
|
|
12
18
|
}
|
|
13
19
|
|
|
20
|
+
const COPIED_DURATION_MS = 1500
|
|
21
|
+
|
|
14
22
|
const props = withDefaults(defineProps<Props>(), {
|
|
15
23
|
className: '',
|
|
24
|
+
copyable: true,
|
|
16
25
|
})
|
|
17
26
|
|
|
18
27
|
const classes = computed(() =>
|
|
19
|
-
['strand-code-block', props.className]
|
|
20
|
-
.filter(Boolean)
|
|
21
|
-
.join(' '),
|
|
28
|
+
['strand-code-block', props.className].filter(Boolean).join(' '),
|
|
22
29
|
)
|
|
30
|
+
|
|
31
|
+
const copied = ref(false)
|
|
32
|
+
let timer: number | null = null
|
|
33
|
+
|
|
34
|
+
onBeforeUnmount(() => {
|
|
35
|
+
if (timer !== null) window.clearTimeout(timer)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
async function handleCopy() {
|
|
39
|
+
try {
|
|
40
|
+
if (navigator.clipboard?.writeText) {
|
|
41
|
+
await navigator.clipboard.writeText(props.code)
|
|
42
|
+
} else {
|
|
43
|
+
const ta = document.createElement('textarea')
|
|
44
|
+
ta.value = props.code
|
|
45
|
+
ta.setAttribute('readonly', '')
|
|
46
|
+
ta.style.position = 'absolute'
|
|
47
|
+
ta.style.left = '-9999px'
|
|
48
|
+
document.body.appendChild(ta)
|
|
49
|
+
ta.select()
|
|
50
|
+
document.execCommand('copy')
|
|
51
|
+
document.body.removeChild(ta)
|
|
52
|
+
}
|
|
53
|
+
copied.value = true
|
|
54
|
+
if (timer !== null) window.clearTimeout(timer)
|
|
55
|
+
timer = window.setTimeout(() => {
|
|
56
|
+
copied.value = false
|
|
57
|
+
}, COPIED_DURATION_MS)
|
|
58
|
+
} catch {
|
|
59
|
+
// Ignore copy failures.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
23
62
|
</script>
|
|
24
63
|
|
|
25
64
|
<template>
|
|
26
|
-
<div :class="classes" v-bind="$attrs">
|
|
65
|
+
<div :class="classes" :data-strand-copy="copyable ? '' : undefined" v-bind="$attrs">
|
|
27
66
|
<span v-if="language" class="strand-code-block__label">{{ language }}</span>
|
|
28
67
|
<pre class="strand-code-block__pre"><code>{{ code }}</code></pre>
|
|
68
|
+
<button
|
|
69
|
+
v-if="copyable"
|
|
70
|
+
type="button"
|
|
71
|
+
:class="['strand-code-block__copy', copied ? 'strand-code-block__copy--copied' : '']"
|
|
72
|
+
:aria-label="copied ? 'Copied' : 'Copy code to clipboard'"
|
|
73
|
+
@click="handleCopy"
|
|
74
|
+
>
|
|
75
|
+
<svg
|
|
76
|
+
class="strand-code-block__copy-icon strand-code-block__copy-icon--clipboard"
|
|
77
|
+
viewBox="0 0 16 16"
|
|
78
|
+
fill="none"
|
|
79
|
+
stroke="currentColor"
|
|
80
|
+
stroke-width="1.75"
|
|
81
|
+
stroke-linecap="round"
|
|
82
|
+
stroke-linejoin="round"
|
|
83
|
+
aria-hidden="true"
|
|
84
|
+
focusable="false"
|
|
85
|
+
>
|
|
86
|
+
<path d="M6 3 V2 a1 1 0 0 1 1-1 h2 a1 1 0 0 1 1 1 v1 M5 3 h6 a1 1 0 0 1 1 1 v9 a1 1 0 0 1 -1 1 h-6 a1 1 0 0 1 -1 -1 v-9 a1 1 0 0 1 1 -1 z" />
|
|
87
|
+
</svg>
|
|
88
|
+
<svg
|
|
89
|
+
class="strand-code-block__copy-icon strand-code-block__copy-icon--check"
|
|
90
|
+
viewBox="0 0 16 16"
|
|
91
|
+
fill="none"
|
|
92
|
+
stroke="currentColor"
|
|
93
|
+
stroke-width="1.75"
|
|
94
|
+
stroke-linecap="round"
|
|
95
|
+
stroke-linejoin="round"
|
|
96
|
+
aria-hidden="true"
|
|
97
|
+
focusable="false"
|
|
98
|
+
>
|
|
99
|
+
<path d="M3 8 l3 3 l7 -7" />
|
|
100
|
+
</svg>
|
|
101
|
+
</button>
|
|
29
102
|
</div>
|
|
30
103
|
</template>
|
|
@@ -37,6 +37,25 @@ describe('InstrumentViewport', () => {
|
|
|
37
37
|
expect(container.firstElementChild?.className).toContain('strand-instrument-viewport--grid')
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
+
// ── Full bleed modifier ──
|
|
41
|
+
|
|
42
|
+
it('does not apply full-bleed modifier by default', () => {
|
|
43
|
+
const { container } = render(InstrumentViewport, { slots: { default: 'Test' } })
|
|
44
|
+
expect(container.firstElementChild?.className).not.toContain(
|
|
45
|
+
'strand-instrument-viewport--full-bleed',
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('applies full-bleed modifier when fullBleed prop is true', () => {
|
|
50
|
+
const { container } = render(InstrumentViewport, {
|
|
51
|
+
props: { fullBleed: true },
|
|
52
|
+
slots: { default: 'Test' },
|
|
53
|
+
})
|
|
54
|
+
expect(container.firstElementChild?.className).toContain(
|
|
55
|
+
'strand-instrument-viewport--full-bleed',
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
40
59
|
// ── Custom className ──
|
|
41
60
|
|
|
42
61
|
it('merges custom className with component classes', () => {
|
|
@@ -5,12 +5,17 @@ import { computed } from 'vue'
|
|
|
5
5
|
interface Props {
|
|
6
6
|
/** Show grid overlay lines */
|
|
7
7
|
grid?: boolean
|
|
8
|
+
/** Render as page-filling instrument cabinet (DL Part 9.3 full-bleed mode).
|
|
9
|
+
* Requires the host page to apply `strand-body--instrument` to <body>
|
|
10
|
+
* so the dark surface reaches the screen edge. */
|
|
11
|
+
fullBleed?: boolean
|
|
8
12
|
/** Additional CSS class */
|
|
9
13
|
className?: string
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
const props = withDefaults(defineProps<Props>(), {
|
|
13
17
|
grid: false,
|
|
18
|
+
fullBleed: false,
|
|
14
19
|
className: '',
|
|
15
20
|
})
|
|
16
21
|
|
|
@@ -18,6 +23,7 @@ const classes = computed(() =>
|
|
|
18
23
|
[
|
|
19
24
|
'strand-instrument-viewport',
|
|
20
25
|
props.grid && 'strand-instrument-viewport--grid',
|
|
26
|
+
props.fullBleed && 'strand-instrument-viewport--full-bleed',
|
|
21
27
|
props.className,
|
|
22
28
|
]
|
|
23
29
|
.filter(Boolean)
|
|
@@ -67,6 +67,18 @@ describe('Link', () => {
|
|
|
67
67
|
expect(container.firstElementChild?.getAttribute('rel')).toBe('noopener noreferrer')
|
|
68
68
|
})
|
|
69
69
|
|
|
70
|
+
// ── Variants ──
|
|
71
|
+
|
|
72
|
+
it('applies cta variant class', () => {
|
|
73
|
+
const { container } = render(Link, { props: { href: '/start', variant: 'cta' }, slots: { default: 'Start' } })
|
|
74
|
+
expect(container.firstElementChild?.className).toContain('strand-link--cta')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('applies mono variant class', () => {
|
|
78
|
+
const { container } = render(Link, { props: { href: '/', variant: 'mono' }, slots: { default: 'Home' } })
|
|
79
|
+
expect(container.firstElementChild?.className).toContain('strand-link--mono')
|
|
80
|
+
})
|
|
81
|
+
|
|
70
82
|
// ── Custom className ──
|
|
71
83
|
|
|
72
84
|
it('merges custom className', () => {
|
|
@@ -7,17 +7,20 @@ interface Props {
|
|
|
7
7
|
href: string
|
|
8
8
|
/** Opens in new tab with rel="noopener noreferrer" */
|
|
9
9
|
external?: boolean
|
|
10
|
+
/** Style variant */
|
|
11
|
+
variant?: 'default' | 'cta' | 'mono'
|
|
10
12
|
/** Additional CSS class */
|
|
11
13
|
className?: string
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
const props = withDefaults(defineProps<Props>(), {
|
|
15
17
|
external: false,
|
|
18
|
+
variant: 'default',
|
|
16
19
|
className: '',
|
|
17
20
|
})
|
|
18
21
|
|
|
19
22
|
const classes = computed(() =>
|
|
20
|
-
['strand-link', props.className].filter(Boolean).join(' '),
|
|
23
|
+
['strand-link', props.variant !== 'default' && `strand-link--${props.variant}`, props.className].filter(Boolean).join(' '),
|
|
21
24
|
)
|
|
22
25
|
</script>
|
|
23
26
|
|
|
@@ -168,4 +168,18 @@ describe('Nav', () => {
|
|
|
168
168
|
})
|
|
169
169
|
expect(container.querySelector('.strand-nav__logo')).toBeNull()
|
|
170
170
|
})
|
|
171
|
+
|
|
172
|
+
it('applies glass class when glass is true', () => {
|
|
173
|
+
const { container } = render(Nav, {
|
|
174
|
+
props: { items: sampleItems, glass: true },
|
|
175
|
+
})
|
|
176
|
+
expect(container.firstElementChild?.className).toContain('strand-nav--glass')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('does not apply glass class by default', () => {
|
|
180
|
+
const { container } = render(Nav, {
|
|
181
|
+
props: { items: sampleItems },
|
|
182
|
+
})
|
|
183
|
+
expect(container.firstElementChild?.className).not.toContain('strand-nav--glass')
|
|
184
|
+
})
|
|
171
185
|
})
|
|
@@ -11,10 +11,13 @@ export interface NavItem {
|
|
|
11
11
|
export interface NavProps {
|
|
12
12
|
/** Navigation items */
|
|
13
13
|
items?: NavItem[]
|
|
14
|
+
/** Glassmorphic variant (fixed, backdrop-filter, DL 11.5) */
|
|
15
|
+
glass?: boolean
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
const props = withDefaults(defineProps<NavProps>(), {
|
|
17
19
|
items: () => [],
|
|
20
|
+
glass: false,
|
|
18
21
|
})
|
|
19
22
|
|
|
20
23
|
const menuOpen = ref(false)
|
|
@@ -23,7 +26,7 @@ function toggleMenu() {
|
|
|
23
26
|
menuOpen.value = !menuOpen.value
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
const classes = computed(() => ['strand-nav'].filter(Boolean).join(' '))
|
|
29
|
+
const classes = computed(() => ['strand-nav', props.glass && 'strand-nav--glass'].filter(Boolean).join(' '))
|
|
27
30
|
</script>
|
|
28
31
|
|
|
29
32
|
<template>
|
|
@@ -30,6 +30,16 @@ describe('Section', () => {
|
|
|
30
30
|
expect(container.firstElementChild?.className).toContain('strand-section--hero')
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
+
it('applies compact variant class', () => {
|
|
34
|
+
const { container } = render(Section, { props: { variant: 'compact' }, slots: { default: 'c' } })
|
|
35
|
+
expect(container.firstElementChild?.className).toContain('strand-section--compact')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('applies border-top class', () => {
|
|
39
|
+
const { container } = render(Section, { props: { borderTop: true }, slots: { default: 'c' } })
|
|
40
|
+
expect(container.firstElementChild?.className).toContain('strand-section--border-top')
|
|
41
|
+
})
|
|
42
|
+
|
|
33
43
|
// ── Background ──
|
|
34
44
|
|
|
35
45
|
it('applies primary background class by default', () => {
|
|
@@ -4,9 +4,11 @@ import { computed } from 'vue'
|
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
/** Padding variant */
|
|
7
|
-
variant?: 'standard' | 'hero'
|
|
7
|
+
variant?: 'standard' | 'hero' | 'compact'
|
|
8
8
|
/** Surface background */
|
|
9
9
|
background?: 'primary' | 'elevated' | 'recessed'
|
|
10
|
+
/** Top border separator */
|
|
11
|
+
borderTop?: boolean
|
|
10
12
|
/** Additional CSS class */
|
|
11
13
|
className?: string
|
|
12
14
|
}
|
|
@@ -14,6 +16,7 @@ interface Props {
|
|
|
14
16
|
const props = withDefaults(defineProps<Props>(), {
|
|
15
17
|
variant: 'standard',
|
|
16
18
|
background: 'primary',
|
|
19
|
+
borderTop: false,
|
|
17
20
|
className: '',
|
|
18
21
|
})
|
|
19
22
|
|
|
@@ -22,6 +25,7 @@ const classes = computed(() =>
|
|
|
22
25
|
'strand-section',
|
|
23
26
|
`strand-section--${props.variant}`,
|
|
24
27
|
`strand-section--bg-${props.background}`,
|
|
28
|
+
props.borderTop && 'strand-section--border-top',
|
|
25
29
|
props.className,
|
|
26
30
|
]
|
|
27
31
|
.filter(Boolean)
|