Compare commits

..

7 Commits

Author SHA1 Message Date
konnorrogers
7150c59334 Bump changelog 2025-07-18 14:01:31 -04:00
Konnor Rogers
bd2a3c3b64 add pro setup (#1201) 2025-07-18 13:41:03 -04:00
Lindsay M
11519625ed Use margin for <wa-icon> spacing in native buttons (#1197) 2025-07-18 09:47:24 -04:00
Konnor Rogers
f19848c11e prevent infinite loop in build watcher (#1200)
* prevent infinite loop in build watcher

* prettier
2025-07-18 00:43:29 -04:00
Konnor Rogers
36b21b0be7 introduce before watch and after watch events (#1199)
* add before / after watch events so pro can properly incrementally build

* add events for pro / app

* add extra hooks for non-wa dev

* add old new line

* use package-lock.json from next

* npm install

* prettier
2025-07-18 00:09:47 -04:00
Cory LaViska
fe2c2ab7af improve accessibility; fixes #1177 (#1190) 2025-07-17 11:56:41 -04:00
Cory LaViska
b98b9baba4 add back id="hint" (#1192) 2025-07-17 11:55:53 -04:00
38 changed files with 208 additions and 38 deletions

21
package-lock.json generated
View File

@@ -13988,7 +13988,6 @@
"qr-creator": "^1.0.0",
"style-observer": "^0.0.7"
},
"devDependencies": {},
"engines": {
"node": ">=14.17.0"
}
@@ -14004,14 +14003,32 @@
"@shoelace-style/localize": "^3.2.1",
"composed-offset-position": "^0.0.6",
"lit": "^3.2.1",
"nanoid": "^5.1.5",
"qr-creator": "^1.0.0",
"style-observer": "^0.0.7"
},
"devDependencies": {},
"engines": {
"node": ">=14.17.0"
}
},
"packages/webawesome-pro/node_modules/nanoid": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz",
"integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"packages/webawesome/node_modules/nanoid": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz",

View File

@@ -261,13 +261,10 @@ export default async function (eleventyConfig) {
// });
// }
if (!isDev) {
// For a server build, we expect a server to run the second transform.
// For dev builds, we run the second transform in a middleware.
if (!isDev && !serverBuild) {
eleventyConfig.addTransform('simulate-webawesome-app', function (content) {
// For a server build, we expect a server to run the second transform.
if (serverBuild) {
return content;
}
// Only run the transform on files nunjucks would transform.
if (!this.page.inputPath.match(/.(md|html|njk)$/)) {
return content;

View File

@@ -46,6 +46,18 @@ Font Awesome users can set their kit code to unlock Font Awesome Pro icons. You
---
{# This looks weird, but without it, markdownItAttrs flags the raw calls incorrectly. #}
<div>
{%- raw -%}
{% if currentUser.hasPro %}
<div>
{% include "server/pro-setup.njk" ignore missing %}
</div>
{% endif %}
{% endraw %}
</div>
## Advanced Setup
The autoloader is the easiest way to use Web Awesome, but different projects (or your own preferences!) may require different installation methods.

View File

@@ -8,7 +8,7 @@ Web Awesome follows [Semantic Versioning](https://semver.org/). Breaking changes
Components with the <wa-badge variant="warning">Experimental</wa-badge> badge should not be used in production. They are made available as release candidates for development and testing purposes. As such, changes to experimental components will not be subject to semantic versioning.
## Next
## 3.0.0-beta.3
### New Features {data-no-outline}
@@ -26,6 +26,7 @@ Components with the <wa-badge variant="warning">Experimental</wa-badge> badge sh
- Fixed a bug in `<wa-card>` that caused slotted media to have incorrectly rounded corners [issue:1107]
- Fixed a bug in `<wa-button-group>` that prevented pill buttons from rendering corners properly [issue:1165]
- Fixed a bug in `<wa-button-group>` that caused some vertical groups to appear horizontal [issue:1152]
- Improved accessibility of `<wa-animated-image>` so keyboard users can focus and toggle the animation [issue:1177]
## 3.0.0-beta.2
@@ -394,4 +395,4 @@ Many of these changes and improvements were the direct result of feedback from u
</details>
Did we miss something? [Let us know!](https://github.com/shoelace-style/webawesome-alpha/discussions)
Did we miss something? [Let us know!](https://github.com/shoelace-style/webawesome-alpha/discussions)

View File

@@ -380,6 +380,19 @@ Add the `wa-pill` class to give buttons rounded edges.
<button class="wa-pill">Pill button</button>
```
When using `<wa-icon>` within a button, wrap adjacent label text in `<span>` or similar to automatically add margin between the icon and the label, just like the `start` and `end` slots of `<wa-button>`.
```html {.example}
<button>
<wa-icon name="plane-departure"></wa-icon>
<span>Start Icon</span>
</button>
<button>
<span>End Icon</span>
<wa-icon name="plane-arrival"></wa-icon>
</button>
```
### Form controls
Create a variety of form controls with `<input type="">`, `<select>`, and `<textarea>`. Each control closely matches the appearance of the corresponding Web Awesome component.

View File

@@ -34,7 +34,8 @@ const isDeveloping = process.argv.includes('--develop');
* @typedef {Object} BuildOptions
* @property {Array<string>} [watchedSrcDirectories]
* @property {Array<string>} [watchedDocsDirectories]
* @property {(eventName: "change" | "add" | "unlink", filePath: string) => unknown} [onWatchEvent]
* @property {(eventName: "change" | "add" | "unlink", filePath: string) => unknown} [beforeWatchEvent]
* @property {(eventName: "change" | "add" | "unlink", filePath: string) => unknown} [afterWatchEvent]
*/
/**
@@ -49,8 +50,6 @@ export async function build(options = {}) {
options.watchedDocsDirectories = [getDocsDir()];
}
function measureStep() {}
/**
* Runs the full build.
*/
@@ -122,6 +121,11 @@ export async function build(options = {}) {
* Generates React wrappers for all components.
*/
function generateReactWrappers() {
// Used by webawesome-app to make re-rendering not miserable with extra React file generation.
if (process.env.SKIP_SLOW_STEPS === 'true') {
return Promise.resolve();
}
spinner.start('Generating React wrappers');
try {
@@ -156,6 +160,11 @@ export async function build(options = {}) {
* Runs TypeScript to generate types.
*/
async function generateTypes() {
// Used by webawesome-app to make re-rendering not miserable with extra TS compilations.
if (process.env.SKIP_SLOW_STEPS === 'true') {
return Promise.resolve();
}
spinner.start('Running the TypeScript compiler');
const cwd = process.cwd();
@@ -377,22 +386,25 @@ export async function build(options = {}) {
},
);
// TODO: Should probably listen for all of these instead of just "change"
const watchEvents = [
'change',
// "unlink",
// "add"
];
const watchEvents = ['change', 'unlink', 'add'];
// Rebuild and reload when source files change
options.watchedSrcDirectories.forEach(dir => {
const watcher = bs.watch(join(dir, '**', '!(*.test).*'));
const watcher = bs.watch(join(dir, '**', '!(*.test).*'), { ignoreInitial: true });
watchEvents.forEach(evt => {
watcher.on(evt, handleWatchEvent(evt));
});
function handleWatchEvent(evt) {
return async filename => {
spinner.info(`File modified ${chalk.gray(`(${relative(getRootDir(), filename)})`)}`);
const changedFile = relative(getRootDir(), filename);
if (evt === 'changed') {
spinner.info(`File modified ${chalk.gray(`(${changedFile})`)}`);
} else if (evt === 'unlink') {
spinner.info(`File deleted ${chalk.gray(`(${changedFile})`)}`);
} else if (evt === 'add') {
spinner.info(`File added ${chalk.gray(`(${changedFile})`)}`);
}
try {
const isTestFile = filename.includes('.test.ts');
@@ -405,8 +417,8 @@ export async function build(options = {}) {
return;
}
if (typeof options.onWatchEvent === 'function') {
await options.onWatchEvent(evt, filename);
if (typeof options.beforeWatchEvent === 'function') {
await options.beforeWatchEvent(evt, filename);
}
// Copy stylesheets when CSS files change
@@ -426,6 +438,10 @@ export async function build(options = {}) {
// This needs to be outside of "isComponent" check because SSR needs to run on CSS files too.
await generateDocs({ spinner });
if (typeof options.afterWatchEvent === 'function') {
await options.afterWatchEvent(evt, filename);
}
reload();
} catch (err) {
console.error(chalk.red(err));
@@ -440,7 +456,7 @@ export async function build(options = {}) {
// Rebuild the docs and reload when the docs change
options.watchedDocsDirectories.forEach(dir => {
const watcher = bs.watch(join(dir, '**', '*.*'));
const watcher = bs.watch(join(dir, '**', '*.*'), { ignoreInitial: true });
watchEvents.forEach(evt => {
watcher.on(evt, handleWatchEvent(evt));
@@ -449,10 +465,14 @@ export async function build(options = {}) {
function handleWatchEvent(evt) {
return async filename => {
spinner.info(`File modified ${chalk.gray(`(${relative(getRootDir(), filename)})`)}`);
if (typeof options.onWatchEvent === 'function') {
await options.onWatchEvent(evt, filename);
if (typeof options.beforeWatchEvent === 'function') {
await options.beforeWatchEvent(evt, filename);
}
await generateDocs({ spinner });
if (typeof options.beforeWatchEvent === 'function') {
await options.afterWatchEvent(evt, filename);
}
reload();
};
}

View File

@@ -72,7 +72,6 @@ export async function generateDocs(options = {}) {
isDeveloping ??= process.argv.includes('--develop');
isIncremental ??= isDeveloping && !process.argv.includes('--no-incremental');
let eleventy = globalThis.eleventy;
/**
* Used by the webawesome-app to skip doc generation since it will do its own.
*/
@@ -80,6 +79,8 @@ export async function generateDocs(options = {}) {
return;
}
let eleventy = globalThis.eleventy;
spinner?.start?.('Writing the docs');
const outputs = {
@@ -118,7 +119,7 @@ export async function generateDocs(options = {}) {
return !line.includes('Watching');
});
const lastLine = info[info.length - 1];
output = chalk.gray(`(${lastLine})`);
output = chalk.gray(`(${info.join('')})`);
eleventy.logger.logger.reset();
}
} else {
@@ -137,13 +138,23 @@ export async function generateDocs(options = {}) {
if (!isDeveloping) {
await copy(getCdnDir(), join(getSiteDir(), 'dist'));
}
spinner?.succeed?.(`Writing the docs ${output}`);
if (spinner) {
spinner.succeed(`Writing the docs ${output}`);
} else {
console.log(`Writing the docs ${output}`);
}
} catch (error) {
console.warn = originalWarn;
console.error('\n\n' + chalk.red(error) + '\n');
spinner?.fail?.(chalk.red(`Error while writing the docs.`));
if (spinner) {
spinner.fail(chalk.red(`Error while writing the docs.`));
} else {
console.error(chalk.red(`Error while writing the docs.`));
}
if (!isDeveloping) {
process.exit(1);
}

View File

@@ -42,7 +42,7 @@ img[aria-hidden='true'] {
}
}
:host([play]:not(:hover)) .control-box {
:where(:host([play]:not(:hover))) .control-box {
opacity: 0;
}
@@ -50,3 +50,16 @@ img[aria-hidden='true'] {
:host(:not([play])) slot[name='pause-icon'] {
display: none;
}
/* Show control box on keyboard focus */
.animated-image {
&:focus {
outline: none;
}
&:focus-visible .control-box {
opacity: 1;
outline: var(--wa-focus-ring);
outline-offset: var(--wa-focus-ring-offset);
}
}

View File

@@ -4,6 +4,7 @@ import { WaErrorEvent } from '../../events/error.js';
import { WaLoadEvent } from '../../events/load.js';
import { watch } from '../../internal/watch.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import { LocalizeController } from '../../utilities/localize.js';
import '../icon/icon.js';
import styles from './animated-image.css';
@@ -30,6 +31,8 @@ import styles from './animated-image.css';
export default class WaAnimatedImage extends WebAwesomeElement {
static css = styles;
private readonly localize = new LocalizeController(this);
@query('.animated') animatedImage: HTMLImageElement;
@state() frozenFrame: string;
@@ -48,6 +51,13 @@ export default class WaAnimatedImage extends WebAwesomeElement {
this.play = !this.play;
}
private handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this.play = !this.play;
}
}
private handleLoad() {
const canvas = document.createElement('canvas');
const { width, height } = this.animatedImage;
@@ -82,15 +92,26 @@ export default class WaAnimatedImage extends WebAwesomeElement {
}
render() {
const verb = this.localize.term(this.play ? 'pauseAnimation' : 'playAnimation');
const label = `${verb} ${this.alt}`;
return html`
<div class="animated-image">
<div
class="animated-image"
tabindex="0"
role="button"
aria-pressed=${this.play ? 'true' : 'false'}
aria-label=${label}
@click=${this.handleClick}
@keydown=${this.handleKeyDown}
>
<img
class="animated"
src=${this.src}
alt=${this.alt}
crossorigin="anonymous"
aria-hidden=${this.play ? 'false' : 'true'}
@click=${this.handleClick}
role="presentation"
@load=${this.handleLoad}
@error=${this.handleError}
/>
@@ -102,10 +123,10 @@ export default class WaAnimatedImage extends WebAwesomeElement {
src=${this.frozenFrame}
alt=${this.alt}
aria-hidden=${this.play ? 'true' : 'false'}
@click=${this.handleClick}
role="presentation"
/>
<div part="control-box" class="control-box">
<div part="control-box" class="control-box" aria-hidden="true">
<slot name="play-icon">
<wa-icon
name="play"

View File

@@ -564,7 +564,6 @@
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5em;
height: var(--wa-form-control-height);
padding: 0 var(--wa-form-control-padding-inline);
@@ -709,6 +708,16 @@
&.wa-pill {
border-radius: var(--wa-border-radius-pill);
}
/* Adds space between icons and adjacent elements
* Prefer sibling selectors over :first-child/:last-child to avoid extra space when an icon is used alone */
& > wa-icon:has(+ *) {
margin-inline-end: 0.75em;
}
& > * + wa-icon {
margin-inline-start: 0.75em;
}
}
/* #endregion */

View File

@@ -24,6 +24,8 @@ const translation: Translation = {
if (num > 2 && num < 11) return `تم تحديد ${num} خيارات`;
return `تم تحديد ${num} خيار`;
},
pauseAnimation: 'إيقاف الرسوم المتحركة مؤقتًا',
playAnimation: 'تشغيل الرسوم المتحركة',
previousSlide: 'الشريحة السابقة',
progress: 'مقدار التقدم',
remove: 'حذف',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return 'Je vybrána jedna možnost';
return `Počet vybraných možností: ${num}`;
},
pauseAnimation: 'Pozastavit animaci',
playAnimation: 'Přehrát animaci',
previousSlide: 'Předchozí slide',
progress: 'Průběh',
remove: 'Odstranit',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return '1 valgt';
return `${num} valgt`;
},
pauseAnimation: 'Pause animation',
playAnimation: 'Afspil animation',
previousSlide: 'Forrige dias',
progress: 'Status',
remove: 'Fjern',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return '1 Option ausgewählt';
return `${num} Optionen ausgewählt`;
},
pauseAnimation: 'Animation pausieren',
playAnimation: 'Animation abspielen',
previousSlide: 'Vorherige Folie',
progress: 'Fortschritt',
remove: 'Entfernen',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return '1 option selected';
return `${num} options selected`;
},
pauseAnimation: 'Pause animation',
playAnimation: 'Play animation',
previousSlide: 'Previous slide',
progress: 'Progress',
remove: 'Remove',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return '1 opción seleccionada';
return `${num} opción seleccionada`;
},
pauseAnimation: 'Pausar animación',
playAnimation: 'Reproducir animación',
previousSlide: 'Diapositiva anterior',
progress: 'Progreso',
remove: 'Eliminar',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return '1 گزینه انتخاب شده است';
return `${num} گزینه انتخاب شده است`;
},
pauseAnimation: 'مکث انیمیشن',
playAnimation: 'پخش انیمیشن',
previousSlide: 'اسلاید قبلی',
progress: 'پیشرفت',
remove: 'حذف',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return 'Yksi vaihtoehto valittu';
return `${num} vaihtoehtoa valittu`;
},
pauseAnimation: 'Keskeytä animaatio',
playAnimation: 'Toista animaatio',
previousSlide: 'Edellinen dia',
progress: 'Edistyminen',
remove: 'Poista',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return '1 option sélectionnée';
return `${num} options sélectionnées`;
},
pauseAnimation: "Suspendre l'animation",
playAnimation: "Lire l'animation",
previousSlide: 'Diapositive précédente',
progress: 'Progrès',
remove: 'Retirer',

View File

@@ -16,13 +16,15 @@ const translation: Translation = {
goToSlide: (slide, count) => `עבור לשקופית ${slide} של ${count}`,
hidePassword: 'הסתר סיסמא',
loading: 'טוען',
nextSlide: 'Next slide',
nextSlide: 'השקף הבא',
numOptionsSelected: num => {
if (num === 0) return 'לא נבחרו אפשרויות';
if (num === 1) return 'נבחרה אפשרות אחת';
return `נבחרו ${num} אפשרויות`;
},
previousSlide: 'Previous slide',
pauseAnimation: 'השהה אנימציה',
playAnimation: 'נגן אנימציה',
previousSlide: 'שקופית קודמת',
progress: 'התקדמות',
remove: 'לְהַסִיר',
resize: 'שנה גודל',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return '1 opcija je odabrana';
return `${num} odabranih opcija`;
},
pauseAnimation: 'Pauziraj animaciju',
playAnimation: 'Reproduciraj animaciju',
previousSlide: 'Prethodni slajd',
progress: 'Napredak',
remove: 'Makni',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return '1 lehetőség kiválasztva';
return `${num} lehetőség kiválasztva`;
},
pauseAnimation: 'Animáció szüneteltetése',
playAnimation: 'Animáció lejátszása',
previousSlide: 'Előző dia',
progress: 'Folyamat',
remove: 'Eltávolítás',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return '1 opsi yang dipilih';
return `${num} opsi yang dipilih`;
},
pauseAnimation: 'Jeda animasi',
playAnimation: 'Putar animasi',
previousSlide: 'Slide sebelumnya',
progress: 'Kemajuan',
remove: 'Hapus',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return '1 opzione selezionata';
return `${num} opzioni selezionate`;
},
pauseAnimation: 'Metti in pausa animazione',
playAnimation: 'Riproduci animazione',
previousSlide: 'Diapositiva precedente',
progress: 'Avanzamento',
remove: 'Rimuovi',

View File

@@ -21,6 +21,8 @@ const translation: Translation = {
if (num === 0) return '項目が選択されていません';
return `${num} 個の項目が選択されました`;
},
pauseAnimation: 'アニメーションを一時停止',
playAnimation: 'アニメーションを再生',
previousSlide: '前のスライド',
progress: '進行',
remove: '削除',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return 'Ett alternativ valgt';
return `${num} alternativer valgt`;
},
pauseAnimation: 'Sett animasjon på pause',
playAnimation: 'Spill av animasjon',
previousSlide: 'Forrige visning',
progress: 'Fremdrift',
remove: 'Fjern',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return '1 optie geselecteerd';
return `${num} opties geselecteerd`;
},
pauseAnimation: 'Animatie pauzeren',
playAnimation: 'Animatie afspelen',
previousSlide: 'Vorige dia',
progress: 'Voortgang',
remove: 'Verwijderen',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return 'Eitt alternativ valt';
return `${num} alternativ valt`;
},
pauseAnimation: 'Set animasjon på pause',
playAnimation: 'Spel av animasjon',
previousSlide: 'Førre visning',
progress: 'Framdrift',
remove: 'Fjern',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return 'Wybrano 1 opcję';
return `Wybrano ${num} opcje`;
},
pauseAnimation: 'Wstrzymaj animację',
playAnimation: 'Odtwórz animację',
previousSlide: 'Poprzedni slajd',
progress: 'Postęp',
remove: 'Usunąć',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return '1 opção selecionada';
return `${num} opções selecionadas`;
},
pauseAnimation: 'Pausar animação',
playAnimation: 'Reproduzir animação',
previousSlide: 'Slide anterior',
progress: 'Progresso',
remove: 'Remover',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return 'Выбран 1 вариант';
return `выбрано ${num} варианта`;
},
pauseAnimation: 'Приостановить анимацию',
playAnimation: 'Воспроизвести анимацию',
previousSlide: 'Предыдущий слайд',
progress: 'Прогресс',
remove: 'Удалить',

View File

@@ -24,6 +24,8 @@ const translation: Translation = {
if (num === 3 || num === 4) return `${num} možnosti izbrane`;
return `${num} možnosti izbranih`;
},
pauseAnimation: 'Zaustavi animacijo',
playAnimation: 'Predvajaj animacijo',
previousSlide: 'Prejšnji diapozitiv',
progress: 'Napredek',
remove: 'Odstrani',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return '1 alternativ valt';
return `${num} alternativ valda`;
},
pauseAnimation: 'Pausa animation',
playAnimation: 'Spela upp animation',
previousSlide: 'Föregående bild',
progress: 'Framsteg',
remove: 'Ta bort',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return '1 seçenek seçildi';
return `${num} seçenek seçildi`;
},
pauseAnimation: 'Animasyonu duraklat',
playAnimation: 'Animasyonu oynat',
previousSlide: 'Bir onceki slayt',
progress: 'İlerleme',
remove: 'Kaldır',

View File

@@ -24,6 +24,8 @@ const translation: Translation = {
if (n === 2 || n === 3 || n === 4) return `вибрано ${num} варіанти`;
return `вибрано ${num} варіантів`;
},
pauseAnimation: 'Призупинити анімацію',
playAnimation: 'Відтворити анімацію',
previousSlide: 'Попередній слайд',
progress: 'Поступ',
remove: 'Видалити',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return '已选择 1 个项目';
return `${num} 选择项目`;
},
pauseAnimation: '暂停动画',
playAnimation: '播放动画',
previousSlide: '上一张幻灯片',
progress: '进度',
remove: '删除',

View File

@@ -22,6 +22,8 @@ const translation: Translation = {
if (num === 1) return '已選擇 1 個項目';
return `${num} 選擇項目`;
},
pauseAnimation: '暫停動畫',
playAnimation: '播放動畫',
previousSlide: '上一張幻燈片',
progress: '進度',
remove: '移除',

View File

@@ -33,6 +33,8 @@ export interface Translation extends DefaultTranslation {
loading: string;
nextSlide: string;
numOptionsSelected: (num: number) => string;
pauseAnimation: string;
playAnimation: string;
previousSlide: string;
progress: string;
remove: string;