mirror of https://github.com/logseq/logseq
feat: finish double click to create poc
parent
f487c5817c
commit
20b81fbd1e
|
@ -1,3 +1,2 @@
|
|||
.logseq-tldraw .tl-container {
|
||||
background-color: var(--ls-secondary-background-color);
|
||||
}
|
||||
|
|
|
@ -51,6 +51,8 @@ export const DevTools = observer(() => {
|
|||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{rendererStatusText}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
ref={rInput}
|
||||
label="Page name or block UUID"
|
||||
type="text"
|
||||
value={q}
|
||||
onChange={handleChange}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
commitChange(q)
|
||||
}
|
||||
}}
|
||||
list="logseq-portal-search-results"
|
||||
/>
|
||||
<datalist id="logseq-portal-search-results">
|
||||
{options?.map(option => (
|
||||
<option key={option} value={secretPrefix + option}>
|
||||
{option}
|
||||
</option>
|
||||
<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}
|
||||
type="text"
|
||||
value={q}
|
||||
placeholder="Search or create page"
|
||||
onChange={handleChange}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
commitChange(q)
|
||||
}
|
||||
}}
|
||||
className="tl-quick-search-input text-input"
|
||||
/>
|
||||
</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,
|
||||
}}
|
||||
>
|
||||
{pageId && Page ? (
|
||||
<div style={{ padding: '12px', height: '100%', cursor: 'default' }}>
|
||||
<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',
|
||||
}}
|
||||
>
|
||||
<Page pageId={pageId} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 : [])}
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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))) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue