summaryrefslogtreecommitdiff
path: root/web/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'web/src/components')
-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
10 files changed, 245 insertions, 16 deletions
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>;