Skip to content

Commit 0fa71c0

Browse files
committed
Polish Meridian frontend motion and drop Tailwind build
Replace the Tailwind dependency with a static preflight stylesheet (the compiled output only ever contributed the reset) and land the motion pass across the showcase and stage surfaces: - Modal enter/exit transitions and thinking-bubble exit in the chat transcript and traveler context panel - RLS probe bar animates via transform instead of width - Recovery route map paths draw on with dash-flow motion - Trace hero bars scale in from the left edge - Shared prefers-reduced-motion helper gates every new animation
1 parent ac17dc5 commit 0fa71c0

15 files changed

Lines changed: 422 additions & 746 deletions

meridian/frontend/package-lock.json

Lines changed: 12 additions & 663 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

meridian/frontend/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
"fast-check": "^3.15.0",
4444
"jsdom": "^23.0.0",
4545
"postcss": "^8.4.32",
46-
"tailwindcss": "^3.4.0",
4746
"typescript": "^5.3.0",
4847
"vite": "^7.3.1",
4948
"vitest": "^4.1.0"
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export default {
22
plugins: {
3-
tailwindcss: {},
43
autoprefixer: {},
54
},
65
}

meridian/frontend/src/index.css

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
@tailwind base;
2-
@tailwind components;
3-
@tailwind utilities;
1+
@import './preflight.css';
42

53
* {
64
box-sizing: border-box;
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/*
2+
* Static reset extracted from Tailwind CSS v3.4.19 Preflight
3+
* (MIT License | https://tailwindcss.com) when the Tailwind build
4+
* dependency was removed. The app's layout was built on these rules,
5+
* so they are kept byte-equivalent rather than replaced with a
6+
* different reset. The two utility classes the markup actually used
7+
* (.sr-only, .bg-app) are inlined at the bottom.
8+
*/
9+
10+
*,
11+
::before,
12+
::after {
13+
box-sizing: border-box;
14+
border-width: 0;
15+
border-style: solid;
16+
border-color: #e5e7eb;
17+
}
18+
19+
html,
20+
:host {
21+
line-height: 1.5;
22+
-webkit-text-size-adjust: 100%;
23+
tab-size: 4;
24+
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
25+
"Segoe UI Symbol", "Noto Color Emoji";
26+
font-feature-settings: normal;
27+
font-variation-settings: normal;
28+
-webkit-tap-highlight-color: transparent;
29+
}
30+
31+
body {
32+
margin: 0;
33+
line-height: inherit;
34+
}
35+
36+
hr {
37+
height: 0;
38+
color: inherit;
39+
border-top-width: 1px;
40+
}
41+
42+
abbr:where([title]) {
43+
text-decoration: underline dotted;
44+
}
45+
46+
h1,
47+
h2,
48+
h3,
49+
h4,
50+
h5,
51+
h6 {
52+
font-size: inherit;
53+
font-weight: inherit;
54+
}
55+
56+
a {
57+
color: inherit;
58+
text-decoration: inherit;
59+
}
60+
61+
b,
62+
strong {
63+
font-weight: bolder;
64+
}
65+
66+
code,
67+
kbd,
68+
samp,
69+
pre {
70+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
71+
"Courier New", monospace;
72+
font-feature-settings: normal;
73+
font-variation-settings: normal;
74+
font-size: 1em;
75+
}
76+
77+
small {
78+
font-size: 80%;
79+
}
80+
81+
sub,
82+
sup {
83+
font-size: 75%;
84+
line-height: 0;
85+
position: relative;
86+
vertical-align: baseline;
87+
}
88+
89+
sub {
90+
bottom: -0.25em;
91+
}
92+
93+
sup {
94+
top: -0.5em;
95+
}
96+
97+
table {
98+
text-indent: 0;
99+
border-color: inherit;
100+
border-collapse: collapse;
101+
}
102+
103+
button,
104+
input,
105+
optgroup,
106+
select,
107+
textarea {
108+
font-family: inherit;
109+
font-feature-settings: inherit;
110+
font-variation-settings: inherit;
111+
font-size: 100%;
112+
font-weight: inherit;
113+
line-height: inherit;
114+
letter-spacing: inherit;
115+
color: inherit;
116+
margin: 0;
117+
padding: 0;
118+
}
119+
120+
button,
121+
select {
122+
text-transform: none;
123+
}
124+
125+
button,
126+
input:where([type='button']),
127+
input:where([type='reset']),
128+
input:where([type='submit']) {
129+
-webkit-appearance: button;
130+
background-color: transparent;
131+
background-image: none;
132+
}
133+
134+
:-moz-focusring {
135+
outline: auto;
136+
}
137+
138+
:-moz-ui-invalid {
139+
box-shadow: none;
140+
}
141+
142+
progress {
143+
vertical-align: baseline;
144+
}
145+
146+
::-webkit-inner-spin-button,
147+
::-webkit-outer-spin-button {
148+
height: auto;
149+
}
150+
151+
[type='search'] {
152+
-webkit-appearance: textfield;
153+
outline-offset: -2px;
154+
}
155+
156+
::-webkit-search-decoration {
157+
-webkit-appearance: none;
158+
}
159+
160+
::-webkit-file-upload-button {
161+
-webkit-appearance: button;
162+
font: inherit;
163+
}
164+
165+
summary {
166+
display: list-item;
167+
}
168+
169+
blockquote,
170+
dl,
171+
dd,
172+
h1,
173+
h2,
174+
h3,
175+
h4,
176+
h5,
177+
h6,
178+
hr,
179+
figure,
180+
p,
181+
pre {
182+
margin: 0;
183+
}
184+
185+
fieldset {
186+
margin: 0;
187+
padding: 0;
188+
}
189+
190+
legend {
191+
padding: 0;
192+
}
193+
194+
ol,
195+
ul,
196+
menu {
197+
list-style: none;
198+
margin: 0;
199+
padding: 0;
200+
}
201+
202+
dialog {
203+
padding: 0;
204+
}
205+
206+
textarea {
207+
resize: vertical;
208+
}
209+
210+
input::placeholder,
211+
textarea::placeholder {
212+
opacity: 1;
213+
color: #9ca3af;
214+
}
215+
216+
button,
217+
[role="button"] {
218+
cursor: pointer;
219+
}
220+
221+
:disabled {
222+
cursor: default;
223+
}
224+
225+
img,
226+
svg,
227+
video,
228+
canvas,
229+
audio,
230+
iframe,
231+
embed,
232+
object {
233+
display: block;
234+
vertical-align: middle;
235+
}
236+
237+
img,
238+
video {
239+
max-width: 100%;
240+
height: auto;
241+
}
242+
243+
[hidden]:where(:not([hidden="until-found"])) {
244+
display: none;
245+
}
246+
247+
.sr-only {
248+
position: absolute;
249+
width: 1px;
250+
height: 1px;
251+
padding: 0;
252+
margin: -1px;
253+
overflow: hidden;
254+
clip: rect(0, 0, 0, 0);
255+
white-space: nowrap;
256+
border-width: 0;
257+
}
258+
259+
.bg-app {
260+
background-color: #f6f5f0;
261+
}

meridian/frontend/src/showcase/components/ChatTranscript.tsx

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,7 @@ import { typewriterCadence } from '../lib/streamingCadence';
2020
import { RankDeltaBadge } from './RankDeltaBadge';
2121
import { TripResultCardContent } from './TripResultCardContent';
2222
import { resultRankLabel } from '../lib/resultRankLabel';
23-
24-
// Respect reduced motion by disabling spring-style reorder animations.
25-
const prefersReducedMotion =
26-
typeof window !== 'undefined' &&
27-
typeof window.matchMedia === 'function' &&
28-
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
23+
import { prefersReducedMotion } from '../lib/prefersReducedMotion';
2924

3025
// Keep a malformed markdown response from taking down the transcript.
3126
class MarkdownBoundary extends Component<
@@ -116,15 +111,29 @@ export function ChatTranscript({
116111
/>
117112
))
118113
)}
119-
{state.isLoading && (
120-
<div className="mds-message bot">
121-
<div className="mds-message-role">Meridian</div>
122-
<div className="mds-message-bubble is-thinking">
123-
<span className="mds-running-dot" />
124-
<ThinkingTicker phase={state.phaseLabel} />
125-
</div>
126-
</div>
127-
)}
114+
<AnimatePresence>
115+
{state.isLoading && (
116+
<motion.div
117+
className="mds-message bot"
118+
// Entry stays with the shared mds-msg-in CSS; Motion only owns the
119+
// exit so the bubble cross-fades into the arriving reply.
120+
initial={false}
121+
exit={{
122+
opacity: 0,
123+
transform: 'translateY(-6px)',
124+
transition: prefersReducedMotion
125+
? { duration: 0 }
126+
: { duration: 0.18, ease: 'easeOut' },
127+
}}
128+
>
129+
<div className="mds-message-role">Meridian</div>
130+
<div className="mds-message-bubble is-thinking">
131+
<span className="mds-running-dot" />
132+
<ThinkingTicker phase={state.phaseLabel} />
133+
</div>
134+
</motion.div>
135+
)}
136+
</AnimatePresence>
128137
</div>
129138
);
130139
}

meridian/frontend/src/showcase/components/RecoveryRouteMap.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,17 @@ function AirportMarker({
3535
code,
3636
coordinates,
3737
labelOffset,
38+
end,
3839
}: {
3940
code: string;
4041
coordinates: [number, number];
4142
labelOffset: number;
43+
end: 'origin' | 'destination';
4244
}) {
4345
const [x, y] = projected(coordinates);
4446

4547
return (
46-
<g transform={`translate(${x} ${y})`}>
48+
<g className={`mds-route-airport is-${end}`} transform={`translate(${x} ${y})`}>
4749
<circle className="mds-route-marker-halo" r={7} />
4850
<circle className="mds-route-marker" r={3.25} />
4951
<text
@@ -82,10 +84,10 @@ export function RecoveryRouteMap() {
8284
d={path(country) ?? undefined}
8385
/>
8486
))}
85-
<path className="mds-route-line-glow" d={routePath} />
87+
<path className="mds-route-line-glow" d={routePath} pathLength={1} />
8688
<path className="mds-route-line" d={routePath} />
87-
<AirportMarker code="JFK" coordinates={JFK} labelOffset={-8} />
88-
<AirportMarker code="TYO" coordinates={TOKYO} labelOffset={8} />
89+
<AirportMarker code="JFK" coordinates={JFK} labelOffset={-8} end="origin" />
90+
<AirportMarker code="TYO" coordinates={TOKYO} labelOffset={8} end="destination" />
8991
</svg>
9092
<span className="mds-route-map-caption">JFK / NORTH PACIFIC / TYO</span>
9193
</div>

meridian/frontend/src/showcase/components/RlsProbeCard.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@ import { useEffect, useState } from 'react';
1515
import { motion } from 'framer-motion';
1616
import { BadgeCheck, Database, Fingerprint, RefreshCw, ShieldX } from 'lucide-react';
1717
import { fetchRlsProbe, type RlsProbeResponse } from '../../api/client';
18-
19-
const prefersReducedMotion =
20-
typeof window !== 'undefined' &&
21-
typeof window.matchMedia === 'function' &&
22-
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
18+
import { prefersReducedMotion } from '../lib/prefersReducedMotion';
2319

2420
export function RlsProbeCard({ travelerId }: { travelerId: string }) {
2521
const [data, setData] = useState<RlsProbeResponse | null>(null);
@@ -133,8 +129,12 @@ export function RlsProbeCard({ travelerId }: { travelerId: string }) {
133129
<div className="mds-rls-bar-ghost" aria-hidden="true" />
134130
<motion.div
135131
className="mds-rls-bar-scoped"
136-
initial={prefersReducedMotion ? { width: `${scopedPct}%` } : { width: '100%' }}
137-
animate={{ width: `${scopedPct}%` }}
132+
initial={{
133+
transform: prefersReducedMotion
134+
? `translateX(-${100 - scopedPct}%)`
135+
: 'translateX(0%)',
136+
}}
137+
animate={{ transform: `translateX(-${100 - scopedPct}%)` }}
138138
transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.7, ease: 'easeOut' }}
139139
/>
140140
</div>

0 commit comments

Comments
 (0)