Skip to content

Commit 0e6d0fa

Browse files
committed
feat(tabs): adds pane to tabs
1 parent 62b9544 commit 0e6d0fa

File tree

6 files changed

+197
-13
lines changed

6 files changed

+197
-13
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<script lang="ts">
2+
import { randomHex } from '@dvcol/common-utils/common/crypto';
3+
4+
import SphereBackdrop from '../utils/SphereBackdrop.svelte';
5+
6+
import type { TabId } from '~/nav/neo-tab.model.js';
7+
import type { TabsProps } from '~/nav/neo-tabs.model.js';
8+
9+
import NeoButton from '~/buttons/NeoButton.svelte';
10+
import NeoButtonGroup from '~/buttons/NeoButtonGroup.svelte';
11+
import IconAccount from '~/icons/IconAccount.svelte';
12+
import NeoTab from '~/nav/NeoTab.svelte';
13+
import NeoTabPane from '~/nav/NeoTabPane.svelte';
14+
import NeoTabs from '~/nav/NeoTabs.svelte';
15+
16+
const added = $state([
17+
{ text: `Added ${randomHex(2)}-0`, tabId: crypto.randomUUID() },
18+
{ text: `Added ${randomHex(2)}-1`, tabId: crypto.randomUUID() },
19+
]);
20+
const onclose = (id: TabId) => {
21+
const index = added.findIndex(tab => tab.tabId === id);
22+
if (index === -1) return;
23+
added.splice(index, 1);
24+
};
25+
const onadd = () => {
26+
added.push({ text: `Added ${randomHex(2)}-${added.length + 1}`, tabId: crypto.randomUUID() });
27+
};
28+
29+
let active: unknown | undefined = $state();
30+
const onClear = () => {
31+
active = undefined;
32+
};
33+
34+
let loading = $state(false);
35+
let skeleton = $state(false);
36+
let vertical = $state(false);
37+
const options = $state({ disabled: false, close: true, add: true, slide: true, shallow: false, toggle: true, position: 'after' });
38+
39+
const togglePosition = () => {
40+
options.position = options.position === 'after' ? 'before' : 'after';
41+
};
42+
43+
const columns: { label: string; props?: TabsProps }[] = [{ label: 'Default' }];
44+
</script>
45+
46+
<div class="row">
47+
<div class="column">
48+
<NeoButtonGroup>
49+
<NeoButton toggle bind:checked={options.disabled}>Disabled</NeoButton>
50+
<NeoButton toggle bind:checked={options.shallow}>Shallow</NeoButton>
51+
<NeoButton onclick={togglePosition}>Position</NeoButton>
52+
<NeoButton toggle bind:checked={vertical}>Vertical</NeoButton>
53+
<NeoButton toggle bind:checked={skeleton}>Skeleton</NeoButton>
54+
<NeoButton toggle bind:checked={loading}>Loading</NeoButton>
55+
<NeoButton onclick={onClear}>Clear</NeoButton>
56+
</NeoButtonGroup>
57+
</div>
58+
</div>
59+
60+
{#snippet icon()}
61+
<IconAccount />
62+
{/snippet}
63+
64+
{#snippet tabs()}
65+
<NeoTab tabId="button" value="button" close={false}>Button</NeoTab>
66+
<NeoTab tabId="icon" value="icon" {loading} close={false} {icon} />
67+
<NeoTab tabId="reversed" value="reversed" reverse close={false} {icon}>Reversed</NeoTab>
68+
{#each added as { text, ...tab } (tab.tabId)}
69+
<NeoTab {...tab}>{text}</NeoTab>
70+
{/each}
71+
{/snippet}
72+
73+
{#snippet panes()}
74+
<NeoTabPane empty>
75+
<div>Empty</div>
76+
</NeoTabPane>
77+
<NeoTabPane tabId="button">
78+
<div>Button</div>
79+
</NeoTabPane>
80+
<NeoTabPane tabId="icon" {loading} {icon}>
81+
<div>Icon</div>
82+
</NeoTabPane>
83+
<NeoTabPane tabId="reversed" {icon}>
84+
<div>Reversed</div>
85+
</NeoTabPane>
86+
87+
{#each added as { text, tabId } (tabId)}
88+
<NeoTabPane {tabId}>{text}</NeoTabPane>
89+
{/each}
90+
{/snippet}
91+
92+
{#snippet group(props: TabsProps = {})}
93+
<div class="column">
94+
<NeoTabs {panes} {active} {vertical} {skeleton} {onclose} {onadd} {...options} {...props}>
95+
{@render tabs()}
96+
</NeoTabs>
97+
</div>
98+
{/snippet}
99+
100+
<div class="row">
101+
{#each columns as { label, props }}
102+
<div class="column">
103+
<span class="label">{label}</span>
104+
105+
{#if props?.glass}
106+
<SphereBackdrop>{@render group(props)}</SphereBackdrop>
107+
{:else}
108+
{@render group(props)}
109+
{/if}
110+
</div>
111+
{/each}
112+
</div>
113+
114+
<style lang="scss">
115+
@use 'src/lib/styles/common/flex' as flex;
116+
117+
.column {
118+
@include flex.column($center: true, $gap: var(--gap-lg));
119+
}
120+
121+
.row {
122+
@include flex.row($gap: var(--gap-xl));
123+
124+
align-items: center;
125+
justify-content: center;
126+
margin: 2rem 0;
127+
}
128+
</style>

demo/router/routes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const Route = {
55
Buttons: 'buttons' as const,
66
ButtonGroups: 'button-groups' as const,
77
Tabs: 'tabs' as const,
8+
TabsPanels: 'tabs-panels' as const,
89
} as const;
910

1011
export type Routes = (typeof Route)[keyof typeof Route];
@@ -13,6 +14,7 @@ export const Path: Record<keyof typeof Route, string> = {
1314
Buttons: '/buttons' as const,
1415
ButtonGroups: '/buttons/groups' as const,
1516
Tabs: '/tabs' as const,
17+
TabsPanels: '/tabs/panels' as const,
1618
Any: '*' as const,
1719
} as const;
1820

@@ -36,6 +38,11 @@ export const options: RouterOptions<Routes> = {
3638
path: Path.Tabs,
3739
component: () => import('../components/DemoTabs.svelte'),
3840
},
41+
{
42+
name: Route.TabsPanels,
43+
path: Path.TabsPanels,
44+
component: () => import('../components/DemoTabsPanels.svelte'),
45+
},
3946
{
4047
name: Route.Any,
4148
path: Path.Any,

src/lib/nav/NeoTabPane.svelte

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script lang="ts">
2+
import type { NeoTabPaneProps } from '~/nav/neo-tab-pane.model.js';
3+
4+
import { getTabContext } from '~/nav/neo-tabs-context.svelte.js';
5+
6+
const { children, tabId, empty }: NeoTabPaneProps = $props();
7+
8+
const context = getTabContext();
9+
const active = $derived(context?.active === tabId);
10+
11+
const show = $derived(empty ? !context?.active : active);
12+
</script>
13+
14+
{#if show}
15+
{@render children?.(context?.state ?? {})}
16+
{/if}

src/lib/nav/NeoTabs.svelte

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
44
import { untrack } from 'svelte';
55
6-
import type { NeoTabsContext, OnChange, TabsProps } from '~/nav/neo-tabs.model.js';
6+
import type { NeoTabContextPositions, NeoTabsContext } from '~/nav/neo-tabs-context.svelte.js';
7+
8+
import type { OnChange, TabsProps } from '~/nav/neo-tabs.model.js';
79
810
import NeoButton from '~/buttons/NeoButton.svelte';
911
import NeoButtonGroup from '~/buttons/NeoButtonGroup.svelte';
1012
import IconAdd from '~/icons/IconAdd.svelte';
11-
import { type NeoTabContextPositions, setTabContext } from '~/nav/neo-tabs-context.svelte.js';
13+
import { setTabContext } from '~/nav/neo-tabs-context.svelte.js';
14+
1215
import { toAction, toActionProps } from '~/utils/action.utils.js';
1316
1417
/* eslint-disable prefer-const -- necessary for binding checked */
@@ -21,6 +24,7 @@
2124
active = $bindable(),
2225
2326
// Styles
27+
position = 'after',
2428
toggle,
2529
close,
2630
add,
@@ -47,7 +51,7 @@
4751
const context = setTabContext({ onChange, onClose: onclose });
4852
const transition = $derived(rest.vertical ? height : width);
4953
50-
let position = $state(transform(context.position));
54+
let translate = $state(transform(context.position));
5155
// Function to compute the transform
5256
function transform({ oldTab, newTab }: NeoTabContextPositions) {
5357
if (!newTab) return;
@@ -63,10 +67,10 @@
6367
6468
$effect.pre(() => {
6569
if (!slide) return;
66-
position = transform(context.position);
70+
translate = transform(context.position);
6771
});
6872
69-
const style = $derived([tabsProps?.style, position].filter(Boolean).join('; '));
73+
const style = $derived([tabsProps?.style, translate].filter(Boolean).join('; '));
7074
7175
// reflect component active to context
7276
$effect(() => {
@@ -102,7 +106,7 @@
102106
class:add
103107
class:line
104108
class:slide
105-
class:animate={!!position}
109+
class:translate
106110
class:flat={rest.flat}
107111
class:text={rest.text}
108112
class:vertical={rest.vertical}
@@ -123,12 +127,14 @@
123127
</div>
124128
{/snippet}
125129

126-
<!-- TODO: placement right left (vertical), top, bottom (horizontal) -->
127-
{#if panes}
128-
{@render tabs()}
129-
{@render panes(context.state)}
130-
{:else}
131-
{@render tabs()}
130+
{#if position === 'before'}
131+
{@render panes?.(context.state)}
132+
{/if}
133+
134+
{@render tabs()}
135+
136+
{#if position === 'after'}
137+
{@render panes?.(context.state)}
132138
{/if}
133139

134140
<style lang="scss">
@@ -256,7 +262,7 @@
256262
box-shadow: var(--box-shadow-inset-2);
257263
}
258264
259-
&.animate :global(.neo-tab.active::before) {
265+
&.translate :global(.neo-tab.active::before) {
260266
animation: slide 0.5s var(--transition-bezier) forwards;
261267
}
262268

src/lib/nav/neo-tab-pane.model.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { Snippet } from 'svelte';
2+
import type { TabId } from '~/nav/neo-tab.model.js';
3+
import type { NeoTabsContext } from '~/nav/neo-tabs-context.svelte.js';
4+
5+
export type NeoTabPaneProps = {
6+
// Snippets
7+
8+
/**
9+
* Optional snippet to display as the tab content.
10+
*/
11+
children?: Snippet<[NeoTabsContext]>;
12+
13+
// States
14+
15+
/**
16+
* The tab id to associate with this pane.
17+
*/
18+
tabId?: TabId;
19+
/**
20+
* If `true`, the pane will appear when no other tabs are active.
21+
*/
22+
empty?: boolean;
23+
};

src/lib/nav/neo-tabs.model.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export type TabsProps<T = unknown> = {
2525
* Only applies when `slide` is `true`.
2626
*/
2727
line?: boolean;
28+
/**
29+
* Sets the position of the pane snippet relative to the tabs.
30+
*/
31+
position?: 'before' | 'after';
2832

2933
// Events
3034
/**

0 commit comments

Comments
 (0)