@tfrc/product 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/package.json +27 -0
- package/src/components.css +137 -0
- package/src/index.css +11 -0
- package/src/themes/finance.css +41 -0
- package/src/tokens.css +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @tfrc/product
|
|
2
|
+
|
|
3
|
+
The product dialect of the company's visual language. Mobile-first,
|
|
4
|
+
restrained, built to be used daily for years. ~40 components, 44px touch
|
|
5
|
+
floor, near-zero novelty tolerance.
|
|
6
|
+
|
|
7
|
+
**This package is not an app.** It is the UI kit every product themes over —
|
|
8
|
+
the finance app is a separate repository that installs this package plus
|
|
9
|
+
`themes/finance` (`decisions/0007.md`). Building a product? Read
|
|
10
|
+
`CONSUMING.md` at the repo root. Renamed from `@tfrc/app` because that name
|
|
11
|
+
was read as "the app"; see `decisions/0008.md`.
|
|
12
|
+
|
|
13
|
+
**Products theme over this layer; they do not fork it.** See
|
|
14
|
+
`src/themes/finance.css` for the pattern — a product theme imports this
|
|
15
|
+
package and adds only what's genuinely product-specific (currently: a
|
|
16
|
+
`--theme-product-hue` identity tag). Everything else — button, field,
|
|
17
|
+
transaction row, balance card, tab bar — is shared by every product that
|
|
18
|
+
will ever theme over this layer.
|
|
19
|
+
|
|
20
|
+
Never imports from `@tfrc/marketing`. If you're tempted to reuse a marketing component
|
|
21
|
+
here, that's a sign it belongs in `@tfrc/foundation` instead, and only
|
|
22
|
+
once a third consumer needs it — see `AGENTS.md` rule 4.
|
|
23
|
+
|
|
24
|
+
## Reserved tokens
|
|
25
|
+
|
|
26
|
+
`--color-gain` and `--color-loss` are owned by this package. See
|
|
27
|
+
`decisions/0002-reserved-hues-for-financial-semantics.md`. Every amount
|
|
28
|
+
or direction indicator must pair color with a sign or arrow — never
|
|
29
|
+
color alone.
|
|
30
|
+
|
|
31
|
+
## Adding a new product theme
|
|
32
|
+
|
|
33
|
+
1. Copy the shape of `src/themes/finance.css`.
|
|
34
|
+
2. Pick an unused hue from `STATE.md`'s `product_hues_in_use` — check the
|
|
35
|
+
ceiling in `decisions/0005.md` first.
|
|
36
|
+
3. Only add token overrides if the product genuinely needs different
|
|
37
|
+
values — most won't. The finance theme adds none.
|
|
38
|
+
4. Update `STATE.md`.
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tfrc/product",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "The product dialect of the visual language — application UI. Mobile-first, ~40 components, 44px touch floor. Products theme over this layer; they do not fork it. Never imports from @tfrc/marketing.",
|
|
6
|
+
"keywords": ["design-system", "design-tokens", "css", "tailwind", "product-ui"],
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/the-full-remote-company/design-system.git",
|
|
10
|
+
"directory": "packages/product"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/the-full-remote-company/design-system/tree/main/packages/product#readme",
|
|
13
|
+
"bugs": "https://github.com/the-full-remote-company/design-system/issues",
|
|
14
|
+
"main": "src/index.css",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./src/index.css",
|
|
17
|
+
"./themes/*": "./src/themes/*.css"
|
|
18
|
+
},
|
|
19
|
+
"files": ["src", "README.md"],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@tfrc/foundation": "1.0.0"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"registry": "https://registry.npmjs.org"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
@tfrc/product — components
|
|
3
|
+
Reference only tokens from tokens.css. Zero raw color literals.
|
|
4
|
+
Every interactive element clears --touch-min (44px) — see AGENTS.md
|
|
5
|
+
and decisions/0001.md's comparison table.
|
|
6
|
+
========================================================================== */
|
|
7
|
+
|
|
8
|
+
body { font-family: var(--font-ui); }
|
|
9
|
+
h1, h2, h3 { font-family: var(--font-display); font-weight: 800; margin: 0; letter-spacing: -0.028em; }
|
|
10
|
+
|
|
11
|
+
/* ---- Button — same API/class names as @tfrc/marketing, different values ---- */
|
|
12
|
+
.btn {
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
gap: 0.5rem;
|
|
17
|
+
min-height: var(--touch-min);
|
|
18
|
+
padding: 0 1rem;
|
|
19
|
+
border: 1px solid transparent;
|
|
20
|
+
border-radius: var(--radius-sm);
|
|
21
|
+
font-family: inherit;
|
|
22
|
+
font-size: var(--step-body);
|
|
23
|
+
font-weight: 600;
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
transition: background var(--duration-fast) var(--ease-quint),
|
|
26
|
+
border-color var(--duration-fast) var(--ease-quint);
|
|
27
|
+
}
|
|
28
|
+
.btn--primary { background: var(--color-accent); color: var(--color-accent-fg); }
|
|
29
|
+
.btn--primary:active { background: var(--color-accent-hover); }
|
|
30
|
+
.btn--secondary { background: var(--color-surface-raised); border-color: var(--color-border-strong); color: var(--color-text-primary); }
|
|
31
|
+
.btn--ghost { background: transparent; color: var(--color-accent); }
|
|
32
|
+
.btn--block { width: 100%; }
|
|
33
|
+
|
|
34
|
+
/* ---- Field — app only ---- */
|
|
35
|
+
.field label { display: block; font-size: var(--step-sm); font-weight: 600; margin-bottom: 0.375rem; }
|
|
36
|
+
.field input {
|
|
37
|
+
width: 100%;
|
|
38
|
+
min-height: var(--touch-min);
|
|
39
|
+
padding: 0 0.75rem;
|
|
40
|
+
background: var(--color-surface-raised);
|
|
41
|
+
border: 1px solid var(--color-border-subtle);
|
|
42
|
+
border-radius: var(--radius-sm);
|
|
43
|
+
font-family: inherit;
|
|
44
|
+
font-size: 1rem;
|
|
45
|
+
color: var(--color-text-primary);
|
|
46
|
+
}
|
|
47
|
+
/* Amounts are a typographic problem, not just an input: tabular numerals,
|
|
48
|
+
right-aligned, decimal-locked. See packages/product README. */
|
|
49
|
+
.field input.num {
|
|
50
|
+
font-variant-numeric: tabular-nums;
|
|
51
|
+
text-align: right;
|
|
52
|
+
font-size: 1.25rem;
|
|
53
|
+
font-weight: 600;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ---- Badge / pill — app only. Reserved gain/loss variants live here. ---- */
|
|
57
|
+
.pill {
|
|
58
|
+
display: inline-flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: 0.3125rem;
|
|
61
|
+
padding: 0.1875rem 0.5rem;
|
|
62
|
+
border-radius: 0.375rem;
|
|
63
|
+
font-size: var(--step-xs);
|
|
64
|
+
font-weight: 600;
|
|
65
|
+
text-transform: uppercase;
|
|
66
|
+
letter-spacing: 0.03em;
|
|
67
|
+
background: var(--color-surface-sunken);
|
|
68
|
+
color: var(--color-text-secondary);
|
|
69
|
+
}
|
|
70
|
+
.pill--gain { background: var(--color-gain-tint); color: var(--color-gain); }
|
|
71
|
+
.pill--loss { background: var(--color-loss-tint); color: var(--color-loss); }
|
|
72
|
+
|
|
73
|
+
/* ---- Amount — app only. Direction is never color-only; every consumer
|
|
74
|
+
must pair this with a sign or arrow glyph. See decisions/0002.md. ---- */
|
|
75
|
+
.amt {
|
|
76
|
+
font-variant-numeric: tabular-nums;
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
font-size: var(--step-body);
|
|
79
|
+
white-space: nowrap;
|
|
80
|
+
letter-spacing: -0.01em;
|
|
81
|
+
}
|
|
82
|
+
.amt--gain { color: var(--color-gain); }
|
|
83
|
+
.amt--loss { color: var(--color-text-primary); }
|
|
84
|
+
|
|
85
|
+
/* ---- Transaction row / list — app only, no web equivalent ---- */
|
|
86
|
+
.list { background: var(--color-surface-raised); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); overflow: hidden; }
|
|
87
|
+
.row {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: 0.75rem;
|
|
91
|
+
min-height: var(--touch-min);
|
|
92
|
+
padding: var(--pad-row);
|
|
93
|
+
border-bottom: 1px solid var(--color-border-subtle);
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
transition: background var(--duration-fast) var(--ease-quint);
|
|
96
|
+
}
|
|
97
|
+
.row:last-child { border-bottom: 0; }
|
|
98
|
+
.row:hover, .row:active { background: var(--color-surface-sunken); }
|
|
99
|
+
.row__icon {
|
|
100
|
+
width: 2rem; height: 2rem; border-radius: 0.625rem; flex-shrink: 0;
|
|
101
|
+
display: flex; align-items: center; justify-content: center;
|
|
102
|
+
font-size: 0.875rem; background: var(--color-surface-sunken);
|
|
103
|
+
}
|
|
104
|
+
.row__main { flex: 1; min-width: 0; }
|
|
105
|
+
.row__t { font-size: var(--step-body); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
106
|
+
.row__s { font-size: var(--step-xs); color: var(--color-text-muted); margin-top: 0.0625rem; }
|
|
107
|
+
|
|
108
|
+
/* ---- Balance card — app only ---- */
|
|
109
|
+
.balance { background: var(--color-accent); color: #fff; border-radius: var(--radius-lg); padding: 1.125rem; }
|
|
110
|
+
.balance__k { font-size: var(--step-xs); text-transform: uppercase; letter-spacing: 0.09em; }
|
|
111
|
+
.balance__v {
|
|
112
|
+
font-family: var(--font-ui); font-size: 2rem; font-weight: 600;
|
|
113
|
+
letter-spacing: -0.02em; font-variant-numeric: tabular-nums; margin-top: 0.25rem; line-height: 1.1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* ---- Segmented control — app only ---- */
|
|
117
|
+
.seg { display: flex; background: var(--color-surface-sunken); border-radius: var(--radius-sm); padding: 2px; gap: 2px; }
|
|
118
|
+
.seg button {
|
|
119
|
+
flex: 1; min-height: 2.25rem; border: 0; border-radius: calc(var(--radius-sm) - 2px);
|
|
120
|
+
background: transparent; font-family: inherit; font-size: var(--step-sm); font-weight: 600;
|
|
121
|
+
color: var(--color-text-secondary); cursor: pointer;
|
|
122
|
+
transition: background var(--duration-fast) var(--ease-quint), color var(--duration-fast) var(--ease-quint);
|
|
123
|
+
}
|
|
124
|
+
.seg button[aria-pressed="true"] { background: var(--color-surface-raised); color: var(--color-text-primary); }
|
|
125
|
+
|
|
126
|
+
/* ---- Tab bar — app only, no web equivalent ---- */
|
|
127
|
+
.tabs {
|
|
128
|
+
display: flex; background: var(--color-surface-raised);
|
|
129
|
+
border-top: 1px solid var(--color-border-subtle); padding-bottom: var(--safe-bottom);
|
|
130
|
+
}
|
|
131
|
+
.tabs button {
|
|
132
|
+
flex: 1; min-height: var(--touch-min); border: 0; background: transparent; cursor: pointer;
|
|
133
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.1875rem;
|
|
134
|
+
font-family: inherit; font-size: 0.625rem; font-weight: 600; color: var(--color-text-muted);
|
|
135
|
+
transition: color var(--duration-fast) var(--ease-quint);
|
|
136
|
+
}
|
|
137
|
+
.tabs button[aria-pressed="true"] { color: var(--color-accent); }
|
package/src/index.css
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
@tfrc/product — entry point
|
|
3
|
+
Import order: foundation (ramps + reset) -> tokens (semantic layer) ->
|
|
4
|
+
components. A product theme (see src/themes/) imports THIS file first,
|
|
5
|
+
then layers its own overrides on top. Never import anything from
|
|
6
|
+
@tfrc/marketing — see AGENTS.md rule 3 and scripts/lint-boundaries.js.
|
|
7
|
+
========================================================================== */
|
|
8
|
+
|
|
9
|
+
@import "@tfrc/foundation";
|
|
10
|
+
@import "./tokens.css";
|
|
11
|
+
@import "./components.css";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
@tfrc/product/themes/finance — the first product theme
|
|
3
|
+
Themes over @tfrc/product; does not fork it. See decisions/0001.md — "one
|
|
4
|
+
product system, not one per product" — this file is the proof of that
|
|
5
|
+
principle, not an exception to it.
|
|
6
|
+
|
|
7
|
+
For v1, the finance theme needs no token overrides: the base @tfrc/product
|
|
8
|
+
semantic layer already IS the finance-appropriate one (reserved gain/
|
|
9
|
+
loss, tabular numerals, 44px touch floor). This file exists so future
|
|
10
|
+
product themes have a real example to follow, and so finance-specific
|
|
11
|
+
additions have an obvious home instead of leaking into @tfrc/product itself.
|
|
12
|
+
========================================================================== */
|
|
13
|
+
|
|
14
|
+
@import "@tfrc/product";
|
|
15
|
+
|
|
16
|
+
:root {
|
|
17
|
+
/* This theme's identity within the product spectrum. See
|
|
18
|
+
decisions/0005-product-spectrum-hue-ceiling.md before reusing this
|
|
19
|
+
hue elsewhere, and before adding a new product theme. */
|
|
20
|
+
--theme-product-hue: var(--product-capital);
|
|
21
|
+
--theme-product-hue-tint: var(--color-accent-bg);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Finance-specific composition, not present in base @tfrc/product: a labeled
|
|
25
|
+
accent strip for surfaces that need to say "this is the finance
|
|
26
|
+
product" inside a cross-product shell (e.g. a unified dashboard). */
|
|
27
|
+
.theme-tag {
|
|
28
|
+
display: inline-flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: 0.375rem;
|
|
31
|
+
font-size: var(--step-xs);
|
|
32
|
+
font-weight: 600;
|
|
33
|
+
color: var(--theme-product-hue);
|
|
34
|
+
}
|
|
35
|
+
.theme-tag::before {
|
|
36
|
+
content: "";
|
|
37
|
+
width: 0.5rem;
|
|
38
|
+
height: 0.5rem;
|
|
39
|
+
border-radius: 9999px;
|
|
40
|
+
background: var(--theme-product-hue);
|
|
41
|
+
}
|
package/src/tokens.css
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
@tfrc/product — semantic tokens
|
|
3
|
+
Same token NAMES as @tfrc/marketing (see CONTRACT.md). Values diverge on
|
|
4
|
+
purpose. Self-contained per decisions/0004.md — does not import
|
|
5
|
+
anything from @tfrc/marketing.
|
|
6
|
+
========================================================================== */
|
|
7
|
+
|
|
8
|
+
:root {
|
|
9
|
+
--color-surface: oklch(0.980 0.004 270);
|
|
10
|
+
--color-surface-raised: oklch(1 0 0);
|
|
11
|
+
--color-surface-sunken: oklch(0.958 0.006 270);
|
|
12
|
+
|
|
13
|
+
--color-border-subtle: oklch(0.916 0.009 270);
|
|
14
|
+
--color-border-strong: oklch(0.812 0.015 270);
|
|
15
|
+
|
|
16
|
+
--color-text-primary: var(--ramp-neutral-900);
|
|
17
|
+
--color-text-secondary: var(--ramp-neutral-600);
|
|
18
|
+
--color-text-muted: var(--ramp-neutral-400);
|
|
19
|
+
|
|
20
|
+
--color-accent: var(--ramp-brand-600);
|
|
21
|
+
--color-accent-hover: var(--ramp-brand-700);
|
|
22
|
+
--color-accent-fg: #fff;
|
|
23
|
+
--color-accent-bg: var(--ramp-brand-100);
|
|
24
|
+
--color-accent-bg-fg: var(--ramp-brand-800);
|
|
25
|
+
|
|
26
|
+
--color-focus: oklch(0.560 0.150 235);
|
|
27
|
+
|
|
28
|
+
--color-info-bg: oklch(0.940 0.032 235);
|
|
29
|
+
--color-info-fg: oklch(0.360 0.070 237);
|
|
30
|
+
--color-info-line: oklch(0.560 0.150 235);
|
|
31
|
+
|
|
32
|
+
--color-danger-solid: oklch(0.615 0.205 25);
|
|
33
|
+
--color-danger-bg: oklch(0.940 0.035 25);
|
|
34
|
+
--color-danger-fg: oklch(0.475 0.170 25);
|
|
35
|
+
--color-danger-line: oklch(0.615 0.205 25);
|
|
36
|
+
|
|
37
|
+
--color-success-bg: oklch(0.940 0.040 150);
|
|
38
|
+
--color-success-fg: oklch(0.455 0.090 150);
|
|
39
|
+
--color-success-line: oklch(0.590 0.110 150);
|
|
40
|
+
|
|
41
|
+
/* Reserved — this package OWNS these. @tfrc/marketing must never define or
|
|
42
|
+
reference them. See decisions/0002.md. */
|
|
43
|
+
--color-gain: oklch(0.510 0.120 150);
|
|
44
|
+
--color-gain-tint: oklch(0.945 0.045 150);
|
|
45
|
+
--color-loss: oklch(0.530 0.190 25);
|
|
46
|
+
--color-loss-tint: oklch(0.940 0.040 25);
|
|
47
|
+
|
|
48
|
+
/* ---- product-only scale: this is most of the divergence ---- */
|
|
49
|
+
--font-display: "Bricolage Grotesque", system-ui, sans-serif;
|
|
50
|
+
--font-ui: "IBM Plex Sans", system-ui, -apple-system, sans-serif;
|
|
51
|
+
--font-mono: "JetBrains Mono", ui-monospace, monospace;
|
|
52
|
+
|
|
53
|
+
--step-title: 1.75rem; /* web hero is up to 6rem */
|
|
54
|
+
--step-lg: 1.25rem;
|
|
55
|
+
--step-body: 0.9375rem; /* web body is 1.0625rem */
|
|
56
|
+
--step-sm: 0.8125rem;
|
|
57
|
+
--step-xs: 0.6875rem;
|
|
58
|
+
|
|
59
|
+
--touch-min: 2.75rem; /* 44px. A floor, not a guideline. */
|
|
60
|
+
--radius-sm: 0.5rem;
|
|
61
|
+
--radius-md: 0.75rem;
|
|
62
|
+
--radius-lg: 1rem; /* web goes to 1.5rem */
|
|
63
|
+
|
|
64
|
+
--pad-screen: 1rem; /* web gutter goes up to 4rem */
|
|
65
|
+
--pad-row: 0.75rem;
|
|
66
|
+
|
|
67
|
+
--duration-fast: 100ms;
|
|
68
|
+
--duration-base: 180ms; /* web goes to 640ms */
|
|
69
|
+
|
|
70
|
+
--safe-top: env(safe-area-inset-top, 0px);
|
|
71
|
+
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
|
72
|
+
}
|