Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
bd82427
toast svelte
Hugos68 Sep 8, 2025
90f6af6
svelte structure
Hugos68 Sep 8, 2025
8995685
stuff
Hugos68 Sep 8, 2025
cc35f3b
toast
Hugos68 Sep 8, 2025
6009ab0
fix svelte tests
Hugos68 Sep 8, 2025
76522df
fix style
Hugos68 Sep 8, 2025
d66ea76
Merge branch 'main' into feat/toast
Hugos68 Sep 9, 2025
a0f51e2
Merge branch 'main' into feat/toast
Hugos68 Sep 9, 2025
410fd23
update conventions
Hugos68 Sep 9, 2025
e7c073f
Merge branch 'main' into feat/toast
Hugos68 Sep 9, 2025
d77b9e4
Merge branch 'main' into feat/toast
Hugos68 Sep 9, 2025
219758c
add action trigger
Hugos68 Sep 9, 2025
2d84c85
fix
Hugos68 Sep 9, 2025
5956cbe
Merge branch 'main' into feat/toast
Hugos68 Sep 9, 2025
33d1d67
styling
Hugos68 Sep 10, 2025
9b1c0b1
toast docs
Hugos68 Sep 11, 2025
0729902
Merge branch 'main' into feat/toast
Hugos68 Sep 11, 2025
8d0eee5
Merge branch 'main' into feat/toast
endigo9740 Sep 11, 2025
287a4b3
Merge branch 'main' into feat/toast
Hugos68 Sep 12, 2025
d128352
Merge branch 'feat/toast' of https://github.com/skeletonlabs/skeleton…
Hugos68 Sep 12, 2025
c5454de
Merge branch 'main' into feat/toast
Hugos68 Sep 12, 2025
2caf925
Merge branch 'main' into feat/toast
Hugos68 Sep 12, 2025
a65558e
format
Hugos68 Sep 12, 2025
026a138
feedback
Hugos68 Sep 12, 2025
8fe8bbf
toast updates
Hugos68 Sep 12, 2025
6803d66
fix key
Hugos68 Sep 12, 2025
765d2cd
doc
Hugos68 Sep 12, 2025
b39610f
format
Hugos68 Sep 12, 2025
4f4a91f
icon for metadata example
Hugos68 Sep 12, 2025
9be79b5
rename
Hugos68 Sep 12, 2025
c24bbe4
fix
Hugos68 Sep 12, 2025
848b69b
fix
Hugos68 Sep 12, 2025
7b6e603
Style and doc updates
endigo9740 Sep 12, 2025
5a1c3e0
Style refinement
endigo9740 Sep 12, 2025
c018878
Added changeset
endigo9740 Sep 12, 2025
72460ea
Ammended changeset
endigo9740 Sep 12, 2025
338d497
Merge branch 'main' into feat/toast
Hugos68 Sep 12, 2025
871836f
Merge branch 'feat/toast' of https://github.com/skeletonlabs/skeleton…
Hugos68 Sep 12, 2025
2b45716
update types
Hugos68 Sep 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/clear-places-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@skeletonlabs/skeleton-common": minor
"@skeletonlabs/skeleton-svelte": minor
"@skeletonlabs/skeleton-react": minor
---

feat: toast
17 changes: 17 additions & 0 deletions packages/skeleton-common/src/classes/toast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { defineSkeletonClasses } from '../internal/define-skeleton-classes' with { type: 'macro' };

export const classesToast = defineSkeletonClasses({
root: [
'card p-3 w-full md:w-md ring flex items-center gap-2',
'data-[type=info]:preset-filled-surface-50-950 data-[type=info]:ring-surface-200-800',
'data-[type=success]:preset-filled-success-500',
'data-[type=warning]:preset-filled-warning-500',
'data-[type=error]:preset-filled-error-500'
],
group: '',
message: 'flex-1',
title: 'font-medium text-sm',
description: 'text-sm',
actionTrigger: 'btn preset-filled',
closeTrigger: 'btn-icon hover:preset-tonal'
});
1 change: 1 addition & 0 deletions packages/skeleton-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './classes/progress-linear';
export * from './classes/rating-group';
export * from './classes/switch';
export * from './classes/tabs';
export * from './classes/toast';
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useContext } from 'react';
import type { HTMLAttributes } from '@/internal/html-attributes';
import { mergeProps } from '@zag-js/react';
import { ToastRootContext } from '../modules/root-context.js';
import { classesToast } from '@skeletonlabs/skeleton-common';
import type { PropsWithElement } from '@/internal/props-with-element';

export interface ToastActionTriggerProps extends PropsWithElement, HTMLAttributes<'button'> {}

export default function (props: ToastActionTriggerProps) {
const rootContext = useContext(ToastRootContext);

const { element, children, ...restAttributes } = props;

const attributes = mergeProps(rootContext.api.getActionTriggerProps(), { className: classesToast.actionTrigger }, restAttributes);

return element ? element({ attributes }) : <button {...attributes}>{children}</button>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useContext } from 'react';
import type { HTMLAttributes } from '@/internal/html-attributes';
import { mergeProps } from '@zag-js/react';
import { ToastRootContext } from '../modules/root-context.js';
import { classesToast } from '@skeletonlabs/skeleton-common';
import X from '@/internal/components/x';
import type { PropsWithElement } from '@/internal/props-with-element';

export interface ToastCloseTriggerProps extends PropsWithElement, HTMLAttributes<'button'> {}

export default function (props: ToastCloseTriggerProps) {
const rootContext = useContext(ToastRootContext);

const { element, children = <X />, ...restAttributes } = props;

const attributes = mergeProps(rootContext.api.getCloseTriggerProps(), { className: classesToast.closeTrigger }, restAttributes);

return element ? element({ attributes }) : <button {...attributes}>{children}</button>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useContext } from 'react';
import type { HTMLAttributes } from '@/internal/html-attributes';
import { mergeProps } from '@zag-js/react';
import { ToastRootContext } from '../modules/root-context';
import { classesToast } from '@skeletonlabs/skeleton-common';
import type { PropsWithElement } from '@/internal/props-with-element';

export interface ToastDescriptionProps extends PropsWithElement, HTMLAttributes<'div'> {}

export default function (props: ToastDescriptionProps) {
const rootContext = useContext(ToastRootContext);

const { element, children, ...restAttributes } = props;

const attributes = mergeProps(rootContext.api.getDescriptionProps(), { className: classesToast.description }, restAttributes);

return element ? element({ attributes }) : <div {...attributes}>{children}</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useContext, type ReactNode } from 'react';
import { ToastGroupContext, type ToastGroupContextType } from '../modules/group-context';

export interface ToastGroupContextProps {
children: (context: ToastGroupContextType) => ReactNode;
}

export default function (props: ToastGroupContextProps) {
const groupContext = useContext(ToastGroupContext);

return props.children(groupContext);
}
31 changes: 31 additions & 0 deletions packages/skeleton-react/src/components/toast/anatomy/group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useId } from 'react';
import type { JSX } from 'react';
import type { HTMLAttributes } from '@/internal/html-attributes';
import { useMachine, normalizeProps, mergeProps } from '@zag-js/react';
import { classesToast } from '@skeletonlabs/skeleton-common';
import { group, type Store, type Props } from '@zag-js/toast';
import { ToastGroupContext } from '../modules/group-context';
import type { PropsWithElement } from '@/internal/props-with-element';

export interface ToastGroupProps extends PropsWithElement, Omit<HTMLAttributes<'div'>, 'id' | 'dir' | 'children'> {
toaster: Store;
children?: (toast: Props) => JSX.Element | null;
}

export default function (props: ToastGroupProps) {
const { element, children, toaster, ...restAttributes } = props;

const service = useMachine(group.machine, {
id: useId(),
store: toaster
});
const api = group.connect(service, normalizeProps);

const attributes = mergeProps(api.getGroupProps(), { className: classesToast.group }, restAttributes);

return (
<ToastGroupContext.Provider value={{ groupApi: api, groupService: service }}>
{element ? element({ attributes }) : <div {...attributes}>{api.getToasts().map((toast) => children?.(toast))}</div>}
</ToastGroupContext.Provider>
);
}
14 changes: 14 additions & 0 deletions packages/skeleton-react/src/components/toast/anatomy/message.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { mergeProps } from '@zag-js/react';
import { classesToast } from '@skeletonlabs/skeleton-common';
import type { HTMLAttributes } from '@/internal/html-attributes';
import type { PropsWithElement } from '@/internal/props-with-element';

export interface ToastMessageProps extends PropsWithElement, HTMLAttributes<'div'> {}

export default function (props: ToastMessageProps) {
const { element, children, ...restAttributes } = props;

const attributes = mergeProps({ className: classesToast.message }, restAttributes);

return element ? element({ attributes }) : <div {...attributes}>{children}</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useContext, type ReactNode } from 'react';
import { ToastRootContext, type ToastRootContextType } from '../modules/root-context';

export interface ToastRootContextProps {
children: (context: ToastRootContextType) => ReactNode;
}

export default function (props: ToastRootContextProps) {
const rootContext = useContext(ToastRootContext);

return props.children(rootContext);
}
61 changes: 61 additions & 0 deletions packages/skeleton-react/src/components/toast/anatomy/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useContext, useId } from 'react';
import type { HTMLAttributes } from '@/internal/html-attributes';
import { useMachine, normalizeProps, mergeProps } from '@zag-js/react';
import { classesToast } from '@skeletonlabs/skeleton-common';
import { machine, connect, type Options } from '@zag-js/toast';
import { ToastRootContext } from '../modules/root-context';
import { ToastGroupContext } from '../modules/group-context';
import type { PropsWithElement } from '@/internal/props-with-element';

export interface ToastRootProps extends PropsWithElement, Omit<HTMLAttributes<'div'>, 'id' | 'dir'> {
toast: Options;
}

export default function (props: ToastRootProps) {
const groupContext = useContext(ToastGroupContext);

const { element, children, toast, ...restAttributes } = props;

const service = useMachine(machine, {
id: useId(),
parent: groupContext.groupService,
...toast
});
const api = connect(service, normalizeProps);

const attributes = mergeProps(api.getRootProps(), { className: classesToast.root }, restAttributes);

return (
<>
<ToastRootContext.Provider value={{ api }}>
<div {...api.getGhostBeforeProps()}></div>
{element ? element({ attributes }) : <div {...attributes}>{children}</div>}
<div {...api.getGhostAfterProps()}></div>
</ToastRootContext.Provider>
<style>{`
[data-part='root'] {
translate: var(--x) var(--y);
scale: var(--scale);
z-index: var(--z-index);
height: var(--height);
opacity: var(--opacity);
will-change: translate, opacity, scale;
}
[data-part='root'] {
transition:
translate 400ms,
scale 400ms,
opacity 400ms;
transition-timing-function: cubic-bezier(0.21, 1.02, 0.73, 1);
}
[data-part='root'][data-state='closed'] {
transition:
translate 400ms,
scale 400ms,
opacity 200ms;
transition-timing-function: cubic-bezier(0.06, 0.71, 0.55, 1);
}
`}</style>
</>
);
}
18 changes: 18 additions & 0 deletions packages/skeleton-react/src/components/toast/anatomy/title.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useContext } from 'react';
import type { HTMLAttributes } from '@/internal/html-attributes';
import { mergeProps } from '@zag-js/react';
import { ToastRootContext } from '../modules/root-context';
import { classesToast } from '@skeletonlabs/skeleton-common';
import type { PropsWithElement } from '@/internal/props-with-element';

export interface ToastTitleProps extends PropsWithElement, HTMLAttributes<'div'> {}

export default function (props: ToastTitleProps) {
const rootContext = useContext(ToastRootContext);

const { element, children, ...restAttributes } = props;

const attributes = mergeProps(rootContext.api.getTitleProps(), { className: classesToast.title }, restAttributes);

return element ? element({ attributes }) : <div {...attributes}>{children}</div>;
}
12 changes: 12 additions & 0 deletions packages/skeleton-react/src/components/toast/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export { Toast } from './modules/anatomy';
export { createStore as createToaster } from '@zag-js/toast';
export type { ToastGroupProps } from './anatomy/group';
export type { ToastGroupContextProps } from './anatomy/group-context';
export type { ToastRootProps } from './anatomy/root';
export type { ToastRootContextProps } from './anatomy/root-context';
export type { ToastMessageProps } from './anatomy/message';
export type { ToastTitleProps } from './anatomy/title';
export type { ToastDescriptionProps } from './anatomy/description';
export type { ToastCloseTriggerProps } from './anatomy/close-trigger';
export type { ToastGroupContextType as ToastGroupContext } from './modules/group-context';
export type { ToastRootContextType as ToastRootContext } from './modules/root-context';
20 changes: 20 additions & 0 deletions packages/skeleton-react/src/components/toast/modules/anatomy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Root from '../anatomy/root';
import RootContext from '../anatomy/root-context';
import Group from '../anatomy/group';
import GroupContext from '../anatomy/group-context';
import Message from '../anatomy/message';
import Title from '../anatomy/title';
import Description from '../anatomy/description';
import ActionTrigger from '../anatomy/action-trigger';
import CloseTrigger from '../anatomy/close-trigger';

export const Toast = Object.assign(Root, {
Context: RootContext,
Group: Group,
GroupContext: GroupContext,
Message: Message,
Title: Title,
Description: Description,
ActionTrigger: ActionTrigger,
CloseTrigger: CloseTrigger
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { GroupService, GroupApi } from '@zag-js/toast';
import { createContext } from 'react';

export interface ToastGroupContextType {
groupApi: GroupApi;
groupService: GroupService;
}

export const ToastGroupContext = createContext<ToastGroupContextType>(null!);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Api } from '@zag-js/toast';
import { createContext } from 'react';

export interface ToastRootContextType {
api: Api;
}

export const ToastRootContext = createContext<ToastRootContextType>(null!);
1 change: 1 addition & 0 deletions packages/skeleton-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './components/progress-linear/index';
export * from './components/rating-group/index';
export * from './components/switch/index';
export * from './components/tabs/index';
export * from './components/toast/index';
18 changes: 18 additions & 0 deletions packages/skeleton-react/src/internal/components/x.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default function () {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
}
57 changes: 57 additions & 0 deletions packages/skeleton-react/test/components/toast/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import Toast from './toast';

describe('toast', () => {
describe('group', () => {
it('renders', () => {
render(<Toast />);
expect(screen.getByTestId('group')).toBeInTheDocument();
});
});

describe.skip('root', () => {
it('renders', async () => {
render(<Toast />);
await waitFor(() => {
expect(screen.getByTestId('root')).toBeInTheDocument();
});
});
});

describe.skip('title', () => {
it('renders', async () => {
render(<Toast />);
await waitFor(() => {
expect(screen.getByTestId('title')).toBeInTheDocument();
});
});
});

describe.skip('description', () => {
it('renders', async () => {
render(<Toast />);
await waitFor(() => {
expect(screen.getByTestId('description')).toBeInTheDocument();
});
});
});

describe.skip('action trigger', () => {
it('renders', async () => {
render(<Toast />);
await waitFor(() => {
expect(screen.getByTestId('action-trigger')).toBeInTheDocument();
});
});
});

describe.skip('close trigger', () => {
it('renders', async () => {
render(<Toast />);
await waitFor(() => {
expect(screen.getByTestId('close-trigger')).toBeInTheDocument();
});
});
});
});
21 changes: 21 additions & 0 deletions packages/skeleton-react/test/components/toast/toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Toast, createToaster } from '@/index';
import { useEffect } from 'react';

export default function () {
const toaster = createToaster({});
useEffect(() => {
toaster.create({});
}, []);
return (
<Toast.Group toaster={toaster} data-testid="group">
{(toast) => (
<Toast key={toast.id} toast={toast} data-testid="root">
<Toast.Title data-testid="title" />
<Toast.Description data-testid="description" />
<Toast.ActionTrigger data-testid="action-trigger" />
<Toast.CloseTrigger data-testid="close-trigger" />
</Toast>
)}
</Toast.Group>
);
}
Loading