chore: update to Svelte@5

This commit is contained in:
Thomas 2025-01-04 15:35:07 +00:00
parent 43513a8236
commit 8b9b1185a7
No known key found for this signature in database
47 changed files with 669 additions and 2144 deletions

View file

@ -16,7 +16,7 @@
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/adapter-netlify": "^4.4.0",
"@sveltejs/kit": "^2.15.1",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/leaflet": "^1.9.15",
"@types/sanitize-html": "^2.13.0",
"@typescript-eslint/eslint-plugin": "^5.62.0",
@ -27,9 +27,9 @@
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.2",
"sass": "^1.83.1",
"svelte": "^4.2.19",
"svelte-check": "^3.8.6",
"svelte-preprocess": "^5.1.4",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-preprocess": "^6.0.0",
"tslib": "^2.8.1",
"typescript": "^5.7.2",
"vite": "^5.4.11",

View file

@ -1,5 +1,9 @@
<script lang="ts">
export let isActive: boolean;
interface Props {
isActive: boolean;
}
let { isActive }: Props = $props();
</script>
<div class="summer-hours">

View file

@ -1,50 +0,0 @@
<script lang="ts">
import { writable } from "svelte/store";
import { onMount, onDestroy, createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher<{ change: string }>();
let apiPassword = "";
let state: "edit" | "view" = "edit";
let unsubscribe: () => void;
function onSubmit() {
state = "view";
if (localStorage) {
localStorage.setItem("apiPassword", apiPassword);
}
dispatch("change", apiPassword);
}
function onEdit() {
state = "edit";
}
onMount(() => {
if (localStorage !== undefined) {
apiPassword = localStorage.getItem("apiPassword") || "";
}
if (apiPassword.length > 0) {
dispatch("change", apiPassword);
state = "view";
}
});
</script>
<section>
{#if apiPassword.length === 0}
<p>
To save things to the ledger you need to enter the password. Right now you
haven't set one.
</p>
{/if}
{#if state === "view"}
<button on:click={onEdit}>Edit Password</button>
{:else}
<form on:submit|preventDefault={onSubmit}>
<input type="text" bind:value={apiPassword} />
<input type="submit" value="Set Password" />
</form>
{/if}
</section>

View file

@ -1,63 +0,0 @@
<script lang="ts">
import { FloriferousPlayer } from '$lib/floriferous';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
let nameInput: HTMLInputElement;
let name = '';
let score = 0;
let rowAtEndOfGame = 1;
function handleFormSubmit() {
const player = new FloriferousPlayer({
name,
score,
rowAtEndOfGame: rowAtEndOfGame
});
dispatch('submit', player);
name = '';
score = 0;
rowAtEndOfGame = 1;
nameInput.focus();
}
</script>
<form on:submit|preventDefault={() => handleFormSubmit()}>
<div class="field">
<label for="player-name">Name</label>
<input bind:this={nameInput} bind:value={name} type="text" id="player-name" />
</div>
<div class="field">
<label for="player-score">Score</label>
<input bind:value={score} type="number" step="1" min="0" id="player-score" />
</div>
<div class="field">
<label for="player-score">Finishing Row</label>
<input bind:value={rowAtEndOfGame} type="number" step="1" min="0" id="player-score" />
<p class="example-text">"1" for the highest row, "2" for the second highest, etc.</p>
</div>
<input type="submit" value="add" />
</form>
<style>
form {
display: flex;
flex-direction: column;
padding: var(--spacing-sm);
border: 1px solid var(--gray-600);
}
.field {
display: flex;
flex-flow: column;
padding: var(--spacing-sm) 0;
}
.example-text {
color: var(--grey800);
font-size: var(--font-size-sm);
}
</style>

View file

@ -1,3 +0,0 @@
import FloriferousPlayerForm from './FloriferousPlayerForm.svelte';
export { FloriferousPlayerForm, PreviousGameScores };

View file

@ -1,10 +1,19 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
export let id: string;
export let name: string;
export let salary: number;
export let count: number;
interface Props {
id: string;
name: string;
salary: number;
count: number;
}
let {
id,
name = $bindable(),
salary = $bindable(),
count = $bindable(),
}: Props = $props();
const dispatch = createEventDispatcher<{
change: { name: string; salary: number; count: number };
@ -27,8 +36,8 @@
<input
type="text"
placeholder="Junior Software Engineer"
bind:value="{name}"
on:input="{handleChange}"
bind:value={name}
oninput={handleChange}
/>
</div>
<div class="form__field">
@ -37,8 +46,8 @@
type="number"
step="1"
placeholder="30,000"
bind:value="{salary}"
on:change="{handleChange}"
bind:value={salary}
onchange={handleChange}
/>
</div>
<div class="form__field">
@ -47,11 +56,11 @@
type="number"
step="1"
placeholder="30,000"
bind:value="{count}"
on:change="{handleChange}"
bind:value={count}
onchange={handleChange}
/>
</div>
<button type="button" on:click="{handleRemove}"> Remove </button>
<button type="button" onclick={handleRemove}> Remove </button>
</form>
<style>

View file

@ -29,7 +29,7 @@
</div>
<div class="right">
<button class="colour-theme-toggle" on:click={onColourSchemeChange}>
<button class="colour-theme-toggle" onclick={onColourSchemeChange}>
<img
class="icon"
src={$colourSchemeStore.name === "light" ? sunSvg : moonSvg}

View file

@ -1,8 +0,0 @@
import type { FloriferousGame } from './floriferous-game';
export interface FloriferousGameRepository {
save(game: FloriferousGame): Promise<FloriferousGame>;
getById(id: string): Promise<FloriferousGame | null>;
getRecent(count: number): Promise<FloriferousGame[]>;
}

View file

@ -1,117 +0,0 @@
import { describe, it, expect } from 'vitest';
import type { ApiGamesFloriferousPostRequest } from './floriferous-api-controller';
import { FloriferousApiController } from './floriferous-api-controller';
import { StubFloriferousGameRepository } from './stub-floriferous-game-repository';
import { FloriferousGame } from './floriferous-game';
import { SimplePasswordAuthenticator } from '../simple-password-authenticator';
import { Headers } from 'node-fetch';
const isDate = (value = 'invalid'): boolean => {
return value !== 'invalid' && !isNaN(Date.parse(value));
};
describe('FloriferousApiController', () => {
const stubGameRepository = new StubFloriferousGameRepository();
const authenticator = new SimplePasswordAuthenticator('expected-password');
const controller = new FloriferousApiController(stubGameRepository, authenticator);
it('should validate a request with a proper password', async () => {
// GIVEN
const headers: Headers = new Headers();
headers.set('x-api-password', 'expected-password');
// WHEN
const result = controller.isRequestAuthenticated({ headers: headers });
// THEN
expect(result).toBe(true);
});
it('should not validate a request with an invalid password', async () => {
// GIVEN
const headers = new Headers();
headers.set('x-api-password', 'invalid-password');
// WHEN
const result = controller.isRequestAuthenticated({ headers });
// THEN
expect(result).toBe(false);
});
it('should not validate a request without a password', async () => {
// GIVEN
const request = {
headers: new Headers()
};
// WHEN
const result = controller.isRequestAuthenticated(request);
// THEN
expect(result).toBe(false);
});
it('should get a list of recent games', async () => {
// GIVEN
const gameOne = new FloriferousGame({
id: 'game-one',
players: [{ name: 'Alice', score: 10, rowAtEndOfGame: 1 }],
playedTs: new Date('2022-07-01T07:00Z')
});
const gameTwo = new FloriferousGame({
id: 'game-two',
players: [],
playedTs: new Date('2022-08-20T13:25Z')
});
stubGameRepository.setAllGames([gameOne, gameTwo]);
// WHEN
const result = await controller.getRecentGames(10);
// THEN
expect(result).toStrictEqual([
{
id: 'game-one',
playedTs: '2022-07-01T07:00:00.000Z',
players: [{ name: 'Alice', score: 10, rowAtEndOfGame: 1 }]
},
{
id: 'game-two',
playedTs: '2022-08-20T13:25:00.000Z',
players: []
}
]);
});
it('should save a new game', async () => {
// GIVEN
const requestBody: ApiGamesFloriferousPostRequest = {
players: [
{
name: 'Alice',
rowAtEndOfGame: 1,
score: 10
}
]
};
// WHEN
const response = await controller.createNewGame(requestBody);
// THEN
expect(isDate(response.playedTs)).toBe(true);
expect(response).toStrictEqual({
id: expect.any(String),
playedTs: expect.any(String),
players: [
{
name: 'Alice',
score: 10,
rowAtEndOfGame: 1
}
]
});
});
});

View file

@ -1,88 +0,0 @@
import { z } from 'zod';
import type { ZodError } from 'zod';
import type { FloriferousGameJson } from './floriferous-game-api-port';
import type { FloriferousGameRepository } from './FloriferousGameRepository';
import { FloriferousGameApiPort } from './floriferous-game-api-port';
import { FloriferousGame } from './floriferous-game';
import { FloriferousPlayer } from './floriferous-player';
import type { Authenticator } from '../Authenticator';
export interface ApiGamesFloriferousPostRequest {
players: {
name: string;
score: number;
rowAtEndOfGame: number;
}[];
}
const apiGamesFloriferousPostRequestSchema = z.object({
players: z
.array(
z.object({
name: z.string().min(1, { message: 'Player names must be at least 1 character long' }),
score: z
.number({ invalid_type_error: 'Player Score must be a number.' })
.int({ message: 'Player Score must be a whole number.' })
.min(0, { message: 'Player score cannot be less than 0.' }),
rowAtEndOfGame: z.number().int().min(0)
})
)
.min(1)
});
export class FloriferousApiController {
constructor(
private readonly repository: FloriferousGameRepository,
private readonly validator: Authenticator
) {}
isRequestAuthenticated(request: any): boolean {
const password = request?.headers?.get('x-api-password') ?? '';
if (password === undefined) {
return false;
}
return this.validator.authenticate(password);
}
async getRecentGames(count = 10): Promise<FloriferousGameJson[]> {
const games = await this.repository.getRecent(count);
return games.map((game) => FloriferousGameApiPort.gameToJson(game));
}
async createNewGame(data: ApiGamesFloriferousPostRequest): Promise<FloriferousGameJson> {
try {
apiGamesFloriferousPostRequestSchema.parse(data);
} catch (e: any) {
if (e.issues !== undefined) {
const zodError: ZodError = e;
throw new Error(zodError.issues[0].message);
}
console.error({
message: `Caught error validating body data in createNewGame`,
error: e,
data: JSON.stringify(data)
});
throw new Error(e?.message ?? e);
}
const validatedData = data;
const players: FloriferousPlayer[] = validatedData.players.map(
({ name, rowAtEndOfGame, score }) => {
return new FloriferousPlayer({ name, rowAtEndOfGame, score });
}
);
const game = new FloriferousGame({
players,
playedTs: new Date()
});
const savedGame = await this.repository.save(game);
return FloriferousGameApiPort.gameToJson(game);
}
}

View file

@ -1,57 +0,0 @@
import { FloriferousGame } from './floriferous-game';
import { FloriferousPlayer } from './floriferous-player';
import { FloriferousGameApiPort } from './floriferous-game-api-port';
import { it, expect } from 'vitest';
it('should stringify a FloriferousGame into JSON', () => {
// GIVEN
const game = new FloriferousGame({
id: 'the-id',
playedTs: new Date('2020-01-01T00:00Z'),
players: [
new FloriferousPlayer({ name: 'first player', rowAtEndOfGame: 1, score: 2 }),
new FloriferousPlayer({ name: 'second player', rowAtEndOfGame: 3, score: 4 })
]
});
// WHEN
const gameAsJson = FloriferousGameApiPort.gameToJson(game);
// THEN
expect(gameAsJson).toStrictEqual({
id: 'the-id',
playedTs: '2020-01-01T00:00:00.000Z',
players: [
{ name: 'first player', rowAtEndOfGame: 1, score: 2 },
{ name: 'second player', rowAtEndOfGame: 3, score: 4 }
]
});
});
it('should parse JSON into a floriferous game', () => {
// GIVEN
const gameAsJson = {
id: 'the-id',
playedTs: '2020-01-01T00:00:00.000Z',
players: [
{ name: 'first player', rowAtEndOfGame: 1, score: 2 },
{ name: 'second player', rowAtEndOfGame: 3, score: 4 }
]
};
// WHEN
const game = FloriferousGameApiPort.jsonToGame(gameAsJson);
// THEN
expect(game).toStrictEqual(
new FloriferousGame({
id: 'the-id',
playedTs: new Date('2020-01-01T00:00Z'),
players: [
{ name: 'first player', rowAtEndOfGame: 1, score: 2 },
{ name: 'second player', rowAtEndOfGame: 3, score: 4 }
]
})
);
});

View file

@ -1,39 +0,0 @@
import { FloriferousGame } from './floriferous-game';
export interface FloriferousGameJson {
id: string;
playedTs: string;
players: {
name: string;
score: number;
rowAtEndOfGame: number;
}[];
}
export class FloriferousGameApiPort {
static jsonToGame(json: FloriferousGameJson): FloriferousGame {
const players = json.players.map((player) => ({
name: player.name,
score: player.score,
rowAtEndOfGame: player.rowAtEndOfGame
}));
return new FloriferousGame({
id: json.id,
playedTs: new Date(json.playedTs),
players
});
}
static gameToJson(game: FloriferousGame): FloriferousGameJson {
return {
id: game.id,
playedTs: game.playedTs.toISOString(),
players: game.players.map((player) => ({
name: player.name,
score: player.score,
rowAtEndOfGame: player.rowAtEndOfGame
}))
};
}
}

View file

@ -1,62 +0,0 @@
import { describe, it, expect } from 'vitest';
import { FloriferousGame } from './floriferous-game.js';
import { FloriferousPlayer } from './floriferous-player.js';
describe('FloriferousGame', () => {
const alice = new FloriferousPlayer({
name: 'Alice',
score: 2,
rowAtEndOfGame: 0,
});
const bob = new FloriferousPlayer({
name: 'Bob',
score: 1,
rowAtEndOfGame: 1,
});
const bobWithTwoPoints = new FloriferousPlayer({
name: 'Bob',
score: 2,
rowAtEndOfGame: 1,
});
it('Determines a winner', () => {
const game = new FloriferousGame();
// WHEN
game.addPlayer(alice);
game.addPlayer(bob);
// THEN
expect(game.winner).toBe('Alice');
});
it('Breaks a tie using the player closest to the top of the board', () => {
// GIVEN
const game = new FloriferousGame();
// WHEN
game.addPlayer(alice);
game.addPlayer(bobWithTwoPoints);
// THEN
expect(game.winner).toBe('Alice');
});
it('Can give a pretty summary', () => {
// GIVEN
const game = new FloriferousGame({
playedTs: new Date('2022-08-28T13:12Z'),
players: [alice, bob],
});
// WHEN
const prettySummary = game.prettySummary;
// THEN
expect(prettySummary).toBe('Sunday, 28 August 2022 at 14:12: Alice won with 2 points. Bob: 1 point.');
});
});

View file

@ -1,85 +0,0 @@
import type { FloriferousPlayer } from './floriferous-player.js';
import { intlFormat as formatDate } from 'date-fns';
import { nanoid } from 'nanoid';
export interface FloriferousGameParams {
playedTs?: Date;
id?: string;
players?: FloriferousPlayer[];
}
export class FloriferousGame {
readonly id: string;
readonly playedTs: Date;
private _players: FloriferousPlayer[] = [];
constructor({ id = nanoid(), playedTs = new Date(), players = [] }: FloriferousGameParams = {}) {
this.id = id;
this.playedTs = playedTs;
this._players = players;
}
addPlayer(player: FloriferousPlayer): void {
this._players.push(player);
}
addPlayers(players: FloriferousPlayer[]): void {
players.forEach((player) => {
this.addPlayer(player);
});
}
get prettySummary(): string {
if (this._players.length === 0) {
return '';
}
const formattedDate = formatDate(this.playedTs, {
localeMatcher: 'best fit',
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
const winnerStatement = `${this.winningPlayer.name} won with ${this.winningPlayer.score} points`;
const otherPlayerStatements = this.nonWinningsPlayers.map((player) => {
return `${player.name}: ${player.score} point${player.score === 1 ? '' : 's'}.`;
});
return [`${formattedDate}: ${winnerStatement}`, ...otherPlayerStatements].join('. ');
}
get players(): FloriferousPlayer[] {
return this._players;
}
private get winningPlayer(): FloriferousPlayer | undefined {
if (this._players.length === 0) {
return undefined;
}
const playersSortedByScore = this._players.sort((a, b) => {
return b.score - a.score;
});
if (playersSortedByScore[0].score === playersSortedByScore[1].score) {
const playersSortedByRowAtEndOfGame = this._players.sort((a, b) => {
return a.rowAtEndOfGame - b.rowAtEndOfGame;
});
return playersSortedByRowAtEndOfGame[0];
}
return playersSortedByScore[0];
}
private get nonWinningsPlayers(): FloriferousPlayer[] {
return this._players.filter((player) => player.name !== this.winner);
}
get winner(): string | undefined {
return this.winningPlayer?.name;
}
}

View file

@ -1,18 +0,0 @@
import { describe, expect, it } from 'vitest';
import { FloriferousPlayer } from './floriferous-player';
describe('FloriferousPlayer', () => {
it('should construct with properties', () => {
// GIVEN
const player = new FloriferousPlayer({
name: 'Alice',
score: 2,
rowAtEndOfGame: 0
});
// THEN
expect(player.name).toBe('Alice');
expect(player.score).toBe(2);
expect(player.rowAtEndOfGame).toBe(0);
});
});

View file

@ -1,17 +0,0 @@
export interface FloriferousPlayerParams {
name: string;
score: number;
rowAtEndOfGame: number;
}
export class FloriferousPlayer {
readonly name: string;
readonly score: number;
readonly rowAtEndOfGame: number;
constructor(params: FloriferousPlayerParams) {
this.name = params.name;
this.score = params.score;
this.rowAtEndOfGame = params.rowAtEndOfGame;
}
}

View file

@ -1,2 +0,0 @@
export { FloriferousGame } from './floriferous-game';
export { FloriferousPlayer } from './floriferous-player';

View file

@ -1,128 +0,0 @@
import { describe, expect, it, beforeEach, afterAll } from 'vitest';
import { FloriferousGame, type FloriferousGameParams } from './floriferous-game.js';
import { FloriferousPlayer, type FloriferousPlayerParams } from './floriferous-player.js';
import { customAlphabet } from 'nanoid';
import { MongodbFloriferousGameRepository } from './mongodb-floriferous-game-repository.js';
import { MongoClient } from 'mongodb';
describe('MongoDB FloriferousGame Repository', () => {
const mongoDbUrl = import.meta.env.VITE_MONGO_URL;
const mongoDbName = import.meta.env.VITE_MONGO_DB_NAME;
const janitor = new MongodbJanitor(mongoDbUrl, mongoDbName);
let playerOne: FloriferousPlayer;
let playerTwo: FloriferousPlayer;
let game: FloriferousGame;
beforeEach(async () => {
playerOne = floriferousPlayerFactory({ name: 'Player 1' });
playerTwo = floriferousPlayerFactory({ name: 'Player 2' });
game = floriferousGameFactory();
game.addPlayer(playerOne);
game.addPlayer(playerTwo);
});
afterAll(async () => {
await janitor.deleteAllDocumentsInConnection('floriferous-games');
});
const repository = new MongodbFloriferousGameRepository(mongoDbUrl, mongoDbName);
it('should save', async () => {
// WHEN
const savedGame = await repository.save(game);
// THEN
expect(savedGame).toBeDefined();
});
it('should get by ID when it exists', async () => {
// GIVEN
const savedGame = await repository.save(game);
// WHEN
const foundGame = await repository.getById(game.id);
// THEN
expect(foundGame).toStrictEqual(game);
});
it('should return null when fetching by ID for a game which did not happen', async () => {
// WHEN
const foundGame = await repository.getById('any-random-id');
// THEN
expect(foundGame).toBeNull();
});
it('should get the last 10 games, sorted by playedTs', async () => {
// GIVEN
await janitor.deleteAllDocumentsInConnection('floriferous-games');
const gameOne = floriferousGameFactory({
playedTs: new Date('2022-08-07T19:00Z')
});
const gameTwo = floriferousGameFactory({
playedTs: new Date('2022-08-07T06:00Z')
});
await repository.save(gameOne);
await repository.save(gameTwo);
// WHEN
const recentGames = await repository.getRecent(10);
// THEN
expect(recentGames).toStrictEqual([gameTwo, gameOne]);
});
it('should return an empty array when no games have taken place', async () => {
// GIVEN
await janitor.deleteAllDocumentsInConnection('floriferous-games');
// WHEN
const recentGames = await repository.getRecent(5);
// THEN
expect(recentGames).toStrictEqual([]);
});
});
class MongodbJanitor {
private client: MongoClient;
constructor(private readonly url: string, private readonly dbName: string) {
this.client = new MongoClient(url);
}
async deleteAllDocumentsInConnection(collectionName: string): Promise<void> {
console.info(`Deleting all documents in ${collectionName}`);
const connection = await this.client.connect();
const deltedDocuments = await connection
.db(this.dbName)
.collection(collectionName)
.deleteMany({});
await connection.close();
console.info(`Deleted ${deltedDocuments.deletedCount} documents`);
}
}
function floriferousPlayerFactory(
overrides: Partial<FloriferousPlayerParams> = {}
): FloriferousPlayer {
return new FloriferousPlayer({
name: `name-${customAlphabet('abcdefghijklmnopqrstuvwxyz', 8)}`,
score: Math.floor(Math.random() * 100),
rowAtEndOfGame: Math.floor(Math.random() * 10),
...overrides
});
}
function floriferousGameFactory(overrides: Partial<FloriferousGameParams> = {}): FloriferousGame {
return new FloriferousGame(overrides);
}

View file

@ -1,123 +0,0 @@
import { FloriferousGame } from './floriferous-game';
import { MongoClient } from 'mongodb';
import { FloriferousPlayer } from './floriferous-player';
import type { FloriferousGameRepository } from './FloriferousGameRepository';
interface FloriferousGameMongoDocument {
id: string;
playedTs: Date;
players: {
name: string;
score: number;
rowAtEndOfGame: number;
}[];
}
export class MongodbFloriferousGameRepository implements FloriferousGameRepository {
private client: MongoClient;
private connection: MongoClient | null = null;
private collectionName = 'floriferous-games';
constructor(private readonly mongodbUrl: string, private readonly dbName: string) {
this.client = new MongoClient(mongodbUrl);
}
private async connect(): Promise<void> {
this.connection = await this.client.connect();
}
private async disconnect(): Promise<void> {
if (this.connection === null) {
return;
}
await this.connection.close();
}
async save(game: FloriferousGame): Promise<FloriferousGame> {
await this.connect();
const data = MongodbFloriferousGameRepository.gameToMongoDocument(game);
const document = await this.connection
.db(this.dbName)
.collection<FloriferousGameMongoDocument>(this.collectionName)
.insertOne(data);
await this.disconnect();
return game;
}
async getById(id: string): Promise<FloriferousGame | null> {
await this.connect();
const document = await this.connection
.db(this.dbName)
.collection<FloriferousGameMongoDocument>(this.collectionName)
.findOne({ id });
await this.disconnect();
if (document === null) {
return null;
}
return MongodbFloriferousGameRepository.documentToGame(document);
}
async getRecent(count = 10): Promise<FloriferousGame[]> {
await this.connect();
const documents = await this.connection
.db(this.dbName)
.collection<FloriferousGameMongoDocument>(this.collectionName)
.find({})
.sort({ playedTs: 1 })
.limit(count)
.toArray();
await this.disconnect();
if (!documents) {
return [];
}
const formatted = documents.map((document) =>
MongodbFloriferousGameRepository.documentToGame(document)
);
return formatted;
}
private static documentToGame(document: FloriferousGameMongoDocument): FloriferousGame {
const players: FloriferousPlayer[] = document.players.map(
({ name, rowAtEndOfGame, score }) =>
new FloriferousPlayer({
name,
rowAtEndOfGame: rowAtEndOfGame,
score
})
);
const game = new FloriferousGame({
id: document.id,
playedTs: document.playedTs,
players
});
return game;
}
private static gameToMongoDocument(game: FloriferousGame): FloriferousGameMongoDocument {
return {
id: game.id,
playedTs: game.playedTs,
players: game.players.map((player) => ({
rowAtEndOfGame: player.rowAtEndOfGame,
name: player.name,
score: player.score
}))
};
}
}

View file

@ -1,30 +0,0 @@
import type { FloriferousGameRepository } from './FloriferousGameRepository';
import type { FloriferousGame } from './floriferous-game';
export class StubFloriferousGameRepository implements FloriferousGameRepository{
private games: FloriferousGame[] = [];
setAllGames(games: FloriferousGame[]): void {
this.games = games;
}
getById(id: string): Promise<FloriferousGame | null> {
throw new Error('Method not implemented.');
}
getRecent(count: number): Promise<FloriferousGame[]> {
return Promise.resolve(this.games.map((game, index) => {
if (index < count -1) {
return game;
}
return undefined
}).filter((game) => game !== undefined));
}
save(game: FloriferousGame): Promise<FloriferousGame> {
return Promise.resolve(undefined);
}
}

View file

@ -8,6 +8,11 @@
} from "../stores/colourSchemeStore.ts";
import { browser } from "$app/environment";
import { onMount } from "svelte";
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
onMount(() => {
const prefersDarkmode: boolean = window.matchMedia(
@ -60,4 +65,4 @@
<title>Thomas Wilson</title>
</svelte:head>
<slot />
{@render children?.()}

View file

@ -2,7 +2,7 @@
import Navbar from "$lib/components/Navbar.svelte";
import HomepageHeader from "./home/HomepageHeader.svelte";
export let data = { latestBlogPosts: [] }
let { data = { latestBlogPosts: [] } } = $props();
</script>

View file

@ -1,51 +0,0 @@
import { MONGO_URL, MONGO_DB_NAME, API_PASSWORD } from '$env/static/private';
import type { error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { MongodbFloriferousGameRepository } from '$lib/floriferous/mongodb-floriferous-game-repository';
import { FloriferousApiController } from '$lib/floriferous/floriferous-api-controller';
import { SimplePasswordAuthenticator } from '$lib/simple-password-authenticator';
import {
FloriferousGameApiPort,
type FloriferousGameJson
} from '$lib/floriferous/floriferous-game-api-port';
export const GET: RequestHandler = async () => {
const controller = new FloriferousApiController(
new MongodbFloriferousGameRepository(MONGO_URL, MONGO_DB_NAME),
new SimplePasswordAuthenticator(API_PASSWORD)
);
const response = await controller.getRecentGames(10);
return new Response(JSON.stringify(response), { status: 200 });
};
export const POST = async ({ request }) => {
const controller = new FloriferousApiController(
new MongodbFloriferousGameRepository(MONGO_URL, MONGO_DB_NAME),
new SimplePasswordAuthenticator(API_PASSWORD)
);
const isAuthenticated = controller.isRequestAuthenticated(request);
if (!isAuthenticated) {
return {
status: 401,
body: 'Unauthorized'
};
}
try {
const requestBody = await request.json();
const response = await controller.createNewGame(requestBody);
return new Response(JSON.stringify(response), {
status: 200
});
} catch (e) {
return new Response(JSON.stringify(e), {
status: 500
});
}
};

View file

@ -3,16 +3,20 @@
import Navbar from "$lib/components/Navbar.svelte";
import BlogPostListItem from "./BlogPostListItem.svelte";
export let data: PageData;
interface Props {
data: PageData;
}
$: ({
let { data }: Props = $props();
let {
posts,
numberOfPosts,
daysSinceLastPublish,
daysSinceFirstPost,
averageDaysBetweenPosts,
numberOfBlogPostsThisYear,
} = data);
} = $derived(data);
</script>
<svelte:head>

View file

@ -1,16 +1,29 @@
<script lang="ts">
import { format as formatDate } from "date-fns";
export let index: number;
export let numberOfPosts: number;
export let book_review: boolean;
export let title: string;
export let preview: string;
export let slug: string;
export let date: string;
export let content_type: "blog" | "book_review" | "snout_street_studios";
interface Props {
index: number;
numberOfPosts: number;
book_review: boolean;
title: string;
preview: string;
slug: string;
date: string;
content_type: "blog" | "book_review" | "snout_street_studios";
}
$: formattedDate = formatDate(new Date(date), "yyyy-MM-dd");
let {
index,
numberOfPosts,
book_review,
title,
preview,
slug,
date,
content_type
}: Props = $props();
let formattedDate = $derived(formatDate(new Date(date), "yyyy-MM-dd"));
</script>
<li

View file

@ -1,9 +1,14 @@
<script>
<script lang="ts">
import "../../../styles/prism.css";
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<svelte:head>
<script src="/prism.js"></script>
</svelte:head>
<slot />
{@render children?.()}

View file

@ -4,8 +4,12 @@
import Navbar from "$lib/components/Navbar.svelte";
import { onMount } from "svelte";
export let data: PageData;
$: ({ date, post } = data);
interface Props {
data: PageData;
}
let { data }: Props = $props();
let { date, post } = $derived(data);
onMount(() => {
console.log({ date, post });

View file

@ -1,12 +1,14 @@
<script lang="ts">
import { preventDefault } from 'svelte/legacy';
import { format as formatDate } from "date-fns";
import { BlogPost } from "$lib/blog/BlogPost.js";
import { goto } from "$app/navigation";
let title = "";
let author = "Thomas Wilson";
let title = $state("");
let author = $state("Thomas Wilson");
let date = new Date();
let content = "";
let slug = "";
let content = $state("");
let slug = $state("");
let blogPost: BlogPost | null = null;
function slugifyString(originalString: string): string {
@ -52,7 +54,7 @@
<section class="new-blog-post">
<a href="/blog">Back to Blog</a>
<h1>New Blog Post</h1>
<form on:submit|preventDefault={onCreate}>
<form onsubmit={preventDefault(onCreate)}>
<div class="field">
<label class="field__label" for="title">Title</label>
<input
@ -60,7 +62,7 @@
id="title"
required
bind:value={title}
on:change={handleTitleChange}
onchange={handleTitleChange}
/>
</div>
<div class="field">
@ -75,7 +77,7 @@
<div class="field">
<label class="field__label" for="content">Content</label>
<textarea id="content" rows="10" cols="50" bind:value={content} />
<textarea id="content" rows="10" cols="50" bind:value={content}></textarea>
</div>
<div class="submit">

View file

@ -1 +0,0 @@
export const prerender = true;

View file

@ -1,18 +0,0 @@
<script lang="ts">
import Navbar from '$lib/components/Navbar.svelte';
</script>
<Navbar />
<main class="thomaswilson-container">
<section class="thomaswilson-strapline section">
<h1>Board Game Scoring & Ledgers</h1>
<p>
I like to play board games, but tallying up scores for some of them is a nightmare. I'm
building some tools to help me keep score.
</p>
<ul>
<li><a href="/games/floriferous">Floriferous</a></li>
</ul>
</section>
</main>

View file

@ -1,39 +0,0 @@
<script lang="ts">
import { slide, fade } from 'svelte/transition';
export let gameSummaries: string[];
const pluralRule = new Intl.PluralRules('en', { type: 'ordinal' });
let isVisible = false;
</script>
<div class="previous-scores">
{#if isVisible}
<button
transition:fade={{ duration: 30, delay: 0 }}
class="thomaswilson-button"
on:click={() => (isVisible = false)}>Hide Previous Scores</button
>
<ul>
{#each gameSummaries as summary}
<li transition:slide>
{summary}
</li>
{/each}
</ul>
{:else}
<button
transition:slide={{ delay: 0, duration: 30 }}
class="thomaswilson-button"
on:click={() => (isVisible = true)}
>Show {gameSummaries.length} Previous {gameSummaries.length === 1 ? 'Game' : 'Games'}</button
>
{/if}
</div>
<style>
.previous-scores {
display: grid;
grid-template-columns: 100%;
width: 100%;
}
</style>

View file

@ -1,199 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { slide } from 'svelte/transition';
import type { PageData } from './$types.js';
import Navbar from '$lib/components/Navbar.svelte';
import FloriferousScoreCalculator from './FloriferousScoreCalculator.svelte';
import { FloriferousGame } from '../../../lib/floriferous/floriferous-game.js';
import type { FloriferousPlayer } from '../../../lib/floriferous';
import PreviousGameScores from '../PreviousGameScores.svelte';
import { FloriferousPlayerForm } from '../../../components/games';
import {
FloriferousGameApiPort,
type FloriferousGameJson
} from '../../../lib/floriferous/floriferous-game-api-port.js';
import ApiPasswordFrom from '../../../components/games/ApiPasswordForm.svelte';
import type { ApiGamesFloriferousPostRequest } from '$lib/floriferous/floriferous-api-controller';
export let data: PageData;
let previousGames: FloriferousGame[] = data.previousGames;
let apiPassword = '';
let players: FloriferousPlayer[] = [];
let isScoreCalculatorVisible = true;
let isPlayersVisible = false;
let isPreviousScoresVisible = false;
let isWinnerVisible = false;
let isSaveSubmitting = false;
let isGameSaved = false;
function handleShowWinner() {
isWinnerVisible = true;
}
function onAddPlayer(event: CustomEvent<FloriferousPlayer>) {
players = [...players, event.detail];
}
function onRemovePlayer(playerToRemove: FloriferousPlayer) {
players = players.filter((player) => {
return playerToRemove.name !== player.name;
});
}
function clearGameData() {
players = [];
isWinnerVisible = false;
}
async function onSaveGame() {
isSaveSubmitting = true;
if (players.length < 2) {
console.warn(`Not enough players to save game`);
isSaveSubmitting = false;
return;
}
const body: { players: FloriferousPlayer[] } = { players };
fetch('/api/games/floriferous.json', {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
'x-api-password': apiPassword
}
})
.then((response) => {
return response.json();
})
.then((gameAsJson: FloriferousGameJson) => {
const game = FloriferousGameApiPort.jsonToGame(gameAsJson);
previousGames = [...previousGames, game];
clearGameData();
})
.catch((error) => {
if (error.status === 401) {
console.warn(`Invalid API password`);
return;
}
console.error(error);
isSaveSubmitting = false;
});
}
$: game = new FloriferousGame({ playedTs: new Date(), players });
$: previousGameSummaries = previousGames.map((game) => game.prettySummary);
</script>
<Navbar />
<main class="thomaswilson-container">
<h1>Floriferous Scoring</h1>
<p>
Floriferous is a board game published by Pencil First games, in which you find find joy in the
abundance of nature.
</p>
<section class="score-calculator">
<div class="score-calculator__header">
<h2>Score Calculator</h2>
{#if isScoreCalculatorVisible}
<button on:click={() => (isScoreCalculatorVisible = false)}>Hide</button>
{:else}
<button on:click={() => (isScoreCalculatorVisible = true)}>Show</button>
{/if}
</div>
<FloriferousScoreCalculator isVisible={isScoreCalculatorVisible} />
</section>
<section class="players">
<div class="players__header">
<h2>Players</h2>
{#if isPlayersVisible}
<button on:click={() => (isPlayersVisible = false)}>Hide</button>
{:else}
<button on:click={() => (isPlayersVisible = true)}>Show</button>
{/if}
</div>
{#if isPlayersVisible}
{#if players.length > 0}
<ul>
{#each players as player}
<li>
{player.name} ({player.score} points, finished on row {player.rowAtEndOfGame}) (<button
on:click={() => onRemovePlayer(player)}>Remove</button
>)
</li>
{/each}
</ul>
{#if players.length > 1}
{#if isWinnerVisible}
<p transition:slide>And the winner is:<strong>{game.winner}</strong></p>
<h3>Add to Ledger</h3>
<ApiPasswordFrom on:change={(event) => (apiPassword = event.detail)} />
{#if apiPassword.length > 0}
<p>You can save this game in the Ledger</p>
<button on:click={onSaveGame}>Save Game</button>
{/if}
{:else}
<button on:click={handleShowWinner}>Show me the winner</button>
{/if}
{/if}
{:else}
<p>Add at least one player to get started</p>
{/if}
{#if !isWinnerVisible}
<h3>Add a New Player</h3>
<FloriferousPlayerForm on:submit={onAddPlayer} />
{/if}
{/if}
</section>
{#if previousGames.length > 0}
<section class="previous-games">
<h2>Previous Games</h2>
<PreviousGameScores gameSummaries={previousGameSummaries} />
</section>
{/if}
</main>
<style>
section {
display: flex;
flex-direction: column;
padding: var(--spacing-sm);
max-width: 600px;
}
.players {
padding: var(--spacing-md) 0;
width: 100%;
}
.players__header {
display: grid;
grid-template-columns: auto min-content;
}
.score-calculator {
padding: var(--spacing-md) 0;
width: 100%;
}
.score-calculator__header {
display: grid;
grid-template-columns: auto min-content;
}
.previous-games {
padding: var(--spacing-md) 0;
width: 100%;
}
</style>

View file

@ -1,12 +0,0 @@
import {
FloriferousGameApiPort,
type FloriferousGameJson
} from '$lib/floriferous/floriferous-game-api-port';
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch }): Promise<{ previousGames: FloriferousGameJson[] }> => {
const previousGames = await fetch('/api/games/floriferous.json').then((res) => res.json());
return {
previousGames: previousGames.map(FloriferousGameApiPort.jsonToGame)
};
};

View file

@ -1,188 +0,0 @@
<script lang="ts">
import { slide } from 'svelte/transition';
export let isVisible: boolean;
let currentScore = 0;
let actions = [];
let newActionScore = 0;
let newActionName = '';
let newActionNameInput;
let isNewActionNameVisible = false;
function incrementNewActionScore() {
newActionScore += 1;
}
function decrementNewActionScore() {
newActionScore -= 1;
}
function onNewActionSubmit(score: number, name = '') {
actions = [...actions, { score, name }];
currentScore += score;
newActionScore = 0;
newActionName = '';
}
function toggleIsNewActionNameVisible() {
isNewActionNameVisible = true;
newActionNameInput.focus();
newActionName = '';
}
const suggestedDescriptions = [
'Arrangement Card',
'Desire Card',
'Bounty',
'Stones',
'Cup of Tea'
];
</script>
{#if isVisible}
<div transition:slide>
<div>
<p>Your score is {currentScore}</p>
<ul class="actions-list">
{#each actions as action}
<li class="actions-list__item">
+{action.score}
{#if action.name.length > 0} ({action.name}) {/if}
</li>
{/each}
</ul>
</div>
<form
class="form"
on:submit|preventDefault={() => onNewActionSubmit(newActionScore, newActionName)}
>
<div class="form-field">
<label class="form__label" for="points">Points*</label>
<input required name="points" type="number" bind:value={newActionScore} step="1" />
</div>
<div class="increment-decrement">
<button type="button" on:click={decrementNewActionScore}>-</button>
<button type="button" on:click={incrementNewActionScore}>+</button>
</div>
<div class="form-field">
<fieldset class="suggested-descriptions">
<legend>Reason for Points</legend>
{#each suggestedDescriptions as suggestion}
<label
class="suggested-descriptions__label"
class:selected={newActionName === suggestion}
for={`suggestion-${suggestion}`}
>
<input
type="radio"
name="suggestion"
id={`suggestion-${suggestion}`}
value={suggestion}
class="suggested-descriptions__item"
on:click={() => (newActionName = suggestion)}
checked={newActionName === suggestion}
/>
{suggestion}</label
>
{/each}
<label class="suggested-descriptions__item">
<button
type="button"
class="suggested-descriptions__button"
on:click={toggleIsNewActionNameVisible}
>
Other
</button>
</label>
<input
transition:slide
name="action-name"
type="text"
step="1"
class:sr-only={!isNewActionNameVisible}
bind:value={newActionName}
bind:this={newActionNameInput}
/>
</fieldset>
</div>
<div class="submit">
<input type="submit" value="Add Points" class="thomaswilson-button form__submit" />
</div>
</form>
</div>
{/if}
<style>
.actions-list {
padding: 0;
}
.actions-list__item {
padding: 0;
}
.form {
display: grid;
grid-template-columns: 100%;
gap: var(--spacing-md);
width: 100%;
}
.form__label {
font-size: var(--font-size-md);
}
.form-field {
display: grid;
grid-template-columns: 100%;
}
.increment-decrement {
padding: var(--spacing-md) 0;
display: grid;
grid-template-columns: 50% 50%;
gap: var(--spacing-md);
}
.suggested-descriptions {
padding: var(--spacing-sm);
margin: 0;
display: grid;
grid-template-columns: 100%;
}
.suggested-descriptions__item {
padding: var(--spacing-sm) 0;
}
.suggested-descriptions__label {
border: none;
background: transparent;
text-decoration: underline;
transition: all 0.15s;
padding: var(--spacing-sm) 0;
}
.suggested-descriptions__label.selected {
color: var(--brand-blue);
}
.submit {
padding-top: var(--spacing-lg);
display: grid;
grid-template-columns: 100%;
width: 100%;
}
.form__submit {
background: var(--brand-blue);
color: white;
border: none;
}
</style>

View file

@ -1,3 +0,0 @@
export interface PreviousGame {
prettyString: string;
}

View file

@ -7,7 +7,11 @@
date: string;
}
export let latestBlogPosts: BlogPost[] = [];
interface Props {
latestBlogPosts?: BlogPost[];
}
let { latestBlogPosts = [] }: Props = $props();
</script>
<section class="homepage-header">

View file

@ -1,468 +1,492 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import Employee from '../../components/salary-calculator/employee.svelte';
import { onDestroy, onMount } from "svelte";
import Employee from "../../components/salary-calculator/employee.svelte";
type Employee = {
id: string;
name: string;
salary: number;
count: number;
};
type IEmployee = {
id: string;
name: string;
salary: number;
count: number;
};
function makeId(): string {
return (
Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
);
}
function makeId(): string {
return (
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)
);
}
function makeRandomJobTitle(): string {
const randomJobNames = [
'Junior Software Engineer',
'Finance Associate',
'Growth Marketeer',
'Customer Support',
'Data Scientist',
'Logistics Manager',
'General Manager',
'Process Manager',
'Head of Department'
];
function makeRandomJobTitle(): string {
const randomJobNames = [
"Junior Software Engineer",
"Finance Associate",
"Growth Marketeer",
"Customer Support",
"Data Scientist",
"Logistics Manager",
"General Manager",
"Process Manager",
"Head of Department",
];
const index = Math.floor(Math.random() * randomJobNames.length);
return randomJobNames[index];
}
const index = Math.floor(Math.random() * randomJobNames.length);
return randomJobNames[index];
}
function makeRandomSalary(): number {
const b = Math.floor(Math.random() * 10);
return 5_000 * b;
}
function makeRandomSalary(): number {
const b = Math.floor(Math.random() * 10);
return 5_000 * b;
}
function makeEmployee(salaryOverride?: number): Employee {
return {
id: makeId(),
name: makeRandomJobTitle(),
salary: salaryOverride ?? makeRandomSalary(),
count: 1
};
}
function makeEmployee(salaryOverride?: number): IEmployee {
return {
id: makeId(),
name: makeRandomJobTitle(),
salary: salaryOverride ?? makeRandomSalary(),
count: 1,
};
}
let INITIAL_SALARY = makeRandomSalary();
let employees: Employee[] = [makeEmployee(INITIAL_SALARY)];
let INITIAL_SALARY = makeRandomSalary();
let employees: IEmployee[] = [makeEmployee(INITIAL_SALARY)];
let totalCost = 0;
let totalCost = 0;
let averageAnnualSalary = INITIAL_SALARY;
let salaryCostPerMinute = annualSalaryToPerMinuteCost(INITIAL_SALARY);
let totalNumberOfEmployees = 2;
let secondsElapsed = 0;
let intervalRef;
let averageAnnualSalary = INITIAL_SALARY;
let salaryCostPerMinute = annualSalaryToPerMinuteCost(INITIAL_SALARY);
let totalNumberOfEmployees = 2;
let secondsElapsed = 0;
let intervalRef;
let currency: 'GBP' | 'USD' = 'GBP';
let salaryCalculationMethod: 'average' | 'individual' = 'average';
let currency: "GBP" | "USD" = "GBP";
let salaryCalculationMethod: "average" | "individual" = "average";
function handleEmployeeChange(employeeId: string) {
return function (event: CustomEvent) {
employees = employees.map(({ id, ...rest }) => {
if (id === employeeId) {
return { ...event.detail, id };
}
function handleEmployeeChange(employeeId: string) {
return function (event: CustomEvent) {
employees = employees.map(({ id, ...rest }) => {
if (id === employeeId) {
return { ...event.detail, id };
}
return { id, ...rest };
});
totalNumberOfEmployees = getNumberOfEmployees(employees);
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
};
}
return { id, ...rest };
});
totalNumberOfEmployees = getNumberOfEmployees(employees);
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
};
}
function handleEmployeeRemove(id: string) {
return function () {
employees = employees.filter(({ id: employeeId }) => employeeId !== id);
totalNumberOfEmployees = getNumberOfEmployees(employees);
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
};
}
function handleEmployeeRemove(id: string) {
return function () {
employees = employees.filter(({ id: employeeId }) => employeeId !== id);
totalNumberOfEmployees = getNumberOfEmployees(employees);
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
};
}
function handleSecondElapsed() {
secondsElapsed++;
}
function handleSecondElapsed() {
secondsElapsed++;
}
function resetInterval() {
intervalRef = setInterval(handleSecondElapsed, 1000);
}
function resetInterval() {
intervalRef = setInterval(handleSecondElapsed, 1000);
}
function reset() {
secondsElapsed = 0;
if (intervalRef) {
clearInterval(intervalRef);
} else {
resetInterval();
}
}
function reset() {
secondsElapsed = 0;
if (intervalRef) {
clearInterval(intervalRef);
} else {
resetInterval();
}
}
function stop() {
clearInterval(intervalRef);
}
function stop() {
clearInterval(intervalRef);
}
function addEmployee() {
employees = [...employees, makeEmployee()];
totalNumberOfEmployees = getNumberOfEmployees(employees);
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
}
function addEmployee() {
employees = [...employees, makeEmployee()];
totalNumberOfEmployees = getNumberOfEmployees(employees);
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
}
function handleAverageMethodChanged(method: 'average' | 'individual') {
return () => {
salaryCalculationMethod = method;
function handleAverageMethodChanged(method: "average" | "individual") {
return () => {
salaryCalculationMethod = method;
secondsElapsed = 0;
if (method === 'average') {
salaryCostPerMinute = annualSalaryToPerMinuteCost(averageAnnualSalary);
} else if (method === 'individual') {
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
totalNumberOfEmployees = getNumberOfEmployees(employees);
}
};
}
secondsElapsed = 0;
if (method === "average") {
salaryCostPerMinute = annualSalaryToPerMinuteCost(averageAnnualSalary);
} else if (method === "individual") {
salaryCostPerMinute = allEmployeesSalaryToPerMinuteCost(employees);
totalNumberOfEmployees = getNumberOfEmployees(employees);
}
};
}
function getNumberOfEmployees(employees: Employee[]): number {
return employees.reduce((runningCount, employee) => {
return runningCount + employee.count;
}, 0);
}
function getNumberOfEmployees(employees: IEmployee[]): number {
return employees.reduce((runningCount, employee) => {
return runningCount + employee.count;
}, 0);
}
function handleAverageAnnualSalaryChange() {
salaryCostPerMinute = annualSalaryToPerMinuteCost(averageAnnualSalary);
}
function handleAverageAnnualSalaryChange() {
salaryCostPerMinute = annualSalaryToPerMinuteCost(averageAnnualSalary);
}
function annualSalaryToPerMinuteCost(annualSalary: number) {
const minutesInHour = 60;
const workingDaysInYear = 48 * 5;
const hoursInWorkingDay = 8;
return annualSalary / workingDaysInYear / hoursInWorkingDay / minutesInHour;
}
function annualSalaryToPerMinuteCost(annualSalary: number) {
const minutesInHour = 60;
const workingDaysInYear = 48 * 5;
const hoursInWorkingDay = 8;
return annualSalary / workingDaysInYear / hoursInWorkingDay / minutesInHour;
}
function allEmployeesSalaryToPerMinuteCost(employees: Employee[]) {
return employees.reduce((acc, employee) => {
return acc + annualSalaryToPerMinuteCost(employee.salary);
}, 0);
}
function allEmployeesSalaryToPerMinuteCost(employees: IEmployee[]) {
return employees.reduce((acc, employee) => {
return acc + annualSalaryToPerMinuteCost(employee.salary);
}, 0);
}
function formatCurrency(amount: number, currency: 'GBP' | 'USD') {
return `${currency === 'GBP' ? '£' : '$'}${amount.toFixed(2)}`;
}
function formatCurrency(amount: number, currency: "GBP" | "USD") {
return `${currency === "GBP" ? "£" : "$"}${amount.toFixed(2)}`;
}
function formatSecondsToMinutes(seconds: number) {
const minutes = Math.floor(seconds / 60);
const secondsRemaining = seconds % 60;
return `${minutes}:${secondsRemaining < 10 ? '0' : ''}${secondsRemaining}`;
}
function formatSecondsToMinutes(seconds: number) {
const minutes = Math.floor(seconds / 60);
const secondsRemaining = seconds % 60;
return `${minutes}:${secondsRemaining < 10 ? "0" : ""}${secondsRemaining}`;
}
onMount(() => {});
onMount(() => {});
onDestroy(() => {
clearInterval(intervalRef);
});
onDestroy(() => {
clearInterval(intervalRef);
});
$: totalCost = (secondsElapsed / 60) * salaryCostPerMinute * totalNumberOfEmployees;
$: totalCostPerMinute = (salaryCostPerMinute * totalNumberOfEmployees).toFixed(2);
$: totalCost =
(secondsElapsed / 60) * salaryCostPerMinute * totalNumberOfEmployees;
$: totalCostPerMinute = (
salaryCostPerMinute * totalNumberOfEmployees
).toFixed(2);
// TODO: Milestones in cost, e.g. price of a kit kat chunky, price of X
// TODO: Milestones in cost, e.g. price of a kit kat chunky, price of X
</script>
<svelte:head>
<title>Meeting Cost Calculator</title>
<meta
name="description"
content="Calculate the cost of a meeting, or at least the salaries of people attending a meeting."
/>
<meta name="twitter:card" content="https://www.thomaswilson.xyz/meeting-cost-calculator.png" />
<meta name="twitter:site" content="@tjwilson92" />
<meta name="twitter:title" content="Meeting Cost Calculator" />
<meta
name="twitter:description"
content="Calculate the cost of a meeting, or at least the salaries of people attending a meeting."
/>
<meta name="twitter:image" content="https://www.thomaswilson.xyz/meeting-cost-calculator.png" />
<meta name="twitter:image:alt" content="Meeting Cost Calculator" />
<title>Meeting Cost Calculator</title>
<meta
name="description"
content="Calculate the cost of a meeting, or at least the salaries of people attending a meeting."
/>
<meta
name="twitter:card"
content="https://www.thomaswilson.xyz/meeting-cost-calculator.png"
/>
<meta name="twitter:site" content="@tjwilson92" />
<meta name="twitter:title" content="Meeting Cost Calculator" />
<meta
name="twitter:description"
content="Calculate the cost of a meeting, or at least the salaries of people attending a meeting."
/>
<meta
name="twitter:image"
content="https://www.thomaswilson.xyz/meeting-cost-calculator.png"
/>
<meta name="twitter:image:alt" content="Meeting Cost Calculator" />
<meta property="og:title" content="Meeting Cost Calculator" />
<meta
property="og:description"
content="Calculate the cost of a meeting, or at least the salaries of people attending a meeting."
/>
<meta property="og:image" content="https://www.thomaswilson.xyz/meeting-cost-calculator.png" />
<meta property="og:image:alt" content="Meeting Cost Calculator" />
<meta property="og:url" content="https://www.thomaswilson.xyz/mcc" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Thomas Wilson" />
<meta property="og:locale" content="en_GB" />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_GB" />
<meta property="og:title" content="Meeting Cost Calculator" />
<meta
property="og:description"
content="Calculate the cost of a meeting, or at least the salaries of people attending a meeting."
/>
<meta
property="og:image"
content="https://www.thomaswilson.xyz/meeting-cost-calculator.png"
/>
<meta property="og:image:alt" content="Meeting Cost Calculator" />
<meta property="og:url" content="https://www.thomaswilson.xyz/mcc" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Thomas Wilson" />
<meta property="og:locale" content="en_GB" />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_GB" />
</svelte:head>
<main>
<section>
<h1>Meeting Cost Calculator</h1>
<p class="subtitle">Meetings aren't free. See how much you're paying for them.</p>
</section>
<section>
<h1>Meeting Cost Calculator</h1>
<p class="subtitle">
Meetings aren't free. See how much you're paying for them.
</p>
</section>
<section>
<h2>Attendee Salaries are</h2>
<div class="modes">
<button
class="modes__button"
class:selected={salaryCalculationMethod == 'average'}
on:click={handleAverageMethodChanged('average')}>A simple average</button
>
<button
class="modes__button"
class:selected={salaryCalculationMethod == 'individual'}
on:click={() => (salaryCalculationMethod = 'individual')}>In different bands</button
>
</div>
</section>
<section>
<h2>Attendee Salaries are</h2>
<div class="modes">
<button
class="modes__button"
class:selected={salaryCalculationMethod == "average"}
on:click={handleAverageMethodChanged("average")}
>A simple average</button
>
<button
class="modes__button"
class:selected={salaryCalculationMethod == "individual"}
on:click={() => (salaryCalculationMethod = "individual")}
>In different bands</button
>
</div>
</section>
<section class="form">
{#if salaryCalculationMethod == 'average'}
<h2>Salary Details</h2>
<form class="simple-average-form">
<div class="simple-average-form__field">
<label for="averageSalary">Average Annual Salary of Attendee</label>
<input
type="number"
step="1"
bind:value={averageAnnualSalary}
on:input={() => handleAverageAnnualSalaryChange()}
/>
</div>
<section class="form">
{#if salaryCalculationMethod == "average"}
<h2>Salary Details</h2>
<form class="simple-average-form">
<div class="simple-average-form__field">
<label for="averageSalary">Average Annual Salary of Attendee</label>
<input
type="number"
step="1"
bind:value={averageAnnualSalary}
on:input={() => handleAverageAnnualSalaryChange()}
/>
</div>
<div class="simple-average-form__field">
<label for="totalNumberOfEmployees">Number of Attendees</label>
<input type="number" step="1" bind:value={totalNumberOfEmployees} />
</div>
</form>
{:else}
<div class="employees-header">
<h2>Employee Details</h2>
<div class="employee-details-container__button">
<button on:click={addEmployee}>Add Employee</button>
</div>
</div>
<div class="simple-average-form__field">
<label for="totalNumberOfEmployees">Number of Attendees</label>
<input type="number" step="1" bind:value={totalNumberOfEmployees} />
</div>
</form>
{:else}
<div class="employees-header">
<h2>Employee Details</h2>
<div class="employee-details-container__button">
<button on:click={addEmployee}>Add Employee</button>
</div>
</div>
<div class="employees-list" role="list">
{#each employees as employee}
<Employee
id={employee.id}
name={employee.name}
salary={employee.salary}
count={employee.count}
on:change={handleEmployeeChange(employee.id)}
on:remove={handleEmployeeRemove(employee.id)}
/>
{/each}
</div>
{/if}
</section>
<div class="employees-list" role="list">
{#each employees as employee}
<Employee
id={employee.id}
name={employee.name}
salary={employee.salary}
count={employee.count}
on:change={handleEmployeeChange(employee.id)}
on:remove={handleEmployeeRemove(employee.id)}
/>
{/each}
</div>
{/if}
</section>
<section class="result">
<h2>Projected Costs</h2>
{#if salaryCalculationMethod === 'average'}
<p>
With {totalNumberOfEmployees ?? 0}
{totalNumberOfEmployees === 1 ? 'Attendee' : 'Attendees'}, each costing aprox. {formatCurrency(
salaryCostPerMinute,
currency
)}
per minute, this meeting will cost £{totalCostPerMinute} per minute.
</p>
{:else}
<p>With the following attendees:</p>
<ul>
{#each employees as employee}
<li>{employee.count} x {employee.name} @ {employee.salary}/year</li>
{/each}
</ul>
<p>This meeting will cost £{totalCostPerMinute}/minute</p>
{/if}
<section class="result">
<h2>Projected Costs</h2>
{#if salaryCalculationMethod === "average"}
<p>
With {totalNumberOfEmployees ?? 0}
{totalNumberOfEmployees === 1 ? "Attendee" : "Attendees"}, each costing
aprox. {formatCurrency(salaryCostPerMinute, currency)}
per minute, this meeting will cost £{totalCostPerMinute} per minute.
</p>
{:else}
<p>With the following attendees:</p>
<ul>
{#each employees as employee}
<li>{employee.count} x {employee.name} @ {employee.salary}/year</li>
{/each}
</ul>
<p>This meeting will cost £{totalCostPerMinute}/minute</p>
{/if}
<p>This meeting will cost:</p>
<p>This meeting will cost:</p>
<ul class="duration-list">
<li class="duration-list__item">
{formatCurrency(salaryCostPerMinute * totalNumberOfEmployees * 30, currency)} for 30 minutes
</li>
<li class="duration-list__item">
{formatCurrency(salaryCostPerMinute * totalNumberOfEmployees * 45, currency)} for 45 minutes
</li>
<li class="duration-list__item">
{formatCurrency(salaryCostPerMinute * totalNumberOfEmployees * 60, currency)} for 60 minutes
</li>
</ul>
</section>
<ul class="duration-list">
<li class="duration-list__item">
{formatCurrency(
salaryCostPerMinute * totalNumberOfEmployees * 30,
currency
)} for 30 minutes
</li>
<li class="duration-list__item">
{formatCurrency(
salaryCostPerMinute * totalNumberOfEmployees * 45,
currency
)} for 45 minutes
</li>
<li class="duration-list__item">
{formatCurrency(
salaryCostPerMinute * totalNumberOfEmployees * 60,
currency
)} for 60 minutes
</li>
</ul>
</section>
<section>
<h2>Timed Costs</h2>
<section>
<h2>Timed Costs</h2>
<div class="action-buttons">
<button
on:click={reset}
class:start={secondsElapsed === 0}
class:start__reset={secondsElapsed > 0}
>
{#if secondsElapsed === 0}
Start the Meeting
{:else}
Reset
{/if}
</button>
<button on:click={stop} disabled={secondsElapsed === 0} id="pause"
>{intervalRef ? 'Pause' : 'Start'}</button
>
</div>
<p class="total-cost">
Total cost so far: {formatCurrency(totalCost, currency)} over {formatSecondsToMinutes(
secondsElapsed
)} <br />
</p>
</section>
<div class="action-buttons">
<button
on:click={reset}
class:start={secondsElapsed === 0}
class:start__reset={secondsElapsed > 0}
>
{#if secondsElapsed === 0}
Start the Meeting
{:else}
Reset
{/if}
</button>
<button on:click={stop} disabled={secondsElapsed === 0} id="pause"
>{intervalRef ? "Pause" : "Start"}</button
>
</div>
<p class="total-cost">
Total cost so far: {formatCurrency(totalCost, currency)} over {formatSecondsToMinutes(
secondsElapsed
)} <br />
</p>
</section>
<section>
<h2>About</h2>
<p>
Made with 🖤 on a tain by <a id="thomas-wilson" href="/">Thomas Wilson</a>
</p>
</section>
<section>
<h2>About</h2>
<p>
Made with 🖤 on a tain by <a id="thomas-wilson" href="/">Thomas Wilson</a>
</p>
</section>
</main>
<style>
main {
display: grid;
place-content: center;
grid-auto-rows: max-content;
padding: 32px 0;
min-height: 100vh;
width: 100vw;
background: var(--gray-100);
}
main {
display: grid;
place-content: center;
grid-auto-rows: max-content;
padding: 32px 0;
min-height: 100vh;
width: 100vw;
background: var(--gray-100);
}
section {
max-width: 95vw;
width: 600px;
height: fit-content;
padding: 12px 0px;
}
section {
max-width: 95vw;
width: 600px;
height: fit-content;
padding: 12px 0px;
}
.subtitle {
font-size: 1.5rem;
color: var(--gray-700);
font-weight: 400;
font-family: var(--font-family-sans);
}
.subtitle {
font-size: 1.5rem;
color: var(--gray-700);
font-weight: 400;
font-family: var(--font-family-sans);
}
.modes {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
}
.modes {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
}
.modes__button {
border: 1px solid var(--gray-300);
border-radius: 4px;
padding: 8px 16px;
color: var(--gray-700);
background: var(--gray-100);
cursor: pointer;
transition: background-color 0.2s ease-in-out;
}
.modes__button {
border: 1px solid var(--gray-300);
border-radius: 4px;
padding: 8px 16px;
color: var(--gray-700);
background: var(--gray-100);
cursor: pointer;
transition: background-color 0.2s ease-in-out;
}
.modes__button.selected {
background: var(--brand-blue);
color: white;
}
.modes__button.selected {
background: var(--brand-blue);
color: white;
}
.employees-header {
display: grid;
grid-template-columns: 1fr max-content;
grid-template-rows: 1fr;
}
.employees-header {
display: grid;
grid-template-columns: 1fr max-content;
grid-template-rows: 1fr;
}
.employees-list {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
}
.employees-list {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
}
.result p {
font-variant-numeric: tabular-nums;
}
.result p {
font-variant-numeric: tabular-nums;
}
.simple-average-form {
display: grid;
grid-template-columns: 1fr;
row-gap: 8px;
}
.simple-average-form {
display: grid;
grid-template-columns: 1fr;
row-gap: 8px;
}
.simple-average-form__field {
display: grid;
grid-template-columns: 1fr;
}
.simple-average-form__field {
display: grid;
grid-template-columns: 1fr;
}
#thomas-wilson {
color: var(--brand-orange);
}
#thomas-wilson {
color: var(--brand-orange);
}
.action-buttons {
padding: 12px 0px;
display: grid;
gap: 6px;
grid-template-columns: 1fr;
}
.action-buttons {
padding: 12px 0px;
display: grid;
gap: 6px;
grid-template-columns: 1fr;
}
.action-buttons button {
padding: var(--spacing-md);
border-radius: 8px;
cursor: pointer;
}
.action-buttons button {
padding: var(--spacing-md);
border-radius: 8px;
cursor: pointer;
}
.start {
border: 1px solid var(--brand-blue);
background: var(--brand-blue);
color: white;
}
.start {
border: 1px solid var(--brand-blue);
background: var(--brand-blue);
color: white;
}
.start__reset {
border: none;
background: none;
color: var(--gray-700);
border: 1px solid var(--gray-500);
}
.start__reset {
border: none;
background: none;
color: var(--gray-700);
border: 1px solid var(--gray-500);
}
#pause {
color: var(--brand-blue);
background: white;
border: 1px solid var(--brand-blue);
transition: 0.1s ease-in;
}
#pause {
color: var(--brand-blue);
background: white;
border: 1px solid var(--brand-blue);
transition: 0.1s ease-in;
}
#pause:disabled {
border: none;
background: var(--gray-200);
color: var(--gray-600);
border: 1px solid var(--gray-200);
cursor: not-allowed;
}
#pause:disabled {
border: none;
background: var(--gray-200);
color: var(--gray-600);
border: 1px solid var(--gray-200);
cursor: not-allowed;
}
ul {
list-style: disc;
padding-left: 24px;
}
ul {
list-style: disc;
padding-left: 24px;
}
li {
padding: 2px 0;
}
li {
padding: 2px 0;
}
.total-cost {
text-align: center;
font-size: 1.2rem;
font-weight: 400;
}
.total-cost {
text-align: center;
font-size: 1.2rem;
font-weight: 400;
}
</style>

View file

@ -7,14 +7,14 @@
differenceInCalendarDays
} from "date-fns";
let lastDayOfMonth = new Date();
let daysUntilPayDay = 0;
let lastDayOfMonth = $state(new Date());
let daysUntilPayDay = $state(0);
function prettyPrintDays(numberOfDays: number): string {
return `${numberOfDays} ${numberOfDays === 1 ? "day" : "days"}`;
}
$: pluralisedDays = prettyPrintDays(Math.abs(daysUntilPayDay));
let pluralisedDays = $derived(prettyPrintDays(Math.abs(daysUntilPayDay)));
onMount(() => {
lastDayOfMonth = endOfMonth(new Date());

View file

@ -1,3 +1,11 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<h1>Snout St. Studios</h1>
<slot />
{@render children?.()}

View file

@ -12,7 +12,7 @@
import { SunriseSunsetStreakCalculator } from "./SunriseSunsetStreakCalculator.js";
import type { ISunriseSunsetGuessingHistory } from "./ISunriseSunsetGuessingHistory.js";
let hasGuessingHistoryBeenLoaded = false;
let hasGuessingHistoryBeenLoaded = $state(false);
let debug = false;
let visibleNotification: Writable<"none" | "success" | "failure"> =
writable("none");
@ -25,15 +25,19 @@
incorrectDays: []
});
export let data: PageData;
interface Props {
data: PageData;
}
let { data }: Props = $props();
const now = new Date();
const todaysDateString = formatDate(now, "yyyy-MM-dd");
const localStorageKey = "sunrise-sunset-guessing-history";
let currentStreakLength = 0;
let currentStreakLength = $state(0);
const streakCalculator = new SunriseSunsetStreakCalculator(todaysDateString);
$: picture = data.body.photo;
let picture = $derived(data.body.photo);
function debugRemoveLocalStorage() {
localStorage.removeItem(localStorageKey);
@ -119,7 +123,7 @@
</section>
{#if debug}
<button on:click={debugRemoveLocalStorage}>Remove Local Storage</button>
<button onclick={debugRemoveLocalStorage}>Remove Local Storage</button>
{/if}
<section class="picture">

View file

@ -3,7 +3,11 @@
import { fade } from "svelte/transition";
import type { Writable } from "svelte/store";
export let visibleNotification: Writable<"none" | "success" | "failure">;
interface Props {
visibleNotification: Writable<"none" | "success" | "failure">;
}
let { visibleNotification }: Props = $props();
let hasAnimationTriggered = false;
const revealResultDelayDurationMs = 550;

View file

@ -1,8 +1,12 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
export let isDisabled: boolean;
export let hasAlreadyGuessedToday: boolean;
interface Props {
isDisabled: boolean;
hasAlreadyGuessedToday: boolean;
}
let { isDisabled, hasAlreadyGuessedToday }: Props = $props();
const eventDispatcher = createEventDispatcher<{
optionSelected: { option: "sunrise" | "sunset" };
@ -18,13 +22,13 @@
<button
disabled={isDisabled}
class="options__button option--sunrise"
on:click={() => onOptionSelected("sunrise")}>Sunrise</button
onclick={() => onOptionSelected("sunrise")}>Sunrise</button
>
<button
disabled={isDisabled}
class="options__button option--sunset"
id="button-sunset"
on:click={() => onOptionSelected("sunset")}>Sunset</button
onclick={() => onOptionSelected("sunset")}>Sunset</button
>
</div>
{#if hasAlreadyGuessedToday}

View file

@ -2,20 +2,29 @@
import { format as formatDate } from "date-fns";
import { SunriseSunsetStreakCalculator } from "./SunriseSunsetStreakCalculator.js";
import { browser } from "$app/environment";
export let doesUserHaveGuessingHistory: boolean;
export let correctGuessDays: string[];
export let incorrectGuessDays: string[];
export let currentStreakLength: number;
interface Props {
doesUserHaveGuessingHistory: boolean;
correctGuessDays: string[];
incorrectGuessDays: string[];
currentStreakLength: number;
}
let {
doesUserHaveGuessingHistory,
correctGuessDays,
incorrectGuessDays,
currentStreakLength
}: Props = $props();
const todayAsString = formatDate(new Date(), "yyyy-MM-dd");
const calculator = new SunriseSunsetStreakCalculator(todayAsString);
let hasTextBeenCopied = false;
let hasTextBeenCopied = $state(false);
$: historyStatement = calculator.getShareableStatement(
let historyStatement = $derived(calculator.getShareableStatement(
correctGuessDays,
incorrectGuessDays,
new Date()
);
));
function copyHistory() {
if (browser) {
@ -32,7 +41,7 @@
<p class="score__text">
{historyStatement}
</p>
<button on:click={() => copyHistory()}> Copy to Clipboard </button>
<button onclick={() => copyHistory()}> Copy to Clipboard </button>
{#if hasTextBeenCopied}
<p>Copied!</p>

View file

@ -1,3 +1,11 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<svelte:head>
<link
rel="stylesheet"
@ -7,4 +15,4 @@
/>
</svelte:head>
<slot />
{@render children?.()}

View file

@ -5,9 +5,13 @@
import type { Wainwright } from './Wainwright.js';
import { browser } from '$app/environment';
export let data: PageData;
interface Props {
data: PageData;
}
$: ({ wainwrights } = data);
let { data }: Props = $props();
let { wainwrights } = $derived(data);
onMount(async () => {
const L = await import('leaflet');
@ -58,7 +62,7 @@
and forteen fells (including four mountains). These have become known as the Wainwrights.
</p>
<div id="map" style="height: 400px;" />
<div id="map" style="height: 400px;"></div>
<style lang="scss">
:global .wainwright-popup {

367
yarn.lock
View file

@ -2,7 +2,7 @@
# yarn lockfile v1
"@ampproject/remapping@^2.2.1":
"@ampproject/remapping@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4"
integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==
@ -863,7 +863,7 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a"
integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==
"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24":
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
version "0.3.25"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0"
integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
@ -1520,25 +1520,24 @@
sirv "^3.0.0"
tiny-glob "^0.2.9"
"@sveltejs/vite-plugin-svelte-inspector@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz#116ba2b73be43c1d7d93de749f37becc7e45bb8c"
integrity sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==
"@sveltejs/vite-plugin-svelte-inspector@^3.0.0-next.0||^3.0.0":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz#006bcab6ea90e09c65459133d4e3eaa6b1e83e28"
integrity sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==
dependencies:
debug "^4.3.4"
debug "^4.3.7"
"@sveltejs/vite-plugin-svelte@^3.1.2":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.2.tgz#be3120b52e6d9facb55d58392b0dad9e5a35ba6f"
integrity sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==
"@sveltejs/vite-plugin-svelte@^4.0.0":
version "4.0.4"
resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.4.tgz#79dfc00377f5456f4c3d95f56817d6486cc0df6c"
integrity sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==
dependencies:
"@sveltejs/vite-plugin-svelte-inspector" "^2.1.0"
debug "^4.3.4"
"@sveltejs/vite-plugin-svelte-inspector" "^3.0.0-next.0||^3.0.0"
debug "^4.3.7"
deepmerge "^4.3.1"
kleur "^4.1.5"
magic-string "^0.30.10"
svelte-hmr "^0.16.0"
vitefu "^0.2.5"
magic-string "^0.30.12"
vitefu "^1.0.3"
"@types/cookie@^0.6.0":
version "0.6.0"
@ -1552,7 +1551,7 @@
dependencies:
"@types/ms" "*"
"@types/estree@1.0.6", "@types/estree@^1.0.0", "@types/estree@^1.0.1", "@types/estree@^1.0.6":
"@types/estree@1.0.6", "@types/estree@^1.0.0", "@types/estree@^1.0.5", "@types/estree@^1.0.6":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
@ -1610,11 +1609,6 @@
resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb"
integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==
"@types/pug@^2.0.6":
version "2.0.10"
resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.10.tgz#52f8dbd6113517aef901db20b4f3fca543b88c1f"
integrity sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==
"@types/sanitize-html@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.13.0.tgz#ac3620e867b7c68deab79c72bd117e2049cdd98e"
@ -1778,6 +1772,11 @@ acorn-jsx@^5.3.2:
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn-typescript@^1.4.13:
version "1.4.13"
resolved "https://registry.yarnpkg.com/acorn-typescript/-/acorn-typescript-1.4.13.tgz#5f851c8bdda0aa716ffdd5f6ac084df8acc6f5ea"
integrity sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==
acorn-walk@^8.3.2:
version "8.3.4"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7"
@ -1785,7 +1784,7 @@ acorn-walk@^8.3.2:
dependencies:
acorn "^8.11.0"
acorn@^8.10.0, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.9.0:
acorn@^8.11.0, acorn@^8.12.1, acorn@^8.14.0, acorn@^8.9.0:
version "8.14.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.0.tgz#063e2c70cac5fb4f6467f0b11152e04c682795b0"
integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==
@ -1812,20 +1811,12 @@ ansi-styles@^5.0.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
aria-query@^5.3.0:
aria-query@^5.3.1:
version "5.3.2"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59"
integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==
@ -1840,7 +1831,7 @@ assertion-error@^1.1.0:
resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==
axobject-query@^4.0.0:
axobject-query@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee"
integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==
@ -1860,11 +1851,6 @@ base64-js@^1.3.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
binary-extensions@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
bowser@^2.11.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f"
@ -1878,7 +1864,7 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
braces@^3.0.3, braces@~3.0.2:
braces@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
@ -1892,11 +1878,6 @@ bson@^4.7.2:
dependencies:
buffer "^5.6.0"
buffer-crc32@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz#a10993b9055081d55304bd9feb4a072de179f405"
integrity sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==
buffer@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
@ -1963,38 +1944,17 @@ check-error@^1.0.3:
dependencies:
get-func-name "^2.0.2"
chokidar@^3.4.1:
version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
chokidar@^4.0.0:
chokidar@^4.0.0, chokidar@^4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
dependencies:
readdirp "^4.0.1"
code-red@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/code-red/-/code-red-1.0.4.tgz#59ba5c9d1d320a4ef795bc10a28bd42bfebe3e35"
integrity sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
"@types/estree" "^1.0.1"
acorn "^8.10.0"
estree-walker "^3.0.3"
periscopic "^3.1.0"
clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
color-convert@^2.0.1:
version "2.0.1"
@ -2037,14 +1997,6 @@ cross-spawn@^7.0.3, cross-spawn@^7.0.6:
shebang-command "^2.0.0"
which "^2.0.1"
css-tree@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20"
integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==
dependencies:
mdn-data "2.0.30"
source-map-js "^1.0.1"
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
@ -2062,7 +2014,7 @@ date-fns@^2.30.0:
dependencies:
"@babel/runtime" "^7.21.0"
debug@^4.0.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
debug@^4.0.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.7:
version "4.4.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
@ -2098,11 +2050,6 @@ dequal@^2.0.0:
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
detect-indent@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6"
integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==
detect-libc@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
@ -2165,11 +2112,6 @@ entities@^4.2.0, entities@^4.4.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
es6-promise@^3.1.2:
version "3.3.1"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
integrity sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==
esbuild@^0.21.3:
version "0.21.5"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
@ -2368,6 +2310,13 @@ esquery@^1.5.0:
dependencies:
estraverse "^5.1.0"
esrap@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/esrap/-/esrap-1.3.2.tgz#a0644603f7f8e9f068c77052d6e16cd4062b5f88"
integrity sha512-C4PXusxYhFT98GjLSmb20k9PREuUdporer50dhzGuJu9IJXktbMddVCMLAERl5dAHyAi73GWWCE4FVHGP1794g==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
esrecurse@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
@ -2385,7 +2334,7 @@ estraverse@^5.1.0, estraverse@^5.2.0:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
estree-walker@^3.0.0, estree-walker@^3.0.3:
estree-walker@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d"
integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==
@ -2464,6 +2413,11 @@ fault@^2.0.0:
dependencies:
format "^0.2.0"
fdir@^6.2.0:
version "6.4.2"
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.2.tgz#ddaa7ce1831b161bc3657bb99cb36e1622702689"
integrity sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==
feed@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e"
@ -2526,11 +2480,6 @@ formdata-polyfill@^4.0.10:
dependencies:
fetch-blob "^3.1.2"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
@ -2546,7 +2495,7 @@ get-stream@^8.0.1:
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2"
integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==
glob-parent@^5.1.2, glob-parent@~5.1.2:
glob-parent@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@ -2560,18 +2509,6 @@ glob-parent@^6.0.2:
dependencies:
is-glob "^4.0.3"
glob@^7.1.3:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.1.1"
once "^1.3.0"
path-is-absolute "^1.0.0"
globals@^14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
@ -2599,11 +2536,6 @@ globrex@^0.1.2:
resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==
graceful-fs@^4.1.3:
version "4.2.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
graphemer@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
@ -2749,19 +2681,6 @@ imurmurhash@^0.1.4:
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ip-address@^9.0.5:
version "9.0.5"
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a"
@ -2770,13 +2689,6 @@ ip-address@^9.0.5:
jsbn "1.1.0"
sprintf-js "^1.1.3"
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-buffer@^2.0.0:
version "2.0.5"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
@ -2787,7 +2699,7 @@ is-extglob@^2.1.1:
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
@ -2809,7 +2721,7 @@ is-plain-object@^5.0.0:
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
is-reference@^3.0.0, is-reference@^3.0.1:
is-reference@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-3.0.3.tgz#9ef7bf9029c70a67b2152da4adf57c23d718910f"
integrity sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==
@ -2935,7 +2847,7 @@ loupe@^2.3.6, loupe@^2.3.7:
dependencies:
get-func-name "^2.0.1"
magic-string@^0.30.10, magic-string@^0.30.4, magic-string@^0.30.5:
magic-string@^0.30.11, magic-string@^0.30.12, magic-string@^0.30.5:
version "0.30.17"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==
@ -3021,11 +2933,6 @@ mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0:
dependencies:
"@types/mdast" "^3.0.0"
mdn-data@2.0.30:
version "2.0.30"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc"
integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==
mdsvex@^0.10.6:
version "0.10.6"
resolved "https://registry.yarnpkg.com/mdsvex/-/mdsvex-0.10.6.tgz#5ba975f4616e5255ca31cd93d33e2c2a22845631"
@ -3268,30 +3175,13 @@ mimic-fn@^4.0.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc"
integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==
min-indent@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
minimatch@^3.1.1, minimatch@^3.1.2:
minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
minimist@^1.2.0, minimist@^1.2.6:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
mkdirp@^0.5.1:
version "0.5.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
dependencies:
minimist "^1.2.6"
mlly@^1.7.3:
version "1.7.3"
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.3.tgz#d86c0fcd8ad8e16395eb764a5f4b831590cee48c"
@ -3376,11 +3266,6 @@ node-fetch@^3.3.2:
fetch-blob "^3.1.4"
formdata-polyfill "^4.0.10"
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
npm-run-path@^5.1.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f"
@ -3388,13 +3273,6 @@ npm-run-path@^5.1.0:
dependencies:
path-key "^4.0.0"
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
dependencies:
wrappy "1"
onetime@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4"
@ -3457,11 +3335,6 @@ path-exists@^4.0.0:
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
@ -3487,21 +3360,12 @@ pathval@^1.1.1:
resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d"
integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==
periscopic@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/periscopic/-/periscopic-3.1.0.tgz#7e9037bf51c5855bd33b48928828db4afa79d97a"
integrity sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==
dependencies:
"@types/estree" "^1.0.0"
estree-walker "^3.0.0"
is-reference "^3.0.0"
picocolors@^1.0.0, picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
@ -3609,13 +3473,6 @@ readdirp@^4.0.1:
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a"
integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
regenerator-runtime@^0.14.0:
version "0.14.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
@ -3688,13 +3545,6 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rimraf@^2.5.2:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
dependencies:
glob "^7.1.3"
rollup@^4.20.0:
version "4.29.1"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.29.1.tgz#a9aaaece817e5f778489e5bf82e379cc8a5c05bc"
@ -3737,16 +3587,6 @@ sade@^1.7.3, sade@^1.7.4, sade@^1.8.1:
dependencies:
mri "^1.1.0"
sander@^0.5.0:
version "0.5.1"
resolved "https://registry.yarnpkg.com/sander/-/sander-0.5.1.tgz#741e245e231f07cafb6fdf0f133adfa216a502ad"
integrity sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==
dependencies:
es6-promise "^3.1.2"
graceful-fs "^4.1.3"
mkdirp "^0.5.1"
rimraf "^2.5.2"
sanitize-html@^2.14.0:
version "2.14.0"
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.14.0.tgz#bd2a7b97ee1d86a7f0e0babf3a4468f639c3a429"
@ -3834,17 +3674,7 @@ socks@^2.7.1:
ip-address "^9.0.5"
smart-buffer "^4.2.0"
sorcery@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/sorcery/-/sorcery-0.11.1.tgz#7cac27ae9c9549b3cd1e4bb85317f7b2dc7b7e22"
integrity sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.14"
buffer-crc32 "^1.0.0"
minimist "^1.2.0"
sander "^0.5.0"
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.2.1:
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
@ -3889,13 +3719,6 @@ strip-final-newline@^3.0.0:
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd"
integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==
strip-indent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
dependencies:
min-indent "^1.0.0"
strip-json-comments@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
@ -3929,17 +3752,16 @@ supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
svelte-check@^3.8.6:
version "3.8.6"
resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-3.8.6.tgz#2f0ab90533f20b8a549a55fccd8142374a316184"
integrity sha512-ij0u4Lw/sOTREP13BdWZjiXD/BlHE6/e2e34XzmVmsp5IN4kVa3PWP65NM32JAgwjZlwBg/+JtiNV1MM8khu0Q==
svelte-check@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-4.1.1.tgz#4d6a97651bdcff84ad10521d0394ce094dee187a"
integrity sha512-NfaX+6Qtc8W/CyVGS/F7/XdiSSyXz+WGYA9ZWV3z8tso14V2vzjfXviKaTFEzB7g8TqfgO2FOzP6XT4ApSTUTw==
dependencies:
"@jridgewell/trace-mapping" "^0.3.17"
chokidar "^3.4.1"
"@jridgewell/trace-mapping" "^0.3.25"
chokidar "^4.0.1"
fdir "^6.2.0"
picocolors "^1.0.0"
sade "^1.7.4"
svelte-preprocess "^5.1.3"
typescript "^5.0.3"
svelte-eslint-parser@^0.43.0:
version "0.43.0"
@ -3952,41 +3774,30 @@ svelte-eslint-parser@^0.43.0:
postcss "^8.4.39"
postcss-scss "^4.0.9"
svelte-hmr@^0.16.0:
version "0.16.0"
resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.16.0.tgz#9f345b7d1c1662f1613747ed7e82507e376c1716"
integrity sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==
svelte-preprocess@^6.0.0:
version "6.0.3"
resolved "https://registry.yarnpkg.com/svelte-preprocess/-/svelte-preprocess-6.0.3.tgz#fdc1f9dc41b6f22bf8b1f059e9f21eaaae181eeb"
integrity sha512-PLG2k05qHdhmRG7zR/dyo5qKvakhm8IJ+hD2eFRQmMLHp7X3eJnjeupUtvuRpbNiF31RjVw45W+abDwHEmP5OA==
svelte-preprocess@^5.1.3, svelte-preprocess@^5.1.4:
version "5.1.4"
resolved "https://registry.yarnpkg.com/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz#14ada075c94bbd2b71c5ec70ff72f8ebe1c95b91"
integrity sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==
svelte@^5.0.0:
version "5.16.1"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-5.16.1.tgz#c3030a4b3f477801c669008e5ab12104df3ab05a"
integrity sha512-FsA1OjAKMAFSDob6j/Tv2ZV9rY4SeqPd1WXQlQkFkePAozSHLp6tbkU9qa1xJ+uTRzMSM2Vx3USdsYZBXd3H3g==
dependencies:
"@types/pug" "^2.0.6"
detect-indent "^6.1.0"
magic-string "^0.30.5"
sorcery "^0.11.0"
strip-indent "^3.0.0"
svelte@^4.2.19:
version "4.2.19"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-4.2.19.tgz#4e6e84a8818e2cd04ae0255fcf395bc211e61d4c"
integrity sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==
dependencies:
"@ampproject/remapping" "^2.2.1"
"@jridgewell/sourcemap-codec" "^1.4.15"
"@jridgewell/trace-mapping" "^0.3.18"
"@types/estree" "^1.0.1"
acorn "^8.9.0"
aria-query "^5.3.0"
axobject-query "^4.0.0"
code-red "^1.0.3"
css-tree "^2.3.1"
estree-walker "^3.0.3"
is-reference "^3.0.1"
"@ampproject/remapping" "^2.3.0"
"@jridgewell/sourcemap-codec" "^1.5.0"
"@types/estree" "^1.0.5"
acorn "^8.12.1"
acorn-typescript "^1.4.13"
aria-query "^5.3.1"
axobject-query "^4.1.0"
clsx "^2.1.1"
esm-env "^1.2.1"
esrap "^1.3.2"
is-reference "^3.0.3"
locate-character "^3.0.0"
magic-string "^0.30.4"
periscopic "^3.1.0"
magic-string "^0.30.11"
zimmerframe "^1.1.2"
tiny-glob@^0.2.9:
version "0.2.9"
@ -4077,7 +3888,7 @@ type-detect@^4.0.0, type-detect@^4.1.0:
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c"
integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==
typescript@^5.0.3, typescript@^5.7.2:
typescript@^5.7.2:
version "5.7.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
@ -4238,10 +4049,10 @@ vite@^5.0.0, vite@^5.4.11:
optionalDependencies:
fsevents "~2.3.3"
vitefu@^0.2.5:
version "0.2.5"
resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-0.2.5.tgz#c1b93c377fbdd3e5ddd69840ea3aa70b40d90969"
integrity sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==
vitefu@^1.0.3:
version "1.0.5"
resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-1.0.5.tgz#eab501e07da167bbb68e957685823e6b425e7ce2"
integrity sha512-h4Vflt9gxODPFNGPwp4zAMZRpZR7eslzwH2c5hn5kNZ5rhnKyRJ50U+yGCdc2IRaBs8O4haIgLNGrV5CrpMsCA==
vitest@^1.6.0:
version "1.6.0"
@ -4312,11 +4123,6 @@ word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
xml-js@^1.6.11:
version "1.6.11"
resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9"
@ -4339,6 +4145,11 @@ yocto-queue@^1.0.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110"
integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==
zimmerframe@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/zimmerframe/-/zimmerframe-1.1.2.tgz#5b75f1fa83b07ae2a428d51e50f58e2ae6855e5e"
integrity sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==
zod@^3.24.1:
version "3.24.1"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee"