@simscloud/design 0.1.0 → 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 +119 -0
- package/package.json +1 -1
- package/tailwind-preset.js +77 -26
- package/tokens.css +36 -6
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# @simscloud/design — SIMS design system
|
|
2
|
+
|
|
3
|
+
The single source of visual truth for SIMS: design **tokens** (`tokens.css`), a **Tailwind
|
|
4
|
+
preset** (`tailwind-preset.js`), and self-hosted **fonts** (`fonts/`). Consumed by the app
|
|
5
|
+
directly (workspace) and by the landing/hosted repos as a version-pinned package (UI.md §2).
|
|
6
|
+
|
|
7
|
+
This document is the **D1 visual spec** (issue #70): the finalized language plus the
|
|
8
|
+
accessibility rules — **contrast ratios** and **minimum tap-target sizes** — that the atoms
|
|
9
|
+
in `@simscloud/ui` and every later screen (D2–D6) must honor.
|
|
10
|
+
|
|
11
|
+
## Voice & audience
|
|
12
|
+
|
|
13
|
+
German-first, _einfache Sprache_, "UpKeep but simpler". The primary users are non-technical
|
|
14
|
+
Spitex staff on **tablets and phones**. Two consequences are non-negotiable:
|
|
15
|
+
|
|
16
|
+
- **Contrast** — body text meets **WCAG AA (≥4.5:1)**; large text / non-text UI ≥3:1.
|
|
17
|
+
- **Touch** — interactive controls are **≥44×44px** (`--tap-target-min`, WCAG 2.5.5).
|
|
18
|
+
|
|
19
|
+
## Brand & logo
|
|
20
|
+
|
|
21
|
+
The mark lives in `misc/brand/logo/` (four SVGs); render it through the `@simscloud/ui`
|
|
22
|
+
`Logo` atom (`src` is passed in, so this package stays asset-pipeline-free).
|
|
23
|
+
|
|
24
|
+
| File | Use |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `logo_text_color.svg` | default — wordmark on light surfaces |
|
|
27
|
+
| `logo_text_white.svg` | wordmark on dark / brand (navy) surfaces |
|
|
28
|
+
| `logo_notext_color.svg` | compact mark (favicon, tight headers) on light |
|
|
29
|
+
| `logo_notext_white.svg` | compact mark on dark / brand |
|
|
30
|
+
|
|
31
|
+
Brand anchor: **navy `#000059`** (`--navy-600`), matching the logo.
|
|
32
|
+
|
|
33
|
+
## Color tokens
|
|
34
|
+
|
|
35
|
+
Two layers (UI.md §2.5): **primitives** (raw values, never used directly) → **semantic**
|
|
36
|
+
(intent; components use only these). Re-theming = swap the semantic layer; a commented dark
|
|
37
|
+
theme in `tokens.css` proves the door is open.
|
|
38
|
+
|
|
39
|
+
| Semantic | Role |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `brand` / `brand-strong` / `brand-soft` / `on-brand` | primary actions, hover, tint, text-on-brand |
|
|
42
|
+
| `bg` / `surface` / `surface-muted` | page, panel, subtle section |
|
|
43
|
+
| `text` / `text-muted` / `heading` / `text-inverse` | body, secondary, titles, on-dark |
|
|
44
|
+
| `line` / `ring` | borders / focus ring |
|
|
45
|
+
| `success` / `warning` / `danger` | **fill/border/icon only** |
|
|
46
|
+
| `success-strong` / … | **status as text** (AA-safe) + solid status-button bg |
|
|
47
|
+
| `success-stronger` / … | solid status-button **hover** |
|
|
48
|
+
|
|
49
|
+
> ⚠️ **Status colors:** the bright `success`/`warning`/`danger` fills fail AA as text
|
|
50
|
+
> (`warning` is ~2.1:1 on white). For any status **text**, use the `*-strong` shades —
|
|
51
|
+
> e.g. `text-danger-strong`. The bright fills are fine for left accents, icons, and chips.
|
|
52
|
+
|
|
53
|
+
## Contrast ratios (verified, WCAG)
|
|
54
|
+
|
|
55
|
+
| Foreground | Background | Ratio | AA |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| `text` #0f172a | `bg` #ffffff | 17.85:1 | ✅ |
|
|
58
|
+
| `text` #0f172a | `surface` #f6f8fb | 16.78:1 | ✅ |
|
|
59
|
+
| `text-muted` #5b6776 | `bg` | 5.76:1 | ✅ |
|
|
60
|
+
| `text-muted` #5b6776 | `surface` | 5.41:1 | ✅ |
|
|
61
|
+
| `heading` #000037 | `bg` | 19.90:1 | ✅ |
|
|
62
|
+
| `on-brand` #fff | `brand` #000059 | 18.35:1 | ✅ |
|
|
63
|
+
| `brand` #000059 (links/ghost) | `bg` | 18.35:1 | ✅ |
|
|
64
|
+
| `success-strong` #15803d | `bg` / `surface` | 5.02 / 4.71 | ✅ |
|
|
65
|
+
| `warning-strong` #b45309 | `bg` / `surface` | 5.02 / 4.72 | ✅ |
|
|
66
|
+
| `danger-strong` #b91c1c | `bg` / `surface` | 6.47 / 6.08 | ✅ |
|
|
67
|
+
| `on-brand` #fff | `danger` #dc2626 (button) | 4.83:1 | ✅ |
|
|
68
|
+
|
|
69
|
+
(Recompute after any palette change — the brand navy is very dark, so the perennial risk is
|
|
70
|
+
the status colors, not the brand.)
|
|
71
|
+
|
|
72
|
+
## Typography
|
|
73
|
+
|
|
74
|
+
Self-hosted **DejaVu Sans** (400/700), so it renders identically on every OS. German strings
|
|
75
|
+
run ~30% longer than English → **never shrink body below 16px**, and let text wrap.
|
|
76
|
+
|
|
77
|
+
| Role | Class | Notes |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| Display | `text-4xl font-bold` | hero, rare |
|
|
80
|
+
| Screen title (h1) | `text-3xl font-bold text-heading` | |
|
|
81
|
+
| Section (h2) | `text-2xl font-semibold` | |
|
|
82
|
+
| Card/group title (h3) | `text-xl font-semibold` | |
|
|
83
|
+
| **Body** | `text-base` (16px) `leading-relaxed` | the floor for readable copy |
|
|
84
|
+
| Meta / secondary | `text-sm text-text-muted` | |
|
|
85
|
+
| Caption | `text-xs` | avoid for primary content |
|
|
86
|
+
|
|
87
|
+
## Spacing, radii, elevation
|
|
88
|
+
|
|
89
|
+
- **Spacing** — the Tailwind **4px base scale** (`1`=4px … `4`=16px, `6`=24px, `8`=32px).
|
|
90
|
+
Card padding `p-8`, list gaps `gap-3`/`gap-4`, section rhythm `space-y-6`/`8`.
|
|
91
|
+
- **Radii** — `rounded-md` (controls), `rounded-xl` (cards), `rounded-full` (pills/spinner).
|
|
92
|
+
- **Elevation** — `shadow-1` (resting card), `shadow-2` (hover/raised), `shadow-3` (overlays);
|
|
93
|
+
navy-tinted so shadows read as brand, not grey.
|
|
94
|
+
|
|
95
|
+
## Focus & tap targets
|
|
96
|
+
|
|
97
|
+
- **Focus** — visible **2px ring** in `ring` (navy): `focus-visible:outline-2
|
|
98
|
+
focus-visible:outline-offset-2 focus-visible:outline-ring`. Never remove focus styles.
|
|
99
|
+
- **Tap target** — `min-h-tap` / `min-w-tap` (44px). Baked into `Button`; D2 nav and any new
|
|
100
|
+
interactive control must meet it too.
|
|
101
|
+
|
|
102
|
+
## Atom states (`@simscloud/ui`)
|
|
103
|
+
|
|
104
|
+
| Atom | States |
|
|
105
|
+
|---|---|
|
|
106
|
+
| **Button** | variants `primary` / `secondary` / `ghost` / `success` / `warning` / `danger` (status buttons use the AA-safe `-strong` bg + `-stronger` hover) · states default · hover (mouse) · press (same emphasis shade — visible on touch; a mouse click also darkens slightly) · focus-visible (ring) · disabled · **loading** (spinner + `aria-busy`) · sizes md/lg · ≥44px. Hover/press are pointer-event-driven (not CSS `:hover`/`:active`) for consistent touch behavior across browsers. |
|
|
107
|
+
| **Card** | resting (`shadow-1`) · **interactive** (hover lift to `shadow-2`; wrap in a real button/link for keyboard a11y) |
|
|
108
|
+
| **Alert** | `info` / `success` / `warning` / `danger` — left accent (bright fill) + AA-safe `*-strong` title; `role="alert"` (warning/danger) vs `role="status"` (info/success) |
|
|
109
|
+
| **Spinner** | sizes sm/md/lg; `role="status"` + localizable `aria-label` |
|
|
110
|
+
| **Logo** | wordmark/mark × color/white via `src` |
|
|
111
|
+
| **Input** | _deferred — added when the first form screen needs it (D5 / #74)._ |
|
|
112
|
+
|
|
113
|
+
## Open / deferred
|
|
114
|
+
|
|
115
|
+
- **Input atom** — intentionally not built yet (added with the first form, #74).
|
|
116
|
+
- **Dark mode & per-tenant brand** — not enabled; the semantic-variable structure keeps the
|
|
117
|
+
door open (swap the semantic layer, zero atom changes).
|
|
118
|
+
- **Approved key-screen mockups** — the subjective visual direction (#70 part A) is settled
|
|
119
|
+
with stakeholders separately; this doc covers the objective token/accessibility spec.
|
package/package.json
CHANGED
package/tailwind-preset.js
CHANGED
|
@@ -16,51 +16,102 @@
|
|
|
16
16
|
|
|
17
17
|
/** @type {import('tailwindcss').Config} */
|
|
18
18
|
export default {
|
|
19
|
+
future: {
|
|
20
|
+
// Gate `hover:` styles to real pointer devices (`@media (hover: hover)`), so a touch tap
|
|
21
|
+
// doesn't leave a "stuck" hover state until you tap elsewhere. (Default in Tailwind v4.)
|
|
22
|
+
hoverOnlyWhenSupported: true,
|
|
23
|
+
},
|
|
19
24
|
theme: {
|
|
20
25
|
extend: {
|
|
21
26
|
colors: {
|
|
22
27
|
brand: {
|
|
23
|
-
DEFAULT:
|
|
24
|
-
strong:
|
|
25
|
-
|
|
28
|
+
DEFAULT: "var(--color-brand)",
|
|
29
|
+
strong: "var(--color-brand-strong)",
|
|
30
|
+
light: "var(--color-brand-light)",
|
|
31
|
+
soft: "var(--color-brand-soft)",
|
|
26
32
|
},
|
|
27
|
-
|
|
33
|
+
"on-brand": "var(--color-on-brand)",
|
|
28
34
|
|
|
29
|
-
bg:
|
|
35
|
+
bg: "var(--color-bg)",
|
|
30
36
|
surface: {
|
|
31
|
-
DEFAULT:
|
|
32
|
-
muted:
|
|
37
|
+
DEFAULT: "var(--color-surface)",
|
|
38
|
+
muted: "var(--color-surface-muted)",
|
|
33
39
|
},
|
|
34
|
-
header:
|
|
35
|
-
banner:
|
|
36
|
-
overlay:
|
|
40
|
+
header: "var(--color-header-bg)",
|
|
41
|
+
banner: "var(--color-banner)",
|
|
42
|
+
overlay: "var(--color-overlay)",
|
|
37
43
|
|
|
38
44
|
text: {
|
|
39
|
-
DEFAULT:
|
|
40
|
-
muted:
|
|
41
|
-
inverse:
|
|
45
|
+
DEFAULT: "var(--color-text)",
|
|
46
|
+
muted: "var(--color-text-muted)",
|
|
47
|
+
inverse: "var(--color-text-inverse)",
|
|
42
48
|
},
|
|
43
|
-
heading:
|
|
49
|
+
heading: "var(--color-heading)",
|
|
44
50
|
|
|
45
|
-
line:
|
|
46
|
-
ring:
|
|
51
|
+
line: "var(--color-border)",
|
|
52
|
+
ring: "var(--color-ring)",
|
|
47
53
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
// DEFAULT = fill/border/icon; `strong` = accessible text + solid-button bg;
|
|
55
|
+
// `stronger` = that button's hover (all white-text-AA on the strong/stronger shades).
|
|
56
|
+
success: {
|
|
57
|
+
DEFAULT: "var(--color-success)",
|
|
58
|
+
strong: "var(--color-success-strong)",
|
|
59
|
+
stronger: "var(--color-success-stronger)",
|
|
60
|
+
soft: "var(--color-success-soft)",
|
|
61
|
+
},
|
|
62
|
+
warning: {
|
|
63
|
+
DEFAULT: "var(--color-warning)",
|
|
64
|
+
strong: "var(--color-warning-strong)",
|
|
65
|
+
stronger: "var(--color-warning-stronger)",
|
|
66
|
+
soft: "var(--color-warning-soft)",
|
|
67
|
+
},
|
|
68
|
+
danger: {
|
|
69
|
+
DEFAULT: "var(--color-danger)",
|
|
70
|
+
strong: "var(--color-danger-strong)",
|
|
71
|
+
stronger: "var(--color-danger-stronger)",
|
|
72
|
+
soft: "var(--color-danger-soft)",
|
|
73
|
+
},
|
|
51
74
|
},
|
|
52
75
|
|
|
53
76
|
fontFamily: {
|
|
54
|
-
sans:
|
|
55
|
-
display:
|
|
77
|
+
sans: "var(--font-sans)",
|
|
78
|
+
display: "var(--font-display)",
|
|
56
79
|
},
|
|
57
80
|
|
|
58
81
|
borderRadius: {
|
|
59
|
-
sm:
|
|
60
|
-
md:
|
|
61
|
-
lg:
|
|
62
|
-
xl:
|
|
63
|
-
full:
|
|
82
|
+
sm: "var(--radius-sm)",
|
|
83
|
+
md: "var(--radius-md)",
|
|
84
|
+
lg: "var(--radius-lg)",
|
|
85
|
+
xl: "var(--radius-xl)",
|
|
86
|
+
full: "var(--radius-full)",
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
boxShadow: {
|
|
90
|
+
1: "var(--shadow-1)",
|
|
91
|
+
2: "var(--shadow-2)",
|
|
92
|
+
3: "var(--shadow-3)",
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
// Motion — one-shot success "pop" for the shared finish animation (FinishScreen · #73).
|
|
96
|
+
// Applied as `animate-finish-pop motion-reduce:animate-none`, so a `prefers-reduced-motion`
|
|
97
|
+
// viewer simply drops the pop and lands on the static, fully-visible success state.
|
|
98
|
+
keyframes: {
|
|
99
|
+
"finish-pop": {
|
|
100
|
+
"0%": { transform: "scale(0.6)", opacity: "0" },
|
|
101
|
+
"60%": { transform: "scale(1.06)", opacity: "1" },
|
|
102
|
+
"100%": { transform: "scale(1)", opacity: "1" },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
animation: {
|
|
106
|
+
"finish-pop": "finish-pop 380ms cubic-bezier(0.2, 0.7, 0.3, 1) both",
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// Minimum touch target (44px) → `min-h-tap` / `min-w-tap` on interactive atoms.
|
|
110
|
+
minHeight: {
|
|
111
|
+
tap: "var(--tap-target-min)",
|
|
112
|
+
},
|
|
113
|
+
minWidth: {
|
|
114
|
+
tap: "var(--tap-target-min)",
|
|
64
115
|
},
|
|
65
116
|
},
|
|
66
117
|
},
|
package/tokens.css
CHANGED
|
@@ -44,15 +44,26 @@
|
|
|
44
44
|
--gray-300: #cbd2de;
|
|
45
45
|
--gray-400: #9aa3b2;
|
|
46
46
|
--gray-500: #64748b;
|
|
47
|
+
--gray-550: #5b6776; /* muted text — passes AA on the surface bg (gray-500 falls just short) */
|
|
47
48
|
--gray-600: #475569;
|
|
48
49
|
--gray-700: #334155;
|
|
49
50
|
--gray-800: #1e293b;
|
|
50
51
|
--gray-900: #0f172a;
|
|
51
52
|
|
|
52
|
-
/* Support / status
|
|
53
|
+
/* Support / status. The -500s are FILL/border/icon colors (not for body text — they fail
|
|
54
|
+
AA on white). The -700s are the accessible TEXT shades (≥4.7:1 on bg AND surface). */
|
|
53
55
|
--green-500: #16a34a;
|
|
54
56
|
--amber-500: #f59e0b;
|
|
55
57
|
--red-500: #dc2626;
|
|
58
|
+
--green-700: #15803d;
|
|
59
|
+
--amber-700: #b45309;
|
|
60
|
+
--red-700: #b91c1c;
|
|
61
|
+
--green-900: #14532d; /* solid status-button hover — a clearly darker step from -700 */
|
|
62
|
+
--amber-900: #78350f;
|
|
63
|
+
--red-900: #7f1d1d;
|
|
64
|
+
--green-50: #f0fdf4; /* pale status tints — row/section wash behind -700 text (passes AA) */
|
|
65
|
+
--amber-50: #fffbeb;
|
|
66
|
+
--red-50: #fef2f2;
|
|
56
67
|
|
|
57
68
|
/* Typography — DejaVu Sans (matches the logo wordmark; self-hosted, so it
|
|
58
69
|
renders identically on every OS). The @font-face lives in the consuming
|
|
@@ -67,6 +78,14 @@
|
|
|
67
78
|
--radius-xl: 1.5rem;
|
|
68
79
|
--radius-full: 9999px;
|
|
69
80
|
|
|
81
|
+
/* Elevation — navy-tinted (rgb 0 0 40) so shadows read as part of the brand, not grey. */
|
|
82
|
+
--shadow-1: 0 1px 2px 0 rgb(0 0 40 / 0.06);
|
|
83
|
+
--shadow-2: 0 4px 12px -2px rgb(0 0 40 / 0.10);
|
|
84
|
+
--shadow-3: 0 12px 32px -8px rgb(0 0 40 / 0.16);
|
|
85
|
+
|
|
86
|
+
/* Minimum touch target (WCAG 2.5.5; tablets/phones are the primary surface). */
|
|
87
|
+
--tap-target-min: 2.75rem; /* 44px */
|
|
88
|
+
|
|
70
89
|
/* ========================================================================
|
|
71
90
|
* Layer 2 — SEMANTIC (light theme)
|
|
72
91
|
* ===================================================================== */
|
|
@@ -74,6 +93,7 @@
|
|
|
74
93
|
/* Brand */
|
|
75
94
|
--color-brand: var(--navy-600);
|
|
76
95
|
--color-brand-strong: var(--navy-800);
|
|
96
|
+
--color-brand-light: var(--navy-400); /* lighter hover for the near-black primary button */
|
|
77
97
|
--color-brand-soft: var(--navy-50);
|
|
78
98
|
--color-on-brand: #ffffff;
|
|
79
99
|
|
|
@@ -87,7 +107,7 @@
|
|
|
87
107
|
|
|
88
108
|
/* Text */
|
|
89
109
|
--color-text: var(--gray-900);
|
|
90
|
-
--color-text-muted: var(--gray-
|
|
110
|
+
--color-text-muted: var(--gray-550);
|
|
91
111
|
--color-text-inverse: #ffffff;
|
|
92
112
|
--color-heading: var(--navy-800);
|
|
93
113
|
|
|
@@ -95,10 +115,20 @@
|
|
|
95
115
|
--color-border: var(--gray-200);
|
|
96
116
|
--color-ring: var(--navy-600);
|
|
97
117
|
|
|
98
|
-
/* Status
|
|
99
|
-
|
|
100
|
-
--color-
|
|
101
|
-
--color-
|
|
118
|
+
/* Status. DEFAULT = fill/border/icon. *-strong = accessible text on light surfaces AND
|
|
119
|
+
the bg of solid status buttons (white text passes AA). *-stronger = those buttons' hover. */
|
|
120
|
+
--color-success: var(--green-500);
|
|
121
|
+
--color-warning: var(--amber-500);
|
|
122
|
+
--color-danger: var(--red-500);
|
|
123
|
+
--color-success-strong: var(--green-700);
|
|
124
|
+
--color-warning-strong: var(--amber-700);
|
|
125
|
+
--color-danger-strong: var(--red-700);
|
|
126
|
+
--color-success-stronger: var(--green-900);
|
|
127
|
+
--color-warning-stronger: var(--amber-900);
|
|
128
|
+
--color-danger-stronger: var(--red-900);
|
|
129
|
+
--color-success-soft: var(--green-50); /* subtle fill: import-wizard row wash, callouts */
|
|
130
|
+
--color-warning-soft: var(--amber-50);
|
|
131
|
+
--color-danger-soft: var(--red-50);
|
|
102
132
|
}
|
|
103
133
|
|
|
104
134
|
/* =============================================================================
|