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.
@@ -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
+ };