feat(canva): Hanzo AI for Canva — Apps SDK copy assistant
A side-panel Canva app over the published @hanzo/ai: generate copy, rewrite the selected text in place, translate, brainstorm content ideas, and ask. - design-core.ts (PURE): the five-action catalog, prompt builders, design- context assembly + budget truncation, and response parsing (fence/quote/ list-marker stripping). Mirrors @hanzo/figma's design-core. Fully unit-tested. - hanzo.ts: thin over @hanzo/ai (createAiClient) — the only model-gateway path. - canva.ts: thin over @canva/design — observe the plaintext selection, replace it via a fresh draft, insert a new text element at the cursor. - app.tsx: the @canva/app-ui-kit panel (model picker, action picker, output, ideas). index.tsx mounts it under AppUiProvider. - config.ts: api.hanzo.ai /v1 gateway, default zen5, hk- key guard. 40 vitest tests green, tsc --noEmit clean, esbuild build succeeds. Registered in pnpm-workspace.yaml.
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# Canva Apps SDK configuration, read by `@canva/cli` (`canva apps preview`).
|
||||
# Copy to `.env` and fill in the App ID from your app in the Canva Developer
|
||||
# Portal (https://www.canva.com/developers/apps). Never commit `.env`.
|
||||
|
||||
# The App ID from the Developer Portal (App details → "App ID").
|
||||
CANVA_APP_ID=
|
||||
|
||||
# The panel dev server / bundle origin the CLI serves. Default matches build.js
|
||||
# output served at localhost:8080; the CLI can also proxy its own dev server.
|
||||
CANVA_APP_ORIGIN=http://localhost:8080
|
||||
|
||||
# Optional: a backend base if you host a Hanzo-side token exchange. Not required —
|
||||
# this app takes a user-pasted hk- key and calls api.hanzo.ai directly via
|
||||
# @hanzo/ai, so no backend is needed for the core flow.
|
||||
CANVA_BACKEND_HOST=
|
||||
@@ -0,0 +1,111 @@
|
||||
# Hanzo AI for Canva
|
||||
|
||||
A Canva **Apps SDK** side-panel app that puts Hanzo's models next to your design.
|
||||
Non-designers, marketers, and SMBs — Canva's core audience — get copywriting help
|
||||
without leaving the canvas:
|
||||
|
||||
- **Generate copy** — headlines, captions, and marketing copy from a short brief.
|
||||
- **Rewrite selected text** — tighten, restyle, or restate the text you selected, in place.
|
||||
- **Translate** — the selected text into any language, tone preserved.
|
||||
- **Content ideas** — a numbered list of angles for a topic; click one to add it.
|
||||
- **Ask** — a question about the design or its copy.
|
||||
|
||||
It reads the current selection and page via `@canva/design`, runs the model gateway
|
||||
through the **published `@hanzo/ai`** headless client over `api.hanzo.ai`, and adds or
|
||||
replaces text elements with the result. You paste your own Hanzo `hk-` key; the app
|
||||
holds no secret and calls no `/api/` path — only `api.hanzo.ai/v1/...`.
|
||||
|
||||
## Architecture — one way to do everything
|
||||
|
||||
| Module | Responsibility | Depends on |
|
||||
| --- | --- | --- |
|
||||
| `src/design-core.ts` | **PURE.** The action catalog, prompt builders, design-context assembly + truncation, and response parsing. No SDK, no DOM. Fully unit-tested. | — |
|
||||
| `src/hanzo.ts` | Thin wrapper over `@hanzo/ai` — the only place the model gateway is called. | `@hanzo/ai` |
|
||||
| `src/canva.ts` | Thin adapter over `@canva/design` — read the selection, replace it, insert a new text element. | `@canva/design` |
|
||||
| `src/config.ts` | Gateway base URL, default model, context budget, `hk-` key guard. | — |
|
||||
| `src/app.tsx` | The App UI Kit panel — binds the three above to widgets; holds only UI state. | `@canva/app-ui-kit` |
|
||||
| `src/index.tsx` | Entry — mounts `App` under `AppUiProvider`. | `react-dom`, `@canva/app-ui-kit` |
|
||||
|
||||
The design-core mirrors [`@hanzo/figma`](../figma)'s design-core so the two design
|
||||
apps expose the same five actions with the same context/prompt/parse contracts.
|
||||
|
||||
### The read-selection → insert-text flow
|
||||
|
||||
1. On mount, `canva.ts` `onSelectionChange` subscribes to `selection.registerOnChange({ scope: 'plaintext' })`. Each change yields a `SelectionSnapshot` (`count`, joined `text`); the panel gates **Rewrite** / **Translate** on `count > 0` and shows a preview of the selected text.
|
||||
2. You pick an action, add a brief/question/language as needed, and press **Run**.
|
||||
3. `design-core.buildActionMessages(id, inputs, { selection })` resolves the action's task, fences the observed design as **data** (with an honest truncation note if it was cut), and returns the OpenAI-shaped message list.
|
||||
4. `hanzo.runChat(messages, { token, model })` calls `@hanzo/ai` `chat.completions.create` (non-streaming) and returns the assistant text.
|
||||
5. `design-core.parseCopy` / `parseIdeas` strips code fences, wrapping quotes, and list markers into paste-ready text.
|
||||
6. You press **Replace selection** (Rewrite → `canva.replaceSelectedText`, which reads a fresh draft, overwrites `draft.contents[].text`, and `draft.save()`s) or **Add to design** (everything else → `canva.insertText` → `addElementAtCursor` with a new `TextElement`). Ideas insert one line per click.
|
||||
|
||||
## Develop locally
|
||||
|
||||
Prerequisites: Node 18+, pnpm, and a Canva account.
|
||||
|
||||
### 1. Create the app in the Canva Developer Portal
|
||||
|
||||
1. Go to <https://www.canva.com/developers/apps> and **Create an app**.
|
||||
2. Under **App details**, copy the **App ID**.
|
||||
3. Under **Add features → Editing**, enable the app to run in the editor side panel (this app needs the *Design editing* scope: read the selection, add/replace elements).
|
||||
4. Under **Configure your app**, set the **Development URL** to the CLI/preview origin (default `http://localhost:8080`).
|
||||
|
||||
### 2. Configure
|
||||
|
||||
```bash
|
||||
cd packages/canva
|
||||
cp .env.template .env
|
||||
# put your App ID into CANVA_APP_ID
|
||||
```
|
||||
|
||||
### 3. Install, test, build
|
||||
|
||||
```bash
|
||||
pnpm install # from the repo root (workspace)
|
||||
pnpm --filter @hanzo/canva test # vitest — the pure design-core + hanzo shaping
|
||||
pnpm --filter @hanzo/canva typecheck # tsc --noEmit, strict
|
||||
pnpm --filter @hanzo/canva build # esbuild → dist/{index.html,app.js,app.css}
|
||||
```
|
||||
|
||||
### 4. Preview in Canva
|
||||
|
||||
Serve the bundle and point the Developer Portal's Development URL at it. With the
|
||||
Canva CLI:
|
||||
|
||||
```bash
|
||||
npx @canva/cli@latest login
|
||||
pnpm --filter @hanzo/canva watch # rebuilds dist/ on change
|
||||
npx @canva/cli@latest apps preview # opens the app in the Canva editor, hot-reloading
|
||||
```
|
||||
|
||||
Open any design, launch **Hanzo AI** from the app side panel, paste your `hk-` key,
|
||||
pick a model, and run an action. Select text on the canvas to enable **Rewrite** and
|
||||
**Translate**.
|
||||
|
||||
### Getting a Hanzo `hk-` key
|
||||
|
||||
Sign in at <https://hanzo.id> and mint an API key (prefixed `hk-`). The panel
|
||||
validates the prefix before use and sends it as the bearer to `api.hanzo.ai`. The key
|
||||
lives only in the panel's session state — it is never persisted or bundled.
|
||||
|
||||
## Submit to the Canva Apps Marketplace
|
||||
|
||||
1. Finish and test the app in preview; ensure `pnpm build` is clean and `dist/` loads.
|
||||
2. Host `dist/` on your production origin (behind `hanzoai/ingress` + the `hanzoai/static` plugin — never nginx/caddy) and set the app's production **App source URL** to it.
|
||||
3. In the Developer Portal, complete **App listing** (name, icon, description, screenshots) and **Submit for review**.
|
||||
4. Canva reviews for the requested scopes (Design editing here) and UX. Address feedback and resubmit; on approval the app is published to the Apps Marketplace.
|
||||
|
||||
## Tests
|
||||
|
||||
`test/design-core.test.ts` covers the pure core: whitespace collapse, context
|
||||
assembly + budget truncation, message fencing (data vs. task, the truncation note,
|
||||
the no-context path), the five prompt builders (brief/tweak/language/count/question
|
||||
embedding and the count clamp), the `id → messages` path, and the response parsers
|
||||
(fence/quote stripping, idempotence, list-marker stripping). `test/hanzo.test.ts`
|
||||
asserts the `@hanzo/ai` request shaping against a mock client (model, `stream:false`,
|
||||
temperature, messages, abort signal, empty-content error, model listing).
|
||||
`test/config.test.ts` covers the `/v1` endpoint builders and the `hk-` key guard.
|
||||
|
||||
```
|
||||
Test Files 3 passed (3)
|
||||
Tests 40 passed (40)
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
// Build @hanzo/canva into dist/: bundle the App UI Kit side-panel app
|
||||
// (src/index.tsx) as ESM for the browser and stamp index.html with the entry. No
|
||||
// framework — esbuild + Node stdlib only, mirroring @hanzo/shopify's build. The
|
||||
// Canva CLI (`canva apps preview`) serves this dist/ (or points at its own dev
|
||||
// server); either way the app is a static bundle Canva loads in the app iframe.
|
||||
// Every model call goes to api.hanzo.ai via @hanzo/ai; the app holds no secret —
|
||||
// the user pastes their own hk- key.
|
||||
//
|
||||
// node build.js → production build (dist/index.html, dist/app.js, dist/app.css)
|
||||
// node build.js --watch → rebuild on change
|
||||
|
||||
import esbuild from 'esbuild';
|
||||
import { rmSync, mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const watch = process.argv.includes('--watch');
|
||||
const src = join(__dirname, 'src');
|
||||
const dist = join(__dirname, 'dist');
|
||||
|
||||
async function build() {
|
||||
rmSync(dist, { recursive: true, force: true });
|
||||
mkdirSync(dist, { recursive: true });
|
||||
|
||||
// Bundle the panel as ESM for the browser. React + App UI Kit are bundled in
|
||||
// (the page is served standalone, no import map); the App UI Kit stylesheet
|
||||
// import emits dist/app.css.
|
||||
const ctx = await esbuild.context({
|
||||
entryPoints: { app: join(src, 'index.tsx') },
|
||||
outdir: dist,
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
platform: 'browser',
|
||||
target: ['chrome90', 'edge90', 'firefox90', 'safari15'],
|
||||
jsx: 'automatic',
|
||||
loader: { '.css': 'css', '.svg': 'dataurl', '.png': 'dataurl', '.woff': 'dataurl', '.woff2': 'dataurl' },
|
||||
define: { 'process.env.NODE_ENV': JSON.stringify(watch ? 'development' : 'production') },
|
||||
sourcemap: true,
|
||||
minify: !watch,
|
||||
logLevel: 'info',
|
||||
});
|
||||
await ctx.rebuild();
|
||||
|
||||
// Stamp index.html with the entry.
|
||||
const html = readFileSync(join(src, 'index.html'), 'utf8').split('__ENTRY__').join('app.js');
|
||||
writeFileSync(join(dist, 'index.html'), html);
|
||||
|
||||
console.log('Hanzo AI for Canva built -> dist/ (App: dist/index.html)');
|
||||
|
||||
if (watch) {
|
||||
await ctx.watch();
|
||||
console.log('watching...');
|
||||
} else {
|
||||
await ctx.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
build()
|
||||
.then(() => {
|
||||
if (!watch && !existsSync(join(dist, 'app.css'))) {
|
||||
// The App UI Kit ships its stylesheet via '@canva/app-ui-kit/styles.css'
|
||||
// (imported in index.tsx); esbuild bundles it to app.css. If a future path
|
||||
// changes, warn rather than ship an unstyled app.
|
||||
console.warn('note: dist/app.css not emitted — check the App UI Kit styles import in index.tsx');
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "@hanzo/canva",
|
||||
"version": "0.1.0",
|
||||
"description": "Hanzo AI for Canva — a side-panel Apps SDK app: generate marketing copy, rewrite the selected text, translate, brainstorm content ideas, and ask about the design. An @canva/app-ui-kit React panel that reads the selection/page via @canva/design and adds/replaces text elements, running the model gateway through the published @hanzo/ai over api.hanzo.ai.",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "node build.js",
|
||||
"watch": "node build.js --watch",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hanzo/ai": "^0.2.0",
|
||||
"@hanzo/iam": "^0.13.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@canva/app-ui-kit": "^5.0.0",
|
||||
"@canva/asset": "^2.0.0",
|
||||
"@canva/design": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@canva/app-ui-kit": "^5.12.0",
|
||||
"@canva/asset": "^2.3.0",
|
||||
"@canva/design": "^2.10.0",
|
||||
"@types/node": "^20.14.0",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"esbuild": "^0.25.8",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.2.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"hanzo",
|
||||
"canva",
|
||||
"apps-sdk",
|
||||
"copywriting",
|
||||
"marketing-copy",
|
||||
"translate",
|
||||
"ai",
|
||||
"design",
|
||||
"app-ui-kit"
|
||||
],
|
||||
"author": "Hanzo AI",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/hanzoai/extension.git",
|
||||
"directory": "packages/canva"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
// The Hanzo AI assistant panel — a Canva App UI Kit React component. All the
|
||||
// logic-heavy work lives in tested, pure modules: the action catalog + prompt
|
||||
// assembly + response parsing in design-core.ts, the model call in hanzo.ts
|
||||
// (@hanzo/ai), and the design I/O in canva.ts (@canva/design). This component
|
||||
// only binds them to App UI Kit widgets and holds the panel's small UI state
|
||||
// (the pasted key, the picked model, the chosen action, the instruction, the
|
||||
// output). It never speaks to api.hanzo.ai or the Canva SDK directly.
|
||||
//
|
||||
// Flow: pick an action → (for generate/ideas/ask) type a brief, (for rewrite/
|
||||
// translate) select text on the canvas → Run → design-core builds the request
|
||||
// from the observed selection/page → hanzo.ts calls the model → design-core
|
||||
// parses the reply → the panel shows it and inserts it via canva.ts (replace the
|
||||
// selection in place, or add a new text element).
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
FormField,
|
||||
LoadingIndicator,
|
||||
MultilineInput,
|
||||
Rows,
|
||||
Select,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@canva/app-ui-kit';
|
||||
import {
|
||||
buildActionMessages,
|
||||
designActionList,
|
||||
parseCopy,
|
||||
parseIdeas,
|
||||
DESIGN_ACTIONS,
|
||||
type ActionInputs,
|
||||
type DesignActionId,
|
||||
type DesignContext,
|
||||
} from './design-core.js';
|
||||
import { runChat, listModels } from './hanzo.js';
|
||||
import { onSelectionChange, replaceSelectedText, insertText, type SelectionSnapshot } from './canva.js';
|
||||
import { isHanzoKey, DEFAULT_MODEL } from './config.js';
|
||||
|
||||
// The catalog the panel renders, derived from design-core so labels/flags never
|
||||
// drift from what will actually run.
|
||||
const ACTIONS = designActionList();
|
||||
|
||||
type Status = { tone: 'positive' | 'critical' | 'info'; text: string } | null;
|
||||
|
||||
export function App(): JSX.Element {
|
||||
const [key, setKey] = useState('');
|
||||
const [models, setModels] = useState<string[]>([]);
|
||||
const [model, setModel] = useState(DEFAULT_MODEL);
|
||||
const [action, setAction] = useState<DesignActionId>('generate');
|
||||
const [instruction, setInstruction] = useState('');
|
||||
const [language, setLanguage] = useState('Spanish');
|
||||
const [selection, setSelection] = useState<SelectionSnapshot>({ count: 0, text: '' });
|
||||
const [output, setOutput] = useState('');
|
||||
const [ideas, setIdeas] = useState<string[]>([]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [status, setStatus] = useState<Status>(null);
|
||||
|
||||
const keyValid = isHanzoKey(key);
|
||||
const spec = DESIGN_ACTIONS[action];
|
||||
|
||||
// Track the live plaintext selection so rewrite/translate know what they act on
|
||||
// and the panel can gate those actions until something is selected.
|
||||
useEffect(() => onSelectionChange(setSelection), []);
|
||||
|
||||
// Load the model catalog once a valid key is pasted (org-scoped by the bearer).
|
||||
useEffect(() => {
|
||||
if (!keyValid) return;
|
||||
let live = true;
|
||||
listModels({ token: key })
|
||||
.then((ids) => {
|
||||
if (!live) return;
|
||||
setModels(ids);
|
||||
if (ids.length && !ids.includes(model)) setModel(ids[0]);
|
||||
})
|
||||
.catch(() => live && setModels([]));
|
||||
return () => {
|
||||
live = false;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [keyValid, key]);
|
||||
|
||||
// The design context we can observe right now — handed to design-core to fence
|
||||
// as data. The page text is left to a future read; the selection is the field
|
||||
// rewrite/translate act on and is always current.
|
||||
const context = useMemo<DesignContext>(
|
||||
() => ({ selection: selection.text || undefined }),
|
||||
[selection.text],
|
||||
);
|
||||
|
||||
const inputs = useMemo<ActionInputs>(
|
||||
() => ({ instruction, language }),
|
||||
[instruction, language],
|
||||
);
|
||||
|
||||
// canRun encodes the action's preconditions: a valid key, plus a selection for
|
||||
// selection-actions and an instruction for instruction-actions.
|
||||
const missing =
|
||||
!keyValid
|
||||
? 'Paste your Hanzo hk- key to begin.'
|
||||
: spec.needsSelection && selection.count === 0
|
||||
? 'Select text on your design to run this action.'
|
||||
: spec.needsInstruction && instruction.trim() === ''
|
||||
? `Enter ${action === 'ask' ? 'a question' : 'a brief'} to run this action.`
|
||||
: '';
|
||||
|
||||
const run = useCallback(async () => {
|
||||
if (missing) {
|
||||
setStatus({ tone: 'info', text: missing });
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setStatus(null);
|
||||
setOutput('');
|
||||
setIdeas([]);
|
||||
try {
|
||||
const messages = buildActionMessages(action, inputs, context);
|
||||
const reply = await runChat(messages, { token: key, model });
|
||||
if (action === 'ideas') {
|
||||
setIdeas(parseIdeas(reply));
|
||||
} else {
|
||||
setOutput(parseCopy(reply));
|
||||
}
|
||||
} catch (e) {
|
||||
setStatus({ tone: 'critical', text: e instanceof Error ? e.message : 'Generation failed' });
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [missing, action, inputs, context, key, model]);
|
||||
|
||||
// Insert the generated copy. Rewrite replaces the selection in place; every
|
||||
// other action adds a new text element at the cursor.
|
||||
const place = useCallback(
|
||||
async (text: string) => {
|
||||
setBusy(true);
|
||||
setStatus(null);
|
||||
try {
|
||||
if (action === 'rewrite') {
|
||||
await replaceSelectedText(text);
|
||||
setStatus({ tone: 'positive', text: 'Replaced the selected text.' });
|
||||
} else {
|
||||
await insertText(text, action === 'generate' ? 32 : 18);
|
||||
setStatus({ tone: 'positive', text: 'Added to your design.' });
|
||||
}
|
||||
} catch (e) {
|
||||
setStatus({ tone: 'critical', text: e instanceof Error ? e.message : 'Could not add to design' });
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
},
|
||||
[action],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box padding="2u">
|
||||
<Rows spacing="2u">
|
||||
<Title size="small">Hanzo AI</Title>
|
||||
|
||||
<FormField
|
||||
label="Hanzo API key"
|
||||
description={keyValid ? 'Key looks valid.' : 'Paste your hk- key from hanzo.id'}
|
||||
control={(props) => (
|
||||
<TextInput {...props} placeholder="hk-..." value={key} onChange={setKey} />
|
||||
)}
|
||||
/>
|
||||
|
||||
{models.length > 0 && (
|
||||
<FormField
|
||||
label="Model"
|
||||
control={() => (
|
||||
<Select
|
||||
stretch
|
||||
value={model}
|
||||
onChange={setModel}
|
||||
options={models.map((id) => ({ value: id, label: id }))}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
label="Action"
|
||||
control={() => (
|
||||
<Select
|
||||
stretch
|
||||
value={action}
|
||||
onChange={(v) => setAction(v as DesignActionId)}
|
||||
options={ACTIONS.map((a) => ({ value: a.id, label: a.label }))}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{action === 'translate' ? (
|
||||
<FormField
|
||||
label="Target language"
|
||||
control={(props) => (
|
||||
<TextInput {...props} placeholder="Spanish" value={language} onChange={setLanguage} />
|
||||
)}
|
||||
/>
|
||||
) : spec.needsInstruction ? (
|
||||
<FormField
|
||||
label={action === 'ask' ? 'Question' : action === 'ideas' ? 'Topic' : 'Brief'}
|
||||
control={() => (
|
||||
<MultilineInput
|
||||
autoGrow
|
||||
minRows={2}
|
||||
placeholder={
|
||||
action === 'ask'
|
||||
? 'What tone should this poster use?'
|
||||
: action === 'ideas'
|
||||
? 'Summer sale for a coffee brand'
|
||||
: 'A bold headline for our spring launch'
|
||||
}
|
||||
value={instruction}
|
||||
onChange={setInstruction}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{spec.needsSelection && (
|
||||
<Text size="small" tone="tertiary">
|
||||
{selection.count > 0
|
||||
? `Selected: ${selection.text.slice(0, 80)}${selection.text.length > 80 ? '…' : ''}`
|
||||
: 'Nothing selected — select text on your design.'}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Button variant="primary" onClick={() => void run()} disabled={busy} stretch loading={busy}>
|
||||
{spec.label}
|
||||
</Button>
|
||||
|
||||
{busy && <LoadingIndicator />}
|
||||
{status && <Alert tone={status.tone}>{status.text}</Alert>}
|
||||
|
||||
{output && (
|
||||
<Rows spacing="1u">
|
||||
<FormField
|
||||
label="Result"
|
||||
control={() => (
|
||||
<MultilineInput autoGrow minRows={3} value={output} onChange={setOutput} />
|
||||
)}
|
||||
/>
|
||||
<Button variant="primary" onClick={() => void place(output)} disabled={busy} stretch>
|
||||
{action === 'rewrite' ? 'Replace selection' : 'Add to design'}
|
||||
</Button>
|
||||
</Rows>
|
||||
)}
|
||||
|
||||
{ideas.length > 0 && (
|
||||
<Rows spacing="1u">
|
||||
<Text size="small" tone="tertiary">Content ideas</Text>
|
||||
{ideas.map((idea, i) => (
|
||||
<Button key={i} variant="secondary" onClick={() => void place(idea)} disabled={busy} stretch>
|
||||
{idea}
|
||||
</Button>
|
||||
))}
|
||||
</Rows>
|
||||
)}
|
||||
</Rows>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// The thin adapter over @canva/design — the only place the Canva Apps SDK is
|
||||
// touched. It turns Canva's event-driven, draft-based design model into the two
|
||||
// operations the panel needs: OBSERVE the current selection (its plaintext + a
|
||||
// mutable handle to overwrite it) and INSERT AI output onto the canvas (add a new
|
||||
// text element, or replace the selected text in place). All the AI logic lives in
|
||||
// design-core.ts (pure) and hanzo.ts (@hanzo/ai); this module carries no prompts
|
||||
// and no model calls — it is pure design I/O.
|
||||
//
|
||||
// Canva's contract (@canva/design 2.x):
|
||||
// - `selection.registerOnChange({ scope: 'plaintext', onChange })` fires with a
|
||||
// `count` and a `read()` that yields a draft whose `contents[].text` are the
|
||||
// selected text runs; mutating them and `draft.save()` writes them back.
|
||||
// - `addElementAtCursor(textElement)` / `addNativeElement(textElement)` add a
|
||||
// new text element at the user's cursor / onto the current page.
|
||||
// - `getCurrentPageContext()` yields the page dimensions (used only to size a
|
||||
// from-scratch insert sensibly).
|
||||
|
||||
import {
|
||||
selection,
|
||||
addElementAtCursor,
|
||||
type TextElement,
|
||||
} from '@canva/design';
|
||||
|
||||
// ---- Selection observation ------------------------------------------------
|
||||
|
||||
// SelectionSnapshot is what the panel needs to reason about the current
|
||||
// selection without holding a Canva draft: how many text elements are selected
|
||||
// and their combined plaintext (for context assembly and for showing the user
|
||||
// what rewrite/translate will act on). A draft is short-lived and must be
|
||||
// re-read at write time, so we never stash it — see replaceSelectedText.
|
||||
export interface SelectionSnapshot {
|
||||
/** Number of selected plaintext elements. 0 means nothing text is selected. */
|
||||
count: number;
|
||||
/** The selected text runs joined with newlines — '' when count is 0. */
|
||||
text: string;
|
||||
}
|
||||
|
||||
// onSelectionChange subscribes to plaintext selection changes and calls back with
|
||||
// a SelectionSnapshot each time. It reads the draft only to extract the text (it
|
||||
// never mutates or saves here) and returns the SDK's unsubscribe function so the
|
||||
// panel can tear the listener down on unmount. This is the panel's single source
|
||||
// of "what is selected right now".
|
||||
export function onSelectionChange(cb: (snap: SelectionSnapshot) => void): () => void {
|
||||
return selection.registerOnChange({
|
||||
scope: 'plaintext',
|
||||
onChange: async (event) => {
|
||||
if (event.count === 0) {
|
||||
cb({ count: 0, text: '' });
|
||||
return;
|
||||
}
|
||||
const draft = await event.read();
|
||||
const text = draft.contents.map((c) => c.text).join('\n');
|
||||
cb({ count: event.count, text });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Writing output onto the canvas ---------------------------------------
|
||||
|
||||
// replaceSelectedText overwrites the currently-selected plaintext with `text`.
|
||||
// Canva requires the write to happen inside a fresh draft read (the snapshot the
|
||||
// panel holds may be stale), so we register a one-shot listener, read the current
|
||||
// draft, write every selected run — the first run gets the full replacement and
|
||||
// any further runs are cleared so N selected elements collapse to the one new
|
||||
// value — save, and unsubscribe. Resolves once saved; rejects if nothing is
|
||||
// selected when the write is attempted.
|
||||
export function replaceSelectedText(text: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let done = false;
|
||||
const unsubscribe = selection.registerOnChange({
|
||||
scope: 'plaintext',
|
||||
onChange: async (event) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
try {
|
||||
if (event.count === 0) {
|
||||
reject(new Error('No text is selected to replace'));
|
||||
return;
|
||||
}
|
||||
const draft = await event.read();
|
||||
draft.contents.forEach((c, i) => {
|
||||
c.text = i === 0 ? text : '';
|
||||
});
|
||||
await draft.save();
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject(e instanceof Error ? e : new Error(String(e)));
|
||||
} finally {
|
||||
unsubscribe();
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// insertText adds a new text element carrying `text` at the user's cursor. Used
|
||||
// for generate / ideas / translate-as-new — anything that adds copy rather than
|
||||
// editing the selection. `fontSize` sizes it (a headline larger than a caption);
|
||||
// Canva positions it at the cursor.
|
||||
export function insertText(text: string, fontSize = 24): Promise<void> {
|
||||
const element: TextElement = {
|
||||
type: 'text',
|
||||
children: [text],
|
||||
fontSize,
|
||||
};
|
||||
return addElementAtCursor(element);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Canva app config — the api.hanzo.ai model gateway (default model + endpoints)
|
||||
// and the design-context character budget. Endpoints and the bearer choice
|
||||
// mirror @hanzo/shopify / @hanzo/figma so the productivity suite stays DRY; the
|
||||
// design-specific pieces (context assembly, the AI actions, response parsing)
|
||||
// live in design-core.ts, and the two thin adapters — @hanzo/ai in hanzo.ts and
|
||||
// @canva/design in canva.ts — carry the I/O.
|
||||
|
||||
// ---- Hanzo model gateway --------------------------------------------------
|
||||
|
||||
// Where the Hanzo model gateway lives. `@hanzo/ai` (createAiClient) defaults
|
||||
// here too. /v1 only, never an /api/ prefix (api.hanzo.ai IS the api host).
|
||||
export const HANZO_API_BASE_URL = 'https://api.hanzo.ai';
|
||||
|
||||
// Default model. A Zen model (qwen3+). Overridable per-request via the picker;
|
||||
// the gateway routes it.
|
||||
export const DEFAULT_MODEL = 'zen5';
|
||||
|
||||
// Public IAM origin that mints Hanzo user tokens, and the OAuth client id an
|
||||
// inbound Hanzo token is audienced to (owner-scoping validation via @hanzo/iam).
|
||||
// The Canva app itself pastes an `hk-` key; these exist so a Hanzo-hosted
|
||||
// backend can validate one under the shared <org>-<app> naming scheme.
|
||||
export const DEFAULT_IAM_SERVER_URL = 'https://hanzo.id';
|
||||
export const DEFAULT_IAM_CLIENT_ID = 'hanzo-canva';
|
||||
|
||||
// chatCompletionsURL / modelsURL — the model gateway endpoints. The client
|
||||
// (createAiClient) builds these itself; these exist for tests + honest docs.
|
||||
export function chatCompletionsURL(): string {
|
||||
return `${HANZO_API_BASE_URL}/v1/chat/completions`;
|
||||
}
|
||||
export function modelsURL(): string {
|
||||
return `${HANZO_API_BASE_URL}/v1/models`;
|
||||
}
|
||||
|
||||
// ---- Design context budget ------------------------------------------------
|
||||
//
|
||||
// A Canva design's on-page text (the selection plus the surrounding page text
|
||||
// we assemble as context) can run long — a poster's body copy, a slide's notes.
|
||||
// This caps the characters of design context we attach to any one request so it
|
||||
// fits comfortably in a model window alongside the reply. Honest truncation,
|
||||
// never silent drop (design-core marks `truncated`).
|
||||
export const DESIGN_CHAR_BUDGET = 12_000;
|
||||
|
||||
// A Hanzo API key is minted by hanzo.id and is always prefixed `hk-`. isHanzoKey
|
||||
// is the boundary guard: the panel validates a pasted key against this before it
|
||||
// is ever handed to the client as a bearer, so an obviously-wrong paste (an IAM
|
||||
// JWT, an OpenAI `sk-` key) is rejected in the UI instead of failing opaquely at
|
||||
// the gateway. Trim first — users paste with trailing whitespace.
|
||||
export function isHanzoKey(key: string | undefined | null): key is string {
|
||||
return typeof key === 'string' && /^hk-[A-Za-z0-9._-]{16,}$/.test(key.trim());
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
// The design action catalog and everything pure around it: the prompt builders
|
||||
// for copy / rewrite / translate / ideas / ask, the design-context assembly and
|
||||
// truncation, the message shaping @hanzo/ai takes, and the response parsing that
|
||||
// turns a model reply into paste-ready text. NO @canva imports, NO @hanzo/ai
|
||||
// imports, NO DOM — this module is host-agnostic and fully unit-testable. The
|
||||
// panel (app.tsx) reads a selection / page via canva.ts, hands the text here to
|
||||
// build a request, sends it through hanzo.ts, and parses the reply back here
|
||||
// before inserting it via canva.ts.
|
||||
//
|
||||
// It mirrors @hanzo/figma's design-core so the two design apps stay consistent:
|
||||
// the same five action families, the same context/prompt/parse contracts. There
|
||||
// is exactly ONE code path from an (action id + inputs) to a message list, and
|
||||
// ONE path from a raw reply to paste-ready text.
|
||||
|
||||
// A message in the OpenAI-compatible schema — the shape @hanzo/ai's
|
||||
// chat.completions.create takes. A local alias so the pure builders have a
|
||||
// precise, testable return type independent of the SDK's broader union.
|
||||
export interface ChatMessage {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
// SYSTEM_PROMPT grounds every answer in the design context provided. It forbids
|
||||
// inventing facts about the brand or product (the failure mode that makes a
|
||||
// marketing assistant dangerous — fake claims, fake prices, fake stats), and
|
||||
// keeps output ready to drop onto the canvas. Brand-neutral, designer-facing.
|
||||
export const SYSTEM_PROMPT =
|
||||
'You are Hanzo AI, a copywriting assistant embedded in Canva. You help create ' +
|
||||
'marketing copy — headlines, captions, body copy, calls to action — and edit ' +
|
||||
'the text already on the design. Work from the design context provided below; ' +
|
||||
'never invent facts, prices, statistics, dates, or claims that are not ' +
|
||||
'supported by it or by the user\'s instruction. Write in clear, natural, ' +
|
||||
'on-brand prose sized for a design surface — punchy for headlines, concise for ' +
|
||||
'captions. Return ONLY the requested text, ready to drop onto the canvas — no ' +
|
||||
'preamble, no explanation, no surrounding quotes, no markdown fences.';
|
||||
|
||||
// ---- Design context assembly ----------------------------------------------
|
||||
//
|
||||
// The panel gathers what it can see about the design — the selected text (the
|
||||
// primary subject of rewrite/translate) and the other text on the current page
|
||||
// (surrounding voice/context) — and hands it here as a DesignContext. We render
|
||||
// it into a compact, labeled block the model reads as data (never as
|
||||
// instructions), and cap the whole block at the budget. Honest truncation.
|
||||
|
||||
// What the panel can observe about the design and pass to a prompt builder. All
|
||||
// fields optional: a blank design has none, a fresh "generate" has only a brief.
|
||||
export interface DesignContext {
|
||||
/** The user's selected text — the subject of rewrite/translate. */
|
||||
selection?: string;
|
||||
/** Other text on the current page, in reading order — surrounding voice. */
|
||||
pageText?: string[];
|
||||
/** The design's title, if the app can read it. */
|
||||
title?: string;
|
||||
}
|
||||
|
||||
// The result of rendering a DesignContext down to what we attach: the text and
|
||||
// whether anything was truncated (so the prompt and UI can say so honestly).
|
||||
export interface RenderedContext {
|
||||
text: string;
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
// collapseWhitespace normalizes a text fragment for the context block: collapse
|
||||
// runs of whitespace (including the newlines Canva stores between text lines)
|
||||
// into single spaces and trim. Pure. The model does not need the layout, and
|
||||
// raw newlines waste budget and blur the data/instruction boundary.
|
||||
export function collapseWhitespace(s: string): string {
|
||||
return s.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
// buildDesignContext renders the observable design into a labeled block, capped
|
||||
// at `budget` characters. The selection is the field the model most needs (it is
|
||||
// what rewrite/translate act on), so it is rendered first and the surrounding
|
||||
// page text — the longest, least essential field — is what truncation trims.
|
||||
// Pure and total. `truncated` is true whenever the render was cut to fit.
|
||||
export function buildDesignContext(ctx: DesignContext, budget: number = DESIGN_CHAR_BUDGET): RenderedContext {
|
||||
const lines: string[] = [];
|
||||
if (ctx.title) {
|
||||
const t = collapseWhitespace(ctx.title);
|
||||
if (t) lines.push(`Design title: ${t}`);
|
||||
}
|
||||
if (ctx.selection) {
|
||||
const sel = collapseWhitespace(ctx.selection);
|
||||
if (sel) lines.push(`Selected text: ${sel}`);
|
||||
}
|
||||
if (ctx.pageText && ctx.pageText.length) {
|
||||
const page = ctx.pageText.map(collapseWhitespace).filter(Boolean);
|
||||
if (page.length) lines.push(`Other text on this page: ${page.join(' | ')}`);
|
||||
}
|
||||
|
||||
const full = lines.join('\n');
|
||||
if (full.length <= budget) return { text: full, truncated: false };
|
||||
return { text: full.slice(0, budget), truncated: true };
|
||||
}
|
||||
|
||||
// DESIGN_CHAR_BUDGET default mirrors config.ts. Re-declared here as the builder's
|
||||
// own default so design-core stays importable with zero config dependency (the
|
||||
// panel passes the config value explicitly when it wants to; tests do not).
|
||||
const DESIGN_CHAR_BUDGET = 12_000;
|
||||
|
||||
// ---- Prompt assembly ------------------------------------------------------
|
||||
|
||||
// contextNote is the one honest sentence prepended when the render was cut, so
|
||||
// the model does not answer as though it saw the whole design.
|
||||
function contextNote(truncated: boolean): string {
|
||||
return truncated
|
||||
? 'Note: the design context below was truncated to fit — answer only from what is shown.'
|
||||
: '';
|
||||
}
|
||||
|
||||
// buildMessages fences the design context as DATA (never instructions) and rides
|
||||
// the honest note inside the user turn so it is never lost. The task (the
|
||||
// resolved action prompt, plus any user instruction) comes last. Pure — the
|
||||
// whole prompt is asserted in a test. When there is no context (a from-scratch
|
||||
// generate with only a brief), the fence is omitted so the user turn is just the
|
||||
// task.
|
||||
export function buildMessages(task: string, rendered: RenderedContext): ChatMessage[] {
|
||||
const system: ChatMessage = { role: 'system', content: SYSTEM_PROMPT };
|
||||
const note = contextNote(rendered.truncated);
|
||||
const notePrefix = note ? `${note}\n\n` : '';
|
||||
const fence = rendered.text
|
||||
? `---- design ----\n${rendered.text}\n---- end design ----\n\n`
|
||||
: '';
|
||||
const user: ChatMessage = {
|
||||
role: 'user',
|
||||
content: `${notePrefix}${fence}---- task ----\n${task}`,
|
||||
};
|
||||
return [system, user];
|
||||
}
|
||||
|
||||
// ---- The action catalog ---------------------------------------------------
|
||||
//
|
||||
// An action is (id → label + a prompt builder). The builder takes the action's
|
||||
// inputs (a brief, a target language, a topic) and returns the task string that
|
||||
// is layered onto the fenced context by buildMessages. Some actions ignore the
|
||||
// context (generate, ideas, ask-with-no-selection); some require the selection
|
||||
// (rewrite, translate). Keeping each action's task in one builder means the
|
||||
// panel, the picker, and any test resolve prompts one way.
|
||||
|
||||
// The inputs a design action may take. All optional; each builder reads only the
|
||||
// fields it needs and ignores the rest. `instruction` is the user's freeform
|
||||
// text (the brief for generate, the question for ask, the tweak for rewrite);
|
||||
// `language` is the target for translate; `count` bounds an ideas list.
|
||||
export interface ActionInputs {
|
||||
instruction?: string;
|
||||
language?: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
// clampCount bounds an ideas request to a sane, promptable range. Pure.
|
||||
function clampCount(count: number | undefined): number {
|
||||
if (!count || !Number.isFinite(count)) return 5;
|
||||
return Math.max(1, Math.min(10, Math.floor(count)));
|
||||
}
|
||||
|
||||
// The five design actions. Each `build` returns the task string. `needsSelection`
|
||||
// marks the actions that operate on the selected text (the panel disables them
|
||||
// until something is selected). `needsInstruction` marks the actions that
|
||||
// require a user brief/question (generate, ask) so the panel can validate before
|
||||
// sending. Frozen so the catalog is immutable at runtime.
|
||||
export const DESIGN_ACTIONS = {
|
||||
generate: {
|
||||
label: 'Generate copy',
|
||||
needsSelection: false,
|
||||
needsInstruction: true,
|
||||
build: (i: ActionInputs): string => {
|
||||
const brief = (i.instruction ?? '').trim();
|
||||
return (
|
||||
`Write marketing copy for this design based on the brief: "${brief}". ` +
|
||||
'Return polished, on-brand copy sized for a design surface — a headline, ' +
|
||||
'a short caption, or a few tight lines as the brief implies. Ground every ' +
|
||||
'claim in the brief and the design context. Return only the copy.'
|
||||
);
|
||||
},
|
||||
},
|
||||
rewrite: {
|
||||
label: 'Rewrite selected text',
|
||||
needsSelection: true,
|
||||
needsInstruction: false,
|
||||
build: (i: ActionInputs): string => {
|
||||
const tweak = (i.instruction ?? '').trim();
|
||||
const how = tweak
|
||||
? `Rewrite it as follows: ${tweak}.`
|
||||
: 'Rewrite it to be clearer, tighter, and more persuasive, keeping the same meaning and roughly the same length.';
|
||||
return (
|
||||
`Rewrite the selected text above. ${how} Do NOT add any fact not present ` +
|
||||
'in the design context or the instruction. Return only the rewritten text.'
|
||||
);
|
||||
},
|
||||
},
|
||||
translate: {
|
||||
label: 'Translate',
|
||||
needsSelection: true,
|
||||
needsInstruction: false,
|
||||
build: (i: ActionInputs): string => {
|
||||
const lang = (i.language ?? '').trim() || 'Spanish';
|
||||
return (
|
||||
`Translate the selected text above into ${lang}. Preserve the tone, ` +
|
||||
'intent, and any brand names verbatim; localize idioms naturally rather ' +
|
||||
'than translating word for word. Return only the translated text.'
|
||||
);
|
||||
},
|
||||
},
|
||||
ideas: {
|
||||
label: 'Content ideas',
|
||||
needsSelection: false,
|
||||
needsInstruction: true,
|
||||
build: (i: ActionInputs): string => {
|
||||
const topic = (i.instruction ?? '').trim();
|
||||
const n = clampCount(i.count);
|
||||
return (
|
||||
`Suggest ${n} distinct content ideas for this design about: "${topic}". ` +
|
||||
'Each idea a single short line (a headline or angle), grounded in the ' +
|
||||
'topic and the design context. Return a numbered list, nothing else.'
|
||||
);
|
||||
},
|
||||
},
|
||||
ask: {
|
||||
label: 'Ask',
|
||||
needsSelection: false,
|
||||
needsInstruction: true,
|
||||
build: (i: ActionInputs): string => {
|
||||
const q = (i.instruction ?? '').trim();
|
||||
return (
|
||||
`Answer this question about the design or its copy: "${q}". Answer only ` +
|
||||
'from the design context and general copywriting knowledge; if the ' +
|
||||
'context does not support an answer, say so plainly.'
|
||||
);
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
// A design action id from the catalog.
|
||||
export type DesignActionId = keyof typeof DESIGN_ACTIONS;
|
||||
|
||||
// isDesignActionId narrows an arbitrary string to a known id. Boundary guard —
|
||||
// the panel validates an inbound id here before running it.
|
||||
export function isDesignActionId(id: string): id is DesignActionId {
|
||||
return Object.prototype.hasOwnProperty.call(DESIGN_ACTIONS, id);
|
||||
}
|
||||
|
||||
// designActionList is the ordered catalog for building the UI, derived from the
|
||||
// action map so the panel and the catalog can never drift.
|
||||
export function designActionList(): Array<{
|
||||
id: DesignActionId;
|
||||
label: string;
|
||||
needsSelection: boolean;
|
||||
needsInstruction: boolean;
|
||||
}> {
|
||||
return (Object.keys(DESIGN_ACTIONS) as DesignActionId[]).map((id) => {
|
||||
const a = DESIGN_ACTIONS[id];
|
||||
return { id, label: a.label, needsSelection: a.needsSelection, needsInstruction: a.needsInstruction };
|
||||
});
|
||||
}
|
||||
|
||||
// actionTask resolves an id + inputs to its task string. Throws on an unknown id
|
||||
// (a boundary error surfaced to the caller) rather than silently running a
|
||||
// default. This is the single id→prompt resolution the panel calls.
|
||||
export function actionTask(id: string, inputs: ActionInputs = {}): string {
|
||||
if (!isDesignActionId(id)) throw new Error(`Unknown design action: ${id}`);
|
||||
return DESIGN_ACTIONS[id].build(inputs);
|
||||
}
|
||||
|
||||
// buildActionMessages is the single path from an (action id, inputs, observed
|
||||
// design) to the message list @hanzo/ai takes. Resolve the task, render the
|
||||
// context, assemble the messages. Pure — the whole request is asserted in a
|
||||
// test. The panel calls exactly this, then hands the result to hanzo.ts.
|
||||
export function buildActionMessages(
|
||||
id: string,
|
||||
inputs: ActionInputs,
|
||||
ctx: DesignContext,
|
||||
budget: number = DESIGN_CHAR_BUDGET,
|
||||
): ChatMessage[] {
|
||||
const task = actionTask(id, inputs);
|
||||
return buildMessages(task, buildDesignContext(ctx, budget));
|
||||
}
|
||||
|
||||
// ---- Response parsing -----------------------------------------------------
|
||||
//
|
||||
// Models sometimes wrap paste-ready text despite the system prompt: a leading
|
||||
// "Here's your headline:", a Markdown code fence, or matched surrounding quotes.
|
||||
// parseCopy strips those so the text drops cleanly onto the canvas. Total and
|
||||
// idempotent — parsing already-clean text returns it unchanged.
|
||||
|
||||
// stripFence removes a single wrapping Markdown code fence (```lang ... ```) if
|
||||
// the whole reply is fenced. It does NOT touch fences in the middle of a reply
|
||||
// (those are intentional content). Pure.
|
||||
function stripFence(s: string): string {
|
||||
const m = s.match(/^```[^\n]*\n([\s\S]*?)\n?```$/);
|
||||
return m ? m[1] : s;
|
||||
}
|
||||
|
||||
// stripWrappingQuotes removes one matched pair of surrounding quotes (straight or
|
||||
// curly) when the ENTIRE string is quoted — the model quoting a headline. It
|
||||
// leaves quotes that are part of the content (an opening quote with no close, an
|
||||
// internal quote). Pure.
|
||||
function stripWrappingQuotes(s: string): string {
|
||||
const pairs: Array<[string, string]> = [
|
||||
['"', '"'],
|
||||
["'", "'"],
|
||||
['“', '”'],
|
||||
['‘', '’'],
|
||||
];
|
||||
for (const [open, close] of pairs) {
|
||||
if (s.length >= 2 && s.startsWith(open) && s.endsWith(close)) {
|
||||
const inner = s.slice(1, -1);
|
||||
// Only strip if the inner text has no unescaped copy of the closing quote,
|
||||
// i.e. the quotes truly wrap the whole thing rather than being content.
|
||||
if (!inner.includes(close)) return inner;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// parseCopy turns a raw model reply into paste-ready text: trim, drop a single
|
||||
// wrapping code fence, drop matched surrounding quotes, trim again. Idempotent.
|
||||
// This is the single reply→text path the panel uses before inserting.
|
||||
export function parseCopy(raw: string): string {
|
||||
let s = raw.trim();
|
||||
s = stripFence(s).trim();
|
||||
s = stripWrappingQuotes(s).trim();
|
||||
return s;
|
||||
}
|
||||
|
||||
// parseIdeas turns a raw "ideas" reply — a numbered or bulleted list — into a
|
||||
// clean array of idea lines, dropping the list markers so each idea can be shown
|
||||
// as its own item and inserted individually. Pure. Blank lines and pure-marker
|
||||
// lines are dropped; a reply with no list structure yields its non-empty lines.
|
||||
export function parseIdeas(raw: string): string[] {
|
||||
return raw
|
||||
.split('\n')
|
||||
.map((line) => line.replace(/^\s*(?:\d+[.)]|[-*•])\s+/, '').trim())
|
||||
.map((line) => stripWrappingQuotes(line).trim())
|
||||
.filter((line) => line.length > 0);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// The single call path to the model — a THIN wrapper over the PUBLISHED headless
|
||||
// client `@hanzo/ai` (createAiClient). We do NOT reimplement the transport. This
|
||||
// module owns only client construction (token + baseURL wiring) and the two
|
||||
// primitives the panel needs: run a built message list to text, and list models.
|
||||
// The design-aware layer (context, prompts, parsing) is entirely in
|
||||
// design-core.ts; the Canva-aware layer (read selection, insert text) is in
|
||||
// canva.ts. Both are pure of the SDK; this is the only place the SDK is called.
|
||||
|
||||
import { createAiClient, type AiClient } from '@hanzo/ai';
|
||||
import { DEFAULT_MODEL, HANZO_API_BASE_URL } from './config.js';
|
||||
import type { ChatMessage } from './design-core.js';
|
||||
|
||||
export interface AskOptions {
|
||||
model?: string;
|
||||
temperature?: number;
|
||||
/** Hanzo `hk-` key pasted by the user; sent as the bearer. */
|
||||
token?: string;
|
||||
baseURL?: string;
|
||||
/** Injected client (tests). Defaults to a real createAiClient. */
|
||||
client?: AiClient;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
// client resolves an AiClient: the injected one (tests) or a real published
|
||||
// @hanzo/ai client pointed at the gateway with the caller's bearer. One place
|
||||
// constructs it so the token/baseURL wiring is identical for every call.
|
||||
function client(opts: AskOptions): AiClient {
|
||||
return opts.client ?? createAiClient({ token: opts.token, baseUrl: opts.baseURL ?? HANZO_API_BASE_URL });
|
||||
}
|
||||
|
||||
// runChat is the single path from a built message list to the model. Every Canva
|
||||
// surface (every action) funnels here. Non-streaming: the panel renders the
|
||||
// final text and inserts it. token may be empty (the gateway serves anonymous/
|
||||
// limited models), but the panel gates on a valid `hk-` key first.
|
||||
export async function runChat(messages: ChatMessage[], opts: AskOptions = {}): Promise<string> {
|
||||
const res = await client(opts).chat.completions.create(
|
||||
{
|
||||
model: opts.model ?? DEFAULT_MODEL,
|
||||
messages,
|
||||
temperature: opts.temperature,
|
||||
stream: false,
|
||||
},
|
||||
{ signal: opts.signal },
|
||||
);
|
||||
const content = res.choices?.[0]?.message?.content;
|
||||
if (typeof content !== 'string' || content === '') {
|
||||
throw new Error('Hanzo API returned no content');
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
// listModels returns the model ids the caller may route to, from /v1/models via
|
||||
// the headless client. Org-scoped by the bearer; an empty token lists public
|
||||
// models.
|
||||
export async function listModels(opts: AskOptions = {}): Promise<string[]> {
|
||||
const models = await client(opts).models.list({ signal: opts.signal });
|
||||
return models.map((m) => m.id);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Hanzo AI for Canva</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="__ENTRY__"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
// The Canva app entry — mounts the App UI Kit panel into the side-panel root
|
||||
// under AppUiProvider (which supplies the Canva theme + tokens the App UI Kit
|
||||
// components read). Canva loads this bundle in the app iframe; the element with
|
||||
// id `root` is the panel's mount point (see index.html). This file is the ONLY
|
||||
// DOM entry — all UI is in app.tsx, all logic in the pure/thin modules.
|
||||
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { AppUiProvider } from '@canva/app-ui-kit';
|
||||
import '@canva/app-ui-kit/styles.css';
|
||||
import { App } from './app.js';
|
||||
|
||||
const root = document.getElementById('root');
|
||||
if (!root) throw new Error('Missing #root mount element');
|
||||
|
||||
createRoot(root).render(
|
||||
<AppUiProvider>
|
||||
<App />
|
||||
</AppUiProvider>,
|
||||
);
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
HANZO_API_BASE_URL,
|
||||
DEFAULT_MODEL,
|
||||
chatCompletionsURL,
|
||||
modelsURL,
|
||||
isHanzoKey,
|
||||
} from '../src/config';
|
||||
|
||||
describe('gateway config', () => {
|
||||
it('points at api.hanzo.ai with /v1 endpoints (never /api/)', () => {
|
||||
expect(HANZO_API_BASE_URL).toBe('https://api.hanzo.ai');
|
||||
expect(chatCompletionsURL()).toBe('https://api.hanzo.ai/v1/chat/completions');
|
||||
expect(modelsURL()).toBe('https://api.hanzo.ai/v1/models');
|
||||
expect(chatCompletionsURL()).not.toContain('/api/');
|
||||
});
|
||||
|
||||
it('defaults to a Zen model', () => {
|
||||
expect(DEFAULT_MODEL).toBe('zen5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isHanzoKey', () => {
|
||||
it('accepts a well-formed hk- key (trimming whitespace)', () => {
|
||||
expect(isHanzoKey('hk-abcdef0123456789ABCDEF')).toBe(true);
|
||||
expect(isHanzoKey(' hk-abcdef0123456789ABCDEF ')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects the wrong prefix, too-short, or non-string', () => {
|
||||
expect(isHanzoKey('sk-abcdef0123456789ABCDEF')).toBe(false);
|
||||
expect(isHanzoKey('hk-short')).toBe(false);
|
||||
expect(isHanzoKey('')).toBe(false);
|
||||
expect(isHanzoKey(undefined)).toBe(false);
|
||||
expect(isHanzoKey(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,209 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
SYSTEM_PROMPT,
|
||||
collapseWhitespace,
|
||||
buildDesignContext,
|
||||
buildMessages,
|
||||
buildActionMessages,
|
||||
DESIGN_ACTIONS,
|
||||
designActionList,
|
||||
isDesignActionId,
|
||||
actionTask,
|
||||
parseCopy,
|
||||
parseIdeas,
|
||||
type ChatMessage,
|
||||
type DesignContext,
|
||||
} from '../src/design-core';
|
||||
|
||||
describe('collapseWhitespace', () => {
|
||||
it('collapses runs of whitespace and newlines and trims', () => {
|
||||
expect(collapseWhitespace(' a\n\n b\t c ')).toBe('a b c');
|
||||
});
|
||||
it('is idempotent on clean text', () => {
|
||||
expect(collapseWhitespace('already clean')).toBe('already clean');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildDesignContext', () => {
|
||||
it('renders title, selection, then page text as labeled data', () => {
|
||||
const ctx: DesignContext = {
|
||||
title: 'Spring Launch',
|
||||
selection: 'Big savings\nthis week',
|
||||
pageText: ['Shop now', ' Limited time '],
|
||||
};
|
||||
const r = buildDesignContext(ctx);
|
||||
expect(r.truncated).toBe(false);
|
||||
expect(r.text).toContain('Design title: Spring Launch');
|
||||
expect(r.text).toContain('Selected text: Big savings this week');
|
||||
expect(r.text).toContain('Other text on this page: Shop now | Limited time');
|
||||
// Selection comes before page text (it is what the model most needs).
|
||||
expect(r.text.indexOf('Selected text')).toBeLessThan(r.text.indexOf('Other text on this page'));
|
||||
});
|
||||
|
||||
it('omits absent fields and yields empty text for an empty design', () => {
|
||||
expect(buildDesignContext({})).toEqual({ text: '', truncated: false });
|
||||
const onlySel = buildDesignContext({ selection: 'x' });
|
||||
expect(onlySel.text).toBe('Selected text: x');
|
||||
});
|
||||
|
||||
it('drops blank page-text entries', () => {
|
||||
const r = buildDesignContext({ pageText: ['', ' ', 'kept'] });
|
||||
expect(r.text).toBe('Other text on this page: kept');
|
||||
});
|
||||
|
||||
it('truncates to the budget and flags it', () => {
|
||||
const r = buildDesignContext({ selection: 'x'.repeat(100) }, 40);
|
||||
expect(r.truncated).toBe(true);
|
||||
expect(r.text.length).toBe(40);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildMessages', () => {
|
||||
it('system grounds, design is fenced as data, task last', () => {
|
||||
const msgs = buildMessages('Do the thing.', { text: 'Selected text: hi', truncated: false });
|
||||
expect(msgs[0]).toEqual<ChatMessage>({ role: 'system', content: SYSTEM_PROMPT });
|
||||
expect(msgs[1].role).toBe('user');
|
||||
expect(msgs[1].content).toContain('---- design ----');
|
||||
expect(msgs[1].content).toContain('Selected text: hi');
|
||||
expect(msgs[1].content).toContain('---- end design ----');
|
||||
expect(msgs[1].content).toContain('---- task ----\nDo the thing.');
|
||||
});
|
||||
|
||||
it('omits the fence when there is no context', () => {
|
||||
const msgs = buildMessages('Generate.', { text: '', truncated: false });
|
||||
expect(msgs[1].content).not.toContain('---- design ----');
|
||||
expect(msgs[1].content).toBe('---- task ----\nGenerate.');
|
||||
});
|
||||
|
||||
it('prepends an honest truncation note when the context was cut', () => {
|
||||
const msgs = buildMessages('Rewrite.', { text: 'partial', truncated: true });
|
||||
expect(msgs[1].content).toContain('truncated to fit');
|
||||
expect(msgs[1].content.indexOf('truncated to fit')).toBeLessThan(msgs[1].content.indexOf('---- design ----'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('action catalog', () => {
|
||||
it('exposes the five design actions in order', () => {
|
||||
expect(Object.keys(DESIGN_ACTIONS)).toEqual(['generate', 'rewrite', 'translate', 'ideas', 'ask']);
|
||||
});
|
||||
|
||||
it('lists are derived from the catalog (labels/flags never drift)', () => {
|
||||
const list = designActionList();
|
||||
expect(list.map((a) => a.id)).toEqual(Object.keys(DESIGN_ACTIONS));
|
||||
expect(list.find((a) => a.id === 'rewrite')?.needsSelection).toBe(true);
|
||||
expect(list.find((a) => a.id === 'translate')?.needsSelection).toBe(true);
|
||||
expect(list.find((a) => a.id === 'generate')?.needsSelection).toBe(false);
|
||||
expect(list.find((a) => a.id === 'generate')?.needsInstruction).toBe(true);
|
||||
expect(list.find((a) => a.id === 'rewrite')?.needsInstruction).toBe(false);
|
||||
expect(list[0].label).toBe(DESIGN_ACTIONS.generate.label);
|
||||
});
|
||||
|
||||
it('narrows known ids and rejects unknown', () => {
|
||||
expect(isDesignActionId('generate')).toBe(true);
|
||||
expect(isDesignActionId('nope')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('actionTask (prompt builders)', () => {
|
||||
it('generate embeds the brief', () => {
|
||||
const t = actionTask('generate', { instruction: 'Bold spring headline' });
|
||||
expect(t).toContain('Bold spring headline');
|
||||
expect(t).toContain('Return only the copy');
|
||||
});
|
||||
|
||||
it('rewrite with no instruction uses the default improve prompt', () => {
|
||||
const t = actionTask('rewrite', {});
|
||||
expect(t).toContain('clearer, tighter, and more persuasive');
|
||||
expect(t).toContain('Do NOT add any fact');
|
||||
});
|
||||
|
||||
it('rewrite with an instruction embeds the tweak', () => {
|
||||
expect(actionTask('rewrite', { instruction: 'make it playful' })).toContain('Rewrite it as follows: make it playful');
|
||||
});
|
||||
|
||||
it('translate embeds the language and defaults to Spanish', () => {
|
||||
expect(actionTask('translate', { language: 'French' })).toContain('into French');
|
||||
expect(actionTask('translate', {})).toContain('into Spanish');
|
||||
});
|
||||
|
||||
it('ideas clamps the count into [1,10] and defaults to 5', () => {
|
||||
expect(actionTask('ideas', { instruction: 'coffee', count: 3 })).toContain('Suggest 3 distinct content ideas');
|
||||
expect(actionTask('ideas', { instruction: 'coffee', count: 99 })).toContain('Suggest 10 distinct content ideas');
|
||||
expect(actionTask('ideas', { instruction: 'coffee', count: 0 })).toContain('Suggest 5 distinct content ideas');
|
||||
expect(actionTask('ideas', { instruction: 'coffee' })).toContain('Suggest 5 distinct content ideas');
|
||||
});
|
||||
|
||||
it('ask embeds the question', () => {
|
||||
expect(actionTask('ask', { instruction: 'What tone?' })).toContain('What tone?');
|
||||
});
|
||||
|
||||
it('throws on an unknown id', () => {
|
||||
expect(() => actionTask('bogus')).toThrow(/Unknown design action/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildActionMessages (id + inputs + design -> messages)', () => {
|
||||
it('resolves the task and fences the selection', () => {
|
||||
const msgs = buildActionMessages('rewrite', { instruction: 'shorter' }, { selection: 'Buy now, save big' });
|
||||
expect(msgs[0].content).toBe(SYSTEM_PROMPT);
|
||||
expect(msgs[1].content).toContain('Selected text: Buy now, save big');
|
||||
expect(msgs[1].content).toContain('Rewrite it as follows: shorter');
|
||||
});
|
||||
|
||||
it('a from-scratch generate has no fence, only the task', () => {
|
||||
const msgs = buildActionMessages('generate', { instruction: 'launch headline' }, {});
|
||||
expect(msgs[1].content).not.toContain('---- design ----');
|
||||
expect(msgs[1].content).toContain('launch headline');
|
||||
});
|
||||
|
||||
it('honors the passed budget (truncation note appears)', () => {
|
||||
const msgs = buildActionMessages('rewrite', {}, { selection: 'x'.repeat(500) }, 50);
|
||||
expect(msgs[1].content).toContain('truncated to fit');
|
||||
});
|
||||
|
||||
it('rejects an unknown id', () => {
|
||||
expect(() => buildActionMessages('nope', {}, {})).toThrow(/Unknown design action/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseCopy', () => {
|
||||
it('returns clean text unchanged (idempotent)', () => {
|
||||
expect(parseCopy('Spring is here.')).toBe('Spring is here.');
|
||||
expect(parseCopy(parseCopy('Spring is here.'))).toBe('Spring is here.');
|
||||
});
|
||||
|
||||
it('strips a wrapping markdown code fence', () => {
|
||||
expect(parseCopy('```\nHeadline copy\n```')).toBe('Headline copy');
|
||||
expect(parseCopy('```text\nHeadline copy\n```')).toBe('Headline copy');
|
||||
});
|
||||
|
||||
it('strips matched surrounding quotes (straight and curly)', () => {
|
||||
expect(parseCopy('"Big sale today"')).toBe('Big sale today');
|
||||
expect(parseCopy('“Big sale today”')).toBe('Big sale today');
|
||||
});
|
||||
|
||||
it('does not strip an internal or unmatched quote', () => {
|
||||
expect(parseCopy('The "best" coffee')).toBe('The "best" coffee');
|
||||
expect(parseCopy('"unterminated')).toBe('"unterminated');
|
||||
});
|
||||
|
||||
it('trims surrounding whitespace', () => {
|
||||
expect(parseCopy(' Hello ')).toBe('Hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseIdeas', () => {
|
||||
it('strips numbered and bulleted list markers', () => {
|
||||
const out = parseIdeas('1. First idea\n2) Second idea\n- Third idea\n* Fourth\n• Fifth');
|
||||
expect(out).toEqual(['First idea', 'Second idea', 'Third idea', 'Fourth', 'Fifth']);
|
||||
});
|
||||
|
||||
it('drops blank lines and strips per-line wrapping quotes', () => {
|
||||
expect(parseIdeas('\n1. "Quoted idea"\n\n2. Plain\n')).toEqual(['Quoted idea', 'Plain']);
|
||||
});
|
||||
|
||||
it('falls back to non-empty lines when there is no list structure', () => {
|
||||
expect(parseIdeas('Just one line')).toEqual(['Just one line']);
|
||||
expect(parseIdeas('')).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { AiClient } from '@hanzo/ai';
|
||||
import { runChat, listModels } from '../src/hanzo';
|
||||
import type { ChatMessage } from '../src/design-core';
|
||||
|
||||
// A fake AiClient capturing the params passed to chat.completions.create and
|
||||
// models.list. This is how we assert request shaping without a network.
|
||||
function fakeClient(reply: string, models: string[] = ['zen5', 'zen5-mini']): {
|
||||
client: AiClient;
|
||||
calls: { params: any; options: any }[];
|
||||
} {
|
||||
const calls: { params: any; options: any }[] = [];
|
||||
const client = {
|
||||
chat: {
|
||||
completions: {
|
||||
create: async (params: any, options: any) => {
|
||||
calls.push({ params, options });
|
||||
return {
|
||||
id: 'x',
|
||||
object: 'chat.completion',
|
||||
created: 0,
|
||||
model: params.model,
|
||||
choices: [{ index: 0, message: { role: 'assistant', content: reply }, finish_reason: 'stop' }],
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
list: async () => models.map((id) => ({ id, object: 'model' })),
|
||||
},
|
||||
} as unknown as AiClient;
|
||||
return { client, calls };
|
||||
}
|
||||
|
||||
const MESSAGES: ChatMessage[] = [
|
||||
{ role: 'system', content: 'sys' },
|
||||
{ role: 'user', content: 'hi' },
|
||||
];
|
||||
|
||||
describe('runChat (request shaping over @hanzo/ai)', () => {
|
||||
it('sends model + messages, stream:false, temperature, and returns the content', async () => {
|
||||
const f = fakeClient('Generated copy.');
|
||||
const out = await runChat(MESSAGES, { client: f.client, model: 'zen5-mini', temperature: 0.4 });
|
||||
expect(out).toBe('Generated copy.');
|
||||
expect(f.calls).toHaveLength(1);
|
||||
expect(f.calls[0].params.model).toBe('zen5-mini');
|
||||
expect(f.calls[0].params.stream).toBe(false);
|
||||
expect(f.calls[0].params.temperature).toBe(0.4);
|
||||
expect(f.calls[0].params.messages).toEqual(MESSAGES);
|
||||
});
|
||||
|
||||
it('defaults the model to zen5 when none is given', async () => {
|
||||
const f = fakeClient('x');
|
||||
await runChat(MESSAGES, { client: f.client });
|
||||
expect(f.calls[0].params.model).toBe('zen5');
|
||||
});
|
||||
|
||||
it('throws when the model returns empty content', async () => {
|
||||
const f = fakeClient('');
|
||||
await expect(runChat(MESSAGES, { client: f.client })).rejects.toThrow(/no content/);
|
||||
});
|
||||
|
||||
it('forwards the abort signal in options', async () => {
|
||||
const f = fakeClient('x');
|
||||
const controller = new AbortController();
|
||||
await runChat(MESSAGES, { client: f.client, signal: controller.signal });
|
||||
expect(f.calls[0].options.signal).toBe(controller.signal);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listModels', () => {
|
||||
it('returns the model ids from the client', async () => {
|
||||
const f = fakeClient('x', ['zen5', 'zen5-mini', 'zen5-coder']);
|
||||
expect(await listModels({ client: f.client })).toEqual(['zen5', 'zen5-mini', 'zen5-coder']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"types": ["node"],
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['test/**/*.test.ts'],
|
||||
environment: 'node',
|
||||
},
|
||||
});
|
||||
Generated
+343
-8
@@ -258,6 +258,49 @@ importers:
|
||||
specifier: ^3.2.6
|
||||
version: 3.2.6(@types/node@22.7.5)(jsdom@26.1.0)(sass@1.60.0)(terser@5.43.1)
|
||||
|
||||
packages/canva:
|
||||
dependencies:
|
||||
'@hanzo/ai':
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
'@hanzo/iam':
|
||||
specifier: ^0.13.2
|
||||
version: 0.13.2(react@18.3.1)
|
||||
react:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1
|
||||
react-dom:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1(react@18.3.1)
|
||||
devDependencies:
|
||||
'@canva/app-ui-kit':
|
||||
specifier: ^5.12.0
|
||||
version: 5.12.0(@types/react@18.3.23)(mobx@6.16.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@canva/asset':
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0(@canva/error@2.2.1)
|
||||
'@canva/design':
|
||||
specifier: ^2.10.0
|
||||
version: 2.10.0(@canva/error@2.2.1)
|
||||
'@types/node':
|
||||
specifier: ^20.14.0
|
||||
version: 20.19.9
|
||||
'@types/react':
|
||||
specifier: ^18.3.3
|
||||
version: 18.3.23
|
||||
'@types/react-dom':
|
||||
specifier: ^18.3.0
|
||||
version: 18.3.7(@types/react@18.3.23)
|
||||
esbuild:
|
||||
specifier: ^0.25.8
|
||||
version: 0.25.8
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
vitest:
|
||||
specifier: ^3.2.6
|
||||
version: 3.2.6(@types/node@20.19.9)(jsdom@26.1.0)(sass@1.60.0)(terser@5.43.1)
|
||||
|
||||
packages/cards:
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
@@ -292,7 +335,7 @@ importers:
|
||||
dependencies:
|
||||
'@hanzo/ai':
|
||||
specifier: 0.1.1
|
||||
version: 0.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(@zxing/library@0.22.0)(plotly.js@3.7.0(mapbox-gl@1.13.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)
|
||||
version: 0.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(@zxing/library@0.22.0)(plotly.js@3.7.0(mapbox-gl@1.13.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.6.0(react@18.3.1))
|
||||
'@hanzo/iam':
|
||||
specifier: 0.13.2
|
||||
version: 0.13.2(react@18.3.1)
|
||||
@@ -440,7 +483,7 @@ importers:
|
||||
version: 1.2.0
|
||||
'@hanzo/ai':
|
||||
specifier: 0.1.1
|
||||
version: 0.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(@zxing/library@0.22.0)(plotly.js@3.7.0(mapbox-gl@1.13.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)
|
||||
version: 0.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(@zxing/library@0.22.0)(plotly.js@3.7.0(mapbox-gl@1.13.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.6.0(react@18.3.1))
|
||||
'@hanzo/iam':
|
||||
specifier: 0.13.2
|
||||
version: 0.13.2(react@18.3.1)
|
||||
@@ -465,7 +508,7 @@ importers:
|
||||
dependencies:
|
||||
'@hanzo/ai':
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(@zxing/library@0.22.0)(plotly.js@3.7.0(mapbox-gl@1.13.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)
|
||||
version: 0.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(@zxing/library@0.22.0)(plotly.js@3.7.0(mapbox-gl@1.13.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.6.0(react@18.3.1))
|
||||
'@hanzo/iam':
|
||||
specifier: ^0.13.2
|
||||
version: 0.13.2(react@18.3.1)
|
||||
@@ -590,7 +633,7 @@ importers:
|
||||
dependencies:
|
||||
'@hanzo/ai':
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(@zxing/library@0.22.0)(plotly.js@3.7.0(mapbox-gl@1.13.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)
|
||||
version: 0.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(@zxing/library@0.22.0)(plotly.js@3.7.0(mapbox-gl@1.13.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.6.0(react@18.3.1))
|
||||
'@hanzo/cards':
|
||||
specifier: workspace:*
|
||||
version: link:../cards
|
||||
@@ -615,7 +658,7 @@ importers:
|
||||
dependencies:
|
||||
'@hanzo/ai':
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(@zxing/library@0.22.0)(plotly.js@3.7.0(mapbox-gl@1.13.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)
|
||||
version: 0.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(@zxing/library@0.22.0)(plotly.js@3.7.0(mapbox-gl@1.13.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.6.0(react@18.3.1))
|
||||
'@hanzo/cards':
|
||||
specifier: workspace:*
|
||||
version: link:../cards
|
||||
@@ -1617,6 +1660,26 @@ packages:
|
||||
'@borewit/text-codec@0.2.2':
|
||||
resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==}
|
||||
|
||||
'@canva/app-ui-kit@5.12.0':
|
||||
resolution: {integrity: sha512-fj0GydEqOrXLYBEBwv95MYunPFqoiPisZHFnD9XLhdvGqK0BAKOlVVbx/Wb6z9vZbQLcjBQcGwYysYHBGWpGmg==}
|
||||
peerDependencies:
|
||||
mobx: ^6.15.1
|
||||
react: ^19.2.3
|
||||
react-dom: ^19.2.3
|
||||
|
||||
'@canva/asset@2.3.0':
|
||||
resolution: {integrity: sha512-dZHywIsZEoJin6fJVd2gFwSFp020otrA3gd6h+XAuCoMam7CjyNfE6FV6uYMSv4I/j56wLj0QHwndVMLJCpnxg==}
|
||||
peerDependencies:
|
||||
'@canva/error': ^2.0.0
|
||||
|
||||
'@canva/design@2.10.0':
|
||||
resolution: {integrity: sha512-LwHA0U0GdS/wD3zn7Ead6iroYWvIVJL3uO2KwXx2J7ULw76vaxJBNZI+uZptr7wwzG+KiGXnrNeQ5fEiAHHriA==}
|
||||
peerDependencies:
|
||||
'@canva/error': ^2.0.0
|
||||
|
||||
'@canva/error@2.2.1':
|
||||
resolution: {integrity: sha512-b5GQMggNaLqtQdN4ENXx2PifFNTO9Ulw8TEwbMZjbPmLS7cAQPfQvXqb0SwPcQ2LQhCIanErrcpOPxQ4s60QNA==}
|
||||
|
||||
'@choojs/findup@0.2.1':
|
||||
resolution: {integrity: sha512-YstAqNb0MCN8PjdLCDfRsBcGVRN41f3vgLvaI0IrIcBp4AqILRSS0DeWNGkicC+f/zRIPJLc+9RURVSepwvfBw==}
|
||||
hasBin: true
|
||||
@@ -2211,9 +2274,30 @@ packages:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@floating-ui/react@0.26.28':
|
||||
resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
|
||||
'@floating-ui/utils@0.2.11':
|
||||
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
|
||||
|
||||
'@formatjs/ecma402-abstract@2.3.6':
|
||||
resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==}
|
||||
|
||||
'@formatjs/fast-memoize@2.2.7':
|
||||
resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==}
|
||||
|
||||
'@formatjs/icu-messageformat-parser@2.11.4':
|
||||
resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==}
|
||||
|
||||
'@formatjs/icu-skeleton-parser@1.8.16':
|
||||
resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==}
|
||||
|
||||
'@formatjs/intl-localematcher@0.6.2':
|
||||
resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==}
|
||||
|
||||
'@google/clasp@2.5.0':
|
||||
resolution: {integrity: sha512-HkZnUP5UibGEYXpk89HR24pFctb9VBlrpjCrU6sjepkQOtpr/Y9rrSP3N7EVrqUDg4/JQmmPQ8Z2ybN4XtdOxg==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
@@ -2810,6 +2894,7 @@ packages:
|
||||
'@lancedb/lancedb@0.21.0':
|
||||
resolution: {integrity: sha512-3c65DfAsTzGb3/Y2WtpJeqPFb2BgoTsYweg3gj9zP6t8CCaKQSeVWEGE8Nowtdj5Jkj9/nmHUoKPv4jCsds2yQ==}
|
||||
engines: {node: '>= 18'}
|
||||
cpu: [x64, arm64]
|
||||
os: [darwin, linux, win32]
|
||||
peerDependencies:
|
||||
apache-arrow: '>=15.0.0 <=18.1.0'
|
||||
@@ -2975,16 +3060,19 @@ packages:
|
||||
'@nut-tree-fork/libnut-darwin@2.7.5':
|
||||
resolution: {integrity: sha512-LbqtPtMPTJUcg4XoPP2jsU1wc8flBcGyKTerKsIfK9cD7nBHROnO0QksbrsbSWEpLym8T8fRtuU7XEY83l6Z2Q==}
|
||||
engines: {node: '>=10.15.3'}
|
||||
cpu: [x64, arm64]
|
||||
os: [darwin, linux, win32]
|
||||
|
||||
'@nut-tree-fork/libnut-linux@2.7.5':
|
||||
resolution: {integrity: sha512-uxaXEcRKnFObAljsoR6tLOBUU1dJ2sctloG6gFgCBGN7+k6Jdv6jZfOuNjd/fpdq2C5WPMm0rtn9EE7h5J3Jcg==}
|
||||
engines: {node: '>=10.15.3'}
|
||||
cpu: [x64, arm64]
|
||||
os: [darwin, linux, win32]
|
||||
|
||||
'@nut-tree-fork/libnut-win32@2.7.5':
|
||||
resolution: {integrity: sha512-yqC87zvmFcDPwFrRU40DYhN0xmEVM3aSkOuyF0IX+y1x+HWSu/i0PNklATpPBhGid3QVb/TOHuVoaraMrUFCNw==}
|
||||
engines: {node: '>=10.15.3'}
|
||||
cpu: [x64, arm64]
|
||||
os: [darwin, linux, win32]
|
||||
|
||||
'@nut-tree-fork/libnut@4.2.6':
|
||||
@@ -2998,6 +3086,7 @@ packages:
|
||||
'@nut-tree-fork/nut-js@4.2.6':
|
||||
resolution: {integrity: sha512-aI/WCX7gE1HFGPH3EZP/UWqpNMM1NMoM/EkXqp7pKMgXFCi8e5+o5p+jd/QOYpmALv9bQg7+s69nI7FONbMqDg==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64, arm64]
|
||||
os: [linux, darwin, win32]
|
||||
|
||||
'@nut-tree-fork/provider-interfaces@4.2.6':
|
||||
@@ -3794,6 +3883,9 @@ packages:
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
'@stylexjs/stylex@0.18.3':
|
||||
resolution: {integrity: sha512-15gDzAJAorOE0yzxaWLNxldW2aqdmCiLG5QcPD8nmVCVqvrWp0Asgv45zk7LtN86WJb/4Ym9eQ6qT5MJW59tWQ==}
|
||||
|
||||
'@supabase/auth-js@2.70.0':
|
||||
resolution: {integrity: sha512-BaAK/tOAZFJtzF1sE3gJ2FwTjLf4ky3PSvcvLGEgEmO4BSBkwWKu8l67rLLIBZPDnCyV7Owk2uPyKHa0kj5QGg==}
|
||||
|
||||
@@ -3982,6 +4074,11 @@ packages:
|
||||
'@types/hast@3.0.4':
|
||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||
|
||||
'@types/hoist-non-react-statics@3.3.7':
|
||||
resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
|
||||
'@types/http-cache-semantics@4.2.0':
|
||||
resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==}
|
||||
|
||||
@@ -4636,6 +4733,9 @@ packages:
|
||||
as-typed@1.3.2:
|
||||
resolution: {integrity: sha512-94ezeKlKB97OJUyMaU7dQUAB+Cmjlgr4T9/cxCoKaLM4F2HAmuIHm3Q5ClGCsX5PvRwCQehCzAa/6foRFXRbqA==}
|
||||
|
||||
asap@2.0.6:
|
||||
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
||||
|
||||
asn1@0.2.6:
|
||||
resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
|
||||
|
||||
@@ -5080,6 +5180,9 @@ packages:
|
||||
clamp@1.0.1:
|
||||
resolution: {integrity: sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==}
|
||||
|
||||
classnames@2.5.1:
|
||||
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
|
||||
|
||||
clean-stack@2.2.0:
|
||||
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -5396,6 +5499,9 @@ packages:
|
||||
css-global-keywords@1.0.1:
|
||||
resolution: {integrity: sha512-X1xgQhkZ9n94WDwntqst5D/FKkmiU0GlJSFZSV3kLvyJ1WC5VeyoXDOuleUD+SIuH9C7W05is++0Woh0CGfKjQ==}
|
||||
|
||||
css-mediaquery@0.1.2:
|
||||
resolution: {integrity: sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==}
|
||||
|
||||
css-select@5.2.2:
|
||||
resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
|
||||
|
||||
@@ -5716,6 +5822,9 @@ packages:
|
||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
dnd-core@7.7.0:
|
||||
resolution: {integrity: sha512-+YqwflWEY1MEAEl2QiEiRaglYkCwIZryyQwximQGuTOm/ns7fS6Lg/i7OCkrtjM10D5FhArf/VUHIL4ZaRBK0g==}
|
||||
|
||||
doctrine@3.0.0:
|
||||
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -6674,6 +6783,9 @@ packages:
|
||||
highlightjs-vue@1.0.0:
|
||||
resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==}
|
||||
|
||||
hoist-non-react-statics@3.3.2:
|
||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||
|
||||
hono@4.12.27:
|
||||
resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
@@ -6856,6 +6968,12 @@ packages:
|
||||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
intl-messageformat@10.7.18:
|
||||
resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==}
|
||||
|
||||
invariant@2.2.4:
|
||||
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
|
||||
|
||||
ip-address@10.1.1:
|
||||
resolution: {integrity: sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw==}
|
||||
engines: {node: '>= 12'}
|
||||
@@ -7857,6 +7975,27 @@ packages:
|
||||
mlly@1.7.4:
|
||||
resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==}
|
||||
|
||||
mobx-react-lite@4.1.1:
|
||||
resolution: {integrity: sha512-iUxiMpsvNraCKXU+yPotsOncNNmyeS2B5DKL+TL6Tar/xm+wwNJAubJmtRSeAoYawdZqwv8Z/+5nPRHeQxTiXg==}
|
||||
peerDependencies:
|
||||
mobx: ^6.9.0
|
||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
|
||||
mobx-utils@6.1.1:
|
||||
resolution: {integrity: sha512-ZR4tOKucWAHOdMjqElRl2BEvrzK7duuDdKmsbEbt2kzgVpuLuoYLiDCjc3QwWQl8CmOlxPgaZQpZ7emwNqPkIg==}
|
||||
peerDependencies:
|
||||
mobx: ^6.0.0
|
||||
|
||||
mobx@6.16.1:
|
||||
resolution: {integrity: sha512-syNcDdX3KT+Jq3je6eGjBhuc24Z68td2VG0zNFqRswaE433D9SNH5VRy/xrGbJsUixfppLLccXhAW9JSf6n+SQ==}
|
||||
|
||||
mocha@11.7.1:
|
||||
resolution: {integrity: sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -8074,6 +8213,9 @@ packages:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
normalize-scroll-left@0.2.1:
|
||||
resolution: {integrity: sha512-YanMJCtsykxVQuWiwQR531bbyPtMt/jpUKy2XcVVe/oHiDgYme0NmuEZfceLeZhFbrQtO/GS/9KvWbSfDGRblA==}
|
||||
|
||||
normalize-svg-path@0.1.0:
|
||||
resolution: {integrity: sha512-1/kmYej2iedi5+ROxkRESL/pI02pkg0OBnaR4hJkSIX6+ORzepwbuUXfrdZaPjysTsJInj0Rj5NuX027+dMBvA==}
|
||||
|
||||
@@ -8688,6 +8830,15 @@ packages:
|
||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||
hasBin: true
|
||||
|
||||
react-dnd-html5-backend@7.7.0:
|
||||
resolution: {integrity: sha512-JgKmWOxqorFyfGPeWV+SAPhVGFe6+LrOR24jETE9rrYZU5hCppzwK9ujjS508kWibeDvbfEgi9j5qC2wB1/MoQ==}
|
||||
|
||||
react-dnd@7.7.0:
|
||||
resolution: {integrity: sha512-anpJDKMgdXE6kXvtBFmIAe1fuaexpVy7meUyNjiTfCnjQ1mRvnttGTVvcW9fMKijsUQYadiyvzd3BRxteFuVXg==}
|
||||
peerDependencies:
|
||||
react: '>= 16.8'
|
||||
react-dom: '>= 16.8'
|
||||
|
||||
react-dom@18.3.1:
|
||||
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
||||
peerDependencies:
|
||||
@@ -8902,6 +9053,9 @@ packages:
|
||||
redeyed@2.1.1:
|
||||
resolution: {integrity: sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==}
|
||||
|
||||
redux@4.2.1:
|
||||
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
|
||||
|
||||
reflect-metadata@0.2.2:
|
||||
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
||||
|
||||
@@ -8966,6 +9120,9 @@ packages:
|
||||
requires-port@1.0.0:
|
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||
|
||||
resize-observer-polyfill@1.5.1:
|
||||
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||
|
||||
resolve-alpn@1.2.1:
|
||||
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
|
||||
|
||||
@@ -9136,6 +9293,9 @@ packages:
|
||||
scheduler@0.23.2:
|
||||
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
|
||||
|
||||
seedrandom@3.0.5:
|
||||
resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==}
|
||||
|
||||
select-hose@2.0.0:
|
||||
resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==}
|
||||
|
||||
@@ -9203,6 +9363,9 @@ packages:
|
||||
shallow-copy@0.0.1:
|
||||
resolution: {integrity: sha512-b6i4ZpVuUxB9h5gfCxPiusKYkqTMOjEbBs4wMaFbkfia4yFv92UKZ6Df8WXcKbn08JNL/abvg3FnMAOfakDvUw==}
|
||||
|
||||
shallowequal@1.1.0:
|
||||
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
|
||||
|
||||
sharp@0.32.6:
|
||||
resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==}
|
||||
engines: {node: '>=14.15.0'}
|
||||
@@ -9286,6 +9449,9 @@ packages:
|
||||
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
|
||||
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
|
||||
|
||||
smoothscroll-polyfill@0.4.4:
|
||||
resolution: {integrity: sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg==}
|
||||
|
||||
socks-proxy-agent@8.0.5:
|
||||
resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==}
|
||||
engines: {node: '>= 14'}
|
||||
@@ -9496,6 +9662,9 @@ packages:
|
||||
resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
styleq@0.2.1:
|
||||
resolution: {integrity: sha512-L0TR0NQb+X4/ktDEKmjWyp27gla+LUYi/by5k5SjKXf6/pvZP7wbwEB5J+tqxdFVPgzbsuz+d4RTScO/QZquBw==}
|
||||
|
||||
sucrase@3.35.0:
|
||||
resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
@@ -9547,6 +9716,9 @@ packages:
|
||||
resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
|
||||
tabbable@6.5.0:
|
||||
resolution: {integrity: sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==}
|
||||
|
||||
table-layout@4.1.1:
|
||||
resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==}
|
||||
engines: {node: '>=12.17'}
|
||||
@@ -10030,6 +10202,11 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
use-sync-external-store@1.6.0:
|
||||
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
utif2@4.1.0:
|
||||
resolution: {integrity: sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==}
|
||||
|
||||
@@ -11474,6 +11651,39 @@ snapshots:
|
||||
|
||||
'@borewit/text-codec@0.2.2': {}
|
||||
|
||||
'@canva/app-ui-kit@5.12.0(@types/react@18.3.23)(mobx@6.16.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@stylexjs/stylex': 0.18.3
|
||||
classnames: 2.5.1
|
||||
intl-messageformat: 10.7.18
|
||||
mobx: 6.16.1
|
||||
mobx-react-lite: 4.1.1(mobx@6.16.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
mobx-utils: 6.1.1(mobx@6.16.1)
|
||||
normalize-scroll-left: 0.2.1
|
||||
react: 18.3.1
|
||||
react-dnd: 7.7.0(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react-dnd-html5-backend: 7.7.0
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
resize-observer-polyfill: 1.5.1
|
||||
seedrandom: 3.0.5
|
||||
smoothscroll-polyfill: 0.4.4
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- react-native
|
||||
|
||||
'@canva/asset@2.3.0(@canva/error@2.2.1)':
|
||||
dependencies:
|
||||
'@canva/error': 2.2.1
|
||||
|
||||
'@canva/design@2.10.0(@canva/error@2.2.1)':
|
||||
dependencies:
|
||||
'@canva/error': 2.2.1
|
||||
|
||||
'@canva/error@2.2.1': {}
|
||||
|
||||
'@choojs/findup@0.2.1':
|
||||
dependencies:
|
||||
commander: 2.20.3
|
||||
@@ -12023,8 +12233,42 @@ snapshots:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
'@floating-ui/react@0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@floating-ui/react-dom': 2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@floating-ui/utils': 0.2.11
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
tabbable: 6.5.0
|
||||
|
||||
'@floating-ui/utils@0.2.11': {}
|
||||
|
||||
'@formatjs/ecma402-abstract@2.3.6':
|
||||
dependencies:
|
||||
'@formatjs/fast-memoize': 2.2.7
|
||||
'@formatjs/intl-localematcher': 0.6.2
|
||||
decimal.js: 10.6.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/fast-memoize@2.2.7':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/icu-messageformat-parser@2.11.4':
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.6
|
||||
'@formatjs/icu-skeleton-parser': 1.8.16
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/icu-skeleton-parser@1.8.16':
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.6
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/intl-localematcher@0.6.2':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@google/clasp@2.5.0(@types/node@22.7.5)(encoding@0.1.13)':
|
||||
dependencies:
|
||||
'@sindresorhus/is': 4.6.0
|
||||
@@ -12077,7 +12321,7 @@ snapshots:
|
||||
protobufjs: 7.6.3
|
||||
yargs: 17.7.2
|
||||
|
||||
'@hanzo/ai@0.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(@zxing/library@0.22.0)(plotly.js@3.7.0(mapbox-gl@1.13.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)':
|
||||
'@hanzo/ai@0.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(@zxing/library@0.22.0)(plotly.js@3.7.0(mapbox-gl@1.13.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.6.0(react@18.3.1))':
|
||||
dependencies:
|
||||
'@hookform/resolvers': 3.10.0(react-hook-form@7.80.0(react@18.3.1))
|
||||
'@radix-ui/react-alert-dialog': 1.1.18(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -12125,7 +12369,7 @@ snapshots:
|
||||
ts-deepmerge: 7.0.3
|
||||
yjs: 13.6.31
|
||||
zod: 3.25.76
|
||||
zustand: 5.0.14(@types/react@18.3.23)(immer@11.1.11)(react@18.3.1)
|
||||
zustand: 5.0.14(@types/react@18.3.23)(immer@11.1.11)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1))
|
||||
transitivePeerDependencies:
|
||||
- '@emotion/is-prop-valid'
|
||||
- '@types/react'
|
||||
@@ -13979,6 +14223,12 @@ snapshots:
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@stylexjs/stylex@0.18.3':
|
||||
dependencies:
|
||||
css-mediaquery: 0.1.2
|
||||
invariant: 2.2.4
|
||||
styleq: 0.2.1
|
||||
|
||||
'@supabase/auth-js@2.70.0':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
@@ -14229,6 +14479,11 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
'@types/hoist-non-react-statics@3.3.7(@types/react@18.3.23)':
|
||||
dependencies:
|
||||
'@types/react': 18.3.23
|
||||
hoist-non-react-statics: 3.3.2
|
||||
|
||||
'@types/http-cache-semantics@4.2.0': {}
|
||||
|
||||
'@types/http-errors@2.0.5': {}
|
||||
@@ -15125,6 +15380,8 @@ snapshots:
|
||||
|
||||
as-typed@1.3.2: {}
|
||||
|
||||
asap@2.0.6: {}
|
||||
|
||||
asn1@0.2.6:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
@@ -15730,6 +15987,8 @@ snapshots:
|
||||
|
||||
clamp@1.0.1: {}
|
||||
|
||||
classnames@2.5.1: {}
|
||||
|
||||
clean-stack@2.2.0: {}
|
||||
|
||||
clean-stack@3.0.1:
|
||||
@@ -16056,6 +16315,8 @@ snapshots:
|
||||
|
||||
css-global-keywords@1.0.1: {}
|
||||
|
||||
css-mediaquery@0.1.2: {}
|
||||
|
||||
css-select@5.2.2:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
@@ -16307,6 +16568,12 @@ snapshots:
|
||||
dependencies:
|
||||
path-type: 4.0.0
|
||||
|
||||
dnd-core@7.7.0:
|
||||
dependencies:
|
||||
asap: 2.0.6
|
||||
invariant: 2.2.4
|
||||
redux: 4.2.1
|
||||
|
||||
doctrine@3.0.0:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
@@ -17570,6 +17837,10 @@ snapshots:
|
||||
|
||||
highlightjs-vue@1.0.0: {}
|
||||
|
||||
hoist-non-react-statics@3.3.2:
|
||||
dependencies:
|
||||
react-is: 16.13.1
|
||||
|
||||
hono@4.12.27: {}
|
||||
|
||||
hosted-git-info@4.1.0:
|
||||
@@ -17814,6 +18085,17 @@ snapshots:
|
||||
|
||||
internmap@2.0.3: {}
|
||||
|
||||
intl-messageformat@10.7.18:
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.6
|
||||
'@formatjs/fast-memoize': 2.2.7
|
||||
'@formatjs/icu-messageformat-parser': 2.11.4
|
||||
tslib: 2.8.1
|
||||
|
||||
invariant@2.2.4:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
ip-address@10.1.1: {}
|
||||
|
||||
ip-address@10.2.0: {}
|
||||
@@ -19059,6 +19341,20 @@ snapshots:
|
||||
pkg-types: 1.3.1
|
||||
ufo: 1.6.1
|
||||
|
||||
mobx-react-lite@4.1.1(mobx@6.16.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
mobx: 6.16.1
|
||||
react: 18.3.1
|
||||
use-sync-external-store: 1.6.0(react@18.3.1)
|
||||
optionalDependencies:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
mobx-utils@6.1.1(mobx@6.16.1):
|
||||
dependencies:
|
||||
mobx: 6.16.1
|
||||
|
||||
mobx@6.16.1: {}
|
||||
|
||||
mocha@11.7.1:
|
||||
dependencies:
|
||||
browser-stdout: 1.3.1
|
||||
@@ -19260,6 +19556,8 @@ snapshots:
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
normalize-scroll-left@0.2.1: {}
|
||||
|
||||
normalize-svg-path@0.1.0: {}
|
||||
|
||||
normalize-svg-path@1.1.0:
|
||||
@@ -19951,6 +20249,22 @@ snapshots:
|
||||
minimist: 1.2.8
|
||||
strip-json-comments: 2.0.1
|
||||
|
||||
react-dnd-html5-backend@7.7.0:
|
||||
dependencies:
|
||||
dnd-core: 7.7.0
|
||||
|
||||
react-dnd@7.7.0(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@types/hoist-non-react-statics': 3.3.7(@types/react@18.3.23)
|
||||
dnd-core: 7.7.0
|
||||
hoist-non-react-statics: 3.3.2
|
||||
invariant: 2.2.4
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
shallowequal: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
react-dom@18.3.1(react@18.3.1):
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
@@ -20178,6 +20492,10 @@ snapshots:
|
||||
dependencies:
|
||||
esprima: 4.0.1
|
||||
|
||||
redux@4.2.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.7
|
||||
|
||||
reflect-metadata@0.2.2: {}
|
||||
|
||||
refractor@5.0.0:
|
||||
@@ -20277,6 +20595,8 @@ snapshots:
|
||||
|
||||
requires-port@1.0.0: {}
|
||||
|
||||
resize-observer-polyfill@1.5.1: {}
|
||||
|
||||
resolve-alpn@1.2.1: {}
|
||||
|
||||
resolve-cwd@3.0.0:
|
||||
@@ -20533,6 +20853,8 @@ snapshots:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
seedrandom@3.0.5: {}
|
||||
|
||||
select-hose@2.0.0: {}
|
||||
|
||||
semver@5.7.2: {}
|
||||
@@ -20636,6 +20958,8 @@ snapshots:
|
||||
|
||||
shallow-copy@0.0.1: {}
|
||||
|
||||
shallowequal@1.1.0: {}
|
||||
|
||||
sharp@0.32.6:
|
||||
dependencies:
|
||||
color: 4.2.3
|
||||
@@ -20771,6 +21095,8 @@ snapshots:
|
||||
|
||||
smart-buffer@4.2.0: {}
|
||||
|
||||
smoothscroll-polyfill@0.4.4: {}
|
||||
|
||||
socks-proxy-agent@8.0.5:
|
||||
dependencies:
|
||||
agent-base: 7.1.3
|
||||
@@ -20996,6 +21322,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@tokenizer/token': 0.3.0
|
||||
|
||||
styleq@0.2.1: {}
|
||||
|
||||
sucrase@3.35.0:
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.12
|
||||
@@ -21058,6 +21386,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@pkgr/core': 0.2.9
|
||||
|
||||
tabbable@6.5.0: {}
|
||||
|
||||
table-layout@4.1.1:
|
||||
dependencies:
|
||||
array-back: 6.2.2
|
||||
@@ -21505,6 +21835,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.23
|
||||
|
||||
use-sync-external-store@1.6.0(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
utif2@4.1.0:
|
||||
dependencies:
|
||||
pako: 1.0.11
|
||||
@@ -22257,8 +22591,9 @@ snapshots:
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
||||
zustand@5.0.14(@types/react@18.3.23)(immer@11.1.11)(react@18.3.1):
|
||||
zustand@5.0.14(@types/react@18.3.23)(immer@11.1.11)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)):
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.23
|
||||
immer: 11.1.11
|
||||
react: 18.3.1
|
||||
use-sync-external-store: 1.6.0(react@18.3.1)
|
||||
|
||||
@@ -3,6 +3,7 @@ packages:
|
||||
- 'packages/aci'
|
||||
- 'packages/auth'
|
||||
- 'packages/browser'
|
||||
- 'packages/canva'
|
||||
- 'packages/cards'
|
||||
- 'packages/clio'
|
||||
- 'packages/docusign'
|
||||
|
||||
Reference in New Issue
Block a user