diff --git a/apps/component-tests/src/global.css b/apps/component-tests/src/global.css index 77ef5bc24..8843c6635 100644 --- a/apps/component-tests/src/global.css +++ b/apps/component-tests/src/global.css @@ -36,9 +36,13 @@ --alert: 0 84.2% 60.2%; --alert-foreground: 210 40% 98%; --ring: 222.2 47.4% 11.2%; + --switch-thumb-color-highlight: 0, 0%, 72%, 0.25; + --switch-track-color-inactive: 80 0% 80%; } .dark { + --switch-thumb-color-highlight: 0, 0%, 100%, 0.25; + --switch-track-color-inactive: 240, 10%, 50%; --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; diff --git a/apps/component-tests/src/routes/[kit]/[component]/[example]/index.tsx b/apps/component-tests/src/routes/[kit]/[component]/[example]/index.tsx index 05cdec8cf..e0d821320 100644 --- a/apps/component-tests/src/routes/[kit]/[component]/[example]/index.tsx +++ b/apps/component-tests/src/routes/[kit]/[component]/[example]/index.tsx @@ -1,6 +1,5 @@ import { component$ } from '@builder.io/qwik'; import { ShowcaseTest } from '../../../../components/showcase-test/showcase-test'; - export default component$(() => { // Need to center the content in the screen // so that tests like popover placement can diff --git a/apps/component-tests/tailwind.config.cjs b/apps/component-tests/tailwind.config.cjs index 3d7d99030..afca48640 100644 --- a/apps/component-tests/tailwind.config.cjs +++ b/apps/component-tests/tailwind.config.cjs @@ -13,14 +13,26 @@ module.exports = { plugins: [ // PLUGIN-START require('tailwindcss-animate'), - plugin(function ({ addUtilities }) { + plugin(function ({ addUtilities, theme, e }) { addUtilities({ '.press': { transform: 'var(--transform-press)', }, }); + const sizelist = theme('spacing'); + const blockSizeUtilities = Object.keys(sizelist).reduce((acc, key) => { + const value = sizelist[key]; + acc[`.${e(`block-size-${key}`)}`] = { + 'block-size': value, + }; + acc[`.${e(`inline-size-${key}`)}`] = { + 'inline-size': value, + }; + return acc; + }, {}); + + addUtilities(blockSizeUtilities, ['responsive', 'hover']); }), - // PLUGIN-END ], darkMode: 'class', theme: { @@ -35,6 +47,8 @@ module.exports = { ring: 'hsl(var(--ring))', background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))', + switchInactive: 'hsl(var(--switch-track-color-inactive))', + switchThumb: 'hsl(var(--switch-thumb-color-highlight))', primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))', diff --git a/apps/website/src/global.css b/apps/website/src/global.css index 715f1dbb1..679d63e7c 100644 --- a/apps/website/src/global.css +++ b/apps/website/src/global.css @@ -36,9 +36,13 @@ --alert: 0 84.2% 60.2%; --alert-foreground: 210 40% 98%; --ring: 222.2 47.4% 11.2%; + --switch-thumb-color-highlight: 0, 0%, 72%, 0.25; + --switch-track-color-inactive: 80 0% 80%; } .dark { + --switch-thumb-color-highlight: 0, 0%, 100%, 0.25; + --switch-track-color-inactive: 240, 10%, 50%; --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; @@ -1362,7 +1366,7 @@ body { min-height: 100%; } -/* Utilities layer for animations. The current arbitrary & docs tailwind animation guidelines are not maintainable long term. +/* Utilities layer for animations. The current arbitrary & docs tailwind animation guidelines are not maintainable long term. It would make more sense to supply the user with the animation declaration in the docs. */ @layer utilities { diff --git a/apps/website/src/routes/docs/headless/menu.md b/apps/website/src/routes/docs/headless/menu.md index 55474c23d..15097bac1 100644 --- a/apps/website/src/routes/docs/headless/menu.md +++ b/apps/website/src/routes/docs/headless/menu.md @@ -32,3 +32,4 @@ - [Tooltip](/docs/headless/tooltip) - [Toggle](/docs/headless/toggle) - [Toggle Group](/docs/headless/toggle-group) +- [Switch](/docs/headless/switch) diff --git a/apps/website/src/routes/docs/headless/switch/examples/checked.tsx b/apps/website/src/routes/docs/headless/switch/examples/checked.tsx new file mode 100644 index 000000000..d70f9838b --- /dev/null +++ b/apps/website/src/routes/docs/headless/switch/examples/checked.tsx @@ -0,0 +1,17 @@ +import { component$, useStyles$, useSignal } from '@builder.io/qwik'; +import { Switch } from '@qwik-ui/headless'; + +export default component$(() => { + const checked = useSignal(true); + useStyles$(styles); + return ( + + test + + + + + ); +}); + +import styles from '../snippets/switch.css?inline'; diff --git a/apps/website/src/routes/docs/headless/switch/examples/defaultChecked.tsx b/apps/website/src/routes/docs/headless/switch/examples/defaultChecked.tsx new file mode 100644 index 000000000..afce67f32 --- /dev/null +++ b/apps/website/src/routes/docs/headless/switch/examples/defaultChecked.tsx @@ -0,0 +1,16 @@ +import { component$, useStyles$ } from '@builder.io/qwik'; +import { Switch } from '@qwik-ui/headless'; + +export default component$(() => { + useStyles$(styles); + return ( + + test + + + + + ); +}); + +import styles from '../snippets/switch.css?inline'; diff --git a/apps/website/src/routes/docs/headless/switch/examples/disabled.tsx b/apps/website/src/routes/docs/headless/switch/examples/disabled.tsx new file mode 100644 index 000000000..990ff910c --- /dev/null +++ b/apps/website/src/routes/docs/headless/switch/examples/disabled.tsx @@ -0,0 +1,17 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Switch } from '@qwik-ui/headless'; + +export default component$(() => { + const checked = useSignal(false); + useStyles$(styles); + return ( + + test + + + + + ); +}); + +import styles from '../snippets/switch.css?inline'; diff --git a/apps/website/src/routes/docs/headless/switch/examples/hero.tsx b/apps/website/src/routes/docs/headless/switch/examples/hero.tsx new file mode 100644 index 000000000..915e8430a --- /dev/null +++ b/apps/website/src/routes/docs/headless/switch/examples/hero.tsx @@ -0,0 +1,19 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Switch } from '@qwik-ui/headless'; + +export default component$(() => { + const checked = useSignal(false); + const count = useSignal(0); + useStyles$(styles); + + return ( + count.value++}> + label{count.value} + + + + + ); +}); + +import styles from '../snippets/switch.css?inline'; diff --git a/apps/website/src/routes/docs/headless/switch/examples/pure.tsx b/apps/website/src/routes/docs/headless/switch/examples/pure.tsx new file mode 100644 index 000000000..b9ed205bd --- /dev/null +++ b/apps/website/src/routes/docs/headless/switch/examples/pure.tsx @@ -0,0 +1,16 @@ +import { component$, useStyles$ } from '@builder.io/qwik'; +import { Switch } from '@qwik-ui/headless'; + +export default component$(() => { + useStyles$(styles); + + return ( + + + + + + ); +}); + +import styles from '../snippets/switch.css?inline'; diff --git a/apps/website/src/routes/docs/headless/switch/index.mdx b/apps/website/src/routes/docs/headless/switch/index.mdx new file mode 100644 index 000000000..d9c297d79 --- /dev/null +++ b/apps/website/src/routes/docs/headless/switch/index.mdx @@ -0,0 +1,158 @@ +--- +title: Qwik UI | Switch +--- + +import { FeatureList } from '~/components/feature-list/feature-list'; + +import { statusByComponent } from '~/_state/component-statuses'; + + + +# Switch + +A toggleable control for user interactions. + + + +## ✨ Features + + + + +## Building blocks + + + +## Anatomy + + + +## Why use a headless Switch? + +The native `` element presents several challenges regarding styling, behavior, and user experience. + +### Native Switch pain points + + + +### Native effort + +While there are efforts to enhance the native checkbox element, such as the [Open UI group](https://open-ui.org/components/switch/), these solutions often fall short in terms of flexibility and customization. A headless Switch component allows developers to create a fully tailored user experience without the constraints of native elements. + +## Behavior Tests + +### Mouse Interaction + + + +- **Toggle State**: Ensures that clicking the switch toggles its checked state correctly. +- **Trigger onChange**: Verifies that the onChange callback is triggered when the switch is clicked. + +### Keyboard Interaction + + + +- **Enter Key**: Tests that pressing the Enter key toggles the switch's state. +- **Space Key**: Checks that pressing the Space key toggles the switch's state. + +### Default Properties + + + +- **Checked by Default**: Confirms that the switch is checked upon initial render if set so. +- **Disabled State**: Ensures that the switch is disabled and does not respond to user interactions when set so. + +### Pure Implementation + + + +- **Minimal Implementation**: Demonstrates a minimal implementation of the switch without additional features. +- **Custom Data Attribute**: Shows how to add custom data attributes to the switch input element. + +### Disabled State + + +- **Checked by Default**: Confirms that the switch is checked upon initial render if set so. +- **Disabled State**: Ensures that the switch is disabled and does not respond to user interactions when set so. +- **Label Support**: Includes a label for the switch to enhance accessibility. + + +## API + +### Switch.Root + + + + + diff --git a/apps/website/src/routes/docs/headless/switch/snippets/building-blocks.tsx b/apps/website/src/routes/docs/headless/switch/snippets/building-blocks.tsx new file mode 100644 index 000000000..fe14be78c --- /dev/null +++ b/apps/website/src/routes/docs/headless/switch/snippets/building-blocks.tsx @@ -0,0 +1,16 @@ +import { component$, useSignal, useStyles$ } from '@builder.io/qwik'; +import { Switch } from '@qwik-ui/headless'; + +export default component$(() => { + const checked = useSignal(false); + useStyles$(styles); + + return ( + + test + + + ); +}); + +import styles from '../snippets/switch.css?inline'; diff --git a/apps/website/src/routes/docs/headless/switch/snippets/switch.css b/apps/website/src/routes/docs/headless/switch/snippets/switch.css new file mode 100644 index 000000000..48d863172 --- /dev/null +++ b/apps/website/src/routes/docs/headless/switch/snippets/switch.css @@ -0,0 +1,11 @@ +/* Define default light theme colors */ +.switch { + flex-direction: row-reverse; + display: flex; + align-items: center; + gap: 1ch; + justify-content: space-between; + cursor: pointer; + user-select: none; + -webkit-tap-highlight-color: transparent; +} diff --git a/apps/website/src/routes/docs/styled/menu.md b/apps/website/src/routes/docs/styled/menu.md index 0945d72aa..c044156fd 100644 --- a/apps/website/src/routes/docs/styled/menu.md +++ b/apps/website/src/routes/docs/styled/menu.md @@ -38,3 +38,4 @@ - [Textarea](/docs/styled/textarea) - [Toggle](/docs/styled/toggle) - [ToggleGroup](/docs/styled/toggle-group) +- [Switch](/docs/styled/switch) diff --git a/apps/website/src/routes/docs/styled/switch/examples/checked.tsx b/apps/website/src/routes/docs/styled/switch/examples/checked.tsx new file mode 100644 index 000000000..fbfe9f464 --- /dev/null +++ b/apps/website/src/routes/docs/styled/switch/examples/checked.tsx @@ -0,0 +1,14 @@ +import { component$, useSignal } from '@builder.io/qwik'; +import { Switch } from '~/components/ui'; + +export default component$(() => { + const checked = useSignal(true); + return ( + + test + + + + + ); +}); diff --git a/apps/website/src/routes/docs/styled/switch/examples/defaultChecked.tsx b/apps/website/src/routes/docs/styled/switch/examples/defaultChecked.tsx new file mode 100644 index 000000000..85f6d3687 --- /dev/null +++ b/apps/website/src/routes/docs/styled/switch/examples/defaultChecked.tsx @@ -0,0 +1,13 @@ +import { component$ } from '@builder.io/qwik'; +import { Switch } from '~/components/ui'; + +export default component$(() => { + return ( + + test + + + + + ); +}); diff --git a/apps/website/src/routes/docs/styled/switch/examples/disabled.tsx b/apps/website/src/routes/docs/styled/switch/examples/disabled.tsx new file mode 100644 index 000000000..9441cb406 --- /dev/null +++ b/apps/website/src/routes/docs/styled/switch/examples/disabled.tsx @@ -0,0 +1,13 @@ +import { component$ } from '@builder.io/qwik'; +import { Switch } from '~/components/ui'; + +export default component$(() => { + return ( + + test + + + + + ); +}); diff --git a/apps/website/src/routes/docs/styled/switch/examples/hero.tsx b/apps/website/src/routes/docs/styled/switch/examples/hero.tsx new file mode 100644 index 000000000..650230a53 --- /dev/null +++ b/apps/website/src/routes/docs/styled/switch/examples/hero.tsx @@ -0,0 +1,16 @@ +import { component$, useSignal } from '@builder.io/qwik'; +import { Switch } from '~/components/ui'; + +export default component$(() => { + const checked = useSignal(false); + return ( + <> + + label + + + + + + ); +}); diff --git a/apps/website/src/routes/docs/styled/switch/examples/pure.tsx b/apps/website/src/routes/docs/styled/switch/examples/pure.tsx new file mode 100644 index 000000000..03f09e45e --- /dev/null +++ b/apps/website/src/routes/docs/styled/switch/examples/pure.tsx @@ -0,0 +1,12 @@ +import { component$ } from '@builder.io/qwik'; +import { Switch } from '~/components/ui'; + +export default component$(() => { + return ( + + + + + + ); +}); diff --git a/apps/website/src/routes/docs/styled/switch/index.mdx b/apps/website/src/routes/docs/styled/switch/index.mdx new file mode 100644 index 000000000..b0ca74e16 --- /dev/null +++ b/apps/website/src/routes/docs/styled/switch/index.mdx @@ -0,0 +1,81 @@ +--- +title: Qwik UI | Styled Switch Component +--- + +import { statusByComponent } from '~/_state/component-statuses'; + + + +# Switch + +The Switch component allows users to toggle between two states, such as on/off or enabled/disabled. It is designed to be accessible and visually appealing, providing a smooth user experience. + + + +## Installation + +To install the Switch component, run the following command in your terminal: + +```sh +qwik-ui add switch +``` + +Alternatively, you can copy and paste the component code directly into your project. + +## Usage + +Here’s how to use the Switch component in your application: + +```tsx +import { component$, useSignal } from '@builder.io/qwik'; +import { Switch } from '~/components/ui'; + +export default component$(() => { + const checked = useSignal(false); + return ( + + + Toggle Option + + ); +}); +``` + +## Examples + +### Mouse Interaction + + + +- **Toggle State**: Ensures that clicking the switch toggles its checked state correctly. +- **Trigger onChange**: Verifies that the onChange callback is triggered when the switch is clicked. + +### Keyboard Interaction + + + +- **Enter Key**: Tests that pressing the Enter key toggles the switch's state. +- **Space Key**: Checks that pressing the Space key toggles the switch's state. + +### Default Properties + + + +- **Checked by Default**: Confirms that the switch is checked upon initial render if set so. +- **Disabled State**: Ensures that the switch is disabled and does not respond to user interactions when set so. + +### Pure Implementation + + + +- **Minimal Implementation**: Demonstrates a minimal implementation of the switch without additional features. +- **Custom Data Attribute**: Shows how to add custom data attributes to the switch input element. + +### Disabled State + + +- **Checked by Default**: Confirms that the switch is checked upon initial render if set so. +- **Disabled State**: Ensures that the switch is disabled and does not respond to user interactions when set so. +- **Label Support**: Includes a label for the switch to enhance accessibility. + + diff --git a/apps/website/tailwind.config.cjs b/apps/website/tailwind.config.cjs index 3d7d99030..1a1d5ec60 100644 --- a/apps/website/tailwind.config.cjs +++ b/apps/website/tailwind.config.cjs @@ -13,12 +13,26 @@ module.exports = { plugins: [ // PLUGIN-START require('tailwindcss-animate'), - plugin(function ({ addUtilities }) { + plugin(function ({ addUtilities,theme,e }) { addUtilities({ '.press': { transform: 'var(--transform-press)', }, }); + const sizelist = theme('spacing'); + const blockSizeUtilities = Object.keys(sizelist).reduce((acc, key) => { + const value = sizelist[key]; + acc[`.${e(`block-size-${key}`)}`] = { + 'block-size': value, + }; + acc[`.${e(`inline-size-${key}`)}`] = { + 'inline-size': value, + }; + return acc; + }, {}); + + addUtilities(blockSizeUtilities, ['responsive', 'hover']); + }), // PLUGIN-END ], @@ -35,6 +49,8 @@ module.exports = { ring: 'hsl(var(--ring))', background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))', + switchInactive: 'hsl(var(--switch-track-color-inactive))', + switchThumb: 'hsl(var(--switch-thumb-color-highlight))', primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))', diff --git a/cla-signs/v1/cla.json b/cla-signs/v1/cla.json index dff2485fa..7916ce0cc 100644 --- a/cla-signs/v1/cla.json +++ b/cla-signs/v1/cla.json @@ -577,4 +577,4 @@ "pullRequestNo": 1069 } ] -} \ No newline at end of file +} diff --git a/package.json b/package.json index dcd350a19..9e2108abc 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "release.resetroot": "cp dist/pnpm-workspace.yaml pnpm-workspace.yaml", "release.e2e": "pnpm release.prepare && pnpm release.setroot && pnpm -r exec pnpm version patch && pnpm -r exec pnpm publish --tag e2e --no-git-checks && pnpm release.resetroot", "test.cli": "NODE_OPTIONS=--experimental-vm-modules nx test cli", - "test.cli.e2e": "nx e2e cli-e2e", + "test.cli.e2e": "nx e2e cli-e2e switch.test.ts", "test.headless": "nx component-test headless --skip-nx-cache", "test.visual.headless": "nx visual-test headless", "test.pw.headless": "nx e2e headless", diff --git a/packages/cli/src/generators/init/init-generator.spec.ts b/packages/cli/src/generators/init/init-generator.spec.ts index e3e9646ad..72972b4fc 100644 --- a/packages/cli/src/generators/init/init-generator.spec.ts +++ b/packages/cli/src/generators/init/init-generator.spec.ts @@ -14,7 +14,7 @@ describe('init generator', () => { } test(` - GIVEN empty options and tree + GIVEN empty options and tree WHEN initGenerator is run THEN it should create a config file with the default values`, async () => { const { tree, options } = setup(); @@ -30,7 +30,7 @@ describe('init generator', () => { }); test(` - GIVEN project root of "/my-project" + GIVEN project root of "/my-project" WHEN initGenerator is run THEN it should create the config file inside my-project`, async () => { const { tree, options } = setup(); diff --git a/packages/cli/src/generators/setup-tailwind/__snapshots__/setup-tailwind-generator.spec.ts.snap b/packages/cli/src/generators/setup-tailwind/__snapshots__/setup-tailwind-generator.spec.ts.snap new file mode 100644 index 000000000..7b9d32ff9 --- /dev/null +++ b/packages/cli/src/generators/setup-tailwind/__snapshots__/setup-tailwind-generator.spec.ts.snap @@ -0,0 +1,584 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Setup Tailwind generator + GIVEN no options are passed + THEN it should generate "simple" style with primary color "cyan-600", base color "slate" and border-radius 0 1`] = ` +"@tailwind components; +@tailwind base; +@tailwind utilities; +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 47.4% 11.2%; + --card: 0 0% 100%; + --card-foreground: 222.2 47.4% 11.2%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --primary: 191.6 91.4% 36.5%; + --primary-foreground: 0 0% 100%; + --secondary: 222.2 47.4% 11.2%; + --secondary-foreground: 0 0% 100%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --alert: 0 84.2% 60.2%; + --alert-foreground: 210 40% 98%; + --ring: 222.2 47.4% 11.2%; + --border-width: 0px; + --border-radius: 0; + --shadow-base: 0 1px 2px 0 rgba(0, 0, 0, 0.01); + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1), 0 1px 5px 0px rgba(0, 0, 0, 0.1); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -2px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -4px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), + 0 8px 10px -6px rgba(0, 0, 0, 0.1); + --shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 1); + --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.01); + --transform-press: scale(0.95); + } + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --primary: 191.6 91.4% 36.5%; + --primary-foreground: 0 0% 100%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 0 0% 0%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --alert: 0 84.2% 60.2%; + --alert-foreground: 210 40% 98%; + --ring: 212.7 26.8% 83.9; + --border-width: 0px; + --border-radius: 0; + --shadow-base: 0 1px 2px 0 rgba(0, 0, 0, 0.01); + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1), 0 1px 5px 0px rgba(0, 0, 0, 0.1); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -2px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -4px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), + 0 8px 10px -6px rgba(0, 0, 0, 0.1); + --shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 1); + --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.01); + --transform-press: scale(0.95); + } +} + +html { + height: 100%; + min-height: 100%; + scroll-behavior: smooth; + background-color: var(--color-bg) !important; + color: var(--color-text) !important; +} +" +`; + +exports[`Setup Tailwind generator + GIVEN style is "brutalist" and primary color is "red-600" and border-radius is 1 + THEN it should generate the correct theme 1`] = ` +"@tailwind components; +@tailwind base; +@tailwind utilities; +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 47.4% 11.2%; + --card: 0 0% 100%; + --card-foreground: 222.2 47.4% 11.2%; + --border: 0 0% 0%; + --input: 0 0% 0%; + --primary: 0 72.2% 50.6%; + --primary-foreground: 0 0% 100%; + --secondary: 222.2 47.4% 11.2%; + --secondary-foreground: 0 0% 100%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --alert: 0 84.2% 60.2%; + --alert-foreground: 210 40% 98%; + --ring: 0 0% 0%; + --border-width: 2px; + --border-radius: 1rem; + --shadow-base: 0px 0px 0px 0 rgba(0, 0, 0, 1); + --shadow-sm: 4px 4px 0px 0 rgba(0, 0, 0, 1); + --shadow: 5px 5px 0px 0px rgba(0, 0, 0, 1); + --shadow-md: 6px 6px 0px 0px rgba(0, 0, 0, 1); + --shadow-lg: 8px 8px 0px 0px rgba(0, 0, 0, 1); + --shadow-xl: 11px 11px 0px 0px rgba(0, 0, 0, 1); + --shadow-2xl: 13px 13px 0px 0px rgba(0, 0, 0, 1); + --shadow-inner: inset 2px 2px 0px 0px rgba(0, 0, 0, 0); + --transform-press: translate(4px, 4px); + } + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --border: 0 0% 0%; + --input: 0 0% 0%; + --primary: 0 72.2% 50.6%; + --primary-foreground: 0 0% 100%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 0 0% 0%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --alert: 0 84.2% 60.2%; + --alert-foreground: 210 40% 98%; + --ring: 0 0% 0%; + --border-width: 2px; + --border-radius: 1rem; + --shadow-base: 0px 0px 0px 0 rgba(0, 0, 0, 1); + --shadow-sm: 4px 4px 0px 0 rgba(0, 0, 0, 1); + --shadow: 5px 5px 0px 0px rgba(0, 0, 0, 1); + --shadow-md: 6px 6px 0px 0px rgba(0, 0, 0, 1); + --shadow-lg: 8px 8px 0px 0px rgba(0, 0, 0, 1); + --shadow-xl: 11px 11px 0px 0px rgba(0, 0, 0, 1); + --shadow-2xl: 13px 13px 0px 0px rgba(0, 0, 0, 1); + --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.01); + --transform-press: translate(4px, 4px); + } +} + +html { + height: 100%; + min-height: 100%; + scroll-behavior: smooth; + background-color: var(--color-bg) !important; + color: var(--color-text) !important; +} +" +`; + +exports[`Setup Tailwind generator + GIVEN global.css and tailwind config exist in commonjs format + THEN it should generate the proper tailwind config values 1`] = ` +"const plugin = require('tailwindcss/plugin'); + +const { join } = require('path'); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + plugins: [ + require('tailwindcss-animate'), + plugin(function ({ addUtilities, theme, e }) { + addUtilities({ + '.press': { + transform: 'var(--transform-press)', + }, + }); + const sizelist = theme('spacing'); + const blockSizeUtilities = Object.keys(sizelist).reduce((acc, key) => { + const value = sizelist[key]; + acc[\`.\${e(\`block-size-\${key}\`)}\`] = { + 'block-size': value, + }; + acc[\`.\${e(\`inline-size-\${key}\`)}\`] = { + 'inline-size': value, + }; + return acc; + }, {}); + + addUtilities(blockSizeUtilities, ['responsive', 'hover']); + }), + ], + + content: [join(__dirname, 'src/**/*.{js,ts,jsx,tsx,mdx}')], + darkMode: 'class', + theme: { + screens: { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + }, + important: true, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + switchInactive: 'hsl(var(--switch-track-color-inactive))', + switchThumb: 'hsl(var(--switch-thumb-color-highlight))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + alert: { + DEFAULT: 'hsl(var(--alert))', + foreground: 'hsl(var(--alert-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + }, + borderRadius: { + base: 'var(--border-radius)', + sm: 'calc(var(--border-radius) + 0.125rem)', + DEFAULT: 'calc(var(--border-radius) + 0.25rem)', + md: 'calc(var(--border-radius) + 0.375rem)', + lg: 'calc(var(--border-radius) + 0.5rem)', + xl: 'calc(var(--border-radius) + 0.75rem)', + '2xl': 'calc(var(--border-radius) + 1rem)', + '3xl': 'calc(var(--border-radius) + 1.5rem)', + }, + borderWidth: { + base: 'var(--border-width)', + DEFAULT: 'calc(var(--border-width) + 1px)', + 2: 'calc(var(--border-width) + 2px)', + 4: 'calc(var(--border-width) + 4px)', + 8: 'calc(var(--border-width) + 8px)', + }, + boxShadow: { + base: 'var(--shadow-base)', + sm: 'var(--shadow-sm)', + DEFAULT: 'var(--shadow)', + md: 'var(--shadow-md)', + lg: 'var(--shadow-lg)', + xl: 'var(--shadow-xl)', + '2xl': 'var(--shadow-2xl)', + inner: 'var(--shadow-inner)', + }, + strokeWidth: { + 0: '0', + base: 'var(--stroke-width)', + 1: 'calc(var(--stroke-width) + 1px)', + 2: 'calc(var(--stroke-width) + 2px)', + }, + animation: { + 'accordion-up': 'collapsible-up 0.2s ease-out 0s 1 normal forwards', + 'accordion-down': 'collapsible-down 0.2s ease-out 0s 1 normal forwards', + }, + keyframes: { + 'collapsible-down': { + from: { height: '0' }, + to: { height: 'var(--qwikui-collapsible-content-height)' }, + }, + 'collapsible-up': { + from: { height: 'var(--qwikui-collapsible-content-height)' }, + to: { height: '0' }, + }, + }, + fontFamily: { + sans: ['Inter Variable', 'sans-serif'], + }, + }, + }, +}; +" +`; + +exports[`Setup Tailwind generator + GIVEN tailwind config exist in esm format + WHEN running the generator + THEN it should generate the proper tailwind config values 1`] = ` +"import plugin from 'tailwindcss/plugin'; + +/** @type {import('tailwindcss').Config} */ +export default { + plugins: [ + require('tailwindcss-animate'), + plugin(function ({ addUtilities, theme, e }) { + addUtilities({ + '.press': { + transform: 'var(--transform-press)', + }, + }); + const sizelist = theme('spacing'); + const blockSizeUtilities = Object.keys(sizelist).reduce((acc, key) => { + const value = sizelist[key]; + acc[\`.\${e(\`block-size-\${key}\`)}\`] = { + 'block-size': value, + }; + acc[\`.\${e(\`inline-size-\${key}\`)}\`] = { + 'inline-size': value, + }; + return acc; + }, {}); + + addUtilities(blockSizeUtilities, ['responsive', 'hover']); + }), + ], + + content: [join(__dirname, 'src/**/*.{js,ts,jsx,tsx,mdx}')], + darkMode: 'class', + theme: { + screens: { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + }, + important: true, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + switchInactive: 'hsl(var(--switch-track-color-inactive))', + switchThumb: 'hsl(var(--switch-thumb-color-highlight))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + alert: { + DEFAULT: 'hsl(var(--alert))', + foreground: 'hsl(var(--alert-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + }, + borderRadius: { + base: 'var(--border-radius)', + sm: 'calc(var(--border-radius) + 0.125rem)', + DEFAULT: 'calc(var(--border-radius) + 0.25rem)', + md: 'calc(var(--border-radius) + 0.375rem)', + lg: 'calc(var(--border-radius) + 0.5rem)', + xl: 'calc(var(--border-radius) + 0.75rem)', + '2xl': 'calc(var(--border-radius) + 1rem)', + '3xl': 'calc(var(--border-radius) + 1.5rem)', + }, + borderWidth: { + base: 'var(--border-width)', + DEFAULT: 'calc(var(--border-width) + 1px)', + 2: 'calc(var(--border-width) + 2px)', + 4: 'calc(var(--border-width) + 4px)', + 8: 'calc(var(--border-width) + 8px)', + }, + boxShadow: { + base: 'var(--shadow-base)', + sm: 'var(--shadow-sm)', + DEFAULT: 'var(--shadow)', + md: 'var(--shadow-md)', + lg: 'var(--shadow-lg)', + xl: 'var(--shadow-xl)', + '2xl': 'var(--shadow-2xl)', + inner: 'var(--shadow-inner)', + }, + strokeWidth: { + 0: '0', + base: 'var(--stroke-width)', + 1: 'calc(var(--stroke-width) + 1px)', + 2: 'calc(var(--stroke-width) + 2px)', + }, + animation: { + 'accordion-up': 'collapsible-up 0.2s ease-out 0s 1 normal forwards', + 'accordion-down': 'collapsible-down 0.2s ease-out 0s 1 normal forwards', + }, + keyframes: { + 'collapsible-down': { + from: { height: '0' }, + to: { height: 'var(--qwikui-collapsible-content-height)' }, + }, + 'collapsible-up': { + from: { height: 'var(--qwikui-collapsible-content-height)' }, + to: { height: '0' }, + }, + }, + fontFamily: { + sans: ['Inter Variable', 'sans-serif'], + }, + }, + }, +}; +" +`; + +exports[`Setup Tailwind generator + GIVEN tailwind config has already a plugins array + THEN it should add the plugin with the right plugin and import 1`] = ` +"import plugin from 'tailwindcss/plugin'; + +/** @type {import('tailwindcss').Config} */ +export default { + plugins: [ + require('tailwindcss-animate'), + plugin(function ({ addUtilities, theme, e }) { + addUtilities({ + '.press': { + transform: 'var(--transform-press)', + }, + }); + const sizelist = theme('spacing'); + const blockSizeUtilities = Object.keys(sizelist).reduce((acc, key) => { + const value = sizelist[key]; + acc[\`.\${e(\`block-size-\${key}\`)}\`] = { + 'block-size': value, + }; + acc[\`.\${e(\`inline-size-\${key}\`)}\`] = { + 'inline-size': value, + }; + return acc; + }, {}); + + addUtilities(blockSizeUtilities, ['responsive', 'hover']); + }), + somePlugin, + ], + content: [join(__dirname, 'src/**/*.{js,ts,jsx,tsx,mdx}')], + darkMode: 'class', + theme: { + screens: { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + }, + important: true, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + switchInactive: 'hsl(var(--switch-track-color-inactive))', + switchThumb: 'hsl(var(--switch-thumb-color-highlight))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + alert: { + DEFAULT: 'hsl(var(--alert))', + foreground: 'hsl(var(--alert-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + }, + borderRadius: { + base: 'var(--border-radius)', + sm: 'calc(var(--border-radius) + 0.125rem)', + DEFAULT: 'calc(var(--border-radius) + 0.25rem)', + md: 'calc(var(--border-radius) + 0.375rem)', + lg: 'calc(var(--border-radius) + 0.5rem)', + xl: 'calc(var(--border-radius) + 0.75rem)', + '2xl': 'calc(var(--border-radius) + 1rem)', + '3xl': 'calc(var(--border-radius) + 1.5rem)', + }, + borderWidth: { + base: 'var(--border-width)', + DEFAULT: 'calc(var(--border-width) + 1px)', + 2: 'calc(var(--border-width) + 2px)', + 4: 'calc(var(--border-width) + 4px)', + 8: 'calc(var(--border-width) + 8px)', + }, + boxShadow: { + base: 'var(--shadow-base)', + sm: 'var(--shadow-sm)', + DEFAULT: 'var(--shadow)', + md: 'var(--shadow-md)', + lg: 'var(--shadow-lg)', + xl: 'var(--shadow-xl)', + '2xl': 'var(--shadow-2xl)', + inner: 'var(--shadow-inner)', + }, + strokeWidth: { + 0: '0', + base: 'var(--stroke-width)', + 1: 'calc(var(--stroke-width) + 1px)', + 2: 'calc(var(--stroke-width) + 2px)', + }, + animation: { + 'accordion-up': 'collapsible-up 0.2s ease-out 0s 1 normal forwards', + 'accordion-down': 'collapsible-down 0.2s ease-out 0s 1 normal forwards', + }, + keyframes: { + 'collapsible-down': { + from: { height: '0' }, + to: { height: 'var(--qwikui-collapsible-content-height)' }, + }, + 'collapsible-up': { + from: { height: 'var(--qwikui-collapsible-content-height)' }, + to: { height: '0' }, + }, + }, + fontFamily: { + sans: ['Inter Variable', 'sans-serif'], + }, + }, + }, +}; +" +`; diff --git a/packages/cli/src/generators/setup-tailwind/setup-tailwind-generator.spec.ts b/packages/cli/src/generators/setup-tailwind/setup-tailwind-generator.spec.ts index 884209c40..15a893a47 100644 --- a/packages/cli/src/generators/setup-tailwind/setup-tailwind-generator.spec.ts +++ b/packages/cli/src/generators/setup-tailwind/setup-tailwind-generator.spec.ts @@ -58,7 +58,7 @@ describe('Setup Tailwind generator', () => { html { height: 100%; - min-height: 100%; + min-height: 100%; scroll-behavior: smooth; background-color: var(--color-bg) !important; color: var(--color-text) !important; @@ -85,125 +85,125 @@ html { const updatedTailwindConfigContent = tree.read('tailwind.config.cjs', 'utf-8'); expect(updatedTailwindConfigContent).toMatchInlineSnapshot(` -"const plugin = require('tailwindcss/plugin'); - -const { join } = require('path'); - -/** @type {import('tailwindcss').Config} */ -module.exports = { - plugins: [ - require('tailwindcss-animate'), - plugin(function ({ addUtilities }) { - addUtilities({ - '.press': { - transform: 'var(--transform-press)', - }, - }); - }), - ], - - content: [join(__dirname, 'src/**/*.{js,ts,jsx,tsx,mdx}')], - darkMode: 'class', - theme: { - screens: { - sm: '640px', - md: '768px', - lg: '1024px', - xl: '1280px', - '2xl': '1536px', - }, - important: true, - extend: { - colors: { - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', - }, - alert: { - DEFAULT: 'hsl(var(--alert))', - foreground: 'hsl(var(--alert-foreground))', + "const plugin = require('tailwindcss/plugin'); + + const { join } = require('path'); + + /** @type {import('tailwindcss').Config} */ + module.exports = { + plugins: [ + require('tailwindcss-animate'), + plugin(function ({ addUtilities }) { + addUtilities({ + '.press': { + transform: 'var(--transform-press)', + }, + }); + }), + ], + + content: [join(__dirname, 'src/**/*.{js,ts,jsx,tsx,mdx}')], + darkMode: 'class', + theme: { + screens: { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + }, + important: true, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + alert: { + DEFAULT: 'hsl(var(--alert))', + foreground: 'hsl(var(--alert-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + }, + borderRadius: { + base: 'var(--border-radius)', + sm: 'calc(var(--border-radius) + 0.125rem)', + DEFAULT: 'calc(var(--border-radius) + 0.25rem)', + md: 'calc(var(--border-radius) + 0.375rem)', + lg: 'calc(var(--border-radius) + 0.5rem)', + xl: 'calc(var(--border-radius) + 0.75rem)', + '2xl': 'calc(var(--border-radius) + 1rem)', + '3xl': 'calc(var(--border-radius) + 1.5rem)', + }, + borderWidth: { + base: 'var(--border-width)', + DEFAULT: 'calc(var(--border-width) + 1px)', + 2: 'calc(var(--border-width) + 2px)', + 4: 'calc(var(--border-width) + 4px)', + 8: 'calc(var(--border-width) + 8px)', + }, + boxShadow: { + base: 'var(--shadow-base)', + sm: 'var(--shadow-sm)', + DEFAULT: 'var(--shadow)', + md: 'var(--shadow-md)', + lg: 'var(--shadow-lg)', + xl: 'var(--shadow-xl)', + '2xl': 'var(--shadow-2xl)', + inner: 'var(--shadow-inner)', + }, + strokeWidth: { + 0: '0', + base: 'var(--stroke-width)', + 1: 'calc(var(--stroke-width) + 1px)', + 2: 'calc(var(--stroke-width) + 2px)', + }, + animation: { + 'accordion-up': 'collapsible-up 0.2s ease-out 0s 1 normal forwards', + 'accordion-down': 'collapsible-down 0.2s ease-out 0s 1 normal forwards', + }, + keyframes: { + 'collapsible-down': { + from: { height: '0' }, + to: { height: 'var(--qwikui-collapsible-content-height)' }, + }, + 'collapsible-up': { + from: { height: 'var(--qwikui-collapsible-content-height)' }, + to: { height: '0' }, + }, + }, + fontFamily: { + sans: ['Inter Variable', 'sans-serif'], + }, + }, }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', - }, - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', - }, - }, - borderRadius: { - base: 'var(--border-radius)', - sm: 'calc(var(--border-radius) + 0.125rem)', - DEFAULT: 'calc(var(--border-radius) + 0.25rem)', - md: 'calc(var(--border-radius) + 0.375rem)', - lg: 'calc(var(--border-radius) + 0.5rem)', - xl: 'calc(var(--border-radius) + 0.75rem)', - '2xl': 'calc(var(--border-radius) + 1rem)', - '3xl': 'calc(var(--border-radius) + 1.5rem)', - }, - borderWidth: { - base: 'var(--border-width)', - DEFAULT: 'calc(var(--border-width) + 1px)', - 2: 'calc(var(--border-width) + 2px)', - 4: 'calc(var(--border-width) + 4px)', - 8: 'calc(var(--border-width) + 8px)', - }, - boxShadow: { - base: 'var(--shadow-base)', - sm: 'var(--shadow-sm)', - DEFAULT: 'var(--shadow)', - md: 'var(--shadow-md)', - lg: 'var(--shadow-lg)', - xl: 'var(--shadow-xl)', - '2xl': 'var(--shadow-2xl)', - inner: 'var(--shadow-inner)', - }, - strokeWidth: { - 0: '0', - base: 'var(--stroke-width)', - 1: 'calc(var(--stroke-width) + 1px)', - 2: 'calc(var(--stroke-width) + 2px)', - }, - animation: { - 'accordion-up': 'collapsible-up 0.2s ease-out 0s 1 normal forwards', - 'accordion-down': 'collapsible-down 0.2s ease-out 0s 1 normal forwards', - }, - keyframes: { - 'collapsible-down': { - from: { height: '0' }, - to: { height: 'var(--qwikui-collapsible-content-height)' }, - }, - 'collapsible-up': { - from: { height: 'var(--qwikui-collapsible-content-height)' }, - to: { height: '0' }, - }, - }, - fontFamily: { - sans: ['Inter Variable', 'sans-serif'], - }, - }, - }, -}; -" -`); + }; + " + `); }); test(` @@ -220,123 +220,123 @@ module.exports = { const updatedTailwindConfigContent = tree.read('tailwind.config.js', 'utf-8'); expect(updatedTailwindConfigContent).toMatchInlineSnapshot(` -"import plugin from 'tailwindcss/plugin'; - -/** @type {import('tailwindcss').Config} */ -export default { - plugins: [ - require('tailwindcss-animate'), - plugin(function ({ addUtilities }) { - addUtilities({ - '.press': { - transform: 'var(--transform-press)', - }, - }); - }), - ], - - content: [join(__dirname, 'src/**/*.{js,ts,jsx,tsx,mdx}')], - darkMode: 'class', - theme: { - screens: { - sm: '640px', - md: '768px', - lg: '1024px', - xl: '1280px', - '2xl': '1536px', - }, - important: true, - extend: { - colors: { - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', - }, - alert: { - DEFAULT: 'hsl(var(--alert))', - foreground: 'hsl(var(--alert-foreground))', - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', - }, - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', + "import plugin from 'tailwindcss/plugin'; + + /** @type {import('tailwindcss').Config} */ + export default { + plugins: [ + require('tailwindcss-animate'), + plugin(function ({ addUtilities }) { + addUtilities({ + '.press': { + transform: 'var(--transform-press)', + }, + }); + }), + ], + + content: [join(__dirname, 'src/**/*.{js,ts,jsx,tsx,mdx}')], + darkMode: 'class', + theme: { + screens: { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + }, + important: true, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + alert: { + DEFAULT: 'hsl(var(--alert))', + foreground: 'hsl(var(--alert-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + }, + borderRadius: { + base: 'var(--border-radius)', + sm: 'calc(var(--border-radius) + 0.125rem)', + DEFAULT: 'calc(var(--border-radius) + 0.25rem)', + md: 'calc(var(--border-radius) + 0.375rem)', + lg: 'calc(var(--border-radius) + 0.5rem)', + xl: 'calc(var(--border-radius) + 0.75rem)', + '2xl': 'calc(var(--border-radius) + 1rem)', + '3xl': 'calc(var(--border-radius) + 1.5rem)', + }, + borderWidth: { + base: 'var(--border-width)', + DEFAULT: 'calc(var(--border-width) + 1px)', + 2: 'calc(var(--border-width) + 2px)', + 4: 'calc(var(--border-width) + 4px)', + 8: 'calc(var(--border-width) + 8px)', + }, + boxShadow: { + base: 'var(--shadow-base)', + sm: 'var(--shadow-sm)', + DEFAULT: 'var(--shadow)', + md: 'var(--shadow-md)', + lg: 'var(--shadow-lg)', + xl: 'var(--shadow-xl)', + '2xl': 'var(--shadow-2xl)', + inner: 'var(--shadow-inner)', + }, + strokeWidth: { + 0: '0', + base: 'var(--stroke-width)', + 1: 'calc(var(--stroke-width) + 1px)', + 2: 'calc(var(--stroke-width) + 2px)', + }, + animation: { + 'accordion-up': 'collapsible-up 0.2s ease-out 0s 1 normal forwards', + 'accordion-down': 'collapsible-down 0.2s ease-out 0s 1 normal forwards', + }, + keyframes: { + 'collapsible-down': { + from: { height: '0' }, + to: { height: 'var(--qwikui-collapsible-content-height)' }, + }, + 'collapsible-up': { + from: { height: 'var(--qwikui-collapsible-content-height)' }, + to: { height: '0' }, + }, + }, + fontFamily: { + sans: ['Inter Variable', 'sans-serif'], + }, + }, }, - }, - borderRadius: { - base: 'var(--border-radius)', - sm: 'calc(var(--border-radius) + 0.125rem)', - DEFAULT: 'calc(var(--border-radius) + 0.25rem)', - md: 'calc(var(--border-radius) + 0.375rem)', - lg: 'calc(var(--border-radius) + 0.5rem)', - xl: 'calc(var(--border-radius) + 0.75rem)', - '2xl': 'calc(var(--border-radius) + 1rem)', - '3xl': 'calc(var(--border-radius) + 1.5rem)', - }, - borderWidth: { - base: 'var(--border-width)', - DEFAULT: 'calc(var(--border-width) + 1px)', - 2: 'calc(var(--border-width) + 2px)', - 4: 'calc(var(--border-width) + 4px)', - 8: 'calc(var(--border-width) + 8px)', - }, - boxShadow: { - base: 'var(--shadow-base)', - sm: 'var(--shadow-sm)', - DEFAULT: 'var(--shadow)', - md: 'var(--shadow-md)', - lg: 'var(--shadow-lg)', - xl: 'var(--shadow-xl)', - '2xl': 'var(--shadow-2xl)', - inner: 'var(--shadow-inner)', - }, - strokeWidth: { - 0: '0', - base: 'var(--stroke-width)', - 1: 'calc(var(--stroke-width) + 1px)', - 2: 'calc(var(--stroke-width) + 2px)', - }, - animation: { - 'accordion-up': 'collapsible-up 0.2s ease-out 0s 1 normal forwards', - 'accordion-down': 'collapsible-down 0.2s ease-out 0s 1 normal forwards', - }, - keyframes: { - 'collapsible-down': { - from: { height: '0' }, - to: { height: 'var(--qwikui-collapsible-content-height)' }, - }, - 'collapsible-up': { - from: { height: 'var(--qwikui-collapsible-content-height)' }, - to: { height: '0' }, - }, - }, - fontFamily: { - sans: ['Inter Variable', 'sans-serif'], - }, - }, - }, -}; -" -`); + }; + " + `); }); test(` GIVEN tailwind config has already a plugins array @@ -352,123 +352,123 @@ export default { const updatedTailwindConfigContent = tree.read('tailwind.config.js', 'utf-8'); expect(updatedTailwindConfigContent).toMatchInlineSnapshot(` -"import plugin from 'tailwindcss/plugin'; - -/** @type {import('tailwindcss').Config} */ -export default { - plugins: [ - require('tailwindcss-animate'), - plugin(function ({ addUtilities }) { - addUtilities({ - '.press': { - transform: 'var(--transform-press)', + "import plugin from 'tailwindcss/plugin'; + + /** @type {import('tailwindcss').Config} */ + export default { + plugins: [ + require('tailwindcss-animate'), + plugin(function ({ addUtilities }) { + addUtilities({ + '.press': { + transform: 'var(--transform-press)', + }, + }); + }), + somePlugin, + ], + content: [join(__dirname, 'src/**/*.{js,ts,jsx,tsx,mdx}')], + darkMode: 'class', + theme: { + screens: { + sm: '640px', + md: '768px', + lg: '1024px', + xl: '1280px', + '2xl': '1536px', + }, + important: true, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + alert: { + DEFAULT: 'hsl(var(--alert))', + foreground: 'hsl(var(--alert-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + }, + borderRadius: { + base: 'var(--border-radius)', + sm: 'calc(var(--border-radius) + 0.125rem)', + DEFAULT: 'calc(var(--border-radius) + 0.25rem)', + md: 'calc(var(--border-radius) + 0.375rem)', + lg: 'calc(var(--border-radius) + 0.5rem)', + xl: 'calc(var(--border-radius) + 0.75rem)', + '2xl': 'calc(var(--border-radius) + 1rem)', + '3xl': 'calc(var(--border-radius) + 1.5rem)', + }, + borderWidth: { + base: 'var(--border-width)', + DEFAULT: 'calc(var(--border-width) + 1px)', + 2: 'calc(var(--border-width) + 2px)', + 4: 'calc(var(--border-width) + 4px)', + 8: 'calc(var(--border-width) + 8px)', + }, + boxShadow: { + base: 'var(--shadow-base)', + sm: 'var(--shadow-sm)', + DEFAULT: 'var(--shadow)', + md: 'var(--shadow-md)', + lg: 'var(--shadow-lg)', + xl: 'var(--shadow-xl)', + '2xl': 'var(--shadow-2xl)', + inner: 'var(--shadow-inner)', + }, + strokeWidth: { + 0: '0', + base: 'var(--stroke-width)', + 1: 'calc(var(--stroke-width) + 1px)', + 2: 'calc(var(--stroke-width) + 2px)', + }, + animation: { + 'accordion-up': 'collapsible-up 0.2s ease-out 0s 1 normal forwards', + 'accordion-down': 'collapsible-down 0.2s ease-out 0s 1 normal forwards', + }, + keyframes: { + 'collapsible-down': { + from: { height: '0' }, + to: { height: 'var(--qwikui-collapsible-content-height)' }, + }, + 'collapsible-up': { + from: { height: 'var(--qwikui-collapsible-content-height)' }, + to: { height: '0' }, + }, + }, + fontFamily: { + sans: ['Inter Variable', 'sans-serif'], + }, + }, }, - }); - }), - somePlugin, - ], - content: [join(__dirname, 'src/**/*.{js,ts,jsx,tsx,mdx}')], - darkMode: 'class', - theme: { - screens: { - sm: '640px', - md: '768px', - lg: '1024px', - xl: '1280px', - '2xl': '1536px', - }, - important: true, - extend: { - colors: { - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', - }, - alert: { - DEFAULT: 'hsl(var(--alert))', - foreground: 'hsl(var(--alert-foreground))', - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', - }, - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', - }, - }, - borderRadius: { - base: 'var(--border-radius)', - sm: 'calc(var(--border-radius) + 0.125rem)', - DEFAULT: 'calc(var(--border-radius) + 0.25rem)', - md: 'calc(var(--border-radius) + 0.375rem)', - lg: 'calc(var(--border-radius) + 0.5rem)', - xl: 'calc(var(--border-radius) + 0.75rem)', - '2xl': 'calc(var(--border-radius) + 1rem)', - '3xl': 'calc(var(--border-radius) + 1.5rem)', - }, - borderWidth: { - base: 'var(--border-width)', - DEFAULT: 'calc(var(--border-width) + 1px)', - 2: 'calc(var(--border-width) + 2px)', - 4: 'calc(var(--border-width) + 4px)', - 8: 'calc(var(--border-width) + 8px)', - }, - boxShadow: { - base: 'var(--shadow-base)', - sm: 'var(--shadow-sm)', - DEFAULT: 'var(--shadow)', - md: 'var(--shadow-md)', - lg: 'var(--shadow-lg)', - xl: 'var(--shadow-xl)', - '2xl': 'var(--shadow-2xl)', - inner: 'var(--shadow-inner)', - }, - strokeWidth: { - 0: '0', - base: 'var(--stroke-width)', - 1: 'calc(var(--stroke-width) + 1px)', - 2: 'calc(var(--stroke-width) + 2px)', - }, - animation: { - 'accordion-up': 'collapsible-up 0.2s ease-out 0s 1 normal forwards', - 'accordion-down': 'collapsible-down 0.2s ease-out 0s 1 normal forwards', - }, - keyframes: { - 'collapsible-down': { - from: { height: '0' }, - to: { height: 'var(--qwikui-collapsible-content-height)' }, - }, - 'collapsible-up': { - from: { height: 'var(--qwikui-collapsible-content-height)' }, - to: { height: '0' }, - }, - }, - fontFamily: { - sans: ['Inter Variable', 'sans-serif'], - }, - }, - }, -}; -" -`); + }; + " + `); }); test(` @@ -483,91 +483,91 @@ export default { const updatedGlobalCssContent = tree.read('src/global.css', 'utf-8'); expect(updatedGlobalCssContent).toMatchInlineSnapshot(` -"@tailwind components; -@tailwind base; -@tailwind utilities; -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 47.4% 11.2%; - --card: 0 0% 100%; - --card-foreground: 222.2 47.4% 11.2%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --primary: 191.6 91.4% 36.5%; - --primary-foreground: 0 0% 100%; - --secondary: 222.2 47.4% 11.2%; - --secondary-foreground: 0 0% 100%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --alert: 0 84.2% 60.2%; - --alert-foreground: 210 40% 98%; - --ring: 222.2 47.4% 11.2%; - --border-width: 0px; - --border-radius: 0; - --shadow-base: 0 1px 2px 0 rgba(0, 0, 0, 0.01); - --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - --shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1), 0 1px 5px 0px rgba(0, 0, 0, 0.1); - --shadow-md: - 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); - --shadow-lg: - 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); - --shadow-xl: - 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); - --shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 1); - --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.01); - --transform-press: scale(0.95); - } - .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --primary: 191.6 91.4% 36.5%; - --primary-foreground: 0 0% 100%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 0 0% 0%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --alert: 0 84.2% 60.2%; - --alert-foreground: 210 40% 98%; - --ring: 212.7 26.8% 83.9; - --border-width: 0px; - --border-radius: 0; - --shadow-base: 0 1px 2px 0 rgba(0, 0, 0, 0.01); - --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - --shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1), 0 1px 5px 0px rgba(0, 0, 0, 0.1); - --shadow-md: - 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); - --shadow-lg: - 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); - --shadow-xl: - 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); - --shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 1); - --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.01); - --transform-press: scale(0.95); - } -} - -html { - height: 100%; - min-height: 100%; - scroll-behavior: smooth; - background-color: var(--color-bg) !important; - color: var(--color-text) !important; -} -" -`); + "@tailwind components; + @tailwind base; + @tailwind utilities; + @layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 47.4% 11.2%; + --card: 0 0% 100%; + --card-foreground: 222.2 47.4% 11.2%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --primary: 191.6 91.4% 36.5%; + --primary-foreground: 0 0% 100%; + --secondary: 222.2 47.4% 11.2%; + --secondary-foreground: 0 0% 100%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --alert: 0 84.2% 60.2%; + --alert-foreground: 210 40% 98%; + --ring: 222.2 47.4% 11.2%; + --border-width: 0px; + --border-radius: 0; + --shadow-base: 0 1px 2px 0 rgba(0, 0, 0, 0.01); + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1), 0 1px 5px 0px rgba(0, 0, 0, 0.1); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -2px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -4px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), + 0 8px 10px -6px rgba(0, 0, 0, 0.1); + --shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 1); + --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.01); + --transform-press: scale(0.95); + } + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --primary: 191.6 91.4% 36.5%; + --primary-foreground: 0 0% 100%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 0 0% 0%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --alert: 0 84.2% 60.2%; + --alert-foreground: 210 40% 98%; + --ring: 212.7 26.8% 83.9; + --border-width: 0px; + --border-radius: 0; + --shadow-base: 0 1px 2px 0 rgba(0, 0, 0, 0.01); + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1), 0 1px 5px 0px rgba(0, 0, 0, 0.1); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -2px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -4px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), + 0 8px 10px -6px rgba(0, 0, 0, 0.1); + --shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 1); + --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.01); + --transform-press: scale(0.95); + } + } + + html { + height: 100%; + min-height: 100%; + scroll-behavior: smooth; + background-color: var(--color-bg) !important; + color: var(--color-text) !important; + } + " + `); }); test(` GIVEN style is "brutalist" and primary color is "red-600" and border-radius is 1 @@ -584,84 +584,84 @@ html { const updatedGlobalCssContent = tree.read('src/global.css', 'utf-8'); expect(updatedGlobalCssContent).toMatchInlineSnapshot(` -"@tailwind components; -@tailwind base; -@tailwind utilities; -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 47.4% 11.2%; - --card: 0 0% 100%; - --card-foreground: 222.2 47.4% 11.2%; - --border: 0 0% 0%; - --input: 0 0% 0%; - --primary: 0 72.2% 50.6%; - --primary-foreground: 0 0% 100%; - --secondary: 222.2 47.4% 11.2%; - --secondary-foreground: 0 0% 100%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --alert: 0 84.2% 60.2%; - --alert-foreground: 210 40% 98%; - --ring: 0 0% 0%; - --border-width: 2px; - --border-radius: 1rem; - --shadow-base: 0px 0px 0px 0 rgba(0, 0, 0, 1); - --shadow-sm: 4px 4px 0px 0 rgba(0, 0, 0, 1); - --shadow: 5px 5px 0px 0px rgba(0, 0, 0, 1); - --shadow-md: 6px 6px 0px 0px rgba(0, 0, 0, 1); - --shadow-lg: 8px 8px 0px 0px rgba(0, 0, 0, 1); - --shadow-xl: 11px 11px 0px 0px rgba(0, 0, 0, 1); - --shadow-2xl: 13px 13px 0px 0px rgba(0, 0, 0, 1); - --shadow-inner: inset 2px 2px 0px 0px rgba(0, 0, 0, 0); - --transform-press: translate(4px, 4px); - } - .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --border: 0 0% 0%; - --input: 0 0% 0%; - --primary: 0 72.2% 50.6%; - --primary-foreground: 0 0% 100%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 0 0% 0%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --alert: 0 84.2% 60.2%; - --alert-foreground: 210 40% 98%; - --ring: 0 0% 0%; - --border-width: 2px; - --border-radius: 1rem; - --shadow-base: 0px 0px 0px 0 rgba(0, 0, 0, 1); - --shadow-sm: 4px 4px 0px 0 rgba(0, 0, 0, 1); - --shadow: 5px 5px 0px 0px rgba(0, 0, 0, 1); - --shadow-md: 6px 6px 0px 0px rgba(0, 0, 0, 1); - --shadow-lg: 8px 8px 0px 0px rgba(0, 0, 0, 1); - --shadow-xl: 11px 11px 0px 0px rgba(0, 0, 0, 1); - --shadow-2xl: 13px 13px 0px 0px rgba(0, 0, 0, 1); - --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.01); - --transform-press: translate(4px, 4px); - } -} - -html { - height: 100%; - min-height: 100%; - scroll-behavior: smooth; - background-color: var(--color-bg) !important; - color: var(--color-text) !important; -} -" -`); + "@tailwind components; + @tailwind base; + @tailwind utilities; + @layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 47.4% 11.2%; + --card: 0 0% 100%; + --card-foreground: 222.2 47.4% 11.2%; + --border: 0 0% 0%; + --input: 0 0% 0%; + --primary: 0 72.2% 50.6%; + --primary-foreground: 0 0% 100%; + --secondary: 222.2 47.4% 11.2%; + --secondary-foreground: 0 0% 100%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --alert: 0 84.2% 60.2%; + --alert-foreground: 210 40% 98%; + --ring: 0 0% 0%; + --border-width: 2px; + --border-radius: 1rem; + --shadow-base: 0px 0px 0px 0 rgba(0, 0, 0, 1); + --shadow-sm: 4px 4px 0px 0 rgba(0, 0, 0, 1); + --shadow: 5px 5px 0px 0px rgba(0, 0, 0, 1); + --shadow-md: 6px 6px 0px 0px rgba(0, 0, 0, 1); + --shadow-lg: 8px 8px 0px 0px rgba(0, 0, 0, 1); + --shadow-xl: 11px 11px 0px 0px rgba(0, 0, 0, 1); + --shadow-2xl: 13px 13px 0px 0px rgba(0, 0, 0, 1); + --shadow-inner: inset 2px 2px 0px 0px rgba(0, 0, 0, 0); + --transform-press: translate(4px, 4px); + } + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --border: 0 0% 0%; + --input: 0 0% 0%; + --primary: 0 72.2% 50.6%; + --primary-foreground: 0 0% 100%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 0 0% 0%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --alert: 0 84.2% 60.2%; + --alert-foreground: 210 40% 98%; + --ring: 0 0% 0%; + --border-width: 2px; + --border-radius: 1rem; + --shadow-base: 0px 0px 0px 0 rgba(0, 0, 0, 1); + --shadow-sm: 4px 4px 0px 0 rgba(0, 0, 0, 1); + --shadow: 5px 5px 0px 0px rgba(0, 0, 0, 1); + --shadow-md: 6px 6px 0px 0px rgba(0, 0, 0, 1); + --shadow-lg: 8px 8px 0px 0px rgba(0, 0, 0, 1); + --shadow-xl: 11px 11px 0px 0px rgba(0, 0, 0, 1); + --shadow-2xl: 13px 13px 0px 0px rgba(0, 0, 0, 1); + --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.01); + --transform-press: translate(4px, 4px); + } + } + + html { + height: 100%; + min-height: 100%; + scroll-behavior: smooth; + background-color: var(--color-bg) !important; + color: var(--color-text) !important; + } + " + `); }); }); diff --git a/packages/kit-headless/src/components/switch/index.ts b/packages/kit-headless/src/components/switch/index.ts new file mode 100644 index 000000000..fb371bbcf --- /dev/null +++ b/packages/kit-headless/src/components/switch/index.ts @@ -0,0 +1,3 @@ +export { SwitchRoot as Root } from './switch-root'; +export { SwitchInput as Input, SwitchThumb as Thumb } from './switch-input'; +export { SwitchLabel as Label } from './switch-label'; diff --git a/packages/kit-headless/src/components/switch/switch-context.tsx b/packages/kit-headless/src/components/switch/switch-context.tsx new file mode 100644 index 000000000..80fec7a3c --- /dev/null +++ b/packages/kit-headless/src/components/switch/switch-context.tsx @@ -0,0 +1,20 @@ +import { createContextId, QRL, type Signal } from '@builder.io/qwik'; + +export interface SwitchState { + 'bind:checked'?: Signal; + checked?: boolean; + disabled?: boolean; + onChange$?: QRL<(event: MouseEvent | KeyboardEvent) => void>; + onClick$?: QRL<(event: MouseEvent | KeyboardEvent) => void>; + onKeyPress$?: QRL<(event: KeyboardEvent) => void>; + autoFocus?: boolean; +} +// +export type SwitchContextState = Omit & { + bindChecked: Signal; + switchRef?: Signal; + disabled?: Signal; + autoFocus?: Signal; +}; + +export const SwitchContext = createContextId('SwitchContext'); diff --git a/packages/kit-headless/src/components/switch/switch-input.tsx b/packages/kit-headless/src/components/switch/switch-input.tsx new file mode 100644 index 000000000..d80208bc6 --- /dev/null +++ b/packages/kit-headless/src/components/switch/switch-input.tsx @@ -0,0 +1,65 @@ +import { + component$, + PropsOf, + useContext, + useId, + $, + useSignal, + Slot, +} from '@builder.io/qwik'; +import { SwitchContext } from './switch-context'; + +export interface SwitchInputProps extends PropsOf<'input'> { + thumbClassName?: string; +} + +export const SwitchInput = component$((props) => { + const context = useContext(SwitchContext); + const switchRef = useSignal(); + const id = useId(); + + const handleClick$ = $(() => { + if (context.disabled?.value) { + return; + } + context.bindChecked.value = !context.bindChecked.value; + }); + + return ( +
+ + +
+ ); +}); + +export interface SwitchThumbProps { + className?: string; +} + +export const SwitchThumb = component$(({ className }) => { + return ; +}); diff --git a/packages/kit-headless/src/components/switch/switch-label.tsx b/packages/kit-headless/src/components/switch/switch-label.tsx new file mode 100644 index 000000000..01eaf3ec2 --- /dev/null +++ b/packages/kit-headless/src/components/switch/switch-label.tsx @@ -0,0 +1,9 @@ +import { component$, PropsOf, Slot, useId } from '@builder.io/qwik'; +export const SwitchLabel = component$>((rest) => { + const id = useId(); + return ( + + ); +}); diff --git a/packages/kit-headless/src/components/switch/switch-root.tsx b/packages/kit-headless/src/components/switch/switch-root.tsx new file mode 100644 index 000000000..2275659ce --- /dev/null +++ b/packages/kit-headless/src/components/switch/switch-root.tsx @@ -0,0 +1,49 @@ +import { + component$, + Slot, + useContextProvider, + useSignal, + type PropsOf, + useStyles$, +} from '@builder.io/qwik'; +import { + type SwitchContextState, + type SwitchState, + SwitchContext, +} from './switch-context'; +import styles from './switch.css?inline'; +import { useBoundSignal } from '../../utils/bound-signal'; + +export type SwitchProps = PropsOf<'div'> & SwitchState; + +export const SwitchRoot = component$( + ({ checked, disabled, onChange$, autoFocus, ...rest }: SwitchProps) => { + useStyles$(styles); + const defaultChecked = useBoundSignal(rest['bind:checked'], checked); + const bindChecked = useSignal(defaultChecked.value || false); + const switchRef = useSignal(); + const isDisabled = useSignal(disabled); + const isAutoFocus = useSignal(autoFocus); + const context: SwitchContextState = { + checked, + disabled: isDisabled, + bindChecked, + onChange$, + switchRef, + autoFocus: isAutoFocus + }; + + useContextProvider(SwitchContext, context); + + return ( +
+ +
+ ); + }, +); diff --git a/packages/kit-headless/src/components/switch/switch.css b/packages/kit-headless/src/components/switch/switch.css new file mode 100644 index 000000000..9e0a8da51 --- /dev/null +++ b/packages/kit-headless/src/components/switch/switch.css @@ -0,0 +1,69 @@ +@layer qwik-ui { + [data-switch-track] { + position: relative; + padding: 2px; + border-radius: 4rem; + background: hsla(var(--switch-track-color-inactive)); + + } + [data-switch-track] > input { + inline-size: 4rem; + block-size: 2rem; + border-radius: 4rem; + appearance: none; + pointer-events: none; + touch-action: pan-y; + border: none; + outline-offset: 5px; + box-sizing: content-box; + flex-shrink: 0; + display: grid; + align-items: center; + grid: [track] 1fr / [track] 1fr; + transition: background-color 0.25s ease; + } + [data-switch-track] > input:checked { + background: hsla(var(--primary)); + } + [data-switch-track]:has(> input:checked) { + background: hsla(var(--primary)); + } + + [data-switch-track]:not(:disabled):hover > [data-switch-thumb] { + box-shadow: 0 0 0 0.5rem hsla(var(--switch-thumb-color-highlight)); + } + + [data-switch-track] > input:checked ~ [data-switch-thumb] { + transform: translateX(100%); + } + + [data-switch-track]:has(> input:disabled) { + cursor: not-allowed; + opacity: 0.35; + } + [data-switch-track] > input:disabled ~ [data-switch-thumb] { + cursor: not-allowed; + box-shadow: inset 0 0 0 2px hsl(0 0% 100% / 10%) !important; + } + + [data-switch-track]:focus { + outline: 2px solid hsl(var(--primary)); + outline-offset: 2px; + } + + + [data-switch-track] > [data-switch-thumb] { + position: absolute; + top: 2px; + left: 2px; + display: inline-block; + cursor: pointer; + pointer-events: auto; + inline-size: 2rem; + block-size: 2rem; + background: hsla(var(--background)); + box-shadow: 0 0 0 0 hsla(var(--switch-thumb-color-highlight)); + border-radius: 50%; + transform: translateX(0%); + } +} diff --git a/packages/kit-headless/src/components/switch/switch.driver.ts b/packages/kit-headless/src/components/switch/switch.driver.ts new file mode 100644 index 000000000..980193f5e --- /dev/null +++ b/packages/kit-headless/src/components/switch/switch.driver.ts @@ -0,0 +1,36 @@ +import { type Locator, type Page } from '@playwright/test'; +type OpenKeys = 'ArrowUp' | 'Enter' | 'Space' | 'ArrowDown'; +export type DriverLocator = Locator | Page; + +export function createTestDriver(rootLocator: T) { + const getRoot = () => { + return rootLocator; + }; + + const getInputElement = () => { + return getRoot().locator('[data-qui-switch-input]'); + }; + + const getTriggerLabel = () => { + return getRoot().locator('[data-switch-label]'); + }; + + const openListbox = async (key: OpenKeys | 'click') => { + await getInputElement().focus(); + + if (key !== 'click') { + await getInputElement().press(key); + } else { + await getInputElement().click(); + } + }; + + return { + ...rootLocator, + locator: rootLocator, + getRoot, + getInputElement, + openListbox, + getTriggerLabel, + }; +} diff --git a/packages/kit-headless/src/components/switch/switch.test.ts b/packages/kit-headless/src/components/switch/switch.test.ts new file mode 100644 index 000000000..f22ef50b6 --- /dev/null +++ b/packages/kit-headless/src/components/switch/switch.test.ts @@ -0,0 +1,160 @@ +import { type Page, test, expect } from '@playwright/test'; +import { createTestDriver } from './switch.driver'; + +declare global { + interface Window { + onChangeTriggered: boolean; + onChangeHandler: () => void; + } +} +async function setup(page: Page, exampleName: string) { + await page.goto(`/headless/switch/${exampleName}`); + + const driver = createTestDriver(page.locator('[data-qui-switch]')); + + return { + driver, + }; +} + +test.describe('Mouse Behavior', () => { + test(`GIVEN a hero switch + WHEN checking data attributes and properties + THEN data-checked and checked property should match`, async ({ page }) => { + const { driver: d } = await setup(page, 'hero'); + await expect(d.getInputElement()).toHaveAttribute('aria-label', 'switch'); + await expect(d.getInputElement()).toHaveAttribute('data-checked', 'false'); + await expect(d.getInputElement()).toHaveAttribute('data-disabled', 'false'); + await expect(d.getInputElement()).toHaveAttribute('aria-describedby', expect.stringMatching(/switch$/)); + await expect(d.getInputElement()).not.toBeDisabled(); + await expect(d.getInputElement()).toHaveAttribute('aria-checked', 'false'); + await expect(d.getInputElement()).not.toBeChecked(); + // type + await expect(d.getInputElement()).toHaveAttribute('type', 'checkbox'); + // role + await expect(d.getInputElement()).toHaveAttribute('role', 'switch'); + }) + test(`GIVEN a hero switch + WHEN toggled + THEN the checked property should correctly reflect the toggle state`, async ({ + page, + }) => { + const { driver: d } = await setup(page, 'hero'); + await expect(d.getInputElement()).not.toBeChecked(); + await expect(d.getInputElement()).toHaveAttribute('aria-describedby', expect.stringMatching(/switch$/)); + await expect(d.getInputElement()).toHaveAttribute('aria-label', 'switch'); + await expect(d.getInputElement()).toHaveAttribute('data-checked', 'false'); + await expect(d.getInputElement()).toHaveAttribute('data-disabled', 'false'); + await d.getInputElement().click(); + await expect(d.getInputElement()).toHaveAttribute('data-checked', 'true'); + await expect(d.getInputElement()).toHaveAttribute('data-disabled', 'false'); + await expect(d.getInputElement()).toBeChecked(); + // type + await expect(d.getInputElement()).toHaveAttribute('type', 'checkbox'); + // role + await expect(d.getInputElement()).toHaveAttribute('role', 'switch'); + }); +}); + +test.describe('Keyboard Behavior', () => { + test(`GIVEN a hero switch + WHEN focusing the trigger and pressing the Enter key + THEN the checked property should toggle`, async ({ page }) => { + const { driver: d } = await setup(page, 'hero'); + await d.getInputElement().focus(); + await expect(d.getInputElement()).not.toBeChecked(); + await expect(d.getInputElement()).toHaveAttribute('aria-describedby', expect.stringMatching(/switch$/)); + await expect(d.getInputElement()).toHaveAttribute('aria-label', 'switch'); + await expect(d.getInputElement()).toHaveAttribute('data-checked', 'false'); + await expect(d.getInputElement()).toHaveAttribute('data-disabled', 'false'); + await d.getInputElement().press('Enter'); + await expect(d.getInputElement()).toBeChecked(); + await expect(d.getInputElement()).toHaveAttribute('data-checked', 'true'); + await d.getInputElement().press('Enter'); + await expect(d.getInputElement()).not.toBeChecked(); + }); + + test(`GIVEN a hero switch + WHEN focusing the trigger and pressing the Space key + THEN the checked property should toggle`, async ({ page }) => { + const { driver: d } = await setup(page, 'hero'); + await d.getInputElement().focus(); + await expect(d.getInputElement()).not.toBeChecked(); + await expect(d.getInputElement()).toHaveAttribute('aria-describedby', expect.stringMatching(/switch$/)); + await expect(d.getInputElement()).toHaveAttribute('aria-label', 'switch'); + await expect(d.getInputElement()).toHaveAttribute('data-checked', 'false'); + await expect(d.getInputElement()).toHaveAttribute('data-disabled', 'false'); + await d.getInputElement().press(' '); + await expect(d.getInputElement()).toBeChecked(); + await expect(d.getInputElement()).toHaveAttribute('data-checked', 'true'); + await d.getInputElement().press(' '); + await expect(d.getInputElement()).not.toBeChecked(); + }); + + test.describe('Default property ', () => { + test(` + GIVEN a checked switch + WHEN the switch is mounted + THEN the switch should be checked + `, async ({ page }) => { + const { driver: d } = await setup(page, 'checked'); + await expect(d.getInputElement()).toBeChecked(); + await expect(d.getInputElement()).toHaveAttribute('aria-describedby', expect.stringMatching(/switch$/)); + await expect(d.getInputElement()).toHaveAttribute('aria-label', 'switch'); + await expect(d.getInputElement()).toHaveAttribute('data-checked', 'true'); + await expect(d.getInputElement()).toHaveAttribute('data-disabled', 'false'); + }); + + test(` + GIVEN a switch that is initially checked + WHEN the switch is mounted + THEN the switch should be checked + `, async ({ page }) => { + const { driver: d } = await setup(page, 'defaultChecked'); + await expect(d.getInputElement()).toBeChecked(); + await expect(d.getInputElement()).toHaveAttribute('aria-describedby', expect.stringMatching(/switch$/)); + await expect(d.getInputElement()).toHaveAttribute('aria-label', 'switch'); + await expect(d.getInputElement()).toHaveAttribute('data-checked', 'true'); + await d.getInputElement().click(); + await expect(d.getInputElement()).toHaveAttribute('data-checked', 'false'); + await expect(d.getInputElement()).toHaveAttribute('data-disabled', 'false'); + await expect(d.getInputElement()).not.toBeChecked() + + }); + + test(` + GIVEN a disabled switch + WHEN the switch is mounted + THEN the switch should be disabled + `, async ({ page }) => { + const { driver: d } = await setup(page, 'disabled'); + await expect(d.getInputElement()).toHaveAttribute('data-disabled', 'true'); + await expect(d.getInputElement()).toHaveAttribute('aria-describedby', expect.stringMatching(/switch$/)); + await expect(d.getInputElement()).toHaveAttribute('aria-label', 'switch'); + await expect(d.getInputElement()).toBeDisabled(); + }); + + test(` + GIVEN a disabled switch + WHEN clicking the switch + THEN the switch should not toggle`, async ({ page }) => { + const { driver: d } = await setup(page, 'disabled'); + await expect(d.getInputElement()).toHaveAttribute('aria-describedby', expect.stringMatching(/switch$/)); + await expect(d.getInputElement()).toHaveAttribute('data-disabled', 'true'); + await d.getInputElement().click(); + await expect(d.getInputElement()).not.toBeChecked(); + }); + + test(` + GIVEN a switch without a label + WHEN the switch is mounted + THEN it should have a default label`, async ({ page }) => { + const { driver: d } = await setup(page, 'pure'); + await expect(d.getTriggerLabel()).not.toBeNull(); + await expect(d.getInputElement()).toHaveAttribute('data-disabled', 'false'); + await expect(d.getInputElement()).toHaveAttribute('aria-describedby', expect.stringMatching(/switch$/)); + await expect(d.getInputElement()).toHaveAttribute('data-test', '11'); + }); + + }); +}); diff --git a/packages/kit-headless/src/index.ts b/packages/kit-headless/src/index.ts index 86198fa29..8e8ddd3a5 100644 --- a/packages/kit-headless/src/index.ts +++ b/packages/kit-headless/src/index.ts @@ -19,3 +19,4 @@ export * as Tooltip from './components/tooltip'; export * as Dropdown from './components/dropdown'; export * as Combobox from './components/combobox'; export { Polymorphic } from './components/polymorphic'; +export * as Switch from './components/switch'; diff --git a/packages/kit-styled/src/components/switch/switch.tsx b/packages/kit-styled/src/components/switch/switch.tsx new file mode 100644 index 000000000..c9ca9674e --- /dev/null +++ b/packages/kit-styled/src/components/switch/switch.tsx @@ -0,0 +1,42 @@ +import { type PropsOf, component$, Slot } from '@builder.io/qwik'; +import { Switch as HeadlessSwitch } from '@qwik-ui/headless'; +import { cn } from '@qwik-ui/utils'; + + + +const Root = component$>(({ ...props }) => { + return ( + + + + ); +}); + +const Label = component$>(({ ...props }) => { + return ( + + + + ); +}); + +const Input = component$>(({ ...props }) => { + return ( + + ); +}); + +export const Switch = { + Root, + Label, + Input, +}; diff --git a/packages/kit-styled/src/index.ts b/packages/kit-styled/src/index.ts index 6c2b24bf7..1746808e8 100644 --- a/packages/kit-styled/src/index.ts +++ b/packages/kit-styled/src/index.ts @@ -21,3 +21,4 @@ export * from './components/textarea/textarea'; export * from './components/toggle/toggle'; export * from './components/toggle-group/toggle-group'; export * from './components/dropdown/dropdown'; +export * from './components/switch/switch'; diff --git a/packages/kit-styled/src/templates/global.css b/packages/kit-styled/src/templates/global.css index 715f1dbb1..679d63e7c 100644 --- a/packages/kit-styled/src/templates/global.css +++ b/packages/kit-styled/src/templates/global.css @@ -36,9 +36,13 @@ --alert: 0 84.2% 60.2%; --alert-foreground: 210 40% 98%; --ring: 222.2 47.4% 11.2%; + --switch-thumb-color-highlight: 0, 0%, 72%, 0.25; + --switch-track-color-inactive: 80 0% 80%; } .dark { + --switch-thumb-color-highlight: 0, 0%, 100%, 0.25; + --switch-track-color-inactive: 240, 10%, 50%; --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; @@ -1362,7 +1366,7 @@ body { min-height: 100%; } -/* Utilities layer for animations. The current arbitrary & docs tailwind animation guidelines are not maintainable long term. +/* Utilities layer for animations. The current arbitrary & docs tailwind animation guidelines are not maintainable long term. It would make more sense to supply the user with the animation declaration in the docs. */ @layer utilities { diff --git a/packages/kit-styled/src/templates/tailwind.config.cjs b/packages/kit-styled/src/templates/tailwind.config.cjs index 3d7d99030..1a1d5ec60 100644 --- a/packages/kit-styled/src/templates/tailwind.config.cjs +++ b/packages/kit-styled/src/templates/tailwind.config.cjs @@ -13,12 +13,26 @@ module.exports = { plugins: [ // PLUGIN-START require('tailwindcss-animate'), - plugin(function ({ addUtilities }) { + plugin(function ({ addUtilities,theme,e }) { addUtilities({ '.press': { transform: 'var(--transform-press)', }, }); + const sizelist = theme('spacing'); + const blockSizeUtilities = Object.keys(sizelist).reduce((acc, key) => { + const value = sizelist[key]; + acc[`.${e(`block-size-${key}`)}`] = { + 'block-size': value, + }; + acc[`.${e(`inline-size-${key}`)}`] = { + 'inline-size': value, + }; + return acc; + }, {}); + + addUtilities(blockSizeUtilities, ['responsive', 'hover']); + }), // PLUGIN-END ], @@ -35,6 +49,8 @@ module.exports = { ring: 'hsl(var(--ring))', background: 'hsl(var(--background))', foreground: 'hsl(var(--foreground))', + switchInactive: 'hsl(var(--switch-track-color-inactive))', + switchThumb: 'hsl(var(--switch-thumb-color-highlight))', primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))',