@warkypublic/svelix 0.1.44 → 0.1.47

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 (109) hide show
  1. package/README.md +93 -33
  2. package/dist/components/BetterMenu/BetterMenu.svelte +21 -37
  3. package/dist/components/BetterMenu/BetterMenuAsyncButton.svelte +21 -31
  4. package/dist/components/BetterMenu/BetterMenuPreview.svelte +27 -29
  5. package/dist/components/BetterMenu/MenuRenderer.svelte +40 -51
  6. package/dist/components/Boxer/Boxer.svelte +488 -602
  7. package/dist/components/Boxer/BoxerTarget.svelte +6 -46
  8. package/dist/components/Boxer/BoxerTarget.svelte.d.ts +1 -0
  9. package/dist/components/Boxer/types.d.ts +0 -1
  10. package/dist/components/Button.svelte +19 -43
  11. package/dist/components/ButtonPreview.svelte +2 -11
  12. package/dist/components/CardGrid/CardGrid.svelte +1141 -0
  13. package/dist/components/CardGrid/CardGrid.svelte.d.ts +61 -0
  14. package/dist/components/CardGrid/CardGridFilterPanel.svelte +320 -0
  15. package/dist/components/CardGrid/CardGridFilterPanel.svelte.d.ts +11 -0
  16. package/dist/components/CardGrid/DefaultCard.svelte +287 -0
  17. package/dist/components/CardGrid/DefaultCard.svelte.d.ts +10 -0
  18. package/dist/components/CardGrid/ImageCardStory.svelte +308 -0
  19. package/dist/components/CardGrid/ImageCardStory.svelte.d.ts +18 -0
  20. package/dist/components/CardGrid/cardGrid.d.ts +33 -0
  21. package/dist/components/CardGrid/cardGrid.js +21 -0
  22. package/dist/components/CardGrid/index.d.ts +4 -0
  23. package/dist/components/CardGrid/index.js +4 -0
  24. package/dist/components/ContentEditor/CollaboraInsertTextDemo.svelte +5 -10
  25. package/dist/components/ContentEditor/ContentEditor.svelte +24 -50
  26. package/dist/components/ContentEditor/subcomponents/AudioPlayer.svelte +17 -17
  27. package/dist/components/ContentEditor/subcomponents/CollaboraEditor.svelte +244 -309
  28. package/dist/components/ContentEditor/subcomponents/EmailViewer.svelte +2 -3
  29. package/dist/components/ContentEditor/subcomponents/ImageViewer.svelte +14 -15
  30. package/dist/components/ContentEditor/subcomponents/MarkdownViewer.svelte +74 -90
  31. package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte +75 -103
  32. package/dist/components/ContentEditor/subcomponents/Office365Editor.svelte +234 -297
  33. package/dist/components/ContentEditor/subcomponents/PdfViewer.svelte +14 -20
  34. package/dist/components/ContentEditor/subcomponents/TextEditor.svelte +122 -150
  35. package/dist/components/ContentEditor/subcomponents/UnknownFile.svelte +105 -126
  36. package/dist/components/ContentEditor/subcomponents/VideoPlayer.svelte +17 -17
  37. package/dist/components/ContentEditor/subcomponents/ZipViewer.svelte +2 -3
  38. package/dist/components/ErrorBoundary/ErrorBoundary.svelte +41 -58
  39. package/dist/components/ErrorBoundary/ErrorBoundaryPreview.svelte +8 -16
  40. package/dist/components/Former/Former.svelte +306 -424
  41. package/dist/components/Former/FormerButtonArea.svelte +17 -68
  42. package/dist/components/Former/FormerDrawer.svelte +26 -78
  43. package/dist/components/Former/FormerDrawerPreview.svelte +99 -89
  44. package/dist/components/Former/FormerModal.svelte +24 -70
  45. package/dist/components/Former/FormerModalPreview.svelte +99 -89
  46. package/dist/components/Former/FormerPreview.svelte +76 -78
  47. package/dist/components/Former/FormerRestApiPreview.svelte +55 -48
  48. package/dist/components/FormerControllers/ButtonCtrl.svelte +13 -44
  49. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +280 -445
  50. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlCalendar.svelte +95 -136
  51. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlPickerPanel.svelte +3 -62
  52. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlTimeFields.svelte +143 -217
  53. package/dist/components/FormerControllers/IconButtonCtrl.svelte +13 -42
  54. package/dist/components/FormerControllers/InlineWrapper.svelte +12 -41
  55. package/dist/components/FormerControllers/InlineWrapperPreview.svelte +4 -26
  56. package/dist/components/FormerControllers/NativeSelectCtrl.svelte +3 -29
  57. package/dist/components/FormerControllers/NumberInputCtrl.svelte +9 -35
  58. package/dist/components/FormerControllers/PasswordInputCtrl.svelte +4 -23
  59. package/dist/components/FormerControllers/SwitchCtrl.svelte +3 -20
  60. package/dist/components/FormerControllers/TextAreaCtrl.svelte +3 -24
  61. package/dist/components/FormerControllers/TextInputCtrl.svelte +3 -24
  62. package/dist/components/GlobalStateStore/GlobalStateStoreProvider.svelte +73 -120
  63. package/dist/components/Gridler/components/Gridler.svelte +338 -442
  64. package/dist/components/Gridler/components/GridlerCanvas.svelte +720 -1115
  65. package/dist/components/Gridler/components/GridlerEditor.svelte +2 -15
  66. package/dist/components/Gridler/components/GridlerFull.svelte +520 -841
  67. package/dist/components/Gridler/components/GridlerFull.svelte.d.ts +1 -1
  68. package/dist/components/Gridler/components/GridlerFullWithFormerPreview.svelte +194 -147
  69. package/dist/components/Gridler/components/GridlerSearch.svelte +7 -16
  70. package/dist/components/Gridler/components/GridlerSearchToggle.svelte +7 -19
  71. package/dist/components/Gridler/types.d.ts +1 -1
  72. package/dist/components/Icons/IconAdd.svelte +1 -3
  73. package/dist/components/Icons/IconAlertCircle.svelte +1 -3
  74. package/dist/components/Icons/IconAlertTriangle.svelte +1 -3
  75. package/dist/components/Icons/IconCamera.svelte +1 -3
  76. package/dist/components/Icons/IconClose.svelte +1 -3
  77. package/dist/components/Icons/IconEdit.svelte +1 -3
  78. package/dist/components/Icons/IconFilter.svelte +1 -3
  79. package/dist/components/Icons/IconSearch.svelte +1 -3
  80. package/dist/components/Icons/IconSort.svelte +1 -3
  81. package/dist/components/Icons/IconTrash.svelte +1 -3
  82. package/dist/components/Portal/Portal.svelte +14 -38
  83. package/dist/components/Screenshot/Screenshot.svelte +7 -13
  84. package/dist/components/Svark/Svark.svelte +680 -852
  85. package/dist/components/Svark/SvarkContextMenu.svelte +44 -63
  86. package/dist/components/Svark/SvarkInfiniteLoader.svelte +2 -6
  87. package/dist/components/Svark/SvarkPager.svelte +2 -28
  88. package/dist/components/Svark/SvarkSelectionDemo.svelte +4 -8
  89. package/dist/components/Svark/SvarkTopBar.svelte +9 -74
  90. package/dist/components/SvarkGrid/SvarkGrid.svelte +877 -1123
  91. package/dist/components/SvarkGrid/components/SvarkColumnFilterForm.svelte +45 -58
  92. package/dist/components/SvarkGrid/components/SvarkContextMenu.svelte +44 -63
  93. package/dist/components/SvarkGrid/components/SvarkHeaderFilterCell.svelte +41 -75
  94. package/dist/components/SvarkGrid/components/SvarkInfiniteLoader.svelte +2 -6
  95. package/dist/components/SvarkGrid/components/SvarkPager.svelte +2 -28
  96. package/dist/components/SvarkGrid/components/SvarkSearchPopover.svelte +8 -16
  97. package/dist/components/SvarkGrid/components/SvarkTopBar.svelte +6 -62
  98. package/dist/components/SvarkGrid/internal/SvarkGridView.svelte +345 -507
  99. package/dist/components/VTree/VTree.svelte +349 -431
  100. package/dist/components/VTree/VTreeContextMenu.svelte +21 -33
  101. package/dist/components/VTree/VTreeEventsDemo.svelte +48 -58
  102. package/dist/components/VTree/VTreeRow.svelte +13 -65
  103. package/dist/components/VTree/VTreeSearch.svelte +6 -13
  104. package/dist/components/VTree/VTreeVirtualViewport.svelte +37 -106
  105. package/dist/components/index.d.ts +1 -0
  106. package/dist/components/index.js +1 -0
  107. package/llm/COMPONENT_GUIDE.md +240 -0
  108. package/llm/plans/card_grid.plan.md +305 -0
  109. package/package.json +48 -46
@@ -1,154 +1,124 @@
1
- <script lang="ts">
2
- import { Tipex, type TipexEditor } from '@friendofsvelte/tipex';
3
- import '@friendofsvelte/tipex/styles/index.css';
4
- import { blobToString } from '../utils.js';
5
- import type { TextEditorProps } from '../types.js';
6
-
7
- let {
8
- value,
9
- filename,
10
- hideHeader = false,
11
- readonly = false,
12
- onChange,
13
- insertText = $bindable(),
14
- insertHtml = $bindable(),
15
- }: TextEditorProps = $props();
16
-
17
- let tipex = $state<TipexEditor>(undefined);
18
- let body = $state('');
19
- let lastBlobHtml = $state('');
20
- let syncingFromValue = false;
21
- let fileInputEl = $state<HTMLInputElement | undefined>(undefined);
22
-
23
- async function fileToDataUrl(file: File): Promise<string> {
24
- return new Promise((resolve, reject) => {
25
- const reader = new FileReader();
26
- reader.onload = () => {
27
- if (typeof reader.result === 'string') {
28
- resolve(reader.result);
29
- return;
30
- }
31
- reject(new Error('Could not convert image to data URL.'));
32
- };
33
- reader.onerror = () => reject(reader.error ?? new Error('Could not read image file.'));
34
- reader.readAsDataURL(file);
35
- });
36
- }
37
-
38
- async function insertEmbeddedImages(files: File[], dropPoint?: { left: number; top: number }): Promise<void> {
39
- if (!tipex || readonly || files.length === 0) return;
40
-
41
- if (dropPoint) {
42
- const pos = tipex.view.posAtCoords(dropPoint);
43
- if (pos?.pos !== undefined) {
44
- tipex.chain().focus().setTextSelection(pos.pos).run();
45
- } else {
46
- tipex.commands.focus();
47
- }
48
- } else {
49
- tipex.commands.focus();
50
- }
51
-
52
- for (const file of files) {
53
- const src = await fileToDataUrl(file);
54
- tipex.chain().focus().setImage({ src, alt: file.name }).run();
55
- }
56
- }
57
-
58
- async function handlePaste(event: ClipboardEvent): Promise<void> {
59
- if (readonly || !event.clipboardData) return;
60
- const files = Array.from(event.clipboardData.files).filter((file) =>
61
- file.type.startsWith('image/')
62
- );
63
- if (files.length === 0) return;
64
-
65
- event.preventDefault();
66
- await insertEmbeddedImages(files);
67
- }
68
-
69
- async function handleDrop(event: DragEvent): Promise<void> {
70
- if (readonly || !event.dataTransfer) return;
71
- const files = Array.from(event.dataTransfer.files).filter((file) =>
72
- file.type.startsWith('image/')
73
- );
74
- if (files.length === 0) return;
75
-
76
- event.preventDefault();
77
- await insertEmbeddedImages(files, { left: event.clientX, top: event.clientY });
78
- }
79
-
80
- function openImagePicker(): void {
81
- if (readonly) return;
82
- fileInputEl?.click();
83
- }
84
-
85
- async function handleFileInputChange(event: Event): Promise<void> {
86
- const input = event.currentTarget as HTMLInputElement;
87
- const files = Array.from(input.files ?? []).filter((file) => file.type.startsWith('image/'));
88
- if (files.length > 0) {
89
- await insertEmbeddedImages(files);
90
- }
91
- input.value = '';
92
- }
93
-
94
- function syncEditorContent(nextHtml: string): void {
95
- if (!tipex) return;
96
- const currentHtml = tipex.getHTML();
97
- if (currentHtml === nextHtml) return;
98
-
99
- syncingFromValue = true;
100
- tipex.commands.setContent(nextHtml, false);
101
- body = tipex.getHTML();
102
- syncingFromValue = false;
103
- }
104
-
105
- function handleUpdate(): void {
106
- if (readonly || syncingFromValue || !tipex) return;
107
- const nextHtml = tipex.getHTML();
108
- body = nextHtml;
109
- if (nextHtml !== lastBlobHtml) {
110
- onChange?.(new Blob([nextHtml], { type: 'text/html' }));
111
- }
112
- }
113
-
114
- $effect(() => {
115
- blobToString(value).then((nextHtml) => {
116
- lastBlobHtml = nextHtml;
117
- if (body !== nextHtml) {
118
- body = nextHtml;
119
- }
120
- syncEditorContent(nextHtml);
121
- });
122
- });
123
-
124
- $effect(() => {
125
- tipex?.setEditable(!readonly);
126
- });
127
-
128
- $effect(() => {
129
- const editor = tipex;
130
- if (!editor) {
131
- insertText = undefined;
132
- insertHtml = undefined;
133
- return;
134
- }
135
- insertText = (text: string) => {
136
- editor.chain().focus().insertContent(text).run();
137
- };
138
- insertHtml = (html: string) => {
139
- editor.chain().focus().insertContent(html).run();
140
- };
141
- });
142
- const editorClass = $derived(
143
- hideHeader
144
- ? 'min-h-full bg-surface-50-900'
145
- : 'min-h-full rounded-container-token border border-surface-200-800 bg-surface-50-900'
146
- );
147
-
148
- const contentPaddingClass = $derived(hideHeader ? 'p-0' : 'p-4');
149
-
1
+ <script lang="ts">import { Tipex } from "@friendofsvelte/tipex";
2
+ import "@friendofsvelte/tipex/styles/index.css";
3
+ import { blobToString } from "../utils.js";
4
+ let { value, filename, hideHeader = false, readonly = false, onChange, insertText = $bindable(), insertHtml = $bindable() } = $props();
5
+ let tipex = $state(undefined);
6
+ let body = $state("");
7
+ let lastBlobHtml = $state("");
8
+ let syncingFromValue = false;
9
+ let fileInputEl = $state(undefined);
10
+ async function fileToDataUrl(file) {
11
+ return new Promise((resolve, reject) => {
12
+ const reader = new FileReader();
13
+ reader.onload = () => {
14
+ if (typeof reader.result === "string") {
15
+ resolve(reader.result);
16
+ return;
17
+ }
18
+ reject(new Error("Could not convert image to data URL."));
19
+ };
20
+ reader.onerror = () => reject(reader.error ?? new Error("Could not read image file."));
21
+ reader.readAsDataURL(file);
22
+ });
23
+ }
24
+ async function insertEmbeddedImages(files, dropPoint) {
25
+ if (!tipex || readonly || files.length === 0) return;
26
+ if (dropPoint) {
27
+ const pos = tipex.view.posAtCoords(dropPoint);
28
+ if (pos?.pos !== undefined) {
29
+ tipex.chain().focus().setTextSelection(pos.pos).run();
30
+ } else {
31
+ tipex.commands.focus();
32
+ }
33
+ } else {
34
+ tipex.commands.focus();
35
+ }
36
+ for (const file of files) {
37
+ const src = await fileToDataUrl(file);
38
+ tipex.chain().focus().setImage({
39
+ src,
40
+ alt: file.name
41
+ }).run();
42
+ }
43
+ }
44
+ async function handlePaste(event) {
45
+ if (readonly || !event.clipboardData) return;
46
+ const files = Array.from(event.clipboardData.files).filter((file) => file.type.startsWith("image/"));
47
+ if (files.length === 0) return;
48
+ event.preventDefault();
49
+ await insertEmbeddedImages(files);
50
+ }
51
+ async function handleDrop(event) {
52
+ if (readonly || !event.dataTransfer) return;
53
+ const files = Array.from(event.dataTransfer.files).filter((file) => file.type.startsWith("image/"));
54
+ if (files.length === 0) return;
55
+ event.preventDefault();
56
+ await insertEmbeddedImages(files, {
57
+ left: event.clientX,
58
+ top: event.clientY
59
+ });
60
+ }
61
+ function openImagePicker() {
62
+ if (readonly) return;
63
+ fileInputEl?.click();
64
+ }
65
+ async function handleFileInputChange(event) {
66
+ const input = event.currentTarget;
67
+ const files = Array.from(input.files ?? []).filter((file) => file.type.startsWith("image/"));
68
+ if (files.length > 0) {
69
+ await insertEmbeddedImages(files);
70
+ }
71
+ input.value = "";
72
+ }
73
+ function syncEditorContent(nextHtml) {
74
+ if (!tipex) return;
75
+ const currentHtml = tipex.getHTML();
76
+ if (currentHtml === nextHtml) return;
77
+ syncingFromValue = true;
78
+ tipex.commands.setContent(nextHtml, false);
79
+ body = tipex.getHTML();
80
+ syncingFromValue = false;
81
+ }
82
+ function handleUpdate() {
83
+ if (readonly || syncingFromValue || !tipex) return;
84
+ const nextHtml = tipex.getHTML();
85
+ body = nextHtml;
86
+ if (nextHtml !== lastBlobHtml) {
87
+ onChange?.(new Blob([nextHtml], { type: "text/html" }));
88
+ }
89
+ }
90
+ $effect(() => {
91
+ blobToString(value).then((nextHtml) => {
92
+ lastBlobHtml = nextHtml;
93
+ if (body !== nextHtml) {
94
+ body = nextHtml;
95
+ }
96
+ syncEditorContent(nextHtml);
97
+ });
98
+ });
99
+ $effect(() => {
100
+ tipex?.setEditable(!readonly);
101
+ });
102
+ $effect(() => {
103
+ const editor = tipex;
104
+ if (!editor) {
105
+ insertText = undefined;
106
+ insertHtml = undefined;
107
+ return;
108
+ }
109
+ insertText = (text) => {
110
+ editor.chain().focus().insertContent(text).run();
111
+ };
112
+ insertHtml = (html) => {
113
+ editor.chain().focus().insertContent(html).run();
114
+ };
115
+ });
116
+ const editorClass = $derived(hideHeader ? "min-h-full bg-surface-50-900" : "min-h-full rounded-container-token border border-surface-200-800 bg-surface-50-900");
117
+ const contentPaddingClass = $derived(hideHeader ? "p-0" : "p-4");
150
118
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
151
- function echain(editor: TipexEditor): any { return echain(editor) ?? {}; }
119
+ function echain(editor) {
120
+ return echain(editor) ?? {};
121
+ }
152
122
  </script>
153
123
 
154
124
  {#snippet controlComponent(editor: TipexEditor)}
@@ -312,6 +282,8 @@ function echain(editor: TipexEditor): any { return echain(editor) ?? {}; }
312
282
 
313
283
  <div
314
284
  class="flex h-full w-full flex-col overflow-hidden bg-surface-50-950"
285
+ role="region"
286
+ aria-label="Text editor"
315
287
  onpaste={handlePaste}
316
288
  ondrop={handleDrop}
317
289
  ondragover={(event) => {
@@ -1,129 +1,108 @@
1
- <script lang="ts">
2
- import type { UnknownFileProps } from '../types.js';
3
- import { getEditorType, getExtFromFilename } from '../utils.js';
4
-
5
- let { value, filename, contentType, note }: UnknownFileProps = $props();
6
-
7
- let headerHex = $state('');
8
- let headerAscii = $state('');
9
- let sniffedType = $state('Unknown');
10
-
11
- const ext = $derived(filename ? getExtFromFilename(filename) : '');
12
- const blobType = $derived(value?.type || contentType || '');
13
- const size = $derived(value?.size ?? 0);
14
- const inferredViewer = $derived(getEditorType({ filename, contentType: blobType }));
15
- const lastModified = $derived(
16
- typeof File !== 'undefined' && value instanceof File && Number.isFinite(value.lastModified)
17
- ? value.lastModified
18
- : undefined
19
- );
20
-
21
- function formatBytes(bytes: number): string {
22
- if (!bytes) return '0 B';
23
- const units = ['B', 'KB', 'MB', 'GB'];
24
- let next = bytes;
25
- let unitIndex = 0;
26
-
27
- while (next >= 1024 && unitIndex < units.length - 1) {
28
- next /= 1024;
29
- unitIndex += 1;
30
- }
31
-
32
- const rounded = next >= 10 || unitIndex === 0 ? next.toFixed(0) : next.toFixed(1);
33
- return `${rounded} ${units[unitIndex]}`;
34
- }
35
-
36
- function formatDateTime(ms?: number): string | null {
37
- if (!ms) return null;
38
- try {
39
- return new Date(ms).toLocaleString();
40
- } catch {
41
- return null;
42
- }
43
- }
44
-
45
- function bytesToHex(bytes: Uint8Array): string {
46
- return Array.from(bytes)
47
- .map((byte) => byte.toString(16).padStart(2, '0').toUpperCase())
48
- .join(' ');
49
- }
50
-
51
- function bytesToAscii(bytes: Uint8Array): string {
52
- return Array.from(bytes)
53
- .map((byte) => (byte >= 32 && byte <= 126 ? String.fromCharCode(byte) : '.'))
54
- .join('');
55
- }
56
-
57
- function detectFromHeader(bytes: Uint8Array): string {
58
- if (bytes.length >= 8 &&
59
- bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47 &&
60
- bytes[4] === 0x0D && bytes[5] === 0x0A && bytes[6] === 0x1A && bytes[7] === 0x0A) {
61
- return 'PNG image';
62
- }
63
- if (bytes.length >= 3 && bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) {
64
- return 'JPEG image';
65
- }
66
- if (bytes.length >= 6 && bytesToAscii(bytes.slice(0, 6)).startsWith('GIF8')) {
67
- return 'GIF image';
68
- }
69
- if (bytes.length >= 4 && bytesToAscii(bytes.slice(0, 4)) === '%PDF') {
70
- return 'PDF document';
71
- }
72
- if (bytes.length >= 4 && bytesToAscii(bytes.slice(0, 4)) === 'PK..') {
73
- return 'ZIP-based archive or document';
74
- }
75
- if (bytes.length >= 12 &&
76
- bytesToAscii(bytes.slice(0, 4)) === 'RIFF' &&
77
- bytesToAscii(bytes.slice(8, 12)) === 'WEBP') {
78
- return 'WEBP image';
79
- }
80
- if (bytes.length >= 12 &&
81
- bytesToAscii(bytes.slice(4, 8)) === 'ftyp') {
82
- return 'ISO Base Media file (mp4/mov/heic family)';
83
- }
84
- if (bytes.length >= 3 && bytesToAscii(bytes.slice(0, 3)) === 'ID3') {
85
- return 'MP3 audio';
86
- }
87
- if (bytes.length >= 5 && bytesToAscii(bytes.slice(0, 5)).toLowerCase() === '<html') {
88
- return 'HTML document';
89
- }
90
- if (bytes.length >= 1 && bytes[0] === 0x3C) {
91
- return 'XML/HTML-like text';
92
- }
93
- if (bytes.length > 0) {
94
- return 'Unknown binary/text payload';
95
- }
96
- return 'Empty file';
97
- }
98
-
99
- $effect(() => {
100
- const blob = value;
101
- if (!blob) {
102
- headerHex = '';
103
- headerAscii = '';
104
- sniffedType = 'No binary payload available';
105
- return;
106
- }
107
-
108
- let cancelled = false;
109
-
110
- blob.slice(0, 32).arrayBuffer().then((buffer) => {
111
- if (cancelled) return;
112
- const bytes = new Uint8Array(buffer);
113
- headerHex = bytesToHex(bytes);
114
- headerAscii = bytesToAscii(bytes);
115
- sniffedType = detectFromHeader(bytes);
116
- }).catch(() => {
117
- if (cancelled) return;
118
- headerHex = '';
119
- headerAscii = '';
120
- sniffedType = 'Could not inspect file header';
121
- });
122
-
123
- return () => {
124
- cancelled = true;
125
- };
126
- });
1
+ <script lang="ts">import { getEditorType, getExtFromFilename } from "../utils.js";
2
+ let { value, filename, contentType, note } = $props();
3
+ let headerHex = $state("");
4
+ let headerAscii = $state("");
5
+ let sniffedType = $state("Unknown");
6
+ const ext = $derived(filename ? getExtFromFilename(filename) : "");
7
+ const blobType = $derived(value?.type || contentType || "");
8
+ const size = $derived(value?.size ?? 0);
9
+ const inferredViewer = $derived(getEditorType({
10
+ filename,
11
+ contentType: blobType
12
+ }));
13
+ const lastModified = $derived(typeof File !== "undefined" && value instanceof File && Number.isFinite(value.lastModified) ? value.lastModified : undefined);
14
+ function formatBytes(bytes) {
15
+ if (!bytes) return "0 B";
16
+ const units = [
17
+ "B",
18
+ "KB",
19
+ "MB",
20
+ "GB"
21
+ ];
22
+ let next = bytes;
23
+ let unitIndex = 0;
24
+ while (next >= 1024 && unitIndex < units.length - 1) {
25
+ next /= 1024;
26
+ unitIndex += 1;
27
+ }
28
+ const rounded = next >= 10 || unitIndex === 0 ? next.toFixed(0) : next.toFixed(1);
29
+ return `${rounded} ${units[unitIndex]}`;
30
+ }
31
+ function formatDateTime(ms) {
32
+ if (!ms) return null;
33
+ try {
34
+ return new Date(ms).toLocaleString();
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ function bytesToHex(bytes) {
40
+ return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0").toUpperCase()).join(" ");
41
+ }
42
+ function bytesToAscii(bytes) {
43
+ return Array.from(bytes).map((byte) => byte >= 32 && byte <= 126 ? String.fromCharCode(byte) : ".").join("");
44
+ }
45
+ function detectFromHeader(bytes) {
46
+ if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10) {
47
+ return "PNG image";
48
+ }
49
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
50
+ return "JPEG image";
51
+ }
52
+ if (bytes.length >= 6 && bytesToAscii(bytes.slice(0, 6)).startsWith("GIF8")) {
53
+ return "GIF image";
54
+ }
55
+ if (bytes.length >= 4 && bytesToAscii(bytes.slice(0, 4)) === "%PDF") {
56
+ return "PDF document";
57
+ }
58
+ if (bytes.length >= 4 && bytesToAscii(bytes.slice(0, 4)) === "PK..") {
59
+ return "ZIP-based archive or document";
60
+ }
61
+ if (bytes.length >= 12 && bytesToAscii(bytes.slice(0, 4)) === "RIFF" && bytesToAscii(bytes.slice(8, 12)) === "WEBP") {
62
+ return "WEBP image";
63
+ }
64
+ if (bytes.length >= 12 && bytesToAscii(bytes.slice(4, 8)) === "ftyp") {
65
+ return "ISO Base Media file (mp4/mov/heic family)";
66
+ }
67
+ if (bytes.length >= 3 && bytesToAscii(bytes.slice(0, 3)) === "ID3") {
68
+ return "MP3 audio";
69
+ }
70
+ if (bytes.length >= 5 && bytesToAscii(bytes.slice(0, 5)).toLowerCase() === "<html") {
71
+ return "HTML document";
72
+ }
73
+ if (bytes.length >= 1 && bytes[0] === 60) {
74
+ return "XML/HTML-like text";
75
+ }
76
+ if (bytes.length > 0) {
77
+ return "Unknown binary/text payload";
78
+ }
79
+ return "Empty file";
80
+ }
81
+ $effect(() => {
82
+ const blob = value;
83
+ if (!blob) {
84
+ headerHex = "";
85
+ headerAscii = "";
86
+ sniffedType = "No binary payload available";
87
+ return;
88
+ }
89
+ let cancelled = false;
90
+ blob.slice(0, 32).arrayBuffer().then((buffer) => {
91
+ if (cancelled) return;
92
+ const bytes = new Uint8Array(buffer);
93
+ headerHex = bytesToHex(bytes);
94
+ headerAscii = bytesToAscii(bytes);
95
+ sniffedType = detectFromHeader(bytes);
96
+ }).catch(() => {
97
+ if (cancelled) return;
98
+ headerHex = "";
99
+ headerAscii = "";
100
+ sniffedType = "Could not inspect file header";
101
+ });
102
+ return () => {
103
+ cancelled = true;
104
+ };
105
+ });
127
106
  </script>
128
107
 
129
108
  <div class="flex h-full w-full flex-col gap-4 overflow-auto p-5 text-surface-700-300">
@@ -1,20 +1,20 @@
1
- <script lang="ts">
2
- import type { VideoPlayerProps } from '../types.js';
3
- import IconAlertCircle from '../../Icons/IconAlertCircle.svelte';
4
- let { value, filename }: VideoPlayerProps = $props();
5
-
6
- let src = $state('');
7
- let srcType = $state<string | undefined>(undefined);
8
- let err = $state<string | null>(null);
9
-
10
- $effect(() => {
11
- if (!value) { src = ''; srcType = undefined; return; }
12
- err = null;
13
- const url = URL.createObjectURL(value);
14
- src = url;
15
- srcType = value.type || undefined;
16
- return () => URL.revokeObjectURL(url);
17
- });
1
+ <script lang="ts">import IconAlertCircle from "../../Icons/IconAlertCircle.svelte";
2
+ let { value, filename } = $props();
3
+ let src = $state("");
4
+ let srcType = $state(undefined);
5
+ let err = $state(null);
6
+ $effect(() => {
7
+ if (!value) {
8
+ src = "";
9
+ srcType = undefined;
10
+ return;
11
+ }
12
+ err = null;
13
+ const url = URL.createObjectURL(value);
14
+ src = url;
15
+ srcType = value.type || undefined;
16
+ return () => URL.revokeObjectURL(url);
17
+ });
18
18
  </script>
19
19
 
20
20
  <div class="flex h-full w-full flex-col items-center justify-center gap-4 bg-black p-4">
@@ -1,6 +1,5 @@
1
- <script lang="ts">
2
- import type { ImageViewerProps } from '../types.js';
3
- let { value: _value, filename }: ImageViewerProps = $props();
1
+ <script lang="ts">let { value: _value, filename } = $props();
2
+ export {};
4
3
  </script>
5
4
 
6
5
  <div class="card p-4 text-surface-700-300">