Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.7.8
0.7.9
4 changes: 2 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
href="https://fonts.googleapis.com/css2?family=Caveat:wght@700&family=Fraunces:ital,opsz,wght@0,9..144,100..900;1,9..144,100..900&family=Geist+Mono:wght@100..900&family=Geist:wght@100..900&family=Caveat+Brush&display=swap"
rel="stylesheet"
/>
<title>Craftbase - minimal whiteboard for builders</title>
<!-- Global site tag (gtag.js) - Google Analytics -->
<title>Craftbase - Open-source online whiteboard for developers</title>

<script
defer
src="https://cloud.umami.is/script.js"
Expand Down
15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "craftbase",
"version": "0.7.8",
"version": "0.7.9",
"private": true,
"main": "src/lib.ts",
"module": "src/lib.ts",
Expand Down Expand Up @@ -34,8 +34,9 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-responsive": "10.0.0",
"react-router": "^6.30.3",
"react-router-dom": "^6.30.3",
"react-router": "^6.30.4",
"react-router-dom": "^6.30.4",
"shell-quote": "^1.8.4",
"styled-components": "^6.1.11",
"subscriptions-transport-ws": "^0.11.0",
"two.js": "^0.8.13",
Expand All @@ -57,7 +58,9 @@
"form-data": ">=3.0.4",
"brace-expansion": ">=5.0.6",
"lodash": ">=4.18.0",
"postcss": ">=8.5.10"
"postcss": ">=8.5.10",
"shell-quote": ">=1.8.4",
"vite": "~6.4.3"
},
"eslintConfig": {
"extends": "react-app/base"
Expand Down Expand Up @@ -91,9 +94,9 @@
"jsdom": "^25.0.0",
"tailwindcss": "^3.4.4",
"typescript": "^6.0.3",
"vite": "^6.4.2",
"vite": "^6.4.3",
"vite-plugin-svgr": "^5.2.0",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^3.2.4"
"vitest": "^3.2.6"
}
}
2 changes: 1 addition & 1 deletion src/assets/blue_star.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/brainstorming_craftbase.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/assets/customize.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/assets/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/assets/no_signup.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/assets/right_arrow_white.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/assets/sticker.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed src/assets/whiteboarding.png
Binary file not shown.
5 changes: 5 additions & 0 deletions src/canvas/selectionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '../utils/textLayout'
import { DEFAULT_TEXT_FONT_FAMILY } from '../constants/misc'
import { getConnectorsEnabled } from '../utils/featureFlags'
import { markSelectionChrome } from '../utils/svgExportShared'

// Two.js scene shapes carry codebase-specific bookkeeping (elementData,
// _renderer, etc.) outside the published types. Stay loose here; Stage 12
Expand Down Expand Up @@ -556,6 +557,10 @@ export default class SelectionController {
this.domElement.classList.add('shape-selected')
this.syncToTarget()
this.two.update()
// Tag the overlay so exports strip it. The `ui` lives at scene level
// (not in any element group), so it only reaches the full-scene PNG
// export — harmless to tag for the per-selection SVG export too.
markSelectionChrome(this.ui)

this._attachHoverListener()
this.callbacks.onSelect(
Expand Down
5 changes: 3 additions & 2 deletions src/components/elements/arrowLine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useImmer } from 'use-immer'
import Two from 'two.js'

import ElementCreator from '../../factory/arrowLine'
import { readOpacity } from '../../utils/canvasUtils'

import {
updateX1Y1Vertices,
Expand Down Expand Up @@ -98,7 +99,7 @@ function ArrowLine(props: ElementProps): ReactElement {
line,
} = elementFactory.createElement()
group.elementData = { ...props.itemData, ...props }
line.opacity = props.metadata?.opacity ?? 1
line.opacity = readOpacity(props)
if (props.stroke) line.stroke = props.stroke
if (props.linewidth) line.linewidth = props.linewidth

Expand Down Expand Up @@ -219,7 +220,7 @@ function ArrowLine(props: ElementProps): ReactElement {
const lineInstance = internalState.line.data
groupInstance.translation.x = props.x
groupInstance.translation.y = props.y
lineInstance.opacity = props.metadata?.opacity ?? 1
lineInstance.opacity = readOpacity(props)
two.update()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
4 changes: 2 additions & 2 deletions src/components/elements/circle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useBoardContext } from '../../views/Board/boardContext'

import CircleFactory from '../../factory/circle'
import { strokeTypeToDashes } from '../../utils/misc'
import { applyShapeText } from '../../utils/canvasUtils'
import { applyShapeText, readOpacity } from '../../utils/canvasUtils'
import { componentTypes } from '../../constants/misc'

// Element components receive a fluid prop bag composed of the ComponentRecord
Expand Down Expand Up @@ -34,7 +34,7 @@ function Circle(props: ElementProps): ReactElement {
})
const { group, circle } = elementFactory.createElement()
group.elementData = { ...props.itemData, ...props }
const opacityValue = props.metadata?.opacity ?? 1
const opacityValue = readOpacity(props)

if (props.parentGroup) {
const parentGroup = props.parentGroup
Expand Down
4 changes: 2 additions & 2 deletions src/components/elements/diamond.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useBoardContext } from '../../views/Board/boardContext'

import ElementFactory from '../../factory/diamond'
import { strokeTypeToDashes } from '../../utils/misc'
import { applyShapeText } from '../../utils/canvasUtils'
import { applyShapeText, readOpacity } from '../../utils/canvasUtils'
import { componentTypes } from '../../constants/misc'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -29,7 +29,7 @@ function Diamond(props: ElementProps): ReactElement {
})
const { group, diamond } = elementFactory.createElement()
group.elementData = { ...props.itemData, ...props }
const opacityValue = props.metadata?.opacity ?? 1
const opacityValue = readOpacity(props)

if (props.parentGroup) {
const parentGroup = props.parentGroup
Expand Down
3 changes: 2 additions & 1 deletion src/components/elements/divider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useImmer } from 'use-immer'
import { strokeTypeToDashes } from '../../utils/misc'

import ElementCreator from '../../factory/divider'
import { readOpacity } from '../../utils/canvasUtils'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ElementProps = any
Expand Down Expand Up @@ -85,7 +86,7 @@ function Divider(props: ElementProps): ReactElement {
const { group, pointCircle1, pointCircle2, line } =
elementFactory.createElement()
group.elementData = { ...props.itemData, ...props }
line.opacity = props.metadata?.opacity ?? 1
line.opacity = readOpacity(props)

if (props.parentGroup) {
const parentGroup = props.parentGroup
Expand Down
3 changes: 2 additions & 1 deletion src/components/elements/geoText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
MOBILE_TEXT_SIZES_OBJECT,
} from '../../utils/constants'
import { lineHeightFor } from '../../utils/textLayout'
import { readOpacity } from '../../utils/canvasUtils'
import { useMediaQueryUtils } from '../../constants/exportHooks'
import { computeCounterScale } from '../../utils/counterScale'
import {
Expand Down Expand Up @@ -113,7 +114,7 @@ function GeoText(props: ElementProps): ReactElement {
const elementFactory = new NewTextFactory(two, prevX, prevY, props)
const { group, twoText } = elementFactory.createElement()
group.elementData = { ...props.itemData, ...props }
twoText.opacity = props.metadata?.opacity ?? 1
twoText.opacity = readOpacity(props)

twoTextRef.current = twoText
groupObject = group
Expand Down
60 changes: 28 additions & 32 deletions src/components/elements/groupobject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import React, { useEffect, useState, useRef } from 'react'
import type { ReactElement } from 'react'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const factoryModules: Record<string, () => Promise<any>> =
import.meta.glob('../../factory/*.ts')
const factoryModules: Record<string, () => Promise<any>> = import.meta.glob(
'../../factory/*.ts'
)
import Two from 'two.js'
import { useBoardContext } from '../../views/Board/boardContext'
import getEditComponents from '../utils/editWrapper'
import { elementOnBlurHandler } from '../../utils/misc'
import { DEFAULT_TEXT_FONT_FAMILY } from '../../constants/misc'
import { layoutStandaloneText } from '../../utils/canvasUtils'
import {
applyShapeText,
layoutStandaloneText,
readOpacity,
} from '../../utils/canvasUtils'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ElementProps = any
Expand Down Expand Up @@ -103,9 +107,7 @@ function GroupedObjectWrapper(props: ElementProps): ReactElement {
two.scene.children.forEach((element: ShapeLike) => {
if (!element.elementData) return
if (!childrenIds.includes(element.elementData.id)) return
const elMeta = element.elementData.metadata
element.opacity =
elMeta && !Array.isArray(elMeta) ? (elMeta.opacity ?? 1) : 1
element.opacity = readOpacity(element.elementData)
})
}

Expand Down Expand Up @@ -242,12 +244,7 @@ function GroupedObjectWrapper(props: ElementProps): ReactElement {
foundOriginalCount++
// Reveal the (now position-synced) original: restore its own
// group-level opacity — it was hidden at 0 under the overlay.
// metadata may be a pencil vertex array, so guard the read.
const elMeta = element.elementData.metadata
element.opacity =
elMeta && !Array.isArray(elMeta)
? (elMeta.opacity ?? 1)
: 1
element.opacity = readOpacity(element.elementData)
}
})
two.update()
Expand Down Expand Up @@ -275,9 +272,7 @@ function GroupedObjectWrapper(props: ElementProps): ReactElement {
childMetadata = child.metadata.map(
(vert: ShapeLike, index: number) => {
const lwProp =
vert.lw !== undefined
? { lw: vert.lw }
: {}
vert.lw !== undefined ? { lw: vert.lw } : {}
if (index === 0) {
return { x: absX, y: absY, ...lwProp }
}
Expand Down Expand Up @@ -440,9 +435,7 @@ function GroupedObjectWrapper(props: ElementProps): ReactElement {
const coreObject = factoryObject.group
coreObject.translation.x = item.x
coreObject.translation.y = item.y
if (item.metadata?.opacity !== undefined) {
coreObject.opacity = item.metadata.opacity
}
coreObject.opacity = readOpacity(item)

// Standalone text: the factory makes ONE Two.Text from the raw
// content, but SVG collapses `\n` to a single line. Re-lay it out
Expand All @@ -457,18 +450,23 @@ function GroupedObjectWrapper(props: ElementProps): ReactElement {
)
}

// Shape-with-text (rectangle/diamond/circle): re-materialise the
// embedded text the SAME way the shape components do on mount —
// via `applyShapeText`, which reflows the raw content to the box
// width and renders it as a stacked multiline text layer. The old
// single `two.makeText(textContent)` collapsed `\n` into one line
// (SVG <text> ignores newlines), so a grouped shape with multiline
// text spilled out of its container. Mirrors the standalone-text
// (`newText`) handling above.
const meta = item.metadata || {}
if (meta.hasText && meta.textContent) {
const twoText = two.makeText(meta.textContent, 0, 0)
twoText.fill = meta.textFill || '#000'
twoText.size = meta.textFontSize || 24
twoText.alignment = 'center'
twoText.baseline = meta.textBaseLine || 'middle'
twoText.family =
meta.textFontFamily ||
meta.textFamily ||
DEFAULT_TEXT_FONT_FAMILY
coreObject.add(twoText)
applyShapeText(
two,
coreObject,
item.componentType,
item.width || meta.width || 120,
meta
)
}

coreObject.elementData = item
Expand Down Expand Up @@ -559,9 +557,7 @@ function GroupedObjectWrapper(props: ElementProps): ReactElement {
// Delete-key path sets isDeletingRef and owns its teardown, so we skip then.
useEffect(() => {
const memberIds = new Set(
(props.children ?? [])
.map((c: ShapeLike) => c?.id)
.filter(Boolean)
(props.children ?? []).map((c: ShapeLike) => c?.id).filter(Boolean)
)
const onMemberRemoved = ((e: CustomEvent<{ id: string }>): void => {
if (isDeletingRef.current) return
Expand Down
10 changes: 5 additions & 5 deletions src/components/elements/newText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useImmer } from 'use-immer'
import { useBoardContext } from '../../views/Board/boardContext'

import NewTextFactory from '../../factory/newText'
import { syncTextHitRect } from '../../utils/canvasUtils'
import { syncTextHitRect, readOpacity } from '../../utils/canvasUtils'
import { lineHeightFor } from '../../utils/textLayout'
import { htmlToBulletText } from '../../utils/htmlToBulletText'
import { DEFAULT_TEXT_FONT_FAMILY } from '../../constants/misc'
Expand Down Expand Up @@ -49,7 +49,7 @@ function NewText(props: ElementProps): ReactElement {
const elementFactory = new NewTextFactory(two, prevX, prevY, props)
const { group, twoText } = elementFactory.createElement()
group.elementData = { ...props.itemData, ...props }
twoText.opacity = props.metadata?.opacity ?? 1
twoText.opacity = readOpacity(props)

twoTextRef.current = twoText

Expand Down Expand Up @@ -252,10 +252,10 @@ function NewText(props: ElementProps): ReactElement {
input.style.padding = `${vertPad}px 8px`
input.style.color = twoText.fill || '#3A342C'
// Match the element's current opacity so the editor doesn't flash to
// full opacity on entering edit mode. The opacity handler stores it
// on metadata (and applies it at group level), so read that.
// full opacity on entering edit mode. Opacity persists in the
// top-level `opacity` field (legacy rows fall back to metadata).
input.style.opacity = String(
group.elementData?.metadata?.opacity ?? group.opacity ?? 1
readOpacity(group.elementData) ?? group.opacity ?? 1
)
input.style.fontSize = `${cssFontSize}px`
input.style.fontFamily = twoText.family || DEFAULT_TEXT_FONT_FAMILY
Expand Down
Loading
Loading