skillet-shared 0.1.0 → 0.2.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 +2 -1
- package/dist/history.d.ts +15 -0
- package/dist/history.js +48 -0
- package/dist/posts.d.ts +20 -0
- package/dist/posts.js +25 -0
- package/dist/relative-time.d.ts +1 -0
- package/dist/relative-time.js +30 -0
- package/package.json +23 -7
- package/src/history.ts +0 -68
- package/src/posts.ts +0 -37
- package/src/relative-time.ts +0 -27
- /package/{src → dist}/tokens.css +0 -0
package/README.md
CHANGED
|
@@ -11,4 +11,5 @@ Both apps run it through Vite, so imports keep their `.ts` form and need no buil
|
|
|
11
11
|
|
|
12
12
|
- `pnpm test` and `pnpm typecheck` run the package's own suite
|
|
13
13
|
- In an app, `pnpm link ../skillet-shared` tests an edit before it is tagged
|
|
14
|
-
- Release:
|
|
14
|
+
- Release: `pnpm version patch` (or `minor`). It tests, tags and pushes, and CI publishes to npm through trusted publishing
|
|
15
|
+
- Then bump the range in each app
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const HISTORY_MAX = 24;
|
|
2
|
+
export interface History<T> {
|
|
3
|
+
readonly past: readonly T[];
|
|
4
|
+
readonly future: readonly T[];
|
|
5
|
+
}
|
|
6
|
+
export declare const EMPTY_HISTORY: History<never>;
|
|
7
|
+
export declare function record<T>(h: History<T>, prev: T, same: (a: T, b: T) => boolean): History<T>;
|
|
8
|
+
export declare function stepBack<T>(h: History<T>, current: T): {
|
|
9
|
+
history: History<T>;
|
|
10
|
+
value: T;
|
|
11
|
+
} | null;
|
|
12
|
+
export declare function stepForward<T>(h: History<T>, current: T): {
|
|
13
|
+
history: History<T>;
|
|
14
|
+
value: T;
|
|
15
|
+
} | null;
|
package/dist/history.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// A bounded undo/redo walk over whole-board snapshots.
|
|
2
|
+
//
|
|
3
|
+
// One step back would be enough if every destructive move were deliberate. It
|
|
4
|
+
// stops being enough the moment finding a sound becomes a search: mutate,
|
|
5
|
+
// mutate, random, mutate, and the board worth keeping is three steps behind the
|
|
6
|
+
// one you can hear, with no way back to it. So the walk is retraceable in both
|
|
7
|
+
// directions — which is also what makes a wild jitter safe to try, since the
|
|
8
|
+
// cost of a bad roll is one keystroke rather than the board you had.
|
|
9
|
+
//
|
|
10
|
+
// Pure and generic on purpose: the interesting behaviour is the dedupe, the cap
|
|
11
|
+
// and when the redo tail dies, and all three are worth pinning down in a test.
|
|
12
|
+
// Deep enough to cover a run of rolls, small enough that a snapshot per entry
|
|
13
|
+
// stays free — a couple of hundred numbers each.
|
|
14
|
+
export const HISTORY_MAX = 24;
|
|
15
|
+
export const EMPTY_HISTORY = { past: [], future: [] };
|
|
16
|
+
// Record the state being replaced. `same` decides what counts as no movement:
|
|
17
|
+
// more than one path snapshots the same board on the way to a single change, and
|
|
18
|
+
// a stack of identical entries turns undo into a key you press four times to
|
|
19
|
+
// hear anything.
|
|
20
|
+
export function record(h, prev, same) {
|
|
21
|
+
const top = h.past.at(-1);
|
|
22
|
+
if (top !== undefined && same(top, prev)) {
|
|
23
|
+
// Still a new branch: the redo tail belonged to a walk this write leaves.
|
|
24
|
+
return h.future.length === 0 ? h : { past: h.past, future: [] };
|
|
25
|
+
}
|
|
26
|
+
return { past: [...h.past, prev].slice(-HISTORY_MAX), future: [] };
|
|
27
|
+
}
|
|
28
|
+
// Step back. `current` is the live state, which becomes the head of the redo
|
|
29
|
+
// tail — the caller owns it, so it is passed in rather than kept here and left
|
|
30
|
+
// to drift.
|
|
31
|
+
export function stepBack(h, current) {
|
|
32
|
+
const value = h.past.at(-1);
|
|
33
|
+
if (value === undefined)
|
|
34
|
+
return null;
|
|
35
|
+
return {
|
|
36
|
+
history: { past: h.past.slice(0, -1), future: [current, ...h.future] },
|
|
37
|
+
value,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function stepForward(h, current) {
|
|
41
|
+
const [value, ...rest] = h.future;
|
|
42
|
+
if (value === undefined)
|
|
43
|
+
return null;
|
|
44
|
+
return {
|
|
45
|
+
history: { past: [...h.past, current].slice(-HISTORY_MAX), future: rest },
|
|
46
|
+
value,
|
|
47
|
+
};
|
|
48
|
+
}
|
package/dist/posts.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from 'astro/zod';
|
|
2
|
+
export declare const postSchema: z.ZodObject<{
|
|
3
|
+
title: z.ZodString;
|
|
4
|
+
date: z.ZodCoercedDate<unknown>;
|
|
5
|
+
summary: z.ZodString;
|
|
6
|
+
image: z.ZodString;
|
|
7
|
+
imageAlt: z.ZodString;
|
|
8
|
+
look: z.ZodOptional<z.ZodString>;
|
|
9
|
+
draft: z.ZodDefault<z.ZodBoolean>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export type PostData = z.infer<typeof postSchema>;
|
|
12
|
+
export declare const published: (post: {
|
|
13
|
+
data: PostData;
|
|
14
|
+
}) => boolean;
|
|
15
|
+
export declare const isoDay: (date: Date) => string;
|
|
16
|
+
export declare function longDate(date: Date): string;
|
|
17
|
+
export declare function newestFirst<T extends {
|
|
18
|
+
id: string;
|
|
19
|
+
data: PostData;
|
|
20
|
+
}>(posts: T[]): T[];
|
package/dist/posts.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The blog's posts are markdown files written by hand. `scripts/blogdraft.mjs`
|
|
2
|
+
// drafts one from the commits since the last post.
|
|
3
|
+
import { z } from 'astro/zod';
|
|
4
|
+
export const postSchema = z.object({
|
|
5
|
+
title: z.string(),
|
|
6
|
+
date: z.coerce.date(),
|
|
7
|
+
summary: z.string(),
|
|
8
|
+
image: z.string(),
|
|
9
|
+
imageAlt: z.string(),
|
|
10
|
+
look: z.string().optional(),
|
|
11
|
+
draft: z.boolean().default(false),
|
|
12
|
+
});
|
|
13
|
+
export const published = (post) => !post.data.draft;
|
|
14
|
+
export const isoDay = (date) => date.toISOString().slice(0, 10);
|
|
15
|
+
export function longDate(date) {
|
|
16
|
+
return date.toLocaleDateString('en-US', {
|
|
17
|
+
year: 'numeric',
|
|
18
|
+
month: 'long',
|
|
19
|
+
day: 'numeric',
|
|
20
|
+
timeZone: 'UTC',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function newestFirst(posts) {
|
|
24
|
+
return posts.toSorted((a, b) => b.data.date.getTime() - a.data.date.getTime() || a.id.localeCompare(b.id));
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sinceWords(at: number, now: number): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// How long ago something happened, in the words the home page puts on a resume
|
|
2
|
+
// card and under a saved voice. Coarse on purpose: the reader is picking which
|
|
3
|
+
// of their own boards to open, so "3 days ago" is the whole answer and a date
|
|
4
|
+
// is one more thing to read.
|
|
5
|
+
const MINUTE = 60_000;
|
|
6
|
+
const HOUR = 60 * MINUTE;
|
|
7
|
+
const DAY = 24 * HOUR;
|
|
8
|
+
const MONTH = 30 * DAY;
|
|
9
|
+
const YEAR = 365 * DAY;
|
|
10
|
+
const count = (ms, unit, name) => {
|
|
11
|
+
const n = Math.floor(ms / unit);
|
|
12
|
+
return `${n} ${name}${n === 1 ? '' : 's'} ago`;
|
|
13
|
+
};
|
|
14
|
+
// A clock that ran backwards — a machine whose time was wrong when the save
|
|
15
|
+
// landed, or two machines disagreeing — reads as "just now" rather than as a
|
|
16
|
+
// negative count.
|
|
17
|
+
export function sinceWords(at, now) {
|
|
18
|
+
const ms = now - at;
|
|
19
|
+
if (!Number.isFinite(ms) || ms < MINUTE)
|
|
20
|
+
return 'just now';
|
|
21
|
+
if (ms < HOUR)
|
|
22
|
+
return count(ms, MINUTE, 'minute');
|
|
23
|
+
if (ms < DAY)
|
|
24
|
+
return count(ms, HOUR, 'hour');
|
|
25
|
+
if (ms < MONTH)
|
|
26
|
+
return count(ms, DAY, 'day');
|
|
27
|
+
if (ms < YEAR)
|
|
28
|
+
return count(ms, MONTH, 'month');
|
|
29
|
+
return count(ms, YEAR, 'year');
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,24 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillet-shared",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Code shared by bender and videoskillet",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Colin Diesh",
|
|
7
7
|
"repository": "github:cmdcolin/skillet-shared",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"exports": {
|
|
10
|
-
"./history":
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
"./history": {
|
|
11
|
+
"types": "./dist/history.d.ts",
|
|
12
|
+
"default": "./dist/history.js"
|
|
13
|
+
},
|
|
14
|
+
"./relative-time": {
|
|
15
|
+
"types": "./dist/relative-time.d.ts",
|
|
16
|
+
"default": "./dist/relative-time.js"
|
|
17
|
+
},
|
|
18
|
+
"./posts": {
|
|
19
|
+
"types": "./dist/posts.d.ts",
|
|
20
|
+
"default": "./dist/posts.js"
|
|
21
|
+
},
|
|
22
|
+
"./tokens.css": "./dist/tokens.css"
|
|
14
23
|
},
|
|
15
|
-
"files": [
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
16
27
|
"publishConfig": {
|
|
17
28
|
"access": "public"
|
|
18
29
|
},
|
|
30
|
+
"packageManager": "pnpm@11.20.0",
|
|
19
31
|
"scripts": {
|
|
20
32
|
"test": "vitest run",
|
|
21
|
-
"typecheck": "tsc"
|
|
33
|
+
"typecheck": "tsc",
|
|
34
|
+
"build": "tsc -p tsconfig.build.json && cp src/tokens.css dist/",
|
|
35
|
+
"prepack": "pnpm build",
|
|
36
|
+
"preversion": "pnpm typecheck && pnpm test",
|
|
37
|
+
"postversion": "git push --follow-tags"
|
|
22
38
|
},
|
|
23
39
|
"peerDependencies": {
|
|
24
40
|
"astro": "^7.4.2"
|
package/src/history.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// A bounded undo/redo walk over whole-board snapshots.
|
|
2
|
-
//
|
|
3
|
-
// One step back would be enough if every destructive move were deliberate. It
|
|
4
|
-
// stops being enough the moment finding a sound becomes a search: mutate,
|
|
5
|
-
// mutate, random, mutate, and the board worth keeping is three steps behind the
|
|
6
|
-
// one you can hear, with no way back to it. So the walk is retraceable in both
|
|
7
|
-
// directions — which is also what makes a wild jitter safe to try, since the
|
|
8
|
-
// cost of a bad roll is one keystroke rather than the board you had.
|
|
9
|
-
//
|
|
10
|
-
// Pure and generic on purpose: the interesting behaviour is the dedupe, the cap
|
|
11
|
-
// and when the redo tail dies, and all three are worth pinning down in a test.
|
|
12
|
-
|
|
13
|
-
// Deep enough to cover a run of rolls, small enough that a snapshot per entry
|
|
14
|
-
// stays free — a couple of hundred numbers each.
|
|
15
|
-
export const HISTORY_MAX = 24
|
|
16
|
-
|
|
17
|
-
export interface History<T> {
|
|
18
|
-
// Oldest first, so the most recent step back is the last element.
|
|
19
|
-
readonly past: readonly T[]
|
|
20
|
-
// Nearest first: the state undo just left, ready to be stepped back into.
|
|
21
|
-
readonly future: readonly T[]
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const EMPTY_HISTORY: History<never> = { past: [], future: [] }
|
|
25
|
-
|
|
26
|
-
// Record the state being replaced. `same` decides what counts as no movement:
|
|
27
|
-
// more than one path snapshots the same board on the way to a single change, and
|
|
28
|
-
// a stack of identical entries turns undo into a key you press four times to
|
|
29
|
-
// hear anything.
|
|
30
|
-
export function record<T>(
|
|
31
|
-
h: History<T>,
|
|
32
|
-
prev: T,
|
|
33
|
-
same: (a: T, b: T) => boolean,
|
|
34
|
-
): History<T> {
|
|
35
|
-
const top = h.past.at(-1)
|
|
36
|
-
if (top !== undefined && same(top, prev)) {
|
|
37
|
-
// Still a new branch: the redo tail belonged to a walk this write leaves.
|
|
38
|
-
return h.future.length === 0 ? h : { past: h.past, future: [] }
|
|
39
|
-
}
|
|
40
|
-
return { past: [...h.past, prev].slice(-HISTORY_MAX), future: [] }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Step back. `current` is the live state, which becomes the head of the redo
|
|
44
|
-
// tail — the caller owns it, so it is passed in rather than kept here and left
|
|
45
|
-
// to drift.
|
|
46
|
-
export function stepBack<T>(
|
|
47
|
-
h: History<T>,
|
|
48
|
-
current: T,
|
|
49
|
-
): { history: History<T>; value: T } | null {
|
|
50
|
-
const value = h.past.at(-1)
|
|
51
|
-
if (value === undefined) return null
|
|
52
|
-
return {
|
|
53
|
-
history: { past: h.past.slice(0, -1), future: [current, ...h.future] },
|
|
54
|
-
value,
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function stepForward<T>(
|
|
59
|
-
h: History<T>,
|
|
60
|
-
current: T,
|
|
61
|
-
): { history: History<T>; value: T } | null {
|
|
62
|
-
const [value, ...rest] = h.future
|
|
63
|
-
if (value === undefined) return null
|
|
64
|
-
return {
|
|
65
|
-
history: { past: [...h.past, current].slice(-HISTORY_MAX), future: rest },
|
|
66
|
-
value,
|
|
67
|
-
}
|
|
68
|
-
}
|
package/src/posts.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// The blog's posts are markdown files written by hand. `scripts/blogdraft.mjs`
|
|
2
|
-
// drafts one from the commits since the last post.
|
|
3
|
-
import { z } from 'astro/zod'
|
|
4
|
-
|
|
5
|
-
export const postSchema = z.object({
|
|
6
|
-
title: z.string(),
|
|
7
|
-
date: z.coerce.date(),
|
|
8
|
-
summary: z.string(),
|
|
9
|
-
image: z.string(),
|
|
10
|
-
imageAlt: z.string(),
|
|
11
|
-
look: z.string().optional(),
|
|
12
|
-
draft: z.boolean().default(false),
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
export type PostData = z.infer<typeof postSchema>
|
|
16
|
-
|
|
17
|
-
export const published = (post: { data: PostData }) => !post.data.draft
|
|
18
|
-
|
|
19
|
-
export const isoDay = (date: Date) => date.toISOString().slice(0, 10)
|
|
20
|
-
|
|
21
|
-
export function longDate(date: Date) {
|
|
22
|
-
return date.toLocaleDateString('en-US', {
|
|
23
|
-
year: 'numeric',
|
|
24
|
-
month: 'long',
|
|
25
|
-
day: 'numeric',
|
|
26
|
-
timeZone: 'UTC',
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function newestFirst<T extends { id: string; data: PostData }>(
|
|
31
|
-
posts: T[],
|
|
32
|
-
) {
|
|
33
|
-
return posts.toSorted(
|
|
34
|
-
(a, b) =>
|
|
35
|
-
b.data.date.getTime() - a.data.date.getTime() || a.id.localeCompare(b.id),
|
|
36
|
-
)
|
|
37
|
-
}
|
package/src/relative-time.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
// How long ago something happened, in the words the home page puts on a resume
|
|
2
|
-
// card and under a saved voice. Coarse on purpose: the reader is picking which
|
|
3
|
-
// of their own boards to open, so "3 days ago" is the whole answer and a date
|
|
4
|
-
// is one more thing to read.
|
|
5
|
-
const MINUTE = 60_000
|
|
6
|
-
const HOUR = 60 * MINUTE
|
|
7
|
-
const DAY = 24 * HOUR
|
|
8
|
-
const MONTH = 30 * DAY
|
|
9
|
-
const YEAR = 365 * DAY
|
|
10
|
-
|
|
11
|
-
const count = (ms: number, unit: number, name: string): string => {
|
|
12
|
-
const n = Math.floor(ms / unit)
|
|
13
|
-
return `${n} ${name}${n === 1 ? '' : 's'} ago`
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// A clock that ran backwards — a machine whose time was wrong when the save
|
|
17
|
-
// landed, or two machines disagreeing — reads as "just now" rather than as a
|
|
18
|
-
// negative count.
|
|
19
|
-
export function sinceWords(at: number, now: number): string {
|
|
20
|
-
const ms = now - at
|
|
21
|
-
if (!Number.isFinite(ms) || ms < MINUTE) return 'just now'
|
|
22
|
-
if (ms < HOUR) return count(ms, MINUTE, 'minute')
|
|
23
|
-
if (ms < DAY) return count(ms, HOUR, 'hour')
|
|
24
|
-
if (ms < MONTH) return count(ms, DAY, 'day')
|
|
25
|
-
if (ms < YEAR) return count(ms, MONTH, 'month')
|
|
26
|
-
return count(ms, YEAR, 'year')
|
|
27
|
-
}
|
/package/{src → dist}/tokens.css
RENAMED
|
File without changes
|