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