chore: starter template

This commit is contained in:
e2e
2026-07-10 20:36:29 +02:00
commit 3304e0b8bc
157 changed files with 6462 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
export { UserCard } from './src/UserCard'
export type { User, UserCardProps } from './src/UserCard'
export { stubQuery } from './src/fixtures/stubQuery'
export { stubMutation } from './src/fixtures/stubMutation'
export { EmptyState } from './src/EmptyState'
export { NotFoundState } from './src/NotFoundState'
export { PageLoader } from './src/PageLoader'
export { ServerErrorState } from './src/ServerErrorState'
// Layout primitives — compose these instead of hand-rolling page chrome.
export { PageHeader } from './src/PageHeader'
export type { PageHeaderProps } from './src/PageHeader'
export { Panel } from './src/Panel'
export type { PanelProps } from './src/Panel'
export { StatCard } from './src/StatCard'
export type { StatCardProps } from './src/StatCard'
export { StatGrid } from './src/StatGrid'
export type { StatGridProps } from './src/StatGrid'
// Data presentation — feed these the output of a list/stats RPC you implement.
export { DataTable } from './src/DataTable'
export type { DataTableProps, DataTableColumn } from './src/DataTable'
export { BarChart } from './src/BarChart'
export type { BarChartProps, BarChartDatum } from './src/BarChart'

View File

@@ -0,0 +1,31 @@
{
"name": "@project/components",
"version": "0.0.1",
"private": true,
"type": "module",
"exports": {
".": "./index.ts"
},
"scripts": {
"tsc": "tsc --noEmit"
},
"devDependencies": {
"@mantine/core": "^9.2.1",
"@mantine/hooks": "^9.2.1",
"@tanstack/react-query": "^5.66.0",
"@types/react": "^19",
"@types/react-dom": "^19",
"lucide-react": "^0.456.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"typescript": "^5.9"
},
"peerDependencies": {
"@mantine/core": "^9.2.1",
"@mantine/hooks": "^9.2.1",
"@tanstack/react-query": "^5.66.0",
"lucide-react": "^0.456.0",
"react": "^19.2.5",
"react-dom": "^19.2.5"
}
}

View File

@@ -0,0 +1,57 @@
import React from 'react'
import { Box, Group, Stack, Text } from '@mantine/core'
export interface BarChartDatum {
label: React.ReactNode
value: number
/** Mantine color key for the bar (e.g. 'teal', 'red'). Defaults to the theme primary. */
color?: string
}
export interface BarChartProps {
data: BarChartDatum[]
/** Format the numeric value shown at the end of each bar. */
valueFormatter?: (value: number) => string
}
// A dependency-free horizontal bar chart (CSS only, theme colours). Feed it
// aggregated counts from a stats RPC — no charting library to install or break.
export function BarChart({ data, valueFormatter }: BarChartProps) {
const max = Math.max(1, ...data.map((d) => d.value))
const fmt = valueFormatter ?? ((n: number) => String(n))
return (
<Stack gap="sm">
{data.map((datum, index) => (
<Stack key={index} gap={4}>
<Group justify="space-between" gap="xs">
<Text size="sm">{datum.label}</Text>
<Text size="sm" fw={600}>
{fmt(datum.value)}
</Text>
</Group>
<Box
style={{
height: 8,
borderRadius: 'var(--mantine-radius-xl)',
background: 'var(--mantine-color-default-hover)',
overflow: 'hidden',
}}
>
<Box
style={{
height: '100%',
width: `${(datum.value / max) * 100}%`,
borderRadius: 'var(--mantine-radius-xl)',
background: datum.color
? `var(--mantine-color-${datum.color}-6)`
: 'var(--mantine-primary-color-6)',
transition: 'width 200ms ease',
}}
/>
</Box>
</Stack>
))}
</Stack>
)
}

View File

@@ -0,0 +1,85 @@
import React from 'react'
import { Center, Loader, Table, Text } from '@mantine/core'
export interface DataTableColumn<Row> {
/** Stable column id; also the default cell value (row[key]) when no render is given. */
key: string
header: React.ReactNode
/** Custom cell renderer. Defaults to String(row[key]). */
render?: (row: Row) => React.ReactNode
align?: 'left' | 'center' | 'right'
width?: number | string
}
export interface DataTableProps<Row> {
columns: DataTableColumn<Row>[]
rows: Row[]
/** Stable key per row (e.g. r => r.taskId). */
rowKey: (row: Row) => string
loading?: boolean
/** Shown when there are no rows (pass an <EmptyState/> for a rich version). */
empty?: React.ReactNode
onRowClick?: (row: Row) => void
}
// A generic, typed table. Wire it to any list RPC: implement listX, pass its
// rows here, and describe the columns. Pure presentation — no data fetching.
export function DataTable<Row>({
columns,
rows,
rowKey,
loading,
empty,
onRowClick,
}: DataTableProps<Row>) {
if (loading) {
return (
<Center py="xl">
<Loader size="sm" />
</Center>
)
}
if (rows.length === 0) {
return (
<Center py="xl">
{empty ?? (
<Text c="dimmed" size="sm">
Nothing here yet.
</Text>
)}
</Center>
)
}
return (
<Table highlightOnHover={!!onRowClick} verticalSpacing="sm" horizontalSpacing="md">
<Table.Thead>
<Table.Tr>
{columns.map((col) => (
<Table.Th key={col.key} style={{ textAlign: col.align ?? 'left', width: col.width }}>
{col.header}
</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{rows.map((row) => (
<Table.Tr
key={rowKey(row)}
onClick={onRowClick ? () => onRowClick(row) : undefined}
style={onRowClick ? { cursor: 'pointer' } : undefined}
>
{columns.map((col) => (
<Table.Td key={col.key} style={{ textAlign: col.align ?? 'left' }}>
{col.render
? col.render(row)
: String((row as Record<string, unknown>)[col.key] ?? '')}
</Table.Td>
))}
</Table.Tr>
))}
</Table.Tbody>
</Table>
)
}

View File

@@ -0,0 +1,58 @@
import { AlertCircle, Boxes, FolderOpen } from 'lucide-react'
import type { Story, StoryMeta } from './csf.types'
import { EmptyState } from './EmptyState'
const meta: StoryMeta = {
title: 'EmptyState',
component: EmptyState,
tags: ['feedback'],
argTypes: {
icon: { description: 'Lucide icon component displayed above the title', control: false },
title: { description: 'Primary heading text', control: 'text' },
description: { description: 'Supporting text below the title', control: 'text' },
action: { description: 'Label for the primary action button', control: 'text' },
onAction: { description: 'Callback fired when the action button is clicked', control: false },
compact: {
description: 'Reduces padding and icon size for inline contexts',
control: 'boolean',
},
docsHref: { description: 'Optional link shown as a Docs button', control: 'text' },
},
}
export default meta
function DefaultStory() {
return (
<EmptyState
icon={Boxes}
title="No items yet"
description="Create your first item to get started."
/>
)
}
export const Default: Story = { render: DefaultStory }
function WithActionStory() {
return (
<EmptyState
icon={FolderOpen}
title="No projects"
description="You don't have any projects yet."
action="New project"
onAction={() => {}}
/>
)
}
export const WithAction: Story = { render: WithActionStory }
function CompactStory() {
return (
<EmptyState
icon={AlertCircle}
title="Nothing here"
description="This section is currently empty."
compact
/>
)
}
export const Compact: Story = { render: CompactStory }

View File

@@ -0,0 +1,77 @@
import React from 'react'
import { Button, Center, Group, Stack, Text } from '@mantine/core'
import { ExternalLink } from 'lucide-react'
interface EmptyStateProps {
icon: React.ComponentType<{ size?: number; strokeWidth?: number }>
title: string
description: React.ReactNode
action?: string
onAction?: () => void
actionIcon?: React.ReactNode
actionLoading?: boolean
secondaryText?: React.ReactNode
docsHref?: string
compact?: boolean
}
export function EmptyState({
icon,
title,
description,
action,
onAction,
actionIcon,
actionLoading,
secondaryText,
docsHref,
compact = false,
}: EmptyStateProps) {
const Icon = icon
return (
<Center flex={1}>
<Stack
align="center"
justify="center"
gap={compact ? 'sm' : 'md'}
py={compact ? 'lg' : 'xl'}
style={{ minHeight: compact ? undefined : '60vh', width: '100%' }}
>
<Icon size={compact ? 36 : 48} strokeWidth={1} />
<Text size={compact ? 'lg' : 'xl'} fw={600}>
{title}
</Text>
<Text c="dimmed" ta="center" maw={500}>
{description}
</Text>
{docsHref || (action && onAction) ? (
<Group justify="center">
{docsHref ? (
<Button
component="a"
href={docsHref}
target="_blank"
rel="noopener noreferrer"
variant="default"
leftSection={<ExternalLink size={16} />}
>
Docs
</Button>
) : null}
{action && onAction ? (
<Button onClick={onAction} loading={actionLoading} leftSection={actionIcon}>
{action}
</Button>
) : null}
</Group>
) : null}
{secondaryText ? (
<Text c="dimmed" size="sm" ta="center" maw={500}>
{secondaryText}
</Text>
) : null}
</Stack>
</Center>
)
}

View File

@@ -0,0 +1,25 @@
import type { Story, StoryMeta } from './csf.types'
import { NotFoundState } from './NotFoundState'
const meta: StoryMeta = {
title: 'NotFoundState',
component: NotFoundState,
tags: ['feedback'],
argTypes: {
title: { description: 'Override the "Page not found" heading', control: 'text' },
description: { description: 'Override the default description text', control: 'text' },
},
}
export default meta
export const Default: Story = {}
function CustomStory() {
return (
<NotFoundState
title="Board not found"
description="The board you were looking for has been removed or never existed."
/>
)
}
export const Custom: Story = { render: CustomStory }

View File

@@ -0,0 +1,40 @@
import { Center, Stack, Text } from '@mantine/core'
interface NotFoundStateProps {
title?: string
description?: string
}
export function NotFoundState({
title = 'Page not found',
description = "The resource you're looking for doesn't exist or has been removed.",
}: NotFoundStateProps) {
return (
<Center flex={1}>
<Stack
align="center"
justify="center"
gap="xs"
py="xl"
style={{ minHeight: '60vh', width: '100%' }}
>
<Text
style={{
fontSize: 72,
lineHeight: 1,
letterSpacing: '-0.06em',
fontWeight: 600,
}}
>
404
</Text>
<Text size="xl" fw={600}>
{title}
</Text>
<Text ta="center" maw={520} c="dimmed">
{description}
</Text>
</Stack>
</Center>
)
}

View File

@@ -0,0 +1,32 @@
import { Button } from '@mantine/core'
import { Plus } from 'lucide-react'
import type { Story, StoryMeta } from './csf.types'
import { PageHeader } from './PageHeader'
const meta: StoryMeta = {
title: 'PageHeader',
component: PageHeader,
tags: ['layout'],
argTypes: {
title: { description: 'Page title', control: 'text' },
description: { description: 'Optional sub-text', control: 'text' },
actions: { description: 'Right-aligned actions slot', control: false },
},
}
export default meta
function DefaultStory() {
return <PageHeader title="Tasks" description="Everything on your plate, in one place." />
}
export const Default: Story = { render: DefaultStory }
function WithActionStory() {
return (
<PageHeader
title="Tasks"
description="Everything on your plate, in one place."
actions={<Button leftSection={<Plus size={16} />}>New task</Button>}
/>
)
}
export const WithAction: Story = { render: WithActionStory }

View File

@@ -0,0 +1,32 @@
import React from 'react'
import { Box, Group, Stack, Text, Title } from '@mantine/core'
export interface PageHeaderProps {
/** Page title. Pass an i18n string from the app (m.foo()). */
title: React.ReactNode
/** Optional one-line description under the title. */
description?: React.ReactNode
/** Right-aligned actions (buttons, menus). */
actions?: React.ReactNode
}
// The single shared page header every screen uses: a title, optional
// description, and a right-aligned action slot. Keeps headers consistent across
// pages — compose it, don't hand-roll a per-page header.
export function PageHeader({ title, description, actions }: PageHeaderProps) {
return (
<Group justify="space-between" align="flex-end" wrap="nowrap" mb="xl">
<Stack gap={4} style={{ minWidth: 0 }}>
<Title order={1} fz={26} fw={680} style={{ letterSpacing: '-0.03em', lineHeight: 1.1 }}>
{title}
</Title>
{description ? (
<Text c="dimmed" size="sm" style={{ lineHeight: 1.5 }}>
{description}
</Text>
) : null}
</Stack>
{actions ? <Box style={{ flexShrink: 0 }}>{actions}</Box> : null}
</Group>
)
}

View File

@@ -0,0 +1,11 @@
import type { Story, StoryMeta } from './csf.types'
import { PageLoader } from './PageLoader'
const meta: StoryMeta = {
title: 'PageLoader',
component: PageLoader,
tags: ['loading'],
}
export default meta
export const Default: Story = {}

View File

@@ -0,0 +1,16 @@
import { Center, Loader } from '@mantine/core'
export function PageLoader() {
return (
<Center
style={{
flex: 1,
width: '100%',
minWidth: 0,
minHeight: 0,
}}
>
<Loader size="md" />
</Center>
)
}

View File

@@ -0,0 +1,48 @@
import React from 'react'
import { Card, Group, Stack, Text, Title } from '@mantine/core'
export interface PanelProps {
/** Optional panel heading. */
title?: React.ReactNode
/** Optional sub-text under the heading. */
description?: React.ReactNode
/** Right-aligned header actions. */
actions?: React.ReactNode
/** Panel body. */
children: React.ReactNode
/** Remove inner padding (e.g. for a flush table). */
noPadding?: boolean
}
// A bordered content section with an optional titled header. The workhorse
// container — group related content into Panels instead of loose Cards.
export function Panel({ title, description, actions, children, noPadding }: PanelProps) {
return (
<Card withBorder radius="lg" padding={noPadding ? 0 : 'lg'}>
{title || actions ? (
<Group
justify="space-between"
align="flex-end"
wrap="nowrap"
p={noPadding ? 'lg' : 0}
pb={noPadding ? 'sm' : 'md'}
>
<Stack gap={2} style={{ minWidth: 0 }}>
{title ? (
<Title order={2} fz={16} fw={620} style={{ letterSpacing: '-0.02em' }}>
{title}
</Title>
) : null}
{description ? (
<Text c="dimmed" size="xs">
{description}
</Text>
) : null}
</Stack>
{actions ? <div style={{ flexShrink: 0 }}>{actions}</div> : null}
</Group>
) : null}
{children}
</Card>
)
}

View File

@@ -0,0 +1,47 @@
import { useState } from 'react'
import type { Story, StoryMeta } from './csf.types'
import { ServerErrorState } from './ServerErrorState'
const meta: StoryMeta = {
title: 'ServerErrorState',
component: ServerErrorState,
tags: ['feedback'],
argTypes: {
title: { description: 'Override the "Something went wrong" heading', control: 'text' },
description: { description: 'Override the default error description', control: 'text' },
onRetry: { description: 'Callback fired when the retry button is clicked', control: false },
retrying: {
description: 'Shows a loading spinner on the retry button',
control: 'boolean',
},
retryLabel: { description: 'Label for the retry button', control: 'text' },
glyph: {
description: 'Custom element displayed above the title instead of "500"',
control: false,
},
},
}
export default meta
export const Default: Story = {}
function WithRetryStory() {
const [retrying, setRetrying] = useState(false)
const handleRetry = () => {
setRetrying(true)
setTimeout(() => setRetrying(false), 1500)
}
return <ServerErrorState onRetry={handleRetry} retrying={retrying} />
}
export const WithRetry: Story = { render: WithRetryStory }
function CustomGlyphStory() {
return (
<ServerErrorState
glyph={<span style={{ fontSize: 64, lineHeight: 1 }}></span>}
title="Unexpected error"
description="Please try again or contact support if the problem persists."
/>
)
}
export const CustomGlyph: Story = { render: CustomGlyphStory }

View File

@@ -0,0 +1,70 @@
import type { ReactNode } from 'react'
import { Button, Center, Stack, Text } from '@mantine/core'
import { RefreshCw } from 'lucide-react'
interface ServerErrorStateProps {
title?: string
description?: string
onRetry?: () => void
retrying?: boolean
retryLabel?: string
glyph?: ReactNode
}
export function ServerErrorState({
title = 'Something went wrong',
description = 'The server returned an error while loading this page.',
onRetry,
retrying = false,
retryLabel = 'Retry',
glyph,
}: ServerErrorStateProps) {
const normalizedDescription = typeof description === 'string' ? description.trim() : ''
const showDescription =
normalizedDescription.length > 0 &&
normalizedDescription !== 'HTTP 500' &&
normalizedDescription !== 'HTTP 500.'
return (
<Center flex={1}>
<Stack
align="center"
justify="center"
gap="xs"
py="xl"
style={{ minHeight: '60vh', width: '100%' }}
>
{glyph ?? (
<Text
style={{
fontSize: 72,
lineHeight: 1,
letterSpacing: '-0.06em',
fontWeight: 600,
}}
>
500
</Text>
)}
<Text size="xl" fw={600}>
{title}
</Text>
{showDescription ? (
<Text ta="center" maw={520} c="dimmed">
{normalizedDescription}
</Text>
) : null}
{onRetry ? (
<Button
onClick={onRetry}
loading={retrying}
leftSection={<RefreshCw size={16} />}
variant="default"
>
{retryLabel}
</Button>
) : null}
</Stack>
</Center>
)
}

View File

@@ -0,0 +1,37 @@
import React from 'react'
import { Card, Group, Stack, Text, ThemeIcon } from '@mantine/core'
export interface StatCardProps {
/** Metric label, e.g. "Open tasks". */
label: React.ReactNode
/** The big number / value. */
value: React.ReactNode
/** Optional Lucide icon component. */
icon?: React.ComponentType<{ size?: number; strokeWidth?: number }>
/** Theme color for the icon tile (Mantine color key). Defaults to primary. */
color?: string
}
// A single metric tile: label, large value, optional icon. Compose several in a
// StatGrid for a dashboard summary row.
export function StatCard({ label, value, icon: Icon, color }: StatCardProps) {
return (
<Card withBorder radius="lg" padding="lg">
<Group justify="space-between" align="flex-start" wrap="nowrap">
<Stack gap={4}>
<Text size="xs" c="dimmed" fw={550} tt="uppercase" style={{ letterSpacing: '0.04em' }}>
{label}
</Text>
<Text fz={30} fw={700} lh={1} style={{ letterSpacing: '-0.03em' }}>
{value}
</Text>
</Stack>
{Icon ? (
<ThemeIcon variant="light" color={color} radius="md" size={38}>
<Icon size={20} strokeWidth={1.8} />
</ThemeIcon>
) : null}
</Group>
</Card>
)
}

View File

@@ -0,0 +1,21 @@
import { SimpleGrid } from '@mantine/core'
import { StatCard, type StatCardProps } from './StatCard'
export interface StatGridProps {
stats: StatCardProps[]
/** Columns on wide screens (responsive down to 1). Defaults to stats.length capped at 4. */
columns?: number
}
// A responsive row of StatCards. Feed it the metrics from a stats RPC
// (e.g. getTaskStats) mapped to {label, value, icon}.
export function StatGrid({ stats, columns }: StatGridProps) {
const cols = columns ?? Math.min(stats.length || 1, 4)
return (
<SimpleGrid cols={{ base: 1, sm: 2, lg: cols }} spacing="md">
{stats.map((stat, index) => (
<StatCard key={index} {...stat} />
))}
</SimpleGrid>
)
}

View File

@@ -0,0 +1,83 @@
import { Avatar, Badge, Card, Group, Loader, Stack, Text } from '@mantine/core'
import type { UseQueryResult } from '@tanstack/react-query'
export type User = {
id: string
name: string
email: string
role: string
status: 'active' | 'away' | 'offline'
}
const STATUS_COLOR: Record<User['status'], string> = {
active: 'green',
away: 'yellow',
offline: 'gray',
}
export type UserCardProps = {
query: UseQueryResult<User | null>
}
export const UserCard: React.FC<UserCardProps> = ({ query }) => {
if (query.isLoading) {
return (
<Card withBorder radius="md" padding="lg">
<Group>
<Loader size="sm" />
<Text size="sm" c="dimmed">
Loading user
</Text>
</Group>
</Card>
)
}
if (query.isError) {
return (
<Card withBorder radius="md" padding="lg">
<Stack gap={4}>
<Text fw={600} c="red">
Couldnt load user
</Text>
<Text size="sm" c="dimmed">
{query.error instanceof Error ? query.error.message : 'Unknown error'}
</Text>
</Stack>
</Card>
)
}
const user = query.data
if (!user) {
return (
<Card withBorder radius="md" padding="lg">
<Text size="sm" c="dimmed">
No user to show.
</Text>
</Card>
)
}
return (
<Card withBorder radius="md" padding="lg">
<Group justify="space-between" wrap="nowrap">
<Group wrap="nowrap">
<Avatar color="initials" name={user.name} radius="xl" />
<Stack gap={0}>
<Text fw={600}>{user.name}</Text>
<Text size="sm" c="dimmed">
{user.email}
</Text>
</Stack>
</Group>
<Badge color={STATUS_COLOR[user.status]} variant="light">
{user.status}
</Badge>
</Group>
<Text size="sm" mt="md">
{user.role}
</Text>
</Card>
)
}

View File

@@ -0,0 +1,21 @@
import type { ComponentType } from 'react'
export interface ArgType {
description?: string
control?: string | false
defaultValue?: unknown
}
export interface StoryMeta {
title: string
component: ComponentType<any>
description?: string
tags?: string[]
argTypes?: Record<string, ArgType>
}
export interface Story {
args?: Record<string, unknown>
render?: ComponentType<any>
name?: string
}

View File

@@ -0,0 +1,69 @@
import type { UseMutationResult } from '@tanstack/react-query'
function build<TData, TVariables>(
partial: Record<string, unknown>,
): UseMutationResult<TData, Error, TVariables> {
return {
failureCount: 0,
failureReason: null,
isPaused: false,
submittedAt: 0,
context: undefined,
variables: undefined,
mutate: () => undefined,
mutateAsync: () => Promise.resolve(undefined as never),
reset: () => undefined,
...partial,
} as unknown as UseMutationResult<TData, Error, TVariables>
}
export const stubMutation = {
idle<TData = unknown, TVariables = unknown>(): UseMutationResult<TData, Error, TVariables> {
return build<TData, TVariables>({
status: 'idle',
isIdle: true,
isPending: false,
isError: false,
isSuccess: false,
data: undefined,
error: null,
})
},
pending<TData = unknown, TVariables = unknown>(): UseMutationResult<TData, Error, TVariables> {
return build<TData, TVariables>({
status: 'pending',
isIdle: false,
isPending: true,
isError: false,
isSuccess: false,
data: undefined,
error: null,
})
},
error<TData = unknown, TVariables = unknown>(
error: Error,
): UseMutationResult<TData, Error, TVariables> {
return build<TData, TVariables>({
status: 'error',
isIdle: false,
isPending: false,
isError: true,
isSuccess: false,
data: undefined,
error,
})
},
success<TData = unknown, TVariables = unknown>(
data: TData,
): UseMutationResult<TData, Error, TVariables> {
return build<TData, TVariables>({
status: 'success',
isIdle: false,
isPending: false,
isError: false,
isSuccess: true,
data,
error: null,
})
},
}

View File

@@ -0,0 +1,66 @@
import type { UseQueryResult } from '@tanstack/react-query'
function build<T>(partial: Record<string, unknown>): UseQueryResult<T> {
return {
failureCount: 0,
failureReason: null,
errorUpdateCount: 0,
isFetched: true,
isFetchedAfterMount: true,
isFetching: partial.fetchStatus === 'fetching',
isPaused: false,
isPlaceholderData: false,
isRefetching: false,
isStale: false,
isInitialLoading: partial.isLoading === true,
isLoadingError: false,
isRefetchError: false,
dataUpdatedAt: 0,
errorUpdatedAt: 0,
refetch: () => Promise.resolve(undefined as never),
promise: Promise.resolve(undefined as never),
...partial,
} as unknown as UseQueryResult<T>
}
export const stubQuery = {
loading<T>(): UseQueryResult<T> {
return build<T>({
status: 'pending',
fetchStatus: 'fetching',
isPending: true,
isLoading: true,
isError: false,
isSuccess: false,
data: undefined,
error: null,
})
},
error<T>(error: Error): UseQueryResult<T> {
return build<T>({
status: 'error',
fetchStatus: 'idle',
isPending: false,
isLoading: false,
isError: true,
isSuccess: false,
data: undefined,
error,
})
},
success<T>(data: T): UseQueryResult<T> {
return build<T>({
status: 'success',
fetchStatus: 'idle',
isPending: false,
isLoading: false,
isError: false,
isSuccess: true,
data,
error: null,
})
},
empty<T>(): UseQueryResult<T> {
return stubQuery.success<T>(null as T)
},
}

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["index.ts", "src"]
}