@johnhenry/cyclable 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 John Henry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # cyclable
2
+
3
+ [![npm version](https://img.shields.io/npm/v/%40johnhenry%2Fcyclable.svg)](https://www.npmjs.com/package/@johnhenry/cyclable)
4
+ [![CI](https://github.com/johnhenry/cyclable/actions/workflows/ci.yml/badge.svg)](https://github.com/johnhenry/cyclable/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/%40johnhenry%2Fcyclable.svg)](LICENSE)
6
+
7
+ Full documentation: [opensource.johnhenry.me/cyclable](https://opensource.johnhenry.me/cyclable/)
8
+
9
+ > **Provenance:** originally four individually-versioned modules under
10
+ > [`johnhenry/lib`](https://github.com/johnhenry/lib)'s `js/` directory
11
+ > (`js/localstorage-cycler/0.0.0/`, `js/localstorage-class-cycler/0.0.0/`,
12
+ > `js/class-cycler.component/0.0.0/`, `js/class-cycler.button.component/0.0.0/`),
13
+ > then briefly part of [`@johnhenry/domkit`](https://github.com/johnhenry/domkit)'s
14
+ > ~33-module toolkit, now extracted into this standalone package because
15
+ > the four form a real, coherent family — one engine plus wrapper variants
16
+ > — rather than an unrelated grab-bag of widgets. `domkit` no longer
17
+ > carries this functionality. See `## Family` below and `CHANGELOG.md` for
18
+ > the full history.
19
+
20
+ A localStorage-backed value cycler: **one engine, three consumption
21
+ shapes.** `localStorageCycler` cycles a `localStorage`-backed value
22
+ through a fixed list of strings and calls an optional change handler.
23
+ `classStorageCycler` wraps it to apply the cycled value as a CSS class on
24
+ a DOM element. `<class-cycler>` and
25
+ `<button is="class-cycler-button">` are two ready-made custom elements
26
+ built on that wrapper — a global-function variant and a self-contained
27
+ button variant, respectively. All four modules are independent files
28
+ under `src/`, importable individually with no shared barrel and no build
29
+ step.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ npm install @johnhenry/cyclable
35
+ ```
36
+
37
+ ## `localstorage-cycler` — the engine
38
+
39
+ Cycle a `localStorage` value through a given list of strings.
40
+
41
+ ```javascript
42
+ import localStorageCycler from "@johnhenry/cyclable/localstorage-cycler/index.mjs";
43
+ const updateLocalStorage = localStorageCycler("my-key", "a", "b", "c");
44
+ ```
45
+
46
+ The call to `localStorageCycler` checks for the existence of the key
47
+ (`"my-key"`) in `localStorage`, and sets it to the first value (`"a"`) if
48
+ not already set.
49
+
50
+ When called, the returned `updateLocalStorage` function cycles the value
51
+ associated with the key in `localStorage` through the given values (`"a"`,
52
+ `"b"`, and `"c"`).
53
+
54
+ `updateLocalStorage` returns an object with the following keys:
55
+
56
+ - `key` — the associated local storage key
57
+ - `value` — the current value of the local storage item
58
+ - `index` — the current index of the local storage item
59
+ - `result` — the result of a handler, if passed (see below)
60
+
61
+ ### Change handler
62
+
63
+ To react to the change, pass an optional change handler as the second
64
+ parameter to `localStorageCycler`.
65
+
66
+ ```javascript
67
+ const onChange = ({ value, key, index, events }) =>
68
+ console.log({ value, key, index, events });
69
+ const updateLocalStorage = localStorageCycler(
70
+ "my-key",
71
+ onChange,
72
+ "a",
73
+ "b",
74
+ "c"
75
+ );
76
+ ```
77
+
78
+ The handler takes four parameters:
79
+
80
+ - the same `key`, `value`, and `index` parameters returned from calling
81
+ `updateLocalStorage`
82
+ - an `events` parameter — an array of everything passed into the
83
+ `updateLocalStorage` function, or an `init` `CustomEvent` if fired from
84
+ the initial call to `localStorageCycler`
85
+
86
+ ## `localstorage-class-cycler` — apply the cycled value as a class
87
+
88
+ Cycle a `localStorage` value through a given list of strings, and render
89
+ the cycled value as a class on a given element.
90
+
91
+ ```javascript
92
+ import classStorageCycler from "@johnhenry/cyclable/localstorage-class-cycler/index.mjs";
93
+ const updateBodyClass = classStorageCycler(
94
+ document.body,
95
+ "my-key",
96
+ "a",
97
+ "b",
98
+ "c"
99
+ );
100
+ updateBodyClass();
101
+ ```
102
+
103
+ ## `class-cycler.component` — global-function custom element
104
+
105
+ Exposes a `localstorage-class-cycler` instance as a named global
106
+ function, so it can be called from anywhere (e.g. a plain `<button
107
+ onclick="...">`).
108
+
109
+ | Attribute | Description |
110
+ |---|---|
111
+ | `global` | Name to assign the cycler function to on `globalThis` (required — removing this attribute, or disconnecting the element, unassigns it) |
112
+ | `selector` | Selector for the element whose classes get cycled. Defaults to `body` |
113
+ | `storage-key` | localStorage key the current class value persists under |
114
+ | `classes` | Comma-delimited list of classes to cycle through |
115
+
116
+ ```html
117
+ <script
118
+ type="module"
119
+ src="https://esm.sh/@johnhenry/cyclable/class-cycler.component/global.mjs"
120
+ ></script>
121
+ <class-cycler
122
+ global="cycleTheme"
123
+ selector="body"
124
+ storage-key="theme"
125
+ classes="light,dark,system"
126
+ ></class-cycler>
127
+ <button onclick="cycleTheme()">Toggle theme</button>
128
+ ```
129
+
130
+ ## `class-cycler.button.component` — self-contained button custom element
131
+
132
+ A `<button is="class-cycler-button">` that cycles the classes of one or
133
+ more target elements every time it's clicked.
134
+
135
+ | Attribute | Description |
136
+ |---|---|
137
+ | `select` | Selector for a single target element. Defaults to `html` if neither `select` nor `select-all` is set |
138
+ | `select-all` | Selector for multiple target elements (all matches get cycled together) |
139
+ | `storage-key` | localStorage key the current class value persists under |
140
+ | `classes` | Comma-delimited list of classes to cycle through |
141
+
142
+ ```html
143
+ <script
144
+ type="module"
145
+ src="https://esm.sh/@johnhenry/cyclable/class-cycler.button.component/global.mjs"
146
+ ></script>
147
+ <button
148
+ is="class-cycler-button"
149
+ select="body"
150
+ storage-key="theme"
151
+ classes="light,dark,system"
152
+ >
153
+ Toggle theme
154
+ </button>
155
+ ```
156
+
157
+ ## Family
158
+
159
+ - [`@johnhenry/domkit`](https://github.com/johnhenry/domkit) — the
160
+ ~33-module DOM/HTML-component toolkit this package was extracted from.
161
+ domkit is a real npm dependency of neither direction of this
162
+ relationship — it does not depend on cyclable, and cyclable does not
163
+ depend on domkit; the four modules here simply no longer live in
164
+ domkit's `src/`.
165
+ - [`@johnhenry/domable`](https://github.com/johnhenry/domable) — the DOM
166
+ ⇄ text ⇄ React conversion primitives domkit itself builds on. Unrelated
167
+ to this package's functionality; listed here only because it's the
168
+ other sibling extraction in this same lineage
169
+ (`johnhenry/lib` → `johnhenry/domkit` → standalone package).
170
+
171
+ ## License
172
+
173
+ MIT
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@johnhenry/cyclable",
3
+ "version": "0.0.0",
4
+ "description": "A localStorage-backed value cycler: one engine, a class-cycler wrapper, and two ready-made custom elements for cycling CSS classes on click.",
5
+ "type": "module",
6
+ "exports": {
7
+ "./*": "./src/*"
8
+ },
9
+ "files": [
10
+ "src/"
11
+ ],
12
+ "scripts": {
13
+ "test": "node scripts/check-syntax.mjs"
14
+ },
15
+ "keywords": [
16
+ "html",
17
+ "dom",
18
+ "web-components",
19
+ "custom-elements",
20
+ "localstorage",
21
+ "cycler",
22
+ "theme-toggle"
23
+ ],
24
+ "author": "John Henry",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/johnhenry/cyclable.git"
29
+ },
30
+ "homepage": "https://opensource.johnhenry.me/cyclable/",
31
+ "engines": {
32
+ "node": ">=26.0.0"
33
+ }
34
+ }
@@ -0,0 +1,4 @@
1
+ import DefineComponent from "./index.mjs";
2
+ globalThis.customElements.define("class-cycler-button", DefineComponent, {
3
+ extends: "button",
4
+ });
@@ -0,0 +1,46 @@
1
+ import classCycler from "../localstorage-class-cycler/index.mjs";
2
+
3
+ const DEFAULT_SELECTOR = "html";
4
+ export default class extends globalThis.HTMLButtonElement {
5
+ updateBound = null;
6
+ constructor() {
7
+ super();
8
+ }
9
+ connectedCallback() {
10
+ this.init();
11
+ this.updateBound = this.update.bind(this);
12
+ this.addEventListener("click", this.updateBound);
13
+ }
14
+ disconnectedCallback() {
15
+ this.removeEventListener("click", this.updateBound);
16
+ }
17
+ update() {
18
+ this.classCycler?.();
19
+ }
20
+ init() {
21
+ const selectors = [];
22
+ if (this.getAttribute("select")) {
23
+ selectors.push(document.querySelector(this.getAttribute("select")));
24
+ } else if (this.getAttribute("select-all")) {
25
+ selectors.push(
26
+ ...document.querySelectorAll(this.getAttribute("select-all"))
27
+ );
28
+ }
29
+ if (
30
+ !selectors.length &&
31
+ this.getAttribute("select") !== "" &&
32
+ this.getAttribute("select-all") !== ""
33
+ ) {
34
+ selectors.push(document.querySelector(DEFAULT_SELECTOR));
35
+ }
36
+ const storageKey = this.getAttribute("storage-key");
37
+ const classes = (this.getAttribute("classes") || "").split(",");
38
+ this.classCycler = classCycler(selectors, storageKey, ...classes);
39
+ }
40
+ static get observedAttributes() {
41
+ return ["select", "storage-key", "select-all", "classes"];
42
+ }
43
+ attributeChangedCallback() {
44
+ this.init();
45
+ }
46
+ }
@@ -0,0 +1,34 @@
1
+ # Class Cycler Button
2
+
3
+ A `<button is="class-cycler-button">` that cycles the classes of one or
4
+ more target elements every time it's clicked, via
5
+ [localstorage-class-cycler](../localstorage-class-cycler/readme.md).
6
+ Self-contained button variant — see
7
+ [class-cycler.component](../class-cycler.component/readme.md)
8
+ for a global-function variant callable from anywhere.
9
+
10
+ ## Attributes
11
+
12
+ | Attribute | Description |
13
+ |---|---|
14
+ | `select` | Selector for a single target element. Defaults to `html` if neither `select` nor `select-all` is set |
15
+ | `select-all` | Selector for multiple target elements (all matches get cycled together) |
16
+ | `storage-key` | localStorage key the current class value persists under |
17
+ | `classes` | Comma-delimited list of classes to cycle through |
18
+
19
+ ## Usage
20
+
21
+ ```html
22
+ <script
23
+ type="module"
24
+ src="https://esm.sh/@johnhenry/domkit/class-cycler.button.component/global.mjs"
25
+ ></script>
26
+ <button
27
+ is="class-cycler-button"
28
+ select="body"
29
+ storage-key="theme"
30
+ classes="light,dark,system"
31
+ >
32
+ Toggle theme
33
+ </button>
34
+ ```
@@ -0,0 +1,2 @@
1
+ import DefineComponent from "./index.mjs";
2
+ globalThis.customElements.define("class-cycler", DefineComponent);
@@ -0,0 +1,40 @@
1
+ import classCycler from "../localstorage-class-cycler/index.mjs";
2
+ const DEFAULT_SELECTOR = "body";
3
+ export default class extends globalThis.HTMLElement {
4
+ constructor() {
5
+ super();
6
+ }
7
+ connectedCallback() {
8
+ this.reset();
9
+ }
10
+ reset() {
11
+ const global = this.getAttribute("global");
12
+ if (global) {
13
+ const selector = this.getAttribute("selector") || DEFAULT_SELECTOR;
14
+ const storageKey = this.getAttribute("storage-key");
15
+ const classes = (this.getAttribute("classes") || "").split(",");
16
+ globalThis[global] = classCycler(
17
+ document.querySelector(selector),
18
+ storageKey,
19
+ ...classes
20
+ );
21
+ }
22
+ }
23
+ disconnectedCallback() {
24
+ delete globalThis[this.getAttribute("global")];
25
+ }
26
+ static get observedAttributes() {
27
+ return ["global", "selector", "storage-key", "classes"];
28
+ }
29
+ attributeChangedCallback(name, old, current) {
30
+ switch (name) {
31
+ case "global":
32
+ if (old && !current) {
33
+ delete globalThis[old];
34
+ return;
35
+ }
36
+ break;
37
+ }
38
+ this.reset();
39
+ }
40
+ }
@@ -0,0 +1,32 @@
1
+ # Class Cycler
2
+
3
+ Exposes a [localstorage-class-cycler](../localstorage-class-cycler/readme.md)
4
+ instance as a named global function, so it can be called from anywhere
5
+ (e.g. a plain `<button onclick="...">`). Container/global variant — see
6
+ [class-cycler.button.component](../class-cycler.button.component/readme.md)
7
+ for a self-contained `<button>` element that cycles its own classes.
8
+
9
+ ## Attributes
10
+
11
+ | Attribute | Description |
12
+ |---|---|
13
+ | `global` | Name to assign the cycler function to on `globalThis` (required — removing this attribute, or disconnecting the element, unassigns it) |
14
+ | `selector` | Selector for the element whose classes get cycled. Defaults to `body` |
15
+ | `storage-key` | localStorage key the current class value persists under |
16
+ | `classes` | Comma-delimited list of classes to cycle through |
17
+
18
+ ## Usage
19
+
20
+ ```html
21
+ <script
22
+ type="module"
23
+ src="https://esm.sh/@johnhenry/domkit/class-cycler.component/global.mjs"
24
+ ></script>
25
+ <class-cycler
26
+ global="cycleTheme"
27
+ selector="body"
28
+ storage-key="theme"
29
+ classes="light,dark,system"
30
+ ></class-cycler>
31
+ <button onclick="cycleTheme()">Toggle theme</button>
32
+ ```
@@ -0,0 +1,14 @@
1
+ import localStorageCycler from "../localstorage-cycler/index.mjs";
2
+ export default (targets, key, ...classes) => {
3
+ const values = classes;
4
+ const emit = ({ value }) => {
5
+ const target_list = Array.isArray(targets) ? targets : [targets];
6
+ for (const target of target_list) {
7
+ target.classList.remove(...values.filter((s) => s));
8
+ if (value) {
9
+ target.classList.add(value);
10
+ }
11
+ }
12
+ };
13
+ return localStorageCycler(key, emit, ...values);
14
+ };
@@ -0,0 +1,18 @@
1
+ # LocalStorage Cycler
2
+
3
+ Cycle local storage values through a given list of strings.
4
+ Renders cycled value as class on given element.
5
+
6
+ ## Usage
7
+
8
+ ```javascript
9
+ import classStorageCycler from "..";
10
+ const updateBodyClass = classStorageCycler(
11
+ document.body,
12
+ "my-key",
13
+ "a",
14
+ "b",
15
+ "c"
16
+ );
17
+ updateBodyClass();
18
+ ```
@@ -0,0 +1,11 @@
1
+ import localStorageCycler from "./index.mjs";
2
+ export default (target, key, ...classes) => {
3
+ const values = classes;
4
+ const emit = ({ value }) => {
5
+ target.classList.remove(...values.filter((s) => s));
6
+ if (value) {
7
+ target.classList.add(value);
8
+ }
9
+ };
10
+ return localStorageCycler(key, emit, ...values);
11
+ };
@@ -0,0 +1,31 @@
1
+ const createNext =
2
+ (key, handler, ...values) =>
3
+ (...events) => {
4
+ const stored = globalThis.localStorage.getItem(key);
5
+ const index = values.indexOf(stored) + 1;
6
+ const value = values[index] ?? values[0];
7
+ globalThis.localStorage.setItem(key, value);
8
+ return {
9
+ value,
10
+ key,
11
+ index,
12
+ result: handler({ value, key, index, events }),
13
+ };
14
+ };
15
+ export default (key, ...values) => {
16
+ if (!key) {
17
+ throw new Error("key is required");
18
+ }
19
+ const handler = typeof values[0] === "function" ? values.shift() : () => {};
20
+ const stored = globalThis.localStorage.getItem(key) ?? values[0];
21
+ const index = values.indexOf(stored);
22
+ const value = values[index] ?? values[0];
23
+ globalThis.localStorage.setItem(key, value);
24
+ handler({
25
+ value,
26
+ key,
27
+ index,
28
+ events: [new CustomEvent("init", { detail: { value, key, index } })],
29
+ });
30
+ return createNext(key, handler, ...values);
31
+ };
@@ -0,0 +1,54 @@
1
+ # LocalStorage Cycler
2
+
3
+ Cycle local storage values through a given list of strings
4
+
5
+ ## Usage
6
+
7
+ ```javascript
8
+ import localStorageCycler from "?";
9
+ const updateLocalStorage = localStorageCycler("my-key", "a", "b", "c");
10
+ ```
11
+
12
+ The call to "localStorageCycler"
13
+ checks for the existence
14
+ of the key ("my-key") in localStorage.
15
+ and sets it to the first key ("a") if not already set.
16
+
17
+ When called, the "updateLocalStorage" function
18
+ cycles the value associated with the key
19
+ in localStorage through the given values ("a", "b", and "c").
20
+
21
+ The "updateLocalStorage" returns an object with the following keys:
22
+
23
+ - key - the associated local storage key
24
+ - value - the current value of the local storage item
25
+ - index - the current index of the local storage item
26
+ - result - the reuslt of an handler, if passed (see below)
27
+
28
+ ## Change Handler
29
+
30
+ To react to the change,
31
+ pass a optional change handler
32
+ as the second parameter to "localStorageCycler".
33
+
34
+ ```javascript
35
+ const onChange = ({ value, key, index, events }) =>
36
+ console.log({ value, key, index, events });
37
+ const updateLocalStorage = localStorageCycler(
38
+ "my-key",
39
+ onChange,
40
+ "a",
41
+ "b",
42
+ "c"
43
+ );
44
+ ```
45
+
46
+ The handler takes four parametes:
47
+
48
+ - the same, "key", "value", and "index" parameters
49
+ returned from calling "updateLocalStorage"
50
+
51
+ - an "events" parameter -- an array of everything
52
+ passed into the "updateLocalStorage" function OR
53
+ an "init" CustomEvent if fired from the initial
54
+ call to localStorageCycler.