Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/desktop/dist/assets/index-BfOOoQLP.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions apps/desktop/dist/assets/index-BlDh6L6x.css

Large diffs are not rendered by default.

9 changes: 0 additions & 9 deletions apps/desktop/dist/assets/index-DUjhv9cr.js

This file was deleted.

1 change: 0 additions & 1 deletion apps/desktop/dist/assets/index-hTIyNl5e.css

This file was deleted.

4 changes: 2 additions & 2 deletions apps/desktop/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Instrument+Serif:ital@0;1&display=swap"
/>
<script type="module" crossorigin src="/assets/index-DUjhv9cr.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-hTIyNl5e.css">
<script type="module" crossorigin src="/assets/index-BfOOoQLP.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BlDh6L6x.css">
</head>
<body>
<div id="root"></div>
Expand Down
27 changes: 22 additions & 5 deletions apps/desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@ describe("Stay GUI", () => {
render(<App client={client} />);

const pinInput = await screen.findByLabelText("Set a four digit PIN");
const keepButton = screen.getByRole("button", { name: "Keep" });

expect(keepButton).toBeDisabled();
expect(pinInput.closest(".pin-code-input")?.querySelectorAll(".pin-code-slot")).toHaveLength(4);

await user.type(pinInput, "12a345");

expect(pinInput).toHaveValue("1234");
expect(keepButton).toBeEnabled();

await user.click(screen.getByRole("button", { name: "Keep" }));
await user.click(keepButton);

expect(client.view()).toMatchObject({ mode: "idle", pin_configured: true });
expect(screen.getByText("Stay is waiting.")).toBeInTheDocument();
Expand Down Expand Up @@ -70,8 +76,14 @@ describe("Stay GUI", () => {
render(<App client={client} />);

const unlockInput = await screen.findByLabelText("Unlock PIN");
const openButton = screen.getByRole("button", { name: "Open" });

expect(openButton).toBeDisabled();
expect(unlockInput.closest(".pin-code-input")?.querySelectorAll(".pin-code-slot")).toHaveLength(4);

await user.type(unlockInput, "48x21");
expect(unlockInput).toHaveValue("4821");
expect(openButton).toBeEnabled();

await user.keyboard("{Enter}");

Expand All @@ -94,7 +106,7 @@ describe("Stay GUI", () => {
expect(await screen.findByText("Stay with this meeting?")).toBeInTheDocument();
});

it("renders setup command errors inline without changing mode", async () => {
it("waits for a complete setup PIN instead of submitting invalid input", async () => {
const user = userEvent.setup();
const client = createMockStayClient();
client.focusMeeting();
Expand All @@ -104,10 +116,15 @@ describe("Stay GUI", () => {
expect(await screen.findByText("Stay with this meeting?")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Stay" })).toBeDisabled();

await user.type(screen.getByLabelText("Set a four digit PIN"), "12");
await user.click(screen.getByRole("button", { name: "Keep" }));
const pinInput = screen.getByLabelText("Set a four digit PIN");
const keepButton = screen.getByRole("button", { name: "Keep" });

await user.type(pinInput, "12");
expect(keepButton).toBeDisabled();
await user.click(keepButton);

expect(await screen.findByText("PIN must be exactly four digits")).toBeInTheDocument();
expect(client.commandLog).not.toContain("set_pin");
expect(screen.queryByText("PIN must be exactly four digits")).not.toBeInTheDocument();
expect(screen.getByText("Stay with this meeting?")).toBeInTheDocument();
});
});
200 changes: 200 additions & 0 deletions apps/desktop/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,134 @@ input[aria-invalid="true"] {
border-color: rgba(168, 58, 45, 0.5);
}

.pin-code-field {
display: grid;
min-width: 0;
gap: 9px;
}

.pin-code-field label {
margin-bottom: 0;
}

.pin-code-input {
position: relative;
width: 212px;
max-width: 100%;
height: 46px;
-webkit-app-region: no-drag;
cursor: text;
}

.pin-code-control {
position: absolute;
inset: 0;
z-index: 2;
width: 100%;
height: 100%;
padding: 0;
border: 0;
border-radius: var(--radius-control);
color: transparent;
caret-color: transparent;
background: transparent;
box-shadow: none;
letter-spacing: 0;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}

.pin-code-control:focus {
border: 0;
background: transparent;
box-shadow: none;
}

.pin-code-control:focus-visible {
outline: none;
}

.pin-code-slots {
display: grid;
grid-template-columns: repeat(4, 44px);
justify-content: start;
gap: 12px;
width: 100%;
height: 100%;
}

.pin-code-slot {
display: grid;
min-width: 0;
place-items: center;
aspect-ratio: 1;
border: 0.5px solid rgba(26, 28, 31, 0.13);
border-radius: var(--radius-control);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.32)),
rgba(255, 255, 255, 0.4);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.86) inset,
0 -0.5px 0 rgba(26, 28, 31, 0.05) inset,
0 7px 18px rgba(26, 28, 31, 0.08);
backdrop-filter: blur(20px) saturate(1.5);
-webkit-backdrop-filter: blur(20px) saturate(1.5);
transition:
border-color 160ms ease,
background 200ms ease,
box-shadow 200ms ease,
transform 160ms ease;
}

.pin-code-slot::before {
width: 10px;
height: 10px;
border-radius: 999px;
background: var(--accent-deep);
box-shadow: 0 0 0 3px rgba(23, 109, 101, 0.12);
opacity: 0;
transform: scale(0.72);
content: "";
transition:
opacity 140ms ease,
transform 160ms cubic-bezier(0.2, 0.7, 0.2, 1);
}

.pin-code-slot-filled::before {
opacity: 1;
transform: scale(1);
}

.pin-code-slot-filled {
border-color: rgba(23, 109, 101, 0.28);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.74), rgba(23, 109, 101, 0.12)),
rgba(255, 255, 255, 0.42);
}

.pin-code-input:focus-within .pin-code-slot {
border-color: rgba(23, 109, 101, 0.24);
background: rgba(255, 255, 255, 0.56);
}

.pin-code-input:focus-within .pin-code-slot-active {
border-color: rgba(23, 109, 101, 0.5);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.94) inset,
0 0 0 3px rgba(23, 109, 101, 0.12),
0 7px 18px rgba(26, 28, 31, 0.07);
transform: translateY(-1px);
}

.pin-code-input[data-invalid="true"] .pin-code-slot {
border-color: rgba(168, 58, 45, 0.5);
}

.pin-code-input[data-invalid="true"] .pin-code-slot::before {
background: var(--danger);
box-shadow: 0 0 0 3px rgba(168, 58, 45, 0.12);
}

/* ---------- Shell ---------- */

.stay-shell {
Expand Down Expand Up @@ -631,6 +759,12 @@ label {
align-items: center;
}

.control-row.pin-entry-row {
grid-template-columns: auto auto;
justify-content: start;
align-items: end;
}

.action-row {
grid-template-columns: auto auto;
justify-content: start;
Expand Down Expand Up @@ -774,6 +908,16 @@ label {
input {
height: 34px;
}

.pin-code-input {
width: 188px;
height: 42px;
}

.pin-code-slots {
grid-template-columns: repeat(4, 40px);
gap: 9px;
}
}

@media (max-width: 360px) {
Expand All @@ -788,6 +932,14 @@ label {
grid-template-columns: 1fr;
}

.pin-code-input {
width: 100%;
}

.pin-code-slots {
grid-template-columns: repeat(4, minmax(0, 1fr));
}

.status-pill {
justify-self: start;
}
Expand Down Expand Up @@ -888,6 +1040,49 @@ label {
0 0 0 3px rgba(95, 199, 188, 0.18);
}

.pin-code-control,
.pin-code-control:focus {
color: transparent;
background: transparent;
box-shadow: none;
}

.pin-code-slot {
border-color: rgba(255, 255, 255, 0.26);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.13), rgba(255, 255, 255, 0.05)),
rgba(255, 255, 255, 0.05);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.06) inset,
0 8px 18px rgba(0, 0, 0, 0.28);
}

.pin-code-slot::before {
background: #d7fffb;
box-shadow:
0 0 0 3px rgba(95, 199, 188, 0.18),
0 0 10px rgba(95, 199, 188, 0.45);
}

.pin-code-slot-filled {
border-color: rgba(95, 199, 188, 0.56);
background:
linear-gradient(180deg, rgba(95, 199, 188, 0.22), rgba(95, 199, 188, 0.08)),
rgba(255, 255, 255, 0.06);
}

.pin-code-input:focus-within .pin-code-slot {
border-color: rgba(95, 199, 188, 0.28);
background: rgba(255, 255, 255, 0.08);
}

.pin-code-input:focus-within .pin-code-slot-active {
border-color: rgba(95, 199, 188, 0.5);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.08) inset,
0 0 0 3px rgba(95, 199, 188, 0.18);
}

.status-pill {
color: var(--accent);
background:
Expand Down Expand Up @@ -1043,4 +1238,9 @@ label {
input {
transition: none;
}

.pin-code-slot,
.pin-code-slot::before {
transition: none;
}
}
27 changes: 15 additions & 12 deletions apps/desktop/src/ui/LockScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { FormEvent, useState } from "react";
import type { GuardView } from "../types";
import { windowLabel } from "../types";
import { cleanPinInput } from "./pinInput";
import { PinCodeInput } from "./PinCodeInput";
import { isCompletePin } from "./pinInput";

type LockedView = Extract<GuardView, { mode: "locked" }>;

Expand All @@ -17,6 +18,10 @@ export function LockScreen({ view, error, onSubmit }: LockScreenProps) {

async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!isCompletePin(pin)) {
return;
}

const accepted = await onSubmit(pin);
if (accepted) {
setPin("");
Expand All @@ -32,19 +37,17 @@ export function LockScreen({ view, error, onSubmit }: LockScreenProps) {
{view.focused.title ? `: ${windowLabel(view.focused)}` : ""}
</p>
<form className="unlock-form" noValidate onSubmit={handleSubmit}>
<div className="control-row">
<input
aria-label="Unlock PIN"
inputMode="numeric"
maxLength={4}
pattern="[0-9]{4}"
autoComplete="off"
<div className="control-row pin-entry-row">
<PinCodeInput
ariaLabel="Unlock PIN"
value={pin}
aria-invalid={Boolean(visibleError)}
aria-describedby={visibleError ? "unlock-error" : undefined}
onChange={(event) => setPin(cleanPinInput(event.currentTarget.value))}
error={visibleError}
errorId="unlock-error"
onChange={setPin}
/>
<button type="submit">Open</button>
<button type="submit" disabled={!isCompletePin(pin)}>
Open
</button>
</div>
</form>
<p id="unlock-error" className="error-line" role="status">
Expand Down
Loading
Loading