@anchrd/intel-ui 0.22.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/i18n/de.json CHANGED
@@ -2,8 +2,6 @@
2
2
  "$locale": "de-DE",
3
3
  "app.name": "Intel",
4
4
  "nav.primary": "Hauptnavigation",
5
- "nav.nodes": "Wissen",
6
- "nav.flows": "Flows",
7
5
  "nav.tools": "Werkzeuge",
8
6
  "shell.toggleSidebar": "Seitenleiste ein- oder ausklappen",
9
7
  "shell.resizeSidebar": "Breite der Seitenleiste",
@@ -33,7 +31,8 @@
33
31
  "tree.expandCalls": "Die Flows zeigen, die {title} aufruft",
34
32
  "tree.collapseCalls": "Die Flows verbergen, die {title} aufruft",
35
33
  "tree.add": "Zu {title} hinzufügen",
36
- "tree.addRoot": "Auf oberster Ebene anlegen",
34
+ "tree.addRoot": "Element",
35
+ "tree.addRoot.long": "Auf oberster Ebene anlegen",
37
36
  "tree.new.folder": "Neuer Ordner",
38
37
  "tree.new.document": "Neues Dokument",
39
38
  "tree.new.table": "Neue Tabelle",
@@ -50,6 +49,7 @@
50
49
  "tree.kind.table": "Tabelle",
51
50
  "tree.kind.flow": "Flow",
52
51
  "tree.move.action": "Verschieben nach …",
52
+ "tree.move.hint": "oder ziehen",
53
53
  "tree.move.title": "{title} verschieben",
54
54
  "tree.move.root": "Oberste Ebene",
55
55
  "tree.move.dropRoot": "Hier ablegen, um auf die oberste Ebene zu verschieben",
@@ -85,9 +85,24 @@
85
85
  "archive.description": "Alles, was du archiviert hast, das Neueste zuerst. Beim Wiederherstellen kommt ein Eintrag dorthin zurück, wo er lag.",
86
86
  "archive.restore": "{title} wiederherstellen",
87
87
  "archive.restoreAction": "Wiederherstellen",
88
+ "archive.restoring": "{title} wird wiederhergestellt",
89
+ "archive.restoringAction": "Wird wiederhergestellt",
88
90
  "archive.empty": "Es ist nichts archiviert.",
89
91
  "archive.failed": "Das Archiv konnte nicht gelesen werden. Prüfe deinen Zugriff und versuche es erneut.",
90
92
  "archive.restoreFailed": "Es wurde nicht wiederhergestellt. Vielleicht hat jemand anderes es geändert — lade neu und versuche es erneut.",
93
+ "archive.purge": "{title} endgültig löschen",
94
+ "archive.purge.title": "Endgültig löschen?",
95
+ "archive.purge.body": "„{title}\u201c und alles, was dazugehört — jede Version, der Inhalt und die Einträge im Suchindex — ist danach weg. Das lässt sich nicht rückgängig machen.",
96
+ "archive.purge.links.none": "Keine anderen Dokumente verweisen darauf.",
97
+ "archive.purge.links.one": "Ein anderes Dokument verweist darauf; dieser Verweis wird brechen.",
98
+ "archive.purge.links.many": "{count} andere Dokumente verweisen darauf; diese Verweise werden brechen.",
99
+ "archive.purge.grants": "Die Freigaben werden ebenfalls gelöscht.",
100
+ "archive.purge.items": "Damit werden insgesamt {count} Elemente endgültig gelöscht.",
101
+ "archive.purge.previewLoading": "Verweise werden geprüft …",
102
+ "archive.purge.previewFailed": "Die Verweise konnten nicht geprüft werden. Lade neu und versuche es vor dem Löschen erneut.",
103
+ "archive.purge.confirm": "Endgültig löschen",
104
+ "archive.purge.cancel": "Abbrechen",
105
+ "archive.purgeFailed": "Es konnte nicht vollständig gelöscht werden. Lade neu und versuche es erneut.",
91
106
  "archive.archivedAt": "Archiviert {when}",
92
107
  "resource.conflict": "Nicht geändert: Jemand anderes hat diesen Eintrag zuerst geändert. Lade ihn neu und versuche es erneut.",
93
108
  "resource.forbidden": "Nicht geändert: Du darfst diesen Eintrag nicht ändern.",
@@ -97,6 +112,7 @@
97
112
  "node.attachmentHelp": "Die kanonische Datei liegt privat in Intel. Ihre KI-lesbare Projektion wird getrennt indexiert.",
98
113
  "node.table.summary": "{rows} Zeilen, {columns} Spalten",
99
114
  "node.table.download": "CSV herunterladen",
115
+ "node.table.downloadFailed": "Die CSV konnte nicht erzeugt werden. Möglicherweise ist die Tabelle noch leer.",
100
116
  "node.table.empty": "Noch keine Zeilen. Zeilen werden von Flows angehängt.",
101
117
  "node.table.undefined": "Diese Tabelle hat noch keine Spalten.",
102
118
  "node.select": "Wähle ein Dokument oder einen Ordner, um damit zu arbeiten.",
@@ -112,7 +128,6 @@
112
128
  "node.share": "Freigeben",
113
129
  "node.shareAction": "Zugriff geben",
114
130
  "node.email": "E-Mail-Adresse",
115
- "node.emailHint": "Intel kann nicht prüfen, ob hinter dieser Adresse ein Konto steht. Die Freigabe greift, sobald sich jemand damit anmeldet — auch nach einer späteren Registrierung.",
116
131
  "node.shareWith": "Wer Zugriff bekommt",
117
132
  "node.shareWithEmail": "Eine E-Mail-Adresse",
118
133
  "node.shareWithOrganization": "Alle in der Organisation",
@@ -130,8 +145,13 @@
130
145
  "node.verbHint.execute": "Die Flows hier drin starten",
131
146
  "node.verbHint.share": "Anderen Zugriff geben",
132
147
  "node.organization": "Alle in der Organisation",
148
+ "node.owner": "Besitzer",
149
+ "node.access": "Zugriff",
150
+ "node.organizationAccess": "Alle in der Organisation haben Zugriff",
151
+ "node.accessDetails": "Zugriffsdetails anzeigen ({count})",
152
+ "node.revokeVerb": "{verb} entziehen",
153
+ "node.revokeVerbFor": "{verb} für {recipient} entziehen",
133
154
  "node.revokeShare": "Zugriff entziehen",
134
- "node.noGrants": "Außer der Besitzerin oder dem Besitzer hat noch niemand Zugriff.",
135
155
  "node.shareFailed": "Der Zugriff wurde nicht geändert. Prüfe deine Berechtigung und versuche es erneut.",
136
156
  "node.shareUnreadable": "Flows in diesem Ordner lesen Dokumente, die diese Freigabe nicht abdeckt: {titles}.",
137
157
  "node.shareUnreadableMore": "{count} weitere liegen ebenfalls außer Reichweite, und du kannst sie nicht sehen.",
@@ -290,6 +310,9 @@
290
310
  "settings.description": "Sprache, Erscheinungsbild und Zeitzone. Alle drei gelten sofort und werden auf diesem Gerät gemerkt.",
291
311
  "settings.language": "Sprache",
292
312
  "settings.appearance": "Erscheinungsbild",
313
+ "settings.timezone": "Zeitzone",
314
+ "settings.timezone.device": "Dieses Gerät ({zone})",
315
+ "settings.timezone.hint": "Zeiten werden nur in dieser Zone GELESEN. Gespeichert und übertragen wird weiterhin UTC.",
293
316
  "settings.appearance.system": "Dem System folgen",
294
317
  "settings.appearance.light": "Hell",
295
318
  "settings.appearance.dark": "Dunkel",
@@ -303,6 +326,7 @@
303
326
  "reindex.failed": "Der Neuaufbau wurde nicht gestartet. Prüfe deine Berechtigung und versuche es noch einmal.",
304
327
  "auth.signOut": "Abmelden",
305
328
  "auth.signOutFailed": "Das Abmelden ist fehlgeschlagen. Prüfe deine Verbindung und versuche es erneut.",
329
+ "signIn.unreachable": "Intel war nicht erreichbar. Prüfe deine Verbindung und versuche es erneut.",
306
330
  "signIn.refusedTitle": "Angemeldet, aber nicht angenommen",
307
331
  "signIn.refusedBody": "Gate hat dich angemeldet, und diese Installation hat den Ausweis nicht angenommen. Du wurdest nicht zurück zum Login geschickt, denn dort beginnt die Schleife.",
308
332
  "signIn.refusedLikely": "Meistens ist die Sitzung zwischen den beiden Schritten abgelaufen, oder der Ausweis wurde für eine andere Installation ausgestellt. Ein Neuladen ist einen Versuch wert; danach muss sich anschauen, wer diese Installation betreibt.",
package/src/i18n/en.json CHANGED
@@ -2,8 +2,6 @@
2
2
  "$locale": "en-US",
3
3
  "app.name": "Intel",
4
4
  "nav.primary": "Primary navigation",
5
- "nav.nodes": "Intelligence",
6
- "nav.flows": "Flows",
7
5
  "nav.tools": "Tools",
8
6
  "shell.toggleSidebar": "Collapse or expand the sidebar",
9
7
  "shell.resizeSidebar": "Sidebar width",
@@ -33,7 +31,8 @@
33
31
  "tree.expandCalls": "Show the flows {title} calls",
34
32
  "tree.collapseCalls": "Hide the flows {title} calls",
35
33
  "tree.add": "Add to {title}",
36
- "tree.addRoot": "Add at the top level",
34
+ "tree.addRoot": "Element",
35
+ "tree.addRoot.long": "Add at the top level",
37
36
  "tree.new.folder": "New folder",
38
37
  "tree.new.document": "New document",
39
38
  "tree.new.table": "New table",
@@ -50,6 +49,7 @@
50
49
  "tree.kind.table": "Table",
51
50
  "tree.kind.flow": "Flow",
52
51
  "tree.move.action": "Move to…",
52
+ "tree.move.hint": "or drag",
53
53
  "tree.move.title": "Move {title}",
54
54
  "tree.move.root": "Top level",
55
55
  "tree.move.dropRoot": "Drop here to move to the top level",
@@ -85,9 +85,24 @@
85
85
  "archive.description": "Everything you archived, newest first. Restoring puts an entry back where it was filed.",
86
86
  "archive.restore": "Restore {title}",
87
87
  "archive.restoreAction": "Restore",
88
+ "archive.restoring": "Restoring {title}",
89
+ "archive.restoringAction": "Restoring",
88
90
  "archive.empty": "Nothing is archived.",
89
91
  "archive.failed": "The archive could not be read. Check your access and try again.",
90
92
  "archive.restoreFailed": "It was not restored. Somebody else may have changed it — reload and try again.",
93
+ "archive.purge": "Delete {title} for good",
94
+ "archive.purge.title": "Delete this for good?",
95
+ "archive.purge.body": "“{title}” and everything belonging to it — every version, its content and its entries in the search index — will be gone. This cannot be undone.",
96
+ "archive.purge.links.none": "No other documents link to it.",
97
+ "archive.purge.links.one": "One other document links to it and that link will break.",
98
+ "archive.purge.links.many": "{count} other documents link to it and those links will break.",
99
+ "archive.purge.grants": "Its shares will also be deleted.",
100
+ "archive.purge.items": "This permanently deletes {count} items in total.",
101
+ "archive.purge.previewLoading": "Checking links…",
102
+ "archive.purge.previewFailed": "The links could not be checked. Reload and try again before deleting.",
103
+ "archive.purge.confirm": "Delete for good",
104
+ "archive.purge.cancel": "Cancel",
105
+ "archive.purgeFailed": "It could not be deleted completely. Reload and try again.",
91
106
  "archive.archivedAt": "Archived {when}",
92
107
  "resource.conflict": "It was not changed: somebody else changed this entry first. Reload it and try again.",
93
108
  "resource.forbidden": "It was not changed: you may not change this entry.",
@@ -97,6 +112,7 @@
97
112
  "node.attachmentHelp": "The canonical file is stored privately in Intel. Its AI-readable projection is indexed separately.",
98
113
  "node.table.summary": "{rows} rows, {columns} columns",
99
114
  "node.table.download": "Download CSV",
115
+ "node.table.downloadFailed": "The CSV could not be created. The table may still be empty.",
100
116
  "node.table.empty": "No rows yet. Rows are appended by flows.",
101
117
  "node.table.undefined": "This table has no columns yet.",
102
118
  "node.select": "Select a document or folder to work with it.",
@@ -112,7 +128,6 @@
112
128
  "node.share": "Share",
113
129
  "node.shareAction": "Grant access",
114
130
  "node.email": "Email address",
115
- "node.emailHint": "Intel cannot check whether an account exists behind this address. The grant takes effect as soon as somebody signs in with it — including after they register.",
116
131
  "node.shareWith": "Who gets access",
117
132
  "node.shareWithEmail": "One email address",
118
133
  "node.shareWithOrganization": "Everyone in the organization",
@@ -130,8 +145,13 @@
130
145
  "node.verbHint.execute": "Start the flows in here",
131
146
  "node.verbHint.share": "Give others access",
132
147
  "node.organization": "Everyone in the organization",
148
+ "node.owner": "Owner",
149
+ "node.access": "Access",
150
+ "node.organizationAccess": "Everyone in the organization has access",
151
+ "node.accessDetails": "Show access details ({count})",
152
+ "node.revokeVerb": "Revoke {verb}",
153
+ "node.revokeVerbFor": "Revoke {verb} for {recipient}",
133
154
  "node.revokeShare": "Revoke access",
134
- "node.noGrants": "Nobody outside the owner has access yet.",
135
155
  "node.shareFailed": "Access was not changed. Check your permission and try again.",
136
156
  "node.shareUnreadable": "Flows in this folder read documents this grant does not cover: {titles}.",
137
157
  "node.shareUnreadableMore": "{count} more are out of reach too, and you cannot see them.",
@@ -290,6 +310,9 @@
290
310
  "settings.description": "Language, appearance and timezone. All three take effect immediately and are remembered on this device.",
291
311
  "settings.language": "Language",
292
312
  "settings.appearance": "Appearance",
313
+ "settings.timezone": "Time zone",
314
+ "settings.timezone.device": "This device ({zone})",
315
+ "settings.timezone.hint": "Times are only READ in this zone. What is stored and sent stays UTC.",
293
316
  "settings.appearance.system": "Follow the system",
294
317
  "settings.appearance.light": "Light",
295
318
  "settings.appearance.dark": "Dark",
@@ -303,6 +326,7 @@
303
326
  "reindex.failed": "The rebuild was not started. Check your access and try again.",
304
327
  "auth.signOut": "Sign out",
305
328
  "auth.signOutFailed": "Signing out failed. Check your connection and try again.",
329
+ "signIn.unreachable": "Intel could not be reached. Check your connection and try again.",
306
330
  "signIn.refusedTitle": "Signed in, but not accepted",
307
331
  "signIn.refusedBody": "Gate signed you in, and this installation did not accept the credential. You have not been sent back to the login, because that is where the loop starts.",
308
332
  "signIn.refusedLikely": "Most often the session expired between the two steps, or the credential was issued for a different installation. Reloading is worth one try; after that, whoever runs this installation has to look.",
package/src/i18n/es.json CHANGED
@@ -2,8 +2,6 @@
2
2
  "$locale": "es-ES",
3
3
  "app.name": "Intel",
4
4
  "nav.primary": "Navegación principal",
5
- "nav.nodes": "Conocimiento",
6
- "nav.flows": "Flujos",
7
5
  "nav.tools": "Herramientas",
8
6
  "shell.toggleSidebar": "Plegar o desplegar la barra lateral",
9
7
  "shell.resizeSidebar": "Ancho de la barra lateral",
@@ -33,7 +31,8 @@
33
31
  "tree.expandCalls": "Mostrar los flujos a los que llama {title}",
34
32
  "tree.collapseCalls": "Ocultar los flujos a los que llama {title}",
35
33
  "tree.add": "Añadir a {title}",
36
- "tree.addRoot": "Añadir en el nivel superior",
34
+ "tree.addRoot": "Elemento",
35
+ "tree.addRoot.long": "Añadir en el nivel superior",
37
36
  "tree.new.folder": "Carpeta nueva",
38
37
  "tree.new.document": "Documento nuevo",
39
38
  "tree.new.table": "Tabla nueva",
@@ -50,6 +49,7 @@
50
49
  "tree.kind.table": "Tabla",
51
50
  "tree.kind.flow": "Flujo",
52
51
  "tree.move.action": "Mover a…",
52
+ "tree.move.hint": "o arrastra",
53
53
  "tree.move.title": "Mover {title}",
54
54
  "tree.move.root": "Nivel superior",
55
55
  "tree.move.dropRoot": "Suelta aquí para mover al nivel superior",
@@ -85,9 +85,24 @@
85
85
  "archive.description": "Todo lo que has archivado, lo más reciente primero. Al restaurar, una entrada vuelve al sitio donde estaba.",
86
86
  "archive.restore": "Restaurar {title}",
87
87
  "archive.restoreAction": "Restaurar",
88
+ "archive.restoring": "Restaurando {title}",
89
+ "archive.restoringAction": "Restaurando",
88
90
  "archive.empty": "No hay nada archivado.",
89
91
  "archive.failed": "El archivo no se ha podido leer. Comprueba tu acceso e inténtalo de nuevo.",
90
92
  "archive.restoreFailed": "No se ha restaurado. Puede que otra persona lo haya cambiado — recarga e inténtalo de nuevo.",
93
+ "archive.purge": "Eliminar «{title}» definitivamente",
94
+ "archive.purge.title": "¿Eliminar definitivamente?",
95
+ "archive.purge.body": "«{title}» y todo lo que le pertenece — cada versión, su contenido y sus entradas en el índice de búsqueda — desaparecerá. Esto no se puede deshacer.",
96
+ "archive.purge.links.none": "Ningún otro documento contiene un enlace a este elemento.",
97
+ "archive.purge.links.one": "Otro documento contiene un enlace a este elemento y ese enlace dejará de funcionar.",
98
+ "archive.purge.links.many": "Otros {count} documentos contienen enlaces a este elemento y dejarán de funcionar.",
99
+ "archive.purge.grants": "Sus permisos compartidos también se eliminarán.",
100
+ "archive.purge.items": "Esto elimina definitivamente {count} elementos en total.",
101
+ "archive.purge.previewLoading": "Comprobando enlaces…",
102
+ "archive.purge.previewFailed": "No se pudieron comprobar los enlaces. Recarga e inténtalo de nuevo antes de eliminar.",
103
+ "archive.purge.confirm": "Eliminar definitivamente",
104
+ "archive.purge.cancel": "Cancelar",
105
+ "archive.purgeFailed": "No se pudo eliminar por completo. Recarga e inténtalo de nuevo.",
91
106
  "archive.archivedAt": "Archivado {when}",
92
107
  "resource.conflict": "No se ha cambiado: otra persona ha cambiado esta entrada antes. Recárgala e inténtalo de nuevo.",
93
108
  "resource.forbidden": "No se ha cambiado: no puedes cambiar esta entrada.",
@@ -97,6 +112,7 @@
97
112
  "node.attachmentHelp": "El archivo canónico se guarda en privado en Intel. Su proyección legible por la IA se indexa por separado.",
98
113
  "node.table.summary": "{rows} filas, {columns} columnas",
99
114
  "node.table.download": "Descargar CSV",
115
+ "node.table.downloadFailed": "No se pudo crear el CSV. Puede que la tabla todavía esté vacía.",
100
116
  "node.table.empty": "Todavía no hay filas. Las filas las añaden los flujos.",
101
117
  "node.table.undefined": "Esta tabla todavía no tiene columnas.",
102
118
  "node.select": "Elige un documento o una carpeta para trabajar con ello.",
@@ -112,7 +128,6 @@
112
128
  "node.share": "Compartir",
113
129
  "node.shareAction": "Conceder acceso",
114
130
  "node.email": "Dirección de correo",
115
- "node.emailHint": "Intel no puede comprobar si existe una cuenta detrás de esta dirección. El permiso surte efecto en cuanto alguien inicie sesión con ella, incluso si se registra más tarde.",
116
131
  "node.shareWith": "Quién obtiene acceso",
117
132
  "node.shareWithEmail": "Una dirección de correo",
118
133
  "node.shareWithOrganization": "Todos en la organización",
@@ -130,8 +145,13 @@
130
145
  "node.verbHint.execute": "Iniciar los flujos que hay aquí dentro",
131
146
  "node.verbHint.share": "Dar acceso a otras personas",
132
147
  "node.organization": "Todas las personas de la organización",
148
+ "node.owner": "Propietario",
149
+ "node.access": "Acceso",
150
+ "node.organizationAccess": "Toda la organización tiene acceso",
151
+ "node.accessDetails": "Mostrar detalles de acceso ({count})",
152
+ "node.revokeVerb": "Retirar {verb}",
153
+ "node.revokeVerbFor": "Retirar {verb} a {recipient}",
133
154
  "node.revokeShare": "Retirar el acceso",
134
- "node.noGrants": "Fuera de quien lo posee, todavía no tiene acceso nadie.",
135
155
  "node.shareFailed": "El acceso no se ha cambiado. Comprueba tu permiso e inténtalo de nuevo.",
136
156
  "node.shareUnreadable": "Los flujos de esta carpeta leen documentos que este acceso no cubre: {titles}.",
137
157
  "node.shareUnreadableMore": "{count} más también quedan fuera de alcance, y no puedes verlos.",
@@ -290,6 +310,9 @@
290
310
  "settings.description": "Idioma, apariencia y zona horaria. Las tres se aplican de inmediato y se recuerdan en este dispositivo.",
291
311
  "settings.language": "Idioma",
292
312
  "settings.appearance": "Apariencia",
313
+ "settings.timezone": "Zona horaria",
314
+ "settings.timezone.device": "Este dispositivo ({zone})",
315
+ "settings.timezone.hint": "Las horas solo se LEEN en esta zona. Lo que se guarda y se envía sigue siendo UTC.",
293
316
  "settings.appearance.system": "Seguir al sistema",
294
317
  "settings.appearance.light": "Claro",
295
318
  "settings.appearance.dark": "Oscuro",
@@ -303,6 +326,7 @@
303
326
  "reindex.failed": "No se inició la reconstrucción. Comprueba tu acceso e inténtalo de nuevo.",
304
327
  "auth.signOut": "Cerrar sesión",
305
328
  "auth.signOutFailed": "El cierre de sesión ha fallado. Comprueba tu conexión e inténtalo de nuevo.",
329
+ "signIn.unreachable": "No se pudo contactar con Intel. Revisa tu conexión e inténtalo de nuevo.",
306
330
  "signIn.refusedTitle": "Sesión iniciada, pero no aceptada",
307
331
  "signIn.refusedBody": "Gate ha iniciado tu sesión, y esta instalación no ha aceptado la credencial. No se te ha devuelto al inicio de sesión, porque ahí es donde empieza el bucle.",
308
332
  "signIn.refusedLikely": "Lo más habitual es que la sesión caducara entre los dos pasos, o que la credencial se emitiera para otra instalación. Recargar merece un intento; después de eso, tiene que mirarlo quien gestiona esta instalación.",
package/src/main.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import { QueryClientProvider } from "@tanstack/react-query";
2
- import { RouterProvider } from "@tanstack/react-router";
3
2
  import { StrictMode } from "react";
4
3
  import { createRoot } from "react-dom/client";
4
+ import { AppRoot, createRefusalStore } from "@/app-root/app-root.tsx";
5
5
  import {
6
6
  createIntelDataProvider,
7
7
  loginPath,
@@ -11,9 +11,9 @@ import { createBrowserSignIn, createSignInGuard } from "@/data/sign-in/sign-in.t
11
11
  import { I18nProvider } from "@/i18n/i18n-context.tsx";
12
12
  import { createLanguages } from "@/i18n/i18n-languages/i18n-languages.ts";
13
13
  import { createIntelRouter } from "@/router/router.tsx";
14
- import { SignInRefused } from "@/sign-in-refused/sign-in-refused.tsx";
15
14
  import { applyTheme, readThemeChoice, SystemThemeQuery, themeFor } from "@/theme/theme.ts";
16
15
  import { ThemeProvider } from "@/theme/theme-context.tsx";
16
+ import { TimeZoneProvider } from "@/time/time-context.tsx";
17
17
  import "./styles.css";
18
18
  import "./theme/custom.css";
19
19
 
@@ -35,37 +35,31 @@ const signIn = createBrowserSignIn(loginPath);
35
35
  const mounted = createRoot(root);
36
36
  const queryClient = createIntelQueryClient();
37
37
 
38
- // The shell is painted from a function because a refused sign-in has to replace it, and that
39
- // refusal arrives from inside a fetch rather than from React.
40
- let refused = false;
41
-
38
+ // ⚠️ The refusal arrives from inside a fetch rather than from React, and it used to be answered by
39
+ // a module-level `let` plus a hand-written repaint. Both are gone (#456): the flag is a store React
40
+ // can subscribe to, and everything that decides what is on screen lives in `AppRoot` — a component
41
+ // a test can render. `main.tsx` is wiring again, and only wiring.
42
+ const refusal = createRefusalStore();
42
43
  const data = createIntelDataProvider({
43
- onUnauthorized: createSignInGuard(signIn, () => {
44
- refused = true;
45
- paint();
46
- }),
44
+ onUnauthorized: createSignInGuard(signIn, () => refusal.refuse()),
47
45
  });
48
46
  const router = createIntelRouter({ data });
49
47
 
50
- function paint(): void {
51
- mounted.render(
52
- <StrictMode>
53
- {/* The refusal screen sits INSIDE the providers too: it is text like any other, and it is the
54
- one screen a reader sees when nothing else works — English-only there would be the worst
55
- place for it. */}
56
- <I18nProvider languages={languages}>
57
- <ThemeProvider store={window.localStorage}>
58
- {refused ? (
59
- <SignInRefused />
60
- ) : (
61
- <QueryClientProvider client={queryClient}>
62
- <RouterProvider router={router} />
63
- </QueryClientProvider>
64
- )}
65
- </ThemeProvider>
66
- </I18nProvider>
67
- </StrictMode>,
68
- );
69
- }
70
-
71
- paint();
48
+ mounted.render(
49
+ <StrictMode>
50
+ {/* The refusal screen sits INSIDE the providers too: it is text like any other, and it is the
51
+ one screen a reader sees when nothing else works English-only there would be the worst
52
+ place for it. */}
53
+ <I18nProvider languages={languages}>
54
+ <ThemeProvider store={window.localStorage}>
55
+ {/* ⚠️ INSIDE `I18nProvider`: the formatter needs both — the language says HOW a time is
56
+ written, the zone WHICH time it is (#467). */}
57
+ <TimeZoneProvider store={window.localStorage}>
58
+ <QueryClientProvider client={queryClient}>
59
+ <AppRoot data={data} router={router} refusal={refusal} />
60
+ </QueryClientProvider>
61
+ </TimeZoneProvider>
62
+ </ThemeProvider>
63
+ </I18nProvider>
64
+ </StrictMode>,
65
+ );
@@ -1,6 +1,5 @@
1
1
  import type { Node } from "@anchrd/intel-contract/node";
2
- import { useMutation, useQuery } from "@tanstack/react-query";
3
- import { Download } from "lucide-react";
2
+ import { useQuery } from "@tanstack/react-query";
4
3
  import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
5
4
  import { useI18n } from "@/i18n/i18n-context.tsx";
6
5
  import { useIntelRouterContext } from "@/router/router-context.ts";
@@ -23,35 +22,17 @@ export function NodeTablePanel({ node }: { node: Node }) {
23
22
  queryKey: ["node-table", node.id],
24
23
  queryFn: () => data.getNodeTable(node.id),
25
24
  });
26
- // CSV is the export. The canonical body is already CSV, so downloading is handing over the bytes
27
- // that are stored rather than rendering a second format that could drift from them.
28
- const download = useMutation({
29
- mutationFn: async () => {
30
- const document_ = await data.getNode(node.id);
31
- return new Blob([document_.content ?? ""], { type: "text/csv;charset=utf-8" });
32
- },
33
- onSuccess: (blob) => {
34
- const url = URL.createObjectURL(blob);
35
- const anchor = document.createElement("a");
36
- anchor.href = url;
37
- anchor.download = node.title.toLowerCase().endsWith(".csv")
38
- ? node.title
39
- : `${node.title}.csv`;
40
- document.body.append(anchor);
41
- anchor.click();
42
- anchor.remove();
43
- setTimeout(() => URL.revokeObjectURL(url), 0);
44
- },
45
- });
46
-
47
25
  return (
48
26
  <div className="flex min-h-0 flex-1 flex-col">
49
- {/* ⚠️ Both of these used to be a second bar of their own, directly under the title line (#54):
50
- two headers stacked, and the table starting a whole row lower for it. A table is one
51
- node kind among four, not a screen with its own chrome — so the count goes beside the
52
- title as a quiet word and the export joins the buttons in the title line. They are rendered
53
- from here rather than from the screen because this is where the query lives; the screen
54
- would otherwise have to load a table it does not show. */}
27
+ {/* ⚠️ This used to be a second bar of its own, directly under the title line (#54): two
28
+ headers stacked, and the table starting a whole row lower for it. A table is one node kind
29
+ among four, not a screen with its own chrome — so the count goes beside the title as a
30
+ quiet word. It is rendered from here rather than from the screen because this is where the
31
+ query lives; the screen would otherwise have to load a table it does not show.
32
+
33
+ ⚠️ The CSV export used to stand here too, as a button WITH TEXT beside two icon buttons.
34
+ Since #454 it is an entry in the title line's three-dot menu (`resource-menu.tsx`) — where
35
+ the bundle export sits as well, because both do the same: hand out the whole thing. */}
55
36
  <ActionSlot name="title-meta">
56
37
  {table.data
57
38
  ? i18n.t("node.table.summary", {
@@ -60,27 +41,6 @@ export function NodeTablePanel({ node }: { node: Node }) {
60
41
  })
61
42
  : ""}
62
43
  </ActionSlot>
63
- <ActionSlot name="title-actions">
64
- <button
65
- type="button"
66
- onClick={() => download.mutate()}
67
- // Unchanged: nothing to export before the header is known, and a button that answers with
68
- // an empty file is worse than one that says it is not ready.
69
- disabled={download.isPending || !table.data?.columns.length}
70
- className="inline-flex h-8 items-center gap-2 rounded-md border px-3 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
71
- >
72
- <Download aria-hidden="true" className="size-4" />
73
- {i18n.t("node.table.download")}
74
- </button>
75
- </ActionSlot>
76
- {/* ⚠️ The refusal stays down here, in the body, where a sentence has room to be read. The
77
- title line has none — a failed export that only greyed a button in a header would be a
78
- failure nobody is told about. */}
79
- {download.isError ? (
80
- <p role="alert" className="mx-6 mt-4 text-sm text-destructive">
81
- {i18n.t("node.downloadFailed")}
82
- </p>
83
- ) : null}
84
44
  {table.isPending ? (
85
45
  <p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
86
46
  ) : table.isError ? (
@@ -3,7 +3,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
3
  import { useNavigate, useRouterState } from "@tanstack/react-router";
4
4
  import { Download, History, Paperclip } from "lucide-react";
5
5
  import { lazy, Suspense, useState } from "react";
6
- import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
7
6
  import { ViewToggle } from "@/app/view-toggle/view-toggle.tsx";
8
7
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
9
8
  import { refusalOf } from "@/data/request-refusal/request-refusal.ts";
@@ -13,6 +12,7 @@ import { useI18n } from "@/i18n/i18n-context.tsx";
13
12
  import { NodeTablePanel } from "@/node-table/node-table.tsx";
14
13
  import { useIntelRouterContext } from "@/router/router-context.ts";
15
14
  import { graphViewFrom, selectedFrom } from "@/router/selection-search.ts";
15
+ import { useDateTime } from "@/time/time-context.tsx";
16
16
  import { TitleRow } from "@/title-row/title-row.tsx";
17
17
 
18
18
  const NodeEditor = lazy(async () => ({
@@ -54,21 +54,24 @@ export function Nodes() {
54
54
  });
55
55
  return (
56
56
  <div className="flex h-full min-h-0 flex-col">
57
- {/* The screen owns the one action that belongs to the tree as a whole. Creating things is
58
- the tree's, in the sidebar, at the place a new thing is meant to go.
59
-
60
- ⚠️ Mounted only where there is a graph to switch to. A switch on a document would point at
61
- nothing, and the header is where that gets noticed first (#19). */}
62
- {graphable ? (
63
- <ActionSlot>
64
- <ViewToggle />
65
- </ActionSlot>
66
- ) : null}
67
57
  <section className="relative flex min-h-0 min-w-0 flex-1 flex-col bg-card">
68
58
  {/* ⚠️ Only the root draws its graph on its own. A folder's graph hangs *under* the folder's
69
59
  title line rather than in place of it (#58): the line is the one place its menu lives,
70
60
  and a folder that could only be renamed while the graph happened to be switched off
71
61
  would be a folder one cannot rename. */}
62
+ {/* ⚠️ #454 took the toggle out of the GLOBAL header — there it applied to the whole
63
+ application although it is a view onto the selected item. For an item it now stands in
64
+ that item's title line.
65
+
66
+ ⚠️ The root has no title line, and that is exactly why it gets a row of its own here.
67
+ Without it the graph of the WHOLE tree would have been unreachable after the move — the
68
+ toggle was its only way in and out. Moving a control must not abolish a view; that would
69
+ be a decision the ticket did not contain. */}
70
+ {selectedId === null ? (
71
+ <div className="flex shrink-0 items-center justify-end gap-2 border-b px-6 py-2">
72
+ <ViewToggle />
73
+ </div>
74
+ ) : null}
72
75
  {selectedId === null && selection.graph ? (
73
76
  <GraphPane
74
77
  query={relations}
@@ -122,14 +125,16 @@ export function Nodes() {
122
125
  ⚠️ The order of the right-hand group is `TitleRow`'s (#53), not this screen's: the
123
126
  version button is handed over as a child and lands before the menu whatever else
124
127
  shows up later.
125
- ⚠️ The view switch is deliberately NOT here: it changes how the current area is
126
- shown, not the document, and it stays beside the breadcrumb where the area is
127
- named. */}
128
+ ⚠️ #454: the toggle now stands HERE. It used to stand in the global header, beside
129
+ the search where what applies everywhere stands. But a graph view does not apply
130
+ everywhere, it is a view onto THIS item. It is only drawn where there is a graph: on
131
+ a document it pointed at nothing. */}
128
132
  <TitleRow
129
133
  title={selected.title}
130
134
  description={selected.description}
131
135
  target={{ type: "node", node: selected }}
132
136
  >
137
+ {graphable && <ViewToggle />}
133
138
  {selected.kind !== "folder" && (
134
139
  <TooltipProvider delayDuration={300}>
135
140
  <Tooltip>
@@ -234,6 +239,7 @@ function AttachmentPanel({ node }: { node: Node }) {
234
239
  function VersionHistory({ nodeId, close }: { nodeId: string; close(): void }) {
235
240
  const { data } = useIntelRouterContext();
236
241
  const i18n = useI18n();
242
+ const dateTime = useDateTime();
237
243
  const versions = useQuery({
238
244
  queryKey: ["node-versions", nodeId],
239
245
  queryFn: () => data.listNodeVersions(nodeId),
@@ -269,10 +275,7 @@ function VersionHistory({ nodeId, close }: { nodeId: string; close(): void }) {
269
275
  {i18n.t("node.version", { sequence: version.sequence })}
270
276
  </span>
271
277
  <time className="mt-1 block text-xs text-muted-foreground" dateTime={version.createdAt}>
272
- {new Intl.DateTimeFormat(i18n.locale, {
273
- dateStyle: "medium",
274
- timeStyle: "short",
275
- }).format(new Date(version.createdAt))}
278
+ {dateTime.at(version.createdAt)}
276
279
  </time>
277
280
  </li>
278
281
  ))}