|
1 | | -<script> |
| 1 | +<script lang="ts"> |
| 2 | +// @ts-nocheck |
2 | 3 | import {flip} from 'svelte/animate' |
3 | 4 | import {fade,scale} from 'svelte/transition' |
4 | 5 |
|
5 | 6 | import { tooltip } from 'svooltip' |
6 | 7 |
|
7 | | - import {gradient_stops, gradient_space, active_stop_index} from '../store/gradient.ts' |
8 | | - import {picker_value} from '../store/colorpicker.ts' |
9 | | - import {updateStops, removeStop} from '../utils/stops.ts' |
10 | | - import {copyToClipboard} from '../utils/clipboard.ts' |
11 | | - import {randomNumber} from '../utils/numbers.ts' |
12 | | - import {whatsTheGamutDamnit} from '../utils/colorspace.ts' |
| 8 | + import {gradient_stops, gradient_space, active_stop_index} from '../store/gradient' |
| 9 | + import {picker_value} from '../store/colorpicker' |
| 10 | +
|
| 11 | + import {copyToClipboard} from '../utils/clipboard' |
| 12 | + import {randomNumber} from '../utils/numbers' |
| 13 | + import {whatsTheGamutDamnit} from '../utils/colorspace' |
13 | 14 |
|
14 | 15 | import RangeSlider from './RangeSlider.svelte' |
15 | 16 | import Hint from './Hint.svelte' |
16 | 17 |
|
| 18 | + type GradientStop = any |
| 19 | +
|
17 | 20 | // Drag-reorder state |
18 | 21 | let dragging = $state(false) |
19 | 22 | let dragStart = $state(null) // start index of dragged unit (stop + optional hint) |
|
27 | 30 | catch { return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}` } |
28 | 31 | } |
29 | 32 |
|
30 | | - function ensureIds(list) { |
| 33 | + function ensureIds(list: GradientStop[]): GradientStop[] { |
31 | 34 | return list.map(item => item?.id ? item : ({ ...item, id: genId(item.kind) })) |
32 | 35 | } |
33 | 36 |
|
34 | | - function colorAction(event, position) { |
35 | | - switch (event.target.value) { |
| 37 | + function colorAction(event: Event, position: number) { |
| 38 | + const target = event.target as HTMLSelectElement |
| 39 | + switch (target.value) { |
36 | 40 | case 'Remove': |
37 | 41 | if (colorStopCount() <= 1) break |
38 | | - $gradient_stops = updateStops(removeStop($gradient_stops, position)) |
| 42 | + const newStops = [...$gradient_stops] |
| 43 | + newStops.splice(position, 2) |
| 44 | + $gradient_stops = newStops |
39 | 45 | break |
40 | 46 | case 'Reset': |
41 | | - $gradient_stops[position].position1 = null |
42 | | - $gradient_stops[position].position2 = null |
43 | | - updateStops($gradient_stops) |
| 47 | + const resetStops = [...$gradient_stops] |
| 48 | + resetStops[position].position1 = undefined |
| 49 | + resetStops[position].position2 = undefined |
| 50 | + $gradient_stops = resetStops |
44 | 51 | break |
45 | 52 | case 'Duplicate': |
46 | 53 | dupeStop(position) |
47 | 54 | break |
48 | 55 | case 'Copy CSS color': |
49 | | - copyToClipboard($gradient_stops[position].color) |
| 56 | + copyToClipboard($gradient_stops[position].color || '') |
50 | 57 | break |
51 | 58 | case 'Random color': |
52 | | - $gradient_stops[position].color = `oklch(80% 0.3 ${randomNumber(0,360)})` |
| 59 | + const randomStops = [...$gradient_stops] |
| 60 | + randomStops[position].color = `oklch(80% 0.3 ${randomNumber(0,360)})` |
| 61 | + $gradient_stops = randomStops |
53 | 62 | break |
54 | 63 | } |
55 | 64 |
|
56 | 65 | // reset |
57 | | - event.target.selectedIndex = 0 |
| 66 | + target.selectedIndex = 0 |
58 | 67 | } |
59 | 68 |
|
60 | 69 | function colorStopCount() { |
61 | 70 | return ($gradient_stops || []).filter(s => s?.kind === 'stop').length |
62 | 71 | } |
63 | 72 |
|
64 | | - function dupeStop(pos) { |
| 73 | + function dupeStop(pos: number) { |
| 74 | + const newStops: any[] = [...$gradient_stops] |
65 | 75 | const clone = { |
66 | 76 | id: genId('stop'), |
67 | 77 | kind: 'stop', |
68 | | - color: $gradient_stops[pos].color, |
69 | | - position1: $gradient_stops[pos].position1, |
70 | | - position2: $gradient_stops[pos].position2, |
| 78 | + color: newStops[pos].color, |
| 79 | + position1: newStops[pos].position1, |
| 80 | + position2: newStops[pos].position2, |
| 81 | + auto: String(Number(newStops[pos].auto || 0)) |
71 | 82 | } |
72 | 83 |
|
73 | | - $gradient_stops.splice(pos, 0, {id: genId('hint'), kind: 'hint', percentage: null}) |
74 | | - $gradient_stops.splice(pos, 0, clone) |
| 84 | + newStops.splice(pos, 0, {id: genId('hint'), kind: 'hint', percentage: undefined, auto: ''}) |
| 85 | + newStops.splice(pos, 0, clone) |
75 | 86 |
|
76 | | - $gradient_stops = updateStops(ensureIds($gradient_stops)) |
| 87 | + $gradient_stops = ensureIds(newStops) |
77 | 88 | } |
78 | 89 |
|
79 | | - function removePositionByIndex(index, pos) { |
80 | | - $gradient_stops[index]['position'+pos] = null |
| 90 | + function removePositionByIndex(index: number, pos: number) { |
| 91 | + const newStops: GradientStop[] = [...$gradient_stops] |
| 92 | + ;(newStops[index] as any)['position'+pos] = undefined |
81 | 93 |
|
82 | 94 | // spec fix, cant have 2nd position without the 1st one |
83 | | - if (pos === 1 && $gradient_stops[index].position2 !== null) |
84 | | - $gradient_stops[index]['position2'] = null |
| 95 | + if (pos === 1 && (newStops[index] as any).position2 !== undefined) { |
| 96 | + ;(newStops[index] as any)['position2'] = undefined |
| 97 | + } |
| 98 | + |
| 99 | + $gradient_stops = newStops |
85 | 100 | } |
86 | 101 |
|
87 | | - function pickColor(stop, e) { |
88 | | - const picker = document.getElementById('color-picker') |
| 102 | + function pickColor(stop: GradientStop, e: Event) { |
| 103 | + const picker = document.getElementById('color-picker') as any |
89 | 104 |
|
90 | 105 | // Seed the picker with the current stop color to avoid stale value flashes |
91 | 106 | $picker_value = stop.color |
|
108 | 123 | }) |
109 | 124 | } |
110 | 125 |
|
111 | | - function fieldsetInteractingStart(stop) { |
| 126 | + function fieldsetInteractingStart(stop: GradientStop) { |
112 | 127 | $active_stop_index = $gradient_stops.indexOf(stop) |
113 | 128 | } |
114 | 129 |
|
115 | | - function fieldsetInteractingEnd(stop) { |
| 130 | + function fieldsetInteractingEnd() { |
116 | 131 | $active_stop_index = null |
117 | 132 | } |
118 | 133 |
|
119 | | - function fixIfEmptied(stop) { |
120 | | - if (stop.percentage === null) { |
121 | | - stop.percentage = stop.auto |
| 134 | + function fixIfEmptied(stop: GradientStop) { |
| 135 | + if (stop.percentage === null || stop.percentage === undefined) { |
| 136 | + stop.percentage = String(stop.auto || '') |
122 | 137 | $gradient_stops = [...$gradient_stops] |
123 | 138 | } |
124 | 139 | } |
125 | 140 |
|
126 | | - function unitBoundsForIndex(i) { |
| 141 | + function unitBoundsForIndex(i: number) { |
127 | 142 | // Drag units are a stop and its following hint (if present) |
128 | 143 | const isStop = $gradient_stops[i]?.kind === 'stop' |
129 | 144 | if (!isStop) return null |
130 | 145 | const hasFollowingHint = $gradient_stops[i+1]?.kind === 'hint' |
131 | 146 | return { start: i, length: hasFollowingHint ? 2 : 1 } |
132 | 147 | } |
133 | 148 |
|
134 | | - function beginDrag(e, i) { |
| 149 | + function beginDrag(e: DragEvent, i: number) { |
135 | 150 | // prevent dragging from inputs/buttons |
136 | 151 | if (e.target.closest('input, select, button')) return e.preventDefault() |
137 | 152 | const unit = unitBoundsForIndex(i) |
|
254 | 269 | ondragend={endDrag} |
255 | 270 | class:drop-before={dragging && dropStart === i && dropPos === 'before'} |
256 | 271 | class:drop-after={dragging && dropStart === i && dropPos === 'after'} |
257 | | - style="accent-color: {stop.color}; --brand: {stop.color}" |
| 272 | + style="accent-color: {stop.color}; --brand: {stop.color}; --gs-glow-color: {stop.color}" |
258 | 273 | class="stop control-set" |
259 | 274 | onmouseenter={() => fieldsetInteractingStart(stop)} |
260 | 275 | onfocusin={() => fieldsetInteractingStart(stop)} |
|
267 | 282 | <Hint title="Color stop" copy="The color and position of that color on the gradient line.<br><br>The three dot menu has actions you can take on the color, like duplicate.<br><br>A color is not required in CSS to only be at a single position on the line, it may span the line by specifying a 2nd position." /> |
268 | 283 | {/if} |
269 | 284 | <div class="chip color-stop" use:tooltip={{content: 'Gamut: '+ whatsTheGamutDamnit(stop.color), placement: 'top-start',}}> |
270 | | - <button class="round" style="background-color: {stop.color}" onclick={e => pickColor(stop,e)}></button> |
| 285 | + <button class="round" style="background-color: {stop.color}" onclick={e => pickColor(stop,e)} aria-label="Pick color"></button> |
271 | 286 | <input type="text" class="color-string" style="caret-color: {stop.color}" bind:value={stop.color}/> |
272 | 287 | </div> |
273 | 288 | <div class="positions-pair"> |
|
311 | 326 | <option disabled={colorStopCount() <= 1}>Remove</option> |
312 | 327 | </select> |
313 | 328 | </button> |
314 | | - <div class="drag-handle" use:tooltip={{content: 'Drag to reorder'}} draggable="true" ondragstart={(e) => beginDrag(e, i)} aria-label="Drag to reorder"></div> |
| 329 | + <div class="drag-handle" use:tooltip={{content: 'Drag to reorder'}} draggable="true" ondragstart={(e) => beginDrag(e, i)} role="button" aria-label="Drag to reorder" tabindex="0"></div> |
315 | 330 | </fieldset> |
316 | 331 | {/if} |
317 | 332 | {#if stop.kind === 'hint'} |
|
343 | 358 | </div> |
344 | 359 | {/each} |
345 | 360 | <!-- End drop zone to allow dropping after the last stop --> |
346 | | - <div class="end-dropzone" ondragover={(e)=> onDragOverEnd(e)} ondrop={(e)=> dropAtEnd(e)}></div> |
| 361 | + <div class="end-dropzone" ondragover={(e)=> onDragOverEnd(e)} ondrop={(e)=> dropAtEnd(e)} role="region" aria-label="Drop zone"></div> |
347 | 362 | </section> |
348 | 363 |
|
349 | 364 | <style> |
|
367 | 382 | box-shadow: var(--shadow-2); |
368 | 383 | gap: var(--size-3); |
369 | 384 | cursor: auto; |
| 385 | + /* Each stop carries its own glow color for the proximity sheen */ |
| 386 | + --gs-glow-color: var(--brand); |
370 | 387 | } |
371 | 388 |
|
372 | 389 | @media (prefers-color-scheme: light) { |
|
0 commit comments