publishing meta and endless-journal

This commit is contained in:
nicwands 2026-07-20 11:57:04 -04:00
parent fac6ecf030
commit 7bf0ab967b
41 changed files with 1168 additions and 27 deletions

View file

@ -17,6 +17,7 @@
"@tiptap/pm": "^3.28.0",
"@tiptap/starter-kit": "^3.28.0",
"@tiptap/vue-3": "^3.28.0",
"fecha": "^4.2.3",
"sass": "^1.98.0",
"sass-embedded": "^1.98.0",
"vue": "^3.5.30",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/drops/endless-journal/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Endless Journal</title>
<script type="module" crossorigin src="/drops/endless-journal/assets/index-D22DIc1j.js"></script>
<link rel="stylesheet" crossorigin href="/drops/endless-journal/assets/index-CjZd22yH.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

View file

@ -0,0 +1,124 @@
{
"name": "endless-journal",
"title": "Endless Journal",
"date": "2026-07-20",
"tags": [
"Process"
],
"description": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": " Creativity is following the thread of intuition and seeing where it goes. It's about being in the moment and not looking back."
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "The process of destruction is just as important as the process of creation. Destruction frees the mind and allows it to always stay on the move, constantly reaching new territory."
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "As you type, the letters will begin to pile up as a mass of objects. These letters no longer signify the words you wrote but progress in the creative process. There are three fill lines to signify this progress:"
}
]
},
{
"type": "bulletList",
"content": [
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"marks": [
{
"type": "bold"
}
],
"text": "Intuition: "
},
{
"type": "text",
"text": "This first stage is about finding that intuitive thread and simply holding onto it to see where it takes you."
}
]
}
]
},
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"marks": [
{
"type": "bold"
}
],
"text": "Chaos: "
},
{
"type": "text",
"text": "The second stage is about throwing out every idea your mind relates to that intuitive thread. Nothing is organized yet and that's a good thing. It's ok if you don't feel like you are getting anywhere yet."
}
]
}
]
},
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"marks": [
{
"type": "bold"
}
],
"text": "Concept: "
},
{
"type": "text",
"text": "By this stage you will hopefully start to take the mass of chaotic ideas and form them into a single core concept. This is the concept you will use to build your creation."
}
]
}
]
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "If a concept still has not shown itself, that's ok, reload and repeat."
}
]
}
]
},
"published": true
}

View file

@ -42,5 +42,6 @@
"date": "2026-07-16",
"tags": [
"visualization"
]
}
],
"published": true
}

View file

@ -1,4 +1,4 @@
[
"flowmap",
"testing"
"endless-journal",
"flowmap"
]

View file

@ -27,9 +27,10 @@
</template>
<script setup>
import { computed } from 'vue'
import Btn from '@shared/Btn.vue'
import RichText from '@shared/RichText.vue';
import { format, parse } from 'fecha'
import Btn from '@shared/Btn.vue'
import { computed } from 'vue'
const props = defineProps({
drop: Object,
@ -51,14 +52,12 @@ const displayTitle = computed(() => {
})
const formattedDate = computed(() => {
if (!props.drop.date) return ''
if (!props.drop.date) return ''
const date = new Date(props.drop.date)
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
const [year, month, day] = props.drop.date.split('-')
const date = new Date(year, month - 1, day)
return format(date, 'MMMM Do, YYYY')
})
</script>

97
bin/buildDrops.js Normal file
View file

@ -0,0 +1,97 @@
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import { spawn } from 'child_process'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const dropsDir = path.join(__dirname, '../drops')
/**
* Get all published drops by reading their meta.json files
*/
const getPublishedDrops = () => {
const drops = fs
.readdirSync(dropsDir, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name)
const publishedDrops = drops.filter((dropName) => {
const metaPath = path.join(dropsDir, dropName, 'public/meta.json')
if (!fs.existsSync(metaPath)) {
console.warn(
`Warning: meta.json not found for drop "${dropName}", skipping...`,
)
return false
}
try {
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'))
return meta.published === true
} catch (err) {
console.error(
`Error reading meta.json for drop "${dropName}":`,
err.message,
)
return false
}
})
return publishedDrops
}
/**
* Build a single drop
*/
const buildDrop = (dropName) => {
return new Promise((resolve, reject) => {
console.log(`Building drop: ${dropName}`)
const child = spawn('npm', ['run', 'build', '-w', `drops/${dropName}`], {
stdio: 'inherit',
shell: true,
})
child.on('close', (code) => {
if (code === 0) {
console.log(`✓ Successfully built: ${dropName}`)
resolve()
} else {
reject(new Error(`Failed to build ${dropName} (exit code ${code})`))
}
})
child.on('error', reject)
})
}
/**
* Build all published drops
*/
const buildAllPublishedDrops = async () => {
const publishedDrops = getPublishedDrops()
if (publishedDrops.length === 0) {
console.log('No published drops found.')
return
}
console.log(`Found ${publishedDrops.length} published drop(s):`, publishedDrops)
console.log('Starting builds...\n')
for (const dropName of publishedDrops) {
try {
await buildDrop(dropName)
} catch (err) {
console.error(`Error building ${dropName}:`, err.message)
process.exit(1)
}
}
console.log('\n✓ All published drops built successfully!')
}
buildAllPublishedDrops().catch((err) => {
console.error('Build failed:', err)
process.exit(1)
})

View file

@ -4,14 +4,90 @@ import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const dropsDir = path.join(__dirname, '../app/public/drops')
const dropsSourceDir = path.join(__dirname, '../drops')
const dropsPublicDir = path.join(__dirname, '../app/public/drops')
const outputPath = path.join(__dirname, '../app/public/manifest.json')
const dropNames = fs
.readdirSync(dropsDir, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name)
/**
* Get all published drops by reading their meta.json files from the source drops directory
*/
const getPublishedDrops = () => {
const drops = fs
.readdirSync(dropsSourceDir, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name)
fs.writeFileSync(outputPath, JSON.stringify(dropNames, null, 2))
const publishedDrops = drops.filter((dropName) => {
const metaPath = path.join(dropsSourceDir, dropName, 'public/meta.json')
console.log(`Manifest created with ${dropNames.length} drops:`, dropNames)
if (!fs.existsSync(metaPath)) {
console.warn(
`Warning: meta.json not found for drop "${dropName}", skipping...`,
)
return false
}
try {
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'))
return meta.published === true
} catch (err) {
console.error(
`Error reading meta.json for drop "${dropName}":`,
err.message,
)
return false
}
})
return publishedDrops
}
const publishedDrops = getPublishedDrops()
// Only include drops that are both published and actually built (exist in public/drops)
const builtPublishedDrops = publishedDrops.filter((dropName) => {
const dropDir = path.join(dropsPublicDir, dropName)
return fs.existsSync(dropDir)
})
/**
* Sort drops by date (newest first)
*/
const sortDropsByDate = (dropNames) => {
const dropsWithDates = dropNames.map((dropName) => {
const metaPath = path.join(dropsPublicDir, dropName, 'meta.json')
let date = null
if (fs.existsSync(metaPath)) {
try {
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'))
date = meta.date || null
} catch (err) {
console.warn(
`Warning: Could not read date from built meta.json for "${dropName}"`,
)
}
}
return { name: dropName, date }
})
// Sort by date descending (newest first), put drops without dates at the end
dropsWithDates.sort((a, b) => {
if (!a.date && !b.date) return 0
if (!a.date) return 1
if (!b.date) return -1
return new Date(b.date) - new Date(a.date)
})
return dropsWithDates.map((drop) => drop.name)
}
const sortedDrops = sortDropsByDate(builtPublishedDrops)
fs.writeFileSync(outputPath, JSON.stringify(sortedDrops, null, 2))
console.log(
`Manifest created with ${sortedDrops.length} published drop(s) (sorted by date):`,
sortedDrops,
)

View file

@ -167,6 +167,8 @@ rl.question('Enter drop name: ', async (name) => {
console.log('Running npm install...')
await runCommand('npm', ['install'])
console.log('npm install complete!')
await runCommand('npm', ['run dev:drops', kebabName])
} catch (err) {
console.error('Error:', err.message)
process.exit(1)

View file

@ -38,6 +38,22 @@
/>
</div>
<!-- Published -->
<div class="form-group checkbox-group">
<label class="checkbox-label">
<input
id="published"
v-model="meta.published"
type="checkbox"
class="checkbox"
/>
<span class="checkbox-text">
<strong>Published</strong>
<small>When checked, this drop will be included in the build</small>
</span>
</label>
</div>
<!-- Description -->
<div class="form-group">
<label for="description">Description</label>
@ -124,6 +140,7 @@ const meta = ref({
tags: [],
links: [],
notes: '',
published: false,
})
const loading = ref(true)
@ -310,6 +327,48 @@ onMounted(async () => {
}
}
.checkbox-group {
.checkbox-label {
display: flex;
align-items: flex-start;
gap: 0.75rem;
cursor: pointer;
padding: 1rem;
border: 1px solid var(--theme-grey);
border-radius: rs(10px);
transition: border-color 0.2s;
&:hover {
border-color: var(--sea);
}
.checkbox {
width: 20px;
height: 20px;
margin-top: 0.125rem;
cursor: pointer;
accent-color: var(--sea);
&:checked {
background: var(--theme-contrast);
}
}
.checkbox-text {
display: flex;
flex-direction: column;
gap: 0.25rem;
flex: 1;
strong {
display: block;
}
small {
color: var(--theme-grey);
}
}
}
}
.form-actions {
display: flex;
gap: 1rem;

View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Endless Journal</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View file

@ -0,0 +1,23 @@
{
"name": "endless-journal",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@vueuse/core": "^14.3.0",
"css-font": "^1.2.0",
"matter-js": "^0.20.0",
"sass": "^1.98.0",
"sass-embedded": "^1.98.0",
"vue": "^3.5.30"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.5",
"vite": "^8.0.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,124 @@
{
"name": "endless-journal",
"title": "Endless Journal",
"date": "2026-07-20",
"tags": [
"Process"
],
"description": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": " Creativity is following the thread of intuition and seeing where it goes. It's about being in the moment and not looking back."
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "The process of destruction is just as important as the process of creation. Destruction frees the mind and allows it to always stay on the move, constantly reaching new territory."
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "As you type, the letters will begin to pile up as a mass of objects. These letters no longer signify the words you wrote but progress in the creative process. There are three fill lines to signify this progress:"
}
]
},
{
"type": "bulletList",
"content": [
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"marks": [
{
"type": "bold"
}
],
"text": "Intuition: "
},
{
"type": "text",
"text": "This first stage is about finding that intuitive thread and simply holding onto it to see where it takes you."
}
]
}
]
},
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"marks": [
{
"type": "bold"
}
],
"text": "Chaos: "
},
{
"type": "text",
"text": "The second stage is about throwing out every idea your mind relates to that intuitive thread. Nothing is organized yet and that's a good thing. It's ok if you don't feel like you are getting anywhere yet."
}
]
}
]
},
{
"type": "listItem",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"marks": [
{
"type": "bold"
}
],
"text": "Concept: "
},
{
"type": "text",
"text": "By this stage you will hopefully start to take the mass of chaotic ideas and form them into a single core concept. This is the concept you will use to build your creation."
}
]
}
]
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "If a concept still has not shown itself, that's ok, reload and repeat."
}
]
}
]
},
"published": true
}

View file

@ -0,0 +1,18 @@
<template>
<main class="drop theme-light">
<EndlessJournal />
</main>
</template>
<script setup>
import EndlessJournal from './components/EndlessJournal.vue'
</script>
<style lang="scss">
main.drop {
background: var(--theme-bg);
color: var(--theme-fg);
height: 100vh;
overflow: hidden;
}
</style>

View file

@ -0,0 +1,120 @@
<template>
<div class="endless-journal">
<input
v-model="input"
class="input"
type="text"
ref="inputEl"
@blur="onBlur"
/>
<div class="cursor" />
<div class="fill-line one">
<h6>Intuition</h6>
<div class="line" />
</div>
<div class="fill-line two">
<h6>Chaos</h6>
<div class="line" />
</div>
<div class="fill-line three">
<h6>Concept</h6>
<div class="line" />
</div>
<physics :input="input" />
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import Physics from './Physics.vue'
import { until } from '@vueuse/core'
const input = ref()
const inputEl = ref()
onMounted(async () => {
await until(inputEl).toBeTruthy()
await new Promise((res) => setTimeout(res, 50))
inputEl.value.focus()
})
const onBlur = async () => {
await new Promise((res) => setTimeout(res, 50))
inputEl.value.focus()
}
</script>
<style lang="scss">
.endless-journal {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
.input {
border: 1px solid var(--theme-fg);
position: absolute;
top: 20vh;
opacity: 0;
}
.cursor {
width: rs(10px);
height: rs(100px);
background: var(--theme-fg);
opacity: 0.2;
position: absolute;
top: 15vh;
z-index: 2;
animation: cursor 1s infinite;
}
.fill-line {
position: absolute;
left: 0;
right: 0;
padding: 0 var(--layout-margin);
z-index: 2;
h6 {
text-transform: uppercase;
color: var(--theme-grey);
font-weight: 400;
}
.line {
position: absolute;
bottom: -1em;
left: 0;
right: 0;
height: 2px;
background: var(--theme-grey);
}
&.one {
bottom: 20vh;
}
&.two {
bottom: 40vh;
}
&.three {
bottom: 60vh;
}
}
@keyframes cursor {
5% {
opacity: 0;
}
40% {
opacity: 0;
}
60% {
opacity: 0.2;
}
95% {
opacity: 0.2;
}
}
}
</style>

View file

@ -0,0 +1,231 @@
<template>
<div class="endless-journal-physics">
<canvas width="64" height="64" ref="canvas" hidden />
<div class="container" ref="container" />
</div>
</template>
<script setup>
import Matter from 'matter-js'
import { onMounted, ref, watch } from 'vue'
import { fontAtlas } from '@/libs/fontAtlas'
import { useElementBounding } from '@vueuse/core'
import { themes } from '@shared/config'
const props = defineProps({ input: String })
const container = ref()
const canvas = ref()
const { width, height } = useElementBounding(container)
const chars = [
'!',
'?',
'"',
"'",
',',
'-',
'.',
':',
';',
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
]
const charTextures = ref({})
const generateCharImages = (atlas) => {
const ctx = canvas.value.getContext('2d')
const step = 64
for (let y = 0; y < 8; y++) {
for (let x = 0; x < 8; x++) {
const index = y * 8 + x
if (chars[index]) {
const char = chars[index]
ctx.clearRect(0, 0, 64, 64)
ctx.drawImage(atlas, x * step, y * step, 64, 64, 0, 0, 64, 64)
charTextures.value[char] = canvas.value.toDataURL()
}
}
}
}
// Matter-js
const Bodies = ref()
const Composite = ref()
const Engine = ref()
const engine = ref()
onMounted(async () => {
const atlas = await fontAtlas({
font: {
size: '60px',
// family: 'Iosevka Charon Mono',
family: 'monospace',
weight: '700',
},
step: [64, 64],
chars: chars,
color: themes.light.fg
})
generateCharImages(atlas)
const Render = Matter.Render
Engine.value = Matter.Engine
Bodies.value = Matter.Bodies
Composite.value = Matter.Composite
// create an engine
engine.value = Engine.value.create()
// create a renderer
const render = Render.create({
element: container.value,
engine: engine.value,
options: {
width: width.value,
height: height.value,
wireframes: false,
background: 'transparent',
},
})
const ground = Bodies.value.rectangle(
width.value / 2,
height.value + 50,
width.value,
100,
{
isStatic: true,
}
)
const wallLeft = Bodies.value.rectangle(
-50,
height.value / 2,
100,
height.value,
{
isStatic: true,
}
)
const wallRight = Bodies.value.rectangle(
width.value,
height.value / 2,
1,
height.value,
{
isStatic: true,
}
)
// add all of the bodies to the world
Composite.value.add(engine.value.world, [ground, wallLeft, wallRight])
// run the renderer
Render.run(render)
run()
})
const run = () => {
const bodies = Composite.value.allBodies(engine.value.world)
bodies.forEach((body) => {
if (body.speed < 0.05 && !body.isStatic) {
Matter.Body.setStatic(body, true)
}
})
const delta = 1000 / 60
Engine.value.update(engine.value, delta)
requestAnimationFrame(run)
}
watch(
() => props.input,
(newInput, oldInput) => {
if (
newInput?.length > oldInput?.length ||
(newInput.length && !oldInput)
) {
const char = newInput.substr(newInput.length - 1).toUpperCase()
if (charTextures.value[char]) {
const charBox = Bodies.value.rectangle(
width.value / 2,
height.value * 0.2,
32,
32,
{
render: {
sprite: {
texture: charTextures.value[char],
xScale: 0.5,
yScale: 0.5,
},
},
}
)
Matter.Body.setVelocity(charBox, {
x: Math.random() * 40 - 20,
y: 30,
})
Composite.value.add(engine.value.world, [charBox])
}
}
}
)
</script>
<style lang="scss">
.endless-journal-physics {
position: absolute;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
.container {
position: absolute;
inset: 5px;
pointer-events: none;
}
}
</style>

View file

@ -0,0 +1,63 @@
'use strict'
import stringifyFont from 'css-font/stringify'
const defaultChars = [32, 126]
export function fontAtlas(options) {
options = options || {}
let shape = options.shape
? options.shape
: options.canvas
? [options.canvas.width, options.canvas.height]
: [512, 512]
const canvas = options.canvas || document.createElement('canvas')
let font = options.font
const step =
typeof options.step === 'number'
? [options.step, options.step]
: options.step || [32, 32]
let chars = options.chars || defaultChars
if (font && typeof font !== 'string') font = stringifyFont(font)
if (!Array.isArray(chars)) {
chars = String(chars).split('')
} else if (
chars.length === 2 &&
typeof chars[0] === 'number' &&
typeof chars[1] === 'number'
) {
const newchars = []
for (let i = chars[0], j = 0; i <= chars[1]; i++) {
newchars[j++] = String.fromCharCode(i)
}
chars = newchars
}
shape = shape.slice()
canvas.width = shape[0]
canvas.height = shape[1]
const ctx = canvas.getContext('2d')
ctx.fillStyle = '#fff'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.font = font
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillStyle = options.color || '#000'
let x = step[0] / 2
let y = step[1] / 2
for (let i = 0; i < chars.length; i++) {
ctx.fillText(chars[i], x, y)
if ((x += step[0]) > shape[0] - step[0] / 2)
(x = step[0] / 2), (y += step[1])
}
return canvas
}

View file

@ -0,0 +1,20 @@
export function clamp(min, input, max) {
return Math.max(min, Math.min(input, max));
}
export function mapRange(in_min, in_max, input, out_min, out_max) {
return ((input - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min;
}
export function lerp(start, end, amt) {
return (1 - amt) * start + amt * end;
}
export function truncate(value, decimals) {
return parseFloat(value.toFixed(decimals));
}
export function wrapValue(value, lowBound, highBound) {
const range = highBound - lowBound;
return ((((value - lowBound) % range) + range) % range) + lowBound;
}

View file

@ -0,0 +1,6 @@
import '@shared/styles/shared.scss'
import './styles/main.scss'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

View file

@ -0,0 +1 @@
/* Drop specific styles */

View file

@ -0,0 +1,21 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, mergeConfig } from 'vite'
import sharedConfig from '../../shared/vite.config.js'
export default defineConfig(
mergeConfig(sharedConfig, {
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@shared': fileURLToPath(
new URL('../../shared', import.meta.url),
),
},
},
base: '/drops/endless-journal/',
build: {
outDir: '../../app/public/drops/endless-journal',
emptyOutDir: true,
},
}),
)

View file

@ -42,5 +42,6 @@
"date": "2026-07-16",
"tags": [
"visualization"
]
}
],
"published": true
}

View file

@ -39,7 +39,7 @@
}
]
},
"date": "2026-07-16",
"date": "2026-07-17",
"tags": [
"tag1",
"tag2",
@ -51,5 +51,6 @@
"url": "https://github.com/username/repo"
}
],
"notes": "Any additional notes or thoughts about this drop"
}
"notes": "Any additional notes or thoughts about this drop",
"published": false
}

112
package-lock.json generated
View file

@ -21,6 +21,7 @@
"@tiptap/pm": "^3.28.0",
"@tiptap/starter-kit": "^3.28.0",
"@tiptap/vue-3": "^3.28.0",
"fecha": "^4.2.3",
"sass": "^1.98.0",
"sass-embedded": "^1.98.0",
"vue": "^3.5.30",
@ -47,6 +48,21 @@
"vite": "^8.0.0"
}
},
"drops/endless-journal": {
"version": "0.0.0",
"dependencies": {
"@vueuse/core": "^14.3.0",
"css-font": "^1.2.0",
"matter-js": "^0.20.0",
"sass": "^1.98.0",
"sass-embedded": "^1.98.0",
"vue": "^3.5.30"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.5",
"vite": "^8.0.0"
}
},
"drops/flowmap": {
"version": "0.0.0",
"dependencies": {
@ -1725,6 +1741,59 @@
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
"license": "MIT"
},
"node_modules/css-font": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/css-font/-/css-font-1.2.0.tgz",
"integrity": "sha512-V4U4Wps4dPDACJ4WpgofJ2RT5Yqwe1lEH6wlOOaIxMi0gTjdIijsc5FmxQlZ7ZZyKQkkutqqvULOp07l9c7ssA==",
"license": "MIT",
"dependencies": {
"css-font-size-keywords": "^1.0.0",
"css-font-stretch-keywords": "^1.0.1",
"css-font-style-keywords": "^1.0.1",
"css-font-weight-keywords": "^1.0.0",
"css-global-keywords": "^1.0.1",
"css-system-font-keywords": "^1.0.0",
"pick-by-alias": "^1.2.0",
"string-split-by": "^1.0.0",
"unquote": "^1.1.0"
}
},
"node_modules/css-font-size-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz",
"integrity": "sha512-Q+svMDbMlelgCfH/RVDKtTDaf5021O486ZThQPIpahnIjUkMUslC+WuOQSWTgGSrNCH08Y7tYNEmmy0hkfMI8Q==",
"license": "MIT"
},
"node_modules/css-font-stretch-keywords": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/css-font-stretch-keywords/-/css-font-stretch-keywords-1.0.1.tgz",
"integrity": "sha512-KmugPO2BNqoyp9zmBIUGwt58UQSfyk1X5DbOlkb2pckDXFSAfjsD5wenb88fNrD6fvS+vu90a/tsPpb9vb0SLg==",
"license": "MIT"
},
"node_modules/css-font-style-keywords": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/css-font-style-keywords/-/css-font-style-keywords-1.0.1.tgz",
"integrity": "sha512-0Fn0aTpcDktnR1RzaBYorIxQily85M2KXRpzmxQPgh8pxUN9Fcn00I8u9I3grNr1QXVgCl9T5Imx0ZwKU973Vg==",
"license": "MIT"
},
"node_modules/css-font-weight-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-font-weight-keywords/-/css-font-weight-keywords-1.0.0.tgz",
"integrity": "sha512-5So8/NH+oDD+EzsnF4iaG4ZFHQ3vaViePkL1ZbZ5iC/KrsCY+WHq/lvOgrtmuOQ9pBBZ1ADGpaf+A4lj1Z9eYA==",
"license": "MIT"
},
"node_modules/css-global-keywords": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/css-global-keywords/-/css-global-keywords-1.0.1.tgz",
"integrity": "sha512-X1xgQhkZ9n94WDwntqst5D/FKkmiU0GlJSFZSV3kLvyJ1WC5VeyoXDOuleUD+SIuH9C7W05is++0Woh0CGfKjQ==",
"license": "MIT"
},
"node_modules/css-system-font-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-system-font-keywords/-/css-system-font-keywords-1.0.0.tgz",
"integrity": "sha512-1umTtVd/fXS25ftfjB71eASCrYhilmEsvDEI6wG/QplnmlfmVM5HkZ/ZX46DT5K3eblFPgLUHt5BRCb0YXkSFA==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@ -1746,6 +1815,10 @@
"integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
"license": "Apache-2.0"
},
"node_modules/endless-journal": {
"resolved": "drops/endless-journal",
"link": true
},
"node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
@ -1787,6 +1860,12 @@
}
}
},
"node_modules/fecha": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
"license": "MIT"
},
"node_modules/fflate": {
"version": "0.6.10",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
@ -2193,6 +2272,12 @@
"url": "https://github.com/sponsors/sxzz"
}
},
"node_modules/matter-js": {
"version": "0.20.0",
"resolved": "https://registry.npmjs.org/matter-js/-/matter-js-0.20.0.tgz",
"integrity": "sha512-iC9fYR7zVT3HppNnsFsp9XOoQdQN2tUyfaKg4CHLH8bN+j6GT4Gw7IH2rP0tflAebrHFw730RR3DkVSZRX8hwA==",
"license": "MIT"
},
"node_modules/mlly": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
@ -2265,6 +2350,12 @@
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
"license": "MIT"
},
"node_modules/parenthesis": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/parenthesis/-/parenthesis-3.1.8.tgz",
"integrity": "sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==",
"license": "MIT"
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@ -2277,6 +2368,12 @@
"integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
"license": "MIT"
},
"node_modules/pick-by-alias": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pick-by-alias/-/pick-by-alias-1.2.0.tgz",
"integrity": "sha512-ESj2+eBxhGrcA1azgHs7lARG5+5iLakc/6nlfbpjcLl00HuuUOIuORhYXN4D1HfvMSKuVtFQjAlnwi1JHEeDIw==",
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -2999,6 +3096,15 @@
"integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==",
"license": "MIT"
},
"node_modules/string-split-by": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/string-split-by/-/string-split-by-1.0.0.tgz",
"integrity": "sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A==",
"license": "MIT",
"dependencies": {
"parenthesis": "^3.1.5"
}
},
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
@ -3188,6 +3294,12 @@
"url": "https://github.com/sponsors/sxzz"
}
},
"node_modules/unquote": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz",
"integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==",
"license": "MIT"
},
"node_modules/varint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",

View file

@ -11,7 +11,7 @@
],
"scripts": {
"create": "node bin/createDrop",
"build:drops": "npm run build -w drops && node bin/buildManifest",
"build:drops": "node bin/buildDrops && node bin/buildManifest",
"dev:drops": "node bin/dev.js",
"dev": "npm run build:drops && npm run dev -w app",
"build": "npm run build:drops && npm run build -w app"

View file

@ -36,6 +36,17 @@ const editor = useEditor({
.rich-text {
font-family: inherit;
.tiptap > * {
margin-top: 1em;
margin-bottom: 1em;
&:first-child {
margin-top: 0;
}
&:last-child {
margin-bottom: 0;
}
}
h1 {
margin: 2em 0 1em;
@include label;

View file

@ -3,5 +3,6 @@
"title": "$NAME",
"date": "2026-07-16",
"tags": ["tag1", "tag2", "tag3"],
"description": "A brief description of what this drop does"
"description": "A brief description of what this drop does",
"published": false
}