@jseeio/jsee 0.4.2 → 0.8.1

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +21 -0
  3. package/README.md +583 -55
  4. package/dist/2b3e1faf89f94a483539.png +0 -0
  5. package/dist/416d91365b44e4b4f477.png +0 -0
  6. package/dist/8f2c4d11474275fbc161.png +0 -0
  7. package/dist/jsee.core.js +1 -0
  8. package/dist/jsee.full.js +1 -0
  9. package/dist/jsee.runtime.js +2 -1
  10. package/package.json +84 -18
  11. package/src/app.js +127 -32
  12. package/src/browser-bundle-node.js +9 -0
  13. package/src/cli.js +479 -67
  14. package/src/extended-imports.js +11 -0
  15. package/src/main.js +232 -44
  16. package/src/overlay.js +26 -1
  17. package/src/utils.js +386 -16
  18. package/templates/common-inputs.js +88 -0
  19. package/templates/common-outputs.js +340 -4
  20. package/templates/minimal-app.vue +367 -0
  21. package/templates/minimal-input.vue +573 -0
  22. package/templates/minimal-output.vue +426 -0
  23. package/templates/virtual-table.vue +194 -0
  24. package/.claude/settings.local.json +0 -15
  25. package/.eslintrc.js +0 -38
  26. package/AGENTS.md +0 -65
  27. package/CLAUDE.md +0 -5
  28. package/CNAME +0 -1
  29. package/_config.yml +0 -26
  30. package/dist/jsee.js +0 -1
  31. package/dump.sh +0 -23
  32. package/jest-puppeteer.config.js +0 -14
  33. package/jest.config.js +0 -8
  34. package/jest.unit.config.js +0 -8
  35. package/jsee.dump.txt +0 -5459
  36. package/load/index.html +0 -52
  37. package/templates/bulma-app.vue +0 -242
  38. package/templates/bulma-input.vue +0 -125
  39. package/templates/bulma-output.vue +0 -101
  40. package/test/arrow-main.html +0 -18
  41. package/test/arrow-worker.html +0 -18
  42. package/test/class.html +0 -22
  43. package/test/code.html +0 -25
  44. package/test/codew.html +0 -25
  45. package/test/example-class.js +0 -8
  46. package/test/example-sum.js +0 -3
  47. package/test/fixtures/lodash-like.js +0 -15
  48. package/test/fixtures/upload-sample.csv +0 -3
  49. package/test/importw.html +0 -28
  50. package/test/minimal.html +0 -14
  51. package/test/minimal1.html +0 -13
  52. package/test/minimal2.html +0 -15
  53. package/test/minimal3.html +0 -10
  54. package/test/minimal4.html +0 -22
  55. package/test/pipeline.html +0 -52
  56. package/test/python.html +0 -41
  57. package/test/runtime-arrow.html +0 -18
  58. package/test/string.html +0 -26
  59. package/test/stringw.html +0 -29
  60. package/test/sum.schema.json +0 -17
  61. package/test/sumw.schema.json +0 -15
  62. package/test/test-basic.test.js +0 -630
  63. package/test/test-python.test.js +0 -23
  64. package/test/unit/cli-fetch.test.js +0 -229
  65. package/test/unit/utils.test.js +0 -908
  66. package/webpack.config.js +0 -101
@@ -0,0 +1,426 @@
1
+ <style scoped>
2
+ .jsee-output-card {
3
+ border: 1px solid var(--jsee-border, #e0e0e0);
4
+ border-radius: var(--jsee-radius, 6px);
5
+ background: var(--jsee-card-bg, #fff);
6
+ margin-bottom: 16px;
7
+ }
8
+ .jsee-output-header {
9
+ display: flex;
10
+ justify-content: space-between;
11
+ align-items: center;
12
+ padding: 8px 12px;
13
+ border-bottom: 1px solid var(--jsee-border, #f0f0f0);
14
+ }
15
+ .jsee-output-title {
16
+ font-size: 14px;
17
+ font-weight: 400;
18
+ margin: 0;
19
+ color: var(--jsee-text, #333);
20
+ }
21
+ .jsee-output-actions {
22
+ display: flex;
23
+ gap: 4px;
24
+ }
25
+ .jsee-output-actions button {
26
+ padding: 2px 8px;
27
+ border: none;
28
+ background: none;
29
+ cursor: pointer;
30
+ font-size: 12px;
31
+ color: var(--jsee-text-secondary, #666);
32
+ border-radius: 3px;
33
+ }
34
+ .jsee-output-actions button:hover {
35
+ background: var(--jsee-bg-secondary, #f0f0f0);
36
+ color: var(--jsee-text, #333);
37
+ }
38
+ .jsee-output-body {
39
+ padding: 12px;
40
+ overflow: auto;
41
+ color: var(--jsee-text, #333);
42
+ }
43
+ .jsee-output-body pre {
44
+ margin: 0;
45
+ white-space: pre-wrap;
46
+ word-wrap: break-word;
47
+ font-size: 13px;
48
+ }
49
+
50
+ .is-fullscreen {
51
+ position: fixed;
52
+ inset: 0;
53
+ z-index: 9999;
54
+ width: 100vw;
55
+ height: 100vh;
56
+ background: var(--jsee-card-bg, #fff);
57
+ display: flex;
58
+ flex-direction: column;
59
+ }
60
+ .is-fullscreen .jsee-output-body {
61
+ flex: 1 1 auto;
62
+ overflow: auto;
63
+ }
64
+ .is-fullscreen .jsee-output-body, .is-fullscreen .custom-container {
65
+ height: 100% !important;
66
+ }
67
+ .jsee-tabs-header {
68
+ display: flex;
69
+ border-bottom: 2px solid var(--jsee-border, #e0e0e0);
70
+ margin-bottom: 8px;
71
+ }
72
+ .jsee-tab-btn {
73
+ padding: 6px 14px;
74
+ border: none;
75
+ background: none;
76
+ cursor: pointer;
77
+ font-size: 12px;
78
+ color: var(--jsee-text-secondary, #666);
79
+ border-bottom: 2px solid transparent;
80
+ margin-bottom: -2px;
81
+ }
82
+ .jsee-tab-btn.active {
83
+ color: var(--jsee-primary, #00d1b2);
84
+ border-bottom-color: var(--jsee-primary, #00d1b2);
85
+ }
86
+ .jsee-tab-btn:hover { color: var(--jsee-text, #333); }
87
+ .jsee-file-output { text-align: center; }
88
+ .jsee-file-download-btn {
89
+ padding: 8px 20px;
90
+ border: 1px solid var(--jsee-border, #e0e0e0);
91
+ border-radius: 4px;
92
+ background: var(--jsee-bg-secondary, #f5f5f5);
93
+ cursor: pointer;
94
+ font-size: 13px;
95
+ color: var(--jsee-text, #333);
96
+ }
97
+ .jsee-file-download-btn:hover {
98
+ background: var(--jsee-border, #e0e0e0);
99
+ }
100
+ .jsee-chat-messages {
101
+ max-height: 400px;
102
+ overflow-y: auto;
103
+ display: flex;
104
+ flex-direction: column;
105
+ gap: 8px;
106
+ padding: 4px 0;
107
+ }
108
+ .jsee-chat-msg {
109
+ display: flex;
110
+ }
111
+ .jsee-chat-user {
112
+ justify-content: flex-end;
113
+ }
114
+ .jsee-chat-assistant {
115
+ justify-content: flex-start;
116
+ }
117
+ .jsee-chat-bubble {
118
+ max-width: 80%;
119
+ padding: 8px 12px;
120
+ border-radius: 12px;
121
+ font-size: 13px;
122
+ line-height: 1.5;
123
+ }
124
+ .jsee-chat-bubble p { margin: 0; }
125
+ .jsee-chat-bubble p + p { margin-top: 4px; }
126
+ .jsee-chat-user .jsee-chat-bubble {
127
+ background: var(--jsee-primary, #00d1b2);
128
+ color: #fff;
129
+ border-bottom-right-radius: 4px;
130
+ }
131
+ .jsee-chat-assistant .jsee-chat-bubble {
132
+ background: var(--jsee-bg-secondary, #f5f5f5);
133
+ color: var(--jsee-text, #333);
134
+ border-bottom-left-radius: 4px;
135
+ }
136
+ .jsee-output-missing {
137
+ padding: 24px;
138
+ text-align: center;
139
+ color: var(--jsee-text-secondary, #888);
140
+ font-size: 13px;
141
+ background: var(--jsee-bg-secondary, #f9f9f9);
142
+ border-radius: 4px;
143
+ }
144
+ .jsee-output-missing a {
145
+ color: var(--jsee-primary, #00d1b2);
146
+ }
147
+ .jsee-gallery-grid {
148
+ display: grid;
149
+ gap: 8px;
150
+ }
151
+ .jsee-gallery-grid img {
152
+ width: 100%;
153
+ height: auto;
154
+ object-fit: cover;
155
+ border-radius: 4px;
156
+ cursor: pointer;
157
+ }
158
+ .jsee-gallery-lightbox {
159
+ position: fixed;
160
+ inset: 0;
161
+ z-index: 10000;
162
+ background: rgba(0, 0, 0, 0.85);
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: center;
166
+ cursor: pointer;
167
+ }
168
+ .jsee-gallery-lightbox img {
169
+ max-width: 90vw;
170
+ max-height: 90vh;
171
+ object-fit: contain;
172
+ }
173
+ .jsee-highlight-segment {
174
+ padding: 2px 4px;
175
+ border-radius: 3px;
176
+ position: relative;
177
+ display: inline;
178
+ }
179
+ .jsee-highlight-label {
180
+ font-size: 10px;
181
+ font-weight: 600;
182
+ margin-left: 2px;
183
+ opacity: 0.7;
184
+ vertical-align: super;
185
+ }
186
+ .jsee-gauge { text-align: center; }
187
+ .jsee-gauge-track { fill: none; stroke: var(--jsee-border, #e0e0e0); stroke-width: 12; stroke-linecap: round; }
188
+ .jsee-gauge-fill { fill: none; stroke-width: 12; stroke-linecap: round; }
189
+ .jsee-gauge-value { font-size: 28px; font-weight: 600; fill: var(--jsee-text, #333); }
190
+ .jsee-gauge-label { font-size: 12px; fill: var(--jsee-text-secondary, #666); }
191
+
192
+ .jsee-number { text-align: center; padding: 12px 0; }
193
+ .jsee-number-value { font-size: 36px; font-weight: 700; color: var(--jsee-text, #333); }
194
+ .jsee-number-prefix, .jsee-number-suffix { font-size: 20px; font-weight: 400; opacity: 0.6; }
195
+ .jsee-number-delta { font-size: 14px; margin-top: 4px; }
196
+ .jsee-number-delta.positive { color: #27ae60; }
197
+ .jsee-number-delta.negative { color: #e74c3c; }
198
+ .jsee-number-delta.neutral { color: var(--jsee-text-secondary, #666); }
199
+ .jsee-number-label { font-size: 13px; color: var(--jsee-text-secondary, #666); margin-top: 4px; }
200
+
201
+ .jsee-alert { padding: 12px 16px; border-radius: 4px; border-left: 4px solid; font-size: 13px; line-height: 1.5; }
202
+ .jsee-alert[data-type="info"] { background: #e3f2fd; border-color: #2196f3; color: #1565c0; }
203
+ .jsee-alert[data-type="success"] { background: #e8f5e9; border-color: #4caf50; color: #2e7d32; }
204
+ .jsee-alert[data-type="warning"] { background: #fff8e1; border-color: #ff9800; color: #e65100; }
205
+ .jsee-alert[data-type="error"] { background: #ffebee; border-color: #f44336; color: #c62828; }
206
+
207
+ .jsee-pdf-controls {
208
+ display: flex;
209
+ align-items: center;
210
+ gap: 8px;
211
+ padding: 4px 0;
212
+ font-size: 13px;
213
+ }
214
+ .jsee-pdf-controls button {
215
+ padding: 2px 10px;
216
+ border: 1px solid var(--jsee-border, #e0e0e0);
217
+ border-radius: 3px;
218
+ background: var(--jsee-bg-secondary, #f5f5f5);
219
+ cursor: pointer;
220
+ font-size: 12px;
221
+ }
222
+ .jsee-viewer-empty {
223
+ padding: 24px;
224
+ text-align: center;
225
+ color: var(--jsee-text-secondary, #888);
226
+ font-size: 13px;
227
+ }
228
+ .jsee-viewer iframe {
229
+ width: 100%;
230
+ border: none;
231
+ min-height: 400px;
232
+ }
233
+ .jsee-viewer img {
234
+ max-width: 100%;
235
+ height: auto;
236
+ }
237
+ .jsee-viewer audio {
238
+ width: 100%;
239
+ }
240
+ .jsee-viewer video {
241
+ max-width: 100%;
242
+ height: auto;
243
+ }
244
+ .jsee-pdf-frame {
245
+ width: 100%;
246
+ height: 100%;
247
+ min-height: 360px;
248
+ border: none;
249
+ display: block;
250
+ }
251
+ </style>
252
+
253
+ <template>
254
+ <!-- Group output: no card wrapper, children render as separate vue-output -->
255
+ <div v-if="output.type === 'group'" :id="outputName">
256
+ <template v-if="output.style === 'tabs'">
257
+ <div class="jsee-tabs-header">
258
+ <button v-for="(el, ti) in output.elements" :key="ti"
259
+ class="jsee-tab-btn" :class="{ active: activeOutputTab === ti }"
260
+ v-on:click="activeOutputTab = ti">
261
+ {{ el.name || 'Tab ' + (ti + 1) }}
262
+ </button>
263
+ </div>
264
+ <div v-for="(el, ti) in output.elements" :key="ti" v-show="activeOutputTab === ti">
265
+ <vue-output :output="el" v-on:notification="$emit('notification', $event)"></vue-output>
266
+ </div>
267
+ </template>
268
+ <template v-else>
269
+ <vue-output v-for="(el, ti) in output.elements" :key="ti"
270
+ :output="el" v-on:notification="$emit('notification', $event)"></vue-output>
271
+ </template>
272
+ </div>
273
+ <!-- Regular output: card wrapper -->
274
+ <div
275
+ v-else
276
+ class="jsee-output-card"
277
+ v-show="!(typeof output.value === 'undefined') || output.type === 'chat'"
278
+ :class="{ 'is-fullscreen': isFullScreen }"
279
+ ref="cardRoot"
280
+ >
281
+ <div class="jsee-output-header" v-if="output.type !== 'chat'">
282
+ <p class="jsee-output-title" v-if="output.name">{{ output.name }}</p>
283
+ <div class="jsee-output-actions">
284
+ <button v-on:click="save()">Save</button>
285
+ <button v-on:click="copy()">Copy</button>
286
+ <button v-if="!isFullScreen" @click="toggleFullScreen" title="Expand to full screen">Fullscreen</button>
287
+ <button v-else @click="toggleFullScreen" title="Exit full screen">Close</button>
288
+ </div>
289
+ </div>
290
+ <div class="jsee-output-body">
291
+ <div :id="outputName" v-if="(output.type == 'svg') || (output.type == 'html')">
292
+ <div v-html="output.value"></div>
293
+ </div>
294
+ <div :id="outputName" v-else-if="output.type == 'object'">
295
+ <json-viewer :value="output.value" copyable sort />
296
+ </div>
297
+ <div :id="outputName" v-else-if="output.type == 'code'">
298
+ <pre>{{ output.value }}</pre>
299
+ </div>
300
+ <div :id="outputName" v-else-if="output.type == 'function'">
301
+ <div class="custom-container" ref="customContainer"></div>
302
+ </div>
303
+ <div :id="outputName" v-else-if="output.type === 'table'">
304
+ <virtual-table :data="output.value" />
305
+ </div>
306
+ <div :id="outputName" v-else-if="output.type === 'markdown'">
307
+ <div v-html="renderMarkdown(output.value)"></div>
308
+ </div>
309
+ <div :id="outputName" v-else-if="output.type === 'image'">
310
+ <img :src="output.value" style="max-width: 100%; height: auto;" />
311
+ </div>
312
+ <div :id="outputName" v-else-if="output.type === 'audio'">
313
+ <audio controls :src="output.value" style="width: 100%;"></audio>
314
+ </div>
315
+ <div :id="outputName" v-else-if="output.type === 'video'">
316
+ <video controls :src="output.value" style="max-width: 100%; height: auto;"></video>
317
+ </div>
318
+ <div :id="outputName" v-else-if="output.type === 'chart'" ref="chartContainer">
319
+ <div v-if="!hasPlot" class="jsee-output-missing">
320
+ Chart requires <a href="https://observablehq.com/plot/" target="_blank">Observable Plot</a>.
321
+ Add to imports or use the full bundle.
322
+ </div>
323
+ </div>
324
+ <div :id="outputName" v-else-if="output.type === '3d'" ref="threeDContainer"
325
+ :style="{ height: (output.height || 400) + 'px' }">
326
+ <div v-if="!hasThree" class="jsee-output-missing">
327
+ 3D viewer requires <a href="https://threejs.org/" target="_blank">Three.js</a>.
328
+ Add to imports or use the full bundle.
329
+ </div>
330
+ </div>
331
+ <div :id="outputName" v-else-if="output.type === 'map'" ref="mapContainer"
332
+ :style="{ height: (output.height || 400) + 'px' }">
333
+ <div v-if="!hasLeaflet" class="jsee-output-missing">
334
+ Map requires <a href="https://leafletjs.com/" target="_blank">Leaflet</a>.
335
+ Add to imports or use the full bundle.
336
+ </div>
337
+ </div>
338
+ <div :id="outputName" v-else-if="output.type === 'pdf'" ref="pdfContainer"
339
+ :style="{ height: (output.height || 600) + 'px', overflow: 'auto' }">
340
+ </div>
341
+ <div :id="outputName" v-else-if="output.type === 'gallery'">
342
+ <div class="jsee-gallery-grid"
343
+ :style="{ gridTemplateColumns: 'repeat(' + (output.columns || 3) + ', 1fr)', gap: (output.gap || 8) + 'px' }">
344
+ <img v-for="(src, gi) in (output.value || [])" :key="gi" :src="src"
345
+ @click="lightboxSrc = src" />
346
+ </div>
347
+ <div v-if="lightboxSrc" class="jsee-gallery-lightbox" @click="lightboxSrc = null">
348
+ <img :src="lightboxSrc" />
349
+ </div>
350
+ </div>
351
+ <div :id="outputName" v-else-if="output.type === 'highlight'">
352
+ <span v-for="(seg, si) in (output.value || [])" :key="si">
353
+ <span v-if="seg.label" class="jsee-highlight-segment"
354
+ :style="{ background: seg.color || '#e3f2fd' }">{{ seg.text }}<span class="jsee-highlight-label">{{ seg.label }}</span></span>
355
+ <span v-else>{{ seg.text }}</span>
356
+ </span>
357
+ </div>
358
+ <div :id="outputName" v-else-if="output.type === 'gauge'" class="jsee-gauge">
359
+ <svg viewBox="0 0 200 120" style="max-width: 220px; margin: 0 auto; display: block;">
360
+ <path class="jsee-gauge-track" d="M 20 90 A 80 80 0 0 1 180 90" />
361
+ <path class="jsee-gauge-fill"
362
+ :stroke="output.color || 'var(--jsee-primary, #00d1b2)'"
363
+ d="M 20 90 A 80 80 0 0 1 180 90"
364
+ :stroke-dasharray="(Math.min(1, Math.max(0, ((typeof output.value === 'object' && output.value !== null ? output.value.value : output.value) - (output.min != null ? output.min : 0)) / ((output.max != null ? output.max : 100) - (output.min != null ? output.min : 0)))) * 251.33) + ' 251.33'" />
365
+ <text class="jsee-gauge-value" x="100" y="88" text-anchor="middle">{{ typeof output.value === 'object' && output.value !== null ? output.value.value : output.value }}</text>
366
+ <text class="jsee-gauge-label" x="100" y="108" text-anchor="middle">{{ (typeof output.value === 'object' && output.value !== null ? output.value.label : null) || output.label || '' }}</text>
367
+ </svg>
368
+ </div>
369
+ <div :id="outputName" v-else-if="output.type === 'number'" class="jsee-number">
370
+ <template v-if="output.value != null">
371
+ <div class="jsee-number-value">
372
+ <span v-if="output.prefix" class="jsee-number-prefix">{{ output.prefix }}</span>{{ output.precision != null
373
+ ? (typeof output.value === 'object' && output.value !== null ? output.value.value : output.value).toFixed(output.precision)
374
+ : (typeof output.value === 'object' && output.value !== null ? output.value.value : output.value).toLocaleString() }}<span v-if="output.suffix" class="jsee-number-suffix">{{ output.suffix }}</span>
375
+ </div>
376
+ <div v-if="typeof output.value === 'object' && output.value !== null && output.value.delta != null"
377
+ class="jsee-number-delta"
378
+ :class="output.value.delta > 0 ? 'positive' : output.value.delta < 0 ? 'negative' : 'neutral'">
379
+ {{ output.value.delta > 0 ? '\u25B2' : output.value.delta < 0 ? '\u25BC' : '\u2013' }} {{ Math.abs(output.value.delta) }}
380
+ </div>
381
+ <div v-if="output.label || (typeof output.value === 'object' && output.value !== null && output.value.label)"
382
+ class="jsee-number-label">
383
+ {{ (typeof output.value === 'object' && output.value !== null ? output.value.label : null) || output.label }}
384
+ </div>
385
+ </template>
386
+ </div>
387
+ <div :id="outputName" v-else-if="output.type === 'alert'" class="jsee-alert"
388
+ :data-type="(typeof output.value === 'object' && output.value !== null ? output.value.type : null) || output.alertType || 'info'">
389
+ {{ typeof output.value === 'object' && output.value !== null ? output.value.message : output.value }}
390
+ </div>
391
+ <div :id="outputName" v-else-if="output.type === 'chat'" class="jsee-chat">
392
+ <div class="jsee-chat-messages" ref="chatMessages">
393
+ <div v-for="(msg, mi) in (output._messages || [])" :key="mi"
394
+ class="jsee-chat-msg" :class="'jsee-chat-' + msg.role">
395
+ <div class="jsee-chat-bubble" v-html="renderMarkdown(msg.content)"></div>
396
+ </div>
397
+ </div>
398
+ </div>
399
+ <div :id="outputName" v-else-if="output.type === 'viewer'" class="jsee-viewer">
400
+ <div v-if="!output.value" class="jsee-viewer-empty">Select a file to preview</div>
401
+ <img v-else-if="viewerMedia === 'image'" :src="output.value" />
402
+ <audio v-else-if="viewerMedia === 'audio'" controls :src="output.value"></audio>
403
+ <video v-else-if="viewerMedia === 'video'" controls :src="output.value"></video>
404
+ <iframe v-else :src="output.value"></iframe>
405
+ </div>
406
+ <div :id="outputName" v-else-if="output.type === 'file'" class="jsee-file-output">
407
+ <button class="jsee-file-download-btn" v-on:click="downloadFile()">⬇ Download {{ output.filename || output.name || 'file' }}</button>
408
+ </div>
409
+ <div :id="outputName" v-else-if="output.type == 'blank'">
410
+ <!-- will be filled by custom render function -->
411
+ </div>
412
+ <div :id="outputName" v-else>
413
+ <pre>{{ output.value }}</pre>
414
+ </div>
415
+ </div>
416
+ </div>
417
+ </template>
418
+
419
+ <script>
420
+ import { component } from "./common-outputs.js"
421
+ import VirtualTable from "./virtual-table.vue"
422
+ export default {
423
+ ...component,
424
+ components: { ...(component.components || {}), VirtualTable }
425
+ }
426
+ </script>
@@ -0,0 +1,194 @@
1
+ <style scoped>
2
+ .vt-wrap {
3
+ font-size: 13px;
4
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
5
+ }
6
+ .vt-scroll {
7
+ overflow: auto;
8
+ max-height: 500px;
9
+ border: 1px solid #eee;
10
+ border-radius: 4px;
11
+ }
12
+ .vt-table {
13
+ width: 100%;
14
+ border-collapse: collapse;
15
+ table-layout: auto;
16
+ }
17
+ .vt-table th {
18
+ padding: 6px 12px;
19
+ text-align: left;
20
+ border-bottom: 2px solid #ddd;
21
+ background: #f8f8f8;
22
+ font-weight: 600;
23
+ position: sticky;
24
+ top: 0;
25
+ cursor: pointer;
26
+ user-select: none;
27
+ white-space: nowrap;
28
+ z-index: 1;
29
+ }
30
+ .vt-table th:hover {
31
+ background: #f0f0f0;
32
+ }
33
+ .vt-table th .sort-icon {
34
+ margin-left: 4px;
35
+ opacity: 0.3;
36
+ }
37
+ .vt-table th.sorted .sort-icon {
38
+ opacity: 1;
39
+ }
40
+ .vt-table td {
41
+ padding: 4px 12px;
42
+ border-bottom: 1px solid #eee;
43
+ white-space: nowrap;
44
+ max-width: 300px;
45
+ overflow: hidden;
46
+ text-overflow: ellipsis;
47
+ }
48
+ .vt-table td.num {
49
+ text-align: right;
50
+ font-variant-numeric: tabular-nums;
51
+ }
52
+ .vt-table tbody tr:hover {
53
+ background: #f9f9f9;
54
+ }
55
+ .vt-footer {
56
+ font-size: 11px;
57
+ color: #888;
58
+ margin-top: 6px;
59
+ display: flex;
60
+ justify-content: space-between;
61
+ }
62
+ </style>
63
+
64
+ <template>
65
+ <div class="vt-wrap">
66
+ <div v-if="label" style="font-size:12px;color:#888;margin-bottom:6px">
67
+ {{ label }} ({{ rowCount }} rows{{ truncated ? ', showing first ' + maxRows : '' }})
68
+ </div>
69
+ <div class="vt-scroll">
70
+ <table class="vt-table">
71
+ <thead>
72
+ <tr>
73
+ <th
74
+ v-for="(col, ci) in columns"
75
+ :key="ci"
76
+ :class="{ sorted: sortCol === ci }"
77
+ @click="toggleSort(ci)"
78
+ >
79
+ {{ col }}
80
+ <span class="sort-icon">{{ sortCol === ci ? (sortAsc ? '▲' : '▼') : '⇅' }}</span>
81
+ </th>
82
+ </tr>
83
+ </thead>
84
+ <tbody>
85
+ <tr v-for="(row, ri) in displayRows" :key="ri">
86
+ <td
87
+ v-for="(cell, ci) in row"
88
+ :key="ci"
89
+ :class="{ num: colTypes[ci] === 'number' }"
90
+ >{{ cell }}</td>
91
+ </tr>
92
+ </tbody>
93
+ </table>
94
+ </div>
95
+ <div class="vt-footer" v-if="rowCount > 0">
96
+ <span>{{ rowCount }} rows × {{ columns.length }} columns</span>
97
+ <span v-if="truncated">Showing first {{ maxRows }} rows</span>
98
+ </div>
99
+ </div>
100
+ </template>
101
+
102
+ <script>
103
+ export default {
104
+ props: {
105
+ data: { type: [Object, Array], required: true }
106
+ },
107
+ data () {
108
+ return {
109
+ sortCol: -1,
110
+ sortAsc: true,
111
+ maxRows: 5000
112
+ }
113
+ },
114
+ computed: {
115
+ normalized () {
116
+ const d = this.data
117
+ if (!d) return { columns: [], rows: [] }
118
+ if (d.columns && d.rows) return d
119
+ if (Array.isArray(d) && d.length > 0) {
120
+ if (Array.isArray(d[0])) {
121
+ return { columns: d[0].map(String), rows: d.slice(1) }
122
+ }
123
+ if (typeof d[0] === 'object') {
124
+ const cols = Object.keys(d[0])
125
+ return { columns: cols, rows: d.map(r => cols.map(c => r[c])) }
126
+ }
127
+ }
128
+ return { columns: [], rows: [] }
129
+ },
130
+ columns () {
131
+ return this.normalized.columns || []
132
+ },
133
+ allRows () {
134
+ return this.normalized.rows || []
135
+ },
136
+ label () {
137
+ return this.data && this.data.label
138
+ },
139
+ rowCount () {
140
+ return this.allRows.length
141
+ },
142
+ truncated () {
143
+ return this.rowCount > this.maxRows
144
+ },
145
+ colTypes () {
146
+ const rows = this.allRows
147
+ const n = Math.min(rows.length, 20)
148
+ return this.columns.map((_, ci) => {
149
+ let numCount = 0
150
+ for (let i = 0; i < n; i++) {
151
+ const v = rows[i] && rows[i][ci]
152
+ if (v !== '' && v !== null && v !== undefined && !isNaN(Number(v))) numCount++
153
+ }
154
+ return numCount > n * 0.5 ? 'number' : 'string'
155
+ })
156
+ },
157
+ sortedRows () {
158
+ if (this.sortCol < 0) return this.allRows
159
+ const ci = this.sortCol
160
+ const asc = this.sortAsc
161
+ const isNum = this.colTypes[ci] === 'number'
162
+ const sorted = [...this.allRows]
163
+ sorted.sort((a, b) => {
164
+ let va = a[ci], vb = b[ci]
165
+ if (isNum) { va = Number(va) || 0; vb = Number(vb) || 0 }
166
+ else { va = String(va || ''); vb = String(vb || '') }
167
+ if (va < vb) return asc ? -1 : 1
168
+ if (va > vb) return asc ? 1 : -1
169
+ return 0
170
+ })
171
+ return sorted
172
+ },
173
+ displayRows () {
174
+ const rows = this.sortedRows
175
+ return this.truncated ? rows.slice(0, this.maxRows) : rows
176
+ }
177
+ },
178
+ methods: {
179
+ toggleSort (ci) {
180
+ if (this.sortCol === ci) {
181
+ if (this.sortAsc) {
182
+ this.sortAsc = false
183
+ } else {
184
+ this.sortCol = -1
185
+ this.sortAsc = true
186
+ }
187
+ } else {
188
+ this.sortCol = ci
189
+ this.sortAsc = true
190
+ }
191
+ }
192
+ }
193
+ }
194
+ </script>
@@ -1,15 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(npm run test:basic:*)",
5
- "Bash(npm run build-dev:*)",
6
- "Bash(npm run test:unit:*)",
7
- "Bash(find:*)",
8
- "Bash(npx eslint:*)",
9
- "Bash(npm run test:python:*)",
10
- "Bash(npm run build:*)",
11
- "Bash(npx webpack:*)",
12
- "Bash(git -C /home/anton/projects/jseeio/jsee diff README.md)"
13
- ]
14
- }
15
- }
package/.eslintrc.js DELETED
@@ -1,38 +0,0 @@
1
- module.exports = {
2
- env: {
3
- browser: true,
4
- node: true,
5
- es2020: true,
6
- jest: true,
7
- },
8
- parserOptions: {
9
- ecmaVersion: 2020,
10
- sourceType: 'module',
11
- },
12
- rules: {
13
- // Match existing style: no semicolons, single quotes, 2-space indent
14
- semi: ['warn', 'never'],
15
- quotes: ['warn', 'single', { avoidEscape: true, allowTemplateLiterals: true }],
16
- indent: ['warn', 2, { SwitchCase: 1, ignoredNodes: ['TemplateLiteral *'] }],
17
- 'no-unused-vars': ['warn', { args: 'none', varsIgnorePattern: '^_' }],
18
- 'no-undef': 'error',
19
- eqeqeq: ['warn', 'smart'],
20
- 'no-eval': 'off', // intentional eval usage in main.js
21
- },
22
- globals: {
23
- VERSION: 'readonly',
24
- importScripts: 'readonly',
25
- JSEE: 'readonly',
26
- loadPyodide: 'readonly',
27
- },
28
- overrides: [
29
- {
30
- // Puppeteer tests use global `page` from jest-puppeteer
31
- files: ['test/**/*.test.js'],
32
- globals: {
33
- page: 'readonly',
34
- },
35
- },
36
- ],
37
- ignorePatterns: ['dist/', 'node_modules/', 'apps/', 'tmp/', 'load/'],
38
- }