@hunterchen/canvas 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/styles.css CHANGED
@@ -1 +1 @@
1
- *,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--canvas-beige:#f7f1e5;--canvas-coral:#ffb5a7;--canvas-lilac:#d9c8e6;--canvas-salmon:#ffa585;--canvas-heavy:#3c204c;--canvas-emphasis:#513b7a;--canvas-active:#8f57ad;--canvas-tinted:#c9a7db;--canvas-medium:#776780;--canvas-light:#c3b8cb;--canvas-faint-lilac:#f5f2f7;--canvas-offwhite:#fdfcfd;--canvas-highlight:#f5f2f7;--canvas-border-light:0 0% 89%}.container{width:100%;margin-right:auto;margin-left:auto;padding-right:2rem;padding-left:2rem}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1600px){.container{max-width:1600px}}@media (min-width:2000px){.container{max-width:2000px}}@media (min-width:3000px){.container{max-width:3000px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.-top-10{top:-2.5rem}.bottom-12{bottom:3rem}.bottom-24{bottom:6rem}.left-1\/2{left:50%}.left-4{left:1rem}.top-1\/2{top:50%}.top-24{top:6rem}.top-6{top:1.5rem}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-50{z-index:50}.z-\[1000\]{z-index:1000}.m-auto{margin:auto}.mb-4{margin-bottom:1rem}.block{display:block}.inline{display:inline}.flex{display:flex}.hidden{display:none}.h-5{height:1.25rem}.h-auto{height:auto}.h-full{height:100%}.w-5{width:1.25rem}.w-full{width:100%}.flex-shrink-0{flex-shrink:0}.grow{flex-grow:1}.origin-center{transform-origin:center}.origin-top-left{transform-origin:top left}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y:-50%}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-default{cursor:default}.touch-none{touch-action:none}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.overflow-hidden{overflow:hidden}.whitespace-nowrap{white-space:nowrap}.rounded-\[10px\]{border-radius:10px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.border,.border-\[1px\]{border-width:1px}.border-border{border-color:hsl(var(--border))}.bg-\[\#EEE2FB\]{--tw-bg-opacity:1;background-color:rgb(238 226 251/var(--tw-bg-opacity,1))}.bg-canvas-highlight{background-color:var(--canvas-highlight)}.bg-canvas-offwhite{background-color:var(--canvas-offwhite)}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.bg-none{background-image:none}.from-black\/10{--tw-gradient-from:rgba(0,0,0,.1) var(--tw-gradient-from-position);--tw-gradient-to:transparent var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.object-contain{-o-object-fit:contain;object-fit:contain}.p-1{padding:.25rem}.p-2{padding:.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.px-\[1px\]{padding-left:1px;padding-right:1px}.py-1{padding-top:.25rem;padding-bottom:.25rem}.pb-\[2\.5px\]{padding-bottom:2.5px}.pt-\[1px\]{padding-top:1px}.text-center{text-align:center}.font-canvas-figtree{font-family:var(--font-figtree)}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.text-canvas-emphasis{color:var(--canvas-emphasis)}.text-canvas-heavy{color:var(--canvas-heavy)}.text-canvas-light{color:var(--canvas-light)}.text-canvas-medium{color:var(--canvas-medium)}.text-neutral-500{--tw-text-opacity:1;color:rgb(115 115 115/var(--tw-text-opacity,1))}.text-neutral-600{--tw-text-opacity:1;color:rgb(82 82 82/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.shadow-\[0_20px_40px_rgba\(103\2c 86\2c 86\2c 0\.15\)\]{--tw-shadow:0 20px 40px hsla(0,9%,37%,.15);--tw-shadow-colored:0 20px 40px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-\[0_6px_12px_rgba\(0\2c 0\2c 0\2c 0\.10\)\]{--tw-shadow:0 6px 12px rgba(0,0,0,.1);--tw-shadow-colored:0 6px 12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.blur{--tw-blur:blur(8px)}.blur,.drop-shadow{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px rgba(0,0,0,.1)) drop-shadow(0 1px 1px rgba(0,0,0,.06))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}@keyframes enter{0%{opacity:var(--tw-enter-opacity,1);transform:translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0) scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1)) rotate(var(--tw-enter-rotate,0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity,1);transform:translate3d(var(--tw-exit-translate-x,0),var(--tw-exit-translate-y,0),0) scale3d(var(--tw-exit-scale,1),var(--tw-exit-scale,1),var(--tw-exit-scale,1)) rotate(var(--tw-exit-rotate,0))}}.duration-200{animation-duration:.2s}.running{animation-play-state:running}@media (min-width:640px){.sm\:top-4{top:1rem}}@media (min-width:768px){.md\:bottom-4{bottom:1rem}.md\:inline{display:inline}.md\:px-8{padding-left:2rem;padding-right:2rem}.md\:text-sm{font-size:.875rem;line-height:1.25rem}}
1
+ *,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--canvas-beige:#f7f1e5;--canvas-coral:#ffb5a7;--canvas-lilac:#d9c8e6;--canvas-salmon:#ffa585;--canvas-heavy:#3c204c;--canvas-emphasis:#513b7a;--canvas-active:#8f57ad;--canvas-tinted:#c9a7db;--canvas-medium:#776780;--canvas-light:#c3b8cb;--canvas-faint-lilac:#f5f2f7;--canvas-offwhite:#fdfcfd;--canvas-highlight:#f5f2f7;--canvas-border-light:0 0% 89%}.container{width:100%;margin-right:auto;margin-left:auto;padding-right:2rem;padding-left:2rem}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1600px){.container{max-width:1600px}}@media (min-width:2000px){.container{max-width:2000px}}@media (min-width:3000px){.container{max-width:3000px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.-top-10{top:-2.5rem}.bottom-12{bottom:3rem}.bottom-24{bottom:6rem}.bottom-6{bottom:1.5rem}.left-1\/2{left:50%}.left-4{left:1rem}.right-4{right:1rem}.top-1\/2{top:50%}.top-24{top:6rem}.top-6{top:1.5rem}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-50{z-index:50}.z-\[1000\]{z-index:1000}.m-auto{margin:auto}.mb-4{margin-bottom:1rem}.block{display:block}.inline{display:inline}.flex{display:flex}.hidden{display:none}.h-5{height:1.25rem}.h-auto{height:auto}.h-full{height:100%}.w-5{width:1.25rem}.w-full{width:100%}.flex-shrink-0{flex-shrink:0}.grow{flex-grow:1}.origin-center{transform-origin:center}.origin-top-left{transform-origin:top left}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y:-50%}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-default{cursor:default}.touch-none{touch-action:none}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.overflow-hidden{overflow:hidden}.whitespace-nowrap{white-space:nowrap}.rounded-\[10px\]{border-radius:10px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.border,.border-\[1px\]{border-width:1px}.border-border{border-color:hsl(var(--border))}.bg-\[\#EEE2FB\]{--tw-bg-opacity:1;background-color:rgb(238 226 251/var(--tw-bg-opacity,1))}.bg-canvas-highlight{background-color:var(--canvas-highlight)}.bg-canvas-offwhite{background-color:var(--canvas-offwhite)}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.bg-none{background-image:none}.from-black\/10{--tw-gradient-from:rgba(0,0,0,.1) var(--tw-gradient-from-position);--tw-gradient-to:transparent var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.object-contain{-o-object-fit:contain;object-fit:contain}.p-1{padding:.25rem}.p-2{padding:.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.px-\[1px\]{padding-left:1px;padding-right:1px}.py-1{padding-top:.25rem;padding-bottom:.25rem}.pb-\[2\.5px\]{padding-bottom:2.5px}.pt-\[1px\]{padding-top:1px}.text-center{text-align:center}.font-canvas-figtree{font-family:var(--font-figtree)}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.text-canvas-emphasis{color:var(--canvas-emphasis)}.text-canvas-heavy{color:var(--canvas-heavy)}.text-canvas-light{color:var(--canvas-light)}.text-canvas-medium{color:var(--canvas-medium)}.text-neutral-500{--tw-text-opacity:1;color:rgb(115 115 115/var(--tw-text-opacity,1))}.text-neutral-600{--tw-text-opacity:1;color:rgb(82 82 82/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.shadow-\[0_20px_40px_rgba\(103\2c 86\2c 86\2c 0\.15\)\]{--tw-shadow:0 20px 40px hsla(0,9%,37%,.15);--tw-shadow-colored:0 20px 40px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-\[0_6px_12px_rgba\(0\2c 0\2c 0\2c 0\.10\)\]{--tw-shadow:0 6px 12px rgba(0,0,0,.1);--tw-shadow-colored:0 6px 12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.blur{--tw-blur:blur(8px)}.blur,.drop-shadow{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px rgba(0,0,0,.1)) drop-shadow(0 1px 1px rgba(0,0,0,.06))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}@keyframes enter{0%{opacity:var(--tw-enter-opacity,1);transform:translate3d(var(--tw-enter-translate-x,0),var(--tw-enter-translate-y,0),0) scale3d(var(--tw-enter-scale,1),var(--tw-enter-scale,1),var(--tw-enter-scale,1)) rotate(var(--tw-enter-rotate,0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity,1);transform:translate3d(var(--tw-exit-translate-x,0),var(--tw-exit-translate-y,0),0) scale3d(var(--tw-exit-scale,1),var(--tw-exit-scale,1),var(--tw-exit-scale,1)) rotate(var(--tw-exit-rotate,0))}}.duration-200{animation-duration:.2s}.running{animation-play-state:running}@media (min-width:640px){.sm\:bottom-4{bottom:1rem}.sm\:top-4{top:1rem}}@media (min-width:768px){.md\:bottom-4{bottom:1rem}.md\:inline{display:inline}.md\:px-8{padding-left:2rem;padding-right:2rem}.md\:text-sm{font-size:.875rem;line-height:1.25rem}}
@@ -37,4 +37,47 @@ export interface NavItem {
37
37
  /** If true, clicking this section triggers the reset/home behavior */
38
38
  isHome?: boolean;
39
39
  }
40
+ /**
41
+ * Preset positions for the toolbar
42
+ */
43
+ export type ToolbarPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
44
+ /**
45
+ * What to display in the toolbar
46
+ */
47
+ export type ToolbarDisplayMode = 'coordinates' | 'scale' | 'both';
48
+ /**
49
+ * Configuration options for the canvas toolbar
50
+ */
51
+ export interface ToolbarConfig {
52
+ /** Hide the toolbar entirely. Default: false */
53
+ hidden?: boolean;
54
+ /** What to show: 'coordinates', 'scale', or 'both'. Default: 'both' */
55
+ display?: ToolbarDisplayMode;
56
+ /** Preset position. Default: 'top-left' */
57
+ position?: ToolbarPosition;
58
+ /** Disable auto-hide when at home position. Default: false */
59
+ disableAutoHide?: boolean;
60
+ /** Additional className for the container */
61
+ className?: string;
62
+ /** Additional className for the coordinates text */
63
+ coordinatesClassName?: string;
64
+ /** Additional className for the scale text */
65
+ scaleClassName?: string;
66
+ /** Additional className for the separator */
67
+ separatorClassName?: string;
68
+ /** Inline styles for the container */
69
+ style?: React.CSSProperties;
70
+ /** Inline styles for the coordinates */
71
+ coordinatesStyle?: React.CSSProperties;
72
+ /** Inline styles for the scale */
73
+ scaleStyle?: React.CSSProperties;
74
+ /** Custom separator between coordinates and scale. Default: ' | ' */
75
+ separator?: string;
76
+ /** Gap around the separator in pixels or CSS value. Default: undefined (uses inline spacing) */
77
+ separatorGap?: number | string;
78
+ /** Format for coordinates. Default: '(x, y)' */
79
+ coordinatesFormat?: (x: number, y: number) => string;
80
+ /** Format for scale. Default: '1.00x' */
81
+ scaleFormat?: (scale: number) => string;
82
+ }
40
83
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,kBAAkB;IACjC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC;AAEnC;;;GAGG;AACH,MAAM,WAAW,OAAO;IACtB,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,aAAa,CAAC;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,iCAAiC;IACjC,CAAC,EAAE,MAAM,CAAC;IACV,iCAAiC;IACjC,CAAC,EAAE,MAAM,CAAC;IACV,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,kBAAkB;IACjC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC;AAEnC;;;GAGG;AACH,MAAM,WAAW,OAAO;IACtB,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC,aAAa,CAAC;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3D,iCAAiC;IACjC,CAAC,EAAE,MAAM,CAAC;IACV,iCAAiC;IACjC,CAAC,EAAE,MAAM,CAAC;IACV,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,WAAW,GAAG,aAAa,GAAG,cAAc,CAAC;AAExF;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,aAAa,GAAG,OAAO,GAAG,MAAM,CAAC;AAElE;;GAEG;AACH,MAAM,WAAW,aAAa;IAE5B,gDAAgD;IAChD,MAAM,CAAC,EAAE,OAAO,CAAC;IAGjB,uEAAuE;IACvE,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAG7B,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,eAAe,CAAC;IAG3B,8DAA8D;IAC9D,eAAe,CAAC,EAAE,OAAO,CAAC;IAG1B,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,8CAA8C;IAC9C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,6CAA6C;IAC7C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAG5B,sCAAsC;IACtC,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,wCAAwC;IACxC,gBAAgB,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IACvC,kCAAkC;IAClC,UAAU,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAGjC,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,gDAAgD;IAChD,iBAAiB,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACrD,yCAAyC;IACzC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CACzC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hunterchen/canvas",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "A React-based canvas library for creating pannable, zoomable, and interactive canvas experiences.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -46,8 +46,8 @@
46
46
  },
47
47
  "peerDependencies": {
48
48
  "framer-motion": "^11.0.0 || ^12.0.0",
49
- "react": "^18.0.0 || ^19.0.0",
50
- "react-dom": "^18.0.0 || ^19.0.0"
49
+ "react": "^19.0.0",
50
+ "react-dom": "^19.0.0"
51
51
  },
52
52
  "dependencies": {
53
53
  "clsx": "^2.1.1",
@@ -39,7 +39,12 @@ import {
39
39
  import useWindowDimensions from "../../hooks/useWindowDimensions";
40
40
  import Navbar from "./navbar";
41
41
  import Toolbar from "./toolbar";
42
- import type { CanvasSection, NavItem, SectionCoordinates } from "../../types";
42
+ import type {
43
+ CanvasSection,
44
+ NavItem,
45
+ SectionCoordinates,
46
+ ToolbarConfig,
47
+ } from "../../types";
43
48
  import { CanvasWrapper } from "./wrapper";
44
49
  import { usePerformanceMode } from "../../hooks/usePerformanceMode";
45
50
  import type { ReactNode } from "react";
@@ -74,12 +79,16 @@ interface Props {
74
79
  canvasBackground?: ReactNode;
75
80
  /** Custom wrapper/intro background. If not provided, uses introBackgroundGradient. */
76
81
  wrapperBackground?: ReactNode;
82
+
83
+ // ============== Toolbar Customization ==============
84
+ /** Toolbar customization options */
85
+ toolbarConfig?: ToolbarConfig;
77
86
  }
78
87
 
79
88
  const stopAllMotion = (
80
89
  x: MotionValue<number>,
81
90
  y: MotionValue<number>,
82
- scale: MotionValue<number>,
91
+ scale: MotionValue<number>
83
92
  ) => {
84
93
  x.stop();
85
94
  y.stop();
@@ -99,6 +108,7 @@ const Canvas: FC<Props> = ({
99
108
  blurTransition,
100
109
  canvasBackground,
101
110
  wrapperBackground,
111
+ toolbarConfig,
102
112
  }) => {
103
113
  const { height: windowHeight, width: windowWidth } = useWindowDimensions();
104
114
 
@@ -130,7 +140,7 @@ const Canvas: FC<Props> = ({
130
140
 
131
141
  const initialBoxWidth = useMemo(
132
142
  () => calcInitialBoxWidth(windowWidth, windowHeight),
133
- [windowWidth, windowHeight],
143
+ [windowWidth, windowHeight]
134
144
  );
135
145
 
136
146
  // somewhere near the middle-ish
@@ -145,7 +155,7 @@ const Canvas: FC<Props> = ({
145
155
  coords: homeCoordinates,
146
156
  targetZoom: 1,
147
157
  }),
148
- [homeCoordinates, windowWidth, windowHeight],
158
+ [homeCoordinates, windowWidth, windowHeight]
149
159
  );
150
160
 
151
161
  const onResetViewAndItems = useCallback(
@@ -157,7 +167,7 @@ const Canvas: FC<Props> = ({
157
167
  if (onComplete) onComplete();
158
168
  });
159
169
  },
160
- [offsetHomeCoordinates, x, y, scale],
170
+ [offsetHomeCoordinates, x, y, scale]
161
171
  );
162
172
 
163
173
  // Shared intro progress (0->1) driven by CanvasWrapper
@@ -167,7 +177,7 @@ const Canvas: FC<Props> = ({
167
177
  const stage1Targets = useMemo(() => {
168
178
  const finalScale = Math.max(
169
179
  (windowWidth || 0) / canvasWidth,
170
- (windowHeight || 0) / canvasHeight,
180
+ (windowHeight || 0) / canvasHeight
171
181
  );
172
182
  const endX = (windowWidth - canvasWidth * finalScale) / 2;
173
183
  const endY = (windowHeight - canvasHeight * finalScale) / 2;
@@ -178,7 +188,7 @@ const Canvas: FC<Props> = ({
178
188
  const derivedScale = useTransform(
179
189
  introProgress,
180
190
  [0, 1],
181
- [initialBoxWidth, stage1Targets.finalScale],
191
+ [initialBoxWidth, stage1Targets.finalScale]
182
192
  );
183
193
  const derivedX = useTransform(introProgress, [0, 1], [0, stage1Targets.endX]);
184
194
  const derivedY = useTransform(introProgress, [0, 1], [0, stage1Targets.endY]);
@@ -240,11 +250,11 @@ const Canvas: FC<Props> = ({
240
250
  node.addEventListener("wheel", wheelWrapper, { passive: false });
241
251
  }
242
252
  },
243
- [wheelWrapper],
253
+ [wheelWrapper]
244
254
  );
245
255
 
246
256
  const activePointersRef = useRef<Map<number, PointerEvent<HTMLDivElement>>>(
247
- new Map(),
257
+ new Map()
248
258
  );
249
259
  const initialPinchStateRef = useRef<{
250
260
  distance: number;
@@ -258,7 +268,7 @@ const Canvas: FC<Props> = ({
258
268
  offset: Point,
259
269
  viewportRef: React.RefObject<HTMLDivElement | null>,
260
270
  onComplete?: () => void,
261
- zoom?: number,
271
+ zoom?: number
262
272
  ): void => {
263
273
  if (!viewportRef.current) return;
264
274
  setIsSceneMoving(true);
@@ -281,13 +291,13 @@ const Canvas: FC<Props> = ({
281
291
  x,
282
292
  y,
283
293
  scale,
284
- zoom,
294
+ zoom
285
295
  ).then(() => {
286
296
  setIsSceneMoving(false);
287
297
  if (onComplete) onComplete();
288
298
  });
289
299
  },
290
- [sceneWidth, sceneHeight, x, y, scale],
300
+ [sceneWidth, sceneHeight, x, y, scale]
291
301
  );
292
302
 
293
303
  // Guarded stop that ignores attempts during intro animations
@@ -340,7 +350,7 @@ const Canvas: FC<Props> = ({
340
350
  viewportRef,
341
351
  animationStage,
342
352
  stopAllSceneMotion,
343
- ],
353
+ ]
344
354
  );
345
355
 
346
356
  const handlePointerMove = useCallback(
@@ -366,11 +376,11 @@ const Canvas: FC<Props> = ({
366
376
 
367
377
  const newX = Math.min(
368
378
  Math.max(initialPanOffsetOnDrag.x + deltaX, minPanX),
369
- maxPanX,
379
+ maxPanX
370
380
  );
371
381
  const newY = Math.min(
372
382
  Math.max(initialPanOffsetOnDrag.y + deltaY, minPanY),
373
- maxPanY,
383
+ maxPanY
374
384
  );
375
385
  x.set(newX);
376
386
  y.set(newY);
@@ -399,7 +409,7 @@ const Canvas: FC<Props> = ({
399
409
  (window.innerWidth / canvasWidth) * ZOOM_BOUND, // Ensure zoom is at least the width of the canvas
400
410
  (window.innerHeight / canvasHeight) * ZOOM_BOUND, // Ensure zoom is at least the height of the canvas
401
411
  Math.min(newZoom, 10),
402
- MIN_ZOOM, // Ensure zoom is not less than MIN_ZOOM
412
+ MIN_ZOOM // Ensure zoom is not less than MIN_ZOOM
403
413
  );
404
414
 
405
415
  const mx = currentMidpoint.x;
@@ -441,7 +451,7 @@ const Canvas: FC<Props> = ({
441
451
  MIN_ZOOM,
442
452
  animationStage,
443
453
  stopAllSceneMotion,
444
- ],
454
+ ]
445
455
  );
446
456
 
447
457
  const handlePointerUpOrCancel = useCallback(
@@ -478,7 +488,7 @@ const Canvas: FC<Props> = ({
478
488
  setInitialPanOffsetOnDrag({ x: x.get(), y: y.get() });
479
489
  }
480
490
  },
481
- [x, y, isPanning, animationStage, stopAllSceneMotion],
491
+ [x, y, isPanning, animationStage, stopAllSceneMotion]
482
492
  );
483
493
 
484
494
  const handleWheelZoom = useCallback(
@@ -504,11 +514,11 @@ const Canvas: FC<Props> = ({
504
514
  const nextZoom = Math.max(
505
515
  Math.min(
506
516
  currentZoom * (1 - event.deltaY * ZOOM_SENSITIVITY),
507
- MAX_ZOOM,
517
+ MAX_ZOOM
508
518
  ),
509
519
  MIN_ZOOM,
510
520
  (window.innerWidth / canvasWidth) * ZOOM_BOUND, // Ensure zoom is at least the width of the canvas
511
- (window.innerHeight / canvasHeight) * ZOOM_BOUND, // Ensure zoom is at least the height of the canvas
521
+ (window.innerHeight / canvasHeight) * ZOOM_BOUND // Ensure zoom is at least the height of the canvas
512
522
  );
513
523
 
514
524
  const rect = viewportRef.current?.getBoundingClientRect();
@@ -567,7 +577,7 @@ const Canvas: FC<Props> = ({
567
577
  windowHeight,
568
578
  animationStage,
569
579
  stopAllSceneMotion,
570
- ],
580
+ ]
571
581
  );
572
582
 
573
583
  // Keep the wheel handler ref pointing to the latest implementation
@@ -579,7 +589,7 @@ const Canvas: FC<Props> = ({
579
589
  (
580
590
  offset: { x: number; y: number },
581
591
  onComplete?: () => void,
582
- zoom?: number,
592
+ zoom?: number
583
593
  ) => {
584
594
  panToOffset(
585
595
  {
@@ -588,10 +598,10 @@ const Canvas: FC<Props> = ({
588
598
  },
589
599
  viewportRef,
590
600
  onComplete,
591
- zoom,
601
+ zoom
592
602
  );
593
603
  },
594
- [panToOffset, viewportRef],
604
+ [panToOffset, viewportRef]
595
605
  );
596
606
 
597
607
  return (
@@ -620,7 +630,12 @@ const Canvas: FC<Props> = ({
620
630
  >
621
631
  {animationStage >= 2 && (
622
632
  <>
623
- <Toolbar homeCoordinates={offsetHomeCoordinates} />
633
+ {!toolbarConfig?.hidden && (
634
+ <Toolbar
635
+ homeCoordinates={offsetHomeCoordinates}
636
+ config={toolbarConfig}
637
+ />
638
+ )}
624
639
  {hasNavbar && navItems ? (
625
640
  <Navbar
626
641
  panToOffset={handlePanToOffset}
@@ -137,6 +137,7 @@ export interface DraggableImageProps extends DraggableProps {
137
137
  width?: string | number;
138
138
  height?: string | number;
139
139
  scale?: number;
140
+ hoverScale?: number;
140
141
  }
141
142
 
142
143
  function drawImageToCanvas(img: HTMLImageElement, canvas: HTMLCanvasElement) {
@@ -202,6 +203,7 @@ export function DraggableImage(props: DraggableImageProps) {
202
203
  animate,
203
204
  className,
204
205
  scale,
206
+ hoverScale,
205
207
  ...restProps
206
208
  } = props;
207
209
  const imgRef = useRef<HTMLImageElement>(null);
@@ -265,7 +267,7 @@ export function DraggableImage(props: DraggableImageProps) {
265
267
  updateCursor(isOpaque, false, imgRef.current);
266
268
  };
267
269
 
268
- const hoverScale = isOpaque ? (scale ?? 1) * 1.05 : (scale ?? 1);
270
+ const hoverScaleValue = isOpaque ? (hoverScale ?? (scale ?? 1)) : (scale ?? 1);
269
271
 
270
272
  return (
271
273
  <Draggable
@@ -285,7 +287,7 @@ export function DraggableImage(props: DraggableImageProps) {
285
287
  height={height}
286
288
  animate={animate}
287
289
  draggable="false"
288
- whileHover={{ scale: hoverScale }}
290
+ whileHover={{ scale: hoverScaleValue }}
289
291
  style={{
290
292
  scale: scale ?? 1,
291
293
  pointerEvents: isOpaque ? "auto" : "none",
@@ -1,19 +1,56 @@
1
1
  import { type Point, useTransform, motion } from "framer-motion";
2
- import { useEffect, useState } from "react";
2
+ import { useEffect, useState, useMemo } from "react";
3
3
  import { useCanvasContext } from "../../contexts/CanvasContext";
4
4
  import {
5
5
  TOOLBAR_OPACITY_POS_EPS,
6
6
  TOOLBAR_OPACITY_SCALE_EPS,
7
7
  } from "../../lib/constants";
8
+ import { cn } from "../../lib/utils";
9
+ import type { ToolbarConfig, ToolbarPosition } from "../../types";
8
10
 
9
11
  type ToolbarProps = {
10
12
  homeCoordinates?: Point;
13
+ config?: ToolbarConfig;
11
14
  };
12
15
 
13
- const Toolbar = ({ homeCoordinates = { x: 0, y: 0 } }: ToolbarProps) => {
16
+ const positionStyles: Record<ToolbarPosition, string> = {
17
+ "top-left": "left-4 top-6 sm:top-4",
18
+ "top-right": "right-4 top-6 sm:top-4",
19
+ "bottom-left": "left-4 bottom-6 sm:bottom-4",
20
+ "bottom-right": "right-4 bottom-6 sm:bottom-4",
21
+ };
22
+
23
+ const Toolbar = ({
24
+ homeCoordinates = { x: 0, y: 0 },
25
+ config = {},
26
+ }: ToolbarProps) => {
14
27
  const { x, y, scale } = useCanvasContext();
15
28
  const [hasMounted, setHasMounted] = useState(false);
16
29
 
30
+ const {
31
+ display = "both",
32
+ position = "top-left",
33
+ disableAutoHide = false,
34
+ className,
35
+ coordinatesClassName,
36
+ scaleClassName,
37
+ separatorClassName,
38
+ style,
39
+ coordinatesStyle,
40
+ scaleStyle,
41
+ separator = "|",
42
+ separatorGap,
43
+ coordinatesFormat,
44
+ scaleFormat,
45
+ } = config;
46
+
47
+ const separatorStyle: React.CSSProperties | undefined = separatorGap
48
+ ? {
49
+ marginInline:
50
+ typeof separatorGap === "number" ? `${separatorGap}px` : separatorGap,
51
+ }
52
+ : undefined;
53
+
17
54
  useEffect(() => {
18
55
  setHasMounted(true);
19
56
  }, []);
@@ -21,44 +58,120 @@ const Toolbar = ({ homeCoordinates = { x: 0, y: 0 } }: ToolbarProps) => {
21
58
  // numeric MotionValues
22
59
  const rawDx = useTransform(
23
60
  [x, scale],
24
- ([lx, ls]) => -((lx as number) / (ls as number)) + homeCoordinates.x,
61
+ ([lx, ls]) => -((lx as number) / (ls as number)) + homeCoordinates.x
25
62
  );
26
63
  const rawDy = useTransform(
27
64
  [y, scale],
28
- ([ly, ls]) => -((ly as number) / (ls as number)) + homeCoordinates.y,
65
+ ([ly, ls]) => -((ly as number) / (ls as number)) + homeCoordinates.y
29
66
  );
30
67
 
31
- // formatted MotionValues
68
+ // formatted MotionValues for default display
32
69
  const displayX = useTransform(rawDx, (v) => Math.round(v).toString());
33
70
  const displayY = useTransform(rawDy, (v) => Math.round(v).toString());
34
71
  const displayScale = useTransform(scale, (v) => v.toFixed(2));
35
72
 
36
- const opacity = useTransform([rawDx, rawDy, scale], ([dx, dy, ls]) =>
37
- Math.abs(dx as number) < TOOLBAR_OPACITY_POS_EPS &&
73
+ // For custom formatters, we need to use state to track values
74
+ const [currentX, setCurrentX] = useState(0);
75
+ const [currentY, setCurrentY] = useState(0);
76
+ const [currentScale, setCurrentScale] = useState(1);
77
+
78
+ useEffect(() => {
79
+ const unsubX = rawDx.on("change", (v) => setCurrentX(Math.round(v)));
80
+ const unsubY = rawDy.on("change", (v) => setCurrentY(Math.round(v)));
81
+ const unsubScale = scale.on("change", (v) => setCurrentScale(v));
82
+ return () => {
83
+ unsubX();
84
+ unsubY();
85
+ unsubScale();
86
+ };
87
+ }, [rawDx, rawDy, scale]);
88
+
89
+ const opacity = useTransform([rawDx, rawDy, scale], ([dx, dy, ls]) => {
90
+ if (disableAutoHide) return 1;
91
+ return Math.abs(dx as number) < TOOLBAR_OPACITY_POS_EPS &&
38
92
  Math.abs(dy as number) < TOOLBAR_OPACITY_POS_EPS &&
39
93
  Math.abs((ls as number) - 1) < TOOLBAR_OPACITY_SCALE_EPS
40
94
  ? 0
41
- : 1,
42
- );
95
+ : 1;
96
+ });
43
97
 
44
98
  const handlePointerDown = (e: React.PointerEvent) => e.stopPropagation();
45
99
 
100
+ const showCoordinates = display === "coordinates" || display === "both";
101
+ const showScale = display === "scale" || display === "both";
102
+ const showSeparator = display === "both";
103
+
104
+ // Compute formatted values
105
+ const formattedCoordinates = useMemo(() => {
106
+ if (coordinatesFormat) {
107
+ return coordinatesFormat(currentX, currentY);
108
+ }
109
+ return null; // Will use motion spans for default
110
+ }, [coordinatesFormat, currentX, currentY]);
111
+
112
+ const formattedScale = useMemo(() => {
113
+ if (scaleFormat) {
114
+ return scaleFormat(currentScale);
115
+ }
116
+ return null; // Will use motion span for default
117
+ }, [scaleFormat, currentScale]);
118
+
119
+ // Placeholder content for SSR/initial render
120
+ const placeholderContent = useMemo(() => {
121
+ const parts: string[] = [];
122
+ if (showCoordinates) parts.push("(0, 0)");
123
+ if (showSeparator) parts.push(separator);
124
+ if (showScale) parts.push("1.00x");
125
+ return parts.join("");
126
+ }, [showCoordinates, showScale, showSeparator, separator]);
127
+
46
128
  return (
47
129
  <motion.div
48
- className="absolute left-4 top-6 z-[1000] cursor-default select-none rounded-[10px] border border-border bg-canvas-offwhite p-2 font-mono text-xs text-canvas-heavy shadow-[0_6px_12px_rgba(0,0,0,0.10)] sm:top-4 md:text-sm"
130
+ className={cn(
131
+ "absolute z-[1000] cursor-default select-none rounded-[10px] border border-border bg-canvas-offwhite p-2 font-mono text-xs text-canvas-heavy shadow-[0_6px_12px_rgba(0,0,0,0.10)] md:text-sm",
132
+ positionStyles[position],
133
+ className
134
+ )}
49
135
  onPointerDown={handlePointerDown}
50
136
  data-toolbar-button
51
- style={{ opacity }}
137
+ style={{ opacity, ...style }}
52
138
  >
53
139
  {hasMounted ? (
54
140
  <>
55
- (<motion.span>{displayX}</motion.span>,{" "}
56
- <motion.span>{displayY}</motion.span>)
57
- <span className="text-canvas-light"> |</span>{" "}
58
- <motion.span>{displayScale}</motion.span>x
141
+ {showCoordinates && (
142
+ <span className={coordinatesClassName} style={coordinatesStyle}>
143
+ {formattedCoordinates !== null ? (
144
+ formattedCoordinates
145
+ ) : (
146
+ <>
147
+ (<motion.span>{displayX}</motion.span>,{" "}
148
+ <motion.span>{displayY}</motion.span>)
149
+ </>
150
+ )}
151
+ </span>
152
+ )}
153
+ {showSeparator && (
154
+ <span
155
+ className={cn("text-canvas-light", separatorClassName)}
156
+ style={separatorStyle}
157
+ >
158
+ {separator}
159
+ </span>
160
+ )}
161
+ {showScale && (
162
+ <span className={scaleClassName} style={scaleStyle}>
163
+ {formattedScale !== null ? (
164
+ formattedScale
165
+ ) : (
166
+ <>
167
+ <motion.span>{displayScale}</motion.span>x
168
+ </>
169
+ )}
170
+ </span>
171
+ )}
59
172
  </>
60
173
  ) : (
61
- <span style={{ opacity: 0 }}>(0, 0) | 1.00x</span>
174
+ <span style={{ opacity: 0 }}>{placeholderContent}</span>
62
175
  )}
63
176
  </motion.div>
64
177
  );