chore: kanban template
This commit is contained in:
8
packages/components/index.ts
Normal file
8
packages/components/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
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'
|
||||
31
packages/components/package.json
Normal file
31
packages/components/package.json
Normal 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": "^8.3.8",
|
||||
"@mantine/hooks": "^8.3.8",
|
||||
"@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": "^8.3.8",
|
||||
"@mantine/hooks": "^8.3.8",
|
||||
"@tanstack/react-query": "^5.66.0",
|
||||
"lucide-react": "^0.456.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5"
|
||||
}
|
||||
}
|
||||
58
packages/components/src/EmptyState.stories.tsx
Normal file
58
packages/components/src/EmptyState.stories.tsx
Normal 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 }
|
||||
77
packages/components/src/EmptyState.tsx
Normal file
77
packages/components/src/EmptyState.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
25
packages/components/src/NotFoundState.stories.tsx
Normal file
25
packages/components/src/NotFoundState.stories.tsx
Normal 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 }
|
||||
40
packages/components/src/NotFoundState.tsx
Normal file
40
packages/components/src/NotFoundState.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
11
packages/components/src/PageLoader.stories.tsx
Normal file
11
packages/components/src/PageLoader.stories.tsx
Normal 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 = {}
|
||||
16
packages/components/src/PageLoader.tsx
Normal file
16
packages/components/src/PageLoader.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
47
packages/components/src/ServerErrorState.stories.tsx
Normal file
47
packages/components/src/ServerErrorState.stories.tsx
Normal 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 }
|
||||
70
packages/components/src/ServerErrorState.tsx
Normal file
70
packages/components/src/ServerErrorState.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
54
packages/components/src/UserCard.app.stories.tsx
Normal file
54
packages/components/src/UserCard.app.stories.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { AppStory, AppStoryMeta } from './csf.types'
|
||||
import { UserCard, type User } from './UserCard'
|
||||
import { stubQuery } from './fixtures/stubQuery'
|
||||
|
||||
// App-level widget: composed from library primitives (Avatar + Badge + Card) and
|
||||
// driven by a query, so it's previewed across its named data-state scenarios in
|
||||
// the Design tab's App lens — not as simple variants.
|
||||
const meta: AppStoryMeta = {
|
||||
title: 'UserCard',
|
||||
component: UserCard,
|
||||
group: 'Account',
|
||||
description: 'Shows the current user, composed from the library Avatar + Badge.',
|
||||
tags: ['app'],
|
||||
inputs: [
|
||||
{
|
||||
name: 'query',
|
||||
kind: 'query',
|
||||
type: 'UseQueryResult<User | null>',
|
||||
description: 'Fetches the user to display.',
|
||||
},
|
||||
],
|
||||
argTypes: {
|
||||
query: { description: 'User query result (loading / error / empty / success).', control: false },
|
||||
},
|
||||
}
|
||||
export default meta
|
||||
|
||||
const sampleUser: User = {
|
||||
id: 'u_1',
|
||||
name: 'Ada Lovelace',
|
||||
email: 'ada@example.com',
|
||||
role: 'Principal Engineer',
|
||||
status: 'active',
|
||||
}
|
||||
|
||||
function LoadingScenario() {
|
||||
return <UserCard query={stubQuery.loading<User | null>()} />
|
||||
}
|
||||
export const Loading: AppStory = { tag: 'query: pending', render: LoadingScenario }
|
||||
|
||||
function ErrorScenario() {
|
||||
return <UserCard query={stubQuery.error<User | null>(new Error('Request failed'))} />
|
||||
}
|
||||
export const Errored: AppStory = { name: 'Error', tag: 'query: error', render: ErrorScenario }
|
||||
|
||||
function EmptyScenario() {
|
||||
return <UserCard query={stubQuery.empty<User | null>()} />
|
||||
}
|
||||
export const Empty: AppStory = { tag: 'query: no data', render: EmptyScenario }
|
||||
|
||||
function ActiveScenario() {
|
||||
return <UserCard query={stubQuery.success<User | null>(sampleUser)} />
|
||||
}
|
||||
export const Active: AppStory = { name: 'Active user', tag: 'query: success', render: ActiveScenario }
|
||||
83
packages/components/src/UserCard.tsx
Normal file
83
packages/components/src/UserCard.tsx
Normal 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">
|
||||
Couldn’t 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>
|
||||
)
|
||||
}
|
||||
45
packages/components/src/csf.types.ts
Normal file
45
packages/components/src/csf.types.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
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
|
||||
/** Left-menu group in the Design tab; falls back to the first tag. */
|
||||
group?: string
|
||||
tags?: string[]
|
||||
argTypes?: Record<string, ArgType>
|
||||
}
|
||||
|
||||
export interface Story {
|
||||
args?: Record<string, unknown>
|
||||
render?: ComponentType<any>
|
||||
name?: string
|
||||
}
|
||||
|
||||
/** A query or mutation an app widget consumes. Listed in the Design tab's
|
||||
* right-column inspector under the App lens. */
|
||||
export interface AppInput {
|
||||
name: string
|
||||
kind: 'query' | 'mutation'
|
||||
type?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
/** Meta for an app-level widget (`*.app.stories.tsx`). Such a widget is composed
|
||||
* from the library and driven by queries/mutations, so it's previewed across
|
||||
* named data-state scenarios rather than simple variants. */
|
||||
export interface AppStoryMeta extends StoryMeta {
|
||||
inputs?: AppInput[]
|
||||
}
|
||||
|
||||
/** One named data-state scenario of an app widget (e.g. Loading / Error / Ready).
|
||||
* `tag` annotates the data state, e.g. "query: pending". */
|
||||
export interface AppStory extends Story {
|
||||
tag?: string
|
||||
}
|
||||
69
packages/components/src/fixtures/stubMutation.ts
Normal file
69
packages/components/src/fixtures/stubMutation.ts
Normal 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,
|
||||
})
|
||||
},
|
||||
}
|
||||
66
packages/components/src/fixtures/stubQuery.ts
Normal file
66
packages/components/src/fixtures/stubQuery.ts
Normal 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)
|
||||
},
|
||||
}
|
||||
14
packages/components/tsconfig.json
Normal file
14
packages/components/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user