efficient-shell 4.127.0 → 4.131.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.
Files changed (128) hide show
  1. package/README.md +926 -5
  2. package/dist/{Browser-F5TKMP4K.js → Browser-PSEMJFZE.js} +13 -10
  3. package/dist/Browser-PSEMJFZE.js.map +1 -0
  4. package/dist/{Calculator-T3XG7S73.js → Calculator-ZXJPXG7F.js} +12 -9
  5. package/dist/Calculator-ZXJPXG7F.js.map +1 -0
  6. package/dist/ConfirmDialog-MTC7QMUJ.js +4 -0
  7. package/dist/{ConfirmDialog-LJXMB45A.js.map → ConfirmDialog-MTC7QMUJ.js.map} +1 -1
  8. package/dist/{CurrencyConverter-CASQMKLO.js → CurrencyConverter-WSJ2BBSX.js} +12 -9
  9. package/dist/CurrencyConverter-WSJ2BBSX.js.map +1 -0
  10. package/dist/{Documents-YX67YTZS.js → Documents-PGMZR2MA.js} +14 -11
  11. package/dist/Documents-PGMZR2MA.js.map +1 -0
  12. package/dist/{Files-QB63GO6J.js → Files-2LKCJGYW.js} +17 -14
  13. package/dist/Files-2LKCJGYW.js.map +1 -0
  14. package/dist/{Notepad-3SUD5XUE.js → Notepad-KW6WVABH.js} +15 -12
  15. package/dist/Notepad-KW6WVABH.js.map +1 -0
  16. package/dist/PdfViewer-M2YGZIYX.js +5 -0
  17. package/dist/{PdfViewer-LSP3YNFC.js.map → PdfViewer-M2YGZIYX.js.map} +1 -1
  18. package/dist/{PomodoroTimer-4EHD2QC4.js → PomodoroTimer-DG3LI5CX.js} +14 -11
  19. package/dist/PomodoroTimer-DG3LI5CX.js.map +1 -0
  20. package/dist/{Preview-4ZHR4SKP.js → Preview-E7EJJNXN.js} +16 -13
  21. package/dist/Preview-E7EJJNXN.js.map +1 -0
  22. package/dist/{Sidebar-J76PV4QF.js → Sidebar-BPU7RCZP.js} +5 -4
  23. package/dist/Sidebar-BPU7RCZP.js.map +1 -0
  24. package/dist/{Spreadsheet-G3SC7XJ5.js → Spreadsheet-IDUODRZU.js} +14 -11
  25. package/dist/Spreadsheet-IDUODRZU.js.map +1 -0
  26. package/dist/{Stock-TLT4VN2R.js → Stock-YMPOMUAW.js} +12 -9
  27. package/dist/Stock-YMPOMUAW.js.map +1 -0
  28. package/dist/{Weather-FRKX3DAK.js → Weather-NZO3CWLH.js} +12 -9
  29. package/dist/Weather-NZO3CWLH.js.map +1 -0
  30. package/dist/{WorldClock-GKCITO3M.js → WorldClock-HUC6D3AD.js} +12 -9
  31. package/dist/WorldClock-HUC6D3AD.js.map +1 -0
  32. package/dist/apps/index.js +26 -22
  33. package/dist/apps/index.js.map +1 -1
  34. package/dist/brand.css +1 -1
  35. package/dist/{chunk-XUCIYMN7.js → chunk-2GZQ352J.js} +4 -4
  36. package/dist/{chunk-XUCIYMN7.js.map → chunk-2GZQ352J.js.map} +1 -1
  37. package/dist/chunk-7IN7YZAL.js +547 -0
  38. package/dist/chunk-7IN7YZAL.js.map +1 -0
  39. package/dist/{chunk-YNGPTOL6.js → chunk-7MA5XBWD.js} +3 -3
  40. package/dist/{chunk-YNGPTOL6.js.map → chunk-7MA5XBWD.js.map} +1 -1
  41. package/dist/{chunk-WK7FV7VV.js → chunk-BJ34QOR3.js} +827 -625
  42. package/dist/chunk-BJ34QOR3.js.map +1 -0
  43. package/dist/{chunk-HNH7FOYB.js → chunk-CGXMWHQW.js} +7 -6
  44. package/dist/chunk-CGXMWHQW.js.map +1 -0
  45. package/dist/{chunk-N3CG5HG5.js → chunk-CLGNTGC5.js} +12 -11
  46. package/dist/chunk-CLGNTGC5.js.map +1 -0
  47. package/dist/chunk-D276EMXS.js +57 -0
  48. package/dist/chunk-D276EMXS.js.map +1 -0
  49. package/dist/{chunk-7KIOEKHS.js → chunk-E3TWIN7Z.js} +21 -135
  50. package/dist/chunk-E3TWIN7Z.js.map +1 -0
  51. package/dist/{chunk-2VM2FRZJ.js → chunk-EENVXIVU.js} +47 -67
  52. package/dist/chunk-EENVXIVU.js.map +1 -0
  53. package/dist/{chunk-YG45DAUT.js → chunk-FONEQRBH.js} +17 -11
  54. package/dist/chunk-FONEQRBH.js.map +1 -0
  55. package/dist/chunk-IPXN646P.js +14 -0
  56. package/dist/chunk-IPXN646P.js.map +1 -0
  57. package/dist/chunk-IRWSRJ4K.js +48 -0
  58. package/dist/chunk-IRWSRJ4K.js.map +1 -0
  59. package/dist/{chunk-JFQGPY7W.js → chunk-KWEX4ZAF.js} +20 -17
  60. package/dist/chunk-KWEX4ZAF.js.map +1 -0
  61. package/dist/{chunk-OKEFO6TI.js → chunk-LQTVTOSY.js} +10 -10
  62. package/dist/chunk-LQTVTOSY.js.map +1 -0
  63. package/dist/{chunk-G4KA3SAX.js → chunk-MQPX6ERJ.js} +4 -4
  64. package/dist/{chunk-G4KA3SAX.js.map → chunk-MQPX6ERJ.js.map} +1 -1
  65. package/dist/{chunk-FQB24B42.js → chunk-SEWF4O3H.js} +3 -3
  66. package/dist/{chunk-FQB24B42.js.map → chunk-SEWF4O3H.js.map} +1 -1
  67. package/dist/{chunk-AXWC3332.js → chunk-TFXJRHJV.js} +3 -3
  68. package/dist/{chunk-AXWC3332.js.map → chunk-TFXJRHJV.js.map} +1 -1
  69. package/dist/{chunk-G3HQLQYZ.js → chunk-TQURTLK2.js} +59 -34
  70. package/dist/chunk-TQURTLK2.js.map +1 -0
  71. package/dist/{chunk-6XQHH7D2.js → chunk-TRCWINHF.js} +5 -5
  72. package/dist/{chunk-6XQHH7D2.js.map → chunk-TRCWINHF.js.map} +1 -1
  73. package/dist/chunk-UHPWZ2F4.js +7 -0
  74. package/dist/{chunk-CJZXR7MG.js.map → chunk-UHPWZ2F4.js.map} +1 -1
  75. package/dist/{chunk-NPJCMM6V.js → chunk-VWZIGZFF.js} +4 -4
  76. package/dist/{chunk-NPJCMM6V.js.map → chunk-VWZIGZFF.js.map} +1 -1
  77. package/dist/{chunk-HYZ5QMVO.js → chunk-XH3ELPQZ.js} +22 -19
  78. package/dist/chunk-XH3ELPQZ.js.map +1 -0
  79. package/dist/{chunk-PPUZJFCI.js → chunk-ZKHD4J73.js} +8 -7
  80. package/dist/chunk-ZKHD4J73.js.map +1 -0
  81. package/dist/file-intake/index.js +3 -1
  82. package/dist/file-intake/index.js.map +1 -1
  83. package/dist/{index-dQia6NpB.d.ts → index-COwksV6G.d.ts} +699 -199
  84. package/dist/index.d.ts +2 -2
  85. package/dist/index.js +37 -32
  86. package/dist/index.js.map +1 -1
  87. package/dist/map/index.js +5 -3
  88. package/dist/map/index.js.map +1 -1
  89. package/dist/markdown/index.js +4 -3
  90. package/dist/markdown/index.js.map +1 -1
  91. package/dist/page-tabs/index.js +12 -8
  92. package/dist/page-tabs/index.js.map +1 -1
  93. package/dist/shell.css +17 -17
  94. package/dist/themes.css +15 -0
  95. package/dist/ui/index.d.ts +1 -1
  96. package/dist/ui/index.js +17 -13
  97. package/dist/ui/index.js.map +1 -1
  98. package/dist/ui.css +673 -283
  99. package/package.json +1 -1
  100. package/dist/Browser-F5TKMP4K.js.map +0 -1
  101. package/dist/Calculator-T3XG7S73.js.map +0 -1
  102. package/dist/ConfirmDialog-LJXMB45A.js +0 -3
  103. package/dist/CurrencyConverter-CASQMKLO.js.map +0 -1
  104. package/dist/Documents-YX67YTZS.js.map +0 -1
  105. package/dist/Files-QB63GO6J.js.map +0 -1
  106. package/dist/Notepad-3SUD5XUE.js.map +0 -1
  107. package/dist/PdfViewer-LSP3YNFC.js +0 -4
  108. package/dist/PomodoroTimer-4EHD2QC4.js.map +0 -1
  109. package/dist/Preview-4ZHR4SKP.js.map +0 -1
  110. package/dist/Sidebar-J76PV4QF.js.map +0 -1
  111. package/dist/Spreadsheet-G3SC7XJ5.js.map +0 -1
  112. package/dist/Stock-TLT4VN2R.js.map +0 -1
  113. package/dist/Weather-FRKX3DAK.js.map +0 -1
  114. package/dist/WorldClock-GKCITO3M.js.map +0 -1
  115. package/dist/chunk-2VM2FRZJ.js.map +0 -1
  116. package/dist/chunk-3CUSBZWG.js +0 -59
  117. package/dist/chunk-3CUSBZWG.js.map +0 -1
  118. package/dist/chunk-7KIOEKHS.js.map +0 -1
  119. package/dist/chunk-CJZXR7MG.js +0 -7
  120. package/dist/chunk-G3HQLQYZ.js.map +0 -1
  121. package/dist/chunk-HNH7FOYB.js.map +0 -1
  122. package/dist/chunk-HYZ5QMVO.js.map +0 -1
  123. package/dist/chunk-JFQGPY7W.js.map +0 -1
  124. package/dist/chunk-N3CG5HG5.js.map +0 -1
  125. package/dist/chunk-OKEFO6TI.js.map +0 -1
  126. package/dist/chunk-PPUZJFCI.js.map +0 -1
  127. package/dist/chunk-WK7FV7VV.js.map +0 -1
  128. package/dist/chunk-YG45DAUT.js.map +0 -1
@@ -1,8 +1,11 @@
1
+ import { currentShellStrings } from './chunk-7IN7YZAL.js';
2
+
1
3
  // src/shell/toast.ts
2
4
  var TOAST_CONTAINER_ID = "toast-container";
3
5
  var TOAST_BOTTOM_CONTAINER_ID = "toast-container-bottom";
4
6
  var NOTIF_CONTAINER_ID = "notif-container";
5
7
  var FADE_MS = 300;
8
+ var SVG_NS = "http://www.w3.org/2000/svg";
6
9
  var defaults = {};
7
10
  var live = /* @__PURE__ */ new Map();
8
11
  function getOrCreate(id, className) {
@@ -15,22 +18,34 @@ function getOrCreate(id, className) {
15
18
  }
16
19
  return el;
17
20
  }
18
- function getMenuOpacity() {
19
- try {
20
- const val = getComputedStyle(document.documentElement).getPropertyValue("--menu-opacity")?.trim();
21
- if (val) return parseFloat(val);
22
- } catch {
23
- }
24
- return 0.95;
21
+ var GLYPHS = {
22
+ success: "M5 13l4 4L19 7",
23
+ error: "M6 18L18 6M6 6l12 12",
24
+ warning: "M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z",
25
+ info: "M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
26
+ loading: "M12 3a9 9 0 019 9",
27
+ bell: "M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0"
28
+ };
29
+ function glyph(name, className) {
30
+ const svg = document.createElementNS(SVG_NS, "svg");
31
+ svg.setAttribute("class", className);
32
+ svg.setAttribute("fill", "none");
33
+ svg.setAttribute("viewBox", "0 0 24 24");
34
+ svg.setAttribute("stroke", "currentColor");
35
+ svg.setAttribute("stroke-width", "2");
36
+ const path = document.createElementNS(SVG_NS, "path");
37
+ path.setAttribute("stroke-linecap", "round");
38
+ if (name !== "loading") path.setAttribute("stroke-linejoin", "round");
39
+ path.setAttribute("d", GLYPHS[name]);
40
+ svg.appendChild(path);
41
+ return svg;
25
42
  }
26
- function glassBackground(o) {
27
- return `linear-gradient(135deg, rgba(255,255,255,${o * 0.85}) 0%, rgba(255,255,255,${o * 0.65}) 50%, rgba(255,255,255,${o * 0.75}) 100%)`;
43
+ function div(className, text) {
44
+ const el = document.createElement("div");
45
+ el.className = className;
46
+ if (text !== void 0) el.textContent = text;
47
+ return el;
28
48
  }
29
- var GLASS_COMMON = `
30
- backdrop-filter: blur(40px) saturate(1.8); -webkit-backdrop-filter: blur(40px) saturate(1.8);
31
- border: 1px solid rgba(255,255,255,0.35);
32
- box-shadow: inset 0 1px 0 rgba(255,255,255,0.4), 0 8px 32px rgba(0,0,0,0.15), 0 2px 8px rgba(0,0,0,0.08);
33
- `;
34
49
  function showToast(variant, message, opts) {
35
50
  const opts_ = { ...defaults, ...opts };
36
51
  const placement = opts_.placement ?? "top";
@@ -52,37 +67,13 @@ function showToast(variant, message, opts) {
52
67
  });
53
68
  }
54
69
  const container = placement === "bottom" ? getOrCreate(TOAST_BOTTOM_CONTAINER_ID, "fixed bottom-4 left-1/2 -translate-x-1/2 z-[9999] flex flex-col-reverse gap-2 items-center pointer-events-none") : getOrCreate(TOAST_CONTAINER_ID, "fixed top-4 left-1/2 -translate-x-1/2 z-[9999] flex flex-col gap-2 items-center pointer-events-none");
55
- const o = getMenuOpacity();
56
- const color = variant === "success" ? "#22c55e" : variant === "error" ? "#ef4444" : (
57
- // Amber, and deliberately not the error red: a warning is something the
58
- // user should read before continuing, not something that failed. Painting
59
- // both red teaches people to ignore the colour.
60
- variant === "warning" ? "#f59e0b" : "#3b82f6"
61
- );
62
- const icons = {
63
- success: '<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="' + color + '" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>',
64
- error: '<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="' + color + '" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>',
65
- warning: '<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="' + color + '" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/></svg>',
66
- info: '<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="' + color + '" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>',
67
- // An open arc that spins — same vocabulary as LoadingSpinner.
68
- loading: '<svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24" stroke="' + color + '" stroke-width="2"><path stroke-linecap="round" d="M12 3a9 9 0 019 9"/></svg>'
69
- };
70
- const icon = icons[variant];
71
70
  const offscreen = placement === "bottom" ? "translateY(10px)" : "translateY(-10px)";
72
- const el = document.createElement("div");
73
- el.className = "pointer-events-auto cursor-pointer";
74
- el.style.cssText = `
75
- padding: 8px 20px; border-radius: 12px;
76
- background: ${glassBackground(o)}; ${GLASS_COMMON}
77
- opacity: 0; transform: ${offscreen} scale(0.95);
78
- transition: opacity ${FADE_MS}ms ease, transform ${FADE_MS}ms ease;
79
- display: flex; align-items: center; gap: 8px;
80
- font-size: 13px; font-weight: 500; color: rgb(55,65,81);
81
- max-width: min(90vw, 460px);
82
- `;
71
+ const el = div("pointer-events-auto cursor-pointer rosh-toast");
72
+ el.style.opacity = "0";
73
+ el.style.transform = `${offscreen} scale(0.95)`;
83
74
  el.setAttribute("role", variant === "error" ? "alert" : "status");
84
75
  el.setAttribute("aria-atomic", "true");
85
- el.innerHTML = icon;
76
+ el.appendChild(glyph(variant, `h-4 w-4 rosh-tone-${variant}${variant === "loading" ? " animate-spin" : ""}`));
86
77
  const span = document.createElement("span");
87
78
  span.textContent = message;
88
79
  el.appendChild(span);
@@ -122,34 +113,23 @@ function showNotification(message, opts) {
122
113
  import('./sounds-NT4DEZGD.js').then((s) => s.playNotification()).catch(() => {
123
114
  });
124
115
  const container = getOrCreate(NOTIF_CONTAINER_ID, "fixed top-4 right-4 z-[9999] flex flex-col gap-3 items-end pointer-events-none");
125
- const o = getMenuOpacity();
126
- const el = document.createElement("div");
127
- el.className = "pointer-events-auto cursor-pointer";
128
- el.style.cssText = `
129
- min-width: 280px; max-width: 380px; padding: 12px 16px; border-radius: 16px;
130
- background: ${glassBackground(o)}; ${GLASS_COMMON}
131
- opacity: 0; transform: translateX(30px) scale(0.95);
132
- transition: opacity ${FADE_MS}ms cubic-bezier(0.4,0,0.2,1), transform ${FADE_MS}ms cubic-bezier(0.4,0,0.2,1);
133
- display: flex; align-items: flex-start; gap: 12px;
134
- `;
135
- el.innerHTML = `
136
- <div style="width: 36px; height: 36px; border-radius: 10px; background: rgba(59,130,246,0.15); display: flex; align-items: center; justify-content: center; flex-shrink: 0;">
137
- <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="#3b82f6" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0"/></svg>
138
- </div>
139
- <div style="flex: 1; min-width: 0;">
140
- <div style="font-size: 11px; font-weight: 600; color: #3b82f6; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 2px;">Notification</div>
141
- <div class="notif-msg" style="font-size: 13px; font-weight: 500; color: rgb(55,65,81); line-height: 1.4;"></div>
142
- </div>
143
- <button style="flex-shrink: 0; padding: 4px; color: rgb(156,163,175); font-size: 18px; line-height: 1; transition: color 0.2s;" onmouseenter="this.style.color='rgb(75,85,99)'" onmouseleave="this.style.color='rgb(156,163,175)'">&times;</button>
144
- `;
145
- const msgEl = el.querySelector(".notif-msg");
146
- if (msgEl) msgEl.textContent = message;
116
+ const el = div("pointer-events-auto cursor-pointer rosh-notif");
117
+ el.style.opacity = "0";
118
+ el.style.transform = "translateX(30px) scale(0.95)";
119
+ const badge = div("rosh-notif-badge");
120
+ badge.appendChild(glyph("bell", "h-5 w-5"));
121
+ const body = div("rosh-notif-body");
122
+ body.append(div("rosh-notif-title", currentShellStrings().toast.notification), div("notif-msg rosh-notif-msg", message));
123
+ const close = document.createElement("button");
124
+ close.className = "rosh-notif-close";
125
+ close.textContent = "\xD7";
126
+ el.append(badge, body, close);
147
127
  const dismiss = () => {
148
128
  el.style.opacity = "0";
149
129
  el.style.transform = "translateX(30px) scale(0.95)";
150
130
  setTimeout(() => el.remove(), FADE_MS);
151
131
  };
152
- el.querySelector("button")?.addEventListener("click", (e) => {
132
+ close.addEventListener("click", (e) => {
153
133
  e.stopPropagation();
154
134
  dismiss();
155
135
  });
@@ -211,5 +191,5 @@ var toast = {
211
191
  var toast_default = toast;
212
192
 
213
193
  export { toast_default };
214
- //# sourceMappingURL=chunk-2VM2FRZJ.js.map
215
- //# sourceMappingURL=chunk-2VM2FRZJ.js.map
194
+ //# sourceMappingURL=chunk-EENVXIVU.js.map
195
+ //# sourceMappingURL=chunk-EENVXIVU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shell/toast.ts"],"names":[],"mappings":"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","file":"chunk-EENVXIVU.js","sourcesContent":["/**\n * Two presentations:\n *\n * 1. toast.success / error / info — brief operation feedback, top-center,\n * auto-dismiss (success/error ~3s, info ~4.5s). The everyday \"what just\n * happened\" feedback — most messages want this.\n * 2. toast.notify — system notification, top-right card, stays 10s, dismissible.\n * For an alert worth lingering on; reach for it deliberately, not by default.\n *\n * (Historically `toast.info` rendered the top-right notification card; it now\n * renders a brief toast — that persistent card moved to `toast.notify`.)\n *\n * ── Built for a strict Content-Security-Policy ──\n *\n * Every node here is made with `createElement`/`createElementNS` and dressed\n * with CLASSES from ui.css (`.rosh-toast`, `.rosh-notif`…). There is no HTML\n * string, no `style=\"…\"` attribute and no `on…=` handler anywhere in the\n * markup, and that is load-bearing rather than tidy:\n *\n * - An inline handler attribute is inline SCRIPT. Under any `script-src`\n * without `'unsafe-inline'` (a nonce-based policy, which is what Next.js\n * apps run) the browser refuses it and files a violation report. The\n * notification card's close button used to carry one for its hover colour,\n * so every `toast.notify` on such a site was a CSP report.\n * - A `style` attribute that arrives through markup is governed by\n * `style-src`; a policy with a nonce or a hash there ignores\n * `'unsafe-inline'` and drops it. Styles set through the CSSOM\n * (`el.style.opacity = …`) are not, which is why the enter/leave motion\n * below still writes `el.style` directly — that is the one dynamic part.\n * - No `innerHTML` also means nothing here needs a Trusted Types policy.\n *\n * The cost is that the toast's look now lives in ui.css, which every consumer\n * already imports (alone, or inside styles.css). Its colours are custom\n * properties there (`--rosh-toast-*`) with the values it always had, so a host\n * can restyle a toast without forking it.\n * `tests/strictCsp.test.ts` holds the DOM to all of this.\n */\n\nimport { currentShellStrings } from './strings';\n\nconst TOAST_CONTAINER_ID = 'toast-container';\nconst TOAST_BOTTOM_CONTAINER_ID = 'toast-container-bottom';\nconst NOTIF_CONTAINER_ID = 'notif-container';\nconst FADE_MS = 300;\nconst SVG_NS = 'http://www.w3.org/2000/svg';\n\nexport type ToastPlacement = 'top' | 'bottom';\n\nexport interface ToastOptions {\n /** Milliseconds before auto-dismiss. Ignored when `sticky`. */\n duration?: number;\n /**\n * Stay until dismissed. For a message the user must actually act on — the\n * kind that must not scroll past while they are looking at a customer.\n * Implies tap-to-dismiss is the only way out, which it already is.\n */\n sticky?: boolean;\n /**\n * `bottom` centres toasts along the bottom edge. On a till the top of the\n * screen is the cart total and the bottom is nearest the hand.\n */\n placement?: ToastPlacement;\n /**\n * Drop a message identical to one already showing, and RESTART its timer\n * rather than stacking a second copy. A failing poll otherwise writes the\n * same sentence a dozen times. The restart matters: the user asked twice, so\n * the message should persist, not expire on the first one's schedule.\n */\n dedupe?: boolean;\n}\n\n/**\n * Defaults for every subsequent toast, set once at app startup — the same\n * \"consumer wires this once\" shape as `setShellApiClient`/`setShellNavIcons`.\n * A till calls `toast.configure({ placement: 'bottom', duration: 6000,\n * dedupe: true })` instead of repeating those options at 40 call sites.\n * Per-call options still win.\n */\nlet defaults: ToastOptions = {};\n\n/** Live toasts by dedupe key, so a repeat can find and refresh its own. */\nconst live = new Map<string, { el: HTMLElement; restart: () => void; dismiss: () => void }>();\n\nfunction getOrCreate(id: string, className: string): HTMLElement {\n let el = document.getElementById(id);\n if (!el) {\n el = document.createElement('div');\n el.id = id;\n el.className = className;\n document.body.appendChild(el);\n }\n return el;\n}\n\ntype Tone = 'success' | 'error' | 'warning' | 'info' | 'loading';\n\n/**\n * Outline glyphs (Heroicons' paths), stroked in `currentColor` so the colour\n * comes from the tone class in ui.css rather than from an attribute here.\n * `loading` is an open arc that spins — the same vocabulary as LoadingSpinner.\n */\nconst GLYPHS: Record<Tone | 'bell', string> = {\n success: 'M5 13l4 4L19 7',\n error: 'M6 18L18 6M6 6l12 12',\n warning: 'M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z',\n info: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',\n loading: 'M12 3a9 9 0 019 9',\n bell: 'M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0',\n};\n\nfunction glyph(name: Tone | 'bell', className: string): SVGSVGElement {\n const svg = document.createElementNS(SVG_NS, 'svg');\n svg.setAttribute('class', className);\n svg.setAttribute('fill', 'none');\n svg.setAttribute('viewBox', '0 0 24 24');\n svg.setAttribute('stroke', 'currentColor');\n svg.setAttribute('stroke-width', '2');\n const path = document.createElementNS(SVG_NS, 'path');\n path.setAttribute('stroke-linecap', 'round');\n if (name !== 'loading') path.setAttribute('stroke-linejoin', 'round');\n path.setAttribute('d', GLYPHS[name]);\n svg.appendChild(path);\n return svg;\n}\n\nfunction div(className: string, text?: string): HTMLDivElement {\n const el = document.createElement('div');\n el.className = className;\n if (text !== undefined) el.textContent = text;\n return el;\n}\n\n// ── Toast (operation feedback) — top-center, brief ──\n\nfunction showToast(variant: Tone, message: string,\n opts?: ToastOptions): { dismiss: () => void } {\n const opts_ = { ...defaults, ...opts };\n const placement: ToastPlacement = opts_.placement ?? 'top';\n const sticky = opts_.sticky ?? false;\n\n // A repeat of a message already on screen refreshes it instead of stacking.\n const key = `${placement}|${variant}|${message}`;\n if (opts_.dedupe) {\n const existing = live.get(key);\n if (existing) { existing.restart(); return { dismiss: existing.dismiss }; }\n }\n\n // A loading toast is the START of an operation — the sound belongs to the\n // outcome, which follows in a moment.\n if (variant !== 'loading') {\n import('../utils/sounds').then(s => {\n if (variant === 'success') s.playSuccess();\n else if (variant === 'error') s.playError();\n else s.playNotification();\n }).catch(() => {});\n }\n\n const container = placement === 'bottom'\n ? getOrCreate(TOAST_BOTTOM_CONTAINER_ID, 'fixed bottom-4 left-1/2 -translate-x-1/2 z-[9999] flex flex-col-reverse gap-2 items-center pointer-events-none')\n : getOrCreate(TOAST_CONTAINER_ID, 'fixed top-4 left-1/2 -translate-x-1/2 z-[9999] flex flex-col gap-2 items-center pointer-events-none');\n\n // Enter from the edge the toast is anchored to, so it reads as arriving from\n // off-screen rather than drifting in from the wrong direction.\n const offscreen = placement === 'bottom' ? 'translateY(10px)' : 'translateY(-10px)';\n\n // The tone is a CLASS: `.rosh-tone-warning` and friends in ui.css colour the\n // glyph through `--rosh-toast-*`. Warning is amber and deliberately not the\n // error red: a warning is something to read before continuing, not something\n // that failed, and painting both red teaches people to ignore the colour.\n const el = div('pointer-events-auto cursor-pointer rosh-toast');\n el.style.opacity = '0';\n el.style.transform = `${offscreen} scale(0.95)`;\n // A toast IS a status message, and it appears without the user moving focus\n // — so without a live region it is announced to nobody. `alert` is\n // assertive and interrupts whatever is being read, which is right for a\n // failure and wrong for a confirmation the user already expected.\n el.setAttribute('role', variant === 'error' ? 'alert' : 'status');\n // Read the whole toast, not the word that changed. Without it a repeat that\n // only alters a number announces the number alone.\n el.setAttribute('aria-atomic', 'true');\n\n el.appendChild(glyph(variant, `h-4 w-4 rosh-tone-${variant}${variant === 'loading' ? ' animate-spin' : ''}`));\n const span = document.createElement('span');\n span.textContent = message;\n el.appendChild(span);\n\n container.appendChild(el);\n requestAnimationFrame(() => { el.style.opacity = '1'; el.style.transform = 'translateY(0) scale(1)'; });\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n let gone = false;\n const dismiss = () => {\n if (gone) return;\n gone = true;\n if (timer) clearTimeout(timer);\n live.delete(key);\n el.style.opacity = '0';\n el.style.transform = `${offscreen} scale(0.95)`;\n setTimeout(() => el.remove(), FADE_MS);\n };\n\n // Info messages are usually a sentence (\"no matches — check X\"); give them a\n // beat longer to read than the terse success/error confirmations.\n const duration = opts_.duration ?? (variant === 'info' ? 4500 : 3000);\n const restart = () => {\n if (timer) clearTimeout(timer);\n if (!sticky) timer = setTimeout(dismiss, duration);\n };\n restart();\n\n // Hold the timer while the pointer rests on it. Three seconds is enough to\n // read a confirmation and not enough to read an address someone leaned in\n // for — and a toast that vanishes as you reach for it cannot be re-read at\n // all, since there is no history to open.\n el.addEventListener('mouseenter', () => { if (timer) clearTimeout(timer); });\n el.addEventListener('mouseleave', () => { if (!gone) restart(); });\n\n // Tap anywhere on the toast to dismiss it. A toast has no other click\n // affordance, and a sticky one has no other exit at all — `notify` has\n // behaved this way since it existed.\n el.addEventListener('click', dismiss);\n\n live.set(key, { el, restart, dismiss });\n // Internal handle — `toast.promise` swaps its loading toast for the outcome.\n return { dismiss };\n}\n\n// ── Notification (system alert) — top-right, stays longer ──\n\nfunction showNotification(message: string, opts?: { duration?: number }) {\n import('../utils/sounds').then(s => s.playNotification()).catch(() => {});\n\n const container = getOrCreate(NOTIF_CONTAINER_ID, 'fixed top-4 right-4 z-[9999] flex flex-col gap-3 items-end pointer-events-none');\n\n const el = div('pointer-events-auto cursor-pointer rosh-notif');\n el.style.opacity = '0';\n el.style.transform = 'translateX(30px) scale(0.95)';\n\n const badge = div('rosh-notif-badge');\n badge.appendChild(glyph('bell', 'h-5 w-5'));\n const body = div('rosh-notif-body');\n // The heading is the app's catalog word — read here, outside React.\n body.append(div('rosh-notif-title', currentShellStrings().toast.notification), div('notif-msg rosh-notif-msg', message));\n // The close button's hover colour is a `:hover` rule in ui.css. It was an\n // `onmouseenter` attribute — inline script, refused by a strict CSP.\n const close = document.createElement('button');\n close.className = 'rosh-notif-close';\n close.textContent = '×';\n el.append(badge, body, close);\n\n const dismiss = () => {\n el.style.opacity = '0';\n el.style.transform = 'translateX(30px) scale(0.95)';\n setTimeout(() => el.remove(), FADE_MS);\n };\n\n close.addEventListener('click', (e) => { e.stopPropagation(); dismiss(); });\n el.addEventListener('click', dismiss);\n\n container.appendChild(el);\n requestAnimationFrame(() => { el.style.opacity = '1'; el.style.transform = 'translateX(0) scale(1)'; });\n\n setTimeout(dismiss, opts?.duration ?? 10000);\n}\n\nconst toast = {\n success: (message: string, opts?: ToastOptions) => showToast('success', message, opts),\n error: (message: string, opts?: ToastOptions) => showToast('error', message, opts),\n /**\n * Something the user should read before continuing — a partial success, a\n * setting that will not apply until they do something else. Distinct from\n * `error`, which is \"this did not happen\".\n */\n warning: (message: string, opts?: ToastOptions) => showToast('warning', message, opts),\n info: (message: string, opts?: ToastOptions) => showToast('info', message, opts),\n // Persistent top-right notification card (the old toast.info presentation).\n notify: (message: string, opts?: { duration?: number }) => showNotification(message, opts),\n /**\n * One toast for one async operation: a spinning \"loading\" toast while `p`\n * is pending, swapped for a success or error toast when it settles. The\n * promise is returned untouched, so `await toast.promise(save(), …)` still\n * throws to the caller — this narrates the operation, it does not handle it.\n *\n * `error` is REQUIRED, and there is deliberately no fallback that prints\n * the exception: a raw `e.message` in a toast is how internals leak to the\n * screen (the same reasoning that keeps ErrorBoundary's stack behind\n * `showDetails`). The success/error messages may be functions of the\n * resolved value / the rejection, for the \"Saved 3 rows\" case.\n */\n promise: <T,>(\n p: Promise<T>,\n msgs: {\n loading: string;\n success: string | ((value: T) => string);\n error: string | ((error: unknown) => string);\n },\n opts?: ToastOptions,\n ): Promise<T> => {\n const pending = showToast('loading', msgs.loading, { ...opts, sticky: true });\n p.then(\n value => {\n pending.dismiss();\n showToast('success', typeof msgs.success === 'function' ? msgs.success(value) : msgs.success, opts);\n },\n error => {\n pending.dismiss();\n showToast('error', typeof msgs.error === 'function' ? msgs.error(error) : msgs.error, opts);\n },\n );\n return p;\n },\n /**\n * Set defaults for every subsequent toast. Call once at app startup; later\n * calls replace the whole object rather than merging, so a consumer sets its\n * policy in one place. Per-call options still take precedence.\n */\n configure: (next: ToastOptions) => { defaults = { ...next }; },\n};\n\nexport default toast;\n"]}
@@ -1,7 +1,8 @@
1
- import { useState, useRef, useCallback, useEffect } from 'react';
2
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
1
+ import { useShellStrings } from './chunk-7IN7YZAL.js';
2
+ import { useBrowserState } from './chunk-IRWSRJ4K.js';
3
+ import { useRef, useCallback, useEffect } from 'react';
4
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
5
 
4
- // src/shell/SidebarLayout.tsx
5
6
  var clamp = (n, lo, hi) => Math.min(Math.max(n, lo), hi);
6
7
  function SidebarLayout({
7
8
  sidebar,
@@ -18,14 +19,19 @@ function SidebarLayout({
18
19
  contentClassName = "bg-white"
19
20
  }) {
20
21
  const sideRight = side === "right";
21
- const [width, setWidth] = useState(() => {
22
- if (storageKey && typeof window !== "undefined") {
23
- const saved = window.localStorage.getItem(storageKey);
22
+ const strings = useShellStrings().sidebar;
23
+ const [width, setWidth] = useBrowserState(() => {
24
+ if (storageKey) {
25
+ let saved = null;
26
+ try {
27
+ saved = window.localStorage.getItem(storageKey);
28
+ } catch {
29
+ }
24
30
  const n = saved ? parseInt(saved, 10) : NaN;
25
31
  if (!Number.isNaN(n)) return clamp(n, minWidth, maxWidth);
26
32
  }
27
33
  return clamp(defaultWidth, minWidth, maxWidth);
28
- });
34
+ }, clamp(defaultWidth, minWidth, maxWidth));
29
35
  const dragRef = useRef(null);
30
36
  const onMove = useCallback((e) => {
31
37
  const d = dragRef.current;
@@ -75,9 +81,9 @@ function SidebarLayout({
75
81
  {
76
82
  onPointerDown: startDrag,
77
83
  onDoubleClick: () => setWidth(clamp(defaultWidth, minWidth, maxWidth)),
78
- title: "Drag to resize \xB7 double-click to reset",
84
+ title: strings.resize,
79
85
  className: `group/resize absolute inset-y-0 ${edge} z-10 w-2 cursor-col-resize`,
80
- children: /* @__PURE__ */ jsx("div", { className: `absolute inset-y-0 ${edge} w-px bg-transparent transition-colors group-hover/resize:bg-[var(--accent-500,#3b82f6)]` })
86
+ children: /* @__PURE__ */ jsx("div", { className: `absolute inset-y-0 ${edge} w-px bg-transparent transition-colors group-hover/resize:bg-[var(--accent-500,var(--rosh-resize-handle))]` })
81
87
  }
82
88
  )
83
89
  ] });
@@ -92,5 +98,5 @@ function SidebarLayout({
92
98
  }
93
99
 
94
100
  export { SidebarLayout };
95
- //# sourceMappingURL=chunk-YG45DAUT.js.map
96
- //# sourceMappingURL=chunk-YG45DAUT.js.map
101
+ //# sourceMappingURL=chunk-FONEQRBH.js.map
102
+ //# sourceMappingURL=chunk-FONEQRBH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shell/SidebarLayout.tsx"],"names":[],"mappings":"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","file":"chunk-FONEQRBH.js","sourcesContent":["import { useCallback, useEffect, useRef } from 'react';\nimport type { ReactNode, PointerEvent as ReactPointerEvent } from 'react';\nimport { useBrowserState } from './hydration';\nimport { useShellStrings } from './strings';\n\n/**\n * Two-pane layout with a drag-to-resize sidebar (left or right).\n *\n * Renders flush (`h-full w-full`) — pair it with a `flushBody` window so the\n * sidebar runs from just under the title bar to the very bottom with no\n * surrounding padding. The user can drag the sidebar's inner edge to resize\n * it; pass `storageKey` to persist that width across reopens, or double-click\n * the handle to reset to `defaultWidth`. Set `side=\"right\"` to put the sidebar\n * on the right (e.g. a detail-panel layout).\n *\n * @example\n * <SidebarLayout sidebar={<MyNav />} storageKey=\"todo.sidebarWidth\">\n * <MyContent />\n * </SidebarLayout>\n */\nexport interface SidebarLayoutProps {\n /** Content of the left sidebar pane. When `sidebarTop`/`sidebarBottom` are\n * set this is the scrolling middle region (the filter nav); otherwise it is\n * the whole pane. */\n sidebar: ReactNode;\n /** Optional node pinned at the TOP of the sidebar, above `sidebar`, in a\n * padded non-scrolling region (e.g. a primary \"New X\" `SidebarActionButton`).\n * The list-window convention: primary create action lives here. */\n sidebarTop?: ReactNode;\n /** Optional node pinned at the BOTTOM of the sidebar, below `sidebar`, in a\n * padded, top-bordered non-scrolling region (e.g. an \"Export CSV\" button).\n * The middle `sidebar` grows to fill, so this stays flush to the bottom. */\n sidebarBottom?: ReactNode;\n /** Content of the main pane (opposite the sidebar). */\n children: ReactNode;\n /** Which side the sidebar sits on. Default `'left'`. When `'right'`, the\n * sidebar renders on the right with the resize handle on its left edge. */\n side?: 'left' | 'right';\n /** localStorage key to persist the sidebar width across reopens. When set,\n * the last dragged width is restored on mount. Omit for session-only width. */\n storageKey?: string;\n /** Initial sidebar width in px (used when nothing is persisted). Default 256. */\n defaultWidth?: number;\n /** Minimum sidebar width in px. Default 180. */\n minWidth?: number;\n /** Maximum sidebar width in px. Default 480. */\n maxWidth?: number;\n /** Extra classes for the outer flex row. */\n className?: string;\n /** Classes for the sidebar pane (background, border, …). The pane is a flex\n * column that scrolls its own overflow. Defaults to a light-grey,\n * right-bordered sidebar. */\n sidebarClassName?: string;\n /** Classes for the main content pane. Defaults to a white background. */\n contentClassName?: string;\n}\n\nconst clamp = (n: number, lo: number, hi: number) => Math.min(Math.max(n, lo), hi);\n\nexport default function SidebarLayout({\n sidebar,\n sidebarTop,\n sidebarBottom,\n children,\n side = 'left',\n storageKey,\n defaultWidth = 256,\n minWidth = 180,\n maxWidth = 480,\n className = '',\n sidebarClassName = side === 'right'\n ? 'border-l border-gray-200 bg-gray-50'\n : 'border-r border-gray-200 bg-gray-50',\n contentClassName = 'bg-white',\n}: SidebarLayoutProps) {\n const sideRight = side === 'right';\n const strings = useShellStrings().sidebar;\n // The saved width is the READER's, and a server has no reader: it draws\n // `defaultWidth`, hydration agrees with it, and the saved width replaces it\n // in a layout effect the moment hydration ends. A page that was never\n // server-rendered reads storage on its first render, as this always did.\n const [width, setWidth] = useBrowserState<number>(() => {\n if (storageKey) {\n let saved: string | null = null;\n // Guarded: a sandboxed frame or a locked-down profile throws on access.\n try { saved = window.localStorage.getItem(storageKey); } catch { /* treat as unsaved */ }\n const n = saved ? parseInt(saved, 10) : NaN;\n if (!Number.isNaN(n)) return clamp(n, minWidth, maxWidth);\n }\n return clamp(defaultWidth, minWidth, maxWidth);\n }, clamp(defaultWidth, minWidth, maxWidth));\n\n const dragRef = useRef<{ startX: number; startWidth: number } | null>(null);\n\n const onMove = useCallback((e: PointerEvent) => {\n const d = dragRef.current;\n if (!d) return;\n // Right-side sidebar grows as the handle is dragged left → invert the delta.\n const delta = (e.clientX - d.startX) * (sideRight ? -1 : 1);\n setWidth(clamp(d.startWidth + delta, minWidth, maxWidth));\n }, [minWidth, maxWidth, sideRight]);\n\n const onUp = useCallback(() => {\n dragRef.current = null;\n window.removeEventListener('pointermove', onMove);\n window.removeEventListener('pointerup', onUp);\n document.body.style.removeProperty('cursor');\n document.body.style.removeProperty('user-select');\n }, [onMove]);\n\n const startDrag = useCallback((e: ReactPointerEvent<HTMLDivElement>) => {\n e.preventDefault();\n dragRef.current = { startX: e.clientX, startWidth: width };\n window.addEventListener('pointermove', onMove);\n window.addEventListener('pointerup', onUp);\n document.body.style.cursor = 'col-resize';\n document.body.style.userSelect = 'none';\n }, [width, onMove, onUp]);\n\n // Persist (debounced) once the width settles.\n useEffect(() => {\n if (!storageKey) return;\n const id = window.setTimeout(() => {\n try { window.localStorage.setItem(storageKey, String(Math.round(width))); } catch { /* ignore quota / privacy mode */ }\n }, 200);\n return () => window.clearTimeout(id);\n }, [width, storageKey]);\n\n // Drop listeners if we unmount mid-drag.\n useEffect(() => () => {\n window.removeEventListener('pointermove', onMove);\n window.removeEventListener('pointerup', onUp);\n }, [onMove, onUp]);\n\n const edge = sideRight ? 'left-0' : 'right-0';\n // With a top/bottom slot the pane becomes a 3-region flex column: a padded\n // pinned header, the scrolling middle (`sidebar`, grows to fill), and a\n // padded, top-bordered pinned footer. Without slots it renders exactly as\n // before (the whole pane scrolls) so existing consumers are unaffected.\n const hasSlots = sidebarTop != null || sidebarBottom != null;\n const sidebarPane = (\n <div className=\"relative flex h-full shrink-0 flex-col\" style={{ width }}>\n <div className={`flex h-full flex-col ${hasSlots ? 'overflow-hidden' : 'overflow-y-auto'} ${sidebarClassName}`}>\n {sidebarTop != null && (\n <div className=\"shrink-0 px-2 pt-3 pb-2\">{sidebarTop}</div>\n )}\n {hasSlots ? (\n <div className=\"min-h-0 flex-1 overflow-y-auto\">{sidebar}</div>\n ) : (\n sidebar\n )}\n {sidebarBottom != null && (\n <div className=\"mt-auto shrink-0 border-t border-gray-200 px-2 py-3\">{sidebarBottom}</div>\n )}\n </div>\n {/* Resize handle — pinned to the sidebar's inner edge, fixed while it scrolls. */}\n <div\n onPointerDown={startDrag}\n onDoubleClick={() => setWidth(clamp(defaultWidth, minWidth, maxWidth))}\n title={strings.resize}\n className={`group/resize absolute inset-y-0 ${edge} z-10 w-2 cursor-col-resize`}\n >\n <div className={`absolute inset-y-0 ${edge} w-px bg-transparent transition-colors group-hover/resize:bg-[var(--accent-500,var(--rosh-resize-handle))]`} />\n </div>\n </div>\n );\n const contentPane = (\n <div className={`flex min-w-0 flex-1 flex-col ${contentClassName}`}>\n {children}\n </div>\n );\n\n return (\n <div className={`flex h-full w-full overflow-hidden ${className}`}>\n {sideRight ? (\n <>{contentPane}{sidebarPane}</>\n ) : (\n <>{sidebarPane}{contentPane}</>\n )}\n </div>\n );\n}\n"]}
@@ -0,0 +1,14 @@
1
+ // src/utils/rootTheme.ts
2
+ function rootIsDark() {
3
+ try {
4
+ const root = document.documentElement;
5
+ return root.getAttribute("data-theme") === "dark" || root.classList.contains("dark");
6
+ } catch {
7
+ return false;
8
+ }
9
+ }
10
+ var ROOT_THEME_ATTRIBUTES = ["data-theme", "class"];
11
+
12
+ export { ROOT_THEME_ATTRIBUTES, rootIsDark };
13
+ //# sourceMappingURL=chunk-IPXN646P.js.map
14
+ //# sourceMappingURL=chunk-IPXN646P.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/rootTheme.ts"],"names":[],"mappings":";AAYO,SAAS,UAAA,GAAsB;AACpC,EAAA,IAAI;AACF,IAAA,MAAM,OAAO,QAAA,CAAS,eAAA;AACtB,IAAA,OAAO,IAAA,CAAK,aAAa,YAAY,CAAA,KAAM,UAAU,IAAA,CAAK,SAAA,CAAU,SAAS,MAAM,CAAA;AAAA,EACrF,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAGO,IAAM,qBAAA,GAAwB,CAAC,YAAA,EAAc,OAAO","file":"chunk-IPXN646P.js","sourcesContent":["/**\n * Whether the document is in its dark theme, read off `<html>` the same way\n * ui.css reads it: `data-theme=\"dark\"` (what `useTheme()` stamps, and what the\n * EFFICIENT portals' own toggles stamp) OR a `dark` class (next-themes,\n * shadcn/ui, Tailwind's class strategy). Every dark rule in the stylesheet is\n * scoped by `:is([data-theme=\"dark\"], .dark)`; a component that decides in\n * script — a canvas, an inline glass tint — must agree with it, or it paints\n * light inside a page the stylesheet has already turned dark.\n *\n * Never throws: a document without a root (a server render, a detached test)\n * is light, which is also what the stylesheet would say.\n */\nexport function rootIsDark(): boolean {\n try {\n const root = document.documentElement;\n return root.getAttribute('data-theme') === 'dark' || root.classList.contains('dark');\n } catch {\n return false;\n }\n}\n\n/** The `<html>` attributes a theme change can arrive on — for a MutationObserver's `attributeFilter`. */\nexport const ROOT_THEME_ATTRIBUTES = ['data-theme', 'class'];\n"]}
@@ -0,0 +1,48 @@
1
+ import { useLayoutEffect, useEffect, useSyncExternalStore, useRef, useState, useCallback } from 'react';
2
+
3
+ // src/shell/hydration.ts
4
+ var IN_BROWSER = typeof window !== "undefined" && typeof document !== "undefined";
5
+ var useBrowserLayoutEffect = IN_BROWSER ? useLayoutEffect : useEffect;
6
+ var NO_SUBSCRIPTION = () => () => {
7
+ };
8
+ var ALWAYS = () => true;
9
+ var NEVER = () => false;
10
+ function useHydrated() {
11
+ return useSyncExternalStore(NO_SUBSCRIPTION, ALWAYS, NEVER);
12
+ }
13
+ function useBrowserValue(read, serverValue, subscribe = NO_SUBSCRIPTION) {
14
+ return useSyncExternalStore(subscribe, read, () => serverValue);
15
+ }
16
+ function useBrowserValueOnce(read, serverValue) {
17
+ const held = useRef(null);
18
+ return useSyncExternalStore(
19
+ NO_SUBSCRIPTION,
20
+ () => (held.current ??= { value: read() }).value,
21
+ () => serverValue
22
+ );
23
+ }
24
+ function useBrowserState(read, serverValue) {
25
+ const hydrated = useHydrated();
26
+ const [value, setValue] = useState(() => hydrated ? read() : serverValue);
27
+ const restored = useRef(hydrated);
28
+ const latestRead = useRef(read);
29
+ latestRead.current = read;
30
+ useBrowserLayoutEffect(() => {
31
+ if (!hydrated || restored.current) return;
32
+ restored.current = true;
33
+ setValue(latestRead.current());
34
+ }, [hydrated]);
35
+ const set = useCallback((next) => {
36
+ if (restored.current) {
37
+ setValue(next);
38
+ return;
39
+ }
40
+ restored.current = true;
41
+ setValue(typeof next === "function" ? next(latestRead.current()) : next);
42
+ }, []);
43
+ return [value, set];
44
+ }
45
+
46
+ export { useBrowserLayoutEffect, useBrowserState, useBrowserValue, useBrowserValueOnce, useHydrated };
47
+ //# sourceMappingURL=chunk-IRWSRJ4K.js.map
48
+ //# sourceMappingURL=chunk-IRWSRJ4K.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/shell/hydration.ts"],"names":[],"mappings":";;;AAyCO,IAAM,UAAA,GAAa,OAAO,MAAA,KAAW,WAAA,IAAe,OAAO,QAAA,KAAa,WAAA;AASxE,IAAM,sBAAA,GAAyB,aAAa,eAAA,GAAkB;AAGrE,IAAM,eAAA,GAAkB,MAAM,MAAM;AAAC,CAAA;AACrC,IAAM,SAAS,MAAM,IAAA;AACrB,IAAM,QAAQ,MAAM,KAAA;AAOb,SAAS,WAAA,GAAuB;AACrC,EAAA,OAAO,oBAAA,CAAqB,eAAA,EAAiB,MAAA,EAAQ,KAAK,CAAA;AAC5D;AAYO,SAAS,eAAA,CACd,IAAA,EACA,WAAA,EACA,SAAA,GAAkD,eAAA,EAC/C;AACH,EAAA,OAAO,oBAAA,CAAqB,SAAA,EAAW,IAAA,EAAM,MAAM,WAAW,CAAA;AAChE;AAYO,SAAS,mBAAA,CAAuB,MAAe,WAAA,EAAmB;AACvE,EAAA,MAAM,IAAA,GAAO,OAA4B,IAAI,CAAA;AAC7C,EAAA,OAAO,oBAAA;AAAA,IACL,eAAA;AAAA,IACA,OAAO,IAAA,CAAK,OAAA,KAAY,EAAE,KAAA,EAAO,IAAA,IAAO,EAAG,KAAA;AAAA,IAC3C,MAAM;AAAA,GACR;AACF;AAiBO,SAAS,eAAA,CAAmB,MAAe,WAAA,EAAkD;AAClG,EAAA,MAAM,WAAW,WAAA,EAAY;AAC7B,EAAA,MAAM,CAAC,OAAO,QAAQ,CAAA,GAAI,SAAY,MAAO,QAAA,GAAW,IAAA,EAAK,GAAI,WAAY,CAAA;AAG7E,EAAA,MAAM,QAAA,GAAW,OAAO,QAAQ,CAAA;AAChC,EAAA,MAAM,UAAA,GAAa,OAAO,IAAI,CAAA;AAC9B,EAAA,UAAA,CAAW,OAAA,GAAU,IAAA;AAErB,EAAA,sBAAA,CAAuB,MAAM;AAC3B,IAAA,IAAI,CAAC,QAAA,IAAY,QAAA,CAAS,OAAA,EAAS;AACnC,IAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AACnB,IAAA,QAAA,CAAS,UAAA,CAAW,SAAS,CAAA;AAAA,EAC/B,CAAA,EAAG,CAAC,QAAQ,CAAC,CAAA;AAEb,EAAA,MAAM,GAAA,GAAM,WAAA,CAAyC,CAAC,IAAA,KAAS;AAC7D,IAAA,IAAI,SAAS,OAAA,EAAS;AACpB,MAAA,QAAA,CAAS,IAAI,CAAA;AACb,MAAA;AAAA,IACF;AACA,IAAA,QAAA,CAAS,OAAA,GAAU,IAAA;AACnB,IAAA,QAAA,CAAS,OAAO,SAAS,UAAA,GAAc,IAAA,CAAwB,WAAW,OAAA,EAAS,IAAI,IAAI,CAAA;AAAA,EAC7F,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,OAAO,CAAC,OAAO,GAAG,CAAA;AACpB","file":"chunk-IRWSRJ4K.js","sourcesContent":["/**\n * Rendering on a server, and hydrating what it sent.\n *\n * Every consumer this package had until now mounted it in the browser and\n * nowhere else — the portals are static exports that load the app behind\n * `dynamic(…, { ssr: false })`, so the first render always ran with a window, a\n * clock and the reader's storage in reach. A Next.js App Router consumer renders\n * on a SERVER first and then hydrates that HTML in the browser, and hydration\n * has one rule nothing here ever had to keep: the browser's first render must\n * produce exactly the HTML the server did. A component that reads the platform,\n * a stored width, today's date or a media query while it renders draws one thing\n * on the server and another in the browser, and React either logs a mismatch\n * and leaves the wrong attribute in place, or throws the server HTML away.\n *\n * So the rule for anything only a browser knows: the server — and the hydration\n * pass that has to agree with it — renders a fixed SERVER value, and the\n * browser's value arrives in the render after. The other half matters as much,\n * because the browser-only consumers are the ones this package has: a root that\n * was NOT server-rendered (`createRoot`, a dialog mounted later, a client-only\n * page) reads the browser's value on its very first render, exactly as before,\n * with no frame of the server value and no extra render.\n *\n * `useSyncExternalStore` is what tells the two apart. React calls its\n * `getServerSnapshot` on the server AND while hydrating, and `getSnapshot`\n * everywhere else, then re-renders with `getSnapshot` once hydration commits if\n * the two disagree. That is the whole mechanism; the hooks below are that one\n * call shaped for the three ways the kit needs it.\n */\nimport {\n useCallback, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore,\n type Dispatch, type SetStateAction,\n} from 'react';\n\n/**\n * A browser, as opposed to a server. Read once, at module scope, because it\n * cannot change for the life of the module: a server never turns into a browser.\n *\n * `window` rather than `navigator`: Node has had a global `navigator` since 21,\n * and its `platform` is the SERVER's — a guard on it reads a Mac build machine\n * as a Mac reader.\n */\nexport const IN_BROWSER = typeof window !== 'undefined' && typeof document !== 'undefined';\n\n/**\n * `useLayoutEffect` in a browser, `useEffect` where there is no DOM.\n *\n * Neither runs on a server, but React 18's server renderer warns about every\n * `useLayoutEffect` it meets, once per component per render. Chosen at module\n * scope, so the hook order is the same on every render either way.\n */\nexport const useBrowserLayoutEffect = IN_BROWSER ? useLayoutEffect : useEffect;\n\n/** Nothing to subscribe to: the value is read again on the next render. */\nconst NO_SUBSCRIPTION = () => () => {};\nconst ALWAYS = () => true;\nconst NEVER = () => false;\n\n/**\n * False while React renders on a server or hydrates the HTML it sent; true\n * otherwise — including the very FIRST render of a root that was never\n * server-rendered, which is what keeps a client-only consumer exactly as it was.\n */\nexport function useHydrated(): boolean {\n return useSyncExternalStore(NO_SUBSCRIPTION, ALWAYS, NEVER);\n}\n\n/**\n * A value only the browser knows, read on every render, with `serverValue` in\n * its place on a server and while hydrating.\n *\n * `read` must return the same value for the same state of the world — a\n * primitive, or a cached object — because React compares consecutive reads to\n * decide whether anything changed. Pass `subscribe` to re-render when it does\n * (a media query); without one the value is simply read again on the next\n * render, which is what the code this replaced did by calling it inline.\n */\nexport function useBrowserValue<T>(\n read: () => T,\n serverValue: T,\n subscribe: (onChange: () => void) => () => void = NO_SUBSCRIPTION,\n): T {\n return useSyncExternalStore(subscribe, read, () => serverValue);\n}\n\n/**\n * A browser value read ONCE per mounted component — the moment it mounted, or\n * the moment hydration ended — and held for its lifetime.\n *\n * For what the kit used to capture with `useState(() => Date.now())`: \"now\" as\n * a timeline's coordinate, which must not drift while the reader looks at it.\n * The state initialiser runs during hydration as well, so it put the browser's\n * clock into a render that had to match the server's; this holds the server\n * value until hydration ends and reads the clock then.\n */\nexport function useBrowserValueOnce<T>(read: () => T, serverValue: T): T {\n const held = useRef<{ value: T } | null>(null);\n return useSyncExternalStore(\n NO_SUBSCRIPTION,\n () => (held.current ??= { value: read() }).value,\n () => serverValue,\n );\n}\n\n/**\n * `useState` whose initial value comes from the browser — a stored width, the\n * saved prefs — and starts as `serverValue` when the HTML came from a server.\n *\n * A client-only root reads the browser on its first render, as the\n * `useState(() => localStorage…)` this replaces did, so nothing about it\n * changes. A hydrating root renders `serverValue`, then reads the browser in a\n * layout effect the moment hydration ends — before that render is painted.\n *\n * An update made in between is applied to the BROWSER's value, not to the\n * placeholder, and counts as the restore. The window is real: the children's\n * effects run when hydration commits, before the restore, and a `save(patch)`\n * from one of them would otherwise merge the patch into the defaults and write\n * that over everything the reader had stored.\n */\nexport function useBrowserState<T>(read: () => T, serverValue: T): [T, Dispatch<SetStateAction<T>>] {\n const hydrated = useHydrated();\n const [value, setValue] = useState<T>(() => (hydrated ? read() : serverValue));\n // True once the state holds the browser's value: from the start on a\n // client-only root, after the restore below on a hydrated one.\n const restored = useRef(hydrated);\n const latestRead = useRef(read);\n latestRead.current = read;\n\n useBrowserLayoutEffect(() => {\n if (!hydrated || restored.current) return;\n restored.current = true;\n setValue(latestRead.current());\n }, [hydrated]);\n\n const set = useCallback<Dispatch<SetStateAction<T>>>((next) => {\n if (restored.current) {\n setValue(next);\n return;\n }\n restored.current = true;\n setValue(typeof next === 'function' ? (next as (prev: T) => T)(latestRead.current()) : next);\n }, []);\n\n return [value, set];\n}\n"]}
@@ -1,5 +1,7 @@
1
1
  import { escapeHtml } from './chunk-MGGY74RY.js';
2
- import { useRef, useState, useEffect, useLayoutEffect, useCallback, useMemo } from 'react';
2
+ import { useShellStrings } from './chunk-7IN7YZAL.js';
3
+ import { useBrowserLayoutEffect } from './chunk-IRWSRJ4K.js';
4
+ import { useRef, useState, useEffect, useCallback, useMemo } from 'react';
3
5
  import { createPortal } from 'react-dom';
4
6
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
7
 
@@ -19,6 +21,7 @@ function CellText({ text, ...props }) {
19
21
  }
20
22
  function EditableGrid({ columns, data, onChange, onColumnsChange, fixedRows = false, minRows = 15, maxHeight = "260px", cellStyles, onFocusChange, onSelectionChange }) {
21
23
  const tableRef = useRef(null);
24
+ const strings = useShellStrings().grid;
22
25
  const [focus, setFocus] = useState(null);
23
26
  useEffect(() => {
24
27
  onFocusChange?.(focus);
@@ -33,7 +36,7 @@ function EditableGrid({ columns, data, onChange, onColumnsChange, fixedRows = fa
33
36
  const [dragOverCol, setDragOverCol] = useState(null);
34
37
  const [editingCell, setEditingCell] = useState(null);
35
38
  const caretToEnd = useRef(null);
36
- useLayoutEffect(() => {
39
+ useBrowserLayoutEffect(() => {
37
40
  const pos = caretToEnd.current;
38
41
  if (!pos || pos.row !== editingCell?.row || pos.col !== editingCell?.col) return;
39
42
  caretToEnd.current = null;
@@ -293,38 +296,38 @@ function EditableGrid({ columns, data, onChange, onColumnsChange, fixedRows = fa
293
296
  e.preventDefault();
294
297
  if (fixedRows) return;
295
298
  setCtxMenu({ x: e.clientX, y: e.clientY, items: [
296
- { label: `Insert row above`, onClick: () => {
299
+ { label: strings.insertRowAbove, onClick: () => {
297
300
  insertRow(ri);
298
301
  setCtxMenu(null);
299
302
  } },
300
- { label: `Insert row below`, onClick: () => {
303
+ { label: strings.insertRowBelow, onClick: () => {
301
304
  insertRow(ri + 1);
302
305
  setCtxMenu(null);
303
306
  } },
304
- { label: `Delete row ${ri + 1}`, onClick: () => {
307
+ { label: strings.deleteRow(ri + 1), onClick: () => {
305
308
  deleteRow(ri);
306
309
  setCtxMenu(null);
307
310
  } }
308
311
  ] });
309
- }, [fixedRows, insertRow, deleteRow]);
312
+ }, [fixedRows, insertRow, deleteRow, strings]);
310
313
  const handleColCtx = useCallback((e, ci) => {
311
314
  e.preventDefault();
312
315
  if (fixedRows) return;
313
316
  setCtxMenu({ x: e.clientX, y: e.clientY, items: [
314
- { label: `Insert column left`, onClick: () => {
317
+ { label: strings.insertColumnLeft, onClick: () => {
315
318
  insertCol(ci);
316
319
  setCtxMenu(null);
317
320
  } },
318
- { label: `Insert column right`, onClick: () => {
321
+ { label: strings.insertColumnRight, onClick: () => {
319
322
  insertCol(ci + 1);
320
323
  setCtxMenu(null);
321
324
  } },
322
- { label: `Delete column ${columns[ci]?.title || ci + 1}`, onClick: () => {
325
+ { label: strings.deleteColumn(String(columns[ci]?.title || ci + 1)), onClick: () => {
323
326
  deleteCol(ci);
324
327
  setCtxMenu(null);
325
328
  } }
326
329
  ] });
327
- }, [fixedRows, columns, insertCol, deleteCol]);
330
+ }, [fixedRows, columns, insertCol, deleteCol, strings]);
328
331
  const getColWidth = (ci) => colWidths[ci] ?? columns[ci]?.width ?? 150;
329
332
  useEffect(() => {
330
333
  const handleMouseMove = (e) => {
@@ -591,7 +594,7 @@ function EditableGrid({ columns, data, onChange, onColumnsChange, fixedRows = fa
591
594
  setSelAnchor({ row: 0, col: 0 });
592
595
  setSelEnd({ row: rows.length - 1, col: columns.length - 1 });
593
596
  },
594
- title: "Select all",
597
+ title: strings.selectAll,
595
598
  children: "#"
596
599
  }
597
600
  ),
@@ -630,7 +633,7 @@ function EditableGrid({ columns, data, onChange, onColumnsChange, fixedRows = fa
630
633
  e.stopPropagation();
631
634
  autoFitColumn(ci);
632
635
  },
633
- title: "Drag to resize \xB7 double-click to auto-fit"
636
+ title: strings.resize
634
637
  }
635
638
  )
636
639
  ]
@@ -677,7 +680,7 @@ function EditableGrid({ columns, data, onChange, onColumnsChange, fixedRows = fa
677
680
  e.stopPropagation();
678
681
  autoFitRow(ri);
679
682
  },
680
- title: "Drag to resize \xB7 double-click to auto-fit"
683
+ title: strings.resize
681
684
  }
682
685
  )
683
686
  ]
@@ -733,7 +736,7 @@ function EditableGrid({ columns, data, onChange, onColumnsChange, fixedRows = fa
733
736
  tabIndex: 0,
734
737
  "data-row": ri,
735
738
  "data-col": ci,
736
- className: `w-full h-full px-2 py-1 outline-none whitespace-nowrap overflow-hidden ${col.align === "right" ? "text-right" : col.align === "center" ? "text-center" : ""} ${col.readOnly ? "cursor-default" : "cursor-cell"} font-mono ${fontSizeCls}${styleCls}`,
739
+ className: `w-full h-full px-2 py-1 rosh-focus-ring whitespace-nowrap overflow-hidden ${col.align === "right" ? "text-right" : col.align === "center" ? "text-center" : ""} ${col.readOnly ? "cursor-default" : "cursor-cell"} font-mono ${fontSizeCls}${styleCls}`,
737
740
  onMouseDown: (e) => handleMouseDown(e, ri, ci),
738
741
  onMouseEnter: () => handleMouseEnter(ri, ci),
739
742
  onDoubleClick: () => {
@@ -770,7 +773,7 @@ function EditableGrid({ columns, data, onChange, onColumnsChange, fixedRows = fa
770
773
  "div",
771
774
  {
772
775
  onMouseDown: startFill,
773
- title: "Drag to fill",
776
+ title: strings.fill,
774
777
  className: "absolute -bottom-[3px] -right-[3px] w-[7px] h-[7px] bg-blue-500 border border-white cursor-crosshair z-30 hover:bg-blue-600"
775
778
  }
776
779
  )
@@ -815,5 +818,5 @@ function EditableGrid({ columns, data, onChange, onColumnsChange, fixedRows = fa
815
818
  }
816
819
 
817
820
  export { EditableGrid, withSpareRow };
818
- //# sourceMappingURL=chunk-JFQGPY7W.js.map
819
- //# sourceMappingURL=chunk-JFQGPY7W.js.map
821
+ //# sourceMappingURL=chunk-KWEX4ZAF.js.map
822
+ //# sourceMappingURL=chunk-KWEX4ZAF.js.map