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 ` + + + + +
+
+

Live demos

+

+ 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. +

+
+ +
+
+

Dialogs

+

+ The four entry points that replace the browser built-ins, plus the generic + open() they are all built on. +

+ +
+
+

alert()

+

One message, one button. Resolves when it closes.

+
+ +
+
+ +
await alert('This is an alert dialog.', { title: 'Heads up' });
+
+
+ +
+

confirm()

+

Resolves true or false rather than blocking the page.

+
+ +
+
+ +
const ok = await confirm('Are you sure you want to continue?', {
+  title: 'Please confirm',
+});
+
+
+ +
+

prompt()

+

Resolves the typed string, or null when dismissed.

+
+ +
+
+ +
const name = await prompt('What is your name?', {
+  title: 'Introduce yourself',
+  defaultValue: 'Ada Lovelace',
+});
+
+
+ +
+

Validated prompt

+

+ 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;
+  },
+});
+
+
+ +
+

open()

+

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();
+
+
+ +
+

Stacking

+

+ 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;
+
+
+
+
+
+ +
+
+

Animations

+

+ Set per dialog. Every animation honours prefers-reduced-motion. +

+ +
+
+

No animation

+

Opens and closes instantly.

+
+ +
+
+ +
await alert('This alert opens and closes instantly.', { animation: 'none' });
+
+
+ +
+

Slide

+

Slides in from below instead of scaling.

+
+ +
+
+ +
await confirm('This dialog slides in from below.', { animation: 'slide' });
+
+
+
+
+
+ +
+
+

Beyond dialogs

+

+ The same core, presented differently. All of these live behind + forgedialog/presentation. +

+ +
+
+

Drawer

+

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 }],
+});
+
+
+ +
+

Bottom sheet

+

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 }],
+});
+
+
+ +
+

Toast

+

Announced politely, dismissed on its own. Four tones.

+
+ + + +
+
+ +
toast('Changes saved successfully.', { tone: 'success' });
+
+
+ +
+

Lightbox

+

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.',
+});
+
+
+ +
+

Loading

+

+ A blocking state you control. update() changes the message, + close() ends it. +

+
+ +
+
+ +
const task = loading('Uploading…');
+task.update('Processing…');
+await task.close();
+
+
+ +
+

Command palette

+

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') },
+]);
+
+
+ +
+

Notification centre

+

+ Every toast raised in this page, kept in history. Raise a few toasts first, then + open it. +

+
+ + +
+
+ +
notificationCenter();
+getNotificationHistory(); // [{ id, message, tone, createdAt }]
+clearNotificationHistory();
+
+
+
+
+
+ +
+
+

Forms and wizards

+

+ Behind forgedialog/workflows. In TypeScript the field names are inferred + into the result type, so the values come back typed without a cast. +

+ +
+
+

form()

+

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' },
+);
+
+
+ +
+

wizard()

+

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;
+
+
+ +
+

formWizard()

+

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 },
+  ] },
+]);
+
+
+
+
+
+ +
+
+

Theming

+

+ setTheme() drives the library and this page together; + setThemePreset() swaps the whole look. +

+ +
+
+

Theme

+

Light, dark, or follow the operating system.

+
+ + + +
+
+ +
setTheme('dark'); // 'light' | 'dark' | 'system'
+
+
+ +
+

Presets

+

Pick one, then open any demo above to see it applied.

+
+ + + + +
+
+ +
setThemePreset('glass');
+
+
+ +
+

Appearance and dragging

+

+ 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' },
+});
+
+
+
+
+
+ +
+
+

What each demo returned

+

+ 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. +

+

+        
+
+
+ + + + + + + + diff --git a/site/demo.js b/site/demo.js new file mode 100644 index 0000000..1b2b634 --- /dev/null +++ b/site/demo.js @@ -0,0 +1,332 @@ +/* Demo-page behaviour. Every card's button really calls the library, and the + snippet beside it is the call being made, so the page cannot drift from what + it documents. The shared chrome (version badge, theme toggle, copy buttons) + lives in common.js. */ +(() => { + const { + alert, + confirm, + prompt, + open, + drawer, + bottomSheet, + toast, + lightbox, + loading, + commandPalette, + notificationCenter, + getNotificationHistory, + clearNotificationHistory, + form, + formWizard, + wizard, + setTheme, + setThemePreset, + registerPlugin, + } = ForgeDialog; + + // Localisable strings come from a JSON data block in the page — a data + // block, not an executable script, so `script-src 'self'` allows it. The + // Vietnamese page supplies its own set; English is the fallback. + const strings = JSON.parse(document.getElementById('fd-strings')?.textContent ?? '{}'); + const t = (key, fallback) => strings[key] ?? fallback; + + const logEl = document.getElementById('demo-log'); + + function print(label, value) { + const time = new Date().toLocaleTimeString(); + const text = value === undefined ? '' : `: ${JSON.stringify(value)}`; + logEl.textContent = `[${time}] ${label}${text}\n${logEl.textContent}`; + } + + // Lifecycle hooks are easiest to believe when you can watch them fire. + registerPlugin({ + name: 'site-demo-logger', + hooks: { + afterOpen: (ctx) => print('hook:afterOpen', ctx.instance.id), + afterClose: (ctx) => print('hook:afterClose', ctx.instance.id), + }, + }); + + const demos = { + async alert() { + await alert(t('alertBody', 'This is an alert dialog.'), { + title: t('alertTitle', 'Heads up'), + }); + print('alert()', 'closed'); + }, + + async confirm() { + print( + 'confirm()', + await confirm(t('confirmBody', 'Are you sure you want to continue?'), { + title: t('confirmTitle', 'Please confirm'), + }), + ); + }, + + async prompt() { + print( + 'prompt()', + await prompt(t('promptBody', 'What is your name?'), { + title: t('promptTitle', 'Introduce yourself'), + defaultValue: 'Ada Lovelace', + }), + ); + }, + + async 'prompt-validate'() { + print( + 'prompt() validated', + await prompt(t('validateBody', 'Enter an even number:'), { + title: t('validateTitle', 'Validated prompt'), + validate: (value) => { + const n = Number(value); + if (Number.isNaN(n)) return t('validateNaN', 'Please enter a number'); + if (n % 2 !== 0) return t('validateOdd', 'Number must be even'); + return true; + }, + }), + ); + }, + + async open() { + const instance = open({ + title: t('openTitle', 'Custom dialog'), + content: (container) => { + const p = document.createElement('p'); + p.textContent = t('openBody', 'This dialog was built with the generic open() API.'); + container.append(p); + }, + buttons: [ + { + text: t('close', 'Close'), + role: 'primary', + autoFocus: true, + onClick: (i) => i.close('done'), + }, + ], + }); + print('open()', await instance.whenClosed()); + }, + + async stacked() { + const first = confirm(t('stackedFirst', 'First dialog. Open a second one on top?'), { + title: t('stackedFirstTitle', 'Dialog 1'), + }); + print( + 'stacked: second', + await confirm(t('stackedSecond', 'This is stacked above the first dialog.'), { + title: t('stackedSecondTitle', 'Dialog 2'), + }), + ); + print('stacked: first', await first); + }, + + async 'no-animation'() { + await alert(t('noAnimBody', 'This alert opens and closes instantly.'), { animation: 'none' }); + print('alert() animation:none', 'closed'); + }, + + async slide() { + print( + 'confirm() animation:slide', + await confirm(t('slideBody', 'This dialog slides in from below.'), { + animation: 'slide', + }), + ); + }, + + drawer() { + drawer({ + title: t('drawerTitle', 'Project settings'), + message: t('drawerBody', 'A native, focus-safe right drawer.'), + side: 'right', + buttons: [{ text: t('done', 'Done'), role: 'primary', closesDialog: true }], + }); + }, + + 'drawer-left'() { + drawer({ + title: t('drawerLeftTitle', 'Navigation'), + message: t('drawerLeftBody', 'The same drawer, anchored to the left.'), + side: 'left', + buttons: [{ text: t('done', 'Done'), role: 'primary', closesDialog: true }], + }); + }, + + sheet() { + bottomSheet({ + title: t('sheetTitle', 'Quick actions'), + message: t('sheetBody', 'Drag down to dismiss this sheet.'), + buttons: [{ text: t('done', 'Done'), role: 'primary', closesDialog: true }], + }); + }, + + 'toast-success'() { + toast(t('toastSuccess', 'Changes saved successfully.'), { tone: 'success' }); + }, + + 'toast-error'() { + toast(t('toastError', 'That upload failed. Nothing was lost.'), { tone: 'error' }); + }, + + 'toast-action'() { + toast(t('toastAction', 'Message archived.'), { + tone: 'info', + actions: [{ text: t('undo', 'Undo'), onClick: () => print('toast action', 'undo') }], + }); + }, + + lightbox() { + lightbox('./og-image.png', { + alt: t('lightboxAlt', 'The Forge Dialog social card'), + caption: t('lightboxCaption', 'Rendered from site/icon.svg at build time.'), + }); + }, + + async loading() { + const task = loading(t('loadingUploading', 'Uploading…')); + print('loading()', 'opened'); + await new Promise((resolve) => setTimeout(resolve, 900)); + task.update(t('loadingProcessing', 'Processing…')); + await new Promise((resolve) => setTimeout(resolve, 900)); + await task.close(); + print('loading()', 'closed'); + }, + + commands() { + commandPalette([ + { + id: 'theme-dark', + label: t('cmdDark', 'Switch to dark theme'), + keywords: ['appearance'], + run: () => setTheme('dark'), + }, + { + id: 'theme-light', + label: t('cmdLight', 'Switch to light theme'), + keywords: ['appearance'], + run: () => setTheme('light'), + }, + { + id: 'preset-glass', + label: t('cmdGlass', 'Use the glass preset'), + keywords: ['theme', 'appearance'], + run: () => setThemePreset('glass'), + }, + ]); + }, + + notifications() { + print('getNotificationHistory()', getNotificationHistory().length); + notificationCenter(); + }, + + 'notifications-clear'() { + clearNotificationHistory(); + print('clearNotificationHistory()', 'history emptied'); + }, + + async form() { + print( + 'form()', + await form( + [ + { name: 'email', type: 'email', label: t('fieldEmail', 'Email'), required: true }, + { + name: 'plan', + type: 'select', + label: t('fieldPlan', 'Plan'), + options: [ + { value: 'free', label: t('planFree', 'Free') }, + { value: 'pro', label: t('planPro', 'Pro') }, + ], + }, + { name: 'notes', type: 'textarea', label: t('fieldNotes', 'Notes'), rows: 3 }, + ], + { title: t('formTitle', 'Create an account') }, + ), + ); + }, + + async wizard() { + const flow = wizard({ + initialData: { name: '' }, + steps: [ + { + id: 'profile', + title: t('wizardStep1', 'Your profile'), + render: (el, ctx) => { + const input = document.createElement('input'); + input.className = 'fd-input'; + input.placeholder = t('fieldName', 'Name'); + input.addEventListener('input', () => ctx.set({ name: input.value })); + el.append(input); + }, + validate: (ctx) => + ctx.data.name ? true : t('wizardRequired', 'Please enter your name.'), + }, + { + id: 'review', + title: t('wizardStep2', 'Review'), + render: (el, ctx) => { + el.textContent = `${t('wizardReview', 'Ready to welcome')} ${ctx.data.name}.`; + }, + }, + ], + }); + print('wizard()', await flow.result); + }, + + async 'form-wizard'() { + print( + 'formWizard()', + await formWizard([ + { + id: 'account', + title: t('fwStep1', 'Account'), + fields: [ + { name: 'email', type: 'email', label: t('fieldEmail', 'Email'), required: true }, + ], + }, + { + id: 'profile', + title: t('fwStep2', 'Profile'), + fields: [ + { + name: 'name', + type: 'text', + label: t('fieldFullName', 'Full name'), + required: true, + }, + ], + }, + ]), + ); + }, + }; + + document.addEventListener('click', (event) => { + const trigger = event.target.closest('[data-demo], [data-theme], [data-preset]'); + if (!trigger) return; + + if (trigger.dataset.theme) { + setTheme(trigger.dataset.theme); + print('setTheme()', trigger.dataset.theme); + return; + } + if (trigger.dataset.preset) { + setThemePreset(trigger.dataset.preset); + print('setThemePreset()', trigger.dataset.preset); + return; + } + + const run = demos[trigger.dataset.demo]; + // A demo that threw should say so in the log rather than only in the + // console, since the log is the page's own record of what happened. + Promise.resolve() + .then(run) + .catch((error) => print(`${trigger.dataset.demo} failed`, String(error))); + }); +})(); diff --git a/site/docs.html b/site/docs.html index 94b3587..e4d2532 100644 --- a/site/docs.html +++ b/site/docs.html @@ -8,6 +8,8 @@ name="description" content="Complete API reference for Forge Dialog: alert, confirm, prompt, open, appearance and shadow options, dragging, drawers, forms, wizards, toasts, theming, plugins, and framework adapters." /> + + - + + + + + + + + + + + @@ -75,13 +90,19 @@ diff --git a/site/favicon.ico b/site/favicon.ico new file mode 100644 index 0000000..382522e Binary files /dev/null and b/site/favicon.ico differ diff --git a/site/index.html b/site/index.html index 106979f..4dfbe95 100644 --- a/site/index.html +++ b/site/index.html @@ -8,6 +8,8 @@ name="description" content="A modern, lightweight, dependency-free TypeScript dialog library: alerts, confirmations, prompts, drawers, wizards, and fully customizable appearance." /> + + - + + + + + + + + + + + @@ -38,13 +53,22 @@ { "@context": "https://schema.org", "@graph": [ + { + "@type": "Organization", + "@id": "https://konexforge.com/#organization", + "name": "KonexForge", + "url": "https://konexforge.com/" + }, { "@type": "WebSite", "@id": "https://forgedialog.konexforge.com/#website", "url": "https://forgedialog.konexforge.com/", "name": "Forge Dialog", "description": "Accessible, dependency-free dialogs for the web.", - "inLanguage": "en" + "inLanguage": "en", + "publisher": { + "@id": "https://konexforge.com/#organization" + } }, { "@type": "SoftwareApplication", @@ -55,6 +79,9 @@ "description": "Forge Dialog is an open-source TypeScript library for building dialogs on the web: alerts, confirmations, prompts, drawers, bottom sheets, lightboxes, toasts, command palettes, and multi-step wizards. It has no runtime dependencies and is published on npm as forgedialog.", "url": "https://forgedialog.konexforge.com/", "codeRepository": "https://github.com/cmm-cmm/ForgeDialog", + "author": { + "@id": "https://konexforge.com/#organization" + }, "programmingLanguage": "TypeScript", "operatingSystem": "Any (web browser)", "license": "https://opensource.org/licenses/MIT", @@ -137,6 +164,14 @@ "@type": "Answer", "text": "Yes. Forge Dialog is released under the MIT license, which permits commercial use, modification, and redistribution." } + }, + { + "@type": "Question", + "name": "How does Forge Dialog compare to SweetAlert2 and Bootstrap modals?", + "acceptedAnswer": { + "@type": "Answer", + "text": "Forge Dialog builds on the browser's native dialog element, so focus trapping, the top layer, and inert backgrounds come from the platform rather than from JavaScript. It has no runtime dependencies, where a Bootstrap modal needs Bootstrap's CSS and JavaScript. Every entry point is tree-shakable, so an application that only needs an alert ships about 5.5 KB gzipped rather than the whole library. It is written in TypeScript and ships types for both ESM and CommonJS." + } } ] } @@ -148,6 +183,7 @@
+

+ Open source · MIT licensed · Zero runtime dependencies +

Dialogs that behave themselves.

Forge Dialog is a dependency-free TypeScript dialog library built on the @@ -514,6 +558,16 @@

Is Forge Dialog free to use commercially?

Yes. Forge Dialog is released under the MIT license, which permits commercial use, modification, and redistribution.

+ +

How does Forge Dialog compare to SweetAlert2 and Bootstrap modals?

+

+ Forge Dialog builds on the browser's native <dialog> element, + so focus trapping, the top layer, and inert backgrounds come from the platform rather + than from JavaScript. It has no runtime dependencies, where a Bootstrap modal needs + Bootstrap's CSS and JavaScript. Every entry point is tree-shakable, so an + application that only needs an alert ships about 5.5 KB gzipped rather than the + whole library. It is written in TypeScript and ships types for both ESM and CommonJS. +

diff --git a/site/site.js b/site/site.js index 6d01b06..252448f 100644 --- a/site/site.js +++ b/site/site.js @@ -6,28 +6,45 @@ const { alert, confirm, prompt, open } = ForgeDialog; const $ = (selector) => document.querySelector(selector); + // Every string this file can put on screen comes from a JSON data block in + // the page, so the Vietnamese page localises them without a second copy of + // this script. A data block is not an executable script, so `script-src + // 'self'` does not block it. English is the fallback. + const strings = JSON.parse($('#fd-strings')?.textContent ?? '{}'); + const t = (key, fallback) => strings[key] ?? fallback; + /* Hero demos ----------------------------------------------------------- */ $('#try-alert').addEventListener('click', () => { - void alert('Focus is trapped here, and returns to the button when this closes.', { - title: 'Heads up', - }); + void alert( + t('alertBody', 'Focus is trapped here, and returns to the button when this closes.'), + { + title: t('alertTitle', 'Heads up'), + }, + ); }); $('#try-confirm').addEventListener('click', async () => { - const result = await confirm('Escape and the backdrop both resolve this to false.', { - title: 'Please confirm', + const result = await confirm( + t('confirmBody', 'Escape and the backdrop both resolve this to false.'), + { title: t('confirmTitle', 'Please confirm') }, + ); + void alert(`${t('confirmResult', 'confirm() resolved')} ${result}.`, { + title: t('resultTitle', 'Result'), }); - void alert(`confirm() resolved ${result}.`, { title: 'Result' }); }); $('#try-prompt').addEventListener('click', async () => { - const value = await prompt('Try submitting it empty.', { - title: 'What should we call you?', + const value = await prompt(t('promptBody', 'Try submitting it empty.'), { + title: t('promptTitle', 'What should we call you?'), defaultValue: 'Ada Lovelace', - validate: (input) => (input.trim() ? true : 'A name is required'), + validate: (input) => (input.trim() ? true : t('promptRequired', 'A name is required')), }); - if (value !== null) void alert(`Nice to meet you, ${value}.`, { title: 'Result' }); + if (value !== null) { + void alert(`${t('promptGreeting', 'Nice to meet you,')} ${value}.`, { + title: t('resultTitle', 'Result'), + }); + } }); $('#try-wizard').addEventListener('click', () => openPlayground()); @@ -84,12 +101,14 @@ } return { - title: 'Styled with the appearance API', - message: + title: t('playgroundTitle', 'Styled with the appearance API'), + message: t( + 'playgroundBody', 'Every colour, the corner radius, and the shadow direction come from the controls on the left. Drag me by the header.', + ), appearance, draggable: controls.draggable.checked ? { bounds: 'viewport' } : false, - buttons: [{ text: 'Close', role: 'primary', closesDialog: true }], + buttons: [{ text: t('closeButton', 'Close'), role: 'primary', closesDialog: true }], }; } diff --git a/site/styles.css b/site/styles.css index bbdf92c..3ff8101 100644 --- a/site/styles.css +++ b/site/styles.css @@ -99,11 +99,20 @@ a { flex: 1 0 100%; margin-inline-end: 0; } + + /* The mark carries no information the adjacent wordmark does not, so it is + decorative: alt="" keeps a screen reader from announcing the name twice. */ + .brand-mark { + width: 26px; + height: 26px; + border-radius: 6px; + flex: none; + } } .brand { display: flex; - align-items: baseline; + align-items: center; gap: 8px; font-weight: 700; font-size: 1.05rem; @@ -178,6 +187,15 @@ a { text-align: center; } +.hero-eyebrow { + margin: 0 0 12px; + font-size: 0.82rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--muted); +} + .hero h1 { font-size: clamp(2.1rem, 6vw, 3.3rem); line-height: 1.12; @@ -638,3 +656,92 @@ pre code { font-size: 0.88rem; color: var(--text); } + +/* Demo page. Each card pairs a control that really runs with the code behind + it, so the page doubles as documentation for what it demonstrates. */ +.demos { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr)); + gap: 20px; +} + +.demo-card { + display: flex; + flex-direction: column; + gap: 12px; + min-width: 0; + padding: 20px; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--surface); +} + +.demo-card h3 { + margin: 0; + font-size: 1rem; +} + +.demo-card p { + margin: 0; + color: var(--muted); + font-size: 0.92rem; +} + +.demo-card .snippet { + margin-top: auto; +} + +.demo-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.demo-log { + margin: 0; + padding: 16px; + border-radius: 10px; + border: 1px solid var(--border); + background: var(--code-bg); + color: var(--code-text); + font: 0.86rem/1.65 var(--mono); + white-space: pre-wrap; + overflow-x: auto; + min-height: 120px; + max-height: 320px; + overflow-y: auto; +} + +.demo-log:empty::after { + content: 'Results from the demos above appear here.'; + opacity: 0.55; +} + +/* Language switcher. Two links, not a select: a crawler follows a link. */ +.lang-switch { + display: inline-flex; + align-items: center; + gap: 2px; + padding: 2px; + border: 1px solid var(--border); + border-radius: 8px; + font-size: 0.78rem; + font-weight: 600; +} + +.lang-switch a { + padding: 3px 8px; + border-radius: 6px; + color: var(--muted); + text-decoration: none; +} + +.lang-switch a:hover { + color: var(--text); + background: var(--surface-2); +} + +.lang-switch a.active { + color: var(--accent-text); + background: var(--accent); +} diff --git a/site/vi/demo.html b/site/vi/demo.html new file mode 100644 index 0000000..eea3b29 --- /dev/null +++ b/site/vi/demo.html @@ -0,0 +1,792 @@ + + + + + + Demo trực tiếp — Forge Dialog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

Demo trực tiếp

+

+ Mọi thành phần Forge Dialog cung cấp, chạy thật trong trang này trên đúng bản build mà + phần còn lại của site đang dùng. Mỗi thẻ hiển thị đoạn mã tạo ra thứ bạn vừa thấy, nên + không có gì ở đây là mô phỏng. +

+
+ +
+
+

Hộp thoại

+

+ Bốn điểm vào thay thế hàm dựng sẵn của trình duyệt, cùng hàm tổng quát + open() mà tất cả đều dựng trên đó. +

+ +
+
+

alert()

+

Một thông điệp, một nút. Trả về khi đóng.

+
+ +
+
+ +
await alert('Đây là một hộp thoại cảnh báo.', { title: 'Lưu ý' });
+
+
+ +
+

confirm()

+

Trả về true hoặc false thay vì chặn cả trang.

+
+ +
+
+ +
const ok = await confirm('Bạn có chắc muốn tiếp tục không?', {
+  title: 'Xin xác nhận',
+});
+
+
+ +
+

prompt()

+

Trả về chuỗi đã nhập, hoặc null khi bị bỏ qua.

+
+ +
+
+ +
const name = await prompt('Tên bạn là gì?', {
+  title: 'Giới thiệu bản thân',
+  defaultValue: 'Ada Lovelace',
+});
+
+
+ +
+

Nhập liệu có kiểm tra

+

+ validate trả về true hoặc một chuỗi lỗi, và hộp thoại vẫn + mở cho tới khi hợp lệ. +

+
+ +
+
+ +
await prompt('Nhập một số chẵn:', {
+  title: 'Nhập liệu có kiểm tra',
+  validate: (value) => {
+    const n = Number(value);
+    if (Number.isNaN(n)) return 'Vui lòng nhập một số';
+    if (n % 2 !== 0) return 'Số phải là số chẵn';
+    return true;
+  },
+});
+
+
+ +
+

open()

+

Tự dựng phần thân và quyết định mỗi nút trả về gì.

+
+ +
+
+ +
const instance = open({
+  title: 'Hộp thoại tự dựng',
+  content: (container) => {
+    const p = document.createElement('p');
+    p.textContent = 'Hộp thoại này được dựng bằng hàm tổng quát open().';
+    container.append(p);
+  },
+  buttons: [
+    { text: 'Đóng', role: 'primary', autoFocus: true, onClick: (i) => i.close('done') },
+  ],
+});
+const result = await instance.whenClosed();
+
+
+ +
+

Xếp chồng

+

+ Mở hộp thoại thứ hai đè lên cái đầu. Tiêu điểm, Escape, và khoá cuộn đều theo cái + trên cùng của chồng. +

+
+ +
+
+ +
const first = confirm('Hộp thoại thứ nhất. Mở thêm một cái đè lên nhé?', { title: 'Hộp thoại 1' });
+const second = await confirm('Cái này xếp chồng lên hộp thoại thứ nhất.', { title: 'Hộp thoại 2' });
+const firstResult = await first;
+
+
+
+
+
+ +
+
+

Hiệu ứng

+

+ Đặt riêng cho từng hộp thoại. Mọi hiệu ứng đều tôn trọng + prefers-reduced-motion. +

+ +
+
+

Không hiệu ứng

+

Mở và đóng tức thì.

+
+ +
+
+ +
await alert('Hộp thoại này mở và đóng tức thì.', { animation: 'none' });
+
+
+ +
+

Trượt lên

+

Trượt lên từ dưới thay vì phóng to.

+
+ +
+
+ +
await confirm('Hộp thoại này trượt lên từ phía dưới.', { animation: 'slide' });
+
+
+
+
+
+ +
+
+

Ngoài hộp thoại

+

+ Cùng một lõi, trình bày khác đi. Tất cả nằm sau + forgedialog/presentation. +

+ +
+
+

Drawer

+

Bảng bên hông, xử lý tiêu điểm giống hệt hộp thoại modal.

+
+ + +
+
+ +
drawer({
+  title: 'Cài đặt dự án',
+  message: 'Drawer bên phải, dùng dialog gốc và an toàn về tiêu điểm.',
+  side: 'right',
+  buttons: [{ text: 'Xong', role: 'primary', closesDialog: true }],
+});
+
+
+ +
+

Bottom sheet

+

Dạng cho di động. Kéo xuống để đóng.

+
+ +
+
+ +
bottomSheet({
+  title: 'Thao tác nhanh',
+  message: 'Kéo xuống để đóng bottom sheet này.',
+  buttons: [{ text: 'Xong', role: 'primary', closesDialog: true }],
+});
+
+
+ +
+

Toast

+

Thông báo lịch sự, tự biến mất. Bốn sắc thái.

+
+ + + +
+
+ +
toast('Đã lưu thay đổi thành công.', { tone: 'success' });
+
+
+ +
+

Lightbox

+

Ảnh cỡ đầy đủ, kèm chú thích và cùng cách đóng như hộp thoại.

+
+ +
+
+ +
lightbox('/og-image.png', {
+  alt: 'Ảnh chia sẻ mạng xã hội của Forge Dialog',
+  caption: 'Kết xuất từ site/icon.svg lúc build.',
+});
+
+
+ +
+

Trạng thái tải

+

+ Trạng thái chặn do bạn điều khiển. update() đổi thông điệp, + close() kết thúc nó. +

+
+ +
+
+ +
const task = loading('Đang tải lên…');
+task.update('Đang xử lý…');
+await task.close();
+
+
+ +
+

Bảng lệnh

+

Danh sách lệnh lọc được, điều hướng bằng bàn phím. Gõ để thu hẹp.

+
+ +
+
+ +
commandPalette([
+  { id: 'theme-dark', label: 'Chuyển sang chủ đề tối', keywords: ['appearance'],
+    run: () => setTheme('dark') },
+  { id: 'theme-light', label: 'Chuyển sang chủ đề sáng', keywords: ['appearance'],
+    run: () => setTheme('light') },
+]);
+
+
+ +
+

Trung tâm thông báo

+

+ Mọi toast đã hiện trong trang này, lưu lại thành lịch sử. Bật vài toast trước rồi + hãy mở nó. +

+
+ + +
+
+ +
notificationCenter();
+getNotificationHistory(); // [{ id, message, tone, createdAt }]
+clearNotificationHistory();
+
+
+
+
+
+ +
+
+

Biểu mẫu và wizard

+

+ Nằm sau forgedialog/workflows. Trong TypeScript, tên các trường được suy ra + vào kiểu kết quả, nên giá trị trả về đã có kiểu mà không cần ép. +

+ +
+
+

form()

+

+ Biểu mẫu có kiểm tra, dựng từ danh sách trường. Trả về các giá trị, hoặc + null. +

+
+ +
+
+ +
const values = await form(
+  [
+    { name: 'email', type: 'email', label: 'Email', required: true },
+    { name: 'plan', type: 'select', label: 'Gói', options: [
+      { value: 'free', label: 'Miễn phí' },
+      { value: 'pro', label: 'Chuyên nghiệp' },
+    ] },
+    { name: 'notes', type: 'textarea', label: 'Ghi chú', rows: 3 },
+  ],
+  { title: 'Tạo tài khoản' },
+);
+
+
+ +
+

wizard()

+

Nhiều bước, kiểm tra theo từng bước và dùng chung dữ liệu.

+
+ +
+
+ +
const flow = wizard({
+  initialData: { name: '' },
+  steps: [
+    {
+      id: 'profile',
+      title: 'Hồ sơ của bạn',
+      render: (el, ctx) => {
+        const input = document.createElement('input');
+        input.className = 'fd-input';
+        input.placeholder = 'Tên';
+        input.addEventListener('input', () => ctx.set({ name: input.value }));
+        el.append(input);
+      },
+      validate: (ctx) => (ctx.data.name ? true : 'Vui lòng nhập tên của bạn.'),
+    },
+    { id: 'review', title: 'Xem lại',
+      render: (el, ctx) => { el.textContent = `Sẵn sàng chào đón ${ctx.data.name}.`; } },
+  ],
+});
+const data = await flow.result;
+
+
+ +
+

formWizard()

+

Cùng danh sách trường như form(), trải ra nhiều bước.

+
+ +
+
+ +
const values = await formWizard([
+  { id: 'account', title: 'Tài khoản', fields: [
+    { name: 'email', type: 'email', label: 'Email', required: true },
+  ] },
+  { id: 'profile', title: 'Hồ sơ', fields: [
+    { name: 'name', type: 'text', label: 'Họ và tên', required: true },
+  ] },
+]);
+
+
+
+
+
+ +
+
+

Chủ đề

+

+ setTheme() điều khiển thư viện và trang này cùng lúc; + setThemePreset() đổi toàn bộ diện mạo. +

+ +
+
+

Chế độ sáng tối

+

Sáng, tối, hoặc theo hệ điều hành.

+
+ + + +
+
+ +
setTheme('dark'); // 'light' | 'dark' | 'system'
+
+
+ +
+

Bộ chủ đề dựng sẵn

+

Chọn một bộ, rồi mở bất kỳ demo nào ở trên để thấy nó được áp dụng.

+
+ + + + +
+
+ +
setThemePreset('glass');
+
+
+ +
+

Giao diện và kéo thả

+

+ Màu sắc, bo góc, hướng đổ bóng, hover, và giới hạn kéo của từng hộp thoại có bảng + điều khiển riêng ở trang chủ. +

+ +
+ +
open({
+  title: 'Hộp thoại đã tạo kiểu',
+  appearance: { radius: 20, shadow: { angle: 180, distance: 20, blur: 60 } },
+  draggable: { bounds: 'viewport' },
+});
+
+
+
+
+
+ +
+
+

Mỗi demo trả về gì

+

+ Mọi demo ở trên in kết quả xuống đây, kèm các hook vòng đời plugin mà nó kích hoạt — + afterOpen và afterClose từ một plugin đăng ký trên trang này. +

+

+        
+
+
+ + + + + + + + + + diff --git a/site/vi/docs.html b/site/vi/docs.html new file mode 100644 index 0000000..53697ce --- /dev/null +++ b/site/vi/docs.html @@ -0,0 +1,1444 @@ + + + + + + Tài liệu API — Forge Dialog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+

Tài liệu API

+

+ Forge Dialog là thư viện hộp thoại TypeScript không phụ thuộc, xây trên phần tử + <dialog> của trình duyệt. Mọi thứ trên trang này đều nằm trong gói đã + phát hành — không có plugin phải cài và không có peer dependency phải thoả mãn. +

+ +
+

Cài đặt

+

Cài từ npm. Tên gói viết liền một từ; tên thư viện là Forge Dialog.

+
+ +
npm install forgedialog
+
+

Import API và một tệp CSS:

+
+ +
import { alert, confirm, prompt, open } from 'forgedialog';
+import 'forgedialog/style.css';
+
+

+ Hoặc nạp thẳng bản dựng cho trình duyệt. Nó tạo biến toàn cục ForgeDialog — + đây là định danh JavaScript nên viết liền một từ. +

+
+ +
<link rel="stylesheet" href="https://unpkg.com/forgedialog/dist/index.css" />
+<script src="https://unpkg.com/forgedialog/dist/index.global.js"></script>
+<script>
+  ForgeDialog.alert('Hello!');
+</script>
+
+

+ Yêu cầu Chrome/Edge 88+, Firefox 78+, Safari 15.4+, hoặc Node.js 20+ cho công cụ đóng + gói và import phía máy chủ. +

+
+ +
+

Điểm vào

+

+ Mọi điểm vào đều tree-shake được và có hạn mức gzip riêng, kiểm tra tự động trong CI. + Hãy import cái hẹp nhất đủ dùng. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ImportBao gồm
forgedialogTất cả, đã bật sẵn kéo thả, hiệu ứng, và bộ áp giao diện đầy đủ.
forgedialog/core + open(), alert, confirm, + prompt, chủ đề, nhãn, plugin, và bộ áp giao diện đầy đủ. +
forgedialog/alertChỉ alert().
forgedialog/confirmChỉ confirm().
forgedialog/promptChỉ prompt().
forgedialog/presentation + drawer, bottomSheet, lightbox, + loading, toast, commandPalette, + notificationCenter. +
forgedialog/workflowsform, formWizard, wizard.
forgedialog/interactions + Bộ điều khiển kéo thả. Có tác dụng phụ: chỉ cần import là kéo thả được bật. +
forgedialog/animations + Các hiệu ứng dựng sẵn. Có tác dụng phụ: chỉ cần import là hiệu ứng được bật. +
forgedialog/appearance + Bộ áp giao diện đầy đủ. Có tác dụng phụ: import nó là nâng cấp các điểm vào + chuyên biệt. +
forgedialog/reactuseForgeDialog() cho React.
forgedialog/vueuseForgeDialog() cho Vue.
forgedialog/svelteAction dialogTrigger.
forgedialog/web-componentdefineForgeDialog() và ForgeDialogElement.
+
+

Import theo năng lực

+

+ Các điểm vào chuyên biệt (/alert, /confirm, + /prompt) đi kèm bộ áp giao diện nhẹ, chỉ gồm độ mờ nền, lớp phủ, viền, và + các bóng dựng sẵn. Thêm năng lực bằng cách import chúng để lấy tác dụng phụ: +

+
+ +
import 'forgedialog/appearance';   // per-component colour, radius, hover
+import 'forgedialog/interactions'; // dragging
+import 'forgedialog/animations';   // animation presets
+import { alert } from 'forgedialog/alert';
+
+

+ forgedialog và forgedialog/core đã có sẵn bộ áp giao diện đầy + đủ, nên chỉ cần import thêm khi bạn dùng điểm vào chuyên biệt. +

+
+ +
+

Tệp CSS

+

+ forgedialog/style.css là tệp gộp tất cả. Muốn gửi ít CSS hơn thì chỉ ghép + các lớp mà trang thật sự dùng — core.css thì luôn bắt buộc. +

+
+ +
import 'forgedialog/style/core.css';      // required
+import 'forgedialog/style/forms.css';     // form fields and file dropzones
+import 'forgedialog/style/workflows.css'; // wizard stepper
+import 'forgedialog/style/toast.css';
+import 'forgedialog/style/lightbox.css';
+import 'forgedialog/style/command.css';
+import 'forgedialog/style/draggable.css';
+
+

+ Mọi quy tắc đều nằm trong cascade layer forgedialog, nên CSS không đặt + trong layer của bạn sẽ thắng mà không cần !important. +

+
+ +
+

alert, confirm, prompt

+

Bản thay thế trả về promise cho các hàm dựng sẵn của trình duyệt.

+
+ + + + + + + + + + + + + + + + + + + + + +
Chữ ký hàmTrả về
alert(message, options?)Promise<void>
confirm(message, options?) + Promise<boolean> — false khi đóng bằng Escape + hoặc click nền +
prompt(message, options?)Promise<string | null> — null khi bị huỷ
+
+
+ +
await alert('Saved successfully.', { title: 'Success' });
+
+const ok = await confirm('Delete this item?', { title: 'Please confirm' });
+
+const name = await prompt('What is your name?', {
+  defaultValue: 'Ada Lovelace',
+  inputType: 'text',
+  inputLabel: 'Full name',
+  placeholder: 'Type a name',
+  validate: (value) => (value.trim() ? true : 'Name is required'),
+});
+
+

+ alert và confirm tự động nhận role="alertdialog". + prompt nhận thêm các tuỳ chọn ở trên, bên cạnh mọi trường của + DialogOptions. +

+
+ +
+

open()

+

+ API tầng thấp. Nó trả về DialogInstance ngay lập tức chứ không + phải promise, nên hộp thoại có thể được cập nhật, di chuyển, hoặc đóng từ bất cứ đâu. +

+
+ +
import { open } from 'forgedialog';
+
+const dialog = open({
+  title: 'Custom dialog',
+  content: (container) => container.append(renderForm()),
+  buttons: [
+    { text: 'Cancel', onClick: (i) => i.close(null) },
+    { text: 'Save', role: 'primary', autoFocus: true, onClick: (i) => i.close(read()) },
+  ],
+});
+
+const result = await dialog.whenClosed();          // the result, or undefined
+const { result: r, reason } = await dialog.whenSettled(); // result plus why it closed
+
+

Nội dung và HTML không tin cậy

+

+ content nhận một chuỗi (hiển thị dưới dạng văn bản), một + phần tử, hoặc một hàm dựng. Còn HTML thì phải quyết định rõ ràng: +

+
+ +
// Untrusted markup: pass a sanitizer.
+open({ html: userContent, sanitizeHtml: (h) => DOMPurify.sanitize(h) });
+
+// Markup you already trust.
+open({ unsafeHtml: '<p>Generated by us</p>' });
+
+

Lý do đóng

+

+ Mỗi lần đóng đều kèm một lý do: 'button', 'escape', + 'backdrop', 'api', 'abort', hoặc + 'destroy'. Đọc nó bằng whenSettled(). +

+
+ +
+

DialogOptions

+

Được nhận bởi open() và mọi hàm tiện ích dựng trên nó.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tuỳ chọnKiểuMô tả
titlestring + Chữ ở thanh tiêu đề. Nếu bỏ trống, hộp thoại vẫn có nhãn ẩn về mặt thị giác để + công nghệ hỗ trợ gọi được tên nó. +
messagestring | HTMLElementVăn bản hoặc phần tử cho phần thân.
contentstring | HTMLElement | (container) => voidNội dung phần thân. Chuỗi được hiển thị dưới dạng văn bản.
htmlstringHTML, chỉ được hiển thị khi có kèm sanitizeHtml.
unsafeHtmlstringHTML chèn nguyên trạng. Chỉ dùng cho nội dung bạn kiểm soát được.
sanitizeHtml(html: string) => stringHàm làm sạch áp cho html.
buttonsButtonConfig[]Các nút ở chân hộp thoại. Xem bên dưới.
type + 'alert' | 'confirm' | 'prompt' | 'form' | 'wizard' | 'custom' + + Loại ngữ nghĩa. alert và confirm mặc định dùng + role="alertdialog". +
role'dialog' | 'alertdialog'Ghi đè role suy ra từ type.
size'sm' | 'md' | 'lg' | 'xl' | 'fullscreen'Bề rộng dựng sẵn.
presentation + 'modal' | 'drawer-left' | 'drawer-right' | 'bottom-sheet' | 'lightbox' + Cách hộp thoại xuất hiện và vị trí nó nằm.
animation + 'fade' | 'scale' | 'slide' | 'spring' | 'bounce' | 'blur' | 'none' + Cần năng lực animations. Tôn trọng prefers-reduced-motion.
appearanceDialogAppearance + Tạo kiểu riêng cho từng hộp thoại. Xem Giao diện. +
draggableboolean | DraggableOptionsXem Kéo thả.
closablebooleanHiện nút đóng ở thanh tiêu đề. Mặc định true.
closeOnEscapebooleanMặc định true.
closeOnOverlayClickbooleanMặc định true.
initialFocusstring | HTMLElement | (root) => HTMLElement | nullPhần tử nhận tiêu điểm khi mở. Mặc định là phần tử tab được đầu tiên.
restoreFocusboolean + Trả tiêu điểm về phần tử đã mở hộp thoại khi đóng. Mặc định true. +
portalTargetHTMLElementNơi hộp thoại được gắn vào. Mặc định là document.body.
classNamestringCác class thêm vào phần tử hộp thoại.
labelsPartial<DialogLabels>Ghi đè nhãn cho riêng hộp thoại này.
signalAbortSignalHuỷ bỏ sẽ đóng hộp thoại với lý do 'abort'.
dataunknownDữ liệu tuỳ ý, đọc được từ các hook.
onOpen(instance) => void | Promise<void>Sau khi hộp thoại mở.
onBeforeClose(instance, result) => boolean | void | Promise<…>Trả về false để giữ nó mở.
onClose(instance, result) => void | Promise<void>Sau khi hộp thoại đóng.
onError(error, instance) => voidTheo dõi lỗi trong vòng đời, những lỗi này được dọn dẹp tự động.
+
+

ButtonConfig

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TrườngKiểuMô tả
textstringNhãn. Bắt buộc.
role'primary' | 'secondary' | 'danger'Mức nhấn thị giác.
onClick(instance) => void | Promise<void>Hàm xử lý khi bấm.
closesDialogbooleanĐóng hộp thoại sau khi chạy onClick.
resultTResultGiá trị hộp thoại trả về khi nút này đóng nó.
autoFocusbooleanNhận tiêu điểm khi mở.
disabledbooleanHiển thị nút ở trạng thái vô hiệu.
idstringId của phần tử, tiện cho việc kiểm thử.
+
+
+ +
+

DialogInstance

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Thành phầnTrả vềMô tả
open()Promise<void>Mở một hộp thoại chưa được mở.
close(result?, reason?)Promise<void>Đóng kèm một kết quả.
cancel(reason?)Promise<void>Đóng mà không có kết quả.
destroy()Promise<void>Đóng rồi gỡ bỏ phần tử cùng các listener của nó.
update(partialOptions)voidĐổi kiểu hoặc vẽ lại hộp thoại tại chỗ, ngay khi nó đang mở.
whenClosed()Promise<TResult | undefined>Trả về kết quả.
whenSettled()Promise<{ result, reason }>Trả về kết quả kèm lý do đóng.
isOpen()booleanHộp thoại có đang mở hay không.
getState()DialogState + 'idle' | 'opening' | 'open' | 'closing' | 'closed' | 'destroyed'. +
getPosition(){ x, y }Độ dịch chuyển kéo hiện tại.
setPosition({ x, y }){ x, y }Di chuyển hộp thoại, giới hạn trong vùng cho phép.
resetPosition()voidĐưa nó về giữa.
elementHTMLElementPhần tử hộp thoại.
idstringId duy nhất.
+
+
+ +
+

Giao diện

+

+ Các tuỳ chỉnh giao diện chỉ áp cho một hộp thoại và đổi được lúc chạy bằng + update(). Giá trị độ mờ bị kẹp trong khoảng 0..1; số trần cho + độ dày, bo góc, và độ nhoè được hiểu là pixel. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TrườngKiểuÁp dụng cho
opacitynumberToàn bộ nền hộp thoại.
surfaceColorstringNền hộp thoại.
overlayOpacitynumberLớp phủ phía sau nó.
backdropBlurnumber | stringBán kính làm mờ lớp phủ.
titleColorstringChữ ở thanh tiêu đề.
titleBackgroundstring + Nền thanh tiêu đề. Mặc định trong suốt và bị cắt theo góc bo của hộp thoại. +
titleOpacitynumberThanh tiêu đề.
contentColorstringChữ ở phần thân.
contentOpacitynumberPhần thân.
borderColorstringViền.
borderOpacitynumberViền.
borderWidthnumber | stringViền.
borderStyle'none' | 'solid' | 'dashed' | 'dotted' | 'double'Viền.
radiusnumber | string | DialogCornerRadiusCác góc. Xem Bo góc.
shadowShadowPreset | string | DialogShadowConfigXem Đổ bóng.
hoverDialogHoverAppearanceXem Hover.
+
+
+ +
open({
+  title: 'Styled',
+  appearance: {
+    opacity: 0.96,
+    surfaceColor: '#12141a',
+    overlayOpacity: 0.55,
+    backdropBlur: 12,
+    titleColor: '#ffd166',
+    titleBackground: '#1b1740',
+    contentColor: '#c8ccd4',
+    borderColor: '#7c5cff',
+    borderWidth: 2,
+    radius: 20,
+    shadow: { angle: 135, distance: 24, blur: 60, opacity: 0.45 },
+  },
+});
+
+

+ Bộ áp đầy đủ đi kèm forgedialog và forgedialog/core. Với điểm + vào chuyên biệt, hãy thêm import 'forgedialog/appearance' — không có nó thì + các trường nâng cao bị bỏ qua chứ không báo lỗi. +

+
+ +
+

Đổ bóng

+

shadow nhận ba dạng.

+
+ + + + + + + + + + + + + + + + + + + + + +
DạngVí dụ
Bóng dựng sẵn + shadow: 'xl' — một trong none, sm, + md, lg, xl +
Bất kỳ giá trị box-shadow nào của CSSshadow: '0 10px 40px rgb(0 0 0 / 30%)'
Ghép từ các thành phầnshadow: { angle: 90, distance: 18, blur: 48, opacity: 0.3 }
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Thành phầnKiểuÝ nghĩa
anglenumber + Hướng bóng đổ, tính bằng độ: 0 lên trên, 90 sang phải, + 180 xuống dưới (mặc định), 270 sang trái. +
distancenumber | stringBóng bị hắt đi xa bao nhiêu.
blurnumber | stringĐộ mềm.
spreadnumber | stringĐộ nở ra trước khi làm mờ.
colorstringMàu bóng.
opacitynumberĐộ đậm, 0..1.
insetbooleanĐổ bóng vào phía trong.
+
+
+ +
+

Bo góc

+

+ radius nhận một giá trị dùng chung cho mọi góc, bất kỳ chuỗi + border-radius nào của CSS, hoặc một object khai theo từng góc. Góc không có + trong object đó vẫn giữ bo góc của chủ đề chứ không tụt về 0. +

+
+ +
appearance: { radius: 20 }
+appearance: { radius: '20px 20px 4px 4px' }
+appearance: { radius: { topLeft: 24, topRight: 24, bottomRight: 4, bottomLeft: 4 } }
+
+

+ Drawer giữ góc vuông và bottom sheet chỉ bo hai góc trên, trừ khi + radius nói khác. +

+
+ +
+

Hover

+

+ hover chỉ đổi kiểu hộp thoại khi con trỏ đang ở trên nó, và chỉ với hộp + thoại có yêu cầu — mọi hộp thoại khác đứng yên hoàn toàn. Nó dùng lại các trường màu, bo + góc, và đổ bóng, cộng thêm ba trường riêng. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
TrườngKiểuMô tả
liftnumber | stringHộp thoại nhấc lên bao nhiêu.
scalenumberHệ số phóng, ví dụ 1.02.
durationnumber | stringThời lượng chuyển động.
+
+

+ Biến đổi khi hover tôn trọng prefers-reduced-motion và bị tạm ngưng trong + lúc kéo hộp thoại, nên không thể đẩy nó ra ngoài vùng giới hạn. +

+
+ +
+

Kéo thả

+

+ draggable: true là đủ để kéo hộp thoại bằng thanh tiêu đề. Dạng object thêm + ràng buộc, hỗ trợ bàn phím, và ghi nhớ vị trí. Kéo thả cần năng lực interactions, thứ mà + forgedialog đã bật sẵn cho bạn. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tuỳ chọnKiểuMô tả
handle'header' | string | HTMLElementThứ khởi động thao tác kéo. Mặc định là thanh tiêu đề.
axis'both' | 'x' | 'y'Khoá chuyển động theo một trục.
bounds'viewport' | HTMLElement | DOMRectVùng mà hộp thoại bị giới hạn trong đó.
initialPosition{ x, y }Vị trí lệch ban đầu.
keyboardbooleanDi chuyển bằng phím mũi tên khi tay cầm đang có tiêu điểm.
keyboardStepnumberSố pixel mỗi lần nhấn phím.
persistKeystringGhi nhớ vị trí qua các phiên dưới khoá này.
onDragStart / onDrag / onDragEnd(event) => voidMỗi hàm nhận { position, originalEvent }.
+
+

+ Bottom sheet giữ cử chỉ vuốt để đóng của riêng nó và bỏ qua kéo thả thông thường. +

+
+ +
+

Drawer, sheet, lightbox, trạng thái tải

+
+ +
import { drawer, bottomSheet, lightbox, loading } from 'forgedialog';
+
+drawer({ title: 'Settings', side: 'right', content: renderSettings });
+
+bottomSheet({ title: 'Share', content: renderShare });
+
+lightbox('/photo.jpg', { alt: 'A photo', caption: 'Taken in 2026' });
+
+const task = loading('Uploading…');
+task.update('Almost there…');
+await task.close();
+
+

+ Đây chỉ là open() kèm một giá trị presentation dựng sẵn, nên + mọi trường của DialogOptions vẫn dùng được. +

+
+ +
+

Biểu mẫu

+

+ form() dựng một hộp thoại có kiểm tra từ danh sách trường và trả về các giá + trị, hoặc null nếu bị huỷ. Tên trường được suy ra vào kiểu kết quả, nên + values.email đã có kiểu mà không cần ép. +

+
+ +
import { form } from 'forgedialog';
+
+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: 4 },
+  ],
+  { title: 'Sign up', submitText: 'Create account' },
+);
+
+

+ Các kiểu trường: text, password, email, + number, date, textarea, select, + checkbox, radio, file. Mỗi trường nhận + label, defaultValue, placeholder, + helpText, required, và một hàm validate; kiểu số + có thêm min, max, step, còn kiểu tệp có thêm + accept, multiple, maxFiles, và + maxSizeBytes. +

+

+ formWizard(steps, options?) trải đúng các trường đó ra nhiều bước, kèm + nextText, backText, finishText, và callback + onStepChange. +

+
+ +
+

Wizard

+

+ wizard() là dạng tổng quát: mỗi bước tự vẽ nội dung tuỳ ý, có thể kiểm tra + dữ liệu, và có thể chọn bước kế tiếp dựa trên dữ liệu đã thu thập. +

+
+ +
import { wizard } from 'forgedialog';
+
+const flow = wizard({
+  initialData: { email: '', plan: 'free' },
+  persistKey: 'signup',      // resumes where the user left off
+  confirmUnsaved: true,      // asks before discarding progress
+  steps: [
+    { id: 'account', title: 'Account', render: renderAccount, validate: validateAccount },
+    { id: 'plan', title: 'Plan', render: renderPlan,
+      next: (data) => (data.plan === 'pro' ? 'billing' : 'review') },
+    { id: 'billing', title: 'Billing', render: renderBilling },
+    { id: 'review', title: 'Review', render: renderReview },
+  ],
+});
+
+const data = await flow.result; // the data, or null if cancelled
+
+

+ Bộ điều khiển còn cung cấp next(), back(), + goTo(stepId), getData(), và instance bên dưới. +

+
+ +
+

Toast và thông báo

+
+ +
import { toast, notificationCenter, getNotificationHistory } from 'forgedialog';
+
+toast('Saved', { tone: 'success' });
+
+// A duration of 0 or Infinity keeps it up until dismiss() is called.
+const upload = toast('Uploading…', { duration: 0 });
+upload.dismiss();
+
+toast('Message sent', { action: { text: 'Undo', onClick: undoSend } });
+
+notificationCenter();          // a dialog listing past toasts
+getNotificationHistory();      // the same list as data
+
+

+ Các sắc thái gồm info, success, warning, và + danger. clearNotificationHistory() xoá sạch nhật ký. +

+
+ +
+

Bảng lệnh

+
+ +
import { commandPalette } from 'forgedialog';
+
+commandPalette([
+  { id: 'new', label: 'New document', shortcut: '⌘N', run: createDocument },
+  { id: 'search', label: 'Search', keywords: ['find', 'filter'], run: openSearch },
+]);
+
+

+ Gõ để lọc theo nhãn và theo keywords; bảng lệnh trả về id của lệnh đã chạy. +

+
+ +
+

Chủ đề

+
+ +
import { setTheme, getTheme, setThemePreset } from 'forgedialog';
+
+setTheme('dark');    // 'light' | 'dark' | 'system'
+getTheme();
+
+setThemePreset('glass'); // 'default' | 'minimal' | 'glass' | 'material'
+
+

+ setTheme() ghi data-fd-theme lên <html>. + CSS của bạn có thể đọc chính thuộc tính đó để trang và hộp thoại đổi chủ đề cùng lúc — + đây đúng là cách site này làm. +

+
+ +
+

Biến CSS

+

+ Mọi thứ thuộc về hình thức đều là một biến trên :root, nên viết được cả một + chủ đề mà không đụng tới JavaScript. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BiếnMặc định
--fd-color-surface#ffffff
--fd-color-text#1a1d23
--fd-color-text-muted#5b6270
--fd-color-border#e2e4e9
--fd-color-overlayrgba(15, 17, 21, 0.5)
--fd-color-primary / --fd-color-primary-text#315fce / #ffffff
+ --fd-color-secondary / --fd-color-secondary-text + #eceef2 / #1a1d23
--fd-color-danger / --fd-color-danger-text#e5484d / #ffffff
--fd-radius10px
--fd-shadow0 20px 60px rgba(15, 17, 21, 0.25)
--fd-spacing-sm / md / lg8px / 16px / 24px
--fd-font-familybộ phông hệ thống
--fd-duration-fast / --fd-duration-normal120ms / 200ms
--fd-easingcubic-bezier(0.16, 1, 0.3, 1)
--fd-z-index-base1000
+
+

+ Các biến riêng từng hộp thoại mà appearance API ghi ra (--fd-dialog-radius, + --fd-dialog-shadow, --fd-dialog-title-background, và những cái + còn lại) cũng đặt tay được, nếu bạn thích tạo kiểu bằng CSS hơn bằng JavaScript. +

+
+ +
+

Nhãn và đa ngôn ngữ

+
+ +
import { setLabels, getLabels } from 'forgedialog';
+
+setLabels({
+  ok: 'Đồng ý',
+  cancel: 'Huỷ',
+  close: 'Đóng',
+  submit: 'Gửi',
+  fieldRequired: 'Bắt buộc',
+  promptPlaceholder: 'Nhập nội dung',
+  notifications: 'Thông báo',
+});
+
+

+ Nhãn đặt theo cách này áp dụng ở mọi nơi; từng hộp thoại vẫn ghi đè được bằng tuỳ chọn + labels của riêng nó. +

+
+ +
+

Plugin và hook

+

+ Năm hook vòng đời chạy cho mọi hộp thoại: beforeOpen, + afterOpen, beforeClose, afterClose, và + beforeDestroy. +

+
+ +
import { on, off, registerPlugin } from 'forgedialog';
+
+// A single hook.
+on('afterOpen', ({ instance, options }) => track('dialog', options.title));
+
+// Or a plugin bundling several.
+registerPlugin({
+  name: 'analytics',
+  install: (api) => api.on('afterClose', ({ reason }) => track('close', reason)),
+  hooks: {
+    beforeClose: ({ preventClose, result }) => {
+      if (result === undefined) preventClose?.();
+    },
+  },
+});
+
+

+ Mỗi hook nhận { instance, options, result, reason, preventClose }. Gọi + preventClose() trong beforeClose sẽ giữ hộp thoại mở. +

+
+ +
+

Adapter cho framework

+

+ Mỗi adapter là một lớp bọc mỏng quanh open(), gắn vòng đời hộp thoại vào + vòng đời component, nên không còn gì sót lại sau khi component bị gỡ. +

+
+ +
// React and Vue share the same hook shape.
+import { useForgeDialog } from 'forgedialog/react';
+const { open } = useForgeDialog();
+await open({ title: 'Hi' }).whenClosed();
+
+// Svelte: an action on the triggering element.
+import { dialogTrigger } from 'forgedialog/svelte';
+// <button use:dialogTrigger={{ title: 'Hi' }}>Open</button>
+
+// Web component.
+import { defineForgeDialog } from 'forgedialog/web-component';
+defineForgeDialog(); // <forge-dialog open title="Hi" message="…" size="md">
+
+
+ +
+

TypeScript, SSR, và CSP

+
    +
  • + Có kiểu cho cả ESM lẫn CJS. open<T>() luồn kiểu kết quả của nó qua + buttons, whenClosed(), và các hook. +
  • +
  • + Import gói này trên máy chủ không đụng tới DOM, nên an toàn với bản dựng SSR; hộp + thoại chỉ được tạo khi bạn gọi API trong trình duyệt. +
  • +
  • + Bản build không chứa eval và new Function, điều này được + kiểm tra trong CI, nên nó chạy được dưới Content Security Policy nghiêm ngặt. Chính + site này cũng đang chạy dưới một CSP như vậy. +
  • +
  • Mọi điểm vào đều có hạn mức gzip bắt buộc, kiểm tra ở từng pull request.
  • +
+
+ + +
+
+ + + + + + + + + + diff --git a/site/vi/index.html b/site/vi/index.html new file mode 100644 index 0000000..f47e981 --- /dev/null +++ b/site/vi/index.html @@ -0,0 +1,604 @@ + + + + + + Forge Dialog — hộp thoại cho web, dễ tiếp cận và không phụ thuộc + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

+ Mã nguồn mở · Giấy phép MIT · Không phụ thuộc lúc chạy +

+

Hộp thoại biết cư xử.

+

+ Forge Dialog là thư viện hộp thoại TypeScript không phụ thuộc, xây trên + phần tử <dialog> của trình duyệt. Dễ tiếp cận từ đầu, tuỳ biến được tới + từng thành phần, và nhẹ tới mức bản chỉ có alert vẫn dưới 6 KB sau gzip. +

+ +
+ + + + + Đọc tài liệu +
+ +
+ npm install forgedialog + +
+
+ +
+
+

Bạn nhận được gì

+

+ Mọi thứ dưới đây đều nằm trong gói đã phát hành — không plugin phải cài, không peer + dependency phải thoả mãn. +

+ +
+
+

Dễ tiếp cận từ đầu

+

+ Tiêu điểm được giữ trong hộp thoại rồi trả về chỗ cũ, Escape và click nền đã nối + sẵn, vai trò ARIA được đặt giúp bạn. Bộ dò tiêu điểm bỏ qua những phần tử chỉ trông + như nhận được tiêu điểm. +

+
+
+

Tạo kiểu theo từng thành phần

+

+ Màu và độ mờ riêng cho nền, tiêu đề, nội dung, và viền. Bo góc theo từng góc. Đổ + bóng ghép từ góc chiếu, khoảng cách, độ nhoè, và độ đậm. +

+
+
+

Hover, kéo thả, và cử chỉ

+

+ Đổi kiểu khi hover (tự chọn bật) kèm nhấc lên và phóng to. Hộp thoại kéo được, có + khoá trục, giới hạn vùng, di chuyển bằng bàn phím, và nhớ vị trí. Bottom sheet vuốt + để đóng. +

+
+
+

Nhiều hơn alert

+

+ Drawer, bottom sheet, lightbox, trạng thái tải, toast, bảng lệnh, và wizard phân + nhánh có lưu trạng thái — tất cả từ cùng một lõi. +

+
+
+

Kích thước trung thực

+

+ Các điểm vào tree-shake được, có hạn mức gzip bắt buộc. Import + forgedialog/alert thì bạn trả cho một alert, không phải cả thư viện. +

+
+
+

Hợp với stack của bạn

+

+ ESM, CJS, và biến toàn cục cho trình duyệt, kèm adapter cho React, Vue, Svelte, và + Web Components. Có kiểu đầy đủ, an toàn với SSR, và chạy sạch dưới CSP nghiêm ngặt. +

+
+
+
+
+ +
+
+

Bảng tuỳ biến

+

+ Đổi bất cứ thứ gì bên dưới và hộp thoại đổi kiểu ngay. Đoạn mã cập nhật theo — chép nó + là bạn có đúng hộp thoại đó trong ứng dụng của mình. +

+ +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + +
+ +
+ +
+
+
+
+
+ +
+
+

Ví dụ

+

+ The everyday calls. Each returns a promise, so there is no callback nesting. +

+ +
+
+

Thay thế hàm dựng sẵn của trình duyệt

+
+ +
import { alert, confirm, prompt } from 'forgedialog';
+import 'forgedialog/style.css';
+
+await alert('Saved successfully.', { title: 'Success' });
+
+const ok = await confirm('Delete this item?');
+
+const name = await prompt('What is your name?', {
+  validate: (value) => value.trim() ? true : 'Name is required',
+});
+
+
+ +
+

Tự dựng hộp thoại

+
+ +
import { open } from 'forgedialog';
+
+const dialog = open({
+  title: 'Custom dialog',
+  content: (container) => container.append(renderForm()),
+  buttons: [
+    { text: 'Cancel', onClick: (i) => i.close(null) },
+    { text: 'Save', role: 'primary', onClick: (i) => i.close(read()) },
+  ],
+});
+
+const result = await dialog.whenClosed();
+
+
+ +
+

Chỉ trả cho phần bạn dùng

+
+ +
// Just an alert, and nothing else.
+import { alert } from 'forgedialog/alert';
+import 'forgedialog/style/core.css';
+
+// Opt into the richer appearance options on a focused entry.
+import 'forgedialog/appearance';
+
+
+ +
+

Ngoài hộp thoại

+
+ +
import { drawer, toast, wizard } from 'forgedialog';
+
+// A duration of 0 keeps the toast until dismiss() is called.
+const upload = toast('Uploading…', { duration: 0 });
+
+drawer({ title: 'Settings', side: 'right', content: renderSettings });
+
+const data = await wizard({ steps }).result;
+
+
+
+
+
+ +
+
+

Tóm tắt nhanh

+

+ Bản ngắn gọn, thuần dữ kiện — các con số lấy từ hạn mức kích thước được kiểm tra ở mỗi + pull request. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TênForge Dialog
Gói npmforgedialog
Giấy phépMIT
Phụ thuộc lúc chạyKhông có
Kích thước≈15 KB sau gzip cho bản đầy đủ; ≈5.5 KB cho bản chỉ có alert
Viết bằngTypeScript, có kiểu cho cả ESM lẫn CommonJS
Trình duyệtChrome/Edge 88+, Firefox 78+, Safari 15.4+
Node.js20 trở lên, cho công cụ đóng gói và import phía máy chủ
FrameworkChạy độc lập; có adapter cho React, Vue, Svelte, và Web Components
Mã nguồn + github.com/cmm-cmm/ForgeDialog +
+
+
+
+ +
+
+

Câu hỏi thường gặp

+ +
+

Forge Dialog là gì?

+

+ Forge Dialog là thư viện TypeScript mã nguồn mở để dựng hộp thoại trên web: alert, + confirm, prompt, drawer, bottom sheet, lightbox, trạng thái tải, toast, bảng lệnh, và + wizard nhiều bước. Nó không có phụ thuộc lúc chạy và được phát hành trên npm với tên + forgedialog. +

+ +

Forge Dialog nặng bao nhiêu?

+

+ Bản đầy đủ khoảng 15 KB sau gzip. Ứng dụng chỉ import forgedialog/alert thì chỉ tốn + khoảng 5,5 KB sau gzip, vì mọi điểm vào đều tree-shake được và đều có hạn mức kích + thước được kiểm tra tự động ở mỗi pull request. +

+ +

Forge Dialog có cần framework không?

+

+ Không. Nó chạy độc lập bằng JavaScript thuần, và có sẵn adapter cho React, Vue, + Svelte, và Web Components nếu bạn muốn dùng. +

+ +

Forge Dialog có dễ tiếp cận không?

+

+ Có. Nó dùng phần tử <dialog> của trình duyệt, giữ tiêu điểm bên + trong hộp thoại rồi trả về phần tử đã mở nó, xử lý Escape và click nền, và tự đặt + role="dialog" hoặc role="alertdialog" tuỳ loại. +

+ +

Nó khác gì window.alert, confirm, và prompt?

+

+ Các hàm dựng sẵn của trình duyệt chặn luồng chính, không tạo kiểu được, và chỉ có ba + hình dạng cố định. Forge Dialog trả về promise thay vì chặn, tạo kiểu được tới từng + thành phần, và còn có drawer, bottom sheet, biểu mẫu, và wizard. +

+ +

Có tuỳ biến được giao diện không?

+

+ Có. Màu và độ mờ đặt riêng cho nền, tiêu đề, nội dung, và viền; bo góc theo từng góc; + đổ bóng ghép từ góc chiếu, khoảng cách, độ nhoè, và độ đậm; cùng hiệu ứng hover tự + chọn có nhấc lên và phóng to. +

+ +

Hỗ trợ những trình duyệt và môi trường nào?

+

+ Chrome và Edge 88+, Firefox 78+, Safari 15.4+, cùng Node.js 20 trở lên cho công cụ + đóng gói và import phía máy chủ. Bản build không chứa eval hay new Function, nên chạy + được dưới Content Security Policy nghiêm ngặt. +

+ +

Forge Dialog có dùng thương mại miễn phí không?

+

+ Có. Forge Dialog phát hành theo giấy phép MIT, cho phép dùng thương mại, sửa đổi, và + phân phối lại. +

+ +

Forge Dialog so với SweetAlert2 và Bootstrap modal thì thế nào?

+

+ Forge Dialog xây trên phần tử <dialog> của trình duyệt, nên việc + giữ tiêu điểm, lớp trên cùng, và vô hiệu hoá nền là do nền tảng lo chứ không phải + JavaScript. Nó không có phụ thuộc lúc chạy, trong khi Bootstrap modal cần CSS và + JavaScript của Bootstrap. Mọi điểm vào đều tree-shake được, nên ứng dụng chỉ cần alert + chỉ tốn khoảng 5,5 KB sau gzip thay vì cả thư viện. Nó viết bằng TypeScript và có kiểu + cho cả ESM lẫn CommonJS. +

+
+
+
+
+ + + + + + + + + +