diff --git a/.agents/skills/site-verification/SKILL.md b/.agents/skills/site-verification/SKILL.md
index ba73c5b..f74c410 100644
--- a/.agents/skills/site-verification/SKILL.md
+++ b/.agents/skills/site-verification/SKILL.md
@@ -7,8 +7,7 @@ description: Verify luinbytes.dev route, metadata, interaction, and responsive U
- Run `npm run lint`, `npm run build`, and `npm run test:e2e` after the final implementation. The browser suite builds the production export once and owns its loopback preview server. Do not rebuild `out` while a verification server serves it.
- Use `requirements-test.txt` with an isolated Python environment when the host lacks Playwright. Put its `bin` first in `PATH` for `npm run test:e2e`. Install Chromium with that environment's Playwright.
-- Run `npm run test:pond` when shared site behavior changes, and `npm run test:pip` for the Pip contact contract.
+- Run `npm run test:pond` when shared site behavior changes.
- Inspect fresh screenshots from `test-results/` at desktop and mobile sizes after the last visual edit. Check at least 320px, 390px, and 1440px widths, readable copy, focus, overflow, reduced motion, and content without JavaScript. Record PASS, REVISE, or BLOCK with the evidence and remaining manual acceptance.
-- `/`, `/pip`, and `/privacy` are the sitemap entries. Keep retired routes in `LEGACY_ROUTES` removed. Check exported `out/pip.html`, `out/privacy.html`, page-specific canonical/OG metadata, and the existing shared PNG.
-- Pip's Telegram username is configured once in `lib/pip.ts`. Verify link/clipboard/QR target agreement, clipboard denial, QR load failure, and invalid configuration handling. Never send a message as a browser test. A browser link assertion does not prove a live bot reply.
+- `/` and `/privacy` are the sitemap entries. Keep retired routes in `LEGACY_ROUTES` removed. Check exported `out/privacy.html`, page-specific canonical/OG metadata, and the existing shared PNG.
- The test harness stops its own preview server. Stop any additional servers or browsers you start; retain only intentional evidence and remove temporary validation environments/scripts. Report exact working-tree state.
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index c64c929..4d41693 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -26,7 +26,6 @@ jobs:
- run: npm ci
- run: npm audit --audit-level=high
- run: npm run lint
- - run: npm run test:pip
- run: npm run test:pond
- name: Install browser test dependencies
run: |
diff --git a/.gitignore b/.gitignore
index 399204b..fc0aa70 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,7 +21,6 @@ __pycache__/
.venv/
/out/
-/public/qr/
/.prerender/
/.next*/
diff --git a/README.md b/README.md
index b6cc8cf..845a7c2 100644
--- a/README.md
+++ b/README.md
@@ -4,19 +4,13 @@ Lu's interactive personal portfolio, built around the living pixel pond version
The homepage focuses on Lu's current work at [Orchid.ai](https://orchid.ai), selected public projects, and an interactive procedural pond rather than a catalogue of product pages.
-The site has three public routes: `/`, the interactive portfolio; `/pip`, a static product page for Pip, Lu's warm, easygoing Telegram agent; and `/privacy`, a short personal-use privacy policy. The Pip page is a deliberate standalone exception to the portfolio's homepage-first structure. Retired concept and product URLs resolve to the pond-themed 404 instead of preserving stale copies of older portfolios.
+The site has two public routes: `/`, the interactive portfolio, and `/privacy`, a short personal-use privacy policy. Retired concept and product URLs resolve to the pond-themed 404.
The portfolio features [HomeBot](https://github.com/luinbytes/HomeBot), [rakazo-android](https://github.com/luinbytes/rakazo-android), [Linux Sonar](https://github.com/luinbytes/linux-sonar), [bongocat](https://github.com/luinbytes/bongocat), and [cursor-barrier](https://github.com/luinbytes/cursor-barrier).
-Pip has a dedicated introduction below the homepage hero, a **Meet Pip** link to
-`/pip`, and a navigation link at every screen size. Both pages share the Pip speech-bubble
-logo and Pixelify wordmark from `components/pip/pip-logo.tsx`.
-
The interface follows the system light/dark preference. The pond, fish, cat,
feeding controls, and portrait foil retain their existing behavior in both modes.
-Navigation is available at every screen size, including without JavaScript. Pip
-uses a dedicated pixel illustration with a readable example conversation and
-keeps its Telegram, copy-handle, and on-demand QR contact paths.
+Navigation is available at every screen size, including without JavaScript.
## Stack
@@ -41,7 +35,6 @@ Run the production checks:
```bash
npm run lint
npm run build
-npm run test:pip
npm run test:pond
```
@@ -57,45 +50,31 @@ npm run test:e2e
`npm run preview` serves the built `out` directory at http://127.0.0.1:3004 with real 404 responses. The browser suite uses this production export.
-The `/pip` page is exported with the rest of the site and keeps its contact action
-available as static, readable HTML. Its Telegram target is defined in
-`PIP_TELEGRAM_HANDLE` in `lib/pip.ts`; rebuild after changing it so the
-configured Telegram handle and contact controls are rendered into the exported
-page. Message Pip and copy-handle fallback are available directly, while the
-QR image is generated locally during development setup and production builds.
-It is requested from this site only after Show QR, without a third-party image service. The
-page's canonical URL is
-`https://luinbytes.dev/pip`, and it uses a Pip-specific share card so embeds keep
-the product page's identity.
-
## Deployment
Pushes to `master` build and deploy the static export to [luinbytes.dev](https://luinbytes.dev/) through GitHub Pages.
## Social preview
-Open Graph and Twitter large-image cards use checked-in 1200 x 630 PNGs:
-`public/share-cards/luinbytes-dev-pond.png` for the homepage and
-`public/share-cards/luinbytes-dev-pip.png` for `/pip`. The versioned filenames
-avoid reusing the old pink card's cached image URL. The old PNG remains available
+Open Graph and Twitter large-image cards use the checked-in 1200 x 630 PNG
+`public/share-cards/luinbytes-dev-pond.png` for the homepage. The versioned filename
+avoids reusing the old pink card's cached image URL. The old PNG remains available
for existing links, but no current metadata references it.
-The editable layouts are `scripts/share-card.html` for the homepage and
-`scripts/pip-share-card.html` for Pip. They use local assets/fonts and render
+The editable layout is `scripts/share-card.html` for the homepage. It uses local assets/fonts and renders
offline. To regenerate after an intentional design edit, use the browser-test
setup above, then:
```bash
npm run build
python3 scripts/generate-share-card.py
-python3 scripts/generate-pip-share-card.py
python3 -m unittest discover -s tests/browser -p '*_test.py' -k ShareCardStaticExportTests -v
```
-The generators embed the original card fonts from installed Fontsource packages, render offline, and check loaded
-fonts plus a 32px text-safe inset. Review both PNGs at full size and at typical
+The generator embeds the original card fonts from installed Fontsource packages, renders offline, and checks loaded
+fonts plus a 32px text-safe inset. Review the PNG at full size and at typical
embed size before updating the expected SHA-256 values in
-`ShareCardStaticExportTests` to the generators' printed values. The browser suite builds once and checks the metadata, asset dimensions/hash and absence of old
+`ShareCardStaticExportTests` to the generator's printed value. The browser suite builds once and checks the metadata, asset dimensions/hash and absence of old
image references in exported HTML. Normal Pages builds need no Python, browser,
image service or runtime route: Vite copies the checked-in PNGs into the static export.
@@ -106,7 +85,7 @@ image service or runtime route: Vite copies the checked-in PNGs into the static
## Security and page loading
-Home, Pip, and the 404 page have separate HTML and client entries. Vite generates
+Home, privacy, and the 404 page have separate HTML and client entries. Vite generates
their stylesheet and preload links. Run `node scripts/measure-page-assets.mjs`
after a build to inspect each page's initial uncompressed JavaScript and CSS.
diff --git a/app/entry-pip.tsx b/app/entry-pip.tsx
deleted file mode 100644
index 5b3faf8..0000000
--- a/app/entry-pip.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-import PipPage from "./pip/page";
-import { hydratePage } from "./hydrate-page";
-
-hydratePage(PipPage);
diff --git a/app/pip/page.tsx b/app/pip/page.tsx
deleted file mode 100644
index 555e14e..0000000
--- a/app/pip/page.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import { ArrowUpRight, Bell, Compass, Heart, Search, Send, Smile, Sparkles } from "lucide-react";
-import { getPipContact } from "@/lib/pip";
-import { PipFace, PipLogo } from "@/components/pip/pip-logo";
-import { PipWebSearchDemo } from "@/components/pip/pip-web-search-demo";
-import { Button } from "@/components/ui/button";
-import { SkulletImage } from "@/components/ui/skullet-image";
-import { PipContact } from "./pip-contact";
-import styles from "./pip.module.css";
-
-const features = [
- {
- icon: Bell,
- title: "Reminders",
- text: "Set a reminder in chat for the time you choose.",
- },
- {
- icon: Smile,
- title: "Stickers and images",
- text: "Share images and stickers when words are not quite enough.",
- },
- {
- icon: Compass,
- title: "Maps and weather",
- text: "Find places on a map and check the weather before you head out.",
- },
- {
- icon: Heart,
- title: "Conversation memory",
- text: "Pip can remember details about each person so later chats have more context.",
- },
- {
- icon: Sparkles,
- title: "Everyday advice",
- text: "Talk through meal ideas, second opinions, and small decisions.",
- },
-];
-
-export default function PipPage() {
- const contact = getPipContact();
-
- return (
-
-
-
-
-
-
-
A little help. A little banter.
-
Pip is your Telegram mate for quick answers, useful reminders, and the everyday things on your mind.
-
- {contact ? (
-
Message Pip
- ) : (
-
Message Pip
- )}
-
- {!contact &&
Pip is not available to message right now.
}
-
-
-
-
-
-
-
-
-
-
A little help, in context.
-
A dinner idea, then a reminder. Both fit in the same conversation.
-
-
- Example conversation
-
-
You
-
Got eggs, rice and zero motivation.
-
-
-
Pip
-
Egg fried rice. Ten minutes, one pan. Future you will thank you.
-
-
-
You
-
Remind me to get soy sauce tomorrow at 6pm.
-
-
-
Pip
-
You got it. Tomorrow at 6pm.
-
-
-
-
-
-
-
Small asks. Sorted.
-
Search, remember a detail, or make a quick plan. Pip can help with the small stuff.
-
-
-
-
- Web search
- Ask a quick question or follow a curiosity. Pip searches the web and brings back useful links.
-
-
-
- {features.map(({ icon: Icon, title: featureTitle, text }) => (
-
-
-
-
{featureTitle}
-
{text}
-
-
- ))}
-
-
-
-
-
-
-
-
Easy company. Clear boundaries.
-
Warm, short, and easygoing. Pip keeps things helpful with a gentle redirect when needed, and a firm no for clearly illegal requests.
-
-
-
-
-
-
-
-
- );
-}
diff --git a/app/pip/pip-contact.tsx b/app/pip/pip-contact.tsx
deleted file mode 100644
index 1ff44eb..0000000
--- a/app/pip/pip-contact.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-import { useEffect, useRef, useState } from "react";
-import { ArrowUpRight, Check, Copy, Send, QrCode } from "lucide-react";
-import type { getPipContact } from "@/lib/pip";
-import { Button } from "@/components/ui/button";
-import { loadSkulletOverlay, SKELETON_HOLD_MS, SkulletImage } from "@/components/ui/skullet-image";
-import styles from "./pip.module.css";
-
-function loadQrOverlay() {
- let timer = 0;
- const timeout = new Promise((resolve) => {
- timer = window.setTimeout(() => resolve(false), SKELETON_HOLD_MS);
- });
- return Promise.race([loadSkulletOverlay(), timeout]).finally(() => {
- window.clearTimeout(timer);
- });
-}
-
-export function PipContact({ contact }: { contact: ReturnType }) {
- const [copyStatus, setCopyStatus] = useState<"idle" | "copied" | "failed">("idle");
- const [showQr, setShowQr] = useState(false);
- const [qrImmediate, setQrImmediate] = useState(false);
- const [qrFailed, setQrFailed] = useState(false);
- const qrOpenRequest = useRef(0);
- useEffect(() => () => {
- qrOpenRequest.current++;
- }, []);
-
- async function copyHandle() {
- if (!contact) return;
- try {
- await navigator.clipboard.writeText(contact.handle);
- setCopyStatus("copied");
- } catch {
- setCopyStatus("failed");
- }
- }
-
- return (
-
-
-
-
A question, a half-formed thought, or just a hello. Start wherever you like.
-
-
-
- {contact ? (
- <>
-
PIP’S TELEGRAM HANDLE
-
{contact.handle}
-
- Message Pip
-
-
Opens Pip in Telegram.
- >
- ) : (
- <>
-
A LITTLE HELLO, SOON
-
Chat opens soon.
-
Pip’s public handle is on its way. Check back here to start a chat.
-
- Message Pip
-
- >
- )}
-
-
- {copyStatus === "copied" ? : } Copy handle
-
- {
- const request = ++qrOpenRequest.current;
- if (showQr) {
- setShowQr(false);
- return;
- }
- void loadQrOverlay().then((ready) => {
- if (request !== qrOpenRequest.current) return;
- setQrImmediate(ready);
- setShowQr(true);
- });
- }}
- >
- {showQr ? "Hide QR code" : "Show QR code"}
-
-
-
- {copyStatus === "copied" && "Handle copied."}
- {copyStatus === "failed" && "Couldn’t copy. Select the handle above and copy it manually."}
-
-
- {showQr && contact && (
-
- {qrFailed ? (
-
QR code couldn’t load. Use Message Pip or copy the handle instead.
- ) : (
- <>
-
setQrFailed(true)}
- />
- Point your phone’s camera here to say hello.
- >
- )}
-
- )}
-
-
Without JavaScript, use the handle above or Message Pip. Copy and QR controls need JavaScript.
-
-
- );
-}
diff --git a/app/pip/pip.module.css b/app/pip/pip.module.css
deleted file mode 100644
index 8d3cb6e..0000000
--- a/app/pip/pip.module.css
+++ /dev/null
@@ -1,811 +0,0 @@
-.page {
- --pip-accent: var(--site-accent);
- --pip-accent-soft: var(--glass-selected);
- --pip-action-bg: var(--site-accent);
- --pip-action-text: var(--site-contrast);
- min-height: 100svh;
- color: var(--site-text);
- background: var(--site-bg);
- font-family: var(--font-body, var(--font-manrope, sans-serif));
-}
-
-.page :where(a, button):focus-visible {
- outline: 3px solid var(--site-accent);
- outline-offset: 4px;
-}
-
-.page ::selection {
- color: var(--pip-action-text);
- background: var(--pip-action-bg);
-}
-
-.shell {
- width: min(100%, 1232px);
- margin-inline: auto;
- padding: 0 clamp(16px, 3.6vw, 40px) max(20px, env(safe-area-inset-bottom));
-}
-
-.header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- min-height: 68px;
- margin-top: max(16px, env(safe-area-inset-top));
- padding: 7px 10px 7px 16px;
- border: 1px solid var(--glass-edge);
- border-radius: var(--site-control-radius);
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--site-panel-bg);
- box-shadow: var(--glass-shadow);
- backdrop-filter: blur(14px) saturate(1.16);
-}
-
-.wordmark {
- display: inline-flex;
- align-items: center;
- min-width: 44px;
- min-height: 48px;
- color: var(--pip-accent);
- text-decoration: none;
-}
-
-.wordmark > span {
- color: var(--pip-accent);
-}
-
-.homeLink {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- min-height: 48px;
- padding-inline: 16px;
- border: 1px solid transparent;
- border-radius: var(--site-control-radius);
- color: var(--site-muted);
- font-size: 13px;
- font-weight: 600;
- text-decoration: none;
- white-space: nowrap;
- transition: color var(--ui-duration) var(--ui-ease), border-color var(--ui-duration) var(--ui-ease), transform var(--ui-duration) var(--ui-ease);
-}
-
-.homeLink svg,
-.footer > a svg {
- width: 16px;
- height: 16px;
- flex: 0 0 auto;
-}
-
-.homeLink:hover,
-.footer > a:hover {
- color: var(--site-text);
-}
-
-.homeLink:hover {
- border-color: var(--glass-edge);
- background: var(--glass-selected);
-}
-
-.hero {
- display: grid;
- gap: 20px;
- align-items: center;
- margin-top: clamp(24px, 5vw, 56px);
- padding: clamp(14px, 2vw, 20px);
- border: 1px solid var(--glass-edge);
- border-radius: 32px;
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--site-panel-bg);
- box-shadow: var(--glass-panel-shadow);
- backdrop-filter: blur(16px) saturate(1.14);
-}
-
-.heroCopy {
- max-width: 600px;
- padding: clamp(8px, 2vw, 22px);
-}
-
-.hero h1 {
- margin: 0;
- font-size: clamp(2.55rem, 8.8vw, 4.85rem);
- font-weight: 600;
- line-height: 1;
- letter-spacing: -.065em;
- text-wrap: balance;
-}
-
-.hero h1 > span,
-.sectionHeading h2 > span,
-.contact h2 > span {
- color: var(--pip-accent);
-}
-
-.intro {
- max-width: 38ch;
- margin: 20px 0 24px;
- color: var(--site-muted);
- font-size: 16px;
- line-height: 1.7;
- text-wrap: pretty;
-}
-
-.heroActions {
- display: flex;
- align-items: center;
- gap: 12px;
-}
-
-.primaryAction {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
- min-width: 44px;
- min-height: 52px;
- padding: 13px 20px;
- border: 1px solid color-mix(in srgb, var(--pip-action-bg) 76%, var(--glass-rim));
- border-radius: var(--site-control-radius);
- background: linear-gradient(145deg, rgb(255 255 255 / .06), transparent), var(--pip-action-bg);
- color: var(--pip-action-text);
- font-size: 14px;
- font-weight: 700;
- line-height: 1.2;
- text-decoration: none;
- text-align: center;
- cursor: pointer;
- box-shadow: var(--glass-shadow);
- transition: border-color var(--ui-duration) var(--ui-ease), box-shadow var(--ui-duration) var(--ui-ease), transform var(--ui-duration) var(--ui-ease);
-}
-
-.primaryAction svg {
- width: 18px;
- height: 18px;
- flex: 0 0 auto;
-}
-
-.primaryAction:not(:disabled):hover {
- border-color: var(--glass-rim);
- box-shadow: var(--glass-panel-shadow);
- transform: translateY(-2px);
-}
-
-.primaryAction:not(:disabled):active {
- transform: translateY(1px);
-}
-
-.primaryAction:disabled {
- border-color: var(--glass-edge);
- background: var(--site-surface-soft);
- color: var(--site-muted);
- cursor: not-allowed;
- box-shadow: none;
-}
-
-.heroVisual {
- position: relative;
- width: 100%;
- aspect-ratio: 1.35 / 1;
- overflow: hidden;
- border: 1px solid var(--glass-rim);
- border-radius: 24px;
- background: var(--site-surface-soft);
- box-shadow: var(--glass-shadow);
-}
-
-.heroImage {
- object-fit: cover;
-}
-
-.example {
- display: grid;
- gap: 24px;
- align-items: start;
- margin-top: clamp(48px, 7vw, 84px);
-}
-
-.exampleHeading,
-.sectionHeading {
- max-width: 58ch;
-}
-
-.example h2,
-.sectionHeading h2,
-.personality h2,
-.contact h2 {
- margin: 0;
- font-size: clamp(2rem, 5.4vw, 3.6rem);
- font-weight: 600;
- line-height: 1.05;
- letter-spacing: -.055em;
- text-wrap: balance;
-}
-
-.exampleHeading > p,
-.sectionHeading > p {
- max-width: 48ch;
- margin: 13px 0 0;
- color: var(--site-muted);
- font-size: 15px;
- line-height: 1.7;
- text-wrap: pretty;
-}
-
-.conversation {
- min-width: 0;
- margin: 0;
- padding: clamp(18px, 3.5vw, 30px);
- border: 1px solid var(--glass-edge);
- border-radius: 28px;
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--site-panel-bg);
- box-shadow: var(--glass-panel-shadow);
- backdrop-filter: blur(12px) saturate(1.12);
-}
-
-.conversationCaption {
- display: inline-flex;
- align-items: center;
- min-height: 30px;
- margin-bottom: 10px;
- padding: 5px 11px;
- border: 1px solid var(--glass-edge);
- border-radius: var(--site-control-radius);
- color: var(--site-muted);
- background: var(--glass-fill);
- font-size: 11px;
- font-weight: 600;
- line-height: 1.4;
-}
-
-.exchange {
- display: grid;
- grid-template-columns: 38px minmax(0, 1fr);
- gap: 10px;
- align-items: start;
- padding: 8px 0;
-}
-
-.speaker {
- padding-top: 13px;
- color: var(--site-muted);
- font-family: var(--font-mono, var(--font-space-mono, monospace));
- font-size: 10px;
- line-height: 1.5;
-}
-
-.exchange:nth-of-type(even) {
- grid-template-columns: minmax(0, 1fr) 38px;
-}
-
-.exchange:nth-of-type(even) .speaker {
- order: 2;
- color: var(--pip-accent);
- text-align: right;
-}
-
-.exchange blockquote {
- width: fit-content;
- max-width: 100%;
- margin: 0;
- padding: 12px 15px;
- border: 1px solid var(--glass-edge);
- border-radius: 18px 18px 18px 5px;
- color: var(--site-text);
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--site-surface-soft);
- font-size: 14px;
- line-height: 1.65;
- overflow-wrap: anywhere;
-}
-
-.exchange:nth-of-type(even) blockquote {
- justify-self: end;
- border-color: color-mix(in srgb, var(--site-accent) 36%, var(--glass-edge));
- border-radius: 18px 18px 5px 18px;
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--glass-selected);
-}
-
-.everyday {
- margin-top: clamp(56px, 8vw, 96px);
- scroll-margin-top: 24px;
-}
-
-.features {
- display: grid;
- gap: 18px;
- margin-top: 28px;
-}
-
-.featureLead,
-.featureList {
- min-width: 0;
- border: 1px solid var(--glass-edge);
- border-radius: 28px;
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--site-panel-bg);
- box-shadow: var(--glass-panel-shadow);
- backdrop-filter: blur(12px) saturate(1.12);
-}
-
-.featureLead {
- padding: clamp(24px, 4vw, 40px);
-}
-
-.featureIcon {
- width: 21px;
- height: 21px;
- color: var(--pip-accent);
- flex: 0 0 auto;
-}
-
-.featureLead .featureIcon {
- width: 30px;
- height: 30px;
- margin-bottom: 24px;
-}
-
-.featureLead h3,
-.featureRow h3 {
- margin: 0;
- font-size: 17px;
- font-weight: 700;
- line-height: 1.4;
- letter-spacing: -.025em;
-}
-
-.featureLead h3 {
- font-size: 25px;
-}
-
-.featureLead > p,
-.featureRow p {
- max-width: 48ch;
- margin: 9px 0 0;
- color: var(--site-muted);
- font-size: 14px;
- line-height: 1.7;
- text-wrap: pretty;
-}
-
-.featureList {
- margin: 0;
- padding: 6px 20px;
- list-style: none;
-}
-
-.featureRow {
- display: grid;
- grid-template-columns: 24px minmax(0, 1fr);
- gap: 13px;
- align-items: start;
- padding: 17px 0;
-}
-
-.featureRow + .featureRow {
- border-top: 1px solid var(--glass-edge);
-}
-
-.featureRow .featureIcon {
- margin-top: 2px;
-}
-
-.personality {
- display: grid;
- gap: 22px;
- align-items: center;
- margin-top: clamp(52px, 7vw, 80px);
- padding: clamp(18px, 3.5vw, 36px);
- border: 1px solid var(--glass-edge);
- border-radius: 30px;
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--site-panel-bg);
- box-shadow: var(--glass-panel-shadow);
- backdrop-filter: blur(14px) saturate(1.12);
-}
-
-.personalityArt {
- display: grid;
- min-height: 190px;
- place-items: center;
- border: 1px solid var(--glass-rim);
- border-radius: 24px;
- background: radial-gradient(circle at 50% 38%, var(--glass-selected), transparent 70%), var(--glass-fill);
- box-shadow: var(--glass-shadow);
-}
-
-.personalityArt svg {
- width: 88px;
- height: 88px;
- color: var(--pip-accent);
- filter: drop-shadow(0 9px 18px color-mix(in srgb, var(--site-accent) 26%, transparent));
-}
-
-.personality h2 {
- font-size: clamp(2rem, 4vw, 3.1rem);
-}
-
-.personality p {
- max-width: 49ch;
- margin: 14px 0 0;
- color: var(--site-muted);
- font-size: 15px;
- line-height: 1.75;
- text-wrap: pretty;
-}
-
-.contact {
- display: grid;
- gap: 24px;
- margin-top: clamp(52px, 7vw, 84px);
- padding: clamp(20px, 4vw, 48px);
- border: 1px solid var(--glass-edge);
- border-radius: 30px;
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--site-panel-bg);
- box-shadow: var(--glass-panel-shadow);
- backdrop-filter: blur(16px) saturate(1.14);
- scroll-margin-top: 24px;
-}
-
-.contactIntro {
- min-width: 0;
-}
-
-.contactIntro > p {
- max-width: 44ch;
- margin: 14px 0 0;
- color: var(--site-muted);
- font-size: 15px;
- line-height: 1.75;
-}
-
-.contactActions {
- min-width: 0;
- padding: clamp(16px, 2.8vw, 26px);
- border: 1px solid var(--glass-edge);
- border-radius: 24px;
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--glass-fill);
- box-shadow: var(--glass-shadow);
-}
-
-.label {
- display: block;
- color: var(--pip-accent);
- font-family: var(--font-mono, var(--font-space-mono, monospace));
- font-size: 10px;
- font-weight: 600;
- line-height: 1.6;
- letter-spacing: .07em;
-}
-
-.handle {
- margin: 8px 0 14px;
- color: var(--site-text);
- font-family: var(--font-mono, var(--font-space-mono, monospace));
- font-size: 14px;
- line-height: 1.7;
- overflow-wrap: anywhere;
- user-select: all;
-}
-
-.contactActions > .primaryAction {
- width: fit-content;
- max-width: 100%;
- margin-top: 14px;
-}
-
-.comingSoon {
- margin: 10px 0;
- font-size: 20px;
- font-weight: 600;
- letter-spacing: -.03em;
-}
-
-.actionHint {
- margin: 10px 0 0;
- color: var(--site-muted);
- font-size: 12px;
- line-height: 1.7;
-}
-
-.secondaryActions {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- margin-top: 14px;
-}
-
-.secondaryAction {
- display: inline-flex;
- flex: 1 1 108px;
- align-items: center;
- justify-content: center;
- gap: 8px;
- min-width: 0;
- min-height: 48px;
- padding: 10px 12px;
- border: 1px solid var(--glass-edge);
- border-radius: var(--site-control-radius);
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--glass-fill);
- color: var(--site-text);
- font-size: 12px;
- font-weight: 600;
- cursor: pointer;
- box-shadow: var(--glass-shadow);
- transition: border-color var(--ui-duration) var(--ui-ease), box-shadow var(--ui-duration) var(--ui-ease), transform var(--ui-duration) var(--ui-ease);
-}
-
-.secondaryAction svg {
- width: 15px;
- height: 15px;
- flex: 0 0 auto;
-}
-
-.secondaryAction:not(:disabled):hover {
- border-color: var(--glass-rim);
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--glass-selected);
- box-shadow: var(--glass-panel-shadow);
-}
-
-.secondaryAction:not(:disabled):active {
- transform: translateY(1px);
-}
-
-.secondaryAction:disabled {
- color: var(--site-muted);
- cursor: not-allowed;
-}
-
-.copyStatus {
- min-height: 1.7em;
- margin: 8px 0 0;
- color: var(--pip-accent);
- font-size: 12px;
- line-height: 1.7;
-}
-
-.copyStatus:empty {
- min-height: 0;
- margin: 0;
-}
-
-.qrPanel {
- display: grid;
- justify-items: center;
- gap: 14px;
- margin-top: 18px;
- padding: 18px;
- border: 1px solid var(--glass-edge);
- border-radius: 20px;
- background: linear-gradient(145deg, var(--glass-top), var(--glass-bottom)), var(--site-panel-bg);
- box-shadow: var(--glass-shadow);
-}
-
-.qrPanel img {
- display: block;
- width: min(100%, 240px);
- height: auto;
- border: 1px solid var(--glass-edge);
- border-radius: 12px;
- background: white;
-}
-
-.qrPanel img[data-pending-media] {
- background: transparent;
- border-color: transparent;
-}
-
-.qrPanel p {
- margin: 0;
- color: var(--site-muted);
- font-size: 12px;
- line-height: 1.6;
- text-align: center;
-}
-
-.footer {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 12px 20px;
- margin-top: 28px;
- padding: 16px 4px max(20px, env(safe-area-inset-bottom));
- border-top: 1px solid var(--glass-edge);
-}
-
-.footer > a {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- min-height: 44px;
- color: var(--site-muted);
- font-family: var(--font-mono, var(--font-space-mono, monospace));
- font-size: 10px;
- letter-spacing: .07em;
- text-decoration: none;
- transition: color var(--ui-duration) var(--ui-ease);
-}
-
-.footer p {
- flex: 1;
- min-width: min(100%, 170px);
- margin: 0;
- color: var(--site-muted);
- font-size: 12px;
- line-height: 1.65;
-}
-
-.footerPip {
- color: var(--pip-accent);
- font-family: var(--font-pixelify), sans-serif;
- font-size: 32px;
-}
-
-@media (min-width: 640px) {
- .shell {
- padding-inline: clamp(24px, 4vw, 40px);
- }
-
- .hero {
- gap: clamp(22px, 3vw, 40px);
- padding: clamp(16px, 2vw, 24px);
- }
-
- .heroCopy {
- padding: clamp(12px, 2vw, 24px);
- }
-
- .heroVisual {
- aspect-ratio: 1.2 / 1;
- }
-
- .conversation {
- padding: 28px;
- }
-
- .featureLead {
- padding: 34px;
- }
-
- .contact {
- padding: 32px;
- }
-
- .footer p {
- text-align: center;
- }
-}
-
-@media (min-width: 960px) {
- .hero {
- grid-template-columns: minmax(0, .95fr) minmax(0, 1.05fr);
- gap: clamp(28px, 4vw, 56px);
- padding: clamp(18px, 2.2vw, 28px);
- }
-
- .heroCopy {
- padding: clamp(16px, 2vw, 28px);
- }
-
- .heroVisual {
- aspect-ratio: 1.16 / 1;
- }
-
- .example {
- grid-template-columns: minmax(240px, .68fr) minmax(0, 1.32fr);
- gap: clamp(28px, 5vw, 64px);
- align-items: start;
- }
-
- .conversation {
- padding: 30px;
- }
-
- .features {
- grid-template-columns: minmax(0, .88fr) minmax(0, 1.12fr);
- gap: 24px;
- align-items: stretch;
- }
-
- .featureLead {
- padding: 40px;
- }
-
- .personality {
- grid-template-columns: minmax(220px, .7fr) minmax(0, 1.3fr);
- gap: clamp(32px, 5vw, 64px);
- padding: 36px;
- }
-
- .personalityArt {
- min-height: 240px;
- }
-
- .personalityArt svg {
- width: 112px;
- height: 112px;
- }
-
- .contact {
- grid-template-columns: minmax(0, 1fr) minmax(280px, .8fr);
- gap: clamp(32px, 5vw, 56px);
- align-items: center;
- padding: 42px;
- }
-}
-
-@media (max-width: 420px) {
- .header {
- min-height: 62px;
- }
-
- .homeLink {
- padding-inline: 12px;
- font-size: 12px;
- }
-
- .heroCopy {
- padding: 5px 4px 0;
- }
-
- .hero h1 {
- font-size: clamp(2.4rem, 11.4vw, 3.1rem);
- }
-
- .intro {
- margin-block: 16px 20px;
- font-size: 15px;
- }
-
- .heroVisual {
- aspect-ratio: 1.18 / 1;
- }
-
- .example h2,
- .sectionHeading h2,
- .contact h2 {
- font-size: clamp(1.9rem, 9.4vw, 2.5rem);
- }
-
- .conversation {
- padding: 16px 12px;
- }
-
- .exchange {
- grid-template-columns: 32px minmax(0, 1fr);
- gap: 7px;
- }
-
- .exchange:nth-of-type(even) {
- grid-template-columns: minmax(0, 1fr) 32px;
- }
-
- .exchange blockquote {
- padding: 10px 12px;
- font-size: 13px;
- }
-
- .featureList {
- padding-inline: 14px;
- }
-
- .contactActions > .primaryAction {
- width: 100%;
- }
-
- .secondaryActions {
- display: grid;
- grid-template-columns: minmax(0, 1fr);
- }
-}
-
-@media (prefers-reduced-motion: reduce) {
- .homeLink,
- .primaryAction,
- .secondaryAction,
- .footer > a {
- transition: none;
- }
-
- .homeLink:hover,
- .primaryAction:not(:disabled):hover,
- .primaryAction:not(:disabled):active,
- .secondaryAction:not(:disabled):active {
- transform: none;
- }
-}
diff --git a/app/routes.tsx b/app/routes.tsx
index 4f88ad5..1bc63a3 100644
--- a/app/routes.tsx
+++ b/app/routes.tsx
@@ -1,7 +1,6 @@
import type { ComponentType } from "react";
import siteConfig from "@/site.config.json";
import Home from "./page";
-import PipPage from "./pip/page";
import PrivacyPage from "./privacy/page";
import NotFound from "./not-found";
@@ -17,8 +16,8 @@ type RouteMetadata = {
};
export type SiteRoute = {
- path: "/" | "/pip" | "/privacy" | "/404";
- output: "index.html" | "pip.html" | "privacy.html" | "404.html";
+ path: "/" | "/privacy" | "/404";
+ output: "index.html" | "privacy.html" | "404.html";
Component: ComponentType;
metadata: RouteMetadata;
};
@@ -33,8 +32,6 @@ const homeMetadata: RouteMetadata = {
imageAlt: "Lu | Software Engineer",
};
-const pipDescription = "A little help, one message away. Pip is your warm, easygoing Telegram agent for questions, web searches, reminders, and everyday life. Built on Keiki.";
-
const notFoundRoute: SiteRoute = {
path: "/404",
output: "404.html",
@@ -44,20 +41,6 @@ const notFoundRoute: SiteRoute = {
export const siteRoutes: readonly SiteRoute[] = [
{ path: "/", output: "index.html", Component: Home, metadata: homeMetadata },
- {
- path: "/pip",
- output: "pip.html",
- Component: PipPage,
- metadata: {
- title: "Pip - Your Telegram mate | Lu",
- socialTitle: "Pip - Your Telegram mate",
- description: pipDescription,
- socialDescription: pipDescription,
- canonical: `${siteConfig.siteUrl}/pip`,
- image: `${siteConfig.siteUrl}/share-cards/luinbytes-dev-pip.png`,
- imageAlt: "Pip - Your Telegram mate",
- },
- },
{
path: "/privacy",
output: "privacy.html",
diff --git a/components/concepts/signal-desk/signal-desk.module.css b/components/concepts/signal-desk/signal-desk.module.css
index 1a657ec..643168c 100644
--- a/components/concepts/signal-desk/signal-desk.module.css
+++ b/components/concepts/signal-desk/signal-desk.module.css
@@ -112,16 +112,6 @@
transition: color var(--ui-duration, 280ms) ease, background-color var(--ui-duration, 280ms) ease, border-color var(--ui-duration, 280ms) ease, transform var(--ui-duration, 280ms) var(--ease-out);
}
-.header nav .pipNavLink {
- gap: 5px;
-}
-
-.pipNavLink svg {
- width: 18px;
- height: 18px;
- color: #e9a982;
-}
-
.hero {
position: relative;
z-index: var(--layer-content, 1);
@@ -791,15 +781,6 @@
font-size: 12px;
}
- .header nav .pipNavLink {
- gap: 3px;
- }
-
- .pipNavLink svg {
- width: 16px;
- height: 16px;
- }
-
.hero {
width: min(calc(100% - 24px - env(safe-area-inset-left) - env(safe-area-inset-right)), 540px);
padding: 126px 0 0;
@@ -896,15 +877,6 @@
font-size: 11px;
}
- .header nav .pipNavLink {
- gap: 2px;
- }
-
- .pipNavLink svg {
- width: 15px;
- height: 15px;
- }
-
.footer > div {
gap: 8px;
}
diff --git a/components/concepts/signal-desk/signal-desk.tsx b/components/concepts/signal-desk/signal-desk.tsx
index e247f7c..eaa415c 100644
--- a/components/concepts/signal-desk/signal-desk.tsx
+++ b/components/concepts/signal-desk/signal-desk.tsx
@@ -10,8 +10,6 @@ import {
} from "@/lib/portfolio-content";
import { PondEnvironment } from "./pond-environment";
import { ProfileCard } from "./profile-card";
-import { PipFace } from "@/components/pip/pip-logo";
-import { PipFeature } from "@/components/pip/pip-feature";
import { Button } from "@/components/ui/button";
import { SkulletImage } from "@/components/ui/skullet-image";
import projectStyles from "./project-explorer.module.css";
@@ -253,7 +251,6 @@ export function SignalDesk() {
Work
- Pip
About
Contact
@@ -272,7 +269,6 @@ export function SignalDesk() {
-
diff --git a/components/pip/pip-feature.module.css b/components/pip/pip-feature.module.css
deleted file mode 100644
index d1b0e49..0000000
--- a/components/pip/pip-feature.module.css
+++ /dev/null
@@ -1,245 +0,0 @@
-.section {
- position: relative;
- z-index: var(--layer-content, 1);
- padding: 0 max(20px, env(safe-area-inset-right)) 0 max(20px, env(safe-area-inset-left));
- pointer-events: none;
- scroll-margin-top: 96px;
-}
-
-.card {
- --pip-action-bg: var(--site-accent, #9984cf);
- --pip-action-text: var(--site-contrast, #1b1227);
- display: grid;
- grid-template-columns: minmax(180px, .55fr) minmax(0, 1.3fr) auto;
- gap: 30px;
- align-items: center;
- width: min(100%, 1316px);
- margin-inline: auto;
- padding: 32px 36px;
- border: 1px solid var(--glass-edge, var(--site-border, #47675b));
- border-radius: var(--site-radius, 24px);
- color: var(--site-text, #eff7f1);
- background: linear-gradient(145deg, var(--glass-top, rgb(255 255 255 / .16)), var(--glass-bottom, transparent)), var(--site-panel-bg, var(--site-surface, #0c2a27));
- box-shadow: var(--glass-panel-shadow, var(--site-shadow));
- backdrop-filter: blur(16px) saturate(1.14);
- pointer-events: auto;
-}
-
-.card > * {
- min-width: 0;
-}
-
-.identity {
- display: grid;
- justify-items: start;
- gap: 12px;
-}
-
-.logo {
- --pip-face-size: 48px;
- --pip-wordmark-size: 68px;
- --pip-logo-gap: 10px;
- /* Pixelify's low lowercase ink needs a lift against the face at this scale. */
- --pip-wordmark-offset: -.09em;
-}
-
-.identity > span:last-child {
- color: var(--site-muted, #b2c8bd);
- font-family: var(--font-mono, var(--font-space-mono, monospace));
- font-size: 11px;
- font-weight: 600;
-}
-
-.copy h2 {
- margin: 0;
- color: var(--site-text, #eff7f1);
- font-size: clamp(2rem, 4vw, 3.4rem);
- line-height: 1.02;
- letter-spacing: -.055em;
- font-weight: 600;
- text-wrap: balance;
-}
-
-.copy h2 > span {
- color: var(--site-accent, #9984cf);
-}
-
-.copy p {
- max-width: 44ch;
- margin: 13px 0 0;
- color: var(--site-muted, #b2c8bd);
- font-size: 14px;
- line-height: 1.68;
-}
-
-.actions {
- display: grid;
- align-self: stretch;
- grid-template-rows: minmax(0, 1fr) auto minmax(0, 1fr);
- justify-items: start;
- gap: 9px;
- min-width: 132px;
-}
-
-.actions::before {
- content: "";
-}
-
-.actions > span {
- grid-row: 3;
- align-self: start;
- color: var(--site-muted, #b2c8bd);
- font-family: var(--font-mono, var(--font-space-mono, monospace));
- font-size: 10px;
- line-height: 1.55;
-}
-
-.actions :global(a) {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 9px;
- min-width: 44px;
- min-height: 48px;
- padding: 12px 19px;
- border: 1px solid color-mix(in srgb, var(--pip-action-bg) 76%, var(--glass-rim, white));
- border-radius: var(--site-control-radius, 999px);
- color: var(--pip-action-text);
- background: linear-gradient(145deg, rgb(255 255 255 / .06), transparent), var(--pip-action-bg);
- box-shadow: var(--glass-shadow, var(--site-shadow));
- font-size: 14px;
- font-weight: 700;
- text-decoration: none;
- transition: border-color var(--ui-duration, 280ms) var(--ui-ease, ease), box-shadow var(--ui-duration, 280ms) var(--ui-ease, ease), transform var(--ui-duration, 280ms) var(--ui-ease, ease);
-}
-
-.actions :global(a):hover {
- border-color: var(--glass-rim, white);
- box-shadow: var(--glass-panel-shadow, var(--site-shadow));
- transform: translateY(-2px);
-}
-
-.actions :global(a):active {
- transform: translateY(1px);
-}
-
-.actionContent {
- display: inline-flex;
- align-items: center;
- gap: 9px;
-}
-
-.actionContent svg {
- width: 16px;
- height: 16px;
- flex: 0 0 auto;
- transition: transform var(--ui-duration, 280ms) var(--ui-ease, ease);
-}
-
-.actions :global(a):hover .actionContent svg {
- transform: translate3d(2px, -2px, 0);
-}
-
-@media (min-width: 821px) and (max-width: 1120px) {
- .card {
- grid-template-columns: minmax(148px, .55fr) minmax(0, 1.25fr) auto;
- gap: 22px;
- padding: 28px;
- }
-
- .logo {
- --pip-face-size: 42px;
- --pip-wordmark-size: 59px;
- --pip-logo-gap: 8px;
- }
-
- .copy h2 {
- font-size: clamp(1.9rem, 3.7vw, 2.8rem);
- }
-}
-
-@media (max-width: 820px) {
- .section {
- padding: 0 max(14px, env(safe-area-inset-right)) 0 max(14px, env(safe-area-inset-left));
- }
-
- .card {
- grid-template-columns: minmax(0, 1fr);
- gap: 16px;
- width: min(100%, 620px);
- padding: 22px;
- }
-
- .identity {
- display: flex;
- align-items: center;
- gap: 13px;
- }
-
- .logo {
- --pip-face-size: 44px;
- --pip-wordmark-size: 60px;
- --pip-logo-gap: 8px;
- }
-
- .identity > span:last-child {
- font-size: 10px;
- }
-
- .copy h2 {
- font-size: clamp(2rem, 8vw, 2.75rem);
- }
-
- .copy p {
- margin-top: 10px;
- font-size: 14px;
- }
-
- .actions {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
- gap: 10px 16px;
- min-width: 0;
- }
-
- .actions::before {
- display: none;
- }
-
- .actions > span {
- grid-row: auto;
- }
-
- .actions :global(a) {
- min-width: min(100%, 150px);
- }
-}
-
-@media (max-width: 370px) {
- .card {
- padding: 18px;
- }
-
- .actions {
- align-items: stretch;
- }
-
- .actions :global(a) {
- width: 100%;
- }
-}
-
-@media (prefers-reduced-motion: reduce) {
- .actions :global(a),
- .actionContent svg {
- transition: none;
- }
-
- .actions :global(a):hover,
- .actions :global(a):active,
- .actions :global(a):hover .actionContent svg {
- transform: none;
- }
-}
diff --git a/components/pip/pip-feature.tsx b/components/pip/pip-feature.tsx
deleted file mode 100644
index 89557ed..0000000
--- a/components/pip/pip-feature.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { ArrowUpRight } from "lucide-react";
-import { PipLogo } from "./pip-logo";
-import { Button } from "@/components/ui/button";
-import deskStyles from "@/components/concepts/signal-desk/signal-desk.module.css";
-import styles from "./pip-feature.module.css";
-
-export function PipFeature() {
- return (
-
-
-
-
-
A little help. A little banter.
-
Meet Pip, your AI mate on Telegram. Quick questions, useful reminders, and a little everyday company.
-
-
-
-
- );
-}
diff --git a/components/pip/pip-logo.module.css b/components/pip/pip-logo.module.css
deleted file mode 100644
index 879a1c0..0000000
--- a/components/pip/pip-logo.module.css
+++ /dev/null
@@ -1,21 +0,0 @@
-.logo {
- display: inline-flex;
- align-items: center;
- gap: var(--pip-logo-gap, 10px);
- color: #f6be91;
-}
-
-.logo svg {
- flex-shrink: 0;
- width: var(--pip-face-size, 34px);
- height: var(--pip-face-size, 34px);
-}
-
-.logo > span {
- font-family: var(--font-pixelify), sans-serif;
- font-size: var(--pip-wordmark-size, 46px);
- font-weight: 400;
- line-height: 1;
- letter-spacing: -3px;
- transform: translateY(var(--pip-wordmark-offset, 0));
-}
diff --git a/components/pip/pip-logo.tsx b/components/pip/pip-logo.tsx
deleted file mode 100644
index 0d94a7c..0000000
--- a/components/pip/pip-logo.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import styles from "./pip-logo.module.css";
-
-export function PipFace({ className = "" }: { className?: string }) {
- return (
-
-
-
-
- );
-}
-
-export function PipLogo({ className = "" }: { className?: string }) {
- return (
-
-
- pip
-
- );
-}
diff --git a/components/pip/pip-web-search-demo.module.css b/components/pip/pip-web-search-demo.module.css
deleted file mode 100644
index 953c306..0000000
--- a/components/pip/pip-web-search-demo.module.css
+++ /dev/null
@@ -1,319 +0,0 @@
-.demo {
- width: 100%;
- margin-top: 14px;
-}
-.stage {
- height: 280px;
- position: relative;
- isolation: isolate;
- overflow: hidden;
- background: radial-gradient(
- ellipse at 50% 60%,
- var(--glass-selected),
- transparent 70%
- );
-}
-.stage canvas {
- position: absolute;
- inset: 0;
- width: 100%;
- height: 100%;
- z-index: 2;
- pointer-events: none;
-}
-.browser {
- position: absolute;
- left: 29%;
- top: 10%;
- width: 69%;
- height: 77%;
- border: 1px solid var(--glass-rim);
- border-radius: 14px;
- background: var(--site-surface);
- box-shadow:
- var(--site-shadow),
- inset 0 1px var(--glass-rim);
- overflow: hidden;
- z-index: 1;
-}
-.chrome {
- height: 28px;
- display: flex;
- align-items: center;
- gap: 4px;
- padding: 0 10px;
- border-bottom: 1px solid var(--site-border);
- color: var(--site-muted);
-}
-.chrome i {
- width: 4px;
- height: 4px;
- border-radius: 50%;
- background: var(--site-accent);
- opacity: 0.6;
-}
-.chrome > span {
- flex: 1;
- text-align: center;
- font-size: 8px;
- letter-spacing: 0.03em;
-}
-.chrome svg {
- width: 11px;
- height: 11px;
-}
-.content {
- padding: 14px;
- color: var(--site-text);
- font-size: 10px;
-}
-.heading {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 6px;
- font-size: 11px;
- font-weight: 600;
- margin-bottom: 12px;
-}
-.heading span {
- color: var(--site-muted);
- font-size: 8px;
-}
-.heading svg {
- width: 13px;
- height: 13px;
- color: var(--site-accent);
-}
-.search {
- height: 27px;
- padding: 6px 8px;
- border: 1px solid var(--site-accent);
- border-radius: 6px;
- font-size: 9px;
- margin-bottom: 12px;
-}
-.product {
- display: grid;
- grid-template-columns: 32% 1fr;
- gap: 12px;
- align-items: center;
-}
-.productArt {
- display: grid;
- place-items: center;
- height: 82px;
- border: 1px solid var(--site-border);
- background: var(--glass-selected);
- border-radius: 9px;
-}
-.book,
-.smallBook {
- border-radius: 3px;
- background: linear-gradient(100deg, #a38e61 8%, #dec998 12%, #c8b07d);
- box-shadow: 3px 5px 8px #0002;
-}
-.book {
- width: 32px;
- height: 47px;
- transform: rotate(-9deg);
-}
-.book::after {
- content: "";
- display: block;
- width: 16px;
- height: 1px;
- margin: 19px auto;
- background: #887447;
-}
-.product strong,
-.cart strong {
- display: block;
- font-size: 10px;
- font-weight: 600;
- line-height: 1.4;
- margin-bottom: 7px;
-}
-.skeleton,
-.shortSkeleton {
- height: 4px;
- border-radius: 4px;
- background: var(--site-border);
- margin-bottom: 5px;
- width: 85%;
-}
-.shortSkeleton {
- width: 55%;
-}
-.product small,
-.cart small {
- display: block;
- color: var(--site-muted);
- font-size: 9px;
- margin: 8px 0;
-}
-.action {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-height: 27px;
- padding: 5px 9px;
- border: 1px solid var(--glass-rim);
- background: var(--site-accent);
- color: var(--site-contrast);
- border-radius: 7px;
- font-size: 9px;
- font-weight: 600;
- white-space: nowrap;
-}
-.cart {
- display: flex;
- align-items: center;
- gap: 12px;
- padding: 16px 0;
- border-block: 1px solid var(--site-border);
-}
-.cart > div:last-child {
- flex: 1;
-}
-.smallBook {
- width: 26px;
- height: 37px;
- flex-shrink: 0;
-}
-.bottom {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 5px;
- margin-top: 15px;
-}
-.bottom small {
- font-size: 8px;
- color: var(--site-muted);
-}
-.field {
- color: var(--site-muted);
- font-size: 8px;
- margin-top: 6px;
-}
-.field span {
- display: block;
- min-height: 24px;
- padding: 5px 7px;
- margin-top: 3px;
- border: 1px solid var(--site-border);
- border-radius: 5px;
- color: var(--site-text);
- font-size: 9px;
-}
-.field [data-typed] {
- border-color: var(--site-accent);
-}
-.fields {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 7px;
-}
-.content:has(.fields) {
- padding-block: 8px;
- line-height: 1.2;
-}
-.content:has(.fields) .heading {
- margin-bottom: 4px;
-}
-.content:has(.fields) .field {
- margin-top: 4px;
-}
-.content:has(.fields) .field span {
- min-height: 22px;
- padding-block: 4px;
-}
-.content:has(.fields) .bottom {
- margin-top: 6px;
-}
-.done {
- text-align: center;
- padding-top: 13px;
-}
-.seal {
- width: 42px;
- height: 42px;
- display: grid;
- place-items: center;
- background: var(--glass-selected);
- color: var(--site-accent);
- border: 1px solid var(--site-border);
- border-radius: 50%;
- margin: 0 auto 12px;
-}
-.seal svg {
- width: 22px;
- height: 22px;
-}
-.done strong {
- font-size: 14px;
-}
-.done p {
- font-size: 10px;
- color: var(--site-muted);
- margin: 8px 0 18px;
-}
-.done .skeleton,
-.done .shortSkeleton {
- margin-inline: auto;
-}
-.companion {
- position: absolute;
- top: 28%;
- left: 2%;
- width: 22%;
- display: flex;
- align-items: center;
- flex-direction: column;
- gap: 12px;
- z-index: 3;
-}
-.companion svg {
- width: 85%;
- color: var(--site-accent);
- filter: drop-shadow(0 6px 8px #1b122720);
-}
-.companion span {
- font-size: 10px;
- line-height: 1.4;
- text-align: center;
- color: var(--site-muted);
- max-width: 11ch;
-}
-.status {
- position: absolute;
- bottom: 4%;
- left: 29%;
- right: 2%;
- font-size: 10px;
- color: var(--site-muted);
- text-align: center;
-}
-@media (max-width: 370px) {
- .content {
- padding: 11px 9px;
- }
- .product {
- gap: 8px;
- }
- .product strong,
- .cart strong {
- font-size: 9px;
- }
- .action {
- padding-inline: 7px;
- font-size: 8px;
- }
- .companion span {
- font-size: 9px;
- }
- .heading {
- font-size: 10px;
- }
-}
diff --git a/components/pip/pip-web-search-demo.tsx b/components/pip/pip-web-search-demo.tsx
deleted file mode 100644
index 6d1233b..0000000
--- a/components/pip/pip-web-search-demo.tsx
+++ /dev/null
@@ -1,428 +0,0 @@
-import { useEffect, useRef, useState } from "react";
-import { Check, Search } from "lucide-react";
-import { PipFace } from "./pip-logo";
-import type { createDemoRenderer } from "./pip-web-search-renderer";
-import styles from "./pip-web-search-demo.module.css";
-
-type Phase = "search" | "product" | "addcart" | "postage" | "review" | "done";
-type Step = {
- phase: Phase;
- start: number;
- duration: number;
- label: string;
- speech: string;
-};
-const timeline: readonly Step[] = [
- {
- phase: "search",
- start: 0,
- duration: 2400,
- label: "Searching the web",
- speech: "I'll find a match.",
- },
- {
- phase: "product",
- start: 2400,
- duration: 2200,
- label: "Opening a result",
- speech: "Found the one.",
- },
- {
- phase: "addcart",
- start: 4600,
- duration: 2000,
- label: "Adding to the basket",
- speech: "Into the basket.",
- },
- {
- phase: "postage",
- start: 6600,
- duration: 2800,
- label: "Filling postage details",
- speech: "Details filled in.",
- },
- {
- phase: "review",
- start: 9400,
- duration: 2400,
- label: "Reviewing checkout",
- speech: "One final check.",
- },
- {
- phase: "done",
- start: 11800,
- duration: 6500,
- label: "All sorted",
- speech: "One less thing.",
- },
-];
-const completedStep = timeline[timeline.length - 1];
-const sequenceDuration = completedStep.start + completedStep.duration;
-
-function sampleFrame(elapsed: number) {
- const time = elapsed % sequenceDuration;
- const step =
- timeline.find(
- (entry) => time >= entry.start && time < entry.start + entry.duration,
- ) ?? completedStep;
- return {
- step,
- portion: (time - step.start) / step.duration,
- drift: Math.sin(time / 1800),
- };
-}
-
-function BrowserContent({ phase }: { phase: Phase }) {
- if (phase === "done")
- return (
-
-
-
-
-
All sorted.
-
One less thing on your list.
-
-
-
- );
- if (phase === "postage")
- return (
- <>
-
- Postage details 1 / 2
-
-
- NameAlex Example
-
-
- Address12 Example Lane
-
-
-
- TownSampleton
-
-
- PostcodeAB1 2CD
-
-
-
- Standard delivery
-
- Continue
-
-
- >
- );
- if (phase === "review" || phase === "addcart")
- return (
- <>
-
- {phase === "review" ? "Review checkout" : "Your basket"}
- {phase === "review" ? "2 / 2" : "1 item"}
-
-
-
-
-
Everyday notebook
-
-
£12.00
-
-
-
-
- {phase === "review" ? "Total · £15" : "Ready when you are"}
-
-
- {phase === "review" ? "Confirm order" : "Checkout"}
-
-
- >
- );
- return (
- <>
-
- {phase === "search" ? "Find something useful" : "Everyday notebook"}
-
-
- {phase === "search" && (
-
- a notebook for my desk
-
- )}
-
-
-
-
- {phase === "search"
- ? "A notebook for your desk"
- : "Everyday notebook"}
-
-
-
-
£12.00
-
- {phase === "search" ? "View notebook" : "Add to cart"}
-
-
-
- >
- );
-}
-
-export function PipWebSearchDemo() {
- const stageRef = useRef(null);
- const companionRef = useRef(null);
- const [step, setStep] = useState(completedStep);
- const [rendererKind, setRendererKind] = useState<
- "pending" | "three" | "fallback"
- >("pending");
- const [playing, setPlaying] = useState(true);
-
- useEffect(() => {
- const stageElement = stageRef.current;
- if (!stageElement) return;
- const stage = stageElement;
- const motion = matchMedia("(prefers-reduced-motion: reduce)");
- const theme = matchMedia("(prefers-color-scheme: light)");
- let renderer: ReturnType | undefined;
- let visible = false;
- let disposed = false;
- let failed = false;
- let loading = false;
- let generation = 0;
- let raf = 0;
- let elapsed = 0;
- let previous = 0;
- let current: Phase = "done";
- let cursorX = 0.4;
- let cursorY = 0.3;
-
- function stop() {
- cancelAnimationFrame(raf);
- raf = 0;
- previous = 0;
- }
-
- function fail() {
- failed = true;
- stop();
- renderer?.dispose();
- renderer = undefined;
- setRendererKind("fallback");
- setStep(completedStep);
- setPlaying(false);
- }
-
- function resize() {
- try {
- renderer?.resize(stage.clientWidth, stage.clientHeight, theme.matches);
- if (renderer) draw(elapsed, 0);
- } catch {
- fail();
- }
- }
-
- function draw(time: number, delta: number) {
- const frame = sampleFrame(time);
- if (current !== frame.step.phase) {
- current = frame.step.phase;
- setStep(frame.step);
- }
- const typed = stage.querySelector("[data-typed]");
- if (typed) {
- const text =
- typed.dataset.typed === "search"
- ? "a notebook for my desk"
- : "12 Example Lane";
- const nextText = text.slice(
- 0,
- Math.floor(Math.min(1, frame.portion * 2.2) * text.length),
- );
- if (typed.textContent !== nextText) typed.textContent = nextText;
- }
- const bounds = stage.getBoundingClientRect();
- const target = stage
- .querySelector("[data-demo-target]")
- ?.getBoundingClientRect();
- const targetX = target
- ? (target.left + target.width * 0.65 - bounds.left) / bounds.width
- : 0.82;
- const targetY = target
- ? (target.top + target.height * 0.6 - bounds.top) / bounds.height
- : 0.65;
- const ease = 1 - Math.exp(-delta / 180);
- cursorX += (targetX - cursorX) * ease;
- cursorY += (targetY - cursorY) * ease;
- if (companionRef.current)
- companionRef.current.style.transform = `translateY(${frame.drift * 3}px) rotate(${frame.drift * 1.5}deg)`;
- const ripple =
- frame.portion > 0.72 && frame.portion < 0.96
- ? (frame.portion - 0.72) / 0.24
- : 0;
- renderer?.render({
- cursorX,
- cursorY,
- ripple,
- drift: frame.drift,
- complete: frame.step.phase === "done",
- });
- }
-
- function tick(now: number) {
- raf = 0;
- if (disposed) return;
- if (motion.matches) {
- motionChanged();
- return;
- }
- if (!visible || document.hidden || !renderer) return;
- const delta = previous ? Math.min(now - previous, 60) : 0;
- previous = now;
- elapsed += delta;
- try {
- draw(elapsed, delta);
- } catch {
- fail();
- return;
- }
- raf = requestAnimationFrame(tick);
- }
-
- async function loadRenderer() {
- if (
- renderer ||
- loading ||
- failed ||
- motion.matches ||
- !visible ||
- document.hidden
- )
- return;
- loading = true;
- const requestGeneration = generation;
- try {
- const module = await import("./pip-web-search-renderer");
- if (
- disposed ||
- requestGeneration !== generation ||
- motion.matches ||
- !visible ||
- document.hidden
- )
- return;
- renderer = module.createDemoRenderer(stage, fail);
- resize();
- if (failed) return;
- setRendererKind("three");
- wake();
- } catch {
- if (!disposed && requestGeneration === generation) fail();
- } finally {
- loading = false;
- if (
- !disposed &&
- !renderer &&
- !failed &&
- visible &&
- !motion.matches &&
- !document.hidden
- )
- wake();
- }
- }
-
- function wake() {
- stop();
- setPlaying(visible && !document.hidden && !motion.matches && !failed);
- if (disposed || motion.matches || failed || !visible || document.hidden)
- return;
- if (!renderer) {
- void loadRenderer();
- return;
- }
- raf = requestAnimationFrame(tick);
- }
-
- function motionChanged() {
- generation += 1;
- stop();
- if (motion.matches) {
- renderer?.dispose();
- renderer = undefined;
- current = "done";
- elapsed = 0;
- setStep(completedStep);
- setRendererKind("fallback");
- setPlaying(false);
- if (companionRef.current) companionRef.current.style.transform = "";
- } else {
- setRendererKind(failed ? "fallback" : "pending");
- wake();
- }
- }
-
- const observer = new IntersectionObserver(([entry]) => {
- visible = entry.isIntersecting;
- wake();
- });
- const resizeObserver = new ResizeObserver(resize);
- observer.observe(stage);
- resizeObserver.observe(stage);
- document.addEventListener("visibilitychange", wake);
- motion.addEventListener("change", motionChanged);
- theme.addEventListener("change", resize);
- if (motion.matches) motionChanged();
- return () => {
- disposed = true;
- generation += 1;
- stop();
- observer.disconnect();
- resizeObserver.disconnect();
- document.removeEventListener("visibilitychange", wake);
- motion.removeEventListener("change", motionChanged);
- theme.removeEventListener("change", resize);
- renderer?.dispose();
- };
- }, []);
-
- return (
-
-
-
-
-
-
-
-
- pip / browsing
-
-
-
-
-
-
-
{step.label}
-
-
- );
-}
diff --git a/components/pip/pip-web-search-renderer.ts b/components/pip/pip-web-search-renderer.ts
deleted file mode 100644
index c7763db..0000000
--- a/components/pip/pip-web-search-renderer.ts
+++ /dev/null
@@ -1,195 +0,0 @@
-import * as THREE from "three";
-
-export type DemoRenderFrame = {
- cursorX: number;
- cursorY: number;
- ripple: number;
- drift: number;
- complete: boolean;
-};
-
-export function createDemoRenderer(stage: HTMLElement, onFailure: () => void) {
- const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
- const geometries: THREE.BufferGeometry[] = [];
- const materials: THREE.Material[] = [];
- try {
- renderer.setClearColor(0x000000, 0);
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
- renderer.domElement.setAttribute("aria-hidden", "true");
- const scene = new THREE.Scene();
- const camera = new THREE.OrthographicCamera(0, 1, 1, 0, 0.1, 10);
- camera.position.z = 4;
- scene.add(new THREE.AmbientLight(0xffffff, 2));
- const light = new THREE.DirectionalLight(0xeee3ff, 3);
- light.position.set(-1, 2, 4);
- scene.add(light);
-
- function rounded(
- width: number,
- height: number,
- radius: number,
- frame = false,
- ) {
- const shape = new THREE.Shape();
- shape.moveTo(radius, 0);
- shape.lineTo(width - radius, 0);
- shape.quadraticCurveTo(width, 0, width, radius);
- shape.lineTo(width, height - radius);
- shape.quadraticCurveTo(width, height, width - radius, height);
- shape.lineTo(radius, height);
- shape.quadraticCurveTo(0, height, 0, height - radius);
- shape.lineTo(0, radius);
- shape.quadraticCurveTo(0, 0, radius, 0);
- if (frame) {
- const inset = 0.01;
- const hole = new THREE.Path();
- hole.moveTo(radius, inset);
- hole.lineTo(width - radius, inset);
- hole.quadraticCurveTo(width - inset, inset, width - inset, radius);
- hole.lineTo(width - inset, height - radius);
- hole.quadraticCurveTo(
- width - inset,
- height - inset,
- width - radius,
- height - inset,
- );
- hole.lineTo(radius, height - inset);
- hole.quadraticCurveTo(inset, height - inset, inset, height - radius);
- hole.lineTo(inset, radius);
- hole.quadraticCurveTo(inset, inset, radius, inset);
- shape.holes.push(hole);
- }
- const geometry = new THREE.ExtrudeGeometry(shape, {
- depth: 0.025,
- bevelEnabled: true,
- bevelThickness: 0.009,
- bevelSize: 0.006,
- bevelSegments: 3,
- steps: 1,
- curveSegments: 10,
- });
- geometries.push(geometry);
- return geometry;
- }
-
- const chassisMaterial = new THREE.MeshStandardMaterial({
- color: 0x665279,
- roughness: 0.35,
- metalness: 0.15,
- transparent: true,
- opacity: 0.55,
- });
- materials.push(chassisMaterial);
- const backingMaterial = new THREE.MeshStandardMaterial({
- color: 0x9c83c7,
- roughness: 0.4,
- metalness: 0.08,
- });
- materials.push(backingMaterial);
- const cursorMaterial = new THREE.MeshStandardMaterial({
- color: 0xf4eaff,
- roughness: 0.3,
- });
- materials.push(cursorMaterial);
- const rippleMaterial = new THREE.MeshBasicMaterial({
- color: 0xbda3ed,
- transparent: true,
- opacity: 0,
- depthWrite: false,
- });
- materials.push(rippleMaterial);
- const chassis = new THREE.Mesh(
- rounded(0.69, 0.77, 0.045, true),
- chassisMaterial,
- );
- chassis.position.set(0.29, 0.13, 0);
- scene.add(chassis);
- const backing = new THREE.Mesh(rounded(0.19, 0.29, 0.06), backingMaterial);
- backing.position.set(0.035, 0.43, 0.04);
- scene.add(backing);
- const cursorShape = new THREE.Shape();
- cursorShape.moveTo(0, 0);
- cursorShape.lineTo(0.027, -0.033);
- cursorShape.lineTo(0.014, -0.032);
- cursorShape.lineTo(0.009, -0.046);
- cursorShape.lineTo(0, 0);
- const cursorGeometry = new THREE.ExtrudeGeometry(cursorShape, {
- depth: 0.005,
- bevelEnabled: false,
- });
- geometries.push(cursorGeometry);
- const ringGeometry = new THREE.RingGeometry(0.017, 0.02, 32);
- geometries.push(ringGeometry);
- const cursor = new THREE.Mesh(cursorGeometry, cursorMaterial);
- const ring = new THREE.Mesh(ringGeometry, rippleMaterial);
- scene.add(cursor, ring);
- let disposed = false;
- let aspect = 1;
- let ringScale = 1;
-
- function dispose() {
- if (disposed) return;
- disposed = true;
- renderer.domElement.removeEventListener("webglcontextlost", contextLost);
- geometries.forEach((geometry) => geometry.dispose());
- materials.forEach((material) => material.dispose());
- renderer.dispose();
- renderer.domElement.remove();
- }
-
- function contextLost(event: Event) {
- event.preventDefault();
- dispose();
- onFailure();
- }
-
- renderer.domElement.addEventListener("webglcontextlost", contextLost);
- stage.appendChild(renderer.domElement);
- return {
- resize(width: number, height: number, lightMode: boolean) {
- renderer.setSize(width, height, false);
- const stageBounds = stage.getBoundingClientRect();
- const avatarBounds = stage
- .querySelector("[data-demo-companion] svg")
- ?.getBoundingClientRect();
- if (avatarBounds) {
- backing.scale.set(
- (avatarBounds.width + 4) / width / 0.19,
- (avatarBounds.height + 4) / height / 0.29,
- 1,
- );
- backing.position.set(
- (avatarBounds.left - stageBounds.left - 2) / width,
- 1 - (avatarBounds.bottom - stageBounds.top + 2) / height,
- 0.04,
- );
- }
- aspect = width / height;
- const cursorScale = 15 / (width * 0.027);
- cursor.scale.set(cursorScale, cursorScale * aspect, 1);
- ringScale = 10 / (width * 0.02);
- chassisMaterial.color.set(lightMode ? 0xd0bcdf : 0x665279);
- backingMaterial.color.set(lightMode ? 0xc7afdf : 0x9c83c7);
- },
- render(frame: DemoRenderFrame) {
- if (disposed) return;
- backing.rotation.z = frame.drift * 0.035;
- cursor.position.set(frame.cursorX, 1 - frame.cursorY, 0.3);
- ring.position.set(frame.cursorX, 1 - frame.cursorY, 0.25);
- ring.scale.x = ringScale * (1 + frame.ripple * 2);
- ring.scale.y = ringScale * aspect * (1 + frame.ripple * 2);
- rippleMaterial.opacity =
- frame.ripple > 0 ? (1 - frame.ripple) * 0.7 : 0;
- cursor.visible = !frame.complete;
- renderer.render(scene, camera);
- },
- dispose,
- };
- } catch (error) {
- geometries.forEach((geometry) => geometry.dispose());
- materials.forEach((material) => material.dispose());
- renderer.dispose();
- renderer.domElement.remove();
- throw error;
- }
-}
diff --git a/components/ui/skullet-image.tsx b/components/ui/skullet-image.tsx
index bf6b9b6..0802717 100644
--- a/components/ui/skullet-image.tsx
+++ b/components/ui/skullet-image.tsx
@@ -26,7 +26,7 @@ export function loadSkulletOverlay() {
}
// Loaded only after an image is still incomplete. A static import pulls Skullet
-// into Pip's initial script. A failed chunk drops the skeleton and leaves the image.
+// into the initial script. A failed chunk drops the skeleton and leaves the image.
const SkulletOverlay = lazy(() =>
import("./skullet-overlay")
.then((mod) => ({ default: rememberOverlay(mod.default) }))
diff --git a/docs/screenshots/homepage-pip-desktop.png b/docs/screenshots/homepage-pip-desktop.png
deleted file mode 100644
index 296a223..0000000
Binary files a/docs/screenshots/homepage-pip-desktop.png and /dev/null differ
diff --git a/docs/screenshots/homepage-pip-mobile.png b/docs/screenshots/homepage-pip-mobile.png
deleted file mode 100644
index 3c94d85..0000000
Binary files a/docs/screenshots/homepage-pip-mobile.png and /dev/null differ
diff --git a/docs/screenshots/pip-button-detail.png b/docs/screenshots/pip-button-detail.png
deleted file mode 100644
index 8a68dad..0000000
Binary files a/docs/screenshots/pip-button-detail.png and /dev/null differ
diff --git a/docs/screenshots/pip-desktop.png b/docs/screenshots/pip-desktop.png
deleted file mode 100644
index b661c19..0000000
Binary files a/docs/screenshots/pip-desktop.png and /dev/null differ
diff --git a/docs/screenshots/pip-mobile.png b/docs/screenshots/pip-mobile.png
deleted file mode 100644
index 522201b..0000000
Binary files a/docs/screenshots/pip-mobile.png and /dev/null differ
diff --git a/lib/pip.ts b/lib/pip.ts
deleted file mode 100644
index 5b28434..0000000
--- a/lib/pip.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-// Pip's public Telegram bot username, supplied by Lu. No leading @ required.
-// Rebuild the static site after changing it. Never put credentials here.
-export const PIP_TELEGRAM_HANDLE = "PIPSOFTWARE_BOT";
-
-export function getPipContact(value: string = PIP_TELEGRAM_HANDLE) {
- const username = value.trim().replace(/^@/, "");
-
- if (!/^[a-zA-Z][a-zA-Z0-9_]{4,31}$/.test(username)) return null;
-
- const telegramUrl = `https://t.me/${username}`;
- return {
- handle: `@${username}`,
- telegramUrl,
- qrUrl: `/qr/${username}.svg`,
- };
-}
diff --git a/package-lock.json b/package-lock.json
index a833ee7..79d721b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35,7 +35,6 @@
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"globals": "^16.5.0",
- "qrcode": "1.5.4",
"typescript": "5.9.3",
"typescript-eslint": "^8.48.1",
"vite": "8.3.3"
@@ -2714,6 +2713,72 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.4",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
+ "version": "0.10.2",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
+ "version": "2.8.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "0BSD",
+ "optional": true
+ },
"node_modules/@tailwindcss/vite/node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz",
@@ -3458,16 +3523,6 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
- "node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -3578,16 +3633,6 @@
"node": ">=6"
}
},
- "node_modules/camelcase": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
- "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/caniuse-lite": {
"version": "1.0.30001815",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001815.tgz",
@@ -3638,18 +3683,6 @@
"url": "https://polar.sh/cva"
}
},
- "node_modules/cliui": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
- "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "string-width": "^4.2.0",
- "strip-ansi": "^6.0.0",
- "wrap-ansi": "^6.2.0"
- }
- },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -3745,16 +3778,6 @@
}
}
},
- "node_modules/decamelize": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
- "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -3778,13 +3801,6 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
- "node_modules/dijkstrajs": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
- "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/electron-to-chromium": {
"version": "1.5.452",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.452.tgz",
@@ -3792,13 +3808,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/enhanced-resolve": {
"version": "5.26.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz",
@@ -4141,16 +4150,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/get-caller-file": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
- "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": "6.* || 8.* || >= 10.*"
- }
- },
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
@@ -4267,16 +4266,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -4588,16 +4577,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/p-try": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
- "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -4638,16 +4617,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/pngjs": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
- "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10.13.0"
- }
- },
"node_modules/postcss": {
"version": "8.5.29",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz",
@@ -4697,24 +4666,6 @@
"node": ">=6"
}
},
- "node_modules/qrcode": {
- "version": "1.5.4",
- "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
- "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "dijkstrajs": "^1.0.1",
- "pngjs": "^5.0.0",
- "yargs": "^15.3.1"
- },
- "bin": {
- "qrcode": "bin/qrcode"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
"node_modules/radix-ui": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.7.0.tgz",
@@ -4882,23 +4833,6 @@
}
}
},
- "node_modules/require-directory": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
- "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/require-main-filename": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
- "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -4959,13 +4893,6 @@
"semver": "bin/semver.js"
}
},
- "node_modules/set-blocking": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
- "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -5025,34 +4952,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -5694,13 +5593,6 @@
"node": ">= 8"
}
},
- "node_modules/which-module": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
- "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -5711,28 +5603,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/wrap-ansi": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
- "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/y18n": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
- "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -5740,99 +5610,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/yargs": {
- "version": "15.4.1",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
- "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cliui": "^6.0.0",
- "decamelize": "^1.2.0",
- "find-up": "^4.1.0",
- "get-caller-file": "^2.0.1",
- "require-directory": "^2.1.1",
- "require-main-filename": "^2.0.0",
- "set-blocking": "^2.0.0",
- "string-width": "^4.2.0",
- "which-module": "^2.0.0",
- "y18n": "^4.0.0",
- "yargs-parser": "^18.1.2"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs-parser": {
- "version": "18.1.3",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
- "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "camelcase": "^5.0.0",
- "decamelize": "^1.2.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/yargs/node_modules/find-up": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
- "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "locate-path": "^5.0.0",
- "path-exists": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/locate-path": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
- "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "p-locate": "^4.1.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/p-limit": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
- "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "p-try": "^2.0.0"
- },
- "engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/yargs/node_modules/p-locate": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
- "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "p-limit": "^2.2.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 9a7f28f..c82efdf 100644
--- a/package.json
+++ b/package.json
@@ -4,9 +4,8 @@
"private": true,
"type": "module",
"scripts": {
- "predev": "node scripts/generate-pip-qr.mjs",
"dev": "vite --host 127.0.0.1",
- "prebuild": "node scripts/generate-sitemap.js && node scripts/generate-pip-qr.mjs",
+ "prebuild": "node scripts/generate-sitemap.js",
"build": "tsc --noEmit && vite build && vite build --ssr app/entry-server.tsx && node scripts/prerender.mjs",
"check": "tsc --noEmit",
"start": "node scripts/serve-preview.mjs",
@@ -14,7 +13,6 @@
"lint": "eslint",
"test:e2e:setup": "python3 -m pip install -r requirements-test.txt && python3 -m playwright install chromium",
"test:e2e": "npm run build && python3 -m unittest discover -s tests/browser -p '*_test.py' -v",
- "test:pip": "node --test tests/pip-contact.test.mts",
"test:pond": "node --test tests/pond-world.test.mts"
},
"dependencies": {
@@ -45,7 +43,6 @@
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"globals": "^16.5.0",
- "qrcode": "1.5.4",
"typescript": "5.9.3",
"typescript-eslint": "^8.48.1",
"vite": "8.3.3"
diff --git a/pip.html b/pip.html
deleted file mode 100644
index 1349850..0000000
--- a/pip.html
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/public/images/portfolio/pip-everyday.webp b/public/images/portfolio/pip-everyday.webp
deleted file mode 100644
index c032065..0000000
Binary files a/public/images/portfolio/pip-everyday.webp and /dev/null differ
diff --git a/public/share-cards/luinbytes-dev-pip.png b/public/share-cards/luinbytes-dev-pip.png
deleted file mode 100644
index ec07112..0000000
Binary files a/public/share-cards/luinbytes-dev-pip.png and /dev/null differ
diff --git a/public/sitemap.xml b/public/sitemap.xml
index 9757604..c8efe92 100644
--- a/public/sitemap.xml
+++ b/public/sitemap.xml
@@ -2,19 +2,13 @@
https://luinbytes.dev
- 2026-10-26
- weekly
- 0.6
-
-
- https://luinbytes.dev/pip
- 2026-10-26
+ 2026-11-01
weekly
0.6
https://luinbytes.dev/privacy
- 2026-10-26
+ 2026-11-01
weekly
0.6
diff --git a/scripts/generate-pip-qr.mjs b/scripts/generate-pip-qr.mjs
deleted file mode 100644
index 837dff5..0000000
--- a/scripts/generate-pip-qr.mjs
+++ /dev/null
@@ -1,16 +0,0 @@
-import { mkdir, writeFile } from "node:fs/promises";
-import QRCode from "qrcode";
-import { getPipContact } from "../lib/pip.ts";
-
-const contact = getPipContact();
-if (contact) {
- const directory = new URL("../public/qr/", import.meta.url);
- await mkdir(directory, { recursive: true });
- const svg = await QRCode.toString(contact.telegramUrl, {
- type: "svg",
- errorCorrectionLevel: "M",
- margin: 4,
- width: 240,
- });
- await writeFile(new URL(`../public${contact.qrUrl}`, import.meta.url), svg);
-}
diff --git a/scripts/generate-pip-share-card.py b/scripts/generate-pip-share-card.py
deleted file mode 100644
index 1946ba9..0000000
--- a/scripts/generate-pip-share-card.py
+++ /dev/null
@@ -1,91 +0,0 @@
-"""Render the checked-in Pip social PNG from the installed local fonts.
-
-Run `npm ci` first, then `python3 scripts/generate-pip-share-card.py`.
-Rendering is offline; normal builds only copy the checked-in PNG into out/.
-"""
-
-import base64
-import hashlib
-import json
-import re
-import tempfile
-from pathlib import Path
-
-from playwright.sync_api import sync_playwright
-from share_card_fonts import local_fonts
-
-
-ROOT = Path(__file__).resolve().parents[1]
-CARD = ROOT / "public/share-cards/luinbytes-dev-pip.png"
-COPY = [
- "PIP / TELEGRAM AGENT",
- "A LITTLE SOMETHING BY LU",
- "pip.",
- "YOUR TELEGRAM MATE",
- "A little help, one message away.",
- "SEARCH",
- "REMINDERS",
- "EVERYDAY LIFE",
- "WARM / SHORT / EASYGOING",
-]
-FONT_FAMILIES = ("Pixelify Sans", "Space Grotesk", "Space Mono")
-
-
-def data_url(path: Path, mime: str) -> str:
- return f"data:{mime};base64,{base64.b64encode(path.read_bytes()).decode()}"
-
-
-
-def main() -> None:
- html = (ROOT / "scripts/pip-share-card.html").read_text().replace(
- "/* FONT_FACES */", local_fonts(include_pixelify=True)
- )
- CARD.parent.mkdir(parents=True, exist_ok=True)
- with tempfile.TemporaryDirectory(dir=ROOT / "scripts") as directory:
- document = Path(directory) / "pip-share-card.html"
- document.write_text(html)
- with sync_playwright() as playwright:
- browser = playwright.chromium.launch()
- try:
- page = browser.new_page(
- viewport={"width": 1200, "height": 630},
- device_scale_factor=1,
- reduced_motion="reduce",
- )
- page.route(re.compile(r"^https?://"), lambda route: route.abort())
- page.goto(document.as_uri(), wait_until="load")
- page.evaluate("document.fonts.ready")
- for family in FONT_FAMILIES:
- loaded = page.evaluate(
- "family => [...document.fonts].some(font => font.family === family && font.status === 'loaded')",
- family,
- )
- if not loaded:
- raise RuntimeError(f"Required font did not load: {family}")
- copy = page.locator("[data-copy]").evaluate_all(
- "nodes => nodes.map(node => node.textContent.replace(/\\s+/g, ' ').trim())"
- )
- if copy != COPY:
- raise RuntimeError(f"Share-card copy changed: {copy!r}")
- boxes = page.locator("[data-copy]").evaluate_all(
- "nodes => nodes.map(node => ({text: node.textContent.trim(), ...node.getBoundingClientRect().toJSON()}))"
- )
- for box in boxes:
- if not (32 <= box["left"] < box["right"] <= 1168 and 32 <= box["top"] < box["bottom"] <= 598):
- raise RuntimeError(f"Text exceeds the 32px crop-safe inset: {box}")
- page.screenshot(path=str(CARD), type="png", animations="disabled")
- finally:
- browser.close()
- png = CARD.read_bytes()
- print(json.dumps({
- "output": str(CARD.resolve()),
- "dimensions": [1200, 630],
- "bytes": len(png),
- "sha256": hashlib.sha256(png).hexdigest(),
- "copy": copy,
- "text_boxes": boxes,
- }, indent=2))
-
-
-if __name__ == "__main__":
- main()
diff --git a/scripts/generate-sitemap.js b/scripts/generate-sitemap.js
index f5dbfd5..d1a8950 100644
--- a/scripts/generate-sitemap.js
+++ b/scripts/generate-sitemap.js
@@ -9,7 +9,7 @@ const projectRoot = fileURLToPath(new URL('..', import.meta.url));
const { siteUrl } = JSON.parse(fs.readFileSync(path.join(projectRoot, 'site.config.json'), 'utf8'));
const outputPath = path.join(projectRoot, 'public/sitemap.xml');
-const staticPages = ['', '/pip', '/privacy'];
+const staticPages = ['', '/privacy'];
function generateSitemap() {
const lastModified = execFileSync('git', ['log', '-1', '--format=%cs'], {
diff --git a/scripts/pip-share-card.html b/scripts/pip-share-card.html
deleted file mode 100644
index 8f83843..0000000
--- a/scripts/pip-share-card.html
+++ /dev/null
@@ -1,163 +0,0 @@
-
-
-
-
- Pip social preview source
-
-
-
-
-
-
- PIP / TELEGRAM AGENT
-
-
-
- A LITTLE SOMETHING BY LU
- pip.
- YOUR TELEGRAM MATE
- A little help, one message away.
-
- SEARCH
- REMINDERS
- EVERYDAY LIFE
-
-
-
-
-
-
-
-
-
-
- Pip
- A LITTLE HELP, ONE TEXT AWAY
-
-
✳
-
-
-
A PERFECTLY ORDINARY EVENING
-
got eggs, rice and zero motivation
-
egg fried rice. ten minutes, one pan. future you is grateful 🍳
-
remind me to get soy sauce tomorrow at 6pm
-
you got it. tomorrow, 6pm 🫡
-
one less thing in your head.
-
- SMALL TALK. USEFUL THINGS.
-
-
-
-
-
diff --git a/tests/browser/browser_test.py b/tests/browser/browser_test.py
index 69a3c56..7566603 100644
--- a/tests/browser/browser_test.py
+++ b/tests/browser/browser_test.py
@@ -26,6 +26,8 @@
READY_PATTERN = re.compile(r"\bReady in\b", re.IGNORECASE)
ANSI_COLOR_PATTERN = re.compile(r"\x1b\[[0-9;]*m")
LEGACY_ROUTES = (
+ "/pip",
+ "/pip.html",
"/concepts/signal-desk",
"/concepts/signal-field",
"/concepts/trace",
@@ -41,7 +43,6 @@
)
PORTFOLIO_NAV_LINKS = (
("Work", "#work"),
- ("Pip", "/pip"),
("About", "#about"),
("Contact", "#contact"),
)
@@ -197,7 +198,41 @@ def poll(self):
class GeneratedArtifactTests(unittest.TestCase):
- def test_sitemap_exposes_the_portfolio_pip_and_privacy(self) -> None:
+ def test_exported_internal_links_resolve(self) -> None:
+ class Links(HTMLParser):
+ def __init__(self):
+ super().__init__()
+ self.hrefs = []
+ self.ids = set()
+
+ def handle_starttag(self, tag, attrs):
+ attrs = dict(attrs)
+ if attrs.get("id"):
+ self.ids.add(attrs["id"])
+ if tag == "a" and attrs.get("href"):
+ self.hrefs.append(attrs["href"])
+
+ root = Path("out")
+ for source in root.glob("*.html"):
+ document = Links()
+ document.feed(source.read_text())
+ for href in document.hrefs:
+ url = urlparse(href)
+ if url.scheme or url.netloc:
+ continue
+ target = root / (url.path.lstrip("/") or source.name)
+ if url.path == "/":
+ target = root / "index.html"
+ elif url.path and not target.suffix:
+ target = target.with_suffix(".html")
+ with self.subTest(source=source, href=href):
+ self.assertTrue(target.is_file())
+ if url.fragment and target.is_file() and target.suffix == ".html":
+ destination = Links()
+ destination.feed(target.read_text())
+ self.assertIn(url.fragment, destination.ids)
+
+ def test_sitemap_exposes_the_portfolio_and_privacy(self) -> None:
root = ET.parse("public/sitemap.xml").getroot()
locations = [
location.text
@@ -211,7 +246,6 @@ def test_sitemap_exposes_the_portfolio_pip_and_privacy(self) -> None:
locations,
[
"https://luinbytes.dev",
- "https://luinbytes.dev/pip",
"https://luinbytes.dev/privacy",
],
)
@@ -252,15 +286,12 @@ class ShareCardStaticExportTests(unittest.TestCase):
)
route_cards = {
"/": "/share-cards/luinbytes-dev-pond.png",
- "/pip": "/share-cards/luinbytes-dev-pip.png",
}
expected_sha256 = {
"/share-cards/luinbytes-dev-pond.png": "60bddd42c8ecc43ac42f94933f15af50cb71c7039574b4d203be4fb2dc46b811",
- "/share-cards/luinbytes-dev-pip.png": "e8ad43690346c7b2ff683e1da1676e82822ec0cc2498365dc095188b56e72789",
}
route_alts = {
"/": "Lu | Software Engineer",
- "/pip": "Pip - Your Telegram mate",
}
def test_editable_source_contains_the_original_card_copy(self) -> None:
@@ -318,35 +349,6 @@ def test_every_scoped_route_exports_a_production_large_image_card(self) -> None:
),
"https://luinbytes.dev",
)
- if route == "/pip":
- self.assertIn(
- "Pip - Your Telegram mate | Lu ",
- html,
- )
- self.assertEqual(
- properties.get("og:title"),
- "Pip - Your Telegram mate",
- )
- self.assertEqual(
- properties.get("og:description"),
- "A little help, one message away. Pip is your warm, easygoing Telegram agent for questions, web searches, reminders, and everyday life. Built on Keiki.",
- )
- self.assertEqual(
- properties.get("og:url"),
- "https://luinbytes.dev/pip",
- )
- self.assertEqual(
- names.get("description"),
- "A little help, one message away. Pip is your warm, easygoing Telegram agent for questions, web searches, reminders, and everyday life. Built on Keiki.",
- )
- self.assertEqual(
- next(
- item.get("href")
- for item in parser.metadata
- if item.get("rel") == "canonical"
- ),
- "https://luinbytes.dev/pip",
- )
for attribute, key in (("property", "og:image"), ("name", "twitter:image")):
self.assertEqual(
sum(item.get(attribute) == key for item in parser.metadata), 1
@@ -444,8 +446,8 @@ def test_html_navigations_keep_their_page_and_status(self) -> None:
page = browser.new_page(reduced_motion="reduce")
for path, status, entry in (
("/", 200, "entry-client.tsx"),
- ("/pip", 200, "entry-pip.tsx"),
- ("/pip.html", 200, "entry-pip.tsx"),
+ ("/pip", 404, "entry-not-found.tsx"),
+ ("/pip.html", 404, "entry-not-found.tsx"),
("/privacy", 200, "entry-privacy.tsx"),
("/privacy.html", 200, "entry-privacy.tsx"),
("/missing.html", 404, "entry-not-found.tsx"),
@@ -456,9 +458,7 @@ def test_html_navigations_keep_their_page_and_status(self) -> None:
self.assertEqual(response.status, status)
self.assertIn(f'/app/{entry}', response.text())
expect(page.locator("h1")).to_be_visible()
- response = page.request.get(f"{self.base_url}/qr/PIPSOFTWARE_BOT.svg", headers={"Accept": "text/html"})
- self.assertEqual(response.status, 200)
- self.assertIn("image/svg+xml", response.headers["content-type"])
+
class SecurityBoundaryTests(BrowserTestCase):
@@ -480,7 +480,7 @@ def serve_without_policy_header(route):
browser = self.playwright.chromium.launch()
self.addCleanup(browser.close)
- for path in ("/", "/pip", "/missing/nested/page"):
+ for path in ("/", "/privacy", "/missing/nested/page"):
with self.subTest(path=path):
context = browser.new_context(reduced_motion="reduce")
try:
@@ -527,7 +527,7 @@ def test_preview_rejects_escape_paths_and_unsupported_methods(self) -> None:
self.assertEqual(status, 400)
self.assertEqual(headers.get("X-Content-Type-Options"), "nosniff")
self.assertEqual(self.request("/", "POST")[0], 405)
- status, headers, body = self.request("/pip", "HEAD")
+ status, headers, body = self.request("/privacy", "HEAD")
self.assertEqual(status, 200)
self.assertEqual(body, b"")
self.assertGreater(int(headers["Content-Length"]), 0)
@@ -538,7 +538,7 @@ def test_preview_rejects_escape_paths_and_unsupported_methods(self) -> None:
self.assertIn(b"Lumi Health", privacy_body)
for path in ("/404", "/404.html", "/missing/nested/page"):
self.assertEqual(self.request(path)[0], 404)
- self.assertEqual(self.request("/pip///")[0], 200)
+ self.assertEqual(self.request("/pip///")[0], 404)
self.assertEqual(self.request("/privacy///")[0], 200)
def test_preview_does_not_follow_symlinks_outside_the_export(self) -> None:
@@ -748,334 +748,6 @@ def compare_reference(actual: str, reference_name: str) -> dict:
)
-class PipRouteTests(BrowserTestCase):
- def test_initial_page_keeps_scripts_below_300_kb_and_requests_only_local_assets(self) -> None:
- browser = self.playwright.chromium.launch()
- self.addCleanup(browser.close)
- context = browser.new_context(viewport={"width": 390, "height": 844}, reduced_motion="reduce")
- requests = []
- page = context.new_page()
- page.on("request", lambda request: requests.append(request.url))
- page.goto(f"{self.base_url}/pip", wait_until="networkidle")
- scripts = page.evaluate("""() => performance.getEntriesByType('resource')
- .filter(entry => new URL(entry.name).pathname.endsWith('.js'))
- .map(entry => entry.decodedBodySize)""")
- self.assertGreater(len(scripts), 0)
- self.assertTrue(all(size > 0 for size in scripts))
- self.assertLess(sum(scripts), 300_000)
- self.assertTrue(all(urlparse(url).netloc == urlparse(self.base_url).netloc for url in requests))
- self.assertFalse(any("/qr/" in url for url in requests))
- context.close()
-
- def test_browsing_demo_renders_and_suspends_offscreen(self) -> None:
- browser = self.playwright.chromium.launch()
- context = browser.new_context(viewport={"width": 1440, "height": 900})
- page = context.new_page()
- page.goto(f"{self.base_url}/pip", wait_until="networkidle")
- demo = page.get_by_test_id("pip-browser-demo")
- demo.scroll_into_view_if_needed()
- expect(demo).to_have_attribute("data-renderer", "three", timeout=15000)
- expect(demo).to_have_attribute("data-playback", "playing")
- self.assertEqual(demo.locator("canvas").count(), 1)
- expect(demo.locator("canvas")).to_be_visible()
- first_phase = demo.get_attribute("data-phase")
- wait_for_condition(
- page,
- "phase => document.querySelector('[data-testid=pip-browser-demo]').dataset.phase !== phase",
- arg=first_phase,
- timeout=12000,
- )
- self.assertEqual(demo.get_by_role("button").count(), 0)
- self.assertNotIn("demo", demo.inner_text().lower())
- page.locator("header").scroll_into_view_if_needed()
- expect(demo).to_have_attribute("data-playback", "paused")
- paused_phase = demo.get_attribute("data-phase")
- page.wait_for_timeout(3500)
- self.assertEqual(demo.get_attribute("data-phase"), paused_phase)
- demo.scroll_into_view_if_needed()
- expect(demo).to_have_attribute("data-playback", "playing")
- demo.screenshot(path=SCREENSHOTS / "pip-browsing-demo-1440.png")
- expect(demo).to_have_attribute("data-phase", "postage", timeout=12000)
- for width in (320, 390, 1440):
- page.set_viewport_size({"width": width, "height": 900})
- demo.scroll_into_view_if_needed()
- expect(demo).to_have_attribute("data-phase", "postage", timeout=25000)
- bounds = demo.locator("[data-demo-browser]").bounding_box()
- target = demo.locator("[data-demo-target]").bounding_box()
- self.assertIsNotNone(bounds)
- self.assertIsNotNone(target)
- self.assertGreaterEqual(target["x"], bounds["x"])
- self.assertLessEqual(target["x"] + target["width"], bounds["x"] + bounds["width"])
- self.assertLessEqual(target["y"] + target["height"], bounds["y"] + bounds["height"])
- page.locator("header").scroll_into_view_if_needed()
- expect(demo).to_have_attribute("data-playback", "paused")
- demo.scroll_into_view_if_needed()
- expect(demo).to_have_attribute("data-playback", "playing")
- page.goto(f"{self.base_url}/", wait_until="networkidle")
- page.goto(f"{self.base_url}/pip", wait_until="networkidle")
- demo.scroll_into_view_if_needed()
- expect(demo).to_have_attribute("data-renderer", "three", timeout=15000)
- self.assertEqual(demo.locator("canvas").count(), 1)
- context.close()
- browser.close()
-
- def test_browsing_demo_respects_reduced_motion(self) -> None:
- browser = self.playwright.chromium.launch()
- context = browser.new_context(
- viewport={"width": 320, "height": 844}, reduced_motion="reduce"
- )
- page = context.new_page()
- page.goto(f"{self.base_url}/pip", wait_until="networkidle")
- demo = page.get_by_test_id("pip-browser-demo")
- demo.scroll_into_view_if_needed()
- expect(demo).to_have_attribute("data-playback", "paused")
- self.assertEqual(demo.locator("canvas").count(), 0)
- phase = demo.get_attribute("data-phase")
- page.wait_for_timeout(1000)
- self.assertEqual(demo.get_attribute("data-phase"), phase)
- self.assertLessEqual(page.evaluate("document.documentElement.scrollWidth"), 320)
- demo.screenshot(path=SCREENSHOTS / "pip-browsing-demo-reduced-320.png")
- page.emulate_media(reduced_motion="no-preference")
- expect(demo).to_have_attribute("data-renderer", "three", timeout=15000)
- expect(demo).to_have_attribute("data-playback", "playing")
- page.emulate_media(reduced_motion="reduce")
- expect(demo).to_have_attribute("data-playback", "paused")
- context.close()
- browser.close()
-
- def test_browsing_demo_keeps_its_fallback_when_webgl_fails(self) -> None:
- browser = self.playwright.chromium.launch()
- context = browser.new_context(viewport={"width": 390, "height": 844})
- context.add_init_script(
- """const original = HTMLCanvasElement.prototype.getContext;
- HTMLCanvasElement.prototype.getContext = function(kind, ...args) {
- if (kind === 'webgl' || kind === 'webgl2' || kind === 'experimental-webgl') return null;
- return original.call(this, kind, ...args);
- };"""
- )
- page = context.new_page()
- page.goto(f"{self.base_url}/pip", wait_until="networkidle")
- demo = page.get_by_test_id("pip-browser-demo")
- demo.scroll_into_view_if_needed()
- expect(demo).to_have_attribute("data-renderer", "fallback", timeout=15000)
- expect(demo).to_have_attribute("data-playback", "paused")
- self.assertEqual(demo.locator("canvas").count(), 0)
- expect(demo.locator("[data-demo-stage]")).to_be_visible()
- self.assertGreater(demo.locator("svg").count(), 0)
- self.assertLessEqual(page.evaluate("document.documentElement.scrollWidth"), 390)
- demo.screenshot(path=SCREENSHOTS / "pip-browsing-demo-fallback-390.png")
- context.close()
- browser.close()
-
- def test_pip_page_has_static_content_and_configured_telegram_contact(self) -> None:
- browser = self.playwright.chromium.launch()
- page = browser.new_page(viewport={"width": 1440, "height": 900})
- response = page.goto(f"{self.base_url}/pip", wait_until="networkidle")
- self.assertIsNotNone(response)
- self.assertEqual(response.status, 200)
- self.assertEqual(page.title(), "Pip - Your Telegram mate | Lu")
- self.assertTrue(
- page.get_by_role(
- "heading", name=re.compile(r"A little help\.\s*A little banter\.")
- ).is_visible()
- )
- self.assertTrue(page.get_by_role("figure", name="Example conversation").is_visible())
- contact = page.locator("#text-pip")
- self.assertTrue(contact.is_visible())
- self.assertTrue(page.get_by_role("heading", name="Say hey to Pip.").is_visible())
- self.assertTrue(contact.get_by_text("PIP’S TELEGRAM HANDLE", exact=True).is_visible())
- self.assertTrue(contact.get_by_text("@PIPSOFTWARE_BOT", exact=True).is_visible())
- self.assertTrue(contact.get_by_text("Opens Pip in Telegram.", exact=True).is_visible())
- self.assertTrue(page.get_by_text("Built on Keiki", exact=False).is_visible())
-
- self.assertGreaterEqual(
- page.get_by_role("link", name="Message Pip", exact=True).count(), 2
- )
- message_link = contact.get_by_role("link", name="Message Pip", exact=True)
- self.assertEqual(message_link.get_attribute("href"), "https://t.me/PIPSOFTWARE_BOT")
- copy_button = contact.get_by_role("button", name="Copy handle", exact=True)
- qr_button = contact.get_by_role("button", name="Show QR code", exact=True)
- self.assertFalse(copy_button.is_disabled())
- self.assertFalse(qr_button.is_disabled())
- self.assertEqual(qr_button.get_attribute("aria-expanded"), "false")
- for theme in ("dark", "light"):
- page.emulate_media(color_scheme=theme)
- page.wait_for_timeout(40)
- for link in page.get_by_role("link", name="Message Pip", exact=True).all():
- self.assertGreaterEqual(control_contrast(link), 4.5)
- page.screenshot(path=SCREENSHOTS / "pip-final-1440.png", full_page=True)
-
- browser.close()
-
- def test_telegram_contact_copy_reports_success_and_failure(self) -> None:
- browser = self.playwright.chromium.launch()
-
- success_context = browser.new_context(
- viewport={"width": 390, "height": 844},
- permissions=["clipboard-read", "clipboard-write"],
- )
- success_context.add_init_script(
- """window.__pipClipboardCalls = [];
- Object.defineProperty(navigator, 'clipboard', {
- configurable: true,
- value: { writeText: async value => window.__pipClipboardCalls.push(value) },
- });"""
- )
- success_page = success_context.new_page()
- success_page.goto(f"{self.base_url}/pip", wait_until="networkidle")
- success_page.locator("#text-pip").get_by_role(
- "button", name="Copy handle", exact=True
- ).click()
- success_page.get_by_text("Handle copied.", exact=True).wait_for(state="visible")
- self.assertTrue(success_page.get_by_text("Handle copied.", exact=True).is_visible())
- self.assertEqual(
- success_page.evaluate("window.__pipClipboardCalls"),
- ["@PIPSOFTWARE_BOT"],
- )
- success_context.close()
-
- failure_context = browser.new_context(viewport={"width": 390, "height": 844})
- failure_context.add_init_script(
- """Object.defineProperty(navigator, 'clipboard', {
- configurable: true,
- value: { writeText: async () => { throw new Error('blocked') } },
- });"""
- )
- failure_page = failure_context.new_page()
- failure_page.goto(f"{self.base_url}/pip", wait_until="networkidle")
- failure_page.locator("#text-pip").get_by_role(
- "button", name="Copy handle", exact=True
- ).click()
- failure_page.get_by_text(
- "Couldn’t copy. Select the handle above and copy it manually.", exact=True
- ).wait_for(state="visible")
- self.assertTrue(
- failure_page.get_by_text(
- "Couldn’t copy. Select the handle above and copy it manually.", exact=True
- ).is_visible()
- )
- failure_context.close()
- browser.close()
-
- def test_telegram_qr_loads_locally_on_demand_and_reports_failure(self) -> None:
- browser = self.playwright.chromium.launch()
- requests = []
- fulfilled_context = browser.new_context(viewport={"width": 390, "height": 844})
- fulfilled_page = fulfilled_context.new_page()
- fulfilled_page.on("request", lambda request: requests.append(request.url))
- fulfilled_page.goto(f"{self.base_url}/pip", wait_until="networkidle")
- self.assertFalse(any("/qr/" in url for url in requests))
- contact = fulfilled_page.locator("#text-pip")
- qr_button = contact.get_by_role("button", name="Show QR code", exact=True)
- qr_button.click()
- image = contact.get_by_alt_text("Scan to message Pip on Telegram")
- image.wait_for(state="visible")
- self.assertEqual(
- contact.locator("button[aria-controls='pip-qr']").get_attribute("aria-expanded"),
- "true",
- )
- self.assertTrue(image.is_visible())
- expect(image).to_have_attribute("src", "/qr/PIPSOFTWARE_BOT.svg")
- wait_for_condition(
- fulfilled_page,
- "() => document.querySelector('#pip-qr img')?.naturalWidth === 240"
- )
- self.assertIn(f"{self.base_url}/qr/PIPSOFTWARE_BOT.svg", requests)
- self.assertTrue(all(urlparse(url).netloc == urlparse(self.base_url).netloc for url in requests))
- fulfilled_context.close()
-
- aborted_context = browser.new_context(viewport={"width": 390, "height": 844})
- aborted_page = aborted_context.new_page()
- aborted_page.route("**/qr/*.svg", lambda route: route.abort())
- aborted_page.goto(f"{self.base_url}/pip", wait_until="networkidle")
- aborted_page.locator("#text-pip").get_by_role(
- "button", name="Show QR code", exact=True
- ).click()
- qr_failure = aborted_page.get_by_text(
- re.compile(r"QR code.*couldn.t load", re.IGNORECASE)
- )
- qr_failure.wait_for(state="visible")
- self.assertTrue(qr_failure.is_visible())
- aborted_context.close()
- browser.close()
-
- def test_pip_page_fits_mobile_viewports_and_exposes_keyboard_skip_and_nav(self) -> None:
- browser = self.playwright.chromium.launch()
- page = browser.new_page(viewport={"width": 390, "height": 844})
- page.goto(f"{self.base_url}/pip", wait_until="networkidle")
-
- for width in (320, 390, 430):
- with self.subTest(viewport=width):
- page.set_viewport_size({"width": width, "height": 844})
- page.wait_for_timeout(80)
- self.assertLessEqual(
- page.evaluate("document.documentElement.scrollWidth"), width
- )
- self.assertLessEqual(
- page.evaluate("document.body.scrollWidth"), width
- )
- bounds = page.locator("#text-pip").bounding_box()
- self.assertIsNotNone(bounds)
- self.assertGreaterEqual(bounds["x"], -1)
- self.assertLessEqual(bounds["x"] + bounds["width"], width + 1)
- if width in (320, 390):
- page.screenshot(
- path=SCREENSHOTS / f"pip-final-{width}.png",
- full_page=True,
- )
-
- page.set_viewport_size({"width": 390, "height": 844})
- page.keyboard.press("Tab")
- self.assertEqual(
- page.evaluate(
- """() => ({
- href: document.activeElement?.getAttribute('href'),
- text: document.activeElement?.textContent?.trim(),
- })"""
- ),
- {"href": "#main", "text": "Skip to content"},
- )
- page.keyboard.press("Enter")
- self.assertEqual(page.evaluate("document.activeElement?.id"), "main")
-
- navigation = page.locator("header")
- self.assertTrue(navigation.is_visible())
- links = navigation.get_by_role("link")
- self.assertGreaterEqual(links.count(), 2)
- for index in range(links.count()):
- with self.subTest(nav_link=index):
- link = links.nth(index)
- link.focus()
- self.assertTrue(link.evaluate("element => document.activeElement === element"))
-
- browser.close()
-
- def test_pip_page_remains_readable_without_javascript(self) -> None:
- browser = self.playwright.chromium.launch()
- context = browser.new_context(
- java_script_enabled=False,
- viewport={"width": 390, "height": 844},
- )
- page = context.new_page()
- response = page.goto(f"{self.base_url}/pip", wait_until="domcontentloaded")
- self.assertIsNotNone(response)
- self.assertEqual(response.status, 200)
- self.assertTrue(
- page.get_by_role(
- "heading", name=re.compile(r"A little help\.\s*A little banter\.")
- ).is_visible()
- )
- self.assertTrue(page.get_by_role("figure", name="Example conversation").is_visible())
- self.assertTrue(page.get_by_role("heading", name="Say hey to Pip.").is_visible())
- self.assertTrue(page.get_by_text("@PIPSOFTWARE_BOT", exact=True).is_visible())
- self.assertTrue(page.get_by_text("Opens Pip in Telegram.", exact=True).is_visible())
- self.assertGreaterEqual(
- page.get_by_role("link", name="Message Pip", exact=True).count(), 2
- )
- self.assertTrue(page.locator("#text-pip").is_visible())
- self.assertLessEqual(page.evaluate("document.documentElement.scrollWidth"), 390)
- context.close()
- browser.close()
class PondProductionTests(BrowserTestCase):
@@ -1271,177 +943,8 @@ def test_mobile_navigation_hero_viewport_and_system_color_modes(self) -> None:
self.assertNotEqual(light_signature, dark_signature)
- def test_homepage_pip_section_roundtrips_to_pip_and_back(self) -> None:
- browser = self.playwright.chromium.launch()
- try:
- for viewport_name, viewport in (
- ("desktop", {"width": 1440, "height": 900}),
- ("mobile", {"width": 390, "height": 844}),
- ):
- with self.subTest(viewport=viewport_name):
- context = browser.new_context(viewport=viewport)
- page = self.pond_page(context)
- console_issues = []
- page.on(
- "console",
- lambda message: record_console_issue(console_issues, message),
- )
- page.goto(
- f"{self.base_url}/?pond-seed=e2e-homepage-pip",
- wait_until="networkidle",
- )
-
- section = page.locator("#pip")
- self.assertTrue(section.is_visible())
- self.assertTrue(
- page.evaluate(
- """() => {
- const hero = document.querySelector('section[aria-labelledby="hero-title"]');
- const pip = document.querySelector('section#pip');
- const work = document.querySelector('section#work');
- return hero?.nextElementSibling === pip && pip?.nextElementSibling === work;
- }"""
- )
- )
- self.assertTrue(
- section.get_by_role(
- "heading",
- name=re.compile(r"A little help\.\s*A little banter\."),
- ).is_visible()
- )
- self.assertTrue(
- section.get_by_text(
- "Meet Pip, your AI mate on Telegram. Quick questions, useful reminders, and a little everyday company.",
- exact=True,
- ).is_visible()
- )
-
- meet_pip = section.get_by_role("link", name="Meet Pip", exact=True)
- self.assertTrue(meet_pip.is_visible())
- self.assertEqual(meet_pip.get_attribute("href"), "/pip")
- for theme in ("dark", "light"):
- page.emulate_media(color_scheme=theme)
- page.wait_for_timeout(40)
- self.assertGreaterEqual(control_contrast(meet_pip), 4.5)
- meet_bounds = meet_pip.bounding_box()
- self.assertIsNotNone(meet_bounds)
- self.assertGreaterEqual(meet_bounds["width"], 44)
- self.assertGreaterEqual(meet_bounds["height"], 44)
-
- section.scroll_into_view_if_needed()
- page.wait_for_timeout(120)
- page.screenshot(
- path=SCREENSHOTS / f"homepage-pip-{viewport['width']}.png",
- full_page=False,
- )
-
- if viewport["width"] > 820:
- navigation = page.locator("header nav a")
- self.assertEqual(navigation.count(), 4)
- for index in range(navigation.count()):
- link = navigation.nth(index)
- self.assertTrue(link.is_visible())
- bounds = link.bounding_box()
- self.assertIsNotNone(bounds)
- self.assertGreaterEqual(bounds["width"], 44)
- self.assertGreaterEqual(bounds["height"], 44)
-
- meet_pip.click()
- page.wait_for_url(re.compile(rf"^{re.escape(self.base_url)}/pip$"))
- self.assertEqual(urlparse(page.url).path, "/pip")
-
- home_link = page.locator("header a[href='/']")
- self.assertEqual(home_link.count(), 1)
- self.assertTrue(home_link.is_visible())
- home_bounds = home_link.bounding_box()
- self.assertIsNotNone(home_bounds)
- self.assertGreaterEqual(home_bounds["width"], 44)
- self.assertGreaterEqual(home_bounds["height"], 44)
-
- home_link.click()
- page.wait_for_url(re.compile(rf"^{re.escape(self.base_url)}/$"))
- self.assertEqual(urlparse(page.url).path, "/")
- self.assertEqual(console_issues, [])
- context.close()
- finally:
- browser.close()
-
- def test_homepage_pip_link_works_without_javascript(self) -> None:
- browser = self.playwright.chromium.launch()
- context = browser.new_context(
- java_script_enabled=False,
- viewport={"width": 390, "height": 844},
- )
- page = self.pond_page(context)
- page.goto(f"{self.base_url}/", wait_until="domcontentloaded")
- navigation = page.get_by_role("navigation", name="Portfolio navigation")
- links = navigation.get_by_role("link")
- self.assertEqual(
- [
- (links.nth(index).inner_text().strip(), links.nth(index).get_attribute("href"))
- for index in range(links.count())
- ],
- list(PORTFOLIO_NAV_LINKS),
- )
- self.assertTrue(all(links.nth(index).is_visible() for index in range(links.count())))
-
- section = page.locator("#pip")
- self.assertTrue(section.is_visible())
- meet_pip = section.get_by_role("link", name="Meet Pip", exact=True)
- self.assertTrue(meet_pip.is_visible())
- self.assertEqual(meet_pip.get_attribute("href"), "/pip")
- meet_pip.click()
- page.wait_for_url(re.compile(rf"^{re.escape(self.base_url)}/pip$"))
- self.assertEqual(urlparse(page.url).path, "/pip")
- self.assertTrue(
- page.get_by_role(
- "heading", name=re.compile(r"A little help\.\s*A little banter\.")
- ).is_visible()
- )
- self.assertLessEqual(page.evaluate("document.documentElement.scrollWidth"), 390)
-
- context.close()
- browser.close()
-
- def test_homepage_pip_section_fits_320px_with_reduced_motion(self) -> None:
- browser = self.playwright.chromium.launch()
- context = browser.new_context(
- reduced_motion="reduce",
- viewport={"width": 320, "height": 568},
- )
- page = self.pond_page(context)
- page.goto(
- f"{self.base_url}/?pond-seed=e2e-homepage-pip-reduced",
- wait_until="networkidle",
- )
-
- section = page.locator("#pip")
- self.assertTrue(section.is_visible())
- self.assertTrue(
- section.get_by_role(
- "heading", name=re.compile(r"A little help\.\s*A little banter\.")
- ).is_visible()
- )
- self.assertLessEqual(page.evaluate("document.documentElement.scrollWidth"), 320)
- self.assertLessEqual(page.evaluate("document.body.scrollWidth"), 320)
-
- section.scroll_into_view_if_needed()
- page.wait_for_timeout(120)
- section_bounds = section.bounding_box()
- self.assertIsNotNone(section_bounds)
- self.assertGreaterEqual(section_bounds["x"], -1)
- self.assertLessEqual(section_bounds["x"] + section_bounds["width"], 321)
- meet_pip = section.get_by_role("link", name="Meet Pip", exact=True)
- self.assertTrue(meet_pip.is_visible())
- meet_bounds = meet_pip.bounding_box()
- self.assertIsNotNone(meet_bounds)
- self.assertGreaterEqual(meet_bounds["width"], 44)
- self.assertGreaterEqual(meet_bounds["height"], 44)
- page.screenshot(path=SCREENSHOTS / "homepage-pip-320.png", full_page=False)
- context.close()
- browser.close()
def test_homepage_preserves_layout_and_world_state_across_viewport_matrix(self) -> None:
viewports = (
@@ -1686,12 +1189,12 @@ def test_navigation_and_bordered_controls_keep_their_visual_hierarchy(self) -> N
self.assertEqual(page.get_by_text("Building at Orchid.ai", exact=True).count(), 0)
navigation = page.locator("header nav a")
- self.assertEqual(navigation.count(), 4)
+ self.assertEqual(navigation.count(), 3)
self.assertEqual(
navigation.evaluate_all(
"elements => elements.map(element => [element.textContent.trim(), element.getAttribute('href')])"
),
- [["Work", "#work"], ["Pip", "/pip"], ["About", "#about"], ["Contact", "#contact"]],
+ [["Work", "#work"], ["About", "#about"], ["Contact", "#contact"]],
)
self.assertEqual(page.locator("header [class*='profileTilt']").count(), 1)
self.assertEqual(page.locator("#about [class*='profileTilt']").count(), 0)
@@ -2495,12 +1998,19 @@ def test_removed_routes_use_the_pond_not_found_page(self) -> None:
browser.close()
+
+
class SkulletImageTests(BrowserTestCase):
- HERO_ALT = "Pixel art of an open notebook, a cup of tea, oranges, and a leafy plant on a desk beside a lake view."
+ HERO_ALT = "Lu's illustrated avatar wearing a pink cap"
+ AVATAR_SKELETON = 'span:has(> img[alt="' + HERO_ALT + '"]) [skullet-loading]'
+ AVATAR_STATUS = 'span:has(> img[alt="' + HERO_ALT + '"]) [skullet-status]'
SKULLET_WATCH = """
window.__skulletShown = false;
const markSkullet = () => {
- if (document.querySelector("[skullet-loading]")) window.__skulletShown = true;
+ const image = document.querySelector('img[alt="' + __HERO_ALT__ + '"]');
+ if (image && image.parentElement.querySelector("[skullet-loading]")) {
+ window.__skulletShown = true;
+ }
};
const armSkulletWatch = () => {
if (window.__skulletArmed || !document.documentElement) return;
@@ -2522,7 +2032,8 @@ class SkulletImageTests(BrowserTestCase):
if (window.__boneArmed || !document.documentElement) return;
window.__boneArmed = true;
new MutationObserver(() => {
- const host = document.querySelector("[skullet-loading]");
+ const image = document.querySelector('img[alt="' + __HERO_ALT__ + '"]');
+ const host = image && image.parentElement.querySelector("[skullet-loading]");
if (!host) return;
if (!window.__boneHost) window.__boneHost = host;
else if (host !== window.__boneHost) window.__boneReplaced = true;
@@ -2551,14 +2062,18 @@ def release(self, held):
def test_delayed_image_shows_a_skeleton_then_clears_it(self) -> None:
browser = self.playwright.chromium.launch()
page = browser.new_page(viewport={"width": 1440, "height": 900})
- page.add_init_script(self.SKULLET_WATCH)
- page.add_init_script(self.BONE_STAYS)
+ page.add_init_script(
+ self.SKULLET_WATCH.replace("__HERO_ALT__", json.dumps(self.HERO_ALT))
+ )
+ page.add_init_script(
+ self.BONE_STAYS.replace("__HERO_ALT__", json.dumps(self.HERO_ALT))
+ )
license_text = page.request.get(f"{self.base_url}/third-party-licenses.txt").text()
self.assertIn("Copyright (c) 2025 Lochie Axon", license_text)
self.assertIn("Permission is hereby granted, free of charge", license_text)
- held = self.hold_matching(page, "pip-everyday.webp")
- page.goto(f"{self.base_url}/pip", wait_until="domcontentloaded")
- page.wait_for_selector("[skullet-loading]")
+ held = self.hold_matching(page, "lu-avatar.jpg")
+ page.goto(f"{self.base_url}/", wait_until="domcontentloaded")
+ page.wait_for_selector(self.AVATAR_SKELETON)
self.assertIs(page.evaluate("window.__skulletShown"), True)
image = page.get_by_alt_text(self.HERO_ALT)
self.assertEqual(image.get_attribute("aria-hidden"), "true")
@@ -2571,33 +2086,35 @@ def test_delayed_image_shows_a_skeleton_then_clears_it(self) -> None:
const box = bar ? bar.getBoundingClientRect() : null;
return !!(host && host.isConnected && box && box.width > 8 && box.height > 8);
}"""))
- self.assertIn("Loading illustration", page.locator("[skullet-status]").inner_text())
+ self.assertIn("Loading portrait", page.locator(self.AVATAR_STATUS).inner_text())
self.release(held)
- page.wait_for_selector("[skullet-loading]", state="detached")
+ page.wait_for_selector(self.AVATAR_SKELETON, state="detached")
self.assertIsNone(image.get_attribute("aria-hidden"))
self.assertGreater(image.evaluate("node => node.naturalWidth"), 0)
browser.close()
- def test_image_error_clears_the_skeleton_and_shows_the_qr_fallback(self) -> None:
+
+ def test_image_error_clears_the_skeleton(self) -> None:
browser = self.playwright.chromium.launch()
- page = browser.new_page(viewport={"width": 390, "height": 844})
- held = self.hold_matching(page, "/qr/")
- page.goto(f"{self.base_url}/pip", wait_until="networkidle")
- page.locator("#text-pip").get_by_role("button", name="Show QR code", exact=True).click()
- page.wait_for_selector("#pip-qr [skullet-loading]")
+ self.addCleanup(browser.close)
+ page = browser.new_page(viewport={"width": 1440, "height": 900})
+ held = self.hold_matching(page, "lu-avatar.jpg")
+ page.goto(f"{self.base_url}/", wait_until="domcontentloaded")
+ page.wait_for_selector(self.AVATAR_SKELETON)
+ image = page.get_by_alt_text(self.HERO_ALT)
+ self.assertEqual(image.get_attribute("aria-hidden"), "true")
for route in held:
route.abort()
- fallback = page.get_by_text(re.compile(r"QR code.*couldn.t load", re.IGNORECASE))
- fallback.wait_for(state="visible")
- self.assertTrue(fallback.is_visible())
- self.assertEqual(page.locator("#pip-qr [skullet-loading]").count(), 0)
- self.assertEqual(page.locator("#pip-qr img").count(), 0)
- browser.close()
+ page.wait_for_selector(self.AVATAR_SKELETON, state="detached")
+ self.assertIsNone(image.get_attribute("aria-hidden"))
+ self.assertEqual(image.evaluate("node => node.naturalWidth"), 0)
def test_complete_image_never_shows_a_skeleton(self) -> None:
browser = self.playwright.chromium.launch()
page = browser.new_page(viewport={"width": 1440, "height": 900})
- page.add_init_script(self.SKULLET_WATCH)
+ page.add_init_script(
+ self.SKULLET_WATCH.replace("__HERO_ALT__", json.dumps(self.HERO_ALT))
+ )
held_scripts = []
def hold_scripts(route):
@@ -2607,7 +2124,7 @@ def hold_scripts(route):
route.continue_()
page.route("**/*", hold_scripts)
- page.goto(f"{self.base_url}/pip", wait_until="commit")
+ page.goto(f"{self.base_url}/", wait_until="commit")
page.wait_for_function(
"""(alt) => {
const image = document.querySelector(`img[alt="${alt}"]`);
@@ -2617,13 +2134,14 @@ def hold_scripts(route):
)
for route in held_scripts:
route.continue_()
+ page.unroute("**/*", hold_scripts)
page.wait_for_load_state("networkidle")
page.wait_for_timeout(250)
image = page.get_by_alt_text(self.HERO_ALT)
self.assertGreater(image.evaluate("node => node.naturalWidth"), 0)
self.assertIsNone(image.get_attribute("aria-hidden"))
self.assertIs(page.evaluate("window.__skulletShown"), False)
- self.assertEqual(page.locator("[skullet-loading]").count(), 0)
+ self.assertEqual(page.locator(self.AVATAR_SKELETON).count(), 0)
browser.close()
def test_failed_skullet_chunk_still_shows_the_image(self) -> None:
@@ -2640,16 +2158,16 @@ def handler(route):
aborted_chunks.append(url)
route.abort()
return
- if "pip-everyday.webp" in url:
+ if "lu-avatar.jpg" in url:
held.append(route)
return
route.continue_()
page.route("**/*", handler)
- page.goto(f"{self.base_url}/pip", wait_until="domcontentloaded")
+ page.goto(f"{self.base_url}/", wait_until="domcontentloaded")
page.wait_for_timeout(500)
self.assertGreaterEqual(len(aborted_chunks), 1)
- self.assertTrue(page.get_by_role("heading", name="A little help.").is_visible())
+ self.assertTrue(page.get_by_role("heading", name="I make stubborn software behave.").is_visible())
self.release(held)
image = page.get_by_alt_text(self.HERO_ALT)
image.wait_for(state="visible")
@@ -2657,133 +2175,3 @@ def handler(route):
self.assertIsNone(image.get_attribute("aria-hidden"))
self.assertEqual(page_errors, [])
browser.close()
-
- def test_qr_bone_is_visible_on_the_first_paint(self) -> None:
- browser = self.playwright.chromium.launch()
- page = browser.new_page(viewport={"width": 390, "height": 844})
- held = self.hold_matching(page, "/qr/")
- page.goto(f"{self.base_url}/pip", wait_until="networkidle")
- page.evaluate(
- """() => {
- window.__qrFirst = null;
- const root = document.getElementById("pip-qr");
- const sample = () => {
- if (!root || root.hidden || window.__qrFirst) return;
- const layer = root.querySelector("[skullet-layer]");
- const bar = layer && layer.shadowRoot ? layer.shadowRoot.querySelector("[skullet-rect]") : null;
- const box = bar ? bar.getBoundingClientRect() : null;
- const image = root.querySelector("img");
- window.__qrFirst = {
- bone: !!(box && box.width > 8 && box.height > 8),
- width: box ? box.width : 0,
- height: box ? box.height : 0,
- opacity: image ? getComputedStyle(image).opacity : null,
- background: image ? getComputedStyle(image).backgroundColor : null,
- };
- };
- new MutationObserver(() => {
- if (root && !root.hidden) requestAnimationFrame(sample);
- }).observe(root, { attributes: true, childList: true, subtree: true });
- }"""
- )
- page.locator("#text-pip").get_by_role("button", name="Show QR code", exact=True).click()
- page.wait_for_function("() => window.__qrFirst && window.__qrFirst.bone")
- first = page.evaluate("window.__qrFirst")
- self.assertTrue(first["bone"])
- self.assertEqual(first["opacity"], "0")
- self.assertIn("0, 0, 0, 0", first["background"])
- self.assertAlmostEqual(first["width"], first["height"], delta=1)
- self.release(held)
- page.wait_for_function(
- """() => {
- const image = document.querySelector("#pip-qr img");
- return image && image.complete && image.naturalWidth > 0 && image.getAttribute("aria-hidden") === null;
- }"""
- )
- browser.close()
-
- def test_stalled_skullet_chunk_still_opens_the_qr_panel(self) -> None:
- browser = self.playwright.chromium.launch()
- page = browser.new_page(viewport={"width": 390, "height": 844})
- page.goto(f"{self.base_url}/pip", wait_until="networkidle")
- held_chunks = []
-
- def hold_chunk(route):
- url = route.request.url
- if "skullet-overlay" in url and url.endswith(".js"):
- held_chunks.append(route)
- return
- route.continue_()
-
- page.route("**/*", hold_chunk)
- started = time.monotonic()
- page.locator("#text-pip").get_by_role("button", name="Show QR code", exact=True).click()
- page.wait_for_function(
- """() => {
- const button = document.querySelector("#text-pip button[aria-controls='pip-qr']");
- const panel = document.getElementById("pip-qr");
- return button && button.getAttribute("aria-expanded") === "true" && panel && !panel.hidden;
- }""",
- timeout=300,
- )
- self.assertLess(time.monotonic() - started, 0.3)
- self.assertEqual(page.locator("#text-pip button[aria-controls='pip-qr']").get_attribute("aria-expanded"), "true")
- self.assertFalse(page.locator("#pip-qr").is_hidden())
- self.assertGreaterEqual(len(held_chunks), 1)
- browser.close()
-
- def test_stale_qr_overlay_does_not_reopen_the_panel(self) -> None:
- browser = self.playwright.chromium.launch()
- page = browser.new_page(viewport={"width": 390, "height": 844})
- page.goto(f"{self.base_url}/pip", wait_until="networkidle")
- held_chunks = []
-
- def hold_chunk(route):
- url = route.request.url
- if "skullet-overlay" in url and url.endswith(".js"):
- held_chunks.append(route)
- return
- route.continue_()
-
- page.route("**/*", hold_chunk)
- final = page.evaluate(
- """() => new Promise((resolve) => {
- const button = document.querySelector("#text-pip button[aria-controls='pip-qr']");
- const panel = document.getElementById("pip-qr");
- let hid = false;
- let secondWasShow = false;
- const finish = () => resolve({
- expanded: button.getAttribute("aria-expanded"),
- hidden: panel.hidden,
- secondWasShow,
- });
- const observer = new MutationObserver(() => {
- if (hid || button.getAttribute("aria-expanded") !== "true") return;
- hid = true;
- observer.disconnect();
- button.click();
- window.setTimeout(finish, 250);
- });
- observer.observe(button, { attributes: true, attributeFilter: ["aria-expanded"] });
- button.click();
- window.setTimeout(() => {
- secondWasShow = (button.textContent || "").includes("Show QR code");
- if (!secondWasShow) {
- observer.disconnect();
- finish();
- return;
- }
- button.click();
- }, 130);
- window.setTimeout(() => {
- if (hid) return;
- observer.disconnect();
- finish();
- }, 1000);
- })"""
- )
- self.assertTrue(final["secondWasShow"])
- self.assertEqual(final["expanded"], "false")
- self.assertTrue(final["hidden"])
- self.assertGreaterEqual(len(held_chunks), 1)
- browser.close()
diff --git a/tests/pip-contact.test.mts b/tests/pip-contact.test.mts
deleted file mode 100644
index 9a860ce..0000000
--- a/tests/pip-contact.test.mts
+++ /dev/null
@@ -1,43 +0,0 @@
-import assert from "node:assert/strict";
-import test from "node:test";
-
-import { getPipContact, PIP_TELEGRAM_HANDLE } from "../lib/pip.ts";
-
-test("normalizes an optional @ prefix and returns Pip's Telegram target", () => {
- const contact = getPipContact(" PIPSOFTWARE_BOT ");
- assert.ok(contact);
- assert.equal(contact.handle, "@PIPSOFTWARE_BOT");
- assert.equal(contact.telegramUrl, "https://t.me/PIPSOFTWARE_BOT");
- assert.equal(contact.qrUrl, "/qr/PIPSOFTWARE_BOT.svg");
-
- const alreadyPrefixed = getPipContact("@PIPSOFTWARE_BOT");
- assert.deepEqual(alreadyPrefixed, contact);
-});
-
-test("keeps QR images on the site and keyed to the configured handle", () => {
- for (const username of ["PIPSOFTWARE_BOT", "another_bot"]) {
- const contact = getPipContact(username);
- assert.ok(contact);
- const qr = new URL(contact.qrUrl, "https://luinbytes.dev");
- assert.equal(qr.origin, "https://luinbytes.dev");
- assert.equal(qr.pathname, `/qr/${username}.svg`);
- assert.equal(contact.telegramUrl, `https://t.me/${username}`);
- }
-});
-
-test("returns null for blank and invalid Telegram handles", () => {
- for (const candidate of [
- "",
- " ",
- "@",
- "@bad handle",
- "bad-handle",
- "https://t.me/PIPSOFTWARE_BOT",
- "PIPSOFTWARE_BOT/other",
- ]) {
- assert.equal(getPipContact(candidate), null, `expected ${candidate} to be rejected`);
- }
-
- assert.notEqual(PIP_TELEGRAM_HANDLE, "");
- assert.ok(getPipContact(PIP_TELEGRAM_HANDLE));
-});
diff --git a/vite.config.ts b/vite.config.ts
index ca865ea..77c2be3 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -43,6 +43,6 @@ export default defineConfig(({ isSsrBuild }) => ({
copyPublicDir: !isSsrBuild,
rolldownOptions: isSsrBuild
? { output: { entryFileNames: "entry-server.js" } }
- : { input: ["index.html", "pip.html", "privacy.html", "404.html"] },
+ : { input: ["index.html", "privacy.html", "404.html"] },
},
}));