ReviewOS

also looking at this

loghqorg/loghq

fix(ui): align component theme and mobile subscription layout

#10
Merged glennmichael123 wants to merge fix/loghq-component-theme into main
4 files +90 -15
config/crosswind.tsmodified+55-14
Changes to config/crosswind.ts
@@ -47,6 +47,8 @@ export default {
4747 theme: {
4848 extend: {
4949 borderRadius: {
50 control: '10px',
51 pill: '999px',
5052 /*
5153 * `rounded-panel` is @stacksjs/components' radius role name:
5254 * <EmptyState variant="panel"> renders `bg-panel rounded-panel
@@ -83,28 +85,67 @@ export default {
8385 // right in both. See public/tokens.css.
8486 'accent-ink': 'var(--accent-ink)',
8587
86 // --- @stacksjs/components' token vocabulary ------------------------
87 // The shipped components are written against their own semantic names
88 // (text-fg, bg-surface, border-line-strong, ...) used 300+ times across
89 // the 102 components. This palette already satisfied `line` and
90 // `accent`; the rest resolved to nothing, so a component that imposes
91 // no colour of its own still rendered half-styled - text fell back to
92 // inherit and panels had no background.
93 //
94 // Purely additive: none of these names appear in this app's markup, so
95 // no existing element changes. Does NOT fix <Button variant="primary">,
96 // which hard-codes bg-blue-500 - that is stacksjs/stx#1993.
97 // See statushqorg/status#20.
98 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)',
9992 'surface-sunken': 'var(--bg)',
10093 'surface-raised': 'color-mix(in srgb, var(--accent) 8%, var(--panel))',
101 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)',
102102 'fg-strong': 'var(--text)',
103103 'fg-muted': 'var(--text-2)',
104104 'fg-soft': 'var(--text-2)',
105105 'fg-subtle': 'var(--text-3)',
106106 '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))',
107110 '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)',
108149 },
109150 // The site's own two breakpoints, as min-widths. public/marketing.css
110151 // states them as `max-width: 560px` and `max-width: 900px`; Crosswind
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; }
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)')