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
7 changes: 5 additions & 2 deletions frontend/src/components/canvas/SelectionLayer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import { isTextElement, selectionOutline, handleCenter, NEUTRAL_SELECT } from '@
import { cornerRadiusOf, supportsCornerRounding, maxCornerRadius } from '@/diagram/shapeGeometry.js'
import { arrowProportions } from '@/diagram/blockArrow.js'

const HANDLE = 12
// Smaller than the shape's own outline stroke would suggest is legible — the
// Slides-style discoverable-but-quiet affordance #550 asks for, not a control
// that reads as bigger than the box it resizes.
const HANDLE = 8
const ROTATION_ARM = 28
const HANDLES = [
'top-left', 'top', 'top-right', 'right',
Expand Down Expand Up @@ -316,7 +319,7 @@ function startRotate(event) {
:y="handlePosition(name).y - handleSize / 2"
:width="handleSize"
:height="handleSize"
:rx="2.5 / zoom"
:rx="1.5 / zoom"
fill="#FFFFFF"
:stroke="handleColor"
:stroke-width="strokeWidth"
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/components/canvas/ShapeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,11 @@ const labelFill = computed(() => {
return textStyle.value.color
})

// Shrink-to-fit the rich text when the shape opts in (spec 6.4). Declared last so
// the inputs getter can reference the computeds above without hitting the TDZ.
// Shrink-to-fit the rich text, always on (#550 — no opt-in toggle any more).
// Declared last so the inputs getter can reference the computeds above without
// hitting the TDZ.
useAutoFitText(richEl, () => ({
enabled: props.shape.text?.autoFit && !isEditingThis.value,
enabled: !isEditingThis.value,
base: props.shape.text?.style?.size || 16,
w: textArea.value?.w,
h: textArea.value?.h,
Expand Down
19 changes: 11 additions & 8 deletions frontend/src/components/canvas/WhiteboardStickyNote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { safeHref } from '@/utils/safeUrl.js'
import { stickyRuns, stickyTextStyle } from '@/diagram/whiteboardModel.js'
import { domToRuns, runsToDom } from '@/utils/richTextDom.js'
import { runsToText, trimRuns } from '@/diagram/richText.js'
import { NEUTRAL_SELECT } from '@/diagram/selectionChrome.js'
import { roughenRect } from '@/diagram/sketch.js'
import { pointsToPath } from '@/diagram/svgPath.js'
import {
Expand Down Expand Up @@ -319,14 +320,14 @@ function openLink(event) {
:height="note.h"
rx="4"
:fill="note.color"
:stroke="selected ? '#006EDB' : 'rgba(0,0,0,0.08)'"
:stroke="selected ? NEUTRAL_SELECT : 'rgba(0,0,0,0.08)'"
:stroke-width="selected ? 1.5 : 1"
style="cursor: move; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.12))"
@pointerdown="startMove"
/>
<g v-else style="cursor: move" @pointerdown="startMove">
<rect :width="note.w" :height="note.h" rx="4" :fill="note.color" />
<path :d="sketchOutline" fill="none" :stroke="selected ? '#006EDB' : 'rgba(0,0,0,0.35)'" stroke-width="1.5" />
<path :d="sketchOutline" fill="none" :stroke="selected ? NEUTRAL_SELECT : 'rgba(0,0,0,0.35)'" stroke-width="1.5" />
</g>

<!-- Editable text (auto-contrast ink). -->
Expand All @@ -352,15 +353,17 @@ function openLink(event) {
<path d="M4 7 a3 3 0 0 1 3 -3 M10 7 a3 3 0 0 1 -3 3" stroke="#006EDB" stroke-width="1.3" fill="none" stroke-linecap="round" transform="translate(0 0)" />
</g>

<!-- Resize handle (bottom-right), shown only for a lone selection. -->
<!-- Resize handle (bottom-right), shown only for a lone selection. Same
size and neutral grey as every other object's handles (#550). -->
<rect
v-if="solo"
:x="note.w - 12"
:y="note.h - 12"
width="12"
height="12"
:x="note.w - 8"
:y="note.h - 8"
width="8"
height="8"
rx="1.5"
fill="#FFFFFF"
stroke="#006EDB"
:stroke="NEUTRAL_SELECT"
stroke-width="1.5"
style="cursor: nwse-resize"
@pointerdown="startResize"
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/components/toolbar/groups/StickyGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,16 @@ function removeSticky() {

<!-- Text colour, which is a different axis from the note's PAPER colour above.
The paper stays the six named fills from the spec, deliberately out of the
palette sweep (#495); the ink is on the shared grid like every other text. -->
palette sweep (#495); the ink is on the shared grid like every other text.
The trigger is the "A" every other text object wears (#550): a filled disc
here read as one more paper swatch, on a bar whose first six controls ARE
paper swatches. The label stays note-specific so it cannot collide with
TextGroup's "Text colour" in a name lookup. -->
<Popover>
<template #trigger>
<ToolbarButton label="Note text colour">
<template #icon>
<span class="size-4 rounded-full" :style="{ background: textStyle.color }" />
<span class="grid size-4 place-items-center rounded text-sm font-semibold" :style="{ color: textStyle.color }">A</span>
</template>
</ToolbarButton>
</template>
Expand Down
64 changes: 49 additions & 15 deletions frontend/src/components/toolbar/groups/TextGroup.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<script setup>
// Text formatting for the current block selection (#361): font, size, the four
// marks, alignment, colour and auto-fit.
// marks, alignment and colour. Fitting text to its shape is automatic and no
// longer a control here (#550) — see useAutoFitText.
//
// This group is shown for a shape whether or not its label is being edited. It
// IS the text-only menu while editing (#259), and part of the shape menu
// otherwise — which is why every control drives the live rich-text editor when
// one is open and the shape-level base style when it is not.
import { computed } from 'vue'
import { Popover, Select } from 'frappe-ui'
import { computed, ref, watch } from 'vue'
import { Popover, Select, TextInput } from 'frappe-ui'
import { useBlockSelection } from '@/composables/useBlockSelection.js'
import { richCommands, isMarkActive } from '@/composables/useRichText.js'
import EspressoSwatchGrid from '@/components/palette-right/EspressoSwatchGrid.vue'
Expand Down Expand Up @@ -64,9 +65,16 @@ const textAlign = computed(() => textRef.value?.text?.align || 'center')
// on every shape that has never had a font chosen — which is most of them.
const font = computed(() => textStyle.value.font || ESPRESSO_SANS)
const fontSize = computed(() => textStyle.value.size ?? 16)
const autoFit = computed(() => Boolean(textRef.value?.text?.autoFit))
const currentTextColor = computed(() => textStyle.value.color || '#171717')

// The typed draft, kept separate from `fontSize` so a half-typed value (the "3"
// on its way to "32") never round-trips through the store. Re-synced whenever
// the committed size changes from elsewhere (the +/- steppers, another shape).
const fontSizeDraft = ref(String(fontSize.value))
watch(fontSize, (value) => {
fontSizeDraft.value = String(value)
})

function updateTextStyle(patch) {
if (textIds.value.length) store.updateShapes(textIds.value, { text: { style: patch } })
}
Expand Down Expand Up @@ -109,8 +117,30 @@ function setFont(value) {
updateTextStyle({ font: value })
}

function toggleAutoFit() {
if (textIds.value.length) store.updateShapes(textIds.value, { text: { autoFit: !autoFit.value } })
// Applies the typed draft as the shape's base font size (#550): same clamp as
// the +/- steppers, so typing "9999" lands at the same ceiling clicking + a
// hundred times would. A draft that isn't a positive number is discarded —
// the input snaps back to the last committed size via the `fontSize` watcher.
//
// Committed on Enter only, never on blur (matching ZoomGroup's typed-value
// control) — clicking a different shape while a size is half-typed fires blur
// AFTER the canvas's pointerdown has already moved the selection, which would
// otherwise write the typed size onto whatever got clicked instead of the
// shape the user was actually editing.
function commitFontSizeDraft() {
const parsed = Number(fontSizeDraft.value)
if (Number.isFinite(parsed) && parsed > 0) {
updateTextStyle({ size: Math.max(6, Math.min(200, Math.round(parsed))) })
} else {
fontSizeDraft.value = String(fontSize.value)
}
}

// Blur with no Enter is a cancel: an edit abandoned by clicking elsewhere
// reverts rather than silently committing to a shape the click may have
// just changed the selection to.
function resetFontSizeDraft() {
fontSizeDraft.value = String(fontSize.value)
}

// Recolours the caret selection live while editing, else sets the shape's base
Expand All @@ -127,7 +157,18 @@ function setTextColor(hex) {

<div class="flex items-center rounded-md border border-outline-gray-2">
<ToolbarButton class="!w-6" label="Decrease font size" icon="lucide-minus" @click="stepFontSize(-1)" />
<span class="w-6 text-center text-sm tabular-nums text-ink-gray-8">{{ fontSize }}</span>
<TextInput
v-model="fontSizeDraft"
type="text"
size="sm"
variant="ghost"
inputmode="numeric"
aria-label="Font size"
class="w-9 [&_input]:px-0 [&_input]:text-center [&_input]:tabular-nums"
@focus="$event.target.select()"
@keydown.enter="commitFontSizeDraft(); $event.target.blur()"
@blur="resetFontSizeDraft"
/>
<ToolbarButton class="!w-6" label="Increase font size" icon="lucide-plus" @click="stepFontSize(1)" />
</div>

Expand Down Expand Up @@ -166,7 +207,7 @@ function setTextColor(hex) {
<template #trigger>
<ToolbarButton label="Text colour">
<template #icon>
<span class="grid size-4 place-items-center rounded text-xs font-semibold" :style="{ color: currentTextColor }">A</span>
<span class="grid size-4 place-items-center rounded text-sm font-semibold" :style="{ color: currentTextColor }">A</span>
</template>
</ToolbarButton>
</template>
Expand All @@ -176,11 +217,4 @@ function setTextColor(hex) {
</div>
</template>
</Popover>

<ToolbarButton
label="Auto-fit text to shape"
icon="lucide-scaling"
:active="autoFit"
@click="toggleAutoFit"
/>
</template>
8 changes: 4 additions & 4 deletions frontend/src/composables/useAutoFitText.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Shrink-to-fit text inside a shape (spec 6.4). When a shape's text.autoFit is
// on, the rendered text is scaled DOWN from its base font size until it fits the
// box (height first, then width), never below a floor. Growing past the base
// size is intentionally not done here — base size is the ceiling.
// Shrink-to-fit text inside a shape (spec 6.4). Always on for a shape that isn't
// being edited (#550) — the rendered text is scaled DOWN from its base font size
// until it fits the box (height first, then width), never below a floor. Growing
// past the base size is intentionally not done here — base size is the ceiling.
//
// Driven from ShapeView: pass the inner text element ref and a getter for the
// fit inputs. We re-measure whenever the box, content, or base size changes.
Expand Down
Loading