Add full shell, replaceable Omics adapter and bilingual interaction

Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
This commit is contained in:
Codex
2026-09-13 14:26:39 +02:00
parent 2d1b714ebe
commit d8a29bfbdd
207 changed files with 8570 additions and 2164 deletions
+104 -20
View File
@@ -1,18 +1,25 @@
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@tailwind base;
/* Omics shares the document. Keep Tailwind's reset inside our mount and portals. */
@thoth-base {
@tailwind base;
}
@tailwind components;
@tailwind utilities;
body.thot-full-document { margin: 0; }
/*
* GSD theme — mirrors the Omics Portal design system (Manrope + Fraunces, warm
* off-white surfaces, #cb333b red). Tokens are RAW OKLCH triplets (L C H) so the
* Tailwind v3 config can wrap them as `oklch(var(--token) / <alpha-value>)` and
* opacity utilities (bg-primary/80, …) keep working. Dark tokens follow either a
* `.dark` class or the portal's `[data-bs-theme="dark"]`, ready for embedding.
* Tokens belong to the React mount and Base UI portal roots, never the host header.
*/
@layer base {
:root {
#root, [data-base-ui-portal] {
color-scheme: light;
--font-sans: "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, Arial, sans-serif;
--font-heading: "Fraunces", "Source Serif Pro", Georgia, "Times New Roman", serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
@@ -64,8 +71,8 @@
--sidebar-ring: 0.5587 0.1881 23.2;
}
.dark,
[data-bs-theme="dark"] {
:is(.dark, [data-bs-theme="dark"]) :is(#root, [data-base-ui-portal]) {
color-scheme: dark;
--shadow-tint: 0 0 0;
--background: 0.2178 0 90;
--foreground: 0.9310 0 90;
@@ -101,77 +108,79 @@
--sidebar-ring: 0.6897 0.1779 16.9;
}
* {
:where(#root, [data-base-ui-portal]), :where(#root, [data-base-ui-portal]) * {
@apply border-border outline-ring/50;
}
/* A click should read as a physical state change, not only as a hover ending.
Keep the response short and compositor-friendly so repeated actions stay fast. */
button:not(:disabled) {
:where(#root, [data-base-ui-portal]) button:not(:disabled) {
transform-origin: center;
transition-property: color, background-color, border-color, box-shadow, filter, transform;
transition-duration: 140ms;
transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
button:not(:disabled):active {
:where(#root, [data-base-ui-portal]) button:not(:disabled):active {
transform: scale(0.97);
filter: brightness(0.94);
box-shadow: none;
}
@media (prefers-reduced-motion: reduce) {
button:not(:disabled) {
:where(#root, [data-base-ui-portal]) button:not(:disabled) {
transition-duration: 0.01ms;
}
button:not(:disabled):active {
:where(#root, [data-base-ui-portal]) button:not(:disabled):active {
transform: none;
filter: brightness(0.9);
box-shadow: none;
}
}
html {
@apply font-sans;
#root, [data-base-ui-portal] {
@apply font-sans text-foreground antialiased;
line-height: 1.5;
-webkit-text-size-adjust: 100%;
}
body {
#root {
@apply bg-background text-foreground antialiased;
font-feature-settings: "ss01", "cv11";
}
h1, h2, h3, h4 {
:where(#root, [data-base-ui-portal]) :is(h1, h2, h3, h4) {
font-family: var(--font-heading);
font-optical-sizing: auto;
letter-spacing: -0.015em;
}
code, pre, kbd, samp {
:where(#root, [data-base-ui-portal]) :is(code, pre, kbd, samp) {
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
}
::selection {
:where(#root, [data-base-ui-portal]) ::selection {
background: oklch(var(--primary) / 0.16);
}
/* Thin, brand-tinted scrollbars */
* {
:where(#root, [data-base-ui-portal]) * {
scrollbar-width: thin;
scrollbar-color: oklch(var(--border)) transparent;
}
*::-webkit-scrollbar {
:where(#root, [data-base-ui-portal]) *::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-thumb {
:where(#root, [data-base-ui-portal]) *::-webkit-scrollbar-thumb {
background: oklch(var(--border));
border-radius: 9999px;
border: 2px solid transparent;
background-clip: padding-box;
}
*::-webkit-scrollbar-thumb:hover {
:where(#root, [data-base-ui-portal]) *::-webkit-scrollbar-thumb:hover {
background: oklch(var(--muted-foreground) / 0.5);
}
}
@@ -181,6 +190,70 @@
* Scoped so it never leaks into widgets.
*/
@layer components {
.thot-host {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
height: var(--thoth-app-height, 100dvh);
color: oklch(var(--foreground));
background: oklch(var(--background));
color-scheme: light;
}
.thot-host[data-theme="dark"] { color-scheme: dark; }
.thot-host--full { --thot-shell-gutter: max(20px, 1.5rem); }
.thot-host--full > .thot-context-layout {
margin-inline: var(--thot-shell-gutter);
border-inline: 1px solid oklch(var(--border));
}
.thot-host > .thot-context-layout { flex: 1; min-height: 0; height: auto; }
.thot-full-header {
display: flex;
flex: none;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem 1rem;
min-height: 3.75rem;
padding: 0.625rem var(--thot-shell-gutter);
border-bottom: 1px solid oklch(var(--border));
background: oklch(var(--card));
}
.thot-full-header__brand { font: 600 1.25rem/1.2 var(--font-heading); }
.thot-full-header__controls { display: flex; align-items: center; flex-wrap: wrap; gap: 0.25rem; min-width: 0; }
.thot-full-header__user { max-width: 15rem; min-width: 0; font-size: 0.8125rem; }
.thot-full-header__error { flex-basis: 100%; font-size: 0.8125rem; color: oklch(var(--destructive)); }
.thot-shell-select {
min-height: 2rem;
max-width: 9rem;
padding: 0.25rem 0.5rem;
border: 1px solid oklch(var(--input));
border-radius: var(--radius);
background: oklch(var(--background));
color: oklch(var(--foreground));
font: 600 0.8125rem/1.5 var(--font-sans);
}
.thot-shell-select:focus-visible { outline: 3px solid oklch(var(--ring) / 0.5); outline-offset: 2px; }
.thot-user-menu {
z-index: 50;
min-width: 10rem;
padding: 0.25rem;
border: 1px solid oklch(var(--border));
border-radius: var(--radius);
background: oklch(var(--popover));
color: oklch(var(--popover-foreground));
box-shadow: var(--shadow-md);
font: 500 0.875rem/1.5 var(--font-sans);
outline: none;
}
.thot-user-menu__item { padding: 0.375rem 0.5rem; border-radius: 0.25rem; cursor: default; outline: none; }
.thot-user-menu__item[data-highlighted] { background: oklch(var(--accent)); color: oklch(var(--accent-foreground)); }
.thot-access-state { flex: 1; display: grid; align-content: center; justify-items: center; gap: 1rem; padding: 2rem max(20px, 1.5rem); color: oklch(var(--foreground)); background: oklch(var(--background)); }
@media (max-width: 480px) {
.thot-full-header__controls { flex: 1; justify-content: flex-end; }
.thot-full-header__user { max-width: 9rem; }
}
/* Omics Portal and Thoth share one document in the embedded deployment.
Keep rail geometry on product-specific class names so Hyper/Bootstrap's
global spacing utilities (for example .px-4 !important) cannot resize it. */
@@ -322,8 +395,19 @@
50% { transform: scale(1.15); box-shadow: 0 0 0 5px oklch(var(--primary) / 0); }
}
/* Shiki emits both palettes; select its dark values without rebuilding SQL. */
:is(.dark, [data-bs-theme="dark"]) .shiki,
:is(.dark, [data-bs-theme="dark"]) .shiki span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
@layer components {
.thot-database-grid {
.thot-database-grid, .thot-preview-grid,
:is(.thot-database-grid, .thot-preview-grid) .ag-theme-alpine {
--ag-font-family: var(--font-sans);
--ag-font-size: 0.8125rem;
--ag-background-color: oklch(var(--card));