@dile/ui 2.27.4 → 3.0.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.
- package/components/button/src/DileButtonIcon.js +1 -0
- package/components/color-picker/color-picker.js +3 -0
- package/components/color-picker/color-picker.spec.js +117 -0
- package/components/color-picker/index.js +1 -0
- package/components/color-picker/src/DileColorPicker.js +496 -0
- package/components/drop-file/src/DileDropFile.js +2 -7
- package/components/input/index.js +1 -0
- package/components/input/src/DileInput.js +14 -18
- package/components/input/src/DileInputSearch.js +1 -1
- package/components/input/src/label-styles.js +11 -0
- package/components/number-picker/src/DileNumberPicker.js +2 -7
- package/components/otp-input/src/DileOtpInput.js +3 -9
- package/components/radio-group/src/DileRadioGroup.js +2 -7
- package/components/range/src/DileRange.js +6 -10
- package/components/select/src/DileSelect.js +6 -11
- package/components/select/src/DileSelectAjax.js +2 -7
- package/components/textarea/src/DileTextarea.js +9 -8
- package/components/time-picker/src/DileTimePicker.js +2 -7
- package/lib/color/colorConversion.js +87 -0
- package/lib/color/colorConversion.test.js +94 -0
- package/package.json +2 -2
|
@@ -19,6 +19,7 @@ export class DileButtonIcon extends DileButton {
|
|
|
19
19
|
--dile-icon-color: var(--dile-button-icon-color, var(--dile-on-primary-color, #ffffff));
|
|
20
20
|
}
|
|
21
21
|
dile-icon {
|
|
22
|
+
--dile-icon-size: var(--dile-button-icon-size, calc(var(--dile-button-font-size, 1rem) * 1.125));
|
|
22
23
|
margin-right: var(--dile-button-icon-separation, 0.3rem);
|
|
23
24
|
}
|
|
24
25
|
button:hover {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import './color-picker.js';
|
|
3
|
+
|
|
4
|
+
describe('dile-color-picker', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
document.body.innerHTML = '';
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
async function renderColorPicker(html) {
|
|
10
|
+
document.body.innerHTML = html;
|
|
11
|
+
const el = document.body.querySelector('dile-color-picker');
|
|
12
|
+
await el.updateComplete;
|
|
13
|
+
return el;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
it('renders a trigger swatch matching the initial value', async () => {
|
|
17
|
+
const el = await renderColorPicker('<dile-color-picker name="color" value="#3366ff"></dile-color-picker>');
|
|
18
|
+
|
|
19
|
+
const trigger = el.shadowRoot.getElementById('trigger');
|
|
20
|
+
expect(trigger).toBeTruthy();
|
|
21
|
+
expect(trigger.style.backgroundColor).toBe('rgb(51, 102, 255)');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('normalizes 3-digit hex shorthand set externally', async () => {
|
|
25
|
+
const el = await renderColorPicker('<dile-color-picker name="color" value="#0f0"></dile-color-picker>');
|
|
26
|
+
|
|
27
|
+
expect(el.value).toBe('#00ff00');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('updates value and hue/saturation state when the hex input changes', async () => {
|
|
31
|
+
const el = await renderColorPicker('<dile-color-picker name="color" value="#000000"></dile-color-picker>');
|
|
32
|
+
|
|
33
|
+
const hexInput = el.shadowRoot.querySelector('.hex-input');
|
|
34
|
+
hexInput.value = '#ff0000';
|
|
35
|
+
hexInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
36
|
+
await el.updateComplete;
|
|
37
|
+
|
|
38
|
+
expect(el.value).toBe('#ff0000');
|
|
39
|
+
expect(el._hue).toBe(0);
|
|
40
|
+
expect(el._saturation).toBe(100);
|
|
41
|
+
expect(el._value).toBe(100);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('dispatches element-changed with name and value when the value changes', async () => {
|
|
45
|
+
const el = await renderColorPicker('<dile-color-picker name="color" value="#000000"></dile-color-picker>');
|
|
46
|
+
let detail = null;
|
|
47
|
+
el.addEventListener('element-changed', (e) => { detail = e.detail; });
|
|
48
|
+
|
|
49
|
+
const hexInput = el.shadowRoot.querySelector('.hex-input');
|
|
50
|
+
hexInput.value = '#00ff00';
|
|
51
|
+
hexInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
52
|
+
await el.updateComplete;
|
|
53
|
+
|
|
54
|
+
expect(detail).toEqual({ name: 'color', value: '#00ff00' });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('updates value when the hue slider changes', async () => {
|
|
58
|
+
const el = await renderColorPicker('<dile-color-picker name="color" value="#ff0000"></dile-color-picker>');
|
|
59
|
+
|
|
60
|
+
const hueSlider = el.shadowRoot.querySelector('.hue-slider');
|
|
61
|
+
hueSlider.value = '120';
|
|
62
|
+
hueSlider.dispatchEvent(new Event('input', { bubbles: true }));
|
|
63
|
+
await el.updateComplete;
|
|
64
|
+
|
|
65
|
+
expect(el.value).toBe('#00ff00');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('nudges saturation and brightness with arrow keys on the sv square', async () => {
|
|
69
|
+
const el = await renderColorPicker('<dile-color-picker name="color" value="#ff0000"></dile-color-picker>');
|
|
70
|
+
|
|
71
|
+
const svSquare = el.shadowRoot.getElementById('sv-square');
|
|
72
|
+
svSquare.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }));
|
|
73
|
+
await el.updateComplete;
|
|
74
|
+
|
|
75
|
+
expect(el._saturation).toBe(99);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('does not change value when disabled', async () => {
|
|
79
|
+
const el = await renderColorPicker('<dile-color-picker name="color" value="#ff0000" disabled></dile-color-picker>');
|
|
80
|
+
|
|
81
|
+
const trigger = el.shadowRoot.getElementById('trigger');
|
|
82
|
+
expect(trigger.disabled).toBe(true);
|
|
83
|
+
|
|
84
|
+
const svSquare = el.shadowRoot.getElementById('sv-square');
|
|
85
|
+
svSquare.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }));
|
|
86
|
+
await el.updateComplete;
|
|
87
|
+
|
|
88
|
+
expect(el.value).toBe('#ff0000');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('reverts the hue slider and ignores hex edits when readonly', async () => {
|
|
92
|
+
const el = await renderColorPicker('<dile-color-picker name="color" value="#ff0000" readonly></dile-color-picker>');
|
|
93
|
+
|
|
94
|
+
const hueSlider = el.shadowRoot.querySelector('.hue-slider');
|
|
95
|
+
hueSlider.value = '200';
|
|
96
|
+
hueSlider.dispatchEvent(new Event('input', { bubbles: true }));
|
|
97
|
+
await el.updateComplete;
|
|
98
|
+
|
|
99
|
+
expect(el.value).toBe('#ff0000');
|
|
100
|
+
expect(hueSlider.value).toBe('0');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('participates in native form submission via FormData', async () => {
|
|
104
|
+
document.body.innerHTML = `
|
|
105
|
+
<form id="testForm">
|
|
106
|
+
<dile-color-picker name="color" value="#123456"></dile-color-picker>
|
|
107
|
+
</form>
|
|
108
|
+
`;
|
|
109
|
+
const el = document.body.querySelector('dile-color-picker');
|
|
110
|
+
await el.updateComplete;
|
|
111
|
+
|
|
112
|
+
const form = document.getElementById('testForm');
|
|
113
|
+
const data = new FormData(form);
|
|
114
|
+
|
|
115
|
+
expect(data.get('color')).toBe('#123456');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DileColorPicker } from './src/DileColorPicker.js';
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import { DileEmmitChange } from '../../../mixins/form/index.js';
|
|
3
|
+
import { DileOverlay } from '../../../mixins/overlay/index.js';
|
|
4
|
+
import { DileCloseDocumentClick } from '../../../mixins/close-document-click/index.js';
|
|
5
|
+
import { DileCloseOnEscPressed } from '../../../mixins/close-on-esc-pressed/index.js';
|
|
6
|
+
import { labelStyles, messageStyles } from '../../input/index.js';
|
|
7
|
+
import { normalizeHex, hexToHsv, hsvToHex } from '../../../lib/color/colorConversion.js';
|
|
8
|
+
|
|
9
|
+
export class DileColorPicker extends DileOverlay(
|
|
10
|
+
DileCloseDocumentClick(
|
|
11
|
+
DileCloseOnEscPressed(
|
|
12
|
+
DileEmmitChange(LitElement)
|
|
13
|
+
)
|
|
14
|
+
)
|
|
15
|
+
) {
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Listen to the generic `element-changed` event to receive value updates
|
|
19
|
+
*
|
|
20
|
+
* @event element-changed
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
static get properties() {
|
|
24
|
+
return {
|
|
25
|
+
/** Label to the element */
|
|
26
|
+
label: { type: String },
|
|
27
|
+
|
|
28
|
+
/** Name for this input field */
|
|
29
|
+
name: { type: String },
|
|
30
|
+
|
|
31
|
+
/** Set initial value to the input. Must be a 6-digit hex color, e.g. "#3366ff" */
|
|
32
|
+
value: { type: String },
|
|
33
|
+
|
|
34
|
+
/** Disable the input field */
|
|
35
|
+
disabled: { type: Boolean },
|
|
36
|
+
|
|
37
|
+
/** ReadOnly attribute */
|
|
38
|
+
readonly: { type: Boolean },
|
|
39
|
+
|
|
40
|
+
/** Has error on this input field */
|
|
41
|
+
errored: { type: Boolean },
|
|
42
|
+
|
|
43
|
+
/** Message Displayed */
|
|
44
|
+
message: { type: String },
|
|
45
|
+
|
|
46
|
+
/** Hide errors on input */
|
|
47
|
+
hideErrorOnInput: { type: Boolean },
|
|
48
|
+
|
|
49
|
+
/** Set the application focus to this the input component after the initialization */
|
|
50
|
+
focusOnStart: { type: Boolean },
|
|
51
|
+
|
|
52
|
+
_hue: { state: true },
|
|
53
|
+
_saturation: { state: true },
|
|
54
|
+
_value: { state: true },
|
|
55
|
+
_hexInputValue: { state: true },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static get formAssociated() {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
constructor() {
|
|
64
|
+
super();
|
|
65
|
+
this.label = '';
|
|
66
|
+
this.name = '';
|
|
67
|
+
this.value = '#000000';
|
|
68
|
+
this.disabled = false;
|
|
69
|
+
this.readonly = false;
|
|
70
|
+
this.errored = false;
|
|
71
|
+
this.message = '';
|
|
72
|
+
this.hideErrorOnInput = false;
|
|
73
|
+
this.focusOnStart = false;
|
|
74
|
+
|
|
75
|
+
const { h, s, v } = hexToHsv(this.value);
|
|
76
|
+
this._hue = h;
|
|
77
|
+
this._saturation = s;
|
|
78
|
+
this._value = v;
|
|
79
|
+
this._hexInputValue = this.value;
|
|
80
|
+
this._skipValueSync = false;
|
|
81
|
+
|
|
82
|
+
this.internals = this.attachInternals();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get opened() {
|
|
86
|
+
return this._overlayClass === 'opened';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get svSquareEl() {
|
|
90
|
+
return this.shadowRoot.getElementById('sv-square');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
firstUpdated(changedProperties) {
|
|
94
|
+
super.firstUpdated(changedProperties);
|
|
95
|
+
if (this.focusOnStart) {
|
|
96
|
+
this.focus();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
willUpdate(changedProperties) {
|
|
101
|
+
const skip = this._skipValueSync;
|
|
102
|
+
this._skipValueSync = false;
|
|
103
|
+
if (changedProperties.has('value') && !skip) {
|
|
104
|
+
const normalized = normalizeHex(this.value);
|
|
105
|
+
if (normalized) {
|
|
106
|
+
const { h, s, v } = hexToHsv(normalized);
|
|
107
|
+
this._hue = h;
|
|
108
|
+
this._saturation = s;
|
|
109
|
+
this._value = v;
|
|
110
|
+
if (normalized !== this.value) {
|
|
111
|
+
this.value = normalized;
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
console.warn(`dile-color-picker: invalid value "${this.value}", ignoring`);
|
|
115
|
+
}
|
|
116
|
+
this._hexInputValue = this.value;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
updated(changedProperties) {
|
|
121
|
+
if (changedProperties.has('value')) {
|
|
122
|
+
this.emmitChange();
|
|
123
|
+
this.internals.setFormValue(this.value);
|
|
124
|
+
}
|
|
125
|
+
if (changedProperties.has('_overlayClass')) {
|
|
126
|
+
if (this._overlayClass === 'opened') {
|
|
127
|
+
this.svSquareEl?.focus();
|
|
128
|
+
} else if (changedProperties.get('_overlayClass') === 'opened') {
|
|
129
|
+
this.trigger?.focus();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static get styles() {
|
|
135
|
+
return [
|
|
136
|
+
labelStyles,
|
|
137
|
+
messageStyles,
|
|
138
|
+
css`
|
|
139
|
+
* {
|
|
140
|
+
box-sizing: border-box;
|
|
141
|
+
}
|
|
142
|
+
:host {
|
|
143
|
+
display: inline-block;
|
|
144
|
+
margin-bottom: 10px;
|
|
145
|
+
}
|
|
146
|
+
#trigger {
|
|
147
|
+
padding: 0;
|
|
148
|
+
width: var(--dile-color-picker-swatch-size, 2em);
|
|
149
|
+
height: var(--dile-color-picker-swatch-size, 2em);
|
|
150
|
+
border-radius: var(--dile-color-picker-swatch-border-radius, 5px);
|
|
151
|
+
border: var(--dile-color-picker-swatch-border-width, 1px) solid var(--dile-color-picker-swatch-border-color, #888);
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
}
|
|
154
|
+
#trigger:disabled {
|
|
155
|
+
cursor: default;
|
|
156
|
+
opacity: 0.6;
|
|
157
|
+
}
|
|
158
|
+
#trigger.errored {
|
|
159
|
+
border-color: var(--dile-color-picker-error-border-color, #c00);
|
|
160
|
+
}
|
|
161
|
+
#trigger:focus-visible {
|
|
162
|
+
outline: none;
|
|
163
|
+
border-width: var(--dile-color-picker-swatch-focus-border-width, 3px);
|
|
164
|
+
border-color: var(--dile-input-focus-border-color, var(--dile-link-color, #6af));
|
|
165
|
+
}
|
|
166
|
+
#overlay {
|
|
167
|
+
display: none;
|
|
168
|
+
position: absolute;
|
|
169
|
+
opacity: 0;
|
|
170
|
+
z-index: var(--dile-color-picker-panel-z-index, 999);
|
|
171
|
+
width: var(--dile-color-picker-panel-width, 220px);
|
|
172
|
+
background-color: var(--dile-color-picker-panel-background-color, #fff);
|
|
173
|
+
border-radius: var(--dile-color-picker-panel-border-radius, 5px);
|
|
174
|
+
box-shadow: var(--dile-color-picker-panel-box-shadow, 0 0 20px rgba(102, 102, 102, 0.5));
|
|
175
|
+
padding: var(--dile-color-picker-panel-padding, 1rem);
|
|
176
|
+
transition: var(--dile-color-picker-panel-transition, ease 0.2s);
|
|
177
|
+
transition-property: transform, opacity;
|
|
178
|
+
transform: translateY(-10px);
|
|
179
|
+
}
|
|
180
|
+
#overlay.opened {
|
|
181
|
+
opacity: 1;
|
|
182
|
+
transform: translateY(0);
|
|
183
|
+
}
|
|
184
|
+
#sv-square {
|
|
185
|
+
position: relative;
|
|
186
|
+
width: 100%;
|
|
187
|
+
height: var(--dile-color-picker-sv-square-size, 160px);
|
|
188
|
+
border-radius: var(--dile-color-picker-sv-square-border-radius, 4px);
|
|
189
|
+
background-image: linear-gradient(to top, #000, transparent),
|
|
190
|
+
linear-gradient(to right, #fff, transparent);
|
|
191
|
+
touch-action: none;
|
|
192
|
+
cursor: crosshair;
|
|
193
|
+
}
|
|
194
|
+
#sv-square:focus-visible {
|
|
195
|
+
outline: 2px solid var(--dile-input-focus-border-color, var(--dile-link-color, #6af));
|
|
196
|
+
outline-offset: 2px;
|
|
197
|
+
}
|
|
198
|
+
.sv-thumb {
|
|
199
|
+
position: absolute;
|
|
200
|
+
width: var(--dile-color-picker-sv-thumb-size, 14px);
|
|
201
|
+
height: var(--dile-color-picker-sv-thumb-size, 14px);
|
|
202
|
+
border-radius: 50%;
|
|
203
|
+
border: var(--dile-color-picker-sv-thumb-border-width, 2px) solid var(--dile-color-picker-sv-thumb-border-color, #fff);
|
|
204
|
+
box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
|
|
205
|
+
transform: translate(-50%, -50%);
|
|
206
|
+
pointer-events: none;
|
|
207
|
+
}
|
|
208
|
+
.hue-slider {
|
|
209
|
+
display: block;
|
|
210
|
+
width: 100%;
|
|
211
|
+
margin-top: 0.75rem;
|
|
212
|
+
-webkit-appearance: none;
|
|
213
|
+
-moz-appearance: none;
|
|
214
|
+
appearance: none;
|
|
215
|
+
background: transparent;
|
|
216
|
+
cursor: pointer;
|
|
217
|
+
}
|
|
218
|
+
.hue-slider:focus {
|
|
219
|
+
outline: none;
|
|
220
|
+
}
|
|
221
|
+
.hue-slider::-webkit-slider-runnable-track {
|
|
222
|
+
height: var(--dile-color-picker-hue-track-height, 12px);
|
|
223
|
+
border-radius: var(--dile-color-picker-hue-track-border-radius, 6px);
|
|
224
|
+
background: linear-gradient(
|
|
225
|
+
to right,
|
|
226
|
+
hsl(0, 100%, 50%), hsl(60, 100%, 50%), hsl(120, 100%, 50%),
|
|
227
|
+
hsl(180, 100%, 50%), hsl(240, 100%, 50%), hsl(300, 100%, 50%), hsl(360, 100%, 50%)
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
.hue-slider::-webkit-slider-thumb {
|
|
231
|
+
-webkit-appearance: none;
|
|
232
|
+
appearance: none;
|
|
233
|
+
margin-top: calc((var(--dile-color-picker-hue-track-height, 12px) - var(--dile-color-picker-hue-thumb-size, 18px)) / 2);
|
|
234
|
+
width: var(--dile-color-picker-hue-thumb-size, 18px);
|
|
235
|
+
height: var(--dile-color-picker-hue-thumb-size, 18px);
|
|
236
|
+
border-radius: 50%;
|
|
237
|
+
background-color: #fff;
|
|
238
|
+
border: var(--dile-color-picker-hue-thumb-border-width, 2px) solid var(--dile-color-picker-hue-thumb-border-color, #888);
|
|
239
|
+
box-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
|
|
240
|
+
}
|
|
241
|
+
.hue-slider::-moz-range-track {
|
|
242
|
+
height: var(--dile-color-picker-hue-track-height, 12px);
|
|
243
|
+
border-radius: var(--dile-color-picker-hue-track-border-radius, 6px);
|
|
244
|
+
background: linear-gradient(
|
|
245
|
+
to right,
|
|
246
|
+
hsl(0, 100%, 50%), hsl(60, 100%, 50%), hsl(120, 100%, 50%),
|
|
247
|
+
hsl(180, 100%, 50%), hsl(240, 100%, 50%), hsl(300, 100%, 50%), hsl(360, 100%, 50%)
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
.hue-slider::-moz-range-thumb {
|
|
251
|
+
width: var(--dile-color-picker-hue-thumb-size, 18px);
|
|
252
|
+
height: var(--dile-color-picker-hue-thumb-size, 18px);
|
|
253
|
+
border-radius: 50%;
|
|
254
|
+
background-color: #fff;
|
|
255
|
+
border: var(--dile-color-picker-hue-thumb-border-width, 2px) solid var(--dile-color-picker-hue-thumb-border-color, #888);
|
|
256
|
+
box-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
|
|
257
|
+
}
|
|
258
|
+
.hex-row {
|
|
259
|
+
display: flex;
|
|
260
|
+
align-items: center;
|
|
261
|
+
gap: 0.5rem;
|
|
262
|
+
margin-top: 0.75rem;
|
|
263
|
+
}
|
|
264
|
+
.hex-swatch {
|
|
265
|
+
display: block;
|
|
266
|
+
flex-shrink: 0;
|
|
267
|
+
width: var(--dile-color-picker-swatch-size, 2em);
|
|
268
|
+
height: var(--dile-color-picker-swatch-size, 2em);
|
|
269
|
+
border-radius: var(--dile-color-picker-swatch-border-radius, 5px);
|
|
270
|
+
border: var(--dile-color-picker-swatch-border-width, 1px) solid var(--dile-color-picker-swatch-border-color, #888);
|
|
271
|
+
}
|
|
272
|
+
.hex-input {
|
|
273
|
+
box-sizing: border-box;
|
|
274
|
+
flex-grow: 1;
|
|
275
|
+
min-width: 0;
|
|
276
|
+
border-radius: var(--dile-color-picker-hex-input-border-radius, 4px);
|
|
277
|
+
border: 1px solid var(--dile-color-picker-hex-input-border-color, #888);
|
|
278
|
+
padding: 5px;
|
|
279
|
+
font-size: var(--dile-color-picker-hex-input-font-size, 0.875rem);
|
|
280
|
+
background-color: var(--dile-color-picker-hex-input-background-color, #fff);
|
|
281
|
+
color: var(--dile-color-picker-hex-input-color, #303030);
|
|
282
|
+
}
|
|
283
|
+
.hex-input:focus {
|
|
284
|
+
outline: none;
|
|
285
|
+
border-color: var(--dile-input-focus-border-color, var(--dile-link-color, #6af));
|
|
286
|
+
}
|
|
287
|
+
.hex-input.errored {
|
|
288
|
+
border-color: var(--dile-color-picker-error-border-color, #c00);
|
|
289
|
+
}
|
|
290
|
+
#trigger-overlay {
|
|
291
|
+
display: flex;
|
|
292
|
+
}
|
|
293
|
+
`,
|
|
294
|
+
];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
render() {
|
|
298
|
+
return html`
|
|
299
|
+
<main>
|
|
300
|
+
${this.label ? html`<label id="cp-label">${this.label}</label>` : ''}
|
|
301
|
+
<div id="trigger-overlay">
|
|
302
|
+
<button
|
|
303
|
+
type="button"
|
|
304
|
+
id="trigger"
|
|
305
|
+
class="${this.errored ? 'errored' : ''}"
|
|
306
|
+
style="background-color: ${this.value}"
|
|
307
|
+
?disabled="${this.disabled}"
|
|
308
|
+
aria-haspopup="dialog"
|
|
309
|
+
aria-expanded="${this.opened}"
|
|
310
|
+
aria-label="${this.label || 'Color picker'}"
|
|
311
|
+
@click="${this.toggle}"
|
|
312
|
+
></button>
|
|
313
|
+
</div>
|
|
314
|
+
${this.messageTemplate}
|
|
315
|
+
${this.overlayTemplate}
|
|
316
|
+
</main>
|
|
317
|
+
`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
get overlayTemplate() {
|
|
321
|
+
return html`
|
|
322
|
+
<div
|
|
323
|
+
id="overlay"
|
|
324
|
+
class="${this._overlayClass}"
|
|
325
|
+
role="dialog"
|
|
326
|
+
aria-label="${this.label || 'Color picker'}"
|
|
327
|
+
@click="${this._onOverlayClick}"
|
|
328
|
+
>
|
|
329
|
+
<div
|
|
330
|
+
id="sv-square"
|
|
331
|
+
role="slider"
|
|
332
|
+
tabindex="${this.disabled ? -1 : 0}"
|
|
333
|
+
aria-valuemin="0"
|
|
334
|
+
aria-valuemax="100"
|
|
335
|
+
aria-valuenow="${this._value}"
|
|
336
|
+
aria-valuetext="Saturation ${this._saturation}%, brightness ${this._value}%"
|
|
337
|
+
aria-readonly="${this.readonly}"
|
|
338
|
+
style="background-color: hsl(${this._hue}, 100%, 50%)"
|
|
339
|
+
@pointerdown="${this._onSvPointerDown}"
|
|
340
|
+
@pointermove="${this._onSvPointerMove}"
|
|
341
|
+
@pointerup="${this._onSvPointerUp}"
|
|
342
|
+
@pointercancel="${this._onSvPointerUp}"
|
|
343
|
+
@keydown="${this._onSvKeydown}"
|
|
344
|
+
>
|
|
345
|
+
<div class="sv-thumb" style="left: ${this._saturation}%; top: ${100 - this._value}%"></div>
|
|
346
|
+
</div>
|
|
347
|
+
<input
|
|
348
|
+
type="range"
|
|
349
|
+
class="hue-slider"
|
|
350
|
+
min="0"
|
|
351
|
+
max="360"
|
|
352
|
+
step="1"
|
|
353
|
+
aria-label="Hue"
|
|
354
|
+
.value="${String(this._hue)}"
|
|
355
|
+
?disabled="${this.disabled}"
|
|
356
|
+
@input="${this._onHueInput}"
|
|
357
|
+
/>
|
|
358
|
+
<div class="hex-row">
|
|
359
|
+
<span class="hex-swatch" style="background-color: ${this.value}"></span>
|
|
360
|
+
<input
|
|
361
|
+
type="text"
|
|
362
|
+
class="hex-input ${this.errored ? 'errored' : ''}"
|
|
363
|
+
aria-label="Hex color"
|
|
364
|
+
maxlength="7"
|
|
365
|
+
spellcheck="false"
|
|
366
|
+
autocomplete="off"
|
|
367
|
+
?disabled="${this.disabled}"
|
|
368
|
+
?readonly="${this.readonly}"
|
|
369
|
+
.value="${this._hexInputValue}"
|
|
370
|
+
@input="${this._onHexInput}"
|
|
371
|
+
@blur="${this._onHexBlur}"
|
|
372
|
+
@keydown="${this._onHexKeydown}"
|
|
373
|
+
/>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
get messageTemplate() {
|
|
380
|
+
return html`
|
|
381
|
+
${this.message
|
|
382
|
+
? html`<div class="message ${this.errored ? 'errored-msg' : ''}"><span>${this.message}</span></div>`
|
|
383
|
+
: ''
|
|
384
|
+
}
|
|
385
|
+
`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
_onOverlayClick(e) {
|
|
389
|
+
e.stopPropagation();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
_commitHsv() {
|
|
393
|
+
const hex = hsvToHex(this._hue, this._saturation, this._value);
|
|
394
|
+
this._hexInputValue = hex;
|
|
395
|
+
if (this.value !== hex) {
|
|
396
|
+
this._skipValueSync = true;
|
|
397
|
+
this.value = hex;
|
|
398
|
+
}
|
|
399
|
+
if (this.hideErrorOnInput && this.errored) {
|
|
400
|
+
this.clearError();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
_onSvPointerDown(e) {
|
|
405
|
+
if (this.disabled || this.readonly) return;
|
|
406
|
+
e.preventDefault();
|
|
407
|
+
this._svDragging = true;
|
|
408
|
+
e.target.setPointerCapture(e.pointerId);
|
|
409
|
+
this._updateSvFromPointer(e);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
_onSvPointerMove(e) {
|
|
413
|
+
if (!this._svDragging) return;
|
|
414
|
+
this._updateSvFromPointer(e);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
_onSvPointerUp() {
|
|
418
|
+
this._svDragging = false;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
_updateSvFromPointer(e) {
|
|
422
|
+
const rect = this.svSquareEl.getBoundingClientRect();
|
|
423
|
+
const x = Math.min(Math.max(e.clientX - rect.left, 0), rect.width);
|
|
424
|
+
const y = Math.min(Math.max(e.clientY - rect.top, 0), rect.height);
|
|
425
|
+
this._saturation = rect.width === 0 ? 0 : Math.round((x / rect.width) * 100);
|
|
426
|
+
this._value = rect.height === 0 ? 100 : Math.round(100 - (y / rect.height) * 100);
|
|
427
|
+
this._commitHsv();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
_onSvKeydown(e) {
|
|
431
|
+
if (this.disabled || this.readonly) return;
|
|
432
|
+
const step = e.shiftKey ? 10 : 1;
|
|
433
|
+
const clamp = (n) => Math.max(0, Math.min(100, n));
|
|
434
|
+
let handled = true;
|
|
435
|
+
switch (e.key) {
|
|
436
|
+
case 'ArrowRight': this._saturation = clamp(this._saturation + step); break;
|
|
437
|
+
case 'ArrowLeft': this._saturation = clamp(this._saturation - step); break;
|
|
438
|
+
case 'ArrowUp': this._value = clamp(this._value + step); break;
|
|
439
|
+
case 'ArrowDown': this._value = clamp(this._value - step); break;
|
|
440
|
+
case 'Home': this._saturation = 0; break;
|
|
441
|
+
case 'End': this._saturation = 100; break;
|
|
442
|
+
case 'PageUp': this._value = clamp(this._value + 10); break;
|
|
443
|
+
case 'PageDown': this._value = clamp(this._value - 10); break;
|
|
444
|
+
default: handled = false;
|
|
445
|
+
}
|
|
446
|
+
if (handled) {
|
|
447
|
+
e.preventDefault();
|
|
448
|
+
this._commitHsv();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
_onHueInput(e) {
|
|
453
|
+
if (this.disabled || this.readonly) {
|
|
454
|
+
e.target.value = this._hue;
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
this._hue = Number(e.target.value);
|
|
458
|
+
this._commitHsv();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
_onHexInput(e) {
|
|
462
|
+
this._hexInputValue = e.target.value;
|
|
463
|
+
if (this.disabled || this.readonly) return;
|
|
464
|
+
const normalized = normalizeHex(this._hexInputValue);
|
|
465
|
+
if (!normalized) return;
|
|
466
|
+
const { h, s, v } = hexToHsv(normalized);
|
|
467
|
+
this._hue = h;
|
|
468
|
+
this._saturation = s;
|
|
469
|
+
this._value = v;
|
|
470
|
+
this._skipValueSync = true;
|
|
471
|
+
this.value = normalized;
|
|
472
|
+
if (this.hideErrorOnInput && this.errored) {
|
|
473
|
+
this.clearError();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
_onHexBlur() {
|
|
478
|
+
this._hexInputValue = this.value;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
_onHexKeydown(e) {
|
|
482
|
+
if (e.key === 'Enter') {
|
|
483
|
+
e.preventDefault();
|
|
484
|
+
this._onHexBlur();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
clearError() {
|
|
489
|
+
this.errored = false;
|
|
490
|
+
this.message = '';
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
focus() {
|
|
494
|
+
this.trigger.focus();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { LitElement, html, css } from 'lit';
|
|
2
2
|
import '../file-preview.js';
|
|
3
3
|
import '../../button/button.js';
|
|
4
|
+
import { labelStyles } from '../../input/src/label-styles.js';
|
|
4
5
|
import { messageStyles } from '../../input/src/message-styles.js';
|
|
5
6
|
import { DileEmmitChange } from '../../../mixins/form/index.js';
|
|
6
7
|
|
|
7
8
|
export class DileDropFile extends DileEmmitChange(LitElement) {
|
|
8
9
|
static styles = [
|
|
10
|
+
labelStyles,
|
|
9
11
|
messageStyles,
|
|
10
12
|
css`
|
|
11
13
|
* { box-sizing: border-box; }
|
|
@@ -41,13 +43,6 @@ export class DileDropFile extends DileEmmitChange(LitElement) {
|
|
|
41
43
|
display: none;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
label {
|
|
45
|
-
display: block;
|
|
46
|
-
margin-bottom: var(--dile-input-label-margin-bottom, 4px);
|
|
47
|
-
font-size: var(--dile-input-label-font-size, 1em);
|
|
48
|
-
color: var(--dile-input-label-color, #59e);
|
|
49
|
-
font-weight: var(--dile-input-label-font-weight, normal);
|
|
50
|
-
}
|
|
51
46
|
main {
|
|
52
47
|
width: var(--dile-input-section-width, 100%);
|
|
53
48
|
}
|
|
@@ -5,5 +5,6 @@ export { DileInputMoney } from './src/DileInputMoney.js';
|
|
|
5
5
|
export { DileInputNumberMask } from './src/DileInputNumberMask.js';
|
|
6
6
|
export { DileInputPercentage } from './src/DileInputPercentage.js';
|
|
7
7
|
export { DileInputSearch } from './src/DileInputSearch.js';
|
|
8
|
+
export { labelStyles } from './src/label-styles.js';
|
|
8
9
|
export { messageStyles } from './src/message-styles.js';
|
|
9
10
|
export { Mask } from './src/Mask.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LitElement, html, css } from 'lit';
|
|
2
2
|
import { DileEmmitChange } from '../../../mixins/form/index.js';
|
|
3
|
+
import { labelStyles } from './label-styles.js';
|
|
3
4
|
import { messageStyles } from './message-styles.js';
|
|
4
5
|
|
|
5
6
|
export class DileInput extends DileEmmitChange(LitElement) {
|
|
@@ -76,7 +77,7 @@ export class DileInput extends DileEmmitChange(LitElement) {
|
|
|
76
77
|
this.disableAutocomplete = false;
|
|
77
78
|
this.name = '';
|
|
78
79
|
this.type = 'text';
|
|
79
|
-
this.types = ['text', 'password', 'email', 'number', 'tel', 'url', 'search', 'date', 'time', 'datetime', 'datetime-local', 'month', 'week'];
|
|
80
|
+
this.types = ['text', 'password', 'email', 'number', 'tel', 'url', 'search', 'date', 'time', 'datetime', 'datetime-local', 'month', 'week'];
|
|
80
81
|
this.internals = this.attachInternals();
|
|
81
82
|
}
|
|
82
83
|
|
|
@@ -95,6 +96,7 @@ export class DileInput extends DileEmmitChange(LitElement) {
|
|
|
95
96
|
|
|
96
97
|
static get styles() {
|
|
97
98
|
return [
|
|
99
|
+
labelStyles,
|
|
98
100
|
messageStyles,
|
|
99
101
|
css`
|
|
100
102
|
* {
|
|
@@ -110,13 +112,6 @@ export class DileInput extends DileEmmitChange(LitElement) {
|
|
|
110
112
|
main {
|
|
111
113
|
width: var(--dile-input-section-width, 100%);
|
|
112
114
|
}
|
|
113
|
-
label {
|
|
114
|
-
display: block;
|
|
115
|
-
margin-bottom: var(--dile-input-label-margin-bottom, 4px);
|
|
116
|
-
font-size: var(--dile-input-label-font-size, 1em);
|
|
117
|
-
color: var(--dile-input-label-color, #59e);
|
|
118
|
-
font-weight: var(--dile-input-label-font-weight, normal);
|
|
119
|
-
}
|
|
120
115
|
input {
|
|
121
116
|
box-sizing: border-box;
|
|
122
117
|
border-radius: var(--dile-input-border-radius, 5px);
|
|
@@ -131,8 +126,9 @@ export class DileInput extends DileEmmitChange(LitElement) {
|
|
|
131
126
|
flex-grow: 1;
|
|
132
127
|
}
|
|
133
128
|
input:focus {
|
|
134
|
-
outline:
|
|
135
|
-
|
|
129
|
+
outline: var(--dile-input-focus-ring-width, 3px) solid var(--dile-input-focus-ring-color, rgba(102, 170, 255, 0.5));
|
|
130
|
+
outline-offset: calc(-1 * var(--dile-input-focus-ring-width, 3px));
|
|
131
|
+
border-color: var(--dile-input-focus-border-color, var(--dile-link-color, #6af));
|
|
136
132
|
}
|
|
137
133
|
input::placeholder {
|
|
138
134
|
color: var(--dile-input-placeholder-color, #ccc);
|
|
@@ -152,13 +148,13 @@ export class DileInput extends DileEmmitChange(LitElement) {
|
|
|
152
148
|
}
|
|
153
149
|
.labelright {
|
|
154
150
|
margin-left: var(--dile-input-label-right-margin-left, 10px);
|
|
155
|
-
color: var(--dile-input-label-color, #59e);
|
|
151
|
+
color: var(--dile-input-label-color, var(--dile-on-background-color, #59e));
|
|
156
152
|
font-size: var(--dile-input-label-right-font-size, 1.2em);
|
|
157
153
|
}
|
|
158
154
|
`
|
|
159
155
|
];
|
|
160
156
|
}
|
|
161
|
-
|
|
157
|
+
|
|
162
158
|
render() {
|
|
163
159
|
return html`
|
|
164
160
|
<main>
|
|
@@ -180,8 +176,8 @@ export class DileInput extends DileEmmitChange(LitElement) {
|
|
|
180
176
|
@input="${this._input}"
|
|
181
177
|
@blur="${this.doBlur}"
|
|
182
178
|
@focus="${this.doFocus}"
|
|
183
|
-
/>
|
|
184
|
-
${this.labelRight
|
|
179
|
+
/>
|
|
180
|
+
${this.labelRight
|
|
185
181
|
? html`<span class="labelright">${this.labelRight}</span>`
|
|
186
182
|
: ''
|
|
187
183
|
}
|
|
@@ -193,7 +189,7 @@ export class DileInput extends DileEmmitChange(LitElement) {
|
|
|
193
189
|
|
|
194
190
|
get messageTemplate() {
|
|
195
191
|
return html`
|
|
196
|
-
${this.message
|
|
192
|
+
${this.message
|
|
197
193
|
? html`<div class="message ${this.errored ? 'errored-msg' : ''}"><span>${this.message}</span></div>`
|
|
198
194
|
: ''
|
|
199
195
|
}
|
|
@@ -212,10 +208,10 @@ export class DileInput extends DileEmmitChange(LitElement) {
|
|
|
212
208
|
_input(e) {
|
|
213
209
|
this.value = e.target.value;
|
|
214
210
|
if (this.hideErrorOnInput && this.errored) {
|
|
215
|
-
this.clearError();
|
|
211
|
+
this.clearError();
|
|
216
212
|
}
|
|
217
213
|
}
|
|
218
|
-
|
|
214
|
+
|
|
219
215
|
clearError() {
|
|
220
216
|
this.errored = false;
|
|
221
217
|
this.message = '';
|
|
@@ -249,4 +245,4 @@ export class DileInput extends DileEmmitChange(LitElement) {
|
|
|
249
245
|
focus() {
|
|
250
246
|
this.el.focus();
|
|
251
247
|
}
|
|
252
|
-
}
|
|
248
|
+
}
|
|
@@ -18,7 +18,7 @@ export class DileInputSearch extends LitElement {
|
|
|
18
18
|
background-color: var(--dile-input-background-color, #fff);
|
|
19
19
|
}
|
|
20
20
|
div:focus-within {
|
|
21
|
-
border-color: var(--dile-input-focus-border-color, #6af);
|
|
21
|
+
border-color: var(--dile-input-focus-border-color, var(--dile-link-color, #6af));
|
|
22
22
|
}
|
|
23
23
|
input {
|
|
24
24
|
flex-grow: 1;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
|
|
3
|
+
export const labelStyles = css`
|
|
4
|
+
label, .label {
|
|
5
|
+
display: block;
|
|
6
|
+
margin-bottom: var(--dile-input-label-margin-bottom, 4px);
|
|
7
|
+
font-size: var(--dile-input-label-font-size, 1em);
|
|
8
|
+
color: var(--dile-input-label-color, var(--dile-on-background-color, #59e));
|
|
9
|
+
font-weight: var(--dile-input-label-font-weight, normal);
|
|
10
|
+
}
|
|
11
|
+
`;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { LitElement, html, css } from 'lit';
|
|
2
2
|
import { DileEmmitChange } from '../../../mixins/form/index.js';
|
|
3
3
|
import '../number-picker-element.js';
|
|
4
|
+
import { labelStyles } from '../../input/src/label-styles.js';
|
|
4
5
|
import { messageStyles } from '../../input/src/message-styles.js';
|
|
5
6
|
|
|
6
7
|
export class DileNumberPicker extends DileEmmitChange(LitElement) {
|
|
7
8
|
static styles = [
|
|
9
|
+
labelStyles,
|
|
8
10
|
messageStyles,
|
|
9
11
|
css`
|
|
10
12
|
:host {
|
|
@@ -14,13 +16,6 @@ export class DileNumberPicker extends DileEmmitChange(LitElement) {
|
|
|
14
16
|
* {
|
|
15
17
|
box-sizing: border-box;
|
|
16
18
|
}
|
|
17
|
-
label {
|
|
18
|
-
display: block;
|
|
19
|
-
margin-bottom: var(--dile-input-label-margin-bottom, 4px);
|
|
20
|
-
font-size: var(--dile-input-label-font-size, 1em);
|
|
21
|
-
color: var(--dile-input-label-color, #59e);
|
|
22
|
-
font-weight: var(--dile-input-label-font-weight, normal);
|
|
23
|
-
}
|
|
24
19
|
`
|
|
25
20
|
];
|
|
26
21
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LitElement, html, css } from 'lit';
|
|
2
2
|
import { DileEmmitChange } from '../../../mixins/form/index.js';
|
|
3
|
-
import { messageStyles } from '../../input/index.js';
|
|
3
|
+
import { labelStyles, messageStyles } from '../../input/index.js';
|
|
4
4
|
import { sanitizeOtpValue } from '../../../lib/otp/sanitizeOtpValue.js';
|
|
5
5
|
|
|
6
6
|
export class DileOtpInput extends DileEmmitChange(LitElement) {
|
|
@@ -106,6 +106,7 @@ export class DileOtpInput extends DileEmmitChange(LitElement) {
|
|
|
106
106
|
|
|
107
107
|
static get styles() {
|
|
108
108
|
return [
|
|
109
|
+
labelStyles,
|
|
109
110
|
messageStyles,
|
|
110
111
|
css`
|
|
111
112
|
* {
|
|
@@ -118,13 +119,6 @@ export class DileOtpInput extends DileEmmitChange(LitElement) {
|
|
|
118
119
|
main {
|
|
119
120
|
width: var(--dile-otp-input-section-width, auto);
|
|
120
121
|
}
|
|
121
|
-
label {
|
|
122
|
-
display: block;
|
|
123
|
-
margin-bottom: var(--dile-otp-input-label-margin-bottom, 4px);
|
|
124
|
-
font-size: var(--dile-otp-input-label-font-size, 1em);
|
|
125
|
-
color: var(--dile-otp-input-label-color, #59e);
|
|
126
|
-
font-weight: var(--dile-otp-input-label-font-weight, normal);
|
|
127
|
-
}
|
|
128
122
|
section.for-input {
|
|
129
123
|
display: flex;
|
|
130
124
|
gap: var(--dile-otp-input-gap, 8px);
|
|
@@ -143,7 +137,7 @@ export class DileOtpInput extends DileEmmitChange(LitElement) {
|
|
|
143
137
|
}
|
|
144
138
|
input:focus {
|
|
145
139
|
outline: none;
|
|
146
|
-
border-color: var(--dile-otp-input-focus-border-color, #6af);
|
|
140
|
+
border-color: var(--dile-otp-input-focus-border-color, var(--dile-link-color, #6af));
|
|
147
141
|
}
|
|
148
142
|
input:disabled {
|
|
149
143
|
background-color: #f5f5f5;
|
|
@@ -2,21 +2,16 @@ import { LitElement, html, css } from 'lit';
|
|
|
2
2
|
import { DileEmmitChange } from '../../../mixins/form/index.js';
|
|
3
3
|
import '../radio.js';
|
|
4
4
|
import '../../input/input-message.js';
|
|
5
|
+
import { labelStyles } from '../../input/index.js';
|
|
5
6
|
|
|
6
7
|
export class DileRadioGroup extends DileEmmitChange(LitElement) {
|
|
7
8
|
static styles = [
|
|
9
|
+
labelStyles,
|
|
8
10
|
css`
|
|
9
11
|
:host {
|
|
10
12
|
display: block;
|
|
11
13
|
margin-bottom: 10px;
|
|
12
14
|
}
|
|
13
|
-
.label {
|
|
14
|
-
display: block;
|
|
15
|
-
margin-bottom: var(--dile-input-label-margin-bottom, 4px);
|
|
16
|
-
font-size: var(--dile-input-label-font-size, 1em);
|
|
17
|
-
color: var(--dile-input-label-color, #59e);
|
|
18
|
-
font-weight: var(--dile-input-label-font-weight, normal);
|
|
19
|
-
}
|
|
20
15
|
:host([disabled]) {
|
|
21
16
|
--dile-radio-icon-color: var(--dile-radio-disabled-icon-color, #ccc);
|
|
22
17
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { LitElement, html, css } from 'lit';
|
|
2
2
|
import { DileEmmitChange } from '../../../mixins/form/index.js';
|
|
3
3
|
import '../../input/input-message.js';
|
|
4
|
+
import { labelStyles } from '../../input/index.js';
|
|
4
5
|
|
|
5
6
|
export class DileRange extends DileEmmitChange(LitElement) {
|
|
6
7
|
|
|
7
8
|
static get styles() {
|
|
8
|
-
return
|
|
9
|
+
return [
|
|
10
|
+
labelStyles,
|
|
11
|
+
css`
|
|
9
12
|
:host {
|
|
10
13
|
display: block;
|
|
11
14
|
margin-bottom: 10px;
|
|
@@ -15,14 +18,6 @@ export class DileRange extends DileEmmitChange(LitElement) {
|
|
|
15
18
|
--dile-range-line-color: var(--dile-range-line-errored-color, #c00);
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
label {
|
|
19
|
-
display: block;
|
|
20
|
-
margin-bottom: var(--dile-input-label-margin-bottom, 4px);
|
|
21
|
-
font-size: var(--dile-input-label-font-size, 1em);
|
|
22
|
-
color: var(--dile-input-label-color, #59e);
|
|
23
|
-
font-weight: var(--dile-input-label-font-weight, normal);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
21
|
/*********** Baseline, reset styles ***********/
|
|
27
22
|
input[type="range"] {
|
|
28
23
|
-webkit-appearance: none;
|
|
@@ -87,7 +82,8 @@ export class DileRange extends DileEmmitChange(LitElement) {
|
|
|
87
82
|
}
|
|
88
83
|
|
|
89
84
|
|
|
90
|
-
|
|
85
|
+
`
|
|
86
|
+
];
|
|
91
87
|
}
|
|
92
88
|
|
|
93
89
|
static get formAssociated() {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { html, css, LitElement } from "lit";
|
|
2
2
|
import { DileEmmitChange } from '../../../mixins/form/index.js';
|
|
3
|
-
import { messageStyles } from '../../input/index.js';
|
|
3
|
+
import { labelStyles, messageStyles } from '../../input/index.js';
|
|
4
4
|
|
|
5
5
|
export class DileSelect extends DileEmmitChange(LitElement) {
|
|
6
6
|
static get styles() {
|
|
7
7
|
return [
|
|
8
|
+
labelStyles,
|
|
8
9
|
messageStyles,
|
|
9
10
|
css`
|
|
10
11
|
:host {
|
|
@@ -14,13 +15,6 @@ export class DileSelect extends DileEmmitChange(LitElement) {
|
|
|
14
15
|
* {
|
|
15
16
|
box-sizing: border-box;
|
|
16
17
|
}
|
|
17
|
-
label {
|
|
18
|
-
display: block;
|
|
19
|
-
margin-bottom: var(--dile-input-label-margin-bottom, 4px);
|
|
20
|
-
font-size: var(--dile-input-label-font-size, 1em);
|
|
21
|
-
color: var(--dile-input-label-color, #59e);
|
|
22
|
-
font-weight: var(--dile-input-label-font-weight, normal);
|
|
23
|
-
}
|
|
24
18
|
::slotted(select) {
|
|
25
19
|
box-sizing: border-box;
|
|
26
20
|
width: var(--dile-input-width, 100%);
|
|
@@ -42,9 +36,10 @@ export class DileSelect extends DileEmmitChange(LitElement) {
|
|
|
42
36
|
::slotted(select.dark) {
|
|
43
37
|
background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20256%20448%22%20enable-background%3D%22new%200%200%20256%20448%22%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E.arrow%7Bfill%3A%23ffffff%3B%7D%3C%2Fstyle%3E%3Cpath%20class%3D%22arrow%22%20d%3D%22M255.9%20168c0-4.2-1.6-7.9-4.8-11.2-3.2-3.2-6.9-4.8-11.2-4.8H16c-4.2%200-7.9%201.6-11.2%204.8S0%20163.8%200%20168c0%204.4%201.6%208.2%204.8%2011.4l112%20112c3.1%203.1%206.8%204.6%2011.2%204.6%204.4%200%208.2-1.5%2011.4-4.6l112-112c3-3.2%204.5-7%204.5-11.4z%22%2F%3E%3C%2Fsvg%3E%0A");
|
|
44
38
|
}
|
|
45
|
-
::slotted(select)
|
|
46
|
-
outline:
|
|
47
|
-
|
|
39
|
+
:host(:focus-within) ::slotted(select) {
|
|
40
|
+
outline: var(--dile-input-focus-ring-width, 3px) solid var(--dile-input-focus-ring-color, rgba(102, 170, 255, 0.5));
|
|
41
|
+
outline-offset: calc(-1 * var(--dile-input-focus-ring-width, 3px));
|
|
42
|
+
border-color: var(--dile-input-focus-border-color, var(--dile-link-color, #6af));
|
|
48
43
|
}
|
|
49
44
|
::slotted(select.errored) {
|
|
50
45
|
border-color: var(--dile-input-error-border-color, #c00);
|
|
@@ -3,6 +3,7 @@ import '../../input/input-search.js';
|
|
|
3
3
|
import '../../spinner/spinner-horizontal.js';
|
|
4
4
|
import { DileEmmitChange } from '../../../mixins/form/index.js';
|
|
5
5
|
import '../select.js';
|
|
6
|
+
import { labelStyles } from '../../input/src/label-styles.js';
|
|
6
7
|
import { messageStyles } from '../../input/src/message-styles.js';
|
|
7
8
|
|
|
8
9
|
|
|
@@ -10,6 +11,7 @@ export class DileSelectAjax extends DileEmmitChange(LitElement) {
|
|
|
10
11
|
|
|
11
12
|
static get styles() {
|
|
12
13
|
return [
|
|
14
|
+
labelStyles,
|
|
13
15
|
messageStyles,
|
|
14
16
|
css`
|
|
15
17
|
* {
|
|
@@ -23,13 +25,6 @@ export class DileSelectAjax extends DileEmmitChange(LitElement) {
|
|
|
23
25
|
#result {
|
|
24
26
|
margin-bottom: 0.5rem;
|
|
25
27
|
}
|
|
26
|
-
label {
|
|
27
|
-
display: block;
|
|
28
|
-
margin-bottom: var(--dile-input-label-margin-bottom, 4px);
|
|
29
|
-
font-size: var(--dile-input-label-font-size, 1em);
|
|
30
|
-
color: var(--dile-input-label-color, #59e);
|
|
31
|
-
font-weight: var(--dile-input-label-font-weight, normal);
|
|
32
|
-
}
|
|
33
28
|
.anchor {
|
|
34
29
|
margin-top: 0.5rem;
|
|
35
30
|
position: relative;
|
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
import { LitElement, html, css } from 'lit';
|
|
2
|
-
import { messageStyles } from '../../input/index.js';
|
|
2
|
+
import { labelStyles, messageStyles } from '../../input/index.js';
|
|
3
3
|
import { DileEmmitChange } from '../../../mixins/form/index.js';
|
|
4
4
|
|
|
5
5
|
export class DileTextarea extends DileEmmitChange(LitElement) {
|
|
6
6
|
|
|
7
7
|
static get styles() {
|
|
8
8
|
return [
|
|
9
|
-
|
|
9
|
+
labelStyles,
|
|
10
|
+
messageStyles,
|
|
10
11
|
css`
|
|
11
12
|
:host {
|
|
12
13
|
display: block;
|
|
13
14
|
margin-bottom: 10px;
|
|
14
15
|
}
|
|
15
|
-
label {
|
|
16
|
-
margin-bottom: var(--dile-textarea-label-margin-bottom, 4px);
|
|
17
|
-
color: var(--dile-textarea-label-color, #59e);
|
|
18
|
-
font-size: var(--dile-textarea-label-font-size, 1em);
|
|
19
|
-
font-weight: var(--dile-textarea-label-font-weight, normal);
|
|
20
|
-
}
|
|
21
16
|
|
|
22
17
|
textarea {
|
|
23
18
|
border: var(--dile-textarea-border, 1px solid #888);
|
|
@@ -32,6 +27,12 @@ export class DileTextarea extends DileEmmitChange(LitElement) {
|
|
|
32
27
|
line-height: 1.5rem;
|
|
33
28
|
}
|
|
34
29
|
|
|
30
|
+
textarea:focus {
|
|
31
|
+
outline: var(--dile-input-focus-ring-width, 3px) solid var(--dile-input-focus-ring-color, rgba(102, 170, 255, 0.5));
|
|
32
|
+
outline-offset: calc(-1 * var(--dile-input-focus-ring-width, 3px));
|
|
33
|
+
border-color: var(--dile-input-focus-border-color, var(--dile-link-color, #6af));
|
|
34
|
+
}
|
|
35
|
+
|
|
35
36
|
textarea::placeholder {
|
|
36
37
|
color: var(--dile-textarea-placeholder-color, #999);
|
|
37
38
|
}
|
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
import { LitElement, html, css } from 'lit';
|
|
2
2
|
import { DileEmmitChange } from '../../../mixins/form/index.js';
|
|
3
3
|
import '../../number-picker/number-picker-element.js';
|
|
4
|
+
import { labelStyles } from '../../input/src/label-styles.js';
|
|
4
5
|
import { messageStyles } from '../../input/src/message-styles.js';
|
|
5
6
|
|
|
6
7
|
export class DileTimePicker extends DileEmmitChange(LitElement) {
|
|
7
8
|
static styles = [
|
|
9
|
+
labelStyles,
|
|
8
10
|
messageStyles,
|
|
9
11
|
css`
|
|
10
12
|
:host {
|
|
11
13
|
display: block;
|
|
12
14
|
margin-bottom: 10px;
|
|
13
15
|
}
|
|
14
|
-
label {
|
|
15
|
-
display: block;
|
|
16
|
-
margin-bottom: var(--dile-input-label-margin-bottom, 4px);
|
|
17
|
-
font-size: var(--dile-input-label-font-size, 1em);
|
|
18
|
-
color: var(--dile-input-label-color, #59e);
|
|
19
|
-
font-weight: var(--dile-input-label-font-weight, normal);
|
|
20
|
-
}
|
|
21
16
|
section {
|
|
22
17
|
display: flex;
|
|
23
18
|
gap: 0.5rem;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export function normalizeHex(input) {
|
|
2
|
+
if (typeof input !== 'string') return null;
|
|
3
|
+
const match = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.exec(input.trim());
|
|
4
|
+
if (!match) return null;
|
|
5
|
+
let digits = match[1];
|
|
6
|
+
if (digits.length === 3) {
|
|
7
|
+
digits = digits.split('').map((c) => c + c).join('');
|
|
8
|
+
}
|
|
9
|
+
return `#${digits.toLowerCase()}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function hexToRgb(hex) {
|
|
13
|
+
const normalized = normalizeHex(hex);
|
|
14
|
+
if (!normalized) return null;
|
|
15
|
+
const int = parseInt(normalized.slice(1), 16);
|
|
16
|
+
return { r: (int >> 16) & 255, g: (int >> 8) & 255, b: int & 255 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function rgbToHex({ r, g, b }) {
|
|
20
|
+
const clamp = (n) => Math.max(0, Math.min(255, Math.round(n)));
|
|
21
|
+
const toHex = (n) => clamp(n).toString(16).padStart(2, '0');
|
|
22
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function rgbToHsv({ r, g, b }) {
|
|
26
|
+
const rN = r / 255;
|
|
27
|
+
const gN = g / 255;
|
|
28
|
+
const bN = b / 255;
|
|
29
|
+
const max = Math.max(rN, gN, bN);
|
|
30
|
+
const min = Math.min(rN, gN, bN);
|
|
31
|
+
const delta = max - min;
|
|
32
|
+
|
|
33
|
+
let h = 0;
|
|
34
|
+
if (delta !== 0) {
|
|
35
|
+
if (max === rN) {
|
|
36
|
+
h = 60 * (((gN - bN) / delta) % 6);
|
|
37
|
+
} else if (max === gN) {
|
|
38
|
+
h = 60 * ((bN - rN) / delta + 2);
|
|
39
|
+
} else {
|
|
40
|
+
h = 60 * ((rN - gN) / delta + 4);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (h < 0) h += 360;
|
|
44
|
+
|
|
45
|
+
const s = max === 0 ? 0 : delta / max;
|
|
46
|
+
return { h: Math.round(h), s: Math.round(s * 100), v: Math.round(max * 100) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function hsvToRgb({ h, s, v }) {
|
|
50
|
+
const hN = ((h % 360) + 360) % 360;
|
|
51
|
+
const sN = Math.max(0, Math.min(100, s)) / 100;
|
|
52
|
+
const vN = Math.max(0, Math.min(100, v)) / 100;
|
|
53
|
+
|
|
54
|
+
const c = vN * sN;
|
|
55
|
+
const x = c * (1 - Math.abs(((hN / 60) % 2) - 1));
|
|
56
|
+
const m = vN - c;
|
|
57
|
+
|
|
58
|
+
let r1, g1, b1;
|
|
59
|
+
if (hN < 60) {
|
|
60
|
+
[r1, g1, b1] = [c, x, 0];
|
|
61
|
+
} else if (hN < 120) {
|
|
62
|
+
[r1, g1, b1] = [x, c, 0];
|
|
63
|
+
} else if (hN < 180) {
|
|
64
|
+
[r1, g1, b1] = [0, c, x];
|
|
65
|
+
} else if (hN < 240) {
|
|
66
|
+
[r1, g1, b1] = [0, x, c];
|
|
67
|
+
} else if (hN < 300) {
|
|
68
|
+
[r1, g1, b1] = [x, 0, c];
|
|
69
|
+
} else {
|
|
70
|
+
[r1, g1, b1] = [c, 0, x];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
r: Math.round((r1 + m) * 255),
|
|
75
|
+
g: Math.round((g1 + m) * 255),
|
|
76
|
+
b: Math.round((b1 + m) * 255),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function hexToHsv(hex) {
|
|
81
|
+
const rgb = hexToRgb(hex);
|
|
82
|
+
return rgb ? rgbToHsv(rgb) : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function hsvToHex(h, s, v) {
|
|
86
|
+
return rgbToHex(hsvToRgb({ h, s, v }));
|
|
87
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
normalizeHex,
|
|
5
|
+
hexToRgb,
|
|
6
|
+
rgbToHex,
|
|
7
|
+
rgbToHsv,
|
|
8
|
+
hsvToRgb,
|
|
9
|
+
hexToHsv,
|
|
10
|
+
hsvToHex,
|
|
11
|
+
} from './colorConversion.js';
|
|
12
|
+
|
|
13
|
+
test('normalizeHex accepts 6-digit hex with or without the hash', () => {
|
|
14
|
+
assert.equal(normalizeHex('#ff0000'), '#ff0000');
|
|
15
|
+
assert.equal(normalizeHex('ff0000'), '#ff0000');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('normalizeHex lowercases uppercase input', () => {
|
|
19
|
+
assert.equal(normalizeHex('#FF00AA'), '#ff00aa');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('normalizeHex expands 3-digit shorthand', () => {
|
|
23
|
+
assert.equal(normalizeHex('#abc'), '#aabbcc');
|
|
24
|
+
assert.equal(normalizeHex('abc'), '#aabbcc');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('normalizeHex rejects invalid input', () => {
|
|
28
|
+
assert.equal(normalizeHex('red'), null);
|
|
29
|
+
assert.equal(normalizeHex('#ff'), null);
|
|
30
|
+
assert.equal(normalizeHex('#gggggg'), null);
|
|
31
|
+
assert.equal(normalizeHex(''), null);
|
|
32
|
+
assert.equal(normalizeHex(null), null);
|
|
33
|
+
assert.equal(normalizeHex(undefined), null);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('hexToRgb / rgbToHex round trip on reference colors', () => {
|
|
37
|
+
assert.deepEqual(hexToRgb('#ff0000'), { r: 255, g: 0, b: 0 });
|
|
38
|
+
assert.deepEqual(hexToRgb('#00ff00'), { r: 0, g: 255, b: 0 });
|
|
39
|
+
assert.deepEqual(hexToRgb('#0000ff'), { r: 0, g: 0, b: 255 });
|
|
40
|
+
assert.equal(rgbToHex({ r: 255, g: 0, b: 0 }), '#ff0000');
|
|
41
|
+
assert.equal(rgbToHex({ r: 0, g: 255, b: 0 }), '#00ff00');
|
|
42
|
+
assert.equal(rgbToHex({ r: 0, g: 0, b: 255 }), '#0000ff');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('rgbToHex clamps out-of-range channels', () => {
|
|
46
|
+
assert.equal(rgbToHex({ r: 300, g: -10, b: 128 }), '#ff0080');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('hexToRgb returns null for invalid hex', () => {
|
|
50
|
+
assert.equal(hexToRgb('not-a-color'), null);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('rgbToHsv matches known reference hues', () => {
|
|
54
|
+
assert.deepEqual(rgbToHsv({ r: 255, g: 0, b: 0 }), { h: 0, s: 100, v: 100 });
|
|
55
|
+
assert.deepEqual(rgbToHsv({ r: 0, g: 255, b: 0 }), { h: 120, s: 100, v: 100 });
|
|
56
|
+
assert.deepEqual(rgbToHsv({ r: 0, g: 0, b: 255 }), { h: 240, s: 100, v: 100 });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('rgbToHsv reports saturation 0 for grays, including white and black', () => {
|
|
60
|
+
assert.deepEqual(rgbToHsv({ r: 255, g: 255, b: 255 }), { h: 0, s: 0, v: 100 });
|
|
61
|
+
assert.deepEqual(rgbToHsv({ r: 0, g: 0, b: 0 }), { h: 0, s: 0, v: 0 });
|
|
62
|
+
assert.deepEqual(rgbToHsv({ r: 128, g: 128, b: 128 }), { h: 0, s: 0, v: 50 });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('hsvToRgb inverts rgbToHsv for reference colors', () => {
|
|
66
|
+
assert.deepEqual(hsvToRgb({ h: 0, s: 100, v: 100 }), { r: 255, g: 0, b: 0 });
|
|
67
|
+
assert.deepEqual(hsvToRgb({ h: 120, s: 100, v: 100 }), { r: 0, g: 255, b: 0 });
|
|
68
|
+
assert.deepEqual(hsvToRgb({ h: 240, s: 100, v: 100 }), { r: 0, g: 0, b: 255 });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('hexToHsv / hsvToHex round trip for arbitrary colors stays within rounding tolerance', () => {
|
|
72
|
+
for (const hex of ['#7bb93d', '#cc0000', '#336699', '#ffffff', '#000000', '#808080']) {
|
|
73
|
+
const { h, s, v } = hexToHsv(hex);
|
|
74
|
+
const original = hexToRgb(hex);
|
|
75
|
+
const roundTripped = hexToRgb(hsvToHex(h, s, v));
|
|
76
|
+
for (const channel of ['r', 'g', 'b']) {
|
|
77
|
+
assert.ok(
|
|
78
|
+
Math.abs(original[channel] - roundTripped[channel]) <= 2,
|
|
79
|
+
`${hex} channel ${channel} drifted too far after HSV round trip: ${original[channel]} vs ${roundTripped[channel]}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('hexToHsv / hsvToHex round trip is exact for colors immune to integer HSV rounding', () => {
|
|
86
|
+
for (const hex of ['#ff0000', '#00ff00', '#0000ff', '#ffffff', '#000000', '#808080']) {
|
|
87
|
+
const { h, s, v } = hexToHsv(hex);
|
|
88
|
+
assert.equal(hsvToHex(h, s, v), hex);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('hexToHsv returns null for invalid hex', () => {
|
|
93
|
+
assert.equal(hexToHsv('nope'), null);
|
|
94
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dile/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "UI Core components from dile-components.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -27,5 +27,5 @@
|
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|
|
29
29
|
},
|
|
30
|
-
"gitHead": "
|
|
30
|
+
"gitHead": "8dd04c6978c60eb0a90891ba7a128f4153348c61"
|
|
31
31
|
}
|