@stanko/ctrls 0.1.8 → 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
@@ -1,5 +1,5 @@
1
1
  import DualRangeInput from "@stanko/dual-range-input";
2
- import type { Ctrl, CtrlChangeHandler, CtrlType, CtrlTypeRegistry } from ".";
2
+ import type { Ctrl, CtrlChangeHandler, CtrlType, ConfigFor } from ".";
3
3
  export type DualRangeControlOptions = {
4
4
  min: number;
5
5
  max: number;
@@ -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;
@@ -25,7 +27,7 @@ export declare class DualRangeCtrl implements Ctrl<DualRangeValue> {
25
27
  maxInput: HTMLInputElement;
26
28
  dualRange: DualRangeInput;
27
29
  valueSpan: HTMLSpanElement;
28
- constructor(config: CtrlTypeRegistry["dual-range"]["config"], onChange: CtrlChangeHandler<DualRangeValue>, onInput: CtrlChangeHandler<DualRangeValue>);
30
+ constructor(config: ConfigFor<"dual-range">, onChange: CtrlChangeHandler<DualRangeValue>, onInput: CtrlChangeHandler<DualRangeValue>);
29
31
  parse: (string: string) => {
30
32
  min: number;
31
33
  max: number;
@@ -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;
@@ -1,7 +1,9 @@
1
- import type { Ctrl, CtrlChangeHandler, CtrlType, CtrlTypeRegistry } from ".";
1
+ 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;
@@ -15,7 +17,7 @@ export declare class EasingCtrl implements Ctrl<Easing> {
15
17
  lines: SVGLineElement[];
16
18
  path: SVGPathElement;
17
19
  presets: Record<string, Easing>;
18
- constructor(config: CtrlTypeRegistry["easing"]["config"], onChange: CtrlChangeHandler<Easing>, onInput: CtrlChangeHandler<Easing>);
20
+ constructor(config: ConfigFor<"easing">, onChange: CtrlChangeHandler<Easing>, onInput: CtrlChangeHandler<Easing>);
19
21
  parse: (string: string) => Easing;
20
22
  getRandomValue: () => Easing;
21
23
  getDefaultValue: () => 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
@@ -1,4 +1,4 @@
1
- import type { Ctrl, CtrlChangeHandler, CtrlType, CtrlTypeRegistry } from ".";
1
+ import type { Ctrl, CtrlChangeHandler, CtrlType, ConfigFor } from ".";
2
2
  type Option = {
3
3
  label: string;
4
4
  value: string;
@@ -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,8 +20,7 @@ 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
- constructor(config: CtrlTypeRegistry["radio"]["config"], onChange: CtrlChangeHandler<string>, onInput: CtrlChangeHandler<string>);
23
+ constructor(config: ConfigFor<"radio">, onChange: CtrlChangeHandler<string>, onInput: CtrlChangeHandler<string>);
22
24
  parse: (string: string) => string;
23
25
  getRandomValue: () => string;
24
26
  getDefaultValue: () => 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
- import type { Ctrl, CtrlChangeHandler, CtrlType, CtrlTypeRegistry } from ".";
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;
@@ -13,7 +15,7 @@ export declare class RangeCtrl implements Ctrl<number> {
13
15
  element: HTMLElement;
14
16
  input: HTMLInputElement;
15
17
  valueSpan: HTMLSpanElement;
16
- constructor(config: CtrlTypeRegistry["range"]["config"], onChange: CtrlChangeHandler<number>, onInput: CtrlChangeHandler<number>);
18
+ constructor(config: ConfigFor<"range">, onChange: CtrlChangeHandler<number>, onInput: CtrlChangeHandler<number>);
17
19
  parse: (string: string) => number;
18
20
  getRandomValue: () => number;
19
21
  getDefaultValue: () => 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;
@@ -31,57 +35,72 @@ export interface Ctrl<T> {
31
35
  update: (value: T) => void;
32
36
  element: HTMLElement;
33
37
  }
34
- export type CtrlComponent = BooleanCtrl | RangeCtrl | RadioCtrl | SeedCtrl | EasingCtrl | DualRangeCtrl;
35
- export type ControlConstructor<T> = new (...args: any[]) => T;
36
- export interface CtrlTypeRegistry {
38
+ export interface CtrlTypeMap {
37
39
  boolean: {
38
- config: CtrlConfig<boolean>;
39
- instance: BooleanCtrl;
40
40
  value: boolean;
41
41
  };
42
42
  range: {
43
- config: CtrlConfig<number> & {
44
- min: number;
45
- max: number;
46
- step?: number;
47
- };
48
- instance: RangeCtrl;
49
43
  value: number;
44
+ min: number;
45
+ max: number;
46
+ step?: number;
50
47
  };
51
48
  radio: {
52
- config: CtrlConfig<string> & {
53
- items: Record<string, string>;
54
- columns?: 1 | 2 | 3 | 4 | 5;
55
- };
56
- instance: RadioCtrl;
57
49
  value: string;
50
+ items: Record<string, string>;
51
+ columns?: 1 | 2 | 3 | 4 | 5;
58
52
  };
59
53
  seed: {
60
- config: CtrlConfig<string>;
61
- instance: SeedCtrl;
62
54
  value: string;
63
55
  };
64
56
  easing: {
65
- config: CtrlConfig<Easing> & {
66
- presets?: Record<string, Easing>;
67
- };
68
- instance: EasingCtrl;
69
57
  value: Easing;
58
+ presets?: Record<string, Easing>;
70
59
  };
71
60
  "dual-range": {
72
- config: CtrlConfig<DualRangeValue> & {
73
- min: number;
74
- max: number;
75
- step?: number;
76
- };
77
- instance: DualRangeCtrl;
78
61
  value: DualRangeValue;
62
+ min: number;
63
+ max: number;
64
+ step?: number;
65
+ };
66
+ group: {
67
+ value: Record<string, unknown>;
68
+ controls: readonly ConfigItem[];
69
+ isRandomizationDisabled?: boolean;
79
70
  };
80
71
  }
81
- export type TypedControlConfig = CtrlTypeRegistry[keyof CtrlTypeRegistry]["config"];
82
- type OptionsMap<Configs extends readonly TypedControlConfig[]> = {
83
- [C in Configs[number] as C["name"]]: CtrlTypeRegistry[C["type"]]["value"];
72
+ export type TypedControlConfig = {
73
+ [K in CtrlType]: {
74
+ type: K;
75
+ id?: string;
76
+ name: string;
77
+ group?: string;
78
+ label?: string;
79
+ defaultValue?: CtrlTypeMap[K]["value"];
80
+ isRandomizationDisabled?: boolean;
81
+ } & Omit<CtrlTypeMap[K], "value">;
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;
91
+ export type ConfigFor<T extends CtrlType> = Extract<TypedControlConfig, {
92
+ type: T;
93
+ }>;
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"];
84
98
  } & {
99
+ [C in Extract<Configs[number], {
100
+ type: "group";
101
+ }> as C["name"]]: OptionsMap<C["controls"]>;
102
+ };
103
+ type DerivedProps<Configs extends readonly ConfigItem[]> = {
85
104
  [C in Extract<Configs[number], {
86
105
  type: "easing";
87
106
  }> as `${C["name"]}Easing`]: ReturnType<typeof BezierEasing>;
@@ -89,7 +108,12 @@ type OptionsMap<Configs extends readonly TypedControlConfig[]> = {
89
108
  [C in Extract<Configs[number], {
90
109
  type: "seed";
91
110
  }> as `${C["name"]}Rng`]: PRNG;
111
+ } & {
112
+ [C in Extract<Configs[number], {
113
+ type: "group";
114
+ }> as C["name"]]: DerivedProps<C["controls"]>;
92
115
  };
116
+ type OptionsMap<Configs extends readonly ConfigItem[]> = ExtractValues<Configs> & DerivedProps<Configs>;
93
117
  type ControlsOptions = {
94
118
  showRandomizeButton?: boolean;
95
119
  storage?: "hash" | "none";
@@ -97,20 +121,23 @@ type ControlsOptions = {
97
121
  parent?: Element;
98
122
  title?: string;
99
123
  };
100
- export declare class Ctrls<Configs extends readonly TypedControlConfig[]> {
124
+ type CtrlComponent = BooleanCtrl | RangeCtrl | RadioCtrl | SeedCtrl | EasingCtrl | DualRangeCtrl;
125
+ export declare class Ctrls<Configs extends readonly ConfigItem[]> {
101
126
  options: ControlsOptions;
102
- controls: CtrlTypeRegistry[keyof CtrlTypeRegistry]["instance"][];
103
- controlsMap: Record<string, CtrlTypeRegistry[keyof CtrlTypeRegistry]["instance"]>;
127
+ controls: CtrlComponent[];
128
+ controlsMap: Record<string, CtrlComponent>;
104
129
  element: HTMLDivElement;
105
130
  onChange?: (updatedValues: Partial<ReturnType<typeof this.getValues>>) => void;
106
131
  onInput?: (updatedValues: Partial<ReturnType<typeof this.getValues>>) => void;
107
- 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;
108
134
  buildUI: () => HTMLDivElement;
109
135
  toggleVisibility: () => void;
110
136
  addHashListeners: () => void;
111
137
  getHash: () => string;
112
138
  setHash: () => void;
113
139
  updateFromHash: () => void;
140
+ updateValuesObject(options: any, control: CtrlComponent): void;
114
141
  getValues(): OptionsMap<Configs>;
115
142
  randomize: () => void;
116
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;
@@ -1,9 +1,11 @@
1
+ import Alea from "./alea";
1
2
  import random from "./random";
2
3
  import { words } from "./words";
4
+ const rng = Alea();
3
5
  export default function generateSeed() {
4
6
  return [1, 2, 3]
5
7
  .map(() => {
6
- const index = random(0, words.length - 1, Math.random, 0);
8
+ const index = random(0, words.length - 1, rng, 0);
7
9
  return words[index];
8
10
  })
9
11
  .join("-");
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@stanko/ctrls",
3
- "version": "0.1.8",
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",