feat: links panel

This commit is contained in:
Peng Xiao
2022-11-20 16:11:12 +08:00
parent c426c21004
commit 4e63a82d27
12 changed files with 366 additions and 122 deletions

View File

@@ -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]

View File

@@ -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<Shape>()
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 (
<span className="flex gap-3 relative">
<ToggleInput
className="px-2 tl-button"
pressed={show}
onPressedChange={s => setShow(s)}
title="Open References & Links"
>
<TablerIcon name="link" />
{hasLinks && <div className="tl-shape-links-count">{shape.props.refs?.length}</div>}
</ToggleInput>
{show && (
<div className="tl-shape-links-panel">
<TextInput
title="Website Url"
className="tl-iframe-src"
value={value}
onChange={e => {
setValue(e.target.value)
}}
onKeyDown={e => {
if (e.key === 'Enter') {
handleChange()
}
e.stopPropagation()
}}
/>
<div className="text-xs font-bold inline-flex gap-1 items-center">
<TablerIcon name="link" />
Your Links
</div>
{shape.props.refs?.map((ref, i) => {
return (
<div className="tl-shape-links-panel-item">
<div>{ref}</div>
<div className="flex-1" />
<Button
title="Open Page in Right Sidebar"
type="button"
onClick={() => handlers?.sidebarAddBlock(ref, validUUID(ref) ? 'block' : 'page')}
>
<TablerIcon name="layout-sidebar-right" />
</Button>
<button
className="hover:opacity-60"
onClick={() => {
shape.update({ refs: shape.props.refs?.filter((_, j) => j !== i) })
app.persist()
}}
>
<TablerIcon name="x" />
</button>
</div>
)
})}
</div>
)}
</span>
<ShapeLinksInput
onRefsChange={handleChange}
refs={shape.props.refs ?? []}
shapeType={shape.props.type}
side="right"
pageId={shape.props.type === 'logseq-portal' ? shape.props.pageId : undefined}
portalType={shape.props.type === 'logseq-portal' ? shape.props.blockType : undefined}
/>
)
})

View File

@@ -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<HTMLButtonElement> {
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<HTMLDivElement>(null)
const [isOpen, setIsOpen] = React.useState(false)
const {
viewport: {
bounds,
camera: { point, zoom },
},
} = useApp()
const [tick, setTick] = React.useState<number>(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 (
<Popover.Root onOpenChange={o => setIsOpen(o)}>
<Popover.Trigger {...rest} data-border={border} className="tl-popover-trigger-button">
{label}
</Popover.Trigger>
<Popover.Content
// it seems like the Popover.Content component doesn't update collission when camera changes
key={'popover-content-' + tick}
ref={contentRef}
className="tl-popover-content"
side={side}
sideOffset={15}
collisionBoundary={document.querySelector('.logseq-tldraw')}
>
{children}
{arrow && <Popover.Arrow className="tl-popover-arrow" />}
</Popover.Content>
</Popover.Root>
)
}
)

View File

@@ -0,0 +1 @@
export * from './PopoverButton'

View File

@@ -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}
/>
</div>

View File

@@ -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<HTMLButtonElement> {
interface ColorInputProps extends React.HTMLAttributes<HTMLButtonElement> {
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<HTMLButtonElement> {
export function ColorInput({
color,
opacity,
collisionRef,
popoverSide,
setColor,
setOpacity,
...rest
}: ColorInputProps) {
const ref = React.useRef<HTMLDivElement>(null)
function renderColor(color: string) {
function renderColor(color?: string) {
return color ? (
<div className="tl-color-bg" style={{ backgroundColor: color }}>
<div className={`w-full h-full bg-${color}-500`}></div>
@@ -38,15 +33,8 @@ export function ColorInput({
}
return (
<Popover.Root>
<Popover.Trigger className="tl-color-drip">{renderColor(color)}</Popover.Trigger>
<Popover.Content
className="tl-popover-content p-1"
side={popoverSide}
sideOffset={15}
collisionBoundary={collisionRef}
>
<PopoverButton {...rest} border arrow side={popoverSide} label={renderColor(color)}>
<div className="p-1">
<div className={'tl-color-palette'}>
{Object.values(Color).map(value => (
<button
@@ -62,7 +50,7 @@ export function ColorInput({
{setOpacity && (
<div className="mx-1 my-2">
<Slider.Root
defaultValue={[opacity]}
defaultValue={[opacity ?? 0]}
onValueCommit={value => setOpacity(value[0])}
max={1}
step={0.1}
@@ -76,9 +64,7 @@ export function ColorInput({
</Slider.Root>
</div>
)}
<Popover.Arrow className="tl-popover-arrow" />
</Popover.Content>
</Popover.Root>
</div>
</PopoverButton>
)
}

View File

@@ -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<HTMLButtonElement> {
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 (
<div className="tl-shape-links-panel-item color-level">
<TablerIcon name={type === 'P' ? 'page' : 'block'} />
{type === 'P' ? <PageNameLink pageName={id} /> : <Breadcrumb levelLimit={2} blockId={id} />}
<div className="flex-1" />
<Button title="Open Page" type="button" onClick={() => handlers?.redirectToPage(id)}>
<TablerIcon name="external-link" />
</Button>
<Button
title="Open Page in Right Sidebar"
type="button"
onClick={() => handlers?.sidebarAddBlock(id, type === 'B' ? 'block' : 'page')}
>
<TablerIcon name="layout-sidebar-right" />
</Button>
{onRemove && (
<Button title="Remove link" type="button" onClick={onRemove}>
<TablerIcon name="x" />
</Button>
)}
</div>
)
}
export function ShapeLinksInput({
pageId,
portalType,
shapeType,
refs,
side,
onRefsChange,
...rest
}: ShapeLinksInputProps) {
const noOfLinks = refs.length + (pageId ? 1 : 0)
const [value, setValue] = React.useState('')
return (
<PopoverButton
{...rest}
side={side}
label={
<div className="flex gap-1 relative items-center justify-center px-1">
<TablerIcon name="link" />
{noOfLinks > 0 && <div className="tl-shape-links-count">{noOfLinks}</div>}
</div>
}
>
<div className="color-level">
{pageId && portalType && (
<div className="tl-shape-links-reference-panel">
<div className="text-base font-bold inline-flex gap-1 items-center">
<TablerIcon name="external-link" />
Your Reference
</div>
<div className="h-2" />
<ShapeLinkItem type={portalType} id={pageId} />
</div>
)}
<div className="tl-shape-links-panel color-level">
<div className="text-base font-bold inline-flex gap-1 items-center">
<TablerIcon name="link" />
Your Links
</div>
<div className="h-2" />
<div className="whitespace-pre-wrap">
This <strong>{shapeType}</strong> can be linked to any other block, page or whiteboard
element you have stored in Logseq.
</div>
<TextInput
value={value}
onChange={e => {
setValue(e.target.value)
}}
onKeyDown={e => {
if (e.key === 'Enter') {
if (value && !refs.includes(value)) {
onRefsChange([...refs, value])
}
}
e.stopPropagation()
}}
/>
<div className="flex flex-col items-stretch gap-2">
{refs.map((ref, i) => {
return (
<ShapeLinkItem
key={ref}
id={ref}
type={validUUID(ref) ? 'B' : 'P'}
onRemove={() => {
onRefsChange(refs.filter((_, j) => i !== j))
}}
/>
)
})}
</div>
</div>
</div>
</PopoverButton>
)
}

View File

@@ -16,6 +16,7 @@ export interface LogseqContextValue {
}>
Breadcrumb: React.FC<{
blockId: string
levelLimit?: number
}>
PageNameLink: React.FC<{
pageName: string

View File

@@ -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;
}
}

View File

@@ -209,7 +209,7 @@ export default function App() {
}, [])
return (
<div className={`h-screen w-screen`}>
<div className={`h-screen w-screen`} id="main-content-container">
<ThemeSwitcher />
<PreviewButton model={model} />
<TldrawApp

View File

@@ -1,3 +1,81 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.color-level {
background-color: var(--color-level-1);
}
.color-level .color-level {
background-color: var(--color-level-2);
}
.color-level .color-level .color-level {
background-color: var(--color-level-3);
}
.color-level .color-level .color-level .color-level {
background-color: var(--color-level-4);
}
.color-level .color-level .color-level .color-level .color-level {
background-color: var(--color-level-5);
}
.color-level .color-level .color-level .color-level .color-level .color-level {
background-color: var(--color-level-3);
}
.color-level .color-level .color-level .color-level .color-level .color-level .color-level {
background-color: var(--color-level-4);
}
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level {
background-color: var(--color-level-5);
}
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level {
background-color: var(--color-level-3);
}
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level {
background-color: var(--color-level-4);
}
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level
.color-level {
background-color: var(--color-level-5);
}

View File

@@ -21,7 +21,7 @@
"postinstall": "yarn build",
"dev": "cd demo && yarn dev",
"fix:style": "yarn run pretty-quick",
"pretty-quick": "pretty-quick --pattern 'tldraw/**/*.{js,jsx,ts,tsx,html}'"
"pretty-quick": "pretty-quick --pattern 'tldraw/**/*.{js,jsx,ts,tsx,css,html}'"
},
"devDependencies": {
"@types/node": "^17.0.42",