color-elements 0.0.1 → 0.0.2

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 (47) hide show
  1. package/README.md +33 -11
  2. package/_build/copy-config.js +11 -1
  3. package/_build/copy-config.json +1 -1
  4. package/_build/eleventy.js +21 -0
  5. package/_build/filters-extra.js +3 -0
  6. package/_data/components.json +8 -0
  7. package/_includes/component.njk +61 -0
  8. package/_includes/partials/_nav-links.njk +12 -0
  9. package/_redirects +1 -1
  10. package/assets/js/index.js +8 -7
  11. package/eslint.config.js +316 -0
  12. package/index.js +6 -4
  13. package/logo.svg +22 -44
  14. package/package.json +11 -5
  15. package/src/channel-slider/README.md +123 -0
  16. package/src/channel-slider/channel-slider.css +19 -0
  17. package/src/channel-slider/channel-slider.js +252 -0
  18. package/{color-gamut → src/color-gamut}/README.md +1 -1
  19. package/{color-gamut → src/color-gamut}/color-gamut.js +1 -1
  20. package/src/color-inline/README.md +31 -0
  21. package/{color-swatch/color-swatch.js → src/color-inline/color-inline.js} +6 -6
  22. package/src/color-inline/style.css +14 -0
  23. package/src/color-picker/README.md +48 -0
  24. package/src/color-picker/color-picker.css +20 -0
  25. package/src/color-picker/color-picker.js +164 -0
  26. package/src/color-slider/README.md +193 -0
  27. package/src/color-slider/color-slider.css +164 -0
  28. package/src/color-slider/color-slider.js +278 -0
  29. package/src/color-swatch/README.md +100 -0
  30. package/src/color-swatch/color-swatch.css +95 -0
  31. package/{css-color/css-color.js → src/color-swatch/color-swatch.js} +30 -8
  32. package/src/common/color.js +12 -0
  33. package/src/common/dom.js +61 -0
  34. package/src/common/util.js +142 -0
  35. package/src/index.js.njk +7 -0
  36. package/src/src.json +3 -0
  37. package/_data/eleventyComputed.11tydata.js +0 -29
  38. package/color-slider/README.md +0 -84
  39. package/color-slider/color-slider.js +0 -79
  40. package/color-slider/style.css +0 -65
  41. package/color-swatch/index.njk +0 -40
  42. package/common/attributes.js +0 -68
  43. package/common/color.js +0 -10
  44. package/css-color/index.njk +0 -43
  45. package/css-color/style.css +0 -67
  46. /package/{color-gamut/style.css → src/color-gamut/color-gamut.css} +0 -0
  47. /package/{color-swatch/color-swatch.css → src/color-inline/color-inline.css} +0 -0
@@ -0,0 +1,278 @@
1
+
2
+ import Color from "../common/color.js";
3
+ import NudeElement from "../../node_modules/nude-element/src/Element.js";
4
+ import { getStep } from "../common/util.js";
5
+
6
+ let supports = {
7
+ inSpace: CSS?.supports("background", "linear-gradient(in oklab, red, tan)"),
8
+ fieldSizing: CSS?.supports("field-sizing", "content"),
9
+ };
10
+
11
+ const Self = class ColorSlider extends NudeElement {
12
+ static postConstruct = [];
13
+ static tagName = "color-slider";
14
+
15
+ constructor () {
16
+ super();
17
+ this.attachShadow({mode: "open"});
18
+
19
+ let styleURL = new URL(`./${this.constructor.tagName}.css`, import.meta.url);
20
+ this.shadowRoot.innerHTML = `
21
+ <style>@import url("${ styleURL }")</style>
22
+ <input type="range" class="color-slider" part="slider" min="0" max="1" step="0.01" part="slider" />
23
+ <slot name="tooltip" class="slider-tooltip">
24
+ <input type="number" part="spinner" min="0" max="1" step="0.01" />
25
+ </slot>
26
+ <slot></slot>
27
+ `;
28
+
29
+ this._el = {
30
+ slider: this.shadowRoot.querySelector(".color-slider"),
31
+ spinner: this.shadowRoot.querySelector("input[type=number]"),
32
+ };
33
+ }
34
+
35
+ connectedCallback() {
36
+ super.connectedCallback?.();
37
+
38
+ this._el.slider.addEventListener("input", this);
39
+ this._el.spinner.addEventListener("input", this);
40
+ }
41
+
42
+ disconnectedCallback() {
43
+ this._el.slider.removeEventListener("input", this);
44
+ this._el.spinner.removeEventListener("input", this);
45
+ }
46
+
47
+ handleEvent (event) {
48
+ if (event.type === "input") {
49
+ if (this.tooltip === "progress" && event.target === this._el.spinner) {
50
+ // Convert to value
51
+ let value = this._el.spinner.value;
52
+ this.value = this.valueAt(value / 100);
53
+ }
54
+ else {
55
+ this.value = event.target.value;
56
+ }
57
+
58
+ this.dispatchEvent(new event.constructor(event.type, {...event}));
59
+ }
60
+ }
61
+
62
+ propChangedCallback ({name, prop, detail: change}) {
63
+ if (["min", "max", "step", "value", "defaultValue"].includes(name)) {
64
+ prop.applyChange(this._el.slider, change);
65
+
66
+ let spinnerValue = this.tooltip === "progress" ? +(this.progress * 100).toPrecision(4) : this.value;
67
+ prop.applyChange(this._el.spinner, {...change, value: spinnerValue});
68
+ }
69
+
70
+ if (name === "stops") {
71
+ // FIXME will fail if there are none values
72
+ let stops = this.stops;
73
+ let supported = stops.every(color => CSS.supports("color", color));
74
+
75
+ if (!supported) {
76
+ stops = this.tessellateStops({ steps: 3 });
77
+ }
78
+
79
+ stops = stops.map(color => color.display()).join(", ");
80
+
81
+ this.style.setProperty("--slider-color-stops", stops);
82
+ }
83
+ else if (name === "space" && supports.inSpace) {
84
+ let space = this.space;
85
+ let spaceId = space.id;
86
+ let supported = CSS.supports("background", `linear-gradient(in ${ spaceId }, red, tan)`);
87
+
88
+ if (!supported) {
89
+ spaceId = this.space.isPolar ? "oklch" : "oklab";
90
+ }
91
+
92
+ this.style.setProperty("--color-space", spaceId);
93
+ }
94
+ else if (name === "color" || name === "defaultColor") {
95
+ let color = this.color;
96
+
97
+ if (color) {
98
+ let displayedColor = color.display();
99
+ this.style.setProperty("--color", displayedColor);
100
+ }
101
+ }
102
+ else if (name === "value") {
103
+ this.style.setProperty("--progress", this.progress);
104
+
105
+ if (!supports.fieldSizing) {
106
+ let valueStr = this.value + "";
107
+ this._el.spinner.style.setProperty("--value-length", valueStr.length);
108
+ }
109
+ }
110
+ }
111
+
112
+ tessellateStops (options = {}) {
113
+ let stops = this.stops;
114
+ let tessellated = [];
115
+
116
+ for (let i=1; i<stops.length; i++) {
117
+ let start = stops[i - 1];
118
+ let end = stops[i];
119
+ let steps = start.steps(end, { space: this.space, ...options });
120
+ tessellated.push(...steps);
121
+
122
+ if (i < stops.length - 1) {
123
+ tessellated.pop();
124
+ }
125
+ }
126
+
127
+ return tessellated;
128
+ }
129
+
130
+ get progress () {
131
+ return this.progressAt(this.value);
132
+ }
133
+
134
+ progressAt (value) {
135
+ return (value - this.min) / (this.max - this.min);
136
+ }
137
+
138
+ valueAt (progress) {
139
+ return this.min + progress * (this.max - this.min);
140
+ }
141
+
142
+ colorAt (value) {
143
+ let progress = this.progressAt(value);
144
+ return this.colorAtProgress(progress);
145
+ }
146
+
147
+ colorAtProgress (progress) {
148
+ let bands = this.scales?.length;
149
+
150
+ if (bands <= 0) {
151
+ return null;
152
+ }
153
+
154
+ // FIXME the values outside of [0, 1] should be scaled
155
+ if (progress >= 1) {
156
+ return this.scales.at(-1)(progress);
157
+ }
158
+ else if (progress <= 0) {
159
+ return this.scales[0](progress);
160
+ }
161
+
162
+ let band = 1 / bands;
163
+ let scaleIndex = Math.max(0, Math.min(Math.floor(progress / band), bands - 1));
164
+ let scale = this.scales[scaleIndex];
165
+ let color = scale((progress % band) * bands);
166
+
167
+ return color;
168
+ }
169
+
170
+ static props = {
171
+ min: {
172
+ type: Number,
173
+ default: 0,
174
+ },
175
+ max: {
176
+ type: Number,
177
+ default: 1,
178
+ },
179
+ step: {
180
+ type: Number,
181
+ default () {
182
+ return getStep(this.max, this.min, { minSteps: 100 });
183
+ },
184
+ },
185
+ stops: {
186
+ type: Array,
187
+ typeOptions: {
188
+ itemType: Color,
189
+ },
190
+ default: el => []
191
+ },
192
+ defaultValue: {
193
+ type: Number,
194
+ default () {
195
+ return (this.min + this.max) / 2;
196
+ },
197
+ reflect: {
198
+ from: "value",
199
+ },
200
+ },
201
+ value: {
202
+ type: Number,
203
+ defaultProp: "defaultValue",
204
+ reflect: false,
205
+ },
206
+
207
+ space: {
208
+ default () {
209
+ return this.stops[0]?.space ?? "oklab";
210
+ },
211
+ parse (value) {
212
+ if (value instanceof Color.Space || value === null || value === undefined) {
213
+ return value;
214
+ }
215
+
216
+ value += "";
217
+
218
+ return Color.Space.get(value);
219
+ },
220
+ stringify (value) {
221
+ return value?.id;
222
+ },
223
+ },
224
+
225
+ color: {
226
+ type: Color,
227
+ get () {
228
+ return this.colorAt(this.value);
229
+ },
230
+ dependencies: ["scales", "value"],
231
+ },
232
+ scales: {
233
+ get () {
234
+ let stops = this.stops;
235
+ let scales = [];
236
+
237
+ for (let i=1; i<stops.length; i++) {
238
+ let start = stops[i - 1];
239
+ let end = stops[i];
240
+ let range = start.range(end, { space: this.space, hue: "raw" });
241
+ scales.push(range);
242
+ }
243
+
244
+ return scales;
245
+ },
246
+ dependencies: ["stops", "space"],
247
+ },
248
+
249
+ tooltip: {
250
+ type: String,
251
+ },
252
+ };
253
+
254
+ static events = {
255
+ change: {
256
+ from () {
257
+ return this._el.slider;
258
+ }
259
+ },
260
+ valuechange: {
261
+ propchange: "value",
262
+ },
263
+ colorchange: {
264
+ propchange: "color",
265
+ },
266
+ };
267
+
268
+ static formAssociated = {
269
+ getSource: el => el._el.slider,
270
+ role: "slider",
271
+ valueProp: "value",
272
+ changeEvent: "valuechange",
273
+ };
274
+ }
275
+
276
+ customElements.define(Self.tagName, Self);
277
+
278
+ export default Self;
@@ -0,0 +1,100 @@
1
+ # `<color-swatch>`
2
+
3
+ ## Examples
4
+
5
+ ### Static
6
+
7
+ ```html
8
+ <color-swatch>oklch(70% 0.25 138)</color-swatch>
9
+ ```
10
+
11
+ ```html
12
+ <color-swatch size="large">oklch(70% 0.25 138)</color-swatch>
13
+ ```
14
+
15
+ ### Editable
16
+
17
+ ```html
18
+ <color-swatch>
19
+ <input value="oklch(70% 0.25 138)" />
20
+ </color-swatch>
21
+ ```
22
+
23
+ ```html
24
+ <color-swatch size="large">
25
+ <input value="oklch(70% 0.25 138)" />
26
+ </color-swatch>
27
+ ```
28
+
29
+ ### With slot content
30
+
31
+ ```html
32
+ <color-swatch>
33
+ <label slot="before" for=c1>Accent color:</label>
34
+ <input value="oklch(70% 0.25 138)" id=c1 />
35
+ </color-swatch>
36
+ ```
37
+
38
+ ```html
39
+ <color-swatch size="large">
40
+ <label slot="before" id=c2>Accent color:</label>
41
+ <input value="oklch(70% 0.25 138)" id=c2 />
42
+ <small slot="after">Tip: Pick a bright medium color.</small>
43
+ </color-swatch>
44
+ ```
45
+
46
+ Adding text within the default swatch:
47
+
48
+ ```html
49
+ <color-swatch size="large">
50
+ <div slot="swatch-content">Some text</div>
51
+ <input value="oklch(70% 0.25 138)" id=c1 />
52
+ </color-swatch>
53
+ ```
54
+
55
+ Replacing the whole swatch with a custom element:
56
+
57
+ ```html
58
+ <color-swatch size="large">
59
+ <div slot="swatch">Some text</div>
60
+ <input value="oklch(70% 0.25 138)" id=c1 />
61
+ </color-swatch>
62
+ ```
63
+
64
+ ### Bound to CSS property
65
+
66
+ You can automatically bind the color swatch to a CSS property by setting the `property` attribute.
67
+ Then you don’t need to provide an initial value, it will be read from the CSS property.
68
+
69
+ ```html
70
+ <color-swatch size="large" property="--color-red">
71
+ <input />
72
+ </color-swatch>
73
+ ```
74
+
75
+ ### Events
76
+
77
+ ```html
78
+ <color-swatch size="large" oncolorchange="this.nextElementSibling.textContent = this.color">
79
+ <input value="oklch(70% 0.25 138)" />
80
+ </color-swatch>
81
+ <color-inline></color-inline>
82
+ ```
83
+
84
+ ### Update via JS
85
+
86
+ #### Static
87
+
88
+ ```html
89
+ <color-swatch id="dynamic_static">oklch(70% 0.25 138)</color-swatch>
90
+ <button onclick='dynamic_static.color = "oklch(60% 0.15 0)"'>Change color</button>
91
+ ```
92
+
93
+ ### Editable
94
+
95
+ ```html
96
+ <color-swatch id="dynamic_editable">
97
+ <input value="oklch(70% 0.25 138)" />
98
+ </color-swatch>
99
+ <button onclick='dynamic_editable.color = "oklch(60% 0.15 0)"'>Change color</button>
100
+ ```
@@ -0,0 +1,95 @@
1
+ :host {
2
+ --_transparency-cell-size: var(--transparency-cell-size, .5em);
3
+ --_transparency-background: var(--transparcency-background, transparent);
4
+ --_transparency-darkness: var(--transparency-darkness, 5%);
5
+ --_transparency: var(--transparcency,
6
+ repeating-conic-gradient(transparent 0 25%, rgb(0 0 0 / var(--_transparency-darkness)) 0 50%)
7
+ 0 0 / calc(2 * var(--_transparency-cell-size)) calc(2 * var(--_transparency-cell-size))
8
+ content-box border-box var(--_transparency-background)
9
+ );
10
+ display: inline-flex;
11
+ gap: .3em;
12
+ width: min-content;
13
+ margin: .3em;
14
+ }
15
+
16
+ :host([size="large"]) {
17
+ flex-flow: column;
18
+ }
19
+
20
+ #gamut {
21
+ font-size: 80%;
22
+ }
23
+
24
+ #wrapper {
25
+ display: flex;
26
+ flex-flow: inherit;
27
+ gap: inherit;
28
+
29
+ &.static {
30
+ gap: .4em;
31
+ }
32
+
33
+ &:not(:host([size="large"]) *) {
34
+ align-items: baseline;
35
+ }
36
+ }
37
+
38
+ [part="color-wrapper"] {
39
+ position: relative;
40
+ display: flex;
41
+ gap: .2em;
42
+
43
+ &:not(.static *) {
44
+ #gamut {
45
+ position: absolute;
46
+ inset: .3em;
47
+ inset-inline-start: auto;
48
+ }
49
+ }
50
+
51
+ ::slotted(input) {
52
+ padding-inline-end: 2em;
53
+ }
54
+ }
55
+
56
+ slot:not([name]) {
57
+ white-space: nowrap;
58
+ }
59
+
60
+ ::slotted(input) {
61
+ font: inherit;
62
+ }
63
+
64
+ slot[name=swatch]::slotted(*),
65
+ #swatch {
66
+ flex: 1;
67
+ display: flex;
68
+ flex-flow: column;
69
+ align-items: center;
70
+ justify-content: center;
71
+ padding: .5em;
72
+ display: flex;
73
+ flex-flow: column;
74
+ flex: 1;
75
+ background:
76
+ linear-gradient(var(--color) 0 100%),
77
+ var(--_transparency);
78
+ border-radius: .2rem;
79
+
80
+ &:is(:host([size="large"]) *) {
81
+ max-inline-size: 15em;
82
+ min-block-size: 6em;
83
+ }
84
+
85
+ &:not(:host([size="large"]) *) {
86
+ display: flex;
87
+ min-inline-size: 2em;
88
+ font-size: 65%;
89
+ flex: 1;
90
+ }
91
+ }
92
+
93
+ :host([swatch="none"]) #swatch {
94
+ display: none;
95
+ }
@@ -1,22 +1,24 @@
1
1
  import Color from "../common/color.js";
2
+ import defineEvents from "../../node_modules/nude-element/src/events/defineEvents.js";
2
3
  import "../color-gamut/color-gamut.js";
3
- // const styles = await fetch("./style.css").then(r => r.text());
4
4
 
5
- const gamuts = ["srgb", "p3", "rec2020"];
5
+ let importIncrementable;
6
6
 
7
- let styleURL = new URL("./style.css", import.meta.url);
8
- let importIncrementable = import("https://incrementable.verou.me/incrementable.mjs").then(m => m.default);
7
+ const Self = class ColorSwatch extends HTMLElement {
8
+ static initQueue = [];
9
9
 
10
- export default class CSSColor extends HTMLElement {
11
10
  #dom = {};
12
11
 
13
12
  constructor () {
14
13
  super();
15
14
  this.attachShadow({mode: "open"});
15
+ let styleURL = new URL("./color-swatch.css", import.meta.url);
16
16
  this.shadowRoot.innerHTML = `
17
17
  <style>@import url("${ styleURL }")</style>
18
18
  <slot name="swatch">
19
- <div id="swatch" part="swatch"></div>
19
+ <div id="swatch" part="swatch">
20
+ <slot name="swatch-content"></slot>
21
+ </div>
20
22
  </slot>
21
23
  <div id="wrapper">
22
24
  <slot name="before"></slot>
@@ -35,6 +37,8 @@ export default class CSSColor extends HTMLElement {
35
37
  this.#initialize();
36
38
  }
37
39
 
40
+ this.constructor.initQueue.forEach(init => init.call(this));
41
+
38
42
  // This should eventually be a custom state
39
43
  this.#dom.wrapper.classList.toggle("static", !this.#dom.input);
40
44
 
@@ -63,8 +67,14 @@ export default class CSSColor extends HTMLElement {
63
67
  this.#dom.wrapper = this.shadowRoot.querySelector("#wrapper");
64
68
  this.#dom.colorWrapper = this.shadowRoot.querySelector("[part=color-wrapper]");
65
69
  this.#dom.input = this.querySelector("input");
70
+ this.#dom.slot = this.shadowRoot.querySelector("slot:not([name])");
71
+
72
+ this.#dom.slot.addEventListener("slotchange", evt => {
73
+ this.#render();
74
+ });
66
75
 
67
76
  if (this.#dom.input) {
77
+ importIncrementable ??= import("https://incrementable.verou.me/incrementable.mjs").then(m => m.default);
68
78
  this.#dom.input.addEventListener("input", evt => {
69
79
  this.#render(evt);
70
80
  });
@@ -228,7 +238,6 @@ export default class CSSColor extends HTMLElement {
228
238
  else {
229
239
  try {
230
240
  colorString = this.#color.display({inGamut: false});
231
-
232
241
  }
233
242
  catch (e) {
234
243
  colorString = this.value;
@@ -250,7 +259,20 @@ export default class CSSColor extends HTMLElement {
250
259
  }
251
260
  }
252
261
 
262
+ static events = {
263
+ colorchange: {
264
+ propchange: "color",
265
+ },
266
+ valuechange: {
267
+ propchange: "value",
268
+ },
269
+ };
270
+
253
271
  static observedAttributes = ["for", "property"];
254
272
  }
255
273
 
256
- customElements.define("css-color", CSSColor);
274
+ defineEvents(Self);
275
+
276
+ customElements.define("color-swatch", Self);
277
+
278
+ export default Self;
@@ -0,0 +1,12 @@
1
+ let specifier;
2
+
3
+ try {
4
+ import.meta.resolve("colorjs.io");
5
+ specifier = "colorjs.io";
6
+ }
7
+ catch (e) {
8
+ // specifier = "../../node_modules/colorjs.io/dist/color.js";
9
+ specifier = "https://colorjs.io/dist/color.js";
10
+ }
11
+
12
+ export default await import(specifier).then(module => module.default);
@@ -0,0 +1,61 @@
1
+ export function named (host, attributes = ["id", "part"]) {
2
+ let ret = {};
3
+ let selector = attributes.map(attr => `[${ attr }]`).join(", ");
4
+
5
+ for (let el of host.shadowRoot.querySelectorAll(selector)) {
6
+ // Get the value of the first attribute in attributes that has a value
7
+ let attribute = attributes.find(attr => el.hasAttribute(attr));
8
+ ret[el[attribute]] = el;
9
+ }
10
+
11
+ return ret;
12
+ }
13
+
14
+ export function slots (host) {
15
+ let ret = {};
16
+
17
+ for (let slot of host.shadowRoot.querySelectorAll("slot")) {
18
+ ret[slot.name] = slot;
19
+
20
+ if (!slot.name || slot.dataset.default !== undefined) {
21
+ ret.default = slot;
22
+ }
23
+ }
24
+
25
+ return ret;
26
+ }
27
+
28
+ export function toSlots ({
29
+ slots = this._slots,
30
+ slotElements = slots ? Object.values(slots) : Array.from(this.shadowRoot.querySelectorAll("slot")),
31
+ }) {
32
+ let children = this.childNodes;
33
+ let assignments = new WeakMap();
34
+
35
+ if (!slots && slotElements) {
36
+ slots = Object.fromEntries(slotElements.map(slot => [slot.name, slot]));
37
+ }
38
+
39
+ // Assign to slots
40
+ for (let child of children) {
41
+ let assignedSlot;
42
+
43
+ if (child.slot) {
44
+ // Explicit slot
45
+ assignedSlot = slots[child.slot];
46
+ }
47
+ else if (child.matches) {
48
+ assignedSlot = slotElements.find(slot => child.matches(slot.dataset.assign));
49
+ }
50
+
51
+ assignedSlot ??= slots.default;
52
+ let all = assignments.get(assignedSlot) ?? new Set();
53
+ all.add(child);
54
+ assignments.set(assignedSlot, all);
55
+ }
56
+
57
+ for (let slot of slotElements) {
58
+ let all = assignments.get(slot) ?? new Set();
59
+ slot.assign(...all);
60
+ }
61
+ }