also looking at this
fix(ui): align component palette and contain homepage hero
#16
7 files
+157
-13
| @@ -1,15 +1,83 @@ | ||
| 1 | import type { CrosswindConfig, Theme } from '@cwcss/crosswind' | |
| 2 | ||
| 1 | 3 | /** |
| 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. | |
| 4 | 7 | */ |
| 5 | 8 | export 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'> } | |
| @@ -35,6 +35,12 @@ export default { | ||
| 35 | 35 | |
| 36 | 36 | partialsDir: 'partials', |
| 37 | 37 | |
| 38 | app: { | |
| 39 | head: { | |
| 40 | link: [{ rel: 'stylesheet', href: '/component-tokens.css' }], | |
| 41 | }, | |
| 42 | }, | |
| 43 | ||
| 38 | 44 | /* |
| 39 | 45 | * stx's client-script DOM guard: 25 rules at script-validation.js:1-127, |
| 40 | 46 | * run by process.js on every non-server <script> body. serve.js forwards the |
| @@ -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 | } | |
| @@ -80,7 +80,7 @@ const plans = [ | ||
| 80 | 80 | </div> |
| 81 | 81 | <p class="mt-4 text-sm muted">No card required. Email stays free for your first 1,000 contacts.</p> |
| 82 | 82 | </div> |
| 83 | <figure class="relative m-0 lg:-mr-16"> | |
| 83 | <figure class="relative m-0 min-w-0"> | |
| 84 | 84 | <div class="overflow-hidden border border-tone rounded-[2rem] shadow-2xl"> |
| 85 | 85 | <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"> |
| 86 | 86 | </div> |
| @@ -1,8 +1,11 @@ | ||
| 1 | 1 | import type { StxConfig } from '@stacksjs/stx' |
| 2 | import ui from './config/ui' | |
| 2 | 3 | |
| 3 | 4 | const config: Partial<StxConfig> = { |
| 4 | 5 | root: 'resources', |
| 5 | 6 | layoutsDir: 'views/layouts', |
| 7 | // This root config takes precedence over config/ui.ts on the serve/build path. | |
| 8 | app: ui.app, | |
| 6 | 9 | build: { |
| 7 | 10 | sitemapExclude: ['/dashboard/'], |
| 8 | 11 | }, |
| @@ -0,0 +1,10 @@ | ||
| 1 | import { expect, test } from 'bun:test' | |
| 2 | import { readFileSync } from 'node:fs' | |
| 3 | ||
| 4 | const home = readFileSync(new URL('../../resources/views/index.stx', import.meta.url), 'utf8') | |
| 5 | ||
| 6 | test('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 | }) | |
| @@ -0,0 +1,29 @@ | ||
| 1 | import { describe, expect, test } from 'bun:test' | |
| 2 | import theme from '../../config/crosswind' | |
| 3 | import stx from '../../stx.config' | |
| 4 | // STX exposes its selected CSS engine through these installed files. | |
| 5 | import { loadCssEngine } from '../../node_modules/@stacksjs/stx/dist/dev-server/ts-css.js' | |
| 6 | import { SEMANTIC_TOKENS, SHAPE_TOKENS } from '../../node_modules/@stacksjs/stx/dist/theme-tokens.js' | |
| 7 | import { mergeCssConfig } from '../../node_modules/@stacksjs/stx/dist/ts-css-config.js' | |
| 8 | ||
| 9 | describe('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 | }) | |