@grapecity-software/dsh-spreadjs-editor 0.1.0
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.md +229 -0
- package/cordis.patch.yml +22 -0
- package/lib/index.js +45 -0
- package/package.json +111 -0
- package/scripts/build.mjs +31 -0
- package/scripts/smoke-client.mjs +47 -0
- package/scripts/smoke-node.mjs +88 -0
- package/scripts/update-grapecity.mjs +34 -0
- package/src/augment.ts +11 -0
- package/src/client/SpreadsheetHost.tsx +420 -0
- package/src/client/SpreadsheetViewer.tsx +92 -0
- package/src/client/better-sidebar.ts +98 -0
- package/src/client/global.d.ts +5 -0
- package/src/client/index.ts +50 -0
- package/src/client/styles.ts +27 -0
- package/src/client/viewer.css +212 -0
- package/src/index.ts +67 -0
- package/tsconfig.json +22 -0
- package/tsdown.config.ts +110 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/* dsh-spreadjs-editor panel styles */
|
|
2
|
+
|
|
3
|
+
.dsh-spreadjs-panel {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
width: 100%;
|
|
7
|
+
height: 100%;
|
|
8
|
+
min-height: 0;
|
|
9
|
+
overflow: hidden;
|
|
10
|
+
background: var(--dsw-bg, #ffffff);
|
|
11
|
+
color: var(--dsw-fg, #1f2329);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.dsh-spreadjs-header {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
flex-wrap: wrap;
|
|
18
|
+
gap: 10px;
|
|
19
|
+
row-gap: 8px;
|
|
20
|
+
padding: 10px 14px;
|
|
21
|
+
border-bottom: 1px solid var(--dsw-border, #d5d8de);
|
|
22
|
+
background: var(--dsw-bg-subtle, #f7f8fa);
|
|
23
|
+
flex: 0 0 auto;
|
|
24
|
+
min-width: 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.dsh-spreadjs-title {
|
|
28
|
+
font-size: 14px;
|
|
29
|
+
font-weight: 600;
|
|
30
|
+
margin: 0;
|
|
31
|
+
min-width: 0;
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
text-overflow: ellipsis;
|
|
34
|
+
white-space: nowrap;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.dsh-spreadjs-spacer {
|
|
38
|
+
flex: 1 1 auto;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.dsh-spreadjs-btn {
|
|
42
|
+
appearance: none;
|
|
43
|
+
border: 1px solid var(--dsw-border, #c9cdd4);
|
|
44
|
+
background: var(--dsw-bg, #ffffff);
|
|
45
|
+
color: inherit;
|
|
46
|
+
border-radius: 6px;
|
|
47
|
+
padding: 5px 12px;
|
|
48
|
+
font-size: 12px;
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
white-space: nowrap;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.dsh-spreadjs-btn:hover:not(:disabled) {
|
|
54
|
+
background: var(--dsw-bg-hover, #eef0f3);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.dsh-spreadjs-btn:disabled {
|
|
58
|
+
opacity: 0.45;
|
|
59
|
+
cursor: default;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.dsh-spreadjs-btn-primary {
|
|
63
|
+
background: #3b6ef6;
|
|
64
|
+
border-color: #3b6ef6;
|
|
65
|
+
color: #fff;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.dsh-spreadjs-btn-primary:hover:not(:disabled) {
|
|
69
|
+
background: #2f5ce0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.dsh-spreadjs-export-select {
|
|
73
|
+
max-width: 110px;
|
|
74
|
+
padding: 4px 6px;
|
|
75
|
+
font-size: 12px;
|
|
76
|
+
border: 1px solid var(--dsw-border, #c9cdd4);
|
|
77
|
+
border-radius: 6px;
|
|
78
|
+
background: var(--dsw-bg, #ffffff);
|
|
79
|
+
color: inherit;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.dsh-spreadjs-editor {
|
|
83
|
+
flex: 1 1 auto;
|
|
84
|
+
min-width: 0;
|
|
85
|
+
min-height: 0;
|
|
86
|
+
display: flex;
|
|
87
|
+
flex-direction: column;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.dsh-spreadjs-host {
|
|
91
|
+
flex: 1 1 auto;
|
|
92
|
+
min-height: 0;
|
|
93
|
+
position: relative;
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.dsh-spreadjs-host > div {
|
|
99
|
+
flex: 1 1 auto;
|
|
100
|
+
min-height: 0;
|
|
101
|
+
width: 100%;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.dsh-spreadjs-empty {
|
|
105
|
+
position: absolute;
|
|
106
|
+
inset: 0;
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
justify-content: center;
|
|
110
|
+
color: var(--dsw-fg-muted, #8a919c);
|
|
111
|
+
font-size: 13px;
|
|
112
|
+
background: var(--dsw-bg, #ffffff);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.dsh-spreadjs-loading {
|
|
116
|
+
position: absolute;
|
|
117
|
+
inset: 0;
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
justify-content: center;
|
|
121
|
+
background: rgba(255, 255, 255, 0.72);
|
|
122
|
+
color: #4b5563;
|
|
123
|
+
font-size: 13px;
|
|
124
|
+
z-index: 5;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.dsh-spreadjs-error {
|
|
128
|
+
position: absolute;
|
|
129
|
+
inset: 0;
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
justify-content: center;
|
|
133
|
+
color: #c0392b;
|
|
134
|
+
font-size: 13px;
|
|
135
|
+
padding: 24px;
|
|
136
|
+
text-align: center;
|
|
137
|
+
z-index: 5;
|
|
138
|
+
background: var(--dsw-bg, #ffffff);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.dsh-spreadjs-statusbar {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
gap: 8px;
|
|
145
|
+
min-height: 28px;
|
|
146
|
+
padding: 4px 12px;
|
|
147
|
+
border-top: 1px solid var(--dsw-border, #d5d8de);
|
|
148
|
+
background: var(--dsw-bg-subtle, #fafbfc);
|
|
149
|
+
color: var(--dsw-fg-muted, #6b7280);
|
|
150
|
+
font-size: 12px;
|
|
151
|
+
flex: 0 0 auto;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.dsh-spreadjs-status-error {
|
|
155
|
+
color: #c0392b;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.dsh-spreadjs-status-busy {
|
|
159
|
+
color: #1a56db;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* Follow the OS light/dark preference. The Designer chrome is themed through
|
|
163
|
+
GC.Spread.Sheets.Designer.setTheme(); these rules are a fallback for the
|
|
164
|
+
Designer container and the spreadsheet canvas so it stays readable in dark. */
|
|
165
|
+
@media (prefers-color-scheme: dark) {
|
|
166
|
+
.dsh-spreadjs-panel {
|
|
167
|
+
color-scheme: dark;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.dsh-spreadjs-header {
|
|
171
|
+
background: #1e2227;
|
|
172
|
+
border-color: #343a42;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.dsh-spreadjs-btn {
|
|
176
|
+
background: #262b32;
|
|
177
|
+
border-color: #3a414a;
|
|
178
|
+
color: #e8eaed;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.dsh-spreadjs-btn:hover:not(:disabled) {
|
|
182
|
+
background: #2c3138;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.dsh-spreadjs-export-select {
|
|
186
|
+
background: #262b32;
|
|
187
|
+
border-color: #3a414a;
|
|
188
|
+
color: #e8eaed;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.dsh-spreadjs-loading {
|
|
192
|
+
background: rgba(20, 22, 26, 0.72);
|
|
193
|
+
color: #c7ccd3;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.dsh-spreadjs-error {
|
|
197
|
+
background: #171a1f;
|
|
198
|
+
color: #f0a0a0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.dsh-spreadjs-host,
|
|
202
|
+
.dsh-spreadjs-host .gc-designer-container,
|
|
203
|
+
.dsh-spreadjs-host .spread-sheets,
|
|
204
|
+
.dsh-spreadjs-host .sheet-container,
|
|
205
|
+
.dsh-spreadjs-host .rowHeader,
|
|
206
|
+
.dsh-spreadjs-host .colHeader,
|
|
207
|
+
.dsh-spreadjs-host .hscrollbar,
|
|
208
|
+
.dsh-spreadjs-host .vscrollbar {
|
|
209
|
+
background-color: #1e2227;
|
|
210
|
+
color: #e8eaed;
|
|
211
|
+
}
|
|
212
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-spreadjs-editor — node half.
|
|
3
|
+
*
|
|
4
|
+
* Registers the `/spreadjs` prefix route on the harness web server. The
|
|
5
|
+
* better-sidebar browser half reads and writes files through ui-all's session
|
|
6
|
+
* routes; this host half only supplies the SpreadJS license/config handshake.
|
|
7
|
+
*
|
|
8
|
+
* Security posture: the server binds to 127.0.0.1 by default and this plugin
|
|
9
|
+
* adds no external exposure of its own.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
13
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
14
|
+
|
|
15
|
+
export const name = 'dsh-spreadjs-editor'
|
|
16
|
+
|
|
17
|
+
/** Required service: the web server whose route table this plugin extends. */
|
|
18
|
+
export const inject = ['webServer']
|
|
19
|
+
|
|
20
|
+
/** Plugin configuration (patch layer). */
|
|
21
|
+
export interface Config {
|
|
22
|
+
/** SpreadJS license key; empty runs the evaluation build. @default '' */
|
|
23
|
+
licenseKey?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sendJson(res: ServerResponse, status: number, body: unknown): void {
|
|
27
|
+
const data = JSON.stringify(body)
|
|
28
|
+
res.writeHead(status, {
|
|
29
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
30
|
+
'Content-Length': Buffer.byteLength(data),
|
|
31
|
+
'Cache-Control': 'no-store',
|
|
32
|
+
})
|
|
33
|
+
res.end(data)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readPathname(req: IncomingMessage): string {
|
|
37
|
+
return new URL(req.url ?? '/', 'http://localhost').pathname
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function apply(ctx: Context, config: Config = {}): void {
|
|
41
|
+
const webServer = ctx.webServer
|
|
42
|
+
|
|
43
|
+
const disposer = webServer.register({
|
|
44
|
+
kind: 'prefix',
|
|
45
|
+
path: '/spreadjs',
|
|
46
|
+
handler: (req: IncomingMessage, res: ServerResponse): void => {
|
|
47
|
+
try {
|
|
48
|
+
const pathname = readPathname(req)
|
|
49
|
+
if (pathname === '/spreadjs/api/health') {
|
|
50
|
+
sendJson(res, 200, { ok: true })
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
if (pathname === '/spreadjs/api/config') {
|
|
54
|
+
sendJson(res, 200, { licenseKey: config.licenseKey ?? '' })
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
sendJson(res, 404, { error: 'unknown endpoint' })
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
60
|
+
if (!res.headersSent) sendJson(res, 500, { error: message })
|
|
61
|
+
else res.destroy()
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
ctx.effect(() => disposer, 'dsh-spreadjs-editor: /spreadjs routes')
|
|
67
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true,
|
|
10
|
+
"noImplicitOverride": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"verbatimModuleSyntax": true,
|
|
17
|
+
"allowImportingTsExtensions": true,
|
|
18
|
+
"noEmit": true,
|
|
19
|
+
"types": ["node"]
|
|
20
|
+
},
|
|
21
|
+
"include": ["src", "tests"]
|
|
22
|
+
}
|
package/tsdown.config.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tsdown build for dsh-spreadjs-editor.
|
|
3
|
+
*
|
|
4
|
+
* Two artifacts (aligned with the harness client bundle preset):
|
|
5
|
+
* - lib/index.js — node half (ESM). Runs on the harness host: registers the
|
|
6
|
+
* /spreadjs file bridge on ctx.webServer. No runtime dependencies beyond
|
|
7
|
+
* node builtins (dsh services are injected, not imported).
|
|
8
|
+
* - lib/client.js — browser half (CJS closure factory). Loaded by the
|
|
9
|
+
* client-modules system through window.__ModuleLoader__.load({id, factory}).
|
|
10
|
+
* React resolves from the module table and stays external; SpreadJS and
|
|
11
|
+
* the stylesheets are inlined into the bundle.
|
|
12
|
+
*/
|
|
13
|
+
import { readFile } from 'node:fs/promises'
|
|
14
|
+
import type { UserConfig } from 'tsdown'
|
|
15
|
+
|
|
16
|
+
const PKG_ID = 'dsh-spreadjs-editor'
|
|
17
|
+
|
|
18
|
+
/** Module-table specifiers the browser bundle resolves at runtime. */
|
|
19
|
+
const CLIENT_EXTERNALS = new Set([
|
|
20
|
+
'react',
|
|
21
|
+
'react/jsx-runtime',
|
|
22
|
+
'react-dom',
|
|
23
|
+
'react-dom/client',
|
|
24
|
+
'@deepseek-ai/cordis',
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
/** Inline any `*.css` import as a raw string module (injected by styles.ts). */
|
|
28
|
+
function cssAsText() {
|
|
29
|
+
const PREFIX = '\0dsh-spreadjs-css:'
|
|
30
|
+
const SUFFIX = '.mjs'
|
|
31
|
+
return {
|
|
32
|
+
name: 'dsh-spreadjs-css-as-text',
|
|
33
|
+
async resolveId(this: any, source: string, importer: string | undefined) {
|
|
34
|
+
if (!source.endsWith('.css') || importer === undefined) return null
|
|
35
|
+
const resolved = await this.resolve(source, importer, { skipSelf: true })
|
|
36
|
+
if (resolved === null) return null
|
|
37
|
+
const fileId = resolved.id
|
|
38
|
+
this.addWatchFile(fileId)
|
|
39
|
+
// Virtual id must NOT end in `.css` or tsdown's css-guard intercepts it.
|
|
40
|
+
return PREFIX + fileId + SUFFIX
|
|
41
|
+
},
|
|
42
|
+
async load(this: any, id: string) {
|
|
43
|
+
if (!id.startsWith(PREFIX)) return null
|
|
44
|
+
const fileId = id.slice(PREFIX.length, -SUFFIX.length)
|
|
45
|
+
const source = await readFile(fileId, 'utf8')
|
|
46
|
+
return `export default ${JSON.stringify(source)}`
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const nodeHalf: UserConfig = {
|
|
52
|
+
name: PKG_ID,
|
|
53
|
+
entry: { index: 'src/index.ts' },
|
|
54
|
+
outDir: 'lib',
|
|
55
|
+
format: ['esm'],
|
|
56
|
+
platform: 'node',
|
|
57
|
+
target: 'es2022',
|
|
58
|
+
fixedExtension: false,
|
|
59
|
+
dts: false,
|
|
60
|
+
clean: true,
|
|
61
|
+
sourcemap: true,
|
|
62
|
+
deps: {
|
|
63
|
+
// Relative sources inline; node: builtins stay external; nothing else
|
|
64
|
+
// (the node half imports no bare runtime deps — dsh services are injected).
|
|
65
|
+
neverBundle: specifier => specifier.startsWith('node:'),
|
|
66
|
+
alwaysBundle: specifier => specifier.startsWith('.'),
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const clientHalf: UserConfig = {
|
|
71
|
+
name: `${PKG_ID}/client`,
|
|
72
|
+
entry: { client: 'src/client/index.ts' },
|
|
73
|
+
outDir: 'lib',
|
|
74
|
+
format: 'cjs',
|
|
75
|
+
platform: 'browser',
|
|
76
|
+
target: 'es2022',
|
|
77
|
+
fixedExtension: false,
|
|
78
|
+
dts: false,
|
|
79
|
+
clean: false,
|
|
80
|
+
sourcemap: true,
|
|
81
|
+
deps: {
|
|
82
|
+
// Requested module-table rows stay imports; everything else inlines.
|
|
83
|
+
neverBundle: specifier => CLIENT_EXTERNALS.has(specifier),
|
|
84
|
+
alwaysBundle: specifier => !CLIENT_EXTERNALS.has(specifier),
|
|
85
|
+
},
|
|
86
|
+
// Dual-mode libs resolve their browser flavor; inline deps that read
|
|
87
|
+
// process.env.NODE_ENV / import.meta.env.MODE need the substitutions.
|
|
88
|
+
inputOptions: {
|
|
89
|
+
resolve: {
|
|
90
|
+
conditionNames: [
|
|
91
|
+
(process.env.NODE_ENV ?? 'production') === 'development' ? 'development' : 'production',
|
|
92
|
+
'browser', 'import', 'module', 'default',
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
define: {
|
|
97
|
+
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'production'),
|
|
98
|
+
'import.meta.env.MODE': JSON.stringify(process.env.NODE_ENV ?? 'production'),
|
|
99
|
+
'import.meta.env': JSON.stringify({ MODE: process.env.NODE_ENV ?? 'production' }),
|
|
100
|
+
},
|
|
101
|
+
plugins: [cssAsText()],
|
|
102
|
+
outputOptions: {
|
|
103
|
+
entryFileNames: 'client.js',
|
|
104
|
+
banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(PKG_ID)}, factory: (require) => {`,
|
|
105
|
+
footer: 'return module.exports; } });',
|
|
106
|
+
intro: 'var module = { exports: {} }; var exports = module.exports;',
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default [nodeHalf, clientHalf]
|