From f7f94046cdc941f6000f3796982b10c0764e2761 Mon Sep 17 00:00:00 2001 From: Jacob Maynard Date: Wed, 24 Dec 2025 17:20:52 -0600 Subject: [PATCH 1/7] convert mcp and ui to typescript --- .github/dependabot.yml | 202 ++++ packages/mcp/.gitignore | 5 +- ...etter-auth.test.js => better-auth.test.ts} | 0 .../{drizzle.test.js => drizzle.test.ts} | 0 .../{icons.test.js => icons.test.ts} | 8 +- .../__tests__/{zag.test.js => zag.test.ts} | 0 packages/mcp/constants.js | 1 - packages/mcp/package.json | 12 +- packages/mcp/src/constants.ts | 1 + .../{scrape-icons.js => src/scrape-icons.ts} | 27 +- packages/mcp/{server.js => src/server.ts} | 6 +- .../tools/better-auth.ts} | 16 +- .../tools/code-review.ts} | 305 ++--- .../drizzle.js => src/tools/drizzle.ts} | 35 +- .../{tools/icons.js => src/tools/icons.ts} | 26 +- .../mcp/{tools/lint.js => src/tools/lint.ts} | 20 +- .../local-docs.js => src/tools/local-docs.ts} | 18 +- .../mcp/{tools/zag.js => src/tools/zag.ts} | 33 +- packages/mcp/src/types.ts | 3 + packages/mcp/tsconfig.json | 25 + .../{vitest.config.js => vitest.config.ts} | 2 +- packages/ui/jsconfig.json | 10 - packages/ui/package.json | 13 +- .../ui/src/__tests__/{setup.js => setup.ts} | 33 +- .../ui/src/constants/{zIndex.js => zIndex.ts} | 2 +- packages/ui/src/index.d.ts | 1043 ----------------- packages/ui/src/{index.js => index.ts} | 8 +- packages/ui/src/lib/{cn.js => cn.ts} | 5 +- .../{useWindowDrag.js => useWindowDrag.ts} | 13 +- .../src/zag/{Accordion.jsx => Accordion.tsx} | 49 +- .../ui/src/zag/{Avatar.jsx => Avatar.tsx} | 28 +- .../ui/src/zag/{Checkbox.jsx => Checkbox.tsx} | 42 +- .../src/zag/{Clipboard.jsx => Clipboard.tsx} | 108 +- .../zag/{Collapsible.jsx => Collapsible.tsx} | 83 +- .../ui/src/zag/{Combobox.jsx => Combobox.tsx} | 89 +- .../ui/src/zag/{Dialog.jsx => Dialog.tsx} | 89 +- .../ui/src/zag/{Drawer.jsx => Drawer.tsx} | 47 +- .../ui/src/zag/{Editable.jsx => Editable.tsx} | 91 +- .../zag/{FileUpload.jsx => FileUpload.tsx} | 63 +- .../{FloatingPanel.jsx => FloatingPanel.tsx} | 105 +- packages/ui/src/zag/{Menu.jsx => Menu.tsx} | 83 +- .../zag/{NumberInput.jsx => NumberInput.tsx} | 74 +- .../{PasswordInput.jsx => PasswordInput.tsx} | 40 +- .../ui/src/zag/{PinInput.jsx => PinInput.tsx} | 42 +- .../ui/src/zag/{Popover.jsx => Popover.tsx} | 76 +- .../ui/src/zag/{Progress.jsx => Progress.tsx} | 38 +- .../ui/src/zag/{QRCode.jsx => QRCode.tsx} | 28 +- .../zag/{RadioGroup.jsx => RadioGroup.tsx} | 49 +- .../ui/src/zag/{Select.jsx => Select.tsx} | 96 +- packages/ui/src/zag/Splitter.jsx | 27 - packages/ui/src/zag/Splitter.tsx | 50 + .../ui/src/zag/{Switch.jsx => Switch.tsx} | 34 +- packages/ui/src/zag/{Tabs.jsx => Tabs.tsx} | 39 +- .../src/zag/{TagsInput.jsx => TagsInput.tsx} | 65 +- packages/ui/src/zag/{Toast.jsx => Toast.tsx} | 41 +- .../zag/{ToggleGroup.jsx => ToggleGroup.tsx} | 55 +- .../ui/src/zag/{Tooltip.jsx => Tooltip.tsx} | 95 +- packages/ui/src/zag/Tour.jsx | 181 --- packages/ui/src/zag/Tour.tsx | 269 +++++ .../{Dialog.test.jsx => Dialog.test.tsx} | 2 +- ...adioGroup.test.jsx => RadioGroup.test.tsx} | 2 +- .../{Checkbox.test.jsx => checkbox.test.tsx} | 2 +- .../{Progress.test.jsx => progress.test.tsx} | 2 +- .../{Switch.test.jsx => switch.test.tsx} | 2 +- .../{Tabs.test.jsx => tabs.test.tsx} | 2 +- .../{Tooltip.test.jsx => tooltip.test.tsx} | 2 +- packages/ui/src/zag/index.js | 32 - packages/ui/src/zag/index.ts | 32 + packages/ui/tsconfig.json | 27 + packages/ui/vitest.config.js | 4 +- packages/workers/turbo.json | 9 - pnpm-lock.yaml | 64 +- renovate.json | 15 + turbo.json | 24 +- 74 files changed, 2132 insertions(+), 2137 deletions(-) create mode 100644 .github/dependabot.yml rename packages/mcp/__tests__/{better-auth.test.js => better-auth.test.ts} (100%) rename packages/mcp/__tests__/{drizzle.test.js => drizzle.test.ts} (100%) rename packages/mcp/__tests__/{icons.test.js => icons.test.ts} (89%) rename packages/mcp/__tests__/{zag.test.js => zag.test.ts} (100%) delete mode 100644 packages/mcp/constants.js create mode 100644 packages/mcp/src/constants.ts rename packages/mcp/{scrape-icons.js => src/scrape-icons.ts} (72%) rename packages/mcp/{server.js => src/server.ts} (91%) rename packages/mcp/{tools/better-auth.js => src/tools/better-auth.ts} (79%) rename packages/mcp/{tools/code-review.js => src/tools/code-review.ts} (87%) rename packages/mcp/{tools/drizzle.js => src/tools/drizzle.ts} (83%) rename packages/mcp/{tools/icons.js => src/tools/icons.ts} (74%) rename packages/mcp/{tools/lint.js => src/tools/lint.ts} (64%) rename packages/mcp/{tools/local-docs.js => src/tools/local-docs.ts} (84%) rename packages/mcp/{tools/zag.js => src/tools/zag.ts} (87%) create mode 100644 packages/mcp/src/types.ts create mode 100644 packages/mcp/tsconfig.json rename packages/mcp/{vitest.config.js => vitest.config.ts} (75%) delete mode 100644 packages/ui/jsconfig.json rename packages/ui/src/__tests__/{setup.js => setup.ts} (70%) rename packages/ui/src/constants/{zIndex.js => zIndex.ts} (99%) delete mode 100644 packages/ui/src/index.d.ts rename packages/ui/src/{index.js => index.ts} (52%) rename packages/ui/src/lib/{cn.js => cn.ts} (73%) rename packages/ui/src/primitives/{useWindowDrag.js => useWindowDrag.ts} (81%) rename packages/ui/src/zag/{Accordion.jsx => Accordion.tsx} (69%) rename packages/ui/src/zag/{Avatar.jsx => Avatar.tsx} (67%) rename packages/ui/src/zag/{Checkbox.jsx => Checkbox.tsx} (76%) rename packages/ui/src/zag/{Clipboard.jsx => Clipboard.tsx} (63%) rename packages/ui/src/zag/{Collapsible.jsx => Collapsible.tsx} (70%) rename packages/ui/src/zag/{Combobox.jsx => Combobox.tsx} (67%) rename packages/ui/src/zag/{Dialog.jsx => Dialog.tsx} (81%) rename packages/ui/src/zag/{Drawer.jsx => Drawer.tsx} (77%) rename packages/ui/src/zag/{Editable.jsx => Editable.tsx} (73%) rename packages/ui/src/zag/{FileUpload.jsx => FileUpload.tsx} (75%) rename packages/ui/src/zag/{FloatingPanel.jsx => FloatingPanel.tsx} (71%) rename packages/ui/src/zag/{Menu.jsx => Menu.tsx} (71%) rename packages/ui/src/zag/{NumberInput.jsx => NumberInput.tsx} (67%) rename packages/ui/src/zag/{PasswordInput.jsx => PasswordInput.tsx} (66%) rename packages/ui/src/zag/{PinInput.jsx => PinInput.tsx} (61%) rename packages/ui/src/zag/{Popover.jsx => Popover.tsx} (67%) rename packages/ui/src/zag/{Progress.jsx => Progress.tsx} (73%) rename packages/ui/src/zag/{QRCode.jsx => QRCode.tsx} (62%) rename packages/ui/src/zag/{RadioGroup.jsx => RadioGroup.tsx} (73%) rename packages/ui/src/zag/{Select.jsx => Select.tsx} (76%) delete mode 100644 packages/ui/src/zag/Splitter.jsx create mode 100644 packages/ui/src/zag/Splitter.tsx rename packages/ui/src/zag/{Switch.jsx => Switch.tsx} (70%) rename packages/ui/src/zag/{Tabs.jsx => Tabs.tsx} (73%) rename packages/ui/src/zag/{TagsInput.jsx => TagsInput.tsx} (70%) rename packages/ui/src/zag/{Toast.jsx => Toast.tsx} (76%) rename packages/ui/src/zag/{ToggleGroup.jsx => ToggleGroup.tsx} (63%) rename packages/ui/src/zag/{Tooltip.jsx => Tooltip.tsx} (57%) delete mode 100644 packages/ui/src/zag/Tour.jsx create mode 100644 packages/ui/src/zag/Tour.tsx rename packages/ui/src/zag/__tests__/{Dialog.test.jsx => Dialog.test.tsx} (99%) rename packages/ui/src/zag/__tests__/{RadioGroup.test.jsx => RadioGroup.test.tsx} (99%) rename packages/ui/src/zag/__tests__/{Checkbox.test.jsx => checkbox.test.tsx} (99%) rename packages/ui/src/zag/__tests__/{Progress.test.jsx => progress.test.tsx} (99%) rename packages/ui/src/zag/__tests__/{Switch.test.jsx => switch.test.tsx} (99%) rename packages/ui/src/zag/__tests__/{Tabs.test.jsx => tabs.test.tsx} (99%) rename packages/ui/src/zag/__tests__/{Tooltip.test.jsx => tooltip.test.tsx} (98%) delete mode 100644 packages/ui/src/zag/index.js create mode 100644 packages/ui/src/zag/index.ts create mode 100644 packages/ui/tsconfig.json delete mode 100644 packages/workers/turbo.json create mode 100644 renovate.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..a1e1b2788 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,202 @@ +version: 2 +updates: + # Root package.json dependencies + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "root" + versioning-strategy: increase + commit-message: + prefix: "chore" + prefix-development: "chore" + include: "scope" + rebase-strategy: "auto" + groups: + root-dev-dependencies: + patterns: + - "*" + dependency-type: "development" + + # Landing package + - package-ecosystem: "npm" + directory: "/packages/landing" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 3 + labels: + - "dependencies" + - "landing" + versioning-strategy: increase + commit-message: + prefix: "chore" + prefix-development: "chore" + include: "scope" + rebase-strategy: "auto" + groups: + landing-dependencies: + patterns: + - "*" + dependency-type: "production" + landing-dev-dependencies: + patterns: + - "*" + dependency-type: "development" + + # MCP package + - package-ecosystem: "npm" + directory: "/packages/mcp" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 3 + labels: + - "dependencies" + - "mcp" + versioning-strategy: increase + commit-message: + prefix: "chore" + prefix-development: "chore" + include: "scope" + rebase-strategy: "auto" + groups: + mcp-dependencies: + patterns: + - "*" + dependency-type: "production" + mcp-dev-dependencies: + patterns: + - "*" + dependency-type: "development" + + # Shared package + - package-ecosystem: "npm" + directory: "/packages/shared" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 3 + labels: + - "dependencies" + - "shared" + versioning-strategy: increase + commit-message: + prefix: "chore" + prefix-development: "chore" + include: "scope" + rebase-strategy: "auto" + groups: + shared-dependencies: + patterns: + - "*" + dependency-type: "production" + shared-dev-dependencies: + patterns: + - "*" + dependency-type: "development" + + # UI package + - package-ecosystem: "npm" + directory: "/packages/ui" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 3 + labels: + - "dependencies" + - "ui" + versioning-strategy: increase + commit-message: + prefix: "chore" + prefix-development: "chore" + include: "scope" + rebase-strategy: "auto" + groups: + ui-dependencies: + patterns: + - "*" + dependency-type: "production" + ui-dev-dependencies: + patterns: + - "*" + dependency-type: "development" + + # Web package + - package-ecosystem: "npm" + directory: "/packages/web" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "web" + versioning-strategy: increase + commit-message: + prefix: "chore" + prefix-development: "chore" + include: "scope" + rebase-strategy: "auto" + groups: + web-dependencies: + patterns: + - "*" + dependency-type: "production" + web-dev-dependencies: + patterns: + - "*" + dependency-type: "development" + + # Workers package + - package-ecosystem: "npm" + directory: "/packages/workers" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "workers" + versioning-strategy: increase + commit-message: + prefix: "chore" + prefix-development: "chore" + include: "scope" + rebase-strategy: "auto" + groups: + workers-dependencies: + patterns: + - "*" + dependency-type: "production" + workers-dev-dependencies: + patterns: + - "*" + dependency-type: "development" + + # GitHub Actions workflows + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 2 + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "ci" + include: "scope" + rebase-strategy: "auto" diff --git a/packages/mcp/.gitignore b/packages/mcp/.gitignore index 6f85b6e17..e12853060 100644 --- a/packages/mcp/.gitignore +++ b/packages/mcp/.gitignore @@ -1,3 +1,6 @@ +dist/ +node_modules/ +*.tsbuildinfo zag-docs icon-manifest.json -zag-manifest.json \ No newline at end of file +zag-manifest.json diff --git a/packages/mcp/__tests__/better-auth.test.js b/packages/mcp/__tests__/better-auth.test.ts similarity index 100% rename from packages/mcp/__tests__/better-auth.test.js rename to packages/mcp/__tests__/better-auth.test.ts diff --git a/packages/mcp/__tests__/drizzle.test.js b/packages/mcp/__tests__/drizzle.test.ts similarity index 100% rename from packages/mcp/__tests__/drizzle.test.js rename to packages/mcp/__tests__/drizzle.test.ts diff --git a/packages/mcp/__tests__/icons.test.js b/packages/mcp/__tests__/icons.test.ts similarity index 89% rename from packages/mcp/__tests__/icons.test.js rename to packages/mcp/__tests__/icons.test.ts index a050a61e0..5ee812fca 100644 --- a/packages/mcp/__tests__/icons.test.js +++ b/packages/mcp/__tests__/icons.test.ts @@ -6,6 +6,10 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +interface IconManifest { + [library: string]: string[]; +} + describe('icons tool', () => { it('should have icon manifest file', async () => { const manifestPath = path.join(__dirname, '..', 'icon-manifest.json'); @@ -16,7 +20,7 @@ describe('icons tool', () => { it('should load valid icon manifest', async () => { const manifestPath = path.join(__dirname, '..', 'icon-manifest.json'); const content = await fs.readFile(manifestPath, 'utf8'); - const manifest = JSON.parse(content); + const manifest = JSON.parse(content) as IconManifest; expect(typeof manifest).toBe('object'); expect(Object.keys(manifest).length).toBeGreaterThan(0); @@ -32,7 +36,7 @@ describe('icons tool', () => { it('should have common icon libraries', async () => { const manifestPath = path.join(__dirname, '..', 'icon-manifest.json'); const content = await fs.readFile(manifestPath, 'utf8'); - const manifest = JSON.parse(content); + const manifest = JSON.parse(content) as IconManifest; // Check for common libraries expect(manifest).toHaveProperty('fa'); // Font Awesome diff --git a/packages/mcp/__tests__/zag.test.js b/packages/mcp/__tests__/zag.test.ts similarity index 100% rename from packages/mcp/__tests__/zag.test.js rename to packages/mcp/__tests__/zag.test.ts diff --git a/packages/mcp/constants.js b/packages/mcp/constants.js deleted file mode 100644 index 6f1fffe73..000000000 --- a/packages/mcp/constants.js +++ /dev/null @@ -1 +0,0 @@ -export const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 36d67f57a..85c0606c8 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -4,12 +4,15 @@ "type": "module", "license": "PolyForm-Noncommercial-1.0.0", "bin": { - "corates-mcp": "./server.js" + "corates-mcp": "./dist/server.js" }, "scripts": { - "start": "nohup node server.js > /dev/null 2>&1 & echo 'MCP server started in background'", + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist", + "start": "nohup node dist/server.js > /dev/null 2>&1 & echo 'MCP server started in background'", "stop": "pkill -f 'node.*server.js' || echo 'No MCP server running'", - "scrape": "node scrape-icons.js", + "scrape": "tsx src/scrape-icons.ts", "test": "vitest run", "test:watch": "vitest" }, @@ -18,6 +21,9 @@ "zod": "^4.2.1" }, "devDependencies": { + "@types/node": "^22.10.1", + "tsx": "^4.19.2", + "typescript": "^5.9.3", "vitest": "^3.2.4" } } diff --git a/packages/mcp/src/constants.ts b/packages/mcp/src/constants.ts new file mode 100644 index 000000000..27c25f065 --- /dev/null +++ b/packages/mcp/src/constants.ts @@ -0,0 +1 @@ +export const CACHE_TTL: number = 24 * 60 * 60 * 1000; // 24 hours diff --git a/packages/mcp/scrape-icons.js b/packages/mcp/src/scrape-icons.ts similarity index 72% rename from packages/mcp/scrape-icons.js rename to packages/mcp/src/scrape-icons.ts index 9cece7591..17611cb34 100644 --- a/packages/mcp/scrape-icons.js +++ b/packages/mcp/src/scrape-icons.ts @@ -1,4 +1,3 @@ -// scripts/scrape-icons.js // Generates a manifest of all available icons from solid-icons // Parses the index.js files directly since Node can't import JSX import fs from 'fs/promises'; @@ -8,8 +7,12 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +interface IconManifest { + [library: string]: string[]; +} + // All icon set abbreviations in solid-icons -const iconSets = [ +const iconSets: readonly string[] = [ 'ai', 'bi', 'bs', @@ -26,15 +29,16 @@ const iconSets = [ 'ti', 'vs', 'wi', -]; +] as const; -async function getIconsFromSet(setAbbr) { +async function getIconsFromSet(setAbbr: string): Promise { try { // Read the index.js file and extract function export names // (Node can't import JSX files directly) const indexPath = path.join( __dirname, '..', + '..', 'web', 'node_modules', 'solid-icons', @@ -45,8 +49,8 @@ async function getIconsFromSet(setAbbr) { // Match: export function IconName(props) const exportRegex = /export\s+function\s+([A-Z][a-zA-Z0-9]*)\s*\(/g; - const icons = []; - let match; + const icons: string[] = []; + let match: RegExpExecArray | null; while ((match = exportRegex.exec(content)) !== null) { icons.push(match[1]); @@ -54,13 +58,14 @@ async function getIconsFromSet(setAbbr) { return icons; } catch (err) { - console.warn(`Could not read solid-icons/${setAbbr}: ${err.message}`); + const errorMessage = err instanceof Error ? err.message : String(err); + console.warn(`Could not read solid-icons/${setAbbr}: ${errorMessage}`); return []; } } -async function main() { - const manifest = {}; +async function main(): Promise { + const manifest: IconManifest = {}; let totalIcons = 0; for (const setAbbr of iconSets) { @@ -71,13 +76,13 @@ async function main() { console.log(` Found ${icons.length} icons`); } - const outFile = path.join(__dirname, 'icon-manifest.json'); + const outFile = path.join(__dirname, '..', 'icon-manifest.json'); await fs.writeFile(outFile, JSON.stringify(manifest, null, 2), 'utf8'); console.log(`\nTotal: ${totalIcons} icons`); console.log(`Manifest written to ${outFile}`); } -main().catch(err => { +main().catch((err: unknown) => { console.error(err); process.exit(1); }); diff --git a/packages/mcp/server.js b/packages/mcp/src/server.ts similarity index 91% rename from packages/mcp/server.js rename to packages/mcp/src/server.ts index 601557736..7f513ff36 100644 --- a/packages/mcp/server.js +++ b/packages/mcp/src/server.ts @@ -20,7 +20,7 @@ import { registerCodeReviewTools } from './tools/code-review.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const repoRoot = path.resolve(__dirname, '..', '..'); +const repoRoot = path.resolve(__dirname, '..', '..', '..'); const docsRoot = path.join(repoRoot, 'docs'); // Create the MCP server @@ -39,13 +39,13 @@ registerZagTools(server); registerCodeReviewTools(server, repoRoot); // Start the server with stdio transport -async function main() { +async function main(): Promise { const transport = new StdioServerTransport(); await server.connect(transport); console.log('Corates MCP Server started'); } -main().catch(err => { +main().catch((err: unknown) => { console.error('Failed to start MCP server:', err); process.exit(1); }); diff --git a/packages/mcp/tools/better-auth.js b/packages/mcp/src/tools/better-auth.ts similarity index 79% rename from packages/mcp/tools/better-auth.js rename to packages/mcp/src/tools/better-auth.ts index f8f1b32db..d8ce9d159 100644 --- a/packages/mcp/tools/better-auth.js +++ b/packages/mcp/src/tools/better-auth.ts @@ -1,13 +1,14 @@ import { z } from 'zod'; import { CACHE_TTL } from '../constants.js'; +import type { McpServerType } from '../types.js'; const BETTER_AUTH_BASE_URL = 'https://www.better-auth.com'; // Cache for fetched docs -const betterAuthCache = new Map(); +const betterAuthCache = new Map(); let cacheTime = 0; -async function fetchWithCache(url) { +async function fetchWithCache(url: string): Promise { const now = Date.now(); // Clear cache if TTL expired @@ -17,7 +18,7 @@ async function fetchWithCache(url) { } if (betterAuthCache.has(url)) { - return betterAuthCache.get(url); + return betterAuthCache.get(url)!; } const response = await fetch(url); @@ -30,7 +31,7 @@ async function fetchWithCache(url) { return text; } -export function registerBetterAuthTools(server) { +export function registerBetterAuthTools(server: McpServerType): void { server.tool( 'better_auth_docs', 'Fetch Better Auth documentation from the official website. Use this to get detailed docs on specific topics like plugins, integrations, authentication providers, etc. Fetch the index to get a summary of all available docs.', @@ -42,7 +43,9 @@ export function registerBetterAuthTools(server) { ) .optional(), }, - async ({ path: docPath }) => { + async ({ + path: docPath, + }): Promise<{ content: Array<{ type: 'text'; text: string }> }> => { try { const url = docPath ? @@ -63,8 +66,9 @@ export function registerBetterAuthTools(server) { ], }; } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); return { - content: [{ type: 'text', text: `Error fetching Better Auth docs: ${error.message}` }], + content: [{ type: 'text', text: `Error fetching Better Auth docs: ${errorMessage}` }], }; } }, diff --git a/packages/mcp/tools/code-review.js b/packages/mcp/src/tools/code-review.ts similarity index 87% rename from packages/mcp/tools/code-review.js rename to packages/mcp/src/tools/code-review.ts index 2a16d55ed..0af7973c0 100644 --- a/packages/mcp/tools/code-review.js +++ b/packages/mcp/src/tools/code-review.ts @@ -6,11 +6,23 @@ import { z } from 'zod'; import { execFile } from 'child_process'; import { promisify } from 'util'; +import type { McpServerType } from '../types.js'; const execFileAsync = promisify(execFile); +interface ExecFileResult { + stdout: string; + stderr: string; +} + +interface ExecFileError extends Error { + stdout?: string; + stderr?: string; + code?: number | string; +} + // Files to exclude from review (binary, generated, lock files) -const IGNORED_EXTENSIONS = [ +const IGNORED_EXTENSIONS: readonly string[] = [ '.txt', '.lock', '.png', @@ -23,17 +35,23 @@ const IGNORED_EXTENSIONS = [ '.woff2', '.ttf', '.eot', -]; -const IGNORED_FILES = ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb']; +] as const; + +const IGNORED_FILES: readonly string[] = [ + 'package-lock.json', + 'pnpm-lock.yaml', + 'yarn.lock', + 'bun.lockb', +] as const; // Strict branch name validation (alphanumeric, dots, slashes, hyphens, underscores) const BRANCH_NAME_REGEX = /^[A-Za-z0-9._/-]+$/; -function isValidBranchName(name) { +function isValidBranchName(name: string): boolean { return BRANCH_NAME_REGEX.test(name) && name.length < 256; } -function filterFiles(files) { +function filterFiles(files: string): string { return files .trim() .split('\n') @@ -47,142 +65,9 @@ function filterFiles(files) { .join('\n'); } -export function registerCodeReviewTools(server, repoRoot) { - server.tool( - 'code_review', - 'Get a structured code review of current git changes or branch diff. Returns the diff with project-specific review criteria for CoRATES (SolidJS + Cloudflare Workers).', - { - base: z.string().optional().describe('Base branch to compare against (default: main)'), - staged: z.boolean().optional().describe('Review only staged changes instead of branch diff'), - filesOnly: z - .boolean() - .optional() - .describe('Return only the list of changed files without full diff'), - }, - async ({ base = 'main', staged = true, filesOnly = false }) => { - try { - // Validate branch name to prevent command injection - if (!staged && !isValidBranchName(base)) { - return { - content: [ - { - type: 'text', - text: `Error: Invalid branch name '${base}'. Branch names must be alphanumeric with dots, slashes, hyphens, or underscores.`, - }, - ], - }; - } - - // Get changed file list using execFile (no shell interpolation) - const filesArgs = - staged ? ['diff', '--staged', '--name-only'] : ['diff', `${base}...HEAD`, '--name-only']; - - const { stdout: rawFiles } = await execFileAsync('git', filesArgs, { - cwd: repoRoot, - maxBuffer: 1024 * 1024, - }); - - // Filter out ignored files - const files = filterFiles(rawFiles); - - if (!files.trim()) { - // Fall back to unstaged changes if no branch diff - const { stdout: rawUnstagedFiles } = await execFileAsync('git', ['diff', '--name-only'], { - cwd: repoRoot, - }); - - const unstagedFiles = filterFiles(rawUnstagedFiles); - - if (!unstagedFiles.trim()) { - return { - content: [ - { - type: 'text', - text: 'No changes to review. Make sure you have uncommitted changes or commits ahead of the base branch.', - }, - ], - }; - } - - // Use unstaged diff instead (with pathspec to exclude ignored files) - const { stdout: diff } = await execFileAsync( - 'git', - ['diff', '--', ...unstagedFiles.trim().split('\n')], - { - cwd: repoRoot, - maxBuffer: 1024 * 1024 * 5, - }, - ); - - return { - content: [ - { - type: 'text', - text: buildReviewPrompt(unstagedFiles, filesOnly ? null : diff, 'unstaged'), - }, - ], - }; - } - - if (filesOnly) { - return { - content: [ - { - type: 'text', - text: buildReviewPrompt(files, null, staged ? 'staged' : base), - }, - ], - }; - } - - // Get the full diff using execFile (no shell) with pathspec to filter files - const fileList = files.trim().split('\n').filter(Boolean); - const diffArgs = - staged ? - ['diff', '--staged', '--', ...fileList] - : ['diff', `${base}...HEAD`, '--', ...fileList]; - - const { stdout: diff } = await execFileAsync('git', diffArgs, { - cwd: repoRoot, - maxBuffer: 1024 * 1024 * 5, // 5MB buffer for large diffs - }); - - return { - content: [ - { - type: 'text', - text: buildReviewPrompt(files, diff, staged ? 'staged' : base), - }, - ], - }; - } catch (error) { - // Handle case where base branch doesn't exist or other git errors - const errMsg = error.message || error.stderr || ''; - if (errMsg.includes('unknown revision') || errMsg.includes('bad revision')) { - return { - content: [ - { - type: 'text', - text: `Error: Branch '${base}' not found. Try specifying a different base branch.`, - }, - ], - }; - } - // Return sanitized error (avoid leaking internal paths) - return { - content: [ - { - type: 'text', - text: 'Error running git command. Check that you have staged changes and the repository is valid.', - }, - ], - }; - } - }, - ); -} +type CompareTarget = 'staged' | 'unstaged' | string; -function buildReviewPrompt(files, diff, compareTarget) { +function buildReviewPrompt(files: string, diff: string | null, compareTarget: CompareTarget): string { const fileList = files .trim() .split('\n') @@ -341,3 +226,143 @@ ${diff} \`\`\` `; } + +export function registerCodeReviewTools(server: McpServerType, repoRoot: string): void { + server.tool( + 'code_review', + 'Get a structured code review of current git changes or branch diff. Returns the diff with project-specific review criteria for CoRATES (SolidJS + Cloudflare Workers).', + { + base: z.string().optional().describe('Base branch to compare against (default: main)'), + staged: z.boolean().optional().describe('Review only staged changes instead of branch diff'), + filesOnly: z + .boolean() + .optional() + .describe('Return only the list of changed files without full diff'), + }, + async ({ + base = 'main', + staged = true, + filesOnly = false, + }): Promise<{ content: Array<{ type: 'text'; text: string }> }> => { + try { + // Validate branch name to prevent command injection + if (!staged && !isValidBranchName(base)) { + return { + content: [ + { + type: 'text', + text: `Error: Invalid branch name '${base}'. Branch names must be alphanumeric with dots, slashes, hyphens, or underscores.`, + }, + ], + }; + } + + // Get changed file list using execFile (no shell interpolation) + const filesArgs: string[] = + staged ? ['diff', '--staged', '--name-only'] : ['diff', `${base}...HEAD`, '--name-only']; + + const { stdout: rawFiles } = await execFileAsync('git', filesArgs, { + cwd: repoRoot, + maxBuffer: 1024 * 1024, + }) as ExecFileResult; + + // Filter out ignored files + const files = filterFiles(rawFiles); + + if (!files.trim()) { + // Fall back to unstaged changes if no branch diff + const { stdout: rawUnstagedFiles } = await execFileAsync('git', ['diff', '--name-only'], { + cwd: repoRoot, + }) as ExecFileResult; + + const unstagedFiles = filterFiles(rawUnstagedFiles); + + if (!unstagedFiles.trim()) { + return { + content: [ + { + type: 'text', + text: 'No changes to review. Make sure you have uncommitted changes or commits ahead of the base branch.', + }, + ], + }; + } + + // Use unstaged diff instead (with pathspec to exclude ignored files) + const { stdout: diff } = await execFileAsync( + 'git', + ['diff', '--', ...unstagedFiles.trim().split('\n')], + { + cwd: repoRoot, + maxBuffer: 1024 * 1024 * 5, + }, + ) as ExecFileResult; + + return { + content: [ + { + type: 'text', + text: buildReviewPrompt(unstagedFiles, filesOnly ? null : diff, 'unstaged'), + }, + ], + }; + } + + if (filesOnly) { + return { + content: [ + { + type: 'text', + text: buildReviewPrompt(files, null, staged ? 'staged' : base), + }, + ], + }; + } + + // Get the full diff using execFile (no shell) with pathspec to filter files + const fileList = files.trim().split('\n').filter(Boolean); + const diffArgs: string[] = + staged ? + ['diff', '--staged', '--', ...fileList] + : ['diff', `${base}...HEAD`, '--', ...fileList]; + + const { stdout: diff } = await execFileAsync('git', diffArgs, { + cwd: repoRoot, + maxBuffer: 1024 * 1024 * 5, // 5MB buffer for large diffs + }) as ExecFileResult; + + return { + content: [ + { + type: 'text', + text: buildReviewPrompt(files, diff, staged ? 'staged' : base), + }, + ], + }; + } catch (error) { + // Handle case where base branch doesn't exist or other git errors + const execError = error as ExecFileError; + const errMsg = execError.message || execError.stderr || ''; + if (errMsg.includes('unknown revision') || errMsg.includes('bad revision')) { + return { + content: [ + { + type: 'text', + text: `Error: Branch '${base}' not found. Try specifying a different base branch.`, + }, + ], + }; + } + // Return sanitized error (avoid leaking internal paths) + return { + content: [ + { + type: 'text', + text: 'Error running git command. Check that you have staged changes and the repository is valid.', + }, + ], + }; + } + }, + ); +} diff --git a/packages/mcp/tools/drizzle.js b/packages/mcp/src/tools/drizzle.ts similarity index 83% rename from packages/mcp/tools/drizzle.js rename to packages/mcp/src/tools/drizzle.ts index 2c2e74e4a..013c8e545 100644 --- a/packages/mcp/tools/drizzle.js +++ b/packages/mcp/src/tools/drizzle.ts @@ -1,17 +1,23 @@ import { z } from 'zod'; import { CACHE_TTL } from '../constants.js'; +import type { McpServerType } from '../types.js'; const DRIZZLE_DOCS_URL = 'https://orm.drizzle.team/llms-full.txt'; +interface DrizzleDocsCache { + header: string; + sections: Map; +} + // Cache for parsed Drizzle docs -let drizzleDocsCache = null; +let drizzleDocsCache: DrizzleDocsCache | null = null; let drizzleCacheTime = 0; /** * Fetch and parse Drizzle docs from remote URL * Parses into sections by "Source:" markers */ -async function fetchDrizzleDocs() { +async function fetchDrizzleDocs(): Promise { const now = Date.now(); if (drizzleDocsCache && now - drizzleCacheTime < CACHE_TTL) { return drizzleDocsCache; @@ -36,9 +42,9 @@ async function fetchDrizzleDocs() { const header = lines.slice(0, headerEnd).join('\n').trim(); // Parse sections by Source: markers - const sections = new Map(); - let currentPath = null; - let currentLines = []; + const sections = new Map(); + let currentPath: string | null = null; + let currentLines: string[] = []; for (let i = headerEnd; i < lines.length; i++) { const line = lines[i]; @@ -67,15 +73,15 @@ async function fetchDrizzleDocs() { /** * Generate an index of all Drizzle doc sections grouped by category */ -function generateDrizzleIndex(header, sections) { - const groups = new Map(); +function generateDrizzleIndex(header: string, sections: Map): string { + const groups = new Map(); for (const docPath of sections.keys()) { const firstSegment = docPath.split('/')[0]; if (!groups.has(firstSegment)) { groups.set(firstSegment, []); } - groups.get(firstSegment).push(docPath); + groups.get(firstSegment)!.push(docPath); } const sortedGroups = [...groups.entries()].sort((a, b) => a[0].localeCompare(b[0])); @@ -97,7 +103,7 @@ function generateDrizzleIndex(header, sections) { return index; } -export function registerDrizzleTools(server) { +export function registerDrizzleTools(server: McpServerType): void { server.tool( 'drizzle_docs', 'Fetch Drizzle ORM documentation. Returns an index of all topics when no path is provided, or specific documentation when a path is given. Use this for schema definitions, queries, migrations, connections, and all Drizzle ORM features.', @@ -109,7 +115,9 @@ export function registerDrizzleTools(server) { ) .optional(), }, - async ({ path: docPath }) => { + async ({ + path: docPath, + }): Promise<{ content: Array<{ type: 'text'; text: string }> }> => { try { const { header, sections } = await fetchDrizzleDocs(); @@ -122,7 +130,7 @@ export function registerDrizzleTools(server) { if (sections.has(docPath)) { return { content: [ - { type: 'text', text: `# Drizzle ORM: ${docPath}\n\n${sections.get(docPath)}` }, + { type: 'text', text: `# Drizzle ORM: ${docPath}\n\n${sections.get(docPath)!}` }, ], }; } @@ -135,7 +143,7 @@ export function registerDrizzleTools(server) { if (matches.length === 1) { return { content: [ - { type: 'text', text: `# Drizzle ORM: ${matches[0]}\n\n${sections.get(matches[0])}` }, + { type: 'text', text: `# Drizzle ORM: ${matches[0]}\n\n${sections.get(matches[0])!}` }, ], }; } @@ -160,8 +168,9 @@ export function registerDrizzleTools(server) { ], }; } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); return { - content: [{ type: 'text', text: `Error fetching Drizzle docs: ${error.message}` }], + content: [{ type: 'text', text: `Error fetching Drizzle docs: ${errorMessage}` }], }; } }, diff --git a/packages/mcp/tools/icons.js b/packages/mcp/src/tools/icons.ts similarity index 74% rename from packages/mcp/tools/icons.js rename to packages/mcp/src/tools/icons.ts index 496247e5a..bb5971fe1 100644 --- a/packages/mcp/tools/icons.js +++ b/packages/mcp/src/tools/icons.ts @@ -2,22 +2,34 @@ import { z } from 'zod'; import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; +import type { McpServerType } from '../types.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +interface IconManifest { + [library: string]: string[]; +} + +interface IconResult { + lib: string; + icon: string; + import: string; +} + // Cache for the icon manifest -let manifestCache = null; +let manifestCache: IconManifest | null = null; -async function loadManifest() { +async function loadManifest(): Promise { if (!manifestCache) { - const manifestPath = path.join(__dirname, '..', 'icon-manifest.json'); - manifestCache = JSON.parse(await fs.readFile(manifestPath, 'utf8')); + const manifestPath = path.join(__dirname, '..', '..', 'icon-manifest.json'); + const content = await fs.readFile(manifestPath, 'utf8'); + manifestCache = JSON.parse(content) as IconManifest; } return manifestCache; } -export function registerIconTools(server) { +export function registerIconTools(server: McpServerType): void { server.tool( 'search_icons', 'Search for icons in solid-icons library by name. Returns matching icon names with their library prefix.', @@ -25,10 +37,10 @@ export function registerIconTools(server) { query: z.string().describe('Search query to match against icon names (case-insensitive)'), limit: z.number().optional().default(20).describe('Maximum number of results to return'), }, - async ({ query, limit = 20 }) => { + async ({ query, limit = 20 }): Promise<{ content: Array<{ type: 'text'; text: string }> }> => { const manifest = await loadManifest(); const q = query.toLowerCase(); - const results = []; + const results: IconResult[] = []; for (const [lib, icons] of Object.entries(manifest)) { for (const icon of icons) { diff --git a/packages/mcp/tools/lint.js b/packages/mcp/src/tools/lint.ts similarity index 64% rename from packages/mcp/tools/lint.js rename to packages/mcp/src/tools/lint.ts index a5f917476..25f66a991 100644 --- a/packages/mcp/tools/lint.js +++ b/packages/mcp/src/tools/lint.ts @@ -1,17 +1,26 @@ import { z } from 'zod'; import { exec as execCallback } from 'child_process'; import util from 'util'; +import type { McpServerType } from '../types.js'; const exec = util.promisify(execCallback); -export function registerLintTools(server, repoRoot) { +interface ExecError extends Error { + stdout?: string; + stderr?: string; + code?: number | string; +} + +export function registerLintTools(server: McpServerType, repoRoot: string): void { server.tool( 'run_lint', 'Run pnpm lint from the repository root. Set fix=true to apply autofixes.', { fix: z.boolean().optional().default(false).describe('Whether to run lint with --fix'), }, - async ({ fix = false }) => { + async ({ + fix = false, + }): Promise<{ content: Array<{ type: 'text'; text: string }> }> => { const command = `pnpm run lint${fix ? ' --fix' : ''}`; try { @@ -25,15 +34,16 @@ export function registerLintTools(server, repoRoot) { content: [{ type: 'text', text: `Command: ${command}\n\n${output}` }], }; } catch (error) { - const stdout = error.stdout || ''; - const stderr = error.stderr || error.message || ''; + const execError = error as ExecError; + const stdout = execError.stdout || ''; + const stderr = execError.stderr || execError.message || ''; const output = [stdout, stderr].filter(Boolean).join('\n').trim() || 'Lint failed with no output'; return { content: [ { type: 'text', - text: `Command: ${command}\nExit code: ${error.code ?? 'unknown'}\n\n${output}`, + text: `Command: ${command}\nExit code: ${execError.code ?? 'unknown'}\n\n${output}`, }, ], }; diff --git a/packages/mcp/tools/local-docs.js b/packages/mcp/src/tools/local-docs.ts similarity index 84% rename from packages/mcp/tools/local-docs.js rename to packages/mcp/src/tools/local-docs.ts index e931bdff2..90f07c246 100644 --- a/packages/mcp/tools/local-docs.js +++ b/packages/mcp/src/tools/local-docs.ts @@ -1,15 +1,21 @@ import { z } from 'zod'; import fs from 'fs/promises'; import path from 'path'; +import type { McpServerType } from '../types.js'; -function assertSafeDocName(doc) { +interface DocInfo { + name: string; + llmsPath: string; +} + +function assertSafeDocName(doc: unknown): asserts doc is string { if (!doc || typeof doc !== 'string') throw new Error('doc is required'); if (!/^[A-Za-z0-9._-]+$/.test(doc)) { throw new Error('Invalid doc name. Use only letters, numbers, dot, underscore, or dash.'); } } -async function listLocalDocsWithLlmsTxt(docsRoot) { +async function listLocalDocsWithLlmsTxt(docsRoot: string): Promise { try { const entries = await fs.readdir(docsRoot, { withFileTypes: true }); const docNames = entries @@ -17,7 +23,7 @@ async function listLocalDocsWithLlmsTxt(docsRoot) { .map(e => e.name) .sort((a, b) => a.localeCompare(b)); - const results = []; + const results: DocInfo[] = []; for (const name of docNames) { const llmsPath = path.join(docsRoot, name, 'llms.txt'); try { @@ -34,12 +40,12 @@ async function listLocalDocsWithLlmsTxt(docsRoot) { } } -export function registerLocalDocsTools(server, docsRoot) { +export function registerLocalDocsTools(server: McpServerType, docsRoot: string): void { server.tool( 'docs_list', 'List local documentation sources available under the repo /docs folder (only those with an llms.txt).', {}, - async () => { + async (): Promise<{ content: Array<{ type: 'text'; text: string }> }> => { const available = await listLocalDocsWithLlmsTxt(docsRoot); if (available.length === 0) { @@ -68,7 +74,7 @@ export function registerLocalDocsTools(server, docsRoot) { { doc: z.string().describe('Docs folder name under /docs (e.g. "solidjs", "hono")'), }, - async ({ doc }) => { + async ({ doc }): Promise<{ content: Array<{ type: 'text'; text: string }> }> => { assertSafeDocName(doc); const llmsPath = path.join(docsRoot, doc, 'llms.txt'); diff --git a/packages/mcp/tools/zag.js b/packages/mcp/src/tools/zag.ts similarity index 87% rename from packages/mcp/tools/zag.js rename to packages/mcp/src/tools/zag.ts index aec15f537..6440ab474 100644 --- a/packages/mcp/tools/zag.js +++ b/packages/mcp/src/tools/zag.ts @@ -1,17 +1,27 @@ import { z } from 'zod'; import { CACHE_TTL } from '../constants.js'; +import type { McpServerType } from '../types.js'; const ZAG_DOCS_URL = 'https://zagjs.com/llms-solid.txt'; +interface ResourceMarker { + lineNum: number; + name: string; +} + +interface ZagDocsCache { + sections: Map; +} + // Cache for parsed Zag docs -let zagDocsCache = null; +let zagDocsCache: ZagDocsCache | null = null; let zagCacheTime = 0; /** * Fetch and parse Zag docs from remote URL * Parses into sections by component name from ## Resources sections */ -async function fetchZagDocs() { +async function fetchZagDocs(): Promise { const now = Date.now(); if (zagDocsCache && now - zagCacheTime < CACHE_TTL) { return zagDocsCache; @@ -26,7 +36,7 @@ async function fetchZagDocs() { const lines = content.split('\n'); // Find all '## Resources' lines and their associated component names - const resourceMarkers = []; + const resourceMarkers: ResourceMarker[] = []; for (let i = 0; i < lines.length; i++) { if (lines[i] === '## Resources') { // Look ahead for npm link (within next 10 lines) @@ -40,7 +50,7 @@ async function fetchZagDocs() { } } - const sections = new Map(); + const sections = new Map(); for (let i = 0; i < resourceMarkers.length; i++) { const marker = resourceMarkers[i]; @@ -95,7 +105,7 @@ async function fetchZagDocs() { /** * Generate an index of all Zag component sections */ -function generateZagIndex(sections) { +function generateZagIndex(sections: Map): string { const sortedComponents = [...sections.keys()].sort(); let index = `# Zag.js Documentation Index\n\n`; @@ -111,7 +121,7 @@ function generateZagIndex(sections) { return index; } -export function registerZagTools(server) { +export function registerZagTools(server: McpServerType): void { server.tool( 'zag_docs', 'Fetch Zag.js documentation for building accessible UI components with SolidJS. Returns an index of all components when no path is provided, or specific component documentation when a path is given. Use this for accordion, dialog, tooltip, tabs, and all other Zag UI components.', @@ -123,7 +133,9 @@ export function registerZagTools(server) { ) .optional(), }, - async ({ path: componentName }) => { + async ({ + path: componentName, + }): Promise<{ content: Array<{ type: 'text'; text: string }> }> => { try { const { sections } = await fetchZagDocs(); @@ -140,7 +152,7 @@ export function registerZagTools(server) { content: [ { type: 'text', - text: `# Zag.js: ${normalizedName}\n\n${sections.get(normalizedName)}`, + text: `# Zag.js: ${normalizedName}\n\n${sections.get(normalizedName)!}`, }, ], }; @@ -154,7 +166,7 @@ export function registerZagTools(server) { if (matches.length === 1) { return { content: [ - { type: 'text', text: `# Zag.js: ${matches[0]}\n\n${sections.get(matches[0])}` }, + { type: 'text', text: `# Zag.js: ${matches[0]}\n\n${sections.get(matches[0])!}` }, ], }; } @@ -179,8 +191,9 @@ export function registerZagTools(server) { ], }; } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); return { - content: [{ type: 'text', text: `Error fetching Zag docs: ${error.message}` }], + content: [{ type: 'text', text: `Error fetching Zag docs: ${errorMessage}` }], }; } }, diff --git a/packages/mcp/src/types.ts b/packages/mcp/src/types.ts new file mode 100644 index 000000000..f11710f28 --- /dev/null +++ b/packages/mcp/src/types.ts @@ -0,0 +1,3 @@ +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; + +export type McpServerType = McpServer; diff --git a/packages/mcp/tsconfig.json b/packages/mcp/tsconfig.json new file mode 100644 index 000000000..377371da5 --- /dev/null +++ b/packages/mcp/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "bundler", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "resolveJsonModule": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "__tests__"] +} diff --git a/packages/mcp/vitest.config.js b/packages/mcp/vitest.config.ts similarity index 75% rename from packages/mcp/vitest.config.js rename to packages/mcp/vitest.config.ts index 53e18806d..c6814e753 100644 --- a/packages/mcp/vitest.config.js +++ b/packages/mcp/vitest.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, - include: ['__tests__/**/*.{test,spec}.js'], + include: ['__tests__/**/*.{test,spec}.{js,ts}'], testTimeout: 10000, // 10s for network requests }, }); diff --git a/packages/ui/jsconfig.json b/packages/ui/jsconfig.json deleted file mode 100644 index 9ca6f6dac..000000000 --- a/packages/ui/jsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["src/*"], - "@zag/*": ["src/zag/*"] - } - }, - "exclude": ["node_modules"] -} diff --git a/packages/ui/package.json b/packages/ui/package.json index 972703467..2f33d06d7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -4,8 +4,8 @@ "private": true, "description": "Shared UI components for Corates built with Ark UI and SolidJS", "type": "module", - "main": "./src/index.js", - "types": "./src/index.d.ts", + "main": "./src/index.ts", + "types": "./src/index.ts", "scripts": { "test": "vitest run", "test:watch": "vitest", @@ -13,11 +13,11 @@ }, "exports": { ".": { - "types": "./src/index.d.ts", - "default": "./src/index.js" + "types": "./src/index.ts", + "default": "./src/index.ts" }, - "./zag": "./src/zag/index.js", - "./zag/*": "./src/zag/*.jsx" + "./zag": "./src/zag/index.ts", + "./zag/*": "./src/zag/*.tsx" }, "dependencies": { "@ark-ui/solid": "^5.30.0", @@ -36,6 +36,7 @@ "jsdom": "^27.3.0", "solid-icons": "^1.1.0", "solid-js": "^1.9.10", + "typescript": "^5.9.3", "vite-plugin-solid": "^2.11.10", "vitest": "^4.0.16" } diff --git a/packages/ui/src/__tests__/setup.js b/packages/ui/src/__tests__/setup.ts similarity index 70% rename from packages/ui/src/__tests__/setup.js rename to packages/ui/src/__tests__/setup.ts index b8d5171e0..9554102cb 100644 --- a/packages/ui/src/__tests__/setup.js +++ b/packages/ui/src/__tests__/setup.ts @@ -17,7 +17,7 @@ vi.stubGlobal('import.meta', { // Mock window.matchMedia Object.defineProperty(window, 'matchMedia', { writable: true, - value: vi.fn().mockImplementation(query => ({ + value: vi.fn().mockImplementation((query: string) => ({ matches: false, media: query, onchange: null, @@ -31,35 +31,37 @@ Object.defineProperty(window, 'matchMedia', { // Mock ResizeObserver as a proper class class MockResizeObserver { - constructor(callback) { + callback: ResizeObserverCallback; + constructor(callback: ResizeObserverCallback) { this.callback = callback; } observe() {} unobserve() {} disconnect() {} } -global.ResizeObserver = MockResizeObserver; +global.ResizeObserver = MockResizeObserver as typeof ResizeObserver; // Mock IntersectionObserver as a proper class class MockIntersectionObserver { - constructor(callback) { + callback: IntersectionObserverCallback; + constructor(callback: IntersectionObserverCallback) { this.callback = callback; } observe() {} unobserve() {} disconnect() {} } -global.IntersectionObserver = MockIntersectionObserver; +global.IntersectionObserver = MockIntersectionObserver as typeof IntersectionObserver; // Mock crypto.randomUUID if (!global.crypto) { - global.crypto = {}; + global.crypto = {} as Crypto; } global.crypto.randomUUID = vi.fn(() => 'test-uuid-' + Math.random().toString(36).slice(2)); // Mock requestAnimationFrame -global.requestAnimationFrame = vi.fn(cb => setTimeout(cb, 0)); -global.cancelAnimationFrame = vi.fn(id => clearTimeout(id)); +global.requestAnimationFrame = vi.fn((cb: FrameRequestCallback) => setTimeout(cb, 0)); +global.cancelAnimationFrame = vi.fn((id: number) => clearTimeout(id)); // Mock Element.prototype methods used by Zag Element.prototype.scrollIntoView = vi.fn(); @@ -68,8 +70,8 @@ Element.prototype.blur = vi.fn(); // Mock getComputedStyle for Zag positioning const originalGetComputedStyle = window.getComputedStyle; -window.getComputedStyle = vi.fn().mockImplementation(element => { - const styles = originalGetComputedStyle?.(element) || {}; +window.getComputedStyle = vi.fn().mockImplementation((element: Element) => { + const styles = originalGetComputedStyle?.(element) || ({} as CSSStyleDeclaration); return { ...styles, getPropertyValue: vi.fn().mockReturnValue(''), @@ -77,7 +79,7 @@ window.getComputedStyle = vi.fn().mockImplementation(element => { display: 'block', visibility: 'visible', overflow: 'visible', - }; + } as CSSStyleDeclaration; }); // Mock getBoundingClientRect for Zag positioning @@ -91,19 +93,22 @@ Element.prototype.getBoundingClientRect = vi.fn().mockReturnValue({ x: 0, y: 0, toJSON: vi.fn(), -}); +} as DOMRect); // Mock PointerEvent if not available if (typeof PointerEvent === 'undefined') { class MockPointerEvent extends MouseEvent { - constructor(type, props) { + pointerId: number; + pointerType: string; + isPrimary: boolean; + constructor(type: string, props?: PointerEventInit) { super(type, props); this.pointerId = props?.pointerId ?? 0; this.pointerType = props?.pointerType ?? 'mouse'; this.isPrimary = props?.isPrimary ?? true; } } - global.PointerEvent = MockPointerEvent; + global.PointerEvent = MockPointerEvent as typeof PointerEvent; } // Mock clipboard API diff --git a/packages/ui/src/constants/zIndex.js b/packages/ui/src/constants/zIndex.ts similarity index 99% rename from packages/ui/src/constants/zIndex.js rename to packages/ui/src/constants/zIndex.ts index 6b1964073..676b3d2f3 100644 --- a/packages/ui/src/constants/zIndex.js +++ b/packages/ui/src/constants/zIndex.ts @@ -44,4 +44,4 @@ export const Z_INDEX = { /** Banner - System-level banners (e.g., ImpersonationBanner) */ BANNER: 'z-100', -}; +} as const; diff --git a/packages/ui/src/index.d.ts b/packages/ui/src/index.d.ts deleted file mode 100644 index 39abea5c7..000000000 --- a/packages/ui/src/index.d.ts +++ /dev/null @@ -1,1043 +0,0 @@ -import { Component, JSX, Accessor } from 'solid-js'; - -// ============================================================================ -// Common Types -// ============================================================================ - -export type Placement = - | 'top' - | 'top-start' - | 'top-end' - | 'bottom' - | 'bottom-start' - | 'bottom-end' - | 'left' - | 'left-start' - | 'left-end' - | 'right' - | 'right-start' - | 'right-end'; - -// ============================================================================ -// Accordion -// ============================================================================ - -export interface AccordionItem { - value: string; - title: JSX.Element; - content: JSX.Element; - disabled?: boolean; -} - -export interface AccordionProps { - /** Accordion items */ - items: AccordionItem[]; - /** Initially expanded items */ - defaultValue?: string[]; - /** Controlled expanded items */ - value?: string[]; - /** Callback when expanded items change */ - onValueChange?: (_details: { value: string[] }) => void; - /** Allow multiple items expanded (default: false) */ - multiple?: boolean; - /** Allow collapsing all items (default: true) */ - collapsible?: boolean; - /** Disable all items */ - disabled?: boolean; - /** Orientation (default: 'vertical') */ - orientation?: 'horizontal' | 'vertical'; - /** Additional class for root element */ - class?: string; -} - -export const Accordion: Component; - -// ============================================================================ -// Avatar -// ============================================================================ - -export interface AvatarProps { - /** Image source URL */ - src?: string; - /** Display name for initials fallback */ - name?: string; - /** Alt text for image */ - alt?: string; - /** Callback when image load status changes */ - onStatusChange?: (_details: { status: 'loading' | 'loaded' | 'error' }) => void; - /** Additional class for root element */ - class?: string; - /** Additional class for fallback element */ - fallbackClass?: string; -} - -export const Avatar: Component; - -// ============================================================================ -// Checkbox -// ============================================================================ - -export interface CheckboxProps { - /** Controlled checked state */ - checked?: boolean; - /** Default checked state (uncontrolled) */ - defaultChecked?: boolean; - /** Whether checkbox is in indeterminate state */ - indeterminate?: boolean; - /** Whether checkbox is disabled */ - disabled?: boolean; - /** Name for form submission */ - name?: string; - /** Value for form submission */ - value?: string; - /** Label text */ - label?: string; - /** Callback when checked state changes */ - onChange?: (_checked: boolean) => void; - /** Additional CSS classes */ - class?: string; -} - -export const Checkbox: Component; - -// ============================================================================ -// Clipboard -// ============================================================================ - -export interface ClipboardApi { - /** Current value */ - value: string; - /** Whether content was copied */ - copied: boolean; - /** Copy to clipboard */ - copy: () => void; - /** Get root props */ - getRootProps: () => JSX.HTMLAttributes; - /** Get label props */ - getLabelProps: () => JSX.HTMLAttributes; - /** Get control props */ - getControlProps: () => JSX.HTMLAttributes; - /** Get input props */ - getInputProps: () => JSX.HTMLAttributes; - /** Get trigger props */ - getTriggerProps: () => JSX.HTMLAttributes; -} - -export interface ClipboardProps { - /** Value to copy to clipboard */ - value?: string; - /** Initial value to copy */ - defaultValue?: string; - /** Callback when value changes */ - onValueChange?: (_details: { value: string }) => void; - /** Callback when copy status changes */ - onStatusChange?: (_details: { copied: boolean }) => void; - /** Time in ms before resetting copied state (default: 3000) */ - timeout?: number; - /** Label for the input */ - label?: string; - /** Show input field (default: true) */ - showInput?: boolean; - /** Render function for custom UI */ - children?: (_api: Accessor) => JSX.Element; - /** Additional class for root element */ - class?: string; -} - -export const Clipboard: Component; - -// ============================================================================ -// CopyButton -// ============================================================================ - -export interface CopyButtonProps { - /** Value to copy */ - value: string; - /** Callback when copy status changes */ - onStatusChange?: (_details: { copied: boolean }) => void; - /** Time in ms before resetting (default: 3000) */ - timeout?: number; - /** Button label (default: 'Copy') */ - label?: string; - /** Label when copied (default: 'Copied!') */ - copiedLabel?: string; - /** Button size (default: 'md') */ - size?: 'sm' | 'md' | 'lg'; - /** Button variant (default: 'solid') */ - variant?: 'solid' | 'outline' | 'ghost'; - /** Show copy/check icon (default: true) */ - showIcon?: boolean; - /** Show text label (default: true) */ - showLabel?: boolean; - /** Additional class for button */ - class?: string; -} - -export const CopyButton: Component; - -// ============================================================================ -// Collapsible -// ============================================================================ - -export interface CollapsibleApi { - open: boolean; - visible: boolean; - setOpen: (_open: boolean) => void; -} - -export interface CollapsibleProps { - /** Controlled open state */ - open?: boolean; - /** Initial open state (uncontrolled) */ - defaultOpen?: boolean; - /** Callback when open state changes */ - onOpenChange?: (_details: { open: boolean }) => void; - /** Disable the collapsible */ - disabled?: boolean; - /** Enable lazy mounting */ - lazyMount?: boolean; - /** Unmount content when closed */ - unmountOnExit?: boolean; - /** Height when collapsed */ - collapsedHeight?: string | number; - /** Width when collapsed */ - collapsedWidth?: string | number; - /** Callback when exit animation completes */ - onExitComplete?: () => void; - /** Custom IDs for root, content, trigger */ - ids?: { root?: string; content?: string; trigger?: string }; - /** Trigger element or render function receiving collapsible API */ - trigger?: JSX.Element | ((_api: CollapsibleApi) => JSX.Element); - /** Indicator element or render function receiving collapsible API */ - indicator?: JSX.Element | ((_api: CollapsibleApi) => JSX.Element); - /** Collapsible content */ - children?: JSX.Element; -} - -export const Collapsible: Component & { - Root: Component; - Trigger: Component; - Content: Component; - Indicator: Component; - RootProvider: Component; -}; - -export function useCollapsible(_props?: any): Accessor; - -// ============================================================================ -// Combobox -// ============================================================================ - -export interface ComboboxItem { - value: string; - label: string; - disabled?: boolean; -} - -export interface ComboboxProps { - /** Available items */ - items: ComboboxItem[]; - /** Input label */ - label?: string; - /** Input placeholder */ - placeholder?: string; - /** Controlled selected values */ - value?: string[]; - /** Initial selected values */ - defaultValue?: string[]; - /** Callback when selection changes */ - onValueChange?: (_details: { value: string[]; items: ComboboxItem[] }) => void; - /** Callback when input changes */ - onInputValueChange?: (_details: { inputValue: string }) => void; - /** Allow multiple selections (default: false) */ - multiple?: boolean; - /** Disable the combobox */ - disabled?: boolean; - /** Make combobox read-only */ - readOnly?: boolean; - /** Mark as invalid */ - invalid?: boolean; - /** Form field name */ - name?: string; - /** Allow custom values not in the list */ - allowCustomValue?: boolean; - /** Close on selection (default: true for single, false for multiple) */ - closeOnSelect?: boolean; - /** Open on input click (default: true) */ - openOnClick?: boolean; - /** Set to true when used inside a Dialog */ - inDialog?: boolean; - /** Additional class for root element */ - class?: string; - /** Additional class for input element */ - inputClass?: string; -} - -export const Combobox: Component; - -// ============================================================================ -// Dialog -// ============================================================================ - -export interface DialogProps { - /** Controlled open state */ - open?: boolean; - /** Callback when open state changes */ - onOpenChange?: (_open: boolean) => void; - /** Dialog title */ - title?: string; - /** Dialog description */ - description?: string; - /** Dialog content */ - children?: JSX.Element; -} - -export const Dialog: Component; - -export interface ConfirmDialogProps { - /** Controlled open state */ - open?: boolean; - /** Callback when open state changes */ - onOpenChange?: (_open: boolean) => void; - /** Dialog title */ - title?: string; - /** Dialog description */ - description?: string; - /** Confirm button label */ - confirmLabel?: string; - /** Cancel button label */ - cancelLabel?: string; - /** Callback when confirmed */ - onConfirm?: () => void; - /** Callback when cancelled */ - onCancel?: () => void; - /** Use destructive styling */ - destructive?: boolean; -} - -export const ConfirmDialog: Component; - -export function useConfirmDialog(): { - open: () => boolean; - setOpen: (_open: boolean) => void; - confirm: () => Promise; -}; - -// ============================================================================ -// Drawer -// ============================================================================ - -export interface DrawerProps { - /** Whether the drawer is open */ - open?: boolean; - /** Callback when open state changes */ - onOpenChange?: (_open: boolean) => void; - /** Drawer title */ - title?: string; - /** Optional description below title */ - description?: string; - /** Drawer content */ - children?: JSX.Element; - /** Which side the drawer slides in from (default: 'right') */ - side?: 'left' | 'right'; - /** Drawer width (default: 'md') */ - size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'; - /** Whether to show the header (default: true) */ - showHeader?: boolean; - /** Close when clicking backdrop (default: true) */ - closeOnOutsideClick?: boolean; - /** Whether to show the dark overlay backdrop (default: true) */ - showBackdrop?: boolean; -} - -export const Drawer: Component; - -// ============================================================================ -// Editable -// ============================================================================ - -export interface EditableProps { - /** Current value */ - value?: string; - /** Callback when value is submitted */ - onSubmit?: (_value: string) => void; - /** How to activate edit mode */ - activationMode?: 'focus' | 'dblclick' | 'click' | 'none'; - /** Style variant */ - variant?: 'default' | 'heading' | 'inline' | 'field'; - /** Show edit icon */ - showEditIcon?: boolean; - /** Make read-only */ - readOnly?: boolean; - /** Additional CSS class */ - class?: string; - /** Placeholder text */ - placeholder?: string; -} - -export const Editable: Component; - -// ============================================================================ -// FileUpload -// ============================================================================ - -export interface FileUploadProps { - /** Accepted file types (e.g., 'application/pdf') */ - accept?: string; - /** Allow multiple file selection (default: false) */ - multiple?: boolean; - /** Callback when files change */ - onFilesChange?: (_files: File[]) => void; - /** Callback when files are accepted */ - onFileAccept?: (_details: { files: File[] }) => void; - /** Custom text for the dropzone */ - dropzoneText?: string; - /** Custom text for the trigger button */ - buttonText?: string; - /** Helper text below the dropzone text */ - helpText?: string; - /** Whether to show the list of uploaded files (default: true) */ - showFileList?: boolean; - /** Additional CSS classes for the root element */ - class?: string; - /** Additional CSS classes for the dropzone */ - dropzoneClass?: string; - /** Disable the file upload */ - disabled?: boolean; - /** Use compact/minimal styling */ - compact?: boolean; - /** Allow dropping directories (default: true for multiple) */ - allowDirectories?: boolean; -} - -export const FileUpload: Component; - -// ============================================================================ -// FloatingPanel -// ============================================================================ - -export interface FloatingPanelSize { - width: number; - height: number; -} - -export interface FloatingPanelPosition { - x: number; - y: number; -} - -export interface FloatingPanelProps { - /** Whether the panel is open (controlled) */ - open?: boolean; - /** Initial open state (uncontrolled) */ - defaultOpen?: boolean; - /** Callback when open state changes */ - onOpenChange?: (_details: { open: boolean }) => void; - /** Panel title */ - title?: string; - /** Panel content */ - children?: JSX.Element; - /** Initial size */ - defaultSize?: FloatingPanelSize; - /** Initial position */ - defaultPosition?: FloatingPanelPosition; - /** Controlled size */ - size?: FloatingPanelSize; - /** Controlled position */ - position?: FloatingPanelPosition; - /** Callback when size changes */ - onSizeChange?: (_details: { size: FloatingPanelSize }) => void; - /** Callback when position changes */ - onPositionChange?: (_details: { position: FloatingPanelPosition }) => void; - /** Callback when stage changes */ - onStageChange?: (_details: { stage: 'minimized' | 'maximized' | 'default' }) => void; - /** Whether the panel can be resized (default: true) */ - resizable?: boolean; - /** Whether the panel can be dragged (default: true) */ - draggable?: boolean; - /** Minimum size constraints */ - minSize?: FloatingPanelSize; - /** Maximum size constraints */ - maxSize?: FloatingPanelSize; - /** Lock aspect ratio when resizing */ - lockAspectRatio?: boolean; - /** Show all control buttons (default: true) */ - showControls?: boolean; - /** Show minimize button (default: true, requires showControls) */ - showMinimize?: boolean; - /** Show maximize button (default: true, requires showControls) */ - showMaximize?: boolean; - /** Show restore button (default: true, requires showControls) */ - showRestore?: boolean; - /** Show close button (default: true, requires showControls) */ - showClose?: boolean; - /** Close panel on Escape key (default: true) */ - closeOnEscape?: boolean; - /** Persist size/position when closed (default: false) */ - persistRect?: boolean; -} - -export const FloatingPanel: Component; - -// ============================================================================ -// Menu -// ============================================================================ - -export interface MenuItem { - value: string; - label: string; - icon?: JSX.Element; - destructive?: boolean; - separator?: boolean; -} - -export interface MenuProps { - /** Trigger element */ - trigger: JSX.Element; - /** Menu items */ - items: MenuItem[]; - /** Callback when item is selected */ - onSelect?: (_details: { value: string }) => void; - /** Menu placement */ - placement?: Placement; - /** Hide the indicator arrow */ - hideIndicator?: boolean; -} - -export const Menu: Component; - -// ============================================================================ -// NumberInput -// ============================================================================ - -export interface NumberInputProps { - /** Input label */ - label?: string; - /** Controlled value (as string for formatting) */ - value?: string; - /** Initial value */ - defaultValue?: string; - /** Callback when value changes */ - onValueChange?: (_details: { value: string; valueAsNumber: number }) => void; - /** Minimum value */ - min?: number; - /** Maximum value */ - max?: number; - /** Increment/decrement step (default: 1) */ - step?: number; - /** Disable the input */ - disabled?: boolean; - /** Make input read-only */ - readOnly?: boolean; - /** Mark as invalid */ - invalid?: boolean; - /** Mark as required */ - required?: boolean; - /** Form field name */ - name?: string; - /** Input placeholder */ - placeholder?: string; - /** Allow mouse wheel to change value (default: false) */ - allowMouseWheel?: boolean; - /** Clamp value to min/max on blur (default: true) */ - clampValueOnBlur?: boolean; - /** Spin on button press and hold (default: true) */ - spinOnPress?: boolean; - /** Number format options */ - formatOptions?: Intl.NumberFormatOptions; - /** Show increment/decrement buttons (default: true) */ - showControls?: boolean; - /** Input size (default: 'md') */ - size?: 'sm' | 'md' | 'lg'; - /** Additional class for root element */ - class?: string; - /** Additional class for input element */ - inputClass?: string; -} - -export const NumberInput: Component; - -// ============================================================================ -// PasswordInput -// ============================================================================ - -export interface PasswordInputProps { - /** Current password value */ - password?: string; - /** Callback when password changes */ - onPasswordChange?: (_value: string) => void; - /** Input label */ - label?: string; - /** Autocomplete attribute */ - autoComplete?: string; - /** Mark as required */ - required?: boolean; - /** Additional class for root element */ - class?: string; - /** Additional class for input element */ - inputClass?: string; - /** Size of the visibility toggle icon */ - iconSize?: number; -} - -export const PasswordInput: Component; - -// ============================================================================ -// PinInput -// ============================================================================ - -export interface PinInputProps { - /** Callback when value changes */ - onInput?: (_value: string) => void; - /** Callback when all digits are entered */ - onComplete?: (_value: string) => void; - /** Mark as required (default: true) */ - required?: boolean; - /** Enable OTP mode (default: true) */ - otp?: boolean; - /** Autocomplete attribute (default: 'one-time-code') */ - autoComplete?: string; - /** Show error styling */ - isError?: boolean; -} - -export const PinInput: Component; - -// ============================================================================ -// Popover -// ============================================================================ - -export interface PopoverProps { - /** The trigger element */ - trigger: JSX.Element; - /** Popover content */ - children?: JSX.Element; - /** Optional popover title */ - title?: string; - /** Optional popover description */ - description?: string; - /** Controlled open state */ - open?: boolean; - /** Initial open state */ - defaultOpen?: boolean; - /** Callback when open state changes */ - onOpenChange?: (_details: { open: boolean }) => void; - /** Popover placement (default: 'bottom') */ - placement?: Placement; - /** Whether to trap focus (default: false) */ - modal?: boolean; - /** Close on outside click (default: true) */ - closeOnInteractOutside?: boolean; - /** Close on escape key (default: true) */ - closeOnEscape?: boolean; - /** Show arrow pointing to trigger (default: false) */ - showArrow?: boolean; - /** Show close button in header (default: true) */ - showCloseButton?: boolean; - /** Set to true when used inside a Dialog */ - inDialog?: boolean; - /** Additional class for content */ - class?: string; -} - -export const Popover: Component; - -// ============================================================================ -// Progress -// ============================================================================ - -export interface ProgressProps { - /** Current progress value */ - value?: number; - /** Minimum value (default: 0) */ - min?: number; - /** Maximum value (default: 100) */ - max?: number; - /** Accessible label */ - label?: string; - /** Show percentage value (default: false) */ - showValue?: boolean; - /** Bar height (default: 'md') */ - size?: 'sm' | 'md' | 'lg'; - /** Color variant (default: 'default') */ - variant?: 'default' | 'success' | 'warning' | 'error'; - /** Show indeterminate animation */ - indeterminate?: boolean; - /** Additional class for root element */ - class?: string; -} - -export const Progress: Component; - -// ============================================================================ -// QRCode -// ============================================================================ - -export interface QRCodeProps { - /** The data to encode in the QR code (e.g., URL, text) */ - data: string; - /** Size of the QR code in pixels (default: 200) */ - size?: number; - /** Additional CSS classes */ - class?: string; - /** Alt text for accessibility (default: 'QR Code') */ - alt?: string; - /** Error correction level (L=7%, M=15%, Q=25%, H=30%) (default: 'M') */ - ecc?: 'L' | 'M' | 'Q' | 'H'; -} - -export const QRCode: Component; - -// ============================================================================ -// RadioGroup -// ============================================================================ - -export interface RadioGroupItem { - value: string; - label: string; - description?: string; - disabled?: boolean; -} - -export interface RadioGroupProps { - /** Radio items */ - items: RadioGroupItem[]; - /** Group label */ - label?: string; - /** Controlled selected value */ - value?: string; - /** Initial selected value */ - defaultValue?: string; - /** Callback when selection changes */ - onValueChange?: (_details: { value: string }) => void; - /** Form field name */ - name?: string; - /** Disable all items */ - disabled?: boolean; - /** Layout orientation (default: 'vertical') */ - orientation?: 'horizontal' | 'vertical'; - /** Additional class for root element */ - class?: string; -} - -export const RadioGroup: Component; - -// ============================================================================ -// Select -// ============================================================================ - -export interface SelectOption { - value: string; - label: string; -} - -export interface SelectProps { - /** Available options */ - options: SelectOption[]; - /** Controlled selected value */ - value?: string; - /** Callback when selection changes */ - onChange?: (_value: string) => void; - /** Placeholder text */ - placeholder?: string; - /** Disable the select */ - disabled?: boolean; - /** Set to true when used inside a Dialog or Popover */ - inDialog?: boolean; -} - -export const Select: Component; - -// ============================================================================ -// Splitter -// ============================================================================ - -export interface SplitterPanel { - id: string; - minSize?: number; - maxSize?: number; -} - -export interface SplitterProps { - /** Default panel sizes as percentages */ - defaultSize?: number[]; - /** Panel configurations */ - panels?: SplitterPanel[]; - /** Orientation (default: 'horizontal') */ - orientation?: 'horizontal' | 'vertical'; - /** Additional class for root element */ - class?: string; -} - -export const Splitter: Component; - -// ============================================================================ -// Switch -// ============================================================================ - -export interface SwitchProps { - /** Controlled checked state */ - checked?: boolean; - /** Default checked state (uncontrolled) */ - defaultChecked?: boolean; - /** Whether switch is disabled */ - disabled?: boolean; - /** Form field name */ - name?: string; - /** Callback when checked state changes */ - onChange?: (_checked: boolean) => void; - /** Additional CSS classes */ - class?: string; -} - -export const Switch: Component; - -// ============================================================================ -// Tabs -// ============================================================================ - -export interface TabDefinition { - value: string; - label: string; - icon?: JSX.Element; - count?: number; - getCount?: () => number; -} - -export interface TabsProps { - /** Array of tab definitions */ - tabs: TabDefinition[]; - /** Default selected tab value */ - defaultValue?: string; - /** Controlled tab value */ - value?: string; - /** Callback when tab changes */ - onValueChange?: (_value: string) => void; - /** Tab content render function */ - children?: (_tabValue: string) => JSX.Element; -} - -export const Tabs: Component; - -// ============================================================================ -// TagsInput -// ============================================================================ - -export interface TagsInputProps { - /** Input label */ - label?: string; - /** Input placeholder (default: 'Add tag...') */ - placeholder?: string; - /** Controlled tag values */ - value?: string[]; - /** Initial tag values */ - defaultValue?: string[]; - /** Callback when tags change */ - onValueChange?: (_details: { value: string[] }) => void; - /** Maximum number of tags */ - max?: number; - /** Allow duplicate tags (default: false) */ - allowDuplicates?: boolean; - /** Disable the input */ - disabled?: boolean; - /** Make input read-only */ - readOnly?: boolean; - /** Mark as invalid */ - invalid?: boolean; - /** Form field name */ - name?: string; - /** What to do with input on blur */ - blurBehavior?: 'add' | 'clear'; - /** Add tags when pasting (default: true) */ - addOnPaste?: boolean; - /** Allow editing tags (default: true) */ - editable?: boolean; - /** Additional class for root element */ - class?: string; - /** Additional class for input element */ - inputClass?: string; -} - -export const TagsInput: Component; - -// ============================================================================ -// Toast -// ============================================================================ - -export interface ToastOptions { - /** Toast title */ - title?: string; - /** Toast description */ - description?: string; - /** Toast type */ - type?: 'info' | 'success' | 'warning' | 'error'; - /** Duration in milliseconds */ - duration?: number; -} - -export const Toaster: Component; - -export const toaster: { - create: (_options: ToastOptions) => void; - success: (_options: ToastOptions) => void; - error: (_options: ToastOptions) => void; - warning: (_options: ToastOptions) => void; - info: (_options: ToastOptions) => void; -}; - -export function showToast(_options: ToastOptions): void; - -// ============================================================================ -// ToggleGroup -// ============================================================================ - -export interface ToggleGroupItem { - value: string; - label: JSX.Element; - disabled?: boolean; -} - -export interface ToggleGroupProps { - /** Toggle items */ - items: ToggleGroupItem[]; - /** Controlled selected values */ - value?: string[]; - /** Initial selected values */ - defaultValue?: string[]; - /** Callback when selection changes */ - onValueChange?: (_details: { value: string[] }) => void; - /** Allow multiple selections (default: false) */ - multiple?: boolean; - /** Disable all toggles */ - disabled?: boolean; - /** Layout orientation (default: 'horizontal') */ - orientation?: 'horizontal' | 'vertical'; - /** Loop focus navigation (default: true) */ - loop?: boolean; - /** Use roving tabindex (default: true) */ - rovingFocus?: boolean; - /** Allow deselecting when single (default: true) */ - deselectable?: boolean; - /** Button size (default: 'md') */ - size?: 'sm' | 'md' | 'lg'; - /** Additional class for root element */ - class?: string; -} - -export const ToggleGroup: Component; - -// ============================================================================ -// Tooltip -// ============================================================================ - -export interface TooltipProps { - /** Tooltip content */ - content: string | JSX.Element; - /** Trigger element */ - children: JSX.Element; - /** Tooltip placement */ - placement?: Placement; -} - -export const Tooltip: Component; - -// ============================================================================ -// Tour -// ============================================================================ - -export interface TourStepAction { - label: string; - action: 'next' | 'prev' | 'dismiss'; -} - -export interface TourStep { - /** Unique step identifier */ - id: string; - /** Step type */ - type: 'tooltip' | 'dialog' | 'floating' | 'wait'; - /** Step title */ - title: string; - /** Step description */ - description: string; - /** Target element for tooltip steps */ - target?: () => Element | null; - /** Tooltip placement */ - placement?: Placement; - /** Step actions */ - actions?: TourStepAction[]; - /** Show backdrop (default: true) */ - backdrop?: boolean; - /** Show arrow for tooltips (default: true) */ - arrow?: boolean; - /** Side effect before showing step */ - effect?: (_ctx: { - next: () => void; - show: () => void; - update: () => void; - }) => void | (() => void); -} - -export interface TourApi { - /** Whether tour is open */ - open: boolean; - /** Current step */ - step: TourStep | null; - /** Start the tour */ - start: () => void; - /** Stop the tour */ - stop: () => void; - /** Go to next step */ - next: () => void; - /** Go to previous step */ - prev: () => void; - /** Get progress text */ - getProgressText: () => string; -} - -export interface TourProviderProps { - /** Tour steps configuration */ - steps: TourStep[]; - /** Callback when step changes */ - onStepChange?: (_details: { stepId: string; stepIndex: number }) => void; - /** Callback when tour status changes */ - onStatusChange?: (_details: { status: 'started' | 'stopped' | 'completed' | 'skipped' }) => void; - /** Close on outside click (default: true) */ - closeOnInteractOutside?: boolean; - /** Close on escape key (default: true) */ - closeOnEscape?: boolean; - /** Allow arrow key navigation (default: true) */ - keyboardNavigation?: boolean; - /** Prevent page interaction during tour */ - preventInteraction?: boolean; - /** Spotlight padding offset */ - spotlightOffset?: { x: number; y: number }; - /** Spotlight border radius */ - spotlightRadius?: number; - /** Child components */ - children: JSX.Element; -} - -export const TourProvider: Component; - -export interface TourProps extends Omit { - /** Render function for trigger */ - renderTrigger?: (_api: Accessor) => JSX.Element; -} - -export const Tour: Component; - -export function useTour(): Accessor; - -// ============================================================================ -// Primitives -// ============================================================================ - -export function useWindowDrag(_options?: { onDragEnd?: () => void }): { - isDragging: () => boolean; -}; - -// ============================================================================ -// Utilities -// ============================================================================ - -export function cn(..._classes: (string | undefined | null | false)[]): string; diff --git a/packages/ui/src/index.js b/packages/ui/src/index.ts similarity index 52% rename from packages/ui/src/index.js rename to packages/ui/src/index.ts index 8111de84e..cc7237b11 100644 --- a/packages/ui/src/index.js +++ b/packages/ui/src/index.ts @@ -1,13 +1,13 @@ // @corates/ui - Shared UI Components // Re-export all UI components (built with Ark UI and some remaining Zag.js components) -export * from './zag/index.js'; +export * from './zag/index.ts'; // Re-export primitives -export { useWindowDrag } from './primitives/useWindowDrag.js'; +export { useWindowDrag } from './primitives/useWindowDrag.ts'; // Re-export utilities -export { cn } from './lib/cn.js'; +export { cn } from './lib/cn.ts'; // Re-export constants -export { Z_INDEX } from './constants/zIndex.js'; +export { Z_INDEX } from './constants/zIndex.ts'; diff --git a/packages/ui/src/lib/cn.js b/packages/ui/src/lib/cn.ts similarity index 73% rename from packages/ui/src/lib/cn.js rename to packages/ui/src/lib/cn.ts index d1ad3816a..c65b80eb7 100644 --- a/packages/ui/src/lib/cn.js +++ b/packages/ui/src/lib/cn.ts @@ -9,10 +9,7 @@ import { twMerge } from 'tailwind-merge'; * cn('px-2 py-1', 'px-4') // → 'py-1 px-4' (px-4 wins) * cn('p-4', props.class) // → consumer can override padding * cn('text-sm', large && 'text-lg') // → conditional classes - * - * @param {...(string|object|array|boolean|null|undefined)} inputs - Class values to merge - * @returns {string} Merged class string */ -export function cn(...inputs) { +export function cn(...inputs: (string | undefined | null | false)[]): string { return twMerge(clsx(inputs)); } diff --git a/packages/ui/src/primitives/useWindowDrag.js b/packages/ui/src/primitives/useWindowDrag.ts similarity index 81% rename from packages/ui/src/primitives/useWindowDrag.js rename to packages/ui/src/primitives/useWindowDrag.ts index cc118e1d6..19b430a95 100644 --- a/packages/ui/src/primitives/useWindowDrag.js +++ b/packages/ui/src/primitives/useWindowDrag.ts @@ -3,15 +3,15 @@ import { createSignal, onCleanup, onMount } from 'solid-js'; /** * Primitive to detect when files are being dragged over the window * Useful for showing a global drop indicator when dragging from external sources (like Finder) - * - * @returns {{ isDraggingOverWindow: () => boolean }} */ -export function useWindowDrag() { +export function useWindowDrag(options?: { onDragEnd?: () => void }): { + isDraggingOverWindow: () => boolean; +} { const [isDraggingOverWindow, setIsDraggingOverWindow] = createSignal(false); let dragCounter = 0; onMount(() => { - const handleDragEnter = e => { + const handleDragEnter = (e: DragEvent) => { // Only respond to file drags if (e.dataTransfer?.types?.includes('Files')) { dragCounter++; @@ -29,7 +29,10 @@ export function useWindowDrag() { const handleDrop = () => { dragCounter = 0; // Delay state update to avoid interfering with drop handlers - setTimeout(() => setIsDraggingOverWindow(false), 50); + setTimeout(() => { + setIsDraggingOverWindow(false); + options?.onDragEnd?.(); + }, 50); }; window.addEventListener('dragenter', handleDragEnter); diff --git a/packages/ui/src/zag/Accordion.jsx b/packages/ui/src/zag/Accordion.tsx similarity index 69% rename from packages/ui/src/zag/Accordion.jsx rename to packages/ui/src/zag/Accordion.tsx index d42e6e73d..7f426a079 100644 --- a/packages/ui/src/zag/Accordion.jsx +++ b/packages/ui/src/zag/Accordion.tsx @@ -3,26 +3,43 @@ */ import { Accordion } from '@ark-ui/solid/accordion'; -import { For, splitProps } from 'solid-js'; +import { Component, For, splitProps, JSX } from 'solid-js'; + +export interface AccordionItem { + value: string; + title: JSX.Element; + content: JSX.Element; + disabled?: boolean; +} + +export interface AccordionProps { + /** Accordion items */ + items: AccordionItem[]; + /** Initially expanded items */ + defaultValue?: string[]; + /** Controlled expanded items */ + value?: string[]; + /** Callback when expanded items change */ + onValueChange?: (details: { value: string[] }) => void; + /** Allow multiple items expanded (default: false) */ + multiple?: boolean; + /** Allow collapsing all items (default: true) */ + collapsible?: boolean; + /** Disable all items */ + disabled?: boolean; + /** Orientation (default: 'vertical') */ + orientation?: 'horizontal' | 'vertical'; + /** Additional class for root element */ + class?: string; +} /** * Accordion - Vertically stacked expandable sections - * - * Props: - * - items: Array<{ value: string, title: JSX.Element, content: JSX.Element, disabled?: boolean }> - Accordion items - * - defaultValue: string[] - Initially expanded items - * - value: string[] - Controlled expanded items - * - onValueChange: (details: { value: string[] }) => void - Callback when expanded items change - * - multiple: boolean - Allow multiple items expanded (default: false) - * - collapsible: boolean - Allow collapsing all items (default: true) - * - disabled: boolean - Disable all items - * - orientation: 'horizontal' | 'vertical' - Orientation (default: 'vertical') - * - class: string - Additional class for root element */ -export default function AccordionComponent(props) { +const AccordionComponent: Component = (props) => { const [local, machineProps] = splitProps(props, ['items', 'class']); - const handleValueChange = details => { + const handleValueChange = (details: { value: string[] }) => { if (machineProps.onValueChange) { machineProps.onValueChange(details); } @@ -40,7 +57,7 @@ export default function AccordionComponent(props) { class={`divide-y divide-gray-200 rounded-lg border border-gray-200 ${local.class || ''}`} > - {item => ( + {(item) => (

@@ -65,6 +82,6 @@ export default function AccordionComponent(props) { ); -} +}; export { AccordionComponent as Accordion }; diff --git a/packages/ui/src/zag/Avatar.jsx b/packages/ui/src/zag/Avatar.tsx similarity index 67% rename from packages/ui/src/zag/Avatar.jsx rename to packages/ui/src/zag/Avatar.tsx index efb06f695..f316a2d24 100644 --- a/packages/ui/src/zag/Avatar.jsx +++ b/packages/ui/src/zag/Avatar.tsx @@ -3,19 +3,27 @@ */ import { Avatar } from '@ark-ui/solid/avatar'; +import { Component } from 'solid-js'; + +export interface AvatarProps { + /** Image source URL */ + src?: string; + /** Name for generating initials fallback */ + name?: string; + /** Alt text for image */ + alt?: string; + /** Callback when image loading status changes */ + onStatusChange?: (details: { status: 'loading' | 'loaded' | 'error' }) => void; + /** CSS classes for fallback element */ + fallbackClass?: string; + /** Additional class for root element */ + class?: string; +} /** * Avatar - User avatar with fallback support - * - * Props: - * - src: string - Image source URL - * - name: string - Name for generating initials fallback - * - alt: string - Alt text for image - * - onStatusChange: (details: StatusChangeDetails) => void - Callback when image loading status changes - * - fallbackClass: string - CSS classes for fallback element - * - class: string - Additional class for root element */ -export default function AvatarComponent(props) { +const AvatarComponent: Component = (props) => { const src = () => props.src; const name = () => props.name; const alt = () => props.alt || name() || 'Avatar'; @@ -44,6 +52,6 @@ export default function AvatarComponent(props) { /> ); -} +}; export { AvatarComponent as Avatar }; diff --git a/packages/ui/src/zag/Checkbox.jsx b/packages/ui/src/zag/Checkbox.tsx similarity index 76% rename from packages/ui/src/zag/Checkbox.jsx rename to packages/ui/src/zag/Checkbox.tsx index 9870a2134..dc9212ca6 100644 --- a/packages/ui/src/zag/Checkbox.jsx +++ b/packages/ui/src/zag/Checkbox.tsx @@ -5,24 +5,36 @@ */ import { Checkbox as ArkCheckbox, useCheckbox } from '@ark-ui/solid/checkbox'; -import { mergeProps, splitProps, Show, createMemo } from 'solid-js'; +import { Component, mergeProps, splitProps, Show, createMemo } from 'solid-js'; import { BiRegularCheck, BiRegularMinus } from 'solid-icons/bi'; +export interface CheckboxProps { + /** Controlled checked state */ + checked?: boolean; + /** Default checked state (uncontrolled) */ + defaultChecked?: boolean; + /** Whether checkbox is in indeterminate state */ + indeterminate?: boolean; + /** Whether checkbox is disabled */ + disabled?: boolean; + /** Name for form submission */ + name?: string; + /** Value for form submission */ + value?: string; + /** Label text */ + label?: string; + /** Callback when checked state changes */ + onChange?: (checked: boolean) => void; + /** Additional CSS classes */ + class?: string; + /** Callback when checked state changes (Ark UI compatible) */ + onCheckedChange?: (details: { checked: boolean | 'indeterminate' }) => void; +} + /** * Checkbox - Full description - * - * Props: - * - checked: boolean - Controlled checked state - * - defaultChecked: boolean - Default checked state (uncontrolled) - * - indeterminate: boolean - Whether checkbox is in indeterminate state - * - disabled: boolean - Whether checkbox is disabled - * - name: string - Name for form submission - * - value: string - Value for form submission - * - label: string - Label text - * - onChange: Function - Callback when checked state changes: (checked: boolean) => void - * - class: string - Additional CSS classes */ -export default function CheckboxComponent(props) { +const CheckboxComponent: Component = (props) => { const merged = mergeProps( { defaultChecked: false, @@ -55,7 +67,7 @@ export default function CheckboxComponent(props) { return defaultChecked(); }); - const handleCheckedChange = details => { + const handleCheckedChange = (details: { checked: boolean | 'indeterminate' }) => { if (onChange()) { // When transitioning from indeterminate, treat it as checking const newChecked = details.checked === true || details.checked === 'indeterminate'; @@ -93,7 +105,7 @@ export default function CheckboxComponent(props) { ); -} +}; export { CheckboxComponent as Checkbox }; diff --git a/packages/ui/src/zag/Clipboard.jsx b/packages/ui/src/zag/Clipboard.tsx similarity index 63% rename from packages/ui/src/zag/Clipboard.jsx rename to packages/ui/src/zag/Clipboard.tsx index 2fa1493aa..76966f232 100644 --- a/packages/ui/src/zag/Clipboard.jsx +++ b/packages/ui/src/zag/Clipboard.tsx @@ -3,35 +3,64 @@ */ import { Clipboard } from '@ark-ui/solid/clipboard'; -import { Show, splitProps, createSignal, createMemo } from 'solid-js'; +import { Component, Show, splitProps, createSignal, createMemo, Accessor, JSX } from 'solid-js'; import { FiCopy, FiCheck } from 'solid-icons/fi'; +export interface ClipboardApi { + /** Current value */ + value: string; + /** Whether content was copied */ + copied: boolean; + /** Copy to clipboard */ + copy: () => void; + /** Get root props */ + getRootProps: () => JSX.HTMLAttributes; + /** Get label props */ + getLabelProps: () => JSX.HTMLAttributes; + /** Get control props */ + getControlProps: () => JSX.HTMLAttributes; + /** Get input props */ + getInputProps: () => JSX.HTMLAttributes; + /** Get trigger props */ + getTriggerProps: () => JSX.HTMLAttributes; +} + +export interface ClipboardProps { + /** Value to copy to clipboard */ + value?: string; + /** Initial value to copy */ + defaultValue?: string; + /** Callback when value changes */ + onValueChange?: (details: { value: string }) => void; + /** Callback when copy status changes */ + onStatusChange?: (details: { copied: boolean }) => void; + /** Time in ms before resetting copied state (default: 3000) */ + timeout?: number; + /** Label for the input */ + label?: string; + /** Show input field (default: true) */ + showInput?: boolean; + /** Render function for custom UI */ + children?: (api: Accessor) => JSX.Element; + /** Additional class for root element */ + class?: string; +} + /** * Clipboard - Copy to clipboard functionality - * - * Props: - * - value: string - Value to copy to clipboard - * - defaultValue: string - Initial value to copy - * - onValueChange: (details: { value: string }) => void - Callback when value changes - * - onStatusChange: (details: { copied: boolean }) => void - Callback when copy status changes - * - timeout: number - Time in ms before resetting copied state (default: 3000) - * - label: string - Label for the input - * - showInput: boolean - Show input field (default: true) - * - children: (api: ClipboardApi) => JSX.Element - Render function for custom UI - * - class: string - Additional class for root element */ -export default function ClipboardComponent(props) { +const ClipboardComponent: Component = (props) => { const [local, machineProps] = splitProps(props, ['label', 'showInput', 'children', 'class']); const [copied, setCopied] = createSignal(false); - const handleStatusChange = details => { + const handleStatusChange = (details: { copied: boolean }) => { setCopied(details.copied); if (machineProps.onStatusChange) { machineProps.onStatusChange({ copied: details.copied }); } }; - const handleValueChange = details => { + const handleValueChange = (details: { value: string }) => { if (machineProps.onValueChange) { machineProps.onValueChange(details); } @@ -45,12 +74,18 @@ export default function ClipboardComponent(props) { copy: () => { // Trigger copy via the Clipboard.Trigger }, + value: machineProps.value || machineProps.defaultValue || '', + getRootProps: () => ({}), + getLabelProps: () => ({}), + getControlProps: () => ({}), + getInputProps: () => ({}), + getTriggerProps: () => ({}), })); const showInput = () => local.showInput !== false; return ( - + ); +}; + +export interface CopyButtonProps { + /** Value to copy */ + value: string; + /** Callback when copy status changes */ + onStatusChange?: (details: { copied: boolean }) => void; + /** Time in ms before resetting (default: 3000) */ + timeout?: number; + /** Button label (default: 'Copy') */ + label?: string; + /** Label when copied (default: 'Copied!') */ + copiedLabel?: string; + /** Button size (default: 'md') */ + size?: 'sm' | 'md' | 'lg'; + /** Button variant (default: 'solid') */ + variant?: 'solid' | 'outline' | 'ghost'; + /** Show copy/check icon (default: true) */ + showIcon?: boolean; + /** Show text label (default: true) */ + showLabel?: boolean; + /** Additional class for button */ + class?: string; } /** * CopyButton - Simple copy button without input field - * - * Props: - * - value: string - Value to copy - * - onStatusChange: (details: { copied: boolean }) => void - Callback when copy status changes - * - timeout: number - Time in ms before resetting (default: 3000) - * - label: string - Button label (default: 'Copy') - * - copiedLabel: string - Label when copied (default: 'Copied!') - * - size: 'sm' | 'md' | 'lg' - Button size (default: 'md') - * - variant: 'solid' | 'outline' | 'ghost' - Button variant (default: 'solid') - * - showIcon: boolean - Show copy/check icon (default: true) - * - showLabel: boolean - Show text label (default: true) - * - class: string - Additional class for button */ -export function CopyButton(props) { +export const CopyButton: Component = (props) => { const [local, machineProps] = splitProps(props, [ 'label', 'copiedLabel', @@ -113,7 +159,7 @@ export function CopyButton(props) { ]); const [copied, setCopied] = createSignal(false); - const handleStatusChange = details => { + const handleStatusChange = (details: { copied: boolean }) => { setCopied(details.copied); if (machineProps.onStatusChange) { machineProps.onStatusChange({ copied: details.copied }); @@ -183,6 +229,6 @@ export function CopyButton(props) { ); -} +}; export { ClipboardComponent as Clipboard }; diff --git a/packages/ui/src/zag/Collapsible.jsx b/packages/ui/src/zag/Collapsible.tsx similarity index 70% rename from packages/ui/src/zag/Collapsible.jsx rename to packages/ui/src/zag/Collapsible.tsx index 498703716..af177d528 100644 --- a/packages/ui/src/zag/Collapsible.jsx +++ b/packages/ui/src/zag/Collapsible.tsx @@ -5,28 +5,52 @@ */ import { Collapsible as ArkCollapsible, useCollapsible } from '@ark-ui/solid/collapsible'; -import { Show, splitProps, createMemo } from 'solid-js'; +import { Component, Show, splitProps, createMemo, JSX, Accessor } from 'solid-js'; + +export interface CollapsibleApi { + open: boolean; + visible: boolean; + setOpen: (open: boolean) => void; +} + +export interface CollapsibleProps { + /** Controlled open state */ + open?: boolean; + /** Initial open state (uncontrolled) */ + defaultOpen?: boolean; + /** Callback when open state changes */ + onOpenChange?: (details: { open: boolean }) => void; + /** Disable the collapsible */ + disabled?: boolean; + /** Enable lazy mounting */ + lazyMount?: boolean; + /** Unmount content when closed */ + unmountOnExit?: boolean; + /** Height when collapsed */ + collapsedHeight?: string | number; + /** Width when collapsed */ + collapsedWidth?: string | number; + /** Callback when exit animation completes */ + onExitComplete?: () => void; + /** Custom IDs for root, content, trigger */ + ids?: { root?: string; content?: string; trigger?: string }; + /** Trigger element or render function receiving collapsible API */ + trigger?: JSX.Element | ((api: Accessor) => JSX.Element); + /** Indicator element or render function receiving collapsible API */ + indicator?: JSX.Element | ((api: Accessor) => JSX.Element); + /** Collapsible content */ + children?: JSX.Element; +} /** - * High-level Collapsible component (convenience API) - * - * @param {Object} props - * @param {boolean} [props.open] - Controlled open state - * @param {boolean} [props.defaultOpen] - Default open state (uncontrolled) - * @param {Function} [props.onOpenChange] - Callback when open state changes (receives { open: boolean }) - * @param {boolean} [props.disabled] - Whether the collapsible is disabled - * @param {boolean} [props.lazyMount] - Enable lazy mounting - * @param {boolean} [props.unmountOnExit] - Unmount content when closed - * @param {string|number} [props.collapsedHeight] - Height when collapsed - * @param {string|number} [props.collapsedWidth] - Width when collapsed - * @param {Function} [props.onExitComplete] - Callback when exit animation completes - * @param {Object} [props.ids] - Custom IDs for root, content, trigger - * @param {JSX.Element | Function} [props.trigger] - Trigger element or render function receiving collapsible API - * @param {JSX.Element | Function} [props.indicator] - Indicator element or render function receiving collapsible API - * @param {JSX.Element} [props.children] - Content to show/hide + * Internal component for when we need programmatic API (function triggers/indicators) */ -// Internal component for when we need programmatic API (function triggers/indicators) -function CollapsibleWithApi(props) { +function CollapsibleWithApi(props: { + arkProps: CollapsibleProps; + trigger: () => CollapsibleProps['trigger']; + indicator: () => CollapsibleProps['indicator']; + children: () => JSX.Element | undefined; +}) { const arkProps = () => props.arkProps; const collapsibleApi = useCollapsible(arkProps()); @@ -35,7 +59,7 @@ function CollapsibleWithApi(props) { if (!triggerValue) return null; if (typeof triggerValue === 'function') { - return triggerValue(collapsibleApi()); + return triggerValue(collapsibleApi); } return triggerValue; @@ -46,15 +70,15 @@ function CollapsibleWithApi(props) { if (!indicatorValue) return null; if (typeof indicatorValue === 'function') { - return indicatorValue(collapsibleApi()); + return indicatorValue(collapsibleApi); } return indicatorValue; }; // Handle click events on trigger to prevent toggling when clicking interactive elements - const handleTriggerClick = e => { - const target = e.target; + const handleTriggerClick = (e: MouseEvent) => { + const target = e.target as HTMLElement; // Check if click is on an interactive element (but not the trigger itself) const interactive = target.closest( 'button:not([data-part="trigger"]), [role="button"]:not([data-part="trigger"]), [role="menuitem"], input, textarea, [data-editable], [data-scope="menu"], [data-scope="editable"], [data-selectable], a', @@ -87,7 +111,10 @@ function CollapsibleWithApi(props) { ); } -export default function CollapsibleComponent(props) { +/** + * High-level Collapsible component (convenience API) + */ +const CollapsibleComponent: Component = (props) => { // Split convenience props from Ark UI props const [local, arkProps] = splitProps(props, ['trigger', 'indicator', 'children']); @@ -117,8 +144,8 @@ export default function CollapsibleComponent(props) { }; // Handle click events on trigger to prevent toggling when clicking interactive elements - const handleTriggerClick = e => { - const target = e.target; + const handleTriggerClick = (e: MouseEvent) => { + const target = e.target as HTMLElement; // Check if click is on an interactive element (but not the trigger itself) const interactive = target.closest( 'button:not([data-part="trigger"]), [role="button"]:not([data-part="trigger"]), [role="menuitem"], input, textarea, [data-editable], [data-scope="menu"], [data-scope="editable"], [data-selectable], a', @@ -160,7 +187,9 @@ export default function CollapsibleComponent(props) { /> ); -} +}; + +export default CollapsibleComponent; // Export hook for programmatic control export { useCollapsible }; diff --git a/packages/ui/src/zag/Combobox.jsx b/packages/ui/src/zag/Combobox.tsx similarity index 67% rename from packages/ui/src/zag/Combobox.jsx rename to packages/ui/src/zag/Combobox.tsx index e4effd89a..677131d0b 100644 --- a/packages/ui/src/zag/Combobox.jsx +++ b/packages/ui/src/zag/Combobox.tsx @@ -7,34 +7,59 @@ import { Combobox as ArkCombobox, useCombobox, useListCollection } from '@ark-ui/solid/combobox'; import { useFilter } from '@ark-ui/solid/locale'; import { Portal } from 'solid-js/web'; -import { mergeProps, splitProps, createMemo, createEffect, Show, Index } from 'solid-js'; +import { Component, mergeProps, splitProps, createMemo, createEffect, Show, Index } from 'solid-js'; import { FiChevronDown, FiX, FiCheck } from 'solid-icons/fi'; import { Z_INDEX } from '../constants/zIndex.js'; +export interface ComboboxItem { + value: string; + label: string; + disabled?: boolean; +} + +export interface ComboboxProps { + /** Available items */ + items: ComboboxItem[]; + /** Input label */ + label?: string; + /** Input placeholder */ + placeholder?: string; + /** Controlled selected values */ + value?: string[]; + /** Initial selected values */ + defaultValue?: string[]; + /** Callback when selection changes */ + onValueChange?: (details: { value: string[]; items: ComboboxItem[] }) => void; + /** Callback when input changes */ + onInputValueChange?: (details: { inputValue: string }) => void; + /** Allow multiple selections (default: false) */ + multiple?: boolean; + /** Disable the combobox */ + disabled?: boolean; + /** Make combobox read-only */ + readOnly?: boolean; + /** Mark as invalid */ + invalid?: boolean; + /** Form field name */ + name?: string; + /** Allow custom values not in the list */ + allowCustomValue?: boolean; + /** Close on selection (default: true for single, false for multiple) */ + closeOnSelect?: boolean; + /** Open on input click (default: true) */ + openOnClick?: boolean; + /** Set to true when used inside a Dialog */ + inDialog?: boolean; + /** Additional class for root element */ + class?: string; + /** Additional class for input element */ + inputClass?: string; +} + /** * Combobox - Searchable select with autocomplete - * - * Props: - * - items: Array<{ value: string, label: string, disabled?: boolean }> - Available items - * - label: string - Input label - * - placeholder: string - Input placeholder - * - value: string[] - Controlled selected values - * - defaultValue: string[] - Initial selected values - * - onValueChange: (details: { value: string[], items: Item[] }) => void - Callback when selection changes - * - onInputValueChange: (details: { inputValue: string }) => void - Callback when input changes - * - multiple: boolean - Allow multiple selections (default: false) - * - disabled: boolean - Disable the combobox - * - readOnly: boolean - Make combobox read-only - * - invalid: boolean - Mark as invalid - * - name: string - Form field name - * - allowCustomValue: boolean - Allow custom values not in the list - * - closeOnSelect: boolean - Close on selection (default: true for single, false for multiple) - * - openOnClick: boolean - Open on input click (default: true) - * - inDialog: boolean - Set to true when used inside a Dialog - * - class: string - Additional class for root element - * - inputClass: string - Additional class for input element */ -export default function ComboboxComponent(props) { +const ComboboxComponent: Component = (props) => { const merged = mergeProps( { openOnClick: true, @@ -61,9 +86,9 @@ export default function ComboboxComponent(props) { const { collection, filter, set } = useListCollection({ initialItems: getItems(), filter: filterFn().contains, - itemToString: item => item.label, - itemToValue: item => item.value, - itemToDisabled: item => item.disabled, + itemToString: (item: ComboboxItem) => item.label, + itemToValue: (item: ComboboxItem) => item.value, + itemToDisabled: (item: ComboboxItem) => item.disabled || false, }); // Sync items when props.items changes @@ -72,16 +97,20 @@ export default function ComboboxComponent(props) { set(items); }); - const handleInputValueChange = details => { + const handleInputValueChange = (details: { inputValue: string }) => { filter(details.inputValue); if (machineProps.onInputValueChange) { machineProps.onInputValueChange(details); } }; - const handleValueChange = details => { + const handleValueChange = (details: { value: string[] }) => { if (machineProps.onValueChange) { - machineProps.onValueChange(details); + // Find the items that match the selected values + const selectedItems = details.value + .map((val) => getItems().find((item) => item.value === val)) + .filter((item): item is ComboboxItem => item !== undefined); + machineProps.onValueChange({ value: details.value, items: selectedItems }); } }; @@ -99,7 +128,7 @@ export default function ComboboxComponent(props) { > - {item => ( + {(item) => ( ); -} +}; export { ComboboxComponent as Combobox }; diff --git a/packages/ui/src/zag/Dialog.jsx b/packages/ui/src/zag/Dialog.tsx similarity index 81% rename from packages/ui/src/zag/Dialog.jsx rename to packages/ui/src/zag/Dialog.tsx index 839808297..cd66299a6 100644 --- a/packages/ui/src/zag/Dialog.jsx +++ b/packages/ui/src/zag/Dialog.tsx @@ -4,29 +4,36 @@ import { Dialog as ArkDialog, useDialog } from '@ark-ui/solid/dialog'; import { Portal } from 'solid-js/web'; -import { createSignal, Show } from 'solid-js'; +import { Component, createSignal, Show, JSX } from 'solid-js'; import { FiAlertTriangle, FiX } from 'solid-icons/fi'; import { Z_INDEX } from '../constants/zIndex.js'; +export interface DialogProps { + /** Controlled open state */ + open?: boolean; + /** Callback when open state changes */ + onOpenChange?: (open: boolean) => void; + /** Dialog title */ + title?: string; + /** Dialog description */ + description?: string; + /** Dialog content */ + children?: JSX.Element; + /** Dialog size */ + size?: 'sm' | 'md' | 'lg' | 'xl'; +} + /** * Dialog - A generic dialog/modal component - * - * Props: - * - open: boolean - Whether the dialog is open - * - onOpenChange: (open: boolean) => void - Callback when open state changes - * - title: string - Dialog title - * - description: string - Optional description below title - * - children: JSX.Element - Dialog content - * - size: 'sm' | 'md' | 'lg' | 'xl' - Dialog width (default: 'md') */ -export default function DialogComponent(props) { +const DialogComponent: Component = (props) => { const open = () => props.open; const size = () => props.size; const title = () => props.title; const description = () => props.description; const children = () => props.children; - const handleOpenChange = details => { + const handleOpenChange = (details: { open: boolean }) => { if (props.onOpenChange) { props.onOpenChange(details.open); } @@ -82,23 +89,33 @@ export default function DialogComponent(props) { ); +}; + +export interface ConfirmDialogProps { + /** Controlled open state */ + open?: boolean; + /** Callback when open state changes */ + onOpenChange?: (open: boolean) => void; + /** Callback when user confirms */ + onConfirm?: () => void; + /** Dialog title */ + title?: string; + /** Dialog description/message */ + description?: string; + /** Text for confirm button (default: "Confirm") */ + confirmText?: string; + /** Text for cancel button (default: "Cancel") */ + cancelText?: string; + /** Visual variant (default: 'danger') */ + variant?: 'danger' | 'warning' | 'info'; + /** Whether confirm action is in progress */ + loading?: boolean; } /** * ConfirmDialog - A reusable confirmation dialog component - * - * Props: - * - open: boolean - Whether the dialog is open - * - onOpenChange: (open: boolean) => void - Callback when open state changes - * - onConfirm: () => void - Callback when user confirms - * - title: string - Dialog title - * - description: string - Dialog description/message - * - confirmText: string - Text for confirm button (default: "Confirm") - * - cancelText: string - Text for cancel button (default: "Cancel") - * - variant: 'danger' | 'warning' | 'info' - Visual variant (default: 'danger') - * - loading: boolean - Whether confirm action is in progress */ -export function ConfirmDialogComponent(props) { +export const ConfirmDialogComponent: Component = (props) => { const open = () => props.open; const loading = () => props.loading; const variant = () => props.variant || 'danger'; @@ -107,7 +124,7 @@ export function ConfirmDialogComponent(props) { const confirmText = () => props.confirmText; const cancelText = () => props.cancelText; - const handleOpenChange = details => { + const handleOpenChange = (details: { open: boolean }) => { if (props.onOpenChange) { props.onOpenChange(details.open); } @@ -210,20 +227,22 @@ export function ConfirmDialogComponent(props) { ); +}; + +export interface ConfirmDialogConfig { + title?: string; + description?: string; + confirmText?: string; + cancelText?: string; + variant?: 'danger' | 'warning' | 'info'; } /** * useConfirmDialog - Hook to manage confirm dialog state - * - * Returns: - * - isOpen: () => boolean - * - open: (config) => Promise - Opens dialog, returns true if confirmed - * - close: () => void - * - dialogProps: object - Props to spread on ConfirmDialog */ export function useConfirmDialog() { const [isOpen, setIsOpen] = createSignal(false); - const [config, setConfig] = createSignal({ + const [config, setConfig] = createSignal({ title: '', description: '', confirmText: 'Confirm', @@ -232,10 +251,10 @@ export function useConfirmDialog() { }); const [loading, setLoading] = createSignal(false); - let resolvePromise = null; + let resolvePromise: ((value: boolean) => void) | null = null; - const open = dialogConfig => { - return new Promise(resolve => { + const open = (dialogConfig: ConfirmDialogConfig) => { + return new Promise((resolve) => { resolvePromise = resolve; setConfig({ title: dialogConfig.title || 'Confirm', @@ -266,7 +285,7 @@ export function useConfirmDialog() { setLoading(false); }; - const handleOpenChange = newOpen => { + const handleOpenChange = (newOpen: boolean) => { if (!newOpen) { close(); } diff --git a/packages/ui/src/zag/Drawer.jsx b/packages/ui/src/zag/Drawer.tsx similarity index 77% rename from packages/ui/src/zag/Drawer.jsx rename to packages/ui/src/zag/Drawer.tsx index 8c426ebfe..fbd5b11c3 100644 --- a/packages/ui/src/zag/Drawer.jsx +++ b/packages/ui/src/zag/Drawer.tsx @@ -4,26 +4,37 @@ import { Dialog } from '@ark-ui/solid/dialog'; import { Portal } from 'solid-js/web'; -import { Show } from 'solid-js'; +import { Component, Show, JSX } from 'solid-js'; import { FiX } from 'solid-icons/fi'; import { Z_INDEX } from '../constants/zIndex.js'; +export interface DrawerProps { + /** Whether the drawer is open */ + open?: boolean; + /** Callback when open state changes */ + onOpenChange?: (open: boolean) => void; + /** Drawer title */ + title?: string; + /** Optional description below title */ + description?: string; + /** Drawer content */ + children?: JSX.Element; + /** Which side the drawer slides in from (default: 'right') */ + side?: 'left' | 'right'; + /** Drawer width (default: 'md') */ + size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'; + /** Whether to show the header (default: true) */ + showHeader?: boolean; + /** Close when clicking backdrop (default: true) */ + closeOnOutsideClick?: boolean; + /** Whether to show the dark overlay backdrop (default: true) */ + showBackdrop?: boolean; +} + /** * Drawer - A slide-in panel component composed from Dialog - * - * Props: - * - open: boolean - Whether the drawer is open - * - onOpenChange: (open: boolean) => void - Callback when open state changes - * - title: string - Drawer title - * - description: string - Optional description below title - * - children: JSX.Element - Drawer content - * - side: 'left' | 'right' - Which side the drawer slides in from (default: 'right') - * - size: 'sm' | 'md' | 'lg' | 'xl' | 'full' - Drawer width (default: 'md') - * - showHeader: boolean - Whether to show the header (default: true) - * - closeOnOutsideClick: boolean - Close when clicking backdrop (default: true) - * - showBackdrop: boolean - Whether to show the dark overlay backdrop (default: true) */ -export default function DrawerComponent(props) { +const DrawerComponent: Component = (props) => { const open = () => props.open; const side = () => props.side || 'right'; const size = () => props.size || 'md'; @@ -33,7 +44,7 @@ export default function DrawerComponent(props) { const showHeader = () => props.showHeader ?? true; const showBackdrop = () => props.showBackdrop ?? true; - const handleOpenChange = details => { + const handleOpenChange = (details: { open: boolean }) => { if (props.onOpenChange) { props.onOpenChange(details.open); } @@ -96,9 +107,7 @@ export default function DrawerComponent(props) {
- - {title()} - + {title()} @@ -119,6 +128,6 @@ export default function DrawerComponent(props) { ); -} +}; export { DrawerComponent as Drawer }; diff --git a/packages/ui/src/zag/Editable.jsx b/packages/ui/src/zag/Editable.tsx similarity index 73% rename from packages/ui/src/zag/Editable.jsx rename to packages/ui/src/zag/Editable.tsx index da86bab19..13189f9ff 100644 --- a/packages/ui/src/zag/Editable.jsx +++ b/packages/ui/src/zag/Editable.tsx @@ -3,7 +3,7 @@ */ import { Editable } from '@ark-ui/solid/editable'; -import { Show, mergeProps } from 'solid-js'; +import { Component, Show, mergeProps } from 'solid-js'; import { FiCheck, FiX, FiEdit2 } from 'solid-icons/fi'; import { cn } from '../lib/cn.js'; @@ -37,38 +37,60 @@ const variants = { input: 'outline-none bg-transparent', preview: 'cursor-pointer', }, -}; +} as const; + +export interface EditableProps { + /** The controlled value */ + value?: string; + /** The initial value (uncontrolled) */ + defaultValue?: string; + /** Placeholder text when empty */ + placeholder?: string; + /** Called when value changes (on each keystroke) */ + onChange?: (_value: string) => void; + /** Called when value is committed (Enter/blur/submit button) */ + onSubmit?: (_value: string) => void; + /** Called when editing is cancelled */ + onCancel?: () => void; + /** Whether the editable is disabled */ + disabled?: boolean; + /** Whether the editable is read-only (can view but not edit) */ + readOnly?: boolean; + /** Whether to auto-resize to fit content */ + autoResize?: boolean; + /** How to enter edit mode (default: 'dblclick') */ + activationMode?: 'focus' | 'dblclick' | 'click' | 'none'; + /** What triggers submit (default: 'both') */ + submitMode?: 'enter' | 'blur' | 'none' | 'both'; + /** Whether to select text when focused */ + selectOnFocus?: boolean; + /** Maximum characters allowed */ + maxLength?: number; + /** Style preset (default: 'default') */ + variant?: 'default' | 'inline' | 'heading' | 'field'; + /** Additional CSS classes for the root */ + class?: string; + /** Additional CSS classes for the area (input/preview container) */ + areaClass?: string; + /** Additional CSS classes for the input (merged with variant) */ + inputClass?: string; + /** Additional CSS classes for the preview (merged with variant) */ + previewClass?: string; + /** Whether to show edit/save/cancel buttons (default: false) */ + showControls?: boolean; + /** Whether to show only an edit icon trigger (no save/cancel) (default: false) */ + showEditIcon?: boolean; + /** Optional label text */ + label?: string; +} /** * Editable - An inline editable single-line text component using Ark UI * * Best for: titles, names, labels - single line text that can be edited inline. * For multi-line text (descriptions, notes), use a manual textarea approach instead. - * - * Props: - * - value: string - The controlled value - * - defaultValue: string - The initial value (uncontrolled) - * - placeholder: string - Placeholder text when empty - * - onChange: (value: string) => void - Called when value changes (on each keystroke) - * - onSubmit: (value: string) => void - Called when value is committed (Enter/blur/submit button) - * - onCancel: () => void - Called when editing is cancelled - * - disabled: boolean - Whether the editable is disabled - * - readOnly: boolean - Whether the editable is read-only (can view but not edit) - * - autoResize: boolean - Whether to auto-resize to fit content - * - activationMode: 'focus' | 'dblclick' | 'click' | 'none' - How to enter edit mode (default: 'dblclick') - * - submitMode: 'enter' | 'blur' | 'none' | 'both' - What triggers submit (default: 'both') - * - selectOnFocus: boolean - Whether to select text when focused - * - maxLength: number - Maximum characters allowed - * - variant: 'default' | 'inline' | 'heading' | 'field' - Style preset (default: 'default') - * - class: string - Additional CSS classes for the root - * - areaClass: string - Additional CSS classes for the area (input/preview container) - * - inputClass: string - Additional CSS classes for the input (merged with variant) - * - previewClass: string - Additional CSS classes for the preview (merged with variant) - * - showControls: boolean - Whether to show edit/save/cancel buttons (default: false) - * - showEditIcon: boolean - Whether to show only an edit icon trigger (no save/cancel) (default: false) - * - label: string - Optional label text */ -export default function EditableComponent(props) { +const EditableComponent: Component = props => { const merged = mergeProps( { placeholder: 'Click to edit...', @@ -89,27 +111,31 @@ export default function EditableComponent(props) { const disabled = () => merged.disabled; const readOnly = () => merged.readOnly; const autoResize = () => merged.autoResize; - const activationMode = () => merged.activationMode; - const submitMode = () => merged.submitMode; + const activationMode = () => + merged.activationMode as 'focus' | 'dblclick' | 'click' | 'none' | undefined; + const submitMode = () => merged.submitMode as 'enter' | 'blur' | 'none' | 'both' | undefined; const selectOnFocus = () => merged.selectOnFocus; const maxLength = () => merged.maxLength; const showControls = () => merged.showControls; const showEditIcon = () => merged.showEditIcon; const variant = () => merged.variant; - const variantStyles = () => variants[variant()] || variants.default; + const variantStyles = () => { + const v = variant() || 'default'; + return variants[v as keyof typeof variants] || variants.default; + }; const classValue = () => merged.class; const areaClass = () => merged.areaClass; const inputClass = () => merged.inputClass; const previewClass = () => merged.previewClass; const label = () => merged.label; - const handleValueChange = details => { + const handleValueChange = (details: { value: string }) => { if (merged.onChange) { merged.onChange(details.value); } }; - const handleValueCommit = details => { + const handleValueCommit = (details: { value: string }) => { if (merged.onSubmit) { merged.onSubmit(details.value); } @@ -203,6 +229,7 @@ export default function EditableComponent(props) { ); -} +}; export { EditableComponent as Editable }; +export default EditableComponent; diff --git a/packages/ui/src/zag/FileUpload.jsx b/packages/ui/src/zag/FileUpload.tsx similarity index 75% rename from packages/ui/src/zag/FileUpload.jsx rename to packages/ui/src/zag/FileUpload.tsx index 532e99a18..6cb250666 100644 --- a/packages/ui/src/zag/FileUpload.jsx +++ b/packages/ui/src/zag/FileUpload.tsx @@ -3,31 +3,46 @@ */ import { FileUpload } from '@ark-ui/solid/file-upload'; -import { Show, mergeProps, For } from 'solid-js'; +import { Component, Show, mergeProps, For } from 'solid-js'; import { BiRegularCloudUpload, BiRegularTrash } from 'solid-icons/bi'; import { CgFileDocument } from 'solid-icons/cg'; import { useWindowDrag } from '../primitives/useWindowDrag.js'; +export interface FileUploadProps { + /** Accepted file types (e.g., 'application/pdf') */ + accept?: string; + /** Allow multiple file selection (default: false) */ + multiple?: boolean; + /** Callback when files change */ + onFilesChange?: (_files: File[]) => void; + /** Callback when files are accepted */ + onFileAccept?: (_details: { files: File[] }) => void; + /** Callback when files are rejected */ + onFileReject?: (_details: { files: unknown[] }) => void; + /** Custom text for the dropzone */ + dropzoneText?: string; + /** Custom text for the trigger button */ + buttonText?: string; + /** Helper text below the dropzone text */ + helpText?: string; + /** Whether to show the list of uploaded files (default: true) */ + showFileList?: boolean; + /** Additional CSS classes for the root element */ + class?: string; + /** Additional CSS classes for the dropzone */ + dropzoneClass?: string; + /** Disable the file upload */ + disabled?: boolean; + /** Use compact/minimal styling */ + compact?: boolean; + /** Allow dropping directories (default: true for multiple) */ + allowDirectories?: boolean; +} + /** * FileUpload - Reusable file upload component using Ark UI - * - * @param {Object} props - * @param {string} [props.accept] - Accepted file types (e.g., 'application/pdf') - * @param {boolean} [props.multiple] - Allow multiple file selection (default: false) - * @param {Function} [props.onFilesChange] - Callback when files change: (files: File[]) => void - * @param {Function} [props.onFileAccept] - Callback when files are accepted: (details: { files: File[] }) => void - * @param {Function} [props.onFileReject] - Callback when files are rejected: (details: { files: FileRejection[] }) => void - * @param {string} [props.dropzoneText] - Custom text for the dropzone - * @param {string} [props.buttonText] - Custom text for the trigger button - * @param {string} [props.helpText] - Helper text below the dropzone text - * @param {boolean} [props.showFileList] - Whether to show the list of uploaded files (default: true) - * @param {string} [props.class] - Additional CSS classes for the root element - * @param {string} [props.dropzoneClass] - Additional CSS classes for the dropzone - * @param {boolean} [props.disabled] - Disable the file upload - * @param {boolean} [props.compact] - Use compact/minimal styling - * @param {boolean} [props.allowDirectories] - Allow dropping directories (default: true for multiple) */ -export default function FileUploadComponent(props) { +const FileUploadComponent: Component = props => { const merged = mergeProps( { multiple: false, @@ -53,15 +68,15 @@ export default function FileUploadComponent(props) { // Detect when files are being dragged over the window (even from external sources like Finder) const { isDraggingOverWindow } = useWindowDrag(); - const handleFileChange = details => { + const handleFileChange = (details: { acceptedFiles: File[] }) => { merged.onFilesChange?.(details.acceptedFiles); }; - const handleFileAccept = details => { - merged.onFileAccept?.(details); + const handleFileAccept = (_details: { files: File[] }) => { + merged.onFileAccept?.(_details); }; - const handleFileReject = details => { + const handleFileReject = (details: { files: unknown[] }) => { merged.onFileReject?.(details); }; @@ -78,7 +93,7 @@ export default function FileUploadComponent(props) { > {api => { - const isHighlighted = () => api().isDragging || isDraggingOverWindow(); + const isHighlighted = () => api().dragging || isDraggingOverWindow(); return ( <> @@ -145,6 +160,6 @@ export default function FileUploadComponent(props) { ); -} +}; export { FileUploadComponent as FileUpload }; diff --git a/packages/ui/src/zag/FloatingPanel.jsx b/packages/ui/src/zag/FloatingPanel.tsx similarity index 71% rename from packages/ui/src/zag/FloatingPanel.jsx rename to packages/ui/src/zag/FloatingPanel.tsx index 065ca5fa9..9506ea11f 100644 --- a/packages/ui/src/zag/FloatingPanel.jsx +++ b/packages/ui/src/zag/FloatingPanel.tsx @@ -3,73 +3,108 @@ */ import { FloatingPanel } from '@ark-ui/solid/floating-panel'; -import { Show } from 'solid-js'; +import { Component, Show, JSX } from 'solid-js'; import { Portal } from 'solid-js/web'; import { AiOutlineMinus, AiOutlineClose } from 'solid-icons/ai'; import { FiMaximize2 } from 'solid-icons/fi'; import { FaSolidWindowRestore } from 'solid-icons/fa'; +export interface FloatingPanelSize { + width: number; + height: number; +} + +export interface FloatingPanelPosition { + x: number; + y: number; +} + +export interface FloatingPanelProps { + /** Whether the panel is open (controlled) */ + open?: boolean; + /** Initial open state (uncontrolled) */ + defaultOpen?: boolean; + /** Callback when open state changes */ + onOpenChange?: (details: { open: boolean }) => void; + /** Panel title */ + title?: string; + /** Panel content */ + children?: JSX.Element; + /** Initial size */ + defaultSize?: FloatingPanelSize; + /** Initial position */ + defaultPosition?: FloatingPanelPosition; + /** Controlled size */ + size?: FloatingPanelSize; + /** Controlled position */ + position?: FloatingPanelPosition; + /** Callback when size changes */ + onSizeChange?: (details: { size: FloatingPanelSize }) => void; + /** Callback when position changes */ + onPositionChange?: (details: { position: FloatingPanelPosition }) => void; + /** Callback when stage changes */ + onStageChange?: (details: { stage: 'minimized' | 'maximized' | 'default' }) => void; + /** Whether the panel can be resized (default: true) */ + resizable?: boolean; + /** Whether the panel can be dragged (default: true) */ + draggable?: boolean; + /** Minimum size constraints */ + minSize?: FloatingPanelSize; + /** Maximum size constraints */ + maxSize?: FloatingPanelSize; + /** Lock aspect ratio when resizing */ + lockAspectRatio?: boolean; + /** Show all control buttons (default: true) */ + showControls?: boolean; + /** Show minimize button (default: true, requires showControls) */ + showMinimize?: boolean; + /** Show maximize button (default: true, requires showControls) */ + showMaximize?: boolean; + /** Show restore button (default: true, requires showControls) */ + showRestore?: boolean; + /** Show close button (default: true, requires showControls) */ + showClose?: boolean; + /** Close panel on Escape key (default: true) */ + closeOnEscape?: boolean; + /** Persist size/position when closed (default: false) */ + persistRect?: boolean; +} + /** * FloatingPanel - A draggable and resizable floating panel component - * - * Props: - * - open: boolean - Whether the panel is open (controlled) - * - defaultOpen: boolean - Initial open state (uncontrolled) - * - onOpenChange: (details: { open: boolean }) => void - Callback when open state changes - * - title: string - Panel title - * - children: JSX.Element - Panel content - * - defaultSize: { width: number, height: number } - Initial size - * - defaultPosition: { x: number, y: number } - Initial position - * - size: { width: number, height: number } - Controlled size - * - position: { x: number, y: number } - Controlled position - * - onSizeChange: (details: SizeChangeDetails) => void - * - onPositionChange: (details: PositionChangeDetails) => void - * - onStageChange: (details: { stage: 'minimized' | 'maximized' | 'default' }) => void - * - resizable: boolean - Whether the panel can be resized (default: true) - * - draggable: boolean - Whether the panel can be dragged (default: true) - * - minSize: { width: number, height: number } - Minimum size constraints - * - maxSize: { width: number, height: number } - Maximum size constraints - * - lockAspectRatio: boolean - Lock aspect ratio when resizing - * - showControls: boolean - Show all control buttons (default: true) - * - showMinimize: boolean - Show minimize button (default: true, requires showControls) - * - showMaximize: boolean - Show maximize button (default: true, requires showControls) - * - showRestore: boolean - Show restore button (default: true, requires showControls) - * - showClose: boolean - Show close button (default: true, requires showControls) - * - closeOnEscape: boolean - Close panel on Escape key (default: true) - * - persistRect: boolean - Persist size/position when closed (default: false) */ -export default function FloatingPanelComponent(props) { +const FloatingPanelComponent: Component = (props) => { const showControls = () => props.showControls ?? true; const showMinimize = () => showControls() && (props.showMinimize ?? true); const showMaximize = () => showControls() && (props.showMaximize ?? true); const showRestore = () => showControls() && (props.showRestore ?? true); const showClose = () => showControls() && (props.showClose ?? true); - const handleOpenChange = details => { + const handleOpenChange = (details: { open: boolean }) => { if (props.onOpenChange) { props.onOpenChange(details); } }; - const handleSizeChange = details => { + const handleSizeChange = (details: { size: FloatingPanelSize }) => { if (props.onSizeChange) { props.onSizeChange(details); } }; - const handlePositionChange = details => { + const handlePositionChange = (details: { position: FloatingPanelPosition }) => { if (props.onPositionChange) { props.onPositionChange(details); } }; - const handleStageChange = details => { + const handleStageChange = (details: { stage: 'minimized' | 'maximized' | 'default' }) => { if (props.onStageChange) { props.onStageChange(details); } }; - const getResizeTriggerStyle = axis => { + const getResizeTriggerStyle = (axis: string): { width?: string; height?: string } => { if (axis.length === 1) { return axis === 'n' || axis === 's' ? { height: '8px' } : { width: '8px' }; } @@ -97,7 +132,7 @@ export default function FloatingPanelComponent(props) { persistRect={props.persistRect ?? false} > - {api => ( + {(api) => ( @@ -177,6 +212,6 @@ export default function FloatingPanelComponent(props) { ); -} +}; export { FloatingPanelComponent as FloatingPanel }; diff --git a/packages/ui/src/zag/Menu.jsx b/packages/ui/src/zag/Menu.tsx similarity index 71% rename from packages/ui/src/zag/Menu.jsx rename to packages/ui/src/zag/Menu.tsx index 827c0c380..22e80c1fc 100644 --- a/packages/ui/src/zag/Menu.jsx +++ b/packages/ui/src/zag/Menu.tsx @@ -4,35 +4,62 @@ import { Menu } from '@ark-ui/solid/menu'; import { Portal } from 'solid-js/web'; -import { createSignal, Show, For, splitProps } from 'solid-js'; +import { Component, createSignal, Show, For, splitProps, JSX } from 'solid-js'; import { Z_INDEX } from '../constants/zIndex.js'; +export type Placement = + | 'top' + | 'top-start' + | 'top-end' + | 'bottom' + | 'bottom-start' + | 'bottom-end' + | 'left' + | 'left-start' + | 'left-end' + | 'right' + | 'right-start' + | 'right-end'; + +export interface MenuItem { + value: string; + label: string; + icon?: JSX.Element; + destructive?: boolean; + separator?: boolean; + groupLabel?: string; + disabled?: boolean; +} + +export interface MenuProps { + /** Trigger element */ + trigger: JSX.Element; + /** Menu items */ + items: MenuItem[]; + /** Callback when item is selected */ + onSelect?: (details: { value: string }) => void; + /** Controlled open state */ + open?: boolean; + /** Initial open state */ + defaultOpen?: boolean; + /** Callback when open state changes */ + onOpenChange?: (details: { open: boolean }) => void; + /** Menu placement (default: 'bottom-start') */ + placement?: Placement; + /** Close menu on selection (default: true) */ + closeOnSelect?: boolean; + /** Set to true when used inside a Dialog */ + inDialog?: boolean; + /** Hide the dropdown indicator chevron */ + hideIndicator?: boolean; + /** Additional class for content */ + class?: string; +} + /** * Menu - Dropdown menu for actions - * - * Props: - * - trigger: JSX.Element - The trigger element - * - items: Array - Menu items - * - onSelect: (details: { value: string }) => void - Callback when item is selected - * - open: boolean - Controlled open state - * - defaultOpen: boolean - Initial open state - * - onOpenChange: (details: { open: boolean }) => void - Callback when open state changes - * - placement: Placement - Menu placement (default: 'bottom-start') - * - closeOnSelect: boolean - Close menu on selection (default: true) - * - inDialog: boolean - Set to true when used inside a Dialog - * - hideIndicator: boolean - Hide the dropdown indicator chevron - * - class: string - Additional class for content - * - * MenuItem: - * - value: string - Unique value for the item - * - label: string - Display label - * - icon?: JSX.Element - Optional icon - * - disabled?: boolean - Whether item is disabled - * - destructive?: boolean - Style as destructive action - * - separator?: boolean - Render as separator instead of item - * - groupLabel?: string - Render as group label */ -export default function MenuComponent(props) { +const MenuComponent: Component = (props) => { const [local, machineProps] = splitProps(props, [ 'trigger', 'items', @@ -42,7 +69,7 @@ export default function MenuComponent(props) { 'class', ]); - const handleSelect = details => { + const handleSelect = (details: { value: string }) => { if (machineProps.onSelect) { machineProps.onSelect(details); } @@ -51,7 +78,7 @@ export default function MenuComponent(props) { // Track open state for conditional rendering const [isOpen, setIsOpen] = createSignal(false); - const handleOpenChange = details => { + const handleOpenChange = (details: { open: boolean }) => { setIsOpen(details.open); if (machineProps.onOpenChange) { machineProps.onOpenChange(details); @@ -64,7 +91,7 @@ export default function MenuComponent(props) { class={`${Z_INDEX.MENU} min-w-40 rounded-lg border border-gray-200 bg-white py-1 shadow-lg focus:outline-none ${local.class || ''}`} > - {item => ( + {(item) => ( ); -} +}; export { MenuComponent as Menu }; diff --git a/packages/ui/src/zag/NumberInput.jsx b/packages/ui/src/zag/NumberInput.tsx similarity index 67% rename from packages/ui/src/zag/NumberInput.jsx rename to packages/ui/src/zag/NumberInput.tsx index 403f1b29c..9086f8bce 100644 --- a/packages/ui/src/zag/NumberInput.jsx +++ b/packages/ui/src/zag/NumberInput.tsx @@ -3,36 +3,58 @@ */ import { NumberInput } from '@ark-ui/solid/number-input'; -import { Show, splitProps, mergeProps, createMemo } from 'solid-js'; +import { Component, Show, splitProps, mergeProps, createMemo } from 'solid-js'; import { FiMinus, FiPlus } from 'solid-icons/fi'; +export interface NumberInputProps { + /** Input label */ + label?: string; + /** Controlled value (as string for formatting) */ + value?: string; + /** Initial value */ + defaultValue?: string; + /** Callback when value changes */ + onValueChange?: (details: { value: string; valueAsNumber: number }) => void; + /** Minimum value */ + min?: number; + /** Maximum value */ + max?: number; + /** Increment/decrement step (default: 1) */ + step?: number; + /** Disable the input */ + disabled?: boolean; + /** Make input read-only */ + readOnly?: boolean; + /** Mark as invalid */ + invalid?: boolean; + /** Mark as required */ + required?: boolean; + /** Form field name */ + name?: string; + /** Input placeholder */ + placeholder?: string; + /** Allow mouse wheel to change value (default: false) */ + allowMouseWheel?: boolean; + /** Clamp value to min/max on blur (default: true) */ + clampValueOnBlur?: boolean; + /** Spin on button press and hold (default: true) */ + spinOnPress?: boolean; + /** Number format options */ + formatOptions?: Intl.NumberFormatOptions; + /** Show increment/decrement buttons (default: true) */ + showControls?: boolean; + /** Input size (default: 'md') */ + size?: 'sm' | 'md' | 'lg'; + /** Additional class for root element */ + class?: string; + /** Additional class for input element */ + inputClass?: string; +} + /** * NumberInput - Numeric input with increment/decrement controls - * - * Props: - * - label: string - Input label - * - value: string - Controlled value (as string for formatting) - * - defaultValue: string - Initial value - * - onValueChange: (details: { value: string, valueAsNumber: number }) => void - Callback when value changes - * - min: number - Minimum value - * - max: number - Maximum value - * - step: number - Increment/decrement step (default: 1) - * - disabled: boolean - Disable the input - * - readOnly: boolean - Make input read-only - * - invalid: boolean - Mark as invalid - * - required: boolean - Mark as required - * - name: string - Form field name - * - placeholder: string - Input placeholder - * - allowMouseWheel: boolean - Allow mouse wheel to change value (default: false) - * - clampValueOnBlur: boolean - Clamp value to min/max on blur (default: true) - * - spinOnPress: boolean - Spin on button press and hold (default: true) - * - formatOptions: Intl.NumberFormatOptions - Number format options - * - showControls: boolean - Show increment/decrement buttons (default: true) - * - size: 'sm' | 'md' | 'lg' - Input size (default: 'md') - * - class: string - Additional class for root element - * - inputClass: string - Additional class for input element */ -export default function NumberInputComponent(props) { +const NumberInputComponent: Component = (props) => { const [local, machineProps] = splitProps(props, [ 'label', 'placeholder', @@ -106,6 +128,6 @@ export default function NumberInputComponent(props) {
); -} +}; export { NumberInputComponent as NumberInput }; diff --git a/packages/ui/src/zag/PasswordInput.jsx b/packages/ui/src/zag/PasswordInput.tsx similarity index 66% rename from packages/ui/src/zag/PasswordInput.jsx rename to packages/ui/src/zag/PasswordInput.tsx index 51d5237d5..282756418 100644 --- a/packages/ui/src/zag/PasswordInput.jsx +++ b/packages/ui/src/zag/PasswordInput.tsx @@ -3,23 +3,32 @@ */ import { PasswordInput } from '@ark-ui/solid/password-input'; -import { createSignal } from 'solid-js'; +import { Component, createSignal } from 'solid-js'; import { FiEyeOff, FiEye } from 'solid-icons/fi'; +export interface PasswordInputProps { + /** Controlled password value */ + password?: string; + /** Callback when password changes */ + onPasswordChange?: (value: string) => void; + /** Autocomplete attribute (default: 'new-password') */ + autoComplete?: 'current-password' | 'new-password'; + /** Whether input is required */ + required?: boolean; + /** Additional class for input element */ + inputClass?: string; + /** Size of visibility icon (default: 20) */ + iconSize?: number; + /** Additional class for root element */ + class?: string; + /** Input label (default: 'Password') */ + label?: string; +} + /** * PasswordInput - Password input with visibility toggle - * - * Props: - * - password: string - Controlled password value - * - onPasswordChange: (value: string) => void - Callback when password changes - * - autoComplete: 'current-password' | 'new-password' - Autocomplete attribute (default: 'new-password') - * - required: boolean - Whether input is required - * - inputClass: string - Additional class for input element - * - iconSize: number - Size of visibility icon (default: 20) - * - class: string - Additional class for root element - * - label: string - Input label (default: 'Password') */ -export default function PasswordInputComponent(props) { +const PasswordInputComponent: Component = (props) => { const autoComplete = () => props.autoComplete || 'new-password'; const password = () => props.password || ''; const required = () => props.required || false; @@ -34,14 +43,14 @@ export default function PasswordInputComponent(props) { inputClass() || 'w-full pl-3 sm:pl-4 pr-3 sm:pr-4 py-2 text-xs sm:text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-400 transition big-placeholder'; - const handleInput = e => { + const handleInput = (e: Event & { target: HTMLInputElement }) => { props.onPasswordChange?.(e.target.value); }; return ( setVisible(details.visible)} + onVisibilityChange={(details: { visible: boolean }) => setVisible(details.visible)} autoComplete={autoComplete()} required={required()} class={classValue()} @@ -64,6 +73,7 @@ export default function PasswordInputComponent(props) { ); -} +}; export { PasswordInputComponent as PasswordInput }; +export default PasswordInputComponent; diff --git a/packages/ui/src/zag/PinInput.jsx b/packages/ui/src/zag/PinInput.tsx similarity index 61% rename from packages/ui/src/zag/PinInput.jsx rename to packages/ui/src/zag/PinInput.tsx index 13fcfc466..e678453d1 100644 --- a/packages/ui/src/zag/PinInput.jsx +++ b/packages/ui/src/zag/PinInput.tsx @@ -3,22 +3,31 @@ */ import { PinInput } from '@ark-ui/solid/pin-input'; -import { splitProps, Index, createMemo, mergeProps } from 'solid-js'; +import { Component, splitProps, Index, createMemo, mergeProps } from 'solid-js'; + +export interface PinInputProps { + /** Number of input fields (default: 6) */ + count?: number; + /** Whether to show error state */ + isError?: boolean; + /** Callback when value changes */ + onInput?: (value: string) => void; + /** Callback when all inputs are filled */ + onComplete?: (value: string) => void; + /** Additional class for root element */ + class?: string; + /** Whether input is required (default: true) */ + required?: boolean; + /** Whether to use OTP mode (default: true) */ + otp?: boolean; + /** Autocomplete attribute (default: 'one-time-code') */ + autoComplete?: string; +} /** * PinInput - OTP/PIN code input - * - * Props: - * - count: number - Number of input fields (default: 6) - * - isError: boolean - Whether to show error state - * - onInput: (value: string) => void - Callback when value changes - * - onComplete: (value: string) => void - Callback when all inputs are filled - * - class: string - Additional class for root element - * - required: boolean - Whether input is required (default: true) - * - otp: boolean - Whether to use OTP mode (default: true) - * - All other props are passed to PinInput.Root */ -export default function PinInputComponent(props) { +const PinInputComponent: Component = (props) => { const [local, machineProps] = splitProps(props, [ 'count', 'isError', @@ -39,13 +48,13 @@ export default function PinInputComponent(props) { const isError = () => local.isError; const classValue = () => local.class; - const handleValueChange = details => { + const handleValueChange = (details: { valueAsString: string }) => { if (local.onInput) { local.onInput(details.valueAsString); } }; - const handleValueComplete = details => { + const handleValueComplete = (details: { valueAsString: string }) => { if (local.onComplete) { local.onComplete(details.valueAsString); } @@ -73,12 +82,13 @@ export default function PinInputComponent(props) { - {item => } + {(item) => }
); -} +}; export { PinInputComponent as PinInput }; +export default PinInputComponent; diff --git a/packages/ui/src/zag/Popover.jsx b/packages/ui/src/zag/Popover.tsx similarity index 67% rename from packages/ui/src/zag/Popover.jsx rename to packages/ui/src/zag/Popover.tsx index 1bbb3941e..23b5a4cd1 100644 --- a/packages/ui/src/zag/Popover.jsx +++ b/packages/ui/src/zag/Popover.tsx @@ -5,31 +5,61 @@ */ import { Popover as ArkPopover, usePopover } from '@ark-ui/solid/popover'; -import { mergeProps, splitProps, Show } from 'solid-js'; +import { Component, mergeProps, splitProps, Show, JSX } from 'solid-js'; import { FiX } from 'solid-icons/fi'; import { Z_INDEX } from '../constants/zIndex.js'; +export type Placement = + | 'top' + | 'top-start' + | 'top-end' + | 'bottom' + | 'bottom-start' + | 'bottom-end' + | 'left' + | 'left-start' + | 'left-end' + | 'right' + | 'right-start' + | 'right-end'; + +export interface PopoverProps { + /** The trigger element */ + trigger: JSX.Element; + /** Popover content */ + children?: JSX.Element; + /** Optional popover title */ + title?: string; + /** Optional popover description */ + description?: string; + /** Controlled open state */ + open?: boolean; + /** Initial open state */ + defaultOpen?: boolean; + /** Callback when open state changes */ + onOpenChange?: (details: { open: boolean }) => void; + /** Popover placement (default: 'bottom') */ + placement?: Placement; + /** Whether to trap focus (default: false) */ + modal?: boolean; + /** Close on outside click (default: true) */ + closeOnInteractOutside?: boolean; + /** Close on escape key (default: true) */ + closeOnEscape?: boolean; + /** Show arrow pointing to trigger (default: false) */ + showArrow?: boolean; + /** Show close button in header (default: true) */ + showCloseButton?: boolean; + /** Set to true when used inside a Dialog */ + inDialog?: boolean; + /** Additional class for content */ + class?: string; +} + /** * Popover - Non-modal floating dialog - * - * Props: - * - trigger: JSX.Element - The trigger element (will be wrapped in a button if not a button) - * - children: JSX.Element - Popover content - * - title: string - Optional popover title - * - description: string - Optional popover description - * - open: boolean - Controlled open state - * - defaultOpen: boolean - Initial open state - * - onOpenChange: (details: { open: boolean }) => void - Callback when open state changes - * - placement: Placement - Popover placement (default: 'bottom') - * - modal: boolean - Whether to trap focus (default: false) - * - closeOnInteractOutside: boolean - Close on outside click (default: true) - * - closeOnEscape: boolean - Close on escape key (default: true) - * - showArrow: boolean - Show arrow pointing to trigger (default: false) - * - showCloseButton: boolean - Show close button in header (default: true) - * - inDialog: boolean - Set to true when used inside a Dialog - * - class: string - Additional class for content */ -export default function PopoverComponent(props) { +const PopoverComponent: Component = (props) => { const merged = mergeProps( { placement: 'bottom', @@ -62,7 +92,7 @@ export default function PopoverComponent(props) { const inDialog = () => local.inDialog; const classValue = () => local.class; - const handleOpenChange = details => { + const handleOpenChange = (details: { open: boolean }) => { if (machineProps.onOpenChange) { machineProps.onOpenChange(details); } @@ -94,9 +124,7 @@ export default function PopoverComponent(props) {
- - {title()} - + {title()} @@ -117,7 +145,7 @@ export default function PopoverComponent(props) { ); -} +}; export { PopoverComponent as Popover }; diff --git a/packages/ui/src/zag/Progress.jsx b/packages/ui/src/zag/Progress.tsx similarity index 73% rename from packages/ui/src/zag/Progress.jsx rename to packages/ui/src/zag/Progress.tsx index 02872281f..81b4e5d1b 100644 --- a/packages/ui/src/zag/Progress.jsx +++ b/packages/ui/src/zag/Progress.tsx @@ -3,23 +3,33 @@ */ import { Progress } from '@ark-ui/solid/progress'; -import { Show, splitProps, createMemo } from 'solid-js'; +import { Component, Show, splitProps, createMemo } from 'solid-js'; + +export interface ProgressProps { + /** Current progress value */ + value?: number; + /** Minimum value (default: 0) */ + min?: number; + /** Maximum value (default: 100) */ + max?: number; + /** Accessible label */ + label?: string; + /** Show percentage value (default: false) */ + showValue?: boolean; + /** Bar height (default: 'md') */ + size?: 'sm' | 'md' | 'lg'; + /** Color variant (default: 'default') */ + variant?: 'default' | 'success' | 'warning' | 'error'; + /** Show indeterminate animation */ + indeterminate?: boolean; + /** Additional class for root element */ + class?: string; +} /** * Progress - Linear progress bar - * - * Props: - * - value: number - Current progress value - * - min: number - Minimum value (default: 0) - * - max: number - Maximum value (default: 100) - * - label: string - Accessible label - * - showValue: boolean - Show percentage value (default: false) - * - size: 'sm' | 'md' | 'lg' - Bar height (default: 'md') - * - variant: 'default' | 'success' | 'warning' | 'error' - Color variant (default: 'default') - * - indeterminate: boolean - Show indeterminate animation - * - class: string - Additional class for root element */ -export default function ProgressComponent(props) { +const ProgressComponent: Component = (props) => { const [local, machineProps] = splitProps(props, [ 'label', 'showValue', @@ -86,6 +96,6 @@ export default function ProgressComponent(props) { ); -} +}; export { ProgressComponent as Progress }; diff --git a/packages/ui/src/zag/QRCode.jsx b/packages/ui/src/zag/QRCode.tsx similarity index 62% rename from packages/ui/src/zag/QRCode.jsx rename to packages/ui/src/zag/QRCode.tsx index 10c5a929c..e117bfac3 100644 --- a/packages/ui/src/zag/QRCode.jsx +++ b/packages/ui/src/zag/QRCode.tsx @@ -3,19 +3,26 @@ */ import { QrCode } from '@ark-ui/solid/qr-code'; +import { Component } from 'solid-js'; + +export interface QRCodeProps { + /** The data to encode in the QR code (e.g., URL, text) */ + data: string; + /** Size of the QR code in pixels (default: 200) */ + size?: number; + /** Additional CSS classes */ + class?: string; + /** Alt text for accessibility (default: 'QR Code') */ + alt?: string; + /** Error correction level (L=7%, M=15%, Q=25%, H=30%) (default: 'M') */ + ecc?: 'L' | 'M' | 'Q' | 'H'; +} /** * QR Code component * Renders an SVG-based QR code with customizable error correction and styling. - * - * @param {Object} props - * @param {string} props.data - The data to encode in the QR code (e.g., URL, text) - * @param {number} [props.size=200] - Size of the QR code in pixels - * @param {string} [props.class] - Additional CSS classes - * @param {string} [props.alt='QR Code'] - Alt text for accessibility - * @param {'L'|'M'|'Q'|'H'} [props.ecc='M'] - Error correction level (L=7%, M=15%, Q=25%, H=30%) */ -export default function QRCodeComponent(props) { +const QRCodeComponent: Component = (props) => { const data = () => props.data; const ecc = () => props.ecc; const size = () => props.size; @@ -48,4 +55,7 @@ export default function QRCodeComponent(props) { ); -} +}; + +export { QRCodeComponent as QRCode }; +export default QRCodeComponent; diff --git a/packages/ui/src/zag/RadioGroup.jsx b/packages/ui/src/zag/RadioGroup.tsx similarity index 73% rename from packages/ui/src/zag/RadioGroup.jsx rename to packages/ui/src/zag/RadioGroup.tsx index d1046ec53..f219700ae 100644 --- a/packages/ui/src/zag/RadioGroup.jsx +++ b/packages/ui/src/zag/RadioGroup.tsx @@ -3,29 +3,46 @@ */ import { RadioGroup } from '@ark-ui/solid/radio-group'; -import { For, splitProps, createMemo } from 'solid-js'; +import { Component, For, splitProps, createMemo } from 'solid-js'; + +export interface RadioGroupItem { + value: string; + label: string; + description?: string; + disabled?: boolean; +} + +export interface RadioGroupProps { + /** Radio items */ + items: RadioGroupItem[]; + /** Group label */ + label?: string; + /** Controlled selected value */ + value?: string; + /** Initial selected value */ + defaultValue?: string; + /** Callback when selection changes */ + onValueChange?: (details: { value: string }) => void; + /** Form field name */ + name?: string; + /** Disable all items */ + disabled?: boolean; + /** Layout orientation (default: 'vertical') */ + orientation?: 'horizontal' | 'vertical'; + /** Additional class for root element */ + class?: string; +} /** * RadioGroup - Radio button group for single selection - * - * Props: - * - items: Array<{ value: string, label: string, description?: string, disabled?: boolean }> - Radio items - * - label: string - Group label - * - value: string - Controlled selected value - * - defaultValue: string - Initial selected value - * - onValueChange: (details: { value: string }) => void - Callback when selection changes - * - name: string - Form field name - * - disabled: boolean - Disable all items - * - orientation: 'horizontal' | 'vertical' - Layout orientation (default: 'vertical') - * - class: string - Additional class for root element */ -export default function RadioGroupComponent(props) { +const RadioGroupComponent: Component = (props) => { const [local, machineProps] = splitProps(props, ['items', 'label', 'class']); const orientation = () => machineProps.orientation || 'vertical'; const isVertical = createMemo(() => orientation() === 'vertical'); - const handleValueChange = details => { + const handleValueChange = (details: { value: string }) => { if (machineProps.onValueChange) { machineProps.onValueChange(details); } @@ -46,7 +63,7 @@ export default function RadioGroupComponent(props) {
- {item => ( + {(item) => ( ); -} +}; export { RadioGroupComponent as RadioGroup }; diff --git a/packages/ui/src/zag/Select.jsx b/packages/ui/src/zag/Select.tsx similarity index 76% rename from packages/ui/src/zag/Select.jsx rename to packages/ui/src/zag/Select.tsx index 97af6b6cb..4c44da8cf 100644 --- a/packages/ui/src/zag/Select.jsx +++ b/packages/ui/src/zag/Select.tsx @@ -6,31 +6,57 @@ import { Select as ArkSelect, createListCollection, useSelect } from '@ark-ui/solid/select'; import { Portal } from 'solid-js/web'; -import { createMemo, Show, Index, splitProps, mergeProps } from 'solid-js'; +import { Component, createMemo, Show, Index, splitProps, mergeProps } from 'solid-js'; import { BiRegularCheck, BiRegularChevronDown } from 'solid-icons/bi'; -import { Z_INDEX } from '../constants/zIndex.js'; +import { Z_INDEX } from '../constants/zIndex.ts'; + +export interface SelectOption { + label: string; + value: string; + disabled?: boolean; +} + +export interface SelectProps { + /** Options to display */ + items: SelectOption[]; + /** The selected value (controlled) */ + value?: string; + /** Callback when value changes */ + onChange?: (value: string) => void; + /** Label text for the select */ + label?: string; + /** Placeholder text when no value selected (default: 'Select option') */ + placeholder?: string; + /** Whether the select is disabled */ + disabled?: boolean; + /** Form input name */ + name?: string; + /** Whether the select is in an invalid state */ + invalid?: boolean; + /** Array of values that should be disabled */ + disabledValues?: string[]; + /** Whether the value can be cleared by clicking the selected item */ + deselectable?: boolean; + /** Whether the select should close after an item is selected (default: true) */ + closeOnSelect?: boolean; + /** Whether to allow multiple selection */ + multiple?: boolean; + /** Positioning options for the dropdown menu */ + positioning?: { + placement?: string; + sameWidth?: boolean; + [key: string]: unknown; + }; + /** Set to true when used inside a Dialog or Popover */ + inDialog?: boolean; + /** Additional props to pass to Select.Root (e.g., open, onOpenChange, etc.) */ + arkProps?: Record; +} /** * Select - Reusable select/dropdown component using Ark UI - * - * @param {Object} props - * @param {Array<{ label: string, value: string, disabled?: boolean }>} props.items - Options to display - * @param {string} [props.value] - The selected value (controlled) - * @param {Function} [props.onChange] - Callback when value changes: (value: string) => void - * @param {string} [props.label] - Label text for the select - * @param {string} [props.placeholder] - Placeholder text when no value selected (default: 'Select option') - * @param {boolean} [props.disabled] - Whether the select is disabled - * @param {string} [props.name] - Form input name - * @param {boolean} [props.invalid] - Whether the select is in an invalid state - * @param {string[]} [props.disabledValues] - Array of values that should be disabled - * @param {boolean} [props.deselectable] - Whether the value can be cleared by clicking the selected item - * @param {boolean} [props.closeOnSelect] - Whether the select should close after an item is selected (default: true) - * @param {boolean} [props.multiple] - Whether to allow multiple selection - * @param {Object} [props.positioning] - Positioning options for the dropdown menu - * @param {boolean} [props.inDialog] - Set to true when used inside a Dialog or Popover - * @param {Object} [props.arkProps] - Additional props to pass to Select.Root (e.g., open, onOpenChange, etc.) */ -export default function SelectComponent(props) { +const SelectComponent: Component = (props) => { // Split convenience props from Ark UI props const [local, arkProps] = splitProps(props, [ 'items', @@ -73,15 +99,15 @@ export default function SelectComponent(props) { // Create collection from items with disabled handling const collection = createMemo(() => { - const collectionItems = items().map(item => ({ + const collectionItems = items().map((item) => ({ ...item, disabled: item.disabled || disabledValues().includes(item.value), })); return createListCollection({ items: collectionItems, - itemToString: item => item.label, - itemToValue: item => item.value, + itemToString: (item: SelectOption) => item.label, + itemToValue: (item: SelectOption) => item.value, }); }); @@ -90,11 +116,7 @@ export default function SelectComponent(props) { const selectValue = createMemo(() => { const v = value(); if (merged.multiple) { - return ( - Array.isArray(v) ? v - : v != null ? [v] - : [] - ); + return Array.isArray(v) ? v : v != null ? [v] : []; } // For single select: return empty array if value is null/undefined, otherwise [value] // Empty string is valid (for "Unassigned" option) @@ -102,16 +124,16 @@ export default function SelectComponent(props) { }); // Handle value change - convert array back to single value for single select - const handleValueChange = details => { + const handleValueChange = (details: { value: string[] }) => { if (!local.onChange) return; if (merged.multiple) { - local.onChange(details.value); + local.onChange(details.value as unknown as string); } else { const newValue = details.value[0] || ''; // Prevent selecting disabled values const currentCollection = collection(); - const item = currentCollection.items.find(i => i.value === newValue); + const item = currentCollection.items.find((i) => i.value === newValue); if (item?.disabled || disabledValues().includes(newValue)) { return; } @@ -120,9 +142,9 @@ export default function SelectComponent(props) { }; // Helper to check if a value is disabled - const isValueDisabled = val => { + const isValueDisabled = (val: string) => { const currentCollection = collection(); - const item = currentCollection.items.find(i => i.value === val); + const item = currentCollection.items.find((i) => i.value === val); const disabledSet = new Set(disabledValues()); return item?.disabled || disabledSet.has(val); }; @@ -175,7 +197,7 @@ export default function SelectComponent(props) { > - {item => { + {(item) => { const isDisabled = () => isValueDisabled(item().value); return ( - {item => { + {(item) => { const isDisabled = () => isValueDisabled(item().value); return ( ); -} +}; + +export default SelectComponent; // Export hook for programmatic control export { useSelect }; diff --git a/packages/ui/src/zag/Splitter.jsx b/packages/ui/src/zag/Splitter.jsx deleted file mode 100644 index 54016acab..000000000 --- a/packages/ui/src/zag/Splitter.jsx +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Splitter component using Ark UI - */ - -import { Splitter } from '@ark-ui/solid/splitter'; - -export default function SplitterComponent() { - return ( - - -

A

-
- - -

B

-
-
- ); -} - -export { SplitterComponent as Splitter }; diff --git a/packages/ui/src/zag/Splitter.tsx b/packages/ui/src/zag/Splitter.tsx new file mode 100644 index 000000000..b955bc5ac --- /dev/null +++ b/packages/ui/src/zag/Splitter.tsx @@ -0,0 +1,50 @@ +/** + * Splitter component using Ark UI + */ + +import { Splitter } from '@ark-ui/solid/splitter'; +import { Component } from 'solid-js'; + +export interface SplitterPanel { + id: string; + minSize?: number; + maxSize?: number; +} + +export interface SplitterProps { + /** Default panel sizes as percentages */ + defaultSize?: number[]; + /** Panel configurations */ + panels?: SplitterPanel[]; + /** Orientation (default: 'horizontal') */ + orientation?: 'horizontal' | 'vertical'; + /** Additional class for root element */ + class?: string; +} + +/** + * Splitter - Resizable panel splitter + */ +const SplitterComponent: Component = (props) => { + return ( + + +

A

+
+ + +

B

+
+
+ ); +}; + +export { SplitterComponent as Splitter }; diff --git a/packages/ui/src/zag/Switch.jsx b/packages/ui/src/zag/Switch.tsx similarity index 70% rename from packages/ui/src/zag/Switch.jsx rename to packages/ui/src/zag/Switch.tsx index bdc2b0ebf..5b7207890 100644 --- a/packages/ui/src/zag/Switch.jsx +++ b/packages/ui/src/zag/Switch.tsx @@ -5,20 +5,29 @@ */ import { Switch as ArkSwitch, useSwitch } from '@ark-ui/solid/switch'; -import { mergeProps, splitProps } from 'solid-js'; +import { Component, mergeProps, splitProps } from 'solid-js'; + +export interface SwitchProps { + /** Controlled checked state */ + checked?: boolean; + /** Default checked state (uncontrolled) */ + defaultChecked?: boolean; + /** Whether switch is disabled */ + disabled?: boolean; + /** Name for form submission */ + name?: string; + /** Callback when checked state changes */ + onChange?: (checked: boolean) => void; + /** Additional CSS classes */ + class?: string; + /** Callback when checked state changes (Ark UI compatible) */ + onCheckedChange?: (details: { checked: boolean }) => void; +} /** * Switch - Full description - * - * Props: - * - checked: boolean - Controlled checked state - * - defaultChecked: boolean - Default checked state (uncontrolled) - * - disabled: boolean - Whether switch is disabled - * - name: string - Name for form submission - * - onChange: Function - Callback when checked state changes: (checked: boolean) => void - * - class: string - Additional CSS classes */ -export default function SwitchComponent(props) { +const SwitchComponent: Component = (props) => { const merged = mergeProps( { defaultChecked: false, @@ -36,7 +45,7 @@ export default function SwitchComponent(props) { const disabled = () => machineProps.disabled; const name = () => machineProps.name; - const handleCheckedChange = details => { + const handleCheckedChange = (details: { checked: boolean }) => { if (onChange()) { onChange()(details.checked === true); } @@ -63,8 +72,9 @@ export default function SwitchComponent(props) { ); -} +}; export { SwitchComponent as Switch }; +export default SwitchComponent; // Export hook for programmatic control export { useSwitch }; diff --git a/packages/ui/src/zag/Tabs.jsx b/packages/ui/src/zag/Tabs.tsx similarity index 73% rename from packages/ui/src/zag/Tabs.jsx rename to packages/ui/src/zag/Tabs.tsx index 4a5dd624e..de23a4f0b 100644 --- a/packages/ui/src/zag/Tabs.jsx +++ b/packages/ui/src/zag/Tabs.tsx @@ -3,24 +3,39 @@ */ import { Tabs } from '@ark-ui/solid/tabs'; -import { For, Show } from 'solid-js'; +import { Component, For, Show, JSX } from 'solid-js'; + +export interface TabDefinition { + value: string; + label: string; + icon?: JSX.Element; + count?: number; + getCount?: () => number; +} + +export interface TabsProps { + /** Array of tab definitions */ + tabs: TabDefinition[]; + /** Default selected tab value */ + defaultValue?: string; + /** Controlled tab value */ + value?: string; + /** Callback when tab changes */ + onValueChange?: (value: string) => void; + /** Tab content render function */ + children?: (tabValue: string) => JSX.Element; +} /** * Reusable Tabs component - * @param {Object} props - * @param {Array<{value: string, label: string, icon?: any}>} props.tabs - Array of tab definitions - * @param {string} [props.defaultValue] - Default selected tab value - * @param {string} [props.value] - Controlled tab value (e.g., from URL) - * @param {(value: string) => void} [props.onValueChange] - Callback when tab changes - * @param {Object} props.children - Tab content as children (use TabContent component) */ -export default function TabsComponent(props) { +const TabsComponent: Component = (props) => { const value = () => props.value; const defaultValue = () => props.defaultValue; const tabsList = () => props.tabs; const children = () => props.children; - const handleValueChange = details => { + const handleValueChange = (details: { value: string }) => { if (props.onValueChange) { props.onValueChange(details.value); } @@ -34,7 +49,7 @@ export default function TabsComponent(props) { > - {tab => ( + {(tab) => ( - {tab => ( + {(tab) => ( ); -} +}; export { TabsComponent as Tabs }; diff --git a/packages/ui/src/zag/TagsInput.jsx b/packages/ui/src/zag/TagsInput.tsx similarity index 70% rename from packages/ui/src/zag/TagsInput.jsx rename to packages/ui/src/zag/TagsInput.tsx index a83d22183..d909fc482 100644 --- a/packages/ui/src/zag/TagsInput.jsx +++ b/packages/ui/src/zag/TagsInput.tsx @@ -3,31 +3,48 @@ */ import { TagsInput } from '@ark-ui/solid/tags-input'; -import { splitProps, mergeProps, Index } from 'solid-js'; +import { Component, splitProps, mergeProps, Index } from 'solid-js'; import { FiX } from 'solid-icons/fi'; +export interface TagsInputProps { + /** Input label */ + label?: string; + /** Input placeholder (default: 'Add tag...') */ + placeholder?: string; + /** Controlled tag values */ + value?: string[]; + /** Initial tag values */ + defaultValue?: string[]; + /** Callback when tags change */ + onValueChange?: (details: { value: string[] }) => void; + /** Maximum number of tags */ + max?: number; + /** Allow duplicate tags (default: false) */ + allowDuplicates?: boolean; + /** Disable the input */ + disabled?: boolean; + /** Make input read-only */ + readOnly?: boolean; + /** Mark as invalid */ + invalid?: boolean; + /** Form field name */ + name?: string; + /** What to do with input on blur */ + blurBehavior?: 'add' | 'clear'; + /** Add tags when pasting (default: true) */ + addOnPaste?: boolean; + /** Allow editing tags (default: true) */ + editable?: boolean; + /** Additional class for root element */ + class?: string; + /** Additional class for input element */ + inputClass?: string; +} + /** * TagsInput - Input for multiple tag values - * - * Props: - * - label: string - Input label - * - placeholder: string - Input placeholder (default: 'Add tag...') - * - value: string[] - Controlled tag values - * - defaultValue: string[] - Initial tag values - * - onValueChange: (details: { value: string[] }) => void - Callback when tags change - * - max: number - Maximum number of tags - * - allowDuplicates: boolean - Allow duplicate tags (default: false) - * - disabled: boolean - Disable the input - * - readOnly: boolean - Make input read-only - * - invalid: boolean - Mark as invalid - * - name: string - Form field name - * - blurBehavior: 'add' | 'clear' - What to do with input on blur - * - addOnPaste: boolean - Add tags when pasting (default: true) - * - editable: boolean - Allow editing tags (default: true) - * - class: string - Additional class for root element - * - inputClass: string - Additional class for input element */ -export default function TagsInputComponent(props) { +const TagsInputComponent: Component = (props) => { const [local, machineProps] = splitProps(props, [ 'label', 'placeholder', @@ -36,7 +53,7 @@ export default function TagsInputComponent(props) { 'inputClass', ]); - const validate = details => { + const validate = (details: { values: string[]; inputValue: string }) => { if (local.allowDuplicates) return true; return !details.values.includes(details.inputValue); }; @@ -47,7 +64,7 @@ export default function TagsInputComponent(props) { validate, }); - const handleValueChange = details => { + const handleValueChange = (details: { value: string[] }) => { if (context.onValueChange) { context.onValueChange(details); } @@ -69,7 +86,7 @@ export default function TagsInputComponent(props) { class={`w-full ${local.class || ''}`} > - {api => ( + {(api) => ( <> {local.label && ( @@ -101,6 +118,6 @@ export default function TagsInputComponent(props) { ); -} +}; export { TagsInputComponent as TagsInput }; diff --git a/packages/ui/src/zag/Toast.jsx b/packages/ui/src/zag/Toast.tsx similarity index 76% rename from packages/ui/src/zag/Toast.jsx rename to packages/ui/src/zag/Toast.tsx index 410a86ae6..3e8a8845e 100644 --- a/packages/ui/src/zag/Toast.jsx +++ b/packages/ui/src/zag/Toast.tsx @@ -3,10 +3,21 @@ */ import { Toast, Toaster, createToaster } from '@ark-ui/solid/toast'; -import { Show } from 'solid-js'; +import { Component, Show } from 'solid-js'; import { FiX, FiCheck, FiAlertCircle, FiInfo, FiLoader } from 'solid-icons/fi'; import { Z_INDEX } from '../constants/zIndex.js'; +export interface ToastOptions { + /** Toast title */ + title?: string; + /** Toast description */ + description?: string; + /** Toast type */ + type?: 'info' | 'success' | 'warning' | 'error' | 'loading'; + /** Duration in milliseconds */ + duration?: number; +} + /** * Create the toast store - this is the global toaster instance * Import this to create toasts from anywhere in the app @@ -21,8 +32,8 @@ export const toaster = createToaster({ /** * Toaster component - renders all active toasts */ -export default function ToasterComponent() { - const getIcon = type => { +const ToasterComponent: Component = () => { + const getIcon = (type?: string) => { switch (type) { case 'success': return ; @@ -35,7 +46,7 @@ export default function ToasterComponent() { } }; - const getStyles = type => { + const getStyles = (type?: string) => { switch (type) { case 'success': return 'border-green-200 bg-green-50'; @@ -53,7 +64,7 @@ export default function ToasterComponent() { toaster={toaster} class={`pointer-events-none fixed inset-0 ${Z_INDEX.TOAST} flex flex-col items-end p-4 sm:p-6`} > - {toast => ( + {(toast) => ( @@ -62,9 +73,7 @@ export default function ToasterComponent() {
{getIcon(toast().type)}
- - {toast().title} - + {toast().title} @@ -84,29 +93,29 @@ export default function ToasterComponent() { )} ); -} +}; /** * Convenience methods for creating toasts */ export const showToast = { - success: (title, description) => + success: (title?: string, description?: string) => toaster.create({ title, description, type: 'success', duration: 3000 }), - error: (title, description) => + error: (title?: string, description?: string) => toaster.create({ title, description, type: 'error', duration: 5000 }), - info: (title, description) => + info: (title?: string, description?: string) => toaster.create({ title, description, type: 'info', duration: 3000 }), - loading: (title, description) => + loading: (title?: string, description?: string) => toaster.create({ title, description, type: 'loading', duration: Infinity }), - promise: (promise, options) => toaster.promise(promise, options), + promise: (promise: Promise, options: ToastOptions) => toaster.promise(promise, options), - dismiss: id => toaster.dismiss(id), + dismiss: (id: string) => toaster.dismiss(id), - update: (id, options) => toaster.update(id, options), + update: (id: string, options: ToastOptions) => toaster.update(id, options), }; export { ToasterComponent as Toaster }; diff --git a/packages/ui/src/zag/ToggleGroup.jsx b/packages/ui/src/zag/ToggleGroup.tsx similarity index 63% rename from packages/ui/src/zag/ToggleGroup.jsx rename to packages/ui/src/zag/ToggleGroup.tsx index ae18c9bb4..2a27cb122 100644 --- a/packages/ui/src/zag/ToggleGroup.jsx +++ b/packages/ui/src/zag/ToggleGroup.tsx @@ -3,26 +3,45 @@ */ import { ToggleGroup } from '@ark-ui/solid/toggle-group'; -import { For, splitProps, createMemo } from 'solid-js'; +import { Component, For, splitProps, createMemo, JSX } from 'solid-js'; + +export interface ToggleGroupItem { + value: string; + label: JSX.Element; + disabled?: boolean; +} + +export interface ToggleGroupProps { + /** Toggle items */ + items: ToggleGroupItem[]; + /** Controlled selected values */ + value?: string[]; + /** Initial selected values */ + defaultValue?: string[]; + /** Callback when selection changes */ + onValueChange?: (details: { value: string[] }) => void; + /** Allow multiple selections (default: false) */ + multiple?: boolean; + /** Disable all toggles */ + disabled?: boolean; + /** Layout orientation (default: 'horizontal') */ + orientation?: 'horizontal' | 'vertical'; + /** Loop focus navigation (default: true) */ + loop?: boolean; + /** Use roving tabindex (default: true) */ + rovingFocus?: boolean; + /** Allow deselecting when single (default: true) */ + deselectable?: boolean; + /** Button size (default: 'md') */ + size?: 'sm' | 'md' | 'lg'; + /** Additional class for root element */ + class?: string; +} /** * ToggleGroup - Group of toggle buttons - * - * Props: - * - items: Array<{ value: string, label: JSX.Element, disabled?: boolean }> - Toggle items - * - value: string[] - Controlled selected values - * - defaultValue: string[] - Initial selected values - * - onValueChange: (details: { value: string[] }) => void - Callback when selection changes - * - multiple: boolean - Allow multiple selections (default: false) - * - disabled: boolean - Disable all toggles - * - orientation: 'horizontal' | 'vertical' - Layout orientation (default: 'horizontal') - * - loop: boolean - Loop focus navigation (default: true) - * - rovingFocus: boolean - Use roving tabindex (default: true) - * - deselectable: boolean - Allow deselecting when single (default: true) - * - size: 'sm' | 'md' | 'lg' - Button size (default: 'md') - * - class: string - Additional class for root element */ -export default function ToggleGroupComponent(props) { +const ToggleGroupComponent: Component = (props) => { const [local, machineProps] = splitProps(props, ['items', 'size', 'class']); const getSizeClass = () => { @@ -39,7 +58,7 @@ export default function ToggleGroupComponent(props) { const orientation = () => machineProps.orientation || 'horizontal'; const isVertical = createMemo(() => orientation() === 'vertical'); - const handleValueChange = details => { + const handleValueChange = (details: { value: string[] }) => { if (machineProps.onValueChange) { machineProps.onValueChange(details); } @@ -73,6 +92,6 @@ export default function ToggleGroupComponent(props) { ); -} +}; export { ToggleGroupComponent as ToggleGroup }; diff --git a/packages/ui/src/zag/Tooltip.jsx b/packages/ui/src/zag/Tooltip.tsx similarity index 57% rename from packages/ui/src/zag/Tooltip.jsx rename to packages/ui/src/zag/Tooltip.tsx index 6ecd096af..2266df462 100644 --- a/packages/ui/src/zag/Tooltip.jsx +++ b/packages/ui/src/zag/Tooltip.tsx @@ -6,36 +6,77 @@ import { Tooltip as ArkTooltip, useTooltip } from '@ark-ui/solid/tooltip'; import { Portal } from 'solid-js/web'; -import { mergeProps, splitProps, createMemo, Show } from 'solid-js'; +import { Component, mergeProps, splitProps, createMemo, Show, JSX } from 'solid-js'; import { Z_INDEX } from '../constants/zIndex.js'; +export type Placement = + | 'top' + | 'top-start' + | 'top-end' + | 'bottom' + | 'bottom-start' + | 'bottom-end' + | 'left' + | 'left-start' + | 'left-end' + | 'right' + | 'right-start' + | 'right-end'; + +export interface TooltipProps { + /** Tooltip content */ + content: string | JSX.Element; + /** Trigger element (will be wrapped) */ + children: JSX.Element; + /** Tooltip placement (default: 'top') */ + placement?: Placement; + /** Delay before opening (default: 400ms) */ + openDelay?: number; + /** Delay before closing (default: 150ms) */ + closeDelay?: number; + /** Allow interaction with tooltip content (default: false) */ + interactive?: boolean; + /** Disable the tooltip */ + disabled?: boolean; + /** Controlled open state */ + open?: boolean; + /** Initial open state (uncontrolled) */ + defaultOpen?: boolean; + /** Callback when open state changes */ + onOpenChange?: (details: { open: boolean }) => void; + /** Show arrow pointing to trigger (default: true) */ + showArrow?: boolean; + /** Enable lazy mounting (default: true) */ + lazyMount?: boolean; + /** Unmount on exit (default: true) */ + unmountOnExit?: boolean; + /** Close on click (default: true) */ + closeOnClick?: boolean; + /** Close on scroll (default: true) */ + closeOnScroll?: boolean; + /** Close on pointer down (default: true) */ + closeOnPointerDown?: boolean; + /** Close on escape key (default: true) */ + closeOnEscape?: boolean; + /** Custom positioning options */ + positioning?: { + placement?: Placement; + gutter?: number; + strategy?: 'absolute' | 'fixed'; + flip?: boolean; + boundary?: string | { x: number; y: number; width: number; height: number }; + [key: string]: unknown; + }; + /** Additional class for root element */ + class?: string; + /** Additional class for content element */ + contentClass?: string; +} + /** * Tooltip - High-level convenience component - * - * Props: - * - content: string | JSX.Element - Tooltip content - * - children: JSX.Element - Trigger element (will be wrapped) - * - placement: Placement - Tooltip placement (default: 'top') - * - openDelay: number - Delay before opening (default: 400ms) - * - closeDelay: number - Delay before closing (default: 150ms) - * - interactive: boolean - Allow interaction with tooltip content (default: false) - * - disabled: boolean - Disable the tooltip - * - open: boolean - Controlled open state - * - defaultOpen: boolean - Initial open state (uncontrolled) - * - onOpenChange: (details: { open: boolean }) => void - Callback when open state changes - * - showArrow: boolean - Show arrow pointing to trigger (default: true) - * - lazyMount: boolean - Enable lazy mounting (default: true) - * - unmountOnExit: boolean - Unmount on exit (default: true) - * - closeOnClick: boolean - Close on click (default: true) - * - closeOnScroll: boolean - Close on scroll (default: true) - * - closeOnPointerDown: boolean - Close on pointer down (default: true) - * - closeOnEscape: boolean - Close on escape key (default: true) - * - positioning: PositioningOptions - Custom positioning options - * - class: string - Additional class for root element - * - contentClass: string - Additional class for content element - * - All other Ark UI Tooltip.Root props are supported */ -export default function TooltipComponent(props) { +const TooltipComponent: Component = (props) => { const merged = mergeProps( { placement: 'top', @@ -88,7 +129,7 @@ export default function TooltipComponent(props) { const positioning = createMemo(() => ({ placement: placement(), gutter: 8, - strategy: 'fixed', + strategy: 'fixed' as const, flip: true, boundary: getBoundary(), ...machineProps.positioning, @@ -113,7 +154,7 @@ export default function TooltipComponent(props) { ); -} +}; // Export high-level component export { TooltipComponent as Tooltip }; diff --git a/packages/ui/src/zag/Tour.jsx b/packages/ui/src/zag/Tour.jsx deleted file mode 100644 index 85e7aa71b..000000000 --- a/packages/ui/src/zag/Tour.jsx +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Tour component - Keeping Zag.js implementation for now - * Ark UI Tour has a different API structure that requires significant refactoring. - * This component is not currently used in the codebase, so migration can be deferred. - */ - -import * as tour from '@zag-js/tour'; -import { Portal } from 'solid-js/web'; -import { normalizeProps, useMachine } from '@zag-js/solid'; -import { - createMemo, - createUniqueId, - For, - Show, - splitProps, - createContext, - useContext, -} from 'solid-js'; -import { FiX } from 'solid-icons/fi'; -import { Z_INDEX } from '../constants/zIndex.js'; - -// Context for providing tour API to children -const TourContext = createContext(); - -/** - * useTour - Hook to access tour API from TourProvider context - */ -export function useTour() { - const context = useContext(TourContext); - if (!context) { - throw new Error('useTour must be used within a TourProvider'); - } - return context; -} - -/** - * TourProvider - Provides tour functionality to children - * - * Props: - * - steps: Array - Tour steps configuration - * - onStepChange: (details: StepChangeDetails) => void - Callback when step changes - * - onStatusChange: (details: StatusChangeDetails) => void - Callback when tour status changes - * - closeOnInteractOutside: boolean - Close on outside click (default: true) - * - closeOnEscape: boolean - Close on escape key (default: true) - * - keyboardNavigation: boolean - Allow arrow key navigation (default: true) - * - preventInteraction: boolean - Prevent page interaction during tour - * - spotlightOffset: { x: number, y: number } - Spotlight padding offset - * - spotlightRadius: number - Spotlight border radius - * - children: JSX.Element - Child components - * - * StepDetails: - * - id: string - Unique step identifier - * - type: 'tooltip' | 'dialog' | 'floating' | 'wait' - Step type - * - title: string - Step title - * - description: string - Step description - * - target: () => Element - Target element for tooltip steps - * - placement: Placement - Tooltip placement - * - actions: Array<{ label: string, action: 'next' | 'prev' | 'dismiss' }> - Step actions - * - backdrop: boolean - Show backdrop (default: true) - * - arrow: boolean - Show arrow for tooltips (default: true) - * - effect: (ctx: { next, show, update }) => void | (() => void) - Side effect before showing step - */ -export function TourProvider(props) { - const [local, machineProps] = splitProps(props, ['children']); - - const service = useMachine(tour.machine, () => ({ - id: createUniqueId(), - closeOnInteractOutside: true, - closeOnEscape: true, - keyboardNavigation: true, - ...machineProps, - })); - - const api = createMemo(() => tour.connect(service, normalizeProps)); - - return ( - - {local.children} - - - - -
- - - -
- - -
-
- -
-
-
- - -
-
-

- {api().step.title} -

- -
- -
- {api().step.description} -
- -
- - {api().getProgressText()} - - -
- - {action => ( - - )} - -
-
-
-
-
- - - - ); -} - -/** - * Tour - Standalone tour component (alternative to TourProvider) - * - * Props: - * - Same as TourProvider - * - renderTrigger: (api: TourApi) => JSX.Element - Render function for trigger - */ -export default function TourComponent(props) { - const [local, rest] = splitProps(props, ['renderTrigger']); - - return ( - - - - ); -} - -function TourContent(props) { - const api = useTour(); - - return {props.renderTrigger(api)}; -} - -export { TourComponent as Tour }; diff --git a/packages/ui/src/zag/Tour.tsx b/packages/ui/src/zag/Tour.tsx new file mode 100644 index 000000000..f7673c610 --- /dev/null +++ b/packages/ui/src/zag/Tour.tsx @@ -0,0 +1,269 @@ +/** + * Tour component - Keeping Zag.js implementation for now + * Ark UI Tour has a different API structure that requires significant refactoring. + * This component is not currently used in the codebase, so migration can be deferred. + */ + +import * as tour from '@zag-js/tour'; +import { Portal } from 'solid-js/web'; +import { normalizeProps, useMachine } from '@zag-js/solid'; +import { + Component, + createMemo, + createUniqueId, + For, + Show, + splitProps, + createContext, + useContext, + Accessor, + JSX, +} from 'solid-js'; +import { FiX } from 'solid-icons/fi'; +import { Z_INDEX } from '../constants/zIndex.ts'; + +export type Placement = + | 'top' + | 'top-start' + | 'top-end' + | 'bottom' + | 'bottom-start' + | 'bottom-end' + | 'left' + | 'left-start' + | 'left-end' + | 'right' + | 'right-start' + | 'right-end'; + +export interface TourStepAction { + label: string; + action: 'next' | 'prev' | 'dismiss'; +} + +export interface TourStep { + /** Unique step identifier */ + id: string; + /** Step type */ + type: 'tooltip' | 'dialog' | 'floating' | 'wait'; + /** Step title */ + title: string; + /** Step description */ + description: string; + /** Target element for tooltip steps */ + target?: () => Element | null; + /** Tooltip placement */ + placement?: Placement; + /** Step actions */ + actions?: TourStepAction[]; + /** Show backdrop (default: true) */ + backdrop?: boolean; + /** Show arrow for tooltips (default: true) */ + arrow?: boolean; + /** Side effect before showing step */ + effect?: (ctx: { + next: () => void; + show: () => void; + update: () => void; + }) => void | (() => void); +} + +export interface TourApi { + /** Whether tour is open */ + open: boolean; + /** Current step */ + step: TourStep | null; + /** Start the tour */ + start: () => void; + /** Stop the tour */ + stop: () => void; + /** Go to next step */ + next: () => void; + /** Go to previous step */ + prev: () => void; + /** Get progress text */ + getProgressText: () => string; + /** Get backdrop props */ + getBackdropProps: () => Record; + /** Get spotlight props */ + getSpotlightProps: () => Record; + /** Get positioner props */ + getPositionerProps: () => Record; + /** Get content props */ + getContentProps: () => Record; + /** Get arrow props */ + getArrowProps: () => Record; + /** Get arrow tip props */ + getArrowTipProps: () => Record; + /** Get title props */ + getTitleProps: () => Record; + /** Get description props */ + getDescriptionProps: () => Record; + /** Get progress text props */ + getProgressTextProps: () => Record; + /** Get action trigger props */ + getActionTriggerProps: (options: { action: TourStepAction }) => Record; + /** Get close trigger props */ + getCloseTriggerProps: () => Record; +} + +// Context for providing tour API to children +const TourContext = createContext>(); + +/** + * useTour - Hook to access tour API from TourProvider context + */ +export function useTour(): Accessor { + const context = useContext(TourContext); + if (!context) { + throw new Error('useTour must be used within a TourProvider'); + } + return context; +} + +export interface TourProviderProps { + /** Tour steps configuration */ + steps: TourStep[]; + /** Callback when step changes */ + onStepChange?: (details: { stepId: string; stepIndex: number }) => void; + /** Callback when tour status changes */ + onStatusChange?: (details: { status: 'started' | 'stopped' | 'completed' | 'skipped' }) => void; + /** Close on outside click (default: true) */ + closeOnInteractOutside?: boolean; + /** Close on escape key (default: true) */ + closeOnEscape?: boolean; + /** Allow arrow key navigation (default: true) */ + keyboardNavigation?: boolean; + /** Prevent page interaction during tour */ + preventInteraction?: boolean; + /** Spotlight padding offset */ + spotlightOffset?: { x: number; y: number }; + /** Spotlight border radius */ + spotlightRadius?: number; + /** Child components */ + children: JSX.Element; +} + +/** + * TourProvider - Provides tour functionality to children + */ +export const TourProvider: Component = (props) => { + const [local, machineProps] = splitProps(props, ['children']); + + const service = useMachine(tour.machine, () => ({ + id: createUniqueId(), + closeOnInteractOutside: true, + closeOnEscape: true, + keyboardNavigation: true, + ...machineProps, + })); + + const api = createMemo(() => tour.connect(service, normalizeProps) as TourApi); + + return ( + + {local.children} + + + + +
+ + + +
+ + +
+
+ +
+
+
+ + +
+
+

+ {api().step?.title} +

+ +
+ +
+ {api().step?.description} +
+ +
+ + {api().getProgressText()} + + +
+ + {(action) => ( + + )} + +
+
+
+
+
+ + + + ); +}; + +export interface TourProps extends Omit { + /** Render function for trigger */ + renderTrigger?: (api: Accessor) => JSX.Element; +} + +/** + * Tour - Standalone tour component (alternative to TourProvider) + */ +const TourComponent: Component = (props) => { + const [local, rest] = splitProps(props, ['renderTrigger']); + + return ( + + + + ); +}; + +function TourContent(props: { renderTrigger?: (api: Accessor) => JSX.Element }) { + const api = useTour(); + + return {props.renderTrigger?.(api)}; +} + +export { TourComponent as Tour }; diff --git a/packages/ui/src/zag/__tests__/Dialog.test.jsx b/packages/ui/src/zag/__tests__/Dialog.test.tsx similarity index 99% rename from packages/ui/src/zag/__tests__/Dialog.test.jsx rename to packages/ui/src/zag/__tests__/Dialog.test.tsx index 4b3bc82d1..c1fbf8364 100644 --- a/packages/ui/src/zag/__tests__/Dialog.test.jsx +++ b/packages/ui/src/zag/__tests__/Dialog.test.tsx @@ -4,7 +4,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, cleanup } from '@solidjs/testing-library'; -import { Dialog, ConfirmDialog, useConfirmDialog } from '../Dialog.jsx'; +import { Dialog, ConfirmDialog, useConfirmDialog } from '../Dialog.tsx'; describe('Dialog', () => { beforeEach(() => { diff --git a/packages/ui/src/zag/__tests__/RadioGroup.test.jsx b/packages/ui/src/zag/__tests__/RadioGroup.test.tsx similarity index 99% rename from packages/ui/src/zag/__tests__/RadioGroup.test.jsx rename to packages/ui/src/zag/__tests__/RadioGroup.test.tsx index 86b4fff0a..965800ad0 100644 --- a/packages/ui/src/zag/__tests__/RadioGroup.test.jsx +++ b/packages/ui/src/zag/__tests__/RadioGroup.test.tsx @@ -4,7 +4,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, cleanup } from '@solidjs/testing-library'; -import { RadioGroup } from '../RadioGroup.jsx'; +import { RadioGroup } from '../RadioGroup.tsx'; describe('RadioGroup', () => { const defaultItems = [ diff --git a/packages/ui/src/zag/__tests__/Checkbox.test.jsx b/packages/ui/src/zag/__tests__/checkbox.test.tsx similarity index 99% rename from packages/ui/src/zag/__tests__/Checkbox.test.jsx rename to packages/ui/src/zag/__tests__/checkbox.test.tsx index 8e247f898..f57172399 100644 --- a/packages/ui/src/zag/__tests__/Checkbox.test.jsx +++ b/packages/ui/src/zag/__tests__/checkbox.test.tsx @@ -5,7 +5,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, cleanup } from '@solidjs/testing-library'; import { createSignal } from 'solid-js'; -import { Checkbox } from '../Checkbox.jsx'; +import { Checkbox } from '../Checkbox.tsx'; describe('Checkbox', () => { beforeEach(() => { diff --git a/packages/ui/src/zag/__tests__/Progress.test.jsx b/packages/ui/src/zag/__tests__/progress.test.tsx similarity index 99% rename from packages/ui/src/zag/__tests__/Progress.test.jsx rename to packages/ui/src/zag/__tests__/progress.test.tsx index ac98933e6..fccf780bc 100644 --- a/packages/ui/src/zag/__tests__/Progress.test.jsx +++ b/packages/ui/src/zag/__tests__/progress.test.tsx @@ -4,7 +4,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { render, screen, cleanup } from '@solidjs/testing-library'; -import { Progress } from '../Progress.jsx'; +import { Progress } from '../Progress.tsx'; describe('Progress', () => { beforeEach(() => { diff --git a/packages/ui/src/zag/__tests__/Switch.test.jsx b/packages/ui/src/zag/__tests__/switch.test.tsx similarity index 99% rename from packages/ui/src/zag/__tests__/Switch.test.jsx rename to packages/ui/src/zag/__tests__/switch.test.tsx index aa808858c..d2e25646c 100644 --- a/packages/ui/src/zag/__tests__/Switch.test.jsx +++ b/packages/ui/src/zag/__tests__/switch.test.tsx @@ -4,7 +4,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, cleanup } from '@solidjs/testing-library'; -import Switch from '../Switch.jsx'; +import { Switch } from '../Switch.tsx'; describe('Switch', () => { beforeEach(() => { diff --git a/packages/ui/src/zag/__tests__/Tabs.test.jsx b/packages/ui/src/zag/__tests__/tabs.test.tsx similarity index 99% rename from packages/ui/src/zag/__tests__/Tabs.test.jsx rename to packages/ui/src/zag/__tests__/tabs.test.tsx index fca030211..26d758110 100644 --- a/packages/ui/src/zag/__tests__/Tabs.test.jsx +++ b/packages/ui/src/zag/__tests__/tabs.test.tsx @@ -5,7 +5,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, fireEvent, cleanup } from '@solidjs/testing-library'; import { createSignal } from 'solid-js'; -import { Tabs } from '../Tabs.jsx'; +import { Tabs } from '../Tabs.tsx'; describe('Tabs', () => { const defaultTabs = [ diff --git a/packages/ui/src/zag/__tests__/Tooltip.test.jsx b/packages/ui/src/zag/__tests__/tooltip.test.tsx similarity index 98% rename from packages/ui/src/zag/__tests__/Tooltip.test.jsx rename to packages/ui/src/zag/__tests__/tooltip.test.tsx index 145a4aefb..3841d88c2 100644 --- a/packages/ui/src/zag/__tests__/Tooltip.test.jsx +++ b/packages/ui/src/zag/__tests__/tooltip.test.tsx @@ -4,7 +4,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { render, screen, cleanup } from '@solidjs/testing-library'; -import { Tooltip } from '../Tooltip.jsx'; +import { Tooltip } from '../Tooltip.tsx'; describe('Tooltip', () => { beforeEach(() => { diff --git a/packages/ui/src/zag/index.js b/packages/ui/src/zag/index.js deleted file mode 100644 index 9bc5d4444..000000000 --- a/packages/ui/src/zag/index.js +++ /dev/null @@ -1,32 +0,0 @@ -// UI Components -// Most components have been migrated to Ark UI, some still use Zag.js -// Re-export all components from individual files - -export { Accordion } from './Accordion.jsx'; -export { Avatar } from './Avatar.jsx'; -export { Checkbox } from './Checkbox.jsx'; -export { Clipboard, CopyButton } from './Clipboard.jsx'; -export { default as Collapsible, useCollapsible } from './Collapsible.jsx'; -export { Combobox } from './Combobox.jsx'; -export { Dialog, ConfirmDialog, useConfirmDialog } from './Dialog.jsx'; -export { Drawer } from './Drawer.jsx'; -export { default as Editable } from './Editable.jsx'; -export { FileUpload } from './FileUpload.jsx'; -export { FloatingPanel } from './FloatingPanel.jsx'; -export { Menu } from './Menu.jsx'; -export { NumberInput } from './NumberInput.jsx'; -export { default as PasswordInput } from './PasswordInput.jsx'; -export { default as PinInput } from './PinInput.jsx'; -export { Popover } from './Popover.jsx'; -export { Progress } from './Progress.jsx'; -export { default as QRCode } from './QRCode.jsx'; -export { RadioGroup } from './RadioGroup.jsx'; -export { default as Select, useSelect } from './Select.jsx'; -export { Splitter } from './Splitter.jsx'; -export { default as Switch } from './Switch.jsx'; -export { Tabs } from './Tabs.jsx'; -export { TagsInput } from './TagsInput.jsx'; -export { Toaster, toaster, showToast } from './Toast.jsx'; -export { ToggleGroup } from './ToggleGroup.jsx'; -export { Tooltip } from './Tooltip.jsx'; -export { Tour, TourProvider, useTour } from './Tour.jsx'; diff --git a/packages/ui/src/zag/index.ts b/packages/ui/src/zag/index.ts new file mode 100644 index 000000000..95600e259 --- /dev/null +++ b/packages/ui/src/zag/index.ts @@ -0,0 +1,32 @@ +// UI Components +// Most components have been migrated to Ark UI, some still use Zag.js +// Re-export all components from individual files + +export { Accordion } from './Accordion.tsx'; +export { Avatar } from './Avatar.tsx'; +export { Checkbox } from './Checkbox.tsx'; +export { Clipboard, CopyButton } from './Clipboard.tsx'; +export { default as Collapsible, useCollapsible } from './Collapsible.tsx'; +export { Combobox } from './Combobox.tsx'; +export { Dialog, ConfirmDialog, useConfirmDialog } from './Dialog.tsx'; +export { Drawer } from './Drawer.tsx'; +export { default as Editable } from './Editable.tsx'; +export { FileUpload } from './FileUpload.tsx'; +export { FloatingPanel } from './FloatingPanel.tsx'; +export { Menu } from './Menu.tsx'; +export { NumberInput } from './NumberInput.tsx'; +export { default as PasswordInput } from './PasswordInput.tsx'; +export { default as PinInput } from './PinInput.tsx'; +export { Popover } from './Popover.tsx'; +export { Progress } from './Progress.tsx'; +export { default as QRCode } from './QRCode.tsx'; +export { RadioGroup } from './RadioGroup.tsx'; +export { default as Select, useSelect } from './Select.tsx'; +export { Splitter } from './Splitter.tsx'; +export { default as Switch } from './Switch.tsx'; +export { Tabs } from './Tabs.tsx'; +export { TagsInput } from './TagsInput.tsx'; +export { Toaster, toaster, showToast } from './Toast.tsx'; +export { ToggleGroup } from './ToggleGroup.tsx'; +export { Tooltip } from './Tooltip.tsx'; +export { Tour, TourProvider, useTour } from './Tour.tsx'; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json new file mode 100644 index 000000000..3ced04584 --- /dev/null +++ b/packages/ui/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022", "DOM"], + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "resolveJsonModule": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@zag/*": ["src/zag/*"] + }, + "types": [] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/ui/vitest.config.js b/packages/ui/vitest.config.js index 8835a6841..d725fd70f 100644 --- a/packages/ui/vitest.config.js +++ b/packages/ui/vitest.config.js @@ -6,8 +6,8 @@ export default defineConfig({ test: { environment: 'jsdom', globals: true, - setupFiles: ['./src/__tests__/setup.js'], - include: ['src/**/*.{test,spec}.{js,jsx}'], + setupFiles: ['./src/__tests__/setup.ts'], + include: ['src/**/*.{test,spec}.{js,jsx,ts,tsx}'], server: { deps: { inline: [/solid-icons/, /@ark-ui/], diff --git a/packages/workers/turbo.json b/packages/workers/turbo.json deleted file mode 100644 index 5271da383..000000000 --- a/packages/workers/turbo.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "../../node_modules/turbo/schema.json", - "extends": ["//"], - "tasks": { - "build": { - "outputs": [] - } - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 215b0b2f1..361e0d0de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,9 +85,18 @@ importers: specifier: ^4.2.1 version: 4.2.1 devDependencies: + '@types/node': + specifier: ^22.10.1 + version: 22.19.3 + tsx: + specifier: ^4.19.2 + version: 4.21.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@25.0.3)(jiti@2.6.1)(jsdom@27.3.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + version: 3.2.4(@types/node@22.19.3)(jiti@2.6.1)(jsdom@27.3.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) packages/shared: devDependencies: @@ -131,6 +140,9 @@ importers: solid-js: specifier: ^1.9.10 version: 1.9.10 + typescript: + specifier: ^5.9.3 + version: 5.9.3 vite-plugin-solid: specifier: ^2.11.10 version: 2.11.10(@testing-library/jest-dom@6.9.1)(solid-js@1.9.10)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) @@ -2107,6 +2119,9 @@ packages: '@types/micromatch@4.0.10': resolution: {integrity: sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==} + '@types/node@22.19.3': + resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} + '@types/node@25.0.3': resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} @@ -5578,6 +5593,9 @@ packages: undici-types@5.28.4: resolution: {integrity: sha512-3OeMF5Lyowe8VW0skf5qaIE7Or3yS9LS7fvMUI0gg4YxpIBVg0L8BxCmROw2CcYhSkpR68Epz7CGc8MPj94Uww==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -7717,6 +7735,10 @@ snapshots: dependencies: '@types/braces': 3.0.5 + '@types/node@22.19.3': + dependencies: + undici-types: 6.21.0 + '@types/node@25.0.3': dependencies: undici-types: 7.16.0 @@ -7728,7 +7750,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 25.0.3 + '@types/node': 22.19.3 optional: true '@typescript-eslint/parser@8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': @@ -7892,13 +7914,13 @@ snapshots: optionalDependencies: vite: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/mocker@3.2.4(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@3.2.4(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@vitest/mocker@4.0.16(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: @@ -11670,7 +11692,6 @@ snapshots: get-tsconfig: 4.13.0 optionalDependencies: fsevents: 2.3.3 - optional: true turbo-darwin-64@2.7.2: optional: true @@ -11734,6 +11755,8 @@ snapshots: undici-types@5.28.4: {} + undici-types@6.21.0: {} + undici-types@7.16.0: optional: true @@ -11972,13 +11995,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vite-node@3.2.4(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -12025,6 +12048,23 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 + vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.54.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.19.3 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + terser: 5.44.1 + tsx: 4.21.0 + yaml: 2.8.2 + vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.2 @@ -12088,11 +12128,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/node@25.0.3)(jiti@2.6.1)(jsdom@27.3.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@3.2.4(@types/node@22.19.3)(jiti@2.6.1)(jsdom@27.3.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -12110,11 +12150,11 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - vite-node: 3.2.4(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.0.3 + '@types/node': 22.19.3 jsdom: 27.3.0 transitivePeerDependencies: - jiti diff --git a/renovate.json b/renovate.json new file mode 100644 index 000000000..2cd64840c --- /dev/null +++ b/renovate.json @@ -0,0 +1,15 @@ +{ + "extends": ["config:recommended"], + "labels": ["dependencies"], + "rangeStrategy": "bump", + "packageRules": [ + { + "matchPaths": ["apps/**", "packages/**"], + "groupName": "workspace dependencies" + }, + { + "matchDepTypes": ["devDependencies"], + "groupName": "dev dependencies" + } + ] +} diff --git a/turbo.json b/turbo.json index 35ee6bf23..e7665d15d 100644 --- a/turbo.json +++ b/turbo.json @@ -2,14 +2,30 @@ "$schema": "./node_modules/turbo/schema.json", "tasks": { "build": { - "outputs": ["dist/**"] + "dependsOn": ["^build"], + "outputs": ["dist/**", ".output/**", ".vinxi/**"] }, - "check-types": { - "dependsOn": ["^check-types"] + "test": { + "dependsOn": ["^build"], + "outputs": ["coverage/**"] + }, + "test:watch": { + "cache": false, + "persistent": true }, "dev": { - "persistent": true, + "cache": false, + "persistent": true + }, + "deploy": { + "dependsOn": ["build"], "cache": false + }, + "lint": { + "outputs": [] + }, + "format": { + "outputs": [] } } } From 918d332d8fc38d859c014071b28db97596f72559 Mon Sep 17 00:00:00 2001 From: Jacob Maynard Date: Wed, 24 Dec 2025 17:29:58 -0600 Subject: [PATCH 2/7] fix tests and config --- packages/ui/tsconfig.json | 1 + .../middleware/__tests__/rateLimit.test.js | 38 +++++++++++-------- .../src/routes/__tests__/contact.test.js | 2 + .../src/routes/__tests__/email.test.js | 1 + renovate.json | 15 -------- turbo.json | 3 +- 6 files changed, 27 insertions(+), 33 deletions(-) delete mode 100644 renovate.json diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 3ced04584..257f39e12 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -10,6 +10,7 @@ "forceConsistentCasingInFileNames": true, "jsx": "preserve", "jsxImportSource": "solid-js", + "sourceMap": true, "resolveJsonModule": true, "noUnusedLocals": true, "noUnusedParameters": true, diff --git a/packages/workers/src/middleware/__tests__/rateLimit.test.js b/packages/workers/src/middleware/__tests__/rateLimit.test.js index b124f95d1..8591cf910 100644 --- a/packages/workers/src/middleware/__tests__/rateLimit.test.js +++ b/packages/workers/src/middleware/__tests__/rateLimit.test.js @@ -29,7 +29,7 @@ describe('rateLimit middleware', () => { headers: { 'CF-Connecting-IP': uniqueIP, }, - }); + }, { ENVIRONMENT: 'production' }); expect(res.status).toBe(200); } }); @@ -40,13 +40,14 @@ describe('rateLimit middleware', () => { app.get('/test', c => c.json({ message: 'success' })); const uniqueIP = `192.168.2.${testCounter}`; + const testEnv = { ENVIRONMENT: 'production' }; // Make 3 requests (within limit) for (let i = 0; i < 3; i++) { const res = await app.request('/test', { headers: { 'CF-Connecting-IP': uniqueIP, }, - }); + }, testEnv); expect(res.status).toBe(200); } @@ -55,7 +56,7 @@ describe('rateLimit middleware', () => { headers: { 'CF-Connecting-IP': uniqueIP, }, - }); + }, testEnv); expect(res.status).toBe(429); const body = await res.json(); @@ -72,7 +73,7 @@ describe('rateLimit middleware', () => { headers: { 'CF-Connecting-IP': '192.168.1.1', }, - }); + }, { ENVIRONMENT: 'production' }); expect(res.headers.get('X-RateLimit-Limit')).toBe('10'); expect(res.headers.get('X-RateLimit-Remaining')).toBeDefined(); @@ -86,6 +87,7 @@ describe('rateLimit middleware', () => { const uniqueIP1 = `192.168.3.${testCounter}`; const uniqueIP2 = `192.168.4.${testCounter}`; + const testEnv = { ENVIRONMENT: 'production' }; // IP 1 makes 2 requests for (let i = 0; i < 2; i++) { @@ -93,7 +95,7 @@ describe('rateLimit middleware', () => { headers: { 'CF-Connecting-IP': uniqueIP1, }, - }); + }, testEnv); expect(res.status).toBe(200); } @@ -102,7 +104,7 @@ describe('rateLimit middleware', () => { headers: { 'CF-Connecting-IP': uniqueIP2, }, - }); + }, testEnv); expect(res.status).toBe(200); }); @@ -119,13 +121,14 @@ describe('rateLimit middleware', () => { ); app.get('/test', c => c.json({ message: 'success' })); + const testEnv = { ENVIRONMENT: 'production' }; // User 1 makes 2 requests for (let i = 0; i < 2; i++) { const res = await app.request('/test', { headers: { 'x-user-id': 'user-1', }, - }); + }, testEnv); expect(res.status).toBe(200); } @@ -134,7 +137,7 @@ describe('rateLimit middleware', () => { headers: { 'x-user-id': 'user-1', }, - }); + }, testEnv); expect(res1.status).toBe(429); // User 2 should still be able to make requests @@ -142,7 +145,7 @@ describe('rateLimit middleware', () => { headers: { 'x-user-id': 'user-2', }, - }); + }, testEnv); expect(res2.status).toBe(200); }); @@ -152,6 +155,7 @@ describe('rateLimit middleware', () => { app.get('/test', c => c.json({ message: 'success' })); const uniqueIP = `192.168.5.${testCounter}`; + const testEnv = { ENVIRONMENT: 'production' }; // Make 2 requests (within limit) for (let i = 0; i < 2; i++) { @@ -159,7 +163,7 @@ describe('rateLimit middleware', () => { headers: { 'CF-Connecting-IP': uniqueIP, }, - }); + }, testEnv); expect(res.status).toBe(200); } @@ -168,7 +172,7 @@ describe('rateLimit middleware', () => { headers: { 'CF-Connecting-IP': uniqueIP, }, - }); + }, testEnv); expect(res1.status).toBe(429); // Wait for window to expire @@ -179,7 +183,7 @@ describe('rateLimit middleware', () => { headers: { 'CF-Connecting-IP': uniqueIP, }, - }); + }, testEnv); expect(res2.status).toBe(200); }); }); @@ -201,6 +205,7 @@ describe('searchRateLimit', () => { app.get('/search', c => c.json({ results: [] })); const uniqueIP = `192.168.10.${testCounter}`; + const testEnv = { ENVIRONMENT: 'production' }; // Make requests up to limit for (let i = 0; i < 30; i++) { @@ -208,7 +213,7 @@ describe('searchRateLimit', () => { headers: { 'CF-Connecting-IP': uniqueIP, }, - }); + }, testEnv); expect(res.status).toBe(200); } @@ -217,7 +222,7 @@ describe('searchRateLimit', () => { headers: { 'CF-Connecting-IP': uniqueIP, }, - }); + }, testEnv); expect(res.status).toBe(429); }); @@ -240,6 +245,7 @@ describe('emailRateLimit', () => { app.post('/email', c => c.json({ success: true })); const uniqueIP = `192.168.20.${testCounter}`; + const testEnv = { ENVIRONMENT: 'production' }; // Make requests up to limit for (let i = 0; i < 5; i++) { @@ -248,7 +254,7 @@ describe('emailRateLimit', () => { headers: { 'CF-Connecting-IP': uniqueIP, }, - }); + }, testEnv); expect(res.status).toBe(200); } @@ -258,7 +264,7 @@ describe('emailRateLimit', () => { headers: { 'CF-Connecting-IP': uniqueIP, }, - }); + }, testEnv); expect(res.status).toBe(429); }); diff --git a/packages/workers/src/routes/__tests__/contact.test.js b/packages/workers/src/routes/__tests__/contact.test.js index 35d7ee19b..7e6be92e8 100644 --- a/packages/workers/src/routes/__tests__/contact.test.js +++ b/packages/workers/src/routes/__tests__/contact.test.js @@ -302,6 +302,7 @@ describe('Contact Routes - POST /api/contact', () => { for (let i = 0; i < 5; i++) { const testEnv = { ...env, + ENVIRONMENT: 'production', POSTMARK_SERVER_TOKEN: 'test-token', CONTACT_EMAIL: 'contact@example.com', EMAIL_FROM: 'noreply@example.com', @@ -328,6 +329,7 @@ describe('Contact Routes - POST /api/contact', () => { // 6th request should be rate limited const testEnv = { ...env, + ENVIRONMENT: 'production', POSTMARK_SERVER_TOKEN: 'test-token', CONTACT_EMAIL: 'contact@example.com', EMAIL_FROM: 'noreply@example.com', diff --git a/packages/workers/src/routes/__tests__/email.test.js b/packages/workers/src/routes/__tests__/email.test.js index 66d5e552b..1ffabb85a 100644 --- a/packages/workers/src/routes/__tests__/email.test.js +++ b/packages/workers/src/routes/__tests__/email.test.js @@ -49,6 +49,7 @@ async function fetchEmail(path, init = {}) { const testEnv = { ...env, + ENVIRONMENT: 'production', EMAIL_QUEUE: { idFromName: vi.fn(() => ({ toString: () => 'do-id' })), get: vi.fn(() => ({ diff --git a/renovate.json b/renovate.json deleted file mode 100644 index 2cd64840c..000000000 --- a/renovate.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": ["config:recommended"], - "labels": ["dependencies"], - "rangeStrategy": "bump", - "packageRules": [ - { - "matchPaths": ["apps/**", "packages/**"], - "groupName": "workspace dependencies" - }, - { - "matchDepTypes": ["devDependencies"], - "groupName": "dev dependencies" - } - ] -} diff --git a/turbo.json b/turbo.json index e7665d15d..9286e97e6 100644 --- a/turbo.json +++ b/turbo.json @@ -6,8 +6,7 @@ "outputs": ["dist/**", ".output/**", ".vinxi/**"] }, "test": { - "dependsOn": ["^build"], - "outputs": ["coverage/**"] + "dependsOn": ["^build"] }, "test:watch": { "cache": false, From da4771185b5a66713ce5cd81d81d248429d9d9ed Mon Sep 17 00:00:00 2001 From: Jacob Maynard Date: Wed, 24 Dec 2025 17:31:47 -0600 Subject: [PATCH 3/7] version now handled by package json --- .github/workflows/prettier.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index 1cfb0f1b7..676cb4443 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -24,8 +24,6 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 - with: - version: 10 - name: Setup Node.js uses: actions/setup-node@v4 From 901c8cc4f46df77f6aa378f436de19806ed123db Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 24 Dec 2025 23:32:20 +0000 Subject: [PATCH 4/7] Apply Prettier formatting --- .github/dependabot.yml | 226 +- packages/mcp/src/tools/better-auth.ts | 4 +- packages/mcp/src/tools/code-review.ts | 28 +- packages/mcp/src/tools/drizzle.ts | 9 +- packages/mcp/src/tools/lint.ts | 4 +- packages/ui/src/zag/Accordion.tsx | 4 +- packages/ui/src/zag/Avatar.tsx | 2 +- packages/ui/src/zag/Checkbox.tsx | 2 +- packages/ui/src/zag/Clipboard.tsx | 4 +- packages/ui/src/zag/Collapsible.tsx | 2 +- packages/ui/src/zag/Combobox.tsx | 6 +- packages/ui/src/zag/Dialog.tsx | 6 +- packages/ui/src/zag/Drawer.tsx | 6 +- packages/ui/src/zag/FloatingPanel.tsx | 4 +- packages/ui/src/zag/Menu.tsx | 4 +- packages/ui/src/zag/NumberInput.tsx | 2 +- packages/ui/src/zag/PasswordInput.tsx | 2 +- packages/ui/src/zag/PinInput.tsx | 4 +- packages/ui/src/zag/Popover.tsx | 6 +- packages/ui/src/zag/Progress.tsx | 2 +- packages/ui/src/zag/QRCode.tsx | 2 +- packages/ui/src/zag/RadioGroup.tsx | 4 +- packages/ui/src/zag/Select.tsx | 18 +- packages/ui/src/zag/Splitter.tsx | 12 +- packages/ui/src/zag/Switch.tsx | 2 +- packages/ui/src/zag/Tabs.tsx | 6 +- packages/ui/src/zag/TagsInput.tsx | 4 +- packages/ui/src/zag/Toast.tsx | 6 +- packages/ui/src/zag/ToggleGroup.tsx | 2 +- packages/ui/src/zag/Tooltip.tsx | 2 +- packages/ui/src/zag/Tour.tsx | 12 +- .../middleware/__tests__/rateLimit.test.js | 196 +- pnpm-lock.yaml | 7877 ++++++++++++----- 33 files changed, 6192 insertions(+), 2278 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a1e1b2788..9bfd7618d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,202 +1,202 @@ version: 2 updates: # Root package.json dependencies - - package-ecosystem: "npm" - directory: "/" + - package-ecosystem: 'npm' + directory: '/' schedule: - interval: "weekly" - day: "monday" - time: "09:00" + interval: 'weekly' + day: 'monday' + time: '09:00' open-pull-requests-limit: 5 labels: - - "dependencies" - - "root" + - 'dependencies' + - 'root' versioning-strategy: increase commit-message: - prefix: "chore" - prefix-development: "chore" - include: "scope" - rebase-strategy: "auto" + prefix: 'chore' + prefix-development: 'chore' + include: 'scope' + rebase-strategy: 'auto' groups: root-dev-dependencies: patterns: - - "*" - dependency-type: "development" + - '*' + dependency-type: 'development' # Landing package - - package-ecosystem: "npm" - directory: "/packages/landing" + - package-ecosystem: 'npm' + directory: '/packages/landing' schedule: - interval: "weekly" - day: "monday" - time: "09:00" + interval: 'weekly' + day: 'monday' + time: '09:00' open-pull-requests-limit: 3 labels: - - "dependencies" - - "landing" + - 'dependencies' + - 'landing' versioning-strategy: increase commit-message: - prefix: "chore" - prefix-development: "chore" - include: "scope" - rebase-strategy: "auto" + prefix: 'chore' + prefix-development: 'chore' + include: 'scope' + rebase-strategy: 'auto' groups: landing-dependencies: patterns: - - "*" - dependency-type: "production" + - '*' + dependency-type: 'production' landing-dev-dependencies: patterns: - - "*" - dependency-type: "development" + - '*' + dependency-type: 'development' # MCP package - - package-ecosystem: "npm" - directory: "/packages/mcp" + - package-ecosystem: 'npm' + directory: '/packages/mcp' schedule: - interval: "weekly" - day: "monday" - time: "09:00" + interval: 'weekly' + day: 'monday' + time: '09:00' open-pull-requests-limit: 3 labels: - - "dependencies" - - "mcp" + - 'dependencies' + - 'mcp' versioning-strategy: increase commit-message: - prefix: "chore" - prefix-development: "chore" - include: "scope" - rebase-strategy: "auto" + prefix: 'chore' + prefix-development: 'chore' + include: 'scope' + rebase-strategy: 'auto' groups: mcp-dependencies: patterns: - - "*" - dependency-type: "production" + - '*' + dependency-type: 'production' mcp-dev-dependencies: patterns: - - "*" - dependency-type: "development" + - '*' + dependency-type: 'development' # Shared package - - package-ecosystem: "npm" - directory: "/packages/shared" + - package-ecosystem: 'npm' + directory: '/packages/shared' schedule: - interval: "weekly" - day: "monday" - time: "09:00" + interval: 'weekly' + day: 'monday' + time: '09:00' open-pull-requests-limit: 3 labels: - - "dependencies" - - "shared" + - 'dependencies' + - 'shared' versioning-strategy: increase commit-message: - prefix: "chore" - prefix-development: "chore" - include: "scope" - rebase-strategy: "auto" + prefix: 'chore' + prefix-development: 'chore' + include: 'scope' + rebase-strategy: 'auto' groups: shared-dependencies: patterns: - - "*" - dependency-type: "production" + - '*' + dependency-type: 'production' shared-dev-dependencies: patterns: - - "*" - dependency-type: "development" + - '*' + dependency-type: 'development' # UI package - - package-ecosystem: "npm" - directory: "/packages/ui" + - package-ecosystem: 'npm' + directory: '/packages/ui' schedule: - interval: "weekly" - day: "monday" - time: "09:00" + interval: 'weekly' + day: 'monday' + time: '09:00' open-pull-requests-limit: 3 labels: - - "dependencies" - - "ui" + - 'dependencies' + - 'ui' versioning-strategy: increase commit-message: - prefix: "chore" - prefix-development: "chore" - include: "scope" - rebase-strategy: "auto" + prefix: 'chore' + prefix-development: 'chore' + include: 'scope' + rebase-strategy: 'auto' groups: ui-dependencies: patterns: - - "*" - dependency-type: "production" + - '*' + dependency-type: 'production' ui-dev-dependencies: patterns: - - "*" - dependency-type: "development" + - '*' + dependency-type: 'development' # Web package - - package-ecosystem: "npm" - directory: "/packages/web" + - package-ecosystem: 'npm' + directory: '/packages/web' schedule: - interval: "weekly" - day: "monday" - time: "09:00" + interval: 'weekly' + day: 'monday' + time: '09:00' open-pull-requests-limit: 5 labels: - - "dependencies" - - "web" + - 'dependencies' + - 'web' versioning-strategy: increase commit-message: - prefix: "chore" - prefix-development: "chore" - include: "scope" - rebase-strategy: "auto" + prefix: 'chore' + prefix-development: 'chore' + include: 'scope' + rebase-strategy: 'auto' groups: web-dependencies: patterns: - - "*" - dependency-type: "production" + - '*' + dependency-type: 'production' web-dev-dependencies: patterns: - - "*" - dependency-type: "development" + - '*' + dependency-type: 'development' # Workers package - - package-ecosystem: "npm" - directory: "/packages/workers" + - package-ecosystem: 'npm' + directory: '/packages/workers' schedule: - interval: "weekly" - day: "monday" - time: "09:00" + interval: 'weekly' + day: 'monday' + time: '09:00' open-pull-requests-limit: 5 labels: - - "dependencies" - - "workers" + - 'dependencies' + - 'workers' versioning-strategy: increase commit-message: - prefix: "chore" - prefix-development: "chore" - include: "scope" - rebase-strategy: "auto" + prefix: 'chore' + prefix-development: 'chore' + include: 'scope' + rebase-strategy: 'auto' groups: workers-dependencies: patterns: - - "*" - dependency-type: "production" + - '*' + dependency-type: 'production' workers-dev-dependencies: patterns: - - "*" - dependency-type: "development" + - '*' + dependency-type: 'development' # GitHub Actions workflows - - package-ecosystem: "github-actions" - directory: "/" + - package-ecosystem: 'github-actions' + directory: '/' schedule: - interval: "weekly" - day: "monday" - time: "09:00" + interval: 'weekly' + day: 'monday' + time: '09:00' open-pull-requests-limit: 2 labels: - - "dependencies" - - "github-actions" + - 'dependencies' + - 'github-actions' commit-message: - prefix: "ci" - include: "scope" - rebase-strategy: "auto" + prefix: 'ci' + include: 'scope' + rebase-strategy: 'auto' diff --git a/packages/mcp/src/tools/better-auth.ts b/packages/mcp/src/tools/better-auth.ts index d8ce9d159..71e4faee5 100644 --- a/packages/mcp/src/tools/better-auth.ts +++ b/packages/mcp/src/tools/better-auth.ts @@ -43,9 +43,7 @@ export function registerBetterAuthTools(server: McpServerType): void { ) .optional(), }, - async ({ - path: docPath, - }): Promise<{ content: Array<{ type: 'text'; text: string }> }> => { + async ({ path: docPath }): Promise<{ content: Array<{ type: 'text'; text: string }> }> => { try { const url = docPath ? diff --git a/packages/mcp/src/tools/code-review.ts b/packages/mcp/src/tools/code-review.ts index 0af7973c0..1e559c064 100644 --- a/packages/mcp/src/tools/code-review.ts +++ b/packages/mcp/src/tools/code-review.ts @@ -67,7 +67,11 @@ function filterFiles(files: string): string { type CompareTarget = 'staged' | 'unstaged' | string; -function buildReviewPrompt(files: string, diff: string | null, compareTarget: CompareTarget): string { +function buildReviewPrompt( + files: string, + diff: string | null, + compareTarget: CompareTarget, +): string { const fileList = files .trim() .split('\n') @@ -261,19 +265,23 @@ export function registerCodeReviewTools(server: McpServerType, repoRoot: string) const filesArgs: string[] = staged ? ['diff', '--staged', '--name-only'] : ['diff', `${base}...HEAD`, '--name-only']; - const { stdout: rawFiles } = await execFileAsync('git', filesArgs, { + const { stdout: rawFiles } = (await execFileAsync('git', filesArgs, { cwd: repoRoot, maxBuffer: 1024 * 1024, - }) as ExecFileResult; + })) as ExecFileResult; // Filter out ignored files const files = filterFiles(rawFiles); if (!files.trim()) { // Fall back to unstaged changes if no branch diff - const { stdout: rawUnstagedFiles } = await execFileAsync('git', ['diff', '--name-only'], { - cwd: repoRoot, - }) as ExecFileResult; + const { stdout: rawUnstagedFiles } = (await execFileAsync( + 'git', + ['diff', '--name-only'], + { + cwd: repoRoot, + }, + )) as ExecFileResult; const unstagedFiles = filterFiles(rawUnstagedFiles); @@ -289,14 +297,14 @@ export function registerCodeReviewTools(server: McpServerType, repoRoot: string) } // Use unstaged diff instead (with pathspec to exclude ignored files) - const { stdout: diff } = await execFileAsync( + const { stdout: diff } = (await execFileAsync( 'git', ['diff', '--', ...unstagedFiles.trim().split('\n')], { cwd: repoRoot, maxBuffer: 1024 * 1024 * 5, }, - ) as ExecFileResult; + )) as ExecFileResult; return { content: [ @@ -326,10 +334,10 @@ export function registerCodeReviewTools(server: McpServerType, repoRoot: string) ['diff', '--staged', '--', ...fileList] : ['diff', `${base}...HEAD`, '--', ...fileList]; - const { stdout: diff } = await execFileAsync('git', diffArgs, { + const { stdout: diff } = (await execFileAsync('git', diffArgs, { cwd: repoRoot, maxBuffer: 1024 * 1024 * 5, // 5MB buffer for large diffs - }) as ExecFileResult; + })) as ExecFileResult; return { content: [ diff --git a/packages/mcp/src/tools/drizzle.ts b/packages/mcp/src/tools/drizzle.ts index 013c8e545..cb6ee325b 100644 --- a/packages/mcp/src/tools/drizzle.ts +++ b/packages/mcp/src/tools/drizzle.ts @@ -115,9 +115,7 @@ export function registerDrizzleTools(server: McpServerType): void { ) .optional(), }, - async ({ - path: docPath, - }): Promise<{ content: Array<{ type: 'text'; text: string }> }> => { + async ({ path: docPath }): Promise<{ content: Array<{ type: 'text'; text: string }> }> => { try { const { header, sections } = await fetchDrizzleDocs(); @@ -143,7 +141,10 @@ export function registerDrizzleTools(server: McpServerType): void { if (matches.length === 1) { return { content: [ - { type: 'text', text: `# Drizzle ORM: ${matches[0]}\n\n${sections.get(matches[0])!}` }, + { + type: 'text', + text: `# Drizzle ORM: ${matches[0]}\n\n${sections.get(matches[0])!}`, + }, ], }; } diff --git a/packages/mcp/src/tools/lint.ts b/packages/mcp/src/tools/lint.ts index 25f66a991..8f1f7211f 100644 --- a/packages/mcp/src/tools/lint.ts +++ b/packages/mcp/src/tools/lint.ts @@ -18,9 +18,7 @@ export function registerLintTools(server: McpServerType, repoRoot: string): void { fix: z.boolean().optional().default(false).describe('Whether to run lint with --fix'), }, - async ({ - fix = false, - }): Promise<{ content: Array<{ type: 'text'; text: string }> }> => { + async ({ fix = false }): Promise<{ content: Array<{ type: 'text'; text: string }> }> => { const command = `pnpm run lint${fix ? ' --fix' : ''}`; try { diff --git a/packages/ui/src/zag/Accordion.tsx b/packages/ui/src/zag/Accordion.tsx index 7f426a079..a34a48f26 100644 --- a/packages/ui/src/zag/Accordion.tsx +++ b/packages/ui/src/zag/Accordion.tsx @@ -36,7 +36,7 @@ export interface AccordionProps { /** * Accordion - Vertically stacked expandable sections */ -const AccordionComponent: Component = (props) => { +const AccordionComponent: Component = props => { const [local, machineProps] = splitProps(props, ['items', 'class']); const handleValueChange = (details: { value: string[] }) => { @@ -57,7 +57,7 @@ const AccordionComponent: Component = (props) => { class={`divide-y divide-gray-200 rounded-lg border border-gray-200 ${local.class || ''}`} > - {(item) => ( + {item => (

diff --git a/packages/ui/src/zag/Avatar.tsx b/packages/ui/src/zag/Avatar.tsx index f316a2d24..9982b8ae8 100644 --- a/packages/ui/src/zag/Avatar.tsx +++ b/packages/ui/src/zag/Avatar.tsx @@ -23,7 +23,7 @@ export interface AvatarProps { /** * Avatar - User avatar with fallback support */ -const AvatarComponent: Component = (props) => { +const AvatarComponent: Component = props => { const src = () => props.src; const name = () => props.name; const alt = () => props.alt || name() || 'Avatar'; diff --git a/packages/ui/src/zag/Checkbox.tsx b/packages/ui/src/zag/Checkbox.tsx index dc9212ca6..c4ebe73fc 100644 --- a/packages/ui/src/zag/Checkbox.tsx +++ b/packages/ui/src/zag/Checkbox.tsx @@ -34,7 +34,7 @@ export interface CheckboxProps { /** * Checkbox - Full description */ -const CheckboxComponent: Component = (props) => { +const CheckboxComponent: Component = props => { const merged = mergeProps( { defaultChecked: false, diff --git a/packages/ui/src/zag/Clipboard.tsx b/packages/ui/src/zag/Clipboard.tsx index 76966f232..9a4044203 100644 --- a/packages/ui/src/zag/Clipboard.tsx +++ b/packages/ui/src/zag/Clipboard.tsx @@ -49,7 +49,7 @@ export interface ClipboardProps { /** * Clipboard - Copy to clipboard functionality */ -const ClipboardComponent: Component = (props) => { +const ClipboardComponent: Component = props => { const [local, machineProps] = splitProps(props, ['label', 'showInput', 'children', 'class']); const [copied, setCopied] = createSignal(false); @@ -147,7 +147,7 @@ export interface CopyButtonProps { /** * CopyButton - Simple copy button without input field */ -export const CopyButton: Component = (props) => { +export const CopyButton: Component = props => { const [local, machineProps] = splitProps(props, [ 'label', 'copiedLabel', diff --git a/packages/ui/src/zag/Collapsible.tsx b/packages/ui/src/zag/Collapsible.tsx index af177d528..1b044d621 100644 --- a/packages/ui/src/zag/Collapsible.tsx +++ b/packages/ui/src/zag/Collapsible.tsx @@ -114,7 +114,7 @@ function CollapsibleWithApi(props: { /** * High-level Collapsible component (convenience API) */ -const CollapsibleComponent: Component = (props) => { +const CollapsibleComponent: Component = props => { // Split convenience props from Ark UI props const [local, arkProps] = splitProps(props, ['trigger', 'indicator', 'children']); diff --git a/packages/ui/src/zag/Combobox.tsx b/packages/ui/src/zag/Combobox.tsx index 677131d0b..2ccca570a 100644 --- a/packages/ui/src/zag/Combobox.tsx +++ b/packages/ui/src/zag/Combobox.tsx @@ -59,7 +59,7 @@ export interface ComboboxProps { /** * Combobox - Searchable select with autocomplete */ -const ComboboxComponent: Component = (props) => { +const ComboboxComponent: Component = props => { const merged = mergeProps( { openOnClick: true, @@ -108,7 +108,7 @@ const ComboboxComponent: Component = (props) => { if (machineProps.onValueChange) { // Find the items that match the selected values const selectedItems = details.value - .map((val) => getItems().find((item) => item.value === val)) + .map(val => getItems().find(item => item.value === val)) .filter((item): item is ComboboxItem => item !== undefined); machineProps.onValueChange({ value: details.value, items: selectedItems }); } @@ -128,7 +128,7 @@ const ComboboxComponent: Component = (props) => { > - {(item) => ( + {item => ( = (props) => { +const DialogComponent: Component = props => { const open = () => props.open; const size = () => props.size; const title = () => props.title; @@ -115,7 +115,7 @@ export interface ConfirmDialogProps { /** * ConfirmDialog - A reusable confirmation dialog component */ -export const ConfirmDialogComponent: Component = (props) => { +export const ConfirmDialogComponent: Component = props => { const open = () => props.open; const loading = () => props.loading; const variant = () => props.variant || 'danger'; @@ -254,7 +254,7 @@ export function useConfirmDialog() { let resolvePromise: ((value: boolean) => void) | null = null; const open = (dialogConfig: ConfirmDialogConfig) => { - return new Promise((resolve) => { + return new Promise(resolve => { resolvePromise = resolve; setConfig({ title: dialogConfig.title || 'Confirm', diff --git a/packages/ui/src/zag/Drawer.tsx b/packages/ui/src/zag/Drawer.tsx index fbd5b11c3..d94595999 100644 --- a/packages/ui/src/zag/Drawer.tsx +++ b/packages/ui/src/zag/Drawer.tsx @@ -34,7 +34,7 @@ export interface DrawerProps { /** * Drawer - A slide-in panel component composed from Dialog */ -const DrawerComponent: Component = (props) => { +const DrawerComponent: Component = props => { const open = () => props.open; const side = () => props.side || 'right'; const size = () => props.size || 'md'; @@ -107,7 +107,9 @@ const DrawerComponent: Component = (props) => {
- {title()} + + {title()} + diff --git a/packages/ui/src/zag/FloatingPanel.tsx b/packages/ui/src/zag/FloatingPanel.tsx index 9506ea11f..10f5c82e5 100644 --- a/packages/ui/src/zag/FloatingPanel.tsx +++ b/packages/ui/src/zag/FloatingPanel.tsx @@ -73,7 +73,7 @@ export interface FloatingPanelProps { /** * FloatingPanel - A draggable and resizable floating panel component */ -const FloatingPanelComponent: Component = (props) => { +const FloatingPanelComponent: Component = props => { const showControls = () => props.showControls ?? true; const showMinimize = () => showControls() && (props.showMinimize ?? true); const showMaximize = () => showControls() && (props.showMaximize ?? true); @@ -132,7 +132,7 @@ const FloatingPanelComponent: Component = (props) => { persistRect={props.persistRect ?? false} > - {(api) => ( + {api => ( diff --git a/packages/ui/src/zag/Menu.tsx b/packages/ui/src/zag/Menu.tsx index 22e80c1fc..7c41ccfb7 100644 --- a/packages/ui/src/zag/Menu.tsx +++ b/packages/ui/src/zag/Menu.tsx @@ -59,7 +59,7 @@ export interface MenuProps { /** * Menu - Dropdown menu for actions */ -const MenuComponent: Component = (props) => { +const MenuComponent: Component = props => { const [local, machineProps] = splitProps(props, [ 'trigger', 'items', @@ -91,7 +91,7 @@ const MenuComponent: Component = (props) => { class={`${Z_INDEX.MENU} min-w-40 rounded-lg border border-gray-200 bg-white py-1 shadow-lg focus:outline-none ${local.class || ''}`} > - {(item) => ( + {item => ( = (props) => { +const NumberInputComponent: Component = props => { const [local, machineProps] = splitProps(props, [ 'label', 'placeholder', diff --git a/packages/ui/src/zag/PasswordInput.tsx b/packages/ui/src/zag/PasswordInput.tsx index 282756418..27ccd3a4a 100644 --- a/packages/ui/src/zag/PasswordInput.tsx +++ b/packages/ui/src/zag/PasswordInput.tsx @@ -28,7 +28,7 @@ export interface PasswordInputProps { /** * PasswordInput - Password input with visibility toggle */ -const PasswordInputComponent: Component = (props) => { +const PasswordInputComponent: Component = props => { const autoComplete = () => props.autoComplete || 'new-password'; const password = () => props.password || ''; const required = () => props.required || false; diff --git a/packages/ui/src/zag/PinInput.tsx b/packages/ui/src/zag/PinInput.tsx index e678453d1..a7e6169d4 100644 --- a/packages/ui/src/zag/PinInput.tsx +++ b/packages/ui/src/zag/PinInput.tsx @@ -27,7 +27,7 @@ export interface PinInputProps { /** * PinInput - OTP/PIN code input */ -const PinInputComponent: Component = (props) => { +const PinInputComponent: Component = props => { const [local, machineProps] = splitProps(props, [ 'count', 'isError', @@ -82,7 +82,7 @@ const PinInputComponent: Component = (props) => { - {(item) => } + {item => } diff --git a/packages/ui/src/zag/Popover.tsx b/packages/ui/src/zag/Popover.tsx index 23b5a4cd1..a8f1c115d 100644 --- a/packages/ui/src/zag/Popover.tsx +++ b/packages/ui/src/zag/Popover.tsx @@ -59,7 +59,7 @@ export interface PopoverProps { /** * Popover - Non-modal floating dialog */ -const PopoverComponent: Component = (props) => { +const PopoverComponent: Component = props => { const merged = mergeProps( { placement: 'bottom', @@ -124,7 +124,9 @@ const PopoverComponent: Component = (props) => {
- {title()} + + {title()} + diff --git a/packages/ui/src/zag/Progress.tsx b/packages/ui/src/zag/Progress.tsx index 81b4e5d1b..bd787617d 100644 --- a/packages/ui/src/zag/Progress.tsx +++ b/packages/ui/src/zag/Progress.tsx @@ -29,7 +29,7 @@ export interface ProgressProps { /** * Progress - Linear progress bar */ -const ProgressComponent: Component = (props) => { +const ProgressComponent: Component = props => { const [local, machineProps] = splitProps(props, [ 'label', 'showValue', diff --git a/packages/ui/src/zag/QRCode.tsx b/packages/ui/src/zag/QRCode.tsx index e117bfac3..dd23630c3 100644 --- a/packages/ui/src/zag/QRCode.tsx +++ b/packages/ui/src/zag/QRCode.tsx @@ -22,7 +22,7 @@ export interface QRCodeProps { * QR Code component * Renders an SVG-based QR code with customizable error correction and styling. */ -const QRCodeComponent: Component = (props) => { +const QRCodeComponent: Component = props => { const data = () => props.data; const ecc = () => props.ecc; const size = () => props.size; diff --git a/packages/ui/src/zag/RadioGroup.tsx b/packages/ui/src/zag/RadioGroup.tsx index f219700ae..070dcc10e 100644 --- a/packages/ui/src/zag/RadioGroup.tsx +++ b/packages/ui/src/zag/RadioGroup.tsx @@ -36,7 +36,7 @@ export interface RadioGroupProps { /** * RadioGroup - Radio button group for single selection */ -const RadioGroupComponent: Component = (props) => { +const RadioGroupComponent: Component = props => { const [local, machineProps] = splitProps(props, ['items', 'label', 'class']); const orientation = () => machineProps.orientation || 'vertical'; @@ -63,7 +63,7 @@ const RadioGroupComponent: Component = (props) => {
- {(item) => ( + {item => ( = (props) => { +const SelectComponent: Component = props => { // Split convenience props from Ark UI props const [local, arkProps] = splitProps(props, [ 'items', @@ -99,7 +99,7 @@ const SelectComponent: Component = (props) => { // Create collection from items with disabled handling const collection = createMemo(() => { - const collectionItems = items().map((item) => ({ + const collectionItems = items().map(item => ({ ...item, disabled: item.disabled || disabledValues().includes(item.value), })); @@ -116,7 +116,11 @@ const SelectComponent: Component = (props) => { const selectValue = createMemo(() => { const v = value(); if (merged.multiple) { - return Array.isArray(v) ? v : v != null ? [v] : []; + return ( + Array.isArray(v) ? v + : v != null ? [v] + : [] + ); } // For single select: return empty array if value is null/undefined, otherwise [value] // Empty string is valid (for "Unassigned" option) @@ -133,7 +137,7 @@ const SelectComponent: Component = (props) => { const newValue = details.value[0] || ''; // Prevent selecting disabled values const currentCollection = collection(); - const item = currentCollection.items.find((i) => i.value === newValue); + const item = currentCollection.items.find(i => i.value === newValue); if (item?.disabled || disabledValues().includes(newValue)) { return; } @@ -144,7 +148,7 @@ const SelectComponent: Component = (props) => { // Helper to check if a value is disabled const isValueDisabled = (val: string) => { const currentCollection = collection(); - const item = currentCollection.items.find((i) => i.value === val); + const item = currentCollection.items.find(i => i.value === val); const disabledSet = new Set(disabledValues()); return item?.disabled || disabledSet.has(val); }; @@ -197,7 +201,7 @@ const SelectComponent: Component = (props) => { > - {(item) => { + {item => { const isDisabled = () => isValueDisabled(item().value); return ( = (props) => { > - {(item) => { + {item => { const isDisabled = () => isValueDisabled(item().value); return ( = (props) => { +const SplitterComponent: Component = props => { return ( = (props) => { +const SwitchComponent: Component = props => { const merged = mergeProps( { defaultChecked: false, diff --git a/packages/ui/src/zag/Tabs.tsx b/packages/ui/src/zag/Tabs.tsx index de23a4f0b..606b429a6 100644 --- a/packages/ui/src/zag/Tabs.tsx +++ b/packages/ui/src/zag/Tabs.tsx @@ -29,7 +29,7 @@ export interface TabsProps { /** * Reusable Tabs component */ -const TabsComponent: Component = (props) => { +const TabsComponent: Component = props => { const value = () => props.value; const defaultValue = () => props.defaultValue; const tabsList = () => props.tabs; @@ -49,7 +49,7 @@ const TabsComponent: Component = (props) => { > - {(tab) => ( + {tab => ( = (props) => { - {(tab) => ( + {tab => ( = (props) => { +const TagsInputComponent: Component = props => { const [local, machineProps] = splitProps(props, [ 'label', 'placeholder', @@ -86,7 +86,7 @@ const TagsInputComponent: Component = (props) => { class={`w-full ${local.class || ''}`} > - {(api) => ( + {api => ( <> {local.label && ( diff --git a/packages/ui/src/zag/Toast.tsx b/packages/ui/src/zag/Toast.tsx index 3e8a8845e..86e22187b 100644 --- a/packages/ui/src/zag/Toast.tsx +++ b/packages/ui/src/zag/Toast.tsx @@ -64,7 +64,7 @@ const ToasterComponent: Component = () => { toaster={toaster} class={`pointer-events-none fixed inset-0 ${Z_INDEX.TOAST} flex flex-col items-end p-4 sm:p-6`} > - {(toast) => ( + {toast => ( @@ -73,7 +73,9 @@ const ToasterComponent: Component = () => {
{getIcon(toast().type)}
- {toast().title} + + {toast().title} + diff --git a/packages/ui/src/zag/ToggleGroup.tsx b/packages/ui/src/zag/ToggleGroup.tsx index 2a27cb122..029293261 100644 --- a/packages/ui/src/zag/ToggleGroup.tsx +++ b/packages/ui/src/zag/ToggleGroup.tsx @@ -41,7 +41,7 @@ export interface ToggleGroupProps { /** * ToggleGroup - Group of toggle buttons */ -const ToggleGroupComponent: Component = (props) => { +const ToggleGroupComponent: Component = props => { const [local, machineProps] = splitProps(props, ['items', 'size', 'class']); const getSizeClass = () => { diff --git a/packages/ui/src/zag/Tooltip.tsx b/packages/ui/src/zag/Tooltip.tsx index 2266df462..cec490d80 100644 --- a/packages/ui/src/zag/Tooltip.tsx +++ b/packages/ui/src/zag/Tooltip.tsx @@ -76,7 +76,7 @@ export interface TooltipProps { /** * Tooltip - High-level convenience component */ -const TooltipComponent: Component = (props) => { +const TooltipComponent: Component = props => { const merged = mergeProps( { placement: 'top', diff --git a/packages/ui/src/zag/Tour.tsx b/packages/ui/src/zag/Tour.tsx index f7673c610..57fb97044 100644 --- a/packages/ui/src/zag/Tour.tsx +++ b/packages/ui/src/zag/Tour.tsx @@ -61,11 +61,7 @@ export interface TourStep { /** Show arrow for tooltips (default: true) */ arrow?: boolean; /** Side effect before showing step */ - effect?: (ctx: { - next: () => void; - show: () => void; - update: () => void; - }) => void | (() => void); + effect?: (ctx: { next: () => void; show: () => void; update: () => void }) => void | (() => void); } export interface TourApi { @@ -147,7 +143,7 @@ export interface TourProviderProps { /** * TourProvider - Provides tour functionality to children */ -export const TourProvider: Component = (props) => { +export const TourProvider: Component = props => { const [local, machineProps] = splitProps(props, ['children']); const service = useMachine(tour.machine, () => ({ @@ -218,7 +214,7 @@ export const TourProvider: Component = (props) => {
- {(action) => ( + {action => (