@finsweet/webflow-apps-utils 1.0.9 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ui/components/LoadingScreen.svelte +6 -2
- package/dist/ui/components/LoadingScreen.svelte.d.ts +1 -0
- package/dist/ui/components/breakpoints/BreakpointItem.svelte +2 -2
- package/dist/ui/components/color-picker/ColorPicker.stories.svelte +42 -0
- package/dist/ui/components/color-picker/ColorPicker.stories.svelte.d.ts +19 -0
- package/dist/ui/components/color-picker/ColorPicker.svelte +155 -0
- package/dist/ui/components/color-picker/ColorPicker.svelte.d.ts +8 -0
- package/dist/ui/components/color-picker/ColorSelect.stories.svelte +61 -0
- package/dist/ui/components/color-picker/ColorSelect.stories.svelte.d.ts +27 -0
- package/dist/ui/components/color-picker/ColorSelect.svelte +940 -0
- package/dist/ui/components/color-picker/ColorSelect.svelte.d.ts +7 -0
- package/dist/ui/components/color-picker/index.d.ts +1 -0
- package/dist/ui/components/color-picker/index.js +1 -0
- package/dist/ui/components/layout/examples/ExampleLayout.svelte +8 -0
- package/dist/ui/components/notification/Notification.svelte +2 -2
- package/dist/ui/components/tooltip/Tooltip.svelte +0 -1
- package/package.json +1 -1
|
@@ -10,10 +10,14 @@
|
|
|
10
10
|
export let raw = false;
|
|
11
11
|
export let backgroundColor = 'rgba(30, 30, 30, 0.96)';
|
|
12
12
|
export let spinnerSize = 50;
|
|
13
|
+
export let className: string = '';
|
|
13
14
|
</script>
|
|
14
15
|
|
|
15
16
|
{#if active}
|
|
16
|
-
<div
|
|
17
|
+
<div
|
|
18
|
+
class="main-loader {className}"
|
|
19
|
+
style="position:{position}; background-color: {backgroundColor};"
|
|
20
|
+
>
|
|
17
21
|
<div class="loading-info {error ? 'error' : ''}">
|
|
18
22
|
{#if error}
|
|
19
23
|
<WarningTriangleOutlineIcon />
|
|
@@ -32,7 +36,7 @@
|
|
|
32
36
|
{/if}
|
|
33
37
|
|
|
34
38
|
<style>
|
|
35
|
-
.
|
|
39
|
+
.main-loader {
|
|
36
40
|
top: 0;
|
|
37
41
|
bottom: 0;
|
|
38
42
|
left: 0;
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
<Switch {disabled} bind:checked={enabled} onchange={handleSwitchChange} />
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
|
-
<div class="content" style="display: {enabled ? 'flex' : 'none'}">
|
|
34
|
+
<div class="breakpoint-content" style="display: {enabled ? 'flex' : 'none'}">
|
|
35
35
|
<slot />
|
|
36
36
|
</div>
|
|
37
37
|
</div>
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
gap: var(--Spacing-8, 8px);
|
|
47
47
|
align-self: stretch;
|
|
48
48
|
}
|
|
49
|
-
.content {
|
|
49
|
+
.breakpoint-content {
|
|
50
50
|
display: flex;
|
|
51
51
|
flex-direction: column;
|
|
52
52
|
align-items: flex-start;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import { defineMeta } from '@storybook/addon-svelte-csf';
|
|
3
|
+
import { fn } from 'storybook/test';
|
|
4
|
+
|
|
5
|
+
import ColorPicker from './ColorPicker.svelte';
|
|
6
|
+
|
|
7
|
+
let controlledColor = $state('#00ff00');
|
|
8
|
+
const handleChange = (color: string) => (controlledColor = color);
|
|
9
|
+
|
|
10
|
+
const { Story } = defineMeta({
|
|
11
|
+
title: 'UI/ColorPicker',
|
|
12
|
+
component: ColorPicker,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
argTypes: {
|
|
15
|
+
color: {
|
|
16
|
+
control: { type: 'color' },
|
|
17
|
+
description: 'Selected color (hex string or writable store)'
|
|
18
|
+
},
|
|
19
|
+
onchange: { action: 'colorChanged' }
|
|
20
|
+
},
|
|
21
|
+
parameters: {
|
|
22
|
+
layout: 'centered',
|
|
23
|
+
viewport: {
|
|
24
|
+
defaultViewport: 'responsive'
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
args: {
|
|
28
|
+
color: '#ff0000',
|
|
29
|
+
onchange: fn()
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<Story name="Default" args={{}} />
|
|
35
|
+
|
|
36
|
+
<Story name="With Initial Color" args={{ color: '#00ff00' }} />
|
|
37
|
+
|
|
38
|
+
<Story name="Store Controlled" args={{ color: controlledColor, onchange: handleChange }} />
|
|
39
|
+
|
|
40
|
+
<Story name="Edge: Invalid Color" args={{ color: '#xyzxyz' }} />
|
|
41
|
+
|
|
42
|
+
<Story name="Edge: No Color" args={{ color: undefined }} />
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import ColorPicker from './ColorPicker.svelte';
|
|
2
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
3
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
4
|
+
$$bindings?: Bindings;
|
|
5
|
+
} & Exports;
|
|
6
|
+
(internal: unknown, props: {
|
|
7
|
+
$$events?: Events;
|
|
8
|
+
$$slots?: Slots;
|
|
9
|
+
}): Exports & {
|
|
10
|
+
$set?: any;
|
|
11
|
+
$on?: any;
|
|
12
|
+
};
|
|
13
|
+
z_$$bindings?: Bindings;
|
|
14
|
+
}
|
|
15
|
+
declare const ColorPicker: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
16
|
+
[evt: string]: CustomEvent<any>;
|
|
17
|
+
}, {}, {}, string>;
|
|
18
|
+
type ColorPicker = InstanceType<typeof ColorPicker>;
|
|
19
|
+
export default ColorPicker;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Tooltip from '../tooltip/Tooltip.svelte';
|
|
3
|
+
import ColorSelect from './ColorSelect.svelte';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
color = $bindable('#fff'),
|
|
7
|
+
onchange,
|
|
8
|
+
width = '80px'
|
|
9
|
+
} = $props<{
|
|
10
|
+
color?: string;
|
|
11
|
+
onchange?: (color: string) => void;
|
|
12
|
+
width?: string;
|
|
13
|
+
}>();
|
|
14
|
+
|
|
15
|
+
function normalizeHex(value: string): string {
|
|
16
|
+
let v = value.trim();
|
|
17
|
+
|
|
18
|
+
if (!v.startsWith('#')) v = `#${v}`;
|
|
19
|
+
|
|
20
|
+
return v.toUpperCase();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isValidColor(value: string): boolean {
|
|
24
|
+
const hexRegex = /^#?([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/;
|
|
25
|
+
return hexRegex.test(value.startsWith('#') ? value.slice(1) : value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function handleInputPaste(event: ClipboardEvent) {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
|
|
31
|
+
const pastedText = event.clipboardData?.getData('text') || '';
|
|
32
|
+
let cleanText = pastedText.replace(/[^0-9A-Fa-f#]/g, '');
|
|
33
|
+
if (!cleanText.startsWith('#')) cleanText = '#' + cleanText;
|
|
34
|
+
|
|
35
|
+
cleanText = cleanText.substring(0, 9);
|
|
36
|
+
|
|
37
|
+
if (isValidColor(cleanText)) {
|
|
38
|
+
const normalizedValue = normalizeHex(cleanText);
|
|
39
|
+
color = normalizedValue;
|
|
40
|
+
onchange?.(normalizedValue);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function handleInputChange(event: Event) {
|
|
45
|
+
const target = event.target as HTMLInputElement;
|
|
46
|
+
const value = target.value.trim();
|
|
47
|
+
|
|
48
|
+
if (isValidColor(value)) {
|
|
49
|
+
const normalizedValue = normalizeHex(value);
|
|
50
|
+
|
|
51
|
+
color = normalizedValue;
|
|
52
|
+
|
|
53
|
+
onchange?.(normalizedValue);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function handleInputKeydown(event: KeyboardEvent) {
|
|
58
|
+
if (event.key === 'Enter') {
|
|
59
|
+
(event.target as HTMLInputElement).blur();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function handleColorChange(newColor: string) {
|
|
64
|
+
const normalizedValue = normalizeHex(newColor);
|
|
65
|
+
|
|
66
|
+
color = normalizedValue;
|
|
67
|
+
onchange?.(normalizedValue);
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<div class="color-picker">
|
|
72
|
+
<Tooltip
|
|
73
|
+
listener="click"
|
|
74
|
+
listenerout="click"
|
|
75
|
+
showArrow={false}
|
|
76
|
+
padding="0"
|
|
77
|
+
stopPropagation={true}
|
|
78
|
+
width="241px"
|
|
79
|
+
placement="bottom"
|
|
80
|
+
fallbackPlacements={['top-end', 'top', 'bottom-end', 'bottom', 'top-start', 'bottom-start']}
|
|
81
|
+
>
|
|
82
|
+
{#snippet target()}
|
|
83
|
+
<div class="color-picker__swatch" data-testid="color-swatch">
|
|
84
|
+
<div class="color-swatch" style="background-color: {color || '#000000'}"></div>
|
|
85
|
+
</div>
|
|
86
|
+
{/snippet}
|
|
87
|
+
{#snippet tooltipContent()}
|
|
88
|
+
<ColorSelect {color} onchange={handleColorChange} />
|
|
89
|
+
{/snippet}
|
|
90
|
+
</Tooltip>
|
|
91
|
+
|
|
92
|
+
<input
|
|
93
|
+
type="text"
|
|
94
|
+
class="color-picker__input"
|
|
95
|
+
bind:value={color}
|
|
96
|
+
oninput={handleInputChange}
|
|
97
|
+
onkeydown={handleInputKeydown}
|
|
98
|
+
onpaste={handleInputPaste}
|
|
99
|
+
placeholder="#ffffff"
|
|
100
|
+
aria-label="Color hex value"
|
|
101
|
+
style="width: {width}"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<style>
|
|
106
|
+
.color-picker {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
gap: 4px;
|
|
110
|
+
height: 24px;
|
|
111
|
+
border: 1px solid var(--border1);
|
|
112
|
+
border-radius: 4px;
|
|
113
|
+
overflow: hidden;
|
|
114
|
+
background: var(--background1);
|
|
115
|
+
box-shadow:
|
|
116
|
+
0px 16px 16px -16px rgba(0, 0, 0, 0.13) inset,
|
|
117
|
+
0px 12px 12px -12px rgba(0, 0, 0, 0.13) inset,
|
|
118
|
+
0px 8px 8px -8px rgba(0, 0, 0, 0.17) inset,
|
|
119
|
+
0px 4px 4px -4px rgba(0, 0, 0, 0.17) inset,
|
|
120
|
+
0px 3px 3px -3px rgba(0, 0, 0, 0.17) inset,
|
|
121
|
+
0px 1px 1px -1px rgba(0, 0, 0, 0.13) inset;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.color-picker__swatch {
|
|
125
|
+
position: relative;
|
|
126
|
+
width: 24px;
|
|
127
|
+
height: 24px;
|
|
128
|
+
overflow: hidden;
|
|
129
|
+
cursor: pointer;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.color-swatch {
|
|
133
|
+
width: 100%;
|
|
134
|
+
height: 100%;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.color-picker__input {
|
|
138
|
+
width: 80px;
|
|
139
|
+
padding: 4px 8px 4px 0px;
|
|
140
|
+
border-radius: 4px;
|
|
141
|
+
font-family: monospace;
|
|
142
|
+
font-size: 12px;
|
|
143
|
+
border: none;
|
|
144
|
+
background: transparent;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.color-picker:has(.color-picker__input:focus) {
|
|
148
|
+
outline: none;
|
|
149
|
+
border-color: var(--blueBorder);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.color-picker:global(:has(.tooltip[aria-hidden='false'])) {
|
|
153
|
+
border-color: var(--blueBorder);
|
|
154
|
+
}
|
|
155
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
color?: string;
|
|
3
|
+
onchange?: (color: string) => void;
|
|
4
|
+
width?: string;
|
|
5
|
+
};
|
|
6
|
+
declare const ColorPicker: import("svelte").Component<$$ComponentProps, {}, "color">;
|
|
7
|
+
type ColorPicker = ReturnType<typeof ColorPicker>;
|
|
8
|
+
export default ColorPicker;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script module>
|
|
2
|
+
import { defineMeta } from '@storybook/addon-svelte-csf';
|
|
3
|
+
import { fn } from 'storybook/test';
|
|
4
|
+
|
|
5
|
+
import ColorSelect from './ColorSelect.svelte';
|
|
6
|
+
|
|
7
|
+
const { Story } = defineMeta({
|
|
8
|
+
title: 'UI/ColorSelect',
|
|
9
|
+
component: ColorSelect,
|
|
10
|
+
tags: ['autodocs'],
|
|
11
|
+
argTypes: {
|
|
12
|
+
color: {
|
|
13
|
+
control: { type: 'color' },
|
|
14
|
+
description: 'Initial color value (hex)'
|
|
15
|
+
},
|
|
16
|
+
onchange: { action: 'colorChanged' }
|
|
17
|
+
},
|
|
18
|
+
args: {
|
|
19
|
+
color: '#ff0000',
|
|
20
|
+
onchange: fn()
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<Story name="Default" args={{}} />
|
|
26
|
+
|
|
27
|
+
<Story name="With Initial Color" args={{ color: '#00ff00' }} />
|
|
28
|
+
|
|
29
|
+
<Story
|
|
30
|
+
name="Alpha Channel"
|
|
31
|
+
args={{ color: '#0000ff' }}
|
|
32
|
+
story={{
|
|
33
|
+
parameters: {
|
|
34
|
+
docs: { description: { story: 'Shows alpha slider and allows changing opacity.' } }
|
|
35
|
+
}
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
|
|
39
|
+
<Story
|
|
40
|
+
name="HSB/RGB Toggle"
|
|
41
|
+
args={{ color: '#ff00ff' }}
|
|
42
|
+
story={{
|
|
43
|
+
parameters: {
|
|
44
|
+
docs: { description: { story: 'Toggle between HSB and RGB modes using the mode switch.' } }
|
|
45
|
+
}
|
|
46
|
+
}}
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
<Story
|
|
50
|
+
name="With EyeDropper"
|
|
51
|
+
args={{ color: '#123456' }}
|
|
52
|
+
story={{
|
|
53
|
+
parameters: {
|
|
54
|
+
docs: {
|
|
55
|
+
description: {
|
|
56
|
+
story: 'Use the EyeDropper button to pick a color from the screen (if supported).'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export default ColorSelect;
|
|
2
|
+
type ColorSelect = SvelteComponent<{
|
|
3
|
+
[x: string]: never;
|
|
4
|
+
}, {
|
|
5
|
+
[evt: string]: CustomEvent<any>;
|
|
6
|
+
}, {}> & {
|
|
7
|
+
$$bindings?: string | undefined;
|
|
8
|
+
};
|
|
9
|
+
declare const ColorSelect: $$__sveltets_2_IsomorphicComponent<{
|
|
10
|
+
[x: string]: never;
|
|
11
|
+
}, {
|
|
12
|
+
[evt: string]: CustomEvent<any>;
|
|
13
|
+
}, {}, {}, string>;
|
|
14
|
+
import ColorSelect from './ColorSelect.svelte';
|
|
15
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
16
|
+
new (options: import("svelte").ComponentConstructorOptions<Props>): import("svelte").SvelteComponent<Props, Events, Slots> & {
|
|
17
|
+
$$bindings?: Bindings;
|
|
18
|
+
} & Exports;
|
|
19
|
+
(internal: unknown, props: {
|
|
20
|
+
$$events?: Events;
|
|
21
|
+
$$slots?: Slots;
|
|
22
|
+
}): Exports & {
|
|
23
|
+
$set?: any;
|
|
24
|
+
$on?: any;
|
|
25
|
+
};
|
|
26
|
+
z_$$bindings?: Bindings;
|
|
27
|
+
}
|
|
@@ -0,0 +1,940 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
|
|
4
|
+
let { color = $bindable('#fff'), onchange } = $props<{
|
|
5
|
+
color?: string;
|
|
6
|
+
onchange?: (color: string) => void;
|
|
7
|
+
}>();
|
|
8
|
+
|
|
9
|
+
function setColor(newColor: string) {
|
|
10
|
+
color = newColor;
|
|
11
|
+
onchange?.(newColor);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let hue = $state(0);
|
|
15
|
+
let saturation = $state(0);
|
|
16
|
+
let brightness = $state(0);
|
|
17
|
+
let alpha = $state(100);
|
|
18
|
+
let hexValue = $state(color);
|
|
19
|
+
let mode = $state<'HSB' | 'RGB'>('HSB');
|
|
20
|
+
|
|
21
|
+
// RGB values for RGB mode
|
|
22
|
+
let rgbRed = $state(0);
|
|
23
|
+
let rgbGreen = $state(0);
|
|
24
|
+
let rgbBlue = $state(0);
|
|
25
|
+
|
|
26
|
+
// DOM references
|
|
27
|
+
let colorWell: HTMLElement;
|
|
28
|
+
let colorPicker: HTMLElement;
|
|
29
|
+
let hueBar: HTMLElement;
|
|
30
|
+
let alphaBar: HTMLElement;
|
|
31
|
+
|
|
32
|
+
// Interaction state
|
|
33
|
+
let isDragging = $state(false);
|
|
34
|
+
let dragTarget = $state<'well' | 'hue' | 'alpha' | null>(null);
|
|
35
|
+
|
|
36
|
+
// Drag detection state
|
|
37
|
+
let dragStartPosition = $state<{ x: number; y: number } | null>(null);
|
|
38
|
+
const DRAG_THRESHOLD = 3; // pixels of movement before considering it a drag
|
|
39
|
+
|
|
40
|
+
// Color conversion utilities
|
|
41
|
+
function hsbToRgb(h: number, s: number, b: number): [number, number, number] {
|
|
42
|
+
h = h / 360;
|
|
43
|
+
s = s / 100;
|
|
44
|
+
b = b / 100;
|
|
45
|
+
const c = b * s;
|
|
46
|
+
const x = c * (1 - Math.abs(((h * 6) % 2) - 1));
|
|
47
|
+
const m = b - c;
|
|
48
|
+
let r = 0,
|
|
49
|
+
g = 0,
|
|
50
|
+
bl = 0;
|
|
51
|
+
|
|
52
|
+
if (0 <= h && h < 1 / 6) {
|
|
53
|
+
r = c;
|
|
54
|
+
g = x;
|
|
55
|
+
bl = 0;
|
|
56
|
+
} else if (1 / 6 <= h && h < 2 / 6) {
|
|
57
|
+
r = x;
|
|
58
|
+
g = c;
|
|
59
|
+
bl = 0;
|
|
60
|
+
} else if (2 / 6 <= h && h < 3 / 6) {
|
|
61
|
+
r = 0;
|
|
62
|
+
g = c;
|
|
63
|
+
bl = x;
|
|
64
|
+
} else if (3 / 6 <= h && h < 4 / 6) {
|
|
65
|
+
r = 0;
|
|
66
|
+
g = x;
|
|
67
|
+
bl = c;
|
|
68
|
+
} else if (4 / 6 <= h && h < 5 / 6) {
|
|
69
|
+
r = x;
|
|
70
|
+
g = 0;
|
|
71
|
+
bl = c;
|
|
72
|
+
} else if (5 / 6 <= h && h < 1) {
|
|
73
|
+
r = c;
|
|
74
|
+
g = 0;
|
|
75
|
+
bl = x;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((bl + m) * 255)];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function rgbToHex(r: number, g: number, b: number): string {
|
|
82
|
+
return (
|
|
83
|
+
'#' +
|
|
84
|
+
[r, g, b]
|
|
85
|
+
.map((x) => {
|
|
86
|
+
const hex = x.toString(16);
|
|
87
|
+
return hex.length === 1 ? '0' + hex : hex;
|
|
88
|
+
})
|
|
89
|
+
.join('')
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hexToHsb(hex: string): [number, number, number] {
|
|
94
|
+
// Handle different hex formats
|
|
95
|
+
let normalizedHex = hex;
|
|
96
|
+
if (hex.startsWith('#')) {
|
|
97
|
+
// Expand 3-digit hex to 6-digit
|
|
98
|
+
if (hex.length === 4) {
|
|
99
|
+
normalizedHex = '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3];
|
|
100
|
+
}
|
|
101
|
+
// Remove alpha channel if present (8-digit hex)
|
|
102
|
+
if (hex.length === 9) {
|
|
103
|
+
normalizedHex = hex.substring(0, 7);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const r = parseInt(normalizedHex.slice(1, 3), 16) / 255;
|
|
108
|
+
const g = parseInt(normalizedHex.slice(3, 5), 16) / 255;
|
|
109
|
+
const b = parseInt(normalizedHex.slice(5, 7), 16) / 255;
|
|
110
|
+
const max = Math.max(r, g, b);
|
|
111
|
+
const min = Math.min(r, g, b);
|
|
112
|
+
const diff = max - min;
|
|
113
|
+
let h = 0;
|
|
114
|
+
let s = max === 0 ? 0 : diff / max;
|
|
115
|
+
let v = max;
|
|
116
|
+
|
|
117
|
+
if (diff !== 0) {
|
|
118
|
+
if (max === r) {
|
|
119
|
+
h = ((g - b) / diff) % 6;
|
|
120
|
+
} else if (max === g) {
|
|
121
|
+
h = (b - r) / diff + 2;
|
|
122
|
+
} else {
|
|
123
|
+
h = (r - g) / diff + 4;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
h = Math.round(h * 60);
|
|
128
|
+
if (h < 0) h += 360;
|
|
129
|
+
return [h, Math.round(s * 100), Math.round(v * 100)];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Color validation utility using CSS.supports()
|
|
133
|
+
function isValidColor(value: unknown): boolean {
|
|
134
|
+
// Ensure value is a string
|
|
135
|
+
if (typeof value !== 'string') {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Handle hex colors specifically
|
|
140
|
+
if (value.startsWith('#')) {
|
|
141
|
+
// Check for valid hex format: #RGB, #RRGGBB, #RRGGBBAA
|
|
142
|
+
const hexRegex = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
|
|
143
|
+
const result = hexRegex.test(value);
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// For non-hex colors, only accept valid hex patterns (without #)
|
|
148
|
+
const hexRegex = /^([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
|
|
149
|
+
const result = hexRegex.test(value);
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function rgbToHsb(r: number, g: number, b: number): [number, number, number] {
|
|
154
|
+
return hexToHsb(rgbToHex(r, g, b));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Update functions
|
|
158
|
+
function updateColor() {
|
|
159
|
+
let r, g, b;
|
|
160
|
+
if (mode === 'HSB') {
|
|
161
|
+
[r, g, b] = hsbToRgb(hue, saturation, brightness);
|
|
162
|
+
} else {
|
|
163
|
+
r = rgbRed;
|
|
164
|
+
g = rgbGreen;
|
|
165
|
+
b = rgbBlue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
hexValue = rgbToHex(r, g, b).toUpperCase();
|
|
169
|
+
setColor(hexValue);
|
|
170
|
+
updateColorPickerPosition();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function syncModeValues(newMode: 'HSB' | 'RGB') {
|
|
174
|
+
if (newMode === 'RGB') {
|
|
175
|
+
[rgbRed, rgbGreen, rgbBlue] = hsbToRgb(hue, saturation, brightness);
|
|
176
|
+
} else {
|
|
177
|
+
[hue, saturation, brightness] = rgbToHsb(rgbRed, rgbGreen, rgbBlue);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function toggleMode() {
|
|
182
|
+
mode = mode === 'HSB' ? 'RGB' : 'HSB';
|
|
183
|
+
syncModeValues(mode);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function updateColorPickerPosition() {
|
|
187
|
+
if (!colorPicker || !colorWell) return;
|
|
188
|
+
const wellRect = colorWell.getBoundingClientRect();
|
|
189
|
+
const x = (saturation / 100) * wellRect.width;
|
|
190
|
+
const y = ((100 - brightness) / 100) * wellRect.height;
|
|
191
|
+
colorPicker.style.left = `${x - 6}px`;
|
|
192
|
+
colorPicker.style.top = `${y - 6}px`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function handleColorWellInteraction(event: MouseEvent) {
|
|
196
|
+
if (!colorWell) return;
|
|
197
|
+
const rect = colorWell.getBoundingClientRect();
|
|
198
|
+
const x = Math.max(0, Math.min(rect.width, event.clientX - rect.left));
|
|
199
|
+
const y = Math.max(0, Math.min(rect.height, event.clientY - rect.top));
|
|
200
|
+
saturation = Math.round((x / rect.width) * 100);
|
|
201
|
+
brightness = Math.round(100 - (y / rect.height) * 100);
|
|
202
|
+
updateColor();
|
|
203
|
+
updateColorPickerPosition();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function handleHueBarInteraction(event: MouseEvent) {
|
|
207
|
+
if (!hueBar) return;
|
|
208
|
+
const rect = hueBar.getBoundingClientRect();
|
|
209
|
+
const x = Math.max(0, Math.min(rect.width, event.clientX - rect.left));
|
|
210
|
+
hue = Math.round((x / rect.width) * 360);
|
|
211
|
+
updateColor();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function handleAlphaBarInteraction(event: MouseEvent) {
|
|
215
|
+
if (!alphaBar) return;
|
|
216
|
+
const rect = alphaBar.getBoundingClientRect();
|
|
217
|
+
const x = Math.max(0, Math.min(rect.width, event.clientX - rect.left));
|
|
218
|
+
alpha = Math.round((x / rect.width) * 100);
|
|
219
|
+
if (onchange) onchange(hexValue);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function handleHexChange(event: Event) {
|
|
223
|
+
const target = event.target as HTMLInputElement;
|
|
224
|
+
let value = target.value;
|
|
225
|
+
|
|
226
|
+
if (!value.startsWith('#')) {
|
|
227
|
+
value = '#' + value;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (isValidColor(value)) {
|
|
231
|
+
hexValue = value.toUpperCase();
|
|
232
|
+
setColor(hexValue);
|
|
233
|
+
const [h, s, b] = hexToHsb(value);
|
|
234
|
+
hue = h;
|
|
235
|
+
saturation = s;
|
|
236
|
+
brightness = b;
|
|
237
|
+
[rgbRed, rgbGreen, rgbBlue] = hsbToRgb(hue, saturation, brightness);
|
|
238
|
+
updateColorPickerPosition();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let hexInputValue = $state('');
|
|
243
|
+
let isHexEditing = $state(false);
|
|
244
|
+
let prevHexValue = $state('');
|
|
245
|
+
|
|
246
|
+
function handleHexFocus(event: FocusEvent) {
|
|
247
|
+
prevHexValue = hexValue;
|
|
248
|
+
hexInputValue = hexValue;
|
|
249
|
+
isHexEditing = true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function handleHexInput(event: Event) {
|
|
253
|
+
hexInputValue = (event.target as HTMLInputElement).value;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function handleHexBlur(event: FocusEvent) {
|
|
257
|
+
isHexEditing = false;
|
|
258
|
+
const value = hexInputValue.trim();
|
|
259
|
+
|
|
260
|
+
if (!value || !isValidColor(value)) {
|
|
261
|
+
hexInputValue = prevHexValue;
|
|
262
|
+
isHexEditing = true;
|
|
263
|
+
|
|
264
|
+
import('svelte').then(({ tick }) => {
|
|
265
|
+
tick().then(() => {
|
|
266
|
+
isHexEditing = false;
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const normalizedValue = value.startsWith('#') ? value : `#${value}`;
|
|
272
|
+
|
|
273
|
+
hexValue = normalizedValue.toUpperCase();
|
|
274
|
+
|
|
275
|
+
setColor(hexValue);
|
|
276
|
+
prevHexValue = hexValue;
|
|
277
|
+
|
|
278
|
+
const hsb = hexToHsb(hexValue);
|
|
279
|
+
|
|
280
|
+
if (Array.isArray(hsb)) {
|
|
281
|
+
const [h, s, b] = hsb;
|
|
282
|
+
hue = h;
|
|
283
|
+
saturation = s;
|
|
284
|
+
brightness = b;
|
|
285
|
+
const rgb = hsbToRgb(hue, saturation, brightness);
|
|
286
|
+
if (Array.isArray(rgb)) {
|
|
287
|
+
[rgbRed, rgbGreen, rgbBlue] = rgb;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
updateColorPickerPosition();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function handleHexKeydown(event: KeyboardEvent) {
|
|
294
|
+
if (event.key === 'Enter') {
|
|
295
|
+
(event.target as HTMLInputElement).blur();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
$effect(() => {
|
|
300
|
+
if (!isHexEditing) {
|
|
301
|
+
hexInputValue = hexValue;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Mouse event handlers with improved drag detection
|
|
306
|
+
function handleMouseDown(event: MouseEvent, target: 'well' | 'hue' | 'alpha') {
|
|
307
|
+
// Record the starting position for drag detection
|
|
308
|
+
dragStartPosition = { x: event.clientX, y: event.clientY };
|
|
309
|
+
|
|
310
|
+
// Always handle the initial interaction
|
|
311
|
+
if (target === 'well') {
|
|
312
|
+
handleColorWellInteraction(event);
|
|
313
|
+
} else if (target === 'hue') {
|
|
314
|
+
handleHueBarInteraction(event);
|
|
315
|
+
} else if (target === 'alpha') {
|
|
316
|
+
handleAlphaBarInteraction(event);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function handleMouseMove(event: MouseEvent) {
|
|
321
|
+
if (!dragStartPosition) return;
|
|
322
|
+
|
|
323
|
+
// Calculate distance moved
|
|
324
|
+
const deltaX = Math.abs(event.clientX - dragStartPosition.x);
|
|
325
|
+
const deltaY = Math.abs(event.clientY - dragStartPosition.y);
|
|
326
|
+
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
327
|
+
|
|
328
|
+
// If we haven't started dragging yet and we've moved beyond threshold
|
|
329
|
+
if (!isDragging && distance > DRAG_THRESHOLD) {
|
|
330
|
+
isDragging = true;
|
|
331
|
+
// Determine drag target based on which element was initially clicked
|
|
332
|
+
if (dragTarget === null) {
|
|
333
|
+
// This shouldn't happen, but fallback to well
|
|
334
|
+
dragTarget = 'well';
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// If we're dragging, handle the interaction
|
|
339
|
+
if (isDragging && dragTarget) {
|
|
340
|
+
if (dragTarget === 'well') {
|
|
341
|
+
handleColorWellInteraction(event);
|
|
342
|
+
} else if (dragTarget === 'hue') {
|
|
343
|
+
handleHueBarInteraction(event);
|
|
344
|
+
} else if (dragTarget === 'alpha') {
|
|
345
|
+
handleAlphaBarInteraction(event);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function handleMouseUp(event: MouseEvent) {
|
|
351
|
+
// If we were dragging, this is the end of the drag
|
|
352
|
+
if (isDragging) {
|
|
353
|
+
isDragging = false;
|
|
354
|
+
dragTarget = null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Reset drag detection state
|
|
358
|
+
dragStartPosition = null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Calculate the position of the handle on the color bar
|
|
363
|
+
* @param value
|
|
364
|
+
* @param min
|
|
365
|
+
* @param max
|
|
366
|
+
* @param range
|
|
367
|
+
*/
|
|
368
|
+
function percentHandlePosition(value: number, min: number, max: number, range: number): number {
|
|
369
|
+
return min + (value * (max - min)) / range;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Watch for prop changes
|
|
373
|
+
$effect(() => {
|
|
374
|
+
if (color !== hexValue) {
|
|
375
|
+
if (isValidColor(color)) {
|
|
376
|
+
hexValue = color;
|
|
377
|
+
const [h, s, b] = hexToHsb(color);
|
|
378
|
+
hue = h;
|
|
379
|
+
saturation = s;
|
|
380
|
+
brightness = b;
|
|
381
|
+
[rgbRed, rgbGreen, rgbBlue] = hsbToRgb(hue, saturation, brightness);
|
|
382
|
+
updateColorPickerPosition();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
let currentColor = $derived(
|
|
388
|
+
`rgba(${hsbToRgb(hue, saturation, brightness).join(', ')}, ${alpha / 100})`
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
onMount(() => {
|
|
392
|
+
// Initialize from color prop
|
|
393
|
+
if (isValidColor(color)) {
|
|
394
|
+
hexValue = color;
|
|
395
|
+
const [h, s, b] = hexToHsb(color);
|
|
396
|
+
hue = h;
|
|
397
|
+
saturation = s;
|
|
398
|
+
brightness = b;
|
|
399
|
+
[rgbRed, rgbGreen, rgbBlue] = hsbToRgb(hue, saturation, brightness);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
updateColor();
|
|
403
|
+
updateColorPickerPosition();
|
|
404
|
+
|
|
405
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
406
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
407
|
+
|
|
408
|
+
return () => {
|
|
409
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
410
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
411
|
+
};
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// EyeDropper API handler
|
|
415
|
+
async function handleEyedropper() {
|
|
416
|
+
if ('EyeDropper' in window) {
|
|
417
|
+
try {
|
|
418
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
419
|
+
const eyeDropper = new (window as any).EyeDropper();
|
|
420
|
+
const result = await eyeDropper.open();
|
|
421
|
+
if (result && result.sRGBHex) {
|
|
422
|
+
hexValue = result.sRGBHex.toUpperCase();
|
|
423
|
+
setColor(hexValue);
|
|
424
|
+
const [h, s, b] = hexToHsb(hexValue);
|
|
425
|
+
hue = h;
|
|
426
|
+
saturation = s;
|
|
427
|
+
brightness = b;
|
|
428
|
+
[rgbRed, rgbGreen, rgbBlue] = hsbToRgb(hue, saturation, brightness);
|
|
429
|
+
updateColorPickerPosition();
|
|
430
|
+
}
|
|
431
|
+
} catch (e) {
|
|
432
|
+
// User cancelled or error
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
alert('EyeDropper API is not supported in this browser.');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
$effect(() => {
|
|
440
|
+
updateColorPickerPosition();
|
|
441
|
+
});
|
|
442
|
+
</script>
|
|
443
|
+
|
|
444
|
+
<div class="color-picker">
|
|
445
|
+
<div class="color-picker__container">
|
|
446
|
+
<!-- Color Well -->
|
|
447
|
+
<div
|
|
448
|
+
class="color-well"
|
|
449
|
+
tabindex="0"
|
|
450
|
+
bind:this={colorWell}
|
|
451
|
+
onmousedown={(e) => {
|
|
452
|
+
dragTarget = 'well';
|
|
453
|
+
handleMouseDown(e, 'well');
|
|
454
|
+
}}
|
|
455
|
+
role="button"
|
|
456
|
+
aria-label="Color selection well"
|
|
457
|
+
style={`background: linear-gradient(rgba(0, 0, 0, 0), black), linear-gradient(to right, white, rgba(255, 255, 255, 0)) rgb(${hsbToRgb(hue, 100, 100).join(',')});`}
|
|
458
|
+
>
|
|
459
|
+
<div class="color-well__picker" bind:this={colorPicker}></div>
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
<!-- Color Bars Section -->
|
|
463
|
+
<div class="color-bars">
|
|
464
|
+
<button
|
|
465
|
+
class="eyedropper-btn"
|
|
466
|
+
type="button"
|
|
467
|
+
onpointerdown={() => {
|
|
468
|
+
handleEyedropper();
|
|
469
|
+
}}
|
|
470
|
+
aria-label="Pick color from screen"
|
|
471
|
+
>
|
|
472
|
+
<div class="eyedropper-btn__icon">
|
|
473
|
+
<svg
|
|
474
|
+
data-wf-icon="EyedropperMediumIcon"
|
|
475
|
+
width="16"
|
|
476
|
+
height="16"
|
|
477
|
+
viewBox="0 0 16 16"
|
|
478
|
+
fill="none"
|
|
479
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
480
|
+
><path
|
|
481
|
+
d="M6.85355 4.14664C6.65829 4.3419 6.65829 4.65848 6.85355 4.85374L11.1464 9.14664C11.3417 9.3419 11.6583 9.3419 11.8536 9.14664L12.1464 8.85374C12.3417 8.65848 12.3417 8.3419 12.1464 8.14664L11 7.00019L12.75 5.25019C13.4404 4.55983 13.4404 3.44055 12.75 2.75019C12.0596 2.05983 10.9404 2.05983 10.25 2.75019L8.5 4.50019L7.85355 3.85374C7.65829 3.65848 7.34171 3.65848 7.14645 3.85374L6.85355 4.14664Z"
|
|
482
|
+
fill="currentColor"
|
|
483
|
+
></path><path
|
|
484
|
+
d="M3 12.0002L2.5 12.5002L3.5 13.5002L4 13.0002H5L9 9.00019L7 7.00019L3 11.0002V12.0002Z"
|
|
485
|
+
fill="currentColor"
|
|
486
|
+
></path></svg
|
|
487
|
+
>
|
|
488
|
+
</div>
|
|
489
|
+
</button>
|
|
490
|
+
|
|
491
|
+
<div class="color-bars__container">
|
|
492
|
+
<!-- Hue Bar -->
|
|
493
|
+
<div class="color-bar">
|
|
494
|
+
<div
|
|
495
|
+
class="color-bar__hue"
|
|
496
|
+
bind:this={hueBar}
|
|
497
|
+
onmousedown={(e) => {
|
|
498
|
+
dragTarget = 'hue';
|
|
499
|
+
handleMouseDown(e, 'hue');
|
|
500
|
+
}}
|
|
501
|
+
role="slider"
|
|
502
|
+
aria-label="Hue slider"
|
|
503
|
+
aria-valuenow={hue}
|
|
504
|
+
aria-valuemin="0"
|
|
505
|
+
aria-valuemax="360"
|
|
506
|
+
tabindex="0"
|
|
507
|
+
style="pointer-events: auto; position: relative;"
|
|
508
|
+
>
|
|
509
|
+
<div
|
|
510
|
+
class="color-bar__hue-handle color-bar__handle"
|
|
511
|
+
style="left: {percentHandlePosition(hue, 2, 98, 360)}%;
|
|
512
|
+
"
|
|
513
|
+
></div>
|
|
514
|
+
</div>
|
|
515
|
+
</div>
|
|
516
|
+
|
|
517
|
+
<!-- Alpha Bar -->
|
|
518
|
+
<div class="color-bar">
|
|
519
|
+
<div
|
|
520
|
+
class="color-bar__alpha"
|
|
521
|
+
bind:this={alphaBar}
|
|
522
|
+
onmousedown={(e) => {
|
|
523
|
+
dragTarget = 'alpha';
|
|
524
|
+
handleMouseDown(e, 'alpha');
|
|
525
|
+
}}
|
|
526
|
+
role="slider"
|
|
527
|
+
aria-label="Alpha slider"
|
|
528
|
+
aria-valuenow={alpha}
|
|
529
|
+
aria-valuemin="0"
|
|
530
|
+
aria-valuemax="100"
|
|
531
|
+
tabindex="0"
|
|
532
|
+
style="pointer-events: auto; position: relative;"
|
|
533
|
+
>
|
|
534
|
+
<div class="color-bar__alpha-bg" style="pointer-events: none;"></div>
|
|
535
|
+
<div
|
|
536
|
+
class="color-bar__alpha-gradient"
|
|
537
|
+
style="background: linear-gradient(90deg, transparent 0%, {`rgba(${hsbToRgb(hue, saturation, brightness).join(',')},1)`} 100%); pointer-events: none;"
|
|
538
|
+
></div>
|
|
539
|
+
<div
|
|
540
|
+
class="color-bar__alpha-handle color-bar__handle"
|
|
541
|
+
style="left: {percentHandlePosition(alpha, 2, 98, 100)}%;
|
|
542
|
+
|
|
543
|
+
"
|
|
544
|
+
></div>
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
<div class="divider"></div>
|
|
551
|
+
|
|
552
|
+
<!-- Controls Section -->
|
|
553
|
+
<div class="controls">
|
|
554
|
+
<!-- Hex Input -->
|
|
555
|
+
<div class="control-group">
|
|
556
|
+
<div class="input-wrapper">
|
|
557
|
+
<input
|
|
558
|
+
id="hex-input"
|
|
559
|
+
type="text"
|
|
560
|
+
class="input input--hex"
|
|
561
|
+
value={hexInputValue}
|
|
562
|
+
oninput={handleHexInput}
|
|
563
|
+
onfocus={handleHexFocus}
|
|
564
|
+
onblur={handleHexBlur}
|
|
565
|
+
onkeydown={handleHexKeydown}
|
|
566
|
+
placeholder="#000000"
|
|
567
|
+
/>
|
|
568
|
+
</div>
|
|
569
|
+
<label class="label" for="hex-input">HEX</label>
|
|
570
|
+
</div>
|
|
571
|
+
|
|
572
|
+
<!-- HSB/RGB Inputs -->
|
|
573
|
+
<div class="control-group control-group--main">
|
|
574
|
+
<div class="input-row">
|
|
575
|
+
<div class="input-wrapper">
|
|
576
|
+
<input
|
|
577
|
+
class="input input--number"
|
|
578
|
+
value={mode === 'HSB' ? hue : rgbRed}
|
|
579
|
+
min="0"
|
|
580
|
+
max={mode === 'HSB' ? 360 : 255}
|
|
581
|
+
role="spinbutton"
|
|
582
|
+
aria-label={mode === 'HSB' ? 'Hue' : 'Red'}
|
|
583
|
+
oninput={(e) => {
|
|
584
|
+
const target = e.target as HTMLInputElement;
|
|
585
|
+
if (!target) return;
|
|
586
|
+
if (mode === 'HSB') {
|
|
587
|
+
hue = +target.value;
|
|
588
|
+
} else {
|
|
589
|
+
rgbRed = +target.value;
|
|
590
|
+
}
|
|
591
|
+
updateColor();
|
|
592
|
+
}}
|
|
593
|
+
/>
|
|
594
|
+
</div>
|
|
595
|
+
<div class="input-wrapper">
|
|
596
|
+
<input
|
|
597
|
+
class="input input--number"
|
|
598
|
+
value={mode === 'HSB' ? saturation : rgbGreen}
|
|
599
|
+
min="0"
|
|
600
|
+
max={mode === 'HSB' ? 100 : 255}
|
|
601
|
+
role="spinbutton"
|
|
602
|
+
aria-label={mode === 'HSB' ? 'Saturation' : 'Green'}
|
|
603
|
+
oninput={(e) => {
|
|
604
|
+
const target = e.target as HTMLInputElement;
|
|
605
|
+
if (!target) return;
|
|
606
|
+
if (mode === 'HSB') {
|
|
607
|
+
saturation = +target.value;
|
|
608
|
+
} else {
|
|
609
|
+
rgbGreen = +target.value;
|
|
610
|
+
}
|
|
611
|
+
updateColor();
|
|
612
|
+
}}
|
|
613
|
+
/>
|
|
614
|
+
</div>
|
|
615
|
+
<div class="input-wrapper">
|
|
616
|
+
<input
|
|
617
|
+
class="input input--number"
|
|
618
|
+
value={mode === 'HSB' ? brightness : rgbBlue}
|
|
619
|
+
min="0"
|
|
620
|
+
max={mode === 'HSB' ? 100 : 255}
|
|
621
|
+
role="spinbutton"
|
|
622
|
+
aria-label={mode === 'HSB' ? 'Brightness' : 'Blue'}
|
|
623
|
+
oninput={(e) => {
|
|
624
|
+
const target = e.target as HTMLInputElement;
|
|
625
|
+
if (!target) return;
|
|
626
|
+
if (mode === 'HSB') {
|
|
627
|
+
brightness = +target.value;
|
|
628
|
+
} else {
|
|
629
|
+
rgbBlue = +target.value;
|
|
630
|
+
}
|
|
631
|
+
updateColor();
|
|
632
|
+
}}
|
|
633
|
+
/>
|
|
634
|
+
</div>
|
|
635
|
+
</div>
|
|
636
|
+
|
|
637
|
+
<div class="label-row--wrapper">
|
|
638
|
+
<div
|
|
639
|
+
class="label-row"
|
|
640
|
+
onpointerdown={toggleMode}
|
|
641
|
+
role="button"
|
|
642
|
+
tabindex="0"
|
|
643
|
+
onkeydown={() => {}}
|
|
644
|
+
aria-label="Mode toggle"
|
|
645
|
+
>
|
|
646
|
+
<span class="label label--clickable">
|
|
647
|
+
{mode === 'HSB' ? 'H' : 'R'}
|
|
648
|
+
</span>
|
|
649
|
+
<span class="label label--clickable">
|
|
650
|
+
{mode === 'HSB' ? 'S' : 'G'}
|
|
651
|
+
</span>
|
|
652
|
+
<span class="label label--clickable">
|
|
653
|
+
{mode === 'HSB' ? 'B' : 'B'}
|
|
654
|
+
</span>
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
|
|
659
|
+
<!-- Alpha Input -->
|
|
660
|
+
<div class="control-group">
|
|
661
|
+
<div class="input-wrapper">
|
|
662
|
+
<input
|
|
663
|
+
class="input input--number"
|
|
664
|
+
bind:value={alpha}
|
|
665
|
+
min="0"
|
|
666
|
+
max="100"
|
|
667
|
+
role="spinbutton"
|
|
668
|
+
aria-label="Alpha"
|
|
669
|
+
/>
|
|
670
|
+
</div>
|
|
671
|
+
<span class="label">A</span>
|
|
672
|
+
</div>
|
|
673
|
+
</div>
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
|
|
677
|
+
<style>
|
|
678
|
+
/* Main Container */
|
|
679
|
+
.color-picker {
|
|
680
|
+
width: 241px;
|
|
681
|
+
height: auto;
|
|
682
|
+
box-shadow: 0px 3px 6px -2px rgba(0, 0, 0, 0.36);
|
|
683
|
+
display: inline-flex;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
.color-picker__container {
|
|
687
|
+
width: 241px;
|
|
688
|
+
padding: 8px;
|
|
689
|
+
background: var(--background4);
|
|
690
|
+
border-radius: 4px;
|
|
691
|
+
display: flex;
|
|
692
|
+
flex-direction: column;
|
|
693
|
+
gap: 8px;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/* Color Well */
|
|
697
|
+
.color-well {
|
|
698
|
+
width: 225px;
|
|
699
|
+
height: 150px;
|
|
700
|
+
position: relative;
|
|
701
|
+
border-radius: 2px;
|
|
702
|
+
outline: 1px solid var(--border1, rgba(255, 255, 255, 0.1));
|
|
703
|
+
outline-offset: -1px;
|
|
704
|
+
cursor: crosshair;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.color-well__picker {
|
|
708
|
+
position: absolute;
|
|
709
|
+
width: 12px;
|
|
710
|
+
height: 12px;
|
|
711
|
+
border: 2px solid white;
|
|
712
|
+
border-radius: 50%;
|
|
713
|
+
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
|
|
714
|
+
pointer-events: none;
|
|
715
|
+
z-index: 10;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/* Color Bars */
|
|
719
|
+
.color-bars {
|
|
720
|
+
width: 225px;
|
|
721
|
+
display: flex;
|
|
722
|
+
align-items: center;
|
|
723
|
+
gap: 6px;
|
|
724
|
+
}
|
|
725
|
+
.color-bar__handle {
|
|
726
|
+
position: absolute;
|
|
727
|
+
top: 1px !important;
|
|
728
|
+
transform: translateX(-50%);
|
|
729
|
+
pointer-events: none;
|
|
730
|
+
height: 11px;
|
|
731
|
+
width: 6px;
|
|
732
|
+
border-radius: 1px;
|
|
733
|
+
box-shadow:
|
|
734
|
+
0 0 0 2px var(--text1),
|
|
735
|
+
0 0 0 3px rgba(0, 0, 0, 0.3),
|
|
736
|
+
inset 0 0 0 1px rgba(0, 0, 0, 0.3);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.eyedropper-btn {
|
|
740
|
+
width: 32px;
|
|
741
|
+
height: 32px;
|
|
742
|
+
padding: 4px;
|
|
743
|
+
background:
|
|
744
|
+
linear-gradient(180deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.1) 100%),
|
|
745
|
+
rgba(255, 255, 255, 0.08);
|
|
746
|
+
box-shadow: 0px 0.5px 1px black;
|
|
747
|
+
border-radius: 4px;
|
|
748
|
+
border: none;
|
|
749
|
+
cursor: pointer;
|
|
750
|
+
display: flex;
|
|
751
|
+
align-items: center;
|
|
752
|
+
justify-content: center;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.eyedropper-btn__icon {
|
|
756
|
+
width: 16px;
|
|
757
|
+
height: 16px;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
.eyedropper-btn__icon svg {
|
|
761
|
+
color: var(--text1, #ebebeb);
|
|
762
|
+
}
|
|
763
|
+
.color-bars__container {
|
|
764
|
+
flex: 1;
|
|
765
|
+
display: flex;
|
|
766
|
+
flex-direction: column;
|
|
767
|
+
gap: 4px;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
.color-bar {
|
|
771
|
+
padding: 1px 0;
|
|
772
|
+
display: flex;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
.color-bar__hue {
|
|
776
|
+
width: 186px;
|
|
777
|
+
height: 12px;
|
|
778
|
+
background: linear-gradient(
|
|
779
|
+
90deg,
|
|
780
|
+
#ff0000 0%,
|
|
781
|
+
#ff8a00 8%,
|
|
782
|
+
#fff500 18%,
|
|
783
|
+
#00ff47 39%,
|
|
784
|
+
#00f0ff 51%,
|
|
785
|
+
#0019fb 64%,
|
|
786
|
+
#fa00ff 84%,
|
|
787
|
+
#ff0000 100%
|
|
788
|
+
);
|
|
789
|
+
border-radius: 2px;
|
|
790
|
+
outline: 1px solid var(--border1, rgba(255, 255, 255, 0.1));
|
|
791
|
+
cursor: pointer;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.color-bar__alpha {
|
|
795
|
+
width: 186px;
|
|
796
|
+
border-radius: 2px;
|
|
797
|
+
height: 12px;
|
|
798
|
+
position: relative;
|
|
799
|
+
cursor: pointer;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.color-bar__alpha-bg {
|
|
803
|
+
position: absolute;
|
|
804
|
+
width: 186px;
|
|
805
|
+
inset: 0;
|
|
806
|
+
border-radius: 2px;
|
|
807
|
+
overflow: hidden;
|
|
808
|
+
background-image:
|
|
809
|
+
linear-gradient(45deg, #ccc 25%, transparent 25%),
|
|
810
|
+
linear-gradient(-45deg, #ccc 25%, transparent 25%),
|
|
811
|
+
linear-gradient(45deg, transparent 75%, #ccc 75%),
|
|
812
|
+
linear-gradient(-45deg, transparent 75%, #ccc 75%);
|
|
813
|
+
background-size: 8px 8px;
|
|
814
|
+
background-position:
|
|
815
|
+
0 0,
|
|
816
|
+
0 4px,
|
|
817
|
+
4px -4px,
|
|
818
|
+
-4px 0px;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.color-bar__alpha-gradient {
|
|
822
|
+
position: absolute;
|
|
823
|
+
inset: 0;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
.color-bar__hue-handle {
|
|
827
|
+
position: absolute;
|
|
828
|
+
top: -1px;
|
|
829
|
+
transform: translateX(-50%);
|
|
830
|
+
pointer-events: none;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
.color-bar__alpha-handle {
|
|
834
|
+
position: absolute;
|
|
835
|
+
top: -1px;
|
|
836
|
+
transform: translateX(-50%);
|
|
837
|
+
pointer-events: none;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/* Divider */
|
|
841
|
+
.divider {
|
|
842
|
+
width: 225px;
|
|
843
|
+
height: 1px;
|
|
844
|
+
background: var(--border1);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/* Controls */
|
|
848
|
+
.controls {
|
|
849
|
+
display: grid;
|
|
850
|
+
grid-template-columns: 70px 1fr 30px;
|
|
851
|
+
gap: 7px;
|
|
852
|
+
width: 100%;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
.control-group {
|
|
856
|
+
display: grid;
|
|
857
|
+
grid-template-rows: 26px 20px;
|
|
858
|
+
gap: 8px;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
.control-group--main {
|
|
862
|
+
flex: 1;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.input-row {
|
|
866
|
+
display: flex;
|
|
867
|
+
gap: 4px;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
.label-row {
|
|
871
|
+
display: flex;
|
|
872
|
+
background: #4e4e4e;
|
|
873
|
+
border-radius: 4px;
|
|
874
|
+
cursor: pointer;
|
|
875
|
+
width: 100%;
|
|
876
|
+
box-shadow:
|
|
877
|
+
0px 0.5px 1px 0px #000,
|
|
878
|
+
0px 0.5px 0.5px 0px rgba(255, 255, 255, 0.12) inset;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.label-row--wrapper {
|
|
882
|
+
display: flex;
|
|
883
|
+
gap: 4px;
|
|
884
|
+
width: 100%;
|
|
885
|
+
flex: 1;
|
|
886
|
+
align-self: stretch;
|
|
887
|
+
justify-content: space-between;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
.input-wrapper {
|
|
891
|
+
flex: 1;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
.input {
|
|
895
|
+
width: 100%;
|
|
896
|
+
padding: 4px;
|
|
897
|
+
background: var(--backgroundInput);
|
|
898
|
+
border: 1px solid var(--border2);
|
|
899
|
+
border-radius: 4px;
|
|
900
|
+
color: var(--text1, #ebebeb);
|
|
901
|
+
font-size: 11px;
|
|
902
|
+
font-family: Inter, sans-serif;
|
|
903
|
+
font-weight: 400;
|
|
904
|
+
line-height: 16px;
|
|
905
|
+
outline: none;
|
|
906
|
+
box-shadow: 0px 1px 1px -1px rgba(0, 0, 0, 0.13) inset;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
.input--hex {
|
|
910
|
+
width: 70px;
|
|
911
|
+
text-align: left;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.input--number {
|
|
915
|
+
width: 30px;
|
|
916
|
+
text-align: center;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
.label {
|
|
920
|
+
color: var(--text1, #ebebeb);
|
|
921
|
+
font-size: 11px;
|
|
922
|
+
font-family: Inter, sans-serif;
|
|
923
|
+
font-weight: 400;
|
|
924
|
+
line-height: 16px;
|
|
925
|
+
text-align: center;
|
|
926
|
+
display: flex;
|
|
927
|
+
align-items: center;
|
|
928
|
+
justify-content: center;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
.label--clickable {
|
|
932
|
+
flex: 1;
|
|
933
|
+
padding: 4px;
|
|
934
|
+
cursor: pointer;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.control-group--main .input--number {
|
|
938
|
+
width: 100%;
|
|
939
|
+
}
|
|
940
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ColorPicker } from './ColorPicker.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ColorPicker } from './ColorPicker.svelte';
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { CheckCircleIcon, CodeIcon, InfoIcon, SettingsIcon } from '../../../icons';
|
|
7
7
|
import { Button } from '../../button';
|
|
8
|
+
import { ColorPicker } from '../../color-picker';
|
|
8
9
|
import { Input } from '../../input';
|
|
9
10
|
import { Section } from '../../section';
|
|
10
11
|
import { Switch } from '../../switch';
|
|
@@ -268,6 +269,13 @@
|
|
|
268
269
|
width="130px"
|
|
269
270
|
/>
|
|
270
271
|
|
|
272
|
+
<ColorPicker
|
|
273
|
+
color="#8C4C4C"
|
|
274
|
+
onchange={(color) => {
|
|
275
|
+
console.log(color);
|
|
276
|
+
}}
|
|
277
|
+
/>
|
|
278
|
+
|
|
271
279
|
<Input
|
|
272
280
|
value="Error input"
|
|
273
281
|
invalid={true}
|
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
<DefaultIcon />
|
|
159
159
|
</span>
|
|
160
160
|
|
|
161
|
-
<div class="content">
|
|
161
|
+
<div class="notification-content">
|
|
162
162
|
{#if title}
|
|
163
163
|
<Text
|
|
164
164
|
label={title}
|
|
@@ -222,7 +222,7 @@
|
|
|
222
222
|
flex-shrink: 0;
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
.content {
|
|
225
|
+
.notification-content {
|
|
226
226
|
display: flex;
|
|
227
227
|
padding-right: var(--spacing-24, 24px);
|
|
228
228
|
flex-direction: column;
|