Files
vergabe-teilnahme/history/2026-05-23-whynot-design-cross-framework-analysis.md
tegwick fbb8def9ce Plan WP-0017: whynot-design adoption (tokens + CSS only)
Add cross-framework analysis to history/ and WP-0017 workplan for the
tokens+CSS phase of adopting ~/whynot-design. Component port deferred until
upstream ships Lit web components and missing atoms (Card, Modal, Input,
Table, Toast).

Decisions captured: vendor (not npm), big-bang swap (not pilot), keep
off-spec red for btn-danger until upstream defines one.
2026-05-23 19:09:18 +02:00

362 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
date: 2026-05-23
topic: whynot-design adoption — cross-framework analysis
status: research / decision pending
author: claude (opus-4.7)
related:
- ~/whynot-design (the React DS in question)
- ~/whynot-control (the org control surface — explicitly out of scope)
- vergabe-teilnahme (the consuming Django app)
follow-ups:
- workplan: VERGABE_TEILNAHME-WP-0017 (whynot-design adoption) — not yet drafted, depends on strategy decision below
- possible new repo: whynot-design-django — see §5
---
# whynot-design — cross-framework adoption analysis
> Persisted from a research conversation on 2026-05-23. The conversation was triggered
> by the question "should vergabe-teilnahme adopt the whynot design system, and how?"
> The strategic question opened up because whynot-design ships React components
> while vergabe-teilnahme is Django + HTMX + Tailwind.
## 1. The headline insight
There is **no community** around "React → Django component porting." That pattern
doesn't exist as a tool, library, or codified practice. The reason: cross-framework
UI sharing has, since ~2020, converged on **Web Components** as the standard answer.
Every major design system that supports multiple frameworks (Shoelace / Web Awesome,
IBM Carbon, Adobe Spectrum, Salesforce Lightning, Material Web) either ships Web
Components or maintains parallel hand-rolled implementations per framework. There is
no third option that the industry has validated.
The W3C Design Tokens Community Group spec reached its **first stable version in
October 2025**, with Style Dictionary, Figma, Penpot, Tokens Studio, and others as
reference implementations. Tokens as a cross-framework contract are now genuinely
portable; *components* still aren't, except via web components.
The strategic question therefore reshapes itself: **do we accept the
parallel-implementations cost, or do we change what `whynot-design` actually ships?**
## 2. Research findings — existing approaches and their communities
| Approach | What it is | Community / activity | Fit for whynot |
|---|---|---|---|
| **Web Components** (Lit, Stencil) | Browser-standard custom elements; works in any framework | Very active; React 19 finally scores perfect on custom-elements-everywhere.com | **High** — best general answer if we're willing to refactor whynot-design |
| **Shoelace / Web Awesome** | Pre-built Web Components UI kit (Shoelace's successor) | Large, active OSS community | Not direct (competing DS), but proves the model. Has explicit HTMX integration guides |
| **Stencil JS** | Compiler that emits framework-specific bindings from one source | Mature; IBM/Apple use it; declining mindshare vs. Lit | **Medium** — heavier than Lit but generates per-framework wrappers automatically |
| **Style Dictionary + DTCG tokens** | One token file → CSS / Tailwind / iOS / Android / Flutter | W3C-backed, stable spec since 2025-10 | **High** — should be our token pipeline regardless of component choice |
| **Carbon / Spectrum / Lightning / Material pattern** | Single design source, hand-maintained parallel packages per framework | Active but they're 100+ engineer teams | **Low** at our scale — the "expensive but principled" reference path |
| **Single-spa / micro-frontends** | Multiple frameworks coexisting in one app | Niche, mostly enterprise integration | **Off-topic** — not about sharing components, about hosting heterogeneous apps |
| **django-cotton / django-components / django-bird** | Django-template-native component libraries; HTMX-friendly | Active, growing in 20242026; mature enough to depend on | **High** as the *Django-side runtime* — but doesn't solve cross-framework, just gives Django a real component model |
| **"Port React to Django" libraries** | (doesn't exist) | None | None |
**Read of the field:** the world has settled on "tokens are cross-framework;
components are framework-native." Most teams either pick web components (one
implementation, runs anywhere with some friction) or accept parallel implementations
from a shared design language.
## 3. Three viable strategies for whynot's situation
**Strategy A — Pivot whynot-design to Web Components.**
Rewrite `Atoms.jsx` + `Chrome.jsx` as Lit components. Ship one runtime artefact
(`@whynot/design`) that Django + HTMX templates can drop into a page via plain
`<whynot-button>` tags. No React dependency anywhere. Tailwind / CSS variables
continue to drive theming.
**Strategy B — Keep whynot-design React-canonical; add a parallel `whynot-design-django` repo.**
Treat the React JSX as the *visual + API specification*. Hand-port to Django
partials in a separate, reusable repo so vergabe-teilnahme and future Django
consumers share one implementation. Tokens stay in whynot-design (single source).
**Strategy C — Tokens only; components are framework-local forever.**
Don't promise component parity. Vergabe re-implements whynot's *look* using the
tokens but its components live and die in vergabe-teilnahme. Other Django apps
would do the same fork. Bernd implied this is unsatisfying — he wants a real
component library for cross-project consistency.
The real choice is **A vs. B**.
## 4. Pros / cons — A (Web Components) vs. B (parallel Django repo)
### Strategy A — Pivot to Web Components (Lit)
**Pros**
- *One* implementation. Zero divergence risk. Bug fixes ship once.
- Standards-backed; outlives any framework choice. Works in vanilla HTML, Django
templates, future Vue/Svelte/Solid apps, marketing sites, claude.ai artifacts.
- HTMX swaps work cleanly — web components are just DOM; HTMX doesn't care.
- The community is *here*. Tutorials, debugging tools, Storybook integration,
accessibility testing infrastructure all exist.
- The Claude atelier in claude.ai can still output JSX prototypes; Lit ports are
mechanical once the design is decided.
**Cons**
- One-time rewrite cost of the existing 11 React components. Real, but small
(~267 lines of JSX, not a library of 200 components).
- Web components have Shadow DOM trade-offs: form-association, deep style scoping,
and slotting have learning curves. Workable but real friction.
- React-shaped APIs (`<Button variant="primary">`) translate to attributes /
properties (`<wn-button variant="primary">`). Slightly less ergonomic in React,
equally ergonomic everywhere else.
- Lose the claude.ai → npm symmetry: claude.ai design tool outputs JSX; you'd
convert each new component.
- Whoever maintains whynot-design has to learn Lit (small surface area, but new
vocabulary).
### Strategy B — Parallel `whynot-design-django` repo
**Pros**
- Each implementation is idiomatic for its stack. Django templates feel like
Django; React feels like React.
- No new technology to learn. Both repos use mature, well-understood patterns.
- Django consumers get to use `django-cotton` or `django-bird` — real ergonomics,
not a half-port.
- claude.ai → JSX → Lit conversion step is avoided. The atelier output is
*directly* the React reference.
**Cons**
- *Two* implementations forever. Every component change needs both sides updated;
PRs need cross-repo review discipline.
- API drift is inevitable. By v0.5 the React `<Button>` and Django `{% button %}`
will have diverged in props/slots/behavior unless enforced with conformance tests.
- A *third* consumer stack (Vue, vanilla HTML, native mobile) means a *third* repo.
Cost scales linearly with the number of stacks; A's cost is roughly constant.
- The "community" of "people maintaining a React→Django twin DS" is exactly Bernd
and whoever he recruits. No prior art to lean on.
- Visual regression testing has to run twice (Playwright against examples in both
repos) to ensure parity.
### Recommendation
**Strategy A is the right long-term answer**; B is the right *interim* answer if
we don't want to halt vergabe-teilnahme to do a Lit rewrite first.
A real third path: **A scheduled, B today.** Build B as a reusable Django port now
to unblock vergabe-teilnahme; treat it as the *bridge* until whynot-design pivots
to Lit at, say, v0.5. The Django repo becomes obsolete when A lands — that's a
feature, not a bug; it forces a clear retirement decision instead of accumulating
tech debt.
If we don't want to commit to a future Lit pivot, then B is the right choice on its
own — just accept the linear-cost-per-stack reality.
## 5. Naming, establishing, workflow — if we go with B
### Naming proposals
| Candidate | Pros | Cons |
|---|---|---|
| **`whynot-design-django`** (recommended) | Mirrors `@whynot/design`. Unambiguous about stack. Discoverable. | Slightly long. |
| `whynot-django` | Short. | Ambiguous — sounds like a Django app, not a DS port. |
| `whynot-design-py` | Hedges for Jinja2/Flask later. | Vague — Python doesn't pick a template engine. |
| `whynot-dj` | Compact. | Cryptic. |
| `whynot-templates` | Honest about what it is. | Doesn't carry the "design" meaning. |
**Recommend `whynot-design-django`.** If a Jinja port appears, it gets its own repo
(`whynot-design-jinja`), not a confusing rename.
### Establishing the repo (concrete steps)
1. **Gitea org placement:** `gitea-remote:whynot/whynot-design-django.git` — same
org as `whynot-design`, signaling sibling status.
2. **Package shape:** a pip-installable Django app (`pyproject.toml`, importable as
`whynot_design`). Two install paths:
- `pip install git+ssh://...@v0.1.0` (matches whynot-design's tag-pinned discipline).
- For dev: editable install (`pip install -e ../whynot-design-django`).
3. **Layout** (matches Django conventions; mirrors whynot-design's structure):
```
whynot-design-django/
├── README.md ← restates the "match the React spec" contract
├── CONTRIBUTING.md ← parity rules: any change here must follow a whynot-design change
├── CHANGELOG.md
├── pyproject.toml
├── whynot_design/
│ ├── __init__.py
│ ├── apps.py
│ ├── templates/whynot_design/
│ │ ├── atoms/eyebrow.html, tag.html, button.html, stage_dot.html, stamp.html, icon.html
│ │ ├── chrome/top_nav.html, sidebar.html, page_header.html, pipeline_strip.html
│ │ └── _base.html ← imports the CSS once
│ ├── templatetags/
│ │ └── whynot.py ← {% wn_button variant="primary" %}…{% endwn_button %}, etc.
│ └── static/whynot_design/
│ └── colors_and_type.css ← vendored from @whynot/design (synced)
├── tests/
│ ├── test_components.py ← Django test client, asserts rendered markup matches expected shape
│ └── parity/ ← Playwright comparison of Django render vs. React render
└── examples/ ← a Django demo project rendering every component
```
4. **Token sync:** a `scripts/sync-from-whynot-design.py` that copies
`colors_and_type.css` and `tokens/*.json` from a pinned whynot-design ref into
the static dir + generates a Django settings constant for tokens. Run on every
whynot-design version bump.
5. **Underlying component mechanism:** plain `{% include %}` partials (simplest, no
new dep), `django-cotton` (HTML-like syntax, very ergonomic, active community),
or `django-components` (most powerful, heaviest). My lean: **`django-cotton`** —
its `<c-button variant="primary">…</c-button>` syntax parallels React JSX best,
so the parity contract is more obvious.
### What changes in the workflow
Before:
```
claude.ai atelier ──► whynot-design ──► consumer
```
After:
```
claude.ai atelier ──► whynot-design ──┬─► whynot-design-django ──► vergabe-teilnahme (+ future Django apps)
├─► future stacks: whynot-design-jinja / -svelte / -vue / …
└─► tokens flow into all child repos via a sync script
```
Concretely, this adds three new ongoing obligations:
1. **Token sync gate.** When whynot-design ships a token change, the Django repo's
sync script must run before its next release. CI check: the static CSS in
`whynot-design-django` must hash-match the CSS at the pinned upstream tag.
2. **Component parity gate.** When whynot-design adds or modifies a component, an
issue is opened automatically in `whynot-design-django` referencing the upstream
PR. A release is blocked until the issue closes. Encode in `CONTRIBUTING.md`
first; later, a CI cross-repo check.
3. **Conformance tests.** Each component's Django partial has a Playwright snapshot
that's compared against the same component rendered in whynot-design's
`examples/whynot-control/`. Drift = failing build.
Vergabe-teilnahme's workplan therefore depends on **whynot-design-django** existing
first. The vergabe workplan becomes: install the new package, replace partials with
cotton/include tags, retheme.
## 6. Component-level inventory — whynot-design vs. vergabe-teilnahme
| whynot-design (React) | vergabe-teilnahme (Django) | Match | Gap action |
|---|---|---|---|
| `Eyebrow` (mono uppercase label, fg-3) | — | Missing | New partial `atoms/eyebrow.html` |
| `Tag` (mono uppercase pill: default / active / draft) | `status_badge.html` (different semantics — domain statuses) | Partial | Replace `status_badge` content with `Tag` variants; keep status→variant mapping |
| `Button` (3 variants: primary / secondary / ghost; lucide icon support) | CSS classes `.btn-primary`, `.btn-secondary`, `.btn-danger`, `.btn-ghost` | Strong overlap; `btn-danger` not upstream | Adopt 3 base variants; propose `danger` upstream OR retire it (whynot's voice avoids red/destructive emphasis) |
| `StageDot` (S0S4 signal levels with grey-ramp + yellow S4) | `phase_nav.html` (numbered phase circles: todo/active/done/warn) | Semantically different — *whynot stages* ≠ *vergabe phases* | Keep both as distinct atoms. Propose `PhaseDot` upstream OR keep vergabe-local |
| `Stamp` (yellow rotated "DRAFT") | — | Missing | New partial; useful for unfreigegeben items |
| `Icon` (lucide via `data-lucide`) | inline SVGs / class-based | Missing as a component | New partial that wraps lucide; pull lucide JS into base.html |
| `TopNav` | `topbar.html` | Both exist; styling and density differ significantly | Reskin vergabe's topbar to whynot's structure (search box + primary action right-aligned) |
| `Sidebar` | `sidebar.html` | Both exist; nav model differs (vergabe has phase grouping; whynot has Work / Control docs grouping) | Adopt whynot's chrome (item style, eyebrow section headers) but keep vergabe's nav items |
| `PageHeader` (eyebrow + h1 + lede + actions) | — (inline per template) | Missing | New partial; refactor all page templates to use it |
| `PipelineStrip` (5-stage horizontal indicator) | `phase_nav.html` (vertical / different) | Semantically related, visually different | Decide: align on whynot's strip OR propose `PipelineStrip` variant upstream |
| — | `field_row.html` (vergabe-specific) | n/a | Propose upstream — it's a generic key/value display row |
| — | `breadcrumb.html` | n/a | Propose upstream as `Breadcrumb` |
| — | `feedback_button.html`, `feedback_modal.html`, `feedback_success.html` | n/a (vergabe-specific UX flow) | Keep vergabe-local; product UI, not DS atoms |
| — | `freigabe_modal.html`, `freigabe_success.html` | n/a (vergabe-specific) | Keep vergabe-local |
| — | `search_results.html` (HTMX results target) | n/a | Probably vergabe-local, but a `Listbox`/`Combobox` atom could live upstream — defer |
### Gap summary
- **5 net-new atoms** in vergabe (Eyebrow, Stamp, PageHeader, Icon wrapper, PipelineStrip).
- **4 atoms to align** (Tag ↔ status_badge, Button variants, TopNav, Sidebar).
- **2 atoms to propose upstream** (Breadcrumb, FieldRow).
- **1 semantic mismatch to resolve** (Stage vs Phase — they're different concepts).
- **All vergabe-specific flow UI** (feedback, freigabe, search_results) stays
vergabe-local — that's correct, those are product UI, not design system.
### Components missing from whynot-design — full gap list
These are components that are absent from `whynot-design` today and would need to
land there (or in `whynot-design-django`) before vergabe-teilnahme can fully adopt
the system. Grouped by origin.
#### Already exists in vergabe-teilnahme — candidates to promote upstream
1. **`Breadcrumb`** — vergabe's `breadcrumb.html`. Trivially generic; no
vergabe-specific semantics.
2. **`FieldRow`** — vergabe's `field_row.html` (label + value, 3-column grid). The
pattern recurs across detail pages; clearly a DS atom.
3. **`PhaseDot` / `PhaseNav`** *(name TBD)* — vergabe's `phase_nav.html`.
Semantically distinct from whynot's `StageDot` (numbered phases with
todo / active / done / warn states, not S0S4 signal levels). Either upstream
as a sibling component, or kept vergabe-local. **Open decision.**
#### Doesn't exist anywhere yet, but the DS clearly needs them
These are components that vergabe-teilnahme has as raw Tailwind classes or ad-hoc
markup, and whynot-design has no equivalent — but any non-trivial app needs them,
so they're real DS gaps:
4. **`Card`** — whynot's house rules reference cards ("no shadows on cards,"
"04px radii for cards/sheets") but there is no `Card` JSX component. Vergabe
has `.card` as a Tailwind class. **Surprising omission given the explicit
design rules.**
5. **`Input` / `Textarea` / `Select`** — whynot ships no form primitives at all.
Vergabe has `.form-input` and `.form-label` CSS classes. Required before any
form-based whynot artefact ships.
6. **`Modal` / `Dialog`** — whynot ships none. Vergabe has two (`freigabe_modal`,
`feedback_modal`). Whynot's house rules even prescribe modal radius (8px),
confirming the intent — just the component is missing.
7. **`Table`** — whynot ships none. Vergabe has `.table-base`, `.table-header`,
`.table-row` classes. Any data-heavy view (likely most of vergabe) needs it.
8. **`Toast` / inline success banner** *(name TBD)* — whynot ships none. Vergabe
has `freigabe_success.html` and `feedback_success.html` as ad-hoc partials.
Even with whynot's quiet voice, success / error states need a defined visual.
#### Worth flagging but defer until first real need
9. **`SearchInput`** — currently inlined inside whynot's `TopNav`. Pulling it out
as a standalone atom would be useful (vergabe's `search_results.html` is a
target for an HTMX-driven search box).
10. **`EmptyState`** — neither codebase has one; vergabe will need it for empty
list views. Worth designing once before either side fills the gap locally.
11. **`Tabs`, `Dropdown`, `Tooltip`, `Pagination`, `Avatar`** — common DS atoms;
neither side has them. Don't add speculatively; add when the first concrete
use case appears.
**Count:** 3 vergabe-existing to promote, 5 genuine DS-level gaps, 5 deferred.
The 5 "genuine DS-level gaps" — especially `Card`, `Input`, `Modal`, `Table` — are
**blockers for adoption**: vergabe can't replace its current chrome without them
existing somewhere shared.
### Stylistic gaps to address before "done"
- Vergabe currently uses Tailwind utility classes pervasively
(`.btn-primary { @apply … }`); whynot uses inline-style CSS-variable assignment.
The Django port should commit to one — recommend CSS variable + utility classes
(re-derive whynot's component styles as Tailwind `@apply` blocks that read from
CSS vars), so HTMX-injected fragments don't need their own style scaffolding.
- Lucide icons aren't wired into vergabe. Need a base.html script tag + an icon partial.
- Font stack mismatch: vergabe is `ui-sans-serif`; whynot is IBM Plex Sans / Mono /
Serif. Importing whynot's CSS handles this — but it pulls Google Fonts at runtime.
Decide self-host vs. CDN.
- Vergabe's brand blue (`#3b5bdb`) has to go entirely. No mid-state — the whynot
aesthetic forbids it.
## 7. Recommendation and open decision
**Recommendation:** Strategy B (parallel `whynot-design-django` repo) as the
immediate move, with the README explicitly framing it as a **bridge** that becomes
obsolete if whynot-design eventually pivots to Lit web components. Concretely this
means **three workplans**, not one:
1. **`whynot-design`**: small additions — tag v0.1.0, add upstream candidates
(`Breadcrumb`, `FieldRow`), document the parity contract for downstream Django port.
2. **`whynot-design-django`** (new repo): bootstrap, ship v0.1.0 with the 11
components + 2 vergabe-contributed atoms, set up token-sync + parity tests.
3. **`vergabe-teilnahme`** (`WP-0017`): consume `whynot-design-django`, retheme,
pilot on one page first, then full sweep.
**Open decision (blocks workplan drafting):**
Strategy A (pivot to Lit immediately) vs. Strategy B (parallel Django repo) vs.
"B now, A later" — Bernd to choose. If "B now, A later," add a workplan stub for
the Lit pivot in `whynot-design` itself so it doesn't get forgotten. If "A from the
start," the Django repo doesn't exist and the workplans collapse to two.
## Sources
- [Shoelace / Web Awesome](https://shoelace.style/) — framework-agnostic web components DS
- [Lit + Web Components for cross-framework UIs](https://thenewstack.io/how-to-build-framework-agnostic-uis-with-web-components/)
- [HTMX + Shoelace example](https://binaryigor.com/htmx-with-shoelace-framework-agnostic-components-in-an-example-app.html)
- [W3C Design Tokens spec first stable version (Oct 2025)](https://www.w3.org/community/design-tokens/2025/10/28/design-tokens-specification-reaches-first-stable-version/)
- [Style Dictionary cross-platform tokens](https://styledictionary.com/info/tokens/)
- [Django Cotton — HTML-like component syntax](https://django-cotton.com/)
- [django-bird](https://github.com/joshuadavidthomas/django-bird)
- [Salesforce — Beyond Components: multi-framework DS](https://medium.com/salesforce-ux/beyond-components-a-design-system-to-support-multiple-frameworks-cb1e4d511f66)
- [AgnosticUI post-mortem (rewrite to Lit)](https://frontendmasters.com/blog/post-mortem-rewriting-agnosticui-with-lit-web-components/)