@remit/ui 0.0.70 → 0.0.71

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/ui",
3
- "version": "0.0.70",
3
+ "version": "0.0.71",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
@@ -17,7 +17,6 @@ export interface AutoMovedBadgeProps {
17
17
  * way to reach the filter that keeps moving mail. Omit for a classifier move.
18
18
  */
19
19
  filtersHref?: string;
20
- manageLabel?: string;
21
20
  className?: string;
22
21
  }
23
22
 
@@ -34,7 +33,6 @@ export function AutoMovedBadge({
34
33
  onUndo,
35
34
  undoLabel = "Undo",
36
35
  filtersHref,
37
- manageLabel = "Manage filter",
38
36
  className,
39
37
  }: AutoMovedBadgeProps) {
40
38
  return (
@@ -61,7 +59,7 @@ export function AutoMovedBadge({
61
59
  }}
62
60
  className="font-semibold underline decoration-dotted underline-offset-2 hover:decoration-solid"
63
61
  >
64
- {manageLabel}
62
+ Manage filter
65
63
  </a>
66
64
  )}
67
65
  </Badge>
@@ -75,17 +75,6 @@ export interface BriefSectionsProps {
75
75
  onSelectSource?: (id: string) => void;
76
76
  /** Seeds the filter panel open on first render (stories / deep links). */
77
77
  defaultExpanded?: boolean;
78
- /**
79
- * Whether this component owns arrow-key traversal and the roving tabindex
80
- * over its rows. On by default so a consumer that only passes rows (the
81
- * Storybook prototype) still has a keyboard.
82
- *
83
- * The web client turns it off: its triage layer routes ↑/↓/Home/End through
84
- * one window-level dispatcher, and a container handler here would swallow
85
- * them first — taking Shift+↑/↓ with them, which the dispatcher needs for
86
- * range selection and `rovingNextIndex` ignores.
87
- */
88
- manageFocus?: boolean;
89
78
  }
90
79
 
91
80
  /**
@@ -107,7 +96,6 @@ export function BriefSections({
107
96
  sourcesNote,
108
97
  onSelectSource,
109
98
  defaultExpanded = false,
110
- manageFocus = true,
111
99
  }: BriefSectionsProps) {
112
100
  const [active, setActive] = useState<ReadonlySet<BriefFilterId>>(new Set());
113
101
  const [sheetExpanded, setSheetExpanded] = useState(defaultExpanded);
@@ -115,7 +103,6 @@ export function BriefSections({
115
103
  useRovingFocus({
116
104
  containerRef: listRef,
117
105
  itemSelector: LIST_ROW_SELECTOR,
118
- enabled: manageFocus,
119
106
  });
120
107
 
121
108
  const toggleFilter = (id: BriefFilterId) => {
@@ -5,8 +5,6 @@ export interface DialogProps {
5
5
  open: boolean;
6
6
  onClose: () => void;
7
7
  title: string;
8
- /** Unique ID for aria-labelledby. Defaults to "dialog-title". */
9
- titleId?: string;
10
8
  children?: ReactNode;
11
9
  className?: string;
12
10
  /**
@@ -22,7 +20,6 @@ export function Dialog({
22
20
  open,
23
21
  onClose,
24
22
  title,
25
- titleId = "dialog-title",
26
23
  children,
27
24
  className,
28
25
  anchor = "center",
@@ -86,7 +83,7 @@ export function Dialog({
86
83
  ref={dialogRef}
87
84
  role="dialog"
88
85
  aria-modal="true"
89
- aria-labelledby={titleId}
86
+ aria-labelledby="dialog-title"
90
87
  className={cn(
91
88
  "relative z-10 overflow-hidden border-line bg-surface shadow-xl",
92
89
  isLeft
@@ -99,7 +96,7 @@ export function Dialog({
99
96
  onClick={(e) => e.stopPropagation()}
100
97
  onKeyDown={(e) => e.stopPropagation()}
101
98
  >
102
- <h2 id={titleId} className="sr-only">
99
+ <h2 id="dialog-title" className="sr-only">
103
100
  {title}
104
101
  </h2>
105
102
  {children}
@@ -7,9 +7,7 @@ import {
7
7
  useState,
8
8
  } from "react";
9
9
  import { isAbortError } from "../lib/abort.js";
10
- import { BottomSheet } from "./bottom-sheet.js";
11
10
  import { Button } from "./button.js";
12
- import { Dialog } from "./dialog.js";
13
11
  import {
14
12
  AddChipButton,
15
13
  ClauseChip,
@@ -699,44 +697,3 @@ export function FilterRuleEditor({
699
697
  </div>
700
698
  );
701
699
  }
702
-
703
- export interface FilterRuleDialogProps extends FilterRuleEditorProps {
704
- open: boolean;
705
- onClose: () => void;
706
- }
707
-
708
- /** Desktop home for the rule editor — the centered modal (RFC 038 D1). */
709
- export function FilterRuleDialog({
710
- open,
711
- onClose,
712
- ...editor
713
- }: FilterRuleDialogProps) {
714
- if (!open) return null;
715
- return (
716
- <Dialog open={open} onClose={onClose} title="Filter rule">
717
- <FilterRuleEditor {...editor} onCancel={onClose} />
718
- </Dialog>
719
- );
720
- }
721
-
722
- export interface FilterRuleSheetProps extends FilterRuleEditorProps {
723
- open: boolean;
724
- onClose: () => void;
725
- }
726
-
727
- /** Mobile home for the rule editor — the bottom sheet (RFC 038 D1). */
728
- export function FilterRuleSheet({
729
- open,
730
- onClose,
731
- ...editor
732
- }: FilterRuleSheetProps): ReactNode {
733
- return (
734
- <BottomSheet
735
- open={open}
736
- onClose={onClose}
737
- dismissLabel="Dismiss filter rule"
738
- >
739
- <FilterRuleEditor {...editor} onCancel={onClose} />
740
- </BottomSheet>
741
- );
742
- }
@@ -31,10 +31,8 @@ import {
31
31
  widenChipLabel,
32
32
  } from "./filter-rule.js";
33
33
  import {
34
- FilterRuleDialog,
35
34
  FilterRuleEditor,
36
35
  type FilterRuleEditorProps,
37
- FilterRuleSheet,
38
36
  } from "./filter-rule-editor.js";
39
37
 
40
38
  /** SSR splits interpolations with comment markers; sentences read across them. */
@@ -671,50 +669,3 @@ describe("FilterRuleEditor", () => {
671
669
  assert.doesNotMatch(html, /…and similar/);
672
670
  });
673
671
  });
674
-
675
- describe("FilterRuleDialog", () => {
676
- it("renders the editor when open", () => {
677
- const html = render(
678
- createElement(FilterRuleDialog, {
679
- open: true,
680
- onClose: () => {},
681
- rule: demoRule,
682
- folders: FOLDERS,
683
- preview: READY,
684
- }),
685
- );
686
- assert.match(html, /role="dialog"/);
687
- assert.match(html, /Filter rule/);
688
- });
689
-
690
- it("renders nothing when closed", () => {
691
- assert.equal(
692
- renderToString(
693
- createElement(FilterRuleDialog, {
694
- open: false,
695
- onClose: () => {},
696
- rule: demoRule,
697
- folders: FOLDERS,
698
- preview: READY,
699
- }),
700
- ),
701
- "",
702
- );
703
- });
704
- });
705
-
706
- describe("FilterRuleSheet", () => {
707
- it("renders the editor inside a dismissible sheet", () => {
708
- const html = render(
709
- createElement(FilterRuleSheet, {
710
- open: true,
711
- onClose: () => {},
712
- rule: demoRule,
713
- folders: FOLDERS,
714
- preview: READY,
715
- }),
716
- );
717
- assert.match(html, /Dismiss filter rule/);
718
- assert.match(html, /Filter rule/);
719
- });
720
- });
@@ -6,7 +6,6 @@ import { Button } from "./button.js";
6
6
  import { Card, CardBody, CardHeader, CardTitle } from "./card.js";
7
7
  import { FieldLabel } from "./field-label.js";
8
8
  import { Input } from "./input.js";
9
- import { ListItem } from "./list-item.js";
10
9
  import { PasswordInput } from "./password-input.js";
11
10
  import { SecuritySelect } from "./security-select.js";
12
11
  import { Select } from "./select.js";
@@ -176,32 +175,3 @@ export const Cards: Story = {
176
175
  </div>
177
176
  ),
178
177
  };
179
-
180
- export const ListItems: Story = {
181
- render: () => (
182
- <div className="max-w-md divide-y divide-line p-8">
183
- <ListItem
184
- unread
185
- leading={<Avatar name="Priya Natarajan" size="md" />}
186
- trailing="08:52"
187
- >
188
- <div className="text-sm font-semibold text-fg">Priya Natarajan</div>
189
- <div className="text-sm text-fg">Q3 roadmap review</div>
190
- <p className="line-clamp-1 text-xs text-fg-subtle">
191
- Sharing the agenda ahead of Thursday…
192
- </p>
193
- </ListItem>
194
- <ListItem
195
- active
196
- leading={<Avatar name="Marcus Webb" size="md" />}
197
- trailing="Wed"
198
- >
199
- <div className="text-sm font-medium text-fg-muted">Marcus Webb</div>
200
- <div className="text-sm text-fg-muted">Re: Reading pane density</div>
201
- <p className="line-clamp-1 text-xs text-fg-subtle">
202
- Strong +1 on tightening the rows…
203
- </p>
204
- </ListItem>
205
- </div>
206
- ),
207
- };
@@ -65,8 +65,6 @@ export interface SearchChipInputProps {
65
65
  */
66
66
  chips?: readonly SearchChip[];
67
67
  onRemoveChip?: (id: string) => void;
68
- /** Opens a chip's own value editor, where the host offers one. */
69
- onActivateChip?: (id: string) => void;
70
68
  /** The free text alongside the chips. */
71
69
  value: string;
72
70
  onChange: (value: string) => void;
@@ -105,8 +103,6 @@ export interface SearchChipInputProps {
105
103
  * when something outside needs to address the field by a stable id.
106
104
  */
107
105
  inputId?: string;
108
- /** Accessible name for the chip grid. */
109
- chipsLabel?: string;
110
106
  /**
111
107
  * Completions for what is being typed. Omit for a field that offers none;
112
108
  * see {@link SearchFieldSuggest} for what the field and the host each own.
@@ -165,7 +161,6 @@ const isEditableTarget = (target: EventTarget | null): boolean => {
165
161
  export const SearchChipInput = ({
166
162
  chips = [],
167
163
  onRemoveChip,
168
- onActivateChip,
169
164
  value,
170
165
  onChange,
171
166
  onClear,
@@ -175,7 +170,6 @@ export const SearchChipInput = ({
175
170
  showClearButton = true,
176
171
  size = "sm",
177
172
  inputId,
178
- chipsLabel = "Search filters",
179
173
  suggest,
180
174
  className,
181
175
  }: SearchChipInputProps) => {
@@ -313,16 +307,13 @@ export const SearchChipInput = ({
313
307
  case "focusInput":
314
308
  moveFocus(null);
315
309
  return;
316
- case "activateChip": {
317
- const chip = chips[action.index];
318
- if (chip && onActivateChip) onActivateChip(chip.id);
310
+ case "activateChip":
319
311
  return;
320
- }
321
312
  case "none":
322
313
  return;
323
314
  }
324
315
  },
325
- [chips, removeChipAt, moveFocus, onActivateChip],
316
+ [chips, removeChipAt, moveFocus],
326
317
  );
327
318
 
328
319
  useEffect(() => {
@@ -371,7 +362,7 @@ export const SearchChipInput = ({
371
362
  // biome-ignore lint/a11y/useSemanticElements: see above
372
363
  <div
373
364
  role="grid"
374
- aria-label={chipsLabel}
365
+ aria-label="Search filters"
375
366
  className="flex min-w-0 flex-wrap items-center gap-1"
376
367
  >
377
368
  {chips.map((chip, index) => (
@@ -386,9 +377,6 @@ export const SearchChipInput = ({
386
377
  onFocusLabel={() => setFocusedChip(index)}
387
378
  onKeyDown={handleChipKeyDown(index)}
388
379
  onRemove={() => removeChipAt(index)}
389
- onActivate={
390
- onActivateChip ? () => onActivateChip(chip.id) : undefined
391
- }
392
380
  />
393
381
  ))}
394
382
  </div>
@@ -351,7 +351,6 @@ export function SelectionSample({
351
351
  }
352
352
 
353
353
  export interface FooterNavProps {
354
- backLabel?: string;
355
354
  onBack: () => void;
356
355
  nextLabel: string;
357
356
  onNext: () => void;
@@ -369,7 +368,6 @@ export interface FooterNavProps {
369
368
  * carries the two ways that reason reaches the user.
370
369
  */
371
370
  export function FooterNav({
372
- backLabel = "Back",
373
371
  onBack,
374
372
  nextLabel,
375
373
  onNext,
@@ -396,7 +394,7 @@ export function FooterNav({
396
394
  icon={<ArrowLeft className="size-4" />}
397
395
  className="shrink-0"
398
396
  >
399
- {backLabel}
397
+ Back
400
398
  </Button>
401
399
  <Button
402
400
  variant={nextVariant}
package/src/index.ts CHANGED
@@ -184,12 +184,8 @@ export {
184
184
  } from "./components/filter-rule.js";
185
185
  export {
186
186
  type ClauseEditState,
187
- FilterRuleDialog,
188
- type FilterRuleDialogProps,
189
187
  FilterRuleEditor,
190
188
  type FilterRuleEditorProps,
191
- FilterRuleSheet,
192
- type FilterRuleSheetProps,
193
189
  } from "./components/filter-rule-editor.js";
194
190
  export {
195
191
  FilterSheet,
@@ -273,7 +269,6 @@ export {
273
269
  type LabelChipData,
274
270
  type LabelChipProps,
275
271
  } from "./components/label-chip.js";
276
- export { ListItem, type ListItemProps } from "./components/list-item.js";
277
272
  export {
278
273
  type MailAction,
279
274
  MailActionToolbar,
@@ -288,10 +283,6 @@ export {
288
283
  MessageBodyView,
289
284
  type MessageBodyViewProps,
290
285
  } from "./components/message-body-view.js";
291
- export {
292
- MessageHeader,
293
- type MessageHeaderProps,
294
- } from "./components/message-header.js";
295
286
  export { MessageListPane } from "./components/message-list-pane.js";
296
287
  export {
297
288
  type FilterReach,
@@ -1,39 +0,0 @@
1
- import type { HTMLAttributes, ReactNode } from "react";
2
- import { cn } from "../lib/cn.js";
3
-
4
- export interface ListItemProps extends HTMLAttributes<HTMLDivElement> {
5
- leading?: ReactNode;
6
- trailing?: ReactNode;
7
- active?: boolean;
8
- unread?: boolean;
9
- }
10
-
11
- export function ListItem({
12
- leading,
13
- trailing,
14
- active,
15
- unread,
16
- className,
17
- children,
18
- ...props
19
- }: ListItemProps) {
20
- return (
21
- <div
22
- className={cn(
23
- "group relative flex cursor-pointer items-start gap-3 px-4 py-3 transition-colors",
24
- active ? "bg-accent-2-soft" : "hover:bg-surface-sunken",
25
- className,
26
- )}
27
- {...props}
28
- >
29
- {unread && (
30
- <span className="absolute left-1.5 top-1/2 size-1.5 -translate-y-1/2 rounded-full bg-accent" />
31
- )}
32
- {leading && <div className="shrink-0 pt-0.5">{leading}</div>}
33
- <div className="min-w-0 flex-1">{children}</div>
34
- {trailing && (
35
- <div className="shrink-0 text-2xs text-fg-subtle">{trailing}</div>
36
- )}
37
- </div>
38
- );
39
- }
@@ -1,58 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { createElement } from "react";
4
- import { renderToString } from "react-dom/server";
5
- import type { EnvelopeAddress } from "./address-display.js";
6
- import { MessageHeader, type MessageHeaderProps } from "./message-header.js";
7
-
8
- const ada: EnvelopeAddress = {
9
- displayName: "Ada Lovelace",
10
- normalizedEmail: "ada@example.com",
11
- };
12
-
13
- const base: MessageHeaderProps = {
14
- subject: "Quarterly numbers",
15
- from: [ada],
16
- to: [{ normalizedEmail: "team@example.com" }],
17
- date: "Mon, 23 Jun 2026, 14:00",
18
- senderTrust: "wellknown",
19
- };
20
-
21
- describe("MessageHeader", () => {
22
- it("renders subject, addresses and the formatted date", () => {
23
- const html = renderToString(createElement(MessageHeader, base));
24
- assert.match(html, /Quarterly numbers/);
25
- assert.match(html, /Ada Lovelace/);
26
- assert.match(html, /team@example.com/);
27
- assert.match(html, /Mon, 23 Jun 2026, 14:00/);
28
- });
29
-
30
- it("shows a fallback for a missing subject", () => {
31
- const html = renderToString(
32
- createElement(MessageHeader, { ...base, subject: undefined }),
33
- );
34
- assert.match(html, /\(No subject\)/);
35
- });
36
-
37
- it("renders the category badge and trust indicator", () => {
38
- const html = renderToString(
39
- createElement(MessageHeader, {
40
- ...base,
41
- category: "newsletter",
42
- senderTrust: "vip",
43
- }),
44
- );
45
- assert.match(html, /aria-label="Category: newsletter"/);
46
- assert.match(html, /aria-label="VIP sender"/);
47
- });
48
-
49
- it("renders an actions slot when provided", () => {
50
- const html = renderToString(
51
- createElement(MessageHeader, {
52
- ...base,
53
- actions: createElement("button", { type: "button" }, "Menu"),
54
- }),
55
- );
56
- assert.match(html, /Menu/);
57
- });
58
- });
@@ -1,79 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react";
2
- import { Menu } from "lucide-react";
3
- import type { EnvelopeAddress } from "./address-display.js";
4
- import { MessageHeader } from "./message-header.js";
5
-
6
- const meta: Meta<typeof MessageHeader> = {
7
- title: "Mail/MessageHeader",
8
- component: MessageHeader,
9
- parameters: { layout: "fullscreen" },
10
- };
11
- export default meta;
12
-
13
- type Story = StoryObj<typeof MessageHeader>;
14
-
15
- const named = (name: string, email: string): EnvelopeAddress => ({
16
- displayName: name,
17
- normalizedEmail: email,
18
- });
19
-
20
- const trustedFrom: EnvelopeAddress = {
21
- ...named("Ada Lovelace", "ada@example.com"),
22
- flags: { trusted: { value: true } },
23
- };
24
-
25
- const date = "Mon, 23 Jun 2026, 14:00";
26
-
27
- export const TrustedSender: Story = {
28
- args: {
29
- subject: "Quarterly numbers are in",
30
- from: [trustedFrom],
31
- to: [named("The team", "team@example.com")],
32
- date,
33
- senderTrust: "wellknown",
34
- },
35
- };
36
-
37
- export const NewSenderNewsletter: Story = {
38
- name: "New sender + newsletter",
39
- args: {
40
- subject: "Welcome to the weekly digest",
41
- from: [named("Digest", "hello@digest.example.com")],
42
- to: [{ normalizedEmail: "you@example.com" }],
43
- date,
44
- category: "newsletter",
45
- senderTrust: "unknown",
46
- },
47
- };
48
-
49
- export const VipWithManyRecipients: Story = {
50
- name: "VIP + many recipients",
51
- args: {
52
- subject: "Board meeting follow-up",
53
- from: [{ ...named("Grace Hopper", "grace@example.com") }],
54
- to: [
55
- named("Alan Turing", "alan@example.com"),
56
- named("Katherine Johnson", "katherine@example.com"),
57
- named("Dorothy Vaughan", "dorothy@example.com"),
58
- named("Mary Jackson", "mary@example.com"),
59
- ],
60
- cc: [named("Margaret Hamilton", "margaret@example.com")],
61
- date,
62
- senderTrust: "vip",
63
- },
64
- };
65
-
66
- export const NoSubjectWithActions: Story = {
67
- name: "No subject + actions slot",
68
- args: {
69
- from: [named("Someone", "someone@example.com")],
70
- to: [{ normalizedEmail: "you@example.com" }],
71
- date,
72
- senderTrust: "wellknown",
73
- actions: (
74
- <button type="button" aria-label="Menu" className="text-fg-muted">
75
- <Menu className="size-5" />
76
- </button>
77
- ),
78
- },
79
- };
@@ -1,59 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import { AddressList, type EnvelopeAddress } from "./address-display.js";
3
- import { CategoryBadge, type MessageCategory } from "./category-badge.js";
4
- import {
5
- type SenderTrust,
6
- SenderTrustIndicator,
7
- } from "./sender-trust-indicator.js";
8
-
9
- export interface MessageHeaderProps {
10
- subject?: string;
11
- from: EnvelopeAddress[];
12
- to: EnvelopeAddress[];
13
- cc?: EnvelopeAddress[];
14
- /** Pre-formatted, human-readable date. The consumer owns formatting. */
15
- date: string;
16
- category?: MessageCategory;
17
- senderTrust: SenderTrust;
18
- /**
19
- * Optional slot for surfaces (hamburger menu, etc) rendered inline on the
20
- * right of the subject line. Kept generic so the header doesn't need to
21
- * know what action set is in play.
22
- */
23
- actions?: ReactNode;
24
- }
25
-
26
- export const MessageHeader = ({
27
- subject,
28
- from,
29
- to,
30
- cc = [],
31
- date,
32
- category,
33
- senderTrust,
34
- actions,
35
- }: MessageHeaderProps) => {
36
- return (
37
- <div className="border-b border-line p-4">
38
- <div className="flex items-start justify-between gap-2 mb-3">
39
- <div className="flex items-center gap-2 min-w-0 flex-1">
40
- <h1 className="text-xl font-semibold truncate">
41
- {subject || "(No subject)"}
42
- </h1>
43
- <CategoryBadge category={category} size="md" />
44
- <SenderTrustIndicator senderTrust={senderTrust} size="md" />
45
- </div>
46
- {actions && <div className="shrink-0">{actions}</div>}
47
- </div>
48
- <div className="space-y-1">
49
- <AddressList label="From" addresses={from} showTrustedBadge />
50
- <AddressList label="To" addresses={to} />
51
- {cc.length > 0 && <AddressList label="Cc" addresses={cc} />}
52
- <div className="flex gap-2 text-sm">
53
- <span className="text-fg-muted shrink-0 w-12">Date:</span>
54
- <span className="text-fg">{date}</span>
55
- </div>
56
- </div>
57
- </div>
58
- );
59
- };