sveltacular 1.0.10 → 1.0.13

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.
@@ -0,0 +1,201 @@
1
+ <script lang="ts">
2
+ import { roundToDecimals } from '../../helpers/round-to-decimals.js';
3
+ import { uniqueId } from '../../helpers/unique-id.js';
4
+ import FormField from '../form-field/form-field.svelte';
5
+ import type { FormFieldSizeOptions } from '../../types/form.js';
6
+
7
+ const baseId = uniqueId();
8
+
9
+ // Generate unique IDs for each dimension input
10
+ function getDimensionId(index: number) {
11
+ return `${baseId}-dimension-${index}`;
12
+ }
13
+
14
+ let {
15
+ dimensions = ['Width', 'Depth', 'Height'] as string[],
16
+ value = $bindable([] as (number | null)[]),
17
+ minAllowed = 0,
18
+ maxAllowed = 99999,
19
+ step = 1,
20
+ decimals = 0,
21
+ prefix = null as string | null,
22
+ suffix = null as string | null,
23
+ required = false,
24
+ size = 'full' as FormFieldSizeOptions,
25
+ label = undefined as string | undefined,
26
+ onChange = undefined as ((value: (number | null)[]) => void) | undefined
27
+ }: {
28
+ dimensions?: string[];
29
+ value?: (number | null)[];
30
+ minAllowed?: number;
31
+ maxAllowed?: number;
32
+ step?: number;
33
+ decimals?: number;
34
+ prefix?: string | null;
35
+ suffix?: string | null;
36
+ required?: boolean;
37
+ size?: FormFieldSizeOptions;
38
+ label?: string;
39
+ onChange?: ((value: (number | null)[]) => void) | undefined;
40
+ } = $props();
41
+
42
+ // Ensure value array matches dimensions array length
43
+ $effect(() => {
44
+ if (dimensions.length > 0 && value.length !== dimensions.length) {
45
+ const newValue = Array(dimensions.length)
46
+ .fill(null)
47
+ .map((_, i) => (i < value.length && value[i] !== undefined ? value[i] : null));
48
+ value = newValue;
49
+ }
50
+ });
51
+
52
+ const handleChange = (index: number) => {
53
+ const currentValue = value[index];
54
+
55
+ if (currentValue === null || currentValue === undefined) {
56
+ onChange?.(value);
57
+ return;
58
+ }
59
+
60
+ // Round to decimals
61
+ value[index] = roundToDecimals(currentValue, decimals);
62
+
63
+ // Ensure value is within allowed range
64
+ if (value[index]! < minAllowed) {
65
+ value[index] = minAllowed;
66
+ }
67
+ if (value[index]! > maxAllowed) {
68
+ value[index] = maxAllowed;
69
+ }
70
+
71
+ onChange?.(value);
72
+ };
73
+
74
+ const onInput = (e: Event, index: number) => {
75
+ const input = e.target as HTMLInputElement;
76
+ const newValue = parseFloat(input.value);
77
+ if (isNaN(newValue)) {
78
+ value[index] = null;
79
+ return;
80
+ }
81
+ value[index] = newValue;
82
+ };
83
+
84
+ // Don't allow certain characters to be typed into the input
85
+ const onKeyPress = (e: KeyboardEvent) => {
86
+ const isNumber = !isNaN(Number(e.key));
87
+ const isDecimal = e.key === '.';
88
+ const allowDecimals = decimals > 0;
89
+ const isAllowed =
90
+ isNumber ||
91
+ isDecimal ||
92
+ ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', '-'].includes(e.key);
93
+ if (!isAllowed) return e.preventDefault();
94
+ if (isDecimal && !allowDecimals) return e.preventDefault();
95
+ };
96
+
97
+ const getPlaceholder = (dimension: string) => {
98
+ return dimension;
99
+ };
100
+ </script>
101
+
102
+ <FormField {size} {label} id={getDimensionId(0)} {required}>
103
+ <div class="dimension-inputs">
104
+ {#each dimensions as dimension, index}
105
+ <div class="input-group">
106
+ <div class="input">
107
+ {#if prefix}
108
+ <span class="prefix">{prefix}</span>
109
+ {/if}
110
+ <input
111
+ id={getDimensionId(index)}
112
+ type="number"
113
+ placeholder={getPlaceholder(dimension)}
114
+ min={minAllowed}
115
+ max={maxAllowed}
116
+ {step}
117
+ bind:value={value[index]}
118
+ onchange={() => handleChange(index)}
119
+ oninput={(e) => onInput(e, index)}
120
+ onkeypress={onKeyPress}
121
+ {required}
122
+ />
123
+ {#if suffix}
124
+ <span class="suffix">{suffix}</span>
125
+ {/if}
126
+ </div>
127
+ </div>
128
+ {#if index < dimensions.length - 1}
129
+ <span class="separator">×</span>
130
+ {/if}
131
+ {/each}
132
+ </div>
133
+ </FormField>
134
+
135
+ <style>.dimension-inputs {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: var(--spacing-md);
139
+ width: 100%;
140
+ }
141
+
142
+ .input-group {
143
+ flex: 1;
144
+ }
145
+
146
+ .separator {
147
+ font-size: var(--font-lg);
148
+ font-weight: 500;
149
+ color: var(--form-input-fg);
150
+ user-select: none;
151
+ flex-shrink: 0;
152
+ }
153
+
154
+ .input {
155
+ display: flex;
156
+ align-items: center;
157
+ justify-content: flex-start;
158
+ position: relative;
159
+ width: 100%;
160
+ height: 100%;
161
+ border-radius: var(--radius-md);
162
+ border: var(--border-thin) solid var(--form-input-border);
163
+ background-color: var(--form-input-bg);
164
+ color: var(--form-input-fg);
165
+ font-size: var(--font-md);
166
+ font-weight: 500;
167
+ line-height: 2rem;
168
+ transition: background-color var(--transition-base) var(--ease-in-out), border-color var(--transition-base) var(--ease-in-out), color var(--transition-base) var(--ease-in-out), fill var(--transition-base) var(--ease-in-out), stroke var(--transition-base) var(--ease-in-out);
169
+ user-select: none;
170
+ white-space: nowrap;
171
+ }
172
+ .input input {
173
+ background-color: transparent;
174
+ border: none;
175
+ line-height: 2rem;
176
+ font-size: var(--font-md);
177
+ width: 100%;
178
+ flex-grow: 1;
179
+ padding-left: var(--spacing-base);
180
+ }
181
+ .input input:focus {
182
+ outline: none;
183
+ }
184
+ .input input::placeholder {
185
+ color: var(--form-input-placeholder);
186
+ }
187
+ .input .prefix,
188
+ .input .suffix {
189
+ font-size: var(--font-md);
190
+ line-height: 2rem;
191
+ padding-left: var(--spacing-base);
192
+ padding-right: var(--spacing-base);
193
+ background-color: var(--form-input-accent-bg);
194
+ color: var(--form-input-accent-fg);
195
+ }
196
+ .input .prefix {
197
+ border-right: var(--border-thin) solid var(--form-input-border);
198
+ }
199
+ .input .suffix {
200
+ border-left: var(--border-thin) solid var(--form-input-border);
201
+ }</style>
@@ -0,0 +1,18 @@
1
+ import type { FormFieldSizeOptions } from '../../types/form.js';
2
+ type $$ComponentProps = {
3
+ dimensions?: string[];
4
+ value?: (number | null)[];
5
+ minAllowed?: number;
6
+ maxAllowed?: number;
7
+ step?: number;
8
+ decimals?: number;
9
+ prefix?: string | null;
10
+ suffix?: string | null;
11
+ required?: boolean;
12
+ size?: FormFieldSizeOptions;
13
+ label?: string;
14
+ onChange?: ((value: (number | null)[]) => void) | undefined;
15
+ };
16
+ declare const DimensionBox: import("svelte").Component<$$ComponentProps, {}, "value">;
17
+ type DimensionBox = ReturnType<typeof DimensionBox>;
18
+ export default DimensionBox;
@@ -1,6 +1,7 @@
1
1
  export { default as Button } from './button/button.svelte';
2
2
  export { default as BoolBox } from './bool-box/bool-box.svelte';
3
3
  export { default as DateBox } from './date-box/date-box.svelte';
4
+ export { default as DimensionBox } from './dimension-box/dimension-box.svelte';
4
5
  export { default as FileArea } from './file-area/file-area.svelte';
5
6
  export { default as FileBox } from './file-box/file-box.svelte';
6
7
  export { default as InfoBox } from './info-box/info-box.svelte';
@@ -2,6 +2,7 @@
2
2
  export { default as Button } from './button/button.svelte';
3
3
  export { default as BoolBox } from './bool-box/bool-box.svelte';
4
4
  export { default as DateBox } from './date-box/date-box.svelte';
5
+ export { default as DimensionBox } from './dimension-box/dimension-box.svelte';
5
6
  export { default as FileArea } from './file-area/file-area.svelte';
6
7
  export { default as FileBox } from './file-box/file-box.svelte';
7
8
  export { default as InfoBox } from './info-box/info-box.svelte';
@@ -193,7 +193,6 @@
193
193
  width: 100%;
194
194
  flex-grow: 1;
195
195
  padding-left: var(--spacing-base);
196
- padding-right: var(--spacing-base);
197
196
  }
198
197
  .input input:focus {
199
198
  outline: none;
@@ -21,6 +21,7 @@
21
21
  helperText = undefined,
22
22
  feedback = undefined,
23
23
  isLoading = false,
24
+ showCharacterCount = false,
24
25
  maxlength = undefined,
25
26
  minlength = undefined,
26
27
  pattern = undefined
@@ -39,6 +40,7 @@
39
40
  helperText?: string;
40
41
  feedback?: FormFieldFeedback;
41
42
  isLoading?: boolean;
43
+ showCharacterCount?: boolean;
42
44
  maxlength?: number | undefined;
43
45
  minlength?: number | undefined;
44
46
  pattern?: string | undefined;
@@ -46,6 +48,16 @@
46
48
 
47
49
  let textareaElement: HTMLTextAreaElement | null = $state(null);
48
50
 
51
+ // Character count functionality
52
+ let characterCount = $derived((value || '').length);
53
+ let characterLimitClass = $derived(
54
+ maxlength && characterCount > maxlength * 0.9
55
+ ? characterCount >= maxlength
56
+ ? 'at-limit'
57
+ : 'near-limit'
58
+ : ''
59
+ );
60
+
49
61
  // Auto-resize functionality
50
62
  const handleAutoResize = () => {
51
63
  if (!autoResize || !textareaElement) return;
@@ -86,25 +98,37 @@
86
98
  </script>
87
99
 
88
100
  <FormField {size} {label} {id} {required} {disabled} {helperText} {feedback}>
89
- <textarea
90
- wrap="soft"
91
- {id}
92
- {placeholder}
93
- rows={autoResize ? minRows : rows}
94
- bind:value
95
- bind:this={textareaElement}
96
- {required}
97
- {disabled}
98
- {readonly}
99
- {maxlength}
100
- {minlength}
101
- aria-busy={isLoading}
102
- data-auto-resize={autoResize}
103
- oninput={handleAutoResize}
104
- ></textarea>
101
+ <div class="textarea-container">
102
+ <textarea
103
+ wrap="soft"
104
+ {id}
105
+ {placeholder}
106
+ rows={autoResize ? minRows : rows}
107
+ bind:value
108
+ bind:this={textareaElement}
109
+ {required}
110
+ {disabled}
111
+ {readonly}
112
+ {maxlength}
113
+ {minlength}
114
+ aria-busy={isLoading}
115
+ data-auto-resize={autoResize}
116
+ oninput={handleAutoResize}
117
+ ></textarea>
118
+ {#if showCharacterCount && maxlength}
119
+ <div class="character-count {characterLimitClass}">
120
+ {characterCount} / {maxlength}
121
+ </div>
122
+ {/if}
123
+ </div>
105
124
  </FormField>
106
125
 
107
- <style>textarea {
126
+ <style>.textarea-container {
127
+ position: relative;
128
+ width: 100%;
129
+ }
130
+
131
+ textarea {
108
132
  width: 100%;
109
133
  height: auto;
110
134
  padding: 0.5rem 1rem;
@@ -128,4 +152,23 @@ textarea::placeholder {
128
152
  textarea[data-auto-resize=true] {
129
153
  resize: none;
130
154
  overflow-y: hidden;
155
+ }
156
+
157
+ .character-count {
158
+ font-size: var(--font-sm);
159
+ line-height: 1.25rem;
160
+ padding: var(--spacing-xs);
161
+ text-align: right;
162
+ color: var(--body-fg);
163
+ position: absolute;
164
+ right: 0;
165
+ bottom: 0;
166
+ }
167
+ .character-count.near-limit {
168
+ color: var(--warning, #ffc107);
169
+ font-weight: 500;
170
+ }
171
+ .character-count.at-limit {
172
+ color: var(--danger, #dc3545);
173
+ font-weight: 600;
131
174
  }</style>
@@ -15,6 +15,7 @@ type $$ComponentProps = {
15
15
  helperText?: string;
16
16
  feedback?: FormFieldFeedback;
17
17
  isLoading?: boolean;
18
+ showCharacterCount?: boolean;
18
19
  maxlength?: number | undefined;
19
20
  minlength?: number | undefined;
20
21
  pattern?: string | undefined;
@@ -2,3 +2,4 @@ export { default as FlexCol } from './flex-col.svelte';
2
2
  export { default as FlexRow } from './flex-row.svelte';
3
3
  export { default as FlexItem } from './flex-item.svelte';
4
4
  export { default as Grid } from './grid.svelte';
5
+ export { default as Main } from './main/main.svelte';
@@ -2,3 +2,4 @@ export { default as FlexCol } from './flex-col.svelte';
2
2
  export { default as FlexRow } from './flex-row.svelte';
3
3
  export { default as FlexItem } from './flex-item.svelte';
4
4
  export { default as Grid } from './grid.svelte';
5
+ export { default as Main } from './main/main.svelte';
@@ -9,6 +9,11 @@
9
9
  </script>
10
10
 
11
11
  <main>
12
+ <a
13
+ id="top"
14
+ aria-hidden="true"
15
+ style="position: absolute; top: 0; left: 0; width: 1px; height: 1px; opacity: 0; pointer-events: none;"
16
+ ></a>
12
17
  {@render children?.()}
13
18
  </main>
14
19
 
@@ -16,6 +21,7 @@
16
21
  BREAKPOINTS - Responsive Design
17
22
  ============================================ */
18
23
  main {
24
+ position: relative;
19
25
  display: flex;
20
26
  flex-direction: column;
21
27
  align-items: stretch;
@@ -23,6 +29,9 @@ main {
23
29
  max-width: 1000px; /* Desktop: good for 1366px-1920px screens */
24
30
  margin: 0 auto;
25
31
  padding: 0 var(--spacing-base, 1rem);
32
+ /* Improve touch scrolling on iOS */
33
+ -webkit-overflow-scrolling: touch;
34
+ overflow-x: hidden; /* Prevent horizontal scroll */
26
35
  /* Responsive padding for larger screens */
27
36
  }
28
37
  @media (min-width: 641px) {
@@ -38,4 +47,12 @@ main {
38
47
  main {
39
48
  max-width: 1440px;
40
49
  }
50
+ }
51
+ main {
52
+ /* Add extra bottom padding on mobile for better UX */
53
+ }
54
+ @media (max-width: 319.98px) {
55
+ main {
56
+ padding-bottom: 2rem;
57
+ }
41
58
  }</style>
@@ -2,7 +2,6 @@ export type TabVariant = 'traditional' | 'underline' | 'outline' | 'overline' |
2
2
  export type TabDefinition = {
3
3
  id: string;
4
4
  label: string;
5
- defaultActive: boolean;
6
5
  index: number;
7
6
  disabled?: boolean;
8
7
  href?: string;
@@ -14,6 +13,6 @@ export interface TabContext {
14
13
  };
15
14
  variant: TabVariant;
16
15
  groupId: string;
17
- register: (id: string, label: string, isActive: boolean, href?: string) => void;
16
+ register: (id: string, label: string, href?: string, disabled?: boolean) => void;
18
17
  }
19
18
  export declare const tabContext = "tabContext";
@@ -7,10 +7,12 @@
7
7
  let {
8
8
  variant = 'traditional' as TabVariant,
9
9
  onChange = undefined,
10
+ activeTab = $bindable(null as string | null),
10
11
  children
11
12
  }: {
12
13
  variant?: TabVariant;
13
14
  onChange?: ((id: string | null) => void) | undefined;
15
+ activeTab?: string | null;
14
16
  children: Snippet;
15
17
  } = $props();
16
18
 
@@ -26,11 +28,73 @@
26
28
  let registrationIndex = 0;
27
29
  let isInitialized = $state(false);
28
30
 
29
- const register = (id: string, label: string, isActive: boolean, href?: string) => {
31
+ // Cache for tab lookups to improve performance
32
+ const tabCache = new Map<string, TabDefinition>();
33
+
34
+ const register = (id: string, label: string, href?: string, disabled = false) => {
35
+ // Prevent duplicate registrations
36
+ if (tabCache.has(id)) {
37
+ console.warn(`Tab with id "${id}" is already registered. Ignoring duplicate.`);
38
+ return;
39
+ }
40
+
30
41
  const index = registrationIndex++;
31
- tabState.tabs.push({ id, label, defaultActive: isActive, index, href });
42
+ const tab: TabDefinition = { id, label, index, href, disabled };
43
+ tabState.tabs.push(tab);
44
+ tabCache.set(id, tab);
32
45
  };
33
46
 
47
+ // Sync from activeTab (parent) to tabState.active (internal)
48
+ // This allows parent to control which tab is active
49
+ // Using untrack to prevent reactive dependencies that could cause loops
50
+ $effect(() => {
51
+ const currentActiveTab = activeTab;
52
+ const currentActiveState = tabState.active;
53
+
54
+ // Check if current active tab becomes disabled - clear it if so
55
+ if (currentActiveState !== null) {
56
+ const activeTabDef = tabCache.get(currentActiveState);
57
+ if (activeTabDef?.disabled) {
58
+ // Active tab was disabled, find first enabled tab or clear
59
+ const enabledTabs = tabState.tabs.filter((tab) => !tab.disabled);
60
+ if (enabledTabs.length > 0) {
61
+ const firstEnabled = enabledTabs.sort((a, b) => a.index - b.index)[0];
62
+ tabState.active = firstEnabled.id;
63
+ activeTab = firstEnabled.id;
64
+ } else {
65
+ tabState.active = null;
66
+ activeTab = null;
67
+ }
68
+ return;
69
+ }
70
+ }
71
+
72
+ // Skip if values are already in sync
73
+ if (currentActiveTab === currentActiveState) return;
74
+
75
+ // If activeTab is explicitly set (including null), sync it to internal state
76
+ if (currentActiveTab !== undefined) {
77
+ if (currentActiveTab === null) {
78
+ // Parent wants to clear active tab
79
+ if (currentActiveState !== null) {
80
+ tabState.active = null;
81
+ }
82
+ } else {
83
+ // Only update if the tab exists and is not disabled
84
+ const tab = tabCache.get(currentActiveTab);
85
+ if (tab) {
86
+ if (!tab.disabled && currentActiveState !== currentActiveTab) {
87
+ tabState.active = currentActiveTab;
88
+ } else if (tab.disabled) {
89
+ // If parent tries to activate a disabled tab, revert activeTab
90
+ activeTab = currentActiveState;
91
+ }
92
+ }
93
+ // If tabs haven't registered yet, initialization will handle it
94
+ }
95
+ }
96
+ });
97
+
34
98
  // Initialize active tab - only run once when tabs are available
35
99
  $effect(() => {
36
100
  // Skip if already initialized or no tabs yet
@@ -45,6 +109,16 @@
45
109
  const currentTabs = tabState.tabs;
46
110
  if (currentTabs.length === 0) return;
47
111
 
112
+ // If activeTab is already set by parent, use it (after verifying it's valid and not disabled)
113
+ if (activeTab !== null && activeTab !== undefined) {
114
+ const tab = tabCache.get(activeTab);
115
+ if (tab && !tab.disabled) {
116
+ tabState.active = activeTab;
117
+ isInitialized = true;
118
+ return;
119
+ }
120
+ }
121
+
48
122
  // Check for anchor in URL first (but only if it matches a tab in THIS group)
49
123
  const anchor = getAnchor();
50
124
  if (anchor) {
@@ -52,58 +126,60 @@
52
126
  const groupPrefix = `${groupId}-`;
53
127
  if (anchor.startsWith(groupPrefix)) {
54
128
  const tabId = anchor.slice(groupPrefix.length);
55
- const tabExists = currentTabs.some((tab) => tab.id === tabId);
56
- if (tabExists) {
129
+ const tab = tabCache.get(tabId);
130
+ if (tab && !tab.disabled) {
57
131
  tabState.active = tabId;
132
+ activeTab = tabId;
58
133
  isInitialized = true;
59
134
  return;
60
135
  }
61
136
  }
62
137
  // Also check for direct tab ID match (for backward compatibility)
63
- const tabExists = currentTabs.some((tab) => tab.id === anchor);
64
- if (tabExists) {
138
+ const tab = tabCache.get(anchor);
139
+ if (tab && !tab.disabled) {
65
140
  tabState.active = anchor;
141
+ activeTab = anchor;
66
142
  isInitialized = true;
67
143
  return;
68
144
  }
69
145
  }
70
146
 
71
- // Check for defaultActive tab
72
- const defaultActiveTab = currentTabs.find((tab) => tab.defaultActive);
73
- if (defaultActiveTab) {
74
- tabState.active = defaultActiveTab.id;
75
- isInitialized = true;
76
- return;
77
- }
78
-
79
- // Default to first tab (by registration index, which should be the first in DOM order)
147
+ // Default to first enabled tab (by registration index, which should be the first in DOM order)
80
148
  // Sort by index to get the first registered tab (lowest index = first)
81
- const sortedTabs = [...currentTabs].sort((a, b) => a.index - b.index);
149
+ const sortedTabs = [...currentTabs]
150
+ .filter((tab) => !tab.disabled)
151
+ .sort((a, b) => a.index - b.index);
82
152
  const firstTab = sortedTabs[0];
83
153
  if (firstTab) {
84
154
  tabState.active = firstTab.id;
155
+ activeTab = firstTab.id;
85
156
  isInitialized = true;
86
157
  }
87
158
  });
88
159
  });
89
160
  const selectTab = (id: string) => {
90
- const tab = tabState.tabs.find((t) => t.id === id);
91
- if (tab?.disabled) return;
161
+ // Use cache for faster lookup
162
+ const tab = tabCache.get(id);
163
+ if (!tab || tab.disabled) return;
92
164
 
93
- tabState.active = id;
165
+ // Only update if different to avoid unnecessary re-renders
166
+ if (tabState.active !== id) {
167
+ tabState.active = id;
168
+ activeTab = id;
94
169
 
95
- // Only use anchor navigation if tab doesn't have href (href navigation is handled by Tab component)
96
- // Scope anchor to this group to avoid conflicts with multiple tab groups
97
- if (!tab?.href) {
98
- navigateToAnchor(`${groupId}-${id}`);
99
- }
170
+ // Only use anchor navigation if tab doesn't have href (href navigation is handled by Tab component)
171
+ // Scope anchor to this group to avoid conflicts with multiple tab groups
172
+ if (!tab.href) {
173
+ navigateToAnchor(`${groupId}-${id}`);
174
+ }
100
175
 
101
- onChange?.(id);
176
+ onChange?.(id);
102
177
 
103
- // Focus the selected tab (scoped to this group)
104
- const button = document.getElementById(`tab-${groupId}-${id}`);
105
- if (button) {
106
- button.focus();
178
+ // Focus the selected tab (scoped to this group)
179
+ const button = document.getElementById(`tab-${groupId}-${id}`);
180
+ if (button) {
181
+ button.focus();
182
+ }
107
183
  }
108
184
  };
109
185
 
@@ -161,15 +237,17 @@
161
237
  <div class="tab-head">
162
238
  <div role="tablist">
163
239
  {#each tabState.tabs as tab}
164
- <li class={tabState.active == tab.id ? 'active' : 'inactive'}>
240
+ <li class={tabState.active == tab.id ? 'active' : 'inactive'} class:disabled={tab.disabled}>
165
241
  <button
166
242
  id="tab-{groupId}-{tab.id}"
167
243
  role="tab"
168
244
  aria-selected={tabState.active === tab.id}
169
245
  aria-controls="tabpanel-{groupId}-{tab.id}"
170
- tabindex={tabState.active === tab.id ? 0 : -1}
246
+ aria-disabled={tab.disabled || false}
247
+ tabindex={tab.disabled ? -1 : tabState.active === tab.id ? 0 : -1}
248
+ disabled={tab.disabled || false}
171
249
  onclick={() => selectTab(tab.id)}
172
- onkeydown={(e) => handleKeydown(e, tab.id)}
250
+ onkeydown={(e) => !tab.disabled && handleKeydown(e, tab.id)}
173
251
  >
174
252
  {tab.label}
175
253
  </button>
@@ -206,6 +284,10 @@
206
284
  font-weight: 700;
207
285
  cursor: default;
208
286
  }
287
+ .tab-head li.disabled button {
288
+ opacity: 0.5;
289
+ cursor: not-allowed;
290
+ }
209
291
  .tab-head button {
210
292
  appearance: none;
211
293
  border: none 0;
@@ -3,8 +3,9 @@ import { type TabVariant } from './tab-context.js';
3
3
  type $$ComponentProps = {
4
4
  variant?: TabVariant;
5
5
  onChange?: ((id: string | null) => void) | undefined;
6
+ activeTab?: string | null;
6
7
  children: Snippet;
7
8
  };
8
- declare const TabGroup: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ declare const TabGroup: import("svelte").Component<$$ComponentProps, {}, "activeTab">;
9
10
  type TabGroup = ReturnType<typeof TabGroup>;
10
11
  export default TabGroup;
@@ -8,15 +8,15 @@
8
8
  let {
9
9
  label,
10
10
  href,
11
- defaultActive = false,
12
11
  id = undefined,
12
+ disabled = false,
13
13
  onActivate = undefined,
14
14
  children = undefined
15
15
  }: {
16
16
  label: string;
17
17
  href?: string | undefined;
18
- defaultActive?: boolean;
19
18
  id?: string | undefined;
19
+ disabled?: boolean;
20
20
  onActivate?: ((id: string) => void) | undefined;
21
21
  children?: Snippet;
22
22
  } = $props();
@@ -32,7 +32,7 @@
32
32
 
33
33
  // Register this tab once on mount (like wizard does)
34
34
  onMount(() => {
35
- ctx.register(_id, label, defaultActive, href);
35
+ ctx.register(_id, label, href, disabled);
36
36
  });
37
37
 
38
38
  // Access the $state object's properties directly - THIS creates reactive dependencies!
@@ -2,8 +2,8 @@ import type { Snippet } from 'svelte';
2
2
  type $$ComponentProps = {
3
3
  label: string;
4
4
  href?: string | undefined;
5
- defaultActive?: boolean;
6
5
  id?: string | undefined;
6
+ disabled?: boolean;
7
7
  onActivate?: ((id: string) => void) | undefined;
8
8
  children?: Snippet;
9
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sveltacular",
3
- "version": "1.0.10",
3
+ "version": "1.0.13",
4
4
  "description": "A Svelte component library",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",