@clef-sh/ui 0.1.20 → 0.1.21

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 (103) hide show
  1. package/dist/client/assets/index-DPWHjBbB.js +34 -0
  2. package/dist/client/assets/index-qsLTYpc9.css +2 -0
  3. package/dist/client/clef.svg +2 -0
  4. package/dist/client/index.html +3 -31
  5. package/dist/client-lib/components/Button.d.ts +1 -1
  6. package/dist/client-lib/components/Button.d.ts.map +1 -1
  7. package/dist/client-lib/components/CopyButton.d.ts.map +1 -1
  8. package/dist/client-lib/components/EnvBadge.d.ts.map +1 -1
  9. package/dist/client-lib/components/MatrixGrid.d.ts.map +1 -1
  10. package/dist/client-lib/components/Sidebar.d.ts +1 -1
  11. package/dist/client-lib/components/Sidebar.d.ts.map +1 -1
  12. package/dist/client-lib/components/StatusDot.d.ts.map +1 -1
  13. package/dist/client-lib/components/SyncPanel.d.ts.map +1 -1
  14. package/dist/client-lib/components/TopBar.d.ts +6 -0
  15. package/dist/client-lib/components/TopBar.d.ts.map +1 -1
  16. package/dist/client-lib/primitives/Badge.d.ts +11 -0
  17. package/dist/client-lib/primitives/Badge.d.ts.map +1 -0
  18. package/dist/client-lib/primitives/Card.d.ts +28 -0
  19. package/dist/client-lib/primitives/Card.d.ts.map +1 -0
  20. package/dist/client-lib/primitives/Dialog.d.ts +30 -0
  21. package/dist/client-lib/primitives/Dialog.d.ts.map +1 -0
  22. package/dist/client-lib/primitives/EmptyState.d.ts +10 -0
  23. package/dist/client-lib/primitives/EmptyState.d.ts.map +1 -0
  24. package/dist/client-lib/primitives/Field.d.ts +36 -0
  25. package/dist/client-lib/primitives/Field.d.ts.map +1 -0
  26. package/dist/client-lib/primitives/Input.d.ts +6 -0
  27. package/dist/client-lib/primitives/Input.d.ts.map +1 -0
  28. package/dist/client-lib/primitives/Stat.d.ts +11 -0
  29. package/dist/client-lib/primitives/Stat.d.ts.map +1 -0
  30. package/dist/client-lib/primitives/Table.d.ts +37 -0
  31. package/dist/client-lib/primitives/Table.d.ts.map +1 -0
  32. package/dist/client-lib/primitives/Tabs.d.ts +29 -0
  33. package/dist/client-lib/primitives/Tabs.d.ts.map +1 -0
  34. package/dist/client-lib/primitives/Toast.d.ts +16 -0
  35. package/dist/client-lib/primitives/Toast.d.ts.map +1 -0
  36. package/dist/client-lib/primitives/Toolbar.d.ts +29 -0
  37. package/dist/client-lib/primitives/Toolbar.d.ts.map +1 -0
  38. package/dist/client-lib/primitives/index.d.ts +23 -0
  39. package/dist/client-lib/primitives/index.d.ts.map +1 -0
  40. package/dist/client-lib/theme.d.ts +18 -41
  41. package/dist/client-lib/theme.d.ts.map +1 -1
  42. package/dist/server/api.d.ts.map +1 -1
  43. package/dist/server/api.js +215 -0
  44. package/dist/server/api.js.map +1 -1
  45. package/dist/server/envelope.d.ts +15 -0
  46. package/dist/server/envelope.d.ts.map +1 -0
  47. package/dist/server/envelope.js +310 -0
  48. package/dist/server/envelope.js.map +1 -0
  49. package/package.json +7 -2
  50. package/src/client/App.tsx +16 -41
  51. package/src/client/components/Button.tsx +13 -22
  52. package/src/client/components/CopyButton.tsx +5 -12
  53. package/src/client/components/EnvBadge.tsx +30 -15
  54. package/src/client/components/MatrixGrid.tsx +108 -252
  55. package/src/client/components/Sidebar.tsx +123 -199
  56. package/src/client/components/StatusDot.tsx +10 -15
  57. package/src/client/components/SyncPanel.tsx +14 -62
  58. package/src/client/components/TopBar.tsx +11 -36
  59. package/src/client/index.html +1 -30
  60. package/src/client/main.tsx +1 -0
  61. package/src/client/primitives/Badge.test.tsx +47 -0
  62. package/src/client/primitives/Badge.tsx +64 -0
  63. package/src/client/primitives/Card.test.tsx +50 -0
  64. package/src/client/primitives/Card.tsx +85 -0
  65. package/src/client/primitives/Dialog.test.tsx +55 -0
  66. package/src/client/primitives/Dialog.tsx +96 -0
  67. package/src/client/primitives/EmptyState.test.tsx +25 -0
  68. package/src/client/primitives/EmptyState.tsx +38 -0
  69. package/src/client/primitives/Field.test.tsx +46 -0
  70. package/src/client/primitives/Field.tsx +95 -0
  71. package/src/client/primitives/Input.tsx +26 -0
  72. package/src/client/primitives/Stat.test.tsx +32 -0
  73. package/src/client/primitives/Stat.tsx +52 -0
  74. package/src/client/primitives/Table.test.tsx +58 -0
  75. package/src/client/primitives/Table.tsx +113 -0
  76. package/src/client/primitives/Tabs.test.tsx +44 -0
  77. package/src/client/primitives/Tabs.tsx +100 -0
  78. package/src/client/primitives/Toast.test.tsx +77 -0
  79. package/src/client/primitives/Toast.tsx +89 -0
  80. package/src/client/primitives/Toolbar.test.tsx +50 -0
  81. package/src/client/primitives/Toolbar.tsx +86 -0
  82. package/src/client/primitives/index.ts +43 -0
  83. package/src/client/public/clef.svg +2 -0
  84. package/src/client/screens/BackendScreen.tsx +104 -363
  85. package/src/client/screens/DiffView.tsx +187 -378
  86. package/src/client/screens/EnvelopeScreen.test.tsx +542 -0
  87. package/src/client/screens/EnvelopeScreen.tsx +948 -0
  88. package/src/client/screens/GitLogView.tsx +48 -106
  89. package/src/client/screens/ImportScreen.tsx +105 -308
  90. package/src/client/screens/LintView.tsx +184 -379
  91. package/src/client/screens/ManifestScreen.tsx +283 -445
  92. package/src/client/screens/MatrixView.tsx +75 -91
  93. package/src/client/screens/NamespaceEditor.tsx +234 -609
  94. package/src/client/screens/PolicyView.tsx +183 -453
  95. package/src/client/screens/RecipientsScreen.tsx +71 -350
  96. package/src/client/screens/ResetScreen.tsx +67 -237
  97. package/src/client/screens/ScanScreen.tsx +85 -249
  98. package/src/client/screens/SchemaEditor.test.tsx +237 -0
  99. package/src/client/screens/SchemaEditor.tsx +435 -0
  100. package/src/client/screens/ServiceIdentitiesScreen.tsx +251 -788
  101. package/src/client/styles.css +77 -0
  102. package/src/client/theme.ts +27 -48
  103. package/dist/client/assets/index-Db6WgHgY.js +0 -38
@@ -1,9 +1,9 @@
1
1
  import React, { useState, useEffect, useCallback } from "react";
2
- import { theme } from "../theme";
2
+ import { ScanSearch } from "lucide-react";
3
3
  import { apiFetch } from "../api";
4
- import { TopBar } from "../components/TopBar";
5
4
  import { Button } from "../components/Button";
6
5
  import { CopyButton } from "../components/CopyButton";
6
+ import { Toolbar } from "../primitives";
7
7
  import type { ScanResult } from "@clef-sh/core";
8
8
 
9
9
  type ScanState = "idle" | "scanning" | "clean" | "issues";
@@ -87,70 +87,50 @@ export function ScanScreen() {
87
87
  const totalIssues = (result?.matches.length ?? 0) + (result?.unencryptedMatrixFiles.length ?? 0);
88
88
  const durationSec = result ? (result.durationMs / 1000).toFixed(1) : "0.0";
89
89
 
90
+ const filterButtons: ReadonlyArray<{ key: MatchFilter; label: string }> = [
91
+ { key: "all", label: "All" },
92
+ { key: "unencrypted", label: "Unencrypted" },
93
+ { key: "pattern", label: "Pattern" },
94
+ { key: "entropy", label: "Entropy" },
95
+ ];
96
+
90
97
  return (
91
- <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
92
- <TopBar
93
- title="Scan"
94
- subtitle="clef scan — detect plaintext secrets"
95
- actions={
96
- scanState === "issues" || scanState === "clean" ? (
97
- <Button onClick={runScan}>&#x21BA; Scan again</Button>
98
- ) : undefined
99
- }
100
- />
98
+ <div className="flex flex-1 flex-col overflow-hidden">
99
+ <Toolbar>
100
+ <div>
101
+ <Toolbar.Title>Scan</Toolbar.Title>
102
+ <Toolbar.Subtitle>clef scan — detect plaintext secrets</Toolbar.Subtitle>
103
+ </div>
104
+ {(scanState === "issues" || scanState === "clean") && (
105
+ <Toolbar.Actions>
106
+ <Button onClick={runScan}>↺ Scan again</Button>
107
+ </Toolbar.Actions>
108
+ )}
109
+ </Toolbar>
101
110
 
102
- <div style={{ flex: 1, overflow: "auto", padding: 24 }}>
111
+ <div className="flex-1 overflow-auto p-6">
103
112
  {/* ── Idle ────────────────────────────────────────────────────── */}
104
113
  {scanState === "idle" && (
105
- <div
106
- data-testid="scan-idle"
107
- style={{
108
- maxWidth: 520,
109
- margin: "0 auto",
110
- paddingTop: 40,
111
- }}
112
- >
113
- <div
114
- style={{
115
- fontFamily: theme.sans,
116
- fontSize: 14,
117
- color: theme.textMuted,
118
- marginBottom: 24,
119
- lineHeight: 1.6,
120
- }}
121
- >
122
- Scans your repository for secrets that have escaped the Clef matrix — plaintext values
123
- in files that should be encrypted.
114
+ <div data-testid="scan-idle" className="mx-auto max-w-[520px] pt-10">
115
+ <div className="mb-6 flex flex-col items-center gap-3 text-center">
116
+ <ScanSearch className="text-ash-dim" size={40} aria-hidden />
117
+ <div className="font-sans text-[14px] leading-relaxed text-ash">
118
+ Scans your repository for secrets that have escaped the Clef matrix — plaintext
119
+ values in files that should be encrypted.
120
+ </div>
124
121
  </div>
125
122
 
126
123
  {/* Severity selector */}
127
- <div style={{ marginBottom: 24 }}>
128
- <div
129
- style={{
130
- fontFamily: theme.sans,
131
- fontSize: 12,
132
- fontWeight: 600,
133
- color: theme.textMuted,
134
- marginBottom: 10,
135
- letterSpacing: "0.05em",
136
- textTransform: "uppercase",
137
- }}
138
- >
124
+ <div className="mb-6">
125
+ <div className="mb-2.5 font-sans text-[12px] font-semibold uppercase tracking-[0.05em] text-ash">
139
126
  Severity
140
127
  </div>
141
128
  {(["all", "high"] as const).map((sev) => (
142
129
  <label
143
130
  key={sev}
144
- style={{
145
- display: "flex",
146
- alignItems: "center",
147
- gap: 10,
148
- marginBottom: 8,
149
- cursor: "pointer",
150
- fontFamily: theme.sans,
151
- fontSize: 13,
152
- color: severity === sev ? theme.text : theme.textMuted,
153
- }}
131
+ className={`mb-2 flex cursor-pointer items-center gap-2.5 font-sans text-[13px] ${
132
+ severity === sev ? "text-bone" : "text-ash"
133
+ }`}
154
134
  >
155
135
  <input
156
136
  type="radio"
@@ -158,7 +138,7 @@ export function ScanScreen() {
158
138
  value={sev}
159
139
  checked={severity === sev}
160
140
  onChange={() => setSeverity(sev)}
161
- style={{ accentColor: theme.accent }}
141
+ className="accent-gold-500"
162
142
  data-testid={`severity-${sev}`}
163
143
  />
164
144
  {sev === "all" ? "All (patterns + entropy)" : "High (patterns only)"}
@@ -170,20 +150,9 @@ export function ScanScreen() {
170
150
  Scan repository
171
151
  </Button>
172
152
 
173
- <div
174
- style={{
175
- marginTop: 24,
176
- padding: "12px 16px",
177
- background: theme.surface,
178
- border: `1px solid ${theme.border}`,
179
- borderRadius: 8,
180
- fontFamily: theme.sans,
181
- fontSize: 12,
182
- color: theme.textMuted,
183
- }}
184
- >
185
- &#x2139;&#xFE0F; <code style={{ fontFamily: theme.mono }}>clef scan</code> runs
186
- automatically on every commit via the pre-commit hook.
153
+ <div className="mt-6 rounded-md border border-edge bg-ink-850 px-4 py-3 font-sans text-[12px] text-ash">
154
+ ℹ️ <code className="font-mono">clef scan</code> runs automatically on every commit via
155
+ the pre-commit hook.
187
156
  </div>
188
157
  </div>
189
158
  )}
@@ -192,28 +161,10 @@ export function ScanScreen() {
192
161
  {scanState === "scanning" && (
193
162
  <div
194
163
  data-testid="scan-scanning"
195
- style={{
196
- display: "flex",
197
- flexDirection: "column",
198
- alignItems: "center",
199
- justifyContent: "center",
200
- gap: 16,
201
- paddingTop: 80,
202
- }}
164
+ className="flex flex-col items-center justify-center gap-4 pt-20"
203
165
  >
204
- <div
205
- style={{
206
- width: 40,
207
- height: 40,
208
- borderRadius: "50%",
209
- border: `3px solid ${theme.accent}44`,
210
- borderTopColor: theme.accent,
211
- animation: "spin 0.8s linear infinite",
212
- }}
213
- />
214
- <div style={{ fontFamily: theme.sans, fontSize: 14, color: theme.textMuted }}>
215
- Scanning...
216
- </div>
166
+ <div className="h-10 w-10 animate-spin rounded-full border-[3px] border-gold-500/30 border-t-gold-500" />
167
+ <div className="font-sans text-[14px] text-ash">Scanning...</div>
217
168
  </div>
218
169
  )}
219
170
 
@@ -221,40 +172,16 @@ export function ScanScreen() {
221
172
  {scanState === "clean" && result && (
222
173
  <div
223
174
  data-testid="scan-clean"
224
- style={{
225
- display: "flex",
226
- flexDirection: "column",
227
- alignItems: "center",
228
- justifyContent: "center",
229
- gap: 14,
230
- paddingTop: 60,
231
- }}
175
+ className="flex flex-col items-center justify-center gap-3.5 pt-14"
232
176
  >
233
- <div
234
- style={{
235
- width: 56,
236
- height: 56,
237
- borderRadius: "50%",
238
- background: theme.greenDim,
239
- border: `1px solid ${theme.green}44`,
240
- display: "flex",
241
- alignItems: "center",
242
- justifyContent: "center",
243
- fontSize: 24,
244
- color: theme.green,
245
- }}
246
- >
247
- &#x2713;
177
+ <div className="flex h-14 w-14 items-center justify-center rounded-full border border-go-500/30 bg-go-500/15 text-[24px] text-go-500">
178
+
248
179
  </div>
249
- <div
250
- style={{ fontFamily: theme.sans, fontWeight: 600, fontSize: 16, color: theme.green }}
251
- >
252
- No issues found
253
- </div>
254
- <div style={{ fontFamily: theme.mono, fontSize: 12, color: theme.textMuted }}>
180
+ <div className="font-sans text-[16px] font-semibold text-go-500">No issues found</div>
181
+ <div className="font-mono text-[12px] text-ash">
255
182
  {result.filesScanned} files scanned in {durationSec}s
256
183
  </div>
257
- <div style={{ fontFamily: theme.mono, fontSize: 11, color: theme.textDim }}>
184
+ <div className="font-mono text-[11px] text-ash-dim">
258
185
  Last run: {formatRunAt(lastRunAt)}
259
186
  </div>
260
187
  </div>
@@ -264,50 +191,30 @@ export function ScanScreen() {
264
191
  {scanState === "issues" && result && (
265
192
  <div>
266
193
  {/* Summary */}
267
- <div
268
- style={{
269
- display: "flex",
270
- alignItems: "center",
271
- gap: 12,
272
- marginBottom: 20,
273
- flexWrap: "wrap",
274
- }}
275
- >
276
- <span
277
- style={{ fontFamily: theme.sans, fontSize: 14, color: theme.text, fontWeight: 600 }}
278
- >
194
+ <div className="mb-5 flex flex-wrap items-center gap-3">
195
+ <span className="font-sans text-[14px] font-semibold text-bone">
279
196
  {totalIssues} issue{totalIssues !== 1 ? "s" : ""} found in {result.filesScanned}{" "}
280
197
  files ({durationSec}s)
281
198
  </span>
282
- <div style={{ flex: 1 }} />
199
+ <div className="flex-1" />
283
200
  {/* Filter */}
284
- {(
285
- [
286
- { key: "all", label: "All" },
287
- { key: "unencrypted", label: "Unencrypted" },
288
- { key: "pattern", label: "Pattern" },
289
- { key: "entropy", label: "Entropy" },
290
- ] as { key: MatchFilter; label: string }[]
291
- ).map(({ key, label }) => (
292
- <button
293
- key={key}
294
- data-testid={`filter-${key}`}
295
- onClick={() => setFilter(key)}
296
- style={{
297
- padding: "4px 10px",
298
- borderRadius: 4,
299
- cursor: "pointer",
300
- fontFamily: theme.mono,
301
- fontSize: 11,
302
- fontWeight: filter === key ? 600 : 400,
303
- color: filter === key ? theme.accent : theme.textMuted,
304
- background: filter === key ? theme.accentDim : "transparent",
305
- border: `1px solid ${filter === key ? theme.accent + "55" : theme.borderLight}`,
306
- }}
307
- >
308
- {label}
309
- </button>
310
- ))}
201
+ {filterButtons.map(({ key, label }) => {
202
+ const active = filter === key;
203
+ return (
204
+ <button
205
+ key={key}
206
+ data-testid={`filter-${key}`}
207
+ onClick={() => setFilter(key)}
208
+ className={`cursor-pointer rounded-md border px-2.5 py-1 font-mono text-[11px] ${
209
+ active
210
+ ? "border-gold-500/30 bg-gold-500/10 font-semibold text-gold-500"
211
+ : "border-edge-strong bg-transparent text-ash"
212
+ }`}
213
+ >
214
+ {label}
215
+ </button>
216
+ );
217
+ })}
311
218
  </div>
312
219
 
313
220
  {/* Unencrypted matrix files */}
@@ -347,14 +254,7 @@ export function ScanScreen() {
347
254
  ))}
348
255
 
349
256
  {dismissedCount > 0 && (
350
- <div
351
- style={{
352
- fontFamily: theme.mono,
353
- fontSize: 11,
354
- color: theme.textDim,
355
- marginTop: 12,
356
- }}
357
- >
257
+ <div className="mt-3 font-mono text-[11px] text-ash-dim">
358
258
  {dismissedCount} dismissed
359
259
  </div>
360
260
  )}
@@ -384,55 +284,26 @@ function IssueCard({
384
284
  onViewFile,
385
285
  onDismiss,
386
286
  }: IssueCardProps) {
387
- const color = type === "error" ? theme.red : theme.yellow;
287
+ const stripeClass = type === "error" ? "border-l-stop-500/40" : "border-l-warn-500/40";
288
+ const tagClass =
289
+ type === "error"
290
+ ? "text-stop-500 bg-stop-500/15 border-stop-500/30"
291
+ : "text-warn-500 bg-warn-500/15 border-warn-500/30";
388
292
 
389
293
  return (
390
294
  <div
391
- style={{
392
- background: theme.surface,
393
- border: `1px solid ${theme.border}`,
394
- borderLeft: `3px solid ${color}66`,
395
- borderRadius: 8,
396
- padding: "14px 18px",
397
- marginBottom: 12,
398
- display: "flex",
399
- alignItems: "flex-start",
400
- gap: 14,
401
- }}
295
+ className={`mb-3 flex items-start gap-3.5 rounded-md border border-edge border-l-[3px] bg-ink-850 px-4.5 py-3.5 ${stripeClass}`}
402
296
  >
403
- <div style={{ flex: 1, minWidth: 0 }}>
297
+ <div className="min-w-0 flex-1">
404
298
  {/* Type badge + file */}
405
- <div
406
- style={{
407
- display: "flex",
408
- alignItems: "center",
409
- gap: 8,
410
- marginBottom: 6,
411
- flexWrap: "wrap",
412
- }}
413
- >
299
+ <div className="mb-1.5 flex flex-wrap items-center gap-2">
414
300
  <span
415
- style={{
416
- fontFamily: theme.mono,
417
- fontSize: 9,
418
- fontWeight: 700,
419
- color,
420
- background: `${color}18`,
421
- border: `1px solid ${color}33`,
422
- borderRadius: 3,
423
- padding: "2px 6px",
424
- letterSpacing: "0.07em",
425
- }}
301
+ className={`rounded-sm border px-1.5 py-0.5 font-mono text-[9px] font-bold tracking-[0.07em] ${tagClass}`}
426
302
  >
427
303
  {typeLabel}
428
304
  </span>
429
305
  <span
430
- style={{
431
- fontFamily: theme.mono,
432
- fontSize: 12,
433
- color: theme.accent,
434
- cursor: onViewFile ? "pointer" : "default",
435
- }}
306
+ className={`font-mono text-[12px] text-gold-500 ${onViewFile ? "cursor-pointer" : ""}`}
436
307
  onClick={onViewFile}
437
308
  role={onViewFile ? "button" : undefined}
438
309
  tabIndex={onViewFile ? 0 : undefined}
@@ -449,30 +320,14 @@ function IssueCard({
449
320
  </div>
450
321
 
451
322
  {/* Message (preview) */}
452
- <div
453
- style={{ fontFamily: theme.mono, fontSize: 12, color: theme.text, marginBottom: 10 }}
454
- data-testid="match-preview"
455
- >
323
+ <div className="mb-2.5 font-mono text-[12px] text-bone" data-testid="match-preview">
456
324
  {message}
457
325
  </div>
458
326
 
459
327
  {/* Fix command */}
460
- <div
461
- style={{
462
- display: "flex",
463
- alignItems: "center",
464
- gap: 8,
465
- background: "#0D0F14",
466
- border: `1px solid ${theme.borderLight}`,
467
- borderRadius: 6,
468
- padding: "6px 10px",
469
- width: "fit-content",
470
- }}
471
- >
472
- <span style={{ fontFamily: theme.mono, fontSize: 11, color: theme.green }}>$</span>
473
- <span style={{ fontFamily: theme.mono, fontSize: 11, color: theme.text }}>
474
- {fixCommand}
475
- </span>
328
+ <div className="flex w-fit items-center gap-2 rounded-md border border-edge-strong bg-ink-800 px-2.5 py-1.5">
329
+ <span className="font-mono text-[11px] text-go-500">$</span>
330
+ <span className="font-mono text-[11px] text-bone">{fixCommand}</span>
476
331
  <CopyButton text={fixCommand} />
477
332
  </div>
478
333
 
@@ -481,17 +336,7 @@ function IssueCard({
481
336
  <button
482
337
  data-testid="view-file-button"
483
338
  onClick={onViewFile}
484
- style={{
485
- marginTop: 8,
486
- background: "none",
487
- border: `1px solid ${theme.borderLight}`,
488
- borderRadius: 4,
489
- cursor: "pointer",
490
- color: theme.textMuted,
491
- fontFamily: theme.sans,
492
- fontSize: 11,
493
- padding: "3px 8px",
494
- }}
339
+ className="mt-2 cursor-pointer rounded-md border border-edge-strong bg-transparent px-2 py-0.5 font-sans text-[11px] text-ash"
495
340
  >
496
341
  View file
497
342
  </button>
@@ -504,18 +349,9 @@ function IssueCard({
504
349
  onClick={onDismiss}
505
350
  title="Dismiss"
506
351
  aria-label="Dismiss issue"
507
- style={{
508
- background: "none",
509
- border: "none",
510
- cursor: "pointer",
511
- color: theme.textDim,
512
- fontSize: 16,
513
- flexShrink: 0,
514
- padding: "0 4px",
515
- lineHeight: 1,
516
- }}
352
+ className="shrink-0 cursor-pointer border-none bg-transparent px-1 text-[16px] leading-none text-ash-dim"
517
353
  >
518
- &#x00D7;
354
+ ×
519
355
  </button>
520
356
  )}
521
357
  </div>