Skip to content
Open
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
788 changes: 788 additions & 0 deletions apps/www/src/app/examples/timeline/page.tsx

Large diffs are not rendered by default.

312 changes: 310 additions & 2 deletions apps/www/src/components/dataview-demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import {
DataViewField,
DataViewListColumn,
Flex,
Text
Text,
type TimelineActions,
type TimelineCardContext
} from '@raystack/apsara';
import { useMemo, useState } from 'react';
import { useMemo, useRef, useState } from 'react';

type Person = {
id: string;
Expand Down Expand Up @@ -604,3 +606,309 @@ export function DataViewPerViewFieldsDemo() {
</Flex>
);
}

/* ── Timeline demo ─────────────────────────────────────────────────────── */

type Task = {
id: string;
title: string;
team: 'Eng' | 'Design' | 'Ops';
status: 'todo' | 'active' | 'done';
start: string;
end: string;
};

const TASK_DAY_MS = 86_400_000;

/* Dates relative to today, pinned to midnight so the today line is always
live and SSR/client renders match (no hydration drift). */
const taskDate = (days: number) => {
const now = new Date();
now.setHours(0, 0, 0, 0);
return new Date(now.getTime() + days * TASK_DAY_MS).toISOString();
};

const taskSpec: Array<
[string, string, Task['team'], Task['status'], number, number]
> = [
['t1', 'Design audit', 'Design', 'done', -16, -9],
['t2', 'API contracts', 'Eng', 'done', -13, -6],
['t3', 'Billing revamp', 'Eng', 'active', -7, 2],
['t4', 'Docs sprint', 'Design', 'active', -4, 4],
['t5', 'Bug bash', 'Ops', 'todo', 1, 2],
['t6', 'Load testing', 'Ops', 'todo', 3, 9],
['t7', 'Beta rollout', 'Eng', 'todo', 6, 14],
['t8', 'Launch comms', 'Design', 'todo', 10, 16]
];

const tasks: Task[] = taskSpec.map(([id, title, team, status, from, to]) => ({
id,
title,
team,
status,
start: taskDate(from),
end: taskDate(to)
}));

const taskFields: DataViewField<Task>[] = [
{
accessorKey: 'title',
label: 'Task',
filterable: true,
filterType: 'string',
hideable: false
},
{
accessorKey: 'team',
label: 'Team',
filterable: true,
filterType: 'select',
hideable: true,
filterOptions: [
{ label: 'Eng', value: 'Eng' },
{ label: 'Design', value: 'Design' },
{ label: 'Ops', value: 'Ops' }
]
},
{
accessorKey: 'status',
label: 'Status',
filterable: true,
filterType: 'select',
hideable: true,
filterOptions: [
{ label: 'To do', value: 'todo' },
{ label: 'Active', value: 'active' },
{ label: 'Done', value: 'done' }
]
},
{
accessorKey: 'start',
label: 'Start',
filterable: true,
filterType: 'date',
sortable: true,
hideable: true
},
{
accessorKey: 'end',
label: 'End',
filterable: true,
filterType: 'date',
sortable: true,
hideable: true
}
];

const TASK_STATUS_BADGE: Record<
Task['status'],
'neutral' | 'accent' | 'success'
> = {
todo: 'neutral',
active: 'accent',
done: 'success'
};

/* The card interior is entirely consumer-owned — the Timeline only positions
the wrapper. `context.collapsed` flags spans narrower than `minCardWidth`. */
function TaskCard({
task,
context
}: {
task: Task;
context: TimelineCardContext;
}) {
const chrome: React.CSSProperties = {
height: '100%',
boxSizing: 'border-box',
borderRadius: 'var(--rs-radius-3)',
border: '1px solid var(--rs-color-border-base-primary)',
background: 'var(--rs-color-background-base-primary)',
overflow: 'hidden'
};
if (context.collapsed) {
return (
<Flex align='center' justify='center' style={chrome}>
<Text size='micro' weight='medium' variant='secondary'>
{task.title.charAt(0)}
</Text>
</Flex>
);
}
return (
<Flex
direction='column'
justify='between'
style={{ ...chrome, padding: 'var(--rs-space-3)' }}
>
<Text
size='small'
weight='medium'
style={{
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
{task.title}
</Text>
<Flex align='center' gap={2}>
<Badge size='micro' variant={TASK_STATUS_BADGE[task.status]}>
{task.status}
</Badge>
<DataView.DisplayAccess accessorKey='team'>
<Badge size='micro' variant='neutral'>
{task.team}
</Badge>
</DataView.DisplayAccess>
</Flex>
</Flex>
);
}

/* ── Timeline point-marker demo (no endField) ──────────────────────────── */

type Release = {
id: string;
version: string;
channel: 'stable' | 'beta';
date: string;
};

const releases: Release[] = [
{ id: 'r1', version: 'v1.8', channel: 'stable', date: taskDate(-14) },
{ id: 'r2', version: 'v1.9', channel: 'stable', date: taskDate(-9) },
{ id: 'r3', version: 'v2.0-b1', channel: 'beta', date: taskDate(-4) },
{ id: 'r4', version: 'v2.0-b2', channel: 'beta', date: taskDate(1) },
{ id: 'r5', version: 'v2.0', channel: 'stable', date: taskDate(6) },
{ id: 'r6', version: 'v2.1-b1', channel: 'beta', date: taskDate(11) }
];

const releaseFields: DataViewField<Release>[] = [
{
accessorKey: 'version',
label: 'Version',
filterable: true,
filterType: 'string',
hideable: false
},
{
accessorKey: 'channel',
label: 'Channel',
filterable: true,
filterType: 'select',
hideable: true,
filterOptions: [
{ label: 'Stable', value: 'stable' },
{ label: 'Beta', value: 'beta' }
]
},
{
accessorKey: 'date',
label: 'Date',
filterable: true,
filterType: 'date',
sortable: true,
hideable: true
}
];

export function DataViewTimelinePointDemo() {
return (
<Flex
direction='column'
style={{ width: '100%', height: 320, overflow: 'hidden' }}
>
<DataView<Release>
data={releases}
fields={releaseFields}
defaultSort={{ name: 'date', order: 'asc' }}
getRowId={release => release.id}
>
<DataView.Toolbar>
<DataView.Filters />
<DataView.DisplayControls hideOrdering hideGrouping />
</DataView.Toolbar>
<Flex
direction='column'
justify='center'
style={{ flex: 1, overflow: 'hidden', minWidth: 0 }}
>
{/* No endField → point markers: the wrapper sizes to its content. */}
<DataView.Timeline<Release>
startField='date'
estimatedRowHeight={28}
renderCard={row => (
<Badge
variant={
row.original.channel === 'stable' ? 'accent' : 'neutral'
}
>
{row.original.version}
</Badge>
)}
/>
<DataView.EmptyState>
<Text>No releases match your filters.</Text>
</DataView.EmptyState>
</Flex>
</DataView>
</Flex>
);
}

export function DataViewTimelineDemo() {
const timelineActions = useRef<TimelineActions | null>(null);
return (
<Flex
direction='column'
style={{ width: '100%', height: 420, overflow: 'hidden' }}
>
<DataView<Task>
data={tasks}
fields={taskFields}
defaultSort={{ name: 'start', order: 'asc' }}
getRowId={task => task.id}
>
<DataView.Toolbar>
<DataView.Filters />
<Flex gap={3} align='center'>
<Button
size='small'
variant='outline'
color='neutral'
onClick={() => timelineActions.current?.scrollTo('today')}
>
Today
</Button>
{/* Sorting/grouping don't affect time-positioned cards — hide them. */}
<DataView.DisplayControls hideOrdering hideGrouping />
</Flex>
</DataView.Toolbar>
{/* Empty state lives inside the flex-1 pane so it centers when the
timeline renders null. */}
<Flex
direction='column'
justify='center'
style={{ flex: 1, overflow: 'hidden', minWidth: 0 }}
>
<DataView.Timeline<Task>
startField='start'
endField='end'
actionsRef={timelineActions}
markers={[
{ date: taskDate(12), label: 'Release', variant: 'accent' }
]}
renderCard={(row, context) => (
<TaskCard task={row.original} context={context} />
)}
/>
<DataView.EmptyState>
<Text>No tasks match your filters.</Text>
</DataView.EmptyState>
</Flex>
</DataView>
</Flex>
);
}
4 changes: 4 additions & 0 deletions apps/www/src/components/demo/demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ import {
DataViewPerViewFieldsDemo,
DataViewSearchDemo,
DataViewTableDemo,
DataViewTimelineDemo,
DataViewTimelinePointDemo,
DataViewVirtualizedDemo,
DataViewVirtualizedGroupingDemo
} from '../dataview-demo';
Expand Down Expand Up @@ -87,6 +89,8 @@ export default function Demo(props: DemoProps) {
DataViewLoadingDemo,
DataViewPerViewFieldsDemo,
DataViewSearchDemo,
DataViewTimelineDemo,
DataViewTimelinePointDemo,
ChipInputDemo,
DataTableSelectionDemo,
LinearMenuDemo,
Expand Down
Loading
Loading