From f775f577aeb6adc1f66bedfc9b7ad6436c48bbed Mon Sep 17 00:00:00 2001 From: albuquezi Date: Thu, 14 Jul 2022 10:57:51 +0200 Subject: [PATCH 01/26] Updated pt-pt for the 2.4 release This version includes all the translation of all the strings present in the 2.4 release --- public/locales/pt-pt.json | 669 +++++++++++++++++++++----------------- 1 file changed, 371 insertions(+), 298 deletions(-) diff --git a/public/locales/pt-pt.json b/public/locales/pt-pt.json index df6096553..72726d247 100644 --- a/public/locales/pt-pt.json +++ b/public/locales/pt-pt.json @@ -1,338 +1,411 @@ { - "colors": { - "blue": "Azul", - "green": "Verde", - "none": "Sem cor", - "orange": "Laranja", - "purple": "Roxo", - "red": "Vermelho", - "yellow": "Amarelo" - }, - "common": { - "add": "Adicionar", - "appName": "Twine", - "cancel": "Cancelar", - "close": "Fechar", - "color": "Cor", - "custom": "Personalizado", - "delete": "Apagar", - "deleteCount": "Apagar ({{count}})", - "duplicate": "Duplicar", - "edit": "Editar", - "editCount": "Editar ({{count}})", - "import": "Importar", - "more": "Mais", - "next": "Próximo", - "ok": "OK", - "play": "Jogar", - "preferences": "Preferências", - "publishToFile": "Publicar como ficheiro", - "redo": "Refazer", - "redoChange": "Refazer {{change}}", - "remove": "Remover", - "rename": "Mudar o nome", - "renamePrompt": "Que novo nome queres dar a “{{name}}”?", - "skip": "Saltar", - "storyFormat": "Formato de história", - "tag": "Etiqueta", - "test": "Testar", - "undo": "Desfazer", - "undoChange": "Desfazer {{change}}" - }, - "components": { - "addStoryFormatButton": { - "addPreview": "O {{storyFormatName}} {{storyFormatVersion}} vai ser adicionado.", - "alreadyAdded": "O {{storyFormatName}} {{storyFormatVersion}} já foi adicionado.", - "fetchError": "Ocorreu um erro ao transferir este formato ( {{errorMessage}} ).", - "invalidUrl": "O endereço que introduziste não é um URL válido.", - "prompt": "Para adicionares um formato de história, introduz o seu endereço em baixo." - }, + "colors": { + "none": "Sem Cor", + "red": "Vermelho", + "orange": "Laranja", + "yellow": "Amarelo", + "green": "Verde", + "blue": "Azul", + "purple": "Roxo" + }, + "common": { + "add": "Acrescentar", + "appName": "Twine", + "back": "Voltar", + "build": "Criar", + "cancel": "Cancelar", + "close": "Fechar", + "color": "Cor", + "create": "Criar", + "custom": "Personalizar", + "delete": "Apagar", + "deleteCount": "Apagar ({{count}})", + "details": "Detalhes", + "duplicate": "Duplicar", + "edit": "Editar", + "editCount": "Editar ({{count}})", + "help": "Ajuda", + "import": "Importar", + "more": "Mais", + "new": "Novo", + "next": "Seguinte", + "ok": "OK", + "passage": "Passagem", + "play": "Jogar", + "preferences": "Preferências", + "publishToFile": "Publicar para Ficheiro", + "redo": "Refazer", + "redoChange": "Refazer {{change}}", + "rename": "Mudar o Título", + "renamePrompt": "Qual será o novo título da história “{{name}}”?", + "remove": "Remover", + "selectAll": "Escolher Tudo", + "skip": "Saltar", + "story": "História", + "storyFormat": "Formato de História", + "tag": "Etiqueta", + "test": "Testar", + "twine": "Twine", + "undo": "Anular", + "undoChange": "Anular {{change}}", + "view": "Ver" + }, + "components": { "addTagButton": { - "addLabel": "Adicionar etiqueta", - "alreadyAdded": "Esta etiqueta já foi adicionada.", - "invalidName": "Por favor, introduz um nome válido para a etiqueta.", + "alreadyAdded": "Este nome de etiqueta já está a ser usado.", + "addLabel": "Acrescentar etiqueta", + "invalidName": "Introduz um nome válido para a etiqueta.", "newTag": "Nova Etiqueta", - "tagColorLabel": "Cor da etiqueta", - "tagNameLabel": "Nome da etiqueta" + "tagColorLabel": "Cor da Etiqueta", + "tagNameLabel": "Nome da Etiqueta" + }, + "dialogCard": { + "contentsCrashed": "Alguma coisa correu mal com esta caixa de texto. Tenta fechá-la e abri-la outra vez." }, "fontSelect": { - "customFamilyDetail": "Por favor, introduz apenas o nome da fonte.", - "customScaleDetail": "Por favor, introduz apenas uma percentagem.", - "familyEmpty": "Por favor, introduz um nome de fonte.", + "customScaleDetail": "Só valores em percentagem, por favor.", + "customFamilyDetail": "Introduz apenas o nome da fonte.", + "familyEmpty": "Introduz o nome da fonte.", "font": "Fonte", - "fontSize": "Tamanho da Fonte", "fonts": { - "monospaced": "Monoespaçado", - "serif": "Serifa", + "monospaced": "Monoespaçada", + "serif": "Serifada", "system": "Sistema" }, + "fontSize": "Tamanho da Fonte", "percentage": "{{percent}}%", - "percentageIsntNumber": "Por favor, introduz um número.", - "percentageNotPositive": "Introduz, por favor, um número maior do que 0." + "percentageIsntNumber": "Introduz um número.", + "percentageNotPositive": "Introduz um número maior do que 0." }, "indentButtons": { "indent": "Indentar", - "unindent": "Retirar indentação" + "unindent": "Remover Indentação" }, "localStorageQuota": { - "measureAgain": "Avaliar o espaço disponível novamente", + "measureAgain": "Calcular o espaço disponível novamente", "percentAvailable": "{percent}% de espaço disponível" }, "passageCard": { - "placeholderClick": "Faz duplo-clique nesta passagem para editá-la.", - "placeholderTouch": "Toca nesta passagem e, em seguida, no ícone do lápis para editá-la." + "placeholderClick": "Faz duplo clique sobre esta passagem para editá-la.", + "placeholderTouch": "Toca nesta passagem, depois escolhe Editar no Separador Passagem para editá-la." }, "renamePassageButton": { - "emptyName": "Indica um nome, por favor.", - "nameAlreadyUsed": "A história já tem uma passagem com esse nome." + "emptyName": "Introduz um título.", + "nameAlreadyUsed": "Já há uma passagem com esse título nesta história." }, "renameStoryButton": { - "emptyName": "Indica um nome, por favor.", - "nameAlreadyUsed": "Já há uma história com esse nome." + "emptyName": "Introduz o título da história.", + "nameAlreadyUsed": "Já há uma história com esse título." }, "safariWarningCard": { - "addToHomeScreen": "Adiciona esta página ao teu ecrã inicial para evitares esta limitação.", - "archiveAndUseAnotherBrowser": "Arquiva as tuas histórias e usa outra plataforma, por favor.", - "howToAddToHomeScreen": "Como é que eu adiciono isto ao meu ecrã inicial?", + "archiveAndUseAnotherBrowser": "Arquiva as tuas histórias e usa outra plataforma.", + "addToHomeScreen": "Acrescenta esta página ao teu ecrã principal para contornar esta limitação.", + "howToAddToHomeScreen": "Como é que eu acrescento esta página ao meu ecrã principal?", "learnMore": "Saber mais", - "message": "O navegador que estás a usar vai apagar todas as tuas histórias se passares sete dias sem visitar este site." + "message": "O navegador que estás a usar vai apagar todas as tuas histórias se não visitares esta página durante sete dias." + }, + "storageQuota": { + "freeSpace": "{{percent}}% de espaço disponível" }, "storyCard": { - "lastUpdated": "Última edição em {{date}}", + "lastUpdated": "Editada pela última vez em {{date}}", "passageCount": "1 passagem", "passageCount_plural": "{{count}} passagens" }, "storyFormatCard": { - "author": "por {{author}}", + "author": "de {{author}}", + "builtIn": "Criado em", + "defaultFormat": "Usado por Defeito", + "editorExtensionsDisabled": "Extensões do Editor Desligadas", "license": "Licença: {{license}}", - "loadError": "Este formato de história não pôde ser carregado ( {{errorMessage}} ).", - "loadingFormat": "A carregar este formato de história...", - "name": "{{version}} {{name}}", - "useFormat": "Usar como formato de história por defeito", - "useProofingFormat": "Usar como formato de revisão" + "loadingFormat": "A carregar o formato de história...", + "loadError": "O formato de história não pôde ser carregado. Deu este erro: ({{errorMessage}}).", + "name": "{{name}} {{version}}", + "proofing": "Revisão", + "proofingFormat": "Usado para a Revisão", + "useEditorExtensions": "Usar as Extensões do Editor", + "useFormat": "Usar como Formato de História por defeito", + "useProofingFormat": "Usar como Formato de Revisão" }, "storyFormatSelect": { - "loadingCount": "A carregar 1 formato de história ...", - "loadingCount_plural": "A carregar {{loadingCount}} formatos de história..." + "loadingCount": "A carregar 1 Formato de História...", + "loadingCount_plural": "A carregar {{loadingCount}} Formatos de História..." }, "tagEditor": { "alreadyExists": "Já existe uma etiqueta com esse nome." } }, - "dialogs": { - "aboutTwine": { - "codeHeader": "Código", - "codeRepo": "Visitar o repositório do código-fonte", - "donateToTwine": "Ajuda o Twine a crescer com uma doação", - "license": "Esta aplicação é lançada de acordo com a licença GPL v3 , mas qualquer trabalho criado com ela, pode ser lançado sob quaisquer termos, incluindo comerciais.", - "localizationHeader": "Localizações", - "title": "Sobre o {{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", - "noThanks": "Não, obrigado", - "onlyOnce": "(Esta mensagem só te será apresentada uma vez. Se quiseres fazer uma doação para o desenvolvimento do Twine, há um \"link\" na caixa de diálogo \"Sobre o Twine\".)", - "supportMessage": "Se não podes viver sem o Twine, talvez o possas ajudar a crescer, fazendo uma doação. O Twine é um projeto de código-fonte aberto que será sempre gratuito — e graças à tua ajuda, o Twine poderá continuar a prosperar.", - "title": "Apoiar o desenvolvimento do Twine" - }, - "appPrefs": { - "codeEditorFont": "Fonte do editor de código", - "codeEditorFontScale": "Tamanho da fonte do editor de código", - "fontExplanation": "Alterar a fonte aqui, afeta 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 passagens", - "theme": "Tema", - "themeDark": "Escuro", - "themeLight": "Claro", - "themeSystem": "Sistema", - "title": "Preferências" - }, - "passageEdit": { - "setAsStart": "Começar a história aqui", - "size": "Tamanho", - "sizeLarge": "Larga", - "sizeSmall": "Pequeno", - "sizeTall": "Estreita", - "sizeWide": "Larga" - }, - "passageTags": { - "noTags": "Não foram adicionadas etiquetas às passagens desta história.", - "title": "Etiquetas da passagem" - }, - "storyInfo": { - "setStoryFormat": "Definir o formato de história", - "snapToGrid": "Ajustar à grelha", - "stats": { - "brokenLinks": "Links quebrados", - "characters": "Personagens", - "ifid": "O IFID desta história é {{ifid}}.", - "ifidExplanation": "O que é um IFID?", - "lastUpdate": "Esta história foi alterada pela última vez em {{date}} .", - "links": "Links", - "passages": "Passagens", - "title": "Estatísticas da história", - "words": "Palavras" - }, - "storyFormatExplanation": "O que é um formato de história?" - }, - "storyJavaScript": { - "explanation": "Qualquer código de JavaScript aqui introduzido vai correr assim que a história for aberta no navegador.", - "title": "JavaScript da História" - }, - "storySearch": { - "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": "Sem passagens correspondentes", - "replaceAll": "Substituir em todas as passagens", - "replaceWith": "Substituir por", - "title": "Encontrar e Substituir", - "useRegexes": "Usar expressões regulares" - }, - "storyStylesheet": { - "explanation": "Qualquer fragmento de código CSS introduzido aqui irá alterar a aparência padrão da sua história.", - "title": "Folha de estilos da história" - }, - "storyTags": { - "noTags": "Não foram adicionadas etiquetas às tuas histórias.", - "title": "Etiquetas de história" - } - }, - "electron": { - "backupsDirectoryName": "Cópias de segurança", - "errors": { - "jsonSave": "Ocorreu um erro ao gravar o ficheiro de configurações.", - "storyDelete": "Ocorreu um erro ao apagar a história.", - "storyFileChangedExternally": { - "detail": "As alterações vão ser gravadas por cima deste ficheiro. Se quiseres usar este ficheiro em vez da versão que está no Twine, o Twine vai reiniciar, e teu trabalho será substituído pelo do ficheiro.", - "message": "O ficheiro “ {{fileName}} ” da tua biblioteca de histórias foi alterado fora do Twine.", - "overwriteChoice": "Gravar alterações no Twine", - "relaunchChoice": "Usar o ficheiro e reiniciar" - }, - "storyRename": "Ocorreu um erro ao renomear a história.", - "storySave": "Ocorreu um erro ao gravar a história." - }, - "menuBar": { - "edit": "Editar", - "showDevTools": "Mostrar consola de depuração", - "showStoryLibrary": "Mostrar a biblioteca de histórias", - "speech": "Fala", - "troubleshooting": "Solução de problemas", - "twineHelp": "Ajuda do Twine", - "view": "Visualizar" - }, - "storiesDirectoryName": "Histórias" - }, - "routes": { - "storyEdit": { - "topBar": { - "addPassage": "Passagem", - "editJavaScript": "Editar o código JavaScript da história", - "editStylesheet": "Editar a folha de estilos da história", - "findAndReplace": "Encontrar e substituir", - "passageTags": "Editar Etiquetas da Passagem", - "proofStory": "Ver uma cópia para revisão", - "publishToFile": "Publicar como ficheiro", - "selectAllPassages": "Marcar todas as passagens", - "storyInfo": "Informações sobre a história", - "zoomIn": "Aumentar zoom", - "zoomOut": "Reduzir o zoom" - } - }, - "storyFormatList": { - "noneVisible": "Nenhum formato de história corresponde aos critérios que selecionaste.", - "show": "Mostrar...", - "storyFormatExplanation": "Os formatos de história controlam a aparência e o comportamento das histórias durante o jogo.", - "title": { - "all": "Todos os formatos de história", - "current": "Formatos de história disponíveis", - "user": "Formatos de história adicionados pelo utilizador" - } - }, - "storyImport": { - "choosePrompt": "Escolhe as histórias que queres importar do arquivo que carregaste:", - "deselectAll": "Desmarcar tudo", - "importDifferentFile": "Importar um ficheiro diferente", - "importSelected": "Importar os ficheiros selecionados", - "importThisStory": "Importar esta história", - "noStoriesInFile": "Não encontrámos nenhumas histórias do Twine no ficheiro que carregaste. Tenta, por favor, um outro ficheiro.", - "selectAll": "Marcar tudo", - "title": "Importar histórias", - "uploadPrompt": "Para importar histórias para o Twine, carrega, em baixo, um ficheiro de arquivo de histórias ou um ficheiro de uma história.", - "willReplaceExisting": "A história da tua biblioteca com esse mesmo nome será substituída." - }, - "storyList": { - "noStories": "Neste momento, não há histórias gravadas no Twine. Para começares, cria uma história nova ou importa uma de um ficheiro.", - "taggedTitleCount": "1 história etiquetada", - "taggedTitleCount_0": "Sem histórias etiquetadas", - "taggedTitleCount_plural": "{{count}} histórias etiquetadas", - "titleCount": "1 história", - "titleCount_0": "Sem histórias", - "titleCount_plural": "{{count}} Histórias", - "titleGeneric": "Histórias", - "topBar": { - "about": "Sobre o Twine", - "archive": "Arquivo", - "createStory": "História", - "help": "Ajuda", - "reportBug": "Reportar um erro", - "showAllStories": "Todas as histórias", - "showTags": "Mostrar etiquetas...", - "sort": "Ordenar por...", - "sortDate": "Data", - "sortName": "Nome", - "storyFormats": "Formatos", - "storyTags": "Editar as etiquetas da história" - } - }, - "welcome": { - "autosave": "

Agora já tens uma pasta chamada Twine na tua pasta de Documentos. Dentro da pasta Twine, criámos uma pasta chamada Histórias, onde todos os teus trabalhos serão gravados. O Twine grava automaticamente enquanto trabalhas, portanto não precisas de te preocupar em gravar a tua história. Podes sempre abrir a pasta em que as tuas histórias estão gravadas através da opção Mostrar biblioteca no menu do Twine.

Como o Twine está sempre a gravar o teu trabalho, os ficheiros da tua biblioteca de histórias não podem ser editados enquanto o Twine estiver aberto.

Se quiseres abrir o ficheiro de uma história do Twine que recebeste de uma outra pessoa, podes importá-lo para a tua biblioteca usando a opção Importar ficheiro na lista de histórias.

", - "autosaveTitle": "O teu trabalho é gravado automaticamente.", - "browserStorage": "

Isto significa que não precisas de criar uma conta para usar o Twine 2, e tudo o que criares não fica armazenado num servidor sabe-se lá onde — fica aqui no teu navegador.

No entanto, há duas coisas muito importantes de que tens de te lembrar. Como o teu trabalho fica apenas gravado no teu navegador, se limpares os dados do navegador, vais perder o teu trabalho! O que não é nada bom. Lembra-te de usar o botão Arquivar com frequência. Também podes publicar cada história separadamente num ficheiro, usando a opção disponível em cada história, na lista de histórias. Tanto os ficheiros de arquivo, como os ficheiros individuais de história podem ser reimportados, a qualquer momento, para o Twine.

Em segundo lugar, qualquer pessoa que usar este navegador, pode ver e fazer alterações ao teu trabalho. Portanto, se tiveres um irmão mais novo abelhudo, talvez seja boa ideia criares um perfil separado só para ti.

", - "browserStorageTitle": "O teu trabalho fica apenas gravado no navegador", - "done": "

Obrigado por leres, e diverte-te com o Twine.

", - "doneTitle": "E é isto!", - "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. Há algumas coisas que é importante saberes antes de começar.

", - "greetingTitle": "Olá!", - "help": "

Se nunca usaste o Twine antes, bem-vinda(o)! O O Livro de Receitas do Twine é um ótimo recurso para aprenderes a usar o programa. Se nunca usaste o Twine, é um ótimo lugar para começar.

", - "helpTitle": "É a primeira vez aqui?", - "tellMeMore": "Quero saber mais" - } - }, - "store": { - "archiveFilename": "{{timestamp}} Arquivo_do_Twine.html", - "errors": { - "cantPersistPrefs": "Erro ao gravar as preferências ( {{error}} ).", - "cantPersistStories": "Erro ao gravar as tuas histórias ( {{error}} ).", - "cantPersistStoryFormats": "Erro ao gravar os formatos de história ( {{error}} ).", - "electronRemediation": "Reiniciar a aplicação poderá ajudar.", - "webRemediation": "Recarregar esta página poderá ajudar." - }, - "passageDefaults": { - "name": "Passagem sem título" - }, - "storyDefaults": { - "name": "História sem título" - }, - "storyFormatDefaults": { - "name": "Formato de história sem título" - } - }, - "undoChange": { - "changeTagColor": "Mudar a cor da etiqueta", - "createPassage": "Criar passagem", - "deletePassage": "Apagar passagem", - "deletePassages": "Apagar passagens", - "movePassage": "Mover passagem", - "movePassages": "Mover passagens", - "removeTag": "Remover etiqueta", - "renamePassage": "Renomear passagem", - "renameTag": "Renomear etiqueta", - "replaceAllText": "Substituir tudo" - } + "dialogs": { + "aboutTwine": { + "donateToTwine": "Ajuda 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 só te será apresentada uma vez. Se quiseres fazer uma doação para o desenvolvimento do Twine, há uma hiperligação para o efeito na caixa de diálogo Sobre o Twine.)", + "supportMessage": "Se não podes viver sem o Twine, talvez o possas ajudar a crescer, fazendo uma doação. O Twine é um projeto de código-fonte aberto que será sempre gratuito — e graças à tua ajuda, o Twine poderá continuar a crescer.", + "noThanks": "Obrigado, mas não.", + "title": "Apoiar o Desenvolviment do Twine" + }, + "appPrefs": { + "codeEditorFont": "Fonte do Editor de Código", + "codeEditorFontScale": "Tamanho da Fonte do Editor de Código", + "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", + "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": "Alguma coisa esquisita aconteceu com o editor. Tenta fechá-lo e editar a passagem outra vez.", + "passageTextEditorLabel": "Texto da Passagem", + "passageTextPlaceholder": "Escreve o texto da passagem aqui. Para ligares a outra passagem, envolve uma letra, palavra ou expressão com dois colchetes, [[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 para o Twine, carrega, em baixo, um ficheiro de arquivo ou um ficheiro de uma história publicada.", + "importDifferentFile": "Importar um Ficheiro Diferente", + "importSelected": "Importar os Ficheiros Marcados", + "importThisStory": "Importar Esta História", + "noStoriesInFile": "Parece-me que não há histórias Twine no ficheiro que carregaste. Tenta com outro ficheiro.", + "storiesPrompt": "Escolhe as histórias que queres importar:", + "title": "Importar Histórias", + "willReplaceExisting": "Uma história da biblioteca com o mesmo título vai ser substituída." + }, + "storyDetails": { + "storyFormatExplanation": "O que é um formato de história?", + "snapToGrid": "Alinhar à Grelha", + "stats": { + "brokenLinks": "Ligações cortadas", + "characters": "Caracteres", + "title": "Estatísticas 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 introduzido aqui vai correr assim que a história for aberta num navegador." + }, + "storySearch": { + "title": "Encontrar e Substituir", + "find": "Encontrar", + "includePassageNames": "Incluir os Títulos das Passagens", + "matchCase": "Sensível a maiúsculas/minúsculas", + "matchCount": "{{count}} passagem correspondente", + "matchCount_plural": "{{count}} passagens correspondentes", + "noMatches": "Sem passagens correspondentes", + "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 trecho de código CSS introduzido aqui irá alterar a aparência padrão da história." + }, + "storyTags": { + "noTags": "Não foram ainda dadas etiquetas às tuas histórias.", + "title": "Etiquetas de História" + } + }, + "electron": { + "backupsDirectoryName": "Cópias de Segurança", + "errors": { + "jsonSave": "Alguma coisa correu mal durante a gravação do ficheiro com as configurações.", + "storyFileChangedExternally": { + "message": "O ficheiro “{{fileName}}” da tua biblioteca de histórias foi alterado fora do Twine.", + "detail": "As alterações vão ser gravadas por cima deste ficheiro. Se quiseres usar este ficheiro em vez da versão que já está no Twine, o Twine vai reiniciar, e o teu trabalho vai ser substituído pelo do ficheiro.", + "overwriteChoice": "Gravar as Alterações no Twine", + "relaunchChoice": "Usar o Ficheiro e Reiniciar" + }, + "storyDelete": "Alguma coisa correu mal ao apagarmos a história.", + "storyRename": "Alguma coisa correu mal ao alterarmos o título da história.", + "storySave": "Alguma coisa correu mal ao gravarmos a história." + }, + "menuBar": { + "checkForUpdates": "Verificar se há atualizações...", + "edit": "Editar", + "showDevTools": "Mostrar a Consola de Depuração", + "showStoryLibrary": "Mostrar a Biblioteca de Histórias", + "speech": "Fala", + "troubleshooting": "Resolução de Problemas", + "twineHelp": "Ajuda do Twine", + "view": "Ver" + }, + "storiesDirectoryName": "Histórias", + "updateCheck": { + "download": "Descarregar", + "error": "Alguma coisa correu mal ao tentarmos verificar se há uma nova versão do Twine.", + "updateAvailable": "Está disponível uma versão mais recente do Twine.", + "upToDate": "Estás a usar a versão mais recente do Twine." + } + }, + "routes": { + "storyEdit": { + "toolbar": { + "findAndReplace": "Encontrar e Substituir", + "javaScript": "JavaScript", + "passageTags": "Etiquetas das Passagens", + "snapToGrid": "Alinhar à Grelha", + "startStoryHere": "Começar a História Aqui", + "stylesheet": "Folha de Estilos", + "testFromHere": "Testar a partir daqui" + }, + "topBar": { + "editJavaScript": "Editar o código 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 Ficheiro", + "selectAllPassages": "Marcar Todas as Passagens" + }, + "zoomButtons": { + "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.", + "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 vais dar à tua história? Podes mudá-lo mais tarde.", + "emptyName": "Introduz um título.", + "nameConflict": "Já há uma história com esse título." + }, + "deleteStoryButton": { + "warning": { + "electron": "Queres mesmo apagar a história “{{storyName}}”? Vai ser movida para o caixote de lixo.", + "web": "Queres mesmo apagar a história “{{storyName}}”? Vai ser apagada para sempre. Não podes 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 tens uma pasta chamada Twine na pasta dos teus Documentos. Dentro dela há uma pasta intitulada Stories, onde as tuas histórias serão gravadas. O Twine grava a história à medida que vais trabalhando, por isso não tens de te preocupar em gravá-la. Podes sempre abrir a pasta em que as tuas histórias são gravadas através do item Mostrar Biblioteca no menu do Twine.

Como o Twine está sempre a gravar o teu trabalho, os ficheiros da tua biblioteca de histórias vão estar trancados e não poderão ser editados enquanto o Twine estiver aberto.

Se quiseres abrir um ficheiro de uma história Twine que recebeste de alguém, podes importá-lo para a tua biblioteca, fazendo uso da ligação Importar de Ficheiro na lista de histórias.

", + "autosaveTitle": "O teu trabalho é automaticamente gravado.", + "browserStorage": "

Isto significa que não precisas de criar uma conta para usar o Twine 2, e tudo o que criares não ficará guardado num servidor sabe-se lá onde—fica sempre aqui no teu navegador.

Mas não te esqueças de duas coisas muito importantes. Como o teu trabalho é gravado apenas no teu navegador, se limpares os dados guardados, irás perder tudo! Não queremos isso. Lembra-te de usar frequentemente o botão Arquivar. Podes também publicar histórias individuais como ficheiros, usando o menu em cada história na lista de histórias. Tanto os arquivos como os ficheiros de história podem ser sempre importados de volta para o Twine.

Segundo, qualquer pessoa que puder usar este navegador pode ver as tuas histórias e alterá-las. Portanto, se tiveres um irmão abelhudo aí por casa, talvez seja boa ideia aprenderes a criar um perfil separado, só para ti.

", + "browserStorageTitle": "O teu trabalho fica apenas gravado no teu navegador", + "done": "

Obrigado pela leitura, e diverte-te com o Twine.

", + "doneTitle": "E é isto!", + "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. Há algumas coisas que deves saber antes de começar.

", + "greetingTitle": "Alô!", + "tellMeMore": "Quero saber mais", + "help": "

Se nunca antes usaste o Twine, deixa dar-te as boas-vindas! O Livro de Receitas do Twine é um ótimo recurso para aprender a usar o Twine. Se nunca escreveste com o Twine, é um bom sítio para começar.

", + "helpTitle": "É a tua primeira vez aqui?" + } + }, + "routeActions": { + "app": { + "aboutApp": "Sobre o Twine", + "preferences": "Preferências", + "reportBug": "Reportar um Problema", + "storyFormats": "Formatos de História" + }, + "build": { + "play": "Jogar", + "proof": "Rever", + "publishToFile": "Publicar para Ficheiro", + "test": "Testar" + } + }, + "store": { + "archiveFilename": "{{timestamp}} Arquivo do Twine.html", + "errors": { + "cantPersistPrefs": "Alguma coisa correu mal ao gravarmos as tuas preferências: ({{error}}).", + "cantPersistStories": "Alguma coisa correu mal ao gravarmos as tuas histórias: ({{error}}).", + "cantPersistStoryFormats": "Alguma coisa correu mal ao gravamos os formatos de história: ({{error}}).", + "electronRemediation": "Reiniciar a aplicação poderá ajudar.", + "webRemediation": "Recarregar esta página poderá ajudar." + }, + "passageDefaults": { + "name": "Passagem sem Título" + }, + "storyDefaults": { + "name": "História sem Título" + }, + "storyFormatDefaults": { + "name": "Formato de História sem Título" + } + }, + "undoChange": { + "addTag": "Acrescentar Etiqueta", + "changeTagColor": "Mudar a Cor da Etiqueta", + "newPassage": "Nova Passagem", + "deletePassage": "Apagar a Passagem", + "deletePassages": "Apagar as Passagens", + "movePassage": "Mover a Passagem", + "movePassages": "Mover as Passagens", + "imortTag": "Remover a Etiqueta", + "renamePassage": "Mudar o Título da Passagem", + "removeTag": "Remover a Etiqueta", + "renameTag": "Mudar o Nome da Etiqueta", + "replaceAllText": "Substituir Tudo" + } } From 4b5833943e098a29c53c33d6fcfd92ae4cd52148 Mon Sep 17 00:00:00 2001 From: kln95130 Date: Mon, 18 Jul 2022 16:02:58 +0200 Subject: [PATCH 02/26] Update of fr.json missing translations --- public/locales/fr.json | 385 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 352 insertions(+), 33 deletions(-) diff --git a/public/locales/fr.json b/public/locales/fr.json index 2aefc6141..ef5c5cf1c 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -1,5 +1,13 @@ { - "colors": {}, + "colors": { + "none": "Aucune", + "red": "Rouge", + "orange": "Orange", + "yellow": "Jaune", + "green": "Vert", + "blue": "Bleu", + "purple": "Violet" + }, "common": { "add": "Ajouter", "appName": "Twine", @@ -14,50 +22,256 @@ "skip": "Passer", "storyFormat": "Format d'Histoire", "tag": "Balise", - "test": "Test", - "undo": "Annuler" + "test": "Tester", + "undo": "Annuler", + "back": "Précédent", + "build": "Build", + "close": "Fermer", + "color": "Couleur", + "create": "Créer", + "custom": "Personnalisé", + "deleteCount": "Supprimer ({{count}})", + "details": "Détails", + "editCount": "Modifier ({{count}})", + "help": "Aide", + "import": "Import", + "more": "Plus", + "new": "Nouveau", + "next": "Suivant", + "passage": "Passage", + "preferences": "Préferences", + "publishToFile": "Publier vers un Fichier", + "redo": "Répéter", + "redoChange": "Répéter {{change}}", + "renamePrompt": "Comment souhaitez-vous renommer “{{name}}” ?", + "selectAll": "Choisir Tout", + "story": "Histoire", + "twine": "Twine", + "undoChange": "Annuler {{change}}", + "view": "Affichage" }, "components": { "addStoryFormatButton": {}, - "addTagButton": {}, - "fontSelect": {"fonts": {}}, - "indentButtons": {}, - "localStorageQuota": {}, + "addTagButton": { + "alreadyAdded": "Cette balise existe déjà.", + "addLabel": "Ajouter balise", + "invalidName": "Veuillez entrer un nom de balise valide.", + "newTag": "Nouvelle balise", + "tagColorLabel": "Couleur de la balise", + "tagNameLabel": "Nom de la balise" + }, + "fontSelect": { + "customScaleDetail": "Veuillez entrer un pourcentage uniquement.", + "customFamilyDetail": "Veuillez entrer le nom de la police uniquement.", + "familyEmpty": "Veuillez entrer un nom de police.", + "font": "Police d'écriture", + "fonts": { + "monospaced": "Mono-espacé", + "serif": "Serif", + "system": "Système" + }, + "fontSize": "Taille de police", + "percentage": "{{percent}}%", + "percentageIsntNumber": "Veuillez entrer un nombre.", + "percentageNotPositive": "Veuillez entrer un nombre supérieur à 0." + }, + "indentButtons": { + "indent": "Indenter", + "unindent": "Désindenter" + }, + "localStorageQuota": { + "measureAgain": "Mesurer de nouveau l'espace disponible", + "percentAvailable": "{percent}% d'espace disponible" + }, "passageCard": { "placeholderClick": "Double-cliquez sur ce passage pour le modifier.", "placeholderTouch": "Cliquez sur ce passage, puis sur l'icône du crayon pour le modifier." }, - "renamePassageButton": {"emptyName": "Veuillez entrer un nom."}, - "renameStoryButton": {"emptyName": "Veuillez entrer un nom."}, - "safariWarningCard": {}, - "storyCard": {}, - "storyFormatCard": {}, - "storyFormatSelect": {}, - "tagEditor": {} + "renamePassageButton": { + "emptyName": "Veuillez entrer un nom.", + "nameAlreadyUsed": "Un passage de cette histoire utilise déjà ce nom." + }, + "renameStoryButton": { + "emptyName": "Veuillez entrer un nom.", + "nameAlreadyUsed": "Une autre histoire a déjà ce nom." + }, + "safariWarningCard": { + "archiveAndUseAnotherBrowser": "Veuillez archiver vos histoires et utiliser un autre navigateur.", + "addToHomeScreen": "Ajoutez ce site à votre page d'accueil pour contourner cette limitation.", + "howToAddToHomeScreen": "Comment ajouter ce site à ma page d'accueil ?", + "learnMore": "En savoir plus", + "message": "Le navigateur que vous utilisez actuellement supprimera toutes vos histoires dans un délai de sept jours suivant votre dernière visite sur ce site web." + }, + "storyCard": { + "lastUpdated": "Dernière modification le {{date}}", + "passageCount": "1 passage", + "passageCount_plural": "{{count}} passages" + }, + "storyFormatCard": { + "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}}).", + "name": "{{name}} {{version}}", + "proofing": "Vérification", + "proofingFormat": "Utilisé pour la Vérification", + "useEditorExtensions": "Utiliser les Extensions de l'Éditeur", + "useFormat": "Utiliser en tant que format d'histoire par défaut", + "useProofingFormat": "Utiliser en tant que Format de Vérification" + }, + "storyFormatSelect": { + "loadingCount": "Chargement d'un Format d'Histoire...", + "loadingCount_plural": "Charement de {{loadingCount}} Formats d'Histoire..." + }, + "tagEditor": { + "alreadyExists": "Une balise portant ce nom existe déjà." + }, + "dialogCard": { + "contentsCrashed": "Un problème est survenu avec cette boîte de dialogue. Essayez de la fermer et de l'ouvrir à nouveau." + }, + "storageQuota": { + "freeSpace": "{{percent}}% d'espace disponible" + } }, "dialogs": { - "aboutTwine": {"donateToTwine": "Aidez Twine à Grandir Grâce À Un Don"}, - "appDonation": {"noThanks": "Non Merci"}, - "appPrefs": {"language": "Langue"}, - "passageEdit": {}, - "passageTags": {}, - "storyInfo": {"stats": {"title": "Statistiques de l'Histoire"}}, + "aboutTwine": { + "donateToTwine": "Aidez Twine à Grandir Grâce À Un Don", + "codeHeader": "Code", + "codeRepo": "Visiter le Dépôt du code source", + "license": "Cette application est publiée sous la licence GPL v3, mais toute oeuvre créée par son biais peut être publiée selon n'importe quel terme, y compris des termes commerciaux.", + "localizationHeader": "Localisations", + "title": "À Propos de Twine {{version}}", + "twineDescription": "Twine est une application open-source servant à la création d'histoires intéractives et non-linéaires." + }, + "appDonation": { + "donate": "Faire un don pour le développement de Twine", + "onlyOnce": "(Ce message ne sera affiché qu'une seule fois. Si vous désirez faire un don pour le développement de Twine à l'avenir, un lien à cet effet se trouve dans la fenêtre 'À Propos de Twine'.)", + "supportMessage": "Si vous aimez Twine, pensez à aider notre travail avec un don. Twine est un projet open-source, qui restera gratuit et dynamique grâce à votre soutien.", + "noThanks": "Non Merci", + "title": "Aider le développement de Twine." + }, + "appPrefs": { + "codeEditorFont": "Police de l'éditeur de code", + "codeEditorFontScale": "Taille de la police de l'éditeur de code", + "editorCursorBlinks": "Curseur clignotant dans les éditeurs", + "fontExplanation": "Modifier la police d'écriture ici affectera uniquement l'éditeur Twine. Cela ne modifiera pas la police utilisée par une histoire lorsqu'elle est jouée.", + "language": "Langue", + "passageEditorFont": "Police de l'éditeur de passage", + "passageEditorFontScale": "Taille de la police de l'éditeur de passage", + "themeLight": "Clair", + "themeDark": "Sombre", + "themeSystem": "Système", + "theme": "Thème", + "title": "Préférences" + }, + "passageEdit": { + "editorCrashed": "Un problème est survenu avec cet éditeur. Essayez de le fermer et d'éditeur à nouveau ce passage.", + "passageTextEditorLabel": "Texte du Passage", + "passageTextPlaceholder": "Entrez le corps de votre passage ici. Pour faire un lien avec un autre passage, entourez son nom entre deux paires de crochets, [[comme ceci]].", + "setAsStart": "Commencez votre histoire ici", + "size": "Taille", + "sizeLarge": "Grand", + "sizeSmall": "Petit", + "sizeTall": "Long", + "sizeWide": "Large" + }, + "passageTags": { + "noTags": "Aucune balise n'a été ajoutée aux passages de cette histoire.", + "title": "Balises du passage" + }, + "storyInfo": { + "stats": { + "title": "Statistiques de l'Histoire" + } + }, "storyJavaScript": { + "editorLabel": "JavaScript de l'histoire", + "title": "JavaScript de l'histoire", "explanation": "Tout JavaScript ajouté ici sera lancé dès que votre histoire sera ouverte dans un navigateur Web." }, "storySearch": { "title": "Chercher et Remplacer", - "replaceWith": "Remplacer Par" + "find": "Rechercher", + "includePassageNames": "Inclure les noms des passages", + "matchCase": "Casse exacte", + "matchCount": "{{count}} passage trouvé", + "matchCount_plural": "{{count}} passages trouvés", + "noMatches": "Aucun passage trouvé", + "replaceAll": "Remplacer dans tous les Passages", + "replaceWith": "Remplacer Par", + "useRegexes": "Utiliser une Expression Régulière" }, "storyStylesheet": { + "editorLabel": "Feuille de style de l'histoire", + "title": "Feuille de style de l'histoire", "explanation": "Tout CSS ajouté ici remplacera l'apparence par défaut de votre histoire" }, - "storyTags": {} + "storyTags": { + "noTags": "Aucune balise n'a été ajoutée à votre histoire.", + "title": "Balises de l'histoire" + }, + + "storyImport": { + "deselectAll": "Désélectionner tout", + "filePrompt": "Pour importer une histoire dans Twine, téléchargez une archive ou une histoire publiée.", + "importDifferentFile": "Importer un autre fichier", + "importSelected": "Importer les fichiers sélectionnés", + "importThisStory": "Importer cette histoire", + "noStoriesInFile": "Le fichier que vous avez chargé ne semble contenir aucune histoire Twine. Veuillez choisir un autre fichier.", + "storiesPrompt": "Choisissez les histoires à importer:", + "title": "Importer les histoires", + "willReplaceExisting": "Une histoire au nom similaire dans votre bibliothèque sera écrasée." + }, + "storyDetails": { + "storyFormatExplanation": "Qu'est-ce qu'un format d'histoire ?", + "snapToGrid": "Ajuster à la grille", + "stats": { + "brokenLinks": "Liens cassés", + "characters": "Caractères", + "title": "Statistiques de l'histoire", + "ifid": "L'IFID de cette histoire est {{ifid}}.", + "ifidExplanation": "Qu'est-ce qu'un IFID?", + "lastUpdate": "Dernière modification de cette histoire le {{date}}.", + "links": "Liens", + "passages": "Passages", + "words": "Mots" + } + } }, "electron": { - "errors": {"storyFileChangedExternally": {}}, - "menuBar": {"edit": "Modifier"}, - "storiesDirectoryName": "Histoires" + "backupsDirectoryName": "Sauvegardes", + "errors": { + "jsonSave": "Un problème est survenu durant la sauvegarde d'un fichier de paramètres.", + "storyFileChangedExternally": { + "message": "Le fichier “{{fileName}}” dans votre bibliothèque a été modifié en-dehors de Twine.", + "detail": "Sauvegarder des modifications écrasera ce fichier. Si vous souhaitez utiliser ce fichier au lieu de sa version dans Twine, Twine redémarrera et votre travail sera écrasé par le fichier.", + "overwriteChoice": "Sauvegarder les modifications dans Twine", + "relaunchChoice": "Utiliser le fichier et redémarrer" + }, + "storyDelete": "Un problème est survenu durant la suppression d'une histoire.", + "storyRename": "Un problème est survenu durant le renommage d'une histoire.", + "storySave": "Un problème est survenu durant la sauvegarde d'une histoire." + }, + "menuBar": { + "checkForUpdates": "Vérification des mises à jour...", + "edit": "Modifier", + "showDevTools": "Afficher la console de debug", + "showStoryLibrary": "Afficher la bibliothèque d'histoires", + "speech": "Discours", + "troubleshooting": "Résolution de problèmes", + "twineHelp": "Aide Twine", + "view": "Afficher" + }, + "storiesDirectoryName": "Histoires", + "updateCheck": { + "download": "Téléchargement", + "error": "Un problème est survenu durant la vérification des mises à jour de Twine.", + "updateAvailable": "Une nouvelle version de Twine est disponible.", + "upToDate": "Vous avez la version la plus récente de Twine." + } }, "routes": { "storyEdit": { @@ -67,15 +281,50 @@ "editStylesheet": "Modifier la Feuille de Style de l'Histoire", "findAndReplace": "Chercher et Remplacer", "proofStory": "Voir Épreuve de Correction", - "publishToFile": "Publier vers un Fichier" + "publishToFile": "Publier vers un Fichier", + "selectAllPassages": "Selectionner tous les Passages" + }, + "toolbar": { + "findAndReplace": "Chercher et Remplacer", + "javaScript": "JavaScript", + "passageTags": "Balises du passage", + "snapToGrid": "Ajuster à la grille", + "startStoryHere": "Commencer l'histoire ici", + "stylesheet": "Feuille de style", + "testFromHere": "Tester à partir d'ici" + }, + "zoomButtons": { + "storyStructure": "Afficher la structure de l'histoire uniquement", + "passageNames": "Afficher les noms des passage uniquement", + "passageNamesAndExcerpts": "Afficher les noms des passages et leurs résumés" } }, "storyFormatList": { - "title": {}, - "storyFormatExplanation": "Les formats d'histoire contrôlent l'apparence et le comportement des histoires durant la lecture." + "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", "noStories": "Il n'y a aucune histoire sauvegardée dans Twine pour le moment. Pour commencer, vous pouvez soit créer une nouvelle histoire soit en importer une depuis un fichier.", "titleGeneric": "Histoires", "topBar": { @@ -85,6 +334,32 @@ "help": "Aide", "sortName": "Nom", "storyFormats": "Formats" + }, + "taggedTitleCount": "1 Histoire balisée", + "taggedTitleCount_0": "Aucune histoire balisée", + "taggedTitleCount_plural": "{{count}} histoires balisées", + "titleCount": "1 histoire", + "titleCount_0": "Aucune histoire", + "titleCount_plural": "{{count}} histoires", + "toolbar": { + "archive": "Archive", + "createStoryButton": { + "prompt": "Comment appelerez-vous votre histoire ? Vous pouvez changer ce nom ultérieurement.", + "emptyName": "Veuillez entrer un titre.", + "nameConflict": "Ce titre est déjà utilisé par une autre histoire." + }, + "deleteStoryButton": { + "warning": { + "electron": "Voulez-vous vraiment supprimer “{{storyName}}”? L'histoire sera mise à la corbeille.", + "web": "Voulez-vous vraiment supprimer “{{storyName}}”? L'histoire sera définitevement supprimée. Vous ne pourrez pas revenir en arrière." + } + }, + "showAllStories": "Afficher toutes les histoires", + "showTags": "Afficher les balises", + "sort": "Trier par", + "sortByDate": "Dernière modification", + "sortByName": "Titre", + "storyTags": "Balises de l'histoire" } }, "welcome": { @@ -92,17 +367,61 @@ "doneTitle": "C'est tout !", "gotoStoryList": "Aller à la Liste des Histoires", "greetingTitle": "Salut !", + "greeting": "

Twine est une application open-source servant à la création d'histoires intéractives et non-linéaires. Il y a quelques petites choses à savoir avant de commencer.

", "tellMeMore": "En Savoir Plus", - "helpTitle": "Nouveau venu ?" + "helpTitle": "Nouveau venu ?", + "help": "

Si vous n'avez jamais utilisé Twine auparavant, bienvenue ! Le Livre de Recettes Twine est une ressource très utilse pour apprendre à l'utiliser. Si vous n'avez jamais utilisé Twine auparavant, c'est un bon endroit pour commencer.

", + "autosave": "

Un dossier appelé 'Twine' se trouve désormais dans votre répertoire 'Documents'. Un répertoire 'Stories' se trouve à l'intérieur, et servira à contenir toutes vos sauvegardes. Twine sauvegarde automatiquement votre travail, vous n'avez donc pas à vous soucier de sauvegarder manuellement. Vous pouvez toujours ouvrir le dossier dans lequel vos histoires sont sauvegardées en utilisant le bouton Afficher la bibliothèque dans le menu de Twine Twine.

Comme Twine sauvegarde constamment votre travail, les fichiers de votre bibliothèque seront vérouillés en écriture tant que Twine est ouvert.

Si vous souhaitez ouvrir un fichier d'histoire Twine que vous avez reçu d'un tiers, vous pouvez l'importer dans votre bibliothèque en utilisant le bouton Importer depuis un fichier dans la liste des histoires.

", + "browserStorage": "

Cela signifie que vous n'avez pas besoin de créer un compte pour utiliser Twine 2, et que tout ce que vous créez n'est pas stocké sur un serveur distant—tout est conservé dans votre navigateur.

Deux choses très importantes sont à retenir, cependant. Comme votre travail n'est sauvegardé que sur votre navigateur, effacer vos données signifiera perdre votre travail! Souvenez-vous d'Archiver régulièrement votre travail. Vous pouvez également publier vos histoires dans des fichiers, en utilisant l'option correspondante dans le menu de chaque histoire dans la liste des histoires. Les archives et les fichiers d'histoire peuvent être réimportées dans Twine à n'importe quel moment.

D'autre part, toute personne utilisant ce navigateur peut voir et éditeur votre travail. Si vous avez un petit frère qui aime mettre son nez partout, considérez la création d'un profil à part pour vous-même.

", + "browserStorageTitle": "Votre travail est uniquement sauvegardé dans votre navigateur", + "done": "

Merci d'avoir lu, et amusez-vous bien avec Twine.

" + } + }, + "routeActions": { + "app": { + "aboutApp": "À propos de Twine", + "preferences": "Préférences", + "reportBug": "Signaler un Bug", + "storyFormats": "Formats d'histoire" + }, + "build": { + "play": "Jouer", + "proof": "Vérifier", + "publishToFile": "Publier dans un fichier", + "test": "Tester" } }, "store": { - "errors": {}, + "archiveFilename": "{{timestamp}} Twine Archive.html", + "errors": { + "cantPersistPrefs": "Un problème est survenu durant la sauvegardes de vos préférences ({{error}}).", + "cantPersistStories": "Un problème est survenu durant la sauvegardes de vos histoires ({{error}}).", + "cantPersistStoryFormats": "Un problème est survenu durant la sauvegardes de vos formats d'histoires ({{error}}).", + "electronRemediation": "Redémarrer cette application peut aider.", + "webRemediation": "Recharger cette page peut aider." + }, "passageDefaults": { "name": "Passage sans titre" }, - "storyDefaults": {"name": "Histoire sans Titre."}, - "storyFormatDefaults": {"name": "Format d'Histoire sans Titre."} + "storyDefaults": { + "name": "Histoire sans Titre" + }, + "storyFormatDefaults": { + "name": "Format d'Histoire sans Titre" + } }, - "undoChange": {"replaceAllText": "Remplacer Tout"} + "undoChange": { + "addTag": "Ajouter balise", + "changeTagColor": "Changer la couleur de la balise", + "newPassage": "Nouveau Passage", + "deletePassage": "Supprimer le Passage", + "deletePassages": "Supprimer les Passages", + "movePassage": "Déplacer le Passage", + "movePassages": "Déplacer les Passages", + "imortTag": "Supprimer la balise", + "renamePassage": "Renommer le Passage", + "removeTag": "Supprimer la balise", + "renameTag": "Renommer la balise", + "replaceAllText": "Remplacer Tout" + } } From 74320b25a99fea065a61412fbadeffd0d5c6e3a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Jul 2022 18:36:17 +0000 Subject: [PATCH 03/26] Bump terser from 4.8.0 to 4.8.1 Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1. - [Release notes](https://github.com/terser/terser/releases) - [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md) - [Commits](https://github.com/terser/terser/commits) --- updated-dependencies: - dependency-name: terser dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 207 +++++++++++++++++++++++++++++++++------------- 1 file changed, 151 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index eaf79ca39..687d8e2d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "Twine", - "version": "2.4.0", + "version": "2.4.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "Twine", - "version": "2.4.0", + "version": "2.4.1", "license": "GPL-3.0", "dependencies": { "@popperjs/core": "^2.9.1", @@ -2637,6 +2637,64 @@ "node": ">=8" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", + "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@malept/cross-spawn-promise": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", @@ -24878,9 +24936,9 @@ } }, "node_modules/source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "dependencies": { "buffer-from": "^1.0.0", @@ -25763,9 +25821,9 @@ } }, "node_modules/terser": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", - "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", "dev": true, "dependencies": { "commander": "^2.20.0", @@ -25806,6 +25864,18 @@ "webpack": "^4.0.0 || ^5.0.0" } }, + "node_modules/terser-webpack-plugin/node_modules/acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/terser-webpack-plugin/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -25908,14 +25978,15 @@ } }, "node_modules/terser-webpack-plugin/node_modules/terser": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.6.0.tgz", - "integrity": "sha512-vyqLMoqadC1uR0vywqOZzriDYzgEkNJFK4q9GeyOBHIbiECHiWLKcWfbQWAUaPfxkjDhapSlZB9f7fkMrvkVjA==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "dev": true, "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", - "source-map-support": "~0.5.19" + "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" @@ -25924,15 +25995,6 @@ "node": ">=10" } }, - "node_modules/terser-webpack-plugin/node_modules/terser/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -27758,15 +27820,6 @@ "node": ">=6" } }, - "node_modules/webpack-dev-server/node_modules/is-absolute-url": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", - "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/webpack-dev-server/node_modules/is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", @@ -31142,6 +31195,55 @@ } } }, + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", + "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "@malept/cross-spawn-promise": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", @@ -49126,9 +49228,9 @@ } }, "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -49848,9 +49950,9 @@ } }, "terser": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", - "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", "dev": true, "requires": { "commander": "^2.20.0", @@ -49889,6 +49991,12 @@ "webpack-sources": "^1.4.3" }, "dependencies": { + "acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true + }, "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -49957,22 +50065,15 @@ "dev": true }, "terser": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.6.0.tgz", - "integrity": "sha512-vyqLMoqadC1uR0vywqOZzriDYzgEkNJFK4q9GeyOBHIbiECHiWLKcWfbQWAUaPfxkjDhapSlZB9f7fkMrvkVjA==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "dev": true, "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", - "source-map-support": "~0.5.19" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } + "source-map-support": "~0.5.20" } } } @@ -51692,12 +51793,6 @@ "resolve-cwd": "^2.0.0" } }, - "is-absolute-url": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", - "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", - "dev": true - }, "is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", From f52d14e3711484e70e9b498b3f839947f93bb6f9 Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Fri, 8 Jul 2022 10:06:19 -0400 Subject: [PATCH 04/26] Delete orphaned passages on text change --- .../src/editing-stories/editing-passages.md | 13 +++ docs/en/src/editing-stories/navigating.md | 13 +++ .../passage/__tests__/passage-card.test.tsx | 5 + src/components/passage/passage-card.css | 23 +++- src/components/passage/passage-card.tsx | 8 +- src/dialogs/passage-edit/passage-toolbar.tsx | 9 +- .../toolbar/passage/passage-actions.tsx | 9 +- .../delete-orphaned-passages.test.ts | 106 ++++++++++++++++++ .../__tests__/update-passage.test.ts | 64 +++++++++-- .../delete-orphaned-passages.ts | 69 ++++++++++++ .../stories/action-creators/update-passage.ts | 22 +++- src/util/__tests__/passage-is-empty.test.ts | 46 ++++++++ src/util/passage-is-empty.ts | 14 +++ 13 files changed, 369 insertions(+), 32 deletions(-) create mode 100644 src/store/stories/action-creators/__tests__/delete-orphaned-passages.test.ts create mode 100644 src/store/stories/action-creators/delete-orphaned-passages.ts create mode 100644 src/util/__tests__/passage-is-empty.test.ts create mode 100644 src/util/passage-is-empty.ts diff --git a/docs/en/src/editing-stories/editing-passages.md b/docs/en/src/editing-stories/editing-passages.md index 0791de530..7f11c081b 100644 --- a/docs/en/src/editing-stories/editing-passages.md +++ b/docs/en/src/editing-stories/editing-passages.md @@ -23,6 +23,19 @@ extensions](../story-formats/extensions.md). Twine automatically saves your changes to a passage after you stop typing for a moment. +## Automatically-Created Links + +As you enter text in a passage, Twine will detect when you've added new links. +If the destination passage doesn't already exist, it will create an empty +passage for you. Deleting the link will delete this empty passage. + +Twine won't delete an empty passage while editing if any of the criteria below are true: + +- It is linked to from another passage +- It has any tags +- It has a different size than the default +- It is the story start + ## Text Formatting, Code, Images, Sound, Video... Basically Everything Cool You should consult the documentation of the story format you are using for how diff --git a/docs/en/src/editing-stories/navigating.md b/docs/en/src/editing-stories/navigating.md index 72fb2b1fd..d001e52a7 100644 --- a/docs/en/src/editing-stories/navigating.md +++ b/docs/en/src/editing-stories/navigating.md @@ -14,6 +14,19 @@ In one corner of the Story Map, you'll see three buttons showing squares of different sizes. These let you zoom in and out of the map, showing different levels of detail in your passages. +## Empty Passages + +An empty passage is one you haven't written any text in (usually). These show up +in the Story Map as translucent cards with a dotted border. Twine automatically +creates and deletes empty passages when you [edit links in +passages](./editing-passages.md). + +## Tags + +If you have [assigned colors to passage tags](tagging.md), passages with those +tags will have a small stripe of that color at their top. Tags that do not have +colors assigned will not show a stripe. + ## The Story Start The story's start passage is drawn in the map with a green rocket icon connected diff --git a/src/components/passage/__tests__/passage-card.test.tsx b/src/components/passage/__tests__/passage-card.test.tsx index fe33747ff..e5d3a7b87 100644 --- a/src/components/passage/__tests__/passage-card.test.tsx +++ b/src/components/passage/__tests__/passage-card.test.tsx @@ -44,6 +44,11 @@ describe('', () => { describe('when passage text is empty', () => { const passage = fakePassage({text: ''}); + it("gives it an 'empty' CSS class", () => { + renderComponent({passage}); + expect(document.querySelector('.passage-card.empty')).toBeInTheDocument(); + }); + it('displays a touch-oriented placeholder message on a touch device', () => { (detectIt as any).deviceType = 'touchOnly'; renderComponent({passage}); diff --git a/src/components/passage/passage-card.css b/src/components/passage/passage-card.css index 372ebfaf2..8d424b475 100644 --- a/src/components/passage/passage-card.css +++ b/src/components/passage/passage-card.css @@ -36,6 +36,19 @@ overflow: ellipsis; } +.passage-card.empty .card { + background: hsla(0, 100%, 100%, 0.33); + border: 1px dashed var(--gray); + box-shadow: none; + backdrop-filter: blur(1px); +} + +.passage-card.empty.selected .card { + background: hsla(212, 90%, 60%, 0.1); + border: 1px dashed var(--blue); + box-shadow: none; +} + .compact-passage-cards .passage-card h2 { align-items: center; display: flex; @@ -53,4 +66,12 @@ .compact-passage-cards .passage-card .tag-stripe span { border-radius: 4px; height: 8px; -} \ No newline at end of file +} + +[data-app-theme='dark'] .passage-card.empty .card { + background: hsla(0, 100%, 100%, 0.1); +} + +[data-app-theme='dark'] .passage-card.empty.selected .card { + background: hsla(212, 90%, 60%, 0.2); +} diff --git a/src/components/passage/passage-card.tsx b/src/components/passage/passage-card.tsx index 40a82057b..26893451f 100644 --- a/src/components/passage/passage-card.tsx +++ b/src/components/passage/passage-card.tsx @@ -36,8 +36,12 @@ export const PassageCard: React.FC = React.memo(props => { } = props; const {t} = useTranslation(); const className = React.useMemo( - () => classNames('passage-card', {selected: passage.selected}), - [passage.selected] + () => + classNames('passage-card', { + empty: passage.text === '' && passage.tags.length === 0, + selected: passage.selected + }), + [passage.selected, passage.tags.length, passage.text] ); const container = React.useRef(null); const excerpt = React.useMemo(() => { diff --git a/src/dialogs/passage-edit/passage-toolbar.tsx b/src/dialogs/passage-edit/passage-toolbar.tsx index cd6e4ce8d..8306fb1ce 100644 --- a/src/dialogs/passage-edit/passage-toolbar.tsx +++ b/src/dialogs/passage-edit/passage-toolbar.tsx @@ -55,14 +55,7 @@ export const PassageToolbar: React.FC = props => { // existing passages, updates them, but does not see that the passage name // has been updated since that hasn't happened yet. - dispatch( - updatePassage( - story, - passage, - {name}, - {dontCreateNewlyLinkedPassages: true} - ) - ); + dispatch(updatePassage(story, passage, {name}, {dontUpdateOthers: true})); } function handleSetAsStart() { diff --git a/src/routes/story-edit/toolbar/passage/passage-actions.tsx b/src/routes/story-edit/toolbar/passage/passage-actions.tsx index 7a80525f0..8e27ef3db 100644 --- a/src/routes/story-edit/toolbar/passage/passage-actions.tsx +++ b/src/routes/story-edit/toolbar/passage/passage-actions.tsx @@ -44,14 +44,7 @@ export const PassageActions: React.FC = ({ // existing passages, updates them, but does not see that the passage name // has been updated since that hasn't happened yet. - dispatch( - updatePassage( - story, - passage, - {name}, - {dontCreateNewlyLinkedPassages: true} - ) - ); + dispatch(updatePassage(story, passage, {name}, {dontUpdateOthers: true})); } return ( diff --git a/src/store/stories/action-creators/__tests__/delete-orphaned-passages.test.ts b/src/store/stories/action-creators/__tests__/delete-orphaned-passages.test.ts new file mode 100644 index 000000000..f184dcc31 --- /dev/null +++ b/src/store/stories/action-creators/__tests__/delete-orphaned-passages.test.ts @@ -0,0 +1,106 @@ +import {fakeStory} from '../../../../test-util'; +import {StoriesDispatch, StoriesState, Story} from '../../stories.types'; +import {deleteOrphanedPassages} from '../delete-orphaned-passages'; + +describe('deleteOrphanedPassages', () => { + let dispatch: StoriesDispatch; + let dispatchMock: jest.Mock; + let getState: () => StoriesState; + let story: Story; + + beforeEach(() => { + dispatch = jest.fn(); + dispatchMock = dispatch as jest.Mock; + getState = () => [story]; + + // Guarantee the first passage is empty. + + story = fakeStory(3); + story.passages[0].height = 100; + story.passages[0].text = ''; + story.passages[0].tags = []; + story.passages[0].width = 100; + story.passages[1].text = `[[${story.passages[0].name}]]`; + + // Guarantee it's not the start and no other links exist. + story.startPassage = story.passages[1].id; + story.passages[2].text = ''; + }); + + it('deletes empty passages that were only linked to from the changed passage', () => { + deleteOrphanedPassages( + story, + story.passages[1], + '', + story.passages[1].text + )(dispatch, getState); + expect(dispatchMock.mock.calls).toEqual([ + [ + { + type: 'deletePassages', + passageIds: [story.passages[0].id], + storyId: story.id + } + ] + ]); + }); + + it("does not delete a passage linked to from another passage, even if it's empty", () => { + story.passages[2].text = `[[${story.passages[0].name}]]`; + deleteOrphanedPassages( + story, + story.passages[1], + '', + story.passages[1].text + )(dispatch, getState); + expect(dispatchMock).not.toBeCalled(); + }); + + it("does not delete a passage that isn't empty", () => { + story.passages[0].text = 'not empty'; + deleteOrphanedPassages( + story, + story.passages[1], + '', + story.passages[1].text + )(dispatch, getState); + expect(dispatchMock).not.toBeCalled(); + }); + + it('does not delete a passage that is the story start', () => { + story.startPassage = story.passages[0].id; + deleteOrphanedPassages( + story, + story.passages[1], + '', + story.passages[1].text + )(dispatch, getState); + expect(dispatchMock).not.toBeCalled(); + }); + + it('does not delete empty passages that have no relationship to the changed passage', () => { + story.passages[1].text = 'no link to passages[0]'; + deleteOrphanedPassages( + story, + story.passages[1], + '', + story.passages[1].text + )(dispatch, getState); + expect(dispatchMock).not.toBeCalled(); + }); + + it('dispatches no actions if no passages are orphaned', () => { + deleteOrphanedPassages( + story, + story.passages[1], + `${story.passages[1].text} [[a new link]]`, + story.passages[1].text + )(dispatch, getState); + expect(dispatchMock).not.toBeCalled(); + }); + + it("throws an error if given a passage that doesn't belong to the story", () => + expect(() => + deleteOrphanedPassages(story, {...story.passages[1], id: 'bad'}, '', '') + ).toThrow()); +}); diff --git a/src/store/stories/action-creators/__tests__/update-passage.test.ts b/src/store/stories/action-creators/__tests__/update-passage.test.ts index 7f33da170..ab60db3e3 100644 --- a/src/store/stories/action-creators/__tests__/update-passage.test.ts +++ b/src/store/stories/action-creators/__tests__/update-passage.test.ts @@ -2,11 +2,14 @@ import {Story, StoriesState, StoriesDispatch} from '../../stories.types'; import {updatePassage} from '../update-passage'; import {fakeStory} from '../../../../test-util'; import {createNewlyLinkedPassages} from '../create-newly-linked-passages'; +import {deleteOrphanedPassages} from '../delete-orphaned-passages'; jest.mock('../create-newly-linked-passages'); +jest.mock('../delete-orphaned-passages'); describe('updatePassage action creator', () => { const createNewlyLinkedPassagesMock = createNewlyLinkedPassages as jest.Mock; + const deleteOrphanedPassagesMock = deleteOrphanedPassages as jest.Mock; let dispatch: StoriesDispatch; let dispatchMock: jest.Mock; let getState: () => StoriesState; @@ -16,7 +19,7 @@ describe('updatePassage action creator', () => { dispatch = jest.fn(); dispatchMock = dispatch as jest.Mock; story = fakeStory(1); - getState = () => [story]; + getState = jest.fn(() => [story]); }); describe('The thunk it returns', () => { @@ -46,7 +49,7 @@ describe('updatePassage action creator', () => { story, story.passages[0], {name: 'test name'}, - {dontCreateNewlyLinkedPassages: true} + {dontUpdateOthers: true} )(dispatch, getState); expect(dispatchMock.mock.calls).toEqual([ [ @@ -77,7 +80,7 @@ describe('updatePassage action creator', () => { story, story.passages[0], {name: 'test name'}, - {dontCreateNewlyLinkedPassages: true} + {dontUpdateOthers: true} )(dispatch, getState); expect(dispatchMock.mock.calls).toEqual([ [ @@ -107,7 +110,7 @@ describe('updatePassage action creator', () => { story, story.passages[0], {name: '.*?\\1$1'}, - {dontCreateNewlyLinkedPassages: true} + {dontUpdateOthers: true} )(dispatch, getState); expect(dispatchMock.mock.calls).toEqual([ [ @@ -137,7 +140,7 @@ describe('updatePassage action creator', () => { story, story.passages[0], {name: 'new .*?\\1$1'}, - {dontCreateNewlyLinkedPassages: true} + {dontUpdateOthers: true} )(dispatch, getState); expect(dispatchMock.mock.calls).toEqual([ [ @@ -178,13 +181,54 @@ describe('updatePassage action creator', () => { ).toThrow(); }); - it('calls createNewlyLinkedPassages when text is changed', () => { + it('calls deleteOrphanedPassages when text is changed', () => { const oldText = story.passages[0].text; updatePassage(story, story.passages[0], {text: 'new text'})( dispatch, getState ); + expect(deleteOrphanedPassagesMock.mock.calls).toEqual([ + [story, story.passages[0], 'new text', oldText] + ]); + }); + + it("doesn't call deleteOrphanedPassages if text isn't being changed", () => { + updatePassage(story, story.passages[0], {name: 'new name'})( + dispatch, + getState + ); + expect(deleteOrphanedPassagesMock).not.toBeCalled(); + }); + + it("doesn't call deleteOrphanedPassages if the dontUpdateOthers option is true", () => { + updatePassage( + story, + story.passages[0], + {text: 'new text'}, + {dontUpdateOthers: true} + )(dispatch, getState); + expect(deleteOrphanedPassagesMock).not.toBeCalled(); + }); + + it('deletes orphans before creating new passages', () => { + updatePassage(story, story.passages[0], {text: 'new text'})( + dispatch, + getState + ); + expect( + deleteOrphanedPassagesMock.mock.invocationCallOrder[0] + ).toBeLessThan(createNewlyLinkedPassagesMock.mock.invocationCallOrder[0]); + }); + + it('calls createNewlyLinkedPassages with the most recent state when text is changed', () => { + const oldText = story.passages[0].text; + + updatePassage(story, story.passages[0], {text: 'new text'})( + dispatch, + getState + ); + expect(getState).toBeCalledTimes(1); expect(createNewlyLinkedPassagesMock.mock.calls).toEqual([ [story, story.passages[0], 'new text', oldText] ]); @@ -195,17 +239,17 @@ describe('updatePassage action creator', () => { dispatch, getState ); - expect(createNewlyLinkedPassagesMock.mock.calls).toEqual([]); + expect(createNewlyLinkedPassagesMock).not.toBeCalled(); }); - it("doesn't call createNewlyLinkedPassages if the dontCreateNewlyLinkedPassages option is true", () => { + it("doesn't call createNewlyLinkedPassages if the dontUpdateOthers option is true", () => { updatePassage( story, story.passages[0], {text: 'new text'}, - {dontCreateNewlyLinkedPassages: true} + {dontUpdateOthers: true} )(dispatch, getState); - expect(createNewlyLinkedPassagesMock.mock.calls).toEqual([]); + expect(createNewlyLinkedPassagesMock).not.toBeCalled(); }); }); }); diff --git a/src/store/stories/action-creators/delete-orphaned-passages.ts b/src/store/stories/action-creators/delete-orphaned-passages.ts new file mode 100644 index 000000000..74e78e36c --- /dev/null +++ b/src/store/stories/action-creators/delete-orphaned-passages.ts @@ -0,0 +1,69 @@ +import {Thunk} from 'react-hook-thunk-reducer'; +import {parseLinks} from '../../../util/parse-links'; +import {passageIsEmpty} from '../../../util/passage-is-empty'; +import { + DeletePassagesAction, + Passage, + StoriesState, + Story +} from '../stories.types'; + +/** + * Deletes empty, orphaned passages from a story after passage text changes. An orphan + * must meet all these criteria: + * + * - It is considered empty (see util/passage-is-empty.ts) + * - It is not the story start + * - It was linked previously from the passage, but is not anymore + * - It is only linked to from the passage whose text is changing + * + * The intent is to delete passages that were automatically created in the past, + * but the user has removed the link through editing without ever editing the + * passage. + */ +export function deleteOrphanedPassages( + story: Story, + passage: Passage, + newText: string, + oldText: string +): Thunk { + if (!story.passages.some(p => p.id === passage.id)) { + throw new Error('This passage does not belong to this story.'); + } + + return dispatch => { + const oldLinks = parseLinks(oldText); + const newLinks = parseLinks(newText); + const orphans = oldLinks.filter(link => !newLinks.includes(link)); + + const passageIds = orphans.reduce((result, orphan) => { + const orphanPassage = story.passages.find(p => p.name === orphan); + + // These tests are fast because they look at the passage object only. + + if ( + !orphanPassage || + !passageIsEmpty(orphanPassage) || + story.startPassage === orphanPassage.id + ) { + return result; + } + + // This is O(n) potentially. + + if ( + story.passages.some( + p => p.id !== passage.id && parseLinks(p.text).includes(orphan) + ) + ) { + return result; + } + + return [...result, orphanPassage.id]; + }, []); + + if (passageIds.length > 0) { + dispatch({type: 'deletePassages', passageIds, storyId: story.id}); + } + }; +} diff --git a/src/store/stories/action-creators/update-passage.ts b/src/store/stories/action-creators/update-passage.ts index a7c32a950..165faa160 100644 --- a/src/store/stories/action-creators/update-passage.ts +++ b/src/store/stories/action-creators/update-passage.ts @@ -1,10 +1,12 @@ import escapeRegExp from 'lodash/escapeRegExp'; import {Thunk} from 'react-hook-thunk-reducer'; +import {storyWithId} from '../getters'; import {Passage, StoriesAction, StoriesState, Story} from '../stories.types'; import {createNewlyLinkedPassages} from './create-newly-linked-passages'; +import {deleteOrphanedPassages} from './delete-orphaned-passages'; export interface UpdatePassageOptions { - dontCreateNewlyLinkedPassages?: boolean; + dontUpdateOthers?: boolean; } /** @@ -44,8 +46,22 @@ export function updatePassage( // Side effects from changes. - if (!options.dontCreateNewlyLinkedPassages && props.text) { - dispatch(createNewlyLinkedPassages(story, passage, props.text, oldText)); + if (!options.dontUpdateOthers && props.text) { + dispatch(deleteOrphanedPassages(story, passage, props.text, oldText)); + + // We need to get an up-to-date version of the story so placement of new + // passages is correct. + // + // still causes passage bounces sometimes :( this is because the placement + // algorithm works differently based on the number of passages it sees. + // will anyone care?? could there be a 'suggested positions'? how would we + // communicate back and forth? + + const updatedStory = storyWithId(getState(), story.id); + + dispatch( + createNewlyLinkedPassages(updatedStory, passage, props.text, oldText) + ); } if (props.name) { diff --git a/src/util/__tests__/passage-is-empty.test.ts b/src/util/__tests__/passage-is-empty.test.ts new file mode 100644 index 000000000..ee07e9a90 --- /dev/null +++ b/src/util/__tests__/passage-is-empty.test.ts @@ -0,0 +1,46 @@ +import {random} from 'faker'; +import {Passage} from '../../store/stories'; +import {fakePassage} from '../../test-util'; +import {passageIsEmpty} from '../passage-is-empty'; + +describe('passageIsEmpty', () => { + let passage: Passage; + + beforeEach( + () => (passage = fakePassage({height: 100, tags: [], text: '', width: 100})) + ); + + it('returns false if the passage has any text', () => { + expect(passageIsEmpty({...passage, text: ' '})).toBe(false); + expect(passageIsEmpty({...passage, text: random.words(1)})).toBe(false); + }); + + it('returns false if the passage has any tags', () => { + expect(passageIsEmpty({...passage, tags: [random.words(1)]})).toBe(false); + expect( + passageIsEmpty({...passage, tags: [random.words(1), random.words(1)]}) + ).toBe(false); + }); + + it('returns false if the passage width is not 100', () => { + expect(passageIsEmpty({...passage, width: 50})).toBe(false); + expect(passageIsEmpty({...passage, width: 200})).toBe(false); + }); + + it('returns false if the passage height is not 100', () => { + expect(passageIsEmpty({...passage, height: 50})).toBe(false); + expect(passageIsEmpty({...passage, height: 200})).toBe(false); + }); + + it('returns true if the passage text is empty, has no tags, and dimensions are 100', () => + expect( + passageIsEmpty({ + ...passage, + height: 100, + name: random.words(5), + width: 100, + tags: [], + text: '' + }) + ).toBe(true)); +}); diff --git a/src/util/passage-is-empty.ts b/src/util/passage-is-empty.ts new file mode 100644 index 000000000..915d6d7b1 --- /dev/null +++ b/src/util/passage-is-empty.ts @@ -0,0 +1,14 @@ +import {Passage} from '../store/stories'; + +/** + * Returns whether a passage is considered empty, e.g. whether the user has put + * any content into it. + */ +export function passageIsEmpty(passage: Passage) { + return ( + passage.text === '' && + passage.tags.length === 0 && + passage.width === 100 && + passage.height === 100 + ); +} From 01a93834ff525b177d8ff451a77aaf3b203e0874 Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Mon, 25 Jul 2022 17:44:58 -0400 Subject: [PATCH 05/26] Add dialog width preference --- docs/en/src/preferences/index.md | 6 ++++ public/locales/en-US.json | 6 ++++ src/dialogs/__tests__/app-prefs.test.tsx | 28 +++++++++++++++++++ src/dialogs/app-prefs.tsx | 13 +++++++++ .../context/__tests__/dialogs.test.tsx | 28 +++++++++++++++---- src/dialogs/context/dialogs.css | 3 +- src/dialogs/context/dialogs.tsx | 5 +++- src/store/prefs/defaults.ts | 1 + src/store/prefs/prefs.types.ts | 4 +++ src/test-util/fakes.ts | 1 + 10 files changed, 87 insertions(+), 8 deletions(-) diff --git a/docs/en/src/preferences/index.md b/docs/en/src/preferences/index.md index 04f728f3b..9695de0b0 100644 --- a/docs/en/src/preferences/index.md +++ b/docs/en/src/preferences/index.md @@ -18,6 +18,12 @@ uses the theme that matches your system's theme setting, if Twine can determine it. If Twine can't determine whether your system is using a dark or light theme, it will default to a light theme. +## Changing Dialogs + +The _Dialog Width_ menu controls the width of dialogs. The placement of dialogs +onscreen (e.g. switching them from the right side of the window) cannot be +changed. + ## Changing Edit Dialogs The _Blinking Cursor in Editors_ checkbox controls whether the cursor blinks in diff --git a/public/locales/en-US.json b/public/locales/en-US.json index 7a8585dae..bed8462dd 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -155,6 +155,12 @@ "appPrefs": { "codeEditorFont": "Code Editor Font", "codeEditorFontScale": "Code Editor Font Size", + "dialogWidth": "Dialog Width", + "dialogWidths": { + "default": "Default", + "wider": "Wider", + "widest": "Widest" + }, "editorCursorBlinks": "Blinking Cursor in Editors", "fontExplanation": "Changing the font here only affects the Twine editor. It will not change the font a story uses when played.", "language": "Language", diff --git a/src/dialogs/__tests__/app-prefs.test.tsx b/src/dialogs/__tests__/app-prefs.test.tsx index 41dbb149b..320e9b2fc 100644 --- a/src/dialogs/__tests__/app-prefs.test.tsx +++ b/src/dialogs/__tests__/app-prefs.test.tsx @@ -20,6 +20,7 @@ describe('', () => { + @@ -65,6 +66,33 @@ describe('', () => { expect(screen.getByTestId('pref-inspector-locale')).toHaveTextContent('cs'); }); + it('displays the dialog width preference', () => { + renderComponent({dialogWidth: 600}); + expect(screen.getByLabelText('dialogs.appPrefs.dialogWidth')).toHaveValue( + '600' + ); + cleanup(); + renderComponent({dialogWidth: 700}); + expect(screen.getByLabelText('dialogs.appPrefs.dialogWidth')).toHaveValue( + '700' + ); + cleanup(); + renderComponent({dialogWidth: 800}); + expect(screen.getByLabelText('dialogs.appPrefs.dialogWidth')).toHaveValue( + '800' + ); + }); + + it('changes the dialog width preference when the menu is changed', () => { + renderComponent({dialogWidth: 700}); + fireEvent.change(screen.getByLabelText('dialogs.appPrefs.dialogWidth'), { + target: {value: '600'} + }); + expect(screen.getByTestId('pref-inspector-dialogWidth')).toHaveTextContent( + '600' + ); + }); + it('displays the editor cursor blink preference', () => { renderComponent({editorCursorBlinks: true}); expect( diff --git a/src/dialogs/app-prefs.tsx b/src/dialogs/app-prefs.tsx index 302648615..ecd1274ac 100644 --- a/src/dialogs/app-prefs.tsx +++ b/src/dialogs/app-prefs.tsx @@ -44,6 +44,19 @@ export const AppPrefsDialog: React.FC< > {t('dialogs.appPrefs.theme')} + + dispatch(setPref('dialogWidth', parseInt(e.target.value))) + } + options={[ + {label: t('dialogs.appPrefs.dialogWidths.default'), value: '600'}, + {label: t('dialogs.appPrefs.dialogWidths.wider'), value: '700'}, + {label: t('dialogs.appPrefs.dialogWidths.widest'), value: '800'} + ]} + value={prefs.dialogWidth.toString()} + > + {t('dialogs.appPrefs.dialogWidth')} + dispatch(setPref('editorCursorBlinks', value))} diff --git a/src/dialogs/context/__tests__/dialogs.test.tsx b/src/dialogs/context/__tests__/dialogs.test.tsx index a93381291..2f6699d3d 100644 --- a/src/dialogs/context/__tests__/dialogs.test.tsx +++ b/src/dialogs/context/__tests__/dialogs.test.tsx @@ -1,6 +1,8 @@ import {render, screen} from '@testing-library/react'; import {axe} from 'jest-axe'; import * as React from 'react'; +import {PrefsState} from '../../../store/prefs'; +import {FakeStateProvider} from '../../../test-util'; import {Dialogs} from '../dialogs'; import {DialogsContext, DialogsContextProps} from '../dialogs-context'; @@ -14,13 +16,18 @@ const MockComponent: React.FC<{collapsed?: boolean}> = ({ ); describe('', () => { - function renderComponent(context?: Partial) { + function renderComponent( + context?: Partial, + prefsContext?: Partial + ) { return render( - - - + + + + + ); } @@ -58,6 +65,15 @@ describe('', () => { expect(screen.getByTestId('mock-component').dataset.collapsed).toBe('true'); }); + it('renders at the width given by the dialogsWidth pref', () => { + const dialogWidth = Math.random() * 1000; + + renderComponent(undefined, {dialogWidth}); + expect( + document.querySelector('.dialogs')?.style.width + ).toBe(`${dialogWidth}px`); + }); + it('is accessible', async () => { const {container} = renderComponent(); diff --git a/src/dialogs/context/dialogs.css b/src/dialogs/context/dialogs.css index 3a9fac584..7dbb226fd 100644 --- a/src/dialogs/context/dialogs.css +++ b/src/dialogs/context/dialogs.css @@ -5,6 +5,8 @@ display: flex; flex-direction: column; justify-content: flex-end; + max-width: calc(100% - 2 * var(--grid-size)); + min-width: 600px; pointer-events: none; position: fixed; bottom: var(--grid-size); @@ -12,7 +14,6 @@ top: calc( 2 * var(--control-height) + var(--grid-size) ); /* below height */ - width: 40em; } .dialogs .dialog-card { diff --git a/src/dialogs/context/dialogs.tsx b/src/dialogs/context/dialogs.tsx index 4fd1faa6b..1eadf77c9 100644 --- a/src/dialogs/context/dialogs.tsx +++ b/src/dialogs/context/dialogs.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import {useScrollbarSize} from 'react-scrollbar-size'; import {CSSTransition, TransitionGroup} from 'react-transition-group'; import {useDialogsContext} from '.'; +import {usePrefsContext} from '../../store/prefs'; import './dialogs.css'; const DialogTransition: React.FC = props => ( @@ -12,11 +13,13 @@ const DialogTransition: React.FC = props => ( export const Dialogs: React.FC = () => { const {height, width} = useScrollbarSize(); + const {prefs} = usePrefsContext(); const {dispatch, dialogs} = useDialogsContext(); const style: React.CSSProperties = { marginBottom: height, - marginRight: width + marginRight: width, + width: `${prefs.dialogWidth}px` }; return ( diff --git a/src/store/prefs/defaults.ts b/src/store/prefs/defaults.ts index 1d936cc63..afc04235a 100644 --- a/src/store/prefs/defaults.ts +++ b/src/store/prefs/defaults.ts @@ -4,6 +4,7 @@ export const defaults = (): PrefsState => ({ appTheme: 'system', codeEditorFontFamily: 'var(--font-monospaced)', codeEditorFontScale: 1, + dialogWidth: 600, disabledStoryFormatEditorExtensions: [], donateShown: false, editorCursorBlinks: true, diff --git a/src/store/prefs/prefs.types.ts b/src/store/prefs/prefs.types.ts index 4889e8996..5260b1125 100644 --- a/src/store/prefs/prefs.types.ts +++ b/src/store/prefs/prefs.types.ts @@ -30,6 +30,10 @@ export interface PrefsState { * Font scale (1 being 100%) for the story JS and stylesheet editor. */ codeEditorFontScale: number; + /** + * Width of side dialogs in pixels. + */ + dialogWidth: number; /** * Story formats whose editor extensions should not be enabled. */ diff --git a/src/test-util/fakes.ts b/src/test-util/fakes.ts index 3c027f51b..eb64b43a6 100644 --- a/src/test-util/fakes.ts +++ b/src/test-util/fakes.ts @@ -119,6 +119,7 @@ export function fakePrefs(overrides?: Partial): PrefsState { appTheme: random.arrayElement(['light', 'dark', 'system']), codeEditorFontFamily: lorem.words(2), codeEditorFontScale: 0.8 + random.number(0.5), + dialogWidth: random.number(600), disabledStoryFormatEditorExtensions: [ {name: lorem.words(2), version: system.semver()} ], From 3712ef9aa785027cff3b277b0e95ad076b2d032e Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Mon, 1 Aug 2022 17:36:37 -0400 Subject: [PATCH 06/26] Update Electron and Electron Builder, ad hoc sign Mac app --- electron-builder.config.js | 18 +- package-lock.json | 807 +++++++++++++++++--------- package.json | 4 +- src/electron/main-process/init-app.ts | 4 - 4 files changed, 545 insertions(+), 288 deletions(-) diff --git a/electron-builder.config.js b/electron-builder.config.js index fa9f47271..47b460458 100644 --- a/electron-builder.config.js +++ b/electron-builder.config.js @@ -1,9 +1,25 @@ +const child_process = require('child_process'); const pkg = require('./package.json'); const isPreview = /alpha|beta|pre/.test(pkg.version) || process.env.FORCE_PREVIEW; module.exports = { + afterSign(context) { + // This step is necessary to ad hoc sign the app. Otherwise, on Apple + // Silicon you get repeated prompts for file access. + // + // If/when we are able to sign the app for real, this must be removed. + // + // This was cribbed from https://github.com/alacritty/alacritty/issues/5840. + + if (context.packager.platform.name === 'mac') { + console.log('Ad hoc signing Mac app...'); + child_process.execSync( + 'codesign --force --deep --sign - dist/electron/mac-universal/Twine.app' + ); + } + }, directories: { output: 'dist/electron' }, @@ -27,6 +43,6 @@ module.exports = { win: { artifactName: `Twine-${pkg.version}-Windows.exe`, icon: `icons/app-${isPreview ? 'preview' : 'release'}.ico`, - target: 'nsis' + target: {arch: ['x64'], target: 'nsis'} } }; diff --git a/package-lock.json b/package-lock.json index 687d8e2d3..cb0c527f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,8 +77,8 @@ "cpy": "^8.1.2", "cpy-cli": "^3.1.1", "cross-var": "^1.1.0", - "electron": "^17.4.10", - "electron-builder": "^23.0.2", + "electron": "^18.3.6", + "electron-builder": "^23.3.3", "faker": "^5.4.0", "history": "^5.1.0", "jest-axe": "^4.1.0", @@ -1720,9 +1720,9 @@ } }, "node_modules/@electron/universal": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.0.tgz", - "integrity": "sha512-eu20BwNsrMPKoe2bZ3/l9c78LclDvxg3PlVXrQf3L50NaUuW5M59gbPytI+V4z7/QMrohUHetQaU0ou+p1UG9Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.1.tgz", + "integrity": "sha512-7323HyMh7KBAl/nPDppdLsC87G6RwRU02dy5FPeGB1eS7rUePh55+WNWiDPLhFQqqVPHzh77M69uhmoT8XnwMQ==", "dev": true, "dependencies": { "@malept/cross-spawn-promise": "^1.1.0", @@ -1796,6 +1796,15 @@ "node": ">= 4" } }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@hapi/address": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", @@ -2151,6 +2160,15 @@ "node": ">=8" } }, + "node_modules/@jest/core/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/environment": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.6.2.tgz", @@ -2386,6 +2404,15 @@ "node": ">=8" } }, + "node_modules/@jest/reporters/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/source-map": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz", @@ -4071,9 +4098,9 @@ } }, "node_modules/@types/verror": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.5.tgz", - "integrity": "sha512-9UjMCHK5GPgQRoNbqdLIAvAy0EInuiqbW0PBMtVP6B5B2HQJlvoJHM+KodPZMEjOa5VkSc+5LH7xy+cUzQdmHw==", + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz", + "integrity": "sha512-NNm+gdePAX1VGvPcGZCDKQZKYSiAWigKhKaz5KF94hG6f2s8de9Ow5+7AbXoeKxL8gavZfk4UquSAygOF2duEQ==", "dev": true, "optional": true }, @@ -4791,35 +4818,36 @@ "dev": true }, "node_modules/app-builder-lib": { - "version": "23.0.2", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-23.0.2.tgz", - "integrity": "sha512-2ytlOKavGQVvVujsGajJURtyrXHRXWIqHTzzZKUtYNrJUbDG2HcPZN7aktf+SDBeoXX0Lp/QA6dBpBpSRuG6rQ==", + "version": "23.3.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-23.3.3.tgz", + "integrity": "sha512-m0+M53+HYMzqKxwNQZT143K7WwXEGUy9LY31l8dJphXx2P/FQod615mVbxHyqbDCG4J5bHdWm21qZ0e2DVY6CQ==", "dev": true, "dependencies": { "@develar/schema-utils": "~2.6.5", - "@electron/universal": "1.2.0", + "@electron/universal": "1.2.1", "@malept/flatpak-bundler": "^0.4.0", "7zip-bin": "~5.1.1", "async-exit-hook": "^2.0.1", "bluebird-lst": "^1.0.9", - "builder-util": "23.0.2", - "builder-util-runtime": "9.0.0", + "builder-util": "23.3.3", + "builder-util-runtime": "9.0.3", "chromium-pickle-js": "^0.2.0", - "debug": "^4.3.2", - "ejs": "^3.1.6", + "debug": "^4.3.4", + "ejs": "^3.1.7", "electron-osx-sign": "^0.6.0", - "electron-publish": "23.0.2", + "electron-publish": "23.3.3", "form-data": "^4.0.0", - "fs-extra": "^10.0.0", - "hosted-git-info": "^4.0.2", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", "is-ci": "^3.0.0", - "isbinaryfile": "^4.0.8", + "isbinaryfile": "^4.0.10", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "read-config-file": "6.2.0", "sanitize-filename": "^1.6.3", - "semver": "^7.3.5", + "semver": "^7.3.7", + "tar": "^6.1.11", "temp-file": "^3.4.0" }, "engines": { @@ -4833,18 +4861,18 @@ "dev": true }, "node_modules/app-builder-lib/node_modules/ci-info": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz", - "integrity": "sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", + "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", "dev": true }, "node_modules/app-builder-lib/node_modules/ejs": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.6.tgz", - "integrity": "sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", + "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", "dev": true, "dependencies": { - "jake": "^10.6.1" + "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" @@ -4867,6 +4895,20 @@ "node": ">= 6" } }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/app-builder-lib/node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -4903,6 +4945,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/app-builder-lib/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -5067,9 +5121,9 @@ "dev": true }, "node_modules/asar": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/asar/-/asar-3.1.0.tgz", - "integrity": "sha512-vyxPxP5arcAqN4F/ebHd/HhwnAiZtwhglvdmc7BR2f0ywbVNTOpSeyhLDbGXtE/y58hv1oC75TaNIXutnsOZsQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/asar/-/asar-3.2.0.tgz", + "integrity": "sha512-COdw2ZQvKdFGFxXwX3oYh2/sOsJWJegrdJCGxnN4MZ7IULgRBp9P6665aqj9z1v9VwP4oP1hRBojRDQ//IGgAg==", "dev": true, "dependencies": { "chromium-pickle-js": "^0.2.0", @@ -7464,7 +7518,7 @@ "node_modules/buffer-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", - "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", + "integrity": "sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ==", "dev": true, "engines": { "node": ">=0.4.0" @@ -7473,7 +7527,7 @@ "node_modules/buffer-fill": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", "dev": true }, "node_modules/buffer-from": { @@ -7501,9 +7555,9 @@ "dev": true }, "node_modules/builder-util": { - "version": "23.0.2", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-23.0.2.tgz", - "integrity": "sha512-HaNHL3axNW/Ms8O1mDx3I07G+ZnZ/TKSWWvorOAPau128cdt9S+lNx5ocbx8deSaHHX4WFXSZVHh3mxlaKJNgg==", + "version": "23.3.3", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-23.3.3.tgz", + "integrity": "sha512-MJZlUiq2PY5hjYv9+XNaoYdsITqvLgRDoHSFg/4nzpInbNxNjLQOolL04Zsyp+hgfcbFvMC4h0KkR1CMPHLWbA==", "dev": true, "dependencies": { "@types/debug": "^4.1.6", @@ -7511,10 +7565,10 @@ "7zip-bin": "~5.1.1", "app-builder-bin": "4.0.0", "bluebird-lst": "^1.0.9", - "builder-util-runtime": "9.0.0", + "builder-util-runtime": "9.0.3", "chalk": "^4.1.1", "cross-spawn": "^7.0.3", - "debug": "^4.3.2", + "debug": "^4.3.4", "fs-extra": "^10.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", @@ -7526,12 +7580,12 @@ } }, "node_modules/builder-util-runtime": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.0.0.tgz", - "integrity": "sha512-SkpEtSmTkREDHRJnxKEv43aAYp8sYWY8fxYBhGLBLOBIRXeaIp6Kv3lBgSD7uR8jQtC7CA659sqJrpSV6zNvSA==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.0.3.tgz", + "integrity": "sha512-SfG2wnyjpUbbdtpnqDpWwklujofC6GarGpvdWrEkg9p5AD/xJmTF2buTNaqs3qtsNBEVQDDjZz9xc2GGpVyMfA==", "dev": true, "dependencies": { - "debug": "^4.3.2", + "debug": "^4.3.4", "sax": "^1.2.4" }, "engines": { @@ -7576,9 +7630,9 @@ } }, "node_modules/builder-util/node_modules/ci-info": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz", - "integrity": "sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", + "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", "dev": true }, "node_modules/builder-util/node_modules/color-convert": { @@ -8080,7 +8134,7 @@ "node_modules/chromium-pickle-js": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha1-BKEGZywYsIWrd02YPfo+oTjyIgU=", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", "dev": true }, "node_modules/ci-info": { @@ -8367,7 +8421,7 @@ "node_modules/colors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", "dev": true, "engines": { "node": ">=0.1.90" @@ -8412,7 +8466,7 @@ "node_modules/compare-version": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha1-AWLsLZNR9d3VmpICy6k1NmpyUIA=", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", "dev": true, "engines": { "node": ">=0.10.0" @@ -9790,9 +9844,9 @@ } }, "node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -10211,7 +10265,7 @@ "node_modules/dir-compare/node_modules/commander": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "integrity": "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==", "dev": true, "dependencies": { "graceful-readlink": ">= 1.0.0" @@ -10233,20 +10287,20 @@ } }, "node_modules/dmg-builder": { - "version": "23.0.2", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-23.0.2.tgz", - "integrity": "sha512-kfJZRKbIN6kM/Vuzrme8SGSA+M/F0VvNrSGa6idWXbqtxIbGZZMF1QxVrXJbxSayf0Jh4hPy6NUNZAfbX9/m3g==", + "version": "23.3.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-23.3.3.tgz", + "integrity": "sha512-ECwAjt+ZWyOvddrkDx1xRD6IVUCZb5SV6vSMHZd+Va3G2sUXHrnglR1cGDKRF4oYRQm8SYVrpLZKbi8npyDcAQ==", "dev": true, "dependencies": { - "app-builder-lib": "23.0.2", - "builder-util": "23.0.2", - "builder-util-runtime": "9.0.0", + "app-builder-lib": "23.3.3", + "builder-util": "23.3.3", + "builder-util-runtime": "9.0.3", "fs-extra": "^10.0.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { - "dmg-license": "^1.0.9" + "dmg-license": "^1.0.11" } }, "node_modules/dmg-builder/node_modules/argparse": { @@ -10283,7 +10337,6 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", - "deprecated": "Disk image license agreements are deprecated by Apple and will probably be removed in a future macOS release. Discussion at: https://github.com/argv-minus-one/dmg-license/issues/11", "dev": true, "optional": true, "os": [ @@ -10574,14 +10627,14 @@ } }, "node_modules/electron": { - "version": "17.4.10", - "resolved": "https://registry.npmjs.org/electron/-/electron-17.4.10.tgz", - "integrity": "sha512-4v5Xwa4rZjWf0LmpYOaBXG8ZQ1rpPEpww+MCe4uuwenFsx3QSLSXmek720EY7drQa/O1YyvcZ1pr2sDBMIq0mA==", + "version": "18.3.6", + "resolved": "https://registry.npmjs.org/electron/-/electron-18.3.6.tgz", + "integrity": "sha512-o1cArbCDkRKOJRKk+UXiBv7/wVa0jA0k0U3LGfMcYaq+M3m20EQ5hUxC5xCHxyLYSdkriAjEp3VDaGP9EwjBsQ==", "dev": true, "hasInstallScript": true, "dependencies": { "@electron/get": "^1.13.0", - "@types/node": "^14.6.2", + "@types/node": "^16.11.26", "extract-zip": "^1.0.3" }, "bin": { @@ -10592,17 +10645,17 @@ } }, "node_modules/electron-builder": { - "version": "23.0.2", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-23.0.2.tgz", - "integrity": "sha512-NG8ywuoHZpq6uk/2fEo9XVKBnjyGwNCnCyPxgGLdEk6xLAXr6nkF54+kqdhrDw4E8alwxc/TPHxUY3G0B8k/Dw==", + "version": "23.3.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-23.3.3.tgz", + "integrity": "sha512-mFYYdhoFPKevP6y5uaaF3dusmB2OtQ/HnwwpyOePeU7QDS0SEIAUokQsHUanAiJAZcBqtY7iyLBgX18QybdFFw==", "dev": true, "dependencies": { "@types/yargs": "^17.0.1", - "app-builder-lib": "23.0.2", - "builder-util": "23.0.2", - "builder-util-runtime": "9.0.0", + "app-builder-lib": "23.3.3", + "builder-util": "23.3.3", + "builder-util-runtime": "9.0.3", "chalk": "^4.1.1", - "dmg-builder": "23.0.2", + "dmg-builder": "23.3.3", "fs-extra": "^10.0.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", @@ -10824,18 +10877,18 @@ "node_modules/electron-osx-sign/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, "node_modules/electron-publish": { - "version": "23.0.2", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-23.0.2.tgz", - "integrity": "sha512-8gMYgWqv96lc83FCm85wd+tEyxNTJQK7WKyPkNkO8GxModZqt1GO8S+/vAnFGxilS/7vsrVRXFfqiCDUCSuxEg==", + "version": "23.3.3", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-23.3.3.tgz", + "integrity": "sha512-1dX17eE5xVXedTxjC+gjsP74oC0+sIHgqysp0ryTlF9+yfQUyXjBk6kcK+zhtBA2SsHMSglDtM+JPxDD/WpPTQ==", "dev": true, "dependencies": { "@types/fs-extra": "^9.0.11", - "builder-util": "23.0.2", - "builder-util-runtime": "9.0.0", + "builder-util": "23.3.3", + "builder-util-runtime": "9.0.3", "chalk": "^4.1.1", "fs-extra": "^10.0.0", "lazy-val": "^1.0.5", @@ -10931,9 +10984,9 @@ "dev": true }, "node_modules/electron/node_modules/@types/node": { - "version": "14.18.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.21.tgz", - "integrity": "sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q==", + "version": "16.11.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", + "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", "dev": true }, "node_modules/element-closest": { @@ -12069,6 +12122,15 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/eslint/node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -13598,7 +13660,7 @@ "node_modules/graceful-readlink": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==", "dev": true }, "node_modules/growly": { @@ -14362,9 +14424,9 @@ "dev": true }, "node_modules/https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "dependencies": { "agent-base": "6", @@ -15264,9 +15326,9 @@ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, "node_modules/isbinaryfile": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.8.tgz", - "integrity": "sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", "dev": true, "engines": { "node": ">= 8.0.0" @@ -15425,13 +15487,13 @@ } }, "node_modules/jake": { - "version": "10.8.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz", - "integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==", + "version": "10.8.5", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", "dev": true, "dependencies": { - "async": "0.9.x", - "chalk": "^2.4.2", + "async": "^3.2.3", + "chalk": "^4.0.2", "filelist": "^1.0.1", "minimatch": "^3.0.4" }, @@ -15439,15 +15501,85 @@ "jake": "bin/cli.js" }, "engines": { - "node": "*" + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jake/node_modules/async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", "dev": true }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jest": { "version": "26.6.0", "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.0.tgz", @@ -15957,6 +16089,15 @@ "node": ">=8" } }, + "node_modules/jest-config/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-diff": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", @@ -16711,6 +16852,15 @@ "node": ">=8" } }, + "node_modules/jest-resolve/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-runner": { "version": "26.6.3", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-26.6.3.tgz", @@ -16878,6 +17028,15 @@ "node": ">=8" } }, + "node_modules/jest-runner/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-runtime": { "version": "26.6.3", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.6.3.tgz", @@ -17058,6 +17217,15 @@ "node": ">=8" } }, + "node_modules/jest-runtime/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-serializer": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", @@ -17234,6 +17402,15 @@ "node": ">=8" } }, + "node_modules/jest-snapshot/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-useragent-mock": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jest-useragent-mock/-/jest-useragent-mock-0.1.1.tgz", @@ -18529,18 +18706,6 @@ "node": ">=8" } }, - "node_modules/meow/node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -20410,27 +20575,18 @@ } }, "node_modules/plist": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.4.tgz", - "integrity": "sha512-ksrr8y9+nXOxQB2osVNqrgvX/XQPOXaU4BQMKjYq8PvaY1U18mo+fKgBSwzK+luSyinOuPae956lSVcBwxlAMg==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz", + "integrity": "sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA==", "dev": true, "dependencies": { "base64-js": "^1.5.1", - "xmlbuilder": "^9.0.7" + "xmlbuilder": "^15.1.1" }, "engines": { "node": ">=6" } }, - "node_modules/plist/node_modules/xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/pnp-webpack-plugin": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz", @@ -24102,9 +24258,9 @@ } }, "node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -24204,19 +24360,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/serialize-error/node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/serialize-javascript": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", @@ -25709,9 +25852,9 @@ } }, "node_modules/tar": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", - "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", "dev": true, "dependencies": { "chownr": "^2.0.0", @@ -26282,7 +26425,7 @@ "node_modules/truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", "dev": true, "dependencies": { "utf8-byte-length": "^1.0.1" @@ -26415,12 +26558,15 @@ } }, "node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "dev": true, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/type-is": { @@ -26920,7 +27066,7 @@ "node_modules/utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", - "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=", + "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==", "dev": true }, "node_modules/util": { @@ -29027,7 +29173,6 @@ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", "dev": true, - "optional": true, "engines": { "node": ">=8.0" } @@ -30466,9 +30611,9 @@ } }, "@electron/universal": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.0.tgz", - "integrity": "sha512-eu20BwNsrMPKoe2bZ3/l9c78LclDvxg3PlVXrQf3L50NaUuW5M59gbPytI+V4z7/QMrohUHetQaU0ou+p1UG9Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.2.1.tgz", + "integrity": "sha512-7323HyMh7KBAl/nPDppdLsC87G6RwRU02dy5FPeGB1eS7rUePh55+WNWiDPLhFQqqVPHzh77M69uhmoT8XnwMQ==", "dev": true, "requires": { "@malept/cross-spawn-promise": "^1.1.0", @@ -30525,6 +30670,12 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -30808,6 +30959,12 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -30998,6 +31155,12 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -32332,9 +32495,9 @@ } }, "@types/verror": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.5.tgz", - "integrity": "sha512-9UjMCHK5GPgQRoNbqdLIAvAy0EInuiqbW0PBMtVP6B5B2HQJlvoJHM+KodPZMEjOa5VkSc+5LH7xy+cUzQdmHw==", + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz", + "integrity": "sha512-NNm+gdePAX1VGvPcGZCDKQZKYSiAWigKhKaz5KF94hG6f2s8de9Ow5+7AbXoeKxL8gavZfk4UquSAygOF2duEQ==", "dev": true, "optional": true }, @@ -32904,35 +33067,36 @@ "dev": true }, "app-builder-lib": { - "version": "23.0.2", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-23.0.2.tgz", - "integrity": "sha512-2ytlOKavGQVvVujsGajJURtyrXHRXWIqHTzzZKUtYNrJUbDG2HcPZN7aktf+SDBeoXX0Lp/QA6dBpBpSRuG6rQ==", + "version": "23.3.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-23.3.3.tgz", + "integrity": "sha512-m0+M53+HYMzqKxwNQZT143K7WwXEGUy9LY31l8dJphXx2P/FQod615mVbxHyqbDCG4J5bHdWm21qZ0e2DVY6CQ==", "dev": true, "requires": { "@develar/schema-utils": "~2.6.5", - "@electron/universal": "1.2.0", + "@electron/universal": "1.2.1", "@malept/flatpak-bundler": "^0.4.0", "7zip-bin": "~5.1.1", "async-exit-hook": "^2.0.1", "bluebird-lst": "^1.0.9", - "builder-util": "23.0.2", - "builder-util-runtime": "9.0.0", + "builder-util": "23.3.3", + "builder-util-runtime": "9.0.3", "chromium-pickle-js": "^0.2.0", - "debug": "^4.3.2", - "ejs": "^3.1.6", + "debug": "^4.3.4", + "ejs": "^3.1.7", "electron-osx-sign": "^0.6.0", - "electron-publish": "23.0.2", + "electron-publish": "23.3.3", "form-data": "^4.0.0", - "fs-extra": "^10.0.0", - "hosted-git-info": "^4.0.2", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", "is-ci": "^3.0.0", - "isbinaryfile": "^4.0.8", + "isbinaryfile": "^4.0.10", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "read-config-file": "6.2.0", "sanitize-filename": "^1.6.3", - "semver": "^7.3.5", + "semver": "^7.3.7", + "tar": "^6.1.11", "temp-file": "^3.4.0" }, "dependencies": { @@ -32943,18 +33107,18 @@ "dev": true }, "ci-info": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz", - "integrity": "sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", + "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", "dev": true }, "ejs": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.6.tgz", - "integrity": "sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.8.tgz", + "integrity": "sha512-/sXZeMlhS0ArkfX2Aw780gJzXSMPnKjtspYZv+f3NiKLlubezAHDU5+9xz6gd3/NhG3txQCo6xlglmTS+oTGEQ==", "dev": true, "requires": { - "jake": "^10.6.1" + "jake": "^10.8.5" } }, "form-data": { @@ -32968,6 +33132,17 @@ "mime-types": "^2.1.12" } }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, "hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -32994,6 +33169,15 @@ "requires": { "argparse": "^2.0.1" } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } } } }, @@ -33119,9 +33303,9 @@ "dev": true }, "asar": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/asar/-/asar-3.1.0.tgz", - "integrity": "sha512-vyxPxP5arcAqN4F/ebHd/HhwnAiZtwhglvdmc7BR2f0ywbVNTOpSeyhLDbGXtE/y58hv1oC75TaNIXutnsOZsQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/asar/-/asar-3.2.0.tgz", + "integrity": "sha512-COdw2ZQvKdFGFxXwX3oYh2/sOsJWJegrdJCGxnN4MZ7IULgRBp9P6665aqj9z1v9VwP4oP1hRBojRDQ//IGgAg==", "dev": true, "requires": { "@types/glob": "^7.1.1", @@ -35248,13 +35432,13 @@ "buffer-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", - "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", + "integrity": "sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ==", "dev": true }, "buffer-fill": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", "dev": true }, "buffer-from": { @@ -35276,9 +35460,9 @@ "dev": true }, "builder-util": { - "version": "23.0.2", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-23.0.2.tgz", - "integrity": "sha512-HaNHL3axNW/Ms8O1mDx3I07G+ZnZ/TKSWWvorOAPau128cdt9S+lNx5ocbx8deSaHHX4WFXSZVHh3mxlaKJNgg==", + "version": "23.3.3", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-23.3.3.tgz", + "integrity": "sha512-MJZlUiq2PY5hjYv9+XNaoYdsITqvLgRDoHSFg/4nzpInbNxNjLQOolL04Zsyp+hgfcbFvMC4h0KkR1CMPHLWbA==", "dev": true, "requires": { "@types/debug": "^4.1.6", @@ -35286,10 +35470,10 @@ "7zip-bin": "~5.1.1", "app-builder-bin": "4.0.0", "bluebird-lst": "^1.0.9", - "builder-util-runtime": "9.0.0", + "builder-util-runtime": "9.0.3", "chalk": "^4.1.1", "cross-spawn": "^7.0.3", - "debug": "^4.3.2", + "debug": "^4.3.4", "fs-extra": "^10.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", @@ -35326,9 +35510,9 @@ } }, "ci-info": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz", - "integrity": "sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", + "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", "dev": true }, "color-convert": { @@ -35423,12 +35607,12 @@ } }, "builder-util-runtime": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.0.0.tgz", - "integrity": "sha512-SkpEtSmTkREDHRJnxKEv43aAYp8sYWY8fxYBhGLBLOBIRXeaIp6Kv3lBgSD7uR8jQtC7CA659sqJrpSV6zNvSA==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.0.3.tgz", + "integrity": "sha512-SfG2wnyjpUbbdtpnqDpWwklujofC6GarGpvdWrEkg9p5AD/xJmTF2buTNaqs3qtsNBEVQDDjZz9xc2GGpVyMfA==", "dev": true, "requires": { - "debug": "^4.3.2", + "debug": "^4.3.4", "sax": "^1.2.4" } }, @@ -35726,7 +35910,7 @@ "chromium-pickle-js": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha1-BKEGZywYsIWrd02YPfo+oTjyIgU=", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", "dev": true }, "ci-info": { @@ -35967,7 +36151,7 @@ "colors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", "dev": true }, "combined-stream": { @@ -36000,7 +36184,7 @@ "compare-version": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha1-AWLsLZNR9d3VmpICy6k1NmpyUIA=", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", "dev": true }, "component-emitter": { @@ -37126,9 +37310,9 @@ } }, "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -37466,7 +37650,7 @@ "commander": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "integrity": "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==", "dev": true, "requires": { "graceful-readlink": ">= 1.0.0" @@ -37484,15 +37668,15 @@ } }, "dmg-builder": { - "version": "23.0.2", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-23.0.2.tgz", - "integrity": "sha512-kfJZRKbIN6kM/Vuzrme8SGSA+M/F0VvNrSGa6idWXbqtxIbGZZMF1QxVrXJbxSayf0Jh4hPy6NUNZAfbX9/m3g==", + "version": "23.3.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-23.3.3.tgz", + "integrity": "sha512-ECwAjt+ZWyOvddrkDx1xRD6IVUCZb5SV6vSMHZd+Va3G2sUXHrnglR1cGDKRF4oYRQm8SYVrpLZKbi8npyDcAQ==", "dev": true, "requires": { - "app-builder-lib": "23.0.2", - "builder-util": "23.0.2", - "builder-util-runtime": "9.0.0", - "dmg-license": "^1.0.9", + "app-builder-lib": "23.3.3", + "builder-util": "23.3.3", + "builder-util-runtime": "9.0.3", + "dmg-license": "^1.0.11", "fs-extra": "^10.0.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" @@ -37788,36 +37972,36 @@ "dev": true }, "electron": { - "version": "17.4.10", - "resolved": "https://registry.npmjs.org/electron/-/electron-17.4.10.tgz", - "integrity": "sha512-4v5Xwa4rZjWf0LmpYOaBXG8ZQ1rpPEpww+MCe4uuwenFsx3QSLSXmek720EY7drQa/O1YyvcZ1pr2sDBMIq0mA==", + "version": "18.3.6", + "resolved": "https://registry.npmjs.org/electron/-/electron-18.3.6.tgz", + "integrity": "sha512-o1cArbCDkRKOJRKk+UXiBv7/wVa0jA0k0U3LGfMcYaq+M3m20EQ5hUxC5xCHxyLYSdkriAjEp3VDaGP9EwjBsQ==", "dev": true, "requires": { "@electron/get": "^1.13.0", - "@types/node": "^14.6.2", + "@types/node": "^16.11.26", "extract-zip": "^1.0.3" }, "dependencies": { "@types/node": { - "version": "14.18.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.21.tgz", - "integrity": "sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q==", + "version": "16.11.47", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.47.tgz", + "integrity": "sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g==", "dev": true } } }, "electron-builder": { - "version": "23.0.2", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-23.0.2.tgz", - "integrity": "sha512-NG8ywuoHZpq6uk/2fEo9XVKBnjyGwNCnCyPxgGLdEk6xLAXr6nkF54+kqdhrDw4E8alwxc/TPHxUY3G0B8k/Dw==", + "version": "23.3.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-23.3.3.tgz", + "integrity": "sha512-mFYYdhoFPKevP6y5uaaF3dusmB2OtQ/HnwwpyOePeU7QDS0SEIAUokQsHUanAiJAZcBqtY7iyLBgX18QybdFFw==", "dev": true, "requires": { "@types/yargs": "^17.0.1", - "app-builder-lib": "23.0.2", - "builder-util": "23.0.2", - "builder-util-runtime": "9.0.0", + "app-builder-lib": "23.3.3", + "builder-util": "23.3.3", + "builder-util-runtime": "9.0.3", "chalk": "^4.1.1", - "dmg-builder": "23.0.2", + "dmg-builder": "23.3.3", "fs-extra": "^10.0.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", @@ -37985,20 +38169,20 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true } } }, "electron-publish": { - "version": "23.0.2", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-23.0.2.tgz", - "integrity": "sha512-8gMYgWqv96lc83FCm85wd+tEyxNTJQK7WKyPkNkO8GxModZqt1GO8S+/vAnFGxilS/7vsrVRXFfqiCDUCSuxEg==", + "version": "23.3.3", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-23.3.3.tgz", + "integrity": "sha512-1dX17eE5xVXedTxjC+gjsP74oC0+sIHgqysp0ryTlF9+yfQUyXjBk6kcK+zhtBA2SsHMSglDtM+JPxDD/WpPTQ==", "dev": true, "requires": { "@types/fs-extra": "^9.0.11", - "builder-util": "23.0.2", - "builder-util-runtime": "9.0.0", + "builder-util": "23.3.3", + "builder-util-runtime": "9.0.3", "chalk": "^4.1.1", "fs-extra": "^10.0.0", "lazy-val": "^1.0.5", @@ -38545,6 +38729,12 @@ "has-flag": "^4.0.0" } }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -40190,7 +40380,7 @@ "graceful-readlink": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "integrity": "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==", "dev": true }, "growly": { @@ -40835,9 +41025,9 @@ "dev": true }, "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "requires": { "agent-base": "6", @@ -41487,9 +41677,9 @@ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, "isbinaryfile": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.8.tgz", - "integrity": "sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w==", + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", "dev": true }, "isexe": { @@ -41609,22 +41799,71 @@ } }, "jake": { - "version": "10.8.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz", - "integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==", + "version": "10.8.5", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", "dev": true, "requires": { - "async": "0.9.x", - "chalk": "^2.4.2", + "async": "^3.2.3", + "chalk": "^4.0.2", "filelist": "^1.0.1", "minimatch": "^3.0.4" }, "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } } } }, @@ -42076,6 +42315,12 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -42646,6 +42891,12 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -42789,6 +43040,12 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -42928,6 +43185,12 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -43068,6 +43331,12 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -44036,12 +44305,6 @@ "dev": true } } - }, - "type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true } } }, @@ -45543,21 +45806,13 @@ } }, "plist": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.4.tgz", - "integrity": "sha512-ksrr8y9+nXOxQB2osVNqrgvX/XQPOXaU4BQMKjYq8PvaY1U18mo+fKgBSwzK+luSyinOuPae956lSVcBwxlAMg==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.6.tgz", + "integrity": "sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA==", "dev": true, "requires": { "base64-js": "^1.5.1", - "xmlbuilder": "^9.0.7" - }, - "dependencies": { - "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", - "dev": true - } + "xmlbuilder": "^15.1.1" } }, "pnp-webpack-plugin": { @@ -48541,9 +48796,9 @@ } }, "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "requires": { "lru-cache": "^6.0.0" } @@ -48626,15 +48881,6 @@ "optional": true, "requires": { "type-fest": "^0.13.1" - }, - "dependencies": { - "type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "optional": true - } } }, "serialize-javascript": { @@ -49862,9 +50108,9 @@ "dev": true }, "tar": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", - "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", "dev": true, "requires": { "chownr": "^2.0.0", @@ -50311,7 +50557,7 @@ "truncate-utf8-bytes": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", "dev": true, "requires": { "utf8-byte-length": "^1.0.1" @@ -50417,9 +50663,9 @@ "dev": true }, "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "dev": true }, "type-is": { @@ -50796,7 +51042,7 @@ "utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", - "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=", + "integrity": "sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==", "dev": true }, "util": { @@ -52557,8 +52803,7 @@ "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, - "optional": true + "dev": true }, "xmlchars": { "version": "2.2.0", diff --git a/package.json b/package.json index 5a4c5b791..f6b04d3cb 100644 --- a/package.json +++ b/package.json @@ -82,8 +82,8 @@ "cpy": "^8.1.2", "cpy-cli": "^3.1.1", "cross-var": "^1.1.0", - "electron": "^17.4.10", - "electron-builder": "^23.0.2", + "electron": "^18.3.6", + "electron-builder": "^23.3.3", "faker": "^5.4.0", "history": "^5.1.0", "jest-axe": "^4.1.0", diff --git a/src/electron/main-process/init-app.ts b/src/electron/main-process/init-app.ts index 55f817d61..3247b1cd7 100644 --- a/src/electron/main-process/init-app.ts +++ b/src/electron/main-process/init-app.ts @@ -17,10 +17,6 @@ async function createWindow() { webPreferences: { // See preload.ts for why context isolation is disabled. contextIsolation: false, - // Seems needed to prevent opening a window from blocking the UI. We force - // them to open outside the app anyway. - // See https://github.com/electron/electron/issues/29509 - nativeWindowOpen: true, nodeIntegration: false, preload: path.resolve(__dirname, './preload.js') } From 51615abc729c3124f542a309606f27dcaf1a4804 Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Sun, 31 Jul 2022 22:41:31 -0400 Subject: [PATCH 07/26] Add dialog maximize option --- package-lock.json | 192 +++++++++++++----- public/locales/en-US.json | 2 + .../__tests__/dialog-card.test.tsx | 63 +++++- .../container/dialog-card/dialog-card.tsx | 36 +++- .../__tests__/story-javascript.test.tsx | 5 + .../__tests__/story-stylesheet.test.tsx | 5 + .../context/__tests__/dialogs.test.tsx | 37 +++- src/dialogs/context/__tests__/reducer.test.ts | 171 +++++++++++++++- src/dialogs/context/dialogs.css | 31 ++- src/dialogs/context/dialogs.tsx | 25 ++- src/dialogs/context/reducer.ts | 16 +- src/dialogs/dialogs.types.ts | 18 +- .../__tests__/passage-edit.test.tsx | 6 + src/dialogs/passage-edit/passage-edit.tsx | 1 + src/dialogs/story-javascript.tsx | 1 + src/dialogs/story-stylesheet.tsx | 1 + 16 files changed, 533 insertions(+), 77 deletions(-) diff --git a/package-lock.json b/package-lock.json index 687d8e2d3..2ca61a0ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1796,6 +1796,15 @@ "node": ">= 4" } }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@hapi/address": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", @@ -2151,6 +2160,15 @@ "node": ">=8" } }, + "node_modules/@jest/core/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/environment": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.6.2.tgz", @@ -2386,6 +2404,15 @@ "node": ">=8" } }, + "node_modules/@jest/reporters/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/source-map": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz", @@ -12069,6 +12096,15 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/eslint/node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -15957,6 +15993,15 @@ "node": ">=8" } }, + "node_modules/jest-config/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-diff": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", @@ -16711,6 +16756,15 @@ "node": ">=8" } }, + "node_modules/jest-resolve/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-runner": { "version": "26.6.3", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-26.6.3.tgz", @@ -16878,6 +16932,15 @@ "node": ">=8" } }, + "node_modules/jest-runner/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-runtime": { "version": "26.6.3", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.6.3.tgz", @@ -17058,6 +17121,15 @@ "node": ">=8" } }, + "node_modules/jest-runtime/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-serializer": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", @@ -17234,6 +17306,15 @@ "node": ">=8" } }, + "node_modules/jest-snapshot/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-useragent-mock": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jest-useragent-mock/-/jest-useragent-mock-0.1.1.tgz", @@ -18529,18 +18610,6 @@ "node": ">=8" } }, - "node_modules/meow/node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -24204,19 +24273,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/serialize-error/node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/serialize-javascript": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", @@ -26415,12 +26471,15 @@ } }, "node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "dev": true, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/type-is": { @@ -30525,6 +30584,12 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -30808,6 +30873,12 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -30998,6 +31069,12 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -38545,6 +38622,12 @@ "has-flag": "^4.0.0" } }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -42076,6 +42159,12 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -42646,6 +42735,12 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -42789,6 +42884,12 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -42928,6 +43029,12 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -43068,6 +43175,12 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -44036,12 +44149,6 @@ "dev": true } } - }, - "type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true } } }, @@ -48626,15 +48733,6 @@ "optional": true, "requires": { "type-fest": "^0.13.1" - }, - "dependencies": { - "type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "optional": true - } } }, "serialize-javascript": { @@ -50417,9 +50515,9 @@ "dev": true }, "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "dev": true }, "type-is": { diff --git a/public/locales/en-US.json b/public/locales/en-US.json index bed8462dd..0f8c47063 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -26,6 +26,7 @@ "editCount": "Edit ({{count}})", "help": "Help", "import": "Import", + "maximize": "Maximize", "more": "More", "new": "New", "next": "Next", @@ -48,6 +49,7 @@ "twine": "Twine", "undo": "Undo", "undoChange": "Undo {{change}}", + "unmaximize": "Restore Size", "view": "View" }, "components": { diff --git a/src/components/container/dialog-card/__tests__/dialog-card.test.tsx b/src/components/container/dialog-card/__tests__/dialog-card.test.tsx index c1ae43de5..f9f44c4d1 100644 --- a/src/components/container/dialog-card/__tests__/dialog-card.test.tsx +++ b/src/components/container/dialog-card/__tests__/dialog-card.test.tsx @@ -9,7 +9,10 @@ describe('', () => { @@ -18,13 +21,68 @@ describe('', () => { ); } - it('calls the onChangeCollapsed prop when the header button is clicked', () => { + it('calls the onChangeCollapsed prop when the header button is clicked when uncollapsed', () => { const onChangeCollapsed = jest.fn(); renderComponent({onChangeCollapsed, headerLabel: 'test-label'}); expect(onChangeCollapsed).not.toHaveBeenCalled(); fireEvent.click(screen.getByText('test-label')); - expect(onChangeCollapsed).toHaveBeenCalledTimes(1); + expect(onChangeCollapsed.mock.calls).toEqual([[true]]); + }); + + it('calls the onChangeCollapsed prop when the header button is clicked when collapsed', () => { + const onChangeCollapsed = jest.fn(); + + renderComponent({ + collapsed: true, + onChangeCollapsed, + headerLabel: 'test-label' + }); + expect(onChangeCollapsed).not.toHaveBeenCalled(); + fireEvent.click(screen.getByText('test-label')); + expect(onChangeCollapsed.mock.calls).toEqual([[false]]); + }); + + it('shows a maximize button when the maximized prop is true', () => { + renderComponent({maximizable: true}); + expect(screen.getByLabelText('common.maximize')).toBeInTheDocument(); + }); + + it('hides the maximize button when the maximized prop is false', () => { + renderComponent({maximizable: false}); + expect(screen.queryByLabelText('common.maximize')).not.toBeInTheDocument(); + }); + + it('adds a CSS class when maximized', () => { + renderComponent({maximized: true}); + expect( + document.querySelector('.dialog-card')?.classList.contains('maximized') + ).toBe(true); + }); + + it("doesn't add a CSS class when unmaximized", () => { + renderComponent({maximized: false}); + expect( + document.querySelector('.dialog-card')?.classList.contains('maximized') + ).toBe(false); + }); + + it('calls the onChangeMaximized prop with true when the maximize button is clicked', () => { + const onChangeMaximized = jest.fn(); + + renderComponent({onChangeMaximized}); + expect(onChangeMaximized).not.toHaveBeenCalled(); + fireEvent.click(screen.getByLabelText('common.maximize')); + expect(onChangeMaximized.mock.calls).toEqual([[true]]); + }); + + it('calls the onChangeMaximized prop with false when the unmaximize button is clicked', () => { + const onChangeMaximized = jest.fn(); + + renderComponent({maximized: true, onChangeMaximized}); + expect(onChangeMaximized).not.toHaveBeenCalled(); + fireEvent.click(screen.getByLabelText('common.unmaximize')); + expect(onChangeMaximized.mock.calls).toEqual([[false]]); }); it('calls the onClose prop when the close button is clicked', () => { @@ -74,6 +132,7 @@ describe('', () => { collapsed={false} headerLabel="mock-header-label" onChangeCollapsed={jest.fn()} + onChangeMaximized={jest.fn()} onClose={jest.fn()} > diff --git a/src/components/container/dialog-card/dialog-card.tsx b/src/components/container/dialog-card/dialog-card.tsx index ec7561506..6163139f0 100644 --- a/src/components/container/dialog-card/dialog-card.tsx +++ b/src/components/container/dialog-card/dialog-card.tsx @@ -1,7 +1,13 @@ import * as React from 'react'; import classNames from 'classnames'; import {useTranslation} from 'react-i18next'; -import {IconChevronDown, IconChevronUp, IconX} from '@tabler/icons'; +import { + IconArrowsDiagonal, + IconArrowsDiagonalMinimize, + IconChevronDown, + IconChevronUp, + IconX +} from '@tabler/icons'; import {Card} from '../card'; import {IconButton} from '../../control/icon-button'; import './dialog-card.css'; @@ -13,7 +19,10 @@ export interface DialogCardProps { collapsed: boolean; fixedSize?: boolean; headerLabel: string; + maximizable?: boolean; + maximized?: boolean; onChangeCollapsed: (value: boolean) => void; + onChangeMaximized: (value: boolean) => void; onClose: () => void; } @@ -24,7 +33,10 @@ export const DialogCard: React.FC = props => { collapsed, fixedSize, headerLabel, + maximizable, + maximized, onChangeCollapsed, + onChangeMaximized, onClose } = props; const {didCatch, ErrorBoundary, error} = useErrorBoundary(); @@ -38,7 +50,8 @@ export const DialogCard: React.FC = props => { const calcdClassName = classNames('dialog-card', className, { collapsed, - 'fixed-size': fixedSize + 'fixed-size': fixedSize, + maximized }); function handleKeyDown(event: React.KeyboardEvent) { @@ -64,12 +77,29 @@ export const DialogCard: React.FC = props => { />
+ {maximizable && ( + + ) : ( + + ) + } + iconOnly + label={ + maximized ? t('common.unmaximize') : t('common.maximize') + } + onClick={() => onChangeMaximized(!maximized)} + tooltipPosition="bottom" + /> + )} } iconOnly label={t('common.close')} onClick={onClose} - tooltipPosition="left" + tooltipPosition="bottom" />
diff --git a/src/dialogs/__tests__/story-javascript.test.tsx b/src/dialogs/__tests__/story-javascript.test.tsx index 13d73b243..aab286396 100644 --- a/src/dialogs/__tests__/story-javascript.test.tsx +++ b/src/dialogs/__tests__/story-javascript.test.tsx @@ -35,6 +35,11 @@ describe('', () => { ); } + it('displays a dialog that can be maximized', () => { + renderComponent(); + expect(screen.getByLabelText('common.maximize')).toBeInTheDocument(); + }); + it("displays the story's JavaScript", () => { const story = fakeStory(); diff --git a/src/dialogs/__tests__/story-stylesheet.test.tsx b/src/dialogs/__tests__/story-stylesheet.test.tsx index 7dce6dd52..6de7ecc18 100644 --- a/src/dialogs/__tests__/story-stylesheet.test.tsx +++ b/src/dialogs/__tests__/story-stylesheet.test.tsx @@ -35,6 +35,11 @@ describe('', () => { ); } + it('displays a dialog that can be maximized', () => { + renderComponent(); + expect(screen.getByLabelText('common.maximize')).toBeInTheDocument(); + }); + it("displays the story's stylesheet", () => { const story = fakeStory(); diff --git a/src/dialogs/context/__tests__/dialogs.test.tsx b/src/dialogs/context/__tests__/dialogs.test.tsx index 2f6699d3d..fdfc6e4c6 100644 --- a/src/dialogs/context/__tests__/dialogs.test.tsx +++ b/src/dialogs/context/__tests__/dialogs.test.tsx @@ -6,11 +6,16 @@ import {FakeStateProvider} from '../../../test-util'; import {Dialogs} from '../dialogs'; import {DialogsContext, DialogsContextProps} from '../dialogs-context'; -const MockComponent: React.FC<{collapsed?: boolean}> = ({ +const MockComponent: React.FC<{collapsed?: boolean; maximized?: boolean}> = ({ children, - collapsed + collapsed, + maximized }) => ( -
+
{children}
); @@ -37,11 +42,13 @@ describe('', () => { { collapsed: false, component: MockComponent, + maximized: false, props: {children: 'mock child 1'} }, { collapsed: false, component: MockComponent, + maximized: false, props: {children: 'mock child 2'} } ] @@ -57,6 +64,7 @@ describe('', () => { { collapsed: true, component: MockComponent, + maximized: false, props: {children: 'mock child 1'} } ] @@ -65,12 +73,31 @@ describe('', () => { expect(screen.getByTestId('mock-component').dataset.collapsed).toBe('true'); }); - it('renders at the width given by the dialogsWidth pref', () => { + it('sets the maximized prop on the dialog component', () => { + renderComponent({ + dialogs: [ + { + collapsed: true, + component: MockComponent, + maximized: true, + props: {children: 'mock child 1'} + } + ] + }); + + expect(screen.getByTestId('mock-component').dataset.maximized).toBe('true'); + }); + + // Using screen.debug() doesn't seem to show the padding-left style. Maybe a + // gap in jsdom? + + it.skip('renders unmaximized dialogs at the width given by the dialogsWidth pref', () => { const dialogWidth = Math.random() * 1000; renderComponent(undefined, {dialogWidth}); + screen.debug(); expect( - document.querySelector('.dialogs')?.style.width + document.querySelector('.dialogs')?.style.paddingLeft ).toBe(`${dialogWidth}px`); }); diff --git a/src/dialogs/context/__tests__/reducer.test.ts b/src/dialogs/context/__tests__/reducer.test.ts index e99b5e4c8..927078905 100644 --- a/src/dialogs/context/__tests__/reducer.test.ts +++ b/src/dialogs/context/__tests__/reducer.test.ts @@ -12,7 +12,12 @@ describe('Dialog reducer', () => { props: {mockProp: true} }) ).toEqual([ - {collapsed: false, component: mockComponent, props: {mockProp: true}} + { + collapsed: false, + component: mockComponent, + maximized: false, + props: {mockProp: true} + } ])); it('adds a dialog if another component of its kind exists, but has different props', () => @@ -22,6 +27,7 @@ describe('Dialog reducer', () => { { collapsed: false, component: mockComponent, + maximized: false, props: {mockProp: true} } ], @@ -32,17 +38,28 @@ describe('Dialog reducer', () => { } ) ).toEqual([ - {collapsed: false, component: mockComponent, props: {mockProp: true}}, - {collapsed: false, component: mockComponent, props: {mockProp: false}} + { + collapsed: false, + component: mockComponent, + maximized: false, + props: {mockProp: true} + }, + { + collapsed: false, + component: mockComponent, + maximized: false, + props: {mockProp: false} + } ])); - it('expands an existing dialog if its component and props are identical', () => + it('expands an existing, collapsed dialog if its component and props are identical', () => expect( reducer( [ { collapsed: true, component: mockComponent, + maximized: false, props: {mockProp: true} } ], @@ -53,7 +70,12 @@ describe('Dialog reducer', () => { } ) ).toEqual([ - {collapsed: false, component: mockComponent, props: {mockProp: true}} + { + collapsed: false, + component: mockComponent, + maximized: false, + props: {mockProp: true} + } ])); }); @@ -65,11 +87,13 @@ describe('Dialog reducer', () => { { collapsed: false, component: mockComponent, + maximized: false, props: {mockProp: true} }, { collapsed: false, component: mockComponent, + maximized: false, props: {mockProp: false} } ], @@ -79,6 +103,7 @@ describe('Dialog reducer', () => { { collapsed: false, component: mockComponent, + maximized: false, props: {mockProp: false} } ]); @@ -88,11 +113,13 @@ describe('Dialog reducer', () => { { collapsed: false, component: mockComponent, + maximized: false, props: {mockProp: true} }, { collapsed: false, component: mockComponent, + maximized: false, props: {mockProp: false} } ], @@ -102,6 +129,7 @@ describe('Dialog reducer', () => { { collapsed: false, component: mockComponent, + maximized: false, props: {mockProp: true} } ]); @@ -114,6 +142,7 @@ describe('Dialog reducer', () => { { collapsed: false, component: mockComponent, + maximized: false, props: {mockProp: true} } ], @@ -123,6 +152,7 @@ describe('Dialog reducer', () => { { collapsed: false, component: mockComponent, + maximized: false, props: {mockProp: true} } ]); @@ -137,11 +167,13 @@ describe('Dialog reducer', () => { { collapsed: false, component: mockComponent, + maximized: false, props: {mockProp: true} }, { collapsed: false, component: mockComponent, + maximized: false, props: {mockProp: false} } ], @@ -151,11 +183,13 @@ describe('Dialog reducer', () => { { collapsed: true, component: mockComponent, + maximized: false, props: {mockProp: true} }, { collapsed: false, component: mockComponent, + maximized: false, props: {mockProp: false} } ])); @@ -167,6 +201,7 @@ describe('Dialog reducer', () => { { collapsed: false, component: mockComponent, + maximized: false, props: {mockProp: true} } ], @@ -176,6 +211,132 @@ describe('Dialog reducer', () => { { collapsed: false, component: mockComponent, + maximized: false, + props: {mockProp: true} + } + ])); + }); + + describe('when a setDialogMaximized action is received', () => { + it('updates the dialog at the index specified', () => { + expect( + reducer( + [ + { + collapsed: false, + component: mockComponent, + maximized: false, + props: {mockProp: true} + }, + { + collapsed: false, + component: mockComponent, + maximized: false, + props: {mockProp: false} + } + ], + {type: 'setDialogMaximized', maximized: true, index: 0} + ) + ).toEqual([ + { + collapsed: false, + component: mockComponent, + maximized: true, + props: {mockProp: true} + }, + { + collapsed: false, + component: mockComponent, + maximized: false, + props: {mockProp: false} + } + ]); + expect( + reducer( + [ + { + collapsed: false, + component: mockComponent, + maximized: true, + props: {mockProp: true} + }, + { + collapsed: false, + component: mockComponent, + maximized: false, + props: {mockProp: false} + } + ], + {type: 'setDialogMaximized', maximized: false, index: 0} + ) + ).toEqual([ + { + collapsed: false, + component: mockComponent, + maximized: false, + props: {mockProp: true} + }, + { + collapsed: false, + component: mockComponent, + maximized: false, + props: {mockProp: false} + } + ]); + }); + + it('does not allow more than one dialog to be maximized', () => + expect( + reducer( + [ + { + collapsed: false, + component: mockComponent, + maximized: false, + props: {mockProp: true} + }, + { + collapsed: false, + component: mockComponent, + maximized: true, + props: {mockProp: false} + } + ], + {type: 'setDialogMaximized', maximized: true, index: 0} + ) + ).toEqual([ + { + collapsed: false, + component: mockComponent, + maximized: true, + props: {mockProp: true} + }, + { + collapsed: false, + component: mockComponent, + maximized: false, + props: {mockProp: false} + } + ])); + + it('does nothing if an incorrect index is specified', () => + expect( + reducer( + [ + { + collapsed: false, + component: mockComponent, + maximized: false, + props: {mockProp: true} + } + ], + {type: 'setDialogMaximized', maximized: true, index: 2} + ) + ).toEqual([ + { + collapsed: false, + component: mockComponent, + maximized: false, props: {mockProp: true} } ])); diff --git a/src/dialogs/context/dialogs.css b/src/dialogs/context/dialogs.css index 7dbb226fd..2417c4e04 100644 --- a/src/dialogs/context/dialogs.css +++ b/src/dialogs/context/dialogs.css @@ -2,18 +2,31 @@ @import '../../styles/metrics.css'; .dialogs { + bottom: var(--grid-size); + left: var(--grid-size); + pointer-events: none; + position: fixed; + right: var(--grid-size); + top: calc( + 2 * var(--control-height) + var(--grid-size) + ); /* below height */ +} + +.dialogs { + align-items: flex-end; + bottom: var(--grid-size); display: flex; flex-direction: column; + grid-gap: var(--grid-size); justify-content: flex-end; - max-width: calc(100% - 2 * var(--grid-size)); - min-width: 600px; pointer-events: none; position: fixed; - bottom: var(--grid-size); right: var(--grid-size); top: calc( 2 * var(--control-height) + var(--grid-size) ); /* below height */ + left: var(--grid-size); + bottom: var(--grid-size); } .dialogs .dialog-card { @@ -21,11 +34,15 @@ pointer-events: auto; } -.dialogs .dialog-card + .dialog-card { - margin-top: var(--grid-size); -} - .dialogs .dialog-card.collapsed, .dialogs .dialog-card.fixed-size { flex: 0; } + +.dialogs .maximized { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} \ No newline at end of file diff --git a/src/dialogs/context/dialogs.tsx b/src/dialogs/context/dialogs.tsx index 1eadf77c9..51f60f1b8 100644 --- a/src/dialogs/context/dialogs.tsx +++ b/src/dialogs/context/dialogs.tsx @@ -16,26 +16,41 @@ export const Dialogs: React.FC = () => { const {prefs} = usePrefsContext(); const {dispatch, dialogs} = useDialogsContext(); - const style: React.CSSProperties = { + const hasUnmaximized = dialogs.some(dialog => !dialog.maximized); + const containerStyle: React.CSSProperties = { + paddingLeft: `calc(100% - (${prefs.dialogWidth}px + 2 * (var(--grid-size))))`, marginBottom: height, - marginRight: width, - width: `${prefs.dialogWidth}px` + marginRight: width + }; + const maximizedStyle: React.CSSProperties = { + marginRight: hasUnmaximized + ? `calc(${prefs.dialogWidth}px + var(--grid-size))` + : 0 }; return ( -
+
{dialogs.map((dialog, index) => { const managementProps = { collapsed: dialog.collapsed, + maximized: dialog.maximized, onChangeCollapsed: (collapsed: boolean) => dispatch({type: 'setDialogCollapsed', collapsed, index}), + onChangeMaximized: (maximized: boolean) => + dispatch({type: 'setDialogMaximized', maximized, index}), onClose: () => dispatch({type: 'removeDialog', index}) }; return ( - + {dialog.maximized ? ( +
+ +
+ ) : ( + + )}
); })} diff --git a/src/dialogs/context/reducer.ts b/src/dialogs/context/reducer.ts index 638ff1820..98057a8f3 100644 --- a/src/dialogs/context/reducer.ts +++ b/src/dialogs/context/reducer.ts @@ -15,9 +15,10 @@ export const reducer: React.Reducer = ( const editedState = state.map(stateDialog => { if ( isEqual(stateDialog, { - // Ignore collapsed property for comparison + // Ignore collapsed and maximized properties for comparison. collapsed: stateDialog.collapsed, component: action.component, + maximized: stateDialog.maximized, props: action.props }) ) { @@ -34,7 +35,12 @@ export const reducer: React.Reducer = ( return [ ...state, - {component: action.component, collapsed: false, props: action.props} + { + collapsed: false, + component: action.component, + maximized: false, + props: action.props + } ]; case 'removeDialog': @@ -46,5 +52,11 @@ export const reducer: React.Reducer = ( ? {...dialog, collapsed: action.collapsed} : dialog ); + + case 'setDialogMaximized': + return state.map((dialog, index) => ({ + ...dialog, + maximized: index === action.index ? action.maximized : false + })); } }; diff --git a/src/dialogs/dialogs.types.ts b/src/dialogs/dialogs.types.ts index bf4e5ceaa..f27a8f183 100644 --- a/src/dialogs/dialogs.types.ts +++ b/src/dialogs/dialogs.types.ts @@ -3,8 +3,23 @@ import {DialogCardProps} from '../components/container/dialog-card'; export type DialogComponentProps = Omit; export interface Dialog { + /** + * Is the dialog collapsed? (only showing its title bar) + */ collapsed: boolean; + /** + * Component to render. + */ component: React.ComponentType; + /** + * Is the dialog maximized? Although only one dialog can be maximized at a + * time, this is an attribute so that when a dialog is un-minimized, it goes + * back to its previous position. + */ + maximized: boolean; + /** + * Props to apply to the component. + */ props?: Record; } @@ -17,4 +32,5 @@ export type DialogsAction = props?: Record; } | {type: 'removeDialog'; index: number} - | {type: 'setDialogCollapsed'; collapsed: boolean; index: number}; + | {type: 'setDialogCollapsed'; collapsed: boolean; index: number} + | {type: 'setDialogMaximized'; maximized: boolean; index: number}; \ No newline at end of file diff --git a/src/dialogs/passage-edit/__tests__/passage-edit.test.tsx b/src/dialogs/passage-edit/__tests__/passage-edit.test.tsx index 2ae64f386..ff0ee30c3 100644 --- a/src/dialogs/passage-edit/__tests__/passage-edit.test.tsx +++ b/src/dialogs/passage-edit/__tests__/passage-edit.test.tsx @@ -27,6 +27,7 @@ const TestPassageEditDialog: React.FC< ', () => { ).toBeInTheDocument(); }); + it('displays a dialog that can be maximized', () => { + renderComponent(); + expect(screen.getByLabelText('common.maximize')).toBeInTheDocument(); + }); + it('updates the passage text when the user edits it', () => { const story = fakeStory(1); const format = fakeUnloadedStoryFormat({ diff --git a/src/dialogs/passage-edit/passage-edit.tsx b/src/dialogs/passage-edit/passage-edit.tsx index 7fe2f9114..331266bee 100644 --- a/src/dialogs/passage-edit/passage-edit.tsx +++ b/src/dialogs/passage-edit/passage-edit.tsx @@ -91,6 +91,7 @@ export const InnerPassageEditDialog: React.FC< {...other} className="passage-edit-dialog" headerLabel={passage.name} + maximizable > {editorCrashed ? ( {t('dialogs.passageEdit.editorCrashed')} diff --git a/src/dialogs/story-javascript.tsx b/src/dialogs/story-javascript.tsx index 2e6d27aea..93f56f8f9 100644 --- a/src/dialogs/story-javascript.tsx +++ b/src/dialogs/story-javascript.tsx @@ -36,6 +36,7 @@ export const StoryJavaScriptDialog: React.FC = props {...other} className="story-javascript-dialog" headerLabel={t('dialogs.storyJavaScript.title')} + maximizable > diff --git a/src/dialogs/story-stylesheet.tsx b/src/dialogs/story-stylesheet.tsx index 36ac34a0d..f9a4d0098 100644 --- a/src/dialogs/story-stylesheet.tsx +++ b/src/dialogs/story-stylesheet.tsx @@ -36,6 +36,7 @@ export const StoryStylesheetDialog: React.FC = props {...other} className="story-stylesheet-dialog" headerLabel={t('dialogs.storyStylesheet.title')} + maximizable > From d013ad29553afd5d82522cc974a028965287758a Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Tue, 16 Aug 2022 17:25:59 -0400 Subject: [PATCH 08/26] Fix linter issue --- src/components/storage-quota/__tests__/storage-quota.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/storage-quota/__tests__/storage-quota.test.tsx b/src/components/storage-quota/__tests__/storage-quota.test.tsx index 66aefdf5a..b519f2eb5 100644 --- a/src/components/storage-quota/__tests__/storage-quota.test.tsx +++ b/src/components/storage-quota/__tests__/storage-quota.test.tsx @@ -1,4 +1,4 @@ -import {act, cleanup, render, screen, waitFor} from '@testing-library/react'; +import {cleanup, render, screen, waitFor} from '@testing-library/react'; import {axe} from 'jest-axe'; import * as React from 'react'; import {isElectronRenderer} from '../../../util/is-electron'; From e6b528da9aa01f1ea1c358d477fd6af0681fb481 Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Tue, 16 Aug 2022 17:26:51 -0400 Subject: [PATCH 09/26] Update browserslist --- package-lock.json | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ca61a0ba..1117fb05a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7996,14 +7996,20 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001307", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001307.tgz", - "integrity": "sha512-+MXEMczJ4FuxJAUp0jvAl6Df0NI/OfW1RWEE61eSmzS7hw6lz4IKutbhbXendwq8BljfFuHtu26VWsg4afQ7Ng==", + "version": "1.0.30001377", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001377.tgz", + "integrity": "sha512-I5XeHI1x/mRSGl96LFOaSk528LA/yZG3m3iQgImGujjO8gotd/DL8QaI1R1h1dg5ATeI2jqPblMpKq4Tr5iKfQ==", "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] }, "node_modules/capture-exit": { "version": "2.0.0", @@ -35719,9 +35725,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001307", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001307.tgz", - "integrity": "sha512-+MXEMczJ4FuxJAUp0jvAl6Df0NI/OfW1RWEE61eSmzS7hw6lz4IKutbhbXendwq8BljfFuHtu26VWsg4afQ7Ng==", + "version": "1.0.30001377", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001377.tgz", + "integrity": "sha512-I5XeHI1x/mRSGl96LFOaSk528LA/yZG3m3iQgImGujjO8gotd/DL8QaI1R1h1dg5ATeI2jqPblMpKq4Tr5iKfQ==", "dev": true }, "capture-exit": { From fc53320fea86ec68c3a6bd62844408178e0375e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Aug 2022 21:35:44 +0000 Subject: [PATCH 10/26] Bump async from 2.6.3 to 2.6.4 Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4. - [Release notes](https://github.com/caolan/async/releases) - [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md) - [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4) --- updated-dependencies: - dependency-name: async dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2d9e7f25f..0d9e8819a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5236,9 +5236,9 @@ } }, "node_modules/async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "dependencies": { "lodash": "^4.17.14" @@ -33410,9 +33410,9 @@ "dev": true }, "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "requires": { "lodash": "^4.17.14" From 2a4b87564ef818880edbfe8c21e9a96525664c78 Mon Sep 17 00:00:00 2001 From: Leon Date: Thu, 18 Aug 2022 00:36:40 +1000 Subject: [PATCH 11/26] Add links to story format issue trackers to ISSUE_TEMPLATE. --- .github/ISSUE_TEMPLATE/bug-report.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 482adf80e..0bf2f73d4 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -9,6 +9,9 @@ body: - Issues in this repository are for tracking problems with the Twine application. "How do I?" type questions are better answered on community resources like the Intfiction.org forums, Reddit, or Discord. - If you are having problems with your Twine story when it is being played, then you should report the problem with the story format you are using, not here. + - For those using the **Harlowe** story format (the default format), reports should be filed at [its issue tracker](https://foss.heptapod.net/games/harlowe/-/issues). Guidelines for bug reports are listed [here](https://twine2.neocities.org/#introduction_report-bugs-and-suggest-features). + - For those using the **SugarCube** format, reports should be filed at [its issue tracker](https://github.com/tmedwards/sugarcube-2/issues). + - For those using the **Chapbook** format, reports should be filed at [its issue tracker](https://github.com/klembot/chapbook/issues). - In order for your issue to be addressed, someone else will need to be able to cause the bug to occur on their own computer in a predictable fashion. Otherwise, it will be impossible to tell if it was actually fixed when it's worked on. Please be as detailed as you can in your description below. - Usually, new issues are reviewed around once a month. It may take longer. - If work begins on your bug report, it will be added to a project in this repository. You can track the status of its implementation there. From 96b48bba8fffd784ad113b9ce365ab593a09b807 Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Sat, 20 Aug 2022 15:05:42 -0400 Subject: [PATCH 12/26] Remove middle-click passage creation Resolves #1206 --- .../passage-map/__mocks__/passage-map.tsx | 3 -- .../__tests__/passage-map.test.tsx | 23 +-------------- .../passage/passage-map/passage-map.tsx | 28 ------------------- .../marqueeable-passage-map.test.tsx | 2 -- .../__tests__/story-edit-route.test.tsx | 17 +---------- src/routes/story-edit/story-edit-route.tsx | 15 ---------- 6 files changed, 2 insertions(+), 86 deletions(-) diff --git a/src/components/passage/passage-map/__mocks__/passage-map.tsx b/src/components/passage/passage-map/__mocks__/passage-map.tsx index 3394c9d44..0406d155a 100644 --- a/src/components/passage/passage-map/__mocks__/passage-map.tsx +++ b/src/components/passage/passage-map/__mocks__/passage-map.tsx @@ -22,9 +22,6 @@ export const PassageMap: React.FC> = props => ( - diff --git a/src/components/passage/passage-map/__tests__/passage-map.test.tsx b/src/components/passage/passage-map/__tests__/passage-map.test.tsx index 61c27758c..5bb38e597 100644 --- a/src/components/passage/passage-map/__tests__/passage-map.test.tsx +++ b/src/components/passage/passage-map/__tests__/passage-map.test.tsx @@ -1,12 +1,5 @@ -import { - createEvent, - fireEvent, - render, - screen, - within -} from '@testing-library/react'; +import {fireEvent, render, screen, within} from '@testing-library/react'; import {axe} from 'jest-axe'; -import * as React from 'react'; import {fakePassage} from '../../../../test-util'; import {PassageMap, PassageMapProps} from '../passage-map'; @@ -25,7 +18,6 @@ describe('', () => { onDeselect={jest.fn()} onDrag={jest.fn()} onEdit={jest.fn()} - onMiddleClick={jest.fn()} onSelect={jest.fn()} passages={passages} startPassageId={passages[0].id} @@ -126,19 +118,6 @@ describe('', () => { expect(onSelect).not.toBeCalled(); }); - it('calls the onMiddleClick prop when the user middle-clicks the component', () => { - const passage = fakePassage({name: 'test'}); - const onMiddleClick = jest.fn(); - - renderComponent({onMiddleClick, passages: [passage]}); - - const target = screen.getByTestId('mock-passage-connections-test'); - - expect(onMiddleClick).not.toBeCalled(); - fireEvent(target, createEvent.mouseUp(target, {button: 1})); - expect(onMiddleClick.mock.calls).toEqual([[{top: 0, left: 0}]]); - }); - it('is accessible', async () => { const {container} = renderComponent(); diff --git a/src/components/passage/passage-map/passage-map.tsx b/src/components/passage/passage-map/passage-map.tsx index 0fcef86fc..2bdf21f91 100644 --- a/src/components/passage/passage-map/passage-map.tsx +++ b/src/components/passage/passage-map/passage-map.tsx @@ -13,7 +13,6 @@ export interface PassageMapProps { onDeselect: (passage: Passage) => void; onDrag: (change: Point) => void; onEdit: (passage: Passage) => void; - onMiddleClick: (position: Point) => void; onSelect: (passage: Passage, exclusive: boolean) => void; passages: Passage[]; startPassageId: string; @@ -77,7 +76,6 @@ export const PassageMap: React.FC = props => { onDeselect, onDrag, onEdit, - onMiddleClick, onSelect, passages, startPassageId, @@ -174,38 +172,12 @@ export const PassageMap: React.FC = props => { }, [onSelect, state.dragging] ); - const handleMouseUp = React.useCallback( - (event: React.MouseEvent) => { - // Listen for middle clicks outside of interactible elements. We can't use - // onClick for this because middle buttons don't seem to generate those - // events. - - if ( - !container.current || - event.button !== 1 || - (event.target as HTMLElement).closest('.passage-card') - ) { - return; - } - - // Adjust the click position for the container's position onscreen. - - const containerRect = container.current.getBoundingClientRect(); - - onMiddleClick({ - left: event.clientX - containerRect.left, - top: event.clientY - containerRect.top - }); - }, - [onMiddleClick] - ); return (
diff --git a/src/routes/story-edit/__tests__/marqueeable-passage-map.test.tsx b/src/routes/story-edit/__tests__/marqueeable-passage-map.test.tsx index 9bca93da6..a001cc6eb 100644 --- a/src/routes/story-edit/__tests__/marqueeable-passage-map.test.tsx +++ b/src/routes/story-edit/__tests__/marqueeable-passage-map.test.tsx @@ -38,7 +38,6 @@ describe('', () => { onDeselect={jest.fn()} onDrag={jest.fn()} onEdit={jest.fn()} - onMiddleClick={jest.fn()} onSelect={jest.fn()} onSelectRect={jest.fn()} passages={story.passages} @@ -165,7 +164,6 @@ describe('', () => { onDeselect: jest.fn(), onDrag: jest.fn(), onEdit: jest.fn(), - onMiddleClick: jest.fn(), onSelect: jest.fn() }; diff --git a/src/routes/story-edit/__tests__/story-edit-route.test.tsx b/src/routes/story-edit/__tests__/story-edit-route.test.tsx index 7e938255d..5d9d83ece 100644 --- a/src/routes/story-edit/__tests__/story-edit-route.test.tsx +++ b/src/routes/story-edit/__tests__/story-edit-route.test.tsx @@ -1,4 +1,4 @@ -import {act, fireEvent, render, screen} from '@testing-library/react'; +import {act, render, screen} from '@testing-library/react'; import {createMemoryHistory} from 'history'; import {axe} from 'jest-axe'; import * as React from 'react'; @@ -98,21 +98,6 @@ describe('', () => { ).toBeInTheDocument(); }); - it('creates a passage if the passage map is middle-clicked', async () => { - const story = fakeStory(1); - - story.zoom = 0.5; - await renderComponent(story); - expect(screen.getAllByTestId(/^passage-/).length).toBe(1); - fireEvent.click(screen.getByText('onMiddleClick')); - - const passages = screen.getAllByTestId(/^passage-/); - - expect(passages.length).toBe(2); - expect(passages[1].dataset.left).toBe('250'); - expect(passages[1].dataset.top).toBe('550'); - }); - it('creates a passage automatically if the story has none', async () => { await renderComponent(fakeStory(0)); expect(screen.getAllByTestId(/^passage-/).length).toBe(1); diff --git a/src/routes/story-edit/story-edit-route.tsx b/src/routes/story-edit/story-edit-route.tsx index 3cbbb0256..ee6405f94 100644 --- a/src/routes/story-edit/story-edit-route.tsx +++ b/src/routes/story-edit/story-edit-route.tsx @@ -60,20 +60,6 @@ export const InnerStoryEditRoute: React.FC = () => { }; }, [story.zoom]); - const handleMiddleClick = React.useCallback( - (position: Point) => { - undoableStoriesDispatch( - createUntitledPassage( - story, - position.left / story.zoom, - position.top / story.zoom - ), - 'undoChange.newPassage' - ); - }, - [story, undoableStoriesDispatch] - ); - const handleDeselectPassage = React.useCallback( (passage: Passage) => undoableStoriesDispatch(deselectPassage(story, passage)), @@ -177,7 +163,6 @@ export const InnerStoryEditRoute: React.FC = () => { onDeselect={handleDeselectPassage} onDrag={handleDragPassages} onEdit={handleEditPassage} - onMiddleClick={handleMiddleClick} onSelect={handleSelectPassage} onSelectRect={handleSelectRect} passages={story.passages} From 8cb091dc2b03bc697b8879bc0383d58e1604bf57 Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Sun, 21 Aug 2022 11:04:12 -0400 Subject: [PATCH 13/26] Fix error message interpolation Resolves #1236 --- public/locales/de.json | 2 +- public/locales/en-US.json | 2 +- public/locales/zh-cn.json | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/locales/de.json b/public/locales/de.json index 96a609217..cb2394971 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -304,7 +304,7 @@ "addStoryFormatButton": { "addPreview": "{{storyFormatName}} {{storyFormatVersion}} wird hinzugefügt.", "alreadyAdded": "{{storyFormatName}} {{storyFormatVersion}} ist bereits vorhanden.", - "fetchError": "Das Geschichtsformat an dieser Adresse konnte nicht abgerufen werden ({errorMessage}).", + "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." }, diff --git a/public/locales/en-US.json b/public/locales/en-US.json index 0f8c47063..f0c64b0ec 100644 --- a/public/locales/en-US.json +++ b/public/locales/en-US.json @@ -312,7 +312,7 @@ "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}).", + "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." }, diff --git a/public/locales/zh-cn.json b/public/locales/zh-cn.json index 7174b5159..8acf0aae6 100644 --- a/public/locales/zh-cn.json +++ b/public/locales/zh-cn.json @@ -297,9 +297,9 @@ "useAsProofingFormat": "用于校对故事", "useAsDefaultFormat": "设为默认格式", "addStoryFormatButton": { - "addPreview": "将添加 {storyFormatName} {storyFormatVersion}。", - "alreadyAdded": "已添加 {storyFormatName} {storyFormatVersion}。", - "fetchError": "无法读取该位置的故事格式({errorMessage})。", + "addPreview": "将添加 {{storyFormatName}} {{storyFormatVersion}}。", + "alreadyAdded": "已添加 {{storyFormatName}} {{storyFormatVersion}}。", + "fetchError": "无法读取该位置的故事格式({{errorMessage}})。", "invalidUrl": "请输入有效的 URL。", "prompt": "要添加故事格式,请在下方输入地址。" }, From 9c5e112e0d5df672f3e534634fdf49c9e0e1f94d Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Sun, 21 Aug 2022 12:04:04 -0400 Subject: [PATCH 14/26] Fix CSS for import dialog getting too tall for viewport --- src/dialogs/story-import/story-chooser.css | 12 ++++++++++++ src/dialogs/story-import/story-chooser.tsx | 15 +++++++++------ src/dialogs/story-import/story-import.css | 7 +++++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/dialogs/story-import/story-chooser.css b/src/dialogs/story-import/story-chooser.css index 004c7a19d..22daab416 100644 --- a/src/dialogs/story-import/story-chooser.css +++ b/src/dialogs/story-import/story-chooser.css @@ -1,3 +1,9 @@ +.story-chooser { + display: flex; + flex-direction: column; + min-height: 0; +} + .story-chooser p { margin-bottom: 0; } @@ -5,9 +11,15 @@ .story-chooser ul { list-style: none; margin: 0; + min-height: 0; + overflow: auto; padding: 0; } .story-chooser .replace-warning { display: block; +} + +.story-chooser .actions { + flex-shrink: 0; } \ No newline at end of file diff --git a/src/dialogs/story-import/story-chooser.tsx b/src/dialogs/story-import/story-chooser.tsx index cc3dc4750..fd5b31586 100644 --- a/src/dialogs/story-import/story-chooser.tsx +++ b/src/dialogs/story-import/story-chooser.tsx @@ -54,12 +54,15 @@ export const StoryChooser: React.FC = props => { ))} - } - label={t('dialogs.storyImport.importSelected')} - onClick={() => onImport(selectedStories)} - /> +
+ } + label={t('dialogs.storyImport.importSelected')} + onClick={() => onImport(selectedStories)} + variant="primary" + /> +
); }; diff --git a/src/dialogs/story-import/story-import.css b/src/dialogs/story-import/story-import.css index 1adc6ef18..9fec47a87 100644 --- a/src/dialogs/story-import/story-import.css +++ b/src/dialogs/story-import/story-import.css @@ -1,3 +1,10 @@ +.story-import-dialog .card-content { + display: flex; + flex-direction: column; + max-height: 100%; + min-height: 0; +} + .story-import-dialog .card-content p:first-child { margin-top: 0; } \ No newline at end of file From 8032edfbe41e1d10944748a2fa9ffdddec28b21b Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Mon, 22 Aug 2022 09:31:00 -0400 Subject: [PATCH 15/26] Add dialog docs --- docs/en/src/getting-started/getting-around.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/en/src/getting-started/getting-around.md b/docs/en/src/getting-started/getting-around.md index ac70e02db..93e3e51ea 100644 --- a/docs/en/src/getting-started/getting-around.md +++ b/docs/en/src/getting-started/getting-around.md @@ -47,9 +47,16 @@ Certain actions in Twine will open dialog boxes along one side of the screen with more detail about a particular action. For example, editing a passage will open a dialog box with the passage text. -Dialog boxes have an x button in their corner that closes the dialog. They also -have a chevron (›) button on the opposite side that collapses the dialog so that -only its title bar is visible. - -You can have as many dialogs open as you have room onscreen from. Right now, the -order of dialogs can't be changed, nor can the size of dialogs onscreen. \ No newline at end of file +Dialog boxes have a few controls in their title bar: + +- An x button in their corner that closes the dialog. +- A chevron (›) button on the opposite side that collapses the dialog so that + only its title bar is visible. +- Some dialogs have a maximize button beside their close button which allows a + dialog to fill the available space. To restore a maximized dialog's original + size, use the maximize button again. Only one dialog can be maximized at a + time. + +You can have as many dialogs open as you have room onscreen for. Right now, the +order of dialogs can't be changed, nor can their position onscreen be changed. +The width of dialogs can be changed in [preferences](../preferences). \ No newline at end of file From b78ebf99599ea72461bf2f1a037cad151881978d Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Mon, 22 Aug 2022 09:37:29 -0400 Subject: [PATCH 16/26] Scroll dialog card content when it overflows Resolves #1183 --- src/components/container/dialog-card/dialog-card.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/container/dialog-card/dialog-card.css b/src/components/container/dialog-card/dialog-card.css index 4d8c27bb8..9c2106785 100644 --- a/src/components/container/dialog-card/dialog-card.css +++ b/src/components/container/dialog-card/dialog-card.css @@ -41,3 +41,8 @@ .dialog-card .card-body h2 { font: bold 100% var(--font-system); } + +.dialog-card .card-content { + min-height: 0; + overflow: auto; +} \ No newline at end of file From 36ffcb34286dc2669189a12630956ae896ce93b4 Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Mon, 22 Aug 2022 15:58:32 -0400 Subject: [PATCH 17/26] Improve error handling around publishing --- .../control/__tests__/card-button.test.tsx | 12 ++ src/components/control/card-button.tsx | 4 +- .../__tests__/build-actions.test.tsx | 99 ++++++++++-- src/route-actions/build-actions.tsx | 145 ++++++++++++++++-- .../__tests__/story-play-route.test.tsx | 12 ++ src/routes/story-play/story-play-route.tsx | 12 +- .../__tests__/story-proof-route.test.tsx | 12 ++ src/routes/story-proof/story-proof-route.tsx | 12 +- .../__tests__/story-test-route.test.tsx | 14 +- src/routes/story-test/story-test-route.tsx | 22 ++- 10 files changed, 303 insertions(+), 41 deletions(-) diff --git a/src/components/control/__tests__/card-button.test.tsx b/src/components/control/__tests__/card-button.test.tsx index 4fa29e1f5..03989b682 100644 --- a/src/components/control/__tests__/card-button.test.tsx +++ b/src/components/control/__tests__/card-button.test.tsx @@ -54,6 +54,18 @@ describe('', () => { expect(onChangeOpen.mock.calls).toEqual([[false]]); }); + it('allows overriding click behavior with the onClick prop', async () => { + const onChangeOpen = jest.fn(); + const onClick = jest.fn(); + + renderComponent({onChangeOpen, onClick}); + expect(onChangeOpen).not.toHaveBeenCalled(); + expect(onClick).not.toHaveBeenCalled(); + fireEvent.click(screen.getByText('mock-label')); + expect(onClick).toBeCalledTimes(1); + expect(onChangeOpen).not.toHaveBeenCalled(); + }); + // This works in isolation but not with other tests--unsure why. it.skip('calls onChangeOpen if the user clicks outside of the card while open', async () => { diff --git a/src/components/control/card-button.tsx b/src/components/control/card-button.tsx index e8abf2dca..ffa928a64 100644 --- a/src/components/control/card-button.tsx +++ b/src/components/control/card-button.tsx @@ -6,7 +6,7 @@ import {IconButton, IconButtonProps} from './icon-button'; import './card-button.css'; import FocusTrap from 'focus-trap-react'; -export interface CardButtonProps extends Omit { +export interface CardButtonProps extends IconButtonProps { /** * ARIA label for the card that opens. */ @@ -39,8 +39,8 @@ export const CardButton: React.FC = props => { return ( onChangeOpen(!open)} + {...other} ref={setButtonEl} /> ', () => { + const usePublishingMock = usePublishing as jest.Mock; + const useStoryLaunchMock = useStoryLaunch as jest.Mock; + function renderComponent(props?: Partial) { return render(); } describe('when not given a story prop', () => { - beforeEach(() => renderComponent({story: undefined})); + beforeEach(() => { + usePublishingMock.mockReturnValue({}); + useStoryLaunchMock.mockReturnValue({}); + renderComponent({story: undefined}); + }); it('disables the test button', () => expect(screen.getByText('routeActions.build.test')).toBeDisabled()); + + it('disables the play button', () => + expect(screen.getByText('routeActions.build.play')).toBeDisabled()); + + it('disables the proof button', () => + expect(screen.getByText('routeActions.build.proof')).toBeDisabled()); + + it('disables the publish to story button', () => + expect( + screen.getByText('routeActions.build.publishToFile') + ).toBeDisabled()); }); describe('when given a story prop', () => { - let openSpy: jest.SpyInstance; + let playStory: jest.SpyInstance; + let proofStory: jest.SpyInstance; + let publishStory: jest.SpyInstance; + let testStory: jest.SpyInstance; let story: Story; beforeEach(() => { - openSpy = jest.spyOn(window, 'open').mockReturnValue(); + playStory = jest.fn(); + proofStory = jest.fn(); + publishStory = jest.fn(); + testStory = jest.fn(); + usePublishingMock.mockReturnValue({publishStory}); + useStoryLaunchMock.mockReturnValue({playStory, proofStory, testStory}); story = fakeStory(); renderComponent({story}); }); it('displays a button to test the story', () => { - expect(openSpy).not.toHaveBeenCalled(); + expect(testStory).not.toHaveBeenCalled(); + fireEvent.click(screen.getByText('routeActions.build.test')); + expect(testStory.mock.calls).toEqual([[story.id]]); + }); + + it('displays the error if testing fails', async () => { + testStory.mockRejectedValue(new Error('mock-test-error')); fireEvent.click(screen.getByText('routeActions.build.test')); - expect(openSpy.mock.calls).toEqual([ - [`#/stories/${story.id}/test`, '_blank'] - ]); + await waitFor(() => + expect(screen.getByText('mock-test-error')).toBeInTheDocument() + ); }); it('displays a button to play the story', () => { - expect(openSpy).not.toHaveBeenCalled(); + expect(playStory).not.toHaveBeenCalled(); fireEvent.click(screen.getByText('routeActions.build.play')); - expect(openSpy.mock.calls).toEqual([ - [`#/stories/${story.id}/play`, '_blank'] - ]); + expect(playStory.mock.calls).toEqual([[story.id]]); + }); + + it('displays the error if playing fails', async () => { + playStory.mockRejectedValue(new Error('mock-play-error')); + fireEvent.click(screen.getByText('routeActions.build.play')); + await waitFor(() => + expect(screen.getByText('mock-play-error')).toBeInTheDocument() + ); }); it('displays a button to proof the story', () => { - expect(openSpy).not.toHaveBeenCalled(); + expect(proofStory).not.toHaveBeenCalled(); + fireEvent.click(screen.getByText('routeActions.build.proof')); + expect(proofStory.mock.calls).toEqual([[story.id]]); + }); + + it('displays the error if proofing fails', async () => { + proofStory.mockRejectedValue(new Error('mock-proof-error')); fireEvent.click(screen.getByText('routeActions.build.proof')); - expect(openSpy.mock.calls).toEqual([ - [`#/stories/${story.id}/proof`, '_blank'] - ]); + await waitFor(() => + expect(screen.getByText('mock-proof-error')).toBeInTheDocument() + ); + }); + + it('displays a button to publish the story to a file', () => { + expect(publishStory).not.toHaveBeenCalled(); + fireEvent.click(screen.getByText('routeActions.build.publishToFile')); + expect(publishStory.mock.calls).toEqual([[story.id]]); }); - it.todo('displays a button to publish the story to a file'); + it('displays the error if publishing fails', async () => { + publishStory.mockRejectedValue(new Error('mock-publish-error')); + fireEvent.click(screen.getByText('routeActions.build.publishToFile')); + await waitFor(() => + expect(screen.getByText('mock-publish-error')).toBeInTheDocument() + ); + }); }); it('is accessible', async () => { + usePublishingMock.mockReturnValue({}); + useStoryLaunchMock.mockReturnValue({}); + const {container} = renderComponent(); expect(await axe(container)).toHaveNoViolations(); diff --git a/src/route-actions/build-actions.tsx b/src/route-actions/build-actions.tsx index ea6f2de63..936328482 100644 --- a/src/route-actions/build-actions.tsx +++ b/src/route-actions/build-actions.tsx @@ -1,7 +1,15 @@ -import {IconEye, IconFileText, IconPlayerPlay, IconTool} from '@tabler/icons'; +import { + IconEye, + IconFileText, + IconPlayerPlay, + IconTool, + IconX +} from '@tabler/icons'; import * as React from 'react'; import {useTranslation} from 'react-i18next/'; import {ButtonBar} from '../components/container/button-bar'; +import {CardContent} from '../components/container/card'; +import {CardButton} from '../components/control/card-button'; import {IconButton} from '../components/control/icon-button'; import {storyFileName} from '../electron/shared'; import {Story} from '../store/stories'; @@ -15,43 +23,154 @@ export interface BuildActionsProps { export const BuildActions: React.FC = ({story}) => { const {publishStory} = usePublishing(); + const [playError, setPlayError] = React.useState(); + const [proofError, setProofError] = React.useState(); + const [publishError, setPublishError] = React.useState(); + const [testError, setTestError] = React.useState(); const {playStory, proofStory, testStory} = useStoryLaunch(); const {t} = useTranslation(); + function resetErrors() { + setPlayError(undefined); + setProofError(undefined); + setPublishError(undefined); + setTestError(undefined); + } + + async function handlePlay() { + if (!story) { + throw new Error('No story provided to publish'); + } + + resetErrors(); + + try { + await playStory(story.id); + } catch (error) { + setPlayError(error as Error); + } + } + + async function handleProof() { + if (!story) { + throw new Error('No story provided to publish'); + } + + resetErrors(); + + try { + await proofStory(story.id); + } catch (error) { + setProofError(error as Error); + } + } + async function handlePublishFile() { if (!story) { throw new Error('No story provided to publish'); } - saveHtml(await publishStory(story.id), storyFileName(story)); + resetErrors(); + + try { + saveHtml(await publishStory(story.id), storyFileName(story)); + } catch (error) { + setPublishError(error as Error); + } + } + + async function handleTest() { + if (!story) { + throw new Error('No story provided to publish'); + } + + resetErrors(); + + try { + await testStory(story.id); + } catch (error) { + setTestError(error as Error); + } } return ( - } label={t('routeActions.build.test')} - onClick={story ? () => testStory(story.id) : () => {}} - /> - setTestError(undefined)} + onClick={handleTest} + open={!!testError} + > + +

{testError?.message}

+ } + label={t('common.close')} + onClick={() => setTestError(undefined)} + variant="primary" + /> +
+
+ } label={t('routeActions.build.play')} - onClick={story ? () => playStory(story.id) : () => {}} - /> - setPlayError(undefined)} + onClick={handlePlay} + open={!!playError} + > + +

{playError?.message}

+ } + label={t('common.close')} + onClick={() => setPlayError(undefined)} + variant="primary" + /> +
+
+ } label={t('routeActions.build.proof')} - onClick={story ? () => proofStory(story?.id) : () => {}} - /> - setProofError(undefined)} + onClick={handleProof} + open={!!proofError} + > + +

{proofError?.message}

+ } + label={t('common.close')} + onClick={() => setProofError(undefined)} + variant="primary" + /> +
+
+ } label={t('routeActions.build.publishToFile')} + onChangeOpen={() => setPublishError(undefined)} onClick={handlePublishFile} - /> + open={!!publishError} + > + +

{publishError?.message}

+ } + label={t('common.close')} + onClick={() => setPublishError(undefined)} + variant="primary" + /> +
+
); }; diff --git a/src/routes/story-play/__tests__/story-play-route.test.tsx b/src/routes/story-play/__tests__/story-play-route.test.tsx index 5a796b02c..5e79a02b1 100644 --- a/src/routes/story-play/__tests__/story-play-route.test.tsx +++ b/src/routes/story-play/__tests__/story-play-route.test.tsx @@ -35,4 +35,16 @@ describe('', () => { ); expect(publishStory.mock.calls).toEqual([['123']]); }); + + it('shows an error message if publishing fails', async () => { + const publishStory = jest.fn( + jest.fn(() => Promise.reject(new Error('mock-error-message'))) + ); + + usePublishingMock.mockReturnValue({publishStory}); + renderComponent('/stories/123/play'); + await waitFor(() => + expect(document.body.textContent).toContain('mock-error-message') + ); + }); }); diff --git a/src/routes/story-play/story-play-route.tsx b/src/routes/story-play/story-play-route.tsx index bb3d193bb..9dc41687f 100644 --- a/src/routes/story-play/story-play-route.tsx +++ b/src/routes/story-play/story-play-route.tsx @@ -2,15 +2,21 @@ import * as React from 'react'; import {useParams} from 'react-router-dom'; import {replaceDom} from '../../util/replace-dom'; import {usePublishing} from '../../store/use-publishing'; +import {ErrorMessage} from '../../components/error'; export const StoryPlayRoute: React.FC = () => { + const [publishError, setPublishError] = React.useState(); const [inited, setInited] = React.useState(false); const {storyId} = useParams<{storyId: string}>(); const {publishStory} = usePublishing(); React.useEffect(() => { async function load() { - replaceDom(await publishStory(storyId)); + try { + replaceDom(await publishStory(storyId)); + } catch (error) { + setPublishError(error as Error); + } } if (!inited) { @@ -19,5 +25,9 @@ export const StoryPlayRoute: React.FC = () => { } }, [inited, publishStory, storyId]); + if (publishError) { + return {publishError.message}; + } + return null; }; diff --git a/src/routes/story-proof/__tests__/story-proof-route.test.tsx b/src/routes/story-proof/__tests__/story-proof-route.test.tsx index 20dbcea27..ffc7ce22e 100644 --- a/src/routes/story-proof/__tests__/story-proof-route.test.tsx +++ b/src/routes/story-proof/__tests__/story-proof-route.test.tsx @@ -35,4 +35,16 @@ describe('', () => { ); expect(proofStory.mock.calls).toEqual([['123']]); }); + + it('shows an error message if publishing fails', async () => { + const proofStory = jest.fn( + jest.fn(() => Promise.reject(new Error('mock-error-message'))) + ); + + usePublishingMock.mockReturnValue({proofStory}); + renderComponent('/stories/123/proof'); + await waitFor(() => + expect(document.body.textContent).toContain('mock-error-message') + ); + }); }); diff --git a/src/routes/story-proof/story-proof-route.tsx b/src/routes/story-proof/story-proof-route.tsx index 2e5e6f64f..51588c2d0 100644 --- a/src/routes/story-proof/story-proof-route.tsx +++ b/src/routes/story-proof/story-proof-route.tsx @@ -2,15 +2,21 @@ import * as React from 'react'; import {useParams} from 'react-router-dom'; import {replaceDom} from '../../util/replace-dom'; import {usePublishing} from '../../store/use-publishing'; +import {ErrorMessage} from '../../components/error'; export const StoryProofRoute: React.FC = () => { + const [publishError, setPublishError] = React.useState(); const [inited, setInited] = React.useState(false); const {storyId} = useParams<{storyId: string}>(); const {proofStory} = usePublishing(); React.useEffect(() => { async function load() { - replaceDom(await proofStory(storyId)); + try { + replaceDom(await proofStory(storyId)); + } catch (error) { + setPublishError(error as Error); + } } if (!inited) { @@ -19,5 +25,9 @@ export const StoryProofRoute: React.FC = () => { } }, [inited, proofStory, storyId]); + if (publishError) { + return {publishError.message}; + } + return null; }; diff --git a/src/routes/story-test/__tests__/story-test-route.test.tsx b/src/routes/story-test/__tests__/story-test-route.test.tsx index db38067b8..b3ee3a951 100644 --- a/src/routes/story-test/__tests__/story-test-route.test.tsx +++ b/src/routes/story-test/__tests__/story-test-route.test.tsx @@ -1,4 +1,4 @@ -import {render, waitFor} from '@testing-library/react'; +import {render, screen, waitFor} from '@testing-library/react'; import {createHashHistory} from 'history'; import * as React from 'react'; import {HashRouter, Route} from 'react-router-dom'; @@ -55,4 +55,16 @@ describe('', () => { ['123', {formatOptions: 'debug', startId: '456'}] ]); }); + + it('shows an error message if publishing fails', async () => { + const publishStory = jest.fn( + jest.fn(() => Promise.reject(new Error('mock-error-message'))) + ); + + usePublishingMock.mockReturnValue({publishStory}); + renderComponent('/stories/123/test/456'); + await waitFor(() => + expect(document.body.textContent).toContain('mock-error-message') + ); + }); }); diff --git a/src/routes/story-test/story-test-route.tsx b/src/routes/story-test/story-test-route.tsx index d5b7505c2..a8400d727 100644 --- a/src/routes/story-test/story-test-route.tsx +++ b/src/routes/story-test/story-test-route.tsx @@ -2,8 +2,10 @@ import * as React from 'react'; import {useParams} from 'react-router-dom'; import {replaceDom} from '../../util/replace-dom'; import {usePublishing} from '../../store/use-publishing'; +import {ErrorMessage} from '../../components/error'; export const StoryTestRoute: React.FC = () => { + const [publishError, setPublishError] = React.useState(); const [inited, setInited] = React.useState(false); const {passageId, storyId} = useParams<{ passageId: string; @@ -13,12 +15,16 @@ export const StoryTestRoute: React.FC = () => { React.useEffect(() => { async function load() { - replaceDom( - await publishStory(storyId, { - formatOptions: 'debug', - startId: passageId - }) - ); + try { + replaceDom( + await publishStory(storyId, { + formatOptions: 'debug', + startId: passageId + }) + ); + } catch (error) { + setPublishError(error as Error); + } } if (!inited) { @@ -27,5 +33,9 @@ export const StoryTestRoute: React.FC = () => { } }, [inited, passageId, publishStory, storyId]); + if (publishError) { + return {publishError.message}; + } + return null; }; From 9734a9beea489735cc5bd01706126641da2f156b Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Tue, 23 Aug 2022 11:19:31 -0400 Subject: [PATCH 18/26] Disable delete button when start passage is selected --- .../passage/__tests__/delete-passages-button.test.tsx | 9 +++++++++ .../toolbar/passage/delete-passages-button.tsx | 11 +++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/routes/story-edit/toolbar/passage/__tests__/delete-passages-button.test.tsx b/src/routes/story-edit/toolbar/passage/__tests__/delete-passages-button.test.tsx index 79d4b729c..9ef6b0ce5 100644 --- a/src/routes/story-edit/toolbar/passage/__tests__/delete-passages-button.test.tsx +++ b/src/routes/story-edit/toolbar/passage/__tests__/delete-passages-button.test.tsx @@ -49,9 +49,18 @@ describe('', () => { expect(screen.getByText('common.delete')).toBeDisabled(); }); + it('is disabled if any of the passages are the start passage', () => { + const story = fakeStory(2); + + story.startPassage = story.passages[1].id; + renderComponent({story, passages: story.passages}); + expect(screen.getByText('common.delete')).toBeDisabled(); + }); + it('deletes passages when clicked', () => { const story = fakeStory(3); + story.startPassage = story.passages[2].id; renderComponent( {story, passages: [story.passages[0], story.passages[1]]}, {stories: [story]} diff --git a/src/routes/story-edit/toolbar/passage/delete-passages-button.tsx b/src/routes/story-edit/toolbar/passage/delete-passages-button.tsx index d569c6dc1..e42cd2720 100644 --- a/src/routes/story-edit/toolbar/passage/delete-passages-button.tsx +++ b/src/routes/story-edit/toolbar/passage/delete-passages-button.tsx @@ -18,6 +18,13 @@ export const DeletePassagesButton: React.FC< const {passages, story} = props; const {dispatch} = useUndoableStoriesContext(); const {t} = useTranslation(); + const disabled = React.useMemo(() => { + if (passages.length === 0) { + return true; + } + + return passages.some(passage => story.startPassage === passage.id); + }, [passages, story.startPassage]); const handleClick = React.useCallback(() => { if (passages.length === 0) { return; @@ -35,10 +42,10 @@ export const DeletePassagesButton: React.FC< return ( } label={ - passages.length > 1 + !disabled && passages.length > 1 ? t('common.deleteCount', {count: passages.length}) : t('common.delete') } From 027e60e084562203e70885254542af887a6028c2 Mon Sep 17 00:00:00 2001 From: Chris Klimas Date: Tue, 23 Aug 2022 11:51:33 -0400 Subject: [PATCH 19/26] Link passages to existing story when importing Resolves #1238 --- .../__tests__/import-stories.test.ts | 44 +++++++++++++++++-- .../stories/action-creators/import-stories.ts | 10 ++++- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/store/stories/action-creators/__tests__/import-stories.test.ts b/src/store/stories/action-creators/__tests__/import-stories.test.ts index 43c26fb71..4e700ca86 100644 --- a/src/store/stories/action-creators/__tests__/import-stories.test.ts +++ b/src/store/stories/action-creators/__tests__/import-stories.test.ts @@ -1,5 +1,5 @@ import {importStories} from '../import-stories'; -import {Story} from '../../stories.types'; +import {Passage, Story} from '../../stories.types'; import {fakeStory} from '../../../../test-util'; describe('importStories action creator', () => { @@ -47,8 +47,32 @@ describe('importStories action creator', () => { delete (toImport[1] as any).id; importStories(toImport, state)(dispatch, () => state); expect(dispatch.mock.calls).toEqual([ - [{type: 'updateStory', storyId: state[0].id, props: toImport[0]}], - [{type: 'updateStory', storyId: state[1].id, props: toImport[1]}] + [ + { + type: 'updateStory', + storyId: state[0].id, + props: { + ...toImport[0], + passages: toImport[0].passages.map(passage => ({ + ...passage, + story: state[0].id + })) + } + } + ], + [ + { + type: 'updateStory', + storyId: state[1].id, + props: { + ...toImport[1], + passages: toImport[1].passages.map(passage => ({ + ...passage, + story: state[1].id + })) + } + } + ] ]); }); @@ -57,6 +81,20 @@ describe('importStories action creator', () => { expect(dispatch.mock.calls[0][0].props.id).not.toBeDefined(); expect(dispatch.mock.calls[1][0].props.id).not.toBeDefined(); }); + + it('links passages to the existing story', () => { + importStories(toImport, state)(dispatch, () => state); + expect( + dispatch.mock.calls[0][0].props.passages.every( + (passage: Passage) => passage.story === state[0].id + ) + ).toBe(true); + expect( + dispatch.mock.calls[1][0].props.passages.every( + (passage: Passage) => passage.story === state[1].id + ) + ).toBe(true); + }); }); it('throws an error if more than one story to import has the same name', () => { diff --git a/src/store/stories/action-creators/import-stories.ts b/src/store/stories/action-creators/import-stories.ts index d6ea04d31..235aaa517 100644 --- a/src/store/stories/action-creators/import-stories.ts +++ b/src/store/stories/action-creators/import-stories.ts @@ -41,9 +41,17 @@ export function importStories( ); // Do an update so that if something goes awry, we won't have deleted the - // story. + // story. We need to update passage props so that their parent story ID is + // set properly. if (existingStory) { + if (props.passages) { + props.passages = props.passages.map(passage => ({ + ...passage, + story: existingStory.id + })); + } + dispatch({props, type: 'updateStory', storyId: existingStory.id}); } else { dispatch({props, type: 'createStory'}); From ef3af4ae61c14bd3c275dfd78b823222f3983d36 Mon Sep 17 00:00:00 2001 From: Leon Date: Thu, 25 Aug 2022 15:27:16 +1000 Subject: [PATCH 20/26] Updated Harlowe to 3.3.2 --- public/story-formats/harlowe-3.3.1/format.js | 2 -- public/story-formats/harlowe-3.3.2/format.js | 4 ++++ .../story-formats/{harlowe-3.3.1 => harlowe-3.3.2}/icon.svg | 0 src/store/prefs/defaults.ts | 2 +- src/store/story-formats/defaults.ts | 4 ++-- 5 files changed, 7 insertions(+), 5 deletions(-) delete mode 100644 public/story-formats/harlowe-3.3.1/format.js create mode 100644 public/story-formats/harlowe-3.3.2/format.js rename public/story-formats/{harlowe-3.3.1 => harlowe-3.3.2}/icon.svg (100%) diff --git a/public/story-formats/harlowe-3.3.1/format.js b/public/story-formats/harlowe-3.3.1/format.js deleted file mode 100644 index a8dea51a0..000000000 --- a/public/story-formats/harlowe-3.3.1/format.js +++ /dev/null @@ -1,2 +0,0 @@ -window.storyFormat({"name":"Harlowe","version":"3.3.1","author":"Leon Arnott","description":"The default story format for Twine 2, with numerous programming features and a rich passage editor. No HTML, JS or CSS experience required. Consult its documentation.","image":"icon.svg","url":"http://twinery.org/","license":"Zlib","proofing":false,"source":"\n\n\n\n\n{{STORY_NAME}}\n\n\n\n\n{{STORY_DATA}}\n\n\n\n\n","setup": function(){"use strict";function _createForOfIteratorHelper(a,t){var r,o="undefined"!=typeof Symbol&&a[Symbol.iterator]||a["@@iterator"];if(!o){if(Array.isArray(a)||(o=_unsupportedIterableToArray(a))||t&&a&&"number"==typeof a.length)return o&&(a=o),r=0,{s:t=function F(){},n:function n(){return r>=a.length?{done:!0}:{done:!1,value:a[r++]}},e:function e(a){throw a},f:t};throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,c=!0,l=!1;return{s:function s(){o=o.call(a)},n:function n(){var e=o.next();return c=e.done,e},e:function e(a){l=!0,i=a},f:function f(){try{c||null==o.return||o.return()}finally{if(l)throw i}}}}function _toArray(e){return _arrayWithHoles(e)||_iterableToArray(e)||_unsupportedIterableToArray(e)||_nonIterableRest()}function _slicedToArray(e,a){return _arrayWithHoles(e)||_iterableToArrayLimit(e,a)||_unsupportedIterableToArray(e,a)||_nonIterableRest()}function _nonIterableRest(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function _iterableToArrayLimit(e,a){var t=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=t){var r,o,n=[],i=!0,s=!1;try{for(t=t.call(e);!(i=(r=t.next()).done)&&(n.push(r.value),!a||n.length!==a);i=!0);}catch(e){s=!0,o=e}finally{try{i||null==t.return||t.return()}finally{if(s)throw o}}return n}}function _arrayWithHoles(e){if(Array.isArray(e))return e}function _toConsumableArray(e){return _arrayWithoutHoles(e)||_iterableToArray(e)||_unsupportedIterableToArray(e)||_nonIterableSpread()}function _nonIterableSpread(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function _unsupportedIterableToArray(e,a){if(e){if("string"==typeof e)return _arrayLikeToArray(e,a);var t=Object.prototype.toString.call(e).slice(8,-1);return"Map"===(t="Object"===t&&e.constructor?e.constructor.name:t)||"Set"===t?Array.from(e):"Arguments"===t||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t)?_arrayLikeToArray(e,a):void 0}}function _iterableToArray(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}function _arrayWithoutHoles(e){if(Array.isArray(e))return _arrayLikeToArray(e)}function _arrayLikeToArray(e,a){(null==a||a>e.length)&&(a=e.length);for(var t=0,r=new Array(a);t=r.length)&&!u.isFront)continue}n=this.end)return null;if(this.children.length)for(var a=0;a=this.end)return[];var a=[];if(this.children.length)for(var t=0;t=this.end?null:this.children?this.children.reduce(function(e,a){return e||(t>=a.start&&t|<=+|=+><=+|<==+>)"+o+l,l=o+"(=+\\|+|\\|+=+|=+\\|+=+|\\|=+\\|)"+o+l,p={opener:"\\[\\[(?!\\[)",text:"("+function notChars(){return"[^"+Array.apply(0,arguments).map(escape).join("")+"]*"}("]")+")",rightSeparator:a("\\->","\\|"),leftSeparator:"<\\-",closer:"\\]\\]",legacySeparator:"\\|",legacyText:"("+a("[^\\|\\]]","\\]"+t("\\]"))+"+)"},g=c+"*"+c.replace("\\w","a-zA-Z")+c+"*",b="\\$("+g+")",y="_("+g+")",f="'s"+n+"("+g+")",w="("+g+")"+n+"of"+i+t("it\\b"),k="'s"+n,v=a("it","time","turns?","visits?","exits?","pos")+i,x="its"+n+"("+g+")",T="("+g+")"+n+"of"+n+"it"+i,C="of"+n+"it"+i,S={opener:"\\(",name:"("+a("\\$","_")+"?"+s+"+):"+t("\\/"),closer:"\\)"},A=a("=<","=>","[gl]te?\\b","n?eq\\b","isnot\\b","are\\b","x\\b","isa\\b","or"+n+"a"+i),N="[a-zA-Z][\\w\\-]*",_="(?:\"[^\"]*\"|'[^']*'|[^'\">])*?",O="\\|("+s+"+)(>|\\))",L="(<|\\()("+s+"+)\\|",P="((?:\\b\\d+(?:\\.\\d+)?|\\.\\d+)(?:[eE][+\\-]?\\d+)?)"+t("m?s")+i;p.main=p.opener+a(p.text+p.rightSeparator,p.text.replace("*","*?")+p.leftSeparator)+p.text,e={upperLetter:"[A-Z\\u00c0-\\u00de\\u0150\\u0170]",lowerLetter:"[a-z0-9_\\-\\u00df-\\u00ff\\u0151\\u0171]",anyLetter:s,anyLetterStrict:c,whitespace:n.replace("[","[\\n\\r"),escapedLine:"\\\\\\n\\\\?|\\n\\\\",br:"\\n(?!\\\\)",tag:"<\\/?"+N+_+">",scriptStyleTag:"<("+a("script","style","textarea")+")"+_+">[^]*?<\\/\\1>",scriptStyleTagOpener:"<",url:"("+a("https?","mailto","javascript","ftp","data")+":\\/\\/[^\\s<]+[^<.,:;\"')\\]\\s])",bullet:"\\*",hr:m,heading:"[ \\f\\t\\v\\u00a0\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000]*(#{1,6})[ \\f\\t\\v\\u00a0\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000]*",align:u,column:l,bulleted:h,numbered:d,verbatimOpener:"`+",hookAppendedFront:"\\["+t("=+"),hookPrependedFront:O+"\\["+t("=+"),hookFront:"\\["+t("=+"),hookBack:"\\]"+t(L),hookAppendedBack:"\\]"+L,unclosedHook:"\\[=+",unclosedHookPrepended:O+"\\[=+",unclosedCollapsed:"\\{=+",passageLink:p.main+p.closer,legacyLink:p.opener+p.legacyText+p.legacySeparator+p.legacyText+p.closer,simpleLink:p.opener+p.legacyText+p.closer,macroFront:S.opener+r(S.name),macroName:S.name,groupingFront:"\\("+t(S.name),twine1Macro:"<<[^>\\s]+\\s*(?:\\\\.|'(?:[^'\\\\]*\\\\.)*[^'\\\\]*'|\"(?:[^\"\\\\]*\\\\.)*[^\"\\\\]*\"|[^'\"\\\\>]|>(?!>))*>>",validPropertyName:g,property:f,belongingProperty:w,possessiveOperator:k,belongingOperator:"of\\b",itsOperator:"its\\b",belongingItOperator:C,variable:b,tempVariable:y,hookName:"\\?("+s+"+)\\b",cssTime:"(\\d+\\.?\\d*|\\d*\\.?\\d+)(m?s)\\b",colour:a(a("Red","Orange","Yellow","Lime","Green","Cyan","Aqua","Blue","Navy","Purple","Fuchsia","Magenta","White","Gray","Grey","Black","Transparent"),"#[\\dA-Fa-f]{3}(?:[\\dA-Fa-f]{3})?"),datatype:a("alnum","alphanumeric","any(?:case)?","array","bool(?:ean)?","changer","codehook","colou?r","const","command","dm","data"+a("map","type","set"),"ds","digit","gradient","empty","even","int"+t("o")+"(?:eger)?","lambda","lowercase","macro","linebreak","newline","num(?:ber)?","odd","str(?:ing)?","uppercase","whitespace")+i,number:P,boolean:a("true","false")+i,identifier:v,itsProperty:x,belongingItProperty:T,escapedStringChar:"\\\\[^\\n]",singleStringOpener:"'",doubleStringOpener:'"',singleStringCloser:"'",doubleStringCloser:'"',is:"is"+t(n+"not"+i,n+"an?"+i,n+"in"+i,n+"<",n+">")+i,isNot:"is"+n+"not"+t(n+a("an?","in")+i)+i,isA:"is"+n+"an?"+i,isNotA:"is"+n+"not"+n+"an?"+i,matches:"matches\\b",doesNotMatch:"does"+n+"not"+n+"match"+i,and:"and\\b",or:"or\\b",not:"not\\b",inequality:"((?:is(?:"+n+"not)?"+o+")*)("+a("<(?!=)","<=",">(?!=)",">=")+")",isIn:"is"+n+"in"+i,contains:"contains\\b",doesNotContain:"does"+n+"not"+n+"contain"+i,isNotIn:"is"+n+"not"+n+"in"+i,addition:escape("+")+t("="),subtraction:escape("-")+t("=","type"),multiplication:escape("*")+t("="),division:a("/","%")+t("="),spread:"\\.\\.\\."+t("\\."),to:a("to\\b","="),into:"into\\b",making:"making\\b",where:"where\\b",when:"when\\b",via:"via\\b",each:"each\\b",augmentedAssign:a("\\+","\\-","\\*","\\/","%")+"=",bind:"2?bind\\b",typeSignature:escape("-type")+i,incorrectOperator:A,PlainCompare:{comma:",",commentFront:"\x3c!--",commentBack:"--\x3e",strikeOpener:"~~",italicOpener:"//",boldOpener:"''",supOpener:"^^",strongFront:"**",strongBack:"**",emFront:"*",emBack:"*",collapsedFront:"{",collapsedBack:"}",groupingBack:")"}},"object"===("undefined"==typeof module?"undefined":_typeof(module))?module.exports=e:"function"==typeof define&&define.amd?define("patterns",[],function(){return e}):this&&this.loaded?(this.modules||(this.modules={}),this.modules.Patterns=e):this.Patterns=e}.call(eval("this")||("undefined"!=typeof global?global:window)),!function(){Object.assign=Object.assign||function polyfilledAssign(e){for(var a=1;a<");return~t?25===(a=Math.round(t/(e.length-2)*50))&&(a="center"):"<"===e[0]&&">"===e.slice(-1)?a="justify":-1")?a="right":-1":">=","=<":"<=",gte:">=",lte:"<=",gt:">",lt:"<",eq:"is",isnot:"is not",neq:"is not",isa:"is a",are:"is",x:"*","or a":"or"}[e[0].toLowerCase().replace(/\s+/g," ")];return{type:"error",message:"Please say "+(a?"'"+a+"'":"something else")+" instead of '"+e[0]+"'.",explanation:"In the interests of readability, I want certain operators to be in a specific form."}},cannotFollowText:!0}},["boolean","is","to","into","where","when","via","making","each","and","or","not","isNot","contains","doesNotContain","isIn","isA","isNotA","isNotIn","matches","doesNotMatch","bind"].reduce(function(e,a){return e[a]={fn:t,cannotFollowText:!0},e},{}),["comma","spread","typeSignature","addition","subtraction","multiplication","division"].reduce(function(e,a){return e[a]={fn:t},e},{}))),h=setupRules(o,{singleStringCloser:l.singleStringOpener,doubleStringCloser:l.doubleStringOpener,escapedStringChar:l.escapedStringChar}),d=(r.push.apply(r,_toConsumableArray(u(n)).concat(_toConsumableArray(u(c)),_toConsumableArray(u(s)))),a.push.apply(a,_toConsumableArray(u(c)).concat(_toConsumableArray(u(l)))),o.push.apply(o,_toConsumableArray(u(h))),p({},n,s,c,l,h));return u(d).forEach(function(e){m.PlainCompare[e]?(d[e].pattern=m.PlainCompare[e],d[e].plainCompare=!0):d[e].pattern=RegExp("^(?:"+m[e]+")","i")}),p(e.rules,d),(s=e.modes).start=s.markup=r,s.macro=a,s.string=o,e}(e).lex,Patterns:m})}"object"===("undefined"==typeof module?"undefined":_typeof(module))?(m=require("./patterns"),module.exports=exporter(require("./lexer"))):"function"==typeof define&&define.amd?define("markup",["lexer","patterns"],function(e,a){return m=a,exporter(e)}):this&&this.loaded&&this.modules?(m=this.modules.Patterns,this.modules.Markup=exporter(this.modules.Lexer)):(m=this.Patterns,this.Markup=exporter(this.Lexer))}.call(eval("this")||("undefined"!=typeof global?global:window)),!function(){var a=Math.round,e=function insensitiveName(e){return(e+"").toLowerCase().replace(/-|_/g,"")},t={"#e61919":"red","#e68019":"orange","#e5e619":"yellow","#80e619":"lime","#19e619":"green","#19e5e6":"cyan","#197fe6":"blue","#1919e6":"navy","#7f19e6":"purple","#e619e5":"magenta","#ffffff":"white","#000000":"black","#888888":"grey"},r=function fontIcon(e){var a=1')},o=function GCD(e,a){return e?a?a
",model:function model(e,a){r(e,"(a:"+a+")")},renumber:function renumber(e,a){var t=((a+=1)+"").slice(-1);e.textContent=a+("1"===t?"st":"2"===t?"nd":"3"===t?"rd":"th")+": "},modelRegistry:e}],["datamap",{type:"datavalue-map",text:"
A datamap is a value that holds any number of other values, each of which is \"mapped\" to a unique name. Use it to store data values that represent parts of a larger game entity, or rows of a table.
",model:function model(e,a){a.every(function(e,a){return a%2!=0||'""'!==e&&"''"!==e})||(e.invalidSubrow=!0),r(e,"(dm:"+a+")")},renumber:function renumber(e){e.textContent=":"},modelRegistry:e}],[],["randomly chosen value",{type:"datavalue-rows",text:"
One of the following values is randomly chosen each time the macro is run.
",model:function model(e,a){a.length||(e.invalidSubrow=!0),r(e,"(either:"+a+")")},renumber:function renumber(e){e.textContent="\u2022",e.style.marginRight="0.5em"},modelRegistry:e}],["random number",z("
A number between these two values is randomly chosen each time the macro is run.
"),{type:"inline-number-textarea",width:"20%",text:"From",model:function model(){},modelRegistry:e},{type:"inline-number-textarea",width:"20%",text:"to",model:function model(e,a){r(e,"(random:"+(+a.previousElementSibling[W]("input").value||0)+","+(+a[W]("input").value||0)+")")},modelRegistry:e}]].concat(a?[]:[[],["itself + value",{type:"datavalue-inner",text:"
The following value is added to the existing value in the variable. NOTE: if the values aren't the same type of data, an error will result.
",model:function model(e,a){1!==a.length&&(e.invalidSubrow=!0),r(e,"it + "+a)},renumber:function renumber(e){(e||{}).textContent="it + "},modelRegistry:e}],["variable + value",{type:"inline-dropdown",text:" Other variable: ",options:["$","_"],model:function model(e,a){e.innerVariable=a[W]("select").value?"_":"$"}},{type:"inline-textarea",width:"25%",text:"",placeholder:"Variable name",model:function model(e,a){a=a[W]("input").value;a&&RegExp("^"+t.validPropertyName+"$").exec(a)&&!RegExp(/^\d/).exec(a)&&(e.innerVariable+=a)}},{type:"datavalue-inner",text:"
The above variable's value and the value below are added together. NOTE: if the values aren't the same type of data, an error will result.
",model:function model(e,a){1!==a.length&&(e.invalidSubrow=!0),r(e,e.innerVariable+" + "+a)},renumber:function renumber(e){(e||{}).textContent=""},modelRegistry:e}]]).concat([[],["coded expression",{type:"expression-textarea",width:"90%",text:"
Write a Harlowe code expression that should be computed to produce the desired value.
",placeholder:"Code",modelCallback:r,modelRegistry:e}]])}};H.Panel=function folddownPanel(){for(var e=arguments.length,a=new Array(e),t=0;t')),O=function makeColourPicker(e){var t=function makeSwatchRow(e,a,t){return z('")+e.map(function(e){return''}).join("")+"")},r=z('
'),e=z('')),o=e[W]("select");return r.append(t(Object.keys(n),"",!0)),i.forEach(function(e,a){r.append(t(e,a)),o.append(z("")))}),o[q]("change",function(){r[B]("[data-index]").forEach(function(e){return e.style.display="none"}),r[W]('[data-index="'.concat(o.value,'"]')).style.display="inline-block"}),r.append(e,z("
"),new Text("Opacity: "),z('')),r[q]("click",function(e){var a,e=e.target;e.classList.contains("harlowe-3-swatch")&&((a=r[W]("input")).value=e.getAttribute("style").slice(-7),a.dispatchEvent(new Event("change")))}),r},e=a.reduce(function reducer(o,t){var e,n,r,i,a,s,c,l,h,d,m,u,p,g,b,y,f,w,k,v,x,T,C="",S="label"===o.tagName.toLowerCase();return Object.getPrototypeOf(t)!==Object.prototype?n=t:k=(C=t.type).startsWith("inline"),C.endsWith("text")&&(n=z("<".concat(k?"span":"div",">").concat(t.text,""))),"notice"===C&&(n=z(''.concat(t.text,""))),"buttons"===C&&o.append.apply(o,_toConsumableArray(t.buttons.map(function(e){if("tagName"in e)return e;var a=z('"));return e.onClick&&a[q]("click",e.onClick),a}))),C.endsWith("preview")&&(v=t.tagName||"span",n=z('
<").concat(v,">").concat(t.text||"","").concat(C.startsWith("t8n")?"<".concat(v,">")+t.text2+""):"","
")),C.startsWith("t8n")&&(n[q]("mouseup",update),n[q]("touchend",update),(e=n[W](":first-child"))[q]("animationend",function(){return e.style.visibility="hidden"}))),(C.endsWith("checkbox")||C.endsWith("checkboxrow"))&&(n=z("').concat(t.text,"")),C.endsWith("w")&&(t.subrow.reduce(reducer,n),t.subrow.forEach(function(e){var t=e.model;e.model=function(e,a){return!n[W](":scope > input:checked")||S&&!o[W](":scope > input:checked")?e:t(e,a)}})),n[q]("change",update)),"checkboxes"===C&&(n=z('
").concat(t.name,"
")),t.options.forEach(function(e){e=z("').concat(e,""));e[q]("change",update),n.append(e)})),"radios"===C&&(n=z('
").concat(t.name,"
")),t.options.forEach(function(e,a){a=z("").concat(e,""));a[q]("change",update),n.append(a)})),C.endsWith("textarea")&&(v="text",r=t.multiline?"textarea":"input",C.endsWith("expression-textarea")&&(t.update=function(e,a){!e.expression&&a[W](r).value?a.setAttribute("invalid","This doesn't seem to be valid code."):a.removeAttribute("invalid")},t.model=function(e,a){a=(a[W](r).value||"").trim();a&&(I(a,"","macro").children.every(function recur(e){return"text"!==e.type&&"error"!==e.type&&("string"===e.type||"hook"===e.type||e.children.every(recur))})?t.modelCallback(e,a):e.valid=!0)}),C.endsWith("string-textarea")&&!t.model&&(t.model=function(e,a){t.modelCallback(e,JSON.stringify(a[W](r).value||""))}),C.endsWith("number-textarea")&&(v="number",t.model||(t.model=function(e,a){t.modelCallback(e,+a[W](r).value||0)})),(n=z("<".concat(k?"span":"div",' class="harlowe-3-labeledInput">').concat(t.text,"<").concat(r," ").concat(t.useSelection?"data-use-selection":"").concat(C.includes("passage")?'list="harlowe-3-passages"':"",' style="width:').concat(t.width,";").concat(t.multiline?"max-width:".concat(t.width,";"):"","padding:var(--grid-size);margin").concat(k?":2px 0.5rem 0 0.5rem":"-left:1rem",";").concat(t.multiline&&k?"display:inline-block;height:40px":"",'" type=').concat(v,' placeholder="').concat(t.placeholder||"",'">")))[W](r)[q]("input",update)),(C.endsWith("number")||C.endsWith("range"))&&(n=z("<"+(k?"span":"div")+' class="harlowe-3-labeledInput">'+t.text+''+t.text+""),v=O(t.value),n.append(v),n[B]("input").forEach(function(e){return e[q]("change",update)})),C.endsWith("gradient")&&(i=(n=z("
")))[W](".harlowe-3-gradientBar"),a=function createColourStop(e,a,t){var r=z("
')+"
"),o=O(a),t=(o[B]("input").forEach(function(e){return e[q]("change",function(){var e=o[W]("[type=range]").value,a=o[W]("[type=color]").value;r.setAttribute("data-colour",F(a,e)),e<1&&r.setAttribute("data-harlowe-colour",M(a,e)),update()})}),z('")));t[q]("click",function(){r.remove(),update()}),o.append(t),r.firstChild.prepend(o),i.append(r),update()},setTimeout(function(){a(0,"#ffffff"),a(.5,"#000000",!0),a(1,"#ffffff")}),n[W]("button")[q]("click",function(){return a(.5,"#888888")}),n[q]("mousedown",v=function listener(e){var a,t,r,o,n=e.target;n.classList.contains("harlowe-3-colourStop")&&(a=document.documentElement,e=i.getBoundingClientRect(),t=e.left,r=e.right-t,e=function onMouseUp(){a[j]("mousemove",o),a[j]("mouseup",onMouseUp),a[j]("touchmove",o),a[j]("touchend",onMouseUp)},a[q]("mousemove",o=function onMouseMove(e){var a=e.pageX,e=e.touches;void 0!==(a=a||e&&e[0].pageX)&&(e=Math.min(1,Math.max(0,(a-window.scrollX-t)/r)),n.style.left="calc(".concat(100*e,"% - 8px)"),n.firstChild.style.left="".concat(-(R?464:384)*e-(R?0:40),"px"),n.setAttribute("data-pos",e),update())}),a[q]("mouseup",e),a[q]("touchmove",o),a[q]("touchend",e),Array.from(i[B]("[selected]")).forEach(function(e){return e.removeAttribute("selected")}),n.setAttribute("selected",!0))}),n[q]("touchstart",v),N.push(function(){i.style.background="linear-gradient(to right, ".concat(Array.from(i[B](".harlowe-3-colourStop")).sort(function(e,a){return e.getAttribute("data-pos")-a.getAttribute("data-pos")}).map(function(e){return e.getAttribute("data-colour")+" "+100*e.getAttribute("data-pos")+"%"}),")")})),C.endsWith("dropdown")&&(s=z("<"+(k?"span":"div")+' style="white-space:nowrap;'+(k?"":"width:50%;")+'position:relative;">'+t.text+'"),t.options.forEach(function(e,a){s[W]("select").append(z('