alert()
+One message, one button. Resolves when it closes.
+await alert('This is an alert dialog.', { title: 'Heads up' });
+ diff --git a/README.md b/README.md index aebb226..9852314 100644 --- a/README.md +++ b/README.md @@ -271,10 +271,11 @@ tooling and SSR imports. ## Website -`site/` holds a dependency-free landing page with an interactive appearance playground, plus a -full API reference at `site/docs.html`. Build and serve both with `npm run site`. The icon, the -generated `sitemap.xml` / `robots.txt` / `llms.txt`, the structured data, and the Cloudflare -deployment settings are covered in [docs/website.md](docs/website.md). +`site/` holds a dependency-free landing page with an interactive appearance playground, a live +demo page that runs every surface the library ships, and a full API reference — each published in +English and Vietnamese. Build and serve them with `npm run site`. The icon, the generated `sitemap.xml` / `robots.txt` / `llms.txt`, the +structured data, and the Cloudflare deployment settings are covered in +[docs/website.md](docs/website.md). ## Contributing diff --git a/docs/website.md b/docs/website.md index d4de9c7..c776624 100644 --- a/docs/website.md +++ b/docs/website.md @@ -21,6 +21,7 @@ CDN, so what you see is always the build you have. | File | Role | | ----------------- | --------------------------------------------------------------------- | | `site/index.html` | The landing page: demos, appearance playground, facts table, and FAQ. | +| `site/demo.html` | Live demos: every surface running, each beside its code. | | `site/docs.html` | The API reference, written against the reviewed report in `etc/`. | | `site/404.html` | Served for any unmatched path. Carries `noindex`. | | `site/styles.css` | Site styling. Its dark mode reads the same `data-fd-theme` attribute | @@ -28,13 +29,15 @@ CDN, so what you see is always the build you have. | `site/common.js` | Shared chrome: version badge, theme toggle, copy buttons. | | `site/site.js` | Landing-page demos and the appearance playground. | | `site/docs.js` | Highlights the sidebar entry for the section being read. | -| `site/icon.svg` | The mark, and the favicon browsers actually load. | +| `site/demo.js` | Runs the demos. One delegated listener, no inline handlers. | +| `site/icon.svg` | The mark: the favicon, and the logo in every page header. | | `wrangler.jsonc` | Cloudflare deployment config. Points at `site/`, not `dist/`. | | `site/_headers` | Cloudflare response headers, including a strict CSP. | ### URLs -The docs page is served at **`/docs`**, not `/docs.html`. Both Cloudflare's static-asset routing +The demo and docs pages are served at **`/demo`** and **`/docs`**, not `/demo.html` and +`/docs.html`, and their Vietnamese counterparts at `/vi/demo` and `/vi/docs`. Both Cloudflare's static-asset routing (`html_handling: "auto-trailing-slash"`, the default) and the `serve` used by `npm run site` drop the `.html` and redirect the extension form, so the extensionless URL is the one that answers with a `200`. Every link, canonical tag, sitemap entry, and `llms.txt` link uses it — a canonical or a @@ -53,15 +56,37 @@ refers to it. The npm package (`forgedialog`), the browser global (`ForgeDialog` element (`forge-dialog`), and the repository name stay as they are: they are identifiers, not prose. +## The demo page + +`site/demo.html` runs every surface the library ships — the four dialog entry points, `open()`, +stacking, animations, drawers, bottom sheets, toasts, the lightbox, loading states, the command +palette, the notification centre, `form()`, `wizard()`, and `formWizard()` — against the same +vendored build the rest of the site uses. + +Each card pairs a control that really runs with the snippet of the call being made, so the page +cannot drift from what it documents: if a snippet were wrong, the button beside it would misbehave. +A plugin registered on the page logs `afterOpen` and `afterClose`, and every demo prints its return +value, so the result log doubles as evidence that the hooks fire. + +`demo.js` uses one delegated listener on `document` keyed by `data-demo`, because the site's CSP is +`script-src 'self'` and inline `onclick` attributes would be blocked. A demo that throws prints the +failure into the log rather than only the console. + +This is separate from `demo/index.html` at the repository root, which is the manual verification +page the visual regression test drives. That one loads `../dist/` directly and is not deployed. + ## Icons -`site/icon.svg` is the source of truth and is what browsers load as the favicon. Two bitmaps are -committed beside it because SVG is not enough everywhere: +`site/icon.svg` is the source of truth. It is what browsers load as the favicon and what every page +header shows beside the wordmark — with `alt=""`, since the adjacent text already names the product +and a screen reader should not announce it twice. Two bitmaps are committed beside it because SVG +is not enough everywhere: -| File | Size | Used by | -| --------------------------- | -------- | ------------------------------ | -| `site/apple-touch-icon.png` | 180×180 | iOS home screens | -| `site/og-image.png` | 1200×630 | Link previews and social cards | +| File | Size | Used by | +| --------------------------- | -------- | ---------------------------------------------- | +| `site/favicon.ico` | 32×32 | Clients that request `/favicon.ico` regardless | +| `site/apple-touch-icon.png` | 180×180 | iOS home screens | +| `site/og-image.png` | 1200×630 | Link previews and social cards | Both are rendered from `icon.svg` through Chromium, which is already a dev dependency: @@ -71,7 +96,70 @@ npm run site:icons Run it only when the icon changes; neither the site build nor a deploy runs it. The apple-touch variant squares off the mark's ground (via `#ground { rx: 0 }`) because iOS applies its own rounded -mask and would otherwise round it twice. +mask and would otherwise round it twice. The `.ico` is a 32×32 PNG wrapped in an ICO container, +which every browser since Vista accepts and which needs no bitmap encoder — the whole file is about +1.2 KB. + +## Vietnamese + +All three pages are published in Vietnamese under `/vi/`, as translations of the English pages +rather than different content. `site/vi/index.html`, `site/vi/demo.html`, and `site/vi/docs.html` +mirror their English counterparts section for section, keeping the same element ids so +`site.js`, `demo.js`, and `docs.js` drive both languages without a second copy. + +### How the strings work + +Those three scripts are shared, so nothing user-visible is hard-coded in them. Each page carries a +JSON data block, and the script reads it with English as the fallback: + +```html + +``` + +A data block is not an executable script, so `script-src 'self'` allows it — an inline ` + +
++ Every surface Forge Dialog ships, running in this page against the same build the rest of + the site uses. Each card shows the code that produced what you just saw, so nothing here + is a mock-up. +
+
+ The four entry points that replace the browser built-ins, plus the generic
+ open() they are all built on.
+
One message, one button. Resolves when it closes.
+await alert('This is an alert dialog.', { title: 'Heads up' });
+ Resolves true or false rather than blocking the page.
const ok = await confirm('Are you sure you want to continue?', {
+ title: 'Please confirm',
+});
+ Resolves the typed string, or null when dismissed.
const name = await prompt('What is your name?', {
+ title: 'Introduce yourself',
+ defaultValue: 'Ada Lovelace',
+});
+
+ validate returns true or an error string, and the dialog
+ stays open until it passes.
+
await prompt('Enter an even number:', {
+ title: 'Validated prompt',
+ validate: (value) => {
+ const n = Number(value);
+ if (Number.isNaN(n)) return 'Please enter a number';
+ if (n % 2 !== 0) return 'Number must be even';
+ return true;
+ },
+});
+ Build the body yourself and decide what each button resolves with.
+const instance = open({
+ title: 'Custom dialog',
+ content: (container) => {
+ const p = document.createElement('p');
+ p.textContent = 'Built with the generic open() API.';
+ container.append(p);
+ },
+ buttons: [
+ { text: 'Close', role: 'primary', autoFocus: true, onClick: (i) => i.close('done') },
+ ],
+});
+const result = await instance.whenClosed();
+ + Open a second dialog over the first. Focus, Escape, and scroll locking follow the + top of the stack. +
+const first = confirm('First dialog. Open a second one on top?', { title: 'Dialog 1' });
+const second = await confirm('This is stacked above the first dialog.', { title: 'Dialog 2' });
+const firstResult = await first;
+
+ Set per dialog. Every animation honours prefers-reduced-motion.
+
Opens and closes instantly.
+await alert('This alert opens and closes instantly.', { animation: 'none' });
+ Slides in from below instead of scaling.
+await confirm('This dialog slides in from below.', { animation: 'slide' });
+
+ The same core, presented differently. All of these live behind
+ forgedialog/presentation.
+
A side panel with the same focus handling as a modal.
+drawer({
+ title: 'Project settings',
+ message: 'A native, focus-safe drawer.',
+ side: 'right',
+ buttons: [{ text: 'Done', role: 'primary', closesDialog: true }],
+});
+ The mobile shape. Drag it down to dismiss.
+bottomSheet({
+ title: 'Quick actions',
+ message: 'Drag down to dismiss this sheet.',
+ buttons: [{ text: 'Done', role: 'primary', closesDialog: true }],
+});
+ Announced politely, dismissed on its own. Four tones.
+toast('Changes saved successfully.', { tone: 'success' });
+ An image at full size, with a caption and the same dismissal rules.
+lightbox('/og-image.png', {
+ alt: 'The Forge Dialog social card',
+ caption: 'Rendered from site/icon.svg at build time.',
+});
+
+ A blocking state you control. update() changes the message,
+ close() ends it.
+
const task = loading('Uploading…');
+task.update('Processing…');
+await task.close();
+ Filterable commands with keyboard navigation. Type to narrow.
+commandPalette([
+ { id: 'theme-dark', label: 'Switch to dark theme', keywords: ['appearance'],
+ run: () => setTheme('dark') },
+ { id: 'theme-light', label: 'Switch to light theme', keywords: ['appearance'],
+ run: () => setTheme('light') },
+]);
+ + Every toast raised in this page, kept in history. Raise a few toasts first, then + open it. +
+notificationCenter();
+getNotificationHistory(); // [{ id, message, tone, createdAt }]
+clearNotificationHistory();
+
+ Behind forgedialog/workflows. In TypeScript the field names are inferred
+ into the result type, so the values come back typed without a cast.
+
A validated form from a field list. Resolves the values, or null.
const values = await form(
+ [
+ { name: 'email', type: 'email', label: 'Email', required: true },
+ { name: 'plan', type: 'select', label: 'Plan', options: [
+ { value: 'free', label: 'Free' },
+ { value: 'pro', label: 'Pro' },
+ ] },
+ { name: 'notes', type: 'textarea', label: 'Notes', rows: 3 },
+ ],
+ { title: 'Create an account' },
+);
+ Multi-step, with per-step validation and shared data.
+const flow = wizard({
+ initialData: { name: '' },
+ steps: [
+ {
+ id: 'profile',
+ title: 'Your profile',
+ render: (el, ctx) => {
+ const input = document.createElement('input');
+ input.className = 'fd-input';
+ input.placeholder = 'Name';
+ input.addEventListener('input', () => ctx.set({ name: input.value }));
+ el.append(input);
+ },
+ validate: (ctx) => (ctx.data.name ? true : 'Please enter your name.'),
+ },
+ { id: 'review', title: 'Review',
+ render: (el, ctx) => { el.textContent = `Ready to welcome ${ctx.data.name}.`; } },
+ ],
+});
+const data = await flow.result;
+ The same field list as form(), spread over steps.
const values = await formWizard([
+ { id: 'account', title: 'Account', fields: [
+ { name: 'email', type: 'email', label: 'Email', required: true },
+ ] },
+ { id: 'profile', title: 'Profile', fields: [
+ { name: 'name', type: 'text', label: 'Full name', required: true },
+ ] },
+]);
+
+ setTheme() drives the library and this page together;
+ setThemePreset() swaps the whole look.
+
Light, dark, or follow the operating system.
+setTheme('dark'); // 'light' | 'dark' | 'system'
+ Pick one, then open any demo above to see it applied.
+setThemePreset('glass');
+ + Per-dialog colour, radius, shadow direction, hover, and drag bounds have their own + interactive builder on the landing page. +
+ +open({
+ title: 'Styled dialog',
+ appearance: { radius: 20, shadow: { angle: 180, distance: 20, blur: 60 } },
+ draggable: { bounds: 'viewport' },
+});
+
+ Every demo above prints its result here, along with the plugin lifecycle hooks it fired
+ — afterOpen and afterClose from a plugin registered on this
+ page.
+