mirror of
https://github.com/hanzoai/store.git
synced 2026-10-30 14:54:46 +00:00
`.btn` is not a rule @hanzo/ui ships — it is a stable class HANDLE it stamps on
a control alongside the @hanzo/gui atomics that actually paint it, so a host can
hook buttons from CSS. This app hooked it, to refuse the user-agent underline on
the five `<Button asChild>` controls that render as anchors.
It was refusing nothing. Measured on the built site: 606 `a.btn` elements, and
deleting the rule at runtime leaves every one of them computing
`text-decoration-line: none` — ui's own atomics already set it. And there is no
first-paint window where it could have mattered either: the store renders its
buttons on the client, so `.btn` does not appear in the exported HTML at all.
That left a store-owned meaning sitting on ui's handle for no benefit, which is
the thing to avoid — one class name, one owner. The store now defines nothing
for the handles and hooks none of them, so `btn` joins the other nine in
gui-css-check.json.
Recorded there, and worth fixing upstream: @hanzo/gui 8.0.2's css-check ships a
DEFAULT_ALLOW naming `hanzo-button` / `hanzo-badge` — the handles retired in
'adopt @hanzo/ui 8.0.44 class handles'. The defaults have not caught up, which
is the only reason this repo carries a gui-css-check.json at all. When a gui
release allows btn/badge by default, delete the file.
Also documents why this app imports @hanzo/ui/theme.css and not styles.css:
styles.css ends with a bare unscoped `:root{--background:var(--t189)}` that is
near-white, beaten only by a block scoped to gui's own `t_dark` class — which
next-themes never writes. Import it last and the dark ground goes white with
every check still green. theme.css sets --background to #0a0a0a directly.
Asserted with numbers rather than trusted: dark is rgb(20,20,20) ground on
rgb(255,255,255) ink at 18.42:1, light is rgb(247,247,247) on rgb(5,5,5) at
19.02:1 — the ground flips and both clear AAA.
Co-authored-by: Hanzo Dev <[email protected]>
357 lines
10 KiB
CSS
357 lines
10 KiB
CSS
/* The design tokens are NOT authored here. `@hanzo/ui/theme.css` is the one
|
|
place the Hanzo colour ladder, radii, type face and elevation are defined, and
|
|
every Hanzo surface reads that same file — a local copy of the semantic block
|
|
is exactly how two surfaces drift a shade apart without anyone editing
|
|
anything. The values `@hanzo/gui` resolves at runtime come from the same
|
|
source, so CSS and components cannot disagree.
|
|
|
|
The faces themselves are a SEPARATE import, and it is not optional. theme.css
|
|
only NAMES the fonts (`--font-sans: "Geist", …`); @hanzo/design owns the
|
|
@font-face rules and ships the woff2 files beside them, so the urls resolve
|
|
from inside node_modules with no config. Omit this line and nothing fails —
|
|
the build is green, gui-css-check is green, and every page quietly renders in
|
|
the `ui-sans-serif, system-ui` fallback. That is why the e2e pass asserts
|
|
`document.fonts.check('16px Geist')` and not just a class name.
|
|
|
|
Self-hosted on purpose: the store makes no third-party request for type. */
|
|
@import '@hanzo/design/tokens/fonts.css';
|
|
@import '@hanzo/ui/theme.css';
|
|
|
|
html,
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
background: var(--background);
|
|
color: var(--foreground);
|
|
-webkit-font-smoothing: antialiased;
|
|
/* A store is one column on a phone: nothing may scroll the body sideways. */
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* ── Page chrome ──────────────────────────────────────────────────────────────
|
|
Document-level layout — the gutter, the sticky bar, the card grid. This is
|
|
the layer @hanzo/gui deliberately does not cover: its stacks are flexbox on
|
|
every platform, and a responsive column grid is a CSS-only idea. Everything
|
|
INSIDE a card is composed from @hanzo/ui components and gui style props;
|
|
everything that positions the page is here. Two concerns, two vocabularies,
|
|
no overlap. */
|
|
.page {
|
|
min-height: 100vh;
|
|
background: var(--background);
|
|
}
|
|
|
|
.container {
|
|
max-width: 80rem;
|
|
margin: 0 auto;
|
|
padding-inline: 1rem;
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
.container {
|
|
padding-inline: 1.5rem;
|
|
}
|
|
}
|
|
|
|
@media (min-width: 1024px) {
|
|
.container {
|
|
padding-inline: 2rem;
|
|
}
|
|
}
|
|
|
|
.topbar {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
border-bottom: 1px solid var(--border);
|
|
background: color-mix(in oklab, var(--card) 70%, transparent);
|
|
backdrop-filter: blur(12px);
|
|
}
|
|
|
|
.bar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
padding-block: 1rem;
|
|
}
|
|
|
|
.brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
color: var(--foreground);
|
|
}
|
|
|
|
/* The mark is a fixed size, not a flexible one. As a plain flex child it took
|
|
its share of the squeeze when the bar ran out of room: 48px on a desktop,
|
|
17px at 390px, and 0 — gone entirely — at 320px. The title beside it is the
|
|
part that gives. */
|
|
.brand > :first-child {
|
|
flex: none;
|
|
}
|
|
|
|
/* …and "gives" has to be said out loud, because the words are a @hanzo/gui box
|
|
and gui's boxes are React Native boxes: there `flex-shrink` defaults to 0,
|
|
not to the web's 1. So the title block held its full 184px, overflowed the
|
|
.brand it lives in, and at 390 its right edge landed at 264px while the
|
|
Connect Wallet button's left edge landed at 257px — the button is opaque, so
|
|
it painted over the tail of the word. `min-width: 0` is the other half of the
|
|
fix: a flex item will not shrink past its content's intrinsic width without
|
|
it, so shrink alone would have changed nothing. */
|
|
.brand > :not(:first-child) {
|
|
min-width: 0;
|
|
flex-shrink: 1;
|
|
}
|
|
|
|
.back {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
font-size: 0.875rem;
|
|
color: var(--muted-foreground);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.back:hover {
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.main {
|
|
padding-block: 3rem;
|
|
}
|
|
|
|
/* One column on a phone, two from tablet, three on a desktop. */
|
|
.cards {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 1.5rem;
|
|
margin-top: 1.5rem;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.cards {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
}
|
|
|
|
@media (min-width: 1024px) {
|
|
.cards {
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
}
|
|
}
|
|
|
|
.app-icon {
|
|
width: 4rem;
|
|
height: 4rem;
|
|
flex-shrink: 0;
|
|
border-radius: var(--radius);
|
|
object-fit: cover;
|
|
}
|
|
|
|
/* The app page shows the same mark, larger. One rule, one modifier — a second
|
|
`.hero-icon` block would be the same icon described twice. */
|
|
.app-icon.lg {
|
|
width: 6rem;
|
|
height: 6rem;
|
|
}
|
|
|
|
/* No artwork: the initial, in the icon's own box. */
|
|
.app-icon.fallback {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--secondary);
|
|
color: var(--secondary-foreground);
|
|
font-size: 2.25rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* ── App page ─────────────────────────────────────────────────────────────────
|
|
One column on a phone; the record on the left and the install/metadata rail
|
|
on the right from a laptop up. */
|
|
.detail {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
align-items: start;
|
|
gap: 2rem;
|
|
}
|
|
|
|
@media (min-width: 1024px) {
|
|
.detail {
|
|
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
|
|
}
|
|
}
|
|
|
|
.shot {
|
|
width: 100%;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
/* Commands and config are prose the theme already typesets — theme.css binds
|
|
`code`/`pre` to Geist Mono, so this only gives the block its box. */
|
|
.code {
|
|
margin: 0;
|
|
padding: 0.75rem;
|
|
border-radius: var(--radius);
|
|
background: var(--muted);
|
|
color: var(--foreground);
|
|
font-size: 0.8125rem;
|
|
line-height: 1.6;
|
|
overflow-x: auto;
|
|
white-space: pre-wrap;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.site-footer {
|
|
margin-top: 5rem;
|
|
padding-block: 3rem;
|
|
border-top: 1px solid var(--border);
|
|
text-align: center;
|
|
}
|
|
|
|
.links {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
gap: 1.5rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.links a {
|
|
color: var(--muted-foreground);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.links a:hover {
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.copyright {
|
|
margin: 1.5rem 0 0;
|
|
font-size: 0.8125rem;
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
/* ── Documents ────────────────────────────────────────────────────────────────
|
|
Guidelines / Terms / Privacy are prose, not product chrome: long-form legal
|
|
copy whose structure IS its markup. So they are written as semantic HTML and
|
|
typeset once, here, instead of carrying a utility class on every heading and
|
|
list item. One scope, one place to retune the reading experience, and the
|
|
pages stay diffable against the legal text they mirror. */
|
|
.doc {
|
|
max-width: 56rem;
|
|
margin: 0 auto;
|
|
padding: 3rem 1rem;
|
|
font-size: 0.9375rem;
|
|
line-height: 1.7;
|
|
color: var(--foreground);
|
|
}
|
|
|
|
.doc h1 {
|
|
margin: 0 0 1rem;
|
|
font-size: 2.25rem;
|
|
font-weight: 700;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.doc h2 {
|
|
margin: 2.5rem 0 1rem;
|
|
font-size: 1.5rem;
|
|
font-weight: 600;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.doc h3 {
|
|
margin: 1.75rem 0 0.5rem;
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.doc p {
|
|
margin: 0 0 1rem;
|
|
}
|
|
|
|
.doc ul {
|
|
margin: 0 0 1rem;
|
|
padding-left: 1.5rem;
|
|
list-style: disc;
|
|
}
|
|
|
|
.doc ul.plain {
|
|
padding-left: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
.doc li {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.doc a {
|
|
color: var(--foreground);
|
|
text-decoration: underline;
|
|
text-underline-offset: 3px;
|
|
}
|
|
|
|
.doc .muted {
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
/* A callout is prose too — a boxed digest of the section it opens, typeset by
|
|
the document rather than by importing a component vocabulary mid-text. */
|
|
.doc .callout {
|
|
display: block;
|
|
margin: 0 0 2rem;
|
|
padding: 1.5rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: var(--card);
|
|
}
|
|
|
|
.doc .callout h2 {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.doc .callout ul:last-child,
|
|
.doc .callout p:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
/* The closing affirmation on every document — smaller, quieter, ruled off. */
|
|
.doc .coda {
|
|
margin-top: 3rem;
|
|
padding-top: 2rem;
|
|
border-top: 1px solid var(--border);
|
|
font-size: 0.875rem;
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
/* ── RainbowKit ───────────────────────────────────────────────────────────────
|
|
The wallet modal is a third-party surface with its own variable namespace.
|
|
Rather than restating a palette, each of its variables is bound to the Hanzo
|
|
token that already means the same thing, so the modal moves when the theme
|
|
moves. This is the only place in the app that names --rk-*. */
|
|
[data-rk] {
|
|
--rk-colors-accentColor: var(--primary) !important;
|
|
--rk-colors-accentColorForeground: var(--primary-foreground) !important;
|
|
--rk-colors-modalBackground: var(--popover) !important;
|
|
--rk-colors-modalBorder: var(--border) !important;
|
|
--rk-colors-modalText: var(--popover-foreground) !important;
|
|
--rk-colors-modalTextSecondary: var(--muted-foreground) !important;
|
|
--rk-colors-modalTextDim: var(--muted-foreground) !important;
|
|
--rk-colors-generalBorder: var(--border) !important;
|
|
--rk-colors-generalBorderDim: var(--border) !important;
|
|
--rk-colors-actionButtonBorder: var(--border) !important;
|
|
--rk-colors-actionButtonSecondaryBackground: var(--secondary) !important;
|
|
--rk-colors-connectButtonBackground: var(--card) !important;
|
|
--rk-colors-connectButtonText: var(--card-foreground) !important;
|
|
--rk-colors-connectButtonInnerBackground: var(--secondary) !important;
|
|
--rk-colors-menuItemBackground: var(--secondary) !important;
|
|
--rk-colors-closeButton: var(--muted-foreground) !important;
|
|
--rk-colors-closeButtonBackground: var(--secondary) !important;
|
|
--rk-radii-modal: var(--radius) !important;
|
|
--rk-radii-connectButton: var(--radius) !important;
|
|
--rk-radii-actionButton: var(--radius) !important;
|
|
--rk-radii-menuButton: var(--radius) !important;
|
|
}
|