diff --git a/src/components/ColorPicker.css b/src/components/ColorPicker.css index 853450ac..06251805 100644 --- a/src/components/ColorPicker.css +++ b/src/components/ColorPicker.css @@ -1,28 +1,18 @@ .color-picker { - width: 205px; background: rgb(255, 255, 255); border: 0px solid rgba(0, 0, 0, 0.25); box-shadow: rgba(0, 0, 0, 0.25) 0px 1px 4px; border-radius: 4px; - position: relative; + position: absolute; + left: -5.5px; } .color-picker-control-container { - display: flex; + display: grid; + grid-template-columns: auto 1fr; align-items: center; } -.color-picker-triangle-shadow { - width: 0px; - height: 0px; - border-style: solid; - border-width: 0px 9px 10px; - border-color: transparent transparent rgba(0, 0, 0, 0.1); - position: absolute; - top: -11px; - left: 12px; -} - .color-picker-triangle { width: 0px; height: 0px; @@ -34,13 +24,20 @@ left: 12px; } -.color-picker-content { - padding: 1rem 0.5rem 0.5rem 1rem; +.color-picker-triangle-shadow { + border-color: transparent transparent rgba(0, 0, 0, 0.1); + top: -11px; } -.colors-gallery { - display: flex; - flex-wrap: wrap; +.color-picker-content { + padding: 0.5rem 0.5rem; + display: grid; + grid-template-columns: repeat(5, auto); + grid-gap: 0.5rem; +} + +.color-picker-content .color-input-container { + grid-column: 1 / span 5; } .color-picker-swatch { @@ -49,7 +46,7 @@ width: 1.875rem; cursor: pointer; border-radius: 4px; - margin: 0px 0.375rem 0.375rem 0px; + margin: 0; box-sizing: border-box; border: 1px solid #ddd; } @@ -68,6 +65,9 @@ right: 0px; bottom: 0px; left: 0px; +} +.color-picker-transparent, +.color-picker-label-swatch { background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==") left center; } @@ -81,6 +81,27 @@ display: flex; align-items: center; justify-content: center; + z-index: 1; + position: relative; +} +.color-input-container:focus-within .color-picker-hash { + box-shadow: 0 0 0 2px #a5d8ff; +} +.color-input-container:focus-within .color-picker-hash::before, +.color-input-container:focus-within .color-picker-hash::after { + content: ""; + width: 1px; + height: 100%; + position: absolute; + top: 0; +} +.color-input-container:focus-within .color-picker-hash::before { + background: #dee2e6; + right: -1px; +} +.color-input-container:focus-within .color-picker-hash::after { + background: #fff; + right: -2px; } .color-input-container { @@ -88,14 +109,14 @@ } .color-picker-input { - width: 6.25em; + width: 12ch; /* length of `transparent` + 1 */ + margin: 0; font-size: 1rem; color: #343a40; border: 0px; outline: none; height: 1.75em; box-shadow: #dee2e6 0px 0px 0px 1px inset; - box-sizing: content-box; border-radius: 0px 4px 4px 0px; float: left; padding: 1px; @@ -108,6 +129,19 @@ width: 1.875rem; margin-right: 0.25rem; border: 1px solid #dee2e6; + position: relative; + overflow: hidden; + background-color: transparent !important; +} + +.color-picker-label-swatch::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--swatch-color); } .color-picker-keybinding { diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx index 8a1b0e1e..b8351b3c 100644 --- a/src/components/ColorPicker.tsx +++ b/src/components/ColorPicker.tsx @@ -24,12 +24,14 @@ const Picker = function({ onChange, onClose, label, + showInput = true, }: { colors: string[]; color: string | null; onChange: (color: string) => void; onClose: () => void; label: string; + showInput: boolean; }) { const firstItem = React.useRef<HTMLButtonElement>(); const activeItem = React.useRef<HTMLButtonElement>(); @@ -72,7 +74,7 @@ const Picker = function({ activeElement, ); if (index !== -1) { - const length = gallery!.current!.children.length; + const length = gallery!.current!.children.length - (showInput ? 1 : 0); const nextIndex = event.key === KEYS.ARROW_RIGHT ? (index + 1) % length @@ -108,57 +110,57 @@ const Picker = function({ aria-label={t("labels.colorPicker")} onKeyDown={handleKeyDown} > - <div className="color-picker-triangle-shadow"></div> + <div className="color-picker-triangle color-picker-triangle-shadow"></div> <div className="color-picker-triangle"></div> - <div className="color-picker-content"> - <div - className="colors-gallery" - ref={el => { - if (el) { - gallery.current = el; - } - }} - > - {colors.map((_color, i) => ( - <button - className="color-picker-swatch" - onClick={() => { - onChange(_color); - }} - title={`${_color} — ${keyBindings[i].toUpperCase()}`} - aria-label={_color} - aria-keyshortcuts={keyBindings[i]} - style={{ backgroundColor: _color }} - key={_color} - ref={el => { - if (el && i === 0) { - firstItem.current = el; - } - if (el && _color === color) { - activeItem.current = el; - } - }} - onFocus={() => { - onChange(_color); - }} - > - {_color === "transparent" ? ( - <div className="color-picker-transparent"></div> - ) : ( - undefined - )} - <span className="color-picker-keybinding">{keyBindings[i]}</span> - </button> - ))} - </div> - <ColorInput - color={color} - label={label} - onChange={color => { - onChange(color); - }} - ref={colorInput} - /> + <div + className="color-picker-content" + ref={el => { + if (el) { + gallery.current = el; + } + }} + > + {colors.map((_color, i) => ( + <button + className="color-picker-swatch" + onClick={() => { + onChange(_color); + }} + title={`${_color} — ${keyBindings[i].toUpperCase()}`} + aria-label={_color} + aria-keyshortcuts={keyBindings[i]} + style={{ backgroundColor: _color }} + key={_color} + ref={el => { + if (el && i === 0) { + firstItem.current = el; + } + if (el && _color === color) { + activeItem.current = el; + } + }} + onFocus={() => { + onChange(_color); + }} + > + {_color === "transparent" ? ( + <div className="color-picker-transparent"></div> + ) : ( + undefined + )} + <span className="color-picker-keybinding">{keyBindings[i]}</span> + </button> + ))} + {showInput && ( + <ColorInput + color={color} + label={label} + onChange={color => { + onChange(color); + }} + ref={colorInput} + /> + )} </div> </div> ); @@ -188,7 +190,7 @@ const ColorInput = React.forwardRef( React.useImperativeHandle(ref, () => inputRef.current); return ( - <div className="color-input-container"> + <label className="color-input-container"> <div className="color-picker-hash">#</div> <input spellCheck={false} @@ -206,7 +208,7 @@ const ColorInput = React.forwardRef( onBlur={() => setInnerValue(color)} ref={inputRef} /> - </div> + </label> ); }, ); @@ -231,7 +233,11 @@ export function ColorPicker({ <button className="color-picker-label-swatch" aria-label={label} - style={color ? { backgroundColor: color } : undefined} + style={ + color + ? ({ "--swatch-color": color } as React.CSSProperties) + : undefined + } onClick={() => setActive(!isActive)} ref={pickerButton} /> @@ -257,6 +263,7 @@ export function ColorPicker({ pickerButton.current?.focus(); }} label={label} + showInput={false} /> </Popover> ) : null} diff --git a/src/components/Dialog.scss b/src/components/Dialog.scss index f696e042..2668f694 100644 --- a/src/components/Dialog.scss +++ b/src/components/Dialog.scss @@ -1,7 +1,6 @@ @import "../_variables"; .Dialog__title { - --metric: calc(var(--space-factor) * 4); display: grid; align-items: center; margin-top: 0; @@ -18,15 +17,23 @@ } @media #{$media-query} { + .Dialog { + --metric: calc(var(--space-factor) * 4); + --inset-left: #{"max(var(--metric), env(safe-area-inset-left))"}; + --inset-right: #{"max(var(--metric), env(safe-area-inset-right))"}; + } .Dialog__title { grid-template-columns: calc(var(--space-factor) * 7) 1fr calc( var(--space-factor) * 7 ); position: sticky; top: calc(-1 * var(--metric)); - margin: calc(-1 * var(--metric)); + margin: calc(-1 * var(--inset-right)); + margin-top: calc(-1 * var(--metric)); margin-bottom: var(--metric); - padding: calc(var(--space-factor) * 2) var(--metric); + padding: calc(var(--space-factor) * 2); + padding-left: var(--inset-left); + padding-right: var(--inset-right); background: white; font-size: 1.25em; @@ -38,9 +45,13 @@ text-align: center; } .Dialog .Island { + width: 100vw; height: 100%; box-sizing: border-box; overflow-y: auto; + padding-left: #{"max(calc(var(--padding) * var(--space-factor)), env(safe-area-inset-left))"}; + padding-right: #{"max(calc(var(--padding) * var(--space-factor)), env(safe-area-inset-right))"}; + padding-bottom: #{"max(calc(var(--padding) * var(--space-factor)), env(safe-area-inset-bottom))"}; } .Dialog .Modal__close { diff --git a/src/components/ExportDialog.scss b/src/components/ExportDialog.scss index 71c335db..6a88e3c1 100644 --- a/src/components/ExportDialog.scss +++ b/src/components/ExportDialog.scss @@ -16,16 +16,30 @@ } .ExportDialog__actions { + width: 100%; display: flex; + grid-gap: calc(var(--space-factor) * 2); align-items: top; justify-content: space-between; - flex-wrap: wrap; } -.ExportDialog__scales { - display: flex; - align-items: baseline; - justify-content: flex-end; +.ExportDialog__name { + grid-column: project-name; + margin: auto; +} + +@media (max-width: 550px) { + .ExportDialog { + display: flex; + flex-direction: column; + } + .ExportDialog__actions { + flex-direction: column; + align-items: center; + } + .ExportDialog__actions > * { + margin-bottom: calc(var(--space-factor) * 3); + } } @media #{$media-query} { @@ -40,13 +54,4 @@ .ExportDialog__dialog .Island { overflow-y: auto; } - .ExportDialog__actions { - flex-direction: column; - } - .ExportDialog__actions > * { - margin-bottom: calc(var(--space-factor) * 3); - } - .ExportDialog__scales { - justify-content: flex-start; - } } diff --git a/src/components/ExportDialog.tsx b/src/components/ExportDialog.tsx index 8cd6845a..ff8cc7f2 100644 --- a/src/components/ExportDialog.tsx +++ b/src/components/ExportDialog.tsx @@ -111,10 +111,10 @@ function ExportModal({ } return ( - <div onKeyDown={handleKeyDown}> + <div onKeyDown={handleKeyDown} className="ExportDialog"> <div className="ExportDialog__preview" ref={previewRef}></div> - <div className="ExportDialog__actions"> - <Stack.Col gap={1}> + <Stack.Col gap={2} align="center"> + <div className="ExportDialog__actions"> <Stack.Row gap={2}> <ToolButton type="button" @@ -148,44 +148,42 @@ function ExportModal({ onClick={() => onExportToBackend(exportedElements)} /> </Stack.Row> - </Stack.Col> - {actionManager.renderAction("changeProjectName")} - <Stack.Col gap={1}> - <div className="ExportDialog__scales"> - <Stack.Row gap={2} align="baseline"> - {scales.map(s => ( - <ToolButton - key={s} - size="s" - type="radio" - icon={`x${s}`} - name="export-canvas-scale" - aria-label={`Scale ${s} x`} - id="export-canvas-scale" - checked={s === scale} - onChange={() => setScale(s)} - /> - ))} - </Stack.Row> + <div className="ExportDialog__name"> + {actionManager.renderAction("changeProjectName")} </div> - {actionManager.renderAction("changeExportBackground")} - {someElementIsSelected && ( - <div> - <label> - <input - type="checkbox" - checked={exportSelected} - onChange={event => - setExportSelected(event.currentTarget.checked) - } - ref={onlySelectedInput} - />{" "} - {t("labels.onlySelected")} - </label> - </div> - )} - </Stack.Col> - </div> + <Stack.Row gap={2}> + {scales.map(s => ( + <ToolButton + key={s} + size="s" + type="radio" + icon={`x${s}`} + name="export-canvas-scale" + aria-label={`Scale ${s} x`} + id="export-canvas-scale" + checked={s === scale} + onChange={() => setScale(s)} + /> + ))} + </Stack.Row> + </div> + {actionManager.renderAction("changeExportBackground")} + {someElementIsSelected && ( + <div> + <label> + <input + type="checkbox" + checked={exportSelected} + onChange={event => + setExportSelected(event.currentTarget.checked) + } + ref={onlySelectedInput} + />{" "} + {t("labels.onlySelected")} + </label> + </div> + )} + </Stack.Col> </div> ); } diff --git a/src/components/Popover.tsx b/src/components/Popover.tsx index 7e981073..c033040d 100644 --- a/src/components/Popover.tsx +++ b/src/components/Popover.tsx @@ -1,4 +1,4 @@ -import React, { useLayoutEffect, useRef } from "react"; +import React, { useLayoutEffect, useRef, useEffect } from "react"; import "./Popover.css"; type Props = { @@ -35,18 +35,20 @@ export function Popover({ } }, [fitInViewport]); + useEffect(() => { + if (onCloseRequest) { + const handler = (e: Event) => { + if (!popoverRef.current?.contains(e.target as Node)) { + onCloseRequest(); + } + }; + document.addEventListener("pointerdown", handler, false); + return () => document.removeEventListener("pointerdown", handler, false); + } + }, [onCloseRequest]); + return ( <div className="popover" style={{ top: top, left: left }} ref={popoverRef}> - <div - className="cover" - onClick={onCloseRequest} - onContextMenu={event => { - event.preventDefault(); - if (onCloseRequest) { - onCloseRequest(); - } - }} - /> {children} </div> ); diff --git a/src/components/ProjectName.css b/src/components/ProjectName.css index 5e66194a..07079b2c 100644 --- a/src/components/ProjectName.css +++ b/src/components/ProjectName.css @@ -2,9 +2,8 @@ display: inline-block; cursor: pointer; border: 1.5px solid #eee; - height: 2.5rem; - line-height: 2.5rem; - padding: 0 0.5rem; + line-height: 1; + padding: 0.75rem; white-space: nowrap; border-radius: var(--space-factor); } diff --git a/src/components/ToolIcon.scss b/src/components/ToolIcon.scss index 68a7bac3..30308aba 100644 --- a/src/components/ToolIcon.scss +++ b/src/components/ToolIcon.scss @@ -13,7 +13,6 @@ cursor: pointer; background-color: var(--button-gray-1); -webkit-tap-highlight-color: transparent; - border-radius: var(--space-factor); }