@aiaiai-pt/design-system 0.1.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.
Files changed (52) hide show
  1. package/components/Alert.svelte +100 -0
  2. package/components/Badge.svelte +108 -0
  3. package/components/BottomNav.svelte +37 -0
  4. package/components/BottomNavItem.svelte +121 -0
  5. package/components/Button.svelte +269 -0
  6. package/components/Card.svelte +108 -0
  7. package/components/Checkbox.svelte +138 -0
  8. package/components/CodeBlock.svelte +187 -0
  9. package/components/CodeEditor.svelte +221 -0
  10. package/components/CollapsibleSection.svelte +160 -0
  11. package/components/Combobox.svelte +396 -0
  12. package/components/EmptyState.svelte +148 -0
  13. package/components/FileUpload.svelte +280 -0
  14. package/components/FileUploadItem.svelte +222 -0
  15. package/components/Input.svelte +222 -0
  16. package/components/KeyValue.svelte +79 -0
  17. package/components/Label.svelte +49 -0
  18. package/components/List.svelte +70 -0
  19. package/components/ListItem.svelte +125 -0
  20. package/components/Menu.svelte +161 -0
  21. package/components/MenuItem.svelte +120 -0
  22. package/components/MenuSeparator.svelte +34 -0
  23. package/components/Modal.svelte +260 -0
  24. package/components/OptionGrid.svelte +195 -0
  25. package/components/Panel.svelte +256 -0
  26. package/components/Popover.svelte +194 -0
  27. package/components/Progress.svelte +78 -0
  28. package/components/Select.svelte +182 -0
  29. package/components/Separator.svelte +47 -0
  30. package/components/Sidebar.svelte +106 -0
  31. package/components/SidebarItem.svelte +154 -0
  32. package/components/SidebarSection.svelte +43 -0
  33. package/components/Skeleton.svelte +79 -0
  34. package/components/Status.svelte +104 -0
  35. package/components/Stepper.svelte +142 -0
  36. package/components/Tab.svelte +94 -0
  37. package/components/TabList.svelte +36 -0
  38. package/components/TabPanel.svelte +45 -0
  39. package/components/Tabs.svelte +46 -0
  40. package/components/Tag.svelte +96 -0
  41. package/components/Textarea.svelte +143 -0
  42. package/components/Toast.svelte +114 -0
  43. package/components/Toggle.svelte +132 -0
  44. package/components/index.js +70 -0
  45. package/package.json +45 -0
  46. package/tokens/base.css +175 -0
  47. package/tokens/components.css +530 -0
  48. package/tokens/semantic.css +211 -0
  49. package/tokens/themes/aiaiai.css +53 -0
  50. package/tokens/themes/bespoke-example.css +148 -0
  51. package/tokens/themes/branded-example.css +55 -0
  52. package/tokens/utilities.css +1865 -0
@@ -0,0 +1,154 @@
1
+ <!--
2
+ @component SidebarItem
3
+
4
+ Navigation item for use inside Sidebar.
5
+ Reads collapsed state from Sidebar context — hides label and badge when collapsed.
6
+ Consumes --nav-item-* tokens from components.css.
7
+
8
+ @example Button
9
+ <SidebarItem active onclick={() => navigate('/')}>
10
+ {#snippet icon()}<svg>...</svg>{/snippet}
11
+ DASHBOARD
12
+ </SidebarItem>
13
+
14
+ @example Link
15
+ <SidebarItem href="/settings">
16
+ {#snippet icon()}<svg>...</svg>{/snippet}
17
+ SETTINGS
18
+ </SidebarItem>
19
+
20
+ @example With badge
21
+ <SidebarItem href="/inbox" badge={3}>
22
+ {#snippet icon()}<svg>...</svg>{/snippet}
23
+ INBOX
24
+ </SidebarItem>
25
+ -->
26
+ <script>
27
+ import { getContext } from 'svelte';
28
+
29
+ let {
30
+ /** @type {boolean} */
31
+ active = false,
32
+ /** @type {boolean} */
33
+ disabled = false,
34
+ /** @type {string | undefined} */
35
+ href = undefined,
36
+ /** @type {string | number | undefined} */
37
+ badge = undefined,
38
+ /** @type {string} */
39
+ class: className = '',
40
+ /** @type {import('svelte').Snippet | undefined} */
41
+ icon = undefined,
42
+ /** @type {import('svelte').Snippet | undefined} */
43
+ children = undefined,
44
+ ...rest
45
+ } = $props();
46
+
47
+ const sidebar = getContext('aiaiai-sidebar');
48
+ const isCollapsed = $derived(sidebar?.collapsed ?? false);
49
+ </script>
50
+
51
+ {#if href && !disabled}
52
+ <a
53
+ {href}
54
+ class="nav-item {className}"
55
+ class:nav-item-active={active}
56
+ aria-current={active ? 'page' : undefined}
57
+ {...rest}
58
+ >
59
+ {#if icon}<span class="nav-icon-wrap">{@render icon()}</span>{/if}
60
+ {#if !isCollapsed}
61
+ {#if children}<span class="nav-item-label">{@render children()}</span>{/if}
62
+ {#if badge != null}<span class="nav-badge">{badge}</span>{/if}
63
+ {/if}
64
+ </a>
65
+ {:else}
66
+ <button
67
+ class="nav-item {className}"
68
+ class:nav-item-active={active}
69
+ class:nav-item-disabled={disabled}
70
+ {disabled}
71
+ aria-current={active ? 'page' : undefined}
72
+ {...rest}
73
+ >
74
+ {#if icon}<span class="nav-icon-wrap">{@render icon()}</span>{/if}
75
+ {#if !isCollapsed}
76
+ {#if children}<span class="nav-item-label">{@render children()}</span>{/if}
77
+ {#if badge != null}<span class="nav-badge">{badge}</span>{/if}
78
+ {/if}
79
+ </button>
80
+ {/if}
81
+
82
+ <style>
83
+ .nav-item {
84
+ all: unset;
85
+ display: flex;
86
+ align-items: center;
87
+ gap: var(--space-sm);
88
+ height: var(--nav-item-height);
89
+ padding: 0 var(--nav-item-padding-x);
90
+ border-radius: var(--nav-item-radius);
91
+ font-family: var(--nav-item-font);
92
+ font-size: var(--nav-item-font-size);
93
+ letter-spacing: var(--nav-item-tracking);
94
+ color: var(--nav-item-color);
95
+ cursor: pointer;
96
+ transition: all var(--nav-item-transition);
97
+ text-decoration: none;
98
+ box-sizing: border-box;
99
+ }
100
+
101
+ .nav-item:hover {
102
+ color: var(--nav-item-color-hover);
103
+ background: var(--nav-item-bg-hover);
104
+ }
105
+
106
+ .nav-item:focus-visible {
107
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
108
+ outline-offset: var(--focus-ring-offset);
109
+ }
110
+
111
+ .nav-item-active {
112
+ color: var(--nav-item-color-active);
113
+ background: var(--nav-item-bg-active);
114
+ }
115
+
116
+ .nav-item-disabled {
117
+ opacity: 0.5;
118
+ cursor: not-allowed;
119
+ pointer-events: none;
120
+ }
121
+
122
+ .nav-icon-wrap {
123
+ display: flex;
124
+ flex-shrink: 0;
125
+ width: 16px;
126
+ height: 16px;
127
+ }
128
+
129
+ .nav-icon-wrap :global(svg) {
130
+ width: 100%;
131
+ height: 100%;
132
+ }
133
+
134
+ .nav-item-label {
135
+ white-space: nowrap;
136
+ overflow: hidden;
137
+ text-overflow: ellipsis;
138
+ }
139
+
140
+ .nav-badge {
141
+ font-family: var(--type-data-font);
142
+ font-size: var(--type-caption-size);
143
+ background: var(--color-accent);
144
+ color: var(--color-text-on-accent);
145
+ border-radius: var(--radius-pill);
146
+ padding: 0 var(--space-xs);
147
+ min-width: 18px;
148
+ height: 18px;
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: center;
152
+ margin-left: auto;
153
+ }
154
+ </style>
@@ -0,0 +1,43 @@
1
+ <!--
2
+ @component SidebarSection
3
+
4
+ Section header for grouping sidebar items.
5
+ Hidden when Sidebar is collapsed (reads context).
6
+ Consumes --nav-section-* tokens from components.css.
7
+
8
+ @example
9
+ <SidebarSection title="WORKSPACE" />
10
+ -->
11
+ <script>
12
+ import { getContext } from 'svelte';
13
+
14
+ let {
15
+ /** @type {string} */
16
+ title,
17
+ /** @type {string} */
18
+ class: className = '',
19
+ ...rest
20
+ } = $props();
21
+
22
+ const sidebar = getContext('aiaiai-sidebar');
23
+ const isCollapsed = $derived(sidebar?.collapsed ?? false);
24
+ </script>
25
+
26
+ {#if !isCollapsed}
27
+ <span class="nav-section {className}" {...rest}>{title}</span>
28
+ {/if}
29
+
30
+ <style>
31
+ .nav-section {
32
+ font-family: var(--nav-section-font);
33
+ font-size: var(--nav-section-size);
34
+ letter-spacing: var(--nav-section-tracking);
35
+ color: var(--nav-section-color);
36
+ padding: var(--space-sm) var(--space-sm) var(--space-xs);
37
+ margin-top: var(--nav-section-margin-top);
38
+ }
39
+
40
+ .nav-section:first-child {
41
+ margin-top: 0;
42
+ }
43
+ </style>
@@ -0,0 +1,79 @@
1
+ <!--
2
+ @component Skeleton
3
+
4
+ Loading placeholder with shimmer animation.
5
+ Consumes --skeleton-* tokens from components.css.
6
+
7
+ @example Text line
8
+ <Skeleton width="80%" height="16px" />
9
+
10
+ @example Circle (avatar)
11
+ <Skeleton width="40px" height="40px" circle />
12
+
13
+ @example Card
14
+ <Skeleton width="100%" height="120px" radius="var(--radius-md)" />
15
+ -->
16
+ <script>
17
+ let {
18
+ /** @type {string} */
19
+ width = '100%',
20
+ /** @type {string} */
21
+ height = '16px',
22
+ /** @type {boolean} */
23
+ circle = false,
24
+ /** @type {string | undefined} */
25
+ radius = undefined,
26
+ /** @type {string} */
27
+ class: className = '',
28
+ ...rest
29
+ } = $props();
30
+
31
+ const borderRadius = $derived(circle
32
+ ? 'var(--radius-circle)'
33
+ : radius ?? 'var(--skeleton-radius)');
34
+ </script>
35
+
36
+ <div
37
+ class="skeleton {className}"
38
+ style:width={width}
39
+ style:height={height}
40
+ style:border-radius={borderRadius}
41
+ aria-hidden="true"
42
+ {...rest}
43
+ ></div>
44
+
45
+ <style>
46
+ .skeleton {
47
+ background: var(--skeleton-bg);
48
+ position: relative;
49
+ overflow: hidden;
50
+ }
51
+
52
+ .skeleton::after {
53
+ content: '';
54
+ position: absolute;
55
+ top: 0;
56
+ left: -100%;
57
+ width: 100%;
58
+ height: 100%;
59
+ background: linear-gradient(
60
+ 90deg,
61
+ transparent,
62
+ var(--skeleton-shine),
63
+ transparent
64
+ );
65
+ animation: shimmer var(--skeleton-duration) infinite;
66
+ }
67
+
68
+ @keyframes shimmer {
69
+ 100% {
70
+ left: 100%;
71
+ }
72
+ }
73
+
74
+ @media (prefers-reduced-motion: reduce) {
75
+ .skeleton::after {
76
+ animation: none;
77
+ }
78
+ }
79
+ </style>
@@ -0,0 +1,104 @@
1
+ <!--
2
+ @component Status
3
+
4
+ Colored indicator dot with label. Used for real-time status, health, presence.
5
+ Consumes --status-* tokens from components.css.
6
+
7
+ Dot shapes encode meaning: circle=success, triangle=warning, diamond=error, ring=inactive.
8
+
9
+ @example
10
+ <Status variant="success">ACTIVE</Status>
11
+
12
+ @example Pulsing
13
+ <Status variant="warning" pulse>DEGRADED</Status>
14
+ -->
15
+ <script>
16
+ /**
17
+ * @typedef {'success' | 'warning' | 'error' | 'inactive'} Variant
18
+ */
19
+
20
+ let {
21
+ /** @type {Variant} */
22
+ variant = 'success',
23
+ /** @type {boolean} */
24
+ pulse = false,
25
+ /** @type {string} */
26
+ class: className = '',
27
+ /** @type {import('svelte').Snippet | undefined} */
28
+ children = undefined,
29
+ ...rest
30
+ } = $props();
31
+
32
+ /** @type {Record<string, string>} */
33
+ const colorMap = {
34
+ success: 'var(--color-success)',
35
+ warning: 'var(--color-warning)',
36
+ error: 'var(--color-destructive)',
37
+ inactive: 'var(--color-text-muted)',
38
+ };
39
+ </script>
40
+
41
+ <span class="status {className}" {...rest}>
42
+ <span
43
+ class="status-dot status-dot-{variant}"
44
+ class:status-dot-pulse={pulse}
45
+ style:background={variant !== 'inactive' ? colorMap[variant] : undefined}
46
+ style:color={variant === 'inactive' ? colorMap[variant] : undefined}
47
+ aria-hidden="true"
48
+ ></span>
49
+ {#if children}
50
+ <span class="status-label">{@render children()}</span>
51
+ {/if}
52
+ </span>
53
+
54
+ <style>
55
+ .status {
56
+ display: inline-flex;
57
+ align-items: center;
58
+ gap: var(--status-gap);
59
+ }
60
+
61
+ .status-dot {
62
+ width: var(--status-dot-size);
63
+ height: var(--status-dot-size);
64
+ border-radius: var(--radius-circle);
65
+ flex-shrink: 0;
66
+ }
67
+
68
+ /* Shape variants: progressive enhancement over base circle */
69
+ .status-dot-warning {
70
+ border-radius: 0;
71
+ clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
72
+ }
73
+
74
+ .status-dot-error {
75
+ border-radius: 0;
76
+ clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
77
+ }
78
+
79
+ .status-dot-inactive {
80
+ background: transparent !important;
81
+ box-shadow: inset 0 0 0 2px currentColor;
82
+ }
83
+
84
+ .status-dot-pulse {
85
+ animation: pulse 2s ease-in-out infinite;
86
+ }
87
+
88
+ @keyframes pulse {
89
+ 0%, 100% { opacity: 1; }
90
+ 50% { opacity: 0.4; }
91
+ }
92
+
93
+ @media (prefers-reduced-motion: reduce) {
94
+ .status-dot-pulse {
95
+ animation: none;
96
+ }
97
+ }
98
+
99
+ .status-label {
100
+ font-family: var(--status-font);
101
+ font-size: var(--status-size);
102
+ letter-spacing: var(--type-label-tracking);
103
+ }
104
+ </style>
@@ -0,0 +1,142 @@
1
+ <!--
2
+ @component Stepper
3
+
4
+ Horizontal step indicator for multi-step flows.
5
+ Consumes --stepper-* tokens from components.css.
6
+
7
+ @example
8
+ <Stepper steps={[
9
+ { label: 'Source', status: 'complete' },
10
+ { label: 'Configure', status: 'active' },
11
+ { label: 'Review', status: 'upcoming' },
12
+ ]} />
13
+ -->
14
+ <script>
15
+ /**
16
+ * @typedef {'complete' | 'active' | 'upcoming'} StepStatus
17
+ * @typedef {{ label: string, status: StepStatus }} Step
18
+ */
19
+
20
+ let {
21
+ /** @type {Step[]} */
22
+ steps = [],
23
+ /** @type {string} */
24
+ class: className = '',
25
+ ...rest
26
+ } = $props();
27
+ </script>
28
+
29
+ <nav class="stepper {className}" aria-label="Progress" {...rest}>
30
+ {#each steps as step, i}
31
+ {#if i > 0}
32
+ <div
33
+ class="stepper-line"
34
+ class:stepper-line-complete={step.status === 'complete' || step.status === 'active'}
35
+ ></div>
36
+ {/if}
37
+
38
+ <div class="stepper-step" aria-current={step.status === 'active' ? 'step' : undefined}>
39
+ <div
40
+ class="stepper-dot"
41
+ class:stepper-dot-active={step.status === 'active'}
42
+ class:stepper-dot-complete={step.status === 'complete'}
43
+ >
44
+ {#if step.status === 'complete'}
45
+ <svg class="stepper-check" viewBox="0 0 12 12" fill="none" aria-hidden="true">
46
+ <path d="M2.5 6l2.5 2.5 4.5-5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
47
+ </svg>
48
+ {:else}
49
+ <span class="stepper-number">{i + 1}</span>
50
+ {/if}
51
+ </div>
52
+
53
+ <span
54
+ class="stepper-label"
55
+ class:stepper-label-active={step.status === 'active'}
56
+ class:stepper-label-complete={step.status === 'complete'}
57
+ >{step.label}</span>
58
+ </div>
59
+ {/each}
60
+ </nav>
61
+
62
+ <style>
63
+ .stepper {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: var(--stepper-gap);
67
+ }
68
+
69
+ .stepper-step {
70
+ display: flex;
71
+ flex-direction: column;
72
+ align-items: center;
73
+ gap: var(--stepper-gap);
74
+ flex-shrink: 0;
75
+ }
76
+
77
+ .stepper-dot {
78
+ width: var(--stepper-dot-size);
79
+ height: var(--stepper-dot-size);
80
+ border-radius: var(--radius-circle);
81
+ background: var(--stepper-dot-bg);
82
+ border: var(--stepper-dot-border);
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ transition: all var(--stepper-transition);
87
+ }
88
+
89
+ .stepper-dot-active {
90
+ background: var(--stepper-active-bg);
91
+ color: var(--stepper-active-color);
92
+ }
93
+
94
+ .stepper-dot-complete {
95
+ background: var(--stepper-complete-bg);
96
+ color: var(--stepper-complete-color);
97
+ }
98
+
99
+ .stepper-number {
100
+ font-family: var(--stepper-dot-font);
101
+ font-size: var(--stepper-dot-size-text);
102
+ letter-spacing: var(--stepper-dot-tracking);
103
+ color: var(--stepper-dot-color);
104
+ }
105
+
106
+ .stepper-dot-active .stepper-number {
107
+ color: var(--stepper-active-color);
108
+ }
109
+
110
+ .stepper-check {
111
+ width: 12px;
112
+ height: 12px;
113
+ }
114
+
115
+ .stepper-label {
116
+ font-family: var(--stepper-label-font);
117
+ font-size: var(--stepper-label-size);
118
+ letter-spacing: var(--stepper-label-tracking);
119
+ color: var(--stepper-label-color);
120
+ white-space: nowrap;
121
+ }
122
+
123
+ .stepper-label-active {
124
+ color: var(--stepper-label-active-color);
125
+ }
126
+
127
+ .stepper-label-complete {
128
+ color: var(--stepper-label-active-color);
129
+ }
130
+
131
+ .stepper-line {
132
+ flex: 1;
133
+ height: var(--stepper-line-width);
134
+ background: var(--stepper-line-color);
135
+ min-width: 24px;
136
+ margin-bottom: calc(var(--stepper-label-size) + var(--stepper-gap));
137
+ }
138
+
139
+ .stepper-line-complete {
140
+ background: var(--stepper-complete-bg);
141
+ }
142
+ </style>
@@ -0,0 +1,94 @@
1
+ <!--
2
+ @component Tab
3
+
4
+ Individual tab trigger inside a TabList.
5
+ Reads active state from Tabs context.
6
+ Consumes --tabs-trigger-* tokens from components.css.
7
+
8
+ @example
9
+ <Tab value="overview">OVERVIEW</Tab>
10
+
11
+ @example Disabled
12
+ <Tab value="admin" disabled>ADMIN</Tab>
13
+ -->
14
+ <script>
15
+ import { getContext } from 'svelte';
16
+
17
+ let {
18
+ /** @type {string} */
19
+ value,
20
+ /** @type {boolean} */
21
+ disabled = false,
22
+ /** @type {string} */
23
+ class: className = '',
24
+ /** @type {import('svelte').Snippet | undefined} */
25
+ children = undefined,
26
+ ...rest
27
+ } = $props();
28
+
29
+ /** @type {{ value: string, setValue: (v: string) => void } | undefined} */
30
+ const tabs = getContext('tabs');
31
+ const isActive = $derived(tabs?.value === value);
32
+
33
+ function handleClick() {
34
+ if (disabled) return;
35
+ if (!tabs) {
36
+ console.warn('[aiaiai] <Tab> used outside <Tabs> — wrap in a <Tabs> parent.');
37
+ return;
38
+ }
39
+ tabs.setValue(value);
40
+ }
41
+ </script>
42
+
43
+ <button
44
+ type="button"
45
+ role="tab"
46
+ aria-selected={isActive}
47
+ {disabled}
48
+ class="tab {className}"
49
+ class:tab-active={isActive}
50
+ onclick={handleClick}
51
+ {...rest}
52
+ >
53
+ {#if children}{@render children()}{/if}
54
+ </button>
55
+
56
+ <style>
57
+ .tab {
58
+ all: unset;
59
+ box-sizing: border-box;
60
+ display: inline-flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ height: var(--tabs-trigger-height);
64
+ padding: 0 var(--tabs-trigger-padding-x);
65
+ border-radius: var(--tabs-trigger-radius);
66
+ font-family: var(--tabs-trigger-font);
67
+ font-size: var(--tabs-trigger-size);
68
+ letter-spacing: var(--tabs-trigger-tracking);
69
+ color: var(--tabs-trigger-color);
70
+ cursor: pointer;
71
+ transition: all var(--tabs-trigger-transition);
72
+ white-space: nowrap;
73
+ }
74
+
75
+ .tab:hover:not(:disabled) {
76
+ color: var(--color-text);
77
+ }
78
+
79
+ .tab:focus-visible {
80
+ outline: var(--focus-ring-width) solid var(--focus-ring-color);
81
+ outline-offset: var(--focus-ring-offset);
82
+ }
83
+
84
+ .tab:disabled {
85
+ opacity: 0.5;
86
+ cursor: not-allowed;
87
+ }
88
+
89
+ .tab-active {
90
+ color: var(--tabs-trigger-color-active);
91
+ background: var(--tabs-trigger-bg-active);
92
+ box-shadow: var(--tabs-trigger-shadow-active);
93
+ }
94
+ </style>
@@ -0,0 +1,36 @@
1
+ <!--
2
+ @component TabList
3
+
4
+ Horizontal bar containing Tab triggers.
5
+ Consumes --tabs-list-* tokens from components.css.
6
+
7
+ @example
8
+ <TabList>
9
+ <Tab value="one">ONE</Tab>
10
+ <Tab value="two">TWO</Tab>
11
+ </TabList>
12
+ -->
13
+ <script>
14
+ let {
15
+ /** @type {string} */
16
+ class: className = '',
17
+ /** @type {import('svelte').Snippet | undefined} */
18
+ children = undefined,
19
+ ...rest
20
+ } = $props();
21
+ </script>
22
+
23
+ <div class="tab-list {className}" role="tablist" {...rest}>
24
+ {#if children}{@render children()}{/if}
25
+ </div>
26
+
27
+ <style>
28
+ .tab-list {
29
+ display: inline-flex;
30
+ align-items: center;
31
+ background: var(--tabs-list-bg);
32
+ border-radius: var(--tabs-list-radius);
33
+ padding: var(--tabs-list-padding);
34
+ gap: var(--space-2xs);
35
+ }
36
+ </style>
@@ -0,0 +1,45 @@
1
+ <!--
2
+ @component TabPanel
3
+
4
+ Content panel shown when its matching Tab is active.
5
+ Reads active state from Tabs context.
6
+ Consumes --tabs-content-* tokens from components.css.
7
+
8
+ @example
9
+ <TabPanel value="overview">
10
+ Overview content goes here.
11
+ </TabPanel>
12
+ -->
13
+ <script>
14
+ import { getContext } from 'svelte';
15
+
16
+ let {
17
+ /** @type {string} */
18
+ value,
19
+ /** @type {string} */
20
+ class: className = '',
21
+ /** @type {import('svelte').Snippet | undefined} */
22
+ children = undefined,
23
+ ...rest
24
+ } = $props();
25
+
26
+ /** @type {{ value: string, setValue: (v: string) => void } | undefined} */
27
+ const tabs = getContext('tabs');
28
+ const isActive = $derived(tabs?.value === value);
29
+ </script>
30
+
31
+ {#if isActive}
32
+ <div
33
+ role="tabpanel"
34
+ class="tab-panel {className}"
35
+ {...rest}
36
+ >
37
+ {#if children}{@render children()}{/if}
38
+ </div>
39
+ {/if}
40
+
41
+ <style>
42
+ .tab-panel {
43
+ padding-top: var(--tabs-content-padding-top);
44
+ }
45
+ </style>