diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9a41861 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + - master + +permissions: + contents: read + +jobs: + quality: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Install pnpm + uses: pnpm/action-setup@v6 + with: + version: 10.33.4 + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 24 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run checks + run: pnpm check + + - name: Install Chromium + run: pnpm exec playwright install --with-deps chromium + + - name: Run browser tests + run: pnpm test:e2e diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..b9809c1 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,78 @@ +name: Deploy to GitHub Pages + +on: + workflow_run: + workflows: + - CI + types: + - completed + +permissions: + contents: read + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + if: >- + ${{ + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + (github.event.workflow_run.head_branch == 'main' || + github.event.workflow_run.head_branch == 'master') + }} + runs-on: ubuntu-latest + permissions: + contents: read + pages: read + steps: + - name: Check out the verified revision + uses: actions/checkout@v7 + with: + ref: ${{ github.event.workflow_run.head_sha }} + persist-credentials: false + + - name: Install pnpm + uses: pnpm/action-setup@v6 + with: + version: 10.33.4 + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 24 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build the verified revision + run: pnpm build + + - name: Verify static output + run: pnpm verify:build + + - name: Configure GitHub Pages + uses: actions/configure-pages@v6 + + - name: Upload GitHub Pages artifact + uses: actions/upload-pages-artifact@v5 + with: + path: dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + permissions: + contents: read + pages: write + id-token: write + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2a46293 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +coverage/ +playwright-report/ +test-results/ +.DS_Store +*.log +tmp/ +.visualizations/ diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24 diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..34ecc3b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,90 @@ +# Agent Instructions — Lucas Mariz Portfolio + +## Scope and source of truth + +- These instructions apply to every coding agent working in this repository. +- Read `docs/ENGINEERING_CONVENTIONS.md`, `docs/DESIGN.md`, and the relevant + roadmap phase before implementation. +- Keep implementation, tests, and documentation synchronized in the same + change. +- Read the nearest nested `AGENTS.md` before editing a scoped module. Nested + instructions may add invariants without replacing these repository-wide + rules. + +## Language policy + +- Write source code, code comments, test descriptions, commit-facing notes, + README content, agent instructions, and technical documentation in English. +- Never hardcode user-facing copy in a component. Add it to the typed i18n + catalog in English, Brazilian Portuguese, and French. +- English is the application fallback locale. + +## Tooling + +- Node.js 24 is required. Keep `.nvmrc`, `.node-version`, CI, and + `package.json` aligned. +- pnpm is the only package manager. Use `pnpm`, `pnpm exec`, and `pnpm dlx`; + never add npm or Yarn commands to project documentation or scripts. +- Biome is the only JavaScript/TypeScript/CSS linter and formatter. The root + `biome.json` is the single source of truth. +- Run `pnpm check` before handing off an implementation change. Run the + production build and relevant browser checks when UI or deployment changes. + +## Git and commits + +- Commit messages must be written in English and follow Conventional Commits. +- Never run `git commit` unless the user explicitly asks for a commit in the + current conversation. Permission to plan, implement, edit, test, or prepare + a change is not permission to commit it. +- Commit authorization applies only to changes the user has already reviewed. + Any later edit requires a new review and a new explicit commit request. +- Lefthook must run the configured pre-commit checks, and Commitlint must + validate commit messages. Never bypass hooks with `--no-verify`. + +## TypeScript and control flow + +- Do not use `let`, `else`, `else if`, or `switch` in TypeScript or JavaScript. +- Prefer arrow functions for Preact components and helpers. +- Prefer immutable values, guard clauses, early returns, lookup maps, and small + named helpers. +- Keep components presentational where possible and move content, browser + preference detection, and external-data logic into dedicated modules. + +## Components and responsive design + +- Keep files focused. Split large sections into small, named components. +- Every reusable component belongs in a PascalCase folder with a matching + `Component.tsx` file, an `index.ts` barrel, and focused tests when it owns + behavior. +- A component used by only one parent belongs in that parent's + `components/ComponentName/` folder with the same file/barrel structure. +- Import components through their folder barrels. +- Treat mobile and desktop as first-class layouts. Avoid horizontal overflow, + preserve accessible touch targets, and verify representative phone and + desktop viewports. +- Use semantic design tokens for every surface, border, foreground, and state + so light and dark themes remain visually equivalent. +- Add only purposeful transitions and always respect `prefers-reduced-motion`. + +## Internationalization and theme + +- Supported locales are `en`, `pt-BR`, and `fr`, with matching catalog keys. +- On a first visit, detect the browser locale. If it is missing or unsupported, + use English. +- On a first visit, detect `prefers-color-scheme`. If it is missing or + unsupported, use light mode. +- Persist explicit user overrides and apply theme selection before first paint. +- Update the document language and localized metadata whenever the locale + changes. +- Language controls must use language names rather than flags alone. + +## Static deployment and public data + +- The site must remain statically buildable for GitHub Pages. Do not introduce + a backend, server runtime, or client-side secret for portfolio features. +- The GitHub avatar may use the public username image endpoint with a local + fallback. +- Keep the project showcase curated. External APIs may enrich nonessential + metadata at build time, but the page must not depend on runtime API success. +- Never expose private repository URLs, credentials, tokens, residential + addresses, or other unnecessary personal data in the generated site. diff --git a/README.md b/README.md index 4a1d3aa..2f4d21e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,107 @@ -# Curriculum -Currículo : https://lucaspmm.github.io/Curriculum/ +# Lucas Mariz Portfolio + +A small, static personal portfolio for GitHub Pages. It presents Lucas Mariz's +professional experience, academic path, selected production work, research, and +projects in English, Brazilian Portuguese, and French. + +Target production URL: [https://lucaspmm.github.io/](https://lucaspmm.github.io/) + +The curated showcase includes Jig Solver, the private Planner financial +assistant, Simplex, Pokémon Base, Greedy K-means, and LZ78 Compression. Private +projects are described without exposing source or application links. + +## Stack + +- Preact, TypeScript, and Vite +- i18next with typed, key-parity translation catalogs +- Native CSS based on the tokens in `docs/DESIGN.md` +- Biome for linting and formatting +- Vitest and Testing Library for behavior tests +- Playwright for responsive browser checks +- Vite prerendering for useful HTML before client hydration +- Lefthook and Commitlint for local Git conventions + +The site has no backend, CMS, or client-side secret. The profile image loads +from the public GitHub username endpoint and falls back to a local asset. + +## Requirements + +- Node.js 24 +- pnpm 10 + +## Local development + +```bash +pnpm install +pnpm dev +``` + +Install the Git hooks once per clone: + +```bash +pnpm exec lefthook install +``` + +## Quality checks + +```bash +pnpm check +``` + +Run the browser suite separately after installing the Playwright browser: + +```bash +pnpm exec playwright install chromium +pnpm test:e2e +``` + +## Production build + +```bash +pnpm build +pnpm preview +``` + +The build output is written to `dist/`. This repository targets the +`LucasPMM.github.io` GitHub Pages user site and therefore uses `/` as its Vite +base path. + +The production check also verifies canonical metadata, the sitemap, +`robots.txt`, and representative prerendered content. + +## GitHub Pages activation + +The deployment workflow runs only after the `CI` workflow succeeds for a push +to `master` or `main`. It rebuilds the exact verified revision and publishes +only `dist/`. + +To activate the final site: + +1. Rename `LucasPMM/Curriculum` to `LucasPMM/LucasPMM.github.io` under + **Settings → General**. +2. Under **Settings → Pages**, change the publishing source from the legacy + branch configuration to **GitHub Actions**. +3. Update the local remote: + + ```bash + git remote set-url origin git@github.com:LucasPMM/LucasPMM.github.io.git + ``` + +4. Merge the modernization branch into the default branch. A successful CI run + will trigger the deployment workflow automatically. +5. Set the repository description, Website field, GitHub profile Website, and + LinkedIn link to `https://lucaspmm.github.io/`. + +## Content and implementation rules + +Read these files before making changes: + +- `AGENTS.md` +- `docs/ENGINEERING_CONVENTIONS.md` +- `docs/DESIGN.md` +- `docs/ROADMAP.md` + +Source code, comments, tests, and documentation are written in English. All +visitor-facing copy belongs in the three-language catalog. Do not commit changes +without explicit authorization from Lucas; authorized messages must follow +English Conventional Commits. diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..5bdae95 --- /dev/null +++ b/biome.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.13/schema.json", + "files": { + "includes": [ + "**", + "!!dist", + "!!coverage", + "!!node_modules", + "!!playwright-report", + "!!test-results", + "!!tmp", + "!!.visualizations" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "lineWidth": 100 + }, + "linter": { + "enabled": true, + "rules": { + "preset": "recommended" + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "asNeeded", + "trailingCommas": "all" + } + }, + "css": { + "formatter": { + "enabled": true + }, + "linter": { + "enabled": true + } + } +} diff --git a/commitlint.config.mjs b/commitlint.config.mjs new file mode 100644 index 0000000..d179c69 --- /dev/null +++ b/commitlint.config.mjs @@ -0,0 +1,3 @@ +export default { + extends: ['@commitlint/config-conventional'], +} diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 0000000..6c61120 --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,464 @@ +# Slite — Style Reference +> Warm parchment notebook with terracotta pen — every surface is cream paper, every accent a single ember-orange stroke. + +**Theme:** light + +Slite uses a warm-parchment workspace language: the entire page sits on a cream paper canvas (#fdf9f4) rather than cold white, and a single vivid orange (#f67748) acts as ember — appearing only on the primary CTA, selected status pills, and card border highlights. Typography is split between Garnett (a custom serif-feeling display face with tight tracking) for headlines and a custom UniversalSans (geometric humanist) for everything else, giving the page the rhythm of a well-edited document: serif headlines that feel handwritten, sans-serif body that feels like typed notes. Components are pill-heavy and shadow-light: ghost and outlined pill buttons, 32px-rounded feature cards, dust-toned tag chips, and hand-drawn circle annotations around key phrases. The dominant motion is restrained (130ms ease) and the dominant structure is a centered column with comfortable 80–120px section gaps — never information-dense, always breathing. + +## Tokens — Colors + +| Name | Value | Token | Role | +|------|-------|-------|------| +| Parchment Cream | `#fdf9f4` | `--color-parchment-cream` | Page canvas and primary card surface — the warm off-white that defines Slite's identity. Never use cold white #ffffff at the page level | +| Star White | `#ffffff` | `--color-star-white` | Elevated surfaces — product mockup cards, tooltips, white-product interiors stacked on top of the cream canvas | +| Dust Sand | `#f9efe4` | `--color-dust-sand` | Secondary surface and tag/chip background — a half-step darker than the canvas. Tag pills, secondary buttons, and warm-emphasis callouts | +| Moon Silver | `#ecedef` | `--color-moon-silver` | Hairline borders, dividers, and 2px outlined button borders — the only border tone used at full opacity | +| Shade Ink | `#2d2f34` | `--color-shade-ink` | Primary heading and body text — slightly warm near-black. The headline color | +| Shade Charcoal | `#3f434a` | `--color-shade-charcoal` | Secondary body text, navigation labels, and subdued headings. Do not promote it to the primary CTA color | +| Shade Slate | `#5e646e` | `--color-shade-slate` | Tertiary body text, captions, helper copy — the quietest readable gray | +| Shade Fog | `#9da3af` | `--color-shade-fog` | Disabled states, placeholder text, and the lightest non-white neutral — used sparingly on the page | +| Shade Dusk | `#6a707c` | `--color-shade-dusk` | Small print and fine print text — pricing footnotes, micro-copy beneath headings | +| Border Mist | `#d9dde6` | `--color-border-mist` | Card borders and stroke at low contrast — slightly bluer than Moon Silver, used when a card edge needs to be felt but not seen | +| Ember Orange | `#f67748` | `--color-ember-orange` | Primary action — filled CTA buttons, selected card border accent, featured testimonial card background, and the scribble-annotation color. The single saturated brand color, used sparingly so it always feels like a deliberate highlight | +| Neptune Blue | `#74a6f1` | `--color-neptune-blue` | Secondary action accent — used on at most one button per page (e.g. alternating testimonial CTA) and link-text accents. Never the primary CTA | +| Verification Green | `#479a53` | `--color-verification-green` | Green text accent for links, tags, and emphasized short phrases. Use as a supporting accent, not as a status color | +| Verified Mint | `#bbf7d0` | `--color-verified-mint` | Green decorative accent for icons, marks, and small graphic details. Use as a supporting accent, not as a status color | +| Tag Violet | `#4b51c3` | `--color-tag-violet` | Violet text accent for links, tags, and emphasized short phrases. Use as a supporting accent, not as a status color | +| Illustration Violet | `#6b70d6` | `--color-illustration-violet` | Decorative illustration fill — light-violet shapes in product mockups, paired with Tag Violet as a tonal pair | + +## Tokens — Typography + +### Garnett — Display and editorial headings. Used at 64px (display), 36px (h1), 28px (h2), 24px (large body), 12px (small links). The serif-like Garnett paired with a humanist sans is Slite's signature typographic contrast — it makes the page feel like a designed document rather than a dashboard. · `--font-garnett` +- **Substitute:** Lora, Source Serif Pro, or PT Serif — pick a serif with similar humanist warmth +- **Weights:** 400, 500, 700 +- **Sizes:** 12, 16, 24, 28, 36, 64 +- **Line height:** 1.20 – 2.13 +- **OpenType features:** `"ss14", "ss15", "ss19"` +- **Role:** Display and editorial headings. Used at 64px (display), 36px (h1), 28px (h2), 24px (large body), 12px (small links). The serif-like Garnett paired with a humanist sans is Slite's signature typographic contrast — it makes the page feel like a designed document rather than a dashboard. + +### UniversalSans — Body text, UI controls, navigation, buttons, and supporting headlines. Carries almost all of the page's content. The 50px / weight 400 / line-height 1.5 hero variant is a deliberate departure from typical 700-weight display sizes — it lets the Garnett headline above do the work, while UniversalSans handles the breathing paragraph copy beneath. · `--font-universalsans` +- **Substitute:** Inter, Roboto, or a humanist sans like Public Sans +- **Weights:** 400, 500, 600, 700 +- **Sizes:** 10, 12, 13, 14, 15, 16, 17, 19, 20, 22, 24, 26, 34, 50 +- **Line height:** 1.00 – 2.00 +- **OpenType features:** `"ss14", "ss15", "ss19"` +- **Role:** Body text, UI controls, navigation, buttons, and supporting headlines. Carries almost all of the page's content. The 50px / weight 400 / line-height 1.5 hero variant is a deliberate departure from typical 700-weight display sizes — it lets the Garnett headline above do the work, while UniversalSans handles the breathing paragraph copy beneath. + +### Type Scale + +| Role | Size | Line Height | Letter Spacing | Token | +|------|------|-------------|----------------|-------| +| label | 10px | 1.2 | — | `--text-label` | +| caption | 13px | 1.2 | — | `--text-caption` | +| body-sm | 15px | 1.5 | — | `--text-body-sm` | +| button | 17px | 1 | — | `--text-button` | +| body-lg | 19px | 1.4 | — | `--text-body-lg` | +| heading-sm | 26px | 1.3 | — | `--text-heading-sm` | +| heading | 28px | 1.25 | — | `--text-heading` | +| heading-lg | 36px | 1.2 | — | `--text-heading-lg` | +| hero | 50px | 1.5 | — | `--text-hero` | +| display | 64px | 1.2 | — | `--text-display` | + +## Tokens — Spacing & Shapes + +**Base unit:** 4px + +**Density:** comfortable + +### Spacing Scale + +| Name | Value | Token | +|------|-------|-------| +| 4 | 4px | `--spacing-4` | +| 8 | 8px | `--spacing-8` | +| 12 | 12px | `--spacing-12` | +| 16 | 16px | `--spacing-16` | +| 20 | 20px | `--spacing-20` | +| 24 | 24px | `--spacing-24` | +| 32 | 32px | `--spacing-32` | +| 40 | 40px | `--spacing-40` | +| 48 | 48px | `--spacing-48` | +| 60 | 60px | `--spacing-60` | +| 72 | 72px | `--spacing-72` | +| 80 | 80px | `--spacing-80` | +| 100 | 100px | `--spacing-100` | +| 120 | 120px | `--spacing-120` | +| 176 | 176px | `--spacing-176` | +| 240 | 240px | `--spacing-240` | + +### Border Radius + +| Element | Value | +|---------|-------| +| tags | 9999px | +| cards | 32px | +| buttons | 999px | +| smallCards | 12px | +| ghostButton | 8px | +| productCards | 18px | + +### Shadows + +| Name | Value | Token | +|------|-------|-------| +| subtle | `rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.05) 0...` | `--shadow-subtle` | +| sm | `rgba(0, 0, 0, 0.2) 0px 2px 6px 0px` | `--shadow-sm` | + +### Layout + +- **Page max-width:** 1200px +- **Section gap:** 96px +- **Element gap:** 8px + +## Components + +### Primary CTA Button (Ember Pill) +**Role:** The single orange button on the page — reserved for the main conversion action. + +Filled #f67748 background, white text, UniversalSans 17px weight 600, line-height 1, border-radius 999px (full pill), padding 12px 24px. Appears at most once above the fold. The only color-saturated button on the page. + +### Dark CTA Button (Charcoal Pill) +**Role:** Secondary high-emphasis action, typically 'Start for free'. + +Filled #2d2f34 (or #3f434a) background, white text, UniversalSans 17px weight 600, border-radius 999px, padding 12px 20px. Used in the header nav for the highest-intent action. + +### Outlined Pill Button +**Role:** Medium-emphasis action — Book demo, secondary nav actions. + +Transparent background, 2px solid #2d2f34 border, #2d2f34 text, UniversalSans 15–17px weight 500–600, border-radius 999px, padding 10px 20px. + +### Ghost Text Button +**Role:** Low-emphasis inline action — nav items, sub-actions inside cards. + +Transparent background, no border, #3f434a text, UniversalSans 14–15px weight 500, padding 4px 8px. Minimal padding signals 'I am a label, not a control'. + +### Square Ghost Button +**Role:** Compact UI control — close buttons, icon toggles, inline editors. + +Transparent background, #2d2f34 text, border-radius 8px, padding 0px 8px. The 8px radius is the sharpest button radius in the system — used only for tiny inline controls. + +### Dust Tag Chip +**Role:** Feature highlight tags and category labels — the most-repeated component on the page. + +#f9efe4 background, #3f434a text, UniversalSans 13–15px weight 500, border-radius 9999px, padding 8px 16px. Always pill-shaped, always warm. Used in rows of 3–4 to label a section's sub-topics. + +### Status Pill (Verified / Self-maintained) +**Role:** Trust and maintenance indicators inside product screenshots. + +Tag Violet (#4b51c3) or Verification Green (#479a53) text on white, paired with a small filled icon. UniversalSans 13px weight 500. Sits inline above content titles. + +### Cream Feature Card +**Role:** Primary marketing card — 3-column feature grid items, testimonial cards. + +#fdf9f4 or #f9efe4 background, border-radius 32px, padding 48px top / 32px bottom / 24px sides, no shadow. The 32px radius is Slite's signature — generous but not pillow-soft. Optionally bordered with a 2px #f67748 ember stroke to denote 'selected' or 'highlighted' cards. + +### White Product Card +**Role:** Product screenshot containers and tooltips stacked on the cream canvas. + +#ffffff background, border-radius 12px, box-shadow 0 1px 3px rgba(0,0,0,0.1) + 0 2px 6px rgba(0,0,0,0.05) + 0 4px 12px rgba(0,0,0,0.01). The three-layer shadow is the only place Slite uses depth — the rest of the page stays flat. + +### Compact UI Card +**Role:** Inline product UI mock elements — sidebar items, list rows, agent chips. + +#fdfdfd (near-white) background, border-radius 16px, padding 12px 16px, no shadow. The tighter 16px radius signals 'I am a UI element inside a product mock, not a marketing card'. + +### Ember Testimonial Card +**Role:** Featured customer quote — the only orange-filled card. + +#f67748 background, white text, border-radius 16px, padding 32px. Used at most once per page as the visual punctuation between sections of cream cards. + +### Logo Trust Bar +**Role:** Social proof — '3,000+ companies trust Slite' row. + +Horizontal row of monochrome black customer logos (Doodle, Lush, Frontify, Karbon, Visma, Omnisend) on the cream canvas, with a small 13px caption beneath each ('Migrated from Notion'). Logos are rendered at a single visual weight and aligned to a shared baseline. + +### Scribble Annotation +**Role:** Hand-drawn circle or underline around a key word in a headline. + +1.5–2px solid #f67748 stroke, no fill, slightly imperfect oval shape (roughened path), placed behind or around a single word ('Verified'). The visual signature that makes headlines feel hand-edited rather than rendered. + +### Underline Link +**Role:** Inline text link inside paragraphs. + +#3f434a text, no underline by default, 1px underline on hover with a #f67748 ember accent color. The hover color is the only place the ember orange appears in text. + +### Hero Product Frame +**Role:** Large product screenshot shown below the headline. + +A White Product Card containing a product mockup — sidebar navigation, breadcrumb, content area. Framed by the canvas and dropped onto the page with a subtle offset (rotation 0–1deg optional). Always 12px radius, always #ffffff, always with the three-layer shadow. + +## Do's and Don'ts + +### Do +- Set the page canvas to #fdf9f4 — never use cold white #ffffff as the page background +- Use #f67748 for exactly one filled CTA per section; everything else is charcoal, outlined, or ghost +- Use Garnett for headlines and UniversalSans for body — never use UniversalSans at 40px+ display sizes +- Set button border-radius to 999px (pill) for all primary actions, 8px only for tiny square icon buttons +- Set card border-radius to 32px for marketing cards, 12–18px for product mockup containers +- Use #f9efe4 dust backgrounds for tag chips and secondary surfaces, not solid gray +- Hand-draw a 1.5px #f67748 circle or underline around exactly one key word in any hero headline + +### Don't +- Do not introduce a second saturated color as a brand accent — #f67748 must remain the only chromatic surface color +- Do not use 700-weight UniversalSans at display sizes — 50px hero text is always weight 400 +- Do not stack more than one shadow elevation on a single element; the three-layer shadow is the maximum +- Do not use pure black #000000 for body text — always #2d2f34 (Shade Ink) or #3f434a (Shade Charcoal) +- Do not use #ecedef or #d9dde6 as background fills — these are border tones only +- Do not break the pill/tag radius system with square chips or rounded-but-not-pill buttons +- Do not place #f67748 fills on large backgrounds (more than 20% of a section) — it dilutes the CTA signal + +## Surfaces + +| Level | Name | Value | Purpose | +|-------|------|-------|---------| +| 0 | Canvas | `#fdf9f4` | Page background — the warm cream that defines the whole site's atmosphere | +| 1 | White Product | `#ffffff` | Product mockup interiors and elevated tooltips — appears as white panels inside the cream canvas | +| 2 | Dust Card | `#f9efe4` | Feature cards and tag chip backgrounds — a half-step darker than canvas to delineate zones without contrast | +| 3 | Silver Border | `#ecedef` | Hairline card and button borders — the sole border tone | +| 4 | Ember Accent | `#f67748` | Selected card border and CTA fill — the only chromatic surface | + +## Elevation + +- **White Product Card:** `0 1px 3px rgba(0,0,0,0.1), 0 2px 6px rgba(0,0,0,0.05), 0 4px 12px rgba(0,0,0,0.01)` + +## Imagery + +Visuals are dominated by product screenshot mockups rendered on white cards inside the cream canvas, not by photography. The product UI is shown in a real working state (sidebar navigation, breadcrumb, content area with 'Troubleshooting' heading) rather than as a stylized hero render. Decorative elements are sparse: hand-drawn orange scribble circles around key headline words, and small abstract illustration washes in mint, violet, and pink used sparingly inside the product mock to add warmth. No lifestyle photography, no stock imagery. The visual density is text-dominant with screenshots functioning as evidence rather than decoration. + +## Layout + +Page model: centered max-width ~1200px with comfortable horizontal margins, full-bleed cream canvas behind. Hero is a centered headline stack — Garnett 64px display headline (with one word circled in orange) sits above a 19px UniversalSans subtitle, then a row of Dust Tag Chips, then the single Ember CTA. The product mockup appears below as a large White Product Card with soft shadow. Section rhythm: consistent vertical breathing room (96px between sections), no alternating dark/light bands — the page stays cream throughout. Social proof is a centered single-row logo wall. Feature blocks are 3-column Cream Feature Card grids with 32px radius. The page reads top-to-bottom as a single editorial document, not as a marketing patchwork. + +## Agent Prompt Guide + +**Quick Color Reference** +- text: #2d2f34 (headings) / #3f434a (body) / #5e646e (tertiary) +- background: #fdf9f4 (canvas) / #ffffff (elevated) / #f9efe4 (dust surface) +- border: #ecedef (hairline) / #d9dde6 (soft card edge) +- accent: #f67748 (ember — used for the single primary CTA, selected card border, and headline scribble annotation only) +- primary action: #f67748 (filled action) + +**3-5 Example Component Prompts** + +1. **Hero Headline with Scribble Annotation**: Centered Garnett 64px weight 500, color #2d2f34, line-height 1.2. One key word (e.g. 'Verified') wrapped in a hand-drawn 1.5px stroke #f67748 oval, positioned slightly above and around the word. Below: UniversalSans 19px weight 400, color #3f434a, line-height 1.4, max-width 640px centered. + +2. Create a Primary Action Button: #f67748 background, #000000 text, 9999px radius, compact pill padding. Use this filled treatment for the main CTA. + +3. **Dust Tag Chip Row**: Horizontal row of 3 pills, each #f9efe4 background, #3f434a text, UniversalSans 13px weight 500, border-radius 9999px, padding 8px 16px, 8px gap between chips. Centered above feature sections. + +4. **Cream Feature Card**: #f9efe4 background, border-radius 32px, padding 48px 24px 32px, no shadow. Optional 2px solid #f67748 left border for highlighted/selected cards. Contains a Garnett 24px weight 500 title in #2d2f34, UniversalSans 15px weight 400 body in #5e646e. + +5. **White Product Card**: #ffffff background, border-radius 12px, box-shadow 0 1px 3px rgba(0,0,0,0.1) + 0 2px 6px rgba(0,0,0,0.05) + 0 4px 12px rgba(0,0,0,0.01). Contains a product UI mockup (sidebar + content area) rendered in actual working state, not a stylized render. + +## Typography Pairing Logic + +The Garnett + UniversalSans pairing is a deliberate editorial choice: Garnett's serif character gives the page the weight of a printed document or annual report, while UniversalSans handles the 'typed notes' density of the body copy. The contrast is most visible at the hero where a Garnett 64px display headline sits above a UniversalSans 50px weight-400 subheadline — the weight-400 choice is anti-convention; most sites use 700 at this size, but Slite's subheadline whispers so the Garnett headline can speak. The two faces share the same font-feature-settings ('ss14', 'ss15', 'ss19'), meaning the stylistic alternates were designed as a pair — substituting with a mismatched system serif will break the visual continuity. + +## Radius as Hierarchy + +Border-radius is Slite's secondary hierarchy system. Marketing cards use the largest radius (32px) to feel designed and deliberate. Product mockup cards use 12–18px to feel like actual UI. Pill buttons and tags always use 999–9999px to feel approachable and tappable. The only sharp corner in the system is the 8px ghost button — it is reserved for tiny inline icon controls where a pill would look strange. When in doubt: larger radius = more editorial, smaller radius = more product. + +## The Ember Accent Rule + +#f67748 must appear on less than 5% of any page. The constraint is what gives the orange its signaling power — it marks 'this is the action you take' and 'this is the word you should remember'. When you use the orange anywhere that is not (a) a primary CTA, (b) a 'selected' card border, (c) a headline scribble annotation, or (d) a featured testimonial card background, you have broken the system. The page should be readable in greyscale; the orange is punctuation, not structure. + +## Similar Brands + +- **Notion** — Same warm cream canvas approach and pill-shaped tag/button system, though Notion uses a heavier weight and more varied type sizes +- **Mem** — Same serif-headline + sans-body editorial pairing and warm paper-canvas aesthetic, with a similarly restrained single-accent color strategy +- **Coda** — Same product-screenshot-driven marketing style and ghost/outlined button hierarchy, though Coda leans cooler in its neutrals +- **Tana** — Same knowledge-management category and warm off-white canvas, with comparable pill-chip tag systems above feature sections +- **Linear** — Same single-accent restraint (Linear uses indigo, Slite uses ember), same 32px-rounded feature cards, and the same three-layer soft shadow on elevated product surfaces + +## Quick Start + +### CSS Custom Properties + +```css +:root { + /* Colors */ + --color-parchment-cream: #fdf9f4; + --color-star-white: #ffffff; + --color-dust-sand: #f9efe4; + --color-moon-silver: #ecedef; + --color-shade-ink: #2d2f34; + --color-shade-charcoal: #3f434a; + --color-shade-slate: #5e646e; + --color-shade-fog: #9da3af; + --color-shade-dusk: #6a707c; + --color-border-mist: #d9dde6; + --color-ember-orange: #f67748; + --color-neptune-blue: #74a6f1; + --color-verification-green: #479a53; + --color-verified-mint: #bbf7d0; + --color-tag-violet: #4b51c3; + --color-illustration-violet: #6b70d6; + + /* Typography — Font Families */ + --font-garnett: 'Garnett', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --font-universalsans: 'UniversalSans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + + /* Typography — Scale */ + --text-label: 10px; + --leading-label: 1.2; + --text-caption: 13px; + --leading-caption: 1.2; + --text-body-sm: 15px; + --leading-body-sm: 1.5; + --text-button: 17px; + --leading-button: 1; + --text-body-lg: 19px; + --leading-body-lg: 1.4; + --text-heading-sm: 26px; + --leading-heading-sm: 1.3; + --text-heading: 28px; + --leading-heading: 1.25; + --text-heading-lg: 36px; + --leading-heading-lg: 1.2; + --text-hero: 50px; + --leading-hero: 1.5; + --text-display: 64px; + --leading-display: 1.2; + + /* Typography — Weights */ + --font-weight-regular: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + + /* Spacing */ + --spacing-unit: 4px; + --spacing-4: 4px; + --spacing-8: 8px; + --spacing-12: 12px; + --spacing-16: 16px; + --spacing-20: 20px; + --spacing-24: 24px; + --spacing-32: 32px; + --spacing-40: 40px; + --spacing-48: 48px; + --spacing-60: 60px; + --spacing-72: 72px; + --spacing-80: 80px; + --spacing-100: 100px; + --spacing-120: 120px; + --spacing-176: 176px; + --spacing-240: 240px; + + /* Layout */ + --page-max-width: 1200px; + --section-gap: 96px; + --element-gap: 8px; + + /* Border Radius */ + --radius-lg: 8px; + --radius-xl: 12px; + --radius-2xl: 18px; + --radius-2xl-2: 22px; + --radius-3xl: 32px; + --radius-3xl-2: 40px; + --radius-full: 999px; + --radius-full-2: 9999px; + + /* Named Radii */ + --radius-tags: 9999px; + --radius-cards: 32px; + --radius-buttons: 999px; + --radius-smallcards: 12px; + --radius-ghostbutton: 8px; + --radius-productcards: 18px; + + /* Shadows */ + --shadow-subtle: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.05) 0px 2px 6px 0px, rgba(0, 0, 0, 0.01) 0px 4px 12px 0px; + --shadow-sm: rgba(0, 0, 0, 0.2) 0px 2px 6px 0px; + + /* Surfaces */ + --surface-canvas: #fdf9f4; + --surface-white-product: #ffffff; + --surface-dust-card: #f9efe4; + --surface-silver-border: #ecedef; + --surface-ember-accent: #f67748; +} +``` + +### Tailwind v4 + +```css +@theme { + /* Colors */ + --color-parchment-cream: #fdf9f4; + --color-star-white: #ffffff; + --color-dust-sand: #f9efe4; + --color-moon-silver: #ecedef; + --color-shade-ink: #2d2f34; + --color-shade-charcoal: #3f434a; + --color-shade-slate: #5e646e; + --color-shade-fog: #9da3af; + --color-shade-dusk: #6a707c; + --color-border-mist: #d9dde6; + --color-ember-orange: #f67748; + --color-neptune-blue: #74a6f1; + --color-verification-green: #479a53; + --color-verified-mint: #bbf7d0; + --color-tag-violet: #4b51c3; + --color-illustration-violet: #6b70d6; + + /* Typography */ + --font-garnett: 'Garnett', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --font-universalsans: 'UniversalSans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + + /* Typography — Scale */ + --text-label: 10px; + --leading-label: 1.2; + --text-caption: 13px; + --leading-caption: 1.2; + --text-body-sm: 15px; + --leading-body-sm: 1.5; + --text-button: 17px; + --leading-button: 1; + --text-body-lg: 19px; + --leading-body-lg: 1.4; + --text-heading-sm: 26px; + --leading-heading-sm: 1.3; + --text-heading: 28px; + --leading-heading: 1.25; + --text-heading-lg: 36px; + --leading-heading-lg: 1.2; + --text-hero: 50px; + --leading-hero: 1.5; + --text-display: 64px; + --leading-display: 1.2; + + /* Spacing */ + --spacing-4: 4px; + --spacing-8: 8px; + --spacing-12: 12px; + --spacing-16: 16px; + --spacing-20: 20px; + --spacing-24: 24px; + --spacing-32: 32px; + --spacing-40: 40px; + --spacing-48: 48px; + --spacing-60: 60px; + --spacing-72: 72px; + --spacing-80: 80px; + --spacing-100: 100px; + --spacing-120: 120px; + --spacing-176: 176px; + --spacing-240: 240px; + + /* Border Radius */ + --radius-lg: 8px; + --radius-xl: 12px; + --radius-2xl: 18px; + --radius-2xl-2: 22px; + --radius-3xl: 32px; + --radius-3xl-2: 40px; + --radius-full: 999px; + --radius-full-2: 9999px; + + /* Shadows */ + --shadow-subtle: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.05) 0px 2px 6px 0px, rgba(0, 0, 0, 0.01) 0px 4px 12px 0px; + --shadow-sm: rgba(0, 0, 0, 0.2) 0px 2px 6px 0px; +} +``` diff --git a/docs/ENGINEERING_CONVENTIONS.md b/docs/ENGINEERING_CONVENTIONS.md new file mode 100644 index 0000000..fa35322 --- /dev/null +++ b/docs/ENGINEERING_CONVENTIONS.md @@ -0,0 +1,208 @@ +# Portfolio Engineering Conventions + +This document is the source of truth for implementation conventions shared by +humans and coding assistants. + +## Language + +- Source code, code comments, test descriptions, README content, agent + instructions, and technical documentation must be written in English. +- Runtime copy must be localized and must never be embedded directly in a + component. +- Translation catalogs must contain the same keys for English, Brazilian + Portuguese, and French. + +## Runtime and package manager + +- Node.js 24 is the only supported Node.js major version. +- `.nvmrc`, `.node-version`, `package.json`, and CI must remain aligned. +- pnpm is the only supported package manager. +- Use `pnpm exec` for project binaries. Do not document npm, npx, or Yarn + commands. +- Add a preinstall guard that rejects unsupported package managers. + +## Quality tools + +- Biome is the only JavaScript, TypeScript, JSON, and CSS formatter/linter. +- The root `biome.json` is the single source of truth. Do not create per-folder + overrides. +- Vitest and Testing Library cover component behavior and browser-preference + helpers. +- Playwright covers the deployed navigation, locale/theme behavior, responsive + layouts, and avatar fallback. +- The minimum handoff gate is `pnpm check`. + +The intended scripts are: + +```json +{ + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "pnpm lint:biome && pnpm lint:conventions", + "lint:biome": "biome lint .", + "lint:conventions": "node scripts/check-code-conventions.mjs", + "format": "biome check --write .", + "format:check": "biome format .", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:e2e": "playwright test", + "check": "pnpm typecheck && pnpm lint && pnpm test && pnpm format:check && pnpm build" +} +``` + +## TypeScript and control flow + +- Do not use `let`, `else`, `else if`, or `switch` in TypeScript or JavaScript. +- Prefer `const`, guard clauses, early returns, lookup objects, and small named + helpers. +- Prefer arrow functions for Preact components and helpers unless a library API + requires a function declaration. +- The convention checker runs with the normal lint command and rejects the + forbidden syntax. +- Avoid unsafe casts and non-null assertions. Model content and browser state + with explicit types. + +## Component structure + +Every reusable component has its own PascalCase directory, a matching source +file, and a barrel: + +```text +components/ +└── ProjectCard/ + ├── ProjectCard.tsx + ├── ProjectCard.test.tsx + └── index.ts +``` + +Imports target the barrel: + +```ts +import { ProjectCard } from '@/components/ProjectCard' +``` + +Components used by only one parent are colocated under that parent's +`components/` directory: + +```text +Hero/ +├── Hero.tsx +├── index.ts +└── components/ + └── GitHubAvatar/ + ├── GitHubAvatar.tsx + ├── GitHubAvatar.test.tsx + └── index.ts +``` + +Additional rules: + +- Keep page composition thin and delegate each meaningful section to a named + component. +- Keep content records outside components. +- Keep browser preference and persistence logic in dedicated `lib` modules. +- Use semantic HTML before introducing abstractions. +- Add a shared icon registry instead of importing icon components throughout + feature code. +- Avoid generic wrappers that exist only to reduce line count. + +## Content model + +- Stable neutral data belongs in typed modules under `src/content`. +- User-facing prose belongs in the translation catalogs. +- Dates are stored as canonical values and localized at render time. +- Career durations are calculated from dates; do not commit text such as + “6 years 4 months” that immediately becomes stale. +- Project order is editorial. Never derive the showcase from repository update + time, star count, or API order. +- Do not invent metrics, proficiency levels, roles, or project outcomes. + +## Internationalization + +- Supported locale identifiers are `en`, `pt-BR`, and `fr`. +- English is the fallback locale. +- On a first visit, resolve the locale in this order: + 1. a supported browser locale from `navigator.languages` or + `navigator.language`; + 2. English. +- After a visitor explicitly selects a language, persist that override locally + and prefer it on later visits. +- Match regional browser values by both exact tag and base language, so `fr-CA` + may select `fr` and `pt-PT` may select the available Portuguese catalog. +- Update ``, title, description, Open Graph copy, accessible names, + and visible content together. +- Use visible language names. Flags may be decorative but cannot be the label. +- French copy requires human review before release. + +## Theme + +- On a first visit, use `prefers-color-scheme` when available. +- Fall back to light mode when the preference is missing or cannot be resolved. +- Persist only an explicit user override. An `auto` option should resume system + detection and respond to later operating-system changes. +- Apply the resolved theme before the first paint to avoid a flash of the wrong + palette. +- Use only semantic color tokens in components. Both themes must preserve the + warm editorial identity defined by `docs/DESIGN.md`. +- Keep the ember accent below five percent of the page and use ink-colored text + on the orange CTA to meet contrast requirements. + +## Responsive behavior and accessibility + +- Design mobile-first and verify at 320 px, a representative tablet width, and + desktop. +- Avoid fixed content widths that cause horizontal scrolling. +- Navigation and every essential action must work without hover. +- Provide visible focus, semantic landmarks, ordered headings, a skip link, and + localized accessible names. +- Keep primary touch targets at least 44 by 44 pixels. +- Support keyboard-only navigation, 200 percent zoom, reduced motion, and + high-contrast text. +- Images require intrinsic dimensions and meaningful localized alternative + text. Decorative images use an empty alt value. + +## GitHub data and privacy + +- Load the profile image from the public GitHub username image endpoint and + provide a local fallback asset. +- Do not make the page depend on a runtime GitHub API request. +- Optional repository metadata enrichment runs at build time and degrades + gracefully when unavailable. +- Never expose an API token in client code or the generated bundle. +- The Jig Solver application is public at `https://jigsolver.app/`, while its + source repository is private. Link to the product, not the private repository. +- Do not publish residential addresses or personal phone numbers. + +## Git workflow + +- Commit messages use Conventional Commits and are written in English. +- Coding agents may create a commit only after an explicit user request in the + current conversation. +- Authorization covers only changes the user has already reviewed. Any later + edit requires another review and explicit commit request. +- Lefthook runs `pnpm lint` before a commit. +- Commitlint validates the commit message in the `commit-msg` hook. +- Never bypass repository hooks with `--no-verify`. + +## Continuous integration and deployment + +- Pull requests and pushes to `main` or `master` run type checking, Biome, + convention checks, unit tests, responsive browser tests, and a production + build. +- Deployment to GitHub Pages starts only after the `CI` workflow succeeds for a + trusted push to `main` or `master`. +- Vite outputs static files to `dist`; GitHub Actions publishes that artifact. +- Use `base: '/'` for the selected `LucasPMM.github.io` repository name. +- Keep the workflow permissions minimal: repository contents read, Pages write, + and ID token write for the deploy job. +- Reference maintained GitHub Actions by their stable major release tag so + compatible fixes are adopted without obscuring the workflow. + +## Documentation synchronization + +- Update the roadmap when scope or completion state changes. +- Update `docs/DESIGN.md` when a visual token or invariant changes. +- Update README setup and deployment instructions with the implementation that + introduces them. +- A behavior change is not complete while its tests or documentation are stale. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..1ea165a --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,855 @@ +# Lucas Mariz Portfolio Modernization Roadmap + +> Updated on September 11, 2026. User-provided career and education details are +> the primary source of truth. Public GitHub, LinkedIn, the local Planner +> repository, and the local Jig Solver repository provide supporting technical +> context. + +## 1. Product objective + +Replace the 2018 résumé page with a fast, accessible, multilingual personal +portfolio that answers four questions in under one minute: + +1. Who is Lucas Mariz today? +2. What professional and academic path brought him here? +3. What problems, technologies, and projects does he work on? +4. How can a visitor learn more or get in touch? + +The product remains intentionally small: a statically generated site with no +backend or CMS, deployed automatically to GitHub Pages. + +## 2. Current repository assessment + +### Preserve + +- The Git history and the narrative value of modernizing the original résumé. +- A single-page, scannable experience. +- The design system in `docs/DESIGN.md`: warm paper surfaces, editorial + typography, generous spacing, rounded cards, and a restrained ember accent. + +### Replace or correct + +- Bulma 0.7.1, Font Awesome 5, and legacy CDN dependencies. +- Invalid HTML, icon-only semantics, and missing social/SEO metadata. +- The outdated student-only biography and incorrect education dates. +- Placeholder project copy and skill lists without context or evidence. +- The local 2018 portrait. +- The residential address and personal phone number. The public site should + show at most city/country and professional contact channels. +- The missing professional timeline, responsive system, theme handling, + localization, tests, and deployment pipeline. + +## 3. Confirmed content source of truth + +### Public identity + +- **Display name:** Lucas Mariz. +- **Primary role:** Senior Software Engineer. +- **Professional focus:** JavaScript, TypeScript, React, React Native, scalable + product architecture, automation, and testability. +- **Current technical interests:** Computer Vision, applied AI, optimization, + software architecture, developer experience, and automation. + +### Professional experience + +#### Senior Software Engineer — ABILITYA + +- Employment: full-time. +- Period: June 2020 to present. +- Location: Milan, Lombardy, Italy — remote. +- Core experience: JavaScript, TypeScript, React, and React Native. +- Responsibilities and impact areas: + - evolve scalable white-label application architecture; + - build engineering and product automations; + - improve testability across the React web/mobile ecosystem; + - establish end-to-end coverage with Cypress and Maestro. + - share engineering responsibility for the official Cagliari Calcio app, + serving more than 10,000 users across Android and iOS. + +The portfolio must calculate duration from canonical dates instead of storing a +text value such as “6 years 4 months.” Quantitative evidence still needs to be +collected, for example the number of brands/tenants, release-frequency change, +test coverage, execution-time reduction, or escaped-defect reduction. No metric +may be invented. + +#### Frontend Developer — Pluritech Brasil + +- Period: September 2018 to June 2020. +- Public location: Belo Horizonte, Brazil. Do not publish the street address + from the old LinkedIn entry. +- Core experience: Angular and Ionic. +- Content angle: early professional experience building cross-platform web and + mobile interfaces before moving into the React ecosystem. + +### Selected professional and academic work + +#### Cagliari Calcio official app + +- Lucas is one of the engineers responsible for the official supporter app as + part of his work at ABILITYA. +- Owner-provided reach: more than 10,000 users across Android and iOS. +- Public evidence: the official product is available on the + [App Store](https://apps.apple.com/it/app/cagliari-calcio/id441230439) and + [Google Play](https://play.google.com/store/apps/details?id=com.cagliaricalcioapp.net). +- Portfolio framing: product responsibility, cross-platform delivery, and a + live supporter experience. Do not imply sole ownership. + +#### Role Vectors paper + +- Title: **Role Vectors: Tracking-Based Representations of Football Players’ + Tactical Behavior**. +- Lucas is a co-author. The paper introduces an interpretable, + tracking-derived representation of football players' relative tactical roles, + including in-possession and off-ball behavior. +- Accepted and presented at the 13th Workshop on Machine Learning and Data + Mining for Sports Analytics (MLSA 2026), in Naples, Italy, on September 7, + 2026. +- Public evidence: [paper](https://dtai.cs.kuleuven.be/events/MLSA26/papers/MLSA26_paper_208.pdf) + and [conference schedule](https://dtai.cs.kuleuven.be/events/MLSA26/schedule.php). + +### Academic background + +- **Bachelor's degree, Computer Science — UFMG:** January 2023 to 2027, + expected graduation in 2027. +- **Bachelor's degree, Computational Mathematics — UFMG:** January 2019 to + January 2023, completed. +- **IT Technician — Colégio Técnico da UFMG (COLTEC):** 2015 to 2017, + completed. + +### Languages + +- Portuguese: native. +- English: Cambridge C1. +- French: currently learning; do not claim a CEFR level. + +### Remaining content inputs + +- Official Cambridge certificate name, issue year, and optional credential URL. +- One or two additional nonconfidential quantitative outcomes from ABILITYA, + beyond the confirmed 10,000+ Cagliari app audience. +- A concise Pluritech product/context description and one representative + outcome. +- Preferred public contact channel: LinkedIn, professional email, or both. +- Whether a downloadable PDF résumé belongs in the first release. +- Native or professional review of the French catalog before public release. + +## 4. Curated project showcase + +Project selection is editorial and must not follow GitHub update time, stars, or +API order. The initial showcase is fixed to these six projects: + +1. Jig Solver. +2. Planner. +3. Simplex. +4. Pokémon Base. +5. Greedy K-means. +6. LZ78 Compression. + +The GitHub account may be reorganized independently. Removing unrelated public +repositories must not change portfolio order or copy. + +### 4.1 Jig Solver — primary case study + +#### Verified facts + +- Public product: [jigsolver.app](https://jigsolver.app/). +- Source repository: `LucasPMM/jig-solver`, currently private. The portfolio + links to the product, not the private repository. +- Product purpose: solve physical jigsaw puzzles computationally and assist a + person assembling the physical puzzle through a camera-oriented workflow. +- Web stack: Next.js 16, React 19, TypeScript, Tailwind CSS 4, TanStack Query, + Three.js, Zod, Vitest, and Playwright. +- API and vision stack: Python 3.12, FastAPI, SQLAlchemy, Alembic, PostgreSQL, + MinIO, NumPy, OpenCV, optional MobileSAM/PyTorch, Pytest, and Ruff. +- Delivery stack: pnpm workspace, Docker Compose, Caddy, Biome, Lefthook, and + Commitlint. +- Current stage: the reference-guided solver and the camera assistant tracks + documented as phases 3 and 4 are complete. The product already includes + piece analysis, ranked compatibility, persisted placements, an asynchronous + solver lifecycle, a Three.js workspace, multi-piece camera analysis, stable + tracking/identity, border detection, guidance, physical confirmations, and + benchmark coverage. Further product and quality work remains active. + +#### Portfolio treatment + +- Label: **Active project**. +- Primary link: `https://jigsolver.app/`. +- Short description: + > A computer-vision platform that digitizes physical jigsaw pieces, evaluates + > compatibility, reconstructs the puzzle, and guides physical assembly + > through a camera-based assistant. +- Explain the engineering split: independent Python solving/vision engine, + statically typed web client, and mobile-first camera experience. +- Mention the research dimensions: segmentation, contour/side analysis, + descriptors, compatibility ranking, reconstruction, tracking, and evaluation. +- Use the existing solver workspace screenshot as the primary visual: + `../../ufmg/jig-solver/apps/web/public/docs/solver-workspace.png`. +- During implementation, copy and optimize that image into + `public/projects/jig-solver/solver-workspace.webp`. Keep the original project + screenshot and portfolio derivative synchronized when visible UI changes. +- The optional secondary visual for a future case-study view is + `../../ufmg/jig-solver/apps/web/public/docs/assistant-borders.png`. + +### 4.2 Planner — private financial assistant + +#### Verified facts + +- Source repository and application: private; the portfolio must not link to + either one. +- Product purpose: an installable personal-finance dashboard for recurring + expenses, income, cash flow, debts, financing scenarios, goals, reports, and + read-only AI guidance. +- Web stack: Next.js 16, React 19, TypeScript, Tailwind CSS 4, React Hook Form, + Zod, Recharts, jsPDF, and static export/PWA delivery. +- Platform stack: Firebase Authentication, Firestore, callable Firebase + Functions, and Secret Manager. +- Assistant architecture: provider-neutral BYOK integration with bounded, + deterministic financial summaries; stored credentials are protected with + AES-256-GCM rather than exposed to the browser or model context. + +#### Portfolio treatment + +- Label: **Private project**. +- Describe the problem, architecture, Lucas's end-to-end contribution, and the + integrated product result without exposing private URLs, identifiers, + credentials, screenshots, or internal operational details. +- Use technology names only when confirmed by the local source. + +### 4.3 Public repositories + +| Display name | Repository | Known focus | Published evidence | +|---|---|---|---| +| Simplex | [`simplex`](https://github.com/LucasPMM/simplex) | Python | Two-phase tableau, Bland's rule, certificates, and fixture coverage | +| Pokémon Base | [`Pokemon-Base`](https://github.com/LucasPMM/Pokemon-Base) | TypeScript | Routed Angular architecture, data wrapper, views, charts, and tests | +| Greedy K-means | [`greedy-kmeans`](https://github.com/LucasPMM/greedy-kmeans) | Jupyter/Python | Ten-dataset method comparison, evaluation metrics, and result tables | +| LZ78 Compression | [`lz78-compression`](https://github.com/LucasPMM/lz78-compression) | Python | Compressed trie, round-trip CLI flows, test corpus, and report | + +Each card should communicate problem, approach, Lucas's contribution, and +result. Technology tags are supporting metadata, not the description. + +## 5. Technical stack decision + +### Recommendation: Preact + TypeScript + Vite + +- **Preact** provides component composition, hooks, and a React-like API with a + small client footprint. +- **TypeScript** validates experience, education, project, locale, and theme + models. +- **Vite** provides fast development, static production builds, and a direct + GitHub Pages deployment path. +- **i18next core** provides `en`, `pt-BR`, and `fr`. A small Preact adapter + subscribes to `languageChanged`; adding React compatibility solely for + localization is unnecessary. +- **Native CSS** implements the existing tokens. Tailwind and a component + framework would add another abstraction without solving a portfolio-specific + problem. +- **Biome** is the only JavaScript, TypeScript, JSON, and CSS lint/format tool. +- **Vitest + Testing Library** cover behavior; **Playwright** covers locale, + theme, responsive layout, external links, and avatar fallback. +- **GitHub Actions + GitHub Pages** run quality checks and deploy `dist`. + +No backend, database, CMS, or client-side secret is required. No router is +required for the one-page MVP. Preact's Vite prerender support can emit useful +HTML at build time without introducing a server runtime. + +### Repository tooling baseline + +Adapt the Planner repository conventions: + +- Node.js 24 pinned in `.nvmrc`, `.node-version`, `package.json`, and CI. +- pnpm as the only package manager, with a preinstall guard. +- A root `biome.json` as the only formatter/linter configuration. +- `scripts/check-code-conventions.mjs` to enforce the agreed TypeScript control + flow rules. +- Lefthook running `pnpm lint` before commits. +- Commitlint enforcing English Conventional Commits. +- `pnpm check` as the minimum handoff gate. +- No commits from coding agents without explicit, current user authorization. + +Repository rules are recorded in `AGENTS.md` and +`docs/ENGINEERING_CONVENTIONS.md`. + +## 6. Component architecture + +Follow the Planner component ownership model: every component has a PascalCase +folder, a matching source file, an `index.ts` barrel, and a focused test when it +owns behavior. A component used by one parent is nested under that parent's +`components` folder. + +```text +. +├── .github/workflows/ +│ ├── ci.yml +│ └── deploy.yml +├── public/ +│ ├── projects/jig-solver/solver-workspace.webp +│ ├── avatar-fallback.svg +│ ├── favicon.svg +│ ├── robots.txt +│ └── sitemap.xml +├── scripts/ +│ ├── check-code-conventions.mjs +│ ├── require-pnpm.mjs +│ └── verify-static-build.mjs +├── src/ +│ ├── components/ +│ │ ├── PortfolioPage/ +│ │ │ ├── PortfolioPage.tsx +│ │ │ ├── index.ts +│ │ │ └── components/ +│ │ │ ├── AppHeader/ +│ │ │ ├── HeroSection/ +│ │ │ ├── AboutSection/ +│ │ │ ├── ExperienceSection/ +│ │ │ ├── SelectedWorkSection/ +│ │ │ ├── EducationSection/ +│ │ │ ├── ProjectsSection/ +│ │ │ │ └── components/ +│ │ │ │ ├── ProjectCard/ +│ │ │ │ └── ProjectDetails/ +│ │ │ ├── CapabilitiesSection/ +│ │ │ └── ContactSection/ +│ │ └── ui/ +│ │ ├── ExternalLink/ +│ │ ├── LanguageSwitcher/ +│ │ ├── SectionHeading/ +│ │ └── ThemeSwitcher/ +│ ├── content/ +│ │ ├── education.ts +│ │ ├── experience.ts +│ │ ├── profile.ts +│ │ └── projects.ts +│ ├── lib/ +│ │ ├── i18n/ +│ │ │ ├── catalog.ts +│ │ │ ├── i18n.ts +│ │ │ ├── locale.ts +│ │ │ ├── index.ts +│ │ │ └── components/I18nProvider/ +│ │ └── theme/ +│ │ ├── theme.ts +│ │ ├── index.ts +│ │ └── components/ThemeProvider/ +│ ├── styles/ +│ │ ├── base.css +│ │ ├── components.css +│ │ └── tokens.css +│ ├── App.tsx +│ ├── main.tsx +│ └── prerender.tsx +├── AGENTS.md +├── biome.json +├── commitlint.config.mjs +├── lefthook.yml +├── vite.config.ts +└── package.json +``` + +Stable neutral data—dates, links, technology identifiers, and project order— +lives in typed content modules. Every visitor-facing sentence lives in the +single typed translation catalog. This prevents three career histories from +drifting apart. + +## 7. Information architecture and draft content + +### 7.1 Header + +- “LM” monogram and Lucas Mariz. +- Anchor navigation: About, Experience, Selected work, Education, Projects, + Contact. +- Visible language and theme controls. +- A compact accessible mobile menu when anchor links no longer fit. + +### 7.2 Hero + +- Circular GitHub profile image. +- Eyebrow: `SENIOR SOFTWARE ENGINEER · REMOTE`. +- Proposed English headline: + **“I build scalable digital products and explore how AI solves visual + problems.”** +- Summary: senior web/mobile engineering in the JavaScript ecosystem, backed by + Computational Mathematics, Computer Science, and applied Computer Vision. +- Chips: Web & Mobile, White-label Platforms, Computer Vision, Applied AI. +- One ember CTA: “View projects.” Secondary links: GitHub and LinkedIn. + +### 7.3 About + +Use one short editorial paragraph connecting: + +- senior product engineering; +- React and React Native delivery; +- white-label architecture and automation; +- mathematical/computer-science education; +- current Computer Vision and optimization work. + +Avoid unsupported adjectives and generic soft-skill claims. + +### 7.4 Experience + +Use a reverse-chronological timeline. Each entry contains role, company, +location/remote status, canonical dates, one problem statement, and two or three +impact bullets. + +ABILITYA is the primary entry and emphasizes scale, white-label architecture, +automation, and Cypress/Maestro testability. Pluritech establishes the Angular +and Ionic foundation of the web/mobile career. + +### 7.5 Selected work + +- A production-product card for the official Cagliari Calcio app with the + owner-provided 10,000+ audience metric, a precise shared-responsibility claim, + and links to both official stores. +- A research card for the Role Vectors paper with its exact title, concise + abstract-derived summary, MLSA 2026 presentation details, paper link, and + schedule link. + +### 7.6 Education + +- Computer Science — UFMG — 2023–2027, expected. +- Computational Mathematics — UFMG — 2019–2023, completed. +- IT Technician — COLTEC/UFMG — 2015–2017, completed. + +### 7.7 Projects + +- A large Jig Solver case-study card with the real solver screenshot and a link + to the public application. +- A private-project card for Planner, with no product or source link. +- Four public-repository cards for Simplex, Pokémon Base, Greedy K-means, and + LZ78 Compression. +- Never show a GitHub link for the private Jig Solver repository. +- If a public repository is removed during GitHub cleanup, either preserve a + stable public case-study URL or remove its card intentionally. Do not leave a + broken link. + +### 7.8 Skills and languages + +Group capabilities by context; do not use percentage bars: + +- **Product engineering:** JavaScript, TypeScript, React, React Native, Angular, + Ionic. +- **Quality and delivery:** Cypress, Maestro, automation, testability, GitHub + Actions. +- **Applied research:** Python, OpenCV, Computer Vision, AI, optimization. +- **Languages:** Portuguese — native; English — Cambridge C1; French — learning. + +### 7.9 Contact + +- Invite conversations about software engineering, mobile products, Computer + Vision, applied AI, and product ideas. +- Link GitHub and LinkedIn. +- Add an email only after confirming it as a public professional channel. +- Do not add a contact form in the MVP; it would introduce an external service, + spam handling, and a privacy surface. + +## 8. Screen outline + +### Desktop + +```text +┌──────────────────────────────────────────────────────────────────────┐ +│ LM Lucas Mariz About Experience Education Projects EN Theme │ +├──────────────────────────────────────────────────────────────────────┤ +│ [ GitHub photo ] │ +│ SENIOR SOFTWARE ENGINEER · REMOTE │ +│ I build scalable digital products and explore how AI solves │ +│ visual problems. │ +│ [Web & Mobile] [White-label] [Computer Vision] [Applied AI] │ +│ [ View projects ] GitHub LinkedIn │ +├──────────────────────────────────────────────────────────────────────┤ +│ About short editorial introduction │ +├──────────────────────────────────────────────────────────────────────┤ +│ Experience Education │ +│ ● ABILITYA · 2020–present ● Computer Science · 2023–2027 │ +│ │ scale · automation · E2E ● Computational Math · 2019–2023 │ +│ ● Pluritech · 2018–2020 ● COLTEC · 2015–2017 │ +├──────────────────────────────────────────────────────────────────────┤ +│ Active project │ +│ ┌────────────────────── JIG SOLVER ────────────────────────────────┐ │ +│ │ product + architecture + real solver screenshot + public link │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +│ ┌──── Planner ────┐ ┌──── Simplex ─────┐ │ +│ └─────────────────┘ └───────────────────┘ │ +│ ┌─ Pokémon Base ─┐ ┌─ Greedy K-means ─┐ │ +│ └─────────────────┘ └───────────────────┘ │ +│ ┌─ LZ78 Compression ───────────────────┐ │ +│ └───────────────────────────────────────┘ │ +├──────────────────────────────────────────────────────────────────────┤ +│ Skills · Languages · Selected certification │ +├──────────────────────────────────────────────────────────────────────┤ +│ Let's talk GitHub · LinkedIn · PDF │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +### Mobile + +```text +┌──────────────────────────┐ +│ LM Lucas Mariz EN ◐ ☰ │ +│ [GitHub photo] │ +│ Senior Software Engineer│ +│ headline in 3–4 lines │ +│ wrapping chips │ +│ [ View projects ] │ +│ GitHub · LinkedIn │ +├──────────────────────────┤ +│ About │ +├──────────────────────────┤ +│ Experience │ +│ vertical timeline │ +├──────────────────────────┤ +│ Education │ +├──────────────────────────┤ +│ Jig Solver │ +│ [real solver image] │ +│ product + architecture │ +├──────────────────────────┤ +│ five supporting cards │ +│ one per row │ +├──────────────────────────┤ +│ Skills · Languages │ +├──────────────────────────┤ +│ Contact │ +└──────────────────────────┘ +``` + +## 9. Applying the design rules + +### Light theme + +- Canvas: `#fdf9f4`, never cold white at page level. +- Elevated surfaces: `#ffffff`; dust cards/chips: `#f9efe4`. +- Text: `#2d2f34`, `#3f434a`, and `#5e646e`. +- Ember `#f67748` on less than five percent of the page: primary CTA, one + editorial scribble, and one selected/highlight state. +- Lora for headings and Public Sans for body copy, using the documented free + substitutes for Garnett and UniversalSans. +- Maximum width 1200 px, 96 px section rhythm, and 32 px editorial cards. + +`docs/DESIGN.md` contains a CTA text-color contradiction: one section requests +white and another requests black. Accessibility resolves it. White on +`#f67748` is approximately 2.74:1, while `#2d2f34` on `#f67748` is +approximately 4.88:1. Use ink-colored CTA text. + +### Dark theme + +The dark palette should still feel like paper and ink, not a blue/black +dashboard: + +| Semantic token | Light | Dark proposal | +|---|---|---| +| canvas | `#fdf9f4` | `#1b1917` | +| elevated surface | `#ffffff` | `#24211f` | +| dust surface | `#f9efe4` | `#302a26` | +| border | `#ecedef` | `#47413d` | +| primary text | `#2d2f34` | `#f5eee7` | +| secondary text | `#5e646e` | `#c9beb4` | +| ember | `#f67748` | `#ff895e` | + +### Interaction and accessibility + +- Use approximately 130 ms transitions and respect `prefers-reduced-motion`. +- Do not hide content behind entrance animations. +- Provide a skip link, ordered headings, landmarks, and visible focus. +- Keep touch targets at least 44 by 44 pixels. +- Never rely on color alone for status. +- Test full keyboard navigation, 200 percent zoom, and reduced motion. +- Give the avatar intrinsic dimensions, localized alt text, and a local + fallback. + +## 10. Browser-derived language and theme + +### Locale resolution + +On the first visit: + +1. Read `navigator.languages`, then `navigator.language`. +2. Match an exact supported tag when possible. +3. Match a supported base language when appropriate. +4. Fall back to **English** when no browser locale is available or supported. + +After an explicit visitor choice, persist and prefer that override. Update +`document.documentElement.lang`, title, description, Open Graph copy, visible +content, and accessibility labels together. + +Supported catalogs: + +- `en` — product fallback. +- `pt-BR` — native-language version. +- `fr` — learning-language version; requires human review before release. + +Use language names—English, Português, Français—rather than flags. + +### Theme resolution + +On the first visit: + +1. Resolve `prefers-color-scheme` when supported. +2. Use light or dark according to the browser/operating-system preference. +3. Fall back to **light** when the preference is unavailable or unresolved. + +Persist an explicit visitor override. An `auto` option removes the override and +resumes system detection. Apply the resolved theme in a small head script before +first paint to prevent a theme flash. + +Tests must cover exact locale matching, base-language matching, unsupported +locale fallback, missing browser APIs, stored overrides, system theme changes, +and invalid persisted values. + +## 11. GitHub avatar and repository metadata + +Use the stable username endpoint for the profile image: + +```tsx +{translate('hero.avatarAlt')} +``` + +If the request fails, replace it once with `/avatar-fallback.svg`. Avoid an +error loop and reserve dimensions to prevent layout shift. + +Project content remains local and curated. Optional GitHub metadata enrichment +may happen during the GitHub Actions build, store only non-sensitive fields, +and degrade without blocking the build. Do not expose a token or rely on a +runtime API request. + +## 12. Execution roadmap + +### Phase 0 — governance, content, and privacy (P0) — complete + +- Add the adapted repository instructions and engineering conventions. +- Convert existing/new technical documentation to English when touched. +- Confirm the remaining content inputs in section 3. +- Remove the residential address and personal phone number. +- Preserve canonical career dates and calculate durations at runtime. +- Record confirmed source content and identify evidence gaps for the deeper + project pass in phase 4. + +**Exit:** English repository governance exists, approved source content is +structured, and unnecessary personal data is excluded. + +### Phase 1 — technical foundation (P0) — complete + +- Create Preact + TypeScript + Vite on a modernization branch. +- Pin Node.js 24 and pnpm; add the package-manager guard. +- Configure Biome, convention checking, Vitest, Playwright, Lefthook, and + Commitlint. +- Add the Planner-style component folder/barrel convention. +- Add typed content schemas and the typed i18next catalog. +- Keep all work uncommitted until Lucas reviews it and explicitly requests a + commit. + +**Exit:** `pnpm check` and `pnpm build` produce a valid static site shell. + +### Phase 2 — design system and responsive composition (P0) — complete + +- Convert `docs/DESIGN.md` tokens into semantic CSS custom properties. +- Implement the component tree from section 6. +- Build the header, hero, timelines, cards, and footer mobile-first. +- Apply Lora/Public Sans, the 96 px section rhythm, and 32 px editorial cards. +- Add accessible focus, interaction, image, and error states. + +**Exit:** the complete English layout works from 320 to 1440 px. + +### Phase 3 — browser defaults, theme, and i18n (P0) — complete + +- Implement browser-locale detection with English fallback. +- Implement system-theme detection with light fallback. +- Persist explicit overrides and prevent first-paint theme flash. +- Complete `en`, `pt-BR`, and `fr` catalogs with key parity tests. +- Update document language and localized metadata. +- Keep the French catalog ready for a final native or professional language + review before public release. + +**Exit:** every control and content section works in three languages and two +themes, including missing/unsupported browser preference paths. + +### Phase 4 — deeper career and project evidence (P1) — complete + +- Publish the confirmed ABILITYA and Pluritech evidence while retaining the + remaining nonconfidential metric gaps for a later content update. +- Deepen the six fixed showcase projects beyond their initial summaries. +- Preserve the Cagliari and Role Vectors evidence cards and verify their public + links during release checks. +- Build the Jig Solver feature card with its public URL, verified architecture, + private-source treatment, and optimized real screenshot. +- Add Planner as a private financial-assistant project using only stack and + product facts verified in the local repository; expose no private link. +- Add concise evidence-based summaries for the four public repositories. +- Keep optional build-time GitHub metadata out of the critical rendering path. + +**Exit:** each project explains problem, approach, contribution, and result; +private links and invented metrics are absent. + +### Phase 5 — quality, SEO, and performance (P0) — complete + +- Add localized Open Graph metadata, favicon, canonical URL, sitemap, and + `robots.txt`. +- Prerender useful page content before JavaScript hydration. +- Test keyboard navigation, contrast, 200 percent zoom, reduced motion, and + avatar fallback. +- Run responsive Playwright checks in English, Portuguese, and French. +- Lighthouse mobile targets: Performance at least 95 and Accessibility, Best + Practices, and SEO at least 95. + +**Exit:** `pnpm check`, production build, browser tests, and accessibility +review pass. + +Completion evidence on September 11, 2026: + +- The production build prerenders the English fallback page before hydration + and passes the static-output verification script. +- The Chromium suite passes in desktop and mobile projects, covering Axe, + locales and metadata, theme persistence, avatar resilience, verified public + links, private Planner link isolation, responsive overflow, keyboard entry, + reduced motion, and 200 percent text sizing. +- Lighthouse mobile scores: Performance 98, Accessibility 100, Best Practices + 100, and SEO 100. Desktop scores 100 in all four categories. + +### Phase 6 — rename and deploy (P0) — implementation complete, activation pending + +- The Vite base path, canonical URL, Open Graph URL, sitemap, and `robots.txt` + target `https://lucaspmm.github.io/`. +- A least-privilege GitHub Pages workflow publishes only after `CI` succeeds + for a trusted push to `main` or `master`. +- GitHub Actions use stable major release tags, and the deployment rebuilds the + exact portfolio revision accepted by CI. +- README and this runbook document the rename, Pages-source migration, remote + update, metadata update, and public smoke test. +- Lucas still needs to rename the repository, switch Pages from the current + legacy branch source to GitHub Actions, merge the branch, and update external + profile links. + +**Exit:** the renamed site is live and all public links use the new URL. + +**Current state:** every repository-side prerequisite is implemented. The exit +condition remains pending until the external activation checklist in section 14 +is completed and the public URL passes its smoke test. + +## 13. Selected repository name + +### `LucasPMM.github.io` + +This makes the repository the GitHub Pages user site and produces the clean URL +`https://lucaspmm.github.io/`. It also allows Vite `base: '/'`. The technical +repository name does not change the public title “Lucas Mariz.” + +The name is available as of September 11, 2026. The existing `Curriculum` +repository is public, uses `master` as its default branch, and currently serves +Pages from the repository root in legacy branch mode. + +## 14. Safe rename tutorial + +The current remote is `git@github.com:LucasPMM/Curriculum.git`, and the current +local default branch is `master`. + +### A. Prepare + +1. Review and explicitly authorize the phase 6 implementation commit. +2. Ensure the new build is versioned and no local work is forgotten. +3. Record the old URL `https://lucaspmm.github.io/Curriculum/`. GitHub does not + automatically redirect project-site URLs when a repository is renamed. +4. If preserving the old URL is critical, configure a custom domain before the + rename. Otherwise, plan to update every public link. + +### B. Rename on GitHub + +1. Open `LucasPMM/Curriculum`. +2. Go to **Settings → General**. +3. Set **Repository name** to `LucasPMM.github.io`. +4. Confirm **Rename**. + +GitHub redirects repository web traffic and Git operations, but the Pages URL +is the important exception. + +### C. Update the local clone + +For the selected name: + +```bash +git remote set-url origin git@github.com:LucasPMM/LucasPMM.github.io.git +git remote -v +``` + +The deployment workflow supports the current `master` branch as well as `main`. +Optionally standardize the primary branch later: + +```bash +git branch -m master main +git push -u origin main +``` + +After the push, change the default branch to `main` under **Settings → +Branches**. Treat this as an independent migration and remove the old remote +branch only after `main`, CI, and Pages are verified. + +### D. Configure Pages + +1. Under **Settings → Pages → Build and deployment**, change **Source** from + the current legacy branch configuration to **GitHub Actions**. +2. Merge `feat/portfolio-modernization` into the default branch. The `CI` + workflow will validate the merge, and its successful push run will trigger + `.github/workflows/deploy.yml`. +3. Confirm that the deployment environment is named `github-pages` and that + its branch protection permits only the default branch. +4. In the repository **About** settings, use: + - description: + `Senior software engineer portfolio, selected work, and applied research.` + - website: `https://lucaspmm.github.io/` + - topics: `portfolio`, `preact`, `typescript`, `vite`, `github-pages`. +5. Set the GitHub profile Website and the relevant LinkedIn link to + `https://lucaspmm.github.io/`. + +### E. Verify + +1. Confirm the `CI` and `Deploy to GitHub Pages` workflows are green. +2. Open the public URL in a private browser window. +3. Verify direct loading, assets, anchor navigation, avatar fallback, external + links, locale, theme, and mobile layout. +4. Update LinkedIn and any other source still pointing to `/Curriculum/`. + +## 15. Definition of done + +- Repository governance and technical documentation are in English. +- Source code and comments are in English; visitor copy is localized. +- No residential address or personal phone number is shipped. +- Career and education dates match section 3, with expected graduation in 2027. +- ABILITYA and Pluritech descriptions are accurate and contain no invented + metrics. +- The showcase order is Jig Solver, Planner, Simplex, Pokémon Base, Greedy + K-means, and LZ78 Compression. +- Jig Solver links only to the public application and uses a real optimized + screenshot. +- The profile image comes from GitHub and has a local fallback. +- A first visit uses the browser locale or English fallback. +- A first visit uses the system color scheme or light fallback. +- Explicit locale/theme choices persist and invalid values recover safely. +- English, Brazilian Portuguese, and French catalogs have key parity. +- Both themes pass contrast and preserve the documented design system. +- The site works by keyboard, at 320 px, at 200 percent zoom, and with reduced + motion. +- Biome is the only web formatter/linter, and `pnpm check` passes. +- Commit messages are English Conventional Commits, hooks are not bypassed, and + coding agents do not commit without explicit authorization. +- GitHub Actions deploys only after all quality checks pass. +- README, canonical, Open Graph, and static discovery files use the final URL. +- After activation, the repository metadata, GitHub profile, and LinkedIn use + the final URL. + +## Technical references + +- [GitHub repository profile](https://github.com/LucasPMM) +- [Lucas Mariz LinkedIn profile](https://www.linkedin.com/in/lucas-mariz-4845b6164/) +- [Jig Solver public application](https://jigsolver.app/) +- [Renaming a repository — GitHub Docs](https://docs.github.com/en/repositories/creating-and-managing-repositories/renaming-a-repository) +- [Deploying a Vite site to GitHub Pages](https://vite.dev/guide/static-deploy.html#github-pages) +- [Custom workflows for GitHub Pages](https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages) +- [Preact getting started](https://preactjs.com/guide/v10/getting-started/) +- [Preact Vite prerendering](https://preactjs.com/blog/prerendering-preset-vite/) +- [i18next configuration and fallback](https://www.i18next.com/overview/configuration-options) diff --git a/e2e/portfolio.spec.ts b/e2e/portfolio.spec.ts new file mode 100644 index 0000000..d54a1ef --- /dev/null +++ b/e2e/portfolio.spec.ts @@ -0,0 +1,164 @@ +import AxeBuilder from '@axe-core/playwright' +import { expect, test } from '@playwright/test' + +const localeCases = [ + { + locale: 'en', + languageLabel: 'Language', + experienceHeading: 'Professional experience', + pageTitle: 'Lucas Mariz — Senior Software Engineer', + openGraphLocale: 'en_US', + }, + { + locale: 'pt-BR', + languageLabel: 'Language', + experienceHeading: 'Experiência profissional', + pageTitle: 'Lucas Mariz — Engenheiro de Software Sênior', + openGraphLocale: 'pt_BR', + }, + { + locale: 'fr', + languageLabel: 'Language', + experienceHeading: 'Expérience professionnelle', + pageTitle: 'Lucas Mariz — Ingénieur logiciel senior', + openGraphLocale: 'fr_FR', + }, +] as const + +test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + const resetMarker = 'portfolio.test-storage-reset' + if (window.sessionStorage.getItem(resetMarker)) return + window.localStorage.clear() + window.sessionStorage.setItem(resetMarker, 'true') + }) +}) + +test('renders without automated accessibility violations', async ({ page }) => { + await page.goto('/') + const results = await new AxeBuilder({ page }).analyze() + expect(results.violations).toEqual([]) +}) + +test('uses browser language and color-scheme defaults on the first visit', async ({ browser }) => { + const context = await browser.newContext({ locale: 'pt-BR', colorScheme: 'dark' }) + const page = await context.newPage() + await page.goto('/') + await expect(page.locator('html')).toHaveAttribute('lang', 'pt-BR') + await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark') + await expect(page.getByRole('heading', { name: 'Experiência profissional' })).toBeVisible() + await context.close() +}) + +localeCases.forEach(({ locale, languageLabel, experienceHeading, pageTitle, openGraphLocale }) => { + test(`synchronizes content and metadata for ${locale}`, async ({ page }) => { + await page.goto('/') + await page.getByLabel(languageLabel).selectOption(locale) + await expect(page.getByRole('heading', { name: experienceHeading })).toBeVisible() + await expect(page.locator('html')).toHaveAttribute('lang', locale) + await expect(page).toHaveTitle(pageTitle) + await expect(page.locator('meta[property="og:locale"]')).toHaveAttribute( + 'content', + openGraphLocale, + ) + }) +}) + +test('persists an explicit dark theme', async ({ page }) => { + await page.goto('/') + await page.getByLabel('Theme').selectOption('dark') + await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark') + await page.reload() + await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark') +}) + +test('falls back to the local avatar when GitHub is unavailable', async ({ page }) => { + await page.route('**/LucasPMM.png**', (route) => + route.fulfill({ status: 503, contentType: 'text/plain', body: 'Unavailable' }), + ) + await page.goto('/') + await expect(page.getByRole('img', { name: 'Lucas Mariz' })).toHaveAttribute( + 'src', + /avatar-fallback\.svg$/, + ) +}) + +test('loads the primary project evidence and exposes only public project links', async ({ + page, +}) => { + await page.goto('/') + const projectImage = page.locator('#projects .product-frame img') + await projectImage.scrollIntoViewIfNeeded() + await expect + .poll(() => + projectImage.evaluate((image) => image instanceof HTMLImageElement && image.naturalWidth > 0), + ) + .toBe(true) + + await expect(page.getByRole('link', { name: 'Open jigsolver.app' })).toHaveAttribute( + 'href', + 'https://jigsolver.app/', + ) + const plannerCard = page + .locator('article') + .filter({ has: page.getByRole('heading', { name: 'Planner', exact: true }) }) + await expect(plannerCard.getByRole('link')).toHaveCount(0) + + const publicRepositories = [ + 'https://github.com/LucasPMM/simplex', + 'https://github.com/LucasPMM/Pokemon-Base', + 'https://github.com/LucasPMM/greedy-kmeans', + 'https://github.com/LucasPMM/lz78-compression', + ] + for (const repository of publicRepositories) { + await expect(page.locator(`a[href="${repository}"]`)).toHaveAttribute('target', '_blank') + } +}) + +test('keeps the page free of horizontal overflow at representative widths', async ({ page }) => { + const widths = [320, 768, 1440] + await page.goto('/') + for (const width of widths) { + await page.setViewportSize({ width, height: 900 }) + const hasOverflow = await page.evaluate( + () => document.documentElement.scrollWidth > document.documentElement.clientWidth, + ) + expect(hasOverflow).toBe(false) + } +}) + +test('supports keyboard entry and reduced motion', async ({ page }) => { + await page.emulateMedia({ reducedMotion: 'reduce' }) + await page.goto('/') + await page.keyboard.press('Tab') + await expect(page.getByRole('link', { name: 'Skip to content' })).toBeFocused() + await page.keyboard.press('Enter') + await expect(page.locator('#main-content')).toBeInViewport() + await expect(page.locator('html')).toHaveCSS('scroll-behavior', 'auto') +}) + +test('remains usable with text enlarged to 200 percent', async ({ page }) => { + await page.setViewportSize({ width: 640, height: 900 }) + await page.goto('/') + await page.evaluate(() => { + document.documentElement.style.fontSize = '200%' + }) + const overflowingElements = await page.locator('body *').evaluateAll((elements) => + elements + .filter((element) => { + const bounds = element.getBoundingClientRect() + return bounds.right > document.documentElement.clientWidth || bounds.left < 0 + }) + .map((element) => ({ + bounds: { + left: Math.round(element.getBoundingClientRect().left), + right: Math.round(element.getBoundingClientRect().right), + }, + className: element.getAttribute('class') ?? '', + tagName: element.tagName, + text: element.textContent?.trim().slice(0, 40), + })), + ) + expect(overflowingElements).toEqual([]) + await expect(page.getByRole('heading', { level: 1 })).toBeVisible() +}) diff --git a/index.html b/index.html index 743bfb1..75deb2c 100644 --- a/index.html +++ b/index.html @@ -1,117 +1,51 @@ - - - - - - - Lucas Paulo Martins Mariz - - - - - -
-
-
-
-
-
- -
- -
-
-

SOBRE

-

Técnico em informática e em desenvolvimento de sistemas formado pelo Colégio Técnico da UFMG e estudante de Ciência da Computação na UFMG. Personalidade forte, interessado em novas tecnologias, aplicado em projetos envolvendo as mais diversas linguagens de programação e músico nos tempos livres.

-
-
-

CONTATO

-

Rua Radialista César Dos Santos - 151 - Belo Horizonte/Brasil

-

(31)97502-7868

-

lucaspaulomm@gmail.com

-

LucasPMM

-

Lucas Mariz

-
-
-

HABILIDADES

-
-
-
-

HTLM

-

CSS

-

JS

-

C

-
-
-

PYTHON

-

REACT

-

PHP

-

Git

-
-
-
-

LÍNGUAS

-

Portugês -

-
-
-
-

-

Inglês -

-
-
-
-

-
-
-
-
-
-
-
-
-
-

LUCAS MARIZ

-

Developer

-
-
-

EDUCAÇÃO

-

2015 - 2017 Coltec

-

Graduação concluída no Colégio Técnico da Universidade Federal de Minas Gerais relacionada ao técnico em informática e em desenvolvimento de sistemas.

-

2018 - Atual UFMG

-

Graduação em andamento no bacharelado de Ciência da Computação na Universidade Federal de Minas Gerais.

-
-
-

PROJETOS

-

Help.me

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis vel interdum metus. Maecenas et blandit lectus. Praesent eget mattis justo, at imperdiet magna. Quisque rutrum dolor non eros mattis, et euismod eros semper.

-

Keep.me

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis vel interdum metus. Maecenas et blandit lectus. Praesent eget mattis justo, at imperdiet magna. Quisque rutrum dolor non eros mattis, et euismod eros semper.

-

Project Absolut

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis vel interdum metus. Maecenas et blandit lectus. Praesent eget mattis justo, at imperdiet magna. Quisque rutrum dolor non eros mattis, et euismod eros semper.

-
-

HOBBIES

-
-
-

-

-
-
-

-

-
-
-

-

-
-
-
-
-
-
-
-
-
- - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + Lucas Mariz — Senior Software Engineer + + +
+ + + diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..ea6ff1b --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,9 @@ +pre-commit: + commands: + lint: + run: pnpm lint + +commit-msg: + commands: + commitlint: + run: pnpm exec commitlint --edit {1} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ebb7927 --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "lucas-mariz-portfolio", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "pnpm@10.33.4", + "engines": { + "node": ">=24 <25", + "pnpm": ">=10 <11" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "preinstall": "node scripts/require-pnpm.mjs", + "lint": "pnpm lint:biome && pnpm lint:conventions", + "lint:biome": "biome lint .", + "lint:conventions": "node scripts/check-code-conventions.mjs", + "format": "biome check --write .", + "format:check": "biome format .", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:e2e": "playwright test", + "verify:build": "node scripts/verify-static-build.mjs", + "check": "pnpm typecheck && pnpm lint && pnpm test && pnpm format:check && pnpm build && pnpm verify:build" + }, + "dependencies": { + "@fontsource/lora": "^5.2.6", + "@fontsource/public-sans": "^5.2.6", + "i18next": "^25.5.2", + "preact": "^10.27.2", + "preact-render-to-string": "^6.6.3" + }, + "devDependencies": { + "@axe-core/playwright": "^4.13.0", + "@biomejs/biome": "^2.2.4", + "@commitlint/cli": "^19.8.1", + "@commitlint/config-conventional": "^19.8.1", + "@playwright/test": "^1.55.0", + "@preact/preset-vite": "^2.10.2", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/preact": "^3.2.4", + "@types/babel__core": "^7.20.5", + "@types/node": "^24.3.1", + "jsdom": "^26.1.0", + "lefthook": "^1.12.3", + "typescript": "^5.9.2", + "vite": "^7.1.4", + "vitest": "^3.2.4" + } +} diff --git a/perfil.jpeg b/perfil.jpeg deleted file mode 100644 index e109f99..0000000 Binary files a/perfil.jpeg and /dev/null differ diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..b52afe3 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + use: { + baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://127.0.0.1:4173', + trace: 'on-first-retry', + }, + webServer: { + command: 'pnpm preview --host 127.0.0.1 --port 4173', + url: 'http://127.0.0.1:4173', + reuseExistingServer: true, + }, + projects: [ + { + name: 'desktop-chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'mobile-chromium', + use: { ...devices['Pixel 7'] }, + }, + ], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..0e80a41 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3716 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@fontsource/lora': + specifier: ^5.2.6 + version: 5.3.0 + '@fontsource/public-sans': + specifier: ^5.2.6 + version: 5.3.0 + i18next: + specifier: ^25.5.2 + version: 25.10.10(typescript@5.9.3) + preact: + specifier: ^10.27.2 + version: 10.29.8(preact-render-to-string@6.7.0) + preact-render-to-string: + specifier: ^6.6.3 + version: 6.7.0(preact@10.29.8) + devDependencies: + '@axe-core/playwright': + specifier: ^4.13.0 + version: 4.13.0(playwright-core@1.63.0) + '@biomejs/biome': + specifier: ^2.2.4 + version: 2.5.13 + '@commitlint/cli': + specifier: ^19.8.1 + version: 19.8.1(@types/node@24.13.4)(typescript@5.9.3) + '@commitlint/config-conventional': + specifier: ^19.8.1 + version: 19.8.1 + '@playwright/test': + specifier: ^1.55.0 + version: 1.63.0 + '@preact/preset-vite': + specifier: ^2.10.2 + version: 2.10.6(@babel/core@7.29.7)(preact@10.29.8)(rollup@4.63.1)(vite@7.3.6(@types/node@24.13.4)(jiti@2.6.1)) + '@testing-library/jest-dom': + specifier: 6.9.1 + version: 6.9.1 + '@testing-library/preact': + specifier: ^3.2.4 + version: 3.2.4(preact@10.29.8) + '@types/babel__core': + specifier: ^7.20.5 + version: 7.20.5 + '@types/node': + specifier: ^24.3.1 + version: 24.13.4 + jsdom: + specifier: ^26.1.0 + version: 26.1.0 + lefthook: + specifier: ^1.12.3 + version: 1.13.6 + typescript: + specifier: ^5.9.2 + version: 5.9.3 + vite: + specifier: ^7.1.4 + version: 7.3.6(@types/node@24.13.4)(jiti@2.6.1) + vitest: + specifier: ^3.2.4 + version: 3.2.7(@types/node@24.13.4)(jiti@2.6.1)(jsdom@26.1.0) + +packages: + + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@axe-core/playwright@4.13.0': + resolution: {integrity: sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==} + peerDependencies: + playwright-core: '>= 1.0.0' + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.29.7': + resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-development@7.29.7': + resolution: {integrity: sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx@7.29.7': + resolution: {integrity: sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@2.5.13': + resolution: {integrity: sha512-+SEC/mFk1a+5mvUANZgbZTaiZXs1nj4iMhL/PHiqDT5TPUEPFIlliEtkKKZB4N862yylHC3UI+/Sj2I0HJEqhA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.5.13': + resolution: {integrity: sha512-nYSuDJ6zgVqZUkAkJkvhXxsF2PYIrUk/g638K4voCXz7foI9f7b6C2/7+oAihsHQlivZNMUrm/y8ywlcHQtZOw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.5.13': + resolution: {integrity: sha512-KVy1ceEDuJ3AzFxjT9kkxbVy+UANw1pjEMUS6lvKfxjJ+fmkRvc7sQn1Xo6ETgseEI7wUQoV03KSga3XfE8YRQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.5.13': + resolution: {integrity: sha512-CH32xpep3dNS5EVJpAHlYchkBYznxyVZQrx0b6YYYlPL9u9ZeD2NtqiK/6s64+HFS2a7hGnryLlqqZAOh8ax5g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.5.13': + resolution: {integrity: sha512-VlNMtoxOqs0dUR6drxxHr18SNUvI7xxAuHZlH4s/dstYSf3g6RaqVgo8JRnhcOhKz9afWrvtJTboNG1JuYlIDQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.5.13': + resolution: {integrity: sha512-F3pmwl+VHoUuVJN/tbNLKeLt0SVK3EIyN1jXlMcNyQGYRQQTVqXF+GgkP0/CjGXFN87e/mv0sBfUnWqWza5PKQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.5.13': + resolution: {integrity: sha512-Fi6gIxbUaJ3ZCIXeG3ggIBPF76O2DjDN67WrPX/ODRv54GeXPm2gbEPC96Yb0yrJoiH0Eax1JLx1Pi0XbsNFZQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.5.13': + resolution: {integrity: sha512-+WD13qshXrr0Icv4BfsAdzm8Fs3TL+nZ59zEQxsYNd8lcgZJ0A5+OrlwsL1PipVCmWeRpxgIPD6DAcEd7smkCQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.5.13': + resolution: {integrity: sha512-VOofU/nW761XWzUeUNE8zzYNyPrxuMuNFMizL0qz7J75yeV8N7WFheUlk1/K6dOkaFpH9wJ+lDKlwjAbw0346w==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@commitlint/cli@19.8.1': + resolution: {integrity: sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA==} + engines: {node: '>=v18'} + hasBin: true + + '@commitlint/config-conventional@19.8.1': + resolution: {integrity: sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ==} + engines: {node: '>=v18'} + + '@commitlint/config-validator@19.8.1': + resolution: {integrity: sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ==} + engines: {node: '>=v18'} + + '@commitlint/ensure@19.8.1': + resolution: {integrity: sha512-mXDnlJdvDzSObafjYrOSvZBwkD01cqB4gbnnFuVyNpGUM5ijwU/r/6uqUmBXAAOKRfyEjpkGVZxaDsCVnHAgyw==} + engines: {node: '>=v18'} + + '@commitlint/execute-rule@19.8.1': + resolution: {integrity: sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA==} + engines: {node: '>=v18'} + + '@commitlint/format@19.8.1': + resolution: {integrity: sha512-kSJj34Rp10ItP+Eh9oCItiuN/HwGQMXBnIRk69jdOwEW9llW9FlyqcWYbHPSGofmjsqeoxa38UaEA5tsbm2JWw==} + engines: {node: '>=v18'} + + '@commitlint/is-ignored@19.8.1': + resolution: {integrity: sha512-AceOhEhekBUQ5dzrVhDDsbMaY5LqtN8s1mqSnT2Kz1ERvVZkNihrs3Sfk1Je/rxRNbXYFzKZSHaPsEJJDJV8dg==} + engines: {node: '>=v18'} + + '@commitlint/lint@19.8.1': + resolution: {integrity: sha512-52PFbsl+1EvMuokZXLRlOsdcLHf10isTPlWwoY1FQIidTsTvjKXVXYb7AvtpWkDzRO2ZsqIgPK7bI98x8LRUEw==} + engines: {node: '>=v18'} + + '@commitlint/load@19.8.1': + resolution: {integrity: sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A==} + engines: {node: '>=v18'} + + '@commitlint/message@19.8.1': + resolution: {integrity: sha512-+PMLQvjRXiU+Ae0Wc+p99EoGEutzSXFVwQfa3jRNUZLNW5odZAyseb92OSBTKCu+9gGZiJASt76Cj3dLTtcTdg==} + engines: {node: '>=v18'} + + '@commitlint/parse@19.8.1': + resolution: {integrity: sha512-mmAHYcMBmAgJDKWdkjIGq50X4yB0pSGpxyOODwYmoexxxiUCy5JJT99t1+PEMK7KtsCtzuWYIAXYAiKR+k+/Jw==} + engines: {node: '>=v18'} + + '@commitlint/read@19.8.1': + resolution: {integrity: sha512-03Jbjb1MqluaVXKHKRuGhcKWtSgh3Jizqy2lJCRbRrnWpcM06MYm8th59Xcns8EqBYvo0Xqb+2DoZFlga97uXQ==} + engines: {node: '>=v18'} + + '@commitlint/resolve-extends@19.8.1': + resolution: {integrity: sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg==} + engines: {node: '>=v18'} + + '@commitlint/rules@19.8.1': + resolution: {integrity: sha512-Hnlhd9DyvGiGwjfjfToMi1dsnw1EXKGJNLTcsuGORHz6SS9swRgkBsou33MQ2n51/boIDrbsg4tIBbRpEWK2kw==} + engines: {node: '>=v18'} + + '@commitlint/to-lines@19.8.1': + resolution: {integrity: sha512-98Mm5inzbWTKuZQr2aW4SReY6WUukdWXuZhrqf1QdKPZBCCsXuG87c+iP0bwtD6DBnmVVQjgp4whoHRVixyPBg==} + engines: {node: '>=v18'} + + '@commitlint/top-level@19.8.1': + resolution: {integrity: sha512-Ph8IN1IOHPSDhURCSXBz44+CIu+60duFwRsg6HqaISFHQHbmBtxVw4ZrFNIYUzEP7WwrNPxa2/5qJ//NK1FGcw==} + engines: {node: '>=v18'} + + '@commitlint/types@19.8.1': + resolution: {integrity: sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==} + engines: {node: '>=v18'} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@fontsource/lora@5.3.0': + resolution: {integrity: sha512-OK1g1z01rHVPqO5aw/FOIoDqc46KKYZdgL3NOLkxLWDk6QhDHhdr/h2BV1cke6gTvR1tzpDfdKyUkGsC+sQLeQ==} + + '@fontsource/public-sans@5.3.0': + resolution: {integrity: sha512-kjODI0S3zdv0mBYCIQ8TbBayaiqszpc2UbhJiO3bjIqVVXzcWfHSt2o3WBCLOY3juaGaQoy4MoWCcgmfI5hCuA==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@playwright/test@1.63.0': + resolution: {integrity: sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==} + engines: {node: '>=20'} + hasBin: true + + '@preact/preset-vite@2.10.6': + resolution: {integrity: sha512-ZZ5RIT2ZVXg8UxRA8VZpoT8CdpDG7RFIqOT4bELqNBp85+HKvQBbgmh3gsoW1UOQXx26TLe+ZRNKI7q281Kckw==} + peerDependencies: + '@babel/core': 7.x + vite: 2.x || 3.x || 4.x || 5.x || 6.x || 7.x || 8.x + + '@prefresh/babel-plugin@0.5.3': + resolution: {integrity: sha512-57LX2SHs4BX2s1IwCjNzTE2OJeEepRCNf1VTEpbNcUyHfMO68eeOWGDIt4ob9aYlW6PEWZ1SuwNikuoIXANDtQ==} + + '@prefresh/core@1.5.11': + resolution: {integrity: sha512-Ml00PP8jeHaADxfQqxUs4+M5JS+T5J26UnvPVfsrmlpGw2I3e9XtM0DMeuzkmvg+Tn7VdHSMpLBrDPG0iHoDqQ==} + peerDependencies: + preact: ^10.0.0 || ^11.0.0-0 + + '@prefresh/utils@1.2.1': + resolution: {integrity: sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==} + + '@prefresh/vite@2.4.12': + resolution: {integrity: sha512-FY1fzXpUjiuosznMV0YM7XAOPZjB5FIdWS0W24+XnlxYkt9hNAwwsiKYn+cuTEoMtD/ZVazS5QVssBr9YhpCQA==} + peerDependencies: + preact: ^10.4.0 || ^11.0.0-0 + vite: '>=2.0.0' + + '@rollup/pluginutils@4.2.1': + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.63.1': + resolution: {integrity: sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.1': + resolution: {integrity: sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.1': + resolution: {integrity: sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.1': + resolution: {integrity: sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.1': + resolution: {integrity: sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.1': + resolution: {integrity: sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.1': + resolution: {integrity: sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.63.1': + resolution: {integrity: sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.63.1': + resolution: {integrity: sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.63.1': + resolution: {integrity: sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.63.1': + resolution: {integrity: sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.63.1': + resolution: {integrity: sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.63.1': + resolution: {integrity: sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.63.1': + resolution: {integrity: sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.63.1': + resolution: {integrity: sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.63.1': + resolution: {integrity: sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.63.1': + resolution: {integrity: sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.63.1': + resolution: {integrity: sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.63.1': + resolution: {integrity: sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.63.1': + resolution: {integrity: sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.1': + resolution: {integrity: sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.1': + resolution: {integrity: sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.1': + resolution: {integrity: sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.1': + resolution: {integrity: sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.1': + resolution: {integrity: sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==} + cpu: [x64] + os: [win32] + + '@testing-library/dom@8.20.1': + resolution: {integrity: sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==} + engines: {node: '>=12'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/preact@3.2.4': + resolution: {integrity: sha512-F+kJ243LP6VmEK1M809unzTE/ijg+bsMNuiRN0JEDIJBELKKDNhdgC/WrUSZ7klwJvtlO3wQZ9ix+jhObG07Fg==} + engines: {node: '>= 12'} + peerDependencies: + preact: '>=10 || ^10.0.0-alpha.0 || ^10.0.0-beta.0' + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/conventional-commits-parser@5.0.2': + resolution: {integrity: sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/node@24.13.4': + resolution: {integrity: sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==} + + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.13.0: + resolution: {integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==} + engines: {node: '>=4'} + + babel-plugin-transform-hook-names@1.0.2: + resolution: {integrity: sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==} + peerDependencies: + '@babel/core': ^7.12.10 + + baseline-browser-mapping@2.11.22: + resolution: {integrity: sha512-pWc4w51fBFd7mav43/zKRC+RI6f4yfzQoVlfvE8dECePyfkn1bzLp01Fj0QACcyCZyFhiEMyD2qScfKRWgWibA==} + engines: {node: '>=6.0.0'} + hasBin: true + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + browserslist@4.28.9: + resolution: {integrity: sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + + conventional-changelog-angular@7.0.0: + resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} + engines: {node: '>=16'} + + conventional-changelog-conventionalcommits@7.0.2: + resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} + engines: {node: '>=16'} + + conventional-commits-parser@5.0.0: + resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} + engines: {node: '>=16'} + hasBin: true + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cosmiconfig-typescript-loader@6.3.0: + resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} + engines: {node: '>=v18'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=9' + typescript: '>=5' + + cosmiconfig@9.0.2: + resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + dargs@8.1.0: + resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} + engines: {node: '>=12'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.427: + resolution: {integrity: sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.7: + resolution: {integrity: sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + git-raw-commits@4.0.0: + resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} + engines: {node: '>=16'} + deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. + hasBin: true + + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + i18next@25.10.10: + resolution: {integrity: sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==} + peerDependencies: + typescript: ^5 || ^6 + peerDependenciesMeta: + typescript: + optional: true + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-text-path@2.0.0: + resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} + engines: {node: '>=8'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + lefthook-darwin-arm64@1.13.6: + resolution: {integrity: sha512-m6Lb77VGc84/Qo21Lhq576pEvcgFCnvloEiP02HbAHcIXD0RTLy9u2yAInrixqZeaz13HYtdDaI7OBYAAdVt8A==} + cpu: [arm64] + os: [darwin] + + lefthook-darwin-x64@1.13.6: + resolution: {integrity: sha512-CoRpdzanu9RK3oXR1vbEJA5LN7iB+c7hP+sONeQJzoOXuq4PNKVtEaN84Gl1BrVtCNLHWFAvCQaZPPiiXSy8qg==} + cpu: [x64] + os: [darwin] + + lefthook-freebsd-arm64@1.13.6: + resolution: {integrity: sha512-X4A7yfvAJ68CoHTqP+XvQzdKbyd935sYy0bQT6Ajz7FL1g7hFiro8dqHSdPdkwei9hs8hXeV7feyTXbYmfjKQQ==} + cpu: [arm64] + os: [freebsd] + + lefthook-freebsd-x64@1.13.6: + resolution: {integrity: sha512-ai2m+Sj2kGdY46USfBrCqLKe9GYhzeq01nuyDYCrdGISePeZ6udOlD1k3lQKJGQCHb0bRz4St0r5nKDSh1x/2A==} + cpu: [x64] + os: [freebsd] + + lefthook-linux-arm64@1.13.6: + resolution: {integrity: sha512-cbo4Wtdq81GTABvikLORJsAWPKAJXE8Q5RXsICFUVznh5PHigS9dFW/4NXywo0+jfFPCT6SYds2zz4tCx6DA0Q==} + cpu: [arm64] + os: [linux] + + lefthook-linux-x64@1.13.6: + resolution: {integrity: sha512-uJl9vjCIIBTBvMZkemxCE+3zrZHlRO7Oc+nZJ+o9Oea3fu+W82jwX7a7clw8jqNfaeBS+8+ZEQgiMHWCloTsGw==} + cpu: [x64] + os: [linux] + + lefthook-openbsd-arm64@1.13.6: + resolution: {integrity: sha512-7r153dxrNRQ9ytRs2PmGKKkYdvZYFPre7My7XToSTiRu5jNCq++++eAKVkoyWPduk97dGIA+YWiEr5Noe0TK2A==} + cpu: [arm64] + os: [openbsd] + + lefthook-openbsd-x64@1.13.6: + resolution: {integrity: sha512-Z+UhLlcg1xrXOidK3aLLpgH7KrwNyWYE3yb7ITYnzJSEV8qXnePtVu8lvMBHs/myzemjBzeIr/U/+ipjclR06g==} + cpu: [x64] + os: [openbsd] + + lefthook-windows-arm64@1.13.6: + resolution: {integrity: sha512-Uxef6qoDxCmUNQwk8eBvddYJKSBFglfwAY9Y9+NnnmiHpWTjjYiObE9gT2mvGVpEgZRJVAatBXc+Ha5oDD/OgQ==} + cpu: [arm64] + os: [win32] + + lefthook-windows-x64@1.13.6: + resolution: {integrity: sha512-mOZoM3FQh3o08M8PQ/b3IYuL5oo36D9ehczIw1dAgp1Ly+Tr4fJ96A+4SEJrQuYeRD4mex9bR7Ps56I73sBSZA==} + cpu: [x64] + os: [win32] + + lefthook@1.13.6: + resolution: {integrity: sha512-ojj4/4IJ29Xn4drd5emqVgilegAPN3Kf0FQM2p/9+lwSTpU+SZ1v4Ig++NF+9MOa99UKY8bElmVrLhnUUNFh5g==} + hasBin: true + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-html-parser@6.1.13: + resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} + + node-releases@2.0.55: + resolution: {integrity: sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==} + engines: {node: '>=18'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nwsapi@2.2.27: + resolution: {integrity: sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw==} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + playwright-core@1.63.0: + resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.63.0: + resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==} + engines: {node: '>=20'} + hasBin: true + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + preact-render-to-string@6.7.0: + resolution: {integrity: sha512-Z4WR8fmLMRpdYqJ9i7vrlXSsSrxVJydwrkEXHapexfARbWfGb7vGcnvNQnIzN0cXciMVOlz/XLoiMCi9gUsy9Q==} + peerDependencies: + preact: '>=10 || >= 11.0.0-0' + + preact@10.29.8: + resolution: {integrity: sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==} + peerDependencies: + preact-render-to-string: '>=5' + peerDependenciesMeta: + preact-render-to-string: + optional: true + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + rollup@4.63.1: + resolution: {integrity: sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + simple-code-frame@1.3.0: + resolution: {integrity: sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stack-trace@1.0.0: + resolution: {integrity: sha512-H6D7134xi6qONvh7ZHKgviXf+rd3vhGBSvebPZCaUkd8zvQ+7PtDw6CljPTe4cXWNf2IKZGNqw6VJXSb9IgBpA==} + engines: {node: '>=20.0.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + text-extensions@2.4.0: + resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} + engines: {node: '>=8'} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.6: + resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + + update-browserslist-db@1.3.3: + resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite-prerender-plugin@0.5.13: + resolution: {integrity: sha512-IKSpYkzDBsKAxa05naRbj7GvNVMSdww/Z/E89oO3xndz+gWnOBOKOAbEXv7qDhktY/j3vHgJmoV1pPzqU2tx9g==} + peerDependencies: + vite: 5.x || 6.x || 7.x || 8.x + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + + zimmerframe@1.1.5: + resolution: {integrity: sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==} + +snapshots: + + '@adobe/css-tools@4.5.0': {} + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@axe-core/playwright@4.13.0(playwright-core@1.63.0)': + dependencies: + axe-core: 4.13.0 + playwright-core: 1.63.0 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.29.7': + dependencies: + '@babel/types': 7.29.8 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.9 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-development@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@biomejs/biome@2.5.13': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.5.13 + '@biomejs/cli-darwin-x64': 2.5.13 + '@biomejs/cli-linux-arm64': 2.5.13 + '@biomejs/cli-linux-arm64-musl': 2.5.13 + '@biomejs/cli-linux-x64': 2.5.13 + '@biomejs/cli-linux-x64-musl': 2.5.13 + '@biomejs/cli-win32-arm64': 2.5.13 + '@biomejs/cli-win32-x64': 2.5.13 + + '@biomejs/cli-darwin-arm64@2.5.13': + optional: true + + '@biomejs/cli-darwin-x64@2.5.13': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.5.13': + optional: true + + '@biomejs/cli-linux-arm64@2.5.13': + optional: true + + '@biomejs/cli-linux-x64-musl@2.5.13': + optional: true + + '@biomejs/cli-linux-x64@2.5.13': + optional: true + + '@biomejs/cli-win32-arm64@2.5.13': + optional: true + + '@biomejs/cli-win32-x64@2.5.13': + optional: true + + '@commitlint/cli@19.8.1(@types/node@24.13.4)(typescript@5.9.3)': + dependencies: + '@commitlint/format': 19.8.1 + '@commitlint/lint': 19.8.1 + '@commitlint/load': 19.8.1(@types/node@24.13.4)(typescript@5.9.3) + '@commitlint/read': 19.8.1 + '@commitlint/types': 19.8.1 + tinyexec: 1.3.1 + yargs: 17.7.3 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/config-conventional@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + conventional-changelog-conventionalcommits: 7.0.2 + + '@commitlint/config-validator@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + ajv: 8.20.0 + + '@commitlint/ensure@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.upperfirst: 4.3.1 + + '@commitlint/execute-rule@19.8.1': {} + + '@commitlint/format@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + chalk: 5.6.2 + + '@commitlint/is-ignored@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + semver: 7.8.5 + + '@commitlint/lint@19.8.1': + dependencies: + '@commitlint/is-ignored': 19.8.1 + '@commitlint/parse': 19.8.1 + '@commitlint/rules': 19.8.1 + '@commitlint/types': 19.8.1 + + '@commitlint/load@19.8.1(@types/node@24.13.4)(typescript@5.9.3)': + dependencies: + '@commitlint/config-validator': 19.8.1 + '@commitlint/execute-rule': 19.8.1 + '@commitlint/resolve-extends': 19.8.1 + '@commitlint/types': 19.8.1 + chalk: 5.6.2 + cosmiconfig: 9.0.2(typescript@5.9.3) + cosmiconfig-typescript-loader: 6.3.0(@types/node@24.13.4)(cosmiconfig@9.0.2(typescript@5.9.3))(typescript@5.9.3) + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + lodash.uniq: 4.5.0 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/message@19.8.1': {} + + '@commitlint/parse@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + conventional-changelog-angular: 7.0.0 + conventional-commits-parser: 5.0.0 + + '@commitlint/read@19.8.1': + dependencies: + '@commitlint/top-level': 19.8.1 + '@commitlint/types': 19.8.1 + git-raw-commits: 4.0.0 + minimist: 1.2.8 + tinyexec: 1.3.1 + + '@commitlint/resolve-extends@19.8.1': + dependencies: + '@commitlint/config-validator': 19.8.1 + '@commitlint/types': 19.8.1 + global-directory: 4.0.1 + import-meta-resolve: 4.2.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + + '@commitlint/rules@19.8.1': + dependencies: + '@commitlint/ensure': 19.8.1 + '@commitlint/message': 19.8.1 + '@commitlint/to-lines': 19.8.1 + '@commitlint/types': 19.8.1 + + '@commitlint/to-lines@19.8.1': {} + + '@commitlint/top-level@19.8.1': + dependencies: + find-up: 7.0.0 + + '@commitlint/types@19.8.1': + dependencies: + '@types/conventional-commits-parser': 5.0.2 + chalk: 5.6.2 + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@fontsource/lora@5.3.0': {} + + '@fontsource/public-sans@5.3.0': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@playwright/test@1.63.0': + dependencies: + playwright: 1.63.0 + + '@preact/preset-vite@2.10.6(@babel/core@7.29.7)(preact@10.29.8)(rollup@4.63.1)(vite@7.3.6(@types/node@24.13.4)(jiti@2.6.1))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-development': 7.29.7(@babel/core@7.29.7) + '@prefresh/vite': 2.4.12(preact@10.29.8)(vite@7.3.6(@types/node@24.13.4)(jiti@2.6.1)) + '@rollup/pluginutils': 5.4.0(rollup@4.63.1) + babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.7) + debug: 4.4.3 + magic-string: 0.30.21 + picocolors: 1.1.1 + vite: 7.3.6(@types/node@24.13.4)(jiti@2.6.1) + vite-prerender-plugin: 0.5.13(vite@7.3.6(@types/node@24.13.4)(jiti@2.6.1)) + zimmerframe: 1.1.5 + transitivePeerDependencies: + - preact + - rollup + - supports-color + + '@prefresh/babel-plugin@0.5.3': {} + + '@prefresh/core@1.5.11(preact@10.29.8)': + dependencies: + preact: 10.29.8(preact-render-to-string@6.7.0) + + '@prefresh/utils@1.2.1': {} + + '@prefresh/vite@2.4.12(preact@10.29.8)(vite@7.3.6(@types/node@24.13.4)(jiti@2.6.1))': + dependencies: + '@babel/core': 7.29.7 + '@prefresh/babel-plugin': 0.5.3 + '@prefresh/core': 1.5.11(preact@10.29.8) + '@prefresh/utils': 1.2.1 + '@rollup/pluginutils': 4.2.1 + preact: 10.29.8(preact-render-to-string@6.7.0) + vite: 7.3.6(@types/node@24.13.4)(jiti@2.6.1) + transitivePeerDependencies: + - supports-color + + '@rollup/pluginutils@4.2.1': + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.2 + + '@rollup/pluginutils@5.4.0(rollup@4.63.1)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.7 + optionalDependencies: + rollup: 4.63.1 + + '@rollup/rollup-android-arm-eabi@4.63.1': + optional: true + + '@rollup/rollup-android-arm64@4.63.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.1': + optional: true + + '@rollup/rollup-darwin-x64@4.63.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.1': + optional: true + + '@testing-library/dom@8.20.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.1.3 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.5.0 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/preact@3.2.4(preact@10.29.8)': + dependencies: + '@testing-library/dom': 8.20.1 + preact: 10.29.8(preact-render-to-string@6.7.0) + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/conventional-commits-parser@5.0.2': + dependencies: + '@types/node': 24.13.4 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/node@24.13.4': + dependencies: + undici-types: 7.18.2 + + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.4)(jiti@2.6.1))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@24.13.4)(jiti@2.6.1) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.6 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + JSONStream@1.3.5: + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + + agent-base@7.1.4: {} + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.7 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + argparse@2.0.1: {} + + aria-query@5.1.3: + dependencies: + deep-equal: 2.2.3 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-ify@1.0.0: {} + + assertion-error@2.0.1: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.13.0: {} + + babel-plugin-transform-hook-names@1.0.2(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + + baseline-browser-mapping@2.11.22: {} + + boolbase@1.0.0: {} + + browserslist@4.28.9: + dependencies: + baseline-browser-mapping: 2.11.22 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.427 + node-releases: 2.0.55 + update-browserslist-db: 1.3.3(browserslist@4.28.9) + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001810: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + check-error@2.1.3: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + compare-func@2.0.0: + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + + conventional-changelog-angular@7.0.0: + dependencies: + compare-func: 2.0.0 + + conventional-changelog-conventionalcommits@7.0.2: + dependencies: + compare-func: 2.0.0 + + conventional-commits-parser@5.0.0: + dependencies: + JSONStream: 1.3.5 + is-text-path: 2.0.0 + meow: 12.1.1 + split2: 4.2.0 + + convert-source-map@2.0.0: {} + + cosmiconfig-typescript-loader@6.3.0(@types/node@24.13.4)(cosmiconfig@9.0.2(typescript@5.9.3))(typescript@5.9.3): + dependencies: + '@types/node': 24.13.4 + cosmiconfig: 9.0.2(typescript@5.9.3) + jiti: 2.6.1 + typescript: 5.9.3 + + cosmiconfig@9.0.2(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.3.2 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-what@6.2.2: {} + + css.escape@1.5.1: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + dargs@8.1.0: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-eql@5.0.2: {} + + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.1 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.22 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.427: {} + + emoji-regex@8.0.0: {} + + entities@4.5.0: {} + + entities@6.0.1: {} + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.9 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escalade@3.2.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.7: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + find-up@7.0.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + functions-have-names@1.2.3: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + git-raw-commits@4.0.0: + dependencies: + dargs: 8.1.0 + meow: 12.1.1 + split2: 4.2.0 + + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + + gopd@1.2.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + i18next@25.10.10(typescript@5.9.3): + dependencies: + '@babel/runtime': 7.29.7 + optionalDependencies: + typescript: 5.9.3 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-meta-resolve@4.2.0: {} + + indent-string@4.0.0: {} + + ini@4.1.1: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-fullwidth-code-point@3.0.0: {} + + is-map@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-obj@2.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-text-path@2.0.0: + dependencies: + text-extensions: 2.4.0 + + is-weakmap@2.0.2: {} + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.27 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.21.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + jsonparse@1.3.1: {} + + kolorist@1.8.0: {} + + lefthook-darwin-arm64@1.13.6: + optional: true + + lefthook-darwin-x64@1.13.6: + optional: true + + lefthook-freebsd-arm64@1.13.6: + optional: true + + lefthook-freebsd-x64@1.13.6: + optional: true + + lefthook-linux-arm64@1.13.6: + optional: true + + lefthook-linux-x64@1.13.6: + optional: true + + lefthook-openbsd-arm64@1.13.6: + optional: true + + lefthook-openbsd-x64@1.13.6: + optional: true + + lefthook-windows-arm64@1.13.6: + optional: true + + lefthook-windows-x64@1.13.6: + optional: true + + lefthook@1.13.6: + optionalDependencies: + lefthook-darwin-arm64: 1.13.6 + lefthook-darwin-x64: 1.13.6 + lefthook-freebsd-arm64: 1.13.6 + lefthook-freebsd-x64: 1.13.6 + lefthook-linux-arm64: 1.13.6 + lefthook-linux-x64: 1.13.6 + lefthook-openbsd-arm64: 1.13.6 + lefthook-openbsd-x64: 1.13.6 + lefthook-windows-arm64: 1.13.6 + lefthook-windows-x64: 1.13.6 + + lines-and-columns@1.2.4: {} + + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash.camelcase@4.3.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.kebabcase@4.1.1: {} + + lodash.merge@4.6.2: {} + + lodash.mergewith@4.6.2: {} + + lodash.snakecase@4.1.1: {} + + lodash.startcase@4.4.0: {} + + lodash.uniq@4.5.0: {} + + lodash.upperfirst@4.3.1: {} + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + math-intrinsics@1.1.0: {} + + meow@12.1.1: {} + + min-indent@1.0.1: {} + + minimist@1.2.8: {} + + ms@2.1.3: {} + + nanoid@3.3.19: {} + + node-html-parser@6.1.13: + dependencies: + css-select: 5.2.2 + he: 1.2.0 + + node-releases@2.0.55: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nwsapi@2.2.27: {} + + object-inspect@1.13.4: {} + + object-is@1.1.6: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.2 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.7 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-exists@5.0.0: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.7: {} + + playwright-core@1.63.0: {} + + playwright@1.63.0: + dependencies: + playwright-core: 1.63.0 + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + preact-render-to-string@6.7.0(preact@10.29.8): + dependencies: + preact: 10.29.8(preact-render-to-string@6.7.0) + + preact@10.29.8(preact-render-to-string@6.7.0): + optionalDependencies: + preact-render-to-string: 6.7.0(preact@10.29.8) + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + punycode@2.3.1: {} + + react-is@17.0.2: {} + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + rollup@4.63.1: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.1 + '@rollup/rollup-android-arm64': 4.63.1 + '@rollup/rollup-darwin-arm64': 4.63.1 + '@rollup/rollup-darwin-x64': 4.63.1 + '@rollup/rollup-freebsd-arm64': 4.63.1 + '@rollup/rollup-freebsd-x64': 4.63.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.1 + '@rollup/rollup-linux-arm-musleabihf': 4.63.1 + '@rollup/rollup-linux-arm64-gnu': 4.63.1 + '@rollup/rollup-linux-arm64-musl': 4.63.1 + '@rollup/rollup-linux-loong64-gnu': 4.63.1 + '@rollup/rollup-linux-loong64-musl': 4.63.1 + '@rollup/rollup-linux-ppc64-gnu': 4.63.1 + '@rollup/rollup-linux-ppc64-musl': 4.63.1 + '@rollup/rollup-linux-riscv64-gnu': 4.63.1 + '@rollup/rollup-linux-riscv64-musl': 4.63.1 + '@rollup/rollup-linux-s390x-gnu': 4.63.1 + '@rollup/rollup-linux-x64-gnu': 4.63.1 + '@rollup/rollup-linux-x64-musl': 4.63.1 + '@rollup/rollup-openbsd-x64': 4.63.1 + '@rollup/rollup-openharmony-arm64': 4.63.1 + '@rollup/rollup-win32-arm64-msvc': 4.63.1 + '@rollup/rollup-win32-ia32-msvc': 4.63.1 + '@rollup/rollup-win32-x64-gnu': 4.63.1 + '@rollup/rollup-win32-x64-msvc': 4.63.1 + fsevents: 2.3.3 + + rrweb-cssom@0.8.0: {} + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + semver@6.3.1: {} + + semver@7.8.5: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + simple-code-frame@1.3.0: + dependencies: + kolorist: 1.8.0 + + source-map-js@1.2.1: {} + + source-map@0.7.6: {} + + split2@4.2.0: {} + + stack-trace@1.0.0: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + symbol-tree@3.2.4: {} + + text-extensions@2.4.0: {} + + through@2.3.8: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.6: {} + + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + typescript@5.9.3: {} + + undici-types@7.18.2: {} + + unicorn-magic@0.1.0: {} + + update-browserslist-db@1.3.3(browserslist@4.28.9): + dependencies: + browserslist: 4.28.9 + escalade: 3.2.0 + picocolors: 1.1.1 + + vite-node@3.2.4(@types/node@24.13.4)(jiti@2.6.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@24.13.4)(jiti@2.6.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite-prerender-plugin@0.5.13(vite@7.3.6(@types/node@24.13.4)(jiti@2.6.1)): + dependencies: + kolorist: 1.8.0 + magic-string: 0.30.21 + node-html-parser: 6.1.13 + simple-code-frame: 1.3.0 + source-map: 0.7.6 + stack-trace: 1.0.0 + vite: 7.3.6(@types/node@24.13.4)(jiti@2.6.1) + + vite@7.3.6(@types/node@24.13.4)(jiti@2.6.1): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.28 + rollup: 4.63.1 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.4 + fsevents: 2.3.3 + jiti: 2.6.1 + + vitest@3.2.7(@types/node@24.13.4)(jiti@2.6.1)(jsdom@26.1.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.4)(jiti@2.6.1)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@24.13.4)(jiti@2.6.1) + vite-node: 3.2.4(@types/node@24.13.4)(jiti@2.6.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.4 + jsdom: 26.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + ws@8.21.3: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@1.2.2: {} + + zimmerframe@1.1.5: {} diff --git a/public/avatar-fallback.svg b/public/avatar-fallback.svg new file mode 100644 index 0000000..f75aff3 --- /dev/null +++ b/public/avatar-fallback.svg @@ -0,0 +1,13 @@ + + Lucas Mariz + + LM + diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..7110c61 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/projects/jig-solver/solver-workspace.webp b/public/projects/jig-solver/solver-workspace.webp new file mode 100644 index 0000000..5511fff Binary files /dev/null and b/public/projects/jig-solver/solver-workspace.webp differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..23e2745 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://lucaspmm.github.io/sitemap.xml diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..2940a7f --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,8 @@ + + + + https://lucaspmm.github.io/ + monthly + 1.0 + + diff --git a/scripts/check-code-conventions.mjs b/scripts/check-code-conventions.mjs new file mode 100644 index 0000000..72db4ea --- /dev/null +++ b/scripts/check-code-conventions.mjs @@ -0,0 +1,64 @@ +import { readdirSync, readFileSync } from 'node:fs' +import { extname, join, relative } from 'node:path' +import ts from 'typescript' + +const rootDirectory = process.cwd() +const checkedDirectories = ['src', 'scripts', 'e2e'] +const checkedExtensions = new Set(['.js', '.jsx', '.mjs', '.ts', '.tsx']) + +const listSourceFiles = (directory) => + readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const entryPath = join(directory, entry.name) + if (entry.isDirectory()) return listSourceFiles(entryPath) + if (checkedExtensions.has(extname(entry.name))) return [entryPath] + return [] + }) + +const formatPosition = (sourceFile, node) => { + const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)) + return `${relative(rootDirectory, sourceFile.fileName)}:${position.line + 1}:${position.character + 1}` +} + +const findViolations = (filePath) => { + const sourceText = readFileSync(filePath, 'utf8') + const scriptKind = filePath.endsWith('x') ? ts.ScriptKind.TSX : ts.ScriptKind.TS + const sourceFile = ts.createSourceFile( + filePath, + sourceText, + ts.ScriptTarget.Latest, + true, + scriptKind, + ) + const violations = [] + + const inspect = (node) => { + if (ts.isVariableDeclarationList(node) && (node.flags & ts.NodeFlags.Let) !== 0) { + violations.push( + `${formatPosition(sourceFile, node)} Avoid let; use const and immutable values.`, + ) + } + if (ts.isIfStatement(node) && node.elseStatement) { + violations.push( + `${formatPosition(sourceFile, node.elseStatement)} Avoid else; use a guard clause.`, + ) + } + if (ts.isSwitchStatement(node)) { + violations.push(`${formatPosition(sourceFile, node)} Avoid switch; use a lookup map.`) + } + ts.forEachChild(node, inspect) + } + + inspect(sourceFile) + return violations +} + +const sourceFiles = checkedDirectories.flatMap((directory) => + listSourceFiles(join(rootDirectory, directory)), +) +const violations = sourceFiles.flatMap(findViolations) + +if (violations.length > 0) { + throw new Error(`Code convention violations:\n${violations.join('\n')}`) +} + +console.log(`Code conventions passed for ${sourceFiles.length} files.`) diff --git a/scripts/require-pnpm.mjs b/scripts/require-pnpm.mjs new file mode 100644 index 0000000..47e2a7f --- /dev/null +++ b/scripts/require-pnpm.mjs @@ -0,0 +1,5 @@ +const userAgent = process.env.npm_config_user_agent ?? '' + +if (!userAgent.startsWith('pnpm/')) { + throw new Error('This project uses pnpm. Run pnpm install instead.') +} diff --git a/scripts/verify-static-build.mjs b/scripts/verify-static-build.mjs new file mode 100644 index 0000000..2a52dca --- /dev/null +++ b/scripts/verify-static-build.mjs @@ -0,0 +1,38 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' + +const distributionDirectory = join(process.cwd(), 'dist') +const indexHtml = readFileSync(join(distributionDirectory, 'index.html'), 'utf8') +const robots = readFileSync(join(distributionDirectory, 'robots.txt'), 'utf8') +const sitemap = readFileSync(join(distributionDirectory, 'sitemap.xml'), 'utf8') + +const requiredIndexFragments = [ + 'id="main-content"', + 'Lucas Mariz', + 'Cagliari Calcio', + 'Role Vectors', + 'Planner', + 'rel="canonical"', + 'href="/favicon.svg"', +] +const missingIndexFragments = requiredIndexFragments.filter( + (fragment) => !indexHtml.includes(fragment), +) + +if (missingIndexFragments.length > 0) { + throw new Error(`The prerendered page is missing: ${missingIndexFragments.join(', ')}`) +} + +if (!robots.includes('Sitemap: https://lucaspmm.github.io/sitemap.xml')) { + throw new Error('robots.txt does not reference the canonical sitemap.') +} + +if (!sitemap.includes('https://lucaspmm.github.io/')) { + throw new Error('sitemap.xml does not contain the canonical portfolio URL.') +} + +if (indexHtml.includes('/Curriculum/')) { + throw new Error('The production build still contains the legacy project-site base path.') +} + +console.log('Static build verification passed.') diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..2ac25f8 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,11 @@ +import { PortfolioPage } from '@/components/PortfolioPage' +import { I18nProvider } from '@/lib/i18n' +import { ThemeProvider } from '@/lib/theme' + +export const App = () => ( + + + + + +) diff --git a/src/components/PortfolioPage/PortfolioPage.tsx b/src/components/PortfolioPage/PortfolioPage.tsx new file mode 100644 index 0000000..600c1fd --- /dev/null +++ b/src/components/PortfolioPage/PortfolioPage.tsx @@ -0,0 +1,38 @@ +import { useI18n } from '@/lib/i18n' +import { AboutSection } from './components/AboutSection' +import { AppHeader } from './components/AppHeader' +import { CapabilitiesSection } from './components/CapabilitiesSection' +import { ContactSection } from './components/ContactSection' +import { EducationSection } from './components/EducationSection' +import { ExperienceSection } from './components/ExperienceSection' +import { HeroSection } from './components/HeroSection' +import { ProjectsSection } from './components/ProjectsSection' +import { SelectedWorkSection } from './components/SelectedWorkSection' + +export const PortfolioPage = () => { + const { t } = useI18n() + return ( + <> + + +
+ + + + + + + + +
+ + + ) +} diff --git a/src/components/PortfolioPage/components/AboutSection/AboutSection.tsx b/src/components/PortfolioPage/components/AboutSection/AboutSection.tsx new file mode 100644 index 0000000..8af952b --- /dev/null +++ b/src/components/PortfolioPage/components/AboutSection/AboutSection.tsx @@ -0,0 +1,12 @@ +import { SectionHeading } from '@/components/ui/SectionHeading' +import { useI18n } from '@/lib/i18n' + +export const AboutSection = () => { + const { t } = useI18n() + return ( +
+ +

{t('about.body')}

+
+ ) +} diff --git a/src/components/PortfolioPage/components/AboutSection/index.ts b/src/components/PortfolioPage/components/AboutSection/index.ts new file mode 100644 index 0000000..9600a61 --- /dev/null +++ b/src/components/PortfolioPage/components/AboutSection/index.ts @@ -0,0 +1 @@ +export { AboutSection } from './AboutSection' diff --git a/src/components/PortfolioPage/components/AppHeader/AppHeader.tsx b/src/components/PortfolioPage/components/AppHeader/AppHeader.tsx new file mode 100644 index 0000000..ea2309b --- /dev/null +++ b/src/components/PortfolioPage/components/AppHeader/AppHeader.tsx @@ -0,0 +1,57 @@ +import { LanguageSwitcher } from '@/components/ui/LanguageSwitcher' +import { ThemeSwitcher } from '@/components/ui/ThemeSwitcher' +import type { TranslationKey } from '@/lib/i18n' +import { useI18n } from '@/lib/i18n' + +const navigationItems: readonly { href: string; labelKey: TranslationKey }[] = [ + { href: '#about', labelKey: 'nav.about' }, + { href: '#experience', labelKey: 'nav.experience' }, + { href: '#selected-work', labelKey: 'nav.work' }, + { href: '#education', labelKey: 'nav.education' }, + { href: '#projects', labelKey: 'nav.projects' }, + { href: '#contact', labelKey: 'nav.contact' }, +] + +const NavigationLinks = () => { + const { t } = useI18n() + return ( + <> + {navigationItems.map((item) => ( + + {t(item.labelKey)} + + ))} + + ) +} + +export const AppHeader = () => { + const { t } = useI18n() + return ( +
+
+ + + Lucas Mariz + + +
+ + +
+ + + + +
+
+
+
+ ) +} diff --git a/src/components/PortfolioPage/components/AppHeader/index.ts b/src/components/PortfolioPage/components/AppHeader/index.ts new file mode 100644 index 0000000..8af702b --- /dev/null +++ b/src/components/PortfolioPage/components/AppHeader/index.ts @@ -0,0 +1 @@ +export { AppHeader } from './AppHeader' diff --git a/src/components/PortfolioPage/components/CapabilitiesSection/CapabilitiesSection.tsx b/src/components/PortfolioPage/components/CapabilitiesSection/CapabilitiesSection.tsx new file mode 100644 index 0000000..55cb11a --- /dev/null +++ b/src/components/PortfolioPage/components/CapabilitiesSection/CapabilitiesSection.tsx @@ -0,0 +1,39 @@ +import { SectionHeading } from '@/components/ui/SectionHeading' +import type { TranslationKey } from '@/lib/i18n' +import { useI18n } from '@/lib/i18n' + +const groups: readonly { bodyKey: TranslationKey; titleKey: TranslationKey }[] = [ + { + titleKey: 'capabilities.product.title', + bodyKey: 'capabilities.product.body', + }, + { + titleKey: 'capabilities.quality.title', + bodyKey: 'capabilities.quality.body', + }, + { + titleKey: 'capabilities.research.title', + bodyKey: 'capabilities.research.body', + }, + { + titleKey: 'capabilities.languages.title', + bodyKey: 'capabilities.languages.body', + }, +] + +export const CapabilitiesSection = () => { + const { t } = useI18n() + return ( +
+ +
+ {groups.map((group) => ( +
+

{t(group.titleKey)}

+

{t(group.bodyKey)}

+
+ ))} +
+
+ ) +} diff --git a/src/components/PortfolioPage/components/CapabilitiesSection/index.ts b/src/components/PortfolioPage/components/CapabilitiesSection/index.ts new file mode 100644 index 0000000..a148beb --- /dev/null +++ b/src/components/PortfolioPage/components/CapabilitiesSection/index.ts @@ -0,0 +1 @@ +export { CapabilitiesSection } from './CapabilitiesSection' diff --git a/src/components/PortfolioPage/components/ContactSection/ContactSection.tsx b/src/components/PortfolioPage/components/ContactSection/ContactSection.tsx new file mode 100644 index 0000000..fba0a1b --- /dev/null +++ b/src/components/PortfolioPage/components/ContactSection/ContactSection.tsx @@ -0,0 +1,24 @@ +import { ExternalLink } from '@/components/ui/ExternalLink' +import { profile } from '@/content/profile' +import { useI18n } from '@/lib/i18n' + +export const ContactSection = () => { + const { t } = useI18n() + return ( +
+
+

{t('contact.eyebrow')}

+

{t('contact.title')}

+

{t('contact.body')}

+ +
+
+ ) +} diff --git a/src/components/PortfolioPage/components/ContactSection/index.ts b/src/components/PortfolioPage/components/ContactSection/index.ts new file mode 100644 index 0000000..f6f2d6f --- /dev/null +++ b/src/components/PortfolioPage/components/ContactSection/index.ts @@ -0,0 +1 @@ +export { ContactSection } from './ContactSection' diff --git a/src/components/PortfolioPage/components/EducationSection/EducationSection.tsx b/src/components/PortfolioPage/components/EducationSection/EducationSection.tsx new file mode 100644 index 0000000..d359491 --- /dev/null +++ b/src/components/PortfolioPage/components/EducationSection/EducationSection.tsx @@ -0,0 +1,22 @@ +import { SectionHeading } from '@/components/ui/SectionHeading' +import { education } from '@/content/education' +import { useI18n } from '@/lib/i18n' + +export const EducationSection = () => { + const { t } = useI18n() + return ( +
+ +
+ {education.map((item) => ( +
+

{item.period}

+

{t(item.degreeKey)}

+

{item.institutionKey ? t(item.institutionKey) : item.institution}

+

{t(item.statusKey)}

+
+ ))} +
+
+ ) +} diff --git a/src/components/PortfolioPage/components/EducationSection/index.ts b/src/components/PortfolioPage/components/EducationSection/index.ts new file mode 100644 index 0000000..cd09396 --- /dev/null +++ b/src/components/PortfolioPage/components/EducationSection/index.ts @@ -0,0 +1 @@ +export { EducationSection } from './EducationSection' diff --git a/src/components/PortfolioPage/components/ExperienceSection/ExperienceSection.tsx b/src/components/PortfolioPage/components/ExperienceSection/ExperienceSection.tsx new file mode 100644 index 0000000..2f9e8ba --- /dev/null +++ b/src/components/PortfolioPage/components/ExperienceSection/ExperienceSection.tsx @@ -0,0 +1,52 @@ +import { SectionHeading } from '@/components/ui/SectionHeading' +import { experiences } from '@/content/experience' +import { formatExperiencePeriod } from '@/lib/date' +import { useI18n } from '@/lib/i18n' + +export const ExperienceSection = () => { + const { locale, t } = useI18n() + return ( +
+ +
+ {experiences.map((experience) => ( +
+
+ ))} +
+
+ ) +} diff --git a/src/components/PortfolioPage/components/ExperienceSection/index.ts b/src/components/PortfolioPage/components/ExperienceSection/index.ts new file mode 100644 index 0000000..fa8c3ff --- /dev/null +++ b/src/components/PortfolioPage/components/ExperienceSection/index.ts @@ -0,0 +1 @@ +export { ExperienceSection } from './ExperienceSection' diff --git a/src/components/PortfolioPage/components/HeroSection/HeroSection.tsx b/src/components/PortfolioPage/components/HeroSection/HeroSection.tsx new file mode 100644 index 0000000..c70f5b9 --- /dev/null +++ b/src/components/PortfolioPage/components/HeroSection/HeroSection.tsx @@ -0,0 +1,41 @@ +import { ExternalLink } from '@/components/ui/ExternalLink' +import { profile } from '@/content/profile' +import { useI18n } from '@/lib/i18n' +import { GitHubAvatar } from './components/GitHubAvatar' + +export const HeroSection = () => { + const { t } = useI18n() + const chips = [ + t('hero.chipWebMobile'), + t('hero.chipWhiteLabel'), + t('hero.chipComputerVision'), + t('hero.chipAppliedAi'), + ] + + return ( +
+ +

{t('hero.eyebrow')}

+

+ {t('hero.titleStart')} {t('hero.titleAccent')} +

+

{t('hero.summary')}

+
    + {chips.map((chip) => ( +
  • {chip}
  • + ))} +
+
+ + {t('hero.viewWork')} + + + {t('hero.github')} + + + {t('hero.linkedin')} + +
+
+ ) +} diff --git a/src/components/PortfolioPage/components/HeroSection/components/GitHubAvatar/GitHubAvatar.test.tsx b/src/components/PortfolioPage/components/HeroSection/components/GitHubAvatar/GitHubAvatar.test.tsx new file mode 100644 index 0000000..34eeffe --- /dev/null +++ b/src/components/PortfolioPage/components/HeroSection/components/GitHubAvatar/GitHubAvatar.test.tsx @@ -0,0 +1,17 @@ +import { fireEvent, render, screen } from '@testing-library/preact' +import { describe, expect, it } from 'vitest' +import { I18nProvider } from '@/lib/i18n' +import { GitHubAvatar } from './GitHubAvatar' + +describe('GitHubAvatar', () => { + it('uses the local fallback after the GitHub image fails', () => { + render( + + + , + ) + const avatar = screen.getByRole('img', { name: 'Lucas Mariz' }) + fireEvent.error(avatar) + expect(avatar).toHaveAttribute('src', '/avatar-fallback.svg') + }) +}) diff --git a/src/components/PortfolioPage/components/HeroSection/components/GitHubAvatar/GitHubAvatar.tsx b/src/components/PortfolioPage/components/HeroSection/components/GitHubAvatar/GitHubAvatar.tsx new file mode 100644 index 0000000..6fb3812 --- /dev/null +++ b/src/components/PortfolioPage/components/HeroSection/components/GitHubAvatar/GitHubAvatar.tsx @@ -0,0 +1,33 @@ +import { useEffect, useRef, useState } from 'preact/hooks' +import { profile } from '@/content/profile' +import { useI18n } from '@/lib/i18n' + +export const GitHubAvatar = () => { + const [hasFailed, setHasFailed] = useState(false) + const imageRef = useRef(null) + const { t } = useI18n() + + useEffect(() => { + const image = imageRef.current + if (!image?.complete || image.naturalWidth > 0) return + setHasFailed(true) + }, []) + + return ( + {t('hero.avatarAlt')} { + if (hasFailed) return + setHasFailed(true) + }} + /> + ) +} diff --git a/src/components/PortfolioPage/components/HeroSection/components/GitHubAvatar/index.ts b/src/components/PortfolioPage/components/HeroSection/components/GitHubAvatar/index.ts new file mode 100644 index 0000000..4b48e53 --- /dev/null +++ b/src/components/PortfolioPage/components/HeroSection/components/GitHubAvatar/index.ts @@ -0,0 +1 @@ +export { GitHubAvatar } from './GitHubAvatar' diff --git a/src/components/PortfolioPage/components/HeroSection/index.ts b/src/components/PortfolioPage/components/HeroSection/index.ts new file mode 100644 index 0000000..7960318 --- /dev/null +++ b/src/components/PortfolioPage/components/HeroSection/index.ts @@ -0,0 +1 @@ +export { HeroSection } from './HeroSection' diff --git a/src/components/PortfolioPage/components/ProjectsSection/ProjectsSection.tsx b/src/components/PortfolioPage/components/ProjectsSection/ProjectsSection.tsx new file mode 100644 index 0000000..a2478c9 --- /dev/null +++ b/src/components/PortfolioPage/components/ProjectsSection/ProjectsSection.tsx @@ -0,0 +1,50 @@ +import { ExternalLink } from '@/components/ui/ExternalLink' +import { SectionHeading } from '@/components/ui/SectionHeading' +import { jigSolver, projects } from '@/content/projects' +import { useI18n } from '@/lib/i18n' +import { ProjectCard } from './components/ProjectCard' +import { ProjectDetails } from './components/ProjectDetails' + +export const ProjectsSection = () => { + const { t } = useI18n() + return ( +
+ +
+
+

{t('projects.jig.status')}

+

{t('projects.jig.title')}

+

{t('projects.jig.summary')}

+ +
    + {jigSolver.technologies.map((technology) => ( +
  • {technology}
  • + ))} +
+ + {t('projects.jig.link')} + +
+
+ {t('projects.jig.imageAlt')} +
+
+
+ {projects.map((project) => ( + + ))} +
+
+ ) +} diff --git a/src/components/PortfolioPage/components/ProjectsSection/components/ProjectCard/ProjectCard.tsx b/src/components/PortfolioPage/components/ProjectsSection/components/ProjectCard/ProjectCard.tsx new file mode 100644 index 0000000..e7fe9e4 --- /dev/null +++ b/src/components/PortfolioPage/components/ProjectsSection/components/ProjectCard/ProjectCard.tsx @@ -0,0 +1,30 @@ +import { ExternalLink } from '@/components/ui/ExternalLink' +import type { Project } from '@/content/projects' +import { useI18n } from '@/lib/i18n' +import { ProjectDetails } from '../ProjectDetails' + +type ProjectCardProps = { + project: Project +} + +export const ProjectCard = ({ project }: ProjectCardProps) => { + const { t } = useI18n() + return ( +
+ {project.statusKey ?

{t(project.statusKey)}

: null} +

{t(project.titleKey)}

+

{t(project.summaryKey)}

+ +
    + {project.technologies.map((technology) => ( +
  • {technology}
  • + ))} +
+ {project.url ? ( + + {t('projects.sourceLink')} + + ) : null} +
+ ) +} diff --git a/src/components/PortfolioPage/components/ProjectsSection/components/ProjectCard/index.ts b/src/components/PortfolioPage/components/ProjectsSection/components/ProjectCard/index.ts new file mode 100644 index 0000000..a8ccec3 --- /dev/null +++ b/src/components/PortfolioPage/components/ProjectsSection/components/ProjectCard/index.ts @@ -0,0 +1 @@ +export { ProjectCard } from './ProjectCard' diff --git a/src/components/PortfolioPage/components/ProjectsSection/components/ProjectDetails/ProjectDetails.tsx b/src/components/PortfolioPage/components/ProjectsSection/components/ProjectDetails/ProjectDetails.tsx new file mode 100644 index 0000000..3edb043 --- /dev/null +++ b/src/components/PortfolioPage/components/ProjectsSection/components/ProjectDetails/ProjectDetails.tsx @@ -0,0 +1,26 @@ +import type { ProjectDetails as ProjectDetailsContent } from '@/content/projects' +import { useI18n } from '@/lib/i18n' + +type ProjectDetailsProps = { + details: ProjectDetailsContent +} + +export const ProjectDetails = ({ details }: ProjectDetailsProps) => { + const { t } = useI18n() + return ( +
+
+
{t('projects.detail.approach')}
+
{t(details.approachKey)}
+
+
+
{t('projects.detail.contribution')}
+
{t(details.contributionKey)}
+
+
+
{t('projects.detail.outcome')}
+
{t(details.outcomeKey)}
+
+
+ ) +} diff --git a/src/components/PortfolioPage/components/ProjectsSection/components/ProjectDetails/index.ts b/src/components/PortfolioPage/components/ProjectsSection/components/ProjectDetails/index.ts new file mode 100644 index 0000000..5b9af14 --- /dev/null +++ b/src/components/PortfolioPage/components/ProjectsSection/components/ProjectDetails/index.ts @@ -0,0 +1 @@ +export { ProjectDetails } from './ProjectDetails' diff --git a/src/components/PortfolioPage/components/ProjectsSection/index.ts b/src/components/PortfolioPage/components/ProjectsSection/index.ts new file mode 100644 index 0000000..d234bd9 --- /dev/null +++ b/src/components/PortfolioPage/components/ProjectsSection/index.ts @@ -0,0 +1 @@ +export { ProjectsSection } from './ProjectsSection' diff --git a/src/components/PortfolioPage/components/SelectedWorkSection/SelectedWorkSection.tsx b/src/components/PortfolioPage/components/SelectedWorkSection/SelectedWorkSection.tsx new file mode 100644 index 0000000..6e28850 --- /dev/null +++ b/src/components/PortfolioPage/components/SelectedWorkSection/SelectedWorkSection.tsx @@ -0,0 +1,55 @@ +import { ExternalLink } from '@/components/ui/ExternalLink' +import { SectionHeading } from '@/components/ui/SectionHeading' +import { cagliariApp, roleVectorsPaper } from '@/content/selectedWork' +import { useI18n } from '@/lib/i18n' + +export const SelectedWorkSection = () => { + const { t } = useI18n() + return ( +
+
+ +
+ +
+

{t('work.paper.kind')}

+

{t('work.paper.title')}

+

{t('work.paper.summary')}

+
+ {t('work.paper.metric')} + {t('work.paper.metricLabel')} +
+ +
+
+
+
+ ) +} diff --git a/src/components/PortfolioPage/components/SelectedWorkSection/index.ts b/src/components/PortfolioPage/components/SelectedWorkSection/index.ts new file mode 100644 index 0000000..7c691a0 --- /dev/null +++ b/src/components/PortfolioPage/components/SelectedWorkSection/index.ts @@ -0,0 +1 @@ +export { SelectedWorkSection } from './SelectedWorkSection' diff --git a/src/components/PortfolioPage/index.ts b/src/components/PortfolioPage/index.ts new file mode 100644 index 0000000..2a10f35 --- /dev/null +++ b/src/components/PortfolioPage/index.ts @@ -0,0 +1 @@ +export { PortfolioPage } from './PortfolioPage' diff --git a/src/components/ui/ExternalLink/ExternalLink.tsx b/src/components/ui/ExternalLink/ExternalLink.tsx new file mode 100644 index 0000000..f8ab4b9 --- /dev/null +++ b/src/components/ui/ExternalLink/ExternalLink.tsx @@ -0,0 +1,19 @@ +import type { ComponentChildren } from 'preact' +import { useI18n } from '@/lib/i18n' + +type ExternalLinkProps = { + children: ComponentChildren + className?: string + href: string +} + +export const ExternalLink = ({ children, className, href }: ExternalLinkProps) => { + const { t } = useI18n() + return ( + + {children} + ({t('common.openExternal')}) + + + ) +} diff --git a/src/components/ui/ExternalLink/index.ts b/src/components/ui/ExternalLink/index.ts new file mode 100644 index 0000000..96f15f6 --- /dev/null +++ b/src/components/ui/ExternalLink/index.ts @@ -0,0 +1 @@ +export { ExternalLink } from './ExternalLink' diff --git a/src/components/ui/LanguageSwitcher/LanguageSwitcher.tsx b/src/components/ui/LanguageSwitcher/LanguageSwitcher.tsx new file mode 100644 index 0000000..8162658 --- /dev/null +++ b/src/components/ui/LanguageSwitcher/LanguageSwitcher.tsx @@ -0,0 +1,32 @@ +import type { JSX } from 'preact' +import { type Locale, useI18n } from '@/lib/i18n' +import { parseLocale } from '@/lib/i18n/locale' + +export const LanguageSwitcher = () => { + const { locale, setLocale, t } = useI18n() + + const handleChange = (event: JSX.TargetedEvent): void => { + const nextLocale = parseLocale(event.currentTarget.value) + if (!nextLocale) return + setLocale(nextLocale) + } + + const options: readonly { label: string; value: Locale }[] = [ + { value: 'en', label: t('controls.languageEnglish') }, + { value: 'pt-BR', label: t('controls.languagePortuguese') }, + { value: 'fr', label: t('controls.languageFrench') }, + ] + + return ( + + ) +} diff --git a/src/components/ui/LanguageSwitcher/index.ts b/src/components/ui/LanguageSwitcher/index.ts new file mode 100644 index 0000000..3221e3b --- /dev/null +++ b/src/components/ui/LanguageSwitcher/index.ts @@ -0,0 +1 @@ +export { LanguageSwitcher } from './LanguageSwitcher' diff --git a/src/components/ui/SectionHeading/SectionHeading.tsx b/src/components/ui/SectionHeading/SectionHeading.tsx new file mode 100644 index 0000000..b05c825 --- /dev/null +++ b/src/components/ui/SectionHeading/SectionHeading.tsx @@ -0,0 +1,13 @@ +type SectionHeadingProps = { + eyebrow: string + title: string + intro?: string +} + +export const SectionHeading = ({ eyebrow, title, intro }: SectionHeadingProps) => ( +
+

{eyebrow}

+

{title}

+ {intro ?

{intro}

: null} +
+) diff --git a/src/components/ui/SectionHeading/index.ts b/src/components/ui/SectionHeading/index.ts new file mode 100644 index 0000000..da196e6 --- /dev/null +++ b/src/components/ui/SectionHeading/index.ts @@ -0,0 +1 @@ +export { SectionHeading } from './SectionHeading' diff --git a/src/components/ui/ThemeSwitcher/ThemeSwitcher.tsx b/src/components/ui/ThemeSwitcher/ThemeSwitcher.tsx new file mode 100644 index 0000000..48f8c11 --- /dev/null +++ b/src/components/ui/ThemeSwitcher/ThemeSwitcher.tsx @@ -0,0 +1,34 @@ +import type { JSX } from 'preact' +import { useI18n } from '@/lib/i18n' +import { type ThemeChoice, useTheme } from '@/lib/theme' +import { parseThemeChoice } from '@/lib/theme/theme' + +export const ThemeSwitcher = () => { + const { t } = useI18n() + const { choice, setChoice } = useTheme() + + const handleChange = (event: JSX.TargetedEvent): void => { + const nextChoice = parseThemeChoice(event.currentTarget.value) + if (!nextChoice) return + setChoice(nextChoice) + } + + const options: readonly { label: string; value: ThemeChoice }[] = [ + { value: 'auto', label: t('controls.themeAuto') }, + { value: 'light', label: t('controls.themeLight') }, + { value: 'dark', label: t('controls.themeDark') }, + ] + + return ( + + ) +} diff --git a/src/components/ui/ThemeSwitcher/index.ts b/src/components/ui/ThemeSwitcher/index.ts new file mode 100644 index 0000000..5792168 --- /dev/null +++ b/src/components/ui/ThemeSwitcher/index.ts @@ -0,0 +1 @@ +export { ThemeSwitcher } from './ThemeSwitcher' diff --git a/src/content/education.ts b/src/content/education.ts new file mode 100644 index 0000000..7bf5256 --- /dev/null +++ b/src/content/education.ts @@ -0,0 +1,35 @@ +import type { TranslationKey } from '@/lib/i18n' + +export type Education = { + id: 'computer-science' | 'computational-mathematics' | 'it-technician' + degreeKey: TranslationKey + institution: string + institutionKey?: TranslationKey + period: string + statusKey: TranslationKey +} + +export const education: readonly Education[] = [ + { + id: 'computer-science', + degreeKey: 'education.computerScience.degree', + institution: 'Universidade Federal de Minas Gerais (UFMG)', + period: '2023—2027', + statusKey: 'education.computerScience.status', + }, + { + id: 'computational-mathematics', + degreeKey: 'education.computationalMath.degree', + institution: 'Universidade Federal de Minas Gerais (UFMG)', + period: '2019—2023', + statusKey: 'education.computationalMath.status', + }, + { + id: 'it-technician', + degreeKey: 'education.technician.degree', + institution: '', + institutionKey: 'education.technician.institution', + period: '2015—2017', + statusKey: 'education.technician.status', + }, +] diff --git a/src/content/experience.ts b/src/content/experience.ts new file mode 100644 index 0000000..ebc328d --- /dev/null +++ b/src/content/experience.ts @@ -0,0 +1,44 @@ +import type { TranslationKey } from '@/lib/i18n' + +export type Experience = { + id: 'abilitya' | 'pluritech' + company: string + roleKey: TranslationKey + employmentKey?: TranslationKey + locationKey: TranslationKey + summaryKey: TranslationKey + bulletKeys: readonly TranslationKey[] + startDate: `${number}-${number}-${number}` + endDate?: `${number}-${number}-${number}` + technologies: readonly string[] +} + +export const experiences: readonly Experience[] = [ + { + id: 'abilitya', + company: 'ABILITYA', + roleKey: 'experience.abilitya.role', + employmentKey: 'experience.abilitya.employment', + locationKey: 'experience.abilitya.location', + summaryKey: 'experience.abilitya.summary', + bulletKeys: [ + 'experience.abilitya.bulletScale', + 'experience.abilitya.bulletAutomation', + 'experience.abilitya.bulletQuality', + 'experience.abilitya.bulletCagliari', + ], + startDate: '2020-06-01', + technologies: ['JavaScript', 'TypeScript', 'React', 'React Native', 'Cypress', 'Maestro'], + }, + { + id: 'pluritech', + company: 'Pluritech Brasil', + roleKey: 'experience.pluritech.role', + locationKey: 'experience.pluritech.location', + summaryKey: 'experience.pluritech.summary', + bulletKeys: [], + startDate: '2018-09-01', + endDate: '2020-06-30', + technologies: ['Angular', 'Ionic', 'JavaScript'], + }, +] diff --git a/src/content/profile.ts b/src/content/profile.ts new file mode 100644 index 0000000..1fd7bad --- /dev/null +++ b/src/content/profile.ts @@ -0,0 +1,7 @@ +export const profile = { + name: 'Lucas Mariz', + githubUsername: 'LucasPMM', + githubUrl: 'https://github.com/LucasPMM', + linkedinUrl: 'https://www.linkedin.com/in/lucas-mariz-4845b6164/', + avatarUrl: 'https://github.com/LucasPMM.png?size=460', +} as const diff --git a/src/content/projects.ts b/src/content/projects.ts new file mode 100644 index 0000000..9078d74 --- /dev/null +++ b/src/content/projects.ts @@ -0,0 +1,80 @@ +import type { TranslationKey } from '@/lib/i18n' + +export type ProjectDetails = { + approachKey: TranslationKey + contributionKey: TranslationKey + outcomeKey: TranslationKey +} + +export type Project = ProjectDetails & { + id: 'planner' | 'simplex' | 'pokemon' | 'kmeans' | 'lz78' + titleKey: TranslationKey + summaryKey: TranslationKey + statusKey?: TranslationKey + url?: string + technologies: readonly string[] +} + +export const jigSolver: ProjectDetails & { + technologies: readonly string[] + url: string +} = { + url: 'https://jigsolver.app/', + technologies: ['Next.js 16', 'React 19', 'TypeScript', 'Python', 'FastAPI', 'OpenCV'], + approachKey: 'projects.jig.approach', + contributionKey: 'projects.jig.contribution', + outcomeKey: 'projects.jig.outcome', +} + +export const projects: readonly Project[] = [ + { + id: 'planner', + titleKey: 'projects.planner.title', + summaryKey: 'projects.planner.summary', + statusKey: 'projects.private', + approachKey: 'projects.planner.approach', + contributionKey: 'projects.planner.contribution', + outcomeKey: 'projects.planner.outcome', + technologies: ['Next.js 16', 'React 19', 'TypeScript', 'Tailwind CSS 4', 'Firebase', 'Zod'], + }, + { + id: 'simplex', + titleKey: 'projects.simplex.title', + summaryKey: 'projects.simplex.summary', + approachKey: 'projects.simplex.approach', + contributionKey: 'projects.simplex.contribution', + outcomeKey: 'projects.simplex.outcome', + url: 'https://github.com/LucasPMM/simplex', + technologies: ['Python', 'NumPy', 'Linear optimization'], + }, + { + id: 'pokemon', + titleKey: 'projects.pokemon.title', + summaryKey: 'projects.pokemon.summary', + approachKey: 'projects.pokemon.approach', + contributionKey: 'projects.pokemon.contribution', + outcomeKey: 'projects.pokemon.outcome', + url: 'https://github.com/LucasPMM/Pokemon-Base', + technologies: ['Angular 7', 'TypeScript', 'RxJS', 'Chart.js'], + }, + { + id: 'kmeans', + titleKey: 'projects.kmeans.title', + summaryKey: 'projects.kmeans.summary', + approachKey: 'projects.kmeans.approach', + contributionKey: 'projects.kmeans.contribution', + outcomeKey: 'projects.kmeans.outcome', + url: 'https://github.com/LucasPMM/greedy-kmeans', + technologies: ['Python', 'Jupyter', 'scikit-learn', 'Clustering'], + }, + { + id: 'lz78', + titleKey: 'projects.lz78.title', + summaryKey: 'projects.lz78.summary', + approachKey: 'projects.lz78.approach', + contributionKey: 'projects.lz78.contribution', + outcomeKey: 'projects.lz78.outcome', + url: 'https://github.com/LucasPMM/lz78-compression', + technologies: ['Python', 'Trie', 'Lossless compression'], + }, +] diff --git a/src/content/selectedWork.ts b/src/content/selectedWork.ts new file mode 100644 index 0000000..005cecf --- /dev/null +++ b/src/content/selectedWork.ts @@ -0,0 +1,9 @@ +export const cagliariApp = { + appStoreUrl: 'https://apps.apple.com/it/app/cagliari-calcio/id441230439', + playStoreUrl: 'https://play.google.com/store/apps/details?id=com.cagliaricalcioapp.net', +} as const + +export const roleVectorsPaper = { + paperUrl: 'https://dtai.cs.kuleuven.be/events/MLSA26/papers/MLSA26_paper_208.pdf', + scheduleUrl: 'https://dtai.cs.kuleuven.be/events/MLSA26/schedule.php', +} as const diff --git a/src/lib/date/formatExperiencePeriod.test.ts b/src/lib/date/formatExperiencePeriod.test.ts new file mode 100644 index 0000000..87d8030 --- /dev/null +++ b/src/lib/date/formatExperiencePeriod.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' +import type { TranslationKey } from '@/lib/i18n' +import { formatExperiencePeriod } from './formatExperiencePeriod' + +const labels: Partial> = { + 'common.present': 'Present', +} + +const t = (key: TranslationKey, options?: { count?: number }): string => { + if (key === 'common.yearShort') return `${options?.count} yr` + if (key === 'common.yearsShort') return `${options?.count} yrs` + if (key === 'common.monthShort' || key === 'common.monthsShort') return `${options?.count} mos` + return labels[key] ?? key +} + +describe('formatExperiencePeriod', () => { + it('calculates inclusive calendar-month durations from canonical dates', () => { + expect( + formatExperiencePeriod( + '2020-06-01', + undefined, + 'en', + t, + new Date('2026-09-11T00:00:00.000Z'), + ), + ).toBe('Jun 2020 — Present · 6 yrs 4 mos') + }) + + it('formats a completed role', () => { + expect(formatExperiencePeriod('2018-09-01', '2020-06-30', 'en', t)).toBe( + 'Sep 2018 — Jun 2020 · 1 yr 10 mos', + ) + }) +}) diff --git a/src/lib/date/formatExperiencePeriod.ts b/src/lib/date/formatExperiencePeriod.ts new file mode 100644 index 0000000..f9d17e8 --- /dev/null +++ b/src/lib/date/formatExperiencePeriod.ts @@ -0,0 +1,47 @@ +import type { TOptions } from 'i18next' +import type { Locale, TranslationKey } from '@/lib/i18n' + +type Translator = (key: TranslationKey, options?: TOptions) => string + +const toDate = (value: string): Date => new Date(`${value}T00:00:00.000Z`) + +const formatMonthYear = (date: Date, locale: Locale): string => + new Intl.DateTimeFormat(locale, { + month: 'short', + year: 'numeric', + timeZone: 'UTC', + }).format(date) + +const getInclusiveMonthCount = (startDate: Date, endDate: Date): number => + (endDate.getUTCFullYear() - startDate.getUTCFullYear()) * 12 + + endDate.getUTCMonth() - + startDate.getUTCMonth() + + 1 + +const formatDuration = (monthCount: number, t: Translator): string => { + const years = Math.floor(monthCount / 12) + const months = monthCount % 12 + const yearKey: TranslationKey = years === 1 ? 'common.yearShort' : 'common.yearsShort' + const monthKey: TranslationKey = months === 1 ? 'common.monthShort' : 'common.monthsShort' + return [ + years > 0 ? t(yearKey, { count: years }) : null, + months > 0 ? t(monthKey, { count: months }) : null, + ] + .filter((part): part is string => Boolean(part)) + .join(' ') +} + +export const formatExperiencePeriod = ( + startValue: string, + endValue: string | undefined, + locale: Locale, + t: Translator, + now = new Date(), +): string => { + const startDate = toDate(startValue) + const endDate = endValue ? toDate(endValue) : now + const startLabel = formatMonthYear(startDate, locale) + const endLabel = endValue ? formatMonthYear(endDate, locale) : t('common.present') + const duration = formatDuration(getInclusiveMonthCount(startDate, endDate), t) + return `${startLabel} — ${endLabel} · ${duration}` +} diff --git a/src/lib/date/index.ts b/src/lib/date/index.ts new file mode 100644 index 0000000..0e90913 --- /dev/null +++ b/src/lib/date/index.ts @@ -0,0 +1 @@ +export { formatExperiencePeriod } from './formatExperiencePeriod' diff --git a/src/lib/i18n/catalog.test.ts b/src/lib/i18n/catalog.test.ts new file mode 100644 index 0000000..be3a3db --- /dev/null +++ b/src/lib/i18n/catalog.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from 'vitest' +import { en, fr, ptBR } from './catalog' + +describe('translation catalogs', () => { + it('keeps key parity across all supported languages', () => { + const englishKeys = Object.keys(en).sort() + expect(Object.keys(ptBR).sort()).toEqual(englishKeys) + expect(Object.keys(fr).sort()).toEqual(englishKeys) + }) +}) diff --git a/src/lib/i18n/catalog.ts b/src/lib/i18n/catalog.ts new file mode 100644 index 0000000..c856f76 --- /dev/null +++ b/src/lib/i18n/catalog.ts @@ -0,0 +1,559 @@ +export const en = { + 'metadata.title': 'Lucas Mariz — Senior Software Engineer', + 'metadata.description': + 'Portfolio of Lucas Mariz: scalable web and mobile products, computer vision, and applied research.', + 'common.skipToContent': 'Skip to content', + 'common.present': 'Present', + 'common.expected': 'Expected', + 'common.completed': 'Completed', + 'common.openExternal': 'opens in a new tab', + 'common.yearsShort': '{{count}} yrs', + 'common.yearShort': '{{count}} yr', + 'common.monthsShort': '{{count}} mos', + 'common.monthShort': '{{count}} mo', + 'controls.language': 'Language', + 'controls.languageEnglish': 'English', + 'controls.languagePortuguese': 'Português', + 'controls.languageFrench': 'Français', + 'controls.theme': 'Theme', + 'controls.themeAuto': 'System', + 'controls.themeLight': 'Light', + 'controls.themeDark': 'Dark', + 'controls.menu': 'Open navigation', + 'nav.about': 'About', + 'nav.experience': 'Experience', + 'nav.work': 'Selected work', + 'nav.education': 'Education', + 'nav.projects': 'Projects', + 'nav.capabilities': 'Capabilities', + 'nav.contact': 'Contact', + 'hero.eyebrow': 'Senior Software Engineer · Remote', + 'hero.titleStart': 'I build scalable digital products and explore how', + 'hero.titleAccent': 'AI solves visual problems.', + 'hero.summary': + 'Senior web and mobile engineering in the JavaScript ecosystem, grounded in Computational Mathematics, Computer Science, and applied computer vision.', + 'hero.avatarAlt': 'Lucas Mariz', + 'hero.avatarFallback': 'LM', + 'hero.chipWebMobile': 'Web & mobile', + 'hero.chipWhiteLabel': 'White-label platforms', + 'hero.chipComputerVision': 'Computer vision', + 'hero.chipAppliedAi': 'Applied AI', + 'hero.viewWork': 'View selected work', + 'hero.github': 'GitHub', + 'hero.linkedin': 'LinkedIn', + 'about.eyebrow': 'About', + 'about.title': 'Engineering depth, mathematical curiosity.', + 'about.body': + 'I connect senior product engineering with an academic path in Computational Mathematics and Computer Science. My work spans React and React Native delivery, scalable white-label architecture, automation, quality systems, optimization, and computer vision.', + 'experience.eyebrow': 'Career', + 'experience.title': 'Professional experience', + 'experience.intro': + 'A web and mobile career shaped by long-lived products, reusable architecture, and testable delivery.', + 'experience.abilitya.role': 'Senior Software Engineer', + 'experience.abilitya.employment': 'Full-time', + 'experience.abilitya.location': 'Milan, Italy · Remote', + 'experience.abilitya.summary': + 'Engineering scalable products across a React and React Native ecosystem for international clients.', + 'experience.abilitya.bulletScale': + 'Evolve reusable white-label foundations so multiple products can grow without duplicating core work.', + 'experience.abilitya.bulletAutomation': + 'Build engineering and product automations that make delivery more consistent.', + 'experience.abilitya.bulletQuality': + 'Improve testability and end-to-end coverage across web and mobile with Cypress and Maestro.', + 'experience.abilitya.bulletCagliari': + 'Share responsibility for the official Cagliari Calcio app, serving more than 10,000 users across Android and iOS.', + 'experience.pluritech.role': 'Frontend Developer', + 'experience.pluritech.location': 'Belo Horizonte, Brazil', + 'experience.pluritech.summary': + 'Built cross-platform web and mobile interfaces with Angular and Ionic, establishing the frontend foundation of my career.', + 'work.eyebrow': 'Selected work', + 'work.title': 'Products in production and research in the field.', + 'work.intro': + 'Two outcomes that connect product responsibility, mobile scale, data, and applied research.', + 'work.cagliari.kind': 'Production mobile app · Android & iOS', + 'work.cagliari.title': 'Cagliari Calcio official app', + 'work.cagliari.summary': + 'I am one of the engineers responsible for the official supporter experience, with news, live match coverage, exclusive content, events, and community features.', + 'work.cagliari.metric': '10K+ users', + 'work.cagliari.metricLabel': 'across Android and iOS', + 'work.cagliari.appStore': 'View on the App Store', + 'work.cagliari.playStore': 'View on Google Play', + 'work.paper.kind': 'Accepted paper · MLSA 2026', + 'work.paper.title': + 'Role Vectors: Tracking-Based Representations of Football Players’ Tactical Behavior', + 'work.paper.summary': + 'I co-authored this tracking-based representation of football players’ relative tactical roles, including interpretable in-possession and off-ball behavior profiles.', + 'work.paper.metric': 'International presentation', + 'work.paper.metricLabel': 'Naples, Italy · September 7, 2026', + 'work.paper.paperLink': 'Read the paper', + 'work.paper.scheduleLink': 'View conference schedule', + 'education.eyebrow': 'Academic path', + 'education.title': 'Education', + 'education.computerScience.degree': "Bachelor's degree, Computer Science", + 'education.computerScience.status': 'Expected graduation in 2027', + 'education.computationalMath.degree': "Bachelor's degree, Computational Mathematics", + 'education.computationalMath.status': 'Completed in 2023', + 'education.technician.degree': 'IT Technician', + 'education.technician.institution': 'Colégio Técnico da UFMG (COLTEC)', + 'education.technician.status': 'Completed in 2017', + 'projects.eyebrow': 'Projects', + 'projects.title': 'Experiments made tangible.', + 'projects.intro': + 'A curated set of products and academic implementations. Repository cleanup will not change this editorial order.', + 'projects.detail.approach': 'Approach', + 'projects.detail.contribution': 'Contribution', + 'projects.detail.outcome': 'Outcome', + 'projects.private': 'Private project', + 'projects.jig.status': 'Active project', + 'projects.jig.title': 'Jig Solver', + 'projects.jig.summary': + 'A computer-vision platform that digitizes physical jigsaw pieces, evaluates compatibility, reconstructs the puzzle, and guides physical assembly through a camera-based assistant.', + 'projects.jig.imageAlt': 'Jig Solver workspace showing analyzed puzzle pieces', + 'projects.jig.approach': + 'A typed Next.js client coordinates a Python and FastAPI solving engine, OpenCV analysis, persisted placements, and a Three.js workspace.', + 'projects.jig.contribution': + 'I designed and implemented the product architecture, puzzle-analysis workflow, reconstruction lifecycle, and camera-guided assembly experience.', + 'projects.jig.outcome': + 'The working product analyzes pieces, ranks compatibility, persists solutions, and guides physical assembly with stable camera tracking.', + 'projects.jig.link': 'Open jigsolver.app', + 'projects.planner.title': 'Planner', + 'projects.planner.summary': + 'A private, installable personal-finance dashboard with a secure assistant for understanding expenses, income, cash flow, debts, and purchase plans.', + 'projects.planner.approach': + 'A statically exported Next.js application uses Firebase Auth, Firestore, and callable Functions. Deterministic summaries constrain the provider-neutral BYOK assistant.', + 'projects.planner.contribution': + 'I built the product end to end, from financial models and responsive workflows to Firestore isolation, encrypted credentials, testing, and delivery automation.', + 'projects.planner.outcome': + 'The PWA brings recurring expenses, analytics, reports, financing projections, goals, and read-only AI guidance into one protected workspace.', + 'projects.simplex.title': 'Simplex', + 'projects.simplex.summary': + 'A command-line linear-programming solver that classifies optimal, infeasible, and unbounded problems.', + 'projects.simplex.approach': + 'A two-phase tableau implementation uses an auxiliary problem, Bland’s rule, numerical tolerance, and explicit feasibility or optimality certificates.', + 'projects.simplex.contribution': + 'I implemented input parsing, pivoting, basis construction, result serialization, and certificate validation in Python and NumPy.', + 'projects.simplex.outcome': + 'A broad fixture corpus covers feasible, infeasible, unbounded, degenerate, and harder linear programs.', + 'projects.pokemon.title': 'Pokémon Base', + 'projects.pokemon.summary': + 'A routed Pokédex-style web application for listing, querying, and inspecting Pokémon and their evolutions.', + 'projects.pokemon.approach': + 'Angular 7 lazy-loaded feature modules separate list, query, and detail experiences, backed by a typed HTTP service and reusable presentation components.', + 'projects.pokemon.contribution': + 'I structured the frontend, data wrapper, routing, filters, charts, detail views, and component tests.', + 'projects.pokemon.outcome': + 'The result is a cohesive multi-view exploration interface rather than a single static catalogue.', + 'projects.kmeans.title': 'Greedy K-means', + 'projects.kmeans.summary': + 'An empirical comparison between a greedy 2-approximation for k-center and classical K-Means clustering.', + 'projects.kmeans.approach': + 'The experiment runs both methods across ten datasets and two Minkowski distances, measuring radius, silhouette, adjusted Rand index, and runtime.', + 'projects.kmeans.contribution': + 'I implemented the greedy center selection, cluster assignment, evaluation pipeline, notebooks, and reproducible result tables.', + 'projects.kmeans.outcome': + 'Per-dataset and aggregate tables make the quality-versus-computational-cost tradeoffs directly inspectable.', + 'projects.lz78.title': 'LZ78 Compression', + 'projects.lz78.summary': + 'A lossless text-compression implementation based on the LZ78 dictionary algorithm.', + 'projects.lz78.approach': + 'A compressed trie stores discovered phrases while separate command-line flows encode text to Z78 and reconstruct it back to text.', + 'projects.lz78.contribution': + 'I implemented the trie, compression and decompression pipelines, command validation, output handling, and test corpus.', + 'projects.lz78.outcome': + 'Ten representative text inputs and an accompanying report document round-trip behavior and implementation choices.', + 'projects.sourceLink': 'View source', + 'capabilities.eyebrow': 'Capabilities', + 'capabilities.title': 'Tools in context, not progress bars.', + 'capabilities.product.title': 'Product engineering', + 'capabilities.product.body': 'JavaScript, TypeScript, React, React Native, Angular, Ionic', + 'capabilities.quality.title': 'Quality & delivery', + 'capabilities.quality.body': 'Cypress, Maestro, automation, testability, GitHub Actions', + 'capabilities.research.title': 'Applied research', + 'capabilities.research.body': 'Python, OpenCV, computer vision, AI, optimization', + 'capabilities.languages.title': 'Languages', + 'capabilities.languages.body': 'Portuguese — native · English — Cambridge C1 · French — learning', + 'contact.eyebrow': 'Contact', + 'contact.title': 'Let’s build something thoughtful.', + 'contact.body': + 'I am open to conversations about software engineering, mobile products, computer vision, applied AI, and ambitious product ideas.', + 'contact.github': 'Continue on GitHub', + 'contact.linkedin': 'Connect on LinkedIn', + 'footer.copy': 'Designed as a warm digital notebook. Built with Preact and TypeScript.', +} as const + +export type TranslationKey = keyof typeof en +export type TranslationCatalog = Record + +export const ptBR: TranslationCatalog = { + 'metadata.title': 'Lucas Mariz — Engenheiro de Software Sênior', + 'metadata.description': + 'Portfólio de Lucas Mariz: produtos web e mobile escaláveis, visão computacional e pesquisa aplicada.', + 'common.skipToContent': 'Pular para o conteúdo', + 'common.present': 'Atual', + 'common.expected': 'Previsto', + 'common.completed': 'Concluído', + 'common.openExternal': 'abre em uma nova aba', + 'common.yearsShort': '{{count}} anos', + 'common.yearShort': '{{count}} ano', + 'common.monthsShort': '{{count}} meses', + 'common.monthShort': '{{count}} mês', + 'controls.language': 'Idioma', + 'controls.languageEnglish': 'English', + 'controls.languagePortuguese': 'Português', + 'controls.languageFrench': 'Français', + 'controls.theme': 'Tema', + 'controls.themeAuto': 'Sistema', + 'controls.themeLight': 'Claro', + 'controls.themeDark': 'Escuro', + 'controls.menu': 'Abrir navegação', + 'nav.about': 'Sobre', + 'nav.experience': 'Experiência', + 'nav.work': 'Trabalhos', + 'nav.education': 'Formação', + 'nav.projects': 'Projetos', + 'nav.capabilities': 'Competências', + 'nav.contact': 'Contato', + 'hero.eyebrow': 'Engenheiro de Software Sênior · Remoto', + 'hero.titleStart': 'Eu construo produtos digitais escaláveis e exploro como a', + 'hero.titleAccent': 'IA resolve problemas visuais.', + 'hero.summary': + 'Engenharia sênior para web e mobile no ecossistema JavaScript, apoiada por Matemática Computacional, Ciência da Computação e visão computacional aplicada.', + 'hero.avatarAlt': 'Lucas Mariz', + 'hero.avatarFallback': 'LM', + 'hero.chipWebMobile': 'Web & mobile', + 'hero.chipWhiteLabel': 'Plataformas white-label', + 'hero.chipComputerVision': 'Visão computacional', + 'hero.chipAppliedAi': 'IA aplicada', + 'hero.viewWork': 'Ver trabalhos selecionados', + 'hero.github': 'GitHub', + 'hero.linkedin': 'LinkedIn', + 'about.eyebrow': 'Sobre', + 'about.title': 'Profundidade em engenharia, curiosidade matemática.', + 'about.body': + 'Conecto engenharia de produto sênior a uma trajetória acadêmica em Matemática Computacional e Ciência da Computação. Meu trabalho passa por React e React Native, arquitetura white-label escalável, automação, sistemas de qualidade, otimização e visão computacional.', + 'experience.eyebrow': 'Carreira', + 'experience.title': 'Experiência profissional', + 'experience.intro': + 'Uma carreira web e mobile moldada por produtos de longa duração, arquitetura reutilizável e entregas testáveis.', + 'experience.abilitya.role': 'Engenheiro de Software Sênior', + 'experience.abilitya.employment': 'Tempo integral', + 'experience.abilitya.location': 'Milão, Itália · Remoto', + 'experience.abilitya.summary': + 'Engenharia de produtos escaláveis em um ecossistema React e React Native para clientes internacionais.', + 'experience.abilitya.bulletScale': + 'Evoluo fundações white-label reutilizáveis para que vários produtos cresçam sem duplicar o trabalho central.', + 'experience.abilitya.bulletAutomation': + 'Crio automações de engenharia e produto que tornam as entregas mais consistentes.', + 'experience.abilitya.bulletQuality': + 'Melhoro a testabilidade e a cobertura ponta a ponta em web e mobile com Cypress e Maestro.', + 'experience.abilitya.bulletCagliari': + 'Divido a responsabilidade pelo aplicativo oficial do Cagliari Calcio, usado por mais de 10 mil pessoas entre Android e iOS.', + 'experience.pluritech.role': 'Desenvolvedor Frontend', + 'experience.pluritech.location': 'Belo Horizonte, Brasil', + 'experience.pluritech.summary': + 'Construí interfaces web e mobile multiplataforma com Angular e Ionic, formando a base frontend da minha carreira.', + 'work.eyebrow': 'Trabalhos selecionados', + 'work.title': 'Produtos em produção e pesquisa em campo.', + 'work.intro': + 'Dois resultados que conectam responsabilidade de produto, escala mobile, dados e pesquisa aplicada.', + 'work.cagliari.kind': 'Aplicativo em produção · Android & iOS', + 'work.cagliari.title': 'Aplicativo oficial do Cagliari Calcio', + 'work.cagliari.summary': + 'Sou um dos engenheiros responsáveis pela experiência oficial dos torcedores, com notícias, partidas ao vivo, conteúdo exclusivo, eventos e recursos de comunidade.', + 'work.cagliari.metric': '10 mil+ usuários', + 'work.cagliari.metricLabel': 'entre Android e iOS', + 'work.cagliari.appStore': 'Ver na App Store', + 'work.cagliari.playStore': 'Ver no Google Play', + 'work.paper.kind': 'Paper aceito · MLSA 2026', + 'work.paper.title': + 'Role Vectors: Tracking-Based Representations of Football Players’ Tactical Behavior', + 'work.paper.summary': + 'Sou coautor desta representação baseada em tracking dos papéis táticos relativos de jogadores de futebol, incluindo perfis interpretáveis com e sem a posse de bola.', + 'work.paper.metric': 'Apresentação internacional', + 'work.paper.metricLabel': 'Nápoles, Itália · 7 de setembro de 2026', + 'work.paper.paperLink': 'Ler o paper', + 'work.paper.scheduleLink': 'Ver programação da conferência', + 'education.eyebrow': 'Trajetória acadêmica', + 'education.title': 'Formação', + 'education.computerScience.degree': 'Bacharelado em Ciência da Computação', + 'education.computerScience.status': 'Formatura prevista para 2027', + 'education.computationalMath.degree': 'Bacharelado em Matemática Computacional', + 'education.computationalMath.status': 'Concluído em 2023', + 'education.technician.degree': 'Técnico em Informática', + 'education.technician.institution': 'Colégio Técnico da UFMG (COLTEC)', + 'education.technician.status': 'Concluído em 2017', + 'projects.eyebrow': 'Projetos', + 'projects.title': 'Experimentos que se tornam concretos.', + 'projects.intro': + 'Uma seleção de produtos e implementações acadêmicas. A organização do GitHub não altera esta ordem editorial.', + 'projects.detail.approach': 'Abordagem', + 'projects.detail.contribution': 'Contribuição', + 'projects.detail.outcome': 'Resultado', + 'projects.private': 'Projeto privado', + 'projects.jig.status': 'Projeto ativo', + 'projects.jig.title': 'Jig Solver', + 'projects.jig.summary': + 'Uma plataforma de visão computacional que digitaliza peças de quebra-cabeça, avalia compatibilidade, reconstrói o puzzle e orienta a montagem física por uma câmera.', + 'projects.jig.imageAlt': 'Área de trabalho do Jig Solver com peças analisadas', + 'projects.jig.approach': + 'Um cliente tipado em Next.js coordena um motor de resolução em Python e FastAPI, análise com OpenCV, posições persistidas e uma área de trabalho em Three.js.', + 'projects.jig.contribution': + 'Projetei e implementei a arquitetura do produto, o fluxo de análise das peças, o ciclo de reconstrução e a experiência de montagem guiada por câmera.', + 'projects.jig.outcome': + 'O produto funcional analisa peças, ordena compatibilidades, persiste soluções e orienta a montagem física com tracking estável pela câmera.', + 'projects.jig.link': 'Abrir jigsolver.app', + 'projects.planner.title': 'Planner', + 'projects.planner.summary': + 'Um painel privado e instalável de finanças pessoais, com um assistente seguro para entender gastos, receitas, fluxo de caixa, dívidas e planos de compra.', + 'projects.planner.approach': + 'Uma aplicação Next.js exportada estaticamente usa Firebase Auth, Firestore e Functions chamáveis. Resumos determinísticos limitam o contexto do assistente BYOK independente de provedor.', + 'projects.planner.contribution': + 'Construí o produto de ponta a ponta, dos modelos financeiros e fluxos responsivos ao isolamento no Firestore, credenciais criptografadas, testes e automação de entrega.', + 'projects.planner.outcome': + 'A PWA reúne gastos recorrentes, análises, relatórios, projeções de financiamentos, metas e orientação por IA somente leitura em um espaço protegido.', + 'projects.simplex.title': 'Simplex', + 'projects.simplex.summary': + 'Um resolvedor de programação linear por linha de comando que classifica problemas ótimos, inviáveis e ilimitados.', + 'projects.simplex.approach': + 'Uma implementação de tableau em duas fases usa problema auxiliar, regra de Bland, tolerância numérica e certificados explícitos de viabilidade ou otimalidade.', + 'projects.simplex.contribution': + 'Implementei parsing de entrada, pivoteamento, construção da base, serialização de resultados e validação de certificados em Python e NumPy.', + 'projects.simplex.outcome': + 'Um corpus amplo de fixtures cobre programas lineares viáveis, inviáveis, ilimitados, degenerados e mais difíceis.', + 'projects.pokemon.title': 'Pokémon Base', + 'projects.pokemon.summary': + 'Uma aplicação web no estilo Pokédex para listar, consultar e inspecionar Pokémon e suas evoluções.', + 'projects.pokemon.approach': + 'Módulos de funcionalidades lazy-loaded em Angular 7 separam as experiências de lista, consulta e detalhes, apoiadas por um serviço HTTP tipado e componentes reutilizáveis.', + 'projects.pokemon.contribution': + 'Estruturei o frontend, o wrapper de dados, as rotas, os filtros, os gráficos, as telas de detalhes e os testes de componentes.', + 'projects.pokemon.outcome': + 'O resultado é uma interface coesa de exploração em múltiplas telas, não apenas um catálogo estático.', + 'projects.kmeans.title': 'Greedy K-means', + 'projects.kmeans.summary': + 'Uma comparação empírica entre uma aproximação gulosa de fator 2 para k-centros e o agrupamento K-Means clássico.', + 'projects.kmeans.approach': + 'O experimento executa os dois métodos em dez datasets e duas distâncias de Minkowski, medindo raio, silhueta, índice Rand ajustado e tempo.', + 'projects.kmeans.contribution': + 'Implementei a seleção gulosa de centros, a atribuição de grupos, o pipeline de avaliação, notebooks e tabelas reprodutíveis de resultados.', + 'projects.kmeans.outcome': + 'Tabelas por dataset e agregadas tornam diretamente inspecionáveis os compromissos entre qualidade e custo computacional.', + 'projects.lz78.title': 'Compressão LZ78', + 'projects.lz78.summary': + 'Uma implementação de compressão de texto sem perdas baseada no algoritmo de dicionário LZ78.', + 'projects.lz78.approach': + 'Uma trie comprimida armazena as frases descobertas enquanto fluxos separados de linha de comando codificam texto para Z78 e o reconstroem.', + 'projects.lz78.contribution': + 'Implementei a trie, os pipelines de compressão e descompressão, a validação dos comandos, o tratamento das saídas e o corpus de testes.', + 'projects.lz78.outcome': + 'Dez entradas de texto representativas e um relatório documentam o comportamento de ida e volta e as escolhas da implementação.', + 'projects.sourceLink': 'Ver código', + 'capabilities.eyebrow': 'Competências', + 'capabilities.title': 'Ferramentas em contexto, sem barras de progresso.', + 'capabilities.product.title': 'Engenharia de produto', + 'capabilities.product.body': 'JavaScript, TypeScript, React, React Native, Angular, Ionic', + 'capabilities.quality.title': 'Qualidade & entrega', + 'capabilities.quality.body': 'Cypress, Maestro, automação, testabilidade, GitHub Actions', + 'capabilities.research.title': 'Pesquisa aplicada', + 'capabilities.research.body': 'Python, OpenCV, visão computacional, IA, otimização', + 'capabilities.languages.title': 'Idiomas', + 'capabilities.languages.body': + 'Português — nativo · Inglês — Cambridge C1 · Francês — em aprendizado', + 'contact.eyebrow': 'Contato', + 'contact.title': 'Vamos construir algo bem pensado.', + 'contact.body': + 'Estou aberto a conversas sobre engenharia de software, produtos mobile, visão computacional, IA aplicada e ideias ambiciosas de produto.', + 'contact.github': 'Continuar no GitHub', + 'contact.linkedin': 'Conectar no LinkedIn', + 'footer.copy': 'Projetado como um caderno digital acolhedor. Feito com Preact e TypeScript.', +} + +export const fr: TranslationCatalog = { + 'metadata.title': 'Lucas Mariz — Ingénieur logiciel senior', + 'metadata.description': + 'Portfolio de Lucas Mariz : produits web et mobiles évolutifs, vision par ordinateur et recherche appliquée.', + 'common.skipToContent': 'Aller au contenu', + 'common.present': 'Aujourd’hui', + 'common.expected': 'Prévu', + 'common.completed': 'Terminé', + 'common.openExternal': 's’ouvre dans un nouvel onglet', + 'common.yearsShort': '{{count}} ans', + 'common.yearShort': '{{count}} an', + 'common.monthsShort': '{{count}} mois', + 'common.monthShort': '{{count}} mois', + 'controls.language': 'Langue', + 'controls.languageEnglish': 'English', + 'controls.languagePortuguese': 'Português', + 'controls.languageFrench': 'Français', + 'controls.theme': 'Thème', + 'controls.themeAuto': 'Système', + 'controls.themeLight': 'Clair', + 'controls.themeDark': 'Sombre', + 'controls.menu': 'Ouvrir la navigation', + 'nav.about': 'À propos', + 'nav.experience': 'Expérience', + 'nav.work': 'Réalisations', + 'nav.education': 'Formation', + 'nav.projects': 'Projets', + 'nav.capabilities': 'Compétences', + 'nav.contact': 'Contact', + 'hero.eyebrow': 'Ingénieur logiciel senior · À distance', + 'hero.titleStart': 'Je conçois des produits numériques évolutifs et j’explore comment', + 'hero.titleAccent': 'l’IA résout des problèmes visuels.', + 'hero.summary': + 'Ingénierie web et mobile senior dans l’écosystème JavaScript, avec une base en mathématiques computationnelles, informatique et vision par ordinateur appliquée.', + 'hero.avatarAlt': 'Lucas Mariz', + 'hero.avatarFallback': 'LM', + 'hero.chipWebMobile': 'Web & mobile', + 'hero.chipWhiteLabel': 'Plateformes en marque blanche', + 'hero.chipComputerVision': 'Vision par ordinateur', + 'hero.chipAppliedAi': 'IA appliquée', + 'hero.viewWork': 'Voir les réalisations', + 'hero.github': 'GitHub', + 'hero.linkedin': 'LinkedIn', + 'about.eyebrow': 'À propos', + 'about.title': 'Profondeur technique, curiosité mathématique.', + 'about.body': + 'Je relie l’ingénierie produit senior à un parcours universitaire en mathématiques computationnelles et en informatique. Mon travail couvre React et React Native, les architectures évolutives en marque blanche, l’automatisation, la qualité, l’optimisation et la vision par ordinateur.', + 'experience.eyebrow': 'Carrière', + 'experience.title': 'Expérience professionnelle', + 'experience.intro': + 'Un parcours web et mobile façonné par des produits durables, une architecture réutilisable et des livraisons testables.', + 'experience.abilitya.role': 'Ingénieur logiciel senior', + 'experience.abilitya.employment': 'Temps plein', + 'experience.abilitya.location': 'Milan, Italie · À distance', + 'experience.abilitya.summary': + 'Ingénierie de produits évolutifs dans un écosystème React et React Native pour des clients internationaux.', + 'experience.abilitya.bulletScale': + 'Je fais évoluer des fondations réutilisables en marque blanche afin que plusieurs produits progressent sans dupliquer le travail essentiel.', + 'experience.abilitya.bulletAutomation': + 'Je crée des automatisations d’ingénierie et de produit qui rendent les livraisons plus cohérentes.', + 'experience.abilitya.bulletQuality': + 'J’améliore la testabilité et la couverture de bout en bout sur le web et le mobile avec Cypress et Maestro.', + 'experience.abilitya.bulletCagliari': + 'Je partage la responsabilité de l’application officielle de Cagliari Calcio, utilisée par plus de 10 000 personnes sur Android et iOS.', + 'experience.pluritech.role': 'Développeur frontend', + 'experience.pluritech.location': 'Belo Horizonte, Brésil', + 'experience.pluritech.summary': + 'J’ai développé des interfaces web et mobiles multiplateformes avec Angular et Ionic, posant les bases frontend de ma carrière.', + 'work.eyebrow': 'Réalisations choisies', + 'work.title': 'Des produits en production et de la recherche sur le terrain.', + 'work.intro': + 'Deux résultats qui réunissent responsabilité produit, échelle mobile, données et recherche appliquée.', + 'work.cagliari.kind': 'Application en production · Android & iOS', + 'work.cagliari.title': 'Application officielle de Cagliari Calcio', + 'work.cagliari.summary': + 'Je suis l’un des ingénieurs responsables de l’expérience officielle des supporters : actualités, suivi des matchs, contenus exclusifs, événements et fonctions communautaires.', + 'work.cagliari.metric': 'Plus de 10 000 utilisateurs', + 'work.cagliari.metricLabel': 'sur Android et iOS', + 'work.cagliari.appStore': 'Voir dans l’App Store', + 'work.cagliari.playStore': 'Voir sur Google Play', + 'work.paper.kind': 'Article accepté · MLSA 2026', + 'work.paper.title': + 'Role Vectors: Tracking-Based Representations of Football Players’ Tactical Behavior', + 'work.paper.summary': + 'J’ai coécrit cette représentation fondée sur le suivi des rôles tactiques relatifs des joueurs de football, avec des profils interprétables en possession et sans ballon.', + 'work.paper.metric': 'Présentation internationale', + 'work.paper.metricLabel': 'Naples, Italie · 7 septembre 2026', + 'work.paper.paperLink': 'Lire l’article', + 'work.paper.scheduleLink': 'Voir le programme de la conférence', + 'education.eyebrow': 'Parcours universitaire', + 'education.title': 'Formation', + 'education.computerScience.degree': 'Licence en informatique', + 'education.computerScience.status': 'Diplôme prévu en 2027', + 'education.computationalMath.degree': 'Licence en mathématiques computationnelles', + 'education.computationalMath.status': 'Diplôme obtenu en 2023', + 'education.technician.degree': 'Technicien en informatique', + 'education.technician.institution': 'Colégio Técnico da UFMG (COLTEC)', + 'education.technician.status': 'Diplôme obtenu en 2017', + 'projects.eyebrow': 'Projets', + 'projects.title': 'Des expériences rendues concrètes.', + 'projects.intro': + 'Une sélection de produits et de réalisations universitaires. Le rangement de GitHub ne change pas cet ordre éditorial.', + 'projects.detail.approach': 'Approche', + 'projects.detail.contribution': 'Contribution', + 'projects.detail.outcome': 'Résultat', + 'projects.private': 'Projet privé', + 'projects.jig.status': 'Projet actif', + 'projects.jig.title': 'Jig Solver', + 'projects.jig.summary': + 'Une plateforme de vision par ordinateur qui numérise les pièces d’un puzzle, évalue leur compatibilité, reconstruit le puzzle et guide l’assemblage physique grâce à une caméra.', + 'projects.jig.imageAlt': 'Espace de travail de Jig Solver avec des pièces analysées', + 'projects.jig.approach': + 'Un client Next.js typé coordonne un moteur de résolution Python et FastAPI, l’analyse OpenCV, les placements persistés et un espace de travail Three.js.', + 'projects.jig.contribution': + 'J’ai conçu et réalisé l’architecture du produit, le flux d’analyse des pièces, le cycle de reconstruction et l’expérience d’assemblage guidée par caméra.', + 'projects.jig.outcome': + 'Le produit opérationnel analyse les pièces, classe leur compatibilité, conserve les solutions et guide l’assemblage physique avec un suivi vidéo stable.', + 'projects.jig.link': 'Ouvrir jigsolver.app', + 'projects.planner.title': 'Planner', + 'projects.planner.summary': + 'Un tableau de bord privé et installable pour les finances personnelles, avec un assistant sécurisé pour comprendre dépenses, revenus, trésorerie, dettes et projets d’achat.', + 'projects.planner.approach': + 'Une application Next.js exportée statiquement utilise Firebase Auth, Firestore et des Functions appelables. Des résumés déterministes limitent le contexte de l’assistant BYOK indépendant du fournisseur.', + 'projects.planner.contribution': + 'J’ai construit le produit de bout en bout, des modèles financiers et parcours adaptatifs à l’isolation Firestore, au chiffrement des identifiants, aux tests et à l’automatisation des livraisons.', + 'projects.planner.outcome': + 'La PWA réunit dépenses récurrentes, analyses, rapports, projections de financement, objectifs et conseils IA en lecture seule dans un espace protégé.', + 'projects.simplex.title': 'Simplex', + 'projects.simplex.summary': + 'Un solveur de programmation linéaire en ligne de commande qui classe les problèmes optimaux, irréalisables et non bornés.', + 'projects.simplex.approach': + 'Une implémentation du tableau en deux phases utilise un problème auxiliaire, la règle de Bland, une tolérance numérique et des certificats explicites.', + 'projects.simplex.contribution': + 'J’ai implémenté l’analyse des entrées, les pivots, la construction de la base, la sérialisation des résultats et la validation des certificats avec Python et NumPy.', + 'projects.simplex.outcome': + 'Un vaste corpus de cas couvre des programmes linéaires réalisables, irréalisables, non bornés, dégénérés et plus difficiles.', + 'projects.pokemon.title': 'Pokémon Base', + 'projects.pokemon.summary': + 'Une application web de type Pokédex pour répertorier, interroger et examiner les Pokémon et leurs évolutions.', + 'projects.pokemon.approach': + 'Des modules Angular 7 chargés à la demande séparent listes, requêtes et détails, avec un service HTTP typé et des composants de présentation réutilisables.', + 'projects.pokemon.contribution': + 'J’ai structuré le frontend, l’accès aux données, le routage, les filtres, les graphiques, les vues détaillées et les tests de composants.', + 'projects.pokemon.outcome': + 'Le résultat est une interface cohérente d’exploration à plusieurs vues, plutôt qu’un catalogue statique.', + 'projects.kmeans.title': 'Greedy K-means', + 'projects.kmeans.summary': + 'Une comparaison empirique entre une approximation gloutonne de facteur 2 pour le problème des k-centres et le K-Means classique.', + 'projects.kmeans.approach': + 'L’expérience exécute les deux méthodes sur dix jeux de données et deux distances de Minkowski, en mesurant rayon, silhouette, indice de Rand ajusté et temps.', + 'projects.kmeans.contribution': + 'J’ai implémenté la sélection gloutonne des centres, l’affectation des groupes, le pipeline d’évaluation, les notebooks et les tableaux reproductibles.', + 'projects.kmeans.outcome': + 'Les tableaux par jeu de données et agrégés rendent directement visibles les compromis entre qualité et coût de calcul.', + 'projects.lz78.title': 'Compression LZ78', + 'projects.lz78.summary': + 'Une implémentation de compression de texte sans perte fondée sur l’algorithme de dictionnaire LZ78.', + 'projects.lz78.approach': + 'Un trie compressé stocke les séquences découvertes tandis que deux flux en ligne de commande encodent le texte en Z78 puis le reconstruisent.', + 'projects.lz78.contribution': + 'J’ai implémenté le trie, les pipelines de compression et décompression, la validation des commandes, la gestion des sorties et le corpus de tests.', + 'projects.lz78.outcome': + 'Dix entrées textuelles représentatives et un rapport documentent le comportement aller-retour et les choix d’implémentation.', + 'projects.sourceLink': 'Voir le code', + 'capabilities.eyebrow': 'Compétences', + 'capabilities.title': 'Des outils en contexte, sans barres de progression.', + 'capabilities.product.title': 'Ingénierie produit', + 'capabilities.product.body': 'JavaScript, TypeScript, React, React Native, Angular, Ionic', + 'capabilities.quality.title': 'Qualité & livraison', + 'capabilities.quality.body': 'Cypress, Maestro, automatisation, testabilité, GitHub Actions', + 'capabilities.research.title': 'Recherche appliquée', + 'capabilities.research.body': 'Python, OpenCV, vision par ordinateur, IA, optimisation', + 'capabilities.languages.title': 'Langues', + 'capabilities.languages.body': + 'Portugais — langue maternelle · Anglais — Cambridge C1 · Français — en apprentissage', + 'contact.eyebrow': 'Contact', + 'contact.title': 'Construisons quelque chose de réfléchi.', + 'contact.body': + 'Je suis disponible pour échanger sur l’ingénierie logicielle, les produits mobiles, la vision par ordinateur, l’IA appliquée et les idées de produit ambitieuses.', + 'contact.github': 'Continuer sur GitHub', + 'contact.linkedin': 'Me contacter sur LinkedIn', + 'footer.copy': 'Conçu comme un carnet numérique chaleureux. Réalisé avec Preact et TypeScript.', +} + +export const resources = { + en: { translation: en }, + 'pt-BR': { translation: ptBR }, + fr: { translation: fr }, +} as const diff --git a/src/lib/i18n/components/I18nProvider/I18nProvider.tsx b/src/lib/i18n/components/I18nProvider/I18nProvider.tsx new file mode 100644 index 0000000..c724ade --- /dev/null +++ b/src/lib/i18n/components/I18nProvider/I18nProvider.tsx @@ -0,0 +1,74 @@ +import type { TOptions } from 'i18next' +import { type ComponentChildren, createContext } from 'preact' +import { useContext, useEffect, useMemo, useState } from 'preact/hooks' +import type { TranslationKey } from '@/lib/i18n/catalog' +import { i18next, translate } from '@/lib/i18n/i18n' +import { detectInitialLocale, type Locale, parseLocale, persistLocale } from '@/lib/i18n/locale' + +type I18nContextValue = { + locale: Locale + setLocale: (locale: Locale) => void + t: (key: TranslationKey, options?: TOptions) => string +} + +const I18nContext = createContext(null) + +const updateMetaContent = (selector: string, content: string): void => { + const element = document.head.querySelector(selector) + if (!element) return + element.content = content +} + +const syncDocumentMetadata = (locale: Locale): void => { + const openGraphLocales: Readonly> = { + en: 'en_US', + 'pt-BR': 'pt_BR', + fr: 'fr_FR', + } + document.documentElement.lang = locale + document.title = translate('metadata.title') + updateMetaContent('meta[name="description"]', translate('metadata.description')) + updateMetaContent('meta[property="og:title"]', translate('metadata.title')) + updateMetaContent('meta[property="og:description"]', translate('metadata.description')) + updateMetaContent('meta[property="og:locale"]', openGraphLocales[locale]) + updateMetaContent('meta[name="twitter:title"]', translate('metadata.title')) + updateMetaContent('meta[name="twitter:description"]', translate('metadata.description')) +} + +type I18nProviderProps = { + children: ComponentChildren +} + +export const I18nProvider = ({ children }: I18nProviderProps) => { + const [locale, setLocaleState] = useState(detectInitialLocale) + + useEffect(() => { + const handleLanguageChange = (language: string): void => { + const nextLocale = parseLocale(language) + if (!nextLocale) return + setLocaleState(nextLocale) + syncDocumentMetadata(nextLocale) + } + + i18next.on('languageChanged', handleLanguageChange) + syncDocumentMetadata(locale) + return () => { + i18next.off('languageChanged', handleLanguageChange) + } + }, [locale]) + + const setLocale = (nextLocale: Locale): void => { + persistLocale(nextLocale) + void i18next.changeLanguage(nextLocale) + } + + const value = useMemo(() => ({ locale, setLocale, t: translate }), [locale]) + + return {children} +} + +export const useI18n = (): I18nContextValue => { + const context = useContext(I18nContext) + if (!context) throw new Error('useI18n must be used within I18nProvider.') + return context +} diff --git a/src/lib/i18n/components/I18nProvider/index.ts b/src/lib/i18n/components/I18nProvider/index.ts new file mode 100644 index 0000000..0cfb733 --- /dev/null +++ b/src/lib/i18n/components/I18nProvider/index.ts @@ -0,0 +1 @@ +export { I18nProvider, useI18n } from './I18nProvider' diff --git a/src/lib/i18n/i18n.ts b/src/lib/i18n/i18n.ts new file mode 100644 index 0000000..a0552be --- /dev/null +++ b/src/lib/i18n/i18n.ts @@ -0,0 +1,23 @@ +import i18next, { type TOptions } from 'i18next' +import { en, resources, type TranslationKey } from './catalog' +import { detectInitialLocale, fallbackLocale } from './locale' + +const initialLocale = detectInitialLocale() + +void i18next.init({ + lng: initialLocale, + fallbackLng: fallbackLocale, + supportedLngs: Object.keys(resources), + resources, + keySeparator: false, + interpolation: { + escapeValue: false, + }, + showSupportNotice: false, + initAsync: false, +}) + +export const translate = (key: TranslationKey, options?: TOptions): string => + i18next.t(key, en[key], options ?? {}) + +export { i18next } diff --git a/src/lib/i18n/index.ts b/src/lib/i18n/index.ts new file mode 100644 index 0000000..d8ea577 --- /dev/null +++ b/src/lib/i18n/index.ts @@ -0,0 +1,3 @@ +export type { TranslationKey } from './catalog' +export { I18nProvider, useI18n } from './components/I18nProvider' +export type { Locale } from './locale' diff --git a/src/lib/i18n/locale.test.ts b/src/lib/i18n/locale.test.ts new file mode 100644 index 0000000..822f0b2 --- /dev/null +++ b/src/lib/i18n/locale.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest' +import { resolveLocale } from './locale' + +describe('resolveLocale', () => { + it('prefers a valid persisted override', () => { + expect(resolveLocale({ storedLocale: 'fr', browserLocales: ['pt-BR'] })).toBe('fr') + }) + + it('matches exact and base browser locales', () => { + expect(resolveLocale({ browserLocales: ['pt-PT'] })).toBe('pt-BR') + expect(resolveLocale({ browserLocales: ['fr-CA'] })).toBe('fr') + }) + + it('falls back to English for missing or unsupported values', () => { + expect(resolveLocale({ browserLocales: ['de-DE'] })).toBe('en') + expect(resolveLocale({ storedLocale: 'invalid', browserLocales: [] })).toBe('en') + }) +}) diff --git a/src/lib/i18n/locale.ts b/src/lib/i18n/locale.ts new file mode 100644 index 0000000..c35d511 --- /dev/null +++ b/src/lib/i18n/locale.ts @@ -0,0 +1,78 @@ +export const supportedLocales = ['en', 'pt-BR', 'fr'] as const +export type Locale = (typeof supportedLocales)[number] + +export const fallbackLocale: Locale = 'en' +export const localeStorageKey = 'portfolio.locale' + +const exactLocaleMatches: Readonly> = { + en: 'en', + 'pt-br': 'pt-BR', + fr: 'fr', +} + +const baseLocaleMatches: Readonly> = { + en: 'en', + pt: 'pt-BR', + fr: 'fr', +} + +export const parseLocale = (candidate: string | null | undefined): Locale | null => { + if (!candidate) return null + return exactLocaleMatches[candidate.toLowerCase()] ?? null +} + +const matchBrowserLocale = (candidate: string): Locale | null => { + const normalizedCandidate = candidate.toLowerCase() + const exactMatch = exactLocaleMatches[normalizedCandidate] + if (exactMatch) return exactMatch + const [baseLanguage] = normalizedCandidate.split('-') + if (!baseLanguage) return null + return baseLocaleMatches[baseLanguage] ?? null +} + +type ResolveLocaleOptions = { + storedLocale?: string | null + browserLocales?: readonly string[] +} + +export const resolveLocale = ({ + storedLocale, + browserLocales = [], +}: ResolveLocaleOptions): Locale => { + const parsedStoredLocale = parseLocale(storedLocale) + if (parsedStoredLocale) return parsedStoredLocale + const browserMatch = browserLocales.map(matchBrowserLocale).find(Boolean) + return browserMatch ?? fallbackLocale +} + +const readStoredLocale = (): string | null => { + if (typeof window === 'undefined') return null + try { + return window.localStorage.getItem(localeStorageKey) + } catch { + return null + } +} + +const readBrowserLocales = (): readonly string[] => { + if (typeof navigator === 'undefined') return [] + const languages = navigator.languages?.filter(Boolean) ?? [] + if (languages.length > 0) return languages + if (navigator.language) return [navigator.language] + return [] +} + +export const detectInitialLocale = (): Locale => + resolveLocale({ + storedLocale: readStoredLocale(), + browserLocales: readBrowserLocales(), + }) + +export const persistLocale = (locale: Locale): void => { + if (typeof window === 'undefined') return + try { + window.localStorage.setItem(localeStorageKey, locale) + } catch { + return + } +} diff --git a/src/lib/theme/components/ThemeProvider/ThemeProvider.tsx b/src/lib/theme/components/ThemeProvider/ThemeProvider.tsx new file mode 100644 index 0000000..62730f0 --- /dev/null +++ b/src/lib/theme/components/ThemeProvider/ThemeProvider.tsx @@ -0,0 +1,60 @@ +import { type ComponentChildren, createContext } from 'preact' +import { useContext, useEffect, useMemo, useState } from 'preact/hooks' +import { + detectInitialThemeChoice, + persistThemeChoice, + type ResolvedTheme, + readSystemPreference, + resolveTheme, + type ThemeChoice, +} from '@/lib/theme/theme' + +type ThemeContextValue = { + choice: ThemeChoice + theme: ResolvedTheme + setChoice: (choice: ThemeChoice) => void +} + +const ThemeContext = createContext(null) + +type ThemeProviderProps = { + children: ComponentChildren +} + +export const ThemeProvider = ({ children }: ThemeProviderProps) => { + const [choice, setChoiceState] = useState(detectInitialThemeChoice) + const [systemPrefersDark, setSystemPrefersDark] = useState(readSystemPreference) + const theme = resolveTheme(choice, systemPrefersDark) + + useEffect(() => { + document.documentElement.dataset.theme = theme + const themeColor = theme === 'dark' ? '#1b1917' : '#fdf9f4' + const themeMeta = document.head.querySelector('meta[name="theme-color"]') + if (themeMeta) themeMeta.content = themeColor + }, [theme]) + + useEffect(() => { + if (typeof window.matchMedia !== 'function') return + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + const handlePreferenceChange = (event: MediaQueryListEvent): void => { + setSystemPrefersDark(event.matches) + } + mediaQuery.addEventListener('change', handlePreferenceChange) + return () => mediaQuery.removeEventListener('change', handlePreferenceChange) + }, []) + + const setChoice = (nextChoice: ThemeChoice): void => { + persistThemeChoice(nextChoice) + setChoiceState(nextChoice) + } + + const value = useMemo(() => ({ choice, theme, setChoice }), [choice, theme]) + + return {children} +} + +export const useTheme = (): ThemeContextValue => { + const context = useContext(ThemeContext) + if (!context) throw new Error('useTheme must be used within ThemeProvider.') + return context +} diff --git a/src/lib/theme/components/ThemeProvider/index.ts b/src/lib/theme/components/ThemeProvider/index.ts new file mode 100644 index 0000000..0b66695 --- /dev/null +++ b/src/lib/theme/components/ThemeProvider/index.ts @@ -0,0 +1 @@ +export { ThemeProvider, useTheme } from './ThemeProvider' diff --git a/src/lib/theme/index.ts b/src/lib/theme/index.ts new file mode 100644 index 0000000..5b7a958 --- /dev/null +++ b/src/lib/theme/index.ts @@ -0,0 +1,2 @@ +export { ThemeProvider, useTheme } from './components/ThemeProvider' +export type { ResolvedTheme, ThemeChoice } from './theme' diff --git a/src/lib/theme/theme.test.ts b/src/lib/theme/theme.test.ts new file mode 100644 index 0000000..03aa953 --- /dev/null +++ b/src/lib/theme/theme.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest' +import { parseThemeChoice, resolveTheme } from './theme' + +describe('theme resolution', () => { + it('uses an explicit visitor choice', () => { + expect(resolveTheme('dark', false)).toBe('dark') + expect(resolveTheme('light', true)).toBe('light') + }) + + it('uses the system preference in auto mode', () => { + expect(resolveTheme('auto', true)).toBe('dark') + expect(resolveTheme('auto', false)).toBe('light') + }) + + it('falls back to light when the system preference is unavailable', () => { + expect(resolveTheme('auto', null)).toBe('light') + expect(parseThemeChoice('invalid')).toBeNull() + }) +}) diff --git a/src/lib/theme/theme.ts b/src/lib/theme/theme.ts new file mode 100644 index 0000000..f906fd3 --- /dev/null +++ b/src/lib/theme/theme.ts @@ -0,0 +1,48 @@ +export const themeChoices = ['auto', 'light', 'dark'] as const +export type ThemeChoice = (typeof themeChoices)[number] +export type ResolvedTheme = Exclude + +export const themeStorageKey = 'portfolio.theme' + +export const parseThemeChoice = (candidate: string | null | undefined): ThemeChoice | null => { + if (candidate === 'auto' || candidate === 'light' || candidate === 'dark') return candidate + return null +} + +export const resolveTheme = ( + choice: ThemeChoice, + systemPrefersDark: boolean | null, +): ResolvedTheme => { + if (choice === 'light' || choice === 'dark') return choice + if (systemPrefersDark) return 'dark' + return 'light' +} + +const readStoredTheme = (): ThemeChoice | null => { + if (typeof window === 'undefined') return null + try { + return parseThemeChoice(window.localStorage.getItem(themeStorageKey)) + } catch { + return null + } +} + +export const readSystemPreference = (): boolean | null => { + if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return null + return window.matchMedia('(prefers-color-scheme: dark)').matches +} + +export const detectInitialThemeChoice = (): ThemeChoice => readStoredTheme() ?? 'auto' + +export const persistThemeChoice = (choice: ThemeChoice): void => { + if (typeof window === 'undefined') return + try { + if (choice === 'auto') { + window.localStorage.removeItem(themeStorageKey) + return + } + window.localStorage.setItem(themeStorageKey, choice) + } catch { + return + } +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..263bb1c --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,18 @@ +import '@fontsource/lora/latin-400.css' +import '@fontsource/lora/latin-500.css' +import '@fontsource/lora/latin-700.css' +import '@fontsource/public-sans/latin-400.css' +import '@fontsource/public-sans/latin-500.css' +import '@fontsource/public-sans/latin-600.css' +import '@fontsource/public-sans/latin-700.css' +import { hydrate, render } from 'preact' +import { App } from './App' +import './styles/tokens.css' +import './styles/base.css' +import './styles/components.css' + +const appRoot = document.getElementById('app') +if (!appRoot) throw new Error('Application root was not found.') + +const mount = appRoot.hasChildNodes() ? hydrate : render +mount(, appRoot) diff --git a/src/prerender.tsx b/src/prerender.tsx new file mode 100644 index 0000000..5a4d160 --- /dev/null +++ b/src/prerender.tsx @@ -0,0 +1,11 @@ +import renderToString from 'preact-render-to-string' +import { App } from '@/App' +import { en } from '@/lib/i18n/catalog' + +export const prerender = () => ({ + html: renderToString(), + head: { + lang: 'en', + title: en['metadata.title'], + }, +}) diff --git a/src/styles/base.css b/src/styles/base.css new file mode 100644 index 0000000..47d6662 --- /dev/null +++ b/src/styles/base.css @@ -0,0 +1,140 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; + scroll-padding-top: 6rem; +} + +body { + margin: 0; + min-width: 20rem; + min-height: 100vh; + overflow-x: hidden; + background: var(--color-canvas); + color: var(--color-text); + font-family: var(--font-body); + font-size: 1rem; + line-height: 1.65; + text-rendering: optimizeLegibility; +} + +button, +select { + color: inherit; + font: inherit; +} + +button, +select, +a { + -webkit-tap-highlight-color: transparent; +} + +a { + color: inherit; +} + +img { + display: block; + max-width: 100%; +} + +h1, +h2, +h3, +p, +ul { + margin-top: 0; +} + +h1, +h2, +h3 { + font-family: var(--font-display); + font-weight: 500; + line-height: 1.18; + text-wrap: balance; +} + +h1 { + margin-bottom: 1.5rem; + font-size: clamp(2.65rem, 7vw, 4rem); + letter-spacing: -0.045em; +} + +h2 { + margin-bottom: 1rem; + font-size: clamp(2rem, 5vw, 2.75rem); + letter-spacing: -0.035em; +} + +h3 { + margin-bottom: 0.75rem; + font-size: clamp(1.35rem, 3vw, 1.65rem); + letter-spacing: -0.02em; +} + +p { + color: var(--color-text-secondary); +} + +::selection { + background: var(--color-accent); + color: var(--color-accent-text); +} + +:focus-visible { + outline: 3px solid var(--color-focus); + outline-offset: 4px; +} + +.page-shell { + width: min(calc(100% - 2 * var(--page-gutter)), var(--page-width)); + margin-inline: auto; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.skip-link { + position: fixed; + top: 0.75rem; + left: 0.75rem; + z-index: 100; + padding: 0.75rem 1rem; + transform: translateY(-160%); + border-radius: var(--radius-pill); + background: var(--color-text); + color: var(--color-canvas); + font-weight: 700; + transition: transform var(--transition-fast); +} + +.skip-link:focus { + transform: translateY(0); +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + *, + *::before, + *::after { + scroll-behavior: auto; + transition-duration: 0.01ms; + } +} diff --git a/src/styles/components.css b/src/styles/components.css new file mode 100644 index 0000000..d21373a --- /dev/null +++ b/src/styles/components.css @@ -0,0 +1,674 @@ +.app-header { + position: sticky; + top: 0; + z-index: 20; + border-bottom: 1px solid var(--color-border); + background: var(--color-header); + backdrop-filter: blur(14px); +} + +.header-inner { + display: flex; + min-height: 4.75rem; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.brand { + display: inline-flex; + min-height: 2.75rem; + align-items: center; + gap: 0.75rem; + font-weight: 700; + text-decoration: none; +} + +.brand-mark { + display: inline-grid; + width: 2.5rem; + height: 2.5rem; + place-items: center; + border: 1px solid var(--color-border-strong); + border-radius: 50%; + font-family: var(--font-display); + font-size: 0.8rem; +} + +.desktop-nav { + display: flex; + align-items: center; + gap: clamp(0.7rem, 1.6vw, 1.4rem); +} + +.desktop-nav a, +.mobile-nav nav a { + color: var(--color-text-secondary); + font-size: 0.875rem; + font-weight: 600; + text-decoration: none; + transition: color var(--transition-fast); +} + +.desktop-nav a:hover, +.mobile-nav nav a:hover, +.text-link:hover { + color: var(--color-accent-strong); +} + +.header-controls { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.select-control select { + min-height: 2.75rem; + padding: 0.5rem 2rem 0.5rem 0.8rem; + border: 1px solid var(--color-border-strong); + border-radius: var(--radius-pill); + background-color: var(--color-canvas); + color: var(--color-text-secondary); + font-size: 0.8rem; + font-weight: 600; +} + +.theme-control select { + max-width: 6.5rem; +} + +.mobile-nav { + position: relative; + display: none; +} + +.mobile-nav summary { + display: grid; + width: 2.75rem; + height: 2.75rem; + cursor: pointer; + list-style: none; + place-items: center; + border: 1px solid var(--color-border-strong); + border-radius: 50%; + font-size: 1.5rem; +} + +.mobile-nav summary::-webkit-details-marker { + display: none; +} + +.mobile-nav nav { + position: absolute; + top: calc(100% + 0.75rem); + right: 0; + display: grid; + min-width: 13rem; + gap: 0.25rem; + padding: 0.75rem; + border: 1px solid var(--color-border); + border-radius: 1rem; + background: var(--color-elevated); + box-shadow: var(--shadow-subtle); +} + +.mobile-nav nav a { + min-height: 2.75rem; + padding: 0.65rem 0.8rem; + border-radius: 0.5rem; +} + +.mobile-nav nav a:hover { + background: var(--color-dust); +} + +.hero-section { + display: flex; + min-height: min(51rem, calc(100vh - 4.75rem)); + flex-direction: column; + align-items: center; + justify-content: center; + padding-block: clamp(4rem, 9vw, 7.5rem); + text-align: center; +} + +.avatar { + width: clamp(8rem, 18vw, 11rem); + height: clamp(8rem, 18vw, 11rem); + margin-bottom: 1.75rem; + border: 5px solid var(--color-elevated); + border-radius: 50%; + background: var(--color-dust); + box-shadow: var(--shadow-subtle); + object-fit: cover; +} + +.avatar-fallback { + display: grid; + place-items: center; + color: var(--color-text); + font-family: var(--font-display); + font-size: 2.5rem; + font-weight: 700; +} + +.eyebrow, +.card-kicker { + margin-bottom: 0.8rem; + color: var(--color-text-muted); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.hero-section h1 { + max-width: 60rem; +} + +.scribble { + position: relative; + display: inline-block; + z-index: 0; +} + +.scribble::after { + position: absolute; + right: -0.18em; + bottom: -0.12em; + left: -0.18em; + z-index: -1; + height: 0.45em; + border: 2px solid var(--color-accent); + border-top: 0; + border-radius: 50%; + content: ""; + transform: rotate(-1.5deg); +} + +.hero-summary { + max-width: 46rem; + margin-bottom: 1.75rem; + color: var(--color-text-muted); + font-size: clamp(1.05rem, 2vw, 1.25rem); + line-height: 1.6; +} + +.chip-list, +.technology-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + padding: 0; + list-style: none; +} + +.chip-list { + justify-content: center; + margin-bottom: 2rem; +} + +.chip-list li, +.technology-list li { + border-radius: var(--radius-pill); + background: var(--color-dust); + color: var(--color-text-secondary); + font-size: 0.8rem; + font-weight: 600; +} + +.chip-list li { + padding: 0.55rem 1rem; +} + +.technology-list li { + padding: 0.35rem 0.7rem; +} + +.hero-actions, +.card-links, +.contact-links { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.75rem 1.25rem; +} + +.hero-actions { + justify-content: center; +} + +.button, +.text-link { + display: inline-flex; + min-height: 2.75rem; + align-items: center; + justify-content: center; + gap: 0.45rem; + font-weight: 700; + text-decoration: none; +} + +.button { + padding: 0.7rem 1.25rem; + border: 2px solid transparent; + border-radius: var(--radius-pill); +} + +.button-primary { + background: var(--color-accent); + color: var(--color-accent-text); +} + +.button-primary:hover { + background: var(--color-accent-strong); +} + +.button-secondary { + background: var(--color-text); + color: var(--color-canvas); +} + +.button-outline { + border-color: var(--color-text); + color: var(--color-text); +} + +.text-link { + color: var(--color-text-secondary); + text-underline-offset: 0.25rem; +} + +.text-link:hover { + text-decoration: underline; +} + +.content-section { + padding-block: var(--section-space); +} + +.section-heading { + max-width: 48rem; + margin-bottom: clamp(2.25rem, 5vw, 4rem); +} + +.section-intro { + max-width: 42rem; + margin-bottom: 0; + color: var(--color-text-muted); + font-size: 1.08rem; +} + +.about-copy { + max-width: 55rem; + margin-bottom: 0; + font-family: var(--font-display); + font-size: clamp(1.45rem, 3.5vw, 2.2rem); + line-height: 1.45; +} + +.timeline { + position: relative; + display: grid; + gap: 2rem; +} + +.timeline::before { + position: absolute; + top: 1rem; + bottom: 1rem; + left: 0.5rem; + width: 1px; + background: var(--color-border-strong); + content: ""; +} + +.timeline-item { + position: relative; + display: grid; + grid-template-columns: 1rem minmax(0, 1fr); + gap: 1.5rem; +} + +.timeline-marker { + z-index: 1; + width: 1rem; + height: 1rem; + margin-top: 1rem; + border: 3px solid var(--color-canvas); + border-radius: 50%; + background: var(--color-accent); + box-shadow: 0 0 0 1px var(--color-accent); +} + +.timeline-card, +.education-card, +.project-card, +.capability-card { + border-radius: var(--radius-card); + background: var(--color-dust); +} + +.timeline-card { + padding: clamp(1.5rem, 4vw, 2.5rem); +} + +.timeline-card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1.5rem; +} + +.company-line, +.date-label, +.location-label, +.status-line { + color: var(--color-text-muted); + font-size: 0.875rem; +} + +.company-line { + margin-bottom: 0.4rem; + font-weight: 700; +} + +.date-label { + flex: none; + margin-bottom: 0.75rem; + font-weight: 700; +} + +.location-label { + margin-bottom: 1.25rem; +} + +.impact-list { + display: grid; + gap: 0.75rem; + padding-left: 1.25rem; + color: var(--color-text-secondary); +} + +.impact-list li::marker { + color: var(--color-accent); +} + +.technology-list { + margin-block: 1.5rem 0; +} + +.selected-work-section { + background: var(--color-dust); +} + +.work-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.25rem; +} + +.work-card { + display: flex; + min-width: 0; + flex-direction: column; + padding: clamp(1.5rem, 4vw, 2.75rem); + border: 1px solid var(--color-border); + border-radius: var(--radius-card); + background: var(--color-elevated); +} + +.work-card-featured { + border-left: 3px solid var(--color-accent); +} + +.work-card > p:not(.card-kicker) { + flex: 1; +} + +.metric-block { + display: grid; + margin-block: 1rem 1.5rem; + gap: 0.15rem; +} + +.metric-block strong { + font-family: var(--font-display); + font-size: clamp(1.4rem, 3vw, 2rem); + font-weight: 500; +} + +.metric-block span { + color: var(--color-text-muted); + font-size: 0.85rem; +} + +.education-grid, +.project-grid, +.capability-grid { + display: grid; + gap: 1rem; +} + +.education-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.education-card, +.project-card, +.capability-card { + padding: clamp(1.35rem, 3vw, 2rem); +} + +.education-card p:last-child, +.project-card p:last-child, +.capability-card p:last-child { + margin-bottom: 0; +} + +.status-line { + margin-bottom: 0; + font-weight: 700; +} + +.jig-card { + display: grid; + grid-template-columns: minmax(16rem, 0.8fr) minmax(0, 1.2fr); + align-items: center; + gap: clamp(1.5rem, 5vw, 4rem); + margin-bottom: 1.25rem; + padding: clamp(1.5rem, 4vw, 3rem); + border-radius: var(--radius-card); + background: var(--color-dust); +} + +.jig-copy .button { + margin-top: 0.5rem; +} + +.product-frame { + overflow: hidden; + border-radius: var(--radius-product); + background: var(--color-elevated); + box-shadow: var(--shadow-subtle); +} + +.product-frame img { + width: 100%; + height: auto; +} + +.project-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.capability-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.project-card, +.capability-card { + display: flex; + min-width: 0; + flex-direction: column; +} + +.project-summary { + color: var(--color-text-secondary); + font-size: 1.02rem; +} + +.project-details { + display: grid; + flex: 1; + gap: 1rem; + margin-block: 1.5rem; +} + +.project-details div { + padding-top: 0.85rem; + border-top: 1px solid var(--color-border); +} + +.project-details dt { + margin-bottom: 0.25rem; + color: var(--color-text); + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.project-details dd { + margin: 0; + color: var(--color-text-muted); + font-size: 0.9rem; + line-height: 1.55; +} + +.project-card .text-link { + align-self: flex-start; +} + +.capability-card { + border-top: 2px solid var(--color-accent); +} + +.contact-section { + padding-block: var(--section-space); +} + +.contact-inner { + padding: clamp(2rem, 7vw, 5rem); + border-radius: var(--radius-card); + background: var(--color-text); + color: var(--color-canvas); +} + +.contact-inner h2, +.contact-inner .eyebrow, +.contact-inner p { + color: inherit; +} + +.contact-inner p:not(.eyebrow) { + max-width: 45rem; + font-size: 1.08rem; +} + +.contact-inner .button-outline { + border-color: var(--color-canvas); + color: var(--color-canvas); +} + +.page-footer { + padding-block: 2rem; + border-top: 1px solid var(--color-border); + color: var(--color-text-muted); + font-size: 0.8rem; +} + +.page-footer .page-shell { + display: flex; + justify-content: space-between; + gap: 1rem; +} + +@media (max-width: 68rem) { + .desktop-nav { + display: none; + } + + .mobile-nav { + display: block; + } + + .project-grid, + .capability-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 48rem) { + .header-inner { + gap: 0.5rem; + } + + .brand > span:last-child { + display: none; + } + + .header-controls { + gap: 0.35rem; + } + + .select-control select { + max-width: 5.75rem; + padding-right: 1.25rem; + padding-left: 0.55rem; + font-size: 0.72rem; + } + + .theme-control select { + max-width: 5rem; + } + + .work-grid, + .education-grid, + .jig-card { + grid-template-columns: 1fr; + } + + .jig-copy { + order: 1; + } + + .timeline-card-header { + display: block; + } + + .page-footer .page-shell { + flex-direction: column; + } +} + +@media (max-width: 36rem) { + .project-grid, + .capability-grid { + grid-template-columns: 1fr; + } + + .timeline-item { + gap: 0.9rem; + } + + .hero-actions, + .contact-links { + align-items: stretch; + flex-direction: column; + } + + .hero-actions .text-link { + min-height: 2.75rem; + } +} diff --git a/src/styles/tokens.css b/src/styles/tokens.css new file mode 100644 index 0000000..097126a --- /dev/null +++ b/src/styles/tokens.css @@ -0,0 +1,45 @@ +:root { + color-scheme: light; + --color-canvas: #fdf9f4; + --color-elevated: #ffffff; + --color-dust: #f9efe4; + --color-border: #d9dde6; + --color-border-strong: #bec3cc; + --color-text: #2d2f34; + --color-text-secondary: #3f434a; + --color-text-muted: #5e646e; + --color-accent: #f67748; + --color-accent-strong: #d95627; + --color-accent-text: #2d2f34; + --color-focus: #9b3b19; + --color-header: rgb(253 249 244 / 92%); + --shadow-subtle: + 0 1px 3px rgb(0 0 0 / 10%), 0 2px 6px rgb(0 0 0 / 5%), 0 4px 12px rgb(0 0 0 / 1%); + --font-display: "Lora", Georgia, serif; + --font-body: "Public Sans", system-ui, sans-serif; + --radius-card: 2rem; + --radius-product: 0.75rem; + --radius-pill: 999px; + --page-width: 75rem; + --section-space: clamp(4.5rem, 9vw, 7.5rem); + --page-gutter: clamp(1rem, 4vw, 2rem); + --transition-fast: 130ms ease; +} + +:root[data-theme="dark"] { + color-scheme: dark; + --color-canvas: #1b1917; + --color-elevated: #24211f; + --color-dust: #302a26; + --color-border: #47413d; + --color-border-strong: #625a54; + --color-text: #f5eee7; + --color-text-secondary: #e3d8ce; + --color-text-muted: #c9beb4; + --color-accent: #ff895e; + --color-accent-strong: #ffad8f; + --color-accent-text: #201b18; + --color-focus: #ffad8f; + --color-header: rgb(27 25 23 / 92%); + --shadow-subtle: 0 1px 3px rgb(0 0 0 / 35%), 0 3px 12px rgb(0 0 0 / 20%); +} diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..a9d0dd3 --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest' diff --git a/style.css b/style.css deleted file mode 100644 index e494e14..0000000 --- a/style.css +++ /dev/null @@ -1,73 +0,0 @@ -#imagePerfil{ - border-radius: 50% !important; -} - -#leftside{ - background-color: #373238; -} - -.profissao{ - margin-bottom: 10% !important; -} - -body{ - font-family: 'Titillium Web', sans-serif !important; -} - -.borda{ - border-style: solid !important; - border-color: #373238 !important; -} - -.titulos{ - color: #A12C3E !important; -} - -.conteudo{ - color: #9E9E9E !important; -} - -#conteudoEsquerda{ - margin-top: 20% !important; - padding: 5% !important; -} - -#conteudoDireita{ - padding: 10% !important; -} - -.subtitulo{ - text-decoration: underline !important; -} - -#github, #linkedin{ - color: white !important; - background-color: transparent !important; - text-decoration: none !important; -} - -.progresso { - overflow: hidden; - height: 10px; - background-color: white; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,.1); - box-shadow: inset 0 1px 2px rgba(0,0,0,.1); - margin-bottom: 20px; -} -.progresso-bar { - width: 0; - height: 100%; - color: #fff; - text-align: center; - background-color: #A12C3E; -} - -i{ - width: 100%; - height: auto; -} - -.hobbies{ - color: black !important; -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..dd244a0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "allowImportingTsExtensions": false, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "jsxImportSource": "preact", + "strict": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + "types": ["vite/client", "vitest/globals"] + }, + "include": ["src", "vite.config.ts", "playwright.config.ts", "e2e"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..07bc85d --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,38 @@ +import { fileURLToPath, URL } from 'node:url' +import preact from '@preact/preset-vite' +import { defineConfig } from 'vite' + +export default defineConfig({ + base: '/', + plugins: [ + preact({ + prerender: { + enabled: true, + renderTarget: '#app', + prerenderScript: fileURLToPath(new URL('./src/prerender.tsx', import.meta.url)), + previewMiddlewareEnabled: true, + }, + }), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + build: { + rollupOptions: { + output: { + manualChunks: { + 'preact-runtime': ['preact', 'preact/hooks', 'preact/jsx-runtime'], + 'ssr-renderer': ['preact-render-to-string'], + }, + }, + }, + }, + test: { + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'], + include: ['src/**/*.test.{ts,tsx}'], + css: true, + }, +})