Files
homarr/packages/custom-widgets/src/core/examples.ts
T

301 lines
12 KiB
TypeScript

export interface CustomJsxExample {
id: string;
title: string;
description: string;
template: string;
requests: readonly Record<string, unknown>[];
}
export const customJsxExamples: readonly CustomJsxExample[] = [
{
id: "simple-metric",
title: "Simple metric",
description: "A compact status and metric display with no nested requests.",
template: `<Stack gap="xs" p="sm">
<Group justify="space-between">
<Text fw={600}>{data.name}</Text>
<Badge color={data.status === "online" ? "green" : "red"}>{data.status}</Badge>
</Group>
<Title order={2}>{data.value}</Title>
</Stack>`,
requests: [],
},
{
id: "pokedex-detail",
title: "Pokédex detail list",
description: "Paginates the list and fetches details only for visible Pokémon.",
template: `<Stack gap="sm" p="xs">
<Title order={3}>Pokédex</Title>
<PaginatedList pageSize={5}>
{data.results.map((pokemon) =>
<Card key={pokemon.name} withBorder p="xs" mb="xs">
<Text fw={700} tt="capitalize">{pokemon.name}</Text>
<SubFetch requestId="pokemon-detail" params={{ name: pokemon.name }}>
{(detail, meta) =>
<Stack gap={4}>
<Group gap="xs">
{detail.types.map((entry) => <Badge key={entry.type.name} color="red">{entry.type.name}</Badge>)}
</Group>
<Text size="xs">HP {detail.stats[0].base_stat} · HTTP {meta.status}</Text>
</Stack>
}
</SubFetch>
</Card>
)}
</PaginatedList>
</Stack>`,
requests: [
{
id: "pokemon-detail",
kind: "query",
method: "GET",
pathTemplate: "/api/v2/pokemon/{name}",
parameters: { name: "string" },
auth: "none",
minimumBoardPermission: "view",
cacheTtlSeconds: 300,
},
],
},
{
id: "stocks-dashboard",
title: "Stocks dashboard",
description: "Responsive portfolio values and chart with parent refresh.",
template: `<Stack gap="md" p="sm">
<Group justify="space-between">
<Title order={3}>Portfolio</Title>
<RefreshButton label="Refresh" size="xs" />
</Group>
<SimpleGrid cols={3}>
{data.holdings.map((stock) =>
<Card key={stock.symbol} withBorder p="xs">
<Text fw={700}>{stock.symbol}</Text>
<NumberFormatter value={stock.price} prefix="$" decimalScale={2} />
<Sparkline data={stock.history} h={40} color={stock.change > 0 ? "green" : "red"} />
</Card>
)}
</SimpleGrid>
<LineChart h={200} data={data.timeline} dataKey="date" series={[{ name: "value", color: "blue" }]} />
</Stack>`,
requests: [],
},
{
id: "navidrome-player",
title: "Navidrome mini-player",
description: "Media controls backed by named actions with explicit permissions.",
template: `<Stack gap="sm" p="sm">
<Group wrap="nowrap">
<Image src={data.albumArt} alt={data.album} w={80} h={80} radius="sm" />
<Stack gap={2} style={{ flex: 1 }}>
<Text fw={700} lineClamp={1}>{data.title}</Text>
<Text size="sm" c="dimmed" lineClamp={1}>{data.artist}</Text>
<Progress value={data.progress} size="xs" color="red" />
</Stack>
</Group>
<Group justify="center" gap="sm">
<ActionButton requestId="previous" label="Previous" size="xs" variant="subtle" />
<ActionButton requestId="toggle-playback" label={data.playing ? "Pause" : "Play"} icon="play" color="red" invalidate={["parent"]} />
<ActionButton requestId="next" label="Next" size="xs" variant="subtle" />
</Group>
<ToggleSwitch requestId="shuffle" onParams={{ enabled: true }} offParams={{ enabled: false }} initialValue={data.shuffle} label="Shuffle" size="xs" />
</Stack>`,
requests: [
{
id: "previous",
kind: "action",
method: "POST",
pathTemplate: "/api/player/previous",
parameters: {},
auth: "inherit",
minimumBoardPermission: "modify",
},
{
id: "toggle-playback",
kind: "action",
method: "POST",
pathTemplate: "/api/player/toggle",
parameters: {},
auth: "inherit",
minimumBoardPermission: "modify",
},
{
id: "next",
kind: "action",
method: "POST",
pathTemplate: "/api/player/next",
parameters: {},
auth: "inherit",
minimumBoardPermission: "modify",
},
{
id: "shuffle",
kind: "action",
method: "PUT",
pathTemplate: "/api/player/shuffle",
parameters: { enabled: "boolean" },
bodyTemplate: { enabled: { $param: "enabled" } },
auth: "inherit",
minimumBoardPermission: "modify",
},
],
},
{
id: "pokedex-interactive",
title: "Interactive Pokédex",
description:
"A full-featured Pokédex with paginated list, detail SubFetch, type badges, stats with progress bars, moves grid, and evolution chain. Exercises filter(Boolean), pop(), find, reduce, sort, replaceAll, padStart, arithmetic, and nested SubFetch.",
template: `<Stack gap="sm" p="sm" style={{ minWidth: 0 }}>
<Group justify="space-between" wrap="nowrap">
<Group gap="sm" wrap="nowrap">
<ThemeIcon size="lg" radius="xl" variant="light" color="red">P</ThemeIcon>
<Box>
<Title order={3}>Pokédex</Title>
<Text size="xs" c="dimmed"><NumberFormatter value={data.count} thousandSeparator /> Pokémon discovered</Text>
</Box>
</Group>
<RefreshButton label="Refresh" size="xs" />
</Group>
<PaginatedList pageSize={5}>
{data.results.map((pokemon, index) =>
<Accordion key={pokemon.name} variant="separated" radius="md">
<Accordion.Item value={pokemon.name}>
<Accordion.Control>
<Group justify="space-between" wrap="nowrap" pr="xs">
<Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
<Avatar size="sm" radius="md" src={"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/" + (index + 1) + ".png"} />
<Box style={{ minWidth: 0 }}>
<Text size="xs" c="dimmed">#{String(index + 1).padStart(4, "0")}</Text>
<Text fw={700} tt="capitalize" lineClamp={1}>{pokemon.name.replaceAll("-", " ")}</Text>
</Box>
</Group>
<Badge size="xs" variant="outline">Details</Badge>
</Group>
</Accordion.Control>
<Accordion.Panel>
<SubFetch requestId="pokemon-detail" params={{ name: pokemon.name }}>
{(detail, meta) =>
<Stack gap="sm">
<Grid gutter="sm">
<Grid.Col span={{ base: 12, sm: 5 }}>
<Center>
<Image src={detail.sprites.front_default} alt={detail.name} fit="contain" h={120} />
</Center>
</Grid.Col>
<Grid.Col span={{ base: 12, sm: 7 }}>
<Stack gap="xs">
<Group gap="xs">
{detail.types.map((entry) =>
<Badge key={entry.type.name} tt="capitalize">{entry.type.name}</Badge>
)}
</Group>
<SimpleGrid cols={3} spacing="xs">
<Box><Text size="xs" c="dimmed">Height</Text><Text fw={700}><NumberFormatter value={detail.height / 10} decimalScale={1} suffix=" m" /></Text></Box>
<Box><Text size="xs" c="dimmed">Weight</Text><Text fw={700}><NumberFormatter value={detail.weight / 10} decimalScale={1} suffix=" kg" /></Text></Box>
<Box><Text size="xs" c="dimmed">Base XP</Text><Text fw={700}>{detail.base_experience || "—"}</Text></Box>
</SimpleGrid>
<Group gap="xs">
{detail.abilities.map((entry) =>
<Badge key={entry.ability.name} variant="light" size="xs" tt="capitalize">
{entry.ability.name.replaceAll("-", " ")}{entry.is_hidden ? " ·H" : ""}
</Badge>
)}
</Group>
</Stack>
</Grid.Col>
</Grid>
<Tabs defaultValue="stats" variant="outline" radius="md">
<Tabs.List>
<Tabs.Tab value="stats">Stats</Tabs.Tab>
<Tabs.Tab value="moves">Moves</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="stats" pt="sm">
<Stack gap="xs">
{detail.stats.map((entry) =>
<Box key={entry.stat.name}>
<Group justify="space-between" mb={2}>
<Text size="xs" tt="capitalize">{entry.stat.name.replaceAll("-", " ")}</Text>
<Text size="xs" fw={700}>{entry.base_stat}</Text>
</Group>
<Progress value={Math.min(100, entry.base_stat / 2.55)} size="sm" radius="xl" />
</Box>
)}
<Group justify="space-between">
<Text size="sm" fw={700}>Total</Text>
<Badge>{detail.stats.reduce((sum, s) => sum + s.base_stat, 0)}</Badge>
</Group>
</Stack>
</Tabs.Panel>
<Tabs.Panel value="moves" pt="sm">
<ScrollArea h={120} type="auto">
<SimpleGrid cols={{ base: 2, sm: 3 }} spacing="xs">
{detail.moves.slice(0, 30).map((entry) =>
<Badge key={entry.move.name} variant="light" size="xs" tt="capitalize" style={{ whiteSpace: "normal" }}>
{entry.move.name.replaceAll("-", " ")}
</Badge>
)}
</SimpleGrid>
</ScrollArea>
</Tabs.Panel>
</Tabs>
<Text size="xs" c="dimmed">Status: {meta.status}</Text>
</Stack>
}
</SubFetch>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
)}
</PaginatedList>
</Stack>`,
requests: [
{
id: "pokemon-detail",
kind: "query",
method: "GET",
pathTemplate: "/api/v2/pokemon/{name}",
parameters: { name: "string" },
auth: "none",
minimumBoardPermission: "view",
cacheTtlSeconds: 3600,
},
],
},
{
id: "smart-home-control",
title: "Smart-home control",
description: "A stateful light control with an explicit destructive confirmation.",
template: `<Stack gap="md" p="sm">
<Group justify="space-between">
<Title order={4}>Living room</Title>
<Badge color={data.state === "on" ? "yellow" : "gray"}>{data.state}</Badge>
</Group>
<ToggleSwitch requestId="set-light" onParams={{ state: "on" }} offParams={{ state: "off" }} initialValue={data.state === "on"} label="Power" color="yellow" />
<ActionButton requestId="delete-scene" label="Delete scene" color="red" icon="trash" confirmMessage="Delete this scene?" />
</Stack>`,
requests: [
{
id: "set-light",
kind: "action",
method: "PUT",
pathTemplate: "/api/lights/living-room",
parameters: { state: "string" },
bodyTemplate: { state: { $param: "state" } },
auth: "inherit",
minimumBoardPermission: "modify",
},
{
id: "delete-scene",
kind: "action",
method: "DELETE",
pathTemplate: "/api/scenes/living-room",
parameters: {},
auth: "inherit",
minimumBoardPermission: "full",
},
],
},
];