jsgui3-server 0.0.148 → 0.0.150
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/.github/agents/Mobile Developer.agent.md +89 -0
- package/.github/workflows/control-scan-manifest-check.yml +31 -0
- package/AGENTS.md +4 -0
- package/README.md +215 -3
- package/admin-ui/client.js +81 -51
- package/admin-ui/v1/admin_auth_service.js +197 -0
- package/admin-ui/v1/admin_user_store.js +71 -0
- package/admin-ui/v1/client.js +17 -0
- package/admin-ui/v1/controls/admin_shell.js +1399 -0
- package/admin-ui/v1/controls/group_box.js +84 -0
- package/admin-ui/v1/controls/stat_card.js +125 -0
- package/admin-ui/v1/server.js +658 -0
- package/admin-ui/v1/utils/formatters.js +68 -0
- package/dev-status.svg +139 -0
- package/docs/admin-extension-guide.md +345 -0
- package/docs/api-reference.md +301 -43
- package/docs/books/adaptive-control-improvements/01-control-candidate-matrix.md +122 -0
- package/docs/books/adaptive-control-improvements/02-tier-1-layout-playbooks.md +207 -0
- package/docs/books/adaptive-control-improvements/03-tier-2-navigation-form-overlay.md +140 -0
- package/docs/books/adaptive-control-improvements/04-cross-cutting-platform-functionality.md +141 -0
- package/docs/books/adaptive-control-improvements/05-styling-theming-density-upgrades.md +114 -0
- package/docs/books/adaptive-control-improvements/06-testing-quality-gates.md +97 -0
- package/docs/books/adaptive-control-improvements/07-delivery-roadmap-and-ownership.md +137 -0
- package/docs/books/adaptive-control-improvements/08-appendix-tier1-acceptance-and-pr-templates.md +261 -0
- package/docs/books/adaptive-control-improvements/README.md +66 -0
- package/docs/books/admin-ui-authentication/01-threat-model-and-goals.md +124 -0
- package/docs/books/admin-ui-authentication/02-session-model-and-token-model.md +75 -0
- package/docs/books/admin-ui-authentication/03-auth-middleware-patterns.md +77 -0
- package/docs/books/admin-ui-authentication/README.md +25 -0
- package/docs/books/creating-a-new-admin-ui/01-introduction-and-vision.md +130 -0
- package/docs/books/creating-a-new-admin-ui/02-architecture-and-data-flow.md +298 -0
- package/docs/books/creating-a-new-admin-ui/03-server-introspection.md +381 -0
- package/docs/books/creating-a-new-admin-ui/04-admin-module-adapter-layer.md +592 -0
- package/docs/books/creating-a-new-admin-ui/05-domain-controls-stat-cards-and-gauges.md +513 -0
- package/docs/books/creating-a-new-admin-ui/06-domain-controls-process-manager.md +544 -0
- package/docs/books/creating-a-new-admin-ui/07-domain-controls-resource-pool-inspector.md +493 -0
- package/docs/books/creating-a-new-admin-ui/08-domain-controls-route-table-and-api-explorer.md +586 -0
- package/docs/books/creating-a-new-admin-ui/09-domain-controls-log-viewer-and-activity-feed.md +490 -0
- package/docs/books/creating-a-new-admin-ui/10-domain-controls-build-status-and-bundle-inspector.md +526 -0
- package/docs/books/creating-a-new-admin-ui/11-domain-controls-configuration-panel.md +808 -0
- package/docs/books/creating-a-new-admin-ui/12-admin-shell-layout-sidebar-navigation.md +210 -0
- package/docs/books/creating-a-new-admin-ui/13-telemetry-integration.md +556 -0
- package/docs/books/creating-a-new-admin-ui/14-realtime-sse-observable-integration.md +485 -0
- package/docs/books/creating-a-new-admin-ui/15-styling-theming-aero-design-system.md +521 -0
- package/docs/books/creating-a-new-admin-ui/16-testing-and-quality-assurance.md +147 -0
- package/docs/books/creating-a-new-admin-ui/17-next-steps-process-resource-roadmap.md +356 -0
- package/docs/books/creating-a-new-admin-ui/README.md +68 -0
- package/docs/books/device-adaptive-composition/01-platform-feature-audit.md +177 -0
- package/docs/books/device-adaptive-composition/02-responsive-composition-model.md +187 -0
- package/docs/books/device-adaptive-composition/03-data-model-vs-view-model.md +231 -0
- package/docs/books/device-adaptive-composition/04-styling-theme-breakpoints.md +234 -0
- package/docs/books/device-adaptive-composition/05-showcase-app-multi-device-assessment.md +193 -0
- package/docs/books/device-adaptive-composition/06-implementation-patterns-and-apis.md +346 -0
- package/docs/books/device-adaptive-composition/07-testing-harness-and-quality-gates.md +265 -0
- package/docs/books/device-adaptive-composition/08-roadmap-and-adoption-plan.md +250 -0
- package/docs/books/device-adaptive-composition/README.md +47 -0
- package/docs/books/jsgui3-bundling-research-book/00-table-of-contents.md +35 -0
- package/docs/books/jsgui3-bundling-research-book/01-pipeline-and-runtime-semantics.md +34 -0
- package/docs/books/jsgui3-bundling-research-book/02-javascript-bundling-core.md +36 -0
- package/docs/books/jsgui3-bundling-research-book/03-style-extraction-and-css-compilation.md +35 -0
- package/docs/books/jsgui3-bundling-research-book/04-static-publishing-and-delivery.md +39 -0
- package/docs/books/jsgui3-bundling-research-book/05-current-limits-and-size-bloat-vectors.md +25 -0
- package/docs/books/jsgui3-bundling-research-book/06-unused-module-elimination-strategy.md +77 -0
- package/docs/books/jsgui3-bundling-research-book/07-jsgui3-html-control-and-mixin-pruning.md +63 -0
- package/docs/books/jsgui3-bundling-research-book/08-test-and-verification-methodology.md +43 -0
- package/docs/books/jsgui3-bundling-research-book/09-roadmap-and-rollout.md +42 -0
- package/docs/books/jsgui3-bundling-research-book/10-further-research-strategies-and-upgrades.md +211 -0
- package/docs/books/jsgui3-bundling-research-book/README.md +35 -0
- package/docs/bundling-system-deep-dive.md +9 -4
- package/docs/comparison-report-express-plex-cpanel.md +549 -0
- package/docs/comprehensive-documentation.md +49 -18
- package/docs/configuration-reference.md +152 -27
- package/docs/core/README.md +19 -0
- package/docs/core/jsgui3-server-core-book/00-table-of-contents.md +21 -0
- package/docs/core/jsgui3-server-core-book/01-startup-readiness-state-machine.md +41 -0
- package/docs/core/jsgui3-server-core-book/02-resource-abstraction-and-lifecycle.md +92 -0
- package/docs/core/jsgui3-server-core-book/03-resource-pool-and-event-topology.md +47 -0
- package/docs/core/jsgui3-server-core-book/04-sse-publisher-semantics.md +41 -0
- package/docs/core/jsgui3-server-core-book/05-serve-factory-resource-wiring.md +46 -0
- package/docs/core/jsgui3-server-core-book/06-e2e-testing-methodology.md +48 -0
- package/docs/core/jsgui3-server-core-book/07-defect-detection-and-hardening-loop.md +47 -0
- package/docs/designs/server-admin-interface-aero.svg +611 -0
- package/docs/publishers-guide.md +59 -4
- package/docs/resources-guide.md +184 -35
- package/docs/simple-server-api-design.md +72 -17
- package/docs/system-architecture.md +18 -14
- package/docs/troubleshooting.md +84 -53
- package/examples/controls/15) window, observable SSE/server.js +6 -1
- package/examples/controls/19) window, auto observable ui/server.js +9 -0
- package/examples/controls/20) window, task manager app/README.md +133 -0
- package/examples/controls/20) window, task manager app/client.js +797 -0
- package/examples/controls/20) window, task manager app/server.js +178 -0
- package/examples/controls/6) window, color_palette/client.js +165 -68
- package/examples/controls/9) window, date picker/client.js +362 -76
- package/examples/controls/9b) window, shared data.model mirrored date pickers/client.js +104 -83
- package/examples/jsgui3-html/06) theming/client.js +22 -1
- package/examples/jsgui3-html/10) binding-debugger/client.js +137 -1
- package/http/responders/static/Static_Route_HTTP_Responder.js +52 -34
- package/lab/experiments/capture-color-controls.js +196 -0
- package/lab/results/screenshots/color-controls/full_page.png +0 -0
- package/lab/results/screenshots/color-controls/section_1_color_grid_12x12.png +0 -0
- package/lab/results/screenshots/color-controls/section_2_color_grid_4x2.png +0 -0
- package/lab/results/screenshots/color-controls/section_3_color_palette.png +0 -0
- package/lab/results/screenshots/color-controls/section_4_palette_comparison.png +0 -0
- package/lab/results/screenshots/color-controls/section_5_raw_swatches.png +0 -0
- package/lab/results/screenshots/color-controls/section_6_optimized_crayola.png +0 -0
- package/lab/results/screenshots/color-controls/section_7_pastel_palette.png +0 -0
- package/lab/results/screenshots/color-controls/section_8_extended_144.png +0 -0
- package/lab/screenshot-utils.js +248 -0
- package/module.js +12 -0
- package/package.json +12 -2
- package/publishers/Publishers.js +4 -3
- package/publishers/helpers/assigners/static-compressed-response-buffers/Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner.js +5 -5
- package/publishers/http-sse-publisher.js +341 -0
- package/resources/process-resource.js +950 -0
- package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +129 -33
- package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +18 -7
- package/resources/processors/bundlers/js/esbuild/JSGUI3_HTML_Control_Optimizer.js +829 -0
- package/resources/remote-process-resource.js +355 -0
- package/resources/server-resource-pool.js +354 -41
- package/serve-factory.js +442 -259
- package/server.js +288 -13
- package/tests/README.md +71 -4
- package/tests/admin-ui-jsgui-controls.test.js +581 -0
- package/tests/admin-ui-render.test.js +24 -0
- package/tests/assigners.test.js +56 -40
- package/tests/bundling-default-control-elimination.puppeteer.test.js +260 -0
- package/tests/configuration-validation.test.js +21 -18
- package/tests/content-analysis.test.js +7 -6
- package/tests/control-optimizer-cache-behavior.test.js +52 -0
- package/tests/control-scan-manifest-regression.test.js +144 -0
- package/tests/end-to-end.test.js +15 -14
- package/tests/error-handling.test.js +222 -179
- package/tests/fixtures/bundling-default-button-client.js +37 -0
- package/tests/fixtures/bundling-default-window-client.js +34 -0
- package/tests/fixtures/control_scan_manifest_expectations.json +48 -0
- package/tests/fixtures/resource-monitor-client.js +319 -0
- package/tests/helpers/puppeteer-e2e-harness.js +317 -0
- package/tests/http-sse-publisher.test.js +136 -0
- package/tests/performance.test.js +69 -65
- package/tests/process-resource.test.js +138 -0
- package/tests/publishers.test.js +7 -7
- package/tests/remote-process-resource.test.js +160 -0
- package/tests/sass-controls.e2e.test.js +7 -1
- package/tests/serve-resources.test.js +270 -0
- package/tests/serve.test.js +120 -50
- package/tests/server-resource-pool.test.js +106 -0
- package/tests/small-controls-bundle-size.test.js +252 -0
- package/tests/test-runner.js +14 -1
- package/tests/window-examples.puppeteer.test.js +204 -1
- package/tests/window-resource-integration.puppeteer.test.js +585 -0
- package/tests/temp_invalid.js +0 -7
- package/tests/temp_invalid_utf8.js +0 -1
- package/tests/temp_malformed.js +0 -10
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Chapter 2 — Responsive Composition Model
|
|
2
|
+
|
|
3
|
+
## The Problem
|
|
4
|
+
|
|
5
|
+
Consider a typical admin dashboard built on jsgui3. On desktop, it shows a navigation sidebar, a main content area, and a property inspector panel — three columns. On tablet portrait, the property inspector should collapse to a slide-over panel. On phone, the sidebar should become a hamburger-triggered drawer and the inspector should become a full-screen modal.
|
|
6
|
+
|
|
7
|
+
The naive approach is to build one big layout and write CSS media queries to hide/show regions. But this creates problems:
|
|
8
|
+
|
|
9
|
+
1. **All three layouts are in the DOM simultaneously**, even when invisible — wasting memory and causing accessibility confusion (screen readers find hidden elements).
|
|
10
|
+
2. **Business logic couples to layout** — event handlers reference elements that may be hidden, requiring defensive checks everywhere.
|
|
11
|
+
3. **Controls can't adapt their own internal structure** — a data table might want to show 2 columns on phone and 8 on desktop, but CSS can only hide columns, not restructure the control.
|
|
12
|
+
|
|
13
|
+
jsgui3's compositional model offers a better path: compose different control trees for different environments, sharing the same domain data.
|
|
14
|
+
|
|
15
|
+
## Design Objective
|
|
16
|
+
|
|
17
|
+
**High-level app code should declare intent, not breakpoint math.**
|
|
18
|
+
|
|
19
|
+
An app developer should write something like *"use a three-column shell on desktop, two columns on tablet, single column with drawers on phone"* and have the platform resolve the details. The developer should not be manually checking `window.innerWidth` in fifteen places.
|
|
20
|
+
|
|
21
|
+
## The Four-Layer Composition Model
|
|
22
|
+
|
|
23
|
+
Adaptive composition in jsgui3 separates concerns into four layers. Each layer has a clear responsibility and communicates with adjacent layers through well-defined contracts.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
┌─────────────────────────────────────────────┐
|
|
27
|
+
│ Layer A: Domain Composition │
|
|
28
|
+
│ (entities, actions, permissions, workflow) │
|
|
29
|
+
│ ─── completely device-agnostic ─── │
|
|
30
|
+
├─────────────────────────────────────────────┤
|
|
31
|
+
│ Layer B: View Composition │
|
|
32
|
+
│ (regions, component hierarchy, adaptive │
|
|
33
|
+
│ intent declarations) │
|
|
34
|
+
├─────────────────────────────────────────────┤
|
|
35
|
+
│ Layer C: Adaptive Resolution │
|
|
36
|
+
│ (environment service resolves intent → │
|
|
37
|
+
│ concrete mode, density, interaction) │
|
|
38
|
+
├─────────────────────────────────────────────┤
|
|
39
|
+
│ Layer D: Concrete Render │
|
|
40
|
+
│ (resolved CSS classes, DOM attributes, │
|
|
41
|
+
│ control params, token values) │
|
|
42
|
+
└─────────────────────────────────────────────┘
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Layer A: Domain Composition
|
|
46
|
+
|
|
47
|
+
This is the business logic layer. It defines what data exists, what actions are available, and what workflows the user follows. It should have **zero awareness of screen size or device type**.
|
|
48
|
+
|
|
49
|
+
Examples of Layer A concerns:
|
|
50
|
+
- "The user has a list of projects"
|
|
51
|
+
- "Each project has a name, status, and due date"
|
|
52
|
+
- "The user can archive a project"
|
|
53
|
+
|
|
54
|
+
In jsgui3, Layer A lives in `data.model` — the Data_Object instances managed by `ensure_control_models()`. Whether the app runs on a phone or a 4K monitor, the domain model is identical.
|
|
55
|
+
|
|
56
|
+
### Layer B: View Composition
|
|
57
|
+
|
|
58
|
+
This layer defines the UI structure: what regions exist, what controls they contain, and how they relate to each other. Critically, Layer B can express **adaptive intent** without committing to specific breakpoints.
|
|
59
|
+
|
|
60
|
+
Examples of Layer B declarations:
|
|
61
|
+
- "The app has a navigation region, a content region, and a tools region"
|
|
62
|
+
- "The navigation region can collapse to a drawer on narrow screens"
|
|
63
|
+
- "The tools region is optional and can be toggled"
|
|
64
|
+
|
|
65
|
+
In jsgui3, Layer B lives in the constructor's composition logic — the `compose_ui()` method that builds the control tree. The key design choice is that Layer B expresses *what should adapt* without specifying *when*.
|
|
66
|
+
|
|
67
|
+
### Layer C: Adaptive Resolution
|
|
68
|
+
|
|
69
|
+
This is the platform layer that answers the question: "given the current environment, how should composition intent be resolved?" It observes:
|
|
70
|
+
|
|
71
|
+
- Viewport width and height
|
|
72
|
+
- Orientation (portrait/landscape/square)
|
|
73
|
+
- Input modality (touch/pointer/hybrid)
|
|
74
|
+
- User preferences (reduced motion, contrast, density preference)
|
|
75
|
+
|
|
76
|
+
And produces a normalized environment contract:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
context.view_environment = {
|
|
80
|
+
viewport: { width: 768, height: 1024, orientation: 'portrait' },
|
|
81
|
+
layout_mode: 'tablet', // 'phone' | 'tablet' | 'desktop'
|
|
82
|
+
density_mode: 'cozy', // 'compact' | 'cozy' | 'comfortable'
|
|
83
|
+
interaction_mode: 'touch', // 'touch' | 'pointer' | 'hybrid'
|
|
84
|
+
motion_mode: 'normal' // 'normal' | 'reduced'
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This contract is observable — controls and view models can listen for changes and recompose when the environment shifts (for instance, when a browser window is resized across a breakpoint boundary, or when a tablet is rotated).
|
|
89
|
+
|
|
90
|
+
### Layer D: Concrete Render
|
|
91
|
+
|
|
92
|
+
The resolved environment drives concrete rendering decisions: CSS classes on the root element (`data-layout-mode="tablet"`), specific token overrides for the density mode, and resolved control parameters.
|
|
93
|
+
|
|
94
|
+
At this layer, everything is deterministic. Given environment state X, the output is always CSS class set Y and token values Z. This makes testing straightforward — you can assert on Layer D output without needing a real browser.
|
|
95
|
+
|
|
96
|
+
## How the Layers Interact
|
|
97
|
+
|
|
98
|
+
Here's the flow for a realistic scenario — a dashboard app loading on a tablet in portrait:
|
|
99
|
+
|
|
100
|
+
1. **Layer A** is set up: the data model contains projects, user preferences, etc.
|
|
101
|
+
2. **Layer B** composes the shell: nav region, content region, tools region. The nav region is marked as "collapsible."
|
|
102
|
+
3. **Layer C** reads the viewport (768×1024), determines `layout_mode: 'tablet'`, `interaction_mode: 'touch'`.
|
|
103
|
+
4. **Layer D** applies: `data-layout-mode="tablet"` on root, nav region renders as a slide-over panel (not inline sidebar), touch density tokens are applied.
|
|
104
|
+
5. The user rotates to landscape (1024×768).
|
|
105
|
+
6. **Layer C** detects the change, updates to `layout_mode: 'desktop'` (1024px exceeds the tablet threshold).
|
|
106
|
+
7. **Layer B** recomposes: nav region switches from slide-over to inline sidebar.
|
|
107
|
+
8. **Layer D** updates: root attribute changes, CSS transitions the layout.
|
|
108
|
+
|
|
109
|
+
## SSR Considerations
|
|
110
|
+
|
|
111
|
+
On the server, there is no viewport. Layer C needs a default. Two strategies:
|
|
112
|
+
|
|
113
|
+
1. **Desktop-first SSR**: Compose for desktop, let the client refine on activation. This is the simplest approach and avoids layout shift on most users (majority of admin/dashboard users are desktop).
|
|
114
|
+
|
|
115
|
+
2. **Hint-based SSR**: Pass environment hints through the request (User-Agent parsing, explicit query param, or stored preference). The server composes for the hinted mode. The client validates and corrects if needed.
|
|
116
|
+
|
|
117
|
+
For most jsgui3 applications, desktop-first SSR is the right default. Mobile refinement happens in `activate()` and is fast because jsgui3's activation is already designed for incremental DOM updates.
|
|
118
|
+
|
|
119
|
+
## Why CSS-Only Approaches Fall Short
|
|
120
|
+
|
|
121
|
+
Pure CSS media queries can handle styling changes (font sizes, padding, hiding elements), but they cannot:
|
|
122
|
+
|
|
123
|
+
- **Restructure the control tree** — replace a sidebar with a tabbed panel
|
|
124
|
+
- **Change control parameters** — switch a table from 8 columns to 2
|
|
125
|
+
- **Coordinate across components** — ensure nav, content, and tools all agree on the same mode
|
|
126
|
+
- **Drive model updates** — notify the view model that the layout changed so other logic can respond
|
|
127
|
+
|
|
128
|
+
CSS is excellent for Layer D concerns (visual polish, transitions, token overrides). But Layers B and C require JavaScript-level composition decisions, which is exactly what jsgui3's constructor + activate pattern enables.
|
|
129
|
+
|
|
130
|
+
This doesn't mean we should ignore CSS entirely. The ideal approach is:
|
|
131
|
+
|
|
132
|
+
- **CSS handles continuous adaptation** — fluid typography, flexible gaps, wrapping
|
|
133
|
+
- **JS handles discrete adaptation** — structural changes at mode boundaries
|
|
134
|
+
|
|
135
|
+
## Example: Before and After
|
|
136
|
+
|
|
137
|
+
### Before (ad-hoc responsive code):
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
compose_ui(context) {
|
|
141
|
+
// Developer manually checks width, duplicates logic
|
|
142
|
+
if (typeof window !== 'undefined' && window.innerWidth < 768) {
|
|
143
|
+
this.nav = new Drawer({ context, position: 'left' });
|
|
144
|
+
this.content = new Stack({ context, direction: 'column' });
|
|
145
|
+
// phone layout...
|
|
146
|
+
} else if (typeof window !== 'undefined' && window.innerWidth < 1024) {
|
|
147
|
+
this.nav = new Stack({ context, direction: 'column' });
|
|
148
|
+
this.content = new Stack({ context, direction: 'column' });
|
|
149
|
+
// tablet layout...
|
|
150
|
+
} else {
|
|
151
|
+
this.nav = new Stack({ context, direction: 'column' });
|
|
152
|
+
this.tools = new Stack({ context, direction: 'column' });
|
|
153
|
+
this.content = new Stack({ context, direction: 'column' });
|
|
154
|
+
// desktop layout...
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Problems: breakpoint values are hardcoded, no coordination with other controls, no observable state, no SSR support, no recomposition on resize.
|
|
160
|
+
|
|
161
|
+
### After (with adaptive composition):
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
compose_ui(context) {
|
|
165
|
+
const env = context.view_environment;
|
|
166
|
+
|
|
167
|
+
compose_adaptive(this, env, {
|
|
168
|
+
phone: () => this.compose_phone_shell(),
|
|
169
|
+
tablet: () => this.compose_tablet_shell(),
|
|
170
|
+
desktop: () => this.compose_desktop_shell()
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The `compose_adaptive` helper reads `env.layout_mode`, calls the right branch, and registers a listener so the shell recomposes if the mode changes. The breakpoint thresholds are defined once in the environment service, not scattered across app code.
|
|
176
|
+
|
|
177
|
+
Each `compose_*_shell()` method shares the same domain model (Layer A) but builds a different control tree (Layer B). The developer writes three short, focused composition functions instead of one tangled conditional.
|
|
178
|
+
|
|
179
|
+
## Key Design Principles
|
|
180
|
+
|
|
181
|
+
1. **Domain state never knows about devices.** If you're putting `layout_mode` in your data model, something is wrong.
|
|
182
|
+
2. **Composition intent is separate from resolution.** A region declared as "collapsible" doesn't decide when to collapse — the environment service does.
|
|
183
|
+
3. **Continuous CSS, discrete JS.** Fluid sizing stays in CSS. Structural reorganization happens in constructors.
|
|
184
|
+
4. **Observable, not polled.** The environment service emits change events. Controls don't poll `window.innerWidth`.
|
|
185
|
+
5. **SSR-safe defaults.** Everything works without a viewport. Client activation refines.
|
|
186
|
+
|
|
187
|
+
**Next:** [Chapter 3](03-data-model-vs-view-model.md) explains how the MVVM model separation keeps adaptive state clean and testable.
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Chapter 3 — Data Model vs View Model in Adaptive UI
|
|
2
|
+
|
|
3
|
+
## Why This Matters
|
|
4
|
+
|
|
5
|
+
The most common mistake in responsive UI architecture is mixing device-specific state into business data. When a developer stores `sidebar_width: 280` next to `project_name: "Alpha"`, two bad things happen:
|
|
6
|
+
|
|
7
|
+
1. **Serialization pollution.** Save the model to a database and you're persisting pixel values that are meaningless on a different device. Restore it on a phone and a 280px sidebar obscures everything.
|
|
8
|
+
|
|
9
|
+
2. **Fragile coupling.** Business logic starts depending on layout state. A function that calculates project metrics now has to guard against `undefined` view properties. Tests for business rules require mocking viewport dimensions.
|
|
10
|
+
|
|
11
|
+
jsgui3's MVVM architecture already provides the right separation. This chapter explains how to use it correctly for adaptive UI, ensuring device-specific decisions never contaminate domain data.
|
|
12
|
+
|
|
13
|
+
## The Three Model Layers in jsgui3
|
|
14
|
+
|
|
15
|
+
The `ensure_control_models()` factory creates a stack of model objects on every control:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
┌──────────────────────────────────┐
|
|
19
|
+
│ this.data.model │ ← Domain state
|
|
20
|
+
│ (Data_Object from lang-tools) │ "What does the business care about?"
|
|
21
|
+
├──────────────────────────────────┤
|
|
22
|
+
│ this.view.data.model │ ← Presentation state
|
|
23
|
+
│ (Data_Object) │ "What does the view need to render?"
|
|
24
|
+
├──────────────────────────────────┤
|
|
25
|
+
│ this.view.model │ ← View-instance state
|
|
26
|
+
│ (Data_Object) │ "What is the current UI interaction state?"
|
|
27
|
+
├──────────────────────────────────┤
|
|
28
|
+
│ this.view.ui │ ← UI structure metadata
|
|
29
|
+
│ (Control_View_UI) │ "What controls exist in this view?"
|
|
30
|
+
└──────────────────────────────────┘
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Each layer serves a different purpose. Getting data into the right layer is the key to clean adaptive architecture.
|
|
34
|
+
|
|
35
|
+
## What Goes Where
|
|
36
|
+
|
|
37
|
+
### `data.model` — Domain State
|
|
38
|
+
|
|
39
|
+
This is the source of truth for business data. It should be serializable, restorable, and completely ignorant of how it's displayed.
|
|
40
|
+
|
|
41
|
+
**Belongs here:**
|
|
42
|
+
- `selected_project_id: 'proj-42'`
|
|
43
|
+
- `user_name: 'Alice'`
|
|
44
|
+
- `filter_active: true`
|
|
45
|
+
- `sort_field: 'due_date'`
|
|
46
|
+
- `saved_theme_preference: 'vs-dark'`
|
|
47
|
+
|
|
48
|
+
**Does NOT belong here:**
|
|
49
|
+
- `sidebar_open: true` (that's a view concern)
|
|
50
|
+
- `viewport_width: 768` (ephemeral device state)
|
|
51
|
+
- `panel_collapsed: false` (UI interaction state)
|
|
52
|
+
- `animation_in_progress: true` (transient rendering state)
|
|
53
|
+
|
|
54
|
+
The litmus test: *"Would this value make sense if I serialized it to a database and loaded it on a completely different device?"* If yes, it's domain state. If no, it belongs in a view model.
|
|
55
|
+
|
|
56
|
+
### `view.data.model` — Presentation State
|
|
57
|
+
|
|
58
|
+
This is derived data that the view needs to render but that doesn't belong in the domain model. It's often computed from the combination of domain state and environment state.
|
|
59
|
+
|
|
60
|
+
**Belongs here:**
|
|
61
|
+
- `layout_mode: 'tablet'` (resolved from viewport)
|
|
62
|
+
- `should_show_sidebar_inline: true` (derived from layout_mode)
|
|
63
|
+
- `visible_column_count: 4` (derived from available width)
|
|
64
|
+
- `formatted_due_date: 'Feb 14, 2026'` (derived from data.model.due_date)
|
|
65
|
+
- `density_mode: 'compact'` (resolved from interaction mode + user preference)
|
|
66
|
+
|
|
67
|
+
This is where adaptive decisions materialize as concrete values that controls can bind to.
|
|
68
|
+
|
|
69
|
+
### `view.model` — View-Instance State
|
|
70
|
+
|
|
71
|
+
This captures the user's current interaction state within the UI. It's ephemeral — losing it on page reload is acceptable (or it can be persisted to localStorage if desired).
|
|
72
|
+
|
|
73
|
+
**Belongs here:**
|
|
74
|
+
- `sidebar_open: true`
|
|
75
|
+
- `active_tab_index: 2`
|
|
76
|
+
- `panel_collapsed: false`
|
|
77
|
+
- `scroll_position: 340`
|
|
78
|
+
- `hover_row_id: 'row-7'`
|
|
79
|
+
|
|
80
|
+
### `view.ui` — UI Structure Metadata
|
|
81
|
+
|
|
82
|
+
This is structural metadata about what controls exist in the current view. It's managed by the control framework, not typically by app developers.
|
|
83
|
+
|
|
84
|
+
## A Worked Example
|
|
85
|
+
|
|
86
|
+
Consider a project dashboard that shows a list of projects and a detail panel. Let's trace a scenario where the user is on a tablet and rotates from portrait to landscape.
|
|
87
|
+
|
|
88
|
+
### The Models
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
// Domain model — same regardless of device
|
|
92
|
+
this.data.model = new Data_Object({
|
|
93
|
+
projects: [
|
|
94
|
+
{ id: 'p1', name: 'Alpha', status: 'active', due: '2026-03-01' },
|
|
95
|
+
{ id: 'p2', name: 'Beta', status: 'draft', due: '2026-04-15' }
|
|
96
|
+
],
|
|
97
|
+
selected_project_id: 'p1',
|
|
98
|
+
filter_status: 'all'
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// View data model — derived from domain + environment
|
|
102
|
+
this.view.data.model = new Data_Object({
|
|
103
|
+
layout_mode: 'tablet',
|
|
104
|
+
visible_columns: ['name', 'status', 'due'], // desktop would show more
|
|
105
|
+
should_show_detail_inline: false, // tablet portrait: overlay
|
|
106
|
+
formatted_projects: [/* computed from domain */]
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// View model — current interaction state
|
|
110
|
+
this.view.model = new Data_Object({
|
|
111
|
+
detail_panel_open: true,
|
|
112
|
+
list_scroll_position: 0,
|
|
113
|
+
active_section: 'overview'
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Portrait (768×1024): `layout_mode: 'tablet'`
|
|
118
|
+
|
|
119
|
+
The environment service resolves `layout_mode: 'tablet'`. View composition responds:
|
|
120
|
+
- `should_show_detail_inline: false` → detail panel is a slide-over
|
|
121
|
+
- `visible_columns: ['name', 'status', 'due']` → 3 columns shown
|
|
122
|
+
- User rotates the device...
|
|
123
|
+
|
|
124
|
+
### Landscape (1024×768): `layout_mode: 'desktop'`
|
|
125
|
+
|
|
126
|
+
The environment service detects the change. `layout_mode` updates to `'desktop'`. The view data model recomputes:
|
|
127
|
+
- `should_show_detail_inline: true` → detail panel is now a side panel
|
|
128
|
+
- `visible_columns: ['name', 'status', 'due', 'owner', 'priority']` → 5 columns
|
|
129
|
+
|
|
130
|
+
**Crucially, nothing in `data.model` changed.** The selected project is still `'p1'`. The filter is still `'all'`. The domain is untouched. Only view-layer state responded to the environment change.
|
|
131
|
+
|
|
132
|
+
## Using Bindings for Adaptive Derived State
|
|
133
|
+
|
|
134
|
+
The `Data_Model_View_Model_Control` base class provides binding and computed property APIs that are ideal for deriving adaptive state:
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
// In the control's constructor:
|
|
138
|
+
ensure_control_models(this, spec);
|
|
139
|
+
|
|
140
|
+
// Bind layout_mode changes to column visibility
|
|
141
|
+
this.watch(this.view.data.model, 'layout_mode', (mode) => {
|
|
142
|
+
const columns = mode === 'phone'
|
|
143
|
+
? ['name']
|
|
144
|
+
: mode === 'tablet'
|
|
145
|
+
? ['name', 'status', 'due']
|
|
146
|
+
: ['name', 'status', 'due', 'owner', 'priority', 'created'];
|
|
147
|
+
|
|
148
|
+
this.view.data.model.set('visible_columns', columns);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Computed property: should detail show inline?
|
|
152
|
+
this.computed(this.view.data.model,
|
|
153
|
+
['layout_mode'],
|
|
154
|
+
(mode) => mode === 'desktop',
|
|
155
|
+
{ property_name: 'should_show_detail_inline' }
|
|
156
|
+
);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
When the environment service updates `layout_mode`, the watchers fire, derived state recomputes, and controls that bind to `visible_columns` or `should_show_detail_inline` update automatically. The reactive pipeline in lang-tools handles the propagation.
|
|
160
|
+
|
|
161
|
+
## The Persistence Rule
|
|
162
|
+
|
|
163
|
+
A simple rule for adaptive state and persistence:
|
|
164
|
+
|
|
165
|
+
| Model Layer | Persist to DB? | Persist to localStorage? | Transfer across devices? |
|
|
166
|
+
|-------------|----------------|--------------------------|--------------------------|
|
|
167
|
+
| `data.model` | Yes | If needed | Yes — it's device-agnostic |
|
|
168
|
+
| `view.data.model` | No | Rarely | No — it's derived from environment |
|
|
169
|
+
| `view.model` | No | Optionally | No — it's per-session |
|
|
170
|
+
|
|
171
|
+
When the showcase app persists theme preferences to localStorage, those preferences are domain-level choices (the user chose "vs-dark"). But the `layout_mode` that determines how the theme studio is displayed is view state — it shouldn't be persisted because it should always reflect the current device.
|
|
172
|
+
|
|
173
|
+
## Integration with Low-Level Complexity
|
|
174
|
+
|
|
175
|
+
The goal of this separation is to keep high-level app code simple. But the reactive binding infrastructure underneath is sophisticated — and that's fine. The complexity lives in the right place:
|
|
176
|
+
|
|
177
|
+
- **lang-tools** provides `Data_Object` with change events, property watching, and computed properties
|
|
178
|
+
- **ModelBinder** and **BindingManager** handle bi-directional syncing between model layers
|
|
179
|
+
- **control_model_factory** auto-wires the model stack
|
|
180
|
+
|
|
181
|
+
App developers don't need to understand how `BindingManager` resolves circular dependencies or how `Data_Object` batches change events. They need to know: *"put business data in data.model, put device-specific state in view.data.model, and use watchers to derive one from the other."*
|
|
182
|
+
|
|
183
|
+
## Anti-Patterns to Avoid
|
|
184
|
+
|
|
185
|
+
### 1. Domain model holding layout state
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
// ❌ Bad: domain model knows about layout
|
|
189
|
+
this.data.model.set('sidebar_visible', window.innerWidth > 960);
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
// ✅ Good: view model holds layout state
|
|
194
|
+
this.view.data.model.set('sidebar_visible', env.layout_mode !== 'phone');
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 2. Controls reading window dimensions directly
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
// ❌ Bad: every control checks the viewport independently
|
|
201
|
+
if (window.innerWidth < 768) { /* phone layout */ }
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
// ✅ Good: controls read from the shared environment contract
|
|
206
|
+
const mode = context.view_environment.layout_mode;
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 3. Persisting derived state
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
// ❌ Bad: saving viewport-dependent state to the database
|
|
213
|
+
save_to_server({ columns: this.view.data.model.get('visible_columns') });
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```js
|
|
217
|
+
// ✅ Good: saving only the user's preference, not the derived result
|
|
218
|
+
save_to_server({ preferred_column_set: this.data.model.get('column_preference') });
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Summary
|
|
222
|
+
|
|
223
|
+
The model separation already built into jsgui3 through `Data_Model_View_Model_Control` is the right foundation for adaptive UI. The key insight is that **adaptive state is view state, not domain state**. The view data model (`view.data.model`) is where layout mode, density, column visibility, and region configuration live. The domain model (`data.model`) remains pure, portable, and testable.
|
|
224
|
+
|
|
225
|
+
This separation means you can:
|
|
226
|
+
- Test business logic without mocking viewports
|
|
227
|
+
- Switch devices without corrupting saved state
|
|
228
|
+
- Add new adaptive behaviors without touching domain code
|
|
229
|
+
- Serialize and restore cleanly across sessions and devices
|
|
230
|
+
|
|
231
|
+
**Next:** [Chapter 4](04-styling-theme-breakpoints.md) covers how the token and theme system supports responsive density and mode-based styling.
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Chapter 4 — Styling, Themes, and Breakpoint Strategy
|
|
2
|
+
|
|
3
|
+
## The Two Kinds of Responsiveness
|
|
4
|
+
|
|
5
|
+
Responsive UI involves two fundamentally different kinds of adaptation:
|
|
6
|
+
|
|
7
|
+
1. **Continuous adaptation** — sizes, spacing, and proportions that scale fluidly. A font might be 14px on desktop and 16px on mobile. Padding might shrink from 24px to 12px. These are gradients, not switches.
|
|
8
|
+
|
|
9
|
+
2. **Discrete adaptation** — structural changes at specific boundaries. A sidebar becomes a drawer. A table becomes a card list. Three columns become one. These are switches, not gradients.
|
|
10
|
+
|
|
11
|
+
CSS handles continuous adaptation well (fluid units, `clamp()`, flexible layouts). Discrete adaptation typically requires JavaScript composition decisions, as discussed in Chapter 2. But there's an important middle ground: **mode-qualified CSS** that uses data attributes set by the environment service.
|
|
12
|
+
|
|
13
|
+
This chapter covers how jsgui3's token and theme system supports both kinds of adaptation.
|
|
14
|
+
|
|
15
|
+
## Current Token Architecture
|
|
16
|
+
|
|
17
|
+
The token system in `css/jsgui-tokens.css` provides two tiers:
|
|
18
|
+
|
|
19
|
+
### Design tokens (`--j-*`)
|
|
20
|
+
|
|
21
|
+
Foundation-level tokens for spacing, typography, radii, shadows, motion, and color:
|
|
22
|
+
|
|
23
|
+
```css
|
|
24
|
+
/* Spacing scale (8px base) */
|
|
25
|
+
--j-space-1: 4px; --j-space-2: 8px; --j-space-3: 12px;
|
|
26
|
+
--j-space-4: 16px; --j-space-5: 24px; --j-space-6: 32px;
|
|
27
|
+
|
|
28
|
+
/* Typography scale */
|
|
29
|
+
--j-text-xs: 0.75rem; --j-text-sm: 0.875rem; --j-text-base: 1rem;
|
|
30
|
+
--j-text-lg: 1.125rem; --j-text-xl: 1.5rem;
|
|
31
|
+
|
|
32
|
+
/* Motion (zeroed under prefers-reduced-motion) */
|
|
33
|
+
--j-duration-fast: 120ms; --j-duration-normal: 200ms; --j-duration-slow: 350ms;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Component tokens (`--admin-*`)
|
|
37
|
+
|
|
38
|
+
Higher-level tokens consumed by admin/data controls:
|
|
39
|
+
|
|
40
|
+
```css
|
|
41
|
+
--admin-font-size: 13px;
|
|
42
|
+
--admin-row-height: 36px;
|
|
43
|
+
--admin-cell-padding: 8px 12px;
|
|
44
|
+
--admin-radius: 4px;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Both tiers include full dark-mode overrides via the `[data-theme="dark"]` selector.
|
|
48
|
+
|
|
49
|
+
## Strategy 1: Mode Classes Over Scattered Breakpoints
|
|
50
|
+
|
|
51
|
+
The traditional approach scatters `@media` queries across component stylesheets:
|
|
52
|
+
|
|
53
|
+
```css
|
|
54
|
+
/* ❌ Scattered: breakpoint values duplicated everywhere */
|
|
55
|
+
@media (max-width: 768px) {
|
|
56
|
+
.project-list { flex-direction: column; }
|
|
57
|
+
}
|
|
58
|
+
@media (max-width: 768px) {
|
|
59
|
+
.tool-panel { display: none; }
|
|
60
|
+
}
|
|
61
|
+
@media (max-width: 768px) {
|
|
62
|
+
.nav-bar { font-size: 14px; }
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Problems: the value `768px` is repeated in dozens of files; there's no single source of truth; changing the breakpoint requires a global search-and-replace.
|
|
67
|
+
|
|
68
|
+
The recommended approach uses **mode attributes** set by the environment service (Layer C):
|
|
69
|
+
|
|
70
|
+
```html
|
|
71
|
+
<body data-layout-mode="phone" data-density-mode="compact" data-interaction-mode="touch">
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Component CSS then targets the mode, not the breakpoint:
|
|
75
|
+
|
|
76
|
+
```css
|
|
77
|
+
/* ✅ Mode-qualified: breakpoint logic lives in one place (the environment service) */
|
|
78
|
+
[data-layout-mode="phone"] .project-list {
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
}
|
|
81
|
+
[data-layout-mode="phone"] .tool-panel {
|
|
82
|
+
display: none;
|
|
83
|
+
}
|
|
84
|
+
[data-layout-mode="phone"] .nav-bar {
|
|
85
|
+
font-size: var(--j-text-sm);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Benefits:
|
|
90
|
+
- **Single source of truth** for breakpoint thresholds (the environment service)
|
|
91
|
+
- **Testable** — set `data-layout-mode="phone"` on any element in a test and verify styles
|
|
92
|
+
- **Composable** — combine mode, density, and interaction qualifiers: `[data-layout-mode="phone"][data-density-mode="compact"]`
|
|
93
|
+
- **SSR-friendly** — the server can set mode attributes based on request hints
|
|
94
|
+
|
|
95
|
+
This is the same pattern that dark mode already uses — `[data-theme="dark"]` overrides tokens. We're extending it to layout mode and density.
|
|
96
|
+
|
|
97
|
+
## Strategy 2: Responsive Token Tiers
|
|
98
|
+
|
|
99
|
+
Some styling changes are too pervasive to write as individual CSS rules. Font size, row height, padding, and spacing should adapt globally. The right approach is to override tokens at the root based on the active mode.
|
|
100
|
+
|
|
101
|
+
### Proposed density token overrides
|
|
102
|
+
|
|
103
|
+
```css
|
|
104
|
+
/* Compact — information-dense, smaller touch targets, less whitespace */
|
|
105
|
+
[data-density-mode="compact"] {
|
|
106
|
+
--j-space-2: 4px;
|
|
107
|
+
--j-space-3: 8px;
|
|
108
|
+
--j-space-4: 12px;
|
|
109
|
+
--j-space-5: 16px;
|
|
110
|
+
--admin-font-size: 12px;
|
|
111
|
+
--admin-row-height: 28px;
|
|
112
|
+
--admin-cell-padding: 4px 8px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Comfortable — generous whitespace, larger touch targets */
|
|
116
|
+
[data-density-mode="comfortable"] {
|
|
117
|
+
--j-space-2: 12px;
|
|
118
|
+
--j-space-3: 16px;
|
|
119
|
+
--j-space-4: 20px;
|
|
120
|
+
--j-space-5: 32px;
|
|
121
|
+
--admin-font-size: 15px;
|
|
122
|
+
--admin-row-height: 44px;
|
|
123
|
+
--admin-cell-padding: 12px 16px;
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
No component CSS needs to change. Every control that uses `--j-space-*` or `--admin-row-height` automatically adapts. The token system does the work.
|
|
128
|
+
|
|
129
|
+
### Why density and theme are orthogonal
|
|
130
|
+
|
|
131
|
+
Theme (color identity: light, dark, vs-dark) and density (space economy: compact, cozy, comfortable) are independent axes. A user might want:
|
|
132
|
+
|
|
133
|
+
- `vs-dark` theme + `compact` density (power user, dark IDE-like)
|
|
134
|
+
- Light theme + `comfortable` density (presentation mode, accessibility)
|
|
135
|
+
- `vs-dark` theme + `comfortable` density (dark mode on a touch tablet)
|
|
136
|
+
|
|
137
|
+
Implementing them as separate data attributes ensures they compose freely:
|
|
138
|
+
|
|
139
|
+
```html
|
|
140
|
+
<body data-theme="vs-dark" data-density-mode="compact">
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Strategy 3: Theme Profiles
|
|
144
|
+
|
|
145
|
+
The showcase app already persists theme state to localStorage. Extending this to include density and layout preferences creates a clean **theme profile** object:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
const profile = {
|
|
149
|
+
theme: 'vs-dark',
|
|
150
|
+
density: 'compact',
|
|
151
|
+
overrides: {
|
|
152
|
+
'--admin-font-size': '12px',
|
|
153
|
+
'--admin-radius': '2px'
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
This profile object is:
|
|
159
|
+
- **Serializable** — JSON-safe for localStorage, export/import, server storage
|
|
160
|
+
- **Transferable** — share profiles between users or across projects
|
|
161
|
+
- **Testable** — apply a profile in a test fixture and assert visual outcomes
|
|
162
|
+
- **Composable** — merge a base profile with user overrides
|
|
163
|
+
|
|
164
|
+
The existing showcase app persistence (`showcase_theme_state_v1` localStorage key) can evolve to store full profiles.
|
|
165
|
+
|
|
166
|
+
## Strategy 4: Touch-Optimized Density
|
|
167
|
+
|
|
168
|
+
Touch interaction requires larger hit targets (minimum 44×44px per WCAG guidelines, 48×48px recommended by Google Material). The density system should enforce this:
|
|
169
|
+
|
|
170
|
+
```css
|
|
171
|
+
[data-interaction-mode="touch"] {
|
|
172
|
+
--admin-row-height: max(var(--admin-row-height), 44px);
|
|
173
|
+
--j-space-2: max(var(--j-space-2), 8px);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
[data-interaction-mode="touch"] button,
|
|
177
|
+
[data-interaction-mode="touch"] [role="button"],
|
|
178
|
+
[data-interaction-mode="touch"] a {
|
|
179
|
+
min-height: 44px;
|
|
180
|
+
min-width: 44px;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
This is applied as a CSS layer, so it works alongside any density setting — `compact` + `touch` still guarantees accessible hit targets.
|
|
185
|
+
|
|
186
|
+
## Reduced Motion and Accessibility
|
|
187
|
+
|
|
188
|
+
The token system already handles reduced motion:
|
|
189
|
+
|
|
190
|
+
```css
|
|
191
|
+
@media (prefers-reduced-motion: reduce) {
|
|
192
|
+
:root {
|
|
193
|
+
--j-duration-fast: 0ms;
|
|
194
|
+
--j-duration-normal: 0ms;
|
|
195
|
+
--j-duration-slow: 0ms;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
This should be extended to cover adaptive transitions — the panel collapse animations, nav morphing transitions, and drawer slide-ins that form part of the adaptive composition. If a control uses `transition: transform var(--j-duration-normal)`, reduced-motion users get instant changes automatically.
|
|
201
|
+
|
|
202
|
+
For high-contrast mode, the token system can add a `[data-contrast="high"]` tier that increases border widths, removes subtle shadows, and ensures minimum contrast ratios.
|
|
203
|
+
|
|
204
|
+
## Putting It Together: Token Cascade
|
|
205
|
+
|
|
206
|
+
The full cascade for a control's styling looks like:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
Base tokens (--j-*, --admin-*)
|
|
210
|
+
↓ overridden by
|
|
211
|
+
Theme tier ([data-theme="vs-dark"])
|
|
212
|
+
↓ overridden by
|
|
213
|
+
Density tier ([data-density-mode="compact"])
|
|
214
|
+
↓ overridden by
|
|
215
|
+
Interaction tier ([data-interaction-mode="touch"])
|
|
216
|
+
↓ overridden by
|
|
217
|
+
User overrides (inline style from theme profile)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Each layer only overrides what it needs. Controls consume tokens through CSS custom properties and adapt automatically as the cascade resolves.
|
|
221
|
+
|
|
222
|
+
## Summary
|
|
223
|
+
|
|
224
|
+
| Strategy | Purpose | Mechanism |
|
|
225
|
+
|----------|---------|-----------|
|
|
226
|
+
| Mode classes | Structural CSS adaptation | `data-layout-mode`, `data-density-mode` attributes |
|
|
227
|
+
| Token tiers | Global spacing/sizing adaptation | Token overrides per density mode |
|
|
228
|
+
| Theme profiles | Persistence and transfer | Serializable JSON objects |
|
|
229
|
+
| Touch density | Accessible hit targets | Minimum size enforcement via CSS |
|
|
230
|
+
| Reduced motion | Motion accessibility | Duration tokens zeroed |
|
|
231
|
+
|
|
232
|
+
The key insight is that jsgui3's token system already handles one axis of adaptation (light/dark theming) very well. Extending the same pattern to density, layout mode, and interaction mode is a natural evolution, not a redesign.
|
|
233
|
+
|
|
234
|
+
**Next:** [Chapter 5](05-showcase-app-multi-device-assessment.md) applies these strategies to the showcase app to assess what works and what needs to change.
|