tutuca 0.11.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tutuca-cli.js +130 -104
- package/dist/tutuca-dev.ext.js +121 -90
- package/dist/tutuca-dev.js +121 -90
- package/dist/tutuca-dev.min.js +3 -3
- package/dist/tutuca-extra.ext.js +116 -59
- package/dist/tutuca-extra.js +116 -59
- package/dist/tutuca-extra.min.js +2 -2
- package/dist/tutuca-storybook.js +3 -3
- package/dist/tutuca.ext.js +116 -59
- package/dist/tutuca.js +116 -59
- package/dist/tutuca.min.js +2 -2
- package/package.json +1 -1
- package/skill/margaui/SKILL.md +105 -0
- package/skill/margaui/components/accordion.md +127 -0
- package/skill/margaui/components/alert.md +174 -0
- package/skill/margaui/components/aura.md +97 -0
- package/skill/margaui/components/avatar.md +220 -0
- package/skill/margaui/components/badge.md +193 -0
- package/skill/margaui/components/breadcrumbs.md +103 -0
- package/skill/margaui/components/button.md +322 -0
- package/skill/margaui/components/calendar.md +67 -0
- package/skill/margaui/components/card.md +373 -0
- package/skill/margaui/components/carousel.md +387 -0
- package/skill/margaui/components/chat.md +171 -0
- package/skill/margaui/components/checkbox.md +101 -0
- package/skill/margaui/components/collapse.md +172 -0
- package/skill/margaui/components/countdown.md +165 -0
- package/skill/margaui/components/diff.md +53 -0
- package/skill/margaui/components/divider.md +107 -0
- package/skill/margaui/components/dock.md +173 -0
- package/skill/margaui/components/drawer.md +184 -0
- package/skill/margaui/components/dropdown.md +388 -0
- package/skill/margaui/components/fab.md +346 -0
- package/skill/margaui/components/fieldset.md +88 -0
- package/skill/margaui/components/file-input.md +84 -0
- package/skill/margaui/components/filter.md +52 -0
- package/skill/margaui/components/footer.md +583 -0
- package/skill/margaui/components/hero.md +135 -0
- package/skill/margaui/components/hover-3d.md +129 -0
- package/skill/margaui/components/hover-gallery.md +49 -0
- package/skill/margaui/components/indicator.md +265 -0
- package/skill/margaui/components/input.md +389 -0
- package/skill/margaui/components/join.md +100 -0
- package/skill/margaui/components/kbd.md +127 -0
- package/skill/margaui/components/label.md +102 -0
- package/skill/margaui/components/link.md +96 -0
- package/skill/margaui/components/list.md +182 -0
- package/skill/margaui/components/loading.md +105 -0
- package/skill/margaui/components/mask.md +168 -0
- package/skill/margaui/components/megamenu.md +131 -0
- package/skill/margaui/components/menu.md +887 -0
- package/skill/margaui/components/mockup-browser.md +39 -0
- package/skill/margaui/components/mockup-code.md +81 -0
- package/skill/margaui/components/mockup-phone.md +39 -0
- package/skill/margaui/components/mockup-window.md +33 -0
- package/skill/margaui/components/modal.md +196 -0
- package/skill/margaui/components/navbar.md +282 -0
- package/skill/margaui/components/otp.md +171 -0
- package/skill/margaui/components/pagination.md +122 -0
- package/skill/margaui/components/progress.md +135 -0
- package/skill/margaui/components/radial-progress.md +67 -0
- package/skill/margaui/components/radio.md +133 -0
- package/skill/margaui/components/range.md +134 -0
- package/skill/margaui/components/rating.md +170 -0
- package/skill/margaui/components/select.md +225 -0
- package/skill/margaui/components/skeleton.md +64 -0
- package/skill/margaui/components/stack.md +142 -0
- package/skill/margaui/components/stat.md +254 -0
- package/skill/margaui/components/status.md +73 -0
- package/skill/margaui/components/steps.md +138 -0
- package/skill/margaui/components/swap.md +152 -0
- package/skill/margaui/components/tab.md +248 -0
- package/skill/margaui/components/table.md +1018 -0
- package/skill/margaui/components/text-rotate.md +91 -0
- package/skill/margaui/components/textarea.md +85 -0
- package/skill/margaui/components/theme-controller.md +266 -0
- package/skill/margaui/components/timeline.md +1356 -0
- package/skill/margaui/components/toast.md +165 -0
- package/skill/margaui/components/toggle.md +135 -0
- package/skill/margaui/components/tooltip.md +181 -0
- package/skill/margaui/components/validator.md +163 -0
- package/skill/tutuca/SKILL.md +56 -0
- package/skill/tutuca/advanced.md +212 -0
- package/skill/tutuca/cli.md +239 -0
- package/skill/tutuca/component-design.md +168 -0
- package/skill/tutuca/core.md +918 -0
- package/skill/tutuca/iteration.md +207 -0
- package/skill/tutuca/macros.md +86 -0
- package/skill/tutuca/margaui.md +175 -0
- package/skill/tutuca/messages-and-intents.md +399 -0
- package/skill/tutuca/patterns/README.md +48 -0
- package/skill/tutuca/patterns/add-a-story.md +26 -0
- package/skill/tutuca/patterns/bind-text-and-attributes.md +30 -0
- package/skill/tutuca/patterns/conditional-attribute-value.md +29 -0
- package/skill/tutuca/patterns/coordinate-components.md +54 -0
- package/skill/tutuca/patterns/edit-through-a-dynamic-target.md +27 -0
- package/skill/tutuca/patterns/enrich-each-item.md +25 -0
- package/skill/tutuca/patterns/file-input.md +39 -0
- package/skill/tutuca/patterns/filter-a-list.md +25 -0
- package/skill/tutuca/patterns/filter-and-paginate.md +60 -0
- package/skill/tutuca/patterns/handle-events.md +44 -0
- package/skill/tutuca/patterns/iterate-a-list.md +18 -0
- package/skill/tutuca/patterns/paginate-a-list.md +29 -0
- package/skill/tutuca/patterns/render-a-child-component.md +21 -0
- package/skill/tutuca/patterns/reuse-markup-with-macros.md +36 -0
- package/skill/tutuca/patterns/share-state-across-the-tree.md +38 -0
- package/skill/tutuca/patterns/show-or-hide-content.md +23 -0
- package/skill/tutuca/patterns/switch-between-views.md +30 -0
- package/skill/tutuca/patterns/tabbed-interface.md +43 -0
- package/skill/tutuca/semantics.md +195 -0
- package/skill/tutuca/storybook.md +270 -0
- package/skill/tutuca/styles.md +48 -0
- package/skill/tutuca/testing.md +345 -0
- package/skill/tutuca-source/SKILL.md +33 -0
- package/skill/tutuca-source/tutuca.ext.js +4301 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Tutuca — List Iteration & Enrichment
|
|
2
|
+
|
|
3
|
+
Read this file when a view iterates a sequence (`@each`,
|
|
4
|
+
`render-each`), filters (`@when`), enriches items or scopes
|
|
5
|
+
(`@enrich-with`), or paginates (`@loop-with`).
|
|
6
|
+
|
|
7
|
+
## List Iteration
|
|
8
|
+
|
|
9
|
+
`@each` accepts: `.field`, `*dynamic`.
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<!-- iterate plain values -->
|
|
13
|
+
<li @each=".items"><span @text="@key"></span>: <x text="@value"></x></li>
|
|
14
|
+
|
|
15
|
+
<!-- filter -->
|
|
16
|
+
<li @each=".items" @when="filterItem">...</li>
|
|
17
|
+
|
|
18
|
+
<!-- per-item enrichment via the enrich handler (binds.X => @X in template) -->
|
|
19
|
+
<li @each=".items" @enrich-with="enrichItem">
|
|
20
|
+
<x text="@count"></x>
|
|
21
|
+
</li>
|
|
22
|
+
|
|
23
|
+
<!-- shared per-loop data + slicing (computed once before iteration) -->
|
|
24
|
+
<li @each=".items" @loop-with="getIterData" @when="filterItem">...</li>
|
|
25
|
+
|
|
26
|
+
<!-- render a list of components -->
|
|
27
|
+
<x render-each=".items"></x>
|
|
28
|
+
<x render-each=".items" as="edit"></x> <!-- specific view -->
|
|
29
|
+
<x render-each=".items" @when="filterItem"></x> <!-- with filter -->
|
|
30
|
+
<x render-each=".items" @loop-with="getIterData" @when="filterItem"></x>
|
|
31
|
+
<x render-each=".items" @show=".isOpen"></x> <!-- wrap in show -->
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Directives carry the `@` prefix everywhere — on `<li @each>` / `<div @each>`
|
|
35
|
+
host-element loops and on `<x render-each>` alike. Only `as=` is bare, because
|
|
36
|
+
it is an argument to the op rather than a directive. Both forms share the
|
|
37
|
+
handler-name resolution rules below.
|
|
38
|
+
|
|
39
|
+
`@enrich-with` is **not** supported on `<x render-each>`: the op renders
|
|
40
|
+
each item as a component in its own frame and drops child content, so
|
|
41
|
+
nothing is left to read the `@X` binds an enricher would set. Reach for a
|
|
42
|
+
host-element `@each` loop when you need enrichment.
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
alter: {
|
|
46
|
+
filterItem(_key, item, iterData) { return item.includes(iterData.q); },
|
|
47
|
+
enrichItem(binds, _key, item, iterData) { binds.count = item.length; },
|
|
48
|
+
// `@loop-with` is `(seq, ctx)` and returns { iterData?, start?, end?, keys? }.
|
|
49
|
+
getIterData(seq, ctx) {
|
|
50
|
+
const start = this.page * this.pageSize;
|
|
51
|
+
return { iterData: { q: this.query.toLowerCase() }, start, end: start + this.pageSize };
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### `@loop-with` return shape — `iterData` + slicing
|
|
57
|
+
|
|
58
|
+
A `@loop-with` handler returns an object with up to four optional keys:
|
|
59
|
+
|
|
60
|
+
- **`iterData`** — the shared per-loop value handed to `@when` /
|
|
61
|
+
`@enrich-with`. Defaults to `{ seq }` when omitted. Inside the loop a
|
|
62
|
+
binding may read one **binding member** directly (`@value.title`) — if
|
|
63
|
+
an enrich handler only copies members of the loop value, the linter
|
|
64
|
+
hints to drop it and read the members instead.
|
|
65
|
+
- **`start`, `end`** — a positional slice of the iteration, with
|
|
66
|
+
`Array.prototype.slice` semantics: `end` is exclusive, negatives count
|
|
67
|
+
from the end (`end: -3` drops the last 3), `undefined` means the
|
|
68
|
+
natural bound. Use this to **paginate** — skip a prefix and/or suffix
|
|
69
|
+
without iterating or rendering it.
|
|
70
|
+
- **`keys`** — an explicit, ordered array of **original keys** to visit,
|
|
71
|
+
for **filter-then-paginate**. The handler filters/sorts/slices the full
|
|
72
|
+
sequence itself and returns the current page's slice of original keys;
|
|
73
|
+
the renderer visits exactly those (`seq.get(key)`), in order. Takes
|
|
74
|
+
precedence over `start`/`end` when both are present.
|
|
75
|
+
|
|
76
|
+
Slicing is positional but **preserves each item's original key**: a List
|
|
77
|
+
sliced to `start: 2` still binds `@key` to `2, 3, …`, so events, drag,
|
|
78
|
+
and two-way binding keep their identity. With `start`/`end`, `@when` then
|
|
79
|
+
filters *within* the window, so a page may yield fewer than `end - start`
|
|
80
|
+
items — to filter *before* paging (so the page count reflects the filtered
|
|
81
|
+
total), return `keys` instead. `keys` are original keys, so identity is
|
|
82
|
+
preserved there too: editing or deleting a row on page 2 of a filtered view
|
|
83
|
+
hits the right item. A `keys` return is **authoritative** — the renderer
|
|
84
|
+
visits exactly those keys and does **not** re-apply `@when` (the handler has
|
|
85
|
+
already decided what renders).
|
|
86
|
+
|
|
87
|
+
### `@loop-with` handler context — `(seq, ctx)`
|
|
88
|
+
|
|
89
|
+
The handler's second argument is `ctx = { lookup, filter }` (an object so it
|
|
90
|
+
can grow):
|
|
91
|
+
|
|
92
|
+
- **`ctx.lookup(name)`** — reads a scope `@`-binding, e.g. one published by an
|
|
93
|
+
ancestor scope `@enrich-with`. Lets the handler **reuse a value the enrich
|
|
94
|
+
already computed** instead of recomputing it.
|
|
95
|
+
- **`ctx.filter(key, value, iterData)`** — wraps the declared `@when` predicate
|
|
96
|
+
(always callable; a no-op that returns `true` when there is no `@when`). Lets
|
|
97
|
+
the handler apply the *declared* filter while building its `keys` slice,
|
|
98
|
+
rather than re-implementing the match test.
|
|
99
|
+
|
|
100
|
+
### Lifecycle of `@each`
|
|
101
|
+
|
|
102
|
+
For each render of an element with `@each=".items"`:
|
|
103
|
+
|
|
104
|
+
1. **Resolve sequence** — evaluate `.items`. Native Arrays and Maps, plus any
|
|
105
|
+
class declaring a `SEQ_INFO` walker, are recognized.
|
|
106
|
+
2. **`@loop-with`** (once per render) — `getIterData.call(this, seq, ctx)`
|
|
107
|
+
is called with the full sequence and the `{ lookup, filter }` context;
|
|
108
|
+
its `iterData` becomes the shared per-loop value and its `start`/`end`
|
|
109
|
+
slice the iteration. Skipped if no `@loop-with`; then `iterData` is
|
|
110
|
+
`{ seq }` and the whole sequence is iterated. If it returns `keys`,
|
|
111
|
+
those exact keys are visited in order (filter-then-paginate) and
|
|
112
|
+
`start`/`end` are ignored.
|
|
113
|
+
3. For each `(key, value)` pair in the sliced sequence (or each `key` in
|
|
114
|
+
`keys`):
|
|
115
|
+
1. **`@when`** — `filterItem.call(this, key, value, iterData)`; if it
|
|
116
|
+
returns `false`, the item is skipped. **Not applied** when the
|
|
117
|
+
handler returned `keys` (those are authoritative).
|
|
118
|
+
2. **`@enrich-with`** — `enrichItem.call(this, binds, key, value, iterData)`.
|
|
119
|
+
`binds` is a **mutable object** seeded with `{ key, value }`;
|
|
120
|
+
mutating it (`binds.count = ...`) creates `@`-prefixed bindings
|
|
121
|
+
available in the templated children. The return value is ignored.
|
|
122
|
+
3. **Render** the element with the new bindings on the stack.
|
|
123
|
+
|
|
124
|
+
Auto-bound names inside the loop are always `@key` and `@value` (or
|
|
125
|
+
whatever you wrote into `binds`).
|
|
126
|
+
|
|
127
|
+
### Handler resolution
|
|
128
|
+
|
|
129
|
+
`@when` / `@enrich-with` / `@loop-with` resolve like event handler names:
|
|
130
|
+
bare `filterItem` → `alter.filterItem` (idiomatic); `$filterItem` →
|
|
131
|
+
method on `this` (works, not idiomatic — `alter` keeps iteration helpers
|
|
132
|
+
grouped).
|
|
133
|
+
|
|
134
|
+
## Scope Enrichment
|
|
135
|
+
|
|
136
|
+
Without an `@each` on the same element, `@enrich-with` becomes a scope
|
|
137
|
+
enricher: it takes no `binds` arg, and its **return value** is the
|
|
138
|
+
bindings object whose keys become `@`-prefixed bindings for descendants.
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
alter: { enrichScope() { return { len: this.text.length }; } }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```html
|
|
145
|
+
<div @enrich-with="enrichScope">Length: <x text="@len"></x></div>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Filter-then-paginate strategies
|
|
149
|
+
|
|
150
|
+
The recipe form is in
|
|
151
|
+
[patterns/filter-and-paginate.md](./patterns/filter-and-paginate.md).
|
|
152
|
+
There are three ways to wire it, trading simplicity for scans-per-render
|
|
153
|
+
(all return `keys`, so all keep identity):
|
|
154
|
+
|
|
155
|
+
**1. Naive — two independent scans.** The loop scans + slices the whole
|
|
156
|
+
list itself; a separate `@enrich-with` scans again for the pager labels.
|
|
157
|
+
Simplest, nothing shared:
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
naiveTablePage(seq, { filter }) { // builds the WHOLE matching list…
|
|
161
|
+
const all = [];
|
|
162
|
+
for (let i = 0; i < seq.size; i++) if (filter(i, seq.get(i))) all.push(i);
|
|
163
|
+
const start = clamp(this.page, all.length, this.pageSize).currentPage * this.pageSize;
|
|
164
|
+
return { keys: all.slice(start, start + this.pageSize) }; // …just to slice it
|
|
165
|
+
},
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**2. Shared — one count + one partial collect** (the recipe's default).
|
|
169
|
+
A scope `@enrich-with` on an ancestor does **one** counting scan and
|
|
170
|
+
publishes the clamped page + pager labels (which the page controls,
|
|
171
|
+
sitting outside the loop, read as `@`-bindings); the `@loop-with` handler
|
|
172
|
+
reads the clamped page via `ctx.lookup`, reuses the predicate via
|
|
173
|
+
`ctx.filter`, and collects only the current page's keys — early-exiting
|
|
174
|
+
once the page is full.
|
|
175
|
+
|
|
176
|
+
**3. Coupled — one scan.** The enrich does *everything*, including the
|
|
177
|
+
page keys, and stashes them in a binding only the loop reads. Fastest,
|
|
178
|
+
but the two handlers are welded together — name them so it shows:
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
enrichBuildsKeysForTheLoopBelow() { // the only scan: count + labels + keys
|
|
182
|
+
const all = []; /* …collect matching indices… */
|
|
183
|
+
const { pageCount, currentPage } = clamp(this.page, all.length, this.pageSize);
|
|
184
|
+
const start = currentPage * this.pageSize;
|
|
185
|
+
return { __keys__: all.slice(start, start + this.pageSize), /* …labels… */ };
|
|
186
|
+
},
|
|
187
|
+
loopJustForwardsTheEnrichsKeys(_seq, { lookup }) { // useless without the enrich
|
|
188
|
+
return { keys: lookup("__keys__") };
|
|
189
|
+
},
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Test any strategy with `collectIterBindings(Comp, instance, seq, opts)`,
|
|
193
|
+
which drives a loop exactly like the renderer — map `when` → `@when`,
|
|
194
|
+
`loopWith` → `@loop-with`, `scopeEnrich` → the ancestor scope
|
|
195
|
+
`@enrich-with` the loop reads via `ctx.lookup`. Mechanics and the
|
|
196
|
+
dev-build caveat in [testing.md](./testing.md).
|
|
197
|
+
|
|
198
|
+
## See also
|
|
199
|
+
|
|
200
|
+
- [patterns/iterate-a-list.md](./patterns/iterate-a-list.md),
|
|
201
|
+
[patterns/filter-a-list.md](./patterns/filter-a-list.md),
|
|
202
|
+
[patterns/paginate-a-list.md](./patterns/paginate-a-list.md),
|
|
203
|
+
[patterns/enrich-each-item.md](./patterns/enrich-each-item.md) — minimal
|
|
204
|
+
recipes for each half.
|
|
205
|
+
- [core.md](./core.md) — the component primer, notation, and the
|
|
206
|
+
frame/scope stack model these directives build on.
|
|
207
|
+
- [advanced.md](./advanced.md) — custom seq types (`SEQ_INFO`).
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Tutuca — Macros
|
|
2
|
+
|
|
3
|
+
Macros are pure template expansion — no state, no methods. Calls inside
|
|
4
|
+
a macro resolve against the *host* component. Read this file when
|
|
5
|
+
authoring `macro({...}, html)` definitions, `<x:name>` calls, or slots.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { macro, html } from "tutuca";
|
|
9
|
+
|
|
10
|
+
const badge = macro(
|
|
11
|
+
{ label: "'New'", kind: "'info'" }, // defaults are *expressions*
|
|
12
|
+
html`<span :class="$'badge badge-{^kind}'" @text="^label"></span>`,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export function getMacros() {
|
|
16
|
+
return { badge };
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<x:badge></x:badge> <!-- defaults -->
|
|
22
|
+
<x:badge label="Sale"></x:badge> <!-- static string (no quotes needed) -->
|
|
23
|
+
<x:badge :label="'Sale'"></x:badge> <!-- dynamic literal -->
|
|
24
|
+
<x:badge :label=".status"></x:badge> <!-- field reference -->
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Inside the macro body, `^param` reads a parameter. Static attributes
|
|
28
|
+
(`label="Sale"`) pass the raw string; dynamic attributes (`:label=…`)
|
|
29
|
+
take the same value forms as any binding — see *Quoting & String
|
|
30
|
+
Literals* in [core.md](./core.md) for the literal-vs-template rules.
|
|
31
|
+
|
|
32
|
+
Register macros at the same scope as components:
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
const scope = app.registerComponents([Comp]);
|
|
36
|
+
scope.registerMacros(getMacros());
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Registry keys are lowercased on insert because the HTML parser already
|
|
40
|
+
lowercases `<x:Tag>` to `<x:tag>`. `{ Card }` and `{ card }` both register
|
|
41
|
+
under `card`; registering two *different* macros under the same lowercased
|
|
42
|
+
name warns via `console.assert`.
|
|
43
|
+
|
|
44
|
+
## Slots
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
const card = macro(
|
|
48
|
+
{ title: "'Card'" },
|
|
49
|
+
html`<div class="card">
|
|
50
|
+
<h2 @text="^title"></h2>
|
|
51
|
+
<x:slot></x:slot>
|
|
52
|
+
</div>`,
|
|
53
|
+
);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```html
|
|
57
|
+
<x:card title="Hi"><p>body</p></x:card> <!-- default slot -->
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Named Slots
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
const panel = macro(
|
|
64
|
+
{},
|
|
65
|
+
html`<div>
|
|
66
|
+
<header><x:slot name="actions"></x:slot></header>
|
|
67
|
+
<main><x:slot></x:slot></main> <!-- default == name="_" -->
|
|
68
|
+
<footer><x:slot name="footer"></x:slot></footer>
|
|
69
|
+
</div>`,
|
|
70
|
+
);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<x:panel>
|
|
75
|
+
<x slot="actions"><button @on.click="inc">+</button></x>
|
|
76
|
+
<p>default slot content</p>
|
|
77
|
+
<x slot="footer">© 2026</x>
|
|
78
|
+
</x:panel>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## See also
|
|
82
|
+
|
|
83
|
+
- [patterns/reuse-markup-with-macros.md](./patterns/reuse-markup-with-macros.md) —
|
|
84
|
+
the minimal recipe form of the badge example.
|
|
85
|
+
- [core.md](./core.md) — notation, quoting rules, and the component
|
|
86
|
+
primer the macro body plugs into.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Tutuca — MargaUI Styling
|
|
2
|
+
|
|
3
|
+
Reach this file to add **MargaUI** (the Tailwind v4 / daisyUI-compatible
|
|
4
|
+
class library) styling to a tutuca app: get margaui into the project,
|
|
5
|
+
link its theme, and let tutuca's extra build compile the utility classes
|
|
6
|
+
it finds in your views into CSS. If you only need scoped/global component
|
|
7
|
+
CSS, [styles.md](./styles.md) is enough.
|
|
8
|
+
|
|
9
|
+
## Get margaui
|
|
10
|
+
|
|
11
|
+
margaui ships two pieces: a `compile` function (class names → CSS text)
|
|
12
|
+
and a `theme.css` stylesheet. Pick one of three ways to obtain them.
|
|
13
|
+
|
|
14
|
+
### CDN (no install)
|
|
15
|
+
|
|
16
|
+
Nothing to install — import from jsDelivr and link the theme. tutuca's
|
|
17
|
+
extra build is on the CDN too:
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<link
|
|
21
|
+
rel="stylesheet"
|
|
22
|
+
href="https://marianoguerra.github.io/margaui/themes/theme.css"
|
|
23
|
+
/>
|
|
24
|
+
<script type="module">
|
|
25
|
+
import { compile } from "https://cdn.jsdelivr.net/npm/margaui/+esm";
|
|
26
|
+
import {
|
|
27
|
+
compileClassesToStyleText,
|
|
28
|
+
injectCss,
|
|
29
|
+
tutuca,
|
|
30
|
+
} from "https://cdn.jsdelivr.net/npm/tutuca/dist/tutuca-extra.js/+esm";
|
|
31
|
+
// …wire it up (see below)
|
|
32
|
+
</script>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
See `docs/examples/getting-started-margaui.html` for a complete runnable page.
|
|
36
|
+
|
|
37
|
+
### npm
|
|
38
|
+
|
|
39
|
+
Install both as dev dependencies and import via bare specifiers:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npm i --save-dev tutuca margaui
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
import { compileClassesToStyleText, injectCss, tutuca } from "tutuca/extra";
|
|
47
|
+
import { compile } from "margaui";
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Serve or copy the theme from `node_modules/margaui` into your build, or
|
|
51
|
+
keep linking the GitHub Pages `theme.css` shown above.
|
|
52
|
+
|
|
53
|
+
### Vendoring
|
|
54
|
+
|
|
55
|
+
Copy a prebuilt `margaui.min.js` and a `theme.css` into the project and
|
|
56
|
+
import from the local path — useful for offline builds or pinning an
|
|
57
|
+
exact version (this repo vendors `docs/deps/margaui.min.js` for exactly
|
|
58
|
+
that reason):
|
|
59
|
+
|
|
60
|
+
```html
|
|
61
|
+
<link rel="stylesheet" href="./vendor/theme.css" />
|
|
62
|
+
<script type="module">
|
|
63
|
+
import { compile } from "./vendor/margaui.min.js";
|
|
64
|
+
// …
|
|
65
|
+
</script>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Trade-off: no runtime network dependency and a frozen version, at the
|
|
69
|
+
cost of updating the vendored files by hand.
|
|
70
|
+
|
|
71
|
+
## Dark mode and the other palettes
|
|
72
|
+
|
|
73
|
+
A margaui theme is a block of CSS custom properties (`--color-*`, `--radius-*`,
|
|
74
|
+
…) under a `[data-theme="<name>"]` selector, and every class it compiles reads
|
|
75
|
+
those through `var(--color-*)`. So switching theme is **one attribute flip on
|
|
76
|
+
`<html>`** — the compiled stylesheet never changes:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
document.documentElement.dataset.theme = "dark";
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Two things to know, both of which bite if you assume otherwise:
|
|
83
|
+
|
|
84
|
+
- **Dark mode never turns itself on.** `dark.css` is keyed on
|
|
85
|
+
`[data-theme="dark"]` alone — there is no `prefers-color-scheme` fallback and
|
|
86
|
+
no `.dark` class. Link `theme.css` and do nothing else and the page is light
|
|
87
|
+
forever, on every machine. Following the OS is your job:
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
const dark = matchMedia("(prefers-color-scheme: dark)");
|
|
91
|
+
document.documentElement.dataset.theme = dark.matches ? "dark" : "light";
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- **`theme.css` is only light + dark.** It is literally
|
|
95
|
+
`@import"./light.css";@import"./dark.css";`. margaui ships ~33 more palettes
|
|
96
|
+
(dracula, nord, cyberpunk, …) as sibling files, each linked separately and
|
|
97
|
+
each cheap:
|
|
98
|
+
|
|
99
|
+
```html
|
|
100
|
+
<link rel="stylesheet" href="https://marianoguerra.github.io/margaui/themes/dracula.css" />
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Link it **after** `theme.css`: `light.css` claims plain `:root` as well as
|
|
104
|
+
`[data-theme=light]`, which ties on specificity with `[data-theme=dracula]`,
|
|
105
|
+
so a palette only wins by coming later in the cascade.
|
|
106
|
+
|
|
107
|
+
`tutuca storybook` does all of this for you — see the Themes section of
|
|
108
|
+
[storybook.md](./storybook.md).
|
|
109
|
+
|
|
110
|
+
## Wire it into tutuca
|
|
111
|
+
|
|
112
|
+
However you obtained `compile`, the integration is the same: register
|
|
113
|
+
your components, compile the classes their views reference, inject the
|
|
114
|
+
resulting CSS, then start.
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
import { compileClassesToStyleText, injectCss, tutuca } from "tutuca/extra";
|
|
118
|
+
import { compile } from "margaui"; // or the CDN / vendored path
|
|
119
|
+
|
|
120
|
+
const app = tutuca("#app");
|
|
121
|
+
app.registerComponents([Comp]);
|
|
122
|
+
const css = await compileClassesToStyleText(app, compile);
|
|
123
|
+
injectCss("myapp", css);
|
|
124
|
+
app.start();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`compileClassesToStyleText` walks every registered component's templates,
|
|
128
|
+
collects the `class=` and `:class=` literals, hands them to a `compile`
|
|
129
|
+
function (any margaui-compatible signature), and returns CSS text. Pair
|
|
130
|
+
with `injectCss(scopeName, css)` to install the result before `start()`.
|
|
131
|
+
|
|
132
|
+
When authoring class lists, load the margaui skill alongside this one if
|
|
133
|
+
available (`npx tutuca install-skill --margaui-skill`) — it lists the
|
|
134
|
+
available components and their canonical class strings, which is what the
|
|
135
|
+
`compile` step expects.
|
|
136
|
+
|
|
137
|
+
## Pitfall: assembled class names are invisible to the scanner
|
|
138
|
+
|
|
139
|
+
The scanner only reads **constant** class literals out of parsed templates. It
|
|
140
|
+
cannot see a class name that is assembled rather than written out verbatim, so
|
|
141
|
+
the margaui CSS for that class is never emitted and it renders unstyled. Two
|
|
142
|
+
cases:
|
|
143
|
+
|
|
144
|
+
- **Interpolated templates** — `:class="$'bg-{.color}'"` contributes only the
|
|
145
|
+
constant prefix `bg-`, never `bg-red` / `bg-blue`. Same for any `${…}` segment.
|
|
146
|
+
- **Classes built in a method** — anything a method returns (e.g. a `headerClass()`
|
|
147
|
+
that builds `` `progress-${this.color}` ``) is never scanned at all; the walker
|
|
148
|
+
only reads view templates, not JS bodies.
|
|
149
|
+
|
|
150
|
+
(Literal `@then` / `@else` strings on `@if.class` — e.g.
|
|
151
|
+
`@if.class=".active" @then="'btn-success'" @else="'btn-ghost'"` — **are** now
|
|
152
|
+
collected, so those don't need the workaround.)
|
|
153
|
+
|
|
154
|
+
Workaround: add a hidden "decoy"/palette view on the component that lists every
|
|
155
|
+
possible assembled class as a real literal, so the walker picks them up:
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
// enumerate color × utility so each full class name appears verbatim
|
|
159
|
+
_margauiClasses: html`<p class="bg-red bg-blue progress-red progress-blue"></p>`,
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The view does not need to be rendered anywhere — registration is enough for the
|
|
163
|
+
template walker to find it. (This is the same rule
|
|
164
|
+
[component-design.md](./component-design.md) gives for runtime-assembled margaui
|
|
165
|
+
classes.) The cost is that the palette and the methods can drift apart with no
|
|
166
|
+
check catching it; keep them adjacent and update both together.
|
|
167
|
+
|
|
168
|
+
## See also
|
|
169
|
+
|
|
170
|
+
- [styles.md](./styles.md) — scoped/global component CSS.
|
|
171
|
+
- [advanced.md](./advanced.md) — dynamic bindings, drag & drop, custom
|
|
172
|
+
seq types, and other advanced view features.
|
|
173
|
+
- [cli.md](./cli.md) — `tutuca storybook` wires margaui by default
|
|
174
|
+
(`--no-margaui` to skip); `install-skill --margaui-skill` installs the
|
|
175
|
+
margaui skill.
|