Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions configurator/src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount, untrack } from 'svelte';
import { SlidersHorizontal, Eye, RotateCcw } from 'lucide-svelte';
import type { PreviewTemplate, SlashedToken } from './types';
import type { PreviewTemplate, SlashedToken, ApiIndex } from './types';
import StudioHeader from './components/shell/StudioHeader.svelte';
import SidebarNav from './components/shell/SidebarNav.svelte';
import StatusBar from './components/shell/StatusBar.svelte';
Expand All @@ -13,7 +13,7 @@
import tokensRaw from './data/api-index.generated.json';
import CommandPalette from './components/CommandPalette.svelte';

const ALL_TOKENS = ((tokensRaw as any).tokens ?? tokensRaw) as SlashedToken[];
const ALL_TOKENS = ((tokensRaw as ApiIndex).tokens ?? tokensRaw) as SlashedToken[];

const DOMAIN_LABELS: Record<string, string> = {
home: "Home", colors: "Colors", typography: "Typography", spacing: "Spacing",
Expand Down
7 changes: 4 additions & 3 deletions configurator/src/components/panels/CheatsheetPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
import classesData from '../../data/classes.generated.json';
import tokensData from '../../data/api-index.generated.json';
import { Copy, Check } from 'lucide-svelte';
import type { SlashedClass } from '../../types';

const classes = classesData.classes;
const tokens = tokensData.tokens.filter((t: any) => t.tier === 'PUBLIC' || t.tier === 'PUBLIC-ADVANCED');
const tokens = tokensData.tokens.filter((t) => t.tier === 'PUBLIC' || t.tier === 'PUBLIC-ADVANCED');

let query = $state('');
let tab = $state<'classes' | 'tokens'>('classes');
Expand All @@ -17,7 +18,7 @@
let filteredClasses = $derived(() => {
const q = query.trim().toLowerCase();
if (!q) return classes;
return classes.filter((c: any) =>
return classes.filter((c: SlashedClass) =>
c.name.toLowerCase().includes(q) ||
c.selector?.toLowerCase().includes(q) ||
c.description?.toLowerCase().includes(q) ||
Expand All @@ -28,7 +29,7 @@
let filteredTokens = $derived(() => {
const q = query.trim().toLowerCase();
if (!q) return tokens;
return tokens.filter((t: any) =>
return tokens.filter((t) =>
t.name.toLowerCase().includes(q) ||
t.description?.toLowerCase().includes(q) ||
t.group?.toLowerCase().includes(q) ||
Expand Down
20 changes: 13 additions & 7 deletions configurator/src/lib/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { deflateSync, inflateSync } from "fflate";
import tokensData from "../data/token-registry.generated.json";
import type { TokenRegistry, DecodeOptions, ShareOptions } from "../types";

declare const __SLASHED_VERSION__: string;

Expand All @@ -13,6 +14,11 @@ const frameworkVersion: string = typeof __SLASHED_VERSION__ !== "undefined" ? __
export const CODEC_VERSION = 2;
export const SHARE_PARAM = "c";

// SL-026: MAX_VALUE_BYTES and MAX_ID happen to share the same value (both
// bounded by the 2-byte length-prefix / id field in the wire format — see
// encode()'s payload layout below) but are independent limits for unrelated
// things (a value's encoded byte length vs. a token's registry id). Don't
// assume changing one should change the other.
const MAX_VALUE_BYTES = 65535;
const MAX_ID = 65535;
const textEncoder = new TextEncoder();
Expand All @@ -24,7 +30,7 @@ function isValidId(id: number): boolean {
return Number.isInteger(id) && id >= 0 && id <= MAX_ID;
}

function buildNameToIdMap(registry: any): Map<string, number> {
function buildNameToIdMap(registry: TokenRegistry): Map<string, number> {
const nameToId = new Map<string, number>();
for (const entry of registry?.tokens ?? []) {
if (entry && !entry.removed && typeof entry.name === "string" && isValidId(entry.id)) {
Expand All @@ -34,7 +40,7 @@ function buildNameToIdMap(registry: any): Map<string, number> {
return nameToId;
}

function buildIdToNameMap(registry: any): Map<number, string> {
function buildIdToNameMap(registry: TokenRegistry): Map<number, string> {
const idToName = new Map<number, string>();
for (const entry of registry?.tokens ?? []) {
if (entry && typeof entry.name === "string" && Number.isInteger(entry.id)) {
Expand Down Expand Up @@ -72,7 +78,7 @@ function base64UrlToBytes(value: string): Uint8Array | null {
}
}

export function encode(overrides: Record<string, string>, registry: any = tokensData): string {
export function encode(overrides: Record<string, string>, registry: TokenRegistry = tokensData): string {
if (!overrides || typeof overrides !== "object") return "";
const nameToId = buildNameToIdMap(registry);
const entries: { id: number; valueBytes: Uint8Array }[] = [];
Expand Down Expand Up @@ -114,7 +120,7 @@ export function encode(overrides: Record<string, string>, registry: any = tokens
return bytesToBase64Url(out);
}

export function decode(code: string, registry: any = tokensData, options: any = {}): Record<string, string> {
export function decode(code: string, registry: TokenRegistry = tokensData, options: DecodeOptions = {}): Record<string, string> {
const trimmed = String(code ?? "").trim();
if (trimmed === "") return {};
const rawBytes = base64UrlToBytes(trimmed);
Expand Down Expand Up @@ -258,8 +264,8 @@ export function encodeOverrides(overrides: Record<string, string>): string {

const SHARE_PARAM_RE = new RegExp(`[#&]?${SHARE_PARAM}=([^&]+)`);

export function readShareFromHash(hashOrParam: string, options: any = {}): Record<string, string> {
const knownTokensSet = new Set(tokensData.tokens.map((tok: any) => tok.name));
export function readShareFromHash(hashOrParam: string, options: ShareOptions = {}): Record<string, string> {
const knownTokensSet = new Set(tokensData.tokens.map((tok) => tok.name));
const isKnown = options.isKnown ?? ((name: string) => knownTokensSet.has(name));
let trimmed = String(hashOrParam ?? "").trim();
const match = trimmed.match(SHARE_PARAM_RE);
Expand All @@ -278,7 +284,7 @@ export function buildShareUrl(overrides: Record<string, string>, baseUrlOverride
return url.toString();
}

export function readShareFromHashIfPresent(hash: string, options: any = {}): Record<string, string> {
export function readShareFromHashIfPresent(hash: string, options: ShareOptions = {}): Record<string, string> {
const value = String(hash ?? "");
return value.includes(`${SHARE_PARAM}=`) ? readShareFromHash(value, options) : {};
}
62 changes: 62 additions & 0 deletions configurator/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,68 @@ export interface SlashedToken {
syntax?: string | null;
}

// SL-017/024: types for the two consumed-generated-JSON shapes and codec.ts's
// options objects, replacing `any` at their 7 call sites.

/** A token entry as it appears in data/api-index.generated.json's `tokens` array. */
export interface ApiIndexToken extends SlashedToken {
fallbackOnly?: boolean;
optional?: boolean;
layer?: string | null;
bundles?: string[];
}

/** Shape of data/api-index.generated.json (configurator/scripts/sync-api.mjs's output). */
export interface ApiIndex {
_sync?: Record<string, unknown>;
tokens: ApiIndexToken[];
}

/** A class entry as it appears in data/classes.generated.json's `classes` array. */
export interface SlashedClass {
name: string;
selector: string;
kind: string;
category: string;
group?: string;
description?: string;
optional?: boolean;
layer?: string | null;
}

/** Shape of data/classes.generated.json (configurator/scripts/sync-api.mjs's output). */
export interface ClassIndex {
_sync?: Record<string, unknown>;
classes: SlashedClass[];
}

/** A registry entry as it appears in data/token-registry.generated.json's `tokens` array. */
export interface TokenRegistryEntry {
id: number;
name: string;
removed?: boolean;
}

/** Shape of data/token-registry.generated.json, consumed by codec.ts's encode/decode. */
export interface TokenRegistry {
_meta?: Record<string, unknown>;
tokens: TokenRegistryEntry[];
}

/** Options accepted by codec.ts's decode(). */
export interface DecodeOptions {
sanitize?: (value: string) => string;
isKnown?: (tokenName: string) => boolean;
}

/**
* Options accepted by codec.ts's readShareFromHash() / readShareFromHashIfPresent().
* Deliberately omits `sanitize` (unlike DecodeOptions): these are the public
* share-link entry points and always force sanitizeValue as a CSS-injection
* safeguard, so a caller-supplied sanitize is never honoured.
*/
export type ShareOptions = Pick<DecodeOptions, "isKnown">;

export interface SlashedCategory {
id: string;
label: string;
Expand Down