also looking at this
chore(release): promote UI, accessibility, and STX updates to production
#11
16 files
+176
-67
bun.lockmodified+9-11
| @@ -19,7 +19,9 @@ | ||
| 19 | 19 | }, |
| 20 | 20 | }, |
| 21 | 21 | "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", | |
| 23 | 25 | }, |
| 24 | 26 | "packages": { |
| 25 | 27 | "@bughq/sdk": ["@bughq/sdk@0.2.2", "", {}, "sha512-OySn397nSGHmFv4qu8/HUhX25sb+ORKCk94yOt/OR1vymScXPJPAA5eV9WcxrnzVUPvNOgEhp4lfEDm8MVS8Kw=="], |
| @@ -94,7 +96,7 @@ | ||
| 94 | 96 | |
| 95 | 97 | "@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=="], |
| 96 | 98 | |
| 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=="], | |
| 98 | 100 | |
| 99 | 101 | "@stacksjs/composables": ["@stacksjs/composables@0.75.79", "", { "dependencies": { "@stacksjs/datetime": "0.75.79", "@stacksjs/stx": "^0.2.378" } }, "sha512-ZPVfcfLqgyexCYmyW7ignmeS+d6fH7S98gWU+0IGGT9NsAJ2tLZ3bafzKp1JinBQvDQxvpbBbrWFsc7n6ZXWbw=="], |
| 100 | 102 | |
| @@ -224,7 +226,7 @@ | ||
| 224 | 226 | |
| 225 | 227 | "@stacksjs/strings": ["@stacksjs/strings@0.75.79", "", {}, "sha512-u5WxVtwM48aMcgBJL3BTOoac/lyKhnfZGzqMjBh+wdR/GDUbwO++6uhBH8GXZwQttgh5OMhvTAgtkyLl4AITkA=="], |
| 226 | 228 | |
| 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=="], | |
| 228 | 230 | |
| 229 | 231 | "@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=="], |
| 230 | 232 | |
| @@ -394,7 +396,7 @@ | ||
| 394 | 396 | |
| 395 | 397 | "bun-plugin-dtsx": ["bun-plugin-dtsx@0.11.5", "", { "dependencies": { "@stacksjs/dtsx": "0.11.5" } }, "sha512-dva7CrHthEYAmnEQu8m0yvaP0joNS3zQQC4TSt7CS/dlEfUYVYU5jTqAB+Iseew4H2HceD5cy57XzpdpG7g9IA=="], |
| 396 | 398 | |
| 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=="], | |
| 398 | 400 | |
| 399 | 401 | "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=="], |
| 400 | 402 | |
| @@ -442,7 +444,7 @@ | ||
| 442 | 444 | |
| 443 | 445 | "stripe": ["stripe@23.0.0", "", { "peerDependencies": { "@types/node": ">=20" }, "optionalPeers": ["@types/node"] }, "sha512-MDE2A5ZVVluYURORaZEVghSmnLJc3w6aHAlZSxDQMsOjOb3V0O6ro/CiaeFlJS4N7uIVmsg98ocRsPmUK6YfyA=="], |
| 444 | 446 | |
| 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=="], | |
| 446 | 448 | |
| 447 | 449 | "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=="], |
| 448 | 450 | |
| @@ -538,11 +540,9 @@ | ||
| 538 | 540 | |
| 539 | 541 | "@stacksjs/stx/@stacksjs/clapp": ["@stacksjs/clapp@0.2.16", "", {}, "sha512-g/5HUXGxD9QffyPKGdywZODte0DJYwzNhSmrPime1z7TLxWPodJ9IYfIwr2G6Wco+fAQISgaVQUtVf0pUNGV0w=="], |
| 540 | 542 | |
| 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=="], | |
| 542 | 544 | |
| 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=="], | |
| 546 | 546 | |
| 547 | 547 | "@stacksjs/stx/bunfig": ["bunfig@0.15.19", "", { "dependencies": { "@stacksjs/clarity": "^0.3.24" }, "bin": { "bunfig": "./dist/cli.js" } }, "sha512-Ig302A74YUmagywXX1Dl8hXNdwjq/MIlB1TBVRduO6kGTMjkFQsxS3LimTFBNEThWypc/JS6cv4+TLeRAl2RGw=="], |
| 548 | 548 | |
| @@ -572,8 +572,6 @@ | ||
| 572 | 572 | |
| 573 | 573 | "@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=="], |
| 574 | 574 | |
| 575 | "bun-plugin-stx/stx-router": ["stx-router@0.2.378", "", {}, "sha512-eqpF1UZo3YOU6GvU8gyfOiR8y3OV+7lg6eNnOXQD8ghsyIS/ZM7I5Tr6b/9p5kJg5RHUkYvVCYhKtfsHzKxGsw=="], | |
| 576 | ||
| 577 | 575 | "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=="], |
| 578 | 576 | |
| 579 | 577 | "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=="], |
| @@ -46,6 +46,18 @@ import type { CrosswindConfig, Theme } from '@cwcss/crosswind' | ||
| 46 | 46 | export default { |
| 47 | 47 | theme: { |
| 48 | 48 | 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 | }, | |
| 49 | 61 | colors: { |
| 50 | 62 | canvas: 'var(--bg)', |
| 51 | 63 | panel: 'var(--panel)', |
| @@ -73,28 +85,67 @@ export default { | ||
| 73 | 85 | // right in both. See public/tokens.css. |
| 74 | 86 | 'accent-ink': 'var(--accent-ink)', |
| 75 | 87 | |
| 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)', | |
| 89 | 92 | 'surface-sunken': 'var(--bg)', |
| 90 | 93 | '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)', | |
| 92 | 102 | 'fg-strong': 'var(--text)', |
| 93 | 103 | 'fg-muted': 'var(--text-2)', |
| 94 | 104 | 'fg-soft': 'var(--text-2)', |
| 95 | 105 | 'fg-subtle': 'var(--text-3)', |
| 96 | 106 | '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))', | |
| 97 | 110 | '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)', | |
| 98 | 149 | }, |
| 99 | 150 | // The site's own two breakpoints, as min-widths. public/marketing.css |
| 100 | 151 | // states them as `max-width: 560px` and `max-width: 900px`; Crosswind |
| @@ -87,7 +87,9 @@ | ||
| 87 | 87 | }, |
| 88 | 88 | "//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.", |
| 89 | 89 | "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" | |
| 91 | 93 | }, |
| 92 | 94 | "devDependencies": { |
| 93 | 95 | "@iconify-json/lucide": "^1.2.121", |
| @@ -281,7 +281,12 @@ a.cell:hover .cell-more { color: var(--accent); gap: 0.55rem; } | ||
| 281 | 281 | /* One line, reserved by min-height so a reply does not shift the section. */ |
| 282 | 282 | .sub-status { margin: 0.85rem auto 0; min-height: 1.25rem; color: var(--text-2); font-size: 0.88rem; } |
| 283 | 283 | .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 | } | |
| 285 | 290 | |
| 286 | 291 | /* footer */ |
| 287 | 292 | .footer { padding: 2.5rem 0; } |
| @@ -7,10 +7,23 @@ | ||
| 7 | 7 | Each crumb is { label, href?, url? }. The last has no href: it is the |
| 8 | 8 | current page. |
| 9 | 9 | |
| 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. | |
| 14 | 27 | |
| 15 | 28 | Ordered list, not a row of anchors: this is a sequence, the position is |
| 16 | 29 | meaningful, and assistive tech should say so. The separators are decorative |
| @@ -382,10 +382,13 @@ summary:focus-visible { | ||
| 382 | 382 | client pre-paint bounce (deleted with AuthGuard) the server renders a |
| 383 | 383 | sign-in panel when the auth-token cookie resolved no user. --}} |
| 384 | 384 | @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 | > | |
| 387 | 390 | <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> | |
| 389 | 392 | @endif |
| 390 | 393 | |
| 391 | 394 | @if (user && projectsUnavailable) |
| @@ -396,10 +399,13 @@ summary:focus-visible { | ||
| 396 | 399 | @endif |
| 397 | 400 | |
| 398 | 401 | @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> | |
| 403 | 409 | @endif |
| 404 | 410 | |
| 405 | 411 | @if (user && projectId) |
| @@ -179,8 +179,14 @@ const action = async ({ form, cookies: reqCookies }: { form: Record<string, stri | ||
| 179 | 179 | </div> |
| 180 | 180 | |
| 181 | 181 | @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> | |
| 184 | 190 | @elseif (!entry) |
| 185 | 191 | <h1 class="mb-2 font-semibold text-xl tracking-tight">Log entry not found</h1> |
| 186 | 192 | <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 | ||
| 211 | 217 | </section> |
| 212 | 218 | |
| 213 | 219 | @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> | |
| 215 | 221 | @endif |
| 216 | 222 | |
| 217 | 223 | @if (!enabled) |
| @@ -53,7 +53,7 @@ const action = async ({ form }: { form: Record<string, string | string[]> }) => | ||
| 53 | 53 | </div> |
| 54 | 54 | |
| 55 | 55 | @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"> | |
| 57 | 57 | If an account exists for that email, a reset link is on its way. It expires in 60 minutes. |
| 58 | 58 | </div> |
| 59 | 59 | <p class="mt-6 text-center text-sm text-subtle"> |
| @@ -139,21 +139,21 @@ async function onVerify(e: Event): Promise<void> { | ||
| 139 | 139 | |
| 140 | 140 | {{-- The shared error box for both steps. `:text` overwrites the (empty) |
| 141 | 141 | 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> | |
| 143 | 143 | |
| 144 | 144 | {{-- Step 1: credentials. A same-origin fetch, not a native POST — the |
| 145 | 145 | submit is intercepted so the browser never navigates to /login itself. --}} |
| 146 | 146 | <form :show="mode() === 'credentials'" action="/api/auth/login" method="POST" @submit="onLogin" class="space-y-4"> |
| 147 | 147 | <div> |
| 148 | 148 | <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' : ''"> | |
| 150 | 150 | </div> |
| 151 | 151 | <div> |
| 152 | 152 | <div class="flex items-center justify-between mb-1.5"> |
| 153 | 153 | <label for="login-password" class="block font-medium text-muted text-xs">Password</label> |
| 154 | 154 | <StxLink to="/forgot-password" class="font-medium text-accent text-xs">Forgot password?</StxLink> |
| 155 | 155 | </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' : ''"> | |
| 157 | 157 | </div> |
| 158 | 158 | |
| 159 | 159 | {{-- Remember me, default OFF: unchecked is the baseline week, checked is |
| @@ -171,13 +171,13 @@ async function onVerify(e: Event): Promise<void> { | ||
| 171 | 171 | <p class="text-sm text-subtle">Enter the 6-digit code from your authenticator app.</p> |
| 172 | 172 | <div> |
| 173 | 173 | <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' : ''"> | |
| 175 | 175 | </div> |
| 176 | 176 | <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> |
| 177 | 177 | </form> |
| 178 | 178 | |
| 179 | 179 | @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> | |
| 181 | 181 | @endif |
| 182 | 182 | |
| 183 | 183 | @if (anyOauth) |
| @@ -167,11 +167,9 @@ function logout(): void { | ||
| 167 | 167 | <div class="text-sm text-subtle">Make sure Postgres is provisioned. Nothing has been lost — reload once the database is back.</div> |
| 168 | 168 | </div> |
| 169 | 169 | @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."> | |
| 173 | 171 | <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> | |
| 175 | 173 | @endif |
| 176 | 174 | @endforelse |
| 177 | 175 | @endif |
| @@ -94,20 +94,24 @@ const action = async ({ form, cookies: reqCookies }: { form: Record<string, stri | ||
| 94 | 94 | {{-- Reached without a session. The client guard this replaces navigated |
| 95 | 95 | away after hydration, so a logged-out visitor saw the form flash |
| 96 | 96 | 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 | > | |
| 99 | 103 | <StxLink to="/login" class="font-medium text-accent">Go to sign in</StxLink> |
| 100 | </p> | |
| 104 | </EmptyState> | |
| 101 | 105 | @else |
| 102 | 106 | {{-- Posts to itself; the action is in the server block. --}} |
| 103 | 107 | <form action="/projects/new" method="POST" class="space-y-4"> |
| 104 | 108 | @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> | |
| 106 | 110 | @endif |
| 107 | 111 | |
| 108 | 112 | <div> |
| 109 | 113 | <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> | |
| 111 | 115 | </div> |
| 112 | 116 | <div> |
| 113 | 117 | <label for="newapp-platform" class="block mb-1.5 font-medium text-muted text-xs">Platform</label> |
| @@ -95,21 +95,21 @@ async function onRegister(e: Event): Promise<void> { | ||
| 95 | 95 | <p class="mb-3.5 invite-hint">You've been invited to a project — sign up to join it.</p> |
| 96 | 96 | @endif |
| 97 | 97 | |
| 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> | |
| 99 | 99 | |
| 100 | 100 | {{-- A same-origin fetch, not a native POST — the submit is intercepted. --}} |
| 101 | 101 | <form action="/api/auth/register" method="POST" @submit="onRegister" class="space-y-4"> |
| 102 | 102 | <div> |
| 103 | 103 | <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' : ''"> | |
| 105 | 105 | </div> |
| 106 | 106 | <div> |
| 107 | 107 | <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' : ''"> | |
| 109 | 109 | </div> |
| 110 | 110 | <div> |
| 111 | 111 | <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' : ''"> | |
| 113 | 113 | </div> |
| 114 | 114 | |
| 115 | 115 | <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> |
| @@ -85,16 +85,16 @@ const action = async ({ form }: { form: Record<string, string | string[]> }) => | ||
| 85 | 85 | <input type="hidden" name="token" value=""> |
| 86 | 86 | <input type="hidden" name="email" value=""> |
| 87 | 87 | @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> | |
| 89 | 89 | @endif |
| 90 | 90 | |
| 91 | 91 | <div> |
| 92 | 92 | <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> | |
| 94 | 94 | </div> |
| 95 | 95 | <div> |
| 96 | 96 | <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> | |
| 98 | 98 | </div> |
| 99 | 99 | |
| 100 | 100 | {{-- A plain button; no client mutation to bind a loading state to. --}} |
| @@ -1455,10 +1455,7 @@ onMount(() => { | ||
| 1455 | 1455 | <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> |
| 1456 | 1456 | </div> |
| 1457 | 1457 | @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." /> | |
| 1462 | 1459 | @endforelse |
| 1463 | 1460 | </div> |
| 1464 | 1461 | <div class="px-5 py-5 app-panel"> |
| @@ -0,0 +1,24 @@ | ||
| 1 | import { describe, expect, test } from 'bun:test' | |
| 2 | import theme from '../../config/crosswind' | |
| 3 | // STX exposes its selected CSS engine through these installed files. | |
| 4 | import { loadCssEngine } from '../../node_modules/@stacksjs/stx/dist/dev-server/ts-css.js' | |
| 5 | import { SEMANTIC_TOKENS, SHAPE_TOKENS } from '../../node_modules/@stacksjs/stx/dist/theme-tokens.js' | |
| 6 | import { mergeCssConfig } from '../../node_modules/@stacksjs/stx/dist/ts-css-config.js' | |
| 7 | ||
| 8 | describe('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 | }) | |
| @@ -9,6 +9,11 @@ const marketing = readFileSync(join(ROOT, 'public/marketing.css'), 'utf8') | ||
| 9 | 9 | // taller than an 812px viewport. The panel has to scroll inside the viewport |
| 10 | 10 | // and the page behind it has to stay put, or the last links are unreachable. |
| 11 | 11 | describe('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 | ||
| 12 | 17 | test('marketing menu is bounded by the visible mobile viewport', () => { |
| 13 | 18 | const panel = marketing.match(/\.nav-menu-panel\s*\{[^}]*\}/)?.[0] ?? '' |
| 14 | 19 | expect(panel).toContain('max-height: calc(100dvh - 92px)') |