diff --git a/README.md b/README.md index 601f6ef0..2296e856 100644 --- a/README.md +++ b/README.md @@ -124,10 +124,10 @@ export default component$(() => { - Accordion - Alert - Avatar -- Checkbox - Badge - Breadcrumb - Button +- Checkbox - Drawer - Dropdown - Footer @@ -135,14 +135,16 @@ export default component$(() => { - Jumbotron - Modal - Navbar +- Radio - Rating +- Select - Sidebar - Spinner - Tabs - Toast - Toggle -## Composables +## Composables / hooks - useToasts - useDark diff --git a/apps/web/src/examples.ts b/apps/web/src/examples.ts index 19c35581..0762365f 100644 --- a/apps/web/src/examples.ts +++ b/apps/web/src/examples.ts @@ -797,6 +797,32 @@ export const examples: Record = { height: '400', }, ], + radio: [ + { + title: 'Default', + description: 'Use the default example of a radio component with the checked and unchecked state.', + url: '/examples/[theme-rtl]/radio/01-default', + content: + 'import { component$, useSignal } from \'@builder.io/qwik\'\nimport { Radio } from \'flowbite-qwik\'\n\nexport default component$(() => {\n const pick = useSignal(\'\')\n\n return (\n <>\n
\n \n First option\n \n \n Second option\n \n
\n \n )\n})', + height: '200', + }, + { + title: 'Color', + description: 'This example can be used for the color of the radio component by applying the color attribute to the input element.', + url: '/examples/[theme-rtl]/radio/02-colors', + content: + 'import { component$, useSignal } from \'@builder.io/qwik\'\nimport { Radio } from \'flowbite-qwik\'\n\nexport default component$(() => {\n const pick = useSignal(\'blue\')\n\n return (\n <>\n

Picked color : {pick.value}

\n
\n \n Blue\n \n \n Purple\n \n \n Red\n \n \n Green\n \n \n Pink\n \n
\n \n )\n})', + height: '200', + }, + { + title: 'Disabled', + description: 'This example can be used for the disabled state of the radio component by applying the disabled attribute to the input element.', + url: '/examples/[theme-rtl]/radio/03-disabled', + content: + 'import { component$, useSignal } from \'@builder.io/qwik\'\nimport { Radio } from \'flowbite-qwik\'\n\nexport default component$(() => {\n const pick = useSignal(\'\')\n\n return (\n <>\n
\n \n First option\n \n \n Second option\n \n
\n \n )\n})', + height: '200', + }, + ], rating: [ { title: 'Default rating', @@ -831,6 +857,48 @@ export const examples: Record = { height: '200', }, ], + select: [ + { + title: 'Default', + description: 'Get started with the default example of a select input component to get a single option selection.', + url: '/examples/[theme-rtl]/select/01-default', + content: + "import { component$, useSignal } from '@builder.io/qwik'\nimport { Select } from 'flowbite-qwik'\n\nexport default component$(() => {\n const selected = useSignal('')\n const countries = [\n { value: 'us', name: 'United States' },\n { value: 'ca', name: 'Canada' },\n { value: 'fr', name: 'France' },\n ]\n\n return (\n <>\n
\n \n
\n \n )\n})", + height: '200', + }, + { + title: 'Selected option', + description: 'Use this example to get a single option selection with the selected state of the select input component.', + url: '/examples/[theme-rtl]/select/03-selected', + content: + "import { component$, useSignal } from '@builder.io/qwik'\nimport { Select } from 'flowbite-qwik'\n\nexport default component$(() => {\n const selected = useSignal('fr')\n const countries = [\n { value: 'us', name: 'United States' },\n { value: 'ca', name: 'Canada' },\n { value: 'fr', name: 'France' },\n ]\n\n return (\n <>\n
\n \n \n
\n \n )\n})', + height: '200', + }, + { + title: 'Underline', + description: 'Use the underline style for the select component as an alternative appearance.', + url: '/examples/[theme-rtl]/select/05-underline', + content: + "import { component$, useSignal } from '@builder.io/qwik'\nimport { Select } from 'flowbite-qwik'\n\nexport default component$(() => {\n const selected = useSignal('')\n const countries = [\n { value: 'us', name: 'United States' },\n { value: 'ca', name: 'Canada' },\n { value: 'fr', name: 'France' },\n ]\n\n return (\n <>\n
\n +
+ + ) +}) + +export const onStaticGenerate: StaticGenerateHandler = async () => { + return staticGenerateHandler() +} diff --git a/apps/web/src/routes/examples/[theme-rtl]/select/02-disabled/index@examples.tsx b/apps/web/src/routes/examples/[theme-rtl]/select/02-disabled/index@examples.tsx new file mode 100644 index 00000000..98573089 --- /dev/null +++ b/apps/web/src/routes/examples/[theme-rtl]/select/02-disabled/index@examples.tsx @@ -0,0 +1,30 @@ +/** + * title: Disabled + * description: Apply the disable state to the select component to disallow the selection of new options. + */ + +import { component$, useSignal } from '@builder.io/qwik' +import { staticGenerateHandler } from '~/routes/examples/[theme-rtl]/layout' +import { StaticGenerateHandler } from '@builder.io/qwik-city' +import { Select } from 'flowbite-qwik' + +export default component$(() => { + const selected = useSignal('') + const countries = [ + { value: 'us', name: 'United States' }, + { value: 'ca', name: 'Canada' }, + { value: 'fr', name: 'France' }, + ] + + return ( + <> +
+ +
+ + ) +}) + +export const onStaticGenerate: StaticGenerateHandler = async () => { + return staticGenerateHandler() +} diff --git a/apps/web/src/routes/examples/[theme-rtl]/select/04-sizes/index@examples.tsx b/apps/web/src/routes/examples/[theme-rtl]/select/04-sizes/index@examples.tsx new file mode 100644 index 00000000..5caefa4d --- /dev/null +++ b/apps/web/src/routes/examples/[theme-rtl]/select/04-sizes/index@examples.tsx @@ -0,0 +1,32 @@ +/** + * title: Sizes + * description: Get started with the small, default, and large sizes for the select component from the example below. + */ + +import { component$, useSignal } from '@builder.io/qwik' +import { staticGenerateHandler } from '~/routes/examples/[theme-rtl]/layout' +import { StaticGenerateHandler } from '@builder.io/qwik-city' +import { Select } from 'flowbite-qwik' + +export default component$(() => { + const selected = useSignal('') + const countries = [ + { value: 'us', name: 'United States' }, + { value: 'ca', name: 'Canada' }, + { value: 'fr', name: 'France' }, + ] + + return ( + <> +
+ + +
+ + ) +}) + +export const onStaticGenerate: StaticGenerateHandler = async () => { + return staticGenerateHandler() +} diff --git a/packages/lib/src/components/Radio/Radio.tsx b/packages/lib/src/components/Radio/Radio.tsx new file mode 100644 index 00000000..9da680d3 --- /dev/null +++ b/packages/lib/src/components/Radio/Radio.tsx @@ -0,0 +1,40 @@ +import { ClassList, QRL, Slot, component$, useComputed$, Signal } from '@builder.io/qwik' +import clsx from 'clsx' +import { twMerge } from 'tailwind-merge' +import { FlowbiteTheme } from '../FlowbiteThemable' +import { useRadioClasses } from './composables/use-radio-classes' + +type CheckboxProps = { + disabled?: boolean + name: string + color?: FlowbiteTheme + value: string + class?: ClassList + 'bind:option': Signal + onChange$?: QRL<(value: string) => void> +} + +export const Radio = component$(({ disabled = false, color, name, class: classNames, onChange$, ...props }) => { + const internalColor = useComputed$(() => color) + const { radioClasses, labelClasses } = useRadioClasses(internalColor) + + return ( + + ) +}) diff --git a/packages/lib/src/components/Radio/composables/use-radio-classes.ts b/packages/lib/src/components/Radio/composables/use-radio-classes.ts new file mode 100644 index 00000000..8963d93c --- /dev/null +++ b/packages/lib/src/components/Radio/composables/use-radio-classes.ts @@ -0,0 +1,30 @@ +import { Signal, useComputed$ } from '@builder.io/qwik' +import { twMerge } from 'tailwind-merge' +import { FlowbiteTheme, useFlowbiteThemable } from '~/components/FlowbiteThemable' + +// LABEL +const defaultLabelClasses = 'block text-sm font-medium text-gray-900 dark:text-gray-300' + +// RADIO +const defaultRadioClasses = 'focus:ring-2' + +const radioClassesByTheme: Record = { + blue: 'text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600', + green: + 'text-green-600 bg-gray-100 border-gray-300 focus:ring-green-500 dark:focus:ring-green-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600', + pink: 'text-pink-600 bg-gray-100 border-gray-300 focus:ring-pink-500 dark:focus:ring-pink-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600', + purple: + 'text-purple-600 bg-gray-100 border-gray-300 focus:ring-purple-500 dark:focus:ring-purple-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600', + red: 'text-red-600 bg-gray-100 border-gray-300 focus:ring-red-500 dark:focus:ring-red-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600', +} + +export function useRadioClasses(color: Signal) { + const { themeName } = useFlowbiteThemable() + const radioClasses = useComputed$(() => twMerge(defaultRadioClasses, radioClassesByTheme[color.value ?? themeName.value])) + const labelClasses = useComputed$(() => defaultLabelClasses) + + return { + radioClasses, + labelClasses, + } +} diff --git a/packages/lib/src/components/Radio/index.ts b/packages/lib/src/components/Radio/index.ts new file mode 100644 index 00000000..b2d70f72 --- /dev/null +++ b/packages/lib/src/components/Radio/index.ts @@ -0,0 +1 @@ +export { Radio } from './Radio' diff --git a/packages/lib/src/components/Select/Select.tsx b/packages/lib/src/components/Select/Select.tsx new file mode 100644 index 00000000..7d5757c2 --- /dev/null +++ b/packages/lib/src/components/Select/Select.tsx @@ -0,0 +1,53 @@ +import { ClassList, PropsOf, Signal, component$, useComputed$, JSXOutput } from '@builder.io/qwik' +import { OptionsType } from './select-types' +import { useSelectClasses } from './composables/use-select-classes' +import { InputSize, ValidationStatus } from '~/index' + +type SelectProps = PropsOf<'select'> & { + label?: string + 'bind:value': Signal + options: OptionsType[] + class?: ClassList + placeholder?: string + disabled?: boolean + underline?: boolean + size?: InputSize + validationStatus?: ValidationStatus + validationMessage?: JSXOutput + helper?: JSXOutput +} + +export const Select = component$(({ label, options, class: classNames, ...props }) => { + const validationStatus = useComputed$(() => props.validationStatus) + const size = useComputed$(() => props.size ?? ('md' as InputSize)) + const disabled = useComputed$(() => props.disabled ?? false) + const underline = useComputed$(() => props.underline ?? false) + + const { labelClasses, selectClasses, validationWrapperClasses } = useSelectClasses({ + size, + disabled, + underline, + validationStatus, + }) + + return ( + <> + + + {!!props.validationMessage &&

{props.validationMessage}

} + {!!props.helper &&

{props.helper}

} + + ) +}) diff --git a/packages/lib/src/components/Select/composables/use-select-classes.ts b/packages/lib/src/components/Select/composables/use-select-classes.ts new file mode 100644 index 00000000..d70ccf1d --- /dev/null +++ b/packages/lib/src/components/Select/composables/use-select-classes.ts @@ -0,0 +1,80 @@ +import { twMerge } from 'tailwind-merge' +import { Signal, useComputed$ } from '@builder.io/qwik' +import { InputSize, ValidationStatus, validationStatusMap } from '~/index' + +// LABEL +const baseLabelClasses = 'block mb-2 text-sm font-medium' + +// INPUT +const defaultSelectClasses = + 'w-full text-gray-900 bg-gray-50 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500' +const disabledSelectClasses = 'cursor-not-allowed bg-gray-100' +const underlineSelectClasses = + 'bg-transparent dark:bg-transparent border-0 border-b-2 border-gray-200 appearance-none dark:border-gray-700 focus:outline-none focus:ring-0 focus:border-gray-200 peer' +const selectSizeClasses: Record = { + lg: 'p-4', + md: 'p-2.5 text-sm', + sm: 'p-2 text-sm', +} + +const successInputClasses = + 'bg-green-50 border-green-500 dark:border-green-500 text-green-900 dark:text-green-400 placeholder-green-700 dark:placeholder-green-500 focus:ring-green-500 focus:border-green-500' +const errorInputClasses = + 'bg-red-50 border-red-500 text-red-900 placeholder-red-700 focus:ring-red-500 focus:border-red-500 dark:text-red-500 dark:placeholder-red-500 dark:border-red-500' + +export type UseSelectClassesProps = { + size: Signal + disabled: Signal + underline: Signal + validationStatus: Signal +} + +export function useSelectClasses(props: UseSelectClassesProps): { + selectClasses: Signal + labelClasses: Signal + validationWrapperClasses: Signal +} { + const selectClasses = useComputed$(() => { + const vs = props.validationStatus.value + + const classByStatus = vs === validationStatusMap.Success ? successInputClasses : vs === validationStatusMap.Error ? errorInputClasses : '' + + const underlineByStatus = + vs === validationStatusMap.Success ? 'focus:border-green-500' : vs === validationStatusMap.Error ? 'focus:border-red-500' : '' + + return twMerge( + defaultSelectClasses, + classByStatus, + selectSizeClasses[props.size.value], + props.disabled.value && disabledSelectClasses, + props.underline.value ? underlineSelectClasses : 'border border-gray-300 rounded-lg', + props.underline.value && underlineByStatus, + ) + }) + + const labelClasses = useComputed$(() => { + const vs = props.validationStatus.value + const classByStatus = + vs === validationStatusMap.Success + ? 'text-green-700 dark:text-green-500' + : vs === validationStatusMap.Error + ? 'text-red-700 dark:text-red-500' + : 'text-gray-900 dark:text-white' + + return twMerge(baseLabelClasses, classByStatus) + }) + + const validationWrapperClasses = useComputed$(() => + twMerge( + 'mt-2 text-sm', + props.validationStatus.value === validationStatusMap.Success ? 'text-green-600 dark:text-green-500' : '', + props.validationStatus.value === validationStatusMap.Error ? 'text-red-600 dark:text-red-500' : '', + ), + ) + + return { + selectClasses, + labelClasses, + validationWrapperClasses, + } +} diff --git a/packages/lib/src/components/Select/index.ts b/packages/lib/src/components/Select/index.ts new file mode 100644 index 00000000..1ea02a43 --- /dev/null +++ b/packages/lib/src/components/Select/index.ts @@ -0,0 +1,2 @@ +export { Select } from './Select' +export * from './select-types' diff --git a/packages/lib/src/components/Select/select-types.ts b/packages/lib/src/components/Select/select-types.ts new file mode 100644 index 00000000..09894b61 --- /dev/null +++ b/packages/lib/src/components/Select/select-types.ts @@ -0,0 +1,4 @@ +export type OptionsType = { + name: string + value: string +} diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts index bfdd42d6..b25f8585 100644 --- a/packages/lib/src/index.ts +++ b/packages/lib/src/index.ts @@ -16,7 +16,9 @@ export * from './components/Input' export * from './components/Jumbotron' export * from './components/Modal' export * from './components/Navbar' +export * from './components/Radio' export * from './components/Rating' +export * from './components/Select' export * from './components/Sidebar' export * from './components/Spinner' export * from './components/Tabs'