[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.

update color gradient picker

Florian (Mar 22, 2025, 5:58 AM +0100) 0b769ddc c010f304

+247 -53
+1 -1
package.json
··· 1 1 { 2 2 "name": "fuchs", 3 - "version": "0.0.11", 3 + "version": "0.0.12", 4 4 "type": "module", 5 5 "description": "Beautiful UI components built with Tailwind 4 and Svelte 5", 6 6 "homepage": "https://flo-bit.dev/ui-kit",
+1
src/lib/index.ts
··· 31 31 export { Tooltip } from '$lib/components/base/tooltip'; 32 32 33 33 // extra components 34 + export { default as ColorGradientPicker } from '$lib/components/extra/color-gradient-picker/ColorGradientPicker.svelte'; 34 35 export { ColorPicker, PopoverColorPicker } from '$lib/components/extra/color-picker/'; 35 36 export { default as Excalidraw } from '$lib/components/extra/excalidraw/Excalidraw.svelte'; 36 37 export { default as Undraw } from '$lib/components/extra/undraw/Undraw.svelte';
+7
src/docs/site-components/components_extra.ts
··· 6 6 import CardTimer from '$docs/cards/extras/CardTimer.svelte'; 7 7 import CardPhone from '$docs/cards/extras/CardPhone.svelte'; 8 8 import CardQuote from '$docs/cards/extras/CardQuote.svelte'; 9 + import CardColorGradientPicker from '$docs/cards/extras/CardColorGradientPicker.svelte'; 9 10 10 11 export const extraComponents = [ 11 12 { ··· 55 56 className: '', 56 57 label: 'Quote', 57 58 href: 'quote' 59 + }, 60 + { 61 + component: CardColorGradientPicker, 62 + className: '', 63 + label: 'Color Gradient Picker', 64 + href: 'color-gradient-picker' 58 65 } 59 66 ].sort((a, b) => a.label.localeCompare(b.label));
+17
src/docs/cards/extras/CardColorGradientPicker.svelte
··· 1 + <div class="gradient-background h-4 w-full rounded-2xl relative border border-base-500"> 2 + <div class="absolute size-6 bg-accent-500 rounded-full border border-base-500 -top-3" style="left: calc(0% - 12px);"></div> 3 + <div class="absolute size-6 bg-accent-500 rounded-full border border-base-500 -top-3" style="left: calc(50% - 12px); background: oklch(from var(--color-accent-500) l c calc(h + 30));"></div> 4 + <div class="absolute size-6 bg-accent-500 rounded-full border border-base-500 -top-3" style="left: calc(100% - 12px); background: oklch(from var(--color-accent-500) l c calc(h + 60));"></div> 5 + </div> 6 + 7 + <style> 8 + /* background gradient */ 9 + .gradient-background { 10 + background: linear-gradient( 11 + to right, 12 + var(--color-accent-500), 13 + oklch(from var(--color-accent-500) l c calc(h + 30)), 14 + oklch(from var(--color-accent-500) l c calc(h + 60)) 15 + ); 16 + } 17 + </style>
+1 -1
src/lib/components/base/copy-code-button/CopyCodeButton.svelte
··· 24 24 {#if !copied} 25 25 <button 26 26 class={cn( 27 - 'not-prose focus-visible:outline-base-600 dark:focus-visible:outline-base-400 focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-offset-2', 27 + 'not-prose cursor-pointer focus-visible:outline-base-600 dark:focus-visible:outline-base-400 focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-offset-2', 28 28 'bg-base-200 inline-flex size-8 items-center justify-center p-1 transition-opacity duration-150 group-hover:opacity-100 [@media(pointer:fine)]:opacity-0', 29 29 'dark:bg-base-800 border-base-400 dark:border-base-700 dark:hover:bg-base-700 hover:bg-base-300 absolute top-3 right-3 rounded-2xl border' 30 30 )}
+146 -47
src/lib/components/extra/color-gradient-picker/ColorGradientPicker.svelte
··· 7 7 type OKlab, 8 8 type RGB 9 9 } from '$lib/components/extra/color-picker/base'; 10 + import { onMount } from 'svelte'; 10 11 import { type OKlch } from '../color-picker/base/color'; 12 + import { DragGesture } from '@use-gesture/vanilla'; 13 + import Button from '$lib/components/base/button/Button.svelte'; 11 14 12 - let { 13 - class: className, 14 - onchange 15 - }: { 16 - rgb?: RGB; 17 - oklab?: OKlab; 18 - okhsv?: OKhsv; 19 - class?: string; 20 - onchange?: (color: { hex: string; rgb: RGB; oklab: OKlab; okhsv: OKhsv; oklch: OKlch }) => void; 21 - } = $props(); 22 15 23 - type Color = { rgb: RGB; position: number }; 16 + type ColorStop = { rgb: RGB; position: number }; 24 17 25 - let colors: Color[] = $state([ 18 + let initialColors: ColorStop[] = $state([ 26 19 { rgb: { r: 0, g: 0, b: 0 }, position: 0 }, 27 20 { rgb: { r: 1, g: 0, b: 0 }, position: 0.25 }, 28 21 { rgb: { r: 0, g: 1, b: 0 }, position: 0.5 }, ··· 30 23 { rgb: { r: 1, g: 1, b: 0 }, position: 1 } 31 24 ]); 32 25 33 - $effect(() => { 34 - colors.sort((a, b) => a.position - b.position); 35 - }); 26 + let { 27 + colors = $bindable(initialColors), 28 + class: className, 29 + onchange 30 + }: { 31 + rgb?: RGB; 32 + 33 + colors?: ColorStop[]; 34 + 35 + class?: string; 36 + 37 + onchange?: (color: { hex: string; rgb: RGB; oklab: OKlab; okhsv: OKhsv; oklch: OKlch }) => void; 38 + } = $props(); 36 39 37 40 const gradientString = $derived( 38 41 `linear-gradient(to right, ${colors 39 - .map(({ rgb, position }) => `rgb(${rgb.r * 255}, ${rgb.g * 255}, ${rgb.b * 255}) ${position * 100}%`) 42 + .toSorted((a, b) => a.position - b.position) 43 + .map( 44 + ({ rgb, position }) => 45 + `rgb(${rgb.r * 255}, ${rgb.g * 255}, ${rgb.b * 255}) ${position * 100}%` 46 + ) 40 47 .join(', ')})` 41 48 ); 42 49 43 50 $inspect(gradientString); 44 51 45 - let activeColor: Color | undefined = $state(undefined); 46 - let activePosition: number | undefined = $state(undefined); 47 - let mouseDownPosition: number | undefined = $state(undefined); 48 - 52 + let colorsRef = $state<(HTMLDivElement | null)[]>(new Array(colors.length).fill(null)); 49 53 let gradientRef = $state<HTMLDivElement | undefined>(undefined); 54 + let isDragging = $state(new Array(colors.length).fill(false)); 55 + 56 + $effect(() => { 57 + let gestures: DragGesture[] = []; 58 + 59 + colorsRef.forEach((ref, i) => { 60 + if (!ref) return; 61 + 62 + gestures.push( 63 + new DragGesture(ref, (state) => { 64 + console.log(state); 65 + const { delta } = state; 66 + 67 + const newPosition = delta[0] / (gradientRef?.clientWidth ?? 1); 68 + 69 + colors[i].position += newPosition; 70 + colors[i].position = Math.max(0, Math.min(1, colors[i].position)); 71 + 72 + if (Math.abs(state.offset[0]) > 10) { 73 + isDragging[i] = state.active; 74 + } else { 75 + isDragging[i] = state.active; 76 + } 77 + 78 + // let offset = Math.hypot(state.movement[0], state.movement[1]); 79 + if (state.tap) { 80 + allOpen[i] = !allOpen[i]; 81 + } 82 + }) 83 + ); 84 + }); 85 + 86 + return () => { 87 + gestures.forEach((gesture) => gesture.destroy()); 88 + }; 89 + }); 90 + 91 + let allOpen = $state(new Array(colors.length).fill(false)); 92 + 93 + function getOpen(i: number) { 94 + if (isDragging[i]) { 95 + return false; 96 + } 97 + return allOpen[i]; 98 + } 50 99 </script> 51 100 101 + <!-- svelte-ignore a11y_no_static_element_interactions --> 102 + <!-- svelte-ignore a11y_click_events_have_key_events --> 52 103 <div 53 - class="relative h-8 w-full rounded-2xl border border-base-300 dark:border-base-700 touch-none" 104 + class={cn( 105 + 'border-base-400 dark:border-base-600 relative h-8 w-full cursor-pointer touch-none rounded-2xl border', 106 + className 107 + )} 54 108 style={'background: ' + gradientString} 55 109 bind:this={gradientRef} 56 - 57 - onpointermove={(evt) => { 58 - if (activeColor && activePosition !== undefined && mouseDownPosition !== undefined) { 59 - const delta = evt.clientX - mouseDownPosition; 60 - 61 - // get the gradient bounding box 62 - const gradientBoundingBox = gradientRef?.getBoundingClientRect(); 63 - let movement = delta / (gradientBoundingBox?.width ?? 1); 64 - 65 - const newPosition = activePosition + movement; 66 - activeColor.position = newPosition; 110 + onclick={(e) => { 111 + // get target 112 + const target = e.target as HTMLDivElement; 113 + // if target is not the gradient itself return 114 + if (target !== gradientRef) { 115 + return; 116 + } 117 + let rect = gradientRef?.getBoundingClientRect(); 118 + // get the position of the click 119 + const position = (e.clientX - rect.left) / (gradientRef?.clientWidth ?? 1); 120 + // add a new color 121 + colors.push({ rgb: { r: 0, g: 0, b: 0 }, position: position }); 122 + colorsRef.push(null); 123 + allOpen.push(true); 124 + }} 125 + onkeydown={(e) => { 126 + if (e.key === '+') { 127 + colors.push({ rgb: { r: 0, g: 0, b: 0 }, position: 0.5 }); 128 + colorsRef.push(null); 129 + allOpen.push(true); 67 130 } 68 131 }} 69 132 > 70 133 {#each colors as color, i} 71 - <Popover.Root> 134 + <Popover.Root bind:open={() => getOpen(i), (open) => {}}> 72 135 <Popover.Trigger 73 136 class={cn( 74 - 'absolute top-4 left-0', 137 + 'absolute -top-4 left-0 touch-none', 75 138 'focus-visible:outline-base-900 dark:focus-visible:outline-base-100 cursor-pointer rounded-full focus-visible:outline-2 focus-visible:outline-offset-2' 76 139 )} 77 140 style={`left: calc(${color.position * 100}% - 16px)`} 78 - onpointerdown={(evt) => { 79 - activeColor = color; 80 - mouseDownPosition = evt.clientX; 81 - activePosition = color.position; 82 - }} 83 - onpointerup={() => { 84 - activeColor = undefined; 85 - activePosition = undefined; 86 - mouseDownPosition = undefined; 141 + bind:ref={colorsRef[i]} 142 + tabindex={Math.floor(color.position * 100 + 10)} 143 + onkeydown={(e) => { 144 + if (e.key === 'Enter') { 145 + allOpen[i] = !allOpen[i]; 146 + } else if (e.key === 'Backspace' && colors.length > 2) { 147 + colors.splice(i, 1); 148 + } 87 149 }} 88 150 > 89 151 <div 90 - class="border-base-300 dark:border-base-950 focus-visible:outline-accent-500 z-10 size-8 rounded-full border-2" 152 + class="border-base-400 dark:border-base-500 focus-visible:outline-accent-500 shadow-base-900/50 z-10 size-8 rounded-full border shadow-lg" 91 153 style={`background-color: rgb(${color.rgb.r * 255}, ${color.rgb.g * 255}, ${color.rgb.b * 255});`} 92 154 ></div> 93 - 94 - <div class="absolute inset-0 size-8 rounded-full border border-base-700 dark:border-base-300"></div> 95 155 </Popover.Trigger> 96 - <Popover.Content side="bottom" sideOffset={10}> 156 + <Popover.Content 157 + side="bottom" 158 + sideOffset={10} 159 + onInteractOutside={() => { 160 + allOpen[i] = false; 161 + }} 162 + onEscapeKeydown={() => { 163 + allOpen[i] = false; 164 + }} 165 + onkeydown={(e) => { 166 + if (e.key === 'Backspace' && colors.length > 2) { 167 + colors.splice(i, 1); 168 + } 169 + }} 170 + > 97 171 <ColorPicker bind:rgb={colors[i].rgb} class={className} /> 172 + 173 + {#if colors.length > 2} 174 + <Button 175 + class="mt-4" 176 + onclick={() => { 177 + colors.splice(i, 1); 178 + }} 179 + size="sm" 180 + > 181 + <svg 182 + xmlns="http://www.w3.org/2000/svg" 183 + viewBox="0 0 24 24" 184 + fill="currentColor" 185 + class="size-6" 186 + > 187 + <path 188 + fill-rule="evenodd" 189 + d="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z" 190 + clip-rule="evenodd" 191 + /> 192 + </svg> 193 + 194 + Delete Gradient Stop</Button 195 + > 196 + {/if} 98 197 </Popover.Content> 99 198 </Popover.Root> 100 199 {/each}
+6 -4
src/routes/(main)/components/extras/color-gradient-picker/+page.svelte
··· 1 - <script> 2 - import ColorGradientPicker from "$lib/components/extra/color-gradient-picker/ColorGradientPicker.svelte"; 3 - 1 + <script lang="ts"> 2 + import ColorGradientPickerDocs from './ColorGradientPicker.md'; 3 + import Prose from '$lib/components/base/prose/Prose.svelte'; 4 4 </script> 5 5 6 - <ColorGradientPicker /> 6 + <Prose> 7 + <ColorGradientPickerDocs /> 8 + </Prose>
+26
src/routes/(main)/components/extras/color-gradient-picker/ColorGradientPicker.md
··· 1 + <script lang="ts"> 2 + import ColorGradientPickerExample from './Example.svelte'; 3 + </script> 4 + 5 + # Color Gradient Picker 6 + 7 + ## Example 8 + 9 + <ColorGradientPickerExample /> 10 + 11 + ## Usage 12 + 13 + ```svelte 14 + <script lang="ts"> 15 + import ColorGradientPicker from 'fuchs'; 16 + 17 + let colors = $state([ 18 + { rgb: { r: 0, g: 0, b: 1 }, position: 0 }, // blue 19 + { rgb: { r: 1, g: 0, b: 0 }, position: 0.5 }, // red 20 + { rgb: { r: 1, g: 1, b: 0 }, position: 1 } // yellow 21 + ]); 22 + </script> 23 + 24 + <ColorGradientPicker bind:colors /> 25 + ``` 26 +
+42
src/routes/(main)/components/extras/color-gradient-picker/Example.svelte
··· 1 + <script lang="ts"> 2 + import ColorGradientPicker from '$lib/components/extra/color-gradient-picker/ColorGradientPicker.svelte'; 3 + import type { OKlch } from '$lib/components/extra/color-picker/base/color'; 4 + import { 5 + oklch_string_to_oklch, 6 + oklch_to_rgb 7 + } from '$lib/components/extra/color-picker/base/color'; 8 + import { ThemeWatcher } from '$lib/helper/ThemeWatcher.svelte'; 9 + import { onMount } from 'svelte'; 10 + 11 + let colors = $state([ 12 + { rgb: { r: 0, g: 0, b: 0 }, position: 0 }, 13 + { rgb: { r: 0, g: 0, b: 0 }, position: 0.5 }, 14 + { rgb: { r: 0, g: 0, b: 0 }, position: 1 } 15 + ]); 16 + 17 + onMount(() => { 18 + const theme = new ThemeWatcher(); 19 + const accentColor = theme.getCSSVar('--color-accent-500'); 20 + let oklch = oklch_string_to_oklch(accentColor); 21 + updateColors(oklch); 22 + 23 + theme.subscribe(() => { 24 + const accentColor = theme.getCSSVar('--color-accent-500'); 25 + let oklch = oklch_string_to_oklch(accentColor); 26 + updateColors(oklch); 27 + }); 28 + }); 29 + 30 + function updateColors(theme: OKlch) { 31 + colors = [ 32 + { rgb: { r: 0, g: 0, b: 0 }, position: 0 }, 33 + { rgb: { r: 0, g: 0, b: 0 }, position: 0.5 }, 34 + { rgb: { r: 0, g: 0, b: 0 }, position: 1 } 35 + ]; 36 + colors.forEach((color, index) => { 37 + colors[index].rgb = oklch_to_rgb({ ...theme, h: theme.h + index * 30 }); 38 + }); 39 + } 40 + </script> 41 + 42 + <ColorGradientPicker class="mt-8" bind:colors />