Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
// Interactive Counter — a tldraw offline document script.
|
||||
//
|
||||
// HOW TO RUN IT ON YOUR MACHINE:
|
||||
// 1. Open tldraw offline, create or open a document.
|
||||
// 2. Develop → Reveal Script… (creates script/main.js + a workspace folder)
|
||||
// 3. Replace the contents of script/main.js with THIS file, and save.
|
||||
// 4. The app reruns the script automatically. You'll see a "Counter" panel
|
||||
// with MINUS / RESET / PLUS buttons — click them; the number updates live.
|
||||
// 5. File → Save to persist the script into the .tldraw file. Now the file
|
||||
// *is* a little app: reopen it anywhere and the buttons still work.
|
||||
//
|
||||
// This is the document-script contract (from the app's script-context.d.ts):
|
||||
// export default function ({ editor, helpers, signal }) { ... }
|
||||
// editor — the live tldraw Editor
|
||||
// helpers — editor-bound conveniences (richTextToPlainText, etc.)
|
||||
// signal — an AbortSignal fired before the script reruns / on close;
|
||||
// register ALL cleanup on it so re-saving never leaks listeners.
|
||||
|
||||
import { createShapeId, toRichText } from 'tldraw'
|
||||
|
||||
export default function ({ editor, helpers, signal }) {
|
||||
// Stable ids => idempotent: re-running reuses shapes instead of duplicating.
|
||||
const IDS = {
|
||||
title: createShapeId('counter-title'),
|
||||
display: createShapeId('counter-display'),
|
||||
dec: createShapeId('counter-btn-dec'),
|
||||
reset: createShapeId('counter-btn-reset'),
|
||||
inc: createShapeId('counter-btn-inc'),
|
||||
}
|
||||
|
||||
// Create-if-missing helper (leaves user edits intact on rerun).
|
||||
function ensure(partial) {
|
||||
if (editor.getShape(partial.id)) return
|
||||
editor.createShape(partial)
|
||||
}
|
||||
|
||||
editor.run(() => {
|
||||
ensure({
|
||||
id: IDS.title, type: 'text', x: 40, y: 20,
|
||||
props: { richText: toRichText('Counter'), size: 'xl', font: 'draw', color: 'black' },
|
||||
})
|
||||
ensure({
|
||||
id: IDS.display, type: 'geo', x: 40, y: 80,
|
||||
props: { geo: 'rectangle', w: 360, h: 160, color: 'black', fill: 'none', richText: toRichText('0'), size: 'xl' },
|
||||
meta: { ui: 'display', count: 0 },
|
||||
})
|
||||
// Button labels are load-bearing — the click handler finds buttons by text.
|
||||
ensure({
|
||||
id: IDS.dec, type: 'geo', x: 40, y: 270,
|
||||
props: { geo: 'rectangle', w: 100, h: 80, color: 'red', fill: 'solid', richText: toRichText('MINUS'), size: 'l' },
|
||||
meta: { ui: 'button', action: 'MINUS' },
|
||||
})
|
||||
ensure({
|
||||
id: IDS.reset, type: 'geo', x: 170, y: 270,
|
||||
props: { geo: 'rectangle', w: 100, h: 80, color: 'grey', fill: 'solid', richText: toRichText('RESET'), size: 'l' },
|
||||
meta: { ui: 'button', action: 'RESET' },
|
||||
})
|
||||
ensure({
|
||||
id: IDS.inc, type: 'geo', x: 300, y: 270,
|
||||
props: { geo: 'rectangle', w: 100, h: 80, color: 'green', fill: 'solid', richText: toRichText('PLUS'), size: 'l' },
|
||||
meta: { ui: 'button', action: 'PLUS' },
|
||||
})
|
||||
})
|
||||
|
||||
const STEP = { MINUS: -1, PLUS: +1 }
|
||||
|
||||
function displayShape() {
|
||||
return editor.getCurrentPageShapes().find((s) => s.meta && s.meta.ui === 'display')
|
||||
}
|
||||
function setCount(n) {
|
||||
const d = displayShape()
|
||||
editor.run(
|
||||
() =>
|
||||
editor.updateShape({
|
||||
id: d.id, type: 'geo',
|
||||
props: { richText: toRichText(String(n)) },
|
||||
meta: { ...d.meta, count: n },
|
||||
}),
|
||||
{ history: 'ignore' } // keep script writes out of the user's undo stack
|
||||
)
|
||||
}
|
||||
function runAction(label) {
|
||||
const d = displayShape()
|
||||
const cur = d.meta && typeof d.meta.count === 'number' ? d.meta.count : 0
|
||||
if (label === 'RESET') setCount(0)
|
||||
else if (label in STEP) setCount(cur + STEP[label])
|
||||
}
|
||||
|
||||
function bounds(s) {
|
||||
return { x: s.x, y: s.y, w: s.props.w ?? 0, h: s.props.h ?? 0 }
|
||||
}
|
||||
function inside(b, p) {
|
||||
return p.x >= b.x && p.x <= b.x + b.w && p.y >= b.y && p.y <= b.y + b.h
|
||||
}
|
||||
|
||||
function onEvent(info) {
|
||||
if (!info || info.name !== 'pointer_down') return
|
||||
let p = null
|
||||
try {
|
||||
if (info.point && editor.screenToPage) p = editor.screenToPage(info.point)
|
||||
} catch {}
|
||||
p = p ?? editor.inputs?.currentPagePoint
|
||||
if (!p) return
|
||||
const hit = editor
|
||||
.getCurrentPageShapes()
|
||||
.find((s) => s.meta && s.meta.ui === 'button' && inside(bounds(s), p))
|
||||
if (hit) runAction(hit.meta.action)
|
||||
}
|
||||
|
||||
editor.on('event', onEvent)
|
||||
signal.addEventListener('abort', () => editor.off('event', onEvent)) // required cleanup
|
||||
|
||||
editor.zoomToFit({ animation: { duration: 200 } })
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// tldraw offline — document script (script/main.js)
|
||||
//
|
||||
// A document script's default export receives a ctx object and runs whenever the
|
||||
// document loads (and reruns when you save the script). Contract, verified against
|
||||
// the app's bundled script-context.d.ts:
|
||||
//
|
||||
// export default function ({ editor, helpers, signal }) { ... }
|
||||
//
|
||||
// editor — the live tldraw Editor for this document
|
||||
// helpers — editor-bound conveniences: createShapeIfMissing, createShapesIfMissing,
|
||||
// createArrowBetweenShapes, translateShapes, onShapeTranslate,
|
||||
// richTextToPlainText, boxShapes, getLints
|
||||
// signal — an AbortSignal fired before the script reruns and when the board
|
||||
// closes. Register ALL cleanup on it (this is how you avoid leaks).
|
||||
//
|
||||
// Pure tldraw primitives (createShapeId, toRichText, Vec, ...) are imported from
|
||||
// the `tldraw` app module — NOT globals. react / react-dom are also importable.
|
||||
// It is not a Node project; only those modules are available.
|
||||
|
||||
import { createShapeId, toRichText } from 'tldraw'
|
||||
|
||||
export default function ({ editor, helpers, signal }) {
|
||||
const { createShapeIfMissing, createArrowBetweenShapes } = helpers
|
||||
|
||||
// --- 1. Build durable "furniture" idempotently (stable ids, create-if-missing).
|
||||
// Re-running the script must NOT duplicate or clobber user edits.
|
||||
const nodes = [
|
||||
{ id: createShapeId('node-ui'), x: 0, y: 0, color: 'blue', label: 'CLI / Gateway' },
|
||||
{ id: createShapeId('node-core'), x: 280, y: 0, color: 'violet', label: 'Agent Core' },
|
||||
{ id: createShapeId('node-tools'), x: 560, y: 0, color: 'green', label: 'Tools' },
|
||||
]
|
||||
|
||||
editor.run(() => {
|
||||
for (const n of nodes) {
|
||||
createShapeIfMissing({
|
||||
id: n.id,
|
||||
type: 'geo',
|
||||
x: n.x,
|
||||
y: n.y,
|
||||
props: {
|
||||
geo: 'rectangle',
|
||||
w: 220,
|
||||
h: 110,
|
||||
color: n.color,
|
||||
fill: 'solid',
|
||||
richText: toRichText(n.label),
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Connect them (arrows bind to the shapes, so they follow when moved).
|
||||
createArrowBetweenShapes(nodes[0].id, nodes[1].id, { arrowheadEnd: 'arrow' })
|
||||
createArrowBetweenShapes(nodes[1].id, nodes[2].id, { arrowheadEnd: 'arrow' })
|
||||
|
||||
// --- 2. Add reactive behavior: recolor the last node based on arrow count.
|
||||
// store.listen fires on the tick AFTER a commit — never read state you just
|
||||
// wrote synchronously and expect the listener to have run yet.
|
||||
const targetId = nodes[2].id
|
||||
function update() {
|
||||
const hasArrows = editor.getCurrentPageShapes().some((s) => s.type === 'arrow')
|
||||
editor.run(
|
||||
() =>
|
||||
editor.updateShape({
|
||||
id: targetId,
|
||||
type: 'geo',
|
||||
props: { fill: hasArrows ? 'solid' : 'none' },
|
||||
}),
|
||||
{ history: 'ignore' } // keep script-owned writes out of the user's undo stack
|
||||
)
|
||||
}
|
||||
|
||||
const stop = editor.store.listen(update)
|
||||
signal.addEventListener('abort', () => stop()) // <-- the one required cleanup
|
||||
update() // run once on load
|
||||
|
||||
editor.zoomToFit({ animation: { duration: 200 } })
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env node
|
||||
// validate_shapes.mjs — verify that the note/text/frame records this skill
|
||||
// documents are valid against the real tldraw SDK v5 schema.
|
||||
//
|
||||
// Usage:
|
||||
// npm install @tldraw/tlschema
|
||||
// node validate_shapes.mjs
|
||||
//
|
||||
// Exits 0 when all sample records validate, 1 otherwise. No network, no DOM.
|
||||
|
||||
import { createTLSchema, toRichText, createShapeId, PageRecordType } from '@tldraw/tlschema'
|
||||
|
||||
const schema = createTLSchema()
|
||||
const shapeRecord = schema.types.shape
|
||||
const pageId = PageRecordType.createId()
|
||||
|
||||
// Complete default prop sets (required when building raw records outside the editor).
|
||||
const COMPLETE = {
|
||||
note: {
|
||||
richText: toRichText(''), color: 'black', labelColor: 'black', size: 'm',
|
||||
font: 'draw', align: 'middle', verticalAlign: 'middle', growY: 0,
|
||||
fontSizeAdjustment: 0, url: '', scale: 1, textLastEditedBy: '',
|
||||
},
|
||||
text: {
|
||||
richText: toRichText(''), color: 'black', size: 'm', font: 'draw',
|
||||
textAlign: 'start', w: 8, scale: 1, autoSize: true,
|
||||
},
|
||||
frame: { w: 300, h: 640, name: '', color: 'black' },
|
||||
}
|
||||
|
||||
function makeRecord(type, props, meta = {}, x = 0, y = 0) {
|
||||
return {
|
||||
id: createShapeId(), typeName: 'shape', type, parentId: pageId, index: 'a1',
|
||||
x, y, rotation: 0, isLocked: false, opacity: 1, meta,
|
||||
props: { ...COMPLETE[type], ...props },
|
||||
}
|
||||
}
|
||||
|
||||
const cases = [
|
||||
['frame', makeRecord('frame', { w: 300, h: 640, name: 'To Do' }, { role: 'column' })],
|
||||
['text', makeRecord('text', { richText: toRichText('To Do · WIP 2'), size: 's', color: 'grey', font: 'sans' }, { role: 'count' }, 8, -34)],
|
||||
['note', makeRecord('note', { richText: toRichText('Design the thing'), size: 's' }, { role: 'card' }, 20, 48)],
|
||||
]
|
||||
|
||||
let ok = 0
|
||||
const validated = []
|
||||
for (const [name, rec] of cases) {
|
||||
try {
|
||||
validated.push(shapeRecord.validate(rec))
|
||||
console.log(`OK ${name}`)
|
||||
ok++
|
||||
} catch (e) {
|
||||
console.log(`FAIL ${name}: ${String(e.message).split('\n')[0]}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Round-trip through JSON to mimic file save/load.
|
||||
let rok = 0
|
||||
for (const v of validated) {
|
||||
try { shapeRecord.validate(JSON.parse(JSON.stringify(v))); rok++ } catch { /* counted below */ }
|
||||
}
|
||||
|
||||
console.log(`\n${ok}/${cases.length} shape records valid against the tldraw schema.`)
|
||||
console.log(`${rok}/${validated.length} survive a JSON round-trip (file save/load).`)
|
||||
process.exit(ok === cases.length && rok === validated.length ? 0 : 1)
|
||||
Reference in New Issue
Block a user