From 9e607eb0c7670c37ddfe7c521c9f99145b17ed62 Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Sat, 21 Sep 2024 14:55:06 -0400 Subject: [PATCH 1/7] Move story format list to dialog --- docs/en/src/story-formats/adding.md | 12 +- docs/en/src/story-formats/default.md | 12 +- docs/en/src/story-formats/extensions.md | 6 +- docs/en/src/story-formats/proofing.md | 13 +- docs/en/src/story-formats/removing.md | 7 +- docs/en/src/story-formats/viewing.md | 19 +- public/locales/ca.json | 6 +- public/locales/cs.json | 36 +- public/locales/da.json | 6 +- public/locales/de.json | 41 +- public/locales/en-US.json | 42 +- public/locales/es.json | 6 +- public/locales/fi.json | 6 +- public/locales/fr.json | 43 +- public/locales/it.json | 6 +- public/locales/jp.json | 3 +- public/locales/ko.json | 41 +- public/locales/ms.json | 6 +- public/locales/nb.json | 6 +- public/locales/nl.json | 41 +- public/locales/pt-BR.json | 567 +++++++++--------- public/locales/pt-PT.json | 47 +- public/locales/ru.json | 6 +- public/locales/sl.json | 6 +- public/locales/sv.json | 6 +- public/locales/tr.json | 43 +- public/locales/uk.json | 41 +- public/locales/zh-CN.json | 41 +- src/app.tsx | 37 +- .../container/dialog-card/dialog-card.css | 12 +- .../story-format/add-story-format-button.tsx | 105 ---- .../__tests__/story-format-card.test.tsx | 127 ---- .../story-format/story-format-card/index.ts | 1 - .../story-format-card/story-format-card.css | 29 - .../story-format-card/story-format-card.tsx | 87 --- .../__mocks__/story-format-item-details.tsx | 5 + .../__mocks__/story-format-item.tsx | 33 + .../story-format-item-details.test.tsx} | 22 +- .../__tests__/story-format-item.test.tsx | 245 ++++++++ .../story-format/story-format-item/index.ts | 2 + .../story-format-item-details.tsx} | 24 +- .../story-format-item/story-format-item.css | 52 ++ .../story-format-item/story-format-item.tsx | 125 ++++ .../add-story-format-button.test.tsx | 26 +- .../story-formats-filter.button.test.tsx | 93 +++ .../__tests__/story-formats.test.tsx | 232 +++++++ .../add-story-format-button.css | 0 .../add-story-format-button.tsx | 43 +- .../story-formats-filter-button.tsx | 43 ++ src/dialogs/story-formats/story-formats.css | 19 + src/dialogs/story-formats/story-formats.tsx | 121 ++++ .../__tests__/app-actions.test.tsx | 19 +- src/route-actions/app-actions.tsx | 5 +- src/routes/__tests__/index.test.tsx | 8 - src/routes/index.tsx | 4 - .../__mocks__/story-format-list-route.tsx | 5 - src/routes/story-format-list/index.ts | 1 - .../story-format-list-route.tsx | 103 ---- .../story-format-list-toolbar.test.tsx | 43 -- .../default-story-format-button.test.tsx | 112 ---- .../formats/__tests__/format-actions.test.tsx | 61 -- .../proofing-story-format-button.test.tsx | 119 ---- .../remove-story-format-button.test.tsx | 104 ---- .../story-format-extensions-button.test.tsx | 136 ----- .../formats/default-story-format-button.tsx | 42 -- .../toolbar/formats/format-actions.tsx | 27 - .../formats/proofing-story-format-button.tsx | 42 -- .../formats/remove-story-format-button.tsx | 37 -- .../story-format-extensions-button.tsx | 60 -- src/routes/story-format-list/toolbar/index.ts | 1 - .../toolbar/story-format-list-toolbar.tsx | 28 - .../toolbar/view-actions.tsx | 36 -- .../story-formats/__tests__/load.test.ts | 5 +- .../electron-ipc/story-formats/load.ts | 1 - .../story-formats/__tests__/reducer.test.ts | 1 - src/store/story-formats/action-creators.ts | 54 +- src/store/story-formats/reducer.ts | 1 - .../story-formats/story-formats.types.ts | 1 - src/test-util/fakes.ts | 4 - 79 files changed, 1533 insertions(+), 2125 deletions(-) delete mode 100644 src/components/story-format/add-story-format-button.tsx delete mode 100644 src/components/story-format/story-format-card/__tests__/story-format-card.test.tsx delete mode 100644 src/components/story-format/story-format-card/index.ts delete mode 100644 src/components/story-format/story-format-card/story-format-card.css delete mode 100644 src/components/story-format/story-format-card/story-format-card.tsx create mode 100644 src/components/story-format/story-format-item/__mocks__/story-format-item-details.tsx create mode 100644 src/components/story-format/story-format-item/__mocks__/story-format-item.tsx rename src/components/story-format/{story-format-card/__tests__/story-format-card-details.test.tsx => story-format-item/__tests__/story-format-item-details.test.tsx} (72%) create mode 100644 src/components/story-format/story-format-item/__tests__/story-format-item.test.tsx create mode 100644 src/components/story-format/story-format-item/index.ts rename src/components/story-format/{story-format-card/story-format-card-details.tsx => story-format-item/story-format-item-details.tsx} (59%) create mode 100644 src/components/story-format/story-format-item/story-format-item.css create mode 100644 src/components/story-format/story-format-item/story-format-item.tsx rename src/{routes/story-format-list/toolbar/formats => dialogs/story-formats}/__tests__/add-story-format-button.test.tsx (82%) create mode 100644 src/dialogs/story-formats/__tests__/story-formats-filter.button.test.tsx create mode 100644 src/dialogs/story-formats/__tests__/story-formats.test.tsx rename src/{routes/story-format-list/toolbar/formats => dialogs/story-formats}/add-story-format-button.css (100%) rename src/{routes/story-format-list/toolbar/formats => dialogs/story-formats}/add-story-format-button.tsx (68%) create mode 100644 src/dialogs/story-formats/story-formats-filter-button.tsx create mode 100644 src/dialogs/story-formats/story-formats.css create mode 100644 src/dialogs/story-formats/story-formats.tsx delete mode 100644 src/routes/story-format-list/__mocks__/story-format-list-route.tsx delete mode 100644 src/routes/story-format-list/index.ts delete mode 100644 src/routes/story-format-list/story-format-list-route.tsx delete mode 100644 src/routes/story-format-list/toolbar/__tests__/story-format-list-toolbar.test.tsx delete mode 100644 src/routes/story-format-list/toolbar/formats/__tests__/default-story-format-button.test.tsx delete mode 100644 src/routes/story-format-list/toolbar/formats/__tests__/format-actions.test.tsx delete mode 100644 src/routes/story-format-list/toolbar/formats/__tests__/proofing-story-format-button.test.tsx delete mode 100644 src/routes/story-format-list/toolbar/formats/__tests__/remove-story-format-button.test.tsx delete mode 100644 src/routes/story-format-list/toolbar/formats/__tests__/story-format-extensions-button.test.tsx delete mode 100644 src/routes/story-format-list/toolbar/formats/default-story-format-button.tsx delete mode 100644 src/routes/story-format-list/toolbar/formats/format-actions.tsx delete mode 100644 src/routes/story-format-list/toolbar/formats/proofing-story-format-button.tsx delete mode 100644 src/routes/story-format-list/toolbar/formats/remove-story-format-button.tsx delete mode 100644 src/routes/story-format-list/toolbar/formats/story-format-extensions-button.tsx delete mode 100644 src/routes/story-format-list/toolbar/index.ts delete mode 100644 src/routes/story-format-list/toolbar/story-format-list-toolbar.tsx delete mode 100644 src/routes/story-format-list/toolbar/view-actions.tsx diff --git a/docs/en/src/story-formats/adding.md b/docs/en/src/story-formats/adding.md index e0493d09d..7460364ee 100644 --- a/docs/en/src/story-formats/adding.md +++ b/docs/en/src/story-formats/adding.md @@ -4,10 +4,10 @@ To add a story format, you'll need to know its address. A story format should include this in its documentation. A story format address must be a URL, with a prefix like `https://` in front of it. -Once you know your story format's address, choose _Add_ from the _Story Format_ -top toolbar tab and enter it in the dialog that appears. If Twine is able to -load the story format from the address you've entered, it will show a preview of -the format name and version you're adding. Choose _Add_ in the dialog to add it. -It'll appear in the list of story format cards immediately. You can now either -[set it as the default story format](default.md) or [change individual +Once you know your story format's address, choose _Add_ at the top of the Story +Formats dialog and enter it in the dialog that appears. If Twine is able to load +the story format from the address you've entered, it will show a preview of the +format name and version you're adding. Choose _Add_ in the dialog to add it. +It'll appear in the list of story formats immediately. You can now either [set +it as the default story format](default.md) or [change individual stories](../editing-stories/changing-story-format.md) to use it, if you like. diff --git a/docs/en/src/story-formats/default.md b/docs/en/src/story-formats/default.md index 96761efb7..1e52cd0f3 100644 --- a/docs/en/src/story-formats/default.md +++ b/docs/en/src/story-formats/default.md @@ -1,11 +1,11 @@ # Changing the Default Story Format -To change the default story format, select its card in the Story Formats screen -and choose _Use as Default Format_ from the _Story Format_ top toolbar tab. A -"Used as Default" sticker will appear on the card to confirm the change. If the -_Use As Default_ button is disabled, the format you've selected is already the -default format, or the format you've selected is for proofing. (In that case, -you probably want to [set it as your proofing format](proofing.md) instead.) +To change the default story format, use the _Use as Default Format_ button below +it. A "Used as Default" sticker will appear on the card to confirm the change. +If the _Use As Default_ button is disabled, the format you've selected is +already the default format, or the format you've selected is for proofing. (In +that case, you probably want to [set it as your proofing format](proofing.md) +instead.) Changing the default story format doesn't affect stories you've already created, only new ones you create in the future. You [can change the story format for diff --git a/docs/en/src/story-formats/extensions.md b/docs/en/src/story-formats/extensions.md index e8c4b5ff2..85d78ac6e 100644 --- a/docs/en/src/story-formats/extensions.md +++ b/docs/en/src/story-formats/extensions.md @@ -8,10 +8,8 @@ Story formats can extend Twine by adding: - Syntax coloring in passage edit dialogs If these extensions are buggy or you just prefer not to use them, you can -disable them. Select the story format card in the Story Format screen and choose -_Disable Editor Extensions_ from the _Story Format_ top toolbar tab. To reverse -this change, select the same card and choose _Enable Editor Extensions_ from the -_Story Format_ top toolbar tab. +disable them. To do this, uncheck the _Use Editor Extensions_ checkbox under the +story format's name in the dialog. To reverse this change, check this checkbox. Disabling extensions disables all extensions for a format. You can't disable just the toolbar, for example, but not a format's references or syntax coloring. diff --git a/docs/en/src/story-formats/proofing.md b/docs/en/src/story-formats/proofing.md index 0b4ee78f0..13fc34423 100644 --- a/docs/en/src/story-formats/proofing.md +++ b/docs/en/src/story-formats/proofing.md @@ -1,9 +1,8 @@ # Changing the Proofing Format -To change the proofing format, select its card in the Story Formats screen and -choose _Use to Proof Stories_ from the _Story Format_ top toolbar tab. A "Used -for Proofing" sticker will appear on the card to confirm the change. If the _Use -to Proof Stories_ button is disabled, the format you've selected is already the -proofing format, or the format you've selected is not a proofing format. (In -that case, you probably want to [set it as your default format](default.md) -instead.) +To change the proofing format, use the _Use As Proofing Format_ button below it. +A "Used for Proofing" sticker will appear on the card to confirm the change. If +the _Use As Proofing Format_ button is disabled, the format you've selected is +already the proofing format, or the format you've selected is not a proofing +format. (In that case, you probably want to [set it as your default +format](default.md) instead.) diff --git a/docs/en/src/story-formats/removing.md b/docs/en/src/story-formats/removing.md index 3cae95060..ede25018b 100644 --- a/docs/en/src/story-formats/removing.md +++ b/docs/en/src/story-formats/removing.md @@ -1,9 +1,8 @@ # Removing a Story Format -To remove a story format, select its card in the Story Format screen and choose -_Remove_ from the _Story Format_ top toolbar tab. If this button is disabled, -you've selected a format that is built-in, or is currently your default or -proofing story format. +To remove a story format, click the _Delete_ button below its name. If you don't +see a _Delete_ button, you've selected a format that is built-in, or is +currently your default or proofing story format. If a story uses a story format you've removed, you'll need to [change it to another format](../editing-stories/changing-story-format.md). Twine will also diff --git a/docs/en/src/story-formats/viewing.md b/docs/en/src/story-formats/viewing.md index 08af8ac91..73beff153 100644 --- a/docs/en/src/story-formats/viewing.md +++ b/docs/en/src/story-formats/viewing.md @@ -2,21 +2,22 @@ To view a list of story formats installed in your version of Twine, choose _Story Formats_ from the _Twine_ top toolbar tab. By default, the Story Formats -screen will show you the newest version of every story format you have -installed. Each card in the Story Formats screen represents a single format -that's installed. This list shows both regular story formats and proofing ones -together, and is sorted alphabetically by name. +dialog will show you the newest version of every story format you have +installed. Each item represents a single format that's installed. This list +shows both regular story formats and proofing ones together, and is sorted +alphabetically by name. Story formats that come installed with Twine have a sticker on them labeled "Built In." The [default story format](default.md) has a sticker on it labeled "Used as Default," and the format that's used for proofing stories has a "Used -for Proofing" sticker. +for Proofing" sticker. These formats also have a blue background in the list to +make them easier to locate. If Twine isn't able to load a story format, it will show an error symbol on its card with a short explanation of the error it encountered. -To view only story formats you've added yourself, choose _User-Added Story -Formats_ from the _View_ top toolbar tab. +The dialog has a button that allows you to choose what formats are shown: -To view all formats, including older versions of story formats, choose _All -Story Formats_ from the _View_ top toolbar tab. +- _Current Story Formats_ shows only the newest version of each story format. +- _User-Added Story Formats_ shows only formats you have added. +- _All Story Formats_ shows all formats installed, including older versions of formats. \ No newline at end of file diff --git a/public/locales/ca.json b/public/locales/ca.json index ea354bff3..9cd56e760 100644 --- a/public/locales/ca.json +++ b/public/locales/ca.json @@ -29,7 +29,7 @@ "renameStoryButton": {"emptyName": "Introduïu un nom."}, "safariWarningCard": {}, "storyCard": {}, - "storyFormatCard": {}, + "storyFormatItem": {}, "storyFormatSelect": {}, "tagEditor": {} }, @@ -81,10 +81,6 @@ "selectAllPassages": "Selecciona tots els passatges" } }, - "storyFormatList": { - "title": {}, - "storyFormatExplanation": "Els formats d'història controlen l'aparença i comportament de les històries durant la reproducció." - }, "storyImport": {}, "storyList": { "noStories": "No hi ha cap història desada al Twine ara mateix. Per començar, podeu crear una història nova i importar una existent des d'un fitxer.", diff --git a/public/locales/cs.json b/public/locales/cs.json index 2aeaa6a04..553bb5cb8 100644 --- a/public/locales/cs.json +++ b/public/locales/cs.json @@ -118,11 +118,10 @@ "passageCount": "1 pasáž", "passageCount_plural": "{{count}} pasáží" }, - "storyFormatCard": { + "storyFormatItem": { "author": "Autor: {{author}}", "builtIn": "Vestavěné", "defaultFormat": "Použito jako výchozí", - "editorExtensionsDisabled": "Rozšíření editoru vypnuta", "license": "Licence: {{license}}", "loadingFormat": "Načítání tohoto formátu příběhu...", "loadError": "Tento formát příběhu se nepodařilo načíst ({{errorMessage}}).", @@ -193,6 +192,16 @@ "noTags": "K pasážím v tomto příběhu nebyly zatím přidány žádné značky.", "title": "Značky pasáží" }, + "storyFormats": { + "addStoryFormatButton": { + "addPreview": "{{storyFormatName}} {{storyFormatVersion}} bude přidán.", + "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} už byl přidán.", + "fetchError": "Formát příběhu nemohl být ze zadaného URL získán ({{errorMessage}}).", + "invalidUrl": "Zadejte validní URL.", + "prompt": "K přidání nového formátu příběhu zadejte jeho URL adresu níže." + }, + "noneVisible": "Žádné formáty příběhu se neshodují s žádnými kritérii, která jste vybrali." + }, "storyImport": { "deselectAll": "Zrušit výběr", "filePrompt": "K importování příběhu do Twinu nahrejte archiv, publikovaný příběh, nebo zdrojový kód v Twee.", @@ -322,29 +331,6 @@ "passageNamesAndExcerpts": "Zobrazi názvy pasáží a úryvky z nich" } }, - "storyFormatList": { - "noneVisible": "Žádné formáty příběhu se neshodují s žádnými kritérii, která jste vybrali.", - "show": "Zobrazit...", - "title": { - "all": "Všechny formáty příběhu", - "current": "Současné formáty příbehu", - "user": "Uživatelské formáty příběhu" - }, - "toolbar": { - "addStoryFormatButton": { - "addPreview": "{{storyFormatName}} {{storyFormatVersion}} bude přidán.", - "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} už byl přidán.", - "fetchError": "Formát příběhu nemohl být ze zadaného URL získán ({{errorMessage}}).", - "invalidUrl": "Zadejte validní URL.", - "prompt": "K přidání nového formátu příběhu zadejte jeho URL adresu níže." - }, - "disableFormatExtensions": "Vypnout rozšíření editoru", - "enableFormatExtensions": "Zapnout rozšíření editoru příběhu", - "useAsDefaultFormat": "Použít jako výchozí formát", - "useAsProofingFormat": "Použít ke korektuře příběhů" - }, - "storyFormatExplanation": "Příběhové formáty ovlivňují vzhled a chování příběhů během jejich přehrávání." - }, "storyList": { "library": "Knihovna", "noStories": "Ve Twinu nejsou uložené žádné příběhy. Aby jste začali, můžete buď vytvořit nový příběh nebo ze souboru importovat již existující.", diff --git a/public/locales/da.json b/public/locales/da.json index a2b760780..7ea474fe6 100644 --- a/public/locales/da.json +++ b/public/locales/da.json @@ -31,7 +31,7 @@ "renameStoryButton": {"emptyName": "Indtast et navn."}, "safariWarningCard": {}, "storyCard": {}, - "storyFormatCard": {}, + "storyFormatItem": {}, "storyFormatSelect": {}, "tagEditor": {} }, @@ -69,10 +69,6 @@ "publishToFile": "Udgiv som fil" } }, - "storyFormatList": { - "title": {}, - "storyFormatExplanation": "Historieformater styrer udseende og opførsel af historier når der spilles." - }, "storyImport": {}, "storyList": { "noStories": "Der er ingen historier gemt i Twine lige nu. For at begynde kan du enten oprette en ny historie eller importere en eksisterende fra en fil.", diff --git a/public/locales/de.json b/public/locales/de.json index 52d2cb3ec..37a61bd9e 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -118,11 +118,10 @@ "passageCount": "1 Abschnitt", "passageCount_plural": "{{count}} Abschnitte" }, - "storyFormatCard": { + "storyFormatItem": { "author": "von {{author}}", "builtIn": "Erstellt am", "defaultFormat": "Als Standard gesetzt", - "editorExtensionsDisabled": "Editor Erweiterungen Deaktiviert", "license": "Lizenz: {{license}}", "loadingFormat": "Lade diese Geschichtsformat...", "loadError": "Dieses Geschichtsformat konnte nicht geladen werden ({{errorMessage}}).", @@ -219,6 +218,21 @@ "words": "Wörter" } }, + "storyFormats": { + "addStoryFormatButton": { + "addPreview": "{{storyFormatName}} {{storyFormatVersion}} wird hinzugefügt.", + "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} ist bereits vorhanden.", + "fetchError": "Das Geschichtsformat an dieser Adresse konnte nicht abgerufen werden ({{errorMessage}}).", + "invalidUrl": "Bitte eine gültige URL eingeben.", + "prompt": "Um ein Geschichtsformat hinzuzufügen, gebe unten die Adresse ein." + }, + "filterButton": { + "all": "Alle Geschichtsformate", + "current": "Momentane Geschichtsformate", + "user": "Vom Benutzer hinzugefügte Geschichtsformate" + }, + "noneVisible": "Keine Geschichtsformate passen zu deinen Kriterien." + }, "storyJavaScript": { "editorLabel": "JavaScript der Geschicht", "title": "JavaScript der Geschicht", @@ -322,29 +336,6 @@ "passageNamesAndExcerpts": "Zeige Abschnittsnamen und -ausschnitte" } }, - "storyFormatList": { - "noneVisible": "Keine Geschichtsformate passen zu deinen Kriterien.", - "show": "Anzeigen...", - "title": { - "all": "Alle Geschichtsformate", - "current": "Momentane Geschichtsformate", - "user": "Vom Benutzer hinzugefügte Geschichtsformate" - }, - "toolbar": { - "addStoryFormatButton": { - "addPreview": "{{storyFormatName}} {{storyFormatVersion}} wird hinzugefügt.", - "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} ist bereits vorhanden.", - "fetchError": "Das Geschichtsformat an dieser Adresse konnte nicht abgerufen werden ({{errorMessage}}).", - "invalidUrl": "Bitte eine gültige URL eingeben.", - "prompt": "Um ein Geschichtsformat hinzuzufügen, gebe unten die Adresse ein." - }, - "disableFormatExtensions": "Deaktiviere Editor Erweiterungen", - "enableFormatExtensions": "Aktiviere Editor Erweiterungen", - "useAsDefaultFormat": "Benutze als Standardformat", - "useAsProofingFormat": "Benutze zur Korrektur von Geschichten" - }, - "storyFormatExplanation": "Geschichtsformate bestimmen Aussehen und Verhalten Deiner Geschichten während des Spielens." - }, "storyList": { "library": "Bibliothek", "noStories": "Es sind derzeit noch keine Geschichten in Twine gespeichert. Du kannst entweder eine neue Geschichte erstellen oder eine bestehende Geschichte importieren.", diff --git a/public/locales/en-US.json b/public/locales/en-US.json index c0f9ed9c9..438de350a 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -118,11 +118,10 @@ "passageCount": "1 passage", "passageCount_plural": "{{count}} passages" }, - "storyFormatCard": { + "storyFormatItem": { "author": "by {{author}}", "builtIn": "Built In", "defaultFormat": "Used as Default", - "editorExtensionsDisabled": "Editor Extensions Disabled", "license": "License: {{license}}", "loadingFormat": "Loading this story format...", "loadError": "This story format could not be loaded ({{errorMessage}}).", @@ -193,6 +192,22 @@ "noTags": "No tags have been added to passages in this story.", "title": "Passage Tags" }, + "storyFormats": { + "addStoryFormatButton": { + "addPreview": "{{storyFormatName}} {{storyFormatVersion}} will be added.", + "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} has already been added.", + "fetchError": "The story format at this address could not be retrieved ({{errorMessage}}).", + "invalidUrl": "Please enter a valid URL.", + "prompt": "To add a story format, enter its address below." + }, + "filterButton": { + "all": "All Story Formats", + "current": "Current Story Formats", + "user": "User-Added Story Formats" + }, + "noneVisible": "No story formats match the criteria you've selected.", + "title": "Story Formats" + }, "storyImport": { "deselectAll": "Deselect All", "filePrompt": "To import stories into Twine, upload an archive, published story, or Twee source file below.", @@ -322,29 +337,6 @@ "passageNamesAndExcerpts": "Show Passage Names and Excerpts" } }, - "storyFormatList": { - "noneVisible": "No story formats match the criteria you've selected.", - "show": "Show...", - "title": { - "all": "All Story Formats", - "current": "Current Story Formats", - "user": "User-Added Story Formats" - }, - "toolbar": { - "addStoryFormatButton": { - "addPreview": "{{storyFormatName}} {{storyFormatVersion}} will be added.", - "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} has already been added.", - "fetchError": "The story format at this address could not be retrieved ({{errorMessage}}).", - "invalidUrl": "Please enter a valid URL.", - "prompt": "To add a story format, enter its address below." - }, - "disableFormatExtensions": "Disable Editor Extensions", - "enableFormatExtensions": "Enable Editor Extensions", - "useAsDefaultFormat": "Use as Default Format", - "useAsProofingFormat": "Use to Proof Stories" - }, - "storyFormatExplanation": "Story formats control the appearance and behavior of stories during play." - }, "storyList": { "library": "Library", "noStories": "There are no stories saved in Twine right now. To get started, you can either create a new story or import an existing one from a file.", diff --git a/public/locales/es.json b/public/locales/es.json index 80bdbd20f..e63648397 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -33,7 +33,7 @@ "renameStoryButton": {"emptyName": "Por favor ingresa un nombre."}, "safariWarningCard": {}, "storyCard": {}, - "storyFormatCard": {}, + "storyFormatItem": {}, "storyFormatSelect": {}, "tagEditor": {} }, @@ -75,10 +75,6 @@ "publishToFile": "Publicar a archivo" } }, - "storyFormatList": { - "title": {}, - "storyFormatExplanation": "Los formatos de historia controlan la apariencia y comportamiento de las historias durante su reproducción." - }, "storyImport": {}, "storyList": { "noStories": "No hay historias guardadas en Twine ahora mismo. Para empezar, puedes crear una historia nueva o bien importar una existente desde un archivo.", diff --git a/public/locales/fi.json b/public/locales/fi.json index c86951b51..a13b81eac 100644 --- a/public/locales/fi.json +++ b/public/locales/fi.json @@ -33,7 +33,7 @@ "renameStoryButton": {"emptyName": "Anna nimi."}, "safariWarningCard": {}, "storyCard": {}, - "storyFormatCard": {}, + "storyFormatItem": {}, "storyFormatSelect": {}, "tagEditor": {} }, @@ -75,10 +75,6 @@ "publishToFile": "Julkaise tiedostoon" } }, - "storyFormatList": { - "title": {}, - "storyFormatExplanation": "Tarinan muotoilut ohjaavat tarinan ulkoasua ja toimintaa tarinaa näytettäessä." - }, "storyImport": {}, "storyList": { "noStories": "Twinessä ei ole tallennettuja tarinoita. Aloita luomalla uusi tarina tai tuo tarina tiedostosta.", diff --git a/public/locales/fr.json b/public/locales/fr.json index eb61ecf8b..e973aadca 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -107,11 +107,10 @@ "passageCount": "1 passage", "passageCount_plural": "{{count}} passages" }, - "storyFormatCard": { + "storyFormatItem": { "author": "par {{author}}", "builtIn": "Formatté En", "defaultFormat": "Utilisé par Défaut", - "editorExtensionsDisabled": "Extensions de l'Éditeur désactivées", "license": "License: {{license}}", "loadingFormat": "Chargement du format de l'histoire...", "loadError": "Ce format d'histoire n'a pas pu être chargé ({{errorMessage}}).", @@ -182,10 +181,25 @@ "noTags": "Aucune balise n'a été ajoutée aux passages de cette histoire.", "title": "Balises du passage" }, + "storyFormats": { + "addStoryFormatButton": { + "addPreview": "{{storyFormatName}} {{storyFormatVersion}} sera ajouté.", + "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} a déjà été ajouté.", + "fetchError": "Le format d'histoire à cette adresse n'a pas pu être récupéré ({errorMessage}).", + "invalidUrl": "Veuillez entrer une URL valide.", + "prompt": "Pour ajouter un format d'histoire, entrez son adresse ci-dessous." + }, + "filterButton": { + "all": "Tous les formats d'histoire", + "current": "Formats d'histoire actuels", + "user": "Formats d'histoire ajoutés par l'utilisateur" + }, + "noneVisible": "Aucun des formats d'histoire ne correspond aux critères choisis." + }, "storyInfo": { "stats": { "title": "Statistiques de l'Histoire" - } + } }, "storyJavaScript": { "editorLabel": "JavaScript de l'histoire", @@ -299,29 +313,6 @@ "passageNamesAndExcerpts": "Afficher les noms des passages et leurs résumés" } }, - "storyFormatList": { - "noneVisible": "Aucun des formats d'histoire ne correspond aux critères choisis.", - "show": "Afficher...", - "title": { - "all": "Tous les formats d'histoire", - "current": "Formats d'histoire actuels", - "user": "Formats d'histoire ajoutés par l'utilisateur" - }, - "toolbar": { - "addStoryFormatButton": { - "addPreview": "{{storyFormatName}} {{storyFormatVersion}} sera ajouté.", - "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} a déjà été ajouté.", - "fetchError": "Le format d'histoire à cette adresse n'a pas pu être récupéré ({errorMessage}).", - "invalidUrl": "Veuillez entrer une URL valide.", - "prompt": "Pour ajouter un format d'histoire, entrez son adresse ci-dessous." - }, - "disableFormatExtensions": "Désactiver les extensions de l'éditeur", - "enableFormatExtensions": "Activer les extensions de l'éditeur", - "useAsDefaultFormat": "Utiliser en tant que format par défaut", - "useAsProofingFormat": "Utiliser pour vérifier les histoires" - }, - "storyFormatExplanation": "Story formats control the appearance and behavior of stories during play." - }, "storyImport": {}, "storyList": { "library": "Bibliothèque", diff --git a/public/locales/it.json b/public/locales/it.json index 1a3bb843f..77aec55a6 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -31,7 +31,7 @@ "renameStoryButton": {"emptyName": "Inserisci un nome."}, "safariWarningCard": {}, "storyCard": {}, - "storyFormatCard": {}, + "storyFormatItem": {}, "storyFormatSelect": {}, "tagEditor": {} }, @@ -70,10 +70,6 @@ "publishToFile": "Pubblica come File" } }, - "storyFormatList": { - "title": {}, - "storyFormatExplanation": "I formati Racconto controllano l’aspetto ed il comportamento delle storie durante l’esecuzione." - }, "storyImport": {}, "storyList": { "noStories": "Al momento non ci sono racconti salvati in Twine. Per cominciare, puoi creare un nuovo racconto oppure importarne uno da file.", diff --git a/public/locales/jp.json b/public/locales/jp.json index 27fc9cf5e..69774518b 100644 --- a/public/locales/jp.json +++ b/public/locales/jp.json @@ -20,7 +20,7 @@ "renameStoryButton": {}, "safariWarningCard": {}, "storyCard": {}, - "storyFormatCard": {}, + "storyFormatItem": {}, "storyFormatSelect": {}, "tagEditor": {} }, @@ -47,7 +47,6 @@ "publishToFile": "ファイルに出力する" } }, - "storyFormatList": {"title": {}}, "storyImport": {}, "storyList": { "noStories": "現在、Twineに保存されているストーリーはありません。開始するには、新しいストーリーを作成するか、ファイルから既存のストーリーをインポートします。", diff --git a/public/locales/ko.json b/public/locales/ko.json index 77c28f9fb..cb8f46c12 100644 --- a/public/locales/ko.json +++ b/public/locales/ko.json @@ -118,11 +118,10 @@ "passageCount": "1개 구절", "passageCount_plural": "{{count}}개 구절" }, - "storyFormatCard": { + "storyFormatItem": { "author": "제작자: {{author}}", "builtIn": "기본 제공", "defaultFormat": "기본값으로 사용", - "editorExtensionsDisabled": "편집기 확장 사용 안 함", "license": "라이선스: {{license}}", "loadingFormat": "이 스토리 형식을 로드하는 중...", "loadError": "이 스토리 형식을 로드할 수 없습니다. ({{errorMessage}})", @@ -219,6 +218,21 @@ "words": "단어" } }, + "storyFormats": { + "addStoryFormatButton": { + "addPreview": "{{storyFormatName}} {{storyFormatVersion}}이(가) 추가됩니다.", + "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}}이(가) 이미 추가되었습니다.", + "fetchError": "이 주소의 스토리 형식을 검색할 수 없습니다. ({{errorMessage}})", + "invalidUrl": "유효한 URL을 입력하세요.", + "prompt": "스토리 형식을 추가하려면 아래에 주소를 입력하세요." + }, + "filterButton": { + "all": "모든 스토리 형식", + "current": "현재 스토리 형식", + "user": "사용자 추가 스토리 형식" + }, + "noneVisible": "선택한 조건에 맞는 스토리 형식이 없습니다." + }, "storyJavaScript": { "editorLabel": "스토리 JavaScript", "title": "스토리 JavaScript", @@ -309,29 +323,6 @@ "passageNamesAndExcerpts": "구절 이름과 발췌문 표시" } }, - "storyFormatList": { - "noneVisible": "선택한 조건에 맞는 스토리 형식이 없습니다.", - "show": "표시...", - "title": { - "all": "모든 스토리 형식", - "current": "현재 스토리 형식", - "user": "사용자 추가 스토리 형식" - }, - "toolbar": { - "addStoryFormatButton": { - "addPreview": "{{storyFormatName}} {{storyFormatVersion}}이(가) 추가됩니다.", - "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}}이(가) 이미 추가되었습니다.", - "fetchError": "이 주소의 스토리 형식을 검색할 수 없습니다. ({{errorMessage}})", - "invalidUrl": "유효한 URL을 입력하세요.", - "prompt": "스토리 형식을 추가하려면 아래에 주소를 입력하세요." - }, - "disableFormatExtensions": "편집기 확장 비활성화", - "enableFormatExtensions": "편집기 확장 활성화", - "useAsDefaultFormat": "기본 형식으로 사용", - "useAsProofingFormat": "스토리 교정에 사용" - }, - "storyFormatExplanation": "스토리 형식은 플레이 중 스토리의 모양과 동작을 제어합니다." - }, "storyList": { "library": "라이브러리", "noStories": "현재 Twine에 저장된 스토리가 없습니다. 시작하려면 새 스토리를 만들거나 파일에서 기존 스토리를 가져올 수 있습니다.", diff --git a/public/locales/ms.json b/public/locales/ms.json index a0d872f00..521bdfa39 100644 --- a/public/locales/ms.json +++ b/public/locales/ms.json @@ -31,7 +31,7 @@ "renameStoryButton": {"emptyName": "Sila masukkan nama."}, "safariWarningCard": {}, "storyCard": {}, - "storyFormatCard": {}, + "storyFormatItem": {}, "storyFormatSelect": {}, "tagEditor": {} }, @@ -72,10 +72,6 @@ "publishToFile": "Terbit ke Fail" } }, - "storyFormatList": { - "title": {}, - "storyFormatExplanation": "Format cerita kawal penampilan dan tingkah laku cerita semasa tengah bermain." - }, "storyImport": {}, "storyList": { "noStories": "Tiada apa-apa cerita disimpan di Twine pada masa sekarang. Untuk bermula, anda boleh sama ada cipta cerita baharu atau import yang sedia ada dari fail.", diff --git a/public/locales/nb.json b/public/locales/nb.json index 2f9a05384..7e5e24fa8 100644 --- a/public/locales/nb.json +++ b/public/locales/nb.json @@ -31,7 +31,7 @@ "renameStoryButton": {"emptyName": "Vennligst skriv inn et navn."}, "safariWarningCard": {}, "storyCard": {}, - "storyFormatCard": {}, + "storyFormatItem": {}, "storyFormatSelect": {}, "tagEditor": {} }, @@ -67,10 +67,6 @@ "publishToFile": "Publiser til fil" } }, - "storyFormatList": { - "title": {}, - "storyFormatExplanation": "Historieformater styrer fremstillingen og oppførselen til historier under spillet." - }, "storyImport": {}, "storyList": { "noStories": "Det er ingen historier lagret i Twine akkurat nå. For å komme i gang kan du enten opprette en ny historie eller importere en eksisterende fra en fil.", diff --git a/public/locales/nl.json b/public/locales/nl.json index 5c43bdc3e..33912c4b4 100644 --- a/public/locales/nl.json +++ b/public/locales/nl.json @@ -114,11 +114,10 @@ "passageCount": "1 passage", "passageCount_plural": "{{count}} passages" }, - "storyFormatCard": { + "storyFormatItem": { "author": "door {{author}}", "builtIn": "Ingebouwd", "defaultFormat": "Gebruikt als standaard", - "editorExtensionsDisabled": "Editor-extensies uitgeschakeld", "license": "Licentie: {{license}}", "loadingFormat": "Laden van dit formaat...", "loadError": "Dit formaat kon niet worden geladen ({{errorMessage}}).", @@ -189,6 +188,21 @@ "noTags": "Er zijn geen labels toegevoegd aan passages in dit verhaal.", "title": "Passage Labels" }, + "storyFormats": { + "addStoryFormatButton": { + "addPreview": "{{storyFormatName}} {{storyFormatVersion}} wordt toegevoegd.", + "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} is al toegevoegd.", + "fetchError": "Het verhaalformaat op dit adres kan niet worden opgehaald ({{errorMessage}}).", + "invalidUrl": "Voer een geldige URL in.", + "prompt": "Voer het adres hieronder in om een verhaalformaat toe te voegen." + }, + "filterButton": { + "all": "Alle Verhaal Formaten", + "current": "Huidige Verhaal Formaten", + "user": "Door Gebruiker Toegevoegde Formaten" + }, + "noneVisible": "Er zijn geen verhaalformaten die voldoen aan de criteria die je hebt geselecteerd." + }, "storyImport": { "deselectAll": "Deselecteer Alles", "filePrompt": "Om verhalen in Twine te importeren, uploadt u hieronder een archief of een gepubliceerd verhaalbestand.", @@ -300,29 +314,6 @@ "passageNamesAndExcerpts": "Passagenamen en fragmenten weergeven" } }, - "storyFormatList": { - "noneVisible": "Er zijn geen verhaalformaten die voldoen aan de criteria die je hebt geselecteerd.", - "show": "Laat zien...", - "title": { - "all": "Alle Verhaal Formaten", - "current": "Huidige Verhaal Formaten", - "user": "Door Gebruiker Toegevoegde Formaten" - }, - "toolbar": { - "addStoryFormatButton": { - "addPreview": "{{storyFormatName}} {{storyFormatVersion}} wordt toegevoegd.", - "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} is al toegevoegd.", - "fetchError": "Het verhaalformaat op dit adres kan niet worden opgehaald ({{errorMessage}}).", - "invalidUrl": "Voer een geldige URL in.", - "prompt": "Voer het adres hieronder in om een verhaalformaat toe te voegen." - }, - "disableFormatExtensions": "Editor-extensies uitschakelen", - "enableFormatExtensions": "Editor-extensies inschakelen", - "useAsDefaultFormat": "Gebruiken als standaardformaat", - "useAsProofingFormat": "Gebruik om verhalen proef te lezen" - }, - "storyFormatExplanation": "Verhaalformaten bepalen het uiterlijk en het gedrag van verhalen tijdens het spelen." - }, "storyList": { "library": "Bibliotheek", "noStories": "Er zijn momenteel geen verhalen opgeslagen in Twine. Om aan de slag te gaan, kunt u een nieuw verhaal maken of een bestaand verhaal uit een bestand importeren.", diff --git a/public/locales/pt-BR.json b/public/locales/pt-BR.json index 746265d99..b629920b5 100644 --- a/public/locales/pt-BR.json +++ b/public/locales/pt-BR.json @@ -53,199 +53,213 @@ "view": "Exibir" }, "components": { - "addTagButton": { - "alreadyAdded": "Este nome de etiqueta já está sendo usado.", - "addLabel": "Adicionar etiqueta", - "invalidName": "Insira um nome válido para a etiqueta.", - "newTag": "Nova Etiqueta", - "tagColorLabel": "Cor da Etiqueta", - "tagNameLabel": "Nome da Etiqueta" - }, - "dialogCard": { - "contentsCrashed": "Algo deu errado com esta caixa de texto. Tente fechá-la e abri-la novamente." - }, - "fontSelect": { - "customScaleDetail": "Somente valores em percentagem, por favor.", - "customFamilyDetail": "Insira apenas o nome da fonte.", - "familyEmpty": "Insira o nome da fonte.", - "font": "Fonte", - "fonts": { - "monospaced": "Monoespaçada", - "serif": "Serifada", - "system": "Sistema" - }, - "fontSize": "Tamanho da Fonte", - "percentage": "{{percent}}%", - "percentageIsntNumber": "Insira um número.", - "percentageNotPositive": "Insira um número maior do que 0." - }, - "indentButtons": { - "indent": "Indentar", - "unindent": "Remover Indentação" - }, - "localStorageQuota": { - "measureAgain": "Calcular o espaço disponível novamente", - "percentAvailable": "{percent}% de espaço disponível" - }, - "passageCard": { - "placeholderClick": "Clique duas vezes nesta passagem para editá-la.", - "placeholderTouch": "Toque nesta passagem, então selecione «Editar» na aba «Passagem» para editá-la." - }, - "passageFuzzyFinder": { - "noResults": "Nenhuma correspondência.", - "prompt": "Procurar por nome da passagem ou por texto" - }, - "renamePassageButton": { - "emptyName": "Insira um nome.", - "nameAlreadyUsed": "Já existe uma passagem com esse nome nesta história." - }, - "renameStoryButton": { - "emptyName": "Insira o título da história.", - "nameAlreadyUsed": "Já existe uma história com esse título." - }, - "safariWarningCard": { - "archiveAndUseAnotherBrowser": "Por favor, arquive as suas histórias e use outra plataforma.", - "addToHomeScreen": "Adicione esta página à sua tela inicial para contornar esta limitação.", - "howToAddToHomeScreen": "Como Adicionar Isto à Minha Tela Inicial?", - "learnMore": "Saiba mais", - "message": "O navegador utilizado apagará todas as suas histórias caso não visite esta página dentro de sete dias." - }, - "storageQuota": { - "freeSpace": "{{percent}}% de espaço disponível" - }, - "storyCard": { - "lastUpdated": "Editada pela última vez em {{date}}", - "passageCount": "1 passagem", - "passageCount_plural": "{{count}} passagens" - }, - "storyFormatCard": { - "author": "de {{author}}", - "builtIn": "Criado em", - "defaultFormat": "Usado por Padrão", - "editorExtensionsDisabled": "Extensões do Editor Desligadas", - "license": "Licença: {{license}}", - "loadingFormat": "Carregando este formato de história...", - "loadError": "Este formato de história não pôde ser carregado ({{errorMessage}}).", - "name": "{{name}} {{version}}", - "proofing": "Revisão", - "proofingFormat": "Usado para a Revisão", - "useEditorExtensions": "Usar Extensões do Editor", - "useFormat": "Usar como Formato de História Padrão", - "useProofingFormat": "Usar como Formato de Revisão" - }, - "storyFormatSelect": { - "loadingCount": "Carregando 1 Formato de História...", - "loadingCount_plural": "Carregando {{loadingCount}} Formatos de História..." - }, - "tagEditor": { - "alreadyExists": "Já existe uma etiqueta com esse nome." - } - }, + "addTagButton": { + "alreadyAdded": "Este nome de etiqueta já está sendo usado.", + "addLabel": "Adicionar etiqueta", + "invalidName": "Insira um nome válido para a etiqueta.", + "newTag": "Nova Etiqueta", + "tagColorLabel": "Cor da Etiqueta", + "tagNameLabel": "Nome da Etiqueta" + }, + "dialogCard": { + "contentsCrashed": "Algo deu errado com esta caixa de texto. Tente fechá-la e abri-la novamente." + }, + "fontSelect": { + "customScaleDetail": "Somente valores em percentagem, por favor.", + "customFamilyDetail": "Insira apenas o nome da fonte.", + "familyEmpty": "Insira o nome da fonte.", + "font": "Fonte", + "fonts": { + "monospaced": "Monoespaçada", + "serif": "Serifada", + "system": "Sistema" + }, + "fontSize": "Tamanho da Fonte", + "percentage": "{{percent}}%", + "percentageIsntNumber": "Insira um número.", + "percentageNotPositive": "Insira um número maior do que 0." + }, + "indentButtons": { + "indent": "Indentar", + "unindent": "Remover Indentação" + }, + "localStorageQuota": { + "measureAgain": "Calcular o espaço disponível novamente", + "percentAvailable": "{percent}% de espaço disponível" + }, + "passageCard": { + "placeholderClick": "Clique duas vezes nesta passagem para editá-la.", + "placeholderTouch": "Toque nesta passagem, então selecione «Editar» na aba «Passagem» para editá-la." + }, + "passageFuzzyFinder": { + "noResults": "Nenhuma correspondência.", + "prompt": "Procurar por nome da passagem ou por texto" + }, + "renamePassageButton": { + "emptyName": "Insira um nome.", + "nameAlreadyUsed": "Já existe uma passagem com esse nome nesta história." + }, + "renameStoryButton": { + "emptyName": "Insira o título da história.", + "nameAlreadyUsed": "Já existe uma história com esse título." + }, + "safariWarningCard": { + "archiveAndUseAnotherBrowser": "Por favor, arquive as suas histórias e use outra plataforma.", + "addToHomeScreen": "Adicione esta página à sua tela inicial para contornar esta limitação.", + "howToAddToHomeScreen": "Como Adicionar Isto à Minha Tela Inicial?", + "learnMore": "Saiba mais", + "message": "O navegador utilizado apagará todas as suas histórias caso não visite esta página dentro de sete dias." + }, + "storageQuota": { + "freeSpace": "{{percent}}% de espaço disponível" + }, + "storyCard": { + "lastUpdated": "Editada pela última vez em {{date}}", + "passageCount": "1 passagem", + "passageCount_plural": "{{count}} passagens" + }, + "storyFormatItem": { + "author": "de {{author}}", + "builtIn": "Criado em", + "defaultFormat": "Usado por Padrão", + "license": "Licença: {{license}}", + "loadingFormat": "Carregando este formato de história...", + "loadError": "Este formato de história não pôde ser carregado ({{errorMessage}}).", + "name": "{{name}} {{version}}", + "proofing": "Revisão", + "proofingFormat": "Usado para a Revisão", + "useEditorExtensions": "Usar Extensões do Editor", + "useFormat": "Usar como Formato de História Padrão", + "useProofingFormat": "Usar como Formato de Revisão" + }, + "storyFormatSelect": { + "loadingCount": "Carregando 1 Formato de História...", + "loadingCount_plural": "Carregando {{loadingCount}} Formatos de História..." + }, + "tagEditor": { + "alreadyExists": "Já existe uma etiqueta com esse nome." + } + }, "dialogs": { - "aboutTwine": { - "donateToTwine": "Ajude o Twine a Crescer com uma Doação", - "codeHeader": "Código", - "codeRepo": "Visitar o Repositório do Código-Fonte", - "license": "Esta aplicação está publicada de acordo com a licença GPL v3, mas qualquer obra criada pode ser publicada de acordo com quaisquer outras condições, incluindo comerciais.", - "localizationHeader": "Localizações", - "title": "Sobre o Twine {{version}}", - "twineDescription": "O Twine é uma aplicação de código-fonte aberto para contar histórias interativas e não lineares." - }, - "appDonation": { - "donate": "Doar para o Desenvolvimento do Twine", - "onlyOnce": "(Esta mensagem será exibida apenas uma vez. Caso queira fazer uma doação para o desenvolvimento do Twine, siga o link na caixa \"Sobre o Twine\".)", - "supportMessage": "Se você ama o Twine, pedimos que o ajude a crescer fazendo uma doação. O Twine é um projeto de código-fonte aberto que será sempre gratuito — e graças à sua ajuda, o Twine poderá continuar crescendo.", - "noThanks": "Não, Obrigado", - "title": "Apoiar o Desenvolvimento do Twine" - }, - "appPrefs": { - "codeEditorFont": "Fonte do Editor de Código", - "codeEditorFontScale": "Tamanho da Fonte do Editor de Código", - "dialogWidth": "Largura da Caixa de Texto", - "dialogWidths": { - "default": "Padrão", - "wider": "Larga", - "widest": "Mais Larga" - }, - "editorCursorBlinks": "Cursor Piscante nos Editores", - "fontExplanation": "Alterar a fonte aqui afetará apenas o editor do Twine. A fonte usada na história não será alterada.", - "language": "Idioma", - "passageEditorFont": "Fonte do Editor de Passagem", - "passageEditorFontScale": "Tamanho da Fonte do Editor de Passagem", - "themeLight": "Claro", - "themeDark": "Escuro", - "themeSystem": "Sistema", - "theme": "Tema", - "title": "Preferências" - }, - "passageEdit": { - "editorCrashed": "Algo deu errado com o editor. Tente fechá-lo e editar a passagem novamente.", - "passageTextEditorLabel": "Texto da Passagem", - "passageTextPlaceholder": "Escreva o texto da passagem aqui. Para ligá-lo a outra passagem, coloque dois colchetes ao redor do nome, [[desta maneira]].", - "setAsStart": "Começar a História Aqui", - "size": "Tamanho", - "sizeLarge": "Grande", - "sizeSmall": "Pequena", - "sizeTall": "Alta", - "sizeWide": "Larga" - }, - "passageTags": { - "noTags": "Ainda não foram adicionadas etiquetas a passagens nesta história.", - "title": "Etiquetas das Passagens" - }, - "storyImport": { - "deselectAll": "Desmarcar Tudo", - "filePrompt": "Para importar histórias ao Twine, carregue um arquivo, história publicada ou arquivo Twee abaixo.", - "importDifferentFile": "Importar um Outro Arquivo", - "importSelected": "Importar os Arquivos Marcados", - "importThisStory": "Importar Esta História", - "noStoriesInFile": "Parece que não há histórias Twine no arquivo carregado. Por favor, escolha outro arquivo.", - "storiesPrompt": "Escolha as histórias que deseja importar:", - "title": "Importar Histórias", - "willReplaceExisting": "Uma história da biblioteca com o mesmo título será substituída." - }, - "storyDetails": { - "storyFormatExplanation": "O que é um formato de história?", - "snapToGrid": "Alinhar à Grade", - "stats": { - "brokenLinks": "Ligações Cortadas", - "characters": "Caracteres", - "title": "Detalhes da História", - "ifid": "O IFID desta história é {{ifid}}.", - "ifidExplanation": "O que é um IFID?", - "lastUpdate": "Esta história foi alterada pela última vez em {{date}}.", - "links": "Ligações", - "passages": "Passagens", - "words": "Palavras" - } - }, - "storyJavaScript": { - "editorLabel": "JavaScript da História", - "title": "JavaScript da História", - "explanation": "O código JavaScript inserido aqui será reproduzido assim que a história for aberta num navegador." - }, - "storySearch": { - "title": "Encontrar e Substituir", - "find": "Encontrar", - "includePassageNames": "Incluir os Nomes das Passagens", - "matchCase": "Sensível a Maiúsculas/Minúsculas", - "matchCount": "{{count}} passagem correspondente", - "matchCount_plural": "{{count}} passagens correspondentes", - "noMatches": "Nenhuma passagem correspondente", - "replaceAll": "Substituir em Todas as Passagens", - "replaceWith": "Substituir por", - "useRegexes": "Usar Expressões Regulares" - }, - "storyStylesheet": { - "editorLabel": "Folha de Estilos da História", - "title": "Folha de Estilos da História", - "explanation": "Qualquer CSS inserido aqui irá sobrescrever a aparência padrão da sua história." - }, - "storyTags": { - "noTags": "Ainda não foram adicionadas etiquetas às suas histórias.", - "title": "Etiquetas de História" - } - }, + "aboutTwine": { + "donateToTwine": "Ajude o Twine a Crescer com uma Doação", + "codeHeader": "Código", + "codeRepo": "Visitar o Repositório do Código-Fonte", + "license": "Esta aplicação está publicada de acordo com a licença GPL v3, mas qualquer obra criada pode ser publicada de acordo com quaisquer outras condições, incluindo comerciais.", + "localizationHeader": "Localizações", + "title": "Sobre o Twine {{version}}", + "twineDescription": "O Twine é uma aplicação de código-fonte aberto para contar histórias interativas e não lineares." + }, + "appDonation": { + "donate": "Doar para o Desenvolvimento do Twine", + "onlyOnce": "(Esta mensagem será exibida apenas uma vez. Caso queira fazer uma doação para o desenvolvimento do Twine, siga o link na caixa \"Sobre o Twine\".)", + "supportMessage": "Se você ama o Twine, pedimos que o ajude a crescer fazendo uma doação. O Twine é um projeto de código-fonte aberto que será sempre gratuito — e graças à sua ajuda, o Twine poderá continuar crescendo.", + "noThanks": "Não, Obrigado", + "title": "Apoiar o Desenvolvimento do Twine" + }, + "appPrefs": { + "codeEditorFont": "Fonte do Editor de Código", + "codeEditorFontScale": "Tamanho da Fonte do Editor de Código", + "dialogWidth": "Largura da Caixa de Texto", + "dialogWidths": { + "default": "Padrão", + "wider": "Larga", + "widest": "Mais Larga" + }, + "editorCursorBlinks": "Cursor Piscante nos Editores", + "fontExplanation": "Alterar a fonte aqui afetará apenas o editor do Twine. A fonte usada na história não será alterada.", + "language": "Idioma", + "passageEditorFont": "Fonte do Editor de Passagem", + "passageEditorFontScale": "Tamanho da Fonte do Editor de Passagem", + "themeLight": "Claro", + "themeDark": "Escuro", + "themeSystem": "Sistema", + "theme": "Tema", + "title": "Preferências" + }, + "passageEdit": { + "editorCrashed": "Algo deu errado com o editor. Tente fechá-lo e editar a passagem novamente.", + "passageTextEditorLabel": "Texto da Passagem", + "passageTextPlaceholder": "Escreva o texto da passagem aqui. Para ligá-lo a outra passagem, coloque dois colchetes ao redor do nome, [[desta maneira]].", + "setAsStart": "Começar a História Aqui", + "size": "Tamanho", + "sizeLarge": "Grande", + "sizeSmall": "Pequena", + "sizeTall": "Alta", + "sizeWide": "Larga" + }, + "passageTags": { + "noTags": "Ainda não foram adicionadas etiquetas a passagens nesta história.", + "title": "Etiquetas das Passagens" + }, + "storyFormats": { + "addStoryFormatButton": { + "addPreview": "O formato {{storyFormatName}} {{storyFormatVersion}} será adicionado.", + "alreadyAdded": "O formato {{storyFormatName}} {{storyFormatVersion}} já foi adicionado. ", + "fetchError": "O formato de história nesse endereço não pôde ser alcançado ({{errorMessage}}).", + "invalidUrl": "Insira um URL válido.", + "prompt": "Para adicionar um formato de história, insira o seu endereço abaixo." + }, + "filterButton": { + "all": "Todos os Formatos de História", + "current": "Formatos de História Atuais", + "user": "Formatos de História do Usuário" + }, + "noneVisible": "Não há formatos de história que correspondam aos critérios escolhidos." + }, + "storyImport": { + "deselectAll": "Desmarcar Tudo", + "filePrompt": "Para importar histórias ao Twine, carregue um arquivo, história publicada ou arquivo Twee abaixo.", + "importDifferentFile": "Importar um Outro Arquivo", + "importSelected": "Importar os Arquivos Marcados", + "importThisStory": "Importar Esta História", + "noStoriesInFile": "Parece que não há histórias Twine no arquivo carregado. Por favor, escolha outro arquivo.", + "storiesPrompt": "Escolha as histórias que deseja importar:", + "title": "Importar Histórias", + "willReplaceExisting": "Uma história da biblioteca com o mesmo título será substituída." + }, + "storyDetails": { + "storyFormatExplanation": "O que é um formato de história?", + "snapToGrid": "Alinhar à Grade", + "stats": { + "brokenLinks": "Ligações Cortadas", + "characters": "Caracteres", + "title": "Detalhes da História", + "ifid": "O IFID desta história é {{ifid}}.", + "ifidExplanation": "O que é um IFID?", + "lastUpdate": "Esta história foi alterada pela última vez em {{date}}.", + "links": "Ligações", + "passages": "Passagens", + "words": "Palavras" + } + }, + "storyJavaScript": { + "editorLabel": "JavaScript da História", + "title": "JavaScript da História", + "explanation": "O código JavaScript inserido aqui será reproduzido assim que a história for aberta num navegador." + }, + "storySearch": { + "title": "Encontrar e Substituir", + "find": "Encontrar", + "includePassageNames": "Incluir os Nomes das Passagens", + "matchCase": "Sensível a Maiúsculas/Minúsculas", + "matchCount": "{{count}} passagem correspondente", + "matchCount_plural": "{{count}} passagens correspondentes", + "noMatches": "Nenhuma passagem correspondente", + "replaceAll": "Substituir em Todas as Passagens", + "replaceWith": "Substituir por", + "useRegexes": "Usar Expressões Regulares" + }, + "storyStylesheet": { + "editorLabel": "Folha de Estilos da História", + "title": "Folha de Estilos da História", + "explanation": "Qualquer CSS inserido aqui irá sobrescrever a aparência padrão da sua história." + }, + "storyTags": { + "noTags": "Ainda não foram adicionadas etiquetas às suas histórias.", + "title": "Etiquetas de História" + } + }, "electron": { "backupsDirectoryName": "Cópias de Segurança", "errors": { @@ -295,102 +309,79 @@ } }, "routes": { - "storyEdit": { - "toolbar": { - "findAndReplace": "Encontrar e Substituir", - "goTo": "Ir Para", - "javaScript": "JavaScript", - "passageTags": "Etiquetas das Passagens", - "snapToGrid": "Alinhar à Grade", - "startStoryHere": "Começar a História Aqui", - "stylesheet": "Folha de Estilos", - "testFromHere": "Testar a Partir Daqui" - }, - "topBar": { - "editJavaScript": "Editar o JavaScript da História", - "editStylesheet": "Editar a Folha de Estilos da História", - "findAndReplace": "Encontrar e Substituir", - "passageTags": "Editar as Etiquetas das Passagens", - "proofStory": "Ver uma Cópia para Revisão", - "publishToFile": "Publicar para Arquivo", - "selectAllPassages": "Marcar Todas as Passagens" - }, - "zoomButtons": { - "legend": "Zoom", - "storyStructure": "Mostrar Apenas a Estrutura da História", - "passageNames": "Mostrar Apenas os Títulos das Passagens", - "passageNamesAndExcerpts": "Mostrar os Títulos das Passagens e os Excertos" - } - }, - "storyFormatList": { - "noneVisible": "Não há formatos de história que correspondam aos critérios escolhidos.", - "show": "Mostrar...", - "title": { - "all": "Todos os Formatos de História", - "current": "Formatos de História Atuais", - "user": "Formatos de História do Usuário" - }, - "toolbar": { - "addStoryFormatButton": { - "addPreview": "O formato {{storyFormatName}} {{storyFormatVersion}} será adicionado.", - "alreadyAdded": "O formato {{storyFormatName}} {{storyFormatVersion}} já foi adicionado. ", - "fetchError": "O formato de história nesse endereço não pôde ser alcançado ({{errorMessage}}).", - "invalidUrl": "Insira um URL válido.", - "prompt": "Para adicionar um formato de história, insira o seu endereço abaixo." - }, - "disableFormatExtensions": "Desligar as Extensões do Editor", - "enableFormatExtensions": "Ligar as Extensões do Editor", - "useAsDefaultFormat": "Usar como Formato Padrão", - "useAsProofingFormat": "Usar para Revisar Histórias" - }, - "storyFormatExplanation": "Os formatos de história controlam a aparência e o comportamento das histórias durante o jogo." - }, - "storyList": { - "library": "Biblioteca", - "noStories": "Não há histórias salvas no Twine no momento. Para começar, você pode criar uma nova história ou importar uma já existente de um arquivo.", - "taggedTitleCount": "1 História Etiquetada", - "taggedTitleCount_0": "Não Há Histórias Etiquetadas", - "taggedTitleCount_plural": "{{count}} Histórias Etiquetadas", - "titleCount": "1 História", - "titleCount_0": "Não há Histórias", - "titleCount_plural": "{{count}} Histórias", - "titleGeneric": "Histórias", - "toolbar": { - "archive": "Arquivar", - "createStoryButton": { - "prompt": "Que título deseja dar à sua história? Você poderá mudá-lo mais tarde.", - "emptyName": "Insira um título.", - "nameConflict": "Já existe uma história com esse título." - }, - "deleteStoryButton": { - "warning": { - "electron": "Tem certeza que deseja apagar a história “{{storyName}}”? Ela será movida para a lixeira.", - "web": "Tem certeza que deseja apagar a história “{{storyName}}”? Ela será excluída permanentemente. Você não poderá reverter esta decisão." - } - }, - "showAllStories": "Mostrar Todas as Histórias", - "showTags": "Mostrar Etiquetas", - "sort": "Ordenar por", - "sortByDate": "Última Atualização", - "sortByName": "Título", - "storyTags": "Etiquetas de História" - } - }, - "welcome": { - "autosave": "

Agora você tem uma pasta chamada Twine na sua pasta Documentos. Dentro dela há uma pasta intitulada Stories, onde as suas histórias serão salvas. O Twine salva a história à medida que você trabalha, então não se preocupe em salvá-la. Você pode a qualquer momento abrir a pasta onde as suas histórias são salvas em Mostrar Biblioteca, no menu do Twine.

Como o Twine está sempre salvando o seu trabalho, os arquivos da sua biblioteca de histórias estarão bloqueados e não poderão ser editados enquanto o Twine estiver aberto.

Caso queira abrir um arquivo de uma história Twine que você recebeu de alguém, será possível importá-lo para a sua biblioteca em Importar de Ficheiro, na lista de histórias.

", - "autosaveTitle": "O seu trabalho é salvo automaticamente.", - "browserStorage": "

Isso significa que você não precisa criar uma conta para usar o Twine 2, e tudo o que criar não ficará salvo em um servidor sabe-se lá onde—fica sempre aqui no seu navegador.

Mas não se esqueça de duas coisas muito importantes. Como o seu trabalho fica salvo apenas no seu navegador, tudo será perdido se você limpar os dados salvos! Não queremos isso. Lembre-se de usar frequentemente o botão Arquivar. Você também pode publicar histórias individuais como arquivos, usando o menu em cada história na lista de histórias. Tanto os arquivos como os arquivos de história podem ser importados de volta para o Twine a qualquer momento.

Segundo, qualquer pessoa com acesso a este navegador poderá ver as suas histórias e alterá-las. Portanto, caso tenha um irmão enxerido em casa, talvez seja uma boa ideia criar um perfil separado, só seu.

", - "browserStorageTitle": "O seu trabalho fica salvo somente no seu navegador", - "done": "

Agradecemos a leitura - divirta-se com o Twine.

", - "doneTitle": "Pronto!", - "gotoStoryList": "Ir para a Lista de Histórias", - "greeting": "

O Twine é uma ferramenta de código-fonte aberto para contar histórias interativas e não lineares. Tem algumas coisas que você precisa saber antes de começar.

", - "greetingTitle": "Olá!", - "tellMeMore": "Quero Saber Mais", - "help": "

Se esta é a sua primeira vez no Twine, deixe-me dar as boas-vindas! O Livro de Receitas do Twine é um ótimo recurso para aprender a usar o Twine. Se você nunca usou o Twine, esse é um bom lugar para começar.

", - "helpTitle": "É a sua primeira vez aqui?" - } - }, + "storyEdit": { + "toolbar": { + "findAndReplace": "Encontrar e Substituir", + "goTo": "Ir Para", + "javaScript": "JavaScript", + "passageTags": "Etiquetas das Passagens", + "snapToGrid": "Alinhar à Grade", + "startStoryHere": "Começar a História Aqui", + "stylesheet": "Folha de Estilos", + "testFromHere": "Testar a Partir Daqui" + }, + "topBar": { + "editJavaScript": "Editar o JavaScript da História", + "editStylesheet": "Editar a Folha de Estilos da História", + "findAndReplace": "Encontrar e Substituir", + "passageTags": "Editar as Etiquetas das Passagens", + "proofStory": "Ver uma Cópia para Revisão", + "publishToFile": "Publicar para Arquivo", + "selectAllPassages": "Marcar Todas as Passagens" + }, + "zoomButtons": { + "legend": "Zoom", + "storyStructure": "Mostrar Apenas a Estrutura da História", + "passageNames": "Mostrar Apenas os Títulos das Passagens", + "passageNamesAndExcerpts": "Mostrar os Títulos das Passagens e os Excertos" + } + }, + "storyList": { + "library": "Biblioteca", + "noStories": "Não há histórias salvas no Twine no momento. Para começar, você pode criar uma nova história ou importar uma já existente de um arquivo.", + "taggedTitleCount": "1 História Etiquetada", + "taggedTitleCount_0": "Não Há Histórias Etiquetadas", + "taggedTitleCount_plural": "{{count}} Histórias Etiquetadas", + "titleCount": "1 História", + "titleCount_0": "Não há Histórias", + "titleCount_plural": "{{count}} Histórias", + "titleGeneric": "Histórias", + "toolbar": { + "archive": "Arquivar", + "createStoryButton": { + "prompt": "Que título deseja dar à sua história? Você poderá mudá-lo mais tarde.", + "emptyName": "Insira um título.", + "nameConflict": "Já existe uma história com esse título." + }, + "deleteStoryButton": { + "warning": { + "electron": "Tem certeza que deseja apagar a história “{{storyName}}”? Ela será movida para a lixeira.", + "web": "Tem certeza que deseja apagar a história “{{storyName}}”? Ela será excluída permanentemente. Você não poderá reverter esta decisão." + } + }, + "showAllStories": "Mostrar Todas as Histórias", + "showTags": "Mostrar Etiquetas", + "sort": "Ordenar por", + "sortByDate": "Última Atualização", + "sortByName": "Título", + "storyTags": "Etiquetas de História" + } + }, + "welcome": { + "autosave": "

Agora você tem uma pasta chamada Twine na sua pasta Documentos. Dentro dela há uma pasta intitulada Stories, onde as suas histórias serão salvas. O Twine salva a história à medida que você trabalha, então não se preocupe em salvá-la. Você pode a qualquer momento abrir a pasta onde as suas histórias são salvas em Mostrar Biblioteca, no menu do Twine.

Como o Twine está sempre salvando o seu trabalho, os arquivos da sua biblioteca de histórias estarão bloqueados e não poderão ser editados enquanto o Twine estiver aberto.

Caso queira abrir um arquivo de uma história Twine que você recebeu de alguém, será possível importá-lo para a sua biblioteca em Importar de Ficheiro, na lista de histórias.

", + "autosaveTitle": "O seu trabalho é salvo automaticamente.", + "browserStorage": "

Isso significa que você não precisa criar uma conta para usar o Twine 2, e tudo o que criar não ficará salvo em um servidor sabe-se lá onde—fica sempre aqui no seu navegador.

Mas não se esqueça de duas coisas muito importantes. Como o seu trabalho fica salvo apenas no seu navegador, tudo será perdido se você limpar os dados salvos! Não queremos isso. Lembre-se de usar frequentemente o botão Arquivar. Você também pode publicar histórias individuais como arquivos, usando o menu em cada história na lista de histórias. Tanto os arquivos como os arquivos de história podem ser importados de volta para o Twine a qualquer momento.

Segundo, qualquer pessoa com acesso a este navegador poderá ver as suas histórias e alterá-las. Portanto, caso tenha um irmão enxerido em casa, talvez seja uma boa ideia criar um perfil separado, só seu.

", + "browserStorageTitle": "O seu trabalho fica salvo somente no seu navegador", + "done": "

Agradecemos a leitura - divirta-se com o Twine.

", + "doneTitle": "Pronto!", + "gotoStoryList": "Ir para a Lista de Histórias", + "greeting": "

O Twine é uma ferramenta de código-fonte aberto para contar histórias interativas e não lineares. Tem algumas coisas que você precisa saber antes de começar.

", + "greetingTitle": "Olá!", + "tellMeMore": "Quero Saber Mais", + "help": "

Se esta é a sua primeira vez no Twine, deixe-me dar as boas-vindas! O Livro de Receitas do Twine é um ótimo recurso para aprender a usar o Twine. Se você nunca usou o Twine, esse é um bom lugar para começar.

", + "helpTitle": "É a sua primeira vez aqui?" + } + }, "routeActions": { "app": { "aboutApp": "Sobre o Twine", diff --git a/public/locales/pt-PT.json b/public/locales/pt-PT.json index 2bf899847..c55301777 100644 --- a/public/locales/pt-PT.json +++ b/public/locales/pt-PT.json @@ -118,11 +118,10 @@ "passageCount": "1 passagem", "passageCount_plural": "{{count}} passagens" }, - "storyFormatCard": { + "storyFormatItem": { "author": "de {{author}}", "builtIn": "Criado em", "defaultFormat": "Usado por Defeito", - "editorExtensionsDisabled": "Extensões do Editor Desligadas", "license": "Licença: {{license}}", "loadingFormat": "A carregar o formato de história...", "loadError": "O formato de história não pôde ser carregado. Deu este erro: ({{errorMessage}}).", @@ -166,7 +165,7 @@ "default": "Padrão", "wider": "Larga", "widest": "Mais Larga" - }, + }, "editorCursorBlinks": "O Cursor Pisca nos Editores", "fontExplanation": "Alterar a fonte aqui, afeta apenas o editor do Twine. A fonte usada na história não será alterada.", "language": "Língua", @@ -193,6 +192,21 @@ "noTags": "Ainda não foram adicionadas etiquetas a passagens nesta história.", "title": "Etiquetas das Passagens" }, + "storyFormats": { + "addStoryFormatButton": { + "addPreview": "O formato {{storyFormatName}} {{storyFormatVersion}} vai ser acrescentado.", + "alreadyAdded": "O formato {{storyFormatName}} {{storyFormatVersion}} já foi acrescentado. ", + "fetchError": "O formato de história no endereço apresentado não pôde ser descarregado: ({{errorMessage}}).", + "invalidUrl": "Introduz um URL válido.", + "prompt": "Para acrescentares um formato de história, introduz o seu endereço em baixo." + }, + "filterButton": { + "all": "Todos os Formatos de História", + "current": "Formatos de História Disponíveis", + "user": "Formatos de História Adicionados pelo Utilizador" + }, + "noneVisible": "Não há formatos de história que correspondam aos critérios escolhidos." + }, "storyImport": { "deselectAll": "Desmarcar Tudo", "filePrompt": "Para importar histórias para o Twine, carrega, em baixo, um ficheiro de arquivo, um ficheiro de uma história publicada ou um ficheiro no formato Twee.", @@ -298,7 +312,7 @@ "storyEdit": { "toolbar": { "findAndReplace": "Encontrar e Substituir", - "goTo": "Ir Para", + "goTo": "Ir Para", "javaScript": "JavaScript", "passageTags": "Etiquetas das Passagens", "snapToGrid": "Alinhar à Grelha", @@ -316,35 +330,12 @@ "selectAllPassages": "Marcar Todas as Passagens" }, "zoomButtons": { - "legend": "Ampliação", + "legend": "Ampliação", "storyStructure": "Mostrar apenas a Estrutura da História", "passageNames": "Mostrar apenas os Títulos das Passagens", "passageNamesAndExcerpts": "Mostrar os Títulos das Passagens e os Excertos" } }, - "storyFormatList": { - "noneVisible": "Não há formatos de história que correspondam aos critérios escolhidos.", - "show": "Mostrar...", - "title": { - "all": "Todos os Formatos de História", - "current": "Formatos de História Disponíveis", - "user": "Formatos de História Adicionados pelo Utilizador" - }, - "toolbar": { - "addStoryFormatButton": { - "addPreview": "O formato {{storyFormatName}} {{storyFormatVersion}} vai ser acrescentado.", - "alreadyAdded": "O formato {{storyFormatName}} {{storyFormatVersion}} já foi acrescentado. ", - "fetchError": "O formato de história no endereço apresentado não pôde ser descarregado: ({{errorMessage}}).", - "invalidUrl": "Introduz um URL válido.", - "prompt": "Para acrescentares um formato de história, introduz o seu endereço em baixo." - }, - "disableFormatExtensions": "Desligar as Extensões do Editor", - "enableFormatExtensions": "Ligar as Extensões do Editor", - "useAsDefaultFormat": "Usar como Formato por Defeito", - "useAsProofingFormat": "Usar para a Revisão das Histórias" - }, - "storyFormatExplanation": "Os formatos de história controlam o aspeto visual e o comportamento das histórias durante o jogo." - }, "storyList": { "library": "Biblioteca", "noStories": "Não há histórias gravadas no Twine neste momento. Para começares, podes criar uma nova história ou importar uma de um ficheiro.", diff --git a/public/locales/ru.json b/public/locales/ru.json index b52f84378..b867dcf99 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -33,7 +33,7 @@ "renameStoryButton": {"emptyName": "Пожалуйста, введите имя."}, "safariWarningCard": {}, "storyCard": {}, - "storyFormatCard": {}, + "storyFormatItem": {}, "storyFormatSelect": {}, "tagEditor": {} }, @@ -72,10 +72,6 @@ "publishToFile": "Опубликовать в файл" } }, - "storyFormatList": { - "title": {}, - "storyFormatExplanation": "Формат истории управляет внешним видом и поведением истории во время игры." - }, "storyImport": {}, "storyList": { "noStories": "Сейчас у вас нет сохранённых историй в Twine. Для начала, вы можете создать новую историю или импортировать уже существующую из файла.", diff --git a/public/locales/sl.json b/public/locales/sl.json index f4897c6c4..0ec06ec72 100644 --- a/public/locales/sl.json +++ b/public/locales/sl.json @@ -31,7 +31,7 @@ "renameStoryButton": {"emptyName": "Prosim, vnesite ime."}, "safariWarningCard": {}, "storyCard": {}, - "storyFormatCard": {}, + "storyFormatItem": {}, "storyFormatSelect": {}, "tagEditor": {} }, @@ -67,10 +67,6 @@ "publishToFile": "Izdaj v datoteko" } }, - "storyFormatList": { - "title": {}, - "storyFormatExplanation": "Zgodbeni formati nadzorujejo videz in obnašanje zgodb med predvajanjem." - }, "storyImport": {}, "storyList": { "noStories": "Trenutno v Twineu ni shranjenih nobenih zgodb. Lahko ali ustvarite novo zgodbo ali uvozite že obstoječo iz datoteke.", diff --git a/public/locales/sv.json b/public/locales/sv.json index 604b3007b..43cf98eae 100644 --- a/public/locales/sv.json +++ b/public/locales/sv.json @@ -33,7 +33,7 @@ "renameStoryButton": {"emptyName": "Var snäll och skriv ett namn."}, "safariWarningCard": {}, "storyCard": {}, - "storyFormatCard": {}, + "storyFormatItem": {}, "storyFormatSelect": {}, "tagEditor": {} }, @@ -72,10 +72,6 @@ "publishToFile": "Publicera till fil" } }, - "storyFormatList": { - "title": {}, - "storyFormatExplanation": "Historieformat kontrollerar utseendet och uppförandet hos historierna under spelet." - }, "storyImport": {}, "storyList": { "noStories": "Det finns inga historier sparade i Twine just nu. För att komma igång kan du antingen skapa en ny historia eller importera en existerande från en fil.", diff --git a/public/locales/tr.json b/public/locales/tr.json index b9c1bf883..a0c000491 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -117,11 +117,10 @@ "passageCount": "1 Bölüm", "passageCount_plural": "{{count}} Bölüm" }, - "storyFormatCard": { + "storyFormatItem": { "author": "{{author}} tarafından", "builtIn": "Dahili", "defaultFormat": "Varsayılan biçim.", - "editorExtensionsDisabled": "Düzenleyici Eklentileri Kapalı", "license": "Lisans: {{license}}", "loadingFormat": "Bu öykü biçimi yükleniyor...", "loadError": "Bu öykü biçimi yüklenemedi. ({{errorMessage}})", @@ -218,7 +217,22 @@ "words": "Sözcük" } }, - "storyInfo": { "stats": {"title": "Öykü İstatistikleri"}}, + "storyFormats": { + "addStoryFormatButton": { + "addPreview": "{{storyFormatName}} {{storyFormatVersion}} eklenecek.", + "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} zaten ekli.", + "fetchError": "Bu adresteki öykü biçimi elde edilemedi. ({{errorMessage}})", + "invalidUrl": "Lütfen geçerli bir URL girin.", + "prompt": "Öykü biçimi eklemek için biçimin internet adresini aşağı girin." + }, + "filterButton": { + "all": "Tüm Öylü Biçimleri", + "current": "Güncel Öykü Biçimleri", + "user": "Kendinizce Eklenen Öykü Biçimleri" + }, + "noneVisible": "Seçtiğiniz kriterlere uygun öykü biçimi bulunmamaktadır." + }, + "storyInfo": {"stats": {"title": "Öykü İstatistikleri"}}, "storyJavaScript": { "editorLabel": "Öykü JavaScript'i", "title": "Öykü JavaScript'i", @@ -304,29 +318,6 @@ "passageNamesAndExcerpts": "Bölüm Adlarını ve İçeriğin Kısımlarını Göster" } }, - "storyFormatList": { - "noneVisible": "Seçtiğiniz kriterlere uygun öykü biçimi bulunmamaktadır.", - "show": "Show...", - "title": { - "all": "Tüm Öylü Biçimleri", - "current": "Güncel Öykü Biçimleri", - "user": "Kendinizce Eklenen Öykü Biçimleri" - }, - "toolbar": { - "addStoryFormatButton": { - "addPreview": "{{storyFormatName}} {{storyFormatVersion}} eklenecek.", - "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} zaten ekli.", - "fetchError": "Bu adresteki öykü biçimi elde edilemedi. ({{errorMessage}})", - "invalidUrl": "Lütfen geçerli bir URL girin.", - "prompt": "Öykü biçimi eklemek için biçimin internet adresini aşağı girin." - }, - "disableFormatExtensions": "Düzenleyici Eklentilerini Kapat", - "enableFormatExtensions": "Düzenleyici Eklentilerini Aç", - "useAsDefaultFormat": "Varsayılan Biçim Olarak Kullan", - "useAsProofingFormat": "Öykü İmlası için Kullan" - }, - "storyFormatExplanation": "Öykü biçimleri öykünün okuma sırasında nasıl göründüğünü ve davrandığını belirler." - }, "storyList": { "library": "Kitaplık", "noStories": "Şu an Twine'da kayıtlı hiçbir öykü yok. Başlamak için yeni bir öykü yaratın veya var olan bir öyküyü içe aktarın.", diff --git a/public/locales/uk.json b/public/locales/uk.json index 210292ff4..27c102cbb 100644 --- a/public/locales/uk.json +++ b/public/locales/uk.json @@ -115,11 +115,10 @@ "passageCount_1": "{{count}} параграфи", "passageCount_2": "{{count}} параграфів" }, - "storyFormatCard": { + "storyFormatItem": { "author": "Автор: {{author}}", "builtIn": "Вбудований", "defaultFormat": "Використовується за замовчуванням", - "editorExtensionsDisabled": "Розширення редактора вимкнено", "license": "Ліцензія: {{license}}", "loadingFormat": "Завантаження формату оповідання...", "loadError": "Цей формат оповідання не вийшло завантажити: ({{errorMessage}}).", @@ -191,6 +190,21 @@ "noTags": "У параграфів в цьому оповіданні немає міток.", "title": "Мітки параграфів" }, + "storyFormats": { + "addStoryFormatButton": { + "addPreview": "Буде додано формат {{storyFormatName}} {{storyFormatVersion}}.", + "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} вже було додано.", + "fetchError": "Не вийшло завантажити формат оповідання за цією адресою ({{errorMessage}}).", + "invalidUrl": "Введіть коректний URL.", + "prompt": "Щоб додати формат оповідання, введіть його адресу." + }, + "filterButton": { + "all": "Всі формати оповідань", + "current": "Формати цього оповідання", + "user": "Формати, додані користувачами" + }, + "noneVisible": "Немає форматів оповідання, що підходять під обрані вами критерії." + }, "storyImport": { "deselectAll": "Зняти виділення", "filePrompt": "Ви можете імпортувати оповідання в Twine з файлу архіву або опублікованого оповідання.", @@ -303,29 +317,6 @@ "passageNamesAndExcerpts": "Показати назви параграфів та уривки" } }, - "storyFormatList": { - "noneVisible": "Немає форматів оповідання, що підходять під обрані вами критерії.", - "show": "Показати...", - "title": { - "all": "Всі формати оповідань", - "current": "Формати цього оповідання", - "user": "Формати, додані користувачами" - }, - "toolbar": { - "addStoryFormatButton": { - "addPreview": "Буде додано формат {{storyFormatName}} {{storyFormatVersion}}.", - "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} вже було додано.", - "fetchError": "Не вийшло завантажити формат оповідання за цією адресою ({{errorMessage}}).", - "invalidUrl": "Введіть коректний URL.", - "prompt": "Щоб додати формат оповідання, введіть його адресу." - }, - "disableFormatExtensions": "Вимкнути розширення редактора", - "enableFormatExtensions": "Ввімкнути розширення редактора", - "useAsDefaultFormat": "Використовувати як формат за замовчуванням", - "useAsProofingFormat": "Використовувати для вичитки" - }, - "storyFormatExplanation": "Формат оповідання контролює вигляд та поведінку оповідань під час відтворення." - }, "storyList": { "library": "Бібліотека", "noStories": "Зараз в Twine немає оповідань. Ви можете створити нове оповідання або імпортувати вже існуюче з файлу.", diff --git a/public/locales/zh-CN.json b/public/locales/zh-CN.json index 4d3e581f4..9beee7d19 100644 --- a/public/locales/zh-CN.json +++ b/public/locales/zh-CN.json @@ -118,11 +118,10 @@ "passageCount": "{{count}}个片段", "passageCount_plural": "{{count}} 个片段" }, - "storyFormatCard": { + "storyFormatItem": { "author": "来自 {{author}}", "builtIn": "内置", "defaultFormat": "设为默认", - "editorExtensionsDisabled": "编辑器扩展被禁用", "license": "许可证:{{license}}", "loadingFormat": "载入故事格式中……", "loadError": "故事格式加载失败({{errorMessage}})。", @@ -193,6 +192,21 @@ "noTags": "尚未向故事中的片段添加任何标签。", "title": "片段标签" }, + "storyFormats": { + "addStoryFormatButton": { + "addPreview": "将添加 {{storyFormatName}} {{storyFormatVersion}}。", + "alreadyAdded": "已添加 {{storyFormatName}} {{storyFormatVersion}}。", + "fetchError": "无法读取该位置的故事格式({{errorMessage}})。", + "invalidUrl": "请输入有效的 URL。", + "prompt": "要添加故事格式,请在下方输入地址。" + }, + "filterButton": { + "all": "全部故事格式", + "current": "当前故事格式", + "user": "用户添加的故事格式" + }, + "noneVisible": "没有符合您筛选标准的故事格式。" + }, "storyImport": { "deselectAll": "全不选", "filePrompt": "要向 Twine 导入故事,请先在下方上传一个档案或已发布的故事文件。", @@ -305,29 +319,6 @@ "passageNamesAndExcerpts": "显示片段名称与摘要" } }, - "storyFormatList": { - "noneVisible": "没有符合您筛选标准的故事格式。", - "show": "显示……", - "title": { - "all": "全部故事格式", - "current": "当前故事格式", - "user": "用户添加的故事格式" - }, - "toolbar": { - "addStoryFormatButton": { - "addPreview": "将添加 {{storyFormatName}} {{storyFormatVersion}}。", - "alreadyAdded": "已添加 {{storyFormatName}} {{storyFormatVersion}}。", - "fetchError": "无法读取该位置的故事格式({{errorMessage}})。", - "invalidUrl": "请输入有效的 URL。", - "prompt": "要添加故事格式,请在下方输入地址。" - }, - "disableFormatExtensions": "禁用编辑器扩展", - "enableFormatExtensions": "启用编辑器扩展", - "useAsDefaultFormat": "设为默认格式", - "useAsProofingFormat": "用于校对故事" - }, - "storyFormatExplanation": "故事格式用于控制游戏过程中的故事外观和行为。" - }, "storyList": { "library": "库", "noStories": "在 Twine 中没有保存的故事。您可以创建一个新故事或从文件导入现有的故事。", diff --git a/src/app.tsx b/src/app.tsx index fafff6818..55ec8c06c 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -10,23 +10,20 @@ import {StateLoader} from './store/state-loader'; import {ThemeSetter} from './store/theme-setter'; import './styles/typography.css'; -export const App: React.FC = () => { - console.log('what'); - return ( - - - - - - - - }> - - - - - - - - ); -}; +export const App: React.FC = () => ( + + + + + + + + }> + + + + + + + +); diff --git a/src/components/container/dialog-card/dialog-card.css b/src/components/container/dialog-card/dialog-card.css index 14c55c86e..3889a7e5e 100644 --- a/src/components/container/dialog-card/dialog-card.css +++ b/src/components/container/dialog-card/dialog-card.css @@ -47,17 +47,7 @@ font-weight: normal; } -.dialog-card .card-body { - display: flex; - flex-direction: column; - padding: 0; -} - -.dialog-card .card-body h2 { - font: bold 100% var(--font-system); -} - .dialog-card .card-content { - min-height: 0; + flex: 1 1 content; overflow: auto; } \ No newline at end of file diff --git a/src/components/story-format/add-story-format-button.tsx b/src/components/story-format/add-story-format-button.tsx deleted file mode 100644 index f4a8cbcd5..000000000 --- a/src/components/story-format/add-story-format-button.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import * as React from 'react'; -import {useTranslation} from 'react-i18next'; -import {IconPlus} from '@tabler/icons'; -import {PromptButton, PromptButtonValidator} from '../control/prompt-button'; -import {StoryFormat, StoryFormatProperties} from '../../store/story-formats'; -import './add-story-format-button.css'; -import {fetchStoryFormatProperties} from '../../util/story-format/fetch-properties'; - -export interface AddStoryFormatButtonProps { - existingFormats: StoryFormat[]; - onAddFormat: ( - formatUrl: string, - formatProperties: StoryFormatProperties - ) => void; -} - -function isValidUrl(value: string) { - try { - new URL(value); - return true; - } catch (e) { - /* empty */ - } - - return false; -} - -export const AddStoryFormatButton: React.FC< - AddStoryFormatButtonProps -> = props => { - const [newFormatUrl, setNewFormatUrl] = React.useState(''); - const {t} = useTranslation(); - - async function handleSubmit() { - props.onAddFormat( - newFormatUrl, - await fetchStoryFormatProperties(newFormatUrl) - ); - } - - const validate: PromptButtonValidator = async (value: string) => { - if (value.trim() === '') { - return {valid: false}; - } - - if (!isValidUrl(value)) { - return { - message: t('components.addStoryFormatButton.invalidUrl'), - valid: false - }; - } - - try { - const properties = await fetchStoryFormatProperties(value); - - if ( - props.existingFormats.some( - format => - format.name === properties.name && - format.version === properties.version - ) - ) { - return { - message: t('components.addStoryFormatButton.alreadyAdded', { - storyFormatName: properties.name, - storyFormatVersion: properties.version - }), - valid: false - }; - } - - return { - message: t('components.addStoryFormatButton.addPreview', { - storyFormatName: properties.name, - storyFormatVersion: properties.version - }), - valid: true - }; - } catch (error) { - return { - message: t('components.addStoryFormatButton.fetchError', { - errorMessage: (error as Error).message - }), - valid: false - }; - } - }; - - return ( - - } - label={t('common.storyFormat')} - onChange={event => setNewFormatUrl(event.target.value)} - onSubmit={handleSubmit} - prompt={t('components.addStoryFormatButton.prompt')} - submitIcon={} - submitLabel={t('common.add')} - submitVariant="create" - validate={validate} - value={newFormatUrl} - /> - - ); -}; diff --git a/src/components/story-format/story-format-card/__tests__/story-format-card.test.tsx b/src/components/story-format/story-format-card/__tests__/story-format-card.test.tsx deleted file mode 100644 index 606e18bbf..000000000 --- a/src/components/story-format/story-format-card/__tests__/story-format-card.test.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import {fireEvent, render, screen} from '@testing-library/react'; -import {axe} from 'jest-axe'; -import * as React from 'react'; -import {fakeLoadedStoryFormat} from '../../../../test-util'; -import {StoryFormatCard, StoryFormatCardProps} from '../story-format-card'; - -describe('', () => { - function renderComponent(props?: Partial) { - return render( - - ); - } - - it.todo('shows a loading icon if the format is loading'); - it.todo('shows an error icon if the format failed to load'); - - describe('when the format is loaded', () => { - it('displays the format name and version', () => { - const format = fakeLoadedStoryFormat(); - - renderComponent({format}); - expect( - screen.getByText('components.storyFormatCard.name') - ).toBeInTheDocument(); - }); - - it('calls the onSelect prop when the card is clicked', () => { - const onSelect = jest.fn(); - - renderComponent({onSelect}); - expect(onSelect).not.toHaveBeenCalled(); - fireEvent.click(screen.getByText('components.storyFormatCard.name')); - expect(onSelect).toBeCalledTimes(1); - }); - - it("shows a badge if the format isn't user-added", () => { - const format = fakeLoadedStoryFormat(); - - format.userAdded = false; - renderComponent({format}); - expect( - screen.getByText('components.storyFormatCard.builtIn') - ).toBeInTheDocument(); - }); - - it("doesn't show that badge if the format is user-added", () => { - const format = fakeLoadedStoryFormat(); - - format.userAdded = true; - renderComponent({format}); - expect( - screen.queryByText('components.storyFormatCard.builtIn') - ).not.toBeInTheDocument(); - }); - - it('shows a badge if the format is the default format', () => { - const format = fakeLoadedStoryFormat(); - - renderComponent({format, defaultFormat: true}); - expect( - screen.getByText('components.storyFormatCard.defaultFormat') - ).toBeInTheDocument(); - }); - - it("doesn't show that badge if the format isn't the default format", () => { - const format = fakeLoadedStoryFormat(); - - renderComponent({format, defaultFormat: false}); - expect( - screen.queryByText('components.storyFormatCard.defaultFormat') - ).not.toBeInTheDocument(); - }); - - it('shows a badge if the format is a proofing format', () => { - const format = fakeLoadedStoryFormat(); - - (format as any).properties.proofing = true; - renderComponent({format}); - expect( - screen.getByText('components.storyFormatCard.proofing') - ).toBeInTheDocument(); - }); - - it("doesn't show the same badge if the format is not proofing", () => { - const format = fakeLoadedStoryFormat(); - - (format as any).properties.proofing = false; - renderComponent({format}); - expect( - screen.queryByText('components.storyFormatCard.proofing') - ).not.toBeInTheDocument(); - }); - - it('shows a badge if the format is the preferred proofing format', () => { - const format = fakeLoadedStoryFormat(); - - (format as any).properties.proofing = true; - renderComponent({format, proofingFormat: true}); - expect( - screen.getByText('components.storyFormatCard.proofingFormat') - ).toBeInTheDocument(); - }); - - it("doesn't show the same badge if the format is not the preferred proofing format", () => { - const format = fakeLoadedStoryFormat(); - - (format as any).properties.proofing = true; - renderComponent({format, proofingFormat: false}); - expect( - screen.queryByText('components.storyFormatCard.proofingFormat') - ).not.toBeInTheDocument(); - }); - }); - - it('is accessible', async () => { - const {container} = renderComponent(); - - expect(await axe(container)).toHaveNoViolations(); - }); -}); diff --git a/src/components/story-format/story-format-card/index.ts b/src/components/story-format/story-format-card/index.ts deleted file mode 100644 index 7846e4408..000000000 --- a/src/components/story-format/story-format-card/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './story-format-card'; diff --git a/src/components/story-format/story-format-card/story-format-card.css b/src/components/story-format/story-format-card/story-format-card.css deleted file mode 100644 index 34b23eb66..000000000 --- a/src/components/story-format/story-format-card/story-format-card.css +++ /dev/null @@ -1,29 +0,0 @@ -@import '../../../styles/metrics.css'; - -.story-format-card { - user-select: none; -} - -.story-format-card .card-content { - display: grid; - flex-grow: 1; - grid-gap: var(--grid-size); - grid-template-columns: 150px 1fr; -} - -.story-format-card .story-format-image img, -.story-format-card .story-format-image svg { - height: auto; - width: 100%; -} - -.story-format-card .story-format-badges { - grid-column-start: 1; - grid-column-end: 3; - margin-bottom: calc(-1 * var(--grid-size)); -} - -.story-format-card .story-format-badges .badge { - margin-right: var(--grid-size); - margin-bottom: var(--grid-size); -} \ No newline at end of file diff --git a/src/components/story-format/story-format-card/story-format-card.tsx b/src/components/story-format/story-format-card/story-format-card.tsx deleted file mode 100644 index 6440d67a6..000000000 --- a/src/components/story-format/story-format-card/story-format-card.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import {IconAlertTriangle} from '@tabler/icons'; -import * as React from 'react'; -import {useTranslation} from 'react-i18next'; -import {formatImageUrl, StoryFormat} from '../../../store/story-formats'; -import {Badge} from '../../badge/badge'; -import {CardContent} from '../../container/card'; -import {SelectableCard} from '../../container/card/selectable-card'; -import {IconLoading} from '../../image/icon'; -import {StoryFormatCardDetails} from './story-format-card-details'; -import './story-format-card.css'; - -export interface StoryFormatCardProps { - defaultFormat: boolean; - editorExtensionsDisabled: boolean; - format: StoryFormat; - onSelect: () => void; - proofingFormat: boolean; -} - -export const StoryFormatCard: React.FC = props => { - const { - defaultFormat, - editorExtensionsDisabled, - format, - onSelect, - proofingFormat - } = props; - const {t} = useTranslation(); - - let image = <>; - - if (format.loadState === 'error') { - image = ; - } else if ( - format.loadState === 'unloaded' || - format.loadState === 'loading' - ) { - image = ; - } else if (format.loadState === 'loaded' && format.properties.image) { - image = ; - } - - return ( -
- - -
{image}
-
-

- {t('components.storyFormatCard.name', { - name: format.name, - version: format.version - })} -

- -
-
- {!format.userAdded && ( - - )} - {defaultFormat && ( - - )} - {editorExtensionsDisabled && ( - - )} - {format.loadState === 'loaded' && format.properties.proofing && ( - - )} - {proofingFormat && ( - - )} -
-
-
-
- ); -}; diff --git a/src/components/story-format/story-format-item/__mocks__/story-format-item-details.tsx b/src/components/story-format/story-format-item/__mocks__/story-format-item-details.tsx new file mode 100644 index 000000000..c002be077 --- /dev/null +++ b/src/components/story-format/story-format-item/__mocks__/story-format-item-details.tsx @@ -0,0 +1,5 @@ +import * as React from 'react'; + +export const StoryFormatItemDetails = () => ( +
+); diff --git a/src/components/story-format/story-format-item/__mocks__/story-format-item.tsx b/src/components/story-format/story-format-item/__mocks__/story-format-item.tsx new file mode 100644 index 000000000..ec23e901c --- /dev/null +++ b/src/components/story-format/story-format-item/__mocks__/story-format-item.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import {StoryFormatItemProps} from '../story-format-item'; + +export const StoryFormatItem: React.FC = ({ + defaultFormat, + editorExtensionsDisabled, + format, + onChangeEditorExtensionsDisabled, + onDelete, + onUseAsDefault, + onUseAsProofing, + proofingFormat +}) => { + return ( +
+ + + + + +
+ ); +}; diff --git a/src/components/story-format/story-format-card/__tests__/story-format-card-details.test.tsx b/src/components/story-format/story-format-item/__tests__/story-format-item-details.test.tsx similarity index 72% rename from src/components/story-format/story-format-card/__tests__/story-format-card-details.test.tsx rename to src/components/story-format/story-format-item/__tests__/story-format-item-details.test.tsx index a3b1bb78c..0e570195c 100644 --- a/src/components/story-format/story-format-card/__tests__/story-format-card-details.test.tsx +++ b/src/components/story-format/story-format-item/__tests__/story-format-item-details.test.tsx @@ -9,35 +9,35 @@ import { fakeUnloadedStoryFormat } from '../../../../test-util'; import { - StoryFormatCardDetails, - StoryFormatCardDetailsProps -} from '../story-format-card-details'; + StoryFormatItemDetails, + StoryFormatItemDetailsProps +} from '../story-format-item-details'; -describe('', () => { - function renderComponent(props?: Partial) { +describe('', () => { + function renderComponent(props?: Partial) { return render( - + ); } it('displays a loading message if the format is unloaded', () => { renderComponent({format: fakeUnloadedStoryFormat()}); expect( - screen.getByText('components.storyFormatCard.loadingFormat') + screen.getByText('components.storyFormatItem.loadingFormat') ).toBeInTheDocument(); }); it('displays a loading message if the format is loading', () => { renderComponent({format: fakePendingStoryFormat()}); expect( - screen.getByText('components.storyFormatCard.loadingFormat') + screen.getByText('components.storyFormatItem.loadingFormat') ).toBeInTheDocument(); }); it('displays an error message if the format failed to load', () => { renderComponent({format: fakeFailedStoryFormat()}); expect( - screen.getByText('components.storyFormatCard.loadError') + screen.getByText('components.storyFormatItem.loadError') ).toBeInTheDocument(); }); @@ -51,7 +51,7 @@ describe('', () => { it('displays the format author', () => expect( - screen.getByText('components.storyFormatCard.author') + screen.getByText('components.storyFormatItem.author') ).toBeInTheDocument()); it('displays the format description', () => @@ -61,7 +61,7 @@ describe('', () => { it('displays the format license', () => expect( - screen.getByText('components.storyFormatCard.license') + screen.getByText('components.storyFormatItem.license') ).toBeInTheDocument()); }); diff --git a/src/components/story-format/story-format-item/__tests__/story-format-item.test.tsx b/src/components/story-format/story-format-item/__tests__/story-format-item.test.tsx new file mode 100644 index 000000000..2f6a9062a --- /dev/null +++ b/src/components/story-format/story-format-item/__tests__/story-format-item.test.tsx @@ -0,0 +1,245 @@ +import {fireEvent, render, screen} from '@testing-library/react'; +import {axe} from 'jest-axe'; +import * as React from 'react'; +import { + fakeFailedStoryFormat, + fakeLoadedStoryFormat, + fakePendingStoryFormat +} from '../../../../test-util'; +import {StoryFormatItem, StoryFormatItemProps} from '../story-format-item'; + +jest.mock('../story-format-item-details'); + +describe('', () => { + function renderComponent(props?: Partial) { + return render( + + ); + } + + it('shows a loading icon if the format is loading', () => { + renderComponent({format: fakePendingStoryFormat()}); + expect(document.querySelector('.icon-loading')).toBeInTheDocument(); + }); + + it('shows an error icon if the format failed to load', () => { + renderComponent({format: fakeFailedStoryFormat()}); + expect( + document.querySelector('.icon-tabler-alert-triangle') + ).toBeInTheDocument(); + }); + + describe('when the format is loaded', () => { + it('displays the format name and version', () => { + renderComponent({format: fakeLoadedStoryFormat()}); + expect( + screen.getByText('components.storyFormatItem.name') + ).toBeInTheDocument(); + }); + + it("shows a badge if the format isn't user-added", () => { + const format = fakeLoadedStoryFormat(); + + format.userAdded = false; + renderComponent({format}); + expect( + screen.getByText('components.storyFormatItem.builtIn') + ).toBeInTheDocument(); + }); + + it("doesn't show that badge if the format is user-added", () => { + const format = fakeLoadedStoryFormat(); + + format.userAdded = true; + renderComponent({format}); + expect( + screen.queryByText('components.storyFormatItem.builtIn') + ).not.toBeInTheDocument(); + }); + + it('shows a badge if the format is the default format', () => { + renderComponent({defaultFormat: true, format: fakeLoadedStoryFormat()}); + expect( + screen.getByText('components.storyFormatItem.defaultFormat') + ).toBeInTheDocument(); + }); + + it("doesn't show that badge if the format isn't the default format", () => { + renderComponent({defaultFormat: false, format: fakeLoadedStoryFormat()}); + expect( + screen.queryByText('components.storyFormatItem.defaultFormat') + ).not.toBeInTheDocument(); + }); + + it('shows a badge if the format is a proofing format', () => { + const format = fakeLoadedStoryFormat(); + + (format as any).properties.proofing = true; + renderComponent({format}); + expect( + screen.getByText('components.storyFormatItem.proofing') + ).toBeInTheDocument(); + }); + + it("doesn't show the same badge if the format is not proofing", () => { + const format = fakeLoadedStoryFormat(); + + (format as any).properties.proofing = false; + renderComponent({format}); + expect( + screen.queryByText('components.storyFormatItem.proofing') + ).not.toBeInTheDocument(); + }); + + it('shows a badge if the format is the preferred proofing format', () => { + const format = fakeLoadedStoryFormat(); + + (format as any).properties.proofing = true; + renderComponent({format, proofingFormat: true}); + expect( + screen.getByText('components.storyFormatItem.proofingFormat') + ).toBeInTheDocument(); + }); + + it("doesn't show the same badge if the format is not the preferred proofing format", () => { + const format = fakeLoadedStoryFormat(); + + (format as any).properties.proofing = true; + renderComponent({format, proofingFormat: false}); + expect( + screen.queryByText('components.storyFormatItem.proofingFormat') + ).not.toBeInTheDocument(); + }); + + it('highlights itself if the format is the preferred proofing format', () => { + const format = fakeLoadedStoryFormat(); + + (format as any).properties.proofing = true; + renderComponent({format, proofingFormat: true}); + expect(document.querySelector('.story-format-item')).toHaveClass( + 'highlighted' + ); + }); + + it('highlights itself if the format is the default format', () => { + const format = fakeLoadedStoryFormat(); + + (format as any).properties.proofing = false; + renderComponent({format, defaultFormat: true}); + expect(document.querySelector('.story-format-item')).toHaveClass( + 'highlighted' + ); + }); + + it("doesn't highlight itself if the format is neither the proofing or default format", () => { + renderComponent({format: fakeLoadedStoryFormat()}); + expect(document.querySelector('.story-format-item')).not.toHaveClass( + 'highlighted' + ); + }); + + it('shows format details', () => { + renderComponent({format: fakeLoadedStoryFormat()}); + expect( + screen.getByTestId('mock-story-format-item-details') + ).toBeInTheDocument(); + }); + + it('shows a button that calls the onDelete prop if the format is user-added', () => { + const onDelete = jest.fn(); + + renderComponent({ + onDelete, + format: fakeLoadedStoryFormat({userAdded: true}) + }); + expect(onDelete).not.toHaveBeenCalled(); + fireEvent.click(screen.getByRole('button', {name: 'common.delete'})); + expect(onDelete).toHaveBeenCalledTimes(1); + }); + + it("doesn't show that button if the format isn't user-added", () => { + renderComponent({format: fakeLoadedStoryFormat({userAdded: false})}); + expect( + screen.queryByRole('button', {name: 'common.delete'}) + ).not.toBeInTheDocument(); + }); + + describe('When editor extensions for the format are enabled', () => { + it('shows a checked checkbox', () => { + renderComponent({ + editorExtensionsDisabled: false, + format: fakeLoadedStoryFormat() + }); + expect( + screen.getByRole('checkbox', { + name: 'components.storyFormatItem.useEditorExtensions' + }) + ).toBeChecked(); + }); + + it('calls the onChangeEditorExtensionsEnabled prop when clicked', () => { + const onChangeEditorExtensionsDisabled = jest.fn(); + + renderComponent({ + onChangeEditorExtensionsDisabled, + editorExtensionsDisabled: false, + format: fakeLoadedStoryFormat() + }); + expect(onChangeEditorExtensionsDisabled).not.toHaveBeenCalled(); + fireEvent.click( + screen.getByRole('checkbox', { + name: 'components.storyFormatItem.useEditorExtensions' + }) + ); + expect(onChangeEditorExtensionsDisabled.mock.calls).toEqual([[true]]); + }); + }); + + describe('When editor extensions for the format are disabled', () => { + it('shows an unchecked checkbox', () => { + renderComponent({ + editorExtensionsDisabled: true, + format: fakeLoadedStoryFormat() + }); + expect( + screen.getByRole('checkbox', { + name: 'components.storyFormatItem.useEditorExtensions' + }) + ).not.toBeChecked(); + }); + + it('calls the onChangeEditorExtensionsEnabled prop when clicked', () => { + const onChangeEditorExtensionsDisabled = jest.fn(); + + renderComponent({ + onChangeEditorExtensionsDisabled, + editorExtensionsDisabled: true, + format: fakeLoadedStoryFormat() + }); + expect(onChangeEditorExtensionsDisabled).not.toHaveBeenCalled(); + fireEvent.click( + screen.getByRole('checkbox', { + name: 'components.storyFormatItem.useEditorExtensions' + }) + ); + expect(onChangeEditorExtensionsDisabled.mock.calls).toEqual([[false]]); + }); + }); + }); + + it('is accessible', async () => { + const {container} = renderComponent(); + + expect(await axe(container)).toHaveNoViolations(); + }); +}); diff --git a/src/components/story-format/story-format-item/index.ts b/src/components/story-format/story-format-item/index.ts new file mode 100644 index 000000000..1d912d2d3 --- /dev/null +++ b/src/components/story-format/story-format-item/index.ts @@ -0,0 +1,2 @@ +export * from './story-format-item'; +export * from './story-format-item-details'; diff --git a/src/components/story-format/story-format-card/story-format-card-details.tsx b/src/components/story-format/story-format-item/story-format-item-details.tsx similarity index 59% rename from src/components/story-format/story-format-card/story-format-card-details.tsx rename to src/components/story-format/story-format-item/story-format-item-details.tsx index 5fe3d9372..3442542a2 100644 --- a/src/components/story-format/story-format-card/story-format-card-details.tsx +++ b/src/components/story-format/story-format-item/story-format-item-details.tsx @@ -2,28 +2,28 @@ import * as React from 'react'; import {useTranslation} from 'react-i18next'; import {StoryFormat} from '../../../store/story-formats'; -export interface StoryFormatCardDetailsProps { +export interface StoryFormatItemDetailsProps { format: StoryFormat; } -export const StoryFormatCardDetails: React.FC = ({ +export const StoryFormatItemDetails: React.FC = ({ format }) => { const {t} = useTranslation(); if (format.loadState === 'unloaded' || format.loadState === 'loading') { return ( -
-

{t('components.storyFormatCard.loadingFormat')}

+
+

{t('components.storyFormatItem.loadingFormat')}

); } if (format.loadState === 'error') { return ( -
+

- {t('components.storyFormatCard.loadError', { + {t('components.storyFormatItem.loadError', { errorMessage: format.loadError.message })}

@@ -32,12 +32,12 @@ export const StoryFormatCardDetails: React.FC = ({ } return ( -
+
{format.properties.author && (

= ({ )} {format.properties.description && (

)} {format.properties.license && ( -

- {t('components.storyFormatCard.license', { +

+ {t('components.storyFormatItem.license', { license: format.properties.license })}

diff --git a/src/components/story-format/story-format-item/story-format-item.css b/src/components/story-format/story-format-item/story-format-item.css new file mode 100644 index 000000000..771f20487 --- /dev/null +++ b/src/components/story-format/story-format-item/story-format-item.css @@ -0,0 +1,52 @@ +@import '../../../styles/colors.css'; +@import '../../../styles/metrics.css'; + +.story-format-item { + display: grid; + flex-grow: 1; + gap: var(--control-inner-padding); + grid-template: "name image" + "description image" + "actions image" / 1fr 100px; + padding: var(--grid-size) var(--grid-size) 0 var(--grid-size); +} + +.story-format-item.highlighted { + background-color: var(--faint-blue); +} + +.story-format-item .story-format-image { + align-self: center; + grid-area: image; +} + +.story-format-item .story-format-image img, +.story-format-item .story-format-image svg { + height: auto; + width: 100%; +} + +.story-format-item .story-format-name { + align-items: center; + display: flex; + gap: var(--grid-size); + grid-area: name; +} + +.story-format-item .story-format-description { + grid-area: description; +} + +.story-format-item .story-format-details :first-child { + /* Cheat first line of details upward. */ + margin-top: 0.5em; +} + +.story-format-item .story-format-details :last-child { + margin-bottom: 0; +} + +.story-format-item .actions { + grid-area: actions; + margin-left: calc(-1 * var(--grid-size)) +} \ No newline at end of file diff --git a/src/components/story-format/story-format-item/story-format-item.tsx b/src/components/story-format/story-format-item/story-format-item.tsx new file mode 100644 index 000000000..3cb05dc21 --- /dev/null +++ b/src/components/story-format/story-format-item/story-format-item.tsx @@ -0,0 +1,125 @@ +import {IconAlertTriangle, IconStar, IconTrash} from '@tabler/icons'; +import * as React from 'react'; +import {useTranslation} from 'react-i18next'; +import {formatImageUrl, StoryFormat} from '../../../store/story-formats'; +import {Badge} from '../../badge/badge'; +import {IconLoading} from '../../image/icon'; +import {StoryFormatItemDetails} from './story-format-item-details'; +import './story-format-item.css'; +import {CheckboxButton} from '../../control/checkbox-button'; +import {IconButton} from '../../control/icon-button'; +import classNames from 'classnames'; + +export interface StoryFormatItemProps { + defaultFormat: boolean; + editorExtensionsDisabled: boolean; + format: StoryFormat; + onChangeEditorExtensionsDisabled: (value: boolean) => void; + onDelete: () => void; + onUseAsDefault: () => void; + onUseAsProofing: () => void; + proofingFormat: boolean; +} + +export const StoryFormatItem: React.FC = props => { + const { + defaultFormat, + editorExtensionsDisabled, + format, + onChangeEditorExtensionsDisabled, + onDelete, + onUseAsDefault, + onUseAsProofing, + proofingFormat + } = props; + const {t} = useTranslation(); + + let image = <>; + + if (format.loadState === 'error') { + image = ; + } else if ( + format.loadState === 'unloaded' || + format.loadState === 'loading' + ) { + image = ; + } else if (format.loadState === 'loaded' && format.properties.image) { + image = ; + } + + let useButton = <>; + + if (format.loadState === 'loaded') { + if (format.properties.proofing) { + useButton = ( + } + label={t('components.storyFormatItem.useProofingFormat')} + onClick={onUseAsProofing} + variant="primary" + /> + ); + } else { + useButton = ( + } + variant="primary" + /> + ); + } + } + + return ( +
+
{image}
+
+
+

+ {t('components.storyFormatItem.name', { + name: format.name, + version: format.version + })} +

+ {!format.userAdded && ( + + )} + {format.loadState === 'loaded' && format.properties.proofing && ( + + )} + {defaultFormat && ( + + )} + {proofingFormat && ( + + )} +
+ +
+
+ {useButton} + {format.userAdded && ( + } + onClick={onDelete} + /> + )} + {format.loadState === 'loaded' && !format.properties.proofing && ( + onChangeEditorExtensionsDisabled(!value)} + value={!editorExtensionsDisabled} + /> + )} +
+
+ ); +}; diff --git a/src/routes/story-format-list/toolbar/formats/__tests__/add-story-format-button.test.tsx b/src/dialogs/story-formats/__tests__/add-story-format-button.test.tsx similarity index 82% rename from src/routes/story-format-list/toolbar/formats/__tests__/add-story-format-button.test.tsx rename to src/dialogs/story-formats/__tests__/add-story-format-button.test.tsx index 02d1e3493..b747bec28 100644 --- a/src/routes/story-format-list/toolbar/formats/__tests__/add-story-format-button.test.tsx +++ b/src/dialogs/story-formats/__tests__/add-story-format-button.test.tsx @@ -14,11 +14,11 @@ import { FakeStateProviderProps, fakeStoryFormatProperties, StoryFormatInspector -} from '../../../../../test-util'; -import {fetchStoryFormatProperties} from '../../../../../util/story-format'; +} from '../../../test-util'; +import {fetchStoryFormatProperties} from '../../../util/story-format'; import {AddStoryFormatButton} from '../add-story-format-button'; -jest.mock('../../../../../util/story-format'); +jest.mock('../../../util/story-format'); const getAddButton = () => within(document.querySelector('.card-button-card')!).getByRole('button', { @@ -60,7 +60,7 @@ describe('', () => { ).not.toBeInTheDocument(); fireEvent.change( screen.getByRole('textbox', { - name: 'routes.storyFormatList.toolbar.addStoryFormatButton.prompt' + name: 'dialogs.storyFormats.addStoryFormatButton.prompt' }), {target: {value: 'http://mock-format-url'}} ); @@ -79,15 +79,13 @@ describe('', () => { await renderComponent(); fireEvent.change( screen.getByRole('textbox', { - name: 'routes.storyFormatList.toolbar.addStoryFormatButton.prompt' + name: 'dialogs.storyFormats.addStoryFormatButton.prompt' }), {target: {value: 'not a url'}} ); await act(async () => Promise.resolve()); expect( - screen.getByText( - 'routes.storyFormatList.toolbar.addStoryFormatButton.invalidUrl' - ) + screen.getByText('dialogs.storyFormats.addStoryFormatButton.invalidUrl') ).toBeInTheDocument(); expect(getAddButton()).toBeDisabled(); }); @@ -97,15 +95,13 @@ describe('', () => { await renderComponent(); fireEvent.change( screen.getByRole('textbox', { - name: 'routes.storyFormatList.toolbar.addStoryFormatButton.prompt' + name: 'dialogs.storyFormats.addStoryFormatButton.prompt' }), {target: {value: 'http://mock-format-url'}} ); await act(async () => Promise.resolve()); expect( - screen.getByText( - 'routes.storyFormatList.toolbar.addStoryFormatButton.fetchError' - ) + screen.getByText('dialogs.storyFormats.addStoryFormatButton.fetchError') ).toBeInTheDocument(); expect(getAddButton()).toBeDisabled(); }); @@ -119,15 +115,13 @@ describe('', () => { await renderComponent({storyFormats: [format]}); fireEvent.change( screen.getByRole('textbox', { - name: 'routes.storyFormatList.toolbar.addStoryFormatButton.prompt' + name: 'dialogs.storyFormats.addStoryFormatButton.prompt' }), {target: {value: 'http://mock-format-url'}} ); await act(async () => Promise.resolve()); expect( - screen.getByText( - 'routes.storyFormatList.toolbar.addStoryFormatButton.alreadyAdded' - ) + screen.getByText('dialogs.storyFormats.addStoryFormatButton.alreadyAdded') ).toBeInTheDocument(); expect(getAddButton()).toBeDisabled(); }); diff --git a/src/dialogs/story-formats/__tests__/story-formats-filter.button.test.tsx b/src/dialogs/story-formats/__tests__/story-formats-filter.button.test.tsx new file mode 100644 index 000000000..0c559d262 --- /dev/null +++ b/src/dialogs/story-formats/__tests__/story-formats-filter.button.test.tsx @@ -0,0 +1,93 @@ +import {fireEvent, render, screen} from '@testing-library/react'; +import {axe} from 'jest-axe'; +import * as React from 'react'; +import {StoryFormatsFilterButton} from '../story-formats-filter-button'; +import {FakeStateProvider, PrefInspector} from '../../../test-util'; +import {PrefsState} from '../../../store/prefs'; + +describe('', () => { + const prefs: ('all' | 'current' | 'user')[][] = [ + ['all'], + ['current'], + ['user'] + ]; + + function renderComponent(prefs?: Partial) { + return render( + + + + + ); + } + + describe.each(prefs)( + 'When the current story format filter is %s', + storyFormatListFilter => { + it('displays the label for this state', () => { + renderComponent({storyFormatListFilter}); + expect( + screen.getByRole('button', { + name: `dialogs.storyFormats.filterButton.${storyFormatListFilter}` + }) + ).toBeInTheDocument(); + }); + + it('checks the appropriate item in the menu', () => { + renderComponent({storyFormatListFilter}); + fireEvent.click( + screen.getByRole('button', { + name: `dialogs.storyFormats.filterButton.${storyFormatListFilter}` + }) + ); + expect( + screen.getByRole('checkbox', { + name: `dialogs.storyFormats.filterButton.${storyFormatListFilter}` + }) + ).toBeChecked(); + }); + + it('unchecks all other items in the menu', () => { + expect.assertions(prefs.length - 1); + renderComponent({storyFormatListFilter}); + fireEvent.click( + screen.getByRole('button', { + name: `dialogs.storyFormats.filterButton.${storyFormatListFilter}` + }) + ); + + for (const pref of prefs.filter( + ([item]) => item !== storyFormatListFilter + )) { + expect( + screen.getByRole('checkbox', { + name: `dialogs.storyFormats.filterButton.${pref}` + }) + ).not.toBeChecked(); + } + }); + } + ); + + it.each(prefs)( + 'Dispatches a preference update when the %s menu item is selected', + pref => { + renderComponent(); + fireEvent.click(screen.getByRole('button')); + fireEvent.click( + screen.getByRole('checkbox', { + name: `dialogs.storyFormats.filterButton.${pref}` + }) + ); + expect( + screen.getByTestId('pref-inspector-storyFormatListFilter') + ).toHaveTextContent(pref); + } + ); + + it('is accessible', async () => { + const {container} = renderComponent(); + + expect(await axe(container)).toHaveNoViolations(); + }); +}); diff --git a/src/dialogs/story-formats/__tests__/story-formats.test.tsx b/src/dialogs/story-formats/__tests__/story-formats.test.tsx new file mode 100644 index 000000000..efd6e0ac3 --- /dev/null +++ b/src/dialogs/story-formats/__tests__/story-formats.test.tsx @@ -0,0 +1,232 @@ +import {act, fireEvent, render, screen, within} from '@testing-library/react'; +import {axe} from 'jest-axe'; +import * as React from 'react'; +import { + fakeLoadedStoryFormat, + FakeStateProvider, + PrefInspector +} from '../../../test-util'; +import {StoryFormatsDialog} from '../story-formats'; +import {StoryFormat} from '../../../store/story-formats'; +import {PrefsState} from '../../../store/prefs'; + +jest.mock( + '../../../components/story-format/story-format-item/story-format-item' +); + +describe('', () => { + function renderComponent( + formats: StoryFormat[] = [], + prefs?: Partial + ) { + return render( + + + + + + + ); + } + + afterEach(async () => { + await act(() => Promise.resolve()); + }); + + it('shows all formats when the view preference is all', () => { + const format1 = fakeLoadedStoryFormat(); + const format2 = fakeLoadedStoryFormat(); + + format1.version = '1.0.0'; + format2.name = format1.name; + format2.version = '1.0.1'; + renderComponent([format1, format2], {storyFormatListFilter: 'all'}); + + const items = screen.getAllByTestId('mock-story-format-item'); + + expect(items.length).toBe(2); + expect(items.some(item => item.dataset.formatId === format1.id)).toBe(true); + expect(items.some(item => item.dataset.formatId === format2.id)).toBe(true); + }); + + it('only shows user-added formats when the view preference is user', () => { + const format1 = fakeLoadedStoryFormat({userAdded: true}); + const format2 = fakeLoadedStoryFormat({userAdded: false}); + + renderComponent([format1, format2], {storyFormatListFilter: 'user'}); + + const items = screen.getAllByTestId('mock-story-format-item'); + + expect(items.length).toBe(1); + expect(items.some(item => item.dataset.formatId === format1.id)).toBe(true); + expect(items.some(item => item.dataset.formatId === format2.id)).toBe( + false + ); + }); + + it('only shows the most current version of a format iof the view preference is current', () => { + const format1 = fakeLoadedStoryFormat(); + const format2 = fakeLoadedStoryFormat(); + const format3 = fakeLoadedStoryFormat(); + + format1.version = '1.0.0'; + format2.name = format1.name; + format2.version = '1.0.1'; + renderComponent([format1, format2, format3], { + storyFormatListFilter: 'current' + }); + expect((format3 as any).properties.name).not.toBe( + (format1 as any).properties.name + ); + + const items = screen.getAllByTestId('mock-story-format-item'); + + expect(items.length).toBe(2); + expect(items.some(item => item.dataset.formatId === format1.id)).toBe( + false + ); + expect(items.some(item => item.dataset.formatId === format2.id)).toBe(true); + expect(items.some(item => item.dataset.formatId === format3.id)).toBe(true); + }); + + it('shows a message if no formats are visible', () => { + renderComponent([fakeLoadedStoryFormat({userAdded: false})], { + storyFormatListFilter: 'user' + }); + expect( + screen.getByText('dialogs.storyFormats.noneVisible') + ).toBeInTheDocument(); + expect( + screen.queryByTestId('mock-story-format-item') + ).not.toBeInTheDocument(); + }); + + it('marks the default format', () => { + const format = fakeLoadedStoryFormat(); + + renderComponent([format], { + storyFormat: {name: format.name, version: format.version} + }); + expect( + screen.getByTestId('mock-story-format-item').dataset.defaultFormat + ).toBe('true'); + }); + + it('marks the proofing format', () => { + const format = fakeLoadedStoryFormat(); + + renderComponent([format], { + proofingFormat: {name: format.name, version: format.version} + }); + expect( + screen.getByTestId('mock-story-format-item').dataset.proofingFormat + ).toBe('true'); + }); + + it('leaves other formats unmarked', () => { + const format = fakeLoadedStoryFormat(); + + renderComponent([format], { + proofingFormat: { + name: format.name + 'other', + version: format.version + 'other' + } + }); + + const item = screen.getByTestId('mock-story-format-item'); + + expect(item.dataset.defaultFormat).toBe('false'); + expect(item.dataset.proofingFormat).toBe('false'); + }); + + it('deletes a format when the delete button is clicked on an item', () => { + const format1 = fakeLoadedStoryFormat(); + const format2 = fakeLoadedStoryFormat(); + + renderComponent([format1, format2]); + + const items = screen.getAllByTestId('mock-story-format-item'); + + expect(items.length).toBe(2); + + const toDelete = items.find(item => item.dataset.formatId === format1.id); + + if (!toDelete) { + throw new Error("Couldn't find first format in DOM"); + } + + fireEvent.click(within(toDelete).getByRole('button', {name: 'onDelete'})); + expect(screen.getAllByTestId('mock-story-format-item').length).toBe(1); + expect(screen.getByTestId('mock-story-format-item').dataset.formatId).toBe( + format2.id + ); + }); + + it('disables editor extensions when the checkbox is clicked on an item', () => { + const format = fakeLoadedStoryFormat(); + + renderComponent([format], { + disabledStoryFormatEditorExtensions: [] + }); + + expect( + screen.getByTestId('pref-inspector-disabledStoryFormatEditorExtensions') + ).toHaveTextContent('[]'); + fireEvent.click(screen.getByText('onChangeEditorExtensionsDisabled true')); + expect( + screen.getByTestId('pref-inspector-disabledStoryFormatEditorExtensions') + ).toHaveTextContent( + JSON.stringify([{name: format.name, version: format.version}]) + ); + }); + + it('enables editor extensions when the checkbox is clicked on an item', () => { + const format = fakeLoadedStoryFormat(); + + renderComponent([format], { + disabledStoryFormatEditorExtensions: [ + {name: format.name, version: format.version}, + {name: 'other', version: '1.0.0'} + ] + }); + + fireEvent.click(screen.getByText('onChangeEditorExtensionsDisabled false')); + expect( + screen.getByTestId('pref-inspector-disabledStoryFormatEditorExtensions') + ).toHaveTextContent(JSON.stringify([{name: 'other', version: '1.0.0'}])); + }); + + it('updates preferences when a format is selected as default', () => { + const format = fakeLoadedStoryFormat(); + + renderComponent([format]); + fireEvent.click(screen.getByText('onUseAsDefault')); + expect(screen.getByTestId('pref-inspector-storyFormat')).toHaveTextContent( + JSON.stringify({name: format.name, version: format.version}) + ); + }); + + it('updates preferences when a format is selected as proofing', () => { + const format = fakeLoadedStoryFormat(); + + renderComponent([format]); + fireEvent.click(screen.getByText('onUseAsProofing')); + expect( + screen.getByTestId('pref-inspector-proofingFormat') + ).toHaveTextContent( + JSON.stringify({name: format.name, version: format.version}) + ); + }); + + it('is accessible', async () => { + const {container} = renderComponent(); + + expect(await axe(container)).toHaveNoViolations(); + }); +}); diff --git a/src/routes/story-format-list/toolbar/formats/add-story-format-button.css b/src/dialogs/story-formats/add-story-format-button.css similarity index 100% rename from src/routes/story-format-list/toolbar/formats/add-story-format-button.css rename to src/dialogs/story-formats/add-story-format-button.css diff --git a/src/routes/story-format-list/toolbar/formats/add-story-format-button.tsx b/src/dialogs/story-formats/add-story-format-button.tsx similarity index 68% rename from src/routes/story-format-list/toolbar/formats/add-story-format-button.tsx rename to src/dialogs/story-formats/add-story-format-button.tsx index 66325bd08..7ccf9707b 100644 --- a/src/routes/story-format-list/toolbar/formats/add-story-format-button.tsx +++ b/src/dialogs/story-formats/add-story-format-button.tsx @@ -4,12 +4,12 @@ import {IconPlus} from '@tabler/icons'; import { createFromProperties, useStoryFormatsContext -} from '../../../../store/story-formats'; +} from '../../store/story-formats'; import { PromptButton, PromptButtonValidator -} from '../../../../components/control/prompt-button'; -import {fetchStoryFormatProperties} from '../../../../util/story-format'; +} from '../../components/control/prompt-button'; +import {fetchStoryFormatProperties} from '../../util/story-format'; import './add-story-format-button.css'; function isValidUrl(value: string) { @@ -44,9 +44,7 @@ export const AddStoryFormatButton: React.FC = () => { if (!isValidUrl(newFormatUrl)) { return { - message: t( - 'routes.storyFormatList.toolbar.addStoryFormatButton.invalidUrl' - ), + message: t('dialogs.storyFormats.addStoryFormatButton.invalidUrl'), valid: false }; } @@ -62,35 +60,26 @@ export const AddStoryFormatButton: React.FC = () => { ) ) { return { - message: t( - 'routes.storyFormatList.toolbar.addStoryFormatButton.alreadyAdded', - { - storyFormatName: properties.name, - storyFormatVersion: properties.version - } - ), + message: t('dialogs.storyFormats.addStoryFormatButton.alreadyAdded', { + storyFormatName: properties.name, + storyFormatVersion: properties.version + }), valid: false }; } return { - message: t( - 'routes.storyFormatList.toolbar.addStoryFormatButton.addPreview', - { - storyFormatName: properties.name, - storyFormatVersion: properties.version - } - ), + message: t('dialogs.storyFormats.addStoryFormatButton.addPreview', { + storyFormatName: properties.name, + storyFormatVersion: properties.version + }), valid: true }; } catch (error) { return { - message: t( - 'routes.storyFormatList.toolbar.addStoryFormatButton.fetchError', - { - errorMessage: (error as Error).message - } - ), + message: t('dialogs.storyFormats.addStoryFormatButton.fetchError', { + errorMessage: (error as Error).message + }), valid: false }; } @@ -103,7 +92,7 @@ export const AddStoryFormatButton: React.FC = () => { label={t('common.add')} onChange={event => setNewFormatUrl(event.target.value)} onSubmit={handleSubmit} - prompt={t('routes.storyFormatList.toolbar.addStoryFormatButton.prompt')} + prompt={t('dialogs.storyFormats.addStoryFormatButton.prompt')} submitIcon={} submitLabel={t('common.add')} submitVariant="create" diff --git a/src/dialogs/story-formats/story-formats-filter-button.tsx b/src/dialogs/story-formats/story-formats-filter-button.tsx new file mode 100644 index 000000000..b70dcff95 --- /dev/null +++ b/src/dialogs/story-formats/story-formats-filter-button.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import {useTranslation} from 'react-i18next'; +import {setPref, usePrefsContext} from '../../store/prefs'; +import {MenuButton} from '../../components/control/menu-button'; +import {IconFilter} from '@tabler/icons'; + +export const StoryFormatsFilterButton: React.FC = () => { + const {dispatch, prefs} = usePrefsContext(); + const {t} = useTranslation(); + + function setFilter(name: string) { + dispatch(setPref('storyFormatListFilter', name)); + } + + return ( + setFilter('current') + }, + { + checked: prefs.storyFormatListFilter === 'user', + checkable: true, + label: t('dialogs.storyFormats.filterButton.user'), + onClick: () => setFilter('user') + }, + { + checked: prefs.storyFormatListFilter === 'all', + checkable: true, + label: t('dialogs.storyFormats.filterButton.all'), + onClick: () => setFilter('all') + } + ]} + icon={} + label={t( + `dialogs.storyFormats.filterButton.${prefs.storyFormatListFilter}` + )} + > + ); +}; diff --git a/src/dialogs/story-formats/story-formats.css b/src/dialogs/story-formats/story-formats.css new file mode 100644 index 000000000..3e3094479 --- /dev/null +++ b/src/dialogs/story-formats/story-formats.css @@ -0,0 +1,19 @@ +@import '../../styles/colors.css'; +@import '../../styles/metrics.css'; + +.story-formats-dialog > .card > .card-content { + padding: 0; +} + +.story-formats-dialog .card-content { + /* We need flex basis to be 0 to force the content to scroll when there isn't enough vertical space. */ + flex: 1 1 0; +} + +.story-formats-dialog .card-content > p { + padding: var(--grid-size); +} + +.story-formats-dialog .story-format-item + .story-format-item { + border-top: 1px solid var(--light-gray); +} \ No newline at end of file diff --git a/src/dialogs/story-formats/story-formats.tsx b/src/dialogs/story-formats/story-formats.tsx new file mode 100644 index 000000000..c889f43b1 --- /dev/null +++ b/src/dialogs/story-formats/story-formats.tsx @@ -0,0 +1,121 @@ +import * as React from 'react'; +import {useTranslation} from 'react-i18next'; +import {ButtonBar} from '../../components/container/button-bar'; +import {CardContent} from '../../components/container/card'; +import { + DialogCard, + DialogCardProps +} from '../../components/container/dialog-card'; +import {StoryFormatItem} from '../../components/story-format/story-format-item'; +import {FormatLoader} from '../../store/format-loader'; +import {setPref, usePrefsContext} from '../../store/prefs'; +import { + deleteFormat, + filteredFormats, + sortFormats, + StoryFormat, + useStoryFormatsContext +} from '../../store/story-formats'; +import {AddStoryFormatButton} from './add-story-format-button'; +import {StoryFormatsFilterButton} from './story-formats-filter-button'; +import './story-formats.css'; + +export type StoryFormatsDialogProps = Omit; + +export const StoryFormatsDialog: React.FC = props => { + const {dispatch: formatsDispatch, formats} = useStoryFormatsContext(); + const {dispatch: prefsDispatch, prefs} = usePrefsContext(); + const {t} = useTranslation(); + + const visibleFormats = sortFormats( + filteredFormats(formats, prefs.storyFormatListFilter) + ); + + function handleChangeEditorExtensionsDisabled( + format: StoryFormat, + disabled: boolean + ) { + if (disabled) { + prefsDispatch( + setPref('disabledStoryFormatEditorExtensions', [ + ...prefs.disabledStoryFormatEditorExtensions, + {name: format.name, version: format.version} + ]) + ); + } else { + prefsDispatch( + setPref( + 'disabledStoryFormatEditorExtensions', + prefs.disabledStoryFormatEditorExtensions.filter( + f => f.name !== format.name || f.version !== format.version + ) + ) + ); + } + } + + function handleDelete(format: StoryFormat) { + formatsDispatch(deleteFormat(format)); + } + + function handleUseAsDefault(format: StoryFormat) { + prefsDispatch({ + type: 'update', + name: 'storyFormat', + value: {name: format.name, version: format.version} + }); + } + + function handleUseAsProofing(format: StoryFormat) { + prefsDispatch({ + type: 'update', + name: 'proofingFormat', + value: {name: format.name, version: format.version} + }); + } + + return ( + + + + + + + + {visibleFormats.length === 0 && ( +

{t('dialogs.storyFormats.noneVisible')}

+ )} + {visibleFormats.map(format => ( + + format.name === disabledFormat.name && + format.version === disabledFormat.version + )} + onChangeEditorExtensionsDisabled={value => + handleChangeEditorExtensionsDisabled(format, value) + } + onDelete={() => handleDelete(format)} + onUseAsDefault={() => handleUseAsDefault(format)} + onUseAsProofing={() => handleUseAsProofing(format)} + proofingFormat={ + format.name === prefs.proofingFormat.name && + format.version === prefs.proofingFormat.version + } + format={format} + key={format.id} + /> + ))} +
+
+
+ ); +}; diff --git a/src/route-actions/__tests__/app-actions.test.tsx b/src/route-actions/__tests__/app-actions.test.tsx index 95f359680..cff17e108 100644 --- a/src/route-actions/__tests__/app-actions.test.tsx +++ b/src/route-actions/__tests__/app-actions.test.tsx @@ -38,20 +38,13 @@ describe('', () => { expect(screen.getByText('dialogs.aboutTwine.title')).toBeInTheDocument(); }); - it('displays a button that allows users to manage story formats', () => { - const history = createMemoryHistory(); - - renderComponent({}, history); - expect(history.location.pathname).toBe('/'); + it('displays a button that shows the story formats dialog', () => { + renderComponent(); + expect( + screen.queryByText('dialogs.storyFormats.title') + ).not.toBeInTheDocument(); fireEvent.click(screen.getByText('routeActions.app.storyFormats')); - expect(history.location.pathname).toBe('/story-formats'); - }); - - it('disables the story format button if the user is already on that route', () => { - const history = createMemoryHistory({initialEntries: ['/story-formats']}); - - renderComponent({}, history); - expect(screen.getByText('routeActions.app.storyFormats')).toBeDisabled(); + expect(screen.getByText('dialogs.storyFormats.title')).toBeInTheDocument(); }); it('displays a button that allows users to report bugs', () => { diff --git a/src/route-actions/app-actions.tsx b/src/route-actions/app-actions.tsx index 24db4fd8a..4ca258549 100644 --- a/src/route-actions/app-actions.tsx +++ b/src/route-actions/app-actions.tsx @@ -5,6 +5,7 @@ import {useHistory} from 'react-router-dom'; import {ButtonBar} from '../components/container/button-bar'; import {IconButton} from '../components/control/icon-button'; import {AboutTwineDialog, AppPrefsDialog, useDialogsContext} from '../dialogs'; +import {StoryFormatsDialog} from '../dialogs/story-formats/story-formats'; export const AppActions: React.FC = () => { const {dispatch} = useDialogsContext(); @@ -22,7 +23,9 @@ export const AppActions: React.FC = () => { disabled={history.location.pathname === '/story-formats'} icon={} label={t('routeActions.app.storyFormats')} - onClick={() => history.push('/story-formats')} + onClick={() => + dispatch({type: 'addDialog', component: StoryFormatsDialog}) + } /> } diff --git a/src/routes/__tests__/index.test.tsx b/src/routes/__tests__/index.test.tsx index a4e39f2b6..90d9136f2 100644 --- a/src/routes/__tests__/index.test.tsx +++ b/src/routes/__tests__/index.test.tsx @@ -6,7 +6,6 @@ import {PrefsContext, PrefsContextProps} from '../../store/prefs'; import {fakePrefs} from '../../test-util'; jest.mock('../story-edit/story-edit-route'); -jest.mock('../story-format-list/story-format-list-route'); jest.mock('../story-list/story-list-route'); jest.mock('../story-play/story-play-route'); jest.mock('../story-proof/story-proof-route'); @@ -47,13 +46,6 @@ describe('', () => { expect(screen.getByTestId('mock-story-edit-route')).toBeInTheDocument(); }); - it('renders the story format list route at /story-formats', () => { - renderAtRoute('/story-formats'); - expect( - screen.getByTestId('mock-story-format-list-route') - ).toBeInTheDocument(); - }); - it('renders the story list at /', () => { renderAtRoute('/'); expect(screen.getByTestId('mock-story-list-route')).toBeInTheDocument(); diff --git a/src/routes/index.tsx b/src/routes/index.tsx index ff8c2d6ea..0f6a3f08d 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import {HashRouter, Route, Switch} from 'react-router-dom'; import {usePrefsContext} from '../store/prefs'; -import {StoryFormatListRoute} from './story-format-list'; import {StoryEditRoute} from './story-edit'; import {StoryListRoute} from './story-list'; import {StoryPlayRoute} from './story-play'; @@ -24,9 +23,6 @@ export const Routes: React.FC = () => { - - - diff --git a/src/routes/story-format-list/__mocks__/story-format-list-route.tsx b/src/routes/story-format-list/__mocks__/story-format-list-route.tsx deleted file mode 100644 index 29b3a7c28..000000000 --- a/src/routes/story-format-list/__mocks__/story-format-list-route.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import * as React from 'react'; - -export const StoryFormatListRoute: React.FC = () => ( -
-); diff --git a/src/routes/story-format-list/index.ts b/src/routes/story-format-list/index.ts deleted file mode 100644 index 4266f3681..000000000 --- a/src/routes/story-format-list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './story-format-list-route'; diff --git a/src/routes/story-format-list/story-format-list-route.tsx b/src/routes/story-format-list/story-format-list-route.tsx deleted file mode 100644 index 1b68fd4d2..000000000 --- a/src/routes/story-format-list/story-format-list-route.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import * as React from 'react'; -import {useTranslation} from 'react-i18next'; -import {CSSTransition, TransitionGroup} from 'react-transition-group'; -import {ClickAwayListener} from '../../components/click-away-listener'; -import {CardGroup} from '../../components/container/card-group'; -import {MainContent} from '../../components/container/main-content'; -import {StoryFormatCard} from '../../components/story-format/story-format-card/story-format-card'; -import {DialogsContextProvider} from '../../dialogs'; -import {FormatLoader} from '../../store/format-loader'; -import {usePrefsContext} from '../../store/prefs'; -import { - deselectAllFormats, - deselectFormat, - filteredFormats, - selectFormat, - sortFormats, - StoryFormat, - useStoryFormatsContext -} from '../../store/story-formats'; -import {StoryFormatListToolbar} from './toolbar'; - -export const StoryFormatListRoute: React.FC = () => { - const {dispatch: formatsDispatch, formats} = useStoryFormatsContext(); - const {dispatch: prefsDispatch, prefs} = usePrefsContext(); - const {t} = useTranslation(); - - const selectedFormats = formats.filter(format => format.selected); - - const visibleFormats = sortFormats( - filteredFormats(formats, prefs.storyFormatListFilter) - ); - - // Any formats no longer visible should be deselected. - - React.useEffect(() => { - for (const format of selectedFormats) { - if (format.selected && !visibleFormats.includes(format)) { - formatsDispatch(deselectFormat(format)); - } - } - }, [prefsDispatch, selectedFormats, visibleFormats, formatsDispatch]); - - function handleSelect(format: StoryFormat) { - formatsDispatch(selectFormat(format, true)); - } - - return ( -
- - - formatsDispatch(deselectAllFormats())} - > - -

- {t( - visibleFormats.length > 0 - ? 'routes.storyFormatList.storyFormatExplanation' - : 'routes.storyFormatList.noneVisible' - )} -

- - - - {visibleFormats.map(format => ( - - - format.name === disabledFormat.name && - format.version === disabledFormat.version - )} - format={format} - onSelect={() => handleSelect(format)} - proofingFormat={ - format.name === prefs.proofingFormat.name && - format.version === prefs.proofingFormat.version - } - /> - - ))} - - - -
-
-
-
- ); -}; diff --git a/src/routes/story-format-list/toolbar/__tests__/story-format-list-toolbar.test.tsx b/src/routes/story-format-list/toolbar/__tests__/story-format-list-toolbar.test.tsx deleted file mode 100644 index 41ba11c7b..000000000 --- a/src/routes/story-format-list/toolbar/__tests__/story-format-list-toolbar.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import {act, render, screen} from '@testing-library/react'; -import {createMemoryHistory} from 'history'; -import {axe} from 'jest-axe'; -import * as React from 'react'; -import {Router} from 'react-router-dom'; -import {FakeStateProvider} from '../../../../test-util'; -import {StoryFormatListToolbar} from '../story-format-list-toolbar'; - -describe('', () => { - async function renderComponent() { - const result = render( - - - - - - ); - - await act(() => Promise.resolve()); - return result; - } - - it('displays a Formats tab', async () => { - await renderComponent(); - expect(screen.getByText('common.storyFormat')).toBeInTheDocument(); - }); - - it('displays a View tab', async () => { - await renderComponent(); - expect(screen.getByText('common.view')).toBeInTheDocument(); - }); - - it('displays an app tab', async () => { - await renderComponent(); - expect(screen.getByText('common.appName')).toBeInTheDocument(); - }); - - it('is accessible', async () => { - const {container} = await renderComponent(); - - expect(await axe(container)).toHaveNoViolations(); - }); -}); diff --git a/src/routes/story-format-list/toolbar/formats/__tests__/default-story-format-button.test.tsx b/src/routes/story-format-list/toolbar/formats/__tests__/default-story-format-button.test.tsx deleted file mode 100644 index dc6f11182..000000000 --- a/src/routes/story-format-list/toolbar/formats/__tests__/default-story-format-button.test.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import {fireEvent, render, screen} from '@testing-library/react'; -import {axe} from 'jest-axe'; -import * as React from 'react'; -import {useStoryFormatsContext} from '../../../../../store/story-formats'; -import { - fakeFailedStoryFormat, - fakeLoadedStoryFormat, - fakePendingStoryFormat, - FakeStateProvider, - FakeStateProviderProps, - PrefInspector -} from '../../../../../test-util'; -import { - DefaultStoryFormatButton, - DefaultStoryFormatButtonProps -} from '../default-story-format-button'; - -const TestDefaultStoryFormatButton: React.FC = props => { - const {formats} = useStoryFormatsContext(); - - return ; -}; - -describe('', () => { - function renderComponent( - props?: Partial, - contexts?: FakeStateProviderProps - ) { - return render( - - - - - ); - } - - it('is disabled if no format is specified', () => { - renderComponent({format: undefined}); - expect(screen.getByRole('button')).toBeDisabled(); - }); - - it('is disabled if the format is pending', () => { - const format = fakePendingStoryFormat(); - - renderComponent({format}, {storyFormats: [format]}); - expect(screen.getByRole('button')).toBeDisabled(); - }); - - it('is disabled if the format failed to load', () => { - const format = fakeFailedStoryFormat(); - - renderComponent({format}, {storyFormats: [format]}); - expect(screen.getByRole('button')).toBeDisabled(); - }); - - it('is disabled if the format is a proofing format', () => { - const format = fakeLoadedStoryFormat(); - - (format as any).properties.proofing = true; - renderComponent({format}, {storyFormats: [format]}); - expect(screen.getByRole('button')).toBeDisabled(); - }); - - it('is disabled if the format is already set as default', () => { - const format = fakeLoadedStoryFormat(); - - renderComponent( - {format}, - { - prefs: {storyFormat: {name: format.name, version: format.version}}, - storyFormats: [format] - } - ); - expect(screen.getByRole('button')).toBeDisabled(); - }); - - it('is enabled is the format is not proofing and not default', () => { - const format = fakeLoadedStoryFormat(); - - renderComponent({format}, {storyFormats: [format]}); - expect(screen.getByRole('button')).toBeEnabled(); - }); - - it('sets the story format preference when clicked', () => { - const format = fakeLoadedStoryFormat(); - - renderComponent({format}, {storyFormats: [format]}); - expect( - JSON.parse(screen.getByTestId('pref-inspector-storyFormat').textContent!) - ).not.toEqual({ - name: format.name, - version: format.version - }); - fireEvent.click( - screen.getByRole('button', { - name: 'routes.storyFormatList.toolbar.useAsDefaultFormat' - }) - ); - expect( - JSON.parse(screen.getByTestId('pref-inspector-storyFormat').textContent!) - ).toEqual({ - name: format.name, - version: format.version - }); - }); - - it('is accessible', async () => { - const {container} = renderComponent(); - - expect(await axe(container)).toHaveNoViolations(); - }); -}); diff --git a/src/routes/story-format-list/toolbar/formats/__tests__/format-actions.test.tsx b/src/routes/story-format-list/toolbar/formats/__tests__/format-actions.test.tsx deleted file mode 100644 index 1c72ee879..000000000 --- a/src/routes/story-format-list/toolbar/formats/__tests__/format-actions.test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import {act, render, screen} from '@testing-library/react'; -import {axe} from 'jest-axe'; -import * as React from 'react'; -import {FormatActions, FormatActionsProps} from '../format-actions'; - -describe('', () => { - async function renderComponent(props?: Partial) { - const result = render(); - - // Need this because of - await act(async () => Promise.resolve()); - return result; - } - - it('displays a button to add story formats', async () => { - await renderComponent(); - expect( - screen.getByRole('button', {name: 'common.add'}) - ).toBeInTheDocument(); - }); - - it('displays a button to remove story formats', async () => { - await renderComponent(); - expect( - screen.getByRole('button', {name: 'common.remove'}) - ).toBeInTheDocument(); - }); - - it('displays a button to manage story format extensions', async () => { - await renderComponent(); - expect( - screen.getByRole('button', { - name: 'routes.storyFormatList.toolbar.disableFormatExtensions' - }) - ).toBeInTheDocument(); - }); - - it('displays a button to set the default story format', async () => { - await renderComponent(); - expect( - screen.getByRole('button', { - name: 'routes.storyFormatList.toolbar.useAsDefaultFormat' - }) - ).toBeInTheDocument(); - }); - - it('displays a button to set the proofing format', async () => { - await renderComponent(); - expect( - screen.getByRole('button', { - name: 'routes.storyFormatList.toolbar.useAsProofingFormat' - }) - ).toBeInTheDocument(); - }); - - it('is accessible', async () => { - const {container} = await renderComponent(); - - expect(await axe(container)).toHaveNoViolations(); - }); -}); diff --git a/src/routes/story-format-list/toolbar/formats/__tests__/proofing-story-format-button.test.tsx b/src/routes/story-format-list/toolbar/formats/__tests__/proofing-story-format-button.test.tsx deleted file mode 100644 index 86d7398b5..000000000 --- a/src/routes/story-format-list/toolbar/formats/__tests__/proofing-story-format-button.test.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import {fireEvent, render, screen} from '@testing-library/react'; -import {axe} from 'jest-axe'; -import * as React from 'react'; -import {useStoryFormatsContext} from '../../../../../store/story-formats'; -import { - fakeFailedStoryFormat, - fakeLoadedStoryFormat, - fakePendingStoryFormat, - FakeStateProvider, - FakeStateProviderProps, - PrefInspector -} from '../../../../../test-util'; -import { - ProofingStoryFormatButton, - ProofingStoryFormatButtonProps -} from '../proofing-story-format-button'; - -const TestProofingStoryFormatButton: React.FC = props => { - const {formats} = useStoryFormatsContext(); - - return ; -}; - -describe('', () => { - function renderComponent( - props?: Partial, - contexts?: FakeStateProviderProps - ) { - return render( - - - - - ); - } - - it('is disabled if no format is specified', () => { - renderComponent({format: undefined}); - expect(screen.getByRole('button')).toBeDisabled(); - }); - - it('is disabled if the format is pending', () => { - const format = fakePendingStoryFormat(); - - renderComponent({format}, {storyFormats: [format]}); - expect(screen.getByRole('button')).toBeDisabled(); - }); - - it('is disabled if the format failed to load', () => { - const format = fakeFailedStoryFormat(); - - renderComponent({format}, {storyFormats: [format]}); - expect(screen.getByRole('button')).toBeDisabled(); - }); - - it("is disabled if the format isn't a proofing format", () => { - const format = fakeLoadedStoryFormat(); - - (format as any).properties.proofing = false; - renderComponent({format}, {storyFormats: [format]}); - expect(screen.getByRole('button')).toBeDisabled(); - }); - - it('is disabled if the format is already set as proofing', () => { - const format = fakeLoadedStoryFormat(); - - (format as any).properties.proofing = true; - renderComponent( - {format}, - { - prefs: {proofingFormat: {name: format.name, version: format.version}}, - storyFormats: [format] - } - ); - expect(screen.getByRole('button')).toBeDisabled(); - }); - - it('is enabled is the format is proofing and not default', () => { - const format = fakeLoadedStoryFormat(); - - (format as any).properties.proofing = true; - renderComponent({format}, {storyFormats: [format]}); - expect(screen.getByRole('button')).toBeEnabled(); - }); - - it('sets the story format preference when clicked', () => { - const format = fakeLoadedStoryFormat(); - - (format as any).properties.proofing = true; - renderComponent({format}, {storyFormats: [format]}); - expect( - JSON.parse( - screen.getByTestId('pref-inspector-proofingFormat').textContent! - ) - ).not.toEqual({ - name: format.name, - version: format.version - }); - fireEvent.click( - screen.getByRole('button', { - name: 'routes.storyFormatList.toolbar.useAsProofingFormat' - }) - ); - expect( - JSON.parse( - screen.getByTestId('pref-inspector-proofingFormat').textContent! - ) - ).toEqual({ - name: format.name, - version: format.version - }); - }); - - it('is accessible', async () => { - const {container} = renderComponent(); - - expect(await axe(container)).toHaveNoViolations(); - }); -}); diff --git a/src/routes/story-format-list/toolbar/formats/__tests__/remove-story-format-button.test.tsx b/src/routes/story-format-list/toolbar/formats/__tests__/remove-story-format-button.test.tsx deleted file mode 100644 index 796fb18ed..000000000 --- a/src/routes/story-format-list/toolbar/formats/__tests__/remove-story-format-button.test.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import {fireEvent, render, screen} from '@testing-library/react'; -import {axe} from 'jest-axe'; -import * as React from 'react'; -import {useStoryFormatsContext} from '../../../../../store/story-formats'; -import { - fakeLoadedStoryFormat, - FakeStateProvider, - FakeStateProviderProps, - StoryFormatInspector -} from '../../../../../test-util'; -import { - RemoveStoryFormatButton, - RemoveStoryFormatButtonProps -} from '../remove-story-format-button'; - -const TestDefaultStoryFormatButton: React.FC = props => { - const {formats} = useStoryFormatsContext(); - - return ; -}; - -describe('', () => { - function renderComponent( - props?: Partial, - contexts?: FakeStateProviderProps - ) { - return render( - - - - - ); - } - - it('is disabled if no format is specified', () => { - renderComponent({format: undefined}); - expect(screen.getByRole('button')).toBeDisabled(); - }); - - it('is disabled if the format is not user-added', () => { - const format = fakeLoadedStoryFormat({userAdded: false}); - - renderComponent( - {format}, - { - prefs: {storyFormat: {name: format.name, version: format.version}}, - storyFormats: [format] - } - ); - expect(screen.getByRole('button')).toBeDisabled(); - }); - - it('is disabled if the format is the default', () => { - const format = fakeLoadedStoryFormat(); - - renderComponent( - {format}, - { - prefs: {storyFormat: {name: format.name, version: format.version}}, - storyFormats: [format] - } - ); - expect(screen.getByRole('button')).toBeDisabled(); - }); - - it('is disabled if the format is the proofing one', () => { - const format = fakeLoadedStoryFormat(); - - renderComponent( - {format}, - { - prefs: {proofingFormat: {name: format.name, version: format.version}}, - storyFormats: [format] - } - ); - expect(screen.getByRole('button')).toBeDisabled(); - }); - - it('is enabled if the format is user-added, not proofing, and not default', () => { - const format = fakeLoadedStoryFormat({userAdded: true}); - - renderComponent({format}, {storyFormats: [format]}); - expect(screen.getByRole('button')).toBeEnabled(); - }); - - it('removes the story format when clicked', () => { - const format = fakeLoadedStoryFormat({userAdded: true}); - - renderComponent({format}, {storyFormats: [format]}); - expect( - screen.getByTestId('story-format-inspector-default') - ).toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', {name: 'common.remove'})); - expect( - screen.queryByTestId('story-format-inspector-default') - ).not.toBeInTheDocument(); - }); - - it('is accessible', async () => { - const {container} = renderComponent(); - - expect(await axe(container)).toHaveNoViolations(); - }); -}); diff --git a/src/routes/story-format-list/toolbar/formats/__tests__/story-format-extensions-button.test.tsx b/src/routes/story-format-list/toolbar/formats/__tests__/story-format-extensions-button.test.tsx deleted file mode 100644 index a23116f2f..000000000 --- a/src/routes/story-format-list/toolbar/formats/__tests__/story-format-extensions-button.test.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import {fireEvent, render, screen} from '@testing-library/react'; -import {axe} from 'jest-axe'; -import * as React from 'react'; -import { - StoryFormat, - useStoryFormatsContext -} from '../../../../../store/story-formats'; -import { - fakeLoadedStoryFormat, - FakeStateProvider, - FakeStateProviderProps, - PrefInspector -} from '../../../../../test-util'; -import { - StoryFormatExtensionsButton, - StoryFormatExtensionsButtonProps -} from '../story-format-extensions-button'; - -const TestStoryFormatExtensionsButton: React.FC = props => { - const {formats} = useStoryFormatsContext(); - - return ; -}; - -describe('', () => { - function renderComponent( - props?: Partial, - contexts?: FakeStateProviderProps - ) { - return render( - - - - - ); - } - - it('is disabled if no format is specified', () => { - renderComponent({format: undefined}); - expect(screen.getByRole('button')).toBeDisabled(); - }); - - describe('when the format extensions are not already disabled', () => { - let format: StoryFormat; - - beforeEach(() => { - format = fakeLoadedStoryFormat(); - renderComponent( - {format}, - { - prefs: { - disabledStoryFormatEditorExtensions: [ - {name: 'existing', version: '1.2.3'} - ] - } - } - ); - }); - - it('is enabled', () => expect(screen.getByRole('button')).toBeEnabled()); - - it('displays the correct label', () => - expect( - screen.getByText( - 'routes.storyFormatList.toolbar.disableFormatExtensions' - ) - ).toBeEnabled()); - - it('disables extensions when clicked', () => { - fireEvent.click( - screen.getByText( - 'routes.storyFormatList.toolbar.disableFormatExtensions' - ) - ); - expect( - JSON.parse( - screen.getByTestId( - 'pref-inspector-disabledStoryFormatEditorExtensions' - ).textContent! - ) - ).toEqual([ - {name: 'existing', version: '1.2.3'}, - {name: format.name, version: format.version} - ]); - }); - }); - - describe('when the format extensions are already disabled', () => { - let format: StoryFormat; - - beforeEach(() => { - format = fakeLoadedStoryFormat(); - renderComponent( - {format}, - { - prefs: { - disabledStoryFormatEditorExtensions: [ - {name: format.name, version: format.version}, - {name: 'existing', version: '1.2.3'} - ] - } - } - ); - }); - - it('is enabled', () => expect(screen.getByRole('button')).toBeEnabled()); - - it('displays the correct label', () => - expect( - screen.getByText( - 'routes.storyFormatList.toolbar.enableFormatExtensions' - ) - ).toBeEnabled()); - - it('enables extensions when clicked', () => { - fireEvent.click( - screen.getByText( - 'routes.storyFormatList.toolbar.enableFormatExtensions' - ) - ); - expect( - JSON.parse( - screen.getByTestId( - 'pref-inspector-disabledStoryFormatEditorExtensions' - ).textContent! - ) - ).toEqual([{name: 'existing', version: '1.2.3'}]); - }); - }); - - it('is accessible', async () => { - const {container} = renderComponent(); - - expect(await axe(container)).toHaveNoViolations(); - }); -}); diff --git a/src/routes/story-format-list/toolbar/formats/default-story-format-button.tsx b/src/routes/story-format-list/toolbar/formats/default-story-format-button.tsx deleted file mode 100644 index d368b3cba..000000000 --- a/src/routes/story-format-list/toolbar/formats/default-story-format-button.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import {IconStar} from '@tabler/icons'; -import * as React from 'react'; -import {useTranslation} from 'react-i18next'; -import {IconButton} from '../../../../components/control/icon-button'; -import {usePrefsContext} from '../../../../store/prefs'; -import {StoryFormat} from '../../../../store/story-formats'; - -export interface DefaultStoryFormatButtonProps { - format?: StoryFormat; -} - -export const DefaultStoryFormatButton: React.FC = props => { - const {format} = props; - const {dispatch, prefs} = usePrefsContext(); - const {t} = useTranslation(); - - const disabled = - format?.loadState !== 'loaded' || - format.properties.proofing || - (prefs.storyFormat.name === format.name && - prefs.storyFormat.version === format.version); - const handleClick: React.MouseEventHandler = () => { - if (!format) { - throw new Error("Can't set undefined format as default"); - } - - dispatch({ - type: 'update', - name: 'storyFormat', - value: {name: format.name, version: format.version} - }); - }; - - return ( - } - label={t('routes.storyFormatList.toolbar.useAsDefaultFormat')} - onClick={handleClick} - /> - ); -}; diff --git a/src/routes/story-format-list/toolbar/formats/format-actions.tsx b/src/routes/story-format-list/toolbar/formats/format-actions.tsx deleted file mode 100644 index fd7f6503d..000000000 --- a/src/routes/story-format-list/toolbar/formats/format-actions.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; -import {ButtonBar} from '../../../../components/container/button-bar'; -import {StoryFormat} from '../../../../store/story-formats'; -import {AddStoryFormatButton} from './add-story-format-button'; -import {DefaultStoryFormatButton} from './default-story-format-button'; -import {ProofingStoryFormatButton} from './proofing-story-format-button'; -import {RemoveStoryFormatButton} from './remove-story-format-button'; -import {StoryFormatExtensionsButton} from './story-format-extensions-button'; - -export interface FormatActionsProps { - selectedFormats: StoryFormat[]; -} - -export const FormatActions: React.FC = props => { - const {selectedFormats} = props; - const format = selectedFormats.length === 1 ? selectedFormats[0] : undefined; - - return ( - - - - - - - - ); -}; diff --git a/src/routes/story-format-list/toolbar/formats/proofing-story-format-button.tsx b/src/routes/story-format-list/toolbar/formats/proofing-story-format-button.tsx deleted file mode 100644 index 42e341f9c..000000000 --- a/src/routes/story-format-list/toolbar/formats/proofing-story-format-button.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import {IconEye} from '@tabler/icons'; -import * as React from 'react'; -import {useTranslation} from 'react-i18next'; -import {IconButton} from '../../../../components/control/icon-button'; -import {usePrefsContext} from '../../../../store/prefs'; -import {StoryFormat} from '../../../../store/story-formats'; - -export interface ProofingStoryFormatButtonProps { - format?: StoryFormat; -} - -export const ProofingStoryFormatButton: React.FC = props => { - const {format} = props; - const {dispatch, prefs} = usePrefsContext(); - const {t} = useTranslation(); - - const disabled = - format?.loadState !== 'loaded' || - !format.properties.proofing || - (prefs.proofingFormat.name === format.name && - prefs.proofingFormat.version === format.version); - const handleClick: React.MouseEventHandler = () => { - if (!format) { - throw new Error("Can't set undefined format as proofing"); - } - - dispatch({ - type: 'update', - name: 'proofingFormat', - value: {name: format.name, version: format.version} - }); - }; - - return ( - } - label={t('routes.storyFormatList.toolbar.useAsProofingFormat')} - onClick={handleClick} - /> - ); -}; diff --git a/src/routes/story-format-list/toolbar/formats/remove-story-format-button.tsx b/src/routes/story-format-list/toolbar/formats/remove-story-format-button.tsx deleted file mode 100644 index 878404845..000000000 --- a/src/routes/story-format-list/toolbar/formats/remove-story-format-button.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import {IconTrash} from '@tabler/icons'; -import * as React from 'react'; -import {useTranslation} from 'react-i18next'; -import {IconButton} from '../../../../components/control/icon-button'; -import {usePrefsContext} from '../../../../store/prefs'; -import { - deleteFormat, - StoryFormat, - useStoryFormatsContext -} from '../../../../store/story-formats'; - -export interface RemoveStoryFormatButtonProps { - format?: StoryFormat; -} - -export const RemoveStoryFormatButton: React.FC = props => { - const {format} = props; - const {dispatch} = useStoryFormatsContext(); - const {prefs} = usePrefsContext(); - const {t} = useTranslation(); - - const isDefault = - format?.name === prefs.storyFormat.name && - format?.version === prefs.storyFormat.version; - const isProofing = - format?.name === prefs.proofingFormat.name && - format?.version === prefs.proofingFormat.version; - - return ( - } - label={t('common.remove')} - onClick={() => format && dispatch(deleteFormat(format))} - /> - ); -}; diff --git a/src/routes/story-format-list/toolbar/formats/story-format-extensions-button.tsx b/src/routes/story-format-list/toolbar/formats/story-format-extensions-button.tsx deleted file mode 100644 index db4ce3648..000000000 --- a/src/routes/story-format-list/toolbar/formats/story-format-extensions-button.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import {IconPuzzle} from '@tabler/icons'; -import * as React from 'react'; -import {useTranslation} from 'react-i18next'; -import {IconButton} from '../../../../components/control/icon-button'; -import {setPref, usePrefsContext} from '../../../../store/prefs'; -import {StoryFormat} from '../../../../store/story-formats'; - -export interface StoryFormatExtensionsButtonProps { - format?: StoryFormat; -} - -export const StoryFormatExtensionsButton: React.FC = props => { - const {format} = props; - const {dispatch, prefs} = usePrefsContext(); - const {t} = useTranslation(); - - const extensionsDisabled = prefs.disabledStoryFormatEditorExtensions.some( - other => other.name === format?.name && other.version === format?.version - ); - - function handleClick() { - if (!format) { - throw new Error('Format is not set'); - } - - // This logic is a little backwards--the user is setting whether to use the - // extensions but our preferences track disabled ones. - - if (extensionsDisabled) { - dispatch( - setPref( - 'disabledStoryFormatEditorExtensions', - prefs.disabledStoryFormatEditorExtensions.filter( - f => f.name !== format.name || f.version !== format.version - ) - ) - ); - } else { - dispatch( - setPref('disabledStoryFormatEditorExtensions', [ - ...prefs.disabledStoryFormatEditorExtensions, - {name: format.name, version: format.version} - ]) - ); - } - } - - return ( - } - label={t( - `routes.storyFormatList.toolbar.${ - extensionsDisabled ? 'enable' : 'disable' - }FormatExtensions` - )} - onClick={handleClick} - /> - ); -}; diff --git a/src/routes/story-format-list/toolbar/index.ts b/src/routes/story-format-list/toolbar/index.ts deleted file mode 100644 index bc2e6f10b..000000000 --- a/src/routes/story-format-list/toolbar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './story-format-list-toolbar'; diff --git a/src/routes/story-format-list/toolbar/story-format-list-toolbar.tsx b/src/routes/story-format-list/toolbar/story-format-list-toolbar.tsx deleted file mode 100644 index 3b7860660..000000000 --- a/src/routes/story-format-list/toolbar/story-format-list-toolbar.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react'; -import {useTranslation} from 'react-i18next'; -import {RouteToolbar} from '../../../components/route-toolbar'; -import {AppActions} from '../../../route-actions'; -import {StoryFormat} from '../../../store/story-formats'; -import {FormatActions} from './formats/format-actions'; -import {ViewActions} from './view-actions'; - -export interface StoryFormatListToolbarProps { - selectedFormats: StoryFormat[]; -} - -export const StoryFormatListToolbar: React.FC = props => { - const {selectedFormats} = props; - const {t} = useTranslation(); - - return ( - - ), - [t('common.view')]: , - [t('common.appName')]: - }} - /> - ); -}; diff --git a/src/routes/story-format-list/toolbar/view-actions.tsx b/src/routes/story-format-list/toolbar/view-actions.tsx deleted file mode 100644 index d94ba5c55..000000000 --- a/src/routes/story-format-list/toolbar/view-actions.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from 'react'; -import {useTranslation} from 'react-i18next'; -import {CheckboxButton} from '../../../components/control/checkbox-button'; -import {setPref, usePrefsContext} from '../../../store/prefs'; - -export const ViewActions: React.FC = () => { - const {dispatch, prefs} = usePrefsContext(); - const {t} = useTranslation(); - - function setFilter(name: string) { - dispatch(setPref('storyFormatListFilter', name)); - } - - return ( - <> - setFilter('current')} - value={prefs.storyFormatListFilter === 'current'} - /> - setFilter('user')} - value={prefs.storyFormatListFilter === 'user'} - /> - setFilter('all')} - value={prefs.storyFormatListFilter === 'all'} - /> - - ); -}; diff --git a/src/store/persistence/electron-ipc/story-formats/__tests__/load.test.ts b/src/store/persistence/electron-ipc/story-formats/__tests__/load.test.ts index 6a64cbe60..15f62f43e 100644 --- a/src/store/persistence/electron-ipc/story-formats/__tests__/load.test.ts +++ b/src/store/persistence/electron-ipc/story-formats/__tests__/load.test.ts @@ -18,10 +18,7 @@ describe('story formats Electron IPC load', () => { afterEach(() => delete electronWindow.twineElectron); it('resolves to data from calling loadStoryFormats on the twineElectron global', async () => { - const storyFormats = [ - fakeUnloadedStoryFormat({selected: false}), - fakeUnloadedStoryFormat({selected: false}) - ]; + const storyFormats = [fakeUnloadedStoryFormat(), fakeUnloadedStoryFormat()]; mockLoadStoryFormats(storyFormats); expect(await load()).toEqual([ diff --git a/src/store/persistence/electron-ipc/story-formats/load.ts b/src/store/persistence/electron-ipc/story-formats/load.ts index 7118149ce..3e5143622 100644 --- a/src/store/persistence/electron-ipc/story-formats/load.ts +++ b/src/store/persistence/electron-ipc/story-formats/load.ts @@ -19,7 +19,6 @@ export async function load(): Promise { id: uuid(), loadState: 'unloaded', name: data.name, - selected: false, version: data.version, url: data.url, userAdded: data.userAdded diff --git a/src/store/story-formats/__tests__/reducer.test.ts b/src/store/story-formats/__tests__/reducer.test.ts index 273636200..a5fcb8ac0 100644 --- a/src/store/story-formats/__tests__/reducer.test.ts +++ b/src/store/story-formats/__tests__/reducer.test.ts @@ -7,7 +7,6 @@ describe('Story format reducer', () => { const builtinFormats = builtins().map(format => ({ ...format, loadState: 'unloaded', - selected: false, userAdded: false })); let format: StoryFormat; diff --git a/src/store/story-formats/action-creators.ts b/src/store/story-formats/action-creators.ts index 99fd9087b..0bd21b3cb 100644 --- a/src/store/story-formats/action-creators.ts +++ b/src/store/story-formats/action-creators.ts @@ -1,5 +1,4 @@ import {Thunk} from 'react-hook-thunk-reducer'; -import {StoryFormatsState} from '.'; import {fetchStoryFormatProperties} from '../../util/story-format/fetch-properties'; import { StoryFormat, @@ -24,7 +23,6 @@ export function createFromProperties( props: { url, name: properties.name, - selected: false, userAdded: true, version: properties.version } @@ -38,26 +36,6 @@ export function deleteFormat(format: StoryFormat): StoryFormatsAction { return {type: 'delete', id: format.id}; } -/** - * Deselects all formats. - */ -export function deselectAllFormats(): Thunk { - return (dispatch, getFormats) => { - for (const format of getFormats()) { - if (format.selected) { - dispatch({type: 'update', id: format.id, props: {selected: false}}); - } - } - }; -} - -/** - * Deselects a single format. - */ -export function deselectFormat(format: StoryFormat): StoryFormatsAction { - return {type: 'update', id: format.id, props: {selected: false}}; -} - async function loadFormatThunk( format: StoryFormat, dispatch: StoryFormatsDispatch @@ -105,7 +83,7 @@ async function loadFormatThunk( dispatch({ type: 'update', id: format.id, - props: {loadError: (loadError as unknown) as Error, loadState: 'error'} + props: {loadError: loadError as unknown as Error, loadState: 'error'} }); } } @@ -145,33 +123,3 @@ export function loadFormatProperties(format: StoryFormat) { return async (dispatch: StoryFormatsDispatch) => await loadFormatThunk(format, dispatch); } - -/** - * Selects a single format. - */ -export function selectFormat( - format: StoryFormat, - exclusive?: boolean -): Thunk { - return (dispatch, getState) => { - if (!format.selected) { - dispatch({ - type: 'update', - id: format.id, - props: {selected: true} - }); - } - - if (exclusive) { - for (const other of getState()) { - if (other.id !== format.id && other.selected) { - dispatch({ - type: 'update', - id: other.id, - props: {selected: false} - }); - } - } - } - }; -} \ No newline at end of file diff --git a/src/store/story-formats/reducer.ts b/src/store/story-formats/reducer.ts index a051242db..8324539dd 100644 --- a/src/store/story-formats/reducer.ts +++ b/src/store/story-formats/reducer.ts @@ -53,7 +53,6 @@ export const reducer: React.Reducer = ( ...builtinFormat, id: uuid(), loadState: 'unloaded', - selected: false, userAdded: false }); } diff --git a/src/store/story-formats/story-formats.types.ts b/src/store/story-formats/story-formats.types.ts index 5fc7c0ab7..2820cbd7f 100644 --- a/src/store/story-formats/story-formats.types.ts +++ b/src/store/story-formats/story-formats.types.ts @@ -5,7 +5,6 @@ interface BaseStoryFormat { id: string; loadState: 'unloaded' | 'loading' | 'loaded' | 'error'; name: string; - selected: boolean; url: string; userAdded: boolean; version: string; diff --git a/src/test-util/fakes.ts b/src/test-util/fakes.ts index d54fb4346..8615c124d 100644 --- a/src/test-util/fakes.ts +++ b/src/test-util/fakes.ts @@ -36,7 +36,6 @@ export function fakeFailedStoryFormat( return { id: faker.string.uuid(), name: faker.lorem.words(2), - selected: faker.datatype.boolean(), url: '', userAdded: false, version: faker.system.semver(), @@ -70,7 +69,6 @@ export function fakeLoadedStoryFormat( ...loadProps }, name: formatName, - selected: faker.datatype.boolean(), url: formatUrl, userAdded: false, version: formatVersion, @@ -86,7 +84,6 @@ export function fakePendingStoryFormat( id: faker.string.uuid(), loadState: 'loading', name: faker.lorem.words(2), - selected: faker.datatype.boolean(), url: faker.internet.url(), userAdded: false, version: faker.system.semver(), @@ -101,7 +98,6 @@ export function fakeUnloadedStoryFormat( id: faker.string.uuid(), loadState: 'unloaded', name: faker.lorem.words(2), - selected: faker.datatype.boolean(), url: faker.internet.url(), userAdded: false, version: faker.system.semver(), From 399471d191730c5f7c49c055e7eac3dfa39ff28a Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Mon, 23 Sep 2024 21:40:29 -0400 Subject: [PATCH 2/7] Validate story format on submit Resolves #1237 --- .../control/__tests__/prompt-button.test.tsx | 51 +++++++++++++------ src/components/control/prompt-button.css | 3 ++ src/components/control/prompt-button.tsx | 29 +++++++++-- .../add-story-format-button.test.tsx | 9 ++-- .../story-formats/add-story-format-button.tsx | 1 + src/dialogs/story-formats/story-formats.css | 2 +- 6 files changed, 72 insertions(+), 23 deletions(-) create mode 100644 src/components/control/prompt-button.css diff --git a/src/components/control/__tests__/prompt-button.test.tsx b/src/components/control/__tests__/prompt-button.test.tsx index df5bee39d..b4a5ebd26 100644 --- a/src/components/control/__tests__/prompt-button.test.tsx +++ b/src/components/control/__tests__/prompt-button.test.tsx @@ -124,23 +124,42 @@ describe('', () => { expect(onChange).toHaveBeenCalledTimes(1); }); - it('prevents submission if the validate prop blocks it', async () => { - const onSubmit = jest.fn(); + it.each(['change', 'submit'])( + 'prevents submission if the validate prop blocks it when the validateOn prop is "%s"', + async validateOn => { + const onSubmit = jest.fn(); - renderComponent({ - onSubmit, - validate, - prompt: 'test-prompt', - submitLabel: 'test-submit', - value: 'bad' - }); - fireEvent.click(screen.getByRole('button')); - await waitFor(() => - expect(screen.getByRole('button', {name: 'test-submit'})).toBeDisabled() - ); - fireEvent.submit(screen.getByRole('textbox', {name: 'test-prompt'})); - expect(onSubmit).not.toHaveBeenCalled(); - }); + renderComponent({ + onSubmit, + validate, + validateOn: validateOn as 'change' | 'submit', + prompt: 'test-prompt', + submitLabel: 'test-submit', + value: 'bad' + }); + fireEvent.click(screen.getByRole('button')); + + if (validateOn === 'change') { + // The button won't disable itself right away if we're validating on submit. + + await waitFor(() => + expect( + screen.getByRole('button', {name: 'test-submit'}) + ).toBeDisabled() + ); + } + fireEvent.submit(screen.getByRole('textbox', {name: 'test-prompt'})); + + if (validateOn === 'submit') { + // ... but should now be disabled if we're validating on submit. + + expect( + screen.getByRole('button', {name: 'test-submit'}) + ).toBeDisabled(); + } + expect(onSubmit).not.toHaveBeenCalled(); + } + ); it('is accessible', async () => { const {container} = renderComponent(); diff --git a/src/components/control/prompt-button.css b/src/components/control/prompt-button.css new file mode 100644 index 000000000..0b001fe6a --- /dev/null +++ b/src/components/control/prompt-button.css @@ -0,0 +1,3 @@ +.prompt-button .validation-message { + padding: 0; +} \ No newline at end of file diff --git a/src/components/control/prompt-button.tsx b/src/components/control/prompt-button.tsx index 64509a4a4..a4262e98c 100644 --- a/src/components/control/prompt-button.tsx +++ b/src/components/control/prompt-button.tsx @@ -6,6 +6,7 @@ import {CardContent} from '../container/card'; import {CardButton, CardButtonProps} from './card-button'; import {IconButton, IconButtonProps} from './icon-button'; import {TextInput} from './text-input'; +import './prompt-button.css'; export interface PromptValidationResponse { message?: string; @@ -27,6 +28,7 @@ export interface PromptButtonProps submitLabel?: string; submitVariant?: IconButtonProps['variant']; validate?: PromptButtonValidator; + validateOn?: 'change' | 'submit'; value: string; } @@ -41,6 +43,7 @@ export const PromptButton: React.FC = props => { submitLabel, submitVariant, validate, + validateOn = 'change', value, ...other } = props; @@ -52,7 +55,7 @@ export const PromptButton: React.FC = props => { React.useEffect(() => { async function updateValidation() { - if (validate) { + if (validateOn === 'change' && validate) { const validation = await validate(value); if (mounted.current) { @@ -79,9 +82,27 @@ export const PromptButton: React.FC = props => { setOpen(false); } - function handleSubmit(event: React.FormEvent) { + async function handleSubmit(event: React.FormEvent) { event.preventDefault(); + if (validateOn === 'submit' && validate) { + // Temporarily set us invalid so that the submit button is disabled while + // validation occurs, then run validation and update accordingly. If we + // fail validation here, then stop. + + setValidation(value => ({...value, valid: false})); + + const validation = await validate(value); + + setValidation(validation); + + if (!validation.valid) { + return; + } + } else { + setValidation({valid: true}); + } + // It's possible to submit with the Enter key and bypass us disabling the // submit button, so we need to catch that here. @@ -104,7 +125,9 @@ export const PromptButton: React.FC = props => { {prompt} - {validation?.message &&

{validation.message}

} + {validation?.message && ( +

{validation.message}

+ )} ', () => { expect(formatInspector.dataset.version).toBe(format.version); }); - it('shows an error if an invalid URL is entered', async () => { + it('shows an error on submit if an invalid URL is entered', async () => { await renderComponent(); fireEvent.change( screen.getByRole('textbox', { @@ -83,6 +83,7 @@ describe('', () => { }), {target: {value: 'not a url'}} ); + fireEvent.click(getAddButton()); await act(async () => Promise.resolve()); expect( screen.getByText('dialogs.storyFormats.addStoryFormatButton.invalidUrl') @@ -90,7 +91,7 @@ describe('', () => { expect(getAddButton()).toBeDisabled(); }); - it('shows an error if fetching story properties fails', async () => { + it('shows an error on submit if fetching story properties fails', async () => { fetchStoryFormatPropertiesMock.mockRejectedValue(new Error()); await renderComponent(); fireEvent.change( @@ -99,6 +100,7 @@ describe('', () => { }), {target: {value: 'http://mock-format-url'}} ); + fireEvent.click(getAddButton()); await act(async () => Promise.resolve()); expect( screen.getByText('dialogs.storyFormats.addStoryFormatButton.fetchError') @@ -106,7 +108,7 @@ describe('', () => { expect(getAddButton()).toBeDisabled(); }); - it('shows an error if the URL points to a format with the same name and version as a format that already exists', async () => { + it('shows an error on submit if the URL points to a format with the same name and version as a format that already exists', async () => { const format = fakeLoadedStoryFormat(); fetchStoryFormatPropertiesMock.mockResolvedValue( @@ -119,6 +121,7 @@ describe('', () => { }), {target: {value: 'http://mock-format-url'}} ); + fireEvent.click(getAddButton()); await act(async () => Promise.resolve()); expect( screen.getByText('dialogs.storyFormats.addStoryFormatButton.alreadyAdded') diff --git a/src/dialogs/story-formats/add-story-format-button.tsx b/src/dialogs/story-formats/add-story-format-button.tsx index 7ccf9707b..3b028e342 100644 --- a/src/dialogs/story-formats/add-story-format-button.tsx +++ b/src/dialogs/story-formats/add-story-format-button.tsx @@ -97,6 +97,7 @@ export const AddStoryFormatButton: React.FC = () => { submitLabel={t('common.add')} submitVariant="create" validate={validate} + validateOn="submit" value={newFormatUrl} /> diff --git a/src/dialogs/story-formats/story-formats.css b/src/dialogs/story-formats/story-formats.css index 3e3094479..cd621c335 100644 --- a/src/dialogs/story-formats/story-formats.css +++ b/src/dialogs/story-formats/story-formats.css @@ -10,7 +10,7 @@ flex: 1 1 0; } -.story-formats-dialog .card-content > p { +.story-formats-dialog .card-content > p:not(.validation-message) { padding: var(--grid-size); } From 96e4c4ed06a7f66aa9a92d3e1a19ad4c81970b5d Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Tue, 5 Nov 2024 22:33:28 -0500 Subject: [PATCH 3/7] Add preference to disable CodeMirror --- public/locales/en-US.json | 3 +- .../control/code-area/__mocks__/code-area.tsx | 22 ++--- .../code-area/__tests__/code-area.test.tsx | 93 ++++++++++++++----- .../control/code-area/code-area.css | 15 +-- .../control/code-area/code-area.tsx | 76 ++++++++++++--- src/dialogs/__tests__/app-prefs.test.tsx | 57 ++++++++++++ .../__tests__/story-javascript.test.tsx | 58 +++++++++++- src/dialogs/__tests__/story-search.test.tsx | 16 ++++ .../__tests__/story-stylesheet.test.tsx | 58 +++++++++++- src/dialogs/app-prefs.tsx | 16 ++++ .../__tests__/passage-edit-contents.test.tsx | 42 +++++++-- .../__tests__/passage-text.test.tsx | 14 +++ .../__tests__/passage-toolbar.test.tsx | 30 +++++- .../passage-edit/passage-edit-contents.tsx | 5 +- src/dialogs/passage-edit/passage-text.tsx | 19 ++-- src/dialogs/passage-edit/passage-toolbar.tsx | 19 ++-- src/dialogs/story-javascript.tsx | 22 ++--- src/dialogs/story-search.tsx | 22 ++--- src/dialogs/story-stylesheet.tsx | 22 ++--- src/store/prefs/defaults.ts | 1 + src/store/prefs/prefs.types.ts | 4 + src/test-util/fakes.ts | 3 + 22 files changed, 492 insertions(+), 125 deletions(-) diff --git a/public/locales/en-US.json b/public/locales/en-US.json index 438de350a..616317053 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -175,7 +175,8 @@ "themeDark": "Dark", "themeSystem": "System", "theme": "Theme", - "title": "Preferences" + "title": "Preferences", + "useEnhancedEditors": "Use Enhanced Editors" }, "passageEdit": { "editorCrashed": "Something went wrong with this editor. Try closing it and editing this passage again.", diff --git a/src/components/control/code-area/__mocks__/code-area.tsx b/src/components/control/code-area/__mocks__/code-area.tsx index 3aa229534..f289f0fd9 100644 --- a/src/components/control/code-area/__mocks__/code-area.tsx +++ b/src/components/control/code-area/__mocks__/code-area.tsx @@ -3,19 +3,14 @@ import {CodeAreaProps} from '../code-area'; export const CodeArea: React.FC = props => { function handleOnChange(e: React.ChangeEvent) { - props.onBeforeChange( - { - historySize: () => ({ - redo: 0, - undo: 0 - }), - mockCodeMirrorEditor: true - } as any, - { - mockCodeMirrorChange: true - } as any, - e.target.value - ); + props.onChangeEditor?.({ + historySize: () => ({ + redo: 0, + undo: 0 + }), + mockCodeMirrorEditor: true + } as any); + props.onChangeText(e.target.value); } return ( @@ -24,6 +19,7 @@ export const CodeArea: React.FC = props => { data-font-family={props.fontFamily} data-font-scale={props.fontScale} data-options={JSON.stringify(props.options)} + data-use-code-mirror={props.useCodeMirror} >
); }; diff --git a/src/dialogs/__tests__/app-prefs.test.tsx b/src/dialogs/__tests__/app-prefs.test.tsx index 7bd21171f..6060e15f0 100644 --- a/src/dialogs/__tests__/app-prefs.test.tsx +++ b/src/dialogs/__tests__/app-prefs.test.tsx @@ -27,6 +27,7 @@ describe('', () => { + ); } @@ -198,6 +199,62 @@ describe('', () => { ).toHaveTextContent('1.25'); }); + it('displays the CodeMirror preference', () => { + renderComponent({useCodeMirror: true}); + + expect( + screen.getByRole('checkbox', { + name: 'dialogs.appPrefs.useEnhancedEditors' + }) + ).toBeChecked(); + cleanup(); + renderComponent({useCodeMirror: false}); + expect( + screen.getByRole('checkbox', { + name: 'dialogs.appPrefs.useEnhancedEditors' + }) + ).not.toBeChecked(); + }); + + it('changes the CodeMirror preference and not the cursor preference when enabled', () => { + renderComponent({editorCursorBlinks: false, useCodeMirror: false}); + expect( + screen.getByTestId('pref-inspector-useCodeMirror') + ).toHaveTextContent('false'); + fireEvent.click( + screen.getByRole('checkbox', { + name: 'dialogs.appPrefs.useEnhancedEditors' + }) + ); + expect( + screen.getByTestId('pref-inspector-editorCursorBlinks') + ).toHaveTextContent('false'); + expect( + screen.getByTestId('pref-inspector-useCodeMirror') + ).toHaveTextContent('true'); + }); + + it('changes the CodeMirror preference and the cursor preference when disabled', () => { + renderComponent({editorCursorBlinks: false, useCodeMirror: true}); + expect( + screen.getByTestId('pref-inspector-editorCursorBlinks') + ).toHaveTextContent('false'); + expect( + screen.getByTestId('pref-inspector-useCodeMirror') + ).toHaveTextContent('true'); + fireEvent.click( + screen.getByRole('checkbox', { + name: 'dialogs.appPrefs.useEnhancedEditors' + }) + ); + expect( + screen.getByTestId('pref-inspector-editorCursorBlinks') + ).toHaveTextContent('true'); + expect( + screen.getByTestId('pref-inspector-useCodeMirror') + ).toHaveTextContent('false'); + }); + it('is accessible', async () => { const {container} = renderComponent(); diff --git a/src/dialogs/__tests__/story-javascript.test.tsx b/src/dialogs/__tests__/story-javascript.test.tsx index 31c8d8a5c..4ce675ac3 100644 --- a/src/dialogs/__tests__/story-javascript.test.tsx +++ b/src/dialogs/__tests__/story-javascript.test.tsx @@ -105,8 +105,62 @@ describe('', () => { ).toEqual(expect.objectContaining({cursorBlinkRate: 0})); }); - it.todo('indents code with its indent buttons'); - it.todo('undos and redos changes with the undo/redo buttons'); + describe('When CodeMirror is enabled', () => { + it('uses CodeMirror on its code area', () => { + renderComponent({prefs: {useCodeMirror: true}}); + expect(screen.getByTestId('mock-code-area')!.dataset.useCodeMirror).toBe( + 'true' + ); + }); + + it('shows undo, redo, and indent buttons', () => { + renderComponent({prefs: {useCodeMirror: true}}); + expect( + screen.getByRole('button', {name: 'common.undo'}) + ).toBeInTheDocument(); + expect( + screen.getByRole('button', {name: 'common.redo'}) + ).toBeInTheDocument(); + expect( + screen.getByRole('button', {name: 'components.indentButtons.indent'}) + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { + name: 'components.indentButtons.unindent' + }) + ).toBeInTheDocument(); + }); + + it.todo('indents code with its indent buttons'); + it.todo('undos and redos changes with the undo/redo buttons'); + }); + + describe('When CodeMirror is disabled', () => { + it("doesn't use CodeMirror on its code area", () => { + renderComponent({prefs: {useCodeMirror: false}}); + expect(screen.getByTestId('mock-code-area')!.dataset.useCodeMirror).toBe( + 'false' + ); + }); + + it('hides undo, redo, and indent buttons', () => { + renderComponent({prefs: {useCodeMirror: false}}); + expect( + screen.queryByRole('button', {name: 'common.undo'}) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', {name: 'common.redo'}) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', {name: 'components.indentButtons.indent'}) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { + name: 'components.indentButtons.unindent' + }) + ).not.toBeInTheDocument(); + }); + }); it('is accessible', async () => { const {container} = renderComponent(); diff --git a/src/dialogs/__tests__/story-search.test.tsx b/src/dialogs/__tests__/story-search.test.tsx index 8eaedc338..2f010812b 100644 --- a/src/dialogs/__tests__/story-search.test.tsx +++ b/src/dialogs/__tests__/story-search.test.tsx @@ -315,6 +315,22 @@ describe('', () => { expect(screen.getByText('dialogs.storySearch.replaceAll')).toBeDisabled(); }); + it('uses CodeMirror on its code areas when CodeMirror is enabled', () => { + renderComponent({}, {prefs: {useCodeMirror: true}}); + + for (const area of screen.getAllByTestId('mock-code-area')) { + expect(area.dataset.useCodeMirror).toBe('true'); + } + }); + + it('disables CodeMirror on its code areas when CodeMirror is disabled', () => { + renderComponent({}, {prefs: {useCodeMirror: false}}); + + for (const area of screen.getAllByTestId('mock-code-area')) { + expect(area.dataset.useCodeMirror).toBe('false'); + } + }); + it('is accessible', async () => { const {container} = renderComponent(); diff --git a/src/dialogs/__tests__/story-stylesheet.test.tsx b/src/dialogs/__tests__/story-stylesheet.test.tsx index 1e00ca1c4..4cf7c80e6 100644 --- a/src/dialogs/__tests__/story-stylesheet.test.tsx +++ b/src/dialogs/__tests__/story-stylesheet.test.tsx @@ -103,8 +103,62 @@ describe('', () => { ).toEqual(expect.objectContaining({cursorBlinkRate: 0})); }); - it.todo('indents code with its indent buttons'); - it.todo('undos and redos changes with the undo/redo buttons'); + describe('When CodeMirror is enabled', () => { + it('uses CodeMirror on its code area', () => { + renderComponent({prefs: {useCodeMirror: true}}); + expect(screen.getByTestId('mock-code-area')!.dataset.useCodeMirror).toBe( + 'true' + ); + }); + + it('shows undo, redo, and indent buttons', () => { + renderComponent({prefs: {useCodeMirror: true}}); + expect( + screen.getByRole('button', {name: 'common.undo'}) + ).toBeInTheDocument(); + expect( + screen.getByRole('button', {name: 'common.redo'}) + ).toBeInTheDocument(); + expect( + screen.getByRole('button', {name: 'components.indentButtons.indent'}) + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { + name: 'components.indentButtons.unindent' + }) + ).toBeInTheDocument(); + }); + + it.todo('indents code with its indent buttons'); + it.todo('undos and redos changes with the undo/redo buttons'); + }); + + describe('When CodeMirror is disabled', () => { + it("doesn't use CodeMirror on its code area", () => { + renderComponent({prefs: {useCodeMirror: false}}); + expect(screen.getByTestId('mock-code-area')!.dataset.useCodeMirror).toBe( + 'false' + ); + }); + + it('hides undo, redo, and indent buttons', () => { + renderComponent({prefs: {useCodeMirror: false}}); + expect( + screen.queryByRole('button', {name: 'common.undo'}) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', {name: 'common.redo'}) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', {name: 'components.indentButtons.indent'}) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { + name: 'components.indentButtons.unindent' + }) + ).not.toBeInTheDocument(); + }); + }); it('is accessible', async () => { const {container} = renderComponent(); diff --git a/src/dialogs/app-prefs.tsx b/src/dialogs/app-prefs.tsx index ecd1274ac..1abf2311e 100644 --- a/src/dialogs/app-prefs.tsx +++ b/src/dialogs/app-prefs.tsx @@ -15,6 +15,16 @@ export const AppPrefsDialog: React.FC< const {dispatch, prefs} = usePrefsContext(); const {t} = useTranslation(); + function handleUseCodeMirrorChange(value: boolean) { + dispatch(setPref('useCodeMirror', value)); + + // If we're disabling CodeMirror, force cursor blinking on because we no longer control it. + + if (!value) { + dispatch(setPref('editorCursorBlinks', true)); + } + } + return ( dispatch(setPref('editorCursorBlinks', value))} value={prefs.editorCursorBlinks} /> +

{t('dialogs.appPrefs.fontExplanation')}

diff --git a/src/dialogs/passage-edit/__tests__/passage-edit-contents.test.tsx b/src/dialogs/passage-edit/__tests__/passage-edit-contents.test.tsx index f72fd31a1..8fd547671 100644 --- a/src/dialogs/passage-edit/__tests__/passage-edit-contents.test.tsx +++ b/src/dialogs/passage-edit/__tests__/passage-edit-contents.test.tsx @@ -82,7 +82,7 @@ describe('', () => { ).toHaveTextContent('mock-changed-text'); }); - it('displays the format toolbar', () => { + it('does not disable story format extensions', () => { const story = fakeStory(1); const format = fakeLoadedStoryFormat({ name: story.storyFormat, @@ -91,8 +91,9 @@ describe('', () => { renderComponent({stories: [story], storyFormats: [format]}); expect( - screen.getByTestId(`mock-story-format-toolbar-${format.id}`) - ).toBeInTheDocument(); + screen.getByTestId(`mock-passage-text-${story.passages[0].id}`) + .dataset.storyFormatExtensionsDisabled + ).toBe('false'); }); it('displays the tag toolbar', () => { @@ -102,24 +103,47 @@ describe('', () => { version: story.storyFormatVersion }); - renderComponent({stories: [story], storyFormats: [format]}); + renderComponent({ + stories: [story], + storyFormats: [format] + }); expect( screen.getByTestId(`mock-tag-toolbar-${story.passages[0].id}`) ).toBeInTheDocument(); }); - it('does not disable story format extensions', () => { + it('displays the format toolbar when CodeMirror is enabled', () => { const story = fakeStory(1); const format = fakeLoadedStoryFormat({ name: story.storyFormat, version: story.storyFormatVersion }); - renderComponent({stories: [story], storyFormats: [format]}); + renderComponent({ + prefs: {useCodeMirror: true}, + stories: [story], + storyFormats: [format] + }); expect( - screen.getByTestId(`mock-passage-text-${story.passages[0].id}`) - .dataset.storyFormatExtensionsDisabled - ).toBe('false'); + screen.getByTestId(`mock-story-format-toolbar-${format.id}`) + ).toBeInTheDocument(); + }); + + it("doesn't display the format toolbar when CodeMirror is enabled", () => { + const story = fakeStory(1); + const format = fakeLoadedStoryFormat({ + name: story.storyFormat, + version: story.storyFormatVersion + }); + + renderComponent({ + prefs: {useCodeMirror: false}, + stories: [story], + storyFormats: [format] + }); + expect( + screen.queryByTestId(`mock-story-format-toolbar-${format.id}`) + ).not.toBeInTheDocument(); }); it('is accessible', async () => { diff --git a/src/dialogs/passage-edit/__tests__/passage-text.test.tsx b/src/dialogs/passage-edit/__tests__/passage-text.test.tsx index a2ad39ff8..4b2f90b3e 100644 --- a/src/dialogs/passage-edit/__tests__/passage-text.test.tsx +++ b/src/dialogs/passage-edit/__tests__/passage-text.test.tsx @@ -237,6 +237,20 @@ describe('', () => { expect(onChange.mock.calls).toEqual([['mock-change2']]); }); + it('uses CodeMirror in the code area if enabled in preferences', () => { + renderComponent({}, {prefs: {useCodeMirror: true}}); + expect(screen.getByTestId('mock-code-area')!.dataset.useCodeMirror).toBe( + 'true' + ); + }); + + it("doesn't use CodeMirror in the code area if disabled in preferences", () => { + renderComponent({}, {prefs: {useCodeMirror: false}}); + expect(screen.getByTestId('mock-code-area')!.dataset.useCodeMirror).toBe( + 'false' + ); + }); + it('is accessible', async () => { jest.useRealTimers(); diff --git a/src/dialogs/passage-edit/__tests__/passage-toolbar.test.tsx b/src/dialogs/passage-edit/__tests__/passage-toolbar.test.tsx index a5e55b029..9305ae290 100644 --- a/src/dialogs/passage-edit/__tests__/passage-toolbar.test.tsx +++ b/src/dialogs/passage-edit/__tests__/passage-toolbar.test.tsx @@ -9,15 +9,23 @@ import { StoryInspector } from '../../../test-util'; import {PassageToolbar} from '../passage-toolbar'; +import {usePrefsContext} from '../../../store/prefs'; jest.mock('../../../components/control/menu-button'); jest.mock('../../../components/passage/rename-passage-button'); jest.mock('../../../components/tag/add-tag-button'); const TestPassageToolbar: React.FC = () => { + const {prefs} = usePrefsContext(); const {stories} = useStoriesContext(); - return ; + return ( + + ); }; describe('', () => { @@ -99,6 +107,26 @@ describe('', () => { } ); + it('shows undo and redo buttons if CodeMirror is enabled in preferences', () => { + renderComponent({prefs: {useCodeMirror: true}}); + expect( + screen.getByRole('button', {name: 'common.undo'}) + ).toBeInTheDocument(); + expect( + screen.getByRole('button', {name: 'common.redo'}) + ).toBeInTheDocument(); + }); + + it('hides undo and redo buttons if CodeMirror is disabled in preferences', () => { + renderComponent({prefs: {useCodeMirror: false}}); + expect( + screen.queryByRole('button', {name: 'common.undo'}) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', {name: 'common.redo'}) + ).not.toBeInTheDocument(); + }); + it('is accessible', async () => { const {container} = await renderComponent(); diff --git a/src/dialogs/passage-edit/passage-edit-contents.tsx b/src/dialogs/passage-edit/passage-edit-contents.tsx index fcdb70ae5..2b8cbd518 100644 --- a/src/dialogs/passage-edit/passage-edit-contents.tsx +++ b/src/dialogs/passage-edit/passage-edit-contents.tsx @@ -13,6 +13,7 @@ import {PassageToolbar} from './passage-toolbar'; import {StoryFormatToolbar} from './story-format-toolbar'; import {TagToolbar} from './tag-toolbar'; import './passage-edit-contents.css'; +import {usePrefsContext} from '../../store/prefs'; export interface PassageEditContentsProps { disabled?: boolean; @@ -29,6 +30,7 @@ export const PassageEditContents: React.FC< const [editorCrashed, setEditorCrashed] = React.useState(false); const [cmEditor, setCmEditor] = React.useState(); const {ErrorBoundary, error, reset: resetError} = useErrorBoundary(); + const {prefs} = usePrefsContext(); const {dispatch, stories} = useUndoableStoriesContext(); const {formats} = useStoryFormatsContext(); const passage = passageWithId(stories, storyId, passageId); @@ -95,8 +97,9 @@ export const PassageEditContents: React.FC< editor={cmEditor} passage={passage} story={story} + useCodeMirror={prefs.useCodeMirror} /> - {storyFormatExtensionsEnabled && ( + {prefs.useCodeMirror && storyFormatExtensionsEnabled && ( = props => { } }, [localText, passage.text]); - const handleLocalChange = React.useCallback( - ( - editor: CodeMirror.Editor, - data: CodeMirror.EditorChange, - text: string - ) => { - // A local change has been made, e.g. the user has typed or pasted into - // the field. It's safe to immediately trigger a CodeMirror editor update. - - onEditorChange(editor); - + const handleLocalChangeText = React.useCallback( + (text: string) => { // Set local state because the CodeMirror instance is controlled, and // updates there should be immediate. @@ -161,11 +152,13 @@ export const PassageText: React.FC = props => { editorDidMount={handleMount} fontFamily={prefs.passageEditorFontFamily} fontScale={prefs.passageEditorFontScale} + id={`passage-dialog-passage-text-code-area-${passage.id}`} label={t('dialogs.passageEdit.passageTextEditorLabel')} labelHidden - onBeforeChange={handleLocalChange} - onChange={onEditorChange} + onChangeEditor={onEditorChange} + onChangeText={handleLocalChangeText} options={options} + useCodeMirror={prefs.useCodeMirror} value={localText} /> diff --git a/src/dialogs/passage-edit/passage-toolbar.tsx b/src/dialogs/passage-edit/passage-toolbar.tsx index e54b63475..f773d697a 100644 --- a/src/dialogs/passage-edit/passage-toolbar.tsx +++ b/src/dialogs/passage-edit/passage-toolbar.tsx @@ -7,14 +7,14 @@ import {ButtonBar} from '../../components/container/button-bar'; import {MenuButton} from '../../components/control/menu-button'; import {RenamePassageButton} from '../../components/passage/rename-passage-button'; import {AddTagButton} from '../../components/tag'; -import { TestPassageButton } from '../../routes/story-edit/toolbar/passage/test-passage-button'; +import {TestPassageButton} from '../../routes/story-edit/toolbar/passage/test-passage-button'; import { addPassageTag, Passage, setTagColor, Story, storyPassageTags, - updatePassage, + updatePassage } from '../../store/stories'; import {useUndoableStoriesContext} from '../../store/undoable-stories'; import {Color} from '../../util/color'; @@ -24,10 +24,11 @@ export interface PassageToolbarProps { editor?: Editor; passage: Passage; story: Story; + useCodeMirror: boolean; } export const PassageToolbar: React.FC = props => { - const {disabled, editor, passage, story} = props; + const {disabled, editor, passage, story, useCodeMirror} = props; const {dispatch} = useUndoableStoriesContext(); const {t} = useTranslation(); @@ -63,11 +64,13 @@ export const PassageToolbar: React.FC = props => { return ( - + {useCodeMirror && ( + + )} = props const story = storyWithId(stories, storyId); const {t} = useTranslation(); - const handleChange = ( - editor: CodeMirror.Editor, - data: CodeMirror.EditorChange, - text: string - ) => { - setCmEditor(editor); + const handleChangeText = (text: string) => { dispatch(updateStory(stories, story, {script: text})); }; @@ -38,18 +33,22 @@ export const StoryJavaScriptDialog: React.FC = props headerLabel={t('dialogs.storyJavaScript.title')} maximizable > - - - - + {prefs.useCodeMirror && ( + + + + + )} = props mode: 'javascript', placeholder: t('dialogs.storyJavaScript.explanation') }} + useCodeMirror={prefs.useCodeMirror} value={story.script} /> diff --git a/src/dialogs/story-search.tsx b/src/dialogs/story-search.tsx index 95350be6c..90cb80317 100644 --- a/src/dialogs/story-search.tsx +++ b/src/dialogs/story-search.tsx @@ -16,6 +16,7 @@ import { import {useUndoableStoriesContext} from '../store/undoable-stories'; import {DialogComponentProps} from './dialogs.types'; import './story-search.css'; +import {usePrefsContext} from '../store/prefs'; // See https://github.com/codemirror/CodeMirror/issues/5444 @@ -38,6 +39,7 @@ export const StorySearchDialog: React.FC = props => { const {find, flags, replace, storyId, onClose, onChangeProps, ...other} = props; const closingRef = React.useRef(false); + const {prefs} = usePrefsContext(); const {dispatch, stories} = useUndoableStoriesContext(); const {t} = useTranslation(); const story = storyWithId(stories, storyId); @@ -82,19 +84,11 @@ export const StorySearchDialog: React.FC = props => { onClose(); } - function handleReplaceWithChange( - editor: CodeMirror.Editor, - data: CodeMirror.EditorChange, - text: string - ) { + function handleReplaceWithChange(text: string) { patchProps({replace: text}); } - function handleSearchForChange( - editor: CodeMirror.Editor, - data: CodeMirror.EditorChange, - text: string - ) { + function handleSearchForChange(text: string) { patchProps({find: text}); } @@ -119,18 +113,22 @@ export const StorySearchDialog: React.FC = props => { >
diff --git a/src/dialogs/story-stylesheet.tsx b/src/dialogs/story-stylesheet.tsx index f9a4d0098..6b2d87144 100644 --- a/src/dialogs/story-stylesheet.tsx +++ b/src/dialogs/story-stylesheet.tsx @@ -22,12 +22,7 @@ export const StoryStylesheetDialog: React.FC = props const story = storyWithId(stories, storyId); const {t} = useTranslation(); - const handleChange = ( - editor: CodeMirror.Editor, - data: CodeMirror.EditorChange, - text: string - ) => { - setCmEditor(editor); + const handleChangeText = (text: string) => { dispatch(updateStory(stories, story, {stylesheet: text})); }; @@ -38,18 +33,22 @@ export const StoryStylesheetDialog: React.FC = props headerLabel={t('dialogs.storyStylesheet.title')} maximizable > - - - - + {prefs.useCodeMirror && ( + + + + + )} = props mode: 'css', placeholder: t('dialogs.storyStylesheet.explanation') }} + useCodeMirror={prefs.useCodeMirror} value={story.stylesheet} /> diff --git a/src/store/prefs/defaults.ts b/src/store/prefs/defaults.ts index c4a7b4d62..2715ef0cf 100644 --- a/src/store/prefs/defaults.ts +++ b/src/store/prefs/defaults.ts @@ -31,5 +31,6 @@ export const defaults = (): PrefsState => ({ storyListSort: 'name', storyListTagFilter: [], storyTagColors: {}, + useCodeMirror: true, welcomeSeen: false }); diff --git a/src/store/prefs/prefs.types.ts b/src/store/prefs/prefs.types.ts index 5260b1125..7f6644c95 100644 --- a/src/store/prefs/prefs.types.ts +++ b/src/store/prefs/prefs.types.ts @@ -106,6 +106,10 @@ export interface PrefsState { * Colors for story tags. */ storyTagColors: Record; + /** + * Use CodeMirror for text editing? + */ + useCodeMirror: boolean; /** * Has the user been shown the welcome route? */ diff --git a/src/test-util/fakes.ts b/src/test-util/fakes.ts index 8615c124d..d18de88ea 100644 --- a/src/test-util/fakes.ts +++ b/src/test-util/fakes.ts @@ -139,6 +139,9 @@ export function fakePrefs(overrides?: Partial): PrefsState { storyListSort: faker.helpers.arrayElement(['date', 'name']), storyListTagFilter: [], storyTagColors: {[tags[0]]: 'red', [tags[1]]: 'green', [tags[2]]: 'blue'}, + // Changing this preference should be explicit in a test because it affects + // editorCursorBlinks in some contexts. + useCodeMirror: true, welcomeSeen: faker.datatype.boolean(), ...overrides }; From 27e854a7334c689e755366c6860f4b969101652a Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Sat, 9 Nov 2024 10:52:47 -0500 Subject: [PATCH 4/7] Add docs on enhanced editor checkbox --- docs/en/src/customizing/preferences.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/en/src/customizing/preferences.md b/docs/en/src/customizing/preferences.md index 6c2d70914..17e24aedf 100644 --- a/docs/en/src/customizing/preferences.md +++ b/docs/en/src/customizing/preferences.md @@ -33,6 +33,18 @@ stylesheet edit dialog](../editing-stories/js-and-css.md). This preference only controls the cursor in the large text fields of these dialogs. Twine uses your system setting for cursor blinking in one-line text fields. +The _Use Enhanced Editors_ checkbox controls whether Twine uses an enhanced +editor control in edit dialogs. Unfortunately, this control can cause problems +for assistive technology like screen readers, so disabling this may help in that +case. Disabling this has a few side effects: + +- The _Blinking Cursor in Editors_ checkbox will become disabled, and whether + the cursor blinks in editors will use use your system setting. +- Some toolbar buttons in dialogs will be hidden, because they use functionality + present in the enhanced editor control. +- Story format toolbars will not be shown in passage editing dialogs, because + they also use functionality in the enhanced editor control + You can change the font and size used in passage edit dialogs and the stylesheet and JavaScript edit dialogs using the controls below the _Blinking Cursor in Editors_ checkbox. (The _Code Editor_ preferences apply to both the stylesheet From 111bd18b5604b06812bc2e7a8af260d9496cd391 Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Sat, 16 Nov 2024 14:59:09 -0500 Subject: [PATCH 5/7] Small improvements to non-CodeMirror editor --- .../container/dialog-card/dialog-editor.tsx | 10 ++++++++-- .../passage-edit/passage-edit-contents.css | 11 +++++++++++ src/dialogs/passage-edit/passage-text.tsx | 18 +++++++++++++++++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/components/container/dialog-card/dialog-editor.tsx b/src/components/container/dialog-card/dialog-editor.tsx index 47b72c2df..bbe472b0a 100644 --- a/src/components/container/dialog-card/dialog-editor.tsx +++ b/src/components/container/dialog-card/dialog-editor.tsx @@ -1,6 +1,12 @@ import * as React from 'react'; import './dialog-editor.css'; -export const DialogEditor: React.FC = props => ( -
{props.children}
+export type DialogEditorProps = React.ComponentPropsWithoutRef<'div'>; + +export const DialogEditor = React.forwardRef( + (props, ref) => ( +
+ {props.children} +
+ ) ); diff --git a/src/dialogs/passage-edit/passage-edit-contents.css b/src/dialogs/passage-edit/passage-edit-contents.css index cc7fd20d1..baacf6064 100644 --- a/src/dialogs/passage-edit/passage-edit-contents.css +++ b/src/dialogs/passage-edit/passage-edit-contents.css @@ -6,4 +6,15 @@ .passage-edit-contents .tags { padding: var(--grid-size); +} + +/* Needed when CodeMirror is disabled. */ + +.passage-edit-contents .dialog-editor textarea { + border-radius: 0; +} + +.passage-edit-contents .dialog-editor textarea:focus { + box-shadow: inset 0 0 0px 2px var(--blue); + outline: none; } \ No newline at end of file diff --git a/src/dialogs/passage-edit/passage-text.tsx b/src/dialogs/passage-edit/passage-text.tsx index 95f8c8fe6..004b6b0f0 100644 --- a/src/dialogs/passage-edit/passage-text.tsx +++ b/src/dialogs/passage-edit/passage-text.tsx @@ -34,6 +34,7 @@ export const PassageText: React.FC = props => { const autocompletePassageNames = useCodeMirrorPassageHints(story); const mode = useFormatCodeMirrorMode(storyFormat.name, storyFormat.version) ?? 'text'; + const codeAreaContainerRef = React.useRef(null); const {t} = useTranslation(); // These are refs so that changing them doesn't trigger a rerender, and more @@ -123,6 +124,21 @@ export const PassageText: React.FC = props => { [onEditorChange] ); + // Emulate the above behavior re: focus if we aren't using CodeMirror. + + React.useEffect(() => { + if (!prefs.useCodeMirror && codeAreaContainerRef.current) { + const area = codeAreaContainerRef.current.querySelector('textarea'); + + if (!area) { + return; + } + + area.focus(); + area.setSelectionRange(area.value.length, area.value.length); + } + }, []); + const options = React.useMemo( () => ({ ...codeMirrorOptionsFromPrefs(prefs), @@ -147,7 +163,7 @@ export const PassageText: React.FC = props => { ); return ( - + Date: Sun, 24 Nov 2024 15:18:43 -0500 Subject: [PATCH 6/7] Add Chapbook 2.3.0 --- public/story-formats/chapbook-2.2.0/format.js | 1 - public/story-formats/chapbook-2.3.0/format.js | 1 + .../story-formats/{chapbook-2.2.0 => chapbook-2.3.0}/logo.svg | 0 src/store/story-formats/defaults.ts | 4 ++-- 4 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 public/story-formats/chapbook-2.2.0/format.js create mode 100644 public/story-formats/chapbook-2.3.0/format.js rename public/story-formats/{chapbook-2.2.0 => chapbook-2.3.0}/logo.svg (100%) diff --git a/public/story-formats/chapbook-2.2.0/format.js b/public/story-formats/chapbook-2.2.0/format.js deleted file mode 100644 index 36527eac5..000000000 --- a/public/story-formats/chapbook-2.2.0/format.js +++ /dev/null @@ -1 +0,0 @@ -window.storyFormat({"source":"{{STORY_NAME}}
{{STORY_DATA}}","author":"Chris Klimas","description":"A Twine story format emphasizing ease of authoring, multimedia, and playability on many different types of devices. Visit the guide for more information.","hydrate":"(function(l){\"use strict\";function r(){return{startState(){return{inVarsSection:!1}},token(e,t){if(t.hasVarsSection===void 0){for(let n=1,o=e.lookAhead(1);o&&t.hasVarsSection===void 0;o=e.lookAhead(++n))o===\"--\"&&(t.hasVarsSection=!0,t.inVarsSection=!0);t.hasVarsSection===void 0&&(t.hasVarsSection=!1)}return t.hasVarsSection&&t.inVarsSection&&e.sol()?e.match(/^--$/)?(t.inVarsSection=!1,e.skipToEnd(),\"punctuation\"):e.skipTo(\":\")?(e.next(),\"def\"):(e.skipToEnd(),\"text\"):e.sol()&&e.match(/^\\[[^[].*\\]$/)||e.match(/^\\{.+?\\}/)?\"keyword\":e.match(/^\\[\\[[^\\]]+?\\]\\]/)?\"link\":(e.eatWhile(/[^[{]/)||e.skipToEnd(),\"text\")}}}function c(e){return Object.keys(e).reduce((t,n)=>({...t,[n]:o=>{o.replaceSelection(e[n]),o.focus()}}),{})}function d(e){return Object.keys(e).reduce((t,n)=>({...t,[n]:o=>{const{matcher:i,wrapper:v}=e[n];o.replaceSelections(o.getSelections().map(s=>i.test(s)?s.replace(i,\"$1\"):v(s)),\"around\"),o.focus()}}),{})}const b={...d({boldText:{matcher:/^(?:__|\\*\\*)(.+)(?:__|\\*\\*)$/,wrapper:e=>`**${e}**`},italicText:{matcher:/^(?:_|\\*)(.+)(?:_|\\*)$/,wrapper:e=>`*${e}*`},monospacedText:{matcher:/^`(.+)`$/,wrapper:e=>\"`\"+e+\"`\"},smallCapsText:{matcher:/^~~(.+)~~$/,wrapper:e=>`~~${e}~~`}}),...c({insertAfter:`\n[after 1 second]\nText\n\n[continued]`,insertAppend:`\n[append]\n`,insertBlockquote:`\n
Text
\n`,insertContinue:`\n[continued]\n`,insertBulletedList:`\n- Item\n- Item\n`,insertCss:`\n[CSS]\n.page article {\n color: green;\n}\n\n[continued]\n`,insertCyclingLink:\"{cycling link for: 'variable name', choices: ['choice', 'choice']}\",insertDropdownMenu:\"{dropdown menu for: 'variable name', choices: ['choice', 'choice']}\",insertEmbedAmbientSound:\"{ambient sound: 'sound name'}\",insertEmbedSoundEffect:\"{sound effect: 'sound name'}\",insertEmbedPassage:\"{embed passage: 'Passage name'}\",insertEmbedYouTubeVideo:\"{embed YouTube video: 'URL'}\",insertImageFlickr:\"{embed Flickr image: 'Flickr embed code'}\",insertImageUrl:\"{embed image: 'URL to image'}\",insertImageUnsplash:\"{embed Unsplash image: }\",insertForkList:`\n> Link\n> Link\n`,insertIf:`\n[if condition]\nText\n\n[continue]\n`,insertIfElse:`\n[if condition]\nText\n\n[else]Text\n\n[continued]\n`,insertJs:`\n[JavaScript]\nwrite('Hello from JavaScript');\n\n[continued]\n`,insertNote:`\n[note]\nNote to self\n\n[continued]\n`,insertNumberedList:`\n1. Item\n2. Item\n`,insertPassageLink:\"{link to: 'Passage name', label: 'Label text'}\",insertRestartLink:\"{restart link, label: 'Label text'}\",insertRevealPassageLink:\"{reveal link: 'Label text', passage: 'Passage name'}\",insertRevealTextLink:\"{restart link: 'Label text', text: 'Displayed text'}\",insertSectionBreak:`\n***\n`,insertTextInput:\"{text input for: 'variable name'}\",insertUnless:`\n[unless condition]\nText\n\n[continued]\n`})},m=`\n \n \n \n\n\n\n`,p=`\n \n \n \n \n \n\n\n\n`,u=`\n \n \n \n \n \n\n\n\n`,h=`\n \n \n \n \n \n \n \n \n \n\n\n\n`,k=`\n \n \n \n \n\n\n\n`;function a(e,t){return`data:image/svg+xml;base64,${window.btoa(e.replace(/currentColor/g,t))}`}function g(e,{foregroundColor:t}){const n=e.getDoc().somethingSelected();return[{type:\"menu\",icon:a(p,t),label:\"Style\",items:[{type:\"button\",iconOnly:!0,label:\"Bold\",command:\"boldText\",disabled:!n},{type:\"button\",label:\"Italic\",command:\"italicText\",disabled:!n},{type:\"button\",label:\"Monospaced Text\",command:\"monospacedText\",disabled:!n},{type:\"button\",label:\"Small Caps\",command:\"smallCapsText\",disabled:!n},{type:\"separator\"},{type:\"button\",label:\"Blockquote\",command:\"insertBlockquote\",disabled:n},{type:\"button\",label:\"Bulleted List\",command:\"insertBulletedList\",disabled:n},{type:\"button\",label:\"Fork List\",command:\"insertForkList\",disabled:n},{type:\"button\",label:\"Numbered List\",command:\"insertNumberedList\",disabled:n},{type:\"button\",label:\"Section Break\",command:\"insertSectionBreak\",disabled:n}]},{type:\"menu\",icon:a(k,t),label:\"Link\",disabled:n,items:[{type:\"button\",label:\"Passage Link\",command:\"insertPassageLink\"},{type:\"button\",label:\"Restart Link\",command:\"insertRestartLink\"},{type:\"button\",label:\"Reveal Passage Link\",command:\"insertRevealPassageLink\"},{type:\"button\",label:\"Reveal Text Link\",command:\"insertRevealTextLink\"}]},{type:\"menu\",icon:a(m,t),label:\"Modifiers\",disabled:n,items:[{type:\"button\",label:\"If\",command:\"insertIf\"},{type:\"button\",label:\"If and Else\",command:\"insertIfElse\"},{type:\"button\",label:\"Unless\",command:\"insertUnless\"},{type:\"button\",label:\"Continue\",command:\"insertContinue\"},{type:\"separator\"},{type:\"button\",label:\"After Delay\",command:\"insertAfter\"},{type:\"button\",label:\"Append Text\",command:\"insertAppend\"},{type:\"button\",label:\"Note\",command:\"insertNote\"},{type:\"separator\"},{type:\"button\",label:\"JavaScript\",command:\"insertJs\"},{type:\"button\",label:\"CSS\",command:\"insertCss\"}]},{type:\"menu\",icon:a(u,t),label:\"Embed\",disabled:n,items:[{type:\"button\",label:\"Embed Passage\",command:\"insertEmbedPassage\"},{type:\"button\",label:\"Embed Image from Flickr\",command:\"insertImageFlickr\"},{type:\"button\",label:\"Embed Image from URL\",command:\"insertImageUrl\"},{type:\"button\",label:\"Embed Image from Unsplash\",command:\"insertImageUnsplash\"},{type:\"button\",label:\"Embed Ambient Sound\",command:\"insertEmbedAmbientSound\"},{type:\"button\",label:\"Embed Sound Effect\",command:\"insertEmbedSoundEffect\"},{type:\"button\",label:\"Embed YouTube Video\",command:\"insertEmbedYouTubeVideo\"}]},{type:\"menu\",icon:a(h,t),label:\"Input\",disabled:n,items:[{type:\"button\",label:\"Cycling Link\",command:\"insertCyclingLink\"},{type:\"button\",label:\"Dropdown Menu\",command:\"insertDropdownMenu\"},{type:\"button\",label:\"Text Input\",command:\"insertTextInput\"}]}]}function f(e){const t=[/\\{embed\\s+passage\\s*:\\s*['\"](.+?)['\"]\\s*}/g,/\\{link\\s+to\\s*:\\s*['\"](.+?)['\"]\\s*\\}/g,/\\{reveal\\s+link.+passage\\s*:\\s*['\"](.+?)['\"].*\\}/g],n=[];for(const o of t){let i;for(;i=o.exec(e);)n.push(i[1])}return n}l.editorExtensions={twine:{\"^2.4.0-beta2\":{codeMirror:{commands:b,mode:r,toolbar:g},references:{parsePassageText:f}}}}})(this);\n","image":"logo.svg","name":"Chapbook","proofing":false,"version":"2.2.0"}) \ No newline at end of file diff --git a/public/story-formats/chapbook-2.3.0/format.js b/public/story-formats/chapbook-2.3.0/format.js new file mode 100644 index 000000000..dfc5bd814 --- /dev/null +++ b/public/story-formats/chapbook-2.3.0/format.js @@ -0,0 +1 @@ +window.storyFormat({"source":"{{STORY_NAME}}
{{STORY_DATA}}","author":"Chris Klimas","description":"A Twine story format emphasizing ease of authoring, multimedia, and playability on many different types of devices. Visit the guide for more information.","hydrate":"(function(l){\"use strict\";function r(){return{startState(){return{inVarsSection:!1}},token(e,t){if(t.hasVarsSection===void 0){for(let n=1,o=e.lookAhead(1);o&&t.hasVarsSection===void 0;o=e.lookAhead(++n))o===\"--\"&&(t.hasVarsSection=!0,t.inVarsSection=!0);t.hasVarsSection===void 0&&(t.hasVarsSection=!1)}return t.hasVarsSection&&t.inVarsSection&&e.sol()?e.match(/^--$/)?(t.inVarsSection=!1,e.skipToEnd(),\"punctuation\"):e.skipTo(\":\")?(e.next(),\"def\"):(e.skipToEnd(),\"text\"):e.sol()&&e.match(/^\\[[^[].*\\]$/)||e.match(/^\\{.+?\\}/)?\"keyword\":e.match(/^\\[\\[[^\\]]+?\\]\\]/)?\"link\":(e.eatWhile(/[^[{]/)||e.skipToEnd(),\"text\")}}}function c(e){return Object.keys(e).reduce((t,n)=>({...t,[n]:o=>{o.replaceSelection(e[n]),o.focus()}}),{})}function d(e){return Object.keys(e).reduce((t,n)=>({...t,[n]:o=>{const{matcher:i,wrapper:v}=e[n];o.replaceSelections(o.getSelections().map(s=>i.test(s)?s.replace(i,\"$1\"):v(s)),\"around\"),o.focus()}}),{})}const b={...d({boldText:{matcher:/^(?:__|\\*\\*)(.+)(?:__|\\*\\*)$/,wrapper:e=>`**${e}**`},italicText:{matcher:/^(?:_|\\*)(.+)(?:_|\\*)$/,wrapper:e=>`*${e}*`},monospacedText:{matcher:/^`(.+)`$/,wrapper:e=>\"`\"+e+\"`\"},smallCapsText:{matcher:/^~~(.+)~~$/,wrapper:e=>`~~${e}~~`}}),...c({insertAfter:`\n[after 1 second]\nText\n\n[continued]`,insertAppend:`\n[append]\n`,insertBlockquote:`\n
Text
\n`,insertContinue:`\n[continued]\n`,insertBulletedList:`\n- Item\n- Item\n`,insertCss:`\n[CSS]\n.page article {\n color: green;\n}\n\n[continued]\n`,insertCyclingLink:\"{cycling link for: 'variable name', choices: ['choice', 'choice']}\",insertDropdownMenu:\"{dropdown menu for: 'variable name', choices: ['choice', 'choice']}\",insertEmbedAmbientSound:\"{ambient sound: 'sound name'}\",insertEmbedSoundEffect:\"{sound effect: 'sound name'}\",insertEmbedPassage:\"{embed passage: 'Passage name'}\",insertEmbedYouTubeVideo:\"{embed YouTube video: 'URL'}\",insertImageFlickr:\"{embed Flickr image: 'Flickr embed code'}\",insertImageUrl:\"{embed image: 'URL to image'}\",insertImageUnsplash:\"{embed Unsplash image: }\",insertForkList:`\n> Link\n> Link\n`,insertIf:`\n[if condition]\nText\n\n[continue]\n`,insertIfElse:`\n[if condition]\nText\n\n[else]Text\n\n[continued]\n`,insertJs:`\n[JavaScript]\nwrite('Hello from JavaScript');\n\n[continued]\n`,insertNote:`\n[note]\nNote to self\n\n[continued]\n`,insertNumberedList:`\n1. Item\n2. Item\n`,insertPassageLink:\"{link to: 'Passage name', label: 'Label text'}\",insertRestartLink:\"{restart link, label: 'Label text'}\",insertRevealPassageLink:\"{reveal link: 'Label text', passage: 'Passage name'}\",insertRevealTextLink:\"{reveal link: 'Label text', text: 'Displayed text'}\",insertSectionBreak:`\n***\n`,insertTextInput:\"{text input for: 'variable name'}\",insertUnless:`\n[unless condition]\nText\n\n[continued]\n`})},m=`\n \n \n \n\n\n\n`,p=`\n \n \n \n \n \n\n\n\n`,u=`\n \n \n \n \n \n\n\n\n`,h=`\n \n \n \n \n \n \n \n \n \n\n\n\n`,k=`\n \n \n \n \n\n\n\n`;function a(e,t){return`data:image/svg+xml;base64,${window.btoa(e.replace(/currentColor/g,t))}`}function g(e,{foregroundColor:t}){const n=e.getDoc().somethingSelected();return[{type:\"menu\",icon:a(p,t),label:\"Style\",items:[{type:\"button\",iconOnly:!0,label:\"Bold\",command:\"boldText\",disabled:!n},{type:\"button\",label:\"Italic\",command:\"italicText\",disabled:!n},{type:\"button\",label:\"Monospaced Text\",command:\"monospacedText\",disabled:!n},{type:\"button\",label:\"Small Caps\",command:\"smallCapsText\",disabled:!n},{type:\"separator\"},{type:\"button\",label:\"Blockquote\",command:\"insertBlockquote\",disabled:n},{type:\"button\",label:\"Bulleted List\",command:\"insertBulletedList\",disabled:n},{type:\"button\",label:\"Fork List\",command:\"insertForkList\",disabled:n},{type:\"button\",label:\"Numbered List\",command:\"insertNumberedList\",disabled:n},{type:\"button\",label:\"Section Break\",command:\"insertSectionBreak\",disabled:n}]},{type:\"menu\",icon:a(k,t),label:\"Link\",disabled:n,items:[{type:\"button\",label:\"Passage Link\",command:\"insertPassageLink\"},{type:\"button\",label:\"Restart Link\",command:\"insertRestartLink\"},{type:\"button\",label:\"Reveal Passage Link\",command:\"insertRevealPassageLink\"},{type:\"button\",label:\"Reveal Text Link\",command:\"insertRevealTextLink\"}]},{type:\"menu\",icon:a(m,t),label:\"Modifiers\",disabled:n,items:[{type:\"button\",label:\"If\",command:\"insertIf\"},{type:\"button\",label:\"If and Else\",command:\"insertIfElse\"},{type:\"button\",label:\"Unless\",command:\"insertUnless\"},{type:\"button\",label:\"Continue\",command:\"insertContinue\"},{type:\"separator\"},{type:\"button\",label:\"After Delay\",command:\"insertAfter\"},{type:\"button\",label:\"Append Text\",command:\"insertAppend\"},{type:\"button\",label:\"Note\",command:\"insertNote\"},{type:\"separator\"},{type:\"button\",label:\"JavaScript\",command:\"insertJs\"},{type:\"button\",label:\"CSS\",command:\"insertCss\"}]},{type:\"menu\",icon:a(u,t),label:\"Embed\",disabled:n,items:[{type:\"button\",label:\"Embed Passage\",command:\"insertEmbedPassage\"},{type:\"button\",label:\"Embed Image from Flickr\",command:\"insertImageFlickr\"},{type:\"button\",label:\"Embed Image from URL\",command:\"insertImageUrl\"},{type:\"button\",label:\"Embed Image from Unsplash\",command:\"insertImageUnsplash\"},{type:\"button\",label:\"Embed Ambient Sound\",command:\"insertEmbedAmbientSound\"},{type:\"button\",label:\"Embed Sound Effect\",command:\"insertEmbedSoundEffect\"},{type:\"button\",label:\"Embed YouTube Video\",command:\"insertEmbedYouTubeVideo\"}]},{type:\"menu\",icon:a(h,t),label:\"Input\",disabled:n,items:[{type:\"button\",label:\"Cycling Link\",command:\"insertCyclingLink\"},{type:\"button\",label:\"Dropdown Menu\",command:\"insertDropdownMenu\"},{type:\"button\",label:\"Text Input\",command:\"insertTextInput\"}]}]}function f(e){const t=[/\\{embed\\s+passage\\s*:\\s*['\"](.+?)['\"]\\s*}/g,/\\{link\\s+to\\s*:\\s*['\"](.+?)['\"][^}]*\\}/g,/\\{reveal\\s+link.+passage\\s*:\\s*['\"](.+?)['\"].*\\}/g],n=[];for(const o of t){let i;for(;i=o.exec(e);)n.push(i[1])}return n}l.editorExtensions={twine:{\"^2.4.0-beta2\":{codeMirror:{commands:b,mode:r,toolbar:g},references:{parsePassageText:f}}}}})(this);\n","image":"logo.svg","name":"Chapbook","proofing":false,"version":"2.3.0"}) \ No newline at end of file diff --git a/public/story-formats/chapbook-2.2.0/logo.svg b/public/story-formats/chapbook-2.3.0/logo.svg similarity index 100% rename from public/story-formats/chapbook-2.2.0/logo.svg rename to public/story-formats/chapbook-2.3.0/logo.svg diff --git a/src/store/story-formats/defaults.ts b/src/store/story-formats/defaults.ts index a3aa03b2b..997c47286 100644 --- a/src/store/story-formats/defaults.ts +++ b/src/store/story-formats/defaults.ts @@ -6,8 +6,8 @@ export const builtins = () => [ }, { name: 'Chapbook', - url: 'story-formats/chapbook-2.2.0/format.js', - version: '2.2.0' + url: 'story-formats/chapbook-2.3.0/format.js', + version: '2.3.0' }, { name: 'Harlowe', From 321de4bfdadc1885b65d01baa4c684d6c9db217d Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Sun, 24 Nov 2024 15:30:21 -0500 Subject: [PATCH 7/7] 2.10.0 release --- docs/en/src/release-notes/2-10.md | 23 +++++++++++++++++++++++ package.json | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 docs/en/src/release-notes/2-10.md diff --git a/docs/en/src/release-notes/2-10.md b/docs/en/src/release-notes/2-10.md new file mode 100644 index 000000000..68c8700b2 --- /dev/null +++ b/docs/en/src/release-notes/2-10.md @@ -0,0 +1,23 @@ +# 2.10 versions + +## 2.10.0 + +Release Date: November 24, 20242 + +## New Features Added + +- The story format list is now a dialog instead of a separate screen. +- A new preference has been added that allows disabling enhanced text editors, + which can have problems with assistive technology. Disabling these editors + disables syntax highlighting and some toolbar buttons, and you cannot control + whether the cursor blinks in non-enhanced fields. (In that case, it will use + your system setting.) + +## Bugs Fixed + +- Validation of story format URLs now occurs when you try to add the format, not + as you type in the field. This could cause the UI to become unresponsive. + +## Story Format Updates + +- Chapbook has been updated to version [2.3.0](https://klembot.github.io/chapbook/guide/references/version-history.html#230-24-november-24). \ No newline at end of file diff --git a/package.json b/package.json index c9d5a54fc..bde129e8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Twine", - "version": "2.9.2", + "version": "2.10.0", "description": "a GUI for creating nonlinear stories", "author": "Chris Klimas ", "license": "GPL-3.0",