|
| 1 | +<script lang="ts" module> |
| 2 | + // Shared state for synchronized tabs across instances |
| 3 | + class TabsState { |
| 4 | + activeLabel = $state<string | null>(null); |
| 5 | + } |
| 6 | +
|
| 7 | + const syncedTabs = new Map<string, TabsState>(); |
| 8 | +
|
| 9 | + function getSyncedState(key: string) { |
| 10 | + if (!syncedTabs.has(key)) { |
| 11 | + syncedTabs.set(key, new TabsState()); |
| 12 | + } |
| 13 | + return syncedTabs.get(key)!; |
| 14 | + } |
| 15 | +</script> |
| 16 | + |
1 | 17 | <script lang="ts"> |
2 | 18 | import type { Snippet, Component } from 'svelte'; |
3 | 19 | import type { HTMLAttributes } from 'svelte/elements'; |
|
6 | 22 |
|
7 | 23 | interface Props extends HTMLAttributes<HTMLDivElement> { |
8 | 24 | children: Snippet; |
| 25 | + key?: string; |
9 | 26 | } |
10 | 27 |
|
11 | | - const { children, class: className, ...restProps }: Props = $props(); |
| 28 | + const { children, key, class: className, ...restProps }: Props = $props(); |
| 29 | +
|
| 30 | + // Use synced state if key is provided, otherwise use local state |
| 31 | + let localActiveIndex = $state(0); |
| 32 | + let syncedState = $derived(key ? getSyncedState(key) : null); |
12 | 33 |
|
13 | | - let activeTab = $state(0); |
14 | | - let tabs = $state<Array<{ label?: string; icon?: string | Component }>>([]); |
| 34 | + let tabs = $state<Array<{ label?: string; icon?: Component }>>([]); |
15 | 35 | let tabCounter = 0; |
16 | 36 |
|
| 37 | + // Compute active tab index based on synced label or local index |
| 38 | + let activeTab = $derived.by(() => { |
| 39 | + if (syncedState && syncedState.activeLabel !== null) { |
| 40 | + // Find the index of the tab with the matching label |
| 41 | + const index = tabs.findIndex((tab) => tab.label === syncedState.activeLabel); |
| 42 | + return index >= 0 ? index : 0; |
| 43 | + } |
| 44 | + return localActiveIndex; |
| 45 | + }); |
| 46 | +
|
| 47 | + // Initialize synced state with first tab's label if not already set |
| 48 | + $effect(() => { |
| 49 | + if (syncedState && syncedState.activeLabel === null && tabs.length > 0) { |
| 50 | + syncedState.activeLabel = tabs[0].label ?? null; |
| 51 | + } |
| 52 | + }); |
| 53 | +
|
17 | 54 | // Provide context for Tab components to register themselves |
18 | 55 | setContext('tabs', { |
19 | 56 | get activeTab() { |
20 | 57 | return activeTab; |
21 | 58 | }, |
22 | 59 | setActiveTab: (index: number) => { |
23 | | - activeTab = index; |
| 60 | + if (syncedState) { |
| 61 | + syncedState.activeLabel = tabs[index]?.label ?? null; |
| 62 | + } else { |
| 63 | + localActiveIndex = index; |
| 64 | + } |
24 | 65 | }, |
25 | | - registerTab: (label: string | undefined, icon: string | Component | undefined) => { |
| 66 | + registerTab: (label: string | undefined, icon: Component | undefined) => { |
26 | 67 | const index = tabCounter++; |
27 | 68 | tabs = [...tabs, { label, icon }]; |
28 | 69 | return index; |
29 | 70 | } |
30 | 71 | }); |
31 | | -
|
32 | | - // Convert i-collection-name format to collection:name format for Iconify |
33 | | - function getIconifyName(name: string): string { |
34 | | - // If already in collection:name format, use as-is |
35 | | - if (name.includes(':')) return name; |
36 | | - // Convert i-collection-name to collection:name |
37 | | - const match = name.match(/^i-([^-]+)-(.+)$/); |
38 | | - if (match) { |
39 | | - const [, collection, iconName] = match; |
40 | | - return `${collection}:${iconName.replace(/-/g, '-')}`; |
41 | | - } |
42 | | - return name; |
43 | | - } |
44 | | -
|
45 | | - // Check if any tabs have string icons (need Iconify) |
46 | | - const hasIconifyIcons = $derived(tabs.some((tab) => typeof tab.icon === 'string')); |
47 | 72 | </script> |
48 | 73 |
|
49 | | -<svelte:head> |
50 | | - <!-- Load Iconify web component if needed --> |
51 | | - {#if hasIconifyIcons} |
52 | | - <script src="https://code.iconify.design/iconify-icon/2.1.0/iconify-icon.min.js"> |
53 | | - </script> |
54 | | - {/if} |
55 | | -</svelte:head> |
56 | | - |
57 | | -<div class={cls('tabs mt-4 flex flex-col', className)} {...restProps}> |
| 74 | +<div class={cls('tabs flex my-5 flex-col', className)} {...restProps}> |
58 | 75 | <!-- Tabs --> |
59 | 76 | <div class="flex gap-1 overflow-auto z-1 -mb-px"> |
60 | 77 | {#each tabs as tab, index} |
|
67 | 84 | ? 'bg-surface-100 text-surface-content border-b-surface-100' |
68 | 85 | : 'bg-surface-200 text-surface-content/50 hover:text-surface-content' |
69 | 86 | )} |
70 | | - onclick={() => (activeTab = index)} |
| 87 | + onclick={() => { |
| 88 | + if (syncedState) { |
| 89 | + syncedState.activeLabel = tab.label ?? null; |
| 90 | + } else { |
| 91 | + localActiveIndex = index; |
| 92 | + } |
| 93 | + }} |
71 | 94 | > |
72 | 95 | {#if tab.icon} |
73 | | - {#if typeof tab.icon === 'string'} |
74 | | - <!-- Iconify web component --> |
75 | | - <iconify-icon icon={getIconifyName(tab.icon)} class="size-4"></iconify-icon> |
76 | | - {:else} |
77 | | - <!-- Component import (dynamic by default in runes mode) --> |
78 | | - {@const IconComponent = tab.icon} |
79 | | - <IconComponent class="size-4" /> |
80 | | - {/if} |
| 96 | + <!-- unplugin-icons component --> |
| 97 | + {@const IconComponent = tab.icon} |
| 98 | + <IconComponent class="size-4 fill-surface-content" /> |
81 | 99 | {/if} |
82 | 100 | {tab.label || `Tab ${index + 1}`} |
83 | 101 | </button> |
84 | 102 | {/each} |
85 | 103 | </div> |
86 | 104 |
|
87 | 105 | <!-- Tab content --> |
88 | | - <div class="border rounded-lg rounded-tl-none p-3 bg-surface-100"> |
| 106 | + <div class="border rounded-lg rounded-tl-none px-3 py-1 bg-surface-100"> |
89 | 107 | {@render children?.()} |
90 | 108 | </div> |
91 | 109 | </div> |
0 commit comments