@memori.ai/memori-react 8.32.0 → 8.34.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 (64) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.css +351 -0
  3. package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.d.ts +34 -0
  4. package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.js +105 -0
  5. package/dist/components/ChatHistoryDrawer/ChatResumeDrawer.js.map +1 -0
  6. package/dist/components/Header/ChatConsumptionDropdown.d.ts +9 -0
  7. package/dist/components/Header/ChatConsumptionDropdown.js +104 -0
  8. package/dist/components/Header/ChatConsumptionDropdown.js.map +1 -0
  9. package/dist/components/Header/Header.css +122 -19
  10. package/dist/components/Header/Header.js +2 -72
  11. package/dist/components/Header/Header.js.map +1 -1
  12. package/dist/components/MicrophoneButton/MicrophoneButton.css +14 -3
  13. package/dist/components/MicrophoneButton/MicrophoneButton.js +1 -1
  14. package/dist/components/MicrophoneButton/MicrophoneButton.js.map +1 -1
  15. package/dist/components/PositionPopover/PositionPopover.js +8 -2
  16. package/dist/components/PositionPopover/PositionPopover.js.map +1 -1
  17. package/dist/components/ui/Tooltip.js +6 -3
  18. package/dist/components/ui/Tooltip.js.map +1 -1
  19. package/dist/locales/de.json +2 -0
  20. package/dist/locales/en.json +2 -0
  21. package/dist/locales/es.json +2 -0
  22. package/dist/locales/fr.json +2 -0
  23. package/dist/locales/it.json +2 -0
  24. package/dist/version.d.ts +1 -1
  25. package/dist/version.js +1 -1
  26. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.css +351 -0
  27. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.d.ts +34 -0
  28. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.js +102 -0
  29. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.js.map +1 -0
  30. package/esm/components/Header/ChatConsumptionDropdown.d.ts +9 -0
  31. package/esm/components/Header/ChatConsumptionDropdown.js +101 -0
  32. package/esm/components/Header/ChatConsumptionDropdown.js.map +1 -0
  33. package/esm/components/Header/Header.css +122 -19
  34. package/esm/components/Header/Header.js +3 -73
  35. package/esm/components/Header/Header.js.map +1 -1
  36. package/esm/components/MicrophoneButton/MicrophoneButton.css +14 -3
  37. package/esm/components/MicrophoneButton/MicrophoneButton.js +1 -1
  38. package/esm/components/MicrophoneButton/MicrophoneButton.js.map +1 -1
  39. package/esm/components/PositionPopover/PositionPopover.js +9 -3
  40. package/esm/components/PositionPopover/PositionPopover.js.map +1 -1
  41. package/esm/components/ui/Tooltip.js +6 -3
  42. package/esm/components/ui/Tooltip.js.map +1 -1
  43. package/esm/locales/de.json +2 -0
  44. package/esm/locales/en.json +2 -0
  45. package/esm/locales/es.json +2 -0
  46. package/esm/locales/fr.json +2 -0
  47. package/esm/locales/it.json +2 -0
  48. package/esm/version.d.ts +1 -1
  49. package/esm/version.js +1 -1
  50. package/package.json +2 -2
  51. package/src/components/Header/ChatConsumptionDropdown.test.tsx +117 -0
  52. package/src/components/Header/ChatConsumptionDropdown.tsx +275 -0
  53. package/src/components/Header/Header.css +122 -19
  54. package/src/components/Header/Header.stories.tsx +48 -41
  55. package/src/components/Header/Header.tsx +7 -147
  56. package/src/components/MicrophoneButton/MicrophoneButton.css +14 -3
  57. package/src/components/MicrophoneButton/MicrophoneButton.tsx +12 -3
  58. package/src/components/ui/Tooltip.tsx +12 -3
  59. package/src/locales/de.json +2 -0
  60. package/src/locales/en.json +2 -0
  61. package/src/locales/es.json +2 -0
  62. package/src/locales/fr.json +2 -0
  63. package/src/locales/it.json +2 -0
  64. package/src/version.ts +1 -1
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "8.32.0",
2
+ "version": "8.34.0",
3
3
  "name": "@memori.ai/memori-react",
4
4
  "author": "Memori Srl",
5
5
  "main": "dist/index.js",
@@ -297,7 +297,7 @@
297
297
  "@react-three/drei": "8.20.2",
298
298
  "@react-three/fiber": "7.0.25",
299
299
  "classnames": "2.5.1",
300
- "dompurify": "^3.3.3",
300
+ "dompurify": "^3.4.0",
301
301
  "ellipsed": "1.6.0",
302
302
  "i18next": "22.0.6",
303
303
  "katex": "^0.16.11",
@@ -0,0 +1,117 @@
1
+ import React from 'react';
2
+ import { fireEvent, render, screen } from '@testing-library/react';
3
+ import { Message } from '@memori.ai/memori-api-client/dist/types';
4
+ import ChatConsumptionDropdown from './ChatConsumptionDropdown';
5
+
6
+ type TestMessage = Message & {
7
+ llmUsage?: {
8
+ provider?: string;
9
+ model?: string;
10
+ totalInputTokens?: number;
11
+ outputTokens?: number;
12
+ energyImpact?: {
13
+ energy?: number | { parsedValue?: number };
14
+ gwp?: number | { parsedValue?: number; source?: string };
15
+ wcf?: number | { parsedValue?: number; source?: string };
16
+ };
17
+ };
18
+ };
19
+
20
+ describe('ChatConsumptionDropdown', () => {
21
+ it('renders aggregated token and environmental usage', () => {
22
+ const history = [
23
+ {
24
+ text: 'First response',
25
+ timestamp: '2021-03-01T12:00:00.000Z',
26
+ llmUsage: {
27
+ provider: 'OpenAI',
28
+ model: 'gpt-5',
29
+ totalInputTokens: 1000,
30
+ outputTokens: 200,
31
+ energyImpact: {
32
+ energy: { parsedValue: 0.0012 },
33
+ gwp: { parsedValue: 0.00045 },
34
+ wcf: { parsedValue: 0.0021 },
35
+ },
36
+ },
37
+ },
38
+ {
39
+ text: 'Second response',
40
+ timestamp: '2021-03-01T12:01:00.000Z',
41
+ llmUsage: {
42
+ provider: 'Anthropic',
43
+ model: 'claude-3',
44
+ totalInputTokens: 250,
45
+ outputTokens: 50,
46
+ energyImpact: {
47
+ energy: 0.0008,
48
+ gwp: { source: '0.00035' },
49
+ wcf: { source: '0.0014' },
50
+ },
51
+ },
52
+ },
53
+ ] as TestMessage[];
54
+
55
+ render(<ChatConsumptionDropdown history={history} />);
56
+
57
+ fireEvent.click(screen.getByTitle('write_and_speak.showMessageConsumptionLabel'));
58
+
59
+ expect(screen.getByText('chatLogs.totalChatConsumptionTitle')).toBeTruthy();
60
+ expect(screen.getByText('chatLogs.modelUsage')).toBeTruthy();
61
+ expect(screen.getByText('chatLogs.environmentalImpact')).toBeTruthy();
62
+ expect(screen.getByText('1,250')).toBeTruthy();
63
+ expect(screen.getByText('250')).toBeTruthy();
64
+ expect(screen.getByText('OpenAI · gpt-5')).toBeTruthy();
65
+ expect(screen.getByText('Anthropic · claude-3')).toBeTruthy();
66
+ expect(screen.getByText('2 Wh')).toBeTruthy();
67
+ expect(screen.getByText('800 mg')).toBeTruthy();
68
+ expect(screen.getByText('3.5 mL')).toBeTruthy();
69
+ });
70
+
71
+ it('does not render when the chat has no llm usage data', () => {
72
+ const history = [
73
+ {
74
+ text: 'Plain message',
75
+ timestamp: '2021-03-01T12:00:00.000Z',
76
+ },
77
+ ] as TestMessage[];
78
+
79
+ const { container } = render(<ChatConsumptionDropdown history={history} />);
80
+
81
+ expect(container.firstChild).toBeNull();
82
+ expect(
83
+ screen.queryByTitle('write_and_speak.showMessageConsumptionLabel')
84
+ ).toBeNull();
85
+ });
86
+
87
+ it('supports a custom trigger node', () => {
88
+ const history = [
89
+ {
90
+ text: 'First response',
91
+ timestamp: '2021-03-01T12:00:00.000Z',
92
+ llmUsage: {
93
+ provider: 'OpenAI',
94
+ model: 'gpt-5',
95
+ totalInputTokens: 1000,
96
+ outputTokens: 200,
97
+ energyImpact: {
98
+ energy: { parsedValue: 0.0012 },
99
+ gwp: { parsedValue: 0.00045 },
100
+ wcf: { parsedValue: 0.0021 },
101
+ },
102
+ },
103
+ },
104
+ ] as TestMessage[];
105
+
106
+ render(
107
+ <ChatConsumptionDropdown
108
+ history={history}
109
+ trigger={<button type="button">Open usage</button>}
110
+ />
111
+ );
112
+
113
+ fireEvent.click(screen.getByRole('button', { name: 'Open usage' }));
114
+
115
+ expect(screen.getByText('chatLogs.totalChatConsumptionTitle')).toBeTruthy();
116
+ });
117
+ });
@@ -0,0 +1,275 @@
1
+ import React, { useMemo } from 'react';
2
+ import cx from 'classnames';
3
+ import { Message } from '@memori.ai/memori-api-client/dist/types';
4
+ import { useTranslation } from 'react-i18next';
5
+ import Button from '../ui/Button';
6
+ import Dropdown from '../ui/Dropdown';
7
+ import GasStation from '../icons/GasStation';
8
+ import { BADGE_EMOJI } from '../../helpers/llmUsage';
9
+
10
+ type ImpactMetricType = 'energy' | 'co2' | 'water';
11
+
12
+ type LlmUsageEnergyImpact = {
13
+ energy?: number | { source?: string; parsedValue?: number };
14
+ gwp?: number | { source?: string; parsedValue?: number };
15
+ wcf?: number | { source?: string; parsedValue?: number };
16
+ };
17
+
18
+ type MessageLlmUsage = {
19
+ provider?: string;
20
+ model?: string;
21
+ totalInputTokens?: number;
22
+ outputTokens?: number;
23
+ energyImpact?: LlmUsageEnergyImpact;
24
+ };
25
+
26
+ export interface ChatConsumptionDropdownProps {
27
+ history: Message[];
28
+ hasSpacedButtons?: boolean;
29
+ trigger?: React.ReactNode;
30
+ }
31
+
32
+ const getMetricValue = (
33
+ metric?: number | { source?: string; parsedValue?: number }
34
+ ): number | undefined => {
35
+ if (typeof metric === 'number' && Number.isFinite(metric)) return metric;
36
+ if (!metric || typeof metric !== 'object') return undefined;
37
+ if (
38
+ typeof metric.parsedValue === 'number' &&
39
+ Number.isFinite(metric.parsedValue)
40
+ ) {
41
+ return metric.parsedValue;
42
+ }
43
+ if (typeof metric.source === 'string') {
44
+ const parsed = Number(metric.source);
45
+ if (Number.isFinite(parsed)) return parsed;
46
+ }
47
+ return undefined;
48
+ };
49
+
50
+ const formatMetricValue = (value: number, locale: string): string =>
51
+ new Intl.NumberFormat(locale, {
52
+ minimumFractionDigits: 0,
53
+ maximumFractionDigits: Math.abs(value) >= 1 ? 3 : 4,
54
+ }).format(value);
55
+
56
+ const formatCountValue = (value: number, locale: string): string =>
57
+ new Intl.NumberFormat(locale, {
58
+ maximumFractionDigits: 0,
59
+ }).format(value);
60
+
61
+ const formatImpactInReadableUnit = (
62
+ value: number,
63
+ metricType: ImpactMetricType,
64
+ locale: string
65
+ ): string => {
66
+ const absValue = Math.abs(value);
67
+
68
+ if (metricType === 'energy') {
69
+ if (absValue >= 1) return `${formatMetricValue(value, locale)} kWh`;
70
+ const wh = value * 1000;
71
+ if (Math.abs(wh) >= 1) return `${formatMetricValue(wh, locale)} Wh`;
72
+ return `${formatMetricValue(wh * 1000, locale)} mWh`;
73
+ }
74
+
75
+ if (metricType === 'co2') {
76
+ if (absValue >= 1) return `${formatMetricValue(value, locale)} kg`;
77
+ const g = value * 1000;
78
+ if (Math.abs(g) >= 1) return `${formatMetricValue(g, locale)} g`;
79
+ return `${formatMetricValue(g * 1000, locale)} mg`;
80
+ }
81
+
82
+ if (absValue >= 1) return `${formatMetricValue(value, locale)} L`;
83
+ const ml = value * 1000;
84
+ if (Math.abs(ml) >= 1) return `${formatMetricValue(ml, locale)} mL`;
85
+ return `${formatMetricValue(ml * 1000, locale)} μL`;
86
+ };
87
+
88
+ const ChatConsumptionDropdown: React.FC<ChatConsumptionDropdownProps> = ({
89
+ history,
90
+ hasSpacedButtons = false,
91
+ trigger,
92
+ }) => {
93
+ const { t, i18n } = useTranslation();
94
+
95
+ const currentLocale = i18n.language || navigator.language || 'en';
96
+ const chatLog = useMemo(() => ({ lines: history }), [history]);
97
+
98
+ const chatConsumptionTotals = useMemo(() => {
99
+ const totals = {
100
+ totalInputTokens: 0,
101
+ totalOutputTokens: 0,
102
+ energy: 0,
103
+ gwp: 0,
104
+ wcf: 0,
105
+ models: new Set<string>(),
106
+ };
107
+
108
+ (chatLog?.lines ?? []).forEach(line => {
109
+ const llmUsage = (line as Message & {
110
+ llmUsage?: MessageLlmUsage;
111
+ }).llmUsage;
112
+
113
+ if (!llmUsage) return;
114
+
115
+ totals.totalInputTokens += llmUsage.totalInputTokens ?? 0;
116
+ totals.totalOutputTokens += llmUsage.outputTokens ?? 0;
117
+
118
+ if (llmUsage.provider || llmUsage.model) {
119
+ totals.models.add(
120
+ [llmUsage.provider, llmUsage.model].filter(Boolean).join(' · ')
121
+ );
122
+ }
123
+
124
+ if (!llmUsage.energyImpact) return;
125
+
126
+ const impact = llmUsage.energyImpact;
127
+ totals.energy += getMetricValue(impact.energy) ?? 0;
128
+ totals.gwp += getMetricValue(impact.gwp) ?? 0;
129
+ totals.wcf += getMetricValue(impact.wcf) ?? 0;
130
+ });
131
+
132
+ return totals;
133
+ }, [chatLog]);
134
+
135
+ const llmUsageModels = useMemo(
136
+ () => Array.from(chatConsumptionTotals.models),
137
+ [chatConsumptionTotals.models]
138
+ );
139
+
140
+ const hasConsumptionData = useMemo(
141
+ () =>
142
+ (chatLog?.lines ?? []).some(
143
+ line =>
144
+ !!(line as Message & { llmUsage?: MessageLlmUsage }).llmUsage
145
+ ),
146
+ [chatLog]
147
+ );
148
+
149
+ if (!hasConsumptionData) return null;
150
+
151
+ return (
152
+ <Dropdown
153
+ placement="bottom-right"
154
+ trigger={
155
+ trigger ?? (
156
+ <Button
157
+ primary
158
+ shape="circle"
159
+ className={cx(
160
+ 'memori-header--button',
161
+ 'memori-header--button--sustainability',
162
+ hasSpacedButtons && 'memori-header--button-spaced'
163
+ )}
164
+ title={
165
+ t('write_and_speak.showMessageConsumptionLabel') ||
166
+ 'LLM consumption'
167
+ }
168
+ icon={
169
+ <GasStation className="memori-header--button--sustainability-icon" />
170
+ }
171
+ />
172
+ )
173
+ }
174
+ >
175
+ <div className="memori-dropdown--sustainability">
176
+ <h4 className="memori-dropdown--sustainability-title">
177
+ {t('chatLogs.totalChatConsumptionTitle') || 'Consumo Totale Chat'}
178
+ </h4>
179
+ <div className="memori-dropdown--sustainability-section">
180
+ <h5 className="memori-dropdown--sustainability-section-title">
181
+ {t('chatLogs.modelUsage') || 'Model usage'}
182
+ </h5>
183
+ <div className="memori-dropdown--sustainability-summary">
184
+ <div className="memori-dropdown--sustainability-stat">
185
+ <span className="memori-dropdown--sustainability-stat-label">
186
+ {t('chatLogs.input') || 'Input'}
187
+ </span>
188
+ <strong className="memori-dropdown--sustainability-stat-value">
189
+ {formatCountValue(chatConsumptionTotals.totalInputTokens, currentLocale)}
190
+ </strong>
191
+ <span className="memori-dropdown--sustainability-stat-meta">
192
+ {t('chatLogs.tokens') || 'Tokens'}
193
+ </span>
194
+ </div>
195
+ <div className="memori-dropdown--sustainability-stat">
196
+ <span className="memori-dropdown--sustainability-stat-label">
197
+ {t('chatLogs.output') || 'Output'}
198
+ </span>
199
+ <strong className="memori-dropdown--sustainability-stat-value">
200
+ {formatCountValue(chatConsumptionTotals.totalOutputTokens, currentLocale)}
201
+ </strong>
202
+ <span className="memori-dropdown--sustainability-stat-meta">
203
+ {t('chatLogs.tokens') || 'Tokens'}
204
+ </span>
205
+ </div>
206
+ </div>
207
+ {llmUsageModels.length > 0 && (
208
+ <div className="memori-dropdown--sustainability-row memori-dropdown--sustainability-row--stacked">
209
+ <span className="memori-dropdown--sustainability-label">
210
+ {t('chatLogs.provider') || 'Provider'} /{' '}
211
+ {t('chatLogs.model') || 'Model'}
212
+ </span>
213
+ <div className="memori-dropdown--sustainability-tags">
214
+ {llmUsageModels.map(modelLabel => (
215
+ <span
216
+ key={modelLabel}
217
+ className="memori-dropdown--sustainability-tag"
218
+ >
219
+ {modelLabel}
220
+ </span>
221
+ ))}
222
+ </div>
223
+ </div>
224
+ )}
225
+ </div>
226
+ <div className="memori-dropdown--sustainability-metrics">
227
+ <h5 className="memori-dropdown--sustainability-section-title">
228
+ {t('chatLogs.environmentalImpact') || 'Environmental impact'}
229
+ </h5>
230
+ <div className="memori-dropdown--sustainability-row">
231
+ <span className="memori-dropdown--sustainability-label">
232
+ <span aria-hidden="true">{BADGE_EMOJI.energy}</span>{' '}
233
+ {t('chatLogs.energy') || 'Energy'}
234
+ </span>
235
+ <strong className="memori-dropdown--sustainability-value">
236
+ {formatImpactInReadableUnit(
237
+ chatConsumptionTotals.energy,
238
+ 'energy',
239
+ currentLocale
240
+ )}
241
+ </strong>
242
+ </div>
243
+ <div className="memori-dropdown--sustainability-row">
244
+ <span className="memori-dropdown--sustainability-label">
245
+ <span aria-hidden="true">{BADGE_EMOJI.co2}</span>{' '}
246
+ {t('chatLogs.co2') || 'CO2'}
247
+ </span>
248
+ <strong className="memori-dropdown--sustainability-value">
249
+ {formatImpactInReadableUnit(
250
+ chatConsumptionTotals.gwp,
251
+ 'co2',
252
+ currentLocale
253
+ )}
254
+ </strong>
255
+ </div>
256
+ <div className="memori-dropdown--sustainability-row">
257
+ <span className="memori-dropdown--sustainability-label">
258
+ <span aria-hidden="true">{BADGE_EMOJI.water}</span>{' '}
259
+ {t('chatLogs.water') || 'Water'}
260
+ </span>
261
+ <strong className="memori-dropdown--sustainability-value">
262
+ {formatImpactInReadableUnit(
263
+ chatConsumptionTotals.wcf,
264
+ 'water',
265
+ currentLocale
266
+ )}
267
+ </strong>
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </Dropdown>
272
+ );
273
+ };
274
+
275
+ export default ChatConsumptionDropdown;
@@ -105,12 +105,11 @@
105
105
  margin-right: 0;
106
106
  }
107
107
 
108
- .memori-header--button--sustainability{
108
+ .memori-header--button--sustainability {
109
109
  max-height: 37px;
110
110
  }
111
111
 
112
-
113
- .memori-dropdown--avatar-input{
112
+ .memori-dropdown--avatar-input {
114
113
  position: absolute;
115
114
  top: 20px;
116
115
  left: 15px;
@@ -120,60 +119,164 @@
120
119
  opacity: 0;
121
120
  }
122
121
 
123
-
124
-
125
- .memori-dropdown--avatar:hover, .memori-dropdown--avatar-initial:hover, .memori-dropdown--avatar-input:hover{
122
+ .memori-dropdown--avatar:hover,
123
+ .memori-dropdown--avatar-initial:hover,
124
+ .memori-dropdown--avatar-input:hover {
126
125
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
127
126
  cursor: pointer;
128
127
  pointer-events: cursor;
129
128
  transform: scale(1.05);
130
129
  }
131
130
 
132
- .memori-dropdown--avatar-initial:hover + .memori-dropdown--avatar-input,.memori-dropdown--avatar:hover + .memori-dropdown--avatar-input{
131
+ .memori-dropdown--avatar-initial:hover + .memori-dropdown--avatar-input,
132
+ .memori-dropdown--avatar:hover + .memori-dropdown--avatar-input {
133
133
  display: block;
134
134
  }
135
135
 
136
136
  .memori-dropdown--sustainability {
137
- min-width: 260px;
138
- padding: 0.625rem 0.75rem;
139
- border-radius: 8px;
140
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(247, 249, 252, 0.96) 100%);
137
+ min-width: 300px;
138
+ padding: 0.85rem;
139
+ border: 1px solid color-mix(in srgb, var(--memori-button-border-color, #d9d9d9) 75%, white);
140
+ border-radius: 12px;
141
+ background: linear-gradient(180deg, rgba(252, 253, 255, 0.98) 0%, rgba(245, 248, 252, 0.98) 100%);
142
+ box-shadow: 0 14px 34px rgba(15, 23, 42, 0.1);
141
143
  }
142
144
 
143
145
  .memori-dropdown--sustainability-title {
144
- margin: 0 0 0.5rem 0;
146
+ padding-bottom: 12px;
147
+ border-bottom: 1px solid rgba(15, 23, 42, 0.08);
148
+ margin: 0.2rem 0.2rem 1rem;
149
+ color: color-mix(in srgb, var(--memori-text-color, #111827) 92%, #36506b);
145
150
  font-size: 0.95rem;
146
151
  font-weight: 700;
147
152
  line-height: 1.2;
148
- text-align: center;
153
+ text-align: left;
149
154
  }
150
155
 
156
+ .memori-dropdown--sustainability-section,
151
157
  .memori-dropdown--sustainability-metrics {
152
158
  display: flex;
153
159
  flex-direction: column;
154
160
  gap: 0.35rem;
155
161
  }
156
162
 
163
+ .memori-dropdown--sustainability-section-title {
164
+ margin: 0 0 0.1rem;
165
+ color: color-mix(in srgb, var(--memori-text-color, #111827) 62%, #64748b);
166
+ font-size: 0.71rem;
167
+ font-weight: 700;
168
+ letter-spacing: 0.06em;
169
+ line-height: 1.2;
170
+ text-transform: uppercase;
171
+ }
172
+
173
+ .memori-dropdown--sustainability-section {
174
+ padding-bottom: 0.65rem;
175
+ margin-bottom: 0.65rem;
176
+ /* border-bottom: 1px solid rgba(15, 23, 42, 0.08); */
177
+ }
178
+
179
+ .memori-dropdown--sustainability-summary {
180
+ display: grid;
181
+ gap: 0.5rem;
182
+ grid-template-columns: repeat(2, minmax(0, 1fr));
183
+ }
184
+
185
+ .memori-dropdown--sustainability-stat {
186
+ display: flex;
187
+ flex-direction: column;
188
+ padding: 0.55rem 0.65rem;
189
+ border: 1px solid rgba(15, 23, 42, 0.07);
190
+ border-radius: 10px;
191
+ background: rgba(255, 255, 255, 0.78);
192
+ gap: 0.15rem;
193
+ }
194
+
195
+ .memori-dropdown--sustainability-stat-label,
196
+ .memori-dropdown--sustainability-stat-meta {
197
+ font-size: 0.72rem;
198
+ line-height: 1.2;
199
+ }
200
+
201
+ .memori-dropdown--sustainability-stat-label {
202
+ color: color-mix(in srgb, var(--memori-text-color, #111827) 68%, #6b7280);
203
+ font-weight: 600;
204
+ letter-spacing: 0.02em;
205
+ }
206
+
207
+ .memori-dropdown--sustainability-stat-value {
208
+ color: color-mix(in srgb, var(--memori-text-color, #111827) 94%, #1d4ed8);
209
+ font-size: 1rem;
210
+ font-weight: 700;
211
+ line-height: 1.1;
212
+ }
213
+
214
+ .memori-dropdown--sustainability-stat-meta {
215
+ color: color-mix(in srgb, var(--memori-text-color, #111827) 55%, #64748b);
216
+ }
217
+
157
218
  .memori-dropdown--sustainability-row {
158
219
  display: flex;
159
220
  align-items: center;
160
221
  justify-content: space-between;
161
- padding: 0.4rem 0.5rem;
162
- border-radius: 6px;
163
- background: rgba(255, 255, 255, 0.8);
222
+ padding: 0.5rem 0.6rem;
223
+ border: 1px solid rgba(15, 23, 42, 0.05);
224
+ border-radius: 10px;
225
+ background: rgba(255, 255, 255, 0.72);
164
226
  gap: 1rem;
165
227
  }
166
228
 
229
+ .memori-dropdown--sustainability-row--stacked {
230
+ flex-direction: column;
231
+ align-items: flex-start;
232
+ gap: 0.45rem;
233
+ }
234
+
167
235
  .memori-dropdown--sustainability-label {
168
236
  display: inline-flex;
169
237
  align-items: center;
238
+ color: color-mix(in srgb, var(--memori-text-color, #111827) 76%, #64748b);
170
239
  font-size: 0.82rem;
171
- font-weight: 500;
240
+ font-weight: 600;
172
241
  gap: 0.35rem;
173
- opacity: 0.9;
174
242
  }
175
243
 
176
244
  .memori-dropdown--sustainability-value {
177
- font-size: 0.85rem;
245
+ color: color-mix(in srgb, var(--memori-text-color, #111827) 96%, black);
246
+ font-size: 0.88rem;
247
+ font-weight: 700;
178
248
  white-space: nowrap;
179
249
  }
250
+
251
+ .memori-dropdown--sustainability-value--multiline {
252
+ width: 100%;
253
+ overflow-wrap: anywhere;
254
+ white-space: normal;
255
+ }
256
+
257
+ .memori-dropdown--sustainability-tags {
258
+ display: flex;
259
+ width: 100%;
260
+ flex-wrap: wrap;
261
+ gap: 0.35rem;
262
+ }
263
+
264
+ .memori-dropdown--sustainability-tag {
265
+ display: inline-flex;
266
+ min-height: 1.75rem;
267
+ align-items: center;
268
+ padding: 0.2rem 0.55rem;
269
+ /* border: 1px solid rgba(15, 23, 42, 0.08); */
270
+ border-radius: 999px;
271
+ background: rgba(255, 255, 255, 0.88);
272
+ color: color-mix(in srgb, var(--memori-text-color, #111827) 85%, #475569);
273
+ font-size: 0.76rem;
274
+ font-weight: 600;
275
+ line-height: 1.1;
276
+ }
277
+
278
+ @media (max-width: 480px) {
279
+ .memori-dropdown--sustainability {
280
+ min-width: min(300px, calc(100vw - 2rem));
281
+ }
282
+ }