diff --git a/apps/www/src/app/not-found.tsx b/apps/www/src/app/not-found.tsx new file mode 100644 index 000000000..d0ab0c94c --- /dev/null +++ b/apps/www/src/app/not-found.tsx @@ -0,0 +1,19 @@ +import { Flex, Separator, Text } from '@raystack/apsara'; + +export default function NotFound() { + return ( + + + 404 + + + This page could not be found. + + ); +} diff --git a/apps/www/src/components/datatable-demo.tsx b/apps/www/src/components/datatable-demo.tsx index b56516ad0..482158948 100644 --- a/apps/www/src/components/datatable-demo.tsx +++ b/apps/www/src/components/datatable-demo.tsx @@ -2,11 +2,9 @@ import { Checkbox, DataTable, DataTableColumnDef, - Flex, - Switch, - Text + Flex } from '@raystack/apsara'; -import { useMemo, useState } from 'react'; +import { useMemo } from 'react'; const statuses = ['pending', 'processing', 'success', 'failed'] as const; @@ -31,12 +29,12 @@ export const columns: DataTableColumnDef[] = [ header: 'Status', styles: { cell: { - width: 120, + width: 180, flex: 'none', paddingLeft: 'var(--rs-space-7)' }, header: { - width: 120, + width: 180, flex: 'none', paddingLeft: 'var(--rs-space-7)' } @@ -110,16 +108,29 @@ export const columns: DataTableColumnDef[] = [ ]; const DataTableDemo = () => { - const [virtualized, setVirtualized] = useState(true); - const data = useMemo(() => generateData(100), []); + return ( + +
+ + + + +
+
+ ); +}; + +const DataTableVirtualizedDemo = () => { + const data = useMemo(() => generateData(1000), []); return ( - - - - Virtualized (100 rows) - +
{ defaultSort={{ name: 'email', order: 'asc' }} > - {virtualized ? ( - - ) : ( - - )} +
); }; -export default DataTableDemo; +export { DataTableDemo, DataTableVirtualizedDemo }; diff --git a/apps/www/src/components/demo/demo-playground.tsx b/apps/www/src/components/demo/demo-playground.tsx index 0fefbe869..7ef253f08 100644 --- a/apps/www/src/components/demo/demo-playground.tsx +++ b/apps/www/src/components/demo/demo-playground.tsx @@ -54,7 +54,8 @@ const getUpdatedProps = ( export default function DemoPlayground({ scope, controls, - getCode + getCode, + style }: DemoPlaygroundProps) { const searchParams = useSearchParams(); const router = useRouter(); @@ -98,7 +99,7 @@ export default function DemoPlayground({ return ( <> - +
- +
)}
- +
{Array.isArray(codePreview) ? ( diff --git a/apps/www/src/components/demo/demo.tsx b/apps/www/src/components/demo/demo.tsx index c28d4e871..5db96e688 100644 --- a/apps/www/src/components/demo/demo.tsx +++ b/apps/www/src/components/demo/demo.tsx @@ -36,8 +36,9 @@ import dayjs from 'dayjs'; import { Home, Info, Laugh, X } from 'lucide-react'; import NextLink from 'next/link'; import { Suspense } from 'react'; -import DataTableDemo from '../datatable-demo'; +import { DataTableDemo, DataTableVirtualizedDemo } from '../datatable-demo'; import DataTableSelectionDemo from '../datatable-selection-demo'; +import ChipInputDemo from '../inputfield-chip-demo'; import LinearMenuDemo from '../linear-dropdown-demo'; import PopoverColorPicker from '../popover-color-picker'; import DemoPlayground from './demo-playground'; @@ -55,6 +56,8 @@ export default function Demo(props: DemoProps) { OrganizationIcon, SidebarIcon, DataTableDemo, + DataTableVirtualizedDemo, + ChipInputDemo, DataTableSelectionDemo, LinearMenuDemo, PopoverColorPicker, diff --git a/apps/www/src/components/demo/styles.module.css b/apps/www/src/components/demo/styles.module.css index e82e16d28..03160e9e7 100644 --- a/apps/www/src/components/demo/styles.module.css +++ b/apps/www/src/components/demo/styles.module.css @@ -2,7 +2,7 @@ display: flex; flex-direction: column; border-radius: var(--rs-radius-2, 4px); - overflow: hidden; + overflow: clip; border: 1px solid var(--rs-color-border-base-primary); width: 100%; } @@ -15,6 +15,7 @@ .preview { flex: 1; + height: 100%; position: relative; display: flex; align-items: center; diff --git a/apps/www/src/components/demo/types.ts b/apps/www/src/components/demo/types.ts index 17f49060c..01de6f49f 100644 --- a/apps/www/src/components/demo/types.ts +++ b/apps/www/src/components/demo/types.ts @@ -1,3 +1,5 @@ +import { CSSProperties } from 'react'; + export type ScopeType = Record; type TabProps = { @@ -10,6 +12,7 @@ export type DemoPreviewProps = { tabs?: { name: string; code: string }[]; scope?: ScopeType; codePreview?: string | TabProps[]; + style?: CSSProperties; }; export type DemoPlaygroundProps = { @@ -17,6 +20,7 @@ export type DemoPlaygroundProps = { controls: ControlsType; getCode: GetCodeType; scope?: ScopeType; + style?: CSSProperties; }; export type DemoProps = { diff --git a/apps/www/src/components/inputfield-chip-demo.tsx b/apps/www/src/components/inputfield-chip-demo.tsx new file mode 100644 index 000000000..ef727a439 --- /dev/null +++ b/apps/www/src/components/inputfield-chip-demo.tsx @@ -0,0 +1,33 @@ +import { InputField } from '@raystack/apsara'; +import { useState } from 'react'; + +export default function ChipInputDemo() { + const [chips, setChips] = useState([ + { label: 'Tag1' }, + { label: 'Tag2' }, + { label: 'Tag3' }, + { label: 'Tag4' }, + { label: 'Tag5' } + ]); + const [input, setInput] = useState(''); + + return ( + setInput(e.target.value)} + onKeyDown={e => { + if (e.key === 'Enter' && input.trim()) { + setChips([...chips, { label: input.trim() }]); + setInput(''); + } + }} + chips={chips.map((c, i) => ({ + label: c.label, + onRemove: () => setChips(chips.filter((_, j) => j !== i)) + }))} + maxChipsVisible={4} + /> + ); +} diff --git a/apps/www/src/components/playground/calendar-examples.tsx b/apps/www/src/components/playground/calendar-examples.tsx index bdf67650c..8bc023e70 100644 --- a/apps/www/src/components/playground/calendar-examples.tsx +++ b/apps/www/src/components/playground/calendar-examples.tsx @@ -9,7 +9,7 @@ export function CalendarExamples() { - + ); diff --git a/apps/www/src/components/playground/data-table-examples.tsx b/apps/www/src/components/playground/data-table-examples.tsx index f06db31b9..6f1f95acf 100644 --- a/apps/www/src/components/playground/data-table-examples.tsx +++ b/apps/www/src/components/playground/data-table-examples.tsx @@ -1,6 +1,6 @@ 'use client'; -import DataTableDemo from '../datatable-demo'; +import { DataTableDemo } from '../datatable-demo'; import PlaygroundLayout from './playground-layout'; export function DataTableExamples() { diff --git a/apps/www/src/components/preview/preview.module.css b/apps/www/src/components/preview/preview.module.css index edb554a0b..ade96c9b5 100644 --- a/apps/www/src/components/preview/preview.module.css +++ b/apps/www/src/components/preview/preview.module.css @@ -1,9 +1,10 @@ .preview { - padding: 0; + padding: var(--rs-space-10); width: 100%; - height: 100%; + height: stretch; + flex: 1; display: flex; justify-content: center; align-items: center; - overflow: auto; -} \ No newline at end of file + overflow: visible; +} diff --git a/apps/www/src/components/preview/preview.tsx b/apps/www/src/components/preview/preview.tsx index eddce393b..4fe8267a9 100644 --- a/apps/www/src/components/preview/preview.tsx +++ b/apps/www/src/components/preview/preview.tsx @@ -2,6 +2,17 @@ import { cx } from 'class-variance-authority'; import { LivePreview } from 'react-live'; import styles from './preview.module.css'; -export default function Preview({ className }: { className?: string }) { - return ; +export default function Preview({ + className, + style +}: { + className?: string; + style?: React.CSSProperties; +}) { + return ( + + ); } diff --git a/apps/www/src/content/docs/components/accordion/demo.ts b/apps/www/src/content/docs/components/accordion/demo.ts index 6bfb7104e..751c486be 100644 --- a/apps/www/src/content/docs/components/accordion/demo.ts +++ b/apps/www/src/content/docs/components/accordion/demo.ts @@ -2,6 +2,10 @@ import { getPropsString } from '@/lib/utils'; +const styleDemo = { + alignItems: 'flex-start' +}; + export const getCode = (props: Record) => { return ` @@ -33,11 +37,13 @@ export const playground = { defaultValue: false } }, + style: styleDemo, getCode }; export const typeDemo = { type: 'code', + style: styleDemo, tabs: [ { name: 'Single', @@ -91,6 +97,7 @@ export const typeDemo = { }; export const controlledDemo = { type: 'code', + style: styleDemo, code: ` function ControlledAccordion() { const [value, setValue] = React.useState('item-1'); @@ -112,6 +119,7 @@ export const controlledDemo = { export const disabledDemo = { type: 'code', + style: styleDemo, code: ` @@ -131,6 +139,7 @@ export const disabledDemo = { export const customContentDemo = { type: 'code', + style: styleDemo, code: ` diff --git a/apps/www/src/content/docs/components/announcement-bar/demo.ts b/apps/www/src/content/docs/components/announcement-bar/demo.ts index eb445d4ba..063166ad7 100644 --- a/apps/www/src/content/docs/components/announcement-bar/demo.ts +++ b/apps/www/src/content/docs/components/announcement-bar/demo.ts @@ -19,5 +19,8 @@ export const playground = { actionLabel: { type: 'text', initialValue: 'Read More' }, actionIcon: { type: 'icon', defaultValue: '' } }, - getCode + getCode, + style: { + padding: 0 + } }; diff --git a/apps/www/src/content/docs/components/breadcrumb/demo.ts b/apps/www/src/content/docs/components/breadcrumb/demo.ts index c9eaee7de..8eb625540 100644 --- a/apps/www/src/content/docs/components/breadcrumb/demo.ts +++ b/apps/www/src/content/docs/components/breadcrumb/demo.ts @@ -4,11 +4,11 @@ import { getPropsString } from '@/lib/utils'; export const getCode = (props: any) => { return ` - Home + Home - Products + Products - Shoes + Shoes `; }; @@ -31,22 +31,22 @@ export const sizeDemo = { name: 'Small', code: ` - Home + Home - Products + Products - Shoes + Shoes ` }, { name: 'Medium', code: ` - Home + Home - Products + Products - Shoes + Shoes ` } ] @@ -57,18 +57,18 @@ export const separatorDemo = { code: ` - Home + Home | - Products + Products | - Shoes + Shoes - Home + Home - - Products + Products - - Shoes + Shoes ` }; @@ -77,11 +77,11 @@ export const ellipsisDemo = { type: 'code', code: ` - Home + Home - Shoes + Shoes ` }; @@ -89,16 +89,16 @@ export const dropdownDemo = { type: 'code', code: ` - Home + Home - Category + Category {console.log('Option 1')}}, { children: 'Option 2', onClick: () => {console.log('Option 2')}} ]}>Subcategory - Current Page + Current Page ` }; @@ -106,15 +106,15 @@ export const dropdownLinksDemo = { type: 'code', code: ` - Home + Home }, - { children: 'Clothing', render: }, + { children: 'Electronics', render: }, + { children: 'Clothing', render: }, { children: 'Books', onClick: () => {console.log('Books')}} ]}>Categories - Current + Current ` }; @@ -122,11 +122,11 @@ export const asDemo = { type: 'code', code: ` - }>Home + }>Home - }>Playground + }>Playground - Docs + Docs ` }; @@ -134,11 +134,11 @@ export const disabledDemo = { type: 'code', code: ` - Home + Home Loading… - Products + Products ` }; @@ -149,44 +149,44 @@ export const iconsDemo = { name: 'Leading Icon', code: ` - }>Home + }>Home - }>Documents + }>Documents - }>Settings + }>Settings ` }, { name: 'Trailing Icon', code: ` - }>Home + }>Home - }>Documents + }>Documents - }>Settings + }>Settings ` }, { name: 'Both Icons', code: ` - } trailingIcon={}>Home + } trailingIcon={}>Home - } trailingIcon={}>Documents + } trailingIcon={}>Documents - } trailingIcon={}>Settings + } trailingIcon={}>Settings ` }, { name: 'Only Icon', code: ` - }/> + }/> - }/> + }/> - }/> + }/> ` } ] diff --git a/apps/www/src/content/docs/components/calendar/demo.ts b/apps/www/src/content/docs/components/calendar/demo.ts index a9bfb2354..2117f781d 100644 --- a/apps/www/src/content/docs/components/calendar/demo.ts +++ b/apps/www/src/content/docs/components/calendar/demo.ts @@ -29,6 +29,10 @@ export const calendarDemo = { { name: 'With Loading', code: `` + }, + { + name: 'With Dropdowns', + code: `` } ] }; @@ -104,21 +108,21 @@ export const dateInfoDemo = { { name: 'With Date Info', code: ` - - - 25% -
- ) - }} -/>` + + + 25% + + ) + }} + />` } ] }; diff --git a/apps/www/src/content/docs/components/datatable/demo.ts b/apps/www/src/content/docs/components/datatable/demo.ts index 768299c32..cda0902ae 100644 --- a/apps/www/src/content/docs/components/datatable/demo.ts +++ b/apps/www/src/content/docs/components/datatable/demo.ts @@ -2,7 +2,51 @@ export const preview = { type: 'code', - code: `` + style: { + padding: 0 + }, + previewCode: false, + code: ``, + codePreview: [ + { + label: 'index.tsx', + code: ` + + + + ` + } + ] +}; + +export const virtualizedPreview = { + type: 'code', + style: { + padding: 0 + }, + previewCode: false, + code: ``, + codePreview: [ + { + label: 'index.tsx', + code: ` + /* Parent container must have a fixed height */ +
+ + + + +
` + } + ] }; export const rowSelectionDemo = { diff --git a/apps/www/src/content/docs/components/datatable/index.mdx b/apps/www/src/content/docs/components/datatable/index.mdx index 0a5040fce..0df920aa8 100644 --- a/apps/www/src/content/docs/components/datatable/index.mdx +++ b/apps/www/src/content/docs/components/datatable/index.mdx @@ -4,7 +4,7 @@ description: An advanced React table that supports filtering, sorting, and pagin source: packages/raystack/components/datatable --- -import { preview, rowSelectionDemo } from "./demo.ts"; +import { preview, virtualizedPreview, rowSelectionDemo } from "./demo.ts"; @@ -82,17 +82,7 @@ For large datasets, use `DataTable.VirtualizedContent` for better performance. P For large datasets, use `DataTable.VirtualizedContent` for better performance. Parent container must have a fixed height. -```tsx -
- - - - -
-``` + #### Fixed Width Columns diff --git a/apps/www/src/content/docs/components/input-field/demo.ts b/apps/www/src/content/docs/components/input-field/demo.ts index 65d89a6b7..a9e525cf7 100644 --- a/apps/www/src/content/docs/components/input-field/demo.ts +++ b/apps/www/src/content/docs/components/input-field/demo.ts @@ -127,3 +127,45 @@ export const sizeChipDemo = { /> ` }; + +export const interactiveChipDemo = { + type: 'code', + style: { + padding: 0 + }, + previewCode: false, + code: ``, + codePreview: [ + { + label: 'index.tsx', + code: ` + const [chips, setChips] = React.useState([ + { label: "Tag1" }, + { label: "Tag2" }, + { label: "Tag3" }, + { label: "Tag4" }, + { label: "Tag5" }, + + ]); + const [input, setInput] = React.useState(""); + + setInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && input.trim()) { + setChips([...chips, { label: input.trim() }]); + setInput(""); + } + }} + chips={chips.map((c, i) => ({ + label: c.label, + onRemove: () => setChips(chips.filter((_, j) => j !== i)) + }))} + maxChipsVisible={4} + />` + } + ] +}; diff --git a/apps/www/src/content/docs/components/input-field/index.mdx b/apps/www/src/content/docs/components/input-field/index.mdx index 09df2b316..7fef82ee1 100644 --- a/apps/www/src/content/docs/components/input-field/index.mdx +++ b/apps/www/src/content/docs/components/input-field/index.mdx @@ -14,6 +14,7 @@ import { widthDemo, sizeDemo, sizeChipDemo, + interactiveChipDemo, withFieldDemo, } from "./demo.ts"; @@ -71,6 +72,24 @@ Input field with leading and trailing icons. +### Interactive Chips + +Accepts an array of chips, each with a `label` and an optional `onRemove` callback. + +`maxChipsVisible` caps how many are rendered before collapsing the rest into an overflow counter. + +```tsx +chips?: Array<{ label: string; onRemove?: () => void }>; +maxChipsVisible?: number; // defaults to 2 +``` + +This is purely a presentation layer — it does not manage chip state. The wrapper component owns the array and decides when items are added or removed. + +The example below + appends a chip on `Enter`, removes one when its dismiss button is clicked. + + + ### Disabled State Input field in disabled state. diff --git a/apps/www/src/content/docs/components/navbar/demo.ts b/apps/www/src/content/docs/components/navbar/demo.ts index 0105a084f..fc8bf6665 100644 --- a/apps/www/src/content/docs/components/navbar/demo.ts +++ b/apps/www/src/content/docs/components/navbar/demo.ts @@ -1,5 +1,9 @@ 'use client'; +const styleDemo = { + padding: 0 +}; + export const preview = { type: 'code', code: ` @@ -19,7 +23,8 @@ export const preview = {
-
` + `, + style: styleDemo }; export const stickyDemo = { @@ -59,7 +64,8 @@ export const stickyDemo = {
` } - ] + ], + style: styleDemo }; export const hideOnScrollDemo = { @@ -77,7 +83,8 @@ export const hideOnScrollDemo = {
-
` + `, + style: styleDemo }; export const sectionsDemo = { @@ -138,7 +145,8 @@ export const sectionsDemo = {
` } - ] + ], + style: styleDemo }; export const accessibilityDemo = { @@ -190,5 +198,6 @@ export const accessibilityDemo = {
` } - ] + ], + style: styleDemo }; diff --git a/apps/www/src/content/docs/components/search/demo.ts b/apps/www/src/content/docs/components/search/demo.ts index 0e99b3dc6..d3c8c3891 100644 --- a/apps/www/src/content/docs/components/search/demo.ts +++ b/apps/www/src/content/docs/components/search/demo.ts @@ -32,9 +32,32 @@ export const sizeDemo = { export const clearDemo = { type: 'code', - code: ` - - - - ` + tabs: [ + { + name: 'Uncontrolled', + code: ` + + + ` + }, + { + name: 'Controlled', + code: ` + function ControlledSearch() { + const [value, setValue] = React.useState("Searchable text"); + + return ( + + setValue(e.target.value)} + showClearButton + onClear={() => setValue("")} + /> + + ); + }` + } + ] }; diff --git a/apps/www/src/content/docs/components/sidebar/demo.ts b/apps/www/src/content/docs/components/sidebar/demo.ts index 8473ab3b1..4ac89fb72 100644 --- a/apps/www/src/content/docs/components/sidebar/demo.ts +++ b/apps/www/src/content/docs/components/sidebar/demo.ts @@ -2,6 +2,10 @@ const mainAreaStyle = `{{ flex: 1, border: '2px dashed var(--rs-color-border-base-secondary)', margin: 'var(--rs-space-4)', boxSizing: 'border-box' }}`; +const styleDemo = { + padding: 0 +}; + const sidebarLayout = (sidebar: string) => ` ${sidebar.trim()} @@ -55,7 +59,8 @@ export const preview = { Help & Support - `) + `), + style: styleDemo }; export const positionDemo = { @@ -111,7 +116,8 @@ export const positionDemo = { `) } - ] + ], + style: styleDemo }; export const variantDemo = { @@ -180,7 +186,8 @@ export const variantDemo = { `) } - ] + ], + style: styleDemo }; export const stateDemo = { @@ -282,7 +289,8 @@ export const stateDemo = { `) } - ] + ], + style: styleDemo }; export const tooltipDemo = { @@ -310,7 +318,8 @@ export const tooltipDemo = { }>Help - `) + `), + style: styleDemo }; export const collapsibleDemo = { @@ -331,7 +340,8 @@ export const collapsibleDemo = { }>Analytics - `) + `), + style: styleDemo }; export const hideTooltipDemo = { @@ -355,7 +365,8 @@ export const hideTooltipDemo = { Help - `) + `), + style: styleDemo }; export const collapsibleGroupDemo = { @@ -387,7 +398,8 @@ export const collapsibleGroupDemo = { - `) + `), + style: styleDemo }; export const moreDemo = { @@ -432,5 +444,6 @@ export const moreDemo = { - `) + `), + style: styleDemo }; diff --git a/packages/raystack/components/accordion/accordion-root.tsx b/packages/raystack/components/accordion/accordion-root.tsx index 6544de9cd..be096404a 100644 --- a/packages/raystack/components/accordion/accordion-root.tsx +++ b/packages/raystack/components/accordion/accordion-root.tsx @@ -35,17 +35,23 @@ export const AccordionRoot = ({ ...rest }: AccordionRootProps) => { // Convert value to array format for Base UI - const baseValue = value - ? Array.isArray(value) - ? value - : [value] - : undefined; + const baseValue = + value !== undefined + ? Array.isArray(value) + ? value + : value === '' + ? [] + : [value] + : undefined; - const baseDefaultValue = defaultValue - ? Array.isArray(defaultValue) - ? defaultValue - : [defaultValue] - : undefined; + const baseDefaultValue = + defaultValue !== undefined + ? Array.isArray(defaultValue) + ? defaultValue + : defaultValue === '' + ? [] + : [defaultValue] + : undefined; const handleValueChange = ( newValue: string[], @@ -55,9 +61,7 @@ export const AccordionRoot = ({ if (multiple) { (onValueChange as (value: string[]) => void)(newValue); } else { - (onValueChange as (value: string | undefined) => void)( - newValue[0] || undefined - ); + (onValueChange as (value: string) => void)(newValue[0] ?? ''); } } }; diff --git a/packages/raystack/components/search/search.tsx b/packages/raystack/components/search/search.tsx index e79fcab6e..993fe1270 100644 --- a/packages/raystack/components/search/search.tsx +++ b/packages/raystack/components/search/search.tsx @@ -1,16 +1,18 @@ 'use client'; +import { BaseUIEvent } from '@base-ui/react'; import { CrossCircledIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons'; +import { ChangeEvent, RefObject, useCallback, useRef, useState } from 'react'; import { IconButton } from '../icon-button'; import { InputField } from '../input-field'; import { InputFieldProps } from '../input-field/input-field'; - import styles from './search.module.css'; export interface SearchProps extends Omit { showClearButton?: boolean; onClear?: () => void; variant?: 'default' | 'borderless'; + inputRef?: RefObject; } export function Search({ @@ -20,22 +22,45 @@ export function Search({ size, showClearButton, onClear, - value, + value: controlledValue, onChange, width = '100%', variant = 'default', + inputRef: externalRef, ...props }: SearchProps) { + const internalRef = useRef(null); + const inputRef = externalRef ?? internalRef; + const isControlled = controlledValue !== undefined; + const [internalValue, setInternalValue] = useState(''); + const currentValue = isControlled ? controlledValue : internalValue; + + const handleChange: typeof onChange = useCallback( + (e: BaseUIEvent>) => { + if (!isControlled) { + setInternalValue(e.target.value); + } + onChange?.(e); + }, + [isControlled, onChange] + ); + + const handleClear = useCallback(() => { + if (disabled) return; + if (!isControlled) { + setInternalValue(''); + } + onClear?.(); + }, [disabled, isControlled, onClear]); + const trailingIconWithClear = - showClearButton && value ? ( + showClearButton && currentValue ? (
{ e.stopPropagation(); - if (!disabled && onClear) { - onClear(); - } + handleClear(); }} disabled={disabled} aria-label='Clear search' @@ -47,14 +72,20 @@ export function Search({ ) : undefined; return ( -
+
inputRef.current?.focus()} + > } trailingIcon={trailingIconWithClear} placeholder={placeholder} disabled={disabled} - value={value} - onChange={onChange} + value={currentValue} + onChange={handleChange} size={size} className={className} aria-label={placeholder}