@weasel-js/select 0.0.0-stage → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 orochi235
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 CHANGED
@@ -1,3 +1,43 @@
1
- # Temporary Holding Version
1
+ # @weasel-js/select
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Selection as a value: which ids a press selects, under toggle, range and anchor rules a surface
4
+ configures. No React, no DOM.
5
+
6
+ Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
7
+ scene-graph canvas kit for React. See the
8
+ [API reference](https://orochi235.github.io/weasel/api/).
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ npm install @weasel-js/select
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```ts
19
+ import { intentOf, select } from '@weasel-js/select';
20
+
21
+ // A list's convention: Cmd/Ctrl toggles a row, shift ranges from the anchor.
22
+ const policy = { mode: 'multi', toggle: ['meta', 'ctrl'], range: 'shift' } as const;
23
+
24
+ let state = { ids: ['b'], anchor: 'b' };
25
+ state = select(state, 'd', intentOf({ shift: true, meta: false, ctrl: false }, policy), {
26
+ order: ['a', 'b', 'c', 'd'], // what a range walks
27
+ eligible: (id) => id !== 'c', // locked rows never join a multi-selection
28
+ });
29
+ // → { ids: ['b', 'd'], anchor: 'b' }
30
+ ```
31
+
32
+ | intent | ids | anchor |
33
+ |---|---|---|
34
+ | `replace` | `[id]` | `id` |
35
+ | `toggle` | eligible ids, with `id` added or removed; `[id]` if `id` is ineligible | `id` |
36
+ | `range` | eligible ids from anchor to `id` in `order`; unchanged if none is eligible; `[id]` if there is no anchor or either end is outside `order` | unchanged |
37
+
38
+ `createSelectionStore` keeps ids outside a component, behind the `SelectionStore` contract that
39
+ `@weasel-js/core`'s `Scene` also satisfies.
40
+
41
+ ## License
42
+
43
+ MIT
@@ -0,0 +1,47 @@
1
+ /** `single` always replaces; `multi` lets a policy's keys toggle and range. */
2
+ type SelectionMode = 'single' | 'multi';
3
+ /** A modifier key a policy can assign to toggling or ranging. */
4
+ type SelectionExtendKey = 'shift' | 'meta' | 'ctrl';
5
+ /** The modifiers held during a press. */
6
+ type SelectModifiers = Readonly<Record<SelectionExtendKey, boolean>>;
7
+ /** What a press does to the selection. */
8
+ type SelectIntent = 'replace' | 'toggle' | 'range';
9
+ /** Which keys toggle and which range, so a surface states its convention: a list usually
10
+ * toggles on Cmd/Ctrl and ranges on shift, a canvas toggles on shift. */
11
+ interface SelectPolicy {
12
+ readonly mode: SelectionMode;
13
+ readonly toggle?: SelectionExtendKey | readonly SelectionExtendKey[];
14
+ readonly range?: SelectionExtendKey;
15
+ }
16
+ /** What is selected, and the end a range extends from. */
17
+ interface Selection<K> {
18
+ readonly ids: readonly K[];
19
+ readonly anchor: K | null;
20
+ }
21
+ interface SelectContext<K> {
22
+ /** The sequence a range walks. A range without one replaces. */
23
+ readonly order?: readonly K[];
24
+ /** Whether an id may join a multi-selection. Ineligible ids are skipped by a range, dropped
25
+ * when the selection grows, and selected alone when pressed. Default: every id. */
26
+ readonly eligible?: (id: K) => boolean;
27
+ }
28
+ /** The intent a press carries under `policy`. A range key beats a toggle key. */
29
+ declare function intentOf(modifiers: SelectModifiers, policy: SelectPolicy): SelectIntent;
30
+ /** The eligible ids from `from` to `to` in `order`, ends included, in order. Empty when either
31
+ * end is not in `order`. */
32
+ declare function rangeOf<K>(order: readonly K[], from: K, to: K, eligible?: (id: K) => boolean): K[];
33
+ /** The selection after pressing `id` with `intent`. Returns `state` itself when nothing changes
34
+ * hands, so a caller can compare by reference. */
35
+ declare function select<K>(state: Selection<K>, id: K, intent: SelectIntent, context?: SelectContext<K>): Selection<K>;
36
+
37
+ /** Somewhere a selection can live outside the component that edits it. core's `Scene` satisfies
38
+ * it, and so does any store with the same three methods. */
39
+ interface SelectionStore<K> {
40
+ getSelection(): readonly K[];
41
+ setSelection(ids: readonly K[]): void;
42
+ subscribe(listener: () => void): () => void;
43
+ }
44
+ /** A {@link SelectionStore} holding the ids itself. A listener reads the new value. */
45
+ declare function createSelectionStore<K>(initial?: readonly K[]): SelectionStore<K>;
46
+
47
+ export { type SelectContext, type SelectIntent, type SelectModifiers, type SelectPolicy, type Selection, type SelectionExtendKey, type SelectionMode, type SelectionStore, createSelectionStore, intentOf, rangeOf, select };
package/dist/index.js ADDED
@@ -0,0 +1,54 @@
1
+ // src/select.ts
2
+ function intentOf(modifiers, policy) {
3
+ if (policy.mode === "single") return "replace";
4
+ if (policy.range && modifiers[policy.range]) return "range";
5
+ const toggle = typeof policy.toggle === "string" ? [policy.toggle] : policy.toggle ?? [];
6
+ return toggle.some((key) => modifiers[key]) ? "toggle" : "replace";
7
+ }
8
+ function rangeOf(order, from, to, eligible = always) {
9
+ const a = order.indexOf(from);
10
+ const b = order.indexOf(to);
11
+ if (a < 0 || b < 0) return [];
12
+ return order.slice(Math.min(a, b), Math.max(a, b) + 1).filter(eligible);
13
+ }
14
+ function select(state, id, intent, context = {}) {
15
+ const eligible = context.eligible ?? always;
16
+ if (intent === "toggle" && eligible(id)) {
17
+ const kept = state.ids.filter(eligible);
18
+ return { ids: kept.includes(id) ? kept.filter((x) => x !== id) : [...kept, id], anchor: id };
19
+ }
20
+ if (intent === "range" && state.anchor !== null && context.order) {
21
+ const { order } = context;
22
+ if (order.includes(state.anchor) && order.includes(id)) {
23
+ const ids = rangeOf(order, state.anchor, id, eligible);
24
+ return ids.length > 0 ? { ids, anchor: state.anchor } : state;
25
+ }
26
+ }
27
+ return { ids: [id], anchor: id };
28
+ }
29
+ function always() {
30
+ return true;
31
+ }
32
+
33
+ // src/store.ts
34
+ function createSelectionStore(initial = []) {
35
+ let ids = initial;
36
+ const listeners = /* @__PURE__ */ new Set();
37
+ return {
38
+ getSelection: () => ids,
39
+ setSelection(next) {
40
+ ids = next;
41
+ for (const listener of listeners) listener();
42
+ },
43
+ subscribe(listener) {
44
+ listeners.add(listener);
45
+ return () => {
46
+ listeners.delete(listener);
47
+ };
48
+ }
49
+ };
50
+ }
51
+
52
+ export { createSelectionStore, intentOf, rangeOf, select };
53
+ //# sourceMappingURL=index.js.map
54
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/select.ts","../src/store.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/** `single` always replaces; `multi` lets a policy's keys toggle and range. */\nexport type SelectionMode = 'single' | 'multi';\n\n/** A modifier key a policy can assign to toggling or ranging. */\nexport type SelectionExtendKey = 'shift' | 'meta' | 'ctrl';\n\n/** The modifiers held during a press. */\nexport type SelectModifiers = Readonly<Record<SelectionExtendKey, boolean>>;\n\n/** What a press does to the selection. */\nexport type SelectIntent = 'replace' | 'toggle' | 'range';\n\n/** Which keys toggle and which range, so a surface states its convention: a list usually\n * toggles on Cmd/Ctrl and ranges on shift, a canvas toggles on shift. */\nexport interface SelectPolicy {\n readonly mode: SelectionMode;\n readonly toggle?: SelectionExtendKey | readonly SelectionExtendKey[];\n readonly range?: SelectionExtendKey;\n}\n\n/** What is selected, and the end a range extends from. */\nexport interface Selection<K> {\n readonly ids: readonly K[];\n readonly anchor: K | null;\n}\n\nexport interface SelectContext<K> {\n /** The sequence a range walks. A range without one replaces. */\n readonly order?: readonly K[];\n /** Whether an id may join a multi-selection. Ineligible ids are skipped by a range, dropped\n * when the selection grows, and selected alone when pressed. Default: every id. */\n readonly eligible?: (id: K) => boolean;\n}\n\n/** The intent a press carries under `policy`. A range key beats a toggle key. */\nexport function intentOf(modifiers: SelectModifiers, policy: SelectPolicy): SelectIntent {\n if (policy.mode === 'single') return 'replace';\n if (policy.range && modifiers[policy.range]) return 'range';\n const toggle = typeof policy.toggle === 'string' ? [policy.toggle] : (policy.toggle ?? []);\n return toggle.some((key) => modifiers[key]) ? 'toggle' : 'replace';\n}\n\n/** The eligible ids from `from` to `to` in `order`, ends included, in order. Empty when either\n * end is not in `order`. */\nexport function rangeOf<K>(order: readonly K[], from: K, to: K, eligible: (id: K) => boolean = always): K[] {\n const a = order.indexOf(from);\n const b = order.indexOf(to);\n if (a < 0 || b < 0) return [];\n return order.slice(Math.min(a, b), Math.max(a, b) + 1).filter(eligible);\n}\n\n/** The selection after pressing `id` with `intent`. Returns `state` itself when nothing changes\n * hands, so a caller can compare by reference. */\nexport function select<K>(state: Selection<K>, id: K, intent: SelectIntent, context: SelectContext<K> = {}): Selection<K> {\n const eligible = context.eligible ?? always;\n if (intent === 'toggle' && eligible(id)) {\n const kept = state.ids.filter(eligible);\n return { ids: kept.includes(id) ? kept.filter((x) => x !== id) : [...kept, id], anchor: id };\n }\n if (intent === 'range' && state.anchor !== null && context.order) {\n const { order } = context;\n if (order.includes(state.anchor) && order.includes(id)) {\n const ids = rangeOf(order, state.anchor, id, eligible);\n return ids.length > 0 ? { ids, anchor: state.anchor } : state;\n }\n }\n return { ids: [id], anchor: id };\n}\n\nfunction always(): boolean {\n return true;\n}\n","/** Somewhere a selection can live outside the component that edits it. core's `Scene` satisfies\n * it, and so does any store with the same three methods. */\nexport interface SelectionStore<K> {\n getSelection(): readonly K[];\n setSelection(ids: readonly K[]): void;\n subscribe(listener: () => void): () => void;\n}\n\n/** A {@link SelectionStore} holding the ids itself. A listener reads the new value. */\nexport function createSelectionStore<K>(initial: readonly K[] = []): SelectionStore<K> {\n let ids = initial;\n const listeners = new Set<() => void>();\n return {\n getSelection: () => ids,\n setSelection(next) {\n ids = next;\n for (const listener of listeners) listener();\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => { listeners.delete(listener); };\n },\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "@weasel-js/select",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.8.1",
4
+ "description": "Selection as a value: which ids a press selects, with toggle, range and anchor rules a surface configures. No React, no DOM.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "author": "orochi235",
19
+ "homepage": "https://orochi235.github.io/weasel/",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/orochi235/weasel.git",
23
+ "directory": "packages/select"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/orochi235/weasel/issues"
27
+ },
28
+ "engines": {
29
+ "node": ">=22"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsup"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "provenance": true
42
+ }
43
+ }