Kiosk drive filter and wider picker
This commit is contained in:
@@ -58,12 +58,27 @@
|
||||
</div>
|
||||
|
||||
<div class="drive-group">
|
||||
<h2>Available</h2>
|
||||
<div class="drive-group-head">
|
||||
<h2>Available</h2>
|
||||
<!-- Typed, not tapped: this panel has a keyboard, and a cabinet of
|
||||
thirty drives is faster to narrow than to read. -->
|
||||
<div v-if="session.available.length" class="drive-filter">
|
||||
<input ref="filterInput" v-model="drivefilter"
|
||||
class="drive-filter-input"
|
||||
autocomplete="off" spellcheck="false"
|
||||
placeholder="Find a drive, a locker..." />
|
||||
<button v-if="drivefilter" class="drive-filter-clear"
|
||||
title="Clear" @click="drivefilter = ''">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!session.available.length" class="kiosk-hint">
|
||||
No drives are free right now.
|
||||
</p>
|
||||
<p v-else-if="!availabledrives.length" class="kiosk-hint">
|
||||
No free drive matches "{{ drivefilter }}".
|
||||
</p>
|
||||
<div v-else class="drive-list">
|
||||
<button v-for="drive in session.available" :key="drive.device_id"
|
||||
<button v-for="drive in availabledrives" :key="drive.device_id"
|
||||
class="drive-row" :disabled="busy"
|
||||
@click="act(drive, 'checkout')">
|
||||
<span class="drive-name">
|
||||
@@ -117,9 +132,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import api, { settingsApi } from '@/api'
|
||||
import TouchKeypad from '@/components/TouchKeypad.vue'
|
||||
import { filterdrives } from '../drivefilter'
|
||||
|
||||
const step = ref('badge')
|
||||
const badge = ref('')
|
||||
@@ -155,6 +171,20 @@ function focusEntry (event) {
|
||||
nextTick(() => entryInput.value?.focus())
|
||||
}
|
||||
|
||||
// Narrowing the free drives. The matcher lives in its own module so it can be
|
||||
// tested without mounting a kiosk.
|
||||
const drivefilter = ref('')
|
||||
const filterInput = ref(null)
|
||||
|
||||
// Focused as the picker opens, so somebody can simply start typing. The badge
|
||||
// step owns the keyboard until then.
|
||||
function focusFilter () {
|
||||
nextTick(() => filterInput.value?.focus())
|
||||
}
|
||||
|
||||
const availabledrives = computed(
|
||||
() => filterdrives(session.value.available, drivefilter.value))
|
||||
|
||||
// Drive ids sort as TEXT everywhere else, which puts -10 before -3 and reads as
|
||||
// a jumble on a wall panel. Compare the numeric runs numerically; falls back to
|
||||
// plain text for ids that carry no digits.
|
||||
@@ -181,6 +211,7 @@ async function startSession (value) {
|
||||
badge.value = scanned
|
||||
manualBadge.value = ''
|
||||
step.value = 'pick'
|
||||
focusFilter()
|
||||
} catch (sessionError) {
|
||||
error.value = messageFrom(sessionError, 'Could not look that badge up')
|
||||
} finally {
|
||||
@@ -247,6 +278,9 @@ function reset () {
|
||||
error.value = ''
|
||||
doneMessage.value = ''
|
||||
manualBadge.value = ''
|
||||
// The next person at the cabinet must not inherit somebody else's filter and
|
||||
// conclude there are no drives left.
|
||||
drivefilter.value = ''
|
||||
focusEntry()
|
||||
}
|
||||
</script>
|
||||
@@ -273,7 +307,7 @@ function reset () {
|
||||
}
|
||||
.kiosk-header {
|
||||
width: 100%;
|
||||
max-width: 40rem;
|
||||
max-width: 72rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -297,8 +331,11 @@ function reset () {
|
||||
max-width: 40rem;
|
||||
width: 100%;
|
||||
}
|
||||
/* The picker holds lists, so its rows stretch - but it keeps the same width as
|
||||
every other step so the panel does not jump when the step changes.
|
||||
/* The picker holds lists, so its rows stretch, and it is WIDER than the steps
|
||||
that hold a single control - a cabinet of thirty drives is three or four
|
||||
columns of one glance rather than a scroll. The steps that are just a keypad
|
||||
or a message stay narrow, because stretching one input across a 24 inch panel
|
||||
reads as a broken page.
|
||||
It is also height-bounded: the lists absorb whatever space is left over and
|
||||
scroll inside themselves, so the PAGE never scrolls and the heading saying
|
||||
whose drives these are stays on screen. A fixed max-height cannot do this -
|
||||
@@ -306,6 +343,7 @@ function reset () {
|
||||
present, which is exactly what it did. */
|
||||
.kiosk-wide {
|
||||
align-items: stretch;
|
||||
max-width: 72rem;
|
||||
max-height: calc(100vh - 9rem);
|
||||
min-height: 0;
|
||||
}
|
||||
@@ -321,7 +359,7 @@ function reset () {
|
||||
.kiosk-wide .drive-group-yours .drive-list { max-height: 30vh; }
|
||||
.kiosk-prompt { font-size: 1.5rem; text-align: center; margin: 0; }
|
||||
.kiosk-greeting { font-size: 1.5rem; margin: 0; text-align: center; }
|
||||
.kiosk-hint { color: var(--textmuted); text-align: center; margin: 0; }
|
||||
.kiosk-hint { color: var(--text-light); text-align: center; margin: 0; }
|
||||
.kiosk-error {
|
||||
padding: 0.9rem 1.1rem;
|
||||
border: 1px solid var(--danger, #b3261e);
|
||||
@@ -335,12 +373,47 @@ function reset () {
|
||||
|
||||
.drive-group { width: 100%; }
|
||||
.drive-group h2 { font-size: 1.2rem; margin: 0 0 0.5rem; }
|
||||
/* Two columns once there is room, so a cabinet of a dozen drives is one glance
|
||||
rather than a scroll. The list itself scrolls when it has to - the PAGE must
|
||||
not, or the person loses the heading telling them whose drives these are. */
|
||||
/* Heading and filter on one line: the filter belongs to the list under it, and
|
||||
a row of its own would cost height the lists need. */
|
||||
.drive-group-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.drive-filter { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.drive-filter-input {
|
||||
/* Touch-sized like everything else here, and wide enough for a locker name
|
||||
rather than just a drive number. */
|
||||
min-width: 16rem;
|
||||
padding: 0.6rem 0.8rem;
|
||||
font-size: 1.1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-card-solid);
|
||||
color: var(--text);
|
||||
}
|
||||
.drive-filter-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.drive-filter-clear {
|
||||
padding: 0.6rem 0.9rem;
|
||||
font-size: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
/* As many columns as the panel has room for - four on a wall display, one on a
|
||||
small screen - so a cabinet of a dozen drives is one glance rather than a
|
||||
scroll. The list itself scrolls when it has to; the PAGE must not, or the
|
||||
person loses the heading telling them whose drives these are. */
|
||||
.drive-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
||||
gap: 0.6rem;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
@@ -364,7 +437,7 @@ function reset () {
|
||||
}
|
||||
.drive-row:disabled { opacity: 0.5; }
|
||||
.drive-name { display: flex; flex-direction: column; gap: 0.15rem; }
|
||||
.drive-name small { color: var(--textmuted); font-size: 0.85rem; }
|
||||
.drive-name small { color: var(--text-light); font-size: 0.85rem; }
|
||||
.drive-action {
|
||||
flex: 0 0 auto;
|
||||
padding: 0.5rem 1.1rem;
|
||||
|
||||
Reference in New Issue
Block a user