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
28 changes: 4 additions & 24 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 12 additions & 28 deletions features/board/utils/sticky-note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import {
type StickyColorName,
} from '@/shared/constants';

const STICKY_TEXT_PADDING = 30;
const STICKY_TEXT_PADDING = 28;
const STICKY_LINE_HEIGHT = 22;
const STICKY_CHAR_WIDTH = 8;
const MIN_STICKY_SIZE = 130;
const MAX_STICKY_SIZE = 320;
const MAX_STICKY_WIDTH = 280;

export function isStickyColorName(value: string): value is StickyColorName {
return value in STICKY_COLORS;
Expand All @@ -28,18 +28,16 @@ export function estimateStickySize(text: string): {

const lines = text.split(/\r?\n/);
const longestLineLength = Math.max(...lines.map((line) => line.length), 1);
const targetWidth = Math.min(
220,
Math.max(
MIN_STICKY_SIZE,
Math.ceil(
longestLineLength * STICKY_CHAR_WIDTH * 0.6 + STICKY_TEXT_PADDING,
),
),

const desiredContentWidth = Math.ceil(longestLineLength * STICKY_CHAR_WIDTH * 0.6);
const width = Math.min(
MAX_STICKY_WIDTH,
Math.max(MIN_STICKY_SIZE, desiredContentWidth + STICKY_TEXT_PADDING),
);

const charsPerLine = Math.max(
1,
Math.floor((targetWidth - STICKY_TEXT_PADDING) / STICKY_CHAR_WIDTH),
Math.floor((width - STICKY_TEXT_PADDING) / (STICKY_CHAR_WIDTH * 0.6)),
);
const wrappedLineCount = Math.max(
1,
Expand All @@ -48,23 +46,9 @@ export function estimateStickySize(text: string): {
0,
),
);
const width = Math.min(
MAX_STICKY_SIZE,
Math.max(
MIN_STICKY_SIZE,
Math.max(
targetWidth,
Math.min(220, longestLineLength * STICKY_CHAR_WIDTH * 0.75) +
STICKY_TEXT_PADDING,
),
),
);
const height = Math.min(
MAX_STICKY_SIZE,
Math.max(
MIN_STICKY_SIZE,
wrappedLineCount * STICKY_LINE_HEIGHT + STICKY_TEXT_PADDING,
),
const height = Math.max(
MIN_STICKY_SIZE,
wrappedLineCount * STICKY_LINE_HEIGHT + STICKY_TEXT_PADDING,
);

return { width, height };
Expand Down
100 changes: 91 additions & 9 deletions features/board/utils/theme-elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,77 @@ function normalizeColorToken(color: string): string {
return value;
}

function parseRgb(color: string): { r: number; g: number; b: number; a: number } | null {
const value = color.trim().toLowerCase();

const shortHex = value.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
if (shortHex) {
const [, r, g, b] = shortHex;
return {
r: parseInt(r + r, 16),
g: parseInt(g + g, 16),
b: parseInt(b + b, 16),
a: 1,
};
}

const longHex = value.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
if (longHex) {
const [, r, g, b] = longHex;
return { r: parseInt(r, 16), g: parseInt(g, 16), b: parseInt(b, 16), a: 1 };
}

const rgbMatch = value.match(
/^rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})(?:[\s,\/]+([01]?(?:\.\d+)?))?\s*\)$/,
);
if (rgbMatch) {
const [, r, g, b, alpha] = rgbMatch;
return {
r: Number(r),
g: Number(g),
b: Number(b),
a: alpha === undefined ? 1 : Number(alpha),
};
}

return null;
}

function relativeLuminance(channel: number): number {
const c = channel / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
}

function isLightFill(fill: string): boolean {
const rgb = parseRgb(fill);
if (!rgb || rgb.a === 0) {
return false;
}
const luminance =
0.2126 * relativeLuminance(rgb.r) +
0.7152 * relativeLuminance(rgb.g) +
0.0722 * relativeLuminance(rgb.b);
return luminance > 0.5;
}

function hasOpaqueFill(fill: string | undefined): fill is string {
if (typeof fill !== 'string') {
return false;
}
const rgb = parseRgb(fill);
return !!rgb && rgb.a > 0;
}

function resolveTextInkForFill(
effectiveFill: string | undefined,
theme: BoardThemeMode,
): string {
if (hasOpaqueFill(effectiveFill)) {
return isLightFill(effectiveFill) ? LIGHT_BOARD_INK.text : DARK_BOARD_INK.text;
}
return getBoardInkColors(theme).text;
}

function remapDefaultInk(color: string, theme: BoardThemeMode, kind: 'stroke' | 'text') {
const normalized = normalizeColorToken(color);
const lightValue = kind === 'stroke' ? LIGHT_BOARD_INK.stroke : LIGHT_BOARD_INK.text;
Expand Down Expand Up @@ -113,34 +184,42 @@ function isRootMindElement(element: ThemeSyncElement): boolean {
return element.type === 'mindmap' && element.isRoot === true;
}

function syncSlateTextValue(text: SlateTextValue | undefined, theme: BoardThemeMode) {
function syncSlateTextValue(
text: SlateTextValue | undefined,
theme: BoardThemeMode,
effectiveFill?: string,
) {
if (!text || !Array.isArray(text.children)) {
return text;
}

let changed = false;
const targetInk = getBoardInkColors(theme);
const targetTextColor = resolveTextInkForFill(effectiveFill, theme);
const nextChildren = text.children.map((child) => {
if (!child || typeof child !== 'object' || typeof child.text !== 'string') {
return child;
}

if (!child.color) {
if (targetInk.text === LIGHT_BOARD_INK.text) {
if (targetTextColor === LIGHT_BOARD_INK.text && !hasOpaqueFill(effectiveFill)) {
return child;
}

changed = true;
return { ...child, color: targetInk.text };
return { ...child, color: targetTextColor };
}

const normalized = normalizeColorToken(child.color);
if (normalized !== LIGHT_BOARD_INK.text && normalized !== DARK_BOARD_INK.text) {
return child;
}

const nextColor = remapDefaultInk(child.color, theme, 'text');
if (nextColor === child.color) {
if (normalized === targetTextColor) {
return child;
}

changed = true;
return { ...child, color: nextColor };
return { ...child, color: targetTextColor };
});

return changed ? { ...text, children: nextChildren } : text;
Expand Down Expand Up @@ -170,13 +249,16 @@ function syncElementForBoardTheme(
changed = true;
}

let effectiveFill: string | undefined;
if (targetRootFill && (typeof element.fill !== 'string' || isThinkixMindRootFill(element.fill))) {
effectiveFill = targetRootFill;
if (element.fill !== targetRootFill) {
next.fill = targetRootFill;
changed = true;
}
} else if (typeof element.fill === 'string') {
const fill = remapDefaultFill(element.fill, theme);
effectiveFill = fill;
if (fill !== element.fill) {
next.fill = fill;
changed = true;
Expand All @@ -194,13 +276,13 @@ function syncElementForBoardTheme(
changed = true;
}

const text = syncSlateTextValue(element.text, theme);
const text = syncSlateTextValue(element.text, theme, effectiveFill);
if (text !== element.text) {
next.text = text;
changed = true;
}

const topic = syncSlateTextValue(element.data?.topic, theme);
const topic = syncSlateTextValue(element.data?.topic, theme, effectiveFill);
if (element.data && topic !== element.data.topic) {
next.data = { ...element.data, topic };
changed = true;
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
],
"scripts": {
"dev": "next dev",
"prebuild": "bash scripts/apply-patches.sh && bun run build:parser",
"prebuild": "bun run build:parser",
"pretest": "bun run build:parser",
"build": "bunx next build",
"start": "next start",
Expand All @@ -23,7 +23,6 @@
"test:e2e:debug": "playwright test --debug",
"test:e2e:report": "playwright show-report",
"test:all": "bun run test:run && bun run test:e2e",
"postinstall": "bash scripts/apply-patches.sh",
"build:parser": "peggy -o features/agent/tools/dsl/parser.js features/agent/tools/dsl/thinkix.peggy --format es"
},
"dependencies": {
Expand Down Expand Up @@ -106,12 +105,14 @@
"jsdom": "^28.1.0",
"msw": "^2.12.10",
"nx": "22.4.2",
"patch-package": "^8.0.1",
"peggy": "5.1.0",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5",
"vitest": "^4.0.18"
},
"nx": {}
"nx": {},
"patchedDependencies": {
"@plait/draw@0.92.3": "patches/@plait%2Fdraw@0.92.3.patch"
}
}
Loading
Loading