@skillre/dsh-plugin-sticky-wall 0.0.0-stage → 0.2.2
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/LICENSE +21 -0
- package/README.en.md +154 -0
- package/README.md +164 -2
- package/UNINSTALL.md +26 -0
- package/compatibility.json +83 -0
- package/cordis.patch.yml +10 -0
- package/docs/DETAIL.md +1 -0
- package/docs/DETAIL.zh.md +1 -0
- package/docs/PREVIEW.md +39 -0
- package/docs/preview.svg +170 -0
- package/docs/preview.zh.svg +170 -0
- package/icon.svg +15 -0
- package/lib/client.js +1694 -0
- package/lib/config.d.ts +22 -0
- package/lib/config.js +21 -0
- package/lib/index.d.ts +23 -0
- package/lib/index.js +184 -0
- package/lib/layout.d.ts +76 -0
- package/lib/layout.js +259 -0
- package/lib/store.d.ts +182 -0
- package/lib/store.js +452 -0
- package/lib/transport.d.ts +48 -0
- package/lib/transport.js +174 -0
- package/lib/types.d.ts +172 -0
- package/lib/types.js +64 -0
- package/locale/en.json +7 -0
- package/locale/zh.json +7 -0
- package/package.json +116 -4
package/lib/transport.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host ↔ Client transport: one exact Fetch route inside Connection's fence.
|
|
3
|
+
*
|
|
4
|
+
* Connection owns the Host/Origin check, the browser-session authentication and
|
|
5
|
+
* the Peer scope; this module registers one exact `POST /api/<endpoint>` route
|
|
6
|
+
* and only has to validate its own envelope and payload. No physical server, no
|
|
7
|
+
* Express, and no second RPC channel is claimed.
|
|
8
|
+
*
|
|
9
|
+
* The wire envelope is the inspected Connection unary shape:
|
|
10
|
+
* `{ type: 'client-request', rpcId, method, payload }` in and
|
|
11
|
+
* `{ type: 'server-response', rpcId, result: { ok: true, value } | { ok: false, error } }`
|
|
12
|
+
* out. A non-owner Peer is rejected by Connection before this handler runs.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { WallError } from './store.js';
|
|
16
|
+
import { COLORS, ENDPOINT, LIMITS, SORTS, WIDTHS, } from './types.js';
|
|
17
|
+
/** Path of the exact route, under the shared `/api` channel. */
|
|
18
|
+
export const EXECUTE_PATH = `/api/${ENDPOINT}`;
|
|
19
|
+
const envelopeSchema = z.object({
|
|
20
|
+
type: z.literal('client-request'),
|
|
21
|
+
rpcId: z.string(),
|
|
22
|
+
method: z.string(),
|
|
23
|
+
payload: z.unknown(),
|
|
24
|
+
});
|
|
25
|
+
const colorSchema = z.enum(COLORS);
|
|
26
|
+
const sortSchema = z.enum(SORTS);
|
|
27
|
+
const idSchema = z.string().min(1).max(128);
|
|
28
|
+
const idsSchema = z.array(idSchema).min(1).max(LIMITS.bulk);
|
|
29
|
+
/** One card placement; only the shape is validated here.
|
|
30
|
+
*
|
|
31
|
+
* The bounds are deliberately NOT enforced with `.min/.max`: a user who drags a
|
|
32
|
+
* card past the world edge should get a clamped position, not an error toast.
|
|
33
|
+
* The store clamps with `clampLayout`, which is the single place the policy
|
|
34
|
+
* lives; rejecting here made that clamp unreachable over RPC. Non-finite values
|
|
35
|
+
* are still refused, because there is no sensible clamp for NaN or Infinity.
|
|
36
|
+
*/
|
|
37
|
+
const layoutSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
x: z.number().finite(),
|
|
40
|
+
y: z.number().finite(),
|
|
41
|
+
rotate: z.number().finite(),
|
|
42
|
+
width: z.enum(WIDTHS),
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
45
|
+
const requestSchema = z.discriminatedUnion('action', [
|
|
46
|
+
z.object({ action: z.literal('list'), includeArchived: z.boolean().optional(), sort: sortSchema.optional() }).strict(),
|
|
47
|
+
z
|
|
48
|
+
.object({
|
|
49
|
+
action: z.literal('create'),
|
|
50
|
+
note: z
|
|
51
|
+
.object({
|
|
52
|
+
body: z.string().min(1).max(LIMITS.body),
|
|
53
|
+
title: z.string().max(LIMITS.title).optional(),
|
|
54
|
+
tags: z.array(z.string().max(LIMITS.tag)).max(LIMITS.tags).optional(),
|
|
55
|
+
color: colorSchema.optional(),
|
|
56
|
+
pinned: z.boolean().optional(),
|
|
57
|
+
layout: layoutSchema.nullable().optional(),
|
|
58
|
+
})
|
|
59
|
+
.strict(),
|
|
60
|
+
})
|
|
61
|
+
.strict(),
|
|
62
|
+
z
|
|
63
|
+
.object({
|
|
64
|
+
action: z.literal('update'),
|
|
65
|
+
id: idSchema,
|
|
66
|
+
patch: z
|
|
67
|
+
.object({
|
|
68
|
+
body: z.string().min(1).max(LIMITS.body).optional(),
|
|
69
|
+
title: z.string().max(LIMITS.title).optional(),
|
|
70
|
+
tags: z.array(z.string().max(LIMITS.tag)).max(LIMITS.tags).optional(),
|
|
71
|
+
color: colorSchema.optional(),
|
|
72
|
+
pinned: z.boolean().optional(),
|
|
73
|
+
archived: z.boolean().optional(),
|
|
74
|
+
})
|
|
75
|
+
.strict(),
|
|
76
|
+
})
|
|
77
|
+
.strict(),
|
|
78
|
+
z.object({ action: z.literal('archive'), ids: idsSchema, archived: z.boolean() }).strict(),
|
|
79
|
+
z.object({ action: z.literal('delete'), ids: idsSchema }).strict(),
|
|
80
|
+
z.object({ action: z.literal('clear-archived') }).strict(),
|
|
81
|
+
z.object({ action: z.literal('move'), id: idSchema, layout: layoutSchema }).strict(),
|
|
82
|
+
z.object({ action: z.literal('arrange') }).strict(),
|
|
83
|
+
]);
|
|
84
|
+
/** A JSON response with no-store caching; the wall is always live state. */
|
|
85
|
+
function json(body, status = 200) {
|
|
86
|
+
return Response.json(body, { status, headers: { 'cache-control': 'no-store' } });
|
|
87
|
+
}
|
|
88
|
+
/** One server-response success envelope. */
|
|
89
|
+
function success(rpcId, value) {
|
|
90
|
+
return json({ type: 'server-response', rpcId, result: { ok: true, value } });
|
|
91
|
+
}
|
|
92
|
+
/** One server-response failure envelope, using a stable plugin-owned code. */
|
|
93
|
+
function failure(rpcId, code, message, details = {}) {
|
|
94
|
+
return json({ type: 'server-response', rpcId, result: { ok: false, error: { code: `sticky-wall/${code}`, message, details } } });
|
|
95
|
+
}
|
|
96
|
+
/** Recover the correlation id from any body shape so failures stay correlatable. */
|
|
97
|
+
function correlation(body) {
|
|
98
|
+
return body !== null && typeof body === 'object' && 'rpcId' in body && typeof body.rpcId === 'string' ? body.rpcId : 'invalid-request';
|
|
99
|
+
}
|
|
100
|
+
/** Dispatch one validated request onto the store. */
|
|
101
|
+
async function dispatch(actions, request) {
|
|
102
|
+
switch (request.action) {
|
|
103
|
+
case 'list':
|
|
104
|
+
return { notes: actions.list(request.includeArchived, request.sort) };
|
|
105
|
+
case 'create':
|
|
106
|
+
return { action: 'create', changed: [await actions.create(request.note)], removed: [] };
|
|
107
|
+
case 'update':
|
|
108
|
+
return { action: 'update', changed: [await actions.update(request.id, request.patch)], removed: [] };
|
|
109
|
+
case 'archive':
|
|
110
|
+
return { action: 'archive', changed: await actions.setArchived(request.ids, request.archived), removed: [] };
|
|
111
|
+
case 'delete':
|
|
112
|
+
return { action: 'delete', changed: [], removed: await actions.remove(request.ids) };
|
|
113
|
+
case 'clear-archived':
|
|
114
|
+
return { action: 'clear-archived', changed: [], removed: await actions.clearArchived() };
|
|
115
|
+
case 'move':
|
|
116
|
+
return { action: 'move', changed: [await actions.move(request.id, request.layout)], removed: [] };
|
|
117
|
+
case 'arrange':
|
|
118
|
+
return { action: 'arrange', changed: await actions.arrange(), removed: [] };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Build the exact route Connection registers; every failure stays inside the
|
|
123
|
+
* envelope. `ready` resolves the store lazily so a storage-open failure is
|
|
124
|
+
* reported on the first request instead of at registration time.
|
|
125
|
+
*/
|
|
126
|
+
export function createExecuteRoute(ready) {
|
|
127
|
+
return {
|
|
128
|
+
path: EXECUTE_PATH,
|
|
129
|
+
methods: ['POST'],
|
|
130
|
+
requestBody: 'buffered',
|
|
131
|
+
async fetch(request) {
|
|
132
|
+
if (request.method !== 'POST' || new URL(request.url).pathname !== EXECUTE_PATH)
|
|
133
|
+
return new Response('not found', { status: 404 });
|
|
134
|
+
const contentType = request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase();
|
|
135
|
+
if (contentType !== 'application/json')
|
|
136
|
+
return new Response('content type must be application/json', { status: 415 });
|
|
137
|
+
let body;
|
|
138
|
+
try {
|
|
139
|
+
body = await request.json();
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
if (request.signal.aborted)
|
|
143
|
+
return failure(correlation(body), 'cancelled', 'The request was cancelled before dispatch.');
|
|
144
|
+
return new Response('body is not JSON', { status: 400 });
|
|
145
|
+
}
|
|
146
|
+
if (request.signal.aborted)
|
|
147
|
+
return failure(correlation(body), 'cancelled', 'The request was cancelled before dispatch.');
|
|
148
|
+
const envelope = envelopeSchema.safeParse(body);
|
|
149
|
+
if (!envelope.success)
|
|
150
|
+
return failure(correlation(body), 'bad-request', 'Invalid client-request message.');
|
|
151
|
+
const message = envelope.data;
|
|
152
|
+
if (message.method !== ENDPOINT)
|
|
153
|
+
return failure(message.rpcId, 'bad-request', 'RPC method does not match the sticky-wall endpoint.');
|
|
154
|
+
const parsed = requestSchema.safeParse(message.payload);
|
|
155
|
+
if (!parsed.success) {
|
|
156
|
+
const first = parsed.error.issues[0];
|
|
157
|
+
return failure(message.rpcId, 'bad-request', first?.message ?? 'Invalid sticky-wall request.', { issues: parsed.error.issues.slice(0, 4) });
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
request.signal.throwIfAborted();
|
|
161
|
+
return success(message.rpcId, await dispatch(await ready(), parsed.data));
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
// A WallError is authored for the user; anything else is a Host fault
|
|
165
|
+
// whose text must not become UI copy (and must not leak backend detail).
|
|
166
|
+
if (error instanceof WallError)
|
|
167
|
+
return failure(correlation(body), error.code, error.message.slice(0, 2000), error.details);
|
|
168
|
+
if (request.signal.aborted)
|
|
169
|
+
return failure(correlation(body), 'cancelled', 'The request was cancelled.');
|
|
170
|
+
return failure(correlation(body), 'internal', 'The note wall hit an internal error; check the Host log.');
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire and storage vocabulary shared by the Host and Client halves.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is plain, lossless JSON: the Host and the Client live in
|
|
5
|
+
* different runtimes and only these shapes cross the Connection RPC boundary.
|
|
6
|
+
* Both halves are compiled from this file, so the Client bundle carries the
|
|
7
|
+
* same literals without importing Host code.
|
|
8
|
+
*/
|
|
9
|
+
/** RPC endpoint path under the shared `/api` channel. */
|
|
10
|
+
export declare const ENDPOINT = "skillre-sticky-wall/execute";
|
|
11
|
+
/** Sidebar panel id, main panel key, and locale namespace. */
|
|
12
|
+
export declare const PANEL_ID = "skillre-sticky-wall";
|
|
13
|
+
export declare const LOCALE_NAMESPACE = "skillreStickyWall";
|
|
14
|
+
/** Storage domain name; the per-record layout gives one file per note. */
|
|
15
|
+
export declare const DOMAIN_NAME = "skillre_sticky_wall";
|
|
16
|
+
export declare const DOMAIN_VERSION = 1;
|
|
17
|
+
/** Hard limits, enforced on both halves so a bad payload never reaches storage. */
|
|
18
|
+
export declare const LIMITS: Readonly<{
|
|
19
|
+
/** One note's body, in characters. */
|
|
20
|
+
body: 4000;
|
|
21
|
+
/** One note's title, in characters. */
|
|
22
|
+
title: 120;
|
|
23
|
+
/** A note's free-form tag list. */
|
|
24
|
+
tags: 8;
|
|
25
|
+
/** One tag, in characters. */
|
|
26
|
+
tag: 32;
|
|
27
|
+
/** Live (non-archived) notes a wall may hold. */
|
|
28
|
+
notes: 400;
|
|
29
|
+
/** Note ids accepted in one request. */
|
|
30
|
+
bulk: 200;
|
|
31
|
+
}>;
|
|
32
|
+
/** Pastel note colors the wall offers; the Client renders them as literal hex. */
|
|
33
|
+
export declare const COLORS: readonly ["yellow", "pink", "green", "blue", "purple", "orange"];
|
|
34
|
+
export type NoteColor = (typeof COLORS)[number];
|
|
35
|
+
/** Sort orders the wall exposes. */
|
|
36
|
+
export declare const SORTS: readonly ["pinned", "created", "updated"];
|
|
37
|
+
export type NoteSort = (typeof SORTS)[number];
|
|
38
|
+
/**
|
|
39
|
+
* Card width presets.
|
|
40
|
+
*
|
|
41
|
+
* A free canvas looks dead when every card is the same width, so the wall keeps
|
|
42
|
+
* three sizes. They are presets rather than arbitrary pixels: the card's text
|
|
43
|
+
* metrics, the auto-arrange packing and the drag handles all assume a known
|
|
44
|
+
* width, and a preset keeps the stored layout small and legible.
|
|
45
|
+
*/
|
|
46
|
+
export declare const WIDTHS: readonly ["sm", "md", "lg"];
|
|
47
|
+
export type NoteWidth = (typeof WIDTHS)[number];
|
|
48
|
+
/** Rendered card width for each preset, in world pixels at zoom 1. */
|
|
49
|
+
export declare const WIDTH_PX: Readonly<Record<NoteWidth, number>>;
|
|
50
|
+
/** Bounds of the free canvas: a note may sit anywhere inside this square. */
|
|
51
|
+
export declare const LAYOUT_LIMITS: Readonly<{
|
|
52
|
+
/** Absolute value of x and y, in world pixels. */
|
|
53
|
+
coord: 20000;
|
|
54
|
+
/** Absolute rotation, in degrees. */
|
|
55
|
+
rotate: 12;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Where one note sits on the free canvas.
|
|
59
|
+
*
|
|
60
|
+
* `null` on a note means "never placed": the Client derives a stable preview
|
|
61
|
+
* position from the note id so a wall written before the canvas existed still
|
|
62
|
+
* opens as a scattered wall instead of a single stack. Dragging or arranging
|
|
63
|
+
* writes a real layout, so the Host stays the only owner of the arrangement.
|
|
64
|
+
*/
|
|
65
|
+
export interface NoteLayout {
|
|
66
|
+
/** World x of the card's top-left corner, in pixels at zoom 1. */
|
|
67
|
+
x: number;
|
|
68
|
+
/** World y of the card's top-left corner, in pixels at zoom 1. */
|
|
69
|
+
y: number;
|
|
70
|
+
/** Rotation in degrees, clamped to {@link LAYOUT_LIMITS.rotate}. */
|
|
71
|
+
rotate: number;
|
|
72
|
+
/** Card width preset. */
|
|
73
|
+
width: NoteWidth;
|
|
74
|
+
}
|
|
75
|
+
/** One sticky note as stored and as sent over the wire. */
|
|
76
|
+
export interface Note {
|
|
77
|
+
/** Stable id; `crypto.randomUUID()` for locally created notes. */
|
|
78
|
+
id: string;
|
|
79
|
+
/** Optional short heading; the body alone is enough for a quick capture. */
|
|
80
|
+
title: string;
|
|
81
|
+
/** The note text. Never empty for a live note. Markdown is rendered, not stored away. */
|
|
82
|
+
body: string;
|
|
83
|
+
/** Normalized lowercase tags, deduplicated, at most {@link LIMITS.tags}. */
|
|
84
|
+
tags: string[];
|
|
85
|
+
/** Pastel key from {@link COLORS}. */
|
|
86
|
+
color: NoteColor;
|
|
87
|
+
/** Pinned notes sort first and survive `archive` sweeps. */
|
|
88
|
+
pinned: boolean;
|
|
89
|
+
/** Archived notes are kept but hidden by the default wall. */
|
|
90
|
+
archived: boolean;
|
|
91
|
+
/** ISO-8601 creation time. */
|
|
92
|
+
createdAt: string;
|
|
93
|
+
/** ISO-8601 last-write time. Moving a card does not bump this. */
|
|
94
|
+
updatedAt: string;
|
|
95
|
+
/** Free-canvas placement, or `null` for a note the user never positioned. */
|
|
96
|
+
layout: NoteLayout | null;
|
|
97
|
+
}
|
|
98
|
+
/** The complete wall state a Client renders. */
|
|
99
|
+
export interface WallSnapshot {
|
|
100
|
+
notes: Note[];
|
|
101
|
+
/** Schema marker so a Client never renders an unknown future shape. */
|
|
102
|
+
domain: {
|
|
103
|
+
name: string;
|
|
104
|
+
version: number;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Request kinds accepted by the Host endpoint. */
|
|
108
|
+
export type WallRequest = {
|
|
109
|
+
action: 'list';
|
|
110
|
+
includeArchived?: boolean;
|
|
111
|
+
sort?: NoteSort;
|
|
112
|
+
} | {
|
|
113
|
+
action: 'create';
|
|
114
|
+
note: NoteInput;
|
|
115
|
+
} | {
|
|
116
|
+
action: 'update';
|
|
117
|
+
id: string;
|
|
118
|
+
patch: NotePatch;
|
|
119
|
+
} | {
|
|
120
|
+
action: 'archive';
|
|
121
|
+
ids: string[];
|
|
122
|
+
archived: boolean;
|
|
123
|
+
} | {
|
|
124
|
+
action: 'delete';
|
|
125
|
+
ids: string[];
|
|
126
|
+
} | {
|
|
127
|
+
action: 'clear-archived';
|
|
128
|
+
} | {
|
|
129
|
+
action: 'move';
|
|
130
|
+
id: string;
|
|
131
|
+
layout: NoteLayout;
|
|
132
|
+
} | {
|
|
133
|
+
action: 'arrange';
|
|
134
|
+
};
|
|
135
|
+
/** The fields a Client may submit when creating a note. */
|
|
136
|
+
export interface NoteInput {
|
|
137
|
+
body: string;
|
|
138
|
+
title?: string;
|
|
139
|
+
tags?: string[];
|
|
140
|
+
color?: NoteColor;
|
|
141
|
+
pinned?: boolean;
|
|
142
|
+
layout?: NoteLayout | null;
|
|
143
|
+
}
|
|
144
|
+
/** The fields a Client may change on an existing note. */
|
|
145
|
+
export interface NotePatch {
|
|
146
|
+
body?: string;
|
|
147
|
+
title?: string;
|
|
148
|
+
tags?: string[];
|
|
149
|
+
color?: NoteColor;
|
|
150
|
+
pinned?: boolean;
|
|
151
|
+
archived?: boolean;
|
|
152
|
+
}
|
|
153
|
+
/** One endpoint outcome, mirroring the Connection RPC result envelope. */
|
|
154
|
+
export type WallResult<T> = {
|
|
155
|
+
ok: true;
|
|
156
|
+
value: T;
|
|
157
|
+
} | {
|
|
158
|
+
ok: false;
|
|
159
|
+
error: {
|
|
160
|
+
code: string;
|
|
161
|
+
message: string;
|
|
162
|
+
details: Record<string, unknown>;
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
/** Result of one mutation, echoed back so the Client can reconcile in place. */
|
|
166
|
+
export interface MutationResult {
|
|
167
|
+
action: string;
|
|
168
|
+
/** Every note the mutation created or changed, in its post-write state. */
|
|
169
|
+
changed: Note[];
|
|
170
|
+
/** Ids the mutation removed. */
|
|
171
|
+
removed: string[];
|
|
172
|
+
}
|
package/lib/types.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire and storage vocabulary shared by the Host and Client halves.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is plain, lossless JSON: the Host and the Client live in
|
|
5
|
+
* different runtimes and only these shapes cross the Connection RPC boundary.
|
|
6
|
+
* Both halves are compiled from this file, so the Client bundle carries the
|
|
7
|
+
* same literals without importing Host code.
|
|
8
|
+
*/
|
|
9
|
+
/** RPC endpoint path under the shared `/api` channel. */
|
|
10
|
+
export const ENDPOINT = 'skillre-sticky-wall/execute';
|
|
11
|
+
/** Sidebar panel id, main panel key, and locale namespace. */
|
|
12
|
+
export const PANEL_ID = 'skillre-sticky-wall';
|
|
13
|
+
export const LOCALE_NAMESPACE = 'skillreStickyWall';
|
|
14
|
+
/** Storage domain name; the per-record layout gives one file per note. */
|
|
15
|
+
export const DOMAIN_NAME = 'skillre_sticky_wall';
|
|
16
|
+
export const DOMAIN_VERSION = 1;
|
|
17
|
+
/** Hard limits, enforced on both halves so a bad payload never reaches storage. */
|
|
18
|
+
export const LIMITS = Object.freeze({
|
|
19
|
+
/** One note's body, in characters. */
|
|
20
|
+
body: 4000,
|
|
21
|
+
/** One note's title, in characters. */
|
|
22
|
+
title: 120,
|
|
23
|
+
/** A note's free-form tag list. */
|
|
24
|
+
tags: 8,
|
|
25
|
+
/** One tag, in characters. */
|
|
26
|
+
tag: 32,
|
|
27
|
+
/** Live (non-archived) notes a wall may hold. */
|
|
28
|
+
notes: 400,
|
|
29
|
+
/** Note ids accepted in one request. */
|
|
30
|
+
bulk: 200,
|
|
31
|
+
});
|
|
32
|
+
/** Pastel note colors the wall offers; the Client renders them as literal hex. */
|
|
33
|
+
export const COLORS = Object.freeze([
|
|
34
|
+
'yellow',
|
|
35
|
+
'pink',
|
|
36
|
+
'green',
|
|
37
|
+
'blue',
|
|
38
|
+
'purple',
|
|
39
|
+
'orange',
|
|
40
|
+
]);
|
|
41
|
+
/** Sort orders the wall exposes. */
|
|
42
|
+
export const SORTS = Object.freeze(['pinned', 'created', 'updated']);
|
|
43
|
+
/**
|
|
44
|
+
* Card width presets.
|
|
45
|
+
*
|
|
46
|
+
* A free canvas looks dead when every card is the same width, so the wall keeps
|
|
47
|
+
* three sizes. They are presets rather than arbitrary pixels: the card's text
|
|
48
|
+
* metrics, the auto-arrange packing and the drag handles all assume a known
|
|
49
|
+
* width, and a preset keeps the stored layout small and legible.
|
|
50
|
+
*/
|
|
51
|
+
export const WIDTHS = Object.freeze(['sm', 'md', 'lg']);
|
|
52
|
+
/** Rendered card width for each preset, in world pixels at zoom 1. */
|
|
53
|
+
export const WIDTH_PX = Object.freeze({
|
|
54
|
+
sm: 178,
|
|
55
|
+
md: 222,
|
|
56
|
+
lg: 276,
|
|
57
|
+
});
|
|
58
|
+
/** Bounds of the free canvas: a note may sit anywhere inside this square. */
|
|
59
|
+
export const LAYOUT_LIMITS = Object.freeze({
|
|
60
|
+
/** Absolute value of x and y, in world pixels. */
|
|
61
|
+
coord: 20_000,
|
|
62
|
+
/** Absolute rotation, in degrees. */
|
|
63
|
+
rotate: 12,
|
|
64
|
+
});
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,118 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skillre/dsh-plugin-sticky-wall",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "便签墙:DSH 侧栏的自由画布便签板,可拖动摆放、一键整理,正文按 Markdown 渲染",
|
|
5
|
+
"icon": "./icon.svg",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "skillre",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": "^22.19.0 || >=24.0.0",
|
|
11
|
+
"dsh": ">=0.2.2-rc.1 <0.3.0"
|
|
12
|
+
},
|
|
13
|
+
"packageManager": "npm@11.12.1",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
22
|
+
"./compatibility.json": "./compatibility.json",
|
|
23
|
+
"./locale/*.json": "./locale/*.json",
|
|
24
|
+
"./package.json": "./package.json",
|
|
25
|
+
"./client": {
|
|
26
|
+
"browser": "./lib/client.js",
|
|
27
|
+
"default": "./lib/client.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"lib/",
|
|
32
|
+
"cordis.patch.yml",
|
|
33
|
+
"compatibility.json",
|
|
34
|
+
"locale/*.json",
|
|
35
|
+
"icon.svg",
|
|
36
|
+
"docs/DETAIL.md",
|
|
37
|
+
"docs/DETAIL.zh.md",
|
|
38
|
+
"docs/preview.svg",
|
|
39
|
+
"docs/preview.zh.svg",
|
|
40
|
+
"docs/PREVIEW.md",
|
|
41
|
+
"README.md",
|
|
42
|
+
"README.en.md",
|
|
43
|
+
"UNINSTALL.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/skillre/dsh-plugin-sticky-wall.git"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/skillre/dsh-plugin-sticky-wall/issues"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/skillre/dsh-plugin-sticky-wall#readme",
|
|
57
|
+
"keywords": [
|
|
58
|
+
"deepseek-harness",
|
|
59
|
+
"dsh-plugin",
|
|
60
|
+
"cordis",
|
|
61
|
+
"sticky-notes",
|
|
62
|
+
"notes",
|
|
63
|
+
"便签"
|
|
64
|
+
],
|
|
65
|
+
"dsh": {
|
|
66
|
+
"manifestVersion": 1,
|
|
67
|
+
"bundle": {
|
|
68
|
+
"patch": "./cordis.patch.yml"
|
|
69
|
+
},
|
|
70
|
+
"client": {
|
|
71
|
+
"platform": "web",
|
|
72
|
+
"immediately": true,
|
|
73
|
+
"inject": [
|
|
74
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
75
|
+
"@deepseek-ai/dsh-client-locale",
|
|
76
|
+
"@deepseek-ai/dsh-client-connection"
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"scripts": {
|
|
81
|
+
"clean": "node scripts/clean.mjs",
|
|
82
|
+
"build": "npm run clean && tsc -p tsconfig.build.json && node scripts/build-client.mjs",
|
|
83
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
84
|
+
"test": "npm run build:client && vitest run",
|
|
85
|
+
"pack:check": "npm run build && publint && npm pack --dry-run --json --ignore-scripts",
|
|
86
|
+
"check": "npm run preview:check && npm run typecheck && npm test && npm run check:lib && npm run pack:check",
|
|
87
|
+
"changeset": "changeset",
|
|
88
|
+
"version": "changeset version",
|
|
89
|
+
"release:prepare": "node scripts/prepare-release.mjs",
|
|
90
|
+
"prepack": "npm run build",
|
|
91
|
+
"build:client": "node scripts/build-client.mjs",
|
|
92
|
+
"test:integration": "node scripts/verify-isolated.mjs",
|
|
93
|
+
"test:market": "node scripts/verify-market-install.mjs",
|
|
94
|
+
"check:lib": "npm run build && node scripts/verify-lib-committed.mjs",
|
|
95
|
+
"preview:check": "node scripts/preview.mjs check"
|
|
96
|
+
},
|
|
97
|
+
"peerDependencies": {
|
|
98
|
+
"@deepseek-ai/cordis": "~4.1.0"
|
|
99
|
+
},
|
|
100
|
+
"devDependencies": {
|
|
101
|
+
"@changesets/cli": "^3.0.2",
|
|
102
|
+
"@deepseek-ai/cordis": "4.1.0",
|
|
103
|
+
"@testing-library/react": "^16.3.3",
|
|
104
|
+
"@types/node": "^24.13.3",
|
|
105
|
+
"@types/react": "~18.3.31",
|
|
106
|
+
"esbuild": "^0.25.0",
|
|
107
|
+
"jsdom": "^25.0.1",
|
|
108
|
+
"publint": "^0.3.24",
|
|
109
|
+
"react": "18.3.1",
|
|
110
|
+
"react-dom": "^18.3.1",
|
|
111
|
+
"typescript": "5.9.3",
|
|
112
|
+
"vitest": "^4.1.8"
|
|
113
|
+
},
|
|
114
|
+
"dependencies": {
|
|
115
|
+
"@deepseek-ai/schemastery": "~3.19.0",
|
|
116
|
+
"zod": "^4.6.7"
|
|
117
|
+
}
|
|
118
|
+
}
|