skillet-shared 0.1.0 → 0.3.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 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: bump `version`, tag it, `npm publish`, then bump the range in each app
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 @@
1
+ export declare function startAnalytics(): void;
@@ -0,0 +1,16 @@
1
+ // A hit counter on every page the site serves. GoatCounter counts page views
2
+ // and sets no cookies, so no page asks first; /privacy/ says what it collects.
3
+ const COUNTER = 'https://cmdcolin.goatcounter.com/count';
4
+ // A browser a script is driving counts nothing: the harnesses that screenshot
5
+ // and record these pages are nobody's visit.
6
+ export function startAnalytics() {
7
+ if (navigator.webdriver)
8
+ return;
9
+ if (document.querySelector('script[data-goatcounter]'))
10
+ return;
11
+ const tag = document.createElement('script');
12
+ tag.async = true;
13
+ tag.src = 'https://gc.zgo.at/count.js';
14
+ tag.dataset.goatcounter = COUNTER;
15
+ document.head.append(tag);
16
+ }
package/dist/dom.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const need: (id: string) => HTMLElement;
package/dist/dom.js ADDED
@@ -0,0 +1,6 @@
1
+ export const need = (id) => {
2
+ const node = document.getElementById(id);
3
+ if (node === null)
4
+ throw new Error(`no #${id}`);
5
+ return node;
6
+ };
@@ -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;
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,19 @@
1
+ export type Point = readonly [number, number];
2
+ /**
3
+ * A run through the given corners, each rounded off by `radius`.
4
+ *
5
+ * The radius is capped at half of the shorter leg either side of a corner, so a
6
+ * short leg between two turns rounds to what fits instead of overshooting into
7
+ * the next one. Two points are a straight line and no corner at all.
8
+ */
9
+ export declare function route(pts: readonly Point[], radius: number): string;
10
+ /**
11
+ * The head on the end of a run: a triangle whose tip is the last point and
12
+ * whose base sits `len` back along the last leg, `half` to either side of it.
13
+ *
14
+ * Off the same list the wire is routed from, so the head cannot point somewhere
15
+ * the wire does not go. The two numbers are the drawing's, not this file's —
16
+ * the miniature and the card set their type at different sizes and their heads
17
+ * with it.
18
+ */
19
+ export declare function arrowhead(pts: readonly Point[], len: number, half: number): string;
@@ -0,0 +1,59 @@
1
+ const round = (n) => Math.round(n * 1000) / 1000;
2
+ const at = (p) => `${round(p[0])} ${round(p[1])}`;
3
+ const dist = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
4
+ // `by` units from `from` along the line to `to`. Clamped by the caller, not
5
+ // here: a zero-length leg has no direction to travel, and the corner it belongs
6
+ // to is the one place that knows what to do about it.
7
+ const toward = (from, to, by) => {
8
+ const d = dist(from, to);
9
+ if (d === 0)
10
+ return from;
11
+ return [
12
+ from[0] + ((to[0] - from[0]) * by) / d,
13
+ from[1] + ((to[1] - from[1]) * by) / d,
14
+ ];
15
+ };
16
+ /**
17
+ * A run through the given corners, each rounded off by `radius`.
18
+ *
19
+ * The radius is capped at half of the shorter leg either side of a corner, so a
20
+ * short leg between two turns rounds to what fits instead of overshooting into
21
+ * the next one. Two points are a straight line and no corner at all.
22
+ */
23
+ export function route(pts, radius) {
24
+ if (pts.length === 0)
25
+ return '';
26
+ const d = [`M${at(pts[0])}`];
27
+ for (let i = 1; i < pts.length - 1; i++) {
28
+ const prev = pts[i - 1];
29
+ const corner = pts[i];
30
+ const next = pts[i + 1];
31
+ const r = Math.min(radius, dist(prev, corner) / 2, dist(corner, next) / 2);
32
+ d.push(`L${at(toward(corner, prev, r))}`);
33
+ d.push(`Q${at(corner)} ${at(toward(corner, next, r))}`);
34
+ }
35
+ d.push(`L${at(pts[pts.length - 1])}`);
36
+ return d.join('');
37
+ }
38
+ /**
39
+ * The head on the end of a run: a triangle whose tip is the last point and
40
+ * whose base sits `len` back along the last leg, `half` to either side of it.
41
+ *
42
+ * Off the same list the wire is routed from, so the head cannot point somewhere
43
+ * the wire does not go. The two numbers are the drawing's, not this file's —
44
+ * the miniature and the card set their type at different sizes and their heads
45
+ * with it.
46
+ */
47
+ export function arrowhead(pts, len, half) {
48
+ if (pts.length < 2)
49
+ return '';
50
+ const tip = pts[pts.length - 1];
51
+ const back = toward(tip, pts[pts.length - 2], len);
52
+ // The perpendicular to the last leg, at unit length times `half`.
53
+ const [dx, dy] = [tip[0] - back[0], tip[1] - back[1]];
54
+ const n = Math.hypot(dx, dy);
55
+ if (n === 0)
56
+ return '';
57
+ const [px, py] = [(-dy / n) * half, (dx / n) * half];
58
+ return `M${at([back[0] + px, back[1] + py])}L${at(tip)}L${at([back[0] - px, back[1] - py])}Z`;
59
+ }
package/package.json CHANGED
@@ -1,24 +1,52 @@
1
1
  {
2
2
  "name": "skillet-shared",
3
- "version": "0.1.0",
3
+ "version": "0.3.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": "./src/history.ts",
11
- "./relative-time": "./src/relative-time.ts",
12
- "./posts": "./src/posts.ts",
13
- "./tokens.css": "./src/tokens.css"
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
+ "./analytics": {
23
+ "types": "./dist/analytics.d.ts",
24
+ "default": "./dist/analytics.js"
25
+ },
26
+ "./dom": {
27
+ "types": "./dist/dom.d.ts",
28
+ "default": "./dist/dom.js"
29
+ },
30
+ "./wire-route": {
31
+ "types": "./dist/wire-route.d.ts",
32
+ "default": "./dist/wire-route.js"
33
+ },
34
+ "./tokens.css": "./dist/tokens.css"
14
35
  },
15
- "files": ["src", "!src/**/*.test.ts"],
36
+ "files": [
37
+ "dist"
38
+ ],
16
39
  "publishConfig": {
17
40
  "access": "public"
18
41
  },
42
+ "packageManager": "pnpm@11.20.0",
19
43
  "scripts": {
20
44
  "test": "vitest run",
21
- "typecheck": "tsc"
45
+ "typecheck": "tsc",
46
+ "build": "tsc -p tsconfig.build.json && cp src/tokens.css dist/",
47
+ "prepack": "pnpm build",
48
+ "preversion": "pnpm typecheck && pnpm test",
49
+ "postversion": "git push --follow-tags"
22
50
  },
23
51
  "peerDependencies": {
24
52
  "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
- }
@@ -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
- }
File without changes