A much better way to handle links

This commit is contained in:
Corey Johnson 2025-07-10 14:22:26 -07:00
parent 331a29eb4a
commit 3abd4447b2
10 changed files with 66 additions and 107 deletions

View File

@ -20,14 +20,9 @@ const buildDynamicRoute = async ({ distDir, routeName, filepath }: BuildRouteOpt
const dynamicRouteFilepath = join(distDir, "routes", outDir, filename)
await mkdirSync(dirname(dynamicRouteFilepath), { recursive: true })
// Create a relative import path from the generated file to the source file
const relativeImportPath = relative(dirname(dynamicRouteFilepath), filepath)
// Normalize the path for cross-platform compatibility and ensure forward slashes
const normalizedImportPath = relativeImportPath.replace(/\\/g, "/")
// Only import the Component so that tree-shaking will get rid of the server-side code
const code = `
import Component from "${normalizedImportPath}"
import Component from "${filepath}"
import { wrapComponentWithLoader} from "@workshop/nano-remix"
import { render } from 'hono/jsx/dom'

View File

@ -9,9 +9,12 @@ type BuildRouteOptions = {
export const buildRoute = async ({ distDir, routeName, filepath, force = false }: BuildRouteOptions) => {
if (!force && !(await shouldRebuild(routeName, filepath, distDir))) {
console.log(`🌭 Skipping build for ${routeName} - up to date`)
return
}
console.log(`🌭 Building route ${routeName} from ${filepath}`)
const scriptPath = join(import.meta.dirname, "../scripts/build.ts")
const proc = Bun.spawn({
@ -32,6 +35,10 @@ export const buildRoute = async ({ distDir, routeName, filepath, force = false }
}
const shouldRebuild = async (routeName: string, sourceFilePath: string, distDir: string) => {
if (process.env.NODE_ENV !== "production") {
return true
}
try {
const outputPath = join(distDir, routeName + ".js")

View File

@ -33,12 +33,7 @@ export const nanoRemix = async (req: Request, options: Options = {}) => {
// If the the route includes an extension it is a static file that we serve from the distDir
if (!ext) {
await buildRoute({
distDir,
routeName,
filepath: route.filePath,
force: options.disableCache, // Force rebuild if cache is disabled
})
await buildRoute({ distDir, routeName, filepath: route.filePath, force: options.disableCache })
return await renderServer(req, route)
} else {
const file = Bun.file(join(distDir, routeName + ext))

View File

@ -6,7 +6,7 @@
"private": true,
"scripts": {
"dev": "bun run src/server.tsx",
"serve-subdomain": "NODE_ENV=production bun run src/server.tsx"
"serve-subdomain": "bun run src/server.tsx"
},
"dependencies": {
"@codemirror/autocomplete": "^6.18.6",

View File

@ -4,6 +4,7 @@ import { triggerTodoCompletionEffect } from "./todoCompletion"
export const todoClickHandler = EditorView.domEventHandlers({
click(event, view) {
const element = event.target as HTMLElement
const pos = view.posAtCoords({ x: event.clientX, y: event.clientY })
if (pos === null) return false
@ -12,40 +13,26 @@ export const todoClickHandler = EditorView.domEventHandlers({
if (!todo) return false
// Find the checkbox position in the line
const checkboxMatch = line.text.match(/^(\s*-\s*\[)\s*(\w?)\s*(\]\s+)/)
if (!checkboxMatch) return false
if (element.matches(".todo-url")) {
const url = element.getAttribute("data-url")
if (url) {
window.open(url, "_blank")
return true
}
}
const [fullMatch, beforeCheckbox, currentState, afterCheckbox] = checkboxMatch
if (!beforeCheckbox || !afterCheckbox) return false
const checkboxStart = line.from + beforeCheckbox.length
const checkboxEnd = checkboxStart + 1
// Check if the click was within the checkbox area (including some padding)
const clickOffset = pos - line.from
// Allow clicking anywhere in the "- [ ]" or "- [x]" part
if (clickOffset >= 0 && clickOffset <= fullMatch.length) {
// Toggle the checkbox state
const newState = todo.done ? " " : "x"
const isCompleting = !todo.done // Will be completing if currently not done
view.dispatch({
changes: {
from: checkboxStart,
to: checkboxEnd,
insert: newState,
},
})
// Add animation effect when completing (not uncompleting)
if (isCompleting) {
if (element.matches(".todo-checkbox")) {
todo.done = !todo.done
if (todo.done) {
triggerTodoCompletionEffect(view, line.from)
}
event.preventDefault()
return true
view.dispatch({
changes: {
from: line.from,
to: line.to,
insert: todo.toString(),
},
})
}
return false

View File

@ -2,8 +2,6 @@ import { completionAnimationEffect } from "@/todoDecorations"
import { EditorView } from "@codemirror/view"
export const triggerTodoCompletionEffect = async (view: EditorView, pos: number) => {
console.log(`🌭 HAHAHA`, pos)
// Dispatch the completion animation effect
view.dispatch({
effects: completionAnimationEffect.of({ pos, duration: 1000 }),

View File

@ -2,25 +2,6 @@ import { Todo } from "@/todo"
import { RangeSetBuilder, StateEffect } from "@codemirror/state"
import { EditorView, Decoration, ViewPlugin, ViewUpdate, WidgetType } from "@codemirror/view"
import { type RefObject } from "hono/jsx"
import { DateTime } from "luxon"
// URL Widget for clickable URLs
class URLWidget extends WidgetType {
constructor(readonly url: string, readonly text: string) {
super()
}
toDOM() {
const span = document.createElement("span")
span.className = "todo-url"
span.textContent = this.text
span.addEventListener("click", (e) => {
e.preventDefault()
window.open(this.url, "_blank")
})
return span
}
}
// Effect to trigger a decoration refresh on filter change
export const refreshFilterEffect = StateEffect.define<void>()
@ -108,13 +89,12 @@ export const todoDecorations = (filterRef: RefObject<string>) => {
builder.add(line.from, line.from + checkboxEnd, Decoration.mark({ class: "todo-checkbox" }))
}
for (const { type, start, end, widget } of decorationsFor(todo)) {
if (widget) {
// Replace the text with a clickable widget
builder.add(line.from + start, line.from + end, Decoration.replace({ widget }))
} else {
builder.add(line.from + start, line.from + end, Decoration.mark({ class: `todo-${type}` }))
}
for (const { type, start, end, attributes } of decorationsFor(todo)) {
builder.add(
line.from + start,
line.from + end,
Decoration.mark({ class: `todo-${type}`, attributes })
)
}
} else if (/^\s*#+\s/.test(text)) {
builder.add(line.from, line.to, Decoration.mark({ class: "todo-header" }))
@ -132,21 +112,20 @@ export const todoDecorations = (filterRef: RefObject<string>) => {
)
}
type TodoAttributes = Record<string, string>
const decorationsFor = (todo: Todo) => {
const decorations: { type: string; start: number; end: number; widget?: WidgetType }[] = []
const decorations: { type: string; start: number; end: number; attributes: TodoAttributes }[] = []
let start = 0
for (const node of todo.nodes) {
const { type } = node
const end = start + node.content.length
const attributes: TodoAttributes = {}
if (type === "url") {
// Create a widget for clickable URLs
const urlWidget = new URLWidget(node.url, node.content)
decorations.push({ type: "url", start, end, widget: urlWidget })
} else {
decorations.push({ type, start, end })
attributes["data-url"] = node.url
}
decorations.push({ type, start, end, attributes })
start = end
}

View File

@ -11,33 +11,7 @@ import { dateAutocompletion } from "@/dateAutocompletion"
import { todoTimer } from "@/todoTimer"
import { DateTime } from "luxon"
import "./editor.css"
// Constants
const COMPLETION_ANIMATION_DURATION = 1000
const FILTER_PLACEHOLDER = "Filter by tag"
const getDefaultDocument = () => {
const today = DateTime.local().toFormat("MM/dd/yyyy")
const future = DateTime.local().plus({ days: 3 }).toFormat("MM/dd/yyyy")
return `
# Today
- [ ] Sample task with a due date @${today}
- [ ] You can use a #tag to filter todos
- [ ] A sub task! Create nested todos by indenting with <tab>
- [x] Complete a todo by pressing <opt+k>
- [ ] You can also set a time estimate for todos (press the play button to start) 10m
# This week
- [ ] Another todo with a due date @${future}
# Later
- [ ] I use later as a junk drawer for todos I don't want to forget
`.trim()
}
import "./todo.css"
type TodoEditorProps = {
defaultValue?: string
@ -171,3 +145,27 @@ const KeyboardShortcuts = ({ onClose }: { onClose: () => void }) => {
</div>
)
}
const FILTER_PLACEHOLDER = "Filter by tag"
const getDefaultDocument = () => {
const today = DateTime.local().toFormat("MM/dd/yyyy")
const future = DateTime.local().plus({ days: 3 }).toFormat("MM/dd/yyyy")
return `
# Today
- [ ] Sample task with a due date @${today}
- [ ] You can use a #tag to filter todos
- [ ] A sub task! Create nested todos by indenting with <tab>
- [x] Complete a todo by pressing <opt+k>
- [ ] You can also set a time estimate for todos (press the play button to start) 10m
# This week
- [ ] Another todo with a due date @${future}
# Later
- [ ] I use later as a junk drawer for todos I don't want to forget
`.trim()
}

View File

@ -7,7 +7,7 @@
"module": "src/index.tsx",
"scripts": {
"dev": "bun --hot src/server.tsx",
"serve-subdomain": "NODE_ENV=production bun src/server.tsx"
"serve-subdomain": "bun src/server.tsx"
},
"prettier": {
"semi": false,