@inshapardaz/likhari-react 0.1.7-dev.81 → 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.
@@ -9,7 +9,6 @@ const en = {
9
9
  placeholder: 'Start writing…',
10
10
  contentLabel: 'Editor content',
11
11
  confirmDiscard: 'You have unsaved changes. Discard them?',
12
- pageBreakLabel: 'Page break',
13
12
  },
14
13
  common: {
15
14
  cancel: 'Cancel',
@@ -19,6 +18,7 @@ const en = {
19
18
  toolbar: {
20
19
  ariaLabel: 'Formatting',
21
20
  save: 'Save',
21
+ drafts: 'Drafts',
22
22
  undo: 'Undo',
23
23
  redo: 'Redo',
24
24
  formattingLabel: 'Formatting',
@@ -34,7 +34,6 @@ const en = {
34
34
  linkOptions: 'Link options',
35
35
  insertLink: 'Insert link (Ctrl+K)',
36
36
  insertImage: 'Insert image',
37
- insertPageBreak: 'Insert page break',
38
37
  poetryBlocks: 'Poetry blocks',
39
38
  comingSoon: (label) => `${label} (coming soon)`,
40
39
  autocorrect: 'Auto-correct',
@@ -67,6 +66,28 @@ const en = {
67
66
  right: 'Align right',
68
67
  },
69
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
+ },
70
91
  link: {
71
92
  menuLabel: 'Link',
72
93
  noUrl: '(no URL)',
@@ -147,7 +168,6 @@ const ur = {
147
168
  placeholder: 'لکھنا شروع کریں…',
148
169
  contentLabel: 'ایڈیٹر کا مواد',
149
170
  confirmDiscard: 'آپ کی غیر محفوظ شدہ تبدیلیاں ہیں۔ کیا انہیں رد کر دیا جائے؟',
150
- pageBreakLabel: 'صفحے کی تقسیم',
151
171
  },
152
172
  common: {
153
173
  cancel: 'منسوخ کریں',
@@ -157,6 +177,7 @@ const ur = {
157
177
  toolbar: {
158
178
  ariaLabel: 'فارمیٹنگ',
159
179
  save: 'محفوظ کریں',
180
+ drafts: 'مسودے',
160
181
  undo: 'کالعدم کریں',
161
182
  redo: 'دوبارہ کریں',
162
183
  formattingLabel: 'فارمیٹنگ',
@@ -172,7 +193,6 @@ const ur = {
172
193
  linkOptions: 'لنک کے اختیارات',
173
194
  insertLink: 'لنک شامل کریں (Ctrl+K)',
174
195
  insertImage: 'تصویر شامل کریں',
175
- insertPageBreak: 'صفحے کی تقسیم شامل کریں',
176
196
  poetryBlocks: 'شاعری کے بلاکس',
177
197
  comingSoon: (label) => `${label} (جلد آ رہا ہے)`,
178
198
  autocorrect: 'خودکار تصحیح',
@@ -205,6 +225,28 @@ const ur = {
205
225
  right: 'دائیں سیدھ',
206
226
  },
207
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
+ },
208
250
  link: {
209
251
  menuLabel: 'لنک',
210
252
  noUrl: '(کوئی یو آر ایل نہیں)',
@@ -285,7 +327,6 @@ const paShahmukhi = {
285
327
  placeholder: 'لکھنا شروع کرو…',
286
328
  contentLabel: 'ایڈیٹر دی سامگری',
287
329
  confirmDiscard: 'تہاڈیاں کجھ تبدیلیاں سنبھالیاں نئیں گئیاں۔ کی ایہناں نوں رد کر دیئے؟',
288
- pageBreakLabel: 'صفحے دی ونڈ',
289
330
  },
290
331
  common: {
291
332
  cancel: 'رد کرو',
@@ -295,6 +336,7 @@ const paShahmukhi = {
295
336
  toolbar: {
296
337
  ariaLabel: 'فارمیٹنگ',
297
338
  save: 'سنبھالو',
339
+ drafts: 'سودھے',
298
340
  undo: 'پہلاں جیہا کرو',
299
341
  redo: 'مُڑ کرو',
300
342
  formattingLabel: 'فارمیٹنگ',
@@ -310,7 +352,6 @@ const paShahmukhi = {
310
352
  linkOptions: 'لنک دے اختیار',
311
353
  insertLink: 'لنک پاؤ (Ctrl+K)',
312
354
  insertImage: 'تصویر پاؤ',
313
- insertPageBreak: 'صفحے دی ونڈ پاؤ',
314
355
  poetryBlocks: 'شاعری بلاک',
315
356
  comingSoon: (label) => `${label} (چھیتی آ رہا اے)`,
316
357
  autocorrect: 'خودکار درستی',
@@ -343,6 +384,28 @@ const paShahmukhi = {
343
384
  right: 'سجے سیدھ',
344
385
  },
345
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
+ },
346
409
  link: {
347
410
  menuLabel: 'لنک',
348
411
  noUrl: '(کوئی یو آر ایل نئیں)',
@@ -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;AAQlD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,WAAW,CAAC,EAS5C,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
@@ -3,7 +3,6 @@ import { ListItemNode, ListNode } from '@lexical/list';
3
3
  import { LinkNode } from '@lexical/link';
4
4
  import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
5
5
  import { ImageNode } from './image/ImageNode.js';
6
- import { PageBreakNode } from './blocks/PageBreakNode.js';
7
6
  /**
8
7
  * Node types registered on the editor at construction time. Per
9
8
  * editor-architecture-design.md §2, node registration cannot be conditional
@@ -21,5 +20,4 @@ export const EDITOR_NODES = [
21
20
  LinkNode,
22
21
  HorizontalRuleNode,
23
22
  ImageNode,
24
- PageBreakNode,
25
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
+ }
@@ -253,45 +253,6 @@
253
253
  margin: 1.5em 0;
254
254
  }
255
255
 
256
- /* Page break — a print/export concept with no visual meaning while editing
257
- on screen, so it gets its own clearly-labeled dashed marker rather than
258
- masquerading as a horizontal rule. The actual page-break behavior only
259
- takes effect when printing (lexical-editor-spec.md §4.3/§4.6). */
260
- .likhari-page-break {
261
- display: flex;
262
- align-items: center;
263
- gap: 8px;
264
- margin: 1.5em 0;
265
- font-size: 12px;
266
- cursor: default;
267
- }
268
-
269
- .likhari-page-break-line {
270
- flex: 1 1 auto;
271
- border-top: 1px dashed var(--editor-line);
272
- }
273
-
274
- .likhari-page-break-label {
275
- color: var(--editor-ink-soft);
276
- white-space: nowrap;
277
- }
278
-
279
- .likhari-page-break-selected {
280
- outline: 2px solid var(--editor-accent);
281
- outline-offset: 2px;
282
- border-radius: 4px;
283
- }
284
-
285
- @media print {
286
- .likhari-page-break {
287
- page-break-after: always;
288
- /* Hide the on-screen marker itself when actually printing — the
289
- browser's own pagination (triggered by page-break-after) is what
290
- matters there, not the editing-time label. */
291
- visibility: hidden;
292
- }
293
- }
294
-
295
256
  /* Images — block-level figure; the caption is static text (edited in the
296
257
  image's dialog), and a selected image gets a corner resize handle. */
297
258
  .likhari-image-block {
@@ -409,3 +370,77 @@
409
370
  text-overflow: ellipsis;
410
371
  white-space: nowrap;
411
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,kBA+BzB,CAAC"}
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,5 +33,4 @@ export const editorTheme = {
33
33
  },
34
34
  hr: 'likhari-hr',
35
35
  image: 'likhari-image-block',
36
- pageBreak: 'likhari-page-break',
37
36
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inshapardaz/likhari-react",
3
- "version": "0.1.7-dev.81",
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": {
@@ -1,30 +0,0 @@
1
- import { type ReactElement } from 'react';
2
- import { DecoratorNode, type DOMConversionMap, type DOMExportOutput, type EditorConfig, type LexicalCommand, type LexicalNode, type SerializedLexicalNode } from 'lexical';
3
- export declare const INSERT_PAGE_BREAK_COMMAND: LexicalCommand<void>;
4
- export type SerializedPageBreakNode = SerializedLexicalNode;
5
- /**
6
- * A manual page break — a print/paginated-export concept with no native
7
- * Lexical node (lexical-editor-spec.md §4.3/§4.6). Renders as a visible
8
- * dashed marker while editing, and as `page-break-after: always` both live
9
- * (a print stylesheet, editor.css) and in exported HTML (`exportDOM`).
10
- *
11
- * Mirrors @lexical/react's HorizontalRuleNode pattern: `createDOM` returns
12
- * the actual visible element (the click target `PageBreakComponent` wires
13
- * selection/delete against), rather than `decorate()` rendering visuals.
14
- */
15
- export declare class PageBreakNode extends DecoratorNode<ReactElement> {
16
- static getType(): string;
17
- static clone(node: PageBreakNode): PageBreakNode;
18
- static importJSON(): PageBreakNode;
19
- exportJSON(): SerializedPageBreakNode;
20
- static importDOM(): DOMConversionMap | null;
21
- exportDOM(): DOMExportOutput;
22
- createDOM(config: EditorConfig): HTMLElement;
23
- getTextContent(): string;
24
- isInline(): false;
25
- updateDOM(): boolean;
26
- decorate(): import("react").JSX.Element;
27
- }
28
- export declare function $createPageBreakNode(): PageBreakNode;
29
- export declare function $isPageBreakNode(node: LexicalNode | null | undefined): node is PageBreakNode;
30
- //# sourceMappingURL=PageBreakNode.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"PageBreakNode.d.ts","sourceRoot":"","sources":["../../src/blocks/PageBreakNode.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AAKlE,OAAO,EAOL,aAAa,EAGb,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,WAAW,EAEhB,KAAK,qBAAqB,EAC3B,MAAM,SAAS,CAAC;AAEjB,eAAO,MAAM,yBAAyB,EAAE,cAAc,CAAC,IAAI,CAA8C,CAAC;AAE1G,MAAM,MAAM,uBAAuB,GAAG,qBAAqB,CAAC;AA2D5D;;;;;;;;;GASG;AACH,qBAAa,aAAc,SAAQ,aAAa,CAAC,YAAY,CAAC;IAC5D,MAAM,CAAC,OAAO,IAAI,MAAM;IAIxB,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,aAAa,GAAG,aAAa;IAIhD,MAAM,CAAC,UAAU,IAAI,aAAa;IAIlC,UAAU,IAAI,uBAAuB;IAIrC,MAAM,CAAC,SAAS,IAAI,gBAAgB,GAAG,IAAI;IAS3C,SAAS,IAAI,eAAe;IAO5B,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,WAAW;IAM5C,cAAc,IAAI,MAAM;IAIxB,QAAQ,IAAI,KAAK;IAIjB,SAAS,IAAI,OAAO;IAIpB,QAAQ;CAGT;AAED,wBAAgB,oBAAoB,IAAI,aAAa,CAEpD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,IAAI,aAAa,CAE5F"}