@inshapardaz/likhari-react 0.1.7-dev.79 → 0.1.7-dev.84
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/dist/EditorRoot.d.ts +27 -0
- package/dist/EditorRoot.d.ts.map +1 -1
- package/dist/EditorRoot.js +92 -4
- package/dist/components/DraftBanner.d.ts +18 -0
- package/dist/components/DraftBanner.d.ts.map +1 -0
- package/dist/components/DraftBanner.js +43 -0
- package/dist/components/DraftsDialog.d.ts +25 -0
- package/dist/components/DraftsDialog.d.ts.map +1 -0
- package/dist/components/DraftsDialog.js +66 -0
- package/dist/components/Toolbar.d.ts +4 -1
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +8 -27
- package/dist/i18n/strings.d.ts +27 -7
- package/dist/i18n/strings.d.ts.map +1 -1
- package/dist/i18n/strings.js +69 -21
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +0 -4
- package/dist/persistence/draftStorage.d.ts +54 -0
- package/dist/persistence/draftStorage.d.ts.map +1 -0
- package/dist/persistence/draftStorage.js +195 -0
- package/dist/persistence/draftUi.d.ts +9 -0
- package/dist/persistence/draftUi.d.ts.map +1 -0
- package/dist/persistence/draftUi.js +23 -0
- package/dist/theme/editor.css +74 -31
- package/dist/theme/editorTheme.d.ts.map +1 -1
- package/dist/theme/editorTheme.js +0 -5
- package/package.json +1 -2
- package/dist/components/TableDialog.d.ts +0 -17
- package/dist/components/TableDialog.d.ts.map +0 -1
- package/dist/components/TableDialog.js +0 -33
package/dist/i18n/strings.js
CHANGED
|
@@ -18,6 +18,7 @@ const en = {
|
|
|
18
18
|
toolbar: {
|
|
19
19
|
ariaLabel: 'Formatting',
|
|
20
20
|
save: 'Save',
|
|
21
|
+
drafts: 'Drafts',
|
|
21
22
|
undo: 'Undo',
|
|
22
23
|
redo: 'Redo',
|
|
23
24
|
formattingLabel: 'Formatting',
|
|
@@ -33,7 +34,6 @@ const en = {
|
|
|
33
34
|
linkOptions: 'Link options',
|
|
34
35
|
insertLink: 'Insert link (Ctrl+K)',
|
|
35
36
|
insertImage: 'Insert image',
|
|
36
|
-
insertTable: 'Insert table',
|
|
37
37
|
poetryBlocks: 'Poetry blocks',
|
|
38
38
|
comingSoon: (label) => `${label} (coming soon)`,
|
|
39
39
|
autocorrect: 'Auto-correct',
|
|
@@ -66,6 +66,28 @@ const en = {
|
|
|
66
66
|
right: 'Align right',
|
|
67
67
|
},
|
|
68
68
|
},
|
|
69
|
+
drafts: {
|
|
70
|
+
bannerMessage: (when) => `An unsaved draft of this document from ${when} was found.`,
|
|
71
|
+
restore: 'Restore',
|
|
72
|
+
ignore: 'Ignore',
|
|
73
|
+
remove: 'Remove draft',
|
|
74
|
+
dialogTitle: 'Saved drafts',
|
|
75
|
+
intro: 'Drafts are saved in this browser as you type, and removed when you save.',
|
|
76
|
+
empty: 'No drafts yet.',
|
|
77
|
+
noText: '(no text)',
|
|
78
|
+
thisDocument: 'Current document',
|
|
79
|
+
untitled: 'Untitled draft',
|
|
80
|
+
documentLabel: (id) => `Document “${id}”`,
|
|
81
|
+
earlierUntitled: 'Earlier version of an untitled draft',
|
|
82
|
+
earlierVersionOf: (id) => `Earlier version of “${id}”`,
|
|
83
|
+
savedAt: (when) => `Saved ${when}`,
|
|
84
|
+
size: (kb) => `${kb} KB`,
|
|
85
|
+
replacePrompt: 'Replace the current content with this draft?',
|
|
86
|
+
replace: 'Replace',
|
|
87
|
+
deletePrompt: 'Delete this draft?',
|
|
88
|
+
delete: 'Delete',
|
|
89
|
+
close: 'Close',
|
|
90
|
+
},
|
|
69
91
|
link: {
|
|
70
92
|
menuLabel: 'Link',
|
|
71
93
|
noUrl: '(no URL)',
|
|
@@ -82,12 +104,6 @@ const en = {
|
|
|
82
104
|
invalidUrl: 'Enter a valid http(s), mailto, tel or relative URL',
|
|
83
105
|
apply: 'Apply',
|
|
84
106
|
},
|
|
85
|
-
tableDialog: {
|
|
86
|
-
title: 'Insert table',
|
|
87
|
-
rows: 'Rows',
|
|
88
|
-
columns: 'Columns',
|
|
89
|
-
headerRow: 'Header row',
|
|
90
|
-
},
|
|
91
107
|
imageDialog: {
|
|
92
108
|
titleInsert: 'Insert image',
|
|
93
109
|
titleEdit: 'Edit image',
|
|
@@ -161,6 +177,7 @@ const ur = {
|
|
|
161
177
|
toolbar: {
|
|
162
178
|
ariaLabel: 'فارمیٹنگ',
|
|
163
179
|
save: 'محفوظ کریں',
|
|
180
|
+
drafts: 'مسودے',
|
|
164
181
|
undo: 'کالعدم کریں',
|
|
165
182
|
redo: 'دوبارہ کریں',
|
|
166
183
|
formattingLabel: 'فارمیٹنگ',
|
|
@@ -176,7 +193,6 @@ const ur = {
|
|
|
176
193
|
linkOptions: 'لنک کے اختیارات',
|
|
177
194
|
insertLink: 'لنک شامل کریں (Ctrl+K)',
|
|
178
195
|
insertImage: 'تصویر شامل کریں',
|
|
179
|
-
insertTable: 'جدول شامل کریں',
|
|
180
196
|
poetryBlocks: 'شاعری کے بلاکس',
|
|
181
197
|
comingSoon: (label) => `${label} (جلد آ رہا ہے)`,
|
|
182
198
|
autocorrect: 'خودکار تصحیح',
|
|
@@ -209,6 +225,28 @@ const ur = {
|
|
|
209
225
|
right: 'دائیں سیدھ',
|
|
210
226
|
},
|
|
211
227
|
},
|
|
228
|
+
drafts: {
|
|
229
|
+
bannerMessage: (when) => `اس دستاویز کا ایک غیر محفوظ شدہ مسودہ (${when}) ملا ہے۔`,
|
|
230
|
+
restore: 'بحال کریں',
|
|
231
|
+
ignore: 'نظر انداز کریں',
|
|
232
|
+
remove: 'مسودہ ہٹائیں',
|
|
233
|
+
dialogTitle: 'محفوظ شدہ مسودے',
|
|
234
|
+
intro: 'لکھتے وقت مسودے اس براؤزر میں خود بخود محفوظ ہوتے ہیں اور محفوظ کرنے پر ہٹا دیے جاتے ہیں۔',
|
|
235
|
+
empty: 'ابھی کوئی مسودہ نہیں۔',
|
|
236
|
+
noText: '(کوئی متن نہیں)',
|
|
237
|
+
thisDocument: 'موجودہ دستاویز',
|
|
238
|
+
untitled: 'بلا عنوان مسودہ',
|
|
239
|
+
documentLabel: (id) => `دستاویز «${id}»`,
|
|
240
|
+
earlierUntitled: 'ایک بلا عنوان مسودے کا پرانا ورژن',
|
|
241
|
+
earlierVersionOf: (id) => `«${id}» کا پرانا ورژن`,
|
|
242
|
+
savedAt: (when) => `محفوظ شدہ: ${when}`,
|
|
243
|
+
size: (kb) => `${kb} کے بی`,
|
|
244
|
+
replacePrompt: 'موجودہ مواد کو اس مسودے سے بدل دیں؟',
|
|
245
|
+
replace: 'بدل دیں',
|
|
246
|
+
deletePrompt: 'یہ مسودہ حذف کریں؟',
|
|
247
|
+
delete: 'حذف کریں',
|
|
248
|
+
close: 'بند کریں',
|
|
249
|
+
},
|
|
212
250
|
link: {
|
|
213
251
|
menuLabel: 'لنک',
|
|
214
252
|
noUrl: '(کوئی یو آر ایل نہیں)',
|
|
@@ -225,12 +263,6 @@ const ur = {
|
|
|
225
263
|
invalidUrl: 'براہِ کرم ایک درست http(s)، mailto، tel یا رشتہ دار یو آر ایل درج کریں',
|
|
226
264
|
apply: 'لاگو کریں',
|
|
227
265
|
},
|
|
228
|
-
tableDialog: {
|
|
229
|
-
title: 'جدول شامل کریں',
|
|
230
|
-
rows: 'قطاریں',
|
|
231
|
-
columns: 'کالم',
|
|
232
|
-
headerRow: 'سرِ فہرست قطار',
|
|
233
|
-
},
|
|
234
266
|
imageDialog: {
|
|
235
267
|
titleInsert: 'تصویر شامل کریں',
|
|
236
268
|
titleEdit: 'تصویر میں ترمیم کریں',
|
|
@@ -304,6 +336,7 @@ const paShahmukhi = {
|
|
|
304
336
|
toolbar: {
|
|
305
337
|
ariaLabel: 'فارمیٹنگ',
|
|
306
338
|
save: 'سنبھالو',
|
|
339
|
+
drafts: 'سودھے',
|
|
307
340
|
undo: 'پہلاں جیہا کرو',
|
|
308
341
|
redo: 'مُڑ کرو',
|
|
309
342
|
formattingLabel: 'فارمیٹنگ',
|
|
@@ -319,7 +352,6 @@ const paShahmukhi = {
|
|
|
319
352
|
linkOptions: 'لنک دے اختیار',
|
|
320
353
|
insertLink: 'لنک پاؤ (Ctrl+K)',
|
|
321
354
|
insertImage: 'تصویر پاؤ',
|
|
322
|
-
insertTable: 'ٹیبل پاؤ',
|
|
323
355
|
poetryBlocks: 'شاعری بلاک',
|
|
324
356
|
comingSoon: (label) => `${label} (چھیتی آ رہا اے)`,
|
|
325
357
|
autocorrect: 'خودکار درستی',
|
|
@@ -352,6 +384,28 @@ const paShahmukhi = {
|
|
|
352
384
|
right: 'سجے سیدھ',
|
|
353
385
|
},
|
|
354
386
|
},
|
|
387
|
+
drafts: {
|
|
388
|
+
bannerMessage: (when) => `ایس دستاویز دا اک غیر محفوظ سودھا (${when}) لبھیا اے۔`,
|
|
389
|
+
restore: 'بحال کرو',
|
|
390
|
+
ignore: 'نظر انداز کرو',
|
|
391
|
+
remove: 'سودھا ہٹاؤ',
|
|
392
|
+
dialogTitle: 'سنبھالے ہوئے سودھے',
|
|
393
|
+
intro: 'لکھدیاں ہویاں سودھے ایس براؤزر وچ آپ سنبھلدے نیں تے سنبھالن تے ہٹ جاندے نیں۔',
|
|
394
|
+
empty: 'ہلے کوئی سودھا نئیں۔',
|
|
395
|
+
noText: '(کوئی متن نئیں)',
|
|
396
|
+
thisDocument: 'موجودہ دستاویز',
|
|
397
|
+
untitled: 'بنا ناں دا سودھا',
|
|
398
|
+
documentLabel: (id) => `دستاویز «${id}»`,
|
|
399
|
+
earlierUntitled: 'اک بنا ناں دے سودھے دا پرانا ورژن',
|
|
400
|
+
earlierVersionOf: (id) => `«${id}» دا پرانا ورژن`,
|
|
401
|
+
savedAt: (when) => `سنبھالیا: ${when}`,
|
|
402
|
+
size: (kb) => `${kb} کے بی`,
|
|
403
|
+
replacePrompt: 'موجودہ مواد نوں ایس سودھے نال بدل دیئے؟',
|
|
404
|
+
replace: 'بدل دیو',
|
|
405
|
+
deletePrompt: 'ایہ سودھا مٹا دیئے؟',
|
|
406
|
+
delete: 'مٹاؤ',
|
|
407
|
+
close: 'بند کرو',
|
|
408
|
+
},
|
|
355
409
|
link: {
|
|
356
410
|
menuLabel: 'لنک',
|
|
357
411
|
noUrl: '(کوئی یو آر ایل نئیں)',
|
|
@@ -368,12 +422,6 @@ const paShahmukhi = {
|
|
|
368
422
|
invalidUrl: 'کِرپا کر کے صحیح http(s)، mailto، tel یا نسبتی یو آر ایل پاؤ',
|
|
369
423
|
apply: 'لاگو کرو',
|
|
370
424
|
},
|
|
371
|
-
tableDialog: {
|
|
372
|
-
title: 'ٹیبل پاؤ',
|
|
373
|
-
rows: 'قطاراں',
|
|
374
|
-
columns: 'کالم',
|
|
375
|
-
headerRow: 'سرکڑی قطار',
|
|
376
|
-
},
|
|
377
425
|
imageDialog: {
|
|
378
426
|
titleInsert: 'تصویر پاؤ',
|
|
379
427
|
titleEdit: 'تصویر وچ تبدیلی کرو',
|
package/dist/nodes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nodes.d.ts","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"nodes.d.ts","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAOlD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,WAAW,CAAC,EAQ5C,CAAC"}
|
package/dist/nodes.js
CHANGED
|
@@ -2,7 +2,6 @@ import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
|
|
2
2
|
import { ListItemNode, ListNode } from '@lexical/list';
|
|
3
3
|
import { LinkNode } from '@lexical/link';
|
|
4
4
|
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
|
|
5
|
-
import { TableCellNode, TableNode, TableRowNode } from '@lexical/table';
|
|
6
5
|
import { ImageNode } from './image/ImageNode.js';
|
|
7
6
|
/**
|
|
8
7
|
* Node types registered on the editor at construction time. Per
|
|
@@ -20,8 +19,5 @@ export const EDITOR_NODES = [
|
|
|
20
19
|
ListItemNode,
|
|
21
20
|
LinkNode,
|
|
22
21
|
HorizontalRuleNode,
|
|
23
|
-
TableNode,
|
|
24
|
-
TableRowNode,
|
|
25
|
-
TableCellNode,
|
|
26
22
|
ImageNode,
|
|
27
23
|
];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autosave-to-localStorage drafts (lexical-editor-spec.md §6.2). Each draft is
|
|
3
|
+
* keyed by an id: the host's `documentId` when it gives one (§6.1), otherwise
|
|
4
|
+
* a generated unique id, so editors without a `documentId` never overwrite each
|
|
5
|
+
* other's drafts. Every function is a no-op (never throws) when `localStorage`
|
|
6
|
+
* is unavailable (SSR, privacy mode) or a stored value is corrupt — autosave is
|
|
7
|
+
* best-effort, never a hard dependency.
|
|
8
|
+
*/
|
|
9
|
+
export interface StoredDraft {
|
|
10
|
+
/** Lexical JSON (as produced by `EditorState.toJSON()`, stringified). */
|
|
11
|
+
json: string;
|
|
12
|
+
/** `Date.now()` at write time — lets a host distinguish a stale draft from a genuinely recent one. */
|
|
13
|
+
savedAt: number;
|
|
14
|
+
/** Bumped only if the stored shape itself changes; lets old drafts be recognised/discarded across an upgrade. */
|
|
15
|
+
version: 1;
|
|
16
|
+
/** First words of the text, for listing drafts without parsing every document. */
|
|
17
|
+
preview?: string;
|
|
18
|
+
/** Set on a draft archived from `documentId` (the id it was an earlier version of). */
|
|
19
|
+
sourceId?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface DraftEntry {
|
|
22
|
+
id: string;
|
|
23
|
+
draft: StoredDraft;
|
|
24
|
+
}
|
|
25
|
+
export declare function draftKey(id: string): string;
|
|
26
|
+
/** A new unique id for a draft whose editor has no `documentId`. */
|
|
27
|
+
export declare function createDraftId(): string;
|
|
28
|
+
/** Whether an id came from createDraftId() rather than a host's `documentId`. */
|
|
29
|
+
export declare function isGeneratedDraftId(id: string): boolean;
|
|
30
|
+
/** One-line text preview of a Lexical JSON document ('' when it has no text). */
|
|
31
|
+
export declare function extractPreview(json: string): string;
|
|
32
|
+
/** Whether a Lexical JSON document has any text or image (a blank one isn't worth a draft). */
|
|
33
|
+
export declare function hasDraftContent(json: string): boolean;
|
|
34
|
+
export declare function readDraft(id: string): StoredDraft | null;
|
|
35
|
+
/**
|
|
36
|
+
* Best-effort write. Silently skips (rather than throwing) when `json` exceeds
|
|
37
|
+
* `maxBytes` — localStorage has a shared ~5-10MB per-origin ceiling (§6.2), so a
|
|
38
|
+
* single large document (e.g. embedded base64 images) must not be allowed to
|
|
39
|
+
* autosave past a caller-configured limit — or when the write itself fails
|
|
40
|
+
* (quota exceeded, privacy mode).
|
|
41
|
+
*/
|
|
42
|
+
export declare function writeDraft(id: string, json: string, maxBytes: number, sourceId?: string): void;
|
|
43
|
+
export declare function clearDraft(id: string): void;
|
|
44
|
+
/** Every stored draft, newest first. */
|
|
45
|
+
export declare function listDrafts(): DraftEntry[];
|
|
46
|
+
/**
|
|
47
|
+
* Keeps a draft that is about to be overwritten or ignored: copies it to a new
|
|
48
|
+
* generated id (remembering `sourceId`) so it stays in the drafts list.
|
|
49
|
+
* Returns the new id, or null if nothing could be copied.
|
|
50
|
+
*/
|
|
51
|
+
export declare function archiveDraft(id: string): string | null;
|
|
52
|
+
/** Deletes the oldest drafts beyond `max`, never touching `keepIds`. */
|
|
53
|
+
export declare function pruneDrafts(max: number, keepIds?: string[]): void;
|
|
54
|
+
//# sourceMappingURL=draftStorage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draftStorage.d.ts","sourceRoot":"","sources":["../../src/persistence/draftStorage.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,MAAM,WAAW,WAAW;IAC1B,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,sGAAsG;IACtG,OAAO,EAAE,MAAM,CAAC;IAChB,iHAAiH;IACjH,OAAO,EAAE,CAAC,CAAC;IACX,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,WAAW,CAAC;CACpB;AAED,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED,oEAAoE;AACpE,wBAAgB,aAAa,IAAI,MAAM,CAOtC;AAED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEtD;AAgDD,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,+FAA+F;AAC/F,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAaD,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAQxD;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAc9F;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAQ3C;AAED,wCAAwC;AACxC,wBAAgB,UAAU,IAAI,UAAU,EAAE,CAezC;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWtD;AAED,wEAAwE;AACxE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,EAAO,GAAG,IAAI,CAQrE"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autosave-to-localStorage drafts (lexical-editor-spec.md §6.2). Each draft is
|
|
3
|
+
* keyed by an id: the host's `documentId` when it gives one (§6.1), otherwise
|
|
4
|
+
* a generated unique id, so editors without a `documentId` never overwrite each
|
|
5
|
+
* other's drafts. Every function is a no-op (never throws) when `localStorage`
|
|
6
|
+
* is unavailable (SSR, privacy mode) or a stored value is corrupt — autosave is
|
|
7
|
+
* best-effort, never a hard dependency.
|
|
8
|
+
*/
|
|
9
|
+
const KEY_PREFIX = 'likhari:draft:';
|
|
10
|
+
const PREVIEW_LENGTH = 140;
|
|
11
|
+
export function draftKey(id) {
|
|
12
|
+
return `${KEY_PREFIX}${id}`;
|
|
13
|
+
}
|
|
14
|
+
/** A new unique id for a draft whose editor has no `documentId`. */
|
|
15
|
+
export function createDraftId() {
|
|
16
|
+
try {
|
|
17
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')
|
|
18
|
+
return crypto.randomUUID();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// fall through to the non-crypto id
|
|
22
|
+
}
|
|
23
|
+
return `draft-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
24
|
+
}
|
|
25
|
+
/** Whether an id came from createDraftId() rather than a host's `documentId`. */
|
|
26
|
+
export function isGeneratedDraftId(id) {
|
|
27
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id) || /^draft-[0-9a-z]+-[0-9a-z]+$/.test(id);
|
|
28
|
+
}
|
|
29
|
+
function getLocalStorage() {
|
|
30
|
+
try {
|
|
31
|
+
return typeof localStorage === 'undefined' ? null : localStorage;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Some browsers throw on accessing localStorage in certain privacy modes.
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function collectText(node, parts, state) {
|
|
39
|
+
if (node.type === 'text' && typeof node.text === 'string') {
|
|
40
|
+
if (node.text.trim())
|
|
41
|
+
state.hasContent = true;
|
|
42
|
+
parts.push(node.text);
|
|
43
|
+
}
|
|
44
|
+
else if (node.type === 'linebreak') {
|
|
45
|
+
parts.push(' ');
|
|
46
|
+
}
|
|
47
|
+
else if (node.type === 'image') {
|
|
48
|
+
state.hasContent = true;
|
|
49
|
+
if (node.altText)
|
|
50
|
+
parts.push(`[${node.altText}]`);
|
|
51
|
+
}
|
|
52
|
+
for (const child of node.children ?? [])
|
|
53
|
+
collectText(child, parts, state);
|
|
54
|
+
// Block boundaries read as a space in a one-line preview.
|
|
55
|
+
if (node.children && node.type !== 'root')
|
|
56
|
+
parts.push(' ');
|
|
57
|
+
}
|
|
58
|
+
function summarize(json) {
|
|
59
|
+
try {
|
|
60
|
+
const root = JSON.parse(json).root;
|
|
61
|
+
if (!root)
|
|
62
|
+
return { preview: '', hasContent: false };
|
|
63
|
+
const parts = [];
|
|
64
|
+
const state = { hasContent: false };
|
|
65
|
+
collectText(root, parts, state);
|
|
66
|
+
const text = parts.join('').replace(/\s+/g, ' ').trim();
|
|
67
|
+
const preview = text.length > PREVIEW_LENGTH ? `${text.slice(0, PREVIEW_LENGTH).trimEnd()}…` : text;
|
|
68
|
+
return { preview, hasContent: state.hasContent };
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return { preview: '', hasContent: false };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** One-line text preview of a Lexical JSON document ('' when it has no text). */
|
|
75
|
+
export function extractPreview(json) {
|
|
76
|
+
return summarize(json).preview;
|
|
77
|
+
}
|
|
78
|
+
/** Whether a Lexical JSON document has any text or image (a blank one isn't worth a draft). */
|
|
79
|
+
export function hasDraftContent(json) {
|
|
80
|
+
return summarize(json).hasContent;
|
|
81
|
+
}
|
|
82
|
+
function parseDraft(raw) {
|
|
83
|
+
if (!raw)
|
|
84
|
+
return null;
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(raw);
|
|
87
|
+
if (typeof parsed.json !== 'string' || typeof parsed.savedAt !== 'number' || parsed.version !== 1)
|
|
88
|
+
return null;
|
|
89
|
+
return parsed;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function readDraft(id) {
|
|
96
|
+
const storage = getLocalStorage();
|
|
97
|
+
if (!storage)
|
|
98
|
+
return null;
|
|
99
|
+
try {
|
|
100
|
+
return parseDraft(storage.getItem(draftKey(id)));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Best-effort write. Silently skips (rather than throwing) when `json` exceeds
|
|
108
|
+
* `maxBytes` — localStorage has a shared ~5-10MB per-origin ceiling (§6.2), so a
|
|
109
|
+
* single large document (e.g. embedded base64 images) must not be allowed to
|
|
110
|
+
* autosave past a caller-configured limit — or when the write itself fails
|
|
111
|
+
* (quota exceeded, privacy mode).
|
|
112
|
+
*/
|
|
113
|
+
export function writeDraft(id, json, maxBytes, sourceId) {
|
|
114
|
+
const storage = getLocalStorage();
|
|
115
|
+
if (!storage)
|
|
116
|
+
return;
|
|
117
|
+
// .length (UTF-16 code units) over-estimates UTF-8 byte size for any
|
|
118
|
+
// non-Latin text, which only makes this check more conservative — fine
|
|
119
|
+
// for a soft cap that doesn't need to be exact.
|
|
120
|
+
if (json.length > maxBytes)
|
|
121
|
+
return;
|
|
122
|
+
const draft = { json, savedAt: Date.now(), version: 1, preview: extractPreview(json) };
|
|
123
|
+
if (sourceId)
|
|
124
|
+
draft.sourceId = sourceId;
|
|
125
|
+
try {
|
|
126
|
+
storage.setItem(draftKey(id), JSON.stringify(draft));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Quota exceeded or similar — autosave is best-effort.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export function clearDraft(id) {
|
|
133
|
+
const storage = getLocalStorage();
|
|
134
|
+
if (!storage)
|
|
135
|
+
return;
|
|
136
|
+
try {
|
|
137
|
+
storage.removeItem(draftKey(id));
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// ignore
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Every stored draft, newest first. */
|
|
144
|
+
export function listDrafts() {
|
|
145
|
+
const storage = getLocalStorage();
|
|
146
|
+
if (!storage)
|
|
147
|
+
return [];
|
|
148
|
+
const entries = [];
|
|
149
|
+
try {
|
|
150
|
+
for (let i = 0; i < storage.length; i++) {
|
|
151
|
+
const key = storage.key(i);
|
|
152
|
+
if (!key || !key.startsWith(KEY_PREFIX))
|
|
153
|
+
continue;
|
|
154
|
+
const draft = parseDraft(storage.getItem(key));
|
|
155
|
+
if (draft)
|
|
156
|
+
entries.push({ id: key.slice(KEY_PREFIX.length), draft });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return entries;
|
|
161
|
+
}
|
|
162
|
+
return entries.sort((a, b) => b.draft.savedAt - a.draft.savedAt);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Keeps a draft that is about to be overwritten or ignored: copies it to a new
|
|
166
|
+
* generated id (remembering `sourceId`) so it stays in the drafts list.
|
|
167
|
+
* Returns the new id, or null if nothing could be copied.
|
|
168
|
+
*/
|
|
169
|
+
export function archiveDraft(id) {
|
|
170
|
+
const storage = getLocalStorage();
|
|
171
|
+
const draft = readDraft(id);
|
|
172
|
+
if (!storage || !draft)
|
|
173
|
+
return null;
|
|
174
|
+
const archivedId = createDraftId();
|
|
175
|
+
try {
|
|
176
|
+
storage.setItem(draftKey(archivedId), JSON.stringify({ ...draft, sourceId: draft.sourceId ?? id }));
|
|
177
|
+
return archivedId;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** Deletes the oldest drafts beyond `max`, never touching `keepIds`. */
|
|
184
|
+
export function pruneDrafts(max, keepIds = []) {
|
|
185
|
+
if (max < 1)
|
|
186
|
+
return;
|
|
187
|
+
const removable = listDrafts().filter((entry) => !keepIds.includes(entry.id));
|
|
188
|
+
const kept = listDrafts().length - removable.length;
|
|
189
|
+
const excess = removable.length + kept - max;
|
|
190
|
+
if (excess <= 0)
|
|
191
|
+
return;
|
|
192
|
+
// removable is newest first, so the oldest are at the end.
|
|
193
|
+
for (const entry of removable.slice(-excess))
|
|
194
|
+
clearDraft(entry.id);
|
|
195
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LexicalEditor } from 'lexical';
|
|
2
|
+
import type { Locale } from '../i18n/index.js';
|
|
3
|
+
/** Replaces the editor's content with a stored draft's Lexical JSON. Returns
|
|
4
|
+
* false (leaving the editor untouched) if the draft is corrupt or from an
|
|
5
|
+
* incompatible node schema, rather than throwing. */
|
|
6
|
+
export declare function applyDraftToEditor(editor: LexicalEditor, json: string): boolean;
|
|
7
|
+
/** A draft's save time as a short localized date and time. */
|
|
8
|
+
export declare function formatDraftTime(savedAt: number, locale: Locale): string;
|
|
9
|
+
//# sourceMappingURL=draftUi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draftUi.d.ts","sourceRoot":"","sources":["../../src/persistence/draftUi.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEtC;;qDAEqD;AACrD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAO/E;AAID,8DAA8D;AAC9D,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAOvE"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Replaces the editor's content with a stored draft's Lexical JSON. Returns
|
|
2
|
+
* false (leaving the editor untouched) if the draft is corrupt or from an
|
|
3
|
+
* incompatible node schema, rather than throwing. */
|
|
4
|
+
export function applyDraftToEditor(editor, json) {
|
|
5
|
+
try {
|
|
6
|
+
editor.setEditorState(editor.parseEditorState(json));
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const INTL_LOCALES = { en: 'en', ur: 'ur', 'pa-shahmukhi': 'pa-Arab' };
|
|
14
|
+
/** A draft's save time as a short localized date and time. */
|
|
15
|
+
export function formatDraftTime(savedAt, locale) {
|
|
16
|
+
const date = new Date(savedAt);
|
|
17
|
+
try {
|
|
18
|
+
return new Intl.DateTimeFormat(INTL_LOCALES[locale], { dateStyle: 'medium', timeStyle: 'short' }).format(date);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return date.toLocaleString();
|
|
22
|
+
}
|
|
23
|
+
}
|
package/dist/theme/editor.css
CHANGED
|
@@ -253,37 +253,6 @@
|
|
|
253
253
|
margin: 1.5em 0;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
/* Tables — cell merge/split is deliberately out of scope for now (a flagged
|
|
257
|
-
open risk, lexical-editor-spec.md §12 item 3, needing its own scope
|
|
258
|
-
decision); TablePlugin is configured with hasCellMerge={false} to keep
|
|
259
|
-
every table a plain grid until that's resolved. */
|
|
260
|
-
.likhari-table {
|
|
261
|
-
border-collapse: collapse;
|
|
262
|
-
margin: 1em 0;
|
|
263
|
-
width: 100%;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
.likhari-table-row {
|
|
267
|
-
border-bottom: 1px solid var(--editor-line);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
.likhari-table-cell {
|
|
271
|
-
border: 1px solid var(--editor-line);
|
|
272
|
-
padding: 6px 10px;
|
|
273
|
-
min-width: 72px;
|
|
274
|
-
vertical-align: top;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
.likhari-table-cell-header {
|
|
278
|
-
background: var(--editor-paper-dim);
|
|
279
|
-
font-weight: 600;
|
|
280
|
-
text-align: start;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
.likhari-table-cell-selected {
|
|
284
|
-
background: var(--editor-accent-soft);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
256
|
/* Images — block-level figure; the caption is static text (edited in the
|
|
288
257
|
image's dialog), and a selected image gets a corner resize handle. */
|
|
289
258
|
.likhari-image-block {
|
|
@@ -401,3 +370,77 @@
|
|
|
401
370
|
text-overflow: ellipsis;
|
|
402
371
|
white-space: nowrap;
|
|
403
372
|
}
|
|
373
|
+
|
|
374
|
+
/* Restore-draft banner: a strip between the toolbar and the canvas. Uses the
|
|
375
|
+
editor's own tokens, so it follows the accent color and dark mode. */
|
|
376
|
+
.likhari-draft-banner {
|
|
377
|
+
display: flex;
|
|
378
|
+
flex-shrink: 0;
|
|
379
|
+
flex-wrap: wrap;
|
|
380
|
+
align-items: center;
|
|
381
|
+
gap: 8px 12px;
|
|
382
|
+
padding: 8px 12px;
|
|
383
|
+
background: var(--editor-accent-soft);
|
|
384
|
+
border-bottom: 1px solid var(--editor-line);
|
|
385
|
+
color: var(--editor-ink);
|
|
386
|
+
font-size: 13px;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.likhari-draft-banner-message {
|
|
390
|
+
display: flex;
|
|
391
|
+
flex: 1 1 240px;
|
|
392
|
+
align-items: center;
|
|
393
|
+
gap: 8px;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.likhari-draft-banner-message svg {
|
|
397
|
+
flex-shrink: 0;
|
|
398
|
+
color: var(--editor-accent);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.likhari-draft-banner-actions {
|
|
402
|
+
display: flex;
|
|
403
|
+
flex-wrap: wrap;
|
|
404
|
+
gap: 6px;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.likhari-draft-banner-button {
|
|
408
|
+
height: 28px;
|
|
409
|
+
padding: 0 12px;
|
|
410
|
+
border: 1px solid var(--editor-line-strong);
|
|
411
|
+
border-radius: 6px;
|
|
412
|
+
background: var(--editor-paper);
|
|
413
|
+
color: var(--editor-ink);
|
|
414
|
+
font: inherit;
|
|
415
|
+
font-weight: 500;
|
|
416
|
+
cursor: pointer;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.likhari-draft-banner-button:hover {
|
|
420
|
+
background: var(--editor-paper-dim);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.likhari-draft-banner-button[data-variant='primary'] {
|
|
424
|
+
border-color: var(--editor-accent);
|
|
425
|
+
background: var(--editor-accent);
|
|
426
|
+
color: var(--editor-paper);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.likhari-draft-banner-button[data-variant='primary']:hover {
|
|
430
|
+
filter: brightness(1.1);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.likhari-draft-banner-button[data-variant='danger'] {
|
|
434
|
+
border-color: transparent;
|
|
435
|
+
background: transparent;
|
|
436
|
+
color: var(--editor-danger);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.likhari-draft-banner-button[data-variant='danger']:hover {
|
|
440
|
+
background: var(--editor-paper-dim);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.likhari-draft-banner-button:focus-visible {
|
|
444
|
+
outline: 2px solid var(--editor-accent);
|
|
445
|
+
outline-offset: 1px;
|
|
446
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editorTheme.d.ts","sourceRoot":"","sources":["../../src/theme/editorTheme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"editorTheme.d.ts","sourceRoot":"","sources":["../../src/theme/editorTheme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,kBA8BzB,CAAC"}
|
|
@@ -33,9 +33,4 @@ export const editorTheme = {
|
|
|
33
33
|
},
|
|
34
34
|
hr: 'likhari-hr',
|
|
35
35
|
image: 'likhari-image-block',
|
|
36
|
-
table: 'likhari-table',
|
|
37
|
-
tableRow: 'likhari-table-row',
|
|
38
|
-
tableCell: 'likhari-table-cell',
|
|
39
|
-
tableCellHeader: 'likhari-table-cell-header',
|
|
40
|
-
tableCellSelected: 'likhari-table-cell-selected',
|
|
41
36
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inshapardaz/likhari-react",
|
|
3
|
-
"version": "0.1.7-dev.
|
|
3
|
+
"version": "0.1.7-dev.84",
|
|
4
4
|
"description": "React implementation of the Likhari editor, built on @lexical/react.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
"@lexical/react": "^0.20.0",
|
|
38
38
|
"@lexical/rich-text": "^0.20.0",
|
|
39
39
|
"@lexical/selection": "^0.20.0",
|
|
40
|
-
"@lexical/table": "^0.20.0",
|
|
41
40
|
"@lexical/utils": "^0.20.0",
|
|
42
41
|
"@inshapardaz/likhari-converters": "^0.1.0",
|
|
43
42
|
"@inshapardaz/likhari-core": "^0.1.0",
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export interface TableDialogValue {
|
|
2
|
-
rows: number;
|
|
3
|
-
columns: number;
|
|
4
|
-
headerRow: boolean;
|
|
5
|
-
}
|
|
6
|
-
export interface TableDialogProps {
|
|
7
|
-
opened: boolean;
|
|
8
|
-
onSubmit: (value: TableDialogValue) => void;
|
|
9
|
-
onClose: () => void;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Insert-table dialog: rows x columns and an optional header row. Cell
|
|
13
|
-
* merge/split is intentionally not offered anywhere in this dialog or the
|
|
14
|
-
* table it creates — see the comment on `TablePlugin` in `EditorRoot.tsx`.
|
|
15
|
-
*/
|
|
16
|
-
export declare function TableDialog({ opened, onSubmit, onClose }: TableDialogProps): import("react").JSX.Element;
|
|
17
|
-
//# sourceMappingURL=TableDialog.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"TableDialog.d.ts","sourceRoot":"","sources":["../../src/components/TableDialog.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC5C,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAOD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,gBAAgB,+BAiD1E"}
|