@paubox/ui 2.0.0 → 3.0.1

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/index.esm.js CHANGED
@@ -30939,12 +30939,12 @@ function _templateObject9() {
30939
30939
  }
30940
30940
  function _templateObject10() {
30941
30941
  var data = _tagged_template_literal$5([
30942
- "\n border-top: 1px solid ",
30943
- ";\n ",
30942
+ "\n ",
30944
30943
  "\n color: ",
30945
- ";\n html.dark & {\n border-top-color: ",
30946
- ";\n color: ",
30947
- ";\n }\n"
30944
+ ";\n html.dark & {\n color: ",
30945
+ ";\n }\n /* Divider above the footer only when there are suggestion rows above\n it. When the footer is the only item in the dropdown (fetchSuggestions\n false, no candidates, non-empty query), the static border-top would\n bleed past the rounded corners of the dropdown container as a stray\n line at the top of the menu. */\n &:not(:first-child) {\n border-top: 1px solid ",
30946
+ ";\n html.dark & {\n border-top-color: ",
30947
+ ";\n }\n }\n"
30948
30948
  ]);
30949
30949
  _templateObject10 = function _templateObject() {
30950
30950
  return data;
@@ -30992,7 +30992,7 @@ var DropdownRow = styled.button(_templateObject6$1(), spacing(1), spacing(1), sp
30992
30992
  var RowIcon = styled.span(_templateObject7$1(), neutral500$1);
30993
30993
  var RowText = styled.span(_templateObject8$1());
30994
30994
  var RowMeta = styled.span(_templateObject9(), $paragraph300Regular, neutral600$1, neutral300$1);
30995
- var FooterRow = styled(DropdownRow)(_templateObject10(), neutral200$1, $paragraph200Regular, neutral700$1, surfaceRaised, neutral300$1);
30995
+ var FooterRow = styled(DropdownRow)(_templateObject10(), $paragraph200Regular, neutral700$1, neutral300$1, neutral200$1, surfaceRaised);
30996
30996
  var PressEnter = styled.span(_templateObject11(), $paragraph300Regular, neutral500$1);
30997
30997
  var DeleteHandle = styled.span(_templateObject12());
30998
30998
  // ─── Inline icons (kept local to avoid bloating @icons) ─────────────────────
@@ -31069,11 +31069,20 @@ var formatDate = function(iso) {
31069
31069
  // with non-memoized callbacks from the parent) drove an infinite render
31070
31070
  // loop on pages that don't pass `localCandidates`.
31071
31071
  var EMPTY_CANDIDATES = [];
31072
+ // Default for the optional `decodeHeader` prop — pass-through.
31073
+ var identityDecode = function(s) {
31074
+ return s;
31075
+ };
31072
31076
  // ─── Component ──────────────────────────────────────────────────────────────
31073
31077
  var SearchOmnibox = function(param) {
31074
31078
  var bar = param.bar, _param_placeholder = param.placeholder, placeholder = _param_placeholder === void 0 ? 'Search' : _param_placeholder, _param_pbSearchBaseUrl = param.pbSearchBaseUrl, pbSearchBaseUrl = _param_pbSearchBaseUrl === void 0 ? '' : _param_pbSearchBaseUrl, _param_getAuthToken = param.getAuthToken, getAuthToken = _param_getAuthToken === void 0 ? function() {
31075
31079
  return '';
31076
- } : _param_getAuthToken, _param_mode = param.mode, mode = _param_mode === void 0 ? 'remote' : _param_mode, _param_withHistory = param.withHistory, withHistory = _param_withHistory === void 0 ? true : _param_withHistory, onResultClick = param.onResultClick, onCommit = param.onCommit, onLiveChange = param.onLiveChange, _param_localCandidates = param.localCandidates, localCandidates = _param_localCandidates === void 0 ? EMPTY_CANDIDATES : _param_localCandidates, _param_testId = param.testId, testId = _param_testId === void 0 ? 'search-omnibox' : _param_testId, _param_initialQuery = param.initialQuery, initialQuery = _param_initialQuery === void 0 ? '' : _param_initialQuery;
31080
+ } : _param_getAuthToken, _param_fetchSuggestions = param.fetchSuggestions, fetchSuggestions = _param_fetchSuggestions === void 0 ? true : _param_fetchSuggestions, _param_withHistory = param.withHistory, withHistory = _param_withHistory === void 0 ? true : _param_withHistory, onResultClick = param.onResultClick, onCommit = param.onCommit, onLiveChange = param.onLiveChange, _param_localCandidates = param.localCandidates, localCandidates = _param_localCandidates === void 0 ? EMPTY_CANDIDATES : _param_localCandidates, decodeHeader = param.decodeHeader, _param_testId = param.testId, testId = _param_testId === void 0 ? 'search-omnibox' : _param_testId, _param_initialQuery = param.initialQuery, initialQuery = _param_initialQuery === void 0 ? '' : _param_initialQuery;
31081
+ // Local identity fallback for the optional decoder. Wrapping at use time
31082
+ // (instead of defaulting via destructure) lets us keep the prop strictly
31083
+ // optional and skip an extra closure allocation on every render when the
31084
+ // caller doesn't pass one.
31085
+ var decode = decodeHeader !== null && decodeHeader !== void 0 ? decodeHeader : identityDecode;
31077
31086
  var _useState = _sliced_to_array$4(useState(initialQuery), 2), query = _useState[0], setQuery = _useState[1];
31078
31087
  var _useState1 = _sliced_to_array$4(useState(initialQuery), 2), debouncedQuery = _useState1[0], setDebouncedQuery = _useState1[1];
31079
31088
  var _useState2 = _sliced_to_array$4(useState(false), 2), focused = _useState2[0], setFocused = _useState2[1];
@@ -31217,7 +31226,7 @@ var SearchOmnibox = function(param) {
31217
31226
  ]);
31218
31227
  // Remote autocomplete fetch
31219
31228
  useEffect(function() {
31220
- if (mode !== 'remote' || !debouncedQuery) {
31229
+ if (!fetchSuggestions || !debouncedQuery) {
31221
31230
  setResults([]);
31222
31231
  return;
31223
31232
  }
@@ -31272,7 +31281,7 @@ var SearchOmnibox = function(param) {
31272
31281
  }, [
31273
31282
  debouncedQuery,
31274
31283
  bar,
31275
- mode,
31284
+ fetchSuggestions,
31276
31285
  apiFetch
31277
31286
  ]);
31278
31287
  // Tracks the last query value we've notified the parent about via the
@@ -31321,7 +31330,7 @@ var SearchOmnibox = function(param) {
31321
31330
  // re-render even when the previous state was also `[]`, because React
31322
31331
  // bails out only on Object.is equality.
31323
31332
  useEffect(function() {
31324
- if (mode !== 'local') return;
31333
+ if (fetchSuggestions) return;
31325
31334
  var q = debouncedQuery.trim().toLowerCase();
31326
31335
  if (!q) {
31327
31336
  setLocalMatches(function(prev) {
@@ -31339,13 +31348,13 @@ var SearchOmnibox = function(param) {
31339
31348
  }
31340
31349
  }, [
31341
31350
  debouncedQuery,
31342
- mode,
31351
+ fetchSuggestions,
31343
31352
  localCandidates
31344
31353
  ]);
31345
31354
  // Live-change notification: fires on debounced typing in both modes.
31346
31355
  // Pages that want to live-filter their table (or fetch as the user
31347
31356
  // types) pass `onLiveChange`; pages that only care about explicit
31348
- // commits leave it out. Independent of `mode`.
31357
+ // commits leave it out. Independent of `fetchSuggestions`.
31349
31358
  useEffect(function() {
31350
31359
  notifyLiveChange(debouncedQuery);
31351
31360
  }, [
@@ -31482,20 +31491,24 @@ var SearchOmnibox = function(param) {
31482
31491
  });
31483
31492
  }
31484
31493
  if (dropdownState === 'results') {
31485
- if (mode === 'local') {
31486
- return localMatches.map(function(s) {
31487
- return {
31488
- kind: 'local',
31489
- key: s
31490
- };
31491
- });
31492
- }
31493
- return _to_consumable_array$2(results.slice(0, 5).map(function(r) {
31494
+ // Per PRD §1.5 every typing-state dropdown ends in the
31495
+ // "All search results for X — Press ENTER" footer row, regardless
31496
+ // of how many (or whether any) suggestion rows precede it. Bars
31497
+ // with fetchSuggestions=false and no localCandidates therefore show
31498
+ // only the footer when the user has typed text, which is the only
31499
+ // mobile-friendly affordance for committing the search.
31500
+ var suggestionRows = !fetchSuggestions ? localMatches.map(function(s) {
31501
+ return {
31502
+ kind: 'local',
31503
+ key: s
31504
+ };
31505
+ }) : results.slice(0, 5).map(function(r) {
31494
31506
  return {
31495
31507
  kind: 'result',
31496
31508
  key: r.message_id
31497
31509
  };
31498
- })).concat([
31510
+ });
31511
+ return _to_consumable_array$2(suggestionRows).concat([
31499
31512
  {
31500
31513
  kind: 'footer',
31501
31514
  key: '__footer__'
@@ -31508,7 +31521,7 @@ var SearchOmnibox = function(param) {
31508
31521
  recent,
31509
31522
  results,
31510
31523
  localMatches,
31511
- mode
31524
+ fetchSuggestions
31512
31525
  ]);
31513
31526
  // Reset highlight whenever the item set changes — including same-length
31514
31527
  // swaps (e.g. one page of 5 results replaced by a different 5 + footer).
@@ -31807,17 +31820,28 @@ var SearchOmnibox = function(param) {
31807
31820
  children: /*#__PURE__*/ jsx(Close, {})
31808
31821
  })
31809
31822
  ]
31810
- }, "recent-".concat(item.key));
31823
+ }, "recent-".concat(idx, "-").concat(item.key));
31811
31824
  }
31812
31825
  if (item.kind === 'result') {
31813
31826
  var result = results.find(function(r) {
31814
31827
  return r.message_id === item.key;
31815
31828
  });
31816
31829
  if (!result) return null;
31830
+ // Decode RFC 2047 encoded-words ("=?UTF-8?Q?..?=") and other
31831
+ // wire-format escapes before rendering. Backend stores the
31832
+ // raw header bytes; presentation is the caller's concern via
31833
+ // the optional `decodeHeader` prop. When the prop is
31834
+ // unset (non-mail bars), `decode` is identity and this is a
31835
+ // no-op. Highlight markup is left as-is — it's a snippet
31836
+ // composed server-side from the indexed (already-tokenized)
31837
+ // text and shouldn't be re-decoded.
31838
+ var decodedSubject = decode(result.subject);
31839
+ var decodedFrom = decode(result.from);
31817
31840
  var _result_to;
31841
+ var decodedTo = ((_result_to = result.to) !== null && _result_to !== void 0 ? _result_to : []).map(decode);
31818
31842
  var participants = [
31819
- result.from
31820
- ].concat(_to_consumable_array$2((_result_to = result.to) !== null && _result_to !== void 0 ? _result_to : [])).filter(Boolean).join(', ');
31843
+ decodedFrom
31844
+ ].concat(_to_consumable_array$2(decodedTo)).filter(Boolean).join(', ');
31821
31845
  return /*#__PURE__*/ jsxs(DropdownRow, {
31822
31846
  id: optionId,
31823
31847
  role: "option",
@@ -31841,7 +31865,7 @@ var SearchOmnibox = function(param) {
31841
31865
  __html: sanitizeHighlight(result.highlight_subject)
31842
31866
  }
31843
31867
  }) : /*#__PURE__*/ jsx("strong", {
31844
- children: result.subject
31868
+ children: decodedSubject
31845
31869
  }),
31846
31870
  /*#__PURE__*/ jsx("span", {
31847
31871
  style: {
@@ -31856,7 +31880,7 @@ var SearchOmnibox = function(param) {
31856
31880
  children: formatDate(result.timestamp)
31857
31881
  })
31858
31882
  ]
31859
- }, "result-".concat(item.key));
31883
+ }, "result-".concat(idx, "-").concat(item.key));
31860
31884
  }
31861
31885
  if (item.kind === 'local') {
31862
31886
  return /*#__PURE__*/ jsx(DropdownRow, {
@@ -31874,7 +31898,7 @@ var SearchOmnibox = function(param) {
31874
31898
  children: /*#__PURE__*/ jsx(RowText, {
31875
31899
  children: item.key
31876
31900
  })
31877
- }, "local-".concat(item.key));
31901
+ }, "local-".concat(idx, "-").concat(item.key));
31878
31902
  }
31879
31903
  // footer
31880
31904
  return /*#__PURE__*/ jsxs(FooterRow, {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@paubox/ui",
3
3
  "author": "Paubox, Inc.",
4
4
  "description": "Paubox Component Library",
5
- "version": "2.0.0",
5
+ "version": "3.0.1",
6
6
  "type": "module",
7
7
  "private": false,
8
8
  "publishConfig": {
@@ -25,19 +25,38 @@ export interface SearchOmniboxProps {
25
25
  placeholder?: string;
26
26
  /**
27
27
  * Base URL of the pb_search service (no trailing slash).
28
- * Required when `mode='remote'` or `withHistory !== false`. Ignored otherwise.
28
+ * Required when `fetchSuggestions !== false` or `withHistory !== false`. Ignored otherwise.
29
29
  */
30
30
  pbSearchBaseUrl?: string;
31
31
  /**
32
32
  * Returns the current Bearer token to send to pb_search.
33
- * Required when `mode='remote'` or `withHistory !== false`. Ignored otherwise.
33
+ * Required when `fetchSuggestions !== false` or `withHistory !== false`. Ignored otherwise.
34
34
  */
35
35
  getAuthToken?: () => Promise<string> | string;
36
36
  /**
37
- * 'remote' (default for mail bars) autocomplete dropdown fetches results from pb_search.
38
- * 'local' autocomplete dropdown filters `localCandidates` client-side; recent searches still come from Redis (unless `withHistory` is false).
37
+ * Whether to fetch suggestion rows from pb_search as the user types.
38
+ * Defaults to `true`.
39
+ *
40
+ * - `true` — fires `GET /search/autocomplete` against pb_search on
41
+ * debounced typing and renders the top-5 hits above the footer row.
42
+ * Use on bars whose data is indexed in pb_search (every mail bar
43
+ * today).
44
+ * - `false` — never fetches `/search/autocomplete`. If `localCandidates`
45
+ * is passed, those are filtered client-side and shown as suggestions;
46
+ * otherwise only the footer row appears. Use on bars whose data
47
+ * isn't indexed in pb_search (rulesets, relays, settings, etc.).
48
+ *
49
+ * Independent of:
50
+ * - `withHistory`, which gates recent-search behavior (Redis), and
51
+ * - the Tab-accept ghost text, which is sourced from recent search
52
+ * history and is always on when `withHistory` is enabled.
53
+ *
54
+ * Named `fetchSuggestions` rather than `autocomplete` to avoid
55
+ * collision with the Tab-accept ghost-text autocomplete feature —
56
+ * both could reasonably be called "autocomplete" but they're
57
+ * independent.
39
58
  */
40
- mode?: 'remote' | 'local';
59
+ fetchSuggestions?: boolean;
41
60
  /**
42
61
  * Whether to use Redis-backed recent-search history. Defaults to `true`.
43
62
  *
@@ -47,12 +66,13 @@ export interface SearchOmniboxProps {
47
66
  * - Ghost-text tab autocomplete is disabled (it's sourced from recent searches)
48
67
  *
49
68
  * Set this to `false` for config / list pages that don't need per-user history.
50
- * Pairs naturally with `mode='local'` to keep the component fully decoupled
51
- * from pb_search.
69
+ * Pairs naturally with `fetchSuggestions={false}` to keep the component
70
+ * fully decoupled from pb_search.
52
71
  */
53
72
  withHistory?: boolean;
54
73
  /** Called when the user clicks a result row in the live-results dropdown
55
- * (remote mode only; the dropdown is a pb_search-fed top-5 list). */
74
+ * (only fires when `fetchSuggestions !== false`; the dropdown is a
75
+ * pb_search-fed top-5 list). */
56
76
  onResultClick?: (result: SearchOmniboxResult) => void;
57
77
  /** Called when the user **commits** a search — explicit action.
58
78
  * Triggers: Enter on the input (no row highlighted), click on the
@@ -65,11 +85,24 @@ export interface SearchOmniboxProps {
65
85
  * Fires in both `local` and `remote` modes. Optional — pages that
66
86
  * only care about explicit commits should just not pass this. */
67
87
  onLiveChange?: (query: string) => void;
68
- /** Local mode only: candidate strings to filter against (e.g. ruleset names, usernames). */
88
+ /** Optional decoder applied to header fields (`from`, `to`, `subject`)
89
+ * on suggestion rows before they're rendered. Use this to handle
90
+ * RFC 2047 encoded-words ("=?UTF-8?Q?Re:_=E2=9A=A1?=") or other
91
+ * on-the-wire encodings the backend hasn't already unwrapped.
92
+ *
93
+ * Kept as a prop (rather than hardcoded) so the component stays
94
+ * mail-agnostic; mail-bar callers wire in `decodeMimeHeader` from
95
+ * paubox-next's `utils/mimeHeader`, non-mail bars can leave it
96
+ * undefined. Defaults to identity. */
97
+ decodeHeader?: (value: string) => string;
98
+ /** Optional candidate strings to filter client-side when
99
+ * `fetchSuggestions={false}` (e.g. ruleset names, usernames). Ignored
100
+ * when `fetchSuggestions` is truthy — pb_search drives the suggestions
101
+ * instead. */
69
102
  localCandidates?: string[];
70
103
  /** Test id prefix; defaults to `search-omnibox`. */
71
104
  testId?: string;
72
105
  /** Optional initial query (e.g. when restoring from URL state). */
73
106
  initialQuery?: string;
74
107
  }
75
- export declare const SearchOmnibox: ({ bar, placeholder, pbSearchBaseUrl, getAuthToken, mode, withHistory, onResultClick, onCommit, onLiveChange, localCandidates, testId, initialQuery, }: SearchOmniboxProps) => import("@emotion/react/jsx-runtime").JSX.Element;
108
+ export declare const SearchOmnibox: ({ bar, placeholder, pbSearchBaseUrl, getAuthToken, fetchSuggestions, withHistory, onResultClick, onCommit, onLiveChange, localCandidates, decodeHeader, testId, initialQuery, }: SearchOmniboxProps) => import("@emotion/react/jsx-runtime").JSX.Element;