sigpro 1.0.14 → 1.2.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +164 -1008
- package/dist/sigpro.editor.js +1 -0
- package/dist/sigpro.grid.js +78 -0
- package/dist/sigpro.js +1 -0
- package/dist/sigpro.ui.css +2 -0
- package/dist/sigpro.ui.js +1 -0
- package/dist/sigpro.utils.js +1 -0
- package/dist/sigpro.vite.js +4 -0
- package/package.json +64 -14
- package/sigpro.d.ts +395 -0
- package/.github/workflows/publish.yml +0 -25
- package/bun.lock +0 -385
- package/docs/404.html +0 -22
- package/docs/api/components.html +0 -595
- package/docs/api/effects.html +0 -787
- package/docs/api/fetch.html +0 -873
- package/docs/api/pages.html +0 -405
- package/docs/api/quick.html +0 -217
- package/docs/api/routing.html +0 -628
- package/docs/api/signals.html +0 -683
- package/docs/api/storage.html +0 -820
- package/docs/assets/api_components.md.BlFwj17l.js +0 -571
- package/docs/assets/api_components.md.BlFwj17l.lean.js +0 -1
- package/docs/assets/api_effects.md.Br_yStBS.js +0 -763
- package/docs/assets/api_effects.md.Br_yStBS.lean.js +0 -1
- package/docs/assets/api_fetch.md.DQLBJSoq.js +0 -849
- package/docs/assets/api_fetch.md.DQLBJSoq.lean.js +0 -1
- package/docs/assets/api_pages.md.BP19nHXw.js +0 -381
- package/docs/assets/api_pages.md.BP19nHXw.lean.js +0 -1
- package/docs/assets/api_quick.md.BDS3ttnt.js +0 -193
- package/docs/assets/api_quick.md.BDS3ttnt.lean.js +0 -1
- package/docs/assets/api_routing.md.7SNAZXtp.js +0 -604
- package/docs/assets/api_routing.md.7SNAZXtp.lean.js +0 -1
- package/docs/assets/api_signals.md.CrW68-BA.js +0 -659
- package/docs/assets/api_signals.md.CrW68-BA.lean.js +0 -1
- package/docs/assets/api_storage.md.COEWBXHk.js +0 -796
- package/docs/assets/api_storage.md.COEWBXHk.lean.js +0 -1
- package/docs/assets/app.DtmzNmNl.js +0 -1
- package/docs/assets/chunks/framework.C8AWLET_.js +0 -19
- package/docs/assets/chunks/theme.yfWKMLQM.js +0 -1
- package/docs/assets/guide_getting-started.md.BeQpK3vd.js +0 -172
- package/docs/assets/guide_getting-started.md.BeQpK3vd.lean.js +0 -1
- package/docs/assets/guide_why.md.DXchYMN-.js +0 -23
- package/docs/assets/guide_why.md.DXchYMN-.lean.js +0 -1
- package/docs/assets/index.md.uvMJmU4o.js +0 -1
- package/docs/assets/index.md.uvMJmU4o.lean.js +0 -1
- package/docs/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
- package/docs/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
- package/docs/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
- package/docs/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
- package/docs/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
- package/docs/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
- package/docs/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
- package/docs/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
- package/docs/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
- package/docs/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
- package/docs/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
- package/docs/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
- package/docs/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
- package/docs/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
- package/docs/assets/style.DJRheFKp.css +0 -1
- package/docs/assets/ui_intro.md.gZ21GFqo.js +0 -1
- package/docs/assets/ui_intro.md.gZ21GFqo.lean.js +0 -1
- package/docs/assets/vite_plugin.md.gDWEi8f0.js +0 -225
- package/docs/assets/vite_plugin.md.gDWEi8f0.lean.js +0 -1
- package/docs/guide/getting-started.html +0 -196
- package/docs/guide/why.html +0 -47
- package/docs/hashmap.json +0 -1
- package/docs/index.html +0 -25
- package/docs/logo.svg +0 -118
- package/docs/ui/intro.html +0 -25
- package/docs/vite/plugin.html +0 -249
- package/docs/vp-icons.css +0 -1
- package/index.js +0 -3
- package/packages/docs/.vitepress/cache/deps/@theme_index.js +0 -275
- package/packages/docs/.vitepress/cache/deps/@theme_index.js.map +0 -7
- package/packages/docs/.vitepress/cache/deps/_metadata.json +0 -40
- package/packages/docs/.vitepress/cache/deps/chunk-3S55Y3P7.js +0 -12951
- package/packages/docs/.vitepress/cache/deps/chunk-3S55Y3P7.js.map +0 -7
- package/packages/docs/.vitepress/cache/deps/chunk-RLEUDPPB.js +0 -9719
- package/packages/docs/.vitepress/cache/deps/chunk-RLEUDPPB.js.map +0 -7
- package/packages/docs/.vitepress/cache/deps/package.json +0 -3
- package/packages/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +0 -4505
- package/packages/docs/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +0 -7
- package/packages/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js +0 -583
- package/packages/docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +0 -7
- package/packages/docs/.vitepress/cache/deps/vue.js +0 -347
- package/packages/docs/.vitepress/cache/deps/vue.js.map +0 -7
- package/packages/docs/.vitepress/config.js +0 -68
- package/packages/docs/api/components.md +0 -760
- package/packages/docs/api/effects.md +0 -1039
- package/packages/docs/api/fetch.md +0 -998
- package/packages/docs/api/pages.md +0 -497
- package/packages/docs/api/quick.md +0 -436
- package/packages/docs/api/routing.md +0 -784
- package/packages/docs/api/signals.md +0 -899
- package/packages/docs/api/storage.md +0 -952
- package/packages/docs/guide/getting-started.md +0 -308
- package/packages/docs/guide/why.md +0 -135
- package/packages/docs/index.md +0 -84
- package/packages/docs/logo.svg +0 -118
- package/packages/docs/public/logo.svg +0 -118
- package/packages/docs/ui/intro.md +0 -16
- package/packages/docs/vite/plugin.md +0 -423
- package/packages/sigpro/plugin.js +0 -91
- package/packages/sigpro/plugin.min.js +0 -1
- package/packages/sigpro/sigpro.js +0 -631
- package/packages/sigpro/sigpro.min.js +0 -1
- package/vite.config.js +0 -24
|
@@ -1,760 +0,0 @@
|
|
|
1
|
-
# Components API 🧩
|
|
2
|
-
|
|
3
|
-
Components in SigPro are native Web Components built on the Custom Elements standard. They provide a way to create reusable, encapsulated pieces of UI with reactive properties and automatic cleanup.
|
|
4
|
-
|
|
5
|
-
## `$.component(tagName, setupFunction, observedAttributes, useShadowDOM)`
|
|
6
|
-
|
|
7
|
-
Creates a custom element with reactive properties and automatic dependency tracking.
|
|
8
|
-
|
|
9
|
-
```javascript
|
|
10
|
-
import { $, html } from 'sigpro';
|
|
11
|
-
|
|
12
|
-
$.component('my-button', (props, { slot, emit }) => {
|
|
13
|
-
return html`
|
|
14
|
-
<button
|
|
15
|
-
class="btn"
|
|
16
|
-
@click=${() => emit('click')}
|
|
17
|
-
>
|
|
18
|
-
${slot()}
|
|
19
|
-
</button>
|
|
20
|
-
`;
|
|
21
|
-
}, ['variant']); // Observe the 'variant' attribute
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## 📋 API Reference
|
|
25
|
-
|
|
26
|
-
### Parameters
|
|
27
|
-
|
|
28
|
-
| Parameter | Type | Default | Description |
|
|
29
|
-
|-----------|------|---------|-------------|
|
|
30
|
-
| `tagName` | `string` | required | Custom element tag name (must include a hyphen, e.g., `my-button`) |
|
|
31
|
-
| `setupFunction` | `Function` | required | Function that returns the component's template |
|
|
32
|
-
| `observedAttributes` | `string[]` | `[]` | Attributes to observe for changes (become reactive props) |
|
|
33
|
-
| `useShadowDOM` | `boolean` | `false` | `true` = Shadow DOM (encapsulated), `false` = Light DOM (inherits styles) |
|
|
34
|
-
|
|
35
|
-
### Setup Function Parameters
|
|
36
|
-
|
|
37
|
-
The setup function receives two arguments:
|
|
38
|
-
|
|
39
|
-
1. **`props`** - Object containing reactive signals for each observed attribute
|
|
40
|
-
2. **`context`** - Object with helper methods and properties
|
|
41
|
-
|
|
42
|
-
#### Context Object Properties
|
|
43
|
-
|
|
44
|
-
| Property | Type | Description |
|
|
45
|
-
|----------|------|-------------|
|
|
46
|
-
| `slot(name)` | `Function` | Returns array of child nodes for the specified slot |
|
|
47
|
-
| `emit(name, detail)` | `Function` | Dispatches a custom event |
|
|
48
|
-
| `select(selector)` | `Function` | Query selector within component's root |
|
|
49
|
-
| `selectAll(selector)` | `Function` | Query selector all within component's root |
|
|
50
|
-
| `host` | `HTMLElement` | Reference to the custom element instance |
|
|
51
|
-
| `root` | `Node` | Component's root (shadow root or element itself) |
|
|
52
|
-
| `onUnmount(callback)` | `Function` | Register cleanup function |
|
|
53
|
-
|
|
54
|
-
## 🏠 Light DOM vs Shadow DOM
|
|
55
|
-
|
|
56
|
-
### Light DOM (`useShadowDOM = false`) - Default
|
|
57
|
-
|
|
58
|
-
The component **inherits global styles** from the application. Perfect for components that should integrate with your site's design system.
|
|
59
|
-
|
|
60
|
-
```javascript
|
|
61
|
-
// Button that uses global Tailwind CSS
|
|
62
|
-
$.component('tw-button', (props, { slot, emit }) => {
|
|
63
|
-
const variant = props.variant() || 'primary';
|
|
64
|
-
|
|
65
|
-
const variants = {
|
|
66
|
-
primary: 'bg-blue-500 hover:bg-blue-600 text-white',
|
|
67
|
-
secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
|
|
68
|
-
outline: 'border border-blue-500 text-blue-500 hover:bg-blue-50'
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
return html`
|
|
72
|
-
<button
|
|
73
|
-
class="px-4 py-2 rounded font-semibold transition-colors ${variants[variant]}"
|
|
74
|
-
@click=${() => emit('click')}
|
|
75
|
-
>
|
|
76
|
-
${slot()}
|
|
77
|
-
</button>
|
|
78
|
-
`;
|
|
79
|
-
}, ['variant']);
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### Shadow DOM (`useShadowDOM = true`) - Encapsulated
|
|
83
|
-
|
|
84
|
-
The component **encapsulates its styles** completely. External styles don't affect it, and its styles don't leak out.
|
|
85
|
-
|
|
86
|
-
```javascript
|
|
87
|
-
// Calendar with encapsulated styles
|
|
88
|
-
$.component('ui-calendar', (props) => {
|
|
89
|
-
return html`
|
|
90
|
-
<style>
|
|
91
|
-
/* These styles won't affect the rest of the page */
|
|
92
|
-
.calendar {
|
|
93
|
-
font-family: system-ui, sans-serif;
|
|
94
|
-
background: white;
|
|
95
|
-
border-radius: 12px;
|
|
96
|
-
padding: 20px;
|
|
97
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
98
|
-
}
|
|
99
|
-
.day {
|
|
100
|
-
aspect-ratio: 1;
|
|
101
|
-
display: flex;
|
|
102
|
-
align-items: center;
|
|
103
|
-
justify-content: center;
|
|
104
|
-
cursor: pointer;
|
|
105
|
-
border-radius: 50%;
|
|
106
|
-
}
|
|
107
|
-
.day.selected {
|
|
108
|
-
background: #2196f3;
|
|
109
|
-
color: white;
|
|
110
|
-
}
|
|
111
|
-
</style>
|
|
112
|
-
|
|
113
|
-
<div class="calendar">
|
|
114
|
-
${renderCalendar(props.date())}
|
|
115
|
-
</div>
|
|
116
|
-
`;
|
|
117
|
-
}, ['date'], true); // true = use Shadow DOM
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
## 🎯 Basic Examples
|
|
121
|
-
|
|
122
|
-
### Simple Counter Component
|
|
123
|
-
|
|
124
|
-
```javascript
|
|
125
|
-
// counter.js
|
|
126
|
-
$.component('my-counter', (props) => {
|
|
127
|
-
const count = $(0);
|
|
128
|
-
|
|
129
|
-
return html`
|
|
130
|
-
<div class="counter">
|
|
131
|
-
<p>Count: ${count}</p>
|
|
132
|
-
<button @click=${() => count(c => c + 1)}>+</button>
|
|
133
|
-
<button @click=${() => count(c => c - 1)}>-</button>
|
|
134
|
-
<button @click=${() => count(0)}>Reset</button>
|
|
135
|
-
</div>
|
|
136
|
-
`;
|
|
137
|
-
});
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
**Usage:**
|
|
141
|
-
```html
|
|
142
|
-
<my-counter></my-counter>
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### Component with Props
|
|
146
|
-
|
|
147
|
-
```javascript
|
|
148
|
-
// greeting.js
|
|
149
|
-
$.component('my-greeting', (props) => {
|
|
150
|
-
const name = props.name() || 'World';
|
|
151
|
-
const greeting = $(() => `Hello, ${name}!`);
|
|
152
|
-
|
|
153
|
-
return html`
|
|
154
|
-
<div class="greeting">
|
|
155
|
-
<h1>${greeting}</h1>
|
|
156
|
-
<p>This is a greeting component.</p>
|
|
157
|
-
</div>
|
|
158
|
-
`;
|
|
159
|
-
}, ['name']); // Observe the 'name' attribute
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
**Usage:**
|
|
163
|
-
```html
|
|
164
|
-
<my-greeting name="John"></my-greeting>
|
|
165
|
-
<my-greeting name="Jane"></my-greeting>
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### Component with Events
|
|
169
|
-
|
|
170
|
-
```javascript
|
|
171
|
-
// toggle.js
|
|
172
|
-
$.component('my-toggle', (props, { emit }) => {
|
|
173
|
-
const isOn = $(props.initial() === 'on');
|
|
174
|
-
|
|
175
|
-
const toggle = () => {
|
|
176
|
-
isOn(!isOn());
|
|
177
|
-
emit('toggle', { isOn: isOn() });
|
|
178
|
-
emit(isOn() ? 'on' : 'off');
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
return html`
|
|
182
|
-
<button
|
|
183
|
-
class="toggle ${() => isOn() ? 'active' : ''}"
|
|
184
|
-
@click=${toggle}
|
|
185
|
-
>
|
|
186
|
-
${() => isOn() ? 'ON' : 'OFF'}
|
|
187
|
-
</button>
|
|
188
|
-
`;
|
|
189
|
-
}, ['initial']);
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
**Usage:**
|
|
193
|
-
```html
|
|
194
|
-
<my-toggle
|
|
195
|
-
initial="off"
|
|
196
|
-
@toggle=${(e) => console.log('Toggled:', e.detail)}
|
|
197
|
-
@on=${() => console.log('Turned on')}
|
|
198
|
-
@off=${() => console.log('Turned off')}
|
|
199
|
-
></my-toggle>
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
## 🎨 Advanced Examples
|
|
203
|
-
|
|
204
|
-
### Form Input Component
|
|
205
|
-
|
|
206
|
-
```javascript
|
|
207
|
-
// form-input.js
|
|
208
|
-
$.component('form-input', (props, { emit }) => {
|
|
209
|
-
const value = $(props.value() || '');
|
|
210
|
-
const error = $(null);
|
|
211
|
-
const touched = $(false);
|
|
212
|
-
|
|
213
|
-
// Validation effect
|
|
214
|
-
$.effect(() => {
|
|
215
|
-
if (props.pattern() && touched()) {
|
|
216
|
-
const regex = new RegExp(props.pattern());
|
|
217
|
-
const isValid = regex.test(value());
|
|
218
|
-
error(isValid ? null : props.errorMessage() || 'Invalid input');
|
|
219
|
-
emit('validate', { isValid, value: value() });
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
const handleInput = (e) => {
|
|
224
|
-
value(e.target.value);
|
|
225
|
-
emit('update', e.target.value);
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
const handleBlur = () => {
|
|
229
|
-
touched(true);
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
return html`
|
|
233
|
-
<div class="form-group">
|
|
234
|
-
${props.label() ? html`
|
|
235
|
-
<label class="form-label">
|
|
236
|
-
${props.label()}
|
|
237
|
-
${props.required() ? html`<span class="required">*</span>` : ''}
|
|
238
|
-
</label>
|
|
239
|
-
` : ''}
|
|
240
|
-
|
|
241
|
-
<input
|
|
242
|
-
type="${props.type() || 'text'}"
|
|
243
|
-
class="form-control ${() => error() ? 'is-invalid' : ''}"
|
|
244
|
-
:value=${value}
|
|
245
|
-
@input=${handleInput}
|
|
246
|
-
@blur=${handleBlur}
|
|
247
|
-
placeholder="${props.placeholder() || ''}"
|
|
248
|
-
?disabled=${props.disabled}
|
|
249
|
-
?required=${props.required}
|
|
250
|
-
/>
|
|
251
|
-
|
|
252
|
-
${() => error() ? html`
|
|
253
|
-
<div class="error-message">${error()}</div>
|
|
254
|
-
` : ''}
|
|
255
|
-
|
|
256
|
-
${props.helpText() ? html`
|
|
257
|
-
<small class="help-text">${props.helpText()}</small>
|
|
258
|
-
` : ''}
|
|
259
|
-
</div>
|
|
260
|
-
`;
|
|
261
|
-
}, ['label', 'type', 'value', 'placeholder', 'disabled', 'required', 'pattern', 'errorMessage', 'helpText']);
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
**Usage:**
|
|
265
|
-
```html
|
|
266
|
-
<form-input
|
|
267
|
-
label="Email"
|
|
268
|
-
type="email"
|
|
269
|
-
required
|
|
270
|
-
pattern="^[^\s@]+@[^\s@]+\.[^\s@]+$"
|
|
271
|
-
errorMessage="Please enter a valid email"
|
|
272
|
-
@update=${(e) => formData.email = e.detail}
|
|
273
|
-
@validate=${(e) => setEmailValid(e.detail.isValid)}
|
|
274
|
-
>
|
|
275
|
-
</form-input>
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
### Modal/Dialog Component
|
|
279
|
-
|
|
280
|
-
```javascript
|
|
281
|
-
// modal.js
|
|
282
|
-
$.component('my-modal', (props, { slot, emit, onUnmount }) => {
|
|
283
|
-
const isOpen = $(false);
|
|
284
|
-
|
|
285
|
-
// Handle escape key
|
|
286
|
-
const handleKeydown = (e) => {
|
|
287
|
-
if (e.key === 'Escape' && isOpen()) {
|
|
288
|
-
close();
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
$.effect(() => {
|
|
293
|
-
if (isOpen()) {
|
|
294
|
-
document.addEventListener('keydown', handleKeydown);
|
|
295
|
-
document.body.style.overflow = 'hidden';
|
|
296
|
-
} else {
|
|
297
|
-
document.removeEventListener('keydown', handleKeydown);
|
|
298
|
-
document.body.style.overflow = '';
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// Cleanup on unmount
|
|
303
|
-
onUnmount(() => {
|
|
304
|
-
document.removeEventListener('keydown', handleKeydown);
|
|
305
|
-
document.body.style.overflow = '';
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const open = () => {
|
|
309
|
-
isOpen(true);
|
|
310
|
-
emit('open');
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
const close = () => {
|
|
314
|
-
isOpen(false);
|
|
315
|
-
emit('close');
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
// Expose methods to parent
|
|
319
|
-
props.open = open;
|
|
320
|
-
props.close = close;
|
|
321
|
-
|
|
322
|
-
return html`
|
|
323
|
-
<div>
|
|
324
|
-
<!-- Trigger button -->
|
|
325
|
-
<button
|
|
326
|
-
class="modal-trigger"
|
|
327
|
-
@click=${open}
|
|
328
|
-
>
|
|
329
|
-
${slot('trigger') || 'Open Modal'}
|
|
330
|
-
</button>
|
|
331
|
-
|
|
332
|
-
<!-- Modal overlay -->
|
|
333
|
-
${() => isOpen() ? html`
|
|
334
|
-
<div class="modal-overlay" @click=${close}>
|
|
335
|
-
<div class="modal-content" @click.stop>
|
|
336
|
-
<div class="modal-header">
|
|
337
|
-
<h3>${props.title() || 'Modal'}</h3>
|
|
338
|
-
<button class="close-btn" @click=${close}>×</button>
|
|
339
|
-
</div>
|
|
340
|
-
<div class="modal-body">
|
|
341
|
-
${slot('body')}
|
|
342
|
-
</div>
|
|
343
|
-
<div class="modal-footer">
|
|
344
|
-
${slot('footer') || html`
|
|
345
|
-
<button @click=${close}>Close</button>
|
|
346
|
-
`}
|
|
347
|
-
</div>
|
|
348
|
-
</div>
|
|
349
|
-
</div>
|
|
350
|
-
` : ''}
|
|
351
|
-
</div>
|
|
352
|
-
`;
|
|
353
|
-
}, ['title'], false);
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
**Usage:**
|
|
357
|
-
```html
|
|
358
|
-
<my-modal title="Confirm Delete">
|
|
359
|
-
<button slot="trigger">Delete Item</button>
|
|
360
|
-
|
|
361
|
-
<div slot="body">
|
|
362
|
-
<p>Are you sure you want to delete this item?</p>
|
|
363
|
-
<p class="warning">This action cannot be undone.</p>
|
|
364
|
-
</div>
|
|
365
|
-
|
|
366
|
-
<div slot="footer">
|
|
367
|
-
<button class="cancel" @click=${close}>Cancel</button>
|
|
368
|
-
<button class="delete" @click=${handleDelete}>Delete</button>
|
|
369
|
-
</div>
|
|
370
|
-
</my-modal>
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
### Data Table Component
|
|
374
|
-
|
|
375
|
-
```javascript
|
|
376
|
-
// data-table.js
|
|
377
|
-
$.component('data-table', (props, { emit }) => {
|
|
378
|
-
const data = $(props.data() || []);
|
|
379
|
-
const columns = $(props.columns() || []);
|
|
380
|
-
const sortColumn = $(null);
|
|
381
|
-
const sortDirection = $('asc');
|
|
382
|
-
const filterText = $('');
|
|
383
|
-
|
|
384
|
-
// Computed: filtered and sorted data
|
|
385
|
-
const processedData = $(() => {
|
|
386
|
-
let result = [...data()];
|
|
387
|
-
|
|
388
|
-
// Filter
|
|
389
|
-
if (filterText()) {
|
|
390
|
-
const search = filterText().toLowerCase();
|
|
391
|
-
result = result.filter(row =>
|
|
392
|
-
Object.values(row).some(val =>
|
|
393
|
-
String(val).toLowerCase().includes(search)
|
|
394
|
-
)
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Sort
|
|
399
|
-
if (sortColumn()) {
|
|
400
|
-
const col = sortColumn();
|
|
401
|
-
const direction = sortDirection() === 'asc' ? 1 : -1;
|
|
402
|
-
|
|
403
|
-
result.sort((a, b) => {
|
|
404
|
-
if (a[col] < b[col]) return -direction;
|
|
405
|
-
if (a[col] > b[col]) return direction;
|
|
406
|
-
return 0;
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return result;
|
|
411
|
-
});
|
|
412
|
-
|
|
413
|
-
const handleSort = (col) => {
|
|
414
|
-
if (sortColumn() === col) {
|
|
415
|
-
sortDirection(sortDirection() === 'asc' ? 'desc' : 'asc');
|
|
416
|
-
} else {
|
|
417
|
-
sortColumn(col);
|
|
418
|
-
sortDirection('asc');
|
|
419
|
-
}
|
|
420
|
-
emit('sort', { column: col, direction: sortDirection() });
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
return html`
|
|
424
|
-
<div class="data-table">
|
|
425
|
-
<!-- Search input -->
|
|
426
|
-
<div class="table-toolbar">
|
|
427
|
-
<input
|
|
428
|
-
type="search"
|
|
429
|
-
:value=${filterText}
|
|
430
|
-
placeholder="Search..."
|
|
431
|
-
class="search-input"
|
|
432
|
-
/>
|
|
433
|
-
<span class="record-count">
|
|
434
|
-
${() => `${processedData().length} of ${data().length} records`}
|
|
435
|
-
</span>
|
|
436
|
-
</div>
|
|
437
|
-
|
|
438
|
-
<!-- Table -->
|
|
439
|
-
<table>
|
|
440
|
-
<thead>
|
|
441
|
-
<tr>
|
|
442
|
-
${columns().map(col => html`
|
|
443
|
-
<th
|
|
444
|
-
@click=${() => handleSort(col.field)}
|
|
445
|
-
class:sortable=${true}
|
|
446
|
-
class:sorted=${() => sortColumn() === col.field}
|
|
447
|
-
>
|
|
448
|
-
${col.label}
|
|
449
|
-
${() => sortColumn() === col.field ? html`
|
|
450
|
-
<span class="sort-icon">
|
|
451
|
-
${sortDirection() === 'asc' ? '↑' : '↓'}
|
|
452
|
-
</span>
|
|
453
|
-
` : ''}
|
|
454
|
-
</th>
|
|
455
|
-
`)}
|
|
456
|
-
</tr>
|
|
457
|
-
</thead>
|
|
458
|
-
<tbody>
|
|
459
|
-
${() => processedData().map(row => html`
|
|
460
|
-
<tr @click=${() => emit('row-click', row)}>
|
|
461
|
-
${columns().map(col => html`
|
|
462
|
-
<td>${row[col.field]}</td>
|
|
463
|
-
`)}
|
|
464
|
-
</tr>
|
|
465
|
-
`)}
|
|
466
|
-
</tbody>
|
|
467
|
-
</table>
|
|
468
|
-
|
|
469
|
-
<!-- Empty state -->
|
|
470
|
-
${() => processedData().length === 0 ? html`
|
|
471
|
-
<div class="empty-state">
|
|
472
|
-
No data found
|
|
473
|
-
</div>
|
|
474
|
-
` : ''}
|
|
475
|
-
</div>
|
|
476
|
-
`;
|
|
477
|
-
}, ['data', 'columns']);
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
**Usage:**
|
|
481
|
-
```javascript
|
|
482
|
-
const userColumns = [
|
|
483
|
-
{ field: 'id', label: 'ID' },
|
|
484
|
-
{ field: 'name', label: 'Name' },
|
|
485
|
-
{ field: 'email', label: 'Email' },
|
|
486
|
-
{ field: 'role', label: 'Role' }
|
|
487
|
-
];
|
|
488
|
-
|
|
489
|
-
const userData = [
|
|
490
|
-
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' },
|
|
491
|
-
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User' }
|
|
492
|
-
];
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
```html
|
|
496
|
-
<data-table
|
|
497
|
-
.data=${userData}
|
|
498
|
-
.columns=${userColumns}
|
|
499
|
-
@row-click=${(e) => console.log('Row clicked:', e.detail)}
|
|
500
|
-
>
|
|
501
|
-
</data-table>
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
### Tabs Component
|
|
505
|
-
|
|
506
|
-
```javascript
|
|
507
|
-
// tabs.js
|
|
508
|
-
$.component('my-tabs', (props, { slot, emit }) => {
|
|
509
|
-
const activeTab = $(props.active() || 0);
|
|
510
|
-
|
|
511
|
-
// Get all tab headers from slots
|
|
512
|
-
const tabs = $(() => {
|
|
513
|
-
const headers = slot('tab');
|
|
514
|
-
return headers.map((node, index) => ({
|
|
515
|
-
index,
|
|
516
|
-
title: node.textContent,
|
|
517
|
-
content: slot(`panel-${index}`)[0]
|
|
518
|
-
}));
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
$.effect(() => {
|
|
522
|
-
emit('change', { index: activeTab(), tab: tabs()[activeTab()] });
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
return html`
|
|
526
|
-
<div class="tabs">
|
|
527
|
-
<div class="tab-headers">
|
|
528
|
-
${tabs().map(tab => html`
|
|
529
|
-
<button
|
|
530
|
-
class="tab-header ${() => activeTab() === tab.index ? 'active' : ''}"
|
|
531
|
-
@click=${() => activeTab(tab.index)}
|
|
532
|
-
>
|
|
533
|
-
${tab.title}
|
|
534
|
-
</button>
|
|
535
|
-
`)}
|
|
536
|
-
</div>
|
|
537
|
-
|
|
538
|
-
<div class="tab-panels">
|
|
539
|
-
${tabs().map(tab => html`
|
|
540
|
-
<div
|
|
541
|
-
class="tab-panel"
|
|
542
|
-
style="display: ${() => activeTab() === tab.index ? 'block' : 'none'}"
|
|
543
|
-
>
|
|
544
|
-
${tab.content}
|
|
545
|
-
</div>
|
|
546
|
-
`)}
|
|
547
|
-
</div>
|
|
548
|
-
</div>
|
|
549
|
-
`;
|
|
550
|
-
}, ['active']);
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
**Usage:**
|
|
554
|
-
```html
|
|
555
|
-
<my-tabs @change=${(e) => console.log('Tab changed:', e.detail)}>
|
|
556
|
-
<div slot="tab">Profile</div>
|
|
557
|
-
<div slot="panel-0">
|
|
558
|
-
<h3>Profile Settings</h3>
|
|
559
|
-
<form>...</form>
|
|
560
|
-
</div>
|
|
561
|
-
|
|
562
|
-
<div slot="tab">Security</div>
|
|
563
|
-
<div slot="panel-1">
|
|
564
|
-
<h3>Security Settings</h3>
|
|
565
|
-
<form>...</form>
|
|
566
|
-
</div>
|
|
567
|
-
|
|
568
|
-
<div slot="tab">Notifications</div>
|
|
569
|
-
<div slot="panel-2">
|
|
570
|
-
<h3>Notification Preferences</h3>
|
|
571
|
-
<form>...</form>
|
|
572
|
-
</div>
|
|
573
|
-
</my-tabs>
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
### Component with External Data
|
|
577
|
-
|
|
578
|
-
```javascript
|
|
579
|
-
// user-profile.js
|
|
580
|
-
$.component('user-profile', (props, { emit, onUnmount }) => {
|
|
581
|
-
const user = $(null);
|
|
582
|
-
const loading = $(false);
|
|
583
|
-
const error = $(null);
|
|
584
|
-
|
|
585
|
-
// Fetch user data when userId changes
|
|
586
|
-
$.effect(() => {
|
|
587
|
-
const userId = props.userId();
|
|
588
|
-
if (!userId) return;
|
|
589
|
-
|
|
590
|
-
loading(true);
|
|
591
|
-
error(null);
|
|
592
|
-
|
|
593
|
-
const controller = new AbortController();
|
|
594
|
-
|
|
595
|
-
fetch(`/api/users/${userId}`, { signal: controller.signal })
|
|
596
|
-
.then(res => res.json())
|
|
597
|
-
.then(data => {
|
|
598
|
-
user(data);
|
|
599
|
-
emit('loaded', data);
|
|
600
|
-
})
|
|
601
|
-
.catch(err => {
|
|
602
|
-
if (err.name !== 'AbortError') {
|
|
603
|
-
error(err.message);
|
|
604
|
-
emit('error', err);
|
|
605
|
-
}
|
|
606
|
-
})
|
|
607
|
-
.finally(() => loading(false));
|
|
608
|
-
|
|
609
|
-
// Cleanup: abort fetch if component unmounts or userId changes
|
|
610
|
-
onUnmount(() => controller.abort());
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
return html`
|
|
614
|
-
<div class="user-profile">
|
|
615
|
-
${() => loading() ? html`
|
|
616
|
-
<div class="spinner">Loading...</div>
|
|
617
|
-
` : error() ? html`
|
|
618
|
-
<div class="error">Error: ${error()}</div>
|
|
619
|
-
` : user() ? html`
|
|
620
|
-
<div class="user-info">
|
|
621
|
-
<img src="${user().avatar}" class="avatar" />
|
|
622
|
-
<h2>${user().name}</h2>
|
|
623
|
-
<p>${user().email}</p>
|
|
624
|
-
<p>Member since: ${new Date(user().joined).toLocaleDateString()}</p>
|
|
625
|
-
</div>
|
|
626
|
-
` : html`
|
|
627
|
-
<div class="no-user">No user selected</div>
|
|
628
|
-
`}
|
|
629
|
-
</div>
|
|
630
|
-
`;
|
|
631
|
-
}, ['user-id']);
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
## 📦 Component Libraries
|
|
635
|
-
|
|
636
|
-
### Building a Reusable Component Library
|
|
637
|
-
|
|
638
|
-
```javascript
|
|
639
|
-
// components/index.js
|
|
640
|
-
import { $, html } from 'sigpro';
|
|
641
|
-
|
|
642
|
-
// Button component
|
|
643
|
-
export const Button = $.component('ui-button', (props, { slot, emit }) => {
|
|
644
|
-
const variant = props.variant() || 'primary';
|
|
645
|
-
const size = props.size() || 'md';
|
|
646
|
-
|
|
647
|
-
const sizes = {
|
|
648
|
-
sm: 'px-2 py-1 text-sm',
|
|
649
|
-
md: 'px-4 py-2',
|
|
650
|
-
lg: 'px-6 py-3 text-lg'
|
|
651
|
-
};
|
|
652
|
-
|
|
653
|
-
const variants = {
|
|
654
|
-
primary: 'bg-blue-500 hover:bg-blue-600 text-white',
|
|
655
|
-
secondary: 'bg-gray-500 hover:bg-gray-600 text-white',
|
|
656
|
-
danger: 'bg-red-500 hover:bg-red-600 text-white'
|
|
657
|
-
};
|
|
658
|
-
|
|
659
|
-
return html`
|
|
660
|
-
<button
|
|
661
|
-
class="rounded font-semibold transition-colors ${sizes[size]} ${variants[variant]}"
|
|
662
|
-
?disabled=${props.disabled}
|
|
663
|
-
@click=${() => emit('click')}
|
|
664
|
-
>
|
|
665
|
-
${slot()}
|
|
666
|
-
</button>
|
|
667
|
-
`;
|
|
668
|
-
}, ['variant', 'size', 'disabled']);
|
|
669
|
-
|
|
670
|
-
// Card component
|
|
671
|
-
export const Card = $.component('ui-card', (props, { slot }) => {
|
|
672
|
-
return html`
|
|
673
|
-
<div class="card border rounded-lg shadow-sm overflow-hidden">
|
|
674
|
-
${props.title() ? html`
|
|
675
|
-
<div class="card-header bg-gray-50 px-4 py-3 border-b">
|
|
676
|
-
<h3 class="font-semibold">${props.title()}</h3>
|
|
677
|
-
</div>
|
|
678
|
-
` : ''}
|
|
679
|
-
|
|
680
|
-
<div class="card-body p-4">
|
|
681
|
-
${slot()}
|
|
682
|
-
</div>
|
|
683
|
-
|
|
684
|
-
${props.footer() ? html`
|
|
685
|
-
<div class="card-footer bg-gray-50 px-4 py-3 border-t">
|
|
686
|
-
${slot('footer')}
|
|
687
|
-
</div>
|
|
688
|
-
` : ''}
|
|
689
|
-
</div>
|
|
690
|
-
`;
|
|
691
|
-
}, ['title']);
|
|
692
|
-
|
|
693
|
-
// Badge component
|
|
694
|
-
export const Badge = $.component('ui-badge', (props, { slot }) => {
|
|
695
|
-
const type = props.type() || 'default';
|
|
696
|
-
|
|
697
|
-
const types = {
|
|
698
|
-
default: 'bg-gray-100 text-gray-800',
|
|
699
|
-
success: 'bg-green-100 text-green-800',
|
|
700
|
-
warning: 'bg-yellow-100 text-yellow-800',
|
|
701
|
-
error: 'bg-red-100 text-red-800',
|
|
702
|
-
info: 'bg-blue-100 text-blue-800'
|
|
703
|
-
};
|
|
704
|
-
|
|
705
|
-
return html`
|
|
706
|
-
<span class="inline-block px-2 py-1 text-xs font-semibold rounded ${types[type]}">
|
|
707
|
-
${slot()}
|
|
708
|
-
</span>
|
|
709
|
-
`;
|
|
710
|
-
}, ['type']);
|
|
711
|
-
|
|
712
|
-
export { $, html };
|
|
713
|
-
```
|
|
714
|
-
|
|
715
|
-
**Usage:**
|
|
716
|
-
```javascript
|
|
717
|
-
import { Button, Card, Badge } from './components/index.js';
|
|
718
|
-
|
|
719
|
-
// Use components anywhere
|
|
720
|
-
const app = html`
|
|
721
|
-
<div>
|
|
722
|
-
<Card title="Welcome">
|
|
723
|
-
<p>This is a card component</p>
|
|
724
|
-
<div slot="footer">
|
|
725
|
-
<Button variant="primary" @click=${handleClick}>
|
|
726
|
-
Save Changes
|
|
727
|
-
</Button>
|
|
728
|
-
<Badge type="success">New</Badge>
|
|
729
|
-
</div>
|
|
730
|
-
</Card>
|
|
731
|
-
</div>
|
|
732
|
-
`;
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
## 🎯 Decision Guide: Light DOM vs Shadow DOM
|
|
736
|
-
|
|
737
|
-
| Use Light DOM (`false`) when... | Use Shadow DOM (`true`) when... |
|
|
738
|
-
|--------------------------------|-------------------------------|
|
|
739
|
-
| Component is part of your main app | Building a UI library for others |
|
|
740
|
-
| Using global CSS (Tailwind, Bootstrap) | Creating embeddable widgets |
|
|
741
|
-
| Need to inherit theme variables | Styles must be pixel-perfect everywhere |
|
|
742
|
-
| Working with existing design system | Component has complex, specific styles |
|
|
743
|
-
| Quick prototyping | Distributing to different projects |
|
|
744
|
-
| Form elements that should match site | Need style isolation/encapsulation |
|
|
745
|
-
|
|
746
|
-
## 📊 Summary
|
|
747
|
-
|
|
748
|
-
| Feature | Description |
|
|
749
|
-
|---------|-------------|
|
|
750
|
-
| **Native Web Components** | Built on Custom Elements standard |
|
|
751
|
-
| **Reactive Props** | Observed attributes become signals |
|
|
752
|
-
| **Two Rendering Modes** | Light DOM (default) or Shadow DOM |
|
|
753
|
-
| **Automatic Cleanup** | Effects and listeners cleaned up on disconnect |
|
|
754
|
-
| **Event System** | Custom events with `emit()` |
|
|
755
|
-
| **Slot Support** | Full slot API for content projection |
|
|
756
|
-
| **Zero Dependencies** | Pure vanilla JavaScript |
|
|
757
|
-
|
|
758
|
-
---
|
|
759
|
-
|
|
760
|
-
> **Pro Tip:** Start with Light DOM components for app-specific UI, and use Shadow DOM when building components that need to work identically across different projects or websites.
|