ReviewOS

also looking at this

loghqorg/loghq

chore(release): promote UI, accessibility, and STX updates to production

#11
Merged glennmichael123 wants to merge main into production
16 files +176 -67
bun.lockmodified+9-11
Changes to bun.lock
@@ -19,7 +19,9 @@
1919 },
2020 },
2121 "overrides": {
22 "@stacksjs/stx": "0.2.382",
22 "@stacksjs/components": "0.2.401",
23 "@stacksjs/stx": "0.2.401",
24 "bun-plugin-stx": "0.2.401",
2325 },
2426 "packages": {
2527 "@bughq/sdk": ["@bughq/sdk@0.2.2", "", {}, "sha512-OySn397nSGHmFv4qu8/HUhX25sb+ORKCk94yOt/OR1vymScXPJPAA5eV9WcxrnzVUPvNOgEhp4lfEDm8MVS8Kw=="],
@@ -94,7 +96,7 @@
9496
9597 "@stacksjs/commerce": ["@stacksjs/commerce@0.75.79", "", { "dependencies": { "@stacksjs/cache": "0.75.79", "@stacksjs/config": "0.75.79", "@stacksjs/database": "0.75.79", "@stacksjs/env": "0.75.79", "@stacksjs/error-handling": "0.75.79", "@stacksjs/events": "0.75.79", "@stacksjs/logging": "0.75.79", "@stacksjs/orm": "0.75.79", "@stacksjs/payments": "0.75.79", "@stacksjs/realtime": "0.75.79", "@stacksjs/strings": "0.75.79", "ts-spreadsheets": "^0.2.0" } }, "sha512-yI9zltR+QUk5tOSF3/LadPxqdK0f5Bmj2eazMUH1dY5X3wpjqzHJMqcwtcFf3yfqsQXknTaKAXU5EeVjw6w5aQ=="],
9698
97 "@stacksjs/components": ["@stacksjs/components@0.2.378", "", { "dependencies": { "@iconify-json/f7": "^1.2.2", "@stacksjs/stx": "0.2.378", "@stacksjs/ts-css": "^0.3.2", "bun-plugin-stx": "0.2.378", "ts-syntax-highlighter": "^0.2.17" } }, "sha512-RJ5KQ0Nd9LfFkhcHylPnYr2Tx5o+GCrdM1hPLZ177RP/bO6LV25hnQvDOAzzAATRQSJZ8s/gls8GJWjY99V+Eg=="],
99 "@stacksjs/components": ["@stacksjs/components@0.2.401", "", { "dependencies": { "@iconify-json/f7": "^1.2.2", "@stacksjs/stx": "0.2.401", "@stacksjs/ts-css": "^0.3.2", "bun-plugin-stx": "0.2.401", "ts-syntax-highlighter": "^0.2.17" } }, "sha512-NqWBHU83zNEGsOHmHwVFOR4N97pteXqpCE5xcIqejeCXGNKpD/ecapG62GbPC+rvPRsHNRwKsOtMPMwObIMLMw=="],
98100
99101 "@stacksjs/composables": ["@stacksjs/composables@0.75.79", "", { "dependencies": { "@stacksjs/datetime": "0.75.79", "@stacksjs/stx": "^0.2.378" } }, "sha512-ZPVfcfLqgyexCYmyW7ignmeS+d6fH7S98gWU+0IGGT9NsAJ2tLZ3bafzKp1JinBQvDQxvpbBbrWFsc7n6ZXWbw=="],
100102
@@ -224,7 +226,7 @@
224226
225227 "@stacksjs/strings": ["@stacksjs/strings@0.75.79", "", {}, "sha512-u5WxVtwM48aMcgBJL3BTOoac/lyKhnfZGzqMjBh+wdR/GDUbwO++6uhBH8GXZwQttgh5OMhvTAgtkyLl4AITkA=="],
226228
227 "@stacksjs/stx": ["@stacksjs/stx@0.2.382", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/desktop": "0.2.382", "@stacksjs/sanitizer": "0.2.382", "@stacksjs/ts-css": "^0.3.2", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.382", "bunfig": "^0.15.19", "stx-router": "0.2.382", "ts-broadcasting": "^0.0.11", "ts-images": "^0.2.22", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-Zubrn6LdC0jHt3YhkUjFKq23JjgoIx7YiTHOwwDZqZa6p7DLKyVVaAstNVhXo8GwPMT0QFK0bwSa2smnw49AWQ=="],
229 "@stacksjs/stx": ["@stacksjs/stx@0.2.401", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/desktop": "0.2.401", "@stacksjs/sanitizer": "0.2.401", "@stacksjs/ts-css": "^0.3.2", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.401", "bunfig": "^0.15.19", "stx-router": "0.2.401", "ts-broadcasting": "^0.0.11", "ts-images": "^0.2.22", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-WMPMaBa/Pf6izmJkwMwj41omsBUBSgVy6RKakWpOZ1XBvTn0yPXrWAh8TZ0k6vBWx9ir+jcg87jk/t7onqBehw=="],
228230
229231 "@stacksjs/testing": ["@stacksjs/testing@0.75.79", "", { "dependencies": { "@stacksjs/auth": "0.75.79", "@stacksjs/cache": "0.75.79", "@stacksjs/config": "0.75.79", "@stacksjs/database": "0.75.79", "@stacksjs/email": "0.75.79", "@stacksjs/events": "0.75.79", "@stacksjs/path": "0.75.79", "@stacksjs/router": "0.75.79", "@stacksjs/storage": "0.75.79", "@stacksjs/strings": "0.75.79", "@stacksjs/ts-cloud": "^0.16.40", "very-happy-dom": "^0.3.5" } }, "sha512-DA7KWv8ZZ+kXqXSxGGwsudCVreVLMfqft1asYRo689NCOpn+Mw+pUR3L2JInyMsASek1JwRnjnUj747MzEhibg=="],
230232
@@ -394,7 +396,7 @@
394396
395397 "bun-plugin-dtsx": ["bun-plugin-dtsx@0.11.5", "", { "dependencies": { "@stacksjs/dtsx": "0.11.5" } }, "sha512-dva7CrHthEYAmnEQu8m0yvaP0joNS3zQQC4TSt7CS/dlEfUYVYU5jTqAB+Iseew4H2HceD5cy57XzpdpG7g9IA=="],
396398
397 "bun-plugin-stx": ["bun-plugin-stx@0.2.378", "", { "dependencies": { "@stacksjs/stx": "0.2.378", "stx-router": "0.2.378" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-3sldSzS82fQAVkKnwyc1HSWyhk8rCwU1I1YNydt487/ucKmrPOufBudtrFO6U66Be988aPZlhzDmeH573U/38A=="],
399 "bun-plugin-stx": ["bun-plugin-stx@0.2.401", "", { "dependencies": { "@stacksjs/stx": "0.2.401", "stx-router": "0.2.401" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-O6rEjZwlgG13eZ7gN1IWZPOO9HWz0SeLKOgXgElxl5bcVMfVkXCCJZ7TfjdnlCnvR4iWZ45ZgmZ5Uouz71lHLQ=="],
398400
399401 "bun-query-builder": ["bun-query-builder@0.2.57", "", { "dependencies": { "@stacksjs/clapp": "^0.2.12", "@stacksjs/ts-faker": "^0.1.8", "@stacksjs/ts-validation": "^0.5.2", "dynamodb-tooling": "^0.3.5" }, "bin": { "query-builder": "./dist/bin/cli.js", "qbx": "./dist/bin/cli.js", "qb": "./dist/bin/cli.js" } }, "sha512-wqmZzUnWPLunYJwtv9BCh02/NrDBQ+pDjRppfwbUBNY17VWAOzOtDixST89XuaVTdh6MmbCYIeBzXrCNRjkXoQ=="],
400402
@@ -442,7 +444,7 @@
442444
443445 "stripe": ["stripe@23.0.0", "", { "peerDependencies": { "@types/node": ">=20" }, "optionalPeers": ["@types/node"] }, "sha512-MDE2A5ZVVluYURORaZEVghSmnLJc3w6aHAlZSxDQMsOjOb3V0O6ro/CiaeFlJS4N7uIVmsg98ocRsPmUK6YfyA=="],
444446
445 "stx-router": ["stx-router@0.2.382", "", {}, "sha512-5XbUlKn7/c+MlYp0edt+XfeEWM/6TlG2z47KyJ1x9/HWWP8Cugmk2FhidSCM6MxdatYEsJx2UFuWDqy+QTfSmw=="],
447 "stx-router": ["stx-router@0.2.401", "", {}, "sha512-WtdMe3rMnJUskdNsbRp5QAVdN8Gucuu7H5GaSReFyk2FMEzYw2HK0YlxruWpckt6y9ipenn+djgsaI9y69GOwQ=="],
446448
447449 "tar-stream": ["tar-stream@3.2.0", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="],
448450
@@ -538,11 +540,9 @@
538540
539541 "@stacksjs/stx/@stacksjs/clapp": ["@stacksjs/clapp@0.2.16", "", {}, "sha512-g/5HUXGxD9QffyPKGdywZODte0DJYwzNhSmrPime1z7TLxWPodJ9IYfIwr2G6Wco+fAQISgaVQUtVf0pUNGV0w=="],
540542
541 "@stacksjs/stx/@stacksjs/desktop": ["@stacksjs/desktop@0.2.382", "", { "dependencies": { "craft-native": ">=0.0.111" } }, "sha512-HldXN9LwBAz+9iujhhSq8M645bIGTDu9wj4zsC36SSyhvO5B2+ca063i9q8LbCjoq1IQXaKKIwF3cfnR5jk4Tg=="],
543 "@stacksjs/stx/@stacksjs/desktop": ["@stacksjs/desktop@0.2.401", "", { "dependencies": { "craft-native": ">=0.0.111" } }, "sha512-y2+7KUJuFpYlb7hE8J+mIMLAKPDwZxa+o1UbNkqnFOMNnfnt2pIoaE/JZQspoWZi4NKrj1vlrY5ZIaJBIENw/g=="],
542544
543 "@stacksjs/stx/@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.382", "", {}, "sha512-CIdaayMSuY2oDWdOdu/eQknLjb8zVZ2JnXVmAzUE/Ey2lEB99ewPQpz5ZmMSNREyau7K0K+KbcsDtqIzFzQBlQ=="],
544
545 "@stacksjs/stx/bun-plugin-stx": ["bun-plugin-stx@0.2.382", "", { "dependencies": { "@stacksjs/stx": "0.2.382", "stx-router": "0.2.382" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-MZVJUMaDt8vj5Erntc6UpOeJkDNH+J9lyrIFRCwdSdbQgMr9OO/B8DcTI6FIMqimPaq4IXFHToUW81pDHID/5Q=="],
545 "@stacksjs/stx/@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.401", "", {}, "sha512-06yOJojUBDvEQFw/ktci2+E2S3ZE+vCYZuxOKSxr/7XHHHiFmvRuFBmDufvmJvaGUWfUCuW6y6XchXERNQaFbg=="],
546546
547547 "@stacksjs/stx/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="],
548548
@@ -572,8 +572,6 @@
572572
573573 "@ts-analytics/tracking/bun-query-builder": ["bun-query-builder@0.1.63", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "@stacksjs/ts-faker": "^0.1.8", "@stacksjs/ts-validation": "^0.5.0", "dynamodb-tooling": "^0.3.5" }, "bin": { "qb": "./dist/bin/cli.js", "qbx": "./dist/bin/cli.js", "query-builder": "./dist/bin/cli.js" } }, "sha512-cywlLKmwXCJAop8H7VwlHEbLKjy9KUfuyps1BGy4rkj2eSvMWU68bgkJlVC4+Q4rFFqMeGMXY78LyEgwU/QBDQ=="],
574574
575 "bun-plugin-stx/stx-router": ["stx-router@0.2.378", "", {}, "sha512-eqpF1UZo3YOU6GvU8gyfOiR8y3OV+7lg6eNnOXQD8ghsyIS/ZM7I5Tr6b/9p5kJg5RHUkYvVCYhKtfsHzKxGsw=="],
576
577575 "stacks/bun-query-builder": ["bun-query-builder@0.3.5", "", { "dependencies": { "@stacksjs/clapp": "^0.2.16", "@stacksjs/ts-faker": "^0.1.8", "@stacksjs/ts-validation": "^0.5.6", "dynamodb-tooling": "^0.3.6" }, "bin": { "qb": "./dist/bin/cli.js", "qbx": "./dist/bin/cli.js", "query-builder": "./dist/bin/cli.js" } }, "sha512-705HMAjhjbb1KaPaJYj45AzUMcJ6nA6xgYPCYfCHZftpv/JrUCBDS0CoknfggvtuKHp8Dk8oa+R8tlnuk9Pwkw=="],
578576
579577 "ts-pantry/@stacksjs/ts-cloud": ["@stacksjs/ts-cloud@0.7.114", "", { "dependencies": { "@stacksjs/ts-xml": "^0.1.0", "@ts-cloud/aws-types": "0.7.114", "@ts-cloud/core": "0.7.114" }, "bin": { "cloud": "./dist/bin/cli.js" } }, "sha512-OYzS++exsGBxY/mfVHlpL2RTu+/cZXAQqhta4ekXtX2K8cPUKZg50CNFuZBsAGKBVHjXoBFpr3ahv4bxcw5VWQ=="],
config/crosswind.tsmodified+65-14
Changes to config/crosswind.ts
@@ -46,6 +46,18 @@ import type { CrosswindConfig, Theme } from '@cwcss/crosswind'
4646export default {
4747 theme: {
4848 extend: {
49 borderRadius: {
50 control: '10px',
51 pill: '999px',
52 /*
53 * `rounded-panel` is @stacksjs/components' radius role name:
54 * <EmptyState variant="panel"> renders `bg-panel rounded-panel
55 * ring-1 ring-line`. Without this it resolves to nothing and the
56 * panel renders square. Pointed at this app's own panel radius:
57 * the `app-panel` shortcut uses rounded-xl.
58 */
59 panel: '0.75rem',
60 },
4961 colors: {
5062 canvas: 'var(--bg)',
5163 panel: 'var(--panel)',
@@ -73,28 +85,67 @@ export default {
7385 // right in both. See public/tokens.css.
7486 'accent-ink': 'var(--accent-ink)',
7587
76 // --- @stacksjs/components' token vocabulary ------------------------
77 // The shipped components are written against their own semantic names
78 // (text-fg, bg-surface, border-line-strong, ...) used 300+ times across
79 // the 102 components. This palette already satisfied `line` and
80 // `accent`; the rest resolved to nothing, so a component that imposes
81 // no colour of its own still rendered half-styled - text fell back to
82 // inherit and panels had no background.
83 //
84 // Purely additive: none of these names appear in this app's markup, so
85 // no existing element changes. Does NOT fix <Button variant="primary">,
86 // which hard-codes bg-blue-500 - that is stacksjs/stx#1993.
87 // See statushqorg/status#20.
88 surface: 'var(--panel)',
88 // Complete component roles follow the app palette, not stock library
89 // colors. Theme entries also avoid layered preflight precedence issues.
90 // --ok alone is too bright for white small labels in light mode.
91 'surface': 'var(--panel)',
8992 'surface-sunken': 'var(--bg)',
9093 'surface-raised': 'color-mix(in srgb, var(--accent) 8%, var(--panel))',
91 fg: 'var(--text)',
94 'surface-hover': 'color-mix(in srgb, var(--text) 6%, var(--panel))',
95 'surface-raised-hover': 'color-mix(in srgb, var(--accent) 14%, var(--panel))',
96 'surface-sunken-hover': 'color-mix(in srgb, var(--text) 6%, var(--bg))',
97 'page': 'var(--bg)',
98 'content': 'var(--panel)',
99 'field': 'var(--panel)',
100 'field-hover': 'color-mix(in srgb, var(--text) 6%, var(--panel))',
101 'fg': 'var(--text)',
92102 'fg-strong': 'var(--text)',
93103 'fg-muted': 'var(--text-2)',
94104 'fg-soft': 'var(--text-2)',
95105 'fg-subtle': 'var(--text-3)',
96106 'line-strong': 'color-mix(in srgb, var(--text-3) 55%, var(--border))',
107 'line-hover': 'color-mix(in srgb, var(--text-2) 70%, var(--border))',
108 'link': 'var(--accent)',
109 'link-hover': 'color-mix(in srgb, var(--accent) 85%, var(--text))',
97110 'accent-solid': 'var(--accent)',
111 'accent-solid-hover': 'color-mix(in srgb, var(--accent) 92%, var(--text))',
112 'accent-soft-ink': 'color-mix(in srgb, var(--accent) 85%, var(--text))',
113 'success': 'color-mix(in srgb, var(--ok) 75%, var(--text))',
114 'success-solid': 'color-mix(in srgb, var(--ok) 75%, var(--text))',
115 'success-solid-hover': 'color-mix(in srgb, color-mix(in srgb, var(--ok) 75%, var(--text)) 92%, var(--text))',
116 'success-ink': 'var(--accent-ink)',
117 'success-soft': 'color-mix(in srgb, color-mix(in srgb, var(--ok) 75%, var(--text)) 8%, var(--panel))',
118 'success-soft-ink': 'color-mix(in srgb, color-mix(in srgb, var(--ok) 75%, var(--text)) 85%, var(--text))',
119 'danger': 'var(--warn)',
120 'danger-solid': 'var(--warn)',
121 'danger-solid-hover': 'color-mix(in srgb, var(--warn) 92%, var(--text))',
122 'danger-ink': 'var(--accent-ink)',
123 'danger-soft': 'color-mix(in srgb, var(--warn) 8%, var(--panel))',
124 'danger-soft-ink': 'color-mix(in srgb, var(--warn) 85%, var(--text))',
125 'warning': 'var(--sev-warning)',
126 'warning-solid': 'var(--sev-warning)',
127 'warning-solid-hover': 'color-mix(in srgb, var(--sev-warning) 92%, var(--text))',
128 'warning-ink': 'var(--accent-ink)',
129 'warning-soft': 'color-mix(in srgb, var(--sev-warning) 8%, var(--panel))',
130 'warning-soft-ink': 'color-mix(in srgb, var(--sev-warning) 85%, var(--text))',
131 'info': 'var(--text-2)',
132 'info-solid': 'var(--text-2)',
133 'info-solid-hover': 'color-mix(in srgb, var(--text-2) 92%, var(--text))',
134 'info-ink': 'var(--accent-ink)',
135 'info-soft': 'color-mix(in srgb, var(--text-2) 8%, var(--panel))',
136 'info-soft-ink': 'color-mix(in srgb, var(--text-2) 85%, var(--text))',
137 'secondary': 'var(--text-2)',
138 'secondary-solid': 'var(--text-2)',
139 'secondary-solid-hover': 'color-mix(in srgb, var(--text-2) 92%, var(--text))',
140 'secondary-ink': 'var(--accent-ink)',
141 'secondary-soft': 'color-mix(in srgb, var(--text-2) 8%, var(--panel))',
142 'secondary-soft-ink': 'color-mix(in srgb, var(--text-2) 85%, var(--text))',
143 'danger-fg': 'var(--warn)',
144 'danger-fg-subtle': 'var(--warn)',
145 'danger-line': 'var(--warn)',
146 'danger-focus': 'var(--warn)',
147 'inverse': 'var(--text)',
148 'inverse-ink': 'var(--bg)',
98149 },
99150 // The site's own two breakpoints, as min-widths. public/marketing.css
100151 // states them as `max-width: 560px` and `max-width: 900px`; Crosswind
package.jsonmodified+3-1
Changes to package.json
@@ -87,7 +87,9 @@
8787 },
8888 "//overrides": "stx is not a direct dependency \u2014 the only reference is a type import in config/ui.ts. But it arrives through nine transitive paths, and bun-plugin-stx asks for a range so loose (^0.2.72) that dropping the old direct pin let the hoisted copy fall to 0.2.176 while the framework packages sat on 0.2.231. bun-plugin-stx resolves to the HOISTED copy, so that is the version that renders every .stx file. Every constraint in the tree (0.2.378, ^0.2.378, ^0.2.375, >=0.2.227, ^0.2.82, as of stacks 0.75.79) is satisfied by 0.2.378, so pinning it here collapses the tree to one copy without re-declaring a dependency this app does not have. Re-check after a stacks bump.",
8989 "overrides": {
90 "@stacksjs/stx": "0.2.382"
90 "@stacksjs/components": "0.2.401",
91 "bun-plugin-stx": "0.2.401",
92 "@stacksjs/stx": "0.2.401"
9193 },
9294 "devDependencies": {
9395 "@iconify-json/lucide": "^1.2.121",
public/marketing.cssmodified+6-1
Changes to public/marketing.css
@@ -281,7 +281,12 @@ a.cell:hover .cell-more { color: var(--accent); gap: 0.55rem; }
281281/* One line, reserved by min-height so a reply does not shift the section. */
282282.sub-status { margin: 0.85rem auto 0; min-height: 1.25rem; color: var(--text-2); font-size: 0.88rem; }
283283.sub-status[data-state="error"] { color: var(--accent); }
284@media (max-width: 30rem) { .sub-form { flex-direction: column; } .sub-form .btn { width: 100%; justify-content: center; } }
284@media (max-width: 30rem) {
285 .sub-form { flex-direction: column; }
286 /* A row's 15rem width basis becomes height when the form stacks. */
287 .sub-field { flex: none; }
288 .sub-form .btn { width: 100%; justify-content: center; }
289}
285290
286291/* footer */
287292.footer { padding: 2.5rem 0; }
resources/partials/Breadcrumbs.stxmodified+17-4
Changes to resources/partials/Breadcrumbs.stx
@@ -7,10 +7,23 @@
77 Each crumb is { label, href?, url? }. The last has no href: it is the
88 current page.
99
10 Deliberately NOT the shipped Breadcrumb component. That component builds its
11 trail with @foreach(item in displayItems), and the loop parser only accepts
12 the ` as ` form — the ` in ` form is skipped outright, so the literal
13 directive text would ship to the browser on all 22 pages.
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.
14
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.
22
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.
1427
1528 Ordered list, not a row of anchors: this is a sequence, the position is
1629 meaningful, and assistive tech should say so. The separators are decorative
resources/views/analytics.stxmodified+13-7
Changes to resources/views/analytics.stx
@@ -382,10 +382,13 @@ summary:focus-visible {
382382 client pre-paint bounce (deleted with AuthGuard) the server renders a
383383 sign-in panel when the auth-token cookie resolved no user. --}}
384384 @if (!user)
385 <div class="py-24 text-center">
386 <p class="mb-4 text-sm text-subtle">You're signed out.</p>
385 <EmptyState
386 variant="bare"
387 title="Sign in required"
388 description="You need to be signed in to see your analytics."
389 >
387390 <StxLink to="/login" class="inline-block px-4 py-2.5 font-semibold text-accent-ink text-sm bg-accent hover:bg-[color-mix(in_srgb,var(--accent)_88%,#000)] rounded-[10px]">Sign in</StxLink>
388 </div>
391 </EmptyState>
389392 @endif
390393
391394 @if (user && projectsUnavailable)
@@ -396,10 +399,13 @@ summary:focus-visible {
396399 @endif
397400
398401 @if (user && !projectsUnavailable && !projectId)
399 <div class="py-24 text-center">
400 <p class="text-muted text-sm">No projects yet.</p>
401 <p class="mt-2 text-subtle text-xs"><StxLink to="/projects/new" class="text-accent">Create one</StxLink> and its analytics appear here.</p>
402 </div>
402 <EmptyState
403 variant="bare"
404 title="No projects yet"
405 description="Create one and its analytics appear here."
406 >
407 <StxLink to="/projects/new" class="text-accent">Create an app</StxLink>
408 </EmptyState>
403409 @endif
404410
405411 @if (user && projectId)
resources/views/fix.stxmodified+9-3
Changes to resources/views/fix.stx
@@ -179,8 +179,14 @@ const action = async ({ form, cookies: reqCookies }: { form: Record<string, stri
179179 </div>
180180
181181 @if (!authed)
182 <div class="px-3 py-3 text-sm rounded-lg err-box">You need to be signed in to analyze a log entry.</div>
183 <p class="mt-6 text-sm text-subtle"><StxLink to="/login" class="font-medium text-accent">Go to sign in</StxLink></p>
182 {{-- Being signed out is not an error, so this is an empty state with an
183 action rather than the red err-box it used to be. --}}
184 <EmptyState
185 title="Sign in required"
186 description="You need to be signed in to analyze a log entry."
187 >
188 <StxLink to="/login" class="font-medium text-accent">Go to sign in</StxLink>
189 </EmptyState>
184190 @elseif (!entry)
185191 <h1 class="mb-2 font-semibold text-xl tracking-tight">Log entry not found</h1>
186192 <p class="text-sm text-subtle">It may have been removed, or it belongs to an app you cannot access.</p>
@@ -211,7 +217,7 @@ const action = async ({ form, cookies: reqCookies }: { form: Record<string, stri
211217 </section>
212218
213219 @if (formError)
214 <div class="mb-6 px-3 py-2 text-sm rounded-lg err-box">{{ formError }}</div>
220 <div role="alert" class="mb-6 px-3 py-2 text-sm rounded-lg err-box">{{ formError }}</div>
215221 @endif
216222
217223 @if (!enabled)
resources/views/forgot-password.stxmodified+1-1
Changes to resources/views/forgot-password.stx
@@ -53,7 +53,7 @@ const action = async ({ form }: { form: Record<string, string | string[]> }) =>
5353 </div>
5454
5555 @if (sent)
56 <div class="px-3 py-3 text-sm rounded-lg ok-box">
56 <div role="status" class="px-3 py-3 text-sm rounded-lg ok-box">
5757 If an account exists for that email, a reset link is on its way. It expires in 60 minutes.
5858 </div>
5959 <p class="mt-6 text-center text-sm text-subtle">
resources/views/login.stxmodified+5-5
Changes to resources/views/login.stx
@@ -139,21 +139,21 @@ async function onVerify(e: Event): Promise<void> {
139139
140140 {{-- The shared error box for both steps. `:text` overwrites the (empty)
141141 static label after hydration and never cloaks. --}}
142 <div :show="!!error()" :text="error()" class="mb-4 px-3 py-2 text-sm rounded-lg err-box"></div>
142 <div id="login-error" role="alert" :show="!!error()" :text="error()" class="mb-4 px-3 py-2 text-sm rounded-lg err-box"></div>
143143
144144 {{-- Step 1: credentials. A same-origin fetch, not a native POST — the
145145 submit is intercepted so the browser never navigates to /login itself. --}}
146146 <form :show="mode() === 'credentials'" action="/api/auth/login" method="POST" @submit="onLogin" class="space-y-4">
147147 <div>
148148 <label for="login-email" class="block mb-1.5 font-medium text-muted text-xs">Email</label>
149 <input id="login-email" name="email" type="email" required autocomplete="email" placeholder="you@example.com" :model="email" class="px-3.5 py-2.5 w-full text-sm field">
149 <input id="login-email" name="email" type="email" required autocomplete="email" placeholder="you@example.com" :model="email" class="px-3.5 py-2.5 w-full text-sm field" :aria-invalid="error() ? 'true' : 'false'" :aria-describedby="error() ? 'login-error' : ''">
150150 </div>
151151 <div>
152152 <div class="flex items-center justify-between mb-1.5">
153153 <label for="login-password" class="block font-medium text-muted text-xs">Password</label>
154154 <StxLink to="/forgot-password" class="font-medium text-accent text-xs">Forgot password?</StxLink>
155155 </div>
156 <input id="login-password" name="password" type="password" required autocomplete="current-password" placeholder="••••••••" :model="password" class="px-3.5 py-2.5 w-full text-sm field">
156 <input id="login-password" name="password" type="password" required autocomplete="current-password" placeholder="••••••••" :model="password" class="px-3.5 py-2.5 w-full text-sm field" :aria-invalid="error() ? 'true' : 'false'" :aria-describedby="error() ? 'login-error' : ''">
157157 </div>
158158
159159 {{-- Remember me, default OFF: unchecked is the baseline week, checked is
@@ -171,13 +171,13 @@ async function onVerify(e: Event): Promise<void> {
171171 <p class="text-sm text-subtle">Enter the 6-digit code from your authenticator app.</p>
172172 <div>
173173 <label for="login-code" class="block mb-1.5 font-medium text-muted text-xs">Authentication code</label>
174 <input id="login-code" name="code" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="6" placeholder="123456" :model="code" class="px-3.5 py-2.5 w-full text-sm field">
174 <input id="login-code" name="code" type="text" inputmode="numeric" autocomplete="one-time-code" maxlength="6" placeholder="123456" :model="code" class="px-3.5 py-2.5 w-full text-sm field" :aria-invalid="error() ? 'true' : 'false'" :aria-describedby="error() ? 'login-error' : ''">
175175 </div>
176176 <button type="submit" :disabled="loading()" :text="loading() ? 'Verifying…' : 'Verify and sign in'" class="py-2.5 w-full font-semibold text-sm btn-solid">Verify and sign in</button>
177177 </form>
178178
179179 @if (oauthError)
180 <div class="mt-4 px-3 py-2 text-sm rounded-lg err-box">{{ oauthError }}</div>
180 <div role="alert" class="mt-4 px-3 py-2 text-sm rounded-lg err-box">{{ oauthError }}</div>
181181 @endif
182182
183183 @if (anyOauth)
resources/views/projects/index.stxmodified+2-4
Changes to resources/views/projects/index.stx
@@ -167,11 +167,9 @@ function logout(): void {
167167 <div class="text-sm text-subtle">Make sure Postgres is provisioned. Nothing has been lost — reload once the database is back.</div>
168168 </div>
169169 @else
170 <div class="px-5 py-16 text-center bg-panel border border-line rounded-xl">
171 <div class="mb-1 font-medium text-base">No apps yet</div>
172 <div class="mb-5 text-sm text-subtle">Create your first app to start capturing errors.</div>
170 <EmptyState title="No apps yet" description="Create your first app to start capturing errors.">
173171 <StxLink to="/projects/new" class="inline-block px-4 py-2.5 font-semibold text-accent-ink text-sm bg-accent hover:bg-[color-mix(in_srgb,var(--accent)_88%,#000)] rounded-[10px]">Create an app</StxLink>
174 </div>
172 </EmptyState>
175173 @endif
176174 @endforelse
177175 @endif
resources/views/projects/new.stxmodified+9-5
Changes to resources/views/projects/new.stx
@@ -94,20 +94,24 @@ const action = async ({ form, cookies: reqCookies }: { form: Record<string, stri
9494 {{-- Reached without a session. The client guard this replaces navigated
9595 away after hydration, so a logged-out visitor saw the form flash
9696 first; this is the honest answer in the first response. --}}
97 <div class="px-3 py-3 text-sm rounded-lg err-box">You need to be signed in to create an app.</div>
98 <p class="mt-6 text-center text-sm text-subtle">
97 {{-- Being signed out is not an error, so this is an empty state with an
98 action rather than the red err-box it used to be. --}}
99 <EmptyState
100 title="Sign in required"
101 description="You need to be signed in to create an app."
102 >
99103 <StxLink to="/login" class="font-medium text-accent">Go to sign in</StxLink>
100 </p>
104 </EmptyState>
101105 @else
102106 {{-- Posts to itself; the action is in the server block. --}}
103107 <form action="/projects/new" method="POST" class="space-y-4">
104108 @if (formError)
105 <div class="px-3 py-2 text-sm rounded-lg err-box">{{ formError }}</div>
109 <div id="form-error" role="alert" class="px-3 py-2 text-sm rounded-lg err-box">{{ formError }}</div>
106110 @endif
107111
108112 <div>
109113 <label for="newapp-name" class="block mb-1.5 font-medium text-muted text-xs">App name</label>
110 <input id="newapp-name" name="name" type="text" required autofocus placeholder="Acme Web" value="" class="px-3.5 py-2.5 w-full text-sm field">
114 <input id="newapp-name" name="name" type="text" required autofocus placeholder="Acme Web" value="" class="px-3.5 py-2.5 w-full text-sm field" aria-invalid="false"@if (formError) aria-describedby="form-error"@endif>
111115 </div>
112116 <div>
113117 <label for="newapp-platform" class="block mb-1.5 font-medium text-muted text-xs">Platform</label>
resources/views/register.stxmodified+4-4
Changes to resources/views/register.stx
@@ -95,21 +95,21 @@ async function onRegister(e: Event): Promise<void> {
9595 <p class="mb-3.5 invite-hint">You've been invited to a project — sign up to join it.</p>
9696 @endif
9797
98 <div :show="!!error()" :text="error()" class="mb-4 px-3 py-2 text-sm rounded-lg err-box"></div>
98 <div id="form-error" role="alert" :show="!!error()" :text="error()" class="mb-4 px-3 py-2 text-sm rounded-lg err-box"></div>
9999
100100 {{-- A same-origin fetch, not a native POST — the submit is intercepted. --}}
101101 <form action="/api/auth/register" method="POST" @submit="onRegister" class="space-y-4">
102102 <div>
103103 <label for="register-name" class="block mb-1.5 font-medium text-muted text-xs">Name</label>
104 <input id="register-name" name="name" type="text" required autocomplete="name" placeholder="Ada Lovelace" :model="name" class="px-3.5 py-2.5 w-full text-sm field">
104 <input id="register-name" name="name" type="text" required autocomplete="name" placeholder="Ada Lovelace" :model="name" class="px-3.5 py-2.5 w-full text-sm field" :aria-invalid="error() ? 'true' : 'false'" :aria-describedby="error() ? 'form-error' : ''">
105105 </div>
106106 <div>
107107 <label for="register-email" class="block mb-1.5 font-medium text-muted text-xs">Email</label>
108 <input id="register-email" name="email" type="email" required autocomplete="email" placeholder="you@example.com" :model="email" {{ invitedEmail ? 'readonly' : '' }} class="px-3.5 py-2.5 w-full text-sm field">
108 <input id="register-email" name="email" type="email" required autocomplete="email" placeholder="you@example.com" :model="email" {{ invitedEmail ? 'readonly' : '' }} class="px-3.5 py-2.5 w-full text-sm field" :aria-invalid="error() ? 'true' : 'false'" :aria-describedby="error() ? 'form-error' : ''">
109109 </div>
110110 <div>
111111 <label for="register-password" class="block mb-1.5 font-medium text-muted text-xs">Password</label>
112 <input id="register-password" name="password" type="password" required autocomplete="new-password" placeholder="At least 8 characters" :model="password" class="px-3.5 py-2.5 w-full text-sm field">
112 <input id="register-password" name="password" type="password" required autocomplete="new-password" placeholder="At least 8 characters" :model="password" class="px-3.5 py-2.5 w-full text-sm field" :aria-invalid="error() ? 'true' : 'false'" :aria-describedby="error() ? 'form-error' : ''">
113113 </div>
114114
115115 <button type="submit" :disabled="loading()" :text="loading() ? 'Creating account…' : 'Create account'" class="py-2.5 w-full font-semibold text-sm btn-solid">Create account</button>
resources/views/reset-password.stxmodified+3-3
Changes to resources/views/reset-password.stx
@@ -85,16 +85,16 @@ const action = async ({ form }: { form: Record<string, string | string[]> }) =>
8585 <input type="hidden" name="token" value="">
8686 <input type="hidden" name="email" value="">
8787 @if (formError)
88 <div class="px-3 py-2 text-sm rounded-lg err-box">{{ formError }}</div>
88 <div id="form-error" role="alert" class="px-3 py-2 text-sm rounded-lg err-box">{{ formError }}</div>
8989 @endif
9090
9191 <div>
9292 <label for="reset-password" class="block mb-1.5 font-medium text-muted text-xs">New password</label>
93 <input id="reset-password" name="password" type="password" required minlength="8" autocomplete="new-password" placeholder="At least 8 characters" class="px-3.5 py-2.5 w-full text-sm field">
93 <input id="reset-password" name="password" type="password" required minlength="8" autocomplete="new-password" placeholder="At least 8 characters" class="px-3.5 py-2.5 w-full text-sm field" aria-invalid="false"@if (formError) aria-describedby="form-error"@endif>
9494 </div>
9595 <div>
9696 <label for="reset-password-confirmation" class="block mb-1.5 font-medium text-muted text-xs">Confirm new password</label>
97 <input id="reset-password-confirmation" name="password_confirmation" type="password" required minlength="8" autocomplete="new-password" placeholder="Repeat the password" class="px-3.5 py-2.5 w-full text-sm field">
97 <input id="reset-password-confirmation" name="password_confirmation" type="password" required minlength="8" autocomplete="new-password" placeholder="Repeat the password" class="px-3.5 py-2.5 w-full text-sm field" aria-invalid="false"@if (formError) aria-describedby="form-error"@endif>
9898 </div>
9999
100100 {{-- A plain button; no client mutation to bind a loading state to. --}}
resources/views/settings.stxmodified+1-4
Changes to resources/views/settings.stx
@@ -1455,10 +1455,7 @@ onMount(() => {
14551455 <button @click="deleteChannel('')" :text="deleteLabel('')" class="px-2.5 py-1.5 font-medium text-warn text-xs hover:text-accent-ink bg-panel hover:bg-warn border border-[color-mix(in_srgb,var(--warn)_45%,var(--border))] rounded-[9px] hover:border-warn [transition:all_0.15s_ease]" x-tooltip="Remove">Remove</button>
14561456 </div>
14571457 @empty
1458 <div class="px-5 py-8 text-center app-panel">
1459 <div class="mb-1 font-medium text-sm">No chat alerts yet</div>
1460 <div class="text-subtle text-xs">Add a Slack or Discord webhook below to start getting pinged.</div>
1461 </div>
1458 <EmptyState title="No chat alerts yet" description="Add a Slack or Discord webhook below to start getting pinged." />
14621459 @endforelse
14631460 </div>
14641461 <div class="px-5 py-5 app-panel">
tests/unit/component-theme.test.tsadded+24-0
Changes to tests/unit/component-theme.test.ts
@@ -0,0 +1,24 @@
1import { describe, expect, test } from 'bun:test'
2import theme from '../../config/crosswind'
3// STX exposes its selected CSS engine through these installed files.
4import { loadCssEngine } from '../../node_modules/@stacksjs/stx/dist/dev-server/ts-css.js'
5import { SEMANTIC_TOKENS, SHAPE_TOKENS } from '../../node_modules/@stacksjs/stx/dist/theme-tokens.js'
6import { mergeCssConfig } from '../../node_modules/@stacksjs/stx/dist/ts-css-config.js'
7
8describe('component palette contract', () => {
9 test('every component color and radius emits an app-owned utility', async () => {
10 const engine = await loadCssEngine()
11 if (!engine) throw new Error('The installed STX CSS engine is required')
12 const generator = new engine.CSSGenerator(mergeCssConfig(engine.defaultConfig || engine.config, theme).config)
13 for (const role of Object.keys(SEMANTIC_TOKENS)) generator.generate(`bg-${role}`)
14 for (const role of Object.keys(SHAPE_TOKENS)) generator.generate(`rounded-${role}`)
15 const css = generator.toCSS(false, false)
16 expect(css).not.toContain('var(--stx-')
17 for (const role of Object.keys(SEMANTIC_TOKENS)) expect(css).toContain(`.bg-${role} {`)
18 for (const role of Object.keys(SHAPE_TOKENS)) expect(css).toContain(`.rounded-${role} {`)
19 expect(css).toContain('.bg-content {\n background-color: var(--panel);')
20 expect(css).toContain('.bg-accent-solid {\n background-color: var(--accent);')
21 expect(css).toContain('.rounded-control {\n border-radius: 10px;')
22 expect(css).toContain('.rounded-panel {\n border-radius: 0.75rem;')
23 })
24})
tests/unit/mobile-layout.test.tsmodified+5-0
Changes to tests/unit/mobile-layout.test.ts
@@ -9,6 +9,11 @@ const marketing = readFileSync(join(ROOT, 'public/marketing.css'), 'utf8')
99// taller than an 812px viewport. The panel has to scroll inside the viewport
1010// and the page behind it has to stay put, or the last links are unreachable.
1111describe('mobile layout contracts', () => {
12 test('stacked subscription fields do not keep the desktop flex basis as height', () => {
13 const mobile = marketing.slice(marketing.indexOf('@media (max-width: 30rem)'))
14 expect(mobile).toMatch(/\.sub-field\s*\{\s*flex:\s*none;\s*\}/)
15 })
16
1217 test('marketing menu is bounded by the visible mobile viewport', () => {
1318 const panel = marketing.match(/\.nav-menu-panel\s*\{[^}]*\}/)?.[0] ?? ''
1419 expect(panel).toContain('max-height: calc(100dvh - 92px)')