Skip to content
Open
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
5 changes: 4 additions & 1 deletion excalidraw-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"idb-keyval": "6.0.3",
"jotai": "2.11.0",
"react": "19.0.0",
"react-colorful": "5.6.1",
"react-dom": "19.0.0",
"socket.io-client": "4.7.2",
"vite-plugin-html": "3.2.2"
Expand All @@ -52,6 +53,8 @@
"build:preview": "yarn build && vite preview --port 5000"
},
"devDependencies": {
"@types/react-color": "3.0.13",
"react-colorful": "5.6.1",
"vite-plugin-sitemap": "0.7.1"
}
}
}
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@types/jest": "27.4.0",
"@types/lodash.throttle": "4.1.7",
"@types/react": "19.0.10",
"@types/react-color": "3.0.13",
"@types/react-dom": "19.0.4",
"@types/socket.io-client": "3.0.0",
"@vitejs/plugin-react": "3.1.0",
Expand Down Expand Up @@ -85,5 +86,9 @@
},
"resolutions": {
"strip-ansi": "6.0.1"
},
"dependencies": {
"react-color": "2.19.3",
"react-colorful": "5.6.1"
}
}
85 changes: 50 additions & 35 deletions packages/excalidraw/actions/actionProperties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export const changeProperty = (
});
};


export const getFormValue = function <T extends Primitive>(
elements: readonly ExcalidrawElement[],
appState: AppState,
Expand Down Expand Up @@ -486,41 +487,55 @@ export const actionChangeStrokeWidth = register({
};
},
PanelComponent: ({ elements, appState, updateData }) => (
//stroke slider
<fieldset>
<legend>{t("labels.strokeWidth")}</legend>
<ButtonIconSelect
group="stroke-width"
options={[
{
value: STROKE_WIDTH.thin,
text: t("labels.thin"),
icon: StrokeWidthBaseIcon,
testId: "strokeWidth-thin",
},
{
value: STROKE_WIDTH.bold,
text: t("labels.bold"),
icon: StrokeWidthBoldIcon,
testId: "strokeWidth-bold",
},
{
value: STROKE_WIDTH.extraBold,
text: t("labels.extraBold"),
icon: StrokeWidthExtraBoldIcon,
testId: "strokeWidth-extraBold",
},
]}
value={getFormValue(
elements,
appState,
(element) => element.strokeWidth,
(element) => element.hasOwnProperty("strokeWidth"),
(hasSelection) =>
hasSelection ? null : appState.currentItemStrokeWidth,
)}
onChange={(value) => updateData(value)}
/>
{/* <legend>{t("labels.strokeWidth")}</legend> */}

<label className="control-label">
{t("labels.strokeWidth")}

<div className="range-wrapper">
<input
type="range"
min={STROKE_WIDTH.thin} // Set the minimum value (thin stroke width)
max={STROKE_WIDTH.extraBold} // Set the maximum value (extra bold stroke width)
step={1} // You can set the step to 1 if you want to go in increments of 1
value={
getFormValue(
elements,
appState,
(element) => element.strokeWidth,
(element) => element.hasOwnProperty("strokeWidth"),
(hasSelection) =>
hasSelection ? null : appState.currentItemStrokeWidth,
) ?? 0 // 👈 fallback to 0 if null
}

onChange={(event) => updateData(+event.target.value)} // Update the value when the slider is changed
className="range-input"
data-testid="strokeWidth-slider"
/>

{/* Show the value as a bubble above the slider */}
<div className="value-bubble">
{getFormValue(
elements,
appState,
(element) => element.strokeWidth,
(element) => element.hasOwnProperty("strokeWidth"),
(hasSelection) =>
hasSelection ? null : appState.currentItemStrokeWidth,
)}
</div>

{/* Show the minimum value label */}
{/* <div className="zero-label">{STROKE_WIDTH.thin}</div> */}
{/* Show the maximum value label */}
{/* <div className="max-label">{STROKE_WIDTH.extraBold}</div> */}
</div>
</label>
</fieldset>

),
});

Expand Down Expand Up @@ -613,7 +628,7 @@ export const actionChangeStrokeStyle = register({
icon: StrokeStyleDottedIcon,
},
]}
value={getFormValue(
value={getFormValue(
elements,
appState,
(element) => element.strokeStyle,
Expand Down Expand Up @@ -849,7 +864,7 @@ export const actionChangeFontFamily = register({

while (
i < selectedTextElements.length &&
textLengthAccumulator < 5000
textLengthAccumulator < 5000 // try to see if you can have a bigger range of width size
) {
const textElement = selectedTextElements[i] as ExcalidrawTextElement;
textLengthAccumulator += textElement?.originalText.length || 0;
Expand Down
58 changes: 58 additions & 0 deletions packages/excalidraw/components/ColorPicker/ColorPicker.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,63 @@
@import "../../css/variables.module.scss";

.color-picker__modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}

.color-picker__modal {
background: white;
padding: 1rem;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
z-index: 1001;
}

.color-picker__close {
margin-top: 1rem;
background: #eee;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}

.rainbow-button {
background: linear-gradient(
45deg,
red,
orange,
yellow,
green,
blue,
indigo,
violet
);
background-size: 400% 400%;
animation: rainbowShift 6s ease infinite;
}

@keyframes rainbowShift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}


.excalidraw {
.focus-visible-none {
&:focus-visible {
Expand Down
17 changes: 17 additions & 0 deletions packages/excalidraw/components/ColorPicker/ColorPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as Popover from "@radix-ui/react-popover";
import clsx from "clsx";
import { useRef } from "react";
// import { ChromePicker } from "react-color";

import {
COLOR_OUTLINE_CONTRAST_THRESHOLD,
Expand Down Expand Up @@ -176,6 +177,22 @@ const ColorPickerPopupContent = ({
) : (
colorInputJSX
)}
{/* <div style={{ marginTop: "1rem", maxWidth: "100%", overflow: "hidden" }}>
<ChromePicker
color={color}
onChange={(colorResult) => {
onChange(colorResult.hex); // Update the color when the user selects a color
}}
styles={{
default: {
picker: {
width: "100%", // Make the picker fit the parent container
boxSizing: "border-box", // Ensure padding is included in the width
}
},
}}
/>
</div> */}
</PropertiesPopover>
);
};
Expand Down
124 changes: 88 additions & 36 deletions packages/excalidraw/components/ColorPicker/TopPicks.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import clsx from "clsx";
import { HexColorPicker } from "react-colorful";
import { useEffect, useState } from "react";

import {
COLOR_OUTLINE_CONTRAST_THRESHOLD,
Expand All @@ -24,51 +26,101 @@ export const TopPicks = ({
activeColor,
topPicks,
}: TopPicksProps) => {
let colors;
if (type === "elementStroke") {
colors = DEFAULT_ELEMENT_STROKE_PICKS;
}
const [showModal, setShowModal] = useState(false);
const [color, setColor] = useState(activeColor);

if (type === "elementBackground") {
colors = DEFAULT_ELEMENT_BACKGROUND_PICKS;
}

if (type === "canvasBackground") {
colors = DEFAULT_CANVAS_BACKGROUND_PICKS;
}
// Sync color with activeColor prop
useEffect(() => {
setColor(activeColor);
}, [activeColor]);

// this one can overwrite defaults
if (topPicks) {
colors = topPicks;
}
// Update parent with new color
const handleColorChange = (newColor: string) => {
if (newColor !== color) {
setColor(newColor);
onChange(newColor); // Pass updated color to the parent
}
};

if (!colors) {
console.error("Invalid type for TopPicks");
return null;
let colors;
switch (type) {
case "elementStroke":
colors = DEFAULT_ELEMENT_STROKE_PICKS;
break;
case "elementBackground":
colors = DEFAULT_ELEMENT_BACKGROUND_PICKS;
break;
case "canvasBackground":
colors = DEFAULT_CANVAS_BACKGROUND_PICKS;
break;
default:
colors = topPicks || [];
if (!colors.length) {
console.error("Invalid type for TopPicks");
return null;
}
}

return (
<div className="color-picker__top-picks">
{colors.map((color: string) => (
<>
<div className="color-picker__top-picks">
{/* Predefined color buttons */}
{colors.map((presetColor: string) => (
<button
className={clsx("color-picker__button", {
active: presetColor === activeColor,
"is-transparent": presetColor === "transparent" || !presetColor,
"has-outline": !isColorDark(
presetColor,
COLOR_OUTLINE_CONTRAST_THRESHOLD
),
})}
style={{ "--swatch-color": presetColor }}
key={presetColor}
type="button"
title={presetColor}
onClick={() => handleColorChange(presetColor)}
data-testid={`color-top-pick-${presetColor}`}
aria-label={`Select ${presetColor} color`}
>
<div className="color-picker__button-outline" />
</button>
))}

{/* Rainbow button to trigger modal */}
<button
className={clsx("color-picker__button", {
active: color === activeColor,
"is-transparent": color === "transparent" || !color,
"has-outline": !isColorDark(
color,
COLOR_OUTLINE_CONTRAST_THRESHOLD,
),
})}
style={{ "--swatch-color": color }}
key={color}
type="button"
title={color}
onClick={() => onChange(color)}
data-testid={`color-top-pick-${color}`}
className={clsx("color-picker__button", "rainbow-button")}
onClick={() => setShowModal(true)}
title="Custom color"
data-testid="color-top-pick-custom"
aria-label="Select custom color"
>
<div className="color-picker__button-outline" />
</button>
))}
</div>
</div>

{/* Modal for custom color picker */}
{showModal && (
<div
className="color-picker__modal-backdrop"
onClick={() => setShowModal(false)} // Close the modal when clicking outside
aria-hidden="true"
>
<div
className="color-picker__modal"
onClick={(e) => e.stopPropagation()} // Prevent closing modal when clicking inside
>
<HexColorPicker color={color} onChange={handleColorChange} />
<button
className="color-picker__close"
onClick={() => setShowModal(false)}
aria-label="Close color picker modal"
>
Close
</button>
</div>
</div>
)}
</>
);
};
Loading