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
36 changes: 31 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,38 @@ implementation:
- `language-provider-protocol` for LPP protocol contracts.

Do not treat an issue state, an unreleased branch, or a hidden command as proof
that a capability ships. Describe direction (for example the intent-driven
agent loop) as direction, not as current behavior. Keep internal terms such as
WIR, HIR, frontend, and provider out of primary homepage messaging.
that a capability ships. Planned features go in the agents section's "Coming
soon" line, never in current-capability copy. Keep internal terms such as
WIR, HIR, frontend, and provider out of primary homepage messaging. Say what a
feature does for a Workshop developer; implementation background belongs in the
owning repository's docs, not on the homepage.

All copy, navigation, and support claims live in `src/lib/site.ts`. The hero
terminal shows real `wright` output; refresh it when the CLI output changes.
Locale-independent data (URLs, commands, repository names, support status,
the hero terminal, and the "What it catches" examples) lives in
`src/lib/site.ts`. The terminal and every example show real `wright` output:
each example has a Workshop snippet per locale, written in that game client's
syntax, and must be re-run through `wright lint` when edited or when the CLI
output changes.

## Localization

English is the default at `/`; Simplified Chinese is at `/zh-CN`. There is no
browser-language redirect; readers switch from the header language menu.

- Copy lives in `src/lib/content/<locale>.ts`, typed by
`src/lib/content/types.ts`. Records keyed by the ids in `site.ts` and
fixed-length lists keep every locale making the same claims; change a claim
in all locales in the same PR.
- Commands, repository names, protocol names, and CLI output stay literal.
- Write each locale for its readers, not word for word. Keep it short and
plain: the audience is Workshop, OverPy, and OSTW developers, not compiler
engineers. Chinese follows the zh-CN game client's Workshop terms (地图工坊,
规则, 动作, 值, 服务器负载) and puts a space between CJK text and Latin words.
- Long Chinese headings may use `\u200b` to mark where they are allowed to
wrap; headings otherwise break only at spaces and punctuation.
- To add a locale: add it to `src/lib/locales.ts`, add a content file, and add
the path to `prerender.entries` in `svelte.config.js` and to
`scripts/verify-build.mjs`.

## Stack

Expand Down
39 changes: 39 additions & 0 deletions scripts/verify-build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,42 @@ for (const installer of installers) {
process.exit(1);
}
}

// Each locale must ship as its own prerendered page with correct language
// metadata, and commands and captured CLI output must stay untranslated.
const SITE_URL = 'https://wrightkit.dev';
const pages = [
{ file: 'index.html', lang: 'en', canonical: `${SITE_URL}/` },
{ file: 'zh-CN.html', lang: 'zh-CN', canonical: `${SITE_URL}/zh-CN` }
];
const literals = [
'curl -fsSL https://wrightkit.dev/install.sh | bash',
'brew install wrightkit/tap/wright',
'wright lint rules.txt',
'warning[min-wait-loop]: loop body waits at the workshop minimum rate',
'warning[while-without-wait]: loop body contains no wait call'
];

for (const page of pages) {
const path = resolve(ROOT_DIR, 'build', page.file);
if (!existsSync(path)) {
console.error(`[verify-build] Error: build/${page.file} does not exist.`);
process.exit(1);
}
const html = readFileSync(path, 'utf8');
const expected = [
`<html lang="${page.lang}"`,
`<link rel="canonical" href="${page.canonical}"`,
...pages.map((alt) => `<link rel="alternate" hreflang="${alt.lang}" href="${alt.canonical}"`),
`<link rel="alternate" hreflang="x-default" href="${SITE_URL}/"`,
...literals
];
for (const snippet of expected) {
if (!html.includes(snippet)) {
console.error(`[verify-build] Error: build/${page.file} is missing: ${snippet}`);
process.exit(1);
}
}
}

console.log(`[verify-build] OK: installers and ${pages.length} localized pages verified.`);
115 changes: 115 additions & 0 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@
--chrome-bg-scrolled: light-dark(rgb(255 255 255 / 0.8), rgb(32 32 29 / 0.8));
--chrome-line: light-dark(rgb(16 16 14 / 0.07), rgb(247 247 245 / 0.07));
--chrome-fade: light-dark(rgb(246 246 244 / 0.85), rgb(16 16 14 / 0.85));
--popover-bg: light-dark(rgb(255 255 255 / 0.92), rgb(36 36 33 / 0.92));
--popover-ring: light-dark(rgb(16 16 14 / 0.1), rgb(247 247 245 / 0.08));
--popover-shadow: light-dark(rgb(16 16 14 / 0.14), rgb(0 0 0 / 0.45));
}

:root[data-theme='light'] {
Expand Down Expand Up @@ -189,6 +192,17 @@
0 1px 3px var(--surface-shadow);
}

/* Small floating panel anchored to a trigger (language menu). */
.popover {
border-radius: var(--radius-card);
background: var(--popover-bg);
backdrop-filter: blur(24px) saturate(180%);
box-shadow:
inset 0 0 0 1px var(--popover-ring),
0 12px 32px -8px var(--popover-shadow),
0 2px 6px var(--popover-shadow);
}

.eyebrow {
font-size: 0.8125rem;
line-height: 1.3;
Expand Down Expand Up @@ -281,6 +295,34 @@
}
}

/* Scroll reveal (see $lib/reveal.ts). Elements start hidden only once JS is
known to run; the fallback animation shows them if hydration never lands. */
:root[data-js] [data-reveal]:not([data-revealed]) {
opacity: 0;
transform: translateY(14px);
animation: reveal-fallback 0s 3s forwards;
}

:root[data-js] [data-reveal='fade']:not([data-revealed]) {
transform: none;
}

[data-reveal] {
transition:
opacity 600ms var(--ease-settle),
transform 800ms var(--ease-settle),
background-color 200ms ease,
box-shadow 200ms ease;
transition-delay: var(--reveal-delay, 0ms);
}

@keyframes reveal-fallback {
to {
opacity: 1;
transform: none;
}
}

/* Reduced motion keeps opacity/color feedback but drops movement and scaling. */
@media (prefers-reduced-motion: reduce) {
html {
Expand All @@ -294,9 +336,23 @@
[data-motion='move'] {
transition-property: opacity, background-color, color !important;
}

:root[data-js] [data-reveal]:not([data-revealed]) {
transform: none;
}

[data-reveal] {
transition: opacity 300ms linear;
transition-delay: calc(var(--reveal-delay, 0ms) / 3);
}
}

@media (prefers-reduced-transparency: reduce) {
.popover {
background: var(--color-ink-900);
backdrop-filter: none;
}

.chrome,
.chrome[data-scrolled] {
background: var(--color-ink-950);
Expand Down Expand Up @@ -326,6 +382,12 @@
box-shadow: inset 0 0 0 1px var(--color-ink-300);
}

.popover {
background: var(--color-ink-950);
backdrop-filter: none;
box-shadow: inset 0 0 0 1px var(--color-ink-300);
}

.btn-primary {
outline: 1px solid var(--color-ink-50);
outline-offset: 2px;
Expand All @@ -335,3 +397,56 @@
box-shadow: inset 0 0 0 1px var(--color-ink-200);
}
}

/*
* Chinese text. CJK glyphs are full-width and dense, so the negative tracking
* and tight leading tuned for Latin display type would crowd them. Unlayered,
* so it overrides the tracking utilities on headings. Latin runs keep the
* system face; CJK falls through to a Simplified Chinese font rather than
* whatever the OS picks for an untagged Han character. The same stack covers
* Chinese labels on English pages (the language switcher).
*/
:root:lang(zh) body,
[lang|='zh']:not(html) {
font-family:
system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', 'Noto Sans CJK SC', 'Source Han Sans SC', sans-serif;
}

:root:lang(zh) body {
line-height: 1.7;
}

/* Workshop snippets in the zh-CN client's syntax mix Latin and CJK; keep the
monospace face and fall back to a CJK face for Chinese element names. */
:lang(zh) :is(code, pre, kbd, samp, .font-mono) {
font-family:
ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', 'Noto Sans CJK SC', 'Source Han Sans SC', monospace;
}

:lang(zh) :is(h1, h2, h3, h4, .btn, .eyebrow) {
letter-spacing: 0;
}

/* Wrap headings only between words (spaces, punctuation, or U+200B hints). */
:lang(zh) :is(h1, h2) {
word-break: keep-all;
overflow-wrap: anywhere;
}

:lang(zh) .text-hero {
line-height: 1.2;
}

:lang(zh) .text-title {
line-height: 1.3;
}

:lang(zh) .text-lead {
line-height: 1.75;
}

:lang(zh) .leading-snug {
line-height: 1.6;
}
12 changes: 12 additions & 0 deletions src/app.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
/// <reference types="@sveltejs/kit" />

import type { Locale } from '$lib/locales';

declare global {
namespace App {
interface PageData {
locale?: Locale;
}
}
}

export {};
3 changes: 2 additions & 1 deletion src/app.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="%lang%">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
Expand All @@ -8,6 +8,7 @@
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#10100e" />
<!-- Apply a saved appearance before first paint; keep in sync with src/lib/theme.svelte.ts. -->
<script>
document.documentElement.dataset.js = '';
(function () {
try {
var t = localStorage.getItem('wrightkit-theme');
Expand Down
10 changes: 10 additions & 0 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Handle } from '@sveltejs/kit';
import { localeFromPath, localeInfo } from '$lib/locales';

// Runs while prerendering, so each static page ships with its own `lang`.
export const handle: Handle = ({ event, resolve }) => {
const lang = localeInfo[localeFromPath(event.url.pathname)].tag;
return resolve(event, {
transformPageChunk: ({ html }) => html.replace('%lang%', lang)
});
};
50 changes: 22 additions & 28 deletions src/lib/components/AgentsSection.svelte
Original file line number Diff line number Diff line change
@@ -1,38 +1,32 @@
<script lang="ts">
import { reveal } from '$lib/reveal';
import Section from './Section.svelte';
import { agents } from '$lib/site';
import { currentMessages } from '$lib/content';

const t = $derived(currentMessages());
const agents = $derived(t.agents);
</script>

<Section id="agents" eyebrow={agents.eyebrow} title={agents.title} lead={agents.lead}>
<div class="mt-10 grid gap-3 sm:mt-14 sm:gap-4 md:grid-cols-2">
{#each agents.columns as column (column.title)}
<div class="surface p-5 sm:p-7">
<h3 class="text-[1.0625rem] font-semibold tracking-[-0.012em] text-ink-50">{column.title}</h3>
<ul class="mt-4 space-y-3">
{#each column.points as point (point)}
<li class="flex gap-3 text-[0.9375rem] leading-relaxed text-ink-300">
<svg class="mt-[0.3em] h-4 w-4 shrink-0 text-accent-400" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<polyline points="3.5 8.5 6.5 11.5 12.5 4.5" />
</svg>
<span>{point}</span>
</li>
{/each}
</ul>
</div>
<Section id="agents" title={agents.title} lead={agents.lead}>
<ul class="mt-10 grid gap-3 sm:mt-14 sm:gap-4 md:grid-cols-3">
{#each agents.capabilities as item, i (i)}
<li class="surface p-5 sm:p-6" data-reveal style="--reveal-delay: {i * 70}ms" use:reveal>
<h3 class="text-[1.0625rem] font-semibold tracking-[-0.012em] text-ink-50">{item.title}</h3>
<p class="mt-2 text-[0.9375rem] leading-relaxed text-ink-400">{item.body}</p>
</li>
{/each}
</div>
</ul>

<div class="mt-12 sm:mt-16">
<h3 class="text-[1.0625rem] font-semibold tracking-[-0.012em] text-ink-50">{agents.loop.title}</h3>
<p class="mt-2 max-w-2xl text-[0.9375rem] leading-relaxed text-pretty text-ink-400">{agents.loop.lead}</p>
<ol class="mt-6 grid gap-px overflow-hidden rounded-card bg-ink-800/70 sm:grid-cols-5">
{#each agents.loop.steps as step, i (step.title)}
<li class="bg-ink-950 p-4 sm:p-5">
<span class="font-mono text-xs text-ink-600">{String(i + 1).padStart(2, '0')}</span>
<p class="mt-2 text-[0.9375rem] font-semibold text-ink-100">{step.title}</p>
<p class="mt-1 text-sm leading-snug text-ink-500">{step.body}</p>
<div data-reveal="fade" style="--reveal-delay: 250ms" use:reveal class="mt-5 flex flex-wrap items-center gap-x-3 gap-y-2 text-[0.9375rem] text-ink-500">
<span class="rounded-full px-2.5 py-0.5 text-xs font-medium text-ink-400 ring-1 ring-inset ring-ink-800">
{agents.upcomingLabel}
</span>
<ul class="flex flex-wrap gap-x-2 gap-y-1">
{#each agents.upcoming as item, i (i)}
<li class="flex gap-2">
{#if i > 0}<span class="text-ink-700" aria-hidden="true">·</span>{/if}{item}
</li>
{/each}
</ol>
</ul>
</div>
</Section>
13 changes: 9 additions & 4 deletions src/lib/components/CopyButton.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<script lang="ts">
let { text, label = 'Copy' }: { text: string; label?: string } = $props();
import { currentMessages } from '$lib/content';

let { text, label }: { text: string; label?: string } = $props();

const t = $derived(currentMessages());
const name = $derived(label ?? t.ui.copy);

let copied = $state(false);
let timeoutId: ReturnType<typeof setTimeout> | undefined;
Expand Down Expand Up @@ -40,8 +45,8 @@
? 'bg-accent-500/15 text-accent-400'
: 'bg-ink-800/80 text-ink-300 hover:bg-ink-700 hover:text-ink-50'}"
onclick={handleCopy}
aria-label={label}
title={label}
aria-label={name}
title={name}
>
{#if copied}
<svg class="h-3.5 w-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
Expand All @@ -53,5 +58,5 @@
<path d="M3.5 10.5V4.5a1 1 0 0 1 1-1h6" />
</svg>
{/if}
<span class="sr-only" aria-live="polite">{copied ? 'Copied to clipboard' : ''}</span>
<span class="sr-only" aria-live="polite">{copied ? t.ui.copied : ''}</span>
</button>
Loading
Loading