diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb6ad9f..78596a4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,18 +1,5 @@ # Contributing to Oreo Badge -
-Panda mascot -
Hi! Thanks for poking around. -
- -We love that you're here. The Oreo Badge is small, weird, and welcoming -on purpose. Whatever skill level you're showing up with β€” first PR, -twentieth β€” we want it to feel easy to land changes. - -This document is the short version. Read it once, then jump in. - ---- - ## Quick map - **πŸ› Bug?** β†’ open an issue with the badge model, firmware version diff --git a/OTOD.MD b/OTOD.MD deleted file mode 100644 index 78dd9c9..0000000 --- a/OTOD.MD +++ /dev/null @@ -1,7 +0,0 @@ -no i bascially want to build the website firts, it will be a fancy website minimalist design, like the look in [IMAGE1] with our logo and the branding, apply artistic framer motion no lag effects and make the color theme, and put a file called as theme.js - -dveelop the websiye in next 15 in the folder oreo.elixpo and we will have the folder essentails thete, the /upload will be a gated upload method whwre the code displayed in the badge has to bve provided in the /upload placeholder to initiate communicatin over https to the badge provided both the badge and the phne / other devicd is in the same netwoek, it will then initiate file transfer via the private local net peer to peer - -make the website look fancy and apply cool animations, show case the instyalled apps the app stoe apps, footer geader, amke it modular with multiple / dir to visit and also mark the open source github too - -this will be deployed to cloudflare pages, create a page for oreo.pages.dev using wrangler and i shall upload it to CF \ No newline at end of file diff --git a/apps/wifi/main.py b/apps/wifi/main.py index ef1ac4e..b7b6647 100644 --- a/apps/wifi/main.py +++ b/apps/wifi/main.py @@ -16,6 +16,8 @@ """ import oreoOS +import time + from oreoOS import api, theme, widgets @@ -88,6 +90,10 @@ def on_enter(self, os_): self._mode = "main" # "main" | "nets" | "transfer" # Cached cursor inside the transfer-page sender list. self._trans_sel = 0 + # LEFT press-timestamp on the Transfer page β€” used to + # distinguish tap (deny) from hold (toggle master kill switch). + # 0 means "not currently pressed." + self._lp_left_press_ms = 0 self._nets = [] # cached saved-networks list self._nets_sel = 0 @@ -183,16 +189,55 @@ def _activate_row(self): return r = self._sel if r == self.ROW_STATUS: - if self._snap.get("connected"): + # Toggle's "on" state is whatever the label shows β€” + # `is_connected()`. The earlier version checked + # `is_radio_on()`, which got out of sync with the label + # any time a boot-time connect failed: radio left up, + # label OFF, but the toggle treated the next tap as + # "turn off" instead of "retry." Now it just mirrors + # the label: tap when label is ON β†’ disconnect; tap when + # label is OFF β†’ try to connect. + on_now = bool(self._snap.get("connected")) + if on_now: try: - self._wifi.disconnect() + radio_off = getattr(self._wifi, "radio_off", None) + if radio_off: + radio_off() + else: + self._wifi.disconnect() except Exception: pass else: + # The credentials live in secrets.py / wifi.json β€” we + # don't need a "searching" UX. Bring the radio up + # then call connect_from_config; the pump keeps the + # run loop responsive so a stuck attempt doesn't hang + # the badge. Result lands in the next _snap refresh. try: - self._wifi.connect_from_config() + radio_on = getattr(self._wifi, "radio_on", None) + if radio_on: + radio_on() except Exception: pass + try: + self._wifi.connect_from_config(pump_cb=self._cancel_pump) + except TypeError: + try: + self._wifi.connect_from_config() + except Exception: + pass + except Exception: + pass + # NOTE: we deliberately do NOT drop the radio when + # connect_from_config returns False. The previous + # version did, which silently powered the MAC down on + # any slow association β€” the user would tap the + # toggle, nothing visible would happen, and the next + # tap would just repeat the same failure. Leaving the + # radio up means: a tap that doesn't immediately + # associate at least *stays* "trying", and a later + # tap to explicitly turn WiFi off still works via + # the ON-path branch above. self._snap = self._read() elif r == self.ROW_POWER: modes = self._wifi.POWER_MODES @@ -375,10 +420,16 @@ def draw(self, d): widgets.draw_hint(d, "A=select HOME=back") snap = self._snap + # Two-state status only: connected β†’ ON, anything else β†’ OFF. + # The credentials are baked in (secrets.py / wifi.json), so + # there's no genuine "searching" β€” the connect either lands + # in a couple of seconds or it doesn't, and exposing an + # intermediate state to the user is more noise than signal. + _stat_connected = bool(snap.get("connected")) + _stat_label = "ON" if _stat_connected else "OFF" + _stat_color = theme.PRIMARY if _stat_connected else theme.MUTED rows = [ - ("Status", "ON" if snap.get("connected") else "OFF", - theme.PRIMARY if snap.get("connected") - else theme.MUTED), + ("Status", _stat_label, _stat_color), ("SSID", snap.get("ssid") or "β€”", theme.TEXT_BRIGHT), ("IP", snap.get("ip") or "β€”", theme.TEXT_DIM), ("RSSI", self._rssi_value(snap), theme.TEXT_DIM), @@ -456,6 +507,21 @@ def _rssi_value(self, snap): return "%d dBm %s" % (rssi, _bars(rssi)) # ── file-transfer sub-page ────────────────────────────────────────── + def _cancel_pump(self): + """Passed to wifi.connect_from_config(). Called ~12 Hz from + inside the wait loop β€” re-reads the button matrix and returns + True on any press so the user can abort 'SEARCH' instantly. + Without this the run loop is frozen for up to 6 s per saved + network and the badge looks crashed.""" + try: + self._os.buttons.update() + for b_ in api.BUTTONS: + if self._os.buttons.just_pressed(b_): + return True + except Exception: + pass + return False + def _http(self): try: from oreoOS import http_server as _hs @@ -478,22 +544,48 @@ def _transfer_summary(self): return "%d active" % n return "ready" + # ── long-press LEFT tracking ── + # Pressing LEFT once = deny the selected sender. Holding LEFT for + # LP_HOLD_MS = toggle the transfer-disabled master kill switch. + # We track the press timestamp on key-down and decide on key-up. + LP_HOLD_MS = 800 + def _on_button_transfer(self, btn): if btn in (api.BTN_HOME, api.BTN_B): self._mode = "main" self._dirty = True return + hs = self._http() + if hs is None: + return + + # Master kill switch on β€” only `A = re-enable` works in this state. + try: + disabled = (hasattr(hs, "is_transfer_enabled") + and not hs.is_transfer_enabled()) + except Exception: + disabled = False + if disabled: + if btn == api.BTN_A: + try: hs.set_transfer_enabled(True) + except Exception: pass + self._dirty = True + return + if btn == api.BTN_RIGHT: - # Refresh β€” sometimes the page shows "Server offline" when - # WiFi is actually associated but the HTTP server hasn't - # picked up the new IP (e.g. after a hotspot handoff). - # Force-reconnect WiFi if needed and rebind the listener. + # R = refresh the badge code. We deliberately overload R + # to ALSO retry the WiFi/HTTP reconnect (the previous + # behaviour) β€” both are "kick things back into shape" + # actions and combining them keeps the button budget low. + try: + if hasattr(hs, "refresh_code"): + hs.refresh_code() + except Exception: + pass self._refresh_transfer() self._dirty = True return - hs = self._http() - if hs is None: - return + sessions = hs.list_sessions() n = len(sessions) if btn == api.BTN_UP and n: @@ -504,14 +596,56 @@ def _on_button_transfer(self, btn): sid = sessions[self._trans_sel].get("id", "") if sid: hs.approve(sid) - elif btn == api.BTN_LEFT and n: - sid = sessions[self._trans_sel].get("id", "") - if sid: - hs.deny(sid) + elif btn == api.BTN_LEFT: + # Mark the press-start time on first-press only. Subsequent + # auto-repeat events skip this branch via the != 0 guard, + # so a long hold keeps the original timestamp and we can + # measure full duration in on_button_release. The actual + # tap-vs-hold decision happens there β€” firing both deny + # and toggle on a single press would be confusing. + if self._lp_left_press_ms == 0: + self._lp_left_press_ms = time.ticks_ms() + return # don't mark dirty β€” visual state hasn't changed yet else: return self._dirty = True + def on_button_release(self, btn): + # LEFT released on the Transfer page β€” decide tap vs hold and + # fire exactly one action. The press-start timestamp was + # captured in _on_button_transfer. + if self._mode != "transfer" or btn != api.BTN_LEFT: + return + start = self._lp_left_press_ms + self._lp_left_press_ms = 0 + if start == 0: + return + held = time.ticks_diff(time.ticks_ms(), start) + hs = self._http() + if hs is None: + return + if held >= self.LP_HOLD_MS: + # Long press β†’ toggle the master kill switch. + try: + cur = (hs.is_transfer_enabled() + if hasattr(hs, "is_transfer_enabled") else True) + hs.set_transfer_enabled(not cur) + except Exception: + pass + else: + # Short tap β†’ deny the focused sender, if any. + sessions = [] + try: + sessions = hs.list_sessions() + except Exception: + pass + if sessions and 0 <= self._trans_sel < len(sessions): + sid = sessions[self._trans_sel].get("id", "") + if sid: + try: hs.deny(sid) + except Exception: pass + self._dirty = True + def _refresh_transfer(self): """Reconcile the transfer page with the live network state. @@ -539,27 +673,80 @@ def _refresh_transfer(self): def _draw_transfer(self, d): widgets.draw_header(d, "SEND FILES") - widgets.draw_hint(d, "A=allow L=deny R=refresh B=back") hs = self._http() running = bool(hs and hs.is_running()) - # ── URL block ── + # ── Master kill switch state β€” entire screen flips to a + # ── single "Transfer closed" panel. + try: + disabled = (running + and hasattr(hs, "is_transfer_enabled") + and not hs.is_transfer_enabled()) + except Exception: + disabled = False + if disabled: + widgets.draw_hint(d, "A=re-enable B=back") + y = ROW_TOP_Y + 40 + label = "TRANSFER CLOSED" + d.text(label, (SW - len(label) * 16) // 2, y, + theme.PRIMARY, scale=2) + sub = "File transfer disabled by you." + d.text(sub, (SW - len(sub) * 8) // 2, y + 28, + theme.TEXT_DIM) + cta = "Tap A to re-enable." + d.text(cta, (SW - len(cta) * 8) // 2, y + 44, + theme.MUTED) + return + + widgets.draw_hint(d, "A=allow L=deny hold-L=close R=refresh") + + # ── Code header β€” big 6-char display + TTL countdown ── y = ROW_TOP_Y if running: - url1 = hs.url() - url2 = hs.url_fallback() - d.text("Open on your phone:", ROW_PAD_X, y, theme.TEXT_DIM) - d.text(url1, ROW_PAD_X, y + 14, theme.PRIMARY, scale=2) - d.text(url2, ROW_PAD_X, y + 36, theme.TEXT_DIM) + try: + code = hs.current_code() + except Exception: + code = "------" + try: + remain_ms = hs.code_remaining_ms() + except Exception: + remain_ms = 0 + # "rotates in 4:32" + secs = max(0, remain_ms // 1000) + mins, ss = divmod(secs, 60) + label = "Type this code in the sender's browser:" + d.text(label, ROW_PAD_X, y, theme.TEXT_DIM) + # Big code β€” scale=3 so it dominates the page. + code_w = len(code) * 8 * 3 + d.text(code, (SW - code_w) // 2, y + 14, + theme.PRIMARY, scale=3) + # Tiny TTL countdown below the code, centred. + ttl = "rotates in %d:%02d" % (mins, ss) + d.text(ttl, (SW - len(ttl) * 8) // 2, y + 14 + 24 + 4, + theme.GOLD if remain_ms > 30_000 else theme.PRIMARY) + # URL hints below the countdown. We show BOTH the mDNS + # form and the raw IP because multicast DNS is unreliable + # in the wild β€” on most networks oreo.local works, but + # the IP is the universal fallback the user can type into + # the website's address field. + url_local = hs.url() + url_ip = hs.url_fallback() + d.text(url_local, ROW_PAD_X, y + 14 + 24 + 18, + theme.TEXT_DIM) + d.text(url_ip, ROW_PAD_X, y + 14 + 24 + 32, + theme.MUTED) else: d.text("Server offline.", ROW_PAD_X, y, theme.MUTED, scale=1) d.text("Connect WiFi to enable transfer.", ROW_PAD_X, y + 14, theme.MUTED2, scale=1) # ── live transfer progress bar ── + # Header now spans: label (14) + code at scale=3 (~26) + TTL + # (12) + url_local (12) + url_ip (12). Push the body start + # past that block. prog = hs.progress() if hs else None - prog_y = y + 56 + prog_y = y + 92 if prog: total = max(1, int(prog.get("total", 0))) done = min(total, int(prog.get("received", 0))) @@ -599,13 +786,17 @@ def _draw_transfer(self, d): # State -> dot colour. RGB tuples instead of theme constants # because we need a clean green, and the theme palette doesn't # ship one β€” primary is pink-red, teal is too cyan. + # State β†’ dot colour. State names changed in the new server + # protocol β€” "authed" = correct code typed but badge owner + # hasn't approved yet (yellow); "approved" = green; "denied" + # is filtered upstream so it shouldn't normally appear. dot_color = { - "pending": api.rgb(255, 209, 102), # yellow + "authed": api.rgb(255, 209, 102), # yellow "approved": api.rgb( 61, 220, 151), # green - "denied": api.rgb(255, 93, 104), # red β€” visible only - # in the brief window - # before the row is - # filtered out + "denied": api.rgb(255, 93, 104), # red (filtered) + # Legacy fallback for the old "pending" state name β€” + # remove once every device is on the new protocol. + "pending": api.rgb(255, 209, 102), } DOT_SIZE = 8 DOT_RIGHT = ROW_PAD_X + 2 # pad from the right edge diff --git a/oreo.elixpo/.gitignore b/oreo.elixpo/.gitignore new file mode 100644 index 0000000..6f44b3b --- /dev/null +++ b/oreo.elixpo/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +.next/ +out/ +*.tsbuildinfo +.env.local +.env.production.local +.wrangler/ diff --git a/oreo.elixpo/README.md b/oreo.elixpo/README.md new file mode 100644 index 0000000..6b687cb --- /dev/null +++ b/oreo.elixpo/README.md @@ -0,0 +1,73 @@ +# oreo.elixpo + +Marketing site, app catalogue, and gated file-transfer launcher for the +**Oreo Badge** β€” `oreo.elixpo.com`. + +Built with Next.js 14 (App Router, static export) + Tailwind + Framer +Motion. The single source of truth for brand tokens is +[`theme.js`](./theme.js) at the project root; Tailwind re-exports those +tokens as utility classes (`bg-bg`, `text-primary`, etc.). + +## Develop + +```bash +npm install +npm run dev # http://localhost:3000 +``` + +## Deploy to Cloudflare Pages + +The site is a fully static export β€” no Workers runtime needed. + +```bash +npm run build # writes ./out +npx wrangler pages deploy out --project-name=oreo # CF push +``` + +Or connect this folder to a Cloudflare Pages project in the dashboard: + +| Setting | Value | +|----------------------|------------------------------------| +| Build command | `npm install && npm run build` | +| Output directory | `out` | +| Root directory | `oreo.elixpo` | +| Node version | `20` | + +## Structure + +``` +src/ + app/ + layout.tsx global chrome (Header + Footer) + page.tsx home β€” hero, feature trio, apps showcase + apps/ preloaded + store catalogue + badge/ hardware specs + upload/ gated file-transfer handoff to the badge + get-started/ flashing + deploy guide + components/ + Header.tsx sticky nav + GitHub badge + Footer.tsx link columns + brand block + AppCard.tsx reusable app tile (Framer-Motion reveal) + MotionWrap.tsx shared motion presets + data/ + apps.ts preloaded + store catalogue (mirrors manifest.json) +theme.js brand tokens (colors, radii, motion presets) +tailwind.config.ts Tailwind config that consumes theme.js +wrangler.toml Cloudflare Pages deploy config +``` + +## Brand tokens + +The palette + spacing live in [`theme.js`](./theme.js). Any time you +add a new shade or motion preset, add it there first so it shows up in +both Tailwind classes and runtime-imported values automatically. + +## /upload + +The upload route is *not* itself a transfer endpoint. Browsers block +HTTPS pages from talking to plain-HTTP endpoints on the LAN, and the +badge speaks HTTP only. So `/upload` collects the 6-character code +displayed on the badge screen and hands the user off to +`http://oreo.local/?prefill=` in a new tab β€” the badge's own +upload page picks up the prefill and continues the gated handshake +from there. End-to-end bytes never leave the local network. diff --git a/oreo.elixpo/next-env.d.ts b/oreo.elixpo/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/oreo.elixpo/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/oreo.elixpo/next.config.mjs b/oreo.elixpo/next.config.mjs new file mode 100644 index 0000000..7c2f1d3 --- /dev/null +++ b/oreo.elixpo/next.config.mjs @@ -0,0 +1,20 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + // Static export β€” Cloudflare Pages serves the resulting /out directory + // as a plain CDN-fronted site. Keeps the deployment story to two + // commands (`next build` + `wrangler pages deploy out`) and lets us + // skip the Workers runtime entirely. The cost is no server-rendered + // routes; the file-transfer page does its work entirely client-side + // against the badge's local HTTP server, so that's fine. + output: "export", + // Image optimisation requires a server runtime β€” disable so the + // static export contains the original asset bytes. + images: { + unoptimized: true, + }, + // Trailing slashes on every URL so Cloudflare Pages serves the + // matching `path/index.html` even without rewrite rules. + trailingSlash: true, +}; + +export default nextConfig; diff --git a/oreo.elixpo/package-lock.json b/oreo.elixpo/package-lock.json new file mode 100644 index 0000000..fedbdd0 --- /dev/null +++ b/oreo.elixpo/package-lock.json @@ -0,0 +1,3738 @@ +{ + "name": "oreo-elixpo", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "oreo-elixpo", + "version": "1.0.0", + "dependencies": { + "framer-motion": "^11.11.17", + "lucide-react": "^0.460.0", + "next": "^15.5.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.10.1", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.16", + "typescript": "^5.7.2", + "wrangler": "^3.95.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", + "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "mime": "^3.0.0" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.0.2.tgz", + "integrity": "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.14", + "workerd": "^1.20250124.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250718.0.tgz", + "integrity": "sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250718.0.tgz", + "integrity": "sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250718.0.tgz", + "integrity": "sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250718.0.tgz", + "integrity": "sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250718.0.tgz", + "integrity": "sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild-plugins/node-globals-polyfill": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", + "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild-plugins/node-modules-polyfill": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", + "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==", + "dev": true, + "license": "ISC", + "dependencies": { + "escape-string-regexp": "^4.0.0", + "rollup-plugin-node-polyfills": "^0.2.1" + }, + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.18.tgz", + "integrity": "sha512-hAV85Ckd9QR6RvH04MEKwsfLTksvFpO47j9xwtoIuvuPnlwecpSi+uZTtm8HirVbtlI2Fnz//xpcSTjFdyJk+g==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.18.tgz", + "integrity": "sha512-w0WvQf1n+txiwns/9pwIQteCJpZTbxzO2SE0FLcwuD4v0WEh1JPOjdyxWL21XwJsdpx8cFRjyzxzCS/siP7HcQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.18.tgz", + "integrity": "sha512-znn71QmDuxm+BOaglihMZfvyySMnNljkVIY5Z2TCssBmm+WqL6c19VhtH5ktFkHa8EZ2bnTUpcNcmNSQsg67og==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.18.tgz", + "integrity": "sha512-yPPe5MNL+igZUa+OsqQJisqSfh6oarIuA1Q0BDxljGJhRQyZeP+WRHh7rs/jZUGMh5aY0YdIjXZG0VohkKkUdw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.18.tgz", + "integrity": "sha512-glaCczEWIrHsokFZ3pP08U4BpKxwIdnT+txdOM32OBgpL9Yw4aqx8NejmgtZQZOdstQ5f0L3CasIZudzCuD+nw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.18.tgz", + "integrity": "sha512-oUfg2EgJmU3R0OCOWiokGFUTvZiPfXtriXiuF3YNxRoROCdgvTedHIzYoeKH34gsZxS/V7mHbfq2hpAHwhH1/A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.18.tgz", + "integrity": "sha512-JLxSP3KTd9iu/bvUMQxH7RJo9xKSHf55/6RPE4a6FTSZygGn7uvZbCej0AHXydwkggQGSD9UddSjwv6Xz5ESfA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.18.tgz", + "integrity": "sha512-ir1v7enP52K2HNz3tQQvwF+x7VNxBk1ciiZ18WBPvxf4C59IqdfmHPJYK3vH7rSxpuCVw/8C712wTXNAtEp+NA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.18.tgz", + "integrity": "sha512-LIu5me6QTANCd25E7I5uIEfvgQ06RK7tvHAbYo3zCb3VpxQEPvMcSpd87NwUABDT6MbGPdEGR5VRiK4PPTJhQg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/as-table": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", + "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "printable-characters": "^1.0.42" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", + "dev": true, + "license": "MIT" + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.359", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.359.tgz", + "integrity": "sha512-8lPELWuYZIWk7NDvCNthtmMw/7Q5Wu25NpM4djFMHBmk8DubPAtL4YTOp7ou0e7HyJtwkVlWv8XMLURnrtgJQw==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-source": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", + "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "data-uri-to-buffer": "^2.0.0", + "source-map": "^0.6.1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lucide-react": { + "version": "0.460.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.460.0.tgz", + "integrity": "sha512-BVtq/DykVeIvRTJvRAgCsOwaGL8Un3Bxh8MbDxMhEWlZay3T4IpEKDEpwt5KZ0KJMHzgm6jrltxlT5eXOWXDHg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/miniflare": { + "version": "3.20250718.3", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20250718.3.tgz", + "integrity": "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "acorn": "8.14.0", + "acorn-walk": "8.3.2", + "exit-hook": "2.2.1", + "glob-to-regexp": "0.4.1", + "stoppable": "1.1.0", + "undici": "^5.28.5", + "workerd": "1.20250718.0", + "ws": "8.18.0", + "youch": "3.3.4", + "zod": "3.22.3" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.5.18", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.18.tgz", + "integrity": "sha512-eKL8zUJkX9Y5lE+RX/2YJoItVdGlIscyVyboeD9wSpp0PaGqjoA4tTpT2qPqz9ax+5IzGESyLSeZ/RCwbSZ2uQ==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.18", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.18", + "@next/swc-darwin-x64": "15.5.18", + "@next/swc-linux-arm64-gnu": "15.5.18", + "@next/swc-linux-arm64-musl": "15.5.18", + "@next/swc-linux-x64-gnu": "15.5.18", + "@next/swc-linux-x64-musl": "15.5.18", + "@next/swc-win32-arm64-msvc": "15.5.18", + "@next/swc-win32-x64-msvc": "15.5.18", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/printable-characters": { + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup-plugin-inject": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", + "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1", + "magic-string": "^0.25.3", + "rollup-pluginutils": "^2.8.1" + } + }, + "node_modules/rollup-plugin-node-polyfills": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", + "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rollup-plugin-inject": "^3.0.0" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, + "node_modules/stacktracey": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.2.0.tgz", + "integrity": "sha512-ETyQEz+CzXiLjEbyJqpbp+/T79RQD/6wqFucRBIlVNZfYq2Ay7wbretD4cxpbymZlaPWx58aIhPEY1Cr8DlVvg==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "as-table": "^1.0.36", + "get-source": "^2.0.12" + } + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unenv": { + "version": "2.0.0-rc.14", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.14.tgz", + "integrity": "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "exsolve": "^1.0.1", + "ohash": "^2.0.10", + "pathe": "^2.0.3", + "ufo": "^1.5.4" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/workerd": { + "version": "1.20250718.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250718.0.tgz", + "integrity": "sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20250718.0", + "@cloudflare/workerd-darwin-arm64": "1.20250718.0", + "@cloudflare/workerd-linux-64": "1.20250718.0", + "@cloudflare/workerd-linux-arm64": "1.20250718.0", + "@cloudflare/workerd-windows-64": "1.20250718.0" + } + }, + "node_modules/wrangler": { + "version": "3.114.17", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.114.17.tgz", + "integrity": "sha512-tAvf7ly+tB+zwwrmjsCyJ2pJnnc7SZhbnNwXbH+OIdVas3zTSmjcZOjmLKcGGptssAA3RyTKhcF9BvKZzMUycA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.3.4", + "@cloudflare/unenv-preset": "2.0.2", + "@esbuild-plugins/node-globals-polyfill": "0.2.3", + "@esbuild-plugins/node-modules-polyfill": "0.2.2", + "blake3-wasm": "2.1.5", + "esbuild": "0.17.19", + "miniflare": "3.20250718.3", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.14", + "workerd": "1.20250718.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=16.17.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20250408.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/wrangler/node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "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 + } + } + }, + "node_modules/youch": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", + "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie": "^0.7.1", + "mustache": "^4.2.0", + "stacktracey": "^2.1.8" + } + }, + "node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/oreo.elixpo/package.json b/oreo.elixpo/package.json new file mode 100644 index 0000000..7e926dd --- /dev/null +++ b/oreo.elixpo/package.json @@ -0,0 +1,30 @@ +{ + "name": "oreo-elixpo", + "version": "1.0.0", + "private": true, + "description": "The Oreo Badge β€” marketing site, app catalogue, and gated file-transfer launcher.", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "deploy": "next build && wrangler pages deploy out --project-name=oreo" + }, + "dependencies": { + "framer-motion": "^11.11.17", + "lucide-react": "^0.460.0", + "next": "^15.5.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.10.1", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.16", + "typescript": "^5.7.2", + "wrangler": "^3.95.0" + } +} diff --git a/oreo.elixpo/plan.md b/oreo.elixpo/plan.md new file mode 100644 index 0000000..df9c4b6 --- /dev/null +++ b/oreo.elixpo/plan.md @@ -0,0 +1,448 @@ +You are given a task to integrate an existing React component in the codebase + +The codebase should support: +- shadcn project structure +- Tailwind CSS +- Typescript + +If it doesn't, provide instructions on how to setup project via shadcn CLI, install Tailwind or Typescript. + +Determine the default path for components and styles. +If default path for components is not /components/ui, provide instructions on why it's important to create this folder +Copy-paste this component to /components/ui folder: +```tsx +glowy-waves-hero-shadcnui.tsx +import { motion, type Variants } from "framer-motion"; +import { ArrowRight, Sparkles } from "lucide-react"; +import { useEffect, useRef } from "react"; + +import { Button } from "@/components/ui/button"; + +type Point = { + x: number; + y: number; +}; + +interface WaveConfig { + offset: number; + amplitude: number; + frequency: number; + color: string; + opacity: number; +} + +const highlightPills = [ + "Immersive visuals", + "Responsive motion", + "GPU friendly", +] as const; + +const heroStats: { label: string; value: string }[] = [ + { label: "Live installations", value: "320+" }, + { label: "Latency", value: "8ms" }, + { label: "Teams onboarded", value: "120+" }, +]; + +const containerVariants: Variants = { + hidden: { opacity: 0, y: 24 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.8, staggerChildren: 0.12 }, + }, +}; + +const itemVariants: Variants = { + hidden: { opacity: 0, y: 24 }, + visible: { + opacity: 1, + y: 0, + transition: { duration: 0.6, ease: "easeOut" }, + }, +}; + +const statsVariants: Variants = { + hidden: { opacity: 0, scale: 0.95 }, + visible: { + opacity: 1, + scale: 1, + transition: { duration: 0.6, ease: "easeOut", staggerChildren: 0.08 }, + }, +}; + +export function GlowyWavesHero() { + const canvasRef = useRef(null); + const mouseRef = useRef({ x: 0, y: 0 }); + const targetMouseRef = useRef({ x: 0, y: 0 }); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return undefined; + + const ctx = canvas.getContext("2d"); + if (!ctx) return undefined; + + let animationId: number; + let time = 0; + + const computeThemeColors = () => { + const rootStyles = getComputedStyle(document.documentElement); + + // Helper to convert any CSS color to a Canvas-compatible format + const resolveColor = (variables: string[], alpha = 1) => { + // Create a temporary element to get computed color + const tempEl = document.createElement("div"); + tempEl.style.position = "absolute"; + tempEl.style.visibility = "hidden"; + tempEl.style.width = "1px"; + tempEl.style.height = "1px"; + document.body.appendChild(tempEl); + + let color = `rgba(255, 255, 255, ${alpha})`; + + for (const variable of variables) { + const value = rootStyles.getPropertyValue(variable).trim(); + if (value) { + // Try to set the background color using the CSS variable + tempEl.style.backgroundColor = `var(${variable})`; + const computedColor = getComputedStyle(tempEl).backgroundColor; + + if (computedColor && computedColor !== "rgba(0, 0, 0, 0)") { + // Convert RGB to RGBA with alpha if needed + if (alpha < 1) { + const rgbMatch = computedColor.match( + /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/ + ); + if (rgbMatch) { + color = `rgba(${rgbMatch[1]}, ${rgbMatch[2]}, ${rgbMatch[3]}, ${alpha})`; + } else { + color = computedColor; + } + } else { + color = computedColor; + } + break; + } + } + } + + document.body.removeChild(tempEl); + return color; + }; + + return { + backgroundTop: resolveColor(["--background"], 1), + backgroundBottom: resolveColor(["--muted", "--background"], 0.95), + wavePalette: [ + { + offset: 0, + amplitude: 70, + frequency: 0.003, + color: resolveColor(["--primary"], 0.8), + opacity: 0.45, + }, + { + offset: Math.PI / 2, + amplitude: 90, + frequency: 0.0026, + color: resolveColor(["--accent", "--primary"], 0.7), + opacity: 0.35, + }, + { + offset: Math.PI, + amplitude: 60, + frequency: 0.0034, + color: resolveColor(["--secondary", "--foreground"], 0.65), + opacity: 0.3, + }, + { + offset: Math.PI * 1.5, + amplitude: 80, + frequency: 0.0022, + color: resolveColor(["--primary-foreground", "--foreground"], 0.25), + opacity: 0.25, + }, + { + offset: Math.PI * 2, + amplitude: 55, + frequency: 0.004, + color: resolveColor(["--foreground"], 0.2), + opacity: 0.2, + }, + ] satisfies WaveConfig[], + }; + }; + + let themeColors = computeThemeColors(); + + const handleThemeMutation = () => { + themeColors = computeThemeColors(); + }; + + const observer = new MutationObserver(handleThemeMutation); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class", "data-theme"], + }); + + const prefersReducedMotion = window.matchMedia( + "(prefers-reduced-motion: reduce)" + ).matches; + + const mouseInfluence = prefersReducedMotion ? 10 : 70; + const influenceRadius = prefersReducedMotion ? 160 : 320; + const smoothing = prefersReducedMotion ? 0.04 : 0.1; + + const resizeCanvas = () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + }; + + const recenterMouse = () => { + const centerPoint = { x: canvas.width / 2, y: canvas.height / 2 }; + mouseRef.current = centerPoint; + targetMouseRef.current = centerPoint; + }; + + const handleResize = () => { + resizeCanvas(); + recenterMouse(); + }; + + const handleMouseMove = (event: MouseEvent) => { + targetMouseRef.current = { x: event.clientX, y: event.clientY }; + }; + + const handleMouseLeave = () => { + recenterMouse(); + }; + + resizeCanvas(); + recenterMouse(); + + window.addEventListener("resize", handleResize); + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseleave", handleMouseLeave); + + const drawWave = (wave: WaveConfig) => { + ctx.save(); + ctx.beginPath(); + + for (let x = 0; x <= canvas.width; x += 4) { + const dx = x - mouseRef.current.x; + const dy = canvas.height / 2 - mouseRef.current.y; + const distance = Math.sqrt(dx * dx + dy * dy); + const influence = Math.max(0, 1 - distance / influenceRadius); + const mouseEffect = + influence * + mouseInfluence * + Math.sin(time * 0.001 + x * 0.01 + wave.offset); + + const y = + canvas.height / 2 + + Math.sin(x * wave.frequency + time * 0.002 + wave.offset) * + wave.amplitude + + Math.sin(x * wave.frequency * 0.4 + time * 0.003) * + (wave.amplitude * 0.45) + + mouseEffect; + + if (x === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + } + + ctx.lineWidth = 2.5; + ctx.strokeStyle = wave.color; + ctx.globalAlpha = wave.opacity; + ctx.shadowBlur = 35; + ctx.shadowColor = wave.color; + ctx.stroke(); + + ctx.restore(); + }; + + const animate = () => { + time += 1; + + mouseRef.current.x += + (targetMouseRef.current.x - mouseRef.current.x) * smoothing; + mouseRef.current.y += + (targetMouseRef.current.y - mouseRef.current.y) * smoothing; + + const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height); + gradient.addColorStop(0, themeColors.backgroundTop); + gradient.addColorStop(1, themeColors.backgroundBottom); + + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.globalAlpha = 1; + ctx.shadowBlur = 0; + + themeColors.wavePalette.forEach(drawWave); + + animationId = window.requestAnimationFrame(animate); + }; + + animationId = window.requestAnimationFrame(animate); + + return () => { + window.removeEventListener("resize", handleResize); + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseleave", handleMouseLeave); + cancelAnimationFrame(animationId); + observer.disconnect(); + }; + }, []); + + return ( +
+
+ ); +} + + +demo.tsx +import { GlowyWavesHero } from "@/components/ui/glowy-waves-hero-shadcnui" + +export default function Demo() { + return ( +
+ +
+ ) +} + +``` + +Install NPM dependencies: +```bash +lucide-react, framer-motion +``` + +Implementation Guidelines + 1. Analyze the component structure and identify all required dependencies + 2. Review the component's argumens and state + 3. Identify any required context providers or hooks and install them + 4. Questions to Ask + - What data/props will be passed to this component? + - Are there any specific state management requirements? + - Are there any required assets (images, icons, etc.)? + - What is the expected responsive behavior? + - What is the best place to use this component in the app? + +Steps to integrate + 0. Copy paste all the code above in the correct directories + 1. Install external dependencies + 2. Fill image assets with Unsplash stock images you know exist + 3. Use lucide-react icons for svgs or logos if component requires them diff --git a/oreo.elixpo/postcss.config.js b/oreo.elixpo/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/oreo.elixpo/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/oreo.elixpo/public/CONTRIBUTING.md b/oreo.elixpo/public/CONTRIBUTING.md new file mode 100644 index 0000000..78596a4 --- /dev/null +++ b/oreo.elixpo/public/CONTRIBUTING.md @@ -0,0 +1,215 @@ +# Contributing to Oreo Badge + +## Quick map + +- **πŸ› Bug?** β†’ open an issue with the badge model, firmware version + (`Settings β†’ Version`), and a one-line description. +- **🎨 New app?** β†’ see [Writing an app](#writing-an-app) below. +- **πŸ”§ Driver / OS change?** β†’ see [Hacking on the OS](#hacking-on-the-os). +- **πŸ“¦ Release / OTA?** β†’ see [Releasing](#releasing). +- **πŸ§‘β€πŸ€β€πŸ§‘ Conduct?** β†’ see [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md). + +--- + +## Setup + +```bash +git clone https://github.com/elixpo/oreo +cd oreo +python -m venv .venv && source .venv/bin/activate +pip install -r oreoOS/requirements.txt +``` + +You don't strictly need a real badge to develop most apps β€” running the +CPython importers locally catches the majority of issues. But the real +test is always on hardware. If you don't have a badge, ping +**hello@elixpo.com** and we'll see what we can do. + +--- + +## Writing an app + +The fastest path to a working app: + +```bash +cp -r templates/example_app apps/my_app +# edit apps/my_app/main.py β€” three methods is the whole API +python tools/deploy.py /dev/ttyACM0 +``` + +**Default-installed vs Market apps.** Where you put the folder decides +how the app reaches the user: + +| Path | Behaviour | +|---|---| +| `apps//` | **Default-installed.** Tile appears in the drawer on every fresh deploy. Use for core OS tools + apps every badge owner should have | +| `apps_market//` | **Opt-in.** Ships in the catalogue but isn't in the drawer until the user installs from the on-device **App Market** tile. Use for games, sketches, hackathon entries, themed extras | + +The two trees have **identical shape** β€” `main.py + manifest.json + +__init__.py + assets/`. `tools/deploy.py` walks both and ships them to +the matching path on the device. The Market app calls into +[`oreoOS.store`](oreoOS/store.py) which `cp -r`s a folder between the +two trees on install / uninstall. + +When in doubt, ship to `apps_market/` β€” flash and drawer real estate +are precious, and the install flow is one tap away. + +**The contract.** Your app subclasses `oreoOS.App` and implements three +lifecycle methods: + +```python +class App(oreoOS.App): + name = "My App" + + def on_enter(self, os): + # one-shot setup. load sprites, restore state, calibrate sensors. + + def update(self, dt): + # per-frame logic. dt is seconds since last frame. + + def draw(self, d): + # per-frame render. d is the framebuffer. don't call d.present(). +``` + +Optional hooks: `on_exit`, `on_button_press(btn)`, +`on_button_release(btn)`, `on_home_press()` (returns True to suppress +the default HOME-to-drawer behaviour). + +**Class attributes you can set:** + +- `name` β€” what appears on the launcher tile + loading screen +- `SHOW_LOADING` β€” `True` if `on_enter` takes > 200 ms +- `BLOCK_IDLE` β€” `True` for apps that should keep the screen on + even without button presses (games, IR scanner) + +**Where things live:** + +``` +apps/my_app/ +β”œβ”€β”€ __init__.py empty marker +β”œβ”€β”€ main.py your App class +β”œβ”€β”€ manifest.json name + version + icon + author +└── assets/ + β”œβ”€β”€ raw/ source images you commit + └── optimized/ baked RGB565 .py modules +``` + +To bake assets: drop a PNG/JPG into `assets/raw/`, run +`python tools/optimize_assets.py --app my_app`, commit the result. + +The `theme` module is the source of truth for colours. If you find +yourself reaching for `api.rgb(...)` directly, ask whether there's a +themed constant that fits. + +--- + +## Hacking on the OS + +The OS lives in two packages: + +- `oreoOS/` β€” pure Python: launcher, splash, theme, widgets, app base + class, OTA client, cache, power manager, etc. +- `oreoWare/` β€” hardware drivers: display, buttons, WiFi, BT, IMU, IR, + battery, touch. **Everything is funnelled through `oreoWare/pins.py`** + so a PCB pin swap only touches that file. + +**Conventions worth knowing:** + +- One source of truth for pins, one for colours, one for VERSION. +- Apps that read network data should cache to disk with a TTL via + `oreoOS.cache`. See `apps/badge/main.py` for the pattern. +- The framebuf is RGB565 big-endian. Use `api.rgb(r, g, b)` to pack β€” + don't construct the integer by hand. +- Anything that might block (network, heavy compute) should either be + short-timeout-bounded OR set `SHOW_LOADING = True` so the user sees a + panel during `on_enter`. + +--- + +## Pull requests + +1. **Branch off main.** Name it something descriptive + (`feature/ir-quest-leaderboard`, not `patch-1`). +2. **Run the deploy** locally to make sure the OS still boots. If your + change touches drivers, test wake-from-sleep, OTA, and at least two + apps you didn't write. +3. **One PR, one purpose.** Don't bundle a bug-fix with a rename, and + don't fold a 4-file refactor into a typo PR. +4. **Title format:** `[scope] short description`, e.g. + `[ota] fix peek() to handle malformed manifest`. +5. **Tag a maintainer** in the PR description so we see it. Right now + that's [@Circuit-Overtime](https://github.com/Circuit-Overtime). + +We try to respond within a week. If we haven't, please nudge β€” the +notification probably got lost in conference-season chaos. + +--- + +## Releasing + +Maintainers only β€” feel free to skip this section. + +Releases are **manual on purpose**. We want a human to look at a badge +running the release candidate before the wider fleet pulls it in. + +**Before tagging:** add the new version's section to the top of +[`CHANGELOG.md`](CHANGELOG.md). The CI release workflow takes the +**top section** of that file verbatim and posts it as the GitHub +release's `body` β€” which is what the badge's **Updates** screen shows +as the in-device changelog when an upgrade is found. Keep entries +short, bullet-style, present tense. + +The one-liner: + +```bash +# Dry-run first so you can read every command the script will execute. +python tools/release.py v1.4.0 --channel stable --dry-run + +# Looks good? Drop --dry-run and ship. +python tools/release.py v1.4.0 --channel stable --notes "Fly mode + new pet sprites" +``` + +Under the hood the script: + +1. Verifies `git` + `gh` are installed and you're authenticated. +2. Refuses to release if the working tree is dirty (override with `--force`). +3. Bumps `oreoOS/config.py:VERSION` to `v1.4.0` if it isn't already. +4. Commits the bump and pushes `main` + the new tag (`stable/v1.4.0`). +5. Runs `tools/build_release.py` to produce + `dist/v1.4.0/{manifest.json, bundle.tar, files/...}`. +6. Calls `gh release create` to publish, uploading every file as a + per-asset attachment so the manifest's per-file URLs resolve. + +A few minutes later every badge in the field with WiFi will see the +SHA-based check find the new version within 6 h, and **Settings β†’ +Check Update** will pull it down on demand. + +For a **beta** channel: `python tools/release.py v1.4.0-rc1 --channel beta`. +Badges only pick up the channel they're configured for (`stable` by +default). + +If you don't have a dev machine handy: the GitHub Actions +[`release` workflow](.github/workflows/release.yml) does the same thing +when you click **Run workflow** in the Actions tab β€” same script under +the hood, no auto-trigger on tag push. + +--- + +## Project values + +- **Friendly is feature.** A confusing-but-correct UI is a bug. +- **Readable comments beat clever code.** This project is meant to be + hackable; if something would surprise a first-time reader, comment it. +- **Hardware is a teammate, not a constraint.** Lean on the chip's + strengths. Don't fight the LDO. +- **Ship small, ship often.** The OTA pipeline is there so we can. + +--- + +## Contact + +- **Email:** hello@elixpo.com +- **GitHub:** https://github.com/elixpo/oreo +- **Maintainer:** [@Circuit-Overtime](https://github.com/Circuit-Overtime) + +Thanks for being here. 🐼 diff --git a/oreo.elixpo/public/favicon.png b/oreo.elixpo/public/favicon.png new file mode 100644 index 0000000..2fc6344 Binary files /dev/null and b/oreo.elixpo/public/favicon.png differ diff --git a/oreo.elixpo/public/icons/IR_Quest_icon.png b/oreo.elixpo/public/icons/IR_Quest_icon.png new file mode 100644 index 0000000..41516b8 Binary files /dev/null and b/oreo.elixpo/public/icons/IR_Quest_icon.png differ diff --git a/oreo.elixpo/public/icons/about_icon.png b/oreo.elixpo/public/icons/about_icon.png new file mode 100644 index 0000000..bef6c80 Binary files /dev/null and b/oreo.elixpo/public/icons/about_icon.png differ diff --git a/oreo.elixpo/public/icons/apps_icon.png b/oreo.elixpo/public/icons/apps_icon.png new file mode 100644 index 0000000..85aa4b9 Binary files /dev/null and b/oreo.elixpo/public/icons/apps_icon.png differ diff --git a/oreo.elixpo/public/icons/badge_icon.png b/oreo.elixpo/public/icons/badge_icon.png new file mode 100644 index 0000000..6937866 Binary files /dev/null and b/oreo.elixpo/public/icons/badge_icon.png differ diff --git a/oreo.elixpo/public/icons/bluetooth_icon.png b/oreo.elixpo/public/icons/bluetooth_icon.png new file mode 100644 index 0000000..f3e66de Binary files /dev/null and b/oreo.elixpo/public/icons/bluetooth_icon.png differ diff --git a/oreo.elixpo/public/icons/cat_games.png b/oreo.elixpo/public/icons/cat_games.png new file mode 100644 index 0000000..bc6aaa5 Binary files /dev/null and b/oreo.elixpo/public/icons/cat_games.png differ diff --git a/oreo.elixpo/public/icons/cat_github.png b/oreo.elixpo/public/icons/cat_github.png new file mode 100644 index 0000000..4c0f1df Binary files /dev/null and b/oreo.elixpo/public/icons/cat_github.png differ diff --git a/oreo.elixpo/public/icons/cat_system.png b/oreo.elixpo/public/icons/cat_system.png new file mode 100644 index 0000000..a863a72 Binary files /dev/null and b/oreo.elixpo/public/icons/cat_system.png differ diff --git a/oreo.elixpo/public/icons/cat_tools.png b/oreo.elixpo/public/icons/cat_tools.png new file mode 100644 index 0000000..99e05e3 Binary files /dev/null and b/oreo.elixpo/public/icons/cat_tools.png differ diff --git a/oreo.elixpo/public/icons/cat_utils.png b/oreo.elixpo/public/icons/cat_utils.png new file mode 100644 index 0000000..d9343f2 Binary files /dev/null and b/oreo.elixpo/public/icons/cat_utils.png differ diff --git a/oreo.elixpo/public/icons/color_icon.png b/oreo.elixpo/public/icons/color_icon.png new file mode 100644 index 0000000..f356b8d Binary files /dev/null and b/oreo.elixpo/public/icons/color_icon.png differ diff --git a/oreo.elixpo/public/icons/commits_breaker_icon.png b/oreo.elixpo/public/icons/commits_breaker_icon.png new file mode 100644 index 0000000..41007b4 Binary files /dev/null and b/oreo.elixpo/public/icons/commits_breaker_icon.png differ diff --git a/oreo.elixpo/public/icons/commits_icon.png b/oreo.elixpo/public/icons/commits_icon.png new file mode 100644 index 0000000..36a5e3d Binary files /dev/null and b/oreo.elixpo/public/icons/commits_icon.png differ diff --git a/oreo.elixpo/public/icons/elixpo_pet_icon.png b/oreo.elixpo/public/icons/elixpo_pet_icon.png new file mode 100644 index 0000000..e461260 Binary files /dev/null and b/oreo.elixpo/public/icons/elixpo_pet_icon.png differ diff --git a/oreo.elixpo/public/icons/elixpo_sketch_icon.png b/oreo.elixpo/public/icons/elixpo_sketch_icon.png new file mode 100644 index 0000000..fee4870 Binary files /dev/null and b/oreo.elixpo/public/icons/elixpo_sketch_icon.png differ diff --git a/oreo.elixpo/public/icons/flappy_icon.png b/oreo.elixpo/public/icons/flappy_icon.png new file mode 100644 index 0000000..1e6721c Binary files /dev/null and b/oreo.elixpo/public/icons/flappy_icon.png differ diff --git a/oreo.elixpo/public/icons/gallery_icon.png b/oreo.elixpo/public/icons/gallery_icon.png new file mode 100644 index 0000000..e5aa48a Binary files /dev/null and b/oreo.elixpo/public/icons/gallery_icon.png differ diff --git a/oreo.elixpo/public/icons/gamepad_icon.png b/oreo.elixpo/public/icons/gamepad_icon.png new file mode 100644 index 0000000..131306e Binary files /dev/null and b/oreo.elixpo/public/icons/gamepad_icon.png differ diff --git a/oreo.elixpo/public/icons/home_bg.png b/oreo.elixpo/public/icons/home_bg.png new file mode 100644 index 0000000..1d0edad Binary files /dev/null and b/oreo.elixpo/public/icons/home_bg.png differ diff --git a/oreo.elixpo/public/icons/identity_icon.png b/oreo.elixpo/public/icons/identity_icon.png new file mode 100644 index 0000000..c98c601 Binary files /dev/null and b/oreo.elixpo/public/icons/identity_icon.png differ diff --git a/oreo.elixpo/public/icons/notifications_icon.png b/oreo.elixpo/public/icons/notifications_icon.png new file mode 100644 index 0000000..135d78f Binary files /dev/null and b/oreo.elixpo/public/icons/notifications_icon.png differ diff --git a/oreo.elixpo/public/icons/racer_icon.png b/oreo.elixpo/public/icons/racer_icon.png new file mode 100644 index 0000000..b3c396b Binary files /dev/null and b/oreo.elixpo/public/icons/racer_icon.png differ diff --git a/oreo.elixpo/public/icons/reader_icon.png b/oreo.elixpo/public/icons/reader_icon.png new file mode 100644 index 0000000..f5c8de9 Binary files /dev/null and b/oreo.elixpo/public/icons/reader_icon.png differ diff --git a/oreo.elixpo/public/icons/settings_icon.png b/oreo.elixpo/public/icons/settings_icon.png new file mode 100644 index 0000000..b9054b4 Binary files /dev/null and b/oreo.elixpo/public/icons/settings_icon.png differ diff --git a/oreo.elixpo/public/icons/snake_icon.png b/oreo.elixpo/public/icons/snake_icon.png new file mode 100644 index 0000000..652cf77 Binary files /dev/null and b/oreo.elixpo/public/icons/snake_icon.png differ diff --git a/oreo.elixpo/public/icons/storage_icon.png b/oreo.elixpo/public/icons/storage_icon.png new file mode 100644 index 0000000..2bab4f3 Binary files /dev/null and b/oreo.elixpo/public/icons/storage_icon.png differ diff --git a/oreo.elixpo/public/icons/store_icon.png b/oreo.elixpo/public/icons/store_icon.png new file mode 100644 index 0000000..2e0f6ca Binary files /dev/null and b/oreo.elixpo/public/icons/store_icon.png differ diff --git a/oreo.elixpo/public/icons/wallpaper_icon.png b/oreo.elixpo/public/icons/wallpaper_icon.png new file mode 100644 index 0000000..0e3078d Binary files /dev/null and b/oreo.elixpo/public/icons/wallpaper_icon.png differ diff --git a/oreo.elixpo/public/icons/wifi_icon.png b/oreo.elixpo/public/icons/wifi_icon.png new file mode 100644 index 0000000..fd617e6 Binary files /dev/null and b/oreo.elixpo/public/icons/wifi_icon.png differ diff --git a/oreo.elixpo/public/mascot.png b/oreo.elixpo/public/mascot.png new file mode 100644 index 0000000..c5a64cc Binary files /dev/null and b/oreo.elixpo/public/mascot.png differ diff --git a/oreo.elixpo/src/app/apps/[slug]/_components/DetailIcon.tsx b/oreo.elixpo/src/app/apps/[slug]/_components/DetailIcon.tsx new file mode 100644 index 0000000..b532334 --- /dev/null +++ b/oreo.elixpo/src/app/apps/[slug]/_components/DetailIcon.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { useState } from "react"; +import { motion } from "framer-motion"; +import type { AppEntry } from "@/data/apps"; + +/* Big hero icon for the detail route. Renders the real PNG with a + * subtle scale-in + pixelated rendering so the badge artwork stays + * crisp at 128 px. Wrapped in a client boundary because we want the + * graceful fallback (Lucide isn't loaded here intentionally β€” if the + * PNG fails we just show the first letter, keeping the bundle thin). */ + +export default function DetailIcon({ + app, tintRing, +}: { + app: AppEntry; + tintRing: string; +}) { + const [pngOk, setPngOk] = useState(true); + return ( + + {app.pngIcon && pngOk ? ( + // eslint-disable-next-line @next/next/no-img-element + {app.name} setPngOk(false)} + className="h-full w-full object-contain" + style={{ imageRendering: "pixelated" }} + /> + ) : ( + + {app.name[0]?.toUpperCase()} + + )} + + ); +} diff --git a/oreo.elixpo/src/app/apps/[slug]/page.tsx b/oreo.elixpo/src/app/apps/[slug]/page.tsx new file mode 100644 index 0000000..d09fa5d --- /dev/null +++ b/oreo.elixpo/src/app/apps/[slug]/page.tsx @@ -0,0 +1,281 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import Link from "next/link"; +import { + ArrowLeft, Github, Tag, User, FileJson, Hash, Layers, ArrowRight, +} from "lucide-react"; +import { ALL_CATALOG, findApp, type AppEntry } from "@/data/apps"; +import DetailIcon from "./_components/DetailIcon"; + +/* /apps/[slug]/ β€” per-app detail route. + * + * Static export means we need every reachable URL pre-generated at + * build time. `generateStaticParams` returns one entry per app in + * `ALL_CATALOG`; the build emits an HTML file for each, so Cloudflare + * Pages can serve them as plain static assets β€” no runtime params. + * + * The page itself is a server component (no `use client`) because all + * its data is build-time-known. The hero "blurred logo" backdrop is + * pure CSS, no canvas, so the route loads instantly with zero JS + * cost. The header / footer animations still come along via the root + * layout. + */ + +// `dynamicParams = false` locks the route to the slugs we declare β€” +// any other URL 404s instead of trying to render at request time. +// Required for `output: "export"` to know what to pre-render. +export const dynamicParams = false; + +export async function generateStaticParams() { + return ALL_CATALOG.map((a) => ({ slug: a.urlSlug })); +} + +// Next 15 made `params` async β€” it's now a Promise that must be +// awaited before its fields can be read. The shape is otherwise +// unchanged. This was the single most common Next 14 β†’ 15 migration +// gotcha; flagged here so future edits don't drop the await. +type RouteParams = Promise<{ slug: string }>; + +export async function generateMetadata({ + params, +}: { + params: RouteParams; +}): Promise { + const { slug } = await params; + const app = findApp(slug); + if (!app) return { title: "Not found" }; + return { + title: `${app.name} Β· Oreo`, + description: app.blurb, + openGraph: { + title: `${app.name} Β· Oreo`, + description: app.blurb, + images: app.pngIcon ? [{ url: app.pngIcon }] : undefined, + }, + }; +} + +const TINT_BG: Record = { + primary: "rgba(255, 93, 104, 0.22)", + teal: "rgba( 61, 220, 151, 0.20)", + gold: "rgba(255, 209, 102, 0.20)", + lilac: "rgba(162, 155, 254, 0.22)", +}; +const TINT_TEXT: Record = { + primary: "text-primary", + teal: "text-teal", + gold: "text-gold", + lilac: "text-lilac", +}; +const TINT_RING: Record = { + primary: "ring-primary/50", + teal: "ring-teal/50", + gold: "ring-gold/50", + lilac: "ring-lilac/50", +}; + +export default async function AppDetail({ + params, +}: { + params: RouteParams; +}) { + const { slug } = await params; + const app = findApp(slug); + if (!app) notFound(); + + // Pick a few "related" apps in the same category for the bottom + // strip. Keep the original ordering so it's deterministic across + // builds (no Math.random). + const related = ALL_CATALOG + .filter((a) => a.urlSlug !== app.urlSlug && a.category === app.category) + .slice(0, 3); + + const bgUrl = app.pngIcon ?? ""; + + return ( +
+ {/* ── BLURRED LOGO BACKDROP ───────────────────────────────────── + Two stacked layers: + 1. The PNG icon itself, scaled up and heavily blurred β€” the + "out-of-focus poster" feel. + 2. A tinted gradient overlay using the app's brand colour + to keep the page readable and on-theme. + Both layers are pointer-events-none so they never intercept + taps on the content above. */} + {bgUrl && ( + + ); +} + +function Meta({ + icon, label, value, mono = false, +}: { + icon: React.ReactNode; + label: string; + value: string; + mono?: boolean; +}) { + return ( +
+

+ {icon} {label} +

+

+ {value} +

+
+ ); +} + +function RelatedIcon({ app }: { app: AppEntry }) { + return ( +
+ {app.pngIcon ? ( + // eslint-disable-next-line @next/next/no-img-element + + ) : ( + {app.name[0]} + )} +
+ ); +} diff --git a/oreo.elixpo/src/app/apps/page.tsx b/oreo.elixpo/src/app/apps/page.tsx new file mode 100644 index 0000000..3721ee7 --- /dev/null +++ b/oreo.elixpo/src/app/apps/page.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { motion } from "framer-motion"; +import { Reveal, staggerContainer, fadeUp } from "@/components/MotionWrap"; +import AppCard from "@/components/AppCard"; +import { PRELOADED, ALL_APPS, STORE } from "@/data/apps"; + +export default function AppsPage() { + return ( +
+ {/* Ambient glows so the page feels like part of the brand. */} +
+
+
+
+ +
+ {/* Centred header */} + + + {ALL_APPS.length + STORE.length} apps and counting + + + Everything that ships, +
+ + and everything that streams in. + +
+ + The preloaded set lands on the badge at flash time. The store + pulls fresh apps from GitHub at runtime β€” no laptop, no + recompile, no developer mode toggle. + +
+ + {/* Preloaded β€” centred */} + +
+

Preloaded

+

+ Shipped with v1. Edit or remove any of them locally. +

+
+
+
+ {PRELOADED.map((a, i) => )} +
+ + {/* Full catalogue */} + +
+

More on the badge

+

+ Settings, tools, and games sit one drawer-tap away. +

+
+
+
+ {ALL_APPS.filter(a => !PRELOADED.find(p => p.slug === a.slug)) + .map((a, i) => )} +
+ + {/* Store */} + +
+

From the store

+

+ Community apps installable at runtime from{" "} + apps_market/ on the repo. +

+
+
+
+ {STORE.map((a, i) => )} +
+ + + + +
+
+ ); +} diff --git a/oreo.elixpo/src/app/badge/page.tsx b/oreo.elixpo/src/app/badge/page.tsx new file mode 100644 index 0000000..7b6318b --- /dev/null +++ b/oreo.elixpo/src/app/badge/page.tsx @@ -0,0 +1,176 @@ +"use client"; + +import { motion } from "framer-motion"; +import { Reveal, fadeUp, staggerContainer } from "@/components/MotionWrap"; +import BadgeMockup from "@/components/BadgeMockup"; +import { + Cpu, MemoryStick, Battery, Radio, Usb, Layers, + Layout, Ruler, Github, +} from "lucide-react"; + +const SPECS = [ + { Icon: Cpu, k: "MCU", v: "ESP32-S3 dual core @ 240 MHz" }, + { Icon: MemoryStick, k: "Memory", v: "16 MB flash Β· 8 MB PSRAM" }, + { Icon: Radio, k: "Radio", v: "WiFi 2.4 GHz Β· BLE 5 Β· IR transceiver" }, + { Icon: Layout, k: "Display", v: "ST7789 240Γ—320 IPS Β· portrait" }, + { Icon: Battery, k: "Power", v: "USB-C + LiPo deep-sleep Β· ~5 Β΅A standby" }, + { Icon: Usb, k: "I/O", v: "8-button matrix Β· IMU Β· 1-wire IR Β· IΒ²C bus" }, + { Icon: Ruler, k: "Dimensions", v: "55 Γ— 90 mm portrait PCB" }, + { Icon: Layers, k: "Layers", v: "4-layer FR-4 Β· 1.6 mm Β· ENIG finish" }, +]; + +const STACK = [ + { lvl: "apps/", body: "Userland β€” manifest.json + main.py per app.", + tint: "from-primary/30 to-primary/5" }, + { lvl: "oreoOS/", body: "The OS: launcher, store, OTA, notifications, file transfer.", + tint: "from-lilac/30 to-lilac/5" }, + { lvl: "oreoWare/", body: "HAL / Board Support: drivers for screen, buttons, IMU, BLE, WiFi, IR, battery.", + tint: "from-teal/30 to-teal/5" }, + { lvl: "MicroPython", body: "Runtime β€” we credit it loudly; we did not write it.", + tint: "from-gold/30 to-gold/5" }, +]; + +export default function BadgePage() { + return ( +
+ {/* Soft brand glows */} +
+
+
+
+
+ +
+ {/* ── HERO: copy left, mockup right ───────────────────────────── */} +
+ + + ESP32-S3-DevKitC Β· breadboard phase + + + The hardware,{" "} + + all open. + + + + Tufty-classic portrait layout. Eight buttons, IR for line-of-sight + quests, an MPU6050 for shake and tilt, four LEDs around the frame. + Schematics and BOM live on the repo β€” fork them and roll your own. + + + + Schematics + BOM + + + Repo + + + + + + + +
+ + {/* ── SCHEMATICS CALLOUT ──────────────────────────────────────── */} + +
+
+
+ +
+
+

Full schematics

+

+ KiCad project (4-layer FR-4, ENIG finish), Gerbers, and a + full BOM with substitutions live in docs/hardware/. + PCB v1 fab files coming soon. +

+ + Coming soon + +
+
+
+
+ + {/* ── SPEC GRID ────────────────────────────────────────────── */} +
+ {SPECS.map((s, i) => ( + +
+ +
+
+

{s.k}

+

{s.v}

+
+
+ ))} +
+ + {/* ── LAYERED ARCHITECTURE ────────────────────────────────── */} + +
+

+ Layered architecture +

+

+ Four layers, each can be rewritten without the others noticing. +

+
+
+ +
+ {STACK.map((row, i) => ( + +
+ {row.lvl} + + L{STACK.length - i} + +
+

{row.body}

+
+ ))} +
+
+
+ ); +} diff --git a/oreo.elixpo/src/app/contribute/page.tsx b/oreo.elixpo/src/app/contribute/page.tsx new file mode 100644 index 0000000..bb38223 --- /dev/null +++ b/oreo.elixpo/src/app/contribute/page.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { motion } from "framer-motion"; +import { Reveal, fadeUp, staggerContainer } from "@/components/MotionWrap"; +import MarkdownView from "@/components/MarkdownView"; +import { + GitPullRequest, Bug, BookOpen, Code2, Heart, ExternalLink, +} from "lucide-react"; + +const TRACKS = [ + { Icon: Code2, title: "Write an app", + body: "manifest.json + main.py and you're in the drawer. Look at apps/snake/ or apps/badge/ for ~50-line templates.", + href: "https://github.com/elixpo/oreo/blob/main/CONTRIBUTING.md#building-an-app", + tint: "text-primary" as const }, + { Icon: Bug, title: "Fix a bug", + body: "Open issues are labelled by area (os / store / ota / wifi / bt). 'good first issue' tags exist; pair them with a serial log.", + href: "https://github.com/elixpo/oreo/issues", + tint: "text-teal" as const }, + { Icon: BookOpen, title: "Write a hack", + body: "Drop Markdown into docs/hacks/ β€” we feature it on /hacks the next deploy.", + href: "https://github.com/elixpo/oreo/tree/main/docs/hacks", + tint: "text-gold" as const }, + { Icon: GitPullRequest, title: "Improve the OS", + body: "Performance, polish, new services. Big changes β€” open a draft PR early so we can review the shape.", + href: "https://github.com/elixpo/oreo/tree/main/oreoOS", + tint: "text-lilac" as const }, +]; + +export default function ContributePage() { + return ( +
+ {/* Soft glows so the page reads as part of the brand even + without the canvas hero. */} +
+
+
+
+ +
+ + + made by humans, with humans + + + Contribute to Oreo.{" "} + + Bring snacks. + + + + OreoOS is open: code, hardware, prompts, assets, the lot. + Pick a lane β€” or invent one. We merge fast and credit loudly. + + + + {/* Contribution tracks */} +
+ {TRACKS.map((t, i) => ( + +
+ +
+
+

{t.title}

+

{t.body}

+ + Open + +
+
+ ))} +
+ + {/* CONTRIBUTING.md β€” fetched from /public at runtime and rendered + through a tiny inline markdown formatter. The file is also a + real on-disk asset at /CONTRIBUTING.md if a contributor wants + the source. */} + +
+
+

+ The full contributing guide +

+ + view raw β†’ + +
+

+ Mirrored verbatim from the repo's CONTRIBUTING.md. Edits to + the source on GitHub re-deploy here automatically. +

+ +
+
+
+
+ ); +} diff --git a/oreo.elixpo/src/app/get-started/page.tsx b/oreo.elixpo/src/app/get-started/page.tsx new file mode 100644 index 0000000..a513a9b --- /dev/null +++ b/oreo.elixpo/src/app/get-started/page.tsx @@ -0,0 +1,114 @@ +"use client"; + +import Link from "next/link"; +import { motion } from "framer-motion"; +import { Reveal, fadeUp, staggerContainer } from "@/components/MotionWrap"; +import { Download, Terminal, Usb, Github } from "lucide-react"; + +const STEPS = [ + { n: "01", t: "Flash MicroPython", b: "Download the ESP32-S3 build, hold the BOOT button, flash via esptool. Two-minute job." }, + { n: "02", t: "Clone the OS", b: "`git clone https://github.com/elixpo/oreo` and copy `.env.example` β†’ `.env` with your WiFi credentials." }, + { n: "03", t: "Deploy", b: "`python tools/deploy.py` β€” pushes everything over USB, skips unchanged files, bumps the version." }, + { n: "04", t: "Open the badge", b: "Drawer β†’ Settings β†’ WiFi β†’ Send files. Cloudflare-served `/upload` works from any phone on the same WiFi." }, +]; + +export default function GetStartedPage() { + return ( +
+
+
+
+
+
+ + ~15 minutes Β· USB-C + a computer + + From box to badge,{" "} + + in four steps. + + + + The deploy script handles version bumps, hash-cache pruning, + free-space guards, and secrets generation. You hold the BOOT + button, you wait, you tap A. + + + + +
    + {STEPS.map((s, i) => ( + +
    + {s.n} +
    +

    {s.t}

    +

    {s.b}

    +
    +
    +
    + ))} +
+
+ + +
+ } title="MicroPython firmware" + body="ESP32-S3 build matching your flash size." + href="https://micropython.org/download/ESP32_GENERIC_S3/" /> + } title="mpremote" + body="Talks to the badge over USB serial." + href="https://docs.micropython.org/en/latest/reference/mpremote.html" /> + } title="Repo" + body="Source, schematics, CHANGELOG." + href="https://github.com/elixpo/oreo" /> +
+
+ + +
+

Need help?

+

+ Open an issue with your serial-console log + (mpremote connect /dev/ttyACM0 repl) β€” + the project's print breadcrumbs make most boot failures + diagnosable in one round trip. +

+
+
+
+
+ ); +} + +function CTA({ icon, title, body, href }: { icon: React.ReactNode; title: string; body: string; href: string }) { + return ( + +
+ {icon} +
+
+

{title}

+

{body}

+
+ + ); +} diff --git a/oreo.elixpo/src/app/globals.css b/oreo.elixpo/src/app/globals.css new file mode 100644 index 0000000..488651d --- /dev/null +++ b/oreo.elixpo/src/app/globals.css @@ -0,0 +1,133 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* ── Design tokens ─────────────────────────────────────────────────── + * + * Two parallel naming schemes coexist on the site: + * + * β€’ Brand tokens (`bg`, `card`, `primary` …) β€” declared in + * theme.js, re-exported via tailwind.config.ts, used by everything + * we wrote by hand. + * + * β€’ shadcn-style semantic tokens (`--background`, `--foreground`, + * `--primary`, `--border` …) β€” needed by the canvas-wave hero + * and any drop-in shadcn component we add later. They're populated + * from the same brand palette so visually they always agree. + * + * Adding a new colour: update theme.js first, then mirror into the + * shadcn variable here if a third-party component needs it. + */ +:root { + color-scheme: dark; + --font-display: "Pixelify Sans", "JetBrains Mono", ui-sans-serif, system-ui, + sans-serif; + + /* shadcn-compatible semantic tokens (kept in oklch-adjacent hex + so we can paste in shadcn components without rewriting them) */ + --background: #0F0C1C; + --foreground: #F5E6DC; + --card: #1C1A2E; + --card-foreground: #F5E6DC; + --popover: #1F1B33; + --popover-foreground: #F5E6DC; + --primary: #FF5D68; + --primary-foreground: #0F0C1C; + --secondary: #A29BFE; + --secondary-foreground: #0F0C1C; + --muted: #1F1B33; + --muted-foreground: #8A8294; + --accent: #3DDC97; + --accent-foreground: #0F0C1C; + --destructive: #FF5D68; + --destructive-foreground:#0F0C1C; + --border: #2A2640; + --input: #2A2640; + --ring: #FF5D68; + --radius: 0.65rem; +} + +html, body { + background: var(--background); + color: var(--foreground); + font-family: theme("fontFamily.sans"); + font-feature-settings: "ss01" 1, "ss02" 1, "cv11" 1; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +/* Subtle ambient gradient layer β€” sits behind everything so the page + feels alive even before any component animates. Kept very faint so + it never fights real content. */ +body::before { + content: ""; + position: fixed; + inset: 0; + background: + radial-gradient(60% 50% at 25% 15%, rgba(255, 93, 104, 0.10), transparent 60%), + radial-gradient(50% 45% at 80% 90%, rgba(162, 155, 254, 0.08), transparent 60%), + radial-gradient(40% 35% at 90% 10%, rgba(61, 220, 151, 0.06), transparent 60%); + pointer-events: none; + z-index: -1; +} + +::selection { + background: theme("colors.primary"); + color: theme("colors.bg"); +} + +/* ── Reusable components ─────────────────────────────────────────── */ +@layer components { + .container-page { + @apply mx-auto w-full max-w-6xl px-6 sm:px-8; + } + .btn-primary { + @apply inline-flex items-center gap-2 rounded-md bg-primary px-5 py-2.5 + font-semibold text-bg transition-colors hover:bg-primary-dim; + } + .btn-ghost { + @apply inline-flex items-center gap-2 rounded-md border border-border + px-5 py-2.5 text-text transition-colors hover:bg-card; + } + .chip { + @apply inline-flex items-center gap-1.5 rounded-pill bg-card-sub px-2.5 py-1 + text-xs uppercase tracking-wider text-text-dim; + } + .card-surface { + @apply rounded-lg border border-border bg-card transition-colors; + } + /* Conic-gradient border treatment used on hero CTAs and accent + cards β€” produces a thin animated luminous edge. Pairs with the + wave hero for visual continuity. */ + .ring-glow { + position: relative; + isolation: isolate; + } + .ring-glow::before { + content: ""; + position: absolute; + inset: -1px; + border-radius: inherit; + padding: 1px; + background: conic-gradient( + from var(--ring-angle, 0deg), + theme("colors.primary"), + theme("colors.lilac"), + theme("colors.teal"), + theme("colors.primary") + ); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; + opacity: 0.7; + animation: spin 8s linear infinite; + } + @keyframes spin { + to { --ring-angle: 360deg; transform: rotate(360deg); } + } +} + +html { overscroll-behavior: none; } diff --git a/oreo.elixpo/src/app/hacks/page.tsx b/oreo.elixpo/src/app/hacks/page.tsx new file mode 100644 index 0000000..9e36a8a --- /dev/null +++ b/oreo.elixpo/src/app/hacks/page.tsx @@ -0,0 +1,149 @@ +"use client"; + +import { motion } from "framer-motion"; +import { Reveal, fadeUp, staggerContainer } from "@/components/MotionWrap"; +import { Clock, Wrench, ExternalLink } from "lucide-react"; + +type Hack = { + slug: string; + title: string; + body: string; + difficulty: "beginner" | "intermediate" | "advanced"; + mins: number; + href: string; +}; + +const HACKS: Hack[] = [ + { slug: "github-handle", title: "Set Your GitHub Handle", + body: "Edit secrets.py so the badge app pulls your live GitHub profile stats.", + difficulty: "beginner", mins: 5, + href: "https://github.com/elixpo/oreo/blob/main/docs/hacks/github-handle.md" }, + { slug: "gallery-photo", title: "Add Your Photo to Gallery", + body: "Drop a PNG into apps/gallery/assets/raw/, deploy, watch it land in the carousel.", + difficulty: "beginner", mins: 8, + href: "https://github.com/elixpo/oreo/blob/main/docs/hacks/gallery-photo.md" }, + { slug: "commits-brick", title: "Add the Commits Brick-Breaker", + body: "Install the Commits arcade so you can bat merge balls through a wall of green squares.", + difficulty: "beginner", mins: 15, + href: "https://github.com/elixpo/oreo/blob/main/docs/hacks/commits-brick.md" }, + { slug: "custom-theme", title: "Theme the OS in your colours", + body: "Edit oreoOS/theme.py to retint the whole UI in a single commit.", + difficulty: "beginner", mins: 10, + href: "https://github.com/elixpo/oreo/blob/main/docs/hacks/custom-theme.md" }, + { slug: "ir-quest", title: "Build an IR-Quest beacon", + body: "Wire a TSOP38 to a spare ESP and beam tokens to nearby badges.", + difficulty: "intermediate", mins: 30, + href: "https://github.com/elixpo/oreo/blob/main/docs/hacks/ir-quest.md" }, + { slug: "imu-tilt", title: "Tilt-to-scroll any app", + body: "Pull the MPU6050 accel readings into your update() and remap UP/DOWN.", + difficulty: "intermediate", mins: 25, + href: "https://github.com/elixpo/oreo/blob/main/docs/hacks/imu-tilt.md" }, + { slug: "custom-app", title: "Ship your first app", + body: "manifest.json + main.py β€” 30 lines, lands in the drawer.", + difficulty: "intermediate", mins: 45, + href: "https://github.com/elixpo/oreo/blob/main/CONTRIBUTING.md#building-an-app" }, + { slug: "ota-self-host", title: "Self-host OTA updates", + body: "Point the OTA module at your own GitHub fork β€” ship private builds to your badge.", + difficulty: "advanced", mins: 60, + href: "https://github.com/elixpo/oreo/blob/main/docs/hacks/ota-fork.md" }, + { slug: "pcb-design", title: "Spin a PCB", + body: "Use the KiCad project in /docs/hardware/ as a starting point for a v2 board.", + difficulty: "advanced", mins: 240, + href: "https://github.com/elixpo/oreo/tree/main/docs/hardware" }, +]; + +const DIFF_TINT = { + beginner: { bg: "bg-teal/10", text: "text-teal", border: "border-teal/30" }, + intermediate: { bg: "bg-gold/10", text: "text-gold", border: "border-gold/30" }, + advanced: { bg: "bg-lilac/10", text: "text-lilac", border: "border-lilac/30" }, +}; + +export default function HacksPage() { + return ( +
+
+
+
+
+
+
+ + + open hardware, open invitation + + + Customize Your Badge.{" "} + + Make it weird. + + + + Step-by-step recipes for retinting the OS, writing your first + app, or wiring extra sensors. Each hack is one Markdown file + on the repo β€” copy, paste, deploy. + + + +
+ {HACKS.map((h, i) => { + const t = DIFF_TINT[h.difficulty]; + return ( + +
+

{h.title}

+ + {h.difficulty} + +
+

{h.body}

+
+ + + {h.mins < 60 ? `${h.mins} min` : `${Math.round(h.mins / 60)} h`} + + + Try this hack + +
+
+ ); + })} +
+ + +
+

Got an idea for a hack?

+

+ Write it up as Markdown, drop it in docs/hacks/, + send a PR. We'll surface it here. +

+ + Open hacks folder on GitHub + +
+
+
+
+ ); +} diff --git a/oreo.elixpo/src/app/layout.tsx b/oreo.elixpo/src/app/layout.tsx new file mode 100644 index 0000000..69bf92a --- /dev/null +++ b/oreo.elixpo/src/app/layout.tsx @@ -0,0 +1,98 @@ +import type { Metadata, Viewport } from "next"; +import "./globals.css"; +import Header from "@/components/Header"; +import Footer from "@/components/Footer"; +import PageTransition from "@/components/PageTransition"; + +// SEO + social-card metadata. The og-banner.png referenced here is the +// same artwork used as the README banner on the main repo β€” generated +// from `prompts/site_assets.md` and dropped into /public/og-banner.png. +// Keeping a single image for both surfaces means a contributor only +// has to refresh one file when the brand shifts. + +const SITE_TITLE = "OreoOS β€” a Python OS in a pocket-sized badge"; +const SITE_DESCRIPTION = + "Open hardware. Open firmware. 20+ apps, on-device store, OTA over " + + "WiFi, AirDrop-style file transfer. MicroPython on ESP32-S3."; +const SITE_URL = "https://oreo.elixpo.com"; +const OG_IMAGE = "/og-banner.png"; + +export const metadata: Metadata = { + metadataBase: new URL(SITE_URL), + title: { + default: SITE_TITLE, + template: "%s Β· Oreo", + }, + description: SITE_DESCRIPTION, + applicationName: "OreoOS", + authors: [{ name: "Elixpo", url: "https://github.com/elixpo" }], + generator: "Next.js", + keywords: [ + "OreoOS", "Elixpo Badge", "conference badge", "MicroPython", + "ESP32-S3", "open hardware", "open source OS", "app store", + "file transfer", "BLE", "WiFi", "IR quest", + ], + // Per-route titles override `default` via Next's metadata API; + // robots gets allow-everywhere here because the site is fully public. + robots: { + index: true, follow: true, + googleBot: { index: true, follow: true, "max-image-preview": "large" }, + }, + openGraph: { + title: SITE_TITLE, + description: SITE_DESCRIPTION, + url: SITE_URL, + siteName: "Oreo", + type: "website", + locale: "en_US", + images: [{ + url: OG_IMAGE, + width: 1200, + height: 630, + alt: "Oreo Badge β€” a Python OS in a pocket-sized conference badge", + }], + }, + twitter: { + card: "summary_large_image", + title: SITE_TITLE, + description: SITE_DESCRIPTION, + images: [OG_IMAGE], + }, + // Icons β€” pulled from the same mascot.png the header + footer use, + // so the favicon/apple-touch-icon look identical to the in-page + // wordmark. Pixel-art works at every favicon size; Cloudflare Pages + // serves the file verbatim from /public. + icons: { + icon: [{ url: "/favicon.png", type: "image/png" }], + shortcut: "/favicon.png", + apple: "/favicon.png", + }, + formatDetection: { telephone: false, email: false, address: false }, +}; + +// Next 14 moved themeColor / colorScheme out of `metadata` into a +// separate `viewport` export β€” they're per-render rather than static. +export const viewport: Viewport = { + themeColor: "#0F0C1C", + colorScheme: "dark", + width: "device-width", + initialScale: 1, +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + +
+
+ {children} +
+
+ + + ); +} diff --git a/oreo.elixpo/src/app/not-found.tsx b/oreo.elixpo/src/app/not-found.tsx new file mode 100644 index 0000000..fce5789 --- /dev/null +++ b/oreo.elixpo/src/app/not-found.tsx @@ -0,0 +1,65 @@ +"use client"; + +import Link from "next/link"; +import { motion } from "framer-motion"; +import { Home, Github, Compass } from "lucide-react"; + +/* 404 β€” uses the same wave gradient palette as the hero so a misroute + * still feels like part of the product. No canvas here because we want + * the page to be as cheap to render as possible (some misrouted bots + * will hammer it). */ + +export default function NotFound() { + return ( +
+ {/* Soft gradient glows behind the content */} +
+
+
+
+ + +

+ 404 / not found +

+

+ + Off the map. + +

+

+ The route you tapped doesn't exist on this build of the site. + Maybe it's an IR-Quest beacon hidden in a different timeline β€” + or maybe a typo. Head back home and we'll forget this ever + happened. +

+ +
+ + Back home + + + Browse apps + + + Report a broken link + +
+
+
+ ); +} diff --git a/oreo.elixpo/src/app/page.tsx b/oreo.elixpo/src/app/page.tsx new file mode 100644 index 0000000..bdabc6c --- /dev/null +++ b/oreo.elixpo/src/app/page.tsx @@ -0,0 +1,132 @@ +import Link from "next/link"; +import { Cpu, Wifi, Bluetooth, Code2 } from "lucide-react"; +import { Reveal } from "@/components/MotionWrap"; +import AppCard from "@/components/AppCard"; +import WavesHero from "@/components/WavesHero"; +import { PRELOADED } from "@/data/apps"; + +const FEATURES = [ + { + Icon: Cpu, + title: "Python all the way down", + body: + "MicroPython on ESP32-S3. Apps are a manifest.json + main.py β€” write one in ~30 lines and it shows up in the drawer.", + }, + { + Icon: Wifi, + title: "AirDrop, the open-hardware way", + body: + "WiFi-based file transfer with on-badge approval. 6-digit code, beacon handshake, RGB565 conversion in the browser.", + }, + { + Icon: Bluetooth, + title: "Peer presence (soon)", + body: + "BT will return for proximity-based features β€” IR-quest assists, sync gestures, badge-to-badge nudges.", + }, +]; + +export default function Home() { + return ( + <> + {/* ── Reactive canvas hero ─────────────────────────────────────── */} + + + {/* ── FEATURE TRIO ─────────────────────────────────────────────── */} +
+ +

+ Three things make the badge feel alive +

+
+
+ {FEATURES.map((f, i) => ( + +
+
+ +
+

{f.title}

+

+ {f.body} +

+
+
+ + ))} +
+
+ + {/* ── APPS CAROUSEL ────────────────────────────────────────────── */} +
+ +
+
+

+ Preloaded apps +

+

+ Ship with the badge. Customise or replace any of them. +

+
+ + All apps β†’ + +
+
+ +
+ {PRELOADED.map((app, i) => ( + + ))} +
+
+ + {/* ── CTA ──────────────────────────────────────────────────────── */} +
+ +
+
+ +
+

+ Build your own apps.
+ It's a manifest and a main.py. +

+

+ The badge ships with 20 first-party apps and an on-device store + that pulls from GitHub at runtime. Fork the repo, drop your app + in apps_market/ + {" "}and submit a PR β€” the next person to refresh the store will see it. +

+
+ + Read the contributing guide + + + Setup the hardware + +
+
+
+
+ + ); +} diff --git a/oreo.elixpo/src/app/upload/page.tsx b/oreo.elixpo/src/app/upload/page.tsx new file mode 100644 index 0000000..11058bc --- /dev/null +++ b/oreo.elixpo/src/app/upload/page.tsx @@ -0,0 +1,438 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { + ArrowRight, ShieldCheck, Wifi, RefreshCw, Lock, Loader2, +} from "lucide-react"; +import { Reveal } from "@/components/MotionWrap"; + +/* The /upload route is the public on-ramp to the badge's local file + * transfer flow. Browsers block HTTPS β†’ HTTP fetches (mixed content), + * and the badge speaks HTTP only β€” so this page collects the 6-char + * code shown on the badge, then hands the user off to + * http://oreo.local/?prefill= in a new tab where the badge's + * own gated upload page picks up from there. + * + * The UI mirrors the badge's local page: six independent code cells + * with auto-advance, paste-friendly behaviour, and ambiguous-character + * filtering. The Open button sits below the cells (not beside them) + * so the card stays balanced at every viewport width. + */ + +const CODE_LEN = 6; +const CODE_CELL_OK = /^[A-HJ-NP-Z2-9]$/i; // single char accepted into a cell + +// mDNS on ESP32 MicroPython is unreliable in the wild (depends on IDF +// build flags, router multicast forwarding, and the client OS's +// happiness with multicast DNS). We default the badge address to +// `oreo.local` for the lucky case but let the user type the raw IP +// the badge prints on its own Send Files screen as a fallback. +const DEFAULT_HOST = "oreo.local"; + +// Accept hostnames OR bare IPv4 addresses with an optional port. +// Examples that match: `oreo.local`, `192.168.1.42`, `192.168.1.42:80`. +const ADDR_OK = /^[A-Za-z0-9.-]+(:\d{1,5})?$/; + +// The badge derives a short hash from its current rotating code and +// expects `?prefill=` (NOT the raw code) on the local page. +// Keeping the raw code out of the URL means it never lands in browser +// history, referer headers, or shared screenshots. The hash is the +// first 4 bytes of SHA-256(code.toUpperCase()), hex-encoded β€” matches +// `code_hash()` in oreoOS/http_server.py exactly. +async function codeHash(code: string): Promise { + const buf = new TextEncoder().encode(code.toUpperCase()); + const hash = await crypto.subtle.digest("SHA-256", buf); + return Array.from(new Uint8Array(hash).slice(0, 4)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +} + +export default function UploadPage() { + const [cells, setCells] = useState(() => Array(CODE_LEN).fill("")); + const [error, setError] = useState(""); + const [phase, setPhase] = useState<"enter" | "loading" | "handoff">("enter"); + const [host, setHost] = useState(DEFAULT_HOST); + const [hash, setHash] = useState(""); + const refs = useRef>([]); + refs.current = refs.current.slice(0, CODE_LEN); + + // Pull a last-used host from localStorage on first render so a + // returning user doesn't have to re-type their badge's IP every + // session. Only restored on the client; SSR sees DEFAULT_HOST. + useEffect(() => { + try { + const saved = localStorage.getItem("oreo-badge-host"); + if (saved) setHost(saved); + } catch { /* private mode β€” fine */ } + }, []); + + const code = cells.join("").toUpperCase(); + const complete = code.length === CODE_LEN; + const hostValid = ADDR_OK.test(host.trim()); + + // Focus the first cell on mount. + useEffect(() => { refs.current[0]?.focus(); }, []); + + // Pre-compute the hash whenever the code is complete so the click + // handler can use it synchronously β€” crypto.subtle.digest is async, + // and awaiting it inside handoff() would break the user-gesture + // flag that popup blockers rely on. + useEffect(() => { + if (!complete) { setHash(""); return; } + let cancelled = false; + codeHash(code).then((h) => { if (!cancelled) setHash(h); }); + return () => { cancelled = true; }; + }, [code, complete]); + + function setCell(i: number, raw: string) { + const v = (raw || "").toUpperCase(); + + // Paste of multiple chars β€” distribute across cells starting at i. + if (v.length > 1) { + const parts = v.replace(/[^A-HJ-NP-Z2-9]/g, "").split("").slice(0, CODE_LEN - i); + const next = [...cells]; + parts.forEach((c, k) => { next[i + k] = c; }); + setCells(next); + const last = Math.min(CODE_LEN - 1, i + parts.length); + refs.current[last]?.focus(); + setError(""); + return; + } + + // Single char β€” filter then auto-advance. + if (v && !CODE_CELL_OK.test(v)) return; + const next = [...cells]; + next[i] = v; + setCells(next); + if (v && i < CODE_LEN - 1) refs.current[i + 1]?.focus(); + setError(""); + } + + function onKeyDown(i: number, e: React.KeyboardEvent) { + if (e.key === "Backspace" && !cells[i] && i > 0) refs.current[i - 1]?.focus(); + if (e.key === "ArrowLeft" && i > 0) refs.current[i - 1]?.focus(); + if (e.key === "ArrowRight" && i < CODE_LEN - 1) refs.current[i + 1]?.focus(); + if (e.key === "Enter" && complete) handoff(); + } + + // Computed once per render so the visible "Open transfer" link + // ALSO points at the live URL β€” that way if window.open is + // blocked, the user can right-click β†’ "open in new tab" on the + // anchor we render. + const rawHost = host.trim(); + const safeHost = ADDR_OK.test(rawHost) ? rawHost : DEFAULT_HOST; + const targetUrl = `http://${safeHost}/?prefill=${encodeURIComponent(hash)}`; + + function handoff(e?: React.SyntheticEvent) { + // Prevent the default form submit so the page doesn't reload + // (which would otherwise wipe our state mid-handoff). + e?.preventDefault(); + if (!complete) { + setError(`Code must be ${CODE_LEN} characters.`); + return; + } + if (!hash) { + // Hash hasn't finished computing yet β€” extremely unlikely since + // SHA-256 of 6 bytes is sub-millisecond, but guard anyway so we + // never open a URL with an empty prefill. + setError("Hashing code… try again."); + return; + } + const h = host.trim() || DEFAULT_HOST; + if (!ADDR_OK.test(h)) { + setError(`"${h}" isn't a valid hostname or IP.`); + return; + } + try { localStorage.setItem("oreo-badge-host", h); } catch {} + + // Canonicalize the destination URL from a strictly validated host. + const normalizedHost = host.trim().toLowerCase(); + if (!ADDR_OK.test(normalizedHost)) { + setError("Enter a valid badge address (hostname or IPv4, optional :port)."); + return; + } + const url = new URL(`http://${normalizedHost}/`); + url.searchParams.set("prefill", hashHex); + const safeTargetUrl = url.toString(); + + // ── Fire window.open SYNCHRONOUSLY inside the user gesture ── + // Wrapping it in setTimeout (even with a tiny delay) makes + // browsers treat the call as scripted and block the popup. We + // open the new tab first; the "loading" UI is rendered after. + const opened = window.open(safeTargetUrl, "_blank", "noopener,noreferrer"); + if (!opened) { + // Popup blocked anyway. Fall back to a top-level navigation β€” + // this loses the website tab but at least gets the user to + // the badge. They can use the browser's back button to return. + window.location.href = safeTargetUrl; + return; + } + // Show the loading state while the new tab is spinning up the + // FTP-style transfer on the badge. After a brief delay we clear + // the code cells and return to the empty entry form so the user + // can start a fresh transfer without manually wiping the input. + setPhase("loading"); + window.setTimeout(() => { + setCells(Array(CODE_LEN).fill("")); + setHash(""); + setError(""); + setPhase("enter"); + // Re-focus the first cell so the user can immediately type the + // next code if they're sending a second file. + refs.current[0]?.focus(); + }, 1400); + } + + return ( +
+ +
+ + peer-to-peer Β· local network only + +

+ Send to your badge. +
No accounts. No cloud. +

+

+ Open Settings β†’ WiFi β†’ Send files on + the badge. Enter the 6-character code shown there to start a + gated, same-network transfer. Files never leave your LAN. +

+
+
+ +
+ + {phase === "enter" ? ( + + + + {/* Six code cells β€” independent inputs so the user can't + fat-finger more than one char per slot. Auto-advance + + paste-distribute logic lives in setCell(). */} +
+ {cells.map((v, i) => ( + { refs.current[i] = el; }} + value={v} + onChange={(e) => setCell(i, e.target.value)} + onKeyDown={(e) => onKeyDown(i, e)} + onFocus={(e) => e.target.select()} + maxLength={2} + inputMode="text" + autoComplete="off" + spellCheck={false} + aria-label={`Code character ${i + 1}`} + className="h-16 w-12 rounded-md border border-border bg-bg + text-center font-mono text-3xl font-semibold uppercase + text-primary outline-none transition-colors + placeholder:text-muted-deep + focus:border-primary focus:ring-2 focus:ring-primary/30 + sm:h-20 sm:w-14 sm:text-4xl" + /> + ))} +
+ + {/* Helper / error line β€” fixed-height so the layout + doesn't jump as the message changes. */} +
+ {error ? ( + + {error} + + ) : ( + + Six characters Β· skips ambiguous shapes (no 0/O/1/I/L) + + )} +
+ + {/* Badge address. Default `oreo.local` works on networks + where multicast DNS resolves; otherwise the user + types the IP printed on the badge's Send Files page. */} +
+ + { setHost(e.target.value); setError(""); }} + spellCheck={false} + autoComplete="off" + autoCapitalize="off" + inputMode="url" + placeholder="oreo.local" + className={`mx-auto mt-2 block w-full max-w-xs rounded-md + border bg-bg px-3 py-2.5 text-center + font-mono text-base text-text outline-none + transition-colors placeholder:text-muted-deep + ${hostValid + ? "border-border focus:border-primary/70" + : "border-primary/50 focus:border-primary"}`} + /> +

+ Default oreo.local works + on networks where mDNS resolves. Otherwise type the IP + shown on the badge's Send Files page (e.g.{" "} + 192.168.1.42). +

+
+ + + + {/* Backup link β€” visible only after the form is valid. + Lets the user right-click β†’ "open in new tab" if the + Submit button's window.open got blocked by their + browser (most common on iOS Safari + Firefox + strict-popup-blocking modes). */} + {complete && hostValid && hash && ( +

+ Button blocked?{" "} + + Open this link manually + . +

+ )} + + {/* Three info tiles below β€” explain the model without + making the user read paragraphs. */} +
+ + + +
+
+ ) : phase === "loading" ? ( + +
+ +
+

+ Opening local page for FTP transfer… +

+

+ A new tab is loading{" "} + http://{host || DEFAULT_HOST}. + Approve the session on the badge to start sending files. +

+
+ +
+
+ ) : ( + +
+ +
+

+ Transfer tab opened. +

+

+ Make sure your device is on the same WiFi as the badge, + approve the session, and pick a file. +

+ +
+ )} +
+ + +
+

Why two pages?

+

+ Browsers block HTTPS pages from talking to plain-HTTP + endpoints. Cloudflare serves this page over HTTPS; the + badge speaks HTTP on your LAN. The handoff puts you on + the badge's own page so the upload itself stays on the + local network β€” no cloud, no proxy, no metadata leakage. +

+
+
+
+
+ ); +} + +function Tile({ + Icon, title, body, +}: { + Icon: React.ComponentType<{ className?: string }>; + title: string; + body: string; +}) { + return ( +
+ +

{title}

+

{body}

+
+ ); +} diff --git a/oreo.elixpo/src/components/AppCard.tsx b/oreo.elixpo/src/components/AppCard.tsx new file mode 100644 index 0000000..22a26c3 --- /dev/null +++ b/oreo.elixpo/src/components/AppCard.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { motion } from "framer-motion"; +import { + Contact, Bird, Image as ImageIcon, Worm, Compass, BookOpen, Car, + Cloud, GitCommit, User, Gamepad2, HardDrive, Palette, PawPrint, + Cpu, Wifi, Bluetooth, RefreshCw, Settings, ArrowUpRight, + type LucideIcon, +} from "lucide-react"; +import type { AppEntry, AppIconId } from "@/data/apps"; + +const ICONS: Record = { + Contact, Bird, Image: ImageIcon, Worm, Compass, BookOpen, Car, Cloud, + GitCommit, User, Gamepad2, HardDrive, Palette, PawPrint, Cpu, Wifi, + Bluetooth, RefreshCw, Settings, +}; + +type Tint = { ring: string; glow: string; text: string }; + +const TINT: Record = { + primary: { ring: "ring-primary/40", glow: "shadow-[0_0_36px_rgba(255,93,104,0.22)]", text: "text-primary" }, + teal: { ring: "ring-teal/40", glow: "shadow-[0_0_36px_rgba(61,220,151,0.20)]", text: "text-teal" }, + gold: { ring: "ring-gold/40", glow: "shadow-[0_0_36px_rgba(255,209,102,0.18)]",text: "text-gold" }, + lilac: { ring: "ring-lilac/40", glow: "shadow-[0_0_36px_rgba(162,155,254,0.20)]",text: "text-lilac" }, +}; + +/* Icon tile reused by AppCard + the detail-page related strip β€” keeps + * the "real PNG with letter fallback" logic in one place. */ +function IconTile({ + app, tint, Icon, +}: { + app: AppEntry; + tint: Tint; + Icon: LucideIcon; +}) { + const [pngOk, setPngOk] = useState(true); + return ( +
+ {app.pngIcon && pngOk ? ( + // eslint-disable-next-line @next/next/no-img-element + setPngOk(false)} + className="h-full w-full object-contain" + style={{ imageRendering: "pixelated" }} + loading="lazy" + decoding="async" + /> + ) : ( + + )} +
+ ); +} + +export default function AppCard({ + app, + index = 0, +}: { + app: AppEntry; + index?: number; +}) { + const tint = TINT[app.tint] ?? TINT.primary; + const Icon = ICONS[app.icon] ?? Cpu; + + return ( + + +
+ +
+
+

+ {app.name} +

+ {app.category} +
+

+ {app.blurb} +

+
+ +
+ + + + + ); +} diff --git a/oreo.elixpo/src/components/BadgeMockup.tsx b/oreo.elixpo/src/components/BadgeMockup.tsx new file mode 100644 index 0000000..31060be --- /dev/null +++ b/oreo.elixpo/src/components/BadgeMockup.tsx @@ -0,0 +1,147 @@ +"use client"; + +import { motion } from "framer-motion"; + +/* SVG mockup of the actual hardware β€” portrait PCB with a screen + * top-60%, two rows of four tactile buttons below, IR transceiver at + * the top edge, four corner LEDs, and a USB-C cutout on the bottom. + * + * Pure SVG so the whole thing scales sharply at any resolution, and + * the animation is just ``/`framer-motion` over the on-screen + * app-tile grid β€” costs ~0% CPU after the initial paint. + * + * Drawing units are tuned so 1 SVG unit β‰ˆ 1 mm on the PCB; final + * artwork lives at viewBox="0 0 100 160". + */ + +const APP_TILES: { x: number; y: number; tint: string; glyph: string }[] = [ + { x: 6, y: 4, tint: "#FF5D68", glyph: "B" }, + { x: 28, y: 4, tint: "#3DDC97", glyph: "S" }, + { x: 50, y: 4, tint: "#FFD166", glyph: "G" }, + { x: 72, y: 4, tint: "#A29BFE", glyph: "F" }, + { x: 6, y: 26, tint: "#A29BFE", glyph: "R" }, + { x: 28, y: 26, tint: "#FF5D68", glyph: "Q" }, + { x: 50, y: 26, tint: "#3DDC97", glyph: "W" }, + { x: 72, y: 26, tint: "#FFD166", glyph: "C" }, +]; + +const BUTTON_LABELS = ["HOME", "A", "B", "C", "UP", "DOWN", "LEFT", "RIGHT"]; + +export default function BadgeMockup({ className = "" }: { className?: string }) { + return ( + + {/* PCB body */} + + + + + + + + + + + + + + + + {/* Outer body with rounded corners */} + + + {/* IR window at top centre */} + + IR + + {/* LCD bezel */} + + + + {/* App-tile grid on the screen */} + {APP_TILES.map((t, i) => ( + + + + + {t.glyph} + + + ))} + + {/* "Receiving 47%" pretend progress bar in screen footer */} + + + + {/* Button matrix β€” 2 rows Γ— 4 buttons */} + {Array.from({ length: 8 }).map((_, i) => { + const col = i % 4; + const row = Math.floor(i / 4); + const x = 8 + col * 22; + const y = 80 + row * 22; + return ( + + + + + {BUTTON_LABELS[i]} + + + ); + })} + + {/* IMU + decoupling caps along the right edge */} + + IMU + + {/* USB-C connector at the bottom */} + + USB-C + + {/* Corner LEDs β€” animated pulse */} + {[ + { cx: 6, cy: 14 }, + { cx: 94, cy: 14 }, + { cx: 6, cy: 144 }, + { cx: 94, cy: 144 }, + ].map((p, i) => ( + + + + + ))} + + ); +} diff --git a/oreo.elixpo/src/components/Footer.tsx b/oreo.elixpo/src/components/Footer.tsx new file mode 100644 index 0000000..6bf3c8d --- /dev/null +++ b/oreo.elixpo/src/components/Footer.tsx @@ -0,0 +1,139 @@ +"use client"; + +import Link from "next/link"; +import { Github, Heart, Star, GitFork } from "lucide-react"; +import { useGithubStats } from "@/lib/useGithubStats"; + +function fmtCount(n: number | null): string { + if (n === null) return "–"; + if (n >= 10_000) return (n / 1000).toFixed(1) + "k"; + if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "k"; + return String(n); +} + +// One column row. `external` is optional; absent β†’ internal Link. +type FooterLink = { label: string; href: string; external?: boolean }; +type FooterColumn = { title: string; links: FooterLink[] }; + +const COLUMNS: FooterColumn[] = [ + { + title: "Project", + links: [ + { label: "Get Started", href: "/get-started/" }, + { label: "Badge", href: "/badge/" }, + { label: "Apps", href: "/apps/" }, + { label: "Hacks", href: "/hacks/" }, + { label: "Contribute", href: "/contribute/" }, + { label: "Upload", href: "/upload/" }, + ], + }, + { + title: "Source", + links: [ + { label: "OreoOS", href: "https://github.com/elixpo/oreo", external: true }, + { label: "Hardware", href: "https://github.com/elixpo/oreo/tree/main/docs", external: true }, + { label: "Contributing", href: "https://github.com/elixpo/oreo/blob/main/CONTRIBUTING.md", external: true }, + { label: "License", href: "https://github.com/elixpo/oreo/blob/main/LICENSE", external: true }, + ], + }, + { + title: "Community", + links: [ + { label: "Contributors", href: "https://github.com/elixpo/oreo/graphs/contributors", external: true }, + { label: "Issues", href: "https://github.com/elixpo/oreo/issues", external: true }, + { label: "Changelog", href: "https://github.com/elixpo/oreo/blob/main/CHANGELOG.md", external: true }, + { label: "Code of Conduct", href: "https://github.com/elixpo/oreo/blob/main/CODE_OF_CONDUCT.md", external: true }, + ], + }, +]; + +export default function Footer() { + const { stars, forks } = useGithubStats(); + return ( +
+
+
+ {/* Brand block */} +
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Oreo mascot +
+ Oreo +
+

+ Python OS, conference badge, app store, OTA. Open hardware, + open firmware, open everything. +

+ + + + elixpo/oreo + + {fmtCount(stars)} + {fmtCount(forks)} + + + +
+ + {/* Link columns */} + {COLUMNS.map((col) => ( +
+

+ {col.title} +

+
    + {col.links.map((l) => ( +
  • + {l.external ? ( + + {l.label} + + ) : ( + + {l.label} + + )} +
  • + ))} +
+
+ ))} +
+ +
+

Β© 2026 Elixpo Β· MIT (code) + trademark carve-out

+

+ Built with on MicroPython. +

+
+
+
+ ); +} diff --git a/oreo.elixpo/src/components/Header.tsx b/oreo.elixpo/src/components/Header.tsx new file mode 100644 index 0000000..98414bb --- /dev/null +++ b/oreo.elixpo/src/components/Header.tsx @@ -0,0 +1,112 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { motion } from "framer-motion"; +import { + Github, Star, GitFork, Home, Rocket, Cpu, LayoutGrid, + Wrench, GitPullRequest, +} from "lucide-react"; +import { useGithubStats } from "@/lib/useGithubStats"; + +const NAV = [ + { href: "/", label: "Home", Icon: Home }, + { href: "/get-started/",label: "Get Started", Icon: Rocket }, + { href: "/badge/", label: "Badge", Icon: Cpu }, + { href: "/apps/", label: "Apps", Icon: LayoutGrid }, + { href: "/hacks/", label: "Hacks", Icon: Wrench }, + { href: "/contribute/", label: "Contribute", Icon: GitPullRequest }, +]; + +function fmtCount(n: number | null): string { + if (n === null) return "–"; + if (n >= 10_000) return (n / 1000).toFixed(1) + "k"; + if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "k"; + return String(n); +} + +export default function Header() { + const pathname = usePathname(); + const { stars, forks } = useGithubStats(); + + return ( + +
+ {/* Logo + wordmark β€” uses the real OreoOS mascot asset, the + same pixel-art panda baked into assets/sprites/optimized/ + on the badge. Pixelated rendering keeps the chunky LCD + artwork crisp at 36 px. */} + +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Oreo mascot +
+ Oreo + + + {/* Centre nav with per-item icons */} + + + {/* Live GitHub stats chip */} + + +
+ elixpo/oreo + + {fmtCount(stars)} + {fmtCount(forks)} + +
+
+
+
+ ); +} diff --git a/oreo.elixpo/src/components/MarkdownView.tsx b/oreo.elixpo/src/components/MarkdownView.tsx new file mode 100644 index 0000000..8eb6110 --- /dev/null +++ b/oreo.elixpo/src/components/MarkdownView.tsx @@ -0,0 +1,223 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { motion } from "framer-motion"; + +/* Hand-rolled minimal Markdown β†’ HTML renderer. + * + * Why not react-markdown / marked / micromark? + * β€’ ~30-80 KB gzipped β€” too much for what's effectively a + * CONTRIBUTING.md panel that renders once on one route. + * β€’ Most of the bundle is feature-completeness we don't need + * (math, mermaid, smart quotes, footnotes). + * + * Supported subset (what CONTRIBUTING.md actually uses): + * β€’ # / ## / ### / #### headings + * β€’ bullet lists (`- ` and `* `) + * β€’ numbered lists (`1. `) + * β€’ bold `**`, italic `*`, inline code `` ` `` + * β€’ fenced code blocks ``` lang + * β€’ blockquotes (`> `) + * β€’ links `[text](url)` + * β€’ horizontal rules `---` + * β€’ paragraphs + * + * Everything else falls through as a plain paragraph β€” safe because + * we sanitise raw HTML in the source on the way in. + */ + +function escapeHtml(s: string): string { + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function renderInline(s: string): string { + let out = escapeHtml(s); + // inline code first so its content isn't re-interpreted + out = out.replace(/`([^`]+)`/g, + '$1'); + // bold (avoid clobbering italics underneath) + out = out.replace(/\*\*([^*]+)\*\*/g, "$1"); + // italic + out = out.replace(/(^|[^*])\*([^*\n]+)\*/g, "$1$2"); + // links β€” only inline form, [text](url) + out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, + '$1'); + return out; +} + +function renderMarkdown(src: string): string { + const lines = src.replace(/\r\n/g, "\n").split("\n"); + const out: string[] = []; + let inCode = false; + let codeLang = ""; + let codeBuf: string[] = []; + let listType: "ul" | "ol" | null = null; + let para: string[] = []; + + const closeList = () => { + if (listType) { out.push(``); listType = null; } + }; + const flushPara = () => { + if (para.length) { + out.push(`

${renderInline(para.join(" "))}

`); + para = []; + } + }; + + for (let raw of lines) { + // ── fenced code blocks ── + const fence = raw.match(/^```(\w*)\s*$/); + if (fence) { + if (inCode) { + out.push( + `
` +
+          escapeHtml(codeBuf.join("\n")) +
+          `
`, + ); + inCode = false; + codeBuf = []; + codeLang = ""; + } else { + flushPara(); closeList(); + inCode = true; + codeLang = fence[1] || ""; + } + continue; + } + if (inCode) { + codeBuf.push(raw); + continue; + } + + // ── horizontal rule ── + if (/^---+\s*$/.test(raw)) { + flushPara(); closeList(); + out.push(`
`); + continue; + } + + // ── headings ── + const h = raw.match(/^(#{1,4})\s+(.*)$/); + if (h) { + flushPara(); closeList(); + const lvl = h[1].length; + const text = renderInline(h[2]); + const klass = { + 1: "mt-10 mb-4 font-display text-4xl tracking-tight text-text", + 2: "mt-10 mb-3 font-display text-2xl tracking-tight text-text", + 3: "mt-7 mb-2 font-display text-xl tracking-tight text-text", + 4: "mt-5 mb-2 font-display text-lg tracking-tight text-text-dim", + }[lvl as 1 | 2 | 3 | 4]; + out.push(`${text}`); + continue; + } + + // ── blockquote ── + const bq = raw.match(/^>\s?(.*)$/); + if (bq) { + flushPara(); closeList(); + out.push( + `
${renderInline(bq[1])}
`, + ); + continue; + } + + // ── list items ── + const ul = raw.match(/^[-*]\s+(.*)$/); + const ol = raw.match(/^\d+\.\s+(.*)$/); + if (ul || ol) { + flushPara(); + const want: "ul" | "ol" = ul ? "ul" : "ol"; + if (listType !== want) { + closeList(); + const klass = want === "ul" + ? "my-3 list-disc space-y-1 pl-6 text-text-dim marker:text-primary/70" + : "my-3 list-decimal space-y-1 pl-6 text-text-dim marker:text-primary/70"; + out.push(`<${want} class="${klass}">`); + listType = want; + } + out.push(`
  • ${renderInline((ul ?? ol)![1])}
  • `); + continue; + } + + // ── blank line β†’ paragraph break ── + if (!raw.trim()) { + flushPara(); closeList(); + continue; + } + + // ── plain paragraph text (join consecutive lines) ── + para.push(raw); + } + flushPara(); closeList(); + if (inCode) { + // Unclosed fence β€” best-effort flush so we don't drop content. + out.push( + `
    ` +
    +      escapeHtml(codeBuf.join("\n")) +
    +      `
    `, + ); + } + return out.join("\n"); +} + +export default function MarkdownView({ + url, fallback, +}: { + url: string; + fallback?: string; +}) { + const [html, setHtml] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + let aborted = false; + (async () => { + try { + const r = await fetch(url); + if (!r.ok) throw new Error("HTTP " + r.status); + const md = await r.text(); + if (!aborted) setHtml(renderMarkdown(md)); + } catch (e) { + if (!aborted) setError((e as Error).message); + } + })(); + return () => { aborted = true; }; + }, [url]); + + if (error) { + return ( +
    + Couldn't load {url}: {error}. + {fallback &&

    {fallback}

    } +
    + ); + } + if (html === null) { + return ( +
    + {[...Array(8)].map((_, i) => ( +
    + ))} +
    + ); + } + return ( + + ); +} diff --git a/oreo.elixpo/src/components/MotionWrap.tsx b/oreo.elixpo/src/components/MotionWrap.tsx new file mode 100644 index 0000000..b1a98c6 --- /dev/null +++ b/oreo.elixpo/src/components/MotionWrap.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { motion, type Variants } from "framer-motion"; + +/* Shared motion presets so every page transition / reveal feels like + it came from the same product. Components import these directly + instead of redeclaring keyframes. The crisp-spring presets are tuned + for "snappy but not bouncy" β€” feels intentional, not gimmicky. */ + +export const fadeUp: Variants = { + hidden: { opacity: 0, y: 16 }, + visible: { opacity: 1, y: 0, + transition: { duration: 0.55, ease: [0.16, 1, 0.3, 1] } }, +}; + +export const fadeIn: Variants = { + hidden: { opacity: 0 }, + visible: { opacity: 1, transition: { duration: 0.4 } }, +}; + +export const staggerContainer: Variants = { + hidden: {}, + visible: { + transition: { staggerChildren: 0.06, delayChildren: 0.04 }, + }, +}; + +export function Reveal({ + children, delay = 0, className = "", +}: { + children: React.ReactNode; + delay?: number; + className?: string; +}) { + return ( + + {children} + + ); +} diff --git a/oreo.elixpo/src/components/PageTransition.tsx b/oreo.elixpo/src/components/PageTransition.tsx new file mode 100644 index 0000000..cf693e5 --- /dev/null +++ b/oreo.elixpo/src/components/PageTransition.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { motion } from "framer-motion"; +import { usePathname } from "next/navigation"; + +/* Lightweight route-change reveal. Just an opacity + y-slide on the + *
    contents, keyed on pathname so Next remounts the wrapper + * each navigation. Deliberately minimal β€” Framer's `AnimatePresence` + * with `mode="wait"` adds a perceptible delay before the new page + * paints, which is the opposite of the "no-lag" feel the user wants. + * + * This is the simplest pattern that still gives the site some + * "transition feel" without ever blocking the new page from showing. + */ + +export default function PageTransition({ + children, +}: { + children: React.ReactNode; +}) { + const pathname = usePathname(); + return ( + + {children} + + ); +} diff --git a/oreo.elixpo/src/components/WavesHero.tsx b/oreo.elixpo/src/components/WavesHero.tsx new file mode 100644 index 0000000..1c642ae --- /dev/null +++ b/oreo.elixpo/src/components/WavesHero.tsx @@ -0,0 +1,390 @@ +"use client"; + +/* + * Reactive canvas hero β€” adapted from the shadcn `glowy-waves-hero` + * reference dropped in plan.md. + * + * Differences from the original: + * β€’ Uses our brand palette + copy instead of shadcn's `--primary` etc. + * (theme tokens are still read from CSS vars so future theme tweaks + * reflow automatically). + * β€’ Replaces the shadcn
    " b"
    " - b"
    " - b"

    ✓ Approved — pick a file:

    " - b"" - b"" + # ── Stage 2: approved, pick a file ── + b"
    " + b"" + b"approved · __DEVICE_ID__" + b"

    Pick a file to send

    " + b"

    Images convert to RGB565 in your browser before upload " + b"(max 240×240). Text and Markdown land in Reader.

    " + b"" + b"
    " + b"
    ?
    " + b"
    " + b"
    " + b"" b"
    " - b"" - b"

    ✓ Sent. Pick another?

    " - b"
    " + b"
    0%" + b"0 / 0 KB
    " + b"
    " + # ── Stage 4: done ── + b"
    " + b"
    " + b"

    Sent!

    Open the matching app on your badge to view it.

    " + b"
    " b"
    " + # ── Custom modal β€” hidden until showModal() flips .open. ── + b"
    " + b"
    !
    " + b"

    Something went wrong

    " + b"

    " + b"
    " + b"" + b"" + b"
    " + b"
    Peer-to-peer on your local network · " + b"Powered by oreo.elixpo.com
    " b"" + b" const v=(r<<11)|(g<<5)|b;out[o++]=(v>>8)&0xff;out[o++]=v&0xff;}" + b" return new Blob([out],{type:'application/octet-stream'});}" + b"async function send(){" + b" if(!picked||!did)return;$('go').disabled=true;$('pct').classList.remove('hide');" + b" let payload=picked,name=picked.name;" + b" if(picked.type.startsWith('image/')){" + b" try{payload=await imgToR565(picked);name=name.replace(/\\.[^.]+$/,'')+'.r565';}" + b" catch(err){" + b" showModal('Image decode failed',String(err),'err',true);" + b" $('go').disabled=false;return;}}" + b" const fd=new FormData();fd.append('f',payload,name);" + b" const xhr=new XMLHttpRequest();activeXhr=xhr;" + b" xhr.upload.addEventListener('progress',(ev)=>{" + b" if(ev.lengthComputable){const p=ev.loaded/ev.total;" + b" $('bar').style.width=(p*100)+'%';" + b" $('pctn').textContent=Math.round(p*100)+'%';" + b" $('pctb').textContent=fmtKB(ev.loaded)+' / '+fmtKB(ev.total);}});" + # onload: success OR a server-side rejection. We treat "100% then + # status===0" as success-with-dropped-response β€” the badge closed + # the socket before the browser finished reading the 200 body, but + # the bytes are already on flash. Reduces the false-alarm rate of + # the "Network error" modal that used to fire here. + b" xhr.onload=()=>{activeXhr=null;if(xhr.status===200||xhr.status===0){" + b" $('form').classList.add('hide');$('done').classList.remove('hide');}" + b" else if(xhr.status===403){" + b" showModal('Device no longer approved'," + b" 'The badge owner revoked this session. Reset to start a new one.'," + b" 'err',true);$('go').disabled=false;}" + b" else{showModal('Upload failed','Server returned status '+xhr.status+'.'," + b" 'err',true);$('go').disabled=false;}};" + b" xhr.onerror=()=>{activeXhr=null;" + # If the upload byte counter reached the total before the error + # fired, the file landed on flash and the badge just closed the + # socket too early. Surface this as a success rather than a scary + # network error. + b" const w=parseInt($('bar').style.width)||0;" + b" if(w>=99){$('form').classList.add('hide');$('done').classList.remove('hide');return;}" + b" showModal('Network error'," + b" 'Lost connection to the badge. Check that both devices are on the same WiFi, then reset.'," + b" 'err',true);$('go').disabled=false;};" + b" xhr.open('POST','/upload?token='+did);xhr.send(fd);}" + b"document.addEventListener('DOMContentLoaded',()=>{" + # No code-entry step any more β€” start the beacon poll + # immediately so the page tracks approval state from the moment + # it loads. + b" beaconTimer=setInterval(beacon,2000);beacon();" + b" $('file').addEventListener('change',e=>onFile(e.target.files[0]));" + b" const dz=$('drop');" + b" ['dragenter','dragover'].forEach(ev=>dz.addEventListener(ev,e=>{e.preventDefault();dz.classList.add('over');}));" + b" ['dragleave','drop'].forEach(ev=>dz.addEventListener(ev,e=>{e.preventDefault();dz.classList.remove('over');}));" + b" dz.addEventListener('drop',e=>{if(e.dataTransfer.files[0])onFile(e.dataTransfer.files[0]);});" + b" $('go').addEventListener('click',send);});" + b"" +) + + +# Served by every endpoint when `transfer_enabled` is False. The +# badge owner long-pressed LEFT on Send Files to close the subsystem +# β€” show them a friendly explanation instead of a bare 503. +_DISABLED_PAGE = ( + b"" + b"" + b"" + b"Transfer disabled" + b"" + b"
    o
    " + b"

    Transfer is disabled

    " + b"

    The badge owner has closed file transfer for safety. " + b"Ask them to re-enable it from the badge's " + b"Settings › WiFi › Send Files page.

    " + b"" +) + + +# Served when GET / arrives without a valid `?prefill=` β€” +# either no query string at all, or a stale prefill from a prior +# code that has since rotated. No code-entry form is exposed here +# (that would let any LAN scanner brute-force the code), just a +# direct pointer back to oreo.elixpo.com/upload where they can +# re-grab a fresh URL. +_NOT_FOUND_PAGE = ( + b"" + b"" + b"" + b"Bad code · Oreo" + b"" + b"
    o
    " + b"

    404

    " + b"

    Bad or expired code

    " + b"

    This page only opens when launched from " + b"oreo.elixpo.com/upload with the current 6-character " + b"code shown on the badge. The code rotates every 5 minutes " + b"— head back and grab a fresh one.

    " + b"Open oreo.elixpo.com/upload" b"" ) @@ -598,51 +1070,127 @@ def _handle(sock): method, full_path, headers = _parse_headers(head) path, qs = _parse_query(full_path) + # ── master kill switch ── + # When the badge owner has flipped the transfer off (long-press + # LEFT on Send Files), every endpoint returns 503 with a tiny + # branded page. We DO still serve /favicon.ico as 204 so the + # browser tab doesn't show a broken icon. + if not _transfer_enabled: + if method == "GET" and path == "/favicon.ico": + _send_status(sock, 204, "No Content", b"") + return + _send_status(sock, 503, "Service Unavailable", + _DISABLED_PAGE) + return + if method == "GET" and path in ("/", "/index.html"): - _send_status(sock, 200, "OK", _UPLOAD_FORM) + # The page is gated on `?prefill=` matching the live + # code's hash. No prefill, wrong prefill, or expired prefill + # all serve the 404 page β€” no code-entry form, no surface + # area for a guesser to brute-force from the LAN. + _handle_root(sock, qs, _peer_addr(sock)) return if method == "GET" and path == "/favicon.ico": _send_status(sock, 204, "No Content", b"") return + if method == "GET" and path == "/mascot.png": + _handle_mascot(sock) + return if method == "GET" and path == "/beacon": _handle_beacon(sock, qs, _peer_addr(sock)) return - if method == "GET" and path == "/session/new": - _handle_session_new(sock, _peer_addr(sock)) - return if method == "POST" and path == "/upload": _handle_upload(sock, headers, after_head, qs) return - _send_status(sock, 404, "Not Found", b"not found") + _send_status(sock, 404, "Not Found", _NOT_FOUND_PAGE) + + +_MASCOT_CACHE = None # bytes lazily loaded on first request + + +def _handle_mascot(sock): + """Serve oreoOS/mascot.png as the page's source. Cached in + RAM after the first read since the file is tiny (~8 KB) and the + page references it on every load.""" + global _MASCOT_CACHE + if _MASCOT_CACHE is None: + try: + with open("oreoOS/mascot.png", "rb") as f: + _MASCOT_CACHE = f.read() + except Exception: + # Fallback path layouts β€” some deploys flatten oreoOS into + # the FS root. Try the bare filename before giving up. + try: + with open("mascot.png", "rb") as f: + _MASCOT_CACHE = f.read() + except Exception: + _MASCOT_CACHE = b"" + if not _MASCOT_CACHE: + _send_status(sock, 404, "Not Found", b"missing mascot") + return + head = ("HTTP/1.1 200 OK\r\n" + "Content-Type: image/png\r\n" + "Content-Length: %d\r\n" + "Cache-Control: public, max-age=86400\r\n" + "Connection: close\r\n\r\n") % len(_MASCOT_CACHE) + try: + sock.send(head.encode()) + sock.send(_MASCOT_CACHE) + except Exception: + pass + +def _handle_root(sock, qs, peer_addr): + """GET / β€” entry point. Auto-authenticates against the prefill + hash and renders the upload page with the device_id inlined. + Wrong / missing prefill renders the branded 404 page so no + randomly-crawled URL ever lands on a working form.""" + prefill = (qs.get("prefill", "") or "").lower() + expected = code_hash().lower() + if not prefill or prefill != expected: + # Two reasons we land here: the URL was hit without a prefill, + # or the prefill was correct ~minutes ago but the code has + # since rotated. Same response either way β€” direct the user + # back to the website to grab a fresh code. + _send_status(sock, 404, "Not Found", _NOT_FOUND_PAGE) + return -def _handle_session_new(sock, peer_addr): - """Hand the sender a freshly-minted session id so it doesn't have - to roll its own (saves us from collisions between two phones that - both picked the same client-side random).""" + # Prefill matched β†’ mint a device session for this client. _prune_sessions() if len(_sessions) >= SESSION_MAX: - _send_status(sock, 503, "Service Unavailable", - b'{"error":"too many active sessions"}', - content_type="application/json") + _send_status(sock, 503, "Service Unavailable", _DISABLED_PAGE) return - sid = _new_session_id() + device_id = _new_session_id() try: import time as _t now = _t.ticks_ms() except Exception: now = 0 - _sessions[sid] = {"state": "pending", "last_ms": now, - "addr": peer_addr, "uploads": 0} - body = ('{"id":"%s","state":"pending"}' % sid).encode() - _send_status(sock, 200, "OK", body, content_type="application/json") + _sessions[device_id] = { + "state": "authed", + "last_ms": now, + "addr": peer_addr, + "uploads": 0, + "authed_at": now, + } + try: + print("[http] auto-authed via prefill: device_id=%s" % device_id) + except Exception: + pass + + # Inline the device_id into the served page by replacing a + # placeholder. Done as a one-pass bytes.replace so we keep the + # form as a compile-time constant and pay the substitution cost + # only on the (rare) success path. + body = _UPLOAD_FORM.replace(b"__DEVICE_ID__", device_id.encode()) + _send_status(sock, 200, "OK", body) def _handle_beacon(sock, qs, peer_addr): - """Heartbeat from a sender's browser. Bumps last_ms so the session - stays alive, and reports the current state so the UI on the phone - can switch from 'Waiting…' to 'Ready' once the badge approves.""" + """Heartbeat from an authed sender's browser. Only refreshes + sessions already in the dict β€” never creates new ones. A sender + that hasn't /auth'd is invisible and stays invisible.""" sid = qs.get("id", "") if not sid or len(sid) != 6: _send_status(sock, 400, "Bad Request", @@ -650,27 +1198,23 @@ def _handle_beacon(sock, qs, peer_addr): content_type="application/json") return _prune_sessions() + s = _sessions.get(sid) + if s is None: + # Session expired or was denied β€” tell the client so it can + # re-prompt for the code. We deliberately do NOT auto-create + # the session here (that's the inverted protocol). + _send_status(sock, 410, "Gone", + b'{"error":"session expired","state":"gone"}', + content_type="application/json") + return try: import time as _t - now = _t.ticks_ms() + s["last_ms"] = _t.ticks_ms() except Exception: - now = 0 - s = _sessions.get(sid) - if s is None: - if len(_sessions) >= SESSION_MAX: - _send_status(sock, 503, "Service Unavailable", - b'{"error":"too many sessions"}', - content_type="application/json") - return - _sessions[sid] = {"state": "pending", "last_ms": now, - "addr": peer_addr, "uploads": 0} - state = "pending" - else: - s["last_ms"] = now - if peer_addr: - s["addr"] = peer_addr - state = s["state"] - body = ('{"id":"%s","state":"%s"}' % (sid, state)).encode() + pass + if peer_addr: + s["addr"] = peer_addr + body = ('{"device_id":"%s","state":"%s"}' % (sid, s["state"])).encode() _send_status(sock, 200, "OK", body, content_type="application/json") @@ -850,6 +1394,30 @@ def _handle_upload(sock, headers, body_prefix, qs): pass _progress = None + # Drain any trailing bytes the browser still has on the wire β€” + # the closing `--boundary--\r\n` and the form-trailer. If we + # close() with unread bytes in the kernel buffer the badge sends + # a TCP RST and the browser surfaces it as a "Network error" even + # though the file is already on flash. A short read loop is + # cheaper than the false-alarm modals it prevents. + try: + sock.settimeout(0.2) + drained = 0 + while drained < 4096: + try: + chunk = sock.recv(512) + except Exception: + break + if not chunk: + break + drained += len(chunk) + except Exception: + pass + try: + sock.settimeout(RECV_TIMEOUT) + except Exception: + pass + if written <= 0: try: _os.remove(dst_path) diff --git a/oreoOS/mascot.png b/oreoOS/mascot.png new file mode 100644 index 0000000..3fdbe7a Binary files /dev/null and b/oreoOS/mascot.png differ diff --git a/oreoOS/notif_panel.py b/oreoOS/notif_panel.py index 6a105c1..5313c4d 100644 --- a/oreoOS/notif_panel.py +++ b/oreoOS/notif_panel.py @@ -311,15 +311,56 @@ def _handle_list(self, btn, items): # ── quick-setting actions ─────────────────────────────────────────── def _toggle_wifi(self): + """Drive the WiFi RADIO state, not the association state. + + The old behaviour was: "if connected β†’ disconnect; else β†’ + connect_from_config". That made the chip look like a no-op + any time auto-association failed: tap β†’ radio up β†’ can't + associate β†’ is_connected() stays False β†’ chip still reads + "OFF" β†’ next tap thinks it's currently off and tries the same + thing again. Now the chip tracks radio_on(): one tap powers + the MAC up (and *tries* to associate), another tap powers it + fully down. Association status surfaces as the sub-label. + """ try: from oreoWare import wifi - if wifi.is_connected(): - wifi.disconnect() + on_now = False + try: + on_now = bool(wifi.is_radio_on()) + except Exception: + # Old build without is_radio_on β€” fall back to the + # association check (no worse than before). + on_now = bool(wifi.is_connected()) + if on_now: + wifi.radio_off() if hasattr(wifi, "radio_off") else wifi.disconnect() else: - wifi.connect_from_config() + if hasattr(wifi, "radio_on"): + wifi.radio_on() + # Pump button input through the wait so the panel can + # still react if the user changes their mind. Older + # wifi.py without pump_cb support falls through to the + # plain blocking call. + try: + wifi.connect_from_config(pump_cb=self._wifi_cancel_pump) + except TypeError: + wifi.connect_from_config() except Exception: pass + def _wifi_cancel_pump(self): + """Called from inside wifi.connect_from_config's wait loop. + Returns True on any keypress so the user can cancel the + in-flight 'searching…' attempt without waiting for the + per-network timeout.""" + try: + self._os.buttons.update() + for b_ in api.BUTTONS: + if self._os.buttons.just_pressed(b_): + return True + except Exception: + pass + return False + def _toggle_bt(self): try: from oreoWare import bt @@ -439,9 +480,24 @@ def _quick_states(self): ] try: from oreoWare import wifi - connected = wifi.is_connected() - out[0]["on"] = bool(connected) - out[0]["sub"] = "on" if connected else "off" + # Two-state chip: connected β†’ ON (with SSID in the sub), + # everything else β†’ OFF. We deliberately don't surface + # a "searching" tier β€” the credentials are known up-front + # via secrets / wifi.json, so a connect either resolves + # in a couple of seconds or it doesn't. + connected = False + try: + connected = bool(wifi.is_connected()) + except Exception: + pass + out[0]["on"] = connected + if connected: + cur = "" + try: cur = wifi.ssid() or "" + except Exception: pass + out[0]["sub"] = (cur[:10] if cur else "on") + else: + out[0]["sub"] = "off" except Exception: pass try: diff --git a/oreoWare/wifi.py b/oreoWare/wifi.py index afe8592..8657be8 100644 --- a/oreoWare/wifi.py +++ b/oreoWare/wifi.py @@ -8,12 +8,24 @@ {"ssid": "Hotspot","password": "...", "priority": 5, "metered": true} ] -`connect_from_config()` walks the list in ascending-priority order -(lower number = tried first) and stops on the first SSID that -associates. `secrets.py` (generated by the deploy script) is only used -as a first-boot bootstrap: if `/wifi.json` doesn't exist yet AND -`secrets.WIFI_SSID` is set, we seed the json with that one network so -the badge associates immediately after the first flash. +Two sources can populate that list: + β€’ `secrets.WIFI_NETWORKS` (from `.env` via `tools/deploy.py`) β€” a + list of {ssid, password, priority, metered} dicts. On every boot + `connect_from_config()` merges this list into `/wifi.json`, + refreshing passwords + priorities and adding any new entries + without touching user-added networks. Edit `.env`, redeploy, + new networks are live. + β€’ The on-device Settings β†’ WiFi β†’ Networks page β€” `add_saved()` / + `remove_saved()` write the same json directly. + +`.env` format is parallel CSV β€” first SSID matches first password: + + WIFI_SSID=home_net,elixpo_srv,office + WIFI_PASSWORD=homepass,srvpass,officepass + +`connect_from_config()` walks the merged list in ascending-priority +order (lower number = tried first) and stops on the first SSID that +associates. Usage: from oreoWare import wifi @@ -41,7 +53,7 @@ _wlan = None _SAVED_PATH = "/wifi.json" -_PER_NET_TIMEOUT = 8000 # ms β€” per-SSID cap so a wrong network is skipped fast +_PER_NET_TIMEOUT = 10000 def _get_wlan(): @@ -80,51 +92,177 @@ def _apply_power_cap(wlan): MDNS_HOSTNAME = "oreo" +_hostname_applied = False # per-WLAN dhcp_hostname / hostname +_global_hostname_applied = False # network.hostname() (mDNS) + + +def _apply_global_hostname(): + """Set the global network hostname BEFORE `wlan.active(True)`. + + This is the one that actually drives the ESP-IDF mDNS responder + β€” once `network.hostname("oreo")` is set and the interface comes + up, IDF auto-advertises `oreo.local` to the LAN. Setting it + AFTER active(True) is a no-op on most builds because the mDNS + service is already configured by then. + + The earlier worry that this call leaves the radio in a half- + initialised state was misplaced β€” the actual culprit was IDF + auto-connecting from cached NVS credentials, which we now + cancel with wlan.disconnect() in connect(). Calling + network.hostname() before any radio activity is safe. + """ + global _global_hostname_applied + if _global_hostname_applied: + return + try: + import network as _net + if hasattr(_net, "hostname"): + _net.hostname(MDNS_HOSTNAME) + _global_hostname_applied = True + except Exception as e: + # Older build without network.hostname() β€” fall through; the + # per-WLAN dhcp_hostname applied later still gets us into the + # router's DHCP-snooping resolver on most home gateways. + try: print("[wifi] network.hostname() failed:", e) + except Exception: pass + + def _apply_hostname(wlan): - """Set the WiFi hostname so the ESP-IDF mDNS responder advertises - `oreo.local` to the LAN. MicroPython builds vary in which kwarg key - actually takes effect (`hostname` vs `dhcp_hostname`) so we try both. - - On most ESP32 IDF builds this is enough β€” IDF auto-spins up an mDNS - responder for the configured hostname. Older builds that don't have - the responder enabled will silently ignore this and `oreo.local` - won't resolve; the badge will still be reachable by IP. + """Per-WLAN hostname config (`dhcp_hostname` / `hostname`). + Called AFTER active+disconnect so the radio is stable. The + global mDNS broadcast was already configured by + `_apply_global_hostname()` before active(True) β€” this is the + secondary "tell the router via DHCP Option 12" path. """ - for key in ("hostname", "dhcp_hostname"): + global _hostname_applied + if _hostname_applied: + return + for key in ("dhcp_hostname", "hostname"): try: wlan.config(**{key: MDNS_HOSTNAME}) + _hostname_applied = True except Exception: pass - # Some forks expose a top-level mdns start. Best-effort. + + +def radio_on(): + """Power up the radio without trying to associate. Lets the user + flip WiFi 'on' from the Settings page even when no saved network + is reachable β€” keeps the toggle from feeling broken when the + badge is somewhere with no known WiFi nearby.""" try: - import network as _net - if hasattr(_net, "hostname"): - _net.hostname(MDNS_HOSTNAME) + wlan = _get_wlan() + wlan.active(True) + return True except Exception: - pass + return False -def connect(ssid, password, timeout_ms=12000): - wlan = _get_wlan() - wlan.active(True) - # Hostname must be set BEFORE associate β€” the DHCP REQUEST carries - # the hostname as Option 12 and the IDF mDNS service uses the same - # name. Setting it after gets ignored by some routers. - _apply_hostname(wlan) - _apply_power_cap(wlan) - if wlan.isconnected() and wlan.config("essid") == ssid: +def radio_off(): + """Power the radio fully down. Releases any active connection + cleanly and saves the ~70 mA the WiFi MAC pulls in idle. Pairs + with `radio_on()` so a UI toggle has a real on/off semantic.""" + try: + wlan = _get_wlan() + try: wlan.disconnect() + except Exception: pass + try: wlan.active(False) + except Exception: pass return True - wlan.connect(ssid, password) + except Exception: + return False + + +def is_radio_on(): + """True iff the WiFi MAC is powered up. Distinct from + `is_connected()`, which only flips true after a full association. + UI 'WiFi: ON/OFF' should track this β€” association state is a + sub-label on the same row.""" + try: + return bool(_get_wlan().active()) + except Exception: + return False + + +def connect(ssid, password, timeout_ms=6000, pump_cb=None): + """Initiate a WiFi association and wait (up to `timeout_ms`) for it + to complete. + + On ESP32-S3, IDF auto-starts a connect attempt with its own + NVS-cached credentials the moment `wlan.active(True)` is called. + If we don't cancel that first, every subsequent `wlan.config()` + or `wlan.connect()` returns `Wifi Internal State Error` because + IDF thinks "sta is already connecting". We unconditionally + `wlan.disconnect()` + sleep briefly to flush that state before + doing any config of our own. + + `pump_cb` is the escape-hatch from the "badge looks frozen during + SEARCH" trap: the wait loop calls it ~12 times per second and + bails immediately if it returns truthy. + """ + wlan = _get_wlan() + # Set the global hostname BEFORE bringing the radio up. The mDNS + # responder in ESP-IDF reads it during interface init; setting + # it later doesn't change the already-broadcast service record. + # This is the single most important line for `oreo.local` to + # actually resolve on the LAN. + _apply_global_hostname() + try: + wlan.active(True) + except Exception as e: + print("[wifi] active(True) raised:", e) + return False + # Cancel any in-flight connect IDF kicked off on active(True). This + # is the single most important line in this function on ESP32 β€” + # without it the next wlan.config()/wlan.connect() returns "sta is + # connecting, cannot set config" and the radio never associates to + # the credentials we actually want. + try: + wlan.disconnect() + except Exception: + pass + # Tiny settle window so the disconnect propagates inside the + # IDF state machine before we issue the new connect. 50 ms is + # enough on every build we've tested. + time.sleep_ms(50) + try: + _apply_hostname(wlan) + except Exception: + pass + try: + _apply_power_cap(wlan) + except Exception: + pass + try: + if wlan.isconnected() and wlan.config("essid") == ssid: + return True + except Exception: + pass + try: + wlan.connect(ssid, password) + except Exception as e: + print("[wifi] wlan.connect raised:", e) + return False start = time.ticks_ms() while not wlan.isconnected(): if time.ticks_diff(time.ticks_ms(), start) > timeout_ms: return False - time.sleep_ms(200) - # Re-apply power-save + hostname AFTER association β€” some IDF - # versions reset PM/hostname state on connect and need it set - # again to actually take effect for the live link. - _apply_power_cap(wlan) - _apply_hostname(wlan) + # Pump first so the very-first frame of "searching..." can be + # cancelled. Sleep duration is intentionally short (~80 ms) so + # button-to-cancel feels instant, but long enough that we don't + # eat all the CPU we're trying to keep available. + if pump_cb: + try: + if pump_cb(): + print("[wifi] connect cancelled by pump_cb") + return False + except Exception: + pass + time.sleep_ms(80) + try: + _apply_power_cap(wlan) + except Exception: + pass return True @@ -164,23 +302,94 @@ def _save_raw(networks): return False -def _bootstrap_from_secrets(): - """First-boot migration: if /wifi.json is missing AND secrets.py - carries a single SSID, materialise it as a one-entry list. Idempotent - β€” once /wifi.json exists this is a no-op.""" - if _exists(_SAVED_PATH): - return _load_saved_raw() +def _secrets_networks(): + """Read the canonical list of deploy-time networks from secrets.py. + + Modern secrets.py (generated by `tools/deploy.py`) ships + WIFI_NETWORKS as a list of {ssid, password, priority, metered} + dicts. Older builds only have the singular WIFI_SSID / WIFI_PASSWORD + pair; we wrap that into a one-entry list so callers don't care. + Returns [] when nothing is configured. + """ try: import secrets - ssid = getattr(secrets, "WIFI_SSID", "") - pw = getattr(secrets, "WIFI_PASSWORD", "") except Exception: - ssid, pw = "", "" - if not ssid: return [] - seed = [{"ssid": ssid, "password": pw, "priority": 1, "metered": False}] - _save_raw(seed) - return seed + nets = getattr(secrets, "WIFI_NETWORKS", None) + if isinstance(nets, (list, tuple)) and nets: + return [dict(n) for n in nets if isinstance(n, dict) and n.get("ssid")] + # Legacy fallback: just the first pair. + ssid = getattr(secrets, "WIFI_SSID", "") + pw = getattr(secrets, "WIFI_PASSWORD", "") + if ssid: + return [{"ssid": ssid, "password": pw, "priority": 1, "metered": False}] + return [] + + +def _sync_secrets_into_saved(): + """Merge the deploy-time WIFI_NETWORKS list into /wifi.json. + + Called once per boot from connect_from_config(). Semantics: + β€’ Every secrets network is ENSURED to be in /wifi.json. If + present already (matched by SSID), its priority is refreshed + and its password is refreshed ONLY when the new password is + non-empty β€” that protects a previously-working credential + from being clobbered if the user trims their .env (e.g. + adds a new SSID slot but doesn't add the matching + WIFI_PASSWORD entry). + β€’ Networks the user added on-device that are NOT in secrets + are preserved as-is. + β€’ If /wifi.json doesn't exist yet, this acts as the initial + bootstrap (replaces the old _bootstrap_from_secrets path). + """ + secret_nets = _secrets_networks() + if not secret_nets: + return + saved = _load_saved_raw() or [] + by_ssid = {n.get("ssid", ""): n for n in saved if n.get("ssid")} + changed = False + added = 0 + refreshed = 0 + for s in secret_nets: + ssid = s.get("ssid", "") + if not ssid: + continue + existing = by_ssid.get(ssid) + if existing is None: + saved.append({ + "ssid": ssid, + "password": s.get("password", ""), + "priority": int(s.get("priority", 10)), + "metered": bool(s.get("metered", False)), + }) + changed = True + added += 1 + continue + # Existing entry β€” refresh priority unconditionally, password + # only if the new value is non-empty. + new_pri = int(s.get("priority", existing.get("priority", 10))) + new_pw = s.get("password", "") or "" + local_changed = False + if int(existing.get("priority", 99)) != new_pri: + existing["priority"] = new_pri + local_changed = True + if new_pw and existing.get("password") != new_pw: + existing["password"] = new_pw + local_changed = True + if local_changed: + changed = True + refreshed += 1 + if changed: + _save_raw(saved) + print("[wifi] secrets sync: +%d new, %d refreshed (of %d)" % + (added, refreshed, len(secret_nets))) + + +def _bootstrap_from_secrets(): + """Compat shim β€” older code paths call this. Delegates to the + sync logic and then returns the freshly-loaded list.""" + _sync_secrets_into_saved() + return _load_saved_raw() def list_saved(): @@ -253,15 +462,59 @@ def is_metered(): return False -def connect_from_config(): +def connect_from_config(pump_cb=None): """Try each saved network in priority order until one associates. + Order of operations: + 0. Merge the .env-defined WIFI_NETWORKS list into /wifi.json + (ensures every secret network is present + has current + creds, without clobbering user-added entries). + 1. /wifi.json β€” the user-managed list, edited via Settings β†’ WiFi. + 2. secrets.py legacy single-pair bootstrap if step 0 produced + nothing somehow. + 3. Otherwise: return False, radio stays off. + Per-network timeout is short (_PER_NET_TIMEOUT) so a stale entry - doesn't stall the boot. Returns True iff we associated to *any* - saved SSID; False if the list is empty or every entry failed. + doesn't stall the boot. """ + # Pull deploy-time networks into the on-flash list. Cheap on + # subsequent calls β€” only re-writes /wifi.json if something + # actually changed since the last sync. + try: + _sync_secrets_into_saved() + except Exception as e: + print("[wifi] secrets sync failed:", e) nets = list_saved() if not nets: + # Last-ditch: try the deploy-time secret directly. Without this + # an empty /wifi.json silently disabled WiFi forever on freshly + # flashed badges β€” the toggle on the Status row would call + # connect_from_config(), find no saved nets, and bail without + # ever bringing the radio up. + try: + import secrets + ssid_ = getattr(secrets, "WIFI_SSID", "") + pw = getattr(secrets, "WIFI_PASSWORD", "") + if ssid_: + print("[wifi] no saved nets, falling back to secrets bootstrap") + # Persist as the new "priority 1" entry so this branch + # only fires once per device β€” subsequent connects use + # the normal path. + try: + add_saved(ssid_, pw, priority=1, metered=False) + except Exception: + pass + ok = False + try: + ok = connect(ssid_, pw, + timeout_ms=_PER_NET_TIMEOUT, + pump_cb=pump_cb) + except Exception: + pass + return ok + except Exception as e: + print("[wifi] secrets bootstrap failed:", e) + print("[wifi] no networks configured") return False for n in nets: ssid_ = n.get("ssid") or "" @@ -269,11 +522,29 @@ def connect_from_config(): continue pw = n.get("password") or "" try: - ok = connect(ssid_, pw, timeout_ms=_PER_NET_TIMEOUT) - except Exception: + print("[wifi] try %s (p=%s)" % + (ssid_, n.get("priority", "?"))) + ok = connect(ssid_, pw, + timeout_ms=_PER_NET_TIMEOUT, + pump_cb=pump_cb) + except Exception as e: + print("[wifi] connect raised:", e) ok = False if ok: + print("[wifi] associated:", ssid_) return True + # If the user cancelled mid-search, stop trying further + # networks β€” they explicitly asked us to give up. Without + # this we'd plough through every saved entry honouring their + # individual timeouts. + if pump_cb is not None: + try: + if pump_cb(): + print("[wifi] connect_from_config cancelled") + return False + except Exception: + pass + print("[wifi] all %d saved networks failed" % len(nets)) return False @@ -492,11 +763,16 @@ def info(): Returns a dict with `connected`, `ssid`, `ip`, `subnet`, `gateway`, `dns`, and `rssi`. Missing fields are None β€” the UI fills with 'β€”'. """ - out = {"connected": False, "ssid": None, "ip": None, + out = {"connected": False, "radio_on": False, + "ssid": None, "ip": None, "subnet": None, "gateway": None, "dns": None, "rssi": None} try: wlan = _get_wlan() + try: + out["radio_on"] = bool(wlan.active()) + except Exception: + pass out["connected"] = bool(wlan.isconnected()) if out["connected"]: cfg = wlan.ifconfig() # (ip, subnet, gateway, dns) diff --git a/prompts/site_assets.md b/prompts/site_assets.md new file mode 100644 index 0000000..8ba8fb3 --- /dev/null +++ b/prompts/site_assets.md @@ -0,0 +1,157 @@ +# Site assets β€” `oreo.elixpo/` + +Image-generation prompts for everything the marketing site needs. +Each block is self-contained: copy the prompt into your generator +of choice (Midjourney, SDXL, DALL-E, Flux), render, then drop the +optimised output into `oreo.elixpo/public/` and reference it from +the relevant component. + +All assets must use the brand palette in `oreo.elixpo/theme.js`: + +| token | hex | use | +|-----------|-----------|--------------------------------------| +| bg | `#0F0C1c` | page background | +| primary | `#FF5D68` | wordmark, CTAs, "active" glow | +| teal | `#3DDC97` | secondary accent | +| gold | `#FFD166` | "metered" / warning accent | +| lilac | `#A29BFE` | tertiary accent | +| text | `#F5E6DC` | foreground text | + +When optimising for shipping: WebP at q=80 for photos, AVIF for hero, +SVG for logo/icons where possible. + +--- + +## 1. `og-banner.png` β€” Open Graph / README banner (1200Γ—630) + +Used for: README header, OG image (Twitter/X, LinkedIn, Discord +preview), `metadata.openGraph.image`. + +``` +A minimalist dark hero image, 1200Γ—630, deep purple-navy background +(#0F0C1C) with a soft radial pink-coral glow (#FF5D68) in the +upper-left. Centred slightly left: a small wireframe illustration of +a portrait pocket-sized conference badge with eight tactile buttons +in two rows below a rectangular screen. The screen shows a tiny app +drawer grid in matching pink. On the right side, large display +typography reads "OreoOS" in pink-coral, with "Python OS in a pocket +badge." below in cream (#F5E6DC). A faint scanline texture overlay +at 5% opacity. No people, no logos other than the wordmark, no +gradients besides the corner glow. Flat geometric style, like an +indie hardware project poster, not photorealistic. +``` + +Output: `oreo.elixpo/public/og-banner.png` and a copy at +`docs/og-banner.png` for the main repo README. + +--- + +## 2. `logo-mark.svg` β€” square logomark (square, scalable) + +Used for: header logo (currently a placeholder `o` glyph), favicon, +app icons on the badge. + +``` +A square pixel-art logomark, 64Γ—64 logical pixels, sharp aliased +edges (no anti-aliasing). Dark purple-navy background (#0F0C1C). +A stylised lowercase 'o' rendered with two cookie-shaped halves β€” +the outer ring in pink-coral (#FF5D68), the inner highlight in +cream (#F5E6DC). The 'o' should read as both a letter AND an Oreo +cookie. No text, no shadow, no glow. Strictly the mark. +``` + +Output: `oreo.elixpo/public/logo-mark.svg` + a `favicon.ico` rendered +at 32px from the same source. + +--- + +## 3. `hero-badge.png` β€” animated hero illustration (800Γ—1000) + +Used for: future home-page hero replacement of the CSS-only floating +tile preview. + +``` +An isometric line-art illustration of the Oreo badge on a clean +desk surface, 800Γ—1000 portrait, viewed from a 35-degree angle. The +badge is a portrait-orientation PCB with a 240Γ—320 screen showing a +3-column app grid in pink-coral tiles on dark background. Around +the screen: eight square tactile buttons in two rows of four, +labelled subtly with HOME / A / B / C / UP / DOWN / LEFT / RIGHT. +A USB-C cable trails out the bottom. Minor LED accents at the four +corners glowing pink. The background is solid dark purple-navy +(#0F0C1C) with a single soft pink radial glow under the badge for +ambient floor light. Style: clean blueprint-poster, flat colours +with thin 2px line art, no gradients on surfaces, no photorealism. +``` + +Output: `oreo.elixpo/public/hero-badge.webp`. + +--- + +## 4. `screen-drawer.png`, `screen-transfer.png`, `screen-store.png` (480Γ—640 each) + +Used for: future "see it in action" carousel. + +Each prompt is the same skeleton; vary only the screen content. + +``` +A pixel-perfect mockup of a single badge screen, 480Γ—640 (showing +the device's 240Γ—320 LCD at 2x). Portrait orientation. Dark +purple-navy chrome around a content area that shows {{SCREEN}}. +Crisp pixel grid, no anti-aliasing, fonts in Pixelify Sans 12px / +24px. Background outside the screen is solid #1F1B33 with a 1px +border at #2A2640. +``` + +Variants: +- `screen-drawer` β€” fill `{{SCREEN}}` with: *"a 3-column grid of 12 + app tiles, each tile is a single coloured square with a centred + glyph; the third tile (Gallery) has a pink ring around it"*. +- `screen-transfer` β€” fill with: *"the SEND FILES page showing + 'oreo.elixpo.com/upload' in pink, a 6-character code 'K9MX72' in + large pink text, a 60% green progress bar, and a small yellow dot + to the right of the code"*. +- `screen-store` β€” fill with: *"the on-device App Store with 4 + rows: 'Color Picker', 'Oreo Pet', 'Flappy Oreo', 'Snake' β€” each + row a coloured icon + name + chevron"*. + +Outputs: `oreo.elixpo/public/screens/.webp` (resize on export). + +--- + +## 5. `panda-mascot.png` β€” Oreo Pet mascot full-size (512Γ—512) + +Used for: 404 page, marketing collateral, future "about the project" +photo. + +``` +A friendly pixel-art panda mascot, 512Γ—512, centred in frame. +Round white body, classic black ear-and-eye-patches, sitting upright +with a small cookie held between its front paws. The cookie has +visible cream filling β€” explicitly an Oreo. Background is solid +dark purple-navy (#0F0C1C). Style: 32Γ—32 pixel-art upscaled to +512px with nearest-neighbour scaling, sharp aliased edges, limited +palette: black, white, cream (#F5E6DC), and a tiny pink-coral +accent (#FF5D68) on the cookie filling and the panda's blush. +``` + +Output: `oreo.elixpo/public/panda.png`. + +--- + +## Optimisation pipeline + +```bash +# WebP for photos (80% quality is the sweet spot for our flat art) +cwebp -q 80 og-banner.png -o og-banner.webp + +# AVIF for the hero +avifenc --min 20 --max 28 hero-badge.png hero-badge.avif + +# SVG β†’ optimised SVG +svgo logo-mark.svg +``` + +Drop the optimised outputs into `oreo.elixpo/public/`. The Next.js +static export will pick them up verbatim β€” no server-side image +optimisation involved. diff --git a/tools/deploy.py b/tools/deploy.py index 35c89ca..122066e 100644 --- a/tools/deploy.py +++ b/tools/deploy.py @@ -426,8 +426,13 @@ def write_secrets_local(): fields = ( # name python repr formatter + # WIFI_SSID / WIFI_PASSWORD remain (as the FIRST entry from + # the .env CSV) so older code paths that read them still + # work. WIFI_NETWORKS is the canonical list that wifi.py + # merges into /wifi.json at boot. ("WIFI_SSID", "%r"), ("WIFI_PASSWORD", "%r"), + ("WIFI_NETWORKS", "%r"), ("WIFI_AUTO_CONNECT","%r"), ("BT_AUTO_ENABLE", "%r"), ("GITHUB_USER", "%r"),