openvibers/openvibe.cam Commits
The camera: a big layout on tablets and desktops, and a swipe changes mode (#98)
Changes
@@@ -207,7 +207,7 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--texcountdown, the filter strip's snap. */.cm-cam-app { display: grid; gap: 22px; }.cm-camera { position: relative; display: grid; gap: 12px; margin: 0; }--.cm-stage { position: relative; overflow: hidden; border-radius: 26px; background: #04060b; aspect-ratio: 4 / 3; max-height: 72vh; box-shadow: inset 0 0 0 1px rgba(255,255,255,.06), 0 30px 70px -40px rgba(0,0,0,.9); isolation: isolate; }++.cm-stage { position: relative; overflow: hidden; touch-action: pan-y; border-radius: 26px; background: #04060b; aspect-ratio: 4 / 3; max-height: 72vh; box-shadow: inset 0 0 0 1px rgba(255,255,255,.06), 0 30px 70px -40px rgba(0,0,0,.9); isolation: isolate; }.cm-stage video, .cm-stage canvas { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; transition: filter .25s ease; }.cm-camera[data-facing="user"] .cm-stage video { transform: scaleX(-1); }.cm-grid-overlay { position: absolute; inset: 0; pointer-events: none; z-index: 1;@@@ -220,6 +220,12 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex@keyframes cm-flash { 0% { opacity: .85; } 100% { opacity: 0; } }.cm-count { position: absolute; inset: 0; z-index: 3; display: grid; place-items: center; font-size: clamp(72px, 22vw, 160px); font-weight: 900; color: #fff; text-shadow: 0 6px 30px rgba(0,0,0,.5); pointer-events: none; }.cm-count:empty { display: none; }++/* A swipe's new mode, shown on the picture for a moment. */++.cm-mode-say { position: absolute; z-index: 3; top: 18px; left: 50%; padding: 6px 14px; border-radius: 999px; background: rgba(4,6,11,.55); color: #fff;++ -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); font-size: 13px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;++ opacity: 0; transform: translate(-50%, -6px); pointer-events: none; transition: opacity .18s ease, transform .18s ease; }++.cm-mode-say.is-on { opacity: 1; transform: translate(-50%, 0); }++.cm-mode-say:empty { display: none; }.cm-count.is-tick { animation: cm-tick .9s cubic-bezier(.2,1.3,.4,1); }@keyframes cm-tick { 0% { transform: scale(1.6); opacity: 0; } 30% { transform: scale(1); opacity: 1; } 100% { opacity: .2; } }/* Off: a calm card on the dark stage, one big button. */@@@ -259,7 +265,7 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex.cm-flip { justify-self: end; color: var(--text-primary, #e6edf7); background: var(--bg-card, #0f1729); box-shadow: inset 0 0 0 1px var(--border, #1f2d47); -webkit-backdrop-filter: none; backdrop-filter: none; }.cm-shoot-wrap { display: grid; justify-items: center; gap: 8px; }.cm-modes { display: inline-flex; gap: 2px; padding: 3px; border-radius: 999px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 6%, transparent); }--.cm-mode { height: 30px; padding: 0 12px; border: 0; border-radius: 999px; background: none; color: var(--text-secondary, #96a7c2); font: inherit; font-size: 12.5px; font-weight: 750; letter-spacing: .02em; text-transform: uppercase; cursor: pointer; }++.cm-mode { height: 30px; padding: 0 12px; border: 0; border-radius: 999px; background: none; transition: background-color .2s ease, color .2s ease; color: var(--text-secondary, #96a7c2); font: inherit; font-size: 12.5px; font-weight: 750; letter-spacing: .02em; text-transform: uppercase; cursor: pointer; }.cm-mode[aria-checked="true"] { color: var(--room-strong, var(--room-accent)); background: color-mix(in srgb, var(--room-accent) 16%, transparent); }.cm-shutter { position: relative; display: grid; place-items: center; width: 76px; height: 76px; padding: 0; border: 0; border-radius: 50%; background: none; cursor: pointer; }.cm-shutter::before { content: ""; position: absolute; inset: 0; border-radius: 50%; box-shadow: inset 0 0 0 4px var(--text-primary, #e6edf7); }@@@ -284,7 +290,25 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex.cm-under { padding: 0 16px; }}@media (min-width: 701px) { .cm-cam-app { padding-top: 18px; } .cm-stage { width: 100%; } .cm-bar { width: min(100%, 560px); margin: 0 auto; } .cm-filters { justify-content: safe center; } }--@media (prefers-reduced-motion: reduce) { .cm-flash.is-on, .cm-count.is-tick, .cm-thumb.is-new { animation: none; } .cm-stage video { transition: none; } }++/* A tablet or a desktop (11-07, "a big layout"): the camera is as tall as the screen under the bar. The picture takes the++ width; a rail on its right holds flip at the top, the modes over the shutter in the middle and your photos at the++ foot, as a camera app does on a tablet. The caption and the status sit under the picture. */++@media (min-width: 900px) {++ .cm-camera { grid-template-columns: minmax(0, 1fr) 128px; grid-template-rows: minmax(0, 1fr) auto; column-gap: 16px; row-gap: 12px;++ height: calc(100dvh - var(--ovnav-h, 60px) - 40px); min-height: 460px; }++ .cm-stage { grid-column: 1; grid-row: 1; aspect-ratio: auto; max-height: none; height: 100%; }++ .cm-bar { grid-column: 2; grid-row: 1 / span 2; width: auto; margin: 0; padding: 18px 0; grid-template-columns: none; grid-template-rows: 1fr auto 1fr;++ justify-items: center; border-radius: 26px; background: var(--bg-card, #0f1729); box-shadow: inset 0 0 0 1px var(--border, #1f2d47); }++ .cm-flip { grid-row: 1; align-self: start; justify-self: center; }++ .cm-shoot-wrap { grid-row: 2; gap: 14px; }++ .cm-thumb { grid-row: 3; align-self: end; justify-self: center; width: 60px; height: 60px; }++ .cm-modes { flex-direction: column; border-radius: 18px; }++ .cm-mode { height: 34px; }++ .cm-shutter { width: 84px; height: 84px; }++ .cm-shutter span { width: 66px; height: 66px; }++ .cm-under { grid-column: 1; grid-row: 2; grid-template-columns: minmax(0, 520px) auto; justify-content: center; align-items: center; gap: 16px; }++}++@media (prefers-reduced-motion: reduce) { .cm-flash.is-on, .cm-count.is-tick, .cm-thumb.is-new { animation: none; } .cm-stage video, .cm-mode-say, .cm-mode { transition: none; } }/* The /developers embed: the attributes, events and methods tables read as text, not as nowrap cells, and the live@@@ -204,6 +204,38 @@modeBtns.forEach(function (x) { x.setAttribute('aria-checked', x === b ? 'true' : 'false'); });shootBtn.setAttribute('aria-label', mode === 'burst' ? 'Take three photos' : 'Take the photo');}); });++ // Swipe the viewfinder sideways to change mode, as a phone camera does (11-07). Not from the filter strip or the++ // top controls, which scroll or press on their own; the mode's name shows on the picture for a moment.++ var stageEl = root.querySelector('.cm-stage');++ var modeSay = root.querySelector('[data-cm-mode-say]');++ var modeSayTimer = null;++ function stepMode(step) {++ var at = -1;++ modeBtns.forEach(function (x, i) { if (x.getAttribute('aria-checked') === 'true') at = i; });++ var next = modeBtns[at + step];++ if (!next) return;++ next.click();++ if (!modeSay) return;++ modeSay.textContent = next.textContent;++ modeSay.classList.remove('is-on');++ void modeSay.offsetWidth;++ modeSay.classList.add('is-on');++ clearTimeout(modeSayTimer);++ modeSayTimer = setTimeout(function () { modeSay.classList.remove('is-on'); }, 900);++ }++ if (stageEl && modeBtns.length > 1) {++ var swipe = null;++ stageEl.addEventListener('pointerdown', function (e) {++ swipe = (root.dataset.state === 'off' || (e.target.closest && e.target.closest('.cm-filters, .cm-top, .cm-off'))) ? null : { x: e.clientX, y: e.clientY, t: Date.now() };++ });++ stageEl.addEventListener('pointerup', function (e) {++ if (!swipe) return;++ var dx = e.clientX - swipe.x, dy = e.clientY - swipe.y, quick = Date.now() - swipe.t < 700;++ swipe = null;++ if (quick && Math.abs(dx) > 50 && Math.abs(dy) < Math.abs(dx) * 0.6) stepMode(dx < 0 ? 1 : -1);++ });++ stageEl.addEventListener('pointercancel', function () { swipe = null; });++ }burstBtn.addEventListener('click', burst);if (gridBtn) gridBtn.addEventListener('click', function () {if (!gridEl) return;@@@ -200,6 +200,7 @@ function app({ signedIn = false, photos = [] } = {}) {<div class="cm-grid-overlay" data-cm-grid hidden aria-hidden="true"></div><div class="cm-flash" data-cm-flash aria-hidden="true"></div><div class="cm-count" data-cm-count aria-hidden="true"></div>++<div class="cm-mode-say" data-cm-mode-say aria-hidden="true"></div><div class="cm-off" data-cm-off><span class="ov-icon" data-icon="cam" data-size="64" aria-hidden="true"></span><h1 class="cm-off-title">Camera</h1>@@@ -46,7 +46,7 @@ function jsonLd(html) {const r = await t.get('/app');assert.strictEqual(r.status, 200);assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=60');-- for (const marker of ['id="cm-app"', 'id="cm-camera"', 'id="cm-gallery"', 'data-cm-preview', 'data-cm-shoot', 'data-cm-timer', 'data-cm-burst', 'data-cm-filter', 'data-cm-file', 'data-cm-grid']) assert.ok(r.text.includes(marker), marker);++ for (const marker of ['id="cm-app"', 'id="cm-camera"', 'id="cm-gallery"', 'data-cm-preview', 'data-cm-shoot', 'data-cm-timer', 'data-cm-burst', 'data-cm-filter', 'data-cm-file', 'data-cm-grid', 'data-cm-mode-say']) assert.ok(r.text.includes(marker), marker);assert.ok(r.text.includes('Sign in'));});
@@ -207,7 +207,7 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
@@ -207,7 +207,7 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
countdown, the filter strip's snap. */
countdown, the filter strip's snap. */
.cm-cam-app { display: grid; gap: 22px; }
.cm-cam-app { display: grid; gap: 22px; }
.cm-camera { position: relative; display: grid; gap: 12px; margin: 0; }
.cm-camera { position: relative; display: grid; gap: 12px; margin: 0; }
-.cm-stage { position: relative; overflow: hidden; border-radius: 26px; background: #04060b; aspect-ratio: 4 / 3; max-height: 72vh; box-shadow: inset 0 0 0 1px rgba(255,255,255,.06), 0 30px 70px -40px rgba(0,0,0,.9); isolation: isolate; }
+.cm-stage { position: relative; overflow: hidden; touch-action: pan-y; border-radius: 26px; background: #04060b; aspect-ratio: 4 / 3; max-height: 72vh; box-shadow: inset 0 0 0 1px rgba(255,255,255,.06), 0 30px 70px -40px rgba(0,0,0,.9); isolation: isolate; }
.cm-stage video, .cm-stage canvas { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; transition: filter .25s ease; }
.cm-stage video, .cm-stage canvas { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; transition: filter .25s ease; }
.cm-camera[data-facing="user"] .cm-stage video { transform: scaleX(-1); }
.cm-camera[data-facing="user"] .cm-stage video { transform: scaleX(-1); }
.cm-grid-overlay { position: absolute; inset: 0; pointer-events: none; z-index: 1;
.cm-grid-overlay { position: absolute; inset: 0; pointer-events: none; z-index: 1;
@@ -220,6 +220,12 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
@@ -220,6 +220,12 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
@keyframes cm-flash { 0% { opacity: .85; } 100% { opacity: 0; } }
@keyframes cm-flash { 0% { opacity: .85; } 100% { opacity: 0; } }
.cm-count { position: absolute; inset: 0; z-index: 3; display: grid; place-items: center; font-size: clamp(72px, 22vw, 160px); font-weight: 900; color: #fff; text-shadow: 0 6px 30px rgba(0,0,0,.5); pointer-events: none; }
.cm-count { position: absolute; inset: 0; z-index: 3; display: grid; place-items: center; font-size: clamp(72px, 22vw, 160px); font-weight: 900; color: #fff; text-shadow: 0 6px 30px rgba(0,0,0,.5); pointer-events: none; }
.cm-count:empty { display: none; }
.cm-count:empty { display: none; }
+/* A swipe's new mode, shown on the picture for a moment. */
+.cm-mode-say { position: absolute; z-index: 3; top: 18px; left: 50%; padding: 6px 14px; border-radius: 999px; background: rgba(4,6,11,.55); color: #fff;
+ -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); font-size: 13px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
+ opacity: 0; transform: translate(-50%, -6px); pointer-events: none; transition: opacity .18s ease, transform .18s ease; }
+.cm-mode-say.is-on { opacity: 1; transform: translate(-50%, 0); }
+.cm-mode-say:empty { display: none; }
.cm-count.is-tick { animation: cm-tick .9s cubic-bezier(.2,1.3,.4,1); }
.cm-count.is-tick { animation: cm-tick .9s cubic-bezier(.2,1.3,.4,1); }
@keyframes cm-tick { 0% { transform: scale(1.6); opacity: 0; } 30% { transform: scale(1); opacity: 1; } 100% { opacity: .2; } }
@keyframes cm-tick { 0% { transform: scale(1.6); opacity: 0; } 30% { transform: scale(1); opacity: 1; } 100% { opacity: .2; } }
/* Off: a calm card on the dark stage, one big button. */
/* Off: a calm card on the dark stage, one big button. */
@@ -259,7 +265,7 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
@@ -259,7 +265,7 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
.cm-flip { justify-self: end; color: var(--text-primary, #e6edf7); background: var(--bg-card, #0f1729); box-shadow: inset 0 0 0 1px var(--border, #1f2d47); -webkit-backdrop-filter: none; backdrop-filter: none; }
.cm-flip { justify-self: end; color: var(--text-primary, #e6edf7); background: var(--bg-card, #0f1729); box-shadow: inset 0 0 0 1px var(--border, #1f2d47); -webkit-backdrop-filter: none; backdrop-filter: none; }
.cm-shoot-wrap { display: grid; justify-items: center; gap: 8px; }
.cm-shoot-wrap { display: grid; justify-items: center; gap: 8px; }
.cm-modes { display: inline-flex; gap: 2px; padding: 3px; border-radius: 999px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 6%, transparent); }
.cm-modes { display: inline-flex; gap: 2px; padding: 3px; border-radius: 999px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 6%, transparent); }
-.cm-mode { height: 30px; padding: 0 12px; border: 0; border-radius: 999px; background: none; color: var(--text-secondary, #96a7c2); font: inherit; font-size: 12.5px; font-weight: 750; letter-spacing: .02em; text-transform: uppercase; cursor: pointer; }
+.cm-mode { height: 30px; padding: 0 12px; border: 0; border-radius: 999px; background: none; transition: background-color .2s ease, color .2s ease; color: var(--text-secondary, #96a7c2); font: inherit; font-size: 12.5px; font-weight: 750; letter-spacing: .02em; text-transform: uppercase; cursor: pointer; }
.cm-mode[aria-checked="true"] { color: var(--room-strong, var(--room-accent)); background: color-mix(in srgb, var(--room-accent) 16%, transparent); }
.cm-mode[aria-checked="true"] { color: var(--room-strong, var(--room-accent)); background: color-mix(in srgb, var(--room-accent) 16%, transparent); }
.cm-shutter { position: relative; display: grid; place-items: center; width: 76px; height: 76px; padding: 0; border: 0; border-radius: 50%; background: none; cursor: pointer; }
.cm-shutter { position: relative; display: grid; place-items: center; width: 76px; height: 76px; padding: 0; border: 0; border-radius: 50%; background: none; cursor: pointer; }
.cm-shutter::before { content: ""; position: absolute; inset: 0; border-radius: 50%; box-shadow: inset 0 0 0 4px var(--text-primary, #e6edf7); }
.cm-shutter::before { content: ""; position: absolute; inset: 0; border-radius: 50%; box-shadow: inset 0 0 0 4px var(--text-primary, #e6edf7); }
@@ -284,7 +290,25 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
@@ -284,7 +290,25 @@ html, body { margin: 0; background: var(--bg-primary, #0a0f1c); color: var(--tex
.cm-under { padding: 0 16px; }
.cm-under { padding: 0 16px; }
}
}
@media (min-width: 701px) { .cm-cam-app { padding-top: 18px; } .cm-stage { width: 100%; } .cm-bar { width: min(100%, 560px); margin: 0 auto; } .cm-filters { justify-content: safe center; } }
@media (min-width: 701px) { .cm-cam-app { padding-top: 18px; } .cm-stage { width: 100%; } .cm-bar { width: min(100%, 560px); margin: 0 auto; } .cm-filters { justify-content: safe center; } }
-@media (prefers-reduced-motion: reduce) { .cm-flash.is-on, .cm-count.is-tick, .cm-thumb.is-new { animation: none; } .cm-stage video { transition: none; } }
+/* A tablet or a desktop (11-07, "a big layout"): the camera is as tall as the screen under the bar. The picture takes the
+ width; a rail on its right holds flip at the top, the modes over the shutter in the middle and your photos at the
+ foot, as a camera app does on a tablet. The caption and the status sit under the picture. */
+@media (min-width: 900px) {
+ .cm-camera { grid-template-columns: minmax(0, 1fr) 128px; grid-template-rows: minmax(0, 1fr) auto; column-gap: 16px; row-gap: 12px;
+ height: calc(100dvh - var(--ovnav-h, 60px) - 40px); min-height: 460px; }
+ .cm-stage { grid-column: 1; grid-row: 1; aspect-ratio: auto; max-height: none; height: 100%; }
+ .cm-bar { grid-column: 2; grid-row: 1 / span 2; width: auto; margin: 0; padding: 18px 0; grid-template-columns: none; grid-template-rows: 1fr auto 1fr;
+ justify-items: center; border-radius: 26px; background: var(--bg-card, #0f1729); box-shadow: inset 0 0 0 1px var(--border, #1f2d47); }
+ .cm-flip { grid-row: 1; align-self: start; justify-self: center; }
+ .cm-shoot-wrap { grid-row: 2; gap: 14px; }
+ .cm-thumb { grid-row: 3; align-self: end; justify-self: center; width: 60px; height: 60px; }
+ .cm-modes { flex-direction: column; border-radius: 18px; }
+ .cm-mode { height: 34px; }
+ .cm-shutter { width: 84px; height: 84px; }
+ .cm-shutter span { width: 66px; height: 66px; }
+ .cm-under { grid-column: 1; grid-row: 2; grid-template-columns: minmax(0, 520px) auto; justify-content: center; align-items: center; gap: 16px; }
+}
+@media (prefers-reduced-motion: reduce) { .cm-flash.is-on, .cm-count.is-tick, .cm-thumb.is-new { animation: none; } .cm-stage video, .cm-mode-say, .cm-mode { transition: none; } }
/* The /developers embed: the attributes, events and methods tables read as text, not as nowrap cells, and the live
/* The /developers embed: the attributes, events and methods tables read as text, not as nowrap cells, and the live
@@ -204,6 +204,38 @@
@@ -204,6 +204,38 @@
modeBtns.forEach(function (x) { x.setAttribute('aria-checked', x === b ? 'true' : 'false'); });
modeBtns.forEach(function (x) { x.setAttribute('aria-checked', x === b ? 'true' : 'false'); });
shootBtn.setAttribute('aria-label', mode === 'burst' ? 'Take three photos' : 'Take the photo');
shootBtn.setAttribute('aria-label', mode === 'burst' ? 'Take three photos' : 'Take the photo');
}); });
}); });
+ // Swipe the viewfinder sideways to change mode, as a phone camera does (11-07). Not from the filter strip or the
+ // top controls, which scroll or press on their own; the mode's name shows on the picture for a moment.
+ var stageEl = root.querySelector('.cm-stage');
+ var modeSay = root.querySelector('[data-cm-mode-say]');
+ var modeSayTimer = null;
+ function stepMode(step) {
+ var at = -1;
+ modeBtns.forEach(function (x, i) { if (x.getAttribute('aria-checked') === 'true') at = i; });
+ var next = modeBtns[at + step];
+ if (!next) return;
+ next.click();
+ if (!modeSay) return;
+ modeSay.textContent = next.textContent;
+ modeSay.classList.remove('is-on');
+ void modeSay.offsetWidth;
+ modeSay.classList.add('is-on');
+ clearTimeout(modeSayTimer);
+ modeSayTimer = setTimeout(function () { modeSay.classList.remove('is-on'); }, 900);
+ }
+ if (stageEl && modeBtns.length > 1) {
+ var swipe = null;
+ stageEl.addEventListener('pointerdown', function (e) {
+ swipe = (root.dataset.state === 'off' || (e.target.closest && e.target.closest('.cm-filters, .cm-top, .cm-off'))) ? null : { x: e.clientX, y: e.clientY, t: Date.now() };
+ });
+ stageEl.addEventListener('pointerup', function (e) {
+ if (!swipe) return;
+ var dx = e.clientX - swipe.x, dy = e.clientY - swipe.y, quick = Date.now() - swipe.t < 700;
+ swipe = null;
+ if (quick && Math.abs(dx) > 50 && Math.abs(dy) < Math.abs(dx) * 0.6) stepMode(dx < 0 ? 1 : -1);
+ });
+ stageEl.addEventListener('pointercancel', function () { swipe = null; });
+ }
burstBtn.addEventListener('click', burst);
burstBtn.addEventListener('click', burst);
if (gridBtn) gridBtn.addEventListener('click', function () {
if (gridBtn) gridBtn.addEventListener('click', function () {
if (!gridEl) return;
if (!gridEl) return;
@@ -200,6 +200,7 @@ function app({ signedIn = false, photos = [] } = {}) {
@@ -200,6 +200,7 @@ function app({ signedIn = false, photos = [] } = {}) {
<div class="cm-grid-overlay" data-cm-grid hidden aria-hidden="true"></div>
<div class="cm-grid-overlay" data-cm-grid hidden aria-hidden="true"></div>
<div class="cm-flash" data-cm-flash aria-hidden="true"></div>
<div class="cm-flash" data-cm-flash aria-hidden="true"></div>
<div class="cm-count" data-cm-count aria-hidden="true"></div>
<div class="cm-count" data-cm-count aria-hidden="true"></div>
+<div class="cm-mode-say" data-cm-mode-say aria-hidden="true"></div>
<div class="cm-off" data-cm-off>
<div class="cm-off" data-cm-off>
<span class="ov-icon" data-icon="cam" data-size="64" aria-hidden="true"></span>
<span class="ov-icon" data-icon="cam" data-size="64" aria-hidden="true"></span>
<h1 class="cm-off-title">Camera</h1>
<h1 class="cm-off-title">Camera</h1>
@@ -46,7 +46,7 @@ function jsonLd(html) {
@@ -46,7 +46,7 @@ function jsonLd(html) {
const r = await t.get('/app');
const r = await t.get('/app');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=60');
assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=60');
- for (const marker of ['id="cm-app"', 'id="cm-camera"', 'id="cm-gallery"', 'data-cm-preview', 'data-cm-shoot', 'data-cm-timer', 'data-cm-burst', 'data-cm-filter', 'data-cm-file', 'data-cm-grid']) assert.ok(r.text.includes(marker), marker);
+ for (const marker of ['id="cm-app"', 'id="cm-camera"', 'id="cm-gallery"', 'data-cm-preview', 'data-cm-shoot', 'data-cm-timer', 'data-cm-burst', 'data-cm-filter', 'data-cm-file', 'data-cm-grid', 'data-cm-mode-say']) assert.ok(r.text.includes(marker), marker);
assert.ok(r.text.includes('Sign in'));
assert.ok(r.text.includes('Sign in'));
});
});