feat(canary): UI design layer — vector-minimalism tokens + primitives

Operator (Carter) redirected from infra Phase 15 to UI design layer; design ethos
"vector-minimalism" (specimen-cataloging mind, off-white paper, deep ink, alarm-red
accent, hairline rules, halftone/grain overlays). Foundation only — pages follow.

Tokens: 7-step paper/ink/alarm/signal palette + paper-grain SVG noise + halftone
dot data-uri + hairline width. Stripped color palette in white-canvas commit is
deliberately NOT re-introduced; this is a project-owned palette, not the template's.

Primitives (each in own folder with module SCSS):
- Strip / StripItem    — archive header (vol/issue/serial pills)
- LeaderLabel          — index + rule + caption annotation
- SpecimenCard         — tag + body + serial container (paper|ink|alarm tone)
- DataRow              — field=value with dotted leader (mono|emphasize|alarm)
- Pill                 — paper|ink|alarm|signal label
- CopyField            — labelled mono code with clipboard button
- Halftone             — repeating dot strip (sparse|normal|dense)
- SerialBar            — fingerprint bar + code two-row mono display
- Glyph                — 7 token-type SVG icons (currentColor, 24×24)
- Field (Text/Textarea/Select/Wrap) — labelled form inputs with leader rule
- Button               — primary|ghost|alarm × sm|md|lg

Gates green: pnpm typecheck + pnpm biome check + pnpm lint:scss + pnpm build (294ms).

No backend changes. No page-level routing changes yet.
This commit is contained in:
CarterPerez-dev 2026-05-17 14:39:30 -04:00
parent 1d205c1791
commit f5d893ab8c
25 changed files with 1448 additions and 4 deletions

View File

@ -0,0 +1,89 @@
// ===================
// ©AngelaMos | 2026
// Button.module.scss
// ===================
@use '../../styles/tokens' as *;
@use '../../styles/fonts' as *;
.button {
display: inline-flex;
align-items: center;
justify-content: center;
border: $hairline solid $ink;
background-color: $ink;
color: $paper;
font-family: $font-mono;
letter-spacing: $tracking-wider;
text-transform: uppercase;
cursor: pointer;
transition: background-color $duration-fast $ease-out,
color $duration-fast $ease-out;
&[data-size='sm'] {
padding-block: $space-1-5;
padding-inline: $space-3;
font-size: $font-size-3xs;
}
&[data-size='md'] {
padding-block: $space-2-5;
padding-inline: $space-5;
font-size: $font-size-2xs;
}
&[data-size='lg'] {
padding-block: $space-3-5;
padding-inline: $space-7;
font-size: $font-size-xs;
}
&[data-full='true'] {
inline-size: 100%;
}
&:hover:not(:disabled) {
background-color: $paper;
color: $ink;
}
&[data-variant='ghost'] {
background-color: transparent;
color: $ink;
&:hover:not(:disabled) {
background-color: $ink;
color: $paper;
}
}
&[data-variant='alarm'] {
background-color: $paper;
color: $alarm-deep;
border-color: $alarm;
&:hover:not(:disabled) {
background-color: $alarm;
color: $paper;
}
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
&[data-busy='true'] {
cursor: progress;
.label::after {
content: '';
}
}
}
.label {
display: inline-flex;
align-items: center;
gap: $space-2;
}

View File

@ -0,0 +1,50 @@
// ===================
// ©AngelaMos | 2026
// index.tsx
// ===================
import type { ButtonHTMLAttributes, PropsWithChildren } from 'react'
import { forwardRef } from 'react'
import styles from './Button.module.scss'
type ButtonVariant = 'primary' | 'ghost' | 'alarm'
type ButtonSize = 'sm' | 'md' | 'lg'
type ButtonProps = PropsWithChildren<
Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className'> & {
variant?: ButtonVariant
size?: ButtonSize
fullWidth?: boolean
busy?: boolean
}
>
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
function Button(props, ref) {
const {
variant = 'primary',
size = 'md',
fullWidth = false,
busy = false,
type = 'button',
children,
disabled,
...rest
} = props
return (
<button
{...rest}
ref={ref}
type={type}
disabled={disabled || busy}
className={styles.button}
data-variant={variant}
data-size={size}
data-full={fullWidth}
data-busy={busy}
>
<span className={styles.label}>{children}</span>
</button>
)
}
)

View File

@ -0,0 +1,63 @@
// ===================
// ©AngelaMos | 2026
// CopyField.module.scss
// ===================
@use '../../styles/tokens' as *;
@use '../../styles/fonts' as *;
.field {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: stretch;
border: $hairline solid $rule;
background-color: $paper;
&[data-full='true'] {
inline-size: 100%;
}
}
.label {
display: inline-flex;
align-items: center;
padding-inline: $space-3;
background-color: $ink;
color: $paper;
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
}
.value {
display: inline-flex;
align-items: center;
padding-block: $space-2;
padding-inline: $space-3;
font-family: $font-mono;
font-size: $font-size-xs;
letter-spacing: $tracking-wide;
color: $ink;
overflow-x: auto;
white-space: nowrap;
}
.copy {
padding-block: $space-2;
padding-inline: $space-3;
border-inline-start: $hairline solid $rule;
background-color: $paper-deep;
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $ink;
cursor: pointer;
transition: background-color $duration-fast $ease-out;
&:hover {
background-color: $ink;
color: $paper;
}
}

View File

@ -0,0 +1,49 @@
// ===================
// ©AngelaMos | 2026
// index.tsx
// ===================
import { useState } from 'react'
import { toast } from 'sonner'
import styles from './CopyField.module.scss'
type CopyFieldProps = {
value: string
label?: string
fullWidth?: boolean
}
const COPY_FEEDBACK_MS = 1200
export function CopyField({
value,
label,
fullWidth = false,
}: CopyFieldProps): React.ReactElement {
const [copied, setCopied] = useState(false)
async function handleCopy(): Promise<void> {
try {
await navigator.clipboard.writeText(value)
setCopied(true)
setTimeout(() => setCopied(false), COPY_FEEDBACK_MS)
} catch (_err) {
toast.error('Copy failed — your browser blocked clipboard access')
}
}
return (
<div className={styles.field} data-full={fullWidth}>
{label ? <span className={styles.label}>{label}</span> : null}
<code className={styles.value}>{value}</code>
<button
type="button"
className={styles.copy}
onClick={handleCopy}
aria-label={`Copy ${label ?? 'value'}`}
>
{copied ? 'copied' : 'copy'}
</button>
</div>
)
}

View File

@ -0,0 +1,60 @@
// ===================
// ©AngelaMos | 2026
// DataRow.module.scss
// ===================
@use '../../styles/tokens' as *;
@use '../../styles/fonts' as *;
.row {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: baseline;
gap: $space-2;
font-size: $font-size-sm;
line-height: $line-height-snug;
&[data-alarm='true'] {
color: $alarm-deep;
}
}
.label {
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $ink-mute;
white-space: nowrap;
}
.leader {
align-self: end;
block-size: $hairline;
background-image: linear-gradient(
to right,
$rule-soft 0,
$rule-soft 4px,
transparent 4px,
transparent 8px
);
background-size: 8px $hairline;
transform: translateY(-3px);
min-inline-size: $space-4;
}
.value {
color: inherit;
text-align: right;
overflow-wrap: anywhere;
&[data-mono='true'] {
font-family: $font-mono;
font-size: $font-size-xs;
letter-spacing: $tracking-wide;
}
&[data-emphasize='true'] {
font-weight: $font-weight-semibold;
}
}

View File

@ -0,0 +1,32 @@
// ===================
// ©AngelaMos | 2026
// index.tsx
// ===================
import type { PropsWithChildren, ReactNode } from 'react'
import styles from './DataRow.module.scss'
type DataRowProps = PropsWithChildren<{
label: ReactNode
mono?: boolean
emphasize?: boolean
alarm?: boolean
}>
export function DataRow({
label,
children,
mono = false,
emphasize = false,
alarm = false,
}: DataRowProps): React.ReactElement {
return (
<div className={styles.row} data-alarm={alarm}>
<span className={styles.label}>{label}</span>
<span className={styles.leader} aria-hidden="true" />
<span className={styles.value} data-mono={mono} data-emphasize={emphasize}>
{children}
</span>
</div>
)
}

View File

@ -0,0 +1,108 @@
// ===================
// ©AngelaMos | 2026
// Field.module.scss
// ===================
@use '../../styles/tokens' as *;
@use '../../styles/fonts' as *;
.field {
display: flex;
flex-direction: column;
gap: $space-2;
}
.head {
display: flex;
align-items: center;
gap: $space-2;
}
.index {
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $ink-mute;
white-space: nowrap;
}
.label {
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $ink;
font-weight: $font-weight-semibold;
white-space: nowrap;
}
.required {
color: $alarm;
}
.rule {
flex: 1 1 auto;
block-size: $hairline;
background-color: $rule-hair;
}
.input,
.textarea,
.select {
inline-size: 100%;
padding-block: $space-2-5;
padding-inline: $space-3;
border: $hairline solid $rule;
background-color: $paper;
font-family: $font-sans;
font-size: $font-size-base;
color: $ink;
appearance: none;
border-radius: 0;
transition: border-color $duration-fast $ease-out;
&::placeholder {
color: $ink-faint;
}
&:focus {
outline: none;
border-color: $ink;
box-shadow: inset 0 0 0 $hairline $ink;
}
.field[data-invalid='true'] & {
border-color: $alarm;
}
}
.textarea {
min-block-size: 5lh;
resize: vertical;
font-family: $font-sans;
line-height: $line-height-normal;
}
.select {
padding-inline-end: $space-8;
background-image: linear-gradient(45deg, transparent 50%, $ink 50%),
linear-gradient(135deg, $ink 50%, transparent 50%);
background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
}
.hint {
font-size: $font-size-xs;
color: $ink-mute;
font-family: $font-sans;
}
.error {
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $alarm;
}

View File

@ -0,0 +1,163 @@
// ===================
// ©AngelaMos | 2026
// index.tsx
// ===================
import type {
InputHTMLAttributes,
PropsWithChildren,
ReactNode,
SelectHTMLAttributes,
TextareaHTMLAttributes,
} from 'react'
import { forwardRef, useId } from 'react'
import styles from './Field.module.scss'
type FieldWrapProps = PropsWithChildren<{
label: ReactNode
hint?: ReactNode
error?: ReactNode
index?: string
required?: boolean
htmlFor?: string
}>
export function FieldWrap({
label,
hint,
error,
index,
required,
htmlFor,
children,
}: FieldWrapProps): React.ReactElement {
return (
<div className={styles.field} data-invalid={error ? 'true' : 'false'}>
<div className={styles.head}>
{index ? <span className={styles.index}>{index}</span> : null}
<label className={styles.label} htmlFor={htmlFor}>
{label}
{required ? <span className={styles.required}> *</span> : null}
</label>
<span className={styles.rule} aria-hidden="true" />
</div>
{children}
{hint && !error ? <p className={styles.hint}>{hint}</p> : null}
{error ? (
<p className={styles.error} role="alert">
{error}
</p>
) : null}
</div>
)
}
type TextFieldProps = Omit<
InputHTMLAttributes<HTMLInputElement>,
'className' | 'id'
> & {
label: ReactNode
hint?: ReactNode
error?: ReactNode
index?: string
}
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
function TextField(props, ref) {
const { label, hint, error, index, required, ...rest } = props
const fallbackId = useId()
const id = rest.name ? `f-${rest.name}` : fallbackId
return (
<FieldWrap
label={label}
hint={hint}
error={error}
index={index}
required={required}
htmlFor={id}
>
<input
{...rest}
id={id}
required={required}
ref={ref}
className={styles.input}
/>
</FieldWrap>
)
}
)
type TextareaFieldProps = Omit<
TextareaHTMLAttributes<HTMLTextAreaElement>,
'className' | 'id'
> & {
label: ReactNode
hint?: ReactNode
error?: ReactNode
index?: string
}
export const TextareaField = forwardRef<HTMLTextAreaElement, TextareaFieldProps>(
function TextareaField(props, ref) {
const { label, hint, error, index, required, ...rest } = props
const fallbackId = useId()
const id = rest.name ? `f-${rest.name}` : fallbackId
return (
<FieldWrap
label={label}
hint={hint}
error={error}
index={index}
required={required}
htmlFor={id}
>
<textarea
{...rest}
id={id}
required={required}
ref={ref}
className={styles.textarea}
/>
</FieldWrap>
)
}
)
type SelectFieldProps = Omit<
SelectHTMLAttributes<HTMLSelectElement>,
'className' | 'id'
> & {
label: ReactNode
hint?: ReactNode
error?: ReactNode
index?: string
}
export const SelectField = forwardRef<HTMLSelectElement, SelectFieldProps>(
function SelectField(props, ref) {
const { label, hint, error, index, required, children, ...rest } = props
const fallbackId = useId()
const id = rest.name ? `f-${rest.name}` : fallbackId
return (
<FieldWrap
label={label}
hint={hint}
error={error}
index={index}
required={required}
htmlFor={id}
>
<select
{...rest}
id={id}
required={required}
ref={ref}
className={styles.select}
>
{children}
</select>
</FieldWrap>
)
}
)

View File

@ -0,0 +1,12 @@
// ===================
// ©AngelaMos | 2026
// Glyph.module.scss
// ===================
@use '../../styles/tokens' as *;
.glyph {
display: block;
color: $ink;
flex-shrink: 0;
}

View File

@ -0,0 +1,159 @@
// ===================
// ©AngelaMos | 2026
// index.tsx
// ===================
import type { TokenType } from '@/api'
import styles from './Glyph.module.scss'
type GlyphProps = {
type: TokenType
size?: number
title?: string
}
export function Glyph({
type,
size = 28,
title,
}: GlyphProps): React.ReactElement {
return (
<svg
className={styles.glyph}
viewBox="0 0 24 24"
width={size}
height={size}
role={title ? 'img' : 'presentation'}
aria-label={title}
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="square"
strokeLinejoin="miter"
>
{renderGlyph(type)}
</svg>
)
}
function renderGlyph(type: TokenType): React.ReactElement {
switch (type) {
case 'webbug':
return <WebbugGlyph />
case 'slowredirect':
return <SlowRedirectGlyph />
case 'docx':
return <DocxGlyph />
case 'pdf':
return <PdfGlyph />
case 'kubeconfig':
return <KubeconfigGlyph />
case 'envfile':
return <EnvfileGlyph />
case 'mysql':
return <MysqlGlyph />
default:
return <WebbugGlyph />
}
}
function WebbugGlyph(): React.ReactElement {
return (
<>
<rect x="3" y="3" width="4" height="4" fill="currentColor" stroke="none" />
<rect x="9" y="3" width="4" height="4" />
<rect x="15" y="3" width="4" height="4" />
<rect x="3" y="9" width="4" height="4" />
<rect x="9" y="9" width="4" height="4" />
<rect x="15" y="9" width="4" height="4" />
<rect x="3" y="15" width="4" height="4" />
<rect x="9" y="15" width="4" height="4" />
<rect x="15" y="15" width="4" height="4" />
</>
)
}
function SlowRedirectGlyph(): React.ReactElement {
return (
<>
<circle cx="3.5" cy="12" r="1" fill="currentColor" stroke="none" />
<circle cx="6.5" cy="12" r="1" fill="currentColor" stroke="none" />
<circle cx="9.5" cy="12" r="1" fill="currentColor" stroke="none" />
<line x1="12" y1="12" x2="20" y2="12" />
<polyline points="17,8 21,12 17,16" />
</>
)
}
function DocxGlyph(): React.ReactElement {
return (
<>
<path d="M5 3 L15 3 L19 7 L19 21 L5 21 Z" />
<line x1="15" y1="3" x2="15" y2="7" />
<line x1="15" y1="7" x2="19" y2="7" />
<line x1="8" y1="11" x2="16" y2="11" />
<line x1="8" y1="14" x2="16" y2="14" />
<line x1="8" y1="17" x2="13" y2="17" />
</>
)
}
function PdfGlyph(): React.ReactElement {
return (
<>
<path d="M5 3 L15 3 L19 7 L19 21 L5 21 Z" />
<line x1="15" y1="3" x2="15" y2="7" />
<line x1="15" y1="7" x2="19" y2="7" />
<text
x="12"
y="17"
fontFamily="ui-monospace, monospace"
fontSize="6"
fontWeight="700"
textAnchor="middle"
fill="currentColor"
stroke="none"
>
PDF
</text>
</>
)
}
function KubeconfigGlyph(): React.ReactElement {
return (
<>
<polygon points="12,3 19.5,7.5 19.5,16.5 12,21 4.5,16.5 4.5,7.5" />
<circle cx="12" cy="12" r="2.5" fill="currentColor" stroke="none" />
<line x1="12" y1="3" x2="12" y2="9.5" />
<line x1="19.5" y1="7.5" x2="14.2" y2="11" />
<line x1="19.5" y1="16.5" x2="14.2" y2="13" />
<line x1="12" y1="21" x2="12" y2="14.5" />
<line x1="4.5" y1="16.5" x2="9.8" y2="13" />
<line x1="4.5" y1="7.5" x2="9.8" y2="11" />
</>
)
}
function EnvfileGlyph(): React.ReactElement {
return (
<>
<path d="M8 4 C5 4 5 8 5 12 C5 16 5 20 8 20" />
<path d="M16 4 C19 4 19 8 19 12 C19 16 19 20 16 20" />
<line x1="8" y1="11.5" x2="16" y2="11.5" />
<line x1="8" y1="13.5" x2="16" y2="13.5" />
<circle cx="12" cy="8.5" r="0.7" fill="currentColor" stroke="none" />
<circle cx="12" cy="16.5" r="0.7" fill="currentColor" stroke="none" />
</>
)
}
function MysqlGlyph(): React.ReactElement {
return (
<>
<ellipse cx="12" cy="5.5" rx="7" ry="2.5" />
<path d="M5 5.5 L5 11 C5 12.4 8.1 13.5 12 13.5 C15.9 13.5 19 12.4 19 11 L19 5.5" />
<path d="M5 11 L5 16.5 C5 17.9 8.1 19 12 19 C15.9 19 19 17.9 19 16.5 L19 11" />
</>
)
}

View File

@ -0,0 +1,24 @@
// ===================
// ©AngelaMos | 2026
// Halftone.module.scss
// ===================
@use '../../styles/tokens' as *;
.halftone {
inline-size: 100%;
background-image: $halftone-dots;
background-repeat: repeat;
&[data-density='sparse'] {
background-size: 14px 14px;
}
&[data-density='normal'] {
background-size: 8px 8px;
}
&[data-density='dense'] {
background-size: 5px 5px;
}
}

View File

@ -0,0 +1,25 @@
// ===================
// ©AngelaMos | 2026
// index.tsx
// ===================
import styles from './Halftone.module.scss'
type HalftoneProps = {
density?: 'sparse' | 'normal' | 'dense'
height?: number
}
export function Halftone({
density = 'normal',
height = 24,
}: HalftoneProps): React.ReactElement {
return (
<div
className={styles.halftone}
data-density={density}
style={{ blockSize: `${height}px` }}
aria-hidden="true"
/>
)
}

View File

@ -0,0 +1,57 @@
// ===================
// ©AngelaMos | 2026
// LeaderLabel.module.scss
// ===================
@use '../../styles/tokens' as *;
@use '../../styles/fonts' as *;
.label {
display: flex;
flex-direction: column;
gap: $space-1;
max-width: 22ch;
font-family: $font-mono;
&[data-align='right'] {
text-align: right;
align-self: flex-end;
}
}
.head {
display: flex;
align-items: center;
gap: $space-1-5;
}
.index {
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $ink-mute;
white-space: nowrap;
}
.rule {
flex: 1 1 auto;
block-size: $hairline;
background-color: $rule;
align-self: center;
}
.body {
font-size: $font-size-2xs;
line-height: $line-height-tight;
letter-spacing: $tracking-wide;
text-transform: uppercase;
color: $ink;
}
.caption {
font-size: $font-size-3xs;
line-height: $line-height-snug;
color: $ink-mute;
text-transform: none;
letter-spacing: $tracking-normal;
}

View File

@ -0,0 +1,31 @@
// ===================
// ©AngelaMos | 2026
// index.tsx
// ===================
import type { PropsWithChildren, ReactNode } from 'react'
import styles from './LeaderLabel.module.scss'
type LeaderLabelProps = PropsWithChildren<{
index?: string
caption?: ReactNode
align?: 'left' | 'right'
}>
export function LeaderLabel({
children,
index,
caption,
align = 'left',
}: LeaderLabelProps): React.ReactElement {
return (
<div className={styles.label} data-align={align}>
<div className={styles.head}>
{index ? <span className={styles.index}>{index}</span> : null}
<span className={styles.rule} aria-hidden="true" />
</div>
<div className={styles.body}>{children}</div>
{caption ? <div className={styles.caption}>{caption}</div> : null}
</div>
)
}

View File

@ -0,0 +1,53 @@
// ===================
// ©AngelaMos | 2026
// Pill.module.scss
// ===================
@use '../../styles/tokens' as *;
@use '../../styles/fonts' as *;
.pill {
display: inline-flex;
align-items: center;
gap: $space-1;
padding-block: $space-0-5;
padding-inline: $space-2;
border: $hairline solid $rule;
font-family: $font-mono;
text-transform: uppercase;
letter-spacing: $tracking-wider;
white-space: nowrap;
&[data-size='sm'] {
font-size: $font-size-3xs;
}
&[data-size='md'] {
font-size: $font-size-2xs;
padding-block: $space-1;
padding-inline: $space-2-5;
}
&[data-tone='paper'] {
background-color: $paper;
color: $ink;
}
&[data-tone='ink'] {
background-color: $ink;
color: $paper;
border-color: $ink;
}
&[data-tone='alarm'] {
background-color: $alarm-tint;
color: $alarm-deep;
border-color: $alarm;
}
&[data-tone='signal'] {
background-color: $signal-tint;
color: $signal;
border-color: $signal;
}
}

View File

@ -0,0 +1,24 @@
// ===================
// ©AngelaMos | 2026
// index.tsx
// ===================
import type { PropsWithChildren } from 'react'
import styles from './Pill.module.scss'
type PillProps = PropsWithChildren<{
tone?: 'paper' | 'ink' | 'alarm' | 'signal'
size?: 'sm' | 'md'
}>
export function Pill({
children,
tone = 'paper',
size = 'sm',
}: PillProps): React.ReactElement {
return (
<span className={styles.pill} data-tone={tone} data-size={size}>
{children}
</span>
)
}

View File

@ -0,0 +1,44 @@
// ===================
// ©AngelaMos | 2026
// SerialBar.module.scss
// ===================
@use '../../styles/tokens' as *;
@use '../../styles/fonts' as *;
.serial {
display: inline-grid;
grid-template-columns: auto auto;
grid-template-rows: auto auto;
gap: 0 $space-2;
align-items: baseline;
font-family: $font-mono;
color: $ink;
}
.prefix {
grid-row: 1;
grid-column: 1;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $ink-mute;
}
.bars {
grid-row: 1;
grid-column: 2;
font-size: $font-size-lg;
line-height: 1;
letter-spacing: -0.05em;
color: $ink;
}
.code {
grid-row: 2;
grid-column: 1 / -1;
font-size: $font-size-3xs;
letter-spacing: $tracking-wide;
color: $ink-soft;
overflow-wrap: anywhere;
}

View File

@ -0,0 +1,42 @@
// ===================
// ©AngelaMos | 2026
// index.tsx
// ===================
import { useMemo } from 'react'
import styles from './SerialBar.module.scss'
type SerialBarProps = {
value: string
prefix?: string
}
const BAR_CHARS = ['▮', '▯', '▮', '▮', '▯', '▮', '▯', '▯', '▮']
function fingerprintToBars(value: string): string {
if (value.length === 0) {
return BAR_CHARS.join('')
}
let acc = ''
for (let i = 0; i < 22; i += 1) {
const ch = value.charCodeAt(i % value.length)
acc += (ch & 1) === 0 ? '▮' : '▯'
}
return acc
}
export function SerialBar({
value,
prefix = 'SN',
}: SerialBarProps): React.ReactElement {
const bars = useMemo(() => fingerprintToBars(value), [value])
return (
<div className={styles.serial}>
<span className={styles.prefix}>{prefix}</span>
<span className={styles.bars} aria-hidden="true">
{bars}
</span>
<span className={styles.code}>{value}</span>
</div>
)
}

View File

@ -0,0 +1,110 @@
// ===================
// ©AngelaMos | 2026
// SpecimenCard.module.scss
// ===================
@use '../../styles/tokens' as *;
@use '../../styles/fonts' as *;
@use '../../styles/mixins' as *;
.card {
position: relative;
padding: $space-8 $space-6 $space-6;
border: $hairline solid $rule;
background-color: $paper;
&[data-tone='ink'] {
background-color: $ink;
color: $paper;
border-color: $ink;
}
&[data-tone='alarm'] {
border-color: $alarm;
}
&[data-dense='true'] {
padding: $space-4 $space-4 $space-3;
}
@include breakpoint-up('md') {
padding-inline: $space-8;
}
}
.tag {
position: absolute;
inset-block-start: calc(-1 * #{$space-3});
inset-inline-start: $space-4;
padding-block: $space-1;
padding-inline: $space-2;
background-color: $paper;
border: $hairline solid $rule;
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $ink;
white-space: nowrap;
.card[data-tone='ink'] & {
background-color: $ink;
color: $paper;
border-color: $paper-edge;
}
.card[data-tone='alarm'] & {
background-color: $paper;
border-color: $alarm;
color: $alarm-deep;
}
}
.serial {
position: absolute;
inset-block-end: calc(-1 * #{$space-3});
inset-inline-end: $space-4;
padding-block: $space-1;
padding-inline: $space-2;
background-color: $paper;
border: $hairline solid $rule;
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
color: $ink-soft;
white-space: nowrap;
.card[data-tone='ink'] & {
background-color: $ink;
color: $paper-edge;
border-color: $paper-edge;
}
}
.body {
display: flex;
flex-direction: column;
gap: $space-5;
}
.section {
display: flex;
flex-direction: column;
gap: $space-2;
}
.sectionHead {
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $ink-mute;
padding-block-end: $space-1;
border-block-end: $hairline solid $rule-hair;
}
.sectionBody {
display: flex;
flex-direction: column;
gap: $space-1-5;
}

View File

@ -0,0 +1,42 @@
// ===================
// ©AngelaMos | 2026
// index.tsx
// ===================
import type { PropsWithChildren, ReactNode } from 'react'
import styles from './SpecimenCard.module.scss'
type SpecimenCardProps = PropsWithChildren<{
tag?: ReactNode
serial?: ReactNode
tone?: 'paper' | 'ink' | 'alarm'
dense?: boolean
}>
export function SpecimenCard({
children,
tag,
serial,
tone = 'paper',
dense = false,
}: SpecimenCardProps): React.ReactElement {
return (
<article className={styles.card} data-tone={tone} data-dense={dense}>
{tag ? <span className={styles.tag}>{tag}</span> : null}
<div className={styles.body}>{children}</div>
{serial ? <span className={styles.serial}>{serial}</span> : null}
</article>
)
}
export function SpecimenCardSection({
children,
label,
}: PropsWithChildren<{ label?: ReactNode }>): React.ReactElement {
return (
<section className={styles.section}>
{label ? <header className={styles.sectionHead}>{label}</header> : null}
<div className={styles.sectionBody}>{children}</div>
</section>
)
}

View File

@ -0,0 +1,76 @@
// ===================
// ©AngelaMos | 2026
// Strip.module.scss
// ===================
@use '../../styles/tokens' as *;
@use '../../styles/fonts' as *;
.strip {
display: flex;
align-items: center;
gap: $space-4;
flex-wrap: wrap;
padding-block: $space-2;
padding-inline: $space-4;
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $ink-soft;
&[data-border='bottom'] {
border-bottom: $hairline solid $rule;
}
&[data-border='top'] {
border-top: $hairline solid $rule;
}
&[data-border='both'] {
border-block: $hairline solid $rule;
}
&[data-align='split'] {
justify-content: space-between;
}
&[data-align='left'] {
justify-content: flex-start;
}
}
.item {
display: inline-flex;
align-items: baseline;
gap: $space-1-5;
padding-block: $space-0-5;
padding-inline: $space-2;
border: $hairline solid $rule;
background-color: transparent;
&[data-inverted='true'] {
background-color: $ink;
color: $paper;
border-color: $ink;
}
}
.mark {
display: inline-block;
font-size: $font-size-2xs;
transform: translateY(1px);
}
.label {
font-weight: $font-weight-semibold;
color: $ink-mute;
&::after {
content: '\00a0';
}
}
.value {
color: inherit;
}

View File

@ -0,0 +1,49 @@
// ===================
// ©AngelaMos | 2026
// index.tsx
// ===================
import type { PropsWithChildren, ReactNode } from 'react'
import styles from './Strip.module.scss'
type StripProps = PropsWithChildren<{
align?: 'left' | 'split'
border?: 'bottom' | 'top' | 'both' | 'none'
}>
export function Strip({
children,
align = 'split',
border = 'bottom',
}: StripProps): React.ReactElement {
return (
<div className={styles.strip} data-align={align} data-border={border}>
{children}
</div>
)
}
type StripItemProps = PropsWithChildren<{
inverted?: boolean
label?: string
mark?: ReactNode
}>
export function StripItem({
children,
inverted = false,
label,
mark,
}: StripItemProps): React.ReactElement {
return (
<span className={styles.item} data-inverted={inverted}>
{mark ? (
<span className={styles.mark} aria-hidden="true">
{mark}
</span>
) : null}
{label ? <span className={styles.label}>{label}</span> : null}
<span className={styles.value}>{children}</span>
</span>
)
}

View File

@ -1,4 +1,21 @@
/**
* ©AngelaMos | 2026
* index.tsx
*/
// ===================
// ©AngelaMos | 2026
// index.tsx
// ===================
export { Button } from './Button'
export { CopyField } from './CopyField'
export { DataRow } from './DataRow'
export {
FieldWrap,
SelectField,
TextareaField,
TextField,
} from './Field'
export { Glyph } from './Glyph'
export { Halftone } from './Halftone'
export { LeaderLabel } from './LeaderLabel'
export { Pill } from './Pill'
export { SerialBar } from './SerialBar'
export { SpecimenCard, SpecimenCardSection } from './SpecimenCard'
export { Strip, StripItem } from './Strip'

View File

@ -7,9 +7,35 @@
@forward 'styles/fonts';
@forward 'styles/mixins';
@use 'styles/tokens' as *;
@use 'styles/fonts' as *;
@use 'styles/reset';
html,
body {
background-color: $paper;
color: $ink;
}
body {
font-family: $font-sans;
font-size: $font-size-base;
font-weight: $font-weight-regular;
line-height: $line-height-normal;
letter-spacing: $tracking-normal;
background-image: $paper-grain;
background-attachment: fixed;
background-size: 240px 240px;
}
::selection {
background-color: $ink;
color: $paper;
}
#root {
min-height: 100vh;
min-height: 100dvh;
display: flex;
flex-direction: column;
}

View File

@ -3,6 +3,45 @@
// _tokens.scss
// ===================
// ============================================================================
// COLOR specimen palette
// ============================================================================
$paper: #f1ede4;
$paper-deep: #ebe5d6;
$paper-edge: #d9d1bd;
$ink: #14110b;
$ink-soft: #46402f;
$ink-mute: #837a64;
$ink-faint: #b8ad95;
$rule: #1f1b13;
$rule-soft: #a39879;
$rule-hair: #d6cdb6;
$alarm: #c4341c;
$alarm-deep: #8c2210;
$alarm-tint: #f3d8d0;
$signal: #2b6b3a;
$signal-tint: #d8e5d6;
// ============================================================================
// PAPER GRAIN (svg noise, data-uri overlay at low opacity)
// ============================================================================
$paper-grain: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' seed='7'/><feColorMatrix values='0 0 0 0 0.08 0 0 0 0 0.07 0 0 0 0 0.05 0 0 0 0.35 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
// ============================================================================
// HALFTONE DOT FIELD (svg, repeats for separators / tonal patches)
// ============================================================================
$halftone-dots: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><circle cx='1' cy='1' r='1' fill='%2314110b'/></svg>");
// ============================================================================
// HAIRLINE (the thin printed rule)
// ============================================================================
$hairline: 1px;
$hairline-bold: 2px;
// ============================================================================
// SPACING (8px base system)
// ============================================================================