ReviewOS

also looking at this

stacks/commshq

fix(ui): align component palette and contain homepage hero

#16
Merged glennmichael123 wants to merge fix/commshq-component-theme into main
7 files +157 -13
config/crosswind.tsmodified+80-12
Changes to config/crosswind.ts
@@ -1,15 +1,83 @@
1import type { CrosswindConfig, Theme } from '@cwcss/crosswind'
2
13/**
2 * Crosswind (utility CSS) — content globs for STX views.
3 * @see https://github.com/cwcss/crosswind
4 * Component roles use the existing editorial palette on marketing/auth pages.
5 * component-tokens.css supplies dashboard defaults where no palette is declared.
6 * Use theme entries rather than layered preflight overrides of library tokens.
47 */
58export default {
6 content: [
7 './resources/views/**/*.{stx,html}',
8 './resources/**/*.{stx,html}',
9 './storage/framework/defaults/resources/views/**/*.{stx,html}',
10 './storage/framework/defaults/resources/components/**/*.{stx,html}',
11 './storage/framework/core/error-handling/src/views/**/*.{stx,html}',
12 ],
13 preflight: true,
14 minify: false,
15}
9 theme: {
10 extend: {
11 borderRadius: {
12 control: '10px',
13 panel: '12px',
14 pill: '999px',
15 },
16 colors: {
17 'panel': 'var(--panel)',
18 'line': 'var(--line)',
19 'accent': 'var(--coral)',
20 'accent-ink': 'var(--coral-ink)',
21 'accent-soft': 'color-mix(in srgb, var(--coral) 8%, var(--panel))',
22 'surface': 'var(--panel)',
23 'surface-sunken': 'var(--paper)',
24 'surface-raised': 'color-mix(in srgb, var(--coral) 8%, var(--panel))',
25 'surface-hover': 'color-mix(in srgb, var(--ink) 6%, var(--panel))',
26 'surface-raised-hover': 'color-mix(in srgb, var(--coral) 14%, var(--panel))',
27 'surface-sunken-hover': 'color-mix(in srgb, var(--ink) 6%, var(--paper))',
28 'page': 'var(--paper)',
29 'content': 'var(--panel)',
30 'field': 'var(--panel)',
31 'field-hover': 'color-mix(in srgb, var(--ink) 6%, var(--panel))',
32 'fg': 'var(--ink)',
33 'fg-strong': 'var(--ink)',
34 'fg-muted': 'var(--muted)',
35 'fg-soft': 'var(--muted)',
36 'fg-subtle': 'var(--muted)',
37 'line-strong': 'color-mix(in srgb, var(--muted) 55%, var(--line))',
38 'line-hover': 'color-mix(in srgb, var(--muted) 70%, var(--line))',
39 'link': 'var(--coral)',
40 'link-hover': 'color-mix(in srgb, var(--coral) 85%, var(--ink))',
41 'accent-solid': 'var(--coral)',
42 'accent-solid-hover': 'color-mix(in srgb, var(--coral) 92%, var(--ink))',
43 'accent-soft-ink': 'color-mix(in srgb, var(--coral) 85%, var(--ink))',
44 'success': 'var(--comms-success)',
45 'success-solid': 'var(--comms-success)',
46 'success-solid-hover': 'color-mix(in srgb, var(--comms-success) 92%, var(--ink))',
47 'success-ink': 'var(--coral-ink)',
48 'success-soft': 'color-mix(in srgb, var(--comms-success) 8%, var(--panel))',
49 'success-soft-ink': 'color-mix(in srgb, var(--comms-success) 85%, var(--ink))',
50 'danger': 'var(--comms-danger)',
51 'danger-solid': 'var(--comms-danger)',
52 'danger-solid-hover': 'color-mix(in srgb, var(--comms-danger) 92%, var(--ink))',
53 'danger-ink': 'var(--coral-ink)',
54 'danger-soft': 'color-mix(in srgb, var(--comms-danger) 8%, var(--panel))',
55 'danger-soft-ink': 'color-mix(in srgb, var(--comms-danger) 85%, var(--ink))',
56 'warning': 'var(--muted)',
57 'warning-solid': 'var(--muted)',
58 'warning-solid-hover': 'color-mix(in srgb, var(--muted) 92%, var(--ink))',
59 'warning-ink': 'var(--coral-ink)',
60 'warning-soft': 'color-mix(in srgb, var(--muted) 8%, var(--panel))',
61 'warning-soft-ink': 'color-mix(in srgb, var(--muted) 85%, var(--ink))',
62 'info': 'var(--muted)',
63 'info-solid': 'var(--muted)',
64 'info-solid-hover': 'color-mix(in srgb, var(--muted) 92%, var(--ink))',
65 'info-ink': 'var(--coral-ink)',
66 'info-soft': 'color-mix(in srgb, var(--muted) 8%, var(--panel))',
67 'info-soft-ink': 'color-mix(in srgb, var(--muted) 85%, var(--ink))',
68 'secondary': 'var(--muted)',
69 'secondary-solid': 'var(--muted)',
70 'secondary-solid-hover': 'color-mix(in srgb, var(--muted) 92%, var(--ink))',
71 'secondary-ink': 'var(--coral-ink)',
72 'secondary-soft': 'color-mix(in srgb, var(--muted) 8%, var(--panel))',
73 'secondary-soft-ink': 'color-mix(in srgb, var(--muted) 85%, var(--ink))',
74 'danger-fg': 'var(--comms-danger)',
75 'danger-fg-subtle': 'var(--comms-danger)',
76 'danger-line': 'var(--comms-danger)',
77 'danger-focus': 'var(--comms-danger)',
78 'inverse': 'var(--ink)',
79 'inverse-ink': 'var(--paper)',
80 },
81 },
82 },
83} satisfies Partial<Omit<CrosswindConfig, 'theme'>> & { theme?: Pick<Theme, 'extend'> }
config/ui.tsmodified+6-0
Changes to config/ui.ts
@@ -35,6 +35,12 @@ export default {
3535
3636 partialsDir: 'partials',
3737
38 app: {
39 head: {
40 link: [{ rel: 'stylesheet', href: '/component-tokens.css' }],
41 },
42 },
43
3844 /*
3945 * stx's client-script DOM guard: 25 rules at script-validation.js:1-127,
4046 * run by process.js on every non-server <script> body. serve.js forwards the
public/component-tokens.cssadded+28-0
Changes to public/component-tokens.css
@@ -0,0 +1,28 @@
1/* Component defaults for the workspace, which otherwise uses literal colors.
2 * Zero specificity lets marketing/auth's existing :root palettes win regardless
3 * of stylesheet order. Theme selection still belongs to the existing bootstrap.
4 * Success/error hues match WorkspaceSurface; warning/info remain neutral.
5 */
6:where(:root) {
7 --paper: #f6f3ee;
8 --panel: #ffffff;
9 --ink: #171916;
10 --muted: #686b65;
11 --line: rgba(23, 25, 22, 0.15);
12 --coral: #bd3e2e;
13 --coral-ink: #ffffff;
14 --comms-success: #466833;
15 --comms-danger: #8f3025;
16}
17
18:where(:root.dark) {
19 --paper: #111210;
20 --panel: #1b1c1a;
21 --ink: #f5f2ec;
22 --muted: #aaa9a3;
23 --line: rgba(255, 255, 255, 0.15);
24 --coral: #ff8a78;
25 --coral-ink: #191715;
26 --comms-success: #acd294;
27 --comms-danger: #ffab9a;
28}
resources/views/index.stxmodified+1-1
Changes to resources/views/index.stx
@@ -80,7 +80,7 @@ const plans = [
8080 </div>
8181 <p class="mt-4 text-sm muted">No card required. Email stays free for your first 1,000 contacts.</p>
8282 </div>
83 <figure class="relative m-0 lg:-mr-16">
83 <figure class="relative m-0 min-w-0">
8484 <div class="overflow-hidden border border-tone rounded-[2rem] shadow-2xl">
8585 <img src="/images/commshq-editorial-hero.png" alt="A coral mailer, notebook, phone, and editorial cards arranged on a warm studio table" class="object-cover h-full min-h-[420px] w-full" width="1536" height="1024">
8686 </div>
stx.config.tsmodified+3-0
Changes to stx.config.ts
@@ -1,8 +1,11 @@
11import type { StxConfig } from '@stacksjs/stx'
2import ui from './config/ui'
23
34const config: Partial<StxConfig> = {
45 root: 'resources',
56 layoutsDir: 'views/layouts',
7 // This root config takes precedence over config/ui.ts on the serve/build path.
8 app: ui.app,
69 build: {
710 sitemapExclude: ['/dashboard/'],
811 },
tests/Unit/HomepageLayout.test.tsadded+10-0
Changes to tests/Unit/HomepageLayout.test.ts
@@ -0,0 +1,10 @@
1import { expect, test } from 'bun:test'
2import { readFileSync } from 'node:fs'
3
4const home = readFileSync(new URL('../../resources/views/index.stx', import.meta.url), 'utf8')
5
6test('the hero image stays inside its grid column at desktop widths', () => {
7 const figure = home.match(/<figure class="([^"]*)">/)?.[1] ?? ''
8 expect(figure).toContain('min-w-0')
9 expect(figure).not.toMatch(/(?:^|\s)(?:\w+:)?-mr-/)
10})
tests/Unit/component-theme.test.tsadded+29-0
Changes to tests/Unit/component-theme.test.ts
@@ -0,0 +1,29 @@
1import { describe, expect, test } from 'bun:test'
2import theme from '../../config/crosswind'
3import stx from '../../stx.config'
4// STX exposes its selected CSS engine through these installed files.
5import { loadCssEngine } from '../../node_modules/@stacksjs/stx/dist/dev-server/ts-css.js'
6import { SEMANTIC_TOKENS, SHAPE_TOKENS } from '../../node_modules/@stacksjs/stx/dist/theme-tokens.js'
7import { mergeCssConfig } from '../../node_modules/@stacksjs/stx/dist/ts-css-config.js'
8
9describe('component palette contract', () => {
10 test('the root STX config delivers the component palette on every page', () => {
11 expect(stx.app?.head?.link).toContainEqual({ rel: 'stylesheet', href: '/component-tokens.css' })
12 })
13
14 test('every component color and radius emits an app-owned utility', async () => {
15 const engine = await loadCssEngine()
16 if (!engine) throw new Error('The installed STX CSS engine is required')
17 const generator = new engine.CSSGenerator(mergeCssConfig(engine.defaultConfig || engine.config, theme).config)
18 for (const role of Object.keys(SEMANTIC_TOKENS)) generator.generate(`bg-${role}`)
19 for (const role of Object.keys(SHAPE_TOKENS)) generator.generate(`rounded-${role}`)
20 const css = generator.toCSS(false, false)
21 expect(css).not.toContain('var(--stx-')
22 for (const role of Object.keys(SEMANTIC_TOKENS)) expect(css).toContain(`.bg-${role} {`)
23 for (const role of Object.keys(SHAPE_TOKENS)) expect(css).toContain(`.rounded-${role} {`)
24 expect(css).toContain('.bg-content {\n background-color: var(--panel);')
25 expect(css).toContain('.bg-accent-solid {\n background-color: var(--coral);')
26 expect(css).toContain('.rounded-control {\n border-radius: 10px;')
27 expect(css).toContain('.rounded-panel {\n border-radius: 12px;')
28 })
29})