summaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/package.json2
-rw-r--r--web/src/components/app/index.css9
-rw-r--r--web/src/components/app/index.ts3
-rw-r--r--web/src/components/grid/cellAtCoord.ts40
-rw-r--r--web/src/components/grid/drawGrid.ts95
-rw-r--r--web/src/components/grid/index.css1
-rw-r--r--web/src/components/grid/index.ts36
-rw-r--r--web/src/components/grid/renderGrid.ts2
-rw-r--r--web/src/components/index.ts1
-rw-r--r--web/src/components/toolbar/index.css50
-rw-r--r--web/src/components/toolbar/index.ts24
-rw-r--r--web/src/html.ts10
-rw-r--r--web/src/index.css10
-rw-r--r--web/src/index.ts38
-rw-r--r--web/src/selection.ts19
-rw-r--r--web/vite.config.ts8
16 files changed, 317 insertions, 31 deletions
diff --git a/web/package.json b/web/package.json
index e15cd1e..a565e36 100644
--- a/web/package.json
+++ b/web/package.json
@@ -8,6 +8,8 @@
"open-color": "^1.9.1"
},
"devDependencies": {
+ "@tailwindcss/vite": "^4.1.16",
+ "tailwindcss": "^4.1.16",
"vite": "^7.1.12"
}
}
diff --git a/web/src/components/app/index.css b/web/src/components/app/index.css
index 3eeaee9..aaf2ced 100644
--- a/web/src/components/app/index.css
+++ b/web/src/components/app/index.css
@@ -1,3 +1,12 @@
ntv-app {
display: block;
+ padding: 1.5rem;
+}
+
+ntv-app > ntv-toolbar {
+ margin-bottom: 1.5rem;
+}
+
+ntv-app > ntv-grid + ntv-grid {
+ margin-top: 1.5rem;
}
diff --git a/web/src/components/app/index.ts b/web/src/components/app/index.ts
index 2782e22..910aa52 100644
--- a/web/src/components/app/index.ts
+++ b/web/src/components/app/index.ts
@@ -1,9 +1,12 @@
+import h from "../../html";
import ntvGrid from "../grid";
+import ntvToolbar from "../toolbar";
import "./index.css";
class NotiveAppElement extends HTMLElement {
connectedCallback() {
this.append(
+ ntvToolbar(),
...window.notive.doc.grids.map((grid) => {
return ntvGrid({ gridId: grid.id });
}),
diff --git a/web/src/components/grid/cellAtCoord.ts b/web/src/components/grid/cellAtCoord.ts
new file mode 100644
index 0000000..dd594a4
--- /dev/null
+++ b/web/src/components/grid/cellAtCoord.ts
@@ -0,0 +1,40 @@
+import Coord from "../../math/Coord";
+import { CellRef } from "../../types";
+import { RenderedGrid, RenderedRow } from "./renderGrid";
+
+function rowAtCoord(grid: RenderedGrid, coord: Coord): RenderedRow | undefined {
+ if (coord.y <= grid.rect.topLeft.y) {
+ return grid.renderedRows[0];
+ }
+
+ if (coord.y >= grid.rect.bottomRight.y) {
+ return grid.renderedRows.at(-1);
+ }
+
+ return grid.renderedRows.find((row) =>
+ row.rect.verticallyContainsCoord(coord),
+ );
+}
+
+export default function cellAtCoord(
+ grid: RenderedGrid,
+ x: number,
+ y: number,
+): CellRef | undefined {
+ const coord = new Coord(x, y);
+ const row = rowAtCoord(grid, coord);
+
+ if (!row) return;
+
+ if (x <= row.rect.topLeft.x) {
+ return row.renderedCells[0]?.cellRef;
+ }
+
+ if (x >= row.rect.bottomRight.x) {
+ return row.renderedCells.at(-1)?.cellRef;
+ }
+
+ return row.renderedCells.find((cell) =>
+ cell.rect.horizontallyContainsCoord(coord),
+ )?.cellRef;
+}
diff --git a/web/src/components/grid/drawGrid.ts b/web/src/components/grid/drawGrid.ts
index 6284693..01240b5 100644
--- a/web/src/components/grid/drawGrid.ts
+++ b/web/src/components/grid/drawGrid.ts
@@ -1,16 +1,21 @@
-import { RenderedGrid } from "./renderGrid";
-import colors from "open-color";
+import colors from "tailwindcss/colors";
+import { PendingSelection, Selection } from "../../selection";
+import { CellRef } from "../../types";
+import { RenderedCell, RenderedGrid } from "./renderGrid";
-export default function drawGrid(
- ctx: CanvasRenderingContext2D,
- grid: RenderedGrid,
-) {
+function fillBackground(ctx: CanvasRenderingContext2D, grid: RenderedGrid) {
ctx.clearRect(0, 0, grid.rect.width, grid.rect.height);
-
- ctx.fillStyle = colors.gray[8];
+ ctx.fillStyle = colors.neutral[800];
ctx.fillRect(0, 0, grid.rect.width, grid.rect.height);
+}
+
+function strokeGrid(ctx: CanvasRenderingContext2D, grid: RenderedGrid) {
+ ctx.strokeStyle = colors.neutral[700];
+ ctx.strokeRect(0.5, 0.5, grid.rect.width - 1, grid.rect.height - 1);
+}
- ctx.strokeStyle = colors.gray[7];
+function strokeGridLines(ctx: CanvasRenderingContext2D, grid: RenderedGrid) {
+ ctx.strokeStyle = colors.neutral[700];
grid.renderedRows.forEach((row, renderedRowIndex) => {
const isLastRow = renderedRowIndex === grid.renderedRows.length - 1;
@@ -28,3 +33,75 @@ export default function drawGrid(
});
});
}
+
+function getRenderedCell(
+ grid: RenderedGrid,
+ cellRef: CellRef,
+): RenderedCell | undefined {
+ const rowsPerPart = grid.renderedRows.length / grid.parts.length;
+ const renderedRowIndex = cellRef.partIndex * rowsPerPart + cellRef.rowIndex;
+ return grid.renderedRows[renderedRowIndex]?.renderedCells[cellRef.cellIndex];
+}
+
+function drawPendingSelection(
+ ctx: CanvasRenderingContext2D,
+ grid: RenderedGrid,
+ selection: PendingSelection,
+) {}
+
+function drawSelection(
+ ctx: CanvasRenderingContext2D,
+ grid: RenderedGrid,
+ selection: Selection,
+) {
+ if (selection.gridId !== grid.id) return;
+
+ const cell = getRenderedCell(grid, selection.activeCellRef);
+
+ if (!cell) return;
+
+ const isLastCell = cell.rect.bottomRight.x === grid.rect.bottomRight.x;
+ const isLastRow = cell.rect.bottomRight.y === grid.rect.bottomRight.y;
+
+ // ctx.fillStyle = colors.green[4] + "30";
+
+ // ctx.fillRect(
+ // cell.rect.topLeft.x + 1,
+ // cell.rect.topLeft.y + 1,
+ // cell.rect.width - 1,
+ // cell.rect.height - 1,
+ // );
+
+ ctx.strokeStyle = colors.green[600];
+ ctx.lineWidth = 2;
+
+ ctx.strokeRect(
+ cell.rect.topLeft.x + 1,
+ cell.rect.topLeft.y + 1,
+ isLastCell ? cell.rect.width - 2 : cell.rect.width - 1,
+ isLastRow ? cell.rect.height - 2 : cell.rect.height - 1,
+ );
+}
+
+export default function drawGrid(
+ ctx: CanvasRenderingContext2D,
+ grid: RenderedGrid,
+ selection?: Selection,
+ pendingSelection?: PendingSelection,
+) {
+ const excursion = (f: () => void) => {
+ ctx.save();
+ f();
+ ctx.restore();
+ };
+
+ excursion(() => fillBackground(ctx, grid));
+ excursion(() => strokeGridLines(ctx, grid));
+ excursion(() => strokeGrid(ctx, grid));
+
+ if (pendingSelection) {
+ excursion(() => drawPendingSelection(ctx, grid, pendingSelection));
+ } else if (selection) {
+ excursion(() => drawSelection(ctx, grid, selection));
+ }
+}
diff --git a/web/src/components/grid/index.css b/web/src/components/grid/index.css
index 0fad720..a733015 100644
--- a/web/src/components/grid/index.css
+++ b/web/src/components/grid/index.css
@@ -1,6 +1,5 @@
ntv-grid {
display: block;
- padding: 1.5rem;
}
ntv-grid > canvas {
diff --git a/web/src/components/grid/index.ts b/web/src/components/grid/index.ts
index 829a511..0acace4 100644
--- a/web/src/components/grid/index.ts
+++ b/web/src/components/grid/index.ts
@@ -1,5 +1,5 @@
import h, { type CreateElement } from "../../html";
-import renderGrid from "./renderGrid";
+import cellAtCoord from "./cellAtCoord";
import drawGrid from "./drawGrid";
import "./index.css";
@@ -15,6 +15,10 @@ class NotiveGridElement extends HTMLElement {
this.setAttribute("grid-id", val);
}
+ get renderedGrid() {
+ return window.notive.getGrid(this.#gridId)!;
+ }
+
canvasEl: HTMLCanvasElement = h.canvas();
connectedCallback() {
@@ -22,19 +26,41 @@ class NotiveGridElement extends HTMLElement {
throw new Error("ntv-grid requries gridId attribute");
}
+ this.canvasEl.addEventListener("mousedown", (event) => {
+ const clientRect = this.canvasEl.getBoundingClientRect();
+ const x = event.x - clientRect.x;
+ const y = event.y - clientRect.y;
+ const cellRef = cellAtCoord(this.renderedGrid, x, y);
+ if (!cellRef) return;
+ window.notive.selectCell(this.#gridId, cellRef);
+ });
+
+ window.addEventListener("ntv:selection-changed", () => {
+ this.draw();
+ });
+
this.append(this.canvasEl);
this.draw();
}
draw() {
const ctx = this.canvasEl.getContext("2d");
+
if (!ctx) throw new Error("Unable to get canvas context");
+
const grid = window.notive.getGrid(this.gridId);
+
if (!grid) return;
- const renderedGrid = renderGrid(grid);
- this.canvasEl.setAttribute("width", renderedGrid.rect.width + "px");
- this.canvasEl.setAttribute("height", renderedGrid.rect.height + "px");
- drawGrid(ctx, renderedGrid);
+
+ this.canvasEl.setAttribute("width", grid.rect.width + "px");
+ this.canvasEl.setAttribute("height", grid.rect.height + "px");
+
+ drawGrid(
+ ctx,
+ grid,
+ window.notive.selection,
+ window.notive.pendingSelection,
+ );
}
}
diff --git a/web/src/components/grid/renderGrid.ts b/web/src/components/grid/renderGrid.ts
index 5666f66..7ef8813 100644
--- a/web/src/components/grid/renderGrid.ts
+++ b/web/src/components/grid/renderGrid.ts
@@ -1,6 +1,6 @@
import Ratio from "../../math/Ratio";
import Rect from "../../math/Rect";
-import { Cell, CellRef, Grid, Row, RowRef } from "./types";
+import { Cell, CellRef, Grid, Row, RowRef } from "../../types";
export interface RenderedCell extends Cell {
cellRef: CellRef;
diff --git a/web/src/components/index.ts b/web/src/components/index.ts
index 8bc14e7..b7f6f55 100644
--- a/web/src/components/index.ts
+++ b/web/src/components/index.ts
@@ -1,2 +1,3 @@
import "./app";
import "./grid";
+import "./toolbar";
diff --git a/web/src/components/toolbar/index.css b/web/src/components/toolbar/index.css
new file mode 100644
index 0000000..3f78671
--- /dev/null
+++ b/web/src/components/toolbar/index.css
@@ -0,0 +1,50 @@
+ntv-toolbar {
+ display: flex;
+ border-radius: 4px;
+ background: var(--color-neutral-800);
+ width: min-content;
+}
+
+ntv-toolbar > section {
+ display: flex;
+ gap: 0.5rem;
+ padding: 0.5rem;
+}
+
+ntv-toolbar button[data-variant="menu"] {
+ border-radius: 4px;
+ background: var(--color-neutral-700);
+ padding: 0 0.625rem;
+ height: 1.5rem;
+ color: white;
+ font-size: 0.75rem;
+}
+
+ntv-toolbar button[data-variant="icon"] {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: 4px;
+ background: var(--color-neutral-700);
+ padding: 0.125rem 0.625rem;
+ aspect-ratio: 1;
+ height: 1.5rem;
+ color: white;
+ font-weight: 600;
+ font-size: 0.75rem;
+}
+
+ntv-toolbar button:hover {
+ background: var(--color-neutral-600);
+}
+
+ntv-toolbar input {
+ border: 1px solid var(--color-neutral-700);
+ border-radius: 4px;
+ background: var(--color-neutral-900);
+ width: 2.5rem;
+ height: 1.5rem;
+ color: white;
+ font-size: 0.75rem;
+ text-align: center;
+}
diff --git a/web/src/components/toolbar/index.ts b/web/src/components/toolbar/index.ts
new file mode 100644
index 0000000..d844a69
--- /dev/null
+++ b/web/src/components/toolbar/index.ts
@@ -0,0 +1,24 @@
+import h, { CreateElement } from "../../html";
+import "./index.css";
+
+class NotiveToolbarElement extends HTMLElement {
+ connectedCallback() {
+ this.append(
+ h.section(
+ h.button({ dataset: { variant: "menu" } }, "File"),
+ h.button({ dataset: { variant: "menu" } }, "Edit"),
+ h.button({ dataset: { variant: "menu" } }, "Format"),
+ ),
+ h.section(
+ h.button({ dataset: { variant: "icon" } }, "-"),
+ h.input({ type: "text", value: "1" }),
+ h.button({ dataset: { variant: "icon" } }, "+"),
+ ),
+ );
+ }
+}
+
+customElements.define("ntv-toolbar", NotiveToolbarElement);
+
+export default ((...args: any[]): NotiveToolbarElement =>
+ (h as any)["ntv-toolbar"](...args)) as CreateElement<NotiveToolbarElement>;
diff --git a/web/src/html.ts b/web/src/html.ts
index 5bfff21..8802e50 100644
--- a/web/src/html.ts
+++ b/web/src/html.ts
@@ -13,8 +13,14 @@ const h = new Proxy({} as ElementCreator, {
(...args: any[]) => {
const el = document.createElement(tag);
- if (typeof args[0] === "object") {
- Object.assign(el, args.shift());
+ if (args[0]?.constructor === Object) {
+ const { dataset, ...attrs } = args.shift();
+
+ Object.assign(el, attrs);
+
+ if (dataset) {
+ Object.assign(el.dataset, dataset);
+ }
}
el.append(...args.flat());
diff --git a/web/src/index.css b/web/src/index.css
index ba2a6a7..4fe2764 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -1,11 +1,5 @@
-@import "open-color";
+@import "tailwindcss";
body {
- margin: 0;
- background: var(--oc-gray-9);
- color: var(--oc-white);
- font-weight: normal;
- font-family:
- Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", source-sans-pro,
- sans-serif;
+ background: var(--color-neutral-900);
}
diff --git a/web/src/index.ts b/web/src/index.ts
index fbbf37c..ac4870c 100644
--- a/web/src/index.ts
+++ b/web/src/index.ts
@@ -1,5 +1,7 @@
import Ratio from "./math/Ratio";
-import { Cell, Doc, Grid } from "./types";
+import { Cell, CellRef, Doc, Grid } from "./types";
+import { ActiveCellSelection, PendingSelection, Selection } from "./selection";
+import renderGrid, { RenderedGrid } from "./components/grid/renderGrid";
function defaultDoc(): Doc {
const defaultCells: Cell[] = Array.from({ length: 16 }, () => ({
@@ -20,17 +22,47 @@ function defaultDoc(): Doc {
},
],
},
+ {
+ id: window.crypto.randomUUID(),
+ baseCellSize: 48,
+ baseCellWidthRatio: new Ratio(1, 16),
+ parts: [
+ {
+ rows: Array.from({ length: 4 }, () => ({
+ cells: [...defaultCells],
+ })),
+ },
+ ],
+ },
],
};
}
export default class Notive {
doc: Doc = defaultDoc();
- gridsById = Object.fromEntries(this.doc.grids.map((grid) => [grid.id, grid]));
- getGrid(id: string): Grid | undefined {
+ gridsById = Object.fromEntries(
+ this.doc.grids.map((grid) => [grid.id, renderGrid(grid)]),
+ );
+
+ selection?: Selection;
+
+ pendingSelection?: Selection;
+
+ getGrid(id: string): RenderedGrid | undefined {
return this.gridsById[id];
}
+
+ selectCell(gridId: string, cellRef: CellRef) {
+ const previousSelection = this.selection;
+ this.selection = new ActiveCellSelection(gridId, cellRef);
+
+ window.dispatchEvent(
+ new CustomEvent("ntv:selection-changed", {
+ detail: { selection: this.selection, previousSelection },
+ }),
+ );
+ }
}
window.notive = new Notive();
diff --git a/web/src/selection.ts b/web/src/selection.ts
new file mode 100644
index 0000000..88d394b
--- /dev/null
+++ b/web/src/selection.ts
@@ -0,0 +1,19 @@
+import { CellRef } from "./types";
+
+export abstract class Selection {
+ readonly gridId: string;
+ readonly activeCellRef: CellRef;
+
+ constructor(gridId: string, activeCellRef: CellRef) {
+ this.gridId = gridId;
+ this.activeCellRef = activeCellRef;
+ }
+}
+
+export class ActiveCellSelection extends Selection {}
+
+export class RangeSelection extends Selection {}
+
+export class AllSelection extends Selection {}
+
+export class PendingSelection extends Selection {}
diff --git a/web/vite.config.ts b/web/vite.config.ts
index d59c396..21088ad 100644
--- a/web/vite.config.ts
+++ b/web/vite.config.ts
@@ -1,3 +1,7 @@
-export default {
+import tailwindcss from "@tailwindcss/vite";
+import { defineConfig } from "vite";
+
+export default defineConfig({
root: "src",
-};
+ plugins: [tailwindcss()],
+});