(null);
- const [activeFilters, setActiveFilters] = useState<
- Array<{
- label: string;
- value: string;
- }>
- >([]);
-
- return (
-
-
- RustLangES Components
-
-
- Change your computer theme to explore the different styles (light, dark)
-
-
-
-
-
- } label="Botón" />
- } label="Botón" />
- } />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {tree.subjects.map(subject => (
-
- {subject.topics.map(topic => (
-
- {topic.subtopics?.map(subtopic => (
-
- ))}
-
- ))}
-
- ))}
-
-
- Conoce todos nuestros proyectos Open Source en
- los que puedes contribuir y potenciar tu aprendizaje 🚀
-
-
-
-
-
- {tree.subjects.map(subject => (
-
- {subject.topics.map(topic => (
-
- {topic.subtopics?.map(subtopic => (
-
- ))}
-
- ))}
-
- ))}
-
-
- {tree.subjects.map(subject => (
-
- {subject.topics.map(topic => (
-
- {topic.subtopics?.map(subtopic => (
-
- ))}
-
- ))}
-
- ))}
-
-
- Conoce todos nuestros proyectos Open Source en
- los que puedes contribuir y potenciar tu aprendizaje 🚀
-
-
-
-
-
-
- } placeholder="Input" />
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/js/react/showcase/ErrorBoundary.ts b/js/react/showcase/ErrorBoundary.ts
deleted file mode 100644
index 8ac98b7..0000000
--- a/js/react/showcase/ErrorBoundary.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import React, { Component, PropsWithChildren } from "react";
-
-export class ErrorBoundary extends Component<
- PropsWithChildren<{ fallback(err: Error): React.ReactNode }>,
- { error?: Error }
-> {
- componentDidCatch(error: Error): void {
- console.error(error);
- this.setState({
- error,
- });
- }
-
- static getDerivedStateFromError(error: Error) {
- return { error };
- }
-
- render(): React.ReactNode {
- return this.state?.error
- ? this.props.fallback(this.state.error)
- : this.props.children;
- }
-}
diff --git a/js/react/showcase/ShowComponent/Container.tsx b/js/react/showcase/ShowComponent/Container.tsx
deleted file mode 100644
index bf477bc..0000000
--- a/js/react/showcase/ShowComponent/Container.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { PropsWithChildren } from "react";
-
-import { ChevronDown } from "../icons";
-
-export function ShowComponentContainer(
- props: PropsWithChildren<{
- title: React.ReactNode;
- className?: string;
- contentClassName?: string;
- }>
-) {
- return (
-
-
- {props.title}
-
-
-
-
-
- {props.children}
-
-
- );
-}
diff --git a/js/react/showcase/ShowComponent/Error.tsx b/js/react/showcase/ShowComponent/Error.tsx
deleted file mode 100644
index 5ba60f2..0000000
--- a/js/react/showcase/ShowComponent/Error.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { BombIcon } from "../icons";
-import { ShowComponentContainer } from "./Container";
-
-export function ShowComponentError({
- title,
- error: err,
-}: {
- title: string;
- error: Error;
-}) {
- return (
-
- {title}
- >
- }
- className="bg-red-300"
- contentClassName="flex-col"
- >
- {err.message}
- {(err.stack?.split?.("\n") ?? []).map(line => {
- // really last @
- const [, name, source_ = ""] = line.match(/(.*)@([^@]*)$/) ?? [, line];
-
- let source = source_;
-
- if (
- source.startsWith("vite/client") ||
- source.startsWith("react-refresh") ||
- name.includes("/node_modules/") ||
- name.startsWith("__require") ||
- name.trim().length === 0
- ) {
- return;
- }
-
- source = source.startsWith(location.origin)
- ? source.substring(location.origin.length)
- : source;
-
- // match to ?::
- const [, sourceFile, lineN, columnN] = source.match(
- /^(.+)?(?:t=\d+|v=\w+):(\d+):(\d+)$/
- ) ?? [, source, "", ""];
- source = `${sourceFile}${lineN}:${columnN}`;
-
- return (
-
- {name}
-
- {source}
-
-
- );
- })}
-
- );
-}
diff --git a/js/react/showcase/ShowComponent/Field.tsx b/js/react/showcase/ShowComponent/Field.tsx
deleted file mode 100644
index dd95fb0..0000000
--- a/js/react/showcase/ShowComponent/Field.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-import { useEffect } from "react";
-import { NormalizedProps } from "./types";
-
-export function ShowComponentField({
- name,
- def,
- setValue,
- modified,
- setModified,
- ...props
-}: {
- name: string;
- def: NormalizedProps[string];
- value: unknown;
- setValue(v: unknown): void;
- modified: boolean;
- setModified(v: boolean): void;
-}) {
- const value = !def.optional || modified ? props.value : def.default;
-
- useEffect(() => {
- if (def.type === "callback" && value) {
- const timer = setTimeout(() => setValue(false), 100);
- return () => clearTimeout(timer);
- }
- });
-
- return (
-
-
- {modified && (
-
- )}
- {def.type === "string" ? (
- <>
- {def.options.length ? (
-
- ) : (
-
- );
-}
diff --git a/js/react/showcase/ShowComponent/index.tsx b/js/react/showcase/ShowComponent/index.tsx
deleted file mode 100644
index d8d4180..0000000
--- a/js/react/showcase/ShowComponent/index.tsx
+++ /dev/null
@@ -1,161 +0,0 @@
-import React, { FC, JSX, useMemo, useState } from "react";
-
-import { ShowComponentContainer } from "./Container";
-import { ErrorBoundary } from "../ErrorBoundary";
-import { ShowComponentError } from "./Error";
-import { ShowComponentField } from "./Field";
-import { GenericPropDef, NormalizedProps, PropsDef } from "./types";
-
-export function ShowComponent>(props: {
- title?: string;
- component: FC
;
- propsDef: PropsDef
;
-}): JSX.Element;
-
-export function ShowComponent(props: {
- title: string;
- children: React.ReactNode;
-}): JSX.Element;
-
-export function ShowComponent(
- props:
- | {
- title?: string;
- component: FC;
- propsDef: GenericPropDef;
- }
- | { title: string; children: React.ReactNode }
-) {
- const title =
- props.title ??
- ("component" in props
- ? (props.component.displayName ?? props.component.name)
- : null) ??
- "No title";
-
- return (
- (
-
- )}
- >
-
-
- );
-}
-
-function ShowComponentInner(
- props: { title: string } & (
- | {
- component: FC;
- propsDef: GenericPropDef;
- }
- | { children: React.ReactNode }
- )
-) {
- const propsDef: GenericPropDef = useMemo(
- () => ("propsDef" in props ? props.propsDef : {}),
- [props]
- );
-
- const normalizedProps: NormalizedProps = useMemo(
- () =>
- Object.fromEntries(
- Object.entries(propsDef).map(([name, prop]) => {
- return [
- name,
- {
- displayName: name,
- hidden: false,
- placeholder: "",
- default:
- prop.type === "string"
- ? ""
- : prop.type === "boolean"
- ? false
- : prop.type === "number"
- ? 0
- : prop.type === "object"
- ? {}
- : prop.type === "function"
- ? () => {}
- : prop.type === "callback"
- ? () => {}
- : (() => {
- throw new Error(
- "Unknown prop type: " + prop.type
- );
- })(),
- disabled: false,
- optional: false,
- options: [],
- settedDefault: prop.default,
- ...prop,
- } satisfies NormalizedProps[string],
- ];
- })
- ),
- [propsDef]
- );
-
- const [childrenProps, inputs] = processProps(normalizedProps);
-
- return (
-
- {!!Object.keys(inputs).length && (
-
- {inputs}
-
- )}
-
- {"component" in props ? (
-
- ) : (
- props.children
- )}
-
-
- );
-}
-
-function processProps(
- props: NormalizedProps
-): [Record, React.ReactNode[]] {
- const newProps = Object.fromEntries(
- Object.entries(props).map(([name, prop]) => {
- const [value, setValue] = useState(prop.default);
- const [modified, setModified] = useState(false);
-
- return [
- name,
- {
- value:
- prop.type === "callback"
- ? () => setValue(true)
- : !prop.optional || modified
- ? value
- : prop.settedDefault,
- element: (
-
- ),
- },
- ];
- })
- );
-
- const childrenProps = Object.fromEntries(
- Object.entries(newProps).map(([k, v]) => [k, v.value])
- );
-
- const inputs = Object.values(newProps).map(p => p.element);
-
- return [childrenProps, inputs];
-}
diff --git a/js/react/showcase/ShowComponent/types.ts b/js/react/showcase/ShowComponent/types.ts
deleted file mode 100644
index f98e400..0000000
--- a/js/react/showcase/ShowComponent/types.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-export type FieldType = unknown extends F
- ? string
- : Field extends string
- ? "string"
- : Field extends boolean
- ? "boolean"
- : Field extends Function
- ? "function" | "callback"
- : Field extends object
- ? "object"
- : string;
-
-export type PropsDefOptional = undefined extends Field
- ? Field extends undefined
- ? { optional: true } | { optional?: true; default: NonNullable }
- : unknown
- : { optional?: false };
-
-export type PropsDefConfig = {
- type: FieldType, Field>;
- displayName?: string;
- hidden?: boolean;
- placeholder?: string;
- default?: NonNullable;
- options?: Field[];
- optional?: boolean;
- disabled?: boolean;
-} & PropsDefOptional;
-
-export type PropsDef = {
- [K in keyof P]: PropsDefConfig
;
-};
-
-export type GenericPropDef = Record>;
-export type NormalizedProps = Record<
- string,
- Required> & { settedDefault: unknown }
->;
diff --git a/js/react/showcase/main.tsx b/js/react/showcase/main.tsx
index db7b4ab..8ee5454 100644
--- a/js/react/showcase/main.tsx
+++ b/js/react/showcase/main.tsx
@@ -1,18 +1,11 @@
-import ReactDOM from "react-dom/client";
-import React from "react";
-import "./styles.css";
-
-import { App } from "./App";
+import { setupShowcase } from "@rustlanges/showcase/react";
+import * as icons from "../lib/icons";
-const root = document.getElementById("root");
+import "@rustlanges/showcase/styles.css";
+import "./styles.css";
-if (!root) {
- alert("No root element");
- throw "No root element";
-}
+import "../lib/showcases";
-ReactDOM.createRoot(root).render(
-
-
-
-);
+setupShowcase({
+ icons,
+});
diff --git a/js/react/showcase/styles.css b/js/react/showcase/styles.css
index 6842d94..f5cfbe3 100644
--- a/js/react/showcase/styles.css
+++ b/js/react/showcase/styles.css
@@ -1,6 +1 @@
@import "@rustlanges/react/styles.css";
-@import "tailwindcss";
-
-html {
- background: var(--color-gray-300);
-}
diff --git a/js/react/vite.config.ts b/js/react/vite.config.ts
index 5ed78f4..c0aa42f 100644
--- a/js/react/vite.config.ts
+++ b/js/react/vite.config.ts
@@ -6,14 +6,20 @@ import react from "@vitejs/plugin-react-swc";
import dts from "vite-plugin-dts";
import tailwindcss from "@tailwindcss/vite";
+const shouldBuildShowcase = process.env.BUILD_SHOWCASE === "1";
+
+if (shouldBuildShowcase)
+ console.log("==================\n BUILDING SHOWCASE \n==================");
+
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss(),
- dts({
- tsconfigPath: resolve(__dirname, "tsconfig.lib.json"),
- }),
+ !shouldBuildShowcase &&
+ dts({
+ tsconfigPath: resolve(__dirname, "tsconfig.lib.json"),
+ }),
],
resolve: {
alias: {
@@ -22,33 +28,38 @@ export default defineConfig({
"@": resolve(__dirname, "./lib"),
},
},
- build: {
- copyPublicDir: false,
- lib: {
- entry: resolve(__dirname, "lib/index.ts"),
- formats: ["es"],
- },
- rollupOptions: {
- external: ["react", "react/jsx-runtime"],
- input: Object.fromEntries(
- // https://rollupjs.org/configuration-options/#input
- glob
- .sync("lib/**/*.{ts,tsx}", {
- ignore: ["lib/**/*.d.ts"],
- })
- .map(file => [
- // 1. The name of the entry point
- // lib/nested/foo.js becomes nested/foo
- relative("lib", file.slice(0, file.length - extname(file).length)),
- // 2. The absolute path to the entry file
- // lib/nested/foo.ts becomes /project/lib/nested/foo.ts
- fileURLToPath(new URL(file, import.meta.url)),
- ])
- ),
- output: {
- assetFileNames: "assets/[name][extname]",
- entryFileNames: "[name].js",
+ build: shouldBuildShowcase
+ ? {}
+ : {
+ copyPublicDir: false,
+ lib: {
+ entry: resolve(__dirname, "lib/index.ts"),
+ formats: ["es"],
+ },
+ rollupOptions: {
+ external: ["react", "react/jsx-runtime"],
+ input: Object.fromEntries(
+ // https://rollupjs.org/configuration-options/#input
+ glob
+ .sync("lib/**/*.{ts,tsx}", {
+ ignore: ["lib/**/*.showcase.{ts,tsx}", "lib/**/*.d.ts"],
+ })
+ .map(file => [
+ // 1. The name of the entry point
+ // lib/nested/foo.js becomes nested/foo
+ relative(
+ "lib",
+ file.slice(0, file.length - extname(file).length)
+ ),
+ // 2. The absolute path to the entry file
+ // lib/nested/foo.ts becomes /project/lib/nested/foo.ts
+ fileURLToPath(new URL(file, import.meta.url)),
+ ])
+ ),
+ output: {
+ assetFileNames: "assets/[name][extname]",
+ entryFileNames: "[name].js",
+ },
+ },
},
- },
- },
});
diff --git a/js/showcase/.gitignore b/js/showcase/.gitignore
new file mode 100644
index 0000000..c0fbd91
--- /dev/null
+++ b/js/showcase/.gitignore
@@ -0,0 +1,25 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+workspace.code-workspace
diff --git a/js/showcase/eslint.config.ts b/js/showcase/eslint.config.ts
new file mode 100644
index 0000000..6fec779
--- /dev/null
+++ b/js/showcase/eslint.config.ts
@@ -0,0 +1,31 @@
+import globals from "globals";
+import { globalIgnores } from "eslint/config";
+import js from "@eslint/js";
+import ts from "typescript-eslint";
+
+export default ts.config([
+ js.configs.recommended,
+ ts.configs.recommended,
+ {
+ languageOptions: {
+ globals: {
+ ...globals.browser,
+ },
+ },
+
+ rules: {
+ "no-sparse-arrays": "off",
+ "@typescript-eslint/no-empty-object-type": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-namespace": "off",
+ "@typescript-eslint/no-unsafe-function-type": "off",
+ "@typescript-eslint/no-unused-vars": [
+ "error",
+ {
+ varsIgnorePattern: "^h$|^_",
+ },
+ ],
+ },
+ },
+ globalIgnores(["**/dist"]),
+]);
diff --git a/js/showcase/package.json b/js/showcase/package.json
new file mode 100644
index 0000000..e860a8d
--- /dev/null
+++ b/js/showcase/package.json
@@ -0,0 +1,61 @@
+{
+ "name": "@rustlanges/showcase",
+ "private": true,
+ "version": "0.0.1",
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./dist/src/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "./react": {
+ "types": "./dist/src/react/index.d.ts",
+ "default": "./dist/react/index.js"
+ },
+ "./styles.css": {
+ "default": "./dist/styles.css"
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/RustLangES/design-system-components",
+ "directory": "js/showcase"
+ },
+ "scripts": {
+ "build": "tsc -b ./tsconfig.lib.json && vite build && npm run check:tsc && npm run build:tailwindcss",
+ "build:tailwindcss": "npx @tailwindcss/cli -i src/styles.css -o dist/styles.css --minify",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0 && npm run check:tsc",
+ "check:tsc": "tsc --noEmit -p tsconfig.lib.json"
+ },
+ "peerDependencies": {
+ "react": "^19.1.0",
+ "react-dom": "^19.1.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.28.0",
+ "@rustlanges/styles": "file:../../styles",
+ "@tailwindcss/cli": "^4.1.8",
+ "@tailwindcss/vite": "^4.1.8",
+ "@types/node": "^22.15.29",
+ "@types/react": "^19.1.6",
+ "@types/react-dom": "^19.1.5",
+ "ajv": "^8.17.1",
+ "dom-expressions": "^0.39.10",
+ "eslint": "^9.28.0",
+ "glob": "^11.0.2",
+ "globals": "^16.2.0",
+ "jiti": "^2.4.2",
+ "tailwindcss": "^4.1.8",
+ "typescript": "^5.8.3",
+ "typescript-eslint": "^8.33.0",
+ "vite": "^6.3.5",
+ "vite-plugin-dts": "^4.5.4"
+ },
+ "dependencies": {
+ "alien-signals": "^2.0.5",
+ "clsx": "^2.1.1"
+ }
+}
diff --git a/js/showcase/src/case.tsx b/js/showcase/src/case.tsx
new file mode 100644
index 0000000..91115c2
--- /dev/null
+++ b/js/showcase/src/case.tsx
@@ -0,0 +1,191 @@
+import { ShowcaseDef } from ".";
+import { prepareProps, ShowcaseField } from "./field";
+import { ChevronDown } from "./icons";
+import { createSignal, h, MiniUI } from "./miniui";
+import { renderErrors } from "./error";
+
+const registeredCases: InternalCase[] = [];
+
+export function getRegisteredCases(): InternalCase[] {
+ return registeredCases;
+}
+
+export type PropDef = {
+ displayName?: string;
+ disabled?: boolean;
+ hidden?: boolean;
+ optional?: boolean;
+} & (
+ | {
+ kind: "raw";
+ value: unknown;
+ default?: any;
+ options?: never[];
+ }
+ | {
+ kind: "boolean";
+ default?: boolean;
+ options?: never[];
+ }
+ | {
+ kind: "callback";
+ default?: any;
+ options?: never[];
+ }
+ | {
+ kind: "function";
+ default?: boolean;
+ options?: never[];
+ }
+ | {
+ kind: "icon";
+ default?: boolean;
+ options?: never[];
+ }
+ | {
+ kind: "number";
+ default?: number;
+ options?: number[];
+ }
+ | {
+ kind: "string";
+ default?: string;
+ options?: string[];
+ }
+);
+
+export type PropKind = PropDef["kind"];
+
+export interface CaseDef {
+ props: Record | PropDef>;
+ component: TComponent;
+}
+
+export type InternalCase =
+ | {
+ title: string;
+ kind: "render";
+ render: TComponent;
+ }
+ | {
+ title: string;
+ kind: "def";
+ def: CaseDef;
+ };
+
+export function registerCase(title: string, def: CaseDef): void;
+export function registerCase(title: string, render: () => TNode): void;
+
+export function registerCase(
+ title: string,
+ defOrRender: CaseDef | (() => TNode)
+): void {
+ if (typeof defOrRender === "function") {
+ registeredCases.push({
+ title,
+ kind: "render",
+ render: defOrRender,
+ });
+ } else {
+ registeredCases.push({
+ title,
+ kind: "def",
+ def: defOrRender,
+ });
+ }
+}
+
+export function ShowCase({
+ showcaseDef,
+ caseDef,
+}: {
+ showcaseDef: ShowcaseDef;
+ caseDef: InternalCase;
+}): MiniUI.Node {
+ let renderer: () => TNode;
+ let isDefRenderer = false;
+ if (caseDef.kind === "render") {
+ renderer = () => showcaseDef.instiate(caseDef.render, {});
+ } else {
+ isDefRenderer = true;
+ renderer = () => ShowCaseDef(caseDef.def, showcaseDef);
+ }
+
+ const caseFailed = createSignal(true);
+
+ return (
+ [
+ "shadow-brutal details-content:flex max-w-case mx-auto mb-5 px-3 py-2",
+ "border-1 rounded-sm border-black",
+ !caseFailed() && "bg-light dark:bg-neutral-900",
+ caseFailed() && "bg-error-400 dark:bg-error-600",
+ ]}
+ >
+
+ {caseDef.title}
+
+
+
+
+ [
+ "flex",
+ !caseFailed() &&
+ isDefRenderer &&
+ "w-full flex-col items-center gap-2",
+ !caseFailed() && !isDefRenderer && "items-center justify-center pt-2",
+ caseFailed() && "flex-col",
+ ]}
+ >
+ {showcaseDef.renderNode(
+ showcaseDef.createErrorBoundary(
+ () => {
+ caseFailed(false);
+ return renderer();
+ },
+ errors => {
+ caseFailed(true);
+ return showcaseDef.attach(renderErrors(errors));
+ }
+ )
+ )}
+
+
+ );
+}
+
+function ShowCaseDef(
+ def: CaseDef,
+ showcaseDef: ShowcaseDef
+): TNode {
+ const {
+ defs: propDefs,
+ componentProps,
+ componentEvents,
+ } = prepareProps(def.props, showcaseDef);
+
+ const inputs = (
+
+ {...propDefs.map(propDef => )}
+
+ );
+
+ return showcaseDef.instiate(showcaseDef.renderCaseSplitted as TComponent, {
+ inputs: showcaseDef.attach(inputs),
+ component: def.component,
+ props: componentProps,
+ events: componentEvents,
+ });
+}
diff --git a/js/showcase/src/error.tsx b/js/showcase/src/error.tsx
new file mode 100644
index 0000000..a322d01
--- /dev/null
+++ b/js/showcase/src/error.tsx
@@ -0,0 +1,35 @@
+import { h, MiniUI } from "./miniui";
+
+export type ErrorStack = {
+ line: number;
+ column: number;
+ name: string;
+ source: string;
+};
+
+export type ErrorsDef = {
+ message: string;
+ stack: ErrorStack[];
+};
+
+export function renderError(error: ErrorStack): MiniUI.Node {
+ return (
+
+ {error.name}
+
+ {error.source}:{error.line}:{error.column}
+
+
+ );
+}
+
+export function renderErrors(error: ErrorsDef): MiniUI.Node {
+ return (
+ <>
+ {error.message}
+ {error.stack.map(renderError)}
+ >
+ );
+}
diff --git a/js/showcase/src/field.tsx b/js/showcase/src/field.tsx
new file mode 100644
index 0000000..82b1f34
--- /dev/null
+++ b/js/showcase/src/field.tsx
@@ -0,0 +1,329 @@
+import { ShowcaseDef } from ".";
+import { CaseDef, PropDef, PropKind } from "./case";
+import { Reset } from "./icons";
+import { createEffect, createSignal, h, Match, MiniUI, Show } from "./miniui";
+
+const PROP_KIND_DEFAULTS: { [K in PropKind]: any } = {
+ raw: undefined,
+ string: "",
+ boolean: false,
+ callback: () => {},
+ icon: void 0,
+ number: 0,
+ function: () => {},
+};
+
+function normalizeProp(
+ propName: string,
+ prop: CaseDef["props"][string]
+): Required {
+ if (typeof prop === "string") {
+ return {
+ displayName: propName,
+ kind: prop,
+ disabled: false,
+ hidden: false,
+ options: [],
+ default: PROP_KIND_DEFAULTS[prop],
+ optional: true,
+ } satisfies Required;
+ }
+
+ return {
+ default: prop.optional ? undefined : PROP_KIND_DEFAULTS[prop.kind],
+ displayName: propName,
+ disabled: false,
+ hidden: false,
+ optional: true,
+ options: [],
+ ...prop,
+ } satisfies Required;
+}
+
+export function normalizeProps(
+ props: CaseDef["props"]
+): Required[] {
+ return Object.entries(props).map(([propName, propDef]) =>
+ normalizeProp(propName, propDef)
+ );
+}
+
+export function prepareProps(
+ props: CaseDef["props"],
+ showcaseDef: ShowcaseDef
+): {
+ defs: ShowcaseFieldProps[];
+ componentProps: Record>;
+ componentEvents: Record>;
+} {
+ const normalizedProps = normalizeProps(props);
+
+ const defs: ShowcaseFieldProps[] = [];
+ const componentProps: [string, MiniUI.Signal][] = [];
+ const componentEvents: [string, MiniUI.Signal][] = [];
+
+ for (const propDef of normalizedProps) {
+ if (propDef.kind === "callback") {
+ const valueSignal = createSignal(false as unknown);
+
+ let timeout: NodeJS.Timeout;
+ createEffect(() => {
+ if (timeout) clearTimeout(timeout);
+ if (valueSignal()) timeout = setTimeout(() => valueSignal(false), 100);
+ });
+
+ defs.push({
+ ...propDef,
+ valueSignal,
+ showcaseDef,
+ });
+ componentEvents.push([
+ propDef.displayName,
+ () => {
+ valueSignal(true);
+ },
+ ]);
+ } else {
+ const valueSignal = createSignal(propDef.default);
+
+ defs.push({
+ ...propDef,
+ valueSignal,
+ showcaseDef,
+ });
+ componentProps.push([propDef.displayName, valueSignal]);
+ }
+ }
+
+ return {
+ defs,
+ componentProps: Object.fromEntries(componentProps),
+ componentEvents: Object.fromEntries(componentEvents),
+ };
+}
+
+export type ShowcaseFieldProps = Required & {
+ valueSignal: MiniUI.WritableSignal;
+ showcaseDef: ShowcaseDef;
+};
+
+export function ShowcaseField(fieldDef: ShowcaseFieldProps) {
+ const {
+ displayName,
+ // `default` is a reserved keyword
+ default: default_,
+ optional,
+ kind,
+ valueSignal,
+ } = fieldDef;
+
+ const modified = createSignal(false);
+
+ return (
+
+
+
+
+
+ [],
+ string: ShowcaseFieldString,
+ }}
+ />
+
+ );
+}
+
+export type ShowcaseTypeFieldProps = ShowcaseFieldProps & {
+ modified: MiniUI.WritableSignal;
+};
+
+export function ShowcaseFieldBoolean({
+ modified,
+ valueSignal,
+}: ShowcaseTypeFieldProps): MiniUI.Node {
+ return (
+
+ );
+}
+
+export function ShowcaseFieldFunction(): MiniUI.Node {
+ return (
+
+ );
+}
+
+export function ShowcaseFieldCallback({
+ valueSignal,
+}: ShowcaseTypeFieldProps): MiniUI.Node {
+ return (
+ [
+ "border-1 shadow-brutal",
+ "h-[1.7rem] min-w-[150px] px-1",
+ "rounded-sm text-center text-base",
+ (valueSignal() as boolean) && "bg-green-400",
+ ]}
+ >
+ Callback
+
+ );
+}
+
+export function ShowcaseFieldIcon({
+ valueSignal,
+ modified,
+ showcaseDef,
+}: ShowcaseTypeFieldProps): MiniUI.Node {
+ const valueToNamed: Map = new Map(
+ Object.entries(showcaseDef.icons).map(([name, comp]) => [comp, name])
+ );
+
+ return (
+
+ );
+}
+
+export function ShowcaseFieldNumber({
+ modified,
+ valueSignal,
+}: ShowcaseTypeFieldProps): MiniUI.Node {
+ return (
+ {
+ createEffect(() => {
+ ref.valueAsNumber = valueSignal() as number;
+ });
+ }}
+ onChange={e => {
+ modified(true);
+ valueSignal(e.currentTarget.valueAsNumber);
+ }}
+ />
+ );
+}
+
+export function ShowcaseFieldString({
+ default: default_,
+ modified,
+ options,
+ valueSignal,
+}: ShowcaseTypeFieldProps): MiniUI.Node {
+ return options.length ? (
+
+ ) : (
+