@notis_ai/cli 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -18
- package/config/notis_app_boundary_rules.json +50 -0
- package/dist/scaffolds/notis-database/app/globals.css +44 -0
- package/dist/scaffolds/notis-database/app/page.tsx +1091 -0
- package/dist/scaffolds/notis-database/components/ui/table.tsx +120 -0
- package/dist/scaffolds/notis-database/index.html +12 -0
- package/dist/scaffolds/notis-database/lib/types.ts +134 -0
- package/dist/scaffolds/notis-database/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-database/notis.config.ts +27 -0
- package/dist/scaffolds/notis-database/package.json +31 -0
- package/dist/scaffolds/notis-database/src/dev-main.tsx +23 -0
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +557 -0
- package/dist/scaffolds/notis-database/tailwind.config.ts +59 -0
- package/dist/scaffolds/{notes → notis-database}/tsconfig.json +2 -1
- package/dist/scaffolds/notis-database/vite.config.ts +22 -0
- package/dist/scaffolds/notis-notes/app/layout.tsx +6 -0
- package/dist/scaffolds/{notes → notis-notes}/app/page.tsx +783 -80
- package/dist/scaffolds/notis-notes/components/ui/badge.tsx +28 -0
- package/dist/scaffolds/notis-notes/components/ui/button.tsx +53 -0
- package/dist/scaffolds/notis-notes/components/ui/card.tsx +56 -0
- package/dist/scaffolds/notis-notes/components.json +20 -0
- package/dist/scaffolds/notis-notes/lib/utils.ts +6 -0
- package/dist/scaffolds/notis-notes/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/{notes → notis-notes}/notis.config.ts +12 -7
- package/dist/scaffolds/{notes → notis-notes}/package.json +2 -2
- package/dist/scaffolds/notis-notes/postcss.config.mjs +8 -0
- package/dist/scaffolds/notis-notes/tsconfig.json +23 -0
- package/dist/scaffolds/notis-random/README.md +33 -0
- package/dist/scaffolds/notis-random/app/globals.css +11 -0
- package/dist/scaffolds/notis-random/app/history/page.tsx +66 -0
- package/dist/scaffolds/notis-random/app/layout.tsx +7 -0
- package/dist/scaffolds/notis-random/app/page.tsx +222 -0
- package/dist/scaffolds/notis-random/index.html +12 -0
- package/dist/scaffolds/notis-random/lib/notis-tools.ts +109 -0
- package/dist/scaffolds/notis-random/lib/rng.ts +42 -0
- package/dist/scaffolds/notis-random/lib/roll-record.ts +102 -0
- package/dist/scaffolds/notis-random/lib/utils.ts +25 -0
- package/dist/scaffolds/notis-random/metadata/cover.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/notis.config.ts +43 -0
- package/dist/scaffolds/notis-random/package.json +32 -0
- package/dist/scaffolds/notis-random/postcss.config.mjs +6 -0
- package/dist/scaffolds/notis-random/src/dev-main.tsx +70 -0
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +128 -0
- package/dist/scaffolds/notis-random/tailwind.config.ts +43 -0
- package/dist/scaffolds/notis-random/tsconfig.json +23 -0
- package/dist/scaffolds/notis-random/vite.config.ts +11 -0
- package/dist/scaffolds.json +27 -4
- package/package.json +3 -1
- package/skills/notis-apps/SKILL.md +2 -2
- package/skills/notis-cli/SKILL.md +18 -16
- package/skills/notis-query/cli.md +9 -10
- package/src/cli.js +30 -2
- package/src/command-specs/apps.js +42 -15
- package/src/command-specs/helpers.js +73 -45
- package/src/command-specs/meta.js +7 -7
- package/src/command-specs/tools.js +196 -83
- package/src/runtime/app-boundary-validator.js +65 -14
- package/src/runtime/profiles.js +10 -4
- package/src/runtime/transport.js +122 -37
- package/template/app/page.tsx +1 -1
- package/template/notis.config.ts +1 -1
- package/template/packages/sdk/src/hooks/useTool.ts +1 -1
- /package/dist/scaffolds/{notes → notis-database}/app/layout.tsx +0 -0
- /package/dist/scaffolds/{notes → notis-database}/components/ui/badge.tsx +0 -0
- /package/dist/scaffolds/{notes → notis-database}/components/ui/button.tsx +0 -0
- /package/dist/scaffolds/{notes → notis-database}/components/ui/card.tsx +0 -0
- /package/dist/scaffolds/{notes → notis-database}/components.json +0 -0
- /package/dist/scaffolds/{notes → notis-database}/lib/utils.ts +0 -0
- /package/dist/scaffolds/{notes → notis-database}/postcss.config.mjs +0 -0
- /package/dist/scaffolds/{notes → notis-notes}/app/globals.css +0 -0
- /package/dist/scaffolds/{notes → notis-notes}/tailwind.config.ts +0 -0
- /package/dist/scaffolds/{notes → notis-notes}/vite.config.ts +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
4
|
+
import { useTool } from '@notis/sdk';
|
|
5
|
+
import type { RollDocument } from '@/lib/roll-record';
|
|
6
|
+
import type { Roll } from '@/lib/rng';
|
|
7
|
+
import { formatNumber } from '@/lib/utils';
|
|
8
|
+
|
|
9
|
+
type QueryRollsArgs = {
|
|
10
|
+
database_slug: 'rolls';
|
|
11
|
+
query?: {
|
|
12
|
+
page_size?: number;
|
|
13
|
+
};
|
|
14
|
+
offset?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type QueryRollsResult = {
|
|
18
|
+
documents?: RollDocument[];
|
|
19
|
+
message?: string;
|
|
20
|
+
error?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type UpsertRollArgs = {
|
|
24
|
+
title: string;
|
|
25
|
+
Value: number;
|
|
26
|
+
Mode: Roll['mode'];
|
|
27
|
+
Min: number;
|
|
28
|
+
Max: number;
|
|
29
|
+
'Rolled At': string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type UpsertRollResult = {
|
|
33
|
+
status?: string;
|
|
34
|
+
document?: RollDocument;
|
|
35
|
+
message?: string;
|
|
36
|
+
error?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function useRollDocuments(pageSize?: number) {
|
|
40
|
+
const queryRolls = useTool<QueryRollsArgs, QueryRollsResult>('LOCAL_NOTIS_DATABASE_QUERY');
|
|
41
|
+
const [documents, setDocuments] = useState<RollDocument[]>([]);
|
|
42
|
+
const [error, setError] = useState<Error | null>(null);
|
|
43
|
+
const [fetchKey, setFetchKey] = useState(0);
|
|
44
|
+
|
|
45
|
+
const refetch = useCallback(() => {
|
|
46
|
+
setFetchKey((key) => key + 1);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
let cancelled = false;
|
|
51
|
+
setError(null);
|
|
52
|
+
|
|
53
|
+
queryRolls
|
|
54
|
+
.call({ database_slug: 'rolls', query: { page_size: pageSize } })
|
|
55
|
+
.then((result) => {
|
|
56
|
+
if (cancelled) return;
|
|
57
|
+
const message = result.error ?? result.message;
|
|
58
|
+
if (message && !result.documents) {
|
|
59
|
+
throw new Error(message);
|
|
60
|
+
}
|
|
61
|
+
setDocuments(result.documents ?? []);
|
|
62
|
+
})
|
|
63
|
+
.catch((err) => {
|
|
64
|
+
if (cancelled) return;
|
|
65
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
66
|
+
setDocuments([]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return () => {
|
|
70
|
+
cancelled = true;
|
|
71
|
+
};
|
|
72
|
+
}, [fetchKey, pageSize, queryRolls.call]);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
documents,
|
|
76
|
+
loading: queryRolls.loading,
|
|
77
|
+
error: queryRolls.error ?? error,
|
|
78
|
+
refetch,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function usePersistRoll() {
|
|
83
|
+
const upsertRoll = useTool<UpsertRollArgs, UpsertRollResult>('LOCAL_NOTIS_DATABASE_UPSERT_ROLLS');
|
|
84
|
+
|
|
85
|
+
const persist = useCallback(
|
|
86
|
+
async (roll: Roll) => {
|
|
87
|
+
const result = await upsertRoll.call({
|
|
88
|
+
title: formatNumber(roll.value),
|
|
89
|
+
Value: roll.value,
|
|
90
|
+
Mode: roll.mode,
|
|
91
|
+
Min: roll.min,
|
|
92
|
+
Max: roll.max,
|
|
93
|
+
'Rolled At': roll.at,
|
|
94
|
+
});
|
|
95
|
+
const message = result.error ?? result.message;
|
|
96
|
+
if (message && result.status === 'error') {
|
|
97
|
+
throw new Error(message);
|
|
98
|
+
}
|
|
99
|
+
return result.document ?? null;
|
|
100
|
+
},
|
|
101
|
+
[upsertRoll.call],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
persist,
|
|
106
|
+
loading: upsertRoll.loading,
|
|
107
|
+
error: upsertRoll.error,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type Mode = 'integer' | 'decimal' | 'dice';
|
|
2
|
+
|
|
3
|
+
export interface Roll {
|
|
4
|
+
value: number;
|
|
5
|
+
mode: Mode;
|
|
6
|
+
min: number;
|
|
7
|
+
max: number;
|
|
8
|
+
at: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function cryptoRandom(): number {
|
|
12
|
+
const array = new Uint32Array(1);
|
|
13
|
+
crypto.getRandomValues(array);
|
|
14
|
+
return (array[0] ?? 0) / 0xffffffff;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function roll(min: number, max: number, mode: Mode): Roll {
|
|
18
|
+
const lo = Math.min(min, max);
|
|
19
|
+
const hi = Math.max(min, max);
|
|
20
|
+
let value: number;
|
|
21
|
+
if (mode === 'integer' || mode === 'dice') {
|
|
22
|
+
value = Math.floor(cryptoRandom() * (hi - lo + 1)) + lo;
|
|
23
|
+
} else {
|
|
24
|
+
value = cryptoRandom() * (hi - lo) + lo;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
value,
|
|
28
|
+
mode,
|
|
29
|
+
min: lo,
|
|
30
|
+
max: hi,
|
|
31
|
+
at: new Date().toISOString(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const DICE_FACES: Record<number, string> = {
|
|
36
|
+
1: 'dice-1',
|
|
37
|
+
2: 'dice-2',
|
|
38
|
+
3: 'dice-3',
|
|
39
|
+
4: 'dice-4',
|
|
40
|
+
5: 'dice-5',
|
|
41
|
+
6: 'dice-6',
|
|
42
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Mode } from '@/lib/rng';
|
|
2
|
+
|
|
3
|
+
export interface RollDocument {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
properties?: Record<string, unknown>;
|
|
7
|
+
createdAt?: string | null;
|
|
8
|
+
created_at?: string | null;
|
|
9
|
+
created_time?: string | null;
|
|
10
|
+
lastEditedTime?: string | null;
|
|
11
|
+
last_edited_time?: string | null;
|
|
12
|
+
updated_at?: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readString(value: unknown): string | null {
|
|
16
|
+
if (typeof value === 'string') {
|
|
17
|
+
const trimmed = value.trim();
|
|
18
|
+
return trimmed.length ? trimmed : null;
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
21
|
+
return String(value);
|
|
22
|
+
}
|
|
23
|
+
if (value && typeof value === 'object') {
|
|
24
|
+
const record = value as Record<string, unknown>;
|
|
25
|
+
if (typeof record.name === 'string') {
|
|
26
|
+
return record.name;
|
|
27
|
+
}
|
|
28
|
+
if (typeof record.start === 'string') {
|
|
29
|
+
return record.start;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readNumber(value: unknown): number | null {
|
|
36
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
const stringValue = readString(value);
|
|
40
|
+
if (!stringValue) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const parsed = Number(stringValue);
|
|
44
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readMode(value: unknown): Mode {
|
|
48
|
+
const raw = readString(value)?.toLowerCase();
|
|
49
|
+
if (raw === 'integer' || raw === 'decimal' || raw === 'dice') {
|
|
50
|
+
return raw;
|
|
51
|
+
}
|
|
52
|
+
return 'integer';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readTimestamp(doc: RollDocument): string {
|
|
56
|
+
return (
|
|
57
|
+
readString(doc.properties?.['Rolled At']) ||
|
|
58
|
+
doc.createdAt ||
|
|
59
|
+
doc.created_at ||
|
|
60
|
+
doc.created_time ||
|
|
61
|
+
doc.lastEditedTime ||
|
|
62
|
+
doc.last_edited_time ||
|
|
63
|
+
doc.updated_at ||
|
|
64
|
+
''
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sortTimestamp(value: string): number {
|
|
69
|
+
if (!value) return 0;
|
|
70
|
+
const parsed = Date.parse(value);
|
|
71
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface RollRecord {
|
|
75
|
+
id: string;
|
|
76
|
+
value: number;
|
|
77
|
+
mode: Mode;
|
|
78
|
+
min: number | null;
|
|
79
|
+
max: number | null;
|
|
80
|
+
at: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function normalizeRollRecord(doc: RollDocument): RollRecord {
|
|
84
|
+
const value =
|
|
85
|
+
readNumber(doc.properties?.['Value']) ??
|
|
86
|
+
readNumber(doc.properties?.['Name']) ??
|
|
87
|
+
readNumber(doc.title) ??
|
|
88
|
+
0;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
id: doc.id,
|
|
92
|
+
value,
|
|
93
|
+
mode: readMode(doc.properties?.['Mode']),
|
|
94
|
+
min: readNumber(doc.properties?.['Min']),
|
|
95
|
+
max: readNumber(doc.properties?.['Max']),
|
|
96
|
+
at: readTimestamp(doc),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function sortRollRecordsDesc(a: RollRecord, b: RollRecord): number {
|
|
101
|
+
return sortTimestamp(b.at) - sortTimestamp(a.at);
|
|
102
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs: ClassValue[]): string {
|
|
5
|
+
return twMerge(clsx(inputs));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatNumber(value: number): string {
|
|
9
|
+
if (Number.isInteger(value)) return value.toString();
|
|
10
|
+
return value.toFixed(4).replace(/0+$/, '').replace(/\.$/, '');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function relativeTime(iso: string | null | undefined): string {
|
|
14
|
+
if (!iso) return '';
|
|
15
|
+
const then = new Date(iso).getTime();
|
|
16
|
+
if (Number.isNaN(then)) return '';
|
|
17
|
+
const diffSeconds = Math.round((Date.now() - then) / 1000);
|
|
18
|
+
if (diffSeconds < 5) return 'just now';
|
|
19
|
+
if (diffSeconds < 60) return `${diffSeconds}s ago`;
|
|
20
|
+
const diffMinutes = Math.round(diffSeconds / 60);
|
|
21
|
+
if (diffMinutes < 60) return `${diffMinutes}m ago`;
|
|
22
|
+
const diffHours = Math.round(diffMinutes / 60);
|
|
23
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
24
|
+
return new Date(iso).toLocaleDateString();
|
|
25
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { defineNotisApp } from '@notis/sdk/config';
|
|
2
|
+
|
|
3
|
+
export default defineNotisApp({
|
|
4
|
+
name: 'notis-random',
|
|
5
|
+
title: 'Random Number Generator',
|
|
6
|
+
description: 'Generate random numbers with configurable bounds, and keep a history of everything you rolled.',
|
|
7
|
+
icon: 'phosphor:dice-five',
|
|
8
|
+
author: { name: 'Notis' },
|
|
9
|
+
categories: ['Personal'],
|
|
10
|
+
tagline: 'Roll dice, keep history.',
|
|
11
|
+
versionNotes: 'Initial scaffold release.',
|
|
12
|
+
|
|
13
|
+
databases: ['rolls'],
|
|
14
|
+
|
|
15
|
+
routes: [
|
|
16
|
+
{
|
|
17
|
+
path: '/',
|
|
18
|
+
slug: 'home',
|
|
19
|
+
name: 'Generator',
|
|
20
|
+
icon: 'phosphor:sparkle',
|
|
21
|
+
default: true,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
path: '/history',
|
|
25
|
+
slug: 'history',
|
|
26
|
+
name: 'History',
|
|
27
|
+
icon: 'phosphor:clock-counter-clockwise',
|
|
28
|
+
collection: {
|
|
29
|
+
database: 'rolls',
|
|
30
|
+
titleProperty: 'Value',
|
|
31
|
+
sidebar: {
|
|
32
|
+
mode: 'flat-list',
|
|
33
|
+
allowCreate: false,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
|
|
39
|
+
tools: [
|
|
40
|
+
'LOCAL_NOTIS_DATABASE_QUERY',
|
|
41
|
+
'LOCAL_NOTIS_DATABASE_UPSERT_ROLLS',
|
|
42
|
+
],
|
|
43
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@notis-apps/notis-random",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"notisAppVersion": "0.1.0",
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"dev": "vite",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@notis/sdk": "file:../../packages/sdk",
|
|
14
|
+
"@phosphor-icons/react": "^2.1.10",
|
|
15
|
+
"clsx": "^2.1.1",
|
|
16
|
+
"react": "^19.0.0",
|
|
17
|
+
"react-dom": "^19.0.0",
|
|
18
|
+
"tailwind-merge": "^2.6.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.0.0",
|
|
22
|
+
"@types/react": "^19.0.0",
|
|
23
|
+
"@types/react-dom": "^19.0.0",
|
|
24
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
25
|
+
"autoprefixer": "^10.4.0",
|
|
26
|
+
"postcss": "^8.4.0",
|
|
27
|
+
"tailwindcss": "^3.4.0",
|
|
28
|
+
"tailwindcss-animate": "^1.0.7",
|
|
29
|
+
"typescript": "^5.5.0",
|
|
30
|
+
"vite": "^6.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Dev entrypoint: mounts the app pages with a tiny hash-router and the mock
|
|
2
|
+
// runtime. Not used in production — `vite build` uses .notis/_entry.tsx.
|
|
3
|
+
import { StrictMode, useEffect, useState } from 'react';
|
|
4
|
+
import { createRoot } from 'react-dom/client';
|
|
5
|
+
import { NotisProvider } from '@notis/sdk';
|
|
6
|
+
import AppShell from '../app/layout';
|
|
7
|
+
import HomePage from '../app/page';
|
|
8
|
+
import HistoryPage from '../app/history/page';
|
|
9
|
+
import { installMockRuntime, setMockRoute } from './mock-runtime';
|
|
10
|
+
|
|
11
|
+
const runtime = installMockRuntime(hashRoute());
|
|
12
|
+
|
|
13
|
+
function hashRoute(): 'home' | 'history' {
|
|
14
|
+
return window.location.hash === '#/history' ? 'history' : 'home';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function DevApp() {
|
|
18
|
+
const [route, setRoute] = useState<'home' | 'history'>(hashRoute());
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const onChange = () => {
|
|
22
|
+
const next = hashRoute();
|
|
23
|
+
setRoute(next);
|
|
24
|
+
setMockRoute(next);
|
|
25
|
+
};
|
|
26
|
+
window.addEventListener('hashchange', onChange);
|
|
27
|
+
return () => window.removeEventListener('hashchange', onChange);
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
const Page = route === 'history' ? HistoryPage : HomePage;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<AppShell>
|
|
34
|
+
<div className="mx-auto max-w-4xl px-4 pt-4">
|
|
35
|
+
<nav className="flex items-center gap-2 rounded-full border border-border bg-card p-1 text-sm w-fit">
|
|
36
|
+
<a
|
|
37
|
+
href="#/"
|
|
38
|
+
className={
|
|
39
|
+
(route === 'home' ? 'bg-accent text-accent-foreground ' : 'text-muted-foreground hover:text-foreground ') +
|
|
40
|
+
'rounded-full px-3 py-1 transition'
|
|
41
|
+
}
|
|
42
|
+
>
|
|
43
|
+
Generator
|
|
44
|
+
</a>
|
|
45
|
+
<a
|
|
46
|
+
href="#/history"
|
|
47
|
+
className={
|
|
48
|
+
(route === 'history' ? 'bg-accent text-accent-foreground ' : 'text-muted-foreground hover:text-foreground ') +
|
|
49
|
+
'rounded-full px-3 py-1 transition'
|
|
50
|
+
}
|
|
51
|
+
>
|
|
52
|
+
History
|
|
53
|
+
</a>
|
|
54
|
+
<span className="pl-2 pr-3 text-[10px] uppercase tracking-wide text-muted-foreground">dev preview</span>
|
|
55
|
+
</nav>
|
|
56
|
+
</div>
|
|
57
|
+
<Page />
|
|
58
|
+
</AppShell>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const root = document.getElementById('root');
|
|
63
|
+
if (!root) throw new Error('#root element missing from index.html');
|
|
64
|
+
createRoot(root).render(
|
|
65
|
+
<StrictMode>
|
|
66
|
+
<NotisProvider runtime={runtime}>
|
|
67
|
+
<DevApp />
|
|
68
|
+
</NotisProvider>
|
|
69
|
+
</StrictMode>,
|
|
70
|
+
);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { NotisRuntime } from '@notis/sdk';
|
|
2
|
+
|
|
3
|
+
interface MockDoc {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
properties: Record<string, unknown>;
|
|
7
|
+
icon?: string | null;
|
|
8
|
+
databaseSlug?: string;
|
|
9
|
+
createdAt?: string | null;
|
|
10
|
+
lastEditedTime?: string | null;
|
|
11
|
+
contentBlocknote?: Array<Record<string, unknown>> | null;
|
|
12
|
+
contentMarkdown?: string | null;
|
|
13
|
+
plainText?: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const STATE: Record<string, MockDoc[]> = { rolls: [] };
|
|
17
|
+
|
|
18
|
+
declare global {
|
|
19
|
+
interface Window {
|
|
20
|
+
__NOTIS_RANDOM_RUNTIME__?: NotisRuntime;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function installMockRuntime(initialRoute: 'home' | 'history' = 'home'): NotisRuntime {
|
|
25
|
+
const runtime: NotisRuntime = {
|
|
26
|
+
app: {
|
|
27
|
+
id: 'dev-notis-random',
|
|
28
|
+
name: 'Random Number Generator',
|
|
29
|
+
icon: 'phosphor:dice-five',
|
|
30
|
+
description: 'Dev preview — data is in-memory only.',
|
|
31
|
+
},
|
|
32
|
+
route: currentRoute(initialRoute),
|
|
33
|
+
context: {},
|
|
34
|
+
|
|
35
|
+
listTools: async () => [
|
|
36
|
+
{
|
|
37
|
+
name: 'LOCAL_NOTIS_DATABASE_QUERY',
|
|
38
|
+
inputSchema: { type: 'object', properties: { database_slug: { type: 'string' } } },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'LOCAL_NOTIS_DATABASE_UPSERT_ROLLS',
|
|
42
|
+
inputSchema: { type: 'object', properties: {} },
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
callTool: async <TResult = unknown>(name: string, args?: Record<string, unknown>): Promise<TResult> => {
|
|
46
|
+
if (name === 'LOCAL_NOTIS_DATABASE_QUERY') {
|
|
47
|
+
const request = args ?? {};
|
|
48
|
+
const databaseSlug = typeof request.database_slug === 'string' ? request.database_slug : 'rolls';
|
|
49
|
+
const query = request.query && typeof request.query === 'object'
|
|
50
|
+
? request.query as Record<string, unknown>
|
|
51
|
+
: {};
|
|
52
|
+
const offset = typeof request.offset === 'number' ? request.offset : 0;
|
|
53
|
+
const all = STATE[databaseSlug] ?? [];
|
|
54
|
+
const sliced = all.slice(offset);
|
|
55
|
+
const pageSize = typeof query.page_size === 'number' ? query.page_size : undefined;
|
|
56
|
+
return { documents: typeof pageSize === 'number' ? sliced.slice(0, pageSize) : sliced } as TResult;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (name === 'LOCAL_NOTIS_DATABASE_UPSERT_ROLLS') {
|
|
60
|
+
const request = args ?? {};
|
|
61
|
+
const docs = STATE.rolls ?? [];
|
|
62
|
+
const now = new Date().toISOString();
|
|
63
|
+
const documentId = typeof request.document_id === 'string' ? request.document_id : undefined;
|
|
64
|
+
const existing = documentId ? docs.find((d) => d.id === documentId) : undefined;
|
|
65
|
+
const controlKeys = new Set(['document_id', 'title']);
|
|
66
|
+
const properties = Object.fromEntries(
|
|
67
|
+
Object.entries(request).filter(([key]) => !controlKeys.has(key)),
|
|
68
|
+
);
|
|
69
|
+
const doc: MockDoc = {
|
|
70
|
+
id: existing?.id ?? `dev-rolls-${Date.now()}`,
|
|
71
|
+
title: typeof request.title === 'string' ? request.title : existing?.title ?? 'Untitled',
|
|
72
|
+
properties: { ...(existing?.properties ?? {}), ...properties },
|
|
73
|
+
databaseSlug: 'rolls',
|
|
74
|
+
icon: null,
|
|
75
|
+
createdAt: existing?.createdAt ?? now,
|
|
76
|
+
lastEditedTime: now,
|
|
77
|
+
};
|
|
78
|
+
if (existing) docs[docs.indexOf(existing)] = doc;
|
|
79
|
+
else docs.unshift(doc);
|
|
80
|
+
STATE.rolls = docs;
|
|
81
|
+
return { status: 'success', document: doc } as TResult;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw new Error(`Tool calls unavailable in dev preview (${name}).`);
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
request: async () => {
|
|
88
|
+
throw new Error('Backend requests unavailable in dev preview.');
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
window.__NOTIS_RANDOM_RUNTIME__ = runtime;
|
|
93
|
+
return runtime;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function setMockRoute(route: 'home' | 'history') {
|
|
97
|
+
if (window.__NOTIS_RANDOM_RUNTIME__) {
|
|
98
|
+
window.__NOTIS_RANDOM_RUNTIME__.route = currentRoute(route);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function currentRoute(route: 'home' | 'history') {
|
|
103
|
+
if (route === 'history') {
|
|
104
|
+
return {
|
|
105
|
+
slug: 'history',
|
|
106
|
+
path: '/history',
|
|
107
|
+
name: 'History',
|
|
108
|
+
icon: null,
|
|
109
|
+
parentSlug: null,
|
|
110
|
+
default: false,
|
|
111
|
+
collection: {
|
|
112
|
+
database: 'rolls',
|
|
113
|
+
titleProperty: 'Value',
|
|
114
|
+
parentProperty: null,
|
|
115
|
+
sidebar: { mode: 'flat-list' as const, allowCreate: false },
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
slug: 'home',
|
|
121
|
+
path: '/',
|
|
122
|
+
name: 'Generator',
|
|
123
|
+
icon: 'phosphor:sparkle',
|
|
124
|
+
parentSlug: null,
|
|
125
|
+
default: true,
|
|
126
|
+
collection: null,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Config } from 'tailwindcss';
|
|
2
|
+
import tailwindAnimate from 'tailwindcss-animate';
|
|
3
|
+
|
|
4
|
+
const config: Config = {
|
|
5
|
+
darkMode: ['class'],
|
|
6
|
+
content: ['./app/**/*.{ts,tsx}', './lib/**/*.{ts,tsx}'],
|
|
7
|
+
theme: {
|
|
8
|
+
extend: {
|
|
9
|
+
colors: {
|
|
10
|
+
border: 'hsl(var(--border))',
|
|
11
|
+
background: 'hsl(var(--background))',
|
|
12
|
+
foreground: 'hsl(var(--foreground))',
|
|
13
|
+
muted: {
|
|
14
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
15
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
16
|
+
},
|
|
17
|
+
primary: {
|
|
18
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
19
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
20
|
+
},
|
|
21
|
+
accent: {
|
|
22
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
23
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
24
|
+
},
|
|
25
|
+
card: {
|
|
26
|
+
DEFAULT: 'hsl(var(--card))',
|
|
27
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
borderRadius: {
|
|
31
|
+
lg: 'var(--radius)',
|
|
32
|
+
md: 'calc(var(--radius) - 2px)',
|
|
33
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
34
|
+
},
|
|
35
|
+
fontFamily: {
|
|
36
|
+
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'monospace'],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
plugins: [tailwindAnimate],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default config;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"preserveSymlinks": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"jsx": "react-jsx",
|
|
16
|
+
"types": ["vite/client", "node"],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./*"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
22
|
+
"exclude": ["node_modules", "dist"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { notisViteConfig } from '@notis/sdk/vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import { defineConfig, type UserConfig } from 'vite';
|
|
4
|
+
import appConfig from './notis.config';
|
|
5
|
+
|
|
6
|
+
const libConfig = notisViteConfig(appConfig) as unknown as UserConfig;
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
...libConfig,
|
|
10
|
+
plugins: [react(), ...((libConfig.plugins as UserConfig['plugins']) ?? [])],
|
|
11
|
+
});
|