@stanko/ctrls 0.3.4 → 0.4.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
@@ -29,7 +29,7 @@ Define controls as an array, then instantiate the `Ctrls` class:
29
29
 
30
30
  ```ts
31
31
  import { Ctrls } from "@stanko/ctrls";
32
- import type { TypedControlConfig } from "@stanko/ctrls";
32
+ import type { ConfigItem } from "@stanko/ctrls";
33
33
 
34
34
  // Import CSS
35
35
  import "@stanko/ctrls/dist/ctrls.css";
@@ -58,7 +58,7 @@ const config = [
58
58
  // - speed, shape and size
59
59
 
60
60
  // Casting the config to enable editor's code completion
61
- ] as const satisfies readonly TypedControlConfig[];
61
+ ] as const satisfies readonly ConfigItem[];
62
62
 
63
63
  // Create the instance of Ctrls
64
64
  export const options = new Ctrls(config, {
@@ -138,11 +138,20 @@ All controls share the following properties:
138
138
  ```ts
139
139
  {
140
140
  // --- Mandatory --- //
141
- type: CtrlType; // "seed" | "easing" | "boolean" | "range" | "dual-range" | "radio"
141
+ type: CtrlType; // "seed" | "easing" | "boolean" | "range" | "dual-range" | "radio" | "group"
142
+ // Name of the component.
143
+ //
144
+ // It will be converted to camel case and used in the values object
145
+ // This might counter-intuitive for some people,
146
+ // but it is my personal preference to have properties named in camel case when using them in code
147
+ //
148
+ // However, names are going to be converted to kebab case when used in the hash,
149
+ // because it is nicer that URL be all lowercase
142
150
  name: string;
143
151
 
144
152
  // --- Optional --- //
145
- // If passed, it will be used instead of the name
153
+ // If passed, it will be used instead of the name,
154
+ // if not, name will be be converted to space case and used as a label.
146
155
  label?: string;
147
156
  // Depends on the control type, the default value for the control
148
157
  defaultValue?: T;
@@ -332,6 +341,31 @@ Example:
332
341
 
333
342
  </div>
334
343
 
344
+ ### File
345
+
346
+ File input.
347
+
348
+ ```ts
349
+ {
350
+ // Optional
351
+ accept?: string; // a comma separated list of allowed file types
352
+ }
353
+ ```
354
+
355
+ Example:
356
+
357
+ <div class="example">
358
+
359
+ ```json
360
+ {
361
+ "type": "file",
362
+ "name": "background",
363
+ "accept": "image/*"
364
+ }
365
+ ```
366
+
367
+ </div>
368
+
335
369
  ### Group
336
370
 
337
371
  Collapsible group of controls. All values are going to be nested in an object using the group's name.
@@ -339,7 +373,10 @@ Collapsible group of controls. All values are going to be nested in an object us
339
373
  ```ts
340
374
  {
341
375
  // Mandatory
342
- controls: ConfigItem[]
376
+ controls: ConfigItem[];
377
+
378
+ // Optional
379
+ isCollapsed?: boolean;
343
380
  }
344
381
  ```
345
382
 
@@ -366,6 +403,42 @@ Example:
366
403
 
367
404
  </div>
368
405
 
406
+ ### HTML
407
+
408
+ Custom HTML to be rendered. You'll need to create your element and add it to the config.
409
+
410
+ Doesn't influence the values object as it doesn't have a value.
411
+
412
+ ```ts
413
+ {
414
+ // Mandatory
415
+ html: HTMLElement;
416
+ }
417
+ ```
418
+
419
+ Example:
420
+
421
+ ```ts
422
+ const downloadButton = document.createElement('button');
423
+ downloadButton.innerHTML = "PNG";
424
+ // Add your styles
425
+ downloadButton.addEventListener('click', () => {
426
+ alert("Download button clicked");
427
+ })
428
+ ```
429
+
430
+ <div class="example">
431
+
432
+ ```json
433
+ {
434
+ "type": "html",
435
+ "name": "download",
436
+ "html": downloadButton
437
+ }
438
+ ```
439
+
440
+ </div>
441
+
369
442
  ## Theming
370
443
 
371
444
  Ctrls uses CSS variables for theming. There are many you can adjust, but I recommend starting with these four:
@@ -411,6 +484,9 @@ Thank you for stopping by! If you end up using Ctrls, please let me know, I woul
411
484
  * [ ] Allow users to pass a custom PRNG lib
412
485
  * [ ] Hash storage - check if there is an instance using hash storage already
413
486
  * [ ] Storage - local storage
487
+ * [ ] Check if `onInput` si triggered when it should
488
+ * [ ] Use dom helpers everywhere
489
+ * [x] `toHtmlId(this.name)` => `toHtmlId(this.id)`
414
490
  * [x] Add `name` and `id` to inputs
415
491
  * [x] TypeDef bug - easing presets should be optional
416
492
  * [x] Add title which collapses the controls
@@ -1,6 +1,6 @@
1
- import type { Ctrl, CtrlType, CtrlChangeHandler, CtrlConfig } from ".";
1
+ import type { Ctrl, CtrlItemType, CtrlChangeHandler, CtrlConfig } from "./types";
2
2
  export declare class BooleanCtrl implements Ctrl<boolean> {
3
- type: CtrlType;
3
+ type: CtrlItemType;
4
4
  id: string;
5
5
  group?: string;
6
6
  name: string;
@@ -16,7 +16,7 @@ export class BooleanCtrl {
16
16
  return value.toString();
17
17
  };
18
18
  this.buildUI = () => {
19
- const id = toHtmlId(this.name);
19
+ const id = toHtmlId(this.id);
20
20
  const input = document.createElement("input");
21
21
  input.classList.add("ctrls__boolean-input");
22
22
  input.setAttribute("type", "checkbox");
@@ -1,5 +1,5 @@
1
1
  import DualRangeInput from "@stanko/dual-range-input";
2
- import type { Ctrl, CtrlChangeHandler, CtrlType, ConfigFor } from ".";
2
+ import type { Ctrl, CtrlChangeHandler, CtrlItemType, ConfigFor } from "./types";
3
3
  export type DualRangeControlOptions = {
4
4
  min: number;
5
5
  max: number;
@@ -10,7 +10,7 @@ export type DualRangeValue = {
10
10
  max: number;
11
11
  };
12
12
  export declare class DualRangeCtrl implements Ctrl<DualRangeValue> {
13
- type: CtrlType;
13
+ type: CtrlItemType;
14
14
  id: string;
15
15
  group?: string;
16
16
  name: string;
@@ -29,7 +29,7 @@ export class DualRangeCtrl {
29
29
  };
30
30
  this.buildUI = () => {
31
31
  const { min, max, step, value } = this;
32
- const id = toHtmlId(this.name);
32
+ const id = toHtmlId(this.id);
33
33
  const changeHandler = () => {
34
34
  this.value = {
35
35
  min: parseFloat(minInput.value),
@@ -1,7 +1,7 @@
1
- import type { Ctrl, CtrlChangeHandler, CtrlType, ConfigFor } from ".";
1
+ import type { Ctrl, CtrlChangeHandler, CtrlItemType, ConfigFor } from "./types";
2
2
  export type Easing = [number, number, number, number];
3
3
  export declare class EasingCtrl implements Ctrl<Easing> {
4
- type: CtrlType;
4
+ type: CtrlItemType;
5
5
  id: string;
6
6
  group?: string;
7
7
  name: string;
@@ -52,7 +52,7 @@ export class EasingCtrl {
52
52
  };
53
53
  this.buildUI = () => {
54
54
  const { value } = this;
55
- const id = toHtmlId(this.name);
55
+ const id = toHtmlId(this.id);
56
56
  const line1 = document.createElementNS("http://www.w3.org/2000/svg", "line");
57
57
  line1.setAttribute("class", "ctrls__easing-line ctrls__easing-line--1");
58
58
  line1.setAttribute("x1", "0");
@@ -0,0 +1,27 @@
1
+ import type { Ctrl, CtrlChangeHandler, CtrlConfig, CtrlItemType } from "./types";
2
+ export declare class FileCtrl implements Ctrl<File | null> {
3
+ id: string;
4
+ type: CtrlItemType;
5
+ name: string;
6
+ label: string;
7
+ group: string;
8
+ value: File | null;
9
+ isRandomizationDisabled: boolean;
10
+ onChange: CtrlChangeHandler;
11
+ onInput: CtrlChangeHandler;
12
+ element: HTMLElement;
13
+ input: HTMLInputElement;
14
+ preview: HTMLDivElement;
15
+ accept: string;
16
+ constructor(config: CtrlConfig<File | null>, onChange: CtrlChangeHandler, onInput: CtrlChangeHandler);
17
+ buildUI: () => {
18
+ element: HTMLDivElement;
19
+ input: HTMLInputElement;
20
+ preview: HTMLDivElement;
21
+ };
22
+ update: (file: File | null) => void;
23
+ parse: () => null;
24
+ getRandomValue: () => null;
25
+ getDefaultValue: () => null;
26
+ valueToString: () => string;
27
+ }
@@ -0,0 +1,99 @@
1
+ import { toHtmlId } from "../utils/string-utils";
2
+ import { dom } from "./dom";
3
+ import { closeIcon } from "../utils/icons";
4
+ const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "webp", "svg+xml"];
5
+ export class FileCtrl {
6
+ constructor(config, onChange, onInput) {
7
+ this.type = "file";
8
+ this.buildUI = () => {
9
+ const id = toHtmlId(this.id);
10
+ const input = dom.input("ctrls__file-input", {
11
+ type: "file",
12
+ id: id,
13
+ name: id,
14
+ });
15
+ input.addEventListener("change", () => {
16
+ this.update(input.files ? input.files[0] : null);
17
+ this.onChange(this);
18
+ this.onInput(this);
19
+ });
20
+ const fakeInput = dom.label("ctrls__file-fake-input ctrls__btn ctrls__btn--sm", { for: id });
21
+ fakeInput.innerHTML = "select file";
22
+ if (this.accept) {
23
+ fakeInput.innerHTML += `<span>${this.accept}</span>`;
24
+ }
25
+ const clearButton = dom.button("ctrls__file-clear ctrls__btn");
26
+ clearButton.innerHTML = closeIcon;
27
+ clearButton.addEventListener("click", () => {
28
+ if (input.files) {
29
+ this.update(null);
30
+ this.onChange(this);
31
+ this.onInput(this);
32
+ }
33
+ });
34
+ const top = dom.div("ctrls__file-top");
35
+ top.append(input);
36
+ top.append(fakeInput);
37
+ top.append(clearButton);
38
+ const preview = dom.div("ctrls__file-preview");
39
+ const right = dom.div("ctrls__control-right");
40
+ right.append(top);
41
+ right.append(preview);
42
+ const label = dom.label("ctrls__control-label", {
43
+ for: id,
44
+ });
45
+ label.textContent = this.label;
46
+ const element = dom.div("ctrls__control ctrls__control--file");
47
+ element.appendChild(label);
48
+ element.appendChild(right);
49
+ return {
50
+ element,
51
+ input,
52
+ preview,
53
+ };
54
+ };
55
+ this.update = (file) => {
56
+ this.value = file;
57
+ if (file) {
58
+ const item = document.createElement("figure");
59
+ item.classList.add("ctrls__file-preview-item");
60
+ if (IMAGE_EXTENSIONS.includes(file.type.split("/")[1])) {
61
+ console.log(URL.createObjectURL(file), file);
62
+ const img = document.createElement("img");
63
+ img.src = URL.createObjectURL(file);
64
+ img.alt = file.name;
65
+ img.classList.add("ctrls__file-image");
66
+ item.appendChild(img);
67
+ }
68
+ const label = document.createElement("figcaption");
69
+ label.classList.add("ctrls__file-label");
70
+ label.textContent = file.name;
71
+ item.appendChild(label);
72
+ this.preview.replaceChildren(item);
73
+ }
74
+ else {
75
+ this.preview.innerHTML = "";
76
+ this.input.value = "";
77
+ }
78
+ };
79
+ // Files can't be preserved in the URL hash,
80
+ // so these are only placeholders to satisfy the interface
81
+ this.parse = () => null;
82
+ this.getRandomValue = () => null;
83
+ this.getDefaultValue = () => null;
84
+ this.valueToString = () => "";
85
+ this.name = config.name;
86
+ this.id = config.id || config.name;
87
+ this.label = config.label || config.name;
88
+ this.group = config.group || "";
89
+ this.value = null;
90
+ this.accept = config.accept || "";
91
+ this.isRandomizationDisabled = config.isRandomizationDisabled || false;
92
+ this.onChange = onChange;
93
+ this.onInput = onInput;
94
+ const { input, element, preview } = this.buildUI();
95
+ this.input = input;
96
+ this.element = element;
97
+ this.preview = preview;
98
+ }
99
+ }
@@ -0,0 +1,2 @@
1
+ import type { HTMLConfig } from "./types";
2
+ export declare const getHTMLControlElement: (config: HTMLConfig) => HTMLDivElement;
@@ -0,0 +1,13 @@
1
+ export const getHTMLControlElement = (config) => {
2
+ const right = document.createElement("div");
3
+ right.classList.add("ctrls__control-right");
4
+ right.append(config.html);
5
+ const label = document.createElement("label");
6
+ label.textContent = config.label || config.name;
7
+ label.classList.add("ctrls__control-label");
8
+ const element = document.createElement("div");
9
+ element.classList.add("ctrls__control", "ctrls__control--seed");
10
+ element.appendChild(label);
11
+ element.appendChild(right);
12
+ return element;
13
+ };
@@ -1,4 +1,4 @@
1
- import type { Ctrl, CtrlChangeHandler, CtrlType, ConfigFor } from ".";
1
+ import type { Ctrl, CtrlChangeHandler, CtrlItemType, ConfigFor } from "./types";
2
2
  type Option = {
3
3
  label: string;
4
4
  value: string;
@@ -7,7 +7,7 @@ export type RadioControlOptions = {
7
7
  items: Option[];
8
8
  };
9
9
  export declare class RadioCtrl implements Ctrl<string> {
10
- type: CtrlType;
10
+ type: CtrlItemType;
11
11
  htmlId: string;
12
12
  id: string;
13
13
  group?: string;
@@ -1,6 +1,6 @@
1
- import type { Ctrl, CtrlChangeHandler, CtrlType, ConfigFor } from ".";
1
+ import type { Ctrl, CtrlChangeHandler, CtrlItemType, ConfigFor } from "./types";
2
2
  export declare class RangeCtrl implements Ctrl<number> {
3
- type: CtrlType;
3
+ type: CtrlItemType;
4
4
  id: string;
5
5
  group?: string;
6
6
  name: string;
@@ -20,7 +20,7 @@ export class RangeCtrl {
20
20
  };
21
21
  this.buildUI = () => {
22
22
  const { min, max, step, value } = this;
23
- const id = toHtmlId(this.name);
23
+ const id = toHtmlId(this.id);
24
24
  const input = document.createElement("input");
25
25
  input.classList.add("ctrls__range-input");
26
26
  input.setAttribute("type", "range");
@@ -1,6 +1,6 @@
1
- import type { Ctrl, CtrlChangeHandler, CtrlConfig, CtrlType } from ".";
1
+ import type { Ctrl, CtrlChangeHandler, CtrlConfig, CtrlItemType } from "./types";
2
2
  export declare class SeedCtrl implements Ctrl<string> {
3
- type: CtrlType;
3
+ type: CtrlItemType;
4
4
  id: string;
5
5
  group?: string;
6
6
  name: string;
@@ -21,7 +21,7 @@ export class SeedCtrl {
21
21
  };
22
22
  this.buildUI = () => {
23
23
  const { value } = this;
24
- const id = toHtmlId(this.name);
24
+ const id = toHtmlId(this.id);
25
25
  const input = document.createElement("input");
26
26
  input.classList.add("ctrls__seed-input");
27
27
  input.setAttribute("type", "text");
@@ -0,0 +1,12 @@
1
+ type Attrs = Record<string, string | number> & {
2
+ for?: string;
3
+ };
4
+ export declare const dom: {
5
+ el: <T extends keyof HTMLElementTagNameMap>(tag: T, className?: string, attrs?: Attrs) => HTMLElementTagNameMap[T];
6
+ div: (className?: string, attrs?: Attrs) => HTMLDivElement;
7
+ span: (className?: string, attrs?: Attrs) => HTMLSpanElement;
8
+ input: (className?: string, attrs?: Attrs) => HTMLInputElement;
9
+ label: (className?: string, attrs?: Attrs) => HTMLLabelElement;
10
+ button: (className?: string, attrs?: Attrs) => HTMLButtonElement;
11
+ };
12
+ export {};
@@ -0,0 +1,16 @@
1
+ const el = (tag, className = "", attrs = {}) => {
2
+ const element = document.createElement(tag);
3
+ element.className = className;
4
+ for (const [key, value] of Object.entries(attrs)) {
5
+ element.setAttribute(key, value.toString());
6
+ }
7
+ return element;
8
+ };
9
+ export const dom = {
10
+ el,
11
+ div: (className = "", attrs = {}) => el("div", className, attrs),
12
+ span: (className = "", attrs = {}) => el("span", className, attrs),
13
+ input: (className = "", attrs = {}) => el("input", className, attrs),
14
+ label: (className = "", attrs = {}) => el("label", className, attrs),
15
+ button: (className = "", attrs = {}) => el("button", className, attrs),
16
+ };
@@ -1,127 +1,4 @@
1
- import BezierEasing from "bezier-easing";
2
- import { BooleanCtrl } from "./ctrl-boolean";
3
- import { DualRangeCtrl, type DualRangeValue } from "./ctrl-dual-range";
4
- import { EasingCtrl, type Easing } from "./ctrl-easing";
5
- import { RadioCtrl } from "./ctrl-radio";
6
- import { RangeCtrl } from "./ctrl-range";
7
- import { SeedCtrl } from "./ctrl-seed";
8
- export interface PRNG {
9
- (): number;
10
- }
11
- export type CtrlType = "boolean" | "range" | "radio" | "seed" | "easing" | "dual-range" | "group";
12
- export type CtrlChangeHandler = (control: CtrlComponent) => void;
13
- export type CtrlConfig<T = unknown> = {
14
- type: CtrlType;
15
- id?: string;
16
- name: string;
17
- group?: string;
18
- label?: string;
19
- defaultValue?: T;
20
- isRandomizationDisabled?: boolean;
21
- };
22
- export interface Ctrl<T> {
23
- id: string;
24
- group?: string;
25
- name: string;
26
- label: string;
27
- type: CtrlType;
28
- isRandomizationDisabled: boolean;
29
- onChange: CtrlChangeHandler;
30
- parse: (value: string) => T;
31
- getRandomValue: () => T;
32
- getDefaultValue: () => T;
33
- buildUI: () => unknown;
34
- valueToString: (value?: T) => string;
35
- update: (value: T) => void;
36
- element: HTMLElement;
37
- }
38
- export interface CtrlTypeMap {
39
- boolean: {
40
- value: boolean;
41
- };
42
- range: {
43
- value: number;
44
- min: number;
45
- max: number;
46
- step?: number;
47
- };
48
- radio: {
49
- value: string;
50
- items: Record<string, string>;
51
- columns?: 1 | 2 | 3 | 4 | 5;
52
- };
53
- seed: {
54
- value: string;
55
- };
56
- easing: {
57
- value: Easing;
58
- presets?: Record<string, Easing>;
59
- };
60
- "dual-range": {
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;
70
- };
71
- }
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"];
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[]> = {
104
- [C in Extract<Configs[number], {
105
- type: "easing";
106
- }> as `${C["name"]}Easing`]: ReturnType<typeof BezierEasing>;
107
- } & {
108
- [C in Extract<Configs[number], {
109
- type: "seed";
110
- }> as `${C["name"]}Rng`]: PRNG;
111
- } & {
112
- [C in Extract<Configs[number], {
113
- type: "group";
114
- }> as C["name"]]: DerivedProps<C["controls"]>;
115
- };
116
- type OptionsMap<Configs extends readonly ConfigItem[]> = ExtractValues<Configs> & DerivedProps<Configs>;
117
- type ControlsOptions = {
118
- showRandomizeButton?: boolean;
119
- storage?: "hash" | "none";
120
- theme?: "system" | "light" | "dark";
121
- parent?: Element;
122
- title?: string;
123
- };
124
- type CtrlComponent = BooleanCtrl | RangeCtrl | RadioCtrl | SeedCtrl | EasingCtrl | DualRangeCtrl;
1
+ import type { CtrlComponent, ConfigItem, ControlsOptions, TypedControlConfig, OptionsMap } from "./types";
125
2
  export declare class Ctrls<Configs extends readonly ConfigItem[]> {
126
3
  options: ControlsOptions;
127
4
  controls: CtrlComponent[];
@@ -130,8 +7,8 @@ export declare class Ctrls<Configs extends readonly ConfigItem[]> {
130
7
  onChange?: (updatedValues: Partial<ReturnType<typeof this.getValues>>) => void;
131
8
  onInput?: (updatedValues: Partial<ReturnType<typeof this.getValues>>) => void;
132
9
  constructor(configs: Configs, options?: ControlsOptions);
133
- registerControl: (config: TypedControlConfig, onChangeControlHandler: (control: CtrlComponent) => void, onInputControlHandler: (control: CtrlComponent) => void, group?: string) => void;
134
- buildUI: () => HTMLDivElement;
10
+ processControls: (configs: Configs) => HTMLElement[];
11
+ registerControl: (config: TypedControlConfig, onChangeControlHandler: (control: CtrlComponent) => void, onInputControlHandler: (control: CtrlComponent) => void, group?: string) => CtrlComponent;
135
12
  toggleVisibility: () => void;
136
13
  addHashListeners: () => void;
137
14
  getHash: () => string;
@@ -141,4 +18,3 @@ export declare class Ctrls<Configs extends readonly ConfigItem[]> {
141
18
  getValues(): OptionsMap<Configs>;
142
19
  randomize: () => void;
143
20
  }
144
- export {};