@stanko/ctrls 0.1.9 → 0.3.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/README.md CHANGED
@@ -332,6 +332,40 @@ Example:
332
332
 
333
333
  </div>
334
334
 
335
+ ### Group
336
+
337
+ Collapsible group of controls. All values are going to be nested in an object using the group's name.
338
+
339
+ ```ts
340
+ {
341
+ // Mandatory
342
+ controls: ConfigItem[]
343
+ }
344
+ ```
345
+
346
+ Example:
347
+
348
+ <div class="example">
349
+
350
+ ```json
351
+ {
352
+ "type": "group",
353
+ "name": "color",
354
+ "controls": [
355
+ {
356
+ "type": "boolean",
357
+ "name": "monochrome"
358
+ },
359
+ {
360
+ "type": "easing",
361
+ "name": "distribution"
362
+ }
363
+ ]
364
+ }
365
+ ```
366
+
367
+ </div>
368
+
335
369
  ## Theming
336
370
 
337
371
  Ctrls uses CSS variables for theming. There are many you can adjust, but I recommend starting with these four:
@@ -1,6 +1,8 @@
1
1
  import type { Ctrl, CtrlType, CtrlChangeHandler, CtrlConfig } from ".";
2
2
  export declare class BooleanCtrl implements Ctrl<boolean> {
3
3
  type: CtrlType;
4
+ id: string;
5
+ group?: string;
4
6
  name: string;
5
7
  label: string;
6
8
  value: boolean;
@@ -56,6 +56,8 @@ export class BooleanCtrl {
56
56
  };
57
57
  this.type = "boolean";
58
58
  this.name = config.name;
59
+ this.id = config.id || config.name;
60
+ this.group = config.group || "";
59
61
  this.label = config.label || config.name;
60
62
  this.value =
61
63
  config.defaultValue === undefined
@@ -11,6 +11,8 @@ export type DualRangeValue = {
11
11
  };
12
12
  export declare class DualRangeCtrl implements Ctrl<DualRangeValue> {
13
13
  type: CtrlType;
14
+ id: string;
15
+ group?: string;
14
16
  name: string;
15
17
  label: string;
16
18
  value: DualRangeValue;
@@ -101,6 +101,8 @@ export class DualRangeCtrl {
101
101
  this.dualRange.update();
102
102
  };
103
103
  this.name = config.name;
104
+ this.id = config.id || config.name;
105
+ this.group = config.group || "";
104
106
  this.label = config.label || config.name;
105
107
  this.min = config.min;
106
108
  this.max = config.max;
@@ -2,6 +2,8 @@ import type { Ctrl, CtrlChangeHandler, CtrlType, ConfigFor } from ".";
2
2
  export type Easing = [number, number, number, number];
3
3
  export declare class EasingCtrl implements Ctrl<Easing> {
4
4
  type: CtrlType;
5
+ id: string;
6
+ group?: string;
5
7
  name: string;
6
8
  label: string;
7
9
  value: Easing;
@@ -270,6 +270,8 @@ export class EasingCtrl {
270
270
  this.element.setAttribute("data-value", value.join(","));
271
271
  };
272
272
  this.name = config.name;
273
+ this.id = config.id || config.name;
274
+ this.group = config.group || "";
273
275
  this.label = config.label || config.name;
274
276
  this.value =
275
277
  config.defaultValue === undefined
@@ -8,6 +8,9 @@ export type RadioControlOptions = {
8
8
  };
9
9
  export declare class RadioCtrl implements Ctrl<string> {
10
10
  type: CtrlType;
11
+ htmlId: string;
12
+ id: string;
13
+ group?: string;
11
14
  name: string;
12
15
  label: string;
13
16
  value: string;
@@ -17,7 +20,6 @@ export declare class RadioCtrl implements Ctrl<string> {
17
20
  items: Option[];
18
21
  element: HTMLElement;
19
22
  columns: 1 | 2 | 3 | 4 | 5;
20
- id: string;
21
23
  constructor(config: ConfigFor<"radio">, onChange: CtrlChangeHandler<string>, onInput: CtrlChangeHandler<string>);
22
24
  parse: (string: string) => string;
23
25
  getRandomValue: () => string;
@@ -23,8 +23,8 @@ export class RadioCtrl {
23
23
  const inputs = items.map((item) => {
24
24
  const input = document.createElement("input");
25
25
  input.setAttribute("type", "radio");
26
- input.setAttribute("name", this.id);
27
- input.setAttribute("id", `${this.id}-${toKebabCase(item.value)}`);
26
+ input.setAttribute("name", this.htmlId);
27
+ input.setAttribute("id", `${this.htmlId}-${toKebabCase(item.value)}`);
28
28
  input.setAttribute("value", item.value);
29
29
  input.checked = item.value === value;
30
30
  input.addEventListener("change", () => {
@@ -77,8 +77,10 @@ export class RadioCtrl {
77
77
  });
78
78
  this.columns = config.columns || 3;
79
79
  this.name = config.name;
80
+ this.id = config.id || config.name;
81
+ this.group = config.group || "";
80
82
  this.label = config.label || config.name;
81
- this.id = `ctrls__${toKebabCase(config.name)}-${getRandomString()}`;
83
+ this.htmlId = `ctrls__${toKebabCase(config.name)}-${getRandomString()}`;
82
84
  const defaultValue = this.items.find((item) => item.value === config.defaultValue);
83
85
  this.value = defaultValue?.value || this.getDefaultValue();
84
86
  this.isRandomizationDisabled = config.isRandomizationDisabled || false;
@@ -1,6 +1,8 @@
1
1
  import type { Ctrl, CtrlChangeHandler, CtrlType, ConfigFor } from ".";
2
2
  export declare class RangeCtrl implements Ctrl<number> {
3
3
  type: CtrlType;
4
+ id: string;
5
+ group?: string;
4
6
  name: string;
5
7
  label: string;
6
8
  value: number;
@@ -73,6 +73,8 @@ export class RangeCtrl {
73
73
  this.element.style.setProperty("--gradient-position", `${percentage.toFixed(2)}%`);
74
74
  };
75
75
  this.name = config.name;
76
+ this.id = config.id || config.name;
77
+ this.group = config.group || "";
76
78
  this.label = config.label || config.name;
77
79
  this.isRandomizationDisabled = config.isRandomizationDisabled || false;
78
80
  this.onChange = onChange;
@@ -1,6 +1,8 @@
1
1
  import type { Ctrl, CtrlChangeHandler, CtrlConfig, CtrlType } from ".";
2
2
  export declare class SeedCtrl implements Ctrl<string> {
3
3
  type: CtrlType;
4
+ id: string;
5
+ group?: string;
4
6
  name: string;
5
7
  label: string;
6
8
  value: string;
@@ -66,6 +66,8 @@ export class SeedCtrl {
66
66
  this.input.value = value;
67
67
  };
68
68
  this.name = config.name;
69
+ this.id = config.id || config.name;
70
+ this.group = config.group || "";
69
71
  this.label = config.label || config.name;
70
72
  this.value =
71
73
  config.defaultValue === undefined
@@ -8,16 +8,20 @@ import { SeedCtrl } from "./ctrl-seed";
8
8
  export interface PRNG {
9
9
  (): number;
10
10
  }
11
- export type CtrlType = "boolean" | "range" | "radio" | "seed" | "easing" | "dual-range";
11
+ export type CtrlType = "boolean" | "range" | "radio" | "seed" | "easing" | "dual-range" | "group";
12
12
  export type CtrlChangeHandler<T> = (name: string, value: T) => void;
13
13
  export type CtrlConfig<T = unknown> = {
14
14
  type: CtrlType;
15
+ id?: string;
15
16
  name: string;
17
+ group?: string;
16
18
  label?: string;
17
19
  defaultValue?: T;
18
20
  isRandomizationDisabled?: boolean;
19
21
  };
20
22
  export interface Ctrl<T> {
23
+ id: string;
24
+ group?: string;
21
25
  name: string;
22
26
  label: string;
23
27
  type: CtrlType;
@@ -59,23 +63,44 @@ export interface CtrlTypeMap {
59
63
  max: number;
60
64
  step?: number;
61
65
  };
66
+ group: {
67
+ value: Record<string, unknown>;
68
+ controls: readonly ConfigItem[];
69
+ isRandomizationDisabled?: boolean;
70
+ };
62
71
  }
63
72
  export type TypedControlConfig = {
64
73
  [K in CtrlType]: {
65
74
  type: K;
75
+ id?: string;
66
76
  name: string;
77
+ group?: string;
67
78
  label?: string;
68
79
  defaultValue?: CtrlTypeMap[K]["value"];
69
80
  isRandomizationDisabled?: boolean;
70
81
  } & Omit<CtrlTypeMap[K], "value">;
71
82
  }[CtrlType];
83
+ export type GroupConfig = {
84
+ type: "group";
85
+ name: string;
86
+ label?: string;
87
+ controls: readonly TypedControlConfig[];
88
+ isRandomizationDisabled?: boolean;
89
+ };
90
+ export type ConfigItem = TypedControlConfig | GroupConfig;
72
91
  export type ConfigFor<T extends CtrlType> = Extract<TypedControlConfig, {
73
92
  type: T;
74
93
  }>;
75
- type ExtractValues<Configs extends readonly TypedControlConfig[]> = {
76
- [C in Configs[number] as C["name"]]: CtrlTypeMap[C["type"]]["value"];
94
+ type ExtractValues<Configs extends readonly ConfigItem[]> = {
95
+ [C in Extract<Configs[number], {
96
+ type: Exclude<CtrlType, "group">;
97
+ }> as C["name"]]: CtrlTypeMap[C["type"]]["value"];
98
+ } & {
99
+ [C in Extract<Configs[number], {
100
+ type: "group";
101
+ }> as C["name"]]: OptionsMap<C["controls"]>;
77
102
  };
78
- type DerivedProps<Configs extends readonly TypedControlConfig[]> = {
103
+ type DerivedProps<Configs extends readonly ConfigItem[]> = {
79
104
  [C in Extract<Configs[number], {
80
105
  type: "easing";
81
106
  }> as `${C["name"]}Easing`]: ReturnType<typeof BezierEasing>;
@@ -83,8 +108,12 @@ type DerivedProps<Configs extends readonly TypedControlConfig[]> = {
83
108
  [C in Extract<Configs[number], {
84
109
  type: "seed";
85
110
  }> as `${C["name"]}Rng`]: PRNG;
111
+ } & {
112
+ [C in Extract<Configs[number], {
113
+ type: "group";
114
+ }> as C["name"]]: DerivedProps<C["controls"]>;
86
115
  };
87
- type OptionsMap<Configs extends readonly TypedControlConfig[]> = ExtractValues<Configs> & DerivedProps<Configs>;
116
+ type OptionsMap<Configs extends readonly ConfigItem[]> = ExtractValues<Configs> & DerivedProps<Configs>;
88
117
  type ControlsOptions = {
89
118
  showRandomizeButton?: boolean;
90
119
  storage?: "hash" | "none";
@@ -93,20 +122,22 @@ type ControlsOptions = {
93
122
  title?: string;
94
123
  };
95
124
  type CtrlComponent = BooleanCtrl | RangeCtrl | RadioCtrl | SeedCtrl | EasingCtrl | DualRangeCtrl;
96
- export declare class Ctrls<Configs extends readonly TypedControlConfig[]> {
125
+ export declare class Ctrls<Configs extends readonly ConfigItem[]> {
97
126
  options: ControlsOptions;
98
127
  controls: CtrlComponent[];
99
128
  controlsMap: Record<string, CtrlComponent>;
100
129
  element: HTMLDivElement;
101
130
  onChange?: (updatedValues: Partial<ReturnType<typeof this.getValues>>) => void;
102
131
  onInput?: (updatedValues: Partial<ReturnType<typeof this.getValues>>) => void;
103
- constructor(controls: Configs, options?: ControlsOptions);
132
+ constructor(configs: Configs, options?: ControlsOptions);
133
+ registerControl: (config: TypedControlConfig, onChangeControlHandler: (name: string, value: unknown) => void, onInputControlHandler: (name: string, value: unknown) => void, group?: string) => void;
104
134
  buildUI: () => HTMLDivElement;
105
135
  toggleVisibility: () => void;
106
136
  addHashListeners: () => void;
107
137
  getHash: () => string;
108
138
  setHash: () => void;
109
139
  updateFromHash: () => void;
140
+ updateValuesObject(options: any, control: CtrlComponent): void;
110
141
  getValues(): OptionsMap<Configs>;
111
142
  randomize: () => void;
112
143
  }
@@ -17,16 +17,65 @@ const controlMap = {
17
17
  "dual-range": DualRangeCtrl,
18
18
  };
19
19
  export class Ctrls {
20
- constructor(controls, options) {
20
+ constructor(configs, options) {
21
+ this.controls = [];
21
22
  this.controlsMap = {};
23
+ this.registerControl = (config, onChangeControlHandler, onInputControlHandler, group = "") => {
24
+ // To make typescript happy
25
+ if (config.type === "group") {
26
+ return;
27
+ }
28
+ // TODO
29
+ // Again, document as it is my personal preference
30
+ if (!config.label) {
31
+ config.label = toSpaceCase(config.name);
32
+ }
33
+ // TODO
34
+ // Document this behaviour
35
+ // This might counter-intuitive for some people,
36
+ // but it is my personal preference to have properties named in camel case
37
+ // when using them in code
38
+ //
39
+ // However, they are going to be converted to kebab case when used in the hash,
40
+ // because it is nicer that URL be all lowercase
41
+ config.name = toCamelCase(config.name);
42
+ if (group) {
43
+ config.id = toCamelCase(`${group}-${config.name}`);
44
+ }
45
+ const ControlComponent = controlMap[config.type];
46
+ const control = new ControlComponent(config, onChangeControlHandler, onInputControlHandler);
47
+ control.group = group;
48
+ this.controlsMap[control.id] = control;
49
+ this.controls.push(control);
50
+ };
22
51
  this.buildUI = () => {
23
52
  const element = document.createElement("div");
24
53
  element.classList.add("ctrls");
25
54
  element.classList.add(`ctrls--${this.options.theme}-theme`);
26
55
  const controlsContainer = document.createElement("div");
27
56
  controlsContainer.classList.add("ctrls__controls");
57
+ let group = "";
58
+ let groupElement;
28
59
  this.controls.forEach((control) => {
29
- controlsContainer.appendChild(control.element);
60
+ if (control.group) {
61
+ if (control.group !== group) {
62
+ group = control.group;
63
+ groupElement = document.createElement("div");
64
+ groupElement.classList.add("ctrls__group");
65
+ const groupTitle = document.createElement("button");
66
+ groupTitle.classList.add("ctrls__group-title");
67
+ groupTitle.innerText = control.group;
68
+ groupTitle.addEventListener("click", () => {
69
+ groupTitle.parentElement?.classList.toggle("ctrls__group--hidden");
70
+ });
71
+ groupElement.append(groupTitle);
72
+ controlsContainer.appendChild(groupElement);
73
+ }
74
+ groupElement.append(control.element);
75
+ }
76
+ else {
77
+ controlsContainer.appendChild(control.element);
78
+ }
30
79
  });
31
80
  if (this.options.showRandomizeButton) {
32
81
  const randomizeButton = document.createElement("button");
@@ -59,7 +108,7 @@ export class Ctrls {
59
108
  this.getHash = () => {
60
109
  const values = this.controls
61
110
  .map((control) => {
62
- return `${toKebabCase(control.name)}:${control.valueToString()}`;
111
+ return `${toKebabCase(control.id)}:${control.valueToString()}`;
63
112
  })
64
113
  .join("/");
65
114
  return `#/${values}`;
@@ -130,25 +179,15 @@ export class Ctrls {
130
179
  const onInputControlHandler = (name, value) => {
131
180
  this.onInput?.({ [name]: value });
132
181
  };
133
- this.controls = controls.map((config) => {
134
- // TODO
135
- // Document this behaviour
136
- // This might counter-intuitive for some people,
137
- // but it is my personal preference to have properties named in camel case
138
- // when using them in code
139
- //
140
- // However, they are going to be converted to kebab case when used in the hash,
141
- // because it is nicer that URL be all lowercase
142
- config.name = toCamelCase(config.name);
143
- // TODO
144
- // Again, document as it is my personal preference
145
- if (!config.label) {
146
- config.label = toSpaceCase(config.name);
182
+ configs.map((config) => {
183
+ if (config.type === "group") {
184
+ config.controls.forEach((groupConfig) => {
185
+ this.registerControl(groupConfig, onChangeControlHandler, onInputControlHandler, toCamelCase(config.name));
186
+ });
187
+ }
188
+ else {
189
+ this.registerControl(config, onChangeControlHandler, onInputControlHandler);
147
190
  }
148
- const ControlComponent = controlMap[config.type];
149
- const control = new ControlComponent(config, onChangeControlHandler, onInputControlHandler);
150
- this.controlsMap[control.name] = control;
151
- return control;
152
191
  });
153
192
  this.element = this.buildUI();
154
193
  if (this.options.storage === "hash") {
@@ -158,15 +197,26 @@ export class Ctrls {
158
197
  this.options.parent.appendChild(this.element);
159
198
  }
160
199
  }
200
+ updateValuesObject(options, control) {
201
+ options[control.name] = control.value;
202
+ if (control.type === "easing") {
203
+ options[control.name + "Easing"] = BezierEasing(...control.value);
204
+ }
205
+ else if (control.type === "seed") {
206
+ options[control.name + "Rng"] = Alea(...control.value.split("-"));
207
+ }
208
+ }
161
209
  getValues() {
162
210
  const options = {};
163
211
  this.controls.forEach((control) => {
164
- options[control.name] = control.value;
165
- if (control.type === "easing") {
166
- options[control.name + "Easing"] = BezierEasing(...control.value);
212
+ if (control.group) {
213
+ if (!options[control.group]) {
214
+ options[control.group] = {};
215
+ }
216
+ this.updateValuesObject(options[control.group], control);
167
217
  }
168
- else if (control.type === "seed") {
169
- options[control.name + "Rng"] = Alea(control.value);
218
+ else {
219
+ this.updateValuesObject(options, control);
170
220
  }
171
221
  });
172
222
  return options;
package/dist/ctrls.css CHANGED
@@ -170,8 +170,8 @@
170
170
  ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas,
171
171
  "DejaVu Sans Mono", monospace;
172
172
  --ctrls-radius: 4px;
173
- --ctrls-label-width: 5rem;
174
- --ctrls-width: 22rem;
173
+ --ctrls-label-width: 7rem;
174
+ --ctrls-width: 24rem;
175
175
  --ctrls-font-size: 0.75rem;
176
176
  --ctrls-value-font-size: 0.6875rem;
177
177
  --ctrls-c: 0.25;
@@ -301,6 +301,7 @@
301
301
  overflow: visible;
302
302
  }
303
303
 
304
+ .ctrls__group-title,
304
305
  .ctrls__title {
305
306
  background: none;
306
307
  border: none;
@@ -312,24 +313,37 @@
312
313
  cursor: pointer;
313
314
  transition: color 300ms, background-color 300ms;
314
315
  }
315
- .ctrls__title:focus-visible, .ctrls__title:hover {
316
+ .ctrls__group-title:focus-visible, .ctrls__group-title:hover,
317
+ .ctrls__title:focus-visible,
318
+ .ctrls__title:hover {
316
319
  color: var(--ctrls-theme);
317
320
  background: var(--ctrls-btn-hover-bg);
318
321
  }
319
322
 
323
+ .ctrls__title {
324
+ border-block-end: 1px solid var(--ctrls-border);
325
+ }
326
+
320
327
  .ctrls__controls {
321
328
  display: grid;
322
- gap: 0.5rem;
323
- padding: 0.5rem;
329
+ padding-top: 0.5rem;
324
330
  overflow: auto;
325
331
  scrollbar-width: thin;
326
332
  scrollbar-color: var(--ctrls-scrollbar-thumb-bg) transparent;
327
333
  }
328
334
 
335
+ .ctrls__controls > * {
336
+ margin-bottom: 0.5rem;
337
+ }
338
+
329
339
  .ctrls--hidden .ctrls__controls {
330
340
  display: none;
331
341
  }
332
342
 
343
+ .ctrls--hidden .ctrls__title {
344
+ border-block-end: none;
345
+ }
346
+
333
347
  /* ----- Buttons ----- */
334
348
  .ctrls__btn {
335
349
  background: none;
@@ -355,7 +369,8 @@
355
369
  }
356
370
 
357
371
  .ctrls__btn--lg {
358
- margin-left: var(--ctrls-label-width);
372
+ margin-inline-start: calc(var(--ctrls-label-width) + 0.5rem);
373
+ margin-inline-end: 0.5rem;
359
374
  padding: 0.5rem 1rem;
360
375
  background: var(--ctrls-btn-bg);
361
376
  border: 1px solid var(--ctrls-input-border);
@@ -375,8 +390,39 @@
375
390
  opacity: 0;
376
391
  }
377
392
 
378
- /* ----- General ----- */
393
+ /* ----- Groups ----- */
394
+ .ctrls__group {
395
+ display: grid;
396
+ gap: 0.5rem;
397
+ padding-block-end: 0.5rem;
398
+ border-block: 1px solid var(--ctrls-border);
399
+ }
400
+
401
+ .ctrls__group .ctrls__control {
402
+ padding-inline-start: 1rem;
403
+ grid-template-columns: calc(var(--ctrls-label-width) - 0.5rem) minmax(0, 1fr);
404
+ }
405
+
406
+ .ctrls__group:has(+ .ctrls__group) {
407
+ border-block-end: none;
408
+ margin-block-end: 0;
409
+ }
410
+
411
+ .ctrls__group-title {
412
+ text-align: left;
413
+ }
414
+
415
+ .ctrls__group--hidden {
416
+ padding-block-end: 0;
417
+ }
418
+
419
+ .ctrls__group--hidden .ctrls__control {
420
+ display: none;
421
+ }
422
+
423
+ /* ----- Controls ----- */
379
424
  .ctrls__control {
425
+ padding-inline: 0.5rem;
380
426
  display: grid;
381
427
  grid-template-columns: var(--ctrls-label-width) minmax(0, 1fr);
382
428
  align-items: center;
@@ -1 +1 @@
1
- {"version":3,"sourceRoot":"","sources":["../node_modules/@stanko/dual-range-input/dist/index.css","../src/scss/_ctrls.scss"],"names":[],"mappings":"AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;;AC3IF;EACE;AAAA;AAAA;EAGA;EACA;EACA;EACA;EACA;EAGA;EACA;EAEA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EAGA;EAGA;EACA;EACA;EAGA;EACA;EAGA;EAGA;EAEA;EACA;EAEA;EACA;EAGA;EAGA;EACA;EAEA;EACA;EAEA;EACA;EACA;EACA;EAGA;EACA;EAEA;EAEA;EACA;EAEA;EAGA;EAEA;EACA;;;AA+CF;EACE;IA3CA;IAEA;IACA;IAGA;IACA;IACA;IACA;IACA;IACA;IAGA;IAGA;IACA;IACA;IAGA;IACA;IAGA;IAGA;IAEA;IAGA;IAGA;AAAA;AAAA;;;AAWF;EAhDE;EAEA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EAGA;EAGA;EACA;EACA;EAGA;EACA;EAGA;EAGA;EAEA;EAGA;EAGA;AAAA;AAAA;;;AAiBF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;AAAA;EAGE;EACA;EACA;;AAGF;AAAA;EAEE;EACA;EACA;;AAGF;EACE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YACE;;AAGF;EAEE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;AAGA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YACE;;AAGF;EAEE;;AAGF;EACE;EACA;EACA,YACE;;AAIJ;EACE;;;AAKJ;EACE;EACA;EACA;EACA;EACA;;AAEA;EAEE;EACA;;;AAOA;EACE;;AAGF;AAAA;AAAA;EAEE;;;AAKN;AAEA;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAKA;AAAA;EACE;EACA;EACA;;;AAIJ;AAEA;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;;AAKJ;EACE;EACA;EACA;;;AAIF;EACE;;;AAIF;EACE;;;AAIF;EACE;EACA;;;AAGF;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAmCJ;EA5BE;EACA;EACA;EACA;EACA;EAEA;;;AA0BF;EAhCE;EACA;EACA;EACA;EACA;EAEA;;;AA8BF;EAtBE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAkBA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAKF;EAvDE;EACA;EACA;EACA;EACA;EAEA;;;AAqDF;EA7CE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAwCA;EACA;;;AAGF;EACE;EACA;;;AAGF;AAEA;EAGE;;AAEA;EACE;;AANJ;EASE;EACA;EAEA;EACA;EACA;EAEA;EACA;EACA;EACA;EAEA;EACA;EACA;;AAEA;EACE;;;AAIJ;AAEA;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YACE;EAEF;EACA;EACA;;AAEA;EAEE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;AAEA;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;;AAIE;EACE;;;AAKN;AAEA;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;AAEA;AAAA;EAEE;EACA;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YACE;;AAGF;EAEE;EACA;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EAEE;;AAEA;EACE;EACA","file":"ctrls.css"}
1
+ {"version":3,"sourceRoot":"","sources":["../node_modules/@stanko/dual-range-input/dist/index.css","../src/scss/_ctrls.scss"],"names":[],"mappings":"AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;;AC3IF;EACE;AAAA;AAAA;EAGA;EACA;EACA;EACA;EACA;EAGA;EACA;EAEA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EAGA;EAGA;EACA;EACA;EAGA;EACA;EAGA;EAGA;EAEA;EACA;EAEA;EACA;EAGA;EAGA;EACA;EAEA;EACA;EAEA;EACA;EACA;EACA;EAGA;EACA;EAEA;EAEA;EACA;EAEA;EAGA;EAEA;EACA;;;AA+CF;EACE;IA3CA;IAEA;IACA;IAGA;IACA;IACA;IACA;IACA;IACA;IAGA;IAGA;IACA;IACA;IAGA;IACA;IAGA;IAGA;IAEA;IAGA;IAGA;AAAA;AAAA;;;AAWF;EAhDE;EAEA;EACA;EAGA;EACA;EACA;EACA;EACA;EACA;EAGA;EAGA;EACA;EACA;EAGA;EACA;EAGA;EAGA;EAEA;EAGA;EAGA;AAAA;AAAA;;;AAiBF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;AAAA;AAAA;EAGE;EACA;EACA;;AAGF;AAAA;EAEE;EACA;EACA;;AAGF;EACE;EACA;;;AAIJ;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YACE;;AAGF;AAAA;AAAA;EAEE;EACA;;;AAIJ;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;AAGA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YACE;;AAGF;EAEE;;AAGF;EACE;EACA;EACA,YACE;;AAIJ;EACE;;;AAKJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EAEE;EACA;;;AAOA;EACE;;AAGF;AAAA;AAAA;EAEE;;;AAKN;AAEA;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;AAEA;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAKA;AAAA;EACE;EACA;EACA;;;AAIJ;AAEA;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;;AAKJ;EACE;EACA;EACA;;;AAIF;EACE;;;AAIF;EACE;;;AAIF;EACE;EACA;;;AAGF;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAmCJ;EA5BE;EACA;EACA;EACA;EACA;EAEA;;;AA0BF;EAhCE;EACA;EACA;EACA;EACA;EAEA;;;AA8BF;EAtBE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAkBA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAKF;EAvDE;EACA;EACA;EACA;EACA;EAEA;;;AAqDF;EA7CE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAwCA;EACA;;;AAGF;EACE;EACA;;;AAGF;AAEA;EAGE;;AAEA;EACE;;AANJ;EASE;EACA;EAEA;EACA;EACA;EAEA;EACA;EACA;EACA;EAEA;EACA;EACA;;AAEA;EACE;;;AAIJ;AAEA;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YACE;EAEF;EACA;EACA;;AAEA;EAEE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;AAEA;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;;AAIE;EACE;;;AAKN;AAEA;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;AAEA;AAAA;EAEE;EACA;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,YACE;;AAGF;EAEE;EACA;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EAEE;;AAEA;EACE;EACA","file":"ctrls.css"}
@@ -1,2 +1,2 @@
1
- declare const Alea: (seed?: string) => (() => number);
1
+ declare const Alea: (...seeds: string[]) => (() => number);
2
2
  export default Alea;
@@ -18,19 +18,21 @@ const getMash = () => {
18
18
  };
19
19
  return mash;
20
20
  };
21
- const Alea = (seed = Date.now().toString()) => {
22
- let s = [0, 0, 0];
21
+ const Alea = (...seeds) => {
22
+ const mash = getMash();
23
+ const s = [mash(" "), mash(" "), mash(" ")];
23
24
  let c = 1;
24
- let mash = getMash();
25
- s.forEach((_, i) => {
26
- s[i] = mash(" ") - mash(seed);
27
- if (s[i] < 0) {
28
- s[i] += 1;
29
- }
25
+ seeds.forEach((seed) => {
26
+ s.forEach((_, i) => {
27
+ s[i] -= mash(seed);
28
+ if (s[i] < 0) {
29
+ s[i] += 1;
30
+ }
31
+ });
30
32
  });
31
33
  const random = () => {
32
34
  const t = 2091639 * s[0] + c * 2.3283064365386963e-10; // 2^-32
33
- c = t | 0;
35
+ c = t | 0; // quicker floor
34
36
  s[0] = s[1];
35
37
  s[1] = s[2];
36
38
  s[2] = t - c;
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@stanko/ctrls",
3
- "version": "0.1.9",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
+ "test": "vitest",
6
7
  "start": "npm run parse-markdown && vite",
7
8
  "build": "npm run parse-markdown && tsc && rm -rf ./docs && vite build && touch ./docs/.nojekyll",
8
9
  "build-css": "sass ./src/scss/index.scss ./dist/ctrls.css",
@@ -24,7 +25,8 @@
24
25
  "sass": "^1.92.1",
25
26
  "simplex-noise": "^4.0.3",
26
27
  "typescript": "~5.9.2",
27
- "vite": "^7.1.5"
28
+ "vite": "^7.1.5",
29
+ "vitest": "^4.0.9"
28
30
  },
29
31
  "dependencies": {
30
32
  "@stanko/dual-range-input": "^1.0.1",