From d2198b4051002f03064e735216e6b9bce4a2f2e5 Mon Sep 17 00:00:00 2001 From: Lucas Mariz Date: Fri, 11 Sep 2026 19:08:29 -0300 Subject: [PATCH 1/3] feat: modernize portfolio foundation --- .github/workflows/ci.yml | 41 + .gitignore | 9 + .node-version | 1 + .nvmrc | 1 + AGENTS.md | 90 + README.md | 76 +- biome.json | 40 + commitlint.config.mjs | 3 + docs/DESIGN.md | 464 +++ docs/ENGINEERING_CONVENTIONS.md | 205 + docs/ROADMAP.md | 801 ++++ e2e/portfolio.spec.ts | 17 + index.html | 155 +- lefthook.yml | 9 + package.json | 48 + perfil.jpeg | Bin 32181 -> 0 bytes playwright.config.ts | 25 + pnpm-lock.yaml | 3683 +++++++++++++++++ public/avatar-fallback.svg | 13 + .../projects/jig-solver/solver-workspace.webp | Bin 0 -> 34718 bytes scripts/check-code-conventions.mjs | 64 + scripts/require-pnpm.mjs | 5 + src/App.tsx | 11 + .../PortfolioPage/PortfolioPage.tsx | 38 + .../components/AboutSection/AboutSection.tsx | 12 + .../components/AboutSection/index.ts | 1 + .../components/AppHeader/AppHeader.tsx | 57 + .../components/AppHeader/index.ts | 1 + .../CapabilitiesSection.tsx | 39 + .../components/CapabilitiesSection/index.ts | 1 + .../ContactSection/ContactSection.tsx | 24 + .../components/ContactSection/index.ts | 1 + .../EducationSection/EducationSection.tsx | 22 + .../components/EducationSection/index.ts | 1 + .../ExperienceSection/ExperienceSection.tsx | 52 + .../components/ExperienceSection/index.ts | 1 + .../components/HeroSection/HeroSection.tsx | 41 + .../GitHubAvatar/GitHubAvatar.test.tsx | 17 + .../components/GitHubAvatar/GitHubAvatar.tsx | 25 + .../components/GitHubAvatar/index.ts | 1 + .../components/HeroSection/index.ts | 1 + .../ProjectsSection/ProjectsSection.tsx | 58 + .../components/ProjectsSection/index.ts | 1 + .../SelectedWorkSection.tsx | 55 + .../components/SelectedWorkSection/index.ts | 1 + src/components/PortfolioPage/index.ts | 1 + .../ui/ExternalLink/ExternalLink.tsx | 19 + src/components/ui/ExternalLink/index.ts | 1 + .../ui/LanguageSwitcher/LanguageSwitcher.tsx | 32 + src/components/ui/LanguageSwitcher/index.ts | 1 + .../ui/SectionHeading/SectionHeading.tsx | 13 + src/components/ui/SectionHeading/index.ts | 1 + .../ui/ThemeSwitcher/ThemeSwitcher.tsx | 34 + src/components/ui/ThemeSwitcher/index.ts | 1 + src/content/education.ts | 35 + src/content/experience.ts | 44 + src/content/profile.ts | 7 + src/content/projects.ts | 45 + src/content/selectedWork.ts | 9 + src/lib/date/formatExperiencePeriod.test.ts | 34 + src/lib/date/formatExperiencePeriod.ts | 47 + src/lib/date/index.ts | 1 + src/lib/i18n/catalog.test.ts | 10 + src/lib/i18n/catalog.ts | 430 ++ .../components/I18nProvider/I18nProvider.tsx | 66 + src/lib/i18n/components/I18nProvider/index.ts | 1 + src/lib/i18n/i18n.ts | 23 + src/lib/i18n/index.ts | 3 + src/lib/i18n/locale.test.ts | 18 + src/lib/i18n/locale.ts | 78 + .../ThemeProvider/ThemeProvider.tsx | 60 + .../theme/components/ThemeProvider/index.ts | 1 + src/lib/theme/index.ts | 2 + src/lib/theme/theme.test.ts | 19 + src/lib/theme/theme.ts | 48 + src/main.tsx | 17 + src/styles/base.css | 140 + src/styles/components.css | 642 +++ src/styles/tokens.css | 45 + src/test/setup.ts | 1 + style.css | 73 - tsconfig.json | 24 + vite.config.ts | 19 + 83 files changed, 8064 insertions(+), 192 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .node-version create mode 100644 .nvmrc create mode 100644 AGENTS.md create mode 100644 biome.json create mode 100644 commitlint.config.mjs create mode 100644 docs/DESIGN.md create mode 100644 docs/ENGINEERING_CONVENTIONS.md create mode 100644 docs/ROADMAP.md create mode 100644 e2e/portfolio.spec.ts create mode 100644 lefthook.yml create mode 100644 package.json delete mode 100644 perfil.jpeg create mode 100644 playwright.config.ts create mode 100644 pnpm-lock.yaml create mode 100644 public/avatar-fallback.svg create mode 100644 public/projects/jig-solver/solver-workspace.webp create mode 100644 scripts/check-code-conventions.mjs create mode 100644 scripts/require-pnpm.mjs create mode 100644 src/App.tsx create mode 100644 src/components/PortfolioPage/PortfolioPage.tsx create mode 100644 src/components/PortfolioPage/components/AboutSection/AboutSection.tsx create mode 100644 src/components/PortfolioPage/components/AboutSection/index.ts create mode 100644 src/components/PortfolioPage/components/AppHeader/AppHeader.tsx create mode 100644 src/components/PortfolioPage/components/AppHeader/index.ts create mode 100644 src/components/PortfolioPage/components/CapabilitiesSection/CapabilitiesSection.tsx create mode 100644 src/components/PortfolioPage/components/CapabilitiesSection/index.ts create mode 100644 src/components/PortfolioPage/components/ContactSection/ContactSection.tsx create mode 100644 src/components/PortfolioPage/components/ContactSection/index.ts create mode 100644 src/components/PortfolioPage/components/EducationSection/EducationSection.tsx create mode 100644 src/components/PortfolioPage/components/EducationSection/index.ts create mode 100644 src/components/PortfolioPage/components/ExperienceSection/ExperienceSection.tsx create mode 100644 src/components/PortfolioPage/components/ExperienceSection/index.ts create mode 100644 src/components/PortfolioPage/components/HeroSection/HeroSection.tsx create mode 100644 src/components/PortfolioPage/components/HeroSection/components/GitHubAvatar/GitHubAvatar.test.tsx create mode 100644 src/components/PortfolioPage/components/HeroSection/components/GitHubAvatar/GitHubAvatar.tsx create mode 100644 src/components/PortfolioPage/components/HeroSection/components/GitHubAvatar/index.ts create mode 100644 src/components/PortfolioPage/components/HeroSection/index.ts create mode 100644 src/components/PortfolioPage/components/ProjectsSection/ProjectsSection.tsx create mode 100644 src/components/PortfolioPage/components/ProjectsSection/index.ts create mode 100644 src/components/PortfolioPage/components/SelectedWorkSection/SelectedWorkSection.tsx create mode 100644 src/components/PortfolioPage/components/SelectedWorkSection/index.ts create mode 100644 src/components/PortfolioPage/index.ts create mode 100644 src/components/ui/ExternalLink/ExternalLink.tsx create mode 100644 src/components/ui/ExternalLink/index.ts create mode 100644 src/components/ui/LanguageSwitcher/LanguageSwitcher.tsx create mode 100644 src/components/ui/LanguageSwitcher/index.ts create mode 100644 src/components/ui/SectionHeading/SectionHeading.tsx create mode 100644 src/components/ui/SectionHeading/index.ts create mode 100644 src/components/ui/ThemeSwitcher/ThemeSwitcher.tsx create mode 100644 src/components/ui/ThemeSwitcher/index.ts create mode 100644 src/content/education.ts create mode 100644 src/content/experience.ts create mode 100644 src/content/profile.ts create mode 100644 src/content/projects.ts create mode 100644 src/content/selectedWork.ts create mode 100644 src/lib/date/formatExperiencePeriod.test.ts create mode 100644 src/lib/date/formatExperiencePeriod.ts create mode 100644 src/lib/date/index.ts create mode 100644 src/lib/i18n/catalog.test.ts create mode 100644 src/lib/i18n/catalog.ts create mode 100644 src/lib/i18n/components/I18nProvider/I18nProvider.tsx create mode 100644 src/lib/i18n/components/I18nProvider/index.ts create mode 100644 src/lib/i18n/i18n.ts create mode 100644 src/lib/i18n/index.ts create mode 100644 src/lib/i18n/locale.test.ts create mode 100644 src/lib/i18n/locale.ts create mode 100644 src/lib/theme/components/ThemeProvider/ThemeProvider.tsx create mode 100644 src/lib/theme/components/ThemeProvider/index.ts create mode 100644 src/lib/theme/index.ts create mode 100644 src/lib/theme/theme.test.ts create mode 100644 src/lib/theme/theme.ts create mode 100644 src/main.tsx create mode 100644 src/styles/base.css create mode 100644 src/styles/components.css create mode 100644 src/styles/tokens.css create mode 100644 src/test/setup.ts delete mode 100644 style.css create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9daddd8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +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@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.33.4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + 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/.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..4ac6f02 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,74 @@ -# 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. + +## 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 +- 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/`. Set `VITE_BASE_PATH` when publishing as +a GitHub Pages project site. The recommended repository name is +`LucasPMM.github.io`, which uses the default `/` base path. + +## 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..9e2b6ec --- /dev/null +++ b/biome.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.13/schema.json", + "files": { + "includes": [ + "**", + "!!dist", + "!!coverage", + "!!node_modules", + "!!playwright-report", + "!!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..e23db02 --- /dev/null +++ b/docs/ENGINEERING_CONVENTIONS.md @@ -0,0 +1,205 @@ +# 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` run type checking, Biome, convention + checks, unit tests, responsive browser tests, and a production build. +- Deployment to GitHub Pages may start only after all quality checks succeed. +- Vite outputs static files to `dist`; GitHub Actions publishes that artifact. +- Use `base: '/'` for the recommended `LucasPMM.github.io` repository name, or + `base: '//'` for a project site. +- Keep the workflow permissions minimal: repository contents read, Pages write, + and ID token write for the deploy job. + +## 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..7381406 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,801 @@ +# 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. +- Final summaries and representative evidence for the four public showcase + repositories. + +## 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 five projects: + +1. Jig Solver. +2. Simplex. +3. Pokémon Base. +4. Greedy K-means. +5. 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 Public repositories + +| Display name | Repository | Known focus | Required content before release | +|---|---|---|---| +| Simplex | [`simplex`](https://github.com/LucasPMM/simplex) | Python | Problem, academic context, algorithm, and result | +| Pokémon Base | [`Pokemon-Base`](https://github.com/LucasPMM/Pokemon-Base) | TypeScript | Product goal, architecture, contribution, and screenshot | +| Greedy K-means | [`greedy-kmeans`](https://github.com/LucasPMM/greedy-kmeans) | Jupyter/Python | Experiment question, method, result, and visualization | +| LZ78 Compression | [`lz78-compression`](https://github.com/LucasPMM/lz78-compression) | Python | Algorithm scope, implementation choices, and learning outcome | + +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.webp +│ ├── favicon.svg +│ └── og-cover.png +├── scripts/ +│ ├── check-code-conventions.mjs +│ └── require-pnpm.mjs +├── src/ +│ ├── components/ +│ │ ├── PortfolioPage/ +│ │ │ ├── PortfolioPage.tsx +│ │ │ ├── PortfolioPage.test.tsx +│ │ │ ├── index.ts +│ │ │ └── components/ +│ │ │ ├── AppHeader/ +│ │ │ ├── HeroSection/ +│ │ │ ├── AboutSection/ +│ │ │ ├── ExperienceSection/ +│ │ │ ├── SelectedWorkSection/ +│ │ │ ├── EducationSection/ +│ │ │ ├── ProjectsSection/ +│ │ │ ├── SkillsSection/ +│ │ │ └── ContactSection/ +│ │ └── ui/ +│ │ ├── ExternalLink/ +│ │ ├── Icon/ +│ │ ├── LanguageSwitcher/ +│ │ ├── ProjectCard/ +│ │ ├── SectionHeading/ +│ │ └── ThemeSwitcher/ +│ ├── content/ +│ │ ├── education.ts +│ │ ├── experience.ts +│ │ ├── profile.ts +│ │ └── projects.ts +│ ├── lib/ +│ │ ├── github/avatar.ts +│ │ ├── i18n/ +│ │ │ ├── catalog.ts +│ │ │ ├── translations.json +│ │ │ ├── index.ts +│ │ │ └── components/I18nProvider/ +│ │ └── theme/ +│ │ ├── theme.ts +│ │ ├── index.ts +│ │ └── components/ThemeProvider/ +│ ├── styles/ +│ │ ├── base.css +│ │ ├── components.css +│ │ └── tokens.css +│ ├── App.tsx +│ └── main.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. +- Four smaller 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 │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +│ ┌──── 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 │ +├──────────────────────────┤ +│ four project 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.webp`. 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, one day) + +- Enrich the published ABILITYA and Pluritech timeline with additional + nonconfidential evidence. +- Deepen the five 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 concise evidence-based summaries for the four public repositories. +- Add optional build-time GitHub metadata without making it critical. + +**Exit:** each project explains problem, approach, contribution, and result; +private links and invented metrics are absent. + +### Phase 5 — quality, SEO, and performance (P0, one day) + +- 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. + +### Phase 6 — rename and deploy (P0, half day) + +- Rename the repository and update the local remote. +- Configure the correct Vite base path. +- Enable GitHub Pages through GitHub Actions. +- Update README, repository description/topics, canonical URL, and profile + links. +- Smoke-test the public site and retain Git-history rollback. + +**Exit:** the renamed site is live and all public links use the new URL. + +**Estimated total:** approximately 5–7 focused days, including content, +implementation, review, and deployment. + +## 13. Repository name recommendation + +### Recommended: `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.” + +Alternatives for a project site: + +1. `lucas-mariz` — personal and memorable; URL `/lucas-mariz/`. +2. `portfolio` — short and conventional; URL `/portfolio/`. +3. `portfolio-cv` — explicitly combines projects and résumé content. +4. `lucas-mariz.dev` — brandable, but resembles a domain that is not the Pages + URL unless a real domain is configured. + +Avoid `Curriculum`: in English it usually means a course/program syllabus; +`portfolio`, `résumé`, or `CV` communicates the product more clearly. + +## 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. Ensure the new build is versioned and no local work is forgotten. +2. Record the old URL `https://lucaspmm.github.io/Curriculum/`. GitHub does not + automatically redirect project-site URLs when a repository is renamed. +3. 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`, or the selected alternative. +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 recommended name: + +```bash +git remote set-url origin git@github.com:LucasPMM/LucasPMM.github.io.git +git remote -v +``` + +Optionally standardize the primary branch: + +```bash +git branch -m master main +git push -u origin main +``` + +After the push, change the default branch to `main` under +**Settings → Branches**. Remove the old remote branch only after `main`, CI, and +Pages are verified. + +### D. Configure Pages + +- For `LucasPMM.github.io`, use `base: '/'` in Vite. +- For `portfolio`, use `base: '/portfolio/'`. +- Under **Settings → Pages → Build and deployment**, choose + **GitHub Actions**. +- Add `.github/workflows/deploy.yml` to install with pnpm, run `pnpm check`, + build, and publish `dist`. +- Update the README URL and GitHub profile Website field. + +### E. Verify + +1. Confirm CI and deployment are green in GitHub Actions. +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, 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, 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..35bbe4e --- /dev/null +++ b/e2e/portfolio.spec.ts @@ -0,0 +1,17 @@ +import { expect, test } from '@playwright/test' + +test('shows the portfolio and changes language', async ({ page }) => { + await page.goto('/') + await expect(page.getByRole('heading', { level: 1 })).toBeVisible() + await page.getByLabel('Language').selectOption('pt-BR') + await expect(page.getByRole('heading', { name: 'Experiência profissional' })).toBeVisible() + await expect(page.locator('html')).toHaveAttribute('lang', 'pt-BR') +}) + +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') +}) diff --git a/index.html b/index.html index 743bfb1..0e73863 100644 --- a/index.html +++ b/index.html @@ -1,117 +1,38 @@ - - - - - - - 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..67396aa --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "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", + "check": "pnpm typecheck && pnpm lint && pnpm test && pnpm format:check && pnpm build" + }, + "dependencies": { + "@fontsource/lora": "^5.2.6", + "@fontsource/public-sans": "^5.2.6", + "i18next": "^25.5.2", + "preact": "^10.27.2" + }, + "devDependencies": { + "@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/node": "^24.3.1", + "@types/babel__core": "^7.20.5", + "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 e109f9961e75b2a88f795cd89ec9df6bd110d297..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32181 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⁣xuKFH1hNwr4a8 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 =pWf9ZmlM2w>t8F8VXu-1iEOW6EDUG>VgCSN zl1PNsXE#`TMbZrt3l1-mNg(83#2fzr*|sN%GZ%U4#l!BBOW5iD5yj2&HLj-((?XI< zrGlLD5c?`qUHK%EBC5Tmi)8e94*Ryd1~!{H_0S9d0QDG>ODxmJ@_T|82_>k04t5W8 zPrbNt*5ljxqI8&7h;y+m(WGBe?LT)(CF5lDR%MvR<~I`XU5m+ga9PIwe22vtCK2;$4xSthf*l1j)H2ysay z39QSS0?ZteO6nb~xl zUj;oy95KNdmBh;Z+YF7vouA$wEB&7*=4L#8Tiib)~@)#%URMV71W5%7)=#zc}?a{Mby9*V^}NiG%G zKH2T)yD!;&qqb&aq!h640|!z~fcTKSJK8<7QMI;}Rc&L->MEpk(?7-K#H5nw;ELqL zWx}ipiz023Np`bWbME9Vxk)4j#uK1ZU|Y>3kZ<0HYyN@h6JX5WTB(qqWB5PTy`Ahy cCB@D2jUZA=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'} + + 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@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 + + '@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 + + '@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 + 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 + + '@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 + + 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@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/projects/jig-solver/solver-workspace.webp b/public/projects/jig-solver/solver-workspace.webp new file mode 100644 index 0000000000000000000000000000000000000000..5511fff5281c7d567c4321b8d43de31a60f5ead8 GIT binary patch literal 34718 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Ð^Zs8qWwP?~GZ-h)8Z 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
+ + + + + + + + +
+
+
+ © {new Date().getFullYear()} Lucas Mariz. + {t('footer.copy')} +
+
+ + ) +} 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..71b6927 --- /dev/null +++ b/src/components/PortfolioPage/components/HeroSection/components/GitHubAvatar/GitHubAvatar.tsx @@ -0,0 +1,25 @@ +import { useState } from 'preact/hooks' +import { profile } from '@/content/profile' +import { useI18n } from '@/lib/i18n' + +export const GitHubAvatar = () => { + const [hasFailed, setHasFailed] = useState(false) + const { t } = useI18n() + + 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..a46fcb0 --- /dev/null +++ b/src/components/PortfolioPage/components/ProjectsSection/ProjectsSection.tsx @@ -0,0 +1,58 @@ +import { ExternalLink } from '@/components/ui/ExternalLink' +import { SectionHeading } from '@/components/ui/SectionHeading' +import { jigSolver, projects } from '@/content/projects' +import { useI18n } from '@/lib/i18n' + +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) => ( +
+

{t(project.titleKey)}

+

{t(project.summaryKey)}

+
    + {project.technologies.map((technology) => ( +
  • {technology}
  • + ))} +
+ + {t('projects.sourceLink')} + +
+ ))} +
+
+ ) +} 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..a27f53d --- /dev/null +++ b/src/content/projects.ts @@ -0,0 +1,45 @@ +import type { TranslationKey } from '@/lib/i18n' + +export type Project = { + id: 'simplex' | 'pokemon' | 'kmeans' | 'lz78' + titleKey: TranslationKey + summaryKey: TranslationKey + url: string + technologies: readonly string[] +} + +export const jigSolver = { + url: 'https://jigsolver.app/', + technologies: ['Next.js', 'TypeScript', 'Python', 'FastAPI', 'OpenCV', 'Three.js'], +} as const + +export const projects: readonly Project[] = [ + { + id: 'simplex', + titleKey: 'projects.simplex.title', + summaryKey: 'projects.simplex.summary', + url: 'https://github.com/LucasPMM/simplex', + technologies: ['Python', 'Optimization'], + }, + { + id: 'pokemon', + titleKey: 'projects.pokemon.title', + summaryKey: 'projects.pokemon.summary', + url: 'https://github.com/LucasPMM/Pokemon-Base', + technologies: ['TypeScript'], + }, + { + id: 'kmeans', + titleKey: 'projects.kmeans.title', + summaryKey: 'projects.kmeans.summary', + url: 'https://github.com/LucasPMM/greedy-kmeans', + technologies: ['Python', 'Jupyter', 'Clustering'], + }, + { + id: 'lz78', + titleKey: 'projects.lz78.title', + summaryKey: 'projects.lz78.summary', + url: 'https://github.com/LucasPMM/lz78-compression', + technologies: ['Python', '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..f66afc2 --- /dev/null +++ b/src/lib/i18n/catalog.ts @@ -0,0 +1,430 @@ +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.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.link': 'Open jigsolver.app', + 'projects.simplex.title': 'Simplex', + 'projects.simplex.summary': + 'A Python implementation exploring linear optimization through the Simplex method.', + 'projects.pokemon.title': 'Pokémon Base', + 'projects.pokemon.summary': + 'A TypeScript project for modeling and exploring a structured Pokémon domain.', + 'projects.kmeans.title': 'Greedy K-means', + 'projects.kmeans.summary': + 'A Python and notebook experiment around greedy initialization for clustering.', + 'projects.lz78.title': 'LZ78 Compression', + 'projects.lz78.summary': + 'A Python implementation of dictionary-based compression with the LZ78 algorithm.', + '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.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.link': 'Abrir jigsolver.app', + 'projects.simplex.title': 'Simplex', + 'projects.simplex.summary': + 'Uma implementação em Python que explora otimização linear pelo método Simplex.', + 'projects.pokemon.title': 'Pokémon Base', + 'projects.pokemon.summary': + 'Um projeto em TypeScript para modelar e explorar um domínio estruturado de Pokémon.', + 'projects.kmeans.title': 'Greedy K-means', + 'projects.kmeans.summary': + 'Um experimento em Python e notebooks sobre inicialização gulosa para agrupamento.', + 'projects.lz78.title': 'Compressão LZ78', + 'projects.lz78.summary': + 'Uma implementação em Python de compressão baseada em dicionário com o algoritmo LZ78.', + '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.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.link': 'Ouvrir jigsolver.app', + 'projects.simplex.title': 'Simplex', + 'projects.simplex.summary': + 'Une implémentation Python qui explore l’optimisation linéaire par la méthode du simplexe.', + 'projects.pokemon.title': 'Pokémon Base', + 'projects.pokemon.summary': + 'Un projet TypeScript pour modéliser et explorer un domaine Pokémon structuré.', + 'projects.kmeans.title': 'Greedy K-means', + 'projects.kmeans.summary': + 'Une expérience en Python et notebooks autour de l’initialisation gloutonne du regroupement.', + 'projects.lz78.title': 'Compression LZ78', + 'projects.lz78.summary': + 'Une implémentation Python de la compression par dictionnaire avec l’algorithme LZ78.', + '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..32b22e4 --- /dev/null +++ b/src/lib/i18n/components/I18nProvider/I18nProvider.tsx @@ -0,0 +1,66 @@ +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 => { + 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')) +} + +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..bfae2cb --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,17 @@ +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 { 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.') + +render(, appRoot) 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..9e82524 --- /dev/null +++ b/src/styles/components.css @@ -0,0 +1,642 @@ +.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, +.capability-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.project-card, +.capability-card { + display: flex; + min-width: 0; + flex-direction: column; +} + +.project-card p { + flex: 1; +} + +.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) { + .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) { + .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; + } + + .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..d744670 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,19 @@ +import { fileURLToPath, URL } from 'node:url' +import preact from '@preact/preset-vite' +import { defineConfig } from 'vite' + +export default defineConfig({ + base: process.env.VITE_BASE_PATH ?? '/', + plugins: [preact()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + test: { + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts'], + include: ['src/**/*.test.{ts,tsx}'], + css: true, + }, +}) From a893ca50cd6cf3031a418f24d9146e3f49031cf7 Mon Sep 17 00:00:00 2001 From: Lucas Mariz Date: Fri, 11 Sep 2026 19:42:05 -0300 Subject: [PATCH 2/3] feat: complete portfolio content and quality --- README.md | 8 + biome.json | 1 + docs/ROADMAP.md | 118 ++++++++----- e2e/portfolio.spec.ts | 155 +++++++++++++++++- index.html | 13 ++ package.json | 9 +- pnpm-lock.yaml | 43 ++++- public/favicon.svg | 5 + public/robots.txt | 4 + public/sitemap.xml | 8 + scripts/verify-static-build.mjs | 33 ++++ .../components/GitHubAvatar/GitHubAvatar.tsx | 12 +- .../ProjectsSection/ProjectsSection.tsx | 18 +- .../components/ProjectCard/ProjectCard.tsx | 30 ++++ .../components/ProjectCard/index.ts | 1 + .../ProjectDetails/ProjectDetails.tsx | 26 +++ .../components/ProjectDetails/index.ts | 1 + src/content/projects.ts | 55 +++++-- src/lib/i18n/catalog.ts | 153 +++++++++++++++-- .../components/I18nProvider/I18nProvider.tsx | 8 + src/main.tsx | 5 +- src/prerender.tsx | 11 ++ src/styles/components.css | 76 ++++++--- vite.config.ts | 21 ++- 24 files changed, 703 insertions(+), 111 deletions(-) create mode 100644 public/favicon.svg create mode 100644 public/robots.txt create mode 100644 public/sitemap.xml create mode 100644 scripts/verify-static-build.mjs create mode 100644 src/components/PortfolioPage/components/ProjectsSection/components/ProjectCard/ProjectCard.tsx create mode 100644 src/components/PortfolioPage/components/ProjectsSection/components/ProjectCard/index.ts create mode 100644 src/components/PortfolioPage/components/ProjectsSection/components/ProjectDetails/ProjectDetails.tsx create mode 100644 src/components/PortfolioPage/components/ProjectsSection/components/ProjectDetails/index.ts create mode 100644 src/prerender.tsx diff --git a/README.md b/README.md index 4ac6f02..f841e21 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ 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. +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 @@ -12,6 +16,7 @@ projects in English, Brazilian Portuguese, and French. - 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 @@ -59,6 +64,9 @@ The build output is written to `dist/`. Set `VITE_BASE_PATH` when publishing as a GitHub Pages project site. The recommended repository name is `LucasPMM.github.io`, which uses the default `/` base path. +The production check also verifies canonical metadata, the sitemap, +`robots.txt`, and representative prerendered content. + ## Content and implementation rules Read these files before making changes: diff --git a/biome.json b/biome.json index 9e2b6ec..5bdae95 100644 --- a/biome.json +++ b/biome.json @@ -7,6 +7,7 @@ "!!coverage", "!!node_modules", "!!playwright-report", + "!!test-results", "!!tmp", "!!.visualizations" ] diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7381406..90c5223 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -131,19 +131,19 @@ may be invented. outcome. - Preferred public contact channel: LinkedIn, professional email, or both. - Whether a downloadable PDF résumé belongs in the first release. -- Final summaries and representative evidence for the four public showcase - repositories. +- 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 five projects: +API order. The initial showcase is fixed to these six projects: 1. Jig Solver. -2. Simplex. -3. Pokémon Base. -4. Greedy K-means. -5. LZ78 Compression. +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. @@ -190,14 +190,39 @@ repositories must not change portfolio order or copy. - The optional secondary visual for a future case-study view is `../../ufmg/jig-solver/apps/web/public/docs/assistant-borders.png`. -### 4.2 Public repositories +### 4.2 Planner — private financial assistant -| Display name | Repository | Known focus | Required content before release | +#### 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 | Problem, academic context, algorithm, and result | -| Pokémon Base | [`Pokemon-Base`](https://github.com/LucasPMM/Pokemon-Base) | TypeScript | Product goal, architecture, contribution, and screenshot | -| Greedy K-means | [`greedy-kmeans`](https://github.com/LucasPMM/greedy-kmeans) | Jupyter/Python | Experiment question, method, result, and visualization | -| LZ78 Compression | [`lz78-compression`](https://github.com/LucasPMM/lz78-compression) | Python | Algorithm scope, implementation choices, and learning outcome | +| 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. @@ -258,17 +283,18 @@ owns behavior. A component used by one parent is nested under that parent's │ └── deploy.yml ├── public/ │ ├── projects/jig-solver/solver-workspace.webp -│ ├── avatar-fallback.webp +│ ├── avatar-fallback.svg │ ├── favicon.svg -│ └── og-cover.png +│ ├── robots.txt +│ └── sitemap.xml ├── scripts/ │ ├── check-code-conventions.mjs -│ └── require-pnpm.mjs +│ ├── require-pnpm.mjs +│ └── verify-static-build.mjs ├── src/ │ ├── components/ │ │ ├── PortfolioPage/ │ │ │ ├── PortfolioPage.tsx -│ │ │ ├── PortfolioPage.test.tsx │ │ │ ├── index.ts │ │ │ └── components/ │ │ │ ├── AppHeader/ @@ -278,13 +304,14 @@ owns behavior. A component used by one parent is nested under that parent's │ │ │ ├── SelectedWorkSection/ │ │ │ ├── EducationSection/ │ │ │ ├── ProjectsSection/ -│ │ │ ├── SkillsSection/ +│ │ │ │ └── components/ +│ │ │ │ ├── ProjectCard/ +│ │ │ │ └── ProjectDetails/ +│ │ │ ├── CapabilitiesSection/ │ │ │ └── ContactSection/ │ │ └── ui/ │ │ ├── ExternalLink/ -│ │ ├── Icon/ │ │ ├── LanguageSwitcher/ -│ │ ├── ProjectCard/ │ │ ├── SectionHeading/ │ │ └── ThemeSwitcher/ │ ├── content/ @@ -293,10 +320,10 @@ owns behavior. A component used by one parent is nested under that parent's │ │ ├── profile.ts │ │ └── projects.ts │ ├── lib/ -│ │ ├── github/avatar.ts │ │ ├── i18n/ │ │ │ ├── catalog.ts -│ │ │ ├── translations.json +│ │ │ ├── i18n.ts +│ │ │ ├── locale.ts │ │ │ ├── index.ts │ │ │ └── components/I18nProvider/ │ │ └── theme/ @@ -308,7 +335,8 @@ owns behavior. A component used by one parent is nested under that parent's │ │ ├── components.css │ │ └── tokens.css │ ├── App.tsx -│ └── main.tsx +│ ├── main.tsx +│ └── prerender.tsx ├── AGENTS.md ├── biome.json ├── commitlint.config.mjs @@ -385,8 +413,9 @@ and Ionic foundation of the web/mobile career. - A large Jig Solver case-study card with the real solver screenshot and a link to the public application. -- Four smaller cards for Simplex, Pokémon Base, Greedy K-means, and LZ78 - Compression. +- 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 @@ -438,10 +467,12 @@ Group capabilities by context; do not use percentage bars: │ ┌────────────────────── JIG SOLVER ────────────────────────────────┐ │ │ │ product + architecture + real solver screenshot + public link │ │ │ └──────────────────────────────────────────────────────────────────┘ │ -│ ┌──── Simplex ────┐ ┌── Pokémon Base ──┐ ┌─ Greedy K-means ─────┐ │ -│ └─────────────────┘ └───────────────────┘ └──────────────────────┘ │ -│ ┌──────────────────────── LZ78 Compression ───────────────────────┐ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌──── Planner ────┐ ┌──── Simplex ─────┐ │ +│ └─────────────────┘ └───────────────────┘ │ +│ ┌─ Pokémon Base ─┐ ┌─ Greedy K-means ─┐ │ +│ └─────────────────┘ └───────────────────┘ │ +│ ┌─ LZ78 Compression ───────────────────┐ │ +│ └───────────────────────────────────────┘ │ ├──────────────────────────────────────────────────────────────────────┤ │ Skills · Languages · Selected certification │ ├──────────────────────────────────────────────────────────────────────┤ @@ -472,7 +503,7 @@ Group capabilities by context; do not use percentage bars: │ [real solver image] │ │ product + architecture │ ├──────────────────────────┤ -│ four project cards │ +│ five supporting cards │ │ one per row │ ├──────────────────────────┤ │ Skills · Languages │ @@ -580,7 +611,7 @@ Use the stable username endpoint for the profile image: /> ``` -If the request fails, replace it once with `/avatar-fallback.webp`. Avoid an +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 @@ -639,22 +670,24 @@ structured, and unnecessary personal data is excluded. **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, one day) +### Phase 4 — deeper career and project evidence (P1) — complete -- Enrich the published ABILITYA and Pluritech timeline with additional - nonconfidential evidence. -- Deepen the five fixed showcase projects beyond their initial summaries. +- 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. -- Add optional build-time GitHub metadata without making it critical. +- 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, one day) +### Phase 5 — quality, SEO, and performance (P0) — complete - Add localized Open Graph metadata, favicon, canonical URL, sitemap, and `robots.txt`. @@ -668,6 +701,17 @@ private links and invented metrics are absent. **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, half day) - Rename the repository and update the local remote. diff --git a/e2e/portfolio.spec.ts b/e2e/portfolio.spec.ts index 35bbe4e..d54a1ef 100644 --- a/e2e/portfolio.spec.ts +++ b/e2e/portfolio.spec.ts @@ -1,11 +1,67 @@ +import AxeBuilder from '@axe-core/playwright' import { expect, test } from '@playwright/test' -test('shows the portfolio and changes language', async ({ page }) => { +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.getByRole('heading', { level: 1 })).toBeVisible() - await page.getByLabel('Language').selectOption('pt-BR') - await expect(page.getByRole('heading', { name: 'Experiência profissional' })).toBeVisible() 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 }) => { @@ -15,3 +71,94 @@ test('persists an explicit dark theme', async ({ page }) => { 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 0e73863..75deb2c 100644 --- a/index.html +++ b/index.html @@ -8,12 +8,25 @@ content="Portfolio of Lucas Mariz, a senior software engineer working across web, mobile, and applied research." /> + + + + + + + + + +

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 literal 0 HcmV?d00001 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/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 ( + <> + + +