From bbbd10ce0b435bbd2e328f35e49374fe7d0b9516 Mon Sep 17 00:00:00 2001 From: Michael Shafir Date: Fri, 25 Apr 2025 10:41:54 -0400 Subject: [PATCH 1/2] switch to biome, mise, add syncpack --- .changeset/config.json | 18 +- .prettierrc.js | 3 - .syncpackrc.json | 11 + .tool-versions | 1 - .vscode/extensions.json | 14 +- .vscode/launch.json | 20 +- .vscode/settings.json | 39 +- apps/reactlit-docs/astro.config.mjs | 88 +- apps/reactlit-docs/ec.config.mjs | 18 +- apps/reactlit-docs/package.json | 58 +- apps/reactlit-docs/src/content/config.ts | 4 +- .../apps/contact-list-basic-async.tsx | 100 +- .../src/examples/apps/contact-list.tsx | 86 +- .../src/examples/components/loader.tsx | 30 +- .../src/examples/contact-list-basic-async.tsx | 6 +- .../src/examples/contact-list-data-fetch.tsx | 192 +- .../src/examples/contact-list-react.tsx | 229 +- .../src/examples/contact-list.tsx | 18 +- .../src/examples/hello-world-react.tsx | 32 +- .../src/examples/hello-world.tsx | 26 +- .../src/examples/inputs/basic-text-input.tsx | 18 +- .../src/examples/layout-example.tsx | 66 +- .../examples/layouts/three-column-layout.tsx | 44 +- .../src/examples/mocks/contacts.ts | 72 +- .../src/examples/render-radix-app.tsx | 20 +- .../src/examples/transform-view.tsx | 62 +- apps/reactlit-docs/src/examples/utils/wait.ts | 2 +- apps/reactlit-docs/tsconfig.json | 12 +- apps/reactlit-examples/.eslintrc.json | 2 +- apps/reactlit-examples/next.config.ts | 8 +- apps/reactlit-examples/package.json | 72 +- apps/reactlit-examples/postcss.config.mjs | 6 +- .../src/components/debug-toggle.tsx | 66 +- .../reactlit-examples/src/components/main.tsx | 134 +- .../reactlit-examples/src/components/menu.tsx | 46 +- .../src/components/theme-toggle.tsx | 48 +- apps/reactlit-examples/src/mocks/todos.ts | 77 +- apps/reactlit-examples/src/pages/_app.tsx | 36 +- .../reactlit-examples/src/pages/_document.tsx | 24 +- .../src/pages/hello-world-vanilla/index.tsx | 238 +- .../src/pages/hello-world/index.tsx | 46 +- apps/reactlit-examples/src/pages/index.tsx | 18 +- .../src/pages/layout-test/index.tsx | 90 +- .../src/pages/radix-inputs/index.tsx | 362 +- .../src/pages/starter/index.tsx | 116 +- .../src/pages/todo-list/index.tsx | 186 +- apps/reactlit-examples/src/styles/globals.css | 62 +- apps/reactlit-examples/src/utils/utils.ts | 6 +- apps/reactlit-examples/src/utils/wait.ts | 2 +- apps/reactlit-examples/tailwind.config.ts | 28 +- apps/reactlit-examples/tsconfig.json | 40 +- biome.json | 43 + justfile | 11 - libs/core/.eslintrc.cjs | 16 +- libs/core/jest.config.cjs | 15 - libs/core/package.json | 122 +- libs/core/src/builtins/changed.ts | 80 +- libs/core/src/builtins/display.tsx | 188 +- libs/core/src/builtins/internal-state.ts | 34 +- libs/core/src/builtins/set.ts | 36 +- libs/core/src/builtins/types.ts | 88 +- libs/core/src/builtins/view.ts | 105 +- libs/core/src/hooks/use-deep-memo.ts | 18 +- libs/core/src/hooks/use-reactlit-state.ts | 83 +- libs/core/src/hooks/use-reactlit.spec.tsx | 58 +- libs/core/src/hooks/use-reactlit.tsx | 90 +- libs/core/src/index.ts | 28 +- libs/core/src/inputs/layout.view.tsx | 212 +- libs/core/src/plugins/data-fetching.ts | 170 +- libs/core/src/reactlit.spec.tsx | 55 +- libs/core/src/reactlit.tsx | 193 +- libs/core/src/utils/deep-equal.ts | 2 +- libs/core/src/utils/tail.ts | 3 +- libs/core/src/utils/tunnel.tsx | 120 +- libs/core/src/utils/unique-by.ts | 7 +- .../utils/use-isomorphic-layout-effect.tsx | 20 +- libs/core/src/wrappers.tsx | 75 +- libs/core/test-setup.ts | 9 + libs/core/tsconfig.build.json | 6 +- libs/core/tsconfig.json | 32 +- libs/core/tsup.config.ts | 20 +- libs/core/vitest.config.ts | 12 + libs/radix/.eslintrc.cjs | 10 - libs/radix/package.json | 106 +- libs/radix/src/index.ts | 26 +- libs/radix/src/inputs.ts | 42 +- libs/radix/src/inputs/async-button.input.tsx | 78 +- libs/radix/src/inputs/check.input.tsx | 73 +- libs/radix/src/inputs/radio.input.tsx | 73 +- libs/radix/src/inputs/search.input.tsx | 76 +- libs/radix/src/inputs/select.input.tsx | 77 +- libs/radix/src/inputs/slider.input.tsx | 90 +- libs/radix/src/inputs/switch.input.tsx | 30 +- libs/radix/src/inputs/table.input.tsx | 404 +- libs/radix/src/inputs/text.input.tsx | 92 +- libs/radix/src/inputs/textarea.input.tsx | 76 +- libs/radix/src/label.tsx | 22 +- libs/radix/src/radix-wrapper.tsx | 52 +- libs/radix/src/utils/repeat-element.tsx | 20 +- libs/radix/src/utils/user-friendly-name.ts | 26 +- libs/radix/tsconfig.json | 30 +- libs/radix/tsup.config.ts | 18 +- libs/vanilla/.eslintrc.cjs | 16 +- libs/vanilla/package.json | 96 +- libs/vanilla/src/index.ts | 14 +- libs/vanilla/src/inputs/check.input.tsx | 189 +- libs/vanilla/src/inputs/radio.input.tsx | 179 +- libs/vanilla/src/inputs/text.input.tsx | 68 +- libs/vanilla/src/label-wrapper.tsx | 24 +- libs/vanilla/tsconfig.json | 30 +- libs/vanilla/tsup.config.ts | 18 +- mise.toml | 22 + package.json | 60 +- pnpm-lock.yaml | 3785 +++++------------ turbo.json | 60 +- 115 files changed, 4597 insertions(+), 6338 deletions(-) delete mode 100644 .prettierrc.js create mode 100644 .syncpackrc.json delete mode 100644 .tool-versions create mode 100644 biome.json delete mode 100644 justfile delete mode 100644 libs/core/jest.config.cjs create mode 100644 libs/core/test-setup.ts create mode 100644 libs/core/vitest.config.ts delete mode 100644 libs/radix/.eslintrc.cjs create mode 100644 mise.toml diff --git a/.changeset/config.json b/.changeset/config.json index 34892af..db5bdd8 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,11 +1,11 @@ { - "$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json", - "changelog": "@changesets/cli/changelog", - "commit": true, - "fixed": [], - "linked": [], - "access": "restricted", - "baseBranch": "main", - "updateInternalDependencies": "patch", - "ignore": [] + "$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": true, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] } diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index 8a9d0e3..0000000 --- a/.prettierrc.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - singleQuote: true, -} \ No newline at end of file diff --git a/.syncpackrc.json b/.syncpackrc.json new file mode 100644 index 0000000..11d4a18 --- /dev/null +++ b/.syncpackrc.json @@ -0,0 +1,11 @@ +{ + "source": ["package.json", "*/*/package.json"], + "versionGroups": [ + { + "label": "Use workspace protocol when developing local packages", + "dependencies": ["$LOCAL"], + "dependencyTypes": ["dev", "prod"], + "pinVersion": "workspace:*" + } + ] +} diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index f31e6b0..0000000 --- a/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -nodejs 20.18.0 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index ba236ff..a8bd4bb 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,9 +1,9 @@ { - "recommendations": [ - "bradlc.vscode-tailwindcss", - "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode", - "astro-build.astro-vscode", - "unifiedjs.vscode-mdx" - ] + "recommendations": [ + "bradlc.vscode-tailwindcss", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "astro-build.astro-vscode", + "unifiedjs.vscode-mdx" + ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index cfcc69c..9f74f62 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,12 +1,12 @@ { - "version": "0.2.0", - "configurations": [ - { - "cwd": "${workspaceFolder}/apps/reactlit-docs", - "command": "./node_modules/.bin/astro dev", - "name": "Development server", - "request": "launch", - "type": "node-terminal" - } - ] + "version": "0.2.0", + "configurations": [ + { + "cwd": "${workspaceFolder}/apps/reactlit-docs", + "command": "./node_modules/.bin/astro dev", + "name": "Development server", + "request": "launch", + "type": "node-terminal" + } + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 976c263..da2d4c9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,14 +1,29 @@ { - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" - }, - "typescript.tsdk": "node_modules/typescript/lib", - "scss.lint.unknownAtRules": "ignore", - "css.lint.unknownAtRules": "ignore", - "editor.tabSize": 2, - "[javascript][typescript][javscriptreact][typescriptreact][css][scss][json][jsonc][markdown][yaml]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "editor.formatOnSave": true, - "files.eol": "\n" + "typescript.tsdk": "node_modules/typescript/lib", + "scss.lint.unknownAtRules": "ignore", + "css.lint.unknownAtRules": "ignore", + "editor.tabSize": 2, + "editor.defaultFormatter": "biomejs.biome", + "[javascript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "eslint.enable": false, + "prettier.enable": false, + "editor.codeActionsOnSave": { + "source.organizeImports.biome": "explicit", + "source.fixAll.eslint": "never", + "source.fixAll.ts": "never", + "source.fixAll.biome": "explicit" + }, + "editor.formatOnSave": true, + "files.eol": "\n" } diff --git a/apps/reactlit-docs/astro.config.mjs b/apps/reactlit-docs/astro.config.mjs index 2b64793..e74ead0 100644 --- a/apps/reactlit-docs/astro.config.mjs +++ b/apps/reactlit-docs/astro.config.mjs @@ -1,48 +1,48 @@ -import { defineConfig } from 'astro/config'; -import starlight from '@astrojs/starlight'; -import react from '@astrojs/react'; +import react from "@astrojs/react"; +import starlight from "@astrojs/starlight"; +import { defineConfig } from "astro/config"; // https://astro.build/config export default defineConfig({ - vite: { - ssr: { - noExternal: ['@radix-ui/themes'], - }, - }, - base: 'reactlit', - site: 'https://mshafir.github.io', - integrations: [ - starlight({ - title: 'Reactlit', - logo: { - src: '/src/assets/ReactlitwText.png', - alt: 'Reactlit', - replacesTitle: true, - }, - customCss: ['/src/styles/app.css'], - social: { - github: 'https://github.com/mshafir/reactlit', - }, - sidebar: [ - { - label: 'Guides', - items: [ - { label: 'Getting Started', slug: 'guides/getting-started' }, - { label: 'Installation', slug: 'guides/installation' }, - { label: 'Basics', slug: 'guides/basics' }, - { label: 'Data Fetching', slug: 'guides/data-fetching' }, - { label: 'Wrappers', slug: 'guides/wrappers' }, - { label: 'Layout', slug: 'guides/layout' }, - { label: 'Managed State', slug: 'guides/managed-state' }, - { label: 'Defining Views', slug: 'guides/defining-views' }, - ], - }, - // { - // label: 'Reference', - // autogenerate: { directory: 'reference' }, - // }, - ], - }), - react(), - ], + vite: { + ssr: { + noExternal: ["@radix-ui/themes"], + }, + }, + base: "reactlit", + site: "https://mshafir.github.io", + integrations: [ + starlight({ + title: "Reactlit", + logo: { + src: "/src/assets/ReactlitwText.png", + alt: "Reactlit", + replacesTitle: true, + }, + customCss: ["/src/styles/app.css"], + social: { + github: "https://github.com/mshafir/reactlit", + }, + sidebar: [ + { + label: "Guides", + items: [ + { label: "Getting Started", slug: "guides/getting-started" }, + { label: "Installation", slug: "guides/installation" }, + { label: "Basics", slug: "guides/basics" }, + { label: "Data Fetching", slug: "guides/data-fetching" }, + { label: "Wrappers", slug: "guides/wrappers" }, + { label: "Layout", slug: "guides/layout" }, + { label: "Managed State", slug: "guides/managed-state" }, + { label: "Defining Views", slug: "guides/defining-views" }, + ], + }, + // { + // label: 'Reference', + // autogenerate: { directory: 'reference' }, + // }, + ], + }), + react(), + ], }); diff --git a/apps/reactlit-docs/ec.config.mjs b/apps/reactlit-docs/ec.config.mjs index 1c37904..34f2db9 100644 --- a/apps/reactlit-docs/ec.config.mjs +++ b/apps/reactlit-docs/ec.config.mjs @@ -1,13 +1,13 @@ -import { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-sections'; -import { pluginLineNumbers } from '@expressive-code/plugin-line-numbers'; +import { pluginCollapsibleSections } from "@expressive-code/plugin-collapsible-sections"; +import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers"; /** @type {import('@astrojs/starlight/expressive-code').StarlightExpressiveCodeOptions} */ export default { - plugins: [pluginCollapsibleSections(), pluginLineNumbers()], - defaultProps: { - showLineNumbers: false, - }, - styleOverrides: { - codeFontSize: '12px', - }, + plugins: [pluginCollapsibleSections(), pluginLineNumbers()], + defaultProps: { + showLineNumbers: false, + }, + styleOverrides: { + codeFontSize: "12px", + }, }; diff --git a/apps/reactlit-docs/package.json b/apps/reactlit-docs/package.json index daae881..1fff107 100644 --- a/apps/reactlit-docs/package.json +++ b/apps/reactlit-docs/package.json @@ -1,30 +1,30 @@ { - "name": "reactlit-docs", - "type": "module", - "version": "0.0.16", - "private": true, - "scripts": { - "dev": "astro dev", - "start": "astro dev", - "build": "astro build", - "preview": "astro preview", - "astro": "astro" - }, - "dependencies": { - "@astrojs/react": "^4.0.0", - "@astrojs/starlight": "^0.29.2", - "@expressive-code/plugin-collapsible-sections": "^0.38.3", - "@expressive-code/plugin-line-numbers": "^0.38.3", - "@radix-ui/themes": "^3.1.6", - "@reactlit/core": "workspace:*", - "@reactlit/radix": "workspace:*", - "@tanstack/react-query": "^5.62.3", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "astro": "^4.16.10", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "sharp": "^0.32.5", - "starlight-package-managers": "^0.8.0" - } -} \ No newline at end of file + "name": "reactlit-docs", + "type": "module", + "version": "0.0.16", + "private": true, + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/react": "^4.0.0", + "@astrojs/starlight": "^0.29.2", + "@expressive-code/plugin-collapsible-sections": "^0.38.3", + "@expressive-code/plugin-line-numbers": "^0.38.3", + "@radix-ui/themes": "^3.1.6", + "@reactlit/core": "workspace:*", + "@reactlit/radix": "workspace:*", + "@tanstack/react-query": "^5.62.3", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.2", + "astro": "^4.16.10", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "sharp": "^0.32.5", + "starlight-package-managers": "^0.8.0" + } +} diff --git a/apps/reactlit-docs/src/content/config.ts b/apps/reactlit-docs/src/content/config.ts index 45f60b0..d8d945f 100644 --- a/apps/reactlit-docs/src/content/config.ts +++ b/apps/reactlit-docs/src/content/config.ts @@ -1,5 +1,5 @@ -import { defineCollection } from 'astro:content'; -import { docsSchema } from '@astrojs/starlight/schema'; +import { defineCollection } from "astro:content"; +import { docsSchema } from "@astrojs/starlight/schema"; export const collections = { docs: defineCollection({ schema: docsSchema() }), diff --git a/apps/reactlit-docs/src/examples/apps/contact-list-basic-async.tsx b/apps/reactlit-docs/src/examples/apps/contact-list-basic-async.tsx index 40c2af5..0d76536 100644 --- a/apps/reactlit-docs/src/examples/apps/contact-list-basic-async.tsx +++ b/apps/reactlit-docs/src/examples/apps/contact-list-basic-async.tsx @@ -1,56 +1,56 @@ -import { Button } from '@radix-ui/themes'; -import { type ReactlitContext } from '@reactlit/core'; -import { Inputs, Label } from '@reactlit/radix'; -import { TopRightLoader } from '../components/loader'; -import { ContactsMockService } from '../mocks/contacts'; +import { Button } from "@radix-ui/themes"; +import { type ReactlitContext } from "@reactlit/core"; +import { Inputs, Label } from "@reactlit/radix"; +import { TopRightLoader } from "../components/loader"; +import { ContactsMockService } from "../mocks/contacts"; // add a delay to the mock API const api = new ContactsMockService([], 500); export async function ContactListApp(app: ReactlitContext) { - // wrap a loader around the contacts fetch - app.display('loading-items', ); - const contacts = await api.getContacts(); - app.display('loading-items', undefined); - app.display( - - ); - const selectedContact = app.view( - 'selectedContact', - Inputs.Table(contacts, { - getRowId: (contact) => contact.id, - columns: ['name', 'email'], - }) - ); - if (!selectedContact) return; - app.display(

Selected Contact Details

); - if (app.changed('selectedContact')) { - app.set('name', selectedContact.name); - app.set('email', selectedContact.email); - } - const updates = { - name: app.view('name', Label('Name'), Inputs.Text()), - email: app.view('email', Label('Email'), Inputs.Text()), - }; - // if you wish, you can use an AsyncButton view for a button that - // has a loading state during async operations - app.view( - 'updating', - Inputs.AsyncButton( - async () => { - await api.updateContact(selectedContact.id, updates); - app.trigger(); - }, - { - content: 'Update', - } - ) - ); + // wrap a loader around the contacts fetch + app.display("loading-items", ); + const contacts = await api.getContacts(); + app.display("loading-items", undefined); + app.display( + , + ); + const selectedContact = app.view( + "selectedContact", + Inputs.Table(contacts, { + getRowId: (contact) => contact.id, + columns: ["name", "email"], + }), + ); + if (!selectedContact) return; + app.display(

Selected Contact Details

); + if (app.changed("selectedContact")) { + app.set("name", selectedContact.name); + app.set("email", selectedContact.email); + } + const updates = { + name: app.view("name", Label("Name"), Inputs.Text()), + email: app.view("email", Label("Email"), Inputs.Text()), + }; + // if you wish, you can use an AsyncButton view for a button that + // has a loading state during async operations + app.view( + "updating", + Inputs.AsyncButton( + async () => { + await api.updateContact(selectedContact.id, updates); + app.trigger(); + }, + { + content: "Update", + }, + ), + ); } diff --git a/apps/reactlit-docs/src/examples/apps/contact-list.tsx b/apps/reactlit-docs/src/examples/apps/contact-list.tsx index 3397c8d..344bc6c 100644 --- a/apps/reactlit-docs/src/examples/apps/contact-list.tsx +++ b/apps/reactlit-docs/src/examples/apps/contact-list.tsx @@ -1,46 +1,46 @@ -import { Button } from '@radix-ui/themes'; -import { type ReactlitContext } from '@reactlit/core'; -import { Inputs, Label } from '@reactlit/radix'; -import { ContactMockApi as api } from '../mocks/contacts'; +import { Button } from "@radix-ui/themes"; +import { type ReactlitContext } from "@reactlit/core"; +import { Inputs, Label } from "@reactlit/radix"; +import { ContactMockApi as api } from "../mocks/contacts"; export async function ContactListApp(app: ReactlitContext) { - const contacts = await api.getContacts(); - app.display( - - ); - const selectedContact = app.view( - 'selectedContact', - Inputs.Table(contacts, { - getRowId: (contact) => contact.id, - columns: ['name', 'email'], - }) - ); - if (!selectedContact) return; - app.display(

Selected Contact Details

); - if (app.changed('selectedContact')) { - app.set('name', selectedContact.name); - app.set('email', selectedContact.email); - } - // the built-in FormView allows you to group inputs together - const updates = { - name: app.view('name', Label('Name'), Inputs.Text()), - email: app.view('email', Label('Email'), Inputs.Text()), - }; - app.display( - - ); + const contacts = await api.getContacts(); + app.display( + , + ); + const selectedContact = app.view( + "selectedContact", + Inputs.Table(contacts, { + getRowId: (contact) => contact.id, + columns: ["name", "email"], + }), + ); + if (!selectedContact) return; + app.display(

Selected Contact Details

); + if (app.changed("selectedContact")) { + app.set("name", selectedContact.name); + app.set("email", selectedContact.email); + } + // the built-in FormView allows you to group inputs together + const updates = { + name: app.view("name", Label("Name"), Inputs.Text()), + email: app.view("email", Label("Email"), Inputs.Text()), + }; + app.display( + , + ); } diff --git a/apps/reactlit-docs/src/examples/components/loader.tsx b/apps/reactlit-docs/src/examples/components/loader.tsx index 3a76e93..e8dfa95 100644 --- a/apps/reactlit-docs/src/examples/components/loader.tsx +++ b/apps/reactlit-docs/src/examples/components/loader.tsx @@ -1,18 +1,18 @@ -import { Spinner, Badge } from '@radix-ui/themes'; +import { Badge, Spinner } from "@radix-ui/themes"; export function TopRightLoader({ text }: { text: string }) { - return ( - - {text} - - ); + return ( + + {text} + + ); } diff --git a/apps/reactlit-docs/src/examples/contact-list-basic-async.tsx b/apps/reactlit-docs/src/examples/contact-list-basic-async.tsx index 2a96106..5b0cfa1 100644 --- a/apps/reactlit-docs/src/examples/contact-list-basic-async.tsx +++ b/apps/reactlit-docs/src/examples/contact-list-basic-async.tsx @@ -1,6 +1,6 @@ -import { ContactListApp } from './apps/contact-list-basic-async'; -import RenderRadixApp from './render-radix-app'; +import { ContactListApp } from "./apps/contact-list-basic-async"; +import RenderRadixApp from "./render-radix-app"; export default function ContactList() { - return ; + return ; } diff --git a/apps/reactlit-docs/src/examples/contact-list-data-fetch.tsx b/apps/reactlit-docs/src/examples/contact-list-data-fetch.tsx index 41d9164..bddfab4 100644 --- a/apps/reactlit-docs/src/examples/contact-list-data-fetch.tsx +++ b/apps/reactlit-docs/src/examples/contact-list-data-fetch.tsx @@ -1,108 +1,108 @@ -import { Button, Theme } from '@radix-ui/themes'; -import '@radix-ui/themes/styles.css'; -import { DataFetchingPlugin, useReactlit } from '@reactlit/core'; -import { Inputs, Label } from '@reactlit/radix'; -import { TopRightLoader } from './components/loader'; -import { ContactsMockService } from './mocks/contacts'; +import { Button, Theme } from "@radix-ui/themes"; +import "@radix-ui/themes/styles.css"; +import { DataFetchingPlugin, useReactlit } from "@reactlit/core"; +import { Inputs, Label } from "@reactlit/radix"; +import { TopRightLoader } from "./components/loader"; +import { ContactsMockService } from "./mocks/contacts"; export default function ContactList() { - return ( - - - - ); + return ( + + + + ); } // slow down the mock API to demo user experience with a slow API const api = new ContactsMockService([], 1000); const ContactListApp = () => { - const Reactlit = useReactlit(DataFetchingPlugin); - return ( - - {async (app) => { - // - // create a fetcher for the contacts with a cache key of ['contacts'] - const contactsFetcher = app.fetcher(['contacts'], () => - api.getContacts() - ); + const Reactlit = useReactlit(DataFetchingPlugin); + return ( + + {async (app) => { + // + // create a fetcher for the contacts with a cache key of ['contacts'] + const contactsFetcher = app.fetcher(["contacts"], () => + api.getContacts(), + ); - if (contactsFetcher.isFetching()) { - app.display('loader', ); - } + if (contactsFetcher.isFetching()) { + app.display("loader", ); + } - // get the current contacts synchronously, back off to empty list if null - const contacts = contactsFetcher.get() ?? []; + // get the current contacts synchronously, back off to empty list if null + const contacts = contactsFetcher.get() ?? []; - app.display( - - ); - const selectedContact = app.view( - 'selectedContact', - Inputs.Table(contacts, { - getRowId: (contact) => contact.id, - columns: ['name', 'email'], - }) - ); - if (!selectedContact) return; - app.display( -

Selected Contact Details

- ); - if (app.changed('selectedContact')) { - app.set('name', selectedContact.name); - app.set('email', selectedContact.email); - } - const updates = { - name: app.view('name', Label('Name'), Inputs.Text()), - email: app.view('email', Label('Email'), Inputs.Text()), - }; - app.display( - - ); - }} -
- ); + app.display( + , + ); + const selectedContact = app.view( + "selectedContact", + Inputs.Table(contacts, { + getRowId: (contact) => contact.id, + columns: ["name", "email"], + }), + ); + if (!selectedContact) return; + app.display( +

Selected Contact Details

, + ); + if (app.changed("selectedContact")) { + app.set("name", selectedContact.name); + app.set("email", selectedContact.email); + } + const updates = { + name: app.view("name", Label("Name"), Inputs.Text()), + email: app.view("email", Label("Email"), Inputs.Text()), + }; + app.display( + , + ); + }} +
+ ); }; diff --git a/apps/reactlit-docs/src/examples/contact-list-react.tsx b/apps/reactlit-docs/src/examples/contact-list-react.tsx index 4761119..46a6ccc 100644 --- a/apps/reactlit-docs/src/examples/contact-list-react.tsx +++ b/apps/reactlit-docs/src/examples/contact-list-react.tsx @@ -1,126 +1,127 @@ -import { Button, Theme } from '@radix-ui/themes'; -import '@radix-ui/themes/styles.css'; +import { Button, Theme } from "@radix-ui/themes"; +import "@radix-ui/themes/styles.css"; import { - SingleTableInputViewComponent, - TextInputComponent, -} from '@reactlit/radix'; + SingleTableInputViewComponent, + TextInputComponent, +} from "@reactlit/radix"; import { - QueryClient, - QueryClientProvider, - useMutation, - useQuery, - useQueryClient, -} from '@tanstack/react-query'; -import { useEffect, useMemo, useState } from 'react'; -import { TopRightLoader } from './components/loader'; -import { ContactsMockService, type Contact } from './mocks/contacts'; + QueryClient, + QueryClientProvider, + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import { useEffect, useMemo, useState } from "react"; +import { TopRightLoader } from "./components/loader"; +import { type Contact, ContactsMockService } from "./mocks/contacts"; const api = new ContactsMockService([], 1000); const queryClient = new QueryClient(); export default function ContactList() { - return ( - - - - - - ); + return ( + + + + + + ); } function ContactListApp() { - const [name, setName] = useState(''); - const [email, setEmail] = useState(''); - const client = useQueryClient(); - const { data: contacts, isFetching } = useQuery({ - queryKey: ['contacts'], - queryFn: () => api.getContacts(), - }); - const { mutateAsync: addContact, isPending: addingContact } = useMutation({ - mutationFn: async () => await api.addContact(), - onSuccess: async (newContact) => { - await client.invalidateQueries({ queryKey: ['contacts'] }); - setSelectedContactId(newContact.id); - }, - }); - const { mutateAsync: updateContact, isPending: updatingContact } = - useMutation({ - mutationFn: (contact: Contact) => api.updateContact(contact.id, contact), - onSuccess: (data) => { - client.setQueryData(['contacts'], (contacts: Contact[]) => - contacts.map((c) => (c.id === data.id ? data : c)) - ); - }, - }); - const [selectedContactId, setSelectedContactId] = useState< - string | undefined - >(); - const selectedContact = useMemo( - () => contacts?.find((c) => c.id === selectedContactId), - [contacts, selectedContactId] - ); - useEffect(() => { - if (selectedContact) { - setName(selectedContact.name); - setEmail(selectedContact.email); - } - }, [selectedContact, setName, setEmail]); - return ( -
- {isFetching && } - {!isFetching && addingContact && ( - - )} - {!isFetching && updatingContact && ( - - )} - - setSelectedContactId(id)} - getRowId={(c) => c.id} - display={() => {}} - view={() => undefined as any} - /> - {selectedContact && ( - <> -

Selected Contact Details

- {}} - view={() => undefined as any} - /> - {}} - view={() => undefined as any} - /> - - - )} -
- ); + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const client = useQueryClient(); + const { data: contacts, isFetching } = useQuery({ + queryKey: ["contacts"], + queryFn: () => api.getContacts(), + }); + const { mutateAsync: addContact, isPending: addingContact } = useMutation({ + mutationFn: async () => await api.addContact(), + onSuccess: async (newContact) => { + await client.invalidateQueries({ queryKey: ["contacts"] }); + setSelectedContactId(newContact.id); + }, + }); + const { mutateAsync: updateContact, isPending: updatingContact } = + useMutation({ + mutationFn: (contact: Contact) => api.updateContact(contact.id, contact), + onSuccess: (data) => { + client.setQueryData(["contacts"], (contacts: Contact[]) => + contacts.map((c) => (c.id === data.id ? data : c)), + ); + }, + }); + const [selectedContactId, setSelectedContactId] = useState< + string | undefined + >(); + const selectedContact = useMemo( + () => contacts?.find((c) => c.id === selectedContactId), + [contacts, selectedContactId], + ); + useEffect(() => { + if (selectedContact) { + setName(selectedContact.name); + setEmail(selectedContact.email); + } + }, [selectedContact]); + return ( +
+ {isFetching && } + {!isFetching && addingContact && ( + + )} + {!isFetching && updatingContact && ( + + )} + + setSelectedContactId(id)} + getRowId={(c) => c.id} + display={() => {}} + // biome-ignore lint/suspicious/noExplicitAny: + view={() => undefined as any} + /> + {selectedContact && ( + <> +

Selected Contact Details

+ {}} + // biome-ignore lint/suspicious/noExplicitAny: + view={() => undefined as any} + /> + {}} + // biome-ignore lint/suspicious/noExplicitAny: + view={() => undefined as any} + /> + + + )} +
+ ); } diff --git a/apps/reactlit-docs/src/examples/contact-list.tsx b/apps/reactlit-docs/src/examples/contact-list.tsx index 6e66def..f87c629 100644 --- a/apps/reactlit-docs/src/examples/contact-list.tsx +++ b/apps/reactlit-docs/src/examples/contact-list.tsx @@ -1,12 +1,12 @@ -import { Theme } from '@radix-ui/themes'; -import { Reactlit } from '@reactlit/core'; -import '@radix-ui/themes/styles.css'; -import { ContactListApp } from './apps/contact-list'; +import { Theme } from "@radix-ui/themes"; +import { Reactlit } from "@reactlit/core"; +import "@radix-ui/themes/styles.css"; +import { ContactListApp } from "./apps/contact-list"; export default function ContactList() { - return ( - - {ContactListApp} - - ); + return ( + + {ContactListApp} + + ); } diff --git a/apps/reactlit-docs/src/examples/hello-world-react.tsx b/apps/reactlit-docs/src/examples/hello-world-react.tsx index f04a773..21f07f9 100644 --- a/apps/reactlit-docs/src/examples/hello-world-react.tsx +++ b/apps/reactlit-docs/src/examples/hello-world-react.tsx @@ -1,19 +1,19 @@ -import { useState } from 'react'; +import { useState } from "react"; export default function HelloWorldReact() { - const [name, setName] = useState(''); - let display =
Hello {name}
; - if (!name) { - display =
Name is required
; - } - return ( -
- setName(e.target.value)} - placeholder="Enter name" - /> - {display} -
- ); + const [name, setName] = useState(""); + let display =
Hello {name}
; + if (!name) { + display =
Name is required
; + } + return ( +
+ setName(e.target.value)} + placeholder="Enter name" + /> + {display} +
+ ); } diff --git a/apps/reactlit-docs/src/examples/hello-world.tsx b/apps/reactlit-docs/src/examples/hello-world.tsx index 99038d0..2337ad1 100644 --- a/apps/reactlit-docs/src/examples/hello-world.tsx +++ b/apps/reactlit-docs/src/examples/hello-world.tsx @@ -1,16 +1,16 @@ -import { Reactlit } from '@reactlit/core'; -import { TextInput } from './inputs/basic-text-input'; +import { Reactlit } from "@reactlit/core"; +import { TextInput } from "./inputs/basic-text-input"; export default function HelloWorld() { - return ( - - {async ({ display, view }) => { - const name = view('name', TextInput); - if (!name) { - throw new Error('Name is required'); - } - display(
Hello {name}
); - }} -
- ); + return ( + + {async ({ display, view }) => { + const name = view("name", TextInput); + if (!name) { + throw new Error("Name is required"); + } + display(
Hello {name}
); + }} +
+ ); } diff --git a/apps/reactlit-docs/src/examples/inputs/basic-text-input.tsx b/apps/reactlit-docs/src/examples/inputs/basic-text-input.tsx index 88e885b..973af99 100644 --- a/apps/reactlit-docs/src/examples/inputs/basic-text-input.tsx +++ b/apps/reactlit-docs/src/examples/inputs/basic-text-input.tsx @@ -1,12 +1,12 @@ -import { defineView } from '@reactlit/core'; +import { defineView } from "@reactlit/core"; export const TextInput = defineView(({ value, setValue, stateKey }) => ( - setValue(e.target.value)} - placeholder={`Enter ${stateKey}`} - /> + setValue(e.target.value)} + placeholder={`Enter ${stateKey}`} + /> )); diff --git a/apps/reactlit-docs/src/examples/layout-example.tsx b/apps/reactlit-docs/src/examples/layout-example.tsx index 69989ae..a7cb504 100644 --- a/apps/reactlit-docs/src/examples/layout-example.tsx +++ b/apps/reactlit-docs/src/examples/layout-example.tsx @@ -1,39 +1,39 @@ -import { LayoutView, useReactlit } from '@reactlit/core'; -import { TextInput } from './inputs/basic-text-input'; +import { LayoutView, useReactlit } from "@reactlit/core"; +import { TextInput } from "./inputs/basic-text-input"; export default function LayoutExample() { - const Reactlit = useReactlit(); - return ( - - {async ({ view }) => { - const [col1, col2, col3] = view( - 'cols', -
, - // the second argument here wraps each slot in a div so that they show - // up as a single grid column in the layout - LayoutView(3,
) - ); + const Reactlit = useReactlit(); + return ( + + {async ({ view }) => { + const [col1, col2, col3] = view( + "cols", +
, + // the second argument here wraps each slot in a div so that they show + // up as a single grid column in the layout + LayoutView(3,
), + ); - col1.display('First Name'); - const first = col1.view('first', TextInput); + col1.display("First Name"); + const first = col1.view("first", TextInput); - col2.display('Last Name'); - const last = col2.view('last', TextInput); + col2.display("Last Name"); + const last = col2.view("last", TextInput); - col3.display('Hello to'); - col3.display( -
- {first} {last} -
- ); - }} - - ); + col3.display("Hello to"); + col3.display( +
+ {first} {last} +
, + ); + }} + + ); } diff --git a/apps/reactlit-docs/src/examples/layouts/three-column-layout.tsx b/apps/reactlit-docs/src/examples/layouts/three-column-layout.tsx index f890401..b35dc4e 100644 --- a/apps/reactlit-docs/src/examples/layouts/three-column-layout.tsx +++ b/apps/reactlit-docs/src/examples/layouts/three-column-layout.tsx @@ -1,25 +1,25 @@ -import { defineLayout } from '@reactlit/core'; +import { defineLayout } from "@reactlit/core"; export const ThreeColumnLayout = defineLayout( - 3, - ({ slots: [Slot1, Slot2, Slot3] }) => ( -
-
- -
-
- -
-
- -
-
- ) + 3, + ({ slots: [Slot1, Slot2, Slot3] }) => ( +
+
+ +
+
+ +
+
+ +
+
+ ), ); diff --git a/apps/reactlit-docs/src/examples/mocks/contacts.ts b/apps/reactlit-docs/src/examples/mocks/contacts.ts index f122a06..3685465 100644 --- a/apps/reactlit-docs/src/examples/mocks/contacts.ts +++ b/apps/reactlit-docs/src/examples/mocks/contacts.ts @@ -1,50 +1,50 @@ // This is mocking a backend API for demo purposes -import { wait } from '../utils/wait'; +import { wait } from "../utils/wait"; export type Contact = { - id: string; - name: string; - email: string; + id: string; + name: string; + email: string; }; // we add a delay to these to simulate a network request export class ContactsMockService { - constructor( - private contacts: Contact[], - private readonly delay: number = 0 - ) {} + constructor( + private contacts: Contact[], + private readonly delay: number = 0, + ) {} - async getContacts() { - await wait(this.delay); - return this.contacts; - } + async getContacts() { + await wait(this.delay); + return this.contacts; + } - async addContact(contact?: Partial>) { - await wait(this.delay); - const newContact = { - name: `New Contact ${this.contacts.length + 1}`, - email: `contact${this.contacts.length + 1}@example.com`, - ...(contact ?? {}), - id: `contact-${this.contacts.length + 1}`, - }; - this.contacts = [...this.contacts, newContact]; - return newContact; - } + async addContact(contact?: Partial>) { + await wait(this.delay); + const newContact = { + name: `New Contact ${this.contacts.length + 1}`, + email: `contact${this.contacts.length + 1}@example.com`, + ...(contact ?? {}), + id: `contact-${this.contacts.length + 1}`, + }; + this.contacts = [...this.contacts, newContact]; + return newContact; + } - async updateContact(id: string, contact: Partial) { - await wait(this.delay); - const index = this.contacts.findIndex((c) => c.id === id); - if (index === -1) { - throw new Error('Contact not found'); - } - this.contacts = [ - ...this.contacts.slice(0, index), - { ...this.contacts[index], ...contact }, - ...this.contacts.slice(index + 1), - ]; - return this.contacts[index]; - } + async updateContact(id: string, contact: Partial) { + await wait(this.delay); + const index = this.contacts.findIndex((c) => c.id === id); + if (index === -1) { + throw new Error("Contact not found"); + } + this.contacts = [ + ...this.contacts.slice(0, index), + { ...this.contacts[index], ...contact }, + ...this.contacts.slice(index + 1), + ]; + return this.contacts[index]; + } } export const ContactMockApi = new ContactsMockService([], 0); diff --git a/apps/reactlit-docs/src/examples/render-radix-app.tsx b/apps/reactlit-docs/src/examples/render-radix-app.tsx index 1359620..eae1c8c 100644 --- a/apps/reactlit-docs/src/examples/render-radix-app.tsx +++ b/apps/reactlit-docs/src/examples/render-radix-app.tsx @@ -1,13 +1,13 @@ -import { Theme } from '@radix-ui/themes'; -import '@radix-ui/themes/styles.css'; -import { Reactlit, type ReactlitFunction } from '@reactlit/core'; +import { Theme } from "@radix-ui/themes"; +import "@radix-ui/themes/styles.css"; +import { Reactlit, type ReactlitFunction } from "@reactlit/core"; export default function RenderRadixApp({ app }: { app: ReactlitFunction }) { - return ( -
- - {app} - -
- ); + return ( +
+ + {app} + +
+ ); } diff --git a/apps/reactlit-docs/src/examples/transform-view.tsx b/apps/reactlit-docs/src/examples/transform-view.tsx index 498eeae..4981df7 100644 --- a/apps/reactlit-docs/src/examples/transform-view.tsx +++ b/apps/reactlit-docs/src/examples/transform-view.tsx @@ -1,36 +1,36 @@ -import { Reactlit } from '@reactlit/core'; -import { defineTransformView } from '@reactlit/core'; +import { Reactlit } from "@reactlit/core"; +import { defineTransformView } from "@reactlit/core"; export const FilterInput = (values: string[]) => - defineTransformView( - ({ value, setValue, stateKey }) => ( - setValue(e.target.value)} - placeholder={`Search`} - /> - ), - (props) => - values.filter( - (value) => - !props.value || - value.toLowerCase().includes(props.value.toLowerCase()) - ) - ); + defineTransformView( + ({ value, setValue, stateKey }) => ( + setValue(e.target.value)} + placeholder="Search" + /> + ), + (props) => + values.filter( + (value) => + !props.value || + value.toLowerCase().includes(props.value.toLowerCase()), + ), + ); export default function HelloWorld() { - return ( - - {async ({ display, view }) => { - const users = ['Michael', 'Mary', 'John', 'Jane', 'Jim', 'Jill']; - display('Search for a user: '); - const filteredUsers = view('filteredUsers', FilterInput(users)); - if (filteredUsers.length === 0) { - throw new Error('No matching users found'); - } - display(
You are filtering to {filteredUsers.join(', ')}
); - }} -
- ); + return ( + + {async ({ display, view }) => { + const users = ["Michael", "Mary", "John", "Jane", "Jim", "Jill"]; + display("Search for a user: "); + const filteredUsers = view("filteredUsers", FilterInput(users)); + if (filteredUsers.length === 0) { + throw new Error("No matching users found"); + } + display(
You are filtering to {filteredUsers.join(", ")}
); + }} +
+ ); } diff --git a/apps/reactlit-docs/src/examples/utils/wait.ts b/apps/reactlit-docs/src/examples/utils/wait.ts index cbd907e..76d5de3 100644 --- a/apps/reactlit-docs/src/examples/utils/wait.ts +++ b/apps/reactlit-docs/src/examples/utils/wait.ts @@ -1,3 +1,3 @@ export async function wait(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/apps/reactlit-docs/tsconfig.json b/apps/reactlit-docs/tsconfig.json index 032ad64..40dde9b 100644 --- a/apps/reactlit-docs/tsconfig.json +++ b/apps/reactlit-docs/tsconfig.json @@ -1,7 +1,7 @@ { - "extends": "astro/tsconfigs/strict", - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "react" - } -} \ No newline at end of file + "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "react" + } +} diff --git a/apps/reactlit-examples/.eslintrc.json b/apps/reactlit-examples/.eslintrc.json index 3722418..a885bd8 100644 --- a/apps/reactlit-examples/.eslintrc.json +++ b/apps/reactlit-examples/.eslintrc.json @@ -1,3 +1,3 @@ { - "extends": ["next/core-web-vitals", "next/typescript"] + "extends": ["next/core-web-vitals", "next/typescript"] } diff --git a/apps/reactlit-examples/next.config.ts b/apps/reactlit-examples/next.config.ts index 9487e69..6439ae3 100644 --- a/apps/reactlit-examples/next.config.ts +++ b/apps/reactlit-examples/next.config.ts @@ -1,9 +1,9 @@ -import type { NextConfig } from 'next'; +import type { NextConfig } from "next"; const nextConfig: NextConfig = { - output: 'export', - transpilePackages: ['@reactlit/core', '@reactlit/vanilla'], - reactStrictMode: true, + output: "export", + transpilePackages: ["@reactlit/core", "@reactlit/vanilla"], + reactStrictMode: true, }; export default nextConfig; diff --git a/apps/reactlit-examples/package.json b/apps/reactlit-examples/package.json index ac4c75a..0ebc097 100644 --- a/apps/reactlit-examples/package.json +++ b/apps/reactlit-examples/package.json @@ -1,37 +1,37 @@ { - "name": "reactlit-examples", - "version": "0.0.16", - "private": true, - "scripts": { - "dev": "next dev --turbopack", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@radix-ui/themes": "^3.1.6", - "@reactlit/core": "workspace:*", - "@reactlit/radix": "workspace:*", - "@reactlit/vanilla": "workspace:*", - "@tanstack/react-query": "^5.62.3", - "clsx": "^2.1.1", - "lucide-react": "^0.468.0", - "next": "15.1.0", - "next-themes": "^0.4.4", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "tailwind-merge": "^2.5.5", - "tunnel-rat": "^0.1.2" - }, - "devDependencies": { - "@eslint/eslintrc": "^3", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "eslint": "^9", - "eslint-config-next": "15.1.0", - "postcss": "^8", - "tailwindcss": "^3.4.1", - "typescript": "^5" - } -} \ No newline at end of file + "name": "reactlit-examples", + "version": "0.0.16", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@radix-ui/themes": "^3.1.6", + "@reactlit/core": "workspace:*", + "@reactlit/radix": "workspace:*", + "@reactlit/vanilla": "workspace:*", + "@tanstack/react-query": "^5.62.3", + "clsx": "^2.1.1", + "lucide-react": "^0.468.0", + "next": "15.1.0", + "next-themes": "^0.4.4", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwind-merge": "^2.5.5", + "tunnel-rat": "^0.1.2" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@types/node": "^22.10.10", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.2", + "eslint": "^9", + "eslint-config-next": "15.1.0", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "typescript": "*" + } +} diff --git a/apps/reactlit-examples/postcss.config.mjs b/apps/reactlit-examples/postcss.config.mjs index 1a69fd2..f6c3605 100644 --- a/apps/reactlit-examples/postcss.config.mjs +++ b/apps/reactlit-examples/postcss.config.mjs @@ -1,8 +1,8 @@ /** @type {import('postcss-load-config').Config} */ const config = { - plugins: { - tailwindcss: {}, - }, + plugins: { + tailwindcss: {}, + }, }; export default config; diff --git a/apps/reactlit-examples/src/components/debug-toggle.tsx b/apps/reactlit-examples/src/components/debug-toggle.tsx index a59017a..35a1b58 100644 --- a/apps/reactlit-examples/src/components/debug-toggle.tsx +++ b/apps/reactlit-examples/src/components/debug-toggle.tsx @@ -1,51 +1,51 @@ -'use client'; -import { Button, Tooltip } from '@radix-ui/themes'; -import { Wrapper } from '@reactlit/core'; -import { Bug, BugOff } from 'lucide-react'; -import { createContext, useContext, useState } from 'react'; +"use client"; +import { Button, Tooltip } from "@radix-ui/themes"; +import { Wrapper } from "@reactlit/core"; +import { Bug, BugOff } from "lucide-react"; +import { createContext, useContext, useState } from "react"; type DebugContextType = { - debug: boolean; - setDebug: (debug: boolean) => void; + debug: boolean; + setDebug: (debug: boolean) => void; }; const DebugContext = createContext({ - debug: false, - setDebug: () => {}, + debug: false, + setDebug: () => {}, }); export const DebugToggle = () => { - const { debug, setDebug } = useContext(DebugContext); - return ( - - - - ); + const { debug, setDebug } = useContext(DebugContext); + return ( + + + + ); }; export const DebugProvider = ({ children }: { children: React.ReactNode }) => { - const [debug, setDebug] = useState(false); - return ( - - {children} - - ); + const [debug, setDebug] = useState(false); + return ( + + {children} + + ); }; export function useDebug() { - const { debug } = useContext(DebugContext); - return debug; + const { debug } = useContext(DebugContext); + return debug; } export const Debug: Wrapper = ({ children, stateKey }) => { - const debug = useDebug(); - if (!debug) return children; - return ( -
-
{stateKey}
-
{children}
-
- ); + const debug = useDebug(); + if (!debug) return children; + return ( +
+
{stateKey}
+
{children}
+
+ ); }; diff --git a/apps/reactlit-examples/src/components/main.tsx b/apps/reactlit-examples/src/components/main.tsx index bcd6cb0..9eda187 100644 --- a/apps/reactlit-examples/src/components/main.tsx +++ b/apps/reactlit-examples/src/components/main.tsx @@ -1,78 +1,78 @@ -import { Box, Container, Flex, Text } from '@radix-ui/themes'; -import { Geist, Geist_Mono } from 'next/font/google'; -import Head from 'next/head'; -import React from 'react'; -import { DebugToggle } from './debug-toggle'; -import { Menu } from './menu'; -import { ThemeToggle } from './theme-toggle'; +import { Box, Container, Flex, Text } from "@radix-ui/themes"; +import { Geist, Geist_Mono } from "next/font/google"; +import Head from "next/head"; +import React from "react"; +import { DebugToggle } from "./debug-toggle"; +import { Menu } from "./menu"; +import { ThemeToggle } from "./theme-toggle"; const geistSans = Geist({ - variable: '--font-geist-sans', - subsets: ['latin'], + variable: "--font-geist-sans", + subsets: ["latin"], }); const geistMono = Geist_Mono({ - variable: '--font-geist-mono', - subsets: ['latin'], + variable: "--font-geist-mono", + subsets: ["latin"], }); export function Main({ - title, - children, + title, + children, }: { - title: string; - children: React.ReactNode; + title: string; + children: React.ReactNode; }) { - return ( -
-
- - {title} - - - - - - {title} - - - - - - - - - - - - - {children} - - - - - - -
-
- ); + return ( +
+
+ + {title} + + + + + + {title} + + + + + + + + + + + + + {children} + + + + + + +
+
+ ); } diff --git a/apps/reactlit-examples/src/components/menu.tsx b/apps/reactlit-examples/src/components/menu.tsx index 682d58c..88f56f3 100644 --- a/apps/reactlit-examples/src/components/menu.tsx +++ b/apps/reactlit-examples/src/components/menu.tsx @@ -1,29 +1,29 @@ -import { Box, Flex, Link } from '@radix-ui/themes'; -import NextLink from 'next/link'; -import { usePathname } from 'next/navigation'; -import { ReactNode } from 'react'; +import { Box, Flex, Link } from "@radix-ui/themes"; +import NextLink from "next/link"; +import { usePathname } from "next/navigation"; +import { ReactNode } from "react"; function MenuItem({ href, children }: { href: string; children: ReactNode }) { - const pathname = usePathname(); - const isActive = pathname === href; - return ( - - - {children} - - - ); + const pathname = usePathname(); + const isActive = pathname === href; + return ( + + + {children} + + + ); } export function Menu() { - return ( - - Hello World - Hello World Vanilla - Radix Inputs - Todo List - Starter - Layout Test - - ); + return ( + + Hello World + Hello World Vanilla + Radix Inputs + Todo List + Starter + Layout Test + + ); } diff --git a/apps/reactlit-examples/src/components/theme-toggle.tsx b/apps/reactlit-examples/src/components/theme-toggle.tsx index 4a09854..089ec39 100644 --- a/apps/reactlit-examples/src/components/theme-toggle.tsx +++ b/apps/reactlit-examples/src/components/theme-toggle.tsx @@ -1,30 +1,30 @@ -'use client'; -import { useEffect, useState } from 'react'; -import { Button, Tooltip } from '@radix-ui/themes'; -import { MoonIcon, SunIcon } from 'lucide-react'; -import { useTheme } from 'next-themes'; +"use client"; +import { Button, Tooltip } from "@radix-ui/themes"; +import { MoonIcon, SunIcon } from "lucide-react"; +import { useTheme } from "next-themes"; +import { useEffect, useState } from "react"; export function ThemeToggle() { - const { resolvedTheme, setTheme } = useTheme(); - const [mounted, setMounted] = useState(false); + const { resolvedTheme, setTheme } = useTheme(); + const [mounted, setMounted] = useState(false); - useEffect(() => { - setMounted(true); - }, []); + useEffect(() => { + setMounted(true); + }, []); - if (!mounted) { - return null; - } + if (!mounted) { + return null; + } - return ( - - - - ); + return ( + + + + ); } diff --git a/apps/reactlit-examples/src/mocks/todos.ts b/apps/reactlit-examples/src/mocks/todos.ts index 28d29b6..9a0133f 100644 --- a/apps/reactlit-examples/src/mocks/todos.ts +++ b/apps/reactlit-examples/src/mocks/todos.ts @@ -1,49 +1,52 @@ // This is mocking a backend API for demo purposes -import { wait } from '../utils/wait'; +import { wait } from "../utils/wait"; export type Todo = { - id: string; - task: string; - completed: boolean; + id: string; + task: string; + completed: boolean; }; // we add a delay to these to simulate a network request export class TodoService { - constructor(private todos: Todo[], private readonly delay: number = 0) {} + constructor( + private todos: Todo[], + private readonly delay: number = 0, + ) {} - async getTodos(fail?: boolean) { - console.log('getTodos'); - await wait(this.delay); - if (fail) { - throw new Error('purposefully failed'); - } - return this.todos; - } + async getTodos(fail?: boolean) { + console.log("getTodos"); + await wait(this.delay); + if (fail) { + throw new Error("purposefully failed"); + } + return this.todos; + } - async addTodo(todo?: Partial>) { - await wait(this.delay); - const newTodo = { - task: `New Todo ${this.todos.length + 1}`, - completed: false, - ...(todo ?? {}), - id: `todo-${this.todos.length + 1}`, - }; - this.todos = [...this.todos, newTodo]; - return newTodo; - } + async addTodo(todo?: Partial>) { + await wait(this.delay); + const newTodo = { + task: `New Todo ${this.todos.length + 1}`, + completed: false, + ...(todo ?? {}), + id: `todo-${this.todos.length + 1}`, + }; + this.todos = [...this.todos, newTodo]; + return newTodo; + } - async updateTodo(id: string, todo: Partial) { - await wait(this.delay); - const index = this.todos.findIndex((t) => t.id === id); - if (index === -1) { - throw new Error(`Todo not found: ${id}`); - } - this.todos = [ - ...this.todos.slice(0, index), - { ...this.todos[index], ...todo }, - ...this.todos.slice(index + 1), - ]; - return this.todos[index]; - } + async updateTodo(id: string, todo: Partial) { + await wait(this.delay); + const index = this.todos.findIndex((t) => t.id === id); + if (index === -1) { + throw new Error(`Todo not found: ${id}`); + } + this.todos = [ + ...this.todos.slice(0, index), + { ...this.todos[index], ...todo }, + ...this.todos.slice(index + 1), + ]; + return this.todos[index]; + } } diff --git a/apps/reactlit-examples/src/pages/_app.tsx b/apps/reactlit-examples/src/pages/_app.tsx index 7aa93eb..3cbf9f5 100644 --- a/apps/reactlit-examples/src/pages/_app.tsx +++ b/apps/reactlit-examples/src/pages/_app.tsx @@ -1,21 +1,21 @@ -import '@/styles/globals.css'; -import '@radix-ui/themes/styles.css'; -import { Theme } from '@radix-ui/themes'; -import { ThemeProvider } from 'next-themes'; -import type { AppProps } from 'next/app'; -import { Main } from '@/components/main'; -import { DebugProvider } from '@/components/debug-toggle'; +import "@/styles/globals.css"; +import "@radix-ui/themes/styles.css"; +import { DebugProvider } from "@/components/debug-toggle"; +import { Main } from "@/components/main"; +import { Theme } from "@radix-ui/themes"; +import { ThemeProvider } from "next-themes"; +import type { AppProps } from "next/app"; export default function App({ Component, pageProps }: AppProps) { - return ( - - - -
- -
-
-
-
- ); + return ( + + + +
+ +
+
+
+
+ ); } diff --git a/apps/reactlit-examples/src/pages/_document.tsx b/apps/reactlit-examples/src/pages/_document.tsx index 89a710b..7525c32 100644 --- a/apps/reactlit-examples/src/pages/_document.tsx +++ b/apps/reactlit-examples/src/pages/_document.tsx @@ -1,15 +1,15 @@ -import { Head, Html, Main, NextScript } from 'next/document'; +import { Head, Html, Main, NextScript } from "next/document"; export default function Document() { - return ( - - - - - -
- - - - ); + return ( + + + + + +
+ + + + ); } diff --git a/apps/reactlit-examples/src/pages/hello-world-vanilla/index.tsx b/apps/reactlit-examples/src/pages/hello-world-vanilla/index.tsx index 180bcfb..8dbe726 100644 --- a/apps/reactlit-examples/src/pages/hello-world-vanilla/index.tsx +++ b/apps/reactlit-examples/src/pages/hello-world-vanilla/index.tsx @@ -1,130 +1,130 @@ -import { Reactlit, useReactlitState } from '@reactlit/core'; -import { Inputs, Label } from '@reactlit/vanilla'; +import { Reactlit, useReactlitState } from "@reactlit/core"; +import { Inputs, Label } from "@reactlit/vanilla"; -const LabelProps = { className: 'flex items-center gap-2 mb-2' }; +const LabelProps = { className: "flex items-center gap-2 mb-2" }; export default function HelloWorldVanilla() { - const [appState, setAppState] = useReactlitState({ - name: '', - pickedNumbers: [], - pickedColors: [], - chosenNumber: '', - chosenColor: '', - }); + const [appState, setAppState] = useReactlitState({ + name: "", + pickedNumbers: [], + pickedColors: [], + chosenNumber: "", + chosenColor: "", + }); - return ( - - {async ({ display, view }) => { - display(
Hello World Vanilla
); + return ( + + {async ({ display, view }) => { + display(
Hello World Vanilla
); - const name = view( - 'name', - Label('Name', LabelProps), - Inputs.Text({ - id: 'name', - className: 'border p-0.5', - placeholder: 'Enter your name', - }) - ); + const name = view( + "name", + Label("Name", LabelProps), + Inputs.Text({ + id: "name", + className: "border p-0.5", + placeholder: "Enter your name", + }), + ); - display(
Hello {name}!
); + display(
Hello {name}!
); - const picked = view( - 'pickedNumbers', - Label('Pick any number', LabelProps), - Inputs.Check(['One', 'Two', 'Three'], { - className: { - wrapper: 'flex gap-2', - item: { - input: 'border p-0.5 mr-1', - }, - }, - }) - ); - display(
Picked: {picked.join(', ')}!
); + const picked = view( + "pickedNumbers", + Label("Pick any number", LabelProps), + Inputs.Check(["One", "Two", "Three"], { + className: { + wrapper: "flex gap-2", + item: { + input: "border p-0.5 mr-1", + }, + }, + }), + ); + display(
Picked: {picked.join(", ")}!
); - const pickedColors = view( - 'pickedColors', - Label('Pick any color', LabelProps), - Inputs.Check( - [ - { label: 'Red', value: '#FF0000' }, - { label: 'Green', value: '#00FF00' }, - { label: 'Blue', value: '#0000FF' }, - ], - { - className: { - wrapper: 'flex gap-2', - item: { - input: 'border p-0.5 mr-1', - }, - }, - valueof: (item) => item.value, - format: (item) => ( - - {item.label} - - ), - disabled: (item) => item.value === '#FFFFFF', - } - ) - ); - display(
Colors: {JSON.stringify(pickedColors)}!
); + const pickedColors = view( + "pickedColors", + Label("Pick any color", LabelProps), + Inputs.Check( + [ + { label: "Red", value: "#FF0000" }, + { label: "Green", value: "#00FF00" }, + { label: "Blue", value: "#0000FF" }, + ], + { + className: { + wrapper: "flex gap-2", + item: { + input: "border p-0.5 mr-1", + }, + }, + valueof: (item) => item.value, + format: (item) => ( + + {item.label} + + ), + disabled: (item) => item.value === "#FFFFFF", + }, + ), + ); + display(
Colors: {JSON.stringify(pickedColors)}!
); - const chosenNumber = view( - 'chosenNumber', - Label('Choose a number', LabelProps), - Inputs.Radio(['One', 'Two', 'Three'], { - className: { - wrapper: 'flex gap-2', - item: { - input: 'border p-0.5 mr-1', - }, - }, - }) - ); - display(
Chosen Number: {chosenNumber}!
); + const chosenNumber = view( + "chosenNumber", + Label("Choose a number", LabelProps), + Inputs.Radio(["One", "Two", "Three"], { + className: { + wrapper: "flex gap-2", + item: { + input: "border p-0.5 mr-1", + }, + }, + }), + ); + display(
Chosen Number: {chosenNumber}!
); - const chosenColor = view( - 'chosenColor', - Label('Choose a color', LabelProps), - Inputs.Radio( - [ - { label: 'Red', value: '#FF0000' }, - { label: 'Green', value: '#00FF00' }, - { label: 'Blue', value: '#0000FF' }, - ], - { - className: { - wrapper: 'flex gap-2', - item: { - input: 'border p-0.5 mr-1', - }, - }, - valueof: (item) => item.value, - format: (item) => ( - - {item.label} - - ), - disabled: (item) => item.value === '#FFFFFF', - } - ) - ); - display(
Chosen Color: {JSON.stringify(chosenColor)}!
); - }} -
- ); + const chosenColor = view( + "chosenColor", + Label("Choose a color", LabelProps), + Inputs.Radio( + [ + { label: "Red", value: "#FF0000" }, + { label: "Green", value: "#00FF00" }, + { label: "Blue", value: "#0000FF" }, + ], + { + className: { + wrapper: "flex gap-2", + item: { + input: "border p-0.5 mr-1", + }, + }, + valueof: (item) => item.value, + format: (item) => ( + + {item.label} + + ), + disabled: (item) => item.value === "#FFFFFF", + }, + ), + ); + display(
Chosen Color: {JSON.stringify(chosenColor)}!
); + }} +
+ ); } diff --git a/apps/reactlit-examples/src/pages/hello-world/index.tsx b/apps/reactlit-examples/src/pages/hello-world/index.tsx index 2fa51d1..47c62f9 100644 --- a/apps/reactlit-examples/src/pages/hello-world/index.tsx +++ b/apps/reactlit-examples/src/pages/hello-world/index.tsx @@ -1,26 +1,26 @@ -import { Reactlit, useReactlitState } from '@reactlit/core'; -import { Inputs, Label } from '@reactlit/radix'; +import { Reactlit, useReactlitState } from "@reactlit/core"; +import { Inputs, Label } from "@reactlit/radix"; export default function HelloWorld() { - const [appState, setAppState] = useReactlitState({ - name: '', - }); - return ( - - {async ({ display, view }) => { - display(
Hello World
); - const name = view( - 'name', - Label('Name'), - Inputs.Text({ - placeholder: 'Enter your name', - }) - ); - if (!name) { - throw new Error('Please enter your name'); - } - display(
Hello {name}!
); - }} -
- ); + const [appState, setAppState] = useReactlitState({ + name: "", + }); + return ( + + {async ({ display, view }) => { + display(
Hello World
); + const name = view( + "name", + Label("Name"), + Inputs.Text({ + placeholder: "Enter your name", + }), + ); + if (!name) { + throw new Error("Please enter your name"); + } + display(
Hello {name}!
); + }} +
+ ); } diff --git a/apps/reactlit-examples/src/pages/index.tsx b/apps/reactlit-examples/src/pages/index.tsx index 0e0c26d..8ad6e22 100644 --- a/apps/reactlit-examples/src/pages/index.tsx +++ b/apps/reactlit-examples/src/pages/index.tsx @@ -1,12 +1,12 @@ -import { ArrowBigLeftDash } from 'lucide-react'; +import { ArrowBigLeftDash } from "lucide-react"; export default function Home() { - return ( -
-

Welcome to ReactLit.

-
- Explore the examples to get started. -
-
- ); + return ( +
+

Welcome to ReactLit.

+
+ Explore the examples to get started. +
+
+ ); } diff --git a/apps/reactlit-examples/src/pages/layout-test/index.tsx b/apps/reactlit-examples/src/pages/layout-test/index.tsx index bbaf310..85718d8 100644 --- a/apps/reactlit-examples/src/pages/layout-test/index.tsx +++ b/apps/reactlit-examples/src/pages/layout-test/index.tsx @@ -1,49 +1,49 @@ -import { Debug, useDebug } from '@/components/debug-toggle'; +import { Debug, useDebug } from "@/components/debug-toggle"; import { - DataFetchingPlugin, - LayoutView, - useReactlit, - useReactlitState, - defaultLayoutState, -} from '@reactlit/core'; -import { Inputs, Label, RadixTheme } from '@reactlit/radix'; + DataFetchingPlugin, + LayoutView, + defaultLayoutState, + useReactlit, + useReactlitState, +} from "@reactlit/core"; +import { Inputs, Label, RadixTheme } from "@reactlit/radix"; export default function Starter() { - const [appState, setAppState] = useReactlitState({ - layout1: defaultLayoutState(2), - name: '', - weight: 'regular', - size: 1, - }); - const Reactlit = useReactlit(DataFetchingPlugin); - const debug = useDebug(); - return ( - - - {async (ctx) => { - const { view } = ctx; - const [col1, col2] = view( - 'layout1', - Debug, -
, - LayoutView(2,
) - ); - const v1 = col1.view( - 'leftInput', - Debug, - Label('Column Left'), - Inputs.Text() - ); - col1.display(Debug, v1); - const v2 = col2.view( - 'rightInput', - Debug, - Label('Column Right'), - Inputs.Text() - ); - col2.display(Debug, v2); - }} - - - ); + const [appState, setAppState] = useReactlitState({ + layout1: defaultLayoutState(2), + name: "", + weight: "regular", + size: 1, + }); + const Reactlit = useReactlit(DataFetchingPlugin); + const debug = useDebug(); + return ( + + + {async (ctx) => { + const { view } = ctx; + const [col1, col2] = view( + "layout1", + Debug, +
, + LayoutView(2,
), + ); + const v1 = col1.view( + "leftInput", + Debug, + Label("Column Left"), + Inputs.Text(), + ); + col1.display(Debug, v1); + const v2 = col2.view( + "rightInput", + Debug, + Label("Column Right"), + Inputs.Text(), + ); + col2.display(Debug, v2); + }} + + + ); } diff --git a/apps/reactlit-examples/src/pages/radix-inputs/index.tsx b/apps/reactlit-examples/src/pages/radix-inputs/index.tsx index 2778412..8da4f1c 100644 --- a/apps/reactlit-examples/src/pages/radix-inputs/index.tsx +++ b/apps/reactlit-examples/src/pages/radix-inputs/index.tsx @@ -1,196 +1,200 @@ -import { Debug, useDebug } from '@/components/debug-toggle'; +import { Debug, useDebug } from "@/components/debug-toggle"; import { - Badge, - ChevronDownIcon, - DataList, - ThickChevronRightIcon, -} from '@radix-ui/themes'; + Badge, + ChevronDownIcon, + DataList, + ThickChevronRightIcon, +} from "@radix-ui/themes"; import { - defaultLayoutState, - LayoutView, - Reactlit, - useReactlitState, - Wrapper, - WrapperComponent, -} from '@reactlit/core'; -import { DefaultRadixWrapper, Inputs, Label } from '@reactlit/radix'; -import { useState } from 'react'; + LayoutView, + Reactlit, + Wrapper, + WrapperComponent, + defaultLayoutState, + useReactlitState, +} from "@reactlit/core"; +import { DefaultRadixWrapper, Inputs, Label } from "@reactlit/radix"; +import { useState } from "react"; interface Country { - name: string; - region: string; - subregion: string; - population: number; - code: string; + name: string; + region: string; + subregion: string; + population: number; + code: string; } export async function fetchCountries(): Promise { - const results = await fetch( - 'https://restcountries.com/v3.1/all?fields=name,region,subregion,population,cca2', - { - cache: 'force-cache', - next: { - revalidate: 300, - tags: ['countries'], - }, - } - ); - const rawResults = await results.json(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return rawResults.map((r: any) => ({ - name: r.name.common, - region: r.region, - subregion: r.subregion, - population: r.population, - code: r.cca2, - })); + const results = await fetch( + "https://restcountries.com/v3.1/all?fields=name,region,subregion,population,cca2", + { + cache: "force-cache", + next: { + revalidate: 300, + tags: ["countries"], + }, + }, + ); + const rawResults = await results.json(); + // biome-ignore lint/suspicious/noExplicitAny: + return rawResults.map((r: any) => ({ + name: r.name.common, + region: r.region, + subregion: r.subregion, + population: r.population, + code: r.cca2, + })); } const DisplayLabel = (label: string) => { - const DisplayLabelComponent: WrapperComponent = ({ children }) => ( - - {label} - {children} - - ); - return DisplayLabelComponent; + const DisplayLabelComponent: WrapperComponent = ({ children }) => ( + + {label} + {children} + + ); + return DisplayLabelComponent; }; const ResultsWrapper: Wrapper = ({ children }) => { - const [open, setOpen] = useState(true); - return ( -
setOpen(!open)} - > -

- {open ? : } - Results -

-
- {children} -
-
- ); + const [open, setOpen] = useState(true); + return ( +
setOpen(!open)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + setOpen(!open); + } + }} + > +

+ {open ? : } + Results +

+
+ {children} +
+
+ ); }; export default function RadixInputs() { - const [appState, setAppState] = useReactlitState({ - countrySearch: '', - country: undefined as Country | undefined, - results: defaultLayoutState(1), - name: '', - bio: '', - number: [], - letter: undefined as string | undefined, - color: 'red', - slider: 0, - rangeSlider: [20, 80], - enableLetter: true, - }); - const debug = useDebug(); - return ( - - - {async ({ display, view }) => { - display(
Inputs test
); - const [results] = view('results', ResultsWrapper, LayoutView(1)); - const displayResult = (label: string, value: React.ReactNode) => { - results.display(Debug, DisplayLabel(label), value); - }; - const name = view( - 'name', - Label('Name'), - Inputs.Text({ - placeholder: 'Enter your name', - }) - ); - displayResult('Name', name); - const bio = view( - 'bio', - Label('Bio'), - Inputs.TextArea({ - placeholder: 'Enter your bio', - }) - ); - displayResult('Bio', bio); - const number = view( - 'number', - Label('Pick any numbers'), - Inputs.Check({ one: '1', two: '2', three: '3' }) - ); - displayResult('Numbers', number); - if ( - view( - 'enableLetter', - Label('Letter Selection'), -
, - Inputs.Switch() - ) - ) { - const letter = view( - 'letter', - Label('Pick one Letter'), - Inputs.Radio(['A', 'B', 'C']) - ); - displayResult('Letter', letter); - } - const color = view( - 'color', - Label('Pick a color'), -
, - Inputs.Select(['red', 'blue', 'green'] as const) - ); - displayResult('Color', {color}); - const slider = view( - 'slider', - Label('Slider'), - Inputs.Slider({ - min: 0, - max: 100, - }) - ); - displayResult('Slider', slider); - const rangeSlider = view( - 'rangeSlider', - Label('Range Slider'), - Inputs.RangeSlider({ - min: 0, - max: 100, - }) - ); - displayResult('Range Slider', rangeSlider.join(' - ')); - const countries = await fetchCountries(); - display(
Select a country
); - const filteredCountries = view( - 'countrySearch', - Label('Search'), - Inputs.Search(countries, { - placeholder: 'Search countries...', - }) - ); - const selectedCountry = view( - 'country', - Label('Countries'), - Inputs.Table(filteredCountries, { - getRowId: (country) => country.code, - className: 'h-[300px]', - }) - ); - displayResult( - 'Country', - <> - {selectedCountry?.code ? ( - - ) : ( - 'Select a country' - )} - - ); - }} - - - ); + const [appState, setAppState] = useReactlitState({ + countrySearch: "", + country: undefined as Country | undefined, + results: defaultLayoutState(1), + name: "", + bio: "", + number: [], + letter: undefined as string | undefined, + color: "red", + slider: 0, + rangeSlider: [20, 80], + enableLetter: true, + }); + const debug = useDebug(); + return ( + + + {async ({ display, view }) => { + display(
Inputs test
); + const [results] = view("results", ResultsWrapper, LayoutView(1)); + const displayResult = (label: string, value: React.ReactNode) => { + results.display(Debug, DisplayLabel(label), value); + }; + const name = view( + "name", + Label("Name"), + Inputs.Text({ + placeholder: "Enter your name", + }), + ); + displayResult("Name", name); + const bio = view( + "bio", + Label("Bio"), + Inputs.TextArea({ + placeholder: "Enter your bio", + }), + ); + displayResult("Bio", bio); + const number = view( + "number", + Label("Pick any numbers"), + Inputs.Check({ one: "1", two: "2", three: "3" }), + ); + displayResult("Numbers", number); + if ( + view( + "enableLetter", + Label("Letter Selection"), +
, + Inputs.Switch(), + ) + ) { + const letter = view( + "letter", + Label("Pick one Letter"), + Inputs.Radio(["A", "B", "C"]), + ); + displayResult("Letter", letter); + } + const color = view( + "color", + Label("Pick a color"), +
, + Inputs.Select(["red", "blue", "green"] as const), + ); + displayResult("Color", {color}); + const slider = view( + "slider", + Label("Slider"), + Inputs.Slider({ + min: 0, + max: 100, + }), + ); + displayResult("Slider", slider); + const rangeSlider = view( + "rangeSlider", + Label("Range Slider"), + Inputs.RangeSlider({ + min: 0, + max: 100, + }), + ); + displayResult("Range Slider", rangeSlider.join(" - ")); + const countries = await fetchCountries(); + display(
Select a country
); + const filteredCountries = view( + "countrySearch", + Label("Search"), + Inputs.Search(countries, { + placeholder: "Search countries...", + }), + ); + const selectedCountry = view( + "country", + Label("Countries"), + Inputs.Table(filteredCountries, { + getRowId: (country) => country.code, + className: "h-[300px]", + }), + ); + displayResult( + "Country", + selectedCountry?.code ? ( + {selectedCountry.name} + ) : ( + "Select a country" + ), + ); + }} + + + ); } diff --git a/apps/reactlit-examples/src/pages/starter/index.tsx b/apps/reactlit-examples/src/pages/starter/index.tsx index 67fcf6f..0fc015a 100644 --- a/apps/reactlit-examples/src/pages/starter/index.tsx +++ b/apps/reactlit-examples/src/pages/starter/index.tsx @@ -1,63 +1,63 @@ -import { Debug, useDebug } from '@/components/debug-toggle'; -import { Text } from '@radix-ui/themes'; -import { textPropDefs } from '@radix-ui/themes/props'; +import { Debug, useDebug } from "@/components/debug-toggle"; +import { Text } from "@radix-ui/themes"; +import { textPropDefs } from "@radix-ui/themes/props"; import { - DataFetchingPlugin, - useReactlit, - useReactlitState, -} from '@reactlit/core'; -import { DefaultRadixWrapper, Inputs, Label } from '@reactlit/radix'; + DataFetchingPlugin, + useReactlit, + useReactlitState, +} from "@reactlit/core"; +import { DefaultRadixWrapper, Inputs, Label } from "@reactlit/radix"; export default function Starter() { - const [appState, setAppState] = useReactlitState({ - name: '', - weight: 'regular', - size: 1, - }); - const Reactlit = useReactlit(DataFetchingPlugin); - const debug = useDebug(); - return ( - - - {async (ctx) => { - const { display, view } = ctx; - const name = view( - 'name', - Debug, - Label('What is your name?'), - Inputs.Text({ - placeholder: 'Enter name', - }) - ); - const weight = view( - 'weight', - Debug, - Label('Weight'), - Inputs.Radio(['light', 'regular', 'medium', 'bold'] as const) - ); - const size = view( - 'size', - Debug, - Label('Size'), - Inputs.Slider({ - min: 1, - max: 9, - }) - ); + const [appState, setAppState] = useReactlitState({ + name: "", + weight: "regular", + size: 1, + }); + const Reactlit = useReactlit(DataFetchingPlugin); + const debug = useDebug(); + return ( + + + {async (ctx) => { + const { display, view } = ctx; + const name = view( + "name", + Debug, + Label("What is your name?"), + Inputs.Text({ + placeholder: "Enter name", + }), + ); + const weight = view( + "weight", + Debug, + Label("Weight"), + Inputs.Radio(["light", "regular", "medium", "bold"] as const), + ); + const size = view( + "size", + Debug, + Label("Size"), + Inputs.Slider({ + min: 1, + max: 9, + }), + ); - display(
); - display( - Debug, - - Hello to {name ? name : Enter Name} from - Reactlit! - - ); - }} -
-
- ); + display(
); + display( + Debug, + + Hello to {name ? name : Enter Name} from + Reactlit! + , + ); + }} +
+
+ ); } diff --git a/apps/reactlit-examples/src/pages/todo-list/index.tsx b/apps/reactlit-examples/src/pages/todo-list/index.tsx index de8d016..344ec53 100644 --- a/apps/reactlit-examples/src/pages/todo-list/index.tsx +++ b/apps/reactlit-examples/src/pages/todo-list/index.tsx @@ -1,101 +1,101 @@ -import { Button, Callout, Spinner } from '@radix-ui/themes'; -import { DataFetchingPlugin, useReactlit } from '@reactlit/core'; -import { DefaultRadixWrapper, Inputs, Label } from '@reactlit/radix'; -import { InfoIcon } from 'lucide-react'; -import { TodoService } from '../../mocks/todos'; +import { Button, Callout, Spinner } from "@radix-ui/themes"; +import { DataFetchingPlugin, useReactlit } from "@reactlit/core"; +import { DefaultRadixWrapper, Inputs, Label } from "@reactlit/radix"; +import { InfoIcon } from "lucide-react"; +import { TodoService } from "../../mocks/todos"; export const Loader = ({ message }: { message: string }) => { - return ( -
- {message} -
- ); + return ( +
+ {message} +
+ ); }; const api = new TodoService([], 1000); export default function TodoList() { - const Reactlit = useReactlit(DataFetchingPlugin); - return ( - - - {async ({ display, view, set, changed, fetcher }) => { - display( - - - - - - This app is purposely slow to show how Reactlit handles loading - states. - - - ); - const todosFetcher = fetcher(['todos'], () => api.getTodos()); - display( -
- -
- ); - view( - 'adding', - Inputs.AsyncButton( - async () => { - const newTodo = await api.addTodo(); - await todosFetcher.refetch(); - set('selectedTodo', newTodo.id); - }, - { - disabled: todosFetcher.isFetching(), - content: 'Add Todo', - } - ) - ); - const todos = todosFetcher.get() ?? []; - const selectedTodo = view( - 'selectedTodo', - Inputs.Table(todos, { - getRowId: (todo) => todo.id, - columns: ['task', 'completed'], - format: { - completed: (completed) => (completed ? '☑️' : ''), - }, - }) - ); - if (selectedTodo) { - if (changed('selectedTodo')) { - set('task', selectedTodo.task); - set('completed', selectedTodo.completed); - } - const task = view('task', Label('Task'), Inputs.Text()); - const completed = view( - 'completed', - Label('Completed'), - Inputs.Switch() - ); - view( - 'updaing', - Inputs.AsyncButton( - async () => { - // todosFetcher.update((todos) => { - // return todos.map((todo) => - // todo.id === selectedTodo.id - // ? { ...todo, task, completed } - // : todo - // ); - // }); - await api.updateTodo(selectedTodo.id, { task, completed }); - await todosFetcher.refetch(); - }, - { - disabled: todosFetcher.isFetching(), - content: 'Update', - } - ) - ); - } - }} -
-
- ); + const Reactlit = useReactlit(DataFetchingPlugin); + return ( + + + {async ({ display, view, set, changed, fetcher }) => { + display( + + + + + + This app is purposely slow to show how Reactlit handles loading + states. + + , + ); + const todosFetcher = fetcher(["todos"], () => api.getTodos()); + display( +
+ +
, + ); + view( + "adding", + Inputs.AsyncButton( + async () => { + const newTodo = await api.addTodo(); + await todosFetcher.refetch(); + set("selectedTodo", newTodo.id); + }, + { + disabled: todosFetcher.isFetching(), + content: "Add Todo", + }, + ), + ); + const todos = todosFetcher.get() ?? []; + const selectedTodo = view( + "selectedTodo", + Inputs.Table(todos, { + getRowId: (todo) => todo.id, + columns: ["task", "completed"], + format: { + completed: (completed) => (completed ? "☑️" : ""), + }, + }), + ); + if (selectedTodo) { + if (changed("selectedTodo")) { + set("task", selectedTodo.task); + set("completed", selectedTodo.completed); + } + const task = view("task", Label("Task"), Inputs.Text()); + const completed = view( + "completed", + Label("Completed"), + Inputs.Switch(), + ); + view( + "updaing", + Inputs.AsyncButton( + async () => { + // todosFetcher.update((todos) => { + // return todos.map((todo) => + // todo.id === selectedTodo.id + // ? { ...todo, task, completed } + // : todo + // ); + // }); + await api.updateTodo(selectedTodo.id, { task, completed }); + await todosFetcher.refetch(); + }, + { + disabled: todosFetcher.isFetching(), + content: "Update", + }, + ), + ); + } + }} +
+
+ ); } diff --git a/apps/reactlit-examples/src/styles/globals.css b/apps/reactlit-examples/src/styles/globals.css index cb50ce9..3471086 100644 --- a/apps/reactlit-examples/src/styles/globals.css +++ b/apps/reactlit-examples/src/styles/globals.css @@ -3,56 +3,56 @@ @tailwind utilities; :root { - --background: #ffffff; - --foreground: #171717; + --background: #ffffff; + --foreground: #171717; } @media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } + :root { + --background: #0a0a0a; + --foreground: #ededed; + } } body { - color: var(--foreground); - background: var(--background); - font-family: Arial, Helvetica, sans-serif; + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; } tr.rt-TableRow:nth-child(even) { - background-color: var(--gray-2); + background-color: var(--gray-2); } .overlay { - position: fixed; - cursor: pointer; - top: 4rem; - right: 2rem; - padding: 1rem; - background: var(--gray-2); - border: 1px solid #bbb; - border-radius: 0.5rem; - opacity: 0.8; - transition: all 0.2s; - z-index: 100; - width: 25%; - overflow-x: hidden; - overflow-y: hidden; - max-height: 3.5rem; + position: fixed; + cursor: pointer; + top: 4rem; + right: 2rem; + padding: 1rem; + background: var(--gray-2); + border: 1px solid #bbb; + border-radius: 0.5rem; + opacity: 0.8; + transition: all 0.2s; + z-index: 100; + width: 25%; + overflow-x: hidden; + overflow-y: hidden; + max-height: 3.5rem; } .overlay.open { - max-height: 80%; + max-height: 80%; } .overlay-header { - display: flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1rem; } .overlay:hover { - opacity: 1; + opacity: 1; } diff --git a/apps/reactlit-examples/src/utils/utils.ts b/apps/reactlit-examples/src/utils/utils.ts index 9ad0df4..ac680b3 100644 --- a/apps/reactlit-examples/src/utils/utils.ts +++ b/apps/reactlit-examples/src/utils/utils.ts @@ -1,6 +1,6 @@ -import { type ClassValue, clsx } from 'clsx'; -import { twMerge } from 'tailwind-merge'; +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); + return twMerge(clsx(inputs)); } diff --git a/apps/reactlit-examples/src/utils/wait.ts b/apps/reactlit-examples/src/utils/wait.ts index cbd907e..76d5de3 100644 --- a/apps/reactlit-examples/src/utils/wait.ts +++ b/apps/reactlit-examples/src/utils/wait.ts @@ -1,3 +1,3 @@ export async function wait(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/apps/reactlit-examples/tailwind.config.ts b/apps/reactlit-examples/tailwind.config.ts index 109807b..5d3c1bd 100644 --- a/apps/reactlit-examples/tailwind.config.ts +++ b/apps/reactlit-examples/tailwind.config.ts @@ -1,18 +1,18 @@ import type { Config } from "tailwindcss"; export default { - content: [ - "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", - "./src/components/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", - ], - theme: { - extend: { - colors: { - background: "var(--background)", - foreground: "var(--foreground)", - }, - }, - }, - plugins: [], + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + colors: { + background: "var(--background)", + foreground: "var(--foreground)", + }, + }, + }, + plugins: [], } satisfies Config; diff --git a/apps/reactlit-examples/tsconfig.json b/apps/reactlit-examples/tsconfig.json index 572b7ad..9be29a0 100644 --- a/apps/reactlit-examples/tsconfig.json +++ b/apps/reactlit-examples/tsconfig.json @@ -1,22 +1,22 @@ { - "compilerOptions": { - "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] } diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..0541bd3 --- /dev/null +++ b/biome.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "ignore": [ + "node_modules", + "dist", + "out", + ".turbo", + ".next", + "public", + ".astro" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "style": { + "useImportType": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + }, + "parser": { + "unsafeParameterDecoratorsEnabled": true + } + } +} diff --git a/justfile b/justfile deleted file mode 100644 index e603642..0000000 --- a/justfile +++ /dev/null @@ -1,11 +0,0 @@ -build: - pnpm run build - -rebuild: - turbo run clean && pnpm i && turbo run build --force - -dev: - pnpm run dev - -devdocs: - pnpm run dev:docs diff --git a/libs/core/.eslintrc.cjs b/libs/core/.eslintrc.cjs index cf8b644..c4d733b 100644 --- a/libs/core/.eslintrc.cjs +++ b/libs/core/.eslintrc.cjs @@ -1,10 +1,10 @@ module.exports = { - extends: ['next'], - plugins: ['prettier'], - rules: { - 'no-console': 'error', - 'prettier/prettier': 'warn', - '@next/next/no-html-link-for-pages': 'off', - 'react/jsx-key': 1, - }, + extends: ["next"], + plugins: ["prettier"], + rules: { + "no-console": "error", + "prettier/prettier": "warn", + "@next/next/no-html-link-for-pages": "off", + "react/jsx-key": 1, + }, }; diff --git a/libs/core/jest.config.cjs b/libs/core/jest.config.cjs deleted file mode 100644 index 4796edd..0000000 --- a/libs/core/jest.config.cjs +++ /dev/null @@ -1,15 +0,0 @@ -/** @type {import('ts-jest').JestConfigWithTsJest} **/ -module.exports = { - transform: { - '^.+.tsx?$': ['ts-jest', {}], - }, - roots: [''], - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - testEnvironment: 'jsdom', - modulePathIgnorePatterns: [ - '/test/__fixtures__', - '/node_modules', - '/dist', - ], - preset: 'ts-jest', -}; diff --git a/libs/core/package.json b/libs/core/package.json index 06635cd..28d6e88 100644 --- a/libs/core/package.json +++ b/libs/core/package.json @@ -1,67 +1,57 @@ { - "name": "@reactlit/core", - "description": "Simple micro-app framework for React", - "version": "0.2.1", - "license": "MIT", - "homepage": "https://github.com/mshafir/reactlit", - "repository": "github:mshafir/reactlit", - "author": "Michael Shafir ", - "type": "module", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - } - }, - "files": [ - "dist" - ], - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, - "scripts": { - "prebuild": "cp ../../README.md .", - "build": "tsup && tsc --project tsconfig.build.json", - "dev": "tsup --dts --watch", - "test": "jest", - "clean": "rm -rf node_modules && rm -rf .turbo && rm -rf dist", - "lint": "eslint src/**/*.ts* --fix" - }, - "dependencies": { - "@tanstack/react-query": "^5.62.3", - "fast-deep-equal": "^3.1.3", - "react-error-boundary": "^4.1.2", - "zustand": "^5.0.3" - }, - "devDependencies": { - "@mollycule/vigilante": "^1.0.2", - "@testing-library/dom": "^10.4.0", - "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.1.0", - "@testing-library/user-event": "^14.5.2", - "@types/jest": "^29.5.14", - "@types/react": "^19.0.1", - "@types/react-dom": "^19.0.2", - "eslint": "^7.23.0", - "eslint-config-next": "^12.0.8", - "eslint-plugin-jest": "^28.9.0", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-react": "7.28.0", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "prettier": "^2.8.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "tsup": "^8.3.5", - "typescript": "^5.7.2" - }, - "peerDependencies": { - "react": ">=17 <=19", - "react-dom": ">=17 <=19" - } -} \ No newline at end of file + "name": "@reactlit/core", + "description": "Simple micro-app framework for React", + "version": "0.2.1", + "license": "MIT", + "homepage": "https://github.com/mshafir/reactlit", + "repository": "github:mshafir/reactlit", + "author": "Michael Shafir ", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "files": ["dist"], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "scripts": { + "prebuild": "cp ../../README.md .", + "build": "tsup && tsc --project tsconfig.build.json", + "dev": "tsup --dts --watch", + "test": "vitest run", + "clean": "rm -rf node_modules && rm -rf .turbo && rm -rf dist", + "lint": "biome check --write" + }, + "dependencies": { + "@tanstack/react-query": "^5.62.3", + "fast-deep-equal": "^3.1.3", + "react-error-boundary": "^4.1.2", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@mollycule/vigilante": "^1.0.2", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@testing-library/user-event": "^14.5.2", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.2", + "@biomejs/biome": "1.9.4", + "jsdom": "^26.1.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tsup": "^8.3.5", + "typescript": "*", + "vitest": "^3.1.2" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + } +} diff --git a/libs/core/src/builtins/changed.ts b/libs/core/src/builtins/changed.ts index ca1c324..513f537 100644 --- a/libs/core/src/builtins/changed.ts +++ b/libs/core/src/builtins/changed.ts @@ -1,48 +1,48 @@ -import { useCallback } from 'react'; -import { ReactlitContext, StateBase } from './types'; -import { InternalReactlitState } from './internal-state'; -import { deepEqual } from '../utils/deep-equal'; +import { useCallback } from "react"; +import { deepEqual } from "../utils/deep-equal"; +import { InternalReactlitState } from "./internal-state"; +import { ReactlitContext, StateBase } from "./types"; function deltas( - state: T, - previousState: T | undefined + state: T, + previousState: T | undefined, ): (keyof T)[] { - return Object.keys(state).filter( - (key: string) => - !previousState || !deepEqual(state[key], previousState[key]) - ); + return Object.keys(state).filter( + (key: string) => + !previousState || !deepEqual(state[key], previousState[key]), + ); } export function useReactlitChanged( - { state, previousState, setPreviousState }: InternalReactlitState, - debug: boolean + { state, previousState, setPreviousState }: InternalReactlitState, + debug: boolean, ) { - return useCallback['changed']>( - (...keys) => { - const changedKeys = deltas(state, previousState); - const selectedChangedKeys = keys.filter((k) => - changedKeys.includes(k as string) - ); - const isChanged = selectedChangedKeys.length > 0; - if (isChanged) { - if (debug) { - for (const k of selectedChangedKeys) { - // eslint-disable-next-line no-console - console.debug( - `changed ${String(k)}: ${previousState?.[k]} -> ${state[k]}` - ); - } - } - setPreviousState((prev) => { - let newState = prev; - for (const k of selectedChangedKeys) { - newState = { ...newState, [k]: state[k] }; - } - return newState; - }); - } - return isChanged; - }, - [state, previousState, setPreviousState, debug] - ); + return useCallback["changed"]>( + (...keys) => { + const changedKeys = deltas(state, previousState); + const selectedChangedKeys = keys.filter((k) => + changedKeys.includes(k as string), + ); + const isChanged = selectedChangedKeys.length > 0; + if (isChanged) { + if (debug) { + for (const k of selectedChangedKeys) { + // eslint-disable-next-line no-console + console.debug( + `changed ${String(k)}: ${previousState?.[k]} -> ${state[k]}`, + ); + } + } + setPreviousState((prev) => { + let newState = prev; + for (const k of selectedChangedKeys) { + newState = { ...newState, [k]: state[k] }; + } + return newState; + }); + } + return isChanged; + }, + [state, previousState, setPreviousState, debug], + ); } diff --git a/libs/core/src/builtins/display.tsx b/libs/core/src/builtins/display.tsx index df7a660..cc819fb 100644 --- a/libs/core/src/builtins/display.tsx +++ b/libs/core/src/builtins/display.tsx @@ -1,12 +1,12 @@ -import { ReactNode, useCallback, useState } from 'react'; -import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary'; -import { tail } from '../utils/tail'; -import { ApplyWrappers, Wrapper } from '../wrappers'; -import { ReactlitContext, ReactlitProps, StateBase } from './types'; +import { ReactNode, useCallback, useState } from "react"; +import { ErrorBoundary as ReactErrorBoundary } from "react-error-boundary"; +import { tail } from "../utils/tail"; +import { ApplyWrappers, Wrapper } from "../wrappers"; +import { ReactlitContext, ReactlitProps, StateBase } from "./types"; interface DisplayState { - position: number; - elements: [string, React.ReactNode][]; + position: number; + elements: [string, React.ReactNode][]; } type KeyedDisplayArgs = [string, ...Wrapper[], ReactNode]; @@ -15,105 +15,105 @@ type UnkeyedDisplayArgs = [...Wrapper[], ReactNode]; export type DisplayArgs = KeyedDisplayArgs | UnkeyedDisplayArgs; export function isKeyedDisplayArgs( - args: DisplayArgs + args: DisplayArgs, ): args is KeyedDisplayArgs { - return args.length > 1 && typeof args[0] === 'string'; + return args.length > 1 && typeof args[0] === "string"; } export function normalizeDisplayArgs(args: DisplayArgs) { - const manualKey = isKeyedDisplayArgs(args) ? args[0] : undefined; - const restArgs = isKeyedDisplayArgs(args) - ? (args.slice(1) as UnkeyedDisplayArgs) - : args; - const [wrappers, node] = tail(restArgs); - return { - manualKey, - wrappers, - node, - }; + const manualKey = isKeyedDisplayArgs(args) ? args[0] : undefined; + const restArgs = isKeyedDisplayArgs(args) + ? (args.slice(1) as UnkeyedDisplayArgs) + : args; + const [wrappers, node] = tail(restArgs); + return { + manualKey, + wrappers, + node, + }; } export function useReactlitDisplay({ - renderError, -}: Pick, 'renderError'>) { - const [renderState, setRenderState] = useState({ - position: 0, - elements: [], - }); + renderError, +}: Pick, "renderError">) { + const [renderState, setRenderState] = useState({ + position: 0, + elements: [], + }); - const display = useCallback['display']>( - (...args: DisplayArgs) => { - const { manualKey, wrappers, node } = normalizeDisplayArgs(args); + const display = useCallback["display"]>( + (...args: DisplayArgs) => { + const { manualKey, wrappers, node } = normalizeDisplayArgs(args); - setRenderState(({ position, elements }) => { - const key = manualKey ?? `${position}`; - const keyIndex = elements - .slice(0, position) - .findIndex(([k]) => manualKey && k === manualKey); + setRenderState(({ position, elements }) => { + const key = manualKey ?? `${position}`; + const keyIndex = elements + .slice(0, position) + .findIndex(([k]) => manualKey && k === manualKey); - const element = ( - - - {node} - - - ); - const newEntry = [key, element] as [string, React.ReactNode]; + const element = ( + + + {node} + + + ); + const newEntry = [key, element] as [string, React.ReactNode]; - if (keyIndex !== -1) { - return { - position, - elements: [ - ...elements.slice(0, keyIndex), - newEntry, - ...elements.slice(keyIndex + 1), - ], - }; - } else if (position < elements.length) { - return { - position: position + 1, - elements: [ - ...elements.slice(0, position), - newEntry, - // for manual keys that haven't been found by the above case - // we don't want to overwrite the index element because - // it's likely a different element - ...elements - .slice(position + (manualKey ? 0 : 1)) - .filter((e) => !manualKey || e[0] !== manualKey), - ], - }; - } else { - return { - position: elements.length + 1, - elements: [...elements, newEntry], - }; - } - }); - }, - [setRenderState, renderError] - ); + if (keyIndex !== -1) { + return { + position, + elements: [ + ...elements.slice(0, keyIndex), + newEntry, + ...elements.slice(keyIndex + 1), + ], + }; + } + if (position < elements.length) { + return { + position: position + 1, + elements: [ + ...elements.slice(0, position), + newEntry, + // for manual keys that haven't been found by the above case + // we don't want to overwrite the index element because + // it's likely a different element + ...elements + .slice(position + (manualKey ? 0 : 1)) + .filter((e) => !manualKey || e[0] !== manualKey), + ], + }; + } + return { + position: elements.length + 1, + elements: [...elements, newEntry], + }; + }); + }, + [renderError], + ); - const resetRenderPosition = useCallback(() => { - setRenderState(({ elements }) => ({ elements, position: 0 })); - }, [setRenderState]); + const resetRenderPosition = useCallback(() => { + setRenderState(({ elements }) => ({ elements, position: 0 })); + }, []); - // truncates stranded elements after the last position - const finalizeRender = useCallback(() => { - setRenderState(({ elements, position }) => ({ - position, - elements: elements.slice(0, position), - })); - }, [setRenderState]); + // truncates stranded elements after the last position + const finalizeRender = useCallback(() => { + setRenderState(({ elements, position }) => ({ + position, + elements: elements.slice(0, position), + })); + }, []); - return { - renderState, - display, - resetRenderPosition, - finalizeRender, - }; + return { + renderState, + display, + resetRenderPosition, + finalizeRender, + }; } diff --git a/libs/core/src/builtins/internal-state.ts b/libs/core/src/builtins/internal-state.ts index 6465651..a69ccd1 100644 --- a/libs/core/src/builtins/internal-state.ts +++ b/libs/core/src/builtins/internal-state.ts @@ -1,25 +1,25 @@ -import { useState } from 'react'; -import { useDeepMemo } from '../hooks/use-deep-memo'; -import { ReactlitStateSetter, StateBase } from './types'; +import { useState } from "react"; +import { useDeepMemo } from "../hooks/use-deep-memo"; +import { ReactlitStateSetter, StateBase } from "./types"; export interface InternalReactlitState { - state: T; - setState: ReactlitStateSetter; - previousState: T; - setPreviousState: React.Dispatch>; + state: T; + setState: ReactlitStateSetter; + previousState: T; + setPreviousState: React.Dispatch>; } export function useInternalReactlitState( - rawState: T, - setState: ReactlitStateSetter + rawState: T, + setState: ReactlitStateSetter, ): InternalReactlitState { - const state = useDeepMemo(() => rawState, [rawState]); - const [previousState, setPreviousState] = useState(state); + const state = useDeepMemo(() => rawState, [rawState]); + const [previousState, setPreviousState] = useState(state); - return { - state, - setState, - previousState, - setPreviousState, - }; + return { + state, + setState, + previousState, + setPreviousState, + }; } diff --git a/libs/core/src/builtins/set.ts b/libs/core/src/builtins/set.ts index 8c4a192..1f01dc6 100644 --- a/libs/core/src/builtins/set.ts +++ b/libs/core/src/builtins/set.ts @@ -1,22 +1,22 @@ -import { useCallback } from 'react'; -import { deepEqual } from '../utils/deep-equal'; -import { ReactlitContext, StateBase } from './types'; -import { InternalReactlitState } from './internal-state'; +import { useCallback } from "react"; +import { deepEqual } from "../utils/deep-equal"; +import { InternalReactlitState } from "./internal-state"; +import { ReactlitContext, StateBase } from "./types"; export function useReactlitSet({ - state, - setState, - setPreviousState, + state, + setState, + setPreviousState, }: InternalReactlitState) { - return useCallback['set']>( - (key, value) => { - setState(key, (prev) => { - if (deepEqual(prev, value)) return prev; - return value; - }); - setPreviousState(state); - return value; - }, - [setState, state, setPreviousState] - ); + return useCallback["set"]>( + (key, value) => { + setState(key, (prev) => { + if (deepEqual(prev, value)) return prev; + return value; + }); + setPreviousState(state); + return value; + }, + [setState, state, setPreviousState], + ); } diff --git a/libs/core/src/builtins/types.ts b/libs/core/src/builtins/types.ts index e7dca15..3fd9bbc 100644 --- a/libs/core/src/builtins/types.ts +++ b/libs/core/src/builtins/types.ts @@ -1,65 +1,67 @@ -import { Dispatch, ReactNode, SetStateAction } from 'react'; -import { DisplayArgs } from './display'; -import { ViewArgs } from './view'; +import { Dispatch, ReactNode, SetStateAction } from "react"; +import { DisplayArgs } from "./display"; +import { ViewArgs } from "./view"; export type StateBase = Record; export interface ViewComponentProps { - stateKey: string; - value: T; - setValue: Dispatch; - display: (...args: DisplayArgs) => void; - view: ( - ...args: ViewArgs - ) => R; + stateKey: string; + value: T; + setValue: Dispatch; + display: (...args: DisplayArgs) => void; + view: ( + ...args: ViewArgs + ) => R; } export type ViewComponent = React.FC>; export interface ViewDefinition { - component: ViewComponent; - getReturnValue?: (props: ViewComponentProps) => ReturnType; + component: ViewComponent; + getReturnValue?: (props: ViewComponentProps) => ReturnType; } +// biome-ignore lint/suspicious/noExplicitAny: export interface ReactlitContext { - view: (...args: ViewArgs) => R; - set: (key: K, value: T[K]) => T[K]; - display: (...args: DisplayArgs) => void; - changed: (...keys: (keyof T)[]) => boolean; - trigger: () => void; - state: T; + view: (...args: ViewArgs) => R; + set: (key: K, value: T[K]) => T[K]; + display: (...args: DisplayArgs) => void; + changed: (...keys: (keyof T)[]) => boolean; + trigger: () => void; + state: T; } export type ReactlitStateSetter = ( - key: K, - value: SetStateAction + key: K, + value: SetStateAction, ) => void; export type ReactlitFunction< - T extends StateBase = any, - C extends ReactlitContext = ReactlitContext + // biome-ignore lint/suspicious/noExplicitAny: + T extends StateBase = any, + C extends ReactlitContext = ReactlitContext, > = (ctx: C) => Promise; export type ReactlitProps = { - state?: T; - setState?: ReactlitStateSetter; - /** - * Render function to display a loading message - */ - renderLoading?: (rendering: boolean) => ReactNode; - /** - * Render function to display an error message - */ - renderError?: (props: { - error: any; - resetErrorBoundary: (...args: any[]) => void; - }) => ReactNode; - /** - * Whether to log debug messages to the console - */ - debug?: boolean; - /** - * Function for the Reactlit rendering logic - */ - children: ReactlitFunction; + state?: T; + setState?: ReactlitStateSetter; + /** + * Render function to display a loading message + */ + renderLoading?: (rendering: boolean) => ReactNode; + /** + * Render function to display an error message + */ + renderError?: (props: { + error: unknown; + resetErrorBoundary: (...args: unknown[]) => void; + }) => ReactNode; + /** + * Whether to log debug messages to the console + */ + debug?: boolean; + /** + * Function for the Reactlit rendering logic + */ + children: ReactlitFunction; }; diff --git a/libs/core/src/builtins/view.ts b/libs/core/src/builtins/view.ts index 7c0259e..3e5f9d3 100644 --- a/libs/core/src/builtins/view.ts +++ b/libs/core/src/builtins/view.ts @@ -1,74 +1,75 @@ -import { useCallback } from 'react'; +import { useCallback } from "react"; +import { tail } from "../utils/tail"; +import { Wrapper } from "../wrappers"; import { - ReactlitContext, - StateBase, - ViewComponent, - ViewComponentProps, - ViewDefinition, -} from './types'; -import { Wrapper } from '../wrappers'; -import { tail } from '../utils/tail'; + ReactlitContext, + StateBase, + ViewComponent, + ViewComponentProps, + ViewDefinition, +} from "./types"; export function defineView( - component: ViewComponent + component: ViewComponent, ): ViewDefinition { - return { component }; + return { component }; } export function defineTransformView( - component: ViewComponent, - getReturnValue: (props: ViewComponentProps) => ReturnType + component: ViewComponent, + getReturnValue: (props: ViewComponentProps) => ReturnType, ): ViewDefinition { - return { component, getReturnValue }; + return { component, getReturnValue }; } export type ViewArgs = [ - key: K, - ...wrappers: Wrapper[], - def: ViewDefinition + key: K, + ...wrappers: Wrapper[], + def: ViewDefinition, ]; export function normalizeViewArgs< - T extends StateBase, - K extends keyof T & string, - V, - R + T extends StateBase, + K extends keyof T & string, + V, + R, >(args: ViewArgs) { - const [key, ...restArgs] = args; - const [wrappers, def] = tail(restArgs); - return { key, wrappers, def }; + const [key, ...restArgs] = args; + const [wrappers, def] = tail(restArgs); + return { key, wrappers, def }; } function makeViewFunction({ - set, - display, - state, -}: Pick, 'set' | 'display' | 'state'>) { - return function view( - ...args: ViewArgs - ) { - const { key, wrappers, def } = normalizeViewArgs(args); - const { component, getReturnValue } = def; - const props: ViewComponentProps = { - stateKey: key, - value: state[key] as V, - setValue: (value: any) => set(key, value), - display, - view, - }; - display(key, ...wrappers, component(props)); - return getReturnValue ? getReturnValue(props) : (state[key] as R); - }; + set, + display, + state, +}: Pick, "set" | "display" | "state">) { + return function view( + ...args: ViewArgs + ) { + const { key, wrappers, def } = normalizeViewArgs(args); + const { component, getReturnValue } = def; + const props: ViewComponentProps = { + stateKey: key, + value: state[key] as V, + // biome-ignore lint/suspicious/noExplicitAny: + setValue: (value: any) => set(key, value), + display, + view, + }; + display(key, ...wrappers, component(props)); + return getReturnValue ? getReturnValue(props) : (state[key] as R); + }; } export function useReactlitView({ - set, - display, - state, -}: Pick, 'set' | 'display' | 'state'>) { - return useCallback['view']>( - (...args: ViewArgs) => - makeViewFunction({ set, display, state })(...args), - [state, set, display] - ); + set, + display, + state, +}: Pick, "set" | "display" | "state">) { + return useCallback["view"]>( + (...args: ViewArgs) => + makeViewFunction({ set, display, state })(...args), + [state, set, display], + ); } diff --git a/libs/core/src/hooks/use-deep-memo.ts b/libs/core/src/hooks/use-deep-memo.ts index facefa3..5cb750d 100644 --- a/libs/core/src/hooks/use-deep-memo.ts +++ b/libs/core/src/hooks/use-deep-memo.ts @@ -1,5 +1,5 @@ -import { useRef } from 'react'; -import { deepEqual } from '../utils/deep-equal'; +import { useRef } from "react"; +import { deepEqual } from "../utils/deep-equal"; /** * Memoize a result using deep equality. This hook has two advantages over @@ -9,14 +9,14 @@ import { deepEqual } from '../utils/deep-equal'; * optimization (see https://reactjs.org/docs/hooks-reference.html#usememo). */ export function useDeepMemo( - memoFn: () => TValue, - key: TKey + memoFn: () => TValue, + key: TKey, ): TValue { - const ref = useRef<{ key: TKey; value: TValue }>(null); + const ref = useRef<{ key: TKey; value: TValue }>(null); - if (!ref.current || !deepEqual(key, ref.current.key)) { - ref.current = { key, value: memoFn() }; - } + if (!ref.current || !deepEqual(key, ref.current.key)) { + ref.current = { key, value: memoFn() }; + } - return ref.current.value; + return ref.current.value; } diff --git a/libs/core/src/hooks/use-reactlit-state.ts b/libs/core/src/hooks/use-reactlit-state.ts index 8c31439..3bcc433 100644 --- a/libs/core/src/hooks/use-reactlit-state.ts +++ b/libs/core/src/hooks/use-reactlit-state.ts @@ -1,67 +1,70 @@ -import { SetStateAction, useCallback, useMemo, useState } from 'react'; +import { SetStateAction, useCallback, useMemo, useState } from "react"; export type StateAndSetter = [T, React.Dispatch>]; export function isSetStateFunction( - value: T | ((prev: T) => void) + value: T | ((prev: T) => void), ): value is (prev: T) => void { - return typeof value === 'function'; + return typeof value === "function"; } export type ComboState> = { - [K in keyof T]: StateAndSetter; + [K in keyof T]: StateAndSetter; }; export type ComboStateResult> = [ - T, - ComboStateSetter + T, + ComboStateSetter, ]; export type ComboStateSetter> = < - K extends keyof T + K extends keyof T, >( - key: K, - value: SetStateAction + key: K, + value: SetStateAction, ) => void; export function useCompoundReaclitState< - T extends Record, - U extends Record + T extends Record, + U extends Record, >( - states: ComboState, - defaultStateSetter: StateAndSetter + states: ComboState, + defaultStateSetter: StateAndSetter, ): ComboStateResult { - const [defaultState, defaultSetter] = defaultStateSetter; - const state = useMemo(() => { - const newState: any = {}; - for (const key in states) { - newState[key] = states[key][0]; - } - return { ...newState, ...defaultState }; - }, [states, defaultState]); + const [defaultState, defaultSetter] = defaultStateSetter; + const state = useMemo(() => { + // biome-ignore lint/suspicious/noExplicitAny: + const newState: any = {}; + for (const key in states) { + newState[key] = states[key][0]; + } + return { ...newState, ...defaultState }; + }, [states, defaultState]); - const setState = useCallback>( - (key: K, value: SetStateAction<(T & U)[K]>) => { - if (key in states) { - states[key as keyof T][1](value as SetStateAction); - } else { - defaultSetter((p) => ({ - ...p, - [key]: isSetStateFunction(value) - ? value(p[key as keyof U] as any) - : value, - })); - } - }, - [states, defaultSetter] - ); + const setState = useCallback>( + (key: K, value: SetStateAction<(T & U)[K]>) => { + if (key in states) { + states[key as keyof T][1](value as SetStateAction); + } else { + defaultSetter((p) => ({ + ...p, + [key]: isSetStateFunction(value) + ? // biome-ignore lint/suspicious/noExplicitAny: + value(p[key as keyof U] as any) + : value, + })); + } + }, + [states, defaultSetter], + ); - return [state, setState]; + return [state, setState]; } export function useReactlitState>( - initialState: T + initialState: T, ): ComboStateResult { - const empty = useMemo>(() => ({}), []); - return useCompoundReaclitState(empty, useState(initialState)); + // biome-ignore lint/complexity/noBannedTypes: + const empty = useMemo>(() => ({}), []); + return useCompoundReaclitState(empty, useState(initialState)); } diff --git a/libs/core/src/hooks/use-reactlit.spec.tsx b/libs/core/src/hooks/use-reactlit.spec.tsx index c86bdb3..e4117d7 100644 --- a/libs/core/src/hooks/use-reactlit.spec.tsx +++ b/libs/core/src/hooks/use-reactlit.spec.tsx @@ -1,32 +1,32 @@ -import '@testing-library/jest-dom'; -import { render, screen } from '@testing-library/react'; -import { definePlugin, useReactlit } from './use-reactlit'; -import { useReactlitState } from './use-reactlit-state'; +import "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import { definePlugin, useReactlit } from "./use-reactlit"; +import { useReactlitState } from "./use-reactlit-state"; -test('reactlit plugin', async () => { - let pluginRan = false; - const TestPlugin = definePlugin(({ display }) => ({ - runPlugin: () => { - pluginRan = true; - return display('Hello from plugin'); - }, - })); +test("reactlit plugin", async () => { + let pluginRan = false; + const TestPlugin = definePlugin(({ display }) => ({ + runPlugin: () => { + pluginRan = true; + return display("Hello from plugin"); + }, + })); - function PluginTest() { - const [state, setState] = useReactlitState({ - firstName: 'John', - lastName: 'Doe', - }); - const Reactlit = useReactlit(TestPlugin); - return ( - - {async (ctx) => { - ctx.runPlugin(); - }} - - ); - } - render(); - expect(pluginRan).toBe(true); - expect(await screen.findByText('Hello from plugin')).toBeVisible(); + function PluginTest() { + const [state, setState] = useReactlitState({ + firstName: "John", + lastName: "Doe", + }); + const Reactlit = useReactlit(TestPlugin); + return ( + + {async (ctx) => { + ctx.runPlugin(); + }} + + ); + } + render(); + expect(pluginRan).toBe(true); + expect(await screen.findByText("Hello from plugin")).toBeVisible(); }); diff --git a/libs/core/src/hooks/use-reactlit.tsx b/libs/core/src/hooks/use-reactlit.tsx index 06bae6a..709d8f8 100644 --- a/libs/core/src/hooks/use-reactlit.tsx +++ b/libs/core/src/hooks/use-reactlit.tsx @@ -1,59 +1,63 @@ -import { useCallback, useMemo } from 'react'; -import { Reactlit } from '../reactlit'; +import { useCallback, useMemo } from "react"; import { - ReactlitContext, - ReactlitFunction, - ReactlitProps, - StateBase, -} from '../builtins/types'; + ReactlitContext, + ReactlitFunction, + ReactlitProps, + StateBase, +} from "../builtins/types"; +import { Reactlit } from "../reactlit"; -export type ReactlitPlugin = (ctx: ReactlitContext) => C; +// biome-ignore lint/suspicious/noExplicitAny: +export type ReactlitPlugin = (ctx: ReactlitContext) => C; export function definePlugin(plugin: ReactlitPlugin) { - return plugin; + return plugin; } type GenericPluginResult = Plugin extends ReactlitPlugin - ? C - : never; + ? C + : never; type ApplyPlugins = Plugins extends readonly [ - infer Plugin, - ...infer Rest + infer Plugin, + ...infer Rest, ] - ? GenericPluginResult & ApplyPlugins - : Record; + ? GenericPluginResult & ApplyPlugins + : Record; export type ReactlitFunctionWithPlugins< - T extends StateBase, - P extends readonly ReactlitPlugin[] + T extends StateBase, + P extends readonly ReactlitPlugin[], > = ReactlitFunction & ReactlitContext>; -export function useReactlit

[]>( - ...plugins: P +export function useReactlit

( + ...plugins: P ) { - return useMemo(() => { - function CustomReactlit({ - children, - ...props - }: Omit, 'children'> & { - children: ReactlitFunction & ReactlitContext>; - }) { - const func = useCallback( - async (ctx: ReactlitContext) => { - return children( - (plugins ?? []).reduce( - (acc, plugin) => ({ ...acc, ...plugin(ctx) }), - ctx as any - ) - ); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [children, ...(plugins ?? [])] - ); - return {func}; - } - return CustomReactlit; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [...plugins]); + // biome-ignore lint/correctness/useExhaustiveDependencies: + return useMemo(() => { + function CustomReactlit({ + children, + ...props + }: Omit, "children"> & { + children: ReactlitFunction & ReactlitContext>; + }) { + // biome-ignore lint/correctness/useExhaustiveDependencies: + const func = useCallback( + async (ctx: ReactlitContext) => { + return children( + (plugins ?? []).reduce( + (acc, plugin) => Object.assign({}, acc, plugin(ctx)), + // biome-ignore lint/suspicious/noExplicitAny: + ctx as any, + ), + ); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [children, ...(plugins ?? [])], + ); + return {func}; + } + return CustomReactlit; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [...plugins]); } diff --git a/libs/core/src/index.ts b/libs/core/src/index.ts index 76fd3bb..5004dd9 100644 --- a/libs/core/src/index.ts +++ b/libs/core/src/index.ts @@ -1,19 +1,19 @@ -export * from './reactlit'; -export * from './builtins/types'; -export { defineView, defineTransformView } from './builtins/view'; -export * from './hooks/use-reactlit'; -export * from './hooks/use-reactlit-state'; -export * from './wrappers'; -export * from './inputs/layout.view'; -export * from './plugins/data-fetching'; +export * from "./reactlit"; +export * from "./builtins/types"; +export { defineView, defineTransformView } from "./builtins/view"; +export * from "./hooks/use-reactlit"; +export * from "./hooks/use-reactlit-state"; +export * from "./wrappers"; +export * from "./inputs/layout.view"; +export * from "./plugins/data-fetching"; // see https://github.com/mdx-js/mdx/issues/2487 -import type { JSX as Jsx } from 'react/jsx-runtime'; +import type { JSX as Jsx } from "react/jsx-runtime"; declare global { - namespace JSX { - type ElementClass = Jsx.ElementClass; - type Element = Jsx.Element; - type IntrinsicElements = Jsx.IntrinsicElements; - } + namespace JSX { + type ElementClass = Jsx.ElementClass; + type Element = Jsx.Element; + type IntrinsicElements = Jsx.IntrinsicElements; + } } diff --git a/libs/core/src/inputs/layout.view.tsx b/libs/core/src/inputs/layout.view.tsx index 6584844..87c56c9 100644 --- a/libs/core/src/inputs/layout.view.tsx +++ b/libs/core/src/inputs/layout.view.tsx @@ -1,139 +1,141 @@ -import { Fragment, useEffect, useRef } from 'react'; -import { normalizeDisplayArgs } from '../builtins/display'; +import { Fragment, useEffect, useRef } from "react"; +import { normalizeDisplayArgs } from "../builtins/display"; import { - ReactlitContext, - StateBase, - ViewComponentProps, -} from '../builtins/types'; + ReactlitContext, + StateBase, + ViewComponentProps, +} from "../builtins/types"; import { - defineTransformView, - normalizeViewArgs, - ViewArgs, -} from '../builtins/view'; -import tunnel from '../utils/tunnel'; -import { applySimpleWrapper, SimpleWrapper, Wrapper } from '../wrappers'; + ViewArgs, + defineTransformView, + normalizeViewArgs, +} from "../builtins/view"; +import tunnel from "../utils/tunnel"; +import { SimpleWrapper, Wrapper, applySimpleWrapper } from "../wrappers"; export type Tunnel = ReturnType; export type Repeat< - T, - C extends number, - Result extends T[] = [], - Counter extends any[] = [] -> = Counter['length'] extends C - ? Result - : Repeat; + T, + C extends number, + Result extends T[] = [], + // biome-ignore lint/suspicious/noExplicitAny: + Counter extends any[] = [], +> = Counter["length"] extends C + ? Result + : Repeat; export type LayoutSlot = Pick< - ReactlitContext, - 'display' | 'view' + ReactlitContext, + "display" | "view" >; // during initialization we create empty layout slots, these are only temporary // until the state gets set up export function createEmptyLayoutSlot< - T extends StateBase = StateBase + T extends StateBase = StateBase, >(): LayoutSlot { - return { - display: () => <>, - view(...args: ViewArgs) { - return undefined as R; - }, - }; + return { + display: () => <>, + view(...args: ViewArgs) { + return undefined as R; + }, + }; } export function createLayoutSlot( - ctx: Pick, 'display' | 'view'>, - t: ReturnType + ctx: Pick, "display" | "view">, + t: ReturnType, ): LayoutSlot { - const TunnelWrapper: Wrapper = ({ stateKey, position, children }) => { - return ( - - {children} - - ); - }; - return { - display(...args) { - const { manualKey, wrappers, node } = normalizeDisplayArgs(args); - if (manualKey) { - ctx.display(manualKey, TunnelWrapper, ...wrappers, node); - } else { - ctx.display(TunnelWrapper, ...wrappers, node); - } - }, - view(...args: ViewArgs) { - const { key, wrappers, def } = normalizeViewArgs(args); - return ctx.view(key, TunnelWrapper, ...wrappers, def); - }, - }; + const TunnelWrapper: Wrapper = ({ stateKey, position, children }) => { + return ( + + {children} + + ); + }; + return { + display(...args) { + const { manualKey, wrappers, node } = normalizeDisplayArgs(args); + if (manualKey) { + ctx.display(manualKey, TunnelWrapper, ...wrappers, node); + } else { + ctx.display(TunnelWrapper, ...wrappers, node); + } + }, + view(...args: ViewArgs) { + const { key, wrappers, def } = normalizeViewArgs(args); + return ctx.view(key, TunnelWrapper, ...wrappers, def); + }, + }; } export function LayoutViewComponent({ - slots, - value, - setValue, - slotWrapper, + slots, + value, + setValue, + slotWrapper, }: { - slots: N; - slotWrapper?: SimpleWrapper; + slots: N; + slotWrapper?: SimpleWrapper; } & ViewComponentProps>) { - const tunnels = useRef([]); - useEffect(() => { - if (tunnels.current.length !== slots) { - tunnels.current = Array.from({ length: slots }, () => tunnel()); - setValue(tunnels.current as Repeat); - } - }, [slots, setValue]); - if (!value) return null; - return ( - <> - {(value as Tunnel[]) - .map((t) => t.Out) - .map((Slot, index) => ( - - {applySimpleWrapper(, slotWrapper)} - - ))} - - ); + const tunnels = useRef([]); + useEffect(() => { + if (tunnels.current.length !== slots) { + tunnels.current = Array.from({ length: slots }, () => tunnel()); + setValue(tunnels.current as Repeat); + } + }, [slots, setValue]); + if (!value) return null; + return ( + <> + {(value as Tunnel[]) + .map((t) => t.Out) + .map((Slot, index) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: + + {applySimpleWrapper(, slotWrapper)} + + ))} + + ); } export type LayoutViewType = Repeat | undefined; export function defaultLayoutState( - slots: N + slots: N, ): LayoutViewType { - return undefined; + return undefined; } export function LayoutView( - slots: N, - slotWrapper?: SimpleWrapper + slots: N, + slotWrapper?: SimpleWrapper, ) { - return defineTransformView< - Repeat | undefined, - Repeat, N> - >( - (viewProps) => ( - - ), - ({ value, display, view }) => { - const tunnels = (value ?? []) as Tunnel[]; - if (tunnels.length !== slots) { - return Array.from({ length: slots }, createEmptyLayoutSlot) as Repeat< - LayoutSlot, - N - >; - } - const subContext = tunnels.map((t) => - createLayoutSlot({ display, view }, t) - ); - return subContext as Repeat, N>; - } - ); + return defineTransformView< + Repeat | undefined, + Repeat, N> + >( + (viewProps) => ( + + ), + ({ value, display, view }) => { + const tunnels = (value ?? []) as Tunnel[]; + if (tunnels.length !== slots) { + return Array.from({ length: slots }, createEmptyLayoutSlot) as Repeat< + LayoutSlot, + N + >; + } + const subContext = tunnels.map((t) => + createLayoutSlot({ display, view }, t), + ); + return subContext as Repeat, N>; + }, + ); } diff --git a/libs/core/src/plugins/data-fetching.ts b/libs/core/src/plugins/data-fetching.ts index 2c75b0b..87b92ad 100644 --- a/libs/core/src/plugins/data-fetching.ts +++ b/libs/core/src/plugins/data-fetching.ts @@ -1,102 +1,102 @@ import { - EnsureQueryDataOptions, - QueryClient, - QueryClientConfig, - QueryKey, - QueryObserver, - Updater, -} from '@tanstack/react-query'; -import { definePlugin, ReactlitPlugin } from '../hooks/use-reactlit'; + EnsureQueryDataOptions, + QueryClient, + QueryClientConfig, + QueryKey, + QueryObserver, + Updater, +} from "@tanstack/react-query"; +import { ReactlitPlugin, definePlugin } from "../hooks/use-reactlit"; export class DataFetcher { - constructor( - public client: QueryClient, - private trigger: () => void, - private key: QueryKey, - private fn: () => Promise, - private options?: Partial< - EnsureQueryDataOptions - > - ) {} + constructor( + public client: QueryClient, + private trigger: () => void, + private key: QueryKey, + private fn: () => Promise, + private options?: Partial< + EnsureQueryDataOptions + >, + ) {} - get() { - if (this.key.some((k) => k === undefined)) { - // eslint-disable-next-line no-console - console.warn( - 'One or more of the data fetching keys are undefined, this can result in unexpected behavior' - ); - } - const state = this.client.getQueryState(this.key); - if (state?.status === 'error') { - throw state.error; - } - this.client.ensureQueryData({ - queryKey: this.key, - queryFn: async () => { - // trigger before so we can see the loading state - this.trigger(); - return this.fn(); - }, - ...this.options, - }); - return this.client.getQueryData(this.key) as T; - } + get() { + if (this.key.some((k) => k === undefined)) { + // eslint-disable-next-line no-console + console.warn( + "One or more of the data fetching keys are undefined, this can result in unexpected behavior", + ); + } + const state = this.client.getQueryState(this.key); + if (state?.status === "error") { + throw state.error; + } + this.client.ensureQueryData({ + queryKey: this.key, + queryFn: async () => { + // trigger before so we can see the loading state + this.trigger(); + return this.fn(); + }, + ...this.options, + }); + return this.client.getQueryData(this.key) as T; + } - update(updater: Updater) { - this.client.setQueryData(this.key, updater); - this.trigger(); - } + update(updater: Updater) { + this.client.setQueryData(this.key, updater); + this.trigger(); + } - async refetch() { - await this.client.invalidateQueries({ - queryKey: this.key, - refetchType: 'all', - }); - this.trigger(); - } + async refetch() { + await this.client.invalidateQueries({ + queryKey: this.key, + refetchType: "all", + }); + this.trigger(); + } - isFetching() { - const state = this.client.getQueryState(this.key); - return ( - !state || state?.status === 'pending' || state?.fetchStatus === 'fetching' - ); - } + isFetching() { + const state = this.client.getQueryState(this.key); + return ( + !state || state?.status === "pending" || state?.fetchStatus === "fetching" + ); + } } export type DataFetchingContext = { - fetcher( - key: QueryKey, - fn: () => Promise, - options?: Partial> - ): DataFetcher; + fetcher( + key: QueryKey, + fn: () => Promise, + options?: Partial>, + ): DataFetcher; }; export function makeDataFetchingPlugin( - config?: QueryClientConfig + config?: QueryClientConfig, ): ReactlitPlugin { - const client = new QueryClient(config); - let isMounted = false; - return definePlugin((ctx) => { - if (!isMounted) { - client.mount(); - const observer = new QueryObserver(client, { - queryKey: [], - }); - observer.subscribe(() => { - ctx.trigger(); - }); - isMounted = true; - } - return { - fetcher( - key: QueryKey, - fn: () => Promise, - options?: Partial> - ) { - return new DataFetcher(client, ctx.trigger, key, fn, options); - }, - }; - }); + const client = new QueryClient(config); + let isMounted = false; + return definePlugin((ctx) => { + if (!isMounted) { + client.mount(); + const observer = new QueryObserver(client, { + queryKey: [], + }); + observer.subscribe(() => { + ctx.trigger(); + }); + isMounted = true; + } + return { + fetcher( + key: QueryKey, + fn: () => Promise, + options?: Partial>, + ) { + return new DataFetcher(client, ctx.trigger, key, fn, options); + }, + }; + }); } export const DataFetchingPlugin = makeDataFetchingPlugin(); diff --git a/libs/core/src/reactlit.spec.tsx b/libs/core/src/reactlit.spec.tsx index 3611a8e..b299579 100644 --- a/libs/core/src/reactlit.spec.tsx +++ b/libs/core/src/reactlit.spec.tsx @@ -1,32 +1,31 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import '@testing-library/jest-dom'; -import { defineView, Reactlit } from '.'; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Reactlit, defineView } from "."; -export const TextInput = defineView(({ value, setValue, stateKey }) => ( - setValue(e.target.value)} - placeholder={`Enter ${stateKey}`} - /> +const TextInput = defineView(({ value, setValue, stateKey }) => ( + setValue(e.target.value)} + placeholder={`Enter ${stateKey}`} + /> )); -test('hello world reactlit', async () => { - render( - - {async ({ display, view }) => { - const name = view('name', TextInput); - if (!name) { - throw new Error('Name is required'); - } - display(

Hello {name}
); - }} - - ); - expect(await screen.findByText('Name is required')).toBeVisible(); - await userEvent.type(screen.getByPlaceholderText('Enter name'), 'World'); - expect(await screen.findByText('Hello World')).toBeVisible(); +test("hello world reactlit", async () => { + render( + + {async ({ display, view }) => { + const name = view("name", TextInput); + if (!name) { + throw new Error("Name is required"); + } + display(
Hello {name}
); + }} +
, + ); + expect(await screen.findByText("Name is required")).toBeVisible(); + await userEvent.type(screen.getByPlaceholderText("Enter name"), "World"); + expect(await screen.findByText("Hello World")).toBeVisible(); }); diff --git a/libs/core/src/reactlit.tsx b/libs/core/src/reactlit.tsx index 08c0ff1..4a4adf3 100644 --- a/libs/core/src/reactlit.tsx +++ b/libs/core/src/reactlit.tsx @@ -1,114 +1,107 @@ -import { - Fragment, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { Fragment, useCallback, useMemo, useRef, useState } from "react"; -import { useReactlitChanged } from './builtins/changed'; -import { useReactlitDisplay } from './builtins/display'; -import { useInternalReactlitState } from './builtins/internal-state'; -import { useReactlitSet } from './builtins/set'; -import { ReactlitProps, StateBase } from './builtins/types'; -import { useReactlitView } from './builtins/view'; -import { useReactlitState } from './hooks/use-reactlit-state'; -import { useIsomorphicLayoutEffect } from './utils/use-isomorphic-layout-effect'; -import { uniqueBy } from './utils/unique-by'; +import { useReactlitChanged } from "./builtins/changed"; +import { useReactlitDisplay } from "./builtins/display"; +import { useInternalReactlitState } from "./builtins/internal-state"; +import { useReactlitSet } from "./builtins/set"; +import { ReactlitProps, StateBase } from "./builtins/types"; +import { useReactlitView } from "./builtins/view"; +import { useReactlitState } from "./hooks/use-reactlit-state"; +import { uniqueBy } from "./utils/unique-by"; +import { useIsomorphicLayoutEffect } from "./utils/use-isomorphic-layout-effect"; const defaultRenderError = ({ error }) => ( -
- {'message' in error ? error.message : `${error}`} -
+
+ {"message" in error ? error.message : `${error}`} +
); +// biome-ignore lint/suspicious/noExplicitAny: export function Reactlit({ - state: rawState, - setState, - renderLoading, - renderError = defaultRenderError, - debug, - children, + state: rawState, + setState, + renderLoading, + renderError = defaultRenderError, + debug, + children, }: ReactlitProps) { - const [defaultRawState, defaultSetState] = useReactlitState({} as T); - rawState = rawState ?? defaultRawState; - setState = setState ?? defaultSetState; - const internalState = useInternalReactlitState(rawState, setState); + const [defaultRawState, defaultSetState] = useReactlitState({} as T); + rawState = rawState ?? defaultRawState; + setState = setState ?? defaultSetState; + const internalState = useInternalReactlitState(rawState, setState); - const { state } = internalState; - const set = useReactlitSet(internalState); - const changed = useReactlitChanged(internalState, debug); - const { renderState, resetRenderPosition, finalizeRender, display } = - useReactlitDisplay({ renderError }); - const view = useReactlitView({ set, display, state }); + const { state } = internalState; + const set = useReactlitSet(internalState); + const changed = useReactlitChanged(internalState, debug); + const { renderState, resetRenderPosition, finalizeRender, display } = + useReactlitDisplay({ renderError }); + const view = useReactlitView({ set, display, state }); - const [triggerCounter, setTriggerCounter] = useState(0); - const trigger = useCallback(() => { - setTriggerCounter((c) => c + 1); - }, [setTriggerCounter]); + const [triggerCounter, setTriggerCounter] = useState(0); + const trigger = useCallback(() => { + setTriggerCounter((c) => c + 1); + }, []); - const childArgs = useMemo( - () => ({ state, changed, set, view, display, trigger }), - [state, changed, set, view, display, trigger] - ); + const childArgs = useMemo( + () => ({ state, changed, set, view, display, trigger }), + [state, changed, set, view, display, trigger], + ); - const [rendering, setRendering] = useState(false); + const [rendering, setRendering] = useState(false); - const renderLock = useRef(false); - const renderAfter = useRef(false); - useIsomorphicLayoutEffect(() => { - async function runScript() { - setRendering(true); - if (renderLock.current) { - renderAfter.current = true; - return; - } - renderLock.current = true; - try { - // eslint-disable-next-line no-console - debug && console.debug('reactlit rendering:', childArgs.state); - resetRenderPosition(); - await children(childArgs); - } catch (e: any) { - // eslint-disable-next-line no-console - debug && console.error(e); - display( - renderError?.({ error: e, resetErrorBoundary: () => trigger() }) ?? ( - <> - ) - ); - } finally { - finalizeRender(); - renderLock.current = false; - if (renderAfter.current) { - renderAfter.current = false; - trigger(); - } else { - setRendering(false); - } - } - } - runScript(); - }, [ - children, - childArgs, - triggerCounter, - trigger, - display, - setRendering, - resetRenderPosition, - finalizeRender, - renderError, - debug, - ]); + const renderLock = useRef(false); + const renderAfter = useRef(false); + useIsomorphicLayoutEffect(() => { + async function runScript() { + setRendering(true); + if (renderLock.current) { + renderAfter.current = true; + return; + } + renderLock.current = true; + try { + // eslint-disable-next-line no-console + debug && console.debug("reactlit rendering:", childArgs.state); + resetRenderPosition(); + await children(childArgs); + } catch (e: unknown) { + // eslint-disable-next-line no-console + debug && console.error(e); + display( + renderError?.({ error: e, resetErrorBoundary: () => trigger() }) ?? + null, + ); + } finally { + finalizeRender(); + renderLock.current = false; + if (renderAfter.current) { + renderAfter.current = false; + trigger(); + } else { + setRendering(false); + } + } + } + runScript(); + }, [ + children, + childArgs, + triggerCounter, + trigger, + display, + setRendering, + resetRenderPosition, + finalizeRender, + renderError, + debug, + ]); - return ( - <> - {uniqueBy(renderState.elements, '0').map(([key, node]) => ( - {node} - ))} - {renderLoading?.(rendering) ?? <>} - - ); + return ( + <> + {uniqueBy(renderState.elements, "0").map(([key, node]) => ( + {node} + ))} + {renderLoading?.(rendering) ?? null} + + ); } diff --git a/libs/core/src/utils/deep-equal.ts b/libs/core/src/utils/deep-equal.ts index 104e0c2..dfcfb4d 100644 --- a/libs/core/src/utils/deep-equal.ts +++ b/libs/core/src/utils/deep-equal.ts @@ -1,3 +1,3 @@ -import equal from 'fast-deep-equal'; +import equal from "fast-deep-equal"; export const deepEqual = equal; diff --git a/libs/core/src/utils/tail.ts b/libs/core/src/utils/tail.ts index c622e6a..39d6fa2 100644 --- a/libs/core/src/utils/tail.ts +++ b/libs/core/src/utils/tail.ts @@ -1,3 +1,4 @@ +// biome-ignore lint/suspicious/noExplicitAny: export function tail(args: [...T, U]): [T, U] { - return [args.slice(0, -1) as T, args.at(-1) as U]; + return [args.slice(0, -1) as T, args.at(-1) as U]; } diff --git a/libs/core/src/utils/tunnel.tsx b/libs/core/src/utils/tunnel.tsx index 8c1a603..6ef2f89 100644 --- a/libs/core/src/utils/tunnel.tsx +++ b/libs/core/src/utils/tunnel.tsx @@ -1,83 +1,83 @@ -import React, { Fragment } from 'react'; -import { create, StoreApi } from 'zustand'; -import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect'; -import { uniqueBy } from './unique-by'; +import React, { Fragment } from "react"; +import { StoreApi, create } from "zustand"; +import { uniqueBy } from "./unique-by"; +import { useIsomorphicLayoutEffect } from "./use-isomorphic-layout-effect"; // modified from tunnel-rat type Props = { childKey: string; order: number; children: React.ReactNode }; type State = { - current: Array; - version: number; - set: StoreApi['setState']; + current: Array; + version: number; + set: StoreApi["setState"]; }; function sortByOrder(array: Props[]) { - return array.sort((a, b) => a.order - b.order); + return array.sort((a, b) => a.order - b.order); } export default function tunnel() { - const useStore = create((set) => ({ - current: new Array(), - version: 0, - set, - })); + const useStore = create((set) => ({ + current: new Array(), + version: 0, + set, + })); - return { - In: ({ childKey, order, children }: Props) => { - const set = useStore((state) => state.set); - const version = useStore((state) => state.version); + return { + In: ({ childKey, order, children }: Props) => { + const set = useStore((state) => state.set); + const version = useStore((state) => state.version); - /* When this component mounts, we increase the store's version number. + /* When this component mounts, we increase the store's version number. This will cause all existing rats to re-render (just like if the Out component were mapping items to a list.) The re-rendering will cause the final order of rendered components to match what the user is expecting. */ - useIsomorphicLayoutEffect(() => { - set((state) => ({ - version: state.version + 1, - })); - }, []); + useIsomorphicLayoutEffect(() => { + set((state) => ({ + version: state.version + 1, + })); + }, []); - /* Any time the children _or_ the store's version number change, insert + /* Any time the children _or_ the store's version number change, insert the specified React children into the list of rats. */ - useIsomorphicLayoutEffect(() => { - set(({ current }) => { - const existing = current.findIndex((c) => c.childKey === childKey); - return { - current: sortByOrder( - existing !== -1 - ? [ - ...current.slice(0, existing), - { childKey, order, children }, - ...current.slice(existing + 1), - ] - : [...current, { childKey, order, children }] - ), - }; - }); + useIsomorphicLayoutEffect(() => { + set(({ current }) => { + const existing = current.findIndex((c) => c.childKey === childKey); + return { + current: sortByOrder( + existing !== -1 + ? [ + ...current.slice(0, existing), + { childKey, order, children }, + ...current.slice(existing + 1), + ] + : [...current, { childKey, order, children }], + ), + }; + }); - // remove the cleanup logic so that nodes stay in position, the key logic keeps things from getting too messy - return () => - set(({ current }) => { - return { - current: current.filter((c) => c.childKey !== childKey), - }; - }); - }, [children, version]); + // remove the cleanup logic so that nodes stay in position, the key logic keeps things from getting too messy + return () => + set(({ current }) => { + return { + current: current.filter((c) => c.childKey !== childKey), + }; + }); + }, [children, version]); - return null; - }, + return null; + }, - Out: () => { - const current = useStore((state) => state.current); - return ( - <> - {uniqueBy(current, 'childKey').map((c) => ( - {c.children} - ))} - - ); - }, - }; + Out: () => { + const current = useStore((state) => state.current); + return ( + <> + {uniqueBy(current, "childKey").map((c) => ( + {c.children} + ))} + + ); + }, + }; } diff --git a/libs/core/src/utils/unique-by.ts b/libs/core/src/utils/unique-by.ts index f3ce6e2..067be45 100644 --- a/libs/core/src/utils/unique-by.ts +++ b/libs/core/src/utils/unique-by.ts @@ -1,5 +1,6 @@ export function uniqueBy(array: T[], key: keyof T) { - return array.filter( - (item, index, self) => index === self.findIndex((t) => t[key] === item[key]) - ); + return array.filter( + (item, index, self) => + index === self.findIndex((t) => t[key] === item[key]), + ); } diff --git a/libs/core/src/utils/use-isomorphic-layout-effect.tsx b/libs/core/src/utils/use-isomorphic-layout-effect.tsx index 00fb045..f8e46e2 100644 --- a/libs/core/src/utils/use-isomorphic-layout-effect.tsx +++ b/libs/core/src/utils/use-isomorphic-layout-effect.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React from "react"; // taken from tunnel-rat @@ -12,14 +12,16 @@ import React from 'react'; * @see https://github.com/facebook/react/issues/14927 */ export const useIsomorphicLayoutEffect = - typeof window !== 'undefined' && - (window.document?.createElement || - window.navigator?.product === 'ReactNative') - ? React.useLayoutEffect - : React.useEffect; + typeof window !== "undefined" && + (window.document?.createElement || + window.navigator?.product === "ReactNative") + ? React.useLayoutEffect + : React.useEffect; export function useMutableCallback(fn: T) { - const ref = React.useRef(fn); - useIsomorphicLayoutEffect(() => void (ref.current = fn), [fn]); - return ref; + const ref = React.useRef(fn); + useIsomorphicLayoutEffect(() => { + ref.current = fn; + }, [fn]); + return ref; } diff --git a/libs/core/src/wrappers.tsx b/libs/core/src/wrappers.tsx index 7eff308..8704402 100644 --- a/libs/core/src/wrappers.tsx +++ b/libs/core/src/wrappers.tsx @@ -1,20 +1,20 @@ import { - cloneElement, - createElement, - Fragment, - isValidElement, - PropsWithChildren, - ReactElement, - ReactNode, -} from 'react'; + Fragment, + PropsWithChildren, + ReactElement, + ReactNode, + cloneElement, + createElement, + isValidElement, +} from "react"; export interface ReactlitWrapperProps { - position: number; - stateKey: string; + position: number; + stateKey: string; } export type WrapperComponent = React.FC< - PropsWithChildren + PropsWithChildren >; export type Wrapper = WrapperComponent | React.ReactElement; @@ -24,42 +24,43 @@ export type SimpleWrapperComponent = React.FC; export type SimpleWrapper = SimpleWrapperComponent | React.ReactElement; export function applySimpleWrapper( - children: ReactElement, - Wrap?: SimpleWrapper + children: ReactElement, + Wrap?: SimpleWrapper, ): ReactNode { - if (!Wrap) return children; - if (isValidElement(Wrap)) return cloneElement(Wrap, {}, children); - return Wrap({ children }); + if (!Wrap) return children; + if (isValidElement(Wrap)) return cloneElement(Wrap, {}, children); + return Wrap({ children }); } export function applyWrapper( - children: ReactElement, - Wrap?: Wrapper, - props?: ReactlitWrapperProps + children: ReactElement, + Wrap?: Wrapper, + props?: ReactlitWrapperProps, ): ReactElement { - if (!Wrap) return children; - if (isValidElement(Wrap)) return cloneElement(Wrap, {}, children); - return Wrap({ children, ...props }) as ReactElement; - // return createElement(Wrap, props, children); + if (!Wrap) return children; + if (isValidElement(Wrap)) return cloneElement(Wrap, {}, children); + return Wrap({ children, ...props }) as ReactElement; + // return createElement(Wrap, props, children); } export function ApplyWrappers({ - wrappers, - children, - ...props + wrappers, + children, + ...props }: { - wrappers: Wrapper[]; - children: ReactNode; + wrappers: Wrapper[]; + children: ReactNode; } & ReactlitWrapperProps) { - const base = [...wrappers]; - const wrappedContent = base.reverse().reduce( - (acc, W) => applyWrapper(acc, W, props), - // this extra Fragment wrapper at the end is necessary for some - // very mysterious reason to keep plain string nodes from shifting positions around - {children} - ); - return wrappedContent; + const base = [...wrappers]; + const wrappedContent = base.reverse().reduce( + (acc, W) => applyWrapper(acc, W, props), + // this extra Fragment wrapper at the end is necessary for some + // very mysterious reason to keep plain string nodes from shifting positions around + // biome-ignore lint/complexity/noUselessFragments: + {children}, + ); + return wrappedContent; } export const FragmentWrapper: Wrapper = ({ children, stateKey }) => { - return {children}; + return {children}; }; diff --git a/libs/core/test-setup.ts b/libs/core/test-setup.ts new file mode 100644 index 0000000..8f57a47 --- /dev/null +++ b/libs/core/test-setup.ts @@ -0,0 +1,9 @@ +import * as matchers from "@testing-library/jest-dom/matchers"; +import { cleanup } from "@testing-library/react"; +import { afterEach, expect } from "vitest"; + +expect.extend(matchers); + +afterEach(() => { + cleanup(); +}); diff --git a/libs/core/tsconfig.build.json b/libs/core/tsconfig.build.json index 9a4c066..ec832f1 100644 --- a/libs/core/tsconfig.build.json +++ b/libs/core/tsconfig.build.json @@ -1,5 +1,5 @@ { - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./tsconfig.json", - "exclude": ["node_modules", "dist", "src/**/*.spec.tsx"] + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", "src/**/*.spec.tsx"] } diff --git a/libs/core/tsconfig.json b/libs/core/tsconfig.json index 7cf44ef..bed9191 100644 --- a/libs/core/tsconfig.json +++ b/libs/core/tsconfig.json @@ -1,18 +1,18 @@ { - "$schema": "https://json.schemastore.org/tsconfig", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "jsx": "react-jsx", - "module": "ESNext", - "target": "ES6", - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "lib": ["ES2021", "DOM"], - "moduleResolution": "node", - "declaration": true, - "emitDeclarationOnly": true - }, - "exclude": ["node_modules", "dist"], - "include": ["src"] + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "jsx": "react-jsx", + "module": "ESNext", + "target": "ES6", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "lib": ["ES2021", "DOM"], + "moduleResolution": "node", + "declaration": true, + "emitDeclarationOnly": true + }, + "exclude": ["node_modules", "dist"], + "include": ["src"] } diff --git a/libs/core/tsup.config.ts b/libs/core/tsup.config.ts index 2be191f..9740c2c 100644 --- a/libs/core/tsup.config.ts +++ b/libs/core/tsup.config.ts @@ -1,13 +1,13 @@ -import { defineConfig } from 'tsup'; +import { defineConfig } from "tsup"; export default defineConfig({ - entry: ['src/index.ts'], - tsconfig: 'tsconfig.build.json', - treeshake: true, - sourcemap: 'inline', - minify: true, - clean: false, - splitting: false, - format: ['cjs', 'esm'], - external: ['react'], + entry: ["src/index.ts"], + tsconfig: "tsconfig.build.json", + treeshake: true, + sourcemap: "inline", + minify: true, + clean: false, + splitting: false, + format: ["cjs", "esm"], + external: ["react"], }); diff --git a/libs/core/vitest.config.ts b/libs/core/vitest.config.ts new file mode 100644 index 0000000..01f56e7 --- /dev/null +++ b/libs/core/vitest.config.ts @@ -0,0 +1,12 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vitest/config"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: "jsdom", + setupFiles: "./test-setup.ts", + }, +}); diff --git a/libs/radix/.eslintrc.cjs b/libs/radix/.eslintrc.cjs deleted file mode 100644 index cf8b644..0000000 --- a/libs/radix/.eslintrc.cjs +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - extends: ['next'], - plugins: ['prettier'], - rules: { - 'no-console': 'error', - 'prettier/prettier': 'warn', - '@next/next/no-html-link-for-pages': 'off', - 'react/jsx-key': 1, - }, -}; diff --git a/libs/radix/package.json b/libs/radix/package.json index 8969a4a..2c8f340 100644 --- a/libs/radix/package.json +++ b/libs/radix/package.json @@ -1,57 +1,51 @@ { - "name": "@reactlit/radix", - "description": "Radix-ui themes input components for Reactlit", - "version": "0.2.1", - "license": "MIT", - "homepage": "https://github.com/mshafir/reactlit", - "repository": "github:mshafir/reactlit", - "author": "Michael Shafir ", - "type": "module", - "main": "./dist/index.js", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - } - }, - "files": [ - "dist" - ], - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, - "scripts": { - "prebuild": "cp ../../README.md .", - "build": "tsup && tsc", - "dev": "tsup --dts --watch", - "clean": "rm -rf node_modules && rm -rf .turbo && rm -rf dist", - "lint": "eslint src/**/*.ts* --fix" - }, - "dependencies": { - "@radix-ui/themes": "^3.1.6", - "@reactlit/core": "workspace:*", - "clsx": "^2.1.1", - "fuse.js": "^7.0.0", - "lucide-react": "^0.468.0", - "tailwind-merge": "^2.5.5", - "use-debounce": "^10.0.4" - }, - "devDependencies": { - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "eslint": "^7.23.0", - "eslint-config-next": "^12.0.8", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-react": "7.28.0", - "prettier": "^2.8.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "tsup": "^8.3.5", - "typescript": "latest" - }, - "peerDependencies": { - "react": ">=17 <=19", - "react-dom": ">=17 <=19" - } -} \ No newline at end of file + "name": "@reactlit/radix", + "description": "Radix-ui themes input components for Reactlit", + "version": "0.2.1", + "license": "MIT", + "homepage": "https://github.com/mshafir/reactlit", + "repository": "github:mshafir/reactlit", + "author": "Michael Shafir ", + "type": "module", + "main": "./dist/index.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "files": ["dist"], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "scripts": { + "prebuild": "cp ../../README.md .", + "build": "tsup && tsc", + "dev": "tsup --dts --watch", + "clean": "rm -rf node_modules && rm -rf .turbo && rm -rf dist", + "lint": "biome check --write" + }, + "dependencies": { + "@radix-ui/themes": "^3.1.6", + "@reactlit/core": "workspace:*", + "clsx": "^2.1.1", + "fuse.js": "^7.0.0", + "lucide-react": "^0.468.0", + "tailwind-merge": "^2.5.5", + "use-debounce": "^10.0.4" + }, + "devDependencies": { + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.2", + "@biomejs/biome": "1.9.4", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tsup": "^8.3.5", + "typescript": "*" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + } +} diff --git a/libs/radix/src/index.ts b/libs/radix/src/index.ts index ff151d2..63e5e0f 100644 --- a/libs/radix/src/index.ts +++ b/libs/radix/src/index.ts @@ -1,13 +1,13 @@ -export * from './inputs'; -export * from './inputs/async-button.input'; -export * from './inputs/table.input'; -export * from './inputs/search.input'; -export * from './inputs/slider.input'; -export * from './inputs/check.input'; -export * from './inputs/radio.input'; -export * from './inputs/select.input'; -export * from './inputs/text.input'; -export * from './inputs/textarea.input'; -export * from './inputs/switch.input'; -export * from './radix-wrapper'; -export * from './label'; +export * from "./inputs"; +export * from "./inputs/async-button.input"; +export * from "./inputs/table.input"; +export * from "./inputs/search.input"; +export * from "./inputs/slider.input"; +export * from "./inputs/check.input"; +export * from "./inputs/radio.input"; +export * from "./inputs/select.input"; +export * from "./inputs/text.input"; +export * from "./inputs/textarea.input"; +export * from "./inputs/switch.input"; +export * from "./radix-wrapper"; +export * from "./label"; diff --git a/libs/radix/src/inputs.ts b/libs/radix/src/inputs.ts index 040790f..6089e81 100644 --- a/libs/radix/src/inputs.ts +++ b/libs/radix/src/inputs.ts @@ -1,24 +1,24 @@ -import { AsyncButton } from './inputs/async-button.input'; -import { CheckInput } from './inputs/check.input'; -import { RadioInput } from './inputs/radio.input'; -import { SearchInput } from './inputs/search.input'; -import { SelectInput } from './inputs/select.input'; -import { RangeSliderInput, SliderInput } from './inputs/slider.input'; -import { SwitchInput } from './inputs/switch.input'; -import { TableInput } from './inputs/table.input'; -import { TextInput } from './inputs/text.input'; -import { TextAreaInput } from './inputs/textarea.input'; +import { AsyncButton } from "./inputs/async-button.input"; +import { CheckInput } from "./inputs/check.input"; +import { RadioInput } from "./inputs/radio.input"; +import { SearchInput } from "./inputs/search.input"; +import { SelectInput } from "./inputs/select.input"; +import { RangeSliderInput, SliderInput } from "./inputs/slider.input"; +import { SwitchInput } from "./inputs/switch.input"; +import { TableInput } from "./inputs/table.input"; +import { TextInput } from "./inputs/text.input"; +import { TextAreaInput } from "./inputs/textarea.input"; export const Inputs = { - Text: TextInput, - TextArea: TextAreaInput, - Check: CheckInput, - Switch: SwitchInput, - Radio: RadioInput, - Select: SelectInput, - Slider: SliderInput, - RangeSlider: RangeSliderInput, - AsyncButton: AsyncButton, - Table: TableInput, - Search: SearchInput, + Text: TextInput, + TextArea: TextAreaInput, + Check: CheckInput, + Switch: SwitchInput, + Radio: RadioInput, + Select: SelectInput, + Slider: SliderInput, + RangeSlider: RangeSliderInput, + AsyncButton: AsyncButton, + Table: TableInput, + Search: SearchInput, }; diff --git a/libs/radix/src/inputs/async-button.input.tsx b/libs/radix/src/inputs/async-button.input.tsx index 0df7c72..6e765e0 100644 --- a/libs/radix/src/inputs/async-button.input.tsx +++ b/libs/radix/src/inputs/async-button.input.tsx @@ -1,49 +1,49 @@ -import { Button, ButtonProps } from '@radix-ui/themes'; -import { defineView, ViewComponentProps } from '@reactlit/core'; +import { Button, ButtonProps } from "@radix-ui/themes"; +import { ViewComponentProps, defineView } from "@reactlit/core"; export interface AsyncButtonInputProps - extends Omit< - ButtonProps, - 'loading' | 'children' | 'onClick' | 'content' | 'value' - > { - content: React.ReactNode; - onClick: () => Promise; + extends Omit< + ButtonProps, + "loading" | "children" | "onClick" | "content" | "value" + > { + content: React.ReactNode; + onClick: () => Promise; } export const AsyncButtonViewComponent = ({ - content, - onClick, - setValue, - value, - stateKey, - display, - view, - ...props + content, + onClick, + setValue, + value, + stateKey, + display, + view, + ...props }: AsyncButtonInputProps & ViewComponentProps) => { - return ( -
- -
- ); + return ( +
+ +
+ ); }; export const AsyncButton = ( - onClick: () => Promise, - props: Omit + onClick: () => Promise, + props: Omit, ) => - defineView((viewProps) => ( - - )); + defineView((viewProps) => ( + + )); diff --git a/libs/radix/src/inputs/check.input.tsx b/libs/radix/src/inputs/check.input.tsx index ca03fbf..26dd347 100644 --- a/libs/radix/src/inputs/check.input.tsx +++ b/libs/radix/src/inputs/check.input.tsx @@ -1,51 +1,52 @@ -import { CheckboxGroup } from '@radix-ui/themes'; -import { defineView, ViewComponentProps } from '@reactlit/core'; -import { useMemo } from 'react'; -import { LabelType } from '../label'; +import { CheckboxGroup } from "@radix-ui/themes"; +import { ViewComponentProps, defineView } from "@reactlit/core"; +import { useMemo } from "react"; +import { LabelType } from "../label"; export type CheckOptionsType = T[] | Record; export type BaseCheckInputProps = Omit< - CheckboxGroup.RootProps, - 'value' | 'onValueChange' + CheckboxGroup.RootProps, + "value" | "onValueChange" > & { - label?: LabelType; - options: CheckOptionsType; + label?: LabelType; + options: CheckOptionsType; }; export const CheckInputComponent = ({ - value, - stateKey, - setValue, - label, - options, - display, - view, - ...props + value, + stateKey, + setValue, + label, + options, + display, + view, + ...props }: BaseCheckInputProps & ViewComponentProps) => { - const optionsEntries = useMemo(() => { - if (Array.isArray(options)) { - return options.map((o) => [o, o] as [string, T]); - } - return Object.entries(options) as [string, T][]; - }, [options]); - return ( - - {optionsEntries.map(([label, value], i) => ( - - {label} - - ))} - - ); + const optionsEntries = useMemo(() => { + if (Array.isArray(options)) { + return options.map((o) => [o, o] as [string, T]); + } + return Object.entries(options) as [string, T][]; + }, [options]); + return ( + + {optionsEntries.map(([label, value], i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: + + {label} + + ))} + + ); }; export type CheckInputProps = BaseCheckInputProps; export const CheckInput = ( - options: CheckOptionsType, - props?: Omit, 'options'> + options: CheckOptionsType, + props?: Omit, "options">, ) => - defineView((viewProps) => ( - - )); + defineView((viewProps) => ( + + )); diff --git a/libs/radix/src/inputs/radio.input.tsx b/libs/radix/src/inputs/radio.input.tsx index 171a2dc..1f4975b 100644 --- a/libs/radix/src/inputs/radio.input.tsx +++ b/libs/radix/src/inputs/radio.input.tsx @@ -1,55 +1,56 @@ -import { RadioGroup } from '@radix-ui/themes'; -import { defineView, ViewComponentProps } from '@reactlit/core'; -import { useMemo } from 'react'; -import { LabelType } from '../label'; +import { RadioGroup } from "@radix-ui/themes"; +import { ViewComponentProps, defineView } from "@reactlit/core"; +import { useMemo } from "react"; +import { LabelType } from "../label"; export type RadioOptionsType = T[] | Record; // Create a named type alias for the RadioGroup.RootProps type NamedRadioGroupProps = Omit< - RadioGroup.RootProps, - 'value' | 'onValueChange' + RadioGroup.RootProps, + "value" | "onValueChange" >; // Update the BaseRadioInputProps to use the named type alias export type BaseRadioInputProps = NamedRadioGroupProps & { - label?: LabelType; - options: RadioOptionsType; + label?: LabelType; + options: RadioOptionsType; }; export const RadioInputComponent = ({ - value, - stateKey, - setValue, - label, - options, - display, - view, - ...props + value, + stateKey, + setValue, + label, + options, + display, + view, + ...props }: BaseRadioInputProps & ViewComponentProps) => { - const optionsEntries = useMemo(() => { - if (Array.isArray(options)) { - return options.map((o) => [o, o] as [string, T]); - } - return Object.entries(options) as [string, T][]; - }, [options]); - return ( - - {optionsEntries.map(([label, value], i) => ( - - {label} - - ))} - - ); + const optionsEntries = useMemo(() => { + if (Array.isArray(options)) { + return options.map((o) => [o, o] as [string, T]); + } + return Object.entries(options) as [string, T][]; + }, [options]); + return ( + + {optionsEntries.map(([label, value], i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: + + {label} + + ))} + + ); }; export type RadioInputProps = BaseRadioInputProps; export const RadioInput = ( - options: RadioOptionsType, - props?: Omit, 'options'> + options: RadioOptionsType, + props?: Omit, "options">, ) => - defineView((viewProps) => ( - - )); + defineView((viewProps) => ( + + )); diff --git a/libs/radix/src/inputs/search.input.tsx b/libs/radix/src/inputs/search.input.tsx index dead533..00cebf3 100644 --- a/libs/radix/src/inputs/search.input.tsx +++ b/libs/radix/src/inputs/search.input.tsx @@ -1,47 +1,47 @@ -import { defineTransformView } from '@reactlit/core'; -import Fuse, { IFuseOptions } from 'fuse.js'; -import { TextInputComponent, TextInputProps } from './text.input'; +import { defineTransformView } from "@reactlit/core"; +import Fuse, { IFuseOptions } from "fuse.js"; +import { TextInputComponent, TextInputProps } from "./text.input"; export type SearchOptions = Partial>; -export type SearchInputProps = Omit & { - searchOptions?: SearchOptions; +export type SearchInputProps = Omit & { + searchOptions?: SearchOptions; }; export function searchData( - data: T[] | undefined, - search: string, - options?: SearchOptions + data: T[] | undefined, + search: string, + options?: SearchOptions, ) { - if (!data) { - return []; - } - if (!search) { - return data; - } - const fuse = new Fuse(data, { - isCaseSensitive: false, - shouldSort: true, - keys: Object.keys(data.at(0) ?? {}), - ...options, - }); + if (!data) { + return []; + } + if (!search) { + return data; + } + const fuse = new Fuse(data, { + isCaseSensitive: false, + shouldSort: true, + keys: Object.keys(data.at(0) ?? {}), + ...options, + }); - return fuse.search(search).map((r) => r.item); + return fuse.search(search).map((r) => r.item); } export function SearchInput( - data: T[] | undefined, - { searchOptions, ...props }: SearchInputProps + data: T[] | undefined, + { searchOptions, ...props }: SearchInputProps, ) { - return defineTransformView( - (viewProps) => ( - - {/* {({ setValue }: ViewComponentProps) => ( + return defineTransformView( + (viewProps) => ( + + {/* {({ setValue }: ViewComponentProps) => ( <> @@ -53,10 +53,10 @@ export function SearchInput( )} */} - - ), - ({ value }) => { - return searchData(data ?? [], value, searchOptions); - } - ); + + ), + ({ value }) => { + return searchData(data ?? [], value, searchOptions); + }, + ); } diff --git a/libs/radix/src/inputs/select.input.tsx b/libs/radix/src/inputs/select.input.tsx index fc96ba9..d6c530d 100644 --- a/libs/radix/src/inputs/select.input.tsx +++ b/libs/radix/src/inputs/select.input.tsx @@ -1,55 +1,56 @@ -import { Select } from '@radix-ui/themes'; -import { defineView, ViewComponentProps } from '@reactlit/core'; -import { useMemo } from 'react'; -import { LabelType } from '../label'; +import { Select } from "@radix-ui/themes"; +import { ViewComponentProps, defineView } from "@reactlit/core"; +import { useMemo } from "react"; +import { LabelType } from "../label"; export type SelectOptionsType = T[] | Record; // Create a named type alias for the SelectGroup.RootProps -type NamedSelectGroupProps = Omit; +type NamedSelectGroupProps = Omit; // Update the BaseSelectInputProps to use the named type alias export type BaseSelectInputProps = NamedSelectGroupProps & { - label?: LabelType; - options: SelectOptionsType; + label?: LabelType; + options: SelectOptionsType; }; export const SelectInputComponent = ({ - value, - stateKey, - setValue, - label, - options, - display, - view, - ...props + value, + stateKey, + setValue, + label, + options, + display, + view, + ...props }: BaseSelectInputProps & ViewComponentProps) => { - const optionsEntries = useMemo(() => { - if (Array.isArray(options)) { - return options.map((o) => [o, o] as [string, T]); - } - return Object.entries(options) as [string, T][]; - }, [options]); - return ( - - - - {optionsEntries.map(([label, value], i) => ( - - {label} - - ))} - - - ); + const optionsEntries = useMemo(() => { + if (Array.isArray(options)) { + return options.map((o) => [o, o] as [string, T]); + } + return Object.entries(options) as [string, T][]; + }, [options]); + return ( + + + + {optionsEntries.map(([label, value], i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: + + {label} + + ))} + + + ); }; export type SelectInputProps = BaseSelectInputProps; export const SelectInput = ( - options: SelectOptionsType, - props?: Omit, 'options'> + options: SelectOptionsType, + props?: Omit, "options">, ) => - defineView((viewProps) => ( - - )); + defineView((viewProps) => ( + + )); diff --git a/libs/radix/src/inputs/slider.input.tsx b/libs/radix/src/inputs/slider.input.tsx index 47c9ab3..1b08bf9 100644 --- a/libs/radix/src/inputs/slider.input.tsx +++ b/libs/radix/src/inputs/slider.input.tsx @@ -1,59 +1,59 @@ -import { Slider, SliderProps } from '@radix-ui/themes'; -import { defineView, ViewComponentProps } from '@reactlit/core'; -import { LabelType } from '../label'; +import { Slider, SliderProps } from "@radix-ui/themes"; +import { ViewComponentProps, defineView } from "@reactlit/core"; +import { LabelType } from "../label"; -export type SliderInputProps = Omit & { - label?: LabelType; +export type SliderInputProps = Omit & { + label?: LabelType; }; export const SliderInputComponent = ({ - value, - stateKey, - setValue, - onChange, - label, - display, - view, - ...props + value, + stateKey, + setValue, + onChange, + label, + display, + view, + ...props }: SliderInputProps & ViewComponentProps) => { - return ( - { - setValue(v[0]); - }} - {...props} - /> - ); + return ( + { + setValue(v[0]); + }} + {...props} + /> + ); }; export const RangeSliderInputComponent = ({ - value, - stateKey, - setValue, - onChange, - label, - display, - view, - ...props + value, + stateKey, + setValue, + onChange, + label, + display, + view, + ...props }: SliderInputProps & ViewComponentProps<[number, number]>) => { - return ( - { - setValue(v as [number, number]); - }} - {...props} - /> - ); + return ( + { + setValue(v as [number, number]); + }} + {...props} + /> + ); }; export const SliderInput = (props: SliderInputProps) => - defineView((viewProps) => ( - - )); + defineView((viewProps) => ( + + )); export const RangeSliderInput = (props: SliderInputProps) => - defineView<[number, number]>((viewProps) => ( - - )); + defineView<[number, number]>((viewProps) => ( + + )); diff --git a/libs/radix/src/inputs/switch.input.tsx b/libs/radix/src/inputs/switch.input.tsx index e24e0a2..7cfe10e 100644 --- a/libs/radix/src/inputs/switch.input.tsx +++ b/libs/radix/src/inputs/switch.input.tsx @@ -1,24 +1,24 @@ -import { Switch, SwitchProps } from '@radix-ui/themes'; -import { defineView, ViewComponentProps } from '@reactlit/core'; -import { LabelType } from '../label'; +import { Switch, SwitchProps } from "@radix-ui/themes"; +import { ViewComponentProps, defineView } from "@reactlit/core"; +import { LabelType } from "../label"; export type SwitchInputProps = Omit< - SwitchProps, - 'checked' | 'onCheckedChange' | 'value' | 'onValueChange' + SwitchProps, + "checked" | "onCheckedChange" | "value" | "onValueChange" >; export const SwitchInputComponent = ({ - value, - stateKey, - setValue, - display, - view, - ...props + value, + stateKey, + setValue, + display, + view, + ...props }: SwitchInputProps & ViewComponentProps) => { - return ; + return ; }; export const SwitchInput = (props?: SwitchInputProps) => - defineView((viewProps) => ( - - )); + defineView((viewProps) => ( + + )); diff --git a/libs/radix/src/inputs/table.input.tsx b/libs/radix/src/inputs/table.input.tsx index 799c8b2..2852605 100644 --- a/libs/radix/src/inputs/table.input.tsx +++ b/libs/radix/src/inputs/table.input.tsx @@ -1,227 +1,227 @@ -import { Checkbox, Radio, ScrollArea, Skeleton, Table } from '@radix-ui/themes'; +import { Checkbox, Radio, ScrollArea, Skeleton, Table } from "@radix-ui/themes"; import { - defineTransformView, - ViewComponentProps, - ViewDefinition, -} from '@reactlit/core'; -import { ReactNode, useMemo } from 'react'; -import { repeatElement } from '../utils/repeat-element'; -import { userFriendlyName } from '../utils/user-friendly-name'; + ViewComponentProps, + ViewDefinition, + defineTransformView, +} from "@reactlit/core"; +import { ReactNode, useMemo } from "react"; +import { repeatElement } from "../utils/repeat-element"; +import { userFriendlyName } from "../utils/user-friendly-name"; export type TableInputProps = Table.RootProps & { - data: T[]; - getRowId: (row: T) => string; - columns?: (keyof T & string)[]; - header?: ColumnHeaderMapping; - format?: ColumnDisplayMapping; - maxHeight?: string; - multiple?: boolean; - loading?: boolean; + data: T[]; + getRowId: (row: T) => string; + columns?: (keyof T & string)[]; + header?: ColumnHeaderMapping; + format?: ColumnDisplayMapping; + maxHeight?: string; + multiple?: boolean; + loading?: boolean; }; type ColumnHeaderMapping = { - [K in keyof T]?: ReactNode; + [K in keyof T]?: ReactNode; }; type ColumnDisplayMapping = { - [K in keyof T]?: (value: T[K]) => ReactNode; + [K in keyof T]?: (value: T[K]) => ReactNode; }; type ColDef = { - id: string; - header: ReactNode; - cell: (props: { row: T }) => ReactNode; + id: string; + header: ReactNode; + cell: (props: { row: T }) => ReactNode; }; export function TableInputViewComponent({ - setValue, - value, - stateKey, - columns, - data, - header, - format, - multiple, - getRowId, - maxHeight = '300px', - loading, - display, - view, - ...props + setValue, + value, + stateKey, + columns, + data, + header, + format, + multiple, + getRowId, + maxHeight = "300px", + loading, + display, + view, + ...props }: TableInputProps & ViewComponentProps) { - const colDefs = useMemo[]>(() => { - const firstRow = data.at(0); - const colKeys = - columns ?? (Object.keys(firstRow ?? {}) as (keyof T & string)[]); - const areAllRowsSelected = value.length === data.length; - return [ - { - id: '__select__', - header: multiple ? ( - - setValue(areAllRowsSelected ? [] : data.map(getRowId)) - } - /> - ) : undefined, - cell: ({ row }) => { - const rowId = getRowId(row); - if (loading) { - return ( - - {multiple ? : } - - ); - } else if (multiple) { - return ( - - setValue( - value.includes(rowId) - ? value.filter((r) => r !== rowId) - : [...value, rowId] - ) - } - /> - ); - } else { - return ( - setValue([v])} - /> - ); - } - }, - }, - ...colKeys.map((key) => ({ - id: key, - header: header?.[key] ?? userFriendlyName(key), - cell: ({ row }) => { - return format?.[key]?.(row[key]) ?? String(row[key]); - }, - })), - ]; - }, [ - data, - columns, - loading, - getRowId, - setValue, - value, - header, - format, - multiple, - stateKey, - ]); - return ( - - - - - {colDefs.map((col) => ( - - {col.header} - - ))} - - - - {data.map((row, i) => ( - - {colDefs.map((col) => ( - {col.cell({ row })} - ))} - - ))} - {loading && - data.length === 0 && - repeatElement( - - {repeatElement( - - Loading... - , - colDefs.length - )} - , - 3 - )} - {!loading && data.length === 0 && ( - - No data - - )} - - - - ); + const colDefs = useMemo[]>(() => { + const firstRow = data.at(0); + const colKeys = + columns ?? (Object.keys(firstRow ?? {}) as (keyof T & string)[]); + const areAllRowsSelected = value.length === data.length; + return [ + { + id: "__select__", + header: multiple ? ( + + setValue(areAllRowsSelected ? [] : data.map(getRowId)) + } + /> + ) : undefined, + cell: ({ row }) => { + const rowId = getRowId(row); + if (loading) { + return ( + + {multiple ? : } + + ); + } + if (multiple) { + return ( + + setValue( + value.includes(rowId) + ? value.filter((r) => r !== rowId) + : [...value, rowId], + ) + } + /> + ); + } + return ( + setValue([v])} + /> + ); + }, + }, + ...colKeys.map((key) => ({ + id: key, + header: header?.[key] ?? userFriendlyName(key), + cell: ({ row }) => { + return format?.[key]?.(row[key]) ?? String(row[key]); + }, + })), + ]; + }, [ + data, + columns, + loading, + getRowId, + setValue, + value, + header, + format, + multiple, + stateKey, + ]); + return ( + + + + + {colDefs.map((col) => ( + + {col.header} + + ))} + + + + {data.map((row, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: + + {colDefs.map((col) => ( + {col.cell({ row })} + ))} + + ))} + {loading && + data.length === 0 && + repeatElement( + + {repeatElement( + + Loading... + , + colDefs.length, + )} + , + 3, + )} + {!loading && data.length === 0 && ( + + No data + + )} + + + + ); } export function SingleTableInputViewComponent({ - setValue, - value, - ...props -}: Omit, 'multiple'> & - ViewComponentProps) { - return ( - - multiple={false} - value={value ? [value] : []} - setValue={(v) => { - setValue(v?.at(0) ?? undefined); - }} - {...props} - /> - ); + setValue, + value, + ...props +}: Omit, "multiple"> & + ViewComponentProps) { + return ( + + multiple={false} + value={value ? [value] : []} + setValue={(v) => { + setValue(v?.at(0) ?? undefined); + }} + {...props} + /> + ); } export type TableViewDefinition = P extends { multiple: true } - ? ViewDefinition - : ViewDefinition; + ? ViewDefinition + : ViewDefinition; -export function TableInput, 'data'>>( - data: T[] | undefined, - { multiple, ...props }: P +export function TableInput, "data">>( + data: T[] | undefined, + { multiple, ...props }: P, ): TableViewDefinition { - if (multiple) { - return defineTransformView( - (viewProps) => ( - - ), - ({ value }) => data.filter((row) => value.includes(props.getRowId(row))) - ) as TableViewDefinition; - } else { - return defineTransformView( - (viewProps) => ( - - ), - ({ value }) => data.find((row) => props.getRowId(row) === value) - ) as TableViewDefinition; - } + if (multiple) { + return defineTransformView( + (viewProps) => ( + + ), + ({ value }) => data.filter((row) => value.includes(props.getRowId(row))), + ) as TableViewDefinition; + } + return defineTransformView( + (viewProps) => ( + + ), + ({ value }) => data.find((row) => props.getRowId(row) === value), + ) as TableViewDefinition; } diff --git a/libs/radix/src/inputs/text.input.tsx b/libs/radix/src/inputs/text.input.tsx index 1bacdb1..69c275a 100644 --- a/libs/radix/src/inputs/text.input.tsx +++ b/libs/radix/src/inputs/text.input.tsx @@ -1,54 +1,54 @@ -import { TextField } from '@radix-ui/themes'; -import { defineView, ViewComponentProps } from '@reactlit/core'; -import { isValidElement, ReactNode, useEffect, useState } from 'react'; -import { useDebouncedCallback } from 'use-debounce'; -import { LabelType } from '../label'; +import { TextField } from "@radix-ui/themes"; +import { ViewComponentProps, defineView } from "@reactlit/core"; +import { ReactNode, isValidElement, useEffect, useState } from "react"; +import { useDebouncedCallback } from "use-debounce"; +import { LabelType } from "../label"; -export type TextInputProps = Omit & { - children?: - | React.ReactNode - | ((props: ViewComponentProps) => React.ReactNode); - debounceDelay?: number; +export type TextInputProps = Omit & { + children?: + | React.ReactNode + | ((props: ViewComponentProps) => React.ReactNode); + debounceDelay?: number; }; export const TextInputComponent = ({ - value, - stateKey, - setValue, - onChange, - debounceDelay = 200, - children, - display, - view, - ...props + value, + stateKey, + setValue, + onChange, + debounceDelay = 200, + children, + display, + view, + ...props }: TextInputProps & ViewComponentProps) => { - const [rawValue, setRawValue] = useState(value ?? ''); - const debouncedSetValue = useDebouncedCallback((value) => { - setValue(value); - }, debounceDelay); - useEffect(() => { - setRawValue(value ?? ''); - }, [value]); - return ( - { - setRawValue(e.target.value); - debouncedSetValue(e.target.value); - onChange?.(e); - }} - {...props} - > - {isValidElement(children) - ? (children as ReactNode) - : typeof children === 'function' - ? children({ value, display, view, stateKey, setValue }) - : undefined} - - ); + const [rawValue, setRawValue] = useState(value ?? ""); + const debouncedSetValue = useDebouncedCallback((value) => { + setValue(value); + }, debounceDelay); + useEffect(() => { + setRawValue(value ?? ""); + }, [value]); + return ( + { + setRawValue(e.target.value); + debouncedSetValue(e.target.value); + onChange?.(e); + }} + {...props} + > + {isValidElement(children) + ? (children as ReactNode) + : typeof children === "function" + ? children({ value, display, view, stateKey, setValue }) + : undefined} + + ); }; export const TextInput = (props?: TextInputProps) => - defineView((viewProps) => ( - - )); + defineView((viewProps) => ( + + )); diff --git a/libs/radix/src/inputs/textarea.input.tsx b/libs/radix/src/inputs/textarea.input.tsx index e5668f6..a1ba9fa 100644 --- a/libs/radix/src/inputs/textarea.input.tsx +++ b/libs/radix/src/inputs/textarea.input.tsx @@ -1,46 +1,46 @@ -import { TextArea, TextAreaProps } from '@radix-ui/themes'; -import { defineView, ViewComponentProps } from '@reactlit/core'; -import { useEffect, useState } from 'react'; -import { useDebouncedCallback } from 'use-debounce'; -import { LabelType } from '../label'; +import { TextArea, TextAreaProps } from "@radix-ui/themes"; +import { ViewComponentProps, defineView } from "@reactlit/core"; +import { useEffect, useState } from "react"; +import { useDebouncedCallback } from "use-debounce"; +import { LabelType } from "../label"; -export type TextAreaInputProps = Omit & { - label?: LabelType; - debounceDelay?: number; +export type TextAreaInputProps = Omit & { + label?: LabelType; + debounceDelay?: number; }; export const TextAreaInputComponent = ({ - value, - stateKey, - setValue, - onChange, - label, - display, - view, - debounceDelay = 300, - ...props + value, + stateKey, + setValue, + onChange, + label, + display, + view, + debounceDelay = 300, + ...props }: TextAreaInputProps & ViewComponentProps) => { - const [rawValue, setRawValue] = useState(value ?? ''); - const debouncedSetValue = useDebouncedCallback((value) => { - setValue(value); - }, debounceDelay); - useEffect(() => { - setRawValue(value ?? ''); - }, [value]); - return ( -