fix(custom-widgets): finish v2 hardening
This commit is contained in:
97 files changed
+2833
-12393
No files matched your search
@@ -86,10 +86,12 @@ Use the interactive API reference to inspect the exact request body for each end
|
||||
|
||||
## Get Custom Widget authoring resources
|
||||
|
||||
Custom JSX v2 provides two HTTP discovery endpoints:
|
||||
Custom JSX v2 provides four HTTP discovery endpoints:
|
||||
|
||||
- `GET /api/custom-widgets/authoring-prompt` returns the current authoring prompt and live resource identifiers.
|
||||
- `GET /api/custom-widgets/skill` returns information about the Agent Skill.
|
||||
- `GET /api/custom-widgets/authoring-prompt` returns the current prompt plus MCP and HTTP resource identifiers.
|
||||
- `GET /api/custom-widgets/schema` returns the canonical widget JSON Schema.
|
||||
- `GET /api/custom-widgets/components` returns the release-matched static component catalog.
|
||||
- `GET /api/custom-widgets/skill` returns the complete Agent Skill files, catalog, examples, and skills.sh metadata.
|
||||
|
||||
Call `customWidget_secretRequestUser` to create a temporary credential entry URL. Do not construct or save this URL.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ npx skills add https://github.com/homarr-labs/homarr --skill homarr-custom-widge
|
||||
|
||||
The portable skill gives authoring instructions that do not require a tool. The Homarr MCP prompt gives instructions for a connected workflow.
|
||||
|
||||
The offline copy prompt includes the complete portable skill and all eight release-matched references. Those references contain the generated component APIs and runtime examples, so Homarr does not append duplicate raw catalog data. A model can author directly from this bundle without MCP tools or repository access.
|
||||
The offline copy prompt includes the complete portable skill and all eleven release-matched references, including the canonical widget JSON Schema, minified component catalog, and canonical runtime examples. A model can author directly from this bundle without MCP tools or repository access.
|
||||
|
||||
The prompt starts with the user's widget request. It ends with:
|
||||
|
||||
@@ -39,23 +39,28 @@ Repository code cannot give this attribution.
|
||||
|
||||
- Prompt: `homarr-custom-widget-author`
|
||||
- Resources: `homarr://custom-widgets/schema`, `components`, `components/{name}`, `examples/{name}`, and `skill`
|
||||
- HTTP: `GET /api/custom-widgets/authoring-prompt` and `GET /api/custom-widgets/skill`
|
||||
- HTTP: `GET /api/custom-widgets/authoring-prompt`, `schema`, `components`, and `skill`
|
||||
|
||||
The authoring-prompt tool description contains one sentence. The prompt contains the workflow instructions and resource links.
|
||||
|
||||
`GET /api/custom-widgets/skill`, the MCP tool, and the MCP resource return the live `SKILL.md` with:
|
||||
`GET /api/custom-widgets/skill` and the MCP tool return the complete release-matched skill discovery payload:
|
||||
|
||||
```ts
|
||||
{
|
||||
name: "homarr-custom-widget";
|
||||
version: string;
|
||||
skillMd: string;
|
||||
files: Array<{ path: string; content: string }>;
|
||||
componentCatalog: CustomJsxAuthoringCatalog;
|
||||
examples: CustomJsxExample[];
|
||||
skillsShUrl: string;
|
||||
sourceUrl: string;
|
||||
installCommand: string;
|
||||
}
|
||||
```
|
||||
|
||||
The `homarr://custom-widgets/skill` MCP resource returns the same material as one plain-text offline bundle, ending with the truncation sentinel and exact character count.
|
||||
|
||||
These interfaces do not return an archive URL or archive hash.
|
||||
|
||||
## Create a Custom Widget with MCP
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
# Create a Custom Widget
|
||||
|
||||
import { CanonicalCustomWidgetExample } from "@site/src/components/custom-widget-code";
|
||||
import { CanonicalCustomWidgetExample, CustomWidgetCodeExample } from "@site/src/components/custom-widget-code";
|
||||
|
||||
export const explicitOptionsBindingExample = `{
|
||||
"pathTemplate": "/api/endpoints/{endpointId}/docker/containers/json",
|
||||
"parameters": { "endpointId": "string", "showAll": "boolean" },
|
||||
"queryTemplate": { "all": { "$param": "showAll" } },
|
||||
"optionsBinding": {
|
||||
"endpointId": { "$option": "endpointId" },
|
||||
"showAll": false
|
||||
}
|
||||
}`;
|
||||
|
||||
The desktop workbench shows the code and preview next to each other. On a mobile device, use the Configure and Preview tabs.
|
||||
|
||||
@@ -68,10 +78,12 @@ The standard prompt contains fewer than 8,000 characters. It includes the instal
|
||||
|
||||
It does not include a complete prop list, MCP workflow, server URL, or credentials. The prompt with the embedded skill is intentionally longer. It starts with your widget request, then includes:
|
||||
|
||||
- `SKILL.md` and all eight release-matched reference files
|
||||
- `SKILL.md` and all eleven release-matched reference files, including the canonical widget JSON Schema
|
||||
- The Custom Widget, Mantine, skill, and catalog-schema versions
|
||||
- The compact canonical component catalog
|
||||
- Canonical runtime examples
|
||||
|
||||
The generated reference files already contain the exact component and prop APIs. Homarr does not append a second raw catalog or a duplicate examples payload.
|
||||
The generated Markdown references remain readable offline. The compact JSON catalog gives tools the identical machine-readable component records used by Homarr.
|
||||
|
||||
The embedded prompt ends with this sentinel and an exact total character count:
|
||||
|
||||
@@ -101,7 +113,7 @@ Widget options use a restricted object JSON Schema. Homarr supports the followin
|
||||
- URL
|
||||
- Nested group
|
||||
- Conditional field
|
||||
- Advanced JSON
|
||||
- JSON value
|
||||
|
||||
Credentials are not widget options. Homarr rejects credential names and static credential values in widget options.
|
||||
|
||||
@@ -121,17 +133,12 @@ A parameter declaration does not give a value to the parameter. A matching widge
|
||||
|
||||
For a load query, bind each parameter in `optionsBinding`. Use an option reference or a type-compatible primitive literal:
|
||||
|
||||
```json
|
||||
{
|
||||
"pathTemplate": "/api/endpoints/{endpointId}/docker/containers/json",
|
||||
"parameters": { "endpointId": "string", "showAll": "boolean" },
|
||||
"queryTemplate": { "all": { "$param": "showAll" } },
|
||||
"optionsBinding": {
|
||||
"endpointId": { "$option": "endpointId" },
|
||||
"showAll": false
|
||||
}
|
||||
}
|
||||
```
|
||||
<CustomWidgetCodeExample
|
||||
id="explicit-options-binding"
|
||||
label="Explicit load-query option bindings"
|
||||
language="json"
|
||||
code={explicitOptionsBindingExample}
|
||||
/>
|
||||
|
||||
For a manual query or action, supply all parameters with the component `params` prop. Use `SubFetch`, `ActionButton`, or `ToggleSwitch`.
|
||||
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
# Use the Custom JSX runtime
|
||||
|
||||
import { CustomJsxComponentReference } from "@site/src/components/custom-jsx-component-reference";
|
||||
import { CustomWidgetCodeExample } from "@site/src/components/custom-widget-code";
|
||||
|
||||
Homarr interprets declarative JSX in the error boundary of the widget. A template cannot import a module or run arbitrary JavaScript.
|
||||
|
||||
## Use data roots
|
||||
|
||||
```jsx
|
||||
<Stack>
|
||||
export const dataRootsExample = `<Stack>
|
||||
<TextInput bind="search" label="Search TV shows" />
|
||||
<SubFetch requestId="search-series" params={{ query: inputs.search }} />
|
||||
{data["search-series"]?.map((series) => (
|
||||
@@ -18,14 +14,87 @@ Homarr interprets declarative JSX in the error boundary of the widget. A templat
|
||||
</ActionButton>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>`;
|
||||
|
||||
export const localBindingExample = `{data.pokemon.map((pokemon) => {
|
||||
const segments = pokemon.url.split("/").filter((part) => part);
|
||||
const dexId = segments.at(-1);
|
||||
const isSelected = inputs.selectedPokemon === pokemon.name;
|
||||
|
||||
return (
|
||||
|
||||
<Card key={dexId} withBorder={isSelected}>
|
||||
<Text>#{dexId}</Text>
|
||||
<Text tt="capitalize">{pokemon.name}</Text>
|
||||
</Card>
|
||||
);
|
||||
})}`;
|
||||
|
||||
export const derivedValueExample = `{(() => {
|
||||
const visiblePokemon = data.pokemon.filter(
|
||||
(pokemon) => options.types.length === 0 || options.types.includes(pokemon.type),
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
<Stack>
|
||||
{visiblePokemon.map((pokemon) => (
|
||||
<Text>{pokemon.name}</Text>
|
||||
))}
|
||||
</Stack>
|
||||
```
|
||||
);
|
||||
})()}`;
|
||||
|
||||
export const recursiveListExample = `<RecursiveList
|
||||
data={data.evolution.chain}
|
||||
childrenPath="evolves_to"
|
||||
keyPath="species.name"
|
||||
defaultExpandedDepth={4}
|
||||
maxDepth={16}
|
||||
showLines
|
||||
>
|
||||
{(node, meta) => {
|
||||
const segments = node.species.url.split("/").filter((part) => part);
|
||||
const dexId = segments.at(-1);
|
||||
|
||||
return (
|
||||
<Group gap="xs">
|
||||
<Badge variant="light">#{dexId}</Badge>
|
||||
<Text tt="capitalize" fw={600}>
|
||||
{node.species.name}
|
||||
</Text>
|
||||
{meta.hasChildren && <Text c="dimmed">{meta.childCount} evolutions</Text>}
|
||||
</Group>
|
||||
);
|
||||
|
||||
}}
|
||||
|
||||
</RecursiveList>`;
|
||||
|
||||
export const temporaryInputsExample = `<TextInput bind="search" defaultValue="" />
|
||||
<DatePicker bind="selectedDate" />
|
||||
<Switch bind="enabled" defaultChecked={false} />
|
||||
<MultiSelect bind="selectedEntities" data={options.entities} />
|
||||
<SubFetch requestId="search" params={{ query: inputs.search }} />`;
|
||||
|
||||
Homarr interprets declarative JSX in the error boundary of the widget. A template cannot import a module or run arbitrary JavaScript.
|
||||
|
||||
## Use data roots
|
||||
|
||||
<CustomWidgetCodeExample
|
||||
id="custom-jsx-data-roots"
|
||||
label="Data roots and manual requests"
|
||||
language="jsx"
|
||||
code={dataRootsExample}
|
||||
/>
|
||||
|
||||
- `data.<requestId>` contains sanitized JSON or text results.
|
||||
- `status.<requestId>` contains the request status.
|
||||
- `status.<requestId>` contains `{ loading, ok, status, statusText, error }`.
|
||||
- `options` contains the validated board item configuration.
|
||||
- `inputs` contains temporary values from controls that use `bind`.
|
||||
|
||||
Check `status.list?.loading` while loading, `status.list?.ok === false` on failure, and `status.list?.ok === true` on success. Do not compare `status.list` with `"loading"`, `"success"`, or `"error"`; the status is an object, and validation rejects those comparisons.
|
||||
|
||||
Load queries get data automatically. They run at the widget refresh interval.
|
||||
|
||||
`SubFetch` runs a manual query. `ActionButton` and `ToggleSwitch` run actions.
|
||||
@@ -73,44 +142,23 @@ The explorer fetches only the plain, versioned static catalog. It does not impor
|
||||
|
||||
A collection callback can use a restricted block when the returned JSX needs the same derived value more than once:
|
||||
|
||||
```jsx
|
||||
{
|
||||
data.pokemon.map((pokemon) => {
|
||||
const segments = pokemon.url.split("/").filter((part) => part);
|
||||
const dexId = segments.at(-1);
|
||||
const isSelected = inputs.selectedPokemon === pokemon.name;
|
||||
|
||||
return (
|
||||
<Card key={dexId} withBorder={isSelected}>
|
||||
<Text>#{dexId}</Text>
|
||||
<Text tt="capitalize">{pokemon.name}</Text>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
}
|
||||
```
|
||||
<CustomWidgetCodeExample
|
||||
id="custom-jsx-local-bindings"
|
||||
label="Immutable local bindings"
|
||||
language="jsx"
|
||||
code={localBindingExample}
|
||||
/>
|
||||
|
||||
The block must contain one or more `const` declarations followed by exactly one final `return`. A declaration uses one simple identifier and a required initializer. An earlier local can be used by later declarations and the final JSX.
|
||||
|
||||
For one page-level derived value, directly invoke a zero-argument inline arrow:
|
||||
|
||||
```jsx
|
||||
{
|
||||
(() => {
|
||||
const visiblePokemon = data.pokemon.filter(
|
||||
(pokemon) => options.types.length === 0 || options.types.includes(pokemon.type),
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{visiblePokemon.map((pokemon) => (
|
||||
<Text>{pokemon.name}</Text>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
})();
|
||||
}
|
||||
```
|
||||
<CustomWidgetCodeExample
|
||||
id="custom-jsx-derived-iife"
|
||||
label="Page-level derived value"
|
||||
language="jsx"
|
||||
code={derivedValueExample}
|
||||
/>
|
||||
|
||||
This is not general JavaScript execution. The runtime rejects:
|
||||
|
||||
@@ -130,31 +178,13 @@ Relevant diagnostics include `INVALID_LOCAL_DECLARATION`, `LOCAL_BINDING_REQUIRE
|
||||
|
||||
Use the trusted `RecursiveList` special form for evolution chains, directory trees, nested comments, and other unknown-depth data. Do not write an authored recursive function or pass Mantine a render callback.
|
||||
|
||||
```jsx
|
||||
<RecursiveList
|
||||
data={data.evolution.chain}
|
||||
childrenPath="evolves_to"
|
||||
keyPath="species.name"
|
||||
defaultExpandedDepth={4}
|
||||
maxDepth={16}
|
||||
showLines
|
||||
>
|
||||
{(node, meta) => {
|
||||
const segments = node.species.url.split("/").filter((part) => part);
|
||||
const dexId = segments.at(-1);
|
||||
|
||||
return (
|
||||
<Group gap="xs">
|
||||
<Badge variant="light">#{dexId}</Badge>
|
||||
<Text tt="capitalize" fw={600}>
|
||||
{node.species.name}
|
||||
</Text>
|
||||
{meta.hasChildren && <Text c="dimmed">{meta.childCount} evolutions</Text>}
|
||||
</Group>
|
||||
);
|
||||
}}
|
||||
</RecursiveList>
|
||||
```
|
||||
<CustomWidgetCodeExample
|
||||
id="custom-jsx-recursive-list"
|
||||
label="Trusted recursive list"
|
||||
language="jsx"
|
||||
code={recursiveListExample}
|
||||
height="420px"
|
||||
/>
|
||||
|
||||
The child template receives the current `node` and this metadata:
|
||||
|
||||
@@ -180,13 +210,12 @@ Homarr detects cycles per ancestor chain. A malformed branch, render failure, or
|
||||
|
||||
Bind a control to a literal name. Read its value from `inputs`:
|
||||
|
||||
```jsx
|
||||
<TextInput bind="search" defaultValue="" />
|
||||
<DatePicker bind="selectedDate" />
|
||||
<Switch bind="enabled" defaultChecked={false} />
|
||||
<MultiSelect bind="selectedEntities" data={options.entities} />
|
||||
<SubFetch requestId="search" params={{ query: inputs.search }} />
|
||||
```
|
||||
<CustomWidgetCodeExample
|
||||
id="custom-jsx-temporary-inputs"
|
||||
label="Temporary inputs"
|
||||
language="jsx"
|
||||
code={temporaryInputsExample}
|
||||
/>
|
||||
|
||||
These values exist in React memory while the widget is mounted. Homarr does not save them in any storage location.
|
||||
|
||||
|
||||
@@ -193,6 +193,8 @@ Discovery resources:
|
||||
- `homarr://custom-widgets/examples/{name}`
|
||||
- `homarr://custom-widgets/skill`
|
||||
|
||||
An HTTP-only harness can use the matching `/api/custom-widgets/authoring-prompt`, `/schema`, `/components`, and `/skill` endpoints instead of MCP resource URIs.
|
||||
|
||||
The Custom Widget tools are:
|
||||
|
||||
- `customWidget_schema`
|
||||
|
||||
@@ -9,6 +9,10 @@ self-hosted Homarr instance.
|
||||
Workshop validates widget JSON in the browser before publishing. Homarr validates it again before installation. Widget
|
||||
exports must not contain credentials; users configure required credentials only after importing a widget.
|
||||
|
||||
Each submission stores the manifest's `$schema` value as separate compatibility metadata. Workshop marks the current
|
||||
Custom JSX schema and unsupported schemas in listings, while Homarr blocks installation if this metadata disagrees with
|
||||
the downloaded manifest. The manifest remains the source of truth and is always validated again locally.
|
||||
|
||||
Votes and reports are unique per user and submission. Listings expose vote totals without exposing voter identities.
|
||||
Reports include a category and explanation. Administrators can see the reporter, dismiss the report, or delete the
|
||||
reported submission.
|
||||
@@ -40,10 +44,12 @@ The central Workshop PocketBase service controls authentication. Register this c
|
||||
https://<workshop-host>/api/oauth2-redirect
|
||||
```
|
||||
|
||||
Self-hosted Homarr origins, including localhost and custom domains, do not need separate GitHub callbacks. The Workshop
|
||||
host completes authentication in a popup and returns the result to the calling Homarr page.
|
||||
Self-hosted Homarr origins, including localhost and custom domains, do not need separate GitHub callbacks. Homarr opens
|
||||
the popup synchronously, while PocketBase completes authentication through its realtime OAuth flow on the Workshop host.
|
||||
On successful sign-in, Homarr copies the GitHub display name or login and HTTPS avatar into that user's Workshop profile.
|
||||
The collection rules let users update their own profile fields but prevent them from setting `isAdmin`.
|
||||
|
||||
PocketBase permits all origins by default. If the deployment uses `--origins` or a restrictive reverse-proxy CORS policy,
|
||||
it must permit self-hosted Homarr origins. See [PocketBase routing and CORS](https://pocketbase.io/docs/go-routing/).
|
||||
|
||||
Homarr shows a visible error when the provider is misconfigured or the popup is blocked, cancelled, or times out.
|
||||
Homarr shows a visible error when the provider is misconfigured or the popup is blocked or cancelled.
|
||||
@@ -18,7 +18,6 @@
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"api-dump-contributors": "node ./scripts/dump-contributors.mjs",
|
||||
"check:custom-widgets-ste": "node ./scripts/check-custom-widgets-ste.mjs",
|
||||
"verify:search": "node ./scripts/verify-algolia-search.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
import { britishSpellings, imperativeVerbs, nonSteTerms, technicalTerms } from "./custom-widgets-ste-terms.mjs";
|
||||
|
||||
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
||||
const docsDirectory = resolve(scriptDirectory, "../docs");
|
||||
|
||||
const files = [
|
||||
{ path: "management/custom-widgets/agent-authoring.mdx" },
|
||||
{ path: "management/custom-widgets/authoring.mdx" },
|
||||
{ path: "management/custom-widgets/custom-jsx.mdx" },
|
||||
{ path: "management/custom-widgets/index.mdx" },
|
||||
{ path: "management/custom-widgets/requests-and-security.mdx" },
|
||||
{ path: "management/custom-widgets/troubleshooting.mdx" },
|
||||
{ path: "management/workshop/index.mdx" },
|
||||
{ path: "widgets/custom-api/index.mdx" },
|
||||
{ path: "management/api/index.mdx", section: "Get Custom Widget authoring resources" },
|
||||
{ path: "management/mcp.mdx", section: "Create a Custom Widget with an AI agent", includeMatching: /Custom Widget/ },
|
||||
{ path: "advanced/environment-variables/index.mdx", includeMatching: /WORKSHOP_API_URL/ },
|
||||
{ path: "community/faq.mdx", section: "Can I add my own widgets? Do I need programming skills?" },
|
||||
{ path: "management/backup.mdx", includeMatching: /Custom Widget/ },
|
||||
];
|
||||
|
||||
const imperativeSet = new Set(imperativeVerbs);
|
||||
const issues = [];
|
||||
|
||||
const overview = await readFile(resolve(docsDirectory, "management/custom-widgets/index.mdx"), "utf8");
|
||||
if (!overview.includes("## Audience and prerequisites")) {
|
||||
addIssue("management/custom-widgets/index.mdx", 1, "Add the required audience and prerequisites section.");
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const absolutePath = resolve(docsDirectory, file.path);
|
||||
const source = await readFile(absolutePath, "utf8");
|
||||
const selectedLines = selectLines(source, file);
|
||||
const paragraphs = getProseParagraphs(selectedLines);
|
||||
|
||||
for (const paragraph of paragraphs) {
|
||||
const sentences = splitSentences(paragraph.text);
|
||||
if (sentences.length > 6) {
|
||||
addIssue(file.path, paragraph.line, `Paragraph has ${sentences.length} sentences. The limit is 6.`);
|
||||
}
|
||||
|
||||
for (const sentence of sentences) {
|
||||
const wordCount = countWords(sentence);
|
||||
const isProcedure = paragraph.isList || startsWithImperative(sentence);
|
||||
const limit = isProcedure ? 20 : 25;
|
||||
if (wordCount > limit) {
|
||||
const type = isProcedure ? "Procedural" : "Descriptive";
|
||||
addIssue(file.path, paragraph.line, `${type} sentence has ${wordCount} words. The limit is ${limit}.`);
|
||||
}
|
||||
}
|
||||
|
||||
checkTerms(file.path, paragraph.line, paragraph.text);
|
||||
}
|
||||
}
|
||||
|
||||
if (issues.length > 0) {
|
||||
console.error("Custom Widget STE check failed:\n");
|
||||
for (const issue of issues) console.error(`- ${issue}`);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`Custom Widget STE check passed. ${technicalTerms.length} technical terms are registered.`);
|
||||
}
|
||||
|
||||
function selectLines(source, file) {
|
||||
const lines = source.split("\n").map((text, index) => ({ text, line: index + 1 }));
|
||||
if (!file.section && !file.includeMatching) return lines;
|
||||
|
||||
const selected = [];
|
||||
if (file.section) {
|
||||
const start = lines.findIndex(({ text }) => text.trim().replace(/^#{2,6}\s+/, "") === file.section);
|
||||
if (start === -1) throw new Error(`Section '${file.section}' was not found in ${file.path}`);
|
||||
const headingLevel = lines[start].text.trim().match(/^#+/)?.[0].length ?? 2;
|
||||
|
||||
for (let index = start; index < lines.length; index += 1) {
|
||||
const nextHeadingLevel = lines[index].text.trim().match(/^#+(?=\s)/)?.[0].length;
|
||||
if (index > start && nextHeadingLevel && nextHeadingLevel <= headingLevel) break;
|
||||
selected.push(lines[index]);
|
||||
}
|
||||
}
|
||||
|
||||
if (file.includeMatching) {
|
||||
for (const line of lines) {
|
||||
if (file.includeMatching.test(line.text) && !selected.some((item) => item.line === line.line))
|
||||
selected.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return selected.toSorted((left, right) => left.line - right.line);
|
||||
}
|
||||
|
||||
function getProseParagraphs(lines) {
|
||||
const paragraphs = [];
|
||||
let paragraph = null;
|
||||
let inFence = false;
|
||||
let inFrontmatter = false;
|
||||
let inTemplateExport = false;
|
||||
let inJsxTag = false;
|
||||
|
||||
const flush = () => {
|
||||
if (paragraph?.text.trim()) paragraphs.push({ ...paragraph, text: paragraph.text.trim() });
|
||||
paragraph = null;
|
||||
};
|
||||
|
||||
for (const { text: originalText, line } of lines) {
|
||||
const trimmed = originalText.trim();
|
||||
|
||||
if (line === 1 && trimmed === "---") {
|
||||
inFrontmatter = true;
|
||||
continue;
|
||||
}
|
||||
if (inFrontmatter && trimmed === "---") {
|
||||
inFrontmatter = false;
|
||||
continue;
|
||||
}
|
||||
if (trimmed.startsWith("```")) {
|
||||
flush();
|
||||
inFence = !inFence;
|
||||
continue;
|
||||
}
|
||||
if (inFence) continue;
|
||||
|
||||
if (/^export const .*?=\s*`/.test(trimmed)) inTemplateExport = true;
|
||||
if (inTemplateExport) {
|
||||
if (/`;\s*$/.test(trimmed)) inTemplateExport = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^(import|export)\s/.test(trimmed)) continue;
|
||||
if (trimmed.startsWith("<")) inJsxTag = true;
|
||||
if (inJsxTag) {
|
||||
if (trimmed.includes(">")) inJsxTag = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let text = trimmed;
|
||||
if (inFrontmatter) {
|
||||
const match = text.match(/^(title|description|sidebar_label):\s*["']?(.*?)["']?$/);
|
||||
if (!match) continue;
|
||||
text = match[2];
|
||||
}
|
||||
|
||||
if (!text || /^\|?\s*:?-+:?/.test(text)) {
|
||||
flush();
|
||||
continue;
|
||||
}
|
||||
|
||||
const isList = /^\s*(?:[-*]|\d+\.)\s+/.test(text);
|
||||
text = cleanMarkdown(text);
|
||||
if (!text) {
|
||||
flush();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isList || text.startsWith("#") || text.startsWith("|")) flush();
|
||||
text = text
|
||||
.replace(/^#{1,6}\s+/, "")
|
||||
.replace(/^\s*(?:[-*]|\d+\.)\s+/, "")
|
||||
.replace(/^\|\s*/, "")
|
||||
.replace(/\s*\|\s*$/, "");
|
||||
|
||||
if (!paragraph) paragraph = { text, line, isList };
|
||||
else paragraph.text += ` ${text}`;
|
||||
|
||||
if (isList || trimmed.startsWith("#") || trimmed.startsWith("|")) flush();
|
||||
}
|
||||
|
||||
flush();
|
||||
return paragraphs;
|
||||
}
|
||||
|
||||
function cleanMarkdown(text) {
|
||||
return text
|
||||
.replace(/\[([^\]]+)]\([^\s)]+(?:\s+"[^"]*")?\)/g, "$1")
|
||||
.replace(/`[^`]*`/g, " ")
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/[*_~]/g, "")
|
||||
.replace(/https?:\/\/\S+/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function splitSentences(text) {
|
||||
const normalized = text.replace(/\b(?:for example|such as)\b/gi, "example");
|
||||
const matches = normalized.match(/[^.!?]+[.!?]+|[^.!?]+$/g) ?? [];
|
||||
return matches.map((sentence) => sentence.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function countWords(sentence) {
|
||||
return sentence.match(/[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)*/g)?.length ?? 0;
|
||||
}
|
||||
|
||||
function startsWithImperative(sentence) {
|
||||
const firstWord = sentence.toLowerCase().match(/[a-z]+/)?.[0];
|
||||
return firstWord ? imperativeSet.has(firstWord) : false;
|
||||
}
|
||||
|
||||
function checkTerms(path, line, text) {
|
||||
const lowerText = text.toLowerCase();
|
||||
for (const spelling of britishSpellings) {
|
||||
if (new RegExp(`\\b${spelling}\\b`, "i").test(text)) {
|
||||
addIssue(path, line, `Use American English instead of '${spelling}'.`);
|
||||
}
|
||||
}
|
||||
for (const term of nonSteTerms) {
|
||||
if (new RegExp(`\\b${term}\\b`, "i").test(lowerText)) {
|
||||
addIssue(path, line, `Replace the non-STE term '${term}'.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addIssue(path, line, message) {
|
||||
issues.push(`${path}:${line} ${message}`);
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
export const technicalTerms = [
|
||||
"Homarr",
|
||||
"Custom Widget",
|
||||
"Custom JSX v2",
|
||||
"Workshop",
|
||||
"API source",
|
||||
"named request",
|
||||
"load query",
|
||||
"manual query",
|
||||
"action",
|
||||
"widget option",
|
||||
"temporary input",
|
||||
"CodeMirror",
|
||||
"Mantine",
|
||||
"PocketBase",
|
||||
"MCP",
|
||||
"JSON Schema",
|
||||
"API key",
|
||||
"network scope",
|
||||
"preview session",
|
||||
"error boundary",
|
||||
"HTTP",
|
||||
"JSON",
|
||||
"JSX",
|
||||
"React",
|
||||
"TypeScript",
|
||||
"Zod",
|
||||
"TLS",
|
||||
"DNS",
|
||||
"CORS",
|
||||
"OAuth",
|
||||
"SSRF",
|
||||
"tRPC",
|
||||
"GitHub",
|
||||
"Docker",
|
||||
"reverse proxy",
|
||||
"container networking",
|
||||
"API authentication",
|
||||
"bearer token",
|
||||
"basic authentication",
|
||||
"primitive literal",
|
||||
"prop metadata",
|
||||
"allowlist",
|
||||
"denylist",
|
||||
];
|
||||
|
||||
export const imperativeVerbs = [
|
||||
"add",
|
||||
"bind",
|
||||
"build",
|
||||
"call",
|
||||
"check",
|
||||
"configure",
|
||||
"correct",
|
||||
"create",
|
||||
"declare",
|
||||
"define",
|
||||
"delete",
|
||||
"disable",
|
||||
"download",
|
||||
"enable",
|
||||
"enter",
|
||||
"examine",
|
||||
"export",
|
||||
"get",
|
||||
"give",
|
||||
"import",
|
||||
"inspect",
|
||||
"install",
|
||||
"keep",
|
||||
"open",
|
||||
"paste",
|
||||
"read",
|
||||
"register",
|
||||
"remove",
|
||||
"repeat",
|
||||
"request",
|
||||
"run",
|
||||
"save",
|
||||
"select",
|
||||
"set",
|
||||
"start",
|
||||
"supply",
|
||||
"test",
|
||||
"use",
|
||||
"verify",
|
||||
"wait",
|
||||
"write",
|
||||
];
|
||||
|
||||
export const britishSpellings = [
|
||||
"authorise",
|
||||
"authorised",
|
||||
"behaviour",
|
||||
"cancelled",
|
||||
"centre",
|
||||
"colour",
|
||||
"customise",
|
||||
"customised",
|
||||
"initialise",
|
||||
"labelling",
|
||||
"licence",
|
||||
"organisation",
|
||||
"recognise",
|
||||
"serialise",
|
||||
"synchronise",
|
||||
"utilise",
|
||||
"visualise",
|
||||
];
|
||||
|
||||
export const nonSteTerms = [
|
||||
"easy",
|
||||
"easily",
|
||||
"genuinely",
|
||||
"leverage",
|
||||
"magic",
|
||||
"obviously",
|
||||
"seamless",
|
||||
"seamlessly",
|
||||
"simply",
|
||||
"straightforward",
|
||||
"utilize",
|
||||
"utilized",
|
||||
"wizard",
|
||||
];
|
||||
@@ -323,14 +323,6 @@
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.codeBlock {
|
||||
font-size: 0.75rem;
|
||||
margin: 0;
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.a11yList {
|
||||
font-size: 0.86rem;
|
||||
margin: 0 0 0.7rem;
|
||||
@@ -389,7 +381,7 @@
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.rawComponent .codeBlock {
|
||||
.rawComponent > div {
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,48 +2,19 @@ import BrowserOnly from "@docusaurus/BrowserOnly";
|
||||
import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { buildCustomJsxComponentUsageExample } from "@homarr/custom-widgets/catalog-example";
|
||||
import type {
|
||||
CustomJsxAuthoringCatalog,
|
||||
CustomJsxComponentApi,
|
||||
CustomJsxPropDescriptor,
|
||||
} from "@homarr/custom-widgets/catalog";
|
||||
|
||||
import { CustomWidgetCodeExample } from "./custom-widget-code";
|
||||
import styles from "./custom-jsx-component-reference.module.css";
|
||||
|
||||
type ComponentSafety = "allowed" | "wrapped" | "denied";
|
||||
type ComponentPackage = "@mantine/core" | "@mantine/dates" | "@mantine/charts" | "@homarr/widgets";
|
||||
type ComponentBindingType = "string" | "number" | "boolean" | "string[]" | "number[]";
|
||||
|
||||
interface CatalogProp {
|
||||
name: string;
|
||||
typeRef: number;
|
||||
required: boolean;
|
||||
source: "global" | "component";
|
||||
literalValues?: Array<string | number | boolean | null>;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface CatalogComponent {
|
||||
name: string;
|
||||
package: ComponentPackage;
|
||||
category: string;
|
||||
safety: ComponentSafety;
|
||||
description?: string;
|
||||
documentationUrl: string;
|
||||
props: CatalogProp[];
|
||||
blockedProps: Array<{ name: string; reason: string }>;
|
||||
subcomponents: string[];
|
||||
bind?: {
|
||||
type: ComponentBindingType;
|
||||
initialProp: "defaultValue" | "defaultChecked";
|
||||
};
|
||||
accessibilityRequirements: string[];
|
||||
deniedReason?: string;
|
||||
}
|
||||
|
||||
interface ComponentCatalog {
|
||||
schemaVersion: number;
|
||||
mantineVersion: string;
|
||||
customWidgetVersion: string;
|
||||
types: string[];
|
||||
globalProps: CatalogProp[];
|
||||
blockedCapabilities: Array<{ kind: string; name: string; reason: string }>;
|
||||
components: CatalogComponent[];
|
||||
}
|
||||
type ComponentCatalog = CustomJsxAuthoringCatalog;
|
||||
type CatalogComponent = CustomJsxComponentApi;
|
||||
type CatalogProp = CustomJsxPropDescriptor;
|
||||
|
||||
const PAGE_SIZE = 40;
|
||||
|
||||
@@ -395,10 +366,13 @@ function ComponentDetails({ id, component, types }: { id: string; component: Cat
|
||||
<div className={styles.details} id={id}>
|
||||
<div className={styles.detailsGrid}>
|
||||
<section>
|
||||
<h5>Usage example</h5>
|
||||
<pre className={styles.codeBlock}>
|
||||
<code>{buildUsageExample(component, types)}</code>
|
||||
</pre>
|
||||
<CustomWidgetCodeExample
|
||||
id={`${componentAnchor(component.name)}-usage`}
|
||||
label="Usage example"
|
||||
language="jsx"
|
||||
code={buildCustomJsxComponentUsageExample(component, { types })}
|
||||
height="180px"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
@@ -457,9 +431,12 @@ function ComponentDetails({ id, component, types }: { id: string; component: Cat
|
||||
|
||||
<details className={styles.rawComponent}>
|
||||
<summary>Raw component JSON</summary>
|
||||
<pre className={styles.codeBlock}>
|
||||
<code>{JSON.stringify(component, null, 2)}</code>
|
||||
</pre>
|
||||
<CustomWidgetCodeExample
|
||||
id={`${componentAnchor(component.name)}-json`}
|
||||
label="Component JSON"
|
||||
code={JSON.stringify(component, null, 2)}
|
||||
height="320px"
|
||||
/>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
@@ -518,47 +495,3 @@ function humanize(value: string) {
|
||||
function formatLiteral(value: string | number | boolean | null) {
|
||||
return typeof value === "string" ? JSON.stringify(value) : String(value);
|
||||
}
|
||||
|
||||
function buildUsageExample(component: CatalogComponent, types: string[]) {
|
||||
if (component.name === "RecursiveList") {
|
||||
return `<RecursiveList data={data.tree} childrenPath="children" keyPath="id" showLines>
|
||||
{(node, meta) => (
|
||||
<Group gap="xs">
|
||||
<Text>{node.label}</Text>
|
||||
{meta.hasChildren && <Badge>{meta.childCount}</Badge>}
|
||||
</Group>
|
||||
)}
|
||||
</RecursiveList>`;
|
||||
}
|
||||
|
||||
const props = component.props
|
||||
.filter((prop) => prop.required)
|
||||
.slice(0, 4)
|
||||
.map((prop) => `${prop.name}=${examplePropValue(prop, types[prop.typeRef] ?? "unknown")}`);
|
||||
if (component.bind) {
|
||||
props.unshift(
|
||||
`bind="value"`,
|
||||
`${component.bind.initialProp}=${bindingInitialValue(component.bind.type, component.bind.initialProp)}`,
|
||||
);
|
||||
}
|
||||
const propText = props.length > 0 ? ` ${props.join(" ")}` : "";
|
||||
return `<${component.name}${propText} />`;
|
||||
}
|
||||
|
||||
function bindingInitialValue(type: ComponentBindingType, initialProp: "defaultValue" | "defaultChecked") {
|
||||
if (initialProp === "defaultChecked") return "{false}";
|
||||
if (type === "string") return '""';
|
||||
if (type === "number") return "{0}";
|
||||
if (type === "boolean") return "{false}";
|
||||
return "{[]}";
|
||||
}
|
||||
|
||||
function examplePropValue(prop: CatalogProp, type: string) {
|
||||
const literal = prop.literalValues?.[0];
|
||||
if (literal !== undefined) return `{${formatLiteral(literal)}}`;
|
||||
if (type.includes("string")) return `"${prop.name}"`;
|
||||
if (type.includes("boolean")) return "{true}";
|
||||
if (type.includes("number")) return "{0}";
|
||||
if (type.includes("[]") || type.includes("Array<")) return "{[]}";
|
||||
return `{options.${prop.name}}`;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
WorkshopUser,
|
||||
} from "@homarr/workshop";
|
||||
import { validateWorkshopWidget, WorkshopClient, workshopExportFilename } from "@homarr/workshop";
|
||||
import { CUSTOM_WIDGET_SCHEMA } from "@homarr/custom-widgets/core";
|
||||
|
||||
import { CustomWidgetCodeExample } from "../custom-widget-code";
|
||||
import styles from "./workshop.module.css";
|
||||
@@ -31,6 +32,7 @@ export function WorkshopPage() {
|
||||
const [reportCategory, setReportCategory] = useState<WorkshopReport["category"]>("other");
|
||||
const [reportText, setReportText] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const draftValidation = useMemo(() => validateWorkshopWidget(draft.content), [draft.content]);
|
||||
const reportCounts = useMemo(() => {
|
||||
@@ -41,6 +43,7 @@ export function WorkshopPage() {
|
||||
const selectedReports = selected ? reports.filter((report) => report.submission === selected.id) : [];
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [nextReports, nextItems] = await Promise.all([
|
||||
user?.isAdmin ? client.listReports() : Promise.resolve([]),
|
||||
@@ -58,6 +61,8 @@ export function WorkshopPage() {
|
||||
setError("");
|
||||
} catch (cause) {
|
||||
setError(cause instanceof Error ? cause.message : "Workshop is unavailable");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [client, search, sort, user?.isAdmin]);
|
||||
|
||||
@@ -252,7 +257,7 @@ export function WorkshopPage() {
|
||||
</select>
|
||||
</div>
|
||||
{error && <div className={styles.error}>{error}</div>}
|
||||
<div className={styles.grid}>
|
||||
<div className={styles.grid} aria-busy={loading}>
|
||||
{items.map((item) => {
|
||||
const reportCount = reportCounts.get(item.id) ?? 0;
|
||||
return (
|
||||
@@ -273,6 +278,9 @@ export function WorkshopPage() {
|
||||
▲ {item.upvotes} · ▼ {item.downvotes}
|
||||
</span>
|
||||
</div>
|
||||
<span className={item.widgetSchema === CUSTOM_WIDGET_SCHEMA ? styles.success : styles.error}>
|
||||
{item.widgetSchema === CUSTOM_WIDGET_SCHEMA ? "Current" : "Unsupported"} · {item.widgetSchema}
|
||||
</span>
|
||||
{user?.isAdmin && reportCount > 0 && (
|
||||
<span className={styles.reportCount}>
|
||||
{reportCount} {reportCount === 1 ? "report" : "reports"}
|
||||
@@ -288,7 +296,8 @@ export function WorkshopPage() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{items.length === 0 && <p className={styles.empty}>No widgets match this search.</p>}
|
||||
{loading && items.length === 0 && <p className={styles.empty}>Loading Workshop…</p>}
|
||||
{!loading && items.length === 0 && <p className={styles.empty}>No widgets match this search.</p>}
|
||||
|
||||
{selected && (
|
||||
<div
|
||||
@@ -308,6 +317,9 @@ export function WorkshopPage() {
|
||||
▲ {selected.upvotes} · ▼ {selected.downvotes}
|
||||
</span>
|
||||
</div>
|
||||
<span className={selected.widgetSchema === CUSTOM_WIDGET_SCHEMA ? styles.success : styles.error}>
|
||||
{selected.widgetSchema === CUSTOM_WIDGET_SCHEMA ? "Current" : "Unsupported"} · {selected.widgetSchema}
|
||||
</span>
|
||||
{user?.isAdmin && selectedReports.length > 0 && (
|
||||
<section className={styles.moderation} aria-labelledby="submission-reports-title">
|
||||
<h3 id="submission-reports-title">
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"mantineVersion":"9.4.1","customWidgetVersion":"2.0.0","types":["\"\" | \"anonymous\" | \"use-credentials\"","\"-moz-initial\" | \"inherit\" | \"initial\" | \"revert\" | \"revert-layer\" | \"unset\" | \"auto\" | \"fixed\"","\"-moz-initial\" | \"inherit\" | \"initial\" | \"revert\" | \"revert-layer\" | \"unset\" | \"none\" | \"auto\" | \"all\" | \"fill\" | \"stroke\" | \"painted\" | \"visible\" | \"visibleFill\" | \"visiblePainted\" | \"visibleStroke\"","\"-moz-initial\" | \"inherit\" | \"initial\" | \"revert\" | \"revert-layer\" | \"unset\" | \"none\" | \"block\" | \"inline\" | \"both\" | \"horizontal\" | \"vertical\"","\"-moz-initial\" | \"inherit\" | \"initial\" | \"revert\" | \"revert-layer\" | \"unset\" | \"none\" | \"contain\" | \"cover\" | \"fill\" | \"scale-down\"","\"activity\" | \"display-none\"","\"all\" | \"parent\" | \"child\"","\"all\" | \"segment\"","\"always\" | \"hover\" | \"not-hover\" | \"never\"","\"auto-fit\" | \"auto-fill\"","\"block\" | \"grid\"","\"both\" | \"rightSection\" | \"clear\"","\"bottom\" | \"top\"","\"bottom\" | \"top\" | \"baseline\" | \"middle\"","\"bottom\" | \"top\" | \"middle\"","\"bottom-end\" | \"bottom-start\" | \"top-end\" | \"top-start\" | \"bottom-center\" | \"top-center\" | \"middle-center\" | \"middle-end\" | \"middle-start\"","\"button\" | \"submit\" | \"reset\"","\"center\" | \"bottom\"","\"center\" | \"left\" | \"right\"","\"center\" | \"side\" | \"merge\"","\"dashed\" | \"dotted\" | \"solid\"","\"default\" | \"responsive\"","\"div\" | \"label\"","\"end\" | \"start\"","\"fixed\" | \"absolute\"","\"horizontal\" | \"vertical\"","\"hover\" | \"click\" | \"click-hover\"","\"inherit\" | \"auto\" | \"baseline\" | \"before-edge\" | \"text-before-edge\" | \"middle\" | \"central\" | \"after-edge\" | \"text-after-edge\" | \"ideographic\" | \"alphabetic\" | \"hanging\" | \"mathematical\"","\"inherit\" | \"auto\" | \"sRGB\" | \"linearRGB\"","\"inherit\" | \"auto\" | \"text-before-edge\" | \"middle\" | \"central\" | \"text-after-edge\" | \"ideographic\" | \"alphabetic\" | \"hanging\" | \"mathematical\" | \"use-script\" | \"no-change\" | \"reset-size\"","\"inherit\" | \"end\" | \"start\" | \"middle\"","\"inherit\" | \"nonzero\" | \"evenodd\"","\"inherit\" | \"round\" | \"butt\" | \"square\"","\"inherit\" | \"round\" | \"miter\" | \"bevel\"","\"initial\" | \"medial\" | \"terminal\" | \"isolated\"","\"inside\" | \"outside\"","\"left\" | \"right\"","\"left\" | \"right\" | \"bottom\" | \"top\" | \"left-end\" | \"left-start\" | \"right-end\" | \"right-start\" | \"bottom-end\" | \"bottom-start\" | \"top-end\" | \"top-start\"","\"left\" | \"right\" | \"inside\"","\"link\" | \"none\" | \"copy\" | \"execute\" | \"move\" | \"popup\"","\"month\" | \"year\" | \"decade\"","\"none\" | \"all\" | \"rows\" | \"groups\" | \"columns\"","\"none\" | \"all\" | \"with-indicators\"","\"none\" | \"ascending\" | \"descending\" | \"other\"","\"none\" | \"inline\" | \"list\" | \"both\"","\"none\" | \"strict\" | \"blur\"","\"none\" | \"sum\"","\"none\" | \"thousand\" | \"lakh\" | \"wan\"","\"none\" | \"x\" | \"y\" | \"xy\"","\"off\" | \"assertive\" | \"polite\"","\"off\" | \"on\"","\"right-to-left\" | \"left-to-right\"","\"search\" | \"enter\" | \"done\" | \"go\" | \"next\" | \"previous\" | \"send\"","\"search\" | \"text\" | \"none\" | \"tel\" | \"url\" | \"email\" | \"numeric\" | \"decimal\"","\"step\" | \"bump\" | \"linear\" | \"natural\" | \"monotone\" | \"stepBefore\" | \"stepAfter\"","\"stepInactive\" | \"stepProgress\" | \"stepCompleted\"","\"sum\" | \"replace\"","\"td\" | \"th\"","\"text\" | \"additions\" | \"additions removals\" | \"additions text\" | \"all\" | \"removals\" | \"removals additions\" | \"removals text\" | \"text additions\" | \"text removals\"","\"value\" | \"name\" | \"percent\"","\"viewport\"","\"wrap\" | \"nowrap\" | \"balance\" | \"pretty\" | \"stable\"","\"x\" | \"y\"","\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\"","\"yes\" | \"no\"","'Text' | 'Title' | 'Badge' | 'Code' | 'Image'","'auto' | 'manual'","'json' | 'text'","'xs' | 'sm' | 'md' | 'lg' | 'xl'","(\"input\" | \"label\" | \"description\" | \"error\")[]","(\"next\" | \"previous\" | \"level\")[]","(ActionIconProps & React.ClassAttributes<HTMLButtonElement> & React.ButtonHTMLAttributes<HTMLButtonElement>)","(CartesianChartProps<unknown> & { ref?: React.Ref<SVGSVGElement>; })","(CloseButtonProps & ElementProps<\"button\"> & DataAttributes)","(CloseButtonProps & React.ClassAttributes<HTMLButtonElement> & React.ButtonHTMLAttributes<HTMLButtonElement>)","(ElementProps<\"button\"> & DataAttributes)","(InputDescriptionProps & DataAttributes)","(InputErrorProps & DataAttributes)","(InputLabelProps & DataAttributes)","(InputSuccessProps & DataAttributes)","(OverlayProps & ElementProps<\"div\">)","(PolarChartProps<unknown> & { ref?: React.Ref<SVGSVGElement>; })","(React.ClassAttributes<HTMLButtonElement> & React.ButtonHTMLAttributes<HTMLButtonElement> & DataAttributes)","(React.ClassAttributes<HTMLDivElement> &Line truncated
|
||||
{"schemaVersion":1,"mantineVersion":"9.4.1","customWidgetVersion":"2.0.0","types":["\"\" | \"anonymous\" | \"use-credentials\"","\"-moz-initial\" | \"inherit\" | \"initial\" | \"revert\" | \"revert-layer\" | \"unset\" | \"auto\" | \"fixed\"","\"-moz-initial\" | \"inherit\" | \"initial\" | \"revert\" | \"revert-layer\" | \"unset\" | \"none\" | \"auto\" | \"all\" | \"fill\" | \"stroke\" | \"painted\" | \"visible\" | \"visibleFill\" | \"visiblePainted\" | \"visibleStroke\"","\"-moz-initial\" | \"inherit\" | \"initial\" | \"revert\" | \"revert-layer\" | \"unset\" | \"none\" | \"block\" | \"inline\" | \"both\" | \"horizontal\" | \"vertical\"","\"-moz-initial\" | \"inherit\" | \"initial\" | \"revert\" | \"revert-layer\" | \"unset\" | \"none\" | \"contain\" | \"cover\" | \"fill\" | \"scale-down\"","\"-moz-initial\" | \"inherit\" | \"initial\" | \"revert\" | \"revert-layer\" | \"unset\" | \"wrap\" | \"nowrap\" | \"wrap-reverse\"","\"12h\" | \"24h\"","\"activity\" | \"display-none\"","\"all\" | \"parent\" | \"child\"","\"all\" | \"segment\"","\"always\" | \"hover\" | \"not-hover\" | \"never\"","\"area\" | \"radial\" | \"scatter\"","\"auto\" | \"scroll\" | \"always\" | \"hover\" | \"never\"","\"auto-fit\" | \"auto-fill\"","\"block\" | \"grid\"","\"both\" | \"rightSection\" | \"clear\"","\"bottom\" | \"top\"","\"bottom\" | \"top\" | \"baseline\" | \"middle\"","\"bottom\" | \"top\" | \"middle\"","\"bottom-end\" | \"bottom-start\" | \"top-end\" | \"top-start\" | \"bottom-center\" | \"top-center\" | \"middle-center\" | \"middle-end\" | \"middle-start\"","\"button\" | \"submit\" | \"reset\"","\"center\" | \"bottom\"","\"center\" | \"left\" | \"right\"","\"center\" | \"left\" | \"right\" | \"justify\" | \"char\"","\"center\" | \"side\" | \"merge\"","\"checkbox\" | \"radio\"","\"dashed\" | \"dotted\" | \"solid\"","\"default\" | \"gradient\"","\"default\" | \"responsive\"","\"default\" | \"stacked\" | \"percent\" | \"split\"","\"default\" | \"stacked\" | \"percent\" | \"waterfall\"","\"div\" | \"label\"","\"end\" | \"start\"","\"filled\" | \"light\"","\"fixed\" | \"absolute\"","\"hex\" | \"hexa\" | \"rgba\" | \"rgb\" | \"hsl\" | \"hsla\"","\"horizontal\" | \"vertical\"","\"hover\" | \"click\" | \"click-hover\"","\"inherit\" | \"auto\" | \"baseline\" | \"before-edge\" | \"text-before-edge\" | \"middle\" | \"central\" | \"after-edge\" | \"text-after-edge\" | \"ideographic\" | \"alphabetic\" | \"hanging\" | \"mathematical\"","\"inherit\" | \"auto\" | \"sRGB\" | \"linearRGB\"","\"inherit\" | \"auto\" | \"text-before-edge\" | \"middle\" | \"central\" | \"text-after-edge\" | \"ideographic\" | \"alphabetic\" | \"hanging\" | \"mathematical\" | \"use-script\" | \"no-change\" | \"reset-size\"","\"inherit\" | \"end\" | \"start\" | \"middle\"","\"inherit\" | \"nonzero\" | \"evenodd\"","\"inherit\" | \"round\" | \"butt\" | \"square\"","\"inherit\" | \"round\" | \"miter\" | \"bevel\"","\"initial\" | \"medial\" | \"terminal\" | \"isolated\"","\"inside\" | \"outside\"","\"left\" | \"right\"","\"left\" | \"right\" | \"bottom\" | \"top\" | \"left-end\" | \"left-start\" | \"right-end\" | \"right-start\" | \"bottom-end\" | \"bottom-start\" | \"top-end\" | \"top-start\"","\"left\" | \"right\" | \"inside\"","\"link\" | \"none\" | \"copy\" | \"execute\" | \"move\" | \"popup\"","\"media\" | \"container\"","\"month\" | \"year\" | \"decade\"","\"native\" | \"scrollarea\"","\"none\" | \"all\" | \"rows\" | \"groups\" | \"columns\"","\"none\" | \"all\" | \"with-indicators\"","\"none\" | \"ascending\" | \"descending\" | \"other\"","\"none\" | \"inline\" | \"list\" | \"both\"","\"none\" | \"strict\" | \"blur\"","\"none\" | \"sum\"","\"none\" | \"thousand\" | \"lakh\" | \"wan\"","\"none\" | \"x\" | \"y\" | \"xy\"","\"number\" | RegExp | \"alphanumeric\"","\"off\" | \"assertive\" | \"polite\"","\"off\" | \"on\"","\"ordered\" | \"unordered\"","\"right-to-left\" | \"left-to-right\"","\"search\" | \"enter\" | \"done\" | \"go\" | \"next\" | \"previous\" | \"send\"","\"search\" | \"text\" | \"none\" | \"tel\" | \"url\" | \"email\" | \"numeric\" | \"decimal\"","\"step\" | \"bump\" | \"linear\" | \"natural\" | \"monotone\" | \"stepBefore\" | \"stepAfter\"","\"stepInactive\" | \"stepProgress\" | \"stepCompleted\"","\"sum\" | \"replace\"","\"td\" | \"th\"","\"text\" | \"additions\" | \"additions removals\" | \"additions text\" | \"all\" | \"removals\" | \"removals additions\" | \"removals text\" | \"text additions\" | \"text removals\"","\"text\" | \"tel\" | \"password\"","\"time\" | \"duration\"","\"up\" | \"down\"","\"value\" | \"name\" | \"percent\"","\"viewport\"","\"wrap\" | \"nowrap\" | \"balance\" | \"pretty\" | \"stable\"","\"x\" | \"y\"","\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\"","\"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | (string & {})","\"yes\" | \"no\"","'Text' | 'Title' | 'Badge' | 'Code' | 'Image'","'auto' | 'manual'","'json' | 'text'","'xs' | 'sm' | 'md' | 'lg' | 'xl'","(\"input\" | \"label\" | \"description\" | \"error\")[]","(\"next\" | \"previLine truncated
|
||||
Reference in new issue
Block a user