@dillingerstaffing/strand-svelte 0.4.1

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.
Files changed (105) hide show
  1. package/README.md +56 -0
  2. package/dist/css/strand-ui.css +2583 -0
  3. package/dist/index.js +4154 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +53 -0
  6. package/src/components/Alert/Alert.svelte +32 -0
  7. package/src/components/Alert/Alert.test.ts +64 -0
  8. package/src/components/Alert/index.ts +2 -0
  9. package/src/components/Avatar/Avatar.svelte +40 -0
  10. package/src/components/Avatar/Avatar.test.ts +55 -0
  11. package/src/components/Avatar/index.ts +2 -0
  12. package/src/components/Badge/Badge.svelte +41 -0
  13. package/src/components/Badge/Badge.test.ts +55 -0
  14. package/src/components/Badge/index.ts +2 -0
  15. package/src/components/Breadcrumb/Breadcrumb.svelte +29 -0
  16. package/src/components/Breadcrumb/Breadcrumb.test.ts +66 -0
  17. package/src/components/Breadcrumb/index.ts +2 -0
  18. package/src/components/Button/Button.svelte +55 -0
  19. package/src/components/Button/Button.test.ts +110 -0
  20. package/src/components/Button/index.ts +2 -0
  21. package/src/components/Card/Card.svelte +17 -0
  22. package/src/components/Card/Card.test.ts +32 -0
  23. package/src/components/Card/index.ts +2 -0
  24. package/src/components/Checkbox/Checkbox.svelte +62 -0
  25. package/src/components/Checkbox/Checkbox.test.ts +67 -0
  26. package/src/components/Checkbox/index.ts +2 -0
  27. package/src/components/CodeBlock/CodeBlock.svelte +14 -0
  28. package/src/components/CodeBlock/CodeBlock.test.ts +36 -0
  29. package/src/components/CodeBlock/index.ts +2 -0
  30. package/src/components/Container/Container.svelte +14 -0
  31. package/src/components/Container/Container.test.ts +23 -0
  32. package/src/components/Container/index.ts +2 -0
  33. package/src/components/DataReadout/DataReadout.svelte +19 -0
  34. package/src/components/DataReadout/DataReadout.test.ts +35 -0
  35. package/src/components/DataReadout/index.ts +2 -0
  36. package/src/components/Dialog/Dialog.svelte +131 -0
  37. package/src/components/Dialog/Dialog.test.ts +77 -0
  38. package/src/components/Dialog/index.ts +2 -0
  39. package/src/components/Divider/Divider.svelte +36 -0
  40. package/src/components/Divider/Divider.test.ts +34 -0
  41. package/src/components/Divider/index.ts +2 -0
  42. package/src/components/FormField/FormField.svelte +39 -0
  43. package/src/components/FormField/FormField.test.ts +58 -0
  44. package/src/components/FormField/index.ts +2 -0
  45. package/src/components/Grid/Grid.svelte +13 -0
  46. package/src/components/Grid/Grid.test.ts +32 -0
  47. package/src/components/Grid/index.ts +2 -0
  48. package/src/components/Input/Input.svelte +41 -0
  49. package/src/components/Input/Input.test.ts +64 -0
  50. package/src/components/Input/index.ts +2 -0
  51. package/src/components/Link/Link.svelte +17 -0
  52. package/src/components/Link/Link.test.ts +28 -0
  53. package/src/components/Link/index.ts +2 -0
  54. package/src/components/Nav/Nav.svelte +69 -0
  55. package/src/components/Nav/Nav.test.ts +75 -0
  56. package/src/components/Nav/index.ts +2 -0
  57. package/src/components/Progress/Progress.svelte +78 -0
  58. package/src/components/Progress/Progress.test.ts +58 -0
  59. package/src/components/Progress/index.ts +2 -0
  60. package/src/components/Radio/Radio.svelte +46 -0
  61. package/src/components/Radio/Radio.test.ts +52 -0
  62. package/src/components/Radio/index.ts +2 -0
  63. package/src/components/Section/Section.svelte +17 -0
  64. package/src/components/Section/Section.test.ts +29 -0
  65. package/src/components/Section/index.ts +2 -0
  66. package/src/components/Select/Select.svelte +45 -0
  67. package/src/components/Select/Select.test.ts +59 -0
  68. package/src/components/Select/index.ts +2 -0
  69. package/src/components/Skeleton/Skeleton.svelte +25 -0
  70. package/src/components/Skeleton/Skeleton.test.ts +44 -0
  71. package/src/components/Skeleton/index.ts +2 -0
  72. package/src/components/Slider/Slider.svelte +37 -0
  73. package/src/components/Slider/Slider.test.ts +45 -0
  74. package/src/components/Slider/index.ts +2 -0
  75. package/src/components/Spinner/Spinner.svelte +15 -0
  76. package/src/components/Spinner/Spinner.test.ts +38 -0
  77. package/src/components/Spinner/index.ts +2 -0
  78. package/src/components/Stack/Stack.svelte +27 -0
  79. package/src/components/Stack/Stack.test.ts +46 -0
  80. package/src/components/Stack/index.ts +2 -0
  81. package/src/components/Switch/Switch.svelte +48 -0
  82. package/src/components/Switch/Switch.test.ts +61 -0
  83. package/src/components/Switch/index.ts +2 -0
  84. package/src/components/Table/Table.svelte +67 -0
  85. package/src/components/Table/Table.test.ts +88 -0
  86. package/src/components/Table/index.ts +2 -0
  87. package/src/components/Tabs/Tabs.svelte +89 -0
  88. package/src/components/Tabs/Tabs.test.ts +66 -0
  89. package/src/components/Tabs/index.ts +2 -0
  90. package/src/components/Tag/Tag.svelte +33 -0
  91. package/src/components/Tag/Tag.test.ts +63 -0
  92. package/src/components/Tag/index.ts +2 -0
  93. package/src/components/Textarea/Textarea.svelte +53 -0
  94. package/src/components/Textarea/Textarea.test.ts +53 -0
  95. package/src/components/Textarea/index.ts +2 -0
  96. package/src/components/Toast/Toast.svelte +29 -0
  97. package/src/components/Toast/Toast.test.ts +60 -0
  98. package/src/components/Toast/ToastProvider.svelte +45 -0
  99. package/src/components/Toast/index.ts +5 -0
  100. package/src/components/Toast/useToast.ts +78 -0
  101. package/src/components/Tooltip/Tooltip.svelte +56 -0
  102. package/src/components/Tooltip/Tooltip.test.ts +50 -0
  103. package/src/components/Tooltip/index.ts +2 -0
  104. package/src/index.ts +46 -0
  105. package/src/test-setup.ts +7 -0
@@ -0,0 +1,32 @@
1
+ /*! Strand Svelte | MIT License | dillingerstaffing.com */
2
+
3
+ import { describe, it, expect } from 'vitest'
4
+ import { render } from '@testing-library/svelte'
5
+ import Card from './Card.svelte'
6
+
7
+ describe('Card', () => {
8
+ it('renders with default classes', () => {
9
+ const { container } = render(Card)
10
+ const el = container.querySelector('.strand-card')
11
+ expect(el).toBeInTheDocument()
12
+ expect(el).toHaveClass('strand-card--elevated', 'strand-card--pad-md')
13
+ })
14
+
15
+ it('applies variant classes', () => {
16
+ const variants = ['elevated', 'outlined', 'interactive'] as const
17
+ for (const variant of variants) {
18
+ const { container, unmount } = render(Card, { props: { variant } })
19
+ expect(container.querySelector('.strand-card')).toHaveClass(`strand-card--${variant}`)
20
+ unmount()
21
+ }
22
+ })
23
+
24
+ it('applies padding classes', () => {
25
+ const paddings = ['none', 'sm', 'md', 'lg'] as const
26
+ for (const padding of paddings) {
27
+ const { container, unmount } = render(Card, { props: { padding } })
28
+ expect(container.querySelector('.strand-card')).toHaveClass(`strand-card--pad-${padding}`)
29
+ unmount()
30
+ }
31
+ })
32
+ })
@@ -0,0 +1,2 @@
1
+ /*! Strand Svelte | MIT License | dillingerstaffing.com */
2
+ export { default as Card } from './Card.svelte'
@@ -0,0 +1,62 @@
1
+ <!--! Strand Svelte | MIT License | dillingerstaffing.com -->
2
+ <script lang="ts">
3
+ /** Controlled checked state */
4
+ export let checked: boolean = false
5
+ /** Indeterminate visual state */
6
+ export let indeterminate: boolean = false
7
+ /** Disabled state */
8
+ export let disabled: boolean = false
9
+ /** Label text */
10
+ export let label: string | undefined = undefined
11
+ /** Change handler */
12
+ export let onchange: ((e: Event) => void) | undefined = undefined
13
+
14
+ let inputEl: HTMLInputElement
15
+
16
+ $: classes = [
17
+ 'strand-checkbox',
18
+ checked && 'strand-checkbox--checked',
19
+ indeterminate && 'strand-checkbox--indeterminate',
20
+ disabled && 'strand-checkbox--disabled',
21
+ ].filter(Boolean).join(' ')
22
+
23
+ $: ariaChecked = indeterminate ? 'mixed' : checked ? 'true' : 'false'
24
+
25
+ $: if (inputEl) {
26
+ inputEl.indeterminate = indeterminate
27
+ }
28
+
29
+ function handleChange(e: Event) {
30
+ if (!disabled) {
31
+ onchange?.(e)
32
+ }
33
+ }
34
+ </script>
35
+
36
+ <label class={classes}>
37
+ <input
38
+ bind:this={inputEl}
39
+ type="checkbox"
40
+ class="strand-checkbox__native"
41
+ {checked}
42
+ {disabled}
43
+ aria-checked={ariaChecked}
44
+ role="checkbox"
45
+ on:change={handleChange}
46
+ {...$$restProps}
47
+ />
48
+ <span class="strand-checkbox__control" aria-hidden="true">
49
+ {#if indeterminate}
50
+ <svg class="strand-checkbox__icon" viewBox="0 0 16 16" fill="none">
51
+ <line x1="4" y1="8" x2="12" y2="8" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
52
+ </svg>
53
+ {:else if checked}
54
+ <svg class="strand-checkbox__icon" viewBox="0 0 16 16" fill="none">
55
+ <path d="M3.5 8L6.5 11L12.5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
56
+ </svg>
57
+ {/if}
58
+ </span>
59
+ {#if label}
60
+ <span class="strand-checkbox__label">{label}</span>
61
+ {/if}
62
+ </label>
@@ -0,0 +1,67 @@
1
+ /*! Strand Svelte | MIT License | dillingerstaffing.com */
2
+
3
+ import { describe, it, expect } from 'vitest'
4
+ import { render } from '@testing-library/svelte'
5
+ import Checkbox from './Checkbox.svelte'
6
+
7
+ describe('Checkbox', () => {
8
+ it('renders with default classes', () => {
9
+ const { container } = render(Checkbox)
10
+ expect(container.querySelector('.strand-checkbox')).toBeInTheDocument()
11
+ const input = container.querySelector('.strand-checkbox__native')
12
+ expect(input).toBeInTheDocument()
13
+ expect(input).toHaveAttribute('type', 'checkbox')
14
+ expect(input).toHaveAttribute('role', 'checkbox')
15
+ })
16
+
17
+ it('applies checked class and aria-checked', () => {
18
+ const { container } = render(Checkbox, { props: { checked: true } })
19
+ expect(container.querySelector('.strand-checkbox')).toHaveClass('strand-checkbox--checked')
20
+ expect(container.querySelector('.strand-checkbox__native')).toHaveAttribute('aria-checked', 'true')
21
+ })
22
+
23
+ it('shows checkmark icon when checked', () => {
24
+ const { container } = render(Checkbox, { props: { checked: true } })
25
+ expect(container.querySelector('.strand-checkbox__icon')).toBeInTheDocument()
26
+ })
27
+
28
+ it('applies indeterminate class and aria-checked mixed', () => {
29
+ const { container } = render(Checkbox, { props: { indeterminate: true } })
30
+ expect(container.querySelector('.strand-checkbox')).toHaveClass('strand-checkbox--indeterminate')
31
+ expect(container.querySelector('.strand-checkbox__native')).toHaveAttribute('aria-checked', 'mixed')
32
+ })
33
+
34
+ it('shows indeterminate icon', () => {
35
+ const { container } = render(Checkbox, { props: { indeterminate: true } })
36
+ expect(container.querySelector('.strand-checkbox__icon')).toBeInTheDocument()
37
+ expect(container.querySelector('.strand-checkbox__icon line')).toBeInTheDocument()
38
+ })
39
+
40
+ it('applies disabled class and attribute', () => {
41
+ const { container } = render(Checkbox, { props: { disabled: true } })
42
+ expect(container.querySelector('.strand-checkbox')).toHaveClass('strand-checkbox--disabled')
43
+ expect(container.querySelector('.strand-checkbox__native')).toBeDisabled()
44
+ })
45
+
46
+ it('renders label text', () => {
47
+ const { container } = render(Checkbox, { props: { label: 'Accept terms' } })
48
+ const labelEl = container.querySelector('.strand-checkbox__label')
49
+ expect(labelEl).toBeInTheDocument()
50
+ expect(labelEl).toHaveTextContent('Accept terms')
51
+ })
52
+
53
+ it('does not render label when not provided', () => {
54
+ const { container } = render(Checkbox)
55
+ expect(container.querySelector('.strand-checkbox__label')).not.toBeInTheDocument()
56
+ })
57
+
58
+ it('control has aria-hidden', () => {
59
+ const { container } = render(Checkbox)
60
+ expect(container.querySelector('.strand-checkbox__control')).toHaveAttribute('aria-hidden', 'true')
61
+ })
62
+
63
+ it('unchecked state has aria-checked false', () => {
64
+ const { container } = render(Checkbox)
65
+ expect(container.querySelector('.strand-checkbox__native')).toHaveAttribute('aria-checked', 'false')
66
+ })
67
+ })
@@ -0,0 +1,2 @@
1
+ /*! Strand Svelte | MIT License | dillingerstaffing.com */
2
+ export { default as Checkbox } from './Checkbox.svelte'
@@ -0,0 +1,14 @@
1
+ <!--! Strand Svelte | MIT License | dillingerstaffing.com -->
2
+ <script lang="ts">
3
+ /** The code content to display */
4
+ export let code: string
5
+ /** Optional language label (e.g. "html", "css", "bash") */
6
+ export let language: string | undefined = undefined
7
+ </script>
8
+
9
+ <div class="strand-code-block" {...$$restProps}>
10
+ {#if language}
11
+ <span class="strand-code-block__label">{language}</span>
12
+ {/if}
13
+ <pre class="strand-code-block__pre"><code>{code}</code></pre>
14
+ </div>
@@ -0,0 +1,36 @@
1
+ /*! Strand Svelte | MIT License | dillingerstaffing.com */
2
+
3
+ import { describe, it, expect } from 'vitest'
4
+ import { render } from '@testing-library/svelte'
5
+ import CodeBlock from './CodeBlock.svelte'
6
+
7
+ describe('CodeBlock', () => {
8
+ it('renders with default class', () => {
9
+ const { container } = render(CodeBlock, { props: { code: 'const x = 1' } })
10
+ expect(container.querySelector('.strand-code-block')).toBeInTheDocument()
11
+ })
12
+
13
+ it('renders code content', () => {
14
+ const { container } = render(CodeBlock, { props: { code: 'console.log("hi")' } })
15
+ const code = container.querySelector('.strand-code-block__pre code')
16
+ expect(code).toHaveTextContent('console.log("hi")')
17
+ })
18
+
19
+ it('shows language label when provided', () => {
20
+ const { container } = render(CodeBlock, { props: { code: 'x = 1', language: 'python' } })
21
+ const label = container.querySelector('.strand-code-block__label')
22
+ expect(label).toBeInTheDocument()
23
+ expect(label).toHaveTextContent('python')
24
+ })
25
+
26
+ it('does not show label when language is not provided', () => {
27
+ const { container } = render(CodeBlock, { props: { code: 'x = 1' } })
28
+ expect(container.querySelector('.strand-code-block__label')).not.toBeInTheDocument()
29
+ })
30
+
31
+ it('renders pre and code elements', () => {
32
+ const { container } = render(CodeBlock, { props: { code: 'test' } })
33
+ expect(container.querySelector('.strand-code-block__pre')).toBeInTheDocument()
34
+ expect(container.querySelector('code')).toBeInTheDocument()
35
+ })
36
+ })
@@ -0,0 +1,2 @@
1
+ /*! Strand Svelte | MIT License | dillingerstaffing.com */
2
+ export { default as CodeBlock } from './CodeBlock.svelte'
@@ -0,0 +1,14 @@
1
+ <!--! Strand Svelte | MIT License | dillingerstaffing.com -->
2
+ <script lang="ts">
3
+ /** Max-width constraint */
4
+ export let size: 'narrow' | 'default' | 'wide' | 'full' = 'default'
5
+
6
+ $: classes = [
7
+ 'strand-container',
8
+ `strand-container--${size}`,
9
+ ].filter(Boolean).join(' ')
10
+ </script>
11
+
12
+ <div class={classes} {...$$restProps}>
13
+ <slot />
14
+ </div>
@@ -0,0 +1,23 @@
1
+ /*! Strand Svelte | MIT License | dillingerstaffing.com */
2
+
3
+ import { describe, it, expect } from 'vitest'
4
+ import { render } from '@testing-library/svelte'
5
+ import Container from './Container.svelte'
6
+
7
+ describe('Container', () => {
8
+ it('renders with default classes', () => {
9
+ const { container } = render(Container)
10
+ const el = container.querySelector('.strand-container')
11
+ expect(el).toBeInTheDocument()
12
+ expect(el).toHaveClass('strand-container--default')
13
+ })
14
+
15
+ it('applies size classes', () => {
16
+ const sizes = ['narrow', 'default', 'wide', 'full'] as const
17
+ for (const size of sizes) {
18
+ const { container, unmount } = render(Container, { props: { size } })
19
+ expect(container.querySelector('.strand-container')).toHaveClass(`strand-container--${size}`)
20
+ unmount()
21
+ }
22
+ })
23
+ })
@@ -0,0 +1,2 @@
1
+ /*! Strand Svelte | MIT License | dillingerstaffing.com */
2
+ export { default as Container } from './Container.svelte'
@@ -0,0 +1,19 @@
1
+ <!--! Strand Svelte | MIT License | dillingerstaffing.com -->
2
+ <script lang="ts">
3
+ /** Overline label text */
4
+ export let label: string
5
+ /** The large displayed value */
6
+ export let value: string | number
7
+ /** Size variant: sm (compact), md (default), lg (hero) */
8
+ export let size: 'sm' | 'md' | 'lg' | undefined = undefined
9
+
10
+ $: classes = [
11
+ 'strand-data-readout',
12
+ size && size !== 'md' ? `strand-data-readout--${size}` : '',
13
+ ].filter(Boolean).join(' ')
14
+ </script>
15
+
16
+ <div class={classes} {...$$restProps}>
17
+ <span class="strand-data-readout__label">{label}</span>
18
+ <span class="strand-data-readout__value">{value}</span>
19
+ </div>
@@ -0,0 +1,35 @@
1
+ /*! Strand Svelte | MIT License | dillingerstaffing.com */
2
+
3
+ import { describe, it, expect } from 'vitest'
4
+ import { render } from '@testing-library/svelte'
5
+ import DataReadout from './DataReadout.svelte'
6
+
7
+ describe('DataReadout', () => {
8
+ it('renders label and value', () => {
9
+ const { container } = render(DataReadout, { props: { label: 'Revenue', value: '$1.2M' } })
10
+ expect(container.querySelector('.strand-data-readout')).toBeInTheDocument()
11
+ expect(container.querySelector('.strand-data-readout__label')).toHaveTextContent('Revenue')
12
+ expect(container.querySelector('.strand-data-readout__value')).toHaveTextContent('$1.2M')
13
+ })
14
+
15
+ it('renders numeric value', () => {
16
+ const { container } = render(DataReadout, { props: { label: 'Count', value: 42 } })
17
+ expect(container.querySelector('.strand-data-readout__value')).toHaveTextContent('42')
18
+ })
19
+
20
+ it('applies sm size class', () => {
21
+ const { container } = render(DataReadout, { props: { label: 'L', value: 'V', size: 'sm' } })
22
+ expect(container.querySelector('.strand-data-readout')).toHaveClass('strand-data-readout--sm')
23
+ })
24
+
25
+ it('applies lg size class', () => {
26
+ const { container } = render(DataReadout, { props: { label: 'L', value: 'V', size: 'lg' } })
27
+ expect(container.querySelector('.strand-data-readout')).toHaveClass('strand-data-readout--lg')
28
+ })
29
+
30
+ it('does not apply size class for md', () => {
31
+ const { container } = render(DataReadout, { props: { label: 'L', value: 'V', size: 'md' } })
32
+ const el = container.querySelector('.strand-data-readout')
33
+ expect(el!.className).not.toContain('strand-data-readout--md')
34
+ })
35
+ })
@@ -0,0 +1,2 @@
1
+ /*! Strand Svelte | MIT License | dillingerstaffing.com */
2
+ export { default as DataReadout } from './DataReadout.svelte'
@@ -0,0 +1,131 @@
1
+ <!--! Strand Svelte | MIT License | dillingerstaffing.com -->
2
+ <script lang="ts">
3
+ import { onDestroy, tick } from 'svelte'
4
+
5
+ /** Whether the dialog is open */
6
+ export let open: boolean = false
7
+ /** Optional title rendered in the dialog header */
8
+ export let title: string | undefined = undefined
9
+ /** Close when clicking the backdrop */
10
+ export let closeOnOutsideClick: boolean = true
11
+ /** Close when pressing Escape */
12
+ export let closeOnEscape: boolean = true
13
+ /** Called when the dialog should close */
14
+ export let onclose: (() => void) | undefined = undefined
15
+
16
+ const FOCUSABLE_SELECTOR =
17
+ 'a[href], button:not(:disabled), textarea:not(:disabled), input:not(:disabled), select:not(:disabled), [tabindex]:not([tabindex="-1"])'
18
+
19
+ let dialogIdCounter = 0
20
+ const titleId = `strand-dialog-title-${++dialogIdCounter}`
21
+
22
+ let panelEl: HTMLDivElement
23
+ let previousFocus: Element | null = null
24
+ let originalOverflow = ''
25
+
26
+ $: if (open) {
27
+ onOpen()
28
+ } else {
29
+ onCloseCleanup()
30
+ }
31
+
32
+ async function onOpen() {
33
+ previousFocus = document.activeElement
34
+ originalOverflow = document.body.style.overflow
35
+ document.body.style.overflow = 'hidden'
36
+
37
+ await tick()
38
+ if (panelEl) {
39
+ const focusable = panelEl.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)
40
+ if (focusable.length > 0) {
41
+ focusable[0].focus()
42
+ } else {
43
+ panelEl.focus()
44
+ }
45
+ }
46
+ }
47
+
48
+ function onCloseCleanup() {
49
+ if (originalOverflow !== undefined) {
50
+ document.body.style.overflow = originalOverflow
51
+ }
52
+ if (previousFocus && previousFocus instanceof HTMLElement) {
53
+ previousFocus.focus()
54
+ }
55
+ }
56
+
57
+ function handleKeyDown(e: KeyboardEvent) {
58
+ if (e.key === 'Escape' && closeOnEscape) {
59
+ e.stopPropagation()
60
+ onclose?.()
61
+ return
62
+ }
63
+
64
+ if (e.key === 'Tab' && panelEl) {
65
+ const focusable = Array.from(panelEl.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))
66
+ if (focusable.length === 0) return
67
+
68
+ const first = focusable[0]
69
+ const last = focusable[focusable.length - 1]
70
+
71
+ if (e.shiftKey) {
72
+ if (document.activeElement === first) {
73
+ e.preventDefault()
74
+ last.focus()
75
+ }
76
+ } else {
77
+ if (document.activeElement === last) {
78
+ e.preventDefault()
79
+ first.focus()
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ function handleBackdropClick(e: MouseEvent) {
86
+ if (closeOnOutsideClick && e.target === e.currentTarget) {
87
+ onclose?.()
88
+ }
89
+ }
90
+
91
+ onDestroy(() => {
92
+ if (open) {
93
+ document.body.style.overflow = originalOverflow
94
+ }
95
+ })
96
+ </script>
97
+
98
+ {#if open}
99
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
100
+ <div
101
+ class="strand-dialog__backdrop"
102
+ on:click={handleBackdropClick}
103
+ on:keydown={handleKeyDown}
104
+ >
105
+ <div
106
+ bind:this={panelEl}
107
+ class="strand-dialog__panel"
108
+ role="dialog"
109
+ aria-modal="true"
110
+ aria-labelledby={title ? titleId : undefined}
111
+ tabindex={-1}
112
+ >
113
+ {#if title}
114
+ <div class="strand-dialog__header">
115
+ <h2 id={titleId} class="strand-dialog__title">{title}</h2>
116
+ </div>
117
+ {/if}
118
+ <button
119
+ type="button"
120
+ class="strand-dialog__close"
121
+ aria-label="Close"
122
+ on:click={() => onclose?.()}
123
+ >
124
+ &#215;
125
+ </button>
126
+ <div class="strand-dialog__body">
127
+ <slot />
128
+ </div>
129
+ </div>
130
+ </div>
131
+ {/if}
@@ -0,0 +1,77 @@
1
+ /*! Strand Svelte | MIT License | dillingerstaffing.com */
2
+
3
+ import { describe, it, expect, vi } from 'vitest'
4
+ import { render, fireEvent } from '@testing-library/svelte'
5
+ import Dialog from './Dialog.svelte'
6
+
7
+ describe('Dialog', () => {
8
+ it('does not render when closed', () => {
9
+ const { container } = render(Dialog, { props: { open: false } })
10
+ expect(container.querySelector('.strand-dialog__backdrop')).not.toBeInTheDocument()
11
+ })
12
+
13
+ it('renders backdrop and panel when open', () => {
14
+ const { container } = render(Dialog, { props: { open: true } })
15
+ expect(container.querySelector('.strand-dialog__backdrop')).toBeInTheDocument()
16
+ const panel = container.querySelector('.strand-dialog__panel')
17
+ expect(panel).toBeInTheDocument()
18
+ expect(panel).toHaveAttribute('role', 'dialog')
19
+ expect(panel).toHaveAttribute('aria-modal', 'true')
20
+ })
21
+
22
+ it('renders title when provided', () => {
23
+ const { container } = render(Dialog, { props: { open: true, title: 'Confirm' } })
24
+ const header = container.querySelector('.strand-dialog__header')
25
+ expect(header).toBeInTheDocument()
26
+ const titleEl = container.querySelector('.strand-dialog__title')
27
+ expect(titleEl).toHaveTextContent('Confirm')
28
+ const panel = container.querySelector('.strand-dialog__panel')
29
+ expect(panel).toHaveAttribute('aria-labelledby', titleEl!.id)
30
+ })
31
+
32
+ it('does not render title when not provided', () => {
33
+ const { container } = render(Dialog, { props: { open: true } })
34
+ expect(container.querySelector('.strand-dialog__header')).not.toBeInTheDocument()
35
+ expect(container.querySelector('.strand-dialog__panel')).not.toHaveAttribute('aria-labelledby')
36
+ })
37
+
38
+ it('has close button', () => {
39
+ const { container } = render(Dialog, { props: { open: true } })
40
+ const close = container.querySelector('.strand-dialog__close')
41
+ expect(close).toBeInTheDocument()
42
+ expect(close).toHaveAttribute('aria-label', 'Close')
43
+ })
44
+
45
+ it('fires onclose on close button click', async () => {
46
+ const onclose = vi.fn()
47
+ const { container } = render(Dialog, { props: { open: true, onclose } })
48
+ await fireEvent.click(container.querySelector('.strand-dialog__close')!)
49
+ expect(onclose).toHaveBeenCalled()
50
+ })
51
+
52
+ it('fires onclose on backdrop click', async () => {
53
+ const onclose = vi.fn()
54
+ const { container } = render(Dialog, { props: { open: true, onclose } })
55
+ await fireEvent.click(container.querySelector('.strand-dialog__backdrop')!)
56
+ expect(onclose).toHaveBeenCalled()
57
+ })
58
+
59
+ it('fires onclose on Escape key', async () => {
60
+ const onclose = vi.fn()
61
+ const { container } = render(Dialog, { props: { open: true, onclose } })
62
+ await fireEvent.keyDown(container.querySelector('.strand-dialog__backdrop')!, { key: 'Escape' })
63
+ expect(onclose).toHaveBeenCalled()
64
+ })
65
+
66
+ it('does not fire onclose on Escape when closeOnEscape is false', async () => {
67
+ const onclose = vi.fn()
68
+ const { container } = render(Dialog, { props: { open: true, closeOnEscape: false, onclose } })
69
+ await fireEvent.keyDown(container.querySelector('.strand-dialog__backdrop')!, { key: 'Escape' })
70
+ expect(onclose).not.toHaveBeenCalled()
71
+ })
72
+
73
+ it('has dialog body', () => {
74
+ const { container } = render(Dialog, { props: { open: true } })
75
+ expect(container.querySelector('.strand-dialog__body')).toBeInTheDocument()
76
+ })
77
+ })
@@ -0,0 +1,2 @@
1
+ /*! Strand Svelte | MIT License | dillingerstaffing.com */
2
+ export { default as Dialog } from './Dialog.svelte'
@@ -0,0 +1,36 @@
1
+ <!--! Strand Svelte | MIT License | dillingerstaffing.com -->
2
+ <script lang="ts">
3
+ /** Separator direction */
4
+ export let direction: 'horizontal' | 'vertical' = 'horizontal'
5
+ /** Optional label text displayed in the middle of the line */
6
+ export let label: string | undefined = undefined
7
+
8
+ $: isVertical = direction === 'vertical'
9
+ </script>
10
+
11
+ {#if isVertical}
12
+ <div
13
+ role="separator"
14
+ aria-orientation="vertical"
15
+ class={['strand-divider', 'strand-divider--vertical'].join(' ')}
16
+ {...$$restProps}
17
+ ></div>
18
+ {:else if label}
19
+ <div
20
+ role="separator"
21
+ aria-orientation="horizontal"
22
+ class={['strand-divider', 'strand-divider--horizontal', 'strand-divider--labeled'].join(' ')}
23
+ {...$$restProps}
24
+ >
25
+ <span class="strand-divider__line"></span>
26
+ <span class="strand-divider__label">{label}</span>
27
+ <span class="strand-divider__line"></span>
28
+ </div>
29
+ {:else}
30
+ <hr
31
+ role="separator"
32
+ aria-orientation="horizontal"
33
+ class={['strand-divider', 'strand-divider--horizontal'].join(' ')}
34
+ {...$$restProps}
35
+ />
36
+ {/if}
@@ -0,0 +1,34 @@
1
+ /*! Strand Svelte | MIT License | dillingerstaffing.com */
2
+
3
+ import { describe, it, expect } from 'vitest'
4
+ import { render } from '@testing-library/svelte'
5
+ import Divider from './Divider.svelte'
6
+
7
+ describe('Divider', () => {
8
+ it('renders horizontal hr by default', () => {
9
+ const { container } = render(Divider)
10
+ const el = container.querySelector('.strand-divider')
11
+ expect(el).toBeInTheDocument()
12
+ expect(el!.tagName).toBe('HR')
13
+ expect(el).toHaveClass('strand-divider--horizontal')
14
+ expect(el).toHaveAttribute('role', 'separator')
15
+ expect(el).toHaveAttribute('aria-orientation', 'horizontal')
16
+ })
17
+
18
+ it('renders vertical divider', () => {
19
+ const { container } = render(Divider, { props: { direction: 'vertical' } })
20
+ const el = container.querySelector('.strand-divider')
21
+ expect(el).toHaveClass('strand-divider--vertical')
22
+ expect(el).toHaveAttribute('aria-orientation', 'vertical')
23
+ })
24
+
25
+ it('renders labeled horizontal divider', () => {
26
+ const { container } = render(Divider, { props: { label: 'OR' } })
27
+ const el = container.querySelector('.strand-divider')
28
+ expect(el).toHaveClass('strand-divider--labeled')
29
+ expect(el!.tagName).toBe('DIV')
30
+ expect(container.querySelector('.strand-divider__label')).toHaveTextContent('OR')
31
+ const lines = container.querySelectorAll('.strand-divider__line')
32
+ expect(lines.length).toBe(2)
33
+ })
34
+ })
@@ -0,0 +1,2 @@
1
+ /*! Strand Svelte | MIT License | dillingerstaffing.com */
2
+ export { default as Divider } from './Divider.svelte'
@@ -0,0 +1,39 @@
1
+ <!--! Strand Svelte | MIT License | dillingerstaffing.com -->
2
+ <script lang="ts">
3
+ /** Label text */
4
+ export let label: string
5
+ /** Associates the label with a form control */
6
+ export let htmlFor: string
7
+ /** Hint text displayed below the input */
8
+ export let hint: string | undefined = undefined
9
+ /** Error text displayed below the input (replaces hint) */
10
+ export let error: string | undefined = undefined
11
+ /** Show required indicator */
12
+ export let required: boolean = false
13
+
14
+ $: classes = [
15
+ 'strand-form-field',
16
+ error && 'strand-form-field--error',
17
+ ].filter(Boolean).join(' ')
18
+ </script>
19
+
20
+ <div class={classes} {...$$restProps}>
21
+ <label class="strand-form-field__label" for={htmlFor}>
22
+ {label}
23
+ {#if required}
24
+ <span class="strand-form-field__required" aria-hidden="true">*</span>
25
+ {/if}
26
+ </label>
27
+ <div class="strand-form-field__control">
28
+ <slot />
29
+ </div>
30
+ {#if error}
31
+ <p class="strand-form-field__error" id={`${htmlFor}-error`} role="alert">
32
+ {error}
33
+ </p>
34
+ {:else if hint}
35
+ <p class="strand-form-field__hint" id={`${htmlFor}-hint`}>
36
+ {hint}
37
+ </p>
38
+ {/if}
39
+ </div>