diff --git a/.changeset/poor-spiders-clap.md b/.changeset/poor-spiders-clap.md
new file mode 100644
index 000000000..72193b11f
--- /dev/null
+++ b/.changeset/poor-spiders-clap.md
@@ -0,0 +1,6 @@
+---
+'@qwik-ui/headless': major
+'@qwik-ui/styled': major
+---
+
+add a new switch component
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 d622123a7..8843c6635 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%;
@@ -1363,7 +1367,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..357d0b839
--- /dev/null
+++ b/apps/website/src/routes/docs/headless/switch/examples/checked.tsx
@@ -0,0 +1,15 @@
+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..becde2c6d
--- /dev/null
+++ b/apps/website/src/routes/docs/headless/switch/examples/defaultChecked.tsx
@@ -0,0 +1,12 @@
+import { component$, useSignal } from '@builder.io/qwik';
+import { Switch } from '@qwik-ui/headless';
+
+export default component$(() => {
+ const checked = useSignal(false);
+ return (
+
+ test
+
+
+ );
+});
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..7ad36833f
--- /dev/null
+++ b/apps/website/src/routes/docs/headless/switch/examples/disabled.tsx
@@ -0,0 +1,15 @@
+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..13aff7b3b
--- /dev/null
+++ b/apps/website/src/routes/docs/headless/switch/examples/hero.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);
+ const count = useSignal(0);
+ useStyles$(styles);
+
+ return (
+ count.value++}>
+ test{count.value}
+
+
+ );
+});
+
+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..2ef7b344e
--- /dev/null
+++ b/apps/website/src/routes/docs/headless/switch/index.mdx
@@ -0,0 +1,143 @@
+---
+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.
+
+## 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..683e4ec7a
--- /dev/null
+++ b/apps/website/src/routes/docs/headless/switch/snippets/switch.css
@@ -0,0 +1,67 @@
+/* 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;
+ & > input {
+ --thumb-position: 0%;
+ --thumb-transition-duration: 0.25s;
+ padding: 4px;
+ background: hsla(var(--switch-track-color-inactive));
+ 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;
+
+ &::before {
+ content: '';
+ 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%);
+ }
+
+ &:not(:disabled):hover::before {
+ box-shadow: 0 0 0 0.5rem hsla(var(--switch-thumb-color-highlight));
+ }
+
+ &:checked {
+ background: hsla(var(--primary));
+ &::before {
+ transform: translateX(100%);
+ }
+ }
+ &:disabled {
+ cursor: not-allowed;
+ opacity: 0.35;
+ &::before {
+ cursor: not-allowed;
+ box-shadow: inset 0 0 0 2px hsl(0 0% 100% / 10%);
+ }
+ }
+
+ &:focus {
+ outline: 2px solid hsl(var(--primary));
+ outline-offset: 2px;
+ }
+ }
+}
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/hero.tsx b/apps/website/src/routes/docs/styled/switch/examples/hero.tsx
new file mode 100644
index 000000000..8d207392e
--- /dev/null
+++ b/apps/website/src/routes/docs/styled/switch/examples/hero.tsx
@@ -0,0 +1,14 @@
+import { component$, useSignal } from '@builder.io/qwik';
+import { Switch } from '~/components/ui';
+
+export default component$(() => {
+ const checked = useSignal(false);
+ return (
+ <>
+
+ test
+
+
+ >
+ );
+});
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..6729a3c2c
--- /dev/null
+++ b/apps/website/src/routes/docs/styled/switch/index.mdx
@@ -0,0 +1,48 @@
+---
+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
+
+### Checked
+
+
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/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 12bfa6e36..47548765b 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;
@@ -84,126 +84,7 @@ 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))',
- },
- 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'],
- },
- },
- },
- };
- "
- `);
+ expect(updatedTailwindConfigContent).toMatchSnapshot();
});
test(`
@@ -219,124 +100,7 @@ html {
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))',
- },
- },
- 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'],
- },
- },
- },
- };
- "
- `);
+ expect(updatedTailwindConfigContent).toMatchSnapshot()
});
test(`
GIVEN tailwind config has already a plugins array
@@ -351,124 +115,7 @@ html {
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)',
- },
- });
- }),
- 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'],
- },
- },
- },
- };
- "
- `);
+ expect(updatedTailwindConfigContent).toMatchSnapshot()
});
test(`
@@ -482,92 +129,8 @@ 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: 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;
- }
- "
- `);
+ expect(updatedGlobalCssContent).toMatchSnapshot();
+
});
test(`
GIVEN style is "brutalist" and primary color is "red-600" and border-radius is 1
@@ -583,85 +146,7 @@ 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;
- }
- "
- `);
+ expect(updatedGlobalCssContent).toMatchSnapshot()
+
});
});
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..7af34d0d5
--- /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 } from './switch-input';
+export { SwitchLable as Label } from './switch-lable';
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..cf47fe652
--- /dev/null
+++ b/packages/kit-headless/src/components/switch/switch-context.tsx
@@ -0,0 +1,17 @@
+import { createContextId, QRL, type Signal } from '@builder.io/qwik';
+
+export interface SwitchState {
+ 'bind:checked': Signal;
+ defaultChecked?: boolean;
+ disabled?: boolean;
+ onChange$?: QRL<(checked: boolean, event: MouseEvent | KeyboardEvent) => void>;
+ onClick$?: QRL<(checked: boolean, event: MouseEvent | KeyboardEvent) => void>;
+ autoFocus?: boolean;
+}
+//
+export type SwitchContextState = Omit & {
+ bindChecked: Signal;
+ switchRef?: 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..b451e2bc5
--- /dev/null
+++ b/packages/kit-headless/src/components/switch/switch-input.tsx
@@ -0,0 +1,63 @@
+import { component$, PropsOf, sync$, useContext, useId, $ } from '@builder.io/qwik';
+import { SwitchContext } from './switch-context';
+export const SwitchInput = component$>((rest) => {
+ const context = useContext(SwitchContext);
+ const id = useId();
+ if (context.defaultChecked && context.bindChecked && !context.bindChecked.value) {
+ context.bindChecked.value = !context.bindChecked.value;
+ }
+
+ if (context.autoFocus && !context.switchRef?.value) {
+ context.switchRef?.value?.focus();
+ }
+
+ const handleClick$ = $((e: MouseEvent | KeyboardEvent) => {
+ const keys = ['Enter', ' '];
+ if (
+ (e as KeyboardEvent)?.key !== undefined &&
+ !keys.includes((e as KeyboardEvent).key)
+ ) {
+ return;
+ }
+ // keycode
+
+ context.switchRef?.value?.focus();
+ context.bindChecked.value = !context.bindChecked.value;
+ if (context.onChange$) {
+ context.onChange$(context.bindChecked.value, e);
+ }
+
+ if (context.onClick$) {
+ context.onClick$(context.bindChecked.value, e);
+ }
+ });
+ const handleClickSync$ = sync$((e: MouseEvent) => {
+ e.preventDefault();
+ });
+
+ const handleKeyPressSync$ = sync$((e: KeyboardEvent) => {
+ const keys = ['Enter', ' '];
+ if (keys.includes(e.key)) {
+ e.preventDefault();
+ }
+ });
+
+ return (
+
+ );
+});
diff --git a/packages/kit-headless/src/components/switch/switch-lable.tsx b/packages/kit-headless/src/components/switch/switch-lable.tsx
new file mode 100644
index 000000000..0eb0504b4
--- /dev/null
+++ b/packages/kit-headless/src/components/switch/switch-lable.tsx
@@ -0,0 +1,9 @@
+import { component$, PropsOf, Slot, useId } from '@builder.io/qwik';
+export const SwitchLable = 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..b58fc909b
--- /dev/null
+++ b/packages/kit-headless/src/components/switch/switch-root.tsx
@@ -0,0 +1,39 @@
+import {
+ component$,
+ Slot,
+ useContextProvider,
+ useSignal,
+ type PropsOf,
+} from '@builder.io/qwik';
+import {
+ type SwitchContextState,
+ type SwitchState,
+ SwitchContext,
+} from './switch-context';
+export type SwitchProps = PropsOf<'div'> & SwitchState;
+
+export const SwitchRoot = component$(
+ ({ defaultChecked, disabled, onChange$, ...rest }: SwitchProps) => {
+ const switchRef = useSignal();
+ const context: SwitchContextState = {
+ defaultChecked,
+ disabled,
+ bindChecked: rest['bind:checked'],
+ onChange$: onChange$,
+ switchRef: switchRef,
+ };
+
+ useContextProvider(SwitchContext, context);
+
+ return (
+
+
+
+ );
+ },
+);
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..295a2ee67
--- /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 getTrigger = () => {
+ return getRoot().locator('[data-value]');
+ };
+
+ const getTriggerlaBle = () => {
+ return getRoot().locator('[data-switch-lable]');
+ };
+
+ const openListbox = async (key: OpenKeys | 'click') => {
+ await getTrigger().focus();
+
+ if (key !== 'click') {
+ await getTrigger().press(key);
+ } else {
+ await getTrigger().click();
+ }
+ };
+
+ return {
+ ...rootLocator,
+ locator: rootLocator,
+ getRoot,
+ getTrigger,
+ openListbox,
+ getTriggerlaBle,
+ };
+}
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..1d010ceec
--- /dev/null
+++ b/packages/kit-headless/src/components/switch/switch.test.ts
@@ -0,0 +1,122 @@
+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 toggled
+ THEN the checked property should correctly reflect the toggle state`, async ({
+ page,
+ }) => {
+ const { driver: d } = await setup(page, 'hero');
+ await expect(d.getTrigger()).not.toBeChecked();
+ await expect(d.getTrigger()).toHaveAttribute('data-checked', 'flase');
+ await d.getTrigger().click();
+ await expect(d.getTrigger()).toHaveAttribute('data-checked', 'true');
+ await expect(d.getTrigger()).toBeChecked();
+ });
+
+ test(`GIVEN a hero switch
+ WHEN clicked
+ THEN the onChange callback should be triggered`, async ({ page }) => {
+ const { driver: d } = await setup(page, 'hero');
+ await expect(d.getTriggerlaBle()).toHaveText('0');
+ await d.getTrigger().click();
+ await expect(d.getTriggerlaBle()).toHaveText('1');
+ await expect(d.getTrigger()).toBeChecked();
+ });
+});
+
+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.getTrigger().focus();
+ await expect(d.getTrigger()).not.toBeChecked();
+ await d.getTrigger().press('Enter');
+ await expect(d.getTrigger()).toBeChecked();
+ await d.getTrigger().press('Enter');
+ await expect(d.getTrigger()).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.getTrigger().focus();
+ await expect(d.getTrigger()).not.toBeChecked();
+ await d.getTrigger().press(' ');
+ await expect(d.getTrigger()).toBeChecked();
+ await d.getTrigger().press(' ');
+ await expect(d.getTrigger()).not.toBeChecked();
+ });
+
+ test(`
+ GIVEN a hero switch
+ WHEN focusing the trigger and pressing the Tab key
+ THEN the checked property should not toggle`, async ({ page }) => {
+ const { driver: d } = await setup(page, 'hero');
+ await d.getTrigger().focus();
+ await expect(d.getTrigger()).not.toBeChecked();
+ await d.getTrigger().press('Tab');
+ await expect(d.getTrigger()).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.getTrigger()).toBeChecked();
+ await expect(d.getTrigger()).toHaveAttribute('data-checked', 'true');
+ await expect(d.getTrigger()).toHaveAttribute('aria-checked', 'true');
+ });
+
+ test(`
+ GIVEN a defaultChecked switch
+ WHEN the switch is mounted
+ THEN the switch should be checked
+ `, async ({ page }) => {
+ const { driver: d } = await setup(page, 'defaultChecked');
+ await expect(d.getTrigger()).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.getTrigger()).toHaveAttribute('data-disabled', 'true');
+ await expect(d.getTrigger()).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.getTrigger()).toHaveAttribute('data-disabled', 'true');
+ await d.getTrigger().click();
+ await expect(d.getTrigger()).not.toBeChecked();
+ });
+});
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..61f9bf727
--- /dev/null
+++ b/packages/kit-styled/src/components/switch/switch.tsx
@@ -0,0 +1,54 @@
+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 d622123a7..8843c6635 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%;
@@ -1363,7 +1367,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))',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 20247193b..523b2d2bb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1490,54 +1490,63 @@ packages:
engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-arm@1.0.2':
resolution: {integrity: sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==}
engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.0.2':
resolution: {integrity: sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==}
engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-x64@1.0.2':
resolution: {integrity: sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==}
engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.0.2':
resolution: {integrity: sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==}
engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.0.2':
resolution: {integrity: sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==}
engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-linux-arm64@0.33.4':
resolution: {integrity: sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==}
engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-arm@0.33.4':
resolution: {integrity: sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==}
engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-s390x@0.33.4':
resolution: {integrity: sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==}
engines: {glibc: '>=2.31', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-x64@0.33.4':
resolution: {integrity: sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==}
@@ -1549,12 +1558,14 @@ packages:
engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-linuxmusl-x64@0.33.4':
resolution: {integrity: sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==}
engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-wasm32@0.33.4':
resolution: {integrity: sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==}
@@ -1901,48 +1912,56 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@nx/nx-linux-arm64-gnu@19.4.2':
resolution: {integrity: sha512-6gbBak/bL4vEV2aoTFc7VaeWYF+ossJ0YOqx+hwLpv9SSt6e3yIJrqf7SiwdKq0lcoPeHq3DO06+bRzNLZxVTQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@nx/nx-linux-arm64-musl@19.1.1':
resolution: {integrity: sha512-lhyVsuT19Ez4ynhen6dT+Zdq2cABXcphYSkVSASvZGvka/65AS+0D1hX0TFDPJvbTdsHwVszJQZzIqGmYUkhLA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@nx/nx-linux-arm64-musl@19.4.2':
resolution: {integrity: sha512-JKc3Bw84jWbOhlqXGBIH9/qz3kzTwpKfsIqtar8K8Gd5/UFJS8GLEdy0mXsnoeFrA1DuYJJ0PWxoHkAa1MYLxg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@nx/nx-linux-x64-gnu@19.1.1':
resolution: {integrity: sha512-zUQhMwz/gQ0up1iymwTqXbyLJca87HXOP+uAD5wfgarh0yhPDwcGaVsV8O8t2z8W/dH/yYmuppe3gAwsvd5SSg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@nx/nx-linux-x64-gnu@19.4.2':
resolution: {integrity: sha512-hyf0cDZ3rAM8WERZ/M82v1rnf6oO1X+xwYq363Qx04SufU+Knto7xHGndLNkx2i18+UtCoEr4ZhDYrIb8ZWHww==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@nx/nx-linux-x64-musl@19.1.1':
resolution: {integrity: sha512-3Gc2iwMbFAp50OlIqfgryTtZno/FqPW+AOP1Pijo/jJOZ8DHP3A7Zy8QoJYUgTQxCffzVbhshXW6yy403pV3OQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@nx/nx-linux-x64-musl@19.4.2':
resolution: {integrity: sha512-XbKut3RTb04FNA0diDhO/OM8DgqaWaaXhyybRocfhITxH+mPQBZPUs/NM3xeQCrzlGjwrBYxt+Y9Ep8Ftgd/MA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@nx/nx-win32-arm64-msvc@19.1.1':
resolution: {integrity: sha512-91LJG0triTdZDHnT9l1N1YuIwhmR7iCbKsEv345OdPhHJeQ6GAuJCD0SqDk6aZ13xr7LoRlS8c6bnfctXeslQQ==}
@@ -2108,46 +2127,55 @@ packages:
resolution: {integrity: sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.18.0':
resolution: {integrity: sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==}
cpu: [arm]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.18.0':
resolution: {integrity: sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.18.0':
resolution: {integrity: sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-powerpc64le-gnu@4.18.0':
resolution: {integrity: sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.18.0':
resolution: {integrity: sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-s390x-gnu@4.18.0':
resolution: {integrity: sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.18.0':
resolution: {integrity: sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.18.0':
resolution: {integrity: sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.18.0':
resolution: {integrity: sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==}
@@ -2269,24 +2297,28 @@ packages:
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@swc/core-linux-arm64-musl@1.5.24':
resolution: {integrity: sha512-vd2/hfOBGbrX21FxsFdXCUaffjkHvlZkeE2UMRajdXifwv79jqOHIJg3jXG1F3ZrhCghCzirFts4tAZgcG8XWg==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@swc/core-linux-x64-gnu@1.5.24':
resolution: {integrity: sha512-Zrdzi7NqzQxm2BvAG5KyOSBEggQ7ayrxh599AqqevJmsUXJ8o2nMiWQOBvgCGp7ye+Biz3pvZn1EnRzAp+TpUg==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@swc/core-linux-x64-musl@1.5.24':
resolution: {integrity: sha512-1F8z9NRi52jdZQCGc5sflwYSctL6omxiVmIFVp8TC9nngjQKc00TtX/JC2Eo2HwvgupkFVl5YQJidAck9YtmJw==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@swc/core-win32-arm64-msvc@1.5.24':
resolution: {integrity: sha512-cKpP7KvS6Xr0jFSTBXY53HZX/YfomK5EMQYpCVDOvfsZeYHN20sQSKXfpVLvA/q2igVt1zzy1XJcOhpJcgiKLg==}
@@ -9688,6 +9720,12 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1
+ '@nrwl/devkit@19.1.1(nx@19.1.1(@swc-node/register@1.9.1(@swc/core@1.5.24(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.4.5))(@swc/core@1.5.24(@swc/helpers@0.5.11)))':
+ dependencies:
+ '@nx/devkit': 19.1.1(nx@19.1.1(@swc-node/register@1.9.1(@swc/core@1.5.24(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.4.5))(@swc/core@1.5.24(@swc/helpers@0.5.11)))
+ transitivePeerDependencies:
+ - nx
+
'@nrwl/devkit@19.1.1(nx@19.4.2(@swc-node/register@1.9.1(@swc/core@1.5.24(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.4.5))(@swc/core@1.5.24(@swc/helpers@0.5.11)))':
dependencies:
'@nx/devkit': 19.1.1(nx@19.4.2(@swc-node/register@1.9.1(@swc/core@1.5.24(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.4.5))(@swc/core@1.5.24(@swc/helpers@0.5.11)))
@@ -9883,7 +9921,7 @@ snapshots:
'@nx/devkit@19.1.1(nx@19.1.1(@swc-node/register@1.9.1(@swc/core@1.5.24(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.4.5))(@swc/core@1.5.24(@swc/helpers@0.5.11)))':
dependencies:
- '@nrwl/devkit': 19.1.1(nx@19.4.2(@swc-node/register@1.9.1(@swc/core@1.5.24(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.4.5))(@swc/core@1.5.24(@swc/helpers@0.5.11)))
+ '@nrwl/devkit': 19.1.1(nx@19.1.1(@swc-node/register@1.9.1(@swc/core@1.5.24(@swc/helpers@0.5.11))(@swc/types@0.1.7)(typescript@5.4.5))(@swc/core@1.5.24(@swc/helpers@0.5.11)))
ejs: 3.1.10
enquirer: 2.3.6
ignore: 5.3.1