Skip to content

Commit faaf689

Browse files
committed
feat(tabs): adds animated tab card wrapper
1 parent 02cfac5 commit faaf689

20 files changed

+350
-129
lines changed

demo/components/DemoTabs.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { useButtonState } from '../utils/use-button-state.svelte';
88
99
import type { TabId } from '~/nav/neo-tab.model';
10-
import type { NeoTabsProps } from '~/nav/neo-tabs.model.js';
10+
import type { NeoTabContextValue, NeoTabsProps } from '~/nav/neo-tabs.model.js';
1111
1212
import NeoButton from '~/buttons/NeoButton.svelte';
1313
import NeoButtonGroup from '~/buttons/NeoButtonGroup.svelte';
@@ -37,9 +37,9 @@
3737
let active: unknown | undefined = $state('button');
3838
let value: unknown | undefined = $state('button');
3939
40-
const onChange = (id?: TabId, _value?: unknown) => {
40+
const onChange = (id?: TabId, context?: NeoTabContextValue) => {
4141
active = id;
42-
value = _value;
42+
value = context?.value;
4343
};
4444
4545
const onClear = () => onChange();

demo/components/DemoTabsPanels.svelte

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
<script lang="ts">
22
import { randomHex } from '@dvcol/common-utils/common/crypto';
33
4-
import { fade } from 'svelte/transition';
5-
64
import SphereBackdrop from '../utils/SphereBackdrop.svelte';
75
86
import type { TabId } from '~/nav/neo-tab.model.js';
97
import type { NeoTabsProps } from '~/nav/neo-tabs.model.js';
108
119
import NeoButton from '~/buttons/NeoButton.svelte';
1210
import NeoButtonGroup from '~/buttons/NeoButtonGroup.svelte';
13-
import NeoCard from '~/cards/NeoCard.svelte';
14-
import TransitionContainer from '~/container/TransitionContainer.svelte';
1511
import IconAccount from '~/icons/IconAccount.svelte';
1612
import NeoTab from '~/nav/NeoTab.svelte';
1713
import NeoTabPane from '~/nav/NeoTabPane.svelte';
1814
import NeoTabs from '~/nav/NeoTabs.svelte';
15+
import NeoTabsCard from '~/nav/NeoTabsCard.svelte';
1916
2017
const added = $state([
2118
{ text: `Added ${randomHex(1)}-0`, tabId: crypto.randomUUID() },
@@ -26,6 +23,7 @@
2623
if (index === -1) return;
2724
added.splice(index, 1);
2825
};
26+
2927
const onadd = () => {
3028
added.push({ text: `Added ${randomHex(1)}-${added.length + 1}`, tabId: crypto.randomUUID() });
3129
};
@@ -40,6 +38,7 @@
4038
close: true,
4139
add: true,
4240
slide: true,
41+
inset: false,
4342
shallow: false,
4443
toggle: true,
4544
before: false,
@@ -55,6 +54,7 @@
5554
<div class="column">
5655
<NeoButtonGroup>
5756
<NeoButton toggle bind:checked={options.disabled}>Disabled</NeoButton>
57+
<NeoButton toggle bind:checked={options.inset}>Inset</NeoButton>
5858
<NeoButton toggle bind:checked={options.shallow}>Shallow</NeoButton>
5959
<NeoButton toggle bind:checked={options.before}>Before</NeoButton>
6060
<NeoButton toggle bind:checked={options.vertical}>Vertical</NeoButton>
@@ -79,43 +79,35 @@
7979
{/snippet}
8080

8181
{#snippet content(word)}
82-
<div class="panel" in:fade={{ delay: 400 }} out:fade>
83-
<div>{Array.from({ length: 10 }, () => word).join(' ')}</div>
84-
<div>{Array.from({ length: 10 }, () => word).join(' ')}</div>
85-
<div>{Array.from({ length: 10 }, () => word).join(' ')}</div>
86-
<div>{Array.from({ length: 10 }, () => word).join(' ')}</div>
87-
<div>{Array.from({ length: 10 }, () => word).join(' ')}</div>
88-
</div>
82+
<p>{Array.from({ length: 50 }, () => word).join(' ')}</p>
8983
{/snippet}
9084

9185
{#snippet panes()}
92-
<NeoCard>
93-
<TransitionContainer>
94-
<NeoTabPane empty>
95-
{@render content('Empty')}
96-
</NeoTabPane>
97-
<NeoTabPane tabId="button">
98-
{@render content('Button')}
99-
</NeoTabPane>
100-
<NeoTabPane tabId="icon">
101-
{@render content('Icon')}
102-
</NeoTabPane>
103-
<NeoTabPane tabId="reversed">
104-
{@render content('Reversed')}
86+
<NeoTabsCard class="panel">
87+
<NeoTabPane empty>
88+
{@render content('Empty')}
89+
</NeoTabPane>
90+
<NeoTabPane tabId="button">
91+
{@render content('Button')}
92+
</NeoTabPane>
93+
<NeoTabPane tabId="icon">
94+
{@render content('Icon')}
95+
</NeoTabPane>
96+
<NeoTabPane tabId="reversed">
97+
{@render content('Reversed')}
98+
</NeoTabPane>
99+
100+
{#each added as { text, tabId } (tabId)}
101+
<NeoTabPane {tabId}>
102+
{@render content(text)}
105103
</NeoTabPane>
106-
107-
{#each added as { text, tabId } (tabId)}
108-
<NeoTabPane {tabId}>
109-
{@render content(text)}
110-
</NeoTabPane>
111-
{/each}
112-
</TransitionContainer>
113-
</NeoCard>
104+
{/each}
105+
</NeoTabsCard>
114106
{/snippet}
115107

116108
{#snippet group(props: NeoTabsProps = {})}
117-
<div class="column">
118-
<NeoTabs {panes} {active} {onclose} {onadd} {...options} {...props}>
109+
<div class="column" class:vertical={options.vertical}>
110+
<NeoTabs {panes} {active} {onclose} {onadd} {onchange} {...options} {...props}>
119111
{@render tabs()}
120112
</NeoTabs>
121113
</div>
@@ -138,8 +130,27 @@
138130
<style lang="scss">
139131
@use 'src/lib/styles/common/flex' as flex;
140132
133+
.content {
134+
flex: 0 1 37.5rem;
135+
max-width: 37.5rem;
136+
}
137+
138+
:global(.panel) {
139+
min-width: 34rem;
140+
min-height: 20rem;
141+
}
142+
141143
.column {
142144
@include flex.column($center: true, $gap: var(--neo-gap-lg));
145+
146+
&.vertical {
147+
flex-direction: row;
148+
149+
:global(.panel) {
150+
min-width: 28.5rem;
151+
min-height: 24rem;
152+
}
153+
}
143154
}
144155
145156
.row {
@@ -149,14 +160,4 @@
149160
justify-content: center;
150161
margin: 2rem 0;
151162
}
152-
153-
.content {
154-
flex: 0 1 37.5rem;
155-
max-width: 37.5rem;
156-
}
157-
158-
.panel {
159-
min-width: 37.5rem;
160-
min-height: 15rem;
161-
}
162163
</style>

src/lib/buttons/NeoButtonGroup.svelte

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import type { NeoButtonGroup } from '~/buttons/neo-button-group.model.js';
2+
import type { NeoButtonGroupProps } from '~/buttons/neo-button-group.model.js';
33
44
import { toAction, toActionProps, toTransition, toTransitionProps } from '~/utils/action.utils.js';
55
@@ -42,7 +42,7 @@
4242
4343
// Other props
4444
...rest
45-
}: NeoButtonGroup = $props();
45+
}: NeoButtonGroupProps = $props();
4646
/* eslint-enable prefer-const */
4747
4848
const inFn = $derived(toTransition(inAction ?? transitionAction));
@@ -96,11 +96,14 @@
9696

9797
<style lang="scss">
9898
@use 'src/lib/styles/mixin' as mixin;
99-
@use 'src/lib/styles/common/flex' as flex;
10099
101100
.neo-button-group {
102-
@include flex.row($flex: 0 1 auto, $center: true, $gap: var(--neo-btn-grp-gap, 0.25rem));
103-
101+
display: inline-flex;
102+
flex: 0 1 auto;
103+
flex-flow: row wrap;
104+
gap: var(--neo-btn-grp-gap, 0.25rem);
105+
align-items: center;
106+
justify-content: center;
104107
box-sizing: border-box;
105108
width: fit-content;
106109
margin: var(--neo-shadow-margin, 0.25rem);

src/lib/buttons/neo-button-group.model.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ export type NeoButtonGroupContext = {
6161
vertical?: boolean;
6262
};
6363

64-
export type NeoButtonGroup = {
64+
export type NeoButtonGroupProps = {
6565
/**
66-
* Optional snippet to display as the button content.
66+
* Snippet to display as the button content.
6767
*/
6868
children?: Snippet<[NeoButtonGroupContext]>;
6969

src/lib/buttons/neo-button.model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export type NeoButtonProps = {
77
// Snippets
88

99
/**
10-
* Optional snippet to display as the button content.
10+
* Snippet to display as the button content.
1111
*/
1212
children?: Snippet;
1313
/**

src/lib/cards/NeoCard.svelte

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
<svelte:element
5959
this={tag}
6060
bind:this={ref}
61-
class="neo-card"
6261
class:borderless
6362
class:glass
6463
class:flat={!elevation}
@@ -70,14 +69,14 @@
7069
in:inFn={inProps}
7170
{...rest}
7271
{style}
72+
class={['neo-card', rest.class].filter(Boolean).join(' ')}
7373
>
7474
{@render children?.()}
7575
</svelte:element>
7676

7777
<style lang="scss">
7878
.neo-card {
7979
display: flex;
80-
flex-direction: column;
8180
box-sizing: border-box;
8281
width: calc(100% - var(--neo-shadow-margin, 0.25rem) * 2);
8382
margin: var(--neo-shadow-margin, 0.25rem);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { Snippet } from 'svelte';
2+
import type { HTMLNeoBaseElement } from '~/utils/html-element.utils.js';
3+
4+
export type NeoTransitionContainerProps = {
5+
// Snippets
6+
/**
7+
* Snippet to display as the container content.
8+
*/
9+
children?: Snippet;
10+
11+
// States
12+
/**
13+
* The HTML tag to use for the container.
14+
* @default 'div'
15+
*/
16+
tag?: keyof HTMLElementTagNameMap;
17+
18+
// Styles
19+
20+
/**
21+
* Overflow style.
22+
*/
23+
overflow: CSSStyleDeclaration['overflow'];
24+
} & HTMLNeoBaseElement;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script lang="ts">
2+
import type { NeoTransitionContainerProps } from '~/container/NeoTransitionContainer.model.js';
3+
4+
const {
5+
// Snippets
6+
children,
7+
8+
// States
9+
tag = 'div',
10+
11+
// Styles
12+
overflow = 'hidden',
13+
14+
// Other props
15+
...rest
16+
}: NeoTransitionContainerProps = $props();
17+
</script>
18+
19+
<svelte:element this={tag} class="neo-transition-container" style:overflow {...rest}>
20+
{@render children?.()}
21+
</svelte:element>
22+
23+
<style lang="scss">
24+
.neo-transition-container {
25+
display: grid;
26+
grid-template-areas: 'transition';
27+
28+
:global(> *) {
29+
grid-area: transition;
30+
}
31+
}
32+
</style>

src/lib/container/TransitionContainer.svelte

Lines changed: 0 additions & 20 deletions
This file was deleted.

src/lib/nav/NeoTab.svelte

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
const context = getTabContext();
3939
const active = $derived(context?.active === tabId);
4040
const disabled = $derived(rest.disabled || (rest.disabled !== false && context?.disabled));
41-
const closeable = $derived(close || (close !== false && context?.closeable));
41+
const closeable = $derived(close || (close !== false && context?.close));
4242
const transition = $derived(context?.vertical ? height : width);
4343
const slide = $derived(context?.slide);
4444
@@ -56,16 +56,18 @@
5656
5757
$effect(() => {
5858
waitForTick();
59-
if (!ref) return;
60-
untrack(() => context?.register(tabId, ref!, value));
59+
untrack(() => {
60+
if (!ref) return;
61+
context?.register(tabId, { ref, value });
62+
});
6163
return () => context?.remove(tabId);
6264
});
6365
6466
const onClose = (e: MouseEvent) => {
6567
e.preventDefault();
6668
e.stopPropagation();
67-
onclose?.(tabId, value, ref);
68-
context?.onClose(tabId, value, ref);
69+
onclose?.(tabId);
70+
context?.onClose(tabId);
6971
};
7072
7173
const useFn = $derived(toAction(tabProps?.use));
@@ -102,6 +104,8 @@
102104

103105
<style lang="scss">
104106
.neo-tab {
107+
display: flex;
108+
105109
:global(.neo-button:active),
106110
:global(.neo-button.pressed),
107111
:global(.neo-button:focus-visible),
@@ -112,6 +116,10 @@
112116
}
113117
}
114118
119+
:global(.neo-button .icon-close:focus-visible) {
120+
transition: none;
121+
}
122+
115123
&.slide {
116124
:global(.neo-button:active .icon-close),
117125
:global(.neo-button.pressed .icon-close) {

0 commit comments

Comments
 (0)