@happyvertical/smrt-template-sveltekit 0.55.16 → 0.55.18
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/AGENTS.md +20 -3
- package/README.md +8 -8
- package/mcp-apps-template/package.dependencies.json +2 -2
- package/mcp-apps-template/src/routes/api/mcp/+server.ts +16 -16
- package/package.json +19 -19
- package/template/README.md +34 -35
- package/template/package.json +11 -11
- package/template/src/app.html +5 -1
- package/template/src/lib/objects/Note.ts +44 -0
- package/template/src/lib/objects/index.ts +1 -1
- package/template/src/lib/sign-out.ts +19 -0
- package/template/src/routes/+layout.svelte +9 -9
- package/template/src/routes/+page.server.ts +26 -21
- package/template/src/routes/+page.svelte +108 -184
- package/template/src/routes/settings/+page.svelte +2 -1
- package/template/src/routes/setup/+page.svelte +1 -1
- package/template/src/routes/sign-out/+server.ts +6 -0
- package/template/static/favicon.svg +1 -0
- package/template.config.js +11 -11
- package/template/src/lib/objects/Item.ts +0 -45
package/AGENTS.md
CHANGED
|
@@ -84,17 +84,34 @@ It is the ground-up alternative to `smrt-saas-starter`.
|
|
|
84
84
|
establishes tenant context. Never turn an untrusted header into authority.
|
|
85
85
|
- App code uses `runtime.getCollection()`/`runtime.classOptions()`, which are
|
|
86
86
|
request-scoped (RLS transaction) and must never be retained.
|
|
87
|
-
- `
|
|
88
|
-
a REST writable allowlist, shared CRUD action metadata, and the explicit
|
|
87
|
+
- `Note` is the single example object (the canonical Notes starter, matching
|
|
88
|
+
`smrt-start`) and demonstrates required tenant scope, a REST writable allowlist, shared CRUD action metadata, and the explicit
|
|
89
89
|
collection registration required by generated CLI/MCP runtime commands.
|
|
90
90
|
- Initial data comes from `+page.server.ts`; loads declare
|
|
91
|
-
`depends('smrt:
|
|
91
|
+
`depends('smrt:notes')`, mutations call `invalidate('smrt:notes')`, and
|
|
92
92
|
hand-written writes call `assertOperationPermission()` with the session's
|
|
93
93
|
exact permission snapshot.
|
|
94
94
|
- The root layout is `AppShell` from `@happyvertical/smrt-svelte/app` with a
|
|
95
95
|
small explicit `nav`; `/setup` and `/settings` mount that package's
|
|
96
96
|
`OwnerSetupForm` and `ShellSettingsPage`. Generated REST routes do not imply
|
|
97
97
|
page routes.
|
|
98
|
+
- Session user and sign-out: `+layout.server.ts` (`createSessionLayoutLoad()`)
|
|
99
|
+
returns `session.user` (`{ id, email, name? }` only, never the record or
|
|
100
|
+
tokens). The root layout passes it as AppShell `user` (account menu) with
|
|
101
|
+
`onSignOut` from `src/lib/sign-out.ts` (same-origin `fetch` `POST /sign-out`,
|
|
102
|
+
then `invalidateAll()` and `goto('/')`); the settings page passes the same
|
|
103
|
+
helper. `src/routes/sign-out/+server.ts` mounts `createSignOutHandler(runtime)`
|
|
104
|
+
(POST only, 405 otherwise; same-origin only, 403 otherwise; 204, or 303 to `/`
|
|
105
|
+
for an HTML form post). The layout enables `phoneTabBar` and sets
|
|
106
|
+
`chrome={!page.route.id?.startsWith('/setup')}` so `/setup` renders only the
|
|
107
|
+
chrome-free `OwnerSetupForm` card.
|
|
108
|
+
- The Notes page is built from smrt-ui primitives only (`Button`, `Card`,
|
|
109
|
+
`Form`, `FormGroup`, `Input`, `Textarea`, `EmptyState`): no raw `<form>`,
|
|
110
|
+
`<input>`, or `<button>`, token spacing, and a "Start your first note" empty
|
|
111
|
+
state. The root layout passes only live AppShell props (no `subtitle`,
|
|
112
|
+
`tenantLabel`, or `appPanelDocs`) and gives each nav item a shell icon name.
|
|
113
|
+
`app.html` carries `viewport-fit=cover`, `color-scheme`, light and dark
|
|
114
|
+
`theme-color`, and `static/favicon.svg`.
|
|
98
115
|
- AppShell's Provider registers generated WebMCP read tools with the
|
|
99
116
|
authenticated page session. Write/destructive effects require an explicit
|
|
100
117
|
page-owned policy.
|
package/README.md
CHANGED
|
@@ -70,13 +70,13 @@ lifecycle script is `smrt app <operation>`.
|
|
|
70
70
|
|
|
71
71
|
## 3. Define the first object
|
|
72
72
|
|
|
73
|
-
The generated `src/lib/objects/
|
|
74
|
-
title
|
|
73
|
+
The generated `src/lib/objects/Note.ts` is one required-tenant object with
|
|
74
|
+
title and body fields. Its small explicit `NoteCollection`
|
|
75
75
|
constructor keeps generated CLI/MCP runtime commands constructible. Its
|
|
76
76
|
`@smrt()` configuration exposes CRUD to REST, MCP, WebMCP definitions, and CLI
|
|
77
77
|
while using an API writable allowlist.
|
|
78
78
|
|
|
79
|
-
Add objects beside
|
|
79
|
+
Add objects beside Note and export them from `src/lib/objects/index.ts`.
|
|
80
80
|
`vite.config.ts` already scans that directory.
|
|
81
81
|
|
|
82
82
|
## 4. Initialize or migrate the database
|
|
@@ -121,9 +121,9 @@ points.
|
|
|
121
121
|
|
|
122
122
|
## 7. Load data into a SvelteKit page
|
|
123
123
|
|
|
124
|
-
The home page loads
|
|
125
|
-
data, and declares `depends('smrt:
|
|
126
|
-
`$props()` data and calls `invalidate('smrt:
|
|
124
|
+
The home page loads Note rows in `+page.server.ts`, returns plain serialized
|
|
125
|
+
data, and declares `depends('smrt:notes')`. The Svelte 5 page reads hydrated
|
|
126
|
+
`$props()` data and calls `invalidate('smrt:notes')` after a successful form
|
|
127
127
|
mutation. It does not fetch initial data in `onMount` or `$effect`.
|
|
128
128
|
|
|
129
129
|
## 8. Use generated REST, MCP, WebMCP, and CLI interfaces
|
|
@@ -134,7 +134,7 @@ declared dev dependency:
|
|
|
134
134
|
|
|
135
135
|
```bash
|
|
136
136
|
pnpm smrt objects
|
|
137
|
-
pnpm smrt schema
|
|
137
|
+
pnpm smrt schema Note
|
|
138
138
|
pnpm smrt generate-mcp --no-config --no-readme
|
|
139
139
|
```
|
|
140
140
|
|
|
@@ -156,7 +156,7 @@ is needed for WebMCP. Live browser collections remain opt-in per page; import
|
|
|
156
156
|
the collection helpers only on pages that need interactive client data.
|
|
157
157
|
|
|
158
158
|
The generated-project README shows the current hydration pattern:
|
|
159
|
-
`createSmrtCollection(getCollectionDefinition('
|
|
159
|
+
`createSmrtCollection(getCollectionDefinition('notes'), { initialData,
|
|
160
160
|
basePath: '/api' })` followed by `liveCollection()`. The root layout's
|
|
161
161
|
read-only WebMCP registration does not materialize live collections.
|
|
162
162
|
|
|
@@ -2,17 +2,17 @@ import type { McpWorkflowToolDefinition } from '@happyvertical/smrt-app-mcp';
|
|
|
2
2
|
import { mountMcpAppRoute } from '@happyvertical/smrt-app-mcp/sveltekit';
|
|
3
3
|
import { openAiDisplayMetadata, withOpenAiEntrypoints } from '@happyvertical/smrt-mcp-openai';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Note } from '$lib/objects/Note';
|
|
6
6
|
import { runtime } from '$lib/server/smrt';
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
const
|
|
8
|
+
const NOTE_RESOURCE_URI = 'ui://smrt-app/v1/notes.html';
|
|
9
|
+
const noteResourceHtml = `<!doctype html><html lang="en"><head><title>Notes</title></head><body><main tabindex="-1"><h1>Notes</h1><p>Use the host controls to review your authorized notes.</p></main></body></html>`;
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const notesOverview: McpWorkflowToolDefinition = withOpenAiEntrypoints(
|
|
12
12
|
{
|
|
13
|
-
name: '
|
|
13
|
+
name: 'notes_overview',
|
|
14
14
|
description: 'Open the authorized item overview.',
|
|
15
|
-
title: '
|
|
15
|
+
title: 'Notes',
|
|
16
16
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
17
17
|
outputSchema: {
|
|
18
18
|
type: 'object',
|
|
@@ -23,10 +23,10 @@ const itemsOverview: McpWorkflowToolDefinition = withOpenAiEntrypoints(
|
|
|
23
23
|
effect: 'read',
|
|
24
24
|
idempotent: true,
|
|
25
25
|
openWorld: false,
|
|
26
|
-
ui: { resourceUri:
|
|
26
|
+
ui: { resourceUri: NOTE_RESOURCE_URI },
|
|
27
27
|
async execute() {
|
|
28
28
|
return {
|
|
29
|
-
content: [{ type: 'text', text: '
|
|
29
|
+
content: [{ type: 'text', text: 'Note overview is ready.' }],
|
|
30
30
|
structuredContent: { state: 'ready' },
|
|
31
31
|
};
|
|
32
32
|
},
|
|
@@ -35,7 +35,7 @@ const itemsOverview: McpWorkflowToolDefinition = withOpenAiEntrypoints(
|
|
|
35
35
|
);
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Stateless SDK-v2 Streamable HTTP MCP endpoint for this app's own `
|
|
38
|
+
* Stateless SDK-v2 Streamable HTTP MCP endpoint for this app's own `Note`.
|
|
39
39
|
* `runtime` supplies the request database, the bearer adapter for the profile
|
|
40
40
|
* it resolves per request, and the principal binding. Locally the principal
|
|
41
41
|
* is the signed session (user, authorized tenant, and permission slugs), or
|
|
@@ -46,22 +46,22 @@ const itemsOverview: McpWorkflowToolDefinition = withOpenAiEntrypoints(
|
|
|
46
46
|
* `createHostedMcpResourceAuth({ ..., resolvePrincipal })` to override it).
|
|
47
47
|
* A bearer principal is bound into the request permission
|
|
48
48
|
* context (under `database-rls`, its own RLS transaction) with its scopes
|
|
49
|
-
* capped to live permissions. Every principal needs `
|
|
49
|
+
* capped to live permissions. Every principal needs `notes.read`, so only
|
|
50
50
|
* read-only tools are published until per-operation authorization exists;
|
|
51
51
|
* browser requests from a foreign `Origin` are refused.
|
|
52
52
|
*/
|
|
53
53
|
export const POST = mountMcpAppRoute({
|
|
54
54
|
runtime,
|
|
55
|
-
models: [
|
|
56
|
-
requiredScopes: ['
|
|
55
|
+
models: [Note],
|
|
56
|
+
requiredScopes: ['notes.read'],
|
|
57
57
|
effects: ['read'],
|
|
58
|
-
workflowTools: [
|
|
58
|
+
workflowTools: [notesOverview],
|
|
59
59
|
resources: [
|
|
60
60
|
{
|
|
61
|
-
uri:
|
|
61
|
+
uri: NOTE_RESOURCE_URI,
|
|
62
62
|
version: 'v1',
|
|
63
|
-
name: '
|
|
64
|
-
html:
|
|
63
|
+
name: 'Notes',
|
|
64
|
+
html: noteResourceHtml,
|
|
65
65
|
csp: {},
|
|
66
66
|
metadata: openAiDisplayMetadata({ availableDisplayModes: ['inline'] }),
|
|
67
67
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-template-sveltekit",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.18",
|
|
4
4
|
"description": "Minimal SvelteKit project template with s-m-r-t framework integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -32,26 +32,26 @@
|
|
|
32
32
|
"directory": "packages/template-sveltekit"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@happyvertical/smrt-core": "0.55.
|
|
35
|
+
"@happyvertical/smrt-core": "0.55.18"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@happyvertical/smrt-app-cli": "0.55.
|
|
39
|
-
"@happyvertical/smrt-app-mcp": "0.55.
|
|
40
|
-
"@happyvertical/smrt-app-runtime": "0.55.
|
|
41
|
-
"@happyvertical/smrt-assets": "0.55.
|
|
42
|
-
"@happyvertical/smrt-chat": "0.55.
|
|
43
|
-
"@happyvertical/smrt-cli": "0.55.
|
|
44
|
-
"@happyvertical/smrt-config": "0.55.
|
|
45
|
-
"@happyvertical/smrt-jobs": "0.55.
|
|
46
|
-
"@happyvertical/smrt-mcp-openai": "0.55.
|
|
47
|
-
"@happyvertical/smrt-playbooks": "0.55.
|
|
48
|
-
"@happyvertical/smrt-profiles": "0.55.
|
|
49
|
-
"@happyvertical/smrt-scanner": "0.55.
|
|
50
|
-
"@happyvertical/smrt-svelte": "0.55.
|
|
51
|
-
"@happyvertical/smrt-tenancy": "0.55.
|
|
52
|
-
"@happyvertical/smrt-ui": "0.55.
|
|
53
|
-
"@happyvertical/smrt-users": "0.55.
|
|
54
|
-
"@happyvertical/smrt-web": "0.55.
|
|
38
|
+
"@happyvertical/smrt-app-cli": "0.55.18",
|
|
39
|
+
"@happyvertical/smrt-app-mcp": "0.55.18",
|
|
40
|
+
"@happyvertical/smrt-app-runtime": "0.55.18",
|
|
41
|
+
"@happyvertical/smrt-assets": "0.55.18",
|
|
42
|
+
"@happyvertical/smrt-chat": "0.55.18",
|
|
43
|
+
"@happyvertical/smrt-cli": "0.55.18",
|
|
44
|
+
"@happyvertical/smrt-config": "0.55.18",
|
|
45
|
+
"@happyvertical/smrt-jobs": "0.55.18",
|
|
46
|
+
"@happyvertical/smrt-mcp-openai": "0.55.18",
|
|
47
|
+
"@happyvertical/smrt-playbooks": "0.55.18",
|
|
48
|
+
"@happyvertical/smrt-profiles": "0.55.18",
|
|
49
|
+
"@happyvertical/smrt-scanner": "0.55.18",
|
|
50
|
+
"@happyvertical/smrt-svelte": "0.55.18",
|
|
51
|
+
"@happyvertical/smrt-tenancy": "0.55.18",
|
|
52
|
+
"@happyvertical/smrt-ui": "0.55.18",
|
|
53
|
+
"@happyvertical/smrt-users": "0.55.18",
|
|
54
|
+
"@happyvertical/smrt-web": "0.55.18",
|
|
55
55
|
"@happyvertical/sql": "^0.89.12",
|
|
56
56
|
"@modelcontextprotocol/client": "2.0.0",
|
|
57
57
|
"@modelcontextprotocol/server": "^2.0.0",
|
package/template/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# s-m-r-t SvelteKit starter
|
|
2
2
|
|
|
3
3
|
This is the small, ground-up starting point for s-m-r-t 0.43.10. It contains
|
|
4
|
-
one object and a profile-aware operational surface for local, self-hosted, and
|
|
4
|
+
one Notes object and a profile-aware operational surface for local, self-hosted, and
|
|
5
5
|
managed-cloud environments. It does not provision external providers.
|
|
6
6
|
|
|
7
7
|
## 1. Install and run
|
|
@@ -69,7 +69,7 @@ objects.
|
|
|
69
69
|
|
|
70
70
|
## 3. Define the first object
|
|
71
71
|
|
|
72
|
-
`src/lib/objects/
|
|
72
|
+
`src/lib/objects/Note.ts` is the only example. It exposes the same CRUD action
|
|
73
73
|
set to REST, MCP, WebMCP definitions, and the CLI, while limiting writable REST
|
|
74
74
|
fields and opting into tenant scoping:
|
|
75
75
|
|
|
@@ -85,26 +85,25 @@ import { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';
|
|
|
85
85
|
@smrt({
|
|
86
86
|
api: {
|
|
87
87
|
include: ['list', 'get', 'create', 'update', 'delete'],
|
|
88
|
-
writable: ['title', '
|
|
88
|
+
writable: ['title', 'body'],
|
|
89
89
|
},
|
|
90
90
|
cli: { include: ['list', 'get', 'create', 'update', 'delete'] },
|
|
91
91
|
mcp: { include: ['list', 'get', 'create', 'update', 'delete'] },
|
|
92
92
|
})
|
|
93
|
-
@TenantScoped({ mode: '
|
|
94
|
-
export class
|
|
95
|
-
@tenantId(
|
|
96
|
-
tenantId: string
|
|
93
|
+
@TenantScoped({ mode: 'required' })
|
|
94
|
+
export class Note extends SmrtObject {
|
|
95
|
+
@tenantId()
|
|
96
|
+
tenantId: string = '';
|
|
97
97
|
|
|
98
98
|
title: string = '';
|
|
99
|
-
|
|
100
|
-
status: string = 'draft';
|
|
99
|
+
body: string = '';
|
|
101
100
|
}
|
|
102
101
|
|
|
103
|
-
export class
|
|
104
|
-
static readonly _itemClass =
|
|
102
|
+
export class NoteCollection extends SmrtCollection<Note> {
|
|
103
|
+
static readonly _itemClass = Note;
|
|
105
104
|
}
|
|
106
105
|
|
|
107
|
-
ObjectRegistry.registerCollection('
|
|
106
|
+
ObjectRegistry.registerCollection('Note', NoteCollection);
|
|
108
107
|
```
|
|
109
108
|
|
|
110
109
|
Add another class beside it and export the class from
|
|
@@ -182,8 +181,8 @@ These are separate records with separate responsibilities:
|
|
|
182
181
|
- Session binds the authenticated user to an active tenant and publishes the
|
|
183
182
|
resolved permission set for the request.
|
|
184
183
|
|
|
185
|
-
CRUD permissions are manifest-derived: `
|
|
186
|
-
`
|
|
184
|
+
CRUD permissions are manifest-derived: `notes.read`, `notes.create`,
|
|
185
|
+
`notes.update`, and `notes.delete`. Provisioning code should sync the catalog
|
|
187
186
|
and seed roles after the database migration:
|
|
188
187
|
|
|
189
188
|
```ts
|
|
@@ -219,17 +218,17 @@ session context directly. Return plain serializable rows:
|
|
|
219
218
|
|
|
220
219
|
```ts
|
|
221
220
|
export const load: PageServerLoad = async ({ depends, locals }) => {
|
|
222
|
-
depends('smrt:
|
|
221
|
+
depends('smrt:notes');
|
|
223
222
|
|
|
224
|
-
if (!locals.permissions.includes('
|
|
225
|
-
return {
|
|
223
|
+
if (!locals.permissions.includes('notes.read')) {
|
|
224
|
+
return { notes: [] };
|
|
226
225
|
}
|
|
227
226
|
|
|
228
|
-
const
|
|
229
|
-
const rows = await
|
|
227
|
+
const notes = await runtime.getCollection<Note>('Note');
|
|
228
|
+
const rows = await notes.list({ limit: 50 });
|
|
230
229
|
return {
|
|
231
|
-
|
|
232
|
-
|
|
230
|
+
notes: rows.flatMap((note) =>
|
|
231
|
+
note.id ? [{ id: note.id, title: note.title, body: note.body }] : [],
|
|
233
232
|
),
|
|
234
233
|
};
|
|
235
234
|
};
|
|
@@ -237,8 +236,8 @@ export const load: PageServerLoad = async ({ depends, locals }) => {
|
|
|
237
236
|
|
|
238
237
|
The page receives that data through `$props()`. Do not fetch initial page data
|
|
239
238
|
from `onMount` or `$effect`; SvelteKit already serialized it into the response.
|
|
240
|
-
After a mutation, call `invalidate('smrt:
|
|
241
|
-
`depends('smrt:
|
|
239
|
+
After a mutation, call `invalidate('smrt:notes')`. Only loads that declared
|
|
240
|
+
`depends('smrt:notes')` re-run.
|
|
242
241
|
|
|
243
242
|
## 8. Use generated REST, MCP, WebMCP, and CLI interfaces
|
|
244
243
|
|
|
@@ -256,8 +255,8 @@ include malformed manifest content.
|
|
|
256
255
|
|
|
257
256
|
The option stages `src/routes/api/mcp/+server.ts`, one
|
|
258
257
|
`mountMcpAppRoute({ runtime, ... })` call from
|
|
259
|
-
`@happyvertical/smrt-app-mcp/sveltekit` that publishes `
|
|
260
|
-
principals holding `
|
|
258
|
+
`@happyvertical/smrt-app-mcp/sveltekit` that publishes `Note` to
|
|
259
|
+
principals holding `notes.read` (the signed session's permissions locally),
|
|
261
260
|
refuses browser requests from a foreign `Origin`, and serves a bounded static
|
|
262
261
|
resource with a restrictive CSP and the optional OpenAI display metadata. It
|
|
263
262
|
also stages the protected-resource metadata route. For a view, mount
|
|
@@ -273,20 +272,20 @@ request nor grants tenant or workflow authority. Register `.app.json` only
|
|
|
273
272
|
after the host creates an eligible server ID; do not copy runtime extension
|
|
274
273
|
metadata into the portable manifest.
|
|
275
274
|
|
|
276
|
-
The
|
|
275
|
+
The Note configuration generates:
|
|
277
276
|
|
|
278
|
-
- REST: `GET`/`POST /api/
|
|
279
|
-
`GET`/`PUT`/`DELETE /api/
|
|
280
|
-
- MCP descriptors and tools for
|
|
277
|
+
- REST: `GET`/`POST /api/notes` and
|
|
278
|
+
`GET`/`PUT`/`DELETE /api/notes/[id]`.
|
|
279
|
+
- MCP descriptors and tools for Note CRUD.
|
|
281
280
|
- Web collection definitions in the virtual
|
|
282
281
|
`@happyvertical/smrt-virt-web` module.
|
|
283
|
-
- CLI commands for
|
|
282
|
+
- CLI commands for Note CRUD.
|
|
284
283
|
|
|
285
284
|
Inspect the registered objects and use the example CLI:
|
|
286
285
|
|
|
287
286
|
```bash
|
|
288
287
|
pnpm smrt objects
|
|
289
|
-
pnpm smrt schema
|
|
288
|
+
pnpm smrt schema Note
|
|
290
289
|
```
|
|
291
290
|
|
|
292
291
|
The CLI's manifest-only `objects` and `schema` commands work directly in this
|
|
@@ -372,12 +371,12 @@ hydrated rows so the first render does not issue a duplicate request:
|
|
|
372
371
|
|
|
373
372
|
let { data }: PageProps = $props();
|
|
374
373
|
|
|
375
|
-
const
|
|
374
|
+
const notes = createSmrtCollection(getCollectionDefinition('notes'), {
|
|
376
375
|
basePath: '/api',
|
|
377
|
-
initialData: data.
|
|
376
|
+
initialData: data.notes,
|
|
378
377
|
staleTimeMs: 30_000,
|
|
379
378
|
});
|
|
380
|
-
const view = liveCollection(
|
|
379
|
+
const view = liveCollection(notes);
|
|
381
380
|
</script>
|
|
382
381
|
|
|
383
382
|
{#each view.rows as item (item.id)}
|
|
@@ -441,7 +440,7 @@ the signed browser session. The other is an owner-minted bearer token for a
|
|
|
441
440
|
local MCP client such as Claude Desktop through `smrt-mcp-bridge`:
|
|
442
441
|
|
|
443
442
|
```bash
|
|
444
|
-
pnpm exec smrt app token --scopes
|
|
443
|
+
pnpm exec smrt app token --scopes notes.read --label "Claude Desktop"
|
|
445
444
|
pnpm exec smrt app token list
|
|
446
445
|
pnpm exec smrt app token revoke <id>
|
|
447
446
|
```
|
package/template/package.json
CHANGED
|
@@ -39,19 +39,19 @@
|
|
|
39
39
|
"svelte-check": "^4.7.6",
|
|
40
40
|
"typescript": "6.0.3",
|
|
41
41
|
"vite": "8.3.0",
|
|
42
|
-
"@happyvertical/smrt-cli": "^0.55.
|
|
42
|
+
"@happyvertical/smrt-cli": "^0.55.18"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@happyvertical/smrt-app-runtime": "^0.55.
|
|
46
|
-
"@happyvertical/smrt-config": "^0.55.
|
|
47
|
-
"@happyvertical/smrt-core": "^0.55.
|
|
48
|
-
"@happyvertical/smrt-jobs": "^0.55.
|
|
49
|
-
"@happyvertical/smrt-profiles": "^0.55.
|
|
50
|
-
"@happyvertical/smrt-svelte": "^0.55.
|
|
51
|
-
"@happyvertical/smrt-tenancy": "^0.55.
|
|
52
|
-
"@happyvertical/smrt-ui": "^0.55.
|
|
53
|
-
"@happyvertical/smrt-users": "^0.55.
|
|
54
|
-
"@happyvertical/smrt-web": "^0.55.
|
|
45
|
+
"@happyvertical/smrt-app-runtime": "^0.55.18",
|
|
46
|
+
"@happyvertical/smrt-config": "^0.55.18",
|
|
47
|
+
"@happyvertical/smrt-core": "^0.55.18",
|
|
48
|
+
"@happyvertical/smrt-jobs": "^0.55.18",
|
|
49
|
+
"@happyvertical/smrt-profiles": "^0.55.18",
|
|
50
|
+
"@happyvertical/smrt-svelte": "^0.55.18",
|
|
51
|
+
"@happyvertical/smrt-tenancy": "^0.55.18",
|
|
52
|
+
"@happyvertical/smrt-ui": "^0.55.18",
|
|
53
|
+
"@happyvertical/smrt-users": "^0.55.18",
|
|
54
|
+
"@happyvertical/smrt-web": "^0.55.18",
|
|
55
55
|
"@happyvertical/sql": "^0.89.12",
|
|
56
56
|
"@modelcontextprotocol/server": "^2.0.0"
|
|
57
57
|
}
|
package/template/src/app.html
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
6
|
+
<meta name="color-scheme" content="light dark" />
|
|
7
|
+
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
|
|
8
|
+
<meta name="theme-color" content="#121212" media="(prefers-color-scheme: dark)" />
|
|
9
|
+
<link rel="icon" type="image/svg+xml" href="%sveltekit.assets%/favicon.svg" />
|
|
6
10
|
%sveltekit.head%
|
|
7
11
|
</head>
|
|
8
12
|
<body data-sveltekit-preload-data="hover">
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example s-m-r-t object.
|
|
3
|
+
*
|
|
4
|
+
* This one declaration is the app's data model: the `smrt()` Vite plugin turns
|
|
5
|
+
* it into the schema, the REST routes under `/api/notes`, the WebMCP read
|
|
6
|
+
* tools, and the generated CLI/MCP commands. Rename or replace it with your
|
|
7
|
+
* own objects.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
ObjectRegistry,
|
|
12
|
+
SmrtCollection,
|
|
13
|
+
SmrtObject,
|
|
14
|
+
smrt,
|
|
15
|
+
} from '@happyvertical/smrt-core';
|
|
16
|
+
import { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';
|
|
17
|
+
|
|
18
|
+
@smrt({
|
|
19
|
+
api: {
|
|
20
|
+
include: ['list', 'get', 'create', 'update', 'delete'],
|
|
21
|
+
writable: ['title', 'body'],
|
|
22
|
+
},
|
|
23
|
+
cli: { include: ['list', 'get', 'create', 'update', 'delete'] },
|
|
24
|
+
mcp: { include: ['list', 'get', 'create', 'update', 'delete'] },
|
|
25
|
+
})
|
|
26
|
+
@TenantScoped({ mode: 'required' })
|
|
27
|
+
export class Note extends SmrtObject {
|
|
28
|
+
/** Tenant rows are isolated automatically by the session's tenant. */
|
|
29
|
+
@tenantId()
|
|
30
|
+
tenantId: string = '';
|
|
31
|
+
|
|
32
|
+
/** Note title */
|
|
33
|
+
title: string = '';
|
|
34
|
+
|
|
35
|
+
/** Note body */
|
|
36
|
+
body: string = '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Explicit collection constructor used by generated CLI/MCP/runtime tools. */
|
|
40
|
+
export class NoteCollection extends SmrtCollection<Note> {
|
|
41
|
+
static readonly _itemClass = Note;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
ObjectRegistry.registerCollection('Note', NoteCollection);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { goto, invalidateAll } from '$app/navigation';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sign out through the runtime's same-origin `POST /sign-out` route, then
|
|
5
|
+
* refresh the session-derived layout data and return home. A failed request
|
|
6
|
+
* (403, 5xx) throws instead of pretending the session ended: the user stays
|
|
7
|
+
* signed in and the error names the status.
|
|
8
|
+
*/
|
|
9
|
+
export async function signOut(): Promise<void> {
|
|
10
|
+
const response = await fetch('/sign-out', {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
credentials: 'same-origin',
|
|
13
|
+
});
|
|
14
|
+
if (!response.ok) {
|
|
15
|
+
throw new Error(`Sign-out failed with status ${response.status}`);
|
|
16
|
+
}
|
|
17
|
+
await invalidateAll();
|
|
18
|
+
await goto('/');
|
|
19
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { webMcpToolDefinitions } from '@happyvertical/smrt-virt-web';
|
|
4
4
|
import { AppShell } from '@happyvertical/smrt-svelte/app';
|
|
5
5
|
import type { ShellNavItem } from '@happyvertical/smrt-svelte/workspace';
|
|
6
|
+
import { signOut } from '$lib/sign-out';
|
|
6
7
|
import type { LayoutProps } from './$types';
|
|
7
8
|
|
|
8
9
|
let { data, children }: LayoutProps = $props();
|
|
@@ -10,8 +11,8 @@
|
|
|
10
11
|
// Add application routes here. Generated REST routes live under /api and do
|
|
11
12
|
// not automatically imply a human-facing page.
|
|
12
13
|
const nav: ShellNavItem[] = [
|
|
13
|
-
{ href: '/', label: '
|
|
14
|
-
{ href: '/settings', label: 'Settings', description: 'Workspace layout and shortcuts' },
|
|
14
|
+
{ href: '/', label: 'Notes', icon: 'book', description: 'Your workspace notes' },
|
|
15
|
+
{ href: '/settings', label: 'Settings', icon: 'settings', description: 'Workspace layout and shortcuts' },
|
|
15
16
|
];
|
|
16
17
|
|
|
17
18
|
// Generated read-only WebMCP tools, only in browsers that expose WebMCP.
|
|
@@ -24,18 +25,17 @@
|
|
|
24
25
|
|
|
25
26
|
<AppShell
|
|
26
27
|
title="s-m-r-t app"
|
|
27
|
-
subtitle="SvelteKit"
|
|
28
28
|
{webmcp}
|
|
29
29
|
{nav}
|
|
30
30
|
currentHref={page.url.pathname}
|
|
31
31
|
runtimeDiagnostics={data.session.authenticated}
|
|
32
|
-
tenantLabel={data.session.activeTenantId ? 'Authorized session tenant' : 'No active tenant'}
|
|
33
32
|
settingsHref="/settings"
|
|
33
|
+
edgeToggles={{ left: true }}
|
|
34
|
+
config={{ left: { initial: 'expanded' } }}
|
|
35
|
+
user={data.session.user}
|
|
36
|
+
onSignOut={signOut}
|
|
37
|
+
phoneTabBar
|
|
38
|
+
chrome={!page.route.id?.startsWith('/setup')}
|
|
34
39
|
>
|
|
35
|
-
{#snippet appPanelDocs()}
|
|
36
|
-
{#if data.session.selectedTenantSlug}
|
|
37
|
-
<p>Selected URL tenant: <strong>{data.session.selectedTenantSlug}</strong></p>
|
|
38
|
-
{/if}
|
|
39
|
-
{/snippet}
|
|
40
40
|
{@render children()}
|
|
41
41
|
</AppShell>
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import { assertOperationPermission } from '@happyvertical/smrt-users';
|
|
2
2
|
import { fail } from '@sveltejs/kit';
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { Note } from '$lib/objects/Note';
|
|
5
5
|
import { runtime } from '$lib/server/smrt';
|
|
6
6
|
import type { Actions, PageServerLoad } from './$types';
|
|
7
7
|
|
|
8
|
-
const READ_PERMISSION = '
|
|
9
|
-
const CREATE_PERMISSION = '
|
|
8
|
+
const READ_PERMISSION = 'notes.read';
|
|
9
|
+
const CREATE_PERMISSION = 'notes.create';
|
|
10
10
|
|
|
11
|
-
export interface
|
|
11
|
+
export interface NoteRow {
|
|
12
12
|
id: string;
|
|
13
13
|
title: string;
|
|
14
|
-
|
|
14
|
+
body: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export const load: PageServerLoad = async ({ depends, locals }) => {
|
|
18
|
-
depends('smrt:
|
|
18
|
+
depends('smrt:notes');
|
|
19
19
|
|
|
20
20
|
const authenticated = Boolean(locals.user);
|
|
21
21
|
const hasTenant = Boolean(locals.tenantId);
|
|
@@ -26,8 +26,9 @@ export const load: PageServerLoad = async ({ depends, locals }) => {
|
|
|
26
26
|
|
|
27
27
|
if (!canRead) {
|
|
28
28
|
return {
|
|
29
|
-
|
|
29
|
+
notes: [] as NoteRow[],
|
|
30
30
|
canCreate,
|
|
31
|
+
authenticated,
|
|
31
32
|
loadError: null as string | null,
|
|
32
33
|
accessMessage: !authenticated
|
|
33
34
|
? 'Sign in before loading tenant data.'
|
|
@@ -38,28 +39,30 @@ export const load: PageServerLoad = async ({ depends, locals }) => {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
try {
|
|
41
|
-
const
|
|
42
|
-
const rows = await
|
|
42
|
+
const notes = await runtime.getCollection<Note>('Note');
|
|
43
|
+
const rows = await notes.list({
|
|
43
44
|
orderBy: 'created_at DESC',
|
|
44
|
-
limit:
|
|
45
|
+
limit: 100,
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
return {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
? [{ id:
|
|
49
|
+
notes: rows.flatMap((note): NoteRow[] =>
|
|
50
|
+
note.id
|
|
51
|
+
? [{ id: note.id, title: note.title, body: note.body }]
|
|
51
52
|
: [],
|
|
52
53
|
),
|
|
53
54
|
canCreate,
|
|
55
|
+
authenticated,
|
|
54
56
|
loadError: null as string | null,
|
|
55
57
|
accessMessage: null as string | null,
|
|
56
58
|
};
|
|
57
59
|
} catch (error) {
|
|
58
60
|
return {
|
|
59
|
-
|
|
61
|
+
notes: [] as NoteRow[],
|
|
60
62
|
canCreate,
|
|
63
|
+
authenticated,
|
|
61
64
|
loadError:
|
|
62
|
-
error instanceof Error ? error.message : 'Failed to load
|
|
65
|
+
error instanceof Error ? error.message : 'Failed to load notes',
|
|
63
66
|
accessMessage: null as string | null,
|
|
64
67
|
};
|
|
65
68
|
}
|
|
@@ -72,9 +75,9 @@ export const actions: Actions = {
|
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
const decision = await assertOperationPermission({
|
|
75
|
-
...runtime.classOptions('
|
|
78
|
+
...runtime.classOptions('Note'),
|
|
76
79
|
action: 'create',
|
|
77
|
-
collection: '
|
|
80
|
+
collection: 'notes',
|
|
78
81
|
onDeny: 'return',
|
|
79
82
|
permissionSet: locals.permissions,
|
|
80
83
|
tenantId: locals.tenantId,
|
|
@@ -86,18 +89,20 @@ export const actions: Actions = {
|
|
|
86
89
|
|
|
87
90
|
const form = await request.formData();
|
|
88
91
|
const title = String(form.get('title') ?? '').trim();
|
|
92
|
+
const body = String(form.get('body') ?? '').trim();
|
|
89
93
|
if (!title) {
|
|
90
|
-
return fail(400, { title, error: '
|
|
94
|
+
return fail(400, { title, body, error: 'A title is required.' });
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
try {
|
|
94
|
-
const
|
|
95
|
-
await
|
|
98
|
+
const notes = await runtime.getCollection<Note>('Note');
|
|
99
|
+
await notes.create({ title, body });
|
|
96
100
|
return { created: title };
|
|
97
101
|
} catch (error) {
|
|
98
102
|
return fail(500, {
|
|
99
103
|
title,
|
|
100
|
-
|
|
104
|
+
body,
|
|
105
|
+
error: error instanceof Error ? error.message : 'Failed to create note',
|
|
101
106
|
});
|
|
102
107
|
}
|
|
103
108
|
},
|
|
@@ -1,213 +1,137 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { enhance } from '$app/forms';
|
|
3
3
|
import { invalidate } from '$app/navigation';
|
|
4
|
+
import { Form, FormGroup, Input, Textarea } from '@happyvertical/smrt-ui/forms';
|
|
5
|
+
import { EmptyState } from '@happyvertical/smrt-ui/layout';
|
|
6
|
+
import { Button, Card } from '@happyvertical/smrt-ui/ui';
|
|
7
|
+
import type { SubmitFunction } from '@sveltejs/kit';
|
|
8
|
+
import { fromAction } from 'svelte/attachments';
|
|
4
9
|
import type { PageProps } from './$types';
|
|
5
10
|
|
|
6
11
|
let { data, form }: PageProps = $props();
|
|
7
12
|
let creating = $state(false);
|
|
13
|
+
let title = $state('');
|
|
14
|
+
let body = $state('');
|
|
15
|
+
let composing = $state(false);
|
|
16
|
+
|
|
17
|
+
// The form shows once the owner has notes, or after "Start your first note".
|
|
18
|
+
const showForm = $derived(
|
|
19
|
+
data.canCreate && (data.notes.length > 0 || composing),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const submit: SubmitFunction = () => {
|
|
23
|
+
creating = true;
|
|
24
|
+
return async ({ result, update }) => {
|
|
25
|
+
try {
|
|
26
|
+
await update({ invalidateAll: false, reset: false });
|
|
27
|
+
if (result.type === 'success') {
|
|
28
|
+
title = '';
|
|
29
|
+
body = '';
|
|
30
|
+
await invalidate('smrt:notes');
|
|
31
|
+
}
|
|
32
|
+
} finally {
|
|
33
|
+
creating = false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
};
|
|
8
37
|
</script>
|
|
9
38
|
|
|
10
39
|
<svelte:head>
|
|
11
|
-
<title>
|
|
40
|
+
<title>Notes</title>
|
|
12
41
|
</svelte:head>
|
|
13
42
|
|
|
14
|
-
<
|
|
15
|
-
<
|
|
16
|
-
<p class="eyebrow">s-m-r-t 0.38.25</p>
|
|
17
|
-
<h1>Learn the application foundation</h1>
|
|
18
|
-
<p>
|
|
19
|
-
One object, SQLite defaults, server-loaded data, generated interfaces,
|
|
20
|
-
session-authorized tenancy, and room to grow.
|
|
21
|
-
</p>
|
|
22
|
-
</header>
|
|
43
|
+
<main class="page">
|
|
44
|
+
<h1>Notes</h1>
|
|
23
45
|
|
|
24
|
-
|
|
25
|
-
<
|
|
26
|
-
<
|
|
27
|
-
<h2>Items</h2>
|
|
28
|
-
<p>The initial list comes from <code>+page.server.ts</code>.</p>
|
|
29
|
-
</div>
|
|
30
|
-
</div>
|
|
31
|
-
|
|
32
|
-
{#if data.loadError}
|
|
33
|
-
<p class="notice notice--error">{data.loadError}</p>
|
|
46
|
+
{#if data.loadError}
|
|
47
|
+
<Card>
|
|
48
|
+
<p role="alert">{data.loadError}</p>
|
|
34
49
|
<p>Run <code>pnpm db:migrate</code> after changing object fields.</p>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<p
|
|
50
|
+
</Card>
|
|
51
|
+
{:else if data.accessMessage}
|
|
52
|
+
<Card>
|
|
53
|
+
<p>{data.accessMessage}</p>
|
|
54
|
+
{#if !data.authenticated}
|
|
55
|
+
<Button href="/setup">Set up this app</Button>
|
|
56
|
+
{/if}
|
|
57
|
+
</Card>
|
|
58
|
+
{:else}
|
|
59
|
+
{#if showForm}
|
|
60
|
+
<Card>
|
|
61
|
+
<Form
|
|
62
|
+
method="POST"
|
|
63
|
+
action="?/create"
|
|
64
|
+
preventDefault={false}
|
|
65
|
+
{@attach fromAction(enhance, () => submit)}
|
|
66
|
+
>
|
|
67
|
+
<FormGroup label="Title" required>
|
|
68
|
+
<Input name="title" bind:value={title} required />
|
|
69
|
+
</FormGroup>
|
|
70
|
+
<FormGroup label="Body">
|
|
71
|
+
<Textarea name="body" bind:value={body} rows={4} />
|
|
72
|
+
</FormGroup>
|
|
73
|
+
{#if form?.error}<p role="alert">{form.error}</p>{/if}
|
|
74
|
+
<div>
|
|
75
|
+
<Button type="submit" loading={creating} disabled={creating}>
|
|
76
|
+
Add note
|
|
77
|
+
</Button>
|
|
78
|
+
</div>
|
|
79
|
+
</Form>
|
|
80
|
+
</Card>
|
|
81
|
+
{/if}
|
|
82
|
+
|
|
83
|
+
{#if data.notes.length === 0}
|
|
84
|
+
<EmptyState
|
|
85
|
+
title="Start your first note"
|
|
86
|
+
description={data.canCreate
|
|
87
|
+
? 'Notes you add show up here for everyone in this workspace.'
|
|
88
|
+
: 'Notes will show up here once someone adds them.'}
|
|
89
|
+
actionLabel={data.canCreate && !composing ? 'New note' : undefined}
|
|
90
|
+
onaction={() => {
|
|
91
|
+
composing = true;
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
39
94
|
{:else}
|
|
40
|
-
<ul class="
|
|
41
|
-
{#each data.
|
|
95
|
+
<ul class="notes">
|
|
96
|
+
{#each data.notes as note (note.id)}
|
|
42
97
|
<li>
|
|
43
|
-
<
|
|
44
|
-
|
|
98
|
+
<Card>
|
|
99
|
+
<h2>{note.title}</h2>
|
|
100
|
+
{#if note.body}<p>{note.body}</p>{/if}
|
|
101
|
+
</Card>
|
|
45
102
|
</li>
|
|
46
103
|
{/each}
|
|
47
104
|
</ul>
|
|
48
105
|
{/if}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<form
|
|
52
|
-
method="POST"
|
|
53
|
-
action="?/create"
|
|
54
|
-
use:enhance={() => {
|
|
55
|
-
creating = true;
|
|
56
|
-
return async ({ result, update }) => {
|
|
57
|
-
try {
|
|
58
|
-
await update({ invalidateAll: false });
|
|
59
|
-
if (result.type === 'success') {
|
|
60
|
-
await invalidate('smrt:items');
|
|
61
|
-
}
|
|
62
|
-
} finally {
|
|
63
|
-
creating = false;
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
}}
|
|
67
|
-
>
|
|
68
|
-
<label for="title">New item</label>
|
|
69
|
-
<div class="form-row">
|
|
70
|
-
<input id="title" name="title" placeholder="Write a title" required />
|
|
71
|
-
<button type="submit" disabled={creating}>
|
|
72
|
-
{creating ? 'Creating…' : 'Create item'}
|
|
73
|
-
</button>
|
|
74
|
-
</div>
|
|
75
|
-
</form>
|
|
76
|
-
{/if}
|
|
77
|
-
|
|
78
|
-
{#if form?.error}
|
|
79
|
-
<p class="notice notice--error">{form.error}</p>
|
|
80
|
-
{/if}
|
|
81
|
-
</section>
|
|
82
|
-
|
|
83
|
-
<section>
|
|
84
|
-
<h2>Where to extend</h2>
|
|
85
|
-
<ol>
|
|
86
|
-
<li>Edit <code>src/lib/objects/Item.ts</code>.</li>
|
|
87
|
-
<li>Run <code>pnpm db:migrate</code>.</li>
|
|
88
|
-
<li>Add pages that query collections in <code>+page.server.ts</code>.</li>
|
|
89
|
-
<li>Add membership-gated login and tenant-switch actions for your app.</li>
|
|
90
|
-
</ol>
|
|
91
|
-
</section>
|
|
92
|
-
</div>
|
|
106
|
+
{/if}
|
|
107
|
+
</main>
|
|
93
108
|
|
|
94
109
|
<style>
|
|
95
|
-
.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
header,
|
|
104
|
-
section,
|
|
105
|
-
.section-heading,
|
|
106
|
-
form {
|
|
107
|
-
display: grid;
|
|
108
|
-
gap: var(--smrt-spacing-3);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
header p,
|
|
112
|
-
h1,
|
|
113
|
-
h2,
|
|
114
|
-
section p {
|
|
115
|
-
margin: 0;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
.eyebrow {
|
|
119
|
-
color: var(--smrt-color-primary);
|
|
120
|
-
font: var(--smrt-typography-label-large);
|
|
121
|
-
letter-spacing: 0.08em;
|
|
122
|
-
text-transform: uppercase;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
section {
|
|
126
|
-
padding: var(--smrt-spacing-6);
|
|
127
|
-
border: 1px solid var(--smrt-color-outline-variant);
|
|
128
|
-
border-radius: var(--smrt-radius-large);
|
|
129
|
-
background: var(--smrt-color-surface);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
.notice {
|
|
133
|
-
padding: var(--smrt-spacing-3);
|
|
134
|
-
border-radius: var(--smrt-radius-medium);
|
|
135
|
-
background: var(--smrt-color-surface-container);
|
|
136
|
-
color: var(--smrt-color-on-surface-variant);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
.notice--error {
|
|
140
|
-
color: var(--smrt-color-error);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
.items {
|
|
144
|
-
display: grid;
|
|
145
|
-
gap: var(--smrt-spacing-2);
|
|
146
|
-
margin: 0;
|
|
147
|
-
padding: 0;
|
|
148
|
-
list-style: none;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.items li {
|
|
152
|
-
display: flex;
|
|
153
|
-
justify-content: space-between;
|
|
154
|
-
gap: var(--smrt-spacing-3);
|
|
155
|
-
padding-block: var(--smrt-spacing-2);
|
|
156
|
-
border-block-end: 1px solid var(--smrt-color-outline-variant);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
.items span {
|
|
160
|
-
color: var(--smrt-color-on-surface-variant);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
label {
|
|
164
|
-
font-weight: var(--smrt-typography-weight-semibold);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.form-row {
|
|
168
|
-
display: flex;
|
|
169
|
-
gap: var(--smrt-spacing-2);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
input,
|
|
173
|
-
button {
|
|
174
|
-
min-height: 2.75rem;
|
|
175
|
-
padding-inline: var(--smrt-spacing-3);
|
|
176
|
-
border-radius: var(--smrt-radius-medium);
|
|
177
|
-
font: inherit;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
input {
|
|
181
|
-
flex: 1;
|
|
182
|
-
border: 1px solid var(--smrt-color-outline);
|
|
183
|
-
background: var(--smrt-color-surface);
|
|
184
|
-
color: var(--smrt-color-on-surface);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
button {
|
|
188
|
-
border: 0;
|
|
189
|
-
background: var(--smrt-color-primary);
|
|
190
|
-
color: var(--smrt-color-on-primary);
|
|
191
|
-
cursor: pointer;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
button:disabled {
|
|
195
|
-
opacity: 0.6;
|
|
196
|
-
cursor: wait;
|
|
197
|
-
}
|
|
110
|
+
@layer smrt.theme {
|
|
111
|
+
.page {
|
|
112
|
+
display: grid;
|
|
113
|
+
gap: var(--smrt-spacing-6);
|
|
114
|
+
width: min(100%, 48rem);
|
|
115
|
+
margin-inline: auto;
|
|
116
|
+
padding: var(--smrt-spacing-6) var(--smrt-spacing-4);
|
|
117
|
+
}
|
|
198
118
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
119
|
+
.notes {
|
|
120
|
+
display: grid;
|
|
121
|
+
gap: var(--smrt-spacing-3);
|
|
122
|
+
margin: 0;
|
|
123
|
+
padding: 0;
|
|
124
|
+
list-style: none;
|
|
125
|
+
}
|
|
202
126
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
127
|
+
h1,
|
|
128
|
+
h2,
|
|
129
|
+
p {
|
|
130
|
+
margin: 0;
|
|
206
131
|
}
|
|
207
132
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
flex-direction: column;
|
|
133
|
+
h2 {
|
|
134
|
+
font: var(--smrt-typography-title-medium);
|
|
211
135
|
}
|
|
212
136
|
}
|
|
213
137
|
</style>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { ShellSettingsPage } from '@happyvertical/smrt-svelte/app';
|
|
3
|
+
import { signOut } from '$lib/sign-out';
|
|
3
4
|
</script>
|
|
4
5
|
|
|
5
6
|
<svelte:head>
|
|
@@ -7,4 +8,4 @@
|
|
|
7
8
|
</svelte:head>
|
|
8
9
|
|
|
9
10
|
<!-- Panel layout and WASD hotkeys, persisted through the shell's storageKey. -->
|
|
10
|
-
<ShellSettingsPage />
|
|
11
|
+
<ShellSettingsPage onSignOut={signOut} />
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><style>svg{color:#1c1b1f}@media(prefers-color-scheme:dark){svg{color:#e6e1e5}}</style><path d="M6 3h9l4 4v14H6z"/><path d="M14 3v5h5M9 13h7M9 17h5"/></svg>
|
package/template.config.js
CHANGED
|
@@ -19,22 +19,22 @@ export default {
|
|
|
19
19
|
|
|
20
20
|
// Dependencies to add to generated project
|
|
21
21
|
dependencies: {
|
|
22
|
-
'@happyvertical/smrt-app-runtime': '^0.55.
|
|
23
|
-
'@happyvertical/smrt-config': '^0.55.
|
|
24
|
-
'@happyvertical/smrt-core': '^0.55.
|
|
25
|
-
'@happyvertical/smrt-jobs': '^0.55.
|
|
26
|
-
'@happyvertical/smrt-profiles': '^0.55.
|
|
27
|
-
'@happyvertical/smrt-svelte': '^0.55.
|
|
28
|
-
'@happyvertical/smrt-tenancy': '^0.55.
|
|
29
|
-
'@happyvertical/smrt-ui': '^0.55.
|
|
30
|
-
'@happyvertical/smrt-users': '^0.55.
|
|
31
|
-
'@happyvertical/smrt-web': '^0.55.
|
|
22
|
+
'@happyvertical/smrt-app-runtime': '^0.55.18',
|
|
23
|
+
'@happyvertical/smrt-config': '^0.55.18',
|
|
24
|
+
'@happyvertical/smrt-core': '^0.55.18',
|
|
25
|
+
'@happyvertical/smrt-jobs': '^0.55.18',
|
|
26
|
+
'@happyvertical/smrt-profiles': '^0.55.18',
|
|
27
|
+
'@happyvertical/smrt-svelte': '^0.55.18',
|
|
28
|
+
'@happyvertical/smrt-tenancy': '^0.55.18',
|
|
29
|
+
'@happyvertical/smrt-ui': '^0.55.18',
|
|
30
|
+
'@happyvertical/smrt-users': '^0.55.18',
|
|
31
|
+
'@happyvertical/smrt-web': '^0.55.18',
|
|
32
32
|
'@happyvertical/sql': '^0.89.12',
|
|
33
33
|
'@modelcontextprotocol/server': '^2.0.0',
|
|
34
34
|
},
|
|
35
35
|
|
|
36
36
|
devDependencies: {
|
|
37
|
-
'@happyvertical/smrt-cli': '^0.55.
|
|
37
|
+
'@happyvertical/smrt-cli': '^0.55.18',
|
|
38
38
|
'@sveltejs/adapter-node': '^5.5.7',
|
|
39
39
|
'@sveltejs/kit': '2.70.3',
|
|
40
40
|
'@sveltejs/vite-plugin-svelte': '^7.3.0',
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Example s-m-r-t object.
|
|
3
|
-
*
|
|
4
|
-
* This is a sample SMRT object to demonstrate the pattern.
|
|
5
|
-
* Rename or replace this with your own objects.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
ObjectRegistry,
|
|
10
|
-
SmrtCollection,
|
|
11
|
-
SmrtObject,
|
|
12
|
-
smrt,
|
|
13
|
-
} from '@happyvertical/smrt-core';
|
|
14
|
-
import { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';
|
|
15
|
-
|
|
16
|
-
@smrt({
|
|
17
|
-
api: {
|
|
18
|
-
include: ['list', 'get', 'create', 'update', 'delete'],
|
|
19
|
-
writable: ['title', 'description', 'status'],
|
|
20
|
-
},
|
|
21
|
-
cli: { include: ['list', 'get', 'create', 'update', 'delete'] },
|
|
22
|
-
mcp: { include: ['list', 'get', 'create', 'update', 'delete'] },
|
|
23
|
-
})
|
|
24
|
-
@TenantScoped({ mode: 'optional' })
|
|
25
|
-
export class Item extends SmrtObject {
|
|
26
|
-
/** Null rows are global; tenant rows are isolated automatically. */
|
|
27
|
-
@tenantId({ nullable: true })
|
|
28
|
-
tenantId: string | null = null;
|
|
29
|
-
|
|
30
|
-
/** Item title */
|
|
31
|
-
title: string = '';
|
|
32
|
-
|
|
33
|
-
/** Item description */
|
|
34
|
-
description: string = '';
|
|
35
|
-
|
|
36
|
-
/** Status of the item */
|
|
37
|
-
status: string = 'draft';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Explicit collection constructor used by generated CLI/MCP/runtime tools. */
|
|
41
|
-
export class ItemCollection extends SmrtCollection<Item> {
|
|
42
|
-
static readonly _itemClass = Item;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
ObjectRegistry.registerCollection('Item', ItemCollection);
|