From 2bd4a09f43cc97299f6da63c79a6b717c53df4f2 Mon Sep 17 00:00:00 2001 From: lingdocs <71590811+lingdocs@users.noreply.github.com> Date: Mon, 9 May 2022 12:57:56 -0500 Subject: [PATCH] some rough equative games --- package.json | 2 +- src/content/equatives/present-equative.mdx | 6 + src/games/GameCore.tsx | 41 ++-- src/games/GameDisplay.tsx | 40 +++- src/games/GamesBrowser.tsx | 4 +- src/games/games.tsx | 70 +++++- src/games/sub-cores/EquativeGame.tsx | 265 +++++++++++++++++++++ src/games/sub-cores/GenderGame.tsx | 20 +- src/games/sub-cores/UnisexNounGame.tsx | 19 +- src/lib/game-utils.ts | 4 - src/types/game-types.d.ts | 4 +- yarn.lock | 8 +- 12 files changed, 424 insertions(+), 59 deletions(-) create mode 100644 src/games/sub-cores/EquativeGame.tsx diff --git a/package.json b/package.json index 2c48721..32a149b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "dependencies": { "@fortawesome/fontawesome-free": "^5.15.4", "@lingdocs/lingdocs-main": "^0.2.0", - "@lingdocs/pashto-inflector": "^2.4.3", + "@lingdocs/pashto-inflector": "^2.4.9", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", diff --git a/src/content/equatives/present-equative.mdx b/src/content/equatives/present-equative.mdx index 92600f4..1eee61c 100644 --- a/src/content/equatives/present-equative.mdx +++ b/src/content/equatives/present-equative.mdx @@ -24,6 +24,10 @@ import { addToForm, InlinePs, } from "@lingdocs/pashto-inflector"; +import { + equativeGamePresent, +} from "../../games/games"; +import GameDisplay from "../../games/GameDisplay"; The [equative](https://en.wikipedia.org/wiki/Equative) might be the most basic way of joining words together. We use it to say that something *is/equals* something else. It's kind of like an equals "=" sign in math. @@ -212,3 +216,5 @@ When you're asking a question, all you have to do is change the intonation. You {[ { p: "خو ته یې افعان", f: "kho tu ye afgháan", e: "But you are Afghan!" } ]} + + diff --git a/src/games/GameCore.tsx b/src/games/GameCore.tsx index 213a592..e176ef2 100644 --- a/src/games/GameCore.tsx +++ b/src/games/GameCore.tsx @@ -20,30 +20,29 @@ import { } from "@lingdocs/pashto-inflector"; const errorVibration = 200; -function GameCore({ questions, Display, timeLimit, Instructions, studyLink, id }:{ +function GameCore({ questions, Display, timeLimit, Instructions, studyLink, id, onStartStop }:{ id: string, studyLink: string, Instructions: (props: { opts?: Types.TextOptions }) => JSX.Element, questions: () => QuestionGenerator, Display: (props: QuestionDisplayProps) => JSX.Element, timeLimit: number; + onStartStop: (a: "start" | "stop") => void, }) { // TODO: report pass with id to user info const rewardRef = useRef(null); const { user, pullUser, setUser } = useUser(); - const [finish, setFinish] = useState(null); + const [finish, setFinish] = useState(undefined); const [current, setCurrent] = useState | undefined>(undefined); const [questionBox, setQuestionBox] = useState>(questions()); const [timerKey, setTimerKey] = useState(1); - function handleCallback(correct: boolean) { - if (correct) handleAdvance(); - else handleFailure(); - } - function handleFailure() { - // rewardRef.current?.punishMe(); - setFinish("fail"); - navigator.vibrate(errorVibration); + function handleCallback(correct: true | JSX.Element) { + if (correct === true) handleAdvance(); + else { + setFinish({ msg: "fail", answer: correct }); + navigator.vibrate(errorVibration); + } } function handleAdvance() { const next = questionBox.next(); @@ -69,6 +68,7 @@ function GameCore({ questions, Display, timeLimit, Instructions, studyLink, i }).catch(console.error); } function handleFinish() { + onStartStop("stop"); setFinish("pass"); rewardRef.current?.rewardMe(); if (!user) return; @@ -80,20 +80,23 @@ function GameCore({ questions, Display, timeLimit, Instructions, studyLink, i handleResult(result); } function handleQuit() { - setFinish(null); + onStartStop("stop"); + setFinish(undefined); setCurrent(undefined); } function handleRestart() { + onStartStop("start"); const newQuestionBox = questions(); const { value } = newQuestionBox.next(); // just for type safety -- the generator will have at least one question if (!value) return; setQuestionBox(newQuestionBox); - setFinish(null); + setFinish(undefined); setCurrent(value); setTimerKey(prev => prev + 1); } function handleTimeOut() { + onStartStop("stop"); setFinish("time out"); navigator.vibrate(errorVibration); } @@ -107,7 +110,7 @@ function GameCore({ questions, Display, timeLimit, Instructions, studyLink, i } const progressColor = finish === "pass" ? "success" - : finish === "fail" + : typeof finish === "object" ? "danger" : "primary"; return
@@ -130,7 +133,7 @@ function GameCore({ questions, Display, timeLimit, Instructions, studyLink, i
}
- {finish === null && + {finish === undefined && (current ?
@@ -151,8 +154,12 @@ function GameCore({ questions, Display, timeLimit, Instructions, studyLink, i
} - {(finish === "fail" || finish === "time out") &&
+ {(typeof finish === "object" || finish === "time out") &&

{failMessage(current?.progress, finish)}

+ {typeof finish === "object" &&
+
The correct answer was:
+ {finish?.answer} +
}
@@ -168,7 +175,7 @@ function GameCore({ questions, Display, timeLimit, Instructions, studyLink, i
; } -function failMessage(progress: Progress | undefined, finish: "time out" | "fail"): string { +function failMessage(progress: Progress | undefined, finish: "time out" | { msg: "fail", answer: JSX.Element }): string { const pDone = progress ? getPercentageDone(progress) : 0; const { message, face } = pDone < 20 ? { message: "No, sorry", face: "😑" } @@ -179,7 +186,7 @@ function failMessage(progress: Progress | undefined, finish: "time out" | "fail" : pDone < 78 ? { message: "You almost got it!", face: "😩" } : { message: "Nooo! So close!", face: "😭" }; - return finish === "fail" + return typeof finish === "object" ? `${message} ${face}` : `⏳ Time's Up ${face}`; } diff --git a/src/games/GameDisplay.tsx b/src/games/GameDisplay.tsx index 6713a28..898c717 100644 --- a/src/games/GameDisplay.tsx +++ b/src/games/GameDisplay.tsx @@ -1,22 +1,44 @@ import { useUser } from "../user-context"; +import { useState } from "react"; function GameDisplay({ record: { title, Game, id } }: { record: GameRecord }) { const { user } = useUser(); + const [running, setRunning] = useState(false); const completed = user?.tests.some((t) => ( // TODO: Or if it's in the locally stored (unposted test results) (t.done === true) && (t.id === id) )); - return
-
-
-

🎮 {title}

-
-
-

{completed ? "✅" : ""}

+ function onStartStop(a: "start" | "stop") { + if (a === "start" && !running) { + setRunning(true); + } + if (a === "stop") { + setRunning(false); + } + } + return <> + {running &&
} +
+
+
+

🎮 {title}

+
+
+

{completed ? "✅" : ""}

+
+ {Game(onStartStop)}
- -
+ } export default GameDisplay; \ No newline at end of file diff --git a/src/games/GamesBrowser.tsx b/src/games/GamesBrowser.tsx index d5a05de..33ddf09 100644 --- a/src/games/GamesBrowser.tsx +++ b/src/games/GamesBrowser.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import { useState } from "react"; import games from "./games"; import { useUser } from "../user-context"; import Link from "../components/Link"; @@ -37,7 +37,7 @@ function GamesBrowser() {
- + {Game(() => null)}
})} diff --git a/src/games/games.tsx b/src/games/games.tsx index 6b00a25..441e89a 100644 --- a/src/games/games.tsx +++ b/src/games/games.tsx @@ -1,3 +1,4 @@ +import EquativeGame from "./sub-cores/EquativeGame"; import GenderGame from "./sub-cores/GenderGame"; import UnisexNounGame from "./sub-cores/UnisexNounGame"; @@ -5,7 +6,7 @@ function makeGameRecord( title: string, id: string, studyLink: string, - game: (id: string, link: string) => (() => JSX.Element), + game: (id: string, link: string) => ((s: (a: "start" | "stop") => void) => JSX.Element), ): GameRecord { return { title, @@ -19,19 +20,68 @@ export const nounGenderGame1 = makeGameRecord( "Identify Noun Genders - Level 1", "gender-nouns-1", "/nouns/nouns-gender#gender-by-ending", - (id, link) => () => , + (id, link) => (s: (a: "start" | "stop") => void) => , ); export const nounGenderGame2 = makeGameRecord( "Identify Noun Genders - Level 2", "gender-nouns-2", "/nouns/nouns-gender#exceptions", - (id, link) => () => , + (id, link) => (s: (a: "start" | "stop") => void) => , ); export const unisexNounGame = makeGameRecord( "Changing genders on unisex nouns", "unisex-nouns-1", "/nouns/nouns-unisex/", - (id, link) => () => , + (id, link) => (s: (a: "start" | "stop") => void) => , +); + +export const equativeGamePresent = makeGameRecord( + "Write the present equative", + "equative-present", + "/equatives/present-equative/", + (id, link) => (s: (a: "start" | "stop") => void) => , +); + +export const equativeGameHabitual = makeGameRecord( + "Write the habitual equative", + "equative-habitual", + "/equatives/habitual-equative/", + (id, link) => (s: (a: "start" | "stop") => void) => , +); + +export const equativeGameSubjunctive = makeGameRecord( + "Write the subjunctive equative", + "equative-subjunctive", + "/equatives/other-equatives/#subjunctive-equative", + (id, link) => (s: (a: "start" | "stop") => void) => , +); + +export const equativeGameFuture = makeGameRecord( + "Write the future equative", + "equative-future", + "/equatives/other-equatives/#future-equative", + (id, link) => (s: (a: "start" | "stop") => void) => , +); + +export const equativeGamePast = makeGameRecord( + "Write the past equative", + "equative-past", + "/equatives/other-equatives/#past-equative", + (id, link) => (s: (a: "start" | "stop") => void) => , +); + +export const equativeGameWouldBe = makeGameRecord( + 'Write the "would be" equative', + "equative-would-be", + "equatives/other-equatives/#would-be-equative", + (id, link) => (s: (a: "start" | "stop") => void) => , +); + +export const equativeGamePastSubjunctive = makeGameRecord( + 'Write the past subjunctive equative', + "equative-past-subjunctive", + "/equatives/other-equatives/#past-subjunctive", + (id, link) => (s: (a: "start" | "stop") => void) => , ); const games: { chapter: string, items: GameRecord[] }[] = [ @@ -43,6 +93,18 @@ const games: { chapter: string, items: GameRecord[] }[] = [ unisexNounGame, ], }, + { + chapter: "Equatives", + items: [ + equativeGamePresent, + equativeGameHabitual, + equativeGameSubjunctive, + equativeGameFuture, + equativeGamePast, + equativeGameWouldBe, + equativeGamePastSubjunctive, + ], + }, ]; export default games; diff --git a/src/games/sub-cores/EquativeGame.tsx b/src/games/sub-cores/EquativeGame.tsx new file mode 100644 index 0000000..8c33c5f --- /dev/null +++ b/src/games/sub-cores/EquativeGame.tsx @@ -0,0 +1,265 @@ +import { useState } from "react"; +import { + makeProgress, +} from "../../lib/game-utils"; +import GameCore from "../GameCore"; +import { + Types as T, + Examples, + defaultTextOptions as opts, + standardizePashto, + typePredicates as tp, + makeNounSelection, + randFromArray, + renderEP, + compileEP, + flattenLengths, + randomPerson, + InlinePs, + grammarUnits, +} from "@lingdocs/pashto-inflector"; + +const kidsColor = "#017BFE"; + +// @ts-ignore +const nouns: T.NounEntry[] = [ + {"ts":1527815251,"i":7790,"p":"سړی","f":"saRéy","g":"saRey","e":"man","c":"n. m.","ec":"man","ep":"men"}, + {"ts":1527812797,"i":8605,"p":"ښځه","f":"xúdza","g":"xudza","e":"woman, wife","c":"n. f.","ec":"woman","ep":"women"}, + {"ts":1527812881,"i":11691,"p":"ماشوم","f":"maashoom","g":"maashoom","e":"child, kid","c":"n. m. anim. unisex","ec":"child","ep":"children"}, + {"ts":1527815197,"i":2503,"p":"پښتون","f":"puxtoon","g":"puxtoon","e":"Pashtun","c":"n. m. anim. unisex / adj.","infap":"پښتانه","infaf":"puxtaanu","infbp":"پښتن","infbf":"puxtan"}, + {"ts":1527815737,"i":484,"p":"استاذ","f":"Ustaaz","g":"Ustaaz","e":"teacher, professor, expert, master (in a field)","c":"n. m. anim. unisex anim.","ec":"teacher"}, +].filter(tp.isNounEntry); + +// @ts-ignore +const adjectives: T.AdjectiveEntry[] = [ + {"ts":1527815306,"i":7582,"p":"ستړی","f":"stúRey","g":"stuRey","e":"tired","c":"adj."}, + {"ts":1527812625,"i":9116,"p":"غټ","f":"ghuT, ghaT","g":"ghuT,ghaT","e":"big, fat","c":"adj."}, + {"ts":1527812792,"i":5817,"p":"خوشاله","f":"khoshaala","g":"khoshaala","e":"happy, glad","c":"adj."}, + {"ts":1527812796,"i":8641,"p":"ښه","f":"xu","g":"xu","e":"good","c":"adj."}, + {"ts":1527812798,"i":5636,"p":"خفه","f":"khúfa","g":"khufa","e":"sad, upset, angry; choked, suffocated","c":"adj."}, +].filter(tp.isAdjectiveEntry); + +// @ts-ignore +const locAdverbs: T.LocativeAdverbEntry[] = [ + {"ts":1527812558,"i":6241,"p":"دلته","f":"dălta","g":"dalta","e":"here","c":"loc. adv."}, + {"ts":1527812449,"i":13937,"p":"هلته","f":"hálta, álta","g":"halta,alta","e":"there","c":"loc. adv."}, +].filter(tp.isLocativeAdverbEntry); + +const amount = 20; +const timeLimit = 55; + +type Question = { + phrase: { ps: T.PsString, e?: string[] }, + equative: T.EquativeRendered, +}; + +const pronounTypes = [ + [T.Person.FirstSingMale, T.Person.FirstSingFemale], + [T.Person.SecondSingMale, T.Person.SecondSingFemale], + [T.Person.ThirdSingMale], + [T.Person.ThirdSingFemale], + [T.Person.FirstPlurMale, T.Person.FirstPlurFemale], + [T.Person.SecondPlurMale, T.Person.SecondPlurFemale], + [T.Person.ThirdPlurMale, T.Person.ThirdPlurFemale], +]; + +export default function EquativeGame({ id, link, tense, onStartStop }: { id: string, link: string, tense: T.EquativeTense, onStartStop: (a: "start" | "stop") => void }) { + function* questions (): Generator> { + let pool = [...pronounTypes]; + function makeRandPronoun(): T.PronounSelection { + let person: T.Person; + console.log(pool); + do { + person = randomPerson(); + // eslint-disable-next-line + } while (!pool.some(p => p.includes(person))); + pool = pool.filter(p => !p.includes(person)); + if (pool.length === 0) { + pool = pronounTypes; + } + return { + type: "pronoun", + distance: "far", + person, + }; + } + function makeRandomNoun(): T.NounSelection { + const n = makeNounSelection(randFromArray(nouns), undefined); + return { + ...n, + gender: n.genderCanChange ? randFromArray(["masc", "fem"]) : n.gender, + number: n.numberCanChange ? randFromArray(["singular", "plural"]) : n.number, + }; + } + for (let i = 0; i < amount; i++) { + console.log("one factory call"); + const subj = randFromArray([ + makeRandPronoun, + makeRandPronoun, + makeRandomNoun, + makeRandPronoun, + ])(); + const pred = randFromArray([...adjectives, ...locAdverbs]); + const EPS = makeEPS(subj, pred, tense); + const rendered = renderEP(EPS); + const compiled = compileEP(rendered, true, { equative: true, ba: false, kidsSection: true }); + const phrase = { + ps: compiled.ps[0], + e: compiled.e, + }; + yield { + progress: makeProgress(i, amount), + question: { + phrase, + equative: rendered.equative, + }, + }; + }; + } + + + function Display({ question, callback }: QuestionDisplayProps) { + const [answer, setAnswer] = useState(""); + const [withBa, setWithBa] = useState(false); + const handleInput = ({ target: { value }}: React.ChangeEvent) => { + setAnswer(value); + } + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const given = standardizePashto(answer.trim()); + const correct = checkAnswer(given, question.equative.ps) + && withBa === question.equative.hasBa; + if (correct) { + setAnswer(""); + } + callback(!correct + ?
+
+ {flattenLengths(question.equative.ps).reduce(((accum, curr, i): JSX.Element[] => ( + [ + ...accum, + ...i > 0 ? [ or ] : [], + {curr.p}, + ] + )), [] as JSX.Element[])} +
+
{question.equative.hasBa ? "with" : "without"} a {grammarUnits.baParticle} in the kids' section.
+
+ : true); + } + + return
+
+ {/* + @ts-ignore */} + + {question.phrase.e && question.phrase.e.map(e => ( +
{e}
+ ))} +
+
+
+ +
+
+
+ setWithBa(e.target.checked)} + /> + +
+
+ {/*
*/} + + {/*
*/} + {/*
+ Type Enter to check +
*/} +
+
+
+ +
+ } + + function Instructions() { + return
+

Fill in the blank with the correct {humanReadableTense(tense)} equative in Pashto script

+
+ } + + return +}; + +function modExs(exs: T.PsString[]): { p: JSX.Element, f: JSX.Element }[] { + return exs.map(ps => { + if (!ps.p.includes(" ___ ")) { + return { + p: <>{ps.p}, + f: <>{ps.f}, + }; + } + const splitP = ps.p.split(" ___ "); + const splitF = ps.f.split(" ___ "); + return { + p: <>{splitP[0]} ___ {splitP[1]}, + f: <>{splitF[0]} ___ {splitF[1]}, + }; + }); +} + +function humanReadableTense(tense: T.EquativeTense): string { + return tense === "pastSubjunctive" + ? "past subjunctive" + : tense === "wouldBe" + ? `"would be"` + : tense; +} + +function checkAnswer(given: string, answer: T.SingleOrLengthOpts): boolean { + const possible = flattenLengths(answer); + return possible.some(x => given === x.p); +} + +function makeEPS(subject: T.NPSelection, predicate: T.AdjectiveEntry | T.LocativeAdverbEntry, tense: T.EquativeTense): T.EPSelectionComplete { + return { + subject, + predicate: { + type: "Complement", + selection: tp.isAdjectiveEntry(predicate) ? { + type: "adjective", + entry: predicate, + } : { + type: "loc. adv.", + entry: predicate, + }, + }, + equative: { + tense, + negative: false, + }, + omitSubject: false, + }; +} \ No newline at end of file diff --git a/src/games/sub-cores/GenderGame.tsx b/src/games/sub-cores/GenderGame.tsx index 422f0f1..e5e3d7b 100644 --- a/src/games/sub-cores/GenderGame.tsx +++ b/src/games/sub-cores/GenderGame.tsx @@ -1,5 +1,4 @@ import { - getRandomFromList, makeProgress, } from "../../lib/game-utils"; import genderColors from "../../lib/gender-colors"; @@ -14,6 +13,7 @@ import { isUnisexSet, typePredicates as tp, firstVariation, + randFromArray, } from "@lingdocs/pashto-inflector"; import { nouns } from "../../words/words"; import { categorize } from "../../lib/categorize"; @@ -86,7 +86,7 @@ const exceptions: Record = { const amount = 35; -export default function GenderGame({level, id, link}: { level: 1 | 2, id: string, link: string }) { +export default function GenderGame({level, id, link, onStartStop }: { level: 1 | 2, id: string, link: string, onStartStop: (a: "start" | "stop") => void }) { function* questions () { const wordPool = {...types}; const exceptionsPool = {...exceptions}; @@ -94,13 +94,13 @@ export default function GenderGame({level, id, link}: { level: 1 | 2, id: string for (let i = 0; i < amount; i++) { const base = level === 1 ? wordPool - : getRandomFromList([wordPool, exceptionsPool]); - const gender = getRandomFromList(genders); + : randFromArray([wordPool, exceptionsPool]); + const gender = randFromArray(genders); let typeToUse: string; do { - typeToUse = getRandomFromList(Object.keys(base[gender])); + typeToUse = randFromArray(Object.keys(base[gender])); } while (!base[gender][typeToUse].length); - const question = getRandomFromList(base[gender][typeToUse]); + const question = randFromArray(base[gender][typeToUse]); base[gender][typeToUse] = base[gender][typeToUse].filter((entry) => entry.ts !== question.ts); yield { progress: makeProgress(i, amount), @@ -111,7 +111,10 @@ export default function GenderGame({level, id, link}: { level: 1 | 2, id: string function Display({ question, callback }: QuestionDisplayProps) { function check(gender: "m" | "f") { - callback(!nounNotIn(gender === "m" ? mascNouns : femNouns)(question)); + const correct = !nounNotIn(gender === "m" ? mascNouns : femNouns)(question); + callback(!correct + ?
ANSWER HERE
+ : true); } return
@@ -132,12 +135,13 @@ export default function GenderGame({level, id, link}: { level: 1 | 2, id: string function Instructions() { return
-

Choose the right gender for each word

+
Choose the right gender for each word
{level === 2 &&
⚠ Exceptions included...
}
} return void }) { function* questions (): Generator> { let pool = { ...types }; for (let i = 0; i < amount; i++) { const keys = Object.keys(types) as NType[]; let type: NType do { - type = getRandomFromList(keys); + type = randFromArray(keys); } while (!pool[type].length); - const entry = getRandomFromList( + const entry = randFromArray( // @ts-ignore pool[type] ); - const gender = getRandomFromList(genders) as T.Gender; + const gender = randFromArray(genders) as T.Gender; // @ts-ignore pool[type] = pool[type].filter((x) => x.ts !== entry.ts); yield { @@ -84,7 +84,9 @@ export default function UnisexNounGame({ id, link }: { id: string, link: string if (correct) { setAnswer(""); } - callback(correct); + callback(!correct + ?
CORRECT ANSWER HERE
+ : true); } return
@@ -120,11 +122,12 @@ export default function UnisexNounGame({ id, link }: { id: string, link: string function Instructions() { return
-

Change the gender of a given noun

+
Change the gender of a given noun
} return (list: T[]): T { - return list[Math.floor((Math.random()*list.length))]; -} - export function makeProgress(i: number, total: number): Progress { return { current: i + 1, total }; } diff --git a/src/types/game-types.d.ts b/src/types/game-types.d.ts index c797070..619052d 100644 --- a/src/types/game-types.d.ts +++ b/src/types/game-types.d.ts @@ -12,12 +12,12 @@ type QuestionGenerator = Generator, void, unknown>; type QuestionDisplayProps = { question: T, - callback: (correct: boolean) => void, + callback: (correct: true | JSX.Element) => void, }; type GameRecord = { title: string, id: string, studyLink: string, - Game: () => JSX.Element, + Game: (onStartStop: (a: "start" | "stop") => void) => JSX.Element, }; diff --git a/yarn.lock b/yarn.lock index 8eb8ab7..d5dc09b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1684,10 +1684,10 @@ pbf "^3.2.1" rambda "^6.7.0" -"@lingdocs/pashto-inflector@^2.4.2": - version "2.4.2" - resolved "https://npm.lingdocs.com/@lingdocs%2fpashto-inflector/-/pashto-inflector-2.4.2.tgz#584be92d04051a4b3f4e557fa4473671570ed24d" - integrity sha512-+vjvNOIgVW74+NQMdU2ctAK/ISL37WPiZItpt43izvzNJ85/mREmArzTzMCxbN+kFtYpEwAFe5tRsIJgviOBQQ== +"@lingdocs/pashto-inflector@^2.4.9": + version "2.5.0" + resolved "https://npm.lingdocs.com/@lingdocs%2fpashto-inflector/-/pashto-inflector-2.5.0.tgz#4f90a1d2db15e8389cd84f0a576c37b814f303fd" + integrity sha512-RwlBQNwNsKxvICHjx6MwOwetzGQWLdrwjWOBLMfSqbRqQzNWQrjm/Gp4+TfIT89IUtutxVSxLXlgwgUq6ebKpA== dependencies: classnames "^2.2.6" jsurl2 "^2.1.0"