feat(widget): restyle storefront scheduler; booking stub with hold countdown

- segmented method control with line icons
- round calendar days with availability dots and scarcity badges
- pill time chips; confirmation stub shows method, place, date, time, fee
- colours derive from theme text colour + one merchant accent (light/dark safe)
- rename leftover ss- internals to dw-

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
metatroncubeswdev 2026-10-13 21:17:09 -04:00
parent e56182e0e3
commit 123ca71d42
4 changed files with 255 additions and 125 deletions

View File

@ -1,65 +1,170 @@
/* Datewise widget — inherits the theme's fonts and text colour; one accent variable. */ /* Datewise widget.
* Inherits the theme's font and text colour; the merchant sets one accent (--dw-accent).
* Every other colour is mixed from currentColor or the accent, so it works on light and dark themes.
* Radius hierarchy: panel 14px · controls 10px · days round · time chips pill. */
.datewise { .datewise {
--ss-accent: #0d6b5d; --dw-accent: #0d6b5d;
--ss-line: rgba(127, 127, 127, 0.32); --dw-on-accent: #fff;
--ss-soft: color-mix(in srgb, var(--ss-accent) 12%, transparent); --dw-line: color-mix(in srgb, currentColor 16%, transparent);
--ss-warn: #9a5b00; --dw-line-strong: color-mix(in srgb, currentColor 34%, transparent);
--ss-warn-soft: rgba(240, 180, 74, 0.18); --dw-wash: color-mix(in srgb, currentColor 4%, transparent);
--ss-bad: #b42318; --dw-tint: color-mix(in srgb, var(--dw-accent) 10%, transparent);
--ss-good: #1e7a3a; --dw-tint-strong: color-mix(in srgb, var(--dw-accent) 18%, transparent);
--dw-warn: #a35a00;
--dw-warn-tint: color-mix(in srgb, #e8a33d 18%, transparent);
--dw-bad: #b42318;
--dw-good: #1e7a3a;
display: grid; display: grid;
gap: 12px; gap: 14px;
margin: 16px 0; margin: 20px 0;
padding: 18px;
border: 1px solid var(--dw-line);
border-radius: 14px;
font-size: 0.95em; font-size: 0.95em;
line-height: 1.4; line-height: 1.45;
min-width: 0; min-width: 0;
text-align: left;
} }
.datewise--drawer { margin: 0 0 12px; font-size: 0.9em; } .datewise--drawer { margin: 0 0 12px; padding: 14px; font-size: 0.9em; gap: 12px; }
.datewise * { box-sizing: border-box; } .datewise * { box-sizing: border-box; }
.datewise button { font: inherit; color: inherit; cursor: pointer; } .datewise button { font: inherit; color: inherit; cursor: pointer; -webkit-tap-highlight-color: transparent; }
.datewise [aria-disabled="true"] { cursor: not-allowed; opacity: 0.5; } .datewise [aria-disabled="true"] { cursor: not-allowed; opacity: 0.45; }
.datewise :focus-visible { outline: 2px solid var(--ss-accent); outline-offset: 2px; } .datewise :focus-visible { outline: 2px solid var(--dw-accent); outline-offset: 2px; }
.ss-h { margin: 0; font-size: 1.15em; } .dw-h { margin: 0; font-size: 1.12em; font-weight: 600; letter-spacing: -0.005em; }
.ss-live:empty { display: none; }
.ss-msg { margin: 0; padding: 8px 12px; border-radius: 8px; background: var(--ss-soft); } /* messages */
.ss-critical { background: rgba(180, 35, 24, 0.1); color: var(--ss-bad); } .dw-live:empty { display: none; }
.ss-success { background: rgba(30, 122, 58, 0.1); color: var(--ss-good); } .dw-msg { margin: 0; padding: 10px 12px; border-radius: 10px; background: var(--dw-tint); }
.ss-peak { background: var(--ss-warn-soft); color: var(--ss-warn); } .dw-critical { background: color-mix(in srgb, var(--dw-bad) 9%, transparent); color: var(--dw-bad); }
.ss-note { margin: 0; opacity: 0.8; font-size: 0.9em; } .dw-success { background: color-mix(in srgb, var(--dw-good) 10%, transparent); color: var(--dw-good); }
.ss-methods { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 8px; } .dw-peak { background: var(--dw-warn-tint); color: var(--dw-warn); }
.ss-method, .ss-loc, .ss-slot { .dw-note { margin: 0; font-size: 0.88em; opacity: 0.75; }
display: grid; gap: 2px; text-align: left; padding: 10px; border-radius: 10px;
border: 1.5px solid var(--ss-line); background: transparent; /* method switcher: one segmented control */
.dw-methods {
display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 4px;
padding: 4px; border-radius: 12px; background: var(--dw-wash); border: 1px solid var(--dw-line);
}
.dw-method {
display: grid; justify-items: center; gap: 2px; padding: 10px 6px 9px; text-align: center;
border: 0; border-radius: 9px; background: transparent; transition: background-color 0.15s, box-shadow 0.15s;
}
.dw-method b { font-weight: 600; font-size: 0.95em; }
.dw-method small { font-size: 0.76em; opacity: 0.7; line-height: 1.25; }
.dw-method[aria-pressed="true"] { background: var(--dw-accent); color: var(--dw-on-accent); box-shadow: 0 1px 2px color-mix(in srgb, var(--dw-accent) 40%, transparent); }
.dw-method[aria-pressed="true"] small { opacity: 0.85; }
.dw-method:not([aria-pressed="true"]):not([aria-disabled="true"]):hover { background: var(--dw-tint); }
.dw-ico { width: 22px; height: 22px; flex: none; }
@media (max-width: 380px) { .dw-method:not([aria-disabled="true"]) small { display: none; } }
/* locations */
.dw-locs { display: grid; gap: 6px; }
.dw-loc {
display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 10px;
padding: 11px 12px; text-align: left; border-radius: 10px; border: 1px solid var(--dw-line); background: transparent;
}
.dw-loc span:first-child { display: grid; gap: 1px; }
.dw-loc small { font-size: 0.82em; opacity: 0.7; }
.dw-loc span:last-child { font-variant-numeric: tabular-nums; font-size: 0.9em; }
.dw-loc[aria-pressed="true"] { border-color: var(--dw-accent); box-shadow: inset 0 0 0 1px var(--dw-accent); background: var(--dw-tint); }
/* postal check */
.dw-postal { display: grid; gap: 6px; }
.dw-postal label { font-size: 0.85em; opacity: 0.75; }
.dw-row { display: flex; gap: 8px; flex-wrap: wrap; }
.dw-row input {
flex: 1; min-width: 0; padding: 10px 12px; border: 1px solid var(--dw-line-strong); border-radius: 10px;
font: inherit; background: transparent; color: inherit;
}
.dw-row input:focus { outline: none; border-color: var(--dw-accent); box-shadow: 0 0 0 3px var(--dw-tint-strong); }
.dw-row button { padding: 10px 16px; border-radius: 10px; border: 0; background: var(--dw-accent); color: var(--dw-on-accent); font-weight: 600; }
/* calendar */
.dw-cal { display: grid; gap: 8px; }
.dw-calhead { display: flex; justify-content: space-between; align-items: center; }
.dw-calhead b { font-weight: 600; }
.dw-calhead button {
width: 34px; height: 34px; display: grid; place-items: center; border: 1px solid var(--dw-line);
border-radius: 50%; background: transparent; font-size: 1.15em; line-height: 1;
}
.dw-calhead button:hover { border-color: var(--dw-line-strong); }
.dw-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px 2px; }
.dw-dow { text-align: center; font-size: 0.74em; opacity: 0.55; padding-bottom: 2px; }
.dw-day {
position: relative; justify-self: center; width: 100%; max-width: 44px; aspect-ratio: 1;
border: 1px solid transparent; border-radius: 50%; background: transparent; padding: 0;
font-variant-numeric: tabular-nums; transition: background-color 0.12s, border-color 0.12s;
}
.dw-avail { font-weight: 600; }
.dw-avail:hover { border-color: var(--dw-accent); }
.dw-avail::before { /* availability dot */
content: ""; position: absolute; left: 50%; bottom: 14%; width: 4px; height: 4px; margin-left: -2px;
border-radius: 50%; background: var(--dw-accent);
}
.dw-full { color: var(--dw-warn); background: var(--dw-warn-tint); }
.dw-closed { opacity: 0.32; text-decoration: line-through; text-decoration-thickness: 1px; }
.dw-out { opacity: 0.18; }
.dw-sel, .dw-sel:hover { background: var(--dw-accent); color: var(--dw-on-accent); border-color: var(--dw-accent); }
.dw-sel::before { background: var(--dw-on-accent); }
.dw-peakday::after {
content: ""; position: absolute; top: 8%; right: 8%; width: 7px; height: 7px; border-radius: 50%;
background: #d98a0b;
}
.dw-left { /* "3 left" — only shown when scarce */
position: absolute; left: 50%; bottom: -3px; transform: translateX(-50%); padding: 0 5px;
font-size: 0.58em; font-style: normal; font-weight: 600; line-height: 1.5; border-radius: 99px;
background: var(--dw-warn-tint); color: var(--dw-warn); white-space: nowrap;
}
.dw-avail:has(.dw-left)::before { display: none; }
/* day detail + time chips */
.dw-day-detail { display: grid; gap: 10px; padding-top: 12px; border-top: 1px solid var(--dw-line); }
.dw-day-detail h3 { margin: 0; font-size: 1em; font-weight: 600; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.dw-badge { font-size: 0.74em; font-weight: 600; padding: 2px 9px; border-radius: 99px; background: var(--dw-warn-tint); color: var(--dw-warn); }
.dw-slots { display: flex; flex-wrap: wrap; gap: 8px; }
.dw-slot {
display: inline-grid; justify-items: center; gap: 0; padding: 8px 16px; border-radius: 999px;
border: 1px solid var(--dw-line-strong); background: transparent; text-align: center; font-variant-numeric: tabular-nums;
}
.dw-slot b { font-weight: 600; white-space: nowrap; }
.dw-slot small { font-size: 0.74em; opacity: 0.7; }
.dw-slot small:empty { display: none; }
.dw-slot:not([aria-disabled="true"]):hover { border-color: var(--dw-accent); }
.dw-slot[aria-pressed="true"] { background: var(--dw-accent); border-color: var(--dw-accent); color: var(--dw-on-accent); }
/* booking stub: the confirmation ticket */
.dw-stub {
position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 14px;
padding: 14px 16px 14px 18px; border-radius: 12px; background: var(--dw-tint);
border-left: 3px dashed var(--dw-accent);
animation: dw-stub-in 0.28s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.dw-stub__main { display: grid; gap: 1px; min-width: 0; }
.dw-stub__method { display: flex; align-items: center; gap: 6px; font-size: 0.82em; opacity: 0.8; }
.dw-stub__method .dw-ico { width: 16px; height: 16px; }
.dw-stub__date { font-size: 1.12em; font-weight: 650; letter-spacing: -0.01em; }
.dw-stub__time { font-variant-numeric: tabular-nums; }
.dw-stub__fee { font-size: 0.82em; opacity: 0.75; font-variant-numeric: tabular-nums; }
.dw-stub__side { display: grid; justify-items: center; gap: 4px; text-align: center; max-width: 9em; }
.dw-stub__hold { font-size: 0.74em; opacity: 0.8; line-height: 1.3; }
.dw-stub__hold b { display: block; font-size: 1.25em; font-variant-numeric: tabular-nums; opacity: 1; }
.dw-stub--expired { background: var(--dw-warn-tint); border-left-color: var(--dw-warn); }
.dw-ring { width: 34px; height: 34px; transform: rotate(-90deg); }
.dw-ring circle { fill: none; stroke: var(--dw-tint-strong); stroke-width: 3; }
.dw-ring .dw-ring__arc { stroke: var(--dw-accent); stroke-linecap: round; stroke-dasharray: 100; transition: stroke-dashoffset 1s linear; }
.dw-check { width: 30px; height: 30px; padding: 6px; border-radius: 50%; background: var(--dw-accent); fill: none; stroke: var(--dw-on-accent); stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
@keyframes dw-stub-in { from { opacity: 0; transform: translateY(4px); } }
/* loading */
.dw-skeleton {
height: 132px; border-radius: 12px;
background: linear-gradient(90deg, var(--dw-wash, rgba(127,127,127,.08)) 0%, rgba(127, 127, 127, 0.16) 50%, var(--dw-wash, rgba(127,127,127,.08)) 100%);
background-size: 200% 100%; animation: dw-shimmer 1.3s infinite linear;
}
@keyframes dw-shimmer { to { background-position: -200% 0; } }
.datewise[aria-busy="true"] .dw-slots, .datewise[aria-busy="true"] .dw-grid { opacity: 0.55; pointer-events: none; transition: opacity 0.2s; }
@media (prefers-reduced-motion: reduce) {
.dw-skeleton, .dw-stub { animation: none; }
.dw-ring .dw-ring__arc, .dw-day, .dw-method { transition: none; }
} }
.ss-method small, .ss-loc small, .ss-slot small { opacity: 0.75; font-size: 0.82em; }
.ss-method[aria-pressed="true"], .ss-loc[aria-pressed="true"], .ss-slot[aria-pressed="true"] { border-color: var(--ss-accent); background: var(--ss-soft); }
.ss-locs { display: grid; gap: 6px; }
.ss-loc { grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
.ss-loc span:first-child { display: grid; }
.ss-postal { display: grid; gap: 4px; }
.ss-postal label { font-size: 0.85em; opacity: 0.8; }
.ss-row { display: flex; gap: 8px; flex-wrap: wrap; }
.ss-row input { flex: 1; min-width: 0; padding: 8px 10px; border: 1px solid var(--ss-line); border-radius: 8px; font: inherit; background: transparent; color: inherit; }
.ss-row button { padding: 8px 14px; border-radius: 8px; border: 1px solid var(--ss-accent); background: var(--ss-accent); color: #fff; }
.ss-calhead { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.ss-calhead button { border: 0; background: transparent; font-size: 1.3em; padding: 2px 10px; }
.ss-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px; }
.ss-dow { text-align: center; font-size: 0.75em; opacity: 0.7; }
.ss-day { position: relative; aspect-ratio: 1 / 0.9; border: 1px solid transparent; border-radius: 8px; background: transparent; padding: 0; font-variant-numeric: tabular-nums; }
.ss-avail { background: var(--ss-soft); }
.ss-avail:hover { border-color: var(--ss-accent); }
.ss-full { background: var(--ss-warn-soft); color: var(--ss-warn); }
.ss-closed { text-decoration: line-through; opacity: 0.45; }
.ss-out { opacity: 0.25; }
.ss-sel { background: var(--ss-accent); color: #fff; }
.ss-peakday::after { content: ""; position: absolute; top: 4px; right: 5px; width: 6px; height: 6px; border-radius: 50%; background: #c77c02; }
.ss-left { position: absolute; bottom: 1px; left: 0; right: 0; font-size: 0.6em; font-style: normal; opacity: 0.8; }
.ss-day-detail { display: grid; gap: 8px; }
.ss-day-detail h3 { margin: 4px 0 0; font-size: 1em; }
.ss-badge { font-size: 0.75em; padding: 1px 8px; border-radius: 99px; background: var(--ss-warn-soft); color: var(--ss-warn); }
.ss-slots { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 8px; }
.ss-hold { margin: 0; padding: 8px 12px; border-radius: 8px; background: var(--ss-soft); }
.ss-skeleton { height: 120px; border-radius: 10px; background: linear-gradient(90deg, var(--ss-line), transparent, var(--ss-line)); background-size: 200% 100%; opacity: 0.35; animation: ss-shimmer 1.2s infinite linear; }
@keyframes ss-shimmer { to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) { .ss-skeleton { animation: none; } }
.datewise[aria-busy="true"] .ss-slots, .datewise[aria-busy="true"] .ss-grid { opacity: 0.6; pointer-events: none; }

View File

@ -36,7 +36,7 @@
var S = { var S = {
data: null, cart: null, lines: [], loading: false, message: null, messageTone: "info", data: null, cart: null, lines: [], loading: false, message: null, messageTone: "info",
method: lsGet("method"), locationId: lsGet("location"), postal: lsGet("postal") || "", method: lsGet("method"), locationId: lsGet("location"), postal: lsGet("postal") || "",
date: null, sel: null, token: lsGet("token"), expiresAt: null, monthOffset: 0, waitlistDone: false, mutating: 0 date: null, sel: null, token: lsGet("token"), expiresAt: null, heldAt: null, monthOffset: 0, waitlistDone: false, mutating: 0
}; };
// ---------- helpers ---------- // ---------- helpers ----------
@ -47,6 +47,12 @@
function slotText(s) { return timeLabel(s.start) + " – " + timeLabel(s.end); } function slotText(s) { return timeLabel(s.start) + " – " + timeLabel(s.end); }
function addMonths(ym, n) { var y = +ym.slice(0, 4), m = +ym.slice(5, 7) - 1 + n; y += Math.floor(m / 12); m = ((m % 12) + 12) % 12; return y + "-" + String(m + 1).padStart(2, "0"); } function addMonths(ym, n) { var y = +ym.slice(0, 4), m = +ym.slice(5, 7) - 1 + n; y += Math.floor(m / 12); m = ((m % 12) + 12) % 12; return y + "-" + String(m + 1).padStart(2, "0"); }
function daysIn(ym) { return new Date(Date.UTC(+ym.slice(0, 4), +ym.slice(5, 7), 0)).getUTCDate(); } function daysIn(ym) { return new Date(Date.UTC(+ym.slice(0, 4), +ym.slice(5, 7), 0)).getUTCDate(); }
var ICON = {
pickup: '<path d="M4 10v9h16v-9M3 10l1.5-5h15L21 10M3 10h18M3 10c0 1.4 1.3 2.5 3 2.5S9 11.4 9 10c0 1.4 1.3 2.5 3 2.5s3-1.1 3-2.5c0 1.4 1.3 2.5 3 2.5s3-1.1 3-2.5M10 19v-4h4v4"/>',
delivery: '<path d="M2 6h11v10H2zM13 9h4.5l3.5 3.5V16h-8M6.5 18.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM17 18.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"/>',
shipping: '<path d="M3.5 7.5 12 3l8.5 4.5v9L12 21l-8.5-4.5zM3.5 7.5 12 12l8.5-4.5M12 12v9M7.8 5.3l8.5 4.5"/>'
};
function icon(m) { return '<svg class="dw-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round">' + (ICON[m] || "") + "</svg>"; }
function weekday(d) { return new Date(d + "T12:00:00Z").getUTCDay(); } function weekday(d) { return new Date(d + "T12:00:00Z").getUTCDay(); }
function api(path, body, method) { function api(path, body, method) {
@ -173,6 +179,7 @@
if (!res.ok) { S.loading = false; setMessage(res.message || T.error, "critical"); refresh(true); return; } if (!res.ok) { S.loading = false; setMessage(res.message || T.error, "critical"); refresh(true); return; }
S.token = res.token; lsSet("token", res.token); S.token = res.token; lsSet("token", res.token);
S.expiresAt = Date.parse(res.expiresAt); S.expiresAt = Date.parse(res.expiresAt);
S.heldAt = Date.now();
S.sel = { method: S.data.method, locationId: S.data.locationId, date: date, slot: slot, fee: res.fee ? res.fee.fee : 0 }; S.sel = { method: S.data.method, locationId: S.data.locationId, date: date, slot: slot, fee: res.fee ? res.fee.fee : 0 };
return writeAttributes(res.attributes) return writeAttributes(res.attributes)
.then(function () { return syncFee(res.feeVariantId); }) .then(function () { return syncFee(res.feeVariantId); })
@ -191,49 +198,49 @@
if (!root.classList.contains("datewise")) root.classList.add("datewise"); // custom mounts get styles too if (!root.classList.contains("datewise")) root.classList.add("datewise"); // custom mounts get styles too
var d = S.data; var d = S.data;
var heading = root.getAttribute("data-heading") || (d && d.text && d.text.heading) || ""; var heading = root.getAttribute("data-heading") || (d && d.text && d.text.heading) || "";
if (d && d.text && d.text.accent) root.style.setProperty("--ss-accent", d.text.accent); if (d && d.text && d.text.accent) root.style.setProperty("--dw-accent", d.text.accent);
var h = ""; var h = "";
if (heading) h += '<h2 class="ss-h">' + esc(heading) + "</h2>"; if (heading) h += '<h2 class="dw-h">' + esc(heading) + "</h2>";
h += '<div class="ss-live" role="status" aria-live="polite">' + (S.message ? '<p class="ss-msg ss-' + S.messageTone + '">' + esc(S.message) + "</p>" : "") + "</div>"; h += '<div class="dw-live" role="status" aria-live="polite">' + (S.message ? '<p class="dw-msg dw-' + S.messageTone + '">' + esc(S.message) + "</p>" : "") + "</div>";
if (!d) { root.innerHTML = h + (S.loading ? '<div class="ss-skeleton" aria-hidden="true"></div>' : ""); root.setAttribute("aria-busy", S.loading); return; } if (!d) { root.innerHTML = h + (S.loading ? '<div class="dw-skeleton" aria-hidden="true"></div>' : ""); root.setAttribute("aria-busy", S.loading); return; }
// methods // methods
h += '<div class="ss-methods" role="group" aria-label="Delivery method">'; h += '<div class="dw-methods" role="group" aria-label="Delivery method">';
d.methods.forEach(function (m) { d.methods.forEach(function (m) {
var on = m.method === d.method; var on = m.method === d.method;
h += '<button type="button" class="ss-method" data-ss="method" data-v="' + m.method + '" aria-pressed="' + on + '"' + (m.available || on ? "" : ' aria-disabled="true"') + ">" + h += '<button type="button" class="dw-method" data-dw="method" data-v="' + m.method + '" aria-pressed="' + on + '"' + (m.available || on ? "" : ' aria-disabled="true"') + ">" + icon(m.method) +
"<b>" + esc(d.text.labels[m.method]) + "</b><small>" + (m.needsAddress ? esc(T.postal) : m.available ? (m.firstDate ? esc(T.nextAvailable + ": " + dateLabel(m.firstDate)) : "") : esc(m.reason || "")) + "</small></button>"; "<b>" + esc(d.text.labels[m.method]) + "</b><small>" + (m.needsAddress ? esc(T.postal) : m.available ? (m.firstDate ? esc(T.nextAvailable + ": " + dateLabel(m.firstDate)) : "") : esc(m.reason || "")) + "</small></button>";
}); });
h += "</div>"; h += "</div>";
if (!d.method) { root.innerHTML = h; return; } if (!d.method) { root.innerHTML = h; return; }
if (d.method === "delivery") { if (d.method === "delivery") {
h += '<form class="ss-postal" data-ss="postal"><label for="ss-postal-' + rid(root) + '">' + esc(T.postal) + '</label><div class="ss-row"><input id="ss-postal-' + rid(root) + '" name="postal" autocomplete="postal-code" value="' + esc(S.postal) + '"><button type="submit">' + esc(T.check) + "</button></div></form>"; h += '<form class="dw-postal" data-dw="postal"><label for="dw-postal-' + rid(root) + '">' + esc(T.postal) + '</label><div class="dw-row"><input id="dw-postal-' + rid(root) + '" name="postal" autocomplete="postal-code" value="' + esc(S.postal) + '"><button type="submit">' + esc(T.check) + "</button></div></form>";
} }
if (d.split) h += '<p class="ss-msg ss-critical">' + esc(d.split) + "</p>"; if (d.split) h += '<p class="dw-msg dw-critical">' + esc(d.split) + "</p>";
if (d.method !== "shipping" && d.locations.length > 1) { if (d.method !== "shipping" && d.locations.length > 1) {
h += '<div class="ss-locs" role="group" aria-label="Location">'; h += '<div class="dw-locs" role="group" aria-label="Location">';
d.locations.forEach(function (l) { d.locations.forEach(function (l) {
var on = l.id === d.locationId; var on = l.id === d.locationId;
var right = l.fee ? (l.fee.waived ? T.free : money(l.fee.fee)) : l.distanceKm != null ? l.distanceKm + " km" : ""; var right = l.fee ? (l.fee.waived ? T.free : money(l.fee.fee)) : l.distanceKm != null ? l.distanceKm + " km" : "";
h += '<button type="button" class="ss-loc" data-ss="loc" data-v="' + esc(l.id) + '" aria-pressed="' + on + '"' + (l.eligible ? "" : ' aria-disabled="true"') + "><span><b>" + esc(l.name) + "</b><small>" + h += '<button type="button" class="dw-loc" data-dw="loc" data-v="' + esc(l.id) + '" aria-pressed="' + on + '"' + (l.eligible ? "" : ' aria-disabled="true"') + "><span><b>" + esc(l.name) + "</b><small>" +
esc(l.eligible ? (l.first ? T.nextAvailable + ": " + dateLabel(l.first.date) : "") : l.reason || "") + "</small></span><span>" + esc(right) + "</span></button>"; esc(l.eligible ? (l.first ? T.nextAvailable + ": " + dateLabel(l.first.date) : "") : l.reason || "") + "</small></span><span>" + esc(right) + "</span></button>";
}); });
h += "</div>"; h += "</div>";
} else if (d.locationId) { } else if (d.locationId) {
var only = d.locations.filter(function (l) { return l.id === d.locationId; })[0]; var only = d.locations.filter(function (l) { return l.id === d.locationId; })[0];
if (only && d.method !== "shipping") h += '<p class="ss-note">' + esc(only.name) + (only.fee ? " · " + (only.fee.waived ? T.free : money(only.fee.fee) + " " + T.fee) : "") + "</p>"; if (only && d.method !== "shipping") h += '<p class="dw-note">' + esc(only.name) + (only.fee ? " · " + (only.fee.waived ? T.free : money(only.fee.fee) + " " + T.fee) : "") + "</p>";
} }
if (!d.locationId) { if (!d.locationId) {
if (d.needsAddress) { root.innerHTML = h; return; } if (d.needsAddress) { root.innerHTML = h; return; }
var why = d.locations[0] && d.locations[0].reason; var why = d.locations[0] && d.locations[0].reason;
root.innerHTML = h + (why ? '<p class="ss-msg ss-critical">' + esc(why) + "</p>" : ""); root.innerHTML = h + (why ? '<p class="dw-msg dw-critical">' + esc(why) + "</p>" : "");
return; return;
} }
if (d.prepNote && d.prepNote.minutes >= 60) h += '<p class="ss-note">' + esc(T.prep + " " + d.prepNote.item) + " (" + Math.round(d.prepNote.minutes / 6) / 10 + " h)</p>"; if (d.prepNote && d.prepNote.minutes >= 60) h += '<p class="dw-note">' + esc(T.prep + " " + d.prepNote.item) + " (" + Math.round(d.prepNote.minutes / 6) / 10 + " h)</p>";
var peak = d.dates.filter(function (x) { return x.peak && x.peak.message; })[0]; var peak = d.dates.filter(function (x) { return x.peak && x.peak.message; })[0];
if (peak) h += '<p class="ss-msg ss-peak">' + esc(peak.peak.message) + "</p>"; if (peak) h += '<p class="dw-msg dw-peak">' + esc(peak.peak.message) + "</p>";
h += calendar(d); h += calendar(d);
h += slots(d, root); h += slots(d, root);
@ -243,7 +250,7 @@
} }
var ridSeq = 0; var ridSeq = 0;
function rid(root) { if (!root.__ssid) root.__ssid = ++ridSeq; return root.__ssid; } function rid(root) { if (!root.__dwid) root.__dwid = ++ridSeq; return root.__dwid; }
function calendar(d) { function calendar(d) {
var map = {}; var map = {};
@ -254,84 +261,102 @@
var start = d.text.weekStartsOn || 0; var start = d.text.weekStartsOn || 0;
var first = ym + "-01"; var first = ym + "-01";
var lead = (weekday(first) - start + 7) % 7; var lead = (weekday(first) - start + 7) % 7;
var h = '<div class="ss-cal"><div class="ss-calhead"><button type="button" data-ss="month" data-v="-1" aria-label="Previous month">‹</button><b>' + var h = '<div class="dw-cal"><div class="dw-calhead"><button type="button" data-dw="month" data-v="-1" aria-label="Previous month">‹</button><b>' +
esc(new Intl.DateTimeFormat(LOCALE, { month: "long", year: "numeric", timeZone: "UTC" }).format(new Date(first + "T12:00:00Z"))) + esc(new Intl.DateTimeFormat(LOCALE, { month: "long", year: "numeric", timeZone: "UTC" }).format(new Date(first + "T12:00:00Z"))) +
'</b><button type="button" data-ss="month" data-v="1" aria-label="Next month">›</button></div><div class="ss-grid" role="grid">'; '</b><button type="button" data-dw="month" data-v="1" aria-label="Next month">›</button></div><div class="dw-grid" role="grid">';
for (var w = 0; w < 7; w++) { for (var w = 0; w < 7; w++) {
var wd = (start + w) % 7; var wd = (start + w) % 7;
h += '<span class="ss-dow" aria-hidden="true">' + esc(new Intl.DateTimeFormat(LOCALE, { weekday: "narrow", timeZone: "UTC" }).format(new Date(Date.UTC(2023, 0, 1 + wd)))) + "</span>"; h += '<span class="dw-dow" aria-hidden="true">' + esc(new Intl.DateTimeFormat(LOCALE, { weekday: "narrow", timeZone: "UTC" }).format(new Date(Date.UTC(2023, 0, 1 + wd)))) + "</span>";
} }
for (var i = 0; i < lead; i++) h += "<span></span>"; for (var i = 0; i < lead; i++) h += "<span></span>";
for (var day = 1; day <= daysIn(ym); day++) { for (var day = 1; day <= daysIn(ym); day++) {
var date = ym + "-" + String(day).padStart(2, "0"); var date = ym + "-" + String(day).padStart(2, "0");
var x = map[date]; var x = map[date];
var cls = "ss-day", label = dateLabel(date, true), extra = ""; var cls = "dw-day", label = dateLabel(date, true), extra = "";
if (!x) cls += " ss-out"; if (!x) cls += " dw-out";
else if (x.available) { cls += " ss-avail"; if (d.text.showRemaining && x.remaining != null && x.remaining <= 5) extra = '<i class="ss-left">' + x.remaining + "</i>"; } else if (x.available) { cls += " dw-avail"; if (d.text.showRemaining && x.remaining != null && x.remaining <= 5) extra = '<i class="dw-left">' + x.remaining + "</i>"; }
else if (x.waitlist || /full|booked/i.test(x.reasons[0] || "")) { cls += " ss-full"; label += ", " + T.full; } else if (x.waitlist || /full|booked/i.test(x.reasons[0] || "")) { cls += " dw-full"; label += ", " + T.full; }
else { cls += " ss-closed"; label += ", " + (x.reasons[0] || ""); } else { cls += " dw-closed"; label += ", " + (x.reasons[0] || ""); }
if (S.date === date) cls += " ss-sel"; if (S.date === date) cls += " dw-sel";
if (x && x.peak) { cls += " ss-peakday"; label += ", " + x.peak.name; } if (x && x.peak) { cls += " dw-peakday"; label += ", " + x.peak.name; }
h += '<button type="button" class="' + cls + '" data-ss="date" data-v="' + date + '" aria-label="' + esc(label) + '"' + (x && (x.available || x.waitlist || x.reasons.length) ? "" : ' aria-disabled="true"') + ' tabindex="' + (S.date === date || (!S.date && date === firstAvail) ? "0" : "-1") + '">' + day + extra + "</button>"; h += '<button type="button" class="' + cls + '" data-dw="date" data-v="' + date + '" aria-label="' + esc(label) + '"' + (x && (x.available || x.waitlist || x.reasons.length) ? "" : ' aria-disabled="true"') + ' tabindex="' + (S.date === date || (!S.date && date === firstAvail) ? "0" : "-1") + '">' + day + extra + "</button>";
} }
return h + "</div></div>"; return h + "</div></div>";
} }
function slots(d, root) { function slots(d, root) {
if (!S.date) return '<p class="ss-note">' + esc(T.choose) + "</p>"; if (!S.date) return '<p class="dw-note">' + esc(T.choose) + "</p>";
var x = d.dates.filter(function (y) { return y.date === S.date; })[0]; var x = d.dates.filter(function (y) { return y.date === S.date; })[0];
if (!x) return ""; if (!x) return "";
var h = '<div class="ss-day-detail"><h3>' + esc(dateLabel(x.date, true)) + (x.peak ? ' <span class="ss-badge">' + esc(x.peak.name) + "</span>" : "") + "</h3>"; var h = '<div class="dw-day-detail"><h3>' + esc(dateLabel(x.date, true)) + (x.peak ? ' <span class="dw-badge">' + esc(x.peak.name) + "</span>" : "") + "</h3>";
if (d.dateOnly) h += '<p class="ss-note">' + esc(T.dispatch) + "</p>"; if (d.dateOnly) h += '<p class="dw-note">' + esc(T.dispatch) + "</p>";
if (!x.available) { if (!x.available) {
h += '<p class="ss-msg ' + (x.waitlist ? "ss-peak" : "ss-critical") + '">' + esc(x.reasons.join(". ")) + "</p>"; h += '<p class="dw-msg ' + (x.waitlist ? "dw-peak" : "dw-critical") + '">' + esc(x.reasons.join(". ")) + "</p>";
if (x.waitlist && !S.waitlistDone) if (x.waitlist && !S.waitlistDone)
h += '<form class="ss-row" data-ss="waitlist"><input type="email" name="email" required placeholder="Email" aria-label="Email"><button type="submit">' + esc(T.waitlist) + "</button></form>"; h += '<form class="dw-row" data-dw="waitlist"><input type="email" name="email" required placeholder="Email" aria-label="Email"><button type="submit">' + esc(T.waitlist) + "</button></form>";
if (S.waitlistDone) h += '<p class="ss-msg ss-success">' + esc(T.joined) + "</p>"; if (S.waitlistDone) h += '<p class="dw-msg dw-success">' + esc(T.joined) + "</p>";
return h + "</div>"; return h + "</div>";
} }
h += '<div class="ss-slots" role="group" aria-label="Times">'; h += '<div class="dw-slots" role="group" aria-label="Times">';
x.slots.forEach(function (s) { x.slots.forEach(function (s) {
var on = S.sel && S.sel.date === x.date && S.sel.slot === s.id; var on = S.sel && S.sel.date === x.date && S.sel.slot === s.id;
var sub = s.available ? (d.text.showRemaining && s.remaining != null ? s.remaining + " " + T.left : "") + (s.surcharge ? (s.remaining != null && d.text.showRemaining ? " · " : "") + "+" + money(s.surcharge) : "") : s.reason || ""; var sub = s.available ? (d.text.showRemaining && s.remaining != null ? s.remaining + " " + T.left : "") + (s.surcharge ? (s.remaining != null && d.text.showRemaining ? " · " : "") + "+" + money(s.surcharge) : "") : s.reason || "";
h += '<button type="button" class="ss-slot" data-ss="slot" data-v="' + s.id + '" aria-pressed="' + !!on + '"' + (s.available || on ? "" : ' aria-disabled="true"') + "><b>" + esc(d.dateOnly ? dateLabel(x.date) : slotText(s)) + "</b><small>" + esc(sub) + "</small></button>"; h += '<button type="button" class="dw-slot" data-dw="slot" data-v="' + s.id + '" aria-pressed="' + !!on + '"' + (s.available || on ? "" : ' aria-disabled="true"') + "><b>" + esc(d.dateOnly ? dateLabel(x.date) : slotText(s)) + "</b><small>" + esc(sub) + "</small></button>";
}); });
h += "</div>"; h += "</div>";
if (S.sel && S.sel.date === x.date) { if (S.sel && S.sel.date === x.date) h += stub(d, x);
var live = S.expiresAt && S.expiresAt > Date.now();
h += '<p class="ss-hold">' + (live ? esc(T.held) + ' <b data-ss-timer></b>' : S.expiresAt ? esc(T.expired) : "✓") + "</p>";
var loc = d.locations.filter(function (l) { return l.id === d.locationId; })[0];
if (loc && loc.pickupInstructions) h += '<p class="ss-note">' + esc(loc.pickupInstructions) + "</p>";
}
return h + "</div>"; return h + "</div>";
} }
/** The "ticket" confirming what the customer chose, with the hold countdown. */
function stub(d, x) {
var loc = d.locations.filter(function (l) { return l.id === d.locationId; })[0];
var s = x.slots.filter(function (y) { return y.id === S.sel.slot; })[0];
var live = S.expiresAt && S.expiresAt > Date.now();
var expired = S.expiresAt && !live;
var h = '<div class="dw-stub' + (expired ? " dw-stub--expired" : "") + '"><div class="dw-stub__main">' +
'<span class="dw-stub__method">' + icon(d.method) + esc(d.text.labels[d.method]) + (loc && d.method !== "shipping" ? ", " + esc(loc.name) : "") + "</span>" +
'<b class="dw-stub__date">' + esc(dateLabel(x.date, true)) + "</b>" +
(s && !d.dateOnly ? '<span class="dw-stub__time">' + esc(slotText(s)) + "</span>" : "") +
(S.sel.fee ? '<span class="dw-stub__fee">+' + esc(money(S.sel.fee)) + " " + esc(T.fee) + "</span>" : "") +
'</div><div class="dw-stub__side">';
if (live) h += '<svg class="dw-ring" viewBox="0 0 36 36" aria-hidden="true"><circle cx="18" cy="18" r="15.5"/><circle class="dw-ring__arc" data-dw-ring cx="18" cy="18" r="15.5" pathLength="100"/></svg><span class="dw-stub__hold">' + esc(T.held) + ' <b data-dw-timer></b></span>';
else if (expired) h += '<span class="dw-stub__hold">' + esc(T.expired) + "</span>";
else h += '<svg class="dw-check" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5 10 17l9-10"/></svg>';
h += "</div></div>";
if (loc && loc.pickupInstructions && d.method === "pickup") h += '<p class="dw-note">' + esc(loc.pickupInstructions) + "</p>";
return h;
}
function tickTimer() { function tickTimer() {
var left = S.expiresAt ? S.expiresAt - Date.now() : 0; var left = S.expiresAt ? S.expiresAt - Date.now() : 0;
var els = document.querySelectorAll("[data-ss-timer]"); var els = document.querySelectorAll("[data-dw-timer]");
if (left <= 0 && els.length) return renderAll(); if (left <= 0 && els.length) return renderAll();
var s = Math.max(0, Math.round(left / 1000)); var s = Math.max(0, Math.round(left / 1000));
for (var i = 0; i < els.length; i++) els[i].textContent = Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0"); for (var i = 0; i < els.length; i++) els[i].textContent = Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0");
var total = S.expiresAt && S.heldAt ? S.expiresAt - S.heldAt : 0;
var rings = document.querySelectorAll("[data-dw-ring]");
for (var j = 0; j < rings.length; j++) rings[j].style.strokeDashoffset = total > 0 ? String(100 - Math.max(0, Math.min(100, (left / total) * 100))) : "0";
} }
setInterval(tickTimer, 1000); setInterval(tickTimer, 1000);
// ---------- events ---------- // ---------- events ----------
document.addEventListener("click", function (e) { document.addEventListener("click", function (e) {
var t = e.target.closest && e.target.closest("[data-datewise-root] [data-ss]"); var t = e.target.closest && e.target.closest("[data-datewise-root] [data-dw]");
if (!t || t.tagName === "FORM") return; if (!t || t.tagName === "FORM") return;
if (t.getAttribute("aria-disabled") === "true" && t.getAttribute("data-ss") !== "date") return; if (t.getAttribute("aria-disabled") === "true" && t.getAttribute("data-dw") !== "date") return;
var a = t.getAttribute("data-ss"), v = t.getAttribute("data-v"); var a = t.getAttribute("data-dw"), v = t.getAttribute("data-v");
if (a === "method" && v !== S.method) { S.method = v; lsSet("method", v); S.date = null; S.monthOffset = 0; refresh(true); } if (a === "method" && v !== S.method) { S.method = v; lsSet("method", v); S.date = null; S.monthOffset = 0; refresh(true); }
else if (a === "loc" && v !== S.locationId) { S.locationId = v; lsSet("location", v); S.date = null; refresh(true); } else if (a === "loc" && v !== S.locationId) { S.locationId = v; lsSet("location", v); S.date = null; refresh(true); }
else if (a === "month") { S.monthOffset += +v; renderAll(); } else if (a === "month") { S.monthOffset += +v; renderAll(); }
else if (a === "date") { S.date = v; S.waitlistDone = false; setMessage(null); renderAll(); focusFirst(".ss-slot:not([aria-disabled])"); } else if (a === "date") { S.date = v; S.waitlistDone = false; setMessage(null); renderAll(); focusFirst(".dw-slot:not([aria-disabled])"); }
else if (a === "slot") hold(S.date, v); else if (a === "slot") hold(S.date, v);
}); });
document.addEventListener("submit", function (e) { document.addEventListener("submit", function (e) {
var f = e.target; var f = e.target;
if (!f.closest || !f.closest("[data-datewise-root]")) return; if (!f.closest || !f.closest("[data-datewise-root]")) return;
var a = f.getAttribute("data-ss"); var a = f.getAttribute("data-dw");
if (!a) return; if (!a) return;
e.preventDefault(); e.preventDefault();
if (a === "postal") { S.postal = f.postal.value.trim(); lsSet("postal", S.postal); S.date = null; refresh(true); } if (a === "postal") { S.postal = f.postal.value.trim(); lsSet("postal", S.postal); S.date = null; refresh(true); }
@ -346,10 +371,10 @@
// Arrow-key navigation in the calendar grid. // Arrow-key navigation in the calendar grid.
document.addEventListener("keydown", function (e) { document.addEventListener("keydown", function (e) {
var t = e.target; var t = e.target;
if (!t.classList || !t.classList.contains("ss-day")) return; if (!t.classList || !t.classList.contains("dw-day")) return;
var step = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[e.key]; var step = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[e.key];
if (!step) return; if (!step) return;
var days = Array.prototype.slice.call(t.parentNode.querySelectorAll(".ss-day")); var days = Array.prototype.slice.call(t.parentNode.querySelectorAll(".dw-day"));
var next = days[days.indexOf(t) + step]; var next = days[days.indexOf(t) + step];
if (next) { e.preventDefault(); t.tabIndex = -1; next.tabIndex = 0; next.focus(); } if (next) { e.preventDefault(); t.tabIndex = -1; next.tabIndex = 0; next.focus(); }
}); });

View File

@ -8,7 +8,7 @@
data-heading="{{ block.settings.heading | escape }}" data-heading="{{ block.settings.heading | escape }}"
{{ block.shopify_attributes }} {{ block.shopify_attributes }}
> >
<div class="ss-skeleton" aria-hidden="true"></div> <div class="dw-skeleton" aria-hidden="true"></div>
<noscript>{{ 'datewise.noscript' | t }}</noscript> <noscript>{{ 'datewise.noscript' | t }}</noscript>
</div> </div>

View File

@ -16,27 +16,27 @@ const step = (s) => console.log("•", s);
await fetch(`${BASE}/__reset`); await fetch(`${BASE}/__reset`);
await page.goto(`${BASE}/cart`); await page.goto(`${BASE}/cart`);
await page.waitForSelector(".ss-methods"); await page.waitForSelector(".dw-methods");
step("shipping is blocked by the fresh-flower rule, with the reason shown"); step("shipping is blocked by the fresh-flower rule, with the reason shown");
const ship = page.locator('.ss-method[data-v="shipping"]'); const ship = page.locator('.dw-method[data-v="shipping"]');
assert.equal(await ship.getAttribute("aria-disabled"), "true"); assert.equal(await ship.getAttribute("aria-disabled"), "true");
assert.match(await ship.innerText(), /Fresh flowers are pickup or local delivery only/); assert.match(await ship.innerText(), /Fresh flowers are pickup or local delivery only/);
step("checkout is stopped until a time is chosen"); step("checkout is stopped until a time is chosen");
await page.click('button[name="checkout"]'); await page.click('button[name="checkout"]');
assert.match(await page.locator(".ss-live").innerText(), /choose a delivery or pickup time/i); assert.match(await page.locator(".dw-live").innerText(), /choose a delivery or pickup time/i);
assert.equal(await page.locator("#checkedout").innerText(), ""); assert.equal(await page.locator("#checkedout").innerText(), "");
step("delivery: postal code → zone fee → pick first date and slot → hold"); step("delivery: postal code → zone fee → pick first date and slot → hold");
await page.click('.ss-method[data-v="delivery"]'); await page.click('.dw-method[data-v="delivery"]');
await page.waitForSelector(".ss-postal"); await page.waitForSelector(".dw-postal");
await page.fill(".ss-postal input", "N2H 5Z6"); await page.fill(".dw-postal input", "N2H 5Z6");
await page.click(".ss-postal button"); await page.click(".dw-postal button");
await page.waitForSelector(".ss-day.ss-avail"); await page.waitForSelector(".dw-day.dw-avail");
await page.locator(".ss-day.ss-avail").first().click(); await page.locator(".dw-day.dw-avail").first().click();
await page.locator('.ss-slot:not([aria-disabled="true"])').first().click(); await page.locator('.dw-slot:not([aria-disabled="true"])').first().click();
await page.waitForSelector("[data-ss-timer]"); await page.waitForSelector("[data-dw-timer]");
let c = await cart(); let c = await cart();
const sel = JSON.parse(c.attributes._datewise); const sel = JSON.parse(c.attributes._datewise);
assert.equal(sel.m, "delivery"); assert.equal(sel.m, "delivery");
@ -59,25 +59,25 @@ c = await cart();
assert.ok(c.attributes._datewise, "selection kept"); assert.ok(c.attributes._datewise, "selection kept");
step("switching to pickup releases the delivery time and removes the fee"); step("switching to pickup releases the delivery time and removes the fee");
await page.click('.ss-method[data-v="pickup"]'); await page.click('.dw-method[data-v="pickup"]');
await page.waitForTimeout(1500); await page.waitForTimeout(1500);
c = await cart(); c = await cart();
assert.equal(c.attributes._datewise, undefined); assert.equal(c.attributes._datewise, undefined);
assert.equal(c.items.filter((i) => i.properties._datewise_fee).length, 0); assert.equal(c.items.filter((i) => i.properties._datewise_fee).length, 0);
step("pickup hold → checkout goes through with the selection attached"); step("pickup hold → checkout goes through with the selection attached");
await page.locator(".ss-day.ss-avail").first().click(); await page.locator(".dw-day.dw-avail").first().click();
await page.locator('.ss-slot:not([aria-disabled="true"])').first().click(); await page.locator('.dw-slot:not([aria-disabled="true"])').first().click();
await page.waitForSelector("[data-ss-timer]"); await page.waitForSelector("[data-dw-timer]");
await page.click('button[name="checkout"]'); await page.click('button[name="checkout"]');
await page.waitForFunction(() => document.getElementById("checkedout").textContent.startsWith("CHECKOUT")); await page.waitForFunction(() => document.getElementById("checkedout").textContent.startsWith("CHECKOUT"));
assert.match(await page.locator("#checkedout").innerText(), /"m\\?":\\?"pickup/); assert.match(await page.locator("#checkedout").innerText(), /"m\\?":\\?"pickup/);
await page.screenshot({ path: `${SHOTS}/e2e-pickup.png`, fullPage: true }); await page.screenshot({ path: `${SHOTS}/e2e-pickup.png`, fullPage: true });
step("keyboard: arrow keys move between calendar days"); step("keyboard: arrow keys move between calendar days");
await page.locator(".ss-day[tabindex='0']").focus(); await page.locator(".dw-day[tabindex='0']").focus();
await page.keyboard.press("ArrowRight"); await page.keyboard.press("ArrowRight");
assert.equal(await page.evaluate(() => document.activeElement.classList.contains("ss-day")), true); assert.equal(await page.evaluate(() => document.activeElement.classList.contains("dw-day")), true);
assert.deepEqual(errors, [], "no page errors"); assert.deepEqual(errors, [], "no page errors");
console.log("\nAll widget checks passed"); console.log("\nAll widget checks passed");