mirror of
https://github.com/hanzoai/team2.git
synced 2026-10-30 14:54:57 +00:00
The board began at its columns, so the screen had no trail, no title, no view row and no filter — the whole band the reference draws above the work, and the band every measurement below it is taken from. A card drew a count where its tags belong, one face where a team was, and a ring reading zero on every card because nothing carried an estimate. Tags, components and collaborators are documents the model already names; the board reads them in one pass rather than one per card. An issue is opened IN a project, so a board across all of them has no `add` and draws no control that would refuse. The same reasoning gives the three answers a region has before a space is open — still opening, refused, nowhere — one mapping, so the board and the inbox beside it cannot report different fictions. Co-authored-by: Hanzo Dev <[email protected]>
163 lines
5.9 KiB
TypeScript
163 lines
5.9 KiB
TypeScript
/**
|
|
* The names, so no component writes a colour.
|
|
*
|
|
* tokens.css chooses the values; this file is the only way to reach one. A
|
|
* component reads `paint.card`, never `var(--team-card)` and never a hex — one
|
|
* spelling, checked by the compiler, and a role that moves moves everywhere.
|
|
*
|
|
* The type ladder, the spacing ramp and the radii are @hanzo/gui's published
|
|
* `$N` tokens and are NOT restated here. What is stated is which rung each ROLE
|
|
* lands on, because that is the decision four regions would otherwise take
|
|
* separately and take differently.
|
|
*/
|
|
|
|
/** A CSS reference to one role. The value lives in tokens.css. */
|
|
const role = <N extends string>(name: N) => `var(--team-${name})` as const
|
|
|
|
export const paint = {
|
|
/** The app. The rail and the navigator sit directly on it. */
|
|
ground: role('ground'),
|
|
/** The board and the inbox — one lift off the ground. */
|
|
panel: role('panel'),
|
|
/** A card, and an unread row: the same rung, because both are objects. */
|
|
card: role('card'),
|
|
/** The one hairline, at a fixed alpha so it lifts with its surface. */
|
|
rule: role('rule'),
|
|
|
|
ink: role('ink'),
|
|
mute: role('mute'),
|
|
dim: role('dim'),
|
|
|
|
/** Where you are and how many: active tab, active breadcrumb, count, progress. */
|
|
accent: role('accent'),
|
|
/** Addressed to you: a mention, the bell, an unread you are named in. */
|
|
alert: role('alert'),
|
|
} as const
|
|
|
|
/** The four workflow states, by category. `IssueStatus.category` is the model's. */
|
|
export const state = {
|
|
backlog: role('backlog'),
|
|
todo: role('todo'),
|
|
doing: role('doing'),
|
|
done: role('done'),
|
|
} as const
|
|
|
|
export type State = keyof typeof state
|
|
|
|
/** Six categorical slots. Nothing outside this module names one. */
|
|
const SLOTS = 6
|
|
|
|
/**
|
|
* Which slot a thing gets, from its id.
|
|
*
|
|
* A stable hash rather than a table: the labels, the projects and the
|
|
* components all come from a space nobody here has seen, so a per-name table
|
|
* would be a table of guesses that goes stale the first time someone adds a
|
|
* tag. The same id gets the same slot on every screen and in every session,
|
|
* which is the only property a reader actually depends on.
|
|
*/
|
|
export const slot = (id: string): number => {
|
|
const key = id.toLowerCase()
|
|
let h = 0
|
|
for (let i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) | 0
|
|
return (Math.abs(h) % SLOTS) + 1
|
|
}
|
|
|
|
/**
|
|
* A chip. Its slot's hue as the fill, the ground as the ink.
|
|
*
|
|
* One value serves the fill and the dot because the ramp's lightness was chosen
|
|
* for both; a second, quieter value per slot would be a second thing to keep in
|
|
* step for no reading a person actually makes.
|
|
*/
|
|
export const chip = (id: string) =>
|
|
({ background: `var(--team-hue-${slot(id)})`, color: 'var(--team-chip-ink)' }) as const
|
|
|
|
/** A mark that carries a colour of its own — a project glyph, a component. */
|
|
export const mark = (id: string) => `var(--team-hue-${slot(id)})` as const
|
|
|
|
/**
|
|
* Priority reads as two tiers, and that is the whole scheme.
|
|
*
|
|
* The platform ranks four, and a colour that separates Low from Medium spends
|
|
* the one signal that has to survive a glance on a distinction the label
|
|
* already makes. Urgent and High carry the alert hue; the rest carry none.
|
|
*/
|
|
export const urgent = (priority: number) => priority === 1 || priority === 2
|
|
|
|
/**
|
|
* Priority as a chip, and it has two faces because priority has two tiers.
|
|
*
|
|
* Priority is ORDERED, so it reads as rank rather than as category, and it must
|
|
* not spend a slot of the categorical ring saying so — a Low chip that happens
|
|
* to land on the same hue as the Design label would claim a kinship neither
|
|
* has. The urgent end carries the alert hue; everything below it carries no
|
|
* hue at all, which is exactly as much as a reader needs.
|
|
*/
|
|
export const rank = (priority: number) =>
|
|
urgent(priority)
|
|
? ({ background: 'var(--team-urgent)', color: 'var(--team-chip-ink)' } as const)
|
|
: ({ background: 'var(--team-quiet)', color: paint.ink } as const)
|
|
|
|
/**
|
|
* Which gui rung each role lands on. Four regions, one answer.
|
|
*
|
|
* These are @hanzo/gui `$N` tokens: type $1 11 · $2 13 · $3 14 · $4 15 · $7 21 ·
|
|
* $10 32; space is the 4px ramp $1 4 · $2 8 · $3 12 · $4 16 · $5 24; radius $1 6
|
|
* control · $3 8 row · $10+ pill. Nothing here invents a number — every rung is
|
|
* one the ladder already publishes, which is why the product cannot drift into
|
|
* a tenth size the way it did before the ladder existed.
|
|
*/
|
|
export const rung = {
|
|
/** "Tracker", "Inbox", the board's own title — one size, three placements. */
|
|
title: '$7',
|
|
/** A navigator row, a tab label. */
|
|
row: '$4',
|
|
/** A card title, an inbox line, a percentage, a count. */
|
|
body: '$3',
|
|
/** A breadcrumb, a column header. */
|
|
small: '$2',
|
|
/** A chip. */
|
|
chip: '$1',
|
|
/** The rail's figure. Measured 22 image px of cap, so ~20 — and $10's 32
|
|
* overruns the 60px capsule the moment the number reaches three glyphs. */
|
|
figure: '$7',
|
|
} as const
|
|
|
|
export const gap = {
|
|
/** Card to card, chip to chip. */
|
|
tight: '$2',
|
|
/** Inside a card. */
|
|
inset: '$3',
|
|
/** Column to column, panel padding. */
|
|
wide: '$4',
|
|
/** The board's own left inset. */
|
|
edge: '$5',
|
|
} as const
|
|
|
|
export const round = {
|
|
card: '$1',
|
|
panel: '$1',
|
|
field: '$1',
|
|
pill: '$10',
|
|
} as const
|
|
|
|
/**
|
|
* Uppercase labels carry tracking, and only they do.
|
|
*
|
|
* Measured on the reference at ~+0.08em; a grotesque set in caps at zero
|
|
* tracking closes up and reads as one word. It applies to the column headers
|
|
* and the navigator's section heads, and to nothing else.
|
|
*/
|
|
export const caps = { textTransform: 'uppercase', letterSpacing: 0.08 } as const
|
|
|
|
/**
|
|
* The focus ring, stated once for the product.
|
|
*
|
|
* A keyboard ring is the one piece of chrome that has to look identical
|
|
* everywhere — a reader learns it once — and it was already written out
|
|
* separately in two files before this line existed. Spread it into
|
|
* `focusVisibleStyle`.
|
|
*/
|
|
export const ring = { outlineColor: '$outlineColor', outlineWidth: 2, outlineStyle: 'solid' } as const
|