tutuca 0.15.0 → 1.0.0-beta.2

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.
Files changed (170) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/LICENSE +1 -1
  3. package/README.md +64 -170
  4. package/{dist → deps}/immer.js +674 -249
  5. package/dist/tutuca.js +2342 -4057
  6. package/dist/tutuca.min.js +3 -3
  7. package/package.json +78 -96
  8. package/src/component.js +66 -0
  9. package/src/css/classes.js +77 -0
  10. package/src/dev/formatters.js +393 -0
  11. package/src/dev/jsonml.js +35 -0
  12. package/src/dev/panel.js +400 -0
  13. package/src/dev/summary.js +239 -0
  14. package/src/dev/vdom-formatters.js +102 -0
  15. package/src/drag.js +23 -0
  16. package/src/embed/element.js +111 -0
  17. package/src/env.js +30 -0
  18. package/src/events.js +85 -0
  19. package/src/history/bar.js +198 -0
  20. package/src/history/index.js +149 -0
  21. package/src/host.js +360 -0
  22. package/src/lint/index.js +470 -0
  23. package/src/meta.js +12 -0
  24. package/src/optimize.js +58 -0
  25. package/src/paths.js +205 -0
  26. package/src/playground/element.js +360 -0
  27. package/src/playground/index.js +5 -0
  28. package/src/playground/sources.js +107 -0
  29. package/src/playground/ui/playground.js +287 -0
  30. package/src/schema.js +460 -0
  31. package/src/scope.js +92 -0
  32. package/src/storybook/index.js +80 -0
  33. package/src/storybook/runner.js +452 -0
  34. package/src/storybook/stories.js +208 -0
  35. package/src/storybook/stress.js +76 -0
  36. package/src/storybook/ui/common.js +37 -0
  37. package/src/storybook/ui/link.js +80 -0
  38. package/src/storybook/ui/panels.js +32 -0
  39. package/src/storybook/ui/story-panel.js +432 -0
  40. package/src/storybook/ui/storybook.js +191 -0
  41. package/src/storybook/ui/stress-panel.js +231 -0
  42. package/src/sync/automerge.js +159 -0
  43. package/src/sync/codec.js +206 -0
  44. package/src/sync/diff.js +153 -0
  45. package/src/sync/index.js +97 -0
  46. package/src/sync/migrate.js +184 -0
  47. package/src/sync/ops.js +160 -0
  48. package/src/sync/remote.js +206 -0
  49. package/src/tag.js +24 -0
  50. package/src/tasks.js +244 -0
  51. package/src/test/driver.js +141 -0
  52. package/src/test/pbt.js +589 -0
  53. package/src/tutuca.js +66 -0
  54. package/src/vdom.js +349 -0
  55. package/src/view.js +354 -0
  56. package/src/walk.js +79 -0
  57. package/dist/chai.js +0 -4210
  58. package/dist/tutuca-cli.js +0 -16315
  59. package/dist/tutuca-components.js +0 -2232
  60. package/dist/tutuca-dev.ext.js +0 -8476
  61. package/dist/tutuca-dev.js +0 -13220
  62. package/dist/tutuca-dev.min.js +0 -5
  63. package/dist/tutuca-extra.ext.js +0 -4261
  64. package/dist/tutuca-extra.js +0 -4964
  65. package/dist/tutuca-extra.min.js +0 -3
  66. package/dist/tutuca-storybook.js +0 -970
  67. package/dist/tutuca.ext.js +0 -4183
  68. package/skill/margaui/SKILL.md +0 -105
  69. package/skill/margaui/components/accordion.md +0 -127
  70. package/skill/margaui/components/alert.md +0 -174
  71. package/skill/margaui/components/aura.md +0 -97
  72. package/skill/margaui/components/avatar.md +0 -220
  73. package/skill/margaui/components/badge.md +0 -193
  74. package/skill/margaui/components/breadcrumbs.md +0 -103
  75. package/skill/margaui/components/button.md +0 -322
  76. package/skill/margaui/components/calendar.md +0 -67
  77. package/skill/margaui/components/card.md +0 -373
  78. package/skill/margaui/components/carousel.md +0 -387
  79. package/skill/margaui/components/chat.md +0 -171
  80. package/skill/margaui/components/checkbox.md +0 -101
  81. package/skill/margaui/components/collapse.md +0 -172
  82. package/skill/margaui/components/countdown.md +0 -165
  83. package/skill/margaui/components/diff.md +0 -53
  84. package/skill/margaui/components/divider.md +0 -107
  85. package/skill/margaui/components/dock.md +0 -173
  86. package/skill/margaui/components/drawer.md +0 -184
  87. package/skill/margaui/components/dropdown.md +0 -388
  88. package/skill/margaui/components/fab.md +0 -346
  89. package/skill/margaui/components/fieldset.md +0 -88
  90. package/skill/margaui/components/file-input.md +0 -84
  91. package/skill/margaui/components/filter.md +0 -52
  92. package/skill/margaui/components/footer.md +0 -583
  93. package/skill/margaui/components/hero.md +0 -135
  94. package/skill/margaui/components/hover-3d.md +0 -129
  95. package/skill/margaui/components/hover-gallery.md +0 -49
  96. package/skill/margaui/components/indicator.md +0 -265
  97. package/skill/margaui/components/input.md +0 -389
  98. package/skill/margaui/components/join.md +0 -100
  99. package/skill/margaui/components/kbd.md +0 -127
  100. package/skill/margaui/components/label.md +0 -102
  101. package/skill/margaui/components/link.md +0 -96
  102. package/skill/margaui/components/list.md +0 -182
  103. package/skill/margaui/components/loading.md +0 -105
  104. package/skill/margaui/components/mask.md +0 -168
  105. package/skill/margaui/components/megamenu.md +0 -131
  106. package/skill/margaui/components/menu.md +0 -887
  107. package/skill/margaui/components/mockup-browser.md +0 -39
  108. package/skill/margaui/components/mockup-code.md +0 -81
  109. package/skill/margaui/components/mockup-phone.md +0 -39
  110. package/skill/margaui/components/mockup-window.md +0 -33
  111. package/skill/margaui/components/modal.md +0 -196
  112. package/skill/margaui/components/navbar.md +0 -282
  113. package/skill/margaui/components/otp.md +0 -171
  114. package/skill/margaui/components/pagination.md +0 -122
  115. package/skill/margaui/components/progress.md +0 -135
  116. package/skill/margaui/components/radial-progress.md +0 -67
  117. package/skill/margaui/components/radio.md +0 -133
  118. package/skill/margaui/components/range.md +0 -134
  119. package/skill/margaui/components/rating.md +0 -170
  120. package/skill/margaui/components/select.md +0 -225
  121. package/skill/margaui/components/skeleton.md +0 -64
  122. package/skill/margaui/components/stack.md +0 -142
  123. package/skill/margaui/components/stat.md +0 -254
  124. package/skill/margaui/components/status.md +0 -73
  125. package/skill/margaui/components/steps.md +0 -138
  126. package/skill/margaui/components/swap.md +0 -152
  127. package/skill/margaui/components/tab.md +0 -248
  128. package/skill/margaui/components/table.md +0 -1018
  129. package/skill/margaui/components/text-rotate.md +0 -91
  130. package/skill/margaui/components/textarea.md +0 -85
  131. package/skill/margaui/components/theme-controller.md +0 -266
  132. package/skill/margaui/components/timeline.md +0 -1356
  133. package/skill/margaui/components/toast.md +0 -165
  134. package/skill/margaui/components/toggle.md +0 -135
  135. package/skill/margaui/components/tooltip.md +0 -181
  136. package/skill/margaui/components/validator.md +0 -163
  137. package/skill/tutuca/SKILL.md +0 -56
  138. package/skill/tutuca/advanced.md +0 -292
  139. package/skill/tutuca/cli.md +0 -239
  140. package/skill/tutuca/component-design.md +0 -168
  141. package/skill/tutuca/core.md +0 -935
  142. package/skill/tutuca/iteration.md +0 -207
  143. package/skill/tutuca/macros.md +0 -86
  144. package/skill/tutuca/margaui.md +0 -175
  145. package/skill/tutuca/messages-and-intents.md +0 -414
  146. package/skill/tutuca/patterns/README.md +0 -48
  147. package/skill/tutuca/patterns/add-a-story.md +0 -26
  148. package/skill/tutuca/patterns/bind-text-and-attributes.md +0 -30
  149. package/skill/tutuca/patterns/conditional-attribute-value.md +0 -29
  150. package/skill/tutuca/patterns/coordinate-components.md +0 -54
  151. package/skill/tutuca/patterns/edit-through-a-dynamic-target.md +0 -32
  152. package/skill/tutuca/patterns/enrich-each-item.md +0 -25
  153. package/skill/tutuca/patterns/file-input.md +0 -39
  154. package/skill/tutuca/patterns/filter-a-list.md +0 -25
  155. package/skill/tutuca/patterns/filter-and-paginate.md +0 -60
  156. package/skill/tutuca/patterns/handle-events.md +0 -45
  157. package/skill/tutuca/patterns/iterate-a-list.md +0 -18
  158. package/skill/tutuca/patterns/paginate-a-list.md +0 -29
  159. package/skill/tutuca/patterns/render-a-child-component.md +0 -21
  160. package/skill/tutuca/patterns/reuse-markup-with-macros.md +0 -36
  161. package/skill/tutuca/patterns/share-state-across-the-tree.md +0 -53
  162. package/skill/tutuca/patterns/show-or-hide-content.md +0 -23
  163. package/skill/tutuca/patterns/switch-between-views.md +0 -30
  164. package/skill/tutuca/patterns/tabbed-interface.md +0 -43
  165. package/skill/tutuca/semantics.md +0 -219
  166. package/skill/tutuca/storybook.md +0 -270
  167. package/skill/tutuca/styles.md +0 -48
  168. package/skill/tutuca/testing.md +0 -345
  169. package/skill/tutuca-source/SKILL.md +0 -33
  170. package/skill/tutuca-source/tutuca.ext.js +0 -4183
@@ -1,970 +0,0 @@
1
- // src/storybook/components.js
2
- import { component, dispatchPhase, html } from "tutuca";
3
- var Storybook = component({
4
- name: "Storybook",
5
- fields: {
6
- selectedSectionIndex: 0,
7
- sections: [],
8
- // Retained navigation tree (SidebarGroup → SidebarEntry), built once from
9
- // `sections` and then mutated in place: selection flips entry `selected`, the
10
- // filter walk sets entry/group `visible`, a header click flips group `collapsed`.
11
- // It owns its own UI state — never rebuilt or projected from `sections`.
12
- sidebar: [],
13
- filter: "",
14
- sectionId: null,
15
- exampleId: null,
16
- focusExample: null,
17
- sidebarCollapsed: false,
18
- // The active margaui palette, and the names the switcher offers. `themes` is
19
- // seeded by mountStorybook from its `themes` option and left EMPTY when no
20
- // margaui theme CSS is on the page (`--no-margaui`) — an empty list hides the
21
- // switcher, so nothing offers a theme that can't load.
22
- theme: "",
23
- themes: []
24
- },
25
- statics: {
26
- // Build a storybook whose sidebar tree is derived from `sections` once. Use this
27
- // instead of a bare `make` anywhere a Storybook is constructed by hand (the
28
- // engine itself, the getRoot/Inception demo) so the sidebar is never empty.
29
- withSections(sections) {
30
- return this.make({ sections, sidebar: buildSidebar(sections) });
31
- }
32
- },
33
- methods: {
34
- // Assemble the full URL snapshot from current state. `overrides` carries the
35
- // change the calling handler is about to make, since `this` is still the
36
- // pre-change state when the persistState intent is issued.
37
- //
38
- // `theme` is deliberately NOT part of the base snapshot: it is resolved on load
39
- // (from the OS preference when the URL says nothing), and writing that resolved
40
- // value back would stamp `?theme=light` onto the URL the first time anyone types
41
- // in the filter. persistState only touches the keys it is handed, so omitting it
42
- // here leaves an already-chosen `?theme=` alone — onSelectTheme passes it as an
43
- // override, which is the only way it enters the URL.
44
- toUrlState(overrides = {}) {
45
- const section = this.sections[this.selectedSectionIndex];
46
- return {
47
- section: section?.id ?? "",
48
- example: this.exampleId ?? "",
49
- sectionFilter: this.filter ?? "",
50
- exampleFilter: section?.filter ?? "",
51
- ...overrides
52
- };
53
- }
54
- },
55
- receive: {
56
- onApplyFilter(draft, value, ctx) {
57
- ctx.intent("persistState", [this.toUrlState({ sectionFilter: value }), this, false], {
58
- route: ["lex"]
59
- });
60
- draft.filter = value;
61
- applyFilterToSidebar(draft, value);
62
- },
63
- onClearFilter(draft, ctx) {
64
- ctx.intent("persistState", [this.toUrlState({ sectionFilter: "" }), this, false], {
65
- route: ["lex"]
66
- });
67
- draft.filter = "";
68
- applyFilterToSidebar(draft, "");
69
- },
70
- onFocusClose(draft, ctx) {
71
- ctx.intent("persistState", [this.toUrlState({ example: "" }), this, true], {
72
- route: ["lex"]
73
- });
74
- draft.sectionId = null;
75
- draft.exampleId = null;
76
- draft.focusExample = null;
77
- },
78
- // Switching a palette is a DOM effect (set data-theme, load the stylesheet), so it
79
- // goes out as an intent — the host handler in mountStorybook owns the document.
80
- // Unregistered (no `themes` option), the intent no-ops via the 404 path and the
81
- // switcher isn't rendered anyway.
82
- onSelectTheme(draft, value, ctx) {
83
- ctx.intent("applyTheme", [value, this], { route: ["lex"] });
84
- ctx.intent("persistState", [this.toUrlState({ theme: value }), this, false], {
85
- route: ["lex"]
86
- });
87
- draft.theme = value;
88
- },
89
- init(draft, ctx) {
90
- ctx.intent("loadState", [], { route: ["lex"] });
91
- },
92
- // The answer to the `loadState` intent, arriving as an ordinary message. There is
93
- // no `err` to branch on: a failure reaches `loadStateError` instead, and nothing
94
- // that ran out of route reaches here at all.
95
- loadStateOk(draft, state, ctx) {
96
- if (!state) return;
97
- selectSectionWithId(draft, state.section);
98
- draft.theme = state.theme ?? "";
99
- draft.filter = state.sectionFilter ?? "";
100
- applyFilterToSidebar(draft, draft.filter);
101
- setSelectedSectionFilter(draft, state.exampleFilter ?? "");
102
- if (state.example) focusExampleByIds(draft, state.section, state.example);
103
- else {
104
- draft.sectionId = null;
105
- draft.exampleId = null;
106
- draft.focusExample = null;
107
- }
108
- transitionSections(ctx, draft, this.selectedSectionIndex, draft.selectedSectionIndex);
109
- },
110
- toggleSidebarCollapsed(draft) {
111
- draft.sidebarCollapsed = !draft.sidebarCollapsed;
112
- }
113
- },
114
- intent: {
115
- // A sidebar entry was clicked; it bubbles its section id. Resolve to the content
116
- // index and select. Sidebar highlight is updated by selectSectionAtIndex.
117
- sectionSelected(draft, sectionId, ctx) {
118
- ctx.stop();
119
- const section = this.sections.find((s) => s.id === sectionId);
120
- ctx.intent(
121
- "persistState",
122
- [this.toUrlState({ section: sectionId, exampleFilter: section?.filter ?? "" }), this, true],
123
- { route: ["lex"] }
124
- );
125
- const oldIndex = this.selectedSectionIndex;
126
- selectSectionAtIndex(
127
- draft,
128
- this.sections.findIndex((s) => s.id === sectionId)
129
- );
130
- transitionSections(ctx, draft, oldIndex, draft.selectedSectionIndex);
131
- },
132
- // Toggle a group open/closed in place — the collapse lives on the SidebarGroup.
133
- groupToggled(draft, name, ctx) {
134
- ctx.stop();
135
- toggleSidebarGroup(draft, name);
136
- },
137
- exampleFocusRequested(draft, example, ctx) {
138
- ctx.stop();
139
- const section = this.sections[this.selectedSectionIndex];
140
- const sectionId = section?.id ?? null;
141
- ctx.intent("persistState", [this.toUrlState({ example: example.id }), this, true], {
142
- route: ["lex"]
143
- });
144
- draft.sectionId = sectionId;
145
- draft.exampleId = example.id;
146
- draft.focusExample = example.value;
147
- },
148
- exampleFilterChanged(draft, value, ctx) {
149
- ctx.stop();
150
- ctx.intent("persistState", [this.toUrlState({ exampleFilter: value }), this, false], {
151
- route: ["lex"]
152
- });
153
- setSelectedSectionFilter(draft, value);
154
- }
155
- },
156
- alter: {
157
- // The sidebar render-each shows only visible groups (kept positional via `when`
158
- // so a click resolves to the right group — see SidebarGroup.rowVisible).
159
- groupVisible(_key, group) {
160
- return group.visible;
161
- }
162
- },
163
- view: html`<div>
164
- <div class="flex flex-col gap-3 p-3 h-screen" @show="truthy? .focusExample">
165
- <div class="flex justify-end">
166
- <button class="btn btn-ghost btn-sm" @on.click="onFocusClose">
167
- close
168
- </button>
169
- </div>
170
- <div class="flex-1 overflow-y-auto">
171
- <x render=".focusExample"></x>
172
- </div>
173
- </div>
174
- <div class="flex gap-3 p-3 h-screen" @hide="truthy? .focusExample">
175
- <button
176
- class="btn btn-ghost btn-sm self-start"
177
- title="Show sections"
178
- @show=".sidebarCollapsed"
179
- @on.click="toggleSidebarCollapsed"
180
- >
181
- »
182
- </button>
183
- <div
184
- class="w-1/4 flex flex-col gap-3 bg-base-100 shadow-md h-full overflow-hidden"
185
- @hide=".sidebarCollapsed"
186
- >
187
- <div class="flex gap-2 items-center">
188
- <button
189
- class="btn btn-ghost btn-sm"
190
- title="Hide sections"
191
- @on.click="toggleSidebarCollapsed"
192
- >
193
- «
194
- </button>
195
- <input
196
- class="input flex-1 focus:outline-none focus-within:outline-none focus:bg-base-200"
197
- type="search"
198
- placeholder="Filter sections"
199
- :value=".filter"
200
- @on.input="onApplyFilter e.value"
201
- @on.keydown.cancel="onClearFilter"
202
- />
203
- <select
204
- class="select w-auto"
205
- title="Theme"
206
- @hide="empty? .themes"
207
- :value=".theme"
208
- @on.input="onSelectTheme e.value"
209
- >
210
- <option @each=".themes" :value="@value" @text="@value"></option>
211
- </select>
212
- </div>
213
- <div class="list h-full flex-1 overflow-y-auto">
214
- <x render-each=".sidebar" @when="groupVisible"></x>
215
- </div>
216
- </div>
217
- <div class="w-full h-full overflow-y-auto">
218
- <x render=".sections[.selectedSectionIndex]"></x>
219
- </div>
220
- </div>
221
- </div>`
222
- });
223
- var Section = component({
224
- name: "Section",
225
- fields: {
226
- id: "?",
227
- title: "No Title Section",
228
- description: "",
229
- // Optional 2-level grouping: sections sharing a `group` name cluster under one
230
- // collapsible sidebar header. Empty string = ungrouped (top-level, flat — the
231
- // default and the backward-compatible behavior).
232
- group: "",
233
- items: [],
234
- filter: "",
235
- initialized: false
236
- },
237
- statics: {
238
- fromData(raw) {
239
- if (!raw || typeof raw !== "object" || Array.isArray(raw) || raw.title == null) {
240
- throw new Error(
241
- `Section.fromData: expected a section object { title, items }, got ${JSON.stringify(raw)}. getExamples() must return a section object or an array of section objects.`
242
- );
243
- }
244
- const { id, title, description = "", group, items = [] } = raw;
245
- return this.make({
246
- id: id ?? slugify(title),
247
- title,
248
- description,
249
- // `group` is an optional string (single 2-level grouping). A non-string is
250
- // ignored here (rendered ungrouped) rather than coerced; the normalize layer
251
- // (tools/core/module.js) reports it as a shape error.
252
- group: typeof group === "string" ? group : "",
253
- items: items.map((v) => Example.fromData(v))
254
- });
255
- }
256
- },
257
- receive: {
258
- onApplyFilter(draft, value, ctx) {
259
- ctx.intent("exampleFilterChanged", [value], { route: ["dyn"] });
260
- draft.filter = value;
261
- },
262
- onClearFilter(draft, ctx) {
263
- ctx.intent("exampleFilterChanged", [""], { route: ["dyn"] });
264
- draft.filter = this.constructor.getMetaClass().fields.filter.defaultValue;
265
- },
266
- // First display of this section: run each example's `on.init`, mark shown.
267
- init(draft, ctx) {
268
- fanoutLifecycle(ctx, this.items, "init");
269
- draft.initialized = true;
270
- },
271
- resume(draft, ctx) {
272
- fanoutLifecycle(ctx, this.items, "resume");
273
- return this;
274
- },
275
- suspend(draft, ctx) {
276
- fanoutLifecycle(ctx, this.items, "suspend");
277
- return this;
278
- }
279
- },
280
- alter: {
281
- filterItem(_key, item) {
282
- return this.filter === "" || fuzzyMatch(this.filter, `${item.title} ${item.description}`);
283
- }
284
- },
285
- view: html`<section class="flex flex-col gap-3">
286
- <div class="sticky top-0 z-10 bg-base-100 pt-1 pb-2 shadow-sm">
287
- <h2 class="text-lg font-bold" @text=".title"></h2>
288
- <p class="text-md italic opacity-60" @text=".description"></p>
289
- </div>
290
- <input
291
- class="input w-full outline-0 focus:bg-base-200"
292
- type="search"
293
- placeholder="Filter examples"
294
- :value=".filter"
295
- @on.input="onApplyFilter e.value"
296
- @on.keydown.cancel="onClearFilter"
297
- />
298
- <div class="flex flex-col gap-3">
299
- <x render-each=".items" @when="filterItem"></x>
300
- </div>
301
- </section>`
302
- });
303
- var SidebarEntry = component({
304
- name: "SidebarEntry",
305
- fields: { sectionId: "?", title: "", description: "", selected: false, visible: true },
306
- receive: {
307
- onClick(draft, ctx) {
308
- ctx.intent("sectionSelected", [this.sectionId], { route: ["dyn"] });
309
- return this;
310
- }
311
- },
312
- view: html`<div
313
- @if.class=".selected"
314
- @then="'list-row cursor-pointer text-blue-400 hover:text-blue-500 font-semibold'"
315
- @else="'list-row cursor-pointer hover:bg-base-200'"
316
- :title=".description"
317
- @on.click="onClick"
318
- >
319
- <div @text=".title" class="list-col-grow"></div>
320
- <p class="text-xs opacity-60 list-col-wrap truncate" @text=".description"></p>
321
- </div>`
322
- });
323
- var SidebarGroup = component({
324
- name: "SidebarGroup",
325
- fields: { name: "", collapsed: false, visible: true, rows: [] },
326
- receive: {
327
- onToggle(draft, ctx) {
328
- ctx.intent("groupToggled", [this.name], { route: ["dyn"] });
329
- return this;
330
- }
331
- },
332
- // Visibility drives the render-each `when` predicates (not `@show` on the items):
333
- // a `when` filter renders only visible rows while keeping each row's positional key,
334
- // so an event's path resolves to the right row. `@show` on a render-each item would
335
- // shift the rendered set and mis-map the click.
336
- alter: {
337
- rowVisible(_key, row) {
338
- return row.visible;
339
- }
340
- },
341
- view: html`<div class="flex flex-col">
342
- <div
343
- class="list-row cursor-pointer hover:bg-base-200 font-semibold flex items-center gap-1"
344
- @show="truthy? .name"
345
- @on.click="onToggle"
346
- >
347
- <span @show=".collapsed">▸</span>
348
- <span @hide=".collapsed">▾</span>
349
- <span @text=".name" class="list-col-grow"></span>
350
- </div>
351
- <div
352
- @if.class="truthy? .name"
353
- @then="'flex flex-col pl-3'"
354
- @else="'flex flex-col'"
355
- >
356
- <x render-each=".rows" @when="rowVisible"></x>
357
- </div>
358
- </div>`
359
- });
360
- var Example = component({
361
- name: "Example",
362
- fields: {
363
- id: "?",
364
- title: "?",
365
- description: "",
366
- value: null,
367
- view: "main",
368
- intentHandlers: null,
369
- on: null,
370
- // Inspector tabs. activeTab selects which body renders; the *View fields hold
371
- // prebuilt inspector instances (attached by mountStorybook before start, via
372
- // src/storybook/inspect.js); the has* flags gate which tabs appear.
373
- activeTab: "preview",
374
- hasInspect: false,
375
- hasComponent: false,
376
- hasLint: false,
377
- hasTest: false,
378
- componentView: null,
379
- instanceView: null,
380
- lintView: null,
381
- testView: null,
382
- // Live dispatch activity for this example's component (`.value`): an ActivityLog
383
- // (baked empty by src/storybook/inspect.js) the core observer appends to via
384
- // logActivity. `hasActivity` gates the Activity tab so it only appears once
385
- // something has actually happened.
386
- activityLog: null,
387
- hasActivity: false
388
- },
389
- // Storybook-only convention (read in mountStorybook, never by core): names the
390
- // field on an example instance that holds its per-example intent-handler mocks.
391
- intentOverridesField: "intentHandlers",
392
- statics: {
393
- fromData({
394
- id,
395
- title = "No Title Example",
396
- description = "",
397
- value = null,
398
- view = "main",
399
- intentHandlers = null,
400
- on = null
401
- }) {
402
- id ??= slugify(title);
403
- return this.make({
404
- id,
405
- title,
406
- description,
407
- value,
408
- view,
409
- intentHandlers,
410
- on
411
- });
412
- }
413
- },
414
- methods: {
415
- // An `intent` on the `dyn` leg leaves the example and walks into the storybook
416
- // engine's own components. Nothing to warn about: if nothing answers, the walk
417
- // runs out and the example hears `<name>Unhandled`.
418
- runPhase(ctx, phase) {
419
- dispatchPhase(ctx, ctx.at.field("value").buildPath(), phase, this.value);
420
- }
421
- },
422
- // Lifecycle hooks: the section forwards init/resume/suspend here; each runs the
423
- // matching `on` phase's actions against this example's component (`.value`).
424
- receive: {
425
- init(draft, ctx) {
426
- this.runPhase(ctx, this.on?.init);
427
- return this;
428
- },
429
- resume(draft, ctx) {
430
- this.runPhase(ctx, this.on?.resume);
431
- return this;
432
- },
433
- suspend(draft, ctx) {
434
- this.runPhase(ctx, this.on?.suspend);
435
- return this;
436
- },
437
- // Append one activity row to the example's ActivityLog. Dispatched by the storybook
438
- // observer at this example's node — a path OUTSIDE `.value`, so the observer's own
439
- // filter ignores it and this append is never itself logged (no loop).
440
- logActivity(draft, entry) {
441
- draft.activityLog.events.unshift(entry);
442
- if (draft.activityLog.events.length > 50) draft.activityLog.events.length = 50;
443
- draft.activityLog.hasEvents = true;
444
- draft.hasActivity = true;
445
- },
446
- onLogSelected(draft) {
447
- console.log(this.value);
448
- return this;
449
- },
450
- onFocusSelected(draft, ctx) {
451
- ctx.intent("exampleFocusRequested", [this], { route: ["dyn"] });
452
- },
453
- setActiveTab(draft, value) {
454
- draft.activeTab = value;
455
- }
456
- },
457
- view: html`<div class="card card-border bg-base-100 shadow-md">
458
- <div class="card-body">
459
- <h2 class="card-title flex justify-between">
460
- <a :href="$'#example-{.id}'" :id="$'example-{.id}'" @text=".title"></a>
461
- <div class="flex gap-2">
462
- <button class="btn btn-ghost btn-sm" @on.click="onFocusSelected">
463
- focus
464
- </button>
465
- <button class="btn btn-ghost btn-sm" @on.click="onLogSelected">
466
- log
467
- </button>
468
- </div>
469
- </h2>
470
- <p class="text-md italic opacity-60" @text=".description"></p>
471
- <div role="tablist" class="tabs tabs-border" @show=".hasInspect">
472
- <a
473
- role="tab"
474
- @if.class="equals? .activeTab 'preview'"
475
- @then="'tab tab-active'"
476
- @else="'tab'"
477
- @on.click="setActiveTab 'preview'"
478
- >
479
- Preview
480
- </a>
481
- <a
482
- role="tab"
483
- @show=".hasComponent"
484
- @if.class="equals? .activeTab 'component'"
485
- @then="'tab tab-active'"
486
- @else="'tab'"
487
- @on.click="setActiveTab 'component'"
488
- >
489
- Component
490
- </a>
491
- <a
492
- role="tab"
493
- @if.class="equals? .activeTab 'instance'"
494
- @then="'tab tab-active'"
495
- @else="'tab'"
496
- @on.click="setActiveTab 'instance'"
497
- >
498
- Instance
499
- </a>
500
- <a
501
- role="tab"
502
- @show=".hasLint"
503
- @if.class="equals? .activeTab 'lint'"
504
- @then="'tab tab-active'"
505
- @else="'tab'"
506
- @on.click="setActiveTab 'lint'"
507
- >
508
- Lint
509
- </a>
510
- <a
511
- role="tab"
512
- @show=".hasTest"
513
- @if.class="equals? .activeTab 'test'"
514
- @then="'tab tab-active'"
515
- @else="'tab'"
516
- @on.click="setActiveTab 'test'"
517
- >
518
- Test
519
- </a>
520
- <a
521
- role="tab"
522
- @show=".hasActivity"
523
- @if.class="equals? .activeTab 'activity'"
524
- @then="'tab tab-active'"
525
- @else="'tab'"
526
- @on.click="setActiveTab 'activity'"
527
- >
528
- Activity
529
- </a>
530
- </div>
531
- <div @show="equals? .activeTab 'preview'">
532
- <div class="bg-base-100 p-3" @push-view=".view">
533
- <x render=".value"></x>
534
- </div>
535
- </div>
536
- <div class="p-3" @show="equals? .activeTab 'component'">
537
- <x render=".componentView"></x>
538
- </div>
539
- <div class="p-3" @show="equals? .activeTab 'instance'">
540
- <x render=".instanceView"></x>
541
- </div>
542
- <div class="p-3" @show="equals? .activeTab 'lint'">
543
- <x render=".lintView"></x>
544
- </div>
545
- <div class="p-3" @show="equals? .activeTab 'test'">
546
- <x render=".testView"></x>
547
- </div>
548
- <div class="p-3" @show="equals? .activeTab 'activity'">
549
- <x render=".activityLog"></x>
550
- </div>
551
- </div>
552
- </div>`
553
- });
554
- function markSidebarSelected(storybook, id) {
555
- for (const group of storybook.sidebar) {
556
- for (const entry of group.rows) entry.selected = entry.sectionId === id;
557
- }
558
- }
559
- function selectSectionAtIndex(storybook, index) {
560
- if (storybook.sections.length === 0) return;
561
- const safeIndex = index >= 0 && index < storybook.sections.length ? index : 0;
562
- storybook.selectedSectionIndex = safeIndex;
563
- markSidebarSelected(storybook, storybook.sections[safeIndex]?.id);
564
- }
565
- function applyFilterToSidebar(storybook, filter) {
566
- const active = (filter ?? "") !== "";
567
- for (const group of storybook.sidebar) {
568
- let anyMatch = false;
569
- for (const entry of group.rows) {
570
- const match = !active || fuzzyMatch(filter, `${entry.title} ${entry.description}`);
571
- if (match) anyMatch = true;
572
- entry.visible = match && !group.collapsed;
573
- }
574
- group.visible = active ? anyMatch : true;
575
- }
576
- }
577
- function toggleSidebarGroup(storybook, name) {
578
- const active = storybook.filter !== "";
579
- const group = storybook.sidebar.find((candidate) => candidate.name === name);
580
- if (!group) return;
581
- group.collapsed = !group.collapsed;
582
- for (const entry of group.rows) {
583
- const match = !active || fuzzyMatch(storybook.filter, `${entry.title} ${entry.description}`);
584
- entry.visible = match && !group.collapsed;
585
- }
586
- }
587
- function selectSectionWithId(storybook, id) {
588
- const index = id ? storybook.sections.findIndex((section) => section.id === id) : storybook.selectedSectionIndex;
589
- selectSectionAtIndex(storybook, index);
590
- }
591
- function focusExampleByIds(storybook, sectionId, exampleId) {
592
- if (!sectionId || !exampleId) return;
593
- const section = storybook.sections.find((candidate) => candidate.id === sectionId);
594
- const example = section?.items.find((candidate) => candidate.id === exampleId);
595
- if (!example) return;
596
- storybook.sectionId = sectionId;
597
- storybook.exampleId = exampleId;
598
- storybook.focusExample = example.value;
599
- }
600
- function setSelectedSectionFilter(storybook, value) {
601
- const section = storybook.sections[storybook.selectedSectionIndex];
602
- if (section) section.filter = value ?? "";
603
- }
604
- function transitionSections(ctx, sb, oldIndex, newIndex) {
605
- const changed = oldIndex !== newIndex;
606
- if (changed && sb.sections[oldIndex]?.initialized)
607
- ctx.at.index("sections", oldIndex).send("suspend", []);
608
- const target = sb.sections[newIndex];
609
- if (!target) return;
610
- if (!target.initialized) ctx.at.index("sections", newIndex).send("init", []);
611
- else if (changed) ctx.at.index("sections", newIndex).send("resume", []);
612
- }
613
- function fanoutLifecycle(ctx, items, name) {
614
- items.forEach((_item, j) => {
615
- ctx.at.index("items", j).send(name, []);
616
- });
617
- }
618
- function fuzzyMatch(query, target) {
619
- const q = query.toLowerCase().trim();
620
- if (q === "") return true;
621
- const t = target.toLowerCase();
622
- if (t.includes(q)) return true;
623
- const budget = q.length + Math.max(1, Math.floor(q.length / 2));
624
- for (let start = 0; start < t.length; start++) {
625
- if (t[start] !== q[0]) continue;
626
- let qi = 1;
627
- let ti = start + 1;
628
- for (; ti < t.length && qi < q.length; ti++) {
629
- if (t[ti] === q[qi]) qi++;
630
- }
631
- if (qi === q.length && ti - start <= budget) return true;
632
- }
633
- return false;
634
- }
635
- function slugify(str) {
636
- return String(str).normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
637
- }
638
- function buildSidebar(sections) {
639
- const groups = /* @__PURE__ */ new Map();
640
- const singles = [];
641
- sections.forEach((s) => {
642
- const name = s.group ?? "";
643
- if (name === "") singles.push(s);
644
- else {
645
- if (!groups.has(name)) groups.set(name, []);
646
- groups.get(name).push(s);
647
- }
648
- });
649
- const buckets = [];
650
- for (const [name, secs] of groups)
651
- buckets.push({ key: name.toLowerCase(), name, sections: secs });
652
- for (const s of singles) buckets.push({ key: s.title.toLowerCase(), name: "", sections: [s] });
653
- buckets.sort((a, b) => a.key.localeCompare(b.key));
654
- return buckets.map(
655
- (b) => SidebarGroup.make({
656
- name: b.name,
657
- collapsed: false,
658
- visible: true,
659
- rows: b.sections.map(
660
- (s) => SidebarEntry.make({
661
- sectionId: s.id,
662
- title: s.title,
663
- description: s.description,
664
- selected: false,
665
- visible: true
666
- })
667
- )
668
- })
669
- );
670
- }
671
-
672
- // src/storybook/build.js
673
- import { PASS } from "tutuca";
674
- import { getComponents as getInspectorComponents } from "tutuca/components";
675
- function buildStorybook(modules) {
676
- const rawSections = modules.flatMap((m) => {
677
- const raw = m.getExamples?.();
678
- if (raw == null) return [];
679
- return Array.isArray(raw) ? raw : [raw];
680
- });
681
- const sections = rawSections.map((s) => Section.fromData(s)).sort((a, b) => a.title.localeCompare(b.title));
682
- const engineComponents = [
683
- Storybook,
684
- Section,
685
- Example,
686
- SidebarGroup,
687
- SidebarEntry,
688
- // ActivityLog + ActivityEntry arrive via getInspectorComponents() (tutuca/components).
689
- ...getInspectorComponents()
690
- ];
691
- const engineSet = new Set(engineComponents);
692
- const moduleComponents = modules.map((m) => {
693
- const seen = /* @__PURE__ */ new Set();
694
- const out = [];
695
- for (const c of m.getComponents?.() ?? []) {
696
- if (!c || engineSet.has(c) || seen.has(c)) continue;
697
- seen.add(c);
698
- out.push(c);
699
- }
700
- return out;
701
- });
702
- const macros = {};
703
- const intentHandlers = {};
704
- const overrideNames = /* @__PURE__ */ new Set();
705
- for (const s of rawSections) {
706
- for (const it of s?.items ?? []) {
707
- for (const name in it?.intentHandlers ?? {}) overrideNames.add(name);
708
- }
709
- }
710
- for (const m of modules) {
711
- if (m.getMacros) Object.assign(macros, m.getMacros());
712
- if (m.getIntentHandlers) Object.assign(intentHandlers, m.getIntentHandlers());
713
- }
714
- const components = [.../* @__PURE__ */ new Set([...engineComponents, ...moduleComponents.flat()])];
715
- return {
716
- root: Storybook.withSections(sections),
717
- components,
718
- engineComponents,
719
- moduleComponents,
720
- macros,
721
- intentHandlers,
722
- overrideNames
723
- };
724
- }
725
- function buildExampleIntentHandlers({ intentHandlers: reals, overrideNames }) {
726
- const names = /* @__PURE__ */ new Set([...Object.keys(reals), ...overrideNames]);
727
- const makeMeta = (name) => async (...rest) => {
728
- const ctx = rest.at(-1);
729
- const args = rest.slice(0, -1);
730
- let override = null;
731
- ctx.walkPath((Comp, inst) => {
732
- const field = Comp.extra?.intentOverridesField;
733
- if (!field) return;
734
- const map = inst[field] ?? null;
735
- if (map && name in map) {
736
- override = map[name];
737
- return false;
738
- }
739
- });
740
- const fn = override ?? reals[name];
741
- if (!fn) return PASS;
742
- return await fn(...args, ctx);
743
- };
744
- const handlers = {};
745
- for (const name of names) handlers[name] = makeMeta(name);
746
- return handlers;
747
- }
748
-
749
- // src/storybook/mount.js
750
- import { injectCss, tutuca } from "tutuca";
751
- import { produce as produce2 } from "tutuca/immer";
752
-
753
- // src/storybook/activity.js
754
- import { rootDispatcher } from "tutuca";
755
- import { makeInspect, pathKeysLabel, recordToEntry } from "tutuca/components";
756
- function storybookPathLabel(pathKeys) {
757
- return pathKeysLabel(pathKeys.slice(2));
758
- }
759
- function subscribeExampleActivity(app) {
760
- const inspect = makeInspect(app);
761
- return app.observe((record) => {
762
- const keys = record.pathKeys;
763
- if (keys.length < 3 || keys[0].field !== "sections" || keys[1].field !== "items" || keys[2].field !== "value")
764
- return;
765
- const si = keys[0].key;
766
- const ii = keys[1].key;
767
- if (si === void 0 || ii === void 0) return;
768
- const entry = recordToEntry(record, { inspect, pathLabel: storybookPathLabel });
769
- rootDispatcher(app.transactor).at.index("sections", si).index("items", ii).send("logActivity", [entry]);
770
- });
771
- }
772
-
773
- // src/storybook/inspect.js
774
- import { ActivityLog, buildInspectorViews, isComponentInstance } from "tutuca/components";
775
- import { produce } from "tutuca/immer";
776
- function buildTestIndex(modules) {
777
- const index = /* @__PURE__ */ new Map();
778
- for (const m of modules) {
779
- if (typeof m.getTests !== "function") continue;
780
- const components = m.getComponents?.() ?? [];
781
- for (const c of components) {
782
- if (!index.has(c.name)) index.set(c.name, { getTests: m.getTests, components });
783
- }
784
- }
785
- return index;
786
- }
787
- async function buildExampleInspectors(example, scope, testIndex, dev) {
788
- const value = example.value;
789
- const comp = isComponentInstance(value) ? scope.getCompFor(value) : null;
790
- const entry = comp ? testIndex.get(comp.name) : null;
791
- const views = await buildInspectorViews(value, scope, {
792
- getTests: entry?.getTests ?? null,
793
- components: entry?.components ?? [],
794
- dev
795
- });
796
- return {
797
- ...views,
798
- activityLog: ActivityLog.make({})
799
- };
800
- }
801
- async function attachInspectorViews(root, scope, modules, dev = null) {
802
- const testIndex = buildTestIndex(modules);
803
- const updates = [];
804
- for (let si = 0; si < root.sections.length; si++) {
805
- for (let ii = 0; ii < root.sections[si].items.length; ii++) {
806
- updates.push({
807
- si,
808
- ii,
809
- values: await buildExampleInspectors(root.sections[si].items[ii], scope, testIndex, dev)
810
- });
811
- }
812
- }
813
- return produce(root, (draft) => {
814
- for (const { si, ii, values } of updates) Object.assign(draft.sections[si].items[ii], values);
815
- });
816
- }
817
-
818
- // src/storybook/themes.js
819
- var MARGAUI_THEMES = [
820
- "light",
821
- "dark",
822
- "abyss",
823
- "acid",
824
- "aqua",
825
- "autumn",
826
- "black",
827
- "bumblebee",
828
- "business",
829
- "caramellatte",
830
- "cmyk",
831
- "coffee",
832
- "corporate",
833
- "cupcake",
834
- "cyberpunk",
835
- "dim",
836
- "dracula",
837
- "emerald",
838
- "fantasy",
839
- "forest",
840
- "garden",
841
- "halloween",
842
- "lemonade",
843
- "lofi",
844
- "luxury",
845
- "night",
846
- "nord",
847
- "pastel",
848
- "retro",
849
- "silk",
850
- "sunset",
851
- "synthwave",
852
- "valentine",
853
- "winter",
854
- "wireframe"
855
- ];
856
- var BUNDLED_THEMES = /* @__PURE__ */ new Set(["light", "dark"]);
857
-
858
- // src/storybook/mount.js
859
- async function mountStorybook(selector, modules, { compileCss, root, persistUrl = true, dev = null, noCache = false, themes = null } = {}) {
860
- const app = tutuca(selector);
861
- const built = buildStorybook(modules);
862
- const themeBaseUrl = themes?.baseUrl ?? null;
863
- const base = root ?? built.root;
864
- app.state.set(
865
- themeBaseUrl ? produce2(base, (draft) => {
866
- draft.themes = MARGAUI_THEMES;
867
- }) : base
868
- );
869
- const rootScope = app.registerComponents(built.engineComponents);
870
- rootScope.registerMacros(built.macros);
871
- rootScope.registerIntentHandlers(buildExampleIntentHandlers(built));
872
- rootScope.registerIntentHandlers({ loadState: persistUrl ? loadState : loadStateBlank });
873
- if (persistUrl) {
874
- rootScope.registerIntentHandlers({ persistState });
875
- }
876
- if (themeBaseUrl) {
877
- rootScope.registerIntentHandlers({ applyTheme });
878
- }
879
- const moduleScopes = built.moduleComponents.map((comps) => {
880
- const scope = rootScope.enter();
881
- scope.registerComponents(comps);
882
- return scope;
883
- });
884
- if (dev && app.state.val?.sections) {
885
- app.state.set(await attachInspectorViews(app.state.val, rootScope, modules, dev));
886
- }
887
- if (compileCss) {
888
- injectCss("tutuca-storybook", await compileCss(app));
889
- }
890
- app.start({ noCache });
891
- if (dev) subscribeExampleActivity(app);
892
- app.sendAtRoot("init", []);
893
- if (persistUrl) {
894
- window.addEventListener("popstate", () => app.sendAtRoot("init", []));
895
- }
896
- app.scopes = { root: rootScope, modules: moduleScopes };
897
- return app;
898
- function persistState(state, instance, push) {
899
- if (instance !== app.state.val) return;
900
- const url = new URL(window.location.href);
901
- for (const [k, v] of Object.entries(state)) {
902
- if (v === "" || v == null) url.searchParams.delete(k);
903
- else url.searchParams.set(k, String(v));
904
- }
905
- window.history[push ? "pushState" : "replaceState"](null, "", url);
906
- }
907
- function applyTheme(name, instance) {
908
- if (instance !== app.state.val) return;
909
- ensureThemeLink(name);
910
- document.documentElement.dataset.theme = name;
911
- }
912
- function loadState() {
913
- const p = new URLSearchParams(window.location.search);
914
- return {
915
- section: p.get("section"),
916
- example: p.get("example"),
917
- sectionFilter: p.get("sectionFilter") ?? "",
918
- exampleFilter: p.get("exampleFilter") ?? "",
919
- theme: resolveTheme(p.get("theme"))
920
- };
921
- }
922
- function loadStateBlank() {
923
- return {
924
- section: null,
925
- example: null,
926
- sectionFilter: "",
927
- exampleFilter: "",
928
- theme: resolveTheme(null)
929
- };
930
- }
931
- function resolveTheme(fromUrl) {
932
- if (!themeBaseUrl) return "";
933
- const name = MARGAUI_THEMES.includes(fromUrl) ? fromUrl : osTheme();
934
- ensureThemeLink(name);
935
- document.documentElement.dataset.theme = name;
936
- return name;
937
- }
938
- function osTheme() {
939
- return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
940
- }
941
- function ensureThemeLink(name) {
942
- if (BUNDLED_THEMES.has(name) || document.getElementById(themeLinkId(name))) return;
943
- const link = document.createElement("link");
944
- link.rel = "stylesheet";
945
- link.id = themeLinkId(name);
946
- link.href = `${themeBaseUrl}${name}.css`;
947
- document.head.append(link);
948
- }
949
- function themeLinkId(name) {
950
- return `tutuca-theme-${name}`;
951
- }
952
- }
953
-
954
- // src/storybook/index.js
955
- function getComponents() {
956
- return [Storybook, Section, Example, SidebarGroup, SidebarEntry];
957
- }
958
- export {
959
- BUNDLED_THEMES,
960
- Example,
961
- MARGAUI_THEMES,
962
- Section,
963
- Storybook,
964
- buildExampleIntentHandlers,
965
- buildStorybook,
966
- fuzzyMatch,
967
- getComponents,
968
- mountStorybook,
969
- slugify
970
- };