@myop/cli 0.1.40 → 0.1.41
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/dist/myop-cli.js +1184 -833
- package/dist/skills/myop-component/SKILL.md +233 -0
- package/dist/skills/myop-component/references/best-practices.md +406 -0
- package/dist/skills/myop-component/references/component-api.md +357 -0
- package/dist/skills/myop-component/references/dev-workflow.md +218 -0
- package/dist/skills/myop-component/references/sizing-and-layout.md +270 -0
- package/dist/skills/myop-component/references/type-definitions.md +270 -0
- package/package.json +2 -2
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: myop-component
|
|
3
|
+
description: "Myop is a platform for creating self-contained HTML UI components that integrate into any host application. This skill covers the component public API (myop_init_interface, myop_cta_handler), type definitions, sizing, layout, development workflow, and CLI commands. When building or modifying a Myop component, you must learn this skill."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Myop Component Developer
|
|
7
|
+
|
|
8
|
+
Build high-performance, self-contained HTML components for the Myop platform.
|
|
9
|
+
|
|
10
|
+
## Immediate Action Required - Read This First
|
|
11
|
+
|
|
12
|
+
This skill activates when a `myop.config.json` file exists in the project, or when the user mentions "myop", "myop component", or asks to create/edit an HTML component for Myop.
|
|
13
|
+
|
|
14
|
+
**Your first action MUST be:**
|
|
15
|
+
1. Check if `myop.config.json` exists in the current directory
|
|
16
|
+
2. If **YES** (existing component):
|
|
17
|
+
- Read `myop.config.json` to understand the component name and state
|
|
18
|
+
- Read `index.html` to understand the current component implementation
|
|
19
|
+
- Implement the requested changes following the patterns in this skill
|
|
20
|
+
3. If **NO** (new component):
|
|
21
|
+
- Guide the user to run `myop create` to scaffold a new component
|
|
22
|
+
- Or create the files manually following the structure below
|
|
23
|
+
|
|
24
|
+
## Component Architecture
|
|
25
|
+
|
|
26
|
+
Every Myop component is a **single HTML file** (`index.html` or `dist/index.html`) that communicates with a host application through exactly **2 global functions**:
|
|
27
|
+
|
|
28
|
+
| Function | Direction | Purpose | Reference |
|
|
29
|
+
|----------|-----------|---------|-----------|
|
|
30
|
+
| `myop_init_interface(data)` | Host -> Component | Receive data, render UI | [component-api.md](references/component-api.md) |
|
|
31
|
+
| `myop_cta_handler(action, payload)` | Component -> Host | Send user actions back | [component-api.md](references/component-api.md) |
|
|
32
|
+
|
|
33
|
+
## CRITICAL: Rules You Must Follow
|
|
34
|
+
|
|
35
|
+
1. **Both functions MUST be defined at top-level script execution** - never inside `setTimeout`, `Promise.then`, `async`, `DOMContentLoaded`, or any deferred code
|
|
36
|
+
2. **Rendering MUST be synchronous** - no `fetch`, no `await`, no `setTimeout` inside `myop_init_interface`
|
|
37
|
+
3. **All state is private** - use an IIFE to encapsulate; only expose the 2 global functions
|
|
38
|
+
4. **Single-file output** - the final deployed artifact is ONE HTML file with all CSS/JS inlined
|
|
39
|
+
5. **No external network requests** - components cannot fetch external resources at runtime
|
|
40
|
+
|
|
41
|
+
## Quick Start - Minimal Component
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<!DOCTYPE html>
|
|
45
|
+
<html lang="en">
|
|
46
|
+
<head>
|
|
47
|
+
<meta charset="UTF-8">
|
|
48
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
49
|
+
<meta name="myop:size" content='{"width":"100%","height":"100%"}'>
|
|
50
|
+
<title>My Component</title>
|
|
51
|
+
<script type="myop/types">
|
|
52
|
+
interface MyopInitData {
|
|
53
|
+
title: string;
|
|
54
|
+
items: Array<{ id: string; label: string }>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface MyopCtaPayloads {
|
|
58
|
+
'item-selected': { itemId: string };
|
|
59
|
+
'size-requested': {
|
|
60
|
+
width?: number | null;
|
|
61
|
+
height?: number | null;
|
|
62
|
+
minWidth?: number | null;
|
|
63
|
+
maxWidth?: number | null;
|
|
64
|
+
minHeight?: number | null;
|
|
65
|
+
maxHeight?: number | null;
|
|
66
|
+
required?: boolean;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
declare function myop_init_interface(): MyopInitData;
|
|
71
|
+
declare function myop_init_interface(data: MyopInitData): void;
|
|
72
|
+
declare function myop_cta_handler<K extends keyof MyopCtaPayloads>(
|
|
73
|
+
action: K, payload: MyopCtaPayloads[K]
|
|
74
|
+
): void;
|
|
75
|
+
</script>
|
|
76
|
+
<style>
|
|
77
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
78
|
+
html, body { width: 100%; height: 100%; overflow: hidden;
|
|
79
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
80
|
+
}
|
|
81
|
+
#app-root { width: 100%; height: 100%; padding: 16px; }
|
|
82
|
+
</style>
|
|
83
|
+
</head>
|
|
84
|
+
<body>
|
|
85
|
+
<div id="app-root"></div>
|
|
86
|
+
|
|
87
|
+
<script>
|
|
88
|
+
(function() {
|
|
89
|
+
var state = {};
|
|
90
|
+
|
|
91
|
+
function render(data) {
|
|
92
|
+
state = data;
|
|
93
|
+
var root = document.getElementById('app-root');
|
|
94
|
+
root.innerHTML =
|
|
95
|
+
'<h2>' + (data.title || '') + '</h2>' +
|
|
96
|
+
'<ul>' + (data.items || []).map(function(item) {
|
|
97
|
+
return '<li data-id="' + item.id + '">' + item.label + '</li>';
|
|
98
|
+
}).join('') + '</ul>';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
window.myop_init_interface = function(data) {
|
|
102
|
+
if (!data) return state;
|
|
103
|
+
render(data);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
window.myop_cta_handler = function(action, payload) {
|
|
107
|
+
// Overridden by host application
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Event delegation
|
|
111
|
+
document.getElementById('app-root').addEventListener('click', function(e) {
|
|
112
|
+
var li = e.target.closest('li');
|
|
113
|
+
if (li) {
|
|
114
|
+
window.myop_cta_handler('item-selected', { itemId: li.dataset.id });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
})();
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<script id="myop_preview">
|
|
121
|
+
window.myop_init_interface({
|
|
122
|
+
title: 'My Component',
|
|
123
|
+
items: [
|
|
124
|
+
{ id: '1', label: 'First item' },
|
|
125
|
+
{ id: '2', label: 'Second item' }
|
|
126
|
+
]
|
|
127
|
+
});
|
|
128
|
+
</script>
|
|
129
|
+
</body>
|
|
130
|
+
</html>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Component File Structure
|
|
134
|
+
|
|
135
|
+
### Single-file mode (simplest)
|
|
136
|
+
```
|
|
137
|
+
project/
|
|
138
|
+
├── index.html # The entire component in one file
|
|
139
|
+
├── myop.config.json # Component metadata
|
|
140
|
+
└── .gitignore
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Multi-file mode (with build step)
|
|
144
|
+
```
|
|
145
|
+
project/
|
|
146
|
+
├── index.html # HTML template with <link> and <script src>
|
|
147
|
+
├── src/
|
|
148
|
+
│ ├── index.js # Entry point
|
|
149
|
+
│ ├── modules/
|
|
150
|
+
│ │ ├── app.js # Application logic
|
|
151
|
+
│ │ └── myop.js # Myop interface setup
|
|
152
|
+
│ └── styles/
|
|
153
|
+
│ ├── index.css # Style entry point
|
|
154
|
+
│ └── main.css # Main styles
|
|
155
|
+
├── build.js # esbuild bundler (inlines JS/CSS into dist/index.html)
|
|
156
|
+
├── dist/
|
|
157
|
+
│ └── index.html # Built single-file output
|
|
158
|
+
├── myop.config.json
|
|
159
|
+
├── package.json
|
|
160
|
+
└── .gitignore
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### myop.config.json
|
|
164
|
+
```json
|
|
165
|
+
{
|
|
166
|
+
"name": "Component Name",
|
|
167
|
+
"componentId": "DEV",
|
|
168
|
+
"type": "html",
|
|
169
|
+
"author": "@myop-cli",
|
|
170
|
+
"HMR": true
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- `componentId` starts as `"DEV"` and gets replaced with a real UUID after first `myop push`
|
|
175
|
+
- `organization` is added automatically after first push
|
|
176
|
+
|
|
177
|
+
## Reference Documentation
|
|
178
|
+
|
|
179
|
+
| Topic | Reference |
|
|
180
|
+
|-------|-----------|
|
|
181
|
+
| Public API (myop_init_interface, myop_cta_handler) | [component-api.md](references/component-api.md) |
|
|
182
|
+
| Type Definitions (MyopInitData, MyopCtaPayloads) | [type-definitions.md](references/type-definitions.md) |
|
|
183
|
+
| Sizing, Layout & Responsive Design | [sizing-and-layout.md](references/sizing-and-layout.md) |
|
|
184
|
+
| CLI Commands & Dev Workflow | [dev-workflow.md](references/dev-workflow.md) |
|
|
185
|
+
| Performance & Best Practices | [best-practices.md](references/best-practices.md) |
|
|
186
|
+
|
|
187
|
+
## Common Mistakes - WRONG vs CORRECT
|
|
188
|
+
|
|
189
|
+
### Function Definition Timing
|
|
190
|
+
|
|
191
|
+
| WRONG | CORRECT |
|
|
192
|
+
|-------|---------|
|
|
193
|
+
| Defining inside `DOMContentLoaded` | Defining in top-level `<script>` |
|
|
194
|
+
| Defining inside `setTimeout` | Defining in synchronous IIFE |
|
|
195
|
+
| Defining inside `async` function | Defining before any async code |
|
|
196
|
+
| Defining in a module with `export` | Assigning directly to `window` |
|
|
197
|
+
|
|
198
|
+
### Rendering
|
|
199
|
+
|
|
200
|
+
| WRONG | CORRECT |
|
|
201
|
+
|-------|---------|
|
|
202
|
+
| `await fetch()` inside `myop_init_interface` | Pure synchronous DOM manipulation |
|
|
203
|
+
| `setTimeout(() => render())` | Immediate `innerHTML` assignment |
|
|
204
|
+
| Multiple `appendChild` calls | Single `innerHTML` write |
|
|
205
|
+
| `document.createElement` loops | Template literal string building |
|
|
206
|
+
|
|
207
|
+
### State Management
|
|
208
|
+
|
|
209
|
+
| WRONG | CORRECT |
|
|
210
|
+
|-------|---------|
|
|
211
|
+
| Global variables on `window` | Variables inside IIFE closure |
|
|
212
|
+
| Mutating data passed to `myop_init_interface` | Storing a copy of the data |
|
|
213
|
+
| Not returning state when called without args | `if (!data) return state;` pattern |
|
|
214
|
+
|
|
215
|
+
## CLI Commands Quick Reference
|
|
216
|
+
|
|
217
|
+
| Command | Description |
|
|
218
|
+
|---------|-------------|
|
|
219
|
+
| `myop create` | Create a new component (scaffolds files + starts dev server) |
|
|
220
|
+
| `myop dev` | Start development server with HMR on port 9292 |
|
|
221
|
+
| `myop push` | Upload component to Myop platform |
|
|
222
|
+
| `myop sync` | Build and upload component to Myop platform |
|
|
223
|
+
| `myop login` | Authenticate with Myop (opens browser) |
|
|
224
|
+
| `myop logout` | Clear stored credentials |
|
|
225
|
+
| `myop whoami` | Show current authenticated user |
|
|
226
|
+
|
|
227
|
+
## Workflow Summary
|
|
228
|
+
|
|
229
|
+
1. `myop create` - Scaffold a new component
|
|
230
|
+
2. Edit `index.html` - Build your component UI
|
|
231
|
+
3. `myop dev` - Preview with hot reload at http://localhost:9292
|
|
232
|
+
4. `myop push` - Upload to Myop platform
|
|
233
|
+
5. View on dashboard: `https://dashboard.myop.dev/dashboard/2.0/component/<componentId>`
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
# Performance & Best Practices
|
|
2
|
+
|
|
3
|
+
Myop components must render instantly within a single event loop tick. This document covers patterns for achieving high performance and avoiding common pitfalls.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
- [Performance Requirements](#performance-requirements)
|
|
7
|
+
- [Rendering Patterns](#rendering-patterns)
|
|
8
|
+
- [State Management](#state-management)
|
|
9
|
+
- [Event Handling](#event-handling)
|
|
10
|
+
- [CSS Patterns](#css-patterns)
|
|
11
|
+
- [Accessibility](#accessibility)
|
|
12
|
+
- [Anti-Patterns](#anti-patterns)
|
|
13
|
+
|
|
14
|
+
## Performance Requirements
|
|
15
|
+
|
|
16
|
+
1. **Synchronous rendering** - `myop_init_interface(data)` must update the DOM and return before yielding to the event loop
|
|
17
|
+
2. **No network requests** - Components cannot fetch external resources
|
|
18
|
+
3. **Single DOM write** - Build the entire HTML string, then set `innerHTML` once
|
|
19
|
+
4. **Minimal reflows** - Avoid reading layout properties (offsetHeight, getBoundingClientRect) between writes
|
|
20
|
+
|
|
21
|
+
## Rendering Patterns
|
|
22
|
+
|
|
23
|
+
### Template Literal Rendering (recommended)
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
function render(data) {
|
|
27
|
+
var root = document.getElementById('app-root');
|
|
28
|
+
root.innerHTML =
|
|
29
|
+
'<div class="header">' +
|
|
30
|
+
'<h2>' + escapeHtml(data.title) + '</h2>' +
|
|
31
|
+
'<span class="count">' + data.items.length + ' items</span>' +
|
|
32
|
+
'</div>' +
|
|
33
|
+
'<ul class="list">' +
|
|
34
|
+
data.items.map(function(item) {
|
|
35
|
+
return '<li class="item' + (item.active ? ' active' : '') + '"' +
|
|
36
|
+
' data-id="' + item.id + '">' +
|
|
37
|
+
escapeHtml(item.name) +
|
|
38
|
+
'</li>';
|
|
39
|
+
}).join('') +
|
|
40
|
+
'</ul>';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function escapeHtml(str) {
|
|
44
|
+
if (!str) return '';
|
|
45
|
+
return String(str)
|
|
46
|
+
.replace(/&/g, '&')
|
|
47
|
+
.replace(/</g, '<')
|
|
48
|
+
.replace(/>/g, '>')
|
|
49
|
+
.replace(/"/g, '"');
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### DOM Caching
|
|
54
|
+
|
|
55
|
+
Cache element references that don't change between renders:
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
(function() {
|
|
59
|
+
// Cache once - these elements are in the static HTML
|
|
60
|
+
var root = document.getElementById('app-root');
|
|
61
|
+
var header = document.querySelector('.header');
|
|
62
|
+
var list = document.querySelector('.list');
|
|
63
|
+
|
|
64
|
+
function render(data) {
|
|
65
|
+
// Only update the parts that change
|
|
66
|
+
header.textContent = data.title;
|
|
67
|
+
list.innerHTML = data.items.map(function(item) {
|
|
68
|
+
return '<li data-id="' + item.id + '">' + item.name + '</li>';
|
|
69
|
+
}).join('');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ...
|
|
73
|
+
})();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Conditional Rendering
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
function render(data) {
|
|
80
|
+
var html = '<div class="container">';
|
|
81
|
+
|
|
82
|
+
if (data.loading) {
|
|
83
|
+
html += '<div class="spinner">Loading...</div>';
|
|
84
|
+
} else if (data.error) {
|
|
85
|
+
html += '<div class="error">' + escapeHtml(data.error) + '</div>';
|
|
86
|
+
} else if (data.items.length === 0) {
|
|
87
|
+
html += '<div class="empty">No items found</div>';
|
|
88
|
+
} else {
|
|
89
|
+
html += renderItemList(data.items);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
html += '</div>';
|
|
93
|
+
document.getElementById('app-root').innerHTML = html;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## State Management
|
|
98
|
+
|
|
99
|
+
### IIFE Encapsulation (required)
|
|
100
|
+
|
|
101
|
+
All component state and logic MUST be encapsulated in an IIFE (Immediately Invoked Function Expression):
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
(function() {
|
|
105
|
+
// Private state - not accessible outside this closure
|
|
106
|
+
var state = {};
|
|
107
|
+
var selectedId = null;
|
|
108
|
+
var isExpanded = false;
|
|
109
|
+
|
|
110
|
+
// Private functions
|
|
111
|
+
function render(data) { /* ... */ }
|
|
112
|
+
function updateSelection(id) { /* ... */ }
|
|
113
|
+
|
|
114
|
+
// Only expose the 2 required globals
|
|
115
|
+
window.myop_init_interface = function(data) {
|
|
116
|
+
if (!data) return state;
|
|
117
|
+
state = data;
|
|
118
|
+
render(data);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
window.myop_cta_handler = function(action, payload) {};
|
|
122
|
+
})();
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Derived State
|
|
126
|
+
|
|
127
|
+
Compute derived values during render, not on every access:
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
function render(data) {
|
|
131
|
+
// Compute derived state once during render
|
|
132
|
+
var completedCount = data.tasks.filter(function(t) { return t.completed; }).length;
|
|
133
|
+
var pendingCount = data.tasks.length - completedCount;
|
|
134
|
+
var progress = data.tasks.length > 0
|
|
135
|
+
? Math.round((completedCount / data.tasks.length) * 100)
|
|
136
|
+
: 0;
|
|
137
|
+
|
|
138
|
+
root.innerHTML =
|
|
139
|
+
'<div class="progress">' + progress + '% complete (' + completedCount + '/' + data.tasks.length + ')</div>' +
|
|
140
|
+
'<div class="list">' + renderTasks(data.tasks) + '</div>';
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Local UI State
|
|
145
|
+
|
|
146
|
+
For state that doesn't come from the host (e.g., expanded/collapsed sections, active tab):
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
(function() {
|
|
150
|
+
var state = {};
|
|
151
|
+
var uiState = {
|
|
152
|
+
activeTab: 'all',
|
|
153
|
+
expandedSections: {}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
function render(data) {
|
|
157
|
+
state = data;
|
|
158
|
+
var filtered = filterByTab(data.items, uiState.activeTab);
|
|
159
|
+
root.innerHTML = renderTabs(uiState.activeTab) + renderList(filtered);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function setTab(tab) {
|
|
163
|
+
uiState.activeTab = tab;
|
|
164
|
+
render(state); // Re-render with same data, different UI state
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ...
|
|
168
|
+
})();
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Event Handling
|
|
172
|
+
|
|
173
|
+
### Event Delegation (required for dynamic content)
|
|
174
|
+
|
|
175
|
+
Never attach event listeners to dynamically created elements. Use event delegation on a static parent:
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
(function() {
|
|
179
|
+
var root = document.getElementById('app-root');
|
|
180
|
+
|
|
181
|
+
// Single listener handles all interactions
|
|
182
|
+
root.addEventListener('click', function(e) {
|
|
183
|
+
// Check what was clicked using closest()
|
|
184
|
+
var item = e.target.closest('.item');
|
|
185
|
+
if (item) {
|
|
186
|
+
window.myop_cta_handler('item-clicked', {
|
|
187
|
+
itemId: item.dataset.id
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
var deleteBtn = e.target.closest('.delete-btn');
|
|
193
|
+
if (deleteBtn) {
|
|
194
|
+
e.stopPropagation(); // Don't trigger item click
|
|
195
|
+
window.myop_cta_handler('item-deleted', {
|
|
196
|
+
itemId: deleteBtn.closest('.item').dataset.id
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
var tab = e.target.closest('.tab');
|
|
202
|
+
if (tab) {
|
|
203
|
+
setTab(tab.dataset.tab);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Input events
|
|
209
|
+
root.addEventListener('change', function(e) {
|
|
210
|
+
if (e.target.matches('.checkbox')) {
|
|
211
|
+
var item = e.target.closest('.item');
|
|
212
|
+
window.myop_cta_handler('item-toggled', {
|
|
213
|
+
itemId: item.dataset.id,
|
|
214
|
+
checked: e.target.checked
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
})();
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Keyboard Support
|
|
222
|
+
|
|
223
|
+
```javascript
|
|
224
|
+
root.addEventListener('keydown', function(e) {
|
|
225
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
226
|
+
var focusable = e.target.closest('[role="button"], .item');
|
|
227
|
+
if (focusable) {
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
focusable.click(); // Reuse click handler
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## CSS Patterns
|
|
236
|
+
|
|
237
|
+
### Scoped Styles
|
|
238
|
+
|
|
239
|
+
Since the component runs in an iframe, global styles are safe. But for clarity, scope to `#app-root`:
|
|
240
|
+
|
|
241
|
+
```css
|
|
242
|
+
#app-root .header { /* ... */ }
|
|
243
|
+
#app-root .item { /* ... */ }
|
|
244
|
+
#app-root .item:hover { /* ... */ }
|
|
245
|
+
#app-root .item.active { /* ... */ }
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Responsive Design
|
|
249
|
+
|
|
250
|
+
Components should adapt to their container size. Use relative units and flexible layouts:
|
|
251
|
+
|
|
252
|
+
```css
|
|
253
|
+
#app-root {
|
|
254
|
+
display: flex;
|
|
255
|
+
flex-direction: column;
|
|
256
|
+
height: 100%;
|
|
257
|
+
font-size: 14px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* Stack horizontally when wide enough */
|
|
261
|
+
@media (min-width: 500px) {
|
|
262
|
+
.content {
|
|
263
|
+
flex-direction: row;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* Compact mode for narrow containers */
|
|
268
|
+
@media (max-width: 300px) {
|
|
269
|
+
.header { padding: 8px; font-size: 12px; }
|
|
270
|
+
.item { padding: 6px 8px; }
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Theme Support
|
|
275
|
+
|
|
276
|
+
Accept theme via data and apply with CSS classes:
|
|
277
|
+
|
|
278
|
+
```javascript
|
|
279
|
+
function render(data) {
|
|
280
|
+
document.documentElement.setAttribute('data-theme', data.config?.theme || 'light');
|
|
281
|
+
// ... render content
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
```css
|
|
286
|
+
:root {
|
|
287
|
+
--bg: #ffffff;
|
|
288
|
+
--text: #333333;
|
|
289
|
+
--border: #e0e0e0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
[data-theme="dark"] {
|
|
293
|
+
--bg: #1a1a2e;
|
|
294
|
+
--text: #e0e0e0;
|
|
295
|
+
--border: #333355;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#app-root {
|
|
299
|
+
background: var(--bg);
|
|
300
|
+
color: var(--text);
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Accessibility
|
|
305
|
+
|
|
306
|
+
### Semantic HTML
|
|
307
|
+
|
|
308
|
+
```html
|
|
309
|
+
<!-- Use semantic elements -->
|
|
310
|
+
<nav class="tabs" role="tablist">...</nav>
|
|
311
|
+
<main class="content" role="tabpanel">...</main>
|
|
312
|
+
|
|
313
|
+
<!-- Use ARIA for custom widgets -->
|
|
314
|
+
<div role="listbox" aria-label="Task list">
|
|
315
|
+
<div role="option" aria-selected="true">...</div>
|
|
316
|
+
</div>
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Focus Management
|
|
320
|
+
|
|
321
|
+
```css
|
|
322
|
+
/* Visible focus indicators */
|
|
323
|
+
:focus-visible {
|
|
324
|
+
outline: 2px solid #4a90d9;
|
|
325
|
+
outline-offset: 2px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* Interactive elements should be focusable */
|
|
329
|
+
.item[tabindex="0"] { cursor: pointer; }
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Anti-Patterns
|
|
333
|
+
|
|
334
|
+
### Never: Async operations in myop_init_interface
|
|
335
|
+
|
|
336
|
+
```javascript
|
|
337
|
+
// WRONG
|
|
338
|
+
window.myop_init_interface = async function(data) {
|
|
339
|
+
const extra = await loadMoreData(); // NO
|
|
340
|
+
render(data, extra);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// WRONG
|
|
344
|
+
window.myop_init_interface = function(data) {
|
|
345
|
+
fetch('/api/data').then(render); // NO
|
|
346
|
+
setTimeout(() => render(data), 0); // NO
|
|
347
|
+
requestAnimationFrame(() => render(data)); // NO
|
|
348
|
+
};
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Never: Multiple DOM writes
|
|
352
|
+
|
|
353
|
+
```javascript
|
|
354
|
+
// WRONG - 3 reflows
|
|
355
|
+
root.innerHTML = '<div class="header"></div>';
|
|
356
|
+
root.querySelector('.header').textContent = data.title;
|
|
357
|
+
root.innerHTML += '<ul>' + listHtml + '</ul>';
|
|
358
|
+
|
|
359
|
+
// CORRECT - 1 DOM write
|
|
360
|
+
root.innerHTML =
|
|
361
|
+
'<div class="header">' + data.title + '</div>' +
|
|
362
|
+
'<ul>' + listHtml + '</ul>';
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Never: Global variables
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
// WRONG - pollutes global scope
|
|
369
|
+
var myState = {};
|
|
370
|
+
window.myHelper = function() {};
|
|
371
|
+
|
|
372
|
+
// CORRECT - everything inside IIFE
|
|
373
|
+
(function() {
|
|
374
|
+
var myState = {};
|
|
375
|
+
function myHelper() {}
|
|
376
|
+
// ...
|
|
377
|
+
})();
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Never: Direct DOM creation in loops
|
|
381
|
+
|
|
382
|
+
```javascript
|
|
383
|
+
// WRONG - O(n) reflows
|
|
384
|
+
data.items.forEach(function(item) {
|
|
385
|
+
var li = document.createElement('li');
|
|
386
|
+
li.textContent = item.name;
|
|
387
|
+
list.appendChild(li); // Reflow on each append
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// CORRECT - single innerHTML write
|
|
391
|
+
list.innerHTML = data.items.map(function(item) {
|
|
392
|
+
return '<li>' + escapeHtml(item.name) + '</li>';
|
|
393
|
+
}).join('');
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Never: External resource loading
|
|
397
|
+
|
|
398
|
+
```javascript
|
|
399
|
+
// WRONG - components can't fetch
|
|
400
|
+
var img = new Image();
|
|
401
|
+
img.src = 'https://external-cdn.com/image.png'; // NO
|
|
402
|
+
|
|
403
|
+
// WRONG - loading external scripts
|
|
404
|
+
var script = document.createElement('script');
|
|
405
|
+
script.src = 'https://cdn.example.com/lib.js'; // NO
|
|
406
|
+
```
|