feat: redesign

Signed-off-by: Dmytro Stanchiev <git@dmytros.dev>
This commit is contained in:
2026-04-21 20:23:15 -04:00
parent 420a971ff7
commit 915e0b7cf8
21 changed files with 1401 additions and 537 deletions

View File

@@ -1,4 +1,5 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { getAiDisabledMessage } from "@/lib/ai-feature-flags";
import { cn } from "@/lib/utils";
@@ -50,6 +51,8 @@ const DESTRUCTIVE_ACTION_CLASSES =
/** Event count badge: auto-positioned to far right via ml-auto */
const BADGE_POSITION_CLASS = "ml-auto";
const readToolbarSource = () => readFileSync("src/components/ai-toolbar.tsx", "utf8");
// ─── Cycle 1: AI zone visual accent ─────────────────────────────────────────
describe("AI zone primary accent ring contract", () => {
@@ -155,6 +158,20 @@ describe("Event count badge positioning contract", () => {
});
});
describe("AI capture redesign", () => {
test("desktop composer treats prompt and attachments as peer panels", () => {
const source = readToolbarSource();
expect(source).toContain("lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]");
});
test("attachments panel is a first-class surfaced region, not an inline footer affordance", () => {
const source = readToolbarSource();
expect(source).toContain("rounded-[10px] bg-card p-3 shadow-[0_0_0_1px_rgba(0,0,0,0.08)]");
});
});
// ─── Cycle 6: Composer footer bar ────────────────────────────────────────────
//
// Below the textarea sits a single horizontal footer row:

View File

@@ -41,4 +41,25 @@ describe("EventCard actions trigger", () => {
expect(markup).toContain("10:0011:00");
expect(markup).not.toContain("AM");
});
test("renders inline warning copy for invalid event ranges", () => {
const markup = renderToStaticMarkup(
EventCard({
event: {
id: "evt_invalid",
title: "Broken Event",
start: "2026-04-09T11:00:00+00:00",
end: "2026-04-09T10:00:00+00:00",
allDay: false,
},
onEdit: () => {},
onDelete: () => {},
}),
);
expect(markup).toContain("Warning:");
expect(markup).toContain("end time is before start time");
expect(markup).not.toContain("Preview");
expect(markup).not.toContain("Ship");
});
});

View File

@@ -1,4 +1,5 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { getEventFormValuesFromEvent } from "@/lib/event-form";
describe("EventDialog public modes", () => {
@@ -13,4 +14,20 @@ describe("EventDialog public modes", () => {
expect(initialValues.start).toBe("2026-04-09T10:00:00.000Z");
expect(initialValues.recurrenceRule).toBe("FREQ=WEEKLY;INTERVAL=1;BYDAY=TH");
});
test("dialog content uses console surface classes instead of generic border and shadow", () => {
const source = readFileSync("src/components/ui/dialog.tsx", "utf8");
expect(source).toContain("rounded-[10px]");
expect(source).toContain("shadow-[0_0_0_1px_rgba(0,0,0,0.08)");
expect(source).not.toContain("rounded-lg border p-6 shadow-lg");
});
test("dialog source uses console section labels and grouped field regions", () => {
const source = readFileSync("src/components/event-dialog.tsx", "utf8");
expect(source).toContain("Event details");
expect(source).toContain("Schedule");
expect(source).toContain("Recurrence");
});
});

View File

@@ -0,0 +1,29 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
describe("home page hierarchy", () => {
test("desktop layout defines aligned AI and timeline top-row sections", () => {
const source = readFileSync("src/app/page.tsx", "utf8");
expect(source).toContain(
"lg:grid-cols-[minmax(0,0.75fr)_minmax(0,1.25fr)]",
);
expect(source).toContain("AI capture");
expect(source).toContain("Event timeline");
});
test("manual create is routed through a More menu instead of a primary mobile action", () => {
const source = readFileSync("src/app/page.tsx", "utf8");
expect(source).toContain("More");
expect(source).not.toContain("New Event</button>");
});
test("mobile layout keeps capture before timeline and keeps manual create secondary", () => {
const source = readFileSync("src/app/page.tsx", "utf8");
expect(source).toContain("order-1 lg:order-none");
expect(source).toContain("Import");
expect(source).toContain("Manual create");
});
});

View File

@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import {
APP_ACTION_BAR_CLASSES,
APP_HEADER_SURFACE_CLASSES,
APP_NAV_SURFACE_CLASSES,
APP_SECTION_SURFACE_CLASSES,
@@ -8,31 +9,36 @@ import {
import { EVENT_CARD_SURFACE_CLASSES } from "@/components/event-card";
describe("app shell surfaces", () => {
test("header, primary sections, and bottom navigation all use shared glass utilities", () => {
expect(APP_HEADER_SURFACE_CLASSES).toMatch(/glass-surface/);
expect(APP_SECTION_SURFACE_CLASSES).toMatch(/glass-panel/);
expect(APP_NAV_SURFACE_CLASSES).toMatch(/glass-surface/);
test("header surface is a thin structural bar instead of a glass panel", () => {
expect(APP_HEADER_SURFACE_CLASSES).toContain("min-h-14");
expect(APP_HEADER_SURFACE_CLASSES).toContain("border-b");
expect(APP_HEADER_SURFACE_CLASSES).not.toContain("glass-surface");
});
test("section surface keeps responsive padding for mobile and larger breakpoints", () => {
expect(APP_SECTION_SURFACE_CLASSES).toContain("p-4");
expect(APP_SECTION_SURFACE_CLASSES).toContain("sm:p-5");
test("section and action surfaces use tokenized shell classes instead of glass helpers", () => {
expect(APP_SECTION_SURFACE_CLASSES).not.toContain("glass-panel");
expect(APP_ACTION_BAR_CLASSES).not.toContain("glass-subtle");
expect(APP_NAV_SURFACE_CLASSES).not.toContain("glass-surface");
});
});
describe("event cards", () => {
test("event cards use the shared glass card treatment instead of a one-off surface", () => {
expect(EVENT_CARD_SURFACE_CLASSES).toMatch(/glass-card/);
test("event cards use the redesigned console card treatment", () => {
expect(EVENT_CARD_SURFACE_CLASSES).toContain("rounded-[10px]");
expect(EVENT_CARD_SURFACE_CLASSES).toContain(
"shadow-[0_0_0_1px_rgba(0,0,0,0.08)",
);
});
});
describe("connection badge", () => {
test("online-ready badge gets a success treatment while offline stays neutral", () => {
test("online-ready badge uses the console utility blue while offline stays neutral", () => {
const onlineClasses = getConnectionBadgeClasses(true);
const offlineClasses = getConnectionBadgeClasses(false);
expect(onlineClasses).toMatch(/emerald/);
expect(offlineClasses).not.toMatch(/emerald/);
expect(onlineClasses).toContain("bg-[#ebf5ff]");
expect(onlineClasses).toContain("text-[#0068d6]");
expect(offlineClasses).not.toContain("bg-[#ebf5ff]");
expect(offlineClasses).toContain("text-muted-foreground");
});
});