[READ-ONLY] Mirror of https://github.com/flo-bit/ui-kit. 🦊 fox ui, svelte 5 and tailwind 4 flo-bit.dev/ui-kit/
svelte tailwindcss ui-components
0

Configure Feed

Select the types of activity you want to include in your feed.

0.0.6, add first extra components

Florian (Mar 19, 2025, 10:29 PM +0100) fae2a3c6 6bf44adf

+696 -425
+4 -4
README.md
··· 16 16 npx sv create my-project 17 17 ``` 18 18 19 - ### 2. Install fox-ui-svelte 19 + ### 2. Install fuchs 20 20 21 21 ```bash 22 - npm install fox-ui-svelte 22 + npm install fuchs 23 23 ``` 24 24 25 25 ### 3. set theme variables in your app.css (changing `gray` and `blue` to your preferred colors, using find and replace). 26 26 27 27 ```css 28 - @source "../node_modules/fox-ui-svelte"; 28 + @source "../node_modules/fuchs"; 29 29 30 30 @theme { 31 31 --color-base-50: var(--color-gray-50); ··· 58 58 59 59 ```svelte 60 60 <script> 61 - import { Button } from 'fox-ui-svelte'; 61 + import { Button } from 'fuchs'; 62 62 </script> 63 63 64 64 <Button onclick={() => alert('clicked')}>Click me</Button>
+1 -1
package.json
··· 1 1 { 2 - "name": "duckui", 2 + "name": "fuchs", 3 3 "version": "0.0.5", 4 4 "type": "module", 5 5 "description": "Beautiful UI components built with Tailwind 4 and Svelte 5",
+9 -1
src/lib/index.ts
··· 1 1 export { cn } from '$lib/utils'; 2 2 3 - // place files you want to import through the `$lib` alias in this folder. 3 + // base components 4 4 export { Accordion, AccordionItem } from '$lib/components/base/accordion'; 5 5 export { Alert, AlertDescription, AlertTitle } from '$lib/components/base/alert'; 6 6 export { Avatar, AvatarGroup } from '$lib/components/base/avatar'; ··· 26 26 export { Text as Paragraph } from '$lib/components/base/text/'; 27 27 export { Textarea } from '$lib/components/base/textarea'; 28 28 export { Tooltip } from '$lib/components/base/tooltip'; 29 + 30 + // extra components 31 + export { ColorPicker, PopoverColorPicker } from '$lib/components/extra/color-picker/'; 32 + export { default as Excalidraw } from '$lib/components/extra/excalidraw/Excalidraw.svelte'; 33 + export { default as Undraw } from '$lib/components/extra/undraw/Undraw.svelte'; 34 + 35 + // 3d components 36 + export { default as ModelPicker } from '$lib/components/3d/model-picker/ModelPicker.svelte';
+15 -33
src/docs/preview/ColorPickerPreview.svelte
··· 4 4 import { cn } from '$lib/utils'; 5 5 import Subheading from '$lib/components/base/heading/Subheading.svelte'; 6 6 import Button from '$lib/components/base/button/Button.svelte'; 7 - import type { SetColorFunction } from '$lib/components/extra/color-picker/base/color'; 7 + import PopoverColorPicker from '$lib/components/extra/color-picker/popover/PopoverColorPicker.svelte'; 8 + import { ThemeWatcher } from '$lib/helper/ThemeWatcher.svelte'; 9 + import { hex_to_rgb, oklch_string_to_oklch, oklch_to_rgb } from '$lib/components/extra/color-picker/base/color'; 8 10 9 - let setColor: SetColorFunction | undefined = $state(undefined); 10 - 11 - let okchl = $state({ 12 - l: 0, 13 - c: 0, 14 - h: 0 11 + let rgb = $state({ 12 + r: 0, 13 + g: 0, 14 + b: 0 15 15 }); 16 16 17 - let okhsv = $state({ 18 - h: 0, 19 - s: 0, 20 - v: 0 21 - }); 22 - 23 - $inspect(okchl); 17 + $inspect(rgb); 24 18 </script> 25 19 26 20 <Subheading class="mb-4">Color Picker</Subheading> 27 21 28 - <Popover.Root> 29 - <Popover.Trigger class={cn('cursor-pointer')}> 30 - <div 31 - class="border-base-300 dark:border-base-700 z-10 size-8 rounded-full border" 32 - style={`background-color: oklch(${okchl.l} ${okchl.c} ${okchl.h});`} 33 - ></div> 34 - </Popover.Trigger> 35 - <Popover.Content side="bottom" sideOffset={10}> 36 - <ColorPicker 37 - bind:setColor 38 - onchange={(color) => { 39 - console.log(color); 40 - okchl = color.oklch; 41 - }} 42 - bind:okhsv 43 - /> 44 - </Popover.Content> 45 - </Popover.Root> 22 + <PopoverColorPicker bind:rgb /> 23 + 24 + <ColorPicker bind:rgb /> 46 25 47 26 <Button 48 27 onclick={() => { 49 - setColor?.('#ff0000'); 28 + // themewatcher 29 + const theme = new ThemeWatcher(); 30 + const color = theme.getCSSVar('--color-accent-500'); 31 + rgb = oklch_to_rgb(oklch_string_to_oklch(color)); 50 32 }}>Reset Color</Button 51 33 >
+8 -180
src/docs/site-components/Cards.svelte
··· 1 1 <script> 2 2 import { base } from '$app/paths'; 3 - import CardAlerts from '$docs/cards/base/CardAlerts.svelte'; 4 - import CardAvatar from '$docs/cards/base/CardAvatar.svelte'; 5 - import CardBadge from '$docs/cards/base/CardBadge.svelte'; 6 - import CardBox from '$docs/cards/base/CardBox.svelte'; 7 - import CardButtons from '$docs/cards/base/CardButtons.svelte'; 8 - import CardCard from '$docs/cards/base/CardCard.svelte'; 9 - import CardChatBubble from '$docs/cards/base/CardChatBubble.svelte'; 10 - import CardCheckbox from '$docs/cards/base/CardCheckbox.svelte'; 3 + import { cn } from '$lib/utils'; 4 + 11 5 import CardGithubCorner from '$docs/cards/base/CardGithubCorner.svelte'; 12 - import CardHead from '$docs/cards/base/CardHead.svelte'; 13 - import CardImage from '$docs/cards/base/CardImage.svelte'; 14 - import CardInput from '$docs/cards/base/CardInput.svelte'; 15 - import CardModal from '$docs/cards/base/CardModal.svelte'; 16 - import CardNumberInput from '$docs/cards/base/CardNumberInput.svelte'; 17 - import CardScrollArea from '$docs/cards/base/CardScrollArea.svelte'; 18 - import CardSlider from '$docs/cards/base/CardSlider.svelte'; 19 - import CardSonner from '$docs/cards/base/CardSonner.svelte'; 20 - import CardSwitch from '$docs/cards/base/CardSwitch.svelte'; 21 - import CardTextarea from '$docs/cards/base/CardTextarea.svelte'; 22 - import CardTooltip from '$docs/cards/base/CardTooltip.svelte'; 23 6 import CardHeatmap from '$docs/cards/graphs/CardHeatmap.svelte'; 24 7 import CardLineGraph from '$docs/cards/graphs/CardLineGraph.svelte'; 25 8 import CardRingChart from '$docs/cards/graphs/CardRingChart.svelte'; 26 - import CardProse from '$docs/cards/base/CardProse.svelte'; 27 - import CardAccordion from '$docs/cards/base/CardAccordion.svelte'; 28 - import { cn } from '$lib/utils'; 29 - import CardExcalidraw from '$docs/cards/illustrations/CardExcalidraw.svelte'; 30 - import CardUndraw from '$docs/cards/illustrations/CardUndraw.svelte'; 31 - 32 - let baseCards = [ 33 - { 34 - component: CardAlerts, 35 - className: '', 36 - label: 'Alerts', 37 - href: 'alert' 38 - }, 39 - { 40 - component: CardAvatar, 41 - className: '', 42 - label: 'Avatars', 43 - href: 'avatar' 44 - }, 45 - { 46 - component: CardBadge, 47 - className: '', 48 - label: 'Badges', 49 - href: 'badge' 50 - }, 51 - { 52 - component: CardBox, 53 - className: '', 54 - label: 'Box', 55 - href: 'box' 56 - }, 57 - { 58 - component: CardButtons, 59 - className: '', 60 - label: 'Buttons', 61 - href: 'button' 62 - }, 63 - { 64 - component: CardChatBubble, 65 - className: '', 66 - label: 'Chat Bubbles', 67 - href: 'chat-bubble' 68 - }, 69 - { 70 - component: CardCheckbox, 71 - className: '', 72 - label: 'Checkboxes', 73 - href: 'checkbox' 74 - }, 75 - { 76 - component: CardImage, 77 - className: '', 78 - label: 'Images', 79 - href: 'image' 80 - }, 81 - { 82 - component: CardInput, 83 - className: '', 84 - label: 'Inputs', 85 - href: 'input' 86 - }, 87 - { 88 - component: CardModal, 89 - className: '', 90 - label: 'Modals', 91 - href: 'modal' 92 - }, 93 - { 94 - component: CardNumberInput, 95 - className: '', 96 - label: 'Number Inputs', 97 - href: 'number-input' 98 - }, 99 - { 100 - component: CardScrollArea, 101 - className: 'p-2', 102 - label: 'Scroll Areas', 103 - href: 'scroll-area' 104 - }, 105 - { 106 - component: CardSlider, 107 - className: '', 108 - label: 'Sliders', 109 - href: 'slider' 110 - }, 111 - { 112 - component: CardSonner, 113 - className: '', 114 - label: 'Sonner', 115 - href: 'sonner' 116 - }, 117 - { 118 - component: CardSwitch, 119 - className: '', 120 - label: 'Switches', 121 - href: 'switch' 122 - }, 123 - { 124 - component: CardTextarea, 125 - className: '', 126 - label: 'Textareas', 127 - href: 'textarea' 128 - }, 129 - { 130 - component: CardTooltip, 131 - className: '', 132 - label: 'Tooltips', 133 - href: 'tooltip' 134 - }, 135 - { 136 - component: CardCard, 137 - className: '', 138 - label: 'Card', 139 - href: 'cards' 140 - }, 141 - { 142 - component: CardHead, 143 - className: 'px-0 gap-0', 144 - label: 'Head', 145 - href: 'head' 146 - }, 147 - { 148 - component: CardProse, 149 - className: '', 150 - label: 'Prose', 151 - href: 'prose' 152 - }, 153 - { 154 - component: CardAccordion, 155 - className: '', 156 - label: 'Accordion', 157 - href: 'accordion' 158 - } 159 - ]; 160 - 161 - // sort cards by label 162 - baseCards.sort((a, b) => a.label.localeCompare(b.label)); 163 - 164 - console.log('cards', baseCards.length); 9 + import { baseComponents } from './components_base'; 10 + import { extraComponents } from './components_extra'; 165 11 166 12 let graphCards = [ 167 13 { ··· 195 41 href: 'github-corner' 196 42 } 197 43 ]; 198 - 199 - let illustrationsCards = [ 200 - { 201 - component: CardExcalidraw, 202 - className: '', 203 - label: 'Excalidraw', 204 - href: 'excalidraw' 205 - }, 206 - { 207 - component: CardUndraw, 208 - className: '', 209 - label: 'Undraw', 210 - href: 'undraw' 211 - } 212 - ]; 213 - 214 - // sort extraCards by label 215 - illustrationsCards.sort((a, b) => a.label.localeCompare(b.label)); 216 44 </script> 217 45 218 46 <h2 class="text-base-800 dark:text-base-200 mb-4 text-xl font-semibold">Base components</h2> 219 47 <div class="grid w-full grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 md:gap-8 lg:grid-cols-4"> 220 - {#each baseCards as card} 48 + {#each baseComponents as card} 221 49 <div 222 50 class="group relative flex flex-col items-start gap-3 transition-opacity duration-150 hover:opacity-90 md:gap-4" 223 51 > ··· 247 75 </div> 248 76 249 77 250 - <h2 class="hidden text-base-800 dark:text-base-200 mb-4 text-xl font-semibold mt-24">Illustrations components</h2> 251 - <div class="hidden w-full grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 md:gap-8 lg:grid-cols-4"> 252 - {#each illustrationsCards as card} 78 + <h2 class="text-base-800 dark:text-base-200 mb-4 text-xl font-semibold mt-24">Extra components</h2> 79 + <div class="grid w-full grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 md:gap-8 lg:grid-cols-4"> 80 + {#each extraComponents as card} 253 81 <div 254 82 class="group relative flex flex-col items-start gap-3 transition-opacity duration-150 hover:opacity-90 md:gap-4" 255 83 >
+22 -29
src/docs/site-components/Sidebar.svelte
··· 5 5 6 6 import { Button } from '$lib/components/base/button'; 7 7 import Sidebar from '$lib/components/base/sidebar/Sidebar.svelte'; 8 + import { baseComponents } from './components_base'; 9 + import { extraComponents } from './components_extra'; 10 + 8 11 import Logo from './Logo.svelte'; 9 - 10 - const baseComponents = [ 11 - { label: 'Alerts', href: 'alert' }, 12 - { label: 'Avatars', href: 'avatar' }, 13 - { label: 'Badges', href: 'badge' }, 14 - { label: 'Box', href: 'box' }, 15 - { label: 'Buttons', href: 'button' }, 16 - { label: 'Chat Bubble', href: 'chat-bubble' }, 17 - { label: 'Checkboxes', href: 'checkbox' }, 18 - { label: 'Image', href: 'image' }, 19 - { label: 'Input', href: 'input' }, 20 - { label: 'Scroll Area', href: 'scroll-area' }, 21 - { label: 'Slider', href: 'slider' }, 22 - { label: 'Switch', href: 'switch' }, 23 - { label: 'Textarea', href: 'textarea' }, 24 - { label: 'Tooltip', href: 'tooltip' }, 25 - { label: 'Modal', href: 'modal' }, 26 - { label: 'Number Input', href: 'number-input' }, 27 - { label: 'Cards', href: 'cards' }, 28 - { label: 'Accordion', href: 'accordion' }, 29 - { label: 'Sonner', href: 'sonner' }, 30 - { label: 'Prose', href: 'prose' }, 31 - { label: 'Head', href: 'head' } 32 - ]; 33 - 34 - baseComponents.sort((a, b) => a.label.localeCompare(b.label)); 35 - console.log('components', baseComponents.length); 36 12 37 13 const components = [ 38 14 { label: 'Select', href: 'select' }, ··· 95 71 data-sveltekit-keepfocus 96 72 variant="ghost" 97 73 onclick={handleClick} 98 - href="{base}/components/theme" 74 + href="{base}/docs/theme" 99 75 class="mb-6 w-full justify-start">Theme</Button 100 76 > 101 77 102 - <Accordion type="single" class="w-full" value="base-components"> 78 + <Accordion type="multiple" class="w-full" value={["base-components", "extra-components"]}> 103 79 <AccordionItem 104 80 value="base-components" 105 81 class="border-0" ··· 113 89 variant="ghost" 114 90 onclick={handleClick} 115 91 href="{base}/components/base/{component.href}" 92 + class="w-full justify-start">{component.label}</Button 93 + > 94 + {/each} 95 + </AccordionItem> 96 + <AccordionItem 97 + value="extra-components" 98 + class="border-0" 99 + title="Extra Components" 100 + triggerClasses="text-sm px-3 py-1 font-semibold" 101 + contentClasses="flex flex-col gap-1 items-start px-1" 102 + > 103 + {#each extraComponents as component} 104 + <Button 105 + data-sveltekit-keepfocus 106 + variant="ghost" 107 + onclick={handleClick} 108 + href="{base}/components/extras/{component.href}" 116 109 class="w-full justify-start">{component.label}</Button 117 110 > 118 111 {/each}
+150
src/docs/site-components/components_base.ts
··· 1 + import CardAlerts from '$docs/cards/base/CardAlerts.svelte'; 2 + import CardAvatar from '$docs/cards/base/CardAvatar.svelte'; 3 + import CardBadge from '$docs/cards/base/CardBadge.svelte'; 4 + import CardBox from '$docs/cards/base/CardBox.svelte'; 5 + import CardButtons from '$docs/cards/base/CardButtons.svelte'; 6 + import CardCard from '$docs/cards/base/CardCard.svelte'; 7 + import CardChatBubble from '$docs/cards/base/CardChatBubble.svelte'; 8 + import CardCheckbox from '$docs/cards/base/CardCheckbox.svelte'; 9 + import CardHead from '$docs/cards/base/CardHead.svelte'; 10 + import CardImage from '$docs/cards/base/CardImage.svelte'; 11 + import CardInput from '$docs/cards/base/CardInput.svelte'; 12 + import CardModal from '$docs/cards/base/CardModal.svelte'; 13 + import CardNumberInput from '$docs/cards/base/CardNumberInput.svelte'; 14 + import CardScrollArea from '$docs/cards/base/CardScrollArea.svelte'; 15 + import CardSlider from '$docs/cards/base/CardSlider.svelte'; 16 + import CardSonner from '$docs/cards/base/CardSonner.svelte'; 17 + import CardSwitch from '$docs/cards/base/CardSwitch.svelte'; 18 + import CardTextarea from '$docs/cards/base/CardTextarea.svelte'; 19 + import CardTooltip from '$docs/cards/base/CardTooltip.svelte'; 20 + import CardProse from '$docs/cards/base/CardProse.svelte'; 21 + import CardAccordion from '$docs/cards/base/CardAccordion.svelte'; 22 + 23 + export const baseComponents = [ 24 + { 25 + component: CardAlerts, 26 + className: '', 27 + label: 'Alerts', 28 + href: 'alert' 29 + }, 30 + { 31 + component: CardAvatar, 32 + className: '', 33 + label: 'Avatars', 34 + href: 'avatar' 35 + }, 36 + { 37 + component: CardBadge, 38 + className: '', 39 + label: 'Badges', 40 + href: 'badge' 41 + }, 42 + { 43 + component: CardBox, 44 + className: '', 45 + label: 'Box', 46 + href: 'box' 47 + }, 48 + { 49 + component: CardButtons, 50 + className: '', 51 + label: 'Buttons', 52 + href: 'button' 53 + }, 54 + { 55 + component: CardChatBubble, 56 + className: '', 57 + label: 'Chat Bubbles', 58 + href: 'chat-bubble' 59 + }, 60 + { 61 + component: CardCheckbox, 62 + className: '', 63 + label: 'Checkboxes', 64 + href: 'checkbox' 65 + }, 66 + { 67 + component: CardImage, 68 + className: '', 69 + label: 'Images', 70 + href: 'image' 71 + }, 72 + { 73 + component: CardInput, 74 + className: '', 75 + label: 'Text Inputs', 76 + href: 'input' 77 + }, 78 + { 79 + component: CardModal, 80 + className: '', 81 + label: 'Modals', 82 + href: 'modal' 83 + }, 84 + { 85 + component: CardNumberInput, 86 + className: '', 87 + label: 'Number Input', 88 + href: 'number-input' 89 + }, 90 + { 91 + component: CardScrollArea, 92 + className: 'p-2', 93 + label: 'Scroll Area', 94 + href: 'scroll-area' 95 + }, 96 + { 97 + component: CardSlider, 98 + className: '', 99 + label: 'Sliders', 100 + href: 'slider' 101 + }, 102 + { 103 + component: CardSonner, 104 + className: '', 105 + label: 'Sonner', 106 + href: 'sonner' 107 + }, 108 + { 109 + component: CardSwitch, 110 + className: '', 111 + label: 'Switches', 112 + href: 'switch' 113 + }, 114 + { 115 + component: CardTextarea, 116 + className: '', 117 + label: 'Textarea', 118 + href: 'textarea' 119 + }, 120 + { 121 + component: CardTooltip, 122 + className: '', 123 + label: 'Tooltips', 124 + href: 'tooltip' 125 + }, 126 + { 127 + component: CardCard, 128 + className: '', 129 + label: 'Card', 130 + href: 'cards' 131 + }, 132 + { 133 + component: CardHead, 134 + className: 'px-0 gap-0', 135 + label: 'Head', 136 + href: 'head' 137 + }, 138 + { 139 + component: CardProse, 140 + className: '', 141 + label: 'Prose', 142 + href: 'prose' 143 + }, 144 + { 145 + component: CardAccordion, 146 + className: '', 147 + label: 'Accordion', 148 + href: 'accordion' 149 + } 150 + ].sort((a, b) => a.label.localeCompare(b.label));
+24
src/docs/site-components/components_extra.ts
··· 1 + import CardExcalidraw from '$docs/cards/illustrations/CardExcalidraw.svelte'; 2 + import CardUndraw from '$docs/cards/illustrations/CardUndraw.svelte'; 3 + import CardColorPicker from '$docs/cards/extras/CardColorPicker.svelte'; 4 + 5 + export const extraComponents = [ 6 + { 7 + component: CardExcalidraw, 8 + className: '', 9 + label: 'Excalidraw', 10 + href: 'excalidraw' 11 + }, 12 + { 13 + component: CardUndraw, 14 + className: '', 15 + label: 'Undraw', 16 + href: 'undraw' 17 + }, 18 + { 19 + component: CardColorPicker, 20 + className: '', 21 + label: 'Color Picker', 22 + href: 'color-picker' 23 + } 24 + ].sort((a, b) => a.label.localeCompare(b.label));
+22
src/docs/cards/extras/CardColorPicker.svelte
··· 1 + <script lang="ts"> 2 + import { ColorPicker } from '$lib'; 3 + import { oklch_string_to_oklch, oklch_to_rgb } from '$lib/components/extra/color-picker/base/color'; 4 + import { ThemeWatcher } from '$lib/helper/ThemeWatcher.svelte'; 5 + import { onMount } from 'svelte'; 6 + 7 + let rgb = $state({ 8 + r: 1, 9 + g: 0, 10 + b: 0 11 + }); 12 + 13 + onMount(() => { 14 + const theme = new ThemeWatcher(); 15 + const color = theme.getCSSVar('--color-accent-500'); 16 + rgb = oklch_to_rgb(oklch_string_to_oklch(color)); 17 + }); 18 + </script> 19 + 20 + <div class="h-full w-full flex items-center justify-center"> 21 + <!-- inline version --> 22 + <ColorPicker bind:rgb class="scale-[0.6]" /></div>
-5
src/routes/components/theme/+page.svelte
··· 1 - <script> 2 - import SelectTheme from '$docs/preview/SelectTheme.svelte'; 3 - </script> 4 - 5 - <SelectTheme />
+26 -6
src/routes/docs/philosophy/Philosophy.md
··· 26 26 27 27 ## Copying components vs installing packages 28 28 29 - I'm a big fan of the philosophy of copying components (shadcn-style) for better customization, but also I think the quickest way to build something is to just install the package and use it. 29 + I'm a big fan of the philosophy of copying components (shadcn-style) for better customization, 30 + but also I think the quickest way to build something is to just install the package and use it. 30 31 31 - So the plan is to have both options (currently only installing as a package is available, but you can copy what you need from the [source code](https://github.com/flo-bit/ui-kit/tree/main/src/lib/components/base) though some imports might need to be adjusted). 32 + So the plan is to have both options (currently only installing as a package is available, 33 + but you can copy what you need from the [source code](https://github.com/flo-bit/ui-kit/tree/main/src/lib/components/base) 34 + though some imports might need to be adjusted). 32 35 33 36 ## Lots of components 34 37 35 38 While currently only the base components are visible, there are lots of components in the pipeline. 36 - The aim is to have a component library that has lots of components that are useful for building webapps/websites/games, all in a consistent style/theme. 39 + The aim is to have a component library that has lots of components that are useful for building 40 + webapps/websites/games, all in a consistent style/theme. 37 41 38 42 See here for a few examples of work in progress components (some may be partially broken): 39 43 ··· 45 49 46 50 ### extras 47 51 48 - - [color picker](/ui-kit/components/extras/color-picker/) 49 - - [excalidraw](/ui-kit/components/extras/excalidraw/) 50 52 - [quote](/ui-kit/components/extras/quote/) 51 53 - [phone](/ui-kit/components/extras/phone/) 52 54 - [timer](/ui-kit/components/extras/timer/) ··· 63 65 - [model-picker](/ui-kit/components/3d/model-picker/) 64 66 - [voxel-art](/ui-kit/components/3d/voxel-art/) 65 67 68 + ## Works without javascript when possible / is accessible 69 + 70 + Whenever possible components should work without javascript. 71 + Also all components should be accessible to the best of my knowledge. 72 + Please let me know [if you find any issues](https://github.com/flo-bit/ui-kit/issues). 73 + 66 74 ## Dark mode 67 75 68 - This ui kit is designed to be used in both light and dark mode (and switch automatically depending on system settings). 76 + This ui kit is designed to be used in both light and dark mode 77 + (and switch automatically depending on system settings). 69 78 If you want to disable dark mode, add the following to your app.css: 70 79 71 80 ```css 72 81 @custom-variant dark (&:is(.dark *)); 73 82 ``` 74 83 84 + Similarly you can disable light mode: 85 + 86 + 1. Add the above code to your app.css 87 + 2. Add the `dark` class to the `html` element in your app. 88 + 75 89 ## Contributing/Feedback 76 90 77 91 If you have any suggestions for components or feedback in general, please open an issue on [github](https://github.com/flo-bit/ui-kit/issues). 78 92 79 93 Also feel free to contribute yourself, preferably by first opening an issue about the component/feature you want to add! 94 + 95 + You can also reach me on [bluesky](https://bsky.app/profile/flo-bit.dev). 96 + 97 + ## Credits 98 + 99 + This ui kit is largely based on [bits-ui](https://bits-ui.com/).
+5 -5
src/routes/docs/quick-start/QuickStart.md
··· 14 14 15 15 Also add the `@tailwindcss/typography` and `@tailwindcss/forms` plugins. 16 16 17 - ## 2. Install fox-ui-svelte 17 + ## 2. Install fuchs 18 18 19 19 ```bash 20 - npm install fox-ui-svelte 20 + npm install fuchs 21 21 ``` 22 22 23 - ## 3. Add this to your `app.css` 23 + ## 3. Set theme variables in your `app.css` 24 24 25 25 You can change the colors to your liking. 26 26 27 27 ```css 28 - @source "../node_modules/fox-ui-svelte"; 28 + @source "../node_modules/fuchs"; 29 29 30 30 @theme { 31 31 --color-base-50: var(--color-gray-50); ··· 58 58 59 59 ```svelte 60 60 <script> 61 - import { Button } from 'fox-ui-svelte'; 61 + import { Button } from 'fuchs'; 62 62 </script> 63 63 64 64 <Button>Click me</Button>
+5
src/routes/docs/theme/+page.svelte
··· 1 + <script> 2 + import SelectTheme from '$docs/preview/SelectTheme.svelte'; 3 + </script> 4 + 5 + <SelectTheme />
-67
src/lib/components/3d/model-picker/ModalPicker.svelte
··· 1 - <script lang="ts"> 2 - import { Canvas } from '@threlte/core'; 3 - import { View } from '@threlte/extras'; 4 - import Scene from './ModelPickerScene.svelte'; 5 - import Modal from '$lib/components/base/modal/Modal.svelte'; 6 - import { Portal } from 'bits-ui'; 7 - 8 - let { 9 - items, 10 - open = $bindable(), 11 - alwaysRotate = false, 12 - title, 13 - onselect 14 - }: { 15 - open: boolean; 16 - items: { path: string; label: string }[]; 17 - alwaysRotate?: boolean; 18 - onselect?: ({ path, label }: { path: string; label: string }) => void; 19 - title?: string; 20 - } = $props(); 21 - 22 - let states = $state( 23 - new Array<{ hover: boolean; dom: HTMLElement | undefined }>(items.length).fill({ 24 - hover: false, 25 - dom: undefined 26 - }) 27 - ); 28 - </script> 29 - 30 - <Modal bind:open class="mx-auto max-h-[100dvh] max-w-5xl overflow-hidden overflow-y-scroll" {title}> 31 - <div 32 - class="relative mx-auto grid h-full w-full max-w-5xl grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4" 33 - > 34 - {#each items as item, index} 35 - <button 36 - id="item" 37 - class="hover:bg-base-200/40 dark:hover:bg-base-800/40 m-4 inline-block cursor-pointer rounded-2xl p-4 transition-all duration-300 hover:scale-105" 38 - onclick={() => { 39 - open = false; 40 - states[index].hover = false; 41 - onselect?.({ path: item.path, label: item.label }); 42 - }} 43 - onpointerenter={() => { 44 - states[index].hover = true; 45 - }} 46 - onpointerleave={() => { 47 - states[index].hover = false; 48 - }} 49 - > 50 - <div class="aspect-square" bind:this={states[index].dom}></div> 51 - <div class="text-base-700 dark:text-base-300 text-sm">{item.label}</div> 52 - </button> 53 - {/each} 54 - </div> 55 - </Modal> 56 - 57 - <Portal> 58 - <div class="pointer-events-none fixed top-0 z-[1000] h-full w-full"> 59 - <Canvas> 60 - {#each items as item, index} 61 - <View dom={states[index].dom}> 62 - <Scene path={item.path} hover={states[index].hover || alwaysRotate} /> 63 - </View> 64 - {/each} 65 - </Canvas> 66 - </div> 67 - </Portal>
+67
src/lib/components/3d/model-picker/ModelPicker.svelte
··· 1 + <script lang="ts"> 2 + import { Canvas } from '@threlte/core'; 3 + import { View } from '@threlte/extras'; 4 + import Scene from './ModelPickerScene.svelte'; 5 + import Modal from '$lib/components/base/modal/Modal.svelte'; 6 + import { Portal } from 'bits-ui'; 7 + 8 + let { 9 + items, 10 + open = $bindable(), 11 + alwaysRotate = false, 12 + title, 13 + onselect 14 + }: { 15 + open: boolean; 16 + items: { path: string; label: string }[]; 17 + alwaysRotate?: boolean; 18 + onselect?: ({ path, label }: { path: string; label: string }) => void; 19 + title?: string; 20 + } = $props(); 21 + 22 + let states = $state( 23 + new Array<{ hover: boolean; dom: HTMLElement | undefined }>(items.length).fill({ 24 + hover: false, 25 + dom: undefined 26 + }) 27 + ); 28 + </script> 29 + 30 + <Modal bind:open class="mx-auto max-h-[100dvh] max-w-5xl overflow-hidden overflow-y-scroll" {title}> 31 + <div 32 + class="relative mx-auto grid h-full w-full max-w-5xl grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4" 33 + > 34 + {#each items as item, index} 35 + <button 36 + id="item" 37 + class="hover:bg-base-200/40 dark:hover:bg-base-800/40 m-4 inline-block cursor-pointer rounded-2xl p-4 transition-all duration-300 hover:scale-105" 38 + onclick={() => { 39 + open = false; 40 + states[index].hover = false; 41 + onselect?.({ path: item.path, label: item.label }); 42 + }} 43 + onpointerenter={() => { 44 + states[index].hover = true; 45 + }} 46 + onpointerleave={() => { 47 + states[index].hover = false; 48 + }} 49 + > 50 + <div class="aspect-square" bind:this={states[index].dom}></div> 51 + <div class="text-base-700 dark:text-base-300 text-sm">{item.label}</div> 52 + </button> 53 + {/each} 54 + </div> 55 + </Modal> 56 + 57 + <Portal> 58 + <div class="pointer-events-none fixed top-0 z-[1000] h-full w-full"> 59 + <Canvas> 60 + {#each items as item, index} 61 + <View dom={states[index].dom}> 62 + <Scene path={item.path} hover={states[index].hover || alwaysRotate} /> 63 + </View> 64 + {/each} 65 + </Canvas> 66 + </div> 67 + </Portal>
+2
src/lib/components/extra/color-picker/index.ts
··· 1 + export { default as ColorPicker } from './base/ColorPicker.svelte'; 2 + export { default as PopoverColorPicker } from './popover/PopoverColorPicker.svelte';
+2 -2
src/routes/components/3d/model-picker/+page.svelte
··· 1 1 <script lang="ts"> 2 2 import { base } from '$app/paths'; 3 - import ModalPicker from '$lib/components/3d/model-picker/ModalPicker.svelte'; 3 + import { ModelPicker } from '$lib'; 4 4 import Button from '$lib/components/base/button/Button.svelte'; 5 5 import Subheading from '$lib/components/base/heading/Subheading.svelte'; 6 6 import { toast } from 'svelte-sonner'; ··· 10 10 11 11 <Subheading class="mb-4">Model Picker</Subheading> 12 12 13 - <ModalPicker 13 + <ModelPicker 14 14 items={[ 15 15 { 16 16 path: base + '/models/gltf/Barrel_A.gltf',
+1 -1
src/routes/components/base/accordion/Accordion.md
··· 19 19 20 20 ```svelte 21 21 <script lang="ts"> 22 - import { Accordion, AccordionItem } from 'fox-ui-svelte'; 22 + import { Accordion, AccordionItem } from 'fuchs'; 23 23 </script> 24 24 25 25 <Accordion type="single">
+1 -1
src/routes/components/base/alert/Alert.md
··· 63 63 64 64 ```svelte 65 65 <script> 66 - import { Alert } from 'fox-ui-svelte'; 66 + import { Alert } from 'fuchs'; 67 67 </script> 68 68 69 69 <Alert title="Alert Title" type="info">
+1 -1
src/routes/components/base/avatar/Avatar.md
··· 44 44 45 45 ```svelte 46 46 <script> 47 - import { Avatar, AvatarGroup } from 'fox-ui-svelte'; 47 + import { Avatar, AvatarGroup } from 'fuchs'; 48 48 </script> 49 49 50 50 <Avatar src="https://github.com/flo-bit.png" alt="flo-bit" fallback="FB" />
+1 -1
src/routes/components/base/badge/Badge.md
··· 86 86 87 87 ```svelte 88 88 <script> 89 - import { Badge } from 'fox-ui-svelte'; 89 + import { Badge } from 'fuchs'; 90 90 </script> 91 91 92 92 <Badge size="sm" variant="primary">Badge</Badge>
+1 -1
src/routes/components/base/box/Box.md
··· 13 13 14 14 ```svelte 15 15 <script> 16 - import { Box } from 'fox-ui-svelte'; 16 + import { Box } from 'fuchs'; 17 17 </script> 18 18 19 19 <Box class="text-sm">This is a box, put your stuff here.</Box>
+1 -1
src/routes/components/base/button/Button.md
··· 180 180 181 181 ```svelte 182 182 <script> 183 - import { Button } from 'fox-ui-svelte'; 183 + import { Button } from 'fuchs'; 184 184 </script> 185 185 186 186 <Button variant="primary" size="sm">Button</Button>
+3 -3
src/routes/components/base/cards/Cards.md
··· 13 13 14 14 ## Examples 15 15 16 - Full width 16 + ### Full width 17 17 18 18 <ImageCard 19 19 src={nature1} ··· 24 24 class="not-prose mb-16" 25 25 /> 26 26 27 - In a grid 27 + ### In a grid 28 28 29 29 <div class="grid w-full grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 not-prose"> 30 30 <ImageCard ··· 53 53 54 54 ```svelte 55 55 <script> 56 - import { ImageCard } from 'fox-ui-svelte'; 56 + import { ImageCard } from 'fuchs'; 57 57 </script> 58 58 59 59 <ImageCard src={'/image.jpg'} alt="" title="Card title" description="Card description" href="#" />
+1 -1
src/routes/components/base/chat-bubble/ChatBubble.md
··· 42 42 43 43 ```svelte 44 44 <script> 45 - import { ChatBubble } from 'fox-ui-svelte'; 45 + import { ChatBubble } from 'fuchs'; 46 46 </script> 47 47 48 48 <ChatBubble side="right" variant="secondary">
+1 -1
src/routes/components/base/checkbox/Checkbox.md
··· 42 42 43 43 ```svelte 44 44 <script> 45 - import { Checkbox } from 'fox-ui-svelte'; 45 + import { Checkbox } from 'fuchs'; 46 46 47 47 let checked = $state(false); 48 48 </script>
+1 -1
src/routes/components/base/head/Head.md
··· 8 8 9 9 ```svelte 10 10 <script> 11 - import { Head } from 'fox-ui-svelte'; 11 + import { Head } from 'fuchs'; 12 12 </script> 13 13 14 14 <Head
+1 -1
src/routes/components/base/image/Image.md
··· 31 31 32 32 ```svelte 33 33 <script> 34 - import { ImageContainer } from 'fox-ui-svelte'; 34 + import { ImageContainer } from 'fuchs'; 35 35 </script> 36 36 37 37 <ImageContainer src="/image.jpg" alt="Alt tag" />
+1 -1
src/routes/components/base/input/Input.md
··· 28 28 29 29 ```svelte 30 30 <script> 31 - import { Input } from 'fox-ui-svelte'; 31 + import { Input } from 'fuchs'; 32 32 33 33 let value = $state(''); 34 34 </script>
+1 -1
src/routes/components/base/modal/Modal.md
··· 46 46 47 47 ```svelte 48 48 <script> 49 - import { Modal, Button } from 'fox-ui-svelte'; 49 + import { Modal, Button } from 'fuchs'; 50 50 51 51 let open = $state(false); 52 52
+1 -1
src/routes/components/base/number-input/NumberInput.md
··· 13 13 14 14 ```svelte 15 15 <script> 16 - import { NumberInput } from 'fox-ui-svelte'; 16 + import { NumberInput } from 'fuchs'; 17 17 18 18 let value = $state(0); 19 19 </script>
+1 -1
src/routes/components/base/prose/Prose.md
··· 18 18 19 19 ```svelte 20 20 <script> 21 - import Prose from 'fox-ui-svelte'; 21 + import Prose from 'fuchs'; 22 22 </script> 23 23 24 24 <Prose>
+1 -1
src/routes/components/base/scroll-area/ScrollArea.md
··· 48 48 49 49 ```svelte 50 50 <script> 51 - import { ScrollArea } from 'fox-ui-svelte'; 51 + import { ScrollArea } from 'fuchs'; 52 52 </script> 53 53 54 54 <ScrollArea class="h-52 w-96">
+1 -1
src/routes/components/base/slider/Slider.md
··· 15 15 16 16 ```svelte 17 17 <script> 18 - import { Slider } from 'fox-ui-svelte'; 18 + import { Slider } from 'fuchs'; 19 19 20 20 let value = $state(50); 21 21 </script>
+2 -2
src/routes/components/base/sonner/Sonner.md
··· 46 46 47 47 ```svelte 48 48 <script> 49 - import { Toaster } from 'fox-ui-svelte'; 49 + import { Toaster } from 'fuchs'; 50 50 </script> 51 51 52 52 <Toaster /> ··· 56 56 57 57 ```svelte 58 58 <script> 59 - import { Button, toast } from 'fox-ui-svelte'; 59 + import { Button, toast } from 'fuchs'; 60 60 61 61 function handleClick() { 62 62 toast('Hello there', { description: 'General Kenobi!' });
+1 -1
src/routes/components/base/switch/Switch.md
··· 36 36 37 37 ```svelte 38 38 <script> 39 - import { Switch } from 'fox-ui-svelte'; 39 + import { Switch } from 'fuchs'; 40 40 41 41 let checked = $state(false); 42 42 </script>
+1 -1
src/routes/components/base/textarea/Textarea.md
··· 28 28 29 29 ```svelte 30 30 <script> 31 - import { Textarea } from 'fox-ui-svelte'; 31 + import { Textarea } from 'fuchs'; 32 32 33 33 let value = $state(''); 34 34 </script>
+1 -1
src/routes/components/base/tooltip/Tooltip.md
··· 26 26 27 27 ```svelte 28 28 <script> 29 - import { Tooltip, Button } from 'fox-ui-svelte'; 29 + import { Tooltip, Button } from 'fuchs'; 30 30 31 31 function handleClick() { 32 32 console.log('clicked');
+6 -3
src/routes/components/extras/color-picker/+page.svelte
··· 1 - <script> 2 - import ColorPickerPreview from '$docs/preview/ColorPickerPreview.svelte'; 1 + <script lang="ts"> 2 + import ColorPickerDocs from './ColorPicker.md'; 3 + import Prose from '$lib/components/base/prose/Prose.svelte'; 3 4 </script> 4 5 5 - <ColorPickerPreview /> 6 + <Prose> 7 + <ColorPickerDocs /> 8 + </Prose>
+33
src/routes/components/extras/color-picker/ColorPicker.md
··· 1 + <script lang="ts"> 2 + import ColorPickerExample from './Example.svelte'; 3 + </script> 4 + 5 + # Color Picker 6 + 7 + ## Example 8 + 9 + <ColorPickerExample /> 10 + 11 + ## Usage 12 + 13 + ```svelte 14 + <script lang="ts"> 15 + import { ColorPicker, PopoverColorPicker } from 'fuchs'; 16 + 17 + let rgb = $state({ 18 + r: 0, 19 + g: 0, 20 + b: 0 21 + }); 22 + </script> 23 + 24 + <!-- inline version --> 25 + <ColorPicker bind:rgb /> 26 + 27 + <!-- popover version --> 28 + <PopoverColorPicker bind:rgb /> 29 + ``` 30 + 31 + ### Credits 32 + 33 + Adapted from [svelte-color-select](https://github.com/CaptainCodeman/svelte-color-select).
+40
src/routes/components/extras/color-picker/Example.svelte
··· 1 + <script lang="ts"> 2 + import { ColorPicker } from '$lib/components/extra/color-picker/base'; 3 + import * as Popover from '$lib/components/base/popover'; 4 + import { cn } from '$lib/utils'; 5 + import Subheading from '$lib/components/base/heading/Subheading.svelte'; 6 + import Button from '$lib/components/base/button/Button.svelte'; 7 + import PopoverColorPicker from '$lib/components/extra/color-picker/popover/PopoverColorPicker.svelte'; 8 + import { ThemeWatcher } from '$lib/helper/ThemeWatcher.svelte'; 9 + import { hex_to_rgb, oklch_string_to_oklch, oklch_to_rgb } from '$lib/components/extra/color-picker/base/color'; 10 + import { onMount } from 'svelte'; 11 + 12 + let rgb = $state({ 13 + r: 0, 14 + g: 0, 15 + b: 0 16 + }); 17 + 18 + onMount(() => { 19 + const theme = new ThemeWatcher(); 20 + const color = theme.getCSSVar('--color-accent-500'); 21 + rgb = oklch_to_rgb(oklch_string_to_oklch(color)); 22 + }); 23 + </script> 24 + 25 + <h3>Inline Color Picker</h3> 26 + 27 + <ColorPicker bind:rgb /> 28 + 29 + <h3>Popover Color Picker</h3> 30 + 31 + <PopoverColorPicker bind:rgb /> 32 + <!-- 33 + <Button 34 + onclick={() => { 35 + // themewatcher 36 + const theme = new ThemeWatcher(); 37 + const color = theme.getCSSVar('--color-accent-500'); 38 + rgb = oklch_to_rgb(oklch_string_to_oklch(color)); 39 + }}>Reset Color</Button 40 + > -->
+6 -3
src/routes/components/extras/excalidraw/+page.svelte
··· 1 - <script> 2 - import ExcalidrawPreview from '$docs/preview/ExcalidrawPreview.svelte'; 1 + <script lang="ts"> 2 + import ExcalidrawDocs from './Excalidraw.md'; 3 + import Prose from '$lib/components/base/prose/Prose.svelte'; 3 4 </script> 4 5 5 - <ExcalidrawPreview /> 6 + <Prose> 7 + <ExcalidrawDocs /> 8 + </Prose>
+7
src/routes/components/extras/excalidraw/Example.svelte
··· 1 + <script> 2 + import Excalidraw from '$lib/components/extra/excalidraw/Excalidraw.svelte'; 3 + 4 + import svg from '$docs/assets/demo.svg?raw'; 5 + </script> 6 + 7 + <Excalidraw {svg} alt="Excalidraw Demo" caption="This is a demo of the Excalidraw component." />
+25
src/routes/components/extras/excalidraw/Excalidraw.md
··· 1 + <script lang="ts"> 2 + import ExcalidrawExample from './Example.svelte'; 3 + </script> 4 + 5 + # Excalidraw 6 + 7 + ## Example 8 + 9 + <ExcalidrawExample /> 10 + 11 + ## Usage 12 + 13 + 1. Draw a diagram in [excalidraw](https://excalidraw.com/) with the default colors and export it as a svg without a background. 14 + 15 + 2. Import that svg and use it with the excalidraw component, colors will be automatically changed to your theme colors. 16 + 17 + ```svelte 18 + <script lang="ts"> 19 + import { Excalidraw } from 'fuchs'; 20 + 21 + import svg from '$docs/assets/demo.svg?raw'; 22 + </script> 23 + 24 + <Excalidraw {svg} alt="Excalidraw Demo" caption="This is a demo of the Excalidraw component." /> 25 + ```
+5 -16
src/routes/components/extras/undraw/+page.svelte
··· 1 1 <script lang="ts"> 2 - import Undraw from '$lib/components/extra/undraw/Undraw.svelte'; 3 - 4 - import svg from '$docs/assets/undraw_yoga.svg?raw'; 2 + import UndrawDocs from './Undraw.md'; 3 + import Prose from '$lib/components/base/prose/Prose.svelte'; 5 4 </script> 6 5 7 - <div class="w-full max-w-md mx-auto"> 8 - <Undraw 9 - {svg} 10 - alt="Undraw" 11 - colorMap={{ 12 - '#090814': 'fill-base-950 dark:fill-accent-950', 13 - '#d6d6e3': 'fill-base-300 dark:fill-base-700', 14 - '#cacaca': 'fill-base-400 dark:fill-base-600', 15 - '#cfcce0': 'fill-base-500 dark:fill-base-500', 16 - '#f2f2f2': 'fill-base-200 dark:fill-base-800' 17 - }} 18 - /> 19 - </div> 6 + <Prose> 7 + <UndrawDocs /> 8 + </Prose>
+19
src/routes/components/extras/undraw/Example.svelte
··· 1 + <script lang="ts"> 2 + import Undraw from '$lib/components/extra/undraw/Undraw.svelte'; 3 + 4 + import svg from '$docs/assets/undraw_yoga.svg?raw'; 5 + </script> 6 + 7 + <div class="w-full max-w-md mx-auto"> 8 + <Undraw 9 + {svg} 10 + alt="Undraw" 11 + colorMap={{ 12 + '#090814': 'fill-base-950 dark:fill-accent-950', 13 + '#d6d6e3': 'fill-base-300 dark:fill-base-700', 14 + '#cacaca': 'fill-base-400 dark:fill-base-600', 15 + '#cfcce0': 'fill-base-500 dark:fill-base-500', 16 + '#f2f2f2': 'fill-base-200 dark:fill-base-800' 17 + }} 18 + /> 19 + </div>
+42
src/routes/components/extras/undraw/Undraw.md
··· 1 + <script lang="ts"> 2 + import UndrawExample from './Example.svelte'; 3 + import {Alert} from '$lib'; 4 + </script> 5 + 6 + # Undraw 7 + 8 + ## Example 9 + 10 + <UndrawExample /> 11 + 12 + ## Usage 13 + 14 + <Alert variant="default" type="warning" title="Doesn't work automatically in dark mode"> 15 + <span> 16 + Use the `colorMap` prop to change the colors of the illustration like in the example below to make it work in both light and dark mode. 17 + </span> 18 + </Alert> 19 + 20 + 21 + 1. Download a svg illustration from [undraw](https://undraw.co/illustrations) leaving the theme color as it is (#6c63ff). 22 + 2. Import that svg and use it with the undraw component. This will automatically change the theme color to your theme color. 23 + 24 + ```svelte 25 + <script lang="ts"> 26 + import Undraw from '$lib/components/extra/undraw/Undraw.svelte'; 27 + 28 + import svg from '$docs/assets/undraw_yoga.svg?raw'; 29 + </script> 30 + 31 + <Undraw 32 + {svg} 33 + alt="Undraw" 34 + colorMap={{ 35 + '#090814': 'fill-base-950 dark:fill-accent-950', 36 + '#d6d6e3': 'fill-base-300 dark:fill-base-700', 37 + '#cacaca': 'fill-base-400 dark:fill-base-600', 38 + '#cfcce0': 'fill-base-500 dark:fill-base-500', 39 + '#f2f2f2': 'fill-base-200 dark:fill-base-800' 40 + }} 41 + /> 42 + ```
+34 -46
src/lib/components/extra/color-picker/base/ColorPicker.svelte
··· 3 3 import { render_main_image, render_slider_image } from './render'; 4 4 import type { RGB, OKlab, OKhsv, OKlch } from './color'; 5 5 import { 6 - oklab_to_okhsv, 7 6 okhsv_to_oklab, 8 - rgb_to_okhsv, 9 7 okhsv_to_rgb, 10 8 okhsv_to_oklch, 11 - rgb_to_hex 9 + rgb_to_hex, 10 + oklab_to_okhsv, 11 + rgb_to_okhsv 12 12 } from './color'; 13 + import { cn } from '$lib'; 13 14 14 - export let rgb: RGB | undefined = undefined; 15 - export let oklab: OKlab | undefined = undefined; 16 - export let okhsv: OKhsv | undefined = undefined; 17 - 18 - export let onchange: (color: { 19 - hex: string; 20 - rgb: RGB; 21 - oklab: OKlab; 22 - okhsv: OKhsv; 23 - oklch: OKlch; 24 - }) => void = () => {}; 15 + let { 16 + rgb = $bindable(), 17 + oklab = $bindable(), 18 + okhsv = $bindable(), 19 + class: className, 20 + onchange 21 + }: { 22 + rgb?: RGB; 23 + oklab?: OKlab; 24 + okhsv?: OKhsv; 25 + class?: string; 26 + onchange?: (color: { hex: string; rgb: RGB; oklab: OKlab; okhsv: OKhsv; oklch: OKlch }) => void; 27 + } = $props(); 25 28 26 29 const width = picker_size + slider_width + gap_size + border_size * 2; 27 30 const height = picker_size + border_size * 2; 28 31 29 - $: color = convertToInternal(rgb, oklab, okhsv); 30 - $: uihsv = scale_to_ui(color); 32 + let color = $derived(convertToInternal(rgb, oklab, okhsv)); 33 + let uihsv = $derived(scale_to_ui(color)); 31 34 32 35 function scale_to_ui(okhsv: OKhsv): OKhsv { 33 36 return { ··· 84 87 rgb = new_rgb; 85 88 } 86 89 87 - onchange({ 90 + onchange?.({ 88 91 rgb: new_rgb, 89 92 oklab: new_oklab, 90 93 okhsv: new_okhsv, ··· 196 199 } 197 200 </script> 198 201 199 - <!-- svelte-ignore a11y-no-noninteractive-tabindex --> 200 - <!-- svelte-ignore a11y-no-static-element-interactions --> 202 + <!-- svelte-ignore a11y_no_noninteractive_tabindex --> 203 + <!-- svelte-ignore a11y_no_static_element_interactions --> 201 204 <div 202 - class={$$props.class} 205 + class={cn( 206 + 'focus-visible:outline-base-900 dark:focus-visible:outline-base-100 relative rounded-xl border-none focus-visible:outline-2 focus-visible:outline-offset-2', 207 + className 208 + )} 203 209 tabindex="0" 204 210 style:width="{width}px" 205 211 style:height="{height}px" 206 - on:keydown={onKeydown} 212 + onkeydown={onKeydown} 207 213 > 208 214 <canvas 209 215 id="okhsv_sv_canvas" ··· 211 217 height={picker_size} 212 218 style:top="{border_size}px" 213 219 style:left="{border_size}px" 214 - class="rounded-xl" 220 + class="absolute touch-none rounded-xl" 215 221 use:pointer={update_sv} 216 222 use:render_main_image={color.h} 217 223 ></canvas> ··· 220 226 height={picker_size} 221 227 style:top="{border_size}px" 222 228 style:left="{picker_size + gap_size}px" 223 - class="rounded-xl" 229 + class="absolute touch-none rounded-xl" 224 230 use:pointer={update_h} 225 231 use:render_slider_image 226 232 ></canvas> 227 233 228 - <svg {width} {height}> 234 + <svg {width} {height} class="pointer-events-none absolute touch-none"> 229 235 <g transform="translate({border_size},{border_size})"> 230 236 <g transform="translate({uihsv.s},{uihsv.v})"> 231 - <circle cx="0" cy="0" r="5" fill="none" stroke-width="1.75" stroke="#fff" /> 232 - <circle cx="0" cy="0" r="6" fill="none" stroke-width="1.25" stroke="#000" /> 237 + <circle cx="0" cy="0" r="5" fill="none" stroke-width="1.75" class="stroke-base-50" /> 238 + <circle cx="0" cy="0" r="6" fill="none" stroke-width="1.25" class="stroke-base-950" /> 233 239 </g> 234 240 </g> 235 241 <g transform="translate({picker_size + gap_size},{border_size})"> 236 242 <g transform="translate(0,{uihsv.h})"> 237 - <polygon points="-7,-4 -1,0 -7,4" fill="#fff" stroke-width="0.8" stroke="#000" /> 243 + <polygon points="-7,-4 -1,0 -7,4" stroke-width="0.8" class="stroke-base-950 fill-base-50" /> 238 244 <polygon 239 245 points="{slider_width + 7},-4 {slider_width + 1},0 {slider_width + 7},4" 240 - fill="#fff" 241 246 stroke-width="0.8" 242 - stroke="#000" 247 + class="stroke-base-950 fill-base-50" 243 248 /> 244 249 </g> 245 250 </g> 246 251 </svg> 247 252 </div> 248 - 249 - <style> 250 - div { 251 - position: relative; 252 - outline: 0; 253 - } 254 - 255 - canvas, 256 - svg { 257 - touch-action: none; 258 - position: absolute; 259 - } 260 - 261 - svg { 262 - pointer-events: none; 263 - } 264 - </style>
+30
src/lib/components/extra/color-picker/base/color.ts
··· 64 64 return { h, s, v }; 65 65 } 66 66 67 + export function rgb_to_oklab(rgb: RGB): OKlab { 68 + const [l, a, b] = convert([rgb.r, rgb.g, rgb.b], sRGB, OKLab); 69 + return { l, a, b }; 70 + } 71 + 67 72 export function okhsl_to_rgb(okhsl: OKhsl): RGB { 68 73 const [r, g, b] = convert([okhsl.h, okhsl.s, okhsl.l], OKHSL, sRGB); 69 74 return { r, g, b }; 75 + } 76 + 77 + export function oklab_to_oklch(oklab: OKlab): OKlch { 78 + const [l, c, h] = convert([oklab.l, oklab.a, oklab.b], OKLab, OKLCH); 79 + return { l, c, h }; 80 + } 81 + 82 + export function rgb_to_oklch(rgb: RGB): OKlch { 83 + const [l, c, h] = convert([rgb.r, rgb.g, rgb.b], sRGB, OKLCH); 84 + return { l, c, h }; 85 + } 86 + 87 + export function oklab_to_rgb(oklab: OKlab): RGB { 88 + const [r, g, b] = convert([oklab.l, oklab.a, oklab.b], OKLab, sRGB); 89 + return { r, g, b }; 90 + } 91 + 92 + export function oklch_to_rgb(oklch: OKlch): RGB { 93 + const [r, g, b] = convert([oklch.l, oklch.c, oklch.h], OKLCH, sRGB); 94 + return { r, g, b }; 95 + } 96 + 97 + export function oklch_string_to_oklch(oklch: string): OKlch { 98 + const converted = oklch.split('oklch(')[1].split(')')[0].split(' '); 99 + return { l: parseFloat(converted[0]), c: parseFloat(converted[1]), h: parseFloat(converted[2]) }; 70 100 } 71 101 72 102 export function rgb_to_hex(rgb: RGB): string {
+63
src/lib/components/extra/color-picker/popover/PopoverColorPicker.svelte
··· 1 + <script lang="ts"> 2 + import { cn } from '$lib'; 3 + import * as Popover from '$lib/components/base/popover'; 4 + import { 5 + ColorPicker, 6 + type OKhsv, 7 + type OKlab, 8 + type RGB 9 + } from '$lib/components/extra/color-picker/base'; 10 + import { okhsv_to_rgb, oklab_to_rgb, type OKlch } from '../base/color'; 11 + 12 + let { 13 + rgb = $bindable(), 14 + oklab = $bindable(), 15 + okhsv = $bindable(), 16 + class: className, 17 + onchange 18 + }: { 19 + rgb?: RGB; 20 + oklab?: OKlab; 21 + okhsv?: OKhsv; 22 + class?: string; 23 + onchange?: (color: { hex: string; rgb: RGB; oklab: OKlab; okhsv: OKhsv; oklch: OKlch }) => void; 24 + } = $props(); 25 + 26 + let internalColor = $derived(convertToInternal(rgb, oklab, okhsv)); 27 + 28 + function convertToInternal( 29 + rgb: RGB | undefined, 30 + oklab: OKlab | undefined, 31 + okhsv: OKhsv | undefined 32 + ): RGB { 33 + if (okhsv) { 34 + return okhsv_to_rgb(okhsv); 35 + } 36 + 37 + if (oklab) { 38 + return oklab_to_rgb(oklab); 39 + } 40 + 41 + if (rgb) { 42 + return rgb; 43 + } 44 + 45 + throw 'rgb, oklab, or okhsv required'; 46 + } 47 + </script> 48 + 49 + <Popover.Root> 50 + <Popover.Trigger 51 + class={cn( 52 + 'focus-visible:outline-base-900 dark:focus-visible:outline-base-100 cursor-pointer rounded-full focus-visible:outline-2 focus-visible:outline-offset-2' 53 + )} 54 + > 55 + <div 56 + class="border-base-300 dark:border-base-700 focus-visible:outline-accent-500 z-10 size-8 rounded-full border" 57 + style={`background-color: rgb(${internalColor.r * 255}, ${internalColor.g * 255}, ${internalColor.b * 255});`} 58 + ></div> 59 + </Popover.Trigger> 60 + <Popover.Content side="bottom" sideOffset={10}> 61 + <ColorPicker bind:rgb class={className} /> 62 + </Popover.Content> 63 + </Popover.Root>