diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e5ad9cb0..ceb91338 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,9 +4,11 @@ on: pull_request: branches: - main + # - solid2 # enable later push: branches: - main + # - solid2 # enable later jobs: build: @@ -20,10 +22,10 @@ jobs: - uses: pnpm/action-setup@v4 - - name: Setup Node.js 21.6.1 - uses: actions/setup-node@v4 + - name: Setup Node.js + uses: actions/setup-node@v6 with: - node-version: 21.6.1 + node-version: "24" cache: pnpm - name: Install dependencies diff --git a/.github/workflows/format-files-prettier.yml b/.github/workflows/format-files-prettier.yml index 804f4efa..80d81461 100644 --- a/.github/workflows/format-files-prettier.yml +++ b/.github/workflows/format-files-prettier.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - main + - solid2 paths: - "**.md" - "**.mdx" @@ -15,6 +16,7 @@ on: push: branches: - main + - solid2 jobs: prettier: diff --git a/.github/workflows/format-files.yml b/.github/workflows/format-files.yml index b2d1d923..8b20880c 100644 --- a/.github/workflows/format-files.yml +++ b/.github/workflows/format-files.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - main + - solid2 paths: - "**.js" - "**.jsx" @@ -14,6 +15,7 @@ on: push: branches: - main + - solid2 jobs: biome: diff --git a/.github/workflows/jsr-ci.yml b/.github/workflows/jsr-ci.yml new file mode 100644 index 00000000..3bf71bcd --- /dev/null +++ b/.github/workflows/jsr-ci.yml @@ -0,0 +1,47 @@ +name: JSR validation + +on: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + jsr: + name: Validate JSR workspace + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: pnpm + + - name: Set up Deno + uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Install dependencies + run: pnpm install + + - name: Check deno.jsonc/package.json version and exports sync + run: pnpm jsr:sync:check + + - name: Build libraries + run: pnpm build:libs + env: + NODE_OPTIONS: "--max_old_space_size=4096" + + - name: Publish dry run with full type checks + run: deno publish --dry-run --allow-slow-types diff --git a/.github/workflows/jsr-publish.yml b/.github/workflows/jsr-publish.yml new file mode 100644 index 00000000..19899dbf --- /dev/null +++ b/.github/workflows/jsr-publish.yml @@ -0,0 +1,57 @@ +name: Publish JSR + +on: + workflow_dispatch: + inputs: + publish: + description: Run the real JSR publish after the dry run passes + required: true + default: "false" + type: choice + options: + - "false" + - "true" + +permissions: + contents: read + id-token: write + +jobs: + publish: + name: Publish JSR workspace + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: pnpm + + - name: Set up Deno + uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Install dependencies + run: pnpm install + + - name: Sync deno.jsonc versions and exports from package.json + run: pnpm jsr:sync + + - name: Build libraries + run: pnpm build:libs + env: + NODE_OPTIONS: "--max_old_space_size=4096" + + - name: Publish dry run + run: deno publish --dry-run --allow-slow-types + + - name: Publish to JSR + if: ${{ inputs.publish == 'true' }} + run: deno publish --allow-slow-types diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 004b912e..7d864531 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,7 @@ on: [workflow_dispatch] permissions: id-token: write - contents: read + contents: write concurrency: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 21dd48e0..c290df3a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,9 +4,11 @@ on: pull_request: branches: - main + - solid2 push: branches: - main + - solid2 jobs: build: @@ -20,10 +22,10 @@ jobs: - uses: pnpm/action-setup@v4 - - name: Setup Node.js 21.6.1 - uses: actions/setup-node@v4 + - name: Setup Node.js + uses: actions/setup-node@v6 with: - node-version: 21.6.1 + node-version: "24" cache: pnpm - name: Install dependencies diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index b4878ca3..db71b8ed 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -16,10 +16,10 @@ jobs: - uses: pnpm/action-setup@v4 - - name: Setup Node.js 21.6.1 - uses: actions/setup-node@v4 + - name: Setup Node.js + uses: actions/setup-node@v6 with: - node-version: 21.6.1 + node-version: "24" cache: pnpm - name: Install Dependencies diff --git a/.gitignore b/.gitignore index 34a09ff9..b18d7a7d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log +.claude # local env files .env.local @@ -17,9 +18,6 @@ lerna-debug.log .env.test.local .env.production.local -# turbo -.turbo - # solid .solid .vinxi @@ -43,3 +41,4 @@ packages/core/NOTICE.txt .DS_Store Thumbs.db .vercel +/storybook-static diff --git a/.storybook/globals.css b/.storybook/globals.css new file mode 100644 index 00000000..426571b7 --- /dev/null +++ b/.storybook/globals.css @@ -0,0 +1,2 @@ +@source "./.storybook/**/*.{ts,tsx}"; +@source "./packages/core/src/**/*.{ts,tsx}"; diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 00000000..3f667384 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,88 @@ +import type { StorybookConfig } from "storybook-solidjs-vite"; +import { mergeConfig } from "vite"; + +const config: StorybookConfig = { + stories: ["../packages/core/src/**/stories/*.stories.{ts,tsx}"], + staticDirs: ["./public"], + addons: ["@storybook/addon-docs"], + framework: { + name: "storybook-solidjs-vite", + // docgen: false — storybook-solidjs-vite@10.5.2 bug: dynamic import('typescript') + // returns { default: ts } in ESM context so ts.sys is undefined inside the plugin + options: { docgen: false }, + }, + docs: {}, + async viteFinal(config) { + return mergeConfig(config, { + plugins: [ + { + name: "solid-addEventListener-compat", + transform(code: string, id: string) { + if ( + /\.(tsx?|jsx?)$/.test(id) && + code.includes("addEventListener as _$addEventListener") + ) { + return { + code: code.replace( + /\{ addEventListener as _\$addEventListener \}/g, + "{ addEvent as _$addEventListener }", + ), + map: null, + }; + } + }, + }, + // Intercept any file that imports the removed `on` helper from solid-js. + // Logs the filename so we can find the culprit, and replaces the import + // with a shim so rendering continues. + { + name: "solid-on-shim", + transform(code: string, id: string) { + if ( + code.includes("from 'solid-js'") || + code.includes('from "solid-js"') + ) { + if (/\bon\b/.test(code)) { + const hasOnImport = + /import\s*\{[^}]*\bon\b[^}]*\}\s*from\s*['"]solid-js['"]/.test( + code, + ); + if (hasOnImport) { + console.warn( + `[solid-on-shim] Found legacy 'on' import in: ${id}`, + ); + // Remove 'on' from the import and inject a no-op shim after the import + const patched = code + .replace(/,\s*\bon\b/g, "") + .replace(/\bon\b\s*,/g, "") + .replace(/\{\s*\bon\b\s*\}/g, "{ createEffect }"); + return { + code: + patched + + "\nconst on = (deps, fn) => { createEffect(deps, fn); };\n", + map: null, + }; + } + } + } + }, + }, + ], + resolve: { + alias: [ + { find: "solid-js/web", replacement: "@solidjs/web" }, + { + find: "@kobalte/utils", + replacement: new URL( + "../packages/utils/src/index.ts", + import.meta.url, + ).pathname, + }, + ], + dedupe: ["react", "react-dom", "solid-js", "@solidjs/signals", "@solidjs/web"], + }, + }); + }, +}; + +export default config; diff --git a/.storybook/manager.ts b/.storybook/manager.ts new file mode 100644 index 00000000..468395a8 --- /dev/null +++ b/.storybook/manager.ts @@ -0,0 +1,12 @@ +import { addons } from "storybook/manager-api"; +import { create } from "storybook/theming/create"; + +addons.setConfig({ + theme: create({ + base: "light", + brandTitle: "Kobalte", + brandImage: "/kobalte-logo.svg", + brandUrl: "https://kobalte.dev", + brandTarget: "_blank", + }), +}); diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 00000000..d08dbb0e --- /dev/null +++ b/.storybook/preview.ts @@ -0,0 +1,8 @@ +import * as docsAnnotations from "@storybook/addon-docs/preview"; +import { definePreview } from "storybook-solidjs-vite/next"; +import "./globals.css"; + +export default definePreview({ + addons: [docsAnnotations], + parameters: { layout: "centered", docs: { toc: true } }, +}); diff --git a/.storybook/public/kobalte-logo.svg b/.storybook/public/kobalte-logo.svg new file mode 100644 index 00000000..3cfa8037 --- /dev/null +++ b/.storybook/public/kobalte-logo.svg @@ -0,0 +1,9 @@ + + Kobalte. + diff --git a/.storybook/tsconfig.json b/.storybook/tsconfig.json new file mode 100644 index 00000000..e7ea5eb1 --- /dev/null +++ b/.storybook/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "jsxImportSource": "@solidjs/web", + "customConditions": ["@solid-primitives/source"], + "verbatimModuleSyntax": false, + "types": ["vite/client"] + }, + "include": ["./**/*", "../packages/core/src/*/stories/**/*"] +} diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 00000000..39aad710 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,102 @@ +# Solid 2.0 Migration Guide + +## Import paths + +`JSX`, `ValidComponent`, and `ComponentProps` moved out of `solid-js` into `@solidjs/web`. + +```ts +// before +import type { JSX, ValidComponent, ComponentProps } from "solid-js"; + +// after +import type { JSX, ValidComponent, ComponentProps } from "@solidjs/web"; +``` + +`solid-js/web` (the DOM renderer) is now `@solidjs/web`. + +```ts +import { render } from "@solidjs/web"; +``` + +## HTML attributes are lowercase + +Solid 2.0 uses lowercase HTML attribute names on native elements, matching the HTML spec rather than React conventions. + +```tsx +// before +
+ + +// after +
+ +``` + +This affects any `RenderProps` interface whose props are spread onto native elements via `Polymorphic`. Both the interface definition and the JSX must use the lowercase name. + +Other affected attributes: `autocomplete`, `autocorrect`, `spellcheck`, `enterkeyhint`, `contenteditable`, `inputmode`. + +## Renamed helpers + +| Solid 1.x | Solid 2.0 | +|-----------|-----------| +| `mergeProps` | `merge` | +| `splitProps` | `omit` | +| `unwrap` | `snapshot` | +| `onMount` | `onSettled` | +| `createComputed` | `createEffect` (split form) / `createMemo` | +| `Index` | `` | +| `classList` | `class` (object/array accepted) | +| `batch` | removed — writes are auto-batched | + +## Effects: split compute/apply form + +`createEffect` now takes two functions. Reactive reads belong in the first (compute), side effects in the second (apply). + +```ts +// before +createEffect(on(source, (value) => { el.title = value; })); + +// after +createEffect( + () => source(), + (value) => { el.title = value; }, +); +``` + +Cleanup is returned from the apply function instead of using `onCleanup`. + +```ts +createEffect( + () => source(), + (value) => { + const id = setTimeout(() => doWork(value), 100); + return () => clearTimeout(id); + }, +); +``` + +## Context: no more `.Provider` + +```tsx +// before + + +// after + +``` + +## Store setters are draft-first + +```ts +// before +import { produce } from "solid-js/store"; +setStore(produce(s => { s.count++; })); + +// after +setStore(s => { s.count++; }); +``` + +## Signals: no writes inside reactive scope + +Writing to a signal inside a component body, memo, or effect throws `SIGNAL_WRITE_IN_OWNED_SCOPE` in dev. Derive values with `createMemo` instead, or write in event handlers. diff --git a/apps/docs/app.config.ts b/apps/docs/app.config.ts deleted file mode 100644 index c586bf91..00000000 --- a/apps/docs/app.config.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { nodeTypes } from "@mdx-js/mdx"; -import { defineConfig } from "@solidjs/start/config"; -// @ts-ignore missing types -import pkg from "@vinxi/plugin-mdx"; -import { type Options as AcornOptions, parse } from "acorn"; -// @ts-ignore -import Slugger from "github-slugger"; -import rehypePrettyCode from "rehype-pretty-code"; -import rehypeRaw from "rehype-raw"; -import rehypeSlug from "rehype-slug"; -import remarkGfm from "remark-gfm"; -import remarkShikiTwoslash from "remark-shiki-twoslash"; -import { visit } from "unist-util-visit"; - -const { default: vinxiMdx } = pkg; - -function jsToTreeNode(jsString: string, acornOpts?: AcornOptions) { - return { - type: "mdxjsEsm", - value: "", - data: { - estree: { - body: [], - ...parse( - jsString, - acornOpts ?? { - sourceType: "module", - ecmaVersion: 2020, - }, - ), - type: "Program", - sourceType: "module", - }, - }, - }; -} - -const headingsCache = new Map(); - -function rehypeCollectHeadings() { - const slugger = new Slugger(); - return (tree: any, file: any) => { - const headings: any[] = []; - visit(tree, (node) => { - if (node.type !== "element") { - return; - } - - const { tagName } = node; - - if (tagName[0] !== "h") { - return; - } - - const [_, level] = tagName.match(/h([0-6])/) ?? []; - - if (!level) { - return; - } - - const depth = Number.parseInt(level); - - let text = ""; - - visit(node, (child, __, parent) => { - if (child.type === "element" || parent == null) { - return; - } - - if (child.type === "raw" && child.value.match(/^\n?<.*>\n?$/)) { - return; - } - - if (new Set(["text", "raw", "mdxTextExpression"]).has(child.type)) { - text += child.value; - } - }); - - node.properties = node.properties || {}; - - if (typeof node.properties.id !== "string") { - let slug = slugger.slug(text); - - if (slug.endsWith("-")) { - slug = slug.slice(0, -1); - } - - node.properties.id = slug; - } - - headings.push({ depth, slug: node.properties.id, text }); - }); - - headingsCache.set(file.path, headings); - - tree.children.unshift( - // @ts-ignore - jsToTreeNode( - `export function getHeadings() { return ${JSON.stringify(headings)} }`, - ), - ); - }; -} - -export default defineConfig({ - server: { - preset: - process.env.GITHUB_ACTIONS || process.env.DEVELOPMENT - ? "node-server" - : "netlify", - experimental: { - asyncContext: true, - }, - prerender: { - routes: ["/docs/core/overview/introduction"], - crawlLinks: true, - }, - }, - - extensions: ["mdx", "md"], - // @ts-ignore: type should be optional, bugged in @solidjs/start@0.6.1 - solid: { - extensions: ["mdx", "md"], - }, - vite() { - return { - plugins: [ - vinxiMdx.withImports({})({ - jsx: true, - jsxImportSource: "solid-js", - providerImportSource: "solid-mdx", - rehypePlugins: [ - rehypePrettyCode, - rehypeSlug, - [rehypeRaw, { passThrough: nodeTypes }], - // rehypeCollectHeadings, - ], - remarkPlugins: [ - remarkGfm, - [ - // @ts-ignore - remarkShikiTwoslash.default, - { - disableImplicitReactImport: true, - includeJSDocInHover: true, - themes: ["github-light", "github-dark"], - defaultOptions: { - lib: ["dom", "es2015"], - }, - defaultCompilerOptions: { - allowSyntheticDefaultImports: true, - esModuleInterop: true, - target: "ESNext", - module: "ESNext", - lib: ["dom", "es2015"], - jsxImportSource: "solid-js", - jsx: "preserve", - types: ["vite/client"], - paths: { - "~/*": ["./src/*"], - }, - }, - }, - ], - ], - }), - // { - // name: "mdx-meta", - // async transform(code: any, id: any) { - // if (id.endsWith(".mdx?meta") || id.endsWith(".md?meta")) { - // const replacedId = id.replace(/\?meta$/, ""); - // - // if (headingsCache.has(replacedId)) { - // return { - // code: ` - // export function getHeadings() { - // return ${JSON.stringify(headingsCache.get(id), null, 2)} - // } - // `, - // }; - // } - // } - // }, - // }, - ], - ssr: { - noExternal: ["@tanstack/solid-virtual"], - }, - }; - }, -}); diff --git a/apps/docs/package.json b/apps/docs/package.json index bad749e2..f9798268 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -22,53 +22,35 @@ "url": "git+https://github.com/kobaltedev/kobalte.git" }, "license": "MIT", - "author": "Fabien Marie-Louise ", + "author": "jer3m01 ", + "contributors": [ + "Fabien Marie-Louise " + ], "type": "module", "scripts": { - "build": "NODE_OPTIONS=\"--max-old-space-size=8192\" vinxi build", - "build:dev": "NODE_OPTIONS=\"--max-old-space-size=8192\" DEVELOPMENT=1 vinxi build", - "clean": "rm -rf .solid && rm -rf netlify && rm -rf .turbo && rm -rf node_modules && rm -rf dist", - "dev": "vinxi dev --host", - "start": "vinxi start" + "build": "vite build", + "build:dev": "DEVELOPMENT=1 vite build", + "clean": "rm -rf .solid && rm -rf netlify && rm -rf node_modules && rm -rf dist", + "dev": "vite dev" }, "dependencies": { - "@docsearch/css": "3.5.2", - "@docsearch/js": "3.5.2", - "@internationalized/date": "3.4.0", + "@internationalized/date": "catalog:", "@kobalte/core": "workspace:*", - "@solidjs/meta": "0.29.3", - "@solidjs/router": "0.12.4", - "@solidjs/start": "0.6.1", + "@kobalte/solidbase": "0.6.11", + "@solidjs/meta": "0.29.4", + "@solidjs/router": "0.16.2", + "@solidjs/start": "2.0.0-beta.0", + "@solidjs/web": "catalog:", "@tanstack/solid-virtual": "3.0.0-beta.6", - "clsx": "2.0.0", "minisearch": "7.1.0", - "solid-js": "1.8.15", - "undici": "5.23.0", - "vinxi": "0.3.9" + "nitro": "3.0.260610-beta", + "solid-js": "catalog:", + "vite": "8.1.3" }, "devDependencies": { - "@kobalte/tailwindcss": "0.9.0", - "@mdx-js/mdx": "3.0.0", - "@mdx-js/rollup": "3.0.0", - "@tailwindcss/typography": "0.5.9", - "@vinxi/plugin-mdx": "3.7.1", - "acorn": "8.10.0", - "autoprefixer": "10.4.15", - "github-slugger": "2.0.0", - "postcss": "8.4.28", - "rehype-pretty-code": "0.12.3", - "rehype-raw": "7.0.0", - "rehype-slug": "6.0.0", - "remark-gfm": "4.0.0", - "remark-shiki-twoslash": "3.1.3", - "shiki": "0.14.7", - "solid-mdx": "0.0.7", - "tailwindcss": "3.3.3", - "typescript": "4.9.5", - "unist-util-visit": "5.0.0", - "vite": "5.1.4" + "@iconify-json/ri": "^1.2.10" }, "engines": { - "node": ">=18" + "node": "^22.18.0 || >=24.11.0" } } diff --git a/apps/docs/postcss.config.cjs b/apps/docs/postcss.config.cjs deleted file mode 100644 index e873f1a4..00000000 --- a/apps/docs/postcss.config.cjs +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/apps/docs/public/kobalte.svg b/apps/docs/public/kobalte.svg new file mode 100644 index 00000000..8582cc45 --- /dev/null +++ b/apps/docs/public/kobalte.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/docs/src/app.tsx b/apps/docs/src/app.tsx index eea0db26..07638d07 100644 --- a/apps/docs/src/app.tsx +++ b/apps/docs/src/app.tsx @@ -1,77 +1,28 @@ -// @refresh reload -import "@docsearch/css"; -import "./root.css"; - -import { MetaProvider, Title } from "@solidjs/meta"; import { Router } from "@solidjs/router"; -import { FileRoutes } from "@solidjs/start"; +import { FileRoutes } from "@solidjs/start/router"; import { Suspense } from "solid-js"; -import { Portal, isServer } from "solid-js/web"; -import { MDXProvider } from "solid-mdx"; +import { Portal } from "solid-js/web"; -import { - ColorModeProvider, - ColorModeScript, - cookieStorageManagerSSR, -} from "@kobalte/core/color-mode"; import { Toast } from "@kobalte/core/toast"; -import { getCookie } from "vinxi/server"; +import { SolidBaseRoot } from "@kobalte/solidbase/client"; import toastStyles from "./examples/toast.module.css"; -import { mdxComponents } from "./mdx-components"; - -//export const mods = /*#__PURE__*/ import.meta.glob< -// true, -// string, -// { -// getHeadings: () => { -// depth: number; -// text: string; -// slug: string; -// }[]; -// } -//>("./routes/docs/**/*.{md,mdx}", { -// eager: true, -// query: { -// meta: "", -// }, -//}); - -function getServerCookies() { - "use server"; - - const colorMode = getCookie("kb-color-mode"); - - return colorMode ? `kb-color-mode=${colorMode}` : ""; -} export default function App() { - const storageManager = cookieStorageManagerSSR( - isServer ? getServerCookies() : document.cookie, - ); - return ( ( - - Kobalte - - - - {props.children} - - - - - - - - - - - - + + {props.children} + + + + + + + + + + )} > diff --git a/apps/docs/src/components/callout.tsx b/apps/docs/src/components/callout.tsx deleted file mode 100644 index 9a732fad..00000000 --- a/apps/docs/src/components/callout.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { clsx } from "clsx"; -import { type ComponentProps, splitProps } from "solid-js"; - -interface CalloutProps extends ComponentProps<"div"> { - type: "note" | "warning"; -} - -export function Callout(props: CalloutProps) { - const [local, others] = splitProps(props, ["type", "class"]); - - return ( -
- ); -} diff --git a/apps/docs/src/components/footer.tsx b/apps/docs/src/components/footer.tsx deleted file mode 100644 index 68a71965..00000000 --- a/apps/docs/src/components/footer.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { - DiscordIcon, - GitHubIcon, - OpenCollectiveIcon, - TwitterIcon, -} from "./icons"; - -export function Footer() { - return ( - - ); -} diff --git a/apps/docs/src/components/header.tsx b/apps/docs/src/components/header.tsx deleted file mode 100644 index 9b9e7a2e..00000000 --- a/apps/docs/src/components/header.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import docsearch from "@docsearch/js"; -import { useMatch } from "@solidjs/router"; -import { clsx } from "clsx"; -import { onMount } from "solid-js"; - -import { - LATEST_CORE_CHANGELOG_URL, - LATEST_CORE_VERSION_NAME, -} from "../VERSIONS"; -import type { NavSection } from "../model/navigation"; -import { GitHubIcon } from "./icons"; -import { MobileNavigation } from "./mobile-navigation"; -import { ThemeSelector } from "./theme-selector"; - -interface HeaderProps { - navSections: NavSection[]; -} - -export function Header(props: HeaderProps) { - const isChangelogPath = useMatch(() => "/docs/changelog/*"); - - onMount(() => { - docsearch({ - appId: "H7ZQSI0SAN", - apiKey: "c9354456dd4bb74c37e4d2b762b89b88", - indexName: "kobalte", - container: "#docsearch", - }); - }); - - return ( -
-
- -
-
- - Kobalte - . - - - {LATEST_CORE_VERSION_NAME} - -
- -
- ); -} diff --git a/apps/docs/src/components/icons.tsx b/apps/docs/src/components/icons.tsx index 73ad954a..b7d71b9b 100644 --- a/apps/docs/src/components/icons.tsx +++ b/apps/docs/src/components/icons.tsx @@ -1,115 +1,5 @@ import type { ComponentProps } from "solid-js"; -export function GitHubIcon(props: ComponentProps<"svg">) { - return ( - - GitHub - - - ); -} - -export function DiscordIcon(props: ComponentProps<"svg">) { - return ( - - Discord - - - ); -} - -export function OpenCollectiveIcon(props: ComponentProps<"svg">) { - return ( - - OpenCollective - - - ); -} - -export function TwitterIcon(props: ComponentProps<"svg">) { - return ( - - Twitter - - - ); -} - -export function SunIcon(props: ComponentProps<"svg">) { - return ( - - Sun - - - ); -} - -export function MoonIcon(props: ComponentProps<"svg">) { - return ( - - Moon - - - ); -} - -export function DesktopIcon(props: ComponentProps<"svg">) { - return ( - - Desktop - - - ); -} - -export function CopyIcon(props: ComponentProps<"svg">) { - return ( - - Copy - - - ); -} - export function CheckIcon(props: ComponentProps<"svg">) { return ( diff --git a/apps/docs/src/components/index.ts b/apps/docs/src/components/index.ts index c2b7d5b3..6ef8d54d 100644 --- a/apps/docs/src/components/index.ts +++ b/apps/docs/src/components/index.ts @@ -1,12 +1 @@ -export * from "./callout"; -export * from "./footer"; -export * from "./header"; export * from "./icons"; -export * from "./kbd"; -export * from "./layout"; -export * from "./navigation"; -export * from "./preview"; -export * from "./prose"; -export * from "./table-of-contents"; -export * from "./tabs-snippets"; -export * from "./theme-selector"; diff --git a/apps/docs/src/components/kbd.tsx b/apps/docs/src/components/kbd.tsx deleted file mode 100644 index bf1cfcc7..00000000 --- a/apps/docs/src/components/kbd.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { clsx } from "clsx"; -import { type ComponentProps, splitProps } from "solid-js"; - -export function Kbd(props: ComponentProps<"kbd">) { - const [local, others] = splitProps(props, ["class"]); - - return ( - - ); -} diff --git a/apps/docs/src/components/layout.tsx b/apps/docs/src/components/layout.tsx deleted file mode 100644 index cc52a66a..00000000 --- a/apps/docs/src/components/layout.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useLocation } from "@solidjs/router"; -import { type ParentProps, Show } from "solid-js"; - -import type { NavSection } from "../model/navigation"; -import { Footer } from "./footer"; -import { Header } from "./header"; -import { Navigation } from "./navigation"; -import { Prose } from "./prose"; -import { TableOfContents } from "./table-of-contents"; - -interface LayoutProps extends ParentProps { - navSections: NavSection[]; -} - -export function Layout(props: LayoutProps) { - const location = useLocation(); - - const allLinks = () => props.navSections.flatMap((section) => section.links); - const linkIndex = () => - allLinks().findIndex((link) => link.href === location.pathname); - const previousPage = () => allLinks()[linkIndex() - 1]; - const nextPage = () => allLinks()[linkIndex() + 1]; - - return ( - <> -
-
- -
-
- {props.children} -
-
- -
-
- Previous -
-
- - - {previousPage().title} - -
-
-
- -
-
- Next -
-
- - {nextPage().title} - - -
-
-
-
-
-
- -
- - ); -} diff --git a/apps/docs/src/components/mobile-navigation.tsx b/apps/docs/src/components/mobile-navigation.tsx deleted file mode 100644 index 2a67d43c..00000000 --- a/apps/docs/src/components/mobile-navigation.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Dialog } from "@kobalte/core/dialog"; -import { createDisclosureState } from "@kobalte/core/primitives/create-disclosure-state"; -import { Separator } from "@kobalte/core/separator"; -import { useIsRouting, useMatch } from "@solidjs/router"; -import { clsx } from "clsx"; -import { type ComponentProps, createComputed, splitProps } from "solid-js"; - -import { - LATEST_CORE_CHANGELOG_URL, - LATEST_CORE_VERSION_NAME, -} from "../VERSIONS"; -import type { NavSection } from "../model/navigation"; -import { CrossIcon, HamburgerMenuIcon } from "./icons"; -import { Navigation } from "./navigation"; - -interface MobileNavigationProps extends ComponentProps<"button"> { - sections: NavSection[]; -} - -export function MobileNavigation(props: MobileNavigationProps) { - const [local] = splitProps(props, ["sections", "class"]); - - const { isOpen, setIsOpen, open, close } = createDisclosureState(); - - const isRouting = useIsRouting(); - - createComputed(() => isRouting() && close()); - - const isChangelogPath = useMatch(() => "/docs/changelog/*"); - - return ( - <> - - - - - - -
-
- - Kobalte - . - - - {LATEST_CORE_VERSION_NAME} - -
- -
- - - -
-
-
- - ); -} diff --git a/apps/docs/src/components/navigation.tsx b/apps/docs/src/components/navigation.tsx deleted file mode 100644 index 48d254b3..00000000 --- a/apps/docs/src/components/navigation.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useLocation } from "@solidjs/router"; -import { clsx } from "clsx"; -import { type ComponentProps, For, Match, Switch, splitProps } from "solid-js"; - -import type { NavSection } from "../model/navigation"; - -interface NavigationProps extends ComponentProps<"nav"> { - sections: NavSection[]; -} - -export function Navigation(props: NavigationProps) { - const [local, others] = splitProps(props, ["sections", "class"]); - - const location = useLocation(); - - return ( - - ); -} diff --git a/apps/docs/src/components/preview.tsx b/apps/docs/src/components/preview.tsx deleted file mode 100644 index f552501d..00000000 --- a/apps/docs/src/components/preview.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { clsx } from "clsx"; -import { type ComponentProps, mergeProps, splitProps } from "solid-js"; - -interface PreviewProps extends ComponentProps<"div"> { - isCentered?: boolean; - isRounded?: boolean; -} - -export function Preview(props: PreviewProps) { - const mergedProps = mergeProps({ isCentered: true }, props); - - const [local, others] = splitProps(mergedProps, [ - "class", - "isCentered", - "isRounded", - ]); - - return ( -
- ); -} diff --git a/apps/docs/src/components/prose.tsx b/apps/docs/src/components/prose.tsx deleted file mode 100644 index 8bbdd2c0..00000000 --- a/apps/docs/src/components/prose.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { clsx } from "clsx"; -import { type ComponentProps, splitProps } from "solid-js"; - -export function Prose(props: ComponentProps<"div">) { - const [local, others] = splitProps(props, ["class"]); - - return ( -
- ); -} diff --git a/apps/docs/src/components/table-of-contents.tsx b/apps/docs/src/components/table-of-contents.tsx deleted file mode 100644 index 517fde0b..00000000 --- a/apps/docs/src/components/table-of-contents.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { useLocation } from "@solidjs/router"; -import { clsx } from "clsx"; -import { - type Accessor, - For, - type Setter, - Suspense, - createEffect, - createSignal, - on, - onCleanup, -} from "solid-js"; -import { isServer } from "solid-js/web"; -//import { mods } from "../app"; - -interface TocItem { - depth: number; - text: string; - slug: string; -} - -function getHeadingsFromToc(tableOfContents: TocItem[]) { - return tableOfContents.map(({ slug }) => { - const el = document.getElementById(slug); - - if (!el) { - return; - } - - const style = window.getComputedStyle(el); - const scrollMt = Number.parseFloat(style.scrollMarginTop) + 1; - - const top = window.scrollY + el.getBoundingClientRect().top - scrollMt; - - return { slug, top }; - }); -} - -function useCurrentSection(tableOfContents: Accessor) { - const [currentSection, setCurrentSection] = createSignal( - tableOfContents()?.[0]?.slug, - ); - - createEffect(() => { - const toc = tableOfContents(); - - if (toc == null || toc.length === 0) { - return; - } - - const headings = getHeadingsFromToc(toc); - - function onScroll() { - const top = window.scrollY; - let current = headings[0]?.slug; - - for (const heading of headings) { - if (heading == null) { - continue; - } - - if (top >= heading.top) { - current = heading.slug; - } else { - break; - } - } - - setCurrentSection(current); - } - - window.addEventListener("scroll", onScroll, { passive: true }); - - onScroll(); - - onCleanup(() => { - // @ts-ignore - window.removeEventListener("scroll", onScroll, { passive: true }); - }); - }); - - return currentSection; -} - -//const getTOC = cache(async (pathname: string) => { -// "use server"; -// -// const mod = mods[`./routes${pathname}.mdx`] ?? mods[`./routes${pathname}.md`]; -// return !mod -// ? [] -// : mod.getHeadings().filter((h) => h.depth > 1 && h.depth <= 3); -//}, "toc"); - -function updateHeadings(setter: Setter) { - if (document.getElementsByTagName("article").length === 0) { - setTimeout(() => updateHeadings(setter), 1); - return; - } - - setter( - [ - ...document - .getElementsByTagName("article")[0] - .querySelectorAll( - "h1[data-toc], h2[data-toc], h3[data-toc], h4[data-toc], h5[data-toc], h6[data-toc]", - ), - ].map((element) => ({ - depth: Number(element.tagName.substr(1)), - text: element.textContent!, - slug: element.id, - })), - ); -} - -export function TableOfContents() { - const path = useLocation(); - - // const toc = createAsync(() => getTOC(path.pathname)); - - const [toc, setToc] = createSignal([]); - - createEffect( - on( - () => path.pathname, - (pathname) => { - if (isServer) return; - - updateHeadings(setToc); - }, - ), - ); - - const currentSection = useCurrentSection(toc); - - createEffect( - on( - () => currentSection(), - (currentSection) => { - if (isServer) return; - - const element = document.querySelector( - `a[data-toc-slug="${currentSection}"]`, - ); - - element?.scrollIntoView({ - behavior: "smooth", - block: "nearest", - }); - }, - ), - ); - - return ( - - ); -} diff --git a/apps/docs/src/components/tabs-snippets.tsx b/apps/docs/src/components/tabs-snippets.tsx deleted file mode 100644 index e988c213..00000000 --- a/apps/docs/src/components/tabs-snippets.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Tabs, type TabsRootProps } from "@kobalte/core/tabs"; -import { clsx } from "clsx"; -import { - type ComponentProps, - type ParentComponent, - splitProps, -} from "solid-js"; - -type TabsSnippetsComposite = { - List: typeof Tabs.List; - Trigger: typeof Tabs.Trigger; - Content: typeof Tabs.Content; -}; - -export const TabsSnippets: ParentComponent & - TabsSnippetsComposite = (props) => { - const [local, others] = splitProps(props, ["class"]); - - return ( - - ); -}; - -TabsSnippets.List = (props: ComponentProps) => { - const [local, others] = splitProps(props, ["children", "class"]); - - return ( - - {local.children} - - - ); -}; - -TabsSnippets.Trigger = (props: ComponentProps) => { - const [local, others] = splitProps(props, ["class"]); - - return ( - - ); -}; - -TabsSnippets.Content = Tabs.Content; diff --git a/apps/docs/src/components/theme-selector.tsx b/apps/docs/src/components/theme-selector.tsx deleted file mode 100644 index e8169cdf..00000000 --- a/apps/docs/src/components/theme-selector.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { - type ConfigColorMode, - type MaybeConfigColorMode, - useColorMode, -} from "@kobalte/core/color-mode"; -import { Select } from "@kobalte/core/select"; -import { type JSX, createSignal, onMount } from "solid-js"; - -import { DesktopIcon, MoonIcon, SunIcon } from "./icons"; - -interface ThemeOption { - value: ConfigColorMode; - label: string; - icon: (clazz: string) => JSX.Element; -} - -const THEME_OPTIONS: ThemeOption[] = [ - { - value: "light", - label: "Light", - icon: (clazz: string) => , - }, - { - value: "dark", - label: "Dark", - icon: (clazz: string) => , - }, - { - value: "system", - label: "System", - icon: (clazz: string) => , - }, -]; - -function parseCookie(): MaybeConfigColorMode { - const match = document.cookie.match(/(^| )kb-color-mode=([^;]+)/); - return match?.[2] as MaybeConfigColorMode; -} - -export function ThemeSelector() { - const { colorMode, setColorMode } = useColorMode(); - const [selectedTheme, setSelectedTheme] = createSignal(); - - onMount(() => { - setSelectedTheme( - THEME_OPTIONS.find((option) => option.value === parseCookie()), - ); - }); - - return ( - - options={THEME_OPTIONS} - optionValue="value" - optionTextValue="label" - value={selectedTheme() ?? THEME_OPTIONS[0]} - onChange={(option) => { - setSelectedTheme(option); - setColorMode(option.value); - }} - gutter={8} - sameWidth={false} - placement="bottom" - itemComponent={(props) => ( - - {props.item.rawValue.icon("h-4 w-4")} - {props.item.rawValue.label} - - )} - > - - > - {(state) => state.selectedOption().icon("h-5 w-5")} - - - - - - - - - ); -} diff --git a/apps/docs/src/entry-client.tsx b/apps/docs/src/entry-client.tsx index cf73e0f7..93211d71 100644 --- a/apps/docs/src/entry-client.tsx +++ b/apps/docs/src/entry-client.tsx @@ -1,3 +1,3 @@ -import { StartClient, mount } from "@solidjs/start/client"; +import { mount, StartClient } from "@solidjs/start/client"; mount(() => , document.getElementById("app")!); diff --git a/apps/docs/src/entry-server.tsx b/apps/docs/src/entry-server.tsx index bf657513..bdf1c503 100644 --- a/apps/docs/src/entry-server.tsx +++ b/apps/docs/src/entry-server.tsx @@ -1,10 +1,16 @@ +import { getHtmlProps } from "@kobalte/solidbase/server"; import { StartServer, createHandler } from "@solidjs/start/server"; export default createHandler(() => ( ( - + + Kobalte + diff --git a/apps/docs/src/examples/accordion.module.css b/apps/docs/src/examples/accordion.module.css index 0e0c5aef..8a799211 100644 --- a/apps/docs/src/examples/accordion.module.css +++ b/apps/docs/src/examples/accordion.module.css @@ -1,19 +1,25 @@ .accordion { - border-radius: 6px; + display: flex; + flex-direction: column; width: 300px; - border: 1px solid hsl(240 5% 84%); + border-radius: 6px; + border: 1px solid hsl(240 6% 90%); + background-color: white; color: hsl(240 4% 16%); } +.accordion__item { + overflow: hidden; +} + .accordion__item:first-child { - margin-top: 0; - border-top-left-radius: 4px; - border-top-right-radius: 4px; + border-top-left-radius: 6px; + border-top-right-radius: 6px; } .accordion__item:last-child { - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; + border-bottom-left-radius: 6px; + border-bottom-right-radius: 6px; } .accordion__item-header { @@ -25,26 +31,38 @@ align-items: center; justify-content: space-between; width: 100%; - padding: 14px; - border-bottom: 1px solid hsl(240 5% 84%); - font-weight: 600; + padding: 14px 16px; + border: none; + border-top: 1px solid hsl(240 6% 90%); + font-size: 16px; + font-weight: 500; text-align: left; outline: none; + cursor: pointer; + background: none; + transition: + border-color 250ms, + color 250ms; +} + +.accordion__item:first-child .accordion__item-trigger { + border-top: none; } -.accordion__item:last-child .accordion__item-trigger { - border-bottom: none; +.accordion__item-trigger:hover { + background-color: hsl(240 6% 97%); } .accordion__item-trigger:focus-visible { z-index: 1; outline: 2px solid hsl(200 98% 39%); - outline-offset: 2px; + outline-offset: -2px; } .accordion__item-trigger-icon { - width: 24px; - height: 24px; + width: 20px; + height: 20px; + color: hsl(240 5% 46%); transition: transform 300ms cubic-bezier(0.87, 0, 0.13, 1); } @@ -54,7 +72,7 @@ .accordion__item-content { overflow: hidden; - font-size: 16px; + font-size: 14px; animation: slideUp 300ms cubic-bezier(0.87, 0, 0.13, 1); } @@ -64,6 +82,7 @@ .accordion__item-content-text { padding: 16px; + color: hsl(240 5% 26%); } @keyframes slideDown { @@ -84,11 +103,24 @@ } } -[data-kb-theme="dark"] .accordion { - border-color: hsl(240 5% 65%); - color: hsl(0 0% 100% / 0.9); +[data-theme*="dark"] .accordion { + border-color: hsl(240 5% 34%); + background-color: hsl(240 4% 16%); + color: hsl(0 100% 100% / 0.9); +} + +[data-theme*="dark"] .accordion__item-trigger { + border-top-color: hsl(240 5% 34%); +} + +[data-theme*="dark"] .accordion__item-trigger:hover { + background-color: hsl(240 4% 22%); +} + +[data-theme*="dark"] .accordion__item-trigger-icon { + color: hsl(240 5% 65%); } -[data-kb-theme="dark"] .accordion__item-trigger { - border-bottom-color: hsl(240 5% 65%); +[data-theme*="dark"] .accordion__item-content-text { + color: hsl(0 100% 100% / 0.9); } diff --git a/apps/docs/src/examples/accordion.tsx b/apps/docs/src/examples/accordion.tsx index a90c283f..2f5b71ce 100644 --- a/apps/docs/src/examples/accordion.tsx +++ b/apps/docs/src/examples/accordion.tsx @@ -1,9 +1,10 @@ -import { Accordion } from "@kobalte/core/accordion"; import { createSignal } from "solid-js"; import { ChevronDownIcon } from "../components"; import style from "./accordion.module.css"; +import { Accordion } from "@kobalte/core/accordion"; + export function BasicExample() { return ( @@ -182,7 +183,9 @@ export function ControlledExample() { -

+

Expanded item: {expandedItem().join(",")}

diff --git a/apps/docs/src/examples/alert-dialog.module.css b/apps/docs/src/examples/alert-dialog.module.css index d619abd3..12399335 100644 --- a/apps/docs/src/examples/alert-dialog.module.css +++ b/apps/docs/src/examples/alert-dialog.module.css @@ -130,33 +130,33 @@ } } -[data-kb-theme="dark"] .alert-dialog__trigger { +[data-theme*="dark"] .alert-dialog__trigger { background-color: hsl(201 96% 32%); color: hsla(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .alert-dialog__trigger:hover { +[data-theme*="dark"] .alert-dialog__trigger:hover { background-color: hsl(200 98% 39%); } -[data-kb-theme="dark"] .alert-dialog__trigger:active { +[data-theme*="dark"] .alert-dialog__trigger:active { background-color: hsl(199 89% 48%); } -[data-kb-theme="dark"] .alert-dialog__content { +[data-theme*="dark"] .alert-dialog__content { border: 1px solid hsl(240 5% 26%); background-color: hsl(240 4% 16%); box-shadow: none; } -[data-kb-theme="dark"] .alert-dialog__close-button { +[data-theme*="dark"] .alert-dialog__close-button { color: hsl(0 100% 100% / 0.8); } -[data-kb-theme="dark"] .alert-dialog__title { +[data-theme*="dark"] .alert-dialog__title { color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .alert-dialog__description { +[data-theme*="dark"] .alert-dialog__description { color: hsl(0 100% 100% / 0.7); } diff --git a/apps/docs/src/examples/alert-dialog.tsx b/apps/docs/src/examples/alert-dialog.tsx index d7eef264..a15dfcc6 100644 --- a/apps/docs/src/examples/alert-dialog.tsx +++ b/apps/docs/src/examples/alert-dialog.tsx @@ -1,8 +1,8 @@ -import { AlertDialog } from "@kobalte/core/alert-dialog"; - import { CrossIcon } from "../components"; import style from "./alert-dialog.module.css"; +import { AlertDialog } from "@kobalte/core/alert-dialog"; + export function BasicExample() { return ( diff --git a/apps/docs/src/examples/alert.module.css b/apps/docs/src/examples/alert.module.css index d4e2af77..b0752b58 100644 --- a/apps/docs/src/examples/alert.module.css +++ b/apps/docs/src/examples/alert.module.css @@ -6,7 +6,7 @@ font-size: 16px; } -[data-kb-theme="dark"] .alert { +[data-theme*="dark"] .alert { background-color: hsla(202 80% 24% / 0.2); color: hsl(198 93% 60%); } diff --git a/apps/docs/src/examples/alert.tsx b/apps/docs/src/examples/alert.tsx index 7e717f84..c0f9ba6d 100644 --- a/apps/docs/src/examples/alert.tsx +++ b/apps/docs/src/examples/alert.tsx @@ -1,7 +1,7 @@ -import { Alert } from "@kobalte/core/alert"; - import style from "./alert.module.css"; +import { Alert } from "@kobalte/core/alert"; + export function BasicExample() { return ( Kobalte is going live soon, get ready! diff --git a/apps/docs/src/examples/badge.tsx b/apps/docs/src/examples/badge.tsx index edcd4f94..faa178d1 100644 --- a/apps/docs/src/examples/badge.tsx +++ b/apps/docs/src/examples/badge.tsx @@ -1,7 +1,7 @@ -import { Badge } from "@kobalte/core/badge"; - import style from "./badge.module.css"; +import { Badge } from "@kobalte/core/badge"; + export function BasicExample() { return ( diff --git a/apps/docs/src/examples/breadcrumbs.module.css b/apps/docs/src/examples/breadcrumbs.module.css index 844879e6..0932790a 100644 --- a/apps/docs/src/examples/breadcrumbs.module.css +++ b/apps/docs/src/examples/breadcrumbs.module.css @@ -31,14 +31,14 @@ color: hsl(240 5% 65%); } -[data-kb-theme="dark"] .breadcrumbs__link { +[data-theme*="dark"] .breadcrumbs__link { color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .breadcrumbs__link[data-disabled] { +[data-theme*="dark"] .breadcrumbs__link[data-disabled] { color: hsl(240 4% 46%); } -[data-kb-theme="dark"] .breadcrumbs__link:hover { +[data-theme*="dark"] .breadcrumbs__link:hover { color: hsl(201 96% 32%); } diff --git a/apps/docs/src/examples/breadcrumbs.tsx b/apps/docs/src/examples/breadcrumbs.tsx index 0800ca78..933181ba 100644 --- a/apps/docs/src/examples/breadcrumbs.tsx +++ b/apps/docs/src/examples/breadcrumbs.tsx @@ -1,8 +1,8 @@ -import { Breadcrumbs } from "@kobalte/core/breadcrumbs"; - import { ChevronRightIcon } from "../components"; import style from "./breadcrumbs.module.css"; +import { Breadcrumbs } from "@kobalte/core/breadcrumbs"; + export function BasicExample() { return ( diff --git a/apps/docs/src/examples/button.module.css b/apps/docs/src/examples/button.module.css index 694fd918..2593a99d 100644 --- a/apps/docs/src/examples/button.module.css +++ b/apps/docs/src/examples/button.module.css @@ -28,15 +28,15 @@ background-color: hsl(201 90% 27%); } -[data-kb-theme="dark"] .button { +[data-theme*="dark"] .button { background-color: hsl(201 96% 32%); color: hsla(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .button:hover { +[data-theme*="dark"] .button:hover { background-color: hsl(200 98% 39%); } -[data-kb-theme="dark"] .button:active { +[data-theme*="dark"] .button:active { background-color: hsl(199 89% 48%); } diff --git a/apps/docs/src/examples/button.tsx b/apps/docs/src/examples/button.tsx index 3e76fee5..93bb10db 100644 --- a/apps/docs/src/examples/button.tsx +++ b/apps/docs/src/examples/button.tsx @@ -1,7 +1,7 @@ -import { Button } from "@kobalte/core/button"; - import style from "./button.module.css"; +import { Button } from "@kobalte/core/button"; + export function BasicExample() { return ; } diff --git a/apps/docs/src/examples/calendar.module.css b/apps/docs/src/examples/calendar.module.css new file mode 100644 index 00000000..8ea15970 --- /dev/null +++ b/apps/docs/src/examples/calendar.module.css @@ -0,0 +1,241 @@ +.calendar { + display: inline-block; + background-color: white; + border: 1px solid hsl(214 32% 91%); + border-radius: 10px; + padding: 16px; + box-shadow: 0 1px 3px rgb(0 0 0 / 0.07); + font-family: inherit; +} + +.calendar__header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.calendar__heading { + font-size: 14px; + font-weight: 600; + color: hsl(222 47% 11%); +} + +.calendar__nav_button { + appearance: none; + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 6px; + border: 1px solid hsl(214 32% 91%); + background-color: white; + color: hsl(215 16% 47%); + cursor: pointer; + font-size: 16px; + line-height: 1; + flex-shrink: 0; + transition: background-color 150ms, color 150ms; +} + +.calendar__nav_button:hover { + background-color: hsl(210 40% 96%); + color: hsl(222 47% 11%); +} + +.calendar__nav_button:focus-visible { + outline: 2px solid hsl(221 83% 53%); + outline-offset: 2px; +} + +.calendar__nav_button[disabled] { + opacity: 0.4; + cursor: default; +} + +.calendar__body { + display: flex; + gap: 24px; +} + +.calendar__grid { + border-collapse: collapse; + width: 100%; +} + +.calendar__day_name { + font-size: 11px; + font-weight: 500; + color: hsl(215 16% 47%); + text-align: center; + padding: 4px 0 8px; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.calendar__cell_trigger { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 9999px; + font-size: 13px; + cursor: pointer; + user-select: none; + color: hsl(222 47% 11%); + transition: background-color 100ms; +} + +.calendar__cell_trigger:focus-visible { + outline: 2px solid hsl(221 83% 53%); + outline-offset: 1px; +} + +.calendar__cell_trigger[data-selected] { + background-color: hsl(221 83% 53%); + color: white; +} + +.calendar__cell_trigger[data-today] { + font-weight: 600; + color: hsl(221 83% 53%); +} + +.calendar__cell_trigger[data-today][data-selected] { + color: white; +} + +.calendar__cell_trigger[data-highlighted] { + background-color: hsl(210 40% 94%); +} + +.calendar__cell_trigger[data-highlighted][data-selected] { + background-color: hsl(221 83% 47%); +} + +.calendar__cell_trigger:hover:not([data-selected]):not([data-disabled]):not([data-unavailable]) { + background-color: hsl(210 40% 96%); +} + +.calendar__cell_trigger[data-selected]:hover { + background-color: hsl(221 83% 47%); +} + +.calendar__cell_trigger[data-disabled] { + opacity: 0.3; + cursor: default; +} + +.calendar__cell_trigger[data-unavailable] { + text-decoration: line-through; + opacity: 0.4; + cursor: default; +} + +.calendar__cell_trigger[data-outside-month] { + opacity: 0.2; +} + +.calendar__controlled_output { + font-size: 12px; + color: hsl(215 16% 47%); + margin: 0; +} + +/* ─── Date picker ───────────────────────────────────────────────────────────── */ + +.datepicker { + display: inline-flex; + align-items: center; + border: 1px solid hsl(214 32% 91%); + border-radius: 8px; + background-color: white; + overflow: hidden; + transition: box-shadow 150ms; +} + +.datepicker:focus-within { + box-shadow: 0 0 0 2px hsl(221 83% 53% / 0.3); + border-color: hsl(221 83% 53%); +} + +.datepicker__input { + flex: 1; + border: none; + outline: none; + padding: 0 12px; + height: 38px; + font-size: 14px; + color: hsl(222 47% 11%); + background: transparent; + cursor: default; + min-width: 180px; +} + +.datepicker__input::placeholder { + color: hsl(215 16% 65%); +} + +.datepicker__button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 38px; + border: none; + border-left: 1px solid hsl(214 32% 91%); + background-color: white; + color: hsl(215 16% 47%); + cursor: pointer; + flex-shrink: 0; + transition: background-color 150ms, color 150ms; +} + +.datepicker__button:hover { + background-color: hsl(210 40% 96%); + color: hsl(221 83% 53%); +} + +.datepicker__button:focus-visible { + outline: 2px solid hsl(221 83% 53%); + outline-offset: -2px; +} + +.datepicker__button[data-expanded] { + background-color: hsl(210 40% 96%); + color: hsl(221 83% 53%); +} + +.datepicker__popover { + z-index: 50; + transform-origin: var(--kb-popover-content-transform-origin); + animation: datepicker-hide 150ms ease-in forwards; +} + +.datepicker__popover[data-expanded] { + animation: datepicker-show 150ms ease-out; +} + +@keyframes datepicker-show { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes datepicker-hide { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(-4px); + } +} diff --git a/apps/docs/src/examples/calendar.tsx b/apps/docs/src/examples/calendar.tsx new file mode 100644 index 00000000..ece7666a --- /dev/null +++ b/apps/docs/src/examples/calendar.tsx @@ -0,0 +1,289 @@ +import { GregorianCalendar, isWeekend } from "@internationalized/date"; +import { createMemo, createSignal, Show } from "solid-js"; + +import { Calendar, type DateValue } from "@kobalte/core/calendar"; +import { Popover } from "@kobalte/core/popover"; +import style from "./calendar.module.css"; + +const createCalendar = () => new GregorianCalendar(); + +function CalendarGrid(props: { offset?: { months: number } }) { + return ( + + + + {(day) => ( + + {day().slice(0, 2)} + + )} + + + + {(weekIndex) => ( + + {(date) => ( + }> + {(d) => ( + + + + )} + + )} + + )} + + + ); +} + +export function BasicExample() { + return ( + + + + ‹ + + + + › + + + + + + + ); +} + +export function MultipleExample() { + return ( + + + + ‹ + + + + › + + + + + + + ); +} + +export function RangeExample() { + return ( + + + + ‹ + + + + › + + + + + + + + ); +} + +export function ControlledExample() { + const [value, setValue] = createSignal(null); + + return ( +
+ setValue(v as DateValue)} + class={style.calendar} + > + + + ‹ + + + + › + + + + + + +

+ {value() ? `Selected: ${value()!.toString()}` : "No date selected"} +

+
+ ); +} + +export function DisabledDatesExample() { + return ( + isWeekend(date, "en-US")} + class={style.calendar} + > + + + ‹ + + + + › + + + + + + + ); +} + +function CalendarIcon() { + return ( + + ); +} + +export function DatePickerExample() { + const [open, setOpen] = createSignal(false); + const [value, setValue] = createSignal(null); + + const formatted = createMemo(() => { + const v = value(); + if (!v) return ""; + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + return new Intl.DateTimeFormat("en-US", { + month: "long", + day: "numeric", + year: "numeric", + }).format(v.toDate(tz)); + }); + + return ( + + + + + + + + + + { + setValue(v as DateValue); + setOpen(false); + }} + class={style.calendar} + > + + + ‹ + + + + › + + + + + + + + + + ); +} diff --git a/apps/docs/src/examples/checkbox.module.css b/apps/docs/src/examples/checkbox.module.css index de6f651e..2fdaa2c3 100644 --- a/apps/docs/src/examples/checkbox.module.css +++ b/apps/docs/src/examples/checkbox.module.css @@ -47,25 +47,25 @@ user-select: none; } -[data-kb-theme="dark"] .checkbox__control { +[data-theme*="dark"] .checkbox__control { border-color: hsl(240 5% 34%); background-color: hsl(240 5% 26%); } -[data-kb-theme="dark"] .checkbox__control[data-checked] { +[data-theme*="dark"] .checkbox__control[data-checked] { border-color: hsl(200 98% 39%); background-color: hsl(200 98% 39%); color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .checkbox__control[data-invalid] { +[data-theme*="dark"] .checkbox__control[data-invalid] { border-color: hsl(0 72% 51%); } -[data-kb-theme="dark"] .checkbox__label { +[data-theme*="dark"] .checkbox__label { color: hsl(240 5% 84%); } -[data-kb-theme="dark"] .checkbox__description { +[data-theme*="dark"] .checkbox__description { color: hsl(240 5% 65%); } diff --git a/apps/docs/src/examples/checkbox.tsx b/apps/docs/src/examples/checkbox.tsx index 32bf0b43..b86ae650 100644 --- a/apps/docs/src/examples/checkbox.tsx +++ b/apps/docs/src/examples/checkbox.tsx @@ -1,10 +1,10 @@ -import { Checkbox } from "@kobalte/core/checkbox"; -import { clsx } from "clsx"; import { createSignal } from "solid-js"; import { CheckIcon } from "../components"; import style from "./checkbox.module.css"; +import { Checkbox } from "@kobalte/core/checkbox"; + export function BasicExample() { return ( @@ -51,7 +51,9 @@ export function ControlledExample() { Subscribe -

+

You are {checked() ? "subscribed" : "unsubscribed"}.

@@ -60,14 +62,21 @@ export function ControlledExample() { export function DescriptionExample() { return ( - + -
+
Subscribe You will receive our weekly newsletter. @@ -82,7 +91,8 @@ export function ErrorMessageExample() { return ( -
+
Subscribe You must agree to our Terms and Conditions. @@ -119,7 +136,12 @@ export function HTMLFormExample() {
@@ -130,7 +152,7 @@ export function HTMLFormExample() { Subscribe -
+
diff --git a/apps/docs/src/examples/collapsible.module.css b/apps/docs/src/examples/collapsible.module.css index 6b2aadc7..9ccca47f 100644 --- a/apps/docs/src/examples/collapsible.module.css +++ b/apps/docs/src/examples/collapsible.module.css @@ -65,8 +65,8 @@ } } -[data-kb-theme="dark"] .collapsible__trigger, -[data-kb-theme="dark"] .collapsible__content { +[data-theme*="dark"] .collapsible__trigger, +[data-theme*="dark"] .collapsible__content { border-color: hsl(240 5% 65%); color: hsl(0 0% 100% / 0.9); } diff --git a/apps/docs/src/examples/collapsible.tsx b/apps/docs/src/examples/collapsible.tsx index 772c3fe7..bc9301cd 100644 --- a/apps/docs/src/examples/collapsible.tsx +++ b/apps/docs/src/examples/collapsible.tsx @@ -1,8 +1,8 @@ -import { Collapsible } from "@kobalte/core/collapsible"; - import { ChevronDownIcon } from "../components"; import style from "./collapsible.module.css"; +import { Collapsible } from "@kobalte/core/collapsible"; + export function BasicExample() { return ( diff --git a/apps/docs/src/examples/color-area.tsx b/apps/docs/src/examples/color-area.tsx index cd86918c..67cb040c 100644 --- a/apps/docs/src/examples/color-area.tsx +++ b/apps/docs/src/examples/color-area.tsx @@ -1,8 +1,9 @@ -import { ColorArea } from "@kobalte/core/color-area"; import { parseColor } from "@kobalte/core/colors"; import { createSignal } from "solid-js"; import style from "./color-area.module.css"; +import { ColorArea } from "@kobalte/core/color-area"; + export function BasicExample() { return ( @@ -51,7 +52,13 @@ export function ControlledValueExample() { -

+

Current color value: {value().toString("hsl")}

@@ -96,7 +103,12 @@ export function HTMLFormExample() { -
+
diff --git a/apps/docs/src/examples/color-channel-field.module.css b/apps/docs/src/examples/color-channel-field.module.css index 6d7552df..b2acae06 100644 --- a/apps/docs/src/examples/color-channel-field.module.css +++ b/apps/docs/src/examples/color-channel-field.module.css @@ -58,13 +58,13 @@ background: rgba(0, 0, 0, 0.2); } -[data-kb-theme="dark"] .color-channel-field__increment, -[data-kb-theme="dark"] .color-channel-field__decrement { +[data-theme*="dark"] .color-channel-field__increment, +[data-theme*="dark"] .color-channel-field__decrement { background: rgba(0, 0, 0, 0.5); } -[data-kb-theme="dark"] .color-channel-field__increment:hover, -[data-kb-theme="dark"] .color-channel-field__decrement:hover { +[data-theme*="dark"] .color-channel-field__increment:hover, +[data-theme*="dark"] .color-channel-field__decrement:hover { background: rgba(0, 0, 0, 0.25); } @@ -117,8 +117,8 @@ border-radius: 0; } -[data-kb-theme="dark"] .color-channel-field__custom-trigger-decrement, -[data-kb-theme="dark"] .color-channel-field__custom-trigger-increment { +[data-theme*="dark"] .color-channel-field__custom-trigger-decrement, +[data-theme*="dark"] .color-channel-field__custom-trigger-increment { border-color: hsl(240 5% 34%); } @@ -156,29 +156,29 @@ user-select: none; } -[data-kb-theme="dark"] .color-channel-field__input { +[data-theme*="dark"] .color-channel-field__input { background-color: hsl(240 4% 16%); border: 1px solid hsl(240 5% 34%); color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .color-channel-field__input:hover { +[data-theme*="dark"] .color-channel-field__input:hover { border-color: hsl(240 4% 46%); } -[data-kb-theme="dark"] .color-channel-field__input[data-invalid] { +[data-theme*="dark"] .color-channel-field__input[data-invalid] { border-color: hsl(0 72% 51%); color: hsl(0 72% 51%); } -[data-kb-theme="dark"] .color-channel-field__input::placeholder { +[data-theme*="dark"] .color-channel-field__input::placeholder { color: hsl(0 100% 100% / 0.5); } -[data-kb-theme="dark"] .color-channel-field__label { +[data-theme*="dark"] .color-channel-field__label { color: hsl(240 5% 84%); } -[data-kb-theme="dark"] .color-channel-field__description { +[data-theme*="dark"] .color-channel-field__description { color: hsl(240 5% 65%); } diff --git a/apps/docs/src/examples/color-channel-field.tsx b/apps/docs/src/examples/color-channel-field.tsx index af2930db..aa9f9235 100644 --- a/apps/docs/src/examples/color-channel-field.tsx +++ b/apps/docs/src/examples/color-channel-field.tsx @@ -1,9 +1,10 @@ -import { ColorChannelField } from "@kobalte/core/color-channel-field"; import { parseColor } from "@kobalte/core/colors"; import { createSignal } from "solid-js"; import { ArrowIcon } from "../components"; import style from "./color-channel-field.module.css"; +import { ColorChannelField } from "@kobalte/core/color-channel-field"; + export function BasicExample() { return ( @@ -91,7 +92,13 @@ export function ControlledExample() {
-

+

Lightness: {value().getChannelValue("lightness")}%

@@ -188,7 +195,12 @@ export function HTMLFormExample() {
-
+
diff --git a/apps/docs/src/examples/color-field.module.css b/apps/docs/src/examples/color-field.module.css index a8ecbea5..76dd9a41 100644 --- a/apps/docs/src/examples/color-field.module.css +++ b/apps/docs/src/examples/color-field.module.css @@ -56,29 +56,29 @@ user-select: none; } -[data-kb-theme="dark"] .color-field__input { +[data-theme*="dark"] .color-field__input { background-color: hsl(240 4% 16%); border: 1px solid hsl(240 5% 34%); color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .color-field__input:hover { +[data-theme*="dark"] .color-field__input:hover { border-color: hsl(240 4% 46%); } -[data-kb-theme="dark"] .color-field__input[data-invalid] { +[data-theme*="dark"] .color-field__input[data-invalid] { border-color: hsl(0 72% 51%); color: hsl(0 72% 51%); } -[data-kb-theme="dark"] .color-field__input::placeholder { +[data-theme*="dark"] .color-field__input::placeholder { color: hsl(0 100% 100% / 0.5); } -[data-kb-theme="dark"] .color-field__label { +[data-theme*="dark"] .color-field__label { color: hsl(240 5% 84%); } -[data-kb-theme="dark"] .color-field__description { +[data-theme*="dark"] .color-field__description { color: hsl(240 5% 65%); } diff --git a/apps/docs/src/examples/color-field.tsx b/apps/docs/src/examples/color-field.tsx index 2faf5622..cef0d9bf 100644 --- a/apps/docs/src/examples/color-field.tsx +++ b/apps/docs/src/examples/color-field.tsx @@ -1,8 +1,9 @@ -import { ColorField } from "@kobalte/core/color-field"; import { createSignal } from "solid-js"; import style from "./color-field.module.css"; +import { ColorField } from "@kobalte/core/color-field"; + export function BasicExample() { return ( @@ -40,7 +41,15 @@ export function ControlledExample() { -

Your favorite color is: {value()}

+

+ Your favorite color is: {value()} +

); } @@ -96,7 +105,12 @@ export function HTMLFormExample() { @@ -104,7 +118,7 @@ export function HTMLFormExample() { -
+
diff --git a/apps/docs/src/examples/color-slider.tsx b/apps/docs/src/examples/color-slider.tsx index 37393b07..89550e2f 100644 --- a/apps/docs/src/examples/color-slider.tsx +++ b/apps/docs/src/examples/color-slider.tsx @@ -1,8 +1,9 @@ -import { ColorSlider } from "@kobalte/core/color-slider"; import { parseColor } from "@kobalte/core/colors"; import { createSignal } from "solid-js"; import style from "./color-slider.module.css"; +import { ColorSlider } from "@kobalte/core/color-slider"; + export function BasicExample() { return ( @@ -62,7 +63,13 @@ export function ControlledValueExample() { -

+

Current color value: {value().toString("hsl")}

@@ -109,7 +116,12 @@ export function HTMLFormExample() { Label @@ -119,7 +131,7 @@ export function HTMLFormExample() { -
+
diff --git a/apps/docs/src/examples/combobox.module.css b/apps/docs/src/examples/combobox.module.css index 3f55cc1a..8b141178 100644 --- a/apps/docs/src/examples/combobox.module.css +++ b/apps/docs/src/examples/combobox.module.css @@ -169,45 +169,45 @@ } } -[data-kb-theme="dark"] .combobox__control { +[data-theme*="dark"] .combobox__control { background-color: hsl(240 4% 16%); border: 1px solid hsl(240 5% 34%); color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .combobox__control:hover { +[data-theme*="dark"] .combobox__control:hover { border-color: hsl(240 4% 46%); } -[data-kb-theme="dark"] .combobox__control[data-invalid] { +[data-theme*="dark"] .combobox__control[data-invalid] { border-color: hsl(0 72% 51%); color: hsl(0 72% 51%); } -[data-kb-theme="dark"] .combobox__input::placeholder { +[data-theme*="dark"] .combobox__input::placeholder { color: hsl(0 100% 100% / 0.5); } -[data-kb-theme="dark"] .combobox__trigger { +[data-theme*="dark"] .combobox__trigger { background: hsl(240 5% 26%); border-left: 1px solid hsl(240 5% 34%); color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .combobox__content { +[data-theme*="dark"] .combobox__content { border: 1px solid hsl(240 5% 26%); background-color: hsl(240 4% 16%); box-shadow: none; } -[data-kb-theme="dark"] .combobox__section { +[data-theme*="dark"] .combobox__section { color: hsl(0 100% 100% / 0.7); } -[data-kb-theme="dark"] .combobox__item { +[data-theme*="dark"] .combobox__item { color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .combobox__description { +[data-theme*="dark"] .combobox__description { color: hsl(0 100% 100% / 0.7); } diff --git a/apps/docs/src/examples/combobox.tsx b/apps/docs/src/examples/combobox.tsx index cab1579a..70d51b25 100644 --- a/apps/docs/src/examples/combobox.tsx +++ b/apps/docs/src/examples/combobox.tsx @@ -1,5 +1,5 @@ import { Combobox } from "@kobalte/core/combobox"; -import { For, createSignal } from "solid-js"; +import { createSignal, For } from "solid-js"; import { CaretSortIcon, CheckIcon, CrossIcon } from "../components"; import style from "./combobox.module.css"; @@ -116,7 +116,15 @@ export function ControlledExample() { -

Your favorite fruit is: {value()}.

+

+ Your favorite fruit is: {value()}. +

); } @@ -218,7 +226,12 @@ export function HTMLFormExample() { -
+
@@ -407,23 +420,41 @@ export function MultipleSelectionExample() { {(state) => ( <>
0 && "p-2" - }`} + style={{ + display: "flex", + "align-items": "center", + gap: "8px", + "flex-wrap": "wrap", + ...(state.selectedOptions().length > 0 && { padding: "8px" }), + }} > {(option) => ( e.stopPropagation()} > {option} )} @@ -438,9 +469,18 @@ export function MultipleSelectionExample() { type="button" onPointerDown={(e) => e.stopPropagation()} onClick={state.clear} - class="ml-auto self-center mr-2 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-600 p-2" + style={{ + "margin-left": "auto", + "align-self": "center", + "margin-right": "8px", + "border-radius": "9999px", + padding: "8px", + cursor: "pointer", + background: "none", + border: "none", + }} > - + @@ -456,7 +496,13 @@ export function MultipleSelectionExample() { -

+

Your favorite fruits are: {values().join(", ")}.

diff --git a/apps/docs/src/examples/context-menu.module.css b/apps/docs/src/examples/context-menu.module.css index 427d9683..addd393d 100644 --- a/apps/docs/src/examples/context-menu.module.css +++ b/apps/docs/src/examples/context-menu.module.css @@ -131,34 +131,34 @@ } } -[data-kb-theme="dark"] .context-menu__trigger { +[data-theme*="dark"] .context-menu__trigger { border-color: rgb(255 255 255 / 0.5); color: rgb(255 255 255 / 0.7); } -[data-kb-theme="dark"] .context-menu__content, -[data-kb-theme="dark"] .context-menu__sub-content { +[data-theme*="dark"] .context-menu__content, +[data-theme*="dark"] .context-menu__sub-content { border: 1px solid hsl(240 5% 26%); background-color: hsl(240 4% 16%); box-shadow: none; } -[data-kb-theme="dark"] .context-menu__group-label { +[data-theme*="dark"] .context-menu__group-label { color: hsl(0 100% 100% / 0.7); } -[data-kb-theme="dark"] .context-menu__item, -[data-kb-theme="dark"] .context-menu__checkbox-item, -[data-kb-theme="dark"] .context-menu__radio-item, -[data-kb-theme="dark"] .context-menu__sub-trigger { +[data-theme*="dark"] .context-menu__item, +[data-theme*="dark"] .context-menu__checkbox-item, +[data-theme*="dark"] .context-menu__radio-item, +[data-theme*="dark"] .context-menu__sub-trigger { color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .context-menu__sub-trigger[data-expanded]:not([data-highlighted]) { +[data-theme*="dark"] .context-menu__sub-trigger[data-expanded]:not([data-highlighted]) { background-color: hsl(202 80% 24% / 0.7); color: hsl(198 93% 60%); } -[data-kb-theme="dark"] .context-menu__separator { +[data-theme*="dark"] .context-menu__separator { border-color: hsl(240 5% 34%); } diff --git a/apps/docs/src/examples/context-menu.tsx b/apps/docs/src/examples/context-menu.tsx index 31be52f2..30c32e63 100644 --- a/apps/docs/src/examples/context-menu.tsx +++ b/apps/docs/src/examples/context-menu.tsx @@ -1,9 +1,10 @@ -import { ContextMenu } from "@kobalte/core/context-menu"; import { createSignal } from "solid-js"; import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "../components"; import style from "./context-menu.module.css"; +import { ContextMenu } from "@kobalte/core/context-menu"; + export function BasicExample() { const [showGitLog, setShowGitLog] = createSignal(true); const [showHistory, setShowHistory] = createSignal(false); diff --git a/apps/docs/src/examples/dialog.module.css b/apps/docs/src/examples/dialog.module.css index 59459491..cbd13930 100644 --- a/apps/docs/src/examples/dialog.module.css +++ b/apps/docs/src/examples/dialog.module.css @@ -130,33 +130,33 @@ } } -[data-kb-theme="dark"] .dialog__trigger { +[data-theme*="dark"] .dialog__trigger { background-color: hsl(201 96% 32%); color: hsla(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .dialog__trigger:hover { +[data-theme*="dark"] .dialog__trigger:hover { background-color: hsl(200 98% 39%); } -[data-kb-theme="dark"] .dialog__trigger:active { +[data-theme*="dark"] .dialog__trigger:active { background-color: hsl(199 89% 48%); } -[data-kb-theme="dark"] .dialog__content { +[data-theme*="dark"] .dialog__content { border: 1px solid hsl(240 5% 26%); background-color: hsl(240 4% 16%); box-shadow: none; } -[data-kb-theme="dark"] .dialog__close-button { +[data-theme*="dark"] .dialog__close-button { color: hsl(0 100% 100% / 0.8); } -[data-kb-theme="dark"] .dialog__title { +[data-theme*="dark"] .dialog__title { color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .dialog__description { +[data-theme*="dark"] .dialog__description { color: hsl(0 100% 100% / 0.7); } diff --git a/apps/docs/src/examples/dialog.tsx b/apps/docs/src/examples/dialog.tsx index ffdbd69b..ca423d01 100644 --- a/apps/docs/src/examples/dialog.tsx +++ b/apps/docs/src/examples/dialog.tsx @@ -1,8 +1,8 @@ -import { Dialog } from "@kobalte/core/dialog"; - import { CrossIcon } from "../components"; import style from "./dialog.module.css"; +import { Dialog } from "@kobalte/core/dialog"; + export function BasicExample() { return ( diff --git a/apps/docs/src/examples/drawer.module.css b/apps/docs/src/examples/drawer.module.css new file mode 100644 index 00000000..3d4611fe --- /dev/null +++ b/apps/docs/src/examples/drawer.module.css @@ -0,0 +1,205 @@ +/* ─── Trigger ─────────────────────────────────────────────────────────────── */ + +.drawer__trigger { + appearance: none; + display: inline-flex; + justify-content: center; + align-items: center; + height: 40px; + width: auto; + outline: none; + border-radius: 6px; + padding: 0 16px; + background-color: hsl(200 98% 39%); + color: white; + font-size: 16px; + line-height: 0; + transition: 250ms background-color; + cursor: pointer; + border: none; +} + +.drawer__trigger:hover { + background-color: hsl(201 96% 32%); +} + +.drawer__trigger:focus-visible { + outline: 2px solid hsl(200 98% 39%); + outline-offset: 2px; +} + +.drawer__trigger:active { + background-color: hsl(201 90% 27%); +} + +/* ─── Overlay ─────────────────────────────────────────────────────────────── */ + +/* Opacity is driven by JS (openPercentage) so only a CSS transition is needed. */ +.drawer__overlay { + position: fixed; + inset: 0; + z-index: 50; + background-color: rgb(0 0 0 / 0.4); + /* Animate opacity changes (open/close and drag) */ + transition: opacity 350ms cubic-bezier(0.32, 0.72, 0, 1); +} + +/* ─── Content — bottom drawer ─────────────────────────────────────────────── */ + +.drawer__content { + position: fixed; + z-index: 50; + bottom: 0; + left: 0; + width: 100%; + height: 320px; + background-color: white; + border-radius: 12px 12px 0 0; + border-top: 1px solid hsl(240 5% 84%); + padding: 16px 24px 32px; + box-shadow: 0 -4px 24px rgb(0 0 0 / 0.08); + overflow-y: auto; + outline: none; + /* The drawer applies transform via inline style; this transition animates it. */ + transition: transform 350ms cubic-bezier(0.32, 0.72, 0, 1); +} + +/* ─── Content — right drawer ──────────────────────────────────────────────── */ + +.drawer__content_right { + position: fixed; + z-index: 50; + top: 0; + right: 0; + height: 100%; + width: 320px; + background-color: white; + border-left: 1px solid hsl(240 5% 84%); + padding: 24px; + box-shadow: -4px 0 24px rgb(0 0 0 / 0.08); + overflow-y: auto; + outline: none; + transition: transform 350ms cubic-bezier(0.32, 0.72, 0, 1); +} + +/* ─── Drag handle ─────────────────────────────────────────────────────────── */ + +.drawer__handle { + width: 48px; + height: 4px; + background-color: hsl(240 5% 84%); + border-radius: 9999px; + margin: 0 auto 16px; + flex-shrink: 0; +} + +/* ─── Header ──────────────────────────────────────────────────────────────── */ + +.drawer__header { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 8px; +} + +/* ─── Close button ────────────────────────────────────────────────────────── */ + +.drawer__close-button { + height: 16px; + width: 16px; + color: hsl(240 5% 34%); + flex-shrink: 0; +} + +/* ─── Title ───────────────────────────────────────────────────────────────── */ + +.drawer__title { + font-size: 18px; + font-weight: 500; + color: hsl(240 6% 10%); +} + +/* ─── Description ─────────────────────────────────────────────────────────── */ + +.drawer__description { + font-size: 14px; + color: hsl(240 5% 26%); + line-height: 1.5; +} + +/* ─── Controlled example ──────────────────────────────────────────────────── */ + +.controlled__root { + display: flex; + align-items: center; + gap: 12px; +} + +.controlled__actions { + display: flex; + justify-content: flex-end; + margin-top: 24px; +} + +.drawer__action { + appearance: none; + display: inline-flex; + justify-content: center; + align-items: center; + height: 36px; + padding: 0 16px; + border: none; + border-radius: 6px; + background-color: hsl(240 6% 10%); + color: white; + font-size: 14px; + cursor: pointer; + transition: 250ms background-color; +} + +.drawer__action:hover { + background-color: hsl(240 5% 26%); +} + +/* ─── Dark mode ───────────────────────────────────────────────────────────── */ + +[data-kb-theme="dark"] .drawer__trigger { + background-color: hsl(201 96% 32%); + color: hsla(0 100% 100% / 0.9); +} + +[data-kb-theme="dark"] .drawer__trigger:hover { + background-color: hsl(200 98% 39%); +} + +[data-kb-theme="dark"] .drawer__trigger:active { + background-color: hsl(199 89% 48%); +} + +[data-kb-theme="dark"] .drawer__content, +[data-kb-theme="dark"] .drawer__content_right { + background-color: hsl(240 4% 16%); + border-color: hsl(240 5% 26%); + box-shadow: none; +} + +[data-kb-theme="dark"] .drawer__handle { + background-color: hsl(240 5% 34%); +} + +[data-kb-theme="dark"] .drawer__close-button { + color: hsl(0 100% 100% / 0.8); +} + +[data-kb-theme="dark"] .drawer__title { + color: hsl(0 100% 100% / 0.9); +} + +[data-kb-theme="dark"] .drawer__description { + color: hsl(0 100% 100% / 0.7); +} + +[data-kb-theme="dark"] .drawer__action { + background-color: hsl(240 5% 84%); + color: hsl(240 6% 10%); +} diff --git a/apps/docs/src/examples/drawer.tsx b/apps/docs/src/examples/drawer.tsx new file mode 100644 index 00000000..1a5ffaa7 --- /dev/null +++ b/apps/docs/src/examples/drawer.tsx @@ -0,0 +1,101 @@ +import { createSignal } from "solid-js"; + +import { Drawer } from "@kobalte/core/drawer"; +import { CrossIcon } from "../components"; +import style from "./drawer.module.css"; + +export function BasicExample() { + return ( + + Open drawer + + + +
+
+ Drawer + + + +
+ + Drag the handle or swipe down to dismiss. The overlay opacity and + drawer position both track your finger in real time. + + + + + ); +} + +export function SnapPointsExample() { + return ( + + + Open with snap points + + + + +
+
+ Snap points + + + +
+ + This drawer has three snap points: closed,{" "} + peek (40%), and expanded (100%). + Drag to snap between them. + + + + + ); +} + +export function ControlledExample() { + const [open, setOpen] = createSignal(false); + + return ( +
+ + + Open settings + + + + +
+ Settings + + + +
+ + Open state is controlled by an external signal. + +
+ +
+
+
+
+ +
+ ); +} diff --git a/apps/docs/src/examples/dropdown-menu.module.css b/apps/docs/src/examples/dropdown-menu.module.css index 1080c68c..d7e9f24f 100644 --- a/apps/docs/src/examples/dropdown-menu.module.css +++ b/apps/docs/src/examples/dropdown-menu.module.css @@ -161,29 +161,29 @@ } } -[data-kb-theme="dark"] .dropdown-menu__content, -[data-kb-theme="dark"] .dropdown-menu__sub-content { +[data-theme*="dark"] .dropdown-menu__content, +[data-theme*="dark"] .dropdown-menu__sub-content { border: 1px solid hsl(240 5% 26%); background-color: hsl(240 4% 16%); box-shadow: none; } -[data-kb-theme="dark"] .dropdown-menu__group-label { +[data-theme*="dark"] .dropdown-menu__group-label { color: hsl(0 100% 100% / 0.7); } -[data-kb-theme="dark"] .dropdown-menu__item, -[data-kb-theme="dark"] .dropdown-menu__checkbox-item, -[data-kb-theme="dark"] .dropdown-menu__radio-item, -[data-kb-theme="dark"] .dropdown-menu__sub-trigger { +[data-theme*="dark"] .dropdown-menu__item, +[data-theme*="dark"] .dropdown-menu__checkbox-item, +[data-theme*="dark"] .dropdown-menu__radio-item, +[data-theme*="dark"] .dropdown-menu__sub-trigger { color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .dropdown-menu__sub-trigger[data-expanded]:not([data-highlighted]) { +[data-theme*="dark"] .dropdown-menu__sub-trigger[data-expanded]:not([data-highlighted]) { background-color: hsl(202 80% 24% / 0.7); color: hsl(198 93% 60%); } -[data-kb-theme="dark"] .dropdown-menu__separator { +[data-theme*="dark"] .dropdown-menu__separator { border-color: hsl(240 5% 34%); } diff --git a/apps/docs/src/examples/dropdown-menu.tsx b/apps/docs/src/examples/dropdown-menu.tsx index b24f327f..ce76ad9d 100644 --- a/apps/docs/src/examples/dropdown-menu.tsx +++ b/apps/docs/src/examples/dropdown-menu.tsx @@ -1,4 +1,3 @@ -import { DropdownMenu } from "@kobalte/core/dropdown-menu"; import { createSignal } from "solid-js"; import { @@ -9,6 +8,8 @@ import { } from "../components"; import style from "./dropdown-menu.module.css"; +import { DropdownMenu } from "@kobalte/core/dropdown-menu"; + export function BasicExample() { const [showGitLog, setShowGitLog] = createSignal(true); const [showHistory, setShowHistory] = createSignal(false); diff --git a/apps/docs/src/examples/file-field.tsx b/apps/docs/src/examples/file-field.tsx index 27e102d3..69ac1f7f 100644 --- a/apps/docs/src/examples/file-field.tsx +++ b/apps/docs/src/examples/file-field.tsx @@ -1,7 +1,7 @@ -import { FileField } from "@kobalte/core/file-field"; - import style from "./file-field.module.css"; +import { FileField } from "@kobalte/core/file-field"; + export function BasicExample() { return ( diff --git a/apps/docs/src/examples/image.module.css b/apps/docs/src/examples/image.module.css index df3d3427..c45ed8c7 100644 --- a/apps/docs/src/examples/image.module.css +++ b/apps/docs/src/examples/image.module.css @@ -31,7 +31,7 @@ font-weight: 500; } -[data-kb-theme="dark"] .image__fallback { +[data-theme*="dark"] .image__fallback { background-color: hsl(202 80% 24%); color: hsl(198 93% 60%); } diff --git a/apps/docs/src/examples/image.tsx b/apps/docs/src/examples/image.tsx index 7879c786..b7466e0d 100644 --- a/apps/docs/src/examples/image.tsx +++ b/apps/docs/src/examples/image.tsx @@ -1,10 +1,10 @@ -import { Image } from "@kobalte/core/image"; - import style from "./image.module.css"; +import { Image } from "@kobalte/core/image"; + export function BasicExample() { return ( -
+
diff --git a/apps/docs/src/examples/menubar.module.css b/apps/docs/src/examples/menubar.module.css index 185bd7e7..5a01730c 100644 --- a/apps/docs/src/examples/menubar.module.css +++ b/apps/docs/src/examples/menubar.module.css @@ -155,39 +155,39 @@ } } -[data-kb-theme="dark"] .menubar__trigger { +[data-theme*="dark"] .menubar__trigger { background-color: hsl(240 4% 16%); color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .menubar__content, -[data-kb-theme="dark"] .menubar__sub-content { +[data-theme*="dark"] .menubar__content, +[data-theme*="dark"] .menubar__sub-content { border: 1px solid hsl(240 5% 26%); background-color: hsl(240 4% 16%); box-shadow: none; } -[data-kb-theme="dark"] .menubar__group-label { +[data-theme*="dark"] .menubar__group-label { color: hsl(0 100% 100% / 0.7); } -[data-kb-theme="dark"] .menubar__item, -[data-kb-theme="dark"] .menubar__checkbox-item, -[data-kb-theme="dark"] .menubar__radio-item, -[data-kb-theme="dark"] .menubar__sub-trigger { +[data-theme*="dark"] .menubar__item, +[data-theme*="dark"] .menubar__checkbox-item, +[data-theme*="dark"] .menubar__radio-item, +[data-theme*="dark"] .menubar__sub-trigger { color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .menubar__sub-trigger[data-expanded]:not([data-highlighted]) { +[data-theme*="dark"] .menubar__sub-trigger[data-expanded]:not([data-highlighted]) { background-color: hsl(202 80% 24% / 0.7); color: hsl(198 93% 60%); } -[data-kb-theme="dark"] .menubar__separator { +[data-theme*="dark"] .menubar__separator { border-color: hsl(240 5% 34%); } -[data-kb-theme="dark"] .menubar__trigger[data-highlighted="true"] { +[data-theme*="dark"] .menubar__trigger[data-highlighted="true"] { background-color: hsl(201 96% 32%); color: hsla(0 100% 100% / 0.9); } diff --git a/apps/docs/src/examples/menubar.tsx b/apps/docs/src/examples/menubar.tsx index 66cb0991..0fab23c6 100644 --- a/apps/docs/src/examples/menubar.tsx +++ b/apps/docs/src/examples/menubar.tsx @@ -1,9 +1,10 @@ -import { Menubar } from "@kobalte/core/menubar"; import { createSignal } from "solid-js"; import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "../components"; import style from "./menubar.module.css"; +import { Menubar } from "@kobalte/core/menubar"; + export function BasicExample() { const [showGitLog, setShowGitLog] = createSignal(true); const [showHistory, setShowHistory] = createSignal(false); diff --git a/apps/docs/src/examples/meter.module.css b/apps/docs/src/examples/meter.module.css index 6910f097..ee8c1d6d 100644 --- a/apps/docs/src/examples/meter.module.css +++ b/apps/docs/src/examples/meter.module.css @@ -28,11 +28,11 @@ transition: width 250ms linear; } -[data-kb-theme="dark"] .meter__label, -[data-kb-theme="dark"] .meter__value-label { +[data-theme*="dark"] .meter__label, +[data-theme*="dark"] .meter__value-label { color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .meter__track { +[data-theme*="dark"] .meter__track { background-color: hsl(240 5% 26%); } diff --git a/apps/docs/src/examples/meter.tsx b/apps/docs/src/examples/meter.tsx index b7dec941..d593de34 100644 --- a/apps/docs/src/examples/meter.tsx +++ b/apps/docs/src/examples/meter.tsx @@ -1,7 +1,7 @@ -import { Meter } from "@kobalte/core/meter"; - import style from "./meter.module.css"; +import { Meter } from "@kobalte/core/meter"; + export function BasicExample() { return ( diff --git a/apps/docs/src/examples/navigation-menu.module.css b/apps/docs/src/examples/navigation-menu.module.css index 0ff97763..c3aad7f7 100644 --- a/apps/docs/src/examples/navigation-menu.module.css +++ b/apps/docs/src/examples/navigation-menu.module.css @@ -6,6 +6,7 @@ background-color: white; width: max-content; border-radius: 6px; + list-style: none; } .navigation-menu__root[data-orientation="vertical"] { @@ -33,7 +34,7 @@ } .navigation-menu__trigger[data-highlighted="true"], -[data-kb-theme="dark"] .navigation-menu__trigger[data-highlighted="true"] { +[data-theme*="dark"] .navigation-menu__trigger[data-highlighted="true"] { background-color: #0284c710; } @@ -178,6 +179,7 @@ animation-timing-function: ease; animation-fill-mode: forwards; pointer-events: none; + list-style: none; } .navigation-menu__content[data-expanded] { @@ -349,31 +351,31 @@ } } -[data-kb-theme="dark"] .navigation-menu__trigger { +[data-theme*="dark"] .navigation-menu__trigger { background-color: #27272a; color: #0284c7; } -[data-kb-theme="dark"] .navigation-menu__item-label, -[data-kb-theme="dark"] .navigation-menu__item-description { +[data-theme*="dark"] .navigation-menu__item-label, +[data-theme*="dark"] .navigation-menu__item-description { color: #e9e9ea; } -[data-kb-theme="dark"] .navigation-menu__item:hover, -[data-kb-theme="dark"] .navigation-menu__item:focus { +[data-theme*="dark"] .navigation-menu__item:hover, +[data-theme*="dark"] .navigation-menu__item:focus { background-color: #0284c710; } -[data-kb-theme="dark"] .navigation-menu__root { +[data-theme*="dark"] .navigation-menu__root { background-color: #27272a; } -[data-kb-theme="dark"] .navigation-menu__viewport { +[data-theme*="dark"] .navigation-menu__viewport { background-color: #27272a; border: 1px solid hsl(240 5% 26%); } -[data-kb-theme="dark"] .navigation-menu__item-callout { +[data-theme*="dark"] .navigation-menu__item-callout { background: linear-gradient(135deg, #0284c7 0%, #00345e 100%); } diff --git a/apps/docs/src/examples/navigation-menu.tsx b/apps/docs/src/examples/navigation-menu.tsx index 76a85be4..0713b385 100644 --- a/apps/docs/src/examples/navigation-menu.tsx +++ b/apps/docs/src/examples/navigation-menu.tsx @@ -1,14 +1,15 @@ -import { - NavigationMenu, - type Orientation, -} from "@kobalte/core/navigation-menu"; import { RadioGroup } from "@kobalte/core/radio-group"; -import { For, createSignal } from "solid-js"; +import { createSignal, For } from "solid-js"; import { ChevronDownIcon } from "../components"; import style from "./navigation-menu.module.css"; import radioStyle from "./radio-group.module.css"; +import { + NavigationMenu, + type Orientation, +} from "@kobalte/core/navigation-menu"; + export function BasicExample() { const [orientation, setOrientation] = createSignal("horizontal"); @@ -224,10 +225,10 @@ export function BasicExample() {
- + setOrientation(value)} + onChange={(value) => setOrientation(value as Orientation)} > Orientation diff --git a/apps/docs/src/examples/number-field.module.css b/apps/docs/src/examples/number-field.module.css index 5fbd651e..22aca1a8 100644 --- a/apps/docs/src/examples/number-field.module.css +++ b/apps/docs/src/examples/number-field.module.css @@ -58,13 +58,13 @@ background: rgba(0, 0, 0, 0.2); } -[data-kb-theme="dark"] .number-field__increment, -[data-kb-theme="dark"] .number-field__decrement { +[data-theme*="dark"] .number-field__increment, +[data-theme*="dark"] .number-field__decrement { background: rgba(0, 0, 0, 0.5); } -[data-kb-theme="dark"] .number-field__increment:hover, -[data-kb-theme="dark"] .number-field__decrement:hover { +[data-theme*="dark"] .number-field__increment:hover, +[data-theme*="dark"] .number-field__decrement:hover { background: rgba(0, 0, 0, 0.25); } @@ -117,8 +117,8 @@ border-radius: 0; } -[data-kb-theme="dark"] .number-field__custom-trigger-decrement, -[data-kb-theme="dark"] .number-field__custom-trigger-increment { +[data-theme*="dark"] .number-field__custom-trigger-decrement, +[data-theme*="dark"] .number-field__custom-trigger-increment { border-color: hsl(240 5% 34%); } @@ -156,29 +156,29 @@ user-select: none; } -[data-kb-theme="dark"] .number-field__input { +[data-theme*="dark"] .number-field__input { background-color: hsl(240 4% 16%); border: 1px solid hsl(240 5% 34%); color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .number-field__input:hover { +[data-theme*="dark"] .number-field__input:hover { border-color: hsl(240 4% 46%); } -[data-kb-theme="dark"] .number-field__input[data-invalid] { +[data-theme*="dark"] .number-field__input[data-invalid] { border-color: hsl(0 72% 51%); color: hsl(0 72% 51%); } -[data-kb-theme="dark"] .number-field__input::placeholder { +[data-theme*="dark"] .number-field__input::placeholder { color: hsl(0 100% 100% / 0.5); } -[data-kb-theme="dark"] .number-field__label { +[data-theme*="dark"] .number-field__label { color: hsl(240 5% 84%); } -[data-kb-theme="dark"] .number-field__description { +[data-theme*="dark"] .number-field__description { color: hsl(240 5% 65%); } diff --git a/apps/docs/src/examples/number-field.tsx b/apps/docs/src/examples/number-field.tsx index 64fdaad5..cbfae9ae 100644 --- a/apps/docs/src/examples/number-field.tsx +++ b/apps/docs/src/examples/number-field.tsx @@ -1,9 +1,10 @@ -import { NumberField } from "@kobalte/core/number-field"; import { createSignal } from "solid-js"; import { ArrowIcon } from "../components"; import style from "./number-field.module.css"; +import { NumberField } from "@kobalte/core/number-field"; + export function BasicExample() { return ( @@ -87,7 +88,13 @@ export function ControlledExample() {
-

+

Quantity: {value()}. Raw: {rawValue()}.

@@ -172,7 +179,12 @@ export function HTMLFormExample() { @@ -195,7 +207,7 @@ export function HTMLFormExample() {
-
+
diff --git a/apps/docs/src/examples/otp-field.module.css b/apps/docs/src/examples/otp-field.module.css new file mode 100644 index 00000000..3ded4786 --- /dev/null +++ b/apps/docs/src/examples/otp-field.module.css @@ -0,0 +1,76 @@ +.otp-field { + display: flex; + gap: 8px; + align-items: center; +} + +.otp-field__slot { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 52px; + border-radius: 8px; + border: 1.5px solid hsl(240 6% 90%); + font-size: 20px; + font-weight: 600; + color: hsl(240 4% 16%); + transition: + border-color 150ms, + box-shadow 150ms; + user-select: none; +} + +.otp-field__slot[data-active] { + border-color: hsl(200 98% 39%); + box-shadow: 0 0 0 3px hsl(200 98% 39% / 0.15); + z-index: 1; +} + +.otp-field__caret { + width: 1.5px; + height: 24px; + background-color: hsl(200 98% 39%); + border-radius: 999px; + animation: otp-caret-blink 1.2s ease-out infinite; +} + +@keyframes otp-caret-blink { + 0%, + 70%, + 100% { + opacity: 1; + } + 20%, + 50% { + opacity: 0; + } +} + +[data-kb-theme="dark"] .otp-field__slot { + border-color: hsl(240 5% 34%); + color: hsl(0 100% 100% / 0.9); +} + +[data-kb-theme="dark"] .otp-field__slot[data-active] { + border-color: hsl(200 98% 39%); + box-shadow: 0 0 0 3px hsl(200 98% 39% / 0.2); +} + +/* Animated variant — char span mounts on each keystroke, triggering the animation */ +.otp-field__char { + display: inline-block; + animation: otp-slip-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +@keyframes otp-slip-in { + from { + transform: translateY(8px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} diff --git a/apps/docs/src/examples/otp-field.tsx b/apps/docs/src/examples/otp-field.tsx new file mode 100644 index 00000000..904f4ac8 --- /dev/null +++ b/apps/docs/src/examples/otp-field.tsx @@ -0,0 +1,186 @@ +import { OTPField, useOTPFieldContext } from "@kobalte/core/otp-field"; +import { createSignal, For } from "solid-js"; + +import style from "./otp-field.module.css"; + +function makeSlots(n: number) { + return Array.from({ length: n }, (_, i) => i); +} + +function OtpSlot(props: { index: number }) { + const context = useOTPFieldContext(); + const char = () => context.value()[props.index]; + const isActive = () => context.activeSlots().includes(props.index); + const showCaret = () => + context.isInserting() && context.activeSlots()[0] === props.index; + + return ( +
+ {showCaret() ?
: char()} +
+ ); +} + +export function BasicExample() { + return ( + + + {(i) => } + + ); +} + +export function DefaultValueExample() { + return ( + + + {(i) => } + + ); +} + +export function ControlledExample() { + const [value, setValue] = createSignal(""); + + return ( + <> + + + {(i) => } + +

Value: {value()}

+ + ); +} + +export function OnCompleteExample() { + const [status, setStatus] = createSignal("Enter your 6-digit code."); + + return ( + <> + setStatus(`Code submitted: ${v}`)} + onChange={() => setStatus("Enter your 6-digit code.")} + class={style["otp-field"]} + > + + {(i) => } + +

{status()}

+ + ); +} + +export function CustomPatternExample() { + return ( + + + {(i) => } + + ); +} + +// AnimatedOtpSlot uses an exclusive caret-or-char render so the char +// mounts fresh on every keystroke, which naturally fires the CSS pop-in animation. +function AnimatedOtpSlot(props: { index: number }) { + const context = useOTPFieldContext(); + const char = () => context.value()[props.index]; + const isActive = () => context.activeSlots().includes(props.index); + const showCaret = () => + context.isInserting() && context.activeSlots()[0] === props.index; + + return ( +
+ {showCaret() ? ( +
+ ) : ( + {char()} + )} +
+ ); +} + +export function AnimatedExample() { + return ( + + + {(i) => } + + ); +} + +export function ValidationExample() { + const [value, setValue] = createSignal(""); + + const isInvalid = () => value().length > 0 && value().length < 6; + + return ( +
+ + + Verification code + +
+ + {(i) => } +
+ + Enter the 6-digit code sent to your device. + + + Please enter all 6 digits. + +
+
+ ); +} + +export function HTMLFormExample() { + let formRef: HTMLFormElement | undefined; + + const onSubmit = (e: SubmitEvent) => { + e.preventDefault(); + e.stopPropagation(); + const formData = new FormData(formRef); + alert(JSON.stringify(Object.fromEntries(formData), null, 2)); + }; + + return ( + + + + {(i) => } + +
+ + +
+ + ); +} diff --git a/apps/docs/src/examples/pagination.module.css b/apps/docs/src/examples/pagination.module.css index aed95705..12508339 100644 --- a/apps/docs/src/examples/pagination.module.css +++ b/apps/docs/src/examples/pagination.module.css @@ -1,4 +1,5 @@ .pagination__root > ul { + list-style: none; display: inline-flex; align-items: center; justify-content: space-between; @@ -66,27 +67,27 @@ color: white; } -[data-kb-theme="dark"] .pagination__item { +[data-theme*="dark"] .pagination__item { background-color: hsl(240 4% 16%); border: 1px solid hsl(240 5% 34%); color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .pagination__ellipsis { +[data-theme*="dark"] .pagination__ellipsis { background-color: hsl(240 4% 16%); border: 1px solid hsl(240 5% 34%); color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .pagination__item[aria-current="page"] { +[data-theme*="dark"] .pagination__item[aria-current="page"] { background-color: hsl(201 96% 32%); color: hsla(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .pagination__item:hover { +[data-theme*="dark"] .pagination__item:hover { border-color: hsl(240 4% 46%); } -[data-kb-theme="dark"] .pagination__item:active { +[data-theme*="dark"] .pagination__item:active { background-color: hsl(199 89% 48%); } diff --git a/apps/docs/src/examples/pagination.tsx b/apps/docs/src/examples/pagination.tsx index ad68d45d..ae64bb44 100644 --- a/apps/docs/src/examples/pagination.tsx +++ b/apps/docs/src/examples/pagination.tsx @@ -1,8 +1,8 @@ -import { Pagination } from "@kobalte/core/pagination"; - import { createSignal } from "solid-js"; import style from "./pagination.module.css"; +import { Pagination } from "@kobalte/core/pagination"; + export function BasicExample() { return ( diff --git a/apps/docs/src/examples/progress.module.css b/apps/docs/src/examples/progress.module.css index 2d3cfc54..ead5b431 100644 --- a/apps/docs/src/examples/progress.module.css +++ b/apps/docs/src/examples/progress.module.css @@ -32,11 +32,11 @@ background-color: #16a34a; } -[data-kb-theme="dark"] .progress__label, -[data-kb-theme="dark"] .progress__value-label { +[data-theme*="dark"] .progress__label, +[data-theme*="dark"] .progress__value-label { color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .progress__track { +[data-theme*="dark"] .progress__track { background-color: hsl(240 5% 26%); } diff --git a/apps/docs/src/examples/progress.tsx b/apps/docs/src/examples/progress.tsx index f356d994..50a68e3b 100644 --- a/apps/docs/src/examples/progress.tsx +++ b/apps/docs/src/examples/progress.tsx @@ -1,7 +1,7 @@ -import { Progress } from "@kobalte/core/progress"; - import style from "./progress.module.css"; +import { Progress } from "@kobalte/core/progress"; + export function BasicExample() { return ( diff --git a/apps/docs/src/examples/radio-group.module.css b/apps/docs/src/examples/radio-group.module.css index 5d2145e5..dd96ea9b 100644 --- a/apps/docs/src/examples/radio-group.module.css +++ b/apps/docs/src/examples/radio-group.module.css @@ -73,35 +73,35 @@ user-select: none; } -[data-kb-theme="dark"] .radio-group__label { +[data-theme*="dark"] .radio-group__label { color: hsl(240 5% 84%); } -[data-kb-theme="dark"] .radio-group__description { +[data-theme*="dark"] .radio-group__description { color: hsl(240 5% 65%); } -[data-kb-theme="dark"] .radio__control { +[data-theme*="dark"] .radio__control { border-color: hsl(240 5% 34%); background-color: hsl(240 5% 26%); } -[data-kb-theme="dark"] .radio__control[data-checked] { +[data-theme*="dark"] .radio__control[data-checked] { border-color: hsl(200 98% 39%); background-color: hsl(200 98% 39%); color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .radio__control[data-checked][data-invalid] { +[data-theme*="dark"] .radio__control[data-checked][data-invalid] { border-color: hsl(0 72% 51%); background-color: hsl(0 72% 51%); color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .radio__indicator { +[data-theme*="dark"] .radio__indicator { background-color: hsl(240 4% 16%); } -[data-kb-theme="dark"] .radio__label { +[data-theme*="dark"] .radio__label { color: hsl(240 5% 84%); } diff --git a/apps/docs/src/examples/radio-group.tsx b/apps/docs/src/examples/radio-group.tsx index 316956c9..fd716eb4 100644 --- a/apps/docs/src/examples/radio-group.tsx +++ b/apps/docs/src/examples/radio-group.tsx @@ -1,5 +1,5 @@ import { RadioGroup } from "@kobalte/core/radio-group"; -import { For, createSignal } from "solid-js"; +import { createSignal, For } from "solid-js"; import style from "./radio-group.module.css"; @@ -82,7 +82,15 @@ export function ControlledExample() {
-

Your favorite fruit is: {value()}.

+

+ Your favorite fruit is: {value()}. +

); } @@ -166,7 +174,12 @@ export function HTMLFormExample() {
@@ -188,7 +201,7 @@ export function HTMLFormExample() {
-
+
diff --git a/apps/docs/src/examples/rating.module.css b/apps/docs/src/examples/rating.module.css index ef021968..47fb517d 100644 --- a/apps/docs/src/examples/rating.module.css +++ b/apps/docs/src/examples/rating.module.css @@ -37,7 +37,7 @@ outline: none; } -[data-kb-theme="dark"] .rating-item { +[data-theme*="dark"] .rating-item { fill: hsl(240 5% 26%); } @@ -49,14 +49,14 @@ fill: hsl(240 6% 90%); } -[data-kb-theme="dark"] .half-star-icon > path + path { +[data-theme*="dark"] .half-star-icon > path + path { fill: hsl(240 5% 26%); } -[data-kb-theme="dark"] .rating__label { +[data-theme*="dark"] .rating__label { color: hsl(240 5% 84%); } -[data-kb-theme="dark"] .rating__description { +[data-theme*="dark"] .rating__description { color: hsl(240 5% 65%); } diff --git a/apps/docs/src/examples/rating.tsx b/apps/docs/src/examples/rating.tsx index d0261653..93698623 100644 --- a/apps/docs/src/examples/rating.tsx +++ b/apps/docs/src/examples/rating.tsx @@ -1,8 +1,9 @@ import { Index, createSignal } from "solid-js"; -import { Rating } from "../../../../packages/core/src/rating"; import style from "./rating.module.css"; +import { Rating } from "@kobalte/core/rating"; + export function BasicExample() { return ( @@ -58,7 +59,15 @@ export function ControlledExample() { -

Your rating is: {value()}/5

+

+ Your rating is: {value()}/5 +

); } @@ -148,7 +157,12 @@ export function HTMLFormExample() { @@ -164,7 +178,7 @@ export function HTMLFormExample() { -
+
diff --git a/apps/docs/src/examples/search.module.css b/apps/docs/src/examples/search.module.css index 2513e9b1..052f814d 100644 --- a/apps/docs/src/examples/search.module.css +++ b/apps/docs/src/examples/search.module.css @@ -196,41 +196,41 @@ } } -[data-kb-theme="dark"] .search__control { +[data-theme*="dark"] .search__control { background-color: hsl(240 4% 16%); border: 1px solid hsl(240 5% 34%); color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .search__control:hover { +[data-theme*="dark"] .search__control:hover { border-color: hsl(240 4% 46%); } -[data-kb-theme="dark"] .search__input::placeholder { +[data-theme*="dark"] .search__input::placeholder { color: hsl(0 100% 100% / 0.5); } -[data-kb-theme="dark"] .search__indicator { +[data-theme*="dark"] .search__indicator { background: hsl(240 5% 26%); border-right: 1px solid hsl(240 5% 34%); color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .search__content { +[data-theme*="dark"] .search__content { border: 1px solid hsl(240 5% 26%); background-color: hsl(240 4% 16%); box-shadow: none; } -[data-kb-theme="dark"] .search__section { +[data-theme*="dark"] .search__section { color: hsl(0 100% 100% / 0.7); } -[data-kb-theme="dark"] .search__item { +[data-theme*="dark"] .search__item { color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .search__description { +[data-theme*="dark"] .search__description { color: hsl(0 100% 100% / 0.7); } @@ -283,13 +283,13 @@ color: hsl(240 4% 46%); } -[data-kb-theme="dark"] .search__control_inline { +[data-theme*="dark"] .search__control_inline { background-color: hsl(240 4% 16%); border: 1px solid hsl(240 5% 34%); color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .search__content_inline { +[data-theme*="dark"] .search__content_inline { background-color: hsl(240 4% 16%); border: 1px solid hsl(240 5% 34%); } diff --git a/apps/docs/src/examples/search.tsx b/apps/docs/src/examples/search.tsx index 6c58ebc1..be7fc843 100644 --- a/apps/docs/src/examples/search.tsx +++ b/apps/docs/src/examples/search.tsx @@ -1,6 +1,5 @@ import MiniSearch from "minisearch"; import { createSignal } from "solid-js"; -import { Search } from "../../../../packages/core/src/search"; import { MagnifyingGlassIcon, ReloadIcon } from "../components"; import style from "./search.module.css"; @@ -550,6 +549,8 @@ const queryEmojiData = (query: string, numSuggestions = 20) => { .slice(0, numSuggestions) as never as EmojiDatum[]; }; +import { Search } from "@kobalte/core/search"; + export function BasicExample() { const [options, setOptions] = createSignal([]); const [emoji, setEmoji] = createSignal(); diff --git a/apps/docs/src/examples/segmented-control.module.css b/apps/docs/src/examples/segmented-control.module.css index 34ecaf9f..7714d19b 100644 --- a/apps/docs/src/examples/segmented-control.module.css +++ b/apps/docs/src/examples/segmented-control.module.css @@ -16,7 +16,7 @@ --indicator-focus-shadow-color: hsl(200 98% 39%); } -[data-kb-theme="dark"] .segmented-control { +[data-theme*="dark"] .segmented-control { --border-color: hsl(240 6% 26%); --border-active-color: hsl(240 6% 41%); --label-color: hsl(240 5% 84%); @@ -93,7 +93,8 @@ box-shadow: 0px 1px 3px rgba(0 0 0 / 8%), 0px 2px 8px rgba(0 0 0 / 12%), - inset 0px 0px 0px var(--indicator-focus-shadow-width, 0px) var(--indicator-focus-shadow-color), + inset 0px 0px 0px var(--indicator-focus-shadow-width, 0px) + var(--indicator-focus-shadow-color), inset 0px 0px 0px var(--border-width) var(--border-active-color); content: ""; opacity: var(--pane-opacity, 1); @@ -132,7 +133,8 @@ } .segmented-control__item:has(.segmented-control__item-input:checked)::before, -.segmented-control__item:has(.segmented-control__item-input:checked) + ::before { +.segmented-control__item:has(.segmented-control__item-input:checked) + + ::before { opacity: 0; } @@ -184,16 +186,19 @@ opacity: 0.5; } -.segmented-control__item-input:not(:checked, :disabled) + .segmented-control__item-label { +.segmented-control__item-input:not(:checked, :disabled) + + .segmented-control__item-label { cursor: pointer; opacity: var(--item-label-opacity, 1); user-select: none; } -.segmented-control__item-input:not(:checked, :disabled) + .segmented-control__item-label:hover { +.segmented-control__item-input:not(:checked, :disabled) + + .segmented-control__item-label:hover { --item-label-opacity: 0.75; } -.segmented-control__item-input:not(:checked, :disabled) + .segmented-control__item-label:active { +.segmented-control__item-input:not(:checked, :disabled) + + .segmented-control__item-label:active { --item-label-opacity: 0.5; } diff --git a/apps/docs/src/examples/segmented-control.tsx b/apps/docs/src/examples/segmented-control.tsx index ca0a2471..d3b20d64 100644 --- a/apps/docs/src/examples/segmented-control.tsx +++ b/apps/docs/src/examples/segmented-control.tsx @@ -1,5 +1,5 @@ import { SegmentedControl } from "@kobalte/core/segmented-control"; -import { For, createSignal } from "solid-js"; +import { createSignal, For } from "solid-js"; import style from "./segmented-control.module.css"; @@ -115,7 +115,15 @@ export function ControlledExample() {
-

Your favorite fruit is: {value()}.

+

+ Your favorite fruit is: {value()}. +

); } @@ -223,7 +231,12 @@ export function HTMLFormExample() {
-
+
diff --git a/apps/docs/src/examples/select.module.css b/apps/docs/src/examples/select.module.css index efdb84ca..5206ed3d 100644 --- a/apps/docs/src/examples/select.module.css +++ b/apps/docs/src/examples/select.module.css @@ -162,39 +162,39 @@ } } -[data-kb-theme="dark"] .select__trigger { +[data-theme*="dark"] .select__trigger { background-color: hsl(240 4% 16%); border: 1px solid hsl(240 5% 34%); color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .select__trigger:hover { +[data-theme*="dark"] .select__trigger:hover { border-color: hsl(240 4% 46%); } -[data-kb-theme="dark"] .select__trigger[data-invalid] { +[data-theme*="dark"] .select__trigger[data-invalid] { border-color: hsl(0 72% 51%); color: hsl(0 72% 51%); } -[data-kb-theme="dark"] .select__value[data-placeholder-shown] { +[data-theme*="dark"] .select__value[data-placeholder-shown] { color: hsl(0 100% 100% / 0.5); } -[data-kb-theme="dark"] .select__content { +[data-theme*="dark"] .select__content { border: 1px solid hsl(240 5% 26%); background-color: hsl(240 4% 16%); box-shadow: none; } -[data-kb-theme="dark"] .select__section { +[data-theme*="dark"] .select__section { color: hsl(0 100% 100% / 0.7); } -[data-kb-theme="dark"] .select__item { +[data-theme*="dark"] .select__item { color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .select__description { +[data-theme*="dark"] .select__description { color: hsl(0 100% 100% / 0.7); } diff --git a/apps/docs/src/examples/select.tsx b/apps/docs/src/examples/select.tsx index d881ff92..21917228 100644 --- a/apps/docs/src/examples/select.tsx +++ b/apps/docs/src/examples/select.tsx @@ -1,12 +1,13 @@ -import { Select } from "@kobalte/core/select"; import { createVirtualizer } from "@tanstack/solid-virtual"; -import { For, createSignal } from "solid-js"; +import { createSignal, For } from "solid-js"; import { CaretSortIcon, CheckIcon, CrossIcon } from "../components"; import style from "./select.module.css"; const STRING_OPTIONS = ["Apple", "Banana", "Blueberry", "Grapes", "Pineapple"]; +import { Select } from "@kobalte/core/select"; + export function BasicExample() { return ( -

Your favorite fruit is: {value()}.

+

+ Your favorite fruit is: {value()}. +

); } @@ -197,7 +206,12 @@ export function HTMLFormExample() { -
+
@@ -382,20 +396,43 @@ export function MultipleSelectionExample() { class={style.select__value}> {(state) => ( <> -
+
{(option) => ( e.stopPropagation()} > {option} )} @@ -405,9 +442,17 @@ export function MultipleSelectionExample() { type="button" onPointerDown={(e) => e.stopPropagation()} onClick={state.clear} - class="ml-auto mr-2 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-600 p-1" + style={{ + "margin-left": "auto", + "margin-right": "8px", + "border-radius": "9999px", + padding: "4px", + cursor: "pointer", + background: "none", + border: "none", + }} > - + )} @@ -422,7 +467,13 @@ export function MultipleSelectionExample() { -

+

Your favorite fruits are: {values().join(", ")}.

diff --git a/apps/docs/src/examples/separator.module.css b/apps/docs/src/examples/separator.module.css index 7abcedb2..702f6958 100644 --- a/apps/docs/src/examples/separator.module.css +++ b/apps/docs/src/examples/separator.module.css @@ -8,6 +8,6 @@ width: 100%; } -[data-kb-theme="dark"] .separator { +[data-theme*="dark"] .separator { background-color: hsl(240 5% 26%); } diff --git a/apps/docs/src/examples/separator.tsx b/apps/docs/src/examples/separator.tsx index df7439f2..077bb02e 100644 --- a/apps/docs/src/examples/separator.tsx +++ b/apps/docs/src/examples/separator.tsx @@ -1,10 +1,10 @@ -import { Separator } from "@kobalte/core/separator"; - import style from "./separator.module.css"; +import { Separator } from "@kobalte/core/separator"; + export function BasicExample() { return ( -
+
Content above Content below diff --git a/apps/docs/src/examples/skeleton.tsx b/apps/docs/src/examples/skeleton.tsx index 2cd54cd6..389437a9 100644 --- a/apps/docs/src/examples/skeleton.tsx +++ b/apps/docs/src/examples/skeleton.tsx @@ -1,10 +1,11 @@ import { Image } from "@kobalte/core/image"; -import { Skeleton } from "@kobalte/core/skeleton"; import { ToggleButton } from "@kobalte/core/toggle-button"; import { createSignal } from "solid-js"; import style from "./skeleton.module.css"; +import { Skeleton } from "@kobalte/core/skeleton"; + export function BasicExample() { return ( @@ -23,7 +24,7 @@ export function MultipleSkeletonsExample() { diff --git a/apps/docs/src/examples/slider.module.css b/apps/docs/src/examples/slider.module.css index 7e817648..4079b4b1 100644 --- a/apps/docs/src/examples/slider.module.css +++ b/apps/docs/src/examples/slider.module.css @@ -66,6 +66,6 @@ justify-content: space-between; } -[data-kb-theme="dark"] .SliderTrack { +[data-theme*="dark"] .SliderTrack { background-color: hsl(240 5% 26%); } diff --git a/apps/docs/src/examples/slider.tsx b/apps/docs/src/examples/slider.tsx index e5ba9c7c..943333b4 100644 --- a/apps/docs/src/examples/slider.tsx +++ b/apps/docs/src/examples/slider.tsx @@ -1,7 +1,8 @@ -import { Slider } from "@kobalte/core/slider"; import { createSignal } from "solid-js"; import style from "./slider.module.css"; +import { Slider } from "@kobalte/core/slider"; + export function BasicExample() { return ( @@ -41,7 +42,7 @@ export function MultipleThumbsExample() { export function StepExample() { return ( -
+
Step size 8 diff --git a/apps/docs/src/examples/switch.module.css b/apps/docs/src/examples/switch.module.css index 795a21c4..005e6bdf 100644 --- a/apps/docs/src/examples/switch.module.css +++ b/apps/docs/src/examples/switch.module.css @@ -62,28 +62,28 @@ user-select: none; } -[data-kb-theme="dark"] .switch__control { +[data-theme*="dark"] .switch__control { border-color: hsl(240 5% 34%); background-color: hsl(240 5% 26%); } -[data-kb-theme="dark"] .switch__control[data-checked] { +[data-theme*="dark"] .switch__control[data-checked] { border-color: hsl(200 98% 39%); background-color: hsl(200 98% 39%); } -[data-kb-theme="dark"] .switch__control[data-invalid] { +[data-theme*="dark"] .switch__control[data-invalid] { border-color: hsl(0 72% 51%); } -[data-kb-theme="dark"] .switch__thumb { +[data-theme*="dark"] .switch__thumb { background-color: hsl(240 5% 84%); } -[data-kb-theme="dark"] .switch__label { +[data-theme*="dark"] .switch__label { color: hsl(240 5% 84%); } -[data-kb-theme="dark"] .switch__description { +[data-theme*="dark"] .switch__description { color: hsl(240 5% 65%); } diff --git a/apps/docs/src/examples/switch.tsx b/apps/docs/src/examples/switch.tsx index e756cde2..f2427567 100644 --- a/apps/docs/src/examples/switch.tsx +++ b/apps/docs/src/examples/switch.tsx @@ -1,8 +1,9 @@ -import { Switch } from "@kobalte/core/switch"; import { createSignal } from "solid-js"; import style from "./switch.module.css"; +import { Switch } from "@kobalte/core/switch"; + export function BasicExample() { return ( @@ -39,7 +40,9 @@ export function ControlledExample() { -

+

Airplane mode is {checked() ? "active" : "inactive"}.

@@ -49,7 +52,14 @@ export function ControlledExample() { export function DescriptionExample() { return ( -
+
Airplane mode Disable all network connections. @@ -73,7 +83,14 @@ export function ErrorMessageExample() { onChange={setChecked} validationState={!checked() ? "invalid" : "valid"} > -
+
Airplane mode You must enable airplane mode. @@ -103,7 +120,12 @@ export function HTMLFormExample() { Airplane mode @@ -112,7 +134,7 @@ export function HTMLFormExample() { -
+
diff --git a/apps/docs/src/examples/tabs.module.css b/apps/docs/src/examples/tabs.module.css index b351303a..43ae4dad 100644 --- a/apps/docs/src/examples/tabs.module.css +++ b/apps/docs/src/examples/tabs.module.css @@ -63,18 +63,18 @@ padding: 16px; } -[data-kb-theme="dark"] .tabs__list { +[data-theme*="dark"] .tabs__list { border-color: hsl(240 5% 26%); } -[data-kb-theme="dark"] .tabs__trigger { +[data-theme*="dark"] .tabs__trigger { color: rgb(255 255 255 / 0.9); } -[data-kb-theme="dark"] .tabs__trigger:hover { +[data-theme*="dark"] .tabs__trigger:hover { background-color: hsl(240 4% 16%); } -[data-kb-theme="dark"] .tabs__trigger:focus-visible { +[data-theme*="dark"] .tabs__trigger:focus-visible { background-color: hsl(240 4% 16%); } diff --git a/apps/docs/src/examples/tabs.tsx b/apps/docs/src/examples/tabs.tsx index 96e1b712..bbf0cbee 100644 --- a/apps/docs/src/examples/tabs.tsx +++ b/apps/docs/src/examples/tabs.tsx @@ -1,8 +1,9 @@ -import { Tabs } from "@kobalte/core/tabs"; -import { For, createSignal } from "solid-js"; +import { createSignal, For } from "solid-js"; import style from "./tabs.module.css"; +import { Tabs } from "@kobalte/core/tabs"; + export function BasicExample() { return ( @@ -114,7 +115,11 @@ export function ControlledExample() { Contact details -

Selected tab: {selectedTab()}

+

+ Selected tab: {selectedTab()} +

); } @@ -139,7 +144,15 @@ export function FocusableContentExample() { @@ -182,17 +195,44 @@ export function DynamicContentExample() { return ( <> -
+
diff --git a/apps/docs/src/examples/time-field.module.css b/apps/docs/src/examples/time-field.module.css index 3fccb4d3..a328a823 100644 --- a/apps/docs/src/examples/time-field.module.css +++ b/apps/docs/src/examples/time-field.module.css @@ -67,11 +67,11 @@ user-select: none; } -[data-kb-theme="dark"] .time-field__label { +[data-theme*="dark"] .time-field__label { color: hsl(240 5% 84%); } -[data-kb-theme="dark"] .time-field__field { +[data-theme*="dark"] .time-field__field { background-color: hsl(240 4% 16%); border: 1px solid hsl(240 5% 34%); color: hsl(0 100% 100% / 0.9); @@ -82,10 +82,10 @@ outline-color: hsl(0 72% 51%); } -[data-kb-theme="dark"] .time-field__segment[data-placeholder] { +[data-theme*="dark"] .time-field__segment[data-placeholder] { color: hsl(0 100% 100% / 0.5); } -[data-kb-theme="dark"] .time-field__description { +[data-theme*="dark"] .time-field__description { color: hsl(240 5% 65%); } diff --git a/apps/docs/src/examples/time-field.tsx b/apps/docs/src/examples/time-field.tsx index 94f4bc0e..d7f7146d 100644 --- a/apps/docs/src/examples/time-field.tsx +++ b/apps/docs/src/examples/time-field.tsx @@ -1,7 +1,8 @@ import { Show, createSignal } from "solid-js"; -import { TimeField } from "../../../../packages/core/src/time-field"; import style from "./time-field.module.css"; +import { TimeField } from "@kobalte/core/time-field"; + export function BasicExample() { return ( @@ -57,7 +58,13 @@ export function ControlledValueExample() { )} -

+

Selected time:{" "} {JSON.stringify(value())} @@ -197,7 +204,12 @@ export function HTMLFormExample() { @@ -210,7 +222,7 @@ export function HTMLFormExample() { -

+
diff --git a/apps/docs/src/examples/toast.module.css b/apps/docs/src/examples/toast.module.css index 42dfc51f..c34f44e4 100644 --- a/apps/docs/src/examples/toast.module.css +++ b/apps/docs/src/examples/toast.module.css @@ -135,24 +135,24 @@ } } -[data-kb-theme="dark"] .toast { +[data-theme*="dark"] .toast { border: 1px solid hsl(240 5% 26%); background-color: hsl(240 4% 16%); box-shadow: none; } -[data-kb-theme="dark"] .toast__close-button { +[data-theme*="dark"] .toast__close-button { color: hsl(0 100% 100% / 0.8); } -[data-kb-theme="dark"] .toast__title { +[data-theme*="dark"] .toast__title { color: hsl(0 100% 100% / 0.9); } -[data-kb-theme="dark"] .toast__description { +[data-theme*="dark"] .toast__description { color: hsl(0 100% 100% / 0.7); } -[data-kb-theme="dark"] .toast__progress-track { +[data-theme*="dark"] .toast__progress-track { background-color: hsl(240 5% 26%); } diff --git a/apps/docs/src/examples/toast.tsx b/apps/docs/src/examples/toast.tsx index 3f37761d..6068c06c 100644 --- a/apps/docs/src/examples/toast.tsx +++ b/apps/docs/src/examples/toast.tsx @@ -1,8 +1,8 @@ -import { Toast, toaster } from "@kobalte/core/toast"; - import { CrossIcon } from "../components"; import style from "./toast.module.css"; +import { Toast, toaster } from "@kobalte/core/toast"; + export function BasicExample() { let id: number; @@ -53,7 +53,7 @@ export function BasicExample() { }; return ( -
+
@@ -96,7 +96,7 @@ export function MultipleRegionsExample() { }; return ( -
+
- {local.children} - - ); - }, - table: (props: ComponentProps<"table">) => { - const [local, others] = splitProps(props, ["class"]); - - return ( -
- - - ); - }, - a: (props: ComponentProps<"a">) => { - return ; - }, -}; diff --git a/apps/docs/src/model/navigation.ts b/apps/docs/src/model/navigation.ts deleted file mode 100644 index d981d5a4..00000000 --- a/apps/docs/src/model/navigation.ts +++ /dev/null @@ -1,10 +0,0 @@ -interface NavLink { - title: string; - href: string; - status?: "new" | "updated" | "unreleased"; -} - -export interface NavSection { - title: string; - links: NavLink[]; -} diff --git a/apps/docs/src/root.css b/apps/docs/src/root.css deleted file mode 100644 index 36dc7b3c..00000000 --- a/apps/docs/src/root.css +++ /dev/null @@ -1,751 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@font-face { - font-family: "Lexend"; - font-style: normal; - font-weight: 100 900; - font-display: swap; - src: url("/fonts/lexend.woff2") format("woff2"); -} -@font-face { - font-family: "Inter"; - font-weight: 100 900; - font-display: block; - font-style: normal; - font-named-instance: "Regular"; - src: url("/fonts/Inter-roman.var.woff2") format("woff2"); -} -@font-face { - font-family: "Inter"; - font-weight: 100 900; - font-display: block; - font-style: italic; - font-named-instance: "Italic"; - src: url("/fonts/Inter-italic.var.woff2") format("woff2"); -} - -@layer base { - /* Hide scrollbar for Chrome, Safari and Opera */ - .hide-scrollbar::-webkit-scrollbar { - display: none; - } - - /* Hide scrollbar for IE, Edge and Firefox */ - .hide-scrollbar { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ - } - - .kb-button { - appearance: none; - display: inline-flex; - justify-content: center; - align-items: center; - height: 40px; - width: auto; - outline: none; - border: 1px solid hsl(240 5% 84%); - border-radius: 6px; - padding: 0 16px; - background-color: transparent; - color: hsl(240 6% 10%); - font-size: 16px; - line-height: 0; - transition: background-color; - } - - .kb-button:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - .kb-button:hover { - background-color: hsl(0 0% 98%); - } - - .kb-button:focus-visible { - outline: 2px solid hsl(200 98% 39%); - outline-offset: 2px; - } - - .kb-button:active { - background-color: hsl(240 6% 90%); - } - - [data-kb-theme="dark"] .kb-button { - border-color: transparent; - background-color: hsl(240 4% 16%); - color: hsla(0 100% 100% / 0.9); - } - - [data-kb-theme="dark"] .kb-button:hover { - background-color: hsl(240 5% 34%); - } - - [data-kb-theme="dark"] .kb-button:active { - background-color: hsl(240 4% 46%); - } - - .kb-button-primary { - appearance: none; - display: inline-flex; - justify-content: center; - align-items: center; - height: 40px; - width: auto; - outline: none; - border-radius: 6px; - padding: 0 16px; - background-color: hsl(200 98% 39%); - color: white; - font-size: 16px; - line-height: 0; - transition: 250ms background-color; - } - - .kb-button-primary:hover { - background-color: hsl(201 96% 32%); - } - - .kb-button-primary:focus-visible { - outline: 2px solid hsl(200 98% 39%); - outline-offset: 2px; - } - - .kb-button-primary:active { - background-color: hsl(201 90% 27%); - } - - [data-kb-theme="dark"] .kb-button-primary { - background-color: hsl(201 96% 32%); - color: hsla(0 100% 100% / 0.9); - } - - [data-kb-theme="dark"] .kb-button-primary:hover { - background-color: hsl(200 98% 39%); - } - - [data-kb-theme="dark"] .kb-button-primary:active { - background-color: hsl(199 89% 48%); - } - - .kb-table .kb-code { - @apply bg-transparent dark:bg-transparent p-0 text-sky-700; - } - - .kb-tabs-snippets pre.shiki { - margin: 0; - border: none; - border-top-right-radius: 0; - border-top-left-radius: 0; - } - - .kb-preview + .kb-tabs-snippets { - margin: 0; - border-top: none; - border-top-right-radius: 0; - border-top-left-radius: 0; - } -} - -/* ------------------------------------------------------------------------------------------------- - * DocSearch - * Credit: https://github.com/vuejs/vitepress/blob/83ce1b8c5e95d2e29e733d9312f514d725fe7f0b/src/client/theme-default/components/VPNavBarSearch.vue - * -----------------------------------------------------------------------------------------------*/ - -:root { - --kb-docsearch-text-light-1: rgba(60, 60, 67); - --kb-docsearch-text-light-2: rgba(60, 60, 67, 0.75); - --kb-docsearch-text-dark-1: rgba(255, 255, 245, 0.86); - --kb-docsearch-text-dark-2: rgba(235, 235, 245, 0.6); - --kb-docsearch-bg: #ffffff; - --kb-docsearch-bg-soft: #f6f6f7; - --kb-docsearch-bg-soft-mute: #e3e3e5; - --kb-docsearch-bg-alt: #f6f6f7; - --kb-docsearch-divider: rgba(60, 60, 67, 0.12); - --kb-docsearch-text-1: var(--kb-docsearch-text-light-1); - --kb-docsearch-text-2: var(--kb-docsearch-text-light-2); - --kb-docsearch-brand: #0284c7; -} - -[data-kb-theme="dark"] { - --kb-docsearch-bg: #1e1e20; - --kb-docsearch-bg-soft: #252529; - --kb-docsearch-bg-soft-mute: #313136; - --kb-docsearch-bg-alt: #27272a; - --kb-docsearch-divider: rgba(82, 82, 89, 0.32); - --kb-docsearch-text-1: var(--kb-docsearch-text-dark-1); - --kb-docsearch-text-2: var(--kb-docsearch-text-dark-2); -} - -.DocSearch { - --docsearch-primary-color: var(--kb-docsearch-brand); - --docsearch-highlight-color: var(--docsearch-primary-color); - --docsearch-text-color: var(--kb-docsearch-text-1); - --docsearch-muted-color: var(--kb-docsearch-text-2); - --docsearch-searchbox-shadow: none; - --docsearch-searchbox-focus-background: transparent; - --docsearch-key-gradient: transparent; - --docsearch-key-shadow: none; - --docsearch-modal-background: var(--kb-docsearch-bg-soft); - --docsearch-footer-background: var(--kb-docsearch-bg); -} - -[data-kb-theme="dark"] .DocSearch { - --docsearch-modal-shadow: none; - --docsearch-footer-shadow: none; - --docsearch-logo-color: var(--kb-docsearch-text-2); - --docsearch-hit-background: var(--kb-docsearch-bg-soft-mute); - --docsearch-hit-color: var(--kb-docsearch-text-2); - --docsearch-hit-shadow: none; -} - -.DocSearch-Button { - display: flex; - justify-content: center; - align-items: center; - margin: 0; - padding: 0; - width: 32px; - height: 40px; - background: transparent; - transition: border-color 0.25s; -} - -.DocSearch-Button:hover { - background: transparent; -} - -.DocSearch-Button:focus { - outline: 1px dotted; - outline: 5px auto -webkit-focus-ring-color; -} - -.DocSearch-Button:focus:not(:focus-visible) { - outline: none !important; -} - -@media (min-width: 768px) { - .DocSearch-Button.DocSearch-Button { - justify-content: flex-start; - border: 1px solid transparent; - border-radius: 8px; - padding: 0 10px 0 12px; - width: 100%; - height: 40px; - background-color: var(--kb-docsearch-bg-alt); - } - - .DocSearch-Button:hover { - border-color: var(--kb-docsearch-brand); - background: var(--kb-docsearch-bg-alt); - } -} - -.DocSearch-Button .DocSearch-Button-Container { - display: flex; - align-items: center; -} - -.DocSearch-Button .DocSearch-Search-Icon { - position: relative; - width: 16px; - height: 16px; - color: var(--kb-docsearch-text-1); - fill: currentColor; - transition: color 0.5s; -} - -.DocSearch-Button:hover .DocSearch-Search-Icon { - color: var(--kb-docsearch-text-1); -} - -@media (min-width: 768px) { - .DocSearch-Button .DocSearch-Search-Icon { - top: 1px; - margin-right: 8px; - width: 14px; - height: 14px; - color: var(--kb-docsearch-text-2); - } -} - -.DocSearch-Button .DocSearch-Button-Placeholder { - display: none; - margin-top: 2px; - padding: 0 16px 0 0; - font-size: 13px; - font-weight: 500; - color: var(--kb-docsearch-text-2); - transition: color 0.5s; -} - -.DocSearch-Button:hover .DocSearch-Button-Placeholder { - color: var(--kb-docsearch-text-1); -} - -@media (min-width: 768px) { - .DocSearch-Button .DocSearch-Button-Placeholder { - display: inline-block; - } -} - -.DocSearch-Button .DocSearch-Button-Keys { - /*rtl:ignore*/ - direction: ltr; - display: none; - min-width: auto; -} - -@media (min-width: 768px) { - .DocSearch-Button .DocSearch-Button-Keys { - display: flex; - align-items: center; - } -} - -.DocSearch-Button .DocSearch-Button-Key { - @apply font-sans; - display: block; - margin: 2px 0 0 0; - border: 1px solid var(--kb-docsearch-divider); - /*rtl:begin:ignore*/ - border-right: none; - border-radius: 4px 0 0 4px; - padding-left: 6px; - /*rtl:end:ignore*/ - min-width: 0; - width: auto; - height: 22px; - line-height: 22px; - font-size: 12px; - font-weight: 500; - transition: - color 0.5s, - border-color 0.5s; -} - -.DocSearch-Button .DocSearch-Button-Key + .DocSearch-Button-Key { - /*rtl:begin:ignore*/ - border-right: 1px solid var(--kb-docsearch-divider); - border-left: none; - border-radius: 0 4px 4px 0; - padding-left: 2px; - padding-right: 6px; - /*rtl:end:ignore*/ -} -.DocSearch-Button .DocSearch-Button-Key:first-child { - font-size: 1px; - letter-spacing: -12px; - color: transparent; -} - -.DocSearch-Button .DocSearch-Button-Key:first-child:after { - content: "⌘"; - font-size: 12px; - letter-spacing: normal; - color: var(--docsearch-muted-color); -} - -.DocSearch-Button .DocSearch-Button-Key:first-child > * { - display: none; -} - -[data-kb-theme="dark"] .DocSearch-Footer { - border-top: 1px solid var(--kb-docsearch-divider); -} - -.DocSearch-Form { - border: 1px solid var(--kb-docsearch-brand); - background-color: #fff; -} - -[data-kb-theme="dark"] .DocSearch-Form { - background-color: var(--kb-docsearch-bg-soft-mute); -} - -.DocSearch-Screen-Icon > svg { - margin: auto; -} - -/* ------------------------------------------------------------------------------------------------- - * Shiki Twoslash CSS - * -----------------------------------------------------------------------------------------------*/ - -.kb-preview + pre.shiki, -.kb-preview + pre.shiki + pre.shiki { - margin: 0 0 16px 0; - padding: 16px; - border-top: none; - border-top-right-radius: 0; - border-top-left-radius: 0; -} - -pre.shiki .kb-copy-btn { - opacity: 0; -} - -pre.shiki:hover .kb-copy-btn { - opacity: 1; -} - -pre { - /* In theory shiki will overwrite these, but this is to make sure there are defaults regardless */ - background-color: white; - color: black; - - /* Give it some space to breathe */ - padding: 12px; - - /* All code samples get a grey border, twoslash ones get a different color */ - border-left: 1px solid #999; - border-bottom: 1px solid #999; - - margin-top: 1rem; - margin-bottom: 1rem; - - /* Important to allow the code to move horizontally; */ - overflow-x: auto; - position: relative; -} - -[data-kb-theme="light"] .shiki.github-dark { - display: none !important; -} - -[data-kb-theme="dark"] .shiki.github-light { - display: none !important; -} - -pre.shiki { - overflow-x: auto; - - display: flex; - align-items: center; - - border-radius: 8px; - border: 1px solid #e4e4e7; - - background-color: #fafafa !important; - - padding: 12px; - - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", - "Courier New", monospace; -} - -[data-kb-theme="dark"] pre.shiki { - border-color: #3f3f46; - background-color: #27272a !important; -} - -/* Reset mdx-components `code`. */ -pre.shiki code { - font-size: 14px; - border-radius: initial; - background: initial; - padding-inline-start: initial; - padding-inline-end: initial; - padding-top: initial; - padding-bottom: initial; - overflow-wrap: initial; -} - -pre.shiki:hover .dim { - opacity: 1; -} -pre.shiki div.dim { - opacity: 0.7; -} -pre.shiki div.dim, -pre.shiki div.highlight { - margin: 0; - padding: 0; -} -pre.shiki div.highlight { - opacity: 1; - background-color: rgb(224 242 254 / 0.8); -} -[data-kb-theme="dark"] pre.shiki div.highlight { - background-color: rgb(12 74 110 / 0.4); -} -pre.shiki div.line { - min-height: 1rem; -} - -/** Don't show the language identifiers */ -pre.shiki .language-id { - display: none; -} - -/* Visually differentiates twoslash code samples */ -pre.twoslash { - border-color: #719af4; -} - -/** When you mouse over the pre, show the underlines */ -pre.twoslash:hover data-lsp { - border-color: #747474; -} - -/** The tooltip-like which provides the LSP response */ -pre.twoslash data-lsp:hover::before { - content: attr(lsp); - position: absolute; - transform: translate(0, 1rem); - - background-color: #3f3f3f; - color: #fff; - text-align: left; - padding: 5px 8px; - border-radius: 2px; - font-family: - "JetBrains Mono", - Menlo, - Monaco, - Consolas, - Courier New, - monospace; - font-size: 14px; - white-space: pre-wrap; - z-index: 100; -} - -pre .code-container { - overflow: auto; - width: 100%; -} -/* The try button */ -pre .code-container > a { - position: absolute; - right: 8px; - bottom: 8px; - border-radius: 4px; - border: 1px solid #719af4; - padding: 0 8px; - color: #719af4; - text-decoration: none; - opacity: 0; - transition-timing-function: ease; - transition: opacity 0.3s; -} -/* Respect no animations */ -@media (prefers-reduced-motion: reduce) { - pre .code-container > a { - transition: none; - } -} -pre .code-container > a:hover { - color: white; - background-color: #719af4; -} -pre .code-container:hover a { - opacity: 1; -} - -pre code { - font-size: 14px; - font-family: - "JetBrains Mono", - Menlo, - Monaco, - Consolas, - Courier New, - monospace; - white-space: pre; - -webkit-overflow-scrolling: touch; -} -pre code a { - text-decoration: none; -} -pre data-err { - /* Extracted from VS Code */ - background: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%206%203'%20enable-background%3D'new%200%200%206%203'%20height%3D'3'%20width%3D'6'%3E%3Cg%20fill%3D'%23c94824'%3E%3Cpolygon%20points%3D'5.5%2C0%202.5%2C3%201.1%2C3%204.1%2C0'%2F%3E%3Cpolygon%20points%3D'4%2C0%206%2C2%206%2C0.6%205.4%2C0'%2F%3E%3Cpolygon%20points%3D'0%2C2%201%2C3%202.4%2C3%200%2C0.6'%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E") - repeat-x bottom left; - padding-bottom: 3px; -} -pre .query { - margin-bottom: 10px; - color: #137998; - display: inline-block; -} - -/* In order to have the 'popped out' style design and to not break the layout - /* we need to place a fake and un-selectable copy of the error which _isn't_ broken out - /* behind the actual error message. - /* This sections keeps both of those two in in sync */ - -pre .error, -pre .error-behind { - margin-left: -14px; - margin-top: 8px; - margin-bottom: 4px; - padding: 6px 6px 6px 14px; - width: calc(100% - 20px); - white-space: pre-wrap; - display: block; -} -pre .error { - position: absolute; - background-color: #fee; - border-left: 2px solid #bf1818; - /* Give the space to the error code */ - display: flex; - align-items: center; - color: black; -} -pre .error .code { - display: none; -} -pre .error-behind { - user-select: none; - visibility: transparent; - color: #fee; -} -/* Queries */ -pre .arrow { - /* Transparent background */ - background-color: #eee; - position: relative; - top: -7px; - margin-left: 0.1rem; - /* Edges */ - border-left: 1px solid #eee; - border-top: 1px solid #eee; - transform: translateY(25%) rotate(45deg); - /* Size */ - height: 8px; - width: 8px; -} -pre .popover { - margin-bottom: 10px; - background-color: #eee; - display: inline-block; - padding: 0 0.5rem 0.3rem; - margin-top: 10px; - border-radius: 3px; -} -/* Completion */ -pre .inline-completions ul.dropdown { - display: inline-block; - position: absolute; - width: 240px; - background-color: gainsboro; - color: grey; - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", - "Courier New", monospace; - font-size: 0.8rem; - margin: 0; - padding: 0; - border-left: 4px solid #4b9edd; -} -pre .inline-completions ul.dropdown::before { - background-color: #4b9edd; - width: 2px; - position: absolute; - top: -1.2rem; - left: -3px; - content: " "; -} -pre .inline-completions ul.dropdown li { - overflow-x: hidden; - padding-left: 4px; - margin-bottom: 4px; -} -pre .inline-completions ul.dropdown li.deprecated { - text-decoration: line-through; -} -pre .inline-completions ul.dropdown li span.result-found { - color: #4b9edd; -} -pre .inline-completions ul.dropdown li span.result { - width: 100px; - color: black; - display: inline-block; -} -.dark-theme .markdown pre { - background-color: #d8d8d8; - border-color: #ddd; - filter: invert(98%) hue-rotate(180deg); -} -data-lsp { - /* Ensures there's no 1px jump when the hover happens */ - border-bottom: 1px dotted transparent; - /* Fades in unobtrusively */ - transition-timing-function: ease; - transition: border-color 0.3s; -} -/* Respect people's wishes to not have animations */ -@media (prefers-reduced-motion: reduce) { - data-lsp { - transition: none; - } -} - -/** Annotations support, providing a tool for meta commentary */ -.tag-container { - position: relative; -} -.tag-container .twoslash-annotation { - position: absolute; - font-family: - "JetBrains Mono", - Menlo, - Monaco, - Consolas, - Courier New, - monospace; - right: -10px; - /** Default annotation text to 200px */ - width: 200px; - color: #187abf; - background-color: #fcf3d9; -} -.tag-container .twoslash-annotation p { - text-align: left; - font-size: 0.8rem; - line-height: 0.9rem; -} -.tag-container .twoslash-annotation svg { - float: left; - margin-left: -44px; -} -.tag-container .twoslash-annotation.left { - right: auto; - left: -200px; -} -.tag-container .twoslash-annotation.left svg { - float: right; - margin-right: -5px; -} - -/** Support for showing console log/warn/errors inline */ -pre .logger { - display: flex; - align-items: center; - color: black; - padding: 6px 6px 6px 8px; - width: calc(100% - 19px); - white-space: pre-wrap; -} -pre .logger svg { - margin-right: 9px; -} -pre .logger.error-log { - background-color: #fee; - border-left: 2px solid #bf1818; -} -pre .logger.warn-log { - background-color: #ffe; - border-left: 2px solid #eae662; -} -pre .logger.log-log { - background-color: #e9e9e9; - border-left: 2px solid #ababab; -} -pre .logger.log-log svg { - margin-left: 6px; - margin-right: 9px; -} diff --git a/apps/docs/src/routes/[...notFound].mdx b/apps/docs/src/routes/[...404].mdx similarity index 100% rename from apps/docs/src/routes/[...notFound].mdx rename to apps/docs/src/routes/[...404].mdx diff --git a/apps/docs/src/routes/docs/changelog.tsx b/apps/docs/src/routes/docs/changelog.tsx deleted file mode 100644 index 667462df..00000000 --- a/apps/docs/src/routes/docs/changelog.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { RouteProps } from "@solidjs/router"; - -import { CORE_VERSIONS } from "../../VERSIONS"; -import { Layout } from "../../components"; -import type { NavSection } from "../../model/navigation"; - -const CHANGELOG_NAV_SECTIONS: NavSection[] = [ - { - title: "Changelog", - links: CORE_VERSIONS.map((version) => ({ - title: `v${version}`, - href: `/docs/changelog/${version.replaceAll(".", "-")}`, - })), - }, -]; - -export default function ChangelogLayout(props: RouteProps) { - return {props.children}; -} diff --git a/apps/docs/src/routes/docs/changelog/(_)index.tsx b/apps/docs/src/routes/docs/changelog/(_)index.tsx new file mode 100644 index 00000000..3c0a20b9 --- /dev/null +++ b/apps/docs/src/routes/docs/changelog/(_)index.tsx @@ -0,0 +1,17 @@ +import { Navigate, redirect } from "@solidjs/router"; + +const target = "./0-13-x"; + +export const route = { + preload() { + return redirect(target); + }, +}; + +export function GET() { + throw redirect(target); +} + +export default function () { + return ; +} diff --git a/apps/docs/src/routes/docs/changelog/0-1-x.mdx b/apps/docs/src/routes/docs/changelog/0-1-x.mdx index c6f2fea4..fd99741b 100644 --- a/apps/docs/src/routes/docs/changelog/0-1-x.mdx +++ b/apps/docs/src/routes/docs/changelog/0-1-x.mdx @@ -1,4 +1,9 @@ -# v0.1.x +--- +title: Changelog v0.1.x +sidebarTitle: v0.1.x +--- + +# {frontmatter.sidebarTitle} ## v0.1.0 (January 04, 2023 - Initial release) diff --git a/apps/docs/src/routes/docs/changelog/0-10-x.mdx b/apps/docs/src/routes/docs/changelog/0-10-x.mdx index 7a84c9f9..d988e3ba 100644 --- a/apps/docs/src/routes/docs/changelog/0-10-x.mdx +++ b/apps/docs/src/routes/docs/changelog/0-10-x.mdx @@ -1,4 +1,9 @@ -# v0.10.x +--- +title: Changelog v0.10.x +sidebarTitle: v0.10.x +--- + +# {frontmatter.sidebarTitle} ## v0.10.0 (August 12, 2023) diff --git a/apps/docs/src/routes/docs/changelog/0-11-x.mdx b/apps/docs/src/routes/docs/changelog/0-11-x.mdx index 3e7ca2c1..8974be49 100644 --- a/apps/docs/src/routes/docs/changelog/0-11-x.mdx +++ b/apps/docs/src/routes/docs/changelog/0-11-x.mdx @@ -1,4 +1,9 @@ -# v0.11.x +--- +title: Changelog v0.11.x +sidebarTitle: v0.11.x +--- + +# {frontmatter.sidebarTitle} ## v0.11.2 (October 21, 2023) diff --git a/apps/docs/src/routes/docs/changelog/0-12-x.mdx b/apps/docs/src/routes/docs/changelog/0-12-x.mdx index 10aad2a2..5dc79039 100644 --- a/apps/docs/src/routes/docs/changelog/0-12-x.mdx +++ b/apps/docs/src/routes/docs/changelog/0-12-x.mdx @@ -1,4 +1,9 @@ -# v0.12.x +--- +title: Changelog v0.12.x +sidebarTitle: v0.12.x +--- + +# {frontmatter.sidebarTitle} ## v0.12.6 (March 16, 2024) diff --git a/apps/docs/src/routes/docs/changelog/0-13-x.mdx b/apps/docs/src/routes/docs/changelog/0-13-x.mdx index 4f79adfd..f75bb1cb 100644 --- a/apps/docs/src/routes/docs/changelog/0-13-x.mdx +++ b/apps/docs/src/routes/docs/changelog/0-13-x.mdx @@ -1,4 +1,9 @@ -# v0.13.x +--- +title: Changelog v0.13.x +sidebarTitle: v0.13.x +--- + +# {frontmatter.sidebarTitle} ## v0.13.12 (July 1, 2026) diff --git a/apps/docs/src/routes/docs/changelog/0-2-x.mdx b/apps/docs/src/routes/docs/changelog/0-2-x.mdx index b8c6b499..144f8502 100644 --- a/apps/docs/src/routes/docs/changelog/0-2-x.mdx +++ b/apps/docs/src/routes/docs/changelog/0-2-x.mdx @@ -1,4 +1,9 @@ -# v0.2.x +--- +title: Changelog v0.2.x +sidebarTitle: v0.2.x +--- + +# {frontmatter.sidebarTitle} ## v0.2.0 (January 13, 2023) diff --git a/apps/docs/src/routes/docs/changelog/0-3-x.mdx b/apps/docs/src/routes/docs/changelog/0-3-x.mdx index 27adfd4e..0c637007 100644 --- a/apps/docs/src/routes/docs/changelog/0-3-x.mdx +++ b/apps/docs/src/routes/docs/changelog/0-3-x.mdx @@ -1,4 +1,9 @@ -# v0.3.x +--- +title: Changelog v0.3.x +sidebarTitle: v0.3.x +--- + +# {frontmatter.sidebarTitle} ## v0.3.1 (January 15, 2023) diff --git a/apps/docs/src/routes/docs/changelog/0-4-x.mdx b/apps/docs/src/routes/docs/changelog/0-4-x.mdx index 708a276c..66461dac 100644 --- a/apps/docs/src/routes/docs/changelog/0-4-x.mdx +++ b/apps/docs/src/routes/docs/changelog/0-4-x.mdx @@ -1,4 +1,9 @@ -# v0.4.x +--- +title: Changelog v0.4.x +sidebarTitle: v0.4.x +--- + +# {frontmatter.sidebarTitle} ## v0.4.0 (January 19, 2023) diff --git a/apps/docs/src/routes/docs/changelog/0-5-x.mdx b/apps/docs/src/routes/docs/changelog/0-5-x.mdx index 2726a8d7..d1963365 100644 --- a/apps/docs/src/routes/docs/changelog/0-5-x.mdx +++ b/apps/docs/src/routes/docs/changelog/0-5-x.mdx @@ -1,4 +1,9 @@ -# v0.5.x +--- +title: Changelog v0.5.x +sidebarTitle: v0.5.x +--- + +# {frontmatter.sidebarTitle} ## v0.5.0 (January 27, 2023) diff --git a/apps/docs/src/routes/docs/changelog/0-6-x.mdx b/apps/docs/src/routes/docs/changelog/0-6-x.mdx index 56c763ac..430296e7 100644 --- a/apps/docs/src/routes/docs/changelog/0-6-x.mdx +++ b/apps/docs/src/routes/docs/changelog/0-6-x.mdx @@ -1,4 +1,9 @@ -# v0.6.x +--- +title: Changelog v0.6.x +sidebarTitle: v0.6.x +--- + +# {frontmatter.sidebarTitle} ## v0.6.2 (February 25, 2023) diff --git a/apps/docs/src/routes/docs/changelog/0-7-x.mdx b/apps/docs/src/routes/docs/changelog/0-7-x.mdx index 70780ef9..4ee5e0e4 100644 --- a/apps/docs/src/routes/docs/changelog/0-7-x.mdx +++ b/apps/docs/src/routes/docs/changelog/0-7-x.mdx @@ -1,4 +1,9 @@ -# v0.7.x +--- +title: Changelog v0.7.x +sidebarTitle: v0.7.x +--- + +# {frontmatter.sidebarTitle} ## v0.7.4 (March 18, 2023) diff --git a/apps/docs/src/routes/docs/changelog/0-8-x.mdx b/apps/docs/src/routes/docs/changelog/0-8-x.mdx index 070669ed..fa8fe294 100644 --- a/apps/docs/src/routes/docs/changelog/0-8-x.mdx +++ b/apps/docs/src/routes/docs/changelog/0-8-x.mdx @@ -1,4 +1,9 @@ -# v0.8.x +--- +title: Changelog v0.8.x +sidebarTitle: v0.8.x +--- + +# {frontmatter.sidebarTitle} ## v0.8.2 (March 30, 2023) diff --git a/apps/docs/src/routes/docs/changelog/0-9-x.mdx b/apps/docs/src/routes/docs/changelog/0-9-x.mdx index 4b12d5a2..7bcdf36d 100644 --- a/apps/docs/src/routes/docs/changelog/0-9-x.mdx +++ b/apps/docs/src/routes/docs/changelog/0-9-x.mdx @@ -1,4 +1,9 @@ -# v0.9.x +--- +title: Changelog v0.9.x +sidebarTitle: v0.9.x +--- + +# {frontmatter.sidebarTitle} ## v0.9.8 (June 29, 2023) diff --git a/apps/docs/src/routes/docs/changelog/2-0-x.mdx b/apps/docs/src/routes/docs/changelog/2-0-x.mdx new file mode 100644 index 00000000..892b52e7 --- /dev/null +++ b/apps/docs/src/routes/docs/changelog/2-0-x.mdx @@ -0,0 +1,46 @@ +# v2.0.x + +## v2.0.0 + +**Breaking changes** + +- **Solid 2.0 is now required.** Peer dependencies are `solid-js@^2.0.0` and `@solidjs/web@^2.0.0`. Solid 1.x is no longer supported. +- The JSX import source has changed from `solid-js` to `@solidjs/web`. Update your `tsconfig.json`: + ```json + { "compilerOptions": { "jsxImportSource": "@solidjs/web" } } + ``` +- `JSX` namespace types and DOM-specific component types must now be imported from `@solidjs/web` instead of `solid-js`. +- Context providers now use the `value` prop directly — `` is replaced by ``. +- `tabIndex` prop renamed to lowercase `tabindex` to match the Solid 2.0 DOM attribute convention. + +**New features** + +- [New `Drawer` component](/docs/core/components/drawer) — a panel that slides in from any edge of the screen with drag-to-dismiss, snap-point support, and real-time overlay opacity tracking. Adapted from [corvu/drawer](https://github.com/corvudev/corvu/tree/main/packages/drawer) by Jasmin Noetzli (MIT). +- [New `Resizable` component](/docs/core/components/resizable) — a set of panels that can be resized by dragging the handles between them. Supports horizontal and vertical layouts, collapsible panels, pixel or fraction sizes, nested layouts, and full keyboard navigation. Adapted from [corvu/resizable](https://github.com/corvudev/corvu/tree/main/packages/resizable) by Jasmin Noetzli (MIT). +- [New `OTPField` component](/docs/core/components/otp-field) — an accessible one-time password input built from a hidden native `` and fully customisable visual slots. Supports autofill, custom character patterns, caret tracking, and Kobalte's standard form control system. + +**Solid Primitives integration** + +Internal primitives have been replaced with their `@solid-primitives` equivalents, reducing the custom code surface and aligning with the broader Solid ecosystem: + +- `createControllableSignal` → `@solid-primitives/controlled-signal` +- `createFormResetListener` → `@solid-primitives/form` +- `createSize` / `createElementSize` → `@solid-primitives/resize-observer` +- Reduced-motion detection → `@solid-primitives/media` (via interaction) +- Presence/exit animation management → `@solid-primitives/presence` +- i18n utilities → `@solid-primitives/i18n` +- Keyboard shortcut tracking → `@solid-primitives/keyboard` +- `combineStyle` / prop utilities → `@solid-primitives/props` +- Scroll locking → `@solid-primitives/scroll` +- A11y helpers → `@solid-primitives/a11y` +- Focus management → `@solid-primitives/focus` + +**Internal changes** + +- All components ported to the Solid 2.0 API: split `createEffect(compute, apply)` form, `omit` replacing `splitProps`, `merge` replacing `mergeProps`, auto-batched writes, and `` / `` replacing `` / `` in internal use. +- `createTransition` removed — superseded by Solid 2.0's built-in transition model. +- TypeScript updated to 6.0 with stricter type checking applied across all components. +- Biome updated to 2.5.1 for formatting and linting. +- pnpm updated to 11. +- Vite updated to 6, Vitest to 2, Storybook to 10, Tailwind CSS to 4. +- `@solidjs/testing-library` updated to 1.0.0-beta.2. diff --git a/apps/docs/src/routes/docs/changelog/index.tsx b/apps/docs/src/routes/docs/changelog/index.tsx deleted file mode 100644 index a9711c69..00000000 --- a/apps/docs/src/routes/docs/changelog/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { Navigate } from "@solidjs/router"; -import { LATEST_CORE_CHANGELOG_URL } from "../../../VERSIONS"; - -export default function () { - return ; -} diff --git a/apps/docs/src/routes/docs/core.tsx b/apps/docs/src/routes/docs/core.tsx deleted file mode 100644 index 6b877769..00000000 --- a/apps/docs/src/routes/docs/core.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import type { RouteProps } from "@solidjs/router"; - -import { Layout } from "../../components"; -import type { NavSection } from "../../model/navigation"; - -const CORE_NAV_SECTIONS: NavSection[] = [ - { - title: "Overview", - links: [ - { - title: "Introduction", - href: "/docs/core/overview/introduction", - }, - { - title: "Getting started", - href: "/docs/core/overview/getting-started", - }, - { - title: "Styling", - href: "/docs/core/overview/styling", - }, - { - title: "Animation", - href: "/docs/core/overview/animation", - }, - { - title: "Polymorphism", - href: "/docs/core/overview/polymorphism", - }, - { - title: "Server side rendering", - href: "/docs/core/overview/ssr", - }, - ], - }, - { - title: "Components", - links: [ - { - title: "Accordion", - href: "/docs/core/components/accordion", - }, - { - title: "Alert", - href: "/docs/core/components/alert", - }, - { - title: "Alert Dialog", - href: "/docs/core/components/alert-dialog", - }, - { - title: "Badge", - href: "/docs/core/components/badge", - }, - { - title: "Breadcrumbs", - href: "/docs/core/components/breadcrumbs", - }, - { - title: "Button", - href: "/docs/core/components/button", - }, - { - title: "Checkbox", - href: "/docs/core/components/checkbox", - }, - { - title: "Collapsible", - href: "/docs/core/components/collapsible", - }, - { - title: "Color Area", - href: "/docs/core/components/color-area", - }, - { - title: "Color Channel Field", - href: "/docs/core/components/color-channel-field", - }, - { - title: "Color Field", - href: "/docs/core/components/color-field", - }, - { - title: "Color Slider", - href: "/docs/core/components/color-slider", - }, - { - title: "Color Swatch", - href: "/docs/core/components/color-swatch", - }, - { - title: "Color Wheel", - href: "/docs/core/components/color-wheel", - }, - { - title: "Combobox", - href: "/docs/core/components/combobox", - }, - { - title: "Context Menu", - href: "/docs/core/components/context-menu", - }, - { - title: "Dialog", - href: "/docs/core/components/dialog", - }, - { - title: "Dropdown Menu", - href: "/docs/core/components/dropdown-menu", - }, - { - title: "File Field", - href: "/docs/core/components/file-field", - }, - { - title: "Hover Card", - href: "/docs/core/components/hover-card", - }, - { - title: "Image", - href: "/docs/core/components/image", - }, - { - title: "Link", - href: "/docs/core/components/link", - }, - { - title: "Menubar", - href: "/docs/core/components/menubar", - }, - { - title: "Meter", - href: "/docs/core/components/meter", - }, - { - title: "Navigation Menu", - href: "/docs/core/components/navigation-menu", - }, - { - title: "Number Field", - href: "/docs/core/components/number-field", - }, - { - title: "Pagination", - href: "/docs/core/components/pagination", - }, - { - title: "Popover", - href: "/docs/core/components/popover", - }, - { - title: "Progress", - href: "/docs/core/components/progress", - }, - { - title: "Radio Group", - href: "/docs/core/components/radio-group", - }, - { - title: "Rating", - href: "/docs/core/components/rating", - status: "new", - }, - { - title: "Search", - href: "/docs/core/components/search", - }, - { - title: "Segmented Control", - href: "/docs/core/components/segmented-control", - }, - { - title: "Select", - href: "/docs/core/components/select", - }, - { - title: "Separator", - href: "/docs/core/components/separator", - }, - { - title: "Skeleton", - href: "/docs/core/components/skeleton", - }, - { - title: "Slider", - href: "/docs/core/components/slider", - }, - { - title: "Switch", - href: "/docs/core/components/switch", - }, - { - title: "Tabs", - href: "/docs/core/components/tabs", - }, - { - title: "Text Field", - href: "/docs/core/components/text-field", - }, - { - title: "Time Field", - href: "/docs/core/components/time-field", - status: "new", - }, - { - title: "Toast", - href: "/docs/core/components/toast", - }, - { - title: "Toggle Button", - href: "/docs/core/components/toggle-button", - }, - { - title: "Toggle Group", - href: "/docs/core/components/toggle-group", - }, - { - title: "Tooltip", - href: "/docs/core/components/tooltip", - }, - { - title: "I18nProvider", - href: "/docs/core/components/i18n-provider", - }, - ], - }, -]; - -export default function CoreLayout(props: RouteProps) { - return {props.children}; -} diff --git a/apps/docs/src/routes/docs/core/overview/introduction.mdx b/apps/docs/src/routes/docs/core/(0)overview/(0)introduction.mdx similarity index 95% rename from apps/docs/src/routes/docs/core/overview/introduction.mdx rename to apps/docs/src/routes/docs/core/(0)overview/(0)introduction.mdx index a38c87e9..511c4d84 100644 --- a/apps/docs/src/routes/docs/core/overview/introduction.mdx +++ b/apps/docs/src/routes/docs/core/(0)overview/(0)introduction.mdx @@ -1,4 +1,8 @@ -# Introduction +--- +title: Introduction +--- + +# {frontmatter.title} Kobalte is a UI toolkit for building accessible web apps and design systems with SolidJS. It provides a set of low-level UI components and primitives which can be the foundation for your design system implementation. diff --git a/apps/docs/src/routes/docs/core/(0)overview/(1)getting-started.mdx b/apps/docs/src/routes/docs/core/(0)overview/(1)getting-started.mdx new file mode 100644 index 00000000..882e5150 --- /dev/null +++ b/apps/docs/src/routes/docs/core/(0)overview/(1)getting-started.mdx @@ -0,0 +1,129 @@ +--- +title: Getting Started +--- + +import { PopoverDemo } from "../../../../examples/getting-started"; + +# {frontmatter.title} + +## Installation + +Install Kobalte by running: + +```package-install +@kobalte/core +``` + +## Using the components + +The example below demonstrate how to create a Popover component with Kobalte. + +:::preview + + + +--- + +```tsx title="index.tsx" tab +import { Popover } from "@kobalte/core/popover"; +import { CrossIcon } from "some-icon-library"; +import "./style.css"; + +function App() { + + Learn more + + + +
+ About Kobalte + + + +
+ + A UI toolkit for building accessible web apps and design systems with SolidJS. + +
+
+
; +} +``` + +```css title="style.css" tab +.popover__trigger { + appearance: none; + display: inline-flex; + justify-content: center; + align-items: center; + height: 40px; + width: auto; + outline: none; + border: none; + border-radius: 6px; + padding: 0 16px; + background-color: hsl(200 98% 39%); + color: white; + font-size: 16px; + line-height: 0; + transition: 250ms background-color; +} + +.popover__trigger:hover { + background-color: hsl(201 96% 32%); +} + +.popover__trigger:focus-visible { + outline: 2px solid hsl(200 98% 39%); + outline-offset: 2px; +} + +.popover__trigger:active { + background-color: hsl(201 90% 27%); +} + +.popover__content { + z-index: 50; + max-width: min(calc(100vw - 16px), 380px); + border: 1px solid hsl(240 5% 84%); + border-radius: 6px; + padding: 12px; + background-color: white; + box-shadow: + 0 10px 15px -3px rgb(0 0 0 / 0.1), + 0 4px 6px -4px rgb(0 0 0 / 0.1); +} + +.popover__header { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 6px; +} + +.popover__close-button { + height: 16px; + width: 16px; + color: hsl(240 5% 34%); +} + +.popover__title { + font-size: 16px; + font-weight: 500; + color: hsl(240 6% 10%); +} + +.popover__description { + font-size: 14px; + color: hsl(240 5% 26%); +} +``` + +::: + +In a few lines of code, we've implemented a fully accessible Popover component that: + +- Adheres to [WAI-ARIA Dialog](https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/) design pattern. +- Can be controlled or uncontrolled. +- Optionally render a pointing arrow. +- Has focus fully managed and customizable. diff --git a/apps/docs/src/routes/docs/core/overview/styling.mdx b/apps/docs/src/routes/docs/core/(0)overview/(2)styling.mdx similarity index 52% rename from apps/docs/src/routes/docs/core/overview/styling.mdx rename to apps/docs/src/routes/docs/core/(0)overview/(2)styling.mdx index 220df0e2..10ccbc3c 100644 --- a/apps/docs/src/routes/docs/core/overview/styling.mdx +++ b/apps/docs/src/routes/docs/core/(0)overview/(2)styling.mdx @@ -1,6 +1,8 @@ -import { Callout, TabsSnippets } from "../../../../components"; +--- +title: Styling +--- -# Styling +# {frontmatter.title} Kobalte components are unstyled, allowing you to completely customize the look and feel. Bring your preferred styling solution (vanilla CSS, Tailwind, CSS-in-JS libraries, etc...). @@ -8,48 +10,29 @@ Kobalte components are unstyled, allowing you to completely customize the look a All components and their parts accept a `class` prop. This class will be passed through to the DOM element. You can style a component part by targeting the `class` that you provide. - - - index.tsx - style.css - - - {/* */} - ```tsx - import { Popover as KPopover } from "@kobalte/core"; - import "./style.css"; - - export const Popover = () => { - return ( - - - Open - - - ... - - - ); - }; - ``` - {/* */} - - - - {/* */} - ```css - .popover__trigger { - /* The popover trigger style. */ - } - - .popover__content { - /* The popover content style. */ - } - ``` - {/* */} - - - +```tsx title="index.tsx" tab +import { Popover as KPopover } from "@kobalte/core/popover"; +import "./style.css"; + +export const Popover = () => { + return ( + + Open + ... + + ); +}; +``` + +```css title="style.css" tab +.popover__trigger { + /* The popover trigger style. */ +} + +.popover__content { + /* The popover content style. */ +} +``` ## Styling a state @@ -60,55 +43,21 @@ When a component or its parts can have multiple states, we automatically attach You can style a component state by targeting the `data-*` attributes added by Kobalte. - - - style.css - - - {/* */} - ```css - .popover__trigger[data-disabled] { - /* The popover trigger style when disabled. */ - } - ``` - {/* */} - - +```css title="style.css" +.popover__trigger[data-disabled] { + /* The popover trigger style when disabled. */ +} +``` ## Using the TailwindCSS plugin -If you are using [TailwindCSS](https://tailwindcss.com/), you can use the `@kobalte/tailwindcss` plugin to target Kobalte's `data-*` attributes with modifiers like `ui-expanded:*`. +If you are using [TailwindCSS v3](https://v3.tailwindcss.com/), you can use the `@kobalte/tailwindcss` plugin to target Kobalte's `data-*` attributes with modifiers like `ui-expanded:*`. From Tailwind v4 the plugin is no longer required as data attributes are natively supported, use them directly instead. ### Installation - - - npm - yarn - pnpm - - - {/* */} - ```bash - npm install @kobalte/tailwindcss - ``` - {/* */} - - - {/* */} - ```bash - yarn add @kobalte/tailwindcss - ``` - {/* */} - - - {/* */} - ```bash - pnpm add @kobalte/tailwindcss - ``` - {/* */} - - +```package-install +@kobalte/tailwindcss +``` ### Usage @@ -171,41 +120,15 @@ If you are using [Vanilla Extract](https://vanilla-extract.style/), you can use ### Installation - - - npm - yarn - pnpm - - - {/* */} - ```bash - npm install @kobalte/vanilla-extract - ``` - {/* */} - - - {/* */} - ```bash - yarn add @kobalte/vanilla-extract - ``` - {/* */} - - - {/* */} - ```bash - pnpm add @kobalte/vanilla-extract - ``` - {/* */} - - +```package-install +@kobalte/vanilla-extract +``` ### Usage Use the `componentStateStyles` utility function to create vanilla-extract styles that target `data-*` attributes of Kobalte components. -```ts -// styles.css +```ts title="style.css.ts" import { componentStateStyles } from "@kobalte/vanilla-extract"; import { style } from "@vanilla-extract/css"; @@ -238,9 +161,9 @@ const button = style([ Then apply your styles to the component: -```tsx +```tsx title="MyButton.tsx" import { Button } from "@kobalte/core/button"; -import { button } from "./styles.css"; +import { button } from "./styles.css.ts"; export const MyButton = () => ; ``` @@ -261,3 +184,5 @@ export const PopoverTrigger = (props: ComponentProps) = return ; }; ``` + +Follow the [Polymorphism Guide](./polymorphism) for more advanced type control and additional props. diff --git a/apps/docs/src/routes/docs/core/overview/animation.mdx b/apps/docs/src/routes/docs/core/(0)overview/(3)animation.mdx similarity index 95% rename from apps/docs/src/routes/docs/core/overview/animation.mdx rename to apps/docs/src/routes/docs/core/(0)overview/(3)animation.mdx index 7f3cf0e6..aa0a9d34 100644 --- a/apps/docs/src/routes/docs/core/overview/animation.mdx +++ b/apps/docs/src/routes/docs/core/(0)overview/(3)animation.mdx @@ -1,6 +1,8 @@ -import { Preview } from "../../../../components"; +--- +title: Animation +--- -# Animation +# {frontmatter.title} Kobalte components can be animated with CSS keyframes or your JavaScript animation library of choice. However, there are some caveats noted here in regard to exiting animations with JavaScript animation libraries. diff --git a/apps/docs/src/routes/docs/core/overview/polymorphism.mdx b/apps/docs/src/routes/docs/core/(0)overview/(4)polymorphism.mdx similarity index 93% rename from apps/docs/src/routes/docs/core/overview/polymorphism.mdx rename to apps/docs/src/routes/docs/core/(0)overview/(4)polymorphism.mdx index fb59bb85..ea6a270e 100644 --- a/apps/docs/src/routes/docs/core/overview/polymorphism.mdx +++ b/apps/docs/src/routes/docs/core/(0)overview/(4)polymorphism.mdx @@ -1,6 +1,8 @@ -import { Callout } from "../../../../components/callout.tsx"; +--- +title: Polymorphism +--- -# Polymorphism +# {frontmatter.title} Kobalte components that render a DOM element support polymorphism via the `as` prop. This allows you to change the rendered element or component while preserving behavior, accessibility, and state management. @@ -14,7 +16,7 @@ Polymorphism is useful when you want to: Use `as` with a native element or a custom Solid component. -```tsx {8, 13} +```tsx {9, 14} import { Tabs } from "@kobalte/core/tabs"; import { MyCustomButton } from "./components"; @@ -48,12 +50,12 @@ When using an `as` callback: - Always spread the provided props - Custom props are forwarded **unchanged** -- Kobalte options are _not_ passed +- Kobalte options are **not** passed - Event handlers must be defined on the **parent** Violating these rules can break behavior or accessibility. -```tsx {17} +```tsx {12} import { Tabs } from "@kobalte/core/tabs"; import { MyCustomButton } from "./components"; @@ -84,7 +86,7 @@ In this example: You can use `PolymorphicCallbackProps` to get exact typing for callback props: -```tsx {6-9} +```tsx {7-11} import { Tabs, TabsTriggerOptions, TabsTriggerRenderProps } from "@kobalte/core/tabs"; import { PolymorphicCallbackProps } from "@kobalte/core/polymorphic"; @@ -111,9 +113,9 @@ Custom event handlers defined on a Kobalte component are called _before_ Kobalte ## Component Prop Types - - This section is intended for **library authors** building on top of Kobalte. - +:::warning +This section is intended for **library authors** building on top of Kobalte. +::: Every Kobalte component that renders an element exposes four core types: @@ -192,10 +194,10 @@ function CustomTabsTrigger( } ``` - - When using generics, TypeScript can lose some precision; splitting local props and spreading the - remaining props helps preserve usability. - +:::info +When using generics, TypeScript can lose some precision; splitting local props and spreading the +remaining props helps preserve usability. +::: Note: diff --git a/apps/docs/src/routes/docs/core/(0)overview/(5)ssr.mdx b/apps/docs/src/routes/docs/core/(0)overview/(5)ssr.mdx new file mode 100644 index 00000000..d788827a --- /dev/null +++ b/apps/docs/src/routes/docs/core/(0)overview/(5)ssr.mdx @@ -0,0 +1,15 @@ +--- +title: SSR +--- + +# Server Side Rendering + +Kobalte should work with any SSR for SolidJS, please make sure the environment is correctly setup. + +## Usage with SolidStart + +Kobalte works out of the box with [SolidStart](https://start.solidjs.com/). + +:::info +Kobalte has been tested with `solid-js@1.9.9`, `@solidjs/start@0.6.1`, and `@solidjs/start@2.0.0-alpha.3`. +::: diff --git a/apps/docs/src/routes/docs/core/(0)overview/(_)index.tsx b/apps/docs/src/routes/docs/core/(0)overview/(_)index.tsx new file mode 100644 index 00000000..9c202c7d --- /dev/null +++ b/apps/docs/src/routes/docs/core/(0)overview/(_)index.tsx @@ -0,0 +1,17 @@ +import { Navigate, redirect } from "@solidjs/router"; + +const target = "./introduction"; + +export const route = { + preload() { + return redirect(target); + }, +}; + +export function GET() { + throw redirect(target); +} + +export default function () { + return ; +} diff --git a/apps/docs/src/routes/docs/core/components/accordion.mdx b/apps/docs/src/routes/docs/core/(1)components/accordion.mdx similarity index 59% rename from apps/docs/src/routes/docs/core/components/accordion.mdx rename to apps/docs/src/routes/docs/core/(1)components/accordion.mdx index 43c0fcb1..7c4ae438 100644 --- a/apps/docs/src/routes/docs/core/components/accordion.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/accordion.mdx @@ -1,4 +1,17 @@ -import { Preview, TabsSnippets, Kbd, Callout } from "../../../../components"; +--- +title: Accordion +badges: + - icon: npm + label: "Since v0.5.0" + - label: "@kobalte/core/accordion" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/accordion" + - icon: aria + label: "Accordion" + url: "https://www.w3.org/WAI/ARIA/apg/patterns/accordion/" +--- + import { BasicExample, DefaultValueExample, @@ -7,7 +20,7 @@ import { AllowMultipleExample, } from "../../../../examples/accordion"; -# Accordion +# {frontmatter.title} A vertically stacked set of interactive headings that each reveal an associated section of content. @@ -51,165 +64,21 @@ The accordion consists of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { Accordion } from "@kobalte/core/accordion"; - import { ChevronDownIcon } from "some-icon-library"; - import "./style.css"; - - function App() { - return ( - - - - - Is it accessible? - - - - -

- Yes. It adheres to the WAI-ARIA design pattern. -

-
-
- - - - - Is it unstyled? - - - - -

- Yes. It's unstyled by default, giving you freedom over the look and feel. -

-
-
- - - - - Can it be animated? - - - - -

- Yes! You can animate the Accordion with CSS or JavaScript. -

-
-
-
- ); - } - ``` - -
- - ```css - .accordion { - border-radius: 6px; - width: 300px; - border: 1px solid hsl(240 5% 84%); - color: hsl(240 4% 16%); - } - - .accordion__item:first-child { - margin-top: 0; - border-top-left-radius: 4px; - border-top-right-radius: 4px; - } - - .accordion__item:last-child { - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; - } - - .accordion__item-header { - display: flex; - } - - .accordion__item-trigger { - display: inline-flex; - align-items: center; - justify-content: space-between; - width: 100%; - padding: 14px; - border-bottom: 1px solid hsl(240 5% 84%); - font-weight: 600; - text-align: left; - outline: none; - } - - .accordion__item:last-child .accordion__item-trigger { - border-bottom: none; - } - - .accordion__item-trigger:focus-visible { - z-index: 1; - outline: 2px solid hsl(200 98% 39%); - outline-offset: 2px; +:::preview -} + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/accordion.tsx#L6-L64" + +``` - .accordion__item-trigger-icon { - width: 24px; - height: 24px; - transition: transform 300ms cubic-bezier(0.87, 0, 0.13, 1); - } - - .accordion__item-trigger[data-expanded] > .accordion__item-trigger-icon { - transform: rotate(180deg); - } - - .accordion__item-content { - overflow: hidden; - font-size: 16px; - animation: slideUp 300ms cubic-bezier(0.87, 0, 0.13, 1); - } - - .accordion__item-content[data-expanded] { - animation: slideDown 300ms cubic-bezier(0.87, 0, 0.13, 1); - } - - .accordion__item-content-text { - padding: 16px; - } - - @keyframes slideDown { - from { - height: 0; - } - to { - height: var(--kb-accordion-content-height); - } - } - - @keyframes slideUp { - from { - height: var(--kb-accordion-content-height); - } - to { - height: 0; - } - } - ``` - - - {/* */} -
+```css title="style.css" tab file="../../../../examples/accordion.module.css" + +``` + +::: ## Usage @@ -217,11 +86,13 @@ The accordion consists of: A default expanded item can be provided using the `defaultValue` prop, which should correspond to the `value` prop provided to each accordion item. - - - +:::preview + + + +--- -```tsx {0,2} +```tsx {1,3} {/* ... */} {/* ... */} @@ -229,19 +100,23 @@ A default expanded item can be provided using the `defaultValue` prop, which sho ``` - - The `value` and `defaultValue` props always take a `string[]`. - +::: + +:::info[] +The `value` and `defaultValue` props always take a `string[]`. +::: ### Controlled value Expanded item can be controlled using the `value` prop, paired with the `onChange` event. The `value` prop from the accordion item will be passed into the callback when the item is expanded, allowing you to update state accordingly. - - - +:::preview + + + +--- -```tsx {3,7} +```tsx {4,8} import { createSignal } from "solid-js"; function ControlledExample() { @@ -260,15 +135,19 @@ function ControlledExample() { } ``` +::: + ### Collapsible item By default, accordion items are not collapsible, meaning you can't toggle them from open to close and vice-versa. Use the `collapsible` prop to enable this behavior. - - - +:::preview + + -```tsx {0} +--- + +```tsx {1} {/* ... */} {/* ... */} @@ -276,15 +155,19 @@ By default, accordion items are not collapsible, meaning you can't toggle them f ``` +::: + ### Allow expanding multiple items Use the `multiple` to allow expanding multiple accordion items at the same time. This also make all accordion items collapsible. - - - +:::preview + + -```tsx {0} +--- + +```tsx {1} {/* ... */} {/* ... */} @@ -292,12 +175,13 @@ Use the `multiple` to allow expanding multiple accordion items at the same time. ``` +::: + ### Animating content size We expose the CSS custom properties `--kb-accordion-content-width` and `--kb-accordion-content-height` which you can use to animate the size of the content when it opens/closes. -```css {16,22} -/* style.css */ +```css {16,22} title="style.css" .accordion__item-content { overflow: hidden; font-size: 16px; @@ -384,11 +268,11 @@ We expose the CSS custom properties `--kb-accordion-content-width` and `--kb-acc | Key | Description | | :-------------------------------- | :------------------------------------------------------------------------------------- | -| Space | When focus is on an `Accordion.Trigger` of a collapsed section, expands the section. | -| Enter | When focus is on an `Accordion.Trigger` of a collapsed section, expands the section. | -| Tab | Moves focus to the next focusable element. | -| Shift + Tab | Moves focus to the previous focusable element. | -| ArrowDown | Moves focus to the next `Accordion.Trigger`. | -| ArrowUp | Moves focus to the previous `Accordion.Trigger`. | -| Home | When focus is on an `Accordion.Trigger`, moves focus to the first `Accordion.Trigger`. | -| End | When focus is on an `Accordion.Trigger`, moves focus to the last `Accordion.Trigger`. | +| Space | When focus is on an `Accordion.Trigger` of a collapsed section, expands the section. | +| Enter | When focus is on an `Accordion.Trigger` of a collapsed section, expands the section. | +| Tab | Moves focus to the next focusable element. | +| Shift + Tab | Moves focus to the previous focusable element. | +| ArrowDown | Moves focus to the next `Accordion.Trigger`. | +| ArrowUp | Moves focus to the previous `Accordion.Trigger`. | +| Home | When focus is on an `Accordion.Trigger`, moves focus to the first `Accordion.Trigger`. | +| End | When focus is on an `Accordion.Trigger`, moves focus to the last `Accordion.Trigger`. | diff --git a/apps/docs/src/routes/docs/core/components/alert-dialog.mdx b/apps/docs/src/routes/docs/core/(1)components/alert-dialog.mdx similarity index 65% rename from apps/docs/src/routes/docs/core/components/alert-dialog.mdx rename to apps/docs/src/routes/docs/core/(1)components/alert-dialog.mdx index a9b3b196..e6c11985 100644 --- a/apps/docs/src/routes/docs/core/components/alert-dialog.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/alert-dialog.mdx @@ -1,7 +1,20 @@ -import { Preview, Kbd, TabsSnippets } from "../../../../components"; +--- +title: Alert Dialog +badges: + - icon: npm + label: "Since v0.8.0" + - label: "@kobalte/core/alert-dialog" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/alert-dialog" + - icon: aria + label: "Alert dialog" + url: "https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/" +--- + import { BasicExample } from "../../../../examples/alert-dialog"; -# Alert Dialog +# {frontmatter.title} A modal dialog that interrupts the user's workflow to communicate an important message and acquire a response. Examples include action confirmation prompts and error message confirmations. The alertdialog role enables assistive technologies and browsers to distinguish alert dialogs from other dialogs so they have the option of giving alert dialogs special treatment, such as playing a system alert sound. @@ -21,7 +34,7 @@ import { AlertDialog } from "@kobalte/core"; - Supports modal and non-modal modes. - Provides screen reader announcements via rendered title and description. - Focus is trapped and scrolling is blocked while it is open. -- Pressing Esc closes the alert dialog. +- Pressing Esc closes the alert dialog. - Can be controlled or uncontrolled. ## Anatomy @@ -53,184 +66,21 @@ The alert dialog consists of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { AlertDialog } from "@kobalte/core/alert-dialog"; - import { CrossIcon } from "some-icon-library"; - import "./style.css"; - - function App() { - return ( - - Open - - -
- -
- Alert Dialog - - - -
- - An Alert Dialog enables assistive technologies and browsers to distinguish alert dialogs from other dialogs so they have the option of giving alert dialogs special treatment, such as playing a system alert sound. - -
-
-
-
- ); - } - ``` - -
- - ```css - .alert-dialog__trigger { - appearance: none; - display: inline-flex; - justify-content: center; - align-items: center; - height: 40px; - width: auto; - outline: none; - border-radius: 6px; - padding: 0 16px; - background-color: hsl(200 98% 39%); - color: white; - font-size: 16px; - line-height: 0; - transition: 250ms background-color; - } - - .alert-dialog__trigger:hover { - background-color: hsl(201 96% 32%); - } - - .alert-dialog__trigger:focus-visible { - outline: 2px solid hsl(200 98% 39%); - outline-offset: 2px; - } - - .alert-dialog__trigger:active { - background-color: hsl(201 90% 27%); - } - - .alert-dialog__overlay { - position: fixed; - inset: 0; - z-index: 50; - background-color: rgb(0 0 0 / 0.2); - animation: overlayHide 250ms ease 100ms forwards; - } - - .alert-dialog__overlay[data-expanded] { - animation: overlayShow 250ms ease; - } - - .alert-dialog__positioner { - position: fixed; - inset: 0; - z-index: 50; - display: flex; - align-items: center; - justify-content: center; - } - - .alert-dialog__content { - z-index: 50; - max-width: min(calc(100vw - 16px), 500px); - border: 1px solid hsl(240 5% 84%); - border-radius: 6px; - padding: 16px; - background-color: white; - box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - animation: contentHide 300ms ease-in forwards; - } - - .alert-dialog__content[data-expanded] { - animation: contentShow 300ms ease-out; - } - - .alert-dialog__header { - display: flex; - align-items: baseline; - justify-content: space-between; - margin-bottom: 12px; - } - - .alert-dialog__close-button { - height: 16px; - width: 16px; - color: hsl(240 5% 34%); - } - - .alert-dialog__title { - font-size: 20px; - font-weight: 500; - color: hsl(240 6% 10%); - } - - .alert-dialog__description { - font-size: 16px; - color: hsl(240 5% 26%); - } - - @keyframes overlayShow { - from { - opacity: 0; - } - to { - opacity: 1; - } - } - - @keyframes overlayHide { - from { - opacity: 1; - } - to { - opacity: 0; - } - } - - @keyframes contentShow { - from { - opacity: 0; - transform: scale(0.96); - } - to { - opacity: 1; - transform: scale(1); - } - } - - @keyframes contentHide { - from { - opacity: 1; - transform: scale(1); - } - to { - opacity: 0; - transform: scale(0.96); - } - } - ``` - - - {/* */} -
+:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/alert-dialog.tsx#L4-L37" + +``` + +```css title="style.css" tab file="../../../../examples/alert-dialog.module.css" + +``` + +::: ## Usage @@ -246,7 +96,7 @@ An initial, uncontrolled open value can be provided using the `defaultOpen` prop The `open` prop can be used to make the open state controlled. The `onOpenChange` event is fired when the user presses the trigger, close button or overlay, and receives the new value. -```tsx {3,6} +```tsx {4,7} import { createSignal } from "solid-js"; function ControlledExample() { @@ -317,8 +167,8 @@ function ControlledExample() { | Key | Description | | :-------------------------------- | :----------------------------------------------------- | -| Space | When focus is on the trigger, opens/closes the dialog. | -| Enter | When focus is on the trigger, opens/closes the dialog. | -| Tab | Moves focus to the next focusable element. | -| Shift + Tab | Moves focus to the previous focusable element. | -| Esc | Closes the dialog and moves focus to the trigger. | +| Space | When focus is on the trigger, opens/closes the dialog. | +| Enter | When focus is on the trigger, opens/closes the dialog. | +| Tab | Moves focus to the next focusable element. | +| Shift + Tab | Moves focus to the previous focusable element. | +| Esc | Closes the dialog and moves focus to the trigger. | diff --git a/apps/docs/src/routes/docs/core/(1)components/alert.mdx b/apps/docs/src/routes/docs/core/(1)components/alert.mdx new file mode 100644 index 00000000..f23b3a61 --- /dev/null +++ b/apps/docs/src/routes/docs/core/(1)components/alert.mdx @@ -0,0 +1,75 @@ +--- +title: Alert +badges: + - icon: npm + label: "Since v0.1.0" + - label: "@kobalte/core/alert" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/alert" + - icon: aria + label: "Alert" + url: "https://www.w3.org/WAI/ARIA/apg/patterns/alert/" +--- + +import { BasicExample } from "../../../../examples/alert"; + +# {frontmatter.title} + +Display a brief, important message in a way that attracts the user's attention without interrupting the user's task. + +## Import + +```ts +import { Alert } from "@kobalte/core/alert"; +// or +import { Root } from "@kobalte/core/alert"; +// or (deprecated) +import { Alert } from "@kobalte/core"; +``` + +## Features + +- Adhere to the [WAI ARIA Alert](https://www.w3.org/WAI/ARIA/apg/patterns/alert/) design pattern. + +## Anatomy + +The alert consists of : + +- **Alert:** The root container for an alert. + +```tsx + +``` + +## Example + +:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/alert.tsx#L3-L9" + +``` + +```css title="style.css" tab file="../../../../examples/alert.module.css" + +``` + +::: + +## API reference + +### Alert + +`Alert` is equivalent to the `Root` import from `@kobalte/core/alert` (and deprecated `Alert.Root`). + +Renders a `div` by default and support all its props. + +## Rendered elements + +| Component | Default rendered element | +| :-------- | :----------------------- | +| `Alert` | `div` | diff --git a/apps/docs/src/routes/docs/core/components/badge.mdx b/apps/docs/src/routes/docs/core/(1)components/badge.mdx similarity index 53% rename from apps/docs/src/routes/docs/core/components/badge.mdx rename to apps/docs/src/routes/docs/core/(1)components/badge.mdx index 652aed4a..47432aa4 100644 --- a/apps/docs/src/routes/docs/core/components/badge.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/badge.mdx @@ -1,7 +1,17 @@ -import { Preview, TabsSnippets, Callout } from "../../../../components"; +--- +title: Badge +badges: + - icon: npm + label: "Since v0.13.8" + - label: "@kobalte/core/badge" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/badge" +--- + import { BasicExample } from "../../../../examples/badge"; -# Badge +# {frontmatter.title} A `Badge` component is used to display small pieces of information or status indicators. @@ -30,45 +40,21 @@ The badge consists of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { Badge } from "@kobalte/core/badge"; - import "./style.css"; - - function App() { - return ( - - 5 messages - - ); - } - ``` - - - - ```css - .badge { - display: inline-block; - padding: 0.2em 0.4em; - background-color: hsl(201 96% 32%); - color: white; border-radius: 12px; - font-size: 0.875rem; - } - ``` - - - {/* */} - +:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/badge.tsx#L3-L11" + +``` + +```css title="style.css" tab file="../../../../examples/badge.module.css" + +``` + +::: ## API Reference diff --git a/apps/docs/src/routes/docs/core/components/breadcrumbs.mdx b/apps/docs/src/routes/docs/core/(1)components/breadcrumbs.mdx similarity index 62% rename from apps/docs/src/routes/docs/core/components/breadcrumbs.mdx rename to apps/docs/src/routes/docs/core/(1)components/breadcrumbs.mdx index a84a3a48..c34016f4 100644 --- a/apps/docs/src/routes/docs/core/components/breadcrumbs.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/breadcrumbs.mdx @@ -1,7 +1,20 @@ -import { Preview, TabsSnippets, Callout } from "../../../../components"; +--- +title: Breadcrumbs +badges: + - icon: npm + label: "Since v0.5.0" + - label: "@kobalte/core/breadcrumbs" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/breadcrumbs" + - icon: aria + label: "Breadcrumb" + url: "https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/" +--- + import { BasicExample, CustomSeparatorExample } from "../../../../examples/breadcrumbs"; -# Breadcrumbs +# {frontmatter.title} Show hierarchy and navigational context for a user’s location within an application. @@ -44,88 +57,21 @@ The breadcrumbs consist of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { Breadcrumbs } from "@kobalte/core/breadcrumbs"; - import "./style.css"; - - function App() { - return ( - - - - ); - } - ``` - - - - ```css - .breadcrumbs__list { - display: inline-flex; - align-items: center; - } - - .breadcrumbs__item { - display: inline-flex; - align-items: center; - } - - .breadcrumbs__link { - font-weight: 500; - color: hsl(240 4% 16%); - } - - .breadcrumbs__link[data-disabled] { - color: hsl(240 4% 46%); - } - - .breadcrumbs__link:hover { - color: hsl(201 96% 32%); - } - - .breadcrumbs__link[data-current] { - font-weight: 400; - } - - .breadcrumbs__separator { - display: inline-block; - margin: 0 4px; - color: hsl(240 5% 65%); - } - ``` - - - {/* */} - +:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/breadcrumbs.tsx#L4-L30" + +``` + +```css title="style.css" tab file="../../../../examples/breadcrumbs.module.css" + +``` + +::: ## Usage @@ -133,11 +79,13 @@ The breadcrumbs consist of: Use the `separator` prop to provide a default content for all `Breadcrumbs.Separator`. You can pass it a `string` or a SolidJS component. - - - +:::preview + + + +--- -```tsx {0,4} +```tsx {1,5} import { ChevronRightIcon } from "some-icon-library"; function App() { @@ -167,11 +115,10 @@ function App() { } ``` - - - You can also override each `Breadcrumbs.Separator` content by providing your own `children`. - - +::: +:::tip +You can also override each `Breadcrumbs.Separator` content by providing your own `children`. +::: ## API Reference diff --git a/apps/docs/src/routes/docs/core/(1)components/button.mdx b/apps/docs/src/routes/docs/core/(1)components/button.mdx new file mode 100644 index 00000000..9f6deda6 --- /dev/null +++ b/apps/docs/src/routes/docs/core/(1)components/button.mdx @@ -0,0 +1,92 @@ +--- +title: Button +badges: + - icon: npm + label: "Since v0.1.0" + - label: "@kobalte/core/button" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/button" + - icon: aria + label: "Button" + url: "https://www.w3.org/WAI/ARIA/apg/patterns/button/" +--- + +import { BasicExample } from "../../../../examples/button"; + +# {frontmatter.title} + +Enables users to trigger an action or event, such as submitting a form, opening a dialog, canceling an action, or performing a delete operation. + +## Import + +```ts +import { Button } from "@kobalte/core/button"; +// or +import { Root } from "@kobalte/core/button"; +// or (deprecated) +import { Button } from "@kobalte/core"; +``` + +## Features + +- Native HTML `
` for one visible month (or duration). Accepts an `offset` prop to show additional months. +- **Calendar.GridHeader:** The `` section. +- **Calendar.GridHeaderRow:** A row of day-name header cells. +- **Calendar.GridHeaderCell:** A single day-name cell (e.g. "Mo"). +- **Calendar.GridBody:** The `` section. +- **Calendar.GridBodyRow:** A week row. Requires a `weekIndex` prop. +- **Calendar.GridBodyCell:** A `
` wrapping one day. Requires a `date` prop. +- **Calendar.GridBodyCellTrigger:** The interactive button inside each cell. + +```tsx + + + + + + + + + + + {(day) => {day()}} + + + + {(weekIndex) => ( + + {(date) => ( + }> + {(d) => ( + + + + )} + + )} + + )} + + + + +``` + +## Example + + + + + + + + index.tsx + style.css + + {/* */} + + ```tsx + import { Show } from "solid-js"; + import { Calendar } from "@kobalte/core/calendar"; + import { GregorianCalendar } from "@internationalized/date"; + import "./style.css"; + + const createCalendar = () => new GregorianCalendar(); + + function App() { + return ( + + + + ‹ + + + + › + + + + + + + {(day) => ( + + {day().slice(0, 2)} + + )} + + + + {(weekIndex) => ( + + {(date) => ( + }> + {(d) => ( + + + + )} + + )} + + )} + + + + + ); + } + ``` + + + ```css + .calendar { + display: inline-block; + background-color: white; + border: 1px solid hsl(214 32% 91%); + border-radius: 10px; + padding: 16px; + } + + .calendar__header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; + } + + .calendar__heading { + font-size: 14px; + font-weight: 600; + } + + .calendar__nav-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 6px; + border: 1px solid hsl(214 32% 91%); + background-color: white; + cursor: pointer; + } + + .calendar__nav-button[disabled] { + opacity: 0.4; + cursor: default; + } + + .calendar__body { + display: flex; + gap: 24px; + } + + .calendar__grid { + border-collapse: collapse; + } + + .calendar__day-name { + font-size: 11px; + text-align: center; + padding: 4px 0 8px; + text-transform: uppercase; + } + + .calendar__cell-trigger { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 9999px; + font-size: 13px; + cursor: pointer; + } + + .calendar__cell-trigger[data-selected] { + background-color: hsl(221 83% 53%); + color: white; + } + + .calendar__cell-trigger[data-today] { + font-weight: 600; + color: hsl(221 83% 53%); + } + + .calendar__cell-trigger[data-disabled] { + opacity: 0.3; + cursor: default; + } + + .calendar__cell-trigger[data-unavailable] { + text-decoration: line-through; + opacity: 0.4; + cursor: default; + } + + .calendar__cell-trigger[data-outside-month] { + opacity: 0.2; + } + ``` + + {/* */} + + +## Usage + +### Selection modes + +Use `selectionMode` to control what the user can pick. + +**Single** — default, picks one date at a time: + +```tsx + + ... + +``` + +**Multiple** — click any day to toggle it; multiple dates can be selected simultaneously: + + + + + +```tsx + + ... + +``` + +**Range** — click a start date, then an end date to select a contiguous span. Show multiple months side-by-side by setting `visibleDuration` and rendering additional `Calendar.Grid` elements with an `offset`: + + + + + +```tsx + + ... + + + + + +``` + +### Controlled value + +Pass `value` and `onChange` to control the selection externally. The value type matches the selection mode: `DateValue` for `"single"`, `DateValue[]` for `"multiple"`, and `RangeValue` for `"range"`. + + + + + +```tsx +import { createSignal } from "solid-js"; +import type { DateValue } from "@kobalte/core/calendar"; + +function App() { + const [value, setValue] = createSignal(null); + + return ( + setValue(v)} + > + ... + + ); +} +``` + +### Unavailable dates + +Use `isDateUnavailable` to mark specific dates as unselectable. Unavailable dates receive `data-unavailable` and cannot be focused via keyboard navigation. + + + + + +```tsx +import { isWeekend } from "@internationalized/date"; + + isWeekend(date, "en-US")} +> + ... + +``` + +In range mode, combine with `allowsNonContiguousRanges` to permit ranges that span unavailable dates: + +```tsx + isWeekend(date, "en-US")} + allowsNonContiguousRanges +> + ... + +``` + +### Min / max dates + +Constrain selectable dates with `minValue` and `maxValue`. Dates outside the range are disabled. + +```tsx +import { CalendarDate } from "@internationalized/date"; + + + ... + +``` + +### Default value + +Use `defaultValue` for an uncontrolled calendar with an initial selection: + +```tsx +import { CalendarDate } from "@internationalized/date"; + + + ... + +``` + +### Read only + +Set `readOnly` to show the current selection without allowing changes: + +```tsx + + ... + +``` + +### Disabled + +Set `disabled` to render the calendar in a non-interactive state: + +```tsx + + ... + +``` + +### Locale + +Pass a BCP 47 locale string to `locale` to control formatting (day names, month names, week start day): + +```tsx + + ... + +``` + +### Non-Gregorian calendars + +Supply any `@internationalized/date` calendar to `createCalendar`. The component adapts its grid logic automatically: + +```tsx +import { HebrewCalendar } from "@internationalized/date"; + + new HebrewCalendar()} selectionMode="single"> + ... + +``` + +## Date picker pattern + +Combine `Calendar` with `Popover` to build a date picker — an input that shows the selected date and a button that opens the calendar in a floating panel. + + + + + + + + index.tsx + style.css + + {/* */} + + ```tsx + import { createMemo, createSignal, Show } from "solid-js"; + import { Calendar, type DateValue } from "@kobalte/core/calendar"; + import { Popover } from "@kobalte/core/popover"; + import { GregorianCalendar } from "@internationalized/date"; + import "./style.css"; + + const createCalendar = () => new GregorianCalendar(); + + function DatePicker() { + const [open, setOpen] = createSignal(false); + const [value, setValue] = createSignal(null); + + const formatted = createMemo(() => { + const v = value(); + if (!v) return ""; + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + return new Intl.DateTimeFormat("en-US", { + month: "long", day: "numeric", year: "numeric", + }).format(v.toDate(tz)); + }); + + return ( + + + + + {/* calendar icon */} + + + + + { + setValue(v as DateValue); + setOpen(false); + }} + class="calendar" + > + + + + + + + + + + {(day) => {day().slice(0, 2)}} + + + + {(weekIndex) => ( + + {(date) => ( + }> + {(d) => ( + + + + )} + + )} + + )} + + + + + + + + ); + } + ``` + + + ```css + .datepicker { + display: inline-flex; + align-items: center; + border: 1px solid hsl(214 32% 91%); + border-radius: 8px; + background-color: white; + overflow: hidden; + } + + .datepicker:focus-within { + box-shadow: 0 0 0 2px hsl(221 83% 53% / 0.3); + border-color: hsl(221 83% 53%); + } + + .datepicker__input { + flex: 1; + border: none; + outline: none; + padding: 0 12px; + height: 38px; + font-size: 14px; + background: transparent; + cursor: default; + min-width: 180px; + } + + .datepicker__input::placeholder { + color: hsl(215 16% 65%); + } + + .datepicker__button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 38px; + border: none; + border-left: 1px solid hsl(214 32% 91%); + background-color: white; + cursor: pointer; + } + + .datepicker__button:hover, + .datepicker__button[data-expanded] { + background-color: hsl(210 40% 96%); + color: hsl(221 83% 53%); + } + + .datepicker__popover { + z-index: 50; + transform-origin: var(--kb-popover-content-transform-origin); + animation: datepicker-hide 150ms ease-in forwards; + } + + .datepicker__popover[data-expanded] { + animation: datepicker-show 150ms ease-out; + } + + @keyframes datepicker-show { + from { opacity: 0; transform: translateY(-4px); } + to { opacity: 1; transform: translateY(0); } + } + + @keyframes datepicker-hide { + from { opacity: 1; transform: translateY(0); } + to { opacity: 0; transform: translateY(-4px); } + } + ``` + + {/* */} + + +## Data attributes + +### Calendar.GridBodyCellTrigger + +| Attribute | Value | +|---|---| +| `data-selected` | Present when the date is selected. | +| `data-today` | Present when the date is today. | +| `data-highlighted` | Present when the date is focused (keyboard). | +| `data-disabled` | Present when the date is disabled. | +| `data-unavailable` | Present when `isDateUnavailable` returns `true`. | +| `data-invalid` | Present when `validationState="invalid"`. | +| `data-weekend` | Present for weekend dates. | +| `data-outside-month` | Present when the date falls outside the grid's month. | +| `data-outside-visible-range` | Present when the date falls outside the visible range. | +| `data-selection-start` | Present on the start date of a range selection. | +| `data-selection-end` | Present on the end date of a range selection. | +| `data-value` | The ISO string of the date (e.g. `"2025-06-15"`). | +| `data-type` | Always `"day"`. | + +## Credits + +Calendar logic ported from [corvu/calendar](https://github.com/corvudev/corvu/tree/main/packages/calendar) by Jasmin Noetzli, licensed MIT. State management also draws on [React Spectrum's `useCalendarState`](https://github.com/adobe/react-spectrum), licensed Apache 2.0. diff --git a/apps/docs/src/routes/docs/core/components/checkbox.mdx b/apps/docs/src/routes/docs/core/(1)components/checkbox.mdx similarity index 82% rename from apps/docs/src/routes/docs/core/components/checkbox.mdx rename to apps/docs/src/routes/docs/core/(1)components/checkbox.mdx index 5d2a226f..8599ee7e 100644 --- a/apps/docs/src/routes/docs/core/components/checkbox.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/checkbox.mdx @@ -1,4 +1,17 @@ -import { Preview, TabsSnippets, Kbd } from "../../../../components"; +--- +title: Checkbox +badges: + - icon: npm + label: "Since v0.1.0" + - label: "@kobalte/core/checkbox" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/checkbox" + - icon: aria + label: "Checkbox" + url: "https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/" +--- + import { BasicExample, ControlledExample, @@ -8,7 +21,7 @@ import { HTMLFormExample, } from "../../../../examples/checkbox"; -# Checkbox +# {frontmatter.title} A control that allows the user to toggle between checked and not checked. @@ -56,75 +69,21 @@ The checkbox consists of: ## Example - - - - - - - index.tsx - style.css - -{/* */} - - ```tsx - import { Checkbox } from "@kobalte/core/checkbox"; - import { CheckIcon } from "some-icon-library"; - import "./style.css"; - - function App() { - return ( - - - - - - - - Subscribe - - ); - } - ``` - - - - ```css - .checkbox { - display: inline-flex; - align-items: center; - } - - .checkbox__control { - height: 20px; - width: 20px; - border-radius: 6px; - border: 1px solid hsl(240 5% 84%); - background-color: hsl(240 6% 90%); - } - - .checkbox__input:focus-visible + .checkbox__control { - outline: 2px solid hsl(200 98% 39%); - outline-offset: 2px; - } - - .checkbox__control[data-checked] { - border-color: hsl(200 98% 39%); - background-color: hsl(200 98% 39%); - color: white; - } - - .checkbox__label { - margin-left: 6px; - color: hsl(240 6% 10%); - font-size: 14px; - user-select: none; - } - ``` - - -{/* */} - +:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/checkbox.tsx#L6-L20" + +``` + +```css title="style.css" tab file="../../../../examples/checkbox.module.css" + +``` + +::: ## Usage @@ -132,23 +91,29 @@ The checkbox consists of: An initial, uncontrolled value can be provided using the `defaultChecked` prop. - - - +:::preview + + + +--- ```tsx ... ``` +::: + ### Controlled checked The `checked` prop can be used to make the checked state controlled. The `onChange` event is fired when the user presses the checkbox, and receives the new value. - - - +:::preview + + -```tsx {3,7} +--- + +```tsx {4,8} import { createSignal } from "solid-js"; function ControlledExample() { @@ -165,15 +130,19 @@ function ControlledExample() { } ``` +::: + ### Description The `Checkbox.Description` component can be used to associate additional help text with a checkbox. - - - +:::preview -```tsx {8} + + +--- + +```tsx {9} @@ -186,17 +155,21 @@ The `Checkbox.Description` component can be used to associate additional help te ``` +::: + ### Error message The `Checkbox.ErrorMessage` component can be used to help the user fix a validation error. It should be combined with the `validationState` prop to semantically mark the checkbox as invalid for assistive technologies. By default, it will render only when the `validationState` prop is set to `invalid`, use the `forceMount` prop to always render the error message (ex: for usage with animation libraries). - - - +:::preview + + -```tsx {9,17} +--- + +```tsx {10,18} import { createSignal } from "solid-js"; function ErrorMessageExample() { @@ -221,15 +194,19 @@ function ErrorMessageExample() { } ``` +::: + ### HTML forms The `name` and `value` props can be used for integration with HTML forms. - - - +:::preview + + -```tsx {7} +--- + +```tsx {8} function HTMLFormExample() { const onSubmit = (e: SubmitEvent) => { // handle form submission. @@ -249,6 +226,8 @@ function HTMLFormExample() { } ``` +::: + ## API Reference ### Checkbox @@ -316,4 +295,4 @@ function HTMLFormExample() { | Key | Description | | :--------------- | :------------------------------- | -| Space | Toggles the checkbox on and off. | +| Space | Toggles the checkbox on and off. | diff --git a/apps/docs/src/routes/docs/core/components/collapsible.mdx b/apps/docs/src/routes/docs/core/(1)components/collapsible.mdx similarity index 56% rename from apps/docs/src/routes/docs/core/components/collapsible.mdx rename to apps/docs/src/routes/docs/core/(1)components/collapsible.mdx index d98c146f..ce693c91 100644 --- a/apps/docs/src/routes/docs/core/components/collapsible.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/collapsible.mdx @@ -1,7 +1,20 @@ -import { Preview, TabsSnippets, Kbd } from "../../../../components"; +--- +title: Collapsible +badges: + - icon: npm + label: "Since v0.5.0" + - label: "@kobalte/core/collapsible" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/collapsible" + - icon: aria + label: "Disclosure" + url: "https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/" +--- + import { BasicExample } from "../../../../examples/collapsible"; -# Collapsible +# {frontmatter.title} An interactive component which expands/collapses a content. @@ -37,115 +50,21 @@ The collapsible consists of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { Collapsible } from "@kobalte/core/collapsible"; - import { ChevronDownIcon } from "some-icon-library"; - import "./style.css"; - - function App() { - return ( - - - What is Kobalte? - - - -

- Kobalte is a UI toolkit for building accessible web apps and design systems with SolidJS. - It provides a set of low-level UI components and primitives which can be the foundation - for your design system implementation. -

-
-
- ); - } - ``` - -
- - ```css - .collapsible { - width: 300px; - } - - .collapsible__trigger { - display: inline-flex; - align-items: center; - justify-content: space-between; - width: 100%; - padding: 14px; - font-weight: 600; - border: 1px solid hsl(240 5% 84%); - color: hsl(240 4% 16%); - text-align: left; - outline: none; - } - - .collapsible__trigger:focus-visible { - outline: 2px solid hsl(200 98% 39%); - outline-offset: 2px; - } - - .collapsible__trigger-icon { - width: 20px; - height: 20px; - transition: transform 250ms; - } - - .collapsible__trigger[data-expanded] .collapsible__trigger-icon { - transform: rotateZ(180deg); - } - - .collapsible__content { - width: 100%; - overflow: hidden; - border: 1px solid hsl(240 5% 84%); - border-top: none; - color: hsl(240 4% 16%); - animation: slideUp 300ms ease-out; - } - - .collapsible__content[data-expanded] { - animation: slideDown 300ms ease-out; - } - - .collapsible__content-text { - padding: 16px; - } - - @keyframes slideDown { - from { - height: 0; - } - to { - height: var(--kb-collapsible-content-height); - } - } - - @keyframes slideUp { - from { - height: var(--kb-collapsible-content-height); - } - to { - height: 0; - } - } - ``` - - - {/* */} -
+:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/collapsible.tsx#L4-L23" + +``` + +```css title="style.css" tab file="../../../../examples/collapsible.module.css" + +``` + +::: ## Usage @@ -219,5 +138,5 @@ We expose the CSS custom properties `--kb-collapsible-content-width` and `--kb-c | Key | Description | | :--------------- | :---------------------------------------------------------- | -| Space | When focus is on the trigger, opens/closes the collapsible. | -| Enter | When focus is on the trigger, opens/closes the collapsible. | +| Space | When focus is on the trigger, opens/closes the collapsible. | +| Enter | When focus is on the trigger, opens/closes the collapsible. | diff --git a/apps/docs/src/routes/docs/core/components/color-area.mdx b/apps/docs/src/routes/docs/core/(1)components/color-area.mdx similarity index 80% rename from apps/docs/src/routes/docs/core/components/color-area.mdx rename to apps/docs/src/routes/docs/core/(1)components/color-area.mdx index 166ac1d7..a26c72c6 100644 --- a/apps/docs/src/routes/docs/core/components/color-area.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/color-area.mdx @@ -1,4 +1,14 @@ -import { Preview, TabsSnippets, Kbd } from "../../../../components"; +--- +title: Color Area +badges: + - icon: npm + label: "Since v0.13.9" + - label: "@kobalte/core/color-area" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/color-area" +--- + import { BasicExample, DefaultValueExample, @@ -7,7 +17,7 @@ import { HTMLFormExample, } from "../../../../examples/color-area"; -# Color Area +# {frontmatter.title} Allows users to adjust two channels of an RGB, HSL, or HSB color value against a two-dimensional gradient background. @@ -57,80 +67,21 @@ The color area consists of: ## Example - - - - - - - index.tsx - style.css - -{/* */} - - ```tsx - import { ColorArea } from "@kobalte/core/color-area"; - import { parseColor } from "@kobalte/core/colors"; - import "./style.css"; - - function App() { - return ( - - Label - - - - - - - - ); - } - ``` - - - - ```css - .ColorAreaRoot { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - user-select: none; - touch-action: none; - width: 200px; - } - - .ColorAreaBackground { - position: relative; - border-radius: 6px; - height: 150px; - width: 150px; - } - - .ColorAreaThumb { - display: block; - width: 16px; - height: 16px; - border-radius: 9999px; - border: 2px solid #fff; - background: var(--kb-color-current); - } - - .ColorAreaThumb:focus { - outline: none; - } - - .ColorArealLabel { - font-size: 14px; - font-weight: 500; - user-select: none; - } - ``` - - -{/* */} - +:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/color-area.tsx#L5-L19" + +``` + +```css title="style.css" tab file="../../../../examples/color-area.module.css" + +``` + +::: ## Usage @@ -140,11 +91,13 @@ If no `xChannel` or `yChannel` is provided, for the RGB color space, the red col ### Default value - - - +:::preview + + -```tsx {0} +--- + +```tsx {1} Label @@ -156,13 +109,17 @@ If no `xChannel` or `yChannel` is provided, for the RGB color space, the red col ``` +::: + ### Controlled value - - - +:::preview + + -```tsx {2, 4} +--- + +```tsx {3,5} import { createSignal } from "solid-js"; const [value, setValue] = createSignal(parseColor("hsl(0, 100%, 50%)")); @@ -180,15 +137,19 @@ const [value, setValue] = createSignal(parseColor("hsl(0, 100%, 50%)")); ; ``` +::: + ### xChannel and yChannel The color channel for each axis of a color area can be specified using the `xChannel` and `yChannel` props. An array of channel names for a color can be returned using the color.getColorChannels method. - - - +:::preview + + -```tsx {1, 3} +--- + +```tsx {2,4} const [value, setValue] = createSignal(parseColor("rgb(100, 149, 237)")); const [rChannel, gChannel, bChannel] = value().getColorChannels(); @@ -203,15 +164,19 @@ const [rChannel, gChannel, bChannel] = value().getColorChannels(); ; ``` +::: + ### HTML forms ColorArea supports the `xName` and `yName` props for integration with HTML forms. - - - +:::preview + + -```tsx {0} +--- + +```tsx {1} Label @@ -223,6 +188,8 @@ ColorArea supports the `xName` and `yName` props for integration with HTML forms ``` +::: + ## API Reference ### ColorArea @@ -292,11 +259,11 @@ The current color is available on the thumb using the custom css property `--kb- | Key | Description | | :-------------------- | :--------------------------------------------------------------------------- | -| PageUp | Increments the value of the thumb in the vertical axis by a larger step. | -| PageDown | Decrements the value of the thumb in the vertical axis by a larger step. | -| ArrowDown | Decrements the value of the thumb in the vertical axis by the step amount. | -| ArrowUp | Increments the value of the thumb in the vertical axis by the step amount. | -| ArrowRight | Increments the value of the thumb in the horizontal axis by the step amount. | -| ArrowLeft | Decrements the value of the thumb in the vertical axis by the step amount. | -| Home | Decrements the value of the thumb in the horizontal axis by a larger step. | -| End | Increments the value of the thumb in the horizontal axis by a larger step. | +| PageUp | Increments the value of the thumb in the vertical axis by a larger step. | +| PageDown | Decrements the value of the thumb in the vertical axis by a larger step. | +| ArrowDown | Decrements the value of the thumb in the vertical axis by the step amount. | +| ArrowUp | Increments the value of the thumb in the vertical axis by the step amount. | +| ArrowRight | Increments the value of the thumb in the horizontal axis by the step amount. | +| ArrowLeft | Decrements the value of the thumb in the vertical axis by the step amount. | +| Home | Decrements the value of the thumb in the horizontal axis by a larger step. | +| End | Increments the value of the thumb in the horizontal axis by a larger step. | diff --git a/apps/docs/src/routes/docs/core/components/color-channel-field.mdx b/apps/docs/src/routes/docs/core/(1)components/color-channel-field.mdx similarity index 77% rename from apps/docs/src/routes/docs/core/components/color-channel-field.mdx rename to apps/docs/src/routes/docs/core/(1)components/color-channel-field.mdx index 7e2f693e..5982f306 100644 --- a/apps/docs/src/routes/docs/core/components/color-channel-field.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/color-channel-field.mdx @@ -1,4 +1,17 @@ -import { Preview, TabsSnippets } from "../../../../components"; +--- +title: Color Channel Field +badges: + - icon: npm + label: "Since v0.13.9" + - label: "@kobalte/core/color-channel-field" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/color-channel-field" + - icon: aria + label: "Spinbutton" + url: "https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/" +--- + import { BasicExample, DefaultValueExample, @@ -9,7 +22,7 @@ import { TriggersExample, } from "../../../../examples/color-channel-field"; -# Color Channel Field +# {frontmatter.title} A number input that allow users to edit individual color channel value. @@ -61,126 +74,21 @@ The color channel field consists of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { ColorChannelField } from "@kobalte/core/color-channel-field"; - import { parseColor } from "@kobalte/core/colors"; - import "./style.css"; - - function App() { - return ( - - - Hue - -
- - - - - - - -
-
- ); - } - ``` - -
- - ```css - .color-channel-field { - display: flex; - flex-direction: column; - gap: 4px; - } - - .color-channel-field__label { - color: hsl(240 6% 10%); - font-size: 14px; - font-weight: 500; - user-select: none; - } - - .color-channel-field__input { - display: inline-flex; - width: 200px; - border-radius: 6px; - padding: 6px 12px; - font-size: 16px; - outline: none; - background-color: white; - border: 1px solid hsl(240 6% 90%); - color: hsl(240 4% 16%); - transition: - border-color 250ms, - color 250ms; - } - - .color-channel-field__group { - position: relative; - border-radius: 6px; - } - - .color-channel-field__increment { - top: .25rem; - border-top-left-radius: .25rem; - border-top-right-radius: .25rem; - } - - .color-channel-field__decrement { - bottom: .25rem; - border-bottom-left-radius: .25rem; - border-bottom-right-radius: .25rem; - } - - .color-channel-field__increment, .color-channel-field__decrement { - right: .25rem; - position: absolute; - height: 1rem; - width: 1rem; - background: rgba(0,0,0,0.1); - cursor: default; - } - - .color-channel-field__increment:hover, .color-channel-field__decrement:hover { - background: rgba(0,0,0,0.2); - } - - .color-channel-field__input:hover { - border-color: hsl(240 5% 65%); - } - - .color-channel-field__input:focus-visible { - outline: none; - } - - .color-channel-field__group:focus-within { - outline: 2px solid hsl(200 98% 39%); - outline-offset: 2px; - } - ``` - - - {/* */} -
+:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/color-channel-field.tsx#L6-L31" + +``` + +```css title="style.css" tab file="../../../../examples/color-channel-field.module.css" + +``` + +::: ## Usage @@ -189,11 +97,13 @@ This must be one of the channels included in the color value, for example, for R ### Default value - - - +:::preview + + -```tsx {0} +--- + +```tsx {1} Saturation
@@ -204,13 +114,17 @@ This must be one of the channels included in the color value, for example, for R ``` +::: + ### Controlled value - - - +:::preview + + -```tsx {3,8-9} +--- + +```tsx {4,9-10} import { createSignal } from "solid-js"; function ControlledExample() { @@ -232,15 +146,19 @@ function ControlledExample() { } ``` +::: + ### Description The `ColorChannelField.Description` component can be used to associate additional help text with a color channel field. - - - +:::preview + + + +--- -```tsx {7} +```tsx {8} Hue
@@ -252,17 +170,21 @@ The `ColorChannelField.Description` component can be used to associate additiona ``` +::: + ### Error message The `ColorChannelField.ErrorMessage` component can be used to help the user fix a validation error. It should be combined with the `validationState` prop to semantically mark the field as invalid for assistive technologies. By default, it will render only when the `validationState` prop is set to `invalid`, use the `forceMount` prop to always render the error message (ex: for usage with animation libraries). - - - +:::preview + + + +--- -```tsx {10,18} +```tsx {11,19} import { createSignal } from "solid-js"; function ErrorMessageExample() { @@ -287,17 +209,21 @@ function ErrorMessageExample() { } ``` +::: + ### HTML forms The color channel field `name` prop along with `` can be used for integration with HTML forms. Only the raw value is passed to the form. If the formatted value is wanted (unrecommended) set the `name` attribute on ``. - - - +:::preview + + -```tsx {7,9} +--- + +```tsx {8,10} function HTMLFormExample() { const onSubmit = (e: SubmitEvent) => { // handle form submission. @@ -323,15 +249,19 @@ function HTMLFormExample() { } ``` +::: + ### Triggers The color channel field supports optional increment/decrement triggers that are easily customizable. - - - +:::preview -```tsx {3,5} + + +--- + +```tsx {4,6} Brightness
@@ -346,6 +276,8 @@ The color channel field supports optional increment/decrement triggers that are ``` +::: + ## API Reference ### ColorChannelField diff --git a/apps/docs/src/routes/docs/core/components/color-field.mdx b/apps/docs/src/routes/docs/core/(1)components/color-field.mdx similarity index 76% rename from apps/docs/src/routes/docs/core/components/color-field.mdx rename to apps/docs/src/routes/docs/core/(1)components/color-field.mdx index ee3c6837..f7409054 100644 --- a/apps/docs/src/routes/docs/core/components/color-field.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/color-field.mdx @@ -1,4 +1,14 @@ -import { Preview, TabsSnippets } from "../../../../components"; +--- +title: Color Field +badges: + - icon: npm + label: "Since v0.13.9" + - label: "@kobalte/core/color-field" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/color-field" +--- + import { ControlledExample, DefaultValueExample, @@ -8,7 +18,7 @@ import { ErrorMessageExample, } from "../../../../examples/color-field"; -# Color Field +# {frontmatter.title} Allows users to enter and adjust a hex color value. @@ -51,94 +61,21 @@ The color field consists of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { ColorField } from "@kobalte/core/color-field"; - import "./style.css"; - - function App() { - return ( - - Favorite hex color - - - ); - } - ``` - - - - ```css - .color-field { - display: flex; - flex-direction: column; - gap: 4px; - } - - .color-field__label { - color: hsl(240 6% 10%); - font-size: 14px; - font-weight: 500; - user-select: none; - } - - .color-field__input { - display: inline-flex; - width: 200px; - border-radius: 6px; - padding: 6px 12px; - font-size: 16px; - outline: none; - background-color: white; - border: 1px solid hsl(240 6% 90%); - color: hsl(240 4% 16%); - transition: border-color 250ms, color 250ms; - } - - .color-field__input:hover { - border-color: hsl(240 5% 65%); - } - - .color-field__input:focus-visible { - outline: 2px solid hsl(200 98% 39%); - outline-offset: 2px; - } - - .color-field__input[data-invalid] { - border-color: hsl(0 72% 51%); - color: hsl(0 72% 51%); - } - - .color-field__input::placeholder { - color: hsl(240 4% 46%); - } - - .color-field__description { - color: hsl(240 5% 26%); - font-size: 12px; - user-select: none; - } - - .color-field__error-message { - color: hsl(0 72% 51%); - font-size: 12px; - user-select: none; - } - ``` - - - {/* */} - +:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/color-field.tsx#L5-L16" + +``` + +```css title="style.css" tab file="../../../../examples/color-field.module.css" + +``` + +::: ## Usage @@ -146,26 +83,32 @@ The color field consists of: An initial, uncontrolled value can be provided using the `defaultValue` prop. - - - +:::preview -```tsx {0} + + +--- + +```tsx {1} Favorite hex color ``` +::: + ### Controlled value The `value` prop can be used to make the value controlled. The `onChange` event is fired when the user type into the input and receive the new value. - - - +:::preview -```tsx {3,7} + + +--- + +```tsx {4,8} import { createSignal } from "solid-js"; function ControlledExample() { @@ -183,15 +126,19 @@ function ControlledExample() { } ``` +::: + ### Description The `ColorField.Description` component can be used to associate additional help text with a color field. - - - +:::preview -```tsx {3} + + +--- + +```tsx {4} Favorite hex color @@ -201,17 +148,21 @@ The `ColorField.Description` component can be used to associate additional help ``` +::: + ### Error message The `ColorField.ErrorMessage` component can be used to help the user fix a validation error. It should be combined with the `validationState` prop to semantically mark the color field as invalid for assistive technologies. By default, it will render only when the `validationState` prop is set to `invalid`, use the `forceMount` prop to always render the error message (ex: for usage with animation libraries). - - - +:::preview -```tsx {9,14} + + +--- + +```tsx {10,15} import { createSignal } from "solid-js"; function ErrorMessageExample() { @@ -231,15 +182,19 @@ function ErrorMessageExample() { } ``` +::: + ### HTML forms The color field `name` prop can be used for integration with HTML forms. - - - +:::preview -```tsx {7} + + +--- + +```tsx {8} function HTMLFormExample() { const onSubmit = (e: SubmitEvent) => { // handle form submission. @@ -256,6 +211,8 @@ function HTMLFormExample() { } ``` +::: + ## API Reference ### ColorField diff --git a/apps/docs/src/routes/docs/core/(1)components/color-slider.mdx b/apps/docs/src/routes/docs/core/(1)components/color-slider.mdx new file mode 100644 index 00000000..7a88d7f4 --- /dev/null +++ b/apps/docs/src/routes/docs/core/(1)components/color-slider.mdx @@ -0,0 +1,451 @@ +--- +title: Color Slider +badges: + - icon: npm + label: "Since v0.13.9" + - label: "@kobalte/core/color-slider" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/color-slider" +--- + +import { + BasicExample, + DefaultValueExample, + ControlledValueExample, + VerticalSliderExample, + CustomValueExample, +} from "../../../../examples/color-slider"; + +# {frontmatter.title} + +Adjusts an individual channel of a color value. + +## Import + +```ts +import { ColorSlider } from "@kobalte/core/color-slider"; +// or +import { Root, Track, ... } from "@kobalte/core/color-slider"; +``` + +## Features + +- Can be controlled or uncontrolled. +- Localized color descriptions for screen reader users. +- Support for adjusting a single channel of RGBA, HSLA and HSBA colors, in both horizontal and vertical orientations. +- Support click or touch on track to change value. +- Support right or left direction. +- Support for custom value label. + +## Anatomy + +The color slider consists of: + +- **ColorSlider:** The root container for the color slider. +- **ColorSlider.Track:** The component that visually represents the color slider track. +- **ColorSlider.Thumb:** The thumb that is used to visually indicate a value in the color slider. +- **ColorSlider.Input:** The native html input that is visually hidden in the color slider thumb. +- **ColorSlider.Label:** The label that gives the user information on the color slider. +- **ColorSlider.ValueLabel:** The accessible label text representing the current value in a human-readable format. + +```tsx + + + + + + + + + +``` + +## Example + +:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/color-slider.tsx#L5-L25" + +``` + +```css title="style.css" tab file="../../../../examples/color-slider.module.css" + +``` + +::: + +## Usage + +The value provided to `defaultValue` or `value` should be `Color` object. You can obtain a Color object by using the `parseColor` function to parse a color from a string. The `channel` prop must also be provided to specify which color channel the slider should display. +This must be one of the channels included in the color value, for example, for RGB colors, the "red", "green", and "blue" channels are available. + +### Default value + +:::preview + + + +--- + +```tsx title="index.tsx" tab +import { ColorSlider } from "@kobalte/core/color-slider"; +import { parseColor } from "@kobalte/core/colors"; +import "./style.css"; + +function App() { + return ( + +
+ Hue + +
+ + + + + +
+ ); +} +``` + +```css title="style.css" tab +.ColorSliderRoot { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + user-select: none; + touch-action: none; + width: 200px; +} + +.ColorSliderTrack { + position: relative; + border-radius: 6px; + height: 24px; + width: 100%; +} + +.ColorSliderThumb { + display: block; + top: 4px; + width: 16px; + height: 16px; + border-radius: 9999px; + border: 2px solid #fff; + box-shadow: 0 0 0 1px #0000006b; +} + +.ColorSliderThumb:focus { + outline: none; +} + +.ColorSliderLabel { + display: flex; + justify-content: space-between; + width: 100%; +} +``` + +::: + +### Controlled value + +:::preview + + + +--- + +```tsx title="index.tsx" tab +import { createSignal } from "solid-js"; +import { ColorSlider } from "@kobalte/core/color-slider"; +import { parseColor } from "@kobalte/core/colors"; +import "./style.css"; + +function App() { + const [value, setValue] = createSignal(parseColor("hsl(0, 100%, 50%)")); + return ( + +
+ Hue + +
+ + + + + +
+ ); +} +``` + +```css title="style.css" tab +.ColorSliderRoot { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + user-select: none; + touch-action: none; + width: 200px; +} + +.ColorSliderTrack { + position: relative; + border-radius: 6px; + height: 24px; + width: 100%; +} + +.ColorSliderThumb { + display: block; + top: 4px; + width: 16px; + height: 16px; + border-radius: 9999px; + border: 2px solid #fff; + box-shadow: 0 0 0 1px #0000006b; +} + +.ColorSliderThumb:focus { + outline: none; +} + +.ColorSliderLabel { + display: flex; + justify-content: space-between; + width: 100%; +} +``` + +::: + +### Vertical Slider + +:::preview + + + +--- + +```tsx title="index.tsx" tab +import { ColorSlider } from "@kobalte/core/color-slider"; +import { parseColor } from "@kobalte/core/colors"; +import "./style.css"; + +function App() { + return ( + +
+ Hue + +
+ + + + + +
+ ); +} +``` + +```css title="style.css" tab +.ColorSliderRoot { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + user-select: none; + touch-action: none; + height: 200px; +} + +.ColorSliderTrack { + position: relative; + border-radius: 6px; + height: 100%; + width: 24px; +} + +.ColorSliderThumb { + display: block; + left: 4px; + width: 16px; + height: 16px; + border-radius: 9999px; + border: 2px solid #fff; + box-shadow: 0 0 0 1px #0000006b; +} + +.ColorSliderThumb:focus { + outline: none; +} + +.ColorSliderLabel { + display: flex; + justify-content: space-between; + width: 100%; +} +``` + +::: + +### Custom Value Label + +:::preview + + + +--- + +```tsx title="index.tsx" tab +import { ColorSlider } from "@kobalte/core/color-slider"; +import { parseColor } from "@kobalte/core/colors"; +import "./style.css"; + +function App() { + return ( + `${param.toFormat("rgb")}`} + > +
+ +
+ + + + + +
+ ); +} +``` + +```css title="style.css" tab +.ColorSliderRoot { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + user-select: none; + touch-action: none; + width: 200px; +} + +.ColorSliderTrack { + position: relative; + border-radius: 6px; + height: 24px; + width: 100%; +} + +.ColorSliderThumb { + display: block; + top: 4px; + width: 16px; + height: 16px; + border-radius: 9999px; + border: 2px solid #fff; + box-shadow: 0 0 0 1px #0000006b; +} + +.ColorSliderThumb:focus { + outline: none; +} + +.ColorSliderLabel { + display: flex; + justify-content: space-between; + width: 100%; +} +``` + +::: + +## API Reference + +### ColorSlider + +`ColorSlider` is equivalent to the `Root` import from `@kobalte/core/color-slider`. + +| Prop | Description | +| :-------------- | :-------------------------------------------------------------------------------------------------------------------------------------- | +| value | `Color`
The controlled value of the slider. Must be used in conjunction with `onChange`. | +| defaultValue | `Color`
The value of the slider when initially rendered. Use when you do not need to control the state of the slider. | +| channel | `ColorChannel`
The color channel that the slider manipulates. | +| colorSpace | `ColorSpace`
The color space that the slider operates in. The `channel` must be in this color space. | +| onChange | `(value: Color) => void`
Event handler called when the value changes. | +| onChangeEnd | `(value: Color) => void`
Event handler called when the value changes at the end of an interaction. | +| getValueLabel | `(param: Color) => string`
A function to get the accessible label text representing the current value in a human-readable format. | +| orientation | `'horizontal' \| 'vertical'`
The orientation of the slider. | +| name | `string`
The name of the slider, used when submitting an HTML form. | +| validationState | `'valid' \| 'invalid'`
Whether the slider should display its "valid" or "invalid" visual styling. | +| required | `boolean`
Whether the user must check a radio group item before the owning form can be submitted. | +| disabled | `boolean`
Whether the radio group is disabled. | +| readOnly | `boolean`
Whether the radio group items can be selected but not changed by the user. | +| translations | `ColorIntlTranslations`
Localization strings. | + +| Data attribute | Description | +| :---------------------------- | :--------------------------------------------------------------------------------- | +| data-orientation='horizontal' | Present when the slider has horizontal orientation. | +| data-orientation='vertical' | Present when the slider has vertical orientation. | +| data-valid | Present when the slider is valid according to the validation rules. | +| data-invalid | Present when the slider is invalid according to the validation rules. | +| data-required | Present when the user must slider an item before the owning form can be submitted. | +| data-disabled | Present when the slider is disabled. | +| data-readonly | Present when the slider is read only. | + +`ColorSlider.ValueLabel`, `ColorSlider.Input`, `ColorSlider.Thumb` and `ColorSlider.Track` share the same data-attributes. + +### ColorSlider.Thumb + +The current color is available on the thumb using the custom css property `--kb-color-current`. + +## Rendered elements + +| Component | Default rendered element | +| :------------------------- | :----------------------- | +| `ColorSlider` | `div` | +| `ColorSlider.Track` | `div` | +| `ColorSlider.Thumb` | `span` | +| `ColorSlider.Input` | `input` | +| `ColorSlider.ValueLabel` | `div` | +| `ColorSlider.Label` | `label` | +| `ColorSlider.Description` | `div` | +| `ColorSlider.ErrorMessage` | `div` | + +## Accessibility + +### Keyboard Interactions + +| Key | Description | +| :-------------------- | :------------------------------------------------------------------ | +| PageUp | Increases the value of the thumb by a larger step. | +| PageDown | Decreases the value of the thumb by a larger step. | +| ArrowDown | Decreases the value of the thumb by the step amount. | +| ArrowUp | Increases the value of the thumb by the step amount. | +| ArrowRight | Increments/decrements by the step value depending on `orientation`. | +| ArrowLeft | Increments/decrements by the step value depending on `orientation`. | +| Home | Sets the value of the thumb to the minimum value. | +| End | Sets the value of the thumb to the maximum value. | diff --git a/apps/docs/src/routes/docs/core/(1)components/color-swatch.mdx b/apps/docs/src/routes/docs/core/(1)components/color-swatch.mdx new file mode 100644 index 00000000..73028390 --- /dev/null +++ b/apps/docs/src/routes/docs/core/(1)components/color-swatch.mdx @@ -0,0 +1,144 @@ +--- +title: Color Swatch +badges: + - icon: npm + label: "Since v0.13.9" + - label: "@kobalte/core/color-swatch" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/color-swatch" +--- + +import { + BasicExample, + ValueExample, + CustomColorNameExample, +} from "../../../../examples/color-swatch"; + +# {frontmatter.title} + +Displays a preview of a selected color. + +## Import + +```ts +import { ColorSwatch } from "@kobalte/core/color-swatch"; +// or +import { Root } from "@kobalte/core/color-swatch"; +``` + +## Features + +- Localized color descriptions for screen reader users. + +## Anatomy + +The color swatch consists of: + +- **ColorSwatch:** The root container for the color swatch. + +```tsx + +``` + +## Example + +:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/color-swatch.tsx#L4-L13" + +``` + +```css title="style.css" tab file="../../../../examples/color-swatch.module.css" + +``` + +::: + +## Usage + +ColorSwatch accepts a value via the `value` prop. The value should be `Color` object. You can obtain a Color object by using the `parseColor` function to parse a color from a string. + +### Value + +:::preview + + + +--- + +```tsx title="index.tsx" tab +import { ColorSwatch } from "@kobalte/core/color-swatch"; +import { parseColor } from "@kobalte/core/colors"; +import "./style.css"; + +function App() { + return ; +} +``` + +```css title="style.css" tab +.ColorSwatchRoot { + height: 56px; + width: 56px; + border-radius: 6px; +} +``` + +::: + +### Custom Color Name + +:::preview + + + +--- + +```tsx title="index.tsx" tab +import { ColorSwatch } from "@kobalte/core/color-swatch"; +import { parseColor } from "@kobalte/core/colors"; +import "./style.css"; + +function App() { + return ( + + ); +} +``` + +```css title="style.css" tab +.ColorSwatchRoot { + height: 56px; + width: 56px; + border-radius: 6px; +} +``` + +::: + +## API Reference + +### ColorSwatch + +`ColorSwatch` is equivalent to the `Root` import from `@kobalte/core/color-swatch`. + +| Prop | Description | +| :----------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| value | `Color`
The color value to display in the swatch. | +| colorName | `string`
A localized accessible name for the color. By default, a description is generated from the color value, but this can be overridden if you have a more specific color name (e.g. Pantone colors). | +| translations | `ColorSwatchIntlTranslations`
Localization strings. | + +## Rendered elements + +| Component | Default rendered element | +| :------------ | :----------------------- | +| `ColorSwatch` | `div` | diff --git a/apps/docs/src/routes/docs/core/components/color-wheel.mdx b/apps/docs/src/routes/docs/core/(1)components/color-wheel.mdx similarity index 76% rename from apps/docs/src/routes/docs/core/components/color-wheel.mdx rename to apps/docs/src/routes/docs/core/(1)components/color-wheel.mdx index c3cb68a7..410c38e6 100644 --- a/apps/docs/src/routes/docs/core/components/color-wheel.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/color-wheel.mdx @@ -1,4 +1,14 @@ -import { Preview, TabsSnippets, Kbd } from "../../../../components"; +--- +title: Color Wheel +badges: + - icon: npm + label: "Since v0.13.9" + - label: "@kobalte/core/color-wheel" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/color-wheel" +--- + import { BasicExample, DefaultValueExample, @@ -8,7 +18,7 @@ import { HTMLFormExample, } from "../../../../examples/color-wheel"; -# Color Wheel +# {frontmatter.title} Allows users to adjust the hue of an HSL or HSB color value on a circular track. @@ -54,76 +64,21 @@ The color wheel consists of: ## Example - - - - - - - index.tsx - style.css - -{/* */} - - ```tsx - import { ColorWheel } from "@kobalte/core/color-wheel"; - import "./style.css"; - - function App() { - return ( - - Label - - - - - - - ); - } - ``` - - - - ```css - .ColorWheelRoot { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - user-select: none; - touch-action: none; - } - - .ColorWheelTrack { - position: relative; - height: 160px; - width: 160px; - } - - .ColorWheelThumb { - display: block; - width: 16px; - height: 16px; - border-radius: 9999px; - border: 2px solid #fff; - background: var(--kb-color-current); - } - - .ColorWheelThumb:focus { - outline: none; - } - - .ColorWheelLabel { - font-size: 14px; - font-weight: 500; - user-select: none; - } - ``` - - -{/* */} - +:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/color-wheel.tsx#L5-L18" + +``` + +```css title="style.css" tab file="../../../../examples/color-wheel.module.css" + +``` + +::: ## Usage @@ -131,11 +86,13 @@ The color wheel consists of: By default, `ColorWheel` is uncontrolled with a default value of red (hue = 0). You can change the default value using the `defaultValue` prop. - - - +:::preview + + -```tsx {6} +--- + +```tsx {7} import { ColorWheel } from "@kobalte/core/color-wheel"; import { parseColor } from "@kobalte/core/colors"; @@ -153,15 +110,19 @@ function App() { } ``` +::: + ### Controlled value A `ColorWheel` can be made controlled using the `value` prop. The `onChange` event is fired when the user drags the thumb and receive the new value. - - - +:::preview -```tsx {3, 8-9} + + +--- + +```tsx {4,9-10} import { createSignal } from "solid-js"; function ControlledValueExample() { @@ -183,15 +144,19 @@ function ControlledValueExample() { } ``` +::: + ### Thickness The `thickness` prop controls the width of the `ColorWheel`'s circular track. This prop is required. - - - +:::preview + + -```tsx {0} +--- + +```tsx {1} Label @@ -202,13 +167,17 @@ The `thickness` prop controls the width of the `ColorWheel`'s circular track. Th ``` +::: + ### Custom Value Label - - - +:::preview + + -```tsx {2-8} +--- + +```tsx {3-9} color @@ -231,15 +200,19 @@ The `thickness` prop controls the width of the `ColorWheel`'s circular track. Th ``` +::: + ### HTML forms `ColorWheel` supports the `name` prop for integration with HTML forms. - - - +:::preview -```tsx {7} + + +--- + +```tsx {8} function HTMLFormExample() { const onSubmit = (e: SubmitEvent) => { // handle form submission. @@ -259,6 +232,8 @@ function HTMLFormExample() { } ``` +::: + ## API Reference ### ColorWheel @@ -313,11 +288,11 @@ The current color is available on the thumb using the custom css property `--kb- | Key | Description | | :-------------------- | :---------------------------------------------------- | -| PageUp | Increments the value of the thumb by a larger step. | -| PageDown | Decrements the value of the thumb by a larger step. | -| ArrowDown | Decrements the value of the thumb by the step amount. | -| ArrowUp | Increments the value of the thumb by the step amount. | -| ArrowRight | Increments the value of the thumb by the step value. | -| ArrowLeft | Decrements the value of the thumb by the step value. | -| Home | Sets the value of the thumb to the minimum value. | -| End | Sets the value of the thumb to the maximum value. | +| PageUp | Increments the value of the thumb by a larger step. | +| PageDown | Decrements the value of the thumb by a larger step. | +| ArrowDown | Decrements the value of the thumb by the step amount. | +| ArrowUp | Increments the value of the thumb by the step amount. | +| ArrowRight | Increments the value of the thumb by the step value. | +| ArrowLeft | Decrements the value of the thumb by the step value. | +| Home | Sets the value of the thumb to the minimum value. | +| End | Sets the value of the thumb to the maximum value. | diff --git a/apps/docs/src/routes/docs/core/components/combobox.mdx b/apps/docs/src/routes/docs/core/(1)components/combobox.mdx similarity index 87% rename from apps/docs/src/routes/docs/core/components/combobox.mdx rename to apps/docs/src/routes/docs/core/(1)components/combobox.mdx index 7d4f1ec6..4adcad2d 100644 --- a/apps/docs/src/routes/docs/core/components/combobox.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/combobox.mdx @@ -1,4 +1,17 @@ -import { Preview, TabsSnippets, Kbd, Callout } from "../../../../components"; +--- +title: Combobox +badges: + - icon: npm + label: "Since v0.9.0" + - label: "@kobalte/core/combobox" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/combobox" + - icon: aria + label: "Combobox" + url: "https://www.w3.org/WAI/ARIA/apg/patterns/combobox/" +--- + import { BasicExample, DefaultValueExample, @@ -11,7 +24,7 @@ import { MultipleSelectionExample, } from "../../../../examples/combobox"; -# Combobox +# {frontmatter.title} Combines a text input with a listbox, allowing users to filter a list of options to items matching a query. @@ -86,231 +99,21 @@ The combobox consists of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { Combobox } from "@kobalte/core/combobox"; - import { CaretSortIcon, CheckIcon } from "some-icon-library"; - import { createSignal } from "solid-js"; - import "./style.css"; - - const ALL_OPTIONS = ["Apple", "Banana", "Blueberry", "Grapes", "Pineapple"]; - - function App() { - return ( - ( - - {props.item.rawValue} - - - - - )} - > - - - - - - - - - - - - - - - ); - } - ``` - - - - ```css - .combobox__control { - display: inline-flex; - justify-content: space-between; - width: 200px; - border-radius: 6px; - font-size: 16px; - line-height: 1; - outline: none; - background-color: white; - border: 1px solid hsl(240 6% 90%); - color: hsl(240 4% 16%); - transition: border-color 250ms, color 250ms; - } - - .combobox__control[data-invalid] { - border-color: hsl(0 72% 51%); - color: hsl(0 72% 51%); - } - - .combobox__control_multi { - width: 100%; - min-width: 200px; - max-width: 300px; - } - - .combobox__input { - appearance: none; - display: inline-flex; - min-width: 0; - min-height: 40px; - padding-left: 16px; - font-size: 16px; - background: transparent; - border-top-left-radius: 6px; - border-bottom-left-radius: 6px; - outline: none; - } - - .combobox__input::placeholder { - color: hsl(240 4% 46%); - } - - .combobox__trigger { - appearance: none; - display: inline-flex; - justify-content: center; - align-items: center; - width: auto; - outline: none; - border-top-right-radius: 6px; - border-bottom-right-radius: 6px; - padding: 0 10px; - background-color: hsl(240 5% 96%); - border-left: 1px solid hsl(240 6% 90%); - color: hsl(240 4% 16%); - font-size: 16px; - line-height: 0; - transition: 250ms background-color; - } - - .combobox__icon { - height: 20px; - width: 20px; - flex: 0 0 20px; - } - - .combobox__description { - margin-top: 8px; - color: hsl(240 5% 26%); - font-size: 12px; - user-select: none; - } - - .combobox__error-message { - margin-top: 8px; - color: hsl(0 72% 51%); - font-size: 12px; - user-select: none; - } - - .combobox__content { - background-color: white; - border-radius: 6px; - border: 1px solid hsl(240 6% 90%); - box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - transform-origin: var(--kb-combobox-content-transform-origin); - animation: contentHide 250ms ease-in forwards; - } - - .combobox__content[data-expanded] { - animation: contentShow 250ms ease-out; - } - - .combobox__listbox { - overflow-y: auto; - max-height: 360px; - padding: 8px; - } - - .combobox__listbox:focus { - outline: none; - } - - .combobox__item { - font-size: 16px; - line-height: 1; - color: hsl(240 4% 16%); - border-radius: 4px; - display: flex; - align-items: center; - justify-content: space-between; - height: 32px; - padding: 0 8px; - position: relative; - user-select: none; - outline: none; - } - - .combobox__item[data-disabled] { - color: hsl(240 5% 65%); - opacity: 0.5; - pointer-events: none; - } - - .combobox__item[data-highlighted] { - outline: none; - background-color: hsl(200 98% 39%); - color: white; - } - - .combobox__section { - padding: 8px 0 0 8px; - font-size: 14px; - line-height: 32px; - color: hsl(240 4% 46%); - } - - .combobox__item-indicator { - height: 20px; - width: 20px; - display: inline-flex; - align-items: center; - justify-content: center; - } - - @keyframes contentShow { - from { - opacity: 0; - transform: translateY(-8px); - } - to { - opacity: 1; - transform: translateY(0); - } - } - - @keyframes contentHide { - from { - opacity: 1; - transform: translateY(0); - } - to { - opacity: 0; - transform: translateY(-8px); - } - } - ``` - - - {/* */} - +:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/combobox.tsx#L14-L45" + +``` + +```css title="style.css" tab file="../../../../examples/combobox.module.css" + +``` + +::: ## Usage @@ -326,7 +129,7 @@ For convenience, we provide 3 built-in filtering mechanism based on substring ma A custom filter function can also be passed to the `defaultFilter` prop, it receives _an option and the input value_ as parameters, and should return a `boolean`. -```tsx {6} +```tsx {7} import { Combobox } from "@kobalte/core/combobox"; const ALL_OPTIONS = ["Apple", "Banana", "Blueberry", "Grapes", "Pineapple"]; @@ -344,11 +147,13 @@ function FilteringExample() { An initial, uncontrolled value can be provided using the `defaultValue` prop, which accepts a value corresponding with the `options`. - - - +:::preview + + -```tsx {8} +--- + +```tsx {9} import { Combobox } from "@kobalte/core/combobox"; import { CaretSortIcon, CheckIcon } from "some-icon-library"; @@ -387,15 +192,19 @@ function DefaultValueExample() { } ``` +::: + ### Controlled value The `value` prop, which accepts a value corresponding with the `options` prop, can be used to make the value controlled. The `onChange` event is fired when the user selects an option, and receives the selected option. - - - +:::preview -```tsx {7,20-21} + + +--- + +```tsx {8,21-22} import { Combobox } from "@kobalte/core/combobox"; import { CaretSortIcon, CheckIcon } from "some-icon-library"; import { createSignal } from "solid-js"; @@ -449,15 +258,19 @@ function ControlledExample() { } ``` +::: + ### Description The `Combobox.Description` component can be used to associate additional help text with a combobox. - - - +:::preview + + -```tsx {27} +--- + +```tsx {28} import { Combobox } from "@kobalte/core/combobox"; import { CaretSortIcon, CheckIcon } from "some-icon-library"; @@ -496,17 +309,21 @@ function DescriptionExample() { } ``` +::: + ### Error message The `Combobox.ErrorMessage` component can be used to help the user fix a validation error. It should be combined with the `validationState` prop to semantically mark the combobox as invalid for assistive technologies. By default, it will render only when the `validationState` prop is set to `invalid`, use the `forceMount` prop to always render the error message (ex: for usage with animation libraries). - - - +:::preview + + + +--- -```tsx {22,41} +```tsx {23,42} import { Combobox } from "@kobalte/core/combobox"; import { CaretSortIcon, CheckIcon } from "some-icon-library"; import { createSignal } from "solid-js"; @@ -559,15 +376,19 @@ function ErrorMessageExample() { } ``` +::: + ### HTML forms The combobox `name` prop, paired with the `Combobox.HiddenSelect` component, can be used for integration with HTML forms. - - - +:::preview + + -```tsx {13,25} +--- + +```tsx {14,26} import { Combobox } from "@kobalte/core/combobox"; import { CaretSortIcon, CheckIcon } from "some-icon-library"; @@ -617,6 +438,8 @@ function HTMLFormExample() { } ``` +::: + ### Using object as options Objects can be used as options instead of plain strings. In this case you have to tell the combobox how it should work with the provided options. For this you **have to use** the following props: @@ -626,11 +449,13 @@ Objects can be used as options instead of plain strings. In this case you have t - `optionLabel`: The property name to use as the label of an option, displayed in the combobox input. - `optionDisabled`: The property name to use as the disabled flag of an option. - - - +:::preview -```tsx {21-24} + + +--- + +```tsx {22-25} import { Combobox } from "@kobalte/core/combobox"; import { CaretSortIcon, CheckIcon } from "some-icon-library"; @@ -684,6 +509,8 @@ function ObjectExample() { } ``` +::: + ### Using option groups When using option groups you have to tell the combobox how to distinguish an option from a group. For this you **have to use** the following props: @@ -692,11 +519,13 @@ When using option groups you have to tell the combobox how to distinguish an opt Additionally, the `sectionComponent` prop is used to display the option group label in the combobox. - - - +:::preview + + -```tsx {44} +--- + +```tsx {45} import { Combobox } from "@kobalte/core/combobox"; import { CaretSortIcon, CheckIcon } from "some-icon-library"; @@ -773,9 +602,10 @@ export function OptionGroupExample() { } ``` - - Notice the usage of generics on `Combobox` for proper TypeScript support. - +::: +:::note +Notice the usage of generics on `Combobox` for proper TypeScript support. +::: ### Multiple selection @@ -783,11 +613,13 @@ The `multiple` prop can be used to create a combobox that allow multi-selection. In this case `Combobox.Input` can't display the values, instead the `Combobox.Control` children _render prop_ expose an array of selected options, and two method for removing an option from the selection and clear the selection. - - - +:::preview + + -```tsx {7,12,14-15,30,34,42} +--- + +```tsx {8,13,15-16,31,35,43} import { Combobox } from "@kobalte/core/combobox"; import { CaretSortIcon, CheckIcon } from "some-icon-library"; import { createSignal } from "solid-js"; @@ -853,6 +685,8 @@ function MultipleSelectionExample() { } ``` +::: + ### Origin-aware animations We expose a CSS custom property `--kb-combobox-content-transform-origin` which can be used to animate the content from its computed origin. @@ -1063,11 +897,11 @@ We expose a CSS custom property `--kb-combobox-content-transform-origin` which c | Key | Description | | :------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Enter | When focus is virtually on an item, selects the focused item. | -| ArrowDown | When focus is on the input, opens the combobox and virtual focuses the first or selected item.
When focus is virtually on an item, moves virtual focus to the next item. | -| ArrowUp | When focus is on the input, opens the combobox and virtual focuses the last or selected item.
When focus is virtually on an item, moves virtual focus to the previous item. | -| Alt + ArrowDown | When focus is on the input, opens the combobox. | -| Alt + ArrowUp | When focus is on the input, closes the combobox. | -| Home | When focus is on the input, moves virtual focus to first item. | -| End | When focus is on the input, moves virtual focus to last item. | -| Esc | When the combobox is open, closes the combobox.
When the combobox is closed, clear the input and selection unless `noResetInputOnBlur` is set to `true`. | +| Enter | When focus is virtually on an item, selects the focused item. | +| ArrowDown | When focus is on the input, opens the combobox and virtual focuses the first or selected item.
When focus is virtually on an item, moves virtual focus to the next item. | +| ArrowUp | When focus is on the input, opens the combobox and virtual focuses the last or selected item.
When focus is virtually on an item, moves virtual focus to the previous item. | +| Alt + ArrowDown | When focus is on the input, opens the combobox. | +| Alt + ArrowUp | When focus is on the input, closes the combobox. | +| Home | When focus is on the input, moves virtual focus to first item. | +| End | When focus is on the input, moves virtual focus to last item. | +| Esc | When the combobox is open, closes the combobox.
When the combobox is closed, clear the input and selection unless `noResetInputOnBlur` is set to `true`. | diff --git a/apps/docs/src/routes/docs/core/components/context-menu.mdx b/apps/docs/src/routes/docs/core/(1)components/context-menu.mdx similarity index 79% rename from apps/docs/src/routes/docs/core/components/context-menu.mdx rename to apps/docs/src/routes/docs/core/(1)components/context-menu.mdx index a2265107..f0996a6b 100644 --- a/apps/docs/src/routes/docs/core/components/context-menu.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/context-menu.mdx @@ -1,7 +1,20 @@ -import { Preview, Kbd, TabsSnippets } from "../../../../components"; +--- +title: Context Menu +badges: + - icon: npm + label: "Since v0.1.0" + - label: "@kobalte/core/context-menu" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/context-menu" + - icon: aria + label: "Menu" + url: "https://www.w3.org/WAI/ARIA/apg/patterns/menubar/" +--- + import { BasicExample } from "../../../../examples/context-menu"; -# Context Menu +# {frontmatter.title} Displays a menu located at the pointer, triggered by a right-click or a long-press. @@ -17,7 +30,7 @@ import { ContextMenu } from "@kobalte/core"; ## Features -- Follows the [WAI ARIA Menu](https://www.w3.org/WAI/ARIA/apg/patterns/menu/) design pattern. +- Follows the [WAI-ARIA Menu and Menubar](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/) design pattern. - Triggers with a long-press on touch devices. - Supports modal and non-modal modes. - Supports submenus. @@ -106,259 +119,21 @@ The checkable menu item consists of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { ContextMenu } from "@kobalte/core/context-menu"; - import { createSignal } from "solid-js"; - import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "some-icon-library"; - import "./style.css"; - - function App() { - const [showGitLog, setShowGitLog] = createSignal(true); - const [showHistory, setShowHistory] = createSignal(false); - const [branch, setBranch] = createSignal("main"); - - return ( - - - Right click here. - - - - - Commit
⌘+K
-
- - Push
⇧+⌘+K
-
- - Update Project
⌘+T
-
- - - GitHub -
- -
-
- - - - Create Pull Request… - - - View Pull Requests - - - Sync Fork - - - - Open on GitHub - - - -
- - - - - - - - Show Git Log - - - - - - Show History - - - - - - - Branches - - - - - - - main - - - - - - develop - - - -
-
-
- ); - } - ``` - -
- - ```css - .context-menu__trigger { - display: block; - border: 2px dashed hsl(240 4% 46%); - color: hsl(240 5% 34%); - border-radius: 4px; - font-size: 15px; - user-select: none; - padding: 45px 0; - width: 300px; - text-align: center; - } - - .context-menu__content, - .context-menu__sub-content { - min-width: 220px; - padding: 8px; - background-color: white; - border-radius: 6px; - border: 1px solid hsl(240 6% 90%); - box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - outline: none; - transform-origin: var(--kb-menu-content-transform-origin); - animation: contentHide 250ms ease-in forwards; - } - - .context-menu__content[data-expanded], - .context-menu__sub-content[data-expanded] { - animation: contentShow 250ms ease-out; - } - - .context-menu__item, - .context-menu__checkbox-item, - .context-menu__radio-item, - .context-menu__sub-trigger { - font-size: 16px; - line-height: 1; - color: hsl(240 4% 16%); - border-radius: 4px; - display: flex; - align-items: center; - height: 32px; - padding: 0 8px 0 24px; - position: relative; - user-select: none; - outline: none; - } - - .context-menu__sub-trigger[data-expanded] { - background-color: hsl(204 94% 94%); - color: hsl(201 96% 32%); - } - - .context-menu__item[data-disabled], - .context-menu__checkbox-item[data-disabled], - .context-menu__radio-item[data-disabled], - .context-menu__sub-trigger[data-disabled] { - color: hsl(240 5% 65%); - opacity: 0.5; - pointer-events: none; - } - - .context-menu__item[data-highlighted], - .context-menu__checkbox-item[data-highlighted], - .context-menu__radio-item[data-highlighted], - .context-menu__sub-trigger[data-highlighted] { - outline: none; - background-color: hsl(200 98% 39%); - color: white; - } - - .context-menu__group-label { - padding: 0 24px; - font-size: 14px; - line-height: 32px; - color: hsl(240 4% 46%); - } - - .context-menu__separator { - height: 1px; - border-top: 1px solid hsl(240 6% 90%); - margin: 6px; - } - - .context-menu__item-indicator { - position: absolute; - left: 0; - height: 20px; - width: 20px; - display: inline-flex; - align-items: center; - justify-content: center; - } - - .context-menu__item-right-slot { - margin-left: auto; - padding-left: 20px; - font-size: 14px; - color: hsl(240 4% 46%); - } - - [data-highlighted] > .context-menu__item-right-slot { - color: white; - } - - [data-disabled] .context-menu__item-right-slot { - color: hsl(240 5% 65%); - opacity: 0.5; - } - - @keyframes contentShow { - from { - opacity: 0; - transform: scale(0.96); - } - to { - opacity: 1; - transform: scale(1); - } - } - - @keyframes contentHide { - from { - opacity: 1; - transform: scale(1); - } - to { - opacity: 0; - transform: scale(0.96); - } - } - ``` - - - {/* */} -
+:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/context-menu.tsx#L6-L122" + +``` + +```css title="style.css" tab file="../../../../examples/context-menu.module.css" + +``` + +::: ## Usage @@ -607,11 +382,11 @@ The popper positioner will copy the same `z-index` as the `ContextMenu.Content`. | Key | Description | | :------------------------------------------- | :----------------------------------------------------------------------------------------------- | -| Space | When focus is on an item, activates the item. | -| Enter | When focus is on an item, activates the item. | -| ArrowDown | When focus is on an item, moves focus to the next item. | -| ArrowUp | When focus is on an item, moves focus to the previous item. | -| ArrowRight / ArrowLeft | When focus is on a sub menu trigger, opens or closes the submenu depending on reading direction. | -| Home | When focus is on an item, moves focus to first item. | -| End | When focus is on an item, moves focus to last item. | -| Esc | Closes the context menu. | +| Space | When focus is on an item, activates the item. | +| Enter | When focus is on an item, activates the item. | +| ArrowDown | When focus is on an item, moves focus to the next item. | +| ArrowUp | When focus is on an item, moves focus to the previous item. | +| ArrowRight / ArrowLeft | When focus is on a sub menu trigger, opens or closes the submenu depending on reading direction. | +| Home | When focus is on an item, moves focus to first item. | +| End | When focus is on an item, moves focus to last item. | +| Esc | Closes the context menu. | diff --git a/apps/docs/src/routes/docs/core/components/dialog.mdx b/apps/docs/src/routes/docs/core/(1)components/dialog.mdx similarity index 65% rename from apps/docs/src/routes/docs/core/components/dialog.mdx rename to apps/docs/src/routes/docs/core/(1)components/dialog.mdx index b9bfc71c..2e9b3d80 100644 --- a/apps/docs/src/routes/docs/core/components/dialog.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/dialog.mdx @@ -1,7 +1,20 @@ -import { Preview, Kbd, TabsSnippets } from "../../../../components"; +--- +title: Dialog +badges: + - icon: npm + label: "Since v0.1.0" + - label: "@kobalte/core/dialog" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/dialog" + - icon: aria + label: "Dialog" + url: "https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/" +--- + import { BasicExample } from "../../../../examples/dialog"; -# Dialog +# {frontmatter.title} A window overlaid on either the primary window or another dialog window. Content behind a modal dialog is inert, meaning that users cannot interact with it. @@ -17,11 +30,11 @@ import { Dialog } from "@kobalte/core"; ## Features -- Follows the [WAI ARIA Dialog](https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/) design pattern. +- Follows the [WAI-ARIA Dialog](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) design pattern. - Supports modal and non-modal modes. - Provides screen reader announcements via rendered title and description. - Focus is trapped and scrolling is blocked while it is open. -- Pressing Esc closes the dialog. +- Pressing Esc closes the dialog. - Can be controlled or uncontrolled. ## Anatomy @@ -53,186 +66,21 @@ The dialog consists of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { Dialog } from "@kobalte/core/dialog"; - import { CrossIcon } from "some-icon-library"; - import "./style.css"; - - function App() { - return ( - - Open - - -
- -
- About Kobalte - - - -
- - Kobalte is a UI toolkit for building accessible web apps and design systems with - SolidJS. It provides a set of low-level UI components and primitives which can be the - foundation for your design system implementation. - -
-
-
-
- ); - } - ``` - -
- - ```css - .dialog__trigger { - appearance: none; - display: inline-flex; - justify-content: center; - align-items: center; - height: 40px; - width: auto; - outline: none; - border-radius: 6px; - padding: 0 16px; - background-color: hsl(200 98% 39%); - color: white; - font-size: 16px; - line-height: 0; - transition: 250ms background-color; - } - - .dialog__trigger:hover { - background-color: hsl(201 96% 32%); - } - - .dialog__trigger:focus-visible { - outline: 2px solid hsl(200 98% 39%); - outline-offset: 2px; - } - - .dialog__trigger:active { - background-color: hsl(201 90% 27%); - } - - .dialog__overlay { - position: fixed; - inset: 0; - z-index: 50; - background-color: rgb(0 0 0 / 0.2); - animation: overlayHide 250ms ease 100ms forwards; - } - - .dialog__overlay[data-expanded] { - animation: overlayShow 250ms ease; - } - - .dialog__positioner { - position: fixed; - inset: 0; - z-index: 50; - display: flex; - align-items: center; - justify-content: center; - } - - .dialog__content { - z-index: 50; - max-width: min(calc(100vw - 16px), 500px); - border: 1px solid hsl(240 5% 84%); - border-radius: 6px; - padding: 16px; - background-color: white; - box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - animation: contentHide 300ms ease-in forwards; - } - - .dialog__content[data-expanded] { - animation: contentShow 300ms ease-out; - } - - .dialog__header { - display: flex; - align-items: baseline; - justify-content: space-between; - margin-bottom: 12px; - } - - .dialog__close-button { - height: 16px; - width: 16px; - color: hsl(240 5% 34%); - } - - .dialog__title { - font-size: 20px; - font-weight: 500; - color: hsl(240 6% 10%); - } - - .dialog__description { - font-size: 16px; - color: hsl(240 5% 26%); - } - - @keyframes overlayShow { - from { - opacity: 0; - } - to { - opacity: 1; - } - } - - @keyframes overlayHide { - from { - opacity: 1; - } - to { - opacity: 0; - } - } - - @keyframes contentShow { - from { - opacity: 0; - transform: scale(0.96); - } - to { - opacity: 1; - transform: scale(1); - } - } - - @keyframes contentHide { - from { - opacity: 1; - transform: scale(1); - } - to { - opacity: 0; - transform: scale(0.96); - } - } - ``` - - - {/* */} -
+:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/dialog.tsx#L4-L33" + +``` + +```css title="style.css" tab file="../../../../examples/dialog.module.css" + +``` + +::: ## Usage @@ -248,7 +96,7 @@ An initial, uncontrolled open value can be provided using the `defaultOpen` prop The `open` prop can be used to make the open state controlled. The `onOpenChange` event is fired when the user presses the trigger, close button or overlay, and receives the new value. -```tsx {3,6} +```tsx {4,7} import { createSignal } from "solid-js"; function ControlledExample() { @@ -320,8 +168,8 @@ function ControlledExample() { | Key | Description | | :-------------------------------- | :----------------------------------------------------- | -| Space | When focus is on the trigger, opens/closes the dialog. | -| Enter | When focus is on the trigger, opens/closes the dialog. | -| Tab | Moves focus to the next focusable element. | -| Shift + Tab | Moves focus to the previous focusable element. | -| Esc | Closes the dialog and moves focus to the trigger. | +| Space | When focus is on the trigger, opens/closes the dialog. | +| Enter | When focus is on the trigger, opens/closes the dialog. | +| Tab | Moves focus to the next focusable element. | +| Shift + Tab | Moves focus to the previous focusable element. | +| Esc | Closes the dialog and moves focus to the trigger. | diff --git a/apps/docs/src/routes/docs/core/(1)components/drawer.mdx b/apps/docs/src/routes/docs/core/(1)components/drawer.mdx new file mode 100644 index 00000000..4ebbc07a --- /dev/null +++ b/apps/docs/src/routes/docs/core/(1)components/drawer.mdx @@ -0,0 +1,464 @@ +import { Preview, Kbd, TabsSnippets } from "../../../../components"; +import { BasicExample, SnapPointsExample, ControlledExample } from "../../../../examples/drawer"; + +# Drawer + +A panel that slides in from the edge of the screen with drag-to-dismiss and snap-point support. Built on [Dialog](/docs/core/components/dialog) with a two-signal open pattern so content stays mounted during the closing animation. + +## Import + +```ts +import { Drawer } from "@kobalte/core/drawer"; +// or +import { Root, Trigger, Content, Overlay, Portal, CloseButton, Title, Description } from "@kobalte/core/drawer"; +``` + +## Features + +- Slides in from any of the four edges (`bottom`, `top`, `left`, `right`). +- Drag-to-dismiss with configurable snap points and velocity-based release. +- Overlay opacity tracks the drag position in real time. +- Scrollable children are handled automatically — the drag gesture does not conflict with internal scrolling. +- Snap points accept fractional values (`0`–`1`) or absolute pixel strings (`"200px"`). +- Programmatic snap-point control via `useContext().setActiveSnapPoint`. +- Supports modal and non-modal modes. +- Focus is trapped and scroll is locked when modal. +- Pressing Esc closes the drawer. +- Can be controlled or uncontrolled. + +## Anatomy + +- **Drawer:** Contains all the parts of a drawer. +- **Drawer.Trigger:** The button that opens the drawer. +- **Drawer.Portal:** Portals its children into the `body` when the drawer is open. +- **Drawer.Overlay:** The layer that covers the inert portion of the view when the drawer is open. Opacity is driven automatically by `openPercentage`. +- **Drawer.Content:** Contains the content rendered when the drawer is open. Handles all pointer and touch drag gestures. +- **Drawer.CloseButton:** The button that closes the drawer. +- **Drawer.Title:** An accessible title announced when the drawer is opened. +- **Drawer.Description:** An optional accessible description announced when the drawer is opened. + +```tsx + + + + + + + + + + + +``` + +## Example + + + + + + + + index.tsx + style.css + + {/* */} + + ```tsx + import { Drawer } from "@kobalte/core/drawer"; + import { CrossIcon } from "some-icon-library"; + import "./style.css"; + + function App() { + return ( + + Open drawer + + + +
+
+ Drawer + + + +
+ + Drag the handle or swipe down to dismiss. + + + + + ); + } + ``` + + + ```css + .drawer__trigger { + display: inline-flex; + align-items: center; + justify-content: center; + height: 40px; + padding: 0 16px; + border: none; + border-radius: 6px; + background-color: hsl(200 98% 39%); + color: white; + font-size: 16px; + cursor: pointer; + } + + /* Opacity is driven by JS (openPercentage). Only a CSS transition is needed. */ + .drawer__overlay { + position: fixed; + inset: 0; + z-index: 50; + background-color: rgb(0 0 0 / 0.4); + transition: opacity 350ms cubic-bezier(0.32, 0.72, 0, 1); + } + + .drawer__content { + position: fixed; + z-index: 50; + bottom: 0; + left: 0; + width: 100%; + height: 320px; + background-color: white; + border-radius: 12px 12px 0 0; + border-top: 1px solid hsl(240 5% 84%); + padding: 16px 24px 32px; + outline: none; + /* The drawer applies transform via inline style. This transition animates it. */ + transition: transform 350ms cubic-bezier(0.32, 0.72, 0, 1); + } + + .drawer__handle { + width: 48px; + height: 4px; + background-color: hsl(240 5% 84%); + border-radius: 9999px; + margin: 0 auto 16px; + } + + .drawer__header { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 8px; + } + + .drawer__close-button { + height: 16px; + width: 16px; + color: hsl(240 5% 34%); + } + + .drawer__title { + font-size: 18px; + font-weight: 500; + color: hsl(240 6% 10%); + } + + .drawer__description { + font-size: 14px; + color: hsl(240 5% 26%); + } + ``` + + {/* */} + + +## Usage + +### Side + +Use the `side` prop to control which edge of the viewport the drawer slides in from. + +```tsx +... +... +... +... {/* default */} +``` + +The `Drawer.Content` must be positioned against the matching edge: + +```css +/* right drawer */ +.drawer__content { + position: fixed; + top: 0; + right: 0; + height: 100%; + width: 320px; + transition: transform 350ms cubic-bezier(0.32, 0.72, 0, 1); +} + +/* bottom drawer */ +.drawer__content { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + height: 320px; + transition: transform 350ms cubic-bezier(0.32, 0.72, 0, 1); +} +``` + +### Snap points + +Snap points define the positions the drawer can settle at. Each value is either a fraction of the drawer size (`0` = fully closed, `1` = fully open) or a pixel string (`"200px"`). + + + + + +```tsx +// Three snap points: closed, 40% peek, fully open + + ... + + +// Pixel-based snap point + + ... + +``` + +`defaultSnapPoint` sets which snap point the drawer opens to. Defaults to `1` (fully open). + +### Default open + +```tsx +... +``` + +### Controlled open + +The `open` prop makes the open state controlled. The `onOpenChange` event fires whenever the user interacts with the trigger, overlay, close button, or escape key. + + + + + +```tsx {3,6} +import { createSignal } from "solid-js"; + +function ControlledExample() { + const [open, setOpen] = createSignal(false); + + return ( + + ... + + ); +} +``` + +### Programmatic snap + +Access `setActiveSnapPoint` from context to snap to a specific position without dragging. + +```tsx +import { Drawer } from "@kobalte/core/drawer"; + +function SnapButtons() { + const ctx = Drawer.useContext(); + + return ( + <> + + + + ); +} + +function App() { + return ( + + Open + + + + + + + + ); +} +``` + +### Non-modal + +Set `modal={false}` to keep the background interactive and remove the overlay. Focus is not trapped. + +```tsx + + Open + + {/* No Drawer.Overlay */} + ... + + +``` + +### Drag handle + +Add a visible drag handle to hint that the drawer is draggable. Any element inside `Drawer.Content` is draggable by default — use `data-no-drag` to opt specific elements out. + +```tsx + +
+ ... + {/* This element will not initiate a drag */} +
Non-draggable region
+ +``` + +### Animating the overlay + +`Drawer.Overlay` automatically drives its `opacity` from the drawer's `openPercentage` (0 when fully closed, 1 when fully open). This means opacity tracks the drag position in real time with no extra JavaScript. + +To animate the opacity on open/close, add a CSS transition: + +```css +.drawer__overlay { + position: fixed; + inset: 0; + background-color: rgb(0 0 0 / 0.4); + /* opacity changes will be animated */ + transition: opacity 350ms cubic-bezier(0.32, 0.72, 0, 1); +} +``` + +The transition is automatically suppressed during drag so the opacity follows the finger instantly. + +### Custom damping and velocity + +Control the rubber-band feel when dragging past the most-open snap point, and the velocity threshold for snap selection on release. + +```tsx + 6 * Math.log(distance + 1)} + // Custom velocity: use raw distance/time ratio with no clamping + velocityFunction={(distance, time) => distance / time} + // Reset velocity window every 100ms (default: 200) + velocityCacheReset={100} + // Prevent fast flings from skipping intermediate snap points + allowSkippingSnapPoints={false} +> + ... + +``` + +## API Reference + +### Drawer + +`Drawer` is equivalent to the `Root` import from `@kobalte/core/drawer`. + +| Prop | Description | +| :--- | :--- | +| open | `boolean`
The controlled open state of the drawer. | +| defaultOpen | `boolean`
The default open state when initially rendered. Useful when you do not need to control the open state. | +| onOpenChange | `(open: boolean) => void`
Event handler called when the open state changes. | +| side | `"bottom" \| "top" \| "left" \| "right"`
Which edge of the viewport the drawer slides in from. **Default:** `"bottom"` | +| snapPoints | `DrawerSize[]`
Positions the drawer can settle at. Each value is a fraction (`0`–`1`) or a pixel string (`"200px"`). `0` = fully closed, `1` = fully open. **Default:** `[0, 1]` | +| breakPoints | `(DrawerSize \| null)[]`
Custom thresholds between adjacent snap points. Length must equal `snapPoints.length - 1`. Pass `null` to use the default midpoint. | +| defaultSnapPoint | `DrawerSize`
The snap point the drawer opens to. **Default:** `1` | +| activeSnapPoint | `DrawerSize`
The controlled active snap point. | +| onActiveSnapPointChange | `(snapPoint: DrawerSize) => void`
Event handler called when the active snap point changes. | +| dampFunction | `(distance: number) => number`
Controls rubber-band resistance when dragging past the most-open snap point. **Default:** `(d) => 6 * Math.log(d + 1)` | +| velocityFunction | `(distance: number, time: number) => number`
Computes the velocity multiplier used to project the release position for snap selection. **Default:** `d / t`, clamped to `1` when `\|v\| < 1` | +| velocityCacheReset | `number`
Milliseconds after which the cached drag distance resets for velocity calculation. **Default:** `200` | +| allowSkippingSnapPoints | `boolean`
Whether a high-velocity release can skip intermediate snap points. **Default:** `true` | +| handleScrollableElements | `boolean`
Whether the drag gesture yields to scrollable children. **Default:** `true` | +| id | `string`
A unique identifier for the component. If not provided, a generated id is used. | +| modal | `boolean`
When `true`: interaction with outside elements is disabled, scroll is locked, and focus is trapped inside the content. **Default:** `true` | +| preventScroll | `boolean`
Whether scroll should be locked even when `modal` is `false`. | +| translations | [`DrawerIntlTranslations`](https://github.com/kobaltedev/kobalte/blob/main/packages/core/src/dialog/dialog.intl.ts)
Localization strings. | + +### Drawer.Content + +| Prop | Description | +| :--- | :--- | +| onOpenAutoFocus | `(event: Event) => void`
Event handler called when focus moves into the content after opening. Can be prevented by calling `event.preventDefault`. | +| onCloseAutoFocus | `(event: Event) => void`
Event handler called when focus returns to the trigger after closing. Can be prevented by calling `event.preventDefault`. | +| onEscapeKeyDown | `(event: KeyboardEvent) => void`
Event handler called when the escape key is pressed. Can be prevented by calling `event.preventDefault`. | +| onPointerDownOutside | `(event: PointerDownOutsideEvent) => void`
Event handler called when a pointer event occurs outside the content. Can be prevented by calling `event.preventDefault`. | +| onFocusOutside | `(event: FocusOutsideEvent) => void`
Event handler called when focus moves outside the content. Can be prevented by calling `event.preventDefault`. | +| onInteractOutside | `(event: InteractOutsideEvent) => void`
Event handler called when an interaction occurs outside the content. Can be prevented by calling `event.preventDefault`. | + +| Data attribute | Description | +| :--- | :--- | +| data-side | The side the drawer appears from: `"bottom"`, `"top"`, `"left"`, or `"right"`. | +| data-expanded | Present when the drawer is open. | +| data-closed | Present when the drawer is closed. | +| data-opening | Present during the open transition. | +| data-closing | Present during the close transition. | +| data-snapping | Present while snapping to a snap point after a drag gesture. | +| data-transitioning | Present during any transition (opening, closing, or snapping). | + +### Drawer.Overlay + +| Data attribute | Description | +| :--- | :--- | +| data-expanded | Present when the drawer is open. | +| data-closing | Present during the close transition. | +| data-transitioning | Present during any transition. | + +### Drawer.Trigger + +`Drawer.Trigger` consists of [Button](/docs/core/components/button). + +| Data attribute | Description | +| :--- | :--- | +| data-expanded | Present when the drawer is open. | +| data-closed | Present when the drawer is closed. | + +### Drawer.useContext + +Access the drawer's reactive state from any component inside `Drawer`. Useful for reading position, building custom controls, or snapping programmatically. + +```tsx +const ctx = Drawer.useContext(); + +ctx.side() // DrawerSide — "bottom" | "top" | "left" | "right" +ctx.activeSnapPoint() // DrawerSize — the current snap point value +ctx.setActiveSnapPoint() // (snapPoint: DrawerSize) => void +ctx.openPercentage() // number — 0 (closed) to 1 (open), tracks drag in real time +ctx.translate() // number — pixel offset from the fully-open position +ctx.isDragging() // boolean +ctx.isTransitioning() // boolean +ctx.transitionState() // null | "opening" | "closing" | "snapping" +``` + +## Rendered elements + +| Component | Default rendered element | +| :--- | :--- | +| `Drawer` | none | +| `Drawer.Trigger` | `button` | +| `Drawer.Portal` | `Portal` | +| `Drawer.Overlay` | `div` | +| `Drawer.Content` | `div` | +| `Drawer.CloseButton` | `button` | +| `Drawer.Title` | `h2` | +| `Drawer.Description` | `p` | + +## Accessibility + +### Keyboard Interactions + +| Key | Description | +| :--- | :--- | +| Space | When focus is on the trigger, opens/closes the drawer. | +| Enter | When focus is on the trigger, opens/closes the drawer. | +| Tab | Moves focus to the next focusable element inside the drawer. | +| Shift + Tab | Moves focus to the previous focusable element inside the drawer. | +| Esc | Closes the drawer and returns focus to the trigger. | + +> Snap-point architecture and drag math are adapted from [corvu/drawer](https://github.com/corvudev/corvu/tree/main/packages/drawer) by Jasmin Noetzli (MIT). diff --git a/apps/docs/src/routes/docs/core/components/dropdown-menu.mdx b/apps/docs/src/routes/docs/core/(1)components/dropdown-menu.mdx similarity index 79% rename from apps/docs/src/routes/docs/core/components/dropdown-menu.mdx rename to apps/docs/src/routes/docs/core/(1)components/dropdown-menu.mdx index 2384281a..424f8a34 100644 --- a/apps/docs/src/routes/docs/core/components/dropdown-menu.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/dropdown-menu.mdx @@ -1,7 +1,20 @@ -import { Preview, Kbd, TabsSnippets } from "../../../../components"; +--- +title: Dropdown Menu +badges: + - icon: npm + label: "Since v0.1.0" + - label: "@kobalte/core/dropdown-menu" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/dropdown-menu" + - icon: aria + label: "Menu button" + url: "https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/" +--- + import { BasicExample, ControlledExample } from "../../../../examples/dropdown-menu"; -# Dropdown Menu +# {frontmatter.title} Displays a menu to the user —such as a set of actions or functions— triggered by a button. @@ -17,7 +30,7 @@ import { DropdownMenu } from "@kobalte/core"; ## Features -- Exposed to assistive technology as a button with a menu using the [WAI ARIA Menu Button](https://www.w3.org/WAI/ARIA/apg/patterns/menubutton/) design pattern. +- Exposed to assistive technology as a button with a menu using the [WAI-ARIA Menu Button](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/) design pattern. - Supports modal and non-modal modes. - Supports submenus. - Supports items, labels, groups of items. @@ -106,294 +119,21 @@ The checkable menu item consists of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { DropdownMenu } from "@kobalte/core/dropdown-menu"; - import { createSignal } from "solid-js"; - import { CheckIcon, ChevronDownIcon, ChevronRightIcon, DotFilledIcon } from "some-icon-library"; - import "./style.css"; - - function App() { - const [showGitLog, setShowGitLog] = createSignal(true); - const [showHistory, setShowHistory] = createSignal(false); - const [branch, setBranch] = createSignal("main"); - - return ( - - - Git Settings - - - - - - - - Commit - - - Push - - - Update Project - - - - GitHub - - - - - - Create Pull Request… - - - View Pull Requests - - - Sync Fork - - - - Open on GitHub - - - - - - - - - - - - Show Git Log - - - - - - Show History - - - - - - - Branches - - - - - - - main - - - - - - develop - - - - - - - - - ); - } - ``` - - - - ```css - .dropdown-menu__trigger { - appearance: none; - display: inline-flex; - justify-content: center; - align-items: center; - height: 40px; - width: auto; - outline: none; - border-radius: 6px; - padding: 0 16px; - background-color: hsl(200 98% 39%); - color: white; - font-size: 16px; - gap: 8px; - line-height: 0; - transition: 250ms background-color; - } - - .dropdown-menu__trigger:hover { - background-color: hsl(201 96% 32%); - } - - .dropdown-menu__trigger:focus-visible { - outline: 2px solid hsl(200 98% 39%); - outline-offset: 2px; - } - - .dropdown-menu__trigger:active { - background-color: hsl(201 90% 27%); - } - - .dropdown-menu__trigger-icon { - height: 20px; - width: 20px; - flex: 0 0 20px; - transition: transform 250ms; - } - - .dropdown-menu__trigger-icon[data-expanded] { - transform: rotate(180deg); - } - - .dropdown-menu__content, - .dropdown-menu__sub-content { - min-width: 220px; - padding: 8px; - background-color: white; - border-radius: 6px; - border: 1px solid hsl(240 6% 90%); - box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - outline: none; - transform-origin: var(--kb-menu-content-transform-origin); - animation: contentHide 250ms ease-in forwards; - } - - .dropdown-menu__content[data-expanded], - .dropdown-menu__sub-content[data-expanded] { - animation: contentShow 250ms ease-out; - } - - .dropdown-menu__item, - .dropdown-menu__checkbox-item, - .dropdown-menu__radio-item, - .dropdown-menu__sub-trigger { - font-size: 16px; - line-height: 1; - color: hsl(240 4% 16%); - border-radius: 4px; - display: flex; - align-items: center; - height: 32px; - padding: 0 8px 0 24px; - position: relative; - user-select: none; - outline: none; - } - - .dropdown-menu__sub-trigger[data-expanded] { - background-color: hsl(204 94% 94%); - color: hsl(201 96% 32%); - } - - .dropdown-menu__item[data-disabled], - .dropdown-menu__checkbox-item[data-disabled], - .dropdown-menu__radio-item[data-disabled], - .dropdown-menu__sub-trigger[data-disabled] { - color: hsl(240 5% 65%); - opacity: 0.5; - pointer-events: none; - } - - .dropdown-menu__item[data-highlighted], - .dropdown-menu__checkbox-item[data-highlighted], - .dropdown-menu__radio-item[data-highlighted], - .dropdown-menu__sub-trigger[data-highlighted] { - outline: none; - background-color: hsl(200 98% 39%); - color: white; - } - - .dropdown-menu__group-label { - padding: 0 24px; - font-size: 14px; - line-height: 32px; - color: hsl(240 4% 46%); - } - - .dropdown-menu__separator { - height: 1px; - border-top: 1px solid hsl(240 6% 90%); - margin: 6px; - } - - .dropdown-menu__item-indicator { - position: absolute; - left: 0; - height: 20px; - width: 20px; - display: inline-flex; - align-items: center; - justify-content: center; - } - - .dropdown-menu__item-right-slot { - margin-left: auto; - padding-left: 20px; - font-size: 14px; - color: hsl(240 4% 46%); - } - - [data-highlighted] > .dropdown-menu__item-right-slot { - color: white; - } - - [data-disabled] .dropdown-menu__item-right-slot { - color: hsl(240 5% 65%); - opacity: 0.5; - } - - @keyframes contentShow { - from { - opacity: 0; - transform: scale(0.96); - } - to { - opacity: 1; - transform: scale(1); - } - } - - @keyframes contentHide { - from { - opacity: 1; - transform: scale(1); - } - to { - opacity: 0; - transform: scale(0.96); - } - } - ``` - - - {/* */} - +:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/dropdown-menu.tsx#L11-L138" + +``` + +```css title="style.css" tab file="../../../../examples/dropdown-menu.module.css" + +``` + +::: ## Usage @@ -409,11 +149,13 @@ An initial, uncontrolled open value can be provided using the `defaultOpen` prop The `open` prop can be used to make the open state controlled. The `onOpenChange` event is fired when the user presses the trigger, an item or interact outside, and receives the new value. - - - +:::preview -```tsx {3,6} + + +--- + +```tsx {4,7} import { createSignal } from "solid-js"; function ControlledExample() { @@ -430,6 +172,8 @@ function ControlledExample() { } ``` +::: + ### Origin-aware animations We expose a CSS custom property `--kb-menu-content-transform-origin` which can be used to animate the content from its computed origin. @@ -675,11 +419,11 @@ The popper positioner will copy the same `z-index` as the `DropdownMenu.Content` | Key | Description | | :------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | -| Space | When focus is on the trigger, opens the menu and focuses the first item.
When focus is on an item, activates the item. | -| Enter | When focus is on the trigger, opens the menu and focuses the first item.
When focus is on an item, activates the item. | -| ArrowDown | When focus is on the trigger, opens the menu and focuses the first item.
When focus is on an item, moves focus to the next item. | -| ArrowUp | When focus is on the trigger, opens the menu and focuses the last item.
When focus is on an item, moves focus to the previous item. | -| ArrowRight / ArrowLeft | When focus is on a sub menu trigger, opens or closes the submenu depending on reading direction. | -| Home | When focus is on an item, moves focus to first item. | -| End | When focus is on an item, moves focus to last item. | -| Esc | Closes the menu and moves focus to the trigger. | +| Space | When focus is on the trigger, opens the menu and focuses the first item.
When focus is on an item, activates the item. | +| Enter | When focus is on the trigger, opens the menu and focuses the first item.
When focus is on an item, activates the item. | +| ArrowDown | When focus is on the trigger, opens the menu and focuses the first item.
When focus is on an item, moves focus to the next item. | +| ArrowUp | When focus is on the trigger, opens the menu and focuses the last item.
When focus is on an item, moves focus to the previous item. | +| ArrowRight / ArrowLeft | When focus is on a sub menu trigger, opens or closes the submenu depending on reading direction. | +| Home | When focus is on an item, moves focus to first item. | +| End | When focus is on an item, moves focus to last item. | +| Esc | Closes the menu and moves focus to the trigger. | diff --git a/apps/docs/src/routes/docs/core/components/file-field.mdx b/apps/docs/src/routes/docs/core/(1)components/file-field.mdx similarity index 69% rename from apps/docs/src/routes/docs/core/components/file-field.mdx rename to apps/docs/src/routes/docs/core/(1)components/file-field.mdx index 350fd79c..a305e0d5 100644 --- a/apps/docs/src/routes/docs/core/components/file-field.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/file-field.mdx @@ -1,7 +1,17 @@ -import { Preview, TabsSnippets, Kbd } from "../../../../components"; +--- +title: File Field +badges: + - icon: npm + label: "Since v0.13.8" + - label: "@kobalte/core/file-field" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/file-field" +--- + import { BasicExample, HTMLFormExample } from "../../../../examples/file-field"; -# File Field +# {frontmatter.title} A file input to upload multiple files, with ability to drag and drop files. @@ -69,165 +79,21 @@ The File Field consists of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { FileField } from "@kobalte/core/file-field"; - import "./style.css"; - - function App() { - return ( - console.log("data", data)} - onFileReject={(data) => console.log("data", data)} - onFileChange={(data) => console.log("data", data)} - > - - File Field - - - Drop your files here... - - Choose files! - - - - - {(file) => ( - - - - - - Delete - - - )} - - - ); - } - ``` - - - - - -```css -.FileField { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 400px; - width: 100%; - row-gap: 5px; -} - -.FileField__label { - color: white; - font-size: 14px; - font-weight: 500; - user-select: none; -} +:::preview -.FileField__dropzone { - display: flex; - flex-direction: column; - row-gap: 10px; - align-items: center; - justify-content: center; - border: 1px dashed rgb(42, 42, 40); - width: 100%; - height: 100%; - color: #ccc; - border-radius: 6px; -} + -.FileField__trigger { - background-color: hsl(201 96% 32%); - color: white; - padding: 5px 10px; - border-radius: 4px; -} +--- -.FileField__itemList { - display: flex; - flex-direction: column; - gap: 3px; - width: 100%; -} - -.FileField__item { - width: 100%; - display: grid; - padding: 16px; - column-gap: 10px; - border-radius: 6px; - grid-template-columns: auto 1fr auto; - grid-template-areas: - "preview name delete" - "preview size delete"; - column-gap: 5px; - border: 1px solid rgb(42, 42, 40); - padding: 10px; -} - -.FileField__itemPreview { - grid-area: preview; -} +```tsx title="index.tsx" tab file="../../../../examples/file-field.tsx#L3-L43" -.FileField__itemPreviewImage { - width: 50px; - object-fit: scale-down; - height: auto; - aspect-ratio: 1; -} - -.FileField__itemName { - grid-area: name; - font-size: 14px; - color: #fff; -} +``` -.FileField__itemSize { - grid-area: size; - font-size: 14px; - color: rgb(181, 179, 173); -} +```css title="style.css" tab file="../../../../examples/file-field.module.css" -.FileField__itemDeleteTrigger { - grid-area: delete; - cursor: pointer; - background-color: #a23434; - color: white; - padding: 2px 8px; - border-radius: 4px; - height: max-content; - align-self: center; -} ``` - - {/* */} - - +::: ## Usage @@ -235,9 +101,11 @@ The File Field consists of: By passing name prop to FileField.HiddenInput, you can use FileField in HTML forms. - - - +:::preview + + + +--- ```tsx function HTMLFormExample() { @@ -284,6 +152,8 @@ function HTMLFormExample() { } ``` +::: + ## API Reference ### FileField @@ -357,5 +227,5 @@ function HTMLFormExample() { | Key | Description | | :--------------- | :------------------------------------------------------------------------------------------------------- | -| Space | When focus is on dropzone, opens the file dialog.
When focus is on trigger, opens the file dialog. | -| Enter | When focus is on dropzone, opens the file dialog.
When focus is on trigger, opens the file dialog. | +| Space | When focus is on dropzone, opens the file dialog.
When focus is on trigger, opens the file dialog. | +| Enter | When focus is on dropzone, opens the file dialog.
When focus is on trigger, opens the file dialog. | diff --git a/apps/docs/src/routes/docs/core/components/hover-card.mdx b/apps/docs/src/routes/docs/core/(1)components/hover-card.mdx similarity index 78% rename from apps/docs/src/routes/docs/core/components/hover-card.mdx rename to apps/docs/src/routes/docs/core/(1)components/hover-card.mdx index 2108214e..20dc557d 100644 --- a/apps/docs/src/routes/docs/core/components/hover-card.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/hover-card.mdx @@ -1,7 +1,17 @@ -import { Preview, Kbd, TabsSnippets } from "../../../../components"; +--- +title: Hover Card +badges: + - icon: npm + label: "Since v0.1.0" + - label: "@kobalte/core/hover-card" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/hover-card" +--- + import { BasicExample } from "../../../../examples/hover-card"; -# Hover Card +# {frontmatter.title} Allows sighted users to preview content available behind a link. @@ -46,126 +56,21 @@ The hovercard consists of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { HoverCard } from "@kobalte/core/hover-card"; - import "./style.css"; - - function App() { - return ( - - - @MLFabien - - - - - Fabien MARIE-LOUISE -

Fabien MARIE-LOUISE

-

- Developer and UI Design enthusiast. Building UI related stuffs for @solid_js -

-
-
-
- ); - } - ``` - -
- - ```css - .hovercard__trigger { - outline: none; - color: hsl(201 96% 32%); - font-size: 16px; - } - - .hovercard__trigger:hover { - text-decoration: underline; - } - - .hovercard__trigger:focus-visible { - outline: 2px solid hsl(200 98% 39%); - outline-offset: 2px; - } - - .hovercard__content { - z-index: 50; - max-width: min(calc(100vw - 16px), 380px); - border: 1px solid hsl(240 5% 84%); - border-radius: 6px; - padding: 16px; - background-color: white; - box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - transform-origin: var(--kb-hovercard-content-transform-origin); - animation: contentHide 250ms ease-in forwards; - } - - .hovercard__content[data-expanded] { - animation: contentShow 250ms ease-out; - } - - .hovercard__title { - color: hsl(240 6% 10%); - font-size: 16px; - font-weight: 600; - } - - .hovercard__description { - font-size: 14px; - color: hsl(240 5% 26%); - } - - .hovercard__avatar { - height: 56px; - width: 56px; - } - - @keyframes contentShow { - from { - opacity: 0; - transform: scale(0.96); - } - to { - opacity: 1; - transform: scale(1); - } - } - - @keyframes contentHide { - from { - opacity: 1; - transform: scale(1); - } - to { - opacity: 0; - transform: scale(0.96); - } - } - ``` - - - {/* */} -
+:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/hover-card.tsx#L3-L32" + +``` + +```css title="style.css" tab file="../../../../examples/hover-card.module.css" + +``` + +::: ## Usage @@ -181,7 +86,7 @@ An initial, uncontrolled open value can be provided using the `defaultOpen` prop The `open` prop can be used to make the open state controlled. The `onOpenChange` event is fired when the user pointer enter or leave the trigger, and receives the new value. -```tsx {3,6} +```tsx {4,7} import { createSignal } from "solid-js"; function ControlledExample() { diff --git a/apps/docs/src/routes/docs/core/components/i18n-provider.mdx b/apps/docs/src/routes/docs/core/(1)components/i18n-provider.mdx similarity index 89% rename from apps/docs/src/routes/docs/core/components/i18n-provider.mdx rename to apps/docs/src/routes/docs/core/(1)components/i18n-provider.mdx index 49968e9e..730a70ab 100644 --- a/apps/docs/src/routes/docs/core/components/i18n-provider.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/i18n-provider.mdx @@ -1,4 +1,15 @@ -# I18nProvider +--- +title: I18n Provider +badges: + - icon: npm + label: "Since v0.1.0" + - label: "@kobalte/core/i18n" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/i18n" +--- + +# {frontmatter.title} Provides the locale for the application to all child components. diff --git a/apps/docs/src/routes/docs/core/components/image.mdx b/apps/docs/src/routes/docs/core/(1)components/image.mdx similarity index 60% rename from apps/docs/src/routes/docs/core/components/image.mdx rename to apps/docs/src/routes/docs/core/(1)components/image.mdx index 9ffb97f2..cd85c31f 100644 --- a/apps/docs/src/routes/docs/core/components/image.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/image.mdx @@ -1,7 +1,17 @@ -import { Preview, TabsSnippets } from "../../../../components"; +--- +title: Image +badges: + - icon: npm + label: "Since v0.5.0" + - label: "@kobalte/core/image" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/image" +--- + import { BasicExample } from "../../../../examples/image"; -# Image +# {frontmatter.title} An image element with an optional fallback for loading and error status. @@ -38,80 +48,21 @@ The image consists of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { Image } from "@kobalte/core/image"; - import "./style.css"; - - function App() { - return ( - <> - - - NS - - - MD - - - ); - } - ``` - - - - ```css - .image { - display: inline-flex; - align-items: center; - justify-content: center; - vertical-align: middle; - overflow: hidden; - user-select: none; - width: 56px; - height: 56px; - border-radius: 100%; - background-color: hsl(240 6% 90%); - } - - .image__img { - width: 100%; - height: 100%; - object-fit: cover; - border-radius: inherit; - } - - .image__fallback { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - background-color: hsl(204 94% 94%); - color: hsl(201 96% 32%); - font-size: 16px; - line-height: 1; - font-weight: 500; - } - ``` - - - {/* */} - +:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/image.tsx#L3-L21" + +``` + +```css title="style.css" tab file="../../../../examples/image.module.css" + +``` + +::: ## Usage @@ -121,7 +72,7 @@ By default `Image.Fallback` will render when the image hasn't loaded. This means If you notice a flash during loading, use the `fallbackDelay` prop to delay its rendering, so it only renders for those with slower internet connections. -```tsx {0} +```tsx {1} - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { Link } from "@kobalte/core/link"; - import "./style.css"; - - function App() { - return ( - - Kobalte - - ); - } - ``` - - - - ```css - .link { - font-weight: 600; - color: hsl(201 96% 32%); - } - - .link:hover { - text-decoration: underline; - } - - .link[data-disabled] { - opacity: 0.5; - cursor: not-allowed; - } - - .link[data-disabled]:hover { - text-decoration: none; - } - ``` - - - {/* */} - +:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/link.tsx#L3-L11" + +``` + +```css title="style.css" tab file="../../../../examples/link.module.css" + +``` + +::: ## Usage @@ -89,9 +68,11 @@ The link consists of: Use the `disabled` prop to disable a link while keeping it accessible for screen readers. - - - +:::preview + + + +--- ```tsx @@ -99,6 +80,8 @@ Use the `disabled` prop to disable a link while keeping it accessible for screen ``` +::: + ## API Reference ### Link @@ -125,4 +108,4 @@ Use the `disabled` prop to disable a link while keeping it accessible for screen | Key | Description | | :--------------- | :------------------ | -| Enter | Activates the link. | +| Enter | Activates the link. | diff --git a/apps/docs/src/routes/docs/core/components/menubar.mdx b/apps/docs/src/routes/docs/core/(1)components/menubar.mdx similarity index 73% rename from apps/docs/src/routes/docs/core/components/menubar.mdx rename to apps/docs/src/routes/docs/core/(1)components/menubar.mdx index dc64d95f..6973ad45 100644 --- a/apps/docs/src/routes/docs/core/components/menubar.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/menubar.mdx @@ -1,8 +1,20 @@ -import { Preview, Kbd, TabsSnippets } from "../../../../components"; +--- +title: Menubar +badges: + - icon: npm + label: "Since v0.12.1" + - label: "@kobalte/core/menubar" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/menubar" + - icon: aria + label: "Menubar" + url: "https://www.w3.org/WAI/ARIA/apg/patterns/menubar/" +--- + import { BasicExample } from "../../../../examples/menubar"; -import { Callout } from "../../../../components/callout"; -# Menubar +# {frontmatter.title} A visually persistent menu common in desktop applications that provides quick access to a consistent set of commands. @@ -112,391 +124,21 @@ The checkable menu item consists of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { Menubar } from "@kobalte/core/menubar"; - import { createSignal } from "solid-js"; - import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "some-icon-library"; - import "./style.css"; - - function App() { - const [showGitLog, setShowGitLog] = createSignal(true); - const [showHistory, setShowHistory] = createSignal(false); - const [branch, setBranch] = createSignal("main"); - - return ( - - - - Git - - - - - Commit - - - Push - - - Update Project - - - - GitHub - - - - - - Create Pull Request… - - - View Pull Requests - - Sync Fork - - - Open on GitHub - - - - - - - - - - - - Show Git Log - - - - - - Show History - - - - - - - Branches - - - - - - - main - - - - - - develop - - - - - - - - - - File - - - - - New Tab - - - New Window - - - New Incognito Window - - - - - - - Share - - - - - - Email Link - - - Messages - - - Notes - - - - - - - - - Print... - - - - - - - - Edit - - - - - Undo - - - Redo - - - - - - - Find - - - - - - Search The Web - - - - Find... - - - Find Next - - - Find Previous - - - - - - - - - Cut - - - Copy - - - Paste - - - - - - ); - } - ``` - - - - ```css - .menubar__root { - display: flex; - justify-content: center; - align-items: center; - } - - .menubar__trigger { - appearance: none; - display: inline-flex; - justify-content: center; - align-items: center; - height: 40px; - width: auto; - outline: none; - padding: 0 16px; - background-color: #f6f6f7; - color: hsl(240 4% 16%); - font-size: 16px; - gap: 8px; - line-height: 0; - transition: 250ms background-color; - } - - .menubar__trigger[data-highlighted="true"] { - background-color: hsl(200 98% 39%); - color: white; - } - - .menubar__trigger:first-child { - border-radius: 4px 0 0 4px; - } - - .menubar__trigger:last-child { - border-left: 2px dashed hsl(240 4% 46%); - border-radius: 0 4px 4px 0; - } - - .menubar__content, - .menubar__sub-content { - min-width: 220px; - padding: 8px; - background-color: white; - border-radius: 6px; - border: 1px solid hsl(240 6% 90%); - box-shadow: - 0 4px 6px -1px rgb(0 0 0 / 0.1), - 0 2px 4px -2px rgb(0 0 0 / 0.1); - outline: none; - transform-origin: var(--kb-menu-content-transform-origin); - animation: contentHide 250ms ease-in forwards; - } - - .menubar__content[data-expanded], - .menubar__sub-content[data-expanded] { - animation: contentShow 250ms ease-out; - } - - .menubar__item, - .menubar__checkbox-item, - .menubar__radio-item, - .menubar__sub-trigger { - font-size: 16px; - line-height: 1; - color: hsl(240 4% 16%); - border-radius: 4px; - display: flex; - align-items: center; - height: 32px; - padding: 0 8px 0 24px; - position: relative; - user-select: none; - outline: none; - } - - .menubar__sub-trigger[data-expanded] { - background-color: hsl(204 94% 94%); - color: hsl(201 96% 32%); - } - - .menubar__item[data-disabled], - .menubar__checkbox-item[data-disabled], - .menubar__radio-item[data-disabled], - .menubar__sub-trigger[data-disabled] { - color: hsl(240 5% 65%); - opacity: 0.5; - pointer-events: none; - } - - .menubar__item[data-highlighted], - .menubar__checkbox-item[data-highlighted], - .menubar__radio-item[data-highlighted], - .menubar__sub-trigger[data-highlighted] { - outline: none; - background-color: hsl(200 98% 39%); - color: white; - } - - .menubar__group-label { - padding: 0 24px; - font-size: 14px; - line-height: 32px; - color: hsl(240 4% 46%); - } - - .menubar__separator { - height: 1px; - border-top: 1px solid hsl(240 6% 90%); - margin: 6px; - } - - .menubar__item-indicator { - position: absolute; - left: 0; - height: 20px; - width: 20px; - display: inline-flex; - align-items: center; - justify-content: center; - } - - .menubar__item-right-slot { - margin-left: auto; - padding-left: 20px; - font-size: 14px; - color: hsl(240 4% 46%); - } - - [data-highlighted] > .menubar__item-right-slot { - color: white; - } - - [data-disabled] .menubar__item-right-slot { - color: hsl(240 5% 65%); - opacity: 0.5; - } - - @keyframes contentShow { - from { - opacity: 0; - transform: scale(0.96); - } - to { - opacity: 1; - transform: scale(1); - } - } - - @keyframes contentHide { - from { - opacity: 1; - transform: scale(1); - } - to { - opacity: 0; - transform: scale(0.96); - } - } - ``` - - - {/* */} - +:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/menubar.tsx#L6-L208" + +``` + +```css title="style.css" tab file="../../../../examples/menubar.module.css" + +``` + +::: ## Usage @@ -504,7 +146,7 @@ The checkable menu item consists of: To make one of the menubar triggers a link instead of opening a menu, omit the `Menubar.Menu` and set `as="a"` on the `Menubar.Trigger`. -```tsx {12} +```tsx {13} Opens a regular menu @@ -776,13 +418,13 @@ The popper positioner will copy the same `z-index` as the `Menubar.Content`. | Key | Description | | :------------------------------------------- | :----------------------------------------------------------------------------------------------- | -| Space | When focus is on an item, activates the item. | -| Enter | When focus is on an item, activates the item. | -| ArrowDown | When focus is on an item, moves focus to the next item. | -| ArrowUp | When focus is on an item, moves focus to the previous item. | -| ArrowRight | When focus is on an item (not sub menu trigger), moves focus to the next menu. | -| ArrowLeft | When focus is on an item (not sub menu item), moves focus to the previous menu. | -| ArrowRight / ArrowLeft | When focus is on a sub menu trigger, opens or closes the submenu depending on reading direction. | -| Home | When focus is on an item, moves focus to first item. | -| End | When focus is on an item, moves focus to last item. | -| Esc | Closes the menu. | +| Space | When focus is on an item, activates the item. | +| Enter | When focus is on an item, activates the item. | +| ArrowDown | When focus is on an item, moves focus to the next item. | +| ArrowUp | When focus is on an item, moves focus to the previous item. | +| ArrowRight | When focus is on an item (not sub menu trigger), moves focus to the next menu. | +| ArrowLeft | When focus is on an item (not sub menu item), moves focus to the previous menu. | +| ArrowRight / ArrowLeft | When focus is on a sub menu trigger, opens or closes the submenu depending on reading direction. | +| Home | When focus is on an item, moves focus to first item. | +| End | When focus is on an item, moves focus to last item. | +| Esc | Closes the menu. | diff --git a/apps/docs/src/routes/docs/core/components/meter.mdx b/apps/docs/src/routes/docs/core/(1)components/meter.mdx similarity index 72% rename from apps/docs/src/routes/docs/core/components/meter.mdx rename to apps/docs/src/routes/docs/core/(1)components/meter.mdx index 70969e31..ca43219a 100644 --- a/apps/docs/src/routes/docs/core/components/meter.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/meter.mdx @@ -1,11 +1,24 @@ -import { Preview, TabsSnippets } from "../../../../components"; +--- +title: Meter +badges: + - icon: npm + label: "Since v0.13.8" + - label: "@kobalte/core/meter" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/meter" + - icon: aria + label: "Meter" + url: "https://www.w3.org/WAI/ARIA/apg/patterns/meter/" +--- + import { BasicExample, CustomValueLabelExample, CustomValueScaleExample, } from "../../../../examples/meter"; -# Meter +# {frontmatter.title} Displays numeric value that varies within a defined range @@ -45,74 +58,21 @@ The meter consists of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { Meter } from "@kobalte/core/meter"; - import "./style.css"; - - function App() { - return ( - -
- Battery Level: - -
- - - -
- ); - } - ``` - -
- - ```css - .meter { - display: flex; - flex-direction: column; - gap: 2px; - width: 300px; - } - - .meter__label-container { - display: flex; - justify-content: space-between; - } - - .meter__label, - .meter__value-label { - color: hsl(240 4% 16%); - font-size: 14px; - } - - .meter__track { - height: 10px; - background-color: hsl(240 6% 90%); - } - - .meter__fill { - background-color: hsl(200 98% 39%); - height: 100%; - width: var(--kb-meter-fill-width); - transition: width 250ms linear; - } - - ``` - - - {/* */} -
+:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/meter.tsx#L3-L17" + +``` + +```css title="style.css" tab file="../../../../examples/meter.module.css" + +``` + +::: ## Usage @@ -120,11 +80,13 @@ The meter consists of: By default, the `value` prop represents the current value of meter, as the minimum and maximum values default to 0 and 100, respectively. Alternatively, a different scale can be used by setting the `minValue` and `maxValue` props. - - - +:::preview + + + +--- -```tsx {0} +```tsx {1}
Disk Space Usage: @@ -136,15 +98,19 @@ By default, the `value` prop represents the current value of meter, as the minim ``` +::: + ### Custom value label The `getValueLabel` prop allows the formatted value used in `Meter.ValueLabel` and ARIA to be replaced with a custom string. It receives the current value, min and max values as parameters. - - - +:::preview + + -```tsx {4} +--- + +```tsx {5} ``` +::: + ### Meter fill width We expose a CSS custom property `--kb-meter-fill-width` which corresponds to the percentage of the meter (ex: 80%). If you are building a linear meter, you can use it to set the width of the `Meter.Fill` component in CSS. diff --git a/apps/docs/src/routes/docs/core/components/navigation-menu.mdx b/apps/docs/src/routes/docs/core/(1)components/navigation-menu.mdx similarity index 63% rename from apps/docs/src/routes/docs/core/components/navigation-menu.mdx rename to apps/docs/src/routes/docs/core/(1)components/navigation-menu.mdx index 9831db9c..4cbe4c65 100644 --- a/apps/docs/src/routes/docs/core/components/navigation-menu.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/navigation-menu.mdx @@ -1,7 +1,20 @@ -import { Preview, Kbd, TabsSnippets } from "../../../../components"; +--- +title: Navigation Menu +badges: + - icon: npm + label: "Since v0.13.3" + - label: "@kobalte/core/navigation-menu" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/navigation-menu" + - icon: aria + label: "Menubar" + url: "https://www.w3.org/WAI/ARIA/apg/patterns/menubar/" +--- + import { BasicExample, AnimationExample } from "../../../../examples/navigation-menu"; -# Navigation Menu +# {frontmatter.title} A collection of links for navigating websites. @@ -15,7 +28,7 @@ import { Root, Menu, ... } from "@kobalte/core/navigation-menu"; ## Features -- Follows the [WAI ARIA Navigation Menubar](https://www.w3.org/WAI/ARIA/apg/patterns/NavigationMenu/) design pattern. +- Follows the [WAI-ARIA Menu and Menubar](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/) design pattern. - Supports modal and non-modal modes. - Supports submenus. - Supports items, labels, groups of items. @@ -112,577 +125,21 @@ The checkable menu item consists of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { NavigationMenu, Orientation } from "@kobalte/core/navigation-menu"; - import { RadioGroup } from "@kobalte/core/radio-group"; - import { For, createSignal } from "solid-js"; - - import "./style.css"; - - export function App() { - const [orientation, setOrientation] = createSignal("horizontal"); - - return ( - <> - - - - Learn{" "} - - - - - - - - Kobalte - - Kobalte - - - Unstyled, accessible components for SolidJS. - - - - - Pigment - - - Ready-to-use components with a consistent look and feel. - - - - - SolidJS - - - Simple and performant reactivity for building user interfaces. - - - - - SolidStart - - - Fine-grained reactivity goes fullstack. - - - - - - - - - Overview{" "} - - - - - - - - - Introduction - - - Build high-quality, accessible design systems and web apps. - - - - - Getting started - - - A quick tutorial to get you up and running with Kobalte. - - - - - Styling - - - Unstyled and compatible with any styling solution. - - - - - Animation - - - Use CSS keyframes or any animation library of your choice. - - - - - Polymorphism - - - Customize behavior or integrate existing libraries. - - - - - Changelog - - - Kobalte releases and their changelogs. - - - - - - - - GitHub - - - - - - - -
- - - class="radio-group" - value={orientation()} - onChange={(value) => setOrientation(value)} - > - - Orientation - -
- - {(orientation) => ( - - - - - - - {orientation} - - - )} - -
- - - ); - } - ``` - - - - ```css - .navigation-menu__root { - display: flex; - justify-content: center; - align-items: center; - padding: 4px; - background-color: white; - width: max-content; - border-radius: 6px; - } - - .navigation-menu__root[data-orientation="vertical"] { - flex-direction: column; - } - - .navigation-menu__trigger { - appearance: none; - display: inline-flex; - justify-content: center; - align-items: center; - width: auto; - outline: none; - padding: 16px 16px; - background-color: white; - color: #0284c7; - font-size: 16px; - gap: 8px; - line-height: 0; - transition: 250ms background-color; - border-radius: 4px; - border: none; - text-decoration: none; - font-weight: 500; - } - - .navigation-menu__trigger[data-highlighted="true"] { - background-color: #0284c710; - } - - .navigation-menu__trigger-indicator { - position: relative; - margin: -7.5px -4px; - height: 15px; - width: 15px; - transition: transform 250ms ease; - } - - .navigation-menu__trigger-indicator[data-expanded] { - transform: rotateX(180deg); - } - - .navigation-menu__root[data-orientation="vertical"] - .navigation-menu-trigger - .navigation-menu__trigger-indicator { - transform: rotate(-90deg); - } - - .navigation-menu__root[data-orientation="vertical"] - .navigation-menu-trigger[data-expanded] - .navigation-menu__trigger-indicator { - transform: rotate(-90deg) rotateX(180deg); - } - - .navigation-menu__viewport { - display: flex; - justify-content: center; - align-items: center; - width: var(--kb-navigation-menu__viewport-width); - height: var(--kb-navigation-menu__viewport-height); - z-index: 1000; - background-color: white; - border: 1px solid hsl(240 6% 90%); - box-shadow: - 0 4px 6px -1px rgb(0 0 0 / 0.1), - 0 2px 4px -2px rgb(0 0 0 / 0.1); - opacity: 0; - overflow-x: clip; - overflow-y: visible; - transform-origin: var(--kb-menu-content-transform-origin); - transition: - width, - height, - 250ms ease; - animation: viewportHide 250ms ease-in forwards; - pointer-events: none; - } - - .navigation-menu__viewport[data-orientation="vertical"] { - overflow-x: visible; - overflow-y: clip; - } - - .navigation-menu__viewport[data-expanded] { - border-radius: 6px; - animation: viewportShow 250ms ease-out; - opacity: 1; - pointer-events: auto; - } - - .navigation-menu__item-callout { - box-sizing: border-box; - display: flex; - justify-content: flex-end; - flex-direction: column; - width: 100%; - height: 100%; - background: linear-gradient(135deg, #0284c7 0%, #42aaff 100%); - border-radius: 6px; - padding: 25px; - text-decoration: none; - outline: none; - user-select: none; - grid-row: span 3; - } - - .navigation-menu__item-callout:focus { - outline: 2px solid #0284c790; - } - - .navigation-menu__item-callout .navigation-menu__item-label { - margin-top: 16px; - font-size: 1.25rem; - color: white; - } - - .navigation-menu__item-callout .navigation-menu__item-description { - color: white; - } - - .navigation-menu__item-label { - font-size: 1rem; - margin-bottom: 8px; - font-weight: 500; - color: #18181b; - line-height: 1.2; - } - - .navigation-menu__item-description { - font-size: 0.9rem; - opacity: 0.8; - color: #18181b; - line-height: 1.4; - } - - .navigation-menu__item-callout > img { - width: 45px; - height: 45px; - } - - .navigation-menu__item { - display: block; - outline: none; - text-decoration: none; - user-select: none; - padding: 12px; - border-radius: 6px; - font-size: 15px; - line-height: 1; - } - - .navigation-menu__item:hover, - .navigation-menu__item:focus { - background-color: #0284c710; - } - - .navigation-menu__content { - position: absolute; - top: 0; - left: 0; - box-sizing: border-box; - outline: none; - display: grid; - padding: 22px; - margin: 0; - column-gap: 10px; - grid-template-rows: repeat(3, 1fr); - grid-auto-flow: column; - animation-duration: 250ms; - animation-timing-function: ease; - animation-fill-mode: forwards; - pointer-events: none; - } - - .navigation-menu__content[data-expanded] { - pointer-events: auto; - } - - .navigation-menu__content[data-motion="from-start"] { - animation-name: enterFromLeft; - } - .navigation-menu__content[data-motion="from-end"] { - animation-name: enterFromRight; - } - .navigation-menu__content[data-motion="to-start"] { - animation-name: exitToLeft; - } - .navigation-menu__content[data-motion="to-end"] { - animation-name: exitToRight; - } - - .navigation-menu__content[data-orientation="vertical"][data-motion="from-start"] { - animation-name: enterFromTop; - } - .navigation-menu__content[data-orientation="vertical"][data-motion="from-end"] { - animation-name: enterFromBottom; - } - .navigation-menu__content[data-orientation="vertical"][data-motion="to-start"] { - animation-name: exitToTop; - } - .navigation-menu__content[data-orientation="vertical"][data-motion="to-end"] { - animation-name: exitToBottom; - } - - @media screen and (max-width: 650px) { - .navigation-menu__item-label { - font-size: 0.9rem; - line-height: 1; - } - - .navigation-menu__item-description { - font-size: 0.75rem; - line-height: 1.1; - } - } - - .navigation-menu__content.content-1 { - width: min(500px, 90dvw); - grid-template-columns: 0.75fr 1fr; - } - - .navigation-menu__content.content-2 { - width: min(600px, 90dvw); - grid-template-columns: 1fr 1fr; - } - - .navigation-menu__content.content-1 > li:first-child { - grid-row: span 3; - } - - .navigation-menu__arrow { - transition: transform 250ms; - } - - @keyframes viewportShow { - from { - opacity: 0; - transform: rotateX(-20deg) scale(0.96); - } - to { - opacity: 1; - transform: rotateX(0deg) scale(1); - } - } - @keyframes viewportHide { - from { - opacity: 1; - transform: rotateX(0deg) scale(1); - } - to { - opacity: 0; - transform: rotateX(-10deg) scale(0.96); - } - } - - @keyframes enterFromRight { - from { - opacity: 0; - transform: translateX(200px); - } - to { - opacity: 1; - transform: translateX(0); - } - } - - @keyframes enterFromLeft { - from { - opacity: 0; - transform: translateX(-200px); - } - to { - opacity: 1; - transform: translateX(0); - } - } - - @keyframes exitToRight { - from { - opacity: 1; - transform: translateX(0); - } - to { - opacity: 0; - transform: translateX(200px); - } - } - - @keyframes exitToLeft { - from { - opacity: 1; - transform: translateX(0); - } - to { - opacity: 0; - transform: translateX(-200px); - } - } - - @keyframes enterFromBottom { - from { - opacity: 0; - transform: translateY(200px); - } - to { - opacity: 1; - transform: translateY(0); - } - } - - @keyframes enterFromTop { - from { - opacity: 0; - transform: translateY(-200px); - } - to { - opacity: 1; - transform: translateY(0); - } - } - - @keyframes exitToBottom { - from { - opacity: 1; - transform: translateY(0); - } - to { - opacity: 0; - transform: translateY(200px); - } - } - - @keyframes exitToTop { - from { - opacity: 1; - transform: translateY(0); - } - to { - opacity: 0; - transform: translateY(-200px); - } - } - ``` - - - {/* */} - +:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/navigation-menu.tsx#L8-L259" + +``` + +```css title="style.css" tab file="../../../../examples/navigation-menu.module.css" + +``` + +::: ## Usage @@ -692,7 +149,7 @@ Since the viewport content has a position of `absolute` to allow animating, Koba The height and width will be copied from the active `NavigationMenu.Content`, which can either be set using CSS or automatically calculated based on it's content. Combined with a transition CSS rule the size can be animated. -```css {2-3} +```css {3-4} /* style.css */ .navigation-menu__viewport { width: var(--kb-navigation-menu-viewport-width); @@ -708,7 +165,7 @@ Combined with a transition CSS rule the size can be animated. To make one of the navigation menu triggers a link instead of opening a menu, omit the `NavigationMenu.Menu` and set `as="a"` on the `NavigationMenu.Trigger`. -```tsx {8} +```tsx {9} Opens a regular menu @@ -731,7 +188,7 @@ To make one of the navigation menu triggers a link instead of opening a menu, om We expose a CSS custom property `--kb-menu-content-transform-origin` which can be used to animate the viewport from its computed origin. -```css {2} +```css {3} /* style.css */ .navigation-menu__viewport { transform-origin: var(--kb-menu-content-transform-origin); @@ -828,202 +285,192 @@ For animating the content of the viewport we apply the attribute `data-motion` o } ``` - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { NavigationMenu } from "@kobalte/core/navigation-menu"; - - import "./style.css"; - - export function App() { - - return ( - - - - Learn{" "} - - - - - - - Learn Content - - - - - - - Overview{" "} - - - - - - - Overview Content - - - - - - GitHub - - - - - - - ); - } - ``` - - - - ```css - /* This snippet only includes the minimum to get the behavior working */ - - .navigation-menu__viewport { - display: flex; - justify-content: center; - align-items: center; - width: var(--kb-navigation-menu__viewport-width); - height: var(--kb-navigation-menu__viewport-height); - z-index: 1000; - opacity: 0; - overflow-x: clip; - overflow-y: visible; - transform-origin: var(--kb-menu-content-transform-origin); - transition: - width, - height, - 250ms ease; - animation: viewportHide 250ms ease-in forwards; - pointer-events: none; - } - - .navigation-menu__viewport[data-expanded] { - animation: viewportShow 250ms ease-out; - opacity: 1; - pointer-events: auto; - } - - .navigation-menu__content { - position: absolute; - top: 0; - left: 0; - box-sizing: border-box; - animation-duration: 250ms; - animation-timing-function: ease; - animation-fill-mode: forwards; - pointer-events: none; - } - - .navigation-menu__content[data-expanded] { - pointer-events: auto; - } - - .navigation-menu__content[data-motion="from-start"] { - animation-name: enterFromLeft; - } - .navigation-menu__content[data-motion="from-end"] { - animation-name: enterFromRight; - } - .navigation-menu__content[data-motion="to-start"] { - animation-name: exitToLeft; - } - .navigation-menu__content[data-motion="to-end"] { - animation-name: exitToRight; - } - - .navigation-menu__arrow { - transition: transform 250ms; - } - - @keyframes viewportShow { - from { - opacity: 0; - transform: rotateX(-20deg) scale(0.96); - } - to { - opacity: 1; - transform: rotateX(0deg) scale(1); - } - } - @keyframes viewportHide { - from { - opacity: 1; - transform: rotateX(0deg) scale(1); - } - to { - opacity: 0; - transform: rotateX(-10deg) scale(0.96); - } - } - - @keyframes enterFromRight { - from { - opacity: 0; - transform: translateX(200px); - } - to { - opacity: 1; - transform: translateX(0); - } - } - - @keyframes enterFromLeft { - from { - opacity: 0; - transform: translateX(-200px); - } - to { - opacity: 1; - transform: translateX(0); - } - } - - @keyframes exitToRight { - from { - opacity: 1; - transform: translateX(0); - } - to { - opacity: 0; - transform: translateX(200px); - } - } - - @keyframes exitToLeft { - from { - opacity: 1; - transform: translateX(0); - } - to { - opacity: 0; - transform: translateX(-200px); - } - } - ``` - - - {/* */} - +:::preview + + + +--- + +```tsx title="index.tsx" tab +import { NavigationMenu } from "@kobalte/core/navigation-menu"; + +import "./style.css"; + +export function App() { + return ( + + + + Learn{" "} + + + + + + + Learn Content + + + + + + + Overview{" "} + + + + + + + Overview Content + + + + + + GitHub + + + + + + + ); +} +``` + +```css title="style.css" tab +/* This snippet only includes the minimum to get the behavior working */ + +.navigation-menu__viewport { + display: flex; + justify-content: center; + align-items: center; + width: var(--kb-navigation-menu__viewport-width); + height: var(--kb-navigation-menu__viewport-height); + z-index: 1000; + opacity: 0; + overflow-x: clip; + overflow-y: visible; + transform-origin: var(--kb-menu-content-transform-origin); + transition: + width, + height, + 250ms ease; + animation: viewportHide 250ms ease-in forwards; + pointer-events: none; +} + +.navigation-menu__viewport[data-expanded] { + animation: viewportShow 250ms ease-out; + opacity: 1; + pointer-events: auto; +} + +.navigation-menu__content { + position: absolute; + top: 0; + left: 0; + box-sizing: border-box; + animation-duration: 250ms; + animation-timing-function: ease; + animation-fill-mode: forwards; + pointer-events: none; +} + +.navigation-menu__content[data-expanded] { + pointer-events: auto; +} + +.navigation-menu__content[data-motion="from-start"] { + animation-name: enterFromLeft; +} +.navigation-menu__content[data-motion="from-end"] { + animation-name: enterFromRight; +} +.navigation-menu__content[data-motion="to-start"] { + animation-name: exitToLeft; +} +.navigation-menu__content[data-motion="to-end"] { + animation-name: exitToRight; +} + +.navigation-menu__arrow { + transition: transform 250ms; +} + +@keyframes viewportShow { + from { + opacity: 0; + transform: rotateX(-20deg) scale(0.96); + } + to { + opacity: 1; + transform: rotateX(0deg) scale(1); + } +} +@keyframes viewportHide { + from { + opacity: 1; + transform: rotateX(0deg) scale(1); + } + to { + opacity: 0; + transform: rotateX(-10deg) scale(0.96); + } +} + +@keyframes enterFromRight { + from { + opacity: 0; + transform: translateX(200px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes enterFromLeft { + from { + opacity: 0; + transform: translateX(-200px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes exitToRight { + from { + opacity: 1; + transform: translateX(0); + } + to { + opacity: 0; + transform: translateX(200px); + } +} + +@keyframes exitToLeft { + from { + opacity: 1; + transform: translateX(0); + } + to { + opacity: 0; + transform: translateX(-200px); + } +} +``` + +::: ## API Reference @@ -1254,13 +701,13 @@ For animating the content of the viewport we apply the attribute `data-motion` o | Key | Description | | :------------------------------------------- | :----------------------------------------------------------------------------------------------- | -| Space | When focus is on an item, activates the item. | -| Enter | When focus is on an item, activates the item. | -| ArrowDown | When focus is on an item, moves focus to the next item. | -| ArrowUp | When focus is on an item, moves focus to the previous item. | -| ArrowRight | When focus is on an item (not sub menu trigger), moves focus to the next menu. | -| ArrowLeft | When focus is on an item (not sub menu item), moves focus to the previous menu. | -| ArrowRight / ArrowLeft | When focus is on a sub menu trigger, opens or closes the submenu depending on reading direction. | -| Home | When focus is on an item, moves focus to first item. | -| End | When focus is on an item, moves focus to last item. | -| Esc | Closes the menu. | +| Space | When focus is on an item, activates the item. | +| Enter | When focus is on an item, activates the item. | +| ArrowDown | When focus is on an item, moves focus to the next item. | +| ArrowUp | When focus is on an item, moves focus to the previous item. | +| ArrowRight | When focus is on an item (not sub menu trigger), moves focus to the next menu. | +| ArrowLeft | When focus is on an item (not sub menu item), moves focus to the previous menu. | +| ArrowRight / ArrowLeft | When focus is on a sub menu trigger, opens or closes the submenu depending on reading direction. | +| Home | When focus is on an item, moves focus to first item. | +| End | When focus is on an item, moves focus to last item. | +| Esc | Closes the menu. | diff --git a/apps/docs/src/routes/docs/core/components/number-field.mdx b/apps/docs/src/routes/docs/core/(1)components/number-field.mdx similarity index 80% rename from apps/docs/src/routes/docs/core/components/number-field.mdx rename to apps/docs/src/routes/docs/core/(1)components/number-field.mdx index ba5139de..436e1715 100644 --- a/apps/docs/src/routes/docs/core/components/number-field.mdx +++ b/apps/docs/src/routes/docs/core/(1)components/number-field.mdx @@ -1,4 +1,17 @@ -import { Preview, TabsSnippets } from "../../../../components"; +--- +title: Number Field +badges: + - icon: npm + label: "Since v0.12.3" + - label: "@kobalte/core/number-field" + - icon: source + label: "Source" + url: "https://github.com/kobaltedev/kobalte/tree/main/packages/core/src/number-field" + - icon: aria + label: "Spinbutton" + url: "https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/" +--- + import { ControlledExample, DefaultValueExample, @@ -10,7 +23,7 @@ import { TriggersExample, } from "../../../../examples/number-field"; -# Number Field +# {frontmatter.title} A number input that allow users to input custom number entries with a keyboard. @@ -65,115 +78,21 @@ The number field consists of: ## Example - - - - - - - index.tsx - style.css - - {/* */} - - ```tsx - import { NumberField } from "@kobalte/core/number-field"; - import "./style.css"; - - function App() { - return ( - - - Quantity - -
- - - -
-
- ); - } - ``` - -
- - ```css - .number-field { - display: flex; - flex-direction: column; - gap: 4px; - } - - .number-field__label { - color: hsl(240 6% 10%); - font-size: 14px; - font-weight: 500; - user-select: none; - } - - .number-field__input { - display: inline-flex; - width: 200px; - border-radius: 6px; - padding: 6px 12px; - font-size: 16px; - outline: none; - background-color: white; - border: 1px solid hsl(240 6% 90%); - color: hsl(240 4% 16%); - transition: - border-color 250ms, - color 250ms; - } - - .number-field__group { - position: relative; - border-radius: 6px; - } - - .number-field__increment { - top: .25rem; - border-top-left-radius: .25rem; - border-top-right-radius: .25rem; - } - - .number-field__decrement { - bottom: .25rem; - border-bottom-left-radius: .25rem; - border-bottom-right-radius: .25rem; - } - - .number-field__increment, .number-field__decrement { - right: .25rem; - position: absolute; - height: 1rem; - width: 1rem; - background: rgba(0,0,0,0.1); - cursor: default; - } - - .number-field__increment:hover, .number-field__decrement:hover { - background: rgba(0,0,0,0.2); - } - - .number-field__input:hover { - border-color: hsl(240 5% 65%); - } - - .number-field__input:focus-visible { - outline: none; - } - - .number-field__group:focus-within { - outline: 2px solid hsl(200 98% 39%); - outline-offset: 2px; - } - ``` - - - {/* */} -
+:::preview + + + +--- + +```tsx title="index.tsx" tab file="../../../../examples/number-field.tsx#L6-L31" + +``` + +```css title="style.css" tab file="../../../../examples/number-field.module.css" + +``` + +::: ## Usage @@ -181,11 +100,13 @@ The number field consists of: An initial, uncontrolled value can be provided using the `defaultValue` prop. - - - +:::preview + + + +--- -```tsx {0} +```tsx {1} Quantity
@@ -196,6 +117,8 @@ An initial, uncontrolled value can be provided using the `defaultValue` prop. ``` +::: + ### Controlled value The `value` and `rawValue` props can be used to make the value controlled. @@ -203,11 +126,13 @@ It is recommended to only use the `rawValue` as it is of type `number`. The `onChange` event is fired when the user type into the input and receive the new value. The `onRawValueChange` prop is called when the value changes and receives a `number`. - - - +:::preview + + -```tsx {3,4,9-12} +--- + +```tsx {4,5,10-13} import { createSignal } from "solid-js"; function ControlledExample() { @@ -237,15 +162,19 @@ function ControlledExample() { } ``` +::: + ### Description The `NumberField.Description` component can be used to associate additional help text with a number field. - - - +:::preview -```tsx {7} + + +--- + +```tsx {8} Quantity
@@ -257,17 +186,21 @@ The `NumberField.Description` component can be used to associate additional help ``` +::: + ### Error message The `NumberField.ErrorMessage` component can be used to help the user fix a validation error. It should be combined with the `validationState` prop to semantically mark the text field as invalid for assistive technologies. By default, it will render only when the `validationState` prop is set to `invalid`, use the `forceMount` prop to always render the error message (ex: for usage with animation libraries). - - - +:::preview + + -```tsx {8,16} +--- + +```tsx {9,17} import { createSignal } from "solid-js"; function ErrorMessageExample() { @@ -290,17 +223,21 @@ function ErrorMessageExample() { } ``` +::: + ### HTML forms The number field `name` prop along with `` can be used for integration with HTML forms. Only the raw value is passed to the form. If the formatted value is wanted (unrecommended) set the `name` attribute on ``. - - - +:::preview + + + +--- -```tsx {7,9} +```tsx {8,10} function HTMLFormExample() { const onSubmit = (e: SubmitEvent) => { // handle form submission. @@ -326,15 +263,19 @@ function HTMLFormExample() { } ``` +::: + ### Triggers The number field supports optional increment/decrement triggers that are easily customizable. - - - +:::preview + + -```tsx {3,5} +--- + +```tsx {4,6} Quantity
@@ -345,15 +286,19 @@ The number field supports optional increment/decrement triggers that are easily ``` +::: + ### Format The value of the number field component can be formatted based on the [locale with the `I18NProvider`](/docs/core/components/i18n-provider) and `formatOptions`. For more information see [React Spectrum NumberField](https://react-spectrum.adobe.com/react-spectrum/NumberField.html). - - - +:::preview -```tsx {0} + + +--- + +```tsx {1} Price
@@ -364,11 +309,13 @@ The value of the number field component can be formatted based on the [locale wi ``` +::: + ### Autofill The number field supports autofill through `NumberField.HiddenInput`. -```tsx {2} +```tsx {3} Quantity diff --git a/apps/docs/src/routes/docs/core/(1)components/otp-field.mdx b/apps/docs/src/routes/docs/core/(1)components/otp-field.mdx new file mode 100644 index 00000000..d6eec593 --- /dev/null +++ b/apps/docs/src/routes/docs/core/(1)components/otp-field.mdx @@ -0,0 +1,415 @@ +import { Preview, TabsSnippets } from "../../../../components"; +import { + AnimatedExample, + BasicExample, + ControlledExample, + CustomPatternExample, + DefaultValueExample, + HTMLFormExample, + OnCompleteExample, + ValidationExample, +} from "../../../../examples/otp-field"; + +# OTP Field + +An accessible one-time password input built from a hidden native `` and fully customizable visual slots. + +## Import + +```ts +import { OTPField } from "@kobalte/core/otp-field"; +// or +import { Root, Input, Label, Description, ErrorMessage } from "@kobalte/core/otp-field"; +// or (deprecated) +import { OTPField } from "@kobalte/core"; +``` + +## Features + +- Built with a hidden native `` that handles all keyboard interaction and browser autofill. +- Visual slots are fully customizable — build any design using the provided context hook. +- Active slot and caret position tracking exposed via context. +- Supports custom character patterns via a regex `pattern` prop. +- Syncs with form reset events. +- Can be controlled or uncontrolled. +- Shifts password manager icons to avoid overlapping the slots. +- Integrates with Kobalte's form control system — supports `validationState`, `required`, `disabled`, and `readOnly` with associated Label, Description, and ErrorMessage sub-components. + +## Anatomy + +The OTP field consists of: + +- **OTPField**: The root container that manages value state and provides form control context. +- **OTPField.Label**: A label that gives the user information on the OTP field. +- **OTPField.Input**: The hidden native `` that handles keyboard events, autofill, and selection tracking. +- **OTPField.Description**: A description that gives the user more information on the OTP field. +- **OTPField.ErrorMessage**: An error message that gives the user information on how to fix a validation error. +- **Slot** *(user-defined)*: A visual character cell built with `useOTPFieldContext`. + +```tsx + + + + {/* user-defined slot elements */} + + + +``` + +## Example + + + + + + + + index.tsx + style.css + + {/* */} + + ```tsx + import { OTPField, useOTPFieldContext } from "@kobalte/core/otp-field"; + import { For } from "solid-js"; + import "./style.css"; + + function makeSlots(n: number) { + return Array.from({ length: n }, (_, i) => i); + } + + function OtpSlot(props: { index: number }) { + const context = useOTPFieldContext(); + const char = () => context.value()[props.index]; + const isActive = () => context.activeSlots().includes(props.index); + const showCaret = () => + context.isInserting() && context.activeSlots()[0] === props.index; + + return ( +
+ {showCaret() ?
: char()} +
+ ); + } + + function App() { + return ( + + + {(i) => } + + ); + } + ``` + + + + ```css + .otp-field { + display: flex; + gap: 8px; + align-items: center; + } + + .otp-field__slot { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 44px; + height: 52px; + border-radius: 8px; + border: 1.5px solid hsl(240 6% 90%); + font-size: 20px; + font-weight: 600; + color: hsl(240 4% 16%); + transition: border-color 150ms, box-shadow 150ms; + user-select: none; + } + + .otp-field__slot[data-active] { + border-color: hsl(200 98% 39%); + box-shadow: 0 0 0 3px hsl(200 98% 39% / 0.15); + } + + .otp-field__caret { + width: 1.5px; + height: 24px; + background-color: hsl(200 98% 39%); + border-radius: 999px; + animation: otp-caret-blink 1.2s ease-out infinite; + } + + @keyframes otp-caret-blink { + 0%, 70%, 100% { opacity: 1; } + 20%, 50% { opacity: 0; } + } + ``` + + + {/* */} + + +## Usage + +### Default value + +An initial, uncontrolled value can be provided using the `defaultValue` prop. + + + + + +```tsx {0} + + + {(i) => } + +``` + +### Controlled value + +The `value` and `onChange` props can be used to make the value controlled. + + + + + +```tsx {3,4,7-9} +import { createSignal } from "solid-js"; + +function ControlledExample() { + const [value, setValue] = createSignal(""); + + return ( + <> + + + {(i) => } + +

Value: {value()}

+ + ); +} +``` + +### Completion callback + +The `onComplete` prop fires once when all slots are filled. + + + + + +```tsx {5-7} + console.log("Complete:", v)} +> + + {(i) => } + +``` + +### Custom character pattern + +By default the input only accepts digits (`^\d*$`). Pass a `pattern` prop on `OTPField.Input` to allow other characters. + + + + + +```tsx {2} + + + {(i) => } + +``` + +Pass `pattern={null}` to disable validation entirely and allow any character. + +### Animated slots + +Render the caret and character **exclusively** (not overlaid) so the character `` mounts fresh on every keystroke. The CSS `animation` property fires on mount, producing a pop-in effect with no JavaScript. + + + + + + + + index.tsx + style.css + + {/* */} + + ```tsx + function AnimatedOtpSlot(props: { index: number }) { + const context = useOTPFieldContext(); + const char = () => context.value()[props.index]; + const isActive = () => context.activeSlots().includes(props.index); + const showCaret = () => + context.isInserting() && context.activeSlots()[0] === props.index; + + return ( +
+ {showCaret() ? ( +
+ ) : ( + {char()} + )} +
+ ); + } + ``` + + + + ```css + .otp-field__char { + display: inline-block; + animation: otp-slip-in 0.5s cubic-bezier(0.16, 1, 0.3, 1) both; + } + + @keyframes otp-slip-in { + from { + transform: translateY(8px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } + } + ``` + + + {/* */} + + +### Validation + +Use `validationState`, `OTPField.Label`, `OTPField.Description`, and `OTPField.ErrorMessage` to integrate with Kobalte's form control system. `OTPField.ErrorMessage` only renders when `validationState` is `"invalid"`. + + + + + +```tsx +function ValidationExample() { + const [value, setValue] = createSignal(""); + + const isInvalid = () => value().length > 0 && value().length < 6; + + return ( + + Verification code + + {/* ...slots... */} + + Enter the 6-digit code sent to your device. + + Please enter all 6 digits. + + ); +} +``` + +### HTML forms + +Add a `name` prop to `OTPField.Input` to include the value in form submissions. The field value resets automatically when the parent form is reset. + + + + + +```tsx {3} + + + + {(i) => } + + +``` + +## API Reference + +### OTPField + +`OTPField` is equivalent to the `Root` import from `@kobalte/core/otp-field` (and deprecated `OTPField.Root`). + +| Prop | Description | +| :--------------- | :-------------------------------------------------------------------------------------------------------------- | +| maxLength | `number`
The maximum number of characters the OTP field accepts. | +| value | `string`
The controlled value of the OTP field. | +| defaultValue | `string`
The default value when initially rendered. Useful when you do not need to control the value. | +| onChange | `(value: string) => void`
Event handler called when the value changes. | +| onComplete | `(value: string) => void`
Event handler called once when all slots are filled. | +| shiftPWManagers | `boolean`
Whether to shift password manager icons to the right to avoid overlapping slots. Defaults to `true`. | +| id | `string`
A unique identifier. Used to generate ids for nested Label, Description, and ErrorMessage. Auto-generated if not provided. | +| name | `string`
The name used when submitting an HTML form. | +| validationState | `'valid' \| 'invalid'`
Whether the OTP field should display its "valid" or "invalid" visual styling. | +| required | `boolean`
Whether the user must fill the OTP field before the form can be submitted. | +| disabled | `boolean`
Whether the OTP field is disabled. | +| readOnly | `boolean`
Whether the OTP field is read only. | + +### OTPField.Label + +| Prop | Description | +| :--- | :---------- | +| `id` | `string`
The id of the label element. Auto-generated from the root id if not provided. | + +### OTPField.Description + +| Prop | Description | +| :--- | :---------- | +| `id` | `string`
The id of the description element. Auto-generated from the root id if not provided. | + +### OTPField.ErrorMessage + +| Prop | Description | +| :----------- | :--------------------------------------------------------------------------------------------------------------------------------------- | +| `forceMount` | `boolean`
Used to force mounting when more control is needed. Useful when controlling animation with SolidJS animation libraries. | + +### OTPField.Input + +| Prop | Description | +| :------------------ | :--------------------------------------------------------------------------------------------------------------------------------------- | +| pattern | `string \| null`
Regex pattern for allowed characters. Defaults to `'^\d*$'` (digits only). Pass `null` to allow any character. | +| noScriptCSSFallback | `string \| null`
CSS injected via `