refactor(custom-widgets): extract domain package
This commit is contained in:
167 files changed
+12282
-10033
No files matched your search
@@ -1,300 +0,0 @@
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -1,739 +0,0 @@
|
||||
export type CustomJsxComponentPackage = "@mantine/core" | "@mantine/charts" | "@mantine/dates" | "@homarr/widgets";
|
||||
|
||||
export type CustomJsxComponentSafety = "allowed" | "wrapped" | "denied";
|
||||
|
||||
export type CustomJsxComponentCategory =
|
||||
| "layout"
|
||||
| "content"
|
||||
| "feedback"
|
||||
| "navigation"
|
||||
| "charts"
|
||||
| "dates"
|
||||
| "interaction"
|
||||
| "network"
|
||||
| "blocked";
|
||||
|
||||
export interface CustomJsxComponentDescriptor {
|
||||
name: string;
|
||||
package: CustomJsxComponentPackage;
|
||||
category: CustomJsxComponentCategory;
|
||||
safety: CustomJsxComponentSafety;
|
||||
supportedProps: readonly string[];
|
||||
subcomponents: readonly string[];
|
||||
accessibilityRequirements: readonly string[];
|
||||
documentationUrl: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
const commonSafeProps = [
|
||||
"key",
|
||||
"id",
|
||||
"style",
|
||||
"title",
|
||||
"role",
|
||||
"aria-label",
|
||||
"aria-describedby",
|
||||
"aria-hidden",
|
||||
"tabIndex",
|
||||
"w",
|
||||
"h",
|
||||
"miw",
|
||||
"maw",
|
||||
"mih",
|
||||
"mah",
|
||||
"m",
|
||||
"mx",
|
||||
"my",
|
||||
"mt",
|
||||
"mr",
|
||||
"mb",
|
||||
"ml",
|
||||
"p",
|
||||
"px",
|
||||
"py",
|
||||
"pt",
|
||||
"pr",
|
||||
"pb",
|
||||
"pl",
|
||||
"bg",
|
||||
"c",
|
||||
"opacity",
|
||||
"display",
|
||||
"hiddenFrom",
|
||||
"visibleFrom",
|
||||
"bd",
|
||||
"bdrs",
|
||||
"flex",
|
||||
"fz",
|
||||
"fw",
|
||||
"lh",
|
||||
"lts",
|
||||
"ta",
|
||||
"td",
|
||||
"tt",
|
||||
"radius",
|
||||
"shadow",
|
||||
"variant",
|
||||
"color",
|
||||
"size",
|
||||
"withBorder",
|
||||
] as const;
|
||||
|
||||
const categorySafeProps: Record<CustomJsxComponentCategory, readonly string[]> = {
|
||||
layout: [
|
||||
"gap",
|
||||
"align",
|
||||
"justify",
|
||||
"wrap",
|
||||
"direction",
|
||||
"grow",
|
||||
"cols",
|
||||
"spacing",
|
||||
"verticalSpacing",
|
||||
"gutter",
|
||||
"span",
|
||||
"offset",
|
||||
"order",
|
||||
"type",
|
||||
"orientation",
|
||||
"withBorder",
|
||||
"shadow",
|
||||
"radius",
|
||||
],
|
||||
content: [
|
||||
"alt",
|
||||
"src",
|
||||
"href",
|
||||
"target",
|
||||
"rel",
|
||||
"size",
|
||||
"fw",
|
||||
"fs",
|
||||
"td",
|
||||
"tt",
|
||||
"ta",
|
||||
"lts",
|
||||
"lh",
|
||||
"lineClamp",
|
||||
"truncate",
|
||||
"inherit",
|
||||
"gradient",
|
||||
"variant",
|
||||
"color",
|
||||
"value",
|
||||
"prefix",
|
||||
"suffix",
|
||||
"decimalScale",
|
||||
"thousandSeparator",
|
||||
"striped",
|
||||
"highlightOnHover",
|
||||
"withTableBorder",
|
||||
"withColumnBorders",
|
||||
"type",
|
||||
"icon",
|
||||
"image",
|
||||
"order",
|
||||
"fit",
|
||||
"fallbackSrc",
|
||||
"loading",
|
||||
"referrerPolicy",
|
||||
"crossOrigin",
|
||||
"decoding",
|
||||
"draggable",
|
||||
"center",
|
||||
"spacing",
|
||||
"listStyleType",
|
||||
],
|
||||
feedback: ["color", "variant", "radius", "size", "value", "sections", "label", "animated", "striped"],
|
||||
navigation: [
|
||||
"value",
|
||||
"defaultValue",
|
||||
"active",
|
||||
"label",
|
||||
"description",
|
||||
"leftSection",
|
||||
"rightSection",
|
||||
"opened",
|
||||
"disabled",
|
||||
"multiple",
|
||||
"chevronPosition",
|
||||
"orientation",
|
||||
"keepMounted",
|
||||
"transitionDuration",
|
||||
],
|
||||
charts: [
|
||||
"data",
|
||||
"dataKey",
|
||||
"series",
|
||||
"type",
|
||||
"curveType",
|
||||
"color",
|
||||
"colors",
|
||||
"withLegend",
|
||||
"withTooltip",
|
||||
"withXAxis",
|
||||
"withYAxis",
|
||||
"withDots",
|
||||
"gridAxis",
|
||||
"unit",
|
||||
"valueFormatter",
|
||||
"strokeWidth",
|
||||
"fillOpacity",
|
||||
"connectNulls",
|
||||
"tooltipAnimationDuration",
|
||||
],
|
||||
dates: [
|
||||
"date",
|
||||
"defaultDate",
|
||||
"value",
|
||||
"minDate",
|
||||
"maxDate",
|
||||
"locale",
|
||||
"firstDayOfWeek",
|
||||
"weekendDays",
|
||||
"hideOutsideDates",
|
||||
"hideWeekdays",
|
||||
"showWeekNumbers",
|
||||
"numberOfColumns",
|
||||
"level",
|
||||
"size",
|
||||
],
|
||||
interaction: [
|
||||
"label",
|
||||
"value",
|
||||
"values",
|
||||
"data",
|
||||
"checked",
|
||||
"defaultChecked",
|
||||
"disabled",
|
||||
"readOnly",
|
||||
"color",
|
||||
"variant",
|
||||
"size",
|
||||
"radius",
|
||||
"leftSection",
|
||||
"rightSection",
|
||||
"loading",
|
||||
"min",
|
||||
"max",
|
||||
"step",
|
||||
"total",
|
||||
"page",
|
||||
"pageSize",
|
||||
"defaultTab",
|
||||
"title",
|
||||
"defaultOpen",
|
||||
"type",
|
||||
"fullWidth",
|
||||
],
|
||||
network: [
|
||||
"requestId",
|
||||
"params",
|
||||
"onParams",
|
||||
"offParams",
|
||||
"initialValue",
|
||||
"label",
|
||||
"loadingLabel",
|
||||
"successMessage",
|
||||
"errorMessage",
|
||||
"confirmMessage",
|
||||
"invalidate",
|
||||
"refreshInterval",
|
||||
"trigger",
|
||||
"render",
|
||||
"path",
|
||||
"as",
|
||||
"fallback",
|
||||
"icon",
|
||||
"color",
|
||||
"variant",
|
||||
"size",
|
||||
"radius",
|
||||
"disabled",
|
||||
],
|
||||
blocked: [],
|
||||
};
|
||||
|
||||
const subcomponentsByName: Readonly<Record<string, readonly string[]>> = {
|
||||
Accordion: ["Accordion.Item", "Accordion.Control", "Accordion.Panel"],
|
||||
Avatar: ["Avatar.Group"],
|
||||
Card: ["Card.Section"],
|
||||
Chip: ["Chip.Group"],
|
||||
DataList: ["DataList.Item", "DataList.ItemLabel", "DataList.ItemValue"],
|
||||
EmptyState: ["EmptyState.Indicator", "EmptyState.Title", "EmptyState.Description", "EmptyState.Actions"],
|
||||
Grid: ["Grid.Col"],
|
||||
List: ["List.Item"],
|
||||
Menu: ["Menu.Target", "Menu.Dropdown", "Menu.Item", "Menu.Label", "Menu.Divider"],
|
||||
Progress: ["Progress.Section"],
|
||||
Table: ["Table.Thead", "Table.Tbody", "Table.Tfoot", "Table.Caption", "Table.Tr", "Table.Th", "Table.Td"],
|
||||
Tabs: ["Tabs.List", "Tabs.Tab", "Tabs.Panel"],
|
||||
Timeline: ["Timeline.Item"],
|
||||
};
|
||||
|
||||
const accessibilityByCategory: Record<CustomJsxComponentCategory, readonly string[]> = {
|
||||
layout: ["Keep visual order consistent with reading order."],
|
||||
content: ["Provide meaningful text alternatives for non-text content."],
|
||||
feedback: ["Do not rely on color alone to communicate status."],
|
||||
navigation: ["Provide an accessible label for controls without visible text."],
|
||||
charts: ["Include a nearby text summary of the chart data."],
|
||||
dates: ["Include a textual date when the visual calendar carries meaning."],
|
||||
interaction: ["Provide a visible label or aria-label."],
|
||||
network: ["Action labels must describe their effect; destructive actions require confirmation."],
|
||||
blocked: [],
|
||||
};
|
||||
|
||||
const componentSafeProps: Readonly<Record<string, readonly string[]>> = {
|
||||
"Tabs.List": ["grow"],
|
||||
ScrollArea: ["offsetScrollbars"],
|
||||
SubData: ["alt", "fit"],
|
||||
};
|
||||
|
||||
function documentationUrl(packageName: CustomJsxComponentPackage, name: string): string {
|
||||
if (packageName === "@homarr/widgets") return "https://homarr.dev/docs/management/custom-widgets/";
|
||||
const section = packageName === "@mantine/core" ? "core" : packageName === "@mantine/charts" ? "charts" : "dates";
|
||||
const slug = (name.split(".")[0] ?? "")
|
||||
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
||||
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
|
||||
.toLowerCase();
|
||||
return `https://mantine.dev/${section}/${slug}/`;
|
||||
}
|
||||
|
||||
const describe = (
|
||||
packageName: CustomJsxComponentPackage,
|
||||
category: CustomJsxComponentCategory,
|
||||
safety: CustomJsxComponentSafety,
|
||||
names: readonly string[],
|
||||
reason?: string,
|
||||
): CustomJsxComponentDescriptor[] =>
|
||||
names.map((name) => ({
|
||||
name,
|
||||
package: packageName,
|
||||
category,
|
||||
safety,
|
||||
supportedProps:
|
||||
safety === "denied"
|
||||
? []
|
||||
: [
|
||||
...new Set<string>([
|
||||
...commonSafeProps,
|
||||
...categorySafeProps[category],
|
||||
...(componentSafeProps[name] ?? []),
|
||||
]),
|
||||
],
|
||||
subcomponents: subcomponentsByName[name] ?? [],
|
||||
accessibilityRequirements: accessibilityByCategory[category],
|
||||
documentationUrl: documentationUrl(packageName, name),
|
||||
reason,
|
||||
}));
|
||||
|
||||
/**
|
||||
* The source of truth for the Custom JSX public component surface.
|
||||
*
|
||||
* Components marked `wrapped` require a runtime wrapper that removes unsafe props
|
||||
* or supplies inert/read-only behavior. `denied` entries are deliberately kept in
|
||||
* the registry so Mantine upgrades cannot silently expand the template surface.
|
||||
*/
|
||||
export const customJsxComponentRegistry: readonly CustomJsxComponentDescriptor[] = [
|
||||
...describe("@mantine/core", "layout", "allowed", [
|
||||
"Box",
|
||||
"Stack",
|
||||
"Group",
|
||||
"Flex",
|
||||
"Grid",
|
||||
"Grid.Col",
|
||||
"SimpleGrid",
|
||||
"Center",
|
||||
"Space",
|
||||
"Container",
|
||||
"AspectRatio",
|
||||
"ScrollArea",
|
||||
"Paper",
|
||||
"Card",
|
||||
"Card.Section",
|
||||
"Divider",
|
||||
"Fieldset",
|
||||
"VisuallyHidden",
|
||||
"Typography",
|
||||
"FloatingIndicator",
|
||||
]),
|
||||
...describe("@mantine/core", "content", "allowed", [
|
||||
"Text",
|
||||
"Title",
|
||||
"Code",
|
||||
"Highlight",
|
||||
"Mark",
|
||||
"Kbd",
|
||||
"Blockquote",
|
||||
"NumberFormatter",
|
||||
"Badge",
|
||||
"ThemeIcon",
|
||||
"ColorSwatch",
|
||||
"Table",
|
||||
"Table.Thead",
|
||||
"Table.Tbody",
|
||||
"Table.Tfoot",
|
||||
"Table.Caption",
|
||||
"Table.Tr",
|
||||
"Table.Th",
|
||||
"Table.Td",
|
||||
"List",
|
||||
"List.Item",
|
||||
"Timeline",
|
||||
"Timeline.Item",
|
||||
"Indicator",
|
||||
"Pill",
|
||||
"Image",
|
||||
"Avatar",
|
||||
"Avatar.Group",
|
||||
"BackgroundImage",
|
||||
"DataList",
|
||||
"DataList.Item",
|
||||
"DataList.ItemLabel",
|
||||
"DataList.ItemValue",
|
||||
"EmptyState",
|
||||
"EmptyState.Indicator",
|
||||
"EmptyState.Title",
|
||||
"EmptyState.Description",
|
||||
"EmptyState.Actions",
|
||||
"Marquee",
|
||||
"RollingNumber",
|
||||
]),
|
||||
...describe("@mantine/core", "feedback", "allowed", [
|
||||
"Alert",
|
||||
"Progress",
|
||||
"Progress.Section",
|
||||
"RingProgress",
|
||||
"SemiCircleProgress",
|
||||
"Skeleton",
|
||||
"Loader",
|
||||
]),
|
||||
...describe("@mantine/core", "feedback", "wrapped", ["Notification", "LoadingOverlay", "Overlay"]),
|
||||
...describe("@mantine/core", "navigation", "allowed", [
|
||||
"Accordion",
|
||||
"Accordion.Item",
|
||||
"Accordion.Control",
|
||||
"Accordion.Panel",
|
||||
"Tabs",
|
||||
"Tabs.List",
|
||||
"Tabs.Tab",
|
||||
"Tabs.Panel",
|
||||
"Stepper.Step",
|
||||
"HoverCard",
|
||||
"HoverCard.Target",
|
||||
"HoverCard.Dropdown",
|
||||
"Menu",
|
||||
"Menu.Target",
|
||||
"Menu.Dropdown",
|
||||
"Menu.Item",
|
||||
"Menu.Label",
|
||||
"Menu.Divider",
|
||||
"Tooltip",
|
||||
"Spoiler",
|
||||
"Collapse",
|
||||
"Transition",
|
||||
]),
|
||||
...describe("@mantine/core", "navigation", "wrapped", ["Anchor", "NavLink", "Breadcrumbs", "Stepper", "Tree"]),
|
||||
...describe("@mantine/core", "interaction", "wrapped", [
|
||||
"Button",
|
||||
"ActionIcon",
|
||||
"Burger",
|
||||
"CloseButton",
|
||||
"Chip",
|
||||
"Chip.Group",
|
||||
"CopyButton",
|
||||
"Pagination",
|
||||
"Rating",
|
||||
"SegmentedControl",
|
||||
"Slider",
|
||||
"Switch",
|
||||
]),
|
||||
...describe("@mantine/charts", "charts", "allowed", [
|
||||
"AreaChart",
|
||||
"BarChart",
|
||||
"BarsList",
|
||||
"BubbleChart",
|
||||
"ChartLegend",
|
||||
"ChartTooltip",
|
||||
"CompositeChart",
|
||||
"DonutChart",
|
||||
"FunnelChart",
|
||||
"Heatmap",
|
||||
"LineChart",
|
||||
"PieChart",
|
||||
"RadarChart",
|
||||
"RadialBarChart",
|
||||
"SankeyChart",
|
||||
"ScatterChart",
|
||||
"Sparkline",
|
||||
"Treemap",
|
||||
]),
|
||||
...describe("@mantine/dates", "dates", "wrapped", [
|
||||
"Calendar",
|
||||
"MiniCalendar",
|
||||
"DatePicker",
|
||||
"Day",
|
||||
"WeekdaysRow",
|
||||
"Month",
|
||||
"CalendarHeader",
|
||||
"YearPicker",
|
||||
"MonthPicker",
|
||||
"TimeGrid",
|
||||
"PickerControl",
|
||||
"YearsList",
|
||||
"MonthsList",
|
||||
"DecadeLevel",
|
||||
"YearLevel",
|
||||
"MonthLevel",
|
||||
"LevelsGroup",
|
||||
"DecadeLevelGroup",
|
||||
"YearLevelGroup",
|
||||
"MonthLevelGroup",
|
||||
]),
|
||||
...describe("@mantine/dates", "dates", "allowed", ["TimeValue"]),
|
||||
...describe("@homarr/widgets", "interaction", "allowed", [
|
||||
"PaginatedList",
|
||||
"TabsContainer",
|
||||
"TabPanel",
|
||||
"Collapsible",
|
||||
"StatBar",
|
||||
"TypeBadge",
|
||||
]),
|
||||
...describe("@homarr/widgets", "network", "wrapped", [
|
||||
"SubFetch",
|
||||
"SubData",
|
||||
"ActionButton",
|
||||
"ToggleSwitch",
|
||||
"RefreshButton",
|
||||
]),
|
||||
{
|
||||
name: "TablerIcon",
|
||||
package: "@homarr/widgets",
|
||||
category: "content",
|
||||
safety: "wrapped",
|
||||
supportedProps: ["name", "size", "color", "stroke"],
|
||||
subcomponents: [],
|
||||
accessibilityRequirements: accessibilityByCategory.content,
|
||||
documentationUrl: documentationUrl("@homarr/widgets", "TablerIcon"),
|
||||
},
|
||||
...describe(
|
||||
"@mantine/core",
|
||||
"blocked",
|
||||
"denied",
|
||||
[
|
||||
"AppShell",
|
||||
"Affix",
|
||||
"ComboboxPopover",
|
||||
"Dialog",
|
||||
"Drawer",
|
||||
"FloatingWindow",
|
||||
"FocusTrap",
|
||||
"Menubar",
|
||||
"Modal",
|
||||
"ModalBase",
|
||||
"Portal",
|
||||
"Popover",
|
||||
"RemoveScroll",
|
||||
],
|
||||
"Escapes the widget layout or focus boundary",
|
||||
),
|
||||
...describe(
|
||||
"@mantine/core",
|
||||
"blocked",
|
||||
"denied",
|
||||
[
|
||||
"AngleSlider",
|
||||
"Autocomplete",
|
||||
"Checkbox",
|
||||
"ColorInput",
|
||||
"ColorPicker",
|
||||
"Combobox",
|
||||
"FileButton",
|
||||
"FileInput",
|
||||
"Input",
|
||||
"InputBase",
|
||||
"JsonInput",
|
||||
"MaskInput",
|
||||
"MultiSelect",
|
||||
"NativeSelect",
|
||||
"NumberInput",
|
||||
"PasswordInput",
|
||||
"PillsInput",
|
||||
"PinInput",
|
||||
"Radio",
|
||||
"Select",
|
||||
"TagsInput",
|
||||
"Textarea",
|
||||
"TextInput",
|
||||
"TreeSelect",
|
||||
"UnstyledButton",
|
||||
],
|
||||
"Arbitrary form controls are not connected to the declarative action model",
|
||||
),
|
||||
...describe(
|
||||
"@mantine/core",
|
||||
"blocked",
|
||||
"denied",
|
||||
["OverflowList", "Scroller", "Splitter", "TableOfContents"],
|
||||
"Requires callback, document, or layout capabilities that are not exposed to templates",
|
||||
),
|
||||
...describe(
|
||||
"@mantine/core",
|
||||
"blocked",
|
||||
"denied",
|
||||
[
|
||||
"AccordionChevron",
|
||||
"ActionIconGroup",
|
||||
"ActionIconGroupSection",
|
||||
"AlphaSlider",
|
||||
"AppShellAside",
|
||||
"AppShellFooter",
|
||||
"AppShellHeader",
|
||||
"AppShellMain",
|
||||
"AppShellNavbar",
|
||||
"AppShellSection",
|
||||
"ButtonGroup",
|
||||
"ButtonGroupSection",
|
||||
"CheckIcon",
|
||||
"CheckboxCard",
|
||||
"CheckboxGroup",
|
||||
"CheckboxIndicator",
|
||||
"CloseIcon",
|
||||
"ColorSchemeScript",
|
||||
"ComboboxChevron",
|
||||
"ComboboxClearButton",
|
||||
"ComboboxDropdown",
|
||||
"ComboboxDropdownTarget",
|
||||
"ComboboxEmpty",
|
||||
"ComboboxEventsTarget",
|
||||
"ComboboxFooter",
|
||||
"ComboboxGroup",
|
||||
"ComboboxHeader",
|
||||
"ComboboxHiddenInput",
|
||||
"ComboboxOption",
|
||||
"ComboboxOptions",
|
||||
"ComboboxPopoverTarget",
|
||||
"ComboboxSearch",
|
||||
"ComboboxTarget",
|
||||
"DirectionProvider",
|
||||
"DrawerBody",
|
||||
"DrawerCloseButton",
|
||||
"DrawerContent",
|
||||
"DrawerHeader",
|
||||
"DrawerOverlay",
|
||||
"DrawerRoot",
|
||||
"DrawerStack",
|
||||
"DrawerTitle",
|
||||
"FlatTreeNode",
|
||||
"FloatingArrow",
|
||||
"FocusTrapInitialFocus",
|
||||
"HeadlessMantineProvider",
|
||||
"HoverCardGroup",
|
||||
"HueSlider",
|
||||
"InlineStyles",
|
||||
"InputClearButton",
|
||||
"InputClearSection",
|
||||
"InputDescription",
|
||||
"InputError",
|
||||
"InputLabel",
|
||||
"InputPlaceholder",
|
||||
"InputSuccess",
|
||||
"InputWrapper",
|
||||
"MantineProvider",
|
||||
"MantineThemeProvider",
|
||||
"MenuCheckboxGroup",
|
||||
"MenuCheckboxItem",
|
||||
"MenuContextMenu",
|
||||
"MenuRadioGroup",
|
||||
"MenuRadioItem",
|
||||
"MenuSearch",
|
||||
"MenuSub",
|
||||
"MenuSubDropdown",
|
||||
"MenuSubItem",
|
||||
"MenuSubTarget",
|
||||
"MenubarDropdown",
|
||||
"MenubarMenu",
|
||||
"MenubarTarget",
|
||||
"ModalBaseBody",
|
||||
"ModalBaseCloseButton",
|
||||
"ModalBaseContent",
|
||||
"ModalBaseHeader",
|
||||
"ModalBaseOverlay",
|
||||
"ModalBaseTitle",
|
||||
"ModalBody",
|
||||
"ModalCloseButton",
|
||||
"ModalContent",
|
||||
"ModalHeader",
|
||||
"ModalOverlay",
|
||||
"ModalRoot",
|
||||
"ModalStack",
|
||||
"ModalTitle",
|
||||
"NativeScrollArea",
|
||||
"OptionalPortal",
|
||||
"OptionsDropdown",
|
||||
"PaginationControl",
|
||||
"PaginationDots",
|
||||
"PaginationFirst",
|
||||
"PaginationItems",
|
||||
"PaginationLabel",
|
||||
"PaginationLast",
|
||||
"PaginationNext",
|
||||
"PaginationPrevious",
|
||||
"PaginationRoot",
|
||||
"PillGroup",
|
||||
"PillsInputField",
|
||||
"PopoverContextMenu",
|
||||
"PopoverDropdown",
|
||||
"PopoverTarget",
|
||||
"ProgressLabel",
|
||||
"ProgressRoot",
|
||||
"RadioCard",
|
||||
"RadioGroup",
|
||||
"RadioIcon",
|
||||
"RadioIndicator",
|
||||
"RangeSlider",
|
||||
"ScrollAreaAutosize",
|
||||
"SplitterPane",
|
||||
"StepperCompleted",
|
||||
"SwitchGroup",
|
||||
"TableScrollContainer",
|
||||
"TooltipFloating",
|
||||
"TooltipGroup",
|
||||
"TreeSelectOption",
|
||||
],
|
||||
"Internal, provider, compound-control, form, portal, or document-level export outside the safe declarative surface",
|
||||
),
|
||||
...describe(
|
||||
"@mantine/charts",
|
||||
"blocked",
|
||||
"denied",
|
||||
["AreaGradient"],
|
||||
"Low-level chart implementation helper that is not a standalone declarative display component",
|
||||
),
|
||||
...describe(
|
||||
"@mantine/dates",
|
||||
"blocked",
|
||||
"denied",
|
||||
[
|
||||
"DatesProvider",
|
||||
"HiddenDatesInput",
|
||||
"TimeInput",
|
||||
"TimePicker",
|
||||
"PickerInputBase",
|
||||
"DateInput",
|
||||
"DateTimePicker",
|
||||
"InlineDateTimePicker",
|
||||
"YearPickerInput",
|
||||
"MonthPickerInput",
|
||||
"DatePickerInput",
|
||||
],
|
||||
"Input and provider components are outside the read-only template surface",
|
||||
),
|
||||
];
|
||||
|
||||
export const enabledCustomJsxComponents = customJsxComponentRegistry.filter(({ safety }) => safety !== "denied");
|
||||
|
||||
export const customJsxComponentByName = new Map(customJsxComponentRegistry.map((entry) => [entry.name, entry]));
|
||||
|
||||
export const customJsxSupportedPropsByName = new Map(
|
||||
enabledCustomJsxComponents.map((component) => [component.name, new Set(component.supportedProps)]),
|
||||
);
|
||||
@@ -7,7 +7,7 @@ import { z } from "zod/v4";
|
||||
import { createDocumentationLink } from "./index";
|
||||
import { integrationDocSlugs } from "./integration-doc-slugs";
|
||||
import { widgetDocSlugs } from "./widget-doc-slugs";
|
||||
import { customJsxComponentRegistry } from "../custom-jsx";
|
||||
import { customJsxComponentRegistry } from "@homarr/custom-widgets/core";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
@@ -18,5 +18,3 @@ export * from "./hotkeys";
|
||||
export * from "./docker-integration-match";
|
||||
export * from "./integration-url-template";
|
||||
export * from "./release-provider";
|
||||
export * from "./custom-jsx";
|
||||
export * from "./custom-jsx-examples";
|
||||
@@ -1,229 +0,0 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { customJsxComponentByName, customJsxComponentRegistry } from "../custom-jsx";
|
||||
|
||||
const mantineCore941ComponentExports = [
|
||||
"RemoveScroll",
|
||||
"Collapse",
|
||||
"ScrollArea",
|
||||
"UnstyledButton",
|
||||
"VisuallyHidden",
|
||||
"Paper",
|
||||
"Popover",
|
||||
"ActionIcon",
|
||||
"CloseButton",
|
||||
"Group",
|
||||
"Loader",
|
||||
"Overlay",
|
||||
"ModalBase",
|
||||
"Input",
|
||||
"InputBase",
|
||||
"Flex",
|
||||
"FloatingIndicator",
|
||||
"Accordion",
|
||||
"Affix",
|
||||
"Alert",
|
||||
"Anchor",
|
||||
"AngleSlider",
|
||||
"AppShell",
|
||||
"AspectRatio",
|
||||
"Autocomplete",
|
||||
"Avatar",
|
||||
"BackgroundImage",
|
||||
"Badge",
|
||||
"Blockquote",
|
||||
"Breadcrumbs",
|
||||
"Burger",
|
||||
"Button",
|
||||
"Card",
|
||||
"Center",
|
||||
"Checkbox",
|
||||
"Chip",
|
||||
"Code",
|
||||
"ColorPicker",
|
||||
"ColorInput",
|
||||
"ColorSwatch",
|
||||
"Combobox",
|
||||
"ComboboxPopover",
|
||||
"Container",
|
||||
"CopyButton",
|
||||
"DataList",
|
||||
"Dialog",
|
||||
"Divider",
|
||||
"Drawer",
|
||||
"EmptyState",
|
||||
"Fieldset",
|
||||
"FileButton",
|
||||
"FileInput",
|
||||
"FloatingWindow",
|
||||
"FocusTrap",
|
||||
"Grid",
|
||||
"Highlight",
|
||||
"HoverCard",
|
||||
"Image",
|
||||
"Indicator",
|
||||
"JsonInput",
|
||||
"Kbd",
|
||||
"List",
|
||||
"LoadingOverlay",
|
||||
"Mark",
|
||||
"Marquee",
|
||||
"MaskInput",
|
||||
"Menu",
|
||||
"Menubar",
|
||||
"Modal",
|
||||
"MultiSelect",
|
||||
"NativeSelect",
|
||||
"NavLink",
|
||||
"Notification",
|
||||
"NumberFormatter",
|
||||
"NumberInput",
|
||||
"OverflowList",
|
||||
"Pagination",
|
||||
"PasswordInput",
|
||||
"Pill",
|
||||
"PillsInput",
|
||||
"PinInput",
|
||||
"Portal",
|
||||
"Progress",
|
||||
"Radio",
|
||||
"Rating",
|
||||
"RollingNumber",
|
||||
"RingProgress",
|
||||
"Scroller",
|
||||
"SegmentedControl",
|
||||
"Select",
|
||||
"SemiCircleProgress",
|
||||
"SimpleGrid",
|
||||
"Skeleton",
|
||||
"Slider",
|
||||
"Space",
|
||||
"Splitter",
|
||||
"Spoiler",
|
||||
"Stack",
|
||||
"Stepper",
|
||||
"Switch",
|
||||
"Table",
|
||||
"TableOfContents",
|
||||
"Tabs",
|
||||
"TagsInput",
|
||||
"Text",
|
||||
"Textarea",
|
||||
"TextInput",
|
||||
"ThemeIcon",
|
||||
"Timeline",
|
||||
"Title",
|
||||
"Tooltip",
|
||||
"Transition",
|
||||
"Tree",
|
||||
"TreeSelect",
|
||||
"Typography",
|
||||
] as const;
|
||||
|
||||
const mantineCharts941ComponentExports = [
|
||||
"ChartTooltip",
|
||||
"ChartLegend",
|
||||
"AreaChart",
|
||||
"BarChart",
|
||||
"LineChart",
|
||||
"Sparkline",
|
||||
"DonutChart",
|
||||
"PieChart",
|
||||
"RadarChart",
|
||||
"ScatterChart",
|
||||
"BubbleChart",
|
||||
"CompositeChart",
|
||||
"RadialBarChart",
|
||||
"FunnelChart",
|
||||
"Heatmap",
|
||||
"BarsList",
|
||||
"Treemap",
|
||||
"SankeyChart",
|
||||
] as const;
|
||||
|
||||
const mantineDates941ComponentExports = [
|
||||
"DatesProvider",
|
||||
"HiddenDatesInput",
|
||||
"TimeInput",
|
||||
"TimePicker",
|
||||
"TimeValue",
|
||||
"Day",
|
||||
"WeekdaysRow",
|
||||
"Month",
|
||||
"PickerControl",
|
||||
"YearsList",
|
||||
"MonthsList",
|
||||
"CalendarHeader",
|
||||
"DecadeLevel",
|
||||
"YearLevel",
|
||||
"MonthLevel",
|
||||
"LevelsGroup",
|
||||
"DecadeLevelGroup",
|
||||
"YearLevelGroup",
|
||||
"MonthLevelGroup",
|
||||
"PickerInputBase",
|
||||
"Calendar",
|
||||
"YearPicker",
|
||||
"MonthPicker",
|
||||
"DatePicker",
|
||||
"DateInput",
|
||||
"DateTimePicker",
|
||||
"InlineDateTimePicker",
|
||||
"YearPickerInput",
|
||||
"MonthPickerInput",
|
||||
"DatePickerInput",
|
||||
"TimeGrid",
|
||||
"MiniCalendar",
|
||||
] as const;
|
||||
|
||||
describe("customJsxComponentRegistry", () => {
|
||||
test("classifies every component name exactly once", () => {
|
||||
const names = customJsxComponentRegistry.map(({ name }) => name);
|
||||
expect(new Set(names).size).toBe(names.length);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["ChartTooltip", "allowed"],
|
||||
["ChartLegend", "allowed"],
|
||||
["VisuallyHidden", "allowed"],
|
||||
["SubFetch", "wrapped"],
|
||||
["Portal", "denied"],
|
||||
["TextInput", "denied"],
|
||||
] as const)("classifies %s as %s", (name, safety) => {
|
||||
expect(customJsxComponentByName.get(name)?.safety).toBe(safety);
|
||||
});
|
||||
|
||||
test("documents why every denied component is unavailable", () => {
|
||||
for (const component of customJsxComponentRegistry) {
|
||||
if (component.safety === "denied") {
|
||||
expect(component.reason, component.name).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("publishes complete authoring metadata for every component", () => {
|
||||
for (const component of customJsxComponentRegistry) {
|
||||
expect(component.documentationUrl, component.name).toMatch(/^https:\/\//);
|
||||
expect(component.supportedProps, component.name).toBeInstanceOf(Array);
|
||||
expect(component.subcomponents, component.name).toBeInstanceOf(Array);
|
||||
expect(component.accessibilityRequirements, component.name).toBeInstanceOf(Array);
|
||||
}
|
||||
});
|
||||
|
||||
test.each([
|
||||
["Tabs.List", "grow"],
|
||||
["ScrollArea", "offsetScrollbars"],
|
||||
["SubData", "fit"],
|
||||
["SubData", "alt"],
|
||||
] as const)("supports the %s.%s authoring prop", (component, prop) => {
|
||||
expect(customJsxComponentByName.get(component)?.supportedProps).toContain(prop);
|
||||
});
|
||||
|
||||
test.each([
|
||||
...mantineCore941ComponentExports,
|
||||
...mantineCharts941ComponentExports,
|
||||
...mantineDates941ComponentExports,
|
||||
])("classifies the Mantine 9.4.1 export %s", (name) => {
|
||||
expect(customJsxComponentByName.has(name), `${name} is not classified`).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user