feat: finish double click to create poc

pull/5742/head
Peng Xiao 2022-06-18 03:06:40 +08:00
parent f487c5817c
commit 20b81fbd1e
16 changed files with 240 additions and 66 deletions

View File

@ -1,3 +1,2 @@
.logseq-tldraw .tl-container {
background-color: var(--ls-secondary-background-color);
}

View File

@ -51,6 +51,8 @@ export const DevTools = observer(() => {
<div
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
}}
>
{rendererStatusText}

View File

@ -17,7 +17,7 @@ export const StatusBar = observer(function StatusBar() {
<div className="statusbar">
{app.selectedTool.id} | {app.selectedTool.currentState.id}
<div style={{ flex: 1 }} />
<div id="tl-statusbar-anchor" />
<div id="tl-statusbar-anchor" style={{ display: 'flex' }} />
</div>
)
})

View File

@ -30,7 +30,7 @@ export class LineShape extends TLLineShape<LineShapeProps> {
start: { id: 'start', canBind: true, point: [0, 0] },
end: { id: 'end', canBind: true, point: [1, 1] },
},
stroke: '#000000',
stroke: 'var(--tl-foreground)',
fill: '#ffffff',
strokeWidth: 1,
opacity: 1,

View File

@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TLBoxShape, TLBoxShapeProps } from '@tldraw/core'
import { HTMLContainer, TLComponentProps, useApp } from '@tldraw/react'
import { MagnifyingGlassIcon } from '@radix-ui/react-icons'
import { makeObservable, transaction } from 'mobx'
import { observer } from 'mobx-react-lite'
import * as React from 'react'
import { TextInput } from '~components/inputs/TextInput'
import { useCameraMovingRef } from '~hooks/useCameraMoving'
import type { Shape } from '~lib'
import { LogseqContext } from '~lib/logseq-context'
@ -56,28 +56,34 @@ const LogseqQuickSearch = observer(({ onChange }: LogseqQuickSearchProps) => {
}, [])
return (
<>
<TextInput
<div className="tl-quick-search">
<div className="tl-quick-search-input-container">
<MagnifyingGlassIcon className="tl-quick-search-icon" width={24} height={24} />
<div className="tl-quick-search-input-sizer" data-value={q}>
<input
ref={rInput}
label="Page name or block UUID"
type="text"
value={q}
placeholder="Search or create page"
onChange={handleChange}
onKeyDown={e => {
if (e.key === 'Enter') {
commitChange(q)
}
}}
list="logseq-portal-search-results"
className="tl-quick-search-input text-input"
/>
<datalist id="logseq-portal-search-results">
{options?.map(option => (
<option key={option} value={secretPrefix + option}>
{option}
</option>
</div>
</div>
<div className="tl-quick-search-options">
{options?.map(name => (
<div key={name} className="tl-quick-search-option" onClick={() => commitChange(name)}>
{name}
</div>
))}
</datalist>
</>
</div>
</div>
)
})
@ -90,7 +96,7 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
type: 'logseq-portal',
parentId: 'page',
point: [0, 0],
size: [180, 75],
size: [600, 50],
stroke: 'transparent',
fill: 'var(--ls-secondary-background-color)',
strokeWidth: 2,
@ -104,15 +110,9 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
canActivate = true
canEdit = true
constructor(props = {} as Partial<LogseqPortalShapeProps>) {
super(props)
makeObservable(this)
this.draft = true
}
ReactComponent = observer(({ events, isErasing, isActivated }: TLComponentProps) => {
const {
props: { opacity, pageId, strokeWidth, stroke },
props: { opacity, pageId, strokeWidth, stroke, fill },
} = this
const app = useApp<Shape>()
@ -141,13 +141,15 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
})
}, [])
if (!Page) {
return null // not being correctly configured
}
return (
<HTMLContainer
style={{
overflow: 'hidden',
pointerEvents: 'all',
opacity: isErasing ? 0.2 : opacity,
backgroundColor: 'var(--ls-primary-background-color)',
}}
{...events}
>
@ -156,6 +158,8 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
onPointerDown={stop}
onPointerUp={stop}
style={{
width: '100%',
height: '100%',
pointerEvents: isActivated ? 'all' : 'none',
}}
>
@ -166,20 +170,42 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
style={{
width: '100%',
overflow: 'auto',
borderRadius: '8px',
overscrollBehavior: 'none',
height: pageId ? 'calc(100% - 33px)' : '100%',
userSelect: 'none',
height: '100%',
display: 'flex',
flexDirection: 'column',
background: fill,
boxShadow: isActivated
? '0px 0px 0 var(--tl-binding-distance) var(--tl-binding)'
: '',
opacity: isSelected ? 0.5 : 1,
opacity: isSelected ? 0.8 : 1,
}}
>
<div className="tl-logseq-portal-header">
<span className="text-xs rounded border mr-2 px-1">P</span>
{pageId}
</div>
<div
style={{
width: '100%',
overflow: 'auto',
borderRadius: '8px',
overscrollBehavior: 'none',
// height: '100%',
flex: 1,
}}
>
<div
style={{
padding: '12px',
height: '100%',
cursor: 'default',
}}
>
{pageId && Page ? (
<div style={{ padding: '12px', height: '100%', cursor: 'default' }}>
<Page pageId={pageId} />
</div>
) : null}
</div>
</div>
)}
</div>

View File

@ -25,7 +25,7 @@ export class PencilShape extends TLDrawShape<PencilShapeProps> {
point: [0, 0],
points: [],
isComplete: false,
stroke: '#000000',
stroke: 'var(--tl-foreground)',
fill: '#ffffff',
strokeWidth: 2,
opacity: 1,

View File

@ -4,7 +4,6 @@ import { HTMLContainer, TLComponentProps, TLTextMeasure } from '@tldraw/react'
import { TextUtils, TLBounds, TLResizeStartInfo, TLTextShape, TLTextShapeProps } from '@tldraw/core'
import { observer } from 'mobx-react-lite'
import { CustomStyleProps, withClampedStyles } from './style-props'
import { NumberInput } from '~components/inputs/NumberInput'
export interface TextShapeProps extends TLTextShapeProps, CustomStyleProps {
borderRadius: number
@ -33,7 +32,7 @@ export class TextShape extends TLTextShape<TextShapeProps> {
padding: 4,
fontFamily: "'Helvetica Neue', Helvetica, Arial, sans-serif",
borderRadius: 0,
stroke: '#000000',
stroke: 'var(--tl-foreground)',
fill: '#ffffff',
strokeWidth: 2,
opacity: 1,

View File

@ -1,10 +1,10 @@
/* TODO: move to useStylesheet */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500&display=swap');
:root {
--color-panel: #ffffff;
--color-text: #000000;
--color-hover: #00000011;
.logseq-tldraw {
--color-panel: var(--ls-secondary-background-color);
--color-text: var(--ls-primary-text-color);
--color-hover: var(--ls-tertiary-background-color);
--color-selectedStroke: rgb(42, 123, 253);
--color-selectedFill: rgba(66, 133, 244);
--color-selectedContrast: #ffffff;
@ -32,6 +32,7 @@
cursor: pointer;
border-radius: 2px;
padding: 4px 8px;
opacity: 1;
}
.logseq-tldraw .toolbar {
@ -54,6 +55,7 @@
pointer-events: all;
position: relative;
background-color: var(--color-panel);
color: var(--color-text);
padding: 8px 12px;
border-radius: 8px;
white-space: nowrap;
@ -106,14 +108,13 @@
.logseq-tldraw .text-input {
height: 24px;
padding: 4px;
padding: 0;
background: none;
border: 1px solid black;
border-radius: 2px;
}
.logseq-tldraw .input > label {
font-size: 10px;
.logseq-tldraw .text-input:focus {
outline: none;
}
.logseq-tldraw .primary-tools {
@ -150,8 +151,8 @@
flex-flow: column;
border-radius: 8px;
overflow: hidden;
padding: 4px;
gap: 4px;
padding: 8px;
gap: 8px;
}
.logseq-tldraw .floating-panel > button {
@ -160,8 +161,8 @@
.logseq-tldraw .primary-tools .button {
position: relative;
height: 40px;
width: 40px;
height: 36px;
width: 36px;
display: flex;
align-items: center;
justify-content: center;
@ -170,6 +171,7 @@
background: none;
border: none;
cursor: pointer;
color: var(--ls-secondary-text-color);
}
.logseq-tldraw .primary-tools .button:hover {
@ -384,3 +386,91 @@
.logseq-tldraw .preview-minimap-toggle[data-active='true'] {
background: #eee;
}
.logseq-tldraw .tl-quick-search {
width: fit-content;
position: relative;
}
.logseq-tldraw .tl-quick-search-icon {
flex-shrink: 0;
margin-right: 12px;
}
.logseq-tldraw .tl-quick-search-input-container {
display: flex;
align-items: center;
font-size: 16px;
background-color: var(--ls-secondary-background-color);
padding: 4px 12px;
border-radius: 8px;
}
.logseq-tldraw .tl-quick-search-input-sizer {
display: inline-grid;
vertical-align: top;
align-items: center;
position: relative;
margin: 5px;
}
.logseq-tldraw .tl-quick-search-input {
grid-area: 1/2;
width: auto;
}
.logseq-tldraw .tl-quick-search-input-sizer::after {
content: attr(data-value) ' ';
visibility: hidden;
white-space: pre-wrap;
grid-area: 1/2;
width: auto;
}
.logseq-tldraw .tl-quick-search-options {
position: absolute;
top: calc(100% + 8px);
left: 0;
background-color: var(--ls-primary-background-color);
max-height: 300px;
width: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
border-radius: 8px;
--tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
overscroll-behavior: none;
}
.logseq-tldraw .tl-quick-search-option {
padding: 8px 16px;
cursor: pointer;
display: flex;
font-size: 0.875rem;
line-height: 1.25rem;
}
.logseq-tldraw .tl-quick-search-option:hover {
background-color: var(--ls-menu-hover-color, #f4f5f7);
}
.logseq-tldraw .tl-logseq-portal-header {
height: 40px;
width: 100%;
flex-shrink: 0;
background: transparent;
display: flex;
color: var(--ls-title-text-color);
padding: 0px 1rem;
align-items: center;
}
html[data-theme='light'] .logseq-tldraw .tl-logseq-portal-header {
backdrop-filter: brightness(0.9);
}
html[data-theme='dark'] .logseq-tldraw .tl-logseq-portal-header {
backdrop-filter: brightness(1.2);
}

View File

@ -1,4 +1,5 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { App as TldrawApp } from 'tldraw-logseq'
const storingKey = 'playground.index'
@ -53,9 +54,53 @@ const Page = props => {
)
}
const ThemeSwitcher = ({ theme, setTheme }) => {
const [anchor, setAnchor] = React.useState(null)
React.useEffect(() => {
if (anchor) {
return
}
let el = document.querySelector('#theme-switcher')
if (!el) {
el = document.createElement('div')
el.id = 'theme-switcher'
let timer = setInterval(() => {
const statusBarAnchor = document.querySelector('#tl-statusbar-anchor')
if (statusBarAnchor) {
statusBarAnchor.appendChild(el)
setAnchor(el)
clearInterval(timer)
}
}, 50)
}
})
React.useEffect(() => {
document.documentElement.setAttribute('data-theme', theme)
}, [theme])
if (!anchor) {
return null
}
return ReactDOM.createPortal(
<button
className="flex items-center justify-center mx-2 bg-grey"
style={{ fontSize: '1em' }}
onClick={() => setTheme(t => (t === 'dark' ? 'light' : 'dark'))}
>
{theme} theme
</button>,
anchor
)
}
export default function App() {
const [theme, setTheme] = React.useState('dark')
return (
<div className="h-screen w-screen">
<div className={`h-screen w-screen`}>
<ThemeSwitcher theme={theme} setTheme={setTheme} />
<TldrawApp
PageComponent={Page}
searchHandler={q => (q ? list : [])}

View File

@ -1,7 +1,7 @@
import React from 'react'
import ReactDOM from 'react-dom'
import 'tldraw-logseq/styles.css'
import '../../../public/static/css/common.css'
import '../../../public/static/css/style.css'
import App from './App'

View File

@ -108,7 +108,7 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
bindingDistance = BINDING_DISTANCE
// For smart shape
@observable draft = false
@observable private _draft = false
@observable private isDirty = false
@observable private lastSerialized: TLShapeModel<P> | undefined
@ -118,8 +118,13 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
return this.props.id
}
@computed
get draft() {
return this._draft
}
@action setDraft(draft: boolean) {
this.draft = draft
this._draft = draft
}
@action setIsDirty(isDirty: boolean) {

View File

@ -20,13 +20,15 @@ export class CreatingState<
onEnter = () => {
const { Shape } = this.tool
this.offset = [Shape.defaultProps.size[0] / 2, Shape.defaultProps.size[1] / 2]
const shape = new Shape({
id: uniqueId(),
parentId: this.app.currentPage.id,
point: Vec.sub(this.app.inputs.originPoint, this.offset),
size: Shape.defaultProps.size,
} as any)
if (Shape.smart) {
shape.setDraft(true)
}
this.creatingShape = shape
}

View File

@ -48,6 +48,7 @@ export class BrushingState<
? BoundsUtils.boundsContain(brushBounds, shape.rotatedBounds)
: shape.hitTestBounds(brushBounds)
)
.filter(s => !s.draft)
if (shiftKey) {
if (hits.every(hit => this.initialSelectedShapes.includes(hit))) {

View File

@ -83,8 +83,11 @@ export class IdleState<
const { selectionBounds, inputs } = this.app
if (selectionBounds && PointUtils.pointInBounds(inputs.currentPoint, selectionBounds)) {
this.tool.transition('pointingShapeBehindBounds', info)
} else {
} else if (!info.shape.draft) {
this.tool.transition('pointingShape', info)
} else {
// as if clicking the canvas
this.tool.transition('pointingCanvas')
}
}
break

View File

@ -52,6 +52,7 @@ export class PointingCanvasState<
parentId: this.app.currentPage.id,
point: [...this.app.inputs.originPoint],
})
shape.setDraft(true)
this.app.setActivatedShapes([shape.id])
this.app.currentPage.addShapes(shape)
}

View File

@ -15,7 +15,7 @@ function makeCssTheme<T = AnyTheme>(prefix: string, theme: T) {
}, '')
}
function useTheme<T = AnyTheme>(prefix: string, theme: T, selector = ':root') {
function useTheme<T = AnyTheme>(prefix: string, theme: T, selector = '.logseq-tldraw') {
React.useLayoutEffect(() => {
const style = document.createElement('style')
const cssTheme = makeCssTheme(prefix, theme)
@ -72,9 +72,9 @@ const defaultTheme: TLTheme = {
selectStroke: 'rgb(66, 133, 244)',
selectFill: 'rgba(65, 132, 244, 0.05)',
binding: 'rgba(65, 132, 244, 0.5)',
background: 'rgb(248, 249, 250)',
foreground: 'rgb(51, 51, 51)',
grid: 'rgba(144, 144, 144, .9)',
background: 'var(--ls-primary-background-color)',
foreground: 'var(--ls-secondary-text-color)',
grid: 'var(--ls-quaternary-background-color)',
}
const tlcss = css`
@ -151,6 +151,7 @@ const tlcss = css`
background-color: var(--tl-background);
cursor: var(--tl-cursor) !important;
box-sizing: border-box;
color: var(--tl-foreground);
}
.tl-overlay {