Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
167 changes: 154 additions & 13 deletions apps/www/src/app/examples/combobox/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,161 @@
'use client';
import { Combobox, Flex, Slider } from '@raystack/apsara';

const Page = () => {
import {
Button,
Combobox,
Dialog,
Flex,
Toast,
toastManager,
useToastManager
} from '@raystack/apsara';

const FRUITS = ['Apple', 'Banana', 'Blueberry', 'Grapes', 'Pineapple'];

function ToastButton({
label,
source,
type
}: {
label: string;
source: string;
type?: 'success' | 'info' | 'warning';
}) {
// Demonstrates the hook flavor — works because every button is a
// descendant of <Toast.Provider> in the tree below.
const { add } = useToastManager();
return (
<Flex
style={{
height: '100vh',
width: '100%',
backgroundColor: 'var(--rs-color-background-base-primary)',
padding: '80px'
}}
direction='column'
gap={8}
<Button
onClick={() =>
add({
title: `Toast from ${source}`,
description: 'Toasts portal to the top-level provider.',
type
})
}
>
<Slider defaultValue={50} thumbSize='small' label='Slider Label' />
</Flex>
{label}
</Button>
);
}

const Page = () => {
return (
<Toast.Provider position='bottom-right'>
<Flex
direction='column'
gap={5}
style={{
minHeight: '100vh',
padding: '80px',
background: 'var(--rs-color-background-base-primary)'
}}
>
<h1>Combobox + nested dialogs + toast</h1>
<p>
Toasts triggered from any depth of nested dialog still render at the
root viewport. Each level has its own combobox and toast button.
</p>

<Combobox>
<Combobox.Input placeholder='Pick a fruit' width={240} />
<Combobox.Content>
{FRUITS.map(f => (
<Combobox.Item key={f} value={f.toLowerCase()}>
{f}
</Combobox.Item>
))}
</Combobox.Content>
</Combobox>

<Flex gap={3}>
{/* Singleton flavor — usable from anywhere, including non-React code. */}
<Button
onClick={() =>
toastManager.add({
title: 'Toast from root',
description: 'Triggered via the singleton toastManager.',
type: 'success'
})
}
>
Show toast (root)
</Button>

<Dialog>
<Dialog.Trigger render={<Button variant='outline' />}>
Open dialog 1
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Dialog 1</Dialog.Title>
<Dialog.Description>
Triggers a toast and opens a nested dialog.
</Dialog.Description>
</Dialog.Header>
<Dialog.Body>
<Flex direction='column' gap={3}>
<Combobox>
<Combobox.Input
placeholder='Pick a fruit (dialog 1)'
width={300}
/>
<Combobox.Content>
{FRUITS.map(f => (
<Combobox.Item key={f} value={f.toLowerCase()}>
{f}
</Combobox.Item>
))}
</Combobox.Content>
</Combobox>
<Flex gap={3}>
<ToastButton
label='Show toast (dialog 1)'
source='dialog 1'
type='info'
/>

<Dialog>
<Dialog.Trigger render={<Button variant='outline' />}>
Open dialog 2
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Dialog 2 (nested)</Dialog.Title>
<Dialog.Description>
A toast fired from here still appears at the root
viewport — even though this dialog is portaled.
</Dialog.Description>
</Dialog.Header>
<Dialog.Body>
<Flex gap={3}>
<ToastButton
label='Show toast (dialog 2)'
source='dialog 2'
type='warning'
/>
</Flex>
</Dialog.Body>
<Dialog.Footer>
<Dialog.Close render={<Button variant='outline' />}>
Close
</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
</Flex>
</Flex>
</Dialog.Body>
<Dialog.Footer>
<Dialog.Close render={<Button variant='outline' />}>
Close
</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
</Flex>
</Flex>
</Toast.Provider>
);
};

Expand Down
123 changes: 123 additions & 0 deletions apps/www/src/content/docs/components/toast/demo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,54 @@
'use client';

export const getCode = (
_updatedProps: Record<string, any>,
allProps: Record<string, any>
) => {
const { title, description, type, actionButton } = allProps;
const opts: string[] = [];
if (title && title !== '') opts.push(`title: "${title}"`);
if (description && description !== '')
opts.push(`description: "${description}"`);
Comment on lines +9 to +11
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unescaped interpolation in getCode will produce malformed code snippets.

If a user types a double-quote in the title or description playground controls (e.g., He said "hello"), the generated snippet becomes title: "He said "hello"" — syntactically invalid JavaScript. A backslash has the same effect.

🛠️ Proposed fix
-  if (title && title !== '') opts.push(`title: "${title}"`);
-  if (description && description !== '')
-    opts.push(`description: "${description}"`);
+  const escape = (s: string) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
+  if (title && title !== '') opts.push(`title: "${escape(title)}"`);
+  if (description && description !== '')
+    opts.push(`description: "${escape(description)}"`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (title && title !== '') opts.push(`title: "${title}"`);
if (description && description !== '')
opts.push(`description: "${description}"`);
const escape = (s: string) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
if (title && title !== '') opts.push(`title: "${escape(title)}"`);
if (description && description !== '')
opts.push(`description: "${escape(description)}"`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/www/src/content/docs/components/toast/demo.ts` around lines 9 - 11, The
generated code snippets are malformed when title or description contain
quotes/backslashes; change the string interpolation in the opts.push calls so
the values are safely escaped (e.g., use JSON.stringify(title) and
JSON.stringify(description) instead of `"${title}"`/`"${description}"`) when
building the options array (the lines that call opts.push for title and
description), so the produced snippet contains a properly escaped JS string.

if (type && type !== 'default') opts.push(`type: "${type}"`);
if (actionButton)
opts.push(`actionProps: { children: "Action", onClick: () => {} }`);

const optsStr = opts.length ? `{ ${opts.join(', ')} }` : '{}';

return `
<Toast.Provider>
<Button onClick={() => toastManager.add(${optsStr})}>
Show toast
</Button>
</Toast.Provider>`;
};

export const playground = {
type: 'playground',
controls: {
title: {
type: 'text',
initialValue: 'Order placed',
defaultValue: ''
},
description: {
type: 'text',
initialValue: 'Monday, 7 Oct 2024 at 10:20 AM',
defaultValue: ''
},
type: {
type: 'select',
options: ['default', 'success', 'error', 'warning', 'info', 'loading'],
defaultValue: 'default'
},
actionButton: {
type: 'checkbox',
defaultValue: false
}
},
getCode
};

export const preview = {
type: 'code',
code: `
Expand Down Expand Up @@ -86,6 +135,52 @@ export const descriptionDemo = {
</Flex>`
};

export const leadingIconDemo = {
type: 'code',
tabs: [
{
name: 'Custom icons',
code: `
<Flex gap="medium" wrap="wrap">
<Button onClick={() => toastManager.add({
title: "Saved successfully",
type: "success",
leadingIcon: <CheckCircledIcon />
})}>
Success with icon
</Button>
<Button onClick={() => toastManager.add({
title: "Upload failed",
description: "We couldn't upload your file. Please try again.",
type: "error",
leadingIcon: <CrossCircledIcon />
})}>
Error with icon
</Button>
<Button onClick={() => toastManager.add({
title: "FYI: System update available",
type: "info",
leadingIcon: <InfoCircledIcon />
})}>
Info with icon
</Button>
</Flex>`
},
{
name: 'No icon',
code: `
<Button onClick={() => toastManager.add({
title: "Plain success toast",
description: "No icon at all, even though type is success.",
type: "success",
leadingIcon: null
})}>
Success without icon
</Button>`
}
]
};

export const actionDemo = {
type: 'code',
code: `
Expand Down Expand Up @@ -237,6 +332,34 @@ export const positionDemo = {
]
};

export const hookDemo = {
type: 'code',
code: `
function HookDemo() {
// Hook usage lives in an inner component so it runs inside the Provider.
function Inner() {
const { add, toasts } = useToastManager();
return (
<Flex direction="column" gap="medium">
<Button onClick={() => add({
title: "Triggered via hook",
description: "Same leadingIcon-aware API as the singleton manager.",
type: "success"
})}>
Show toast
</Button>
<span>Active toasts: {toasts.length}</span>
</Flex>
)
}
return (
<Toast.Provider>
<Inner />
</Toast.Provider>
)
}`
};

export const updateDemo = {
type: 'code',
code: `
Expand Down
Loading
Loading