Show size and a draft indicator in snippet-library rows

This commit is contained in:
2026-06-05 14:11:00 +03:00
parent da20475d2f
commit ad239d7d49
4 changed files with 138 additions and 50 deletions
+35
View File
@@ -2,10 +2,12 @@ import { describe, expect, test } from 'vitest';
import {
CURRENT_SNIPPET_VERSION,
createSnippet,
formatSnippetSize,
generateSnippetName,
hasUnpublishedChanges,
SAMPLE_SPEC,
sampleSpecText,
snippetSizeBytes,
} from './snippet';
describe('createSnippet', () => {
@@ -72,3 +74,36 @@ describe('hasUnpublishedChanges', () => {
expect(hasUnpublishedChanges({ ...s, draftSpec: s.spec + ' ' })).toBe(true);
});
});
describe('snippetSizeBytes', () => {
test('measures the draft as UTF-8 byte length, not character count', () => {
const s = createSnippet({ spec: 'abc' });
expect(snippetSizeBytes(s)).toBe(3);
// A 3-character string of multibyte glyphs is more than 3 bytes.
expect(snippetSizeBytes({ ...s, draftSpec: '€€€' })).toBe(9);
});
test('reflects the working draft, not the published spec', () => {
const s = createSnippet({ spec: 'ab' });
expect(snippetSizeBytes({ ...s, draftSpec: 'abcdef' })).toBe(6);
});
});
describe('formatSnippetSize', () => {
test('omits the size under ~1 KB (spec §02 clutter rule)', () => {
expect(formatSnippetSize(0)).toBeNull();
expect(formatSnippetSize(512)).toBeNull();
expect(formatSnippetSize(1023)).toBeNull();
});
test('shows whole KB at and above the threshold', () => {
expect(formatSnippetSize(1024)).toBe('1 KB');
expect(formatSnippetSize(2048)).toBe('2 KB');
expect(formatSnippetSize(1536)).toBe('2 KB'); // rounds to nearest KB
});
test('rolls over to MB for large payloads', () => {
expect(formatSnippetSize(1024 * 1024)).toBe('1 MB');
expect(formatSnippetSize(3 * 1024 * 1024)).toBe('3 MB');
});
});
+26
View File
@@ -121,3 +121,29 @@ export function createSnippet(options: CreateSnippetOptions = {}): Snippet {
export function hasUnpublishedChanges(snippet: Snippet): boolean {
return snippet.draftSpec !== snippet.spec;
}
/**
* Approximate payload size of a snippet, in bytes (spec §09 `size`). Measured on
* the working draft — the version the editor and preview currently show — as its
* UTF-8 byte length, so the figure tracks what the user is actually editing.
* `TextEncoder` is a platform global (like `crypto.randomUUID` above), not a
* browser/DOM API, so it stays within the portable core.
*/
export function snippetSizeBytes(snippet: Snippet): number {
return new TextEncoder().encode(snippet.draftSpec).length;
}
/** Below this we omit the size in the library to reduce clutter (spec §02). */
const SIZE_DISPLAY_THRESHOLD = 1024;
/**
* Human-readable size for the library row, or `null` when the snippet is small
* enough that the spec says to omit it (under ~1 KB). Rounded to whole KB/MB —
* a list hint, not a precise measure.
*/
export function formatSnippetSize(bytes: number): string | null {
if (bytes < SIZE_DISPLAY_THRESHOLD) return null;
const kb = bytes / 1024;
if (kb < 1024) return `${Math.round(kb)} KB`;
return `${Math.round(kb / 1024)} MB`;
}