feat(landing-purchase): Astro Landing/Purchase page + weownchat-design skill - #165
Conversation
There was a problem hiding this comment.
Pull request overview
Rebuilds the WeOwnChat Landing/Purchase page as a static Astro + Tailwind CSS v4 site (replacing the prior Next.js approach), while preserving the existing content/design and codifying the WeOwnChat design tokens as a reusable .claude skill.
Changes:
- Adds a new
landing-purchase/Astro project (static output) implementing the landing/purchase page with minimal vanilla-JS enhancements. - Implements progressive-enhancement scroll-reveal (
IntersectionObserver+ timeout fallback) and a demo “Live preview” iframe backed by seeded/mock data. - Updates repo
.gitignorerules (macOSIcon\rmatch + track.claude/skills/) and indexes the new project in/CHANGELOG.md.
Reviewed changes
Copilot reviewed 37 out of 39 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| landing-purchase/tsconfig.json | Adds Astro strict TS config for the new project. |
| landing-purchase/src/styles/global.css | Defines Tailwind v4 theme tokens and global styles (incl. reveal animations + reduced-motion handling). |
| landing-purchase/src/scripts/reveal.ts | Adds the page-wide scroll-reveal implementation with timeout fallback. |
| landing-purchase/src/pages/index.astro | Composes the landing page from Astro components. |
| landing-purchase/src/layouts/Layout.astro | Provides the base HTML layout and includes global CSS + reveal script. |
| landing-purchase/src/components/Container.astro | Shared layout container primitive. |
| landing-purchase/src/components/Button.astro | Shared button/link component with variants and external-link handling. |
| landing-purchase/src/components/BrandMark.astro | Implements the WeOwn “W” brand mark for nav/footer. |
| landing-purchase/src/components/Nav.astro | Sticky navigation with mobile menu toggle behavior. |
| landing-purchase/src/components/Hero.astro | Hero section with CTA and animated chat mock embed. |
| landing-purchase/src/components/AnimatedChatMock.astro | Vanilla-JS animated chat loop mock with reduced-motion behavior. |
| landing-purchase/src/components/TrustLabelStrip.astro | Renders the “trust-label” strip used near CTAs. |
| landing-purchase/src/components/ProductShowcase.astro | “Live preview” section embedding the demo dashboard iframe + tab toggle UI. |
| landing-purchase/src/components/HowItWorks.astro | “How it works” steps section. |
| landing-purchase/src/components/TwoTabExplainer.astro | Private/Public explainer section with iconography. |
| landing-purchase/src/components/ValuePillars.astro | Value pillars grid with icon components. |
| landing-purchase/src/components/Pricing.astro | Pricing section (intentionally no hard price) + CTAs. |
| landing-purchase/src/components/FAQ.astro | FAQ accordion using native <details>/<summary> with exclusive-open behavior. |
| landing-purchase/src/components/Footer.astro | Footer content, links, and repeated trust-label strip. |
| landing-purchase/src/components/icons/IconChat.astro | Adds inline SVG icon component. |
| landing-purchase/src/components/icons/IconCheck.astro | Adds inline SVG icon component. |
| landing-purchase/src/components/icons/IconDocument.astro | Adds inline SVG icon component. |
| landing-purchase/src/components/icons/IconGlobe.astro | Adds inline SVG icon component. |
| landing-purchase/src/components/icons/IconKey.astro | Adds inline SVG icon component. |
| landing-purchase/src/components/icons/IconServer.astro | Adds inline SVG icon component. |
| landing-purchase/src/components/icons/IconShield.astro | Adds inline SVG icon component. |
| landing-purchase/src/components/icons/IconUpload.astro | Adds inline SVG icon component. |
| landing-purchase/src/components/icons/IconUsers.astro | Adds inline SVG icon component. |
| landing-purchase/public/_demo/dashboard-preview.html | Adds demo dashboard HTML with fetch mocking to seed preview content. |
| landing-purchase/package.json | Adds Astro/Tailwind/TS dependencies and scripts. |
| landing-purchase/astro.config.mjs | Configures Astro for static output + Tailwind Vite plugin. |
| landing-purchase/README.md | Documents purpose, local run instructions, and design-token provenance. |
| landing-purchase/CHANGELOG.md | Adds project-specific changelog for the landing/purchase project. |
| landing-purchase/.gitignore | Adds local ignores for the new Astro project. |
| CHANGELOG.md | Indexes the new project changelog and documents the new landing/purchase project. |
| .gitignore | Fixes Icon ignore rule and adjusts .claude ignore to track .claude/skills/. |
| .claude/skills/weownchat-design/SKILL.md | Adds the WeOwnChat design-system skill and its rules/provenance. |
| .claude/skills/weownchat-design/references/tokens.md | Adds the copied design tokens reference (dashboard + Keycloak sources). |
Suppressed comments (1)
landing-purchase/src/components/ProductShowcase.astro:77
- Setting
iframe.contentWindow.location.hashcan be lost if a user clicks the tab before the iframe has loaded (the hash is applied toabout:blankand then overwritten whensrcloads). Persist the selected tab and apply it on iframeload, and/or updateiframe.srcwith the hash before load.
if (iframe.contentWindow) {
iframe.contentWindow.location.hash = `#${tab}`;
}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| <body class="antialiased"> | ||
| <slot /> | ||
| <script src="../scripts/reveal.ts"></script> | ||
| </body> |
| <svg viewBox="0 0 24 24" class="h-3 w-3 text-ok" fill="none" stroke="currentColor" stroke-width="2"> | ||
| <rect x="5" y="11" width="14" height="9" rx="2"></rect> | ||
| <path d="M8 11V8a4 4 0 0 1 8 0v3"></path> | ||
| </svg> | ||
| harborviewcpa.weown.dev/app | ||
| </div> |
| "/api/embed-domains": { domains: ["harborviewcpa.example.com", "www.harborviewcpa.example.com"], always: [] }, | ||
| "/api/snippet": { snippet: "<scr" + "ipt src=\"https://harborview.weown.dev/embed/widget.min.js\" data-tenant=\"harborview\"></scr" + "ipt>" }, | ||
| "/api/threads": { threads: [ |
…n skill Rebuild of the WeOwnChat Landing/Purchase page (PRD roadmap item 2) on Astro + Tailwind CSS v4 (static output), replacing an earlier Next.js version per direct direction to match the framework used elsewhere in the WeOwn ecosystem outside this repo — a repo-wide search here found no existing Astro usage to match conventions against, so this project's setup choices are its own. `npm run build` (astro check && astro build) is clean; 0 npm audit findings. Full page: nav (real working mobile menu, vanilla JS), hero (a live, looping recreation of the real chat UI — vanilla-JS state machine, not a static mockup), a "Live preview" section embedding the real, unmodified dashboard HTML in an iframe with seeded fictional demo content (public/_demo/dashboard-preview.html — see its own header comment for the maintenance contract), how-it-works, the two-tab (private/public) explainer, value pillars, pricing (no dollar figure — commercial terms still draft/unsigned per the PRD), FAQ (native <details name="faq- accordion">, zero JS, same collapsible pattern the real dashboard's own .doc-strip already uses), footer. Design tokens copied verbatim from the real product (dashboard :root + Keycloak weown login theme), not invented — codified as .claude/skills/weownchat-design/ so future frontend work has a source of truth to check against instead of drifting. Scroll-reveal is hardened, not just ported: content is visible by default and only opts into the hidden-then-reveal transition once the script runs, with a 2.5s timeout that force-reveals anything IntersectionObserver never fires for. Added after testing surfaced the observer sometimes not firing at all for an element plainly in the viewport, in the specific automated- browser environment used to test this — cause unconfirmed, fix holds regardless. Also fixes a real, pre-existing bug in the repo-root .gitignore: the classic macOS "Icon?" rule (meant to exclude the special Icon+CR folder-icon file) was found to silently swallow any file whose name starts with "Icon" and is longer than 5 characters — it ate this project's own icon components until caught. Replaced with an unambiguous bracket negation, verified to still catch the intended file and nothing else.
58dd61f to
b5bbbc2
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 39 changed files in this pull request and generated no new comments.
Suppressed comments (7)
landing-purchase/src/components/ProductShowcase.astro:27
- The Public tab button is visually stateful, but it doesn’t expose state to assistive tech. Adding
aria-pressed(or properrole="tab"semantics) makes the current selection discoverable to screen readers.
<button type="button" id="showcase-tab-public" class="rounded-full px-4 py-2 text-sm font-semibold text-text-faint transition-colors hover:text-text-mut">
landing-purchase/src/layouts/Layout.astro:25
src/scripts/reveal.tsis being loaded via a plain<script src="../scripts/reveal.ts">tag. Browsers can’t execute TypeScript syntax, and Astro/Vite won’t bundle the file this way, so this will 404 and/or throw a syntax error and the reveal behavior won’t run. UseAstro.resolve()(bundled) and load as a module instead.
landing-purchase/src/components/ProductShowcase.astro:46- This demo URL uses a
*.weown.devhostname. In a public repo, demo/fixture content should use RFC 2606 example domains to avoid leaking real or potentially-internal topology (even in UI-only strings). Consider switching this to anexample.com-style domain (and keep it consistent with the allowlist values in the demo).
harborviewcpa.weown.dev/app
landing-purchase/public/_demo/dashboard-preview.html:605
- This demo embed snippet hardcodes a
*.weown.devdomain. Since this repository is public, demo/fixture URLs should use RFC 2606 example domains (e.g.,example.com) unless the intent is explicitly to publish a production endpoint.
"/api/snippet": { snippet: "<scr" + "ipt src=\"https://harborview.weown.dev/embed/widget.min.js\" data-tenant=\"harborview\"></scr" + "ipt>" },
landing-purchase/src/components/ProductShowcase.astro:24
- The Private tab button is visually stateful, but it doesn’t expose state to assistive tech. Adding
aria-pressed(or properrole="tab"semantics) makes the current selection discoverable to screen readers.
This issue also appears on line 27 of the same file.
<button type="button" id="showcase-tab-private" class="rounded-full bg-ok/[0.16] px-4 py-2 text-sm font-semibold text-ok transition-colors">
landing-purchase/src/components/ProductShowcase.astro:78
setTab()updates styles but doesn’t update any ARIA state, so assistive tech can’t tell which tab is active after interaction. If you addaria-pressedin the markup, also toggle it here when switching tabs.
function setTab(tab: "private" | "public") {
const isPrivate = tab === "private";
privateTab.className = `rounded-full px-4 py-2 text-sm font-semibold transition-colors ${
isPrivate ? "bg-ok/[0.16] text-ok" : "text-text-faint hover:text-text-mut"
}`;
landing-purchase/src/components/Button.astro:31
href.startsWith("http")will treat any string beginning withhttpas external, and will not mark other external schemes (e.g.mailto:) as external. Restrict this tohttp:///https://to avoid misclassification.
const isExternal = href.startsWith("http");
…ion (fix MD024 duplicate heading)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 39 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
landing-purchase/src/components/ProductShowcase.astro:78
- The Private/Public toggle updates styles but doesn’t expose the selected state to assistive tech. Adding/updating
aria-pressed(or tab semantics) makes the toggle state discoverable for screen readers and other AT.
const iframe = document.getElementById("showcase-iframe") as HTMLIFrameElement;
const privateTab = document.getElementById("showcase-tab-private")!;
const publicTab = document.getElementById("showcase-tab-public")!;
landing-purchase/src/components/ProductShowcase.astro:46
- This demo UI hardcodes a
*.weown.devhostname (harborviewcpa.weown.dev) even though the rest of the demo data already usesexample.com. For a public repo (and to avoid implying a real tenant URL pattern), prefer RFC 2606 example domains for all demo hostnames (see §3.0 public-repo precautions).
harborviewcpa.weown.dev/app
landing-purchase/public/_demo/dashboard-preview.html:605
- The demo snippet includes a hardcoded
harborview.weown.devscript URL. Since this is demo content in a public repo, use an RFC 2606 example domain so it can’t be mistaken for a real endpoint or internal topology (see §3.0 public-repo precautions).
"/api/snippet": { snippet: "<scr" + "ipt src=\"https://harborview.weown.dev/embed/widget.min.js\" data-tenant=\"harborview\"></scr" + "ipt>" },
| </head> | ||
| <body class="antialiased"> | ||
| <slot /> | ||
| <script src="../scripts/reveal.ts"></script> |
…ng-purchase-astro
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 39 changed files in this pull request and generated no new comments.
Suppressed comments (3)
landing-purchase/src/components/ProductShowcase.astro:46
- This uses a
*.weown.devhostname in a demo UI. Since this repo is public, it’s safer to use an RFC 2606 example domain for fictional/seeded content to avoid accidentally referencing a real internal/customer subdomain.
harborviewcpa.weown.dev/app
landing-purchase/public/_demo/dashboard-preview.html:605
- The demo snippet text includes
https://harborview.weown.dev/.... In a public repo, prefer RFC 2606 example domains for seeded/demo hostnames to avoid leaking or implying real internal/customer infrastructure naming.
"/api/snippet": { snippet: "<scr" + "ipt src=\"https://harborview.weown.dev/embed/widget.min.js\" data-tenant=\"harborview\"></scr" + "ipt>" },
landing-purchase/src/styles/global.css:134
- The reduced-motion fallback doesn’t override the more-specific
[data-reveal].reveal-armedrule, so users withprefers-reduced-motion: reducecan still have reveal-armed sections start atopacity: 0until JS marks them revealed (or the timeout fires). Override thereveal-armedselector inside the media query (and ideally disable transitions) so reduced-motion users never see hidden content.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 39 changed files in this pull request and generated no new comments.
Suppressed comments (4)
landing-purchase/src/layouts/Layout.astro:25
- The page loads a TypeScript file via a plain
<script src="../scripts/reveal.ts">, which will try to fetch../scripts/reveal.tsat runtime (not emitted indist/) and won’t be treated as an ES module. This will likely break scroll-reveal in the static build.
landing-purchase/src/components/ProductShowcase.astro:46 - This visible URL uses a real-looking
*.weown.devhostname. Because this repo is public, demo/placeholder domains should use RFC 2606 example domains to avoid leaking internal topology or implying a real tenant host.
harborviewcpa.weown.dev/app
landing-purchase/public/_demo/dashboard-preview.html:605
- The demo embed snippet hardcodes
https://harborview.weown.dev/.... Since this repo is public, avoid non-exampleweown.devdemo hostnames in committed fixtures; use an RFC 2606 example domain for the placeholder snippet URL.
"/api/snippet": { snippet: "<scr" + "ipt src=\"https://harborview.weown.dev/embed/widget.min.js\" data-tenant=\"harborview\"></scr" + "ipt>" },
landing-purchase/src/components/ProductShowcase.astro:78
bg-warn/[0.16]only appears inside this JS template string, so Tailwind may not generate the CSS for it (result: the Public tab can lose its active styling in the built output). Also, the buttons don’t expose their selected state to assistive tech. Consider using an existing opacity already present in templates and setaria-pressedon toggle.
function setTab(tab: "private" | "public") {
const isPrivate = tab === "private";
privateTab.className = `rounded-full px-4 py-2 text-sm font-semibold transition-colors ${
isPrivate ? "bg-ok/[0.16] text-ok" : "text-text-faint hover:text-text-mut"
}`;
Summary
Rebuild of the WeOwnChat Landing/Purchase page (PRD roadmap item 2) on Astro + Tailwind CSS v4 (static output), replacing #163 (closed) which was built on Next.js. Same page, same content, same design — different, and leaner, foundation.
Why the switch: direct instruction to match the framework used elsewhere in the WeOwn ecosystem. Worth being upfront: a repo-wide search across this specific repository found no existing Astro usage anywhere to match conventions against (no
astro.config, no.astrofiles, nothing in anypackage.json), so the setup below reflects this project's own reasonable defaults, not an established in-repo pattern.What's the same as #163
All content, copy, and design carry over unchanged: WeOwnChat branding (not "WeOwn AI" — see #164), the real dashboard's color/type/radius tokens via
.claude/skills/weownchat-design/, the "Live preview" section embedding the actual dashboard HTML in an iframe with seeded demo content, the two-tab private/public explainer, pricing with no dollar figure, the FAQ content, everything.What's different
<details name="faq-accordion">gives the exact same "only one open at a time" behavior the ReactuseStateversion had, natively. This is incidentally the same collapsible pattern the real dashboard's own.doc-stripCSS already uses.npm audit: 0 findings (was 3 high-severity, nested inside Next.js's ownpostcss/sharp). Not the reason for the rewrite, but a real side benefit of the newer, smaller dependency tree.[data-reveal]content atopacity:0unconditionally, fully dependent on React +IntersectionObserverto ever make it visible. Porting it surfaced a real gap worth fixing regardless of the rewrite:IntersectionObserverwas observed, during testing, to sometimes never fire its callback at all — even for an element plainly sitting in the viewport — in the specific automated browser environment used here. Cause unconfirmed (real browsers are presumably fine; this is an extremely well-supported API), but "unconfirmed" isn't "ruled out," so it's fixed properly: elements are visible by default and only opt into the hidden-then-reveal transition once the script actually runs (.reveal-armed), and a 2.5s timeout force-reveals anything the observer never fires for. Nothing on the page can end up permanently invisible because one animation mechanism didn't fire.A real, pre-existing bug found and fixed along the way
The repo-root
.gitignore'sIcon?rule (the classic macOS "ignore the special custom-folder-icon file" entry) turned out to silently match any file starting with "Icon" longer than 5 characters in this specific file's context — not just the intended 5-byte special file. It ate this project's own icon components (IconChat.astroetc.) until the build showed them missing and I traced it back. Reproduced in an isolated sandbox against the real file to confirm before touching it. Fixed with an unambiguous bracket-negation (Icon[^a-zA-Z0-9]), verified to still catch the actual macOS special file (Icon+ a literal CR byte) while no longer matching normal source filenames. Full repro/reasoning is in the commit message.Verification
astro check && astro build— 0 errors/warnings/hints, clean static generation.details.openstate,aria-expanded), not just visual screenshots.opacity:1either via genuine intersection or the timeout fallback, tested both paths directly.Open follow-ups (tracked in
landing-purchase/CHANGELOG.md)billing.weown.dev.