domma-cms 0.84.1 → 0.86.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 (70) hide show
  1. package/README.md +4 -4
  2. package/admin/dist/domma/domma-tools.css +3 -3
  3. package/admin/dist/domma/domma-tools.min.js +4 -4
  4. package/admin/js/lib/sidebar-renderer.js +3 -3
  5. package/admin/js/lib/themes.js +1 -1
  6. package/config/menus/admin-sidebar.json +21 -8
  7. package/config/plugins.json +0 -18
  8. package/package.json +2 -2
  9. package/plugins/_lib/mail/diagnostics.js +3 -2
  10. package/plugins/blog/CLAUDE.md +33 -0
  11. package/plugins/blog/lib/access.js +64 -0
  12. package/plugins/blog/lib/notices.js +38 -0
  13. package/plugins/blog/plugin.js +84 -26
  14. package/plugins/blog/plugin.json +173 -20
  15. package/plugins/blog/roles/blog-author.json +15 -2
  16. package/plugins/blog/tests/access.test.js +32 -0
  17. package/plugins/blog/tests/comment-notices.test.js +140 -0
  18. package/plugins/blog/tests/notices.test.js +20 -0
  19. package/plugins/blog/tests/public.test.js +3 -1
  20. package/plugins/free-tier.lock.json +63 -10
  21. package/plugins/mail-reader/plugin.js +12 -1
  22. package/plugins/mail-reader/plugin.json +23 -4
  23. package/plugins/shopping-cart/CLAUDE.md +93 -0
  24. package/plugins/shopping-cart/admin/css/index.css +224 -0
  25. package/plugins/shopping-cart/admin/lib/kit.js +190 -0
  26. package/plugins/shopping-cart/admin/templates/shop.html +1 -0
  27. package/plugins/shopping-cart/admin/views/orders.js +365 -0
  28. package/plugins/shopping-cart/admin/views/overview.js +214 -0
  29. package/plugins/shopping-cart/admin/views/products.js +547 -0
  30. package/plugins/shopping-cart/admin/views/settings.js +247 -0
  31. package/plugins/shopping-cart/admin/views/shop.js +130 -0
  32. package/plugins/shopping-cart/collections/shop-orders.json +20 -0
  33. package/plugins/shopping-cart/collections/shop-products.json +24 -0
  34. package/plugins/shopping-cart/config.js +52 -0
  35. package/plugins/shopping-cart/inject/body.html +2 -0
  36. package/plugins/shopping-cart/lib/extensions.js +96 -0
  37. package/plugins/shopping-cart/lib/payments.js +281 -0
  38. package/plugins/shopping-cart/lib/pricing.js +232 -0
  39. package/plugins/shopping-cart/lib/render.js +282 -0
  40. package/plugins/shopping-cart/lib/samples.js +34 -0
  41. package/plugins/shopping-cart/lib/shop.js +436 -0
  42. package/plugins/shopping-cart/lib/summary.js +127 -0
  43. package/plugins/shopping-cart/plugin.js +559 -0
  44. package/plugins/shopping-cart/plugin.json +52 -0
  45. package/plugins/shopping-cart/plugin.public.js +144 -0
  46. package/plugins/shopping-cart/public/cart.js +715 -0
  47. package/plugins/shopping-cart/public/lib/money.js +105 -0
  48. package/plugins/shopping-cart/public/lib/orders.js +122 -0
  49. package/plugins/shopping-cart/public/lib/products.js +235 -0
  50. package/plugins/shopping-cart/public/samples/candle-2.svg +16 -0
  51. package/plugins/shopping-cart/public/samples/candle.svg +16 -0
  52. package/plugins/shopping-cart/public/samples/mug-2.svg +14 -0
  53. package/plugins/shopping-cart/public/samples/mug.svg +14 -0
  54. package/plugins/shopping-cart/public/samples/notebook-2.svg +15 -0
  55. package/plugins/shopping-cart/public/samples/notebook.svg +15 -0
  56. package/plugins/shopping-cart/public/samples/print-2.svg +14 -0
  57. package/plugins/shopping-cart/public/samples/print.svg +14 -0
  58. package/plugins/shopping-cart/public/samples/tee-2.svg +12 -0
  59. package/plugins/shopping-cart/public/samples/tee.svg +12 -0
  60. package/plugins/shopping-cart/public/samples/tote-2.svg +13 -0
  61. package/plugins/shopping-cart/public/samples/tote.svg +13 -0
  62. package/plugins/shopping-cart/public/shop.css +415 -0
  63. package/plugins/shopping-cart/templates/shop.html +4 -0
  64. package/plugins/shopping-cart/tests/api.test.js +144 -0
  65. package/plugins/shopping-cart/tests/pure.test.js +210 -0
  66. package/scripts/setup.js +1 -0
  67. package/server/routes/api/notifications.js +4 -3
  68. package/server/services/notify.js +4 -2
  69. package/server/services/pluginScope.js +85 -0
  70. package/server/services/plugins.js +7 -2
@@ -0,0 +1,224 @@
1
+ /* Shop admin - only --dm-* tokens, so it follows the admin theme (light, dark, custom). */
2
+ .shp, .shp-form, .shp-order { --shp-soft: color-mix(in srgb, var(--dm-text) 5%, transparent); --shp-line: var(--dm-border); }
3
+ .shp [hidden], .shp-form [hidden], .shp-order [hidden] { display: none !important; }
4
+ .shp { display: flex; flex-direction: column; gap: 1rem; }
5
+ .shp-body { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
6
+ .shp-error { color: var(--dm-danger); }
7
+ .shp-muted, .shp-note { color: var(--dm-text-muted); }
8
+ .shp-note { margin: 0; font-size: var(--dm-text-sm); }
9
+ .shp-note.is-bad { color: var(--dm-danger); }
10
+ .shp-spacer { flex: 1; }
11
+ .shp-loading { color: var(--dm-text-muted); padding: 2rem 0; }
12
+
13
+ /* Tabs under the banner */
14
+ .shp-nav { display: flex; flex-wrap: wrap; gap: .15rem; border-bottom: 1px solid var(--dm-border); }
15
+ .shp-nav a { display: inline-flex; align-items: center; gap: .4rem; padding: .5rem .85rem; color: var(--dm-text-muted); text-decoration: none;
16
+ font-size: var(--dm-text-sm); font-weight: 500; border-bottom: 2px solid transparent; margin-bottom: -1px; border-radius: var(--dm-radius-sm) var(--dm-radius-sm) 0 0; }
17
+ .shp-nav a:hover { color: var(--dm-text); background: var(--shp-soft); }
18
+ .shp-nav a.is-on { color: var(--dm-primary); border-bottom-color: var(--dm-primary); font-weight: 600; }
19
+ .shp-nav a.is-ext::after { content: 'Pro'; font-size: .62rem; font-weight: 700; letter-spacing: .04em; padding: .05rem .35rem; border-radius: 999px;
20
+ background: color-mix(in srgb, var(--dm-primary) 15%, transparent); color: var(--dm-primary); }
21
+
22
+ /* Cards, bars, chips */
23
+ .shp-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .9rem 1rem; display: flex; flex-direction: column; gap: .7rem;
24
+ background: var(--dm-surface, transparent); min-width: 0; }
25
+ .shp-card > header { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
26
+ .shp-card h3 { margin: 0; font-size: var(--dm-text-base, 1rem); font-weight: 600; display: flex; align-items: center; gap: .3rem; }
27
+ .shp-more { font-size: var(--dm-text-sm); color: var(--dm-primary); text-decoration: none; }
28
+ .shp-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
29
+ .shp-search { flex: 1; min-width: 10rem; max-width: 22rem; }
30
+ .shp-seg { display: inline-flex; flex-wrap: wrap; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
31
+ .shp-seg button { border: 0; background: none; color: var(--dm-text); padding: .32rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm);
32
+ display: inline-flex; align-items: center; gap: .3rem; }
33
+ .shp-seg button + button { border-left: 1px solid var(--dm-border); }
34
+ .shp-seg button b { font-weight: 500; opacity: .6; }
35
+ .shp-seg button:hover { background: var(--shp-soft); }
36
+ .shp-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text, #fff); }
37
+ .shp-chip { display: inline-flex; align-items: center; gap: .25rem; font-size: var(--dm-text-xs); font-weight: 600; padding: .12rem .55rem; border-radius: 999px;
38
+ white-space: nowrap; background: color-mix(in srgb, var(--dm-text) 9%, transparent); color: var(--dm-text-muted); }
39
+ .shp-chip.is-primary { background: color-mix(in srgb, var(--dm-primary) 14%, transparent); color: var(--dm-primary); }
40
+ .shp-chip.is-success { background: color-mix(in srgb, var(--dm-success) 15%, transparent); color: var(--dm-success); }
41
+ .shp-chip.is-warning { background: color-mix(in srgb, var(--dm-warning) 16%, transparent); color: color-mix(in srgb, var(--dm-warning) 55%, var(--dm-text)); }
42
+ .shp-chip.is-danger { background: color-mix(in srgb, var(--dm-danger) 14%, transparent); color: var(--dm-danger); }
43
+
44
+ /* Rows */
45
+ .shp-list { display: flex; flex-direction: column; }
46
+ .shp-row { display: flex; align-items: center; gap: .85rem; padding: .6rem .4rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; border-radius: var(--dm-radius-sm); }
47
+ .shp-row:last-child { border-bottom: 0; }
48
+ .shp-row:hover, .shp-row:focus-visible { background: var(--shp-soft); outline: none; }
49
+ .shp-num { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--dm-text-muted); min-width: 3.4rem; }
50
+ .shp-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
51
+ .shp-main strong, .shp-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
52
+ .shp-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
53
+ .shp-amt { font-variant-numeric: tabular-nums; font-weight: 600; min-width: 5rem; text-align: right; }
54
+ .shp-kebab { border: 0; background: none; cursor: pointer; font-size: 1.15rem; color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .35rem; line-height: 1.6; }
55
+ .shp-kebab:hover { color: var(--dm-primary); background: var(--shp-soft); }
56
+ .shp-thumb { width: 2.6rem; height: 2.6rem; border-radius: var(--dm-radius-sm); overflow: hidden; flex: none; background: var(--shp-soft); }
57
+ .shp-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
58
+ .shp-star { color: var(--dm-warning); font-size: .85em; }
59
+ .shp-stock { font-size: var(--dm-text-xs); color: var(--dm-text-muted); min-width: 5.5rem; text-align: right; font-variant-numeric: tabular-nums; }
60
+ .shp-stock.is-warning { color: color-mix(in srgb, var(--dm-warning) 55%, var(--dm-text)); font-weight: 600; }
61
+ .shp-stock.is-danger { color: var(--dm-danger); font-weight: 600; }
62
+ @media (max-width: 720px) { .shp-stock, .shp-num { display: none; } .shp-amt { min-width: 0; } }
63
+
64
+ /* Overview */
65
+ .shp-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: .75rem; }
66
+ .shp-tile { position: relative; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .9rem 1rem; display: flex; flex-direction: column;
67
+ gap: .1rem; color: inherit; text-decoration: none; background: var(--dm-surface, transparent); overflow: hidden; }
68
+ a.shp-tile:hover { border-color: var(--dm-primary); }
69
+ .shp-tile-icon { position: absolute; top: .85rem; right: .9rem; color: var(--dm-text-muted); opacity: .7; }
70
+ .shp-tile-label { font-size: var(--dm-text-sm); color: var(--dm-text-muted); }
71
+ .shp-tile-value { font-size: 1.65rem; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
72
+ .shp-tile-sub { font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
73
+ .shp-tile.is-up .shp-tile-sub { color: var(--dm-success); font-weight: 600; }
74
+ .shp-tile.is-down .shp-tile-sub { color: var(--dm-danger); font-weight: 600; }
75
+ .shp-tile.is-attention { border-color: color-mix(in srgb, var(--dm-primary) 50%, var(--dm-border)); box-shadow: inset 3px 0 0 var(--dm-primary); }
76
+ .shp-grid2 { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: 1rem; }
77
+ @media (max-width: 960px) { .shp-grid2 { grid-template-columns: 1fr; } }
78
+
79
+ .shp-chart { position: relative; }
80
+ .shp-chart svg { width: 100%; height: auto; display: block; overflow: visible; }
81
+ .shp-chart .shp-gridline { stroke: var(--dm-border); stroke-width: 1; }
82
+ .shp-chart text { fill: var(--dm-text-muted); font-size: 11px; font-family: inherit; }
83
+ .shp-bar path { fill: var(--dm-primary); opacity: .82; transition: opacity .15s; }
84
+ .shp-bar .shp-bar-zero { fill: var(--dm-border); }
85
+ .shp-bar.is-on path, .shp-bar:focus path { opacity: 1; }
86
+ .shp-bar:focus { outline: none; }
87
+ .shp-bar { cursor: default; }
88
+ .shp-tip { position: absolute; top: -.25rem; width: 120px; pointer-events: none; background: var(--dm-surface, #fff); color: var(--dm-text); border: 1px solid var(--dm-border);
89
+ border-radius: var(--dm-radius-sm); padding: .35rem .55rem; box-shadow: var(--dm-shadow-md, 0 6px 20px rgba(0,0,0,.15)); display: flex; flex-direction: column; font-size: var(--dm-text-xs); }
90
+ .shp-tip strong { font-size: var(--dm-text-sm); }
91
+ .shp-tip span { color: var(--dm-text-muted); }
92
+
93
+ .shp-todo { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .3rem; }
94
+ .shp-todo-item { width: 100%; display: flex; align-items: center; gap: .6rem; border: 0; background: var(--shp-soft); color: var(--dm-text); border-radius: var(--dm-radius-sm);
95
+ padding: .55rem .7rem; cursor: pointer; text-align: left; font: inherit; font-size: var(--dm-text-sm); }
96
+ .shp-todo-item:hover { background: color-mix(in srgb, var(--dm-primary) 10%, transparent); }
97
+ .shp-todo-item > span:nth-child(2) { flex: 1; }
98
+ .shp-todo-item.is-danger > span:first-child { color: var(--dm-danger); }
99
+ .shp-todo-item.is-warning > span:first-child { color: var(--dm-warning); }
100
+ .shp-todo-go { color: var(--dm-text-muted); }
101
+ .shp-all-clear { display: flex; align-items: center; gap: .5rem; color: var(--dm-success); padding: .5rem 0; }
102
+ .shp-mini-list { display: flex; flex-direction: column; }
103
+ .shp-mini { display: flex; align-items: center; gap: .75rem; padding: .5rem .3rem; border-bottom: 1px solid var(--dm-border); color: inherit; text-decoration: none; border-radius: var(--dm-radius-sm); }
104
+ .shp-mini:last-child { border-bottom: 0; }
105
+ .shp-mini:hover { background: var(--shp-soft); }
106
+ .shp-mini-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
107
+ .shp-mini-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
108
+ .shp-mini-amt { font-weight: 600; font-variant-numeric: tabular-nums; }
109
+ .shp-top { display: grid; grid-template-columns: minmax(0, 1fr) 30% auto; gap: .75rem; align-items: center; padding: .4rem 0; }
110
+ .shp-top-name { display: flex; flex-direction: column; min-width: 0; }
111
+ .shp-top-name small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
112
+ .shp-top-bar { height: 8px; border-radius: 999px; background: var(--shp-soft); overflow: hidden; }
113
+ .shp-top-bar span { display: block; height: 100%; background: var(--dm-primary); border-radius: 0 4px 4px 0; }
114
+
115
+ .shp-welcome { border: 1px dashed var(--dm-border); border-radius: var(--dm-radius-lg, 14px); padding: 2rem; text-align: center; max-width: 46rem; margin: 1rem auto; }
116
+ .shp-welcome h2 { margin: .5rem 0 .25rem; }
117
+ .shp-welcome-icon { display: inline-grid; place-items: center; width: 3.6rem; height: 3.6rem; border-radius: 50%; color: var(--dm-primary);
118
+ background: color-mix(in srgb, var(--dm-primary) 12%, transparent); }
119
+ .shp-steps { list-style: none; padding: 0; margin: 1.5rem 0 0; display: grid; gap: .6rem; text-align: left; counter-reset: st; }
120
+ .shp-steps li { counter-increment: st; display: grid; grid-template-columns: 2rem 1fr auto; grid-template-rows: auto auto; column-gap: .75rem; align-items: center;
121
+ border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem 1rem; }
122
+ .shp-steps li::before { content: counter(st); grid-row: 1 / 3; width: 2rem; height: 2rem; border-radius: 50%; display: grid; place-items: center;
123
+ background: var(--shp-soft); font-weight: 700; }
124
+ .shp-steps li.is-done::before { content: '✓'; background: var(--dm-success); color: #fff; }
125
+ .shp-steps li span { grid-column: 2; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
126
+ .shp-steps li .btn { grid-column: 3; grid-row: 1 / 3; }
127
+
128
+ /* Forms (slideovers) */
129
+ .shp-form { display: flex; flex-direction: column; gap: .9rem; }
130
+ .shp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem 1rem; }
131
+ .shp-wide { grid-column: 1 / -1; }
132
+ @media (max-width: 640px) { .shp-grid { grid-template-columns: 1fr; } }
133
+ .shp-form .form-label { display: flex; align-items: center; gap: .3rem; margin-bottom: .25rem; }
134
+ .shp-input-lg { font-size: 1.1rem; font-weight: 600; }
135
+ .shp-mono { font-family: var(--dm-font-mono, ui-monospace, monospace); font-size: .88rem; }
136
+ .shp-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
137
+ .shp-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .3rem; }
138
+ .shp-link { border: 0; background: none; padding: 0; color: var(--dm-primary); cursor: pointer; font: inherit; text-decoration: underline; }
139
+ .shp-check { display: inline-flex; align-items: center; gap: .45rem; cursor: pointer; font-size: var(--dm-text-sm); }
140
+ .shp-checks { display: flex; flex-direction: column; gap: .5rem; }
141
+ .shp-toggles { display: flex; align-items: flex-end; padding-bottom: .5rem; }
142
+ .shp-money { display: flex; align-items: stretch; }
143
+ .shp-money > span { display: grid; place-items: center; padding: 0 .6rem; border: 1px solid var(--dm-border); border-right: 0; border-radius: var(--dm-radius-sm) 0 0 var(--dm-radius-sm);
144
+ background: var(--shp-soft); color: var(--dm-text-muted); font-size: var(--dm-text-xs); font-weight: 600; }
145
+ .shp-money input { border-top-left-radius: 0; border-bottom-left-radius: 0; }
146
+ .shp-actions { display: flex; align-items: center; gap: .4rem; padding-top: .75rem; border-top: 1px solid var(--dm-border); position: sticky; bottom: 0;
147
+ background: var(--dm-surface, var(--dm-body-bg, #fff)); padding-bottom: .25rem; }
148
+ .shp-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
149
+ .shp-sect { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .6rem .9rem; }
150
+ .shp-sect[open] { padding-bottom: .9rem; }
151
+ .shp-sect > summary { cursor: pointer; font-weight: 600; padding: .2rem 0; }
152
+ .shp-sect[open] > summary { margin-bottom: .6rem; }
153
+ .shp-sect > * + * { margin-top: .6rem; }
154
+
155
+ .shp-pics { display: flex; flex-wrap: wrap; gap: .5rem; }
156
+ .shp-pic, .shp-pic-add { position: relative; width: 5.5rem; height: 5.5rem; border-radius: var(--dm-radius-md); overflow: hidden; }
157
+ .shp-pic { cursor: grab; background: var(--shp-soft); }
158
+ .shp-pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
159
+ .shp-pic-x { position: absolute; top: .25rem; right: .25rem; width: 1.4rem; height: 1.4rem; border-radius: 50%; border: 0; background: rgba(0,0,0,.6); color: #fff;
160
+ cursor: pointer; line-height: 1; opacity: 0; transition: opacity .15s; }
161
+ .shp-pic:hover .shp-pic-x, .shp-pic-x:focus { opacity: 1; }
162
+ .shp-pic-main { position: absolute; left: .25rem; bottom: .25rem; font-size: .62rem; font-weight: 700; background: var(--dm-primary); color: var(--dm-primary-text, #fff);
163
+ padding: .05rem .35rem; border-radius: 999px; }
164
+ .shp-pic-add { border: 1.5px dashed var(--dm-border); background: none; color: var(--dm-text-muted); cursor: pointer; display: flex; flex-direction: column;
165
+ align-items: center; justify-content: center; gap: .2rem; font: inherit; font-size: var(--dm-text-xs); }
166
+ .shp-pic-add:hover { border-color: var(--dm-primary); color: var(--dm-primary); }
167
+ .shp-media { display: grid; grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr)); gap: .5rem; }
168
+ .shp-media-tile { border: 2px solid transparent; border-radius: var(--dm-radius-md); padding: 0; background: var(--shp-soft); cursor: pointer; overflow: hidden;
169
+ display: flex; flex-direction: column; color: var(--dm-text); font: inherit; font-size: var(--dm-text-xs); }
170
+ .shp-media-tile img { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; }
171
+ .shp-media-tile span { padding: .2rem .35rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
172
+ .shp-media-tile.is-on { border-color: var(--dm-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--dm-primary) 30%, transparent); }
173
+
174
+ .shp-variants { display: flex; flex-direction: column; gap: .5rem; align-items: flex-start; }
175
+ .shp-variants-head { display: flex; align-items: center; gap: .5rem; }
176
+ .shp-variants-head .form-label { margin: 0; white-space: nowrap; }
177
+ .shp-optname { max-width: 12rem; }
178
+ .shp-vtable { width: 100%; display: flex; flex-direction: column; gap: .35rem; }
179
+ .shp-vrow { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1.2fr) minmax(0, .9fr) 1.8rem; gap: .35rem; align-items: center; }
180
+ .shp-vrow.is-head { font-size: var(--dm-text-xs); color: var(--dm-text-muted); font-weight: 600; }
181
+ .shp-vrow .form-input { padding: .35rem .5rem; font-size: var(--dm-text-sm); }
182
+
183
+ /* An order */
184
+ .shp-order { display: flex; flex-direction: column; gap: 1rem; }
185
+ .shp-order-head { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; }
186
+ .shp-order-acts { display: flex; gap: .35rem; }
187
+ .shp-order-acts .btn { display: inline-flex; align-items: center; gap: .3rem; }
188
+ .shp-lines { width: 100%; border-collapse: collapse; }
189
+ .shp-lines td { padding: .5rem .35rem; border-bottom: 1px solid var(--dm-border); vertical-align: middle; }
190
+ .shp-lines td small { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
191
+ .shp-lines-img { width: 3rem; }
192
+ .shp-lines-img img { width: 2.6rem; height: 2.6rem; border-radius: var(--dm-radius-sm); object-fit: cover; display: block; }
193
+ .shp-lines .shp-num { text-align: right; white-space: nowrap; color: var(--dm-text-muted); font-weight: 500; }
194
+ .shp-totals { margin: 0 0 0 auto; width: min(22rem, 100%); display: flex; flex-direction: column; gap: .3rem; font-variant-numeric: tabular-nums; }
195
+ .shp-totals > div { display: flex; justify-content: space-between; gap: 1rem; font-size: var(--dm-text-sm); }
196
+ .shp-totals dt { color: var(--dm-text-muted); } .shp-totals dd { margin: 0; font-weight: 600; }
197
+ .shp-totals .is-included { font-size: var(--dm-text-xs); } .shp-totals .is-included dd { font-weight: 400; color: var(--dm-text-muted); }
198
+ .shp-totals .is-total { border-top: 1px solid var(--dm-border); padding-top: .4rem; font-size: 1.05rem; }
199
+ .shp-totals .is-total dt { color: var(--dm-text); font-weight: 700; } .shp-totals .is-total dd { font-weight: 800; }
200
+ .shp-order-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
201
+ @media (max-width: 640px) { .shp-order-cols { grid-template-columns: 1fr; } }
202
+ .shp-order h4 { margin: 0 0 .35rem; font-size: var(--dm-text-xs); text-transform: uppercase; letter-spacing: .06em; color: var(--dm-text-muted); }
203
+ .shp-order p { margin: 0; }
204
+ .shp-order a { color: var(--dm-primary); }
205
+ .shp-callout { border-left: 3px solid var(--dm-warning); background: color-mix(in srgb, var(--dm-warning) 9%, transparent); padding: .6rem .8rem; border-radius: var(--dm-radius-sm); }
206
+ .shp-callout p { white-space: pre-wrap; margin-top: .25rem; }
207
+ .shp-ref { font-size: var(--dm-text-xs); }
208
+ .shp-ext { display: flex; flex-wrap: wrap; gap: .3rem; }
209
+ .shp-history { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .4rem; border-left: 2px solid var(--dm-border); padding-left: .8rem; }
210
+ .shp-history li { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; font-size: var(--dm-text-sm); }
211
+ .shp-history li > span:last-child { color: var(--dm-text-muted); }
212
+
213
+ /* Settings */
214
+ .shp-settings-nav { align-self: flex-start; }
215
+ .shp-countries { display: flex; flex-wrap: wrap; gap: .3rem; margin-bottom: .45rem; }
216
+ .shp-country button { border: 0; background: none; color: inherit; cursor: pointer; padding: 0 0 0 .2rem; font-size: 1rem; line-height: 1; }
217
+ .shp-add-country { max-width: 16rem; }
218
+ .shp-pay { display: flex; flex-direction: column; gap: .75rem; }
219
+ .shp-provider { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .75rem; }
220
+ .shp-provider-head { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
221
+ .shp-copy { display: flex; align-items: center; gap: .3rem; background: var(--shp-soft); border-radius: var(--dm-radius-sm); padding: .3rem .5rem; }
222
+ .shp-copy code { flex: 1; overflow-wrap: anywhere; font-size: var(--dm-text-xs); }
223
+ .shp-steps li .shp-step-btns { grid-column: 3; grid-row: 1 / 3; display: flex; gap: .3rem; color: inherit; font-size: inherit; }
224
+ .shp-steps li .shp-step-btns .btn { grid-column: auto; grid-row: auto; }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Shop admin - what every screen shares: the admin's helpers (loaded at run
3
+ * time; a plugin never imports admin/ statically - the updater replaces it),
4
+ * the API, escaping, money in and out, slideover forms, and rows with a
5
+ * right-click / ⋮ / Shift+F10 menu.
6
+ *
7
+ * @module shopping-cart/admin/lib/kit
8
+ */
9
+
10
+ import {formatMoney, fromMinor, toMinor} from '/plugins/shopping-cart/public/lib/money.js';
11
+
12
+ export const SIDEBAR_URL = '#/plugins/shopping-cart';
13
+ export const BASE = '/api/plugins/shopping-cart';
14
+
15
+ let kit = null;
16
+
17
+ /** tool-kit, plugin-chrome and help-popover, once, with stand-ins for an older admin. */
18
+ export function loadKit() {
19
+ if (!kit) {
20
+ kit = Promise.all([
21
+ import('/admin/js/lib/tool-kit.js').catch(() => ({})),
22
+ import('/admin/js/lib/plugin-chrome.js').catch(() => ({})),
23
+ import('/admin/js/lib/help-popover.js').catch(() => ({}))
24
+ ]).then(([tk, chrome, help]) => ({
25
+ onceClosed: tk.onceClosed || ((so, fn) => { const c = so.close.bind(so); so.close = (...a) => { const r = c(...a); fn?.(); return r; }; }),
26
+ openMenuAt: tk.openMenuAt || ((el) => {
27
+ const r = el.getBoundingClientRect();
28
+ el.dispatchEvent(new MouseEvent('contextmenu', {bubbles: true, cancelable: true, clientX: r.left, clientY: r.bottom, view: window}));
29
+ }),
30
+ viewLifetime: tk.viewLifetime || (() => ({add() {}, reset() {}})),
31
+ addBannerAction: chrome.addBannerAction || (() => null),
32
+ helpPopovers: help.helpPopovers || (() => () => {})
33
+ }));
34
+ }
35
+ return kit;
36
+ }
37
+
38
+ /** The Shop's API. H attaches the token and refreshes it on a 401; it throws with the server's `error`. */
39
+ const seg = (id) => encodeURIComponent(String(id));
40
+ export const api = {
41
+ summary: () => H.get(`${BASE}/summary`),
42
+ extensions: () => H.get(`${BASE}/extensions`),
43
+ products: {
44
+ list: () => H.get(`${BASE}/products`),
45
+ create: (d) => H.post(`${BASE}/products`, d),
46
+ update: (id, d) => H.put(`${BASE}/products/${seg(id)}`, d),
47
+ patch: (id, d) => H.patch(`${BASE}/products/${seg(id)}`, d),
48
+ duplicate: (id) => H.post(`${BASE}/products/${seg(id)}/duplicate`, {}),
49
+ remove: (id) => H.delete(`${BASE}/products/${seg(id)}`),
50
+ samples: () => H.post(`${BASE}/samples`, {live: true})
51
+ },
52
+ orders: {
53
+ list: () => H.get(`${BASE}/orders`),
54
+ get: (id) => H.get(`${BASE}/orders/${seg(id)}`),
55
+ move: (id, body) => H.put(`${BASE}/orders/${seg(id)}/status`, body),
56
+ note: (id, body) => H.put(`${BASE}/orders/${seg(id)}/note`, body),
57
+ refund: (id, body) => H.post(`${BASE}/orders/${seg(id)}/refund`, body),
58
+ remove: (id) => H.delete(`${BASE}/orders/${seg(id)}`)
59
+ },
60
+ settings: {
61
+ get: () => H.get(`${BASE}/settings`),
62
+ save: (d) => H.put(`${BASE}/settings`, d),
63
+ test: (provider, d) => H.post(`${BASE}/settings/test/${seg(provider)}`, d)
64
+ }
65
+ };
66
+
67
+ /** A list route's entries, whatever shape they came back in. */
68
+ export const entriesOf = (res) => (Array.isArray(res) ? res : res?.entries ?? res?.data ?? []);
69
+
70
+ /** E.confirm / titles render HTML: user text goes in escaped. */
71
+ export const esc = (s) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
72
+
73
+ /** A small DOM tree from an HTML string that holds NO unescaped user data. */
74
+ export function fragment(html) {
75
+ const t = document.createElement('template');
76
+ t.innerHTML = html.trim();
77
+ return t.content.firstElementChild;
78
+ }
79
+
80
+ export const store = {
81
+ get(k) { try { return S.get(k); } catch { return null; } },
82
+ set(k, v) { try { S.set(k, v); } catch { /* a convenience */ } }
83
+ };
84
+
85
+ /** The sidebar re-asks the Shop's badge. */
86
+ export function refreshBadge() {
87
+ try { window.dispatchEvent(new CustomEvent('dm:sidebar-count', {detail: {url: SIDEBAR_URL}})); } catch { /* no sidebar */ }
88
+ }
89
+
90
+ // ---- Money ------------------------------------------------------------------------
91
+
92
+ let currency = 'GBP';
93
+ export const setCurrency = (c) => { currency = c || 'GBP'; };
94
+ export const getCurrency = () => currency;
95
+ export const money = (n, opts) => formatMoney(n, currency, opts);
96
+ /** For an input: "12.50". Blank for null. */
97
+ export const moneyIn = (n) => (n === null || n === undefined || n === '' ? '' : fromMinor(n, currency));
98
+ /** From an input: 1250, null when blank, NaN when not an amount. */
99
+ export const moneyOut = (text) => {
100
+ if (String(text ?? '').trim() === '') return null;
101
+ const m = toMinor(text, currency);
102
+ return m === null ? NaN : m;
103
+ };
104
+
105
+ /** "3 min ago", "26 Sep". */
106
+ export function when(iso) {
107
+ const t = new Date(iso).getTime();
108
+ if (!Number.isFinite(t)) return '';
109
+ const s = (Date.now() - t) / 1000;
110
+ if (s < 60) return 'just now';
111
+ if (s < 3600) return `${Math.round(s / 60)} min ago`;
112
+ if (s < 86400) return `${Math.round(s / 3600)} h ago`;
113
+ if (s < 86400 * 6) return `${Math.round(s / 86400)} d ago`;
114
+ return new Date(t).toLocaleDateString(undefined, {day: 'numeric', month: 'short', ...(s > 86400 * 300 ? {year: 'numeric'} : {})});
115
+ }
116
+
117
+ // ---- Slideovers ---------------------------------------------------------------------
118
+
119
+ /**
120
+ * Open a slideover holding `html`, bound to `model`. Closing with something
121
+ * changed asks first (once). Everything is disposed and removed on close.
122
+ *
123
+ * @returns {Promise<{so, panel, close: (force?: boolean) => void}>}
124
+ */
125
+ export async function openPanel({title, size = 'md', html, model = {}, methods = {}, dirty, cleanup = () => {}, before}) {
126
+ const {onceClosed, helpPopovers} = await loadKit();
127
+ const so = E.slideover({title: esc(title), size, position: 'right'});
128
+ const panel = fragment(html);
129
+ so.element.appendChild(panel);
130
+ before?.(panel);
131
+ const disposeHelp = helpPopovers(panel);
132
+ const handle = M.applyBindings(model, panel, {methods});
133
+ onceClosed(so, () => { cleanup(); handle.dispose(); disposeHelp(); });
134
+ const close = so.close.bind(so);
135
+ let closed = false;
136
+ let asking = false;
137
+ so.close = async (force) => {
138
+ if (!closed && force !== true && dirty?.()) {
139
+ if (asking) return undefined;
140
+ asking = true;
141
+ const ok = await E.confirm('Discard your changes?');
142
+ asking = false;
143
+ if (!ok) return undefined;
144
+ }
145
+ closed = true;
146
+ return close();
147
+ };
148
+ panel.addEventListener('keydown', (e) => {
149
+ if ((e.ctrlKey || e.metaKey) && (e.key === 'Enter' || e.key === 's') && methods.save) { e.preventDefault(); methods.save(); }
150
+ });
151
+ I.scan(panel);
152
+ so.open();
153
+ return {so, panel, close: (force) => so.close(force)};
154
+ }
155
+
156
+ /**
157
+ * Rows: click (or Enter) = the main action; right-click, ⋮ or Shift+F10 = the
158
+ * menu. `scope` receives the teardown.
159
+ */
160
+ export function wireRows(list, {items, main, openMenuAt, scope, match = '.shp-row'}) {
161
+ const menu = E.contextMenu(list, {cursor: 'glyph', match, inherit: false, density: 'compact',
162
+ items: (row) => items(row.dataset.id)});
163
+ const onClick = (e) => {
164
+ const row = e.target.closest(match);
165
+ if (!row) return;
166
+ if (e.target.closest('[data-stop]')) return;
167
+ const kebab = e.target.closest('.shp-kebab');
168
+ if (kebab) { openMenuAt(kebab); return; }
169
+ main(row.dataset.id);
170
+ };
171
+ const onKey = (e) => {
172
+ const row = e.target.closest(match);
173
+ if (!row) return;
174
+ if (e.key === 'F10' && e.shiftKey) { e.preventDefault(); openMenuAt(row); }
175
+ else if (e.key === 'Enter' && e.target === row) main(row.dataset.id);
176
+ else if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && e.target === row) {
177
+ e.preventDefault();
178
+ const rows = [...list.querySelectorAll(match)].filter(r => r.offsetParent);
179
+ rows[rows.indexOf(row) + (e.key === 'ArrowDown' ? 1 : -1)]?.focus();
180
+ }
181
+ };
182
+ list.addEventListener('click', onClick);
183
+ list.addEventListener('keydown', onKey);
184
+ scope(() => { list.removeEventListener('click', onClick); list.removeEventListener('keydown', onKey); menu?.destroy?.(); });
185
+ }
186
+
187
+ /** Copy text, and say so. */
188
+ export function copy(text, what = 'Copied') {
189
+ navigator.clipboard?.writeText(text).then(() => E.toast(`${what}.`, {type: 'success'}), () => E.toast(text));
190
+ }
@@ -0,0 +1 @@
1
+ <div class="shp" data-shop-admin></div>