diff --git a/src/main/frontend/extensions/tldraw.cljs b/src/main/frontend/extensions/tldraw.cljs index 421213ea97..b4ff57928d 100644 --- a/src/main/frontend/extensions/tldraw.cljs +++ b/src/main/frontend/extensions/tldraw.cljs @@ -31,7 +31,11 @@ (rum/defc breadcrumb [props] - (block/breadcrumb {:preview? true} (state/get-current-repo) (uuid (gobj/get props "blockId")) {:end-separator? true})) + (block/breadcrumb {:preview? true} + (state/get-current-repo) + (uuid (gobj/get props "blockId")) + {:end-separator? true + :level-limit (gobj/get props "levelLimit" 3)})) (rum/defc page-name-link [props] diff --git a/tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx b/tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx index da36ccb2fa..08e8af6ae1 100644 --- a/tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx +++ b/tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx @@ -20,6 +20,7 @@ import { Button } from '../Button' import { TablerIcon } from '../icons' import { ColorInput } from '../inputs/ColorInput' import { SelectInput, type SelectOption } from '../inputs/SelectInput' +import { ShapeLinksInput } from '../inputs/ShapeLinksInput' import { TextInput } from '../inputs/TextInput' import { ToggleGroupInput, @@ -364,7 +365,6 @@ const SwatchAction = observer(() => { popoverSide="top" color={color} opacity={shapes[0].props.opacity} - collisionRef={document.getElementById('main-content-container')} setOpacity={handleSetOpacity} setColor={handleSetColor} /> @@ -504,80 +504,20 @@ const LinksAction = observer(() => { const app = useApp() const shape = app.selectedShapesArray[0] - const [value, setValue] = React.useState('') - - const [show, setShow] = React.useState(false) - - const { handlers } = React.useContext(LogseqContext) - - const handleChange = () => { - const refs = shape.props.refs ?? [] - if (refs.includes(value)) return - shape.update({ refs: [...refs, value] }) + const handleChange = (refs: string[]) => { + shape.update({ refs: refs }) app.persist() } - const hasLinks = shape.props.refs && shape.props.refs.length > 0 - return ( - - setShow(s)} - title="Open References & Links" - > - - {hasLinks &&
{shape.props.refs?.length}
} -
- - {show && ( -
- { - setValue(e.target.value) - }} - onKeyDown={e => { - if (e.key === 'Enter') { - handleChange() - } - e.stopPropagation() - }} - /> -
- - Your Links -
- {shape.props.refs?.map((ref, i) => { - return ( -
-
{ref}
-
- - -
- ) - })} -
- )} - + ) }) diff --git a/tldraw/apps/tldraw-logseq/src/components/PopoverButton/PopoverButton.tsx b/tldraw/apps/tldraw-logseq/src/components/PopoverButton/PopoverButton.tsx new file mode 100644 index 0000000000..2ce56da974 --- /dev/null +++ b/tldraw/apps/tldraw-logseq/src/components/PopoverButton/PopoverButton.tsx @@ -0,0 +1,78 @@ +import * as Popover from '@radix-ui/react-popover' +import type { Side } from '@radix-ui/react-popper' +import { BoundsUtils } from '@tldraw/core' +import { useApp } from '@tldraw/react' +import { observer } from 'mobx-react-lite' +import * as React from 'react' + +interface PopoverButton extends React.HTMLAttributes { + side: Side // default side + label: React.ReactNode + children: React.ReactNode + border?: boolean + arrow?: boolean +} + +const sideAndOpposite = { + top: 'bottom', + bottom: 'top', + left: 'right', + right: 'left', +} as const + +export const PopoverButton = observer( + ({ side, label, arrow, children, border, ...rest }: PopoverButton) => { + const contentRef = React.useRef(null) + + const [isOpen, setIsOpen] = React.useState(false) + + const { + viewport: { + bounds, + camera: { point, zoom }, + }, + } = useApp() + + const [tick, setTick] = React.useState(0) + + // Change side if popover is out of bounds + React.useEffect(() => { + if (!contentRef.current || !isOpen) return + + const boundingRect = contentRef.current.getBoundingClientRect() + const outOfView = !BoundsUtils.boundsContain(bounds, { + minX: boundingRect.x, + minY: boundingRect.y, + maxX: boundingRect.right, + maxY: boundingRect.bottom, + width: boundingRect.width, + height: boundingRect.height, + }) + + if (outOfView) { + setTick(tick => tick + 1) + } + }, [point[0], point[1], zoom, isOpen]) + + return ( + setIsOpen(o)}> + + {label} + + + + {children} + {arrow && } + + + ) + } +) diff --git a/tldraw/apps/tldraw-logseq/src/components/PopoverButton/index.ts b/tldraw/apps/tldraw-logseq/src/components/PopoverButton/index.ts new file mode 100644 index 0000000000..da8743bee0 --- /dev/null +++ b/tldraw/apps/tldraw-logseq/src/components/PopoverButton/index.ts @@ -0,0 +1 @@ +export * from './PopoverButton' diff --git a/tldraw/apps/tldraw-logseq/src/components/PrimaryTools/PrimaryTools.tsx b/tldraw/apps/tldraw-logseq/src/components/PrimaryTools/PrimaryTools.tsx index 2956c87e33..c751c4c617 100644 --- a/tldraw/apps/tldraw-logseq/src/components/PrimaryTools/PrimaryTools.tsx +++ b/tldraw/apps/tldraw-logseq/src/components/PrimaryTools/PrimaryTools.tsx @@ -33,7 +33,6 @@ export const PrimaryTools = observer(function PrimaryTools() { title="Color Picker" popoverSide="left" color={app.settings.color} - collisionRef={document.getElementById('main-content-container')} setColor={handleSetColor} />
diff --git a/tldraw/apps/tldraw-logseq/src/components/inputs/ColorInput.tsx b/tldraw/apps/tldraw-logseq/src/components/inputs/ColorInput.tsx index b37460855e..77006bc66d 100644 --- a/tldraw/apps/tldraw-logseq/src/components/inputs/ColorInput.tsx +++ b/tldraw/apps/tldraw-logseq/src/components/inputs/ColorInput.tsx @@ -1,14 +1,12 @@ -import * as React from 'react' -import * as Popover from '@radix-ui/react-popover' import type { Side } from '@radix-ui/react-popper' import * as Slider from '@radix-ui/react-slider' -import { TablerIcon } from '../icons' import { Color } from '@tldraw/core' +import { TablerIcon } from '../icons' +import { PopoverButton } from '../PopoverButton' -interface ColorInputProps extends React.InputHTMLAttributes { +interface ColorInputProps extends React.HTMLAttributes { color?: string opacity?: number - collisionRef: HTMLElement | null popoverSide: Side setColor: (value: string) => void setOpacity?: (value: number) => void @@ -17,15 +15,12 @@ interface ColorInputProps extends React.InputHTMLAttributes { export function ColorInput({ color, opacity, - collisionRef, popoverSide, setColor, setOpacity, ...rest }: ColorInputProps) { - const ref = React.useRef(null) - - function renderColor(color: string) { + function renderColor(color?: string) { return color ? (
@@ -38,15 +33,8 @@ export function ColorInput({ } return ( - - {renderColor(color)} - - + +
{Object.values(Color).map(value => (
)} - - - - +
+
) } diff --git a/tldraw/apps/tldraw-logseq/src/components/inputs/ShapeLinksInput.tsx b/tldraw/apps/tldraw-logseq/src/components/inputs/ShapeLinksInput.tsx new file mode 100644 index 0000000000..cfc037fa6f --- /dev/null +++ b/tldraw/apps/tldraw-logseq/src/components/inputs/ShapeLinksInput.tsx @@ -0,0 +1,133 @@ +import type { Side } from '@radix-ui/react-popper' +import { validUUID } from '@tldraw/core' +import React from 'react' +import { LogseqContext } from '../../lib/logseq-context' +import { Button } from '../Button' +import { TablerIcon } from '../icons' +import { PopoverButton } from '../PopoverButton' +import { TextInput } from './TextInput' + +interface ShapeLinksInputProps extends React.HTMLAttributes { + shapeType: string + side: Side + refs: string[] + pageId?: string // the portal referenced block id or page name + portalType?: 'B' | 'P' + onRefsChange: (value: string[]) => void +} + +function ShapeLinkItem({ + id, + type, + onRemove, +}: { + id: string + type: 'B' | 'P' + onRemove?: () => void +}) { + const { + handlers, + renderers: { Breadcrumb, PageNameLink }, + } = React.useContext(LogseqContext) + + return ( +
+ + {type === 'P' ? : } +
+ + + {onRemove && ( + + )} +
+ ) +} + +export function ShapeLinksInput({ + pageId, + portalType, + shapeType, + refs, + side, + onRefsChange, + ...rest +}: ShapeLinksInputProps) { + const noOfLinks = refs.length + (pageId ? 1 : 0) + const [value, setValue] = React.useState('') + + return ( + + + {noOfLinks > 0 &&
{noOfLinks}
} +
+ } + > +
+ {pageId && portalType && ( +
+
+ + Your Reference +
+
+ +
+ )} +
+
+ + Your Links +
+
+
+ This {shapeType} can be linked to any other block, page or whiteboard + element you have stored in Logseq. +
+ { + setValue(e.target.value) + }} + onKeyDown={e => { + if (e.key === 'Enter') { + if (value && !refs.includes(value)) { + onRefsChange([...refs, value]) + } + } + e.stopPropagation() + }} + /> +
+ {refs.map((ref, i) => { + return ( + { + onRefsChange(refs.filter((_, j) => i !== j)) + }} + /> + ) + })} +
+
+
+ + ) +} diff --git a/tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts b/tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts index c1a8af1e4e..a8420fc2f2 100644 --- a/tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts +++ b/tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts @@ -16,6 +16,7 @@ export interface LogseqContextValue { }> Breadcrumb: React.FC<{ blockId: string + levelLimit?: number }> PageNameLink: React.FC<{ pageName: string diff --git a/tldraw/apps/tldraw-logseq/src/styles.css b/tldraw/apps/tldraw-logseq/src/styles.css index 7c3b7df8ff..c35b51bfac 100644 --- a/tldraw/apps/tldraw-logseq/src/styles.css +++ b/tldraw/apps/tldraw-logseq/src/styles.css @@ -125,7 +125,7 @@ html[data-theme='light'] { margin-left: auto; padding-left: 20px; - .keyboard-shortcut>code { + .keyboard-shortcut > code { padding: 4px !important; text-rendering: initial; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', @@ -349,7 +349,7 @@ button.tl-select-input-trigger { bottom: -3px; } -.floating-panel[data-tool-locked='true']>.tl-button[data-selected='true']::after { +.floating-panel[data-tool-locked='true'] > .tl-button[data-selected='true']::after { @apply block absolute; content: ''; @@ -602,7 +602,7 @@ button.tl-select-input-trigger { background-color: rgba(0, 0, 0, 0.5) !important; } - >i.ti { + > i.ti { transform: translateY(-0.5px); } } @@ -721,7 +721,7 @@ button.tl-select-input-trigger { .page-ref { color: var(--ls-title-text-color); - background: var(--ls-tertiary-background-color)); + background: var(--ls-tertiary-background-color); } .breadcrumb { @@ -750,7 +750,7 @@ button.tl-select-input-trigger { .tl-image-shape-container { @apply h-full w-full overflow-hidden flex items-center justify-center pointer-events-auto; - &[data-asset-loaded="false"] { + &[data-asset-loaded='false'] { background-color: var(--ls-secondary-background-color); } } @@ -770,7 +770,7 @@ button.tl-select-input-trigger { width: fit-content; } -.tl-html-anchor>iframe { +.tl-html-anchor > iframe { @apply h-full w-full !important; margin: 0; } @@ -778,7 +778,7 @@ button.tl-select-input-trigger { .tl-video-container { @apply h-full w-full m-0 relative; - >video { + > video { @apply h-full w-full m-0 relative; } } @@ -905,7 +905,7 @@ html[data-theme='dark'] { } .tl-popover-content { - @apply rounded-sm drop-shadow-md; + @apply rounded-lg drop-shadow-md; background-color: var(--ls-secondary-background-color); z-index: 100000; @@ -951,8 +951,7 @@ html[data-theme='dark'] { user-select: none; touch-action: none; - background: url("../img/checker.png"); - + background: url('../img/checker.png'); } .tl-slider-track { @@ -961,11 +960,11 @@ html[data-theme='dark'] { background: linear-gradient(90deg, transparent, var(--ls-tertiary-background-color)); border: 1px solid var(--ls-secondary-border-color); - &[data-orientation="horizontal"] { + &[data-orientation='horizontal'] { height: 10px; } - &[data-orientation="vertical"] { + &[data-orientation='vertical'] { width: 10px; } } @@ -999,8 +998,8 @@ html[data-theme='dark'] { border-top-right-radius: 6px; border-bottom-right-radius: 6px; - - &:hover, &.open { + &:hover, + &.open { @apply p-1.5; } } @@ -1011,28 +1010,53 @@ html[data-theme='dark'] { height: 32px; transform: translate(4px, -6px); - &:hover, &.open { + &:hover, + &.open { height: 36px; width: 36px; } } .tl-shape-links-count { - @apply px-1; + @apply px-1 rounded-sm; background-color: var(--ls-page-properties-background-color); } -.tl-shape-links-panel { - @apply absolute shadow-lg rounded-lg p-3; +.tl-shape-links-panel, +.tl-shape-links-reference-panel { + @apply p-3; width: 320px; - transform: translate(20px, -8px); - left: 100%; - top: 0; - background-color: var(--ls-secondary-background-color); + color: var(--ls-primary-text-color); +} + +.tl-shape-links-reference-panel { + @apply rounded-t-lg; } .tl-shape-links-panel-item { - @apply rounded py-1 px-4 flex items-center justify-center gap-1; + @apply rounded py-1 px-4 pr-2 flex items-center justify-center gap-1; color: var(--color-text); - background-color: var(--ls-tertiary-background-color); + + .page-ref { + color: var(--ls-title-text-color); + } +} + +.tl-popover-trigger-button { + @apply rounded text-sm; + + min-width: 32px; + height: 32px; + padding: 3px; + color: var(--ls-secondary-text-color); + + &[data-border='true'] { + border: 1px solid var(--ls-secondary-border-color); + } + + &[data-state='open'] { + background-color: var(--ls-tertiary-background-color); + color: var(--ls-primary-text-color); + opacity: 1; + } } diff --git a/tldraw/demo/src/App.jsx b/tldraw/demo/src/App.jsx index 47ba96d3a0..31a06007e2 100644 --- a/tldraw/demo/src/App.jsx +++ b/tldraw/demo/src/App.jsx @@ -209,7 +209,7 @@ export default function App() { }, []) return ( -
+