ReviewOS

also looking at this

loghqorg/loghq

Promote main to production: signed-out chrome and the subscriber tables

#17
Merged glennmichael123 wants to merge main into production
54 files +4252 -1353

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

resources/partials/Breadcrumbs.stxmodified+25-46
Changes to resources/partials/Breadcrumbs.stx
@@ -1,50 +1,29 @@
1{{-- Breadcrumb trail. Reads a `crumbs` array from the including page's server
2 block — @include does not accept inline data in this version (an
3 `['crumbs' => …]` argument leaves the variable undefined and the loop
4 reports "crumbs is not iterable"), and the page needs the array declared
5 anyway to drive its @structuredData BreadcrumbList from the same source.
1{{-- Breadcrumb trail, on the shared <Breadcrumb>.
62
7 Each crumb is { label, href?, url? }. The last has no href: it is the
8 current page.
3 Reads a `crumbs` array from the including page's server block. @include does
4 not accept inline data in this version (an `['crumbs' => …]` argument leaves
5 the variable undefined and the loop reports "crumbs is not iterable"), and
6 the page needs the array declared anyway to drive its @structuredData
7 BreadcrumbList from the same source. Each crumb is { label, href?, url? };
8 the last has no href because it is the current page. That is the component's
9 `items` shape, and the extra `url` the JSON-LD needs is ignored by it.
910
10 Still hand-written rather than the shipped <Breadcrumb>, but no longer
11 because the component is broken. It used to be: the component loops with
12 @foreach(item in displayItems), the parser accepted only the ` as ` form,
13 and the directive text shipped literally on all 22 pages.
11 Hand-written until now for a reason that is gone: the component loops with
12 @foreach(item in displayItems), the parser accepted only the ` as ` form, and
13 the directive text shipped literally on all 22 pages. Fixed as of 0.2.395.
1414
15 That is fixed as of stx 0.2.395, checked by rendering rather than by
16 reading: `<Breadcrumb :items="…" />` emits a real
17 <nav aria-label="Breadcrumb"><ol>, the separator inside each <li> and
18 hidden from assistive tech, aria-current="page" on the last item, and
19 data-stx-link on the anchors so the SPA router still intercepts them.
20 That is the same markup this file writes by hand. analyticshq's own
21 partial already calls the component this way.
15 `shell subnav` stays, so the container is unchanged: public/marketing.css
16 owns the width, the 28px of top padding and the flex row, and all 22 pages
17 that include this extend the marketing layout, so that file is always
18 linked. The component's own utilities land on top and resolve to the same
19 values here - its links are `fg-muted` (--text-2, what `.subnav a` gave) and
20 its separators `fg-soft` (--text-3, what `.sep` gave).
2221
23 What is left is a styling choice rather than a constraint: the component
24 brings its own utilities (flex, gap-2, text-sm, text-fg-muted) where this
25 trail is dressed by `shell subnav`. Worth doing deliberately, not as a
26 side effect of editing this file.
27
28 Ordered list, not a row of anchors: this is a sequence, the position is
29 meaningful, and assistive tech should say so. The separators are decorative
30 and hidden from it; the current page is marked aria-current.
31
32 StxLink rather than a bare anchor. It compiles to `<a href data-stx-link>`,
33 and the SPA router only intercepts elements carrying that attribute — a
34 plain `<a href="/projects">` does a native full page reload instead. This
35 was the last internal link in the app still doing that. Where a full load
36 IS wanted (the OAuth redirects in login/register), the anchor stays plain
37 and says so with data-no-router. --}}
38<nav class="shell subnav" aria-label="Breadcrumb">
39 <ol>
40 @foreach (crumbs as c)
41 <li>
42 @if ($loop.last)
43 <span aria-current="page">{{ c.label }}</span>
44 @else
45 <StxLink to="">{{ c.label }}</StxLink><span class="sep" aria-hidden="true">/</span>
46 @endif
47 </li>
48 @endforeach
49 </ol>
50</nav>
22 One thing does change, and it is the reason this was left for a deliberate
23 pass. The current crumb was a bare <span> with no class, so it inherited
24 `.subnav`'s own `color: var(--text-3)` and rendered QUIETER than the links
25 beside it - the one item telling you where you are was the faintest thing in
26 the row. That reads as an accident of inheritance rather than a choice. The
27 component gives it `text-fg font-medium`, so it is now the most prominent
28 crumb, which is what a trail is for. --}}
29<Breadcrumb className="shell subnav" :items="crumbs" />
tests/unit/breadcrumb-trail.test.tsadded+86-0
Changes to tests/unit/breadcrumb-trail.test.ts
@@ -0,0 +1,86 @@
1/**
2 * The marketing breadcrumb trail.
3 *
4 * `resources/partials/Breadcrumbs.stx` is one <Breadcrumb> call now, included
5 * by 22 marketing pages. Two things make this worth pinning rather than
6 * trusting:
7 *
8 * 1. An unresolved component renders an HTML COMMENT, not an error. So a
9 * partial that silently stops producing a trail passes every "no error"
10 * assertion while 22 pages ship no breadcrumbs at all. The harness resolves
11 * the component explicitly and fails on "could not be resolved".
12 * 2. The trail is server-rendered. `:items` is visible to the server for this
13 * component, which is the whole point on pages whose job is to be crawled:
14 * the markup a crawler and a no-JS visitor get already has the trail, and
15 * the anchors carry data-stx-link so the SPA router still intercepts them.
16 */
17import { describe, expect, test } from 'bun:test'
18import { readFileSync } from 'node:fs'
19import { resolve } from 'node:path'
20import { processDirectives } from '../../node_modules/@stacksjs/stx/dist/process.js'
21
22const root = resolve(import.meta.dir, '../..')
23const breadcrumbPath = resolve(root, 'node_modules/@stacksjs/components/src/ui/breadcrumb/Breadcrumb.stx')
24const partialPath = resolve(root, 'resources/partials/Breadcrumbs.stx')
25
26const crumbs = [
27 { label: 'Home', href: '/', url: 'https://loghq.org/' },
28 { label: 'Features', href: '/features', url: 'https://loghq.org/features' },
29 { label: 'Capture' },
30]
31
32async function renderTrail() {
33 const template = readFileSync(partialPath, 'utf8')
34 const dependencies = new Set<string>()
35 const rendered = await processDirectives(template, {
36 crumbs,
37 __importedComponents: new Map([['breadcrumb', breadcrumbPath]]),
38 }, partialPath, {
39 componentsDir: resolve(root, 'resources/components'),
40 partialsDir: resolve(root, 'resources/partials'),
41 }, dependencies)
42
43 expect(rendered).not.toContain('[Component Error')
44 expect(rendered).not.toContain('[Foreach Error')
45 expect(rendered).not.toContain('<Breadcrumb')
46 expect(rendered).not.toMatch(/could not be resolved/)
47 return { html: rendered, dependencies }
48}
49
50describe('marketing breadcrumb trail', () => {
51 test('renders the shared component, server side, with the whole trail', async () => {
52 const { html, dependencies } = await renderTrail()
53 expect([...dependencies]).toContain(breadcrumbPath)
54 expect(html).toContain('aria-label="Breadcrumb"')
55 expect((html.match(/<li\b/g) ?? []).length).toBe(3)
56 for (const label of ['Home', 'Features', 'Capture'])
57 expect(html).toContain(label)
58 expect(html).toContain('href="/"')
59 expect(html).toContain('href="/features"')
60 })
61
62 test('the deepest crumb is the current page and is not a link', async () => {
63 const { html } = await renderTrail()
64 expect(html).toMatch(/aria-current="page">\s*Capture\s*<\/span>/)
65 // `url` is only for the page's own BreadcrumbList JSON-LD; the component
66 // reads `href`, so an absolute URL must never become a crumb link.
67 expect(html).not.toContain('https://loghq.org')
68 })
69
70 test('the container classes this app owns survive onto the nav', () => {
71 /*
72 * public/marketing.css owns the width (`shell`), the 28px of top padding
73 * and the flex row (`subnav`), and all 22 pages that include this partial
74 * extend the marketing layout, so that file is always linked. className is
75 * the only way those reach the component's nav.
76 */
77 const source = readFileSync(partialPath, 'utf8')
78 expect(source).toContain('className="shell subnav"')
79 })
80
81 test('separators are decorative and the anchors stay router-intercepted', async () => {
82 const { html } = await renderTrail()
83 expect((html.match(/aria-hidden="true"/g) ?? []).length).toBe(2)
84 expect((html.match(/data-stx-link/g) ?? []).length).toBe(2)
85 })
86})