@escape-game-over/atlas 0.1.12 → 0.1.14
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/docs/client-scripts.md +19 -1
- package/package.json +2 -2
- package/src/astro/filters.ts +55 -14
package/docs/client-scripts.md
CHANGED
|
@@ -112,9 +112,27 @@ than an item that silently never matches.
|
|
|
112
112
|
| Kind | State | Item value | Matches when | In the URL | History |
|
|
113
113
|
| -------- | --------------- | --------------- | ------------------------------------------------ | ------------------------------- | ------- |
|
|
114
114
|
| `text` | the query | searchable text | folded query is a substring; empty keeps all | `?q=…`, dropped if empty | replace |
|
|
115
|
-
| `choice` | one value, `""` | the item
|
|
115
|
+
| `choice` | one value, `""` | one, or several | the item holds the chosen one; `""` keeps all | `?category=…`, dropped if empty | push |
|
|
116
116
|
| `flag` | boolean | boolean | off keeps all; on keeps only items that carry it | `?featured=1`, absent when off | push |
|
|
117
117
|
|
|
118
|
+
**A `choice` item may hold several values.** The state stays one — the tab strip
|
|
119
|
+
picks one category, the URL carries one — but an item can sit in more than one
|
|
120
|
+
and then answers to any of them:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
items: [
|
|
124
|
+
{ key: "burgos", values: { category: "city" } },
|
|
125
|
+
{ key: "avila", values: { category: ["walk", "city"] } },
|
|
126
|
+
]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
An escape room is adventure *and* sci-fi; a film is a comedy *and* a drama. This
|
|
130
|
+
is a widening of `choice` rather than a fourth kind, because a kind for it would
|
|
131
|
+
carry the same state, the same parameter, the same `set` semantics and the same
|
|
132
|
+
control, and differ by one operator — two kinds doing one job, which is what the
|
|
133
|
+
`flag` note below refuses. A list whose items each sit in one category writes a
|
|
134
|
+
bare string and is untouched.
|
|
135
|
+
|
|
118
136
|
**`text` folds both sides** — `NFD`, drop the combining marks, lowercase, trim.
|
|
119
137
|
This is the piece that is missing nearly everywhere, and it is not cosmetic: a
|
|
120
138
|
reader who types `malaga` is otherwise told there is no *Málaga*, and every
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escape-game-over/atlas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
|
|
6
6
|
"private": false,
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@biomejs/biome": "2.5.12",
|
|
57
|
-
"@types/node": "26.
|
|
57
|
+
"@types/node": "26.5.0",
|
|
58
58
|
"@vitest/coverage-istanbul": "5.0.0",
|
|
59
59
|
"astro": "7.3.1",
|
|
60
60
|
"typescript": "6.0.3",
|
package/src/astro/filters.ts
CHANGED
|
@@ -61,7 +61,9 @@
|
|
|
61
61
|
* The kinds are closed, and deliberately few:
|
|
62
62
|
*
|
|
63
63
|
* - `text` — free entry, folded and substring-matched. The search box.
|
|
64
|
-
* - `choice` — one of a set, or none. Tabs, a `<select>`, a radio group.
|
|
64
|
+
* - `choice` — one of a set, or none. Tabs, a `<select>`, a radio group. The
|
|
65
|
+
* control picks one; an item may hold several and answer to any — see
|
|
66
|
+
* `ItemValue`.
|
|
65
67
|
* - `flag` — a narrowing toggle: off matches everything, on keeps only the
|
|
66
68
|
* items that carry it. "In stock", "Step-free access".
|
|
67
69
|
*
|
|
@@ -78,26 +80,58 @@ export type Field =
|
|
|
78
80
|
export type FieldMap = Readonly<Record<string, Field>>;
|
|
79
81
|
|
|
80
82
|
/**
|
|
81
|
-
* What one field contributes,
|
|
83
|
+
* What one field contributes, in the state.
|
|
82
84
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* typed. A `flag` is what the item is on one side and what was asked for on the
|
|
87
|
-
* other.
|
|
85
|
+
* Always one value per field, whatever the kind: a `choice` is the one category
|
|
86
|
+
* asked for, a `text` is what someone typed, a `flag` is whether the toggle is
|
|
87
|
+
* on. This is what `set` takes and what reaches the address bar.
|
|
88
88
|
*/
|
|
89
|
-
export type
|
|
89
|
+
export type StateValue<F extends Field> = F["kind"] extends "flag"
|
|
90
90
|
? boolean
|
|
91
91
|
: string;
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* What one field contributes, on an item.
|
|
95
|
+
*
|
|
96
|
+
* The two roles read alike but are not the same, and `choice` is where they part
|
|
97
|
+
* company. On an item a `text` value is everything that field searches — a name
|
|
98
|
+
* and a blurb joined — while in the state it is what someone typed; a `flag` is
|
|
99
|
+
* what the item *is* on one side and what was *asked for* on the other.
|
|
100
|
+
*
|
|
101
|
+
* **A `choice` item may hold several values, and then it answers to any of
|
|
102
|
+
* them.** The asymmetry is the point: an escape room is adventure *and* sci-fi,
|
|
103
|
+
* a film is a comedy *and* a drama, but the tab strip above them still picks
|
|
104
|
+
* one, and the URL still carries one. Widened here rather than given a kind of
|
|
105
|
+
* its own, because a second kind would carry the same state, the same
|
|
106
|
+
* parameter, the same `set` semantics and the same control, and differ by one
|
|
107
|
+
* operator — two kinds doing one job, which is exactly what `Field` refuses
|
|
108
|
+
* when it declines to let `flag` become a tri-state.
|
|
109
|
+
*
|
|
110
|
+
* A list whose items each sit in one category writes a bare string and is
|
|
111
|
+
* untouched by this.
|
|
112
|
+
*/
|
|
113
|
+
export type ItemValue<F extends Field> = F["kind"] extends "flag"
|
|
114
|
+
? boolean
|
|
115
|
+
: F["kind"] extends "choice"
|
|
116
|
+
? string | readonly string[]
|
|
117
|
+
: string;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* What one field contributes.
|
|
121
|
+
*
|
|
122
|
+
* @deprecated Say which side you are on: `StateValue` or `ItemValue`. This
|
|
123
|
+
* named both while they were the same type, and is `StateValue` now.
|
|
124
|
+
*/
|
|
125
|
+
export type FieldValue<F extends Field> = StateValue<F>;
|
|
126
|
+
|
|
93
127
|
/** Every field's value for one item, derived from the field declaration. */
|
|
94
128
|
export type ItemValues<F extends FieldMap> = {
|
|
95
|
-
readonly [K in keyof F]:
|
|
129
|
+
readonly [K in keyof F]: ItemValue<F[K]>;
|
|
96
130
|
};
|
|
97
131
|
|
|
98
132
|
/** What the list is filtered to right now. */
|
|
99
133
|
export type FilterState<F extends FieldMap> = {
|
|
100
|
-
readonly [K in keyof F]:
|
|
134
|
+
readonly [K in keyof F]: StateValue<F[K]>;
|
|
101
135
|
};
|
|
102
136
|
|
|
103
137
|
export interface FilterItem<F extends FieldMap> {
|
|
@@ -168,7 +202,7 @@ export interface Filters<F extends FieldMap> {
|
|
|
168
202
|
readonly matched: ReadonlySet<string>;
|
|
169
203
|
set<K extends keyof F>(
|
|
170
204
|
field: K,
|
|
171
|
-
value:
|
|
205
|
+
value: StateValue<F[K]>,
|
|
172
206
|
options?: SetOptions
|
|
173
207
|
): void;
|
|
174
208
|
/** Clears one field, or all of them. Always a push: clearing is deliberate. */
|
|
@@ -268,8 +302,8 @@ export function filters<const F extends FieldMap>(
|
|
|
268
302
|
haystacks.set(item.key, perField);
|
|
269
303
|
}
|
|
270
304
|
|
|
271
|
-
const empty = (name: keyof F & string):
|
|
272
|
-
(fields[name]?.kind === "flag" ? false : "") as
|
|
305
|
+
const empty = (name: keyof F & string): StateValue<F[typeof name]> =>
|
|
306
|
+
(fields[name]?.kind === "flag" ? false : "") as StateValue<
|
|
273
307
|
F[typeof name]
|
|
274
308
|
>;
|
|
275
309
|
|
|
@@ -323,7 +357,14 @@ export function filters<const F extends FieldMap>(
|
|
|
323
357
|
} else if (kind === "choice") {
|
|
324
358
|
const wanted = state[name] as string;
|
|
325
359
|
if (wanted === "") continue;
|
|
326
|
-
|
|
360
|
+
// An item may sit in several categories while the control
|
|
361
|
+
// still picks one — see `ItemValue`. One value is the same
|
|
362
|
+
// question asked of a set of one.
|
|
363
|
+
const held = item.values[name];
|
|
364
|
+
const holds = Array.isArray(held)
|
|
365
|
+
? held.includes(wanted)
|
|
366
|
+
: held === wanted;
|
|
367
|
+
if (!holds) {
|
|
327
368
|
hit = false;
|
|
328
369
|
break;
|
|
329
370
|
}
|