Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions public/cbox/tokens/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@
.cbx-kv { display: grid; grid-template-columns: 140px 1fr; align-items: baseline; column-gap: 24px; padding: 12px 0; border-bottom: 1px solid var(--border); }
.cbx-kv:last-child { border-bottom: 0; }
.cbx-kv dt { text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.6875rem; font-weight: 600; color: var(--muted-foreground); }

/* A small section label above a grouped control (the invoice action panels use it). It was
referenced in Blade with no rule behind it, so those headings rendered as plain body text —
indistinguishable from the content beneath them. Matches the `.cbx-kv dt` treatment so the
two read as one system. */
.cbx-label { text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.6875rem; font-weight: 600; color: var(--muted-foreground); }
.cbx-kv dd { margin: 0; font-family: var(--font-mono); font-size: 13px; word-break: break-all; color: var(--foreground); }
.cbx-kv dd.prose { font-family: var(--font-sans); font-size: 14px; }

Expand Down
2 changes: 1 addition & 1 deletion resources/views/billing/import/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
@elseif ($run->status === 'running')
<span class="cbx-pill cbx-pill--warning">running</span>
@elseif ($run->status === 'failed')
<span class="cbx-pill cbx-pill--danger">failed</span>
<span class="cbx-pill cbx-pill--destructive">failed</span>
@else
<span class="cbx-pill cbx-pill--muted">planned (dry-run)</span>
@endif
Expand Down
2 changes: 1 addition & 1 deletion resources/views/billing/import/plan.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
'updated' => 'cbx-pill--info',
'skipped' => 'cbx-pill--muted',
'conflict' => 'cbx-pill--warning',
'failed' => 'cbx-pill--danger',
'failed' => 'cbx-pill--destructive',
default => 'cbx-pill--muted',
};
@endphp
Expand Down
2 changes: 1 addition & 1 deletion resources/views/billing/import/run.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
'updated' => 'cbx-pill--info',
'skipped' => 'cbx-pill--muted',
'conflict' => 'cbx-pill--warning',
'failed' => 'cbx-pill--danger',
'failed' => 'cbx-pill--destructive',
default => 'cbx-pill--muted',
};
@endphp
Expand Down
4 changes: 2 additions & 2 deletions resources/views/billing/nexus.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<p class="cbx-page-desc" style="font-size:13px">Where your default selling entity has — or is approaching — a US sales-tax registration obligation. Thresholds come from the us-tax-data dataset; sales from your invoices (every currency, valued in USD) plus any external-channel sales you record below. <strong>Triggered</strong> = register now; <strong>Approaching</strong> = watch.</p>
</div>
<div style="display:flex;gap:8px;align-items:center">
<span class="cbx-pill cbx-pill--danger">{{ $triggeredCount }} triggered</span>
<span class="cbx-pill cbx-pill--destructive">{{ $triggeredCount }} triggered</span>
<span class="cbx-pill cbx-pill--warning">{{ $approachingCount }} approaching</span>
<span class="cbx-pill cbx-pill--success">{{ $registeredCount }} registered</span>
@if ($unknownCount > 0)<span class="cbx-pill cbx-pill--warning">{{ $unknownCount }} unknown</span>@endif
Expand Down Expand Up @@ -76,7 +76,7 @@
<td class="mut" style="font-size:12.5px">{{ $e->threshold?->describe() ?? '—' }}</td>
<td class="num">{{ $e->progress !== null ? number_format($e->progress * 100, 1).'%' : '—' }}</td>
<td style="font-size:11.5px">
@if ($e->physicalPresence)<span class="cbx-pill cbx-pill--neutral">presence</span>@endif
@if ($e->physicalPresence)<span class="cbx-pill cbx-pill--muted">presence</span>@endif
@if (in_array($e->state->value, $registeredStates, true))<span class="cbx-pill cbx-pill--success">registered</span>@endif
</td>
</tr>
Expand Down
2 changes: 1 addition & 1 deletion resources/views/layouts/app.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ class="cbx-btn cbx-btn--sm cbx-btn--ghost @if(! empty($testMode)) cbx-pill--warn
@if (! empty($environments))
<details class="cbx-env-clone" style="position:relative">
<summary class="cbx-btn cbx-btn--sm cbx-btn--ghost" style="height:28px;list-style:none;cursor:pointer;display:inline-flex;align-items:center;gap:5px" title="Clone an environment's config into a new sandbox">@include('partials.icon', ['name' => 'copy', 'size' => 13, 'sw' => 1.7])<span>Clone</span></summary>
<form method="POST" action="{{ route('billing.environment.clone') }}" class="cbx-card"
<form method="POST" action="{{ route('billing.environment.clone') }}" class="cbx-panel"
style="position:absolute;top:calc(100% + 6px);right:0;z-index:40;width:280px;padding:12px;display:flex;flex-direction:column;gap:8px">
@csrf
<div style="font-size:11px;font-weight:600;color:var(--muted-foreground)">Clone environment config</div>
Expand Down
143 changes: 143 additions & 0 deletions tests/Feature/DesignSystemClassesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php

declare(strict_types=1);

namespace Tests\Feature;

use Tests\TestCase;

/**
* Every `cbx-*` class used in a Blade template must have a rule behind it.
*
* A class name with no rule fails SILENTLY — the element just renders unstyled, and in a
* console that mostly looks fine, so nothing draws attention to it. The review found four:
*
* - `.cbx-pill--danger` (the design system's variant is `--destructive`). On /console/nexus
* that made "3 triggered" — meaning US economic-nexus thresholds are crossed and the seller
* has an unregistered tax liability — render as the same neutral chip as any inert label.
* The severity signal was dropped entirely, on the page whose only job is severity.
* - `.cbx-pill--neutral`, which happened to coincide with the default and so was harmless.
* - `.cbx-card` on the console topbar's clone popover: no background, no border, no shadow,
* so the form rendered transparent over the table beneath it — on every console page.
* - `.cbx-label`, which left section headings looking like body text.
*
* A rendering test would not have caught any of these: the markup was valid and the page
* returned 200. Only comparing the two sources does.
*/
class DesignSystemClassesTest extends TestCase
{
/**
* Class names that carry NO rule and are not meant to.
*
* These elements are styled by inline `style=` attributes, so nothing renders wrong — the
* class is a leftover semantic hook. That is cosmetic debt, not the defect this test exists
* to catch, which is an element that DEPENDS on a class for its appearance and silently
* loses it (`.cbx-pill--danger` dropping the severity colour on the nexus page).
*
* Each entry is a deliberate acceptance, not a blanket allowance: a NEW undefined class
* still fails. Tracked for cleanup alongside the inline-style dialect they belong to.
*/
private const array EXEMPT = [
'cbx-testmode-strip',
'cbx-strip-expand',
'cbx-env-clone',
'cbx-input--sm',
'cbx-confirm-plane',
'cbx-pricing',
'cbx-pricing-card',
'cbx-paywall',
'cbx-upgrade',
];

public function test_every_cbx_class_used_in_blade_is_defined_in_the_stylesheets(): void
{
$defined = $this->definedClasses();
$this->assertNotSame([], $defined, 'No cbx-* rules found — the stylesheet paths are wrong.');

$undefined = [];

foreach ($this->bladeFiles() as $file) {
$contents = (string) file_get_contents($file);

// Only `class="..."` attributes, so prose and comments mentioning a class name
// cannot trip the check.
preg_match_all('/class\s*=\s*"([^"]*)"/', $contents, $attributes);

foreach ($attributes[1] as $attribute) {
foreach (preg_split('/\s+/', $attribute) ?: [] as $class) {
if (! str_starts_with($class, 'cbx-') || str_contains($class, '{')) {
continue; // interpolated (`{{ }}`) classes are resolved at runtime
}

if (! in_array($class, $defined, true) && ! in_array($class, self::EXEMPT, true)) {
$undefined[$class][] = basename((string) $file);
}
}
}
}

$this->assertSame([], $undefined, $this->describe($undefined));
}

/** @return list<string> */
private function definedClasses(): array
{
$classes = [];

foreach (glob(public_path('cbox/**/*.css')) ?: [] as $sheet) {
preg_match_all('/\.(cbx-[A-Za-z0-9_-]+)/', (string) file_get_contents($sheet), $matches);
$classes = [...$classes, ...$matches[1]];
}

foreach (glob(public_path('cbox/*.css')) ?: [] as $sheet) {
preg_match_all('/\.(cbx-[A-Za-z0-9_-]+)/', (string) file_get_contents($sheet), $matches);
$classes = [...$classes, ...$matches[1]];
}

// Blade `<style>` blocks count as definitions: the public storefront, paywall and quote
// pages are deliberately self-contained (CSP-safe, no external stylesheet), so their
// rules legitimately live inline. Ignoring them would push working CSS out of the pages
// that are designed to carry it.
foreach ($this->bladeFiles() as $file) {
preg_match_all('/<style[^>]*>(.*?)<\\/style>/s', (string) file_get_contents($file), $blocks);

foreach ($blocks[1] as $block) {
preg_match_all('/\\.(cbx-[A-Za-z0-9_-]+)/', $block, $matches);
$classes = [...$classes, ...$matches[1]];
}
}

return array_values(array_unique($classes));
}

/** @return list<string> */
private function bladeFiles(): array
{
$files = [];
$directory = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(resource_path('views')));

foreach ($directory as $file) {
if ($file instanceof \SplFileInfo && str_ends_with($file->getFilename(), '.blade.php')) {
$files[] = $file->getPathname();
}
}

return $files;
}

/** @param array<string, list<string>> $undefined */
private function describe(array $undefined): string
{
if ($undefined === []) {
return '';
}

$lines = ['These cbx-* classes are used in Blade but defined in no stylesheet, so they render as nothing:'];

foreach ($undefined as $class => $files) {
$lines[] = sprintf(' .%s — %s', $class, implode(', ', array_unique($files)));
}

return implode("\n", $lines);
}
}
Loading