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 +2 -1
- package/dist/analytics.d.ts +1 -0
- package/dist/analytics.js +16 -0
- package/dist/dom.d.ts +1 -0
- package/dist/dom.js +6 -0
- 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/dist/wire-route.d.ts +19 -0
- package/dist/wire-route.js +59 -0
- package/package.json +35 -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 @@
|
|
|
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,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
|
+
}
|
|
@@ -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.
|
|
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":
|
|
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
|
+
"./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": [
|
|
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
|
-
}
|
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
|