cursedbelt 3.0.2 → 4.0.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/dist/react/components/ListToolbar.d.ts +94 -0
- package/dist/react/components/ListToolbar.d.ts.map +1 -0
- package/dist/react/components/ListToolbar.js +66 -0
- package/dist/react/components/ListToolbar.js.map +1 -0
- package/dist/react/file-tree/FileTree.d.ts +282 -3
- package/dist/react/file-tree/FileTree.d.ts.map +1 -1
- package/dist/react/file-tree/FileTree.js +799 -131
- package/dist/react/file-tree/FileTree.js.map +1 -1
- package/dist/react/file-tree/fileTree.css +151 -0
- package/dist/react/file-tree/fileTreeSelection.d.ts +187 -0
- package/dist/react/file-tree/fileTreeSelection.d.ts.map +1 -0
- package/dist/react/file-tree/fileTreeSelection.js +264 -0
- package/dist/react/file-tree/fileTreeSelection.js.map +1 -0
- package/dist/react/file-tree/index.d.ts +1 -0
- package/dist/react/file-tree/index.d.ts.map +1 -1
- package/dist/react/file-tree/index.js +8 -0
- package/dist/react/file-tree/index.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/lib/form.d.ts +3 -0
- package/dist/react/lib/form.d.ts.map +1 -1
- package/dist/react/lib/form.js +43 -5
- package/dist/react/lib/form.js.map +1 -1
- package/dist/react/media/DenseGalleryShell.d.ts +19 -3
- package/dist/react/media/DenseGalleryShell.d.ts.map +1 -1
- package/dist/react/media/DenseGalleryShell.js +55 -15
- package/dist/react/media/DenseGalleryShell.js.map +1 -1
- package/dist/react/media/FocusViewer.d.ts.map +1 -1
- package/dist/react/media/FocusViewer.js +14 -14
- package/dist/react/media/FocusViewer.js.map +1 -1
- package/dist/react/media/FrameGrabber.d.ts +23 -0
- package/dist/react/media/FrameGrabber.d.ts.map +1 -1
- package/dist/react/media/FrameGrabber.js +67 -1
- package/dist/react/media/FrameGrabber.js.map +1 -1
- package/dist/react/media/MediaResize.d.ts +51 -19
- package/dist/react/media/MediaResize.d.ts.map +1 -1
- package/dist/react/media/MediaResize.js +20 -13
- package/dist/react/media/MediaResize.js.map +1 -1
- package/dist/react/media/VideoChapterEditor.d.ts +67 -0
- package/dist/react/media/VideoChapterEditor.d.ts.map +1 -0
- package/dist/react/media/VideoChapterEditor.js +148 -0
- package/dist/react/media/VideoChapterEditor.js.map +1 -0
- package/dist/react/media/VideoPlayer.d.ts +46 -4
- package/dist/react/media/VideoPlayer.d.ts.map +1 -1
- package/dist/react/media/VideoPlayer.js +124 -11
- package/dist/react/media/VideoPlayer.js.map +1 -1
- package/dist/react/media/VideoTurner.d.ts +15 -2
- package/dist/react/media/VideoTurner.d.ts.map +1 -1
- package/dist/react/media/VideoTurner.js +29 -2
- package/dist/react/media/VideoTurner.js.map +1 -1
- package/dist/react/media/denseGallery.css +104 -46
- package/dist/react/media/hlsSource.d.ts +57 -0
- package/dist/react/media/hlsSource.d.ts.map +1 -1
- package/dist/react/media/hlsSource.js +177 -1
- package/dist/react/media/hlsSource.js.map +1 -1
- package/dist/react/media/index.d.ts +2 -0
- package/dist/react/media/index.d.ts.map +1 -1
- package/dist/react/media/index.js +6 -0
- package/dist/react/media/index.js.map +1 -1
- package/dist/react/media/mediaResize.css +127 -54
- package/dist/react/media/pictureEditor.css +52 -6
- package/dist/react/media/videoChapterEditor.css +203 -0
- package/dist/react/media/videoChapters.d.ts +117 -0
- package/dist/react/media/videoChapters.d.ts.map +1 -0
- package/dist/react/media/videoChapters.js +95 -0
- package/dist/react/media/videoChapters.js.map +1 -0
- package/dist/react/media-gallery/GalleryTable.d.ts +52 -1
- package/dist/react/media-gallery/GalleryTable.d.ts.map +1 -1
- package/dist/react/media-gallery/GalleryTable.js +40 -9
- package/dist/react/media-gallery/GalleryTable.js.map +1 -1
- package/dist/react/media-gallery/MediaGallery.d.ts +107 -4
- package/dist/react/media-gallery/MediaGallery.d.ts.map +1 -1
- package/dist/react/media-gallery/MediaGallery.js +484 -104
- package/dist/react/media-gallery/MediaGallery.js.map +1 -1
- package/dist/react/media-gallery/MediaMetaEditor.d.ts +48 -0
- package/dist/react/media-gallery/MediaMetaEditor.d.ts.map +1 -0
- package/dist/react/media-gallery/MediaMetaEditor.js +66 -0
- package/dist/react/media-gallery/MediaMetaEditor.js.map +1 -0
- package/dist/react/media-gallery/index.d.ts +2 -0
- package/dist/react/media-gallery/index.d.ts.map +1 -1
- package/dist/react/media-gallery/index.js +5 -0
- package/dist/react/media-gallery/index.js.map +1 -1
- package/dist/react/media-gallery/mediaGallery.css +180 -4
- package/dist/react/media-gallery/mediaMeta.d.ts +62 -0
- package/dist/react/media-gallery/mediaMeta.d.ts.map +1 -0
- package/dist/react/media-gallery/mediaMeta.js +56 -0
- package/dist/react/media-gallery/mediaMeta.js.map +1 -0
- package/dist/react/media-gallery/playbackPreferences.d.ts +77 -0
- package/dist/react/media-gallery/playbackPreferences.d.ts.map +1 -1
- package/dist/react/media-gallery/playbackPreferences.js +42 -0
- package/dist/react/media-gallery/playbackPreferences.js.map +1 -1
- package/dist/react/media-gallery/types.d.ts +34 -0
- package/dist/react/media-gallery/types.d.ts.map +1 -1
- package/dist/scripts/guardrailsEnforce.d.ts +29 -0
- package/dist/scripts/guardrailsEnforce.d.ts.map +1 -0
- package/dist/styles-areas/activity-bar.css +30 -0
- package/dist/styles-areas/analytics.css +672 -0
- package/dist/styles-areas/auth.css +154 -0
- package/dist/styles-areas/calendar.css +416 -0
- package/dist/styles-areas/chart-table.css +41 -0
- package/dist/styles-areas/charts.css +89 -0
- package/dist/styles-areas/chat.css +364 -0
- package/dist/styles-areas/clipboard.css +26 -0
- package/dist/styles-areas/code-editor.css +130 -0
- package/dist/styles-areas/companion-link.css +70 -0
- package/dist/styles-areas/core.css +2503 -0
- package/dist/styles-areas/dashboard-grid.css +84 -0
- package/dist/styles-areas/data-table.css +657 -0
- package/dist/styles-areas/deep-link.css +26 -0
- package/dist/styles-areas/diff-viewer.css +30 -0
- package/dist/styles-areas/disk-usage.css +262 -0
- package/dist/styles-areas/emoji.css +154 -0
- package/dist/styles-areas/fields.css +582 -0
- package/dist/styles-areas/file-tree.css +33 -0
- package/dist/styles-areas/filter-rail.css +235 -0
- package/dist/styles-areas/folder-tree.css +219 -0
- package/dist/styles-areas/keep-awake.css +26 -0
- package/dist/styles-areas/layout-engine.css +239 -0
- package/dist/styles-areas/markdown.css +87 -0
- package/dist/styles-areas/master-detail.css +239 -0
- package/dist/styles-areas/master-lock.css +26 -0
- package/dist/styles-areas/media-gallery.css +351 -0
- package/dist/styles-areas/media.css +658 -0
- package/dist/styles-areas/nav.css +488 -0
- package/dist/styles-areas/notifications.css +245 -0
- package/dist/styles-areas/overlays.css +133 -0
- package/dist/styles-areas/palette.css +140 -0
- package/dist/styles-areas/pdf-viewer.css +47 -0
- package/dist/styles-areas/problem-detail.css +97 -0
- package/dist/styles-areas/rich-text.css +167 -0
- package/dist/styles-areas/sharing.css +153 -0
- package/dist/styles-areas/spreadsheet.css +37 -0
- package/dist/styles-areas/stats.css +281 -0
- package/dist/styles-areas/test-report.css +133 -0
- package/dist/styles-areas/virtual.css +30 -0
- package/dist/styles-areas/wizard.css +381 -0
- package/dist/styles-areas/workbench.css +543 -0
- package/dist/styles-areas/workbook-viewer.css +44 -0
- package/dist/styles-static.css +34 -23
- package/dist/styles.css +34 -23
- package/package.json +111 -31
- package/scripts/checkDistExports.ts +67 -0
- package/scripts/cssRules.ts +125 -0
- package/scripts/fixtureAppCss.ts +344 -0
- package/scripts/generateAreaStyles.ts +253 -0
- package/scripts/guardrailsEnforce.spec.ts +62 -0
- package/scripts/guardrailsEnforce.ts +67 -3
- package/scripts/styleAreas.ts +585 -0
- package/scripts/verify.ts +5 -0
- package/src/barrelsReachNoOptionalPeer.spec.ts +117 -9
- package/src/docsMatchTheSplit.spec.ts +98 -0
- package/src/fixtureAppCss.spec.ts +177 -0
- package/src/namedSubpathsResolve.spec.ts +35 -0
- package/src/publishShape.spec.ts +67 -0
- package/src/react/components/ListToolbar.spec.tsx +172 -0
- package/src/react/components/ListToolbar.tsx +151 -0
- package/src/react/file-tree/FileTree.spec.tsx +1009 -0
- package/src/react/file-tree/FileTree.tsx +1459 -309
- package/src/react/file-tree/fileTree.css +151 -0
- package/src/react/file-tree/fileTreeSelection.spec.ts +327 -0
- package/src/react/file-tree/fileTreeSelection.ts +321 -0
- package/src/react/file-tree/index.ts +21 -0
- package/src/react/index.ts +1 -0
- package/src/react/lib/form.ts +51 -8
- package/src/react/media/DenseGalleryShell.tsx +108 -36
- package/src/react/media/FocusViewer.tsx +15 -8
- package/src/react/media/FrameGrabber.spec.tsx +175 -0
- package/src/react/media/FrameGrabber.tsx +67 -0
- package/src/react/media/MediaResize.spec.tsx +84 -14
- package/src/react/media/MediaResize.tsx +51 -19
- package/src/react/media/VideoChapterEditor.tsx +428 -0
- package/src/react/media/VideoPlayer.spec.tsx +111 -1
- package/src/react/media/VideoPlayer.tsx +158 -11
- package/src/react/media/VideoTurner.spec.tsx +67 -0
- package/src/react/media/VideoTurner.tsx +70 -15
- package/src/react/media/denseGallery.css +104 -46
- package/src/react/media/hlsSource.recovery.spec.ts +213 -0
- package/src/react/media/hlsSource.spec.ts +8 -0
- package/src/react/media/hlsSource.ts +189 -1
- package/src/react/media/index.ts +14 -0
- package/src/react/media/mediaResize.css +127 -54
- package/src/react/media/pictureEditor.css +52 -6
- package/src/react/media/videoChapterEditor.css +203 -0
- package/src/react/media/videoChapters.spec.ts +176 -0
- package/src/react/media/videoChapters.ts +181 -0
- package/src/react/media-gallery/GalleryTable.tsx +93 -3
- package/src/react/media-gallery/MediaGallery.spec.tsx +1156 -10
- package/src/react/media-gallery/MediaGallery.tsx +813 -95
- package/src/react/media-gallery/MediaMetaEditor.tsx +237 -0
- package/src/react/media-gallery/galleryTable.spec.ts +41 -1
- package/src/react/media-gallery/index.ts +7 -0
- package/src/react/media-gallery/mediaGallery.css +180 -4
- package/src/react/media-gallery/mediaMeta.spec.ts +95 -0
- package/src/react/media-gallery/mediaMeta.ts +101 -0
- package/src/react/media-gallery/playbackPreferences.spec.ts +172 -0
- package/src/react/media-gallery/playbackPreferences.ts +100 -0
- package/src/react/media-gallery/types.ts +34 -0
- package/src/shippedFilesAreTracked.spec.ts +69 -0
- package/src/styles-areas/activity-bar.css +30 -0
- package/src/styles-areas/analytics.css +672 -0
- package/src/styles-areas/auth.css +154 -0
- package/src/styles-areas/calendar.css +416 -0
- package/src/styles-areas/chart-table.css +41 -0
- package/src/styles-areas/charts.css +89 -0
- package/src/styles-areas/chat.css +364 -0
- package/src/styles-areas/clipboard.css +26 -0
- package/src/styles-areas/code-editor.css +130 -0
- package/src/styles-areas/companion-link.css +70 -0
- package/src/styles-areas/core.css +2503 -0
- package/src/styles-areas/dashboard-grid.css +84 -0
- package/src/styles-areas/data-table.css +657 -0
- package/src/styles-areas/deep-link.css +26 -0
- package/src/styles-areas/diff-viewer.css +30 -0
- package/src/styles-areas/disk-usage.css +262 -0
- package/src/styles-areas/emoji.css +154 -0
- package/src/styles-areas/fields.css +582 -0
- package/src/styles-areas/file-tree.css +33 -0
- package/src/styles-areas/filter-rail.css +235 -0
- package/src/styles-areas/folder-tree.css +219 -0
- package/src/styles-areas/keep-awake.css +26 -0
- package/src/styles-areas/layout-engine.css +239 -0
- package/src/styles-areas/markdown.css +87 -0
- package/src/styles-areas/master-detail.css +239 -0
- package/src/styles-areas/master-lock.css +26 -0
- package/src/styles-areas/media-gallery.css +351 -0
- package/src/styles-areas/media.css +658 -0
- package/src/styles-areas/nav.css +488 -0
- package/src/styles-areas/notifications.css +245 -0
- package/src/styles-areas/overlays.css +133 -0
- package/src/styles-areas/palette.css +140 -0
- package/src/styles-areas/pdf-viewer.css +47 -0
- package/src/styles-areas/problem-detail.css +97 -0
- package/src/styles-areas/rich-text.css +167 -0
- package/src/styles-areas/sharing.css +153 -0
- package/src/styles-areas/spreadsheet.css +37 -0
- package/src/styles-areas/stats.css +281 -0
- package/src/styles-areas/test-report.css +133 -0
- package/src/styles-areas/virtual.css +30 -0
- package/src/styles-areas/wizard.css +381 -0
- package/src/styles-areas/workbench.css +543 -0
- package/src/styles-areas/workbook-viewer.css +44 -0
- package/src/styles-static.css +34 -23
- package/src/styles.css +34 -23
- package/src/stylesAreas.spec.ts +247 -0
- package/src/stylesUtilitiesMatches.spec.ts +6 -77
- package/src/testFilesRunInParallel.spec.ts +79 -0
- package/src/typecheckCachesAreSeparate.spec.ts +128 -0
- package/src/verifyGraph.spec.ts +6 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { MediaGalleryItem } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tags and a description, expressed as a CHANGE rather than as a new value.
|
|
5
|
+
*
|
|
6
|
+
* ── Why a change and not a pair of fields ──────────────────────────────────────────────
|
|
7
|
+
* The owner's ask, `ideas/collections-09-14.md`, 2026-09-14: *"In media galleries have an
|
|
8
|
+
* option for apps to tag media and aadd descriptions to media."* The case he will actually
|
|
9
|
+
* hit is forty holiday photographs at once, and forty items do not share one tag list — each
|
|
10
|
+
* one has its own, and "the new tags" cannot be a single array without wiping thirty-nine
|
|
11
|
+
* lists that were never on screen.
|
|
12
|
+
*
|
|
13
|
+
* So the plural verb carries `addTags`/`removeTags`, which mean the same thing for one item
|
|
14
|
+
* as for forty, and a consumer writes ONE handler rather than a singular one plus a loop.
|
|
15
|
+
* That is the rule `tasks/Completed/240-*.md` states for the file tree's multi-select in as
|
|
16
|
+
* many words — *"One request for N ids, not N requests: the tree deliberately does not loop
|
|
17
|
+
* `onMove`, because a tree re-derived between each write is how a batch move half-lands."*
|
|
18
|
+
*
|
|
19
|
+
* 🔴 Everything here is case-INSENSITIVE and spelling-preserving. A vocabulary that mints
|
|
20
|
+
* `Bank`, `bank` and `BANK` as three tags is a vocabulary nothing can filter by, and the
|
|
21
|
+
* gallery's own chips would show all three. The first spelling seen wins; a tag that differs
|
|
22
|
+
* only in case from one already present is not an addition at all.
|
|
23
|
+
*/
|
|
24
|
+
export interface MediaMetaChange {
|
|
25
|
+
/** Tags to add to EVERY item in the set. Already trimmed, de-duplicated, never empty strings. */
|
|
26
|
+
addTags: string[];
|
|
27
|
+
/** Tags to remove from EVERY item in the set. Matched case-insensitively. */
|
|
28
|
+
removeTags: string[];
|
|
29
|
+
/**
|
|
30
|
+
* The new description, or `''` to clear it. `undefined` means "not part of this change" —
|
|
31
|
+
* which is what a bulk tag always sends, because one sentence cannot describe forty files
|
|
32
|
+
* and writing it onto all of them would delete thirty-nine that were never read.
|
|
33
|
+
*/
|
|
34
|
+
description?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Trim, drop the empties, drop case-insensitive duplicates keeping the first spelling. */
|
|
38
|
+
export function cleanTags(tags: readonly string[]): string[] {
|
|
39
|
+
const seen = new Set<string>();
|
|
40
|
+
const out: string[] = [];
|
|
41
|
+
for (const raw of tags) {
|
|
42
|
+
const tag = raw.trim();
|
|
43
|
+
if (!tag) continue;
|
|
44
|
+
const key = tag.toLowerCase();
|
|
45
|
+
if (seen.has(key)) continue;
|
|
46
|
+
seen.add(key);
|
|
47
|
+
out.push(tag);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What it takes to get from `before` to `after` — the single-item editor's save, expressed in
|
|
54
|
+
* the same two lists the bulk one sends, so both paths arrive at a consumer as one shape.
|
|
55
|
+
*/
|
|
56
|
+
export function tagDiff(
|
|
57
|
+
before: readonly string[] | undefined,
|
|
58
|
+
after: readonly string[],
|
|
59
|
+
): { addTags: string[]; removeTags: string[] } {
|
|
60
|
+
const was = cleanTags(before ?? []);
|
|
61
|
+
const now = cleanTags(after);
|
|
62
|
+
const wasKeys = new Set(was.map((t) => t.toLowerCase()));
|
|
63
|
+
const nowKeys = new Set(now.map((t) => t.toLowerCase()));
|
|
64
|
+
return {
|
|
65
|
+
addTags: now.filter((tag) => !wasKeys.has(tag.toLowerCase())),
|
|
66
|
+
removeTags: was.filter((tag) => !nowKeys.has(tag.toLowerCase())),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Nothing to save. The editor's Save is disabled on this, so a no-op never reaches an app. */
|
|
71
|
+
export function isEmptyMetaChange(change: MediaMetaChange): boolean {
|
|
72
|
+
return (
|
|
73
|
+
change.addTags.length === 0 &&
|
|
74
|
+
change.removeTags.length === 0 &&
|
|
75
|
+
change.description === undefined
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The merge a consumer would otherwise write itself — one item's row after the change lands.
|
|
81
|
+
*
|
|
82
|
+
* Exported for the same reason `fileTreeSelection.ts`'s rules are: the gallery persists
|
|
83
|
+
* nothing, so every app has to do this arithmetic, and four apps re-deriving "add these,
|
|
84
|
+
* remove those, keep the spelling" is four chances to mint a duplicate tag. `tags` comes back
|
|
85
|
+
* cleaned; `description` is omitted from the result when the change did not carry one, so a
|
|
86
|
+
* caller can spread the result over its row without clearing a description it never touched.
|
|
87
|
+
*/
|
|
88
|
+
export function applyMetaChange(
|
|
89
|
+
item: Pick<MediaGalleryItem, 'tags' | 'description'>,
|
|
90
|
+
change: MediaMetaChange,
|
|
91
|
+
): { tags: string[]; description?: string } {
|
|
92
|
+
const removed = new Set(change.removeTags.map((t) => t.toLowerCase()));
|
|
93
|
+
const kept = cleanTags(item.tags ?? []).filter((tag) => !removed.has(tag.toLowerCase()));
|
|
94
|
+
const tags = cleanTags([...kept, ...change.addTags]);
|
|
95
|
+
return change.description === undefined ? { tags } : { tags, description: change.description };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Every tag on any of these items, cleaned and in first-seen order — the "Remove" vocabulary. */
|
|
99
|
+
export function tagsOn(items: readonly Pick<MediaGalleryItem, 'tags'>[]): string[] {
|
|
100
|
+
return cleanTags(items.flatMap((item) => item.tags ?? []));
|
|
101
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, expect, spyOn, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_SESSION_VOLUME,
|
|
4
|
+
PLAYER_VOLUME_KEY,
|
|
5
|
+
readSessionVolume,
|
|
6
|
+
STAGE_VOLUME_KEY,
|
|
7
|
+
writePlaybackEnd,
|
|
8
|
+
writeSessionVolume,
|
|
9
|
+
} from './playbackPreferences';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 🔴 The volume he set stays set for the PAGE — and is gone on the next one.
|
|
13
|
+
*
|
|
14
|
+
* Owner, `ideas/collections-09-14.md`: *"Keep the media player volume where I set it after
|
|
15
|
+
* changing it during a session except if I refresh or first log in then continue starting it
|
|
16
|
+
* at mute. Then if I unmute it, keep that value as I change to other videos."*
|
|
17
|
+
*
|
|
18
|
+
* Three rules, and every one of them is asserted below, because each fails silently:
|
|
19
|
+
*
|
|
20
|
+
* · a fresh page starts MUTED — a stored level would have to be noticed to be undone
|
|
21
|
+
* · a level set on one clip reaches the next — the whole ask
|
|
22
|
+
* · a refresh forgets it — which is why the value is held in memory, and is proven here as
|
|
23
|
+
* "neither web store is touched". 🔴 That is the load-bearing assertion in this file:
|
|
24
|
+
* `sessionStorage` sounds like the right home and is not, because it is cleared when the
|
|
25
|
+
* TAB closes rather than when the page loads (measured in this repo's own Chromium:
|
|
26
|
+
* write, reload, the value is still there). Moving this onto either store brings his
|
|
27
|
+
* volume back loud across exactly the gesture he asked to return to silence, and nothing
|
|
28
|
+
* else in the suite would see it.
|
|
29
|
+
*
|
|
30
|
+
* The starting values the player derives from these live in `media/VideoPlayer.tsx`
|
|
31
|
+
* (`resolveAutoplay`) and are asserted in its own spec — a Vidstack player under happy-dom is
|
|
32
|
+
* an inert custom element, so the decision has to be readable without one.
|
|
33
|
+
*
|
|
34
|
+
* Every test below writes under a key of its own rather than under the two real ones, for the
|
|
35
|
+
* reason the map exists at all: a page-lifetime value has no teardown, so tests that shared a
|
|
36
|
+
* key would be an ordering dependency in a file about remembering things.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
describe('the session volume', () => {
|
|
40
|
+
test('🔴 a surface nobody has touched has nothing set — so it starts at mute', () => {
|
|
41
|
+
// `null`, NOT the default value: the player has to be able to tell "he has not touched
|
|
42
|
+
// it" from "he set it to 1.0 unmuted", or every click-to-play consumer in the fleet gets
|
|
43
|
+
// silently muted on upgrade. See `readSessionVolume`.
|
|
44
|
+
expect(readSessionVolume('test:untouched')).toBeNull();
|
|
45
|
+
// And the fallback the player uses in that case is silent.
|
|
46
|
+
expect(DEFAULT_SESSION_VOLUME.muted).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('a level set on one item is what the next item reads', () => {
|
|
50
|
+
writeSessionVolume('test:carries-over', { volume: 0.4, muted: false });
|
|
51
|
+
// The next clip is a fresh mount of the player, which reads exactly this.
|
|
52
|
+
expect(readSessionVolume('test:carries-over')).toEqual({ volume: 0.4, muted: false });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('🔴 muting keeps the level underneath, so unmuting is not a jump to full', () => {
|
|
56
|
+
// *"if I unmute it, keep that value"* — a mute stored as `volume: 0` would satisfy every
|
|
57
|
+
// other test here and return him to silence on the press that asks for sound.
|
|
58
|
+
writeSessionVolume('test:muted-level', { volume: 0.4, muted: false });
|
|
59
|
+
writeSessionVolume('test:muted-level', { volume: 0.4, muted: true });
|
|
60
|
+
expect(readSessionVolume('test:muted-level')).toEqual({ volume: 0.4, muted: true });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('a reader gets a copy — a live player cannot mutate the next one’s starting value', () => {
|
|
64
|
+
writeSessionVolume('test:copy', { volume: 0.4, muted: false });
|
|
65
|
+
const held = readSessionVolume('test:copy');
|
|
66
|
+
if (held) held.volume = 0.99;
|
|
67
|
+
expect(readSessionVolume('test:copy')?.volume).toBe(0.4);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('the two real surfaces do not fight over one key', () => {
|
|
71
|
+
// The gallery stage and a `VideoPlayer` an app mounts on a page of its own are two
|
|
72
|
+
// players — the same reason `media/stageResize.ts` keys the remembered height. One
|
|
73
|
+
// shared key would make setting the level on either of them reach into the other.
|
|
74
|
+
expect(STAGE_VOLUME_KEY).not.toBe(PLAYER_VOLUME_KEY);
|
|
75
|
+
writeSessionVolume('test:surface-a', { volume: 0.2, muted: false });
|
|
76
|
+
expect(readSessionVolume('test:surface-b')).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('a level outside 0–1 is clamped rather than handed to an element that would throw', () => {
|
|
80
|
+
// `HTMLMediaElement.volume` throws `IndexSizeError` outside 0–1, and the value arrives
|
|
81
|
+
// from a player's own event — clamping at the store is the one place every path passes.
|
|
82
|
+
writeSessionVolume('test:clamp', { volume: 4, muted: false });
|
|
83
|
+
expect(readSessionVolume('test:clamp')?.volume).toBe(1);
|
|
84
|
+
writeSessionVolume('test:clamp', { volume: -2, muted: false });
|
|
85
|
+
expect(readSessionVolume('test:clamp')?.volume).toBe(0);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 🔴 Nothing reaches a web store — that is what makes a refresh silent.
|
|
91
|
+
*
|
|
92
|
+
* `localStorage` is the obvious wrong home and `sessionStorage` is the subtle one: it is
|
|
93
|
+
* cleared when the tab closes, NOT when the page reloads, so a "session" volume put there
|
|
94
|
+
* survives the F5 he asked to return him to mute. Both are asserted, because the point of a
|
|
95
|
+
* check is to red on the change a later reader would consider an improvement.
|
|
96
|
+
*/
|
|
97
|
+
describe('a refresh forgets it, by construction', () => {
|
|
98
|
+
test('a write touches neither `localStorage` nor `sessionStorage`', () => {
|
|
99
|
+
const persisted = spyOn(globalThis.localStorage, 'setItem');
|
|
100
|
+
const sessioned = spyOn(globalThis.sessionStorage, 'setItem');
|
|
101
|
+
try {
|
|
102
|
+
writeSessionVolume(STAGE_VOLUME_KEY, { volume: 0.55, muted: false });
|
|
103
|
+
expect(persisted).not.toHaveBeenCalled();
|
|
104
|
+
expect(sessioned).not.toHaveBeenCalled();
|
|
105
|
+
} finally {
|
|
106
|
+
persisted.mockRestore();
|
|
107
|
+
sessioned.mockRestore();
|
|
108
|
+
}
|
|
109
|
+
// Belt and braces: neither store has an entry under that key for a reload to find.
|
|
110
|
+
expect(globalThis.localStorage.getItem(STAGE_VOLUME_KEY)).toBeNull();
|
|
111
|
+
expect(globalThis.sessionStorage.getItem(STAGE_VOLUME_KEY)).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('…while the sibling preferences beside it still DO persist', () => {
|
|
115
|
+
// The guard above must not be read as "this module stopped remembering things", which is
|
|
116
|
+
// how a later reader could "fix" the inconsistency in the wrong direction. The end
|
|
117
|
+
// behavior is a way of WORKING and survives a reload on purpose; only loudness was asked
|
|
118
|
+
// to be forgotten, and the two live in one file.
|
|
119
|
+
try {
|
|
120
|
+
writePlaybackEnd('all');
|
|
121
|
+
expect(globalThis.localStorage.getItem('cbmg:playback-end')).toBe('all');
|
|
122
|
+
} finally {
|
|
123
|
+
globalThis.localStorage.removeItem('cbmg:playback-end');
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 🔴 A read cannot throw, and that is not an accident of the implementation.
|
|
130
|
+
*
|
|
131
|
+
* Every other reader in this module is wrapped because `localStorage` THROWS rather than
|
|
132
|
+
* returning null in a sandboxed iframe and in Safari's private mode, and an unguarded read
|
|
133
|
+
* takes the whole gallery render down over a remembered preference. This one reaches for no
|
|
134
|
+
* storage at all, so there is nothing to be denied — asserted against a hostile environment
|
|
135
|
+
* rather than assumed, so that moving it onto a web store without a guard fails here.
|
|
136
|
+
*/
|
|
137
|
+
describe('a hostile storage environment', () => {
|
|
138
|
+
function withHostileStorage<T>(run: () => T): T {
|
|
139
|
+
const originals = (['sessionStorage', 'localStorage'] as const).map(
|
|
140
|
+
(name) => [name, Object.getOwnPropertyDescriptor(globalThis, name)] as const,
|
|
141
|
+
);
|
|
142
|
+
for (const [name] of originals) {
|
|
143
|
+
Object.defineProperty(globalThis, name, {
|
|
144
|
+
configurable: true,
|
|
145
|
+
get() {
|
|
146
|
+
throw new Error(`SecurityError: access to ${name} is denied`);
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
return run();
|
|
152
|
+
} finally {
|
|
153
|
+
for (const [name, descriptor] of originals) {
|
|
154
|
+
if (descriptor) Object.defineProperty(globalThis, name, descriptor);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
test('a read still answers instead of taking the render down with it', () => {
|
|
160
|
+
writeSessionVolume('test:hostile', { volume: 0.25, muted: false });
|
|
161
|
+
expect(withHostileStorage(() => readSessionVolume('test:hostile'))).toEqual({
|
|
162
|
+
volume: 0.25,
|
|
163
|
+
muted: false,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('a write is not an error either — an unremembered volume still works this clip', () => {
|
|
168
|
+
expect(() =>
|
|
169
|
+
withHostileStorage(() => writeSessionVolume('test:hostile', { volume: 0.3, muted: false })),
|
|
170
|
+
).not.toThrow();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -257,3 +257,103 @@ export function writeViewMode(mode: GalleryViewMode): void {
|
|
|
257
257
|
// Unremembered is still usable this session.
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
|
+
|
|
261
|
+
// ── How loud, for THIS SESSION and no longer ──────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The level a player opens at, and whether it opens silent.
|
|
265
|
+
*
|
|
266
|
+
* ── The owner's spec, verbatim (`ideas/collections-09-14.md`, 2026-09-14) ───────────────
|
|
267
|
+
* *"Keep the media player volume where I set it after changing it during a session except if
|
|
268
|
+
* I refresh or first log in then continue starting it at mute. Then if I unmute it, keep that
|
|
269
|
+
* value as I change to other videos."*
|
|
270
|
+
*
|
|
271
|
+
* Three rules, and the middle one is the whole design:
|
|
272
|
+
*
|
|
273
|
+
* | fresh page load / first sign-in | muted, exactly as it is today |
|
|
274
|
+
* | he changes it, then opens another clip | that level, for the rest of the session |
|
|
275
|
+
* | he refreshes | muted again |
|
|
276
|
+
*
|
|
277
|
+
* ── 🔴 It is kept IN MEMORY. Not `localStorage`, and not `sessionStorage` either ────────
|
|
278
|
+
* The neighbours above are `localStorage` because they are ways of WORKING — how a folder
|
|
279
|
+
* plays through, how a stage is pinned — and a way of working survives a reload. Loudness is
|
|
280
|
+
* not: *"except if I refresh … continue starting it at mute"* is a request for this to be
|
|
281
|
+
* FORGOTTEN, so the only correct store is one that cannot outlive the page.
|
|
282
|
+
*
|
|
283
|
+
* 🔴 `sessionStorage` is NOT that store, however much its name suggests it. It is cleared
|
|
284
|
+
* when the TAB closes, not when the page loads — MDN, in as many words: *"the data survives
|
|
285
|
+
* page reloads and restores"*. Measured here 2026-09-16 in the repo's own Chromium (write,
|
|
286
|
+
* `page.reload()`, read: the value is still there), because the difference is invisible in a
|
|
287
|
+
* unit test and is precisely the row of the table above that he called out. A volume put
|
|
288
|
+
* there comes back LOUD on the one gesture he named as the one that must return to silence.
|
|
289
|
+
*
|
|
290
|
+
* A module-level `Map` dies with the document, which is what a person means by "this
|
|
291
|
+
* session". It is also why nothing here needs the `try` wrapping every reader above carries:
|
|
292
|
+
* there is no web storage to be denied by a sandboxed iframe or by Safari's private mode, and
|
|
293
|
+
* therefore no read that can throw the gallery render away over a remembered volume. The
|
|
294
|
+
* specs assert both stores stay untouched, so a later move onto one of them — which would
|
|
295
|
+
* silently restore loudness across a refresh — fails rather than passes.
|
|
296
|
+
*
|
|
297
|
+
* SSR-safe for the same reason: writes only ever happen in a browser event handler, so the
|
|
298
|
+
* map a server renders against is always empty and every server-rendered player starts muted.
|
|
299
|
+
*
|
|
300
|
+
* ── 🔴 Muted and the level are TWO facts ────────────────────────────────────────────────
|
|
301
|
+
* *"if I unmute it, keep that value"* — so a mute may not be stored as `volume: 0`. Pressing
|
|
302
|
+
* mute and pressing unmute has to leave the level where he put it, which it only does if the
|
|
303
|
+
* level survives the mute independently. Every media element already models these two
|
|
304
|
+
* separately; collapsing them here is what would break it.
|
|
305
|
+
*/
|
|
306
|
+
export interface SessionVolume {
|
|
307
|
+
/** 0–1: what it plays at, and what UNMUTING returns to. */
|
|
308
|
+
volume: number;
|
|
309
|
+
/** Silent — whatever level is underneath. */
|
|
310
|
+
muted: boolean;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* What a player opens at when the person has not touched the volume this session: full level,
|
|
315
|
+
* no sound. It is the level that matters — a restored `volume: 1` under a mute is what makes
|
|
316
|
+
* the control bar's unmute button behave the way it did before any of this existed.
|
|
317
|
+
*/
|
|
318
|
+
export const DEFAULT_SESSION_VOLUME: SessionVolume = { volume: 1, muted: true };
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Where a session volume is kept — the CALLER names it, for the same reason
|
|
322
|
+
* `media/stageResize.ts` makes the remembered HEIGHT a keyed value: there is more than one
|
|
323
|
+
* player in this package, and one shared key would make setting the level on one of them
|
|
324
|
+
* silently reach into the other. The gallery stage and the shared `VideoPlayer` primitive an
|
|
325
|
+
* app mounts on a page of its own are two players.
|
|
326
|
+
*/
|
|
327
|
+
export const PLAYER_VOLUME_KEY = 'cbvp:volume';
|
|
328
|
+
|
|
329
|
+
/** The gallery stage's own — both transports of it (Vidstack and the native `<video>`). */
|
|
330
|
+
export const STAGE_VOLUME_KEY = 'cbmg:stage-volume';
|
|
331
|
+
|
|
332
|
+
/** Per key, for this document only. See {@link SessionVolume} for why it is not a web store. */
|
|
333
|
+
const sessionVolumes = new Map<string, SessionVolume>();
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* What he set this session, or `null` for "he has not touched it".
|
|
337
|
+
*
|
|
338
|
+
* 🔴 `null` rather than {@link DEFAULT_SESSION_VOLUME}, and the distinction is load-bearing:
|
|
339
|
+
* a player that was never going to make a sound anyway (`autoPlay={false}`, which mutes
|
|
340
|
+
* nothing today) must keep behaving exactly as it did, and it only can if the caller can tell
|
|
341
|
+
* "nothing set" apart from "set to the default". Handing back the default here would silence
|
|
342
|
+
* every click-to-play consumer in the fleet on upgrade.
|
|
343
|
+
*/
|
|
344
|
+
export function readSessionVolume(key: string): SessionVolume | null {
|
|
345
|
+
const held = sessionVolumes.get(key);
|
|
346
|
+
// A COPY: the caller hands this to a player that will go on changing it, and a shared
|
|
347
|
+
// object would let one surface's live state leak into the next one's starting value.
|
|
348
|
+
return held ? { ...held } : null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Remember it for the rest of this PAGE. Never longer — see {@link SessionVolume}. */
|
|
352
|
+
export function writeSessionVolume(key: string, value: SessionVolume): void {
|
|
353
|
+
sessionVolumes.set(key, {
|
|
354
|
+
// Clamped here rather than at every call site: this lands on `HTMLMediaElement.volume`,
|
|
355
|
+
// which throws `IndexSizeError` outside 0–1.
|
|
356
|
+
volume: Math.min(1, Math.max(0, value.volume)),
|
|
357
|
+
muted: value.muted,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FileCategory } from 'cwip/file-kind';
|
|
2
2
|
import type { ProcessingStatus } from 'cursedbelt-core/media';
|
|
3
|
+
import type { VideoChapter } from '../media/videoChapters';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Sort orders the gallery header offers.
|
|
@@ -91,6 +92,25 @@ export interface MediaGalleryItem extends MediaGalleryItemMedia {
|
|
|
91
92
|
durationSeconds?: number;
|
|
92
93
|
/** Free-form tags — shown in the meta line and as filter chips when any item has them. */
|
|
93
94
|
tags?: string[];
|
|
95
|
+
/**
|
|
96
|
+
* What this file IS, in the owner's own words — shown under the title on the stage.
|
|
97
|
+
*
|
|
98
|
+
* 🔴 The owner's ask, `ideas/collections-09-14.md`, 2026-09-14: *"In media galleries have an
|
|
99
|
+
* option for apps to tag media and aadd descriptions to media."* Tags were already readable
|
|
100
|
+
* here and there was no description field at all, so half of that sentence had nowhere to
|
|
101
|
+
* land even for an app that stored one.
|
|
102
|
+
*
|
|
103
|
+
* Like {@link tags} it is READ freely: an item that carries one renders it whether or not
|
|
104
|
+
* the app can write one. Authoring both is a capability — pass
|
|
105
|
+
* {@link MediaGalleryProps.onSaveMeta} and the control appears.
|
|
106
|
+
*
|
|
107
|
+
* 🔴 Deliberately NOT on the tile caption. That caption is two lines by design (see
|
|
108
|
+
* `galleryTileLine`) and is already contested between the tags and the measurement; a
|
|
109
|
+
* sentence there would either be an ellipsis or a third line on every tile in the grid.
|
|
110
|
+
* The stage has the room, and the stage is where somebody is actually reading about one
|
|
111
|
+
* file rather than scanning forty.
|
|
112
|
+
*/
|
|
113
|
+
description?: string;
|
|
94
114
|
/** Download href when it differs from `url` (e.g. `?download=1` variants). */
|
|
95
115
|
downloadUrl?: string;
|
|
96
116
|
/**
|
|
@@ -109,6 +129,20 @@ export interface MediaGalleryItem extends MediaGalleryItemMedia {
|
|
|
109
129
|
* cannot render differently. An app that bakes its edits simply never sets this.
|
|
110
130
|
*/
|
|
111
131
|
orientation?: { turns: 0 | 1 | 2 | 3; flipX?: boolean; flipY?: boolean };
|
|
132
|
+
/**
|
|
133
|
+
* The named sections of a video — YouTube chapters, stored by the app on its own row.
|
|
134
|
+
*
|
|
135
|
+
* 🔴 The owner's ask, 2026-09-15: *"allow putting markers on videos at points in time and
|
|
136
|
+
* giving them a title for the section for knowing what that part of the video is about, like
|
|
137
|
+
* youtube chapters."* Like {@link orientation}, this is metadata the app keeps beside the
|
|
138
|
+
* file rather than anything baked into it — a chapter costs no encode, applies the instant it
|
|
139
|
+
* is saved, and is free to be wrong.
|
|
140
|
+
*
|
|
141
|
+
* The stage passes them to the player, which draws the dividers on the scrub bar and fills
|
|
142
|
+
* its chapters menu (see `stagePlayableVideo`). Authoring them is a separate capability:
|
|
143
|
+
* pass {@link MediaGalleryProps.onSaveChapters} and the Sections control appears.
|
|
144
|
+
*/
|
|
145
|
+
chapters?: VideoChapter[];
|
|
112
146
|
/**
|
|
113
147
|
* Lazily resolve this item's media URLs the first time a tile or stage needs them.
|
|
114
148
|
* Called once per item id per gallery mount (cached); return `null` when the bytes
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔴 `bun publish` packs the WORKING DIRECTORY, not the git index — so a package can
|
|
3
|
+
* ship a file the repo does not have.
|
|
4
|
+
*
|
|
5
|
+
* ── What this cost, 2026-09-15 ─────────────────────────────────────────────────────
|
|
6
|
+
* The cursedbelt split (task 148) moved two integration specs into this package from
|
|
7
|
+
* `cursedbelt`. `git add -u .` stages only files git already knows about, so both were
|
|
8
|
+
* committed nowhere — and `bun run verify` was green *because they were present on disk*,
|
|
9
|
+
* which is the same reason nobody noticed. `1.0.2` went to the registry carrying two
|
|
10
|
+
* `src/` files that existed in no commit, and `git status` said `tracked-dirty=0`.
|
|
11
|
+
*
|
|
12
|
+
* "The tree is clean" and "everything that ships is committed" are different claims, and
|
|
13
|
+
* only the first one is what people actually check. npm versions are immutable, so the
|
|
14
|
+
* window to notice closes at publish time.
|
|
15
|
+
*
|
|
16
|
+
* ── Why this shape ─────────────────────────────────────────────────────────────────
|
|
17
|
+
* It asks git, for exactly the directories `package.json#files` promises to ship, which
|
|
18
|
+
* of those paths git does not track. That is narrower than "any untracked file" on
|
|
19
|
+
* purpose: scratch files, `.agent.noindex/`, a half-written script and an uncommitted
|
|
20
|
+
* lockfile are all legitimate states for a working tree and reddening on them would teach
|
|
21
|
+
* people to skip this. A file INSIDE the published surface is the only case where
|
|
22
|
+
* untracked means "about to ship something no one can review or revert".
|
|
23
|
+
*
|
|
24
|
+
* 🔴 It must run from a real checkout to mean anything, so it refuses rather than passes
|
|
25
|
+
* when `git` cannot answer — a check that silently degrades to green is worse than none.
|
|
26
|
+
*/
|
|
27
|
+
import { describe, expect, test } from 'bun:test';
|
|
28
|
+
import { spawnSync } from 'node:child_process';
|
|
29
|
+
import { existsSync } from 'node:fs';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import pkg from '../package.json';
|
|
32
|
+
|
|
33
|
+
const repoRoot = fileURLToPath(new URL('..', import.meta.url));
|
|
34
|
+
|
|
35
|
+
/** The directories `files` promises to ship, minus build output nobody commits. */
|
|
36
|
+
const shippedDirs = (): string[] =>
|
|
37
|
+
((pkg as { files?: string[] }).files ?? []).filter((f) => f !== 'dist' && existsSync(`${repoRoot}/${f}`));
|
|
38
|
+
|
|
39
|
+
const git = (...args: string[]): { ok: boolean; out: string } => {
|
|
40
|
+
const r = spawnSync('git', ['-C', repoRoot, ...args], { encoding: 'utf8' });
|
|
41
|
+
return { ok: r.status === 0, out: (r.stdout ?? '').trim() };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
describe('every file inside the published surface is tracked', () => {
|
|
45
|
+
test('git can answer at all — a degraded check must refuse, not pass', () => {
|
|
46
|
+
expect(git('rev-parse', '--is-inside-work-tree').ok, 'not a git checkout, so this check proves nothing').toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('package.json names a shippable directory to scan', () => {
|
|
50
|
+
// Vacuity guard: an empty `files`, or one naming only `dist`, makes the assertion
|
|
51
|
+
// below pass while scanning nothing at all.
|
|
52
|
+
expect(shippedDirs().length, '`files` names no committed directory — nothing would be scanned').toBeGreaterThan(0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('no untracked file sits inside a directory `files` ships', () => {
|
|
56
|
+
// --others = untracked; --exclude-standard honours .gitignore, so deliberately
|
|
57
|
+
// ignored paths (.agent.noindex, node_modules) are not offences.
|
|
58
|
+
const { ok, out } = git('ls-files', '--others', '--exclude-standard', '--', ...shippedDirs());
|
|
59
|
+
expect(ok, 'git ls-files failed').toBe(true);
|
|
60
|
+
const untracked = out === '' ? [] : out.split('\n');
|
|
61
|
+
expect(
|
|
62
|
+
untracked,
|
|
63
|
+
'these would be PUBLISHED but exist in no commit — `bun publish` packs the working directory, not the index,\n' +
|
|
64
|
+
'so the registry would carry code nobody can review, diff or revert, and npm versions are immutable.\n' +
|
|
65
|
+
'Commit them (or add them to .gitignore if they genuinely must not ship):\n ' +
|
|
66
|
+
untracked.join('\n '),
|
|
67
|
+
).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* cursedbelt — the `activity-bar` area. GENERATED BUILD ARTIFACT. Never hand-edit.
|
|
3
|
+
*
|
|
4
|
+
* the `cursedbelt/react/activity-bar` rail — app chrome most apps do not mount.
|
|
5
|
+
*
|
|
6
|
+
* Every Tailwind utility the modules in this area's import closure use, MINUS the
|
|
7
|
+
* ones `styles-areas/core.css` already ships. 🔴 Import ALL of these, in
|
|
8
|
+
* this order — this file alone is incomplete:
|
|
9
|
+
*
|
|
10
|
+
* @import "tailwindcss";
|
|
11
|
+
* @import "cursedbelt/styles-static.css";
|
|
12
|
+
* @import "cursedbelt/styles-areas/core.css";
|
|
13
|
+
* @import "cursedbelt/styles-areas/activity-bar.css";
|
|
14
|
+
* @import "cursedbelt/theme.css";
|
|
15
|
+
*
|
|
16
|
+
* An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
|
|
17
|
+
* emits — proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
|
|
18
|
+
* when this file is stale. 72 candidate(s) scanned.
|
|
19
|
+
*
|
|
20
|
+
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
21
|
+
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
22
|
+
* import closures rather than a hand-written list, is scripts/styleAreas.ts.
|
|
23
|
+
*/
|
|
24
|
+
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
|
25
|
+
@layer theme, base, components, utilities;
|
|
26
|
+
@layer utilities {
|
|
27
|
+
.collapse {
|
|
28
|
+
visibility: collapse;
|
|
29
|
+
}
|
|
30
|
+
}
|