@picked/svelte 0.0.45-next.1 → 0.0.45-next.3
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/plugins/devtools/DevToolsExperience.svelte +61 -44
- package/dist/plugins/devtools/components/ExperienceInstanceListItem.svelte +2 -1
- package/dist/plugins/devtools/components/ExperienceInstancesTable.svelte +22 -20
- package/dist/plugins/devtools/components/Tabs.svelte +20 -7
- package/dist/plugins/devtools/experience.js +1 -1
- package/dist/plugins/devtools/screens/components/ComponentsListScreen.svelte +18 -18
- package/dist/plugins/devtools/screens/events/EventListItem.svelte +28 -42
- package/dist/plugins/devtools/screens/events/EventsListScreen.svelte +10 -11
- package/dist/plugins/devtools/screens/events/EventsTable.svelte +31 -33
- package/dist/plugins/devtools/screens/experiences/ExperienceDetailsScreen.svelte +13 -16
- package/dist/plugins/devtools/screens/experiences/ExperienceTreeScreen.svelte +20 -71
- package/dist/plugins/devtools/screens/features/FeaturesListScreen.svelte +96 -232
- package/dist/plugins/devtools/screens/frameworks/FrameworksListScreen.svelte +14 -16
- package/dist/plugins/devtools/screens/history/HistoryEntriesTable.svelte +18 -16
- package/dist/plugins/devtools/screens/predicates/PredicatesListScreen.svelte +14 -16
- package/dist/plugins/devtools/screens/responses/ResponseTableRow.svelte +25 -63
- package/dist/plugins/devtools/screens/responses/ResponsesTable.svelte +43 -64
- package/dist/plugins/devtools/ui/actions/Button.svelte +171 -0
- package/dist/plugins/devtools/ui/actions/CopyButton.svelte +88 -0
- package/dist/plugins/devtools/ui/data/Badge.svelte +62 -0
- package/dist/plugins/devtools/ui/data/CodeBlock.svelte +38 -0
- package/dist/plugins/devtools/ui/data/DataTable.svelte +134 -0
- package/dist/plugins/devtools/ui/data/EmptyState.svelte +26 -0
- package/dist/plugins/devtools/ui/data/ExpandableRow.svelte +66 -0
- package/dist/plugins/devtools/ui/data/VisualizationContainer.svelte +55 -0
- package/dist/plugins/devtools/ui/feedback/Banner.svelte +116 -0
- package/dist/plugins/devtools/ui/feedback/Spinner.svelte +79 -0
- package/dist/plugins/devtools/ui/forms/Checkbox.svelte +139 -0
- package/dist/plugins/devtools/ui/forms/RadioGroup.svelte +218 -0
- package/dist/plugins/devtools/ui/forms/Select.svelte +137 -0
- package/dist/plugins/devtools/ui/forms/Switch.svelte +158 -0
- package/dist/plugins/devtools/ui/forms/TextField.svelte +125 -0
- package/dist/plugins/devtools/ui/index.d.ts +22 -0
- package/dist/plugins/devtools/ui/index.js +1 -0
- package/dist/plugins/devtools/ui/layout/Card.svelte +55 -0
- package/dist/plugins/devtools/ui/layout/Divider.svelte +34 -0
- package/dist/plugins/devtools/ui/layout/Spacer.svelte +11 -0
- package/dist/plugins/devtools/ui/layout/Stack.svelte +129 -0
- package/dist/plugins/devtools/ui/overlays/Tooltip.svelte +90 -0
- package/dist/plugins/devtools/ui/tokens.css +144 -0
- package/dist/plugins/devtools/ui/tokens.css.js +1 -0
- package/dist/plugins/devtools/ui/typography/Heading.svelte +55 -0
- package/dist/plugins/devtools/ui/typography/Text.svelte +93 -0
- package/package.json +5 -2
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
code: string
|
|
4
|
+
inline?: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let { code, inline = false }: Props = $props()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
{#if inline}
|
|
11
|
+
<code class="code-inline">{code}</code>
|
|
12
|
+
{:else}
|
|
13
|
+
<pre class="code-block"><code>{code}</code></pre>
|
|
14
|
+
{/if}
|
|
15
|
+
|
|
16
|
+
<style>
|
|
17
|
+
.code-inline {
|
|
18
|
+
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
|
|
19
|
+
font-size: var(--text-xs);
|
|
20
|
+
background-color: var(--color-gray-100);
|
|
21
|
+
border: 1px solid var(--color-gray-200);
|
|
22
|
+
border-radius: var(--radius-md);
|
|
23
|
+
padding: var(--space-0-5) var(--space-1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.code-block {
|
|
27
|
+
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
|
|
28
|
+
font-size: var(--text-xs);
|
|
29
|
+
background-color: var(--color-gray-50);
|
|
30
|
+
border: 1px solid var(--color-gray-200);
|
|
31
|
+
border-radius: var(--radius-md);
|
|
32
|
+
padding: var(--space-3);
|
|
33
|
+
margin: 0;
|
|
34
|
+
overflow-x: auto;
|
|
35
|
+
white-space: pre-wrap;
|
|
36
|
+
word-break: break-all;
|
|
37
|
+
}
|
|
38
|
+
</style>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
|
|
4
|
+
interface Column {
|
|
5
|
+
key: string
|
|
6
|
+
header: string
|
|
7
|
+
sortable?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
columns: Column[]
|
|
12
|
+
rows: unknown[]
|
|
13
|
+
row: Snippet<[unknown, number]>
|
|
14
|
+
empty?: Snippet
|
|
15
|
+
sortKey?: string | null
|
|
16
|
+
sortDirection?: 'asc' | 'desc'
|
|
17
|
+
onSort?: (key: string) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
columns,
|
|
22
|
+
rows,
|
|
23
|
+
row,
|
|
24
|
+
empty,
|
|
25
|
+
sortKey = null,
|
|
26
|
+
sortDirection = 'asc',
|
|
27
|
+
onSort,
|
|
28
|
+
}: Props = $props()
|
|
29
|
+
|
|
30
|
+
function handleHeaderClick(column: Column): void {
|
|
31
|
+
if (column.sortable && onSort) {
|
|
32
|
+
onSort(column.key)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function handleHeaderKeydown(event: KeyboardEvent, column: Column): void {
|
|
37
|
+
if ((event.key === 'Enter' || event.key === ' ') && column.sortable && onSort) {
|
|
38
|
+
event.preventDefault()
|
|
39
|
+
onSort(column.key)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<table class="data-table">
|
|
45
|
+
<thead>
|
|
46
|
+
<tr>
|
|
47
|
+
{#each columns as column}
|
|
48
|
+
<th
|
|
49
|
+
class:sortable={column.sortable}
|
|
50
|
+
aria-sort={column.sortable && sortKey === column.key
|
|
51
|
+
? sortDirection === 'asc' ? 'ascending' : 'descending'
|
|
52
|
+
: undefined}
|
|
53
|
+
onclick={() => handleHeaderClick(column)}
|
|
54
|
+
onkeydown={(e: KeyboardEvent) => handleHeaderKeydown(e, column)}
|
|
55
|
+
tabindex={column.sortable ? 0 : undefined}
|
|
56
|
+
role={column.sortable ? 'columnheader button' : 'columnheader'}
|
|
57
|
+
>
|
|
58
|
+
{column.header}
|
|
59
|
+
{#if column.sortable && sortKey === column.key}
|
|
60
|
+
<span class="sort-indicator">{sortDirection === 'asc' ? '↑' : '↓'}</span>
|
|
61
|
+
{/if}
|
|
62
|
+
</th>
|
|
63
|
+
{/each}
|
|
64
|
+
</tr>
|
|
65
|
+
</thead>
|
|
66
|
+
<tbody>
|
|
67
|
+
{#if rows.length === 0 && empty}
|
|
68
|
+
<tr>
|
|
69
|
+
<td colspan={columns.length} class="empty-cell">
|
|
70
|
+
{@render empty()}
|
|
71
|
+
</td>
|
|
72
|
+
</tr>
|
|
73
|
+
{:else}
|
|
74
|
+
{#each rows as item, index}
|
|
75
|
+
{@render row(item, index)}
|
|
76
|
+
{/each}
|
|
77
|
+
{/if}
|
|
78
|
+
</tbody>
|
|
79
|
+
</table>
|
|
80
|
+
|
|
81
|
+
<style>
|
|
82
|
+
.data-table {
|
|
83
|
+
width: 100%;
|
|
84
|
+
border-collapse: collapse;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
th {
|
|
88
|
+
padding: var(--space-2);
|
|
89
|
+
text-align: left;
|
|
90
|
+
font-weight: var(--font-medium);
|
|
91
|
+
font-size: var(--text-sm);
|
|
92
|
+
color: var(--color-gray-700);
|
|
93
|
+
background-color: var(--color-gray-50);
|
|
94
|
+
border-bottom: 1px solid var(--color-gray-200);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
th.sortable {
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
user-select: none;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
th.sortable:hover {
|
|
103
|
+
background-color: var(--color-gray-100);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
th.sortable:focus-visible {
|
|
107
|
+
outline: var(--ring-width) solid var(--ring-color);
|
|
108
|
+
outline-offset: calc(-1 * var(--ring-width));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.sort-indicator {
|
|
112
|
+
margin-left: var(--space-1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.empty-cell {
|
|
116
|
+
padding: 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
:global(tbody td) {
|
|
120
|
+
padding: var(--space-2);
|
|
121
|
+
text-align: left;
|
|
122
|
+
font-size: var(--text-sm);
|
|
123
|
+
color: var(--color-gray-900);
|
|
124
|
+
border-bottom: 1px solid var(--color-gray-200);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
:global(tbody tr:hover) {
|
|
128
|
+
background-color: var(--color-gray-50);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
:global(tbody tr.selected) {
|
|
132
|
+
background-color: color-mix(in srgb, var(--color-blue-500) 8%, var(--color-white));
|
|
133
|
+
}
|
|
134
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
message: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
let { message }: Props = $props()
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<div class="empty-state">
|
|
10
|
+
<p class="message">{message}</p>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<style>
|
|
14
|
+
.empty-state {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
padding: var(--space-8);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.message {
|
|
22
|
+
color: var(--color-gray-500);
|
|
23
|
+
font-size: var(--text-sm);
|
|
24
|
+
margin: 0;
|
|
25
|
+
}
|
|
26
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
expanded?: boolean
|
|
6
|
+
onToggle?: () => void
|
|
7
|
+
summary: Snippet
|
|
8
|
+
details: Snippet
|
|
9
|
+
colspan?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
expanded = false,
|
|
14
|
+
onToggle,
|
|
15
|
+
summary,
|
|
16
|
+
details,
|
|
17
|
+
colspan = 1,
|
|
18
|
+
}: Props = $props()
|
|
19
|
+
|
|
20
|
+
function handleKeydown(event: KeyboardEvent): void {
|
|
21
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
22
|
+
event.preventDefault()
|
|
23
|
+
onToggle?.()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<tr
|
|
29
|
+
class="expandable-row"
|
|
30
|
+
class:expanded
|
|
31
|
+
role="button"
|
|
32
|
+
tabindex="0"
|
|
33
|
+
aria-expanded={expanded}
|
|
34
|
+
onclick={() => onToggle?.()}
|
|
35
|
+
onkeydown={handleKeydown}
|
|
36
|
+
>
|
|
37
|
+
{@render summary()}
|
|
38
|
+
</tr>
|
|
39
|
+
{#if expanded}
|
|
40
|
+
<tr class="details-row">
|
|
41
|
+
<td {colspan} class="details-cell">
|
|
42
|
+
{@render details()}
|
|
43
|
+
</td>
|
|
44
|
+
</tr>
|
|
45
|
+
{/if}
|
|
46
|
+
|
|
47
|
+
<style>
|
|
48
|
+
.expandable-row {
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.expandable-row:hover {
|
|
53
|
+
background-color: var(--color-gray-50);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.expandable-row:focus-visible {
|
|
57
|
+
outline: var(--ring-width) solid var(--ring-color);
|
|
58
|
+
outline-offset: calc(-1 * var(--ring-width));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.details-cell {
|
|
62
|
+
background-color: var(--color-gray-50);
|
|
63
|
+
border-left: 3px solid var(--color-blue-500);
|
|
64
|
+
padding: var(--space-3);
|
|
65
|
+
}
|
|
66
|
+
</style>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Container for interactive visualizations (e.g., D3 tree graphs).
|
|
3
|
+
Provides a bordered frame with an optional controls overlay.
|
|
4
|
+
-->
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
import type { Snippet } from 'svelte'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
children: Snippet
|
|
10
|
+
controls?: Snippet
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { children, controls }: Props = $props()
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<div class="visualization">
|
|
17
|
+
{#if controls}
|
|
18
|
+
<div class="controls">
|
|
19
|
+
{@render controls()}
|
|
20
|
+
</div>
|
|
21
|
+
{/if}
|
|
22
|
+
<div class="content">
|
|
23
|
+
{@render children()}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<style>
|
|
28
|
+
.visualization {
|
|
29
|
+
position: relative;
|
|
30
|
+
height: 100%;
|
|
31
|
+
width: 100%;
|
|
32
|
+
border: 1px solid var(--color-gray-300);
|
|
33
|
+
overflow: hidden;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.controls {
|
|
37
|
+
position: absolute;
|
|
38
|
+
right: var(--space-4);
|
|
39
|
+
top: var(--space-4);
|
|
40
|
+
z-index: 10;
|
|
41
|
+
display: flex;
|
|
42
|
+
gap: var(--space-2);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.content {
|
|
46
|
+
height: 100%;
|
|
47
|
+
width: 100%;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.content :global(svg) {
|
|
51
|
+
height: 100%;
|
|
52
|
+
width: 100%;
|
|
53
|
+
background-color: var(--color-white);
|
|
54
|
+
}
|
|
55
|
+
</style>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@paired-component apps/dashboard/src/shared/ui/feedback/banner.tsx
|
|
3
|
+
@design-parity Props, variants, sizes, and visual output must stay in sync
|
|
4
|
+
with the Dashboard React counterpart. When modifying this component, replicate
|
|
5
|
+
the change in the paired React component to maintain visual and behavioral parity.
|
|
6
|
+
-->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import type { Snippet } from 'svelte'
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
children: Snippet
|
|
12
|
+
status?: 'info' | 'success' | 'warning' | 'error'
|
|
13
|
+
title?: string
|
|
14
|
+
onDismiss?: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let { children, status = 'info', title, onDismiss }: Props = $props()
|
|
18
|
+
|
|
19
|
+
const bannerRole = $derived(
|
|
20
|
+
status === 'error' || status === 'warning' ? 'alert' : 'status',
|
|
21
|
+
)
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div class="banner status-{status}" role={bannerRole}>
|
|
25
|
+
<div class="banner-content">
|
|
26
|
+
{#if title}
|
|
27
|
+
<p class="banner-title">{title}</p>
|
|
28
|
+
{/if}
|
|
29
|
+
<div class="banner-body">
|
|
30
|
+
{@render children()}
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
{#if onDismiss}
|
|
34
|
+
<button
|
|
35
|
+
class="dismiss-button"
|
|
36
|
+
onclick={onDismiss}
|
|
37
|
+
aria-label="Dismiss"
|
|
38
|
+
>
|
|
39
|
+
<svg class="dismiss-icon" viewBox="0 0 20 20" fill="currentColor">
|
|
40
|
+
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
|
|
41
|
+
</svg>
|
|
42
|
+
</button>
|
|
43
|
+
{/if}
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<style>
|
|
47
|
+
.banner {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: flex-start;
|
|
50
|
+
gap: var(--space-3);
|
|
51
|
+
border-radius: var(--radius-md);
|
|
52
|
+
border: 1px solid;
|
|
53
|
+
padding: var(--space-4);
|
|
54
|
+
background-color: var(--color-white);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.status-info {
|
|
58
|
+
border-color: #bfdbfe;
|
|
59
|
+
color: var(--color-blue-800, #1e40af);
|
|
60
|
+
}
|
|
61
|
+
.status-success {
|
|
62
|
+
border-color: #bbf7d0;
|
|
63
|
+
color: var(--color-green-800);
|
|
64
|
+
}
|
|
65
|
+
.status-warning {
|
|
66
|
+
border-color: #fef08a;
|
|
67
|
+
color: var(--color-yellow-800);
|
|
68
|
+
}
|
|
69
|
+
.status-error {
|
|
70
|
+
border-color: #fecaca;
|
|
71
|
+
color: var(--color-red-700);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.banner-content {
|
|
75
|
+
flex: 1;
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-direction: column;
|
|
78
|
+
gap: var(--space-1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.banner-title {
|
|
82
|
+
font-weight: var(--font-semibold);
|
|
83
|
+
font-size: var(--text-sm);
|
|
84
|
+
margin: 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.banner-body {
|
|
88
|
+
font-size: var(--text-sm);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.dismiss-button {
|
|
92
|
+
margin-left: var(--space-4);
|
|
93
|
+
border-radius: var(--radius-sm);
|
|
94
|
+
background: none;
|
|
95
|
+
border: none;
|
|
96
|
+
color: currentColor;
|
|
97
|
+
opacity: 0.7;
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
padding: 0;
|
|
100
|
+
transition: opacity 0.15s ease;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.dismiss-button:hover {
|
|
104
|
+
opacity: 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.dismiss-button:focus-visible {
|
|
108
|
+
outline: var(--ring-width) solid currentColor;
|
|
109
|
+
outline-offset: var(--ring-offset);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.dismiss-icon {
|
|
113
|
+
width: 1.25rem;
|
|
114
|
+
height: 1.25rem;
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@paired-component apps/dashboard/src/shared/ui/feedback/spinner.tsx
|
|
3
|
+
@design-parity Props, variants, sizes, and visual output must stay in sync
|
|
4
|
+
with the Dashboard React counterpart. When modifying this component, replicate
|
|
5
|
+
the change in the paired React component to maintain visual and behavioral parity.
|
|
6
|
+
-->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
interface Props {
|
|
9
|
+
size?: 'small' | 'medium' | 'large'
|
|
10
|
+
label?: string
|
|
11
|
+
color?: 'primary' | 'current'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { size = 'medium', label = 'Loading', color = 'primary' }: Props = $props()
|
|
15
|
+
|
|
16
|
+
const dimensions = $derived(
|
|
17
|
+
size === 'small' ? 16 : size === 'large' ? 32 : 24,
|
|
18
|
+
)
|
|
19
|
+
const strokeWidth = $derived(
|
|
20
|
+
size === 'small' ? 2 : size === 'large' ? 4 : 3,
|
|
21
|
+
)
|
|
22
|
+
const r = $derived((dimensions - strokeWidth) / 2)
|
|
23
|
+
const circumference = $derived(2 * Math.PI * r)
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<svg
|
|
27
|
+
class="spinner color-{color}"
|
|
28
|
+
width={dimensions}
|
|
29
|
+
height={dimensions}
|
|
30
|
+
viewBox="0 0 {dimensions} {dimensions}"
|
|
31
|
+
role="progressbar"
|
|
32
|
+
aria-label={label}
|
|
33
|
+
>
|
|
34
|
+
<circle
|
|
35
|
+
class="track"
|
|
36
|
+
cx={dimensions / 2}
|
|
37
|
+
cy={dimensions / 2}
|
|
38
|
+
r={r}
|
|
39
|
+
fill="none"
|
|
40
|
+
stroke="currentColor"
|
|
41
|
+
stroke-width={strokeWidth}
|
|
42
|
+
/>
|
|
43
|
+
<circle
|
|
44
|
+
class="arc"
|
|
45
|
+
cx={dimensions / 2}
|
|
46
|
+
cy={dimensions / 2}
|
|
47
|
+
r={r}
|
|
48
|
+
fill="none"
|
|
49
|
+
stroke="currentColor"
|
|
50
|
+
stroke-width={strokeWidth}
|
|
51
|
+
stroke-dasharray="{circumference * 0.25} {circumference * 0.75}"
|
|
52
|
+
transform="rotate(-90 {dimensions / 2} {dimensions / 2})"
|
|
53
|
+
>
|
|
54
|
+
<animateTransform
|
|
55
|
+
attributeName="transform"
|
|
56
|
+
type="rotate"
|
|
57
|
+
from="0 {dimensions / 2} {dimensions / 2}"
|
|
58
|
+
to="360 {dimensions / 2} {dimensions / 2}"
|
|
59
|
+
dur="1s"
|
|
60
|
+
repeatCount="indefinite"
|
|
61
|
+
/>
|
|
62
|
+
</circle>
|
|
63
|
+
</svg>
|
|
64
|
+
|
|
65
|
+
<style>
|
|
66
|
+
.color-primary {
|
|
67
|
+
color: var(--color-blue-600);
|
|
68
|
+
}
|
|
69
|
+
.color-current {
|
|
70
|
+
color: currentColor;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.track {
|
|
74
|
+
opacity: 0.25;
|
|
75
|
+
}
|
|
76
|
+
.arc {
|
|
77
|
+
opacity: 0.75;
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@paired-component apps/dashboard/src/shared/ui/forms/checkbox.tsx
|
|
3
|
+
@design-parity Props, variants, sizes, and visual output must stay in sync
|
|
4
|
+
with the Dashboard React counterpart. When modifying this component, replicate
|
|
5
|
+
the change in the paired React component to maintain visual and behavioral parity.
|
|
6
|
+
-->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
interface Props {
|
|
9
|
+
label: string
|
|
10
|
+
name: string
|
|
11
|
+
checked?: boolean
|
|
12
|
+
onchange?: (checked: boolean) => void
|
|
13
|
+
helpText?: string
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
label,
|
|
19
|
+
name,
|
|
20
|
+
checked = false,
|
|
21
|
+
onchange,
|
|
22
|
+
helpText,
|
|
23
|
+
disabled = false,
|
|
24
|
+
}: Props = $props()
|
|
25
|
+
|
|
26
|
+
function handleChange(event: Event): void {
|
|
27
|
+
if (event.target instanceof HTMLInputElement) {
|
|
28
|
+
onchange?.(event.target.checked)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div class="checkbox-field">
|
|
34
|
+
<label class="checkbox-label" class:is-disabled={disabled}>
|
|
35
|
+
<span class="checkbox-wrapper">
|
|
36
|
+
<input
|
|
37
|
+
type="checkbox"
|
|
38
|
+
class="checkbox-input"
|
|
39
|
+
{name}
|
|
40
|
+
{checked}
|
|
41
|
+
{disabled}
|
|
42
|
+
onchange={handleChange}
|
|
43
|
+
/>
|
|
44
|
+
<span class="checkbox-box" class:checked aria-hidden="true">
|
|
45
|
+
{#if checked}
|
|
46
|
+
<svg class="checkmark" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2">
|
|
47
|
+
<polyline points="2,6 5,9 10,3" />
|
|
48
|
+
</svg>
|
|
49
|
+
{/if}
|
|
50
|
+
</span>
|
|
51
|
+
</span>
|
|
52
|
+
<span class="label-text">{label}</span>
|
|
53
|
+
</label>
|
|
54
|
+
{#if helpText}
|
|
55
|
+
<p class="help-text">{helpText}</p>
|
|
56
|
+
{/if}
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<style>
|
|
60
|
+
.checkbox-field {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: var(--space-1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.checkbox-label {
|
|
67
|
+
display: flex;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: var(--space-2);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.checkbox-label.is-disabled {
|
|
74
|
+
cursor: not-allowed;
|
|
75
|
+
opacity: 0.5;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.checkbox-wrapper {
|
|
79
|
+
position: relative;
|
|
80
|
+
width: 1rem;
|
|
81
|
+
height: 1rem;
|
|
82
|
+
flex-shrink: 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.checkbox-input {
|
|
86
|
+
position: absolute;
|
|
87
|
+
inset: 0;
|
|
88
|
+
z-index: 10;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
opacity: 0;
|
|
91
|
+
margin: 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.checkbox-input:disabled {
|
|
95
|
+
cursor: not-allowed;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.checkbox-box {
|
|
99
|
+
pointer-events: none;
|
|
100
|
+
position: absolute;
|
|
101
|
+
inset: 0;
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
justify-content: center;
|
|
105
|
+
border-radius: var(--radius-sm);
|
|
106
|
+
border: 1px solid var(--color-gray-300);
|
|
107
|
+
background-color: var(--color-white);
|
|
108
|
+
transition: background-color 0.15s ease, border-color 0.15s ease;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.checkbox-box.checked {
|
|
112
|
+
border-color: var(--color-blue-600);
|
|
113
|
+
background-color: var(--color-blue-600);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.checkbox-input:focus-visible + .checkbox-box {
|
|
117
|
+
outline: var(--ring-width) solid var(--ring-color);
|
|
118
|
+
outline-offset: var(--ring-offset);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.checkmark {
|
|
122
|
+
width: 0.75rem;
|
|
123
|
+
height: 0.75rem;
|
|
124
|
+
color: var(--color-white);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.label-text {
|
|
128
|
+
font-size: var(--text-sm);
|
|
129
|
+
font-weight: var(--font-medium);
|
|
130
|
+
color: var(--color-gray-900);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.help-text {
|
|
134
|
+
margin: 0;
|
|
135
|
+
margin-left: 1.5rem;
|
|
136
|
+
font-size: var(--text-sm);
|
|
137
|
+
color: var(--color-gray-500);
|
|
138
|
+
}
|
|
139
|
+
</style>
|