@memori.ai/memori-react 8.31.0 → 8.33.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 (71) hide show
  1. package/CHANGELOG.md +21 -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 +8 -0
  7. package/dist/components/Header/ChatConsumptionDropdown.js +103 -0
  8. package/dist/components/Header/ChatConsumptionDropdown.js.map +1 -0
  9. package/dist/components/Header/Header.css +128 -17
  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/icons/GasStation.d.ts +6 -0
  18. package/dist/components/icons/GasStation.js +6 -0
  19. package/dist/components/icons/GasStation.js.map +1 -0
  20. package/dist/components/ui/Tooltip.js +6 -3
  21. package/dist/components/ui/Tooltip.js.map +1 -1
  22. package/dist/locales/de.json +3 -0
  23. package/dist/locales/en.json +3 -0
  24. package/dist/locales/es.json +3 -0
  25. package/dist/locales/fr.json +3 -0
  26. package/dist/locales/it.json +3 -0
  27. package/dist/version.d.ts +1 -1
  28. package/dist/version.js +1 -1
  29. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.css +351 -0
  30. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.d.ts +34 -0
  31. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.js +102 -0
  32. package/esm/components/ChatHistoryDrawer/ChatResumeDrawer.js.map +1 -0
  33. package/esm/components/Header/ChatConsumptionDropdown.d.ts +8 -0
  34. package/esm/components/Header/ChatConsumptionDropdown.js +100 -0
  35. package/esm/components/Header/ChatConsumptionDropdown.js.map +1 -0
  36. package/esm/components/Header/Header.css +128 -17
  37. package/esm/components/Header/Header.js +3 -73
  38. package/esm/components/Header/Header.js.map +1 -1
  39. package/esm/components/MicrophoneButton/MicrophoneButton.css +14 -3
  40. package/esm/components/MicrophoneButton/MicrophoneButton.js +1 -1
  41. package/esm/components/MicrophoneButton/MicrophoneButton.js.map +1 -1
  42. package/esm/components/PositionPopover/PositionPopover.js +9 -3
  43. package/esm/components/PositionPopover/PositionPopover.js.map +1 -1
  44. package/esm/components/icons/GasStation.d.ts +6 -0
  45. package/esm/components/icons/GasStation.js +4 -0
  46. package/esm/components/icons/GasStation.js.map +1 -0
  47. package/esm/components/ui/Tooltip.js +6 -3
  48. package/esm/components/ui/Tooltip.js.map +1 -1
  49. package/esm/locales/de.json +3 -0
  50. package/esm/locales/en.json +3 -0
  51. package/esm/locales/es.json +3 -0
  52. package/esm/locales/fr.json +3 -0
  53. package/esm/locales/it.json +3 -0
  54. package/esm/version.d.ts +1 -1
  55. package/esm/version.js +1 -1
  56. package/package.json +2 -2
  57. package/src/components/Header/ChatConsumptionDropdown.test.tsx +86 -0
  58. package/src/components/Header/ChatConsumptionDropdown.tsx +266 -0
  59. package/src/components/Header/Header.css +128 -17
  60. package/src/components/Header/Header.stories.tsx +48 -41
  61. package/src/components/Header/Header.tsx +7 -147
  62. package/src/components/MicrophoneButton/MicrophoneButton.css +14 -3
  63. package/src/components/MicrophoneButton/MicrophoneButton.tsx +12 -3
  64. package/src/components/icons/GasStation.tsx +36 -0
  65. package/src/components/ui/Tooltip.tsx +12 -3
  66. package/src/locales/de.json +3 -0
  67. package/src/locales/en.json +3 -0
  68. package/src/locales/es.json +3 -0
  69. package/src/locales/fr.json +3 -0
  70. package/src/locales/it.json +3 -0
  71. package/src/version.ts +1 -1
@@ -418,6 +418,9 @@
418
418
  "co2": "CO2",
419
419
  "water": "Eau",
420
420
  "usageBadgesHint": "Cliquez sur un de ces boutons pour afficher plus d'informations",
421
+ "totalChatConsumptionTitle": "Consommation Totale du Chat",
422
+ "modelUsage": "Utilisation du modèle",
423
+ "environmentalImpact": "Impact environnemental",
421
424
  "impactComparisonUnavailable": "Comparaison indicative non disponible.",
422
425
  "impactComparisonEnergy": "Comparaison indicative : environ comme laisser allumee une ampoule LED de 10 W pendant {{duration}}.",
423
426
  "impactComparisonCo2": "Comparaison indicative : environ {{distance}} parcourus en voiture essence moyenne.",
@@ -450,6 +450,9 @@
450
450
  "co2": "CO2",
451
451
  "water": "Acqua",
452
452
  "usageBadgesHint": "Clicca uno di questi pulsanti per mostrare maggiori informazioni",
453
+ "totalChatConsumptionTitle": "Consumo Totale Chat",
454
+ "modelUsage": "Utilizzo del modello",
455
+ "environmentalImpact": "Impatto ambientale",
453
456
  "impactComparisonUnavailable": "Confronto indicativo non disponibile.",
454
457
  "impactComparisonEnergy": "Confronto indicativo: circa quanto tenere accesa una lampadina LED da 10 W per {{duration}}.",
455
458
  "impactComparisonCo2": "Confronto indicativo: circa {{distance}} percorsi in auto a benzina media.",
package/esm/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "8.31.0";
1
+ export declare const version = "8.33.0";
package/esm/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const version = '8.31.0';
1
+ export const version = '8.33.0';
2
2
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "8.31.0",
2
+ "version": "8.33.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,86 @@
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
+ });
@@ -0,0 +1,266 @@
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
+ }
30
+
31
+ const getMetricValue = (
32
+ metric?: number | { source?: string; parsedValue?: number }
33
+ ): number | undefined => {
34
+ if (typeof metric === 'number' && Number.isFinite(metric)) return metric;
35
+ if (!metric || typeof metric !== 'object') return undefined;
36
+ if (
37
+ typeof metric.parsedValue === 'number' &&
38
+ Number.isFinite(metric.parsedValue)
39
+ ) {
40
+ return metric.parsedValue;
41
+ }
42
+ if (typeof metric.source === 'string') {
43
+ const parsed = Number(metric.source);
44
+ if (Number.isFinite(parsed)) return parsed;
45
+ }
46
+ return undefined;
47
+ };
48
+
49
+ const formatMetricValue = (value: number, locale: string): string =>
50
+ new Intl.NumberFormat(locale, {
51
+ minimumFractionDigits: 0,
52
+ maximumFractionDigits: Math.abs(value) >= 1 ? 3 : 4,
53
+ }).format(value);
54
+
55
+ const formatCountValue = (value: number, locale: string): string =>
56
+ new Intl.NumberFormat(locale, {
57
+ maximumFractionDigits: 0,
58
+ }).format(value);
59
+
60
+ const formatImpactInReadableUnit = (
61
+ value: number,
62
+ metricType: ImpactMetricType,
63
+ locale: string
64
+ ): string => {
65
+ const absValue = Math.abs(value);
66
+
67
+ if (metricType === 'energy') {
68
+ if (absValue >= 1) return `${formatMetricValue(value, locale)} kWh`;
69
+ const wh = value * 1000;
70
+ if (Math.abs(wh) >= 1) return `${formatMetricValue(wh, locale)} Wh`;
71
+ return `${formatMetricValue(wh * 1000, locale)} mWh`;
72
+ }
73
+
74
+ if (metricType === 'co2') {
75
+ if (absValue >= 1) return `${formatMetricValue(value, locale)} kg`;
76
+ const g = value * 1000;
77
+ if (Math.abs(g) >= 1) return `${formatMetricValue(g, locale)} g`;
78
+ return `${formatMetricValue(g * 1000, locale)} mg`;
79
+ }
80
+
81
+ if (absValue >= 1) return `${formatMetricValue(value, locale)} L`;
82
+ const ml = value * 1000;
83
+ if (Math.abs(ml) >= 1) return `${formatMetricValue(ml, locale)} mL`;
84
+ return `${formatMetricValue(ml * 1000, locale)} μL`;
85
+ };
86
+
87
+ const ChatConsumptionDropdown: React.FC<ChatConsumptionDropdownProps> = ({
88
+ history,
89
+ hasSpacedButtons = false,
90
+ }) => {
91
+ const { t, i18n } = useTranslation();
92
+
93
+ const currentLocale = i18n.language || navigator.language || 'en';
94
+ const chatLog = useMemo(() => ({ lines: history }), [history]);
95
+
96
+ const chatConsumptionTotals = useMemo(() => {
97
+ const totals = {
98
+ totalInputTokens: 0,
99
+ totalOutputTokens: 0,
100
+ energy: 0,
101
+ gwp: 0,
102
+ wcf: 0,
103
+ models: new Set<string>(),
104
+ };
105
+
106
+ (chatLog?.lines ?? []).forEach(line => {
107
+ const llmUsage = (line as Message & {
108
+ llmUsage?: MessageLlmUsage;
109
+ }).llmUsage;
110
+
111
+ if (!llmUsage) return;
112
+
113
+ totals.totalInputTokens += llmUsage.totalInputTokens ?? 0;
114
+ totals.totalOutputTokens += llmUsage.outputTokens ?? 0;
115
+
116
+ if (llmUsage.provider || llmUsage.model) {
117
+ totals.models.add(
118
+ [llmUsage.provider, llmUsage.model].filter(Boolean).join(' · ')
119
+ );
120
+ }
121
+
122
+ if (!llmUsage.energyImpact) return;
123
+
124
+ const impact = llmUsage.energyImpact;
125
+ totals.energy += getMetricValue(impact.energy) ?? 0;
126
+ totals.gwp += getMetricValue(impact.gwp) ?? 0;
127
+ totals.wcf += getMetricValue(impact.wcf) ?? 0;
128
+ });
129
+
130
+ return totals;
131
+ }, [chatLog]);
132
+
133
+ const llmUsageModels = useMemo(
134
+ () => Array.from(chatConsumptionTotals.models),
135
+ [chatConsumptionTotals.models]
136
+ );
137
+
138
+ const hasConsumptionData = useMemo(
139
+ () =>
140
+ (chatLog?.lines ?? []).some(
141
+ line =>
142
+ !!(line as Message & { llmUsage?: MessageLlmUsage }).llmUsage
143
+ ),
144
+ [chatLog]
145
+ );
146
+
147
+ if (!hasConsumptionData) return null;
148
+
149
+ return (
150
+ <Dropdown
151
+ placement="bottom-right"
152
+ trigger={
153
+ <Button
154
+ primary
155
+ shape="circle"
156
+ className={cx(
157
+ 'memori-header--button',
158
+ 'memori-header--button--sustainability',
159
+ hasSpacedButtons && 'memori-header--button-spaced'
160
+ )}
161
+ title={t('write_and_speak.showMessageConsumptionLabel') || 'LLM consumption'}
162
+ icon={<GasStation className="memori-header--button--sustainability-icon" />}
163
+ />
164
+ }
165
+ >
166
+ <div className="memori-dropdown--sustainability">
167
+ <h4 className="memori-dropdown--sustainability-title">
168
+ {t('chatLogs.totalChatConsumptionTitle') || 'Consumo Totale Chat'}
169
+ </h4>
170
+ <div className="memori-dropdown--sustainability-section">
171
+ <h5 className="memori-dropdown--sustainability-section-title">
172
+ {t('chatLogs.modelUsage') || 'Model usage'}
173
+ </h5>
174
+ <div className="memori-dropdown--sustainability-summary">
175
+ <div className="memori-dropdown--sustainability-stat">
176
+ <span className="memori-dropdown--sustainability-stat-label">
177
+ {t('chatLogs.input') || 'Input'}
178
+ </span>
179
+ <strong className="memori-dropdown--sustainability-stat-value">
180
+ {formatCountValue(chatConsumptionTotals.totalInputTokens, currentLocale)}
181
+ </strong>
182
+ <span className="memori-dropdown--sustainability-stat-meta">
183
+ {t('chatLogs.tokens') || 'Tokens'}
184
+ </span>
185
+ </div>
186
+ <div className="memori-dropdown--sustainability-stat">
187
+ <span className="memori-dropdown--sustainability-stat-label">
188
+ {t('chatLogs.output') || 'Output'}
189
+ </span>
190
+ <strong className="memori-dropdown--sustainability-stat-value">
191
+ {formatCountValue(chatConsumptionTotals.totalOutputTokens, currentLocale)}
192
+ </strong>
193
+ <span className="memori-dropdown--sustainability-stat-meta">
194
+ {t('chatLogs.tokens') || 'Tokens'}
195
+ </span>
196
+ </div>
197
+ </div>
198
+ {llmUsageModels.length > 0 && (
199
+ <div className="memori-dropdown--sustainability-row memori-dropdown--sustainability-row--stacked">
200
+ <span className="memori-dropdown--sustainability-label">
201
+ {t('chatLogs.provider') || 'Provider'} /{' '}
202
+ {t('chatLogs.model') || 'Model'}
203
+ </span>
204
+ <div className="memori-dropdown--sustainability-tags">
205
+ {llmUsageModels.map(modelLabel => (
206
+ <span
207
+ key={modelLabel}
208
+ className="memori-dropdown--sustainability-tag"
209
+ >
210
+ {modelLabel}
211
+ </span>
212
+ ))}
213
+ </div>
214
+ </div>
215
+ )}
216
+ </div>
217
+ <div className="memori-dropdown--sustainability-metrics">
218
+ <h5 className="memori-dropdown--sustainability-section-title">
219
+ {t('chatLogs.environmentalImpact') || 'Environmental impact'}
220
+ </h5>
221
+ <div className="memori-dropdown--sustainability-row">
222
+ <span className="memori-dropdown--sustainability-label">
223
+ <span aria-hidden="true">{BADGE_EMOJI.energy}</span>{' '}
224
+ {t('chatLogs.energy') || 'Energy'}
225
+ </span>
226
+ <strong className="memori-dropdown--sustainability-value">
227
+ {formatImpactInReadableUnit(
228
+ chatConsumptionTotals.energy,
229
+ 'energy',
230
+ currentLocale
231
+ )}
232
+ </strong>
233
+ </div>
234
+ <div className="memori-dropdown--sustainability-row">
235
+ <span className="memori-dropdown--sustainability-label">
236
+ <span aria-hidden="true">{BADGE_EMOJI.co2}</span>{' '}
237
+ {t('chatLogs.co2') || 'CO2'}
238
+ </span>
239
+ <strong className="memori-dropdown--sustainability-value">
240
+ {formatImpactInReadableUnit(
241
+ chatConsumptionTotals.gwp,
242
+ 'co2',
243
+ currentLocale
244
+ )}
245
+ </strong>
246
+ </div>
247
+ <div className="memori-dropdown--sustainability-row">
248
+ <span className="memori-dropdown--sustainability-label">
249
+ <span aria-hidden="true">{BADGE_EMOJI.water}</span>{' '}
250
+ {t('chatLogs.water') || 'Water'}
251
+ </span>
252
+ <strong className="memori-dropdown--sustainability-value">
253
+ {formatImpactInReadableUnit(
254
+ chatConsumptionTotals.wcf,
255
+ 'water',
256
+ currentLocale
257
+ )}
258
+ </strong>
259
+ </div>
260
+ </div>
261
+ </div>
262
+ </Dropdown>
263
+ );
264
+ };
265
+
266
+ export default ChatConsumptionDropdown;
@@ -97,12 +97,19 @@
97
97
  }
98
98
  }
99
99
 
100
+ .memori-header--button--sustainability-icon {
101
+ width: 1em;
102
+ color: currentColor;
103
+ }
100
104
  .memori-header .memori-header--button--position {
101
105
  margin-right: 0;
102
106
  }
103
107
 
108
+ .memori-header--button--sustainability {
109
+ max-height: 37px;
110
+ }
104
111
 
105
- .memori-dropdown--avatar-input{
112
+ .memori-dropdown--avatar-input {
106
113
  position: absolute;
107
114
  top: 20px;
108
115
  left: 15px;
@@ -112,60 +119,164 @@
112
119
  opacity: 0;
113
120
  }
114
121
 
115
-
116
-
117
- .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 {
118
125
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
119
126
  cursor: pointer;
120
127
  pointer-events: cursor;
121
128
  transform: scale(1.05);
122
129
  }
123
130
 
124
- .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 {
125
133
  display: block;
126
134
  }
127
135
 
128
136
  .memori-dropdown--sustainability {
129
- min-width: 260px;
130
- padding: 0.625rem 0.75rem;
131
- border-radius: 8px;
132
- 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);
133
143
  }
134
144
 
135
145
  .memori-dropdown--sustainability-title {
136
- 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);
137
150
  font-size: 0.95rem;
138
151
  font-weight: 700;
139
152
  line-height: 1.2;
140
- text-align: center;
153
+ text-align: left;
141
154
  }
142
155
 
156
+ .memori-dropdown--sustainability-section,
143
157
  .memori-dropdown--sustainability-metrics {
144
158
  display: flex;
145
159
  flex-direction: column;
146
160
  gap: 0.35rem;
147
161
  }
148
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
+
149
218
  .memori-dropdown--sustainability-row {
150
219
  display: flex;
151
220
  align-items: center;
152
221
  justify-content: space-between;
153
- padding: 0.4rem 0.5rem;
154
- border-radius: 6px;
155
- 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);
156
226
  gap: 1rem;
157
227
  }
158
228
 
229
+ .memori-dropdown--sustainability-row--stacked {
230
+ flex-direction: column;
231
+ align-items: flex-start;
232
+ gap: 0.45rem;
233
+ }
234
+
159
235
  .memori-dropdown--sustainability-label {
160
236
  display: inline-flex;
161
237
  align-items: center;
238
+ color: color-mix(in srgb, var(--memori-text-color, #111827) 76%, #64748b);
162
239
  font-size: 0.82rem;
163
- font-weight: 500;
240
+ font-weight: 600;
164
241
  gap: 0.35rem;
165
- opacity: 0.9;
166
242
  }
167
243
 
168
244
  .memori-dropdown--sustainability-value {
169
- 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;
170
248
  white-space: nowrap;
171
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
+ }