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))',