feat: adicionar browser e fallback do notebook
This commit is contained in:
1 parent
c66ff7fde6
commit
b4a2b81d55
5 files changed
+757
No files matched your search
@@ -0,0 +1,112 @@
|
|||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { portfolio } from "@/lib/portfolio-data";
|
||||||
|
import NotebookBrowser, { type NotebookBrowserProps } from "./NotebookBrowser";
|
||||||
|
import { selectNotebookProjects } from "./notebookProjects";
|
||||||
|
|
||||||
|
const projects = selectNotebookProjects(portfolio.projects);
|
||||||
|
const frame: NotebookBrowserProps["frame"] = {
|
||||||
|
cursorVisible: true,
|
||||||
|
cursorX: 0.4,
|
||||||
|
cursorY: 0.6,
|
||||||
|
cursorClicking: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderBrowser(overrides: Partial<NotebookBrowserProps> = {}) {
|
||||||
|
return render(
|
||||||
|
<NotebookBrowser
|
||||||
|
projects={projects}
|
||||||
|
activeIndex={0}
|
||||||
|
frame={frame}
|
||||||
|
onSelectTab={jest.fn()}
|
||||||
|
{...overrides}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("NotebookBrowser", () => {
|
||||||
|
it("renders the active project as an accessible tabpanel", () => {
|
||||||
|
renderBrowser({ activeIndex: 1 });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByRole("tablist", { name: /projetos em produção/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByRole("tab")[1]).toHaveAttribute(
|
||||||
|
"aria-selected",
|
||||||
|
"true",
|
||||||
|
);
|
||||||
|
expect(screen.getByRole("tabpanel")).toHaveAccessibleName(
|
||||||
|
/SC Plus Aquicultura/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moves between tabs with arrow keys and Home/End", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onSelectTab = jest.fn();
|
||||||
|
renderBrowser({ onSelectTab });
|
||||||
|
const tabs = screen.getAllByRole("tab");
|
||||||
|
|
||||||
|
await user.click(tabs[1]);
|
||||||
|
await user.keyboard("{ArrowRight}");
|
||||||
|
await user.keyboard("{Home}");
|
||||||
|
await user.keyboard("{End}");
|
||||||
|
|
||||||
|
expect(onSelectTab).toHaveBeenNthCalledWith(1, 1);
|
||||||
|
expect(onSelectTab).toHaveBeenNthCalledWith(2, 2);
|
||||||
|
expect(onSelectTab).toHaveBeenNthCalledWith(3, 0);
|
||||||
|
expect(onSelectTab).toHaveBeenNthCalledWith(4, projects.length - 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("bounds arrow navigation at the first and last tab", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onSelectTab = jest.fn();
|
||||||
|
renderBrowser({ onSelectTab });
|
||||||
|
const tabs = screen.getAllByRole("tab");
|
||||||
|
|
||||||
|
await user.click(tabs[0]);
|
||||||
|
await user.keyboard("{ArrowLeft}");
|
||||||
|
await user.click(tabs.at(-1) as HTMLElement);
|
||||||
|
await user.keyboard("{ArrowRight}");
|
||||||
|
|
||||||
|
expect(onSelectTab).toHaveBeenNthCalledWith(2, 0);
|
||||||
|
expect(onSelectTab).toHaveBeenNthCalledWith(4, projects.length - 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exposes a real deployment link with safe new-tab attributes", () => {
|
||||||
|
renderBrowser();
|
||||||
|
|
||||||
|
const link = screen.getByRole("link", { name: /abrir projeto/i });
|
||||||
|
expect(link).toHaveAttribute("href", projects[0].demoUrl);
|
||||||
|
expect(link).toHaveAttribute("target", "_blank");
|
||||||
|
expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the animated cursor decorative and never embeds an iframe", () => {
|
||||||
|
const { container } = renderBrowser({
|
||||||
|
frame: { ...frame, cursorClicking: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.querySelector(".notebook-cursor")).toHaveAttribute(
|
||||||
|
"aria-hidden",
|
||||||
|
"true",
|
||||||
|
);
|
||||||
|
expect(container.querySelector(".notebook-cursor")).toHaveStyle({
|
||||||
|
left: "40%",
|
||||||
|
top: "60%",
|
||||||
|
});
|
||||||
|
expect(container.querySelector("iframe")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the selected tab to label the panel and screenshot", () => {
|
||||||
|
renderBrowser({ activeIndex: 2 });
|
||||||
|
|
||||||
|
const tab = screen.getAllByRole("tab")[2];
|
||||||
|
const panel = screen.getByRole("tabpanel");
|
||||||
|
const image = screen.getByRole("img");
|
||||||
|
|
||||||
|
expect(tab).toHaveAttribute("aria-controls", panel.id);
|
||||||
|
expect(panel).toHaveAttribute("aria-labelledby", tab.id);
|
||||||
|
expect(image).toHaveAccessibleName(/SEV Moda Íntima/i);
|
||||||
|
expect(image).toHaveAttribute("src", "/assets/prints/print-sev.png");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import type { NotebookProject } from "./notebookProjects";
|
||||||
|
import type { NotebookFrame } from "./notebookTimeline";
|
||||||
|
|
||||||
|
export type NotebookBrowserProps = {
|
||||||
|
projects: NotebookProject[];
|
||||||
|
activeIndex: number;
|
||||||
|
frame: Pick<
|
||||||
|
NotebookFrame,
|
||||||
|
"cursorVisible" | "cursorX" | "cursorY" | "cursorClicking"
|
||||||
|
>;
|
||||||
|
onSelectTab: (index: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function clampIndex(index: number, length: number): number {
|
||||||
|
if (length < 1) return -1;
|
||||||
|
return Math.min(Math.max(index, 0), length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toDomId(value: string): string {
|
||||||
|
return value.replace(/[^a-zA-Z0-9_-]/g, "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NotebookBrowser({
|
||||||
|
projects,
|
||||||
|
activeIndex,
|
||||||
|
frame,
|
||||||
|
onSelectTab,
|
||||||
|
}: NotebookBrowserProps) {
|
||||||
|
const selectedIndex = clampIndex(activeIndex, projects.length);
|
||||||
|
const selectedProject =
|
||||||
|
selectedIndex >= 0 ? projects[selectedIndex] : undefined;
|
||||||
|
|
||||||
|
function handleTabKeyDown(
|
||||||
|
event: React.KeyboardEvent<HTMLButtonElement>,
|
||||||
|
index: number,
|
||||||
|
) {
|
||||||
|
if (projects.length < 1) return;
|
||||||
|
|
||||||
|
let nextIndex: number | null = null;
|
||||||
|
if (event.key === "ArrowRight" || event.key === "ArrowDown") {
|
||||||
|
nextIndex = Math.min(index + 1, projects.length - 1);
|
||||||
|
} else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
|
||||||
|
nextIndex = Math.max(index - 1, 0);
|
||||||
|
} else if (event.key === "Home") {
|
||||||
|
nextIndex = 0;
|
||||||
|
} else if (event.key === "End") {
|
||||||
|
nextIndex = projects.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextIndex === null) return;
|
||||||
|
event.preventDefault();
|
||||||
|
onSelectTab(nextIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedProject) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="notebook-browser"
|
||||||
|
data-testid="notebook-browser"
|
||||||
|
data-empty
|
||||||
|
>
|
||||||
|
<p aria-live="polite">Nenhum projeto disponível para esta cena.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedId = toDomId(selectedProject.id);
|
||||||
|
const panelId = `notebook-panel-${selectedId}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="notebook-browser" data-testid="notebook-browser">
|
||||||
|
<div className="notebook-browser-bar" aria-hidden="true">
|
||||||
|
<span className="notebook-browser-dot" />
|
||||||
|
<span className="notebook-browser-dot" />
|
||||||
|
<span className="notebook-browser-dot" />
|
||||||
|
<span className="notebook-browser-address">
|
||||||
|
evandro.dev.br / projetos
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="notebook-browser-body">
|
||||||
|
<div className="notebook-browser-heading">
|
||||||
|
<span className="notebook-browser-eyebrow">Projetos em produção</span>
|
||||||
|
<span className="notebook-browser-counter">
|
||||||
|
{String(selectedIndex + 1).padStart(2, "0")} /{" "}
|
||||||
|
{String(projects.length).padStart(2, "0")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="notebook-tablist"
|
||||||
|
role="tablist"
|
||||||
|
aria-label="Projetos em produção"
|
||||||
|
>
|
||||||
|
{projects.map((project, index) => {
|
||||||
|
const tabId = `notebook-tab-${toDomId(project.id)}`;
|
||||||
|
const isSelected = index === selectedIndex;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
aria-controls={panelId}
|
||||||
|
aria-selected={isSelected}
|
||||||
|
className="notebook-tab"
|
||||||
|
id={tabId}
|
||||||
|
key={project.id}
|
||||||
|
onClick={() => onSelectTab(index)}
|
||||||
|
onKeyDown={(event) => handleTabKeyDown(event, index)}
|
||||||
|
role="tab"
|
||||||
|
tabIndex={isSelected ? 0 : -1}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className="notebook-tab-number">
|
||||||
|
{String(index + 1).padStart(2, "0")}
|
||||||
|
</span>
|
||||||
|
<span className="notebook-tab-title">{project.title}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
aria-labelledby={`notebook-tab-${selectedId}`}
|
||||||
|
className="notebook-panel"
|
||||||
|
id={panelId}
|
||||||
|
role="tabpanel"
|
||||||
|
>
|
||||||
|
<div className="notebook-panel-copy">
|
||||||
|
<h3>{selectedProject.title}</h3>
|
||||||
|
<p>{selectedProject.subtitle}</p>
|
||||||
|
<ul aria-label={`Tecnologias de ${selectedProject.title}`}>
|
||||||
|
{selectedProject.technologies.slice(0, 4).map((technology) => (
|
||||||
|
<li key={technology}>{technology}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<a
|
||||||
|
href={selectedProject.demoUrl}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Abrir projeto <span aria-hidden="true">↗</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="notebook-screenshot-wrap">
|
||||||
|
<Image
|
||||||
|
alt={`Captura de tela de ${selectedProject.title}`}
|
||||||
|
className="notebook-screenshot"
|
||||||
|
fill
|
||||||
|
sizes="(min-width: 681px) 60vw, 100vw"
|
||||||
|
src={selectedProject.screenshotSrc}
|
||||||
|
unoptimized
|
||||||
|
/>
|
||||||
|
{frame.cursorVisible ? (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`notebook-cursor${frame.cursorClicking ? " is-clicking" : ""}`}
|
||||||
|
data-testid="notebook-cursor"
|
||||||
|
style={{
|
||||||
|
left: `${frame.cursorX * 100}%`,
|
||||||
|
top: `${frame.cursorY * 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { portfolio } from "@/lib/portfolio-data";
|
||||||
|
import NotebookFallback from "./NotebookFallback";
|
||||||
|
import { selectNotebookProjects } from "./notebookProjects";
|
||||||
|
|
||||||
|
const projects = selectNotebookProjects(portfolio.projects);
|
||||||
|
|
||||||
|
describe("NotebookFallback", () => {
|
||||||
|
it("keeps project content and controls in normal document flow", () => {
|
||||||
|
const { container } = render(<NotebookFallback projects={projects} />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId("notebook-fallback")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("tablist")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("tabpanel")).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole("link", { name: /abrir projeto/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(container.querySelector("canvas")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows manual tab changes without an animated stage", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const { container } = render(<NotebookFallback projects={projects} />);
|
||||||
|
|
||||||
|
await user.click(screen.getAllByRole("tab")[2]);
|
||||||
|
|
||||||
|
expect(screen.getByRole("tabpanel")).toHaveAccessibleName(
|
||||||
|
/SEV Moda Íntima/i,
|
||||||
|
);
|
||||||
|
expect(container.querySelector("canvas")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects the requested initial project", () => {
|
||||||
|
render(<NotebookFallback initialIndex={3} projects={projects} />);
|
||||||
|
|
||||||
|
expect(screen.getByRole("tabpanel")).toHaveAccessibleName(/AS Serviços/i);
|
||||||
|
expect(screen.getAllByRole("tab")[3]).toHaveAttribute(
|
||||||
|
"aria-selected",
|
||||||
|
"true",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import NotebookBrowser from "./NotebookBrowser";
|
||||||
|
import type { NotebookProject } from "./notebookProjects";
|
||||||
|
|
||||||
|
export type NotebookFallbackProps = {
|
||||||
|
projects: NotebookProject[];
|
||||||
|
initialIndex?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function clampIndex(index: number, length: number): number {
|
||||||
|
if (length < 1) return -1;
|
||||||
|
return Math.min(Math.max(index, 0), length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NotebookFallback({
|
||||||
|
projects,
|
||||||
|
initialIndex = 0,
|
||||||
|
}: NotebookFallbackProps) {
|
||||||
|
const [activeIndex, setActiveIndex] = useState(() =>
|
||||||
|
clampIndex(initialIndex, projects.length),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="notebook-fallback" data-testid="notebook-fallback">
|
||||||
|
<div className="notebook-fallback-intro">
|
||||||
|
<span className="notebook-fallback-kicker">Interface em produção</span>
|
||||||
|
<p>
|
||||||
|
Navegue pelos projetos com as abas abaixo. A experiência permanece
|
||||||
|
completa sem movimento ou WebGL.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<NotebookBrowser
|
||||||
|
activeIndex={activeIndex}
|
||||||
|
frame={{
|
||||||
|
cursorClicking: false,
|
||||||
|
cursorVisible: false,
|
||||||
|
cursorX: 0,
|
||||||
|
cursorY: 0,
|
||||||
|
}}
|
||||||
|
onSelectTab={setActiveIndex}
|
||||||
|
projects={projects}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+383
@@ -824,3 +824,386 @@ textarea {
|
|||||||
.cinematic-reveal {
|
.cinematic-reveal {
|
||||||
will-change: transform, opacity;
|
will-change: transform, opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Notebook story */
|
||||||
|
.notebook-story {
|
||||||
|
position: relative;
|
||||||
|
overflow: clip;
|
||||||
|
background: var(--ink);
|
||||||
|
color: var(--paper);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-story-runway {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-story-sticky {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
display: grid;
|
||||||
|
min-height: 100dvh;
|
||||||
|
place-items: center;
|
||||||
|
padding: clamp(40px, 7vw, 96px) clamp(20px, 6vw, 80px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-story-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(180px, 0.44fr) minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: clamp(28px, 6vw, 96px);
|
||||||
|
width: min(1240px, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-story-copy {
|
||||||
|
max-width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-story-kicker,
|
||||||
|
.notebook-fallback-kicker,
|
||||||
|
.notebook-browser-eyebrow {
|
||||||
|
color: var(--slash);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-story-copy h2 {
|
||||||
|
margin-top: var(--sp-4);
|
||||||
|
max-width: 8ch;
|
||||||
|
color: var(--white);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: clamp(38px, 5vw, 72px);
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.06em;
|
||||||
|
line-height: 0.94;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-story-copy p {
|
||||||
|
max-width: 32ch;
|
||||||
|
margin-top: var(--sp-6);
|
||||||
|
color: rgba(244, 242, 237, 0.64);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-story-stage {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: min(62vw, 680px);
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-story-canvas {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-fallback {
|
||||||
|
width: min(1080px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: clamp(48px, 9vw, 112px) clamp(20px, 6vw, 80px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-fallback-intro {
|
||||||
|
width: min(560px, 100%);
|
||||||
|
margin-bottom: var(--sp-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-fallback-intro p {
|
||||||
|
margin-top: var(--sp-3);
|
||||||
|
color: rgba(244, 242, 237, 0.68);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-browser {
|
||||||
|
position: relative;
|
||||||
|
width: min(100%, 940px);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(244, 242, 237, 0.16);
|
||||||
|
background: #101313;
|
||||||
|
color: var(--paper);
|
||||||
|
box-shadow: 0 26px 80px rgba(0, 0, 0, 0.34);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-browser-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0 var(--sp-4);
|
||||||
|
border-bottom: 1px solid rgba(244, 242, 237, 0.1);
|
||||||
|
background: #181d1d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-browser-dot {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(244, 242, 237, 0.34);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-browser-dot:first-child {
|
||||||
|
background: #e46c4d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-browser-dot:nth-child(2) {
|
||||||
|
background: #d6aa57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-browser-dot:nth-child(3) {
|
||||||
|
background: #7cbd67;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-browser-address {
|
||||||
|
margin-left: var(--sp-3);
|
||||||
|
color: rgba(244, 242, 237, 0.42);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-browser-body {
|
||||||
|
padding: clamp(20px, 3vw, 32px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-browser-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--sp-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-browser-counter {
|
||||||
|
color: rgba(244, 242, 237, 0.44);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-tablist {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 1px;
|
||||||
|
margin-top: var(--sp-5);
|
||||||
|
border-block: 1px solid rgba(244, 242, 237, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-tab {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
padding: 14px 10px;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(244, 242, 237, 0.48);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition:
|
||||||
|
color 180ms ease,
|
||||||
|
border-color 180ms ease,
|
||||||
|
background-color 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-tab:hover,
|
||||||
|
.notebook-tab[aria-selected="true"] {
|
||||||
|
border-bottom-color: var(--slash);
|
||||||
|
background: rgba(204, 255, 0, 0.06);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-tab-number {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--slash);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-tab-title {
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-panel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(140px, 0.35fr) minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: clamp(20px, 3vw, 36px);
|
||||||
|
margin-top: var(--sp-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-panel-copy h3 {
|
||||||
|
color: var(--white);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: clamp(22px, 3vw, 36px);
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-panel-copy > p {
|
||||||
|
margin-top: var(--sp-2);
|
||||||
|
color: rgba(244, 242, 237, 0.6);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-panel-copy ul {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: var(--sp-4);
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-panel-copy li {
|
||||||
|
padding: 4px 7px;
|
||||||
|
border: 1px solid rgba(244, 242, 237, 0.15);
|
||||||
|
color: rgba(244, 242, 237, 0.7);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-panel-copy a {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: var(--sp-5);
|
||||||
|
color: var(--slash);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-panel-copy a:hover {
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-screenshot-wrap {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
border: 1px solid rgba(244, 242, 237, 0.16);
|
||||||
|
background: var(--paper-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-screenshot-wrap img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-cursor {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
width: 15px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--white);
|
||||||
|
background: var(--ink);
|
||||||
|
box-shadow: 2px 2px 0 var(--slash);
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translate(-2px, -2px) rotate(-18deg);
|
||||||
|
transition:
|
||||||
|
width 120ms ease,
|
||||||
|
height 120ms ease,
|
||||||
|
box-shadow 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-cursor.is-clicking {
|
||||||
|
width: 12px;
|
||||||
|
height: 16px;
|
||||||
|
box-shadow: 1px 1px 0 var(--slash);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.notebook-story-runway {
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-story-sticky {
|
||||||
|
position: relative;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-story-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-story-copy {
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-story-copy h2 {
|
||||||
|
max-width: 12ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-story-stage {
|
||||||
|
min-height: min(78vw, 620px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.notebook-story-sticky {
|
||||||
|
padding-inline: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-story-copy h2 {
|
||||||
|
font-size: clamp(38px, 14vw, 64px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-story-stage {
|
||||||
|
min-height: 84vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-fallback {
|
||||||
|
padding-inline: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-browser-body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-tablist {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-tab {
|
||||||
|
padding: 12px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-tab-title {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-panel {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notebook-panel-copy {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.notebook-tab,
|
||||||
|
.notebook-cursor {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user