@fusion.dev/helix-headless 0.57.0 → 0.58.1

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.
@@ -18896,169 +18896,11 @@
18896
18896
  }
18897
18897
  ]
18898
18898
  },
18899
- {
18900
- "kind": "javascript-module",
18901
- "path": "src/color-picker/color.ts",
18902
- "declarations": [
18903
- {
18904
- "kind": "function",
18905
- "name": "toRgb",
18906
- "return": {
18907
- "type": {
18908
- "text": "{ blue: number; green: number; red: number }"
18909
- }
18910
- },
18911
- "parameters": [
18912
- {
18913
- "name": "color",
18914
- "type": {
18915
- "text": "Color"
18916
- }
18917
- }
18918
- ],
18919
- "description": "HSB to bytes, which is the road every written syntax goes down."
18920
- },
18921
- {
18922
- "kind": "function",
18923
- "name": "splitColors",
18924
- "return": {
18925
- "type": {
18926
- "text": "string[]"
18927
- }
18928
- },
18929
- "parameters": [
18930
- {
18931
- "name": "value",
18932
- "type": {
18933
- "text": "string"
18934
- }
18935
- }
18936
- ],
18937
- "description": "Splits a comma-separated list of colors, ignoring the commas inside them.\n\nA plain `split(',')` cannot hold the legacy syntaxes:\n`#ef4444, rgb(249 115 22), rgba(34,197,94,.5)` comes apart into six fragments,\nfour of them not colors, and the row quietly renders three swatches instead of\nfour. The modern space-separated forms survive it, which is what makes such a\nlist look like it works.\n\nDepth rather than a regular expression, because the thing that matters is\nwhether a comma is inside a function call and that is exactly what a counter\nanswers. No color syntax nests, so a single counter is enough."
18938
- },
18939
- {
18940
- "kind": "function",
18941
- "name": "parseColor",
18942
- "return": {
18943
- "type": {
18944
- "text": "Color | null"
18945
- }
18946
- },
18947
- "parameters": [
18948
- {
18949
- "name": "value",
18950
- "type": {
18951
- "text": "string"
18952
- }
18953
- }
18954
- ],
18955
- "description": "Any of the syntaxes to the working model, or `null` where it is not a color.\n\nNamed colors are not here. Resolving `rebeccapurple` means either shipping\nthe table of a hundred and forty or asking the browser, and the browser is not\npresent on the server: a field that accepted a name client-side and rejected\nit during a server render would be worse than one that never took it."
18956
- },
18957
- {
18958
- "kind": "function",
18959
- "name": "formatColor",
18960
- "return": {
18961
- "type": {
18962
- "text": "string"
18963
- }
18964
- },
18965
- "parameters": [
18966
- {
18967
- "name": "color",
18968
- "type": {
18969
- "text": "Color"
18970
- }
18971
- },
18972
- {
18973
- "name": "format",
18974
- "type": {
18975
- "text": "ColorFormat"
18976
- }
18977
- },
18978
- {
18979
- "name": "withAlpha",
18980
- "type": {
18981
- "text": "boolean"
18982
- }
18983
- }
18984
- ],
18985
- "description": "The working model back out, in the syntax asked for.\n\nThe alpha is written only where the component carries one. A consumer who did\nnot ask for transparency should not have to strip `ff` off every hex or read\npast a fourth argument they never wanted."
18986
- },
18987
- {
18988
- "kind": "function",
18989
- "name": "toCss",
18990
- "return": {
18991
- "type": {
18992
- "text": "string"
18993
- }
18994
- },
18995
- "parameters": [
18996
- {
18997
- "name": "color",
18998
- "type": {
18999
- "text": "Color"
19000
- }
19001
- }
19002
- ],
19003
- "description": "The color as CSS, for the swatches and the tracks. Always with its alpha."
19004
- }
19005
- ],
19006
- "exports": [
19007
- {
19008
- "kind": "js",
19009
- "name": "toRgb",
19010
- "declaration": {
19011
- "name": "toRgb",
19012
- "module": "src/color-picker/color.ts"
19013
- }
19014
- },
19015
- {
19016
- "kind": "js",
19017
- "name": "splitColors",
19018
- "declaration": {
19019
- "name": "splitColors",
19020
- "module": "src/color-picker/color.ts"
19021
- }
19022
- },
19023
- {
19024
- "kind": "js",
19025
- "name": "parseColor",
19026
- "declaration": {
19027
- "name": "parseColor",
19028
- "module": "src/color-picker/color.ts"
19029
- }
19030
- },
19031
- {
19032
- "kind": "js",
19033
- "name": "formatColor",
19034
- "declaration": {
19035
- "name": "formatColor",
19036
- "module": "src/color-picker/color.ts"
19037
- }
19038
- },
19039
- {
19040
- "kind": "js",
19041
- "name": "toCss",
19042
- "declaration": {
19043
- "name": "toCss",
19044
- "module": "src/color-picker/color.ts"
19045
- }
19046
- }
19047
- ]
19048
- },
19049
18899
  {
19050
18900
  "kind": "javascript-module",
19051
18901
  "path": "src/color-picker/index.ts",
19052
18902
  "declarations": [],
19053
18903
  "exports": [
19054
- {
19055
- "kind": "js",
19056
- "name": "*",
19057
- "declaration": {
19058
- "name": "*",
19059
- "module": "src/color-picker/color.js"
19060
- }
19061
- },
19062
18904
  {
19063
18905
  "kind": "js",
19064
18906
  "name": "*",
@@ -94129,7 +93971,7 @@
94129
93971
  }
94130
93972
  }
94131
93973
  ],
94132
- "description": "Whether a string is a time the rest of this component can reason about."
93974
+ "description": "Whether a string is a time the rest of this component can reason about.\n\n**Narrower than core's `valueAsTime` on purpose, and the difference is\nseconds.** That one takes them because `<input type=\"time\">` does, at a step\nbelow a minute. This library has no such step: `step` here is minutes and is\nfloored at one, so nothing it offers or writes ever carries a second, and\n`toMinutes` below would drop one silently if this let it through. Widening\nthis to the platform's shape would let a value in that the rest of the family\ncannot hold."
94133
93975
  },
94134
93976
  {
94135
93977
  "kind": "function",
@@ -1,7 +1,6 @@
1
- import { AriaForwardController, HelixFormElement } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, type ColorFormat, HelixFormElement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../color-picker/index.js';
4
- import { type ColorFormat } from '../../color-picker/color.js';
5
4
  declare const HelixHeadlessColorInput_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinPickerFieldInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinFieldInterface) & typeof HelixFormElement;
6
5
  /**
7
6
  * @summary A color, typed in any syntax or picked from a panel.
@@ -4,12 +4,11 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { AriaForwardController, customElement, HelixFormElement, MixinControllerOpen, MixinControllerValue, MixinField, MixinPickerField, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, customElement, HelixFormElement, MixinControllerOpen, MixinControllerValue, MixinField, MixinPickerField, parseColor, toCss, } from '@fusion.dev/helix-core';
8
8
  import { nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { html } from 'lit/static-html.js';
11
11
  import '../../color-picker/index.js';
12
- import { parseColor, toCss } from '../../color-picker/color.js';
13
12
  let HelixHeadlessColorInput = class HelixHeadlessColorInput extends MixinPickerField(MixinControllerOpen(MixinControllerValue(MixinField(HelixFormElement)))) {
14
13
  constructor() {
15
14
  super(...arguments);
@@ -1,2 +1 @@
1
- export * from './color.js';
2
1
  export * from './root/color-picker-root.js';
@@ -1,2 +1 @@
1
- export * from './color.js';
2
1
  export * from './root/color-picker-root.js';
@@ -1,9 +1,8 @@
1
- import { HelixFormElement } from '@fusion.dev/helix-core';
1
+ import { type Color, type ColorFormat, HelixFormElement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../button/index.js';
4
4
  import '../../button-group/index.js';
5
5
  import '../../slider/index.js';
6
- import { type Color, type ColorFormat } from '../color.js';
7
6
  declare const HelixHeadlessColorPicker_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerValueInterface) & typeof HelixFormElement;
8
7
  /**
9
8
  * @summary Picks a color, from a square and two ramps.
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { CopyController, capturePointer, customElement, drawIcon, HelixFormElement, HelixSampleEndEvent, HelixSampleStartEvent, MixinControllerValue, } from '@fusion.dev/helix-core';
7
+ import { CopyController, capturePointer, customElement, drawIcon, formatColor, HelixFormElement, HelixSampleEndEvent, HelixSampleStartEvent, MixinControllerValue, parseColor, splitColors, toCss, } from '@fusion.dev/helix-core';
8
8
  import { eyedropper } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -12,7 +12,6 @@ import { html } from 'lit/static-html.js';
12
12
  import '../../button/index.js';
13
13
  import '../../button-group/index.js';
14
14
  import '../../slider/index.js';
15
- import { formatColor, parseColor, splitColors, toCss, } from '../color.js';
16
15
  const FORMATS = ['hex', 'rgb', 'hsl'];
17
16
  const START = { alpha: 100, brightness: 100, hue: 0, saturation: 100 };
18
17
  let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinControllerValue(HelixFormElement) {
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { AriaForwardController, valueAsDate as asDate, customElement, dateAsValue as fromDate, HelixFormElement, MixinControllerOpen, MixinControllerValue, MixinField, MixinPickerEditing, MixinPickerField, MixinTokens, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, valueAsDate as asDate, customElement, dateAsValue as fromDate, HelixFormElement, isDateValue, MixinControllerOpen, MixinControllerValue, MixinField, MixinPickerEditing, MixinPickerField, MixinTokens, } from '@fusion.dev/helix-core';
8
8
  import { calendarBlank } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -12,7 +12,6 @@ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
12
  import { html } from 'lit/static-html.js';
13
13
  import '../../date-picker/index.js';
14
14
  import '../../tag/index.js';
15
- const isIso = (value) => /^\d{4}-\d{2}-\d{2}$/.test(value);
16
15
  const dates = (value) => value
17
16
  .replace(/\s+[–-]\s+/g, ',')
18
17
  .split(',')
@@ -47,7 +46,7 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinPickerEdi
47
46
  return;
48
47
  }
49
48
  const held = dates(typed);
50
- const whole = held.length > 0 && held.every(isIso);
49
+ const whole = held.length > 0 && held.every(isDateValue);
51
50
  this.requestValue(whole ? held.join(',') : typed);
52
51
  this.restore();
53
52
  };
@@ -56,7 +55,7 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinPickerEdi
56
55
  return;
57
56
  }
58
57
  const held = dates(this.value);
59
- if (event.key === 'Enter' && isIso(this.draft)) {
58
+ if (event.key === 'Enter' && isDateValue(this.draft)) {
60
59
  event.preventDefault();
61
60
  if (!held.includes(this.draft)) {
62
61
  this.requestValue([...held, this.draft].join(','));
@@ -107,10 +106,10 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinPickerEdi
107
106
  }
108
107
  get picked() {
109
108
  const held = dates(this.value);
110
- return held.length > 0 && held.every(isIso) ? held.join(',') : '';
109
+ return held.length > 0 && held.every(isDateValue) ? held.join(',') : '';
111
110
  }
112
111
  written(date) {
113
- if (this.dateStyle === 'iso' || !isIso(date)) {
112
+ if (this.dateStyle === 'iso' || !isDateValue(date)) {
114
113
  return date;
115
114
  }
116
115
  const at = new Date(`${date}T00:00:00Z`);
@@ -141,7 +140,7 @@ let HelixHeadlessDateInput = class HelixHeadlessDateInput extends MixinPickerEdi
141
140
  }
142
141
  validate() {
143
142
  const held = dates(this.value);
144
- if (held.length > 0 && !held.every(isIso)) {
143
+ if (held.length > 0 && !held.every(isDateValue)) {
145
144
  return this.mode === undefined
146
145
  ? 'Enter a date as YYYY-MM-DD.'
147
146
  : 'Enter dates as YYYY-MM-DD, separated by commas.';
@@ -21,15 +21,6 @@ const KEYS = new Set([
21
21
  'PageUp',
22
22
  ' ',
23
23
  ]);
24
- const toUtc = (iso) => {
25
- const parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
26
- if (!parts) {
27
- return null;
28
- }
29
- const date = new Date(Date.UTC(Number(parts[1]), Number(parts[2]) - 1, Number(parts[3])));
30
- return Number.isNaN(date.getTime()) ? null : date;
31
- };
32
- const toIso = (date) => date.toISOString().slice(0, 10);
33
24
  const localToday = () => {
34
25
  const now = new Date();
35
26
  const month = `${now.getMonth() + 1}`.padStart(2, '0');
@@ -104,7 +95,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
104
95
  this.step = 1;
105
96
  }
106
97
  get month() {
107
- return this.monthState ?? (this.defaultMonth || toIso(this.derivedMonth).slice(0, 7));
98
+ return this.monthState ?? (this.defaultMonth || fromDate(this.derivedMonth).slice(0, 7));
108
99
  }
109
100
  set month(next) {
110
101
  const previous = this.month;
@@ -137,12 +128,12 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
137
128
  return this.derivedMonth;
138
129
  }
139
130
  get derivedMonth() {
140
- const from = toUtc(this.selected[0] ?? '') ?? new Date();
131
+ const from = asDate(this.selected[0] ?? '') ?? new Date();
141
132
  return new Date(Date.UTC(from.getUTCFullYear(), from.getUTCMonth(), 1));
142
133
  }
143
134
  get anchor() {
144
135
  const shown = this.shown;
145
- for (const candidate of [toUtc(this.focused), toUtc(this.selected[0] ?? '')]) {
136
+ for (const candidate of [asDate(this.focused), asDate(this.selected[0] ?? '')]) {
146
137
  if (candidate && sameMonth(candidate, shown)) {
147
138
  return candidate;
148
139
  }
@@ -155,7 +146,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
155
146
  return Array.from({ length: 6 }, (_, week) => Array.from({ length: 7 }, (_, day) => addDays(start, week * 7 + day)));
156
147
  }
157
148
  barred(date) {
158
- const iso = toIso(date);
149
+ const iso = fromDate(date);
159
150
  if (this.min !== '' && iso < this.min) {
160
151
  return true;
161
152
  }
@@ -195,7 +186,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
195
186
  });
196
187
  }
197
188
  moveTo(date) {
198
- this.focused = toIso(date);
189
+ this.focused = fromDate(date);
199
190
  this.show(date);
200
191
  this.updateComplete.then(() => {
201
192
  this.renderRoot.querySelector('[part~="cell"][tabindex="0"]')?.focus();
@@ -208,7 +199,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
208
199
  if (this.barred(date)) {
209
200
  return;
210
201
  }
211
- const iso = toIso(date);
202
+ const iso = fromDate(date);
212
203
  const held = this.selected;
213
204
  let next;
214
205
  if (this.mode === 'range') {
@@ -309,7 +300,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
309
300
  renderMonth(month, index) {
310
301
  const { full, heading, weekday } = this.formats;
311
302
  const weeks = this.weeksOf(month);
312
- const anchor = toIso(this.anchor);
303
+ const anchor = fromDate(this.anchor);
313
304
  const today = localToday();
314
305
  const range = this.range;
315
306
  const first = index === 0;
@@ -349,7 +340,7 @@ let HelixHeadlessDatePicker = class HelixHeadlessDatePicker extends MixinControl
349
340
  </div>
350
341
  ${weeks.map((week) => html `<div part="week" role="row">
351
342
  ${week.map((date) => {
352
- const iso = toIso(date);
343
+ const iso = fromDate(date);
353
344
  const disabled = this.barred(date);
354
345
  const selected = this.isSelected(iso);
355
346
  const outside = !sameMonth(date, month);
@@ -2,7 +2,17 @@ import { HelixFormElement } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../listbox/index.js';
4
4
  import '../../option/index.js';
5
- /** Whether a string is a time the rest of this component can reason about. */
5
+ /**
6
+ * Whether a string is a time the rest of this component can reason about.
7
+ *
8
+ * **Narrower than core's `valueAsTime` on purpose, and the difference is
9
+ * seconds.** That one takes them because `<input type="time">` does, at a step
10
+ * below a minute. This library has no such step: `step` here is minutes and is
11
+ * floored at one, so nothing it offers or writes ever carries a second, and
12
+ * `toMinutes` below would drop one silently if this let it through. Widening
13
+ * this to the platform's shape would let a value in that the rest of the family
14
+ * cannot hold.
15
+ */
6
16
  export declare const isTime: (value: string) => boolean;
7
17
  /** Minutes since midnight, or `null` where it is not a time. */
8
18
  export declare const toMinutes: (value: string) => number | null;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-headless",
3
3
  "description": "Headless component tier. Structure, accessibility, state, and form participation. No styling.",
4
- "version": "0.57.0",
4
+ "version": "0.58.1",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -46,14 +46,14 @@
46
46
  },
47
47
  "bugs": "https://github.com/project-fusion/fusion/issues",
48
48
  "dependencies": {
49
- "@fusion.dev/helix-core": "0.57.0",
50
- "@fusion.dev/helix-icons": "0.57.0",
49
+ "@fusion.dev/helix-core": "0.58.1",
50
+ "@fusion.dev/helix-icons": "0.58.1",
51
51
  "uqr": "^0.1.3"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@custom-elements-manifest/analyzer": "^0.11.0",
55
- "@fusion.dev/helix-cem": "0.57.0",
56
- "@fusion.dev/helix-testing": "0.57.0",
55
+ "@fusion.dev/helix-cem": "0.58.1",
56
+ "@fusion.dev/helix-testing": "0.58.1",
57
57
  "@fusion.dev/scripts": "0.0.0-dev",
58
58
  "@happy-dom/global-registrator": "^20.0.0",
59
59
  "@lit-labs/ssr": "^4.1.0",
@@ -1,73 +0,0 @@
1
- /**
2
- * Reading and writing the color syntaxes a field is allowed to hold.
3
- *
4
- * **The working model is HSB**, hue and saturation and brightness, because that
5
- * is what the panel is: a square of saturation against brightness with a hue
6
- * ramp beside it. HSL would put the square's corners somewhere a reader does not
7
- * expect, since full saturation in HSL is a mid-lightness band rather than a
8
- * corner.
9
- *
10
- * **Every syntax here is read; three are written.** A field that refuses
11
- * `rgb(255 0 0)` because it prefers hex is a field arguing with the person using
12
- * it, so the space-separated forms, the comma forms, `rgba` and `hsla` are all
13
- * taken. What is written back is whichever of hex, rgb or hsl the component is
14
- * set to, so a value stays in the shape the consumer asked for however it was
15
- * entered.
16
- *
17
- * **What is written is the comma form.** Both are valid, and the commas are what
18
- * a reader recognizes, what most code around them holds, and what pastes into
19
- * anything without a second thought. A syntax a value is handed *out* in is not
20
- * the place to be early. `rgba` and `hsla` come with them, since a comma form
21
- * with a fourth argument is spelled that way.
22
- *
23
- * Its own module rather than sitting in the element, because it is arithmetic
24
- * with no DOM in it: this is the half worth testing directly.
25
- */
26
- /** A color as the panel holds it. Alpha and the two percentages are 0 to 100. */
27
- export interface Color {
28
- alpha: number;
29
- brightness: number;
30
- hue: number;
31
- saturation: number;
32
- }
33
- /** The syntaxes a value can be written in. */
34
- export type ColorFormat = 'hex' | 'hsl' | 'rgb';
35
- /** HSB to bytes, which is the road every written syntax goes down. */
36
- export declare const toRgb: (color: Color) => {
37
- blue: number;
38
- green: number;
39
- red: number;
40
- };
41
- /**
42
- * Splits a comma-separated list of colors, ignoring the commas inside them.
43
- *
44
- * A plain `split(',')` cannot hold the legacy syntaxes:
45
- * `#ef4444, rgb(249 115 22), rgba(34,197,94,.5)` comes apart into six fragments,
46
- * four of them not colors, and the row quietly renders three swatches instead of
47
- * four. The modern space-separated forms survive it, which is what makes such a
48
- * list look like it works.
49
- *
50
- * Depth rather than a regular expression, because the thing that matters is
51
- * whether a comma is inside a function call and that is exactly what a counter
52
- * answers. No color syntax nests, so a single counter is enough.
53
- */
54
- export declare const splitColors: (value: string) => string[];
55
- /**
56
- * Any of the syntaxes to the working model, or `null` where it is not a color.
57
- *
58
- * Named colors are not here. Resolving `rebeccapurple` means either shipping
59
- * the table of a hundred and forty or asking the browser, and the browser is not
60
- * present on the server: a field that accepted a name client-side and rejected
61
- * it during a server render would be worse than one that never took it.
62
- */
63
- export declare const parseColor: (value: string) => Color | null;
64
- /**
65
- * The working model back out, in the syntax asked for.
66
- *
67
- * The alpha is written only where the component carries one. A consumer who did
68
- * not ask for transparency should not have to strip `ff` off every hex or read
69
- * past a fourth argument they never wanted.
70
- */
71
- export declare const formatColor: (color: Color, format: ColorFormat, withAlpha: boolean) => string;
72
- /** The color as CSS, for the swatches and the tracks. Always with its alpha. */
73
- export declare const toCss: (color: Color) => string;
@@ -1,168 +0,0 @@
1
- const clamp = (value, low, high) => Math.min(high, Math.max(low, value));
2
- const toAlpha = (token) => {
3
- if (token === undefined) {
4
- return 100;
5
- }
6
- const number = Number.parseFloat(token);
7
- if (Number.isNaN(number)) {
8
- return 100;
9
- }
10
- return clamp(token.includes('%') ? number : number * 100, 0, 100);
11
- };
12
- const toByte = (token) => {
13
- const number = Number.parseFloat(token);
14
- return clamp(token.includes('%') ? (number / 100) * 255 : number, 0, 255);
15
- };
16
- const rgbToHsb = (red, green, blue, alpha) => {
17
- const r = red / 255;
18
- const g = green / 255;
19
- const b = blue / 255;
20
- const high = Math.max(r, g, b);
21
- const low = Math.min(r, g, b);
22
- const span = high - low;
23
- let hue = 0;
24
- if (span !== 0) {
25
- if (high === r) {
26
- hue = (g - b) / span + (g < b ? 6 : 0);
27
- }
28
- else if (high === g) {
29
- hue = (b - r) / span + 2;
30
- }
31
- else {
32
- hue = (r - g) / span + 4;
33
- }
34
- hue *= 60;
35
- }
36
- return {
37
- alpha,
38
- brightness: Math.round(high * 100),
39
- hue: Math.round(hue),
40
- saturation: Math.round(high === 0 ? 0 : (span / high) * 100),
41
- };
42
- };
43
- export const toRgb = (color) => {
44
- const hue = (((color.hue % 360) + 360) % 360) / 60;
45
- const saturation = color.saturation / 100;
46
- const brightness = color.brightness / 100;
47
- const chroma = brightness * saturation;
48
- const middle = chroma * (1 - Math.abs((hue % 2) - 1));
49
- const base = brightness - chroma;
50
- const sextants = [
51
- [chroma, middle, 0],
52
- [middle, chroma, 0],
53
- [0, chroma, middle],
54
- [0, middle, chroma],
55
- [middle, 0, chroma],
56
- [chroma, 0, middle],
57
- ];
58
- const [red, green, blue] = sextants[Math.floor(hue) % 6] ?? [0, 0, 0];
59
- return {
60
- blue: Math.round((blue + base) * 255),
61
- green: Math.round((green + base) * 255),
62
- red: Math.round((red + base) * 255),
63
- };
64
- };
65
- const toHsl = (color) => {
66
- const saturation = color.saturation / 100;
67
- const brightness = color.brightness / 100;
68
- const lightness = (brightness * (2 - saturation)) / 2;
69
- return {
70
- hue: ((color.hue % 360) + 360) % 360,
71
- lightness: Math.round(lightness * 100),
72
- saturation: Math.round((lightness === 0 || lightness === 1
73
- ? 0
74
- : (brightness - lightness) / Math.min(lightness, 1 - lightness)) * 100),
75
- };
76
- };
77
- export const splitColors = (value) => {
78
- const found = [];
79
- let depth = 0;
80
- let current = '';
81
- for (const character of value) {
82
- if (character === '(') {
83
- depth += 1;
84
- }
85
- else if (character === ')') {
86
- depth = Math.max(0, depth - 1);
87
- }
88
- if (character === ',' && depth === 0) {
89
- found.push(current.trim());
90
- current = '';
91
- }
92
- else {
93
- current += character;
94
- }
95
- }
96
- found.push(current.trim());
97
- return found.filter(Boolean);
98
- };
99
- export const parseColor = (value) => {
100
- const text = value.trim().toLowerCase();
101
- if (text === '') {
102
- return null;
103
- }
104
- if (text.startsWith('#')) {
105
- const digits = text.slice(1);
106
- const short = digits.length === 3 || digits.length === 4;
107
- const long = digits.length === 6 || digits.length === 8;
108
- if ((!short && !long) || !/^[\da-f]+$/.test(digits)) {
109
- return null;
110
- }
111
- const width = short ? 1 : 2;
112
- const channel = (index) => {
113
- const piece = digits.slice(index * width, index * width + width);
114
- return Number.parseInt(short ? piece + piece : piece, 16);
115
- };
116
- const carriesAlpha = digits.length === 4 || digits.length === 8;
117
- return rgbToHsb(channel(0), channel(1), channel(2), carriesAlpha ? Math.round((channel(3) / 255) * 100) : 100);
118
- }
119
- const call = /^(rgba?|hsla?)\(([^)]*)\)$/.exec(text);
120
- if (!call?.[1] || call[2] === undefined) {
121
- return null;
122
- }
123
- const [first = '', second = '', third = '', fourth] = call[2]
124
- .replace(/[,/]/g, ' ')
125
- .split(/\s+/)
126
- .filter(Boolean);
127
- if (first === '' || second === '' || third === '') {
128
- return null;
129
- }
130
- const alpha = toAlpha(fourth);
131
- if (call[1].startsWith('rgb')) {
132
- return rgbToHsb(toByte(first), toByte(second), toByte(third), alpha);
133
- }
134
- const hue = Number.parseFloat(first);
135
- const saturation = clamp(Number.parseFloat(second), 0, 100) / 100;
136
- const lightness = clamp(Number.parseFloat(third), 0, 100) / 100;
137
- if (Number.isNaN(hue) || Number.isNaN(saturation) || Number.isNaN(lightness)) {
138
- return null;
139
- }
140
- const brightness = lightness + saturation * Math.min(lightness, 1 - lightness);
141
- return {
142
- alpha,
143
- brightness: Math.round(brightness * 100),
144
- hue: Math.round(((hue % 360) + 360) % 360),
145
- saturation: Math.round((brightness === 0 ? 0 : 2 * (1 - lightness / brightness)) * 100),
146
- };
147
- };
148
- export const formatColor = (color, format, withAlpha) => {
149
- const alpha = clamp(color.alpha, 0, 100) / 100;
150
- const opacity = Number(alpha.toFixed(2));
151
- if (format === 'hsl') {
152
- const { hue, lightness, saturation } = toHsl(color);
153
- const body = `${Math.round(hue)}, ${saturation}%, ${lightness}%`;
154
- return withAlpha ? `hsla(${body}, ${opacity})` : `hsl(${body})`;
155
- }
156
- const { blue, green, red } = toRgb(color);
157
- if (format === 'rgb') {
158
- const body = `${red}, ${green}, ${blue}`;
159
- return withAlpha ? `rgba(${body}, ${opacity})` : `rgb(${body})`;
160
- }
161
- const byte = (one) => one.toString(16).padStart(2, '0');
162
- const hex = `#${byte(red)}${byte(green)}${byte(blue)}`;
163
- return withAlpha ? hex + byte(Math.round(alpha * 255)) : hex;
164
- };
165
- export const toCss = (color) => {
166
- const { blue, green, red } = toRgb(color);
167
- return `rgb(${red} ${green} ${blue} / ${clamp(color.alpha, 0, 100)}%)`;
168
- };