domma-cms 1.6.31 → 1.6.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/js/lib/effect-defs.js +1 -1
- package/admin/js/lib/effects-builder.js +2 -2
- package/admin/js/lib/favicon-generator.js +1 -0
- package/admin/js/lib/markdown-toolbar.js +9 -9
- package/admin/js/lib/page-comments.js +9 -0
- package/admin/js/lib/page-editor-reference.js +1 -1
- package/admin/js/lib/scribe-composer.js +1 -1
- package/admin/js/lib/scribe-model.js +1 -1
- package/admin/js/lib/shortcode-preview.js +1 -1
- package/admin/js/lib/site-settings-arrange.js +1 -1
- package/admin/js/lib/syntax-highlight.js +1 -1
- package/admin/js/templates/settings.html +25 -7
- package/admin/js/views/settings.js +4 -4
- package/package.json +1 -1
- package/plugins/free-tier.lock.json +19 -19
- package/plugins/shopping-cart/CLAUDE.md +25 -0
- package/plugins/shopping-cart/admin/css/index.css +2 -0
- package/plugins/shopping-cart/admin/lib/kit.js +41 -0
- package/plugins/shopping-cart/admin/views/orders.js +11 -2
- package/plugins/shopping-cart/admin/views/products.js +19 -4
- package/plugins/shopping-cart/admin/views/shop.js +1 -1
- package/plugins/shopping-cart/docs/guide.md +2 -0
- package/plugins/shopping-cart/inject/body.html +1 -1
- package/plugins/shopping-cart/lib/extensions.js +71 -0
- package/plugins/shopping-cart/lib/render.js +6 -2
- package/plugins/shopping-cart/lib/shop.js +36 -12
- package/plugins/shopping-cart/plugin.js +19 -5
- package/plugins/shopping-cart/plugin.json +3 -3
- package/plugins/shopping-cart/plugin.public.js +2 -1
- package/plugins/shopping-cart/public/shop.css +12 -0
- package/plugins/shopping-cart/tests/api.test.js +51 -0
- package/plugins/shopping-cart/tests/pure.test.js +49 -0
- package/public/js/effects.js +1 -1
- package/public/js/scribe-style.mjs +1 -0
- package/public/js/site.js +1 -1
- package/scripts/run-tests.mjs +4 -0
- package/server/routes/api/pages.js +3 -3
- package/server/routes/api/settings.js +20 -1
- package/server/routes/api/versions.js +4 -3
- package/server/services/favicon.js +33 -0
- package/server/services/markdown.js +17 -5
- package/server/services/versions.js +10 -0
- package/server/templates/page.html +3 -3
|
@@ -17,6 +17,7 @@ import {getPluginSettings, getPluginStates, savePluginState} from '../../../serv
|
|
|
17
17
|
import {cleanProduct} from '../public/lib/products.js';
|
|
18
18
|
import {NEXT, SOLD, canMove} from '../public/lib/orders.js';
|
|
19
19
|
import {currencyOf} from '../public/lib/money.js';
|
|
20
|
+
import {cartRegistry} from './extensions.js';
|
|
20
21
|
import defaults from '../config.js';
|
|
21
22
|
|
|
22
23
|
export const NAME = 'shopping-cart';
|
|
@@ -174,22 +175,45 @@ async function uniqueSlug(slug, exceptId = null) {
|
|
|
174
175
|
return `${slug}-${Date.now()}`;
|
|
175
176
|
}
|
|
176
177
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
178
|
+
/**
|
|
179
|
+
* Save a product. Under the order lock: a sale's stock and an extension's
|
|
180
|
+
* `updateProductExt` (licence keys taken) never interleave with an edit.
|
|
181
|
+
* `variantMap` (a copy) is old option id -> the copy's, for extensions' parts.
|
|
182
|
+
*/
|
|
183
|
+
export function saveProduct(input, {id = null, currency = 'GBP', by = null, copy = false, variantMap = null} = {}) {
|
|
184
|
+
return withLock(async () => {
|
|
185
|
+
const had = id ? await getProduct(id) : null;
|
|
186
|
+
if (id && !had) return {notFound: true};
|
|
187
|
+
const {data, errors} = cleanProduct(input, {currency, minor: true});
|
|
188
|
+
// What extensions keep on a product (Shopping Cart Pro's downloads), each checked by its own extension.
|
|
189
|
+
const x = await cartRegistry().cleanProductExt(input?.ext, copy ? input?.ext : had?.data?.ext, {copy, product: data, variantMap});
|
|
190
|
+
Object.assign(errors, x.errors);
|
|
191
|
+
if (Object.keys(errors).length) return {errors};
|
|
192
|
+
if (Object.keys(x.ext).length) data.ext = x.ext;
|
|
193
|
+
data.slug = await uniqueSlug(data.slug, id);
|
|
194
|
+
const entry = id ? await updateEntry(PRODUCTS, id, data) : await createEntry(PRODUCTS, data, {createdBy: by, source: 'plugin'});
|
|
185
195
|
await refreshShelf();
|
|
186
196
|
return {entry};
|
|
187
|
-
}
|
|
188
|
-
const entry = await createEntry(PRODUCTS, data, {createdBy: by, source: 'plugin'});
|
|
189
|
-
await refreshShelf();
|
|
190
|
-
return {entry};
|
|
197
|
+
});
|
|
191
198
|
}
|
|
192
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Change only an extension's part of a product, under the order lock, from
|
|
202
|
+
* the product as it is now: `fn(part) -> part` (null removes it). No form
|
|
203
|
+
* check - the extension's own write (Shopping Cart Pro taking licence keys).
|
|
204
|
+
*/
|
|
205
|
+
export const updateProductExt = (id, owner, fn) => withLock(async () => {
|
|
206
|
+
const had = await getProduct(id);
|
|
207
|
+
if (!had) throw Object.assign(new Error('No such product.'), {status: 404});
|
|
208
|
+
const next = await fn(had.data.ext?.[owner] ?? null);
|
|
209
|
+
const ext = {...(had.data.ext || {})};
|
|
210
|
+
if (next === null || next === undefined) delete ext[owner];
|
|
211
|
+
else ext[owner] = next;
|
|
212
|
+
const entry = await updateEntry(PRODUCTS, id, {...had.data, ext});
|
|
213
|
+
await refreshShelf();
|
|
214
|
+
return entry;
|
|
215
|
+
});
|
|
216
|
+
|
|
193
217
|
export async function removeProduct(id) {
|
|
194
218
|
await deleteEntry(PRODUCTS, id);
|
|
195
219
|
await refreshShelf();
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Admin (the `shop` permission; read to look, manage to change):
|
|
5
5
|
* GET /summary Overview figures
|
|
6
6
|
* GET /count sidebar badge: paid orders to fulfil (else how many products)
|
|
7
|
-
* GET /extensions admin tabs extensions (Pro) add
|
|
7
|
+
* GET /extensions admin tabs and panels extensions (Pro) add
|
|
8
8
|
* GET /products · POST /products · PUT /products/:id · DELETE /products/:id
|
|
9
9
|
* POST /products/:id/duplicate · PUT /products/:id/stock
|
|
10
10
|
* GET /orders · GET /orders/:id · PUT /orders/:id/status · PUT /orders/:id/note
|
|
@@ -34,7 +34,7 @@ import * as pay from './lib/payments.js';
|
|
|
34
34
|
import {
|
|
35
35
|
NAME, allOrders, allProducts, annotateOrder, cleanSettings, cleanTracking, createOrder, ensureCollections, extendOrder,
|
|
36
36
|
getOrder, getProduct, liveSettings, markPaid, maskSettings, methodsOf, moveOrder, publicOrder, removeOrder,
|
|
37
|
-
refreshShelf, removeProduct, saveProduct, saveSettings, setProviderRef, settingsNow, shelfNow, tokenOk
|
|
37
|
+
refreshShelf, removeProduct, saveProduct, saveSettings, setProviderRef, settingsNow, shelfNow, tokenOk, updateProductExt
|
|
38
38
|
} from './lib/shop.js';
|
|
39
39
|
import {cardHtml, esc, gridHtml} from './lib/render.js';
|
|
40
40
|
|
|
@@ -177,6 +177,14 @@ export default async function shoppingCart(fastify, options) {
|
|
|
177
177
|
liveSettings, allOrders, getOrder, allProducts, getProduct, extendOrder, publicOrder,
|
|
178
178
|
formatMoney, orderLabel,
|
|
179
179
|
siteBase: () => siteBase(null),
|
|
180
|
+
/** Change only an extension's part of a product, under the order lock (lib/shop.js updateProductExt). */
|
|
181
|
+
updateProductExt,
|
|
182
|
+
/** Move an order on (Shopping Cart Pro fulfils an order with nothing to post), telling every extension. */
|
|
183
|
+
async moveOrder(id, to, {by = 'shop', note = ''} = {}) {
|
|
184
|
+
const {order, from} = await moveOrder(id, String(to), {by, note});
|
|
185
|
+
announceMove(order, from, order.data.status);
|
|
186
|
+
return order;
|
|
187
|
+
},
|
|
180
188
|
receiptUrl: (order) => liveSettings().then(s => `${siteBase(null)}${receiptPath(s, order)}`)
|
|
181
189
|
};
|
|
182
190
|
|
|
@@ -199,6 +207,7 @@ export default async function shoppingCart(fastify, options) {
|
|
|
199
207
|
|
|
200
208
|
fastify.get('/extensions', can('read'), async () => ({
|
|
201
209
|
tabs: registry.tabs(),
|
|
210
|
+
panels: registry.panels(),
|
|
202
211
|
extensions: registry.list().map(e => ({owner: e.owner, label: e.label}))
|
|
203
212
|
}));
|
|
204
213
|
|
|
@@ -237,7 +246,9 @@ export default async function shoppingCart(fastify, options) {
|
|
|
237
246
|
const patch = {};
|
|
238
247
|
for (const k of ['status', 'featured', 'stock', 'trackStock', 'position', 'variants']) if (b[k] !== undefined) patch[k] = b[k];
|
|
239
248
|
const s = await liveSettings();
|
|
240
|
-
|
|
249
|
+
// An extension's part of the product (ext) is left as it is: no quick edit touches it.
|
|
250
|
+
const {ext, ...data} = had.data;
|
|
251
|
+
const r = await saveProduct({...data, ...patch}, {id: had.id, currency: s.currency});
|
|
241
252
|
if (r.errors) return fail(reply, 400, Object.values(r.errors)[0], {errors: r.errors});
|
|
242
253
|
return r.entry;
|
|
243
254
|
});
|
|
@@ -247,8 +258,11 @@ export default async function shoppingCart(fastify, options) {
|
|
|
247
258
|
if (!had) return fail(reply, 404, 'That product is gone.');
|
|
248
259
|
const s = await liveSettings();
|
|
249
260
|
// A copy is the shop's own product, even of an extension's: listed, and nobody else's to change.
|
|
250
|
-
|
|
251
|
-
|
|
261
|
+
// Its options get new ids; extensions are told which old one became which (a file for one option).
|
|
262
|
+
const variantMap = {};
|
|
263
|
+
const variants = (had.data.variants || []).map(v => { variantMap[v.id] = Math.random().toString(36).slice(2, 10); return {...v, id: variantMap[v.id]}; });
|
|
264
|
+
const r = await saveProduct({...had.data, name: `${had.data.name} (copy)`, slug: '', status: 'draft', unlisted: false, owner: '', variants},
|
|
265
|
+
{currency: s.currency, by: request.user?.id, copy: true, variantMap});
|
|
252
266
|
if (r.errors) return fail(reply, 400, Object.values(r.errors)[0]);
|
|
253
267
|
return reply.code(201).send(r.entry);
|
|
254
268
|
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shopping-cart",
|
|
3
3
|
"displayName": "Shopping Cart",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"tier": "free",
|
|
6
6
|
"description": "Sell from your site: products with options, stock and pictures, a storefront and a cart that follows shoppers from page to page, and checkout with Stripe, PayPal or payment offline. Orders to fulfil in the sidebar, refunds from the order.",
|
|
7
7
|
"author": "Domma CMS",
|
|
8
|
-
"date": "2026-
|
|
8
|
+
"date": "2026-10-12",
|
|
9
9
|
"icon": "shopping-bag",
|
|
10
10
|
"permissions": [
|
|
11
11
|
{
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
],
|
|
46
46
|
"views": {
|
|
47
47
|
"plugin-shopping-cart": {
|
|
48
|
-
"entry": "shopping-cart/admin/views/shop.js?v=1.0
|
|
48
|
+
"entry": "shopping-cart/admin/views/shop.js?v=1.2.0",
|
|
49
49
|
"exportName": "shopView"
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -118,9 +118,10 @@ export default async function shopPublic(fastify) {
|
|
|
118
118
|
const confirm = cartRegistry().services?.confirmStripe;
|
|
119
119
|
if (confirm) entry = await confirm(entry, request.query?.session_id, request.log);
|
|
120
120
|
const order = publicOrder(entry);
|
|
121
|
+
const parts = await cartRegistry().receiptParts(entry, {settings: s});
|
|
121
122
|
return send(reply, request, s, {
|
|
122
123
|
page: 'order',
|
|
123
|
-
body: receiptHtml({order, settings: s, base: s.shopPath, problem: String(request.query?.problem || '')}),
|
|
124
|
+
body: receiptHtml({order, settings: s, base: s.shopPath, problem: String(request.query?.problem || ''), parts}),
|
|
124
125
|
title: `Order #${order.number}`,
|
|
125
126
|
urlPath: `${s.shopPath}/order/${id}`,
|
|
126
127
|
noindex: true,
|
|
@@ -300,6 +300,17 @@ body {
|
|
|
300
300
|
@keyframes shop-spin { to { transform: rotate(360deg); } }
|
|
301
301
|
.shop-r-note { border-radius: var(--shop-radius-sm); padding: 1rem 1.2rem; background: var(--shop-softer); border: 1px solid var(--shop-line); margin: 0 0 1.25rem; }
|
|
302
302
|
.shop-r-note p { margin: .35rem 0 0; }
|
|
303
|
+
.shop-r-ext { margin: 0 0 1.25rem; }
|
|
304
|
+
.shop-r-ext > :last-child { margin-bottom: 0; }
|
|
305
|
+
/* Files and keys on the receipt (Shopping Cart Pro's downloads). */
|
|
306
|
+
.shop-r-files { list-style: none; padding: 0; margin: .6rem 0 0; display: grid; gap: .6rem; }
|
|
307
|
+
.shop-r-files li { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
|
|
308
|
+
.shop-r-fname { flex: 1 1 12rem; font-weight: 600; }
|
|
309
|
+
.shop-r-fname small { display: block; color: var(--shop-muted); font-weight: 400; }
|
|
310
|
+
.shop-r-files code { padding: .15rem .45rem; border-radius: 6px; background: var(--shop-soft); user-select: all; word-break: break-all; }
|
|
311
|
+
.shop-r-get { display: inline-flex; align-items: center; gap: .35rem; border-radius: 999px; padding: .45rem 1.1rem; background: var(--shop-accent);
|
|
312
|
+
color: var(--shop-accent-ink); text-decoration: none; font-weight: 600; }
|
|
313
|
+
.shop-r-get:hover { filter: brightness(1.05); }
|
|
303
314
|
.shop-r-steps { list-style: none; padding: 0; margin: 0 0 2rem; display: grid; grid-template-columns: repeat(3, 1fr); counter-reset: s; }
|
|
304
315
|
.shop-r-steps li { position: relative; padding-top: 1.6rem; font-size: .88rem; color: var(--shop-muted); text-align: center; }
|
|
305
316
|
.shop-r-steps li span { position: absolute; top: .3rem; left: 50%; width: .9rem; height: .9rem; margin-left: -.45rem; border-radius: 50%;
|
|
@@ -400,6 +411,7 @@ html.shop-locked { overflow: hidden; }
|
|
|
400
411
|
.shop-page .shop-qty input, .shop-drawer .shop-qty input, .shop-search input {
|
|
401
412
|
background: transparent !important; border: 0 !important; box-shadow: none !important; outline: 0; padding-left: 0; padding-right: 0; margin: 0; min-height: 0; }
|
|
402
413
|
.shop-card-title a, .shop-card-title a:hover, .shop-line-name, .shop-crumbs a, .shop-quick { text-decoration: none !important; }
|
|
414
|
+
.shop-r-get, .shop-r-get:hover, .shop-r-get:visited { color: var(--shop-accent-ink) !important; text-decoration: none !important; }
|
|
403
415
|
.shop-card-title a:hover { color: var(--shop-accent); }
|
|
404
416
|
|
|
405
417
|
@media (max-width: 640px) {
|
|
@@ -188,3 +188,54 @@ test('an extension sells its own unlisted product through services.startCheckout
|
|
|
188
188
|
reg.unregister('test-credits');
|
|
189
189
|
}
|
|
190
190
|
});
|
|
191
|
+
|
|
192
|
+
test('an extension keeps its own part of a product, and moves its orders on', async () => {
|
|
193
|
+
const {cartRegistry} = await import('../lib/extensions.js');
|
|
194
|
+
const reg = cartRegistry();
|
|
195
|
+
const moves = [];
|
|
196
|
+
reg.register('test-dl', {
|
|
197
|
+
product: {clean: (input, {copy}) => (input?.file === 'x' ? {data: null, errors: {file: 'No such file.'}}
|
|
198
|
+
: {data: {file: String(input?.file || ''), keys: copy ? [] : [].concat(input?.keys || [])}})},
|
|
199
|
+
on: {status: (order, from, to) => moves.push(`${from}>${to}`)}
|
|
200
|
+
});
|
|
201
|
+
try {
|
|
202
|
+
await call('PUT', '/settings', {offlineEnabled: true, countries: ['GB']});
|
|
203
|
+
let r = await call('POST', '/products', {name: 'E-book', price: 500, status: 'active', shippable: false, ext: {'test-dl': {file: 'f1', keys: ['K1']}}});
|
|
204
|
+
assert.equal(r.status, 201, JSON.stringify(r.body));
|
|
205
|
+
const book = r.body;
|
|
206
|
+
assert.deepEqual(book.data.ext, {'test-dl': {file: 'f1', keys: ['K1']}});
|
|
207
|
+
// Never on the storefront.
|
|
208
|
+
assert.ok(!('ext' in (await call('GET', '/catalogue')).body.products.find(p => p.id === book.id)));
|
|
209
|
+
// Refused by the extension: not saved, its message back.
|
|
210
|
+
r = await call('PUT', `/products/${book.id}`, {...book.data, ext: {'test-dl': {file: 'x'}}});
|
|
211
|
+
assert.equal(r.status, 400);
|
|
212
|
+
assert.equal(r.body.error, 'No such file.');
|
|
213
|
+
// A form that sends no ext (the section did not load) keeps it; quick edits keep it.
|
|
214
|
+
r = await call('PUT', `/products/${book.id}`, {...book.data, ext: undefined, name: 'The E-book'});
|
|
215
|
+
assert.deepEqual(r.body.data.ext, book.data.ext);
|
|
216
|
+
r = await call('PATCH', `/products/${book.id}`, {featured: true});
|
|
217
|
+
assert.deepEqual(r.body.data.ext, book.data.ext);
|
|
218
|
+
// A copy carries what the extension says (no licence keys).
|
|
219
|
+
r = await call('POST', `/products/${book.id}/duplicate`);
|
|
220
|
+
assert.deepEqual(r.body.data.ext, {'test-dl': {file: 'f1', keys: []}});
|
|
221
|
+
// updateProductExt changes only that part, from the product as it is now, with no form check.
|
|
222
|
+
await call('PATCH', `/products/${book.id}`, {stock: 7});
|
|
223
|
+
const up = await reg.services.updateProductExt(book.id, 'test-dl', (part) => ({...part, keys: part.keys.slice(1)}));
|
|
224
|
+
assert.deepEqual(up.data.ext['test-dl'].keys, []);
|
|
225
|
+
assert.equal(up.data.stock, 7, 'the rest of the product as it is now');
|
|
226
|
+
assert.equal(up.data.name, 'The E-book');
|
|
227
|
+
|
|
228
|
+
// services.moveOrder moves an order and tells every extension.
|
|
229
|
+
r = await call('POST', '/checkout', {lines: [{productId: book.id, qty: 1}], provider: 'offline', customer: {name: 'Ann', email: 'ann@example.com'}});
|
|
230
|
+
assert.equal(r.status, 200, JSON.stringify(r.body));
|
|
231
|
+
const id = r.body.orderId;
|
|
232
|
+
await call('PUT', `/orders/${id}/status`, {status: 'paid'});
|
|
233
|
+
const moved = await reg.services.moveOrder(id, 'fulfilled', {by: 'test-dl', note: 'Nothing to post'});
|
|
234
|
+
assert.equal(moved.data.status, 'fulfilled');
|
|
235
|
+
await new Promise(res => setTimeout(res, 20));
|
|
236
|
+
assert.ok(moves.includes('paid>fulfilled'));
|
|
237
|
+
await assert.rejects(reg.services.moveOrder(id, 'awaiting'), /cannot become/);
|
|
238
|
+
} finally {
|
|
239
|
+
reg.unregister('test-dl');
|
|
240
|
+
}
|
|
241
|
+
});
|
|
@@ -10,6 +10,7 @@ import {badge, summarise} from '../lib/summary.js';
|
|
|
10
10
|
import {cartRegistry} from '../lib/extensions.js';
|
|
11
11
|
import {matchShopPath} from '../plugin.public.js';
|
|
12
12
|
import {cleanPath, cleanSettings, MASK, maskSettings} from '../lib/shop.js';
|
|
13
|
+
import {receiptHtml} from '../lib/render.js';
|
|
13
14
|
import defaults from '../config.js';
|
|
14
15
|
|
|
15
16
|
test('money: minor units in, formatted out', () => {
|
|
@@ -126,6 +127,54 @@ test('registry: stages, tabs, and emit that never throws', async () => {
|
|
|
126
127
|
assert.equal(reg.list().length, 0);
|
|
127
128
|
});
|
|
128
129
|
|
|
130
|
+
test('registry: panels, a product\'s ext kept per extension, receipt parts', async () => {
|
|
131
|
+
const reg = cartRegistry();
|
|
132
|
+
reg.register('test-dl', {
|
|
133
|
+
panels: [{key: 'files', label: 'Downloads', module: '/plugins/test-dl/admin/files.js', on: 'product'},
|
|
134
|
+
{key: 'files', label: 'Downloads', module: '/plugins/test-dl/admin/order.js', on: 'order'},
|
|
135
|
+
{key: 'bad', module: 'https://evil/x.js', on: 'product'}, {key: 'where', module: '/plugins/test-dl/x.js', on: 'basket'}],
|
|
136
|
+
product: {clean: (input, {had, copy}) => {
|
|
137
|
+
if (input?.files === 'bad') return {data: had, errors: {files: 'Not a file.'}};
|
|
138
|
+
if (input?.files === 'none') return {data: null};
|
|
139
|
+
return {data: {files: [].concat(input?.files || []), keys: copy ? [] : (input?.keys || [])}};
|
|
140
|
+
}},
|
|
141
|
+
receipt: (order) => (order.data.status === 'paid' ? '<p>Your files</p>' : '')
|
|
142
|
+
});
|
|
143
|
+
reg.register('test-boom', {product: {clean: () => { throw new Error('nope'); }}, receipt: () => { throw new Error('nope'); }});
|
|
144
|
+
try {
|
|
145
|
+
assert.deepEqual(reg.panels().map(p => `${p.owner}:${p.on}`), ['test-dl:product', 'test-dl:order']);
|
|
146
|
+
|
|
147
|
+
const had = {'test-dl': {files: ['a'], keys: ['K1']}, 'test-boom': {x: 1}, 'gone-ext': {kept: true}};
|
|
148
|
+
// Nothing sent: everything stored stays.
|
|
149
|
+
assert.deepEqual((await reg.cleanProductExt(undefined, had)).ext, had);
|
|
150
|
+
// An extension's part is its own to check; a lapsed one's and a throwing one's stay as stored.
|
|
151
|
+
let r = await reg.cleanProductExt({'test-dl': {files: ['b']}, 'test-boom': {x: 2}, 'gone-ext': {kept: false}}, had);
|
|
152
|
+
assert.deepEqual(r.ext, {'test-dl': {files: ['b'], keys: []}, 'test-boom': {x: 1}, 'gone-ext': {kept: true}});
|
|
153
|
+
r = await reg.cleanProductExt({'test-dl': {files: 'bad'}}, had);
|
|
154
|
+
assert.deepEqual(r.errors, {'test-dl.files': 'Not a file.'});
|
|
155
|
+
r = await reg.cleanProductExt({'test-dl': {files: 'none'}}, had);
|
|
156
|
+
assert.ok(!('test-dl' in r.ext), 'null data removes the part');
|
|
157
|
+
// A copy starts empty and carries what each extension says.
|
|
158
|
+
r = await reg.cleanProductExt(had, had, {copy: true});
|
|
159
|
+
assert.deepEqual(r.ext, {'test-dl': {files: ['a'], keys: []}});
|
|
160
|
+
|
|
161
|
+
assert.deepEqual(await reg.receiptParts({data: {status: 'paid'}}), [{owner: 'test-dl', html: '<p>Your files</p>'}]);
|
|
162
|
+
assert.deepEqual(await reg.receiptParts({data: {status: 'pending'}}), []);
|
|
163
|
+
} finally {
|
|
164
|
+
reg.unregister('test-dl');
|
|
165
|
+
reg.unregister('test-boom');
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('receipt: extension parts sit above the items', () => {
|
|
170
|
+
const order = {id: 'o1', number: 1001, status: 'paid', currency: 'GBP', lines: [], subtotal: 0, adjustments: [], total: 0,
|
|
171
|
+
customer: {name: 'Ann'}, email: 'ann@x.test', address: null, delivery: 'none', placedAt: new Date().toISOString()};
|
|
172
|
+
const html = receiptHtml({order, settings: {}, base: '/shop', parts: [{owner: 'test-dl', html: '<p>Your files</p>'}]});
|
|
173
|
+
assert.match(html, /<div class="shop-r-ext" data-ext="test-dl"><p>Your files<\/p><\/div>/);
|
|
174
|
+
assert.ok(html.indexOf('shop-r-ext') < html.indexOf('shop-r-grid'));
|
|
175
|
+
assert.ok(!receiptHtml({order, settings: {}, base: '/shop'}).includes('shop-r-ext'));
|
|
176
|
+
});
|
|
177
|
+
|
|
129
178
|
test('stripe: form encoding, itemising, signature, paid check', () => {
|
|
130
179
|
assert.equal(stripeForm({a: {b: [{c: 1}]}, d: 'x', e: null}).toString(), 'a%5Bb%5D%5B0%5D%5Bc%5D=1&d=x');
|
|
131
180
|
const order = {id: 'o1', number: 1001, currency: 'GBP', total: 1395,
|
package/public/js/effects.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";fetch("/api/effects/settings").then(function(g){return g.json()}).then(function(g){var z=g.respectMotion!==!1,k=g.defaultDuration||600,M=g.defaultAnimation||"fade",I=g.defaultThreshold!=null?g.defaultThreshold:.1,p=z&&window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches,m=document.querySelector(".page-body");if(!m)return;var A=m.querySelector?m.querySelector(".dm-fx-demo-controls"):null;if(A){A.hidden=!1;var S=A.querySelector("[data-fx-demo-status]"),E=A.querySelector("[data-fx-demo-play]");S.textContent=p?"Animations are disabled by your Reduce motion preference. The demos show static content.":"Animations are enabled. Scroll to each demo to see it, or replay the page.",E.textContent=p?"Enable animations on this site":"Replay demos",E.addEventListener("click",function(){if(p)try{localStorage.setItem("domma:reduced_motion","false")}catch{S.textContent="Your browser blocked saving the motion preference. Use its accessibility settings to allow animations.";return}window.location.reload()})}function r(t,e){return t.getAttribute("data-fx-"+e)}function i(t,e,n){var o=r(t,e),a=o!==null?parseFloat(o):NaN;return Number.isFinite(a)?a:n}function b(t,e,n){var o=r(t,e);return o===null?n:o==="true"||o==="1"}function C(t,e,n){var o=new IntersectionObserver(function(a){a[0].isIntersecting&&(o.unobserve(t),n())},{threshold:e});o.observe(t)}m.querySelectorAll(".dm-fx-reveal").forEach(function(t){var e=r(t,"animation")||M,n=i(t,"duration",k),o=i(t,"delay",0),a=i(t,"threshold",I),c=b(t,"once",!0);p||(t.style.opacity="0",t.style.transition="none",e==="slide-up"?t.style.transform="translateY(30px)":e==="slide-down"?t.style.transform="translateY(-30px)":e==="zoom"?t.style.transform="scale(0.85)":e==="flip"&&(t.style.transform="rotateY(90deg)"),requestAnimationFrame(function(){requestAnimationFrame(function(){t.style.transition="opacity "+n+"ms ease "+o+"ms, transform "+n+"ms ease "+o+"ms";var f=new IntersectionObserver(function(u){u.forEach(function(v){v.isIntersecting?(t.style.opacity="1",t.style.transform="",c&&f.unobserve(t)):c||(t.style.opacity="0",e==="slide-up"?t.style.transform="translateY(30px)":e==="slide-down"?t.style.transform="translateY(-30px)":e==="zoom"?t.style.transform="scale(0.85)":e==="flip"?t.style.transform="rotateY(90deg)":t.style.transform="")})},{threshold:a});f.observe(t)})}))}),m.querySelectorAll(".dm-fx-counter").forEach(function(t){var e=i(t,"to",0),n=i(t,"from",0),o=i(t,"duration",2e3),a=r(t,"prefix")||"",c=r(t,"suffix")||"",f=i(t,"decimals",0),u=r(t,"separator")||"";function v(l){var d=l.toFixed(f);if(u){var y=d.split(".");y[0]=y[0].replace(/\B(?=(\d{3})+(?!\d))/g,u),d=y.join(".")}return a+d+c}if(p){t.textContent=v(e);return}C(t,.5,function(){var l=null;requestAnimationFrame(function d(y){l||(l=y);var x=Math.min((y-l)/o,1),s=1-Math.pow(1-x,3);t.textContent=v(n+(e-n)*s),x<1&&requestAnimationFrame(d)})})}),p||m.querySelectorAll(".dm-fx-scramble").forEach(function(t){var e=i(t,"speed",50),n=t.textContent,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&",a=0;C(t,.3,function(){var c=setInterval(function(){for(var f="",u=0;u<n.length;u++)u<a||n[u]===" "?f+=n[u]:f+=o[Math.floor(Math.random()*o.length)];t.textContent=f,a<n.length?a++:clearInterval(c)},e)})}),p||m.querySelectorAll(".dm-fx-shake").forEach(function(t){var e=i(t,"intensity",5),n=i(t,"duration",500),o=r(t,"direction")||"horizontal",a=o==="vertical"?"translateY":"translateX";t.animate([{transform:a+"(0)"},{transform:a+"(-"+e+"px)"},{transform:a+"("+e+"px)"},{transform:a+"(-"+e+"px)"},{transform:a+"("+e+"px)"},{transform:a+"(0)"}],{duration:n,easing:"ease-in-out"})}),m.querySelectorAll(".dm-fx-ripple").forEach(function(t){t.style.position="relative",t.style.overflow="hidden",t.addEventListener("click",function(e){var n=r(t,"colour")||"rgba(255,255,255,0.3)",o=i(t,"duration",600),a=t.getBoundingClientRect(),c=Math.max(a.width,a.height)*2,f=document.createElement("span");f.style.cssText="position:absolute;border-radius:50%;pointer-events:none;width:"+c+"px;height:"+c+"px;left:"+(e.clientX-a.left-c/2)+"px;top:"+(e.clientY-a.top-c/2)+"px;background:"+n+";transform:scale(0);opacity:1;transition:transform "+o+"ms ease,opacity "+o+"ms ease;",t.appendChild(f),requestAnimationFrame(function(){f.style.transform="scale(1)",f.style.opacity="0"}),setTimeout(function(){f.remove()},o)})});var h=window.Domma&&window.Domma.effects;h&&(m.querySelectorAll(".dm-fx-breathe").forEach(function(t){if(!p){var e={};r(t,"amplitude")&&(e.amplitude=i(t,"amplitude",6)),r(t,"duration")&&(e.duration=i(t,"duration",3e3)),r(t,"easing")&&(e.easing=r(t,"easing")),r(t,"stagger")&&(e.stagger=i(t,"stagger",0)),h.breathe(t,e)}}),m.querySelectorAll(".dm-fx-pulse").forEach(function(t){if(!p){var e={};r(t,"scale")&&(e.scale=i(t,"scale",1.05)),r(t,"duration")&&(e.duration=i(t,"duration",2e3)),r(t,"easing")&&(e.easing=r(t,"easing")),h.pulse(t,e)}}),m.querySelectorAll(".dm-fx-scribe").forEach(function(t){var e={respectMotionPreference:z};[["speed","speed",50],["delete-speed","deleteSpeed",30],["loop-delay","loopDelay",1e3]].forEach(function(s){r(t,s[0])!==null&&(e[s[1]]=Math.max(0,i(t,s[0],s[2])))}),[["cursor","cursor",!0],["cursor-blink","cursorBlink",!0],["pause-on-hover","pauseOnHover",!1],["auto-start","autoStart",!0],["respect-motion-preference","respectMotionPreference",z]].forEach(function(s){r(t,s[0])!==null&&(e[s[1]]=b(t,s[0],s[2]))}),r(t,"cursor-char")!==null&&(e.cursorChar=r(t,"cursor-char"));var n=r(t,"mode");["typewriter","word","sentence"].includes(n)&&(e.mode=n);var o=r(t,"cursor-type");["caret","block","underline"].includes(o)&&(e.cursorType=o);var a=r(t,"loop");if(a!==null){var c=Number(a);e.loop=a==="true"?!0:Number.isInteger(c)&&c>=1?c:!1}var f=r(t,"effect");["none","fade","bounce","glow"].includes(f)||(f="none");var u=r(t,"actions");if(u){try{e.actions=JSON.parse(u)}catch{return}if(!Array.isArray(e.actions)||e.actions.some(function(s){return!s||typeof s!="object"}))return;e.actions=e.actions.map(function(s){return s.render!==void 0&&s.effect===void 0?Object.assign({},s,{effect:f}):s})}else{var v=t.textContent.trim();if(!v)return;e.actions=[{render:v,effect:f}]}var l=e.respectMotionPreference&&window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches;if(l){var d=e.actions.find(function(s){return s.render!=null});d&&(t.textContent=String(d.render));return}u||(t.textContent=""),[["onStart","start"],["onComplete","complete"],["onCharacter","character"],["onRender","render"],["onUndoRender","undo"],["onLoop","loop"]].forEach(function(s){e[s[0]]=function(w,T){t.dispatchEvent(new CustomEvent("scribe:"+s[1],{bubbles:!0,detail:{value:w,index:T}}))}});var y=h.scribe(t,e);if(y&&(b(t,"controls",!1)||e.autoStart===!1)){var x=document.createElement("span");x.className="dm-scribe-controls",x.setAttribute("role","group"),x.setAttribute("aria-label","Text animation playback"),[["Play","resume"],["Pause","pause"],["Replay","restart"]].forEach(function(s){var w=document.createElement("button");w.type="button",w.className="btn btn-ghost btn-sm",w.textContent=s[0],w.addEventListener("click",function(){y[s[1]]()}),x.appendChild(w)}),t.insertAdjacentElement("afterend",x)}}),m.querySelectorAll(".dm-fx-twinkle").forEach(function(t){var e={};r(t,"count")&&(e.count=i(t,"count",100)),r(t,"shape")&&(e.shape=r(t,"shape")),r(t,"colour")&&(e.colour=r(t,"colour")),r(t,"min-size")&&(e.minSize=i(t,"min-size",1)),r(t,"max-size")&&(e.maxSize=i(t,"max-size",3)),h.twinkle(t,e)}),typeof h.tickerTape=="function"&&m.querySelectorAll(".dm-fx-ticker-tape").forEach(function(t){if(!p){var e={};r(t,"palette")&&(e.palette=r(t,"palette")),r(t,"density")&&(e.density=i(t,"density",50)),r(t,"speed")&&(e.speed=i(t,"speed",1)),r(t,"sway")&&(e.sway=i(t,"sway",60)),r(t,"rotation-speed")&&(e.rotationSpeed=i(t,"rotation-speed",6)),r(t,"min-width")&&(e.minWidth=i(t,"min-width",5)),r(t,"max-width")&&(e.maxWidth=i(t,"max-width",9)),r(t,"min-height")&&(e.minHeight=i(t,"min-height",12)),r(t,"max-height")&&(e.maxHeight=i(t,"max-height",22)),r(t,"fade-start")&&(e.fadeStart=i(t,"fade-start",.55)),r(t,"burst")&&(e.burst=b(t,"burst",!1)),r(t,"burst-count")&&(e.burstCount=i(t,"burst-count",150)),r(t,"z-index")&&(e.zIndex=i(t,"z-index",1));var n=r(t,"mode");n==="page"?h.tickerTape(null,e):h.tickerTape(t,e)}}),typeof h.butterflies=="function"&&m.querySelectorAll(".dm-fx-butterflies").forEach(function(t){if(!p){var e={};r(t,"palette")&&(e.palette=r(t,"palette")),r(t,"density")&&(e.density=i(t,"density",18)),r(t,"speed")&&(e.speed=i(t,"speed",1)),r(t,"wander")&&(e.wander=i(t,"wander",1)),r(t,"rise-speed")&&(e.riseSpeed=i(t,"rise-speed",.4)),r(t,"flap-speed")&&(e.flapSpeed=i(t,"flap-speed",1)),r(t,"min-size")&&(e.minSize=i(t,"min-size",14)),r(t,"max-size")&&(e.maxSize=i(t,"max-size",26)),r(t,"two-tone")&&(e.twoTone=b(t,"two-tone",!0)),r(t,"burst")&&(e.burst=b(t,"burst",!1)),r(t,"burst-count")&&(e.burstCount=i(t,"burst-count",40)),r(t,"z-index")&&(e.zIndex=i(t,"z-index",1));var n=r(t,"mode");n==="page"?h.butterflies(null,e):h.butterflies(t,e)}}),typeof h.strobe=="function"&&m.querySelectorAll(".dm-fx-strobe").forEach(function(t){if(p)return;function e(f){var u=r(t,f);if(!u)return null;var v=u.split(",").map(function(l){return l.trim()}).filter(Boolean);return v.length?v:null}var n={};r(t,"preset")&&(n.preset=r(t,"preset")),r(t,"motion")&&(n.motion=r(t,"motion")),r(t,"speed")&&(n.speed=i(t,"speed",1)),r(t,"sweep-arc")&&(n.sweepArc=i(t,"sweep-arc",100)),r(t,"beam-width")&&(n.beamWidth=i(t,"beam-width",16)),r(t,"intensity")&&(n.intensity=i(t,"intensity",.7)),r(t,"flicker")&&(n.flicker=b(t,"flicker",!0)),r(t,"hz")&&(n.hz=i(t,"hz",10));var o=e("origins");o&&(n.origins=o);var a=e("colours");a&&(n.colours=a),r(t,"duration")&&(n.duration=i(t,"duration",null)),r(t,"z-index")&&(n.zIndex=i(t,"z-index",9999));var c=r(t,"mode");c==="page"?h.strobe(null,n):h.strobe(t,n)}));var q=m.querySelectorAll(".dm-fx-celebrate");q.length&&import("/public/js/celebrations/index.js?v=20261003-characters").then(function(t){var e=t.CelebrationsEffect;q.forEach(function(n){var o=r(n,"character");if(o){if(n.querySelector("canvas"))return;t.mountCharacter(n,{character:o,size:r(n,"size")||"medium",facing:r(n,"facing")||"right",animate:!p&&b(n,"animate",!0)}).then(function(d){var y=new MutationObserver(function(){n.isConnected||(d.destroy(),y.disconnect())});n.isConnected?y.observe(document.body,{childList:!0,subtree:!0}):d.destroy()}).catch(function(d){console.error("[celebrate]",d)});return}if(!p){var a=n.getAttribute("data-fx-theme")||"auto",c=n.getAttribute("data-fx-intensity")||"medium",f=n.getAttribute("data-fx-z-index")?parseInt(n.getAttribute("data-fx-z-index"),10):999;if(!(a==="auto"&&(a=e.getCurrentTheme(),!a))){var u={},v=["data-fx-theme","data-fx-intensity","data-fx-z-index"];Array.from(n.attributes).forEach(function(d){d.name.startsWith("data-fx-")&&!v.includes(d.name)&&(u[d.name.slice(8)]=d.value)});var l=new e({theme:a,intensity:c,enabled:!0,zIndex:f,overrides:u});l.init().then(function(){l.start()})}}})}).catch(function(){})}).catch(function(){})})();
|
|
1
|
+
(function(){"use strict";Promise.all([fetch("/api/effects/settings").then(function(z){return z.json()}),import("/public/js/scribe-style.mjs")]).then(function(z){var g=z[0],I=z[1].buildScribeStyle,C=g.respectMotion!==!1,Y=g.defaultDuration||600,F=g.defaultAnimation||"fade",N=g.defaultThreshold!=null?g.defaultThreshold:.1,p=C&&window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches,m=document.querySelector(".page-body");if(!m)return;var A=m.querySelector?m.querySelector(".dm-fx-demo-controls"):null;if(A){A.hidden=!1;var q=A.querySelector("[data-fx-demo-status]"),k=A.querySelector("[data-fx-demo-play]");q.textContent=p?"Animations are disabled by your Reduce motion preference. The demos show static content.":"Animations are enabled. Scroll to each demo to see it, or replay the page.",k.textContent=p?"Enable animations on this site":"Replay demos",k.addEventListener("click",function(){if(p)try{localStorage.setItem("domma:reduced_motion","false")}catch{q.textContent="Your browser blocked saving the motion preference. Use its accessibility settings to allow animations.";return}window.location.reload()})}function r(t,e){return t.getAttribute("data-fx-"+e)}function i(t,e,n){var a=r(t,e),o=a!==null?parseFloat(a):NaN;return Number.isFinite(o)?o:n}function x(t,e,n){var a=r(t,e);return a===null?n:a==="true"||a==="1"}function M(t,e,n){var a=new IntersectionObserver(function(o){o[0].isIntersecting&&(a.unobserve(t),n())},{threshold:e});a.observe(t)}m.querySelectorAll(".dm-fx-reveal").forEach(function(t){var e=r(t,"animation")||F,n=i(t,"duration",Y),a=i(t,"delay",0),o=i(t,"threshold",N),c=x(t,"once",!0);p||(t.style.opacity="0",t.style.transition="none",e==="slide-up"?t.style.transform="translateY(30px)":e==="slide-down"?t.style.transform="translateY(-30px)":e==="zoom"?t.style.transform="scale(0.85)":e==="flip"&&(t.style.transform="rotateY(90deg)"),requestAnimationFrame(function(){requestAnimationFrame(function(){t.style.transition="opacity "+n+"ms ease "+a+"ms, transform "+n+"ms ease "+a+"ms";var f=new IntersectionObserver(function(u){u.forEach(function(l){l.isIntersecting?(t.style.opacity="1",t.style.transform="",c&&f.unobserve(t)):c||(t.style.opacity="0",e==="slide-up"?t.style.transform="translateY(30px)":e==="slide-down"?t.style.transform="translateY(-30px)":e==="zoom"?t.style.transform="scale(0.85)":e==="flip"?t.style.transform="rotateY(90deg)":t.style.transform="")})},{threshold:o});f.observe(t)})}))}),m.querySelectorAll(".dm-fx-counter").forEach(function(t){var e=i(t,"to",0),n=i(t,"from",0),a=i(t,"duration",2e3),o=r(t,"prefix")||"",c=r(t,"suffix")||"",f=i(t,"decimals",0),u=r(t,"separator")||"";function l(v){var d=v.toFixed(f);if(u){var y=d.split(".");y[0]=y[0].replace(/\B(?=(\d{3})+(?!\d))/g,u),d=y.join(".")}return o+d+c}if(p){t.textContent=l(e);return}M(t,.5,function(){var v=null;requestAnimationFrame(function d(y){v||(v=y);var w=Math.min((y-v)/a,1),E=1-Math.pow(1-w,3);t.textContent=l(n+(e-n)*E),w<1&&requestAnimationFrame(d)})})}),p||m.querySelectorAll(".dm-fx-scramble").forEach(function(t){var e=i(t,"speed",50),n=t.textContent,a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&",o=0;M(t,.3,function(){var c=setInterval(function(){for(var f="",u=0;u<n.length;u++)u<o||n[u]===" "?f+=n[u]:f+=a[Math.floor(Math.random()*a.length)];t.textContent=f,o<n.length?o++:clearInterval(c)},e)})}),p||m.querySelectorAll(".dm-fx-shake").forEach(function(t){var e=i(t,"intensity",5),n=i(t,"duration",500),a=r(t,"direction")||"horizontal",o=a==="vertical"?"translateY":"translateX";t.animate([{transform:o+"(0)"},{transform:o+"(-"+e+"px)"},{transform:o+"("+e+"px)"},{transform:o+"(-"+e+"px)"},{transform:o+"("+e+"px)"},{transform:o+"(0)"}],{duration:n,easing:"ease-in-out"})}),m.querySelectorAll(".dm-fx-ripple").forEach(function(t){t.style.position="relative",t.style.overflow="hidden",t.addEventListener("click",function(e){var n=r(t,"colour")||"rgba(255,255,255,0.3)",a=i(t,"duration",600),o=t.getBoundingClientRect(),c=Math.max(o.width,o.height)*2,f=document.createElement("span");f.style.cssText="position:absolute;border-radius:50%;pointer-events:none;width:"+c+"px;height:"+c+"px;left:"+(e.clientX-o.left-c/2)+"px;top:"+(e.clientY-o.top-c/2)+"px;background:"+n+";transform:scale(0);opacity:1;transition:transform "+a+"ms ease,opacity "+a+"ms ease;",t.appendChild(f),requestAnimationFrame(function(){f.style.transform="scale(1)",f.style.opacity="0"}),setTimeout(function(){f.remove()},a)})});var h=window.Domma&&window.Domma.effects;h&&(m.querySelectorAll(".dm-fx-breathe").forEach(function(t){if(!p){var e={};r(t,"amplitude")&&(e.amplitude=i(t,"amplitude",6)),r(t,"duration")&&(e.duration=i(t,"duration",3e3)),r(t,"easing")&&(e.easing=r(t,"easing")),r(t,"stagger")&&(e.stagger=i(t,"stagger",0)),h.breathe(t,e)}}),m.querySelectorAll(".dm-fx-pulse").forEach(function(t){if(!p){var e={};r(t,"scale")&&(e.scale=i(t,"scale",1.05)),r(t,"duration")&&(e.duration=i(t,"duration",2e3)),r(t,"easing")&&(e.easing=r(t,"easing")),h.pulse(t,e)}}),m.querySelectorAll(".dm-fx-scribe").forEach(function(t){var e={};["font","size","font-size","weight","colour","color","italic","align","line-height","spacing"].forEach(function(s){var b=r(t,s);b!==null&&(e[s]=b)});var n=I(e);n&&(t.style.cssText+=";"+n);var a={respectMotionPreference:C};[["speed","speed",50],["delete-speed","deleteSpeed",30],["loop-delay","loopDelay",1e3]].forEach(function(s){r(t,s[0])!==null&&(a[s[1]]=Math.max(0,i(t,s[0],s[2])))}),[["cursor","cursor",!0],["cursor-blink","cursorBlink",!0],["pause-on-hover","pauseOnHover",!1],["auto-start","autoStart",!0],["respect-motion-preference","respectMotionPreference",C]].forEach(function(s){r(t,s[0])!==null&&(a[s[1]]=x(t,s[0],s[2]))}),r(t,"cursor-char")!==null&&(a.cursorChar=r(t,"cursor-char"));var o=r(t,"mode");["typewriter","word","sentence"].includes(o)&&(a.mode=o);var c=r(t,"cursor-type");["caret","block","underline"].includes(c)&&(a.cursorType=c);var f=r(t,"loop");if(f!==null){var u=Number(f);a.loop=f==="true"?!0:Number.isInteger(u)&&u>=1?u:!1}var l=r(t,"effect");["none","fade","bounce","glow"].includes(l)||(l="none");var v=r(t,"actions");if(v){try{a.actions=JSON.parse(v)}catch{return}if(!Array.isArray(a.actions)||a.actions.some(function(s){return!s||typeof s!="object"}))return;a.actions=a.actions.map(function(s){return s.render!==void 0&&s.effect===void 0?Object.assign({},s,{effect:l}):s})}else{var d=t.textContent.trim();if(!d)return;a.actions=[{render:d,effect:l}]}var y=a.respectMotionPreference&&window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches;if(y){var w=a.actions.find(function(s){return s.render!=null});w&&(t.textContent=String(w.render));return}v||(t.textContent=""),[["onStart","start"],["onComplete","complete"],["onCharacter","character"],["onRender","render"],["onUndoRender","undo"],["onLoop","loop"]].forEach(function(s){a[s[0]]=function(b,R){t.dispatchEvent(new CustomEvent("scribe:"+s[1],{bubbles:!0,detail:{value:b,index:R}}))}});var E=h.scribe(t,a);if(E&&(x(t,"controls",!1)||a.autoStart===!1)){var S=document.createElement("span");S.className="dm-scribe-controls",S.setAttribute("role","group"),S.setAttribute("aria-label","Text animation playback"),[["Play","resume"],["Pause","pause"],["Replay","restart"]].forEach(function(s){var b=document.createElement("button");b.type="button",b.className="btn btn-ghost btn-sm",b.textContent=s[0],b.addEventListener("click",function(){E[s[1]]()}),S.appendChild(b)}),t.insertAdjacentElement("afterend",S)}}),m.querySelectorAll(".dm-fx-twinkle").forEach(function(t){var e={};r(t,"count")&&(e.count=i(t,"count",100)),r(t,"shape")&&(e.shape=r(t,"shape")),r(t,"colour")&&(e.colour=r(t,"colour")),r(t,"min-size")&&(e.minSize=i(t,"min-size",1)),r(t,"max-size")&&(e.maxSize=i(t,"max-size",3)),h.twinkle(t,e)}),typeof h.tickerTape=="function"&&m.querySelectorAll(".dm-fx-ticker-tape").forEach(function(t){if(!p){var e={};r(t,"palette")&&(e.palette=r(t,"palette")),r(t,"density")&&(e.density=i(t,"density",50)),r(t,"speed")&&(e.speed=i(t,"speed",1)),r(t,"sway")&&(e.sway=i(t,"sway",60)),r(t,"rotation-speed")&&(e.rotationSpeed=i(t,"rotation-speed",6)),r(t,"min-width")&&(e.minWidth=i(t,"min-width",5)),r(t,"max-width")&&(e.maxWidth=i(t,"max-width",9)),r(t,"min-height")&&(e.minHeight=i(t,"min-height",12)),r(t,"max-height")&&(e.maxHeight=i(t,"max-height",22)),r(t,"fade-start")&&(e.fadeStart=i(t,"fade-start",.55)),r(t,"burst")&&(e.burst=x(t,"burst",!1)),r(t,"burst-count")&&(e.burstCount=i(t,"burst-count",150)),r(t,"z-index")&&(e.zIndex=i(t,"z-index",1));var n=r(t,"mode");n==="page"?h.tickerTape(null,e):h.tickerTape(t,e)}}),typeof h.butterflies=="function"&&m.querySelectorAll(".dm-fx-butterflies").forEach(function(t){if(!p){var e={};r(t,"palette")&&(e.palette=r(t,"palette")),r(t,"density")&&(e.density=i(t,"density",18)),r(t,"speed")&&(e.speed=i(t,"speed",1)),r(t,"wander")&&(e.wander=i(t,"wander",1)),r(t,"rise-speed")&&(e.riseSpeed=i(t,"rise-speed",.4)),r(t,"flap-speed")&&(e.flapSpeed=i(t,"flap-speed",1)),r(t,"min-size")&&(e.minSize=i(t,"min-size",14)),r(t,"max-size")&&(e.maxSize=i(t,"max-size",26)),r(t,"two-tone")&&(e.twoTone=x(t,"two-tone",!0)),r(t,"burst")&&(e.burst=x(t,"burst",!1)),r(t,"burst-count")&&(e.burstCount=i(t,"burst-count",40)),r(t,"z-index")&&(e.zIndex=i(t,"z-index",1));var n=r(t,"mode");n==="page"?h.butterflies(null,e):h.butterflies(t,e)}}),typeof h.strobe=="function"&&m.querySelectorAll(".dm-fx-strobe").forEach(function(t){if(p)return;function e(f){var u=r(t,f);if(!u)return null;var l=u.split(",").map(function(v){return v.trim()}).filter(Boolean);return l.length?l:null}var n={};r(t,"preset")&&(n.preset=r(t,"preset")),r(t,"motion")&&(n.motion=r(t,"motion")),r(t,"speed")&&(n.speed=i(t,"speed",1)),r(t,"sweep-arc")&&(n.sweepArc=i(t,"sweep-arc",100)),r(t,"beam-width")&&(n.beamWidth=i(t,"beam-width",16)),r(t,"intensity")&&(n.intensity=i(t,"intensity",.7)),r(t,"flicker")&&(n.flicker=x(t,"flicker",!0)),r(t,"hz")&&(n.hz=i(t,"hz",10));var a=e("origins");a&&(n.origins=a);var o=e("colours");o&&(n.colours=o),r(t,"duration")&&(n.duration=i(t,"duration",null)),r(t,"z-index")&&(n.zIndex=i(t,"z-index",9999));var c=r(t,"mode");c==="page"?h.strobe(null,n):h.strobe(t,n)}));var T=m.querySelectorAll(".dm-fx-celebrate");T.length&&import("/public/js/celebrations/index.js?v=20261003-characters").then(function(t){var e=t.CelebrationsEffect;T.forEach(function(n){var a=r(n,"character");if(a){if(n.querySelector("canvas"))return;t.mountCharacter(n,{character:a,size:r(n,"size")||"medium",facing:r(n,"facing")||"right",animate:!p&&x(n,"animate",!0)}).then(function(d){var y=new MutationObserver(function(){n.isConnected||(d.destroy(),y.disconnect())});n.isConnected?y.observe(document.body,{childList:!0,subtree:!0}):d.destroy()}).catch(function(d){console.error("[celebrate]",d)});return}if(!p){var o=n.getAttribute("data-fx-theme")||"auto",c=n.getAttribute("data-fx-intensity")||"medium",f=n.getAttribute("data-fx-z-index")?parseInt(n.getAttribute("data-fx-z-index"),10):999;if(!(o==="auto"&&(o=e.getCurrentTheme(),!o))){var u={},l=["data-fx-theme","data-fx-intensity","data-fx-z-index"];Array.from(n.attributes).forEach(function(d){d.name.startsWith("data-fx-")&&!l.includes(d.name)&&(u[d.name.slice(8)]=d.value)});var v=new e({theme:o,intensity:c,enabled:!0,zIndex:f,overrides:u});v.init().then(function(){v.start()})}}})}).catch(function(){})}).catch(function(){})})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{FONT_MAP as c,SIZE_MAP as f,WEIGHT_MAP as g,COLOUR_TOKENS as p,SPACING_MAP as a}from"./text-style.mjs";export const SCRIBE_STYLE_FIELDS=[{name:"font",key:"font",label:"Font",type:"select",default:"",options:[["","Inherit"],["system","System"],["serif","Serif"],["mono","Monospace"],...Object.keys(c).map(e=>[e,e])]},{name:"size",key:"size",label:"Font size (e.g. 3xl, 48px, 3rem)",type:"text",default:""},{name:"weight",key:"weight",label:"Font weight",type:"select",default:"",options:[["","Inherit"],...Object.keys(g).map(e=>[e,e])]},{name:"colour",key:"colour",label:"Text colour (name or hex)",type:"text",default:""},{name:"italic",key:"italic",label:"Italic",type:"checkbox",default:!1},{name:"align",key:"align",label:"Alignment",type:"select",default:"",options:[["","Inherit"],["left","Left"],["center","Centre"],["right","Right"]]},{name:"line-height",key:"lineHeight",label:"Line height (e.g. 1.5)",type:"text",default:""},{name:"spacing",key:"spacing",label:"Letter spacing (e.g. wide, .05em)",type:"text",default:""}];const h=/^(?:\d+(?:\.\d+)?|\.\d+)(?:px|pt|rem|em|vw|vh|%)$/i,u={...c,system:"system-ui,sans-serif",serif:"serif",mono:"monospace"};export function buildScribeStyle(e={}){const t=[],i=e.size||e["font-size"];f[i]?t.push(`font-size:${f[i]}`):h.test(String(i||""))&&t.push(`font-size:${i}`),u[e.font]&&t.push(`font-family:${u[e.font]}`);const s=g[e.weight]||(/^[1-9]00$/.test(String(e.weight||""))?e.weight:"");s&&t.push(`font-weight:${s}`),(e.italic===!0||e.italic==="true")&&t.push("font-style:italic");const n=e.colour||e.color;p[n]?t.push(`color:${p[n]}`):/^(?:#[\da-f]{3,4}|#[\da-f]{6}|#[\da-f]{8}|[a-z]+)$/i.test(String(n||""))&&t.push(`color:${n}`);const o=e["line-height"]||e.lineHeight;(/^(?:\d+(?:\.\d+)?|\.\d+)$/.test(String(o||""))||h.test(String(o||"")))&&t.push(`line-height:${o}`);const l=e.spacing;return a[l]?t.push(`letter-spacing:${a[l]}`):/^-?(?:\d+(?:\.\d+)?|\.\d+)(?:px|rem|em)$/i.test(String(l||""))&&t.push(`letter-spacing:${l}`),["left","center","right"].includes(e.align)&&t.push(`display:block;text-align:${e.align}`),t.join(";")}
|