tutuca 0.9.112 → 0.9.113

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.
@@ -17368,9 +17368,33 @@ function buildImports(base, { margauiEnabled, margauiJsUrl }) {
17368
17368
  imports.margaui = margauiJsUrl;
17369
17369
  return imports;
17370
17370
  }
17371
+ function themesBaseUrl(themeUrl) {
17372
+ return themeUrl.slice(0, themeUrl.lastIndexOf("/") + 1);
17373
+ }
17374
+ function renderNoFlashScript(themesUrl) {
17375
+ return `
17376
+ <script>
17377
+ (() => {
17378
+ const want = new URLSearchParams(location.search).get("theme");
17379
+ const name = /^[a-z]+$/.test(want ?? "")
17380
+ ? want
17381
+ : matchMedia("(prefers-color-scheme: dark)").matches
17382
+ ? "dark"
17383
+ : "light";
17384
+ document.documentElement.dataset.theme = name;
17385
+ if (name !== "light" && name !== "dark") {
17386
+ const link = document.createElement("link");
17387
+ link.rel = "stylesheet";
17388
+ link.id = "tutuca-theme-" + name;
17389
+ link.href = ${JSON.stringify(themesUrl)} + name + ".css";
17390
+ document.head.append(link);
17391
+ }
17392
+ })();
17393
+ </script>`;
17394
+ }
17371
17395
  function renderIndexHtml(imports, { margauiEnabled, margauiThemeUrl, bootstrapUrl }) {
17372
17396
  const theme = margauiEnabled ? `
17373
- <link rel="stylesheet" href="${margauiThemeUrl}" />` : "";
17397
+ <link rel="stylesheet" href="${margauiThemeUrl}" />${renderNoFlashScript(themesBaseUrl(margauiThemeUrl))}` : "";
17374
17398
  return `<!doctype html>
17375
17399
  <html lang="en">
17376
17400
  <head>
@@ -17388,7 +17412,7 @@ ${JSON.stringify({ imports }, null, 6)}
17388
17412
  </html>
17389
17413
  `;
17390
17414
  }
17391
- function renderBootstrap(devModuleUrls, { margauiEnabled, check, inspect: inspect3, noCache }) {
17415
+ function renderBootstrap(devModuleUrls, { margauiEnabled, margauiThemeUrl, check, inspect: inspect3, noCache }) {
17392
17416
  const lines = ['import { mountStorybook } from "tutuca/storybook";'];
17393
17417
  if (margauiEnabled) {
17394
17418
  lines.push('import { compileClassesToStyleText } from "tutuca/extra";');
@@ -17404,8 +17428,10 @@ function renderBootstrap(devModuleUrls, { margauiEnabled, check, inspect: inspec
17404
17428
  });
17405
17429
  const modules = devModuleUrls.map((_, i) => `m${i}`).join(", ");
17406
17430
  const optParts = [];
17407
- if (margauiEnabled)
17431
+ if (margauiEnabled) {
17408
17432
  optParts.push("compileCss: (app) => compileClassesToStyleText(app, compile)");
17433
+ optParts.push(`themes: { baseUrl: ${JSON.stringify(themesBaseUrl(margauiThemeUrl))} }`);
17434
+ }
17409
17435
  if (inspect3)
17410
17436
  optParts.push("dev: { shadowCheckComponent, runTests, expect }");
17411
17437
  if (noCache)
@@ -17617,7 +17643,13 @@ async function run5(argv, opts = {}) {
17617
17643
  margauiThemeUrl: mg2.themeUrl,
17618
17644
  bootstrapUrl: `./${bootstrapName}`
17619
17645
  }));
17620
- writeFileSync(resolve5(outDir, bootstrapName), renderBootstrap(devModuleUrls, { margauiEnabled, check, inspect: inspect3, noCache }));
17646
+ writeFileSync(resolve5(outDir, bootstrapName), renderBootstrap(devModuleUrls, {
17647
+ margauiEnabled,
17648
+ margauiThemeUrl: mg2.themeUrl,
17649
+ check,
17650
+ inspect: inspect3,
17651
+ noCache
17652
+ }));
17621
17653
  process.stdout.write(`wrote static storybook → ${relative2(process.cwd(), outDir) || "."}/
17622
17654
  index.html + ${bootstrapName} (${devModuleUrls.length} dev modules, CDN import map)
17623
17655
  Host it from the project root so /*.dev.js paths resolve.
@@ -17717,7 +17749,13 @@ async function run5(argv, opts = {}) {
17717
17749
  margauiThemeUrl: mg.themeUrl,
17718
17750
  bootstrapUrl: BOOTSTRAP_URL
17719
17751
  });
17720
- const bootstrapJs = renderBootstrap(devModuleUrls, { margauiEnabled, check, inspect: inspect3, noCache });
17752
+ const bootstrapJs = renderBootstrap(devModuleUrls, {
17753
+ margauiEnabled,
17754
+ margauiThemeUrl: mg.themeUrl,
17755
+ check,
17756
+ inspect: inspect3,
17757
+ noCache
17758
+ });
17721
17759
  const server = createServer((req, res) => {
17722
17760
  const path = req.url.split("?")[0];
17723
17761
  if (path === "/" || path === "/index.html") {
@@ -10,7 +10,9 @@ var Storybook = component({
10
10
  sectionId: null,
11
11
  exampleId: null,
12
12
  focusExample: null,
13
- sidebarCollapsed: false
13
+ sidebarCollapsed: false,
14
+ theme: "",
15
+ themes: []
14
16
  },
15
17
  statics: {
16
18
  withSections(sections) {
@@ -106,6 +108,11 @@ var Storybook = component({
106
108
  onFocusClose(ctx) {
107
109
  ctx.request("persistState", [this.toUrlState({ example: "" }), this, true]);
108
110
  return this.setSectionId(null).setExampleId(null).setFocusExample(null);
111
+ },
112
+ onSelectTheme(value, ctx) {
113
+ ctx.request("applyTheme", [value, this]);
114
+ ctx.request("persistState", [this.toUrlState({ theme: value }), this, false]);
115
+ return this.setTheme(value);
109
116
  }
110
117
  },
111
118
  bubble: {
@@ -149,7 +156,7 @@ var Storybook = component({
149
156
  loadState(state, err, ctx) {
150
157
  if (err || !state)
151
158
  return this;
152
- const selected = this.selectSectionWithId(state.section).setFilter(state.sectionFilter ?? "").applyFilterToSidebar(state.sectionFilter ?? "").setSelectedSectionFilter(state.exampleFilter ?? "");
159
+ const selected = this.selectSectionWithId(state.section).setTheme(state.theme ?? "").setFilter(state.sectionFilter ?? "").applyFilterToSidebar(state.sectionFilter ?? "").setSelectedSectionFilter(state.exampleFilter ?? "");
153
160
  const next = state.example ? selected.focusExampleByIds(state.section, state.example) : selected.setSectionId(null).setExampleId(null).setFocusExample(null);
154
161
  transitionSections(ctx, next, this.selectedSectionIndex, next.selectedSectionIndex);
155
162
  return next;
@@ -195,6 +202,15 @@ var Storybook = component({
195
202
  @on.input="onApplyFilter value"
196
203
  @on.keydown.cancel="onClearFilter"
197
204
  />
205
+ <select
206
+ class="select w-auto"
207
+ title="Theme"
208
+ @hide="empty? .themes"
209
+ :value=".theme"
210
+ @on.input="onSelectTheme value"
211
+ >
212
+ <option @each=".themes" :value="@value" @text="@value"></option>
213
+ </select>
198
214
  </div>
199
215
  <div class="list h-full flex-1 overflow-y-auto">
200
216
  <x render-each=".sidebar" @when="groupVisible"></x>
@@ -729,11 +745,53 @@ async function attachInspectorViews(root, scope, modules, dev = null) {
729
745
  return root.setSections(sections);
730
746
  }
731
747
 
748
+ // src/storybook/themes.js
749
+ var MARGAUI_THEMES = [
750
+ "light",
751
+ "dark",
752
+ "abyss",
753
+ "acid",
754
+ "aqua",
755
+ "autumn",
756
+ "black",
757
+ "bumblebee",
758
+ "business",
759
+ "caramellatte",
760
+ "cmyk",
761
+ "coffee",
762
+ "corporate",
763
+ "cupcake",
764
+ "cyberpunk",
765
+ "dim",
766
+ "dracula",
767
+ "emerald",
768
+ "fantasy",
769
+ "forest",
770
+ "garden",
771
+ "halloween",
772
+ "lemonade",
773
+ "lofi",
774
+ "luxury",
775
+ "night",
776
+ "nord",
777
+ "pastel",
778
+ "retro",
779
+ "silk",
780
+ "sunset",
781
+ "synthwave",
782
+ "valentine",
783
+ "winter",
784
+ "wireframe"
785
+ ];
786
+ var BUNDLED_THEMES = new Set(["light", "dark"]);
787
+
732
788
  // src/storybook/mount.js
733
- async function mountStorybook(selector, modules, { compileCss, root, persistUrl = true, dev = null, noCache = false } = {}) {
789
+ async function mountStorybook(selector, modules, { compileCss, root, persistUrl = true, dev = null, noCache = false, themes = null } = {}) {
734
790
  const app = tutuca(selector);
735
791
  const built = buildStorybook(modules);
736
- app.state.set(root ?? built.root);
792
+ const themeBaseUrl = themes?.baseUrl ?? null;
793
+ const base = root ?? built.root;
794
+ app.state.set(themeBaseUrl && base.setThemes ? base.setThemes(MARGAUI_THEMES) : base);
737
795
  const rootScope = app.registerComponents(built.engineComponents);
738
796
  rootScope.registerMacros(built.macros);
739
797
  rootScope.registerRequestHandlers(buildExampleRequestHandlers(built));
@@ -741,6 +799,9 @@ async function mountStorybook(selector, modules, { compileCss, root, persistUrl
741
799
  if (persistUrl) {
742
800
  rootScope.registerRequestHandlers({ persistState });
743
801
  }
802
+ if (themeBaseUrl) {
803
+ rootScope.registerRequestHandlers({ applyTheme });
804
+ }
744
805
  const moduleScopes = built.moduleComponents.map((comps) => {
745
806
  const scope = rootScope.enter();
746
807
  scope.registerComponents(comps);
@@ -773,17 +834,53 @@ async function mountStorybook(selector, modules, { compileCss, root, persistUrl
773
834
  }
774
835
  window.history[push ? "pushState" : "replaceState"](null, "", url);
775
836
  }
837
+ function applyTheme(name, instance) {
838
+ if (instance !== app.state.val)
839
+ return;
840
+ ensureThemeLink(name);
841
+ document.documentElement.dataset.theme = name;
842
+ }
776
843
  function loadState() {
777
844
  const p = new URLSearchParams(window.location.search);
778
845
  return {
779
846
  section: p.get("section"),
780
847
  example: p.get("example"),
781
848
  sectionFilter: p.get("sectionFilter") ?? "",
782
- exampleFilter: p.get("exampleFilter") ?? ""
849
+ exampleFilter: p.get("exampleFilter") ?? "",
850
+ theme: resolveTheme(p.get("theme"))
783
851
  };
784
852
  }
785
853
  function loadStateBlank() {
786
- return { section: null, example: null, sectionFilter: "", exampleFilter: "" };
854
+ return {
855
+ section: null,
856
+ example: null,
857
+ sectionFilter: "",
858
+ exampleFilter: "",
859
+ theme: resolveTheme(null)
860
+ };
861
+ }
862
+ function resolveTheme(fromUrl) {
863
+ if (!themeBaseUrl)
864
+ return "";
865
+ const name = MARGAUI_THEMES.includes(fromUrl) ? fromUrl : osTheme();
866
+ ensureThemeLink(name);
867
+ document.documentElement.dataset.theme = name;
868
+ return name;
869
+ }
870
+ function osTheme() {
871
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
872
+ }
873
+ function ensureThemeLink(name) {
874
+ if (BUNDLED_THEMES.has(name) || document.getElementById(themeLinkId(name)))
875
+ return;
876
+ const link = document.createElement("link");
877
+ link.rel = "stylesheet";
878
+ link.id = themeLinkId(name);
879
+ link.href = `${themeBaseUrl}${name}.css`;
880
+ document.head.append(link);
881
+ }
882
+ function themeLinkId(name) {
883
+ return `tutuca-theme-${name}`;
787
884
  }
788
885
  }
789
886
 
@@ -800,5 +897,7 @@ export {
800
897
  buildExampleRequestHandlers,
801
898
  Storybook,
802
899
  Section,
803
- Example
900
+ MARGAUI_THEMES,
901
+ Example,
902
+ BUNDLED_THEMES
804
903
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tutuca",
3
- "version": "0.9.112",
3
+ "version": "0.9.113",
4
4
  "type": "module",
5
5
  "description": "SPA framework with immutable state and virtual DOM; dependency-free browser bundle",
6
6
  "main": "./dist/tutuca.js",
@@ -68,6 +68,45 @@ that reason):
68
68
  Trade-off: no runtime network dependency and a frozen version, at the
69
69
  cost of updating the vendored files by hand.
70
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
+
71
110
  ## Wire it into tutuca
72
111
 
73
112
  However you obtained `compile`, the integration is the same: register
@@ -215,6 +215,32 @@ version-pinned CDN. `--out` always pins the CDN so the artifact is portable —
215
215
  host it from the project root so `/*.dev.js` paths resolve. See [cli.md](./cli.md)
216
216
  for the exhaustive flag list and exit codes.
217
217
 
218
+ ## Themes
219
+
220
+ The page starts in the theme the URL asks for (`?theme=dracula`), else the one
221
+ the OS prefers, and the sidebar has a switcher for margaui's palettes. Picking
222
+ one sets `?theme=`, so a themed storybook is shareable and survives a reload.
223
+ That OS step is on us, not margaui: its `dark.css` is keyed on
224
+ `[data-theme="dark"]` alone, with **no** `prefers-color-scheme` fallback, so a
225
+ page that only links `theme.css` renders light forever. See
226
+ [margaui.md](./margaui.md).
227
+
228
+ Embedding the storybook yourself? The switcher is opt-in through the `themes`
229
+ option, which — like `compileCss` — is injected, so the library still never
230
+ imports margaui:
231
+
232
+ ```js
233
+ mountStorybook("#app", modules, {
234
+ compileCss: (app) => compileClassesToStyleText(app, compile),
235
+ // The directory your theme.css lives in. Every other palette is a sibling
236
+ // file there; only the one selected is fetched, and only the first time.
237
+ themes: { baseUrl: "https://marianoguerra.github.io/margaui/themes/" },
238
+ });
239
+ ```
240
+
241
+ Omit `themes` (what `--no-margaui` does) and no switcher renders — there would
242
+ be no theme CSS to switch to.
243
+
218
244
  ## Footguns
219
245
 
220
246
  - ⚠️ `value` must be a real instance (`Comp.make(...)`), not a plain object or