domma-cms 1.6.60 → 1.7.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.
Files changed (137) hide show
  1. package/CHANGELOG.md +4344 -0
  2. package/README.md +1 -1
  3. package/admin/dist/domma/domma-tools.css +3 -3
  4. package/admin/dist/domma/domma-tools.min.js +3 -3
  5. package/admin/js/lib/effect-defs.js +1 -1
  6. package/admin/js/lib/effects-builder.js +2 -2
  7. package/admin/js/lib/page-editor-reference.js +5 -5
  8. package/admin/js/lib/shortcode-tags.js +1 -1
  9. package/admin/js/templates/docs/usage-shortcodes.html +39 -0
  10. package/docs/DommaFeatures.md +3005 -0
  11. package/docs/MultiTenant.md +176 -0
  12. package/docs/api-reference.md +1667 -0
  13. package/docs/cache.md +96 -0
  14. package/docs/configuration.md +1811 -0
  15. package/docs/editions-and-licences.md +190 -0
  16. package/docs/getting-started.md +201 -0
  17. package/docs/guides/form-triggers.md +260 -0
  18. package/docs/guides/mongodb-storage-adapter.md +282 -0
  19. package/docs/guides/multi-page-forms.md +121 -0
  20. package/docs/images/scribe-composer.png +0 -0
  21. package/docs/markdown-shortcodes.md +1738 -0
  22. package/docs/pentest-report-2026-04-03.md +181 -0
  23. package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
  24. package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
  25. package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
  26. package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
  27. package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
  28. package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
  29. package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
  30. package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
  31. package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
  32. package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
  33. package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
  34. package/docs/plans/2026-03-05-user-types-design.md +139 -0
  35. package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
  36. package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
  37. package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
  38. package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
  39. package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
  40. package/docs/plans/2026-06-10-core-project-design.md +240 -0
  41. package/docs/plugin-development.md +827 -0
  42. package/docs/recruitment-recipe.md +501 -0
  43. package/docs/scaffolding.md +312 -0
  44. package/docs/screenshots/calculator-floating.png +0 -0
  45. package/docs/screenshots/calculator-standard.png +0 -0
  46. package/docs/screenshots/email-settings.png +0 -0
  47. package/docs/screenshots/messages-count.png +0 -0
  48. package/docs/structure/user-types.md +107 -0
  49. package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
  50. package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
  51. package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
  52. package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
  53. package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
  54. package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
  55. package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
  56. package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
  57. package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
  58. package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
  59. package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
  60. package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
  61. package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
  62. package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
  63. package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
  64. package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
  65. package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
  66. package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
  67. package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
  68. package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
  69. package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
  70. package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
  71. package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
  72. package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
  73. package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
  74. package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
  75. package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
  76. package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
  77. package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
  78. package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
  79. package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
  80. package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
  81. package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
  82. package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
  83. package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
  84. package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
  85. package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
  86. package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
  87. package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
  88. package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
  89. package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
  90. package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
  91. package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
  92. package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
  93. package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
  94. package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
  95. package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
  96. package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
  97. package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
  98. package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
  99. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
  100. package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
  101. package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
  102. package/docs/theming.md +320 -0
  103. package/package.json +2 -2
  104. package/public/js/celebrations/core/canvas.js +1 -1
  105. package/public/js/celebrations/index.js +1 -1
  106. package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
  107. package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
  108. package/public/js/celebrations/message-attrs.mjs +1 -0
  109. package/public/js/celebrations/themes/anniversary.js +1 -0
  110. package/public/js/celebrations/themes/autumn.js +1 -0
  111. package/public/js/celebrations/themes/birthday.js +1 -0
  112. package/public/js/celebrations/themes/butterfly-flight.js +1 -0
  113. package/public/js/celebrations/themes/calendar.js +1 -0
  114. package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
  115. package/public/js/celebrations/themes/chinese-new-year.js +1 -0
  116. package/public/js/celebrations/themes/christmas.js +1 -1
  117. package/public/js/celebrations/themes/easter.js +1 -0
  118. package/public/js/celebrations/themes/garden-drawing.js +1 -0
  119. package/public/js/celebrations/themes/garden.js +1 -0
  120. package/public/js/celebrations/themes/graduation.js +1 -0
  121. package/public/js/celebrations/themes/greeting.js +1 -0
  122. package/public/js/celebrations/themes/make-it-rain.js +1 -0
  123. package/public/js/celebrations/themes/money-rain.js +1 -0
  124. package/public/js/celebrations/themes/new-year.js +1 -0
  125. package/public/js/celebrations/themes/occasion-drawing.js +1 -0
  126. package/public/js/celebrations/themes/occasions.js +1 -0
  127. package/public/js/celebrations/themes/rain-control.js +1 -0
  128. package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
  129. package/public/js/celebrations/themes/sparkler-font.js +1 -0
  130. package/public/js/celebrations/themes/sparkler-writing.js +1 -0
  131. package/public/js/celebrations/themes/summer-garden.js +1 -0
  132. package/public/js/celebrations/themes/winter.js +1 -0
  133. package/public/js/celebrations/trait-data.mjs +1 -1
  134. package/public/js/effects.js +1 -1
  135. package/scripts/build.js +4 -0
  136. package/server/services/markdown.js +5 -2
  137. package/server/templates/page.html +1 -1
@@ -0,0 +1,260 @@
1
+ # Promoting Plugins into Core Tools, and the Marketplace Rename
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Rename the Plugins screen to **Marketplace**, and promote four plugins into core so they
6
+ stop being things you install and start being things the CMS has.
7
+
8
+ **The model, decided 2026-09-22:** a plugin is something you acquire from the **Marketplace**; once
9
+ enabled it is a **Tool**. The sidebar already works this way - `sidebar-grouping.js` gathers every
10
+ enabled plugin into a Tools folder, on the grounds that "once a plugin is turned on it is part of the
11
+ system, not an optional extra". Only the naming lagged behind.
12
+
13
+ **Destinations differ by plugin.** Three of the four are not Tools at all:
14
+
15
+ | Plugin | LOC | Destination | Why |
16
+ |---|---|---|---|
17
+ | `demo-viewer` | 40 | a core shortcode in `markdown.js` | registers `[demo]` and nothing else. No admin view, no routes, no data. A shortcode is not a Tool. |
18
+ | `theme-switcher` | 101 | a toggle in **Content → Theme** | a floating disc on the *public* site. No admin screen worth opening; it manipulates exactly what `themeSettings.js` already owns. |
19
+ | `notes` | 412 | core Tool | genuine admin app: view, routes, per-user collection. |
20
+ | `todo` | 452 | core Tool | as above. |
21
+ | `analytics` | 678 | core Tool | already in `CORE_PLUGINS`: always loaded, cannot be switched off. A Tool in all but name. |
22
+ | `contacts` | 1,427 | core Tool | enabled on 9 of 9 sites. Two collections, plus a seeded sample file. |
23
+
24
+ **Spec:** `docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md` (tier register, and the
25
+ Marketplace framing)
26
+
27
+ **Critical project rules** (from `CLAUDE.md` + memory):
28
+
29
+ - Server on port **4096** is always running; never start or stop it. Test via `curl`.
30
+ - `npm test` is serial. `domma-cms` is worked on concurrently - commit early, named paths only.
31
+ - These four are vendored in **two** places: `domma-cms/plugins/` and `dcms-marketplace/plugins/`.
32
+ Both must lose them, and the monorepo's `reserved.json` must gain them.
33
+ - Promotion changes core, so it reaches the fleet only in a release. Until then the plugins keep
34
+ working exactly as they do now, which is what makes each phase independently shippable.
35
+
36
+ **Ordering constraint:** each retirement migration runs **before** the plugin loader, or a site
37
+ briefly runs the core feature and the plugin at once. `search-migration.js` is the template and is
38
+ already wired that way in `server.js`.
39
+
40
+ ---
41
+
42
+ ## Phase 1 - Rename, and delete a dead control
43
+
44
+ Framing first: it is cheap, carries no data risk, and makes the promotions read correctly.
45
+
46
+ - [ ] `MANAGE_PLUGINS_ITEM.text` in `admin/js/lib/sidebar-grouping.js`: "Manage Plugins" →
47
+ **"Marketplace"**. Consider `shopping-bag` or similar for the icon.
48
+ - [ ] Update the heading and tab labels in `admin/js/views/plugins.js`.
49
+ - [ ] **Keep the URL as `#/plugins`.** Renaming the hash means touching every plugin manifest's
50
+ `admin.sidebar[].url` and any stored admin-sidebar menu that references them, all invisible to a
51
+ user who reads labels.
52
+ - [ ] **Delete the `bundled` control.** `btn-bundle-plugin` writes `bundled` through
53
+ `PUT /plugins/:name` into `config/plugins.json`, and **nothing reads it**: not `server/`, not the
54
+ manager, not the scaffolder (which reads `tier` since 2026-09-22), and no site on the fleet has it
55
+ set. Remove the badge, the handler, and the `bundled` branch in the route. The `bundled` flag in
56
+ `blocks.js` is a different concept and stays.
57
+ - [ ] Leave **Update** off the screen. `installPlugin()` refuses an existing directory, so the verb
58
+ has no mechanism until the upgrade path lands in the marketplace work. A shop with no Update is
59
+ honest; one with a button that throws is not.
60
+
61
+ **No migration needed.** `stripItemByUrl(tree, MANAGE_PLUGINS_URL)` removes any persisted copy of the
62
+ item from stored menu data and re-synthesises it on every render, so the new label reaches every site
63
+ on next load.
64
+
65
+ **Exit:** the sidebar reads Marketplace, the screen's verbs are install / enable / configure, and no
66
+ control writes a value nothing consumes.
67
+
68
+ ## Phase 2 - `demo-viewer` → a core shortcode
69
+
70
+ Smallest promotion, and it establishes the retirement pattern.
71
+
72
+ - [ ] Move the `[demo]` handler out of `plugins/demo-viewer/plugin.js` and into core's shortcode set
73
+ alongside the others in `markdown.js`. Keep the attribute contract identical - it is in use on 8 of
74
+ 9 sites.
75
+ - [ ] Settings from `config.js` (if any are read) move to the shortcode's own defaults.
76
+ - [ ] Retirement migration, modelled on `search-migration.js`: drop the `plugins.json` entry, and
77
+ delete `plugins/demo-viewer/` **only if it is the bundled one** (manifest `name` and `author` both
78
+ match). A directory of that name carrying somebody else's work is left alone.
79
+ - [ ] Tests: `[demo]` renders identically before and after, on a page that already uses it.
80
+
81
+ **Exit:** `[demo]` works with no plugin present, on a site that never had one.
82
+
83
+ ## Phase 3 - `theme-switcher` → Content → Theme
84
+
85
+ - [ ] Add the switcher's options to `config/theme.json` via `themeSettings.js`, and a control for
86
+ them on the **Content → Theme** screen. It is a theme feature, so it belongs beside the theme,
87
+ fonts and per-element overrides it manipulates.
88
+ - [ ] The public assets from `public/inject-head.html` and `public/inject-body.html` become a core
89
+ builder, in the shape of `buildThemeView()` and `buildSearchAssets()`.
90
+ - [ ] **Wire it into BOTH render paths in `renderer.js`.** This is the standing trap in that file:
91
+ `buildThemeView()` and `buildSearchAssets()` each have two call sites, and wiring one and not the
92
+ other is how a feature works on one path and silently does nothing on the other.
93
+ - [ ] `enabled: false` must emit nothing at all, matching how search settings behave.
94
+ - [ ] Retirement migration as Phase 2, lifting any existing settings out of `plugins.json` into
95
+ `theme.json` before deleting the directory.
96
+ - [ ] Verify in a browser on a real page, both themes, since the whole feature is visual.
97
+
98
+ **Exit:** the disc appears from a theme setting, the plugin is gone, and a site that had it
99
+ configured keeps its configuration.
100
+
101
+ ## Phase 4 - `notes` and `todo` → core Tools
102
+
103
+ Both have the same shape, so they are one phase done twice.
104
+
105
+ - [ ] Admin view and template → `admin/js/views/`, registered in `admin/js/views/index.js` and the
106
+ core route list in `app.js`. An editor view reads its slug from `location.hash`, never from a
107
+ second `onMount` parameter, which is always `undefined`.
108
+ - [ ] Routes → `server/routes/api/`, registered in `server.js`.
109
+ - [ ] **Permissions**: plugin routes currently get theirs through `registerPluginResource()`. Core
110
+ routes need explicit entries in `permissionRegistry.js`, and the sidebar item needs a `permission`
111
+ key so it hides for a role that lacks it.
112
+ - [ ] Collection schemas → the `PRESETS` array in `server/services/presetCollections.js`, keeping the
113
+ slugs **exactly**: `notes-notes` and `todo-items`.
114
+
115
+ **The directory name is not the slug.** `setupPlugin()` reads `schema.slug`, never the folder it
116
+ found the file in, and every plugin collection in this repo disagrees with its own directory:
117
+ `collections/user-notes/` declares `notes-notes`, `collections/todos/` declares `todo-items`,
118
+ `collections/user-contacts/` declares `contacts-contacts`. Key a preset off the directory name and
119
+ core will create a new empty collection while the site's real one sits beside it, orphaned.
120
+ - [ ] **The data does not move.** Entries already live in `content/collections/<slug>/`, outside the
121
+ plugin directory, because `setupPlugin()` seeded them there. Keep the slug and existing entries are
122
+ untouched; change it and they are orphaned.
123
+ - [ ] Preserve the per-user scope. Both filter on `userId` by hand in their route handlers, driven by
124
+ `config.scope`. Core has `rowAccess.js` for exactly this. Moving to it is the better end state, but
125
+ it is a **behaviour change** and belongs in its own task, not smuggled into a move.
126
+ - [ ] Preserve `notes`' storage declaration: its `config.js` asks for the mongodb adapter with file
127
+ fallback. A site with no `connections.json` must keep degrading gracefully.
128
+ - [ ] Retirement migration per plugin, as Phase 2.
129
+
130
+ **Exit:** both Tools work with no plugin directory present; a site that had notes keeps every note.
131
+
132
+ ## Phase 5 - `analytics` → core Tool
133
+
134
+ It is already `CORE_PLUGINS`, so it always loads and files under Tools whatever `plugins.json` says.
135
+ Promotion makes the code match what the behaviour has been for a while. **It is the riskiest phase
136
+ here**, for one reason the others do not have.
137
+
138
+ - [ ] **The data moves, and it is real history.** Unlike `notes` and `todo`, whose entries already sit
139
+ in `content/collections/`, analytics keeps `stats.json`, `daily.json`, `lifetime.json` and
140
+ `journeys.json` inside `plugins/analytics/data/` - **28K to 96K per site** of accumulated traffic
141
+ history across the fleet. Losing it is losing every site's past.
142
+ - [ ] Move the store to `content/analytics/`, not `var/` and not a collection. `content/` is the
143
+ operator's own directory and survives an update; `plugins/` is upstream-managed territory, which is
144
+ the wrong home for data a site cannot regenerate.
145
+ - [ ] The migration must **copy, verify, then leave the originals in place** for one release rather
146
+ than moving. A failed move here is unrecoverable; a duplicated 96K is not a problem.
147
+ - [ ] The `scaffold.reset` entries in the manifest (which blank the four files for a new site) become
148
+ part of core's site scaffolding.
149
+ - [ ] Drop the five `!plugins/analytics/*` exclusions from `package.json` `files` once the data lives
150
+ elsewhere.
151
+ - [ ] The public beacon from `public/inject-head.html` / `inject-body.html` becomes a core builder,
152
+ and like Phase 3 it must be **wired into both render paths in `renderer.js`**.
153
+ - [ ] Keep Do-Not-Track and per-session dedup exactly as they are. They are in the manifest
154
+ description, which means they are a promise.
155
+
156
+ ### `analytics-pro` stays a paid plugin, and needs no supersede
157
+
158
+ The two do not overlap the way a free/paid pair usually does:
159
+
160
+ | | collection method | store |
161
+ |---|---|---|
162
+ | `analytics` (core) | **client-side beacon**, injected head + bodyEnd | `data/*.json` |
163
+ | `analytics-pro` (paid) | **server-side `onRequest` hook**, no inject at all | `hits.jsonl` |
164
+
165
+ Pro is not a better version of the free one, it is a *different instrument*: the beacon misses bots,
166
+ blocked scripts and Do-Not-Track visitors by design, while request logging catches everything. So
167
+ they coexist correctly, and `supersedes` is neither needed nor available - `resolveSupersessions()`
168
+ refuses to let anything supersede a core plugin, and promotion makes that permanent.
169
+
170
+ - [ ] **Decide how two numbers are presented.** On the four sites running both, the server-side count
171
+ will always exceed the client-side one, for good reasons a customer will not infer. Labelling them
172
+ as what they are - "page views" against "requests" - is the cheap fix. Showing one number that
173
+ silently changes when you buy Pro is the expensive mistake.
174
+
175
+ ## Phase 6 - `contacts` → core Tool
176
+
177
+ Same shape as `notes` and `todo`, with one extra wrinkle and one dependent.
178
+
179
+ - [ ] Admin view, template and `admin/css/index.css` → core. It is the only one of these with its own
180
+ stylesheet.
181
+ - [ ] Collections → `PRESETS`, keeping the slugs **`contacts-contacts` and `contacts-groups`** - see
182
+ the warning in Phase 4, since both directories are named something else entirely.
183
+ - [ ] `data/contacts.json` is **seeded sample data, not user data**: 401 bytes, two entries, byte
184
+ identical on every site that has it. It does not need migrating, and core should stop shipping it -
185
+ a new install with two fictional contacts in it is worse than an empty one.
186
+ - [ ] `surveys` (paid) imports from contacts, in `admin/views/audience.js`. Promotion **improves**
187
+ that: a paid plugin depending on core is sound, where depending on another plugin that may be
188
+ uninstalled was always fragile. Check the import path still resolves once contacts is no longer at
189
+ `plugins/contacts/`.
190
+
191
+ ### What the data actually says about this tier
192
+
193
+ Measured across all nine sites: **`contacts-contacts` and `contacts-groups` exist on six sites and
194
+ hold zero entries on every one of them.** No site has a blog post either. So "enabled on 9 of 9" -
195
+ which is how contacts was described earlier in this work - is a measure of what the scaffolder
196
+ switched on, not of anything anyone uses.
197
+
198
+ That does not argue against promoting it. It does mean promotion should not be expected to change
199
+ usage, and the honest reason to do it is coherence: a contacts book is the kind of thing a CMS has
200
+ rather than something you go shopping for.
201
+
202
+ ## Phase 7 - Remove them from the marketplace
203
+
204
+ **Done 2026-09-22.** All seven phases complete.
205
+
206
+ - [x] All six deleted from `dcms-marketplace/plugins/`; `make verify` reports **14 plugins: 2 free,
207
+ 12 paid, 10 reserved slugs**.
208
+ - [x] All six added to `reserved.json` with a reason and date, so the slugs cannot return as
209
+ third-party plugins wearing familiar names. `make verify` already fails a reserved slug that has a
210
+ directory.
211
+ - [x] **The free tier is 2 plugins**: `blog` and `mail-reader`. At two, it is worth
212
+ asking whether it is still a tier or just the two things that have not been promoted yet. `blog` is
213
+ genuinely optional - a site either has one or does not. `mail-reader` is the trial for `email-pro`,
214
+ which supersedes it, so it earns its place as the free half of a paid product rather than as a
215
+ feature. Both defensible, but state the reason rather than leaving it as residue.
216
+ - [ ] `CORE_PLUGINS` becomes empty and can be deleted along with `isCorePlugin()`, or kept as the
217
+ mechanism for the next promotion. It called itself "a waiting room rather than a destination"; with
218
+ `analytics` gone it has no residents.
219
+ - [x] `CLAUDE.md` gains the Tools model and the two promotion traps; `README.md`'s table is
220
+ regenerated from what is actually in `plugins/`; `docs/plugin-development.md`'s example table
221
+ pointed at `analytics`, which is no longer a plugin, and now points at `blog`.
222
+
223
+ **Exit: met.** The marketplace lists only plugins you can actually acquire.
224
+
225
+ ### What the promotions cost, for the next one
226
+
227
+ Three things bit during this work and are now written into `CLAUDE.md`:
228
+
229
+ 1. **`schema.slug` is the slug, never the directory name.** All five plugin collections disagreed
230
+ with their own folders. This plan named two wrong slugs before it was corrected.
231
+ 2. **`seedAll()` rewrites `schema.json` on every boot.** A preset's `storage` block must match what
232
+ the plugin declared, or an install's adapter flips and its rows go out of sight.
233
+ 3. **The admin-sidebar menu is seeded once, file-existence guarded.** Core entries must go through
234
+ `registerSidebarItem()` or they reach new installs only.
235
+
236
+ And two behaviours were found and pinned rather than tidied, because a relocation is the wrong place
237
+ to change behaviour: `contacts` groups are site-wide while contacts are per-user, and `[demo]` had
238
+ been emitting an iframe with no `src` since `allowedIframeHostnames` was introduced.
239
+
240
+ ## Verification
241
+
242
+ - [ ] `npm test` passes, serially, after each phase.
243
+ - [ ] A site with none of the four installed has working `[demo]`, a theme switcher toggle, Notes and
244
+ Todo.
245
+ - [ ] A site that **had** notes with entries still has every entry afterwards.
246
+ - [ ] A site that had `theme-switcher` configured keeps its configuration in `theme.json`.
247
+ - [ ] Each migration is idempotent: run twice, second run is a no-op.
248
+ - [ ] A directory named `notes` that is *not* ours survives the migration untouched.
249
+ - [ ] The Marketplace screen offers install / enable / configure and nothing that writes a dead value.
250
+
251
+ ## Out of scope
252
+
253
+ **`contacts` stays a plugin for now**, despite being the strongest candidate on usage - enabled on
254
+ **9 of 9 sites**, the only free plugin anybody runs. Recorded here so the argument is not had twice:
255
+ if the test for promotion is "this is part of the system rather than an add-on", contacts passes it
256
+ more clearly than `notes` or `todo` do. Revisit once the four above have proved the pattern.
257
+
258
+ Also out of scope: the plugin **Update** verb and its upgrade path, the developer surface split
259
+ (scaffolding and the code editor are gated by `plugins.create` / `plugins.develop` and arguably want
260
+ their own screen rather than tabs on a shop), and moving `notes`/`todo` onto `rowAccess.js`.
@@ -0,0 +1,320 @@
1
+ # Theming
2
+
3
+ Domma CMS uses the **Domma** UI framework's theming system. Themes are applied via a CSS class on `<html>` and control all colour tokens, shadows, and typography scales through CSS custom properties.
4
+
5
+ ---
6
+
7
+ ## Available Themes
8
+
9
+ Twenty-eight public themes ship out of the box - fourteen colour palettes each available in a dark and a light variant.
10
+
11
+ | Palette | Dark variant | Light variant |
12
+ |-----------|--------------------|---------------------|
13
+ | Charcoal | `charcoal-dark` | `charcoal-light` |
14
+ | Ocean | `ocean-dark` | `ocean-light` |
15
+ | Forest | `forest-dark` | `forest-light` |
16
+ | Sunset | `sunset-dark` | `sunset-light` |
17
+ | Royal | `royal-dark` | `royal-light` |
18
+ | Lemon | `lemon-dark` | `lemon-light` |
19
+ | Silver | `silver-dark` | `silver-light` |
20
+ | Grayve | `grayve-dark` | `grayve-light` |
21
+ | Mint | `mint-dark` | `mint-light` |
22
+ | Unicorn | `unicorn-dark` | `unicorn-light` |
23
+ | Dreamy | `dreamy-dark` | `dreamy-light` |
24
+ | Christmas | `christmas-dark` | `christmas-light` |
25
+ | Wedding | `wedding-dark` | `wedding-light` |
26
+ | Corporate | `corporate-dark` | `corporate-light` |
27
+
28
+ ### Admin Themes
29
+
30
+ Nine dedicated admin-chrome themes engineered across three finishes:
31
+
32
+ | Finish | Indigo | Steel | Teal | Description |
33
+ |---|---|---|---|---|
34
+ | **Sharp** | `admin-sharp-indigo` | `admin-sharp-steel` | `admin-sharp-teal` | Crisp, high-contrast light chrome |
35
+ | **Slate** | `admin-slate-indigo` | `admin-slate-steel` | `admin-slate-teal` | Balanced mid-tone chrome for long administrative sessions |
36
+ | **Smooth** | `admin-smooth-indigo` | `admin-smooth-steel` | `admin-smooth-teal` | Low-glare dark chrome |
37
+
38
+ ---
39
+
40
+ ## Setting the Theme
41
+
42
+ ### Via the admin
43
+
44
+ 1. Log in to the admin panel
45
+ 2. Go to **Content > Theme** in the sidebar
46
+ 3. On the **Themes** tab, choose a **Front-end Theme** for the public site and an **Admin Theme** for the
47
+ admin panel, and set the typography
48
+ 4. Click **Save**
49
+
50
+ Everything on that page is stored in `config/theme.json`. It used to live in `config/site.json`; a one-shot
51
+ migration lifts it out on first boot after upgrading, and the old keys are still rewritten there on every
52
+ save so plugins and fleet tooling that read them keep working. `theme.json` is the authoritative copy -
53
+ editing the mirrored keys in `site.json` by hand has no effect.
54
+
55
+ ### Via `config/theme.json` directly
56
+
57
+ ```json
58
+ {
59
+ "theme": "ocean-dark",
60
+ "adminTheme": "charcoal-dark",
61
+ "autoTheme": {"enabled": false, "dayTheme": "ocean-light", "nightTheme": "ocean-dark",
62
+ "dayStart": "07:00", "nightStart": "19:00"},
63
+ "font": {"family": "Nunito", "size": 18},
64
+ "overrides": []
65
+ }
66
+ ```
67
+
68
+ The server reads this per render and stamps the theme onto `<body>` as `dm-theme-<id>`.
69
+
70
+ ---
71
+
72
+ ## Element Overrides
73
+
74
+ A theme is nothing but a block of custom properties scoped to a class:
75
+
76
+ ```css
77
+ .dm-theme-ocean-dark { --dm-primary: #00b4d8; --dm-surface: #1e293b; /* ... */ }
78
+ ```
79
+
80
+ So re-theming *one region* of the page is a scoping problem, not a CSS-authoring one. Declare the same
81
+ properties on a selector and everything inside it follows - including components rendered later, which is
82
+ what hand-written rules always miss.
83
+
84
+ **Content > Theme > Element Overrides** does exactly that. Each override is:
85
+
86
+ | Field | Meaning |
87
+ |-------|---------|
88
+ | Target selector | The region to re-theme, e.g. `#site-navbar`, `.site-content`, `.card` |
89
+ | Adopt a theme here | Copies every token of that theme onto the region |
90
+ | Token overrides | Individual `--dm-*` values, applied after the adopted theme so they win |
91
+ | Custom CSS | Escape hatch, appended verbatim after the generated rule |
92
+
93
+ A light navbar on a dark site is therefore one override, not a stylesheet:
94
+
95
+ ```json
96
+ {
97
+ "label": "Navbar island",
98
+ "scope": "#site-navbar",
99
+ "theme": "ocean-light",
100
+ "tokens": {"--dm-primary": "#0096c7"}
101
+ }
102
+ ```
103
+
104
+ which compiles to a single rule injected into every public page, **before** `content/custom.css` so your
105
+ hand-written CSS is still the last word:
106
+
107
+ ```css
108
+ #site-navbar { color-scheme: light; --dm-background: #ffffff; /* ...every ocean-light token... */
109
+ --dm-primary: #0096c7; }
110
+ ```
111
+
112
+ ### Why tokens rather than rules
113
+
114
+ A rule like `#site-navbar a { color: #fff }` has to win a specificity fight against Domma's own selectors,
115
+ and loses to anything carrying `!important` (Domma's legacy light-mode link hover, for one). A token
116
+ override never enters that fight: it changes the *input* to the rule that was going to win anyway.
117
+
118
+ ### What overrides refuse
119
+
120
+ - `*`, `:root` and `html` as a selector - that is the site theme, not an override
121
+ - Token names that are not `--dm-*` custom properties, and names the shipped stylesheet does not define
122
+ (the admin's picker is built from the stylesheet, so it cannot offer one that does not exist)
123
+ - Token values carrying `;`, `{`, `}`, `<`, `url()`, `@import` or `javascript:` - a value that can close
124
+ its own declaration can write arbitrary CSS into every page
125
+ - An override with a selector but nothing to apply
126
+
127
+ ### Known limit: mode-flipped regions
128
+
129
+ A few Domma component rules key off an **ancestor's** mode rather than tokens - `[data-mode="dark"]
130
+ .alert-info`, and the syntax-highlight palette. Scoping a light theme into a dark page re-colours
131
+ everything token-driven, but those handful of rules still follow the page, because no element can stop an
132
+ ancestor selector from matching. If a mode-flipped region contains alerts or code blocks, add the fix to
133
+ that override's **Custom CSS** box.
134
+
135
+ ---
136
+
137
+ ## Customising via `public/css/site.css`
138
+
139
+ For site-specific overrides - colours, fonts, spacing, or layout tweaks - edit `public/css/site.css`. This file is served as a static asset and loaded on every public page.
140
+
141
+ ```css
142
+ /* Override the primary accent colour */
143
+ :root {
144
+ --dm-color-primary: #e63946;
145
+ --dm-color-primary-hover: #c1121f;
146
+ }
147
+
148
+ /* Adjust the navbar link font size */
149
+ .navbar-link,
150
+ .navbar-dropdown-toggle {
151
+ font-size: 15px;
152
+ }
153
+
154
+ /* Tighten icon size in nav links */
155
+ .navbar-link span[data-icon],
156
+ .navbar-link svg,
157
+ .navbar-dropdown-toggle span[data-icon],
158
+ .navbar-dropdown-toggle svg {
159
+ width: 15px;
160
+ height: 15px;
161
+ margin-right: 10px;
162
+ }
163
+ ```
164
+
165
+ Changes to `site.css` take effect immediately on the next page load (no build step required).
166
+
167
+ You can also manage custom CSS through the admin panel under **Settings → Custom CSS**, which writes to `content/custom.css` - a separate file loaded after `site.css`.
168
+
169
+ ---
170
+
171
+ ## Template Slots
172
+
173
+ The public page HTML shell is at `server/templates/page.html`. It contains two injection slots where the server inserts dynamic content:
174
+
175
+ | Slot | Location | Purpose |
176
+ |---|---|---|
177
+ | `{{headInject}}` | Inside `<head>` | Server-generated `<script>` tags for site config, nav, and plugin head snippets |
178
+ | `{{bodyEndInject}}` | Before `</body>` | Plugin body-end injection snippets |
179
+
180
+ ### What goes into `{{headInject}}`
181
+
182
+ The renderer injects a `<script>` block containing:
183
+
184
+ ```js
185
+ window.__CMS_SITE__ = { title, theme, adminTheme, ... };
186
+ window.__CMS_NAV__ = { brand, items, variant, position };
187
+ window.__CMS_DCONFIG__ = { /* per-page declarative config */ };
188
+ ```
189
+
190
+ These objects are consumed by `public/js/site.js` to initialise the Domma navbar, apply the theme, and wire any declarative page config.
191
+
192
+ If any enabled plugins declare a `inject.head` file in their `plugin.json`, those HTML snippets are also appended inside `{{headInject}}`.
193
+
194
+ ### What goes into `{{bodyEndInject}}`
195
+
196
+ Plugin `inject.bodyEnd` snippets are concatenated here - typically `<script>` tags for client-side analytics, chat widgets, or other integrations.
197
+
198
+ ---
199
+
200
+ ## CSS Custom Properties
201
+
202
+ Domma themes expose a consistent set of custom properties. These are the **real** names - a `var()` on a
203
+ token that does not exist silently resolves to nothing, which is how a popup ends up with no background at
204
+ all. The admin's token picker (Content > Theme) lists every one the installed stylesheet defines, and
205
+ `GET /api/theme/catalog` returns the same list.
206
+
207
+ ```css
208
+ /* Colours - each of primary/secondary/success/warning/danger/info also has
209
+ -hover, -active, -light, -dark, -text and -hover-text variants */
210
+ --dm-primary
211
+ --dm-primary-hover
212
+ --dm-secondary
213
+ --dm-success
214
+ --dm-warning
215
+ --dm-danger
216
+ --dm-info
217
+
218
+ /* Surfaces and text */
219
+ --dm-background
220
+ --dm-background-alt
221
+ --dm-surface
222
+ --dm-surface-raised
223
+ --dm-surface-overlay
224
+ --dm-text
225
+ --dm-text-secondary
226
+ --dm-text-muted
227
+ --dm-text-inverse
228
+ --dm-border
229
+ --dm-border-light
230
+ --dm-border-dark
231
+ --dm-border-focus
232
+
233
+ /* Typography */
234
+ --dm-font-sans
235
+ --dm-font-serif
236
+ --dm-font-mono
237
+ --dm-text-sm
238
+ --dm-text-base
239
+ --dm-text-lg
240
+ --dm-leading-normal
241
+
242
+ /* Spacing */
243
+ --dm-space-1 /* 4px */
244
+ --dm-space-2 /* 8px */
245
+ --dm-space-3 /* 12px */
246
+ --dm-space-4 /* 16px */
247
+ --dm-space-6 /* 24px */
248
+ --dm-space-8 /* 32px */
249
+
250
+ /* Borders and shadows */
251
+ --dm-radius-md
252
+ --dm-radius-lg
253
+ --dm-shadow-md
254
+ --dm-shadow-lg
255
+ ```
256
+
257
+ The theme stylesheet defines them on `.dm-theme-<id>`, which sits on `<body>`. Redeclaring one under any
258
+ selector re-scopes it to that subtree - which is all an element override does.
259
+
260
+ ---
261
+
262
+
263
+ ## Theme Switcher (public)
264
+
265
+ A floating disc of theme dots that lets a visitor pick their own theme. Off by default - a
266
+ visitor-facing theme picker is a choice a site makes.
267
+
268
+ Enable it at **Content → Theme → Theme Switcher**, or in `config/theme.json`:
269
+
270
+ ```json
271
+ "switcher": {
272
+ "enabled": true,
273
+ "position": "right-center",
274
+ "offsetX": 32,
275
+ "offsetY": 32,
276
+ "themes": []
277
+ }
278
+ ```
279
+
280
+ | Key | Default | Notes |
281
+ |-----|---------|-------|
282
+ | `enabled` | `false` | `false` emits no stylesheet, no script and no settings at all |
283
+ | `position` | `right-center` | `right-center`, `left-center`, `bottom-right`, `bottom-left`, `top-right`, `top-left` |
284
+ | `offsetX` / `offsetY` | `32` | Pixels from the anchored edges, 0 to 400 |
285
+ | `themes` | `[]` | **A filter, not a list to maintain.** Empty means every theme the stylesheet ships |
286
+
287
+ Each dot's colours are that theme's own `--dm-primary` and `--dm-secondary`, so a theme added to
288
+ `domma-themes.css` appears automatically. Was the `theme-switcher` plugin until 0.67.0, which
289
+ hardcoded 26 dots against a stylesheet carrying 32 themes.
290
+
291
+ ## Custom Themes (the Theme Roller)
292
+
293
+ A custom theme is a built-in theme plus the `--dm-*` tokens changed on top of it. The **Theme Roller**
294
+ plugin makes them: pick a base, change colours, fonts, spacing and borders with a live preview, and
295
+ save. A saved theme then joins the built-in ones everywhere a theme is chosen - the site theme, the
296
+ admin theme, the day and night themes, and "Adopt a theme here" in an element override - listed under
297
+ **Custom themes** in Content › Theme, or used straight from the Theme Roller's saved-themes menu.
298
+
299
+ How it is applied: Domma's theme engine only knows its built-in themes (`Domma.theme.set()` refuses an
300
+ unknown id and strips every `dm-theme-*` class it did not add), so a custom theme `brand` on
301
+ `ocean-dark` is two classes on `<body>`:
302
+
303
+ ```html
304
+ <body class="dm-theme-ocean-dark dm-custom-brand">
305
+ ```
306
+
307
+ and one generated rule carrying only the changed tokens, emitted after Domma's theme stylesheet so it
308
+ wins on source order:
309
+
310
+ ```css
311
+ .dm-custom-brand { --dm-primary: #1144ff; }
312
+ ```
313
+
314
+ Adopted onto a region by an override, a custom theme carries its base's full token set plus its
315
+ changes. The public **theme switcher** cannot offer custom themes - it switches through Domma's engine.
316
+
317
+ If the plugin providing a theme goes away (disabled, licence lapsed), the page keeps the theme's base:
318
+ config/theme.json remembers each custom theme's base in `customBases` when the config is saved.
319
+
320
+ Plugins provide themes with `hooks.registerThemes()` - see `docs/plugin-development.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.6.60",
3
+ "version": "1.7.0",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -84,7 +84,7 @@
84
84
  "@fastify/rate-limit": "^10.3.0",
85
85
  "@fastify/static": "9.1.1",
86
86
  "bcryptjs": "^3.0.3",
87
- "domma-js": "0.52.3",
87
+ "domma-js": "0.52.4",
88
88
  "dotenv": "^17.2.3",
89
89
  "fastify": "5.8.5",
90
90
  "gray-matter": "^4.0.3",
@@ -6,4 +6,4 @@ export class CanvasManager{constructor(e={}){this.canvasId=e.canvasId||"celebrat
6
6
  height: 100vh;
7
7
  pointer-events: none;
8
8
  z-index: ${this.zIndex};
9
- `,this.ctx=this.canvas.getContext("2d",{alpha:!0}),this.resize(),document.body.appendChild(this.canvas),window.addEventListener("resize",this._handleResize))}resize(){this.canvas&&(this.canvas.width=window.innerWidth,this.canvas.height=window.innerHeight)}clear(){this.ctx&&this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height)}getDimensions(){return{width:this.canvas?.width||0,height:this.canvas?.height||0}}isMobile(){return window.innerWidth<768}destroy(){window.removeEventListener("resize",this._handleResize),this.canvas&&this.canvas.parentNode&&this.canvas.parentNode.removeChild(this.canvas),this.canvas=null,this.ctx=null}_handleResize(){this.resizeTimeout&&clearTimeout(this.resizeTimeout),this.resizeTimeout=setTimeout(()=>{this.resize(),this.resizeCallback&&this.resizeCallback()},250)}}
9
+ `,this.ctx=this.canvas.getContext("2d",{alpha:!0}),this.resize(),document.body.appendChild(this.canvas),window.addEventListener("resize",this._handleResize))}resize(){this.canvas&&(this.canvas.width=window.innerWidth,this.canvas.height=window.innerHeight)}clear(){this.ctx&&this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height)}getDimensions(){return{width:this.canvas?.width||0,height:this.canvas?.height||0}}isMobile(){return window.innerWidth<768}destroy(){this.resizeTimeout&&clearTimeout(this.resizeTimeout),this.resizeTimeout=null,window.removeEventListener("resize",this._handleResize),this.canvas&&this.canvas.parentNode&&this.canvas.parentNode.removeChild(this.canvas),this.canvas=null,this.ctx=null}_handleResize(){this.resizeTimeout&&clearTimeout(this.resizeTimeout),this.resizeTimeout=setTimeout(()=>{this.resize(),this.resizeCallback&&this.resizeCallback()},250)}}
@@ -1 +1 @@
1
- var w=Object.defineProperty;var b=(u,e,i)=>e in u?w(u,e,{enumerable:!0,configurable:!0,writable:!0,value:i}):u[e]=i;var v=(u,e,i)=>b(u,typeof e!="symbol"?e+"":e,i);export{mountCharacter,getCharacters,CHARACTER_SIZES}from"./characters.js";import{resolveCelebrationTraits as M}from"./trait-data.mjs";import{CanvasManager as P}from"./core/canvas.js";import{PhysicsEngine as C,updateMovingParticle as z,updateParticlePhysics as j}from"./core/physics.js";import{createParticle as F}from"./core/particles.js";const c=class c{static isDateInRange(e,i,s,o){const l=new Date,n=l.getMonth()+1,d=l.getDate();return s<e?n===e&&d>=i||n>e||n<e&&n<=s||n===s&&d<=o:(n>e||n===e&&d>=i)&&(n<s||n===s&&d<=o)}static getCurrentTheme(){for(const[e,i]of Object.entries(c.themes)){const[[s,o],[l,n]]=i.dates;if(c.isDateInRange(s,o,l,n))return e}return null}static isCelebrationSeason(){return c.getCurrentTheme()!==null}static getThemes(){return c.themes}constructor(e={}){this.options={theme:e.theme||"auto",intensity:e.intensity||"medium",enabled:e.enabled!==void 0?e.enabled:!0,zIndex:e.zIndex||999,overrides:e.overrides||{}},this.canvasManager=null,this.physicsEngine=null,this.themeModule=null,this.currentTheme=null,this.particles=[],this.specialParticles=[],this.animationFrame=null,this.lastTime=0,this.running=!1,this.initialized=!1}async init(){if(this.initialized){console.warn("[Celebrations] Already initialized");return}console.log("[Celebrations] Initializing...");let e=this.options.theme;if(e==="auto"&&(e=c.getCurrentTheme(),!e)){console.log("[Celebrations] No active celebration detected");return}if(!c.themes[e]){console.error(`[Celebrations] Unknown theme: ${e}`);return}this.currentTheme=e,console.log(`[Celebrations] Loading theme: ${e}`);try{const s=await import(c.themes[e].module);this.themeModule=s.default,console.log(`[Celebrations] Theme loaded: ${this.themeModule.displayName}`)}catch(i){console.error(`[Celebrations] Failed to load theme module: ${i.message}`);return}this.canvasManager=new P({canvasId:"celebrations-canvas",zIndex:this.options.zIndex}),this.canvasManager.create(),this.physicsEngine=new C({gustInterval:[5e3,15e3],gustStrength:[-2,2]}),window.addEventListener("resize",()=>{this.canvasManager.resize(),this.resetParticles()}),this.resetParticles(),this.initialized=!0,console.log("[Celebrations] Initialized successfully"),this.options.enabled&&this.start()}isParticleEnabled(e){return!this.disabledTypes?.has(e.type)}resetParticles(){const e=Object.assign({},this.themeModule.intensityConfig[this.options.intensity]),i=a=>String(a).toLowerCase().replace(/[^a-z0-9]/g,""),s=[...Object.keys(e),...this.themeModule.decorationKeys||[]],o=a=>s.find(r=>i(r)===i(a))||a,l=a=>["true","1","yes","on"].includes(String(a).toLowerCase()),n=this.themeModule.optInDecorations||{},d=Object.entries(this.options.overrides).some(([a,r])=>["snowonly","fallingonly"].includes(i(a))&&l(r));for(const a of Object.values(n))e[a.chance]=0;for(const[a,r]of Object.entries(this.options.overrides)){if((i(a)==="snowonly"||i(a)==="fallingonly")&&l(r)){for(const m of this.themeModule.decorationKeys||[])e[m]=0;continue}const p=Object.keys(n).find(m=>i(m)===i(a));if(p&&!d&&l(r)){const m=n[p];e[m.chance]=m.value}r==="off"&&(e[o(a)]=0)}const t=M(this.themeModule.name,this.options.overrides);Object.assign(e,t.config),this.disabledTypes=t.disabledTypes,this.disabledGlobals=t.disabledGlobals,this.config=e;const f=this.canvasManager.canvas.width,h=this.canvasManager.canvas.height;this.particles=[],this.specialParticles=[];const g=e.initialParticleRatio!==void 0?e.initialParticleRatio:1,y=Math.floor(e.count*g);if(this.themeModule.createFallingParticle)for(let a=0;a<y;a++){const r=this.themeModule.createFallingParticle(f,h,e);r.y=Math.random()*h,this.isParticleEnabled(r)&&this.particles.push(r)}else for(let a=0;a<y;a++){const r=F(e,f,h);r.y=Math.random()*h,this.isParticleEnabled(r)&&this.particles.push(r)}if(this.themeModule.createInitialDecorations){const a=this.themeModule.createInitialDecorations(f,h,e);this.specialParticles.push(...a.filter(r=>this.isParticleEnabled(r))),console.log(`[Celebrations] Created ${a.length} decorations`)}console.log(`[Celebrations] Created ${y}/${e.count} particles (${this.options.intensity}, ${Math.round(g*100)}% initial)`)}start(){if(!this.initialized){console.warn("[Celebrations] Cannot start - not initialized");return}if(this.running){console.warn("[Celebrations] Already running");return}console.log("[Celebrations] Starting animation"),this.running=!0,this.lastTime=performance.now(),this.animate()}pause(){this.running&&(console.log("[Celebrations] Pausing animation"),this.running=!1,this.animationFrame&&(cancelAnimationFrame(this.animationFrame),this.animationFrame=null))}resume(){this.running||this.start()}destroy(){console.log("[Celebrations] Destroying effect"),this.pause(),this.canvasManager&&this.canvasManager.canvas&&this.canvasManager.canvas.remove(),this.particles=[],this.specialParticles=[],this.canvasManager=null,this.physicsEngine=null,this.themeModule=null,this.initialized=!1}setIntensity(e){if(!["light","medium","heavy"].includes(e)){console.error(`[Celebrations] Invalid intensity: ${e}`);return}console.log(`[Celebrations] Changing intensity to: ${e}`),this.options.intensity=e,this.resetParticles()}async setTheme(e){console.log(`[Celebrations] Changing theme to: ${e}`);const i=this.running;this.pause(),await new Promise(s=>setTimeout(s,20)),this.initialized=!1,this.canvasManager&&this.canvasManager.clear(),this.particles=[],this.specialParticles=[],this.canvasManager&&this.canvasManager.clear(),this.options.theme=e,await this.init(),i&&this.start()}animate(){if(!this.running)return;const e=performance.now(),i=e-this.lastTime;this.lastTime=e,this.canvasManager.clear();const s=this.canvasManager.ctx,o=this.canvasManager.canvas.width,l=this.canvasManager.canvas.height,n=this.config;this.physicsEngine.updateWind(e);const d=this.physicsEngine.getWindForce();if(this.particles.length<n.count&&Math.random()<.05&&this.themeModule.createFallingParticle){const t=this.themeModule.createFallingParticle(o,l,n);(t.y===void 0||t.y<0)&&(t.y=-20),this.isParticleEnabled(t)&&this.particles.push(t)}if(this.particles.forEach((t,f)=>{if(j(t,i,d),t.y>l+50&&(t.y=-20,t.x=Math.random()*o),t.x<-50&&(t.x=o+50),t.x>o+50&&(t.x=-50),this.themeModule.drawSnowflake&&t.type==="snowflake")this.themeModule.drawSnowflake(s,t);else if(this.themeModule["draw"+this.capitalizeFirst(t.type)]){const h="draw"+this.capitalizeFirst(t.type);this.themeModule[h](s,t,e)}else s.save(),s.globalAlpha=t.opacity,s.fillStyle=this.themeModule.colors?.primary||"#ffffff",s.beginPath(),s.arc(t.x,t.y,t.size,0,Math.PI*2),s.fill(),s.restore()}),this.themeModule.updateSpecialParticles&&this.themeModule.updateSpecialParticles(this.specialParticles,i,o,l),this.specialParticles=this.specialParticles.filter(t=>{if(!t.active||!this.isParticleEnabled(t))return!1;if(!t.static){z(t,i,e);const g=t.type==="train"?600:200;if(t.x<-g||t.x>o+g)return t.active=!1,!1}if(!isFinite(t.x)||!isFinite(t.y)||t.size&&!isFinite(t.size))return console.warn("[Celebrations] Invalid particle values, removing:",t.type),t.active=!1,!1;const h="draw"+this.capitalizeFirst(t.type);return this.themeModule[h]&&this.themeModule[h](s,t,e),!0}),this.themeModule.spawnSpecialParticle){const t=this.themeModule.spawnSpecialParticle(this.specialParticles,o,l,n);t&&this.isParticleEnabled(t)&&this.specialParticles.push(t)}this.themeModule.drawGlobalEffects&&!this.disabledGlobals?.has("lightning")&&this.themeModule.drawGlobalEffects(s,e,o,l),this.animationFrame=requestAnimationFrame(()=>this.animate())}capitalizeFirst(e){if(!e)return"";const i=e.replace(/-([a-z])/g,(s,o)=>o.toUpperCase());return i.charAt(0).toUpperCase()+i.slice(1)}};v(c,"themes",{christmas:{module:"./themes/christmas.js?v=20261003-characters",dates:[[12,1],[1,1]],displayName:"Christmas",emoji:"\u{1F384}"},halloween:{module:"./themes/halloween.js?v=20261003-characters",dates:[[10,26],[10,31]],displayName:"Halloween",emoji:"\u{1F383}"},valentines:{module:"./themes/valentines.js?v=20261003-characters",dates:[[2,9],[2,14]],displayName:"Valentine's Day",emoji:"\u{1F495}"},"guy-fawkes":{module:"./themes/guy-fawkes.js?v=20261003-characters",dates:[[11,1],[11,5]],displayName:"Guy Fawkes Night",emoji:"\u{1F386}"},"st-patricks":{module:"./themes/st-patricks.js?v=20261003-characters",dates:[[3,12],[3,17]],displayName:"St Patrick's Day",emoji:"\u2618\uFE0F"},"st-andrews":{module:"./themes/st-andrews.js?v=20261003-characters",dates:[[11,25],[11,30]],displayName:"St Andrew's Day",emoji:"\u{1F3F4}\u{E0067}\u{E0062}\u{E0073}\u{E0063}\u{E0074}\u{E007F}"},"st-davids":{module:"./themes/st-davids.js?v=20261003-characters",dates:[[2,24],[3,1]],displayName:"St David's Day",emoji:"\u{1F3F4}\u{E0067}\u{E0062}\u{E0077}\u{E006C}\u{E0073}\u{E007F}"},"st-georges":{module:"./themes/st-georges.js?v=20261003-characters",dates:[[4,18],[4,23]],displayName:"St George's Day",emoji:"\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}"}});export let CelebrationsEffect=c;export default CelebrationsEffect;
1
+ var M=Object.defineProperty;var b=(d,e,s)=>e in d?M(d,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):d[e]=s;var v=(d,e,s)=>b(d,typeof e!="symbol"?e+"":e,s);export{mountCharacter,getCharacters,CHARACTER_SIZES}from"./characters.js";import{launchMakeItRain as w}from"./themes/rain-control.js";import{isEasterSeason as P,isChineseNewYearSeason as j}from"./themes/calendar.js";import{resolveCelebrationTraits as C}from"./trait-data.mjs";import{CanvasManager as k}from"./core/canvas.js";import{PhysicsEngine as N,updateMovingParticle as S,updateParticlePhysics as I}from"./core/physics.js";import{createParticle as z}from"./core/particles.js";const h=class h{static isDateInRange(e,s,i,n,l=new Date){const o=l.getMonth()+1,c=l.getDate();return i<e?o===e&&c>=s||o>e||o<i||o===i&&c<=n:(o>e||o===e&&c>=s)&&(o<i||o===i&&c<=n)}static getCurrentTheme(e=new Date){for(const[s,i]of Object.entries(h.themes)){if(i.isInSeason){if(i.isInSeason(e))return s;continue}if(!i.dates)continue;const[[n,l],[o,c]]=i.dates;if(h.isDateInRange(n,l,o,c,e))return s}return null}static isCelebrationSeason(){return h.getCurrentTheme()!==null}static getThemes(){return h.themes}constructor(e={}){this.options={theme:e.theme||(e.message?"greeting":"auto"),intensity:e.intensity||"medium",enabled:e.enabled!==void 0?e.enabled:!0,zIndex:e.zIndex||999,overrides:{...e.overrides||{},...typeof e.message=="string"?{message:e.message}:{}}},this.canvasManager=null,this.physicsEngine=null,this.themeModule=null,this.currentTheme=null,this.particles=[],this.specialParticles=[],this.animationFrame=null,this.lastTime=0,this.running=!1,this.initialized=!1}async init(){if(this.initialized){console.warn("[Celebrations] Already initialized");return}console.log("[Celebrations] Initializing...");let e=this.options.theme;if(e==="auto"&&this.options.overrides.message&&(e="greeting"),e==="auto"&&(e=h.getCurrentTheme(),!e)){console.log("[Celebrations] No active celebration detected");return}if(!h.themes[e]){console.error(`[Celebrations] Unknown theme: ${e}`);return}this.currentTheme=e,console.log(`[Celebrations] Loading theme: ${e}`);try{const i=await import(h.themes[e].module);this.themeModule=i.default;const n=this.options.overrides.message||this.themeModule.defaultMessage;if(n){const{prepareSparklerFont:l}=await import("./themes/sparkler-font.js");await l(n)}console.log(`[Celebrations] Theme loaded: ${this.themeModule.displayName}`)}catch(s){console.error(`[Celebrations] Failed to load theme module: ${s.message}`);return}this.canvasManager=new k({canvasId:"celebrations-canvas",zIndex:this.options.zIndex,onResize:()=>{this.initialized&&this.resetParticles()}}),this.canvasManager.create(),this.physicsEngine=new N({gustInterval:[5e3,15e3],gustStrength:[-2,2]}),this.resetParticles(),this.initialized=!0,console.log("[Celebrations] Initialized successfully"),this.options.enabled&&this.start()}isParticleEnabled(e){return!this.disabledTypes?.has(e.type)}resetParticles(){const e=Object.assign({},this.themeModule.intensityConfig[this.options.intensity]);e.sparkler={message:this.options.overrides.message,style:this.options.overrides["sparkler-style"],colour:this.options.overrides["sparkler-colour"]||this.options.overrides["sparkler-color"],greeting:this.options.overrides["sparkler-greeting"],year:this.options.overrides["sparkler-year"]};const s=a=>String(a).toLowerCase().replace(/[^a-z0-9]/g,""),i=[...Object.keys(e),...this.themeModule.decorationKeys||[]],n=a=>i.find(r=>s(r)===s(a))||a,l=a=>["true","1","yes","on"].includes(String(a).toLowerCase()),o=this.themeModule.optInDecorations||{},c=Object.entries(this.options.overrides).some(([a,r])=>["snowonly","fallingonly"].includes(s(a))&&l(r));for(const a of Object.values(o))e[a.chance]=0;for(const[a,r]of Object.entries(this.options.overrides)){if((s(a)==="snowonly"||s(a)==="fallingonly")&&l(r)){for(const u of this.themeModule.decorationKeys||[])e[u]=0;continue}const p=Object.keys(o).find(u=>s(u)===s(a));if(p&&!c&&l(r)){const u=o[p];e[u.chance]=u.value}r==="off"&&(e[n(a)]=0)}const t=C(this.themeModule.name,this.options.overrides);Object.assign(e,t.config),this.disabledTypes=t.disabledTypes,this.disabledGlobals=t.disabledGlobals,this.config=e;const g=this.canvasManager.canvas.width,m=this.canvasManager.canvas.height;this.particles=[],this.specialParticles=[];const f=e.initialParticleRatio!==void 0?e.initialParticleRatio:1,y=Math.floor(e.count*f);if(this.themeModule.createFallingParticle)for(let a=0;a<y;a++){const r=this.themeModule.createFallingParticle(g,m,e);r.y=Math.random()*m,this.isParticleEnabled(r)&&this.particles.push(r)}else for(let a=0;a<y;a++){const r=z(e,g,m);r.y=Math.random()*m,this.isParticleEnabled(r)&&this.particles.push(r)}if(this.themeModule.createInitialDecorations){const a=this.themeModule.createInitialDecorations(g,m,e);this.specialParticles.push(...a.filter(r=>this.isParticleEnabled(r))),console.log(`[Celebrations] Created ${a.length} decorations`)}console.log(`[Celebrations] Created ${y}/${e.count} particles (${this.options.intensity}, ${Math.round(f*100)}% initial)`)}start(){if(!this.initialized){console.warn("[Celebrations] Cannot start - not initialized");return}if(this.running){console.warn("[Celebrations] Already running");return}console.log("[Celebrations] Starting animation"),this.running=!0,this.lastTime=performance.now(),this.animate()}pause(){this.running&&(console.log("[Celebrations] Pausing animation"),this.running=!1,this.animationFrame&&(cancelAnimationFrame(this.animationFrame),this.animationFrame=null))}resume(){this.running||this.start()}destroy(){console.log("[Celebrations] Destroying effect"),this.pause(),this.canvasManager&&this.canvasManager.destroy(),this.particles=[],this.specialParticles=[],this.canvasManager=null,this.physicsEngine=null,this.themeModule=null,this.initialized=!1}setIntensity(e){if(!["light","medium","heavy"].includes(e)){console.error(`[Celebrations] Invalid intensity: ${e}`);return}console.log(`[Celebrations] Changing intensity to: ${e}`),this.options.intensity=e,this.resetParticles()}async setTheme(e){console.log(`[Celebrations] Changing theme to: ${e}`);const s=this.running;this.pause(),await new Promise(i=>setTimeout(i,20)),this.initialized=!1,this.canvasManager&&this.canvasManager.clear(),this.particles=[],this.specialParticles=[],this.canvasManager&&this.canvasManager.clear(),this.options.theme=e,await this.init(),s&&this.start()}animate(){if(!this.running)return;const e=performance.now(),s=e-this.lastTime;this.lastTime=e,this.canvasManager.clear();const i=this.canvasManager.ctx,n=this.canvasManager.canvas.width,l=this.canvasManager.canvas.height,o=this.config;this.physicsEngine.updateWind(e);const c=this.physicsEngine.getWindForce();if(this.particles.length<o.count&&Math.random()<.05&&this.themeModule.createFallingParticle){const t=this.themeModule.createFallingParticle(n,l,o);(t.y===void 0||t.y<0)&&(t.y=-20),this.isParticleEnabled(t)&&this.particles.push(t)}if(this.particles.forEach((t,g)=>{if(I(t,s,c),t.y>l+50&&(t.y=-20,t.x=Math.random()*n),t.x<-50&&(t.x=n+50),t.x>n+50&&(t.x=-50),this.themeModule.drawSnowflake&&t.type==="snowflake")this.themeModule.drawSnowflake(i,t);else if(this.themeModule["draw"+this.capitalizeFirst(t.type)]){const m="draw"+this.capitalizeFirst(t.type);this.themeModule[m](i,t,e)}else i.save(),i.globalAlpha=t.opacity,i.fillStyle=this.themeModule.colors?.primary||"#ffffff",i.beginPath(),i.arc(t.x,t.y,t.size,0,Math.PI*2),i.fill(),i.restore()}),this.themeModule.updateSpecialParticles&&this.themeModule.updateSpecialParticles(this.specialParticles,s,n,l),this.specialParticles=this.specialParticles.filter(t=>{if(!t.active||!this.isParticleEnabled(t))return!1;if(!t.static){S(t,s,e);const f=t.type==="train"?600:200;if(t.x<-f||t.x>n+f)return t.active=!1,!1}if(!isFinite(t.x)||!isFinite(t.y)||t.size&&!isFinite(t.size))return console.warn("[Celebrations] Invalid particle values, removing:",t.type),t.active=!1,!1;const m="draw"+this.capitalizeFirst(t.type);return this.themeModule[m]&&this.themeModule[m](i,t,e),!0}),this.themeModule.spawnSpecialParticle){const t=this.themeModule.spawnSpecialParticle(this.specialParticles,n,l,o);t&&this.isParticleEnabled(t)&&this.specialParticles.push(t)}this.themeModule.drawGlobalEffects&&!this.disabledGlobals?.has("lightning")&&this.themeModule.drawGlobalEffects(i,e,n,l),this.animationFrame=requestAnimationFrame(()=>this.animate())}capitalizeFirst(e){if(!e)return"";const s=e.replace(/-([a-z])/g,(i,n)=>n.toUpperCase());return s.charAt(0).toUpperCase()+s.slice(1)}};v(h,"themes",{"chinese-new-year":{module:"./themes/chinese-new-year.js?v=20261015-occasions",dates:null,isInSeason:j,displayName:"Chinese New Year",emoji:"\u{1F409}"},"new-year":{module:"./themes/new-year.js?v=20261015-occasions",dates:[[12,31],[1,2]],displayName:"New Year",emoji:"\u{1F389}"},easter:{module:"./themes/easter.js?v=20261015-occasions",dates:null,isInSeason:P,displayName:"Easter",emoji:"\u{1F423}"},"summer-garden":{module:"./themes/summer-garden.js?v=20261015-occasions",dates:[[6,1],[8,31]],displayName:"Summer Garden",emoji:"\u{1F33C}"},christmas:{module:"./themes/christmas.js?v=20261015-occasions",dates:[[12,1],[12,30]],displayName:"Christmas",emoji:"\u{1F384}"},halloween:{module:"./themes/halloween.js?v=20261003-characters",dates:[[10,26],[10,31]],displayName:"Halloween",emoji:"\u{1F383}"},valentines:{module:"./themes/valentines.js?v=20261003-characters",dates:[[2,9],[2,14]],displayName:"Valentine's Day",emoji:"\u{1F495}"},"guy-fawkes":{module:"./themes/guy-fawkes.js?v=20261003-characters",dates:[[11,1],[11,5]],displayName:"Guy Fawkes Night",emoji:"\u{1F386}"},"st-patricks":{module:"./themes/st-patricks.js?v=20261003-characters",dates:[[3,12],[3,17]],displayName:"St Patrick's Day",emoji:"\u2618\uFE0F"},"st-andrews":{module:"./themes/st-andrews.js?v=20261003-characters",dates:[[11,25],[11,30]],displayName:"St Andrew's Day",emoji:"\u{1F3F4}\u{E0067}\u{E0062}\u{E0073}\u{E0063}\u{E0074}\u{E007F}"},"st-davids":{module:"./themes/st-davids.js?v=20261003-characters",dates:[[2,24],[3,1]],displayName:"St David's Day",emoji:"\u{1F3F4}\u{E0067}\u{E0062}\u{E0077}\u{E006C}\u{E0073}\u{E007F}"},"st-georges":{module:"./themes/st-georges.js?v=20261003-characters",dates:[[4,18],[4,23]],displayName:"St George's Day",emoji:"\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}"}}),(()=>{h.themes.winter={module:"./themes/winter.js?v=20261015-occasions",dates:[[12,1],[2,28]],displayName:"Winter",emoji:"\u2744\uFE0F"},h.themes["make-it-rain"]={module:"./themes/make-it-rain.js?v=20261015-occasions",dates:null,displayName:"Make it rain",emoji:"\u{1F327}\uFE0F"},h.themes["money-rain"]={module:"./themes/money-rain.js?v=20261015-occasions",dates:null,displayName:"Money Rain",emoji:"\u{1F4B8}"};for(const[e,s,i]of[["graduation","Graduation","\u{1F393}"],["birthday","Birthday","\u{1F382}"],["anniversary","Anniversary","\u{1F48D}"],["greeting","Sparkler greeting","\u2728"]])h.themes[e]={module:`./themes/${e}.js?v=20261015-occasions`,dates:null,displayName:s,emoji:i};h.themes.autumn={module:"./themes/autumn.js?v=20261015-occasions",dates:[[9,1],[11,30]],displayName:"Autumn",emoji:"\u{1F342}"}})();export let CelebrationsEffect=h;export default CelebrationsEffect;export function makeItRain(d={}){return w(CelebrationsEffect,d)}export function moneyRain(d={}){return w(CelebrationsEffect,d,"money-rain")}