domma-cms 0.84.0 → 0.85.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.
- package/README.md +4 -4
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +4 -4
- package/admin/js/lib/themes.js +1 -1
- package/package.json +2 -2
- package/plugins/free-tier.lock.json +53 -5
- package/plugins/security/plugin.js +10 -0
- package/plugins/security/plugin.json +1 -1
- package/plugins/security/server/facts.js +3 -1
- package/plugins/shopping-cart/CLAUDE.md +93 -0
- package/plugins/shopping-cart/admin/css/index.css +224 -0
- package/plugins/shopping-cart/admin/lib/kit.js +190 -0
- package/plugins/shopping-cart/admin/templates/shop.html +1 -0
- package/plugins/shopping-cart/admin/views/orders.js +365 -0
- package/plugins/shopping-cart/admin/views/overview.js +214 -0
- package/plugins/shopping-cart/admin/views/products.js +547 -0
- package/plugins/shopping-cart/admin/views/settings.js +247 -0
- package/plugins/shopping-cart/admin/views/shop.js +130 -0
- package/plugins/shopping-cart/collections/shop-orders.json +20 -0
- package/plugins/shopping-cart/collections/shop-products.json +24 -0
- package/plugins/shopping-cart/config.js +52 -0
- package/plugins/shopping-cart/inject/body.html +2 -0
- package/plugins/shopping-cart/lib/extensions.js +96 -0
- package/plugins/shopping-cart/lib/payments.js +281 -0
- package/plugins/shopping-cart/lib/pricing.js +232 -0
- package/plugins/shopping-cart/lib/render.js +282 -0
- package/plugins/shopping-cart/lib/samples.js +34 -0
- package/plugins/shopping-cart/lib/shop.js +436 -0
- package/plugins/shopping-cart/lib/summary.js +127 -0
- package/plugins/shopping-cart/plugin.js +559 -0
- package/plugins/shopping-cart/plugin.json +52 -0
- package/plugins/shopping-cart/plugin.public.js +144 -0
- package/plugins/shopping-cart/public/cart.js +715 -0
- package/plugins/shopping-cart/public/lib/money.js +105 -0
- package/plugins/shopping-cart/public/lib/orders.js +122 -0
- package/plugins/shopping-cart/public/lib/products.js +235 -0
- package/plugins/shopping-cart/public/samples/candle-2.svg +16 -0
- package/plugins/shopping-cart/public/samples/candle.svg +16 -0
- package/plugins/shopping-cart/public/samples/mug-2.svg +14 -0
- package/plugins/shopping-cart/public/samples/mug.svg +14 -0
- package/plugins/shopping-cart/public/samples/notebook-2.svg +15 -0
- package/plugins/shopping-cart/public/samples/notebook.svg +15 -0
- package/plugins/shopping-cart/public/samples/print-2.svg +14 -0
- package/plugins/shopping-cart/public/samples/print.svg +14 -0
- package/plugins/shopping-cart/public/samples/tee-2.svg +12 -0
- package/plugins/shopping-cart/public/samples/tee.svg +12 -0
- package/plugins/shopping-cart/public/samples/tote-2.svg +13 -0
- package/plugins/shopping-cart/public/samples/tote.svg +13 -0
- package/plugins/shopping-cart/public/shop.css +415 -0
- package/plugins/shopping-cart/templates/shop.html +4 -0
- package/plugins/shopping-cart/tests/api.test.js +144 -0
- package/plugins/shopping-cart/tests/pure.test.js +210 -0
- package/scripts/setup.js +1 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 1000" width="800" height="1000">
|
|
2
|
+
<defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#dfe8d2"/><stop offset="1" stop-color="#a9c29b"/></linearGradient>
|
|
3
|
+
<radialGradient id="glow" cx=".3" cy=".2" r=".8"><stop offset="0" stop-color="#fff" stop-opacity=".45"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></radialGradient></defs>
|
|
4
|
+
<rect width="800" height="1000" fill="url(#bg)"/><rect width="800" height="1000" fill="url(#glow)"/><circle cx="640" cy="180" r="220" fill="#fff" opacity=".18"/>
|
|
5
|
+
<ellipse cx="400" cy="800" rx="230" ry="26" fill="#000" opacity=".13"/>
|
|
6
|
+
<g transform="translate(400 520) scale(1.12) translate(-400 -520)">
|
|
7
|
+
<path d="M300 360c0-110 40-170 100-170s100 60 100 170" fill="none" stroke="#e8dcc4" stroke-width="26"/>
|
|
8
|
+
<path d="M300 360c0-110 40-170 100-170s100 60 100 170" fill="none" stroke="#cdbd9c" stroke-width="26" stroke-dasharray="4 18"/>
|
|
9
|
+
<path d="M220 360h360l30 440H190z" fill="#efe4cc"/>
|
|
10
|
+
<path d="M220 360h360l4 50H216z" fill="#e2d4b6"/>
|
|
11
|
+
<circle cx="400" cy="590" r="95" fill="none" stroke="#4f6b45" stroke-width="14"/>
|
|
12
|
+
<path d="M400 510c-30 40-30 120 0 160M400 510c30 40 30 120 0 160M320 590h160" fill="none" stroke="#4f6b45" stroke-width="10" stroke-linecap="round"/></g>
|
|
13
|
+
</svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 1000" width="800" height="1000">
|
|
2
|
+
<defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#dfe8d2"/><stop offset="1" stop-color="#a9c29b"/></linearGradient>
|
|
3
|
+
<radialGradient id="glow" cx=".3" cy=".2" r=".8"><stop offset="0" stop-color="#fff" stop-opacity=".45"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></radialGradient></defs>
|
|
4
|
+
<rect width="800" height="1000" fill="url(#bg)"/><rect width="800" height="1000" fill="url(#glow)"/>
|
|
5
|
+
<ellipse cx="400" cy="800" rx="230" ry="26" fill="#000" opacity=".13"/>
|
|
6
|
+
|
|
7
|
+
<path d="M300 360c0-110 40-170 100-170s100 60 100 170" fill="none" stroke="#e8dcc4" stroke-width="26"/>
|
|
8
|
+
<path d="M300 360c0-110 40-170 100-170s100 60 100 170" fill="none" stroke="#cdbd9c" stroke-width="26" stroke-dasharray="4 18"/>
|
|
9
|
+
<path d="M220 360h360l30 440H190z" fill="#efe4cc"/>
|
|
10
|
+
<path d="M220 360h360l4 50H216z" fill="#e2d4b6"/>
|
|
11
|
+
<circle cx="400" cy="590" r="95" fill="none" stroke="#4f6b45" stroke-width="14"/>
|
|
12
|
+
<path d="M400 510c-30 40-30 120 0 160M400 510c30 40 30 120 0 160M320 590h160" fill="none" stroke="#4f6b45" stroke-width="10" stroke-linecap="round"/>
|
|
13
|
+
</svg>
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shopping Cart - the storefront, the basket drawer and checkout.
|
|
3
|
+
*
|
|
4
|
+
* Built on the site's theme tokens (--dm-*), with a fallback for each, so the
|
|
5
|
+
* shop wears whatever theme the site wears - light, dark or custom. Loaded on
|
|
6
|
+
* every public page (the drawer and the [shop-*] shortcodes can be anywhere),
|
|
7
|
+
* so it is kept to the storefront's own classes: nothing here styles a bare
|
|
8
|
+
* element outside .shop-*.
|
|
9
|
+
*
|
|
10
|
+
* The tokens are resolved on body, not :root: a theme sets its --dm-* on the
|
|
11
|
+
* body's dm-theme-* class, and a custom property defined on :root would
|
|
12
|
+
* freeze the root's (light) values for every descendant.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
--shop-accent: var(--dm-primary, #4f46e5);
|
|
17
|
+
--shop-accent-ink: #fff;
|
|
18
|
+
--shop-bg: var(--dm-surface, #fff);
|
|
19
|
+
--shop-page: var(--dm-background, var(--shop-bg));
|
|
20
|
+
--shop-text: var(--dm-text, #16181d);
|
|
21
|
+
--shop-muted: var(--dm-text-muted, #6b7280);
|
|
22
|
+
--shop-line: var(--dm-border, rgba(127, 127, 127, .22));
|
|
23
|
+
--shop-soft: color-mix(in srgb, var(--shop-text) 5%, transparent);
|
|
24
|
+
--shop-softer: color-mix(in srgb, var(--shop-text) 3%, transparent);
|
|
25
|
+
--shop-sale: var(--dm-danger, #e11d48);
|
|
26
|
+
--shop-good: var(--dm-success, #16a34a);
|
|
27
|
+
--shop-warn: var(--dm-warning, #d97706);
|
|
28
|
+
--shop-radius: var(--dm-radius-lg, 14px);
|
|
29
|
+
--shop-radius-sm: var(--dm-radius-md, 10px);
|
|
30
|
+
--shop-shadow: 0 1px 2px rgba(0, 0, 0, .06), 0 8px 24px -12px rgba(0, 0, 0, .25);
|
|
31
|
+
--shop-shadow-lift: 0 2px 4px rgba(0, 0, 0, .06), 0 18px 40px -16px rgba(0, 0, 0, .35);
|
|
32
|
+
--shop-ease: cubic-bezier(.2, .8, .2, 1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.shop-page { padding-top: 1.5rem; padding-bottom: 4rem; color: var(--shop-text); }
|
|
36
|
+
.shop-page h1, .shop-page h2, .shop-page h3 { color: var(--shop-text); }
|
|
37
|
+
.shop-muted { color: var(--shop-muted); }
|
|
38
|
+
.shop-page [hidden], .shop-drawer [hidden] { display: none !important; }
|
|
39
|
+
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
40
|
+
|
|
41
|
+
/* ---- Storefront header and toolbar ------------------------------------------- */
|
|
42
|
+
|
|
43
|
+
.shop-hero { margin: .5rem 0 1.75rem; }
|
|
44
|
+
.shop-hero h1 { font-size: clamp(2rem, 4vw, 3rem); letter-spacing: -.02em; line-height: 1.05; margin: 0 0 .5rem; }
|
|
45
|
+
.shop-intro { max-width: 60ch; font-size: 1.1rem; color: var(--shop-muted); margin: 0; }
|
|
46
|
+
|
|
47
|
+
.shop-toolbar { display: flex; flex-wrap: wrap; gap: .75rem 1rem; align-items: center; justify-content: space-between;
|
|
48
|
+
margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--shop-line); }
|
|
49
|
+
.shop-tools { display: flex; gap: .5rem; align-items: center; margin-left: auto; }
|
|
50
|
+
.shop-chips { display: flex; flex-wrap: wrap; gap: .4rem; }
|
|
51
|
+
.shop-chip { border: 1px solid var(--shop-line); background: transparent; color: var(--shop-text); border-radius: 999px;
|
|
52
|
+
padding: .38rem .9rem; font: inherit; font-size: .9rem; cursor: pointer; transition: all .2s var(--shop-ease); }
|
|
53
|
+
.shop-chip:hover { border-color: var(--shop-accent); color: var(--shop-accent); }
|
|
54
|
+
.shop-chip.is-on { background: var(--shop-text); border-color: var(--shop-text); color: var(--shop-bg); }
|
|
55
|
+
.shop-search { display: flex; align-items: center; gap: .4rem; border: 1px solid var(--shop-line); border-radius: 999px;
|
|
56
|
+
padding: 0 .8rem; color: var(--shop-muted); background: var(--shop-bg); }
|
|
57
|
+
.shop-search:focus-within { border-color: var(--shop-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--shop-accent) 20%, transparent); }
|
|
58
|
+
.shop-search input { border: 0; outline: 0; background: transparent; color: var(--shop-text); font: inherit; padding: .45rem 0; width: 12rem; max-width: 40vw; }
|
|
59
|
+
.shop-sort { background-repeat: no-repeat; border: 1px solid var(--shop-line); border-radius: 999px; padding: .45rem 2rem .45rem .9rem; background-color: var(--shop-bg);
|
|
60
|
+
color: var(--shop-text); font: inherit; font-size: .9rem; cursor: pointer; }
|
|
61
|
+
.shop-empty, .shop-none { text-align: center; padding: 4rem 1rem; color: var(--shop-muted); }
|
|
62
|
+
.shop-linkbtn { background: none; border: 0; padding: 0; color: var(--shop-accent); font: inherit; cursor: pointer; text-decoration: underline; text-underline-offset: 3px; }
|
|
63
|
+
|
|
64
|
+
/* ---- Product cards ------------------------------------------------------------- */
|
|
65
|
+
|
|
66
|
+
.shop-grid { display: grid; grid-template-columns: repeat(var(--shop-cols, 3), minmax(0, 1fr)); gap: 1.75rem 1.25rem; }
|
|
67
|
+
@media (max-width: 960px) { .shop-grid { grid-template-columns: repeat(min(var(--shop-cols, 3), 3), minmax(0, 1fr)); } }
|
|
68
|
+
@media (max-width: 720px) { .shop-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.25rem .85rem; } }
|
|
69
|
+
.shop-embed { margin: 1.5rem 0; }
|
|
70
|
+
.shop-embed.is-single { max-width: 340px; }
|
|
71
|
+
|
|
72
|
+
.shop-card { position: relative; display: flex; flex-direction: column; min-width: 0; animation: shop-rise .5s var(--shop-ease) both; }
|
|
73
|
+
.shop-card:nth-child(2) { animation-delay: .04s; } .shop-card:nth-child(3) { animation-delay: .08s; }
|
|
74
|
+
.shop-card:nth-child(4) { animation-delay: .12s; } .shop-card:nth-child(5) { animation-delay: .16s; }
|
|
75
|
+
.shop-card:nth-child(n+6) { animation-delay: .2s; }
|
|
76
|
+
@keyframes shop-rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
|
|
77
|
+
|
|
78
|
+
.shop-card-media { position: relative; display: block; aspect-ratio: 4 / 5; border-radius: var(--shop-radius); overflow: hidden;
|
|
79
|
+
background: var(--shop-soft); isolation: isolate; }
|
|
80
|
+
.shop-card-media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
|
|
81
|
+
transition: transform .7s var(--shop-ease), opacity .45s ease; }
|
|
82
|
+
.shop-card-media .shop-card-alt { opacity: 0; }
|
|
83
|
+
.shop-card:hover .shop-card-media img { transform: scale(1.045); }
|
|
84
|
+
.shop-card:hover .shop-card-media .shop-card-alt { opacity: 1; }
|
|
85
|
+
.shop-card-noimg { position: absolute; inset: 0; display: grid; place-items: center; color: var(--shop-muted); opacity: .5; }
|
|
86
|
+
.shop-card-noimg svg { width: 42px; height: 42px; }
|
|
87
|
+
.shop-card.is-soldout .shop-card-media img { filter: grayscale(.7); opacity: .65; }
|
|
88
|
+
|
|
89
|
+
.shop-flags { position: absolute; top: .7rem; left: .7rem; display: flex; flex-direction: column; gap: .3rem; z-index: 1; align-items: flex-start; }
|
|
90
|
+
.shop-flag { font-size: .72rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; padding: .28rem .55rem;
|
|
91
|
+
border-radius: 999px; background: var(--shop-bg); color: var(--shop-text); box-shadow: 0 1px 3px rgba(0, 0, 0, .15); }
|
|
92
|
+
.shop-flag.is-sale { background: var(--shop-sale); color: #fff; }
|
|
93
|
+
.shop-flag.is-soldout { background: var(--shop-text); color: var(--shop-bg); }
|
|
94
|
+
.shop-flag.is-low { background: color-mix(in srgb, var(--shop-warn) 18%, var(--shop-bg)); color: var(--shop-text); }
|
|
95
|
+
.shop-flag.is-featured { background: var(--shop-accent); color: var(--shop-accent-ink); }
|
|
96
|
+
|
|
97
|
+
.shop-card-body { padding: .85rem .15rem 0; display: flex; flex-direction: column; gap: .2rem; flex: 1; }
|
|
98
|
+
.shop-card-cat { font-size: .75rem; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: var(--shop-muted); }
|
|
99
|
+
.shop-card-title { font-size: 1.02rem; font-weight: 600; margin: 0; line-height: 1.3; }
|
|
100
|
+
.shop-card-title a { color: inherit; text-decoration: none; }
|
|
101
|
+
.shop-card-title a::after { content: ''; position: absolute; inset: 0; } /* the whole card is the link */
|
|
102
|
+
.shop-card-summary { margin: .1rem 0 0; font-size: .88rem; color: var(--shop-muted); display: -webkit-box; -webkit-line-clamp: 2;
|
|
103
|
+
-webkit-box-orient: vertical; overflow: hidden; }
|
|
104
|
+
.shop-card-foot { display: flex; align-items: center; justify-content: space-between; gap: .5rem; margin-top: auto; padding-top: .55rem; }
|
|
105
|
+
.shop-price { font-weight: 700; font-size: 1.02rem; font-variant-numeric: tabular-nums; }
|
|
106
|
+
.shop-from { font-weight: 500; font-size: .82rem; color: var(--shop-muted); }
|
|
107
|
+
.shop-was { color: var(--shop-muted); font-weight: 500; font-size: .88em; margin-left: .25rem; }
|
|
108
|
+
.shop-card .shop-was + *, .shop-card:has(.shop-was) .shop-now { color: var(--shop-sale); }
|
|
109
|
+
|
|
110
|
+
.shop-quick { position: relative; z-index: 2; display: inline-flex; align-items: center; gap: .3rem; border: 1px solid var(--shop-line);
|
|
111
|
+
background: var(--shop-bg); color: var(--shop-text); border-radius: 999px; padding: .38rem .8rem .38rem .6rem; font: inherit;
|
|
112
|
+
font-size: .85rem; font-weight: 600; cursor: pointer; text-decoration: none; white-space: nowrap;
|
|
113
|
+
transition: background .2s, color .2s, border-color .2s, transform .15s; }
|
|
114
|
+
.shop-quick.is-link { padding: .38rem .85rem; }
|
|
115
|
+
.shop-quick:hover, .shop-quick:focus-visible { background: var(--shop-accent); border-color: var(--shop-accent); color: var(--shop-accent-ink); }
|
|
116
|
+
.shop-quick:active { transform: scale(.95); }
|
|
117
|
+
.shop-quick.is-added { background: var(--shop-good); border-color: var(--shop-good); color: #fff; }
|
|
118
|
+
@media (max-width: 480px) { .shop-quick span { display: none; } .shop-quick { padding: .45rem; } .shop-quick.is-link { padding: .38rem .7rem; } }
|
|
119
|
+
|
|
120
|
+
.shop-buy-button { display: inline-flex; align-items: center; gap: .4rem; border: 0; border-radius: 999px; padding: .75rem 1.4rem;
|
|
121
|
+
background: var(--shop-accent); color: var(--shop-accent-ink) !important; font: inherit; font-weight: 600; cursor: pointer;
|
|
122
|
+
text-decoration: none !important; box-shadow: var(--shop-shadow); transition: transform .15s, box-shadow .2s; }
|
|
123
|
+
.shop-buy-button:hover { transform: translateY(-1px); box-shadow: var(--shop-shadow-lift); }
|
|
124
|
+
|
|
125
|
+
/* ---- A product's page ------------------------------------------------------------- */
|
|
126
|
+
|
|
127
|
+
.shop-crumbs { display: flex; align-items: center; gap: .4rem; font-size: .9rem; margin-bottom: 1.25rem; color: var(--shop-muted); }
|
|
128
|
+
.shop-crumbs a { display: inline-flex; align-items: center; gap: .2rem; color: var(--shop-muted); text-decoration: none; }
|
|
129
|
+
.shop-crumbs a:hover { color: var(--shop-accent); }
|
|
130
|
+
|
|
131
|
+
.shop-product { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: clamp(1.5rem, 4vw, 3.5rem); align-items: start; }
|
|
132
|
+
@media (max-width: 820px) { .shop-product { grid-template-columns: 1fr; } }
|
|
133
|
+
.shop-gallery { position: sticky; top: 5rem; }
|
|
134
|
+
@media (max-width: 820px) { .shop-gallery { position: static; } }
|
|
135
|
+
.shop-gallery-main { position: relative; aspect-ratio: 1; border-radius: var(--shop-radius); overflow: hidden; background: var(--shop-soft); }
|
|
136
|
+
.shop-gallery-main img { width: 100%; height: 100%; object-fit: cover; display: block; transition: opacity .25s; }
|
|
137
|
+
.shop-gallery-main.is-empty { display: grid; place-items: center; color: var(--shop-muted); }
|
|
138
|
+
.shop-gallery-main.is-empty svg { width: 64px; height: 64px; opacity: .4; }
|
|
139
|
+
.shop-thumbs { display: flex; gap: .5rem; margin-top: .75rem; overflow-x: auto; padding-bottom: .25rem; }
|
|
140
|
+
.shop-thumb { flex: 0 0 4.5rem; aspect-ratio: 1; border-radius: var(--shop-radius-sm); overflow: hidden; padding: 0; cursor: pointer;
|
|
141
|
+
border: 2px solid transparent; background: var(--shop-soft); opacity: .7; transition: opacity .2s, border-color .2s; }
|
|
142
|
+
.shop-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
143
|
+
.shop-thumb.is-on, .shop-thumb:hover { opacity: 1; border-color: var(--shop-accent); }
|
|
144
|
+
|
|
145
|
+
.shop-buy { display: flex; flex-direction: column; gap: .6rem; }
|
|
146
|
+
.shop-product-title { font-size: clamp(1.7rem, 3vw, 2.4rem); letter-spacing: -.02em; line-height: 1.1; margin: 0; }
|
|
147
|
+
.shop-product-price { font-size: 1.5rem; font-weight: 700; margin: 0; font-variant-numeric: tabular-nums; }
|
|
148
|
+
.shop-product-price:has(.shop-was) .shop-now { color: var(--shop-sale); }
|
|
149
|
+
.shop-product-summary { font-size: 1.05rem; color: var(--shop-muted); margin: 0; }
|
|
150
|
+
.shop-buy-form { display: flex; flex-direction: column; gap: 1rem; margin: .75rem 0 .25rem; }
|
|
151
|
+
.shop-options { border: 0; padding: 0; margin: 0; }
|
|
152
|
+
.shop-options legend { font-weight: 600; font-size: .9rem; margin-bottom: .5rem; }
|
|
153
|
+
.shop-option-list { display: flex; flex-wrap: wrap; gap: .45rem; }
|
|
154
|
+
.shop-option { position: relative; cursor: pointer; }
|
|
155
|
+
.shop-option input { position: absolute; opacity: 0; pointer-events: none; }
|
|
156
|
+
.shop-option span { display: inline-block; border: 1.5px solid var(--shop-line); border-radius: var(--shop-radius-sm);
|
|
157
|
+
padding: .55rem 1rem; font-weight: 500; transition: all .15s; background: var(--shop-bg); }
|
|
158
|
+
.shop-option span small { color: var(--shop-muted); font-weight: 500; }
|
|
159
|
+
.shop-option:hover span { border-color: var(--shop-text); }
|
|
160
|
+
.shop-option input:checked + span { border-color: var(--shop-accent); background: color-mix(in srgb, var(--shop-accent) 10%, var(--shop-bg));
|
|
161
|
+
box-shadow: 0 0 0 1px var(--shop-accent); }
|
|
162
|
+
.shop-option input:focus-visible + span { outline: 2px solid var(--shop-accent); outline-offset: 2px; }
|
|
163
|
+
.shop-option.is-out span { opacity: .45; text-decoration: line-through; cursor: not-allowed; }
|
|
164
|
+
|
|
165
|
+
.shop-buy-row { display: flex; gap: .6rem; }
|
|
166
|
+
.shop-qty { display: inline-flex; align-items: center; border: 1.5px solid var(--shop-line); border-radius: 999px; overflow: hidden; flex: none; }
|
|
167
|
+
.shop-qty button { width: 2.4rem; height: 2.9rem; border: 0; background: transparent; color: var(--shop-text); font-size: 1.15rem; cursor: pointer; }
|
|
168
|
+
.shop-qty button:hover { background: var(--shop-soft); }
|
|
169
|
+
.shop-qty input { width: 2.6rem; text-align: center; border: 0; background: transparent; color: var(--shop-text); font: inherit;
|
|
170
|
+
font-weight: 600; -moz-appearance: textfield; }
|
|
171
|
+
.shop-qty input::-webkit-outer-spin-button, .shop-qty input::-webkit-inner-spin-button { -webkit-appearance: none; }
|
|
172
|
+
.shop-qty.is-small button { width: 1.9rem; height: 2rem; font-size: 1rem; }
|
|
173
|
+
.shop-qty.is-small input { width: 2rem; font-size: .9rem; }
|
|
174
|
+
.shop-add-btn { flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: .5rem; border: 0; border-radius: 999px;
|
|
175
|
+
background: var(--shop-accent); color: var(--shop-accent-ink); font: inherit; font-weight: 650; font-size: 1.02rem;
|
|
176
|
+
min-height: 2.9rem; padding: 0 1.4rem; cursor: pointer; box-shadow: var(--shop-shadow); transition: transform .15s, box-shadow .2s, background .2s; }
|
|
177
|
+
.shop-add-btn:hover:not(:disabled) { transform: translateY(-1px); box-shadow: var(--shop-shadow-lift); }
|
|
178
|
+
.shop-add-btn:active:not(:disabled) { transform: scale(.98); }
|
|
179
|
+
.shop-add-btn:disabled { background: var(--shop-soft); color: var(--shop-muted); box-shadow: none; cursor: not-allowed; }
|
|
180
|
+
.shop-add-btn.is-added { background: var(--shop-good); }
|
|
181
|
+
.shop-stock { margin: 0; min-height: 1.2em; font-size: .9rem; font-weight: 600; color: var(--shop-warn); }
|
|
182
|
+
.shop-assure { list-style: none; padding: 1rem 0 0; margin: .25rem 0 0; border-top: 1px solid var(--shop-line); display: grid; gap: .5rem;
|
|
183
|
+
font-size: .92rem; color: var(--shop-muted); }
|
|
184
|
+
.shop-assure li { display: flex; gap: .55rem; align-items: center; }
|
|
185
|
+
.shop-description { margin-top: 1rem; padding-top: 1.25rem; border-top: 1px solid var(--shop-line); line-height: 1.7; }
|
|
186
|
+
.shop-description img { max-width: 100%; border-radius: var(--shop-radius-sm); }
|
|
187
|
+
.shop-related { margin-top: 4.5rem; }
|
|
188
|
+
.shop-related h2 { font-size: 1.4rem; margin-bottom: 1.25rem; }
|
|
189
|
+
|
|
190
|
+
/* ---- Basket and checkout ------------------------------------------------------------ */
|
|
191
|
+
|
|
192
|
+
.shop-checkout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, 380px); gap: clamp(1.5rem, 4vw, 3rem); align-items: start; }
|
|
193
|
+
@media (max-width: 880px) { .shop-checkout { grid-template-columns: 1fr; } .shop-co-summary { order: -1; } }
|
|
194
|
+
.shop-co-main h1 { font-size: clamp(1.8rem, 3vw, 2.4rem); letter-spacing: -.02em; margin: 0 0 1.25rem; }
|
|
195
|
+
.shop-co-lines { display: flex; flex-direction: column; margin-bottom: 2rem; }
|
|
196
|
+
.shop-line { display: grid; grid-template-columns: 4.5rem minmax(0, 1fr) auto; gap: .5rem 1rem; align-items: center;
|
|
197
|
+
padding: 1rem 0; border-bottom: 1px solid var(--shop-line); animation: shop-rise .35s var(--shop-ease) both; }
|
|
198
|
+
.shop-line:first-child { border-top: 1px solid var(--shop-line); }
|
|
199
|
+
.shop-line-img { width: 4.5rem; aspect-ratio: 1; border-radius: var(--shop-radius-sm); overflow: hidden; background: var(--shop-soft);
|
|
200
|
+
display: grid; place-items: center; color: var(--shop-muted); }
|
|
201
|
+
.shop-line-img img { width: 100%; height: 100%; object-fit: cover; }
|
|
202
|
+
.shop-line-name { font-weight: 600; color: var(--shop-text); text-decoration: none; }
|
|
203
|
+
.shop-line-name:hover { color: var(--shop-accent); }
|
|
204
|
+
.shop-line-meta { font-size: .85rem; color: var(--shop-muted); }
|
|
205
|
+
.shop-line-controls { display: flex; align-items: center; gap: .6rem; margin-top: .45rem; }
|
|
206
|
+
.shop-line-amt { font-weight: 650; font-variant-numeric: tabular-nums; text-align: right; }
|
|
207
|
+
.shop-remove { border: 0; background: none; color: var(--shop-muted); font: inherit; font-size: .83rem; cursor: pointer; padding: .2rem; }
|
|
208
|
+
.shop-remove:hover { color: var(--shop-sale); }
|
|
209
|
+
.shop-line.is-leaving { opacity: 0; transform: translateX(12px); transition: all .25s ease; }
|
|
210
|
+
|
|
211
|
+
.shop-co-empty { text-align: center; padding: 3rem 1rem; border: 1px dashed var(--shop-line); border-radius: var(--shop-radius); }
|
|
212
|
+
.shop-co-empty .shop-bigicon { display: inline-grid; place-items: center; width: 4rem; height: 4rem; border-radius: 50%;
|
|
213
|
+
background: var(--shop-soft); color: var(--shop-muted); margin-bottom: .75rem; }
|
|
214
|
+
.shop-co-empty .shop-bigicon svg { width: 28px; height: 28px; }
|
|
215
|
+
|
|
216
|
+
.shop-co-form { display: flex; flex-direction: column; gap: 2rem; }
|
|
217
|
+
.shop-co-step h2 { display: flex; align-items: center; gap: .6rem; font-size: 1.15rem; margin: 0 0 1rem; }
|
|
218
|
+
.shop-step { display: inline-grid; place-items: center; width: 1.7rem; height: 1.7rem; border-radius: 50%; background: var(--shop-text);
|
|
219
|
+
color: var(--shop-bg); font-size: .85rem; font-weight: 700; }
|
|
220
|
+
.shop-fields { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; }
|
|
221
|
+
@media (max-width: 560px) { .shop-fields { grid-template-columns: 1fr; } }
|
|
222
|
+
.shop-field { display: flex; flex-direction: column; gap: .3rem; font-size: .88rem; font-weight: 600; }
|
|
223
|
+
.shop-field.is-wide { grid-column: 1 / -1; }
|
|
224
|
+
.shop-field em { font-style: normal; font-weight: 400; color: var(--shop-muted); }
|
|
225
|
+
.shop-field input, .shop-field select, .shop-field textarea, .shop-code input {
|
|
226
|
+
border: 1.5px solid var(--shop-line); border-radius: var(--shop-radius-sm); padding: .7rem .85rem; font: inherit; font-weight: 400;
|
|
227
|
+
background-color: var(--shop-bg); background-repeat: no-repeat; color: var(--shop-text); transition: border-color .15s, box-shadow .15s; width: 100%; box-sizing: border-box; }
|
|
228
|
+
.shop-field input:focus, .shop-field select:focus, .shop-field textarea:focus, .shop-code input:focus {
|
|
229
|
+
outline: 0; border-color: var(--shop-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--shop-accent) 18%, transparent); }
|
|
230
|
+
.shop-field.is-bad input, .shop-field.is-bad select { border-color: var(--shop-sale); }
|
|
231
|
+
.shop-field-err { color: var(--shop-sale); font-weight: 500; font-size: .8rem; }
|
|
232
|
+
|
|
233
|
+
.shop-choice-row, .shop-methods { display: grid; gap: .6rem; margin-bottom: 1rem; }
|
|
234
|
+
.shop-choice-row { grid-template-columns: 1fr 1fr; }
|
|
235
|
+
@media (max-width: 560px) { .shop-choice-row { grid-template-columns: 1fr; } }
|
|
236
|
+
.shop-choice { position: relative; cursor: pointer; }
|
|
237
|
+
.shop-choice input { position: absolute; opacity: 0; }
|
|
238
|
+
.shop-choice > span { display: flex; align-items: center; gap: .65rem; border: 1.5px solid var(--shop-line); border-radius: var(--shop-radius-sm);
|
|
239
|
+
padding: .9rem 1rem; font-weight: 600; transition: all .15s; background: var(--shop-bg); }
|
|
240
|
+
.shop-choice > span small { display: block; font-weight: 400; color: var(--shop-muted); }
|
|
241
|
+
.shop-choice > span .shop-choice-end { margin-left: auto; color: var(--shop-muted); display: flex; gap: .3rem; }
|
|
242
|
+
.shop-choice:hover > span { border-color: var(--shop-text); }
|
|
243
|
+
.shop-choice input:checked + span { border-color: var(--shop-accent); box-shadow: 0 0 0 1px var(--shop-accent);
|
|
244
|
+
background: color-mix(in srgb, var(--shop-accent) 7%, var(--shop-bg)); }
|
|
245
|
+
.shop-choice input:focus-visible + span { outline: 2px solid var(--shop-accent); outline-offset: 2px; }
|
|
246
|
+
.shop-radio { width: 1.1rem; height: 1.1rem; border-radius: 50%; border: 2px solid var(--shop-line); flex: none; transition: all .15s; }
|
|
247
|
+
.shop-choice input:checked + span .shop-radio { border: 5px solid var(--shop-accent); }
|
|
248
|
+
.shop-paymark { font-size: .7rem; font-weight: 800; letter-spacing: .02em; border: 1px solid var(--shop-line); border-radius: 4px; padding: .1rem .3rem; color: var(--shop-muted); }
|
|
249
|
+
|
|
250
|
+
.shop-terms { display: flex; gap: .5rem; align-items: flex-start; font-size: .9rem; margin: .5rem 0 0; }
|
|
251
|
+
.shop-co-error { background: color-mix(in srgb, var(--shop-sale) 10%, var(--shop-bg)); color: var(--shop-text);
|
|
252
|
+
border: 1px solid color-mix(in srgb, var(--shop-sale) 40%, transparent); border-radius: var(--shop-radius-sm); padding: .75rem 1rem; font-size: .92rem; margin: 1rem 0 0; }
|
|
253
|
+
.shop-pay-btn { width: 100%; margin-top: 1rem; display: inline-flex; align-items: center; justify-content: center; gap: .55rem; border: 0;
|
|
254
|
+
border-radius: 999px; background: var(--shop-accent); color: var(--shop-accent-ink); font: inherit; font-weight: 700; font-size: 1.08rem;
|
|
255
|
+
min-height: 3.3rem; cursor: pointer; box-shadow: var(--shop-shadow); transition: transform .15s, box-shadow .2s, opacity .2s; }
|
|
256
|
+
.shop-pay-btn:hover:not(:disabled) { transform: translateY(-1px); box-shadow: var(--shop-shadow-lift); }
|
|
257
|
+
.shop-pay-btn:disabled { opacity: .6; cursor: progress; }
|
|
258
|
+
.shop-secure { display: flex; align-items: center; justify-content: center; gap: .35rem; font-size: .8rem; color: var(--shop-muted); margin: .75rem 0 0; text-align: center; }
|
|
259
|
+
|
|
260
|
+
.shop-co-summary { position: sticky; top: 5rem; border-radius: var(--shop-radius); background: var(--shop-softer);
|
|
261
|
+
border: 1px solid var(--shop-line); padding: 1.35rem 1.35rem 1.1rem; }
|
|
262
|
+
@media (max-width: 880px) { .shop-co-summary { position: static; } }
|
|
263
|
+
.shop-co-summary h2 { font-size: 1.1rem; margin: 0 0 1rem; }
|
|
264
|
+
.shop-totals { margin: 0; display: flex; flex-direction: column; gap: .55rem; font-variant-numeric: tabular-nums; }
|
|
265
|
+
.shop-totals > div { display: flex; justify-content: space-between; gap: 1rem; font-size: .95rem; }
|
|
266
|
+
.shop-totals dt { color: var(--shop-muted); } .shop-totals dd { margin: 0; font-weight: 600; }
|
|
267
|
+
.shop-totals .is-discount dd { color: var(--shop-good); }
|
|
268
|
+
.shop-totals .is-included { font-size: .82rem; } .shop-totals .is-included dd { font-weight: 400; color: var(--shop-muted); }
|
|
269
|
+
.shop-totals .is-total { border-top: 1px solid var(--shop-line); margin-top: .35rem; padding-top: .8rem; font-size: 1.2rem; }
|
|
270
|
+
.shop-totals .is-total dt { color: var(--shop-text); font-weight: 700; } .shop-totals .is-total dd { font-weight: 800; }
|
|
271
|
+
.shop-code { display: flex; gap: .4rem; margin-top: 1rem; }
|
|
272
|
+
.shop-code button { border: 1.5px solid var(--shop-text); border-radius: var(--shop-radius-sm); background: transparent; color: var(--shop-text);
|
|
273
|
+
font: inherit; font-weight: 600; padding: 0 1rem; cursor: pointer; }
|
|
274
|
+
.shop-code button:hover { background: var(--shop-text); color: var(--shop-bg); }
|
|
275
|
+
.shop-notes { list-style: none; padding: 0 !important; margin: .9rem 0 0; font-size: .85rem; display: grid; gap: .35rem; }
|
|
276
|
+
.shop-notes li { padding: .5rem .7rem; border-radius: var(--shop-radius-sm); background: color-mix(in srgb, var(--shop-warn) 12%, transparent); }
|
|
277
|
+
.shop-notes li.is-good { background: color-mix(in srgb, var(--shop-good) 12%, transparent); }
|
|
278
|
+
|
|
279
|
+
/* The road to free delivery. */
|
|
280
|
+
.shop-freebar { margin: 0 0 1rem; font-size: .88rem; }
|
|
281
|
+
.shop-freebar p { margin: 0 0 .45rem; }
|
|
282
|
+
.shop-freebar-track { height: 6px; border-radius: 999px; background: var(--shop-soft); overflow: hidden; }
|
|
283
|
+
.shop-freebar-fill { height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--shop-accent), var(--shop-good));
|
|
284
|
+
transition: width .6s var(--shop-ease); }
|
|
285
|
+
|
|
286
|
+
/* ---- Receipt ------------------------------------------------------------------------ */
|
|
287
|
+
|
|
288
|
+
.shop-receipt { max-width: 900px; margin: 0 auto; }
|
|
289
|
+
.shop-r-head { display: flex; gap: 1.25rem; align-items: flex-start; margin: 1rem 0 1.5rem; }
|
|
290
|
+
.shop-r-head h1 { font-size: clamp(1.7rem, 3vw, 2.3rem); letter-spacing: -.02em; margin: .1rem 0 .35rem; }
|
|
291
|
+
.shop-r-head p { margin: 0; }
|
|
292
|
+
.shop-r-badge { flex: none; width: 3.6rem; height: 3.6rem; border-radius: 50%; display: grid; place-items: center; color: #fff;
|
|
293
|
+
background: var(--shop-good); box-shadow: 0 0 0 8px color-mix(in srgb, var(--shop-good) 15%, transparent); animation: shop-pop .6s var(--shop-ease) both; }
|
|
294
|
+
.shop-r-badge svg { width: 1.7rem; height: 1.7rem; stroke-dasharray: 30; stroke-dashoffset: 30; animation: shop-draw .5s .3s ease forwards; }
|
|
295
|
+
.shop-r-badge.is-waiting { background: var(--shop-soft); box-shadow: none; }
|
|
296
|
+
.shop-receipt.is-closed .shop-r-badge { background: var(--shop-muted); box-shadow: none; }
|
|
297
|
+
@keyframes shop-pop { from { transform: scale(.4); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
298
|
+
@keyframes shop-draw { to { stroke-dashoffset: 0; } }
|
|
299
|
+
.shop-spinner { width: 1.5rem; height: 1.5rem; border-radius: 50%; border: 3px solid var(--shop-line); border-top-color: var(--shop-accent); animation: shop-spin .8s linear infinite; }
|
|
300
|
+
@keyframes shop-spin { to { transform: rotate(360deg); } }
|
|
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
|
+
.shop-r-note p { margin: .35rem 0 0; }
|
|
303
|
+
.shop-r-steps { list-style: none; padding: 0; margin: 0 0 2rem; display: grid; grid-template-columns: repeat(3, 1fr); counter-reset: s; }
|
|
304
|
+
.shop-r-steps li { position: relative; padding-top: 1.6rem; font-size: .88rem; color: var(--shop-muted); text-align: center; }
|
|
305
|
+
.shop-r-steps li span { position: absolute; top: .3rem; left: 50%; width: .9rem; height: .9rem; margin-left: -.45rem; border-radius: 50%;
|
|
306
|
+
background: var(--shop-bg); border: 2px solid var(--shop-line); z-index: 1; }
|
|
307
|
+
.shop-r-steps li::before { content: ''; position: absolute; top: .7rem; left: -50%; right: 50%; height: 2px; background: var(--shop-line); }
|
|
308
|
+
.shop-r-steps li:first-child::before { display: none; }
|
|
309
|
+
.shop-r-steps li.is-done { color: var(--shop-text); font-weight: 600; }
|
|
310
|
+
.shop-r-steps li.is-done span { background: var(--shop-good); border-color: var(--shop-good); }
|
|
311
|
+
.shop-r-steps li.is-done::before { background: var(--shop-good); }
|
|
312
|
+
.shop-r-grid { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); gap: 2rem; }
|
|
313
|
+
@media (max-width: 720px) { .shop-r-grid { grid-template-columns: 1fr; } }
|
|
314
|
+
.shop-r-grid h2 { font-size: .8rem; text-transform: uppercase; letter-spacing: .08em; color: var(--shop-muted); margin: 0 0 .75rem; }
|
|
315
|
+
.shop-r-side h2:not(:first-child) { margin-top: 1.25rem; }
|
|
316
|
+
.shop-r-side address { font-style: normal; line-height: 1.6; }
|
|
317
|
+
.shop-r-lines { list-style: none; padding: 0; margin: 0 0 1rem; }
|
|
318
|
+
.shop-r-line { display: grid; grid-template-columns: 3rem minmax(0, 1fr) auto auto; gap: .9rem; align-items: center; padding: .6rem 0; border-bottom: 1px solid var(--shop-line); }
|
|
319
|
+
.shop-r-line img, .shop-r-noimg { width: 3rem; height: 3rem; border-radius: 8px; object-fit: cover; background: var(--shop-soft); display: grid; place-items: center; color: var(--shop-muted); }
|
|
320
|
+
.shop-r-name small { display: block; color: var(--shop-muted); font-size: .82rem; }
|
|
321
|
+
.shop-r-qty { color: var(--shop-muted); font-size: .9rem; } .shop-r-amt { font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
322
|
+
@media print {
|
|
323
|
+
.shop-crumbs, .shop-r-side p:last-child, .shop-fab, .shop-drawer { display: none !important; }
|
|
324
|
+
.shop-r-badge { box-shadow: none; animation: none; } .shop-r-badge svg { stroke-dashoffset: 0; animation: none; }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* ---- The basket button and drawer (every page) ------------------------------------------ */
|
|
328
|
+
|
|
329
|
+
.shop-fab { position: fixed; bottom: 1.25rem; right: 1.25rem; z-index: 1040; display: inline-flex; align-items: center; gap: .55rem;
|
|
330
|
+
border: 0; border-radius: 999px; padding: .85rem 1.15rem .85rem 1rem; background: var(--shop-text); color: var(--shop-bg);
|
|
331
|
+
font: inherit; font-weight: 650; cursor: pointer; box-shadow: 0 10px 30px -8px rgba(0, 0, 0, .45);
|
|
332
|
+
transform: translateY(0); transition: transform .35s var(--shop-ease), opacity .3s; }
|
|
333
|
+
.shop-fab.is-left { right: auto; left: 1.25rem; }
|
|
334
|
+
.shop-fab.is-hidden { transform: translateY(160%); opacity: 0; pointer-events: none; }
|
|
335
|
+
.shop-fab:hover { transform: translateY(-2px); }
|
|
336
|
+
.shop-fab-count { min-width: 1.45rem; height: 1.45rem; border-radius: 999px; background: var(--shop-accent); color: var(--shop-accent-ink);
|
|
337
|
+
display: inline-grid; place-items: center; font-size: .78rem; font-weight: 800; padding: 0 .35rem; }
|
|
338
|
+
.shop-fab.is-bump .shop-fab-count { animation: shop-bump .5s var(--shop-ease); }
|
|
339
|
+
@keyframes shop-bump { 0% { transform: scale(1); } 35% { transform: scale(1.45); } 100% { transform: scale(1); } }
|
|
340
|
+
@media (max-width: 560px) { .shop-fab-label { display: none; } .shop-fab { padding: .9rem; } }
|
|
341
|
+
|
|
342
|
+
.shop-drawer { position: fixed; inset: 0; z-index: 1050; visibility: hidden; }
|
|
343
|
+
.shop-drawer.is-open { visibility: visible; }
|
|
344
|
+
.shop-drawer-backdrop { position: absolute; inset: 0; background: rgba(10, 12, 16, .45); opacity: 0; transition: opacity .3s;
|
|
345
|
+
backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); }
|
|
346
|
+
.shop-drawer.is-open .shop-drawer-backdrop { opacity: 1; }
|
|
347
|
+
.shop-drawer-panel { position: absolute; top: 0; bottom: 0; right: 0; width: min(430px, 100vw); background: var(--shop-bg); color: var(--shop-text);
|
|
348
|
+
display: flex; flex-direction: column; transform: translateX(100%); transition: transform .38s var(--shop-ease);
|
|
349
|
+
box-shadow: -20px 0 50px -20px rgba(0, 0, 0, .4); }
|
|
350
|
+
.shop-drawer.is-left .shop-drawer-panel { right: auto; left: 0; transform: translateX(-100%); }
|
|
351
|
+
.shop-drawer.is-open .shop-drawer-panel { transform: none; }
|
|
352
|
+
.shop-drawer-head { display: flex; align-items: center; justify-content: space-between; padding: 1.15rem 1.25rem; border-bottom: 1px solid var(--shop-line); }
|
|
353
|
+
.shop-drawer-head h2 { font-size: 1.15rem; margin: 0; display: flex; align-items: center; gap: .5rem; color: var(--shop-text); }
|
|
354
|
+
.shop-drawer-head h2 small { color: var(--shop-muted); font-weight: 500; font-size: .9rem; }
|
|
355
|
+
.shop-x { border: 0; background: var(--shop-soft); color: var(--shop-text); width: 2.2rem; height: 2.2rem; border-radius: 50%;
|
|
356
|
+
cursor: pointer; font-size: 1.3rem; line-height: 1; display: grid; place-items: center; }
|
|
357
|
+
.shop-x:hover { background: var(--shop-line); }
|
|
358
|
+
.shop-drawer-body { flex: 1; overflow-y: auto; padding: 1rem 1.25rem; overscroll-behavior: contain; }
|
|
359
|
+
.shop-drawer-body .shop-line { grid-template-columns: 4rem minmax(0, 1fr) auto; padding: .85rem 0; }
|
|
360
|
+
.shop-drawer-body .shop-line-img { width: 4rem; }
|
|
361
|
+
.shop-drawer-body .shop-line:first-child { border-top: 0; }
|
|
362
|
+
.shop-drawer-foot { border-top: 1px solid var(--shop-line); padding: 1rem 1.25rem 1.25rem; background: var(--shop-softer); }
|
|
363
|
+
.shop-drawer-sub { display: flex; justify-content: space-between; font-weight: 700; font-size: 1.1rem; margin-bottom: .2rem; font-variant-numeric: tabular-nums; }
|
|
364
|
+
.shop-drawer-hint { font-size: .8rem; color: var(--shop-muted); margin: 0 0 .9rem; }
|
|
365
|
+
.shop-drawer-go { display: flex; width: 100%; align-items: center; justify-content: center; gap: .5rem; border: 0; border-radius: 999px;
|
|
366
|
+
background: var(--shop-accent); color: var(--shop-accent-ink) !important; font: inherit; font-weight: 700; min-height: 3rem;
|
|
367
|
+
text-decoration: none !important; cursor: pointer; box-shadow: var(--shop-shadow); }
|
|
368
|
+
.shop-drawer-go:hover { filter: brightness(1.05); }
|
|
369
|
+
.shop-drawer-more { display: block; width: 100%; margin-top: .5rem; text-align: center; background: none; border: 0; color: var(--shop-muted);
|
|
370
|
+
font: inherit; font-size: .9rem; cursor: pointer; padding: .4rem; }
|
|
371
|
+
.shop-drawer-more:hover { color: var(--shop-text); }
|
|
372
|
+
.shop-drawer-empty { text-align: center; padding: 3rem 1rem; color: var(--shop-muted); }
|
|
373
|
+
.shop-drawer-empty .shop-bigicon { display: inline-grid; place-items: center; width: 4rem; height: 4rem; border-radius: 50%; background: var(--shop-soft); margin-bottom: .75rem; }
|
|
374
|
+
.shop-drawer-empty .shop-bigicon svg { width: 28px; height: 28px; }
|
|
375
|
+
html.shop-locked { overflow: hidden; }
|
|
376
|
+
|
|
377
|
+
/* "Added" - a small card that rises and goes. */
|
|
378
|
+
.shop-toast { position: fixed; z-index: 1060; bottom: 5.5rem; right: 1.25rem; display: flex; align-items: center; gap: .75rem;
|
|
379
|
+
background: var(--shop-bg); color: var(--shop-text); border: 1px solid var(--shop-line); border-radius: var(--shop-radius);
|
|
380
|
+
padding: .65rem .8rem .65rem .65rem; box-shadow: var(--shop-shadow-lift); max-width: min(360px, calc(100vw - 2.5rem));
|
|
381
|
+
animation: shop-toast-in .4s var(--shop-ease) both; }
|
|
382
|
+
.shop-toast.is-left { right: auto; left: 1.25rem; }
|
|
383
|
+
.shop-toast.is-out { animation: shop-toast-out .3s ease forwards; }
|
|
384
|
+
.shop-toast img, .shop-toast .shop-toast-img { width: 3rem; height: 3rem; border-radius: 8px; object-fit: cover; flex: none; background: var(--shop-soft);
|
|
385
|
+
display: grid; place-items: center; color: var(--shop-muted); }
|
|
386
|
+
.shop-toast strong { display: block; font-size: .92rem; }
|
|
387
|
+
.shop-toast small { color: var(--shop-muted); font-size: .8rem; }
|
|
388
|
+
.shop-toast .shop-toast-check { color: var(--shop-good); display: inline-flex; vertical-align: -3px; margin-right: .2rem; }
|
|
389
|
+
.shop-toast a { margin-left: auto; font-weight: 700; font-size: .85rem; color: var(--shop-accent); white-space: nowrap; text-decoration: none; }
|
|
390
|
+
@keyframes shop-toast-in { from { opacity: 0; transform: translateY(14px) scale(.97); } to { opacity: 1; transform: none; } }
|
|
391
|
+
@keyframes shop-toast-out { to { opacity: 0; transform: translateY(8px); } }
|
|
392
|
+
|
|
393
|
+
@media (prefers-reduced-motion: reduce) {
|
|
394
|
+
.shop-card, .shop-line, .shop-r-badge, .shop-toast { animation: none !important; }
|
|
395
|
+
.shop-card-media img, .shop-drawer-panel, .shop-fab, .shop-freebar-fill { transition: none !important; }
|
|
396
|
+
.shop-r-badge svg { animation: none; stroke-dashoffset: 0; }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/* The site's own form and link styles reach in here; these inputs sit inside a drawn control. */
|
|
400
|
+
.shop-page .shop-qty input, .shop-drawer .shop-qty input, .shop-search input {
|
|
401
|
+
background: transparent !important; border: 0 !important; box-shadow: none !important; outline: 0; padding-left: 0; padding-right: 0; margin: 0; min-height: 0; }
|
|
402
|
+
.shop-card-title a, .shop-card-title a:hover, .shop-line-name, .shop-crumbs a, .shop-quick { text-decoration: none !important; }
|
|
403
|
+
.shop-card-title a:hover { color: var(--shop-accent); }
|
|
404
|
+
|
|
405
|
+
@media (max-width: 640px) {
|
|
406
|
+
.shop-toolbar { flex-direction: column; align-items: stretch; }
|
|
407
|
+
.shop-tools { margin-left: 0; width: 100%; }
|
|
408
|
+
.shop-search { flex: 1; min-width: 0; }
|
|
409
|
+
.shop-search input { width: 100%; max-width: none; }
|
|
410
|
+
.shop-chips { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; margin: 0 -1rem; padding: 0 1rem .15rem; }
|
|
411
|
+
.shop-chips::-webkit-scrollbar { display: none; }
|
|
412
|
+
.shop-chip { flex: none; }
|
|
413
|
+
.shop-line-controls { flex-wrap: wrap; }
|
|
414
|
+
}
|
|
415
|
+
.shop-co-title { font-size: clamp(1.8rem, 3vw, 2.4rem); letter-spacing: -.02em; margin: 0 0 1.25rem; }
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Cart's API against the real collection service, inside the domma-cms
|
|
3
|
+
* checkout `make test` runs in. Its two collections are made for the test and
|
|
4
|
+
* removed after; config/plugins.json is restored byte for byte.
|
|
5
|
+
*/
|
|
6
|
+
import {test, before, after} from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import Fastify from 'fastify';
|
|
11
|
+
import shoppingCart from '../plugin.js';
|
|
12
|
+
|
|
13
|
+
const PLUGINS = path.resolve('config/plugins.json');
|
|
14
|
+
const DIRS = ['shop-products', 'shop-orders'].map(c => path.resolve('content/collections', c));
|
|
15
|
+
let savedPlugins;
|
|
16
|
+
let app;
|
|
17
|
+
const notices = [];
|
|
18
|
+
|
|
19
|
+
before(async () => {
|
|
20
|
+
savedPlugins = await fs.readFile(PLUGINS, 'utf8').catch(() => null);
|
|
21
|
+
for (const d of DIRS) await fs.rm(d, {recursive: true, force: true});
|
|
22
|
+
app = Fastify();
|
|
23
|
+
const authenticate = async (req) => { req.user = {id: 'u1', name: 'Sam', role: 'admin'}; };
|
|
24
|
+
await app.register(shoppingCart, {
|
|
25
|
+
auth: {authenticate, requirePermission: () => async () => {}},
|
|
26
|
+
settings: {},
|
|
27
|
+
hooks: {notify: (n) => notices.push(n), registerShortcode() {}}
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
after(async () => {
|
|
32
|
+
await app?.close();
|
|
33
|
+
for (const d of DIRS) await fs.rm(d, {recursive: true, force: true});
|
|
34
|
+
if (savedPlugins === null) await fs.rm(PLUGINS, {force: true});
|
|
35
|
+
else await fs.writeFile(PLUGINS, savedPlugins);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const call = async (method, url, body) => {
|
|
39
|
+
const res = await app.inject({method, url, payload: body});
|
|
40
|
+
let json = null;
|
|
41
|
+
try { json = res.json(); } catch { /* not JSON */ }
|
|
42
|
+
return {status: res.statusCode, body: json};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
test('a sale, from product to refund', async () => {
|
|
46
|
+
// Settings: flat delivery, VAT inclusive, offline payment on.
|
|
47
|
+
let r = await call('PUT', '/settings', {shippingFlat: 300, taxRate: 20, pricesIncludeTax: true, offlineEnabled: true, countries: ['GB']});
|
|
48
|
+
assert.equal(r.status, 200, JSON.stringify(r.body));
|
|
49
|
+
|
|
50
|
+
r = await call('POST', '/products', {name: 'Mug', price: 1200, status: 'active', trackStock: true, stock: 2});
|
|
51
|
+
assert.equal(r.status, 201, JSON.stringify(r.body));
|
|
52
|
+
const mug = r.body;
|
|
53
|
+
r = await call('POST', '/products', {name: 'Mug', price: 900, status: 'draft'});
|
|
54
|
+
assert.equal(r.body.data.slug, 'mug-2', 'slugs stay unique');
|
|
55
|
+
|
|
56
|
+
r = await call('GET', '/catalogue');
|
|
57
|
+
assert.deepEqual(r.body.products.map(p => p.name), ['Mug'], 'drafts are not for sale');
|
|
58
|
+
|
|
59
|
+
r = await call('POST', '/quote', {lines: [{productId: mug.id, qty: 5}]});
|
|
60
|
+
assert.equal(r.body.lines[0].qty, 2);
|
|
61
|
+
assert.equal(r.body.total, 2700);
|
|
62
|
+
assert.ok(r.body.methods.some(m => m.id === 'offline'));
|
|
63
|
+
|
|
64
|
+
// Checkout refuses a basket that changed until the shopper has seen it.
|
|
65
|
+
r = await call('POST', '/checkout', {lines: [{productId: mug.id, qty: 5}], provider: 'offline',
|
|
66
|
+
customer: {name: 'Ann', email: 'ann@example.com'}, address: {line1: '1 High St', city: 'York', postcode: 'YO1 1AA', country: 'GB'}});
|
|
67
|
+
assert.equal(r.status, 409);
|
|
68
|
+
|
|
69
|
+
r = await call('POST', '/checkout', {lines: [{productId: mug.id, qty: 2}], provider: 'offline', expectTotal: 2700,
|
|
70
|
+
customer: {name: 'Ann', email: 'ann@example.com'}, address: {line1: '1 High St', city: 'York', postcode: 'YO1 1AA', country: 'GB'}});
|
|
71
|
+
assert.equal(r.status, 200, JSON.stringify(r.body));
|
|
72
|
+
const orderId = r.body.orderId;
|
|
73
|
+
assert.match(r.body.redirect, /\/shop\/order\/.+\?t=[a-f0-9]{36}$/);
|
|
74
|
+
|
|
75
|
+
let o = (await call('GET', `/orders/${orderId}`)).body;
|
|
76
|
+
assert.equal(o.data.status, 'awaiting');
|
|
77
|
+
assert.equal(o.data.number, 1001);
|
|
78
|
+
|
|
79
|
+
// The receipt needs the token.
|
|
80
|
+
assert.equal((await call('GET', `/order/${orderId}?t=nope`)).status, 404);
|
|
81
|
+
const token = o.data.token;
|
|
82
|
+
assert.equal((await call('GET', `/order/${orderId}?t=${token}`)).body.status, 'awaiting');
|
|
83
|
+
|
|
84
|
+
// Money arrives: paid, stock taken, the owner told, the badge counts it.
|
|
85
|
+
r = await call('PUT', `/orders/${orderId}/status`, {status: 'paid', note: 'Bank transfer seen'});
|
|
86
|
+
assert.equal(r.status, 200, JSON.stringify(r.body));
|
|
87
|
+
assert.equal((await call('GET', '/products')).body.find(p => p.id === mug.id).data.stock, 0);
|
|
88
|
+
assert.ok(notices.some(n => n.title.includes('#1001')));
|
|
89
|
+
assert.equal((await call('GET', '/count')).body.count, 1);
|
|
90
|
+
|
|
91
|
+
// Sold out now.
|
|
92
|
+
r = await call('POST', '/quote', {lines: [{productId: mug.id, qty: 1}]});
|
|
93
|
+
assert.equal(r.body.lines.length, 0);
|
|
94
|
+
|
|
95
|
+
// A move the table does not allow.
|
|
96
|
+
assert.equal((await call('PUT', `/orders/${orderId}/status`, {status: 'awaiting'})).status, 409);
|
|
97
|
+
|
|
98
|
+
r = await call('PUT', `/orders/${orderId}/status`, {status: 'fulfilled', tracking: {carrier: 'Royal Mail', number: 'AB123', url: 'javascript:x'}});
|
|
99
|
+
assert.equal(r.body.data.tracking.url, '', 'only https tracking links');
|
|
100
|
+
assert.equal((await call('GET', '/count')).body.count, 0);
|
|
101
|
+
|
|
102
|
+
// A paid order cannot be deleted; it can be refunded (offline: marked), with the stock back.
|
|
103
|
+
assert.equal((await call('DELETE', `/orders/${orderId}`)).status, 409);
|
|
104
|
+
r = await call('POST', `/orders/${orderId}/refund`, {restock: true});
|
|
105
|
+
assert.equal(r.status, 200, JSON.stringify(r.body));
|
|
106
|
+
assert.equal(r.body.order.data.status, 'refunded');
|
|
107
|
+
assert.equal((await call('GET', '/products')).body.find(p => p.id === mug.id).data.stock, 2);
|
|
108
|
+
|
|
109
|
+
const sum = (await call('GET', '/summary')).body;
|
|
110
|
+
assert.equal(sum.days30.orders, 0, 'a refunded sale is not revenue');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('free orders are paid at once; a provider that is off is refused', async () => {
|
|
114
|
+
let r = await call('POST', '/products', {name: 'Free guide', price: 0, status: 'active', shippable: false});
|
|
115
|
+
const guide = r.body;
|
|
116
|
+
r = await call('POST', '/checkout', {lines: [{productId: guide.id, qty: 1}], customer: {name: 'Bo', email: 'bo@example.com'}});
|
|
117
|
+
assert.equal(r.status, 200, JSON.stringify(r.body));
|
|
118
|
+
assert.equal((await call('GET', `/orders/${r.body.orderId}`)).body.data.status, 'paid');
|
|
119
|
+
|
|
120
|
+
r = await call('POST', '/products', {name: 'Print', price: 500, status: 'active', shippable: false});
|
|
121
|
+
r = await call('POST', '/checkout', {lines: [{productId: r.body.id, qty: 1}], provider: 'stripe', customer: {name: 'Bo', email: 'bo@example.com'}});
|
|
122
|
+
assert.equal(r.status, 400);
|
|
123
|
+
assert.match(r.body.error, /Choose how to pay/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('settings never send a secret back', async () => {
|
|
127
|
+
let r = await call('PUT', '/settings', {stripeSecretKey: 'sk_test_1234567890abcdef', stripeEnabled: true});
|
|
128
|
+
assert.equal(r.status, 200, JSON.stringify(r.body));
|
|
129
|
+
r = await call('GET', '/settings');
|
|
130
|
+
assert.equal(r.body.settings.stripeSecretKey, '••••••••');
|
|
131
|
+
assert.match(r.body.webhookUrl, /\/api\/plugins\/shopping-cart\/stripe\/webhook$/);
|
|
132
|
+
r = await call('PUT', '/settings', {stripeSecretKey: '••••••••', shopTitle: 'Emporium'});
|
|
133
|
+
assert.equal(r.status, 200);
|
|
134
|
+
const stored = JSON.parse(await fs.readFile(PLUGINS, 'utf8'))['shopping-cart'].settings;
|
|
135
|
+
assert.equal(stored.stripeSecretKey, 'sk_test_1234567890abcdef');
|
|
136
|
+
assert.equal(stored.shopTitle, 'Emporium');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('the Stripe webhook refuses an unsigned body', async () => {
|
|
140
|
+
await call('PUT', '/settings', {stripeWebhookSecret: 'whsec_abcdefghijklmnop'});
|
|
141
|
+
const res = await app.inject({method: 'POST', url: '/stripe/webhook', payload: '{"type":"checkout.session.completed"}',
|
|
142
|
+
headers: {'content-type': 'application/json', 'stripe-signature': 't=1,v1=00'}});
|
|
143
|
+
assert.equal(res.statusCode, 400);
|
|
144
|
+
});
|