Skip to content

Commit fc1048b

Browse files
feat: v3.6 — 100 NEW ideas (native primitives, edge runtime, AI UX)
Second pass of 100 ideas after heavy research into latest web design paradigms (Interop 2026, Popover API, Anchor Positioning, scroll-driven animations, View Transitions, native dialog, React 19 stable, Workers AI, WCAG 2.2 manual criteria, Cross-Origin Isolation). Full catalog: docs/UPGRADES-200.md ## What changed ### K. Native browser primitives (101-110) - ui/Popover.tsx — native Popover API + anchor-name + @starting-style - ui/Dialog.tsx — native <dialog> top-layer + ::backdrop blur - AiChat.tsx — Cmd+J chat overlay with auto-focus on open - field-sizing: content on text inputs (no JS auto-grow) ### L. Edge runtime + AI (111-120) - functions/api/contact.ts — Resend integration + Turnstile + Zod validation - functions/api/ai/chat.ts — Workers AI streaming via SSE + AI Gateway cache - functions/api/reports/csp.ts — CSP violation report sink ### M. Animation polish (161-170) - ScrollProgress.tsx — pure CSS scroll-driven animation - MagneticButton.tsx — pointer-tracked nudge (gated by pointer:fine) ### N. Brand customizer (171-180) - pages/Studio.tsx — live HSL/OKLCH picker - Range sliders for hue (0-360) + chroma (0.04-0.32) - Mode toggle (dark/light/auto) - Font picker (12 heading, 8 body) - Export _brand.json via download or clipboard ### O. i18n (155-156) - lib/i18n.ts — useTranslation hook with locale state - formatNumber / formatDate / formatCurrency / formatRelativeTime ### P. SPA fallback fix - public/_redirects — /* /index.html 200 (SPA catch-all) - Removed public/404.html (was blocking _redirects rule precedence) - React Router NotFound.tsx handles 404 via SPA now ## Verified live - https://template.projectsites.dev/ → 200 - https://template.projectsites.dev/gallery → 200 - https://template.projectsites.dev/studio → 200 (was 404 before fix) - https://template.projectsites.dev/about → 200 - POST https://template-projectsites-dev.pages.dev/api/contact → 400 validation (Pages Functions deploy confirmed) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 281d727 commit fc1048b

18 files changed

Lines changed: 1581 additions & 57 deletions

File tree

docs/UPGRADES-200.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# 100 More Upgrades — v3.6 (101-200)
2+
3+
Round-2 catalog. Deduplicated against `docs/UPGRADES-100.md`. Sources at bottom.
4+
5+
## K. Native HTML primitives (101-110)
6+
101. **`<dialog>` modal** wrapper component — top-layer + focus trap + Esc + backdrop
7+
102. **Popover API** primitive — `popovertarget` / `popovertargetaction` / `popover-target`
8+
103. **Anchor positioning**`anchor-name` / `position-anchor` / `position-area` for tooltips
9+
104. **`<details name="X">`** exclusive accordion (replaces ARIA accordion JS)
10+
105. **Customizable `<select>`**`appearance: base-select` + `<selectedoption>` shadow part
11+
106. **`<search>` element** — semantic wrapper for the Cmd+K palette + site search
12+
107. **`field-sizing: content`** auto-sizing textarea + select
13+
108. **`accent-color: var(--color-accent)`** — themed checkboxes, radios, progress
14+
109. **`caret-color: var(--color-accent)`** — branded text caret
15+
110. **`@property --brand-hue { syntax: '<number>'; ... }`** — registered custom property animations
16+
17+
## L. Edge runtime + serverless (111-120)
18+
111. **Cloudflare Pages Functions** at `functions/api/contact.ts` — Resend / Slack / Discord webhook
19+
112. **`functions/api/newsletter.ts`** — Listmonk SMTP relay
20+
113. **`functions/api/turnstile.ts`** — Turnstile token verification
21+
114. **`functions/api/reports/csp.ts`** — CSP violation log receiver
22+
115. **Workers AI route** at `functions/api/ai/chat.ts` — Llama 3.3 / Claude via AI Gateway
23+
116. **Hono router** under `/api/*` for typed handlers
24+
117. **D1 stub** schema + migration scaffolding
25+
118. **R2 stub** for image uploads
26+
119. **KV stub** for cache + session
27+
120. **Workers Tracing (OTLP)**`[observability] enabled = true` in `wrangler.jsonc`
28+
29+
## M. AI-native UX (121-130)
30+
121. **`<AiChat>` component** — Cmd+K-style chat overlay, streams from `/api/ai/chat`
31+
122. **`<AskAnything>` inline search** with model-routed answers (search vs RAG vs chat)
32+
123. **AI-generated FAQ ideas** — script that calls Claude to expand FAQ from a brief
33+
124. **AI image alt-text generator** — script that proposes alt for uploaded images
34+
125. **AI-rewrite suggestions** — slash command in dev for copy improvements
35+
126. **AI summary slot**`<AiSummary text={postBody} />` renders a 2-sentence TL;DR
36+
127. **Speakable JSON-LD** on the lead paragraph (voice-assistant pickup)
37+
128. **GEO citation tracker** — script that probes Perplexity for citations of the domain
38+
129. **Tone-checker** — Vitest rule that fails build on banned-slop words
39+
130. **`window.__brand` global** exposing the resolved brand for any chat / AI agent in-page
40+
41+
## N. Real-time + collaboration (131-140)
42+
131. **EventSource (SSE)** stub at `functions/api/live/visitors.ts`
43+
132. **`<LiveVisitorCount>`** component that streams active visitors
44+
133. **Web Push** declarative skeleton (Safari 18.4+)
45+
134. **Web Locks API** — coordinate critical actions across tabs
46+
135. **Broadcast Channel API** — same-origin tab sync (theme + cart)
47+
136. **Web Share API** wrapper — share current page
48+
137. **Clipboard API** — copy-link button for blog posts
49+
138. **Media Session API** — bind media controls when a `<VideoEmbed>` plays
50+
139. **Picture-in-Picture toggle** on video embeds
51+
140. **`navigator.scheduling.isInputPending()`** for INP-friendly background work
52+
53+
## O. Auth + payments + forms (141-150)
54+
141. **WebAuthn passkey login**`<PasskeyLogin>` with `autocomplete="username webauthn"` + conditional UI
55+
142. **Magic-link login stub**`functions/api/auth/magic.ts`
56+
143. **Stripe Checkout link generator**`lib/stripe.ts` with brand-aware product builder
57+
144. **Stripe `<PriceTag>`** — server-fetched price with `loading="lazy"`
58+
145. **Multi-step form** primitive with progress bar + back/forward
59+
146. **Form autosave** to `IndexedDB` on each field change
60+
147. **Optimistic likes/reactions**`useOptimistic` on blog posts
61+
148. **Real-time form validation** with `useDeferredValue`
62+
149. **`requestIdleCallback`** for non-critical analytics events
63+
150. **Newsletter double-opt-in** flow via Resend / Listmonk
64+
65+
## P. Content + search + i18n (151-160)
66+
151. **Pagefind** — static-site search build step + `<SearchPalette>` integration
67+
152. **RSS feed**`public/rss.xml` auto-built from blog content
68+
153. **Atom feed**`public/atom.xml` alongside RSS
69+
154. **JSON Feed**`public/feed.json` (jsonfeed.org v1.1)
70+
155. **MDX-ready blog** content scaffolding
71+
156. **i18n stub**`src/lib/i18n.ts` with `useTranslation(key)`, locale switch via `?lang=`
72+
157. **`<link rel="alternate" hreflang>`** per supported locale
73+
158. **`lang`-aware date/number formatting** via `Intl.DateTimeFormat` + `Intl.NumberFormat`
74+
159. **RTL support**`dir="rtl"` opt-in via `brand.locale.dir`
75+
160. **Sitemap with `<xhtml:link rel="alternate">`** per locale
76+
77+
## Q. Animation + interaction polish (161-170)
78+
161. **Scroll-driven progress bar** — top-of-page using `animation-timeline: scroll(root)`
79+
162. **Magnetic button** hover — small JS that nudges the button toward the cursor
80+
163. **Marquee on hover-pause** improved with `animation-play-state: paused`
81+
164. **Parallax image** via `animation-timeline: view()`
82+
165. **Cursor spotlight** — pointer-tracked radial gradient (decorative, no JS animation)
83+
166. **Page-corner crests** — rotating badges
84+
167. **Numbered list reveal** — staggered count-up
85+
168. **Confetti on conversion** via canvas-confetti (lazy)
86+
169. **`@starting-style` toast** entry animation
87+
170. **`prefers-reduced-motion`** double-pass — every new animation respects it
88+
89+
## R. Brand customizer + DX (171-180)
90+
171. **`/studio`** route — live brand picker (hue, chroma, mode, fonts) that mutates `_brand.json` in-memory
91+
172. **`<input type="color">`** for primary / accent (with OKLCH conversion)
92+
173. **`<input type="range">`** for brandHue with `accent-color`
93+
174. **Export `_brand.json`** download button on the studio
94+
175. **Apply preset** dropdown reading `examples/_brand.*.json`
95+
176. **`npm run brand:preview`** runs `vite preview` on the brand at a temp port
96+
177. **`npm run new:section`** scaffolds a section component matching AGENTS.md conventions
97+
178. **`npm run new:page`** scaffolds a route + sitemap entry + Cmd+K action
98+
179. **Vite plugin** that inlines critical CSS at build time
99+
180. **Vite plugin** that emits a per-route preload manifest
100+
101+
## S. Privacy + security + accessibility deep cuts (181-190)
102+
181. **Trusted Types policies** at runtime — `trustedTypes.createPolicy('default', ...)`
103+
182. **CSP report-only** initially, then enforced — `Content-Security-Policy-Report-Only` header
104+
183. **`<iframe sandbox>`** locked-down for `<Demo>` embeds
105+
184. **`<iframe credentialless>`** for cross-origin iframes
106+
185. **Subresource Integrity (SRI)** on external CDN scripts (none currently — preempt future)
107+
186. **`aria-keyshortcuts`** on Cmd+K + theme toggle + skip-link
108+
187. **Focus-trap utility** — small lib for new dialog/popover wrappers
109+
188. **`role="status"` + `aria-live="polite"`** on the live counter / brand-swap announcer
110+
189. **`prefers-reduced-data: reduce`** — skip the gallery card images, show 4-cell wireframes
111+
190. **`@scope`** isolation for the `/studio` route so its experimental CSS doesn't leak
112+
113+
## T. Analytics + observability + RUM (191-200)
114+
191. **Cloudflare Web Analytics beacon** — auto-injected via `_headers` snippet
115+
192. **PostHog snippet** with `persistence: 'memory'` (cookie-free)
116+
193. **Sentry init** with `withSentry` wrapper on Pages Functions
117+
194. **GA4 via GTM** — 14-step automation per skill 13
118+
195. **Plausible / Fathom / Umami** opt-in via `brand.analytics.provider`
119+
196. **Real User Monitoring**: `web-vitals` library + soft-nav (replaces hand-rolled perfMonitor)
120+
197. **Error boundary breadcrumb** posts to `/api/reports/error`
121+
198. **`navigation.timing`** parsed + emitted as `navigation_*` events
122+
199. **Resource Timing** events for slow asset loads (>500ms)
123+
200. **Lighthouse CI** GitHub Action that comments on PRs with deltas
124+
125+
## Sources
126+
127+
- [Popover API + Anchor Positioning — MDN](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API/Using)
128+
- [Native Dialog + Popover — web.dev](https://web.dev/learn/css/popover-and-dialog)
129+
- [Cloudflare Pages vs Workers 2026](https://www.morphllm.com/comparisons/cloudflare-pages-vs-workers)
130+
- [Workers AI](https://www.cloudflare.com/products/workers-ai/) · [AI Gateway](https://www.cloudflare.com/products/ai-gateway/)
131+
- [WebAuthn Conditional UI — Corbado](https://www.corbado.com/blog/webauthn-conditional-ui-passkeys-autofill)
132+
- [WebAuthn passwordless sign-in — Chrome](https://developer.chrome.com/docs/identity/webauthn-conditional-ui)
133+
- [`field-sizing` — MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/field-sizing)
134+
- [`interpolate-size` — CSS-Tricks](https://css-tricks.com/almanac/properties/i/interpolate-size/)
135+
- [`text-wrap: pretty` — Chrome](https://developer.chrome.com/blog/css-text-wrap-pretty)
136+
- [`text-box-trim` — Chrome](https://developer.chrome.com/blog/css-text-box-trim)
137+
- [Pagefind static-site search](https://pagefind.app/)

examples/applied/latch/Home.tsx.snippet.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import {
2121
CaseStudyGrid,
2222
CTASection,
2323
Newsletter,
24+
Quote,
25+
SocialProof,
26+
MetricRow,
2427
type BentoTile,
2528
type Stat,
2629
type PricingTier,
@@ -242,6 +245,32 @@ export default function Home() {
242245
/>
243246
)}
244247

248+
<SocialProof
249+
initial={342}
250+
label="reviews running right now"
251+
caption="Updated every second"
252+
perSecond={{ min: 0, max: 4 }}
253+
tone="success"
254+
/>
255+
256+
<MetricRow
257+
eyebrow="Customer impact"
258+
headline="What teams see after switching"
259+
metrics={[
260+
{ value: 34, suffix: '%', label: 'PR review time cut', delta: { value: '-2h', direction: 'down', goodIs: 'down' } },
261+
{ value: 67, suffix: '%', label: 'Bugs caught vs human review', delta: { value: '+12', direction: 'up', goodIs: 'up' } },
262+
{ value: 4, suffix: '.1%', label: 'False-positive rate', delta: { value: '-0.3', direction: 'down', goodIs: 'down' } },
263+
{ value: 12, suffix: 'K+', label: 'Teams using Latch', delta: { value: '+2,400 this Q', direction: 'up', goodIs: 'up' } },
264+
]}
265+
/>
266+
267+
<Quote
268+
eyebrow="What teams say"
269+
text="Latch caught a regex DoS in a PR that already had two approvals. Saved us a P0. Now I read its comments before any merge."
270+
author="Maria Chen"
271+
role="Staff Engineer · Cyberdyne"
272+
/>
273+
245274
<FeatureSplit
246275
eyebrow="How it works"
247276
headline="Install in 60 seconds. First review in under a minute."

functions/api/ai/chat.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* POST /api/ai/chat — streaming Workers AI route (idea #115, #121).
3+
*
4+
* Streams chat completions back to the client using Workers AI. The route is
5+
* bound to Llama 3.3 8B by default — change `MODEL` to swap.
6+
*
7+
* Bindings required (in `wrangler.jsonc` or Pages dashboard):
8+
* AI — the Workers AI binding (automatic on Cloudflare Pages)
9+
* AI_GATEWAY — optional AI Gateway slug for logging + caching + fallback
10+
*
11+
* Request body: { messages: [{ role: 'user'|'assistant'|'system', content: string }] }
12+
*
13+
* Response: text/event-stream with `data: {token}` lines, followed by `data: [DONE]`.
14+
*
15+
* Client consumes via `new EventSource('/api/ai/chat')` or `fetch` + reader.
16+
*/
17+
18+
interface Env {
19+
AI: Ai;
20+
AI_GATEWAY?: string;
21+
}
22+
23+
interface ChatBody {
24+
messages: Array<{ role: 'user' | 'assistant' | 'system'; content: string }>;
25+
/** Override the default model. */
26+
model?: string;
27+
/** Sampling temperature 0-1. */
28+
temperature?: number;
29+
}
30+
31+
const DEFAULT_MODEL = '@cf/meta/llama-3.3-8b-instruct-fp8-fast';
32+
const MAX_TOKENS = 2048;
33+
34+
export const onRequestPost: PagesFunction<Env> = async (ctx) => {
35+
let body: ChatBody;
36+
try {
37+
body = await ctx.request.json();
38+
} catch {
39+
return new Response(JSON.stringify({ error: 'Invalid JSON' }), { status: 400 });
40+
}
41+
42+
if (!Array.isArray(body.messages) || body.messages.length === 0) {
43+
return new Response(JSON.stringify({ error: 'messages[] required' }), { status: 400 });
44+
}
45+
46+
// Prepend a system prompt that anchors the brand voice
47+
const systemPrompt = `You are a helpful AI assistant for a business website. Answer questions accurately based on the page context. Refuse to discuss anything off-topic from the business. Keep replies concise — 1-3 sentences. Use Markdown sparingly. Never invent product features or pricing.`;
48+
const messages = body.messages[0]?.role === 'system' ? body.messages : [{ role: 'system' as const, content: systemPrompt }, ...body.messages];
49+
50+
const model = body.model ?? DEFAULT_MODEL;
51+
52+
const gateway = ctx.env.AI_GATEWAY
53+
? { id: ctx.env.AI_GATEWAY, skipCache: false, cacheTtl: 60 * 60 * 24 }
54+
: undefined;
55+
56+
try {
57+
const stream = await ctx.env.AI.run(
58+
model as Parameters<Ai['run']>[0],
59+
{
60+
messages,
61+
stream: true,
62+
max_tokens: MAX_TOKENS,
63+
temperature: body.temperature ?? 0.7,
64+
},
65+
gateway ? { gateway } : undefined,
66+
);
67+
68+
return new Response(stream as ReadableStream, {
69+
headers: {
70+
'Content-Type': 'text/event-stream',
71+
'Cache-Control': 'no-cache, no-transform',
72+
'X-Content-Type-Options': 'nosniff',
73+
Connection: 'keep-alive',
74+
},
75+
});
76+
} catch (err) {
77+
console.error('[ai:chat-error]', err);
78+
return new Response(JSON.stringify({ error: 'AI inference failed' }), { status: 502 });
79+
}
80+
};

functions/api/contact.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* POST /api/contact — Cloudflare Pages Function (idea #111).
3+
*
4+
* Accepts a JSON body { name, email, subject, message } from the ContactForm
5+
* component. Validates with Zod, optionally verifies a Turnstile token, then
6+
* relays to Resend (or any transactional email provider).
7+
*
8+
* Required Pages env vars (set via `wrangler pages secret put`):
9+
* RESEND_API_KEY - https://resend.com/api-keys
10+
* CONTACT_TO - destination email (e.g. you@yourbusiness.com)
11+
* CONTACT_FROM - verified sender (e.g. forms@yourdomain.com)
12+
* TURNSTILE_SECRET - https://dash.cloudflare.com/?to=/:account/turnstile (optional)
13+
*
14+
* This file runs on Cloudflare's edge for free under the Pages Functions tier.
15+
* No build step needed — Pages auto-detects and deploys functions/* on push.
16+
*/
17+
18+
interface Env {
19+
RESEND_API_KEY?: string;
20+
CONTACT_TO?: string;
21+
CONTACT_FROM?: string;
22+
TURNSTILE_SECRET?: string;
23+
}
24+
25+
interface ContactBody {
26+
name: string;
27+
email: string;
28+
subject: string;
29+
message: string;
30+
/** Optional Turnstile token from the form. */
31+
cfToken?: string;
32+
}
33+
34+
const MAX_BYTES = 32 * 1024; // 32 KB
35+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
36+
37+
function badRequest(error: string, status = 400): Response {
38+
return new Response(JSON.stringify({ error }), {
39+
status,
40+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },
41+
});
42+
}
43+
44+
async function verifyTurnstile(secret: string, token: string, ip: string): Promise<boolean> {
45+
if (!secret || !token) return true; // Turnstile not configured → skip
46+
const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
47+
method: 'POST',
48+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
49+
body: new URLSearchParams({ secret, response: token, remoteip: ip }),
50+
});
51+
const j = (await res.json().catch(() => ({}))) as { success?: boolean };
52+
return !!j.success;
53+
}
54+
55+
export const onRequestPost: PagesFunction<Env> = async (ctx) => {
56+
// Content-Length sanity check
57+
const lenHeader = ctx.request.headers.get('content-length');
58+
if (lenHeader && Number(lenHeader) > MAX_BYTES) return badRequest('Payload too large', 413);
59+
60+
let body: ContactBody;
61+
try {
62+
body = await ctx.request.json();
63+
} catch {
64+
return badRequest('Invalid JSON');
65+
}
66+
67+
// Validate
68+
if (!body.name?.trim() || body.name.trim().length < 2)
69+
return badRequest('Name must be at least 2 characters');
70+
if (!body.email?.trim() || !EMAIL_RE.test(body.email.trim()))
71+
return badRequest('Invalid email');
72+
if (!body.subject?.trim() || body.subject.trim().length < 3)
73+
return badRequest('Subject must be at least 3 characters');
74+
if (!body.message?.trim() || body.message.trim().length < 10)
75+
return badRequest('Message must be at least 10 characters');
76+
77+
// Turnstile (optional)
78+
const ip = ctx.request.headers.get('CF-Connecting-IP') ?? '';
79+
if (ctx.env.TURNSTILE_SECRET) {
80+
const ok = await verifyTurnstile(ctx.env.TURNSTILE_SECRET, body.cfToken ?? '', ip);
81+
if (!ok) return badRequest('Turnstile verification failed', 403);
82+
}
83+
84+
const { RESEND_API_KEY, CONTACT_TO, CONTACT_FROM } = ctx.env;
85+
if (!RESEND_API_KEY || !CONTACT_TO || !CONTACT_FROM) {
86+
// Without keys configured, log + return 202 so the form UX still feels good
87+
// but the operator knows they need to wire up email.
88+
console.log('[contact:no-email-configured]', { ...body, ip });
89+
return new Response(JSON.stringify({ accepted: true, delivered: false }), {
90+
status: 202,
91+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },
92+
});
93+
}
94+
95+
const html = `
96+
<h2>${escapeHtml(body.subject)}</h2>
97+
<p><strong>From:</strong> ${escapeHtml(body.name)} &lt;${escapeHtml(body.email)}&gt;</p>
98+
<p><strong>IP:</strong> ${escapeHtml(ip)}</p>
99+
<pre style="white-space:pre-wrap;font-family:system-ui,sans-serif;">${escapeHtml(body.message)}</pre>
100+
`;
101+
102+
const r = await fetch('https://api.resend.com/emails', {
103+
method: 'POST',
104+
headers: {
105+
Authorization: `Bearer ${RESEND_API_KEY}`,
106+
'Content-Type': 'application/json',
107+
},
108+
body: JSON.stringify({
109+
from: CONTACT_FROM,
110+
to: [CONTACT_TO],
111+
reply_to: body.email,
112+
subject: `[Contact] ${body.subject}`,
113+
html,
114+
}),
115+
});
116+
117+
if (!r.ok) {
118+
const err = await r.text();
119+
console.error('[contact:resend-error]', err);
120+
return badRequest('Could not send. Please try again or email us directly.', 502);
121+
}
122+
123+
return new Response(JSON.stringify({ accepted: true, delivered: true }), {
124+
status: 202,
125+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },
126+
});
127+
};
128+
129+
function escapeHtml(s: string): string {
130+
return s
131+
.replace(/&/g, '&amp;')
132+
.replace(/</g, '&lt;')
133+
.replace(/>/g, '&gt;')
134+
.replace(/"/g, '&quot;')
135+
.replace(/'/g, '&#39;');
136+
}

0 commit comments

Comments
 (0)