Compare commits
23
Commits
272bddab4f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0b6c23068 | ||
|
|
8f7b9a1a87 | ||
|
|
5d146aaf3f | ||
|
|
87e482e943 | ||
|
|
de6af8c6b5 | ||
|
|
1c0957b7bb | ||
|
|
c47d16fa30 | ||
|
|
fb2951d335 | ||
|
|
26d5677546 | ||
|
|
a7568c1bb2 | ||
|
|
43c81b206f | ||
|
|
1eb540d88e | ||
|
|
e244dd7c3f | ||
|
|
cf6e26b890 | ||
|
|
cfec7982e0 | ||
|
|
7628ccc991 | ||
|
|
9f4dff0417 | ||
|
|
3e44846e22 | ||
|
|
b1e9e2ec7d | ||
|
|
b615bce8ce | ||
|
|
94664892e4 | ||
|
|
cf08a4ecf0 | ||
|
|
b8cd715d2a |
@@ -0,0 +1,2 @@
|
|||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
name: Build and Publish Docker Container
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Login to Docker
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
registry: git.joshuacoles.me
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push multi-arch Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: git.joshuacoles.me/${{ github.repository }}:${{ github.sha }},git.joshuacoles.me/${{ github.repository }}:latest
|
||||||
@@ -22,3 +22,4 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
/.env
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# global options
|
||||||
|
{
|
||||||
|
admin off # theres no need for the admin api in railway's environment
|
||||||
|
persist_config off # storage isn't persistent anyway
|
||||||
|
auto_https off # railway handles https for us, this would cause issues if left enabled
|
||||||
|
log { # runtime logs
|
||||||
|
format console # set runtime log format to console mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:3000 {
|
||||||
|
log { # access logs
|
||||||
|
format console # set access log format to console mode
|
||||||
|
}
|
||||||
|
|
||||||
|
# health check for railway
|
||||||
|
respond /health 200
|
||||||
|
|
||||||
|
# serve from the 'dist' folder (Vite builds into the 'dist' folder)
|
||||||
|
root * /app/dist
|
||||||
|
|
||||||
|
# enable gzipping responses
|
||||||
|
encode gzip
|
||||||
|
|
||||||
|
# serve files from 'dist'
|
||||||
|
file_server
|
||||||
|
|
||||||
|
# if path doesn't exist, redirect it to 'index.html' for client side routing
|
||||||
|
try_files {path} /index.html
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM oven/bun:1 AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package.json
|
||||||
|
COPY bun.lockb bun.lockb
|
||||||
|
RUN bun install
|
||||||
|
|
||||||
|
FROM node:18-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
FROM caddy AS runner
|
||||||
|
COPY --from=builder /app/dist /usr/share/caddy
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Obsidian Note Finder
|
||||||
|
|
||||||
|
A simple web UI which allows for easily finding, creating, and copying Obsidian wiki style links from a given vault. I use this to quickly put links to notes inside non Obsidian contexts, which are then exported to obsidian.
|
||||||
|
|
||||||
|
This requires an Obsidian Beachhead installation which is a service to provide information and interaction with your obsidian vault over the network. Currently, this comprises a single route `/metadata` which returns the [metadata-extractor](https://github.com/kometenstaub/metadata-extractor) json data file.
|
||||||
|
|
||||||
|
## Dev
|
||||||
|
|
||||||
|
Set the `OBSIDIAN_BEACHHEAD_SERVER` to point the Obsidian Beachhead installation.
|
||||||
|
|
||||||
|
## Prod
|
||||||
|
|
||||||
|
Make sure you reverse proxy the `/metadata` route to the Obsidian Beachhead installation.
|
||||||
+11
-1
@@ -10,12 +10,22 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@headlessui/react": "^1.7.17",
|
||||||
|
"@heroicons/react": "^2.0.18",
|
||||||
|
"@tailwindcss/forms": "^0.5.6",
|
||||||
|
"@tanstack/react-query": "^4.35.3",
|
||||||
|
"classnames": "^2.3.2",
|
||||||
|
"ramda": "^0.29.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0",
|
||||||
|
"react-select": "^5.7.5",
|
||||||
|
"react-window": "^1.8.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/ramda": "^0.29.5",
|
||||||
"@types/react": "^18.0.37",
|
"@types/react": "^18.0.37",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@types/react-window": "^1.8.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.0",
|
"@typescript-eslint/eslint-plugin": "^5.59.0",
|
||||||
"@typescript-eslint/parser": "^5.59.0",
|
"@typescript-eslint/parser": "^5.59.0",
|
||||||
"@vitejs/plugin-react-swc": "^3.0.0",
|
"@vitejs/plugin-react-swc": "^3.0.0",
|
||||||
|
|||||||
-44
@@ -1,44 +0,0 @@
|
|||||||
#root {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 6em;
|
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo.react:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
a:nth-of-type(2) .logo {
|
|
||||||
animation: logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
+150
-31
@@ -1,35 +1,154 @@
|
|||||||
import {useState} from 'react'
|
import {useEffect, useState} from "react";
|
||||||
import reactLogo from './assets/react.svg'
|
import classNames from "classnames";
|
||||||
import viteLogo from '/vite.svg'
|
|
||||||
import './App.css'
|
|
||||||
|
|
||||||
function App() {
|
import {useQuery} from "@tanstack/react-query";
|
||||||
const [count, setCount] = useState(0)
|
import {LinkCollection, type Option} from "./aliases";
|
||||||
|
|
||||||
return (
|
import {CheckIcon, ChevronUpDownIcon} from '@heroicons/react/20/solid'
|
||||||
<>
|
import {Combobox} from '@headlessui/react'
|
||||||
<div>
|
|
||||||
<a href="https://vitejs.dev" target="_blank">
|
type ResultNoteApi = {
|
||||||
<img src={viteLogo} className="logo" alt="Vite logo"/>
|
score: number
|
||||||
</a>
|
vault: string
|
||||||
<a href="https://react.dev" target="_blank">
|
path: string
|
||||||
<img src={reactLogo} className="logo react" alt="React logo"/>
|
basename: string
|
||||||
</a>
|
foundWords: string[]
|
||||||
</div>
|
matches: SearchMatchApi[]
|
||||||
<h1>Vite + React</h1>
|
excerpt: string
|
||||||
<div className="card">
|
|
||||||
<button onClick={() => setCount((count) => count + 1)}>
|
|
||||||
count is {count}
|
|
||||||
</button>
|
|
||||||
<p>
|
|
||||||
Edit <code>src/App.tsx</code> and save to test HMR
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p className="read-the-docs">
|
|
||||||
Click on the Vite and React logos to learn more
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
type SearchMatchApi = {
|
||||||
|
match: string
|
||||||
|
offset: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OmnisearchSelect({setSelected}: {
|
||||||
|
setSelected: (value: Option) => void
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
data: metadata,
|
||||||
|
isLoading,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ['obsidian-metadata'],
|
||||||
|
initialData: [],
|
||||||
|
refetchInterval: false,
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch("/metadata")
|
||||||
|
const fullData: any[] = await response.json();
|
||||||
|
return fullData;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
const [selectedPerson, setSelectedPerson] = useState<ResultNoteApi | null>(null);
|
||||||
|
|
||||||
|
const {data: filteredPeople} = useQuery({
|
||||||
|
queryKey: ['obsidian-omnisearch', query],
|
||||||
|
initialData: [],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (query === '') {
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
const response = await fetch(`/search?q=${query}`)
|
||||||
|
const fullData: ResultNoteApi[] = await response.json();
|
||||||
|
|
||||||
|
return fullData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedPerson != undefined && metadata.length > 0) {
|
||||||
|
console.log(selectedPerson);
|
||||||
|
const data = metadata.find(md => md.relativePath === selectedPerson.path);
|
||||||
|
if (!data) {
|
||||||
|
debugger
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelected({
|
||||||
|
value: selectedPerson.path,
|
||||||
|
label: selectedPerson.basename,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [selectedPerson, metadata]);
|
||||||
|
|
||||||
|
return (<Combobox as="div" value={selectedPerson} onChange={setSelectedPerson}>
|
||||||
|
<Combobox.Label className="block text-sm font-medium leading-6 text-gray-900">Search for note</Combobox.Label>
|
||||||
|
<div className="relative mt-2">
|
||||||
|
<Combobox.Input
|
||||||
|
aria-disabled={isLoading}
|
||||||
|
className="w-full rounded-md border-0 bg-white py-1.5 pl-3 pr-12 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
displayValue={(person: ResultNoteApi) => person?.basename}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Combobox.Button
|
||||||
|
className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none">
|
||||||
|
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>
|
||||||
|
</Combobox.Button>
|
||||||
|
|
||||||
|
{filteredPeople.length > 0 && (
|
||||||
|
<Combobox.Options
|
||||||
|
className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||||
|
{filteredPeople.map((person) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={person.path}
|
||||||
|
value={person}
|
||||||
|
className={({active}) =>
|
||||||
|
classNames(
|
||||||
|
'relative cursor-default select-none py-2 pl-3 pr-9',
|
||||||
|
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({
|
||||||
|
active,
|
||||||
|
selected
|
||||||
|
}) => (
|
||||||
|
<>
|
||||||
|
<div className="flex">
|
||||||
|
<span
|
||||||
|
className={classNames('truncate', selected && 'font-semibold')}>{person?.basename}</span>
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'ml-2 truncate text-gray-500',
|
||||||
|
active ? 'text-indigo-200' : 'text-gray-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{person?.path}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selected && (
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'absolute inset-y-0 right-0 flex items-center pr-4',
|
||||||
|
active ? 'text-white' : 'text-indigo-600'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-5 w-5" aria-hidden="true"/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Combobox>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const [selected, setSelected] = useState<Option | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={"m-5 text-lg"}>
|
||||||
|
{/*<NaiveSelect setSelected={setSelected}/>*/}
|
||||||
|
<OmnisearchSelect setSelected={setSelected}/>
|
||||||
|
|
||||||
|
{selected && <LinkCollection selected={selected}/>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// TODO: Fix this for wrapping items, esp on phones
|
||||||
|
import Select, {createFilter, MenuListProps} from "react-select";
|
||||||
|
import {ReactNode, useCallback} from "react";
|
||||||
|
import {FixedSizeList as List} from "react-window";
|
||||||
|
import type {Option} from "./aliases.tsx";
|
||||||
|
import {useQuery} from "@tanstack/react-query";
|
||||||
|
import * as R from "ramda";
|
||||||
|
|
||||||
|
const height = 35;
|
||||||
|
|
||||||
|
function MenuList(props: MenuListProps) {
|
||||||
|
const {
|
||||||
|
options,
|
||||||
|
children,
|
||||||
|
maxHeight,
|
||||||
|
getValue
|
||||||
|
} = props as Omit<MenuListProps, 'children'> & {
|
||||||
|
children: ReactNode[]
|
||||||
|
};
|
||||||
|
|
||||||
|
const [value] = getValue();
|
||||||
|
const initialOffset = options.indexOf(value) * height;
|
||||||
|
|
||||||
|
return (<List
|
||||||
|
width={'100%'}
|
||||||
|
height={maxHeight}
|
||||||
|
itemCount={children?.length ?? 0}
|
||||||
|
itemSize={height}
|
||||||
|
initialScrollOffset={initialOffset}
|
||||||
|
>
|
||||||
|
{({
|
||||||
|
index,
|
||||||
|
style
|
||||||
|
}) => <div style={style}>{children[index]}</div>}
|
||||||
|
</List>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NaiveSelect({setSelected}: {
|
||||||
|
setSelected: (value: Option) => void
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
data: options,
|
||||||
|
isLoading,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ['obsidian-metadata'],
|
||||||
|
initialData: [],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await fetch("/metadata")
|
||||||
|
const fullData: any[] = await response.json();
|
||||||
|
|
||||||
|
return R.sortBy(v => -(v.data.backlinks?.length ?? 0), fullData.map(md => ({
|
||||||
|
value: md.relativePath,
|
||||||
|
label: md.fileName,
|
||||||
|
data: md,
|
||||||
|
}) as Option));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onChange = useCallback((value: Option) => {
|
||||||
|
setSelected(value);
|
||||||
|
navigator.clipboard.writeText(`[[${value.label}]]`)
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (<Select
|
||||||
|
classNames={{input: () => 'select-input-wrapper'}}
|
||||||
|
onChange={onChange as any}
|
||||||
|
components={{MenuList}}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isClearable={true}
|
||||||
|
options={options}
|
||||||
|
filterOption={createFilter({ignoreAccents: false})}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
import {
|
||||||
|
ChangeEventHandler,
|
||||||
|
Dispatch,
|
||||||
|
KeyboardEventHandler,
|
||||||
|
MouseEventHandler,
|
||||||
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
export interface Option {
|
||||||
|
label: string,
|
||||||
|
value: string,
|
||||||
|
data: {
|
||||||
|
backlinks: unknown[]
|
||||||
|
aliases?: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LinkCollection({ selected }: { selected: Option }) {
|
||||||
|
return (
|
||||||
|
<div className={"mt-2"}>
|
||||||
|
<div className="py-1">
|
||||||
|
<Alias original={selected.label}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selected.data.aliases?.map(alias => (
|
||||||
|
<div className="py-1">
|
||||||
|
<Alias original={selected.label} alias={alias}/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="py-1">
|
||||||
|
<CustomAlias selected={selected}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CustomAlias({selected}: {
|
||||||
|
selected: Option
|
||||||
|
}) {
|
||||||
|
const [alias, setAlias] = useState('');
|
||||||
|
|
||||||
|
// Reset when selection changes
|
||||||
|
useEffect(() => {
|
||||||
|
setAlias('');
|
||||||
|
}, [selected.value]);
|
||||||
|
|
||||||
|
const onClick: MouseEventHandler = useCallback((e) => {
|
||||||
|
if ((e.target as HTMLElement).tagName == 'SPAN' && alias.length > 0) {
|
||||||
|
navigator.clipboard.writeText(`[[${selected!.label}|${alias}]]`);
|
||||||
|
}
|
||||||
|
}, [selected.value, selected.label, alias]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span onClick={onClick} className={"rounded-md p-1 hover:bg-slate-100"}>
|
||||||
|
<span className={"text-slate-300 p-0.5"}>[[</span>
|
||||||
|
<span>{selected.label}</span>
|
||||||
|
<span className={"text-slate-300 p-0.5"}>|</span>
|
||||||
|
<span>
|
||||||
|
<CustomAliasField content={alias} setContent={setAlias} selected={selected}/>
|
||||||
|
</span>
|
||||||
|
<span className={"text-slate-300 p-0.5"}>]]</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomAliasField({
|
||||||
|
selected,
|
||||||
|
content,
|
||||||
|
setContent
|
||||||
|
}: {
|
||||||
|
selected: Option,
|
||||||
|
content: string,
|
||||||
|
setContent: Dispatch<SetStateAction<string>>,
|
||||||
|
}) {
|
||||||
|
const [width, setWidth] = useState<any>(0);
|
||||||
|
const span = useRef<HTMLSpanElement>(null);
|
||||||
|
|
||||||
|
// Resize on change of content
|
||||||
|
useEffect(() => {
|
||||||
|
setWidth(span.current!.offsetWidth);
|
||||||
|
// setWidth(content.length + 'ch');
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
|
const changeHandler: ChangeEventHandler<HTMLInputElement> = useCallback(evt => {
|
||||||
|
setContent(evt.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onCustomElementKeyDown: KeyboardEventHandler<HTMLInputElement> = useCallback((e) => {
|
||||||
|
if (e.key == 'Enter') {
|
||||||
|
navigator.clipboard.writeText(`[[${selected!.label}|${content}]]`);
|
||||||
|
(e.target as HTMLInputElement).blur()
|
||||||
|
}
|
||||||
|
}, [selected.value, selected.label, content]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<span style={{
|
||||||
|
position: 'absolute',
|
||||||
|
opacity: 0,
|
||||||
|
zIndex: -100,
|
||||||
|
whiteSpace: 'pre',
|
||||||
|
}} ref={span}>{content}</span>
|
||||||
|
<input
|
||||||
|
className={"border-none p-0 px-1"}
|
||||||
|
type="text" style={{width: `calc(${width}px + 0.25rem)`}} autoFocus onChange={changeHandler}
|
||||||
|
onKeyDown={onCustomElementKeyDown}/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Alias({
|
||||||
|
original,
|
||||||
|
alias
|
||||||
|
}: {
|
||||||
|
original: string,
|
||||||
|
alias?: string
|
||||||
|
}) {
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
if (alias) {
|
||||||
|
navigator.clipboard.writeText(`[[${original}|${alias}]]`)
|
||||||
|
} else {
|
||||||
|
navigator.clipboard.writeText(`[[${original}]]`)
|
||||||
|
}
|
||||||
|
}, [original, alias]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={"rounded-md p-1 hover:bg-slate-100"} onClick={onClick}>
|
||||||
|
<span className={"text-slate-300 p-0.5"}>[[</span>
|
||||||
|
<span>{original}</span>
|
||||||
|
{alias && <><span className={"text-slate-300 p-0.5"}>|</span>
|
||||||
|
<span>{alias}</span></>}
|
||||||
|
<span className={"text-slate-300 p-0.5"}>]]</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
.select-input-wrapper > input:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|||||||
+6
-1
@@ -1,10 +1,15 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App.tsx'
|
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
|
||||||
|
import {App} from "./App.tsx";
|
||||||
|
|
||||||
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<App/>
|
<App/>
|
||||||
|
</QueryClientProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-1
@@ -7,6 +7,8 @@ export default {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [
|
||||||
|
require('@tailwindcss/forms')
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
-2
@@ -1,7 +1,24 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
import react from '@vitejs/plugin-react-swc'
|
import react from '@vitejs/plugin-react-swc'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig(({ command, mode }) => {
|
||||||
|
// Load env file based on `mode` in the current working directory.
|
||||||
|
// Set the third parameter to '' to load all env regardless of the `VITE_` prefix.
|
||||||
|
const env = loadEnv(mode, process.cwd(), '')
|
||||||
|
|
||||||
|
return {
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
proxy: {
|
||||||
|
'/metadata': {
|
||||||
|
target: env['OBSIDIAN_BEACHHEAD_SERVER']
|
||||||
|
},
|
||||||
|
'/search': {
|
||||||
|
target: env['OBSIDIAN_BEACHHEAD_SERVER']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user