diff --git a/CREDITS.md b/CREDITS.md
index 448532e..a72b3c3 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -1,15 +1,5 @@
# Credits / Third-party assets
-## 3D model — “Laptop”
-- Author: **Kenney** (https://kenney.nl)
-- Source: https://poly.pizza/m/GnbwSUiVty
-- License: **CC0 1.0 Universal (Public Domain)** (https://creativecommons.org/publicdomain/zero/1.0/)
-- Used in: `app/components/story/NotebookStage3D.tsx`,
- file at `public/models/laptop-kenney.glb`.
-- Modifications: the downloaded open-laptop geometry was split into
- `Laptop_Base` and `Laptop_Lid` nodes around the hinge for scroll-driven
- animation. CC0 does not require attribution; credit given anyway.
-
## Project screenshots
- `public/assets/prints/*.png` are captures taken with headless Chrome from
production deployments owned by the site author (evandro.dev.br), used as
diff --git a/app/components/story/NotebookBrowser.test.tsx b/app/components/story/NotebookBrowser.test.tsx
index 2867654..db431ad 100644
--- a/app/components/story/NotebookBrowser.test.tsx
+++ b/app/components/story/NotebookBrowser.test.tsx
@@ -126,18 +126,29 @@ describe("NotebookBrowser", () => {
const { container } = renderBrowser({
frame: { ...frame, cursorClicking: true },
});
+ const cursor = container.querySelector(".notebook-cursor");
- expect(container.querySelector(".notebook-cursor")).toHaveAttribute(
- "aria-hidden",
- "true",
+ expect(cursor).toHaveAttribute("aria-hidden", "true");
+ expect(cursor).toHaveStyle({ left: "40%", top: "60%" });
+ expect(cursor?.closest(".notebook-browser")).toBe(
+ container.querySelector(".notebook-browser"),
);
- expect(container.querySelector(".notebook-cursor")).toHaveStyle({
- left: "40%",
- top: "60%",
- });
expect(container.querySelector("iframe")).not.toBeInTheDocument();
});
+ it("marks only the selected tab as pressed by the choreographed cursor", () => {
+ const { container } = renderBrowser({
+ activeIndex: 1,
+ frame: { ...frame, cursorClicking: true },
+ });
+ const tabs = container.querySelectorAll(".notebook-tab");
+
+ expect(tabs[1]).toHaveAttribute("data-cursor-clicking", "true");
+ expect(tabs[0]).toHaveAttribute("data-cursor-clicking", "false");
+ expect(tabs[2]).toHaveAttribute("data-cursor-clicking", "false");
+ expect(tabs[3]).toHaveAttribute("data-cursor-clicking", "false");
+ });
+
it("uses the selected tab to label the panel and screenshot", () => {
renderBrowser({ activeIndex: 2 });
diff --git a/app/components/story/NotebookBrowser.tsx b/app/components/story/NotebookBrowser.tsx
index d798ad8..3efbc5e 100644
--- a/app/components/story/NotebookBrowser.tsx
+++ b/app/components/story/NotebookBrowser.tsx
@@ -142,6 +142,9 @@ export default function NotebookBrowser({
aria-controls={panelId}
aria-selected={isSelected}
className="notebook-tab"
+ data-cursor-clicking={
+ isSelected && frame.cursorClicking ? "true" : "false"
+ }
id={tabId}
key={project.id}
onClick={() => onSelectTab(index)}
@@ -213,20 +216,20 @@ export default function NotebookBrowser({
unoptimized
/>
) : null}
- {frame.cursorVisible ? (
-
- ) : null}
+ {frame.cursorVisible ? (
+
+ ) : null}
);
}
diff --git a/app/components/story/NotebookStage3D.test.tsx b/app/components/story/NotebookStage3D.test.tsx
index 6b617e2..42678b3 100644
--- a/app/components/story/NotebookStage3D.test.tsx
+++ b/app/components/story/NotebookStage3D.test.tsx
@@ -1,25 +1,25 @@
-import { useGLTF } from "@react-three/drei";
import { render, screen } from "@testing-library/react";
import * as THREE from "three";
import { portfolio } from "@/lib/portfolio-data";
import NotebookStage3D, {
- NOTEBOOK_MODEL_URL,
+ getNotebookLidRotation,
type NotebookStage3DProps,
} from "./NotebookStage3D";
import { selectNotebookProjects } from "./notebookProjects";
+type MockCanvasProps = {
+ children: React.ReactNode;
+ frameloop?: string;
+};
+
+const mockCanvas = jest.fn(({ children, frameloop }: MockCanvasProps) => (
+
+ {children}
+
+));
+
jest.mock("@react-three/fiber", () => ({
- Canvas: ({
- children,
- frameloop,
- }: {
- children: React.ReactNode;
- frameloop: string;
- }) => (
-
- {children}
-
- ),
+ Canvas: (props: MockCanvasProps) => mockCanvas(props),
useFrame: jest.fn(),
}));
@@ -27,10 +27,9 @@ jest.mock("@react-three/drei", () => ({
Html: ({ children }: { children: React.ReactNode }) => (
{children}
),
- useGLTF: jest.fn(() => ({ scene: new THREE.Group() })),
+ RoundedBox: () => ,
}));
-const mockedUseGLTF = jest.mocked(useGLTF);
const projects = selectNotebookProjects(portfolio.projects);
const props: NotebookStage3DProps = {
activeIndex: 0,
@@ -40,25 +39,43 @@ const props: NotebookStage3DProps = {
projects,
};
+describe("getNotebookLidRotation", () => {
+ it("keeps the lid closed at rest and opens around the hinge", () => {
+ expect(getNotebookLidRotation(0)).toBe(0);
+ expect(getNotebookLidRotation(112)).toBeCloseTo(
+ -THREE.MathUtils.degToRad(112),
+ 6,
+ );
+ expect(getNotebookLidRotation(56)).toBeCloseTo(
+ -THREE.MathUtils.degToRad(56),
+ 6,
+ );
+ });
+
+ it("clamps angles outside the physical hinge range", () => {
+ expect(getNotebookLidRotation(-10)).toBe(0);
+ expect(getNotebookLidRotation(200)).toBe(getNotebookLidRotation(180));
+ });
+});
+
describe("NotebookStage3D", () => {
beforeEach(() => {
jest.spyOn(console, "error").mockImplementation(() => undefined);
- mockedUseGLTF.mockImplementation((() => ({
- scene: new THREE.Group(),
- })) as never);
});
afterEach(() => {
jest.restoreAllMocks();
});
-
- it("renders a single stage using the local model and browser surface", () => {
- render();
+ it("renders the procedural laptop and the browser surface", () => {
+ const { container } = render();
expect(screen.getByTestId("notebook-stage")).toBeInTheDocument();
expect(screen.getByTestId("mock-canvas")).toBeInTheDocument();
expect(screen.getByTestId("notebook-browser")).toBeInTheDocument();
- expect(mockedUseGLTF).toHaveBeenCalledWith(NOTEBOOK_MODEL_URL);
+ expect(
+ screen.getAllByTestId("mock-roundedbox").length,
+ ).toBeGreaterThanOrEqual(5);
+ expect(container.querySelector("canvas")).not.toBeInTheDocument();
});
it("pauses the renderer when the stage is outside the viewport", () => {
@@ -70,12 +87,12 @@ describe("NotebookStage3D", () => {
);
});
- it("returns usable browser content when the GLB boundary fails", () => {
- const error = new Error("GLB failed");
+ it("returns usable browser content when the stage boundary fails", () => {
const onStageError = jest.fn();
- mockedUseGLTF.mockImplementation(() => {
- throw error;
+ mockCanvas.mockImplementation(() => {
+ throw new Error("canvas failed");
});
+
render();
expect(screen.getByTestId("notebook-fallback")).toBeInTheDocument();
diff --git a/app/components/story/NotebookStage3D.tsx b/app/components/story/NotebookStage3D.tsx
index a6154b0..b6462cc 100644
--- a/app/components/story/NotebookStage3D.tsx
+++ b/app/components/story/NotebookStage3D.tsx
@@ -1,12 +1,12 @@
"use client";
-import { Html, useGLTF } from "@react-three/drei";
+import { Html, RoundedBox } from "@react-three/drei";
import { Canvas, useFrame } from "@react-three/fiber";
import {
Component,
type ReactNode,
- Suspense,
useEffect,
+ useLayoutEffect,
useMemo,
useRef,
useState,
@@ -17,8 +17,6 @@ import NotebookFallback from "./NotebookFallback";
import type { NotebookProject } from "./notebookProjects";
import { getNotebookFrame, type NotebookFrame } from "./notebookTimeline";
-export const NOTEBOOK_MODEL_URL = "/models/laptop-kenney.glb";
-
type ProgressRef = { current: number };
export type NotebookStage3DProps = {
@@ -32,96 +30,199 @@ export type NotebookStage3DProps = {
type NotebookSceneProps = Omit;
-type MaterialMap = {
- body: THREE.MeshStandardMaterial;
- frame: THREE.MeshStandardMaterial;
+/**
+ * Maps the timeline lid angle onto the hinge rotation. 0° = closed (lid
+ * resting on the deck), 112° = open (screen facing the camera, reclined
+ * ~22° past vertical). The lid pivots around the hinge axis — never around
+ * the display center.
+ */
+export function getNotebookLidRotation(lidAngle: number): number {
+ const angle = THREE.MathUtils.clamp(lidAngle, 0, 180);
+ const radians = THREE.MathUtils.degToRad(angle);
+ return radians === 0 ? 0 : -radians;
+}
+
+// The laptop is built procedurally in world units and the rig stays at scale
+// 1 so the DOM overlay (drei Html transform) aligns with the physical screen.
+const RIG_POSITION: [number, number, number] = [0, -0.45, 0];
+const CAMERA_POSITION: [number, number, number] = [0, 2, 7.4];
+const CAMERA_ROTATION: [number, number, number] = [-0.2, 0, 0];
+
+const BASE = { width: 3.2, height: 0.16, depth: 2.2 };
+const HINGE: [number, number, number] = [0, 0.16, -1.06];
+const LID = { width: 3.2, thickness: 0.1, depth: 2.2 };
+const SCREEN_INSET = { width: 2.9, depth: 1.96 };
+const SCREEN_GLASS = { width: 2.8, depth: 1.86 };
+
+const KEY_COLUMNS = 12;
+const KEY_ROWS = 5;
+const KEY_SIZE: [number, number, number] = [0.185, 0.05, 0.155];
+const KEY_GAP_X = 0.212;
+const KEY_GAP_Z = 0.2;
+const KEYBOARD_Z_START = -0.9;
+const KEYBOARD_Y = 0.185;
+const SPACEBAR_Z = 0.12;
+
+const SCREEN_DOM_DISTANCE_FACTOR = 2.2;
+
+type LaptopMaterials = {
+ chassis: THREE.MeshStandardMaterial;
+ keys: THREE.MeshStandardMaterial;
+ dark: THREE.MeshStandardMaterial;
+ accent: THREE.MeshStandardMaterial;
screen: THREE.MeshStandardMaterial;
};
-const MODEL_SCALE = 3.5;
-const OPEN_LID_ANGLE = 112;
-
-function createMaterials(): MaterialMap {
+function createLaptopMaterials(): LaptopMaterials {
return {
- body: new THREE.MeshStandardMaterial({
- color: "#183333",
- metalness: 0.78,
- roughness: 0.3,
+ chassis: new THREE.MeshStandardMaterial({
+ color: "#3f545b",
+ metalness: 0.35,
+ roughness: 0.32,
transparent: true,
}),
- frame: new THREE.MeshStandardMaterial({
- color: "#3f6264",
- metalness: 0.64,
- roughness: 0.38,
+ keys: new THREE.MeshStandardMaterial({
+ color: "#5a747c",
+ metalness: 0.2,
+ roughness: 0.5,
+ transparent: true,
+ }),
+ dark: new THREE.MeshStandardMaterial({
+ color: "#131f24",
+ metalness: 0.15,
+ roughness: 0.55,
+ transparent: true,
+ }),
+ accent: new THREE.MeshStandardMaterial({
+ color: "#465f66",
+ metalness: 0.3,
+ roughness: 0.4,
transparent: true,
}),
screen: new THREE.MeshStandardMaterial({
- color: "#a8b8b8",
- metalness: 0.05,
- roughness: 0.72,
+ color: "#0d1417",
+ emissive: "#22343a",
+ emissiveIntensity: 0.55,
+ metalness: 0.1,
+ roughness: 0.4,
transparent: true,
}),
};
}
-type ClonedNotebookScene = {
- root: THREE.Object3D;
- materials: MaterialMap;
- meshes: THREE.Mesh[];
-};
-
-function cloneNotebookScene(scene: THREE.Object3D): ClonedNotebookScene {
- const root = scene.clone(true);
- const materials = createMaterials();
- const meshes: THREE.Mesh[] = [];
-
- root.traverse((object) => {
- if (!(object instanceof THREE.Mesh)) return;
- meshes.push(object);
-
- object.geometry = object.geometry.clone();
- const mapMaterial = (sourceMaterial: THREE.Material) => {
- const materialName = sourceMaterial.name;
- return materialName === "metal"
- ? materials.screen
- : materialName === "metalMedium"
- ? materials.frame
- : materials.body;
- };
- object.material = Array.isArray(object.material)
- ? object.material.map(mapMaterial)
- : mapMaterial(object.material);
- object.frustumCulled = false;
- });
-
- return { root, materials, meshes };
-}
-
-function updateMaterialOpacity(materials: MaterialMap, opacity: number) {
- materials.body.opacity = opacity;
- materials.frame.opacity = opacity;
- materials.screen.opacity = opacity;
-}
-
-function disposeNotebookScene(cloned: ClonedNotebookScene) {
- for (const mesh of cloned.meshes) {
- mesh.geometry.dispose();
+function setLaptopOpacity(materials: LaptopMaterials, opacity: number) {
+ for (const material of Object.values(materials)) {
+ material.opacity = opacity;
}
- cloned.materials.body.dispose();
- cloned.materials.frame.dispose();
- cloned.materials.screen.dispose();
}
-function LoadingNotebook() {
+function disposeLaptopMaterials(materials: LaptopMaterials) {
+ for (const material of Object.values(materials)) {
+ material.dispose();
+ }
+}
+
+function LaptopKeys({ material }: { material: THREE.MeshStandardMaterial }) {
+ const ref = useRef(null);
+ const positions = useMemo(() => {
+ const list: Array<[number, number]> = [];
+ const startX = (-(KEY_COLUMNS - 1) * KEY_GAP_X) / 2;
+ for (let row = 0; row < KEY_ROWS; row += 1) {
+ for (let column = 0; column < KEY_COLUMNS; column += 1) {
+ list.push([
+ startX + column * KEY_GAP_X,
+ KEYBOARD_Z_START + row * KEY_GAP_Z,
+ ]);
+ }
+ }
+ return list;
+ }, []);
+
+ useLayoutEffect(() => {
+ const mesh = ref.current;
+ if (!(mesh instanceof THREE.InstancedMesh)) return;
+
+ const matrix = new THREE.Matrix4();
+ positions.forEach(([x, z], index) => {
+ matrix.makeTranslation(x, KEYBOARD_Y, z);
+ mesh.setMatrixAt(index, matrix);
+ });
+ mesh.instanceMatrix.needsUpdate = true;
+ }, [positions]);
+
return (
-
-
-
-
+
+
+
+ );
+}
+
+function LaptopBase({ materials }: { materials: LaptopMaterials }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function LaptopLid({ materials }: { materials: LaptopMaterials }) {
+ return (
+
+
+
+
+
);
@@ -141,9 +242,8 @@ function ScreenSurface({
return (
(null);
const lidRig = useRef
(null);
const smoothProgress = useRef(progress.current);
const elapsedForDom = useRef(0);
const [frame, setFrame] = useState(() =>
getNotebookFrame(progress.current, projects.length),
);
- const cloned = useMemo(() => cloneNotebookScene(scene), [scene]);
- const lid = cloned.root.getObjectByName("Laptop_Lid");
- const base = cloned.root.getObjectByName("Laptop_Base");
+ const materials = useMemo(createLaptopMaterials, []);
+
+ useEffect(() => () => disposeLaptopMaterials(materials), [materials]);
useFrame((_, delta) => {
smoothProgress.current +=
(progress.current - smoothProgress.current) * Math.min(1, delta * 8);
const nextFrame = getNotebookFrame(smoothProgress.current, projects.length);
- if (rig.current) {
- rig.current.position.set(0.46, -0.34, 0);
- rig.current.scale.setScalar(MODEL_SCALE);
- }
if (lidRig.current) {
- lidRig.current.rotation.x = THREE.MathUtils.degToRad(
- nextFrame.lidAngle - OPEN_LID_ANGLE,
- );
+ lidRig.current.rotation.x = getNotebookLidRotation(nextFrame.lidAngle);
}
- updateMaterialOpacity(cloned.materials, nextFrame.deviceOpacity);
+ setLaptopOpacity(materials, nextFrame.deviceOpacity);
elapsedForDom.current += delta;
if (elapsedForDom.current >= 1 / 24) {
@@ -203,42 +295,22 @@ function NotebookScene({
}
});
- useEffect(() => () => disposeNotebookScene(cloned), [cloned]);
-
return (
-
-
-
-
- {base && lid ? (
- <>
-
-
-
-
- >
- ) : (
- <>
-
-
-
-
- >
- )}
+
+
+
+
+
+
+
+
+
+
);
}
@@ -269,7 +341,11 @@ class NotebookStageBoundary extends Component<
return (
);
}
diff --git a/app/components/story/notebookTimeline.test.ts b/app/components/story/notebookTimeline.test.ts
index 9782a5d..e85fc1a 100644
--- a/app/components/story/notebookTimeline.test.ts
+++ b/app/components/story/notebookTimeline.test.ts
@@ -42,6 +42,20 @@ describe("getNotebookFrame", () => {
expect(getNotebookFrame(0.4, 4).cursorVisible).toBe(true);
});
+ it("turns the screen off before the lid closes", () => {
+ const screenOff = getNotebookFrame(0.9, 4);
+ const closing = getNotebookFrame(0.95, 4);
+
+ expect(screenOff).toMatchObject({
+ phase: "close",
+ lidAngle: 112,
+ screenOpacity: 0,
+ });
+ expect(closing.screenOpacity).toBe(0);
+ expect(closing.lidAngle).toBeGreaterThan(0);
+ expect(closing.lidAngle).toBeLessThan(112);
+ });
+
it("selects each tab only inside the browse window", () => {
expect(getNotebookFrame(0.43, 4).activeTabIndex).toBe(-1);
expect(getNotebookFrame(0.48, 4).activeTabIndex).toBe(0);
@@ -82,8 +96,20 @@ describe("getNotebookFrame", () => {
expect(moving.cursorX).toBeGreaterThan(previous.cursorX);
expect(moving.cursorX).toBeLessThan(target.cursorX);
- expect(moving.cursorY).toBeLessThan(previous.cursorY);
- expect(moving.cursorY).toBeGreaterThan(target.cursorY);
+ expect(moving.cursorY).toBe(target.cursorY);
+ });
+
+ it("lands the click pulse on the center of the selected tab", () => {
+ for (const progress of [0.475, 0.56, 0.65, 0.735]) {
+ const frame = getNotebookFrame(progress, 4);
+ expect(frame.cursorClicking).toBe(true);
+ expect(frame.cursorX).toBeCloseTo((frame.activeTabIndex + 0.5) / 4, 6);
+ }
+ });
+
+ it("tracks the real number of tabs", () => {
+ expect(getNotebookFrame(0.68, 2).cursorX).toBeCloseTo(0.75, 6);
+ expect(getNotebookFrame(0.6, 3).cursorX).toBeCloseTo(0.5, 6);
});
it("is deterministic when rewound to the same progress", () => {
diff --git a/app/components/story/notebookTimeline.ts b/app/components/story/notebookTimeline.ts
index dd0c2b1..8c51fc8 100644
--- a/app/components/story/notebookTimeline.ts
+++ b/app/components/story/notebookTimeline.ts
@@ -23,12 +23,9 @@ const OPEN_ANGLE = 112;
const BROWSE_START = 0.44;
const BROWSE_END = 0.78;
const HOLD_END = 0.87;
-const CURSOR_POINTS = [
- { x: 0.26, y: 0.68 },
- { x: 0.58, y: 0.32 },
- { x: 0.74, y: 0.72 },
- { x: 0.38, y: 0.28 },
-];
+const CLOSE_LID_START = 0.9;
+const TAB_CLICK_Y = 0.3;
+const PARK = { x: 0.5, y: 0.95 };
function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
@@ -77,6 +74,11 @@ function getActiveTabIndex(
return clamp(Math.floor(browseProgress * tabCount), 0, tabCount - 1);
}
+function tabCenterX(index: number, tabCount: number): number {
+ if (tabCount < 1) return PARK.x;
+ return (index + 0.5) / tabCount;
+}
+
function getCursorPoint(
progress: number,
phase: NotebookPhase,
@@ -86,27 +88,30 @@ function getCursorPoint(
if (phase === "cursor-in") {
const cursorProgress = (progress - 0.34) / 0.1;
return {
- x: interpolate(0.06, 0.26, cursorProgress),
- y: interpolate(0.88, 0.68, cursorProgress),
+ x: interpolate(PARK.x, tabCenterX(0, tabCount), cursorProgress),
+ y: interpolate(PARK.y, TAB_CLICK_Y, cursorProgress),
};
}
if (activeTabIndex < 0) {
- return { x: 0.06, y: 0.88 };
+ return { ...PARK };
}
- const target = CURSOR_POINTS[activeTabIndex % CURSOR_POINTS.length];
+ const target = { x: tabCenterX(activeTabIndex, tabCount), y: TAB_CLICK_Y };
if (phase !== "browse" || activeTabIndex === 0) return target;
const browseProgress =
(progress - BROWSE_START) / (BROWSE_END - BROWSE_START);
const slotProgress = browseProgress * tabCount - activeTabIndex;
- const previous = CURSOR_POINTS[(activeTabIndex - 1) % CURSOR_POINTS.length];
+ const previous = {
+ x: tabCenterX(activeTabIndex - 1, tabCount),
+ y: TAB_CLICK_Y,
+ };
const moveProgress = clamp(slotProgress / 0.36, 0, 1);
return {
x: interpolate(previous.x, target.x, moveProgress),
- y: interpolate(previous.y, target.y, moveProgress),
+ y: target.y,
};
}
@@ -158,11 +163,13 @@ export function getNotebookFrame(
const lidAngle = isOpening
? interpolate(0, OPEN_ANGLE, (normalizedProgress - 0.1) / 0.15)
: isClosing
- ? interpolate(
- OPEN_ANGLE,
- 0,
- (normalizedProgress - HOLD_END) / (1 - HOLD_END),
- )
+ ? normalizedProgress < CLOSE_LID_START
+ ? OPEN_ANGLE
+ : interpolate(
+ OPEN_ANGLE,
+ 0,
+ (normalizedProgress - CLOSE_LID_START) / (1 - CLOSE_LID_START),
+ )
: normalizedProgress >= 0.25
? OPEN_ANGLE
: 0;
@@ -171,7 +178,11 @@ export function getNotebookFrame(
: normalizedProgress >= 0.34 && !isClosing
? 1
: isClosing
- ? interpolate(1, 0, (normalizedProgress - HOLD_END) / (1 - HOLD_END))
+ ? interpolate(
+ 1,
+ 0,
+ (normalizedProgress - HOLD_END) / (CLOSE_LID_START - HOLD_END),
+ )
: 0;
const deviceOpacity = isApproach
? interpolate(0, 1, normalizedProgress / 0.1)
diff --git a/app/globals.css b/app/globals.css
index dbed694..cfe2297 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -905,6 +905,11 @@ textarea {
width: 100%;
height: 100%;
}
+.notebook-story-canvas > div:first-child,
+.notebook-story-canvas canvas {
+ pointer-events: none;
+}
+
.notebook-story-static-content {
width: min(1080px, 100%);
margin: 0 auto;
@@ -919,6 +924,7 @@ textarea {
.notebook-screen-surface {
width: min(660px, 68vw);
+ pointer-events: auto;
min-width: 280px;
overflow: hidden;
border-radius: 2px;
@@ -1036,7 +1042,8 @@ textarea {
transition:
color 180ms ease,
border-color 180ms ease,
- background-color 180ms ease;
+ background-color 180ms ease,
+ transform 160ms var(--ease-out, cubic-bezier(0.23, 1, 0.32, 1));
}
.notebook-tab:hover,
@@ -1046,6 +1053,10 @@ textarea {
color: var(--white);
}
+.notebook-tab[data-cursor-clicking="true"] {
+ transform: scale(0.97);
+}
+
.notebook-tab-number {
flex: 0 0 auto;
color: var(--slash);
@@ -1199,14 +1210,14 @@ textarea {
.notebook-cursor {
position: absolute;
- z-index: 1;
+ z-index: 3;
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);
+ transform: translate(-50%, -45%) rotate(-18deg);
transition:
width 120ms ease,
height 120ms ease,
@@ -1296,4 +1307,8 @@ textarea {
transition: none;
animation: none;
}
+
+ .notebook-tab[data-cursor-clicking="true"] {
+ transform: none;
+ }
}
diff --git a/public/models/laptop-kenney.glb b/public/models/laptop-kenney.glb
deleted file mode 100644
index d60b514..0000000
Binary files a/public/models/laptop-kenney.glb and /dev/null differ