diff --git a/apps/docs/scripts/generate-components-index.ts b/apps/docs/scripts/generate-components-index.ts index bf45905f..1b397ecd 100644 --- a/apps/docs/scripts/generate-components-index.ts +++ b/apps/docs/scripts/generate-components-index.ts @@ -31,10 +31,9 @@ export function generateComponentsIndex(rootDir: string = componentsDir): string \t\ttitle: ${quote(group.title)}, \t\tentries: [ ${group.entries - .map( - (entry) => - `\t\t\t{ description: ${quote(entry.description)}, name: ${quote(entry.name)}, url: ${quote(entry.url)} },`, - ) + .map((entry) => { + return `\t\t\t{ description: ${quote(entry.description)}, name: ${quote(entry.name)}, url: ${quote(entry.url)} },`; + }) .join('\n')} \t\t], \t},`, diff --git a/apps/docs/scripts/generate-playground-types.ts b/apps/docs/scripts/generate-playground-types.ts index 5eadc9e9..0e93249d 100644 --- a/apps/docs/scripts/generate-playground-types.ts +++ b/apps/docs/scripts/generate-playground-types.ts @@ -79,8 +79,8 @@ function addTypesPackage(packageName: string, packageDir: string): void { // @luke-ui/react — dist .d.ts files plus a package.json stub so Monaco's // bundler-mode resolution can follow the subpath exports map. const reactPackageJsonSchema = z.object({ - name: z.string(), exports: z.record(z.string(), z.string()), + name: z.string(), }); const reactPackageJson = reactPackageJsonSchema.parse( diff --git a/apps/docs/scripts/generate-token-reference.ts b/apps/docs/scripts/generate-token-reference.ts index e949cd42..f18f795f 100644 --- a/apps/docs/scripts/generate-token-reference.ts +++ b/apps/docs/scripts/generate-token-reference.ts @@ -30,10 +30,9 @@ export function generateTokenReference(): string { const familyUnion = families.map((family) => `\n\t| ${quote(family)}`).join(''); const tokenEntries = tokens - .map( - ({ family, path, variable }) => - `\t{ family: ${quote(family)}, path: ${quote(path)}, variable: ${quote(variable)} },`, - ) + .map(({ family, path, variable }) => { + return `\t{ family: ${quote(family)}, path: ${quote(path)}, variable: ${quote(variable)} },`; + }) .join('\n'); return `// Generated by scripts/generate-token-reference.ts — do not edit. diff --git a/apps/docs/src/components/components-index.tsx b/apps/docs/src/components/components-index.tsx index af0125c1..24d93621 100644 --- a/apps/docs/src/components/components-index.tsx +++ b/apps/docs/src/components/components-index.tsx @@ -14,11 +14,11 @@ export function ComponentsIndex(): JSX.Element { return ( {componentIndexGroups.map((group) => ( @@ -31,8 +31,8 @@ export function ComponentsIndex(): JSX.Element { function CategoryGroup({ group }: { group: ComponentIndexGroup }) { return ( & { prefetch?: boolean }) { - return ; + return ; + }, + useParams() { + return useParams({ strict: false }); }, usePathname() { const { isLoading, pathname } = useRouterState({ @@ -36,9 +39,6 @@ const framework = { [router], ); }, - useParams() { - return useParams({ strict: false }); - }, }; /** diff --git a/apps/docs/src/components/playground/icon-toggle-button-group.tsx b/apps/docs/src/components/playground/icon-toggle-button-group.tsx index 941763ef..252e0f37 100644 --- a/apps/docs/src/components/playground/icon-toggle-button-group.tsx +++ b/apps/docs/src/components/playground/icon-toggle-button-group.tsx @@ -1,5 +1,5 @@ -import { Icon } from '@luke-ui/react/icon'; import type { IconName } from '@luke-ui/react/icon'; +import { Icon } from '@luke-ui/react/icon'; import { cx } from '@luke-ui/react/utils'; import type { Selection } from 'react-aria-components/GridList'; import { ToggleButton } from 'react-aria-components/ToggleButton'; @@ -61,13 +61,13 @@ export function IconToggleButtonGroup({ {options.map(({ icon, label: optionLabel, value: optionValue }) => ( - cx( + className={({ isSelected }) => { + return cx( 'flex size-8 items-center justify-center', PILL_CLASS_NAME, isSelected && PILL_SELECTED_CLASS_NAME, - ) - } + ); + }} id={optionValue} key={optionValue} > @@ -112,13 +112,13 @@ export function TextToggleButtonGroup({ > {options.map(({ label: optionLabel, value: optionValue }) => ( - cx( + className={({ isSelected }) => { + return cx( 'flex h-8 items-center justify-center whitespace-nowrap px-3 font-medium text-xs', PILL_CLASS_NAME, isSelected && PILL_SELECTED_CLASS_NAME, - ) - } + ); + }} id={optionValue} key={optionValue} > diff --git a/apps/docs/src/components/playground/use-is-desktop.browser.test.tsx b/apps/docs/src/components/playground/use-is-desktop.browser.test.tsx index 0316115f..ced1b5a0 100644 --- a/apps/docs/src/components/playground/use-is-desktop.browser.test.tsx +++ b/apps/docs/src/components/playground/use-is-desktop.browser.test.tsx @@ -23,9 +23,9 @@ function stubMatchMedia(matches: boolean) { originalMatchMedia = window.matchMedia; window.matchMedia = ((query: string) => { return { + addEventListener: () => {}, matches: query === DESKTOP_MEDIA_QUERY && matches, media: query, - addEventListener: () => {}, removeEventListener: () => {}, } as unknown as MediaQueryList; }) as typeof window.matchMedia; diff --git a/apps/docs/src/components/site-nav.browser.test.tsx b/apps/docs/src/components/site-nav.browser.test.tsx index cb62cf11..a97d79c5 100644 --- a/apps/docs/src/components/site-nav.browser.test.tsx +++ b/apps/docs/src/components/site-nav.browser.test.tsx @@ -9,8 +9,8 @@ import { RouterProvider, } from '@tanstack/react-router'; import { RootProvider } from 'fumadocs-ui/provider/tanstack'; -import { act } from 'react'; import type { ReactNode } from 'react'; +import { act } from 'react'; import type { Root } from 'react-dom/client'; import { createRoot } from 'react-dom/client'; import { afterEach, expect, test } from 'vite-plus/test'; diff --git a/apps/docs/src/components/theme-controls.browser.test.tsx b/apps/docs/src/components/theme-controls.browser.test.tsx index 15503e0b..be017d4e 100644 --- a/apps/docs/src/components/theme-controls.browser.test.tsx +++ b/apps/docs/src/components/theme-controls.browser.test.tsx @@ -5,8 +5,8 @@ import { IconSpritesheetProvider } from '@luke-ui/react/icon'; import spriteSheetHref from '@luke-ui/react/spritesheet.svg?url&no-inline'; import { themeClassName as paperThemeClassName } from '@luke-ui/react/themes/paper'; import { ThemeProvider } from 'next-themes'; -import { act } from 'react'; import type { ComponentProps, ReactNode } from 'react'; +import { act } from 'react'; import type { Root } from 'react-dom/client'; import { createRoot } from 'react-dom/client'; import { renderToString } from 'react-dom/server'; @@ -168,9 +168,9 @@ test('boots the stored colour mode before the themed root hydrates', async () => ); const iframe = document.body.appendChild(document.createElement('iframe')); iframe.srcdoc = `${bootstrap}`; - await new Promise((resolve) => - iframe.addEventListener('load', () => resolve(), { once: true }), - ); + await new Promise((resolve) => { + iframe.addEventListener('load', () => resolve(), { once: true }); + }); expect(iframe.contentDocument?.documentElement).toHaveAttribute('data-color-mode', 'dark'); expect(iframe.contentDocument?.documentElement).toHaveAttribute('data-mode-at-hydration', 'dark'); diff --git a/apps/docs/src/components/theme-dom-guard.browser.test.tsx b/apps/docs/src/components/theme-dom-guard.browser.test.tsx index 8169759f..d03aa69e 100644 --- a/apps/docs/src/components/theme-dom-guard.browser.test.tsx +++ b/apps/docs/src/components/theme-dom-guard.browser.test.tsx @@ -3,8 +3,8 @@ import '@luke-ui/react/themes/tactile/stylesheet.css'; import { rootClassName } from '@luke-ui/react/theme'; import { themeClassName as tactileThemeClassName } from '@luke-ui/react/themes/tactile'; import { cx } from '@luke-ui/react/utils'; -import { act } from 'react'; import type { ReactNode } from 'react'; +import { act } from 'react'; import type { Root } from 'react-dom/client'; import { createRoot } from 'react-dom/client'; import { afterEach, expect, test } from 'vite-plus/test'; diff --git a/apps/docs/src/components/token-explorer.tsx b/apps/docs/src/components/token-explorer.tsx index 1760a874..dbd26d22 100644 --- a/apps/docs/src/components/token-explorer.tsx +++ b/apps/docs/src/components/token-explorer.tsx @@ -71,10 +71,11 @@ function matchGroups(query: string): ReadonlyArray { return tokenPurposeGroups.flatMap((group) => { if (group.title.toLowerCase().includes(query)) return [group]; - const tokens = group.tokens.filter( - (token) => - token.path.toLowerCase().includes(query) || token.variable.toLowerCase().includes(query), - ); + const tokens = group.tokens.filter((token) => { + return ( + token.path.toLowerCase().includes(query) || token.variable.toLowerCase().includes(query) + ); + }); return tokens.length === 0 ? [] : [{ ...group, tokens }]; }); } @@ -89,7 +90,7 @@ function PurposeDetails({ group }: { group: TokenPurposeGroup }) { > @@ -129,10 +130,7 @@ function TokenTable({ return (
diff --git a/apps/docs/src/examples/box/semantic-element.tsx b/apps/docs/src/examples/box/semantic-element.tsx index 76756cfe..40d6be7e 100644 --- a/apps/docs/src/examples/box/semantic-element.tsx +++ b/apps/docs/src/examples/box/semantic-element.tsx @@ -4,7 +4,7 @@ import { Text } from '@luke-ui/react/text'; export default () => { return ( - + Account settings Manage your profile and sign-in details. diff --git a/apps/docs/src/examples/overview/radius-roles.tsx b/apps/docs/src/examples/overview/radius-roles.tsx index 968c72c2..2f549609 100644 --- a/apps/docs/src/examples/overview/radius-roles.tsx +++ b/apps/docs/src/examples/overview/radius-roles.tsx @@ -26,12 +26,12 @@ export default () => { diff --git a/apps/docs/src/lib/components-index-generator.test.ts b/apps/docs/src/lib/components-index-generator.test.ts index 9ca6ba53..adf8c563 100644 --- a/apps/docs/src/lib/components-index-generator.test.ts +++ b/apps/docs/src/lib/components-index-generator.test.ts @@ -86,6 +86,6 @@ Body. expect(generated).not.toContain('/components/forms/topic'); expect(generated).not.toContain("name: 'Topic'"); } finally { - rmSync(scratchDir, { recursive: true, force: true }); + rmSync(scratchDir, { force: true, recursive: true }); } }); diff --git a/apps/docs/src/lib/generate-props-pages.test.ts b/apps/docs/src/lib/generate-props-pages.test.ts index 38ed7991..062fa0ba 100644 --- a/apps/docs/src/lib/generate-props-pages.test.ts +++ b/apps/docs/src/lib/generate-props-pages.test.ts @@ -107,8 +107,8 @@ test('every component guide declaring props has a generated Props page on disk', expect(existsSync(metaPath), `${relativeOutputDir}/meta.json should exist`).toBe(true); expect(readFileSync(propsPath, 'utf8')).toBe(renderPropsPage(frontmatter)); expect(JSON.parse(readFileSync(metaPath, 'utf8'))).toEqual({ - pages: ['!props'], collapsible: false, + pages: ['!props'], pagesIndex: `../${componentName}`, }); } @@ -165,8 +165,8 @@ test('generates props.mdx and meta.json under // from the /< expect(existsSync(metaPath)).toBe(true); expect(resolve(propsPath, '..')).toBe(outputDir); expect(JSON.parse(readFileSync(metaPath, 'utf8'))).toEqual({ - pages: ['!props'], collapsible: false, + pages: ['!props'], pagesIndex: '../button', }); }); diff --git a/apps/docs/src/lib/layout.shared.tsx b/apps/docs/src/lib/layout.shared.tsx index 7fcd03e0..c037e5f2 100644 --- a/apps/docs/src/lib/layout.shared.tsx +++ b/apps/docs/src/lib/layout.shared.tsx @@ -12,10 +12,10 @@ export function baseOptions(): Omit { nav: { mode: 'top', }, - tabs: false, slots: { header: DocsSiteNav, themeSwitch: false, }, + tabs: false, }; } diff --git a/apps/docs/src/lib/playground-protocol.ts b/apps/docs/src/lib/playground-protocol.ts index 4141c10d..660fb0a0 100644 --- a/apps/docs/src/lib/playground-protocol.ts +++ b/apps/docs/src/lib/playground-protocol.ts @@ -1,14 +1,14 @@ import { z } from 'zod'; const codeMessageSchema = z.object({ - type: z.literal('playground:code'), code: z.string(), + type: z.literal('playground:code'), }); const appearanceMessageSchema = z.object({ - type: z.literal('playground:appearance'), colorMode: z.enum(['light', 'dark', 'system']), themeIdentity: z.enum(['tactile', 'paper']), + type: z.literal('playground:appearance'), }); const parentMessageSchema = z.discriminatedUnion('type', [ @@ -19,7 +19,7 @@ const parentMessageSchema = z.discriminatedUnion('type', [ const previewMessageSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('playground:ready') }), z.object({ type: z.literal('playground:success') }), - z.object({ type: z.literal('playground:error'), message: z.string() }), + z.object({ message: z.string(), type: z.literal('playground:error') }), ]); export type PlaygroundCodeMessage = z.infer; diff --git a/apps/docs/src/lib/shiki-theme.ts b/apps/docs/src/lib/shiki-theme.ts index 53ff3dd7..be62ac66 100644 --- a/apps/docs/src/lib/shiki-theme.ts +++ b/apps/docs/src/lib/shiki-theme.ts @@ -12,6 +12,6 @@ export const SHIKI_THEME_REGISTRATIONS: [typeof githubLight, typeof githubDark] * Theme order controls Shiki's generated classes and custom properties. */ export const SHIKI_THEMES = { - light: SHIKI_THEME_REGISTRATIONS[0].name, dark: SHIKI_THEME_REGISTRATIONS[1].name, + light: SHIKI_THEME_REGISTRATIONS[0].name, }; diff --git a/apps/docs/src/lib/token-purpose-groups.test.ts b/apps/docs/src/lib/token-purpose-groups.test.ts index f7a00568..eeeb5884 100644 --- a/apps/docs/src/lib/token-purpose-groups.test.ts +++ b/apps/docs/src/lib/token-purpose-groups.test.ts @@ -38,9 +38,9 @@ test('shows samples only when a purpose group has one coherent preview treatment test('splits the colour family across the purposes it serves', () => { const purposeOf = new Map( - buildTokenPurposeGroups().flatMap((group) => - group.tokens.map((token) => [token.path, group.id] as const), - ), + buildTokenPurposeGroups().flatMap((group) => { + return group.tokens.map((token) => [token.path, group.id] as const); + }), ); expect(purposeOf.get('color.surface.canvas')).toBe('surfaces'); diff --git a/apps/docs/src/routes/playground/index.tsx b/apps/docs/src/routes/playground/index.tsx index ef11fa20..d887ccc6 100644 --- a/apps/docs/src/routes/playground/index.tsx +++ b/apps/docs/src/routes/playground/index.tsx @@ -148,8 +148,8 @@ function Playground() { {/* Pane backgrounds match the Catppuccin Latte/Mocha `editor.background` values in monaco-setup.ts. */} dispatch({ type: 'code-error', message }), []); + const markError = useCallback((message: string) => dispatch({ message, type: 'code-error' }), []); const markReady = useCallback(() => dispatch({ type: 'iframe-ready' }), []); const markSuccess = useCallback(() => dispatch({ type: 'code-applied' }), []); diff --git a/apps/docs/src/samples/theming/author-theme.tsx b/apps/docs/src/samples/theming/author-theme.tsx index cecbc643..80963535 100644 --- a/apps/docs/src/samples/theming/author-theme.tsx +++ b/apps/docs/src/samples/theming/author-theme.tsx @@ -1,5 +1,5 @@ -import { defineTheme } from '@luke-ui/react/theme'; import type { ThemeInput } from '@luke-ui/react/theme'; +import { defineTheme } from '@luke-ui/react/theme'; import { writeFile } from 'node:fs/promises'; export async function writeTheme(input: ThemeInput) { diff --git a/apps/docs/src/samples/theming/minimal-theme.tsx b/apps/docs/src/samples/theming/minimal-theme.tsx index 1acccbd1..f7ed57e2 100644 --- a/apps/docs/src/samples/theming/minimal-theme.tsx +++ b/apps/docs/src/samples/theming/minimal-theme.tsx @@ -1,6 +1,6 @@ import { defineTheme } from '@luke-ui/react/theme'; export const css = defineTheme({ - name: 'product', color: { accent: '#3b82f6', neutralStyle: 'cool' }, + name: 'product', }); diff --git a/apps/docs/vite.config.ts b/apps/docs/vite.config.ts index 9a8096c3..165b47d5 100644 --- a/apps/docs/vite.config.ts +++ b/apps/docs/vite.config.ts @@ -72,21 +72,6 @@ export default defineConfig(async () => { }, }, }, - // `vp pack` compiles the pre-hydration skeleton script to an inline-able - // IIFE artifact; it runs as part of `docs#generate`, not `vp build`. - pack: { - clean: false, - dts: false, - // Emitted as src/generated/editor-skeleton-script.iife.js — the `.iife` - // suffix is fixed by tsdown for this format. - entry: ['src/components/playground/editor-skeleton-script.ts'], - format: 'iife' as const, - // The artifact is inlined into every playground HTML response, so - // strip the source's documentation comments. - minify: true, - outDir: 'src/generated', - platform: 'browser' as const, - }, optimizeDeps: { // @luke-ui/react is a workspace package excluded from pre-bundling so its // source hot-reloads directly. Its runtime npm dependencies are listed @@ -129,6 +114,21 @@ export default defineConfig(async () => { 'sucrase', ], }, + // `vp pack` compiles the pre-hydration skeleton script to an inline-able + // IIFE artifact; it runs as part of `docs#generate`, not `vp build`. + pack: { + clean: false, + dts: false, + // Emitted as src/generated/editor-skeleton-script.iife.js — the `.iife` + // suffix is fixed by tsdown for this format. + entry: ['src/components/playground/editor-skeleton-script.ts'], + format: 'iife' as const, + // The artifact is inlined into every playground HTML response, so + // strip the source's documentation comments. + minify: true, + outDir: 'src/generated', + platform: 'browser' as const, + }, plugins: lazyPlugins(async () => [ staticFunctionBasePathPlugin(), highlightSourcePlugin(), diff --git a/apps/docs/vitest.config.ts b/apps/docs/vitest.config.ts index c6bcd761..c68e3b04 100644 --- a/apps/docs/vitest.config.ts +++ b/apps/docs/vitest.config.ts @@ -5,7 +5,6 @@ import { playwright } from 'vite-plus/test/browser-playwright'; import * as sourceConfig from './source.config.js'; export default defineConfig({ - plugins: [tailwindcss(), mdx(sourceConfig)], optimizeDeps: { include: [ 'next-themes', @@ -17,6 +16,7 @@ export default defineConfig({ 'react-resizable-panels', ], }, + plugins: [tailwindcss(), mdx(sourceConfig)], test: { passWithNoTests: true, projects: [ diff --git a/package.json b/package.json index e94312c9..9c234959 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,11 @@ "build:storybook": "turbo run build:storybook --filter=@luke-ui/react", "changeset": "changeset", "check": "turbo run check check:cycles check:unused", + "check:cycles": "knip --cycles", "check:format": "turbo run check:format-root check:format", "check:format-root": "vp fmt . --check", "check:lint": "turbo run check:lint", "check:types": "turbo run check:types", - "check:cycles": "knip --cycles", "check:unused": "knip", "clean": "turbo run clean && rm -rf node_modules", "dev": "turbo run dev dev:storybook", diff --git a/packages/@luke-ui/react/icons/add.svg b/packages/@luke-ui/react/icons/add.svg index 61872540..1026ef48 100644 --- a/packages/@luke-ui/react/icons/add.svg +++ b/packages/@luke-ui/react/icons/add.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/arrowDown.svg b/packages/@luke-ui/react/icons/arrowDown.svg index 3e2d6ca7..59056b0a 100644 --- a/packages/@luke-ui/react/icons/arrowDown.svg +++ b/packages/@luke-ui/react/icons/arrowDown.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/arrowLeft.svg b/packages/@luke-ui/react/icons/arrowLeft.svg index a3f778db..89ae9bf1 100644 --- a/packages/@luke-ui/react/icons/arrowLeft.svg +++ b/packages/@luke-ui/react/icons/arrowLeft.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/arrowRight.svg b/packages/@luke-ui/react/icons/arrowRight.svg index e942994e..3a8a2f58 100644 --- a/packages/@luke-ui/react/icons/arrowRight.svg +++ b/packages/@luke-ui/react/icons/arrowRight.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/arrowUp.svg b/packages/@luke-ui/react/icons/arrowUp.svg index 7cbb4580..4413a3df 100644 --- a/packages/@luke-ui/react/icons/arrowUp.svg +++ b/packages/@luke-ui/react/icons/arrowUp.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/bookOpen.svg b/packages/@luke-ui/react/icons/bookOpen.svg index a8d40281..23fd2a70 100644 --- a/packages/@luke-ui/react/icons/bookOpen.svg +++ b/packages/@luke-ui/react/icons/bookOpen.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/check.svg b/packages/@luke-ui/react/icons/check.svg index e0ad98c9..b1b299c2 100644 --- a/packages/@luke-ui/react/icons/check.svg +++ b/packages/@luke-ui/react/icons/check.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/checkCircle.svg b/packages/@luke-ui/react/icons/checkCircle.svg index e63e8fc2..54167fa7 100644 --- a/packages/@luke-ui/react/icons/checkCircle.svg +++ b/packages/@luke-ui/react/icons/checkCircle.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/chevronDown.svg b/packages/@luke-ui/react/icons/chevronDown.svg index 3664435d..829f53f8 100644 --- a/packages/@luke-ui/react/icons/chevronDown.svg +++ b/packages/@luke-ui/react/icons/chevronDown.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/chevronLeft.svg b/packages/@luke-ui/react/icons/chevronLeft.svg index c3063d33..5f5c1043 100644 --- a/packages/@luke-ui/react/icons/chevronLeft.svg +++ b/packages/@luke-ui/react/icons/chevronLeft.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/chevronRight.svg b/packages/@luke-ui/react/icons/chevronRight.svg index 7e3f638b..d0ec1016 100644 --- a/packages/@luke-ui/react/icons/chevronRight.svg +++ b/packages/@luke-ui/react/icons/chevronRight.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/chevronUp.svg b/packages/@luke-ui/react/icons/chevronUp.svg index a6bc51fc..db7a7321 100644 --- a/packages/@luke-ui/react/icons/chevronUp.svg +++ b/packages/@luke-ui/react/icons/chevronUp.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/circleHalf.svg b/packages/@luke-ui/react/icons/circleHalf.svg index b5b192d5..48a903b9 100644 --- a/packages/@luke-ui/react/icons/circleHalf.svg +++ b/packages/@luke-ui/react/icons/circleHalf.svg @@ -1,4 +1,4 @@ - - + + diff --git a/packages/@luke-ui/react/icons/close.svg b/packages/@luke-ui/react/icons/close.svg index 6b8ab35b..8f570a24 100644 --- a/packages/@luke-ui/react/icons/close.svg +++ b/packages/@luke-ui/react/icons/close.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/closeCircle.svg b/packages/@luke-ui/react/icons/closeCircle.svg index 47458a4d..68ca9932 100644 --- a/packages/@luke-ui/react/icons/closeCircle.svg +++ b/packages/@luke-ui/react/icons/closeCircle.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/codeBlock.svg b/packages/@luke-ui/react/icons/codeBlock.svg index c99c3471..e84cc0ea 100644 --- a/packages/@luke-ui/react/icons/codeBlock.svg +++ b/packages/@luke-ui/react/icons/codeBlock.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/copy.svg b/packages/@luke-ui/react/icons/copy.svg index c36ab5e0..d75028ad 100644 --- a/packages/@luke-ui/react/icons/copy.svg +++ b/packages/@luke-ui/react/icons/copy.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/delete.svg b/packages/@luke-ui/react/icons/delete.svg index 49d9b6ed..9764055f 100644 --- a/packages/@luke-ui/react/icons/delete.svg +++ b/packages/@luke-ui/react/icons/delete.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/edit.svg b/packages/@luke-ui/react/icons/edit.svg index ee2a66dd..3ffa4e05 100644 --- a/packages/@luke-ui/react/icons/edit.svg +++ b/packages/@luke-ui/react/icons/edit.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/exclamationTriangle.svg b/packages/@luke-ui/react/icons/exclamationTriangle.svg index b868254c..dd5df46f 100644 --- a/packages/@luke-ui/react/icons/exclamationTriangle.svg +++ b/packages/@luke-ui/react/icons/exclamationTriangle.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/expand.svg b/packages/@luke-ui/react/icons/expand.svg index 4a19bb7f..eeba308a 100644 --- a/packages/@luke-ui/react/icons/expand.svg +++ b/packages/@luke-ui/react/icons/expand.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/externalLink.svg b/packages/@luke-ui/react/icons/externalLink.svg index 4f1e3aef..23c8116d 100644 --- a/packages/@luke-ui/react/icons/externalLink.svg +++ b/packages/@luke-ui/react/icons/externalLink.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/minimize.svg b/packages/@luke-ui/react/icons/minimize.svg index 9233d904..7356bb84 100644 --- a/packages/@luke-ui/react/icons/minimize.svg +++ b/packages/@luke-ui/react/icons/minimize.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/mobilePhone.svg b/packages/@luke-ui/react/icons/mobilePhone.svg index 042d10e9..d37508cd 100644 --- a/packages/@luke-ui/react/icons/mobilePhone.svg +++ b/packages/@luke-ui/react/icons/mobilePhone.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/monitor.svg b/packages/@luke-ui/react/icons/monitor.svg index 5df21364..d6a45017 100644 --- a/packages/@luke-ui/react/icons/monitor.svg +++ b/packages/@luke-ui/react/icons/monitor.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/moon.svg b/packages/@luke-ui/react/icons/moon.svg index e01a7f40..9037a1b0 100644 --- a/packages/@luke-ui/react/icons/moon.svg +++ b/packages/@luke-ui/react/icons/moon.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/search.svg b/packages/@luke-ui/react/icons/search.svg index e38b1cf8..3880638b 100644 --- a/packages/@luke-ui/react/icons/search.svg +++ b/packages/@luke-ui/react/icons/search.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/icons/sun.svg b/packages/@luke-ui/react/icons/sun.svg index 6c72f4cf..96e6792d 100644 --- a/packages/@luke-ui/react/icons/sun.svg +++ b/packages/@luke-ui/react/icons/sun.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/@luke-ui/react/package.json b/packages/@luke-ui/react/package.json index 59f25494..63678cd8 100644 --- a/packages/@luke-ui/react/package.json +++ b/packages/@luke-ui/react/package.json @@ -87,8 +87,8 @@ "generate:icons": "tsx scripts/build-icons.ts", "generate:themes": "tsx scripts/build-themes.ts", "test": "pnpm run test:ci && pnpm run test:visual", - "test:ci": "pnpm run test:unit && pnpm run test:browser && pnpm run test:storybook", "test:browser": "vp test run --project=browser", + "test:ci": "pnpm run test:unit && pnpm run test:browser && pnpm run test:storybook", "test:storybook": "vp test run --project=storybook", "test:ui": "vp test --project=storybook --ui", "test:unit": "vp test run --project=unit", diff --git a/packages/@luke-ui/react/scripts/check-story-plays.ts b/packages/@luke-ui/react/scripts/check-story-plays.ts index 3bd04134..53934b20 100644 --- a/packages/@luke-ui/react/scripts/check-story-plays.ts +++ b/packages/@luke-ui/react/scripts/check-story-plays.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { parseSync, visitorKeys } from 'oxc-parser'; import type { Node, Program } from 'oxc-parser'; +import { parseSync, visitorKeys } from 'oxc-parser'; const packageRoot = path.resolve(import.meta.dirname, '..'); const storiesRoot = path.join(packageRoot, 'src'); diff --git a/packages/@luke-ui/react/scripts/visual-regression-lib.test.ts b/packages/@luke-ui/react/scripts/visual-regression-lib.test.ts index c57e259b..8d87afd8 100644 --- a/packages/@luke-ui/react/scripts/visual-regression-lib.test.ts +++ b/packages/@luke-ui/react/scripts/visual-regression-lib.test.ts @@ -19,7 +19,7 @@ const png = (red: number) => { * default discards as anti-aliasing rather than counting as a mismatch. */ const pngWithAAStroke = (width: number, height: number, x: number, withStroke: boolean) => { - const image = new PNG({ width, height }); + const image = new PNG({ height, width }); image.data.fill(255); if (withStroke) { for (let y = 10; y < height - 10; y++) { diff --git a/packages/@luke-ui/react/scripts/visual-regression-lib.ts b/packages/@luke-ui/react/scripts/visual-regression-lib.ts index 41087060..91e183f5 100644 --- a/packages/@luke-ui/react/scripts/visual-regression-lib.ts +++ b/packages/@luke-ui/react/scripts/visual-regression-lib.ts @@ -1,5 +1,5 @@ import path from 'node:path'; -import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'; +import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'; import pixelmatch from 'pixelmatch'; import { PNG } from 'pngjs'; diff --git a/packages/@luke-ui/react/src/blockquote/index.tsx b/packages/@luke-ui/react/src/blockquote/index.tsx index 5638532b..85d9a6ef 100644 --- a/packages/@luke-ui/react/src/blockquote/index.tsx +++ b/packages/@luke-ui/react/src/blockquote/index.tsx @@ -1,6 +1,6 @@ import * as styles from '../recipes/blockquote.css.js'; -import { Text } from '../text/index.js'; import type { TextProps } from '../text/index.js'; +import { Text } from '../text/index.js'; import type { DistributiveOmit } from '../types/distributive-omit.js'; import type { Prettify } from '../types/prettify.js'; import { cx } from '../utils/index.js'; @@ -23,7 +23,7 @@ export type BlockquoteProps = Prettify<_BlockquoteProps>; export function Blockquote(props: BlockquoteProps) { const { children, className, ...textProps } = props; return ( - + {children} ); diff --git a/packages/@luke-ui/react/src/box/box.browser.test.tsx b/packages/@luke-ui/react/src/box/box.browser.test.tsx index eb8f0c79..f15f2c43 100644 --- a/packages/@luke-ui/react/src/box/box.browser.test.tsx +++ b/packages/@luke-ui/react/src/box/box.browser.test.tsx @@ -1,4 +1,3 @@ -import type { ComponentProps } from 'react'; import { afterEach, expect, test } from 'vite-plus/test'; import { page } from 'vite-plus/test/context'; import { testUniversalConformance } from '../conformance/helpers.js'; @@ -14,7 +13,7 @@ testUniversalConformance({ }, name: 'Box', registration: componentTestRegistration, - render: (props = {}) => render()}>Content), + render: (props = {}) => render(Content), }); afterEach(async () => { diff --git a/packages/@luke-ui/react/src/button/button.browser.test.tsx b/packages/@luke-ui/react/src/button/button.browser.test.tsx index 2c48f4be..568fbad8 100644 --- a/packages/@luke-ui/react/src/button/button.browser.test.tsx +++ b/packages/@luke-ui/react/src/button/button.browser.test.tsx @@ -1,4 +1,3 @@ -import type { ComponentProps } from 'react'; import { expect } from 'vite-plus/test'; import { testIntegration, testUniversalConformance } from '../conformance/helpers.js'; import { render } from '../test-utils/render.js'; @@ -13,8 +12,9 @@ testUniversalConformance({ }, name: 'Button', registration: componentTestRegistration, - render: (props = {}) => - render(), + render: (props = {}) => { + return render(); + }, }); testIntegration(componentTestRegistration, 'Button', async () => { diff --git a/packages/@luke-ui/react/src/checkbox/checkbox.browser.test.tsx b/packages/@luke-ui/react/src/checkbox/checkbox.browser.test.tsx index 81572537..74a65614 100644 --- a/packages/@luke-ui/react/src/checkbox/checkbox.browser.test.tsx +++ b/packages/@luke-ui/react/src/checkbox/checkbox.browser.test.tsx @@ -1,8 +1,7 @@ import { createRef } from 'react'; -import type { ComponentProps } from 'react'; import { expect, test } from 'vite-plus/test'; -import { cdp, page, userEvent } from 'vite-plus/test/context'; import type { Locator } from 'vite-plus/test/context'; +import { cdp, page, userEvent } from 'vite-plus/test/context'; import { testFieldShapedConformance, testIntegration } from '../conformance/helpers.js'; import { render } from '../test-utils/render.js'; import { componentTestRegistration } from './component-test-registration.js'; @@ -27,8 +26,9 @@ testFieldShapedConformance({ }, name: 'Checkbox', registration: componentTestRegistration, - render: (props = {}) => - render()}>Terms), + render: (props = {}) => { + return render(Terms); + }, }); testIntegration(componentTestRegistration, 'Checkbox', async () => { diff --git a/packages/@luke-ui/react/src/checkbox/checkbox.visual.test.tsx b/packages/@luke-ui/react/src/checkbox/checkbox.visual.test.tsx index 6e7f2f8d..32ac6b38 100644 --- a/packages/@luke-ui/react/src/checkbox/checkbox.visual.test.tsx +++ b/packages/@luke-ui/react/src/checkbox/checkbox.visual.test.tsx @@ -1,6 +1,6 @@ import { test } from 'vite-plus/test'; -import { page, userEvent } from 'vite-plus/test/context'; import type { Locator } from 'vite-plus/test/context'; +import { page, userEvent } from 'vite-plus/test/context'; import { render, visualAppearances } from '../test-utils/render.js'; import { captureVisual, @@ -30,7 +30,7 @@ test('kitchen sink', async () => { Invalid {(['100', '200', '300', '400', '500', '600', '700', '800', '900'] as const).map((size) => ( - + {size}: This label wraps to show that the control aligns with its first line. @@ -88,11 +88,11 @@ test('interactive states', async () => { , ); - const unchecked = page.getByRole('checkbox', { name: 'Invalid', exact: true }); - const selected = page.getByRole('checkbox', { name: 'Invalid selected', exact: true }); + const unchecked = page.getByRole('checkbox', { exact: true, name: 'Invalid' }); + const selected = page.getByRole('checkbox', { exact: true, name: 'Invalid selected' }); const indeterminate = page.getByRole('checkbox', { - name: 'Invalid indeterminate', exact: true, + name: 'Invalid indeterminate', }); for (const [name, checkbox] of [ diff --git a/packages/@luke-ui/react/src/checkbox/index.tsx b/packages/@luke-ui/react/src/checkbox/index.tsx index e9b57b13..92aca147 100644 --- a/packages/@luke-ui/react/src/checkbox/index.tsx +++ b/packages/@luke-ui/react/src/checkbox/index.tsx @@ -1,23 +1,29 @@ import { useObjectRef } from '@react-aria/utils'; import type { JSX, ReactNode, Ref } from 'react'; import type { CheckboxFieldProps as RacCheckboxFieldProps } from 'react-aria-components/Checkbox'; -import { FieldDescription, FieldError } from '../field/primitive/index.js'; import type { FieldErrorProps } from '../field/primitive/index.js'; +import { FieldDescription, FieldError } from '../field/primitive/index.js'; import type { DistributiveOmit } from '../types/distributive-omit.js'; import type { Prettify } from '../types/prettify.js'; +import type { CheckboxProps as PrimitiveCheckboxProps } from './primitive/index.js'; import { - Checkbox as PrimitiveCheckbox, CheckboxContent, CheckboxControl, CheckboxIndicator, + Checkbox as PrimitiveCheckbox, } from './primitive/index.js'; -import type { CheckboxProps as PrimitiveCheckboxProps } from './primitive/index.js'; type _CheckboxOmit = DistributiveOmit; interface _CheckboxProps extends _CheckboxOmit { /** Checkbox label content. */ children: ReactNode; + /** Initial selection state for an uncontrolled checkbox. */ + defaultSelected?: PrimitiveCheckboxProps['defaultSelected']; + /** Supporting text shown beneath the checkbox label. */ + description?: ReactNode; + /** Validation message shown when the checkbox is invalid. */ + errorMessage?: FieldErrorProps['children']; /** * Forwarded to the underlying `
$ - + USD
diff --git a/packages/@luke-ui/react/src/text/index.tsx b/packages/@luke-ui/react/src/text/index.tsx index 0ab36dee..b1319ce5 100644 --- a/packages/@luke-ui/react/src/text/index.tsx +++ b/packages/@luke-ui/react/src/text/index.tsx @@ -126,7 +126,6 @@ export function Text(props: TextProps) { return ( {children} diff --git a/packages/@luke-ui/react/src/theme/__fixtures__/radix-scales.ts b/packages/@luke-ui/react/src/theme/__fixtures__/radix-scales.ts index 6f3c2ca1..0c990054 100644 --- a/packages/@luke-ui/react/src/theme/__fixtures__/radix-scales.ts +++ b/packages/@luke-ui/react/src/theme/__fixtures__/radix-scales.ts @@ -17,10 +17,10 @@ import { parseColor } from '../color.js'; /** A Radix 12-step scale as published hex values, step 1 through step 12. */ interface RadixScaleHex { - /** The Radix family name. */ - name: string; /** The colour mode the scale is authored for. */ mode: 'light' | 'dark'; + /** The Radix family name. */ + name: string; /** The 12 step hex values, step 1 (app background) through step 12 (high-contrast text). */ steps: readonly [ string, @@ -228,8 +228,8 @@ const RADIX_DARK_SCALES: ReadonlyArray = [ /** A Radix scale converted to the generator's OKLCH space, step 1 through step 12. */ export interface RadixScaleOklch { - name: string; mode: 'light' | 'dark'; + name: string; steps: Array; } @@ -272,10 +272,10 @@ function envelope( export interface CorpusEntry { /** A short label for the case. */ name: string; - /** The source colour, as any string `parseColor` accepts. */ - source: string; /** Why this case is interesting for the generator. */ note: string; + /** The source colour, as any string `parseColor` accepts. */ + source: string; } /** diff --git a/packages/@luke-ui/react/src/theme/build-theme.test.ts b/packages/@luke-ui/react/src/theme/build-theme.test.ts index 34b9ff69..629257c2 100644 --- a/packages/@luke-ui/react/src/theme/build-theme.test.ts +++ b/packages/@luke-ui/react/src/theme/build-theme.test.ts @@ -163,7 +163,11 @@ describe('bundled themes meet WCAG 2.2 AA', () => { ); expect(lightCanvas.c).toBe(0); - expect(lightRecessed).toEqual({ l: 1, c: 0, h: 0 }); + expect(lightRecessed).toEqual({ + l: 1, + c: 0, + h: 0, + }); expect(darkCanvas.l - darkRecessed.l).toBeGreaterThanOrEqual(0.02); }); diff --git a/packages/@luke-ui/react/src/theme/build-theme.ts b/packages/@luke-ui/react/src/theme/build-theme.ts index b03bc7df..4267dea8 100644 --- a/packages/@luke-ui/react/src/theme/build-theme.ts +++ b/packages/@luke-ui/react/src/theme/build-theme.ts @@ -148,9 +148,9 @@ export class ThemeGenerationError extends Error { type ColorMode = 'light' | 'dark'; interface ModeValues { + diagnostics: ThemeModeDiagnostics; failures: Array; values: Record; - diagnostics: ThemeModeDiagnostics; } function buildModeValues(mode: ColorMode, modeFoundation: ThemeModeFoundation): ModeValues { diff --git a/packages/@luke-ui/react/src/theme/color.test.ts b/packages/@luke-ui/react/src/theme/color.test.ts index 991230df..1fbf1ae2 100644 --- a/packages/@luke-ui/react/src/theme/color.test.ts +++ b/packages/@luke-ui/react/src/theme/color.test.ts @@ -38,7 +38,11 @@ describe('contrastRatio', () => { describe('gamutMapOklch', () => { it('reduces chroma until the colour fits in sRGB while preserving lightness and hue', () => { - const outOfGamut = { l: 0.6, c: 0.4, h: 150 }; + const outOfGamut = { + l: 0.6, + c: 0.4, + h: 150, + }; const mapped = gamutMapOklch(outOfGamut); expect(mapped.l).toBe(0.6); expect(mapped.h).toBe(150); @@ -47,8 +51,22 @@ describe('gamutMapOklch', () => { }); it('leaves in-gamut colours unchanged and clamps extreme lightness', () => { - const inGamut = { l: 0.5, c: 0.05, h: 30 }; + const inGamut = { + l: 0.5, + c: 0.05, + h: 30, + }; expect(gamutMapOklch(inGamut)).toEqual(inGamut); - expect(gamutMapOklch({ l: 1.2, c: 0.2, h: 30 })).toEqual({ l: 1, c: 0, h: 30 }); + expect( + gamutMapOklch({ + l: 1.2, + c: 0.2, + h: 30, + }), + ).toEqual({ + l: 1, + c: 0, + h: 30, + }); }); }); diff --git a/packages/@luke-ui/react/src/theme/color.ts b/packages/@luke-ui/react/src/theme/color.ts index 9b80eead..4d27bba4 100644 --- a/packages/@luke-ui/react/src/theme/color.ts +++ b/packages/@luke-ui/react/src/theme/color.ts @@ -30,7 +30,11 @@ export function parseColor(input: string): Oklch { if (l < 0 || l > 1) { throw new Error(`cannot parse colour "${input}"; oklch lightness must be 0-1 or 0%-100%`); } - return { l, c, h: normalizeHue(h) }; + return { + l, + c, + h: normalizeHue(h), + }; } throw new Error(`cannot parse colour "${input}"; expected #rgb, #rrggbb, or oklch( )`); } @@ -52,21 +56,45 @@ export function contrastRatio(a: Oklch, b: Oklch): number { */ export function gamutMapOklch(color: Oklch): Oklch { const h = normalizeHue(color.h); - if (color.l <= 0) return { l: 0, c: 0, h }; - if (color.l >= 1) return { l: 1, c: 0, h }; - const candidate = { l: color.l, c: Math.max(color.c, 0), h }; + if (color.l <= 0) + return { + l: 0, + c: 0, + h, + }; + if (color.l >= 1) + return { + l: 1, + c: 0, + h, + }; + const candidate = { + l: color.l, + c: Math.max(color.c, 0), + h, + }; if (isInSrgbGamut(candidate)) return candidate; let inGamutChroma = 0; let outOfGamutChroma = candidate.c; for (let iteration = 0; iteration < 32; iteration++) { const mid = (inGamutChroma + outOfGamutChroma) / 2; - if (isInSrgbGamut({ l: color.l, c: mid, h })) { + if ( + isInSrgbGamut({ + l: color.l, + c: mid, + h, + }) + ) { inGamutChroma = mid; } else { outOfGamutChroma = mid; } } - return { l: color.l, c: inGamutChroma, h }; + return { + l: color.l, + c: inGamutChroma, + h, + }; } /** Formats an OKLCH colour as a CSS `oklch()` value, with an optional alpha channel. */ @@ -158,5 +186,9 @@ function linearSrgbToOklch(rgb: SrgbTriple): Oklch { const labB = 0.0259040371 * lCubeRoot + 0.7827717662 * mCubeRoot - 0.808675766 * sCubeRoot; const c = Math.hypot(labA, labB); const h = c < 0.000001 ? 0 : normalizeHue((Math.atan2(labB, labA) * 180) / Math.PI); - return { l, c, h }; + return { + l, + c, + h, + }; } diff --git a/packages/@luke-ui/react/src/theme/contract.test.ts b/packages/@luke-ui/react/src/theme/contract.test.ts index 65661553..fd63d2cd 100644 --- a/packages/@luke-ui/react/src/theme/contract.test.ts +++ b/packages/@luke-ui/react/src/theme/contract.test.ts @@ -44,12 +44,12 @@ describe('theme contract', () => { // wrong. Comparing the whole set, not a sample, also catches a seventh role or a stray leaf. const expected = SEMANTIC_ROLES.flatMap((role) => [ [`color.border.${role}`, `--luke-color-border-${role}`], - ...['subtle', 'solid'].flatMap((prominence) => - ['rest', 'hover', 'pressed'].map((state) => [ + ...['subtle', 'solid'].flatMap((prominence) => { + return ['rest', 'hover', 'pressed'].map((state) => [ `color.background.${role}.${prominence}.${state}`, `--luke-color-background-${role}-${prominence}-${state}`, - ]), - ), + ]); + }), [`color.foreground.${role}.rest`, `--luke-color-foreground-${role}-rest`], [`color.foreground.${role}.hover`, `--luke-color-foreground-${role}-hover`], [`color.foreground.${role}.onSolid`, `--luke-color-foreground-${role}-on-solid`], @@ -64,8 +64,9 @@ describe('theme contract', () => { }); expect(expected).toHaveLength(60); - const byPath = (a: ReadonlyArray, b: ReadonlyArray) => - (a[0] ?? '').localeCompare(b[0] ?? ''); + const byPath = (a: ReadonlyArray, b: ReadonlyArray) => { + return (a[0] ?? '').localeCompare(b[0] ?? ''); + }; expect([...emitted].sort(byPath)).toEqual([...expected].sort(byPath)); }); diff --git a/packages/@luke-ui/react/src/theme/contrast-validation.ts b/packages/@luke-ui/react/src/theme/contrast-validation.ts index e2cade6d..bbd32912 100644 --- a/packages/@luke-ui/react/src/theme/contrast-validation.ts +++ b/packages/@luke-ui/react/src/theme/contrast-validation.ts @@ -14,12 +14,12 @@ type ColorMode = 'light' | 'dark'; /** One WCAG contrast failure recorded while generating a theme. */ export interface ThemeContrastFailure { - /** The colour mode the pair was generated for. */ - mode: 'light' | 'dark'; - /** Token path of the foreground colour, for example `color.text.primary`. */ - foreground: string; /** Token path of the background colour, for example `color.surface.floating`. */ background: string; + /** Token path of the foreground colour, for example `color.text.primary`. */ + foreground: string; + /** The colour mode the pair was generated for. */ + mode: 'light' | 'dark'; /** The contrast ratio achieved by the best attempt. */ ratio: number; /** The WCAG 2.2 AA ratio the pair must reach. */ @@ -27,8 +27,8 @@ export interface ThemeContrastFailure { } interface ValidationResult { - failures: Array; checks: Array; + failures: Array; } /** diff --git a/packages/@luke-ui/react/src/theme/control-border.ts b/packages/@luke-ui/react/src/theme/control-border.ts index f0e4843a..2abb95cc 100644 --- a/packages/@luke-ui/react/src/theme/control-border.ts +++ b/packages/@luke-ui/react/src/theme/control-border.ts @@ -14,14 +14,14 @@ type ColorMode = 'light' | 'dark'; /** The inputs to {@link solveControlBorder}. */ interface SolveControlBorderRequest { - /** The generated neutral family for this mode, whose step 7 seeds the search. */ - neutral: ScaleFamily; /** The canvas surface the boundary is gated against. */ canvas: Oklch; - /** The recessed surface the boundary is gated against. */ - recessed: Oklch; /** The colour mode being solved for. */ mode: ColorMode; + /** The generated neutral family for this mode, whose step 7 seeds the search. */ + neutral: ScaleFamily; + /** The recessed surface the boundary is gated against. */ + recessed: Oklch; } /** @@ -39,13 +39,18 @@ export function solveControlBorder(params: SolveControlBorderRequest): Oklch { const seed = neutral[7]; const direction = mode === 'light' ? -1 : 1; const target = UI_RATIO + RATIO_HEADROOM; - const worstRatio = (candidate: Oklch) => - Math.min(contrastRatio(candidate, canvas), contrastRatio(candidate, recessed)); + const worstRatio = (candidate: Oklch) => { + return Math.min(contrastRatio(candidate, canvas), contrastRatio(candidate, recessed)); + }; let lightness = seed.l; for (;;) { const clamped = clampUnit(lightness); - const candidate = gamutMapOklch({ c: seed.c, h: seed.h, l: clamped }); + const candidate = gamutMapOklch({ + l: clamped, + c: seed.c, + h: seed.h, + }); if (worstRatio(candidate) >= target || clamped === 0 || clamped === 1) return candidate; lightness = clamped + direction * CONTRAST_SEARCH_STEP; } diff --git a/packages/@luke-ui/react/src/theme/define-theme.test.ts b/packages/@luke-ui/react/src/theme/define-theme.test.ts index 1f13a368..6a6de7e0 100644 --- a/packages/@luke-ui/react/src/theme/define-theme.test.ts +++ b/packages/@luke-ui/react/src/theme/define-theme.test.ts @@ -43,9 +43,12 @@ describe('defineTheme colour-only authoring', () => { // statement of the same property is that building from just an accent and a neutral character // does not throw. it('builds a theme from just an accent and a neutral character without a WCAG hard-gate failure', () => { - expect(() => - defineTheme({ color: { accent: '#3b82f6', neutralStyle: 'cool' }, name: 'colour-only' }), - ).not.toThrow(); + expect(() => { + return defineTheme({ + color: { accent: '#3b82f6', neutralStyle: 'cool' }, + name: 'colour-only', + }); + }).not.toThrow(); }); }); @@ -186,9 +189,9 @@ describe('defineTheme partial per-mode merges', () => { ); } // The explicit light info override changed the light info kit and did not bleed into dark. - const overriddenLight = infoVarNames.map((varName) => - extractValue(overridden.baseLight, varName), - ); + const overriddenLight = infoVarNames.map((varName) => { + return extractValue(overridden.baseLight, varName); + }); const defaultLight = infoVarNames.map((varName) => extractValue(allDefault.baseLight, varName)); expect(overriddenLight).not.toEqual(defaultLight); }); @@ -198,12 +201,12 @@ describe('defineTheme scrim validation', () => { it('rejects an unsafe authored scrim value with a message naming the field', () => { // The scrim is deliberately excluded from OKLCH colour parsing (its alpha channel does not fit // that pattern) and emitted verbatim, so it needs its own shape check rather than none at all. - expect(() => - defineTheme({ + expect(() => { + return defineTheme({ color: { accent: '#3b82f6', scrim: 'oklch(0 0 0 / 0.2); } .evil {' }, name: 'unsafe-scrim', - }), - ).toThrow('color.scrim: must be a non-empty CSS colour value'); + }); + }).toThrow('color.scrim: must be a non-empty CSS colour value'); }); }); diff --git a/packages/@luke-ui/react/src/theme/define-theme.ts b/packages/@luke-ui/react/src/theme/define-theme.ts index d56366b1..ed7c0bef 100644 --- a/packages/@luke-ui/react/src/theme/define-theme.ts +++ b/packages/@luke-ui/react/src/theme/define-theme.ts @@ -24,26 +24,26 @@ export type ColorInput = string | { light?: string; dark?: string }; /** A composite `box-shadow` ladder for one colour mode, rung by rung. */ export interface DepthLadder { - /** Inset treatment for a pressed control or sunken surface. */ - recessed: string; - /** Resting treatment for an interactive control or surface. */ - resting: string; - /** Treatment for a hovered control or elevated surface. */ - raised: string; /** Treatment for a floating surface such as a menu. */ floating: string; /** Treatment for a high-elevation surface such as a dialog. */ overlay: string; + /** Treatment for a hovered control or elevated surface. */ + raised: string; + /** Inset treatment for a pressed control or sunken surface. */ + recessed: string; + /** Resting treatment for an interactive control or surface. */ + resting: string; } /** A Button/IconButton `background-image` face-finish ladder for one colour mode. */ export interface ControlFinish { + /** Face lighting for a hovered control. */ + raised: string; /** Face lighting for a pressed control. */ recessed: string; /** Face lighting for a resting control. */ resting: string; - /** Face lighting for a hovered control. */ - raised: string; } /** @@ -51,21 +51,21 @@ export interface ControlFinish { * authors its own accent and one that extends another theme. */ interface ThemeInputCommon { + /** + * Button/IconButton face finish, per mode. Optional and deep-partial: an omitted rung falls back + * to `'none'` (a flat control). + */ + actionControlFinish?: { light?: Partial; dark?: Partial }; + /** + * Composite `box-shadow` depth ladder, per mode. Optional and deep-partial: an omitted rung + * falls back to the curated extremely-subtle default for that mode. + */ + depth?: { light?: Partial; dark?: Partial }; /** * Kebab-case theme identity, for example `'tactile'`. The theme's identity class is * `luke-ui-theme-${name}`. */ name: string; - /** Typography — family and weights only. The type scale is source-owned (not authored here). */ - typography?: { - /** - * Curated Capsize-compatible font-family choice. - * @default 'inter' - */ - fontFamily?: 'inter' | 'apple-system' | 'dm-sans'; - /** Font weights for the four theme-controlled weight roles. */ - fontWeight?: { body?: number; label?: number; heading?: number; emphasis?: number }; - }; /** Corner radii. A generative base + multiplier scale, with explicit per-step overrides. */ radius?: { /** @@ -89,16 +89,16 @@ interface ThemeInputCommon { overlay?: number; // radius.full is fixed at 9999px and is not authored. }; - /** - * Composite `box-shadow` depth ladder, per mode. Optional and deep-partial: an omitted rung - * falls back to the curated extremely-subtle default for that mode. - */ - depth?: { light?: Partial; dark?: Partial }; - /** - * Button/IconButton face finish, per mode. Optional and deep-partial: an omitted rung falls back - * to `'none'` (a flat control). - */ - actionControlFinish?: { light?: Partial; dark?: Partial }; + /** Typography — family and weights only. The type scale is source-owned (not authored here). */ + typography?: { + /** + * Curated Capsize-compatible font-family choice. + * @default 'inter' + */ + fontFamily?: 'inter' | 'apple-system' | 'dm-sans'; + /** Font weights for the four theme-controlled weight roles. */ + fontWeight?: { body?: number; label?: number; heading?: number; emphasis?: number }; + }; } /** @@ -151,10 +151,10 @@ export interface ThemeInput extends ThemeInputCommon { * base. */ export interface ExtendingThemeInput extends ThemeInputCommon { - /** The theme to start from. */ - extends: ThemeInput | ExtendingThemeInput; /** Source-colour overrides. Every role the theme leaves out comes from the base. */ color?: Partial; + /** The theme to start from. */ + extends: ThemeInput | ExtendingThemeInput; } type ColorMode = 'light' | 'dark'; @@ -291,13 +291,13 @@ function resolveColors(input: ThemeInput, mode: ColorMode): ThemeSourceColors { const neutral = resolveNeutral(color, mode); const colors: ThemeSourceColors = { accent: resolveAdaptedRole(color.accent, mode, adaptAccent), - neutral, // The canvas anchor, split from `neutral`'s hue/chroma character: explicit per-mode value wins, // a single value or the opposite side is adapted to the mode canvas lightness, and an entirely // omitted `background` copies the resolved neutral canvas anchor exactly (not a second, // independent adaptation of the neutral source). `buildModeColors` takes this resolved value // directly as the canvas anchor for every family's ramp and the elevation surfaces. background: resolveOptionalModeColour(color.background, mode, neutral), + neutral, // Emitted verbatim; a single string applies to both modes, an omitted side falls back to the // curated mode-aware default. scrim: resolveVerbatimRole(color.scrim, mode, defaultScrim[mode]), @@ -348,12 +348,24 @@ function resolveNeutral(color: ThemeInput['color'], mode: ColorMode): string { if (other !== undefined) return adaptNeutralString(gamutMapOklch(parseColor(other)), mode); } const style = NEUTRAL_STYLE[color.neutralStyle ?? 'neutral']; - return formatOklch(gamutMapOklch({ l: NEUTRAL_LIGHTNESS[mode], c: style.chroma, h: style.hue })); + return formatOklch( + gamutMapOklch({ + l: NEUTRAL_LIGHTNESS[mode], + c: style.chroma, + h: style.hue, + }), + ); } /** Adapts a single neutral source to the mode canvas lightness, preserving hue and chroma. */ function adaptNeutralString(source: Oklch, mode: ColorMode): string { - return formatOklch(gamutMapOklch({ l: NEUTRAL_LIGHTNESS[mode], c: source.c, h: source.h })); + return formatOklch( + gamutMapOklch({ + l: NEUTRAL_LIGHTNESS[mode], + c: source.c, + h: source.h, + }), + ); } /** @@ -411,7 +423,13 @@ function sideOf(input: ColorInput, mode: ColorMode): string | undefined { function adaptAccent(source: Oklch, mode: ColorMode, raw: string): Oklch { const target = ACCENT_TARGET[mode]; const [low, high] = ACCENT_BAND[mode]; - const makeSolid = (l: number) => gamutMapOklch({ l, c: source.c, h: source.h }); + const makeSolid = (l: number) => { + return gamutMapOklch({ + l, + c: source.c, + h: source.h, + }); + }; const passes = (l: number) => passesOnSolidGate({ lightness: l, mode, source }); if (passes(target)) return makeSolid(target); diff --git a/packages/@luke-ui/react/src/theme/diagnostics.ts b/packages/@luke-ui/react/src/theme/diagnostics.ts index 7e42c31f..d0539529 100644 --- a/packages/@luke-ui/react/src/theme/diagnostics.ts +++ b/packages/@luke-ui/react/src/theme/diagnostics.ts @@ -12,30 +12,30 @@ import type { FamilyRequirements, FamilyRole, ScaleFamily, ScaleStep } from './s /** A chroma reduction forced by sRGB gamut mapping on one generated rung. */ export interface GamutReduction { - /** The step whose chroma was reduced. */ - step: ScaleStep; /** The chroma the generator requested before gamut mapping. */ requestedChroma: number; /** The chroma left after mapping the colour into the sRGB gamut. */ resolvedChroma: number; + /** The step whose chroma was reduced. */ + step: ScaleStep; } /** How the step-9 solid anchor was resolved for a family. */ export interface SolidAnchorDiagnostics { - /** The lightness the search preferred: the source lightness (vibrant) or the curated target (neutral). */ - targetLightness: number; - /** The lightness the solid anchor resolved to. */ - resolvedLightness: number; - /** The lightness range the solid-anchor search was allowed to explore. */ - band: [number, number]; /** Whether the anchor was moved off its preferred lightness to satisfy the on-solid gate. */ adaptedForOnSolid: boolean; + /** The lightness range the solid-anchor search was allowed to explore. */ + band: [number, number]; /** The on-solid contrast achieved against the solid (step 9). */ onSolidRatioSolid: number; /** The on-solid contrast achieved against the solid hover (step 10). */ onSolidRatioSolidHover: number; + /** The lightness the solid anchor resolved to. */ + resolvedLightness: number; /** Whether the family satisfies its on-solid guarantee (always true when the role does not need one). */ satisfied: boolean; + /** The lightness the search preferred: the source lightness (vibrant) or the curated target (neutral). */ + targetLightness: number; } /** @@ -43,24 +43,24 @@ export interface SolidAnchorDiagnostics { * scale, the resolved solid anchor, the chosen on-solid colour, and any gamut-driven reductions. */ export interface FamilyDiagnostics { - /** The semantic role the family was generated for. */ - role: FamilyRole; - /** The colour mode the family was generated for. */ - mode: 'light' | 'dark'; - /** The capability guarantees the role declares. */ - requirements: FamilyRequirements; - /** The family character the generator was given. */ - source: Oklch; /** The canvas anchor the generator was given. */ background: Oklch; /** The generated 12-step family plus its on-solid colour. */ family: ScaleFamily; - /** How the step-9 solid anchor was resolved. */ - solidAnchor: SolidAnchorDiagnostics; - /** The chosen on-solid colour and the contrast it reaches over the solid and its hover. */ - onSolid: { color: Oklch; ratioSolid: number; ratioSolidHover: number }; /** Every rung whose chroma the sRGB gamut forced down. */ gamutReductions: Array; + /** The colour mode the family was generated for. */ + mode: 'light' | 'dark'; + /** The chosen on-solid colour and the contrast it reaches over the solid and its hover. */ + onSolid: { color: Oklch; ratioSolid: number; ratioSolidHover: number }; + /** The capability guarantees the role declares. */ + requirements: FamilyRequirements; + /** The semantic role the family was generated for. */ + role: FamilyRole; + /** How the step-9 solid anchor was resolved. */ + solidAnchor: SolidAnchorDiagnostics; + /** The family character the generator was given. */ + source: Oklch; } /** @@ -69,34 +69,34 @@ export interface FamilyDiagnostics { * gate. Tooling reads that classification instead of inferring it from token paths. */ export interface ContrastCheck { - /** Token path of the foreground colour, for example `color.text.primary`. */ - foreground: string; /** Token path of the background colour, for example `color.surface.floating`. */ background: string; - /** The WCAG 2.2 contrast ratio the pair achieves. */ - ratio: number; - /** The ratio the pair is measured against (4.5 for text, 3 for non-text UI). */ - required: number; - /** Whether the achieved ratio clears the required minimum. */ - passes: boolean; + /** Token path of the foreground colour, for example `color.text.primary`. */ + foreground: string; /** * Whether missing `required` fails the build: `true` for a hard gate that contributes to a thrown * {@link import('./build-theme.js').ThemeContrastError}, `false` for an advisory check that is * measured and reported only. */ hard: boolean; + /** Whether the achieved ratio clears the required minimum. */ + passes: boolean; + /** The WCAG 2.2 contrast ratio the pair achieves. */ + ratio: number; + /** The ratio the pair is measured against (4.5 for text, 3 for non-text UI). */ + required: number; } /** Everything the compiler resolved for one colour mode of a fully compiled theme. */ export interface ThemeModeDiagnostics { - /** The colour mode the diagnostics describe. */ - mode: 'light' | 'dark'; + /** Every WCAG pair the semantic validation matrix checked for the mode. */ + contrastChecks: Array; /** The per-role scale families the mode was generated from. */ families: Record; + /** The colour mode the diagnostics describe. */ + mode: 'light' | 'dark'; /** The mode-aware elevation surfaces the canvas anchor produced. */ surfaces: GeneratedSurfaces; - /** Every WCAG pair the semantic validation matrix checked for the mode. */ - contrastChecks: Array; } /** @@ -105,10 +105,10 @@ export interface ThemeModeDiagnostics { * alongside the emitted CSS. */ export interface ThemeDiagnostics { - /** The light mode's complete diagnostics. */ - light: ThemeModeDiagnostics; /** The dark mode's complete diagnostics. */ dark: ThemeModeDiagnostics; + /** The light mode's complete diagnostics. */ + light: ThemeModeDiagnostics; } /** @@ -117,10 +117,10 @@ export interface ThemeDiagnostics { * the failing role threw. Attached to {@link import('./build-theme.js').ThemeGenerationError}. */ export interface ThemeGenerationDiagnostics { + /** The families successfully generated for the failing mode before the failure. */ + completedFamilies: Partial>; /** The mode being compiled when generation failed. */ mode: 'light' | 'dark'; /** The role whose family could not be generated. */ role: FamilyRole; - /** The families successfully generated for the failing mode before the failure. */ - completedFamilies: Partial>; } diff --git a/packages/@luke-ui/react/src/theme/elevation.ts b/packages/@luke-ui/react/src/theme/elevation.ts index 751b52d4..89fae9fb 100644 --- a/packages/@luke-ui/react/src/theme/elevation.ts +++ b/packages/@luke-ui/react/src/theme/elevation.ts @@ -11,12 +11,12 @@ type ElevationMode = 'light' | 'dark'; export interface GeneratedSurfaces { /** The page canvas. Always equal to the `background` input — canvas IS the background. */ canvas: Oklch; - /** A well recessed below the canvas (dark: darker than canvas; light: neutral white). */ - recessed: Oklch; /** A surface that lifts off the canvas, such as a card or menu. */ floating: Oklch; /** The most separated surface, such as a modal or popover. */ overlay: Oklch; + /** A well recessed below the canvas (dark: darker than canvas; light: neutral white). */ + recessed: Oklch; } /** Input to {@link generateSurfaces}. */ @@ -31,7 +31,11 @@ export interface GenerateSurfacesRequest { // canvas, and detached surfaces separate more strongly without exposing generated palette steps. // This module owns the surface deltas outright — `build-theme.ts` consumes `generateSurfaces` rather // than re-deriving them, so there is a single source of truth for the elevation model. -const LIGHT_RECESSED_SURFACE = { l: 1, c: 0, h: 0 } as const satisfies Oklch; +const LIGHT_RECESSED_SURFACE = { + l: 1, + c: 0, + h: 0, +} as const satisfies Oklch; const DARK_RECESSED_SURFACE_LIGHTNESS_DELTA = -0.025; const SURFACE_LIGHTNESS_DELTAS = { dark: { floating: 0.07, overlay: 0.09 }, diff --git a/packages/@luke-ui/react/src/theme/extend-theme.test.ts b/packages/@luke-ui/react/src/theme/extend-theme.test.ts index 5e847cec..8ea0629a 100644 --- a/packages/@luke-ui/react/src/theme/extend-theme.test.ts +++ b/packages/@luke-ui/react/src/theme/extend-theme.test.ts @@ -30,10 +30,12 @@ describe('theme inheritance', () => { extends: tactileTheme, name: 'product', }); - const withoutAccent = (css: string) => - declarations(css).filter(([name]) => !name.includes('accent')); - const accentOnly = (css: string) => - declarations(css).filter(([name]) => name.includes('accent')); + const withoutAccent = (css: string) => { + return declarations(css).filter(([name]) => !name.includes('accent')); + }; + const accentOnly = (css: string) => { + return declarations(css).filter(([name]) => name.includes('accent')); + }; expect(withoutAccent(subject)).toEqual(withoutAccent(reference)); expect(accentOnly(subject)).not.toEqual(accentOnly(reference)); diff --git a/packages/@luke-ui/react/src/theme/extend-theme.ts b/packages/@luke-ui/react/src/theme/extend-theme.ts index 048033c0..3ae03b5d 100644 --- a/packages/@luke-ui/react/src/theme/extend-theme.ts +++ b/packages/@luke-ui/react/src/theme/extend-theme.ts @@ -19,10 +19,10 @@ export interface ThemeInheritance { /** A theme input with its `extends` chain already resolved. */ export interface ResolvedThemeInput { - /** The merged input, with no `extends` left to resolve. */ - input: ThemeInput; /** The colour provenance, or `null` when the theme extends nothing. */ inheritance: ThemeInheritance | null; + /** The merged input, with no `extends` left to resolve. */ + input: ThemeInput; } /** Colour roles in `ThemeInput['color']` declaration order, for a stable provenance report. */ diff --git a/packages/@luke-ui/react/src/theme/foundation.ts b/packages/@luke-ui/react/src/theme/foundation.ts index 5dacff3e..70da79df 100644 --- a/packages/@luke-ui/react/src/theme/foundation.ts +++ b/packages/@luke-ui/react/src/theme/foundation.ts @@ -9,17 +9,40 @@ */ export interface ThemeFoundation { /** - * Kebab-case theme identity, for example `'tactile'`. The theme's identity class is - * `luke-ui-theme-${name}`. + * The dark colour-mode foundation. Dark is authored independently and is never derived from + * light. */ - name: string; + dark: ThemeModeFoundation; /** The light colour-mode foundation. */ light: ThemeModeFoundation; /** - * The dark colour-mode foundation. Dark is authored independently and is never derived from - * light. + * Kebab-case theme identity, for example `'tactile'`. The theme's identity class is + * `luke-ui-theme-${name}`. */ - dark: ThemeModeFoundation; + name: string; + /** Corner radii in pixels, shared by both modes. `radius.full` is fixed at 9999px. */ + radius?: { + /** + * Radius for checkbox boxes, tags, badges, and compact details. + * @default 4 + */ + detail?: number; + /** + * Radius for buttons, fields, selects, and other controls. + * @default 8 + */ + control?: number; + /** + * Radius for cards, popovers, and menus. + * @default 12 + */ + surface?: number; + /** + * Radius for dialogs, sheets, and large overlays. + * @default 16 + */ + overlay?: number; + }; /** Typography choices shared by both modes. */ typography?: { /** @@ -52,29 +75,6 @@ export interface ThemeFoundation { emphasis?: number; }; }; - /** Corner radii in pixels, shared by both modes. `radius.full` is fixed at 9999px. */ - radius?: { - /** - * Radius for checkbox boxes, tags, badges, and compact details. - * @default 4 - */ - detail?: number; - /** - * Radius for buttons, fields, selects, and other controls. - * @default 8 - */ - control?: number; - /** - * Radius for cards, popovers, and menus. - * @default 12 - */ - surface?: number; - /** - * Radius for dialogs, sheets, and large overlays. - * @default 16 - */ - overlay?: number; - }; } /** The per-mode authored inputs: source colours, action-control finish, and depth treatments. */ @@ -89,12 +89,12 @@ export interface ThemeModeFoundation { /** Authored action-control face lighting for one colour mode. */ interface ActionControlFinishFoundation { + /** Face lighting for a hovered control. */ + raised: string; /** Face lighting for a pressed control. */ recessed: string; /** Face lighting for a resting control. */ resting: string; - /** Face lighting for a hovered control. */ - raised: string; } /** @@ -102,12 +102,8 @@ interface ActionControlFinishFoundation { * lightness as a 0-1 number or a percentage, and no alpha channel. */ export interface ThemeSourceColors { - /** - * Required. Anchors the surface, text, and border ramps — the family's hue/chroma character. - * `background` is the actual canvas colour; the two coincide unless `background` is authored - * separately. - */ - neutral: string; + /** Required. The brand or interaction accent colour. */ + accent: string; /** * Required. The canvas anchor, resolved per mode from an explicit `background`, an adapted * opposite-mode `background`, or (when `background` is entirely omitted) a copy of the resolved @@ -116,14 +112,6 @@ export interface ThemeSourceColors { * against `background`, not `neutral`. */ background: string; - /** Required. The brand or interaction accent colour. */ - accent: string; - /** Source colour for the `info` role. Defaults to an accessible Luke UI blue for the mode. */ - info?: string; - /** Source colour for the `success` role. Defaults to an accessible Luke UI green for the mode. */ - success?: string; - /** Source colour for the `warning` role. Defaults to an accessible Luke UI amber for the mode. */ - warning?: string; /** Source colour for the `danger` role. Defaults to an accessible Luke UI red for the mode. */ danger?: string; /** @@ -131,11 +119,23 @@ export interface ThemeSourceColors { * Luke UI blue for the mode. */ focus?: string; + /** Source colour for the `info` role. Defaults to an accessible Luke UI blue for the mode. */ + info?: string; + /** + * Required. Anchors the surface, text, and border ramps — the family's hue/chroma character. + * `background` is the actual canvas colour; the two coincide unless `background` is authored + * separately. + */ + neutral: string; /** * Modal-backdrop dimming colour, emitted verbatim (may carry an alpha channel). Required * internally: `defineTheme` always resolves it, from the author's value or a mode-aware default. */ scrim: string; + /** Source colour for the `success` role. Defaults to an accessible Luke UI green for the mode. */ + success?: string; + /** Source colour for the `warning` role. Defaults to an accessible Luke UI amber for the mode. */ + warning?: string; } /** @@ -157,16 +157,16 @@ export const SOURCE_COLOR_FIELDS = [ /** Authored composite `box-shadow` values for one colour mode. */ interface ThemeDepthFoundation { - /** Inset treatment for a pressed control or sunken surface. */ - recessed: string; - /** Resting treatment for an interactive control or surface. */ - resting: string; - /** Treatment for a hovered control or elevated surface. */ - raised: string; /** Treatment for a floating surface such as a menu. */ floating: string; /** Treatment for a high-elevation surface such as a dialog. */ overlay: string; + /** Treatment for a hovered control or elevated surface. */ + raised: string; + /** Inset treatment for a pressed control or sunken surface. */ + recessed: string; + /** Resting treatment for an interactive control or surface. */ + resting: string; } /** Curated Capsize-compatible font stacks for each font-family choice. */ diff --git a/packages/@luke-ui/react/src/theme/scale.test.ts b/packages/@luke-ui/react/src/theme/scale.test.ts index ca497b8b..6fca838a 100644 --- a/packages/@luke-ui/react/src/theme/scale.test.ts +++ b/packages/@luke-ui/react/src/theme/scale.test.ts @@ -312,7 +312,11 @@ describe('the one on-solid gate', () => { for (const hue of [0, 100, 210, 300]) { for (const chroma of [0.01, 0.1, 0.2]) { for (let lightness = 0.3; lightness <= 0.92 + 1e-9; lightness += 0.01) { - const source: Oklch = { c: chroma, h: hue, l: lightness }; + const source: Oklch = { + l: lightness, + c: chroma, + h: hue, + }; const anchor = resolveAnchor(source, mode); const honoured = anchor !== null && @@ -334,7 +338,11 @@ describe('the one on-solid gate', () => { it('solves past the AA text ratio, so a pair that only just clears 4.5:1 does not pass', () => { // Light `oklch(0.5575 0.01 0)` reaches 4.53:1 across its solid and hover: enough for a plain 4.5 // check, short of the headroom the gate solves for so 4-decimal emission cannot round it under. - const source: Oklch = { c: 0.01, h: 0, l: 0.5575 }; + const source: Oklch = { + l: 0.5575, + c: 0.01, + h: 0, + }; const ratio = onSolidGateRatio({ lightness: source.l, mode: 'light', source }); expect(ratio).toBeGreaterThan(TEXT_RATIO); expect(ratio).toBeLessThan(TEXT_RATIO + 0.05); @@ -347,8 +355,16 @@ describe('the one on-solid gate', () => { // Light `oklch(0.64 0 0)` clears 4.58:1 across the two states the engine emits. A phantom third // state 0.09 darker would drag it to 3.88:1 and fail. The pressed solid reuses step 10, so no // such colour exists and the gate must not invent one. - const source: Oklch = { c: 0, h: 0, l: 0.64 }; - const phantomPressed = { c: 0, h: 0, l: source.l - 0.09 }; + const source: Oklch = { + l: 0.64, + c: 0, + h: 0, + }; + const phantomPressed = { + l: source.l - 0.09, + c: 0, + h: 0, + }; const onSolid = family('oklch(0.64 0 0)', 'light', 'accent').contrast; expect(contrastRatio(onSolid, phantomPressed)).toBeLessThan(TEXT_RATIO); expect(onSolidGateRatio({ lightness: source.l, mode: 'light', source })).toBeGreaterThan( diff --git a/packages/@luke-ui/react/src/theme/scale.ts b/packages/@luke-ui/react/src/theme/scale.ts index 056031e7..23c3fcc5 100644 --- a/packages/@luke-ui/react/src/theme/scale.ts +++ b/packages/@luke-ui/react/src/theme/scale.ts @@ -30,16 +30,16 @@ type ColorMode = 'light' | 'dark'; * actually consumes (not by hue). Named for capabilities so the flags do not leak token names. */ export interface FamilyRequirements { - /** Steps 3-5: component surface trio (normal / hover / active). */ - needsSubtleStates: boolean; - /** Steps 9-10: solid surface and its hover. */ - needsSolidStates: boolean; + /** Step 7: UI border and focus ring. */ + needsBorder: boolean; /** `contrast` clears WCAG AA text contrast against steps 9 and 10. */ needsOnSolid: boolean; + /** Steps 9-10: solid surface and its hover. */ + needsSolidStates: boolean; + /** Steps 3-5: component surface trio (normal / hover / active). */ + needsSubtleStates: boolean; /** Step 11 low-contrast text (neutral additionally uses 12 / secondary / disabled). */ needsText: boolean; - /** Step 7: UI border and focus ring. */ - needsBorder: boolean; } /** @@ -92,14 +92,14 @@ export interface ScaleFamily { /** The inputs to {@link generateFamily}. */ export interface GenerateFamilyRequest { - /** The family's hue/chroma character. Its lightness anchors vibrant solids only. */ - source: Oklch; /** The resolved canvas anchor. Steps 1-8 ramp away from it toward the solid. */ background: Oklch; /** The colour mode the family is generated for. */ mode: ColorMode; /** The semantic role, which selects the capability guarantees. */ role: FamilyRole; + /** The family's hue/chroma character. Its lightness anchors vibrant solids only. */ + source: Oklch; } /** @@ -156,9 +156,9 @@ export const MIN_STATE_DELTA = 0.015; // `chromaFraction` scales the source chroma and `chromaCap` caps it so the pale near-background // steps stay tinted rather than saturated. interface RampRungSpec { - offset: number; - chromaFraction: number; chromaCap: number; + chromaFraction: number; + offset: number; } const RAMP_SPEC = { dark: [ @@ -189,10 +189,10 @@ const RAMP_SPEC = { const SOLID_HOVER_DELTA = 0.05; interface SolidBand { - /** The lightness the search prefers when it clears the gate. */ - target: number; /** The inclusive lightness range the search may explore. */ band: [number, number]; + /** The lightness the search prefers when it clears the gate. */ + target: number; } // A vibrant solid (accent / danger) stays faithful to its authored lightness: the search explores a @@ -229,12 +229,12 @@ const ON_SOLID_BLACK_CHROMA = 0.01; /** A candidate solid the on-solid gate is asked about. */ export interface OnSolidGateRequest { - /** The family character. Only its hue and chroma are read; `lightness` supplies the tone. */ - source: Oklch; /** The candidate step-9 solid lightness. */ lightness: number; /** The colour mode, which sets the direction step 10 (hover) moves in. */ mode: ColorMode; + /** The family character. Only its hue and chroma are read; `lightness` supplies the tone. */ + source: Oklch; } /** @@ -259,7 +259,11 @@ export function passesOnSolidGate(request: OnSolidGateRequest): boolean { export function onSolidGateRatio(request: OnSolidGateRequest): number { const { lightness, mode, source } = request; const direction = mode === 'light' ? -1 : 1; - const solid = gamutMapOklch({ c: source.c, h: source.h, l: clampUnit(lightness) }); + const solid = gamutMapOklch({ + l: clampUnit(lightness), + c: source.c, + h: source.h, + }); const hover = gamutMapOklch({ c: source.c, h: source.h, @@ -406,10 +410,10 @@ function buildFamily(request: GenerateFamilyRequest): { } interface ResolvedAnchor { + adapted: boolean; + band: [number, number]; lightness: number; target: number; - band: [number, number]; - adapted: boolean; } /** @@ -471,7 +475,11 @@ function resolveSolidAnchor( } } if (best === null) { - const solid = gamutMapOklch({ c: source.c, h: source.h, l: clampUnit(bestAttemptLightness) }); + const solid = gamutMapOklch({ + l: clampUnit(bestAttemptLightness), + c: source.c, + h: source.h, + }); throw new ScaleGenerationError(role, mode, { lightness: bestAttemptLightness, onSolidRatio: bestAttemptRatio, @@ -487,11 +495,15 @@ function resolveSolidAnchor( * higher minimum contrast across the given solids. */ function chooseOnSolid(hue: number, solids: Array): { color: Oklch; minRatio: number } { - const nearWhite = gamutMapOklch({ c: 0, h: hue, l: ON_SOLID_WHITE_LIGHTNESS }); + const nearWhite = gamutMapOklch({ + l: ON_SOLID_WHITE_LIGHTNESS, + c: 0, + h: hue, + }); const nearBlack = gamutMapOklch({ + l: ON_SOLID_BLACK_LIGHTNESS, c: ON_SOLID_BLACK_CHROMA, h: hue, - l: ON_SOLID_BLACK_LIGHTNESS, }); const whiteMinimum = minimumRatio(nearWhite, solids); const blackMinimum = minimumRatio(nearBlack, solids); diff --git a/packages/@luke-ui/react/src/theme/semantic-map.ts b/packages/@luke-ui/react/src/theme/semantic-map.ts index 8ec5b29c..95d17cef 100644 --- a/packages/@luke-ui/react/src/theme/semantic-map.ts +++ b/packages/@luke-ui/react/src/theme/semantic-map.ts @@ -24,22 +24,22 @@ export type SemanticColorValues = Record; /** The inputs to {@link mapSemanticColors}. */ interface MapSemanticColorsRequest { - /** The generated scale family for each role, already resolved for `mode`. */ - families: Record; - /** The generated elevation surface set, already resolved for `mode`. */ - surfaces: GeneratedSurfaces; /** * `color.border.control`'s solved value is a dedicated contrast boundary, not a scale-step alias. * `control-border.ts`'s `solveControlBorder` resolves it against `surfaces.canvas` and * `surfaces.recessed` before this map runs, then this function passes it through verbatim. */ controlBorder: Oklch; - /** The authored scrim value, passed through verbatim (it may carry an alpha channel). */ - scrim: string; + /** The generated scale family for each role, already resolved for `mode`. */ + families: Record; /** The authored keyboard-focus source colour. Defaults to the accent family's step 8. */ focus?: Oklch; /** The colour mode the families and surfaces were resolved for. */ mode: ColorMode; + /** The authored scrim value, passed through verbatim (it may carry an alpha channel). */ + scrim: string; + /** The generated elevation surface set, already resolved for `mode`. */ + surfaces: GeneratedSurfaces; } /** diff --git a/packages/@luke-ui/react/src/theme/theme-diagnostics-inspector.tsx b/packages/@luke-ui/react/src/theme/theme-diagnostics-inspector.tsx index 7cdac3b6..970d9afe 100644 --- a/packages/@luke-ui/react/src/theme/theme-diagnostics-inspector.tsx +++ b/packages/@luke-ui/react/src/theme/theme-diagnostics-inspector.tsx @@ -4,8 +4,8 @@ import { compileTheme } from './build-theme.js'; import type { Oklch } from './color.js'; import { formatOklch } from './color.js'; import { vars } from './contract.css.js'; -import { normalizeTheme } from './define-theme.js'; import type { ThemeInput } from './define-theme.js'; +import { normalizeTheme } from './define-theme.js'; import type { ContrastCheck, FamilyDiagnostics, @@ -330,9 +330,9 @@ function SolidAnchorSection({ families }: { families: Record{role}
- + @@ -411,9 +411,13 @@ function ContrastCheckTable({ } function GamutReductionsSection({ families }: { families: Record }) { - const rows = FAMILY_ROLES.flatMap((role) => - families[role].gamutReductions.map((reduction, index) => ({ index, role, ...reduction })), - ); + const rows = FAMILY_ROLES.flatMap((role) => { + return families[role].gamutReductions.map((reduction, index) => ({ + index, + role, + ...reduction, + })); + }); return ( {rows.length === 0 ? ( diff --git a/packages/@luke-ui/react/src/theme/token-board.tsx b/packages/@luke-ui/react/src/theme/token-board.tsx index 5f13873c..6c00c0cb 100644 --- a/packages/@luke-ui/react/src/theme/token-board.tsx +++ b/packages/@luke-ui/react/src/theme/token-board.tsx @@ -11,8 +11,8 @@ import type { CSSProperties, ReactNode } from 'react'; import { createElement, useLayoutEffect, useRef, useState } from 'react'; import { vars } from './contract.css.js'; -import { flattenThemeContract, themeVarName } from './contract.js'; import type { themeContractTree } from './contract.js'; +import { flattenThemeContract, themeVarName } from './contract.js'; interface TokenLeafNode { kind: 'leaf'; @@ -21,8 +21,8 @@ interface TokenLeafNode { } export interface TokenGroupNode { - kind: 'group'; children: Record; + kind: 'group'; } export type TokenTreeNode = TokenLeafNode | TokenGroupNode; diff --git a/packages/@luke-ui/react/src/themes/themes.test.ts b/packages/@luke-ui/react/src/themes/themes.test.ts index b3028a83..e35af80e 100644 --- a/packages/@luke-ui/react/src/themes/themes.test.ts +++ b/packages/@luke-ui/react/src/themes/themes.test.ts @@ -2,10 +2,10 @@ import { readFile } from 'node:fs/promises'; import { describe, expect, it } from 'vite-plus/test'; import packageJson from '../../package.json' with { type: 'json' }; import { getThemeClassName } from '../theme/theme-class-name.js'; -import { theme as paperThemeInput, themeClassName as paperThemeClassName } from './paper/index.js'; +import { themeClassName as paperThemeClassName, theme as paperThemeInput } from './paper/index.js'; import { - theme as tactileThemeInput, themeClassName as tactileThemeClassName, + theme as tactileThemeInput, } from './tactile/index.js'; const themeArtifacts = { diff --git a/packages/@luke-ui/react/vitest.config.ts b/packages/@luke-ui/react/vitest.config.ts index aaaa887c..215c0603 100644 --- a/packages/@luke-ui/react/vitest.config.ts +++ b/packages/@luke-ui/react/vitest.config.ts @@ -97,8 +97,8 @@ export default defineConfig({ ], test: { browser: { - enabled: true, api: { allowWrite: true }, + enabled: true, expect: { toMatchScreenshot: { // Tall scenes are handled in captureVisual, which grows both diff --git a/packages/turbo-generators/config.ts b/packages/turbo-generators/config.ts index 9bc493ae..0bf37bca 100644 --- a/packages/turbo-generators/config.ts +++ b/packages/turbo-generators/config.ts @@ -12,12 +12,12 @@ const CONFORMANCE_TIERS = ['universal', 'field-shaped', 'none'] as const; const DOC_GROUPS = ['actions', 'feedback', 'forms', 'typography', 'visuals'] as const; const componentAnswersSchema = z.object({ + conformanceTier: z.enum(CONFORMANCE_TIERS).default('universal'), docsGroup: z.enum(DOC_GROUPS), + integrationTripwire: z.boolean().default(false), name: z.string().min(1), styling: z.enum(COMPONENT_STYLING), tier: z.enum(COMPONENT_TIERS), - conformanceTier: z.enum(CONFORMANCE_TIERS).default('universal'), - integrationTripwire: z.boolean().default(false), visualCoverage: z.boolean().default(true), }); diff --git a/packages/turbo-generators/src/apply-component-creation-plan.test.ts b/packages/turbo-generators/src/apply-component-creation-plan.test.ts index e9025698..5c33dd20 100644 --- a/packages/turbo-generators/src/apply-component-creation-plan.test.ts +++ b/packages/turbo-generators/src/apply-component-creation-plan.test.ts @@ -20,10 +20,10 @@ describe('applyComponentCreationPlan', () => { const plan: ComponentCreationPlan = { expected: { + exampleSlug: 'status-badge/basic', hostedDocsPath: 'components/feedback/status-badge', packageDocsSlug: 'status-badge', packageExportPath: './status-badge', - exampleSlug: 'status-badge/basic', }, files: [ { @@ -86,21 +86,21 @@ describe('applyComponentCreationPlan', () => { const plan: ComponentCreationPlan = { expected: { + exampleSlug: 'status-badge/basic', hostedDocsPath: 'components/actions/status-badge', packageDocsSlug: 'status-badge', packageExportPath: './status-badge', - exampleSlug: 'status-badge/basic', }, files: [], jsonEdits: [], textFileAppends: [ { kind: 'text-append', - path: barrelPath, lines: [ "export type { StatusBadgeVariants } from '../recipes/status-badge.css.js';", "export { statusBadge } from '../recipes/status-badge.css.js';", ], + path: barrelPath, }, ], }; @@ -135,10 +135,10 @@ describe('applyComponentCreationPlan', () => { const plan: ComponentCreationPlan = { expected: { + exampleSlug: 'status-badge/basic', hostedDocsPath: 'components/feedback/status-badge', packageDocsSlug: 'status-badge', packageExportPath: './status-badge', - exampleSlug: 'status-badge/basic', }, files: [], jsonEdits: [], @@ -171,10 +171,10 @@ describe('applyComponentCreationPlan', () => { const plan: ComponentCreationPlan = { expected: { + exampleSlug: 'status-badge/basic', hostedDocsPath: 'components/feedback/status-badge', packageDocsSlug: 'status-badge', packageExportPath: './status-badge', - exampleSlug: 'status-badge/basic', }, files: [], jsonEdits: [ diff --git a/packages/turbo-generators/src/component-creation-plan.test.ts b/packages/turbo-generators/src/component-creation-plan.test.ts index 404c16a4..abd0f51a 100644 --- a/packages/turbo-generators/src/component-creation-plan.test.ts +++ b/packages/turbo-generators/src/component-creation-plan.test.ts @@ -61,11 +61,11 @@ describe('createComponentPlan', () => { expect(plan.textFileAppends).toEqual([ { kind: 'text-append', - path: 'packages/@luke-ui/react/src/recipes/index.ts', lines: [ "export type { StatusBadgeVariants } from '../recipes/status-badge.css.js';", "export { statusBadge } from '../recipes/status-badge.css.js';", ], + path: 'packages/@luke-ui/react/src/recipes/index.ts', }, ]); expect(plan.textFileInserts).toEqual([ @@ -139,12 +139,12 @@ describe('createComponentPlan', () => { it('uses explicit applicability overrides in the manifest entry', () => { const plan = createComponentPlan({ + conformanceTier: 'field-shaped', docsGroup: 'forms', + integrationTripwire: true, name: 'DateField', styling: 'none', tier: 'composed', - conformanceTier: 'field-shaped', - integrationTripwire: true, visualCoverage: false, }); @@ -215,9 +215,9 @@ describe('createComponentPlan', () => { tier: 'atom', }); - const guide = plan.files.find((file) => - file.path.endsWith('feedback/status-badge.mdx'), - )?.contents; + const guide = plan.files.find((file) => { + return file.path.endsWith('feedback/status-badge.mdx'); + })?.contents; if (guide === undefined) throw new Error('Expected the scaffold to write the guide.'); const frontmatter = parseComponentFrontmatter(guide); diff --git a/packages/turbo-generators/src/component-creation-plan.ts b/packages/turbo-generators/src/component-creation-plan.ts index 06296668..74a8aeb4 100644 --- a/packages/turbo-generators/src/component-creation-plan.ts +++ b/packages/turbo-generators/src/component-creation-plan.ts @@ -3,12 +3,12 @@ type ComponentStyling = 'none' | 'recipe'; type ConformanceTier = 'universal' | 'field-shaped' | 'none'; export interface CreateComponentInput { + conformanceTier?: ConformanceTier; docsGroup: string; + integrationTripwire?: boolean; name: string; styling: ComponentStyling; tier: ComponentTier; - conformanceTier?: ConformanceTier; - integrationTripwire?: boolean; visualCoverage?: boolean; } @@ -136,10 +136,10 @@ export function createComponentPlan(input: CreateComponentInput): ComponentCreat return { expected: { + exampleSlug: `${name}/basic`, hostedDocsPath: `components/${docsGroup}/${name}`, packageDocsSlug: name, packageExportPath: `./${name}`, - exampleSlug: `${name}/basic`, }, files, jsonEdits: [ @@ -321,7 +321,7 @@ function renderComponentTest(input: { `import { ${input.pascalName} } from './index.js';`, ]; - const renderComponent = `render(<${input.pascalName} {...(props as ComponentProps)}>Content)`; + const renderComponent = `render(<${input.pascalName} {...(props}>Content)`; const contract = input.conformanceTier === 'universal' ? `testUniversalConformance({ diff --git a/turbo.json b/turbo.json index 4d93fc15..226ddc2a 100644 --- a/turbo.json +++ b/turbo.json @@ -10,13 +10,13 @@ "@luke-ui/react#fix:lint": { "dependsOn": ["build", "generate"] }, - "//#check:format-root": { - "cache": true - }, "//#check:cycles": { "cache": true, "dependsOn": ["@luke-ui/react#generate", "docs#generate"] }, + "//#check:format-root": { + "cache": true + }, "//#check:unused": { "cache": true, "dependsOn": ["@luke-ui/react#generate", "docs#generate"] diff --git a/vite.config.ts b/vite.config.ts index 4f7d460e..02e698c7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -76,72 +76,27 @@ export default defineConfig({ typeAware: true, typeCheck: true, }, - plugins: ['import', 'jsx-a11y', 'react', 'typescript', 'vitest'], - rules: { - curly: 'off', - 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], - 'import/no-duplicates': 'error', - 'jsdoc/check-access': 'error', - 'jsdoc/check-property-names': 'error', - 'jsdoc/check-tag-names': 'error', - 'jsdoc/empty-tags': 'error', - 'jsx-a11y/prefer-tag-over-role': 'off', - 'no-console': 'warn', - 'no-unused-vars': 'error', - 'vitest/expect-expect': [ - 'error', - { assertFunctionNames: ['captureVisual', 'captureVisualAppearance', 'expect'] }, - ], - 'react-hooks-js/config': 'error', - 'react-hooks-js/error-boundaries': 'error', - 'react-hooks-js/gating': 'error', - 'react-hooks-js/globals': 'error', - 'react-hooks-js/immutability': 'error', - 'react-hooks-js/incompatible-library': 'warn', - 'react-hooks-js/preserve-manual-memoization': 'error', - 'react-hooks-js/purity': 'error', - 'react-hooks-js/refs': 'error', - 'react-hooks-js/set-state-in-effect': 'error', - 'react-hooks-js/set-state-in-render': 'error', - 'react-hooks-js/static-components': 'error', - 'react-hooks-js/unsupported-syntax': 'warn', - 'react-hooks-js/use-memo': 'error', - 'react/exhaustive-deps': 'warn', - 'react/no-array-index-key': 'off', - 'react/rules-of-hooks': 'error', - 'sort-imports': 'off', - 'typescript/array-type': ['error', { default: 'generic' }], - 'typescript/consistent-type-exports': [ - 'error', - { fixMixedExportsWithInlineTypeSpecifier: false }, - ], - 'typescript/consistent-type-imports': [ - 'error', - { fixStyle: 'separate-type-imports', prefer: 'type-imports' }, - ], - 'vite-plus/prefer-vite-plus-imports': 'error', - }, overrides: [ { - files: ['packages/@luke-ui/react/src/**/*.browser.test.tsx'], excludeFiles: [ 'packages/@luke-ui/react/src/theme/**', 'packages/@luke-ui/react/src/styles/**', 'packages/@luke-ui/react/src/use-synchronize-animations/**', ], + files: ['packages/@luke-ui/react/src/**/*.browser.test.tsx'], rules: { 'no-restricted-imports': [ 'error', { paths: [ { - name: 'react-dom/client', message: 'Mount components through src/test-utils/render.tsx.', + name: 'react-dom/client', }, { - name: '@luke-ui/react/theme', message: 'Do not assert resolved theme tokens in component tests. Suppress this rule only for structural fixture setup.', + name: '@luke-ui/react/theme', }, ], }, @@ -149,8 +104,8 @@ export default defineConfig({ }, }, { - files: ['packages/@luke-ui/react/src/**/*.visual.test.tsx'], excludeFiles: ['packages/@luke-ui/react/src/theme/**'], + files: ['packages/@luke-ui/react/src/**/*.visual.test.tsx'], rules: { 'no-await-in-loop': 'off', 'no-restricted-imports': [ @@ -158,8 +113,8 @@ export default defineConfig({ { paths: [ { - name: 'react-dom/client', message: 'Mount components through src/test-utils/render.tsx.', + name: 'react-dom/client', }, ], }, @@ -167,5 +122,50 @@ export default defineConfig({ }, }, ], + plugins: ['import', 'jsx-a11y', 'react', 'typescript', 'vitest'], + rules: { + curly: 'off', + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], + 'import/no-duplicates': 'error', + 'jsdoc/check-access': 'error', + 'jsdoc/check-property-names': 'error', + 'jsdoc/check-tag-names': 'error', + 'jsdoc/empty-tags': 'error', + 'jsx-a11y/prefer-tag-over-role': 'off', + 'no-console': 'warn', + 'no-unused-vars': 'error', + 'react-hooks-js/config': 'error', + 'react-hooks-js/error-boundaries': 'error', + 'react-hooks-js/gating': 'error', + 'react-hooks-js/globals': 'error', + 'react-hooks-js/immutability': 'error', + 'react-hooks-js/incompatible-library': 'warn', + 'react-hooks-js/preserve-manual-memoization': 'error', + 'react-hooks-js/purity': 'error', + 'react-hooks-js/refs': 'error', + 'react-hooks-js/set-state-in-effect': 'error', + 'react-hooks-js/set-state-in-render': 'error', + 'react-hooks-js/static-components': 'error', + 'react-hooks-js/unsupported-syntax': 'warn', + 'react-hooks-js/use-memo': 'error', + 'react/exhaustive-deps': 'warn', + 'react/no-array-index-key': 'off', + 'react/rules-of-hooks': 'error', + 'sort-imports': 'off', + 'typescript/array-type': ['error', { default: 'generic' }], + 'typescript/consistent-type-exports': [ + 'error', + { fixMixedExportsWithInlineTypeSpecifier: false }, + ], + 'typescript/consistent-type-imports': [ + 'error', + { fixStyle: 'separate-type-imports', prefer: 'type-imports' }, + ], + 'vite-plus/prefer-vite-plus-imports': 'error', + 'vitest/expect-expect': [ + 'error', + { assertFunctionNames: ['captureVisual', 'captureVisualAppearance', 'expect'] }, + ], + }, }, });
{solidAnchor.targetLightness.toFixed(3)} {solidAnchor.resolvedLightness.toFixed(3)} - {`[${solidAnchor.band[0].toFixed(2)}, ${solidAnchor.band[1].toFixed(2)}]`} - {`[${solidAnchor.band[0].toFixed(2)}, ${solidAnchor.band[1].toFixed(2)}]`} {solidAnchor.adaptedForOnSolid ? 'yes' : 'no'} {`${solidAnchor.onSolidRatioSolid.toFixed(2)}:1`} {`${solidAnchor.onSolidRatioSolidHover.toFixed(2)}:1`}