@alfakit/vue 0.1.8 → 0.1.10
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/CHANGELOG.md +14 -0
- package/dist/app/components.d.ts.map +1 -1
- package/dist/app/components.js +1 -0
- package/dist/app/components.js.map +1 -1
- package/dist/components/actions/ActionModal.js.map +1 -1
- package/dist/components/actions/ActionModal.vue.d.ts.map +1 -1
- package/dist/components/actions/ActionModal.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/actions/ActionModal.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/dev-tools/DevToolsInspector.js.map +1 -1
- package/dist/components/dev-tools/DevToolsInspector.vue.d.ts.map +1 -1
- package/dist/components/dev-tools/DevToolsInspector.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/dev-tools/DevToolsInspector.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/fields/CheckboxListField.js.map +1 -1
- package/dist/components/fields/CheckboxListField.vue.d.ts.map +1 -1
- package/dist/components/fields/CheckboxListField.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/fields/CheckboxListField.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/fields/DatePickerField.js.map +1 -1
- package/dist/components/fields/DatePickerField.vue.d.ts.map +1 -1
- package/dist/components/fields/DatePickerField.vue_vue_type_script_setup_true_lang.js +34 -32
- package/dist/components/fields/DatePickerField.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/fields/RichEditorSuggestions.js.map +1 -1
- package/dist/components/fields/RichEditorSuggestions.vue.d.ts +1 -1
- package/dist/components/fields/RichEditorSuggestions.vue.d.ts.map +1 -1
- package/dist/components/fields/RichEditorSuggestions.vue_vue_type_script_setup_true_lang.js +29 -23
- package/dist/components/fields/RichEditorSuggestions.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/fields/RichEditorToolbarGroup.js.map +1 -1
- package/dist/components/fields/RichEditorToolbarGroup.vue.d.ts.map +1 -1
- package/dist/components/fields/RichEditorToolbarGroup.vue_vue_type_script_setup_true_lang.js +9 -4
- package/dist/components/fields/RichEditorToolbarGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/file-manager/FileManagerView.js.map +1 -1
- package/dist/components/file-manager/FileManagerView.vue.d.ts.map +1 -1
- package/dist/components/file-manager/FileManagerView.vue_vue_type_script_setup_true_lang.js +5 -1
- package/dist/components/file-manager/FileManagerView.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/form/FormBlock.js.map +1 -1
- package/dist/components/form/FormBlock.vue.d.ts.map +1 -1
- package/dist/components/form/FormBlock.vue_vue_type_script_setup_true_lang.js +79 -78
- package/dist/components/form/FormBlock.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/layout/CalloutComponent.js.map +1 -1
- package/dist/components/layout/CalloutComponent.vue.d.ts.map +1 -1
- package/dist/components/layout/CalloutComponent.vue_vue_type_script_setup_true_lang.js +43 -39
- package/dist/components/layout/CalloutComponent.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/layout/EmptyStateComponent.js.map +1 -1
- package/dist/components/layout/EmptyStateComponent.vue.d.ts.map +1 -1
- package/dist/components/layout/EmptyStateComponent.vue_vue_type_script_setup_true_lang.js +29 -26
- package/dist/components/layout/EmptyStateComponent.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/layout/MixedContent.js.map +1 -1
- package/dist/components/layout/MixedContent.vue.d.ts +5 -3
- package/dist/components/layout/MixedContent.vue.d.ts.map +1 -1
- package/dist/components/layout/MixedContent.vue_vue_type_script_setup_true_lang.js +5 -4
- package/dist/components/layout/MixedContent.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/layout/SectionComponent.js.map +1 -1
- package/dist/components/layout/SectionComponent.vue.d.ts.map +1 -1
- package/dist/components/layout/SectionComponent.vue_vue_type_script_setup_true_lang.js +12 -8
- package/dist/components/layout/SectionComponent.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/notifications/NotificationsMenu.js.map +1 -1
- package/dist/components/notifications/NotificationsMenu.vue.d.ts.map +1 -1
- package/dist/components/notifications/NotificationsMenu.vue_vue_type_script_setup_true_lang.js +133 -104
- package/dist/components/notifications/NotificationsMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/panel/BottomNav.js.map +1 -1
- package/dist/components/panel/BottomNav.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/panel/BottomNav.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/panel/KeyboardShortcuts.js.map +1 -1
- package/dist/components/panel/KeyboardShortcuts.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/panel/KeyboardShortcuts.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/panel/NavGroup.js.map +1 -1
- package/dist/components/panel/NavGroup.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/components/panel/NavGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/panel/NavItem.js.map +1 -1
- package/dist/components/panel/NavItem.vue.d.ts.map +1 -1
- package/dist/components/panel/NavItem.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/panel/NavItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/panel/NavSubItem.js.map +1 -1
- package/dist/components/panel/NavSubItem.vue.d.ts.map +1 -1
- package/dist/components/panel/NavSubItem.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/panel/NavSubItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/panel/ShortcutsOverlay.js.map +1 -1
- package/dist/components/panel/ShortcutsOverlay.vue.d.ts.map +1 -1
- package/dist/components/panel/ShortcutsOverlay.vue_vue_type_script_setup_true_lang.js +20 -19
- package/dist/components/panel/ShortcutsOverlay.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/table/ColumnManager.js.map +1 -1
- package/dist/components/table/ColumnManager.vue.d.ts.map +1 -1
- package/dist/components/table/ColumnManager.vue_vue_type_script_setup_true_lang.js +6 -6
- package/dist/components/table/ColumnManager.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/table/Spreadsheet.js.map +1 -1
- package/dist/components/table/Spreadsheet.vue.d.ts.map +1 -1
- package/dist/components/table/Spreadsheet.vue_vue_type_script_setup_true_lang.js +37 -33
- package/dist/components/table/Spreadsheet.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/translation-manager/TranslationManagerView.js.map +1 -1
- package/dist/components/translation-manager/TranslationManagerView.vue.d.ts.map +1 -1
- package/dist/components/translation-manager/TranslationManagerView.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/translation-manager/TranslationManagerView.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/ui/SidebarMenuButton.js.map +1 -1
- package/dist/components/ui/SidebarMenuButton.vue.d.ts.map +1 -1
- package/dist/components/ui/SidebarMenuButton.vue_vue_type_script_setup_true_lang.js +12 -12
- package/dist/components/ui/SidebarMenuButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/ui/TooltipTrigger.js.map +1 -1
- package/dist/components/ui/TooltipTrigger.vue.d.ts +2 -2
- package/dist/components/ui/TooltipTrigger.vue.d.ts.map +1 -1
- package/dist/components/ui/TooltipTrigger.vue_vue_type_script_setup_true_lang.js +10 -7
- package/dist/components/ui/TooltipTrigger.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/widgets/StatCard.js.map +1 -1
- package/dist/components/widgets/StatCard.vue.d.ts.map +1 -1
- package/dist/components/widgets/StatCard.vue_vue_type_script_setup_true_lang.js +30 -30
- package/dist/components/widgets/StatCard.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/eject-manifest.json +2 -2
- package/dist/layouts/TopNavLayout.js.map +1 -1
- package/dist/layouts/TopNavLayout.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/layouts/TopNavLayout.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/layouts/TopNavMenu.js.map +1 -1
- package/dist/layouts/TopNavMenu.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/layouts/TopNavMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/pages/AlfakitPage.js.map +1 -1
- package/dist/pages/AlfakitPage.vue.d.ts.map +1 -1
- package/dist/pages/AlfakitPage.vue_vue_type_script_setup_true_lang.js +16 -19
- package/dist/pages/AlfakitPage.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/standalone/AlfakitComponent.js.map +1 -1
- package/dist/standalone/AlfakitComponent.vue.d.ts.map +1 -1
- package/dist/standalone/AlfakitComponent.vue_vue_type_script_setup_true_lang.js +29 -24
- package/dist/standalone/AlfakitComponent.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/standalone/runtime.d.ts.map +1 -1
- package/dist/standalone/runtime.js +25 -25
- package/dist/standalone/runtime.js.map +1 -1
- package/dist/standalone.css +12 -7
- package/dist/starter-kit/AlfakitStarterKitRoot.js.map +1 -1
- package/dist/starter-kit/AlfakitStarterKitRoot.vue.d.ts.map +1 -1
- package/dist/starter-kit/AlfakitStarterKitRoot.vue_vue_type_script_setup_true_lang.js +25 -21
- package/dist/starter-kit/AlfakitStarterKitRoot.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/starter-kit/navigation.d.ts +6 -1
- package/dist/starter-kit/navigation.d.ts.map +1 -1
- package/dist/starter-kit/navigation.js +11 -10
- package/dist/starter-kit/navigation.js.map +1 -1
- package/dist/styles.css +2 -1
- package/dist/themes.css +4 -0
- package/package.json +2 -2
- package/src/app/components.ts +3 -0
- package/src/components/actions/ActionModal.vue +2 -0
- package/src/components/dev-tools/DevToolsInspector.vue +1 -1
- package/src/components/fields/CheckboxListField.vue +2 -2
- package/src/components/fields/DatePickerField.vue +1 -0
- package/src/components/fields/RichEditorSuggestions.vue +13 -6
- package/src/components/fields/RichEditorToolbarGroup.vue +16 -1
- package/src/components/file-manager/FileManagerView.vue +3 -1
- package/src/components/form/FormBlock.vue +15 -2
- package/src/components/layout/CalloutComponent.vue +15 -2
- package/src/components/layout/EmptyStateComponent.vue +10 -1
- package/src/components/layout/MixedContent.vue +12 -4
- package/src/components/layout/SectionComponent.vue +20 -4
- package/src/components/notifications/NotificationsMenu.vue +45 -1
- package/src/components/panel/BottomNav.vue +2 -2
- package/src/components/panel/KeyboardShortcuts.vue +1 -1
- package/src/components/panel/NavGroup.vue +3 -3
- package/src/components/panel/NavItem.vue +5 -1
- package/src/components/panel/NavSubItem.vue +5 -1
- package/src/components/panel/ShortcutsOverlay.vue +27 -22
- package/src/components/table/ColumnManager.vue +6 -4
- package/src/components/table/Spreadsheet.vue +9 -4
- package/src/components/translation-manager/TranslationManagerView.vue +1 -1
- package/src/components/ui/SidebarMenuButton.vue +6 -0
- package/src/components/ui/TooltipTrigger.vue +4 -1
- package/src/components/widgets/StatCard.vue +6 -3
- package/src/layouts/TopNavLayout.vue +2 -2
- package/src/layouts/TopNavMenu.vue +1 -1
- package/src/pages/AlfakitPage.vue +13 -3
- package/src/standalone/AlfakitComponent.vue +16 -5
- package/src/standalone/runtime.ts +3 -0
- package/src/starter-kit/AlfakitStarterKitRoot.vue +7 -0
- package/src/starter-kit/navigation.ts +12 -1
- package/src/styles.css +2 -1
|
@@ -27,12 +27,12 @@ var Se = {
|
|
|
27
27
|
}, we = {
|
|
28
28
|
key: 1,
|
|
29
29
|
class: "text-destructive ms-2"
|
|
30
|
-
},
|
|
30
|
+
}, Te = [
|
|
31
31
|
"aria-label",
|
|
32
32
|
"aria-rowcount",
|
|
33
33
|
"aria-colcount",
|
|
34
34
|
"aria-busy"
|
|
35
|
-
],
|
|
35
|
+
], Ee = { "aria-rowindex": "1" }, De = ["aria-colindex"], Oe = ["data-index", "aria-rowindex"], j = [
|
|
36
36
|
"id",
|
|
37
37
|
"tabindex",
|
|
38
38
|
"aria-selected",
|
|
@@ -69,13 +69,17 @@ var Se = {
|
|
|
69
69
|
"aria-atomic": "true",
|
|
70
70
|
class: "sr-only",
|
|
71
71
|
"data-slot": "spreadsheet-announcement"
|
|
72
|
-
}, Ve = {
|
|
72
|
+
}, Ve = { class: "block" }, He = {
|
|
73
|
+
key: 0,
|
|
74
|
+
class: "block",
|
|
75
|
+
"data-slot": "spreadsheet-paste-skipped"
|
|
76
|
+
}, Ue = {
|
|
73
77
|
"data-slot": "spreadsheet-paste",
|
|
74
78
|
class: "flex flex-col gap-3"
|
|
75
|
-
},
|
|
79
|
+
}, We = {
|
|
76
80
|
key: 0,
|
|
77
81
|
class: "text-muted-foreground flex items-center gap-2 text-sm"
|
|
78
|
-
},
|
|
82
|
+
}, Ge = {
|
|
79
83
|
key: 1,
|
|
80
84
|
class: "text-destructive flex max-h-48 flex-col gap-1 overflow-auto text-sm"
|
|
81
85
|
}, M = /*@__PURE__*/ b({
|
|
@@ -87,9 +91,9 @@ var Se = {
|
|
|
87
91
|
},
|
|
88
92
|
emits: ["close"],
|
|
89
93
|
setup(b, { emit: M }) {
|
|
90
|
-
let N = b,
|
|
94
|
+
let N = b, Ke = M, P = r(), F = e(), qe = t(), Je = n(), I = `spreadsheet-${be()}`, L = p(() => me(N.table)), R = p(() => he(N.table.records ?? [], L.value)), z = new ue(R.value, L.value), B = ee(z.store), V = C(""), H = C(null), U = C(null), W = `${I}-help`;
|
|
91
95
|
D([R, L], ([e, t]) => z.setData(e, t));
|
|
92
|
-
let G = ne(() => R.value.length, () => _e(N.table.virtualize, de.row)), K = p(() =>
|
|
96
|
+
let G = ne(() => R.value.length, () => _e(N.table.virtualize, de.row)), K = p(() => Je.value.endpoints.tableCells), q = p(() => (B.value, z.errorCells()));
|
|
93
97
|
function J(e) {
|
|
94
98
|
e && (V.value = F(e.key, e.count, {}, e.fallback));
|
|
95
99
|
}
|
|
@@ -125,7 +129,7 @@ var Se = {
|
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
|
-
async function
|
|
132
|
+
async function Ye() {
|
|
129
133
|
let e = z.undoSaveChanges();
|
|
130
134
|
if (K.value && e.length !== 0) {
|
|
131
135
|
z.setSaving(!0);
|
|
@@ -137,17 +141,17 @@ var Se = {
|
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
143
|
}
|
|
140
|
-
function
|
|
141
|
-
z.dirtyCount > 0 && !window.confirm(P("table.spreadsheet.discard_confirm", {}, "Discard your unsaved changes?")) || (z.discard(),
|
|
144
|
+
function Xe() {
|
|
145
|
+
z.dirtyCount > 0 && !window.confirm(P("table.spreadsheet.discard_confirm", {}, "Discard your unsaved changes?")) || (z.discard(), Ke("close"));
|
|
142
146
|
}
|
|
143
|
-
function
|
|
144
|
-
let t = z.handleKey(e,
|
|
147
|
+
function Ze(e) {
|
|
148
|
+
let t = z.handleKey(e, qe.value);
|
|
145
149
|
t !== "ignored" && (e.preventDefault(), t === "save" ? X() : t === "undo" ? J(u("undo", z.undo())) : (t === "fill" || t === "clear") && J(u(t, z.lastApplied)), (t === "commit" || t === "cancel" || t === "toggle") && Y(z.state.active.row, z.state.active.col));
|
|
146
150
|
}
|
|
147
|
-
function
|
|
151
|
+
function Qe(e) {
|
|
148
152
|
!z.state.editing && e.clipboardData && (e.preventDefault(), e.clipboardData.setData("text/plain", z.copyText()), J(u("copy", ge(z.bounds()))));
|
|
149
153
|
}
|
|
150
|
-
function
|
|
154
|
+
function $e(e) {
|
|
151
155
|
if (z.state.editing || !e.clipboardData) return;
|
|
152
156
|
e.preventDefault();
|
|
153
157
|
let t = z.planPaste(e.clipboardData.getData("text/plain"));
|
|
@@ -174,13 +178,13 @@ var Se = {
|
|
|
174
178
|
};
|
|
175
179
|
});
|
|
176
180
|
}
|
|
177
|
-
function
|
|
181
|
+
function et() {
|
|
178
182
|
let e = H.value;
|
|
179
183
|
if (!e) return;
|
|
180
184
|
let t = z.apply(e.plan.changes);
|
|
181
185
|
e.errors.length > 0 && z.setErrors(e.errors), J(u("paste", t)), H.value = null;
|
|
182
186
|
}
|
|
183
|
-
function
|
|
187
|
+
function tt(e) {
|
|
184
188
|
let t = le[e.reason];
|
|
185
189
|
return P(t.key, {
|
|
186
190
|
text: e.text,
|
|
@@ -188,7 +192,7 @@ var Se = {
|
|
|
188
192
|
row: e.row + 1
|
|
189
193
|
}, t.fallback);
|
|
190
194
|
}
|
|
191
|
-
function
|
|
195
|
+
function nt(e) {
|
|
192
196
|
let t = z.position(e.record, e.column);
|
|
193
197
|
return P("table.spreadsheet.cell_error", {
|
|
194
198
|
row: (t?.row ?? -1) + 1,
|
|
@@ -203,10 +207,10 @@ var Se = {
|
|
|
203
207
|
function Q(e, t) {
|
|
204
208
|
return B.value.editing?.row === e && B.value.editing.col === t;
|
|
205
209
|
}
|
|
206
|
-
function
|
|
210
|
+
function rt(e, t, n) {
|
|
207
211
|
z.updateDraft(e.target.value), z.commitEdit(), Y(t, n);
|
|
208
212
|
}
|
|
209
|
-
let
|
|
213
|
+
let it = (e) => e.replace(/<[^>]*>/g, ""), $ = { mounted: (e) => e.focus() };
|
|
210
214
|
return (e, t) => (S(), g("div", Se, [
|
|
211
215
|
_("div", Ce, [
|
|
212
216
|
y(i, {
|
|
@@ -234,7 +238,7 @@ var Se = {
|
|
|
234
238
|
size: "sm",
|
|
235
239
|
variant: "outline",
|
|
236
240
|
disabled: E(B).saving,
|
|
237
|
-
onClick:
|
|
241
|
+
onClick: Ye
|
|
238
242
|
}, {
|
|
239
243
|
default: O(() => [v(T(E(P)("table.spreadsheet.undo_save", {}, "Undo last save")), 1)]),
|
|
240
244
|
_: 1
|
|
@@ -255,7 +259,7 @@ var Se = {
|
|
|
255
259
|
type: "button",
|
|
256
260
|
size: "sm",
|
|
257
261
|
variant: "outline",
|
|
258
|
-
onClick:
|
|
262
|
+
onClick: Xe
|
|
259
263
|
}, {
|
|
260
264
|
default: O(() => [v(T(E(P)("table.spreadsheet.close", {}, "Done")), 1)]),
|
|
261
265
|
_: 1
|
|
@@ -281,16 +285,16 @@ var Se = {
|
|
|
281
285
|
"aria-busy": E(B).saving || void 0,
|
|
282
286
|
"data-slot": "spreadsheet-grid",
|
|
283
287
|
class: x(E(s).table),
|
|
284
|
-
onKeydown:
|
|
285
|
-
onCopy:
|
|
286
|
-
onPaste:
|
|
287
|
-
}, [_("thead", null, [_("tr",
|
|
288
|
+
onKeydown: Ze,
|
|
289
|
+
onCopy: Qe,
|
|
290
|
+
onPaste: $e
|
|
291
|
+
}, [_("thead", null, [_("tr", Ee, [(S(!0), g(f, null, w(L.value, (e, t) => (S(), g("th", {
|
|
288
292
|
key: e.name,
|
|
289
293
|
role: "columnheader",
|
|
290
294
|
scope: "col",
|
|
291
295
|
"aria-colindex": t + 1,
|
|
292
296
|
class: x(E(s).header)
|
|
293
|
-
}, T(e.label), 11,
|
|
297
|
+
}, T(e.label), 11, De))), 128))])]), _("tbody", { ref: E(G).attach }, [
|
|
294
298
|
y(o, {
|
|
295
299
|
height: E(G).range.value.before,
|
|
296
300
|
"col-span": L.value.length
|
|
@@ -322,13 +326,13 @@ var Se = {
|
|
|
322
326
|
class: x(E(s).editor),
|
|
323
327
|
"aria-label": n.label,
|
|
324
328
|
value: Z(),
|
|
325
|
-
onChange: (t) =>
|
|
329
|
+
onChange: (t) => rt(t, e, r),
|
|
326
330
|
onBlur: t[2] ||= (e) => E(z).cancelEdit()
|
|
327
331
|
}, [_("option", Ae, T(E(P)("table.spreadsheet.no_value", {}, "—")), 1), (S(!0), g(f, null, w(n.options, (e) => (S(), g("option", {
|
|
328
332
|
key: e.value,
|
|
329
333
|
value: e.value,
|
|
330
334
|
disabled: e.disabled
|
|
331
|
-
}, T(
|
|
335
|
+
}, T(it(e.label)), 9, je))), 128))], 42, ke)), [[$]]) : Q(e, r) ? k((S(), g("input", {
|
|
332
336
|
key: 1,
|
|
333
337
|
type: n.kind === "number" ? "text" : n.inputType ?? "text",
|
|
334
338
|
inputmode: n.kind === "number" ? "decimal" : void 0,
|
|
@@ -344,12 +348,12 @@ var Se = {
|
|
|
344
348
|
}, [E(z).value(e, r) ? (S(), m(E(xe), {
|
|
345
349
|
key: 0,
|
|
346
350
|
class: "size-3"
|
|
347
|
-
})) : h("", !0)], 8, Pe), _("span", Fe, T(E(z).value(e, r) ? E(P)("table.spreadsheet.yes", {}, "Yes") : E(P)("table.spreadsheet.no", {}, "No")), 1)])) : (S(), g(f, { key: 3 }, [v(T(E(z).text(e, r)), 1)], 64)), E(z).errorsAt(e, r).length > 0 ? (S(), g("span", Ie, T(E(z).errorsAt(e, r).join(" ")), 1)) : h("", !0)], 16,
|
|
351
|
+
})) : h("", !0)], 8, Pe), _("span", Fe, T(E(z).value(e, r) ? E(P)("table.spreadsheet.yes", {}, "Yes") : E(P)("table.spreadsheet.no", {}, "No")), 1)])) : (S(), g(f, { key: 3 }, [v(T(E(z).text(e, r)), 1)], 64)), E(z).errorsAt(e, r).length > 0 ? (S(), g("span", Ie, T(E(z).errorsAt(e, r).join(" ")), 1)) : h("", !0)], 16, j))), 128))], 8, Oe)) : h("", !0)], 64))), 128)),
|
|
348
352
|
y(o, {
|
|
349
353
|
height: E(G).range.value.after,
|
|
350
354
|
"col-span": L.value.length
|
|
351
355
|
}, null, 8, ["height", "col-span"])
|
|
352
|
-
], 512)], 42,
|
|
356
|
+
], 512)], 42, Te), R.value.length === 0 ? (S(), g("p", Le, T(E(P)("table.spreadsheet.empty", {}, "No records to edit.")), 1)) : h("", !0)], 2),
|
|
353
357
|
q.value.length > 0 ? (S(), g("ul", Re, [(S(!0), g(f, null, w(q.value, (e) => (S(), g("li", { key: e.key }, [_("button", {
|
|
354
358
|
type: "button",
|
|
355
359
|
class: "text-start hover:underline",
|
|
@@ -373,12 +377,12 @@ var Se = {
|
|
|
373
377
|
default: O(() => [v(T(E(P)("table.spreadsheet.paste_title", {}, "Paste cells")), 1)]),
|
|
374
378
|
_: 1
|
|
375
379
|
}), y(ae, null, {
|
|
376
|
-
default: O(() => [
|
|
380
|
+
default: O(() => [_("span", Ve, T(E(F)("table.spreadsheet.paste_summary", H.value?.plan.changes.length ?? 0, {}, ":count cell will change|:count cells will change")), 1), (H.value?.plan.skipped ?? 0) > 0 ? (S(), g("span", He, T(E(F)("table.spreadsheet.paste_skipped", H.value?.plan.skipped ?? 0, {}, ":count read-only cell is skipped|:count read-only cells are skipped")), 1)) : h("", !0)]),
|
|
377
381
|
_: 1
|
|
378
382
|
})]),
|
|
379
383
|
_: 1
|
|
380
384
|
}),
|
|
381
|
-
_("div",
|
|
385
|
+
_("div", Ue, [H.value?.validating ? (S(), g("p", We, [y(a), v(" " + T(E(P)("table.spreadsheet.paste_validating", {}, "Checking the values…")), 1)])) : h("", !0), H.value && (H.value.plan.issues.length > 0 || H.value.errors.length > 0) ? (S(), g("ul", Ge, [(S(!0), g(f, null, w(H.value.plan.issues, (e) => (S(), g("li", { key: `issue-${e.row}-${e.col}` }, T(tt(e)), 1))), 128)), (S(!0), g(f, null, w(H.value.errors, (e) => (S(), g("li", { key: `error-${String(e.record)}-${e.column}` }, T(nt(e)), 1))), 128))])) : h("", !0)]),
|
|
382
386
|
y(ce, null, {
|
|
383
387
|
default: O(() => [y(i, {
|
|
384
388
|
type: "button",
|
|
@@ -390,7 +394,7 @@ var Se = {
|
|
|
390
394
|
}), y(i, {
|
|
391
395
|
type: "button",
|
|
392
396
|
disabled: !H.value || H.value.plan.changes.length === 0,
|
|
393
|
-
onClick:
|
|
397
|
+
onClick: et
|
|
394
398
|
}, {
|
|
395
399
|
default: O(() => [v(T(E(P)("table.spreadsheet.paste_apply", {}, "Apply")), 1)]),
|
|
396
400
|
_: 1
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Spreadsheet.vue_vue_type_script_setup_true_lang.js","names":["_openBlock","_createElementBlock","_createElementVNode","_createVNode","Button","_unref","_withCtx","_createBlock","Spinner","_createCommentVNode","_createTextVNode","_toDisplayString","_Fragment","_normalizeClass","_renderList","VirtualSpacerRow","_mergeProps","_withDirectives","Dialog","DialogContent","DialogHeader","DialogTitle","DialogDescription","DialogFooter","Button_default","Spinner_default","Dialog_default","DialogContent_default","DialogDescription_default","DialogHeader_default","DialogTitle_default","DialogFooter_default","VirtualSpacerRow_default","_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_hoisted_8","_hoisted_9","_hoisted_10","_hoisted_11","_hoisted_12","_hoisted_13","_hoisted_14","_hoisted_15","_hoisted_16","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","_hoisted_21","_hoisted_22","_hoisted_23","_hoisted_24","Spreadsheet_vue_vue_type_script_setup_true_lang_default","__emit","props","__props","emit","t","useT","choice","useChoice","direction","useDirection","panel","usePanel","baseId","useId","columns","computed","spreadsheetColumns","rows","spreadsheetRows","grid","SpreadsheetGrid","state","useStore","announcement","ref","preview","gridElement","helpId","nextRows","nextColumns","virtual","useVirtualList","virtualizeOptions","VIRTUAL_ESTIMATES","endpoint","errorList","announce","message","focusCell","cell","spreadsheetCellId","row","col","active","editing","save","changes","result","saveSpreadsheetChanges","spreadsheetAnnouncement","inertiaReload","error","undoSave","close","onKeyDown","event","onCopy","spreadsheetSelectionSize","onPaste","plan","url","change","applyPaste","current","count","issueText","SPREADSHEET_ISSUE_MESSAGES","issue","errorText","position","record","column","draft","value","isEditing","onSelectChange","optionText","label","vFocus","element","openBlock","createElementBlock","createElementVNode","createVNode","unref","withCtx","createBlock","createCommentVNode","createTextVNode","toDisplayString","_cache","Fragment","normalizeClass","SPREADSHEET_CLASSES","renderList","mergeProps","spreadsheetAria","spreadsheetCellClass","$event","withDirectives","option","CheckIcon"],"sources":["../../../src/components/table/Spreadsheet.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type {\n PastePlan,\n SpreadsheetAnnouncement,\n SpreadsheetSaveResult,\n TableSchema,\n} from '@alfakit/core'\nimport {\n SPREADSHEET_CLASSES,\n SPREADSHEET_ISSUE_MESSAGES,\n SpreadsheetGrid,\n VIRTUAL_ESTIMATES,\n inertiaReload,\n saveSpreadsheetChanges,\n spreadsheetAnnouncement,\n spreadsheetAria,\n spreadsheetCellClass,\n spreadsheetCellId,\n spreadsheetColumns,\n spreadsheetRows,\n spreadsheetSelectionSize,\n virtualizeOptions,\n} from '@alfakit/core'\nimport { CheckIcon } from '@lucide/vue'\nimport { computed, nextTick, ref, useId, watch } from 'vue'\nimport { useStore } from '../../composables/use-store'\nimport { useVirtualList } from '../../composables/use-virtual-list'\nimport { useChoice, useDirection, usePanel, useT } from '../../context'\nimport { showToast } from '../notifications/flash-toasts'\nimport VirtualSpacerRow from '../schema/VirtualSpacerRow.vue'\nimport Button from '../ui/Button.vue'\nimport Dialog from '../ui/Dialog.vue'\nimport DialogContent from '../ui/DialogContent.vue'\nimport DialogDescription from '../ui/DialogDescription.vue'\nimport DialogFooter from '../ui/DialogFooter.vue'\nimport DialogHeader from '../ui/DialogHeader.vue'\nimport DialogTitle from '../ui/DialogTitle.vue'\nimport Spinner from '../ui/Spinner.vue'\n\n/**\n * Spreadsheet mode of a `Table::spreadsheet()` table (loaded on first use): a keyboard\n * grid over the page's records. Edits stay local (highlighted) until \"Save\" sends them in\n * one request; the server validates each cell with its column's rules and the record's\n * `update` policy, and its errors show on the cells.\n */\nconst props = defineProps<{ table: TableSchema; token: string; component: string }>()\nconst emit = defineEmits<{ close: [] }>()\n\ninterface PastePreview {\n plan: PastePlan\n errors: SpreadsheetSaveResult['errors']\n validating: boolean\n}\n\nconst t = useT()\nconst choice = useChoice()\nconst direction = useDirection()\nconst panel = usePanel()\nconst baseId = `spreadsheet-${useId()}`\nconst columns = computed(() => spreadsheetColumns(props.table))\nconst rows = computed(() => spreadsheetRows(props.table.records ?? [], columns.value))\nconst grid = new SpreadsheetGrid(rows.value, columns.value)\nconst state = useStore(grid.store)\nconst announcement = ref('')\nconst preview = ref<PastePreview | null>(null)\nconst gridElement = ref<HTMLTableElement | null>(null)\nconst helpId = `${baseId}-help`\n\n// A reload (save, live update, polling) brings new records; edits of rows still on the\n// page are kept.\nwatch([rows, columns], ([nextRows, nextColumns]) => grid.setData(nextRows, nextColumns))\n\nconst virtual = useVirtualList(\n () => rows.value.length,\n () => virtualizeOptions(props.table.virtualize, VIRTUAL_ESTIMATES.row),\n)\n\nconst endpoint = computed(() => panel.value.endpoints.tableCells)\nconst errorList = computed(() => {\n void state.value\n\n return grid.errorCells()\n})\n\nfunction announce(message: SpreadsheetAnnouncement | null) {\n if (message) announcement.value = choice(message.key, message.count, {}, message.fallback)\n}\n\nfunction focusCell(row: number, col: number) {\n void nextTick(() => {\n const cell = document.getElementById(spreadsheetCellId(baseId, row, col))\n\n if (cell) {\n cell.focus()\n cell.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })\n } else {\n virtual.list.scrollToIndex(row)\n }\n })\n}\n\n// Keep focus on the active cell (roving tabindex).\nwatch(\n () => [state.value.active, state.value.editing] as const,\n ([active, editing]) => {\n if (editing || !gridElement.value?.contains(document.activeElement)) return\n\n focusCell(active.row, active.col)\n },\n)\n\nasync function save() {\n const changes = grid.changes()\n\n if (!endpoint.value || changes.length === 0 || state.value.saving) return\n\n grid.setSaving(true)\n\n try {\n const result = await saveSpreadsheetChanges(\n endpoint.value,\n props.token,\n props.component,\n changes,\n )\n\n grid.applySaveResult(result)\n\n if (result.saved.length > 0) {\n announce(spreadsheetAnnouncement('saved', result.saved.length))\n inertiaReload()\n }\n\n if (result.errors.length > 0) announce(spreadsheetAnnouncement('failed', result.errors.length))\n } catch (error) {\n showToast({\n id: 'alfakit-spreadsheet-failed',\n title: t('table.spreadsheet.request_failed', {}, \"Couldn't save the changes\"),\n body: error instanceof Error ? error.message : String(error),\n status: 'danger',\n format: 'alfakit',\n })\n } finally {\n grid.setSaving(false)\n }\n}\n\nasync function undoSave() {\n const changes = grid.undoSaveChanges()\n\n if (!endpoint.value || changes.length === 0) return\n\n grid.setSaving(true)\n\n try {\n const result = await saveSpreadsheetChanges(\n endpoint.value,\n props.token,\n props.component,\n changes,\n )\n\n grid.applySaveResult(result)\n announce(spreadsheetAnnouncement('undo', result.saved.length))\n\n if (result.saved.length > 0) inertiaReload()\n } finally {\n grid.setSaving(false)\n }\n}\n\nfunction close() {\n if (\n grid.dirtyCount > 0 &&\n !window.confirm(t('table.spreadsheet.discard_confirm', {}, 'Discard your unsaved changes?'))\n ) {\n return\n }\n\n grid.discard()\n emit('close')\n}\n\nfunction onKeyDown(event: KeyboardEvent) {\n const result = grid.handleKey(event, direction.value)\n\n if (result === 'ignored') return\n\n event.preventDefault()\n\n if (result === 'save') {\n void save()\n } else if (result === 'undo') {\n announce(spreadsheetAnnouncement('undo', grid.undo()))\n } else if (result === 'fill' || result === 'clear') {\n announce(spreadsheetAnnouncement(result, grid.lastApplied))\n }\n\n if (result === 'commit' || result === 'cancel' || result === 'toggle') {\n focusCell(grid.state.active.row, grid.state.active.col)\n }\n}\n\nfunction onCopy(event: ClipboardEvent) {\n if (grid.state.editing || !event.clipboardData) return\n\n event.preventDefault()\n event.clipboardData.setData('text/plain', grid.copyText())\n announce(spreadsheetAnnouncement('copy', spreadsheetSelectionSize(grid.bounds())))\n}\n\nfunction onPaste(event: ClipboardEvent) {\n if (grid.state.editing || !event.clipboardData) return\n\n event.preventDefault()\n\n const plan = grid.planPaste(event.clipboardData.getData('text/plain'))\n\n if (plan.changes.length === 0 && plan.issues.length === 0) return\n\n const url = endpoint.value\n\n preview.value = { plan, errors: [], validating: Boolean(url && plan.changes.length > 0) }\n\n if (!url || plan.changes.length === 0) return\n\n // Validate the pasted values on the server before they are applied.\n saveSpreadsheetChanges(\n url,\n props.token,\n props.component,\n plan.changes.map((change) => ({\n record: grid.getRows()[change.row]?.key ?? '',\n column: grid.getColumns()[change.col]?.name ?? '',\n state: change.value,\n })),\n { dryRun: true },\n )\n .then((result) => {\n if (preview.value)\n preview.value = { ...preview.value, errors: result.errors, validating: false }\n })\n .catch(() => {\n if (preview.value) preview.value = { ...preview.value, validating: false }\n })\n}\n\nfunction applyPaste() {\n const current = preview.value\n\n if (!current) return\n\n const count = grid.apply(current.plan.changes)\n\n if (current.errors.length > 0) grid.setErrors(current.errors)\n\n announce(spreadsheetAnnouncement('paste', count))\n preview.value = null\n}\n\nfunction issueText(issue: PastePlan['issues'][number]): string {\n const message = SPREADSHEET_ISSUE_MESSAGES[issue.reason]\n\n return t(\n message.key,\n { text: issue.text, column: columns.value[issue.col]?.label ?? '', row: issue.row + 1 },\n message.fallback,\n )\n}\n\nfunction errorText(record: SpreadsheetSaveResult['errors'][number]): string {\n const position = grid.position(record.record, record.column)\n\n return t(\n 'table.spreadsheet.cell_error',\n {\n row: (position?.row ?? -1) + 1,\n column: columns.value.find((column) => column.name === record.column)?.label ?? record.column,\n message: record.messages.join(' '),\n },\n 'Row :row, :column: :message',\n )\n}\n\nfunction draft(): string {\n const value = state.value.editing?.value\n\n return value === null || value === undefined ? '' : String(value)\n}\n\nfunction isEditing(row: number, col: number): boolean {\n return state.value.editing?.row === row && state.value.editing.col === col\n}\n\nfunction onSelectChange(event: Event, row: number, col: number) {\n grid.updateDraft((event.target as HTMLSelectElement).value)\n grid.commitEdit()\n focusCell(row, col)\n}\n\nconst optionText = (label: string) => label.replace(/<[^>]*>/g, '')\n\n/** Focus an editor as soon as it is shown. */\nconst vFocus = { mounted: (element: HTMLElement) => element.focus() }\n</script>\n\n<template>\n <div data-slot=\"spreadsheet\" class=\"flex flex-col gap-3\">\n <div data-slot=\"spreadsheet-toolbar\" class=\"flex flex-wrap items-center gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n :disabled=\"grid.dirtyCount === 0 || state.saving\"\n @click=\"save\"\n >\n <Spinner v-if=\"state.saving\" />\n {{ t('table.spreadsheet.save', {}, 'Save changes') }}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n :disabled=\"!grid.canUndo || state.saving\"\n @click=\"announce(spreadsheetAnnouncement('undo', grid.undo()))\"\n >\n {{ t('table.spreadsheet.undo', {}, 'Undo') }}\n </Button>\n <Button\n v-if=\"grid.canUndoSave\"\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n :disabled=\"state.saving\"\n @click=\"undoSave\"\n >\n {{ t('table.spreadsheet.undo_save', {}, 'Undo last save') }}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n :disabled=\"grid.dirtyCount === 0 || state.saving\"\n @click=\"grid.discard()\"\n >\n {{ t('table.spreadsheet.discard', {}, 'Discard') }}\n </Button>\n <span class=\"text-muted-foreground text-sm\" aria-live=\"polite\">\n <template v-if=\"grid.dirtyCount > 0\">{{\n choice(\n 'table.spreadsheet.unsaved',\n grid.dirtyCount,\n {},\n ':count unsaved change|:count unsaved changes',\n )\n }}</template>\n <span v-if=\"grid.errorCount > 0\" class=\"text-destructive ms-2\">{{\n choice('table.spreadsheet.errors', grid.errorCount, {}, ':count error|:count errors')\n }}</span>\n </span>\n <span class=\"flex-1\" />\n <Button type=\"button\" size=\"sm\" variant=\"outline\" @click=\"close\">\n {{ t('table.spreadsheet.close', {}, 'Done') }}\n </Button>\n </div>\n <p :id=\"helpId\" class=\"sr-only\">\n {{\n t(\n 'table.spreadsheet.help',\n {},\n 'Arrow keys move between cells. Enter or F2 edits a cell, Escape cancels, Tab moves to the next cell. Ctrl+C copies, Ctrl+V pastes, Ctrl+D fills down, Ctrl+Z undoes and Ctrl+S saves.',\n )\n }}\n </p>\n <div data-surface=\"table\" :class=\"SPREADSHEET_CLASSES.root\" style=\"max-height: 70vh\">\n <table\n ref=\"gridElement\"\n role=\"grid\"\n :aria-label=\"props.table.heading ?? t('table.spreadsheet.label', {}, 'Spreadsheet')\"\n :aria-describedby=\"helpId\"\n :aria-rowcount=\"rows.length + 1\"\n :aria-colcount=\"columns.length\"\n aria-multiselectable=\"true\"\n :aria-busy=\"state.saving || undefined\"\n data-slot=\"spreadsheet-grid\"\n :class=\"SPREADSHEET_CLASSES.table\"\n @keydown=\"onKeyDown\"\n @copy=\"onCopy\"\n @paste=\"onPaste\"\n >\n <thead>\n <tr aria-rowindex=\"1\">\n <th\n v-for=\"(column, col) in columns\"\n :key=\"column.name\"\n role=\"columnheader\"\n scope=\"col\"\n :aria-colindex=\"col + 1\"\n :class=\"SPREADSHEET_CLASSES.header\"\n >\n {{ column.label }}\n </th>\n </tr>\n </thead>\n <tbody :ref=\"virtual.attach\">\n <VirtualSpacerRow :height=\"virtual.range.value.before\" :col-span=\"columns.length\" />\n <template v-for=\"row in virtual.indexes.value\" :key=\"String(rows[row]?.key ?? row)\">\n <tr v-if=\"rows[row]\" :ref=\"virtual.item\" :data-index=\"row\" :aria-rowindex=\"row + 2\">\n <td\n v-for=\"(column, col) in columns\"\n :id=\"spreadsheetCellId(baseId, row, col)\"\n :key=\"column.name\"\n role=\"gridcell\"\n v-bind=\"spreadsheetAria(row, col)\"\n :tabindex=\"grid.isActive(row, col) ? 0 : -1\"\n :aria-selected=\"grid.isSelected(row, col)\"\n :aria-readonly=\"!grid.isEditable(row, col) || undefined\"\n :aria-invalid=\"grid.errorsAt(row, col).length > 0 || undefined\"\n :title=\"grid.errorsAt(row, col).join(' ') || undefined\"\n data-slot=\"spreadsheet-cell\"\n :data-dirty=\"grid.isDirty(row, col) || undefined\"\n :data-active=\"grid.isActive(row, col) || undefined\"\n :class=\"spreadsheetCellClass(grid, row, col)\"\n @mousedown=\"!isEditing(row, col) && grid.moveTo(row, col, $event.shiftKey)\"\n @dblclick=\"grid.beginEdit()\"\n >\n <select\n v-if=\"isEditing(row, col) && column.kind === 'select'\"\n v-focus\n :class=\"SPREADSHEET_CLASSES.editor\"\n :aria-label=\"column.label\"\n :value=\"draft()\"\n @change=\"onSelectChange($event, row, col)\"\n @blur=\"grid.cancelEdit()\"\n >\n <option value=\"\">{{ t('table.spreadsheet.no_value', {}, '—') }}</option>\n <option\n v-for=\"option in column.options\"\n :key=\"option.value\"\n :value=\"option.value\"\n :disabled=\"option.disabled\"\n >\n {{ optionText(option.label) }}\n </option>\n </select>\n <input\n v-else-if=\"isEditing(row, col)\"\n v-focus\n :type=\"column.kind === 'number' ? 'text' : (column.inputType ?? 'text')\"\n :inputmode=\"column.kind === 'number' ? 'decimal' : undefined\"\n :class=\"SPREADSHEET_CLASSES.editor\"\n :aria-label=\"column.label\"\n :value=\"draft()\"\n @input=\"grid.updateDraft(($event.target as HTMLInputElement).value)\"\n @blur=\"grid.commitEdit()\"\n />\n <span v-else-if=\"column.kind === 'boolean'\" class=\"inline-flex items-center gap-1\">\n <span\n aria-hidden=\"true\"\n class=\"border-input data-[checked=true]:bg-primary data-[checked=true]:text-primary-foreground inline-flex size-4 items-center justify-center rounded-[4px] border\"\n :data-checked=\"Boolean(grid.value(row, col))\"\n >\n <CheckIcon v-if=\"grid.value(row, col)\" class=\"size-3\" />\n </span>\n <span class=\"sr-only\">{{\n grid.value(row, col)\n ? t('table.spreadsheet.yes', {}, 'Yes')\n : t('table.spreadsheet.no', {}, 'No')\n }}</span>\n </span>\n <template v-else>{{ grid.text(row, col) }}</template>\n <span v-if=\"grid.errorsAt(row, col).length > 0\" class=\"sr-only\">{{\n grid.errorsAt(row, col).join(' ')\n }}</span>\n </td>\n </tr>\n </template>\n <VirtualSpacerRow :height=\"virtual.range.value.after\" :col-span=\"columns.length\" />\n </tbody>\n </table>\n <p v-if=\"rows.length === 0\" class=\"text-muted-foreground p-6 text-center text-sm\">\n {{ t('table.spreadsheet.empty', {}, 'No records to edit.') }}\n </p>\n </div>\n <ul\n v-if=\"errorList.length > 0\"\n data-slot=\"spreadsheet-errors\"\n class=\"text-destructive flex flex-col gap-1 text-sm\"\n >\n <li v-for=\"error in errorList\" :key=\"error.key\">\n <button\n type=\"button\"\n class=\"text-start hover:underline\"\n @click=\"\n () => {\n grid.moveTo(error.row, error.col)\n focusCell(error.row, error.col)\n }\n \"\n >\n {{\n t(\n 'table.spreadsheet.cell_error',\n {\n row: error.row + 1,\n column: columns[error.col]?.label ?? '',\n message: error.messages.join(' '),\n },\n 'Row :row, :column: :message',\n )\n }}\n </button>\n </li>\n </ul>\n <span\n aria-live=\"polite\"\n aria-atomic=\"true\"\n class=\"sr-only\"\n data-slot=\"spreadsheet-announcement\"\n >{{ announcement }}</span\n >\n\n <Dialog :open=\"preview !== null\" @update:open=\"(value) => !value && (preview = null)\">\n <DialogContent class=\"sm:max-w-lg\">\n <DialogHeader>\n <DialogTitle>{{ t('table.spreadsheet.paste_title', {}, 'Paste cells') }}</DialogTitle>\n <DialogDescription>\n {{\n choice(\n 'table.spreadsheet.paste_summary',\n preview?.plan.changes.length ?? 0,\n {},\n ':count cell will change|:count cells will change',\n )\n }}\n <template v-if=\"(preview?.plan.skipped ?? 0) > 0\">\n {{\n choice(\n 'table.spreadsheet.paste_skipped',\n preview?.plan.skipped ?? 0,\n {},\n ':count read-only cell is skipped|:count read-only cells are skipped',\n )\n }}\n </template>\n </DialogDescription>\n </DialogHeader>\n <div data-slot=\"spreadsheet-paste\" class=\"flex flex-col gap-3\">\n <p\n v-if=\"preview?.validating\"\n class=\"text-muted-foreground flex items-center gap-2 text-sm\"\n >\n <Spinner />\n {{ t('table.spreadsheet.paste_validating', {}, 'Checking the values…') }}\n </p>\n <ul\n v-if=\"preview && (preview.plan.issues.length > 0 || preview.errors.length > 0)\"\n class=\"text-destructive flex max-h-48 flex-col gap-1 overflow-auto text-sm\"\n >\n <li v-for=\"issue in preview.plan.issues\" :key=\"`issue-${issue.row}-${issue.col}`\">\n {{ issueText(issue) }}\n </li>\n <li\n v-for=\"error in preview.errors\"\n :key=\"`error-${String(error.record)}-${error.column}`\"\n >\n {{ errorText(error) }}\n </li>\n </ul>\n </div>\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" @click=\"preview = null\">{{\n t('cancel', {}, 'Cancel')\n }}</Button>\n <Button\n type=\"button\"\n :disabled=\"!preview || preview.plan.changes.length === 0\"\n @click=\"applyPaste\"\n >\n {{ t('table.spreadsheet.paste_apply', {}, 'Apply') }}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </div>\n</template>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Spreadsheet.vue_vue_type_script_setup_true_lang.js","names":["_openBlock","_createElementBlock","_createElementVNode","_createVNode","Button","_unref","_withCtx","_createBlock","Spinner","_createCommentVNode","_createTextVNode","_toDisplayString","_Fragment","_normalizeClass","_renderList","VirtualSpacerRow","_mergeProps","_withDirectives","Dialog","DialogContent","DialogHeader","DialogTitle","DialogDescription","DialogFooter","Button_default","Spinner_default","Dialog_default","DialogContent_default","DialogDescription_default","DialogHeader_default","DialogTitle_default","DialogFooter_default","VirtualSpacerRow_default","_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_hoisted_8","_hoisted_9","_hoisted_10","_hoisted_11","_hoisted_12","_hoisted_13","_hoisted_14","_hoisted_15","_hoisted_16","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","_hoisted_21","_hoisted_22","_hoisted_23","_hoisted_24","_hoisted_25","_hoisted_26","Spreadsheet_vue_vue_type_script_setup_true_lang_default","__emit","props","__props","emit","t","useT","choice","useChoice","direction","useDirection","panel","usePanel","baseId","useId","columns","computed","spreadsheetColumns","rows","spreadsheetRows","grid","SpreadsheetGrid","state","useStore","announcement","ref","preview","gridElement","helpId","nextRows","nextColumns","virtual","useVirtualList","virtualizeOptions","VIRTUAL_ESTIMATES","endpoint","errorList","announce","message","focusCell","cell","spreadsheetCellId","row","col","active","editing","save","changes","result","saveSpreadsheetChanges","spreadsheetAnnouncement","inertiaReload","error","undoSave","close","onKeyDown","event","onCopy","spreadsheetSelectionSize","onPaste","plan","url","change","applyPaste","current","count","issueText","SPREADSHEET_ISSUE_MESSAGES","issue","errorText","position","record","column","draft","value","isEditing","onSelectChange","optionText","label","vFocus","element","openBlock","createElementBlock","createElementVNode","createVNode","unref","withCtx","createBlock","createCommentVNode","createTextVNode","toDisplayString","_cache","Fragment","normalizeClass","SPREADSHEET_CLASSES","renderList","mergeProps","spreadsheetAria","spreadsheetCellClass","$event","withDirectives","option","CheckIcon"],"sources":["../../../src/components/table/Spreadsheet.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type {\n PastePlan,\n SpreadsheetAnnouncement,\n SpreadsheetSaveResult,\n TableSchema,\n} from '@alfakit/core'\nimport {\n SPREADSHEET_CLASSES,\n SPREADSHEET_ISSUE_MESSAGES,\n SpreadsheetGrid,\n VIRTUAL_ESTIMATES,\n inertiaReload,\n saveSpreadsheetChanges,\n spreadsheetAnnouncement,\n spreadsheetAria,\n spreadsheetCellClass,\n spreadsheetCellId,\n spreadsheetColumns,\n spreadsheetRows,\n spreadsheetSelectionSize,\n virtualizeOptions,\n} from '@alfakit/core'\nimport { CheckIcon } from '@lucide/vue'\nimport { computed, nextTick, ref, useId, watch } from 'vue'\nimport { useStore } from '../../composables/use-store'\nimport { useVirtualList } from '../../composables/use-virtual-list'\nimport { useChoice, useDirection, usePanel, useT } from '../../context'\nimport { showToast } from '../notifications/flash-toasts'\nimport VirtualSpacerRow from '../schema/VirtualSpacerRow.vue'\nimport Button from '../ui/Button.vue'\nimport Dialog from '../ui/Dialog.vue'\nimport DialogContent from '../ui/DialogContent.vue'\nimport DialogDescription from '../ui/DialogDescription.vue'\nimport DialogFooter from '../ui/DialogFooter.vue'\nimport DialogHeader from '../ui/DialogHeader.vue'\nimport DialogTitle from '../ui/DialogTitle.vue'\nimport Spinner from '../ui/Spinner.vue'\n\n/**\n * Spreadsheet mode of a `Table::spreadsheet()` table (loaded on first use): a keyboard\n * grid over the page's records. Edits stay local (highlighted) until \"Save\" sends them in\n * one request; the server validates each cell with its column's rules and the record's\n * `update` policy, and its errors show on the cells.\n */\nconst props = defineProps<{ table: TableSchema; token: string; component: string }>()\nconst emit = defineEmits<{ close: [] }>()\n\ninterface PastePreview {\n plan: PastePlan\n errors: SpreadsheetSaveResult['errors']\n validating: boolean\n}\n\nconst t = useT()\nconst choice = useChoice()\nconst direction = useDirection()\nconst panel = usePanel()\nconst baseId = `spreadsheet-${useId()}`\nconst columns = computed(() => spreadsheetColumns(props.table))\nconst rows = computed(() => spreadsheetRows(props.table.records ?? [], columns.value))\nconst grid = new SpreadsheetGrid(rows.value, columns.value)\nconst state = useStore(grid.store)\nconst announcement = ref('')\nconst preview = ref<PastePreview | null>(null)\nconst gridElement = ref<HTMLTableElement | null>(null)\nconst helpId = `${baseId}-help`\n\n// A reload (save, live update, polling) brings new records; edits of rows still on the\n// page are kept.\nwatch([rows, columns], ([nextRows, nextColumns]) => grid.setData(nextRows, nextColumns))\n\nconst virtual = useVirtualList(\n () => rows.value.length,\n () => virtualizeOptions(props.table.virtualize, VIRTUAL_ESTIMATES.row),\n)\n\nconst endpoint = computed(() => panel.value.endpoints.tableCells)\nconst errorList = computed(() => {\n void state.value\n\n return grid.errorCells()\n})\n\nfunction announce(message: SpreadsheetAnnouncement | null) {\n if (message) announcement.value = choice(message.key, message.count, {}, message.fallback)\n}\n\nfunction focusCell(row: number, col: number) {\n void nextTick(() => {\n const cell = document.getElementById(spreadsheetCellId(baseId, row, col))\n\n if (cell) {\n cell.focus()\n cell.scrollIntoView?.({ block: 'nearest', inline: 'nearest' })\n } else {\n virtual.list.scrollToIndex(row)\n }\n })\n}\n\n// Keep focus on the active cell (roving tabindex).\nwatch(\n () => [state.value.active, state.value.editing] as const,\n ([active, editing]) => {\n if (editing || !gridElement.value?.contains(document.activeElement)) return\n\n focusCell(active.row, active.col)\n },\n)\n\nasync function save() {\n const changes = grid.changes()\n\n if (!endpoint.value || changes.length === 0 || state.value.saving) return\n\n grid.setSaving(true)\n\n try {\n const result = await saveSpreadsheetChanges(\n endpoint.value,\n props.token,\n props.component,\n changes,\n )\n\n grid.applySaveResult(result)\n\n if (result.saved.length > 0) {\n announce(spreadsheetAnnouncement('saved', result.saved.length))\n inertiaReload()\n }\n\n if (result.errors.length > 0) announce(spreadsheetAnnouncement('failed', result.errors.length))\n } catch (error) {\n showToast({\n id: 'alfakit-spreadsheet-failed',\n title: t('table.spreadsheet.request_failed', {}, \"Couldn't save the changes\"),\n body: error instanceof Error ? error.message : String(error),\n status: 'danger',\n format: 'alfakit',\n })\n } finally {\n grid.setSaving(false)\n }\n}\n\nasync function undoSave() {\n const changes = grid.undoSaveChanges()\n\n if (!endpoint.value || changes.length === 0) return\n\n grid.setSaving(true)\n\n try {\n const result = await saveSpreadsheetChanges(\n endpoint.value,\n props.token,\n props.component,\n changes,\n )\n\n grid.applySaveResult(result)\n announce(spreadsheetAnnouncement('undo', result.saved.length))\n\n if (result.saved.length > 0) inertiaReload()\n } finally {\n grid.setSaving(false)\n }\n}\n\nfunction close() {\n if (\n grid.dirtyCount > 0 &&\n !window.confirm(t('table.spreadsheet.discard_confirm', {}, 'Discard your unsaved changes?'))\n ) {\n return\n }\n\n grid.discard()\n emit('close')\n}\n\nfunction onKeyDown(event: KeyboardEvent) {\n const result = grid.handleKey(event, direction.value)\n\n if (result === 'ignored') return\n\n event.preventDefault()\n\n if (result === 'save') {\n void save()\n } else if (result === 'undo') {\n announce(spreadsheetAnnouncement('undo', grid.undo()))\n } else if (result === 'fill' || result === 'clear') {\n announce(spreadsheetAnnouncement(result, grid.lastApplied))\n }\n\n if (result === 'commit' || result === 'cancel' || result === 'toggle') {\n focusCell(grid.state.active.row, grid.state.active.col)\n }\n}\n\nfunction onCopy(event: ClipboardEvent) {\n if (grid.state.editing || !event.clipboardData) return\n\n event.preventDefault()\n event.clipboardData.setData('text/plain', grid.copyText())\n announce(spreadsheetAnnouncement('copy', spreadsheetSelectionSize(grid.bounds())))\n}\n\nfunction onPaste(event: ClipboardEvent) {\n if (grid.state.editing || !event.clipboardData) return\n\n event.preventDefault()\n\n const plan = grid.planPaste(event.clipboardData.getData('text/plain'))\n\n if (plan.changes.length === 0 && plan.issues.length === 0) return\n\n const url = endpoint.value\n\n preview.value = { plan, errors: [], validating: Boolean(url && plan.changes.length > 0) }\n\n if (!url || plan.changes.length === 0) return\n\n // Validate the pasted values on the server before they are applied.\n saveSpreadsheetChanges(\n url,\n props.token,\n props.component,\n plan.changes.map((change) => ({\n record: grid.getRows()[change.row]?.key ?? '',\n column: grid.getColumns()[change.col]?.name ?? '',\n state: change.value,\n })),\n { dryRun: true },\n )\n .then((result) => {\n if (preview.value)\n preview.value = { ...preview.value, errors: result.errors, validating: false }\n })\n .catch(() => {\n if (preview.value) preview.value = { ...preview.value, validating: false }\n })\n}\n\nfunction applyPaste() {\n const current = preview.value\n\n if (!current) return\n\n const count = grid.apply(current.plan.changes)\n\n if (current.errors.length > 0) grid.setErrors(current.errors)\n\n announce(spreadsheetAnnouncement('paste', count))\n preview.value = null\n}\n\nfunction issueText(issue: PastePlan['issues'][number]): string {\n const message = SPREADSHEET_ISSUE_MESSAGES[issue.reason]\n\n return t(\n message.key,\n { text: issue.text, column: columns.value[issue.col]?.label ?? '', row: issue.row + 1 },\n message.fallback,\n )\n}\n\nfunction errorText(record: SpreadsheetSaveResult['errors'][number]): string {\n const position = grid.position(record.record, record.column)\n\n return t(\n 'table.spreadsheet.cell_error',\n {\n row: (position?.row ?? -1) + 1,\n column: columns.value.find((column) => column.name === record.column)?.label ?? record.column,\n message: record.messages.join(' '),\n },\n 'Row :row, :column: :message',\n )\n}\n\nfunction draft(): string {\n const value = state.value.editing?.value\n\n return value === null || value === undefined ? '' : String(value)\n}\n\nfunction isEditing(row: number, col: number): boolean {\n return state.value.editing?.row === row && state.value.editing.col === col\n}\n\nfunction onSelectChange(event: Event, row: number, col: number) {\n grid.updateDraft((event.target as HTMLSelectElement).value)\n grid.commitEdit()\n focusCell(row, col)\n}\n\nconst optionText = (label: string) => label.replace(/<[^>]*>/g, '')\n\n/** Focus an editor as soon as it is shown. */\nconst vFocus = { mounted: (element: HTMLElement) => element.focus() }\n</script>\n\n<template>\n <div data-slot=\"spreadsheet\" class=\"flex flex-col gap-3\">\n <div data-slot=\"spreadsheet-toolbar\" class=\"flex flex-wrap items-center gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n :disabled=\"grid.dirtyCount === 0 || state.saving\"\n @click=\"save\"\n >\n <Spinner v-if=\"state.saving\" />\n {{ t('table.spreadsheet.save', {}, 'Save changes') }}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n :disabled=\"!grid.canUndo || state.saving\"\n @click=\"announce(spreadsheetAnnouncement('undo', grid.undo()))\"\n >\n {{ t('table.spreadsheet.undo', {}, 'Undo') }}\n </Button>\n <Button\n v-if=\"grid.canUndoSave\"\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n :disabled=\"state.saving\"\n @click=\"undoSave\"\n >\n {{ t('table.spreadsheet.undo_save', {}, 'Undo last save') }}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n :disabled=\"grid.dirtyCount === 0 || state.saving\"\n @click=\"grid.discard()\"\n >\n {{ t('table.spreadsheet.discard', {}, 'Discard') }}\n </Button>\n <span class=\"text-muted-foreground text-sm\" aria-live=\"polite\">\n <template v-if=\"grid.dirtyCount > 0\">{{\n choice(\n 'table.spreadsheet.unsaved',\n grid.dirtyCount,\n {},\n ':count unsaved change|:count unsaved changes',\n )\n }}</template>\n <span v-if=\"grid.errorCount > 0\" class=\"text-destructive ms-2\">{{\n choice('table.spreadsheet.errors', grid.errorCount, {}, ':count error|:count errors')\n }}</span>\n </span>\n <span class=\"flex-1\" />\n <Button type=\"button\" size=\"sm\" variant=\"outline\" @click=\"close\">\n {{ t('table.spreadsheet.close', {}, 'Done') }}\n </Button>\n </div>\n <p :id=\"helpId\" class=\"sr-only\">\n {{\n t(\n 'table.spreadsheet.help',\n {},\n 'Arrow keys move between cells. Enter or F2 edits a cell, Escape cancels, Tab moves to the next cell. Ctrl+C copies, Ctrl+V pastes, Ctrl+D fills down, Ctrl+Z undoes and Ctrl+S saves.',\n )\n }}\n </p>\n <div data-surface=\"table\" :class=\"SPREADSHEET_CLASSES.root\" style=\"max-height: 70vh\">\n <table\n ref=\"gridElement\"\n role=\"grid\"\n :aria-label=\"props.table.heading ?? t('table.spreadsheet.label', {}, 'Spreadsheet')\"\n :aria-describedby=\"helpId\"\n :aria-rowcount=\"rows.length + 1\"\n :aria-colcount=\"columns.length\"\n aria-multiselectable=\"true\"\n :aria-busy=\"state.saving || undefined\"\n data-slot=\"spreadsheet-grid\"\n :class=\"SPREADSHEET_CLASSES.table\"\n @keydown=\"onKeyDown\"\n @copy=\"onCopy\"\n @paste=\"onPaste\"\n >\n <thead>\n <tr aria-rowindex=\"1\">\n <th\n v-for=\"(column, col) in columns\"\n :key=\"column.name\"\n role=\"columnheader\"\n scope=\"col\"\n :aria-colindex=\"col + 1\"\n :class=\"SPREADSHEET_CLASSES.header\"\n >\n {{ column.label }}\n </th>\n </tr>\n </thead>\n <tbody :ref=\"virtual.attach\">\n <VirtualSpacerRow :height=\"virtual.range.value.before\" :col-span=\"columns.length\" />\n <template v-for=\"row in virtual.indexes.value\" :key=\"String(rows[row]?.key ?? row)\">\n <tr v-if=\"rows[row]\" :ref=\"virtual.item\" :data-index=\"row\" :aria-rowindex=\"row + 2\">\n <td\n v-for=\"(column, col) in columns\"\n :id=\"spreadsheetCellId(baseId, row, col)\"\n :key=\"column.name\"\n role=\"gridcell\"\n v-bind=\"spreadsheetAria(row, col)\"\n :tabindex=\"grid.isActive(row, col) ? 0 : -1\"\n :aria-selected=\"grid.isSelected(row, col)\"\n :aria-readonly=\"!grid.isEditable(row, col) || undefined\"\n :aria-invalid=\"grid.errorsAt(row, col).length > 0 || undefined\"\n :title=\"grid.errorsAt(row, col).join(' ') || undefined\"\n data-slot=\"spreadsheet-cell\"\n :data-dirty=\"grid.isDirty(row, col) || undefined\"\n :data-active=\"grid.isActive(row, col) || undefined\"\n :class=\"spreadsheetCellClass(grid, row, col)\"\n @mousedown=\"!isEditing(row, col) && grid.moveTo(row, col, $event.shiftKey)\"\n @dblclick=\"grid.beginEdit()\"\n >\n <select\n v-if=\"isEditing(row, col) && column.kind === 'select'\"\n v-focus\n :class=\"SPREADSHEET_CLASSES.editor\"\n :aria-label=\"column.label\"\n :value=\"draft()\"\n @change=\"onSelectChange($event, row, col)\"\n @blur=\"grid.cancelEdit()\"\n >\n <option value=\"\">{{ t('table.spreadsheet.no_value', {}, '—') }}</option>\n <option\n v-for=\"option in column.options\"\n :key=\"option.value\"\n :value=\"option.value\"\n :disabled=\"option.disabled\"\n >\n {{ optionText(option.label) }}\n </option>\n </select>\n <input\n v-else-if=\"isEditing(row, col)\"\n v-focus\n :type=\"column.kind === 'number' ? 'text' : (column.inputType ?? 'text')\"\n :inputmode=\"column.kind === 'number' ? 'decimal' : undefined\"\n :class=\"SPREADSHEET_CLASSES.editor\"\n :aria-label=\"column.label\"\n :value=\"draft()\"\n @input=\"grid.updateDraft(($event.target as HTMLInputElement).value)\"\n @blur=\"grid.commitEdit()\"\n />\n <span v-else-if=\"column.kind === 'boolean'\" class=\"inline-flex items-center gap-1\">\n <span\n aria-hidden=\"true\"\n class=\"border-input data-[checked=true]:bg-primary data-[checked=true]:text-primary-foreground inline-flex size-4 items-center justify-center rounded-[4px] border\"\n :data-checked=\"Boolean(grid.value(row, col))\"\n >\n <CheckIcon v-if=\"grid.value(row, col)\" class=\"size-3\" />\n </span>\n <span class=\"sr-only\">{{\n grid.value(row, col)\n ? t('table.spreadsheet.yes', {}, 'Yes')\n : t('table.spreadsheet.no', {}, 'No')\n }}</span>\n </span>\n <template v-else>{{ grid.text(row, col) }}</template>\n <span v-if=\"grid.errorsAt(row, col).length > 0\" class=\"sr-only\">{{\n grid.errorsAt(row, col).join(' ')\n }}</span>\n </td>\n </tr>\n </template>\n <VirtualSpacerRow :height=\"virtual.range.value.after\" :col-span=\"columns.length\" />\n </tbody>\n </table>\n <p v-if=\"rows.length === 0\" class=\"text-muted-foreground p-6 text-center text-sm\">\n {{ t('table.spreadsheet.empty', {}, 'No records to edit.') }}\n </p>\n </div>\n <ul\n v-if=\"errorList.length > 0\"\n data-slot=\"spreadsheet-errors\"\n class=\"text-destructive flex flex-col gap-1 text-sm\"\n >\n <li v-for=\"error in errorList\" :key=\"error.key\">\n <button\n type=\"button\"\n class=\"text-start hover:underline\"\n @click=\"\n () => {\n grid.moveTo(error.row, error.col)\n focusCell(error.row, error.col)\n }\n \"\n >\n {{\n t(\n 'table.spreadsheet.cell_error',\n {\n row: error.row + 1,\n column: columns[error.col]?.label ?? '',\n message: error.messages.join(' '),\n },\n 'Row :row, :column: :message',\n )\n }}\n </button>\n </li>\n </ul>\n <span\n aria-live=\"polite\"\n aria-atomic=\"true\"\n class=\"sr-only\"\n data-slot=\"spreadsheet-announcement\"\n >{{ announcement }}</span\n >\n\n <Dialog :open=\"preview !== null\" @update:open=\"(value) => !value && (preview = null)\">\n <DialogContent class=\"sm:max-w-lg\">\n <DialogHeader>\n <DialogTitle>{{ t('table.spreadsheet.paste_title', {}, 'Paste cells') }}</DialogTitle>\n <DialogDescription>\n <!-- One line per count: translations can't join two sentences. -->\n <span class=\"block\">{{\n choice(\n 'table.spreadsheet.paste_summary',\n preview?.plan.changes.length ?? 0,\n {},\n ':count cell will change|:count cells will change',\n )\n }}</span>\n <span\n v-if=\"(preview?.plan.skipped ?? 0) > 0\"\n class=\"block\"\n data-slot=\"spreadsheet-paste-skipped\"\n >\n {{\n choice(\n 'table.spreadsheet.paste_skipped',\n preview?.plan.skipped ?? 0,\n {},\n ':count read-only cell is skipped|:count read-only cells are skipped',\n )\n }}\n </span>\n </DialogDescription>\n </DialogHeader>\n <div data-slot=\"spreadsheet-paste\" class=\"flex flex-col gap-3\">\n <p\n v-if=\"preview?.validating\"\n class=\"text-muted-foreground flex items-center gap-2 text-sm\"\n >\n <Spinner />\n {{ t('table.spreadsheet.paste_validating', {}, 'Checking the values…') }}\n </p>\n <ul\n v-if=\"preview && (preview.plan.issues.length > 0 || preview.errors.length > 0)\"\n class=\"text-destructive flex max-h-48 flex-col gap-1 overflow-auto text-sm\"\n >\n <li v-for=\"issue in preview.plan.issues\" :key=\"`issue-${issue.row}-${issue.col}`\">\n {{ issueText(issue) }}\n </li>\n <li\n v-for=\"error in preview.errors\"\n :key=\"`error-${String(error.record)}-${error.column}`\"\n >\n {{ errorText(error) }}\n </li>\n </ul>\n </div>\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" @click=\"preview = null\">{{\n t('cancel', {}, 'Cancel')\n }}</Button>\n <Button\n type=\"button\"\n :disabled=\"!preview || preview.plan.changes.length === 0\"\n @click=\"applyPaste\"\n >\n {{ t('table.spreadsheet.paste_apply', {}, 'Apply') }}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </div>\n</template>\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TranslationManagerView.js","names":["_sfc_main","TranslationManagerView_vue_vue_type_script_setup_true_lang_default","TranslationManagerView_default"],"sources":["../../../src/components/translation-manager/TranslationManagerView.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type {\n TranslationCellRef,\n TranslationFilter,\n TranslationManagerEnvironment,\n TranslationManagerSchema,\n} from '@alfakit/core'\nimport { TranslationManagerController, clientErrorNotification } from '@alfakit/core'\nimport {\n CheckIcon,\n ChevronLeftIcon,\n ChevronRightIcon,\n CopyIcon,\n HistoryIcon,\n LanguagesIcon,\n SearchIcon,\n SparklesIcon,\n TriangleAlertIcon,\n XIcon,\n} from '@lucide/vue'\nimport { computed, nextTick, onBeforeUnmount, toRaw, useId, watch } from 'vue'\nimport { useActionRunner, useDirection, useFormatter, useT } from '../../context'\nimport { useStore } from '../../composables/use-store'\nimport { cn } from '../../lib/utils'\nimport { usePageActionContext } from '../actions/action-context'\nimport { showToast } from '../notifications/flash-toasts'\nimport Badge from '../ui/Badge.vue'\nimport Button from '../ui/Button.vue'\nimport Card from '../ui/Card.vue'\nimport CardContent from '../ui/CardContent.vue'\nimport CardHeader from '../ui/CardHeader.vue'\nimport DropdownMenu from '../ui/DropdownMenu.vue'\nimport DropdownMenuCheckboxItem from '../ui/DropdownMenuCheckboxItem.vue'\nimport DropdownMenuContent from '../ui/DropdownMenuContent.vue'\nimport DropdownMenuTrigger from '../ui/DropdownMenuTrigger.vue'\nimport Input from '../ui/Input.vue'\nimport Textarea from '../ui/Textarea.vue'\n\n/**\n * Keys by locale with search, filters, inline editing (plural forms, right-to-left\n * inputs), keyboard navigation, AI suggestion review and history. Everything is computed\n * and saved by the server.\n */\nconst props = defineProps<{ schema: TranslationManagerSchema }>()\n\nconst SELECT =\n 'border-input focus-visible:border-ring focus-visible:ring-ring/50 h-9 min-w-0 rounded-md border bg-transparent px-2 text-sm shadow-xs outline-none focus-visible:ring-[3px]'\n\nconst runner = useActionRunner()\nconst page = usePageActionContext()\nconst t = useT()\nconst direction = useDirection()\nconst formatter = useFormatter()\nconst labelId = useId()\nconst hintId = useId()\nconst errorId = useId()\n\nconst environment = (): TranslationManagerEnvironment => ({\n runner,\n token: page.token,\n t,\n notify: (notifications) => notifications.forEach((notification) => showToast(notification)),\n error: (message) => showToast(clientErrorNotification('translation-manager', message)),\n})\n\nconst controller = new TranslationManagerController(toRaw(props.schema), environment())\nconst state = useStore(controller.store)\nconst cells = new Map<string, HTMLElement>()\nconst cellKey = (ref: TranslationCellRef) => `${ref.row}\\u0000${ref.locale}`\n\nwatch(\n () => props.schema,\n (schema) => controller.setSchema(toRaw(schema)),\n)\n\nonBeforeUnmount(() => controller.dispose())\n\nconst locales = computed(() => {\n void state.value.visibleLocales\n\n return controller.visibleLocales()\n})\nconst active = computed<TranslationCellRef | null>(() => {\n const current = state.value.active\n\n if (current && state.value.rows.some((row) => row.id === current.row)) return current\n\n const row = state.value.rows[0]\n const locale = locales.value[0]\n\n return row && locale ? { row: row.id, locale: locale.code } : null\n})\nconst range = computed(() => {\n void state.value.rows\n\n return controller.range()\n})\nconst suggestionLocales = computed(() => {\n void state.value.rows\n\n return controller.suggestionLocales()\n})\nconst activeRow = computed(() =>\n active.value ? state.value.rows.find((row) => row.id === active.value?.row) : undefined,\n)\nconst activeLocale = computed(() =>\n active.value ? controller.locale(active.value.locale) : undefined,\n)\nconst activeCell = computed(() =>\n activeRow.value && activeLocale.value\n ? activeRow.value.cells[activeLocale.value.code]\n : undefined,\n)\n\nconst isActive = (ref: TranslationCellRef) =>\n active.value?.row === ref.row && active.value.locale === ref.locale\nconst isEditing = (ref: TranslationCellRef) =>\n state.value.editing?.row === ref.row && state.value.editing.locale === ref.locale\nconst editable = (ref: TranslationCellRef) => {\n void state.value.rows\n\n return controller.isEditable(ref)\n}\n\nconst setCell = (ref: TranslationCellRef) => (element: unknown) => {\n if (element instanceof HTMLElement) cells.set(cellKey(ref), element)\n else cells.delete(cellKey(ref))\n}\n\nconst focusCell = (ref: TranslationCellRef | undefined) => {\n if (!ref) return\n controller.focus(ref)\n void nextTick(() => cells.get(cellKey(ref))?.focus())\n}\n\nwatch(\n () => state.value.editing,\n (editing) => {\n if (editing) {\n void nextTick(() => cells.get(cellKey(editing))?.querySelector('textarea')?.focus())\n }\n },\n)\n\nconst onCellKeyDown = (ref: TranslationCellRef, event: KeyboardEvent) => {\n if (event.target !== event.currentTarget) return\n\n const result = controller.handleCellKey(ref, event, direction.value)\n\n if (result.handled) {\n event.preventDefault()\n if (!controller.state.editing) focusCell(result.focus)\n }\n}\n\nconst onEditorKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape' || (event.key === 'Enter' && !event.shiftKey) || event.key === 'Tab') {\n event.preventDefault()\n void controller\n .handleEditorKey({ key: event.key, shiftKey: event.shiftKey, isComposing: event.isComposing })\n .then((result) => focusCell(result.focus))\n }\n}\n\nconst onCellClick = (ref: TranslationCellRef) => {\n if (isEditing(ref)) return\n const wasActive = isActive(ref)\n controller.focus(ref)\n if (wasActive) controller.startEdit(ref)\n}\n\nconst save = (ref: TranslationCellRef) => {\n void controller.save().then((saved) => saved && focusCell(ref))\n}\n\nconst cancel = (ref: TranslationCellRef) => {\n controller.cancelEdit()\n focusCell(ref)\n}\n\nconst editLabel = (key: string, locale: string) =>\n t('translation-manager.edit', { key, locale }, 'Edit :key in :locale')\n</script>\n\n<template>\n <Card class=\"gap-4 py-4\" data-slot=\"translation-manager\">\n <CardHeader class=\"flex flex-col gap-4 px-5\">\n <span :id=\"labelId\" class=\"sr-only\">{{\n t('translation-manager.label', {}, 'Translations')\n }}</span>\n <ul\n class=\"grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-6\"\n data-slot=\"translation-progress\"\n >\n <li\n v-for=\"locale in schema.locales\"\n :key=\"locale.code\"\n class=\"flex min-w-0 flex-col gap-1 rounded-md border px-3 py-2\"\n >\n <div class=\"flex items-center justify-between gap-2 text-sm\">\n <span class=\"truncate font-medium\" :lang=\"locale.code\">{{ locale.label }}</span>\n <span class=\"text-muted-foreground font-mono text-xs\">{{ locale.code }}</span>\n </div>\n <div\n role=\"progressbar\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n :aria-valuenow=\"Math.round((state.progress[locale.code]?.percent ?? 0) * 100)\"\n :aria-label=\"\n t(\n 'translation-manager.progress_label',\n {\n locale: locale.label,\n translated: state.progress[locale.code]?.translated ?? 0,\n total: state.progress[locale.code]?.total ?? 0,\n },\n ':locale: :translated of :total translated',\n )\n \"\n class=\"bg-muted h-1.5 overflow-hidden rounded-full\"\n >\n <div\n class=\"bg-primary h-full rounded-full transition-[width]\"\n :style=\"{ width: `${(state.progress[locale.code]?.percent ?? 0) * 100}%` }\"\n />\n </div>\n <span class=\"text-muted-foreground text-xs\">\n {{\n t(\n 'translation-manager.progress',\n { percent: formatter.percent(state.progress[locale.code]?.percent ?? 0) },\n ':percent translated',\n )\n }}\n </span>\n </li>\n </ul>\n <div class=\"flex flex-wrap items-center gap-2\" data-slot=\"translation-toolbar\">\n <div class=\"relative min-w-48 flex-1\">\n <SearchIcon\n class=\"text-muted-foreground pointer-events-none absolute start-2.5 top-1/2 size-4 -translate-y-1/2\"\n aria-hidden=\"true\"\n />\n <Input\n type=\"search\"\n :model-value=\"state.search\"\n :placeholder=\"t('translation-manager.search', {}, 'Search keys and texts…')\"\n :aria-label=\"t('translation-manager.search_label', {}, 'Search translations')\"\n class=\"ps-8\"\n @update:model-value=\"(value: string) => controller.setSearch(value)\"\n />\n </div>\n <label class=\"flex items-center gap-2 text-sm\">\n <span class=\"sr-only\">{{ t('translation-manager.filter_label', {}, 'Show') }}</span>\n <select\n :class=\"SELECT\"\n :value=\"state.filter\"\n @change=\"\n controller.setFilter(($event.target as HTMLSelectElement).value as TranslationFilter)\n \"\n >\n <option v-for=\"filter in schema.filters\" :key=\"filter\" :value=\"filter\">\n {{ t(`translation-manager.filters.${filter}`, {}, filter) }}\n </option>\n </select>\n </label>\n <label class=\"flex items-center gap-2 text-sm\">\n <span class=\"sr-only\">{{ t('translation-manager.group_label', {}, 'Group') }}</span>\n <select\n :class=\"SELECT\"\n :value=\"state.group\"\n @change=\"controller.setGroup(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"\">{{ t('translation-manager.all_groups', {}, 'All groups') }}</option>\n <option v-for=\"group in schema.groups\" :key=\"group.value\" :value=\"group.value\">\n {{ group.label }}\n </option>\n </select>\n </label>\n <DropdownMenu :dir=\"direction\">\n <DropdownMenuTrigger as-child>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" class=\"h-9\">\n <LanguagesIcon aria-hidden=\"true\" />\n {{ t('translation-manager.locales_label', {}, 'Languages') }}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuCheckboxItem\n v-for=\"locale in schema.locales\"\n :key=\"locale.code\"\n :model-value=\"state.visibleLocales.includes(locale.code)\"\n @update:model-value=\"\n (checked: boolean) => controller.toggleLocale(locale.code, checked)\n \"\n @select=\"(event: Event) => event.preventDefault()\"\n >\n {{ locale.label }}\n <span class=\"text-muted-foreground ms-auto font-mono text-xs\">{{ locale.code }}</span>\n </DropdownMenuCheckboxItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n <div\n v-if=\"suggestionLocales.length > 0\"\n class=\"bg-muted/50 flex flex-col gap-2 rounded-md border px-3 py-2 text-sm\"\n data-slot=\"translation-suggestions\"\n >\n <div\n v-for=\"code in suggestionLocales\"\n :key=\"code\"\n class=\"flex flex-wrap items-center gap-2\"\n >\n <SparklesIcon class=\"text-primary size-4\" aria-hidden=\"true\" />\n <span class=\"flex-1 font-medium\">\n {{ t('translation-manager.suggestion', {}, 'AI suggestion') }} ·\n {{ controller.locale(code)?.label }}\n </span>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n @click=\"controller.decideAll(code, 'reject')\"\n >\n {{ t('translation-manager.reject_all', {}, 'Reject all') }}\n </Button>\n <Button type=\"button\" size=\"sm\" @click=\"controller.decideAll(code, 'accept')\">\n {{ t('translation-manager.accept_all', {}, 'Accept all without warnings') }}\n </Button>\n </div>\n </div>\n </CardHeader>\n <CardContent class=\"flex flex-col gap-4 px-5\">\n <p :id=\"hintId\" class=\"text-muted-foreground text-xs\">\n {{\n t(\n 'translation-manager.keyboard',\n {},\n 'Arrow keys move between texts. Enter edits and saves, Shift + Enter adds a line break, Escape cancels.',\n )\n }}\n </p>\n <p v-if=\"state.rows.length === 0\" class=\"text-muted-foreground py-10 text-center text-sm\">\n {{ t('translation-manager.empty', {}, 'No translations match your search.') }}\n </p>\n <div v-else class=\"overflow-x-auto rounded-md border\" data-slot=\"translation-grid-scroll\">\n <table\n role=\"grid\"\n :aria-labelledby=\"labelId\"\n :aria-describedby=\"hintId\"\n :aria-busy=\"state.loading || undefined\"\n :aria-rowcount=\"state.total + 1\"\n :class=\"cn('w-full border-collapse text-sm', state.loading && 'opacity-60')\"\n data-slot=\"translation-grid\"\n >\n <thead class=\"bg-muted/50\">\n <tr>\n <th\n scope=\"col\"\n class=\"bg-muted sticky start-0 z-10 min-w-56 border-b px-3 py-2 text-start font-medium\"\n >\n {{ t('translation-manager.key', {}, 'Key') }}\n </th>\n <th\n v-for=\"locale in locales\"\n :key=\"locale.code\"\n scope=\"col\"\n class=\"min-w-64 border-s border-b px-3 py-2 text-start font-medium\"\n >\n <span class=\"flex items-center gap-2\">\n <span :lang=\"locale.code\">{{ locale.label }}</span>\n <span class=\"text-muted-foreground font-mono text-xs\">{{ locale.code }}</span>\n <Badge v-if=\"locale.base\" variant=\"secondary\">\n {{ t('translation-manager.base', {}, 'Base') }}\n </Badge>\n <Badge v-if=\"!locale.editable\" variant=\"outline\">\n {{ t('translation-manager.read_only', {}, 'Read only') }}\n </Badge>\n </span>\n </th>\n </tr>\n </thead>\n <tbody>\n <tr\n v-for=\"row in state.rows\"\n :key=\"row.id\"\n class=\"border-b last:border-b-0\"\n data-slot=\"translation-row\"\n >\n <th\n scope=\"row\"\n class=\"bg-card sticky start-0 z-10 px-3 py-2 text-start align-top font-normal\"\n >\n <div class=\"flex flex-col gap-1\">\n <code class=\"font-mono text-xs break-all\">{{ row.key }}</code>\n <div class=\"flex flex-wrap gap-1\">\n <span class=\"text-muted-foreground text-xs\">{{ row.groupLabel }}</span>\n <Badge v-if=\"row.undefined\" variant=\"destructive\">\n {{ t('translation-manager.undefined', {}, 'Not defined') }}\n </Badge>\n <Badge v-if=\"row.unused\" variant=\"outline\">\n {{ t('translation-manager.unused', {}, 'Unused') }}\n </Badge>\n </div>\n </div>\n </th>\n <td\n v-for=\"locale in locales\"\n :key=\"locale.code\"\n :ref=\"setCell({ row: row.id, locale: locale.code })\"\n role=\"gridcell\"\n :tabindex=\"\n isActive({ row: row.id, locale: locale.code }) &&\n !isEditing({ row: row.id, locale: locale.code })\n ? 0\n : -1\n \"\n :aria-selected=\"isActive({ row: row.id, locale: locale.code })\"\n :aria-readonly=\"!editable({ row: row.id, locale: locale.code }) || undefined\"\n :aria-label=\"\n isEditing({ row: row.id, locale: locale.code })\n ? undefined\n : editLabel(row.key, locale.label)\n \"\n data-slot=\"translation-cell\"\n :data-missing=\"row.cells[locale.code]?.value ? undefined : ''\"\n :data-changed=\"row.cells[locale.code]?.changed ? '' : undefined\"\n :data-active=\"isActive({ row: row.id, locale: locale.code }) ? '' : undefined\"\n :class=\"\n cn(\n 'hover:bg-accent/40 border-s px-3 py-2 align-top outline-none',\n isActive({ row: row.id, locale: locale.code }) &&\n 'ring-ring/50 ring-2 ring-inset',\n editable({ row: row.id, locale: locale.code }) && 'cursor-text',\n )\n \"\n @focus=\"\n $event.target === $event.currentTarget &&\n controller.focus({ row: row.id, locale: locale.code })\n \"\n @click=\"onCellClick({ row: row.id, locale: locale.code })\"\n @dblclick=\"controller.startEdit({ row: row.id, locale: locale.code })\"\n @keydown=\"onCellKeyDown({ row: row.id, locale: locale.code }, $event)\"\n >\n <div\n v-if=\"isEditing({ row: row.id, locale: locale.code })\"\n class=\"flex flex-col gap-2\"\n data-slot=\"translation-editor\"\n @click.stop\n >\n <template v-if=\"state.draftForms\">\n <label\n v-for=\"(form, index) in state.draftForms\"\n :key=\"index\"\n class=\"flex flex-col gap-1\"\n >\n <span class=\"text-muted-foreground text-xs\">\n {{\n t(\n 'translation-manager.plural_form',\n {\n numbers:\n row.cells[locale.code]?.formLabels?.[index] ?? String(index + 1),\n },\n 'For :numbers',\n )\n }}\n </span>\n <Textarea\n :model-value=\"form\"\n rows=\"1\"\n :dir=\"locale.direction\"\n :lang=\"locale.code\"\n :aria-label=\"`${editLabel(row.key, locale.label)} (${row.cells[locale.code]?.formLabels?.[index] ?? index + 1})`\"\n :aria-invalid=\"state.error ? true : undefined\"\n :aria-describedby=\"state.error ? errorId : undefined\"\n class=\"min-h-9\"\n @update:model-value=\"\n (value: string) => controller.setDraftForm(index, value)\n \"\n @keydown=\"onEditorKeyDown\"\n />\n </label>\n </template>\n <Textarea\n v-else\n :model-value=\"state.draft\"\n :rows=\"Math.min(8, Math.max(2, state.draft.split('\\n').length))\"\n :dir=\"locale.direction\"\n :lang=\"locale.code\"\n :aria-label=\"editLabel(row.key, locale.label)\"\n :aria-invalid=\"state.error ? true : undefined\"\n :aria-describedby=\"state.error ? errorId : undefined\"\n @update:model-value=\"(value: string) => controller.setDraft(value)\"\n @keydown=\"onEditorKeyDown\"\n />\n <p v-if=\"state.error\" :id=\"errorId\" class=\"text-destructive text-xs\">\n {{ state.error }}\n </p>\n <div class=\"flex flex-wrap items-center gap-1\">\n <Button\n type=\"button\"\n size=\"sm\"\n :disabled=\"state.saving\"\n @click=\"save({ row: row.id, locale: locale.code })\"\n >\n {{\n state.saving\n ? t('translation-manager.saving', {}, 'Saving…')\n : t('translation-manager.save', {}, 'Save')\n }}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n :disabled=\"state.saving\"\n @click=\"cancel({ row: row.id, locale: locale.code })\"\n >\n {{ t('translation-manager.cancel', {}, 'Cancel') }}\n </Button>\n <Button\n v-if=\"!locale.base\"\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n class=\"ms-auto\"\n :disabled=\"state.saving\"\n @click=\"controller.copyBase()\"\n >\n <CopyIcon aria-hidden=\"true\" />\n {{ t('translation-manager.copy_base', {}, 'Copy the base text') }}\n </Button>\n </div>\n </div>\n <div v-else class=\"flex flex-col gap-1\">\n <span\n v-if=\"row.cells[locale.code]?.value\"\n class=\"line-clamp-4 whitespace-pre-wrap\"\n :dir=\"locale.direction\"\n :lang=\"locale.code\"\n >{{ row.cells[locale.code]?.value }}</span\n >\n <span v-else class=\"text-muted-foreground italic\">\n {{ t('translation-manager.missing', {}, 'Missing') }}\n </span>\n <span\n v-if=\"\n row.cells[locale.code]?.changed ||\n row.cells[locale.code]?.warnings ||\n row.cells[locale.code]?.suggestion\n \"\n class=\"flex flex-wrap gap-1\"\n >\n <Badge v-if=\"row.cells[locale.code]?.changed\" variant=\"secondary\">\n {{ t('translation-manager.changed', {}, 'Changed in the panel') }}\n </Badge>\n <Badge\n v-if=\"row.cells[locale.code]?.warnings\"\n variant=\"destructive\"\n :title=\"row.cells[locale.code]?.warnings?.join('\\n')\"\n >\n <TriangleAlertIcon aria-hidden=\"true\" />\n {{ t('translation-manager.warnings', {}, 'Warnings') }}\n </Badge>\n <Badge v-if=\"row.cells[locale.code]?.suggestion\" variant=\"outline\">\n <SparklesIcon aria-hidden=\"true\" />\n {{ t('translation-manager.suggestion', {}, 'AI suggestion') }}\n </Badge>\n </span>\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <div\n class=\"flex flex-wrap items-center justify-between gap-2 text-sm\"\n data-slot=\"translation-pagination\"\n >\n <span class=\"text-muted-foreground\">\n {{\n t(\n 'translation-manager.pagination',\n { ...range, total: formatter.number(state.total) },\n ':from–:to of :total',\n )\n }}\n </span>\n <div class=\"flex items-center gap-1\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n :aria-label=\"t('translation-manager.previous', {}, 'Previous page')\"\n :disabled=\"state.page <= 1 || state.loading\"\n @click=\"controller.goToPage(state.page - 1)\"\n >\n <ChevronLeftIcon class=\"rtl:rotate-180\" aria-hidden=\"true\" />\n </Button>\n <span class=\"px-2 tabular-nums\">\n {{\n t(\n 'translation-manager.page',\n { page: state.page, pages: state.lastPage },\n 'Page :page of :pages',\n )\n }}\n </span>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n :aria-label=\"t('translation-manager.next', {}, 'Next page')\"\n :disabled=\"state.page >= state.lastPage || state.loading\"\n @click=\"controller.goToPage(state.page + 1)\"\n >\n <ChevronRightIcon class=\"rtl:rotate-180\" aria-hidden=\"true\" />\n </Button>\n </div>\n </div>\n <section\n v-if=\"activeRow && activeLocale\"\n class=\"flex flex-col gap-3 rounded-md border p-4 text-sm\"\n data-slot=\"translation-details\"\n :aria-label=\"t('translation-manager.details', {}, 'Details')\"\n >\n <div class=\"flex flex-wrap items-baseline gap-2\">\n <code class=\"font-mono text-xs break-all\">{{ activeRow.key }}</code>\n <span class=\"text-muted-foreground\">· {{ activeLocale.label }}</span>\n </div>\n <div v-if=\"activeRow.placeholders?.length\" class=\"flex flex-wrap items-center gap-1\">\n <span class=\"text-muted-foreground me-1\">\n {{ t('translation-manager.placeholders', {}, 'Placeholders') }}\n </span>\n <Badge\n v-for=\"placeholder in activeRow.placeholders\"\n :key=\"placeholder\"\n variant=\"outline\"\n class=\"font-mono\"\n >\n {{ placeholder }}\n </Badge>\n </div>\n <p v-if=\"activeRow.files?.length\" class=\"text-muted-foreground\">\n {{ t('translation-manager.used_in', {}, 'Used in') }}:\n <span class=\"font-mono text-xs\">{{ activeRow.files.join(', ') }}</span>\n </p>\n <p\n v-for=\"warning in activeCell?.warnings ?? []\"\n :key=\"warning\"\n class=\"text-destructive flex items-start gap-2\"\n >\n <TriangleAlertIcon class=\"mt-0.5 size-4 shrink-0\" aria-hidden=\"true\" />\n {{ warning }}\n </p>\n <div\n v-if=\"activeCell?.suggestion\"\n class=\"bg-muted/50 flex flex-col gap-2 rounded-md p-3\"\n data-slot=\"translation-suggestion\"\n >\n <span class=\"flex items-center gap-2 font-medium\">\n <SparklesIcon class=\"text-primary size-4\" aria-hidden=\"true\" />\n {{ t('translation-manager.suggestion', {}, 'AI suggestion') }}\n </span>\n <p class=\"whitespace-pre-wrap\" :dir=\"activeLocale.direction\" :lang=\"activeLocale.code\">\n {{ activeCell.suggestion.value }}\n </p>\n <p\n v-for=\"warning in activeCell.suggestion.warnings ?? []\"\n :key=\"warning\"\n class=\"text-destructive text-xs\"\n >\n {{ warning }}\n </p>\n <div v-if=\"activeLocale.editable\" class=\"flex gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n @click=\"controller.decide({ row: activeRow.id, locale: activeLocale.code }, 'accept')\"\n >\n <CheckIcon aria-hidden=\"true\" />\n {{ t('translation-manager.accept', {}, 'Accept') }}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n @click=\"controller.decide({ row: activeRow.id, locale: activeLocale.code }, 'reject')\"\n >\n <XIcon aria-hidden=\"true\" />\n {{ t('translation-manager.reject', {}, 'Reject') }}\n </Button>\n </div>\n </div>\n <div\n v-if=\"state.history?.row === activeRow.id && state.history.locale === activeLocale.code\"\n class=\"flex flex-col gap-2\"\n data-slot=\"translation-history\"\n >\n <span class=\"font-medium\">{{ t('translation-manager.history', {}, 'History') }}</span>\n <p v-if=\"state.history.entries?.length === 0\" class=\"text-muted-foreground\">\n {{ t('translation-manager.history_empty', {}, 'No changes yet.') }}\n </p>\n <ol class=\"flex flex-col gap-2\">\n <li\n v-for=\"entry in state.history.entries ?? []\"\n :key=\"entry.id\"\n class=\"border-s-2 ps-3\"\n >\n <span class=\"text-muted-foreground text-xs\">\n {{ t(`translation-manager.sources.${entry.source}`, {}, entry.source) }}\n <template v-if=\"entry.user\">\n · {{ t('translation-manager.by', { name: entry.user }, 'by :name') }}\n </template>\n <template v-if=\"entry.at\"> · {{ formatter.relative(entry.at) }}</template>\n </span>\n <p\n class=\"whitespace-pre-wrap\"\n :dir=\"activeLocale.direction\"\n :lang=\"activeLocale.code\"\n >\n <template v-if=\"entry.new !== null\">{{ entry.new }}</template>\n <em v-else class=\"text-muted-foreground\">\n {{ t('translation-manager.empty_value', {}, 'Empty') }}\n </em>\n </p>\n </li>\n </ol>\n </div>\n <div v-else>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n @click=\"controller.loadHistory({ row: activeRow.id, locale: activeLocale.code })\"\n >\n <HistoryIcon aria-hidden=\"true\" />\n {{ t('translation-manager.show_history', {}, 'Show history') }}\n </Button>\n </div>\n </section>\n <div class=\"sr-only\" role=\"status\" aria-live=\"polite\">{{ state.announcement }}</div>\n </CardContent>\n </Card>\n</template>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"TranslationManagerView.js","names":["_sfc_main","TranslationManagerView_vue_vue_type_script_setup_true_lang_default","TranslationManagerView_default"],"sources":["../../../src/components/translation-manager/TranslationManagerView.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport type {\n TranslationCellRef,\n TranslationFilter,\n TranslationManagerEnvironment,\n TranslationManagerSchema,\n} from '@alfakit/core'\nimport { TranslationManagerController, clientErrorNotification } from '@alfakit/core'\nimport {\n CheckIcon,\n ChevronLeftIcon,\n ChevronRightIcon,\n CopyIcon,\n HistoryIcon,\n LanguagesIcon,\n SearchIcon,\n SparklesIcon,\n TriangleAlertIcon,\n XIcon,\n} from '@lucide/vue'\nimport { computed, nextTick, onBeforeUnmount, toRaw, useId, watch } from 'vue'\nimport { useActionRunner, useDirection, useFormatter, useT } from '../../context'\nimport { useStore } from '../../composables/use-store'\nimport { cn } from '../../lib/utils'\nimport { usePageActionContext } from '../actions/action-context'\nimport { showToast } from '../notifications/flash-toasts'\nimport Badge from '../ui/Badge.vue'\nimport Button from '../ui/Button.vue'\nimport Card from '../ui/Card.vue'\nimport CardContent from '../ui/CardContent.vue'\nimport CardHeader from '../ui/CardHeader.vue'\nimport DropdownMenu from '../ui/DropdownMenu.vue'\nimport DropdownMenuCheckboxItem from '../ui/DropdownMenuCheckboxItem.vue'\nimport DropdownMenuContent from '../ui/DropdownMenuContent.vue'\nimport DropdownMenuTrigger from '../ui/DropdownMenuTrigger.vue'\nimport Input from '../ui/Input.vue'\nimport Textarea from '../ui/Textarea.vue'\n\n/**\n * Keys by locale with search, filters, inline editing (plural forms, right-to-left\n * inputs), keyboard navigation, AI suggestion review and history. Everything is computed\n * and saved by the server.\n */\nconst props = defineProps<{ schema: TranslationManagerSchema }>()\n\nconst SELECT =\n 'border-input focus-visible:border-ring focus-visible:ring-ring/50 h-9 min-w-0 rounded-md border bg-transparent px-2 text-sm shadow-xs outline-none focus-visible:ring-[3px]'\n\nconst runner = useActionRunner()\nconst page = usePageActionContext()\nconst t = useT()\nconst direction = useDirection()\nconst formatter = useFormatter()\nconst labelId = useId()\nconst hintId = useId()\nconst errorId = useId()\n\nconst environment = (): TranslationManagerEnvironment => ({\n runner,\n token: page.token,\n t,\n notify: (notifications) => notifications.forEach((notification) => showToast(notification)),\n error: (message) => showToast(clientErrorNotification('translation-manager', message)),\n})\n\nconst controller = new TranslationManagerController(toRaw(props.schema), environment())\nconst state = useStore(controller.store)\nconst cells = new Map<string, HTMLElement>()\nconst cellKey = (ref: TranslationCellRef) => `${ref.row}\\u0000${ref.locale}`\n\nwatch(\n () => props.schema,\n (schema) => controller.setSchema(toRaw(schema)),\n)\n\nonBeforeUnmount(() => controller.dispose())\n\nconst locales = computed(() => {\n void state.value.visibleLocales\n\n return controller.visibleLocales()\n})\nconst active = computed<TranslationCellRef | null>(() => {\n const current = state.value.active\n\n if (current && state.value.rows.some((row) => row.id === current.row)) return current\n\n const row = state.value.rows[0]\n const locale = locales.value[0]\n\n return row && locale ? { row: row.id, locale: locale.code } : null\n})\nconst range = computed(() => {\n void state.value.rows\n\n return controller.range()\n})\nconst suggestionLocales = computed(() => {\n void state.value.rows\n\n return controller.suggestionLocales()\n})\nconst activeRow = computed(() =>\n active.value ? state.value.rows.find((row) => row.id === active.value?.row) : undefined,\n)\nconst activeLocale = computed(() =>\n active.value ? controller.locale(active.value.locale) : undefined,\n)\nconst activeCell = computed(() =>\n activeRow.value && activeLocale.value\n ? activeRow.value.cells[activeLocale.value.code]\n : undefined,\n)\n\nconst isActive = (ref: TranslationCellRef) =>\n active.value?.row === ref.row && active.value.locale === ref.locale\nconst isEditing = (ref: TranslationCellRef) =>\n state.value.editing?.row === ref.row && state.value.editing.locale === ref.locale\nconst editable = (ref: TranslationCellRef) => {\n void state.value.rows\n\n return controller.isEditable(ref)\n}\n\nconst setCell = (ref: TranslationCellRef) => (element: unknown) => {\n if (element instanceof HTMLElement) cells.set(cellKey(ref), element)\n else cells.delete(cellKey(ref))\n}\n\nconst focusCell = (ref: TranslationCellRef | undefined) => {\n if (!ref) return\n controller.focus(ref)\n void nextTick(() => cells.get(cellKey(ref))?.focus())\n}\n\nwatch(\n () => state.value.editing,\n (editing) => {\n if (editing) {\n void nextTick(() => cells.get(cellKey(editing))?.querySelector('textarea')?.focus())\n }\n },\n)\n\nconst onCellKeyDown = (ref: TranslationCellRef, event: KeyboardEvent) => {\n if (event.target !== event.currentTarget) return\n\n const result = controller.handleCellKey(ref, event, direction.value)\n\n if (result.handled) {\n event.preventDefault()\n if (!controller.state.editing) focusCell(result.focus)\n }\n}\n\nconst onEditorKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape' || (event.key === 'Enter' && !event.shiftKey) || event.key === 'Tab') {\n event.preventDefault()\n void controller\n .handleEditorKey({ key: event.key, shiftKey: event.shiftKey, isComposing: event.isComposing })\n .then((result) => focusCell(result.focus))\n }\n}\n\nconst onCellClick = (ref: TranslationCellRef) => {\n if (isEditing(ref)) return\n const wasActive = isActive(ref)\n controller.focus(ref)\n if (wasActive) controller.startEdit(ref)\n}\n\nconst save = (ref: TranslationCellRef) => {\n void controller.save().then((saved) => saved && focusCell(ref))\n}\n\nconst cancel = (ref: TranslationCellRef) => {\n controller.cancelEdit()\n focusCell(ref)\n}\n\nconst editLabel = (key: string, locale: string) =>\n t('translation-manager.edit', { key, locale }, 'Edit :key in :locale')\n</script>\n\n<template>\n <Card class=\"gap-4 py-4\" data-slot=\"translation-manager\">\n <CardHeader class=\"flex flex-col gap-4 px-5\">\n <span :id=\"labelId\" class=\"sr-only\">{{\n t('translation-manager.label', {}, 'Translations')\n }}</span>\n <ul\n class=\"grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-6\"\n data-slot=\"translation-progress\"\n >\n <li\n v-for=\"locale in schema.locales\"\n :key=\"locale.code\"\n class=\"flex min-w-0 flex-col gap-1 rounded-md border px-3 py-2\"\n >\n <div class=\"flex items-center justify-between gap-2 text-sm\">\n <span class=\"truncate font-medium\" :lang=\"locale.code\">{{ locale.label }}</span>\n <span class=\"text-muted-foreground font-mono text-xs\">{{ locale.code }}</span>\n </div>\n <div\n role=\"progressbar\"\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n :aria-valuenow=\"Math.round((state.progress[locale.code]?.percent ?? 0) * 100)\"\n :aria-label=\"\n t(\n 'translation-manager.progress_label',\n {\n locale: locale.label,\n translated: state.progress[locale.code]?.translated ?? 0,\n total: state.progress[locale.code]?.total ?? 0,\n },\n ':locale: :translated of :total translated',\n )\n \"\n class=\"bg-muted h-1.5 overflow-hidden rounded-full\"\n >\n <div\n class=\"bg-primary h-full rounded-full transition-[width]\"\n :style=\"{ width: `${(state.progress[locale.code]?.percent ?? 0) * 100}%` }\"\n />\n </div>\n <span class=\"text-muted-foreground text-xs\">\n {{\n t(\n 'translation-manager.progress',\n { percent: formatter.percent(state.progress[locale.code]?.percent ?? 0) },\n ':percent translated',\n )\n }}\n </span>\n </li>\n </ul>\n <div class=\"flex flex-wrap items-center gap-2\" data-slot=\"translation-toolbar\">\n <div class=\"relative min-w-48 flex-1\">\n <SearchIcon\n class=\"text-muted-foreground pointer-events-none absolute start-2.5 top-1/2 size-4 -translate-y-1/2\"\n aria-hidden=\"true\"\n />\n <Input\n type=\"search\"\n :model-value=\"state.search\"\n :placeholder=\"t('translation-manager.search', {}, 'Search keys and texts…')\"\n :aria-label=\"t('translation-manager.search_label', {}, 'Search translations')\"\n class=\"ps-8\"\n @update:model-value=\"(value: string) => controller.setSearch(value)\"\n />\n </div>\n <label class=\"flex items-center gap-2 text-sm\">\n <span class=\"sr-only\">{{ t('translation-manager.filter_label', {}, 'Show') }}</span>\n <select\n :class=\"SELECT\"\n :value=\"state.filter\"\n @change=\"\n controller.setFilter(($event.target as HTMLSelectElement).value as TranslationFilter)\n \"\n >\n <option v-for=\"filter in schema.filters\" :key=\"filter\" :value=\"filter\">\n {{ t(`translation-manager.filters.${filter}`, {}, filter) }}\n </option>\n </select>\n </label>\n <label class=\"flex items-center gap-2 text-sm\">\n <span class=\"sr-only\">{{ t('translation-manager.group_label', {}, 'Group') }}</span>\n <select\n :class=\"SELECT\"\n :value=\"state.group\"\n @change=\"controller.setGroup(($event.target as HTMLSelectElement).value)\"\n >\n <option value=\"\">{{ t('translation-manager.all_groups', {}, 'All groups') }}</option>\n <option v-for=\"group in schema.groups\" :key=\"group.value\" :value=\"group.value\">\n {{ group.label }}\n </option>\n </select>\n </label>\n <DropdownMenu :dir=\"direction\">\n <DropdownMenuTrigger as-child>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" class=\"h-9\">\n <LanguagesIcon aria-hidden=\"true\" />\n {{ t('translation-manager.locales_label', {}, 'Languages') }}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuCheckboxItem\n v-for=\"locale in schema.locales\"\n :key=\"locale.code\"\n :model-value=\"state.visibleLocales.includes(locale.code)\"\n @update:model-value=\"\n (checked: boolean) => controller.toggleLocale(locale.code, checked)\n \"\n @select=\"(event: Event) => event.preventDefault()\"\n >\n {{ locale.label }}\n <span class=\"text-muted-foreground ms-auto font-mono text-xs\">{{ locale.code }}</span>\n </DropdownMenuCheckboxItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n <div\n v-if=\"suggestionLocales.length > 0\"\n class=\"bg-muted/50 flex flex-col gap-2 rounded-md border px-3 py-2 text-sm\"\n data-slot=\"translation-suggestions\"\n >\n <div\n v-for=\"code in suggestionLocales\"\n :key=\"code\"\n class=\"flex flex-wrap items-center gap-2\"\n >\n <SparklesIcon class=\"text-primary size-4\" aria-hidden=\"true\" />\n <span class=\"flex-1 font-medium\">\n {{ t('translation-manager.suggestion', {}, 'AI suggestion') }} ·\n {{ controller.locale(code)?.label }}\n </span>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n @click=\"controller.decideAll(code, 'reject')\"\n >\n {{ t('translation-manager.reject_all', {}, 'Reject all') }}\n </Button>\n <Button type=\"button\" size=\"sm\" @click=\"controller.decideAll(code, 'accept')\">\n {{ t('translation-manager.accept_all', {}, 'Accept all without warnings') }}\n </Button>\n </div>\n </div>\n </CardHeader>\n <CardContent class=\"flex flex-col gap-4 px-5\">\n <p :id=\"hintId\" class=\"text-muted-foreground text-xs\">\n {{\n t(\n 'translation-manager.keyboard',\n {},\n 'Arrow keys move between texts. Enter edits and saves, Shift + Enter adds a line break, Escape cancels.',\n )\n }}\n </p>\n <p v-if=\"state.rows.length === 0\" class=\"text-muted-foreground py-10 text-center text-sm\">\n {{ t('translation-manager.empty', {}, 'No translations match your search.') }}\n </p>\n <div v-else class=\"overflow-x-auto rounded-md border\" data-slot=\"translation-grid-scroll\">\n <table\n role=\"grid\"\n :aria-labelledby=\"labelId\"\n :aria-describedby=\"hintId\"\n :aria-busy=\"state.loading || undefined\"\n :aria-rowcount=\"state.total + 1\"\n :class=\"cn('w-full border-collapse text-sm', state.loading && 'opacity-60')\"\n data-slot=\"translation-grid\"\n >\n <thead class=\"bg-muted/50\">\n <tr>\n <th\n scope=\"col\"\n class=\"bg-card sticky start-0 z-10 min-w-56 border-b [background-image:linear-gradient(var(--muted),var(--muted))] px-3 py-2 text-start font-medium\"\n >\n {{ t('translation-manager.key', {}, 'Key') }}\n </th>\n <th\n v-for=\"locale in locales\"\n :key=\"locale.code\"\n scope=\"col\"\n class=\"min-w-64 border-s border-b px-3 py-2 text-start font-medium\"\n >\n <span class=\"flex items-center gap-2\">\n <span :lang=\"locale.code\">{{ locale.label }}</span>\n <span class=\"text-muted-foreground font-mono text-xs\">{{ locale.code }}</span>\n <Badge v-if=\"locale.base\" variant=\"secondary\">\n {{ t('translation-manager.base', {}, 'Base') }}\n </Badge>\n <Badge v-if=\"!locale.editable\" variant=\"outline\">\n {{ t('translation-manager.read_only', {}, 'Read only') }}\n </Badge>\n </span>\n </th>\n </tr>\n </thead>\n <tbody>\n <tr\n v-for=\"row in state.rows\"\n :key=\"row.id\"\n class=\"border-b last:border-b-0\"\n data-slot=\"translation-row\"\n >\n <th\n scope=\"row\"\n class=\"bg-card sticky start-0 z-10 px-3 py-2 text-start align-top font-normal\"\n >\n <div class=\"flex flex-col gap-1\">\n <code class=\"font-mono text-xs break-all\">{{ row.key }}</code>\n <div class=\"flex flex-wrap gap-1\">\n <span class=\"text-muted-foreground text-xs\">{{ row.groupLabel }}</span>\n <Badge v-if=\"row.undefined\" variant=\"destructive\">\n {{ t('translation-manager.undefined', {}, 'Not defined') }}\n </Badge>\n <Badge v-if=\"row.unused\" variant=\"outline\">\n {{ t('translation-manager.unused', {}, 'Unused') }}\n </Badge>\n </div>\n </div>\n </th>\n <td\n v-for=\"locale in locales\"\n :key=\"locale.code\"\n :ref=\"setCell({ row: row.id, locale: locale.code })\"\n role=\"gridcell\"\n :tabindex=\"\n isActive({ row: row.id, locale: locale.code }) &&\n !isEditing({ row: row.id, locale: locale.code })\n ? 0\n : -1\n \"\n :aria-selected=\"isActive({ row: row.id, locale: locale.code })\"\n :aria-readonly=\"!editable({ row: row.id, locale: locale.code }) || undefined\"\n :aria-label=\"\n isEditing({ row: row.id, locale: locale.code })\n ? undefined\n : editLabel(row.key, locale.label)\n \"\n data-slot=\"translation-cell\"\n :data-missing=\"row.cells[locale.code]?.value ? undefined : ''\"\n :data-changed=\"row.cells[locale.code]?.changed ? '' : undefined\"\n :data-active=\"isActive({ row: row.id, locale: locale.code }) ? '' : undefined\"\n :class=\"\n cn(\n 'hover:bg-accent/40 border-s px-3 py-2 align-top outline-none',\n isActive({ row: row.id, locale: locale.code }) &&\n 'ring-ring/50 ring-2 ring-inset',\n editable({ row: row.id, locale: locale.code }) && 'cursor-text',\n )\n \"\n @focus=\"\n $event.target === $event.currentTarget &&\n controller.focus({ row: row.id, locale: locale.code })\n \"\n @click=\"onCellClick({ row: row.id, locale: locale.code })\"\n @dblclick=\"controller.startEdit({ row: row.id, locale: locale.code })\"\n @keydown=\"onCellKeyDown({ row: row.id, locale: locale.code }, $event)\"\n >\n <div\n v-if=\"isEditing({ row: row.id, locale: locale.code })\"\n class=\"flex flex-col gap-2\"\n data-slot=\"translation-editor\"\n @click.stop\n >\n <template v-if=\"state.draftForms\">\n <label\n v-for=\"(form, index) in state.draftForms\"\n :key=\"index\"\n class=\"flex flex-col gap-1\"\n >\n <span class=\"text-muted-foreground text-xs\">\n {{\n t(\n 'translation-manager.plural_form',\n {\n numbers:\n row.cells[locale.code]?.formLabels?.[index] ?? String(index + 1),\n },\n 'For :numbers',\n )\n }}\n </span>\n <Textarea\n :model-value=\"form\"\n rows=\"1\"\n :dir=\"locale.direction\"\n :lang=\"locale.code\"\n :aria-label=\"`${editLabel(row.key, locale.label)} (${row.cells[locale.code]?.formLabels?.[index] ?? index + 1})`\"\n :aria-invalid=\"state.error ? true : undefined\"\n :aria-describedby=\"state.error ? errorId : undefined\"\n class=\"min-h-9\"\n @update:model-value=\"\n (value: string) => controller.setDraftForm(index, value)\n \"\n @keydown=\"onEditorKeyDown\"\n />\n </label>\n </template>\n <Textarea\n v-else\n :model-value=\"state.draft\"\n :rows=\"Math.min(8, Math.max(2, state.draft.split('\\n').length))\"\n :dir=\"locale.direction\"\n :lang=\"locale.code\"\n :aria-label=\"editLabel(row.key, locale.label)\"\n :aria-invalid=\"state.error ? true : undefined\"\n :aria-describedby=\"state.error ? errorId : undefined\"\n @update:model-value=\"(value: string) => controller.setDraft(value)\"\n @keydown=\"onEditorKeyDown\"\n />\n <p v-if=\"state.error\" :id=\"errorId\" class=\"text-destructive text-xs\">\n {{ state.error }}\n </p>\n <div class=\"flex flex-wrap items-center gap-1\">\n <Button\n type=\"button\"\n size=\"sm\"\n :disabled=\"state.saving\"\n @click=\"save({ row: row.id, locale: locale.code })\"\n >\n {{\n state.saving\n ? t('translation-manager.saving', {}, 'Saving…')\n : t('translation-manager.save', {}, 'Save')\n }}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n :disabled=\"state.saving\"\n @click=\"cancel({ row: row.id, locale: locale.code })\"\n >\n {{ t('translation-manager.cancel', {}, 'Cancel') }}\n </Button>\n <Button\n v-if=\"!locale.base\"\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n class=\"ms-auto\"\n :disabled=\"state.saving\"\n @click=\"controller.copyBase()\"\n >\n <CopyIcon aria-hidden=\"true\" />\n {{ t('translation-manager.copy_base', {}, 'Copy the base text') }}\n </Button>\n </div>\n </div>\n <div v-else class=\"flex flex-col gap-1\">\n <span\n v-if=\"row.cells[locale.code]?.value\"\n class=\"line-clamp-4 whitespace-pre-wrap\"\n :dir=\"locale.direction\"\n :lang=\"locale.code\"\n >{{ row.cells[locale.code]?.value }}</span\n >\n <span v-else class=\"text-muted-foreground italic\">\n {{ t('translation-manager.missing', {}, 'Missing') }}\n </span>\n <span\n v-if=\"\n row.cells[locale.code]?.changed ||\n row.cells[locale.code]?.warnings ||\n row.cells[locale.code]?.suggestion\n \"\n class=\"flex flex-wrap gap-1\"\n >\n <Badge v-if=\"row.cells[locale.code]?.changed\" variant=\"secondary\">\n {{ t('translation-manager.changed', {}, 'Changed in the panel') }}\n </Badge>\n <Badge\n v-if=\"row.cells[locale.code]?.warnings\"\n variant=\"destructive\"\n :title=\"row.cells[locale.code]?.warnings?.join('\\n')\"\n >\n <TriangleAlertIcon aria-hidden=\"true\" />\n {{ t('translation-manager.warnings', {}, 'Warnings') }}\n </Badge>\n <Badge v-if=\"row.cells[locale.code]?.suggestion\" variant=\"outline\">\n <SparklesIcon aria-hidden=\"true\" />\n {{ t('translation-manager.suggestion', {}, 'AI suggestion') }}\n </Badge>\n </span>\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n <div\n class=\"flex flex-wrap items-center justify-between gap-2 text-sm\"\n data-slot=\"translation-pagination\"\n >\n <span class=\"text-muted-foreground\">\n {{\n t(\n 'translation-manager.pagination',\n { ...range, total: formatter.number(state.total) },\n ':from–:to of :total',\n )\n }}\n </span>\n <div class=\"flex items-center gap-1\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n :aria-label=\"t('translation-manager.previous', {}, 'Previous page')\"\n :disabled=\"state.page <= 1 || state.loading\"\n @click=\"controller.goToPage(state.page - 1)\"\n >\n <ChevronLeftIcon class=\"rtl:rotate-180\" aria-hidden=\"true\" />\n </Button>\n <span class=\"px-2 tabular-nums\">\n {{\n t(\n 'translation-manager.page',\n { page: state.page, pages: state.lastPage },\n 'Page :page of :pages',\n )\n }}\n </span>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n :aria-label=\"t('translation-manager.next', {}, 'Next page')\"\n :disabled=\"state.page >= state.lastPage || state.loading\"\n @click=\"controller.goToPage(state.page + 1)\"\n >\n <ChevronRightIcon class=\"rtl:rotate-180\" aria-hidden=\"true\" />\n </Button>\n </div>\n </div>\n <section\n v-if=\"activeRow && activeLocale\"\n class=\"flex flex-col gap-3 rounded-md border p-4 text-sm\"\n data-slot=\"translation-details\"\n :aria-label=\"t('translation-manager.details', {}, 'Details')\"\n >\n <div class=\"flex flex-wrap items-baseline gap-2\">\n <code class=\"font-mono text-xs break-all\">{{ activeRow.key }}</code>\n <span class=\"text-muted-foreground\">· {{ activeLocale.label }}</span>\n </div>\n <div v-if=\"activeRow.placeholders?.length\" class=\"flex flex-wrap items-center gap-1\">\n <span class=\"text-muted-foreground me-1\">\n {{ t('translation-manager.placeholders', {}, 'Placeholders') }}\n </span>\n <Badge\n v-for=\"placeholder in activeRow.placeholders\"\n :key=\"placeholder\"\n variant=\"outline\"\n class=\"font-mono\"\n >\n {{ placeholder }}\n </Badge>\n </div>\n <p v-if=\"activeRow.files?.length\" class=\"text-muted-foreground\">\n {{ t('translation-manager.used_in', {}, 'Used in') }}:\n <span class=\"font-mono text-xs\">{{ activeRow.files.join(', ') }}</span>\n </p>\n <p\n v-for=\"warning in activeCell?.warnings ?? []\"\n :key=\"warning\"\n class=\"text-destructive flex items-start gap-2\"\n >\n <TriangleAlertIcon class=\"mt-0.5 size-4 shrink-0\" aria-hidden=\"true\" />\n {{ warning }}\n </p>\n <div\n v-if=\"activeCell?.suggestion\"\n class=\"bg-muted/50 flex flex-col gap-2 rounded-md p-3\"\n data-slot=\"translation-suggestion\"\n >\n <span class=\"flex items-center gap-2 font-medium\">\n <SparklesIcon class=\"text-primary size-4\" aria-hidden=\"true\" />\n {{ t('translation-manager.suggestion', {}, 'AI suggestion') }}\n </span>\n <p class=\"whitespace-pre-wrap\" :dir=\"activeLocale.direction\" :lang=\"activeLocale.code\">\n {{ activeCell.suggestion.value }}\n </p>\n <p\n v-for=\"warning in activeCell.suggestion.warnings ?? []\"\n :key=\"warning\"\n class=\"text-destructive text-xs\"\n >\n {{ warning }}\n </p>\n <div v-if=\"activeLocale.editable\" class=\"flex gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n @click=\"controller.decide({ row: activeRow.id, locale: activeLocale.code }, 'accept')\"\n >\n <CheckIcon aria-hidden=\"true\" />\n {{ t('translation-manager.accept', {}, 'Accept') }}\n </Button>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n @click=\"controller.decide({ row: activeRow.id, locale: activeLocale.code }, 'reject')\"\n >\n <XIcon aria-hidden=\"true\" />\n {{ t('translation-manager.reject', {}, 'Reject') }}\n </Button>\n </div>\n </div>\n <div\n v-if=\"state.history?.row === activeRow.id && state.history.locale === activeLocale.code\"\n class=\"flex flex-col gap-2\"\n data-slot=\"translation-history\"\n >\n <span class=\"font-medium\">{{ t('translation-manager.history', {}, 'History') }}</span>\n <p v-if=\"state.history.entries?.length === 0\" class=\"text-muted-foreground\">\n {{ t('translation-manager.history_empty', {}, 'No changes yet.') }}\n </p>\n <ol class=\"flex flex-col gap-2\">\n <li\n v-for=\"entry in state.history.entries ?? []\"\n :key=\"entry.id\"\n class=\"border-s-2 ps-3\"\n >\n <span class=\"text-muted-foreground text-xs\">\n {{ t(`translation-manager.sources.${entry.source}`, {}, entry.source) }}\n <template v-if=\"entry.user\">\n · {{ t('translation-manager.by', { name: entry.user }, 'by :name') }}\n </template>\n <template v-if=\"entry.at\"> · {{ formatter.relative(entry.at) }}</template>\n </span>\n <p\n class=\"whitespace-pre-wrap\"\n :dir=\"activeLocale.direction\"\n :lang=\"activeLocale.code\"\n >\n <template v-if=\"entry.new !== null\">{{ entry.new }}</template>\n <em v-else class=\"text-muted-foreground\">\n {{ t('translation-manager.empty_value', {}, 'Empty') }}\n </em>\n </p>\n </li>\n </ol>\n </div>\n <div v-else>\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n @click=\"controller.loadHistory({ row: activeRow.id, locale: activeLocale.code })\"\n >\n <HistoryIcon aria-hidden=\"true\" />\n {{ t('translation-manager.show_history', {}, 'Show history') }}\n </Button>\n </div>\n </section>\n <div class=\"sr-only\" role=\"status\" aria-live=\"polite\">{{ state.announcement }}</div>\n </CardContent>\n </Card>\n</template>\n"],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TranslationManagerView.vue.d.ts","sourceRoot":"","sources":["../../../src/components/translation-manager/TranslationManagerView.vue"],"names":[],"mappings":"AA2uBA,OAAO,KAAK,EAIV,wBAAwB,EACzB,MAAM,eAAe,CAAA;AAgCtB;;;;GAIG;AACH,KAAK,WAAW,GAAG;IAAE,MAAM,EAAE,wBAAwB,CAAA;CAAE,CAAC;;
|
|
1
|
+
{"version":3,"file":"TranslationManagerView.vue.d.ts","sourceRoot":"","sources":["../../../src/components/translation-manager/TranslationManagerView.vue"],"names":[],"mappings":"AA2uBA,OAAO,KAAK,EAIV,wBAAwB,EACzB,MAAM,eAAe,CAAA;AAgCtB;;;;GAIG;AACH,KAAK,WAAW,GAAG;IAAE,MAAM,EAAE,wBAAwB,CAAA;CAAE,CAAC;;AA6oDxD,wBAMG"}
|
|
@@ -42,7 +42,7 @@ var Ce = ["id"], we = {
|
|
|
42
42
|
"aria-rowcount"
|
|
43
43
|
], Je = { class: "bg-muted/50" }, Ye = {
|
|
44
44
|
scope: "col",
|
|
45
|
-
class: "bg-
|
|
45
|
+
class: "bg-card sticky start-0 z-10 min-w-56 border-b [background-image:linear-gradient(var(--muted),var(--muted))] px-3 py-2 text-start font-medium"
|
|
46
46
|
}, Xe = { class: "flex items-center gap-2" }, Ze = ["lang"], Qe = { class: "text-muted-foreground font-mono text-xs" }, $e = {
|
|
47
47
|
scope: "row",
|
|
48
48
|
class: "bg-card sticky start-0 z-10 px-3 py-2 text-start align-top font-normal"
|