@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simscloud/design",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "Shared SIMS design tokens — CSS variables, Tailwind preset, and web fonts. Framework-agnostic (no React).",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -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: 'var(--color-brand)',
24
- strong: 'var(--color-brand-strong)',
25
- soft: 'var(--color-brand-soft)',
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
- 'on-brand': 'var(--color-on-brand)',
33
+ "on-brand": "var(--color-on-brand)",
28
34
 
29
- bg: 'var(--color-bg)',
35
+ bg: "var(--color-bg)",
30
36
  surface: {
31
- DEFAULT: 'var(--color-surface)',
32
- muted: 'var(--color-surface-muted)',
37
+ DEFAULT: "var(--color-surface)",
38
+ muted: "var(--color-surface-muted)",
33
39
  },
34
- header: 'var(--color-header-bg)',
35
- banner: 'var(--color-banner)',
36
- overlay: 'var(--color-overlay)',
40
+ header: "var(--color-header-bg)",
41
+ banner: "var(--color-banner)",
42
+ overlay: "var(--color-overlay)",
37
43
 
38
44
  text: {
39
- DEFAULT: 'var(--color-text)',
40
- muted: 'var(--color-text-muted)',
41
- inverse: 'var(--color-text-inverse)',
45
+ DEFAULT: "var(--color-text)",
46
+ muted: "var(--color-text-muted)",
47
+ inverse: "var(--color-text-inverse)",
42
48
  },
43
- heading: 'var(--color-heading)',
49
+ heading: "var(--color-heading)",
44
50
 
45
- line: 'var(--color-border)',
46
- ring: 'var(--color-ring)',
51
+ line: "var(--color-border)",
52
+ ring: "var(--color-ring)",
47
53
 
48
- success: 'var(--color-success)',
49
- warning: 'var(--color-warning)',
50
- danger: 'var(--color-danger)',
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: 'var(--font-sans)',
55
- display: 'var(--font-display)',
77
+ sans: "var(--font-sans)",
78
+ display: "var(--font-display)",
56
79
  },
57
80
 
58
81
  borderRadius: {
59
- sm: 'var(--radius-sm)',
60
- md: 'var(--radius-md)',
61
- lg: 'var(--radius-lg)',
62
- xl: 'var(--radius-xl)',
63
- full: 'var(--radius-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-500);
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
- --color-success: var(--green-500);
100
- --color-warning: var(--amber-500);
101
- --color-danger: var(--red-500);
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
  /* =============================================================================