From 75c6c2ec2b40ae62687e9cf481db7276e164e05e Mon Sep 17 00:00:00 2001 From: Naman Date: Sun, 2 Aug 2026 14:23:29 +0200 Subject: [PATCH 1/2] chore: update star history (20 stars) Co-Authored-By: Claude Opus 5 --- docs/star-history-dark.svg | 14 +++++++------- docs/star-history.json | 7 ++++--- docs/star-history.svg | 14 +++++++------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/star-history-dark.svg b/docs/star-history-dark.svg index 716ce1a..7b9a088 100644 --- a/docs/star-history-dark.svg +++ b/docs/star-history-dark.svg @@ -1,5 +1,5 @@ - - + + @@ -8,12 +8,12 @@ - 19 stars + 20 stars nmndwivedi/quickdraw - - - - + + + + 01020Jul 31Aug 1Aug 2 diff --git a/docs/star-history.json b/docs/star-history.json index 1d2a663..439a0c9 100644 --- a/docs/star-history.json +++ b/docs/star-history.json @@ -1,8 +1,8 @@ { "repo": "nmndwivedi/quickdraw", - "stars": 19, + "stars": 20, "createdAt": "2026-07-31T22:22:15Z", - "generatedAt": "2026-08-02T10:40:48.779Z", + "generatedAt": "2026-08-02T12:23:29.848Z", "starredAt": [ "2026-08-01T01:18:08Z", "2026-08-01T05:46:50Z", @@ -22,6 +22,7 @@ "2026-08-02T06:42:06Z", "2026-08-02T08:48:12Z", "2026-08-02T09:13:33Z", - "2026-08-02T09:35:30Z" + "2026-08-02T09:35:30Z", + "2026-08-02T11:39:14Z" ] } diff --git a/docs/star-history.svg b/docs/star-history.svg index 259f7ec..c90d649 100644 --- a/docs/star-history.svg +++ b/docs/star-history.svg @@ -1,5 +1,5 @@ - - + + @@ -8,12 +8,12 @@ - 19 stars + 20 stars nmndwivedi/quickdraw - - - - + + + + 01020Jul 31Aug 1Aug 2 From e88a6f9b4d0e642b0c1de92b203e2fbb357661ef Mon Sep 17 00:00:00 2001 From: Naman Date: Sun, 2 Aug 2026 16:06:30 +0200 Subject: [PATCH 2/2] Add action bar, five grid backdrops, nested grid picker, and hero draw-hint animation - New vertical action bar (left side): undo/redo moved out of the dock, plus duplicate/delete that enable only with a selection - Grid backdrops: lines (new default), ruled, dots (uniform color), crosses, isometric; picked via a standard nested dropdown flyout - loadSnapshot now resets undo/redo history and notifies toolbars - Popovers animate in; geo picker opens on first click - Website hero: animated pencil traces a squiggle over a "Draw something!" pill; hides once the board has content - Bump packages to 0.2.0 Co-Authored-By: Claude Fable 5 --- .claude/launch.json | 8 +- README.md | 4 +- apps/app/package.json | 2 +- apps/app/src/main.js | 2 +- apps/docs/app/brand.css | 10 ++ apps/docs/app/layout.jsx | 73 ++++++++++++-- apps/docs/content/core.mdx | 2 +- apps/docs/content/index.mdx | 6 +- apps/docs/content/react-native.mdx | 2 +- apps/docs/content/react.mdx | 2 +- apps/docs/content/theming.mdx | 4 +- apps/website/package.json | 2 +- apps/website/src/pages/index.astro | 90 ++++++++++++++++- examples/react-demo/package.json | 2 +- packages/core/package.json | 2 +- packages/core/src/editor.js | 66 ++++++++++-- packages/core/src/palette.js | 7 +- packages/core/src/quickdraw.css | 74 ++++++++++++++ packages/core/src/store.js | 7 ++ packages/core/src/ui.js | 155 +++++++++++++++++++++-------- packages/core/test/editor.test.js | 82 +++++++++++---- packages/core/test/palette.test.js | 2 +- packages/core/test/store.test.js | 15 +++ packages/react-native/package.json | 4 +- packages/react-native/src/index.js | 4 +- packages/react/package.json | 4 +- packages/react/src/index.js | 4 +- 27 files changed, 521 insertions(+), 114 deletions(-) diff --git a/.claude/launch.json b/.claude/launch.json index da9a472..42ca8a9 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -18,12 +18,10 @@ "runtimeArgs": [ "run", "dev", - "--workspace=quickdraw-website", - "--", - "--port", - "4399" + "--workspace=quickdraw-website" ], - "port": 4399 + "port": 4399, + "autoPort": true }, { "name": "app", diff --git a/README.md b/README.md index f3863de..4d98f4d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # Quickdraw [![CI](https://github.com/nmndwivedi/quickdraw/actions/workflows/ci.yml/badge.svg)](https://github.com/nmndwivedi/quickdraw/actions/workflows/ci.yml) -[![npm](https://img.shields.io/npm/v/@quickdrawjs/core?label=%40quickdrawjs%2Fcore)](https://www.npmjs.com/package/@quickdrawjs/core) +[![npm: @quickdrawjs/core](https://img.shields.io/npm/v/@quickdrawjs/core?label=%40quickdrawjs%2Fcore&logo=npm)](https://www.npmjs.com/package/@quickdrawjs/core) +[![npm: @quickdrawjs/react](https://img.shields.io/npm/v/@quickdrawjs/react?label=%40quickdrawjs%2Freact&logo=npm)](https://www.npmjs.com/package/@quickdrawjs/react) +[![npm: @quickdrawjs/react-native](https://img.shields.io/npm/v/@quickdrawjs/react-native?label=%40quickdrawjs%2Freact-native&logo=npm)](https://www.npmjs.com/package/@quickdrawjs/react-native) [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) **The MIT-licensed infinite-canvas whiteboard SDK.** Drop a complete, diff --git a/apps/app/package.json b/apps/app/package.json index c5e272a..361a67c 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -10,7 +10,7 @@ "preview": "vite preview" }, "dependencies": { - "@quickdrawjs/core": "^0.1.3" + "@quickdrawjs/core": "^0.2.0" }, "devDependencies": { "vite": "^6.3.0" diff --git a/apps/app/src/main.js b/apps/app/src/main.js index 5281240..895df1e 100644 --- a/apps/app/src/main.js +++ b/apps/app/src/main.js @@ -12,7 +12,7 @@ const theme = localStorage.getItem(THEME_KEY) || (prefersDark ? 'dark' : 'light' const board = createQuickdraw({ container: document.getElementById('board'), theme, - grid: 'dots', + grid: 'lines', }) const { editor } = board diff --git a/apps/docs/app/brand.css b/apps/docs/app/brand.css index 5cfab49..d3ad017 100644 --- a/apps/docs/app/brand.css +++ b/apps/docs/app/brand.css @@ -18,6 +18,16 @@ html.light aside { --nextra-bg: 250, 248, 244; } +/* npm mark in the navbar, matching the GitHub icon's hover feel */ +.qd-npm-link { + display: flex; + align-items: center; + transition: opacity 0.15s; +} +.qd-npm-link:hover { + opacity: 0.75; +} + /* slightly rounder code blocks, matching the marketing site */ .nextra-code pre { border-radius: 12px; diff --git a/apps/docs/app/layout.jsx b/apps/docs/app/layout.jsx index b35f377..30611d4 100644 --- a/apps/docs/app/layout.jsx +++ b/apps/docs/app/layout.jsx @@ -41,26 +41,77 @@ const logo = ( ) +// npm's wordmark, sized to sit next to Nextra's 24px GitHub icon. +const npmLink = ( + + + +) + const navbar = ( + > + {npmLink} + ) +const footerLink = { textDecoration: 'underline' } + +// One so the whole line is a single flex item of Nextra's footer and +// wraps as ordinary text instead of one child per row. const footer = ( ) diff --git a/apps/docs/content/core.mdx b/apps/docs/content/core.mdx index cd79dbf..7585316 100644 --- a/apps/docs/content/core.mdx +++ b/apps/docs/content/core.mdx @@ -37,7 +37,7 @@ Returns `{ editor, ui, destroy }`. | --- | --- | --- | --- | | `container` | `HTMLElement` | required | The board fills it | | `theme` | `'light' \| 'dark'` | `'light'` | | -| `grid` | `'none' \| 'lines' \| 'dots'` | `'none'` | | +| `grid` | `'none' \| 'lines' \| 'ruled' \| 'dots' \| 'crosses' \| 'iso'` | `'lines'` | | | `readonly` | `boolean` | `false` | View-only board | | `hideUi` | `boolean` | `false` | No toolbar — [headless](/docs/headless/) | | `themeToggle` / `gridControl` | `boolean` | `true` | Board-menu switches | diff --git a/apps/docs/content/index.mdx b/apps/docs/content/index.mdx index a54f8d6..7bc739f 100644 --- a/apps/docs/content/index.mdx +++ b/apps/docs/content/index.mdx @@ -10,9 +10,9 @@ three packages: | Package | For | | --- | --- | -| `@quickdrawjs/react` | React apps — a `` component | -| `@quickdrawjs/react-native` | React Native / Expo — a WebView component with a typed bridge | -| `@quickdrawjs/core` | Any web page — framework-free, zero dependencies | +| [`@quickdrawjs/react`](https://www.npmjs.com/package/@quickdrawjs/react) | React apps — a `` component | +| [`@quickdrawjs/react-native`](https://www.npmjs.com/package/@quickdrawjs/react-native) | React Native / Expo — a WebView component with a typed bridge | +| [`@quickdrawjs/core`](https://www.npmjs.com/package/@quickdrawjs/core) | Any web page — framework-free, zero dependencies | Every board comes complete: pressure ink, highlighter, shapes with editable labels, arrows, lines, text, sticky notes, images, laser pointer, selection, diff --git a/apps/docs/content/react-native.mdx b/apps/docs/content/react-native.mdx index 03ff571..3484f7d 100644 --- a/apps/docs/content/react-native.mdx +++ b/apps/docs/content/react-native.mdx @@ -42,7 +42,7 @@ export default function Board() { | Prop | Type | Default | | | --- | --- | --- | --- | | `theme` | `'light' \| 'dark'` | `'light'` | Live-switchable | -| `grid` | `'none' \| 'lines' \| 'dots'` | `'none'` | Live-switchable | +| `grid` | `'none' \| 'lines' \| 'ruled' \| 'dots' \| 'crosses' \| 'iso'` | `'lines'` | Live-switchable | | `readonly` | `boolean` | `false` | Lock input | | `hideUi` | `boolean` | `false` | Hide the toolbar | | `themeToggle` / `gridControl` | `boolean` | `true` | Board-menu switches | diff --git a/apps/docs/content/react.mdx b/apps/docs/content/react.mdx index a5f040d..9bc36dc 100644 --- a/apps/docs/content/react.mdx +++ b/apps/docs/content/react.mdx @@ -35,7 +35,7 @@ real dimensions. | Prop | Type | Default | | | --- | --- | --- | --- | | `theme` | `'light' \| 'dark'` | `'light'` | Live-switchable | -| `grid` | `'none' \| 'lines' \| 'dots'` | `'none'` | The backdrop; live-switchable | +| `grid` | `'none' \| 'lines' \| 'ruled' \| 'dots' \| 'crosses' \| 'iso'` | `'lines'` | The backdrop; live-switchable | | `readonly` | `boolean` | `false` | Lock input (also hides the toolbar) | | `hideUi` | `boolean` | `false` | Hide the stock toolbar — [bring your own](/docs/headless/) | | `themeToggle` | `boolean` | `true` | Show the theme switch in the board menu | diff --git a/apps/docs/content/theming.mdx b/apps/docs/content/theming.mdx index ce17e02..563a54d 100644 --- a/apps/docs/content/theming.mdx +++ b/apps/docs/content/theming.mdx @@ -43,10 +43,10 @@ and a glow on dark ones. ## Grids -Three backdrops: bare paper, ruled lines, or dotted intersections. +Six backdrops: bare paper, grid lines, ruled notebook lines, dotted intersections, blueprint crosses, or an isometric weave. ```js -editor.setGrid('dots') // 'none' | 'lines' | 'dots' +editor.setGrid('dots') // 'none' | 'lines' | 'ruled' | 'dots' | 'crosses' | 'iso' editor.on('grid', sync) ``` diff --git a/apps/website/package.json b/apps/website/package.json index d15f9ec..9148882 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@astrojs/sitemap": "^3.3.0", - "@quickdrawjs/core": "^0.1.3", + "@quickdrawjs/core": "^0.2.0", "astro": "^5.7.0" } } diff --git a/apps/website/src/pages/index.astro b/apps/website/src/pages/index.astro index 3991fa5..b10f7ee 100644 --- a/apps/website/src/pages/index.astro +++ b/apps/website/src/pages/index.astro @@ -163,6 +163,24 @@ const features = [
+
@@ -198,7 +216,7 @@ const features = [ import '@quickdrawjs/core/quickdraw.css' export default function Board() { - return + return }`}
@@ -312,12 +330,19 @@ createQuickdraw({ const container = document.getElementById('hero-board') if (container) { - const board = createQuickdraw({ container, theme: 'light', grid: 'dots' }) + const board = createQuickdraw({ container, theme: 'light', grid: 'lines' }) const { store } = board.editor const saved = sessionStorage.getItem('qd-hero') if (saved) { try { store.loadSnapshot(JSON.parse(saved), 'remote') } catch {} } + // "draw something" nudge: only over an untouched board, gone at first touch + const hint = document.getElementById('hero-hint') + if (hint) { + const hide = () => hint.classList.add('off') + if (store.shapes().length > 0) hide() + else container.addEventListener('pointerdown', hide, { once: true }) + } let t store.listen(() => { clearTimeout(t) @@ -413,6 +438,67 @@ createQuickdraw({ box-shadow: 0 24px 60px -28px rgba(28, 27, 24, 0.28); overflow: hidden; background: #fff; } + #hero-hint { + position: absolute; inset: 0; z-index: 5; + display: flex; align-items: center; justify-content: center; + pointer-events: none; + } + #hero-hint.off { display: none; } + .hint-stage { + position: relative; + display: flex; flex-direction: column; align-items: center; gap: 2px; + } + /* the wrap is exactly the SVG's box, so the hand's offset-path (element + coordinates) and the squiggle (SVG viewBox) share one origin */ + .hint-doodle-wrap { position: relative; width: 220px; height: 90px; } + .hint-doodle { overflow: visible; display: block; } + .hint-squiggle { + stroke-dasharray: 1; + stroke-dashoffset: 1; + animation: hint-draw 3.2s ease-in-out infinite; + } + .hint-hand { + position: absolute; top: 0; left: 0; + /* same curve as .hint-squiggle, so the pencil tip rides the line */ + offset-path: path('M18 62 C 40 18, 62 18, 78 50 C 92 78, 110 78, 124 50 C 138 22, 158 22, 172 44 C 182 60, 194 62, 204 52'); + offset-rotate: 0deg; + /* offset-path anchors the glyph CENTER to the curve (Chrome has no + offset-anchor), so shift the box until the pencil tip — bottom-left + of the glyph — is what touches the line */ + transform: translate(14px, -14px); + animation: hint-trace 3.2s ease-in-out infinite; + filter: drop-shadow(0 3px 4px rgba(28, 27, 24, 0.25)); + } + @supports not (offset-path: path('M0 0 L1 1')) { + .hint-hand { display: none; } + } + .hint-pill { + display: inline-flex; align-items: center; gap: 8px; + padding: 9px 16px; border-radius: 999px; + background: rgba(255, 255, 255, 0.9); + border: 1.5px dashed color-mix(in srgb, var(--accent) 55%, transparent); + box-shadow: 0 10px 26px -14px rgba(28, 27, 24, 0.35); + backdrop-filter: blur(4px); + color: var(--ink); font-weight: 600; font-size: 14.5px; + } + /* line and pen share the same fade window (74–90%) so they vanish together */ + @keyframes hint-draw { + 0% { stroke-dashoffset: 1; opacity: 1; } + 62% { stroke-dashoffset: 0; opacity: 1; } + 74% { stroke-dashoffset: 0; opacity: 1; } + 90%, 100% { stroke-dashoffset: 0; opacity: 0; } + } + @keyframes hint-trace { + 0% { offset-distance: 0%; opacity: 0; } + 6% { opacity: 1; } + 62% { offset-distance: 100%; opacity: 1; } + 74% { offset-distance: 100%; opacity: 1; } + 90%, 100% { offset-distance: 100%; opacity: 0; } + } + @media (prefers-reduced-motion: reduce) { + .hint-squiggle { animation: none; stroke-dashoffset: 0; } + .hint-hand { animation: none; offset-distance: 100%; } + } .features .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(270px, 1fr)); gap: 18px; } .card { diff --git a/examples/react-demo/package.json b/examples/react-demo/package.json index 987dcb9..bec522d 100644 --- a/examples/react-demo/package.json +++ b/examples/react-demo/package.json @@ -9,7 +9,7 @@ "preview": "vite preview" }, "dependencies": { - "@quickdrawjs/react": "^0.1.3", + "@quickdrawjs/react": "^0.2.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/packages/core/package.json b/packages/core/package.json index f7046e5..2f03b2e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@quickdrawjs/core", - "version": "0.1.3", + "version": "0.2.0", "description": "Framework-free infinite-canvas whiteboard engine — the heart of Quickdraw, the MIT-licensed drawing SDK.", "type": "module", "main": "./src/index.js", diff --git a/packages/core/src/editor.js b/packages/core/src/editor.js index a24cc84..28bba64 100644 --- a/packages/core/src/editor.js +++ b/packages/core/src/editor.js @@ -32,11 +32,11 @@ const bendMidpoint = (pr) => { } export class Editor { - constructor({ container, store, theme = 'light', grid = 'none', readonly = false, camera, styles, geoKind } = {}) { + constructor({ container, store, theme = 'light', grid = 'lines', readonly = false, camera, styles, geoKind } = {}) { this.container = container this.store = store || new Store() this.theme = themeOf(theme) - this.grid = GRID_IDS.includes(grid) ? grid : 'none' + this.grid = GRID_IDS.includes(grid) ? grid : 'lines' this.readonly = !!readonly this.camera = camera || { x: 0, y: 0, z: 1 } this.styles = { ...DEFAULT_STYLES, ...(styles || {}) } @@ -1515,7 +1515,8 @@ export class Editor { // W/H are device px; the ctx must be untransformed. _drawGrid(ctx, cam, W, H, dpr) { if (this.grid === 'none' || !(cam.z > 0)) return - const g = this.theme.grid?.[this.grid === 'dots' ? 'dot' : 'line'] + // point-like marks (dots, crosses) carry less ink, so they use the darker ramp + const g = this.theme.grid?.[['dots', 'crosses'].includes(this.grid) ? 'dot' : 'line'] if (!g) return let step = GRID_STEP while (step * cam.z < 18) step *= 2 @@ -1532,32 +1533,75 @@ export class Editor { for (let m = m0, y = (m0 * step + cam.y) * z; y <= H; m++, y += step * z) rows.push([y, isMajor(m)]) ctx.save() - if (this.grid === 'lines') { + if (this.grid === 'lines' || this.grid === 'ruled') { for (const major of [false, true]) { ctx.beginPath() // half-pixel offsets keep a 1px rule on one device pixel, not two - for (const [x, m] of cols) if (m === major) { const p = Math.round(x) + 0.5; ctx.moveTo(p, 0); ctx.lineTo(p, H) } + if (this.grid === 'lines') { + for (const [x, m] of cols) if (m === major) { const p = Math.round(x) + 0.5; ctx.moveTo(p, 0); ctx.lineTo(p, H) } + } for (const [y, m] of rows) if (m === major) { const p = Math.round(y) + 0.5; ctx.moveTo(0, p); ctx.lineTo(W, p) } ctx.strokeStyle = major ? g.major : g.minor ctx.globalAlpha = fade ctx.lineWidth = 1 ctx.stroke() } - } else { + } else if (this.grid === 'crosses') { + // a small + at each intersection — the draughtsman's registration marks for (const major of [false, true]) { - const r = (major ? 2.3 : 1.6) * dpr + const arm = (major ? 4.5 : 3) * dpr ctx.beginPath() for (const [y, my] of rows) { + const py = Math.round(y) + 0.5 for (const [x, mx] of cols) { if ((mx && my) !== major) continue - ctx.moveTo(x + r, y) - ctx.arc(x, y, r, 0, Math.PI * 2) + const px = Math.round(x) + 0.5 + ctx.moveTo(px - arm, py); ctx.lineTo(px + arm, py) + ctx.moveTo(px, py - arm); ctx.lineTo(px, py + arm) } } - ctx.fillStyle = major ? g.major : g.minor + ctx.strokeStyle = major ? g.major : g.minor ctx.globalAlpha = fade - ctx.fill() + ctx.lineWidth = 1 + ctx.stroke() + } + } else if (this.grid === 'iso') { + // isometric weave: the two 30° diagonal families make a diamond lattice + // that stays self-aligned at every zoom. One quiet weight — major + // emphasis turns a woven field into noise. + const s = Math.tan(Math.PI / 6) // 30° from horizontal + ctx.beginPath() + for (const sign of [1, -1]) { + const slope = sign * s + // page-space intercepts k*step, mapped into device space + const b0 = -slope * cam.x + cam.y // device intercept of the k=0 line, /z + const lo = Math.min(0, -slope * (W / z)) // device-x range → intercept range + const hi = Math.max(H / z, H / z - slope * (W / z)) + const k0 = Math.ceil((lo - b0) / step) + const k1 = Math.floor((hi - b0) / step) + for (let k = k0; k <= k1; k++) { + const b = (b0 + k * step) * z + ctx.moveTo(0, b) + ctx.lineTo(W, b + slope * W) + } + } + ctx.strokeStyle = g.minor + ctx.globalAlpha = fade + ctx.lineWidth = 1 + ctx.stroke() + } else { + // one weight, one ink — emphasized dots read as stray marks on the paper + const r = 1.6 * dpr + ctx.beginPath() + for (const [y] of rows) { + for (const [x] of cols) { + ctx.moveTo(x + r, y) + ctx.arc(x, y, r, 0, Math.PI * 2) + } } + ctx.fillStyle = g.minor + ctx.globalAlpha = fade + ctx.fill() } ctx.restore() } diff --git a/packages/core/src/palette.js b/packages/core/src/palette.js index 98d75ae..71ebd36 100644 --- a/packages/core/src/palette.js +++ b/packages/core/src/palette.js @@ -78,9 +78,10 @@ export const THEMES = { }, } -// Board backdrops. 'none' is bare paper; the other two draw the same lattice, -// once as rules and once as the intersections only. -export const GRID_IDS = ['none', 'lines', 'dots'] +// Board backdrops. 'none' is bare paper; the rest dress the same lattice: +// full rules, notebook rules only, the intersections as dots or as small +// crosses (blueprint style), and an isometric triangle weave. +export const GRID_IDS = ['none', 'lines', 'ruled', 'dots', 'crosses', 'iso'] // Lattice spacing in page px at zoom 1, and the every-Nth emphasis. export const GRID_STEP = 40 export const GRID_MAJOR = 5 diff --git a/packages/core/src/quickdraw.css b/packages/core/src/quickdraw.css index 1bc9630..79f2c01 100644 --- a/packages/core/src/quickdraw.css +++ b/packages/core/src/quickdraw.css @@ -47,6 +47,32 @@ pointer-events: auto; } +/* history + selection actions: a vertical pill hugging the left edge. + Hosts with their own chrome there can move it via the custom props. */ +.qd-actions { + position: absolute; + left: var(--qd-actions-left, 10px); + top: var(--qd-actions-top, 50%); + transform: translateY(var(--qd-actions-shift, -50%)); + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + padding: 6px 4px; + border-radius: 999px; + background: rgba(19, 17, 13, 0.72); + border: 1px solid rgba(255, 255, 255, 0.14); + box-shadow: 0 4px 16px rgba(10, 8, 4, 0.25); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + pointer-events: auto; +} +.qd-actions button svg { width: 17px; height: 17px; display: block; } +.qd-actions .qd-div { width: 18px; height: 1px; margin: 3px 0; } +.qd-actions button:hover { background: rgba(255, 255, 255, 0.1); } +.qd-actions button:disabled { opacity: 0.32; pointer-events: none; } +.qd-actions .qd-tool { width: 30px; height: 30px; } + .qd-tool, .qd-dock button { width: 32px; height: 32px; @@ -106,6 +132,16 @@ padding: 8px; max-width: calc(100% - 16px); box-sizing: border-box; + /* rise out of the dock: a short fade + lift + settle */ + transform-origin: 50% 100%; + animation: qd-pop-in 160ms cubic-bezier(0.2, 0.9, 0.3, 1.15); +} +@keyframes qd-pop-in { + from { opacity: 0; transform: translateY(7px) scale(0.96); } + to { opacity: 1; transform: translateY(0) scale(1); } +} +@media (prefers-reduced-motion: reduce) { + .qd-popover { animation: none; } } /* every popover tool icon needs the explicit size — an unsized svg falls back @@ -233,6 +269,44 @@ .qd-seg-btn:hover { background: rgba(255, 255, 255, 0.12); color: #fff; } .qd-seg-btn.on { background: rgba(255, 255, 255, 0.92); color: #211d14; } +/* nested dropdown (grid backdrop): a flyout beside the parent row */ +.qd-mi-value { flex: 0 0 auto; color: rgba(255, 255, 255, 0.45); font-size: 11.5px; } +.qd-mi-chev, .qd-mi-check { flex: 0 0 14px; display: flex; color: rgba(255, 255, 255, 0.4); } +.qd-mi-chev svg, .qd-mi-check svg { width: 14px; height: 14px; } +.qd-mi-check { color: rgba(255, 255, 255, 0.85); } +.qd-has-sub { position: relative; } +.qd-has-sub.sub-open { background: rgba(255, 255, 255, 0.1); } +.qd-submenu { + position: absolute; + left: calc(100% + 2px); + top: -7px; /* first sub-row lines up with the parent row */ + display: none; + flex-direction: column; + min-width: 148px; + padding: 6px; + border-radius: 13px; + background: rgba(19, 17, 13, 0.85); + border: 1px solid rgba(255, 255, 255, 0.14); + box-shadow: 0 8px 28px rgba(10, 8, 4, 0.35); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + animation: qd-sub-in 130ms ease; +} +.qd-has-sub.sub-open .qd-submenu { display: flex; } +.qd-submenu.qd-sub-left { left: auto; right: calc(100% + 2px); } +@keyframes qd-sub-in { + from { opacity: 0; transform: translateX(-4px); } + to { opacity: 1; transform: none; } +} +.qd-submenu.qd-sub-left { animation-name: qd-sub-in-left; } +@keyframes qd-sub-in-left { + from { opacity: 0; transform: translateX(4px); } + to { opacity: 1; transform: none; } +} +@media (prefers-reduced-motion: reduce) { + .qd-submenu { animation: none; } +} + /* ---- watermark ---- */ /* Quiet by default, legible on hover; sits under the chrome so popovers and the dock always win. Theme-aware like the rest of the board. */ diff --git a/packages/core/src/store.js b/packages/core/src/store.js index abe691c..68ec28e 100644 --- a/packages/core/src/store.js +++ b/packages/core/src/store.js @@ -221,6 +221,13 @@ export class Store { this.remove(this.ids(), source) for (const rec of Object.values(recs)) if (rec && rec.id) this.put(rec, source) }, source) + // this is a document swap, not an edit: history built against the old + // document is invalid (its diffs reference records that no longer exist), + // and stale canUndo/canRedo would leave toolbars lit on a fresh board + this.undos.length = 0 + this.redos.length = 0 + this._batch = null + this._notifyHistory() } clear(source = 'user') { diff --git a/packages/core/src/ui.js b/packages/core/src/ui.js index 8b74bf8..3469615 100644 --- a/packages/core/src/ui.js +++ b/packages/core/src/ui.js @@ -43,17 +43,30 @@ const ICONS = { copy: SVG(''), fit: SVG(''), trash: SVG(''), + check: SVG(''), + chevronRight: SVG(''), + chevronLeft: SVG(''), sun: SVG(''), moon: SVG(''), } +// the action bar wears the same glyphs the menu already uses +ICONS.duplicate = ICONS.copy +ICONS.delete = ICONS.trash // grid backdrops: bare paper, ruled lines, dotted intersections const GRID_ICONS = { none: SVG(''), lines: SVG(''), + ruled: SVG(''), dots: SVG(''), + crosses: SVG(''), + iso: SVG(''), } -const GRID_TIPS = { none: 'No grid', lines: 'Grid lines', dots: 'Grid dots' } +const GRID_TIPS = { + none: 'No grid', lines: 'Grid lines', ruled: 'Ruled paper', + dots: 'Grid dots', crosses: 'Crosses', iso: 'Isometric', +} +const GRID_LABELS = { none: 'None', lines: 'Lines', ruled: 'Ruled', dots: 'Dots', crosses: 'Crosses', iso: 'Isometric' } const DASH_ICONS = { draw: SVG(''), @@ -73,13 +86,13 @@ const TIPS = { eraser: 'Eraser — E', laser: 'Laser — K', line: 'Line — L', arrow: 'Arrow — A', geo: 'Shape — G', text: 'Text — T', note: 'Sticky note — N', image: 'Insert image', undo: 'Undo — ⌘Z', redo: 'Redo — ⇧⌘Z', menu: 'Board menu', more: 'More tools', - tools: 'Tools', + tools: 'Tools', duplicate: 'Duplicate — ⌘D', delete: 'Delete — ⌫', } // dock buttons in visual order (styles/more/menu ride at the end, always) -const DOCK_NAMES = ['undo', 'redo', 'select', 'hand', 'draw', 'highlight', 'eraser', 'laser', 'line', 'arrow', 'geo', 'text', 'note', 'image'] +const DOCK_NAMES = ['select', 'hand', 'draw', 'highlight', 'eraser', 'laser', 'line', 'arrow', 'geo', 'text', 'note', 'image'] // what gives way first as the frame narrows (select and draw never yield) -const DROP_ORDER = ['redo', 'undo', 'hand', 'laser', 'line', 'note', 'image', 'highlight', 'text', 'arrow', 'eraser', 'geo'] +const DROP_ORDER = ['hand', 'laser', 'line', 'note', 'image', 'highlight', 'text', 'arrow', 'eraser', 'geo'] export function buildUI(editor, { hidden = false, onSave, themeToggle = true, gridControl = true } = {}) { const root = editor.container @@ -114,35 +127,30 @@ export function buildUI(editor, { hidden = false, onSave, themeToggle = true, gr // ---- actions ------------------------------------------------------------- const run = (name, b) => { - if (name === 'undo') return editor.store.undo() - if (name === 'redo') return editor.store.redo() if (name === 'image') { closePopover(); return editor.pickImage() } if (name === 'geo') return geoTap(b) closePopover() editor.setTool(name) } - // the shape button: tap arms the current kind, tap again opens the kinds + // the shape button: every tap arms the current kind AND shows the kinds, + // so picking a shape never takes a second hunt for the menu const geoTap = (b) => { - if (editor.tool === 'geo') { - openPopover('geo', (p) => { - p.classList.add('qd-geo-pop') - for (const g of GEO_IDS) { - const gb = el('button', 'qd-tool' + (editor.geoKind === g ? ' on' : '')) - gb.innerHTML = ICONS[g] - gb.title = g - gb.addEventListener('click', (ev) => { - ev.stopPropagation() - editor.setGeoKind(g) - editor.setTool('geo') - closePopover() - }) - p.appendChild(gb) - } - }, b) - } else { - closePopover() - editor.setTool('geo') - } + editor.setTool('geo') + openPopover('geo', (p) => { + p.classList.add('qd-geo-pop') + for (const g of GEO_IDS) { + const gb = el('button', 'qd-tool' + (editor.geoKind === g ? ' on' : '')) + gb.innerHTML = ICONS[g] + gb.title = g + gb.addEventListener('click', (ev) => { + ev.stopPropagation() + editor.setGeoKind(g) + editor.setTool('geo') + closePopover() + }) + p.appendChild(gb) + } + }, b) } const makeBtn = (name, onClick, cls = 'qd-tool') => { @@ -169,8 +177,6 @@ export function buildUI(editor, { hidden = false, onSave, themeToggle = true, gr } const divider = () => { const d = el('i', 'qd-div'); dock.appendChild(d); dividers.push(d) } - addBtn('undo'); addBtn('redo') - divider() addBtn('select'); addBtn('hand') divider() addBtn('draw'); addBtn('highlight'); addBtn('eraser'); addBtn('laser') @@ -198,16 +204,32 @@ export function buildUI(editor, { hidden = false, onSave, themeToggle = true, gr const menuBtn = makeBtn('menu', (e, b) => openPopover('menu', buildMenu, b)) dock.appendChild(menuBtn) + // ---- action bar ---------------------------------------------------------- + // history + selection actions ride their own small pill so they stay one + // tap away no matter how far the tool dock folds + const actionBar = el('div', 'qd-actions') + ui.appendChild(actionBar) + const actBtns = new Map() + const addAction = (name, fn) => { + const b = makeBtn(name, fn) + actionBar.appendChild(b) + actBtns.set(name, b) + return b + } + addAction('undo', () => editor.store.undo()) + addAction('redo', () => editor.store.redo()) + actionBar.appendChild(el('i', 'qd-div')) + addAction('duplicate', () => editor.duplicateSelection()) + addAction('delete', () => editor.deleteSelection()) + // ---- overflow grid (compact "more" / folded "tools") --------------------- function buildGrid(p, names) { p.classList.add('qd-grid-pop') for (const name of names) { - const isTool = !['undo', 'redo', 'image'].includes(name) + const isTool = name !== 'image' const b = makeBtn(name, (e, b2) => run(name, b2)) if (name === 'geo') b.innerHTML = ICONS[editor.geoKind] if (isTool && editor.tool === name) b.classList.add('on') - if (name === 'undo') b.disabled = !editor.store.canUndo - if (name === 'redo') b.disabled = !editor.store.canRedo p.appendChild(b) } } @@ -320,10 +342,62 @@ export function buildUI(editor, { hidden = false, onSave, themeToggle = true, gr if (opts.gridControl || opts.themeToggle) p.appendChild(el('i', 'qd-menu-div')) if (opts.gridControl) { - segment('Grid', GRID_IDS, { - icons: GRID_ICONS, tips: GRID_TIPS, current: editor.grid, - onPick: (id) => editor.setGrid(id), + // a standard nested dropdown: the row grows a flyout beside the menu — + // six buttons inline read as clutter + // a div, not a button: the flyout nests inside, and buttons can't nest + const row = el('div', 'qd-menu-item qd-has-sub') + row.setAttribute('role', 'button') + row.tabIndex = 0 + row.innerHTML = + `${GRID_ICONS[editor.grid]}` + + 'Grid' + + '' + + `${ICONS.chevronRight}` + row.querySelector('.qd-mi-value').textContent = GRID_LABELS[editor.grid] + + const sub = el('div', 'qd-submenu') + for (const id of GRID_IDS) { + const b = el('button', 'qd-menu-item') + b.innerHTML = + `${GRID_ICONS[id]}` + + '' + + `${editor.grid === id ? ICONS.check : ''}` + b.querySelector('.qd-mi-label').textContent = GRID_LABELS[id] + b.title = GRID_TIPS[id] + b.addEventListener('click', (e) => { + e.stopPropagation() + editor.setGrid(id) + sub.querySelectorAll('.qd-mi-check').forEach((c, i) => { c.innerHTML = GRID_IDS[i] === id ? ICONS.check : '' }) + row.querySelector('.qd-mi-ico').innerHTML = GRID_ICONS[id] + row.querySelector('.qd-mi-value').textContent = GRID_LABELS[id] + }) + sub.appendChild(b) + } + row.appendChild(sub) + + const openSub = () => { + row.classList.add('sub-open') + // side with room wins: nested menus prefer the right, but the board + // menu usually hugs the right edge of the frame + const rr = root.getBoundingClientRect() + const br = row.getBoundingClientRect() + const fitsRight = br.right + sub.offsetWidth + 12 <= rr.right + sub.classList.toggle('qd-sub-left', !fitsRight) + // grow upward when the row sits low in the frame + const fitsDown = br.top - 7 + sub.offsetHeight <= rr.bottom - 8 + sub.style.top = fitsDown ? '' : 'auto' + sub.style.bottom = fitsDown ? '' : '-7px' + } + const closeSub = () => row.classList.remove('sub-open') + let subT + row.addEventListener('mouseenter', () => { clearTimeout(subT); openSub() }) + row.addEventListener('mouseleave', () => { subT = setTimeout(closeSub, 180) }) + // tap toggles, for pointers that don't hover + row.addEventListener('click', (e) => { + e.stopPropagation() + row.classList.contains('sub-open') ? closeSub() : openSub() }) + p.appendChild(row) } if (opts.themeToggle) { segment('Theme', ['light', 'dark'], { @@ -371,8 +445,6 @@ export function buildUI(editor, { hidden = false, onSave, themeToggle = true, gr const extra = Math.max(0, slots - 5) const keep = new Set(['select', 'draw']) for (let i = DROP_ORDER.length - 1, n = extra; i >= 0 && n > 0; i--, n--) keep.add(DROP_ORDER[i]) - // undo/redo travel as a pair — half of it in the flyout reads as a bug - if (!keep.has('redo')) keep.delete('undo') hid = DOCK_NAMES.filter((n) => !keep.has(n)) } const changed = m !== mode || hid.join() !== hiddenNames.join() @@ -396,13 +468,16 @@ export function buildUI(editor, { hidden = false, onSave, themeToggle = true, gr function refresh() { for (const n of DOCK_NAMES) { const b = dockBtns.get(n) - if (['undo', 'redo', 'image'].includes(n)) continue + if (n === 'image') continue b.classList.toggle('on', editor.tool === n) } const geoBtn = dockBtns.get('geo') geoBtn.innerHTML = ICONS[editor.geoKind] - dockBtns.get('undo').disabled = !editor.store.canUndo - dockBtns.get('redo').disabled = !editor.store.canRedo + actBtns.get('undo').disabled = !editor.store.canUndo + actBtns.get('redo').disabled = !editor.store.canRedo + const hasSel = editor.selection.size > 0 + actBtns.get('duplicate').disabled = !hasSel + actBtns.get('delete').disabled = !hasSel // the folded button wears the active tool so the state stays visible toolsBtn.innerHTML = ICONS[editor.tool === 'geo' ? editor.geoKind : editor.tool] || ICONS.select toolsBtn.classList.toggle('on', popover?.name === 'tools') diff --git a/packages/core/test/editor.test.js b/packages/core/test/editor.test.js index be29905..c9c7f5a 100644 --- a/packages/core/test/editor.test.js +++ b/packages/core/test/editor.test.js @@ -324,19 +324,20 @@ function recordingCtx() { } describe('grid', () => { - it('defaults to none, switches, and emits', () => { + it('defaults to lines, switches, and emits', () => { const seen = [] editor.on('grid', () => seen.push(editor.grid)) - expect(editor.grid).toBe('none') - editor.setGrid('lines') - editor.setGrid('lines') // no-op, no event + expect(editor.grid).toBe('lines') + editor.setGrid('none') + editor.setGrid('none') // no-op, no event editor.setGrid('nonsense') // rejected editor.setGrid('dots') expect(editor.grid).toBe('dots') - expect(seen).toEqual(['lines', 'dots']) + expect(seen).toEqual(['none', 'dots']) }) it('draws nothing when off', () => { + editor.setGrid('none') const ctx = recordingCtx() editor._drawGrid(ctx, { x: 0, y: 0, z: 1 }, 400, 300, 1) expect(ctx.calls.length).toBe(0) @@ -370,21 +371,18 @@ describe('grid', () => { expect(stepAt(3)).toBe(20) }) - it('dots mark intersections, the every-fifth pair bigger', () => { + it('dots mark intersections in one uniform weight and ink', () => { editor.setGrid('dots') const ctx = recordingCtx() editor._drawGrid(ctx, { x: 0, y: 0, z: 1 }, 400, 200, 1) const arcs = ctx.calls.filter((c) => c[0] === 'arc') expect(arcs.length).toBe(11 * 6) - const radii = [...new Set(arcs.map((c) => c[3]))].sort((a, b) => a - b) - expect(radii).toEqual([1.6, 2.3]) + const radii = [...new Set(arcs.map((c) => c[3]))] + expect(radii).toEqual([1.6]) // dots carry less ink than rules, so they run darker to read as calm const fills = ctx.calls.filter((c) => c[0] === 'fill') + expect(fills.length).toBe(1) expect(fills[0][1]).toBe(editor.theme.grid.dot.minor) - expect(fills[1][1]).toBe(editor.theme.grid.dot.major) - // majors are where both axes land on a fifth: (0,0), (200,0), (400,0), (0,200)... - const big = arcs.filter((c) => c[3] === 2.3).map((c) => `${c[1]},${c[2]}`) - expect(big.sort()).toEqual(['0,0', '0,200', '200,0', '200,200', '400,0', '400,200']) }) it('travels with the camera', () => { @@ -523,7 +521,7 @@ describe('createQuickdraw UI', () => { const c2 = document.createElement('div') document.body.appendChild(c2) const board = createQuickdraw({ container: c2 }) - const btn = (n) => c2.querySelector(`.qd-dock button[data-name="${n}"]`) + const btn = (n) => c2.querySelector(`.qd-actions button[data-name="${n}"]`) expect(btn('undo').disabled).toBe(true) board.editor.setTool('draw') @@ -543,6 +541,30 @@ describe('createQuickdraw UI', () => { c2.remove() }) + it('duplicate/delete light up with a selection and act on it', () => { + const c2 = document.createElement('div') + document.body.appendChild(c2) + const board = createQuickdraw({ container: c2 }) + const btn = (n) => c2.querySelector(`.qd-actions button[data-name="${n}"]`) + expect(btn('duplicate').disabled).toBe(true) + expect(btn('delete').disabled).toBe(true) + + board.editor.setTool('draw') + drag(board.editor, [[10, 10], [40, 40], [80, 60]]) + const id = board.editor.store.shapes()[0].id + board.editor.setSelection([id]) + expect(btn('duplicate').disabled).toBe(false) + expect(btn('delete').disabled).toBe(false) + + btn('duplicate').click() + expect(board.editor.store.shapes().length).toBe(2) + btn('delete').click() + expect(board.editor.store.shapes().length).toBe(1) + expect(btn('duplicate').disabled).toBe(true) + board.destroy() + c2.remove() + }) + it('the board menu switches theme and grid', () => { const c2 = document.createElement('div') document.body.appendChild(c2) @@ -557,11 +579,32 @@ describe('createQuickdraw UI', () => { expect(board.editor.theme.id).toBe('dark') expect(btns('Theme')[1].classList.contains('on')).toBe(true) - expect(board.editor.grid).toBe('none') - btns('Grid')[1].click() + // grid is a nested dropdown: the row shows the current value… expect(board.editor.grid).toBe('lines') - btns('Grid')[2].click() + const gridRow = c2.querySelector('.qd-has-sub') + expect(gridRow.textContent).toContain('Grid') + expect(gridRow.textContent).toContain('Lines') + expect(gridRow.classList.contains('sub-open')).toBe(false) + gridRow.click() + expect(gridRow.classList.contains('sub-open')).toBe(true) + // …and the flyout lists every backdrop with a check on the current one + const options = [...gridRow.querySelectorAll('.qd-submenu .qd-menu-item')] + expect(options.length).toBe(6) + expect(options[1].querySelector('.qd-mi-check').innerHTML).not.toBe('') + options[0].click() + expect(board.editor.grid).toBe('none') + options[3].click() expect(board.editor.grid).toBe('dots') + options[5].click() + expect(board.editor.grid).toBe('iso') + // picking keeps the flyout open and refreshes the check + parent value + expect(gridRow.classList.contains('sub-open')).toBe(true) + expect(options[5].querySelector('.qd-mi-check').innerHTML).not.toBe('') + expect(options[1].querySelector('.qd-mi-check').innerHTML).toBe('') + expect(gridRow.querySelector('.qd-mi-value').textContent).toBe('Isometric') + // tapping the parent row again folds the flyout + gridRow.click() + expect(gridRow.classList.contains('sub-open')).toBe(false) board.destroy() c2.remove() @@ -576,9 +619,10 @@ describe('createQuickdraw UI', () => { // and back on again, live board.ui.setOptions({ gridControl: true }) c2.querySelector('.qd-dock button[data-name="menu"]').click() - const rows = [...c2.querySelectorAll('.qd-menu-row')] - expect(rows.length).toBe(1) - expect(rows[0].textContent).toContain('Grid') + const gridRow = [...c2.querySelectorAll('.qd-menu-item')] + .find((r) => r.textContent.trim().startsWith('Grid')) + expect(gridRow).toBeTruthy() + expect(c2.querySelectorAll('.qd-menu-row').length).toBe(0) // theme still off board.destroy() c2.remove() }) diff --git a/packages/core/test/palette.test.js b/packages/core/test/palette.test.js index 6fa1716..de2b655 100644 --- a/packages/core/test/palette.test.js +++ b/packages/core/test/palette.test.js @@ -36,7 +36,7 @@ describe('palette', () => { }) it('every theme carries two-weight grid ink for rules and dots', () => { - expect(GRID_IDS).toEqual(['none', 'lines', 'dots']) + expect(GRID_IDS).toEqual(['none', 'lines', 'ruled', 'dots', 'crosses', 'iso']) const alpha = (c) => parseFloat(c.split(',').pop()) for (const theme of Object.values(THEMES)) { for (const kind of ['line', 'dot']) { diff --git a/packages/core/test/store.test.js b/packages/core/test/store.test.js index f81dd52..81b9a88 100644 --- a/packages/core/test/store.test.js +++ b/packages/core/test/store.test.js @@ -265,6 +265,21 @@ describe('snapshots', () => { expect(s.ids()).toEqual(['a']) }) + it('loadSnapshot resets history — a document swap invalidates old undo/redo', () => { + const s = new Store() + s.put(shape('old')) + s.undo() + expect(s.canRedo).toBe(true) + s.put(shape('other')) + expect(s.canUndo).toBe(true) + let notified = 0 + s.listenHistory(() => notified++) + s.loadSnapshot({ document: { store: { a: shape('a') } } }) + expect(s.canUndo).toBe(false) + expect(s.canRedo).toBe(false) + expect(notified).toBeGreaterThan(0) // toolbars must hear about the reset + }) + it('clear empties the store (undoably for users)', () => { const s = new Store() s.put(shape('a')) diff --git a/packages/react-native/package.json b/packages/react-native/package.json index f2dd498..2abc17d 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -1,6 +1,6 @@ { "name": "@quickdrawjs/react-native", - "version": "0.1.3", + "version": "0.2.0", "description": "React Native component for Quickdraw — the MIT-licensed infinite-canvas whiteboard SDK (WebView-based).", "type": "module", "main": "./src/index.js", @@ -38,7 +38,7 @@ "excalidraw-alternative" ], "dependencies": { - "@quickdrawjs/core": "0.1.3" + "@quickdrawjs/core": "0.2.0" }, "devDependencies": { "esbuild": "^0.25.0" diff --git a/packages/react-native/src/index.js b/packages/react-native/src/index.js index 9c1e226..d705b9e 100644 --- a/packages/react-native/src/index.js +++ b/packages/react-native/src/index.js @@ -17,7 +17,7 @@ export { createBridge, encodeDispatch } from './bridge.js' * * props: * theme 'light' | 'dark' (live-switchable) - * grid 'none' | 'lines' | 'dots' — the backdrop (live-switchable) + * grid 'none' | 'lines' | 'ruled' | 'dots' | 'crosses' | 'iso' — the backdrop (live-switchable) * readonly lock input (also hides the toolbar) * hideUi hide the stock toolbar * themeToggle show the theme switch in the board menu (default true) @@ -44,7 +44,7 @@ export { createBridge, encodeDispatch } from './bridge.js' export const Quickdraw = forwardRef(function Quickdraw(props, ref) { const { theme = 'light', - grid = 'none', + grid = 'lines', readonly = false, hideUi = false, themeToggle = true, diff --git a/packages/react/package.json b/packages/react/package.json index 85787e6..ae3d84e 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@quickdrawjs/react", - "version": "0.1.3", + "version": "0.2.0", "description": "React component for Quickdraw — the MIT-licensed infinite-canvas whiteboard SDK.", "type": "module", "main": "./src/index.js", @@ -30,7 +30,7 @@ "excalidraw-alternative" ], "dependencies": { - "@quickdrawjs/core": "0.1.3" + "@quickdrawjs/core": "0.2.0" }, "peerDependencies": { "react": ">=17" diff --git a/packages/react/src/index.js b/packages/react/src/index.js index c64b607..4e2f07e 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -13,7 +13,7 @@ export * from '@quickdrawjs/core' * * props: * theme 'light' | 'dark' (live-switchable) - * grid 'none' | 'lines' | 'dots' — the backdrop (live-switchable) + * grid 'none' | 'lines' | 'ruled' | 'dots' | 'crosses' | 'iso' — the backdrop (live-switchable) * readonly lock input (also hides the toolbar) * hideUi hide the stock toolbar (bring your own chrome) * themeToggle show the theme switch in the board menu (default true) @@ -37,7 +37,7 @@ export * from '@quickdrawjs/core' export const Quickdraw = forwardRef(function Quickdraw(props, ref) { const { theme = 'light', - grid = 'none', + grid = 'lines', readonly = false, hideUi = false, themeToggle = true,