@aquera/nile-elements 1.6.9 → 1.7.0

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.
Files changed (55) hide show
  1. package/README.md +3 -0
  2. package/dist/index.cjs.js +1 -1
  3. package/dist/index.esm.js +1 -1
  4. package/dist/index.js +291 -123
  5. package/dist/nile-color-picker/index.cjs.js +2 -0
  6. package/dist/nile-color-picker/index.cjs.js.map +1 -0
  7. package/dist/nile-color-picker/index.esm.js +1 -0
  8. package/dist/nile-color-picker/nile-color-picker.cjs.js +2 -0
  9. package/dist/nile-color-picker/nile-color-picker.cjs.js.map +1 -0
  10. package/dist/nile-color-picker/nile-color-picker.css.cjs.js +2 -0
  11. package/dist/nile-color-picker/nile-color-picker.css.cjs.js.map +1 -0
  12. package/dist/nile-color-picker/nile-color-picker.css.esm.js +1 -0
  13. package/dist/nile-color-picker/nile-color-picker.esm.js +111 -0
  14. package/dist/nile-color-swatch/index.cjs.js +2 -0
  15. package/dist/nile-color-swatch/index.cjs.js.map +1 -0
  16. package/dist/nile-color-swatch/index.esm.js +1 -0
  17. package/dist/nile-color-swatch/nile-color-swatch.cjs.js +2 -0
  18. package/dist/nile-color-swatch/nile-color-swatch.cjs.js.map +1 -0
  19. package/dist/nile-color-swatch/nile-color-swatch.css.cjs.js +2 -0
  20. package/dist/nile-color-swatch/nile-color-swatch.css.cjs.js.map +1 -0
  21. package/dist/nile-color-swatch/nile-color-swatch.css.esm.js +44 -0
  22. package/dist/nile-color-swatch/nile-color-swatch.esm.js +16 -0
  23. package/dist/src/index.d.ts +3 -0
  24. package/dist/src/index.js +2 -0
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/nile-color-picker/index.d.ts +3 -0
  27. package/dist/src/nile-color-picker/index.js +3 -0
  28. package/dist/src/nile-color-picker/index.js.map +1 -0
  29. package/dist/src/nile-color-picker/nile-color-picker.css.d.ts +1 -0
  30. package/dist/src/nile-color-picker/nile-color-picker.css.js +228 -0
  31. package/dist/src/nile-color-picker/nile-color-picker.css.js.map +1 -0
  32. package/dist/src/nile-color-picker/nile-color-picker.d.ts +92 -0
  33. package/dist/src/nile-color-picker/nile-color-picker.js +613 -0
  34. package/dist/src/nile-color-picker/nile-color-picker.js.map +1 -0
  35. package/dist/src/nile-color-swatch/index.d.ts +1 -0
  36. package/dist/src/nile-color-swatch/index.js +2 -0
  37. package/dist/src/nile-color-swatch/index.js.map +1 -0
  38. package/dist/src/nile-color-swatch/nile-color-swatch.css.d.ts +1 -0
  39. package/dist/src/nile-color-swatch/nile-color-swatch.css.js +46 -0
  40. package/dist/src/nile-color-swatch/nile-color-swatch.css.js.map +1 -0
  41. package/dist/src/nile-color-swatch/nile-color-swatch.d.ts +15 -0
  42. package/dist/src/nile-color-swatch/nile-color-swatch.js +66 -0
  43. package/dist/src/nile-color-swatch/nile-color-swatch.js.map +1 -0
  44. package/dist/src/version.js +1 -1
  45. package/dist/src/version.js.map +1 -1
  46. package/dist/tsconfig.tsbuildinfo +1 -1
  47. package/package.json +4 -2
  48. package/src/index.ts +3 -0
  49. package/src/nile-color-picker/index.ts +3 -0
  50. package/src/nile-color-picker/nile-color-picker.css.ts +225 -0
  51. package/src/nile-color-picker/nile-color-picker.ts +541 -0
  52. package/src/nile-color-swatch/index.ts +1 -0
  53. package/src/nile-color-swatch/nile-color-swatch.css.ts +46 -0
  54. package/src/nile-color-swatch/nile-color-swatch.ts +72 -0
  55. package/vscode-html-custom-data.json +126 -20
@@ -0,0 +1,541 @@
1
+ import { LitElement, html, nothing } from 'lit';
2
+ import { customElement, property, state } from 'lit/decorators.js';
3
+ import { getCssText } from './nile-color-picker.css';
4
+ import '../nile-dropdown/nile-dropdown';
5
+ import '../nile-menu/nile-menu';
6
+ import '../nile-menu-item/nile-menu-item';
7
+
8
+ export type ColorPickerType = 'swatches' | 'picker';
9
+ export type ColorPickerMode = 'inline' | 'popover';
10
+ export type SwatchTooltip = 'name' | 'value' | 'both';
11
+ export type PaletteEntry = { color: string; name: string };
12
+
13
+ export const DEFAULT_PALETTE: PaletteEntry[] = [
14
+ { color: '#131316', name: 'Black' },
15
+ { color: '#26272B', name: 'Dark Gray' },
16
+ { color: '#525252', name: 'Gray' },
17
+ { color: '#737373', name: 'Medium Gray' },
18
+ { color: '#A3A3A3', name: 'Silver' },
19
+ { color: '#D6D6D6', name: 'Light Gray' },
20
+ { color: '#F5F5F5', name: 'Off White' },
21
+ { color: '#FFFFFF', name: 'White' },
22
+ { color: '#CA8504', name: 'Yellow 3' },
23
+ { color: '#C01048', name: 'Red 3' },
24
+ { color: '#DD2590', name: 'Pink 3' },
25
+ { color: '#155EEF', name: 'Blue Dark 3' },
26
+ { color: '#444CE7', name: 'Indigo 3' },
27
+ { color: '#7A5AF8', name: 'Purple 3' },
28
+ { color: '#3E4784', name: 'Gray Blue 3' },
29
+ { color: '#087443', name: 'Green 3' },
30
+ { color: '#FDE272', name: 'Yellow 2' },
31
+ { color: '#FEA3B4', name: 'Red 2' },
32
+ { color: '#FAA7E0', name: 'Pink 2' },
33
+ { color: '#528BFF', name: 'Blue Dark 2' },
34
+ { color: '#A4BCFD', name: 'Indigo 2' },
35
+ { color: '#BDB4FE', name: 'Purple 2' },
36
+ { color: '#B3B8DB', name: 'Gray Blue 2' },
37
+ { color: '#73E2A3', name: 'Green 2' },
38
+ { color: '#FEFBE8', name: 'Yellow 1' },
39
+ { color: '#FFF1F3', name: 'Red 1' },
40
+ { color: '#FDF2FA', name: 'Pink 1' },
41
+ { color: '#EFF4FF', name: 'Blue Dark 1' },
42
+ { color: '#EEF4FF', name: 'Indigo 1' },
43
+ { color: '#F4F3FF', name: 'Purple 1' },
44
+ { color: '#F8F9FC', name: 'Gray Blue 1' },
45
+ { color: '#EDFCF2', name: 'Green 1' },
46
+ ];
47
+
48
+ /* ── color math helpers ──────────────────────────────────────────────── */
49
+ export function hexToRgb(hex: string): [number, number, number] {
50
+ const h = hex.replace('#', '');
51
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
52
+ }
53
+
54
+ export function rgbToHex(r: number, g: number, b: number): string {
55
+ return '#' + [r, g, b].map(v => v.toString(16).padStart(2, '0')).join('').toUpperCase();
56
+ }
57
+
58
+ export function rgbToHsv(r: number, g: number, b: number): [number, number, number] {
59
+ r /= 255; g /= 255; b /= 255;
60
+ const max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min;
61
+ let h = 0;
62
+ if (d !== 0) {
63
+ if (max === r) h = ((g - b) / d + 6) % 6;
64
+ else if (max === g) h = (b - r) / d + 2;
65
+ else h = (r - g) / d + 4;
66
+ h *= 60;
67
+ }
68
+ return [h, max === 0 ? 0 : d / max, max];
69
+ }
70
+
71
+ export function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
72
+ const c = v * s, x = c * (1 - Math.abs(((h / 60) % 2) - 1)), m = v - c;
73
+ let r = 0, g = 0, b = 0;
74
+ if (h < 60) { r = c; g = x; }
75
+ else if (h < 120) { r = x; g = c; }
76
+ else if (h < 180) { g = c; b = x; }
77
+ else if (h < 240) { g = x; b = c; }
78
+ else if (h < 300) { r = x; b = c; }
79
+ else { r = c; b = x; }
80
+ return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
81
+ }
82
+
83
+ @customElement('nile-color-picker')
84
+ export class NileColorPicker extends LitElement {
85
+ protected createRenderRoot() { return this; }
86
+
87
+
88
+ @property({ type: String, attribute: true, reflect: true }) type: ColorPickerType = 'swatches';
89
+
90
+ /** Currently selected hex color value. */
91
+ @property({ type: String, attribute: true, reflect: true }) value = '#000000';
92
+
93
+ /** What to show in swatch tooltips. */
94
+ @property({ type: String, reflect: true }) swatchTooltip: SwatchTooltip = 'name';
95
+
96
+ /** How many recent/custom colors to display. */
97
+ @property({ type: Number, reflect: true }) recentColorsCount = 3;
98
+
99
+ /** Show a "No Fill" button (useful for background-color use cases). */
100
+ @property({ type: Boolean, reflect: true }) noFill = false;
101
+
102
+
103
+ @property({ type: String, reflect: true })mode: ColorPickerMode = 'inline';
104
+
105
+ /** Show the SV gradient canvas (the large colour area). Applies when type="picker". */
106
+ @property({ type: Boolean, reflect: true }) showCanvas = true;
107
+
108
+ /** Show the hue slider row (rainbow bar + eyedropper). Applies when type="picker". */
109
+ @property({ type: Boolean, reflect: true }) showHue = true;
110
+
111
+ /** Show the HEX / RGB input row. Applies when type="picker". */
112
+ @property({ type: Boolean, reflect: true }) showInputs = true;
113
+
114
+ /** Show the Cancel / Okay action buttons. Applies when type="picker". */
115
+ @property({ type: Boolean, reflect: true }) showActions = true;
116
+
117
+ @property({ type: Boolean, reflect: true, attribute: true}) open = false;
118
+
119
+ /** Custom palette. Accepts a JSON array of `{ color, name }` objects or plain
120
+ * hex strings. Falls back to the built-in palette when empty. */
121
+ @property({
122
+ attribute: 'palette',
123
+ converter: {
124
+ fromAttribute: (value: string): PaletteEntry[] => {
125
+ try {
126
+ const parsed = JSON.parse(value);
127
+ if (!Array.isArray(parsed)) return [];
128
+ return parsed.map((e: any) => {
129
+ if (typeof e === 'string') return { color: e, name: e };
130
+ if (e && typeof e.color === 'string') return { color: e.color, name: e.name || e.color };
131
+ return null;
132
+ }).filter(Boolean) as PaletteEntry[];
133
+ } catch { return []; }
134
+ },
135
+ toAttribute: (v: PaletteEntry[]) => JSON.stringify(v),
136
+ },
137
+ })
138
+ palette: PaletteEntry[] = [];
139
+
140
+ /* ── internal state ──────────────────────────────────────────────── */
141
+ @state() private _view: ColorPickerType = 'swatches';
142
+ @state() private _recentColors: PaletteEntry[] = [];
143
+
144
+ // picker canvas state
145
+ @state() private _hue = 220;
146
+ @state() private _sat = 0.7;
147
+ @state() private _val = 0.93;
148
+ @state() private _colorMode: 'HEX' | 'RGB' = 'HEX';
149
+ @state() private _hexInput = '#366AEE';
150
+ @state() private _r = 54;
151
+ @state() private _g = 106;
152
+ @state() private _b = 238;
153
+
154
+ private _svCanvas: HTMLCanvasElement | null = null;
155
+ private _hueCanvas: HTMLCanvasElement | null = null;
156
+ private _draggingSV = false;
157
+ private _draggingHue = false;
158
+ private _pendingColor = '#366AEE';
159
+
160
+ /* ── lifecycle ───────────────────────────────────────────────────── */
161
+ connectedCallback() {
162
+ super.connectedCallback();
163
+ this._view = this.type;
164
+ if (this.type === 'picker') {
165
+ const [r, g, b] = hexToRgb(this._norm(this.value));
166
+ [this._hue, this._sat, this._val] = rgbToHsv(r, g, b);
167
+ this._syncInputs();
168
+ }
169
+ this._injectCss();
170
+ document.addEventListener('mousemove', this._onGlobalMouseMove);
171
+ document.addEventListener('mouseup', this._onGlobalMouseUp);
172
+ }
173
+
174
+ disconnectedCallback() {
175
+ document.removeEventListener('mousemove', this._onGlobalMouseMove);
176
+ document.removeEventListener('mouseup', this._onGlobalMouseUp);
177
+ super.disconnectedCallback();
178
+ }
179
+
180
+ protected updated(changed: Map<string, unknown>) {
181
+ if (changed.has('type')) {
182
+ this._view = this.type;
183
+ if (this.type === 'picker') {
184
+ const [r, g, b] = hexToRgb(this._norm(this.value));
185
+ [this._hue, this._sat, this._val] = rgbToHsv(r, g, b);
186
+ this._syncInputs();
187
+ }
188
+ }
189
+ if (changed.has('value') && this.type === 'picker' && this._view === 'picker' &&
190
+ !this._draggingSV && !this._draggingHue) {
191
+ const [r, g, b] = hexToRgb(this._norm(this.value));
192
+ [this._hue, this._sat, this._val] = rgbToHsv(r, g, b);
193
+ this._syncInputs();
194
+ }
195
+
196
+ if (this._view === 'picker') {
197
+ requestAnimationFrame(() => {
198
+ this._svCanvas = this.querySelector('.ncp-sv-canvas') as HTMLCanvasElement;
199
+ this._hueCanvas = this.querySelector('.ncp-hue-canvas') as HTMLCanvasElement;
200
+ this._drawSV();
201
+ this._drawHue();
202
+ });
203
+ }
204
+ }
205
+
206
+ /* ── public API ──────────────────────────────────────────────────── */
207
+ /** Reset the view back to the default type (call this when the host popover closes). */
208
+ reset() {
209
+ this._view = this.type;
210
+ }
211
+
212
+
213
+ private _injectCss() {
214
+ if (this.querySelector('style[data-nile-color-picker]')) return;
215
+ const style = document.createElement('style');
216
+ style.setAttribute('data-nile-color-picker', '');
217
+ style.textContent = getCssText();
218
+ this.insertBefore(style, this.firstChild);
219
+ }
220
+
221
+
222
+ private _norm(hex: string) {
223
+ const t = hex.trim();
224
+ if (/^#[0-9a-f]{6}$/i.test(t)) return t.toUpperCase();
225
+ if (/^#[0-9a-f]{3}$/i.test(t)) {
226
+ const [a, b, c] = t.slice(1).toUpperCase().split('');
227
+ return `#${a}${a}${b}${b}${c}${c}`;
228
+ }
229
+ return '#000000';
230
+ }
231
+
232
+ private get _paletteColors(): PaletteEntry[] {
233
+ return this.palette.length > 0 ? this.palette : DEFAULT_PALETTE;
234
+ }
235
+
236
+
237
+ private _emit(value: string) {
238
+ this.dispatchEvent(new CustomEvent('nile-change', {
239
+ detail: { value },
240
+ bubbles: true,
241
+ composed: true,
242
+ }));
243
+ }
244
+
245
+ private _apply(color: string) {
246
+ const hex = this._norm(color);
247
+ this.value = hex;
248
+ const entry: PaletteEntry = { color: hex, name: hex };
249
+ this._recentColors = [entry, ...this._recentColors.filter(e => this._norm(e.color) !== hex)].slice(0, this.recentColorsCount);
250
+ this._emit(hex);
251
+ if (this.mode === 'popover' && this._view === 'swatches') {
252
+ this._closePopover();
253
+ }
254
+ }
255
+
256
+ private _closePopover() {
257
+ const pop = this.querySelector('nile-popover') as any;
258
+ if (pop) pop.isShow = false;
259
+ }
260
+
261
+ private _openPicker() {
262
+ const [r, g, b] = hexToRgb(this._norm(this.value));
263
+ const [h, s, v] = rgbToHsv(r, g, b);
264
+ this._hue = h; this._sat = s; this._val = v;
265
+ this._syncInputs();
266
+ this._view = 'picker';
267
+ }
268
+
269
+ private _syncInputs() {
270
+ const [r, g, b] = hsvToRgb(this._hue, this._sat, this._val);
271
+ const hex = rgbToHex(r, g, b);
272
+ this._hexInput = hex; this._r = r; this._g = g; this._b = b;
273
+ this._pendingColor = hex;
274
+ }
275
+
276
+ private _drawSV() {
277
+ const canvas = this._svCanvas;
278
+ if (!canvas) return;
279
+ const ctx = canvas.getContext('2d');
280
+ if (!ctx) return;
281
+ const { width: w, height: h } = canvas;
282
+ const [hr, hg, hb] = hsvToRgb(this._hue, 1, 1);
283
+ ctx.fillStyle = `rgb(${hr},${hg},${hb})`;
284
+ ctx.fillRect(0, 0, w, h);
285
+ const wg = ctx.createLinearGradient(0, 0, w, 0);
286
+ wg.addColorStop(0, 'rgba(255,255,255,1)'); wg.addColorStop(1, 'rgba(255,255,255,0)');
287
+ ctx.fillStyle = wg; ctx.fillRect(0, 0, w, h);
288
+ const bg = ctx.createLinearGradient(0, 0, 0, h);
289
+ bg.addColorStop(0, 'rgba(0,0,0,0)'); bg.addColorStop(1, 'rgba(0,0,0,1)');
290
+ ctx.fillStyle = bg; ctx.fillRect(0, 0, w, h);
291
+ }
292
+
293
+ private _drawHue() {
294
+ const canvas = this._hueCanvas;
295
+ if (!canvas) return;
296
+ const ctx = canvas.getContext('2d');
297
+ if (!ctx) return;
298
+ const grad = ctx.createLinearGradient(0, 0, canvas.width, 0);
299
+ ['#FF0000','#FFFF00','#00FF00','#00FFFF','#0000FF','#FF00FF','#FF0000']
300
+ .forEach((c, i, a) => grad.addColorStop(i / (a.length - 1), c));
301
+ ctx.fillStyle = grad; ctx.fillRect(0, 0, canvas.width, canvas.height);
302
+ }
303
+
304
+ private _onSVMouseDown = (e: MouseEvent) => { e.preventDefault(); this._draggingSV = true; this._updateSV(e); };
305
+ private _onHueMouseDown = (e: MouseEvent) => { e.preventDefault(); this._draggingHue = true; this._updateHue(e); };
306
+
307
+ private _onGlobalMouseMove = (e: MouseEvent) => {
308
+ if (this._draggingSV) this._updateSV(e);
309
+ if (this._draggingHue) this._updateHue(e);
310
+ };
311
+ private _onGlobalMouseUp = () => {
312
+ const wasDraggingSV = this._draggingSV;
313
+ const wasDraggingHue = this._draggingHue;
314
+ if (!this.showActions && this._view === 'picker' && (wasDraggingSV || wasDraggingHue)) {
315
+ this._commitPickerIfNoActions();
316
+ }
317
+ this._draggingSV = false;
318
+ this._draggingHue = false;
319
+ };
320
+
321
+ private _commitPickerIfNoActions() {
322
+ if (this.showActions) return;
323
+ this._apply(this._pendingColor);
324
+ if (this.mode === 'popover') {
325
+ if (this.type === 'swatches') this._view = 'swatches';
326
+ this._closePopover();
327
+ }
328
+ }
329
+
330
+ private _updateSV(e: MouseEvent) {
331
+ const canvas = this._svCanvas; if (!canvas) return;
332
+ const rect = canvas.getBoundingClientRect();
333
+ this._sat = Math.max(0, Math.min(e.clientX - rect.left, rect.width)) / rect.width;
334
+ this._val = 1 - Math.max(0, Math.min(e.clientY - rect.top, rect.height)) / rect.height;
335
+ this._syncInputs();
336
+ }
337
+
338
+ private _updateHue(e: MouseEvent) {
339
+ const canvas = this._hueCanvas; if (!canvas) return;
340
+ const rect = canvas.getBoundingClientRect();
341
+ this._hue = (Math.max(0, Math.min(e.clientX - rect.left, rect.width)) / rect.width) * 360;
342
+ this._syncInputs();
343
+ }
344
+
345
+ private _onHexInput = (e: Event) => {
346
+ let val = ((e as CustomEvent).detail?.value ?? (e.target as HTMLInputElement).value ?? '').trim();
347
+ if (!val.startsWith('#')) val = '#' + val;
348
+ if (/^#[0-9a-f]{6}$/i.test(val)) {
349
+ const [r, g, b] = hexToRgb(val);
350
+ [this._hue, this._sat, this._val] = rgbToHsv(r, g, b);
351
+ this._syncInputs();
352
+ if (this._view === 'picker') this._commitPickerIfNoActions();
353
+ }
354
+ };
355
+
356
+ private _onRgbInput = (ch: 'r' | 'g' | 'b', e: Event) => {
357
+ const raw = (e as CustomEvent).detail?.value ?? (e.target as HTMLInputElement).value;
358
+ const v = Math.max(0, Math.min(255, parseInt(String(raw), 10) || 0));
359
+ if (ch === 'r') this._r = v; else if (ch === 'g') this._g = v; else this._b = v;
360
+ [this._hue, this._sat, this._val] = rgbToHsv(this._r, this._g, this._b);
361
+ this._syncInputs();
362
+ if (this._view === 'picker') this._commitPickerIfNoActions();
363
+ };
364
+
365
+ private _onColorModeSelect = (e: CustomEvent<{ value: string }>) => {
366
+ e.stopPropagation();
367
+ const v = e.detail?.value;
368
+ if (v === 'HEX' || v === 'RGB') {
369
+ this._colorMode = v;
370
+ }
371
+ };
372
+
373
+ private _pickEyedropper = async () => {
374
+ const Ctor = (window as any).EyeDropper;
375
+ if (!Ctor) { this._openPicker(); return; }
376
+ try {
377
+ const result = await new Ctor().open();
378
+ if (result?.sRGBHex) this._apply(result.sRGBHex);
379
+ } catch { /* cancelled */ }
380
+ };
381
+ private _renderSwatch(entry: PaletteEntry) {
382
+ const norm = this._norm(entry.color);
383
+ const isActive = this._norm(this.value) === norm;
384
+ const tip = this.swatchTooltip === 'name'
385
+ ? (entry.name || norm)
386
+ : this.swatchTooltip === 'value'
387
+ ? norm
388
+ : entry.name ? `${entry.name} (${norm})` : norm;
389
+ return html`
390
+ <nile-lite-tooltip content=${tip}>
391
+ <nile-color-swatch
392
+ color=${norm}
393
+ ?active=${isActive}
394
+ @click=${() => this._apply(norm)}
395
+ ></nile-color-swatch>
396
+ </nile-lite-tooltip>
397
+ `;
398
+ }
399
+
400
+ private _renderSwatchesView() {
401
+ const recent = this._recentColors.slice(0, this.recentColorsCount);
402
+ return html`
403
+ <div class="ncp-panel">
404
+ <div class="ncp-palette-grid">
405
+ ${this._paletteColors.map(e => this._renderSwatch(e))}
406
+ </div>
407
+
408
+ ${this.noFill ? html`
409
+ <button class="ncp-no-fill" @click=${() => this._emit('none')}>
410
+ <nile-glyph name="disabled"></nile-glyph>
411
+ <span>No Fill</span>
412
+ </button>
413
+ ` : nothing}
414
+
415
+ <div class="ncp-custom-section">
416
+ <p class="ncp-custom-title">Custom</p>
417
+ <div class="ncp-custom-row">
418
+ ${recent.map(e => this._renderSwatch(e))}
419
+ <nile-lite-tooltip content="Add custom color">
420
+ <button class="ncp-action-btn" @click=${() => this._openPicker()}>
421
+ <nile-glyph name="plus"></nile-glyph>
422
+ </button>
423
+ </nile-lite-tooltip>
424
+ <nile-lite-tooltip content="Pick from screen">
425
+ <button class="ncp-action-btn" @click=${this._pickEyedropper}>
426
+ <nile-glyph name="colorize" size="12"></nile-glyph>
427
+ </button>
428
+ </nile-lite-tooltip>
429
+ </div>
430
+ </div>
431
+ </div>
432
+ `;
433
+ }
434
+
435
+ private _renderPickerView() {
436
+ const svX = this._sat * 100;
437
+ const svY = (1 - this._val) * 100;
438
+ const hueX = (this._hue / 360) * 100;
439
+
440
+ return html`
441
+ <div class="ncp-panel ncp-panel--picker">
442
+
443
+ ${this.showCanvas ? html`
444
+ <div class="ncp-sv-area" @mousedown=${this._onSVMouseDown}>
445
+ <canvas class="ncp-sv-canvas" width="244" height="130"></canvas>
446
+ <div class="ncp-sv-thumb" style="left:${svX}%;top:${svY}%">
447
+ <div class="ncp-sv-thumb-inner"></div>
448
+ </div>
449
+ </div>
450
+ ` : nothing}
451
+
452
+ ${this.showHue ? html`
453
+ <div class="ncp-hue-row">
454
+ <div class="ncp-hue-wrap" @mousedown=${this._onHueMouseDown}>
455
+ <canvas class="ncp-hue-canvas" width="244" height="14"></canvas>
456
+ <div class="ncp-hue-thumb" style="left:${hueX}%"></div>
457
+ </div>
458
+ <button class="ncp-eyedropper" title="Pick from screen" @click=${this._pickEyedropper}>
459
+ <nile-glyph name="colorize" size="16"></nile-glyph>
460
+ </button>
461
+ </div>
462
+ ` : nothing}
463
+
464
+ ${this.showInputs ? html`
465
+ <div class="ncp-input-row">
466
+ <nile-dropdown class="ncp-mode-dropdown" placement="bottom-start" distance="4" hoist>
467
+ <nile-button slot="trigger" class="ncp-mode-btn" variant="tertiary">
468
+ ${this._colorMode} <nile-icon name="arrowdown" size="12"></nile-icon>
469
+ </nile-button>
470
+ <nile-menu @nile-select=${this._onColorModeSelect}>
471
+ <nile-menu-item value="HEX" ?active=${this._colorMode === 'HEX'}>HEX</nile-menu-item>
472
+ <nile-menu-item value="RGB" ?active=${this._colorMode === 'RGB'}>RGB</nile-menu-item>
473
+ </nile-menu>
474
+ </nile-dropdown>
475
+ ${this._colorMode === 'HEX'
476
+ ? html`<nile-input class="ncp-hex" .value=${this._hexInput} @nile-input=${this._onHexInput}></nile-input>`
477
+ : html`
478
+ <nile-input class="ncp-rgb" type="number" min="0" max="255" .value=${String(this._r)} @nile-input=${(e: Event) => this._onRgbInput('r', e)}></nile-input>
479
+ <nile-input class="ncp-rgb" type="number" min="0" max="255" .value=${String(this._g)} @nile-input=${(e: Event) => this._onRgbInput('g', e)}></nile-input>
480
+ <nile-input class="ncp-rgb" type="number" min="0" max="255" .value=${String(this._b)} @nile-input=${(e: Event) => this._onRgbInput('b', e)}></nile-input>
481
+ `}
482
+ </div>
483
+ ` : nothing}
484
+
485
+ ${this.showActions ? html`
486
+ <div class="ncp-picker-actions">
487
+ ${this.type === 'swatches'
488
+ ? html`<nile-button variant="secondary" size="small" @click=${() => { this._view = 'swatches'; }}>Cancel</nile-button>`
489
+ : nothing}
490
+ <nile-button variant="primary" size="small" @click=${() => { this._apply(this._pendingColor); if (this.type === 'swatches') this._view = 'swatches'; this._closePopover(); }}>Okay</nile-button>
491
+ </div>
492
+ ` : nothing}
493
+
494
+ </div>
495
+ `;
496
+ }
497
+
498
+ private _renderTrigger() {
499
+ const isNoFill = this.value === 'none';
500
+ const displayColor = isNoFill ? null : this._norm(this.value);
501
+ return html`
502
+ <button
503
+ slot="anchor"
504
+ class="ncp-trigger${isNoFill ? ' ncp-trigger--no-fill' : ''}"
505
+ type="button"
506
+ aria-label=${isNoFill ? 'No Fill' : displayColor}
507
+ style=${isNoFill ? '' : `background:${displayColor}`}
508
+ ></button>
509
+ `;
510
+ }
511
+
512
+ render() {
513
+ const panel = this._view === 'picker' ? this._renderPickerView() : this._renderSwatchesView();
514
+
515
+ if (this.mode === 'popover') {
516
+ return html`
517
+ <nile-popover
518
+ .arrow=${false}
519
+ flip
520
+ .open=${this.open}
521
+ placement="bottom-start"
522
+ id="color-picker-popover"
523
+ distance="8"
524
+ @nile-hide=${() => this.reset()}
525
+ >
526
+ ${this._renderTrigger()}
527
+ ${panel}
528
+ </nile-popover>
529
+ `;
530
+ }
531
+
532
+ return panel;
533
+ }
534
+ }
535
+
536
+
537
+ declare global {
538
+ interface HTMLElementTagNameMap {
539
+ 'nile-color-picker': NileColorPicker;
540
+ }
541
+ }
@@ -0,0 +1 @@
1
+ export { NileColorSwatch } from './nile-color-swatch';
@@ -0,0 +1,46 @@
1
+ import { css } from 'lit';
2
+
3
+ export const styles = css`
4
+ :host {
5
+ display: inline-block;
6
+ }
7
+
8
+ .swatch {
9
+ position: relative;
10
+ width: var(--nile-spacing-3xl, var(--ng-spacing-3xl));
11
+ height: var(--nile-spacing-3xl, var(--ng-spacing-3xl));
12
+ border: 1px solid var(--nile-colors-neutral-500, var(--ng-colors-border-secondary));
13
+ border-radius: var(--nile-radius-sm, var(--ng-radius-sm));
14
+ background: transparent;
15
+ cursor: pointer;
16
+ box-sizing: border-box;
17
+ padding: 0;
18
+ outline: none;
19
+ transition: transform 0.15s ease, border-color 0.15s ease;
20
+ overflow: hidden;
21
+ }
22
+
23
+ .swatch:hover {
24
+ transform: translateY(-1px);
25
+ }
26
+
27
+
28
+ .swatch__fill {
29
+ position: absolute;
30
+ inset: 0;
31
+ }
32
+
33
+ .swatch__check {
34
+ position: absolute;
35
+ inset: 0;
36
+ display: grid;
37
+ place-items: center;
38
+ pointer-events: none;
39
+ filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.24));
40
+ }
41
+
42
+ .swatch__check nile-glyph {
43
+ --size: var(--nile-spacing-2xl, var(--ng-spacing-2xl));
44
+ display: block;
45
+ }
46
+ `;
@@ -0,0 +1,72 @@
1
+ import { LitElement, html, nothing, CSSResultArray } from 'lit';
2
+ import { customElement, property } from 'lit/decorators.js';
3
+ import { styles } from './nile-color-swatch.css';
4
+
5
+
6
+ @customElement('nile-color-swatch')
7
+ export class NileColorSwatch extends LitElement {
8
+ static get styles(): CSSResultArray {
9
+ return [styles];
10
+ }
11
+
12
+ /** The hex color this swatch represents. */
13
+ @property({ type: String, reflect: true })color = '#000000';
14
+
15
+ /** Whether this swatch is the currently selected color. */
16
+ @property({ type: Boolean, reflect: true })active = false;
17
+
18
+
19
+ private _tickColor(): string {
20
+ const hex = this.color.replace('#', '');
21
+
22
+ if (hex.length !== 6) {
23
+ return '#FFFFFF';
24
+ }
25
+
26
+ const r = parseInt(hex.substring(0, 2), 16) / 255;
27
+ const g = parseInt(hex.substring(2, 4), 16) / 255;
28
+ const b = parseInt(hex.substring(4, 6), 16) / 255;
29
+
30
+ const toLinear = (c: number) =>
31
+ c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
32
+
33
+ const R = toLinear(r);
34
+ const G = toLinear(g);
35
+ const B = toLinear(b);
36
+
37
+ const luminance = 0.2126 * R + 0.7152 * G + 0.0722 * B;
38
+
39
+ const contrastWithWhite = (1.05) / (luminance + 0.05);
40
+ const contrastWithBlack = (luminance + 0.05) / 0.05;
41
+ return contrastWithWhite > contrastWithBlack ? '#FFFFFF' : '#111827';
42
+ }
43
+
44
+ render() {
45
+ const cls = `swatch${this.active ? ' swatch--active' : ''}`;
46
+ const tickColor = this._tickColor();
47
+ return html`
48
+ <button
49
+ class=${cls}
50
+ part="base"
51
+ type="button"
52
+ aria-pressed=${this.active ? 'true' : 'false'}
53
+ aria-label=${this.color}
54
+ >
55
+ <span class="swatch__fill" style="background:${this.color}"></span>
56
+ ${this.active
57
+ ? html`
58
+ <span class="swatch__check">
59
+ <nile-glyph name="tick" color=${tickColor}></nile-glyph>
60
+ </span>
61
+ `
62
+ : nothing}
63
+ </button>
64
+ `;
65
+ }
66
+ }
67
+
68
+ declare global {
69
+ interface HTMLElementTagNameMap {
70
+ 'nile-color-swatch': NileColorSwatch;
71
+ }
72
+ }