diff --git a/cypress/e2e/demos-of-tutorials.cy.ts b/cypress/e2e/demos-of-tutorials.cy.ts index e4b3f53c4..1f8676e14 100644 --- a/cypress/e2e/demos-of-tutorials.cy.ts +++ b/cypress/e2e/demos-of-tutorials.cy.ts @@ -5,6 +5,19 @@ context("Demos of all tutorials", () => { cy.get("ul.tutorial-list li").should("have.length", kExpNTutorials); } + beforeEach(() => { + cy.pytchResetDatabase({ initialUrl: "/tutorials/" }); + assertNTutorials(); + }); + + function launchNthTutorial(tutorialIndex: number) { + const childNumber = tutorialIndex + 1; + cy.get( + `ul.tutorial-list li:nth-child(${childNumber})` + + ' button[title="Learn how to make this project"]' + ).click(); + } + function launchNthTutorialDemo(tutorialIndex: number) { const childNumber = tutorialIndex + 1; cy.get( @@ -13,10 +26,16 @@ context("Demos of all tutorials", () => { ).click(); } - it("can run demos", () => { - cy.pytchResetDatabase({ initialUrl: "/tutorials/" }); - assertNTutorials(); + it("can start tutorials", () => { + for (let tutIdx = 0; tutIdx !== kExpNTutorials; ++tutIdx) { + launchNthTutorial(tutIdx); + cy.get(".ActivityContent .ProgressTrail").should("be.visible"); + cy.pytchHomeFromIDE(); + cy.get(".NavBar li").contains("Tutorials").click(); + } + }); + it("can run demos", () => { for (let tutIdx = 0; tutIdx !== kExpNTutorials; ++tutIdx) { launchNthTutorialDemo(tutIdx); cy.contains("Click the green flag to run"); diff --git a/cypress/e2e/junior/actor-assets.cy.ts b/cypress/e2e/junior/actor-assets.cy.ts index 11ece6e7c..db7751757 100644 --- a/cypress/e2e/junior/actor-assets.cy.ts +++ b/cypress/e2e/junior/actor-assets.cy.ts @@ -154,6 +154,15 @@ context("Working with assets of an actor", () => { assertCostumeNames(allCostumes); }); + it("asset with uppercase filename", () => { + const assetFilename = "RECTANGLE-RED-80-60.PNG"; + + selectSprite("Snake"); + selectActorAspect("Costumes"); + addFromFixture(assetFilename); + assertCostumeNames(["python-logo.png", assetFilename]); + }); + it("has useful UI text for uploading", () => { const assertContentCorrect = (headerMatch: string, bodyMatch: string) => { launchAdd.assetFromThisDevice(); diff --git a/cypress/e2e/junior/lesson.cy.ts b/cypress/e2e/junior/lesson.cy.ts index 6de4da39d..a305f69e7 100644 --- a/cypress/e2e/junior/lesson.cy.ts +++ b/cypress/e2e/junior/lesson.cy.ts @@ -1,8 +1,13 @@ import { DiffViewKind, PrettyPrintedLine } from "../../../src/model/code-diff"; import { LinkedJrTutorialRef } from "../../../src/model/junior/jr-tutorial"; -import { assertInIDE, withDownloadedZipfile } from "../utils"; +import { + assertInIDE, + assertShowsLinkedContentError, + withDownloadedZipfile, +} from "../utils"; import { assertActorNames, + assertJrTutChapterNumber, assertTwoStateSwitchState, clickUniqueSelected, getActivityBarTab, @@ -73,30 +78,17 @@ context("Navigation of per-method lesson", () => { ).click(); } - function assertChapterNumber(expNumber: number) { - if (expNumber === 0) { - cy.get(".chapter-title").should("be.visible"); - cy.get(".chapter-title .chapter-number").should("not.exist"); - return; - } - - cy.get(".chapter-title .chapter-number").should( - "have.text", - `${expNumber} —` - ); - } - it("can move through chapters", () => { for (let i = 0; i !== 5; ++i) { advanceToNextChapter(i); const expChapter = i + 1; - assertChapterNumber(expChapter); + assertJrTutChapterNumber(expChapter); } // Jump directly back one at a time until chapter 1. for (let i = 4; i !== 0; --i) { jumpToChapter(i); - assertChapterNumber(i); + assertJrTutChapterNumber(i); } }); @@ -135,7 +127,7 @@ context("Navigation of per-method lesson", () => { settleModalDialog("Make a copy"); cy.title().should("match", /Pytch: Copy of/); - assertChapterNumber(3); + assertJrTutChapterNumber(3); cy.get('.LearnerTask[data-task-index="6"][data-task-kind="current"]'); // help-sidebar, lesson, keynav-help-sidebar @@ -185,13 +177,13 @@ context("Navigation of per-method lesson", () => { for (let i = 0; i !== 5; ++i) { advanceToNextChapter(i); } - assertChapterNumber(5); + assertJrTutChapterNumber(5); cy.pytchSwitchProject("LESSON-LINKED-1"); - assertChapterNumber(0); + assertJrTutChapterNumber(0); cy.pytchSwitchProject("LESSON-LINKED-0"); - assertChapterNumber(5); + assertJrTutChapterNumber(5); }); }); @@ -331,7 +323,7 @@ context("Navigation of per-method lesson", () => { assertHelpVisible(); getActivityBarTab("book").click(); - assertChapterNumber(3); + assertJrTutChapterNumber(3); assertNoHelp(); getActivityBarTab("circle-question").click(); @@ -357,3 +349,20 @@ context("launch demo from tutorial card", () => { assertActorNames(["Stage", "Bowl", "Apple", "ScoreKeeper"]); }); }); + +context("rejects wrong program-kind", () => { + beforeEach(() => { + cy.pytchResetDatabase(); + cy.contains("My projects").click(); + }); + + it("flat project but per-method tutorial", () => { + cy.pytchTryUploadZipfiles(["v4-flat-linked-to-jr-tutorial.zip"]); + + // The error is caught in two places. To see the message we have to + // explicitly select the "lesson" activity. + cy.get('button[data-activity-bar-tab="lesson"]').click(); + + assertShowsLinkedContentError(/project.*flat.*tutorial.*per-method/); + }); +}); diff --git a/cypress/e2e/junior/utils.ts b/cypress/e2e/junior/utils.ts index ee6a532f8..11dbef624 100644 --- a/cypress/e2e/junior/utils.ts +++ b/cypress/e2e/junior/utils.ts @@ -8,7 +8,7 @@ import { deIndent } from "../../common/utils"; import { IconName } from "@fortawesome/fontawesome-common-types"; import { AceControllerMap } from "../../../src/skulpt-connection/code-editor"; -import { launchProjectInListDropdownAction } from "../utils"; +import { assertInIDE, launchProjectInListDropdownAction } from "../utils"; import { Actions } from "easy-peasy"; import { IActiveProject } from "../../../src/model/project"; import { assertNever, range } from "../../../src/utils"; @@ -746,3 +746,21 @@ export const assertTwoStateSwitchState = ( const expStateStr = expState.toString(); cy.get(selector).should("have.attr", "aria-checked", expStateStr); }; + +/** Assert that a "per-method" tutorial is being displayed, showing the + * chapter with the given `expChapterNumber`. */ +export function assertJrTutChapterNumber(expChapterNumber: number) { + assertInIDE("per-method"); + + cy.get(".Junior-LessonContent-HeaderBar .chapter-title").as("title"); + + if (expChapterNumber === 0) { + cy.get("@title").should("be.visible"); + cy.get("@title").find(".chapter-number").should("not.exist"); + return; + } + + cy.get("@title") + .find(".chapter-number") + .should("have.text", `${expChapterNumber} —`); +} diff --git a/cypress/e2e/keyboard-navigation/global-steer-focus.cy.ts b/cypress/e2e/keyboard-navigation/global-steer-focus.cy.ts new file mode 100644 index 000000000..c14040754 --- /dev/null +++ b/cypress/e2e/keyboard-navigation/global-steer-focus.cy.ts @@ -0,0 +1,234 @@ +import { selectActorAspect, selectSprite, selectStage } from "../junior/utils"; +import { assertInIDE } from "../utils"; +import { + activateFlatAsset, + assertFocus, + KeyOrShortcut, + realPress, +} from "./utils"; + +context("Global focus steering shortcuts", () => { + // These tests are a bit scrappy, ad-hoc, and near-duplicative, but it + // didn't quite seem worth the trouble to try to factor out the common + // behaviour. + + beforeEach(() => { + cy.pytchResetDatabase(); + }); + + function invokeFocusShortcut(key: KeyOrShortcut) { + realPress("g"); + realPress(key); + } + + context("flat IDE", () => { + it("specimen link", () => { + cy.intercept("GET", "**/_by_content_hash_/1234.zip", { + fixture: "lesson-specimens/hello-world-lesson.zip", + }); + cy.pytchTryUploadZipfiles(["v4-flat-linked-to-specimen.zip"]); + assertInIDE("flat"); + + cy.get('button[data-activity-bar-tab="helpsidebar"]').click(); + realPress("ArrowDown"); + realPress("Enter"); + + invokeFocusShortcut("c"); + assertFocus("flat-code-editor"); + realPress("Escape"); + + invokeFocusShortcut("h"); + assertFocus("specimen-info"); + }); + + it("tutorial link", () => { + cy.pytchProjectFollowingTutorial(); + assertInIDE("flat"); + activateFlatAsset(0); + + invokeFocusShortcut("h"); + assertFocus("tutorial-content"); + + invokeFocusShortcut("a"); + assertFocus("flat-asset", 0); + realPress("ArrowDown", 4); + assertFocus("flat-asset", 4); + + invokeFocusShortcut("h"); + assertFocus("tutorial-content"); + + cy.get('button[data-activity-bar-tab="helpsidebar"]') + .as("helpButton") + .click(); + + invokeFocusShortcut("h"); + assertFocus("help-sidebar", [0]); + + realPress("ArrowDown", 3); + realPress("Enter"); + realPress("ArrowDown", 3); + assertFocus("help-sidebar", [3, 2]); + + invokeFocusShortcut("a"); + assertFocus("flat-asset", 4); + + invokeFocusShortcut("h"); + assertFocus("help-sidebar", [3, 2]); + + cy.get("@helpButton").click(); + invokeFocusShortcut("c"); + assertFocus("flat-code-editor"); + realPress("Escape"); + invokeFocusShortcut("h"); + assertFocus("activity-tab", "helpsidebar"); + + realPress("End"); + assertFocus("activity-tab", "keynavhelp"); + invokeFocusShortcut("a"); + assertFocus("flat-asset", 4); + invokeFocusShortcut("h"); + assertFocus("activity-tab", "keynavhelp"); + + realPress("Enter"); + realPress("Tab"); + assertFocus("keynav-help"); + + invokeFocusShortcut("a"); + assertFocus("flat-asset", 4); + invokeFocusShortcut("h"); + assertFocus("keynav-help"); + }); + }); + + context("per-method IDE", () => { + it("specimen link", () => { + cy.intercept("GET", "**/_by_content_hash_/1234.zip", { + fixture: "lesson-specimens/per-method-blue-invaders.zip", + }); + cy.pytchTryUploadZipfiles(["v4-jr-linked-to-specimen.zip"]); + assertInIDE("per-method"); + + selectStage(); + assertFocus("actor-card", 0); + + cy.get('button[data-activity-bar-tab="helpsidebar"]').click(); + realPress("ArrowDown"); + realPress("Enter"); + + invokeFocusShortcut("h"); + assertFocus("specimen-info"); + + invokeFocusShortcut("c"); + assertFocus("add-script-button"); + + invokeFocusShortcut("h"); + assertFocus("specimen-info"); + }); + + it("multiple costumes", () => { + cy.pytchTryUploadZipfiles(["pytch-jr-5-costumes-4-sounds.zip"]); + assertInIDE("per-method"); + + selectSprite("Snake"); + selectActorAspect("Sounds"); + + invokeFocusShortcut("c"); + assertFocus("sound-card", 0); + realPress("ArrowDown", 2); + assertFocus("sound-card", 2); + + invokeFocusShortcut("s"); + assertFocus("actor-card", 1); + invokeFocusShortcut("c"); + assertFocus("sound-card", 2); + + selectActorAspect("Costumes"); + + invokeFocusShortcut("c"); + assertFocus("appearance-card", 0); + realPress("ArrowDown", 3); + assertFocus("appearance-card", 3); + + invokeFocusShortcut("s"); + assertFocus("actor-card", 1); + invokeFocusShortcut("c"); + assertFocus("appearance-card", 3); + }); + + it("tutorial link", () => { + cy.pytchTryUploadZipfiles(["v4-jr-linked-to-tutorial.zip"]); + assertInIDE("per-method"); + + selectStage(); + assertFocus("actor-card", 0); + realPress("ArrowRight"); + assertFocus("actor-card", 1); + + invokeFocusShortcut("h"); + assertFocus("tutorial-content"); + + invokeFocusShortcut("c"); + assertFocus("add-script-button"); + + invokeFocusShortcut("s"); + assertFocus("actor-card", 1); + + cy.get('button[data-activity-bar-tab="helpsidebar"]') + .as("helpButton") + .click(); + + invokeFocusShortcut("h"); + assertFocus("help-sidebar", [0]); + + realPress("ArrowDown", 3); + realPress("Enter"); + realPress("ArrowDown", 3); + assertFocus("help-sidebar", [3, 2]); + + invokeFocusShortcut("s"); + assertFocus("actor-card", 1); + + invokeFocusShortcut("h"); + assertFocus("help-sidebar", [3, 2]); + + cy.get("@helpButton").click(); + invokeFocusShortcut("c"); + assertFocus("add-script-button"); + invokeFocusShortcut("h"); + assertFocus("activity-tab", "helpsidebar"); + + realPress("End"); + assertFocus("activity-tab", "keynavhelp"); + invokeFocusShortcut("c"); + assertFocus("add-script-button"); + invokeFocusShortcut("h"); + assertFocus("activity-tab", "keynavhelp"); + + realPress("Enter"); + realPress("Tab"); + assertFocus("keynav-help"); + + invokeFocusShortcut("c"); + assertFocus("add-script-button"); + invokeFocusShortcut("h"); + assertFocus("keynav-help"); + + selectSprite("Snake"); + selectActorAspect("Costumes"); + invokeFocusShortcut("c"); + assertFocus("appearance-card", 0); + + selectActorAspect("Code"); + invokeFocusShortcut("h"); + assertFocus("keynav-help"); + invokeFocusShortcut("c"); + assertFocus("script", 0); + + selectActorAspect("Sounds"); + invokeFocusShortcut("h"); + assertFocus("keynav-help"); + invokeFocusShortcut("c"); + assertFocus("add-sound-button"); + }); + }); +}); diff --git a/cypress/e2e/keyboard-navigation/utils.ts b/cypress/e2e/keyboard-navigation/utils.ts index b7bf397d4..27f4ce8c5 100644 --- a/cypress/e2e/keyboard-navigation/utils.ts +++ b/cypress/e2e/keyboard-navigation/utils.ts @@ -212,10 +212,12 @@ type FocusableAreaKind = | "selected-projects-back-button" | "selected-projects-delete-button" | "help-sidebar" + | "keynav-help" | "actor-property-tab" | "info-panel-tab" | "info-panel-disclosure-toggle" | "flat-code-tab" + | "flat-code-editor" | "script" | "script-code" | "hat-block-option" @@ -243,6 +245,7 @@ type FocusableAreaKind = | "stage" | "progress-node" | "tutorial-content" + | "specimen-info" | "learner-task-done-button" | "learner-task-help-button" | "learner-task-diff-tab" @@ -292,12 +295,14 @@ export function assertFocus( export function assertFocus( area: + | "keynav-help" | "add-project-button" | "project-new-name-input" | "selected-projects-back-button" | "selected-projects-delete-button" | "info-panel-disclosure-toggle" | "flat-code-tab" + | "flat-code-editor" | "key-pressed-cancel-button" | "add-script-button" | "add-sprite-button" @@ -310,6 +315,7 @@ export function assertFocus( | "medialib-filter-switch" | "medialib-cancel-button" | "tutorial-content" + | "specimen-info" | "green-flag" | "stage", locWithinArea: void @@ -393,6 +399,9 @@ export function assertFocus(area: FocusableAreaKind, locWithinArea: any): void { } } } + case "keynav-help": { + return ".KeyNavHelpSidebar"; + } case "activity-tab": { const tabKey = locWithinArea as ActivityBarTabKey; return `.activity-bar-tabs button[data-activity-bar-tab="${tabKey}"]`; @@ -421,6 +430,9 @@ export function assertFocus(area: FocusableAreaKind, locWithinArea: any): void { case "flat-code-tab": { return ".CodeEditor ul.nav-tabs li:first-child button"; } + case "flat-code-editor": { + return ".CodeEditor textarea"; + } case "script": { const scriptIdx = locWithinArea as number; const childIdx1b = scriptIdx + 1; @@ -563,6 +575,11 @@ export function assertFocus(area: FocusableAreaKind, locWithinArea: any): void { case "tutorial-content": { return ".Junior-LessonContent"; } + case "specimen-info": { + // TODO: Distinguish the various (mis)uses of the + // Junior-LessonContent class? + return ".Junior-LessonContent"; + } default: return assertNever(area); } diff --git a/cypress/e2e/media-library.cy.ts b/cypress/e2e/media-library.cy.ts index c91b6683d..fdbd2897b 100644 --- a/cypress/e2e/media-library.cy.ts +++ b/cypress/e2e/media-library.cy.ts @@ -169,7 +169,7 @@ context("All/just-tut switch", () => { { label: "per-method", tutorialSlug: "script-by-script-boing", - expNEntries: 4, + expNEntries: 5, preLaunch: () => selectActorAspect("Backdrops"), }, { diff --git a/cypress/e2e/notable-change-toasts.cy.ts b/cypress/e2e/notable-change-toasts.cy.ts index 8bdd09c85..8059fff60 100644 --- a/cypress/e2e/notable-change-toasts.cy.ts +++ b/cypress/e2e/notable-change-toasts.cy.ts @@ -30,6 +30,7 @@ type ItShowsToastForDescriptor = { only?: boolean; setup: () => void; submit: () => void; + assertCompletion?: () => void; failurePredicate?: FailurePredicate; toastBodyMatch: string | RegExp | null; dismissFun: (gatedDelay: GatedDelay) => void; @@ -58,6 +59,7 @@ function itShowsToastFor(label: string, descr: ItShowsToastForDescriptor) { const gatedDelay = GatedDelay.installNew(window); const dismiss = () => descr.dismissFun(gatedDelay); descr.submit(); + descr.assertCompletion?.(); if (descr.failurePredicate != null) { const failPred = descr.failurePredicate; cy.get(failPred.selector).contains(failPred.reportMatch); @@ -118,6 +120,9 @@ context("Toasts are generated (s/b/s)", () => { cy.get(".CompoundTextInput input").type("{selectAll}{del}two-snakes"); }, submit: () => settleModalDialog("Rename"), + assertCompletion: () => { + assertFocus("appearance-card", 0); + }, toastBodyMatch: 'Costume renamed to "two-snakes.png"', dismissFun: kDismissSpaceOnCloseButton, }); diff --git a/cypress/e2e/project-from-specimen.cy.ts b/cypress/e2e/project-from-specimen.cy.ts index 7b2f628d0..8483b42b2 100644 --- a/cypress/e2e/project-from-specimen.cy.ts +++ b/cypress/e2e/project-from-specimen.cy.ts @@ -1,6 +1,7 @@ /// import { + assertShowsLinkedContentError, initSpecimenIntercepts, kFlatLessonUrl, kPerMethodLessonUrl, @@ -360,3 +361,28 @@ context("Compare user code to original", () => { cy.get(".ViewCodeDiffModal").should("not.exist"); }); }); + +context("rejects wrong program-kind", () => { + beforeEach(() => { + cy.pytchResetDatabase(); + cy.contains("My projects").click(); + }); + + it("flat project but per-method specimen", () => { + cy.intercept("GET", "**/_by_content_hash_/1234.zip", { + fixture: "lesson-specimens/per-method-blue-invaders.zip", + }); + cy.pytchTryUploadZipfiles(["v4-flat-linked-to-specimen.zip"]); + + assertShowsLinkedContentError(/project.*flat.*specimen.*per-method/); + }); + + it("per-method project but flat specimen", () => { + cy.intercept("GET", "**/_by_content_hash_/1234.zip", { + fixture: "lesson-specimens/hello-world-lesson.zip", + }); + cy.pytchTryUploadZipfiles(["v4-jr-linked-to-specimen.zip"]); + + assertShowsLinkedContentError(/project.*per-method.*specimen.*flat/); + }); +}); diff --git a/cypress/e2e/start-tutorial-at-chapter.cy.ts b/cypress/e2e/start-tutorial-at-chapter.cy.ts new file mode 100644 index 000000000..620f63bf2 --- /dev/null +++ b/cypress/e2e/start-tutorial-at-chapter.cy.ts @@ -0,0 +1,77 @@ +/// + +import { assertJrTutChapterNumber, settleModalDialog } from "./junior/utils"; +import { assertInIDE, assertOnFrontPage } from "./utils"; + +context("Start jr tutorial at chapter", () => { + beforeEach(() => { + cy.pytchResetDatabase(); + }); + + function assertTutorialNameIncludes(match: string) { + cy.get(".TutorialCard.start-at-chapter .card-title").should( + "include.text", + match + ); + } + + function assertChapterStartContent(expChapterIndex: number) { + cy.get(".TutorialCard.start-at-chapter .chapter-index-content").should( + "have.text", + `Starting at chapter ${expChapterIndex}` + ); + } + + function attemptCreateProject() { + cy.get(".TutorialCard.start-at-chapter button").click(); + } + + function assertError(messageMatch: string) { + cy.get(".GenericErrorModal").contains(messageMatch); + + // Dismissing the modal should give the full page structure, but + // with error message. + settleModalDialog("OK"); + cy.get(".NavBar.inert"); + cy.get(".TutorialList"); + cy.get(".ExceptionDisplay").contains(messageMatch); + } + + it("start tutorial at specified chapter", () => { + cy.visit("/tutorial-checkpoint/script-by-script-boing/6"); + assertTutorialNameIncludes("a Pong-like game"); + assertChapterStartContent(6); + + attemptCreateProject(); + assertJrTutChapterNumber(6); + cy.contains("Bounce the ball off the bats"); + }); + + it("reject invalid tutorial slug", () => { + cy.visit("/tutorial-checkpoint/no-such-tutorial/0"); + assertError("failed to fetch"); + }); + + it("reject invalid chapter index for valid tutorial", () => { + cy.visit("/tutorial-checkpoint/script-by-script-boing/42"); + attemptCreateProject(); + assertError("chapter 42 not found"); + }); + + it("navigate with replacement", () => { + cy.visit("/tutorial-checkpoint/script-by-script-boing/6"); + attemptCreateProject(); + assertInIDE("per-method"); + cy.go("back"); + assertOnFrontPage(); + }); + + it("navigate home outside router", () => { + cy.visit("/tutorial-checkpoint/script-by-script-boing/6"); + cy.get(".home-link").click(); + assertOnFrontPage(); + cy.go("back"); + assertTutorialNameIncludes("a Pong-like game"); + assertChapterStartContent(6); + }); +}); diff --git a/cypress/e2e/tutorials.cy.ts b/cypress/e2e/tutorials.cy.ts index 6ce0d1031..fcb069c4d 100644 --- a/cypress/e2e/tutorials.cy.ts +++ b/cypress/e2e/tutorials.cy.ts @@ -27,6 +27,8 @@ context("Interact with a tutorial", () => { cy.pytchProjectFollowingTutorial(); }); + // This only works when the progress trail display does not include + // the ⋯ markers for elided chapter nodes. const assertActiveChapterIndex = (expActiveIndex: number) => { cy.get(".ProgressTrail .progress-node-background.isActive").should( "have.length", diff --git a/cypress/e2e/upload-zipfile.cy.ts b/cypress/e2e/upload-zipfile.cy.ts index aff244239..f35079357 100644 --- a/cypress/e2e/upload-zipfile.cy.ts +++ b/cypress/e2e/upload-zipfile.cy.ts @@ -42,6 +42,11 @@ context("Upload project from zipfile", () => { cy.get(".ActorCardContent").should("have.length", 2); }); + it("v4 zipfile uppercase asset filename", () => { + cy.pytchTryUploadZipfiles(["uppercase-asset-filename.zip"]); + cy.get(".ActorCardContent").should("have.length", 2); + }); + it("can upload multiple valid zipfiles", () => { cy.pytchTryUploadZipfiles([ "hello-world-format-v1.zip", diff --git a/cypress/e2e/utils.ts b/cypress/e2e/utils.ts index 31b81f972..027b40b57 100644 --- a/cypress/e2e/utils.ts +++ b/cypress/e2e/utils.ts @@ -3,7 +3,7 @@ import { PytchProgramKind } from "../../src/model/pytch-program"; import { assertNever, promiseAndResolve } from "../../src/utils"; export const kExpNTutorials = 18; -export const kExpNMediaLibEntries = 52; +export const kExpNMediaLibEntries = 53; /** Set up request intercepts for a specimen for use in tests. */ export function initSpecimenIntercepts() { @@ -247,6 +247,11 @@ export function assertCopiedText(match: string | ((text: string) => boolean)) { ); } +/** Assert that the webapp is on the front (welcome) page. */ +export function assertOnFrontPage() { + cy.get(".welcome-text header .content-text h2").should("have.text", "Pytch"); +} + /** Assert that the webapp is on the IDE for a program of the given * `programKind`, and that the program has finished loading. */ export function assertInIDE(programKind: PytchProgramKind) { @@ -274,6 +279,16 @@ export function jumpToTutorialChapter(chapterIndex: number) { cy.get(selector).click(); } +/** Assuming that the webapp is in the IDE for a project which + * supposedly has some linked content, assert that there was in fact an + * error loading the content, with technical details matching the given + * reg-exp `match`. */ +export function assertShowsLinkedContentError(match: RegExp) { + cy.get(".ActivityContent .ErrorMessageDisplay .error-message").contains( + match + ); +} + //////////////////////////////////////////////////////////////////////// export function withDownloadedZipfile( diff --git a/cypress/e2e/window-title.cy.ts b/cypress/e2e/window-title.cy.ts index a8d498619..513698dd1 100644 --- a/cypress/e2e/window-title.cy.ts +++ b/cypress/e2e/window-title.cy.ts @@ -18,7 +18,7 @@ context("Browser window title", () => { it("navigate around including tutorials", () => { cy.visit("/tutorials/"); cy.title().should("eq", "Pytch: Tutorials"); - cy.get(".NavBar").contains("Pytch").click(); + cy.get(".NavBar").find(".home-link").click(); cy.title().should("eq", "Pytch"); cy.get(".NavBar").contains("My projects").click(); cy.title().should("eq", "Pytch: My projects"); diff --git a/cypress/fixtures/project-zipfiles/make.sh b/cypress/fixtures/project-zipfiles/make.sh index 937d30405..0c9e4fb71 100755 --- a/cypress/fixtures/project-zipfiles/make.sh +++ b/cypress/fixtures/project-zipfiles/make.sh @@ -28,6 +28,13 @@ make_content_v4_jr() { unzip -q ../simple-v4-jr.zip } +make_content_v4_flat() { + rm -rf tmp-content + mkdir tmp-content + cd tmp-content + unzip -q ../v4-print-things.zip +} + make_zipfile() { rm -f ../$1.zip zip -qr ../$1.zip * @@ -158,3 +165,30 @@ EOT EOT make_zipfile v4-jr-linked-to-specimen ) +( + make_content_v4_flat + cat << EOT > meta.json +{ + "projectName": "Print some things", + "linkedContentRef": { + "kind": "specimen", + "specimenContentHash": "1234" + } +} +EOT + make_zipfile v4-flat-linked-to-specimen +) +( + make_content_v4_flat + cat << EOT > meta.json +{ + "projectName": "Print some things", + "linkedContentRef": { + "kind": "jr-tutorial", + "name": "script-by-script-boing", + "interactionState": { "chapterIndex": 0, "nTasksDone": 0 } + } +} +EOT + make_zipfile v4-flat-linked-to-jr-tutorial +) diff --git a/cypress/fixtures/project-zipfiles/uppercase-asset-filename.zip b/cypress/fixtures/project-zipfiles/uppercase-asset-filename.zip new file mode 100644 index 000000000..9201e42ba Binary files /dev/null and b/cypress/fixtures/project-zipfiles/uppercase-asset-filename.zip differ diff --git a/cypress/fixtures/project-zipfiles/v4-flat-linked-to-jr-tutorial.zip b/cypress/fixtures/project-zipfiles/v4-flat-linked-to-jr-tutorial.zip new file mode 100644 index 000000000..f46b47fc2 Binary files /dev/null and b/cypress/fixtures/project-zipfiles/v4-flat-linked-to-jr-tutorial.zip differ diff --git a/cypress/fixtures/project-zipfiles/v4-flat-linked-to-specimen.zip b/cypress/fixtures/project-zipfiles/v4-flat-linked-to-specimen.zip new file mode 100644 index 000000000..b94944e72 Binary files /dev/null and b/cypress/fixtures/project-zipfiles/v4-flat-linked-to-specimen.zip differ diff --git a/cypress/fixtures/project-zipfiles/v4-print-things.zip b/cypress/fixtures/project-zipfiles/v4-print-things.zip new file mode 100644 index 000000000..86949351c Binary files /dev/null and b/cypress/fixtures/project-zipfiles/v4-print-things.zip differ diff --git a/cypress/fixtures/sample-project-assets/RECTANGLE-RED-80-60.PNG b/cypress/fixtures/sample-project-assets/RECTANGLE-RED-80-60.PNG new file mode 100644 index 000000000..ec4cd1178 Binary files /dev/null and b/cypress/fixtures/sample-project-assets/RECTANGLE-RED-80-60.PNG differ diff --git a/index.html b/index.html index 67c2a65d1..3e248d3cc 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - + =16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } @@ -1018,10 +1025,11 @@ } }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -1054,17 +1062,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.2.tgz", - "integrity": "sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", "dev": true, + "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.13.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz", @@ -1087,6 +1110,18 @@ "node": ">=6" } }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.1.0.tgz", + "integrity": "sha512-9byUd9bgNfthsZAjBl6GxOu1VPHgBuRUP9juI7ZoM98h8xNPTCTagfwUFyYscdZq4Hr7gD1azMfM9s5tIWKZZA==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@fortawesome/free-regular-svg-icons": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-7.1.0.tgz", @@ -2200,20 +2235,20 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.13.0.tgz", - "integrity": "sha512-nQtBLiZYMUPkclSeC3id+x4uVd1SGtHuElTxL++SfP47jR0zfkZBJHc+gL4qPsgTuypz0k8Y2GheaDYn6Gy3rg==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.1.tgz", + "integrity": "sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.13.0", - "@typescript-eslint/type-utils": "8.13.0", - "@typescript-eslint/utils": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/type-utils": "8.50.1", + "@typescript-eslint/utils": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2223,26 +2258,187 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" + "@typescript-eslint/parser": "^8.50.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.1.tgz", + "integrity": "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.1.tgz", + "integrity": "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.1.tgz", + "integrity": "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.50.1", + "@typescript-eslint/tsconfig-utils": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.1.tgz", + "integrity": "sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.1.tgz", + "integrity": "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.13.0.tgz", - "integrity": "sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.1.tgz", + "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.13.0", - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/typescript-estree": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0", + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", "debug": "^4.3.4" }, "engines": { @@ -2253,12 +2449,174 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.1.tgz", + "integrity": "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.1.tgz", + "integrity": "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.1.tgz", + "integrity": "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.50.1", + "@typescript-eslint/tsconfig-utils": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.1.tgz", + "integrity": "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.1.tgz", + "integrity": "sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.50.1", + "@typescript-eslint/types": "^8.50.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service/node_modules/@typescript-eslint/types": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.1.tgz", + "integrity": "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/scope-manager": { @@ -2278,16 +2636,35 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.1.tgz", + "integrity": "sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.13.0.tgz", - "integrity": "sha512-Rqnn6xXTR316fP4D2pohZenJnp+NwQ1mo7/JM+J1LWZENSLkJI8ID8QNtlvFeb0HnFSK94D6q0cnMX6SbE5/vA==", + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.1.tgz", + "integrity": "sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.13.0", - "@typescript-eslint/utils": "8.13.0", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1", + "@typescript-eslint/utils": "8.50.1", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2296,10 +2673,163 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.1.tgz", + "integrity": "sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.1.tgz", + "integrity": "sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.1.tgz", + "integrity": "sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.50.1", + "@typescript-eslint/tsconfig-utils": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/visitor-keys": "8.50.1", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.1.tgz", + "integrity": "sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.50.1", + "@typescript-eslint/types": "8.50.1", + "@typescript-eslint/typescript-estree": "8.50.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.50.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.1.tgz", + "integrity": "sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.50.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, "node_modules/@typescript-eslint/types": { @@ -2344,10 +2874,11 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2915,10 +3446,11 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3033,6 +3565,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -3047,6 +3580,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3321,9 +3883,10 @@ "optional": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3690,18 +4253,6 @@ "node": ">=10" } }, - "node_modules/cypress-parallel/node_modules/nanoid": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", - "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/cypress-parallel/node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -3963,11 +4514,12 @@ "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3978,6 +4530,12 @@ } } }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/decamelize": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", @@ -4012,6 +4570,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -4079,9 +4638,10 @@ "license": "Apache-2.0" }, "node_modules/diff": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -4121,6 +4681,20 @@ "csstype": "^3.0.2" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/easy-peasy": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/easy-peasy/-/easy-peasy-6.1.0.tgz", @@ -4259,12 +4833,10 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -4304,10 +4876,10 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -4316,14 +4888,15 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -5002,12 +5575,15 @@ } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -5112,15 +5688,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5129,6 +5711,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -5248,11 +5843,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5267,7 +5863,8 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/growl": { "version": "1.10.5", @@ -5300,6 +5897,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -5311,6 +5909,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -5319,9 +5918,10 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5333,7 +5933,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -5991,10 +6590,11 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -6430,6 +7030,15 @@ "node": ">= 18" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6517,33 +7126,33 @@ } }, "node_modules/mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -6551,19 +7160,6 @@ }, "engines": { "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "node_modules/mocha/node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "engines": { - "node": ">=6" } }, "node_modules/mocha/node_modules/argparse": { @@ -6572,13 +7168,14 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/mocha/node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, - "engines": { - "node": ">=0.3.1" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" } }, "node_modules/mocha/node_modules/escape-string-regexp": { @@ -6594,42 +7191,32 @@ } }, "node_modules/mocha/node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "minimatch": "^5.0.1", + "once": "^1.3.0" }, "engines": { - "node": "*" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/glob/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/mocha/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -6638,10 +7225,11 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -6649,31 +7237,43 @@ "node": ">=10" } }, - "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/mocha/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/mocha/node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", "dev": true, + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -6731,9 +7331,10 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7161,11 +7762,12 @@ } }, "node_modules/qs": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", - "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -7377,6 +7979,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-resizable-panels": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.3.1.tgz", + "integrity": "sha512-Y+kWgV80snvOvSmWf5ivndOaaxRjaEz/b1snYJaPASqnFzYdfRd6eAXwPPKWtOm/96DNeDGdFssYR8pA8U/zRg==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/react-router": { "version": "7.9.6", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz", @@ -8299,6 +8911,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -8351,14 +8964,69 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -8767,9 +9435,10 @@ "integrity": "sha512-LRbChn2YRpic1KxY+ldL1pGXN/oVvKfCVufwfVzEQdFYNo39uF7AJa/WXdo+gYO7PTvdfkCPCed6Hkvz/kR7jg==" }, "node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", "engines": { "node": ">=14.14" } @@ -9129,6 +9798,95 @@ } } }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.13.0.tgz", + "integrity": "sha512-nQtBLiZYMUPkclSeC3id+x4uVd1SGtHuElTxL++SfP47jR0zfkZBJHc+gL4qPsgTuypz0k8Y2GheaDYn6Gy3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.13.0", + "@typescript-eslint/type-utils": "8.13.0", + "@typescript-eslint/utils": "8.13.0", + "@typescript-eslint/visitor-keys": "8.13.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.13.0.tgz", + "integrity": "sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.13.0", + "@typescript-eslint/types": "8.13.0", + "@typescript-eslint/typescript-estree": "8.13.0", + "@typescript-eslint/visitor-keys": "8.13.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/type-utils": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.13.0.tgz", + "integrity": "sha512-Rqnn6xXTR316fP4D2pohZenJnp+NwQ1mo7/JM+J1LWZENSLkJI8ID8QNtlvFeb0HnFSK94D6q0cnMX6SbE5/vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.13.0", + "@typescript-eslint/utils": "8.13.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -9499,10 +10257,11 @@ } }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", - "dev": true + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/wrap-ansi": { "version": "7.0.0", diff --git a/package.json b/package.json index 896d50a7d..f6d28ae68 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "dependencies": { "@fortawesome/fontawesome-common-types": "^7.1.0", "@fortawesome/fontawesome-svg-core": "^7.1.0", + "@fortawesome/free-brands-svg-icons": "^7.1.0", "@fortawesome/free-regular-svg-icons": "^7.1.0", "@fortawesome/free-solid-svg-icons": "^7.1.0", "@fortawesome/react-fontawesome": "^3.1.0", @@ -28,6 +29,7 @@ "react-error-boundary": "^6.0.0", "react-image-crop": "^11.0.10", "react-redux": "^9.2.0", + "react-resizable-panels": "^4.3.1", "react-router-dom": "^7.9.6", "scratchblocks": "^3.6.2", "standardized-audio-context": "^25.3.54", @@ -51,8 +53,8 @@ "@types/node": "^24.10.1", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", - "@typescript-eslint/eslint-plugin": "^8.13.0", - "@typescript-eslint/parser": "^8.13.0", + "@typescript-eslint/eslint-plugin": "^8.50.1", + "@typescript-eslint/parser": "^8.50.1", "@vitejs/plugin-react": "^5.1.1", "ajv-cli": "^5.0.0", "chai": "^4.3.7", diff --git a/public/favicon.png b/public/favicon.png index 70dac3218..25a1bd6b9 100644 Binary files a/public/favicon.png and b/public/favicon.png differ diff --git a/src/App.tsx b/src/App.tsx index f5b1e19e6..fed56ffcd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,7 +21,7 @@ import "./font-awesome-lib"; import { AllModals } from "./components/AllModals"; import { SingleTutorial } from "./components/SingleTutorial"; import { Link } from "./components/LinkWithinApp"; -import NavBanner from "./components/NavBanner"; +import { NavBanner } from "./components/NavBanner"; import { DemoFromZipfileURL } from "./components/DemoFromZipfileURL"; import { useStoreState, useStoreActions } from "./store"; import { useEffect } from "react"; @@ -31,6 +31,7 @@ import { ProjectFromSpecimenFlow } from "./components/ProjectFromSpecimenFlow"; import { DeliberateFailureWithBoundary } from "./components/DeliberateFailure"; import { fireAndForgetEvent } from "./model/anonymous-instrumentation"; import { StandalonePlayDemo } from "./components/StandalonePlayDemo"; +import { StartTutorialAtCheckpoint } from "./components/StartTutorialAtCheckpoint"; const UnknownRoute: React.FC = () => { return ( @@ -109,6 +110,10 @@ function App() { path: "suggested-tutorial/:slug", element: , }, + { + path: "tutorial-checkpoint/:slug/:chapterIndex", + element: , + }, { path: "suggested-demo/:buildId/:demoId", element: , diff --git a/src/components/CaptiveContextMenu.tsx b/src/components/CaptiveContextMenu.tsx index 68c2579da..8a9882f3c 100644 --- a/src/components/CaptiveContextMenu.tsx +++ b/src/components/CaptiveContextMenu.tsx @@ -245,7 +245,7 @@ const DropdownMenu: React.FC> = ({ children }) => { onKeyDown={onKeydown} data-captive-context-menu-container-id={ctx.containerId} > - + {children} ); diff --git a/src/components/DecorativeUnderscore.tsx b/src/components/DecorativeUnderscore.tsx deleted file mode 100644 index c5dbd7574..000000000 --- a/src/components/DecorativeUnderscore.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from "react"; -import { EmptyProps } from "../utils"; - -export const DecorativeUnderscore: React.FC = () => ( - _ -); diff --git a/src/components/DemoFromZipfileURL.tsx b/src/components/DemoFromZipfileURL.tsx index a2ebe65f8..61b259d19 100644 --- a/src/components/DemoFromZipfileURL.tsx +++ b/src/components/DemoFromZipfileURL.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from "react"; import { demoURLFromId } from "../storage/zipfile"; import { useStoreActions, useStoreState } from "../store"; -import NavBanner from "./NavBanner"; +import { NavBanner } from "./NavBanner"; import Button from "react-bootstrap/Button"; import LoadingOverlay from "./LoadingOverlay"; import { Link } from "./LinkWithinApp"; diff --git a/src/components/EditorAndOutErr.tsx b/src/components/EditorAndOutErr.tsx index 4c0e3324c..db096b982 100644 --- a/src/components/EditorAndOutErr.tsx +++ b/src/components/EditorAndOutErr.tsx @@ -1,11 +1,19 @@ import React from "react"; import classNames from "classnames"; import { useStoreState } from "../store"; -import { useJrEditState } from "./Junior/hooks"; +import { useJrEditActions, useJrEditState } from "./Junior/hooks"; import { EmptyProps, assertNever } from "../utils"; import { CodeEditor } from "./CodeEditor"; import { InfoPanel } from "./Junior/InfoPanel"; import { ActorProperties } from "./Junior/ActorProperties"; +import { + Group, + Panel, + Separator, + usePanelRef, +} from "react-resizable-panels"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { minInfoPanelHeight } from "../constants"; const EditorForProgramKind: React.FC = () => { const programKind = useStoreState( @@ -27,11 +35,30 @@ export const EditorAndOutErr: React.FC = () => { (s) => s.infoPanelState === "collapsed" ); + const infoResizablePanelRef = usePanelRef(); + const toggleStateAction = useJrEditActions((a) => a.toggleInfoPanelState); + const isCollapsed = useJrEditState((s) => s.infoPanelState === "collapsed"); + const classes = classNames("EditorAndOutErr", { infoPanelIsCollapsed }); + return ( -
- - -
+ + + + + +
+ +
+
+ { + console.log('panelSize', panelSize.inPixels); + if ((panelSize.inPixels <= minInfoPanelHeight && prevPanelSize?.inPixels > minInfoPanelHeight && !isCollapsed) || panelSize.inPixels > minInfoPanelHeight && prevPanelSize?.inPixels <= minInfoPanelHeight && isCollapsed) { + toggleStateAction(); + } + }}> + + +
); }; diff --git a/src/components/ErrorMessageDisplay.tsx b/src/components/ErrorMessageDisplay.tsx new file mode 100644 index 000000000..eb153ea0c --- /dev/null +++ b/src/components/ErrorMessageDisplay.tsx @@ -0,0 +1,22 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React from "react"; + +type ErrorMessageDisplayProps = { errorMessage: string }; + +export const ErrorMessageDisplay: React.FC = ({ + errorMessage, +}) => { + return ( +
+

+ + Sorry, there was an unexpected problem. Please contact the Pytch team if + the problem persists. +

+
+

Technical details:

+

{errorMessage}

+
+
+ ); +}; diff --git a/src/components/ExceptionDisplay.tsx b/src/components/ExceptionDisplay.tsx index ce211c2b8..1cb1f8862 100644 --- a/src/components/ExceptionDisplay.tsx +++ b/src/components/ExceptionDisplay.tsx @@ -1,10 +1,14 @@ import React from "react"; -import { FallbackProps } from "react-error-boundary"; import { DivSettingWindowTitle } from "./DivSettingWindowTitle"; import Button from "react-bootstrap/Button"; import { envVarOrDefault } from "../env-utils"; +import { ErrorMessageDisplay } from "./ErrorMessageDisplay"; -export function ExceptionDisplay(props: FallbackProps): React.JSX.Element { +// Accept props of broader type than "FallbackProps" to allow use in +// other contexts. +type ExceptionDisplayProps = { error: { message: string } }; + +export const ExceptionDisplay: React.FC = (props) => { const { error } = props; // Use in the below, rather than , to ensure true @@ -15,14 +19,7 @@ export function ExceptionDisplay(props: FallbackProps): React.JSX.Element { windowTitle="Pytch: Unexpected error" >
-

- Sorry, there was an unexpected problem. Please contact the Pytch team - if the problem persists. -

-

- (Technical details:{" "} - {error.message}) -

+
); -} +}; diff --git a/src/components/IDELayout.tsx b/src/components/IDELayout.tsx index da5edadbe..a5e037a86 100644 --- a/src/components/IDELayout.tsx +++ b/src/components/IDELayout.tsx @@ -1,6 +1,6 @@ import React, { KeyboardEventHandler, useEffect } from "react"; import classNames from "classnames"; -import { useStoreState } from "../store"; +import { useStoreActions, useStoreState } from "../store"; import { useJrEditState } from "./Junior/hooks"; import { assertNever, EmptyProps } from "../utils"; import { DivSettingWindowTitle } from "./DivSettingWindowTitle"; @@ -13,6 +13,10 @@ import { FlatModals } from "./FlatModals"; import { useFocusContext } from "./hooks/focus-steering"; import { NotableChangeToasts } from "./NotableChangeToasts"; import { useActionAsEffect } from "./hooks/use-action-as-effect"; +import {Group, Panel, PanelSize, Separator} from "react-resizable-panels"; +import {minStageWidth} from "./Junior/WidthMonitor"; +import { stageWidth } from "../constants"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; const Modals: React.FC = () => { const programKind = useStoreState( @@ -103,6 +107,10 @@ export const IDELayout: React.FC = () => { } }; + const setStageDisplayWidth = useStoreActions( + (actions) => actions.ideLayout.setStageDisplayWidth + ); + return ( = () => {
- - - + + + + + + + + + + + + + + { + //TODO update stage width + const targetWidth = Math.min( + stageWidth, + Math.max(minStageWidth, panelSize.inPixels - 20) + ); + setStageDisplayWidth(targetWidth); + })}> + + +
); diff --git a/src/components/Junior/CodeEditor.tsx b/src/components/Junior/CodeEditor.tsx index bd6f143fd..487b383e5 100644 --- a/src/components/Junior/CodeEditor.tsx +++ b/src/components/Junior/CodeEditor.tsx @@ -144,7 +144,7 @@ const ScriptsEditor = () => { groupedFocusKey={`ActorProperties/${actorId}/code`} opts={{ onReorder }} > -
+
{maybeNoContentHelp}
    {scriptsContent}
diff --git a/src/components/Junior/InfoPanel.tsx b/src/components/Junior/InfoPanel.tsx index d4bda0aed..b61b7e7d2 100644 --- a/src/components/Junior/InfoPanel.tsx +++ b/src/components/Junior/InfoPanel.tsx @@ -8,6 +8,8 @@ import { ErrorReportList } from "./ErrorReportList"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import classNames from "classnames"; import { Button } from "react-bootstrap"; +import { PanelImperativeHandle } from "react-resizable-panels"; +import {minInfoPanelHeight} from "../../constants"; const StandardOutput = () => { // TODO: Remove duplication between this and non-jr component. @@ -45,10 +47,26 @@ const Errors = () => { return
{content}
; }; -type InfoDisclosureProps = { tabContentId: string }; -const InfoDisclosure: React.FC = ({ tabContentId }) => { +type InfoDisclosureProps = { + tabContentId: string; + resizablePanelRef?: + | React.RefObject + | undefined; +}; +const InfoDisclosure: React.FC = ({ + tabContentId, + resizablePanelRef, +}) => { + const isCollapsed = useJrEditState((s) => s.infoPanelState === "collapsed"); const toggleStateAction = useJrEditActions((a) => a.toggleInfoPanelState); - const toggleState = () => toggleStateAction(); + const toggleState = () => { + console.log('expand panel'); + if (isCollapsed) { + toggleStateAction(); + resizablePanelRef?.current.expand(); + if (resizablePanelRef?.current.getSize().inPixels === minInfoPanelHeight) resizablePanelRef.current.resize(380); + } + }; return (
@@ -68,7 +86,11 @@ const InfoDisclosure: React.FC = ({ tabContentId }) => { ); }; -export const InfoPanel = () => { +interface InfoPanelProps { + resizablePanelRef?: React.RefObject; +} + +export const InfoPanel = ({ resizablePanelRef }: InfoPanelProps) => { const activeTab = useJrEditState((s) => s.infoPanelActiveTab); const isCollapsed = useJrEditState((s) => s.infoPanelState === "collapsed"); const setActiveTab = useJrEditActions((a) => a.expandAndSetActive); @@ -76,7 +98,13 @@ export const InfoPanel = () => { const tabContentId = useId(); const wasCollapsed = useRef(null); - const toggleState = () => toggleStateAction(); + const toggleState = () => { + console.log('collapse panel'); + if (!isCollapsed) { + toggleStateAction(); + resizablePanelRef?.current.collapse(); + } + }; const classes = classNames( "Junior-InfoPanel-container", @@ -121,7 +149,10 @@ export const InfoPanel = () => { {isCollapsed ? ( - + ) : ( -

{" "} +

); diff --git a/src/components/StageControls.tsx b/src/components/StageControls.tsx index 2dfcb277f..42f2260ce 100644 --- a/src/components/StageControls.tsx +++ b/src/components/StageControls.tsx @@ -7,7 +7,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { EmptyProps } from "../utils"; import { filenameFormatSpecifier } from "../model/format-spec-for-linked-content"; import { pathWithinApp } from "../env-utils"; -import { useNavigate } from "react-router-dom"; +import {Link, useNavigate} from "react-router-dom"; import { useRunFlow } from "../model"; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -49,8 +49,9 @@ const GreenFlag = () => {

Click the green flag to run the project

@@ -65,8 +66,9 @@ export const RedStop = () => { focusStage(); }; return ( - ); }; @@ -90,13 +92,22 @@ const ExportToDriveDropdownItem: React.FC = () => { switch (googleDriveStatus.kind) { case "not-yet-started": case "pending": - return Export to Google Drive; + return + + Export to Google Drive + ; case "succeeded": return ( - Export to Google Drive + + + Export to Google Drive + ); case "failed": - return Google Drive unavailable; + return + + Google Drive unavailable + ; } }; @@ -116,7 +127,10 @@ const LaunchCoordsChooserDropdownItem: React.FC = () => { const GoToMyProjectsDropdownItem: React.FC = () => { const navigate = useNavigate(); const goToMyProjects = () => navigate(pathWithinApp("/my-projects/")); - return My projects; + return + + My projects + ; }; export const StageControls: React.FC = () => { @@ -160,8 +174,9 @@ export const StageControls: React.FC = () => { const onCreateCopy = () => runSaveProjectAs(copyArgs); const fullScreenButton = ( - ); @@ -180,29 +195,50 @@ export const StageControls: React.FC = () => { variant={"secondary"} onClick={() => setIsFullScreen(false)} > - + ) : (
- - +
+ + +
+ {fullScreenButton} - {fullScreenButton} - - + {/**/} + + + + - Screenshot - Make a copy... - Download as zipfile + + + Screenshot + + + + + Make a copy... + + + + Download as zipfile + + Show tooltips diff --git a/src/components/StartTutorialAtCheckpoint.tsx b/src/components/StartTutorialAtCheckpoint.tsx new file mode 100644 index 000000000..1628abd80 --- /dev/null +++ b/src/components/StartTutorialAtCheckpoint.tsx @@ -0,0 +1,112 @@ +import { useEffect, useRef } from "react"; +import { useParams } from "react-router-dom"; +import { EmptyProps } from "../utils"; +import { useFlowState, useRunFlow } from "../model"; +import { asyncFlowModal } from "./async-flow-modals/utils"; +import { settleFunctions } from "../model/user-interactions/async-user-flow"; +import { Button, Card, Spinner } from "react-bootstrap"; +import { ExceptionDisplay } from "./ExceptionDisplay"; +import { InertNavBanner } from "./NavBanner"; + +const Content: React.FC = () => { + const { fsmState } = useFlowState((f) => f.startTutorialAtCheckpointFlow); + let lastErrorMessage = useRef(null); + + // In the case of error, remember the message and display it even + // after the async-user-flow has completed. This avoids the user + // looking at a mostly-blank page. + + if (fsmState.kind === "awaiting-ack-of-error") + lastErrorMessage.current = fsmState.errorMessage; + + if (fsmState.kind === "idle" && lastErrorMessage.current != null) + return ; + + // Otherwise, handle as normal "modal". + + return asyncFlowModal(fsmState, (activeFsmState) => { + const { displaySummary, displayName, chapterIndex } = + activeFsmState.runState; + const settle = settleFunctions(true, activeFsmState); + + const summaryDivRef: React.Ref = (div) => { + if (div == null || div.hasAttribute("data-populated")) return; + displaySummary.forEach((node) => div.appendChild(node.cloneNode(true))); + div.setAttribute("data-populated", "yes"); + }; + + // TODO: Add difficulty badge and program-kind badge? + + const buttonContent = + activeFsmState.kind === "attempting" ? : "Tutorial"; + + return ( +
+
    +
  • + + +
    + + {displayName} + +
    +

    + Starting at chapter {chapterIndex} +

    +
    + +
    +

    + You will start this tutorial at the start of chapter{" "} + {chapterIndex}. +

    + + +
    + +
    +
    + +
  • +
+
+ ); + }); +}; + +export const StartTutorialAtCheckpoint: React.FC = () => { + const params = useParams(); + const run = useRunFlow((f) => f.startTutorialAtCheckpointFlow); + + useEffect(() => { + // Allow invalid slug or chapterIndex to get fed into the prepare() + // or attempt() function of the flow. If either of those throws an + // error, it will be handled by the GenericErrorModal. + // + // In development mode, the run() will get called twice, and will + // log a warning about "expecting idle but preparing". Should not + // happen in production build. + // + run({ mSlug: params.slug, mChapterIndexStr: params.chapterIndex }); + }); + + return ( + <> + +
+

This tutorial was suggested for you:

+ +
+ + ); +}; diff --git a/src/components/Tutorial.tsx b/src/components/Tutorial.tsx index 578ba8b4b..75b117afd 100644 --- a/src/components/Tutorial.tsx +++ b/src/components/Tutorial.tsx @@ -472,7 +472,7 @@ const ActiveTutorial = () => {
diff --git a/src/components/TutorialList.tsx b/src/components/TutorialList.tsx index feb8df3fb..4bc3310c1 100644 --- a/src/components/TutorialList.tsx +++ b/src/components/TutorialList.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from "react"; -import NavBanner from "./NavBanner"; +import { NavBanner } from "./NavBanner"; import { useStoreActions, useStoreState } from "../store"; import { SyncState } from "../model/project"; import { TutorialSummaryDisplay } from "./TutorialSummaryDisplay"; diff --git a/src/components/TutorialSummaryDisplay.tsx b/src/components/TutorialSummaryDisplay.tsx index 664b2bfe4..c5673a1e2 100644 --- a/src/components/TutorialSummaryDisplay.tsx +++ b/src/components/TutorialSummaryDisplay.tsx @@ -13,7 +13,7 @@ import { useRunFlow } from "../model"; interface TutorialSummaryDisplayProps { tutorial: ITutorialSummary; - kind?: SingleTutorialDisplayKind; + kind: SingleTutorialDisplayKind; } export const TutorialSummaryDisplay: React.FC = ({ @@ -30,7 +30,6 @@ export const TutorialSummaryDisplay: React.FC = ({ const runShareTutorial = useRunFlow((f) => f.shareTutorialFlow); const cardRef = React.useRef(null); - const buttonsRef = React.useRef(null); const maybeSlugCreating = useStoreState( (state) => state.tutorialCollection.maybeSlugCreating @@ -43,8 +42,7 @@ export const TutorialSummaryDisplay: React.FC = ({ useEffect(() => { let elt = cardRef.current; - const buttonsElt = buttonsRef.current; - if (elt == null || buttonsElt == null) return; + if (elt == null) return; if (elt.hasAttribute("data-populated")) return; for (const ch of tutorial.contentNodes) { @@ -54,7 +52,7 @@ export const TutorialSummaryDisplay: React.FC = ({ }); const launchTutorial = () => { - createProjectFromTutorial(tutorial.slug); + createProjectFromTutorial({ slug: tutorial.slug, chapterIndex: 0 }); }; const launchDemo = () => { @@ -96,7 +94,7 @@ export const TutorialSummaryDisplay: React.FC = ({ -
+
{showDemoButton && (
diff --git a/src/components/decorations.tsx b/src/components/decorations.tsx new file mode 100644 index 000000000..e6de72199 --- /dev/null +++ b/src/components/decorations.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { EmptyProps } from "../utils"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +export const DecorativeUnderscore: React.FC = () => ( + _ +); + +export const ExternalLinkIndicator: React.FC = () => { + return ( + + ); +}; diff --git a/src/components/front-page/Footer.tsx b/src/components/front-page/Footer.tsx index f14367d57..137b0a697 100644 --- a/src/components/front-page/Footer.tsx +++ b/src/components/front-page/Footer.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Link } from "../LinkWithinApp"; import { urlWithinApp } from "../../env-utils"; -import { DecorativeUnderscore } from "../DecorativeUnderscore"; +import { DecorativeUnderscore } from "../decorations"; import "./Footer.scss"; // TODO: Use research site URL from constants. Or generalise to function giving diff --git a/src/components/front-page/LearnPython.tsx b/src/components/front-page/LearnPython.tsx index d53fdc467..780291169 100644 --- a/src/components/front-page/LearnPython.tsx +++ b/src/components/front-page/LearnPython.tsx @@ -1,7 +1,7 @@ import React from "react"; import { EmptyProps } from "../../utils"; import { welcomeAssetUrl } from "./utils"; -import { DecorativeUnderscore } from "../DecorativeUnderscore"; +import { DecorativeUnderscore } from "../decorations"; import "./LearnPython.scss"; import { Link } from "react-router-dom"; import { sharingUrlFromSlugForDemo } from "../../model/user-interactions/share-tutorial"; diff --git a/src/constants.ts b/src/constants.ts index b15393f4f..f621edd37 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,3 +7,5 @@ export const stageHalfHeight = 0.5 * stageHeight; export const stageFullScreenBorderPx = 8; export const pytchResearchSiteUrl = "https://pytch.scss.tcd.ie/"; + +export const minInfoPanelHeight = 36; \ No newline at end of file diff --git a/src/font-awesome-lib.ts b/src/font-awesome-lib.ts index 10e3b43d3..1cf5cb46d 100644 --- a/src/font-awesome-lib.ts +++ b/src/font-awesome-lib.ts @@ -9,6 +9,7 @@ import { faClipboard, faFileArchive, faInfoCircle, + faArrowUpRightFromSquare, faCheck, faCheckCircle, faCheckSquare, @@ -38,8 +39,18 @@ import { faExclamationCircle, faWindowMinimize, faKeyboard, + faFloppyDisk, + faCamera, + faClone, + faCode, + faDownload, + faEllipsisV, + faEllipsisH } from "@fortawesome/free-solid-svg-icons"; +import { faGoogleDrive } from '@fortawesome/free-brands-svg-icons' + + import { faTimesCircle, faEnvelope } from "@fortawesome/free-regular-svg-icons"; library.add( @@ -53,6 +64,7 @@ library.add( faClipboard, faFileArchive, faInfoCircle, + faArrowUpRightFromSquare, faCheck, faCheckCircle, faCheckSquare, @@ -82,5 +94,13 @@ library.add( faExclamationCircle, faWindowMinimize, faKeyboard, - faTimesCircle + faTimesCircle, + faFloppyDisk, + faCamera, + faClone, + faCode, + faDownload, + faGoogleDrive, + faEllipsisV, + faEllipsisH ); diff --git a/src/images/pytch.png b/src/images/pytch.png new file mode 100644 index 000000000..17b2760b7 Binary files /dev/null and b/src/images/pytch.png differ diff --git a/src/model/demo-from-zipfile-url.ts b/src/model/demo-from-zipfile-url.ts index 4ef2a1604..2059b92bc 100644 --- a/src/model/demo-from-zipfile-url.ts +++ b/src/model/demo-from-zipfile-url.ts @@ -7,15 +7,10 @@ import { } from "../storage/zipfile"; import { delaySeconds } from "../utils"; -type DemoFromZipfileProposingState = { - state: "proposing"; - projectDescriptor: StandaloneProjectDescriptor; -}; - type DemoFromZipfileURLState = | { state: "booting" } | { state: "fetching" } - | DemoFromZipfileProposingState + | { state: "proposing"; projectDescriptor: StandaloneProjectDescriptor } | { state: "creating"; projectDescriptor: StandaloneProjectDescriptor } | { state: "error"; message: string } | { state: "idle" }; diff --git a/src/model/junior/global-steer-focus.ts b/src/model/junior/global-steer-focus.ts index 6224f5f43..8f46b126e 100644 --- a/src/model/junior/global-steer-focus.ts +++ b/src/model/junior/global-steer-focus.ts @@ -84,17 +84,19 @@ export class GlobalFocusSteering { this.actionFromSecondKey.set("p", elementAction("#pytch-speech-bubbles")); + const helpContentAction = bookmarkedOrElementAction( + "gfs__help", + ".gfs__help-content" + ); + switch (pageKind) { case "per-method": - this.actionFromSecondKey.set( - "h", - bookmarkedOrElementAction("gfs__help", ".Junior-LessonContent") - ); + this.actionFromSecondKey.set("h", helpContentAction); this.actionFromSecondKey.set("s", bookmarkedAction("gfs__actors")); this.actionFromSecondKey.set("c", bookmarkedAction("gfs__actorprops")); break; case "flat": - this.actionFromSecondKey.set("h", bookmarkedAction("gfs__help")); + this.actionFromSecondKey.set("h", helpContentAction); this.actionFromSecondKey.set("a", bookmarkedAction("gfs__flatassets")); this.actionFromSecondKey.set( "c", diff --git a/src/model/junior/grouped-focus.ts b/src/model/junior/grouped-focus.ts index e82fd3420..e75497a7e 100644 --- a/src/model/junior/grouped-focus.ts +++ b/src/model/junior/grouped-focus.ts @@ -668,6 +668,8 @@ export class GroupedFocusManager { if (elt == null) { if (eltWithHandler != null && onKeyDown != null) { eltWithHandler.removeEventListener("keydown", onKeyDown); + eltWithHandler = null; + onKeyDown = null; } } else { const key = GroupedFocusManager.keyFromElt(elt); diff --git a/src/model/junior/jr-tutorial.ts b/src/model/junior/jr-tutorial.ts index c76af5958..f866ad247 100644 --- a/src/model/junior/jr-tutorial.ts +++ b/src/model/junior/jr-tutorial.ts @@ -4,8 +4,10 @@ import { isDivOfClass, parsedHtmlBody, } from "../../utils"; +import { PytchProgramKind } from "../pytch-program"; import { patchImageSrcURLs, tutorialResourceText } from "../tutorial"; import { EventDescriptor } from "./structured-program"; +import { NoIdsStructuredProject } from "./structured-program/skeleton"; import * as z from "zod/mini"; // Use full word "Identifier" so as not to make people think it's a @@ -126,6 +128,17 @@ export type JrTutorialPersistentInteractionState = z.infer< typeof zJrTutorialPersistentInteractionState >; +/** A skeleton of a "checkpoint" of a user's progress through a + * tutorial. The use-case is allowing a user to start a tutorial as of + * the start of a particular chapter. A new project can be embodied + * from this skeleton and recorded as being linked to the relevant + * tutorial at the correct chapter and with the correct number of tasks + * done. */ +export type JrTutorialCheckpointSkeleton = { + programSkeleton: NoIdsStructuredProject; + interactionState: JrTutorialPersistentInteractionState; +}; + /** The state of the learner's interaction with a particular task of the * lesson. There is no slot here for "has the learner marked this task * as done?" because that information is represented by the `nTasksDone` @@ -165,8 +178,14 @@ export type LinkedJrTutorial = { * state. * */ export async function dereferenceLinkedJrTutorial( + programKind: PytchProgramKind, ref: LinkedJrTutorialRef ): Promise { + if (programKind !== "per-method") { + throw new Error(`project is "${programKind}" but tutorial is "per-method"`); + } + + // TODO: What happens if a tutorial is deleted? const content = await jrTutorialContentFromName(ref.name); const taskStates: Array = []; diff --git a/src/model/linked-content.ts b/src/model/linked-content.ts index 9e3731c8e..e6d4cdf83 100644 --- a/src/model/linked-content.ts +++ b/src/model/linked-content.ts @@ -15,6 +15,8 @@ import { LinkedSpecimenRef, SpecimenContentHash, } from "./linked-content-core"; +import { PytchProgramKind } from "./pytch-program"; +import { LinkedContentLoadingState } from "./project"; export type LessonDescriptor = { specimenContentHash: SpecimenContentHash; @@ -56,6 +58,7 @@ export function linkedContentIsReferent( } export async function dereferenceLinkedNoContent( + _programKind: PytchProgramKind, _ref: LinkedNoContentRef ): Promise { return kLinkedNoContent; @@ -84,16 +87,25 @@ export async function lessonDescriptorFromRelativePath( } export async function dereferenceLinkedSpecimen( + programKind: PytchProgramKind, ref: LinkedSpecimenRef ): Promise { const contentHash = ref.specimenContentHash; const relativePath = `_by_content_hash_/${contentHash}`; const lesson = await lessonDescriptorFromRelativePath(relativePath); + + const specimenKind = lesson.project.program.kind; + if (specimenKind !== programKind) { + throw new Error( + `project is "${programKind}" but specimen is "${specimenKind}"` + ); + } + return { kind: "specimen", lesson }; } type LinkedContentLoadingStateSummary = - | { kind: "idle" | "failed" } + | (LinkedContentLoadingState & { kind: "idle" | "failed" }) | { kind: "pending" | "succeeded"; contentKind: LinkedContentKind }; function mapLCLSS( @@ -103,7 +115,7 @@ function mapLCLSS( switch (contentState.kind) { case "idle": case "failed": - return { kind: contentState.kind }; + return contentState; case "succeeded": return { kind: "succeeded", diff --git a/src/model/project.ts b/src/model/project.ts index 919a787f5..9cb45c622 100644 --- a/src/model/project.ts +++ b/src/model/project.ts @@ -207,7 +207,7 @@ export type LinkedContentLoadingState = | { kind: "idle" } | { kind: "pending"; projectId: ProjectId; contentRef: LinkedContentRef } | { kind: "succeeded"; projectId: ProjectId; content: LinkedContent } - | { kind: "failed" }; + | { kind: "failed"; contentKind: LinkedContentKind; message: string }; type SucceededStateOfKind = LinkedContentLoadingState & { @@ -267,6 +267,7 @@ function assertLinkedContentSucceededOfKind( type LinkedContentLoadTaskDescriptor = { projectId: ProjectId; + projectProgramKind: PytchProgramKind; linkedContentRef: LinkedContentRef; }; @@ -878,15 +879,16 @@ export const activeProject: IActiveProject = { try { const summary = await projectSummary(projectId); + const descriptor = await projectDescriptor(projectId); + // Just set this off; do not await it. If the network is slow or // broken we don't want to hold up the rest of the student's work. actions.doLinkedContentLoadTask({ projectId, + projectProgramKind: descriptor.program.kind, linkedContentRef: summary.linkedContentRef, }); - const descriptor = await projectDescriptor(projectId); - // TODO: Should the asset-server be local to the project? Might // save all the to/fro with prepare/clear and knowing when to revoke // the image-urls? @@ -962,7 +964,7 @@ export const activeProject: IActiveProject = { }), doLinkedContentLoadTask: thunk(async (actions, taskDescriptor, helpers) => { - const { projectId, linkedContentRef } = taskDescriptor; + const { projectId, projectProgramKind, linkedContentRef } = taskDescriptor; const initialState = helpers.getState().linkedContentLoadingState; const correctLoadIsPending = @@ -983,11 +985,20 @@ export const activeProject: IActiveProject = { const content = await (() => { switch (linkedContentRef.kind) { case "none": - return dereferenceLinkedNoContent(linkedContentRef); + return dereferenceLinkedNoContent( + projectProgramKind, + linkedContentRef + ); case "jr-tutorial": - return dereferenceLinkedJrTutorial(linkedContentRef); + return dereferenceLinkedJrTutorial( + projectProgramKind, + linkedContentRef + ); case "specimen": - return dereferenceLinkedSpecimen(linkedContentRef); + return dereferenceLinkedSpecimen( + projectProgramKind, + linkedContentRef + ); default: return assertNever(linkedContentRef); } @@ -1006,7 +1017,11 @@ export const activeProject: IActiveProject = { } } catch (e) { console.error("doLinkedContentLoadTask():", e); - actions.setLinkedContentLoadingState({ kind: "failed" }); + actions.setLinkedContentLoadingState({ + kind: "failed", + contentKind: linkedContentRef.kind, + message: (e as Error).message ?? "unknown error", + }); } }), diff --git a/src/model/tutorial.ts b/src/model/tutorial.ts index d5bd6b2d3..fce0cfe3c 100644 --- a/src/model/tutorial.ts +++ b/src/model/tutorial.ts @@ -26,11 +26,23 @@ export const tutorialUrl = (relativeUrl: string) => { return [tutorialsDataRoot, relativeUrl].join("/"); }; +const fetchTutorialResourceOrThrow = async (relativeUrl: string) => { + const url = tutorialUrl(relativeUrl); + + const response = await fetch(url); + if (!response.ok) + throw new Error( + `failed to fetch tutorial resource "${relativeUrl}":` + + ` "${response.statusText}"` + ); + + return response; +}; + export const tutorialResourceText = async ( relativeUrl: string ): Promise => { - const url = tutorialUrl(relativeUrl); - const response = await fetch(url); + const response = await fetchTutorialResourceOrThrow(relativeUrl); return await response.text(); }; @@ -39,8 +51,7 @@ type PromiseOfAny = Promise; export const tutorialResourceParsedJson = async ( relativeUrl: string ): PromiseOfAny => { - const url = tutorialUrl(relativeUrl); - const response = await fetch(url); + const response = await fetchTutorialResourceOrThrow(relativeUrl); return await response.json(); }; diff --git a/src/model/tutorials.ts b/src/model/tutorials.ts index deee0272f..6c6e09426 100644 --- a/src/model/tutorials.ts +++ b/src/model/tutorials.ts @@ -15,17 +15,18 @@ import { IPytchAppModel, PytchAppModelActions } from "."; import { PytchProgramOps } from "./pytch-program"; import { assertNever, - fetchArrayBuffer, fetchMimeTypedArrayBuffer, propSetterAction, } from "../utils"; -import { urlWithinApp } from "../env-utils"; import { tutorialResourceParsedJson, tutorialUrl } from "./tutorial"; import { Uuid, IEmbodyContext, StructuredProgramOps, } from "./junior/structured-program"; +import { NavigateOptions } from "react-router-dom"; +import { JrTutorialCheckpointSkeleton } from "./junior/jr-tutorial"; +import { kLinkedContentRefNone, LinkedContentRef } from "./linked-content-core"; const kAllowRandomChapterAccessSearchParam = "allowRandomChapterAccessInTutorials"; @@ -42,33 +43,41 @@ export interface ITutorialSummary { metadata: any; } +export type CreateProjectFromTutorialArgs = { + slug: string; + chapterIndex: number; + navigateWithReplace?: boolean; +}; + +type SAction = Action; +type SThunk = Thunk< + ITutorialCollection, + PayloadT, + unknown, + IPytchAppModel, + ReturnT +>; + export interface ITutorialCollection { syncState: SyncState; available: Array; maybeSlugCreating: string | undefined; allowRandomChapterAccess: boolean; - setSyncState: Action; - setAvailable: Action>; - setSlugCreating: Action; - clearSlugCreating: Action; - setAllowRandomChapterAccess: Action; - loadSummaries: Thunk; - - createProjectFromTutorial: Thunk< - ITutorialCollection, - string, - void, - IPytchAppModel - >; - createDemoFromTutorial: Thunk< - ITutorialCollection, - string, - void, - IPytchAppModel + setSyncState: SAction; + setAvailable: SAction>; + setSlugCreating: SAction; + clearSlugCreating: SAction; + setAllowRandomChapterAccess: SAction; + loadSummaries: SThunk>; + + createProjectFromTutorial: SThunk< + CreateProjectFromTutorialArgs, + Promise >; + createDemoFromTutorial: SThunk>; - bootAllowRandomChapterAccessFromQuery: Thunk; + bootAllowRandomChapterAccessFromQuery: SThunk; } type ProjectCreationArgs = { @@ -76,9 +85,7 @@ type ProjectCreationArgs = { options: CreateProjectOptions; }; -type ProjectCreationArgsFun = ( - tutorialSlug: string -) => Promise; +type ProjectCreationArgsFun = () => Promise; const createProjectFromTutorial = async ( actions: Actions, @@ -90,6 +97,7 @@ const createProjectFromTutorial = async ( methods: { projectCreationArgs: ProjectCreationArgsFun; completionAction: () => void; + navigateOptions?: () => NavigateOptions; } ) => { const storeActions = helpers.getStoreActions(); @@ -101,7 +109,7 @@ const createProjectFromTutorial = async ( actions.setSlugCreating(tutorialSlug); - const createProjectArgs = await methods.projectCreationArgs(tutorialSlug); + const createProjectArgs = await methods.projectCreationArgs(); const project = await createNewProject( createProjectArgs.name, createProjectArgs.options @@ -129,7 +137,69 @@ const createProjectFromTutorial = async ( actions.clearSlugCreating(); methods.completionAction(); storeActions.projectCollection.noteDatabaseChange(); - storeActions.navigationRequestQueue.enqueue({ path: `/ide/${project.id}` }); + storeActions.navigationRequestQueue.enqueue({ + path: `/ide/${project.id}`, + opts: methods.navigateOptions?.(), + }); +}; + +/** Return a new `CreateProjectOptions` instance specifying that the + * to-be-created project should be linked to the tutorial with the given + * `tutorialSlug`, starting at the chapter with the given + * `chapterIndex`. As a special case, if `chapterIndex` is `-1`, + * instead return options specifying a "demo", i.e., that the project + * should have the content of the finished tutorial, but not be linked + * to anything. In either case, the project's summary will describe its + * origin. + */ +const jrTutorialCheckpointCreateOptions = async ( + tutorialSlug: string, + chapterIndex: number +): Promise => { + const relativeUrl = `${tutorialSlug}/chapter-starts.json`; + const demoRequested = chapterIndex === -1; + + const checkpointsObj = await tutorialResourceParsedJson(relativeUrl); + + // TODO: Parse with zod to validate structure. + const checkpoints = checkpointsObj as Array; + + const checkpoint = demoRequested + ? checkpoints[checkpoints.length - 1] + : checkpoints[chapterIndex]; + + if (checkpoint == null) { + throw new Error( + `chapter ${chapterIndex} not found in` + + ` ${checkpoints.length}-element list of` + + ` chapter-starts for tutorial "${tutorialSlug}"` + ); + } + + const skeleton = checkpoint.programSkeleton; + const embodyContext = new EmbodyDemoFromTutorial(tutorialSlug); + const jrProgram = StructuredProgramOps.fromSkeleton(skeleton, embodyContext); + const program = PytchProgramOps.fromStructuredProgram(jrProgram); + const assets = await embodyContext.allAddAssetDescriptors(); + + const linkedContentRef: LinkedContentRef = demoRequested + ? kLinkedContentRefNone + : { + kind: "jr-tutorial", + name: tutorialSlug, + interactionState: checkpoint.interactionState, + }; + + const summary = demoRequested + ? `This project is a demo of the tutorial "${tutorialSlug}"` + : `This project is following the tutorial "${tutorialSlug}"`; + + return { + summary, + linkedContentRef, + program, + assets, + }; }; export const tutorialCollection: ITutorialCollection = { @@ -162,9 +232,11 @@ export const tutorialCollection: ITutorialCollection = { actions.setSyncState(SyncState.Syncd); }), - createProjectFromTutorial: thunk(async (actions, tutorialSlug, helpers) => { + createProjectFromTutorial: thunk(async (actions, args, helpers) => { + const tutorialSlug = args.slug; + const navigateWithReplace = args.navigateWithReplace ?? false; await createProjectFromTutorial(actions, tutorialSlug, helpers, { - projectCreationArgs: async (tutorialSlug: string) => { + projectCreationArgs: async () => { const content = await tutorialContent(tutorialSlug); // TODO: Can this be tidied up? @@ -176,7 +248,13 @@ export const tutorialCollection: ITutorialCollection = { // mechanism. const options: CreateProjectOptions = await (async () => { switch (content.programKind) { - case "flat": + case "flat": { + if (args.chapterIndex !== 0) { + throw new Error( + 'cannot create project for "flat" tutorial other than at start' + ); + } + return { summary: `This project is following the tutorial "${tutorialSlug}"`, trackedTutorialRef: { @@ -185,37 +263,12 @@ export const tutorialCollection: ITutorialCollection = { }, program: PytchProgramOps.fromPythonCode(content.initialCode), }; + } case "per-method": { - const program = PytchProgramOps.newEmpty("per-method"); - - // This is clunky; see also other comment above, in the - // function `createProjectFromTutorial()`. - // - // We currently assume that all "per-method" tutorials - // should start empty except for a stage with a - // solid-white background. One day this might not always - // be true. - const stageId = program.program.actors[0].id; - const stageImageUrl = urlWithinApp("/assets/solid-white.png"); - const data = await fetchArrayBuffer(stageImageUrl); - const assets: Array = [ - { - name: `${stageId}/solid-white.png`, - mimeType: "image/png", - data, - }, - ]; - - return { - summary: `This project is following the tutorial "${tutorialSlug}"`, - linkedContentRef: { - kind: "jr-tutorial" as const, - name: tutorialSlug, - interactionState: { chapterIndex: 0, nTasksDone: 0 }, - }, - program, - assets, - }; + return jrTutorialCheckpointCreateOptions( + tutorialSlug, + args.chapterIndex + ); } default: return assertNever(content.programKind); @@ -230,12 +283,13 @@ export const tutorialCollection: ITutorialCollection = { completionAction: () => { helpers.getStoreActions().ideLayout.dismissButtonTour(); }, + navigateOptions: () => ({ replace: navigateWithReplace }), }); }), createDemoFromTutorial: thunk(async (actions, tutorialSlug, helpers) => { await createProjectFromTutorial(actions, tutorialSlug, helpers, { - projectCreationArgs: async (tutorialSlug: string) => { + projectCreationArgs: async () => { const content = await tutorialContent(tutorialSlug); const summary = `This project is a demo of the tutorial "${tutorialSlug}"`; const options: CreateProjectOptions = await (async () => { @@ -247,17 +301,7 @@ export const tutorialCollection: ITutorialCollection = { return { summary, program }; } case "per-method": { - const skeletonUrl = `${tutorialSlug}/skeleton-structured-program.json`; - const skeleton = await tutorialResourceParsedJson(skeletonUrl); - const embodyContext = new EmbodyDemoFromTutorial(tutorialSlug); - const structuredProgram = StructuredProgramOps.fromSkeleton( - skeleton, - embodyContext - ); - const program = - PytchProgramOps.fromStructuredProgram(structuredProgram); - const assets = await embodyContext.allAddAssetDescriptors(); - return { summary, program, assets }; + return jrTutorialCheckpointCreateOptions(tutorialSlug, -1); } default: return assertNever(content.programKind); diff --git a/src/model/ui.ts b/src/model/ui.ts index cab6c6899..800793240 100644 --- a/src/model/ui.ts +++ b/src/model/ui.ts @@ -75,6 +75,10 @@ import { KeyboardShortcutsHelpContent, keyboardShortcutsHelpContent, } from "./keyboard-shortcuts-help"; +import { + StartTutorialAtCheckpointFlow, + startTutorialAtCheckpointFlow, +} from "./user-interactions/start-tutorial-at-checkpoint"; export interface IStageDisplaySize { width: number; @@ -292,6 +296,7 @@ export interface IUserConfirmations { deleteManyProjectsFlow: DeleteManyProjectsFlow; createProjectFlow: CreateProjectFlow; + startTutorialAtCheckpointFlow: StartTutorialAtCheckpointFlow; addAssetsFlow: AddAssetsFlow; addClipArtFlow: AddClipArtFlow; renameAssetFlow: RenameAssetFlow; @@ -316,6 +321,7 @@ export const userConfirmations: IUserConfirmations = { deleteManyProjectsFlow, createProjectFlow, + startTutorialAtCheckpointFlow, addAssetsFlow, addClipArtFlow, renameAssetFlow, diff --git a/src/model/user-interactions/start-tutorial-at-checkpoint.ts b/src/model/user-interactions/start-tutorial-at-checkpoint.ts new file mode 100644 index 000000000..a565e1766 --- /dev/null +++ b/src/model/user-interactions/start-tutorial-at-checkpoint.ts @@ -0,0 +1,96 @@ +import { IPytchAppModel, PytchAppModelActions } from ".."; +import { failIfNull, parsedHtmlBody } from "../../utils"; +import { patchImageSrcURLs, tutorialResourceText } from "../tutorial"; +import { + alwaysSubmittable, + asyncUserFlowSlice, + AsyncUserFlowSlice, + noModalWithVoid, + VoidOutcome, +} from "./async-user-flow"; + +type StartTutorialAtCheckpointRunArgs = { + mSlug?: string; + mChapterIndexStr?: string; +}; + +type ValidatedRunArgs = { + slug: string; + chapterIndex: number; +}; + +type StartTutorialAtCheckpointRunState = ValidatedRunArgs & { + displayName: string; + displaySummary: Array; +}; + +export type StartTutorialAtCheckpointFlow = AsyncUserFlowSlice< + IPytchAppModel, + StartTutorialAtCheckpointRunArgs, + StartTutorialAtCheckpointRunState +>; + +function validatedArgs( + args: StartTutorialAtCheckpointRunArgs +): ValidatedRunArgs { + const slug = args.mSlug; + if (slug == null) throw new Error("no tutorial slug found in parameters"); + + const chapterIndex = parseInt(args.mChapterIndexStr ?? ""); + if (isNaN(chapterIndex)) + throw new Error("no/bad chapter index found in parameters"); + + return { slug, chapterIndex }; +} + +async function prepare( + args: StartTutorialAtCheckpointRunArgs +): Promise { + const { slug, chapterIndex } = validatedArgs(args); + + const summaryRelUrl = `${slug}/summary.html`; + const summaryHtmlText = await tutorialResourceText(summaryRelUrl); + const summaryHtml = parsedHtmlBody(summaryHtmlText, summaryRelUrl); + + // There is some duplication between here and allTutorialSummaries(). + // Tidy up somehow? + + let summaryDiv = failIfNull( + summaryHtml.querySelector("div.tutorial-summary"), + "no tutorial-summary div found" + ); + + const h1 = failIfNull(summaryDiv.querySelector("h1"), "no h1 found"); + const displayName = h1.innerText; + + patchImageSrcURLs(slug, summaryDiv); + + const displaySummary = Array.from(summaryDiv.childNodes).filter( + (node) => node.nodeName !== "H1" + ); + + return { slug, chapterIndex, displayName, displaySummary }; +} + +async function attempt( + runState: StartTutorialAtCheckpointRunState, + actions: PytchAppModelActions +): Promise { + await actions.tutorialCollection.createProjectFromTutorial({ + slug: runState.slug, + chapterIndex: runState.chapterIndex, + navigateWithReplace: true, + }); + + return noModalWithVoid; +} + +// No "pulse notable change" needed because the only non-error outcome +// is that the user is now looking at a newly-created project, making it +// obvious that the action has had the desired effect. + +export let startTutorialAtCheckpointFlow: StartTutorialAtCheckpointFlow = + asyncUserFlowSlice( + {}, + { prepare, isSubmittable: alwaysSubmittable, attempt } + ); diff --git a/src/pytch-ide.scss b/src/pytch-ide.scss index c03f0478a..f742f817b 100644 --- a/src/pytch-ide.scss +++ b/src/pytch-ide.scss @@ -26,19 +26,6 @@ $stage-controls-height: 37.6px; } .FullScreenStage { margin: auto; - - .run-stop-controls { - display: flex; - flex-direction: row; - - > * { - margin-left: 16px; - } - - > *:first-child { - margin-left: 0px; - } - } } } @@ -187,6 +174,7 @@ $stage-controls-height: 37.6px; background: white; display: block; visibility: visible; + box-shadow: rgba(99, 99, 99, 0.05) 0px 2px 8px 0px; &.resize-active { visibility: hidden; @@ -298,7 +286,6 @@ $speech-bubble-max-height: 180px; grid-template-rows: auto 1fr; position: relative; z-index: 0; - /* Override Ace's ordering of fonts to avoid Monaco-related mis-rendering in Safari. https://github.com/ajaxorg/ace/issues/3385 @@ -307,6 +294,10 @@ $speech-bubble-max-height: 180px; font-family: $monospace-fonts; } + .tab-content { + border-radius: 0 0 10px 10px; + } + .ReadOnlyOverlay { z-index: 10; position: absolute; @@ -438,11 +429,13 @@ $speech-bubble-max-height: 180px; } &.GreenFlag { - @include as-bootstrap-button(#2c2, #1a1); + @include as-bootstrap-button(#00B5B0, #00B5B0); + border-radius: 10px 0 0 10px; } &.RedStop { - @include as-bootstrap-button(#e33, #c11); + @include as-bootstrap-button(#FF5F38, #FF5F38); + border-radius: 0 10px 10px 0; } } @@ -956,3 +949,70 @@ ul.ClipArtTagButtonCollection { } } } + +.btn-primary { + @include as-bootstrap-button(#306998, #306998); +} + +.btn.square-button { + width: 36px; + height: 36px; + padding: 0; +} + +.run-stop-controls { + display: flex; + flex-direction: row; +} + +.moreOptionsDropdown button.btn.btn-primary { + background-color: transparent; + border-color: transparent; + color: black; +} + +.moreOptionsDropdown button.btn.btn-primary:hover { + background-color: #c2d5dd; + border-color: transparent; + color: black; +} + +.moreOptionsDropdown button.btn.btn-primary.show { + background-color: #c2d5dd; + border-color: transparent; + color: black; +} + +.moreOptionsDropdown button.btn.btn-primary.show:hover { + background-color: #c2d5dd; + border-color: transparent; + color: black; +} + +.moreOptionsDropdown button.btn.btn-primary::after { + display: none !important; +} + +.dropdown-menu.show { + //background-color: #EBF5F9; + //border-color: $pytch-colour-main-blue; + border: 0; + box-shadow: rgba(0, 0, 0, 0.05) 0px 6px 24px 0px, rgba(0, 0, 0, 0.08) 0px 0px 0px 1px; + padding: 6px; + border-radius: 10px; +} + +.dropdown-menu .dropdown-item { + border-radius: 10px; + margin: 4px 0; + padding-top: 6px; + padding-bottom: 6px; +} + +.dropdown-menu:first-child { + margin-top: 0px; +} + +.full-screen { + margin-right: auto; +} \ No newline at end of file diff --git a/src/pytch-jr-ide.scss b/src/pytch-jr-ide.scss index 1fdff99ed..76c9693da 100644 --- a/src/pytch-jr-ide.scss +++ b/src/pytch-jr-ide.scss @@ -42,6 +42,7 @@ .ActivityPane { @include grid-hstack-two($activity-bar-width, auto); + height: 100%; } @include grid-hstack-three( @@ -49,6 +50,8 @@ minmax($min-actor-properties-width, 1fr), auto ); + + background-color: #ebf5f9; } // The special-casing for each type of activity is a bit clunky but @@ -69,7 +72,7 @@ @include grid-hstack-three( $activity-bar-width + $help-sidebar-content-width, minmax($min-actor-properties-width, 1fr), - auto + min-content ); } } @@ -105,19 +108,24 @@ width: 100%; height: $activity-bar-tab-height; font-size: 1.5rem; + + color: $inactive-icon-option-color; + transition: color 0.15s ease-in-out; + button { - width: 100%; - height: 100%; - color: $inactive-icon-option-color; + width: calc($activity-bar-width - 4px); + height: calc($activity-bar-tab-height - 4px); + color: inherit; + background-color: inherit; border-width: 0px; border-style: none; - transition: color 0.15s ease-in-out; + padding-inline: 0px; } - &:hover button { + &:hover { color: $pytch-colour-accent-blue; } - &.isActive button { + &.isActive { color: $pytch-colour-main-blue; background-color: $pytch-colour-main-yellow; } @@ -178,29 +186,50 @@ @include min-dimens-zero(); @include grid-vstack-two(2fr, 1fr); - border-left: $panel-divider-border-spec; + //border-left: $panel-divider-border-spec; &.infoPanelIsCollapsed { @include grid-vstack-two(1fr, auto); } + + height: 100%; + padding-top: 6px; + padding-bottom: 6px; } .StageAndActorsOrAssets { @include min-dimens-zero(); @include grid-vstack-two(auto, 1fr); - border-left: $panel-divider-border-spec; + //border-left: $panel-divider-border-spec; .StageWithControls { - background-color: $main-bg-color; - padding: 6px; + background-color: #EBF5F9; + padding-bottom: 6px; + } + + .StageControls { + padding-left: 6px; } > *:nth-child(2) { position: relative; background-color: $panel-background; - border-top: $panel-divider-border-spec; + //border-top: $panel-divider-border-spec; + //background-color: $panel-background; + //border-top: $panel-divider-border-spec; + + background: rgba(255, 255, 255, 0.7); + border-radius: 16px; + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.05); + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + border: 1px solid rgba(255, 255, 255, 0.3); } + + height: 100%; + padding: 6px 6px 6px 0; + background-color: #ebf5f9; } //////////////////////////////////////////////////////////////////////// @@ -210,6 +239,8 @@ @include grid-vstack-two(auto, 1fr); position: relative; + border-radius: 10px; + background-color: $panel-background; .tab-content { @@ -225,11 +256,25 @@ > li:first-child > button { border-left: none; } + + button { + padding: 8px 20px; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + border-top-left-radius: 10px; + border-top-right-radius: 10px; + } + + ul.nav-tabs .nav-item:first-child button { + border-top-left-radius: 10px; } ul.nav { border-bottom-color: $panel-background; - background-color: $secondary-bg-color; + //background-color: $secondary-bg-color; + background-color: #f0f0f066; } button.nav-link { @@ -261,6 +306,15 @@ visibility: visible; } } + + height: 100%; + + background: rgba(255, 255, 255, 0.7); + border-radius: 16px; + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.05); + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + border: 1px solid rgba(255, 255, 255, 0.3); } .busy-overlay { @@ -285,13 +339,13 @@ } .Junior-InfoPanel-container { - border-top: $panel-divider-border-spec; + //border-top: $panel-divider-border-spec; button.collapse-button { font-size: 0.75rem; position: absolute; - top: 2px; - right: 3px; + top: 8px; + right: 8px; padding: 0px 8px; } @@ -306,6 +360,19 @@ display: none; } } + + height: 100%; + //margin-top: 6px; + + background: rgba(255, 255, 255, 0.7); + border-radius: 16px; + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.05); + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + border: 1px solid rgba(255, 255, 255, 0.3); + + display: flex; + flex-direction: column; } //////////////////////////////////////////////////////////////////////// @@ -391,6 +458,7 @@ &.isActive { background-color: $focused-item-background-glow; + border-radius: 10px; } .dropdown { @@ -432,8 +500,8 @@ position: relative; display: flex; flex-direction: column; - border: 3px solid; - border-radius: 2px; + border: 5px solid; + border-radius: 10px; background-color: white; .label, @@ -517,6 +585,8 @@ flex-direction: column; justify-content: center; background-color: #ddd; + border-top-left-radius: 5px; + border-top-right-radius: 5px; .asset-preview { text-align: center; @@ -569,6 +639,7 @@ border-top-left-radius: 0.75rem; background-color: $main-bg-color; opacity: 0.6; + border-bottom-right-radius: 20px; } .AddSomethingButtonStrip { @@ -594,7 +665,7 @@ .AddSomethingButton { span.label { - margin: 0.5rem 0px 0.5rem 1rem; + margin: 10px 10px 10px 15px; font-size: 1rem; } @@ -602,7 +673,6 @@ user-select: none; display: flex; flex-direction: row; - align-items: center; justify-content: center; min-height: 2.5rem; margin: 0.5rem; @@ -610,6 +680,10 @@ &.add-script { @include background-color-as-button($scratch-hat-block-background); color: $scratch-hat-block-text-colour; + + span.icon { + box-shadow: inset #e1bf58 0px 0px 4px; + } } &.add-sprite, &.add-flat-asset, @@ -622,11 +696,20 @@ color: white; border-radius: 0.75rem; + display: flex; + align-items: center; + min-height: 3rem; span.icon { font-size: 1rem; - margin-left: 0.75rem; - margin-right: 0.75rem; + background-color: $pytch-colour-main-blue; + border-radius: 0.75rem; + padding: 10px; + display: flex; + align-self: stretch; + align-items: center; + margin: 3px; + box-shadow: inset #234c6e 0px 0px 4px; } } } @@ -687,10 +770,15 @@ @include background-color-as-button($scratch-hat-block-background); cursor: pointer; display: block; - padding: 0px 6px 0px 2px; - border: 1px solid black; + //padding: 0px 6px 0px 2px; + //border: 1px solid black; border-radius: 4px; font-size: 0.875rem; + padding: 0 8px; + } + + .dropdown-toggle:after { + display: none; } } } @@ -942,3 +1030,129 @@ $key-chooser-width: 54rem; @include full-height-grid-center-center; color: #444; } + +.HelpSidebar code { + line-break: anywhere; +} + +.CodeEditor { + height: 100%; +} + +.gfs__actorprops__container, .gfs__actors__container { + height: 100%; + display: flex; + flex-direction: column; +} + +.gfs__actorprops__container > div:first-child, .gfs__actors__container > .ActorsList:first-child { + flex-grow: 1; +} + +.ActorsList { + margin: 0; + padding: 0; +} + +.AddSomethingButton.add-script span.icon { + background-color: #ffd450; +} + +.disclosure-button.expand-button { + border: none; +} +.resizablePanels { + height: 100vh; +} + +.customSeparator { + border-radius: 2px; + transition: 0.3s ease-in-out; +} + +.customSeparator:hover { + background-color: #306998; +} + +.customSeparator:focus { + background-color: #0060DF; + border: none; + outline: none; +} + +.horizontalSeparator { + width: 5px; + margin: 6px 0; +} + +.verticalSeparator { + height: 5px; +} + +.customSeparator .separatorIcon { + color: white; + display: block; + transition: 0.3s; + background-color: #87a3b9; + + border-radius: 2px; +} + +.horizontalSeparator .separatorIcon { + width: 5px; + padding: 20px 0; +} + +.verticalSeparator .separatorIcon { + height: 5px; + padding: 0 20px; +} + +.customSeparator:hover .separatorIcon, .customSeparator:focus .separatorIcon { + display: block; + color: white; + background-color: #87a3b9; +} + +.horizontalSeparator:hover .separatorIcon{ + padding: 25px 0; +} + +.horizontalSeparator:focus .separatorIcon { + padding: 10px 0; +} + +.verticalSeparator:hover .separatorIcon { + padding: 0 25px; +} + +.verticalSeparator:focus .separatorIcon { + padding: 0 10px; +} + +.HelpSidebar code { + line-break: anywhere; +} + +.CodeEditor { + height: 100%; +} + +.gfs__actorprops__container, .gfs__actors__container { + height: 100%; + display: flex; + flex-direction: column; +} + +.gfs__actorprops__container > div:first-child, .gfs__actors__container > .ActorsList:first-child { + flex-grow: 1; +} + +.ActorsList { + margin: 0; + padding: 0; +} + +.AddSomethingButton.add-script span.icon { + background-color: #ffd450; +} \ No newline at end of file diff --git a/src/pytch-navbar.scss b/src/pytch-navbar.scss index 793f82377..228943ff6 100644 --- a/src/pytch-navbar.scss +++ b/src/pytch-navbar.scss @@ -1,18 +1,18 @@ @import "./pytch-variables.scss"; -$navbar-single-row-height: 3.5rem; +$navbar-single-row-height: 70px; -@mixin yellow-hover-white() { - color: $pytch-colour-main-yellow; +@mixin white-hover-yellow() { + color: white; &:hover { - color: white; + color: $pytch-colour-main-yellow; } } .NavBar { display: flex; flex-direction: row; - justify-content: space-between; + justify-content: center; align-items: center; min-height: $navbar-single-row-height; @@ -26,7 +26,7 @@ $navbar-single-row-height: 3.5rem; font-size: 1.5rem; font-weight: bold; font-family: $pytch-title-font; - @include yellow-hover-white(); + @include white-hover-yellow(); } h1, @@ -58,7 +58,7 @@ $navbar-single-row-height: 3.5rem; margin: 0px; padding-top: 1rem; - @media screen and (min-width: $width-breakpoint-md) { + @media screen and (min-width: $width-breakpoint-lg) { display: block; position: relative; min-height: auto; @@ -69,9 +69,9 @@ $navbar-single-row-height: 3.5rem; li { padding: 12px 0px; - font-size: 20px; + font-size: 1rem; - @include yellow-hover-white(); + @include white-hover-yellow(); cursor: pointer; user-select: none; @@ -79,9 +79,9 @@ $navbar-single-row-height: 3.5rem; color: inherit; } - @media screen and (min-width: $width-breakpoint-md) { + @media screen and (min-width: $width-breakpoint-lg) { display: inline-block; - padding: 12px; + padding: 20px; } } } @@ -89,11 +89,48 @@ $navbar-single-row-height: 3.5rem; .burger-menu { margin-right: 1rem; font-size: 32px; - @include yellow-hover-white(); + @include white-hover-yellow(); cursor: pointer; - @media screen and (min-width: $width-breakpoint-md) { + @media screen and (min-width: $width-breakpoint-lg) { display: none; } } } + +.NavBarContent { + background-color: $pytch-colour-main-blue; + max-width: 1200px; + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 0 0 0 20px; + + @media screen and (min-width: $width-breakpoint-lg) { + padding: 0 30px; + } +} + +.title-and-version { + margin-right: auto; +} + +.NavBarContent ul li svg { + height: 1em; + margin-left: 5px; + margin-top: -9px; +} + +.NavBarContent ul li .contact-us-link svg { + margin: 0; +} + +.NavBarContent ul li:hover svg { + stroke: $pytch-colour-main-yellow; +} + +.NavBarContent ul li svg { + stroke: white; +} \ No newline at end of file diff --git a/src/pytch-variables.scss b/src/pytch-variables.scss index d52284bc4..2551a7f4a 100644 --- a/src/pytch-variables.scss +++ b/src/pytch-variables.scss @@ -46,7 +46,7 @@ $text-very-subdued-color: #aaa; $min-lesson-column-width: 27rem; $min-actor-properties-width: 24rem; -$activity-bar-width: 2.5rem; +$activity-bar-width: 2.75rem; $activity-bar-tab-height: 2.75rem; $panel-divider-border-spec: 1px solid $panel-divider-color; diff --git a/src/pytch.scss b/src/pytch.scss index aacf9e7d4..4395d4c2e 100644 --- a/src/pytch.scss +++ b/src/pytch.scss @@ -29,7 +29,6 @@ code { bottom: 0; display: flex; flex-direction: column; - background-color: $main-bg-color; overflow-y: auto; } @@ -349,6 +348,12 @@ button.awaiting-action { } } +.ErrorMessageDisplay { + .technical-details .error-message { + font-style: italic; + } +} + .ExceptionDisplay { @include full-center-middle(); @@ -369,7 +374,26 @@ button.awaiting-action { } } +.TutorialList .ExceptionDisplay { + height: auto; + margin-top: 3rem; +} + .EditorKindThumbnail { margin-right: 1.25rem; box-shadow: 0px 0px 6px 4px $box-shadow-color; } + +.Junior-AppearancesList-container { + display: flex; + flex-direction: column; + height: 100%; +} + +.Junior-AssetsList { + flex-grow: 1; +} + +.tab-pane.active { + height: 100%; +} \ No newline at end of file diff --git a/src/storage/mime-types.ts b/src/storage/mime-types.ts index 94ef485d3..4ba080740 100644 --- a/src/storage/mime-types.ts +++ b/src/storage/mime-types.ts @@ -28,6 +28,7 @@ export const typeFromExtension = (() => { const extensionsWithTypes = JSON.parse("[[\"3gpp\", \"audio/3gpp\"], [\"adts\", \"audio/aac\"], [\"aac\", \"audio/aac\"], [\"adp\", \"audio/adpcm\"], [\"amr\", \"audio/amr\"], [\"au\", \"audio/basic\"], [\"snd\", \"audio/basic\"], [\"mid\", \"audio/midi\"], [\"midi\", \"audio/midi\"], [\"kar\", \"audio/midi\"], [\"rmi\", \"audio/midi\"], [\"mxmf\", \"audio/mobile-xmf\"], [\"mp3\", \"audio/mpeg\"], [\"m4a\", \"audio/mp4\"], [\"mp4a\", \"audio/mp4\"], [\"mpga\", \"audio/mpeg\"], [\"mp2\", \"audio/mpeg\"], [\"mp2a\", \"audio/mpeg\"], [\"m2a\", \"audio/mpeg\"], [\"m3a\", \"audio/mpeg\"], [\"oga\", \"audio/ogg\"], [\"ogg\", \"audio/ogg\"], [\"spx\", \"audio/ogg\"], [\"opus\", \"audio/ogg\"], [\"s3m\", \"audio/s3m\"], [\"sil\", \"audio/silk\"], [\"uva\", \"audio/vnd.dece.audio\"], [\"uvva\", \"audio/vnd.dece.audio\"], [\"eol\", \"audio/vnd.digital-winds\"], [\"dra\", \"audio/vnd.dra\"], [\"dts\", \"audio/vnd.dts\"], [\"dtshd\", \"audio/vnd.dts.hd\"], [\"lvp\", \"audio/vnd.lucent.voice\"], [\"pya\", \"audio/vnd.ms-playready.media.pya\"], [\"ecelp4800\", \"audio/vnd.nuera.ecelp4800\"], [\"ecelp7470\", \"audio/vnd.nuera.ecelp7470\"], [\"ecelp9600\", \"audio/vnd.nuera.ecelp9600\"], [\"rip\", \"audio/vnd.rip\"], [\"wav\", \"audio/wav\"], [\"weba\", \"audio/webm\"], [\"aif\", \"audio/x-aiff\"], [\"aiff\", \"audio/x-aiff\"], [\"aifc\", \"audio/x-aiff\"], [\"caf\", \"audio/x-caf\"], [\"flac\", \"audio/x-flac\"], [\"mka\", \"audio/x-matroska\"], [\"m3u\", \"audio/x-mpegurl\"], [\"wax\", \"audio/x-ms-wax\"], [\"wma\", \"audio/x-ms-wma\"], [\"ram\", \"audio/x-pn-realaudio\"], [\"ra\", \"audio/x-pn-realaudio\"], [\"rmp\", \"audio/x-pn-realaudio-plugin\"], [\"xm\", \"audio/xm\"], [\"exr\", \"image/aces\"], [\"apng\", \"image/apng\"], [\"avci\", \"image/avci\"], [\"avcs\", \"image/avcs\"], [\"avif\", \"image/avif\"], [\"bmp\", \"image/bmp\"], [\"dib\", \"image/bmp\"], [\"cgm\", \"image/cgm\"], [\"drle\", \"image/dicom-rle\"], [\"dpx\", \"image/dpx\"], [\"emf\", \"image/emf\"], [\"fits\", \"image/fits\"], [\"g3\", \"image/g3fax\"], [\"gif\", \"image/gif\"], [\"heic\", \"image/heic\"], [\"heics\", \"image/heic-sequence\"], [\"heif\", \"image/heif\"], [\"heifs\", \"image/heif-sequence\"], [\"hej2\", \"image/hej2k\"], [\"hsj2\", \"image/hsj2\"], [\"ief\", \"image/ief\"], [\"jls\", \"image/jls\"], [\"jp2\", \"image/jp2\"], [\"jpg2\", \"image/jp2\"], [\"jpeg\", \"image/jpeg\"], [\"jpg\", \"image/jpeg\"], [\"jpe\", \"image/jpeg\"], [\"jph\", \"image/jph\"], [\"jhc\", \"image/jphc\"], [\"jpm\", \"image/jpm\"], [\"jpgm\", \"image/jpm\"], [\"jpx\", \"image/jpx\"], [\"jpf\", \"image/jpx\"], [\"jxr\", \"image/jxr\"], [\"jxra\", \"image/jxra\"], [\"jxrs\", \"image/jxrs\"], [\"jxs\", \"image/jxs\"], [\"jxsc\", \"image/jxsc\"], [\"jxsi\", \"image/jxsi\"], [\"jxss\", \"image/jxss\"], [\"ktx\", \"image/ktx\"], [\"ktx2\", \"image/ktx2\"], [\"png\", \"image/png\"], [\"btif\", \"image/prs.btif\"], [\"btf\", \"image/prs.btif\"], [\"pti\", \"image/prs.pti\"], [\"sgi\", \"image/sgi\"], [\"svg\", \"image/svg+xml\"], [\"svgz\", \"image/svg+xml\"], [\"t38\", \"image/t38\"], [\"tif\", \"image/tiff\"], [\"tiff\", \"image/tiff\"], [\"tfx\", \"image/tiff-fx\"], [\"psd\", \"image/vnd.adobe.photoshop\"], [\"azv\", \"image/vnd.airzip.accelerator.azv\"], [\"uvi\", \"image/vnd.dece.graphic\"], [\"uvvi\", \"image/vnd.dece.graphic\"], [\"uvg\", \"image/vnd.dece.graphic\"], [\"uvvg\", \"image/vnd.dece.graphic\"], [\"djvu\", \"image/vnd.djvu\"], [\"djv\", \"image/vnd.djvu\"], [\"sub\", \"image/vnd.dvb.subtitle\"], [\"dwg\", \"image/vnd.dwg\"], [\"dxf\", \"image/vnd.dxf\"], [\"fbs\", \"image/vnd.fastbidsheet\"], [\"fpx\", \"image/vnd.fpx\"], [\"fst\", \"image/vnd.fst\"], [\"mmr\", \"image/vnd.fujixerox.edmics-mmr\"], [\"rlc\", \"image/vnd.fujixerox.edmics-rlc\"], [\"ico\", \"image/vnd.microsoft.icon\"], [\"dds\", \"image/vnd.ms-dds\"], [\"mdi\", \"image/vnd.ms-modi\"], [\"wdp\", \"image/vnd.ms-photo\"], [\"npx\", \"image/vnd.net-fpx\"], [\"b16\", \"image/vnd.pco.b16\"], [\"tap\", \"image/vnd.tencent.tap\"], [\"vtf\", \"image/vnd.valve.source.texture\"], [\"wbmp\", \"image/vnd.wap.wbmp\"], [\"xif\", \"image/vnd.xiff\"], [\"pcx\", \"image/vnd.zbrush.pcx\"], [\"webp\", \"image/webp\"], [\"wmf\", \"image/wmf\"], [\"3ds\", \"image/x-3ds\"], [\"ras\", \"image/x-cmu-raster\"], [\"cmx\", \"image/x-cmx\"], [\"fh\", \"image/x-freehand\"], [\"fhc\", \"image/x-freehand\"], [\"fh4\", \"image/x-freehand\"], [\"fh5\", \"image/x-freehand\"], [\"fh7\", \"image/x-freehand\"], [\"jng\", \"image/x-jng\"], [\"sid\", \"image/x-mrsid-image\"], [\"pic\", \"image/x-pict\"], [\"pct\", \"image/x-pict\"], [\"pnm\", \"image/x-portable-anymap\"], [\"pbm\", \"image/x-portable-bitmap\"], [\"pgm\", \"image/x-portable-graymap\"], [\"ppm\", \"image/x-portable-pixmap\"], [\"rgb\", \"image/x-rgb\"], [\"tga\", \"image/x-tga\"], [\"xbm\", \"image/x-xbitmap\"], [\"xpm\", \"image/x-xpixmap\"], [\"xwd\", \"image/x-xwindowdump\"]]") const typeFromExtensionMap = new Map(extensionsWithTypes); - return (extension: string) => typeFromExtensionMap.get(extension) || false; + return (extension: string) => + typeFromExtensionMap.get(extension.toLowerCase()) || false; })(); diff --git a/src/storage/refresh_mime_types.py b/src/storage/refresh_mime_types.py index b2615f8c5..2b19f6dc9 100644 --- a/src/storage/refresh_mime_types.py +++ b/src/storage/refresh_mime_types.py @@ -60,7 +60,8 @@ export const typeFromExtension = (() => {{ const extensionsWithTypes = JSON.parse({data_json_str}) const typeFromExtensionMap = new Map(extensionsWithTypes); - return (extension: string) => typeFromExtensionMap.get(extension) || false; + return (extension: string) => + typeFromExtensionMap.get(extension.toLowerCase()) || false; }})(); """ diff --git a/src/storage/zipfile.ts b/src/storage/zipfile.ts index 9449a47ce..d6c212a6f 100644 --- a/src/storage/zipfile.ts +++ b/src/storage/zipfile.ts @@ -373,7 +373,6 @@ const parseZipfile_V2_V3_V4 = async ( : PytchProgramOps.fromJson(codeTextOrJson); const rawProjectMetadata = await _jsonOrFail(zip, "meta.json", bareError); - console.log(rawProjectMetadata, typeof rawProjectMetadata); if ( typeof rawProjectMetadata !== "object" || rawProjectMetadata == null || diff --git a/unit-tests/models/asset.spec.ts b/unit-tests/models/asset.spec.ts index a9b245419..80cded48a 100644 --- a/unit-tests/models/asset.spec.ts +++ b/unit-tests/models/asset.spec.ts @@ -10,8 +10,6 @@ import { UuidOps, } from "../../src/model/junior/structured-program"; -globalThis.crypto = globalThis.crypto ?? crypto; - describe("Asset operations", () => { describe("mime-type operations", () => { const Ops = AssetMetaDataOps; diff --git a/vite.config.mts b/vite.config.mts index 654b0be73..50c2e61a3 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -4,7 +4,7 @@ export default defineConfig({ server: { port: 3000 }, preview: { port: 3000 }, build: { - chunkSizeWarningLimit: 2000, + chunkSizeWarningLimit: 2500, rollupOptions: { onwarn(warning, warn) { if (warning.code === "MODULE_LEVEL_DIRECTIVE") return;