Skip to content

Commit a48e9e3

Browse files
committed
feat: Add @layerstack/svelte-state package, initially with selectionState and uniqueState
1 parent 1f91bc0 commit a48e9e3

21 files changed

Lines changed: 744 additions & 6 deletions

.changeset/fluffy-eggs-joke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@layerstack/svelte-state': minor
3+
---
4+
5+
feat: Add `@layerstack/svelte-state` package, initially with `selectionState` and `uniqueState`
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.DS_Store
2+
node_modules
3+
/build
4+
/dist
5+
/.svelte-kit
6+
/package
7+
.env
8+
.env.*
9+
!.env.example
10+
coverage/
11+
12+
# Ignore files for PNPM, NPM and YARN
13+
pnpm-lock.yaml
14+
package-lock.json
15+
yarn.lock

packages/svelte-state/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# @layerstack/svelte-state

packages/svelte-state/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2024 Sean Lynch
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
13+
all 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
21+
THE SOFTWARE.

packages/svelte-state/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# LayerStack svelte-stores
2+
3+
![](https://img.shields.io/github/license/techniq/layerstack?style=flat)
4+
[![](https://img.shields.io/github/actions/workflow/status/techniq/layerstack/ci.yml?style=flat)](https://github.com/techniq/layerstack/actions/workflows/ci.yml)
5+
6+
![](https://img.shields.io/github/license/layerstack?style=flat)
7+
[![](https://dcbadge.vercel.app/api/server/697JhMPD3t?style=flat)](https://discord.gg/697JhMPD3t)
8+
9+
See also the companion libraries [LayerChart](https://layerchart.com) for a large collection of composable chart components to build a wide range of visualizations, and [Svelte UX](https://svelte-ux.techniq.dev/) for a collection of components to build highly interactive applications.

packages/svelte-state/package.json

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"name": "@layerstack/svelte-state",
3+
"description": "TODO",
4+
"author": "Sean Lynch <techniq35@gmail.com>",
5+
"license": "MIT",
6+
"repository": "techniq/layerstack",
7+
"version": "0.0.0",
8+
"scripts": {
9+
"dev": "rimraf dist && tsc -p tsconfig.build.json --watch",
10+
"build": "rimraf dist && tsc -p tsconfig.build.json",
11+
"preview": "vite preview",
12+
"package": "svelte-package",
13+
"prepublishOnly": "svelte-package",
14+
"check": "svelte-check --tsconfig ./tsconfig.json",
15+
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
16+
"test:unit": "TZ=UTC+4 vitest",
17+
"lint": "prettier --check .",
18+
"format": "prettier --write ."
19+
},
20+
"devDependencies": {
21+
"@sveltejs/kit": "^2.20.8",
22+
"@sveltejs/package": "^2.3.11",
23+
"@sveltejs/vite-plugin-svelte": "^5.0.3",
24+
"@vitest/coverage-v8": "^3.1.2",
25+
"prettier": "^3.5.3",
26+
"prettier-plugin-svelte": "^3.3.3",
27+
"rimraf": "6.0.1",
28+
"svelte": "^5.28.2",
29+
"svelte-check": "^4.1.6",
30+
"svelte2tsx": "^0.7.36",
31+
"tslib": "^2.8.1",
32+
"typescript": "^5.8.3",
33+
"vite": "^6.3.4",
34+
"vitest": "^3.1.2"
35+
},
36+
"type": "module",
37+
"dependencies": {
38+
"@layerstack/utils": "workspace:*"
39+
},
40+
"main": "./dist/index.js",
41+
"exports": {
42+
".": {
43+
"types": "./dist/index.d.ts",
44+
"svelte": "./dist/index.js"
45+
},
46+
"./*": {
47+
"types": "./dist/*.d.ts",
48+
"svelte": "./dist/*.js"
49+
}
50+
},
51+
"files": [
52+
"dist"
53+
],
54+
"svelte": "./dist/index.js"
55+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { selectionState, type SelectionState } from './selectionState.svelte.js';
2+
export { uniqueState } from './uniqueState.svelte.js';
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { uniqueState } from './uniqueState.svelte.js';
2+
3+
export type SelectionProps<T> = {
4+
/** Initial values */
5+
initial?: T[];
6+
7+
/** All values to select when `toggleAll()` is called */
8+
all?: T[];
9+
10+
/** Only allow 1 selected value */
11+
single?: boolean;
12+
13+
/** Maximum number of values that can be selected */
14+
max?: number;
15+
};
16+
17+
export type SelectionState<T> = ReturnType<typeof selectionState<T>>;
18+
19+
export function selectionState<T>(props: SelectionProps<T> = {}) {
20+
const selected = uniqueState(props.initial ?? []);
21+
const selectedArr = $derived([...selected.current.values()]);
22+
let all = $state(props.all ?? []);
23+
const single = props.single ?? false;
24+
const max = props.max;
25+
26+
function isSelected(value: T) {
27+
return selected.current.has(value);
28+
}
29+
30+
function isEmpty() {
31+
return selectedArr.length === 0;
32+
}
33+
34+
function isAllSelected() {
35+
return all.every((v) => selected.current.has(v));
36+
}
37+
38+
function isAnySelected() {
39+
return all.some((v) => selected.current.has(v));
40+
}
41+
42+
function isMaxSelected() {
43+
return max != null ? selected.current.size >= max : false;
44+
}
45+
46+
function isDisabled(value: T) {
47+
return !isSelected(value) && isMaxSelected();
48+
}
49+
50+
function clear() {
51+
selected.reset();
52+
}
53+
54+
function reset() {
55+
selected.reset();
56+
selected.addEach(props.initial ?? []);
57+
}
58+
59+
function setSelected(values: T[]) {
60+
if (max == null || values.length < max) {
61+
selected.reset();
62+
selected.addEach(values);
63+
}
64+
}
65+
66+
function toggleSelected(value: T) {
67+
if (selected.current.has(value)) {
68+
// Remove
69+
const prevSelected = [...selected.current];
70+
selected.reset();
71+
selected.addEach(prevSelected.filter((v) => v != value));
72+
} else if (single) {
73+
// Replace
74+
selected.reset();
75+
selected.add(value);
76+
} else {
77+
// Add
78+
if (max == null || selected.current.size < max) {
79+
return selected.add(value);
80+
}
81+
}
82+
}
83+
84+
function toggleAll() {
85+
let values: T[];
86+
if (isAllSelected()) {
87+
// Deselect all (within current `all`, for example page/filtered result)
88+
values = [...selected.current].filter((v) => !all.includes(v));
89+
} else {
90+
// Select all (`new Set()` will dedupe)
91+
values = [...selected.current, ...all];
92+
}
93+
selected.reset();
94+
selected.addEach(values);
95+
}
96+
97+
return {
98+
get selected() {
99+
return single ? (selectedArr[0] ?? null) : selectedArr;
100+
},
101+
setSelected,
102+
toggleSelected,
103+
isSelected,
104+
isDisabled,
105+
toggleAll,
106+
isAllSelected,
107+
isAnySelected,
108+
isMaxSelected,
109+
isEmpty,
110+
clear,
111+
reset,
112+
get all() {
113+
return all;
114+
},
115+
};
116+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { SvelteSet } from 'svelte/reactivity';
2+
3+
/**
4+
* State to manage unique values using `SvelteSet` with improved
5+
* ergonomics and better control of updates
6+
*/
7+
export function uniqueState<T = string | number>(initialValues?: T[]) {
8+
const current = new SvelteSet<T>(initialValues ?? []);
9+
10+
return {
11+
current,
12+
reset() {
13+
current.clear();
14+
},
15+
add(value: T) {
16+
current.add(value);
17+
},
18+
addEach(values: T[]) {
19+
for (const value of values) {
20+
current.add(value);
21+
}
22+
},
23+
delete(value: T) {
24+
current.delete(value);
25+
},
26+
toggle(value: T) {
27+
if (current.has(value)) {
28+
current.delete(value);
29+
} else {
30+
current.add(value);
31+
}
32+
},
33+
};
34+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"exclude": ["./src/**/*.test.ts"]
4+
}

0 commit comments

Comments
 (0)