@dillingerstaffing/strand-svelte 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dillingerstaffing/strand-svelte",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Strand UI - Svelte component library built on the Strand Design Language",
5
5
  "author": "Dillinger Staffing <engineering@dillingerstaffing.com> (https://dillingerstaffing.com)",
6
6
  "license": "MIT",
@@ -55,7 +55,7 @@
55
55
  "svelte": "^4.0.0 || ^5.0.0"
56
56
  },
57
57
  "dependencies": {
58
- "@dillingerstaffing/strand": "^0.13.0"
58
+ "@dillingerstaffing/strand": "^0.15.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@sveltejs/vite-plugin-svelte": "^5.0.0",
@@ -1,14 +1,90 @@
1
1
  <!--! Strand Svelte | MIT License | dillingerstaffing.com -->
2
2
  <script lang="ts">
3
+ import { onDestroy } from 'svelte'
4
+
3
5
  /** The code content to display */
4
6
  export let code: string
5
7
  /** Optional language label (e.g. "html", "css", "bash") */
6
8
  export let language: string | undefined = undefined
9
+ /**
10
+ * Render the one-click copy-to-clipboard button. Defaults to true so
11
+ * every CodeBlock is copyable out of the box; pass false to opt out
12
+ * for blocks that should not advertise a copy affordance.
13
+ */
14
+ export let copyable = true
15
+
16
+ const COPIED_DURATION_MS = 1500
17
+ let copied = false
18
+ let timer: number | null = null
19
+
20
+ onDestroy(() => {
21
+ if (timer !== null) window.clearTimeout(timer)
22
+ })
23
+
24
+ async function handleCopy() {
25
+ try {
26
+ if (navigator.clipboard?.writeText) {
27
+ await navigator.clipboard.writeText(code)
28
+ } else {
29
+ const ta = document.createElement('textarea')
30
+ ta.value = code
31
+ ta.setAttribute('readonly', '')
32
+ ta.style.position = 'absolute'
33
+ ta.style.left = '-9999px'
34
+ document.body.appendChild(ta)
35
+ ta.select()
36
+ document.execCommand('copy')
37
+ document.body.removeChild(ta)
38
+ }
39
+ copied = true
40
+ if (timer !== null) window.clearTimeout(timer)
41
+ timer = window.setTimeout(() => {
42
+ copied = false
43
+ }, COPIED_DURATION_MS)
44
+ } catch {
45
+ // Ignore copy failures.
46
+ }
47
+ }
7
48
  </script>
8
49
 
9
- <div class="strand-code-block" {...$$restProps}>
50
+ <div class="strand-code-block" data-strand-copy={copyable ? '' : undefined} {...$$restProps}>
10
51
  {#if language}
11
52
  <span class="strand-code-block__label">{language}</span>
12
53
  {/if}
13
54
  <pre class="strand-code-block__pre"><code>{code}</code></pre>
55
+ {#if copyable}
56
+ <button
57
+ type="button"
58
+ class="strand-code-block__copy {copied ? 'strand-code-block__copy--copied' : ''}"
59
+ aria-label={copied ? 'Copied' : 'Copy code to clipboard'}
60
+ on:click={handleCopy}
61
+ >
62
+ <svg
63
+ class="strand-code-block__copy-icon strand-code-block__copy-icon--clipboard"
64
+ viewBox="0 0 16 16"
65
+ fill="none"
66
+ stroke="currentColor"
67
+ stroke-width="1.75"
68
+ stroke-linecap="round"
69
+ stroke-linejoin="round"
70
+ aria-hidden="true"
71
+ focusable="false"
72
+ >
73
+ <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" />
74
+ </svg>
75
+ <svg
76
+ class="strand-code-block__copy-icon strand-code-block__copy-icon--check"
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="M3 8 l3 3 l7 -7" />
87
+ </svg>
88
+ </button>
89
+ {/if}
14
90
  </div>
@@ -2,10 +2,15 @@
2
2
  <script lang="ts">
3
3
  /** Show grid overlay lines */
4
4
  export let grid: boolean = false
5
+ /** Render as page-filling instrument cabinet (DL Part 9.3 full-bleed mode).
6
+ * Requires the host page to apply `strand-body--instrument` to <body>
7
+ * so the dark surface reaches the screen edge. */
8
+ export let fullBleed: boolean = false
5
9
 
6
10
  $: classes = [
7
11
  'strand-instrument-viewport',
8
12
  grid && 'strand-instrument-viewport--grid',
13
+ fullBleed && 'strand-instrument-viewport--full-bleed',
9
14
  ].filter(Boolean).join(' ')
10
15
  </script>
11
16
 
@@ -22,4 +22,16 @@ describe('InstrumentViewport', () => {
22
22
  const el = container.querySelector('.strand-instrument-viewport')
23
23
  expect(el).toHaveClass('strand-instrument-viewport--grid')
24
24
  })
25
+
26
+ it('does not apply full-bleed modifier by default', () => {
27
+ const { container } = render(InstrumentViewport)
28
+ const el = container.querySelector('.strand-instrument-viewport')
29
+ expect(el).not.toHaveClass('strand-instrument-viewport--full-bleed')
30
+ })
31
+
32
+ it('applies full-bleed modifier when fullBleed prop is true', () => {
33
+ const { container } = render(InstrumentViewport, { props: { fullBleed: true } })
34
+ const el = container.querySelector('.strand-instrument-viewport')
35
+ expect(el).toHaveClass('strand-instrument-viewport--full-bleed')
36
+ })
25
37
  })
@@ -4,11 +4,15 @@
4
4
  export let href: string
5
5
  /** Opens in new tab with rel="noopener noreferrer" */
6
6
  export let external: boolean = false
7
+ /** Style variant */
8
+ export let variant: 'default' | 'cta' | 'mono' = 'default'
9
+
10
+ $: linkClasses = ['strand-link', variant !== 'default' && `strand-link--${variant}`].filter(Boolean).join(' ')
7
11
  </script>
8
12
 
9
13
  <a
10
14
  {href}
11
- class="strand-link"
15
+ class={linkClasses}
12
16
  target={external ? '_blank' : undefined}
13
17
  rel={external ? 'noopener noreferrer' : undefined}
14
18
  {...$$restProps}
@@ -25,4 +25,14 @@ describe('Link', () => {
25
25
  expect(el).toHaveAttribute('target', '_blank')
26
26
  expect(el).toHaveAttribute('rel', 'noopener noreferrer')
27
27
  })
28
+
29
+ it('applies cta variant class', () => {
30
+ const { container } = render(Link, { props: { href: '/start', variant: 'cta' } })
31
+ expect(container.querySelector('.strand-link')).toHaveClass('strand-link--cta')
32
+ })
33
+
34
+ it('applies mono variant class', () => {
35
+ const { container } = render(Link, { props: { href: '/', variant: 'mono' } })
36
+ expect(container.querySelector('.strand-link')).toHaveClass('strand-link--mono')
37
+ })
28
38
  })
@@ -8,15 +8,19 @@
8
8
 
9
9
  /** Navigation items */
10
10
  export let items: NavItem[] = []
11
+ /** Glassmorphic variant (fixed, backdrop-filter, DL 11.5) */
12
+ export let glass: boolean = false
11
13
 
12
14
  let menuOpen = false
13
15
 
14
16
  function toggleMenu() {
15
17
  menuOpen = !menuOpen
16
18
  }
19
+
20
+ $: navClasses = ['strand-nav', glass && 'strand-nav--glass'].filter(Boolean).join(' ')
17
21
  </script>
18
22
 
19
- <nav class="strand-nav" aria-label="Main navigation" {...$$restProps}>
23
+ <nav class={navClasses} aria-label="Main navigation" {...$$restProps}>
20
24
  <div class="strand-nav__inner">
21
25
  {#if $$slots.logo}
22
26
  <div class="strand-nav__logo">
@@ -72,4 +72,9 @@ describe('Nav', () => {
72
72
  const { container } = render(Nav, { props: { items: testItems } })
73
73
  expect(container.querySelector('.strand-nav__inner')).toBeInTheDocument()
74
74
  })
75
+
76
+ it('applies glass class', () => {
77
+ const { container } = render(Nav, { props: { items: testItems, glass: true } })
78
+ expect(container.querySelector('.strand-nav')).toHaveClass('strand-nav--glass')
79
+ })
75
80
  })
@@ -1,14 +1,17 @@
1
1
  <!--! Strand Svelte | MIT License | dillingerstaffing.com -->
2
2
  <script lang="ts">
3
3
  /** Padding variant */
4
- export let variant: 'standard' | 'hero' = 'standard'
4
+ export let variant: 'standard' | 'hero' | 'compact' = 'standard'
5
5
  /** Surface background */
6
6
  export let background: 'primary' | 'elevated' | 'recessed' = 'primary'
7
+ /** Top border separator */
8
+ export let borderTop: boolean = false
7
9
 
8
10
  $: classes = [
9
11
  'strand-section',
10
12
  `strand-section--${variant}`,
11
13
  `strand-section--bg-${background}`,
14
+ borderTop && 'strand-section--border-top',
12
15
  ].filter(Boolean).join(' ')
13
16
  </script>
14
17
 
@@ -18,6 +18,16 @@ describe('Section', () => {
18
18
  expect(container.querySelector('.strand-section')).toHaveClass('strand-section--hero')
19
19
  })
20
20
 
21
+ it('applies compact variant class', () => {
22
+ const { container } = render(Section, { props: { variant: 'compact' } })
23
+ expect(container.querySelector('.strand-section')).toHaveClass('strand-section--compact')
24
+ })
25
+
26
+ it('applies border-top class', () => {
27
+ const { container } = render(Section, { props: { borderTop: true } })
28
+ expect(container.querySelector('.strand-section')).toHaveClass('strand-section--border-top')
29
+ })
30
+
21
31
  it('applies background classes', () => {
22
32
  const backgrounds = ['primary', 'elevated', 'recessed'] as const
23
33
  for (const background of backgrounds) {