tutuca 0.9.82 → 0.9.84
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 +5 -3
- package/dist/tutuca-cli.js +13259 -12909
- package/dist/tutuca-dev.ext.js +551 -629
- package/dist/tutuca-dev.js +481 -570
- package/dist/tutuca-dev.min.js +2 -2
- package/dist/tutuca-extra.ext.js +333 -432
- package/dist/tutuca-extra.js +266 -365
- package/dist/tutuca-extra.min.js +2 -2
- package/dist/tutuca-storybook.js +283 -0
- package/dist/tutuca.ext.js +274 -280
- package/dist/tutuca.js +253 -259
- package/dist/tutuca.min.js +2 -2
- package/package.json +6 -2
- package/skill/tutuca/SKILL.md +2 -2
- package/skill/tutuca/advanced.md +14 -4
- package/skill/tutuca/cli.md +1 -0
- package/skill/tutuca/component-design.md +1 -1
- package/skill/tutuca/core.md +11 -0
- package/skill/tutuca/patterns/README.md +1 -0
- package/skill/tutuca/patterns/file-input.md +39 -0
- package/skill/tutuca-source/tutuca.ext.js +274 -280
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// src/storybook.js
|
|
2
|
+
import { component, html, injectCss, tutuca } from "tutuca";
|
|
3
|
+
var Storybook = component({
|
|
4
|
+
name: "Storybook",
|
|
5
|
+
fields: {
|
|
6
|
+
selectedSectionIndex: 0,
|
|
7
|
+
sections: [],
|
|
8
|
+
filter: "",
|
|
9
|
+
sectionId: null,
|
|
10
|
+
exampleId: null,
|
|
11
|
+
focusExample: null
|
|
12
|
+
},
|
|
13
|
+
methods: {
|
|
14
|
+
selectSectionAtIndex(index) {
|
|
15
|
+
if (this.sections.size === 0)
|
|
16
|
+
return this;
|
|
17
|
+
const safeIndex = index >= 0 && index < this.sections.size ? index : 0;
|
|
18
|
+
const sections = this.sections.map((s, i) => s.setSelected(i === safeIndex));
|
|
19
|
+
return this.setSelectedSectionIndex(safeIndex).setSections(sections);
|
|
20
|
+
},
|
|
21
|
+
selectSectionWithId(id) {
|
|
22
|
+
if (!id)
|
|
23
|
+
return this.selectSectionAtIndex(this.selectedSectionIndex);
|
|
24
|
+
const index = this.sections.findIndex((s) => s.id === id);
|
|
25
|
+
return this.selectSectionAtIndex(index);
|
|
26
|
+
},
|
|
27
|
+
focusExampleByIds(sectionId, exampleId) {
|
|
28
|
+
if (!sectionId || !exampleId) {
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
const section = this.sections.find((s) => s.id === sectionId);
|
|
32
|
+
const example = section?.items.find((e) => e.id === exampleId);
|
|
33
|
+
if (!example) {
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
return this.setSectionId(sectionId).setExampleId(exampleId).setFocusExample(example.value);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
input: {
|
|
40
|
+
onApplyFilter(value, ctx) {
|
|
41
|
+
ctx.request("persistState", [{ key: "sectionFilter", value }]);
|
|
42
|
+
return this.setFilter(value);
|
|
43
|
+
},
|
|
44
|
+
onClearFilter(ctx) {
|
|
45
|
+
ctx.request("persistState", [{ key: "sectionFilter", value: "" }]);
|
|
46
|
+
return this.resetFilter();
|
|
47
|
+
},
|
|
48
|
+
onFocusClose(ctx) {
|
|
49
|
+
ctx.request("persistState", [{ key: "sectionId", value: "" }]);
|
|
50
|
+
ctx.request("persistState", [{ key: "exampleId", value: "" }]);
|
|
51
|
+
return this.setSectionId(null).setExampleId(null).setFocusExample(null);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
alter: {
|
|
55
|
+
filterSection(_key, section) {
|
|
56
|
+
return this.filter === "" || fuzzyMatch(this.filter, `${section.title} ${section.description}`);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
bubble: {
|
|
60
|
+
sectionSelected(section, ctx) {
|
|
61
|
+
ctx.stopPropagation();
|
|
62
|
+
ctx.request("persistState", [{ key: "section", value: section.id }]);
|
|
63
|
+
return this.selectSectionAtIndex(this.sections.indexOf(section));
|
|
64
|
+
},
|
|
65
|
+
exampleFocusRequested(example, ctx) {
|
|
66
|
+
ctx.stopPropagation();
|
|
67
|
+
const section = this.sections.get(this.selectedSectionIndex);
|
|
68
|
+
const sectionId = section?.id ?? null;
|
|
69
|
+
ctx.request("persistState", [{ key: "sectionId", value: sectionId }]);
|
|
70
|
+
ctx.request("persistState", [{ key: "exampleId", value: example.id }]);
|
|
71
|
+
return this.setSectionId(sectionId).setExampleId(example.id).setFocusExample(example.value);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
view: html`<div>
|
|
75
|
+
<div class="flex flex-col gap-3 p-3 h-screen" @show="truthy? .focusExample">
|
|
76
|
+
<div class="flex justify-end">
|
|
77
|
+
<button class="btn btn-ghost btn-sm" @on.click="onFocusClose">
|
|
78
|
+
close
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="flex-1 overflow-y-auto">
|
|
82
|
+
<x render=".focusExample"></x>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="flex gap-3 p-3 h-screen" @hide="truthy? .focusExample">
|
|
86
|
+
<div
|
|
87
|
+
class="w-1/4 flex flex-col gap-3 bg-base-100 shadow-md h-full overflow-hidden"
|
|
88
|
+
>
|
|
89
|
+
<input
|
|
90
|
+
class="input w-full outline-0 focus:bg-base-200"
|
|
91
|
+
type="search"
|
|
92
|
+
placeholder="Filter sections"
|
|
93
|
+
:value=".filter"
|
|
94
|
+
@on.input="onApplyFilter value"
|
|
95
|
+
@on.keydown.cancel="onClearFilter"
|
|
96
|
+
/>
|
|
97
|
+
<div class="list h-full flex-1 overflow-y-auto">
|
|
98
|
+
<x render-each=".sections" as="listEntry" when="filterSection"></x>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="w-full h-full overflow-y-auto">
|
|
102
|
+
<x render=".sections[.selectedSectionIndex]"></x>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>`
|
|
106
|
+
});
|
|
107
|
+
var Section = component({
|
|
108
|
+
name: "Section",
|
|
109
|
+
fields: {
|
|
110
|
+
id: "?",
|
|
111
|
+
title: "No Title Section",
|
|
112
|
+
description: "",
|
|
113
|
+
items: [],
|
|
114
|
+
filter: "",
|
|
115
|
+
selected: false
|
|
116
|
+
},
|
|
117
|
+
statics: {
|
|
118
|
+
fromData({ id, title = "???", description = "", items = [] }) {
|
|
119
|
+
id ??= slugify(title);
|
|
120
|
+
return this.make({
|
|
121
|
+
id,
|
|
122
|
+
title,
|
|
123
|
+
description,
|
|
124
|
+
items: items.map((v) => Example.Class.fromData(v))
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
alter: {
|
|
129
|
+
filterItem(_key, item) {
|
|
130
|
+
return this.filter === "" || fuzzyMatch(this.filter, `${item.title} ${item.description}`);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
input: {
|
|
134
|
+
onApplyFilter(value, ctx) {
|
|
135
|
+
ctx.request("persistState", [{ key: "exampleFilter", value }]);
|
|
136
|
+
return this.setFilter(value);
|
|
137
|
+
},
|
|
138
|
+
onClearFilter(ctx) {
|
|
139
|
+
ctx.request("persistState", [{ key: "exampleFilter", value: "" }]);
|
|
140
|
+
return this.resetFilter();
|
|
141
|
+
},
|
|
142
|
+
onListItemClick(ctx) {
|
|
143
|
+
ctx.bubble("sectionSelected", [this]);
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
view: html`<section class="flex flex-col gap-3">
|
|
148
|
+
<div class="sticky top-0 z-10 bg-base-100 pt-1 pb-2 shadow-sm">
|
|
149
|
+
<h2 class="text-lg font-bold" @text=".title"></h2>
|
|
150
|
+
<p class="text-md italic opacity-60" @text=".description"></p>
|
|
151
|
+
</div>
|
|
152
|
+
<input
|
|
153
|
+
class="input w-full outline-0 focus:bg-base-200"
|
|
154
|
+
type="search"
|
|
155
|
+
placeholder="Filter examples"
|
|
156
|
+
:value=".filter"
|
|
157
|
+
@on.input="onApplyFilter value"
|
|
158
|
+
@on.keydown.cancel="onClearFilter"
|
|
159
|
+
/>
|
|
160
|
+
<div class="flex flex-col gap-3">
|
|
161
|
+
<x render-each=".items" when="filterItem"></x>
|
|
162
|
+
</div>
|
|
163
|
+
</section>`,
|
|
164
|
+
views: {
|
|
165
|
+
listEntry: html`<div
|
|
166
|
+
@if.class=".selected"
|
|
167
|
+
@then="'list-row cursor-pointer text-blue-400 hover:text-blue-500 font-semibold'"
|
|
168
|
+
@else="'list-row cursor-pointer hover:bg-base-200'"
|
|
169
|
+
:title=".description"
|
|
170
|
+
@on.click="onListItemClick"
|
|
171
|
+
>
|
|
172
|
+
<div @text=".title" class="list-col-grow"></div>
|
|
173
|
+
<p
|
|
174
|
+
class="text-xs opacity-60 list-col-wrap truncate"
|
|
175
|
+
@text=".description"
|
|
176
|
+
></p>
|
|
177
|
+
</div> `
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
var Example = component({
|
|
181
|
+
name: "Example",
|
|
182
|
+
fields: { id: "?", title: "?", description: "", value: null, view: "main" },
|
|
183
|
+
statics: {
|
|
184
|
+
fromData({ id, title = "No Title Example", description = "", value = null, view = "main" }) {
|
|
185
|
+
id ??= slugify(title);
|
|
186
|
+
return this.make({
|
|
187
|
+
id,
|
|
188
|
+
title,
|
|
189
|
+
description,
|
|
190
|
+
value,
|
|
191
|
+
view
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
input: {
|
|
196
|
+
onLogSelected() {
|
|
197
|
+
console.log(this.value);
|
|
198
|
+
return this;
|
|
199
|
+
},
|
|
200
|
+
onFocusSelected(ctx) {
|
|
201
|
+
ctx.bubble("exampleFocusRequested", [this]);
|
|
202
|
+
return this;
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
view: html`<div class="card card-border bg-base-100 shadow-md">
|
|
206
|
+
<div class="card-body">
|
|
207
|
+
<h2 class="card-title flex justify-between">
|
|
208
|
+
<a :href="$'#example-{.id}'" :id="$'example-{.id}'" @text=".title"></a>
|
|
209
|
+
<div class="flex gap-2">
|
|
210
|
+
<button class="btn btn-ghost btn-sm" @on.click="onFocusSelected">
|
|
211
|
+
focus
|
|
212
|
+
</button>
|
|
213
|
+
<button class="btn btn-ghost btn-sm" @on.click="onLogSelected">
|
|
214
|
+
log
|
|
215
|
+
</button>
|
|
216
|
+
</div>
|
|
217
|
+
</h2>
|
|
218
|
+
<p class="text-md italic opacity-60" @text=".description"></p>
|
|
219
|
+
<div class="bg-base-100 p-3" @push-view=".view">
|
|
220
|
+
<x render=".value"></x>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>`
|
|
224
|
+
});
|
|
225
|
+
function fuzzyMatch(query, target) {
|
|
226
|
+
const q = query.toLowerCase(), t = target.toLowerCase();
|
|
227
|
+
let qi = 0;
|
|
228
|
+
for (let ti = 0;ti < t.length && qi < q.length; ti++) {
|
|
229
|
+
if (t[ti] === q[qi])
|
|
230
|
+
qi++;
|
|
231
|
+
}
|
|
232
|
+
return qi === q.length;
|
|
233
|
+
}
|
|
234
|
+
function slugify(str) {
|
|
235
|
+
return String(str).normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
236
|
+
}
|
|
237
|
+
function buildStorybook(modules) {
|
|
238
|
+
const sections = modules.map((m) => Section.Class.fromData(m.getExamples())).sort((a, b) => a.title.localeCompare(b.title));
|
|
239
|
+
const components = new Set([Storybook, Section, Example]);
|
|
240
|
+
const macros = {};
|
|
241
|
+
const requestHandlers = {};
|
|
242
|
+
for (const m of modules) {
|
|
243
|
+
for (const c of m.getComponents?.() ?? []) {
|
|
244
|
+
components.add(c);
|
|
245
|
+
}
|
|
246
|
+
if (m.getMacros)
|
|
247
|
+
Object.assign(macros, m.getMacros());
|
|
248
|
+
if (m.getRequestHandlers)
|
|
249
|
+
Object.assign(requestHandlers, m.getRequestHandlers());
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
root: Storybook.make({ sections }),
|
|
253
|
+
components: [...components],
|
|
254
|
+
macros,
|
|
255
|
+
requestHandlers
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
async function mountStorybook(selector, modules, { compileCss, root } = {}) {
|
|
259
|
+
const app = tutuca(selector);
|
|
260
|
+
const built = buildStorybook(modules);
|
|
261
|
+
app.state.set(root ?? built.root);
|
|
262
|
+
const scope = app.registerComponents(built.components);
|
|
263
|
+
scope.registerMacros(built.macros);
|
|
264
|
+
scope.registerRequestHandlers(built.requestHandlers);
|
|
265
|
+
if (compileCss) {
|
|
266
|
+
injectCss("tutuca-storybook", await compileCss(app));
|
|
267
|
+
}
|
|
268
|
+
app.start();
|
|
269
|
+
return app;
|
|
270
|
+
}
|
|
271
|
+
function getComponents() {
|
|
272
|
+
return [Storybook, Section, Example];
|
|
273
|
+
}
|
|
274
|
+
export {
|
|
275
|
+
slugify,
|
|
276
|
+
mountStorybook,
|
|
277
|
+
getComponents,
|
|
278
|
+
fuzzyMatch,
|
|
279
|
+
buildStorybook,
|
|
280
|
+
Storybook,
|
|
281
|
+
Section,
|
|
282
|
+
Example
|
|
283
|
+
};
|