@stringpush/sdk 0.2.0 → 0.3.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.
@@ -1,6 +1,7 @@
1
1
  // src/overlay/constants.ts
2
2
  var OVERLAY_ROOT_ID = "translation-overlay-root";
3
3
  var EDIT_PANEL_HOST_ID = "translation-edit-panel-host";
4
+ var EDIT_PANEL_BACKDROP_ID = "stringpush-edit-panel-backdrop";
4
5
  var OVERLAY_CHUNK_MARKER = "translation-overlay-chunk-v1";
5
6
 
6
7
  // src/overlay/overlay-api.ts
@@ -160,24 +161,68 @@ function notifyEditPanelRemoteUpdate(update) {
160
161
  return activeHandler.applyRemoteUpdate(update);
161
162
  }
162
163
 
163
- // src/overlay/edit-panel.ts
164
- var PANEL_STYLES = `
164
+ // src/overlay/edit-panel-copy.ts
165
+ var EDIT_PANEL_CONFLICT_TITLE = "Conflict detected";
166
+ var EDIT_PANEL_CONFLICT_BODY = "Another editor just saved changes to this key.";
167
+ var EDIT_PANEL_RELOAD_LABEL = "Reload latest values";
168
+ var EDIT_PANEL_FOOTNOTE_STAGING = "Production requires separate promote step.";
169
+ function formatEnvironmentBadgeLabel(environmentName) {
170
+ const label = capitalizeEnvironment(environmentName);
171
+ return `${label} Environment`;
172
+ }
173
+ function formatSaveToEnvironmentLabel(environmentName) {
174
+ return `Save to ${capitalizeEnvironment(environmentName)}`;
175
+ }
176
+ function buildAdminKeyUrl(adminWebOrigin, projectId, keyId) {
177
+ const origin = adminWebOrigin.replace(/\/$/, "");
178
+ const params = new URLSearchParams({
179
+ projectId,
180
+ keyId
181
+ });
182
+ return `${origin}/keys?${params.toString()}`;
183
+ }
184
+ function capitalizeEnvironment(environmentName) {
185
+ if (!environmentName) {
186
+ return "Environment";
187
+ }
188
+ return environmentName.charAt(0).toUpperCase() + environmentName.slice(1);
189
+ }
190
+
191
+ // src/overlay/edit-panel-styles.ts
192
+ var EDIT_PANEL_STYLES = `
165
193
  :host {
166
194
  all: initial;
167
- font-family: system-ui, -apple-system, Segoe UI, sans-serif;
195
+ font-family: Inter, system-ui, -apple-system, Segoe UI, sans-serif;
168
196
  --sp-bg: #ffffff;
169
- --sp-text: #0f172a;
170
- --sp-muted: #64748b;
197
+ --sp-text: #0f172b;
198
+ --sp-muted: #62748e;
171
199
  --sp-border: #e2e8f0;
172
- --sp-accent: #4338ca;
173
- --sp-accent-hover: #3730a3;
174
- --sp-warning-bg: #fef3c7;
175
- --sp-warning-text: #92400e;
176
- --sp-warning-border: #fcd34d;
200
+ --sp-accent: #4f39f6;
201
+ --sp-accent-hover: #4338ca;
202
+ --sp-accent-subtle: #c6d2ff;
203
+ --sp-surface-muted: #f8fafc;
204
+ --sp-warning-bg: #fffbeb;
205
+ --sp-warning-border: #fee685;
206
+ --sp-warning-title: #7b3306;
207
+ --sp-warning-body: #bb4d00;
208
+ --sp-warning-btn-border: #ffd230;
177
209
  --sp-badge-draft-bg: #fef3c6;
178
210
  --sp-badge-draft-text: #973c00;
211
+ --sp-badge-reviewed-bg: #eff6ff;
212
+ --sp-badge-reviewed-text: #1447e6;
213
+ --sp-badge-reviewed-border: #bedbff;
214
+ --sp-badge-published-bg: #ecfdf5;
215
+ --sp-badge-published-text: #007a55;
216
+ --sp-badge-published-border: #a4f4cf;
179
217
  --sp-badge-default-bg: #f1f5f9;
180
- --sp-badge-default-text: #475569;
218
+ --sp-badge-default-text: #45556c;
219
+ --sp-env-staging-bg: #ecfdf5;
220
+ --sp-env-staging-border: #a4f4cf;
221
+ --sp-env-staging-text: #007a55;
222
+ --sp-env-production-bg: #fff7ed;
223
+ --sp-env-production-border: #fdba74;
224
+ --sp-env-production-text: #c2410c;
225
+ --sp-unsaved: #e17100;
181
226
  }
182
227
  .panel {
183
228
  position: fixed;
@@ -188,77 +233,169 @@ var PANEL_STYLES = `
188
233
  height: 100vh;
189
234
  background: var(--sp-bg);
190
235
  color: var(--sp-text);
191
- box-shadow: -8px 0 32px rgba(15, 23, 42, 0.12);
236
+ box-shadow: -25px 0 25px rgba(0, 0, 0, 0.25);
237
+ border-left: 1px solid var(--sp-border);
192
238
  display: flex;
193
239
  flex-direction: column;
194
240
  z-index: 2147483647;
195
241
  }
196
242
  .header {
197
- display: flex;
198
- align-items: flex-start;
199
- justify-content: space-between;
200
- gap: 12px;
201
- padding: 16px 16px 12px;
243
+ background: rgba(248, 250, 252, 0.5);
202
244
  border-bottom: 1px solid var(--sp-border);
245
+ padding: 20px 24px 16px;
203
246
  }
204
- .header-main {
205
- min-width: 0;
247
+ .header-top {
248
+ display: flex;
249
+ align-items: center;
250
+ justify-content: space-between;
251
+ margin-bottom: 12px;
206
252
  }
207
253
  .brand {
208
- margin: 0 0 8px;
254
+ display: inline-flex;
255
+ align-items: center;
256
+ gap: 6px;
209
257
  font-size: 11px;
210
258
  font-weight: 700;
211
- letter-spacing: 0.08em;
259
+ letter-spacing: 0.06em;
212
260
  text-transform: uppercase;
213
- color: var(--sp-accent);
261
+ color: var(--sp-muted);
262
+ }
263
+ .brand-icon {
264
+ width: 14px;
265
+ height: 14px;
266
+ border-radius: 50%;
267
+ background: var(--sp-accent);
268
+ opacity: 0.85;
214
269
  }
215
270
  .title {
216
271
  margin: 0;
217
- font-size: 14px;
272
+ font-size: 18px;
218
273
  font-weight: 600;
274
+ line-height: 1.25;
275
+ letter-spacing: -0.02em;
219
276
  }
220
- .key-path {
221
- margin: 4px 0 0;
222
- font: 12px/1.4 ui-monospace, Menlo, monospace;
223
- color: var(--sp-muted);
224
- word-break: break-all;
277
+ .meta-row {
278
+ display: flex;
279
+ flex-wrap: wrap;
280
+ align-items: center;
281
+ gap: 8px;
282
+ margin-top: 8px;
283
+ }
284
+ .key-chip {
285
+ font: 11px/1.4 ui-monospace, Menlo, monospace;
286
+ color: #45556c;
287
+ background: #fff;
288
+ border: 1px solid var(--sp-border);
289
+ border-radius: 4px;
290
+ padding: 4px 8px;
291
+ }
292
+ .env-badge {
293
+ display: inline-flex;
294
+ align-items: center;
295
+ gap: 6px;
296
+ padding: 5px 9px;
297
+ border-radius: 4px;
298
+ font-size: 10px;
299
+ font-weight: 500;
300
+ letter-spacing: 0.01em;
301
+ border: 1px solid transparent;
302
+ }
303
+ .env-badge--staging {
304
+ background: var(--sp-env-staging-bg);
305
+ border-color: var(--sp-env-staging-border);
306
+ color: var(--sp-env-staging-text);
307
+ }
308
+ .env-badge--production {
309
+ background: var(--sp-env-production-bg);
310
+ border-color: var(--sp-env-production-border);
311
+ color: var(--sp-env-production-text);
312
+ }
313
+ .env-dot {
314
+ width: 6px;
315
+ height: 6px;
316
+ border-radius: 50%;
317
+ background: currentColor;
225
318
  }
226
319
  .close-btn {
227
320
  border: none;
228
321
  background: transparent;
229
- font-size: 22px;
322
+ font-size: 20px;
230
323
  line-height: 1;
231
324
  cursor: pointer;
232
325
  color: var(--sp-muted);
233
- padding: 2px 4px;
326
+ padding: 6px;
327
+ border-radius: 8px;
328
+ }
329
+ .close-btn:focus-visible {
330
+ outline: 2px solid var(--sp-accent);
331
+ outline-offset: 2px;
234
332
  }
235
333
  .body {
236
334
  flex: 1;
237
335
  overflow: auto;
238
- padding: 12px 16px;
336
+ padding: 24px;
239
337
  }
240
- .locale-row {
241
- margin-bottom: 16px;
242
- padding-bottom: 16px;
243
- border-bottom: 1px solid var(--sp-border);
338
+ .banner {
339
+ margin-bottom: 32px;
340
+ padding: 17px;
341
+ border-radius: 10px;
342
+ border: 1px solid var(--sp-warning-border);
343
+ background: var(--sp-warning-bg);
344
+ }
345
+ .banner-row {
346
+ display: flex;
347
+ gap: 12px;
348
+ align-items: flex-start;
349
+ }
350
+ .banner-icon {
351
+ width: 20px;
352
+ height: 20px;
353
+ flex-shrink: 0;
354
+ margin-top: 2px;
355
+ color: var(--sp-warning-body);
356
+ }
357
+ .banner-title {
358
+ margin: 0;
359
+ font-size: 14px;
360
+ font-weight: 600;
361
+ color: var(--sp-warning-title);
362
+ }
363
+ .banner-body {
364
+ margin: 4px 0 0;
365
+ font-size: 14px;
366
+ line-height: 1.45;
367
+ color: var(--sp-warning-body);
368
+ }
369
+ .banner-reload {
370
+ margin-top: 12px;
371
+ border: 1px solid var(--sp-warning-btn-border);
372
+ background: rgba(255, 251, 235, 0.5);
373
+ border-radius: 8px;
374
+ padding: 6px 13px;
375
+ cursor: pointer;
376
+ font-size: 14px;
377
+ font-weight: 500;
378
+ color: var(--sp-badge-draft-text);
379
+ }
380
+ .banner-reload:focus-visible {
381
+ outline: 2px solid var(--sp-warning-body);
382
+ outline-offset: 2px;
244
383
  }
245
- .locale-row:last-child {
246
- border-bottom: none;
247
- margin-bottom: 0;
248
- padding-bottom: 0;
384
+ .locale-row + .locale-row {
385
+ margin-top: 24px;
249
386
  }
250
387
  .locale-header {
251
388
  display: flex;
252
389
  align-items: center;
253
390
  justify-content: space-between;
254
391
  gap: 8px;
255
- margin-bottom: 8px;
392
+ margin-bottom: 12px;
256
393
  }
257
394
  .locale-label {
258
395
  display: inline-flex;
259
396
  align-items: center;
260
- gap: 6px;
261
- font-size: 13px;
397
+ gap: 8px;
398
+ font-size: 14px;
262
399
  font-weight: 600;
263
400
  color: var(--sp-text);
264
401
  }
@@ -268,102 +405,154 @@ var PANEL_STYLES = `
268
405
  }
269
406
  .locale-badges {
270
407
  display: inline-flex;
408
+ align-items: center;
409
+ gap: 8px;
410
+ }
411
+ .unsaved {
412
+ display: inline-flex;
413
+ align-items: center;
271
414
  gap: 4px;
415
+ font-size: 10px;
416
+ font-weight: 500;
417
+ color: var(--sp-unsaved);
418
+ }
419
+ .unsaved-dot {
420
+ width: 6px;
421
+ height: 6px;
422
+ border-radius: 50%;
423
+ background: #fe9a00;
272
424
  }
273
425
  .badge {
274
426
  display: inline-flex;
275
427
  align-items: center;
276
- padding: 2px 8px;
277
- border-radius: 999px;
428
+ padding: 2px 6px;
429
+ border-radius: 4px;
278
430
  font-size: 10px;
279
- font-weight: 700;
280
- letter-spacing: 0.02em;
431
+ font-weight: 500;
432
+ letter-spacing: 0.01em;
433
+ border: 1px solid transparent;
281
434
  }
282
435
  .badge-default {
283
436
  background: var(--sp-badge-default-bg);
437
+ border-color: var(--sp-border);
284
438
  color: var(--sp-badge-default-text);
285
439
  }
286
440
  .badge-draft {
287
441
  background: var(--sp-badge-draft-bg);
442
+ border-color: var(--sp-warning-border);
288
443
  color: var(--sp-badge-draft-text);
289
444
  }
445
+ .badge-reviewed {
446
+ background: var(--sp-badge-reviewed-bg);
447
+ border-color: var(--sp-badge-reviewed-border);
448
+ color: var(--sp-badge-reviewed-text);
449
+ }
450
+ .badge-published {
451
+ background: var(--sp-badge-published-bg);
452
+ border-color: var(--sp-badge-published-border);
453
+ color: var(--sp-badge-published-text);
454
+ }
290
455
  .locale-row textarea {
291
456
  width: 100%;
292
- min-height: 80px;
457
+ min-height: 60px;
293
458
  box-sizing: border-box;
294
- border: 1px solid #cbd5e1;
459
+ border: 1px solid #cad5e2;
295
460
  border-radius: 8px;
296
461
  padding: 10px 12px;
297
462
  font: 14px/1.45 inherit;
298
463
  resize: vertical;
299
464
  color: var(--sp-text);
465
+ background: var(--sp-surface-muted);
466
+ }
467
+ .locale-row textarea.is-dirty {
468
+ background: #fff;
469
+ border-color: #a3b3ff;
470
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
300
471
  }
301
472
  .locale-row textarea:focus-visible {
302
473
  outline: 2px solid var(--sp-accent);
303
474
  outline-offset: 1px;
304
475
  border-color: var(--sp-accent);
305
476
  }
306
- .banner {
307
- margin: 0 16px 12px;
308
- padding: 12px 14px;
309
- border-radius: 8px;
310
- border: 1px solid var(--sp-warning-border);
311
- background: var(--sp-warning-bg);
312
- color: var(--sp-warning-text);
313
- font-size: 13px;
314
- line-height: 1.45;
315
- }
316
- .banner p {
317
- margin: 0;
318
- }
319
- .banner button {
320
- margin-top: 10px;
321
- border: 1px solid #d97706;
322
- background: #fff;
323
- border-radius: 6px;
324
- padding: 6px 12px;
325
- cursor: pointer;
326
- font-size: 12px;
327
- font-weight: 600;
328
- color: var(--sp-warning-text);
329
- }
330
477
  .footer {
331
- display: flex;
332
- gap: 8px;
333
- justify-content: flex-end;
334
- padding: 12px 16px 16px;
335
478
  border-top: 1px solid var(--sp-border);
479
+ background: var(--sp-surface-muted);
480
+ padding: 16px 24px;
481
+ }
482
+ .footer-actions {
483
+ display: flex;
484
+ gap: 12px;
336
485
  }
337
- .footer button {
486
+ .footer-actions button {
338
487
  border-radius: 8px;
339
- padding: 9px 14px;
340
- font-size: 14px;
341
- font-weight: 600;
342
488
  cursor: pointer;
343
- display: inline-flex;
344
- align-items: center;
345
- gap: 6px;
489
+ font-weight: 500;
346
490
  }
347
- .footer .secondary {
348
- border: 1px solid #cbd5e1;
491
+ .footer .discard {
492
+ flex: 1;
493
+ min-height: 40px;
494
+ border: 1px solid rgba(0, 0, 0, 0.1);
349
495
  background: #fff;
350
- color: #334155;
496
+ color: var(--sp-text);
497
+ font-size: 16px;
351
498
  }
352
499
  .footer .primary {
500
+ flex: 1;
501
+ min-height: 53px;
353
502
  border: none;
354
503
  background: var(--sp-accent);
355
504
  color: #fff;
505
+ display: flex;
506
+ flex-direction: column;
507
+ align-items: center;
508
+ justify-content: center;
509
+ padding: 8px 16px;
356
510
  }
357
511
  .footer .primary:hover:not(:disabled) {
358
512
  background: var(--sp-accent-hover);
359
513
  }
360
- .footer .primary::before {
361
- content: "";
362
- width: 14px;
363
- height: 14px;
364
- background: currentColor;
365
- mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14' fill='none'%3E%3Cpath d='M11.667 3.5 5.25 9.917 2.333 7' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")
366
- center / contain no-repeat;
514
+ .footer .primary:disabled {
515
+ opacity: 0.55;
516
+ cursor: not-allowed;
517
+ }
518
+ .primary-label {
519
+ font-size: 14px;
520
+ font-weight: 600;
521
+ line-height: 1.25;
522
+ }
523
+ .primary-sub {
524
+ margin-top: 2px;
525
+ font-size: 10px;
526
+ font-weight: 400;
527
+ color: var(--sp-accent-subtle);
528
+ line-height: 1.2;
529
+ }
530
+ .footer-meta {
531
+ display: flex;
532
+ align-items: center;
533
+ justify-content: space-between;
534
+ gap: 12px;
535
+ margin-top: 16px;
536
+ padding-bottom: 4px;
537
+ }
538
+ .admin-link {
539
+ display: inline-flex;
540
+ align-items: center;
541
+ gap: 4px;
542
+ font-size: 12px;
543
+ font-weight: 500;
544
+ color: var(--sp-accent);
545
+ text-decoration: none;
546
+ }
547
+ .admin-link:focus-visible {
548
+ outline: 2px solid var(--sp-accent);
549
+ outline-offset: 2px;
550
+ }
551
+ .footer-note {
552
+ font-size: 10px;
553
+ color: var(--sp-muted);
554
+ text-align: right;
555
+ line-height: 1.35;
367
556
  }
368
557
  .status {
369
558
  padding: 24px 16px;
@@ -411,41 +600,69 @@ var PANEL_STYLES = `
411
600
  font-size: 13px;
412
601
  }
413
602
  `;
603
+ var EDIT_PANEL_BACKDROP_STYLES = `
604
+ position: fixed;
605
+ inset: 0;
606
+ background: rgba(15, 23, 43, 0.2);
607
+ z-index: 2147483646;
608
+ pointer-events: none;
609
+ `;
610
+
611
+ // src/overlay/edit-panel.ts
414
612
  function openEditPanel(keyPath, context) {
415
613
  if (typeof document === "undefined") {
416
614
  return { destroy: () => {
417
615
  } };
418
616
  }
419
617
  closeEditPanel();
618
+ const backdrop = document.createElement("div");
619
+ backdrop.id = EDIT_PANEL_BACKDROP_ID;
620
+ backdrop.setAttribute("aria-hidden", "true");
621
+ backdrop.style.cssText = EDIT_PANEL_BACKDROP_STYLES;
420
622
  const host = document.createElement("div");
421
623
  host.id = EDIT_PANEL_HOST_ID;
422
624
  const shadow = host.attachShadow({ mode: "open" });
423
625
  const style = document.createElement("style");
424
- style.textContent = PANEL_STYLES;
626
+ style.textContent = EDIT_PANEL_STYLES;
425
627
  const panel = document.createElement("div");
426
628
  panel.className = "panel";
427
629
  panel.setAttribute("role", "dialog");
428
630
  panel.setAttribute("aria-label", "Edit translation");
429
631
  const header = document.createElement("header");
430
632
  header.className = "header";
431
- const headerMain = document.createElement("div");
432
- headerMain.className = "header-main";
433
- const brand = document.createElement("p");
633
+ const headerTop = document.createElement("div");
634
+ headerTop.className = "header-top";
635
+ const brand = document.createElement("div");
434
636
  brand.className = "brand";
435
- brand.textContent = "StringPush";
436
- const title = document.createElement("h2");
437
- title.className = "title";
438
- title.textContent = "Edit translation";
439
- const keyEl = document.createElement("p");
440
- keyEl.className = "key-path";
441
- keyEl.textContent = keyPath;
442
- headerMain.append(brand, title, keyEl);
637
+ const brandIcon = document.createElement("span");
638
+ brandIcon.className = "brand-icon";
639
+ brandIcon.setAttribute("aria-hidden", "true");
640
+ brand.append(brandIcon, document.createTextNode("StringPush"));
443
641
  const closeBtn = document.createElement("button");
444
642
  closeBtn.className = "close-btn";
445
643
  closeBtn.type = "button";
446
644
  closeBtn.setAttribute("aria-label", "Close");
447
645
  closeBtn.textContent = "\xD7";
448
- header.append(headerMain, closeBtn);
646
+ headerTop.append(brand, closeBtn);
647
+ const title = document.createElement("h2");
648
+ title.className = "title";
649
+ title.textContent = "Edit translation";
650
+ const metaRow = document.createElement("div");
651
+ metaRow.className = "meta-row";
652
+ const keyChip = document.createElement("span");
653
+ keyChip.className = "key-chip";
654
+ keyChip.textContent = keyPath;
655
+ const envBadge = document.createElement("span");
656
+ envBadge.className = context.environmentName === "production" ? "env-badge env-badge--production" : "env-badge env-badge--staging";
657
+ const envDot = document.createElement("span");
658
+ envDot.className = "env-dot";
659
+ envDot.setAttribute("aria-hidden", "true");
660
+ envBadge.append(
661
+ envDot,
662
+ document.createTextNode(formatEnvironmentBadgeLabel(context.environmentName))
663
+ );
664
+ metaRow.append(keyChip, envBadge);
665
+ header.append(headerTop, title, metaRow);
449
666
  const bodyEl = document.createElement("div");
450
667
  bodyEl.className = "body";
451
668
  const status = document.createElement("div");
@@ -457,19 +674,38 @@ function openEditPanel(keyPath, context) {
457
674
  banner.hidden = true;
458
675
  const footer = document.createElement("footer");
459
676
  footer.className = "footer";
460
- const cancelBtn = document.createElement("button");
461
- cancelBtn.type = "button";
462
- cancelBtn.className = "secondary";
463
- cancelBtn.textContent = "Cancel";
677
+ const footerActions = document.createElement("div");
678
+ footerActions.className = "footer-actions";
679
+ const discardBtn = document.createElement("button");
680
+ discardBtn.type = "button";
681
+ discardBtn.className = "discard";
682
+ discardBtn.textContent = "Discard";
464
683
  const saveBtn = document.createElement("button");
465
684
  saveBtn.type = "button";
466
685
  saveBtn.className = "primary";
467
- saveBtn.textContent = "Save draft";
468
686
  saveBtn.disabled = true;
469
- footer.append(cancelBtn, saveBtn);
470
- panel.append(header, banner, bodyEl, footer);
687
+ const saveLabel = document.createElement("span");
688
+ saveLabel.className = "primary-label";
689
+ const saveSub = document.createElement("span");
690
+ saveSub.className = "primary-sub";
691
+ saveBtn.append(saveLabel, saveSub);
692
+ updateSaveButtonLabels(context, saveLabel, saveSub);
693
+ footerActions.append(discardBtn, saveBtn);
694
+ const footerMeta = document.createElement("div");
695
+ footerMeta.className = "footer-meta";
696
+ const adminLink = document.createElement("a");
697
+ adminLink.className = "admin-link";
698
+ adminLink.target = "_blank";
699
+ adminLink.rel = "noopener noreferrer";
700
+ adminLink.textContent = "Open in admin \u2197";
701
+ const footerNote = document.createElement("span");
702
+ footerNote.className = "footer-note";
703
+ footerNote.textContent = context.environmentName === "production" ? "Changes apply to production immediately." : EDIT_PANEL_FOOTNOTE_STAGING;
704
+ footerMeta.append(adminLink, footerNote);
705
+ footer.append(footerActions, footerMeta);
706
+ panel.append(header, bodyEl, footer);
471
707
  shadow.append(style, panel);
472
- document.body.append(host);
708
+ document.body.append(backdrop, host);
473
709
  let destroyed = false;
474
710
  let rows = [];
475
711
  let keyId = "";
@@ -479,8 +715,15 @@ function openEditPanel(keyPath, context) {
479
715
  }
480
716
  destroyed = true;
481
717
  registerEditPanelRemoteHandler(null);
718
+ backdrop.remove();
482
719
  host.remove();
483
720
  };
721
+ function hasUnsavedChanges() {
722
+ return rows.some((row) => row.draftValue !== row.value);
723
+ }
724
+ function updateSaveDisabled() {
725
+ saveBtn.disabled = !hasUnsavedChanges() || destroyed;
726
+ }
484
727
  function applyRemoteUpdate(update) {
485
728
  if (!shouldApplyTranslationUpdate(update.keyPath, update.localeCode, update.version)) {
486
729
  return false;
@@ -493,9 +736,7 @@ function openEditPanel(keyPath, context) {
493
736
  return false;
494
737
  }
495
738
  if (row.draftValue !== row.value) {
496
- showConflict(
497
- `${update.localeCode} was updated in another tab. Reload to merge your changes.`
498
- );
739
+ showConflict();
499
740
  return true;
500
741
  }
501
742
  row.value = update.value;
@@ -506,10 +747,14 @@ function openEditPanel(keyPath, context) {
506
747
  return true;
507
748
  }
508
749
  closeBtn.addEventListener("click", destroy);
509
- cancelBtn.addEventListener("click", destroy);
750
+ discardBtn.addEventListener("click", destroy);
510
751
  function renderRows() {
511
752
  bodyEl.replaceChildren();
753
+ if (!banner.hidden) {
754
+ bodyEl.append(banner);
755
+ }
512
756
  for (const row of rows) {
757
+ const isDirty = row.draftValue !== row.value;
513
758
  const wrap = document.createElement("div");
514
759
  wrap.className = "locale-row";
515
760
  const localeHeader = document.createElement("div");
@@ -526,26 +771,65 @@ function openEditPanel(keyPath, context) {
526
771
  if (row.localeCode === context.activeLocaleCode) {
527
772
  const defaultBadge = document.createElement("span");
528
773
  defaultBadge.className = "badge badge-default";
529
- defaultBadge.textContent = "Default";
774
+ defaultBadge.textContent = "Default locale";
530
775
  badges.append(defaultBadge);
531
776
  }
532
- const draftBadge = document.createElement("span");
533
- draftBadge.className = "badge badge-draft";
534
- draftBadge.textContent = row.draftValue !== row.value ? "Unsaved" : "Draft";
535
- badges.append(draftBadge);
777
+ const unsaved = document.createElement("span");
778
+ unsaved.className = "unsaved";
779
+ unsaved.hidden = !isDirty;
780
+ const dot = document.createElement("span");
781
+ dot.className = "unsaved-dot";
782
+ dot.setAttribute("aria-hidden", "true");
783
+ unsaved.append(dot, document.createTextNode("Unsaved changes"));
784
+ badges.append(unsaved);
785
+ let draftBadge = null;
786
+ if (row.status === "draft" && (isDirty || row.version !== null)) {
787
+ draftBadge = document.createElement("span");
788
+ draftBadge.className = "badge badge-draft";
789
+ draftBadge.textContent = "Draft";
790
+ badges.append(draftBadge);
791
+ } else if (row.status === "reviewed") {
792
+ const reviewedBadge = document.createElement("span");
793
+ reviewedBadge.className = "badge badge-reviewed";
794
+ reviewedBadge.textContent = "Reviewed";
795
+ badges.append(reviewedBadge);
796
+ } else if (row.status === "published") {
797
+ const publishedBadge = document.createElement("span");
798
+ publishedBadge.className = "badge badge-published";
799
+ publishedBadge.textContent = "Published";
800
+ badges.append(publishedBadge);
801
+ }
536
802
  localeHeader.append(label, badges);
537
803
  const textarea = document.createElement("textarea");
538
804
  textarea.id = `locale-${row.localeId}`;
539
805
  textarea.setAttribute("aria-label", `${row.localeCode} translation`);
540
806
  textarea.value = row.draftValue;
807
+ if (isDirty) {
808
+ textarea.classList.add("is-dirty");
809
+ }
541
810
  textarea.addEventListener("input", () => {
542
811
  row.draftValue = textarea.value;
543
- draftBadge.textContent = row.draftValue !== row.value ? "Unsaved" : "Draft";
812
+ const dirty = row.draftValue !== row.value;
813
+ textarea.classList.toggle("is-dirty", dirty);
814
+ unsaved.hidden = !dirty;
815
+ if (draftBadge) {
816
+ draftBadge.hidden = !dirty && row.status !== "draft";
817
+ }
818
+ updateSaveDisabled();
544
819
  });
545
820
  wrap.append(localeHeader, textarea);
546
821
  bodyEl.append(wrap);
547
822
  }
548
- saveBtn.disabled = false;
823
+ updateSaveDisabled();
824
+ updateAdminLink();
825
+ }
826
+ function updateAdminLink() {
827
+ if (context.adminWebOrigin && keyId) {
828
+ adminLink.href = buildAdminKeyUrl(context.adminWebOrigin, context.projectId, keyId);
829
+ adminLink.hidden = false;
830
+ } else {
831
+ adminLink.hidden = true;
832
+ }
549
833
  }
550
834
  async function load() {
551
835
  try {
@@ -557,6 +841,8 @@ function openEditPanel(keyPath, context) {
557
841
  return;
558
842
  }
559
843
  keyId = keyValues.keyId;
844
+ context.approvalEnabled = keyValues.approvalEnabled;
845
+ updateSaveButtonLabels(context, saveLabel, saveSub);
560
846
  const localeCodes = new Set(locales.map((l) => l.code));
561
847
  rows = keyValues.entries.filter((entry) => localeCodes.has(entry.localeCode)).map((entry) => ({
562
848
  ...entry,
@@ -629,15 +915,30 @@ function openEditPanel(keyPath, context) {
629
915
  wrap.append(intro, label, input, errorEl, createBtn);
630
916
  bodyEl.append(wrap);
631
917
  }
632
- function showConflict(message) {
918
+ function showConflict() {
633
919
  banner.hidden = false;
634
920
  banner.replaceChildren();
635
- const text = document.createElement("p");
636
- text.textContent = message;
637
- banner.append(text);
921
+ const row = document.createElement("div");
922
+ row.className = "banner-row";
923
+ const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
924
+ icon.setAttribute("class", "banner-icon");
925
+ icon.setAttribute("viewBox", "0 0 20 20");
926
+ icon.setAttribute("fill", "currentColor");
927
+ icon.innerHTML = '<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>';
928
+ const copy = document.createElement("div");
929
+ const titleEl = document.createElement("p");
930
+ titleEl.className = "banner-title";
931
+ titleEl.textContent = EDIT_PANEL_CONFLICT_TITLE;
932
+ const body = document.createElement("p");
933
+ body.className = "banner-body";
934
+ body.textContent = EDIT_PANEL_CONFLICT_BODY;
935
+ copy.append(titleEl, body);
936
+ row.append(icon, copy);
937
+ banner.append(row);
638
938
  const reloadBtn = document.createElement("button");
639
939
  reloadBtn.type = "button";
640
- reloadBtn.textContent = "Reload";
940
+ reloadBtn.className = "banner-reload";
941
+ reloadBtn.textContent = EDIT_PANEL_RELOAD_LABEL;
641
942
  reloadBtn.addEventListener("click", () => {
642
943
  status.textContent = "Loading\u2026";
643
944
  bodyEl.replaceChildren(status);
@@ -645,6 +946,9 @@ function openEditPanel(keyPath, context) {
645
946
  void load();
646
947
  });
647
948
  banner.append(reloadBtn);
949
+ if (!bodyEl.contains(banner)) {
950
+ renderRows();
951
+ }
648
952
  }
649
953
  saveBtn.addEventListener("click", () => {
650
954
  void (async () => {
@@ -664,23 +968,23 @@ function openEditPanel(keyPath, context) {
664
968
  row.value = saved.value;
665
969
  row.version = saved.version;
666
970
  row.draftValue = saved.value;
971
+ row.status = saved.status;
667
972
  context.onSaved(keyPath, row.localeCode, saved.value, saved.version);
668
973
  } catch (error) {
669
974
  if (error instanceof OverlayApiError && error.isVersionConflict) {
670
975
  hadConflict = true;
671
- showConflict(
672
- "Someone else updated this key. Reload to fetch the latest versions, then try again."
673
- );
976
+ showConflict();
674
977
  break;
675
978
  }
676
- showConflict(error instanceof Error ? error.message : "Save failed");
979
+ showConflict();
677
980
  hadConflict = true;
678
981
  break;
679
982
  }
680
983
  }
681
- saveBtn.disabled = hadConflict;
682
984
  if (!hadConflict) {
683
985
  destroy();
986
+ } else {
987
+ updateSaveDisabled();
684
988
  }
685
989
  })();
686
990
  });
@@ -688,8 +992,18 @@ function openEditPanel(keyPath, context) {
688
992
  void load();
689
993
  return { destroy };
690
994
  }
995
+ function updateSaveButtonLabels(context, saveLabel, saveSub) {
996
+ if (context.approvalEnabled) {
997
+ saveLabel.textContent = "Save draft";
998
+ saveSub.textContent = "Needs review before publish";
999
+ return;
1000
+ }
1001
+ saveLabel.textContent = formatSaveToEnvironmentLabel(context.environmentName);
1002
+ saveSub.textContent = "Updates site immediately";
1003
+ }
691
1004
  function closeEditPanel() {
692
1005
  registerEditPanelRemoteHandler(null);
1006
+ document.getElementById(EDIT_PANEL_BACKDROP_ID)?.remove();
693
1007
  document.getElementById(EDIT_PANEL_HOST_ID)?.remove();
694
1008
  }
695
1009
 
@@ -1005,6 +1319,10 @@ function mountOverlay(context) {
1005
1319
  apiBaseUrl: context.apiBaseUrl,
1006
1320
  editToken: context.editToken,
1007
1321
  origin: context.origin,
1322
+ projectId: context.projectId,
1323
+ environmentName: context.environmentName,
1324
+ approvalEnabled: context.approvalEnabled ?? false,
1325
+ adminWebOrigin: context.adminWebOrigin,
1008
1326
  activeLocaleCode: context.activeLocaleCode,
1009
1327
  suggestedDefaultMessage,
1010
1328
  onSaved: context.onCatalogPatched
@@ -1036,4 +1354,4 @@ export {
1036
1354
  OVERLAY_ROOT_ID,
1037
1355
  mountOverlay
1038
1356
  };
1039
- //# sourceMappingURL=overlay-7KC2RRGB.mjs.map
1357
+ //# sourceMappingURL=overlay-Y3NQFQBI.mjs.map