@keepkit/ui 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -107,6 +107,8 @@ var DE_LABELS = {
107
107
  filterCollections: "Nach Sammlung filtern",
108
108
  uncategorized: "Nicht kategorisiert",
109
109
  saveWithNote: "Mit Notiz speichern",
110
+ editSavedItem: "Gespeichertes Element bearbeiten",
111
+ unsavedChanges: "Nicht gespeicherte \xC4nderungen",
110
112
  save: "Speichern",
111
113
  saved: "Gespeichert",
112
114
  remove: "Entfernen",
@@ -204,6 +206,8 @@ var EN_LABELS = {
204
206
  filterCollections: "Filter collections",
205
207
  uncategorized: "Uncategorized",
206
208
  saveWithNote: "Save with note",
209
+ editSavedItem: "Edit saved item",
210
+ unsavedChanges: "Unsaved changes",
207
211
  save: "Save",
208
212
  saved: "Saved",
209
213
  remove: "Remove",
@@ -301,6 +305,8 @@ var ES_LABELS = {
301
305
  filterCollections: "Filtrar por colecci\xF3n",
302
306
  uncategorized: "Sin categor\xEDa",
303
307
  saveWithNote: "Guardar con nota",
308
+ editSavedItem: "Editar elemento guardado",
309
+ unsavedChanges: "Cambios sin guardar",
304
310
  save: "Guardar",
305
311
  saved: "Guardado",
306
312
  remove: "Eliminar",
@@ -398,6 +404,8 @@ var FIL_LABELS = {
398
404
  filterCollections: "I-filter ayon sa koleksyon",
399
405
  uncategorized: "Walang kategorya",
400
406
  saveWithNote: "I-save na may tala",
407
+ editSavedItem: "I-edit ang naka-save na item",
408
+ unsavedChanges: "Mga hindi pa nai-save na pagbabago",
401
409
  save: "I-save",
402
410
  saved: "Nai-save",
403
411
  remove: "Alisin",
@@ -495,6 +503,8 @@ var FR_LABELS = {
495
503
  filterCollections: "Filtrer par collection",
496
504
  uncategorized: "Non class\xE9",
497
505
  saveWithNote: "Enregistrer avec une note",
506
+ editSavedItem: "Modifier l\u2019\xE9l\xE9ment enregistr\xE9",
507
+ unsavedChanges: "Modifications non enregistr\xE9es",
498
508
  save: "Enregistrer",
499
509
  saved: "Enregistr\xE9",
500
510
  remove: "Supprimer",
@@ -592,6 +602,8 @@ var ID_LABELS = {
592
602
  filterCollections: "Filter koleksi",
593
603
  uncategorized: "Tanpa kategori",
594
604
  saveWithNote: "Simpan dengan catatan",
605
+ editSavedItem: "Edit item tersimpan",
606
+ unsavedChanges: "Perubahan belum disimpan",
595
607
  save: "Simpan",
596
608
  saved: "Tersimpan",
597
609
  remove: "Hapus",
@@ -689,6 +701,8 @@ var IT_LABELS = {
689
701
  filterCollections: "Filtra per raccolta",
690
702
  uncategorized: "Senza categoria",
691
703
  saveWithNote: "Salva con nota",
704
+ editSavedItem: "Modifica elemento salvato",
705
+ unsavedChanges: "Modifiche non salvate",
692
706
  save: "Salva",
693
707
  saved: "Salvato",
694
708
  remove: "Rimuovi",
@@ -786,6 +800,8 @@ var JA_LABELS = {
786
800
  filterCollections: "\u30B3\u30EC\u30AF\u30B7\u30E7\u30F3\u3067\u7D5E\u308A\u8FBC\u3080",
787
801
  uncategorized: "\u672A\u5206\u985E",
788
802
  saveWithNote: "\u30E1\u30E2\u4ED8\u304D\u3067\u4FDD\u5B58",
803
+ editSavedItem: "\u4FDD\u5B58\u3057\u305F\u30A2\u30A4\u30C6\u30E0\u3092\u7DE8\u96C6",
804
+ unsavedChanges: "\u672A\u4FDD\u5B58\u306E\u5909\u66F4\u304C\u3042\u308A\u307E\u3059",
789
805
  save: "\u4FDD\u5B58",
790
806
  saved: "\u4FDD\u5B58\u6E08\u307F",
791
807
  remove: "\u524A\u9664",
@@ -883,6 +899,8 @@ var KO_LABELS = {
883
899
  filterCollections: "\uCEEC\uB809\uC158 \uD544\uD130",
884
900
  uncategorized: "\uBBF8\uBD84\uB958",
885
901
  saveWithNote: "\uBA54\uBAA8\uC640 \uD568\uAED8 \uC800\uC7A5",
902
+ editSavedItem: "\uC800\uC7A5\uB41C \uD56D\uBAA9 \uD3B8\uC9D1",
903
+ unsavedChanges: "\uC800\uC7A5\uB418\uC9C0 \uC54A\uC740 \uBCC0\uACBD \uC0AC\uD56D",
886
904
  save: "\uC800\uC7A5",
887
905
  saved: "\uC800\uC7A5\uB428",
888
906
  remove: "\uC0AD\uC81C",
@@ -980,6 +998,8 @@ var MS_LABELS = {
980
998
  filterCollections: "Tapis koleksi",
981
999
  uncategorized: "Tiada kategori",
982
1000
  saveWithNote: "Simpan dengan nota",
1001
+ editSavedItem: "Edit item yang disimpan",
1002
+ unsavedChanges: "Perubahan belum disimpan",
983
1003
  save: "Simpan",
984
1004
  saved: "Disimpan",
985
1005
  remove: "Buang",
@@ -1077,6 +1097,8 @@ var PT_BR_LABELS = {
1077
1097
  filterCollections: "Filtrar por cole\xE7\xE3o",
1078
1098
  uncategorized: "Sem categoria",
1079
1099
  saveWithNote: "Salvar com nota",
1100
+ editSavedItem: "Editar item salvo",
1101
+ unsavedChanges: "Altera\xE7\xF5es n\xE3o salvas",
1080
1102
  save: "Salvar",
1081
1103
  saved: "Salvo",
1082
1104
  remove: "Remover",
@@ -1174,6 +1196,8 @@ var RU_LABELS = {
1174
1196
  filterCollections: "\u0424\u0438\u043B\u044C\u0442\u0440 \u043A\u043E\u043B\u043B\u0435\u043A\u0446\u0438\u0439",
1175
1197
  uncategorized: "\u0411\u0435\u0437 \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u0438",
1176
1198
  saveWithNote: "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0441 \u0437\u0430\u043C\u0435\u0442\u043A\u043E\u0439",
1199
+ editSavedItem: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0439 \u044D\u043B\u0435\u043C\u0435\u043D\u0442",
1200
+ unsavedChanges: "\u041D\u0435\u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u044B\u0435 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F",
1177
1201
  save: "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C",
1178
1202
  saved: "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043E",
1179
1203
  remove: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C",
@@ -1271,6 +1295,8 @@ var TH_LABELS = {
1271
1295
  filterCollections: "\u0E01\u0E23\u0E2D\u0E07\u0E04\u0E2D\u0E25\u0E40\u0E25\u0E01\u0E0A\u0E31\u0E19",
1272
1296
  uncategorized: "\u0E44\u0E21\u0E48\u0E21\u0E35\u0E2B\u0E21\u0E27\u0E14\u0E2B\u0E21\u0E39\u0E48",
1273
1297
  saveWithNote: "\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E1E\u0E23\u0E49\u0E2D\u0E21\u0E42\u0E19\u0E49\u0E15",
1298
+ editSavedItem: "\u0E41\u0E01\u0E49\u0E44\u0E02\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E17\u0E35\u0E48\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E44\u0E27\u0E49",
1299
+ unsavedChanges: "\u0E21\u0E35\u0E01\u0E32\u0E23\u0E40\u0E1B\u0E25\u0E35\u0E48\u0E22\u0E19\u0E41\u0E1B\u0E25\u0E07\u0E17\u0E35\u0E48\u0E22\u0E31\u0E07\u0E44\u0E21\u0E48\u0E44\u0E14\u0E49\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01",
1274
1300
  save: "\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01",
1275
1301
  saved: "\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01\u0E41\u0E25\u0E49\u0E27",
1276
1302
  remove: "\u0E25\u0E1A",
@@ -1368,6 +1394,8 @@ var VI_LABELS = {
1368
1394
  filterCollections: "L\u1ECDc theo b\u1ED9 s\u01B0u t\u1EADp",
1369
1395
  uncategorized: "Ch\u01B0a ph\xE2n lo\u1EA1i",
1370
1396
  saveWithNote: "L\u01B0u k\xE8m ghi ch\xFA",
1397
+ editSavedItem: "Ch\u1EC9nh s\u1EEDa m\u1EE5c \u0111\xE3 l\u01B0u",
1398
+ unsavedChanges: "Thay \u0111\u1ED5i ch\u01B0a \u0111\u01B0\u1EE3c l\u01B0u",
1371
1399
  save: "L\u01B0u",
1372
1400
  saved: "\u0110\xE3 l\u01B0u",
1373
1401
  remove: "X\xF3a",
@@ -1465,6 +1493,8 @@ var ZH_HANS_LABELS = {
1465
1493
  filterCollections: "\u6309\u96C6\u5408\u7B5B\u9009",
1466
1494
  uncategorized: "\u672A\u5206\u7C7B",
1467
1495
  saveWithNote: "\u4FDD\u5B58\u5E76\u6DFB\u52A0\u5907\u6CE8",
1496
+ editSavedItem: "\u7F16\u8F91\u5DF2\u4FDD\u5B58\u9879\u76EE",
1497
+ unsavedChanges: "\u6709\u672A\u4FDD\u5B58\u7684\u66F4\u6539",
1468
1498
  save: "\u4FDD\u5B58",
1469
1499
  saved: "\u5DF2\u4FDD\u5B58",
1470
1500
  remove: "\u5220\u9664",
@@ -1562,6 +1592,8 @@ var ZH_HANT_LABELS = {
1562
1592
  filterCollections: "\u4F9D\u6536\u85CF\u96C6\u7BE9\u9078",
1563
1593
  uncategorized: "\u672A\u5206\u985E",
1564
1594
  saveWithNote: "\u5132\u5B58\u4E26\u52A0\u5099\u8A3B",
1595
+ editSavedItem: "\u7DE8\u8F2F\u5DF2\u5132\u5B58\u9805\u76EE",
1596
+ unsavedChanges: "\u6709\u672A\u5132\u5B58\u7684\u8B8A\u66F4",
1565
1597
  save: "\u5132\u5B58",
1566
1598
  saved: "\u5DF2\u5132\u5B58",
1567
1599
  remove: "\u522A\u9664",
@@ -2464,7 +2496,13 @@ function renderIcon(icon, className) {
2464
2496
 
2465
2497
  // src/features/actions/KeepSavePopover.tsx
2466
2498
  import { useKeepItem as useKeepItem4 } from "@keepkit/core/react";
2467
- import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef5, useState as useState4 } from "react";
2499
+ import {
2500
+ useCallback as useCallback3,
2501
+ useEffect as useEffect4,
2502
+ useId,
2503
+ useRef as useRef5,
2504
+ useState as useState4
2505
+ } from "react";
2468
2506
 
2469
2507
  // src/features/editor/KeepQuickEditor.tsx
2470
2508
  import { useKeepContext as useKeepContext2, useKeepItem as useKeepItem3 } from "@keepkit/core/react";
@@ -2483,12 +2521,16 @@ function useKeepQuickEditor(item, options = {}) {
2483
2521
  const [note, setNote] = useState3(baseline.note ?? "");
2484
2522
  const [tags, setTags] = useState3(baseline.tags ?? []);
2485
2523
  const [collectionId, setCollectionId] = useState3(baseline.collectionId);
2524
+ const [saveStatus, setSaveStatus] = useState3("idle");
2525
+ const [saveError, setSaveError] = useState3(null);
2486
2526
  const baselineRef = useRef4({
2487
2527
  note: baseline.note ?? "",
2488
2528
  tags: baseline.tags ?? [],
2489
2529
  collectionId: baseline.collectionId
2490
2530
  });
2491
2531
  const draftRef = useRef4({ note, tags, collectionId });
2532
+ const flushRequestedRef = useRef4(false);
2533
+ const flushPromiseRef = useRef4(null);
2492
2534
  draftRef.current = { note, tags, collectionId };
2493
2535
  useEffect3(() => {
2494
2536
  if (draftRef.current.note === baselineRef.current.note && sameTags(draftRef.current.tags, baselineRef.current.tags) && draftRef.current.collectionId === baselineRef.current.collectionId) {
@@ -2503,24 +2545,49 @@ function useKeepQuickEditor(item, options = {}) {
2503
2545
  }
2504
2546
  }, [baseline]);
2505
2547
  const isDirty = note !== baselineRef.current.note || !sameTags(tags, baselineRef.current.tags) || collectionId !== baselineRef.current.collectionId;
2506
- const flush = useCallback2(async () => {
2507
- const draft = draftRef.current;
2508
- try {
2509
- if (draft.note !== baselineRef.current.note) await updateNote(draft.note.trim() || void 0);
2510
- if (!sameTags(draft.tags, baselineRef.current.tags)) await updateTags(draft.tags);
2511
- if (draft.collectionId !== baselineRef.current.collectionId) await moveToCollection(draft.collectionId);
2512
- baselineRef.current = { note: draft.note, tags: [...draft.tags], collectionId: draft.collectionId };
2513
- const { collectionId: _oldCollectionId, ...withoutCollection } = baseline;
2514
- onSaved?.({
2515
- ...withoutCollection,
2516
- note: draft.note.trim() || void 0,
2517
- tags: draft.tags,
2518
- ...draft.collectionId ? { collectionId: draft.collectionId } : {}
2519
- });
2520
- } catch (error2) {
2521
- onSaveError?.(error2);
2522
- throw error2;
2523
- }
2548
+ useEffect3(() => {
2549
+ if (!isDirty && saveStatus === "dirty" && !flushPromiseRef.current) setSaveStatus("idle");
2550
+ }, [isDirty, saveStatus]);
2551
+ const flush = useCallback2(() => {
2552
+ flushRequestedRef.current = true;
2553
+ if (flushPromiseRef.current) return flushPromiseRef.current;
2554
+ const run = async () => {
2555
+ let saved = false;
2556
+ while (flushRequestedRef.current || !sameDraft(draftRef.current, baselineRef.current)) {
2557
+ flushRequestedRef.current = false;
2558
+ const draft = { ...draftRef.current, tags: [...draftRef.current.tags] };
2559
+ const previous = baselineRef.current;
2560
+ if (sameDraft(draft, previous)) continue;
2561
+ setSaveError(null);
2562
+ setSaveStatus("saving");
2563
+ try {
2564
+ if (draft.note !== previous.note) await updateNote(draft.note.trim() || void 0);
2565
+ if (!sameTags(draft.tags, previous.tags)) await updateTags(draft.tags);
2566
+ if (draft.collectionId !== previous.collectionId) await moveToCollection(draft.collectionId);
2567
+ } catch (error2) {
2568
+ setSaveError(error2);
2569
+ setSaveStatus("error");
2570
+ onSaveError?.(error2);
2571
+ throw error2;
2572
+ }
2573
+ baselineRef.current = draft;
2574
+ saved = true;
2575
+ const { collectionId: _oldCollectionId, ...withoutCollection } = baseline;
2576
+ onSaved?.({
2577
+ ...withoutCollection,
2578
+ note: draft.note.trim() || void 0,
2579
+ tags: draft.tags,
2580
+ ...draft.collectionId ? { collectionId: draft.collectionId } : {}
2581
+ });
2582
+ if (!sameDraft(draftRef.current, baselineRef.current)) flushRequestedRef.current = true;
2583
+ }
2584
+ if (saved) setSaveStatus("saved");
2585
+ };
2586
+ const promise = run().finally(() => {
2587
+ flushPromiseRef.current = null;
2588
+ });
2589
+ flushPromiseRef.current = promise;
2590
+ return promise;
2524
2591
  }, [baseline, moveToCollection, onSaveError, onSaved, updateNote, updateTags]);
2525
2592
  useEffect3(() => {
2526
2593
  if (!isDirty || debounceMs <= 0) return;
@@ -2533,12 +2600,25 @@ function useKeepQuickEditor(item, options = {}) {
2533
2600
  note,
2534
2601
  tags,
2535
2602
  collectionId,
2536
- setNote,
2537
- setTags,
2538
- setCollectionId,
2603
+ setNote: (value) => {
2604
+ setSaveError(null);
2605
+ setSaveStatus("dirty");
2606
+ setNote(value);
2607
+ },
2608
+ setTags: (value) => {
2609
+ setSaveError(null);
2610
+ setSaveStatus("dirty");
2611
+ setTags(value);
2612
+ },
2613
+ setCollectionId: (value) => {
2614
+ setSaveError(null);
2615
+ setSaveStatus("dirty");
2616
+ setCollectionId(value);
2617
+ },
2539
2618
  isDirty,
2540
- isSaving: isMutating,
2541
- error,
2619
+ isSaving: isMutating || saveStatus === "saving",
2620
+ saveStatus,
2621
+ error: saveError ?? error,
2542
2622
  flush
2543
2623
  }
2544
2624
  };
@@ -2546,17 +2626,24 @@ function useKeepQuickEditor(item, options = {}) {
2546
2626
  function KeepQuickEditor({
2547
2627
  item,
2548
2628
  debounceMs = 300,
2629
+ onSaved,
2630
+ onSaveError,
2631
+ ...props
2632
+ }) {
2633
+ const view = useKeepQuickEditor(item, { debounceMs, onSaved, onSaveError });
2634
+ return /* @__PURE__ */ jsx7(KeepQuickEditorView, { ...props, state: view.state });
2635
+ }
2636
+ function KeepQuickEditorView({
2637
+ state,
2549
2638
  collectionIds,
2550
2639
  collectionLabels,
2640
+ showSaveButton = true,
2551
2641
  children,
2552
2642
  onClose,
2553
- onSaved,
2554
- onSaveError,
2643
+ focusScopeRef,
2555
2644
  ...props
2556
2645
  }) {
2557
- const view = useKeepQuickEditor(item, { debounceMs, onSaved, onSaveError });
2558
2646
  const context = useKeepContext2();
2559
- const { state } = view;
2560
2647
  const rootRef = useRef4(null);
2561
2648
  const noteLabel = useUiLabel("note");
2562
2649
  const tagsLabel = useUiLabel("tags");
@@ -2564,17 +2651,24 @@ function KeepQuickEditor({
2564
2651
  const uncategorizedLabel = useUiLabel("uncategorized");
2565
2652
  const saveLabel = useUiLabel("saveWithNote");
2566
2653
  const closeLabel = useUiLabel("close");
2654
+ const unsavedChangesLabel = useUiLabel("unsavedChanges");
2655
+ const savingLabel = useUiLabel("saving");
2656
+ const savedLabel = useUiLabel("saved");
2657
+ const errorLabel = useUiLabel("error");
2567
2658
  const submit = (event) => {
2568
2659
  event.preventDefault();
2569
2660
  void state.flush().catch(() => void 0);
2570
2661
  };
2571
2662
  const onKeyDown = (event) => {
2663
+ props.onKeyDown?.(event);
2664
+ if (event.defaultPrevented) return;
2572
2665
  if (event.key === "Escape") {
2573
2666
  event.preventDefault();
2574
2667
  void state.flush().then(() => onClose?.()).catch(() => void 0);
2575
2668
  }
2576
- if (event.key === "Tab") trapFocus(event, rootRef.current);
2669
+ if (event.key === "Tab") trapFocus(event, focusScopeRef?.current ?? rootRef.current);
2577
2670
  };
2671
+ const statusMessage = state.saveStatus === "dirty" ? unsavedChangesLabel : state.saveStatus === "saving" ? savingLabel : state.saveStatus === "saved" ? savedLabel : state.saveStatus === "error" ? getErrorMessage2(state.error, errorLabel) : null;
2578
2672
  const body = typeof children === "function" ? children(state) : children ?? /* @__PURE__ */ jsxs5(Fragment4, { children: [
2579
2673
  /* @__PURE__ */ jsxs5("label", { children: [
2580
2674
  noteLabel,
@@ -2610,17 +2704,28 @@ function KeepQuickEditor({
2610
2704
  }
2611
2705
  )
2612
2706
  ] }),
2613
- /* @__PURE__ */ jsx7("button", { type: "submit", disabled: state.isSaving, "data-keep-action": "save-quick-edit", children: saveLabel }),
2614
- /* @__PURE__ */ jsx7(
2707
+ statusMessage ? /* @__PURE__ */ jsx7(
2708
+ "p",
2709
+ {
2710
+ role: state.saveStatus === "error" ? "alert" : "status",
2711
+ "aria-live": state.saveStatus === "error" ? "assertive" : "polite",
2712
+ tabIndex: state.saveStatus === "error" ? -1 : void 0,
2713
+ "data-keep-editor-status": "true",
2714
+ "data-state": state.saveStatus,
2715
+ children: statusMessage
2716
+ }
2717
+ ) : null,
2718
+ showSaveButton ? /* @__PURE__ */ jsx7("button", { type: "submit", disabled: state.isSaving, "data-keep-action": "save-quick-edit", children: saveLabel }) : null,
2719
+ onClose ? /* @__PURE__ */ jsx7(
2615
2720
  "button",
2616
2721
  {
2617
2722
  type: "button",
2618
- onClick: () => void state.flush().then(() => onClose?.()).catch(() => void 0),
2723
+ onClick: () => void state.flush().then(onClose).catch(() => void 0),
2619
2724
  disabled: state.isSaving,
2620
2725
  "data-keep-action": "close-quick-edit",
2621
2726
  children: closeLabel
2622
2727
  }
2623
- )
2728
+ ) : null
2624
2729
  ] });
2625
2730
  return /* @__PURE__ */ jsx7(
2626
2731
  "form",
@@ -2631,11 +2736,18 @@ function KeepQuickEditor({
2631
2736
  onKeyDown,
2632
2737
  "data-keepkit": "quick-editor",
2633
2738
  "data-state": state.isDirty ? "dirty" : "clean",
2739
+ "data-save-status": state.saveStatus,
2634
2740
  "aria-busy": state.isSaving || props["aria-busy"],
2635
2741
  children: body
2636
2742
  }
2637
2743
  );
2638
2744
  }
2745
+ function sameDraft(left, right) {
2746
+ return left.note === right.note && sameTags(left.tags, right.tags) && left.collectionId === right.collectionId;
2747
+ }
2748
+ function getErrorMessage2(error, fallback) {
2749
+ return error instanceof Error ? error.message : fallback;
2750
+ }
2639
2751
  function sameTags(left, right) {
2640
2752
  return left.length === right.length && left.every((tag, index) => tag === right[index]);
2641
2753
  }
@@ -2663,6 +2775,7 @@ function KeepSavePopover({
2663
2775
  open: controlledOpen,
2664
2776
  defaultOpen = false,
2665
2777
  onOpenChange,
2778
+ dialogLabel,
2666
2779
  buttonProps,
2667
2780
  editorProps,
2668
2781
  children,
@@ -2672,18 +2785,24 @@ function KeepSavePopover({
2672
2785
  const [internalOpen, setInternalOpen] = useState4(defaultOpen);
2673
2786
  const isOpen = controlledOpen ?? internalOpen;
2674
2787
  const triggerRef = useRef5(null);
2788
+ const wasOpenRef = useRef5(isOpen);
2675
2789
  const previousSaved = useRef5(state.isSaved);
2676
2790
  const observedHydration = useRef5(!state.isLoading);
2677
- const closeLabel = useUiLabel("close");
2791
+ const panelId = useId();
2792
+ const titleId = `${panelId}-title`;
2793
+ const editSavedItemLabel = useUiLabel("editSavedItem");
2678
2794
  const { ref: _buttonRef, ...resolvedButtonProps } = buttonProps ?? {};
2679
2795
  const setOpen = useCallback3(
2680
2796
  (next) => {
2681
2797
  if (controlledOpen === void 0) setInternalOpen(next);
2682
2798
  onOpenChange?.(next);
2683
- if (!next) window.setTimeout(() => triggerRef.current?.focus(), 0);
2684
2799
  },
2685
2800
  [controlledOpen, onOpenChange]
2686
2801
  );
2802
+ useEffect4(() => {
2803
+ if (wasOpenRef.current && !isOpen) window.setTimeout(() => triggerRef.current?.focus(), 0);
2804
+ wasOpenRef.current = isOpen;
2805
+ }, [isOpen]);
2687
2806
  useEffect4(() => {
2688
2807
  if (!observedHydration.current) {
2689
2808
  if (state.isLoading) return;
@@ -2694,16 +2813,112 @@ function KeepSavePopover({
2694
2813
  if (!previousSaved.current && state.isSaved) setOpen(true);
2695
2814
  previousSaved.current = state.isSaved;
2696
2815
  }, [setOpen, state.isLoading, state.isSaved]);
2816
+ useEffect4(() => {
2817
+ if (isOpen && observedHydration.current && !state.isSaved) setOpen(false);
2818
+ }, [isOpen, setOpen, state.isSaved]);
2697
2819
  const currentItem = state.item;
2698
2820
  return /* @__PURE__ */ jsxs6("div", { ...props, "data-keepkit": "save-popover", "data-open": isOpen ? "true" : "false", children: [
2699
- /* @__PURE__ */ jsx8(KeepButton, { ...resolvedButtonProps, item, asChild: false, ref: triggerRef }),
2700
- isOpen && currentItem ? /* @__PURE__ */ jsxs6("div", { role: "dialog", "aria-label": children ? void 0 : closeLabel, "data-keep-popover-panel": "true", children: [
2701
- children,
2702
- /* @__PURE__ */ jsx8(KeepQuickEditor, { ...editorProps, item: currentItem, onClose: () => setOpen(false) }),
2703
- /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => setOpen(false), "data-keep-action": "close-save-popover", children: closeLabel })
2704
- ] }) : null
2821
+ /* @__PURE__ */ jsx8(
2822
+ KeepButton,
2823
+ {
2824
+ ...resolvedButtonProps,
2825
+ item,
2826
+ asChild: false,
2827
+ ref: triggerRef,
2828
+ "aria-haspopup": "dialog",
2829
+ "aria-expanded": isOpen,
2830
+ "aria-controls": panelId
2831
+ }
2832
+ ),
2833
+ isOpen && currentItem ? /* @__PURE__ */ jsx8(
2834
+ KeepSavePopoverPanel,
2835
+ {
2836
+ id: panelId,
2837
+ titleId,
2838
+ dialogLabel: dialogLabel ?? editSavedItemLabel,
2839
+ item: currentItem,
2840
+ editorProps,
2841
+ triggerRef,
2842
+ onClose: () => setOpen(false),
2843
+ children
2844
+ }
2845
+ ) : null
2705
2846
  ] });
2706
2847
  }
2848
+ function KeepSavePopoverPanel({
2849
+ id,
2850
+ titleId,
2851
+ dialogLabel,
2852
+ item,
2853
+ editorProps,
2854
+ triggerRef,
2855
+ onClose,
2856
+ children
2857
+ }) {
2858
+ const { debounceMs, onSaved, onSaveError, ...editorViewProps } = editorProps ?? {};
2859
+ const { state } = useKeepQuickEditor(item, { debounceMs, onSaved, onSaveError });
2860
+ const panelRef = useRef5(null);
2861
+ const closingRef = useRef5(null);
2862
+ const closeLabel = useUiLabel("close");
2863
+ const requestClose = useCallback3(() => {
2864
+ if (closingRef.current) return closingRef.current;
2865
+ const promise = state.flush().then(onClose).catch(() => {
2866
+ window.setTimeout(() => panelRef.current?.querySelector('[role="alert"]')?.focus(), 0);
2867
+ }).finally(() => {
2868
+ closingRef.current = null;
2869
+ });
2870
+ closingRef.current = promise;
2871
+ return promise;
2872
+ }, [onClose, state]);
2873
+ useEffect4(() => {
2874
+ const frame = window.requestAnimationFrame(() => {
2875
+ panelRef.current?.querySelector('[data-keepkit="quick-editor"] :is(textarea, input, select, button)')?.focus();
2876
+ });
2877
+ return () => window.cancelAnimationFrame(frame);
2878
+ }, []);
2879
+ useEffect4(() => {
2880
+ const closeFromOutside = (event) => {
2881
+ const target = event.target;
2882
+ if (!(target instanceof Node) || panelRef.current?.contains(target) || triggerRef.current?.contains(target))
2883
+ return;
2884
+ void requestClose();
2885
+ };
2886
+ document.addEventListener("pointerdown", closeFromOutside);
2887
+ return () => document.removeEventListener("pointerdown", closeFromOutside);
2888
+ }, [requestClose, triggerRef]);
2889
+ return /* @__PURE__ */ jsxs6(
2890
+ "div",
2891
+ {
2892
+ ref: panelRef,
2893
+ id,
2894
+ role: "dialog",
2895
+ "aria-labelledby": titleId,
2896
+ "data-keep-popover-panel": "true",
2897
+ "data-save-status": state.saveStatus,
2898
+ onKeyDownCapture: (event) => {
2899
+ if (event.key !== "Escape") return;
2900
+ event.preventDefault();
2901
+ event.stopPropagation();
2902
+ void requestClose();
2903
+ },
2904
+ children: [
2905
+ /* @__PURE__ */ jsx8("h2", { id: titleId, "data-keep-popover-title": "true", children: dialogLabel }),
2906
+ children,
2907
+ /* @__PURE__ */ jsx8(KeepQuickEditorView, { ...editorViewProps, state, focusScopeRef: panelRef }),
2908
+ /* @__PURE__ */ jsx8(
2909
+ "button",
2910
+ {
2911
+ type: "button",
2912
+ onClick: () => void requestClose(),
2913
+ disabled: state.isSaving,
2914
+ "data-keep-action": "close-save-popover",
2915
+ children: closeLabel
2916
+ }
2917
+ )
2918
+ ]
2919
+ }
2920
+ );
2921
+ }
2707
2922
 
2708
2923
  // src/features/actions/hooks/useKeepUndo.ts
2709
2924
  import { useKeepContext as useKeepContext3 } from "@keepkit/core/react";
@@ -3854,26 +4069,10 @@ function KeepItemCardRoot({
3854
4069
  " ",
3855
4070
  /* @__PURE__ */ jsx16("time", { dateTime: new Date(item.savedAt).toISOString(), children: formatSavedAt(item.savedAt) })
3856
4071
  ] }) : null;
3857
- const error = view.itemState.error ? /* @__PURE__ */ jsx16("p", { role: "alert", children: getErrorMessage2(view.itemState.error, view.labels.error) }) : null;
4072
+ const error = view.itemState.error ? /* @__PURE__ */ jsx16("p", { role: "alert", children: getErrorMessage3(view.itemState.error, view.labels.error) }) : null;
3858
4073
  const actions = view.statusLabel ? /* @__PURE__ */ jsx16(KeepStaleNotice, { item, onRetry, onRemoved }) : /* @__PURE__ */ jsxs14(Fragment8, { children: [
3859
- showSaveButton ? /* @__PURE__ */ jsx16(
3860
- KeepButton,
3861
- {
3862
- item: toKeepButtonItem(item),
3863
- labels: saveButtonLabels,
3864
- getAriaLabel: (buttonState) => `${buttonState.isSaved ? view.labels.remove : view.labels.save} ${String(view.resolvedTitle)}`
3865
- }
3866
- ) : null,
3867
- /* @__PURE__ */ jsx16(
3868
- "button",
3869
- {
3870
- type: "button",
3871
- "data-keep-action": "remove-item",
3872
- onClick: () => void view.remove(),
3873
- disabled: view.itemState.isMutating,
3874
- children: removeLabel ?? view.labels.remove
3875
- }
3876
- )
4074
+ showSaveButton ? /* @__PURE__ */ jsx16(KeepItemCardSave, {}) : null,
4075
+ /* @__PURE__ */ jsx16(KeepItemCardRemove, {})
3877
4076
  ] });
3878
4077
  const compoundValue = {
3879
4078
  item,
@@ -3889,6 +4088,11 @@ function KeepItemCardRoot({
3889
4088
  meta,
3890
4089
  error,
3891
4090
  actions,
4091
+ saveButtonLabels,
4092
+ removeLabel: removeLabel ?? view.labels.remove,
4093
+ remove: view.remove,
4094
+ isMutating: view.itemState.isMutating,
4095
+ saveAriaLabel: `${view.labels.remove} ${String(view.resolvedTitle)}`,
3892
4096
  renderText: (content) => /* @__PURE__ */ jsx16(KeepHighlight, { query: searchQuery, children: content })
3893
4097
  };
3894
4098
  const defaultBody = /* @__PURE__ */ jsxs14(Fragment8, { children: [
@@ -3962,6 +4166,36 @@ function KeepItemCardActions({ children, ...props }) {
3962
4166
  const context = useKeepItemCardCompound("Actions");
3963
4167
  return /* @__PURE__ */ jsx16("div", { ...props, "data-keep-card-part": "actions", children: children ?? context.actions });
3964
4168
  }
4169
+ function KeepItemCardSave({ labels, getAriaLabel, ...props }) {
4170
+ const context = useKeepItemCardCompound("Save");
4171
+ return /* @__PURE__ */ jsx16(
4172
+ KeepButton,
4173
+ {
4174
+ ...props,
4175
+ item: toKeepButtonItem(context.item),
4176
+ asChild: false,
4177
+ labels: labels ?? context.saveButtonLabels,
4178
+ getAriaLabel: getAriaLabel ?? (() => context.saveAriaLabel)
4179
+ }
4180
+ );
4181
+ }
4182
+ function KeepItemCardRemove({ children, disabled, onClick, ...props }) {
4183
+ const context = useKeepItemCardCompound("Remove");
4184
+ return /* @__PURE__ */ jsx16(
4185
+ "button",
4186
+ {
4187
+ ...props,
4188
+ type: props.type ?? "button",
4189
+ "data-keep-action": "remove-item",
4190
+ disabled: disabled ?? context.isMutating,
4191
+ onClick: (event) => {
4192
+ onClick?.(event);
4193
+ if (!event.defaultPrevented) void context.remove();
4194
+ },
4195
+ children: children ?? context.removeLabel
4196
+ }
4197
+ );
4198
+ }
3965
4199
  function KeepItemCardPin({ children, ...props }) {
3966
4200
  const context = useKeepItemCardCompound("Pin");
3967
4201
  return /* @__PURE__ */ jsx16(KeepPinButton, { ...props, item: context.item, children });
@@ -3984,6 +4218,8 @@ var KeepItemCard = Object.assign(KeepItemCardRoot, {
3984
4218
  Title: KeepItemCardTitle,
3985
4219
  Tags: KeepItemCardTags,
3986
4220
  Actions: KeepItemCardActions,
4221
+ Save: KeepItemCardSave,
4222
+ Remove: KeepItemCardRemove,
3987
4223
  Pin: KeepItemCardPin,
3988
4224
  Archive: KeepItemCardArchive,
3989
4225
  CollectionBadge: KeepItemCardCollectionBadge
@@ -4000,7 +4236,7 @@ function KeepItemCardSkeleton({ layout = "list", ...props }) {
4000
4236
  function formatSavedAt(timestamp) {
4001
4237
  return new Date(timestamp).toISOString().slice(0, 10);
4002
4238
  }
4003
- function getErrorMessage2(error, fallback) {
4239
+ function getErrorMessage3(error, fallback) {
4004
4240
  return error instanceof Error ? error.message : fallback;
4005
4241
  }
4006
4242
 
@@ -4588,7 +4824,7 @@ function KeepNoteEditor({
4588
4824
  "data-disabled": isSaving ? "true" : void 0,
4589
4825
  children: [
4590
4826
  body,
4591
- error ? /* @__PURE__ */ jsx23("p", { role: "alert", children: getErrorMessage3(error, view.labels.error) }) : null
4827
+ error ? /* @__PURE__ */ jsx23("p", { role: "alert", children: getErrorMessage4(error, view.labels.error) }) : null
4592
4828
  ]
4593
4829
  }
4594
4830
  );
@@ -4610,7 +4846,7 @@ function KeepNoteEditor({
4610
4846
  "KeepNoteEditor"
4611
4847
  );
4612
4848
  }
4613
- function getErrorMessage3(error, fallback) {
4849
+ function getErrorMessage4(error, fallback) {
4614
4850
  return error instanceof Error ? error.message : fallback;
4615
4851
  }
4616
4852
 
@@ -4712,12 +4948,12 @@ function KeepTagEditor({
4712
4948
  "data-disabled": isSaving ? "true" : void 0,
4713
4949
  children: [
4714
4950
  body,
4715
- view.error ? /* @__PURE__ */ jsx24("p", { role: "alert", children: getErrorMessage4(view.error, view.labels.error) }) : null
4951
+ view.error ? /* @__PURE__ */ jsx24("p", { role: "alert", children: getErrorMessage5(view.error, view.labels.error) }) : null
4716
4952
  ]
4717
4953
  }
4718
4954
  );
4719
4955
  }
4720
- function getErrorMessage4(error, fallback) {
4956
+ function getErrorMessage5(error, fallback) {
4721
4957
  return error instanceof Error ? error.message : fallback;
4722
4958
  }
4723
4959
 
@@ -4743,7 +4979,7 @@ function useKeepToastFeedback(showToast) {
4743
4979
 
4744
4980
  // src/features/navigation/KeepTourBar.tsx
4745
4981
  import { useKeepNavigator } from "@keepkit/core/react";
4746
- import { useId } from "react";
4982
+ import { useId as useId2 } from "react";
4747
4983
 
4748
4984
  // src/features/navigation/hooks/useKeepTourShortcuts.ts
4749
4985
  import { useEffect as useEffect11 } from "react";
@@ -4865,7 +5101,7 @@ function KeepTourBar({
4865
5101
  );
4866
5102
  }
4867
5103
  function TourAction({ href, disabled = false, onClick, children, preview, shortcut, ...props }) {
4868
- const previewId = useId();
5104
+ const previewId = useId2();
4869
5105
  const content = /* @__PURE__ */ jsxs21(Fragment13, { children: [
4870
5106
  /* @__PURE__ */ jsx25("span", { "data-tour-label": "true", children }),
4871
5107
  shortcut ? /* @__PURE__ */ jsx25(KeepShortcutHint, { shortcut }) : null,
@@ -5226,7 +5462,7 @@ function useKeepSyncStatusBanner({ onRetry, children }) {
5226
5462
  const syncedLabel = useUiLabel("syncSynced");
5227
5463
  const status = context.syncState.status;
5228
5464
  const hasConflicts = (context.syncState.conflicts?.length ?? 0) > 0 || context.syncState.conflictIds.length > 0;
5229
- const message = children ?? (status === "error" ? getErrorMessage5(context.syncState.error) : status === "conflict" || hasConflicts ? conflictLabel : status === "pending" || status === "syncing" ? pendingLabel : syncedLabel);
5465
+ const message = children ?? (status === "error" ? getErrorMessage6(context.syncState.error) : status === "conflict" || hasConflicts ? conflictLabel : status === "pending" || status === "syncing" ? pendingLabel : syncedLabel);
5230
5466
  return {
5231
5467
  status,
5232
5468
  hasConflicts,
@@ -5245,7 +5481,7 @@ function useKeepSyncStatusBanner({ onRetry, children }) {
5245
5481
  }
5246
5482
  };
5247
5483
  }
5248
- function getErrorMessage5(error) {
5484
+ function getErrorMessage6(error) {
5249
5485
  return error instanceof Error ? error.message : "Sync failed.";
5250
5486
  }
5251
5487