@dillingerstaffing/strand-vue 0.14.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.
@@ -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)