skillet-shared 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Colin Diesh
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 all
13
+ 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 THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,14 @@
1
- # Temporary Holding Version
1
+ # skillet-shared
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Code that bender and videoskillet both ship, as raw TypeScript and CSS.
4
+
5
+ - **In:** nothing at build time; each app compiles the source itself
6
+ - **Out:** modules under `skillet-shared/<name>`, listed in `exports`
7
+
8
+ Both apps run it through Vite, so imports keep their `.ts` form and need no build step.
9
+
10
+ ## Develop
11
+
12
+ - `pnpm test` and `pnpm typecheck` run the package's own suite
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
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "name": "skillet-shared",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Code shared by bender and videoskillet",
5
+ "license": "MIT",
6
+ "author": "Colin Diesh",
7
+ "repository": "github:cmdcolin/skillet-shared",
8
+ "type": "module",
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"
14
+ },
15
+ "files": ["src", "!src/**/*.test.ts"],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "test": "vitest run",
21
+ "typecheck": "tsc"
22
+ },
23
+ "peerDependencies": {
24
+ "astro": "^7.4.2"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^26.6.5",
28
+ "astro": "^7.4.2",
29
+ "typescript": "^7.0.2",
30
+ "vitest": "^5.0.5"
31
+ }
32
+ }
package/src/history.ts ADDED
@@ -0,0 +1,68 @@
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 ADDED
@@ -0,0 +1,37 @@
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
+ }
@@ -0,0 +1,27 @@
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/tokens.css ADDED
@@ -0,0 +1,65 @@
1
+ /* The design system videoskillet and bender share: neutrals, borders, type,
2
+ spacing and corners, for the static site and the app alike. Each product
3
+ adds its own accent, signal colours and reset in its own sheet, and nothing
4
+ here names one.
5
+
6
+ Four text tiers: primary reading text, secondary (headers, labels about
7
+ labels), tertiary (prose, meta), and faint (icon affordances, placeholders).
8
+ --fg3 clears 8:1 on --surface, so explanatory prose reads at a glance.
9
+ Anything dimmer than --fg4 is decoration only.
10
+
11
+ The type scale is in rem, which nests without compounding and honours a
12
+ reader who set their browser's default font size. The comments give the px
13
+ each size lands on at the 16px default. */
14
+ :root {
15
+ /* Tells the browser the UI is dark, so scrollbars, dropped lists, clear
16
+ buttons and focus rings come out dark too. */
17
+ color-scheme: dark;
18
+ --font: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
19
+ --mono: ui-monospace, 'SF Mono', 'Cascadia Code', Menlo, monospace;
20
+
21
+ /* The page, a card or panel on it, and a filled control on that. */
22
+ --bg: #0e0e11;
23
+ --surface: #16161a;
24
+ --surface-raised: #26262e;
25
+
26
+ --fg: #e6e6ee;
27
+ --fg2: #c6c6d6;
28
+ --fg3: #a6a6ba;
29
+ --fg4: #85859a;
30
+
31
+ /* --border is a surface's own edge. --border-quiet outlines chrome that sits
32
+ beside the controls it acts on. Both lift to --border-hover under the
33
+ pointer. --border-faint is a hairline that only has to exist. */
34
+ --border: #44444f;
35
+ --border-quiet: #33333f;
36
+ --border-hover: #63637a;
37
+ --border-faint: #2e2e38;
38
+
39
+ /* A failed write, or a button whose job is to remove something. */
40
+ --danger: #e08484;
41
+
42
+ --fs-xs: 0.6875rem; /* 11px */
43
+ --fs-sm: 0.75rem; /* 12px */
44
+ --fs: 0.8125rem; /* 13px */
45
+ --fs-md: 0.875rem; /* 14px */
46
+ --fs-lg: 1rem; /* 16px */
47
+ --fs-xl: 1.25rem; /* 20px */
48
+ /* Prose wraps to three to five lines in a narrow panel, where 1.2 is
49
+ unreadable. */
50
+ --prose-lh: 1.45;
51
+
52
+ /* The spacing scale, for gaps, padding and margins, in px because a panel's
53
+ density stays put when the reader scales their text. */
54
+ --space-2xs: 2px;
55
+ --space-xs: 4px;
56
+ --space-sm: 6px;
57
+ --space-md: 8px;
58
+ --space-lg: 12px;
59
+ --space-xl: 16px;
60
+ --space-2xl: 24px;
61
+
62
+ /* A control's corner, and a pill's at any height. */
63
+ --ctl-radius: 3px;
64
+ --pill-radius: 999px;
65
+ }