@stacksjs/stx 0.2.176 → 0.2.178

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.
Files changed (44) hide show
  1. package/dist/builtins/attrs.d.ts +19 -0
  2. package/dist/builtins/attrs.js +18 -0
  3. package/dist/builtins/safe-image.js +2 -1
  4. package/dist/builtins/stx-image.js +2 -1
  5. package/dist/builtins/stx-link.js +2 -1
  6. package/dist/cli-flags.d.ts +34 -0
  7. package/dist/cli-flags.js +20 -0
  8. package/dist/cli.js +899 -778
  9. package/dist/client-script.js +2 -1
  10. package/dist/component-registry.d.ts +9 -0
  11. package/dist/component-renderer.js +20 -1
  12. package/dist/composables/use-fetch.d.ts +6 -1
  13. package/dist/composables/use-fetch.js +40 -18
  14. package/dist/composables/use-query.d.ts +7 -2
  15. package/dist/cookie-serialize.d.ts +40 -0
  16. package/dist/cookie-serialize.js +18 -0
  17. package/dist/craft.js +500 -381
  18. package/dist/crosswind-config.d.ts +13 -0
  19. package/dist/crosswind-config.js +6 -2
  20. package/dist/dev-server/crosswind.js +2 -1
  21. package/dist/edge-runtime.d.ts +2 -9
  22. package/dist/edge-runtime.js +2 -16
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +2 -0
  25. package/dist/page-action.d.ts +10 -20
  26. package/dist/page-action.js +33 -9
  27. package/dist/production-server.js +8 -7
  28. package/dist/pwa.js +452 -333
  29. package/dist/reactive.js +2 -3
  30. package/dist/signal-processing.js +37 -6
  31. package/dist/signals.js +150 -32
  32. package/dist/ssg.js +465 -346
  33. package/dist/ssr.js +2 -16
  34. package/dist/stx-virtual-ts.d.ts +45 -1
  35. package/dist/stx-virtual-ts.js +75 -5
  36. package/dist/stx.d.ts +66 -7
  37. package/dist/template-hydrator.d.ts +1 -1
  38. package/dist/template-hydrator.js +6 -6
  39. package/dist/theme-tokens.d.ts +88 -0
  40. package/dist/theme-tokens.js +71 -0
  41. package/dist/typecheck.d.ts +6 -1
  42. package/dist/typecheck.js +7 -6
  43. package/package.json +5 -5
  44. package/stx.d.ts +66 -7
package/dist/ssr.js CHANGED
@@ -4,6 +4,7 @@ import { errorLogger } from "./error-handling.js";
4
4
  import { processDirectives } from "./process.js";
5
5
  import { extractVariables } from "./utils.js";
6
6
  import { patternToRegex } from "stx-router";
7
+ import { serializeSetCookie } from "./cookie-serialize.js";
7
8
 
8
9
  export class MemorySessionStore {
9
10
  store = new Map;
@@ -89,22 +90,7 @@ function parseCookies(cookieHeader) {
89
90
  }, {});
90
91
  }
91
92
  function serializeCookie(name, value, options = {}) {
92
- let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
93
- if (options.maxAge !== void 0)
94
- cookie += `; Max-Age=${options.maxAge}`;
95
- if (options.expires)
96
- cookie += `; Expires=${options.expires.toUTCString()}`;
97
- if (options.path)
98
- cookie += `; Path=${options.path}`;
99
- if (options.domain)
100
- cookie += `; Domain=${options.domain}`;
101
- if (options.secure)
102
- cookie += "; Secure";
103
- if (options.httpOnly)
104
- cookie += "; HttpOnly";
105
- if (options.sameSite)
106
- cookie += `; SameSite=${options.sameSite}`;
107
- return cookie;
93
+ return serializeSetCookie(name, value, options);
108
94
  }
109
95
  function generateCsrfToken() {
110
96
  return crypto.randomBytes(32).toString("hex");
@@ -131,7 +131,23 @@ export declare function splitTopLevelImports(code: string): { imports: string, b
131
131
  * So the incentive runs the right way: declare a payload and the names are
132
132
  * checked against the server block, or do not and they are `any`.
133
133
  */
134
- export declare function clientPayloadDeclarations(serverCode: string): string;
134
+ /**
135
+ * Top-level `interface X {…}` / `type X = …` statements, with their source text.
136
+ *
137
+ * The payload projection wraps the whole server body in a scope function so the
138
+ * checker can infer each published value's type from real code. That works for
139
+ * VALUES and traps TYPES: an `interface AutofixState` declared in
140
+ * `<script server>` lives inside that function, so a `<script client>` block in
141
+ * the same file naming it got `Cannot find name 'AutofixState'` — an error about
142
+ * code that is correct and runs (stacksjs/stx#1924).
143
+ *
144
+ * Scanned at depth zero so an interface declared inside a helper is not lifted
145
+ * out of the scope it belongs to. The brace matcher walks the real source rather
146
+ * than a blanked copy, because the declaration's BODY is what has to be carried
147
+ * across, not just its name.
148
+ */
149
+ export declare function extractTypeDeclarations(code: string): Array<{ name: string, text: string }>;
150
+ export declare function clientPayloadDeclarations(serverCode: string, clientCode?: string): string;
135
151
  /**
136
152
  * Declarations at the TOP LEVEL of a block, ignoring anything nested.
137
153
  *
@@ -181,6 +197,33 @@ export declare function crossScopeCollisions(blocks: readonly ScriptBlock[]): st
181
197
  * `const`, and that form is documented stx syntax, not a mistake.
182
198
  */
183
199
  export declare function expressionStatement(expression: TemplateExpression): { text: string, prefixLength: number } | null;
200
+ /**
201
+ * Rewrite relative module specifiers to absolute paths against the real file.
202
+ *
203
+ * A block's virtual buffer is written into the state directory, and TypeScript
204
+ * resolves a relative specifier against the file that CONTAINS it — so
205
+ * `../target`, written in `pages/deep/rel.stx`, was looked for next to
206
+ * `.stx/typecheck/` and reported as a missing module (stacksjs/stx#1928).
207
+ *
208
+ * A `paths` alias to the same file resolved fine, because those go through the
209
+ * absolute `baseUrl` the checker emits. That asymmetry is the whole bug: a
210
+ * relative import is the first thing anyone writes and what every editor's
211
+ * auto-import produces, so an app hitting it either rewrites correct imports
212
+ * into aliases to appease the checker, or mutes the checker.
213
+ *
214
+ * Rewriting each specifier is preferred over pointing the program at the source
215
+ * directory — `rootDirs`, the built-in answer for a virtual layout, MERGES its
216
+ * entries into one namespace, so `./util` from one checked page could silently
217
+ * resolve to a different page's `util` when both directories are roots. Each
218
+ * buffer knows exactly one origin, so resolving per file cannot cross-talk.
219
+ *
220
+ * Textual rather than parsed, and deliberately: `Bun.Transpiler.scanImports`
221
+ * erases `import type`, which is the form a type-checker cares about most. The
222
+ * cost is that the literal text `from './x'` inside a string or comment is
223
+ * rewritten too — inert, since this buffer is never executed, and it cannot
224
+ * change a line count, so every diagnostic still lands on the author's line.
225
+ */
226
+ export declare function absolutizeRelativeSpecifiers(code: string, originDir: string): string;
184
227
  /**
185
228
  * Build one line-aligned TypeScript buffer for a whole `.stx` file.
186
229
  *
@@ -279,6 +322,7 @@ export declare interface TemplateBinding {
279
322
  }
280
323
  export declare interface BuildVirtualOptions {
281
324
  templateExpressions?: boolean
325
+ originDir?: string
282
326
  globals?: boolean
283
327
  runtimeGlobals?: boolean
284
328
  }
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  import { stripCommentsAndLiterals } from "./strip-literals.js";
2
3
  import { STX_RUNTIME_GLOBALS } from "./runtime-globals.js";
3
4
  const SCRIPT_RE = /<script(\s[^>]*)?>([\s\S]*?)<\/script>/gi;
@@ -330,27 +331,87 @@ export function splitTopLevelImports(code) {
330
331
  return { imports: imports.join(`
331
332
  `), body };
332
333
  }
333
- export function clientPayloadDeclarations(serverCode) {
334
- const published = extractClientPayloadNames(serverCode);
334
+ export function extractTypeDeclarations(code) {
335
+ const searchable = stripCommentsAndLiterals(code), depthAt = Array(searchable.length);
336
+ let depth = 0;
337
+ for (let i = 0;i < searchable.length; i++) {
338
+ const c = searchable[i];
339
+ if (c === "}" || c === ")" || c === "]")
340
+ depth = Math.max(0, depth - 1);
341
+ depthAt[i] = depth;
342
+ if (c === "{" || c === "(" || c === "[")
343
+ depth++;
344
+ }
345
+ const found = [], start = /^[ \t]*(?:export[ \t]+)?(interface|type)[ \t]+([A-Za-z_$][\w$]*)/gm;
346
+ let match;
347
+ while ((match = start.exec(searchable)) !== null) {
348
+ if (depthAt[match.index] !== 0)
349
+ continue;
350
+ const kind = match[1], name = match[2];
351
+ let end;
352
+ if (kind === "interface") {
353
+ const open = searchable.indexOf("{", match.index);
354
+ if (open === -1)
355
+ continue;
356
+ let depth = 0;
357
+ end = -1;
358
+ for (let i = open;i < searchable.length; i++)
359
+ if (searchable[i] === "{")
360
+ depth++;
361
+ else if (searchable[i] === "}" && --depth === 0) {
362
+ end = i + 1;
363
+ break;
364
+ }
365
+ if (end === -1)
366
+ continue;
367
+ } else {
368
+ let depth = 0;
369
+ end = searchable.length;
370
+ for (let i = searchable.indexOf("=", match.index) + 1;i < searchable.length; i++) {
371
+ const c = searchable[i];
372
+ if (c === "{" || c === "[" || c === "(" || c === "<")
373
+ depth++;
374
+ else if (c === "}" || c === "]" || c === ")" || c === ">")
375
+ depth--;
376
+ else if (depth <= 0 && (c === ";" || c === `
377
+ ` && !/[=|&,<([{]\s*$/.test(searchable.slice(Math.max(0, i - 40), i)))) {
378
+ end = c === ";" ? i + 1 : i;
379
+ break;
380
+ }
381
+ }
382
+ }
383
+ found.push({ name, text: code.slice(match.index, end).replace(/^[ \t]*export[ \t]+/, "") });
384
+ start.lastIndex = end;
385
+ }
386
+ return found;
387
+ }
388
+ function declaredTypeNames(code) {
389
+ return new Set(extractTypeDeclarations(code).map((entry) => entry.name));
390
+ }
391
+ export function clientPayloadDeclarations(serverCode, clientCode = "") {
392
+ const published = extractClientPayloadNames(serverCode), ownTypes = declaredTypeNames(clientCode), serverTypes = extractTypeDeclarations(serverCode).filter((entry) => !ownTypes.has(entry.name)).map((entry) => entry.text);
335
393
  if (!published) {
336
394
  const scraped = collectBlockDeclarations(serverCode);
337
- if (scraped.length === 0)
395
+ if (scraped.length === 0 && serverTypes.length === 0)
338
396
  return "";
339
397
  return [
340
398
  "",
341
399
  `// ${SCRAPED_COMMENT}`,
400
+ ...serverTypes,
342
401
  ...scraped.map((name) => `declare var ${name}: any`)
343
402
  ].join(`
344
403
  `);
345
404
  }
346
405
  if (published.length === 0)
347
- return "";
406
+ return serverTypes.length > 0 ? ["", `// ${PAYLOAD_COMMENT}`, ...serverTypes].join(`
407
+ `) : "";
348
408
  const { imports, body } = splitTopLevelImports(serverCode), scopeBody = body.replace(/^[ \t]*export[ \t]+(?=(?:const|let|var|function|async|class)\b)/gm, ""), parts = [
349
409
  "",
350
410
  `// ${PAYLOAD_COMMENT}`
351
411
  ];
352
412
  if (imports)
353
413
  parts.push(imports);
414
+ parts.push(...serverTypes);
354
415
  parts.push(`async function ${PAYLOAD_SCOPE}() {`, scopeBody, ` return { ${published.join(", ")} }`, "}", `type ${PAYLOAD_TYPE} = Awaited<ReturnType<typeof ${PAYLOAD_SCOPE}>>`, ...published.map((name) => `declare var ${name}: ${PAYLOAD_TYPE}[${JSON.stringify(name)}]`));
355
416
  return parts.join(`
356
417
  `);
@@ -426,11 +487,20 @@ export function expressionStatement(expression) {
426
487
  const prefix = ";void ((";
427
488
  return { text: `${prefix}${code}));`, prefixLength: prefix.length };
428
489
  }
490
+ const RELATIVE_SPECIFIER_RE = /(\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)(['"])(\.{1,2}\/[^'"\n]*)\2/g;
491
+ export function absolutizeRelativeSpecifiers(code, originDir) {
492
+ if (!originDir)
493
+ return code;
494
+ return code.replace(RELATIVE_SPECIFIER_RE, (_whole, prefix, quote, specifier) => {
495
+ const absolute = path.resolve(originDir, specifier).replace(/\\/g, "/");
496
+ return `${prefix}${quote}${absolute}${quote}`;
497
+ });
498
+ }
429
499
  export function buildVirtualTypeScript(source, options = {}) {
430
500
  const blocks = extractScriptBlocks(source), sourceLines = source.split(`
431
501
  `), lines = Array.from({ length: sourceLines.length }, () => ""), lineMap = new Map;
432
502
  for (const block of blocks)
433
- block.code.split(`
503
+ absolutizeRelativeSpecifiers(block.code, options.originDir ?? "").split(`
434
504
  `).forEach((text, i) => {
435
505
  const index = block.startLine - 1 + i;
436
506
  if (index < lines.length)
package/dist/stx.d.ts CHANGED
@@ -199,11 +199,42 @@ declare function useSearchParams(): {
199
199
  // Data fetching
200
200
  // ============================================================================
201
201
 
202
- interface StxQueryResult<T> {
202
+ /** How one run of a request should behave. */
203
+ interface StxRefetchOptions {
204
+ /**
205
+ * Refresh without touching `loading` or clearing the current error.
206
+ *
207
+ * A poll refreshes data that is already on screen, so driving the first-load
208
+ * state from it puts a spinner over a populated view on every tick — the
209
+ * reason a polling view could not use these primitives at all (#1929). Bind
210
+ * `isFetching` for a subtle in-flight indicator instead.
211
+ */
212
+ background?: boolean
213
+ }
214
+
215
+ interface StxFetchResult<T> {
203
216
  data: StxSignal<T | null>
217
+ /** True while a first-load request is in flight. A background one leaves it alone. */
204
218
  loading: StxSignal<boolean>
219
+ /** True while ANY request is in flight, background ones included. */
220
+ isFetching: StxSignal<boolean>
205
221
  error: StxSignal<Error | null>
206
- refetch: () => Promise<void>
222
+ refetch: (_options?: StxRefetchOptions) => Promise<void>
223
+ }
224
+
225
+ /*
226
+ * `useQuery` returns more than `useFetch` does, and declaring one shape for
227
+ * both hid it: `isStale` and `invalidate` existed at runtime and were invisible
228
+ * to the type, so reading either was an error on a value that was really there
229
+ * (#1929). Extending rather than widening the shared interface, because the
230
+ * inverse mistake is worse — declaring `invalidate` on `useFetch` would
231
+ * type-check a call to something that is undefined at runtime.
232
+ */
233
+ interface StxQueryResult<T> extends StxFetchResult<T> {
234
+ /** True when cached data is on screen while a revalidation runs. */
235
+ isStale: StxSignal<boolean>
236
+ /** Drop this key from the cache and refetch. */
237
+ invalidate: (_options?: StxRefetchOptions) => Promise<void>
207
238
  }
208
239
 
209
240
  interface StxMutationResult<T> {
@@ -213,7 +244,7 @@ interface StxMutationResult<T> {
213
244
  mutate: (body?: unknown) => Promise<T>
214
245
  }
215
246
 
216
- declare function useFetch<T = any>(_url: string, _options?: any): StxQueryResult<T>
247
+ declare function useFetch<T = any>(_url: string, _options?: any): StxFetchResult<T>
217
248
  declare function useQuery<T = any>(_url: string, _options?: any): StxQueryResult<T>
218
249
  declare function useMutation<T = any>(_url: string, _options?: any): StxMutationResult<T>
219
250
 
@@ -271,17 +302,45 @@ declare function useOptimistic<T = any, A = any>(
271
302
  // DOM utilities
272
303
  // ============================================================================
273
304
 
305
+ /**
306
+ * Options the runtime reads, including the target it binds to.
307
+ *
308
+ * The target travels HERE rather than in a leading parameter, because that is
309
+ * what the implementation does: `var target = (options && options.target) || window`.
310
+ */
311
+ interface StxEventListenerOptions {
312
+ /** Element, selector, or document/window. Defaults to `window`. */
313
+ target?: Window | Document | HTMLElement | string | null
314
+ capture?: boolean
315
+ passive?: boolean
316
+ once?: boolean
317
+ }
318
+
319
+ /*
320
+ * Event first, then handler, then options (stacksjs/stx#1923).
321
+ *
322
+ * This was declared target-first while the runtime has always been
323
+ * event-first, so the two disagreed in the worst possible direction: `_target`
324
+ * accepted `string`, so the CORRECT call got as far as reading 'keydown' as a
325
+ * selector before failing on the handler, and an author who trusted the type
326
+ * and added a target wrote `useEventListener(window, 'keydown', fn)` — which
327
+ * binds an event named "window" with the string 'keydown' as its handler, and
328
+ * silently attaches nothing.
329
+ *
330
+ * `browser-composables.ts` had it right all along; the package shipped two
331
+ * declarations of one name with different arities and the registry got the
332
+ * wrong one. Same shape as the `reactive` split: one name, two conventions, and
333
+ * no way for the call site to tell which it has.
334
+ */
274
335
  declare function useEventListener<K extends keyof WindowEventMap>(
275
- _target: Window | Document | HTMLElement | string | null,
276
336
  _event: K,
277
337
  _handler: (event: WindowEventMap[K]) => void,
278
- _options?: boolean | AddEventListenerOptions,
338
+ _options?: StxEventListenerOptions,
279
339
  ): StxCleanup
280
340
  declare function useEventListener(
281
- _target: Window | Document | HTMLElement | string | null,
282
341
  _event: string,
283
342
  _handler: (event: Event) => void,
284
- _options?: boolean | AddEventListenerOptions,
343
+ _options?: StxEventListenerOptions,
285
344
  ): StxCleanup
286
345
  declare function useScrollLock(
287
346
  _target?: HTMLElement | null | { current?: HTMLElement | null, value?: HTMLElement | null } | (() => HTMLElement | null | undefined),
@@ -20,7 +20,7 @@ export declare function hydrateTemplate(compiled: CompiledTemplate, requestConte
20
20
  * templates aren't serialized into the compiled template, so use the function
21
21
  * form in production.)
22
22
  */
23
- export declare function hydrateTemplateStream(compiled: CompiledTemplate, requestContext?: Record<string, any>): Promise<{ html: string, boundaries?: { id: string, render: () => Promise<string> }[], redirect?: string }>;
23
+ export declare function hydrateTemplateStream(compiled: CompiledTemplate, requestContext?: Record<string, any>): Promise<{ html: string, boundaries?: { id: string, render: () => Promise<string> }[], redirect?: string, cookies?: string[] }>;
24
24
  /**
25
25
  * Hydrate a fragment (for SPA navigation).
26
26
  * Same as hydrateTemplate but returns the fragment portion.
@@ -6,9 +6,9 @@ export async function hydrateTemplate(compiled, requestContext = {}) {
6
6
  return (await runHydration(compiled, requestContext)).html;
7
7
  }
8
8
  export async function hydrateTemplateStream(compiled, requestContext = {}) {
9
- const { html, context, redirect } = await runHydration(compiled, requestContext);
9
+ const { html, context, redirect, cookies } = await runHydration(compiled, requestContext);
10
10
  if (redirect)
11
- return { html, redirect };
11
+ return { html, redirect, cookies };
12
12
  const { extractStreamBoundaries } = await import("./streaming.js");
13
13
  let boundaries = extractStreamBoundaries(context);
14
14
  const templates = compiled.streamTemplates;
@@ -22,7 +22,7 @@ export async function hydrateTemplateStream(compiled, requestContext = {}) {
22
22
  })), tplIds = new Set(tplBoundaries.map((b) => b.id));
23
23
  boundaries = [...(boundaries || []).filter((b) => !tplIds.has(b.id)), ...tplBoundaries];
24
24
  }
25
- return { html, boundaries: boundaries && boundaries.length > 0 ? boundaries : void 0 };
25
+ return { html, boundaries: boundaries && boundaries.length > 0 ? boundaries : void 0, cookies };
26
26
  }
27
27
  async function runHydration(compiled, requestContext) {
28
28
  const context = {
@@ -45,9 +45,9 @@ async function runHydration(compiled, requestContext) {
45
45
  cookies: requestContext.cookies ?? {}
46
46
  });
47
47
  if (action.redirect)
48
- return { html: compiled.html, context, redirect: action.redirect };
48
+ return { html: compiled.html, context, redirect: action.redirect, cookies: action.cookies };
49
49
  if (!hasPlaceholders(compiled.html))
50
- return { html: compiled.html, context };
50
+ return { html: compiled.html, context, cookies: action.cookies };
51
51
  const values = new Map;
52
52
  for (const [token, info] of Object.entries(compiled.placeholders))
53
53
  if (info.expression)
@@ -60,7 +60,7 @@ async function runHydration(compiled, requestContext) {
60
60
  } catch {
61
61
  values.set(token, "");
62
62
  }
63
- return { html: replacePlaceholders(compiled.html, values), context };
63
+ return { html: replacePlaceholders(compiled.html, values), context, cookies: action.cookies };
64
64
  }
65
65
  export async function hydrateFragment(compiled, requestContext = {}) {
66
66
  if (!compiled.hasServerScripts && !hasPlaceholders(compiled.fragment))
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Role-named colours, so a component can say what a colour is FOR.
3
+ *
4
+ * `@stacksjs/components` named palette shades directly — `bg-gray-100`,
5
+ * `text-gray-600`, `ring-indigo-600` — with a `dark:` variant beside each. That
6
+ * renders correctly and is impossible for a host app to redirect by meaning: you
7
+ * can remap `gray`, but you cannot say "every border in the library comes from
8
+ * `--stx-line`", because nothing distinguishes a gray that is a border from a
9
+ * gray that is muted text (stacksjs/stx#1930).
10
+ *
11
+ * These tokens are added to the BASE theme rather than shipped as an opt-in
12
+ * preset. That matters for one reason: a component that says `text-fg-muted`
13
+ * must resolve in every app, or adopting the component library would silently
14
+ * require a config change and render unstyled without it. Adding names is purely
15
+ * additive — no existing utility changes, and no opacity modifier on an existing
16
+ * palette colour turns into `color-mix()`.
17
+ *
18
+ * ## Where the values come from
19
+ *
20
+ * Each token names a shade in crosswind's own palette rather than carrying a hex
21
+ * value, and the light/dark pairs are the ones the component library already
22
+ * used most — measured, not invented:
23
+ *
24
+ * text-gray-900 dark:text-gray-100 ×70 -> text-fg
25
+ * text-gray-500 dark:text-gray-400 ×24 -> text-fg-soft
26
+ * border-gray-200 dark:border-gray-700 ×21 -> border-line
27
+ *
28
+ * So migrating a component to a token is appearance-preserving wherever its
29
+ * pairing was the dominant one, and the defaults stay in step with the palette
30
+ * because they are resolved from it rather than copied.
31
+ *
32
+ * ## How dark mode works
33
+ *
34
+ * Each token resolves to `var(--stx-<name>, <light value>)`, and
35
+ * {@link semanticTokenCSS} emits `:root` and `.dark` blocks that set the
36
+ * variable. So `text-fg-muted` is one class that is correct in both modes, and
37
+ * an app overrides a role by setting the variable — at runtime, with no rebuild.
38
+ *
39
+ * The fallback is deliberately the LIGHT value. If the variable block ever fails
40
+ * to reach a page, light mode is still correct and dark mode falls back to the
41
+ * light colour: degraded, but readable. The alternative — no fallback — would
42
+ * render the token as an invalid colour and take the whole component's styling
43
+ * with it.
44
+ *
45
+ * @module theme-tokens
46
+ */
47
+ /** A role, and the palette shades that back it in each mode. */
48
+ export interface SemanticToken {
49
+ /** Shade reference, e.g. `gray-900`. Resolved against the live palette. */
50
+ light: string;
51
+ dark: string;
52
+ /** What the role means, for the generated docs and for anyone reading here. */
53
+ description: string;
54
+ }
55
+ /**
56
+ * The role vocabulary.
57
+ *
58
+ * Five text roles rather than the usual three because the library genuinely
59
+ * distinguishes five: 900, 700, 600, 500 and 400 all appear as body-ish text
60
+ * with different dark partners. Collapsing them here would have been a redesign
61
+ * rather than a migration.
62
+ */
63
+ export declare const SEMANTIC_TOKENS: Record<string, SemanticToken>;
64
+ /** The CSS custom property that backs a role. */
65
+ export declare function tokenVariable(name: string): string;
66
+ type Palette = Record<string, unknown>;
67
+ /**
68
+ * The palette entries to merge into `theme.colors`.
69
+ *
70
+ * Every entry is `var(--stx-<name>, <light value>)`, so the name works as an
71
+ * ordinary utility (`bg-surface`, `text-fg-muted`, `ring-line-strong`) and an
72
+ * app can move it by setting the variable.
73
+ */
74
+ export declare function semanticColors(palette: Palette): Record<string, string>;
75
+ /**
76
+ * The `:root` / `.dark` block that gives each role its value per mode.
77
+ *
78
+ * Emitted ahead of the generated utilities so an app's own stylesheet — which
79
+ * comes after — can override any of it without a specificity fight.
80
+ *
81
+ * `.dark` matches the class strategy the rest of stx uses (`dark:` variants
82
+ * compile to `.dark .dark\:x`), so a token and a `dark:` utility on the same
83
+ * page agree about what dark mode is.
84
+ */
85
+ export declare function semanticTokenCSS(palette: Palette): string;
86
+ /** Every variable a role token reads, for documentation and tooling. */
87
+ export declare function semanticTokenNames(): string[];
88
+ export {};
@@ -0,0 +1,71 @@
1
+ export const SEMANTIC_TOKENS = {
2
+ fg: { light: "gray-900", dark: "gray-100", description: "Primary text" },
3
+ "fg-strong": { light: "gray-700", dark: "gray-300", description: "Labels, secondary headings" },
4
+ "fg-muted": { light: "gray-600", dark: "gray-400", description: "Supporting text" },
5
+ "fg-soft": { light: "gray-500", dark: "gray-400", description: "De-emphasised text" },
6
+ "fg-subtle": { light: "gray-400", dark: "gray-500", description: "Placeholders, disabled text" },
7
+ surface: { light: "gray-50", dark: "gray-800", description: "Page and panel background" },
8
+ "surface-raised": { light: "gray-100", dark: "gray-700", description: "Cards, popovers" },
9
+ "surface-sunken": { light: "gray-200", dark: "gray-700", description: "Wells, track backgrounds" },
10
+ line: { light: "gray-200", dark: "gray-700", description: "Default border, divider" },
11
+ "line-strong": { light: "gray-300", dark: "gray-600", description: "Input border, focus ring track" },
12
+ accent: { light: "indigo-600", dark: "indigo-400", description: "Primary action, selected state" },
13
+ info: { light: "blue-600", dark: "blue-400", description: "Informational emphasis, links" },
14
+ danger: { light: "red-600", dark: "red-400", description: "Errors, destructive actions" },
15
+ success: { light: "green-600", dark: "green-400", description: "Confirmation" },
16
+ warning: { light: "yellow-500", dark: "yellow-400", description: "Caution" },
17
+ "accent-solid": { light: "indigo-600", dark: "indigo-500", description: "Primary button fill" },
18
+ "info-solid": { light: "blue-600", dark: "blue-500", description: "Informational fill" },
19
+ "danger-solid": { light: "red-600", dark: "red-500", description: "Destructive button fill" },
20
+ "success-solid": { light: "green-600", dark: "green-500", description: "Confirmation fill" }
21
+ };
22
+ export function tokenVariable(name) {
23
+ return `--stx-${name}`;
24
+ }
25
+ function resolveShade(palette, reference) {
26
+ const split = reference.lastIndexOf("-");
27
+ if (split === -1)
28
+ return typeof palette[reference] === "string" ? palette[reference] : void 0;
29
+ const family = palette[reference.slice(0, split)];
30
+ if (!family || typeof family !== "object")
31
+ return;
32
+ const value = family[reference.slice(split + 1)];
33
+ return typeof value === "string" ? value : void 0;
34
+ }
35
+ export function semanticColors(palette) {
36
+ const colors = {};
37
+ for (const [name, token] of Object.entries(SEMANTIC_TOKENS)) {
38
+ const light = resolveShade(palette, token.light);
39
+ if (light === void 0)
40
+ continue;
41
+ colors[name] = `var(${tokenVariable(name)}, ${light})`;
42
+ }
43
+ return colors;
44
+ }
45
+ export function semanticTokenCSS(palette) {
46
+ const light = [], dark = [];
47
+ for (const [name, token] of Object.entries(SEMANTIC_TOKENS)) {
48
+ const lightValue = resolveShade(palette, token.light), darkValue = resolveShade(palette, token.dark);
49
+ if (lightValue === void 0)
50
+ continue;
51
+ light.push(` ${tokenVariable(name)}: ${lightValue};`);
52
+ if (darkValue !== void 0)
53
+ dark.push(` ${tokenVariable(name)}: ${darkValue};`);
54
+ }
55
+ if (light.length === 0)
56
+ return "";
57
+ return [
58
+ "/* stx role tokens \u2014 override any of these to re-theme by meaning. */",
59
+ ":root {",
60
+ ...light,
61
+ "}",
62
+ ".dark {",
63
+ ...dark,
64
+ "}",
65
+ ""
66
+ ].join(`
67
+ `);
68
+ }
69
+ export function semanticTokenNames() {
70
+ return Object.keys(SEMANTIC_TOKENS).map(tokenVariable);
71
+ }
@@ -14,8 +14,13 @@ export type { ScriptBlock, ScriptKind } from './stx-virtual-ts';
14
14
  * in a `<script server>` block both reported "Cannot redeclare block-scoped
15
15
  * variable", and those are ordinary names for server-rendered page data. It is
16
16
  * appended rather than prepended so the line numbers above it are untouched.
17
+ *
18
+ * `originDir` is the directory the `.stx` file really lives in. The buffer is
19
+ * written into the state directory, and tsc resolves a relative specifier
20
+ * against the file containing it — so without this, `../target` was looked for
21
+ * next to `.stx/typecheck/` (#1928).
17
22
  */
18
- export declare function buildVirtualSource(block: ScriptBlock, serverCode?: string): string;
23
+ export declare function buildVirtualSource(block: ScriptBlock, serverCode?: string, originDir?: string): string;
19
24
  /** Virtual path for a block, stable and traceable back to its origin. */
20
25
  export declare function virtualPathFor(filePath: string, kind: DiagnosticOrigin, index: number): string;
21
26
  /** Map a virtual path back to the `.stx` file it came from. */
package/dist/typecheck.js CHANGED
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { STX_RUNTIME_GLOBALS } from "./runtime-globals.js";
4
4
  import { stateDir } from "./state-dir.js";
5
5
  import {
6
+ absolutizeRelativeSpecifiers,
6
7
  buildVirtualTypeScript,
7
8
  extractScriptBlocks,
8
9
  resolvePosition,
@@ -16,13 +17,13 @@ export {
16
17
  extractScriptBlocks,
17
18
  extractTemplateExpressions
18
19
  } from "./stx-virtual-ts.js";
19
- export function buildVirtualSource(block, serverCode = "") {
20
+ export function buildVirtualSource(block, serverCode = "", originDir = "") {
20
21
  const leadingNewlines = block.startLine > 1 ? `
21
- `.repeat(block.startLine - 1) : "", code = blankScriptDirectives(substituteInterpolationsInPlace(block.code)), payload = block.kind === "client" || block.kind === "plain" ? clientPayloadDeclarations(serverCode) : "";
22
- return `${leadingNewlines + code}
22
+ `.repeat(block.startLine - 1) : "", code = blankScriptDirectives(substituteInterpolationsInPlace(block.code)), payload = block.kind === "client" || block.kind === "plain" ? clientPayloadDeclarations(serverCode, block.code) : "";
23
+ return absolutizeRelativeSpecifiers(`${leadingNewlines + code}
23
24
  ${payload}
24
25
  export {}
25
- `;
26
+ `, originDir);
26
27
  }
27
28
  function virtualStxModuleDeclaration(files) {
28
29
  const anchor = files.length > 0 ? path.dirname(path.resolve(files[0])) : process.cwd();
@@ -157,7 +158,7 @@ export async function typecheckStxFiles(files, options = {}) {
157
158
  if (names.length > 0)
158
159
  scrapedByFile.set(file, { names, line: clientBlocks[0].startLine, kind: clientBlocks[0].kind });
159
160
  }
160
- const templateBuffer = checkTemplates ? buildVirtualTypeScript(source, { runtimeGlobals: !runtimeTypes }) : null, expressions = templateBuffer ? [...templateBuffer.lineMap.values()].filter((m) => m.expression).length : 0;
161
+ const originDir = path.dirname(path.resolve(file)), templateBuffer = checkTemplates ? buildVirtualTypeScript(source, { runtimeGlobals: !runtimeTypes, originDir }) : null, expressions = templateBuffer ? [...templateBuffer.lineMap.values()].filter((m) => m.expression).length : 0;
161
162
  if (blocks.length === 0 && expressions === 0)
162
163
  continue;
163
164
  checkedFiles.push(file);
@@ -165,7 +166,7 @@ export async function typecheckStxFiles(files, options = {}) {
165
166
  expressionCount += expressions;
166
167
  blocks.forEach((block, i) => {
167
168
  virtualFiles.set(virtualPathFor(file, block.kind, i), {
168
- source: buildVirtualSource(block, serverCode),
169
+ source: buildVirtualSource(block, serverCode, originDir),
169
170
  origin: file,
170
171
  kind: block.kind
171
172
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/stx",
3
3
  "type": "module",
4
- "version": "0.2.176",
4
+ "version": "0.2.178",
5
5
  "description": "A performant UI Framework. Powered by Bun.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",
@@ -117,12 +117,12 @@
117
117
  "dependencies": {
118
118
  "@cwcss/crosswind": "^0.2.14",
119
119
  "@stacksjs/clapp": "^0.2.0",
120
- "@stacksjs/desktop": "0.2.176",
121
- "@stacksjs/sanitizer": "0.2.176",
120
+ "@stacksjs/desktop": "0.2.178",
121
+ "@stacksjs/sanitizer": "0.2.178",
122
122
  "@stacksjs/ts-i18n": "^0.1.11",
123
- "bun-plugin-stx": "0.2.176",
123
+ "bun-plugin-stx": "0.2.178",
124
124
  "bunfig": "^0.15.17",
125
- "stx-router": "0.2.176",
125
+ "stx-router": "0.2.178",
126
126
  "ts-broadcasting": "^0.0.9",
127
127
  "ts-images": "^0.1.9",
128
128
  "ts-syntax-highlighter": "^0.2.1"