@revenexx/studio-integrations 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/CHANGELOG.md +428 -0
  2. package/README.md +48 -4
  3. package/dist/module.d.mts +17 -1
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +10 -2
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +43 -41
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +123 -50
  8. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +108 -71
  9. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +12 -15
  10. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +18 -17
  11. package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +1 -0
  12. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +7 -4
  13. package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +1 -0
  14. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +4 -0
  15. package/dist/runtime/components/Integrations/ExpressionInput.vue +67 -37
  16. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +4 -0
  17. package/dist/runtime/components/Integrations/GroupFrameView.vue +30 -20
  18. package/dist/runtime/components/Integrations/Layout.vue +48 -4
  19. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +23 -23
  20. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +11 -13
  21. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +2 -0
  22. package/dist/runtime/components/Integrations/NodeConfigField.vue +187 -93
  23. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +2 -0
  24. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +6 -0
  25. package/dist/runtime/components/Integrations/NodeInspector.vue +1157 -376
  26. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +6 -0
  27. package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +12 -0
  28. package/dist/runtime/components/Integrations/NodeLogo.vue +2 -30
  29. package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +12 -0
  30. package/dist/runtime/components/Integrations/NodeMark.d.vue.ts +15 -0
  31. package/dist/runtime/components/Integrations/NodeMark.vue +79 -0
  32. package/dist/runtime/components/Integrations/NodeMark.vue.d.ts +15 -0
  33. package/dist/runtime/components/Integrations/NodeOutputSchema.d.vue.ts +21 -0
  34. package/dist/runtime/components/Integrations/NodeOutputSchema.vue +43 -0
  35. package/dist/runtime/components/Integrations/NodeOutputSchema.vue.d.ts +21 -0
  36. package/dist/runtime/components/Integrations/NodePalette.vue +122 -55
  37. package/dist/runtime/components/Integrations/QueryBuilder.vue +46 -38
  38. package/dist/runtime/components/Integrations/RunActionsMenu.vue +38 -12
  39. package/dist/runtime/components/Integrations/RunDetailPanel.vue +114 -62
  40. package/dist/runtime/components/Integrations/RunPayloadInput.vue +27 -13
  41. package/dist/runtime/components/Integrations/RunResponseModal.vue +11 -10
  42. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +0 -1
  43. package/dist/runtime/components/Integrations/RunsTable.vue +38 -16
  44. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +0 -1
  45. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +8 -7
  46. package/dist/runtime/components/Integrations/Sidebar.vue +14 -10
  47. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +14 -4
  48. package/dist/runtime/components/Integrations/StickyNoteView.vue +38 -23
  49. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +14 -4
  50. package/dist/runtime/components/Integrations/TemplateCard.d.vue.ts +18 -0
  51. package/dist/runtime/components/Integrations/TemplateCard.vue +119 -0
  52. package/dist/runtime/components/Integrations/TemplateCard.vue.d.ts +18 -0
  53. package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +2 -2
  54. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +16 -18
  55. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +2 -2
  56. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +34 -33
  57. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +52 -67
  58. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +8 -7
  59. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +18 -4
  60. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +29 -11
  61. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +18 -4
  62. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +20 -1
  63. package/dist/runtime/components/Integrations/WorkflowEditor.vue +240 -116
  64. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +20 -1
  65. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +28 -43
  66. package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +19 -1
  67. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +17 -7
  68. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +19 -1
  69. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +4 -0
  70. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +198 -56
  71. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +4 -0
  72. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +56 -59
  73. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +28 -21
  74. package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +1 -0
  75. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +15 -9
  76. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +1 -0
  77. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +15 -10
  78. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +50 -36
  79. package/dist/runtime/components/Integrations/WorkflowsTable.vue +126 -48
  80. package/dist/runtime/composables/integrationsApi.js +4 -2
  81. package/dist/runtime/composables/useCredentialTypes.js +8 -6
  82. package/dist/runtime/composables/useIntegrationsAudit.js +4 -3
  83. package/dist/runtime/composables/useIntegrationsCredentials.js +10 -8
  84. package/dist/runtime/composables/useIntegrationsDeadLetters.js +7 -6
  85. package/dist/runtime/composables/useIntegrationsFocus.d.ts +12 -0
  86. package/dist/runtime/composables/useIntegrationsLocale.d.ts +26 -0
  87. package/dist/runtime/composables/useIntegrationsLocale.js +8 -0
  88. package/dist/runtime/composables/useIntegrationsNodes.js +7 -5
  89. package/dist/runtime/composables/useIntegrationsRealtime.d.ts +149 -0
  90. package/dist/runtime/composables/useIntegrationsRealtime.js +80 -0
  91. package/dist/runtime/composables/useIntegrationsRevisions.js +9 -7
  92. package/dist/runtime/composables/useIntegrationsRuns.d.ts +1 -1
  93. package/dist/runtime/composables/useIntegrationsRuns.js +14 -7
  94. package/dist/runtime/composables/useIntegrationsSchedules.d.ts +4 -0
  95. package/dist/runtime/composables/useIntegrationsSchedules.js +10 -3
  96. package/dist/runtime/composables/useIntegrationsSecrets.js +14 -9
  97. package/dist/runtime/composables/useIntegrationsT.d.ts +35 -0
  98. package/dist/runtime/composables/useIntegrationsT.js +9 -0
  99. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +3 -3
  100. package/dist/runtime/composables/useIntegrationsTemplates.js +28 -16
  101. package/dist/runtime/composables/useIntegrationsTriggers.js +10 -8
  102. package/dist/runtime/composables/useIntegrationsWorkflows.js +13 -7
  103. package/dist/runtime/composables/useLoadGate.d.ts +17 -0
  104. package/dist/runtime/composables/useLoadGate.js +8 -0
  105. package/dist/runtime/composables/useNodeConfigResolve.js +2 -1
  106. package/dist/runtime/data/branchReorder.d.ts +37 -0
  107. package/dist/runtime/data/branchReorder.js +71 -0
  108. package/dist/runtime/data/handleId.d.ts +23 -0
  109. package/dist/runtime/data/handleId.js +6 -0
  110. package/dist/runtime/data/integrationNodeCatalog.d.ts +41 -0
  111. package/dist/runtime/data/integrationNodeCatalog.js +14 -0
  112. package/dist/runtime/data/integrationTemplates.d.ts +48 -0
  113. package/dist/runtime/data/integrationTemplates.js +21 -0
  114. package/dist/runtime/data/integrationWorkflowSamples.js +1 -1
  115. package/dist/runtime/data/nodeIcons.d.ts +2 -0
  116. package/dist/runtime/data/nodeIcons.js +73 -0
  117. package/dist/runtime/data/nodeInputData.d.ts +16 -4
  118. package/dist/runtime/data/nodeInputData.js +4 -0
  119. package/dist/runtime/data/payloadForm.d.ts +9 -2
  120. package/dist/runtime/data/payloadForm.js +29 -4
  121. package/dist/runtime/data/portTone.d.ts +17 -0
  122. package/dist/runtime/data/portTone.js +7 -0
  123. package/dist/runtime/data/webhookAuth.d.ts +18 -0
  124. package/dist/runtime/data/webhookAuth.js +19 -0
  125. package/dist/runtime/data/workflowIconCatalog.js +4 -6
  126. package/dist/runtime/i18n/messages/de.json +1207 -0
  127. package/dist/runtime/i18n/messages/en.json +1207 -0
  128. package/dist/runtime/i18n/translate.d.ts +83 -0
  129. package/dist/runtime/i18n/translate.js +41 -0
  130. package/dist/runtime/pages/integrations/audit.vue +140 -37
  131. package/dist/runtime/pages/integrations/credentials/oauth-complete.vue +9 -6
  132. package/dist/runtime/pages/integrations/credentials.vue +180 -64
  133. package/dist/runtime/pages/integrations/index.vue +91 -92
  134. package/dist/runtime/pages/integrations/runs.vue +33 -21
  135. package/dist/runtime/pages/integrations/schedules.vue +88 -23
  136. package/dist/runtime/pages/integrations/secrets.vue +57 -35
  137. package/dist/runtime/pages/integrations/templates/[slug].vue +119 -88
  138. package/dist/runtime/pages/integrations/templates/index.vue +82 -103
  139. package/dist/runtime/pages/integrations/workflows/[id].vue +344 -91
  140. package/dist/runtime/pages/integrations/workflows/index.vue +23 -5
  141. package/dist/runtime/plugins/locale.d.ts +10 -0
  142. package/dist/runtime/plugins/locale.js +6 -0
  143. package/dist/runtime/plugins/realtime.client.d.ts +32 -0
  144. package/dist/runtime/plugins/realtime.client.js +4 -0
  145. package/dist/runtime/types/integrations.d.ts +65 -6
  146. package/dist/runtime/utils/apiError.d.ts +47 -0
  147. package/dist/runtime/utils/apiError.js +32 -0
  148. package/dist/runtime/utils/cockpitSources.d.ts +153 -0
  149. package/dist/runtime/utils/cockpitSources.js +14 -0
  150. package/dist/runtime/utils/credentialState.d.ts +24 -1
  151. package/dist/runtime/utils/credentialState.js +26 -11
  152. package/dist/runtime/utils/loadGate.d.ts +61 -0
  153. package/dist/runtime/utils/loadGate.js +22 -0
  154. package/dist/runtime/utils/localize.d.ts +46 -4
  155. package/dist/runtime/utils/localize.js +34 -7
  156. package/dist/runtime/utils/notify.d.ts +59 -0
  157. package/dist/runtime/utils/notify.js +21 -0
  158. package/dist/runtime/utils/pollBudget.d.ts +72 -0
  159. package/dist/runtime/utils/pollBudget.js +17 -0
  160. package/dist/runtime/utils/revenexxCredentialSource.d.ts +26 -0
  161. package/dist/runtime/utils/revenexxCredentialSource.js +8 -0
  162. package/dist/runtime/utils/schedule.d.ts +116 -0
  163. package/dist/runtime/utils/schedule.js +219 -0
  164. package/dist/runtime/utils/statusStyle.d.ts +26 -2
  165. package/dist/runtime/utils/statusStyle.js +9 -1
  166. package/dist/runtime/utils/validationFindings.d.ts +101 -0
  167. package/dist/runtime/utils/validationFindings.js +54 -0
  168. package/dist/runtime/utils/whenIdle.d.ts +42 -0
  169. package/dist/runtime/utils/whenIdle.js +12 -0
  170. package/dist/types.d.mts +2 -6
  171. package/package.json +10 -3
@@ -1,41 +1,48 @@
1
1
  <script setup>
2
2
  import { computed, ref, watch } from "vue";
3
- import { Magnifier as Search, RefreshCircle as Loader2, Layers, DangerTriangle as AlertTriangle, Bolt as Zap, ClockCircle as Clock, Global as Webhook, Play } from "#solar-icons";
3
+ import { Magnifier as Search, Layers, DangerTriangle as AlertTriangle } from "#solar-icons";
4
4
  import { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@revenexx/studio";
5
- import TemplateFlowPreview from "../../../components/Integrations/TemplateFlowPreview.vue";
6
- import { TEMPLATE_CATEGORIES } from "../../../data/integrationTemplates";
7
- useHead({ title: "Templates \xB7 Integration Studio" });
5
+ import TemplateCard from "../../../components/Integrations/TemplateCard.vue";
6
+ import { templateCategoryLabel, templatePackageLabel } from "../../../data/integrationTemplates";
7
+ const t = useIntegrationsT();
8
+ useHead({ title: () => t("integration.templates.gallery.head.title") });
8
9
  const { templates, loading, error, loaded, load, ensureDetails } = useIntegrationsTemplates();
9
10
  const { load: loadNodeCatalog } = useIntegrationsNodes();
10
11
  onMounted(async () => {
11
12
  await Promise.all([load(true), loadNodeCatalog()]);
12
- void ensureDetails(templates.value.map((t) => t.slug));
13
+ void ensureDetails(templates.value.map((t2) => t2.slug));
13
14
  });
14
15
  const search = ref("");
15
16
  const activeCategory = ref("all");
16
17
  const activeLevel = ref("all");
17
18
  const sortBy = ref("name");
18
- const CATEGORY_LABELS = Object.fromEntries(
19
- TEMPLATE_CATEGORIES.map((c) => [c.key, c.label])
20
- );
21
- function categoryLabel(key) {
22
- return CATEGORY_LABELS[key] ?? key.charAt(0).toUpperCase() + key.slice(1);
23
- }
24
19
  const categories = computed(() => {
25
20
  const counts = /* @__PURE__ */ new Map();
26
- for (const t of templates.value) counts.set(t.category, (counts.get(t.category) ?? 0) + 1);
27
- return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([key, count]) => ({ key, label: categoryLabel(key), count }));
21
+ for (const t2 of templates.value) {
22
+ if (!t2.category) continue;
23
+ counts.set(t2.category, (counts.get(t2.category) ?? 0) + 1);
24
+ }
25
+ return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([key, count]) => ({ key, label: templateCategoryLabel(key), count }));
28
26
  });
29
27
  const LEVELS = ["beginner", "intermediate", "advanced"];
30
28
  const LEVEL_RANK = { beginner: 0, intermediate: 1, advanced: 2 };
29
+ const levelOptions = computed(() => LEVELS.map((l) => ({
30
+ value: l,
31
+ label: t(`integration.templates.gallery.filter.level.${l}`)
32
+ })));
33
+ const sortOptions = computed(() => [
34
+ { value: "name", label: t("integration.templates.gallery.sort.name") },
35
+ { value: "level", label: t("integration.templates.gallery.sort.level") },
36
+ { value: "nodes", label: t("integration.templates.gallery.sort.nodes") }
37
+ ]);
31
38
  const filtered = computed(() => {
32
39
  const q = search.value.trim().toLowerCase();
33
40
  let items = templates.value;
34
- if (activeCategory.value !== "all") items = items.filter((t) => t.category === activeCategory.value);
35
- if (activeLevel.value !== "all") items = items.filter((t) => t.level === activeLevel.value);
41
+ if (activeCategory.value !== "all") items = items.filter((t2) => t2.category === activeCategory.value);
42
+ if (activeLevel.value !== "all") items = items.filter((t2) => t2.level === activeLevel.value);
36
43
  if (q) {
37
44
  items = items.filter(
38
- (t) => t.name.toLowerCase().includes(q) || t.description.toLowerCase().includes(q) || t.vendors.some((v) => v.includes(q)) || (packageLabel(t) ?? "").toLowerCase().includes(q) || t.industries.some((i) => i.includes(q))
45
+ (t2) => t2.name.toLowerCase().includes(q) || t2.description.toLowerCase().includes(q) || t2.vendors.some((v) => v.toLowerCase().includes(q)) || (templatePackageLabel(t2) ?? "").toLowerCase().includes(q) || t2.industries.some((i) => i.toLowerCase().includes(q))
39
46
  );
40
47
  }
41
48
  const sorted = [...items];
@@ -45,23 +52,10 @@ const filtered = computed(() => {
45
52
  return sorted;
46
53
  });
47
54
  watch(() => templates.value.length, () => {
48
- if (templates.value.length) void ensureDetails(templates.value.map((t) => t.slug));
55
+ if (templates.value.length) void ensureDetails(templates.value.map((t2) => t2.slug));
49
56
  });
50
- const TRIGGER_ICON = { schedule: Clock, webhook: Webhook, manual: Play, event: Zap };
51
- const TRIGGER_LABEL = { schedule: "Scheduled", webhook: "Webhook", manual: "Run on demand", event: "Event-driven" };
52
- function levelDot(level) {
53
- return level === "beginner" ? "bg-emerald-500" : level === "intermediate" ? "bg-amber-500" : "bg-rose-500";
54
- }
55
- function bannerFor(tpl) {
56
- return tpl.images.find((i) => i.category === "banner")?.src ?? null;
57
- }
58
- function packageLabel(tpl) {
59
- const pkg = tpl.package;
60
- if (!pkg) return null;
61
- const tail = (pkg.name.split("/").pop() ?? pkg.name).replace(/^integrations-nodes-/, "");
62
- if (tail === "core") return null;
63
- return pkg.label ?? tail.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ");
64
- }
57
+ const awaitingCatalogue = computed(() => loading.value && !templates.value.length);
58
+ const statusText = computed(() => awaitingCatalogue.value ? t("integration.templates.gallery.state.loading") : "");
65
59
  </script>
66
60
 
67
61
  <template>
@@ -69,8 +63,8 @@ function packageLabel(tpl) {
69
63
  <!-- The toolbar lives in the header's #tabs slot: PageHeader is the
70
64
  sticky element (top-0 z-20), so filters + search stay pinned. -->
71
65
  <PageHeader
72
- title="Templates"
73
- description="Pre-built workflows you can clone, then tweak. Wired by revenexx and our partners."
66
+ :title="t('integration.templates.gallery.header.title')"
67
+ :description="t('integration.templates.gallery.header.description')"
74
68
  >
75
69
  <template #tabs>
76
70
  <div class="-mx-3 w-[calc(100%+1.5rem)] flex items-center gap-3 border-t border-black/[0.06] px-4 pt-2.5 pb-1">
@@ -81,7 +75,7 @@ function packageLabel(tpl) {
81
75
  :class="activeCategory === 'all' ? 'border-primary/30 bg-primary/10 text-primary font-medium' : 'border-black/[0.08] text-muted-foreground hover:border-black/20'"
82
76
  @click="activeCategory = 'all'"
83
77
  >
84
- All <span class="opacity-70">({{ templates.length }})</span>
78
+ {{ t("integration.templates.gallery.filter.all") }} <span class="opacity-70">({{ templates.length }})</span>
85
79
  </button>
86
80
  <button
87
81
  v-for="cat in categories"
@@ -96,35 +90,58 @@ function packageLabel(tpl) {
96
90
  </div>
97
91
 
98
92
  <Select v-model="activeLevel">
99
- <SelectTrigger class="h-7 data-[size=default]:h-7 min-h-0 py-0 w-36 rounded-full bg-white text-sm">
93
+ <SelectTrigger :aria-label="t('integration.templates.gallery.filter.levelAria')" class="h-7 data-[size=default]:h-7 min-h-0 py-0 w-36 rounded-full bg-white text-sm">
100
94
  <SelectValue />
101
95
  </SelectTrigger>
102
96
  <SelectContent>
103
- <SelectItem value="all">All levels</SelectItem>
104
- <SelectItem v-for="l in LEVELS" :key="l" :value="l">{{ l.charAt(0).toUpperCase() + l.slice(1) }}</SelectItem>
97
+ <SelectItem value="all">{{ t("integration.templates.gallery.filter.levelAllOption") }}</SelectItem>
98
+ <SelectItem v-for="opt in levelOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</SelectItem>
105
99
  </SelectContent>
106
100
  </Select>
107
101
  <Select v-model="sortBy">
108
- <SelectTrigger class="h-7 data-[size=default]:h-7 min-h-0 py-0 w-40 rounded-full bg-white text-sm">
102
+ <SelectTrigger :aria-label="t('integration.templates.gallery.sort.aria')" class="h-7 data-[size=default]:h-7 min-h-0 py-0 w-40 rounded-full bg-white text-sm">
109
103
  <SelectValue />
110
104
  </SelectTrigger>
111
105
  <SelectContent>
112
- <SelectItem value="name">Sort: A–Z</SelectItem>
113
- <SelectItem value="level">Sort: Simplest first</SelectItem>
114
- <SelectItem value="nodes">Sort: Most steps</SelectItem>
106
+ <SelectItem v-for="opt in sortOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</SelectItem>
115
107
  </SelectContent>
116
108
  </Select>
117
109
  <div class="relative w-60">
118
110
  <Search :size="12" class="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
119
- <Input v-model="search" placeholder="Search templates, vendors…" class="h-7 min-h-0 py-0 rounded-full pl-7 text-sm bg-white" />
111
+ <Input v-model="search" :aria-label="t('integration.templates.gallery.search.aria')" :placeholder="t('integration.templates.gallery.search.placeholder')" class="h-7 min-h-0 py-0 rounded-full pl-7 text-sm bg-white" />
120
112
  </div>
121
113
  </div>
122
114
  </template>
123
115
  </PageHeader>
124
116
 
125
117
  <div class="p-4">
126
- <!-- Loading (first fetch) -->
127
- <div v-if="loading && !templates.length" class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
118
+ <!-- What the page is doing, in words (specs/templates.md AC-9).
119
+ The shimmer says "still working" only to somebody who can see it, and
120
+ six blank tiles carry no text, no name and no state — so waiting and
121
+ "nothing is registered" were the same silence, which are the two states
122
+ that ask for opposite reactions.
123
+
124
+ The region is OUTSIDE the v-if chain below and always in the DOM, with
125
+ only its text changing. A live region that appears together with its
126
+ content is a region assistive technology may never have been watching:
127
+ what is announced is a change inside an existing region.
128
+
129
+ Named, because it is not the only `role="status"` on this page: every
130
+ card draws one while its flow miniature loads ("Loading the flow"). Two
131
+ unnamed status regions on one page are two things a reader — and a test —
132
+ cannot tell apart, and this one is about the gallery rather than a card. -->
133
+ <p role="status" aria-live="polite" :aria-label="t('integration.templates.gallery.state.regionLabel')" class="sr-only">{{ statusText }}</p>
134
+
135
+ <!-- The placeholders are taken out of the accessibility tree rather than
136
+ marked busy. `aria-busy` only asks assistive technology to defer
137
+ exposing changes, and on a role-less div that is not a live region NVDA
138
+ and JAWS effectively ignore it — it would have expressed the state
139
+ without keeping anything from being walked. A placeholder carries no
140
+ information, so there is nothing to lose by removing it, and the region
141
+ above says in words what this area means. Which is also why the named
142
+ "Templates" group below is absent while this branch renders: the set of
143
+ cards does not exist yet, rather than existing and being empty. -->
144
+ <div v-if="awaitingCatalogue" aria-hidden="true" class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
128
145
  <div v-for="n in 6" :key="`sk-${n}`" class="rounded-lg border border-black/[0.06] bg-white p-0 overflow-hidden">
129
146
  <div class="h-36 bg-black/[0.04] animate-pulse" />
130
147
  <div class="p-4 space-y-2">
@@ -135,11 +152,18 @@ function packageLabel(tpl) {
135
152
  </div>
136
153
  </div>
137
154
 
138
- <!-- Fetch failed -->
139
- <div v-else-if="error && !templates.length" class="rounded-lg border border-rose-500/20 bg-rose-500/5 p-4 text-sm text-rose-700 flex items-start gap-2">
155
+ <!-- Fetch failed. `role="alert"` because this block is how the wait ends for
156
+ somebody who was told through the region above to hold on: it is
157
+ announced when it is inserted, so the sequence is "Loading templates…"
158
+ then the reason, rather than "Loading templates…" then silence. An alert
159
+ rather than a copy of the text in the status region, which would put the
160
+ same sentence in the page twice and read it out twice to anybody walking
161
+ it. The other two empty states are not alerts: nothing had spoken before
162
+ them, and their copy is where the reader is going anyway. -->
163
+ <div v-else-if="error && !templates.length" role="alert" class="rounded-lg border border-rose-500/20 bg-rose-500/5 p-4 text-sm text-rose-700 flex items-start gap-2">
140
164
  <AlertTriangle :size="15" class="mt-0.5 shrink-0" />
141
165
  <div>
142
- <div class="font-medium">Couldn't load templates.</div>
166
+ <div class="font-medium">{{ t("integration.templates.gallery.error.title") }}</div>
143
167
  <div class="text-rose-700/80">{{ error }}</div>
144
168
  </div>
145
169
  </div>
@@ -149,71 +173,26 @@ function packageLabel(tpl) {
149
173
  <span class="mx-auto mb-3 flex size-10 items-center justify-center rounded-lg bg-slate-100 text-slate-400">
150
174
  <Layers :size="18" />
151
175
  </span>
152
- <div class="text-sm font-semibold text-foreground">No templates registered yet</div>
176
+ <div class="text-sm font-semibold text-foreground">{{ t("integration.templates.gallery.empty.registered.title") }}</div>
153
177
  <p class="mx-auto mt-1 max-w-sm text-sm text-muted-foreground">
154
- Templates ship with node packages. Once a package that publishes templates is installed, they show up here.
178
+ {{ t("integration.templates.gallery.empty.registered.description") }}
155
179
  </p>
156
180
  </div>
157
181
 
158
- <!-- Cards -->
159
- <div v-else class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
160
- <NuxtLink
182
+ <!-- The group is named so the cards can be addressed as a set; a card names
183
+ itself from what it shows, which TemplateCard says more about. -->
184
+ <div v-else role="group" :aria-label="t('integration.templates.gallery.cards.ariaLabel')" class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
185
+ <TemplateCard
161
186
  v-for="tpl in filtered"
162
187
  :key="tpl.slug"
188
+ :template="tpl"
189
+ density="full"
163
190
  :to="`/integrations/templates/${tpl.slug}`"
164
- class="group flex flex-col overflow-hidden rounded-lg border border-black/[0.06] bg-white transition hover:border-black/20 hover:shadow-sm"
165
- >
166
- <!-- Live workflow preview (or the registry banner, when one ships) -->
167
- <div class="relative h-36 shrink-0 border-b border-black/[0.06] bg-slate-50/70 [background-image:radial-gradient(circle,rgba(15,23,42,0.07)_1px,transparent_1px)] [background-size:14px_14px]">
168
- <img
169
- v-if="bannerFor(tpl)"
170
- :src="bannerFor(tpl)"
171
- alt=""
172
- class="h-full w-full object-cover"
173
- >
174
- <TemplateFlowPreview v-else-if="tpl.detailLoaded" :blob="tpl.blob" :height="144" class="px-2" />
175
- <div v-else class="flex h-full items-center justify-center">
176
- <Loader2 :size="16" class="animate-spin text-muted-foreground/50" />
177
- </div>
178
- <div class="absolute left-2 top-2 flex items-center gap-1.5">
179
- <span class="inline-flex items-center whitespace-nowrap rounded-full bg-slate-100 px-2 py-0.5 uppercase text-xs font-medium text-slate-700">
180
- {{ categoryLabel(tpl.category) }}
181
- </span>
182
- <span
183
- v-if="packageLabel(tpl)"
184
- class="inline-flex items-center whitespace-nowrap rounded-full bg-violet-100 px-2 py-0.5 uppercase text-xs font-medium text-violet-700"
185
- >
186
- {{ packageLabel(tpl) }}
187
- </span>
188
- </div>
189
- </div>
190
-
191
- <div class="flex min-h-0 flex-1 flex-col p-4">
192
- <div class="text-sm font-semibold text-foreground line-clamp-1">{{ tpl.name }}</div>
193
- <p class="mt-1 flex-1 text-sm text-muted-foreground line-clamp-2">{{ tpl.description }}</p>
194
-
195
- <div class="mt-3 flex items-center gap-2 text-xs text-muted-foreground">
196
- <span
197
- v-for="t in tpl.triggerTypes"
198
- :key="t"
199
- class="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-2 py-0.5 uppercase"
200
- >
201
- <component :is="TRIGGER_ICON[t] ?? Zap" :size="10" />
202
- {{ TRIGGER_LABEL[t] ?? t }}
203
- </span>
204
- <span class="flex-1" />
205
- <span class="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-2 py-0.5 uppercase">
206
- <span class="size-1.5 rounded-full" :class="levelDot(tpl.level)" />
207
- {{ tpl.level }}
208
- </span>
209
- <span v-if="tpl.publisher.kind !== 'revenexx'" class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 uppercase">{{ tpl.publisher.name }}</span>
210
- </div>
211
- </div>
212
- </NuxtLink>
191
+ />
213
192
  </div>
214
193
 
215
194
  <div v-if="templates.length && !filtered.length" class="mt-4 rounded-lg border border-black/[0.06] bg-white p-8 text-center text-sm text-muted-foreground">
216
- No templates match these filters.
195
+ {{ t("integration.templates.gallery.empty.filtered") }}
217
196
  </div>
218
197
  </div>
219
198
  </IntegrationsLayout>