ionbase-ui 0.76.0 → 0.81.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 (82) hide show
  1. package/dist/components/Card.d.ts +61 -0
  2. package/dist/components/Card.d.ts.map +1 -0
  3. package/dist/components/Card.js +37 -0
  4. package/dist/components/Card.js.map +1 -0
  5. package/dist/components/CommandPalette.d.ts +76 -0
  6. package/dist/components/CommandPalette.d.ts.map +1 -0
  7. package/dist/components/CommandPalette.js +194 -0
  8. package/dist/components/CommandPalette.js.map +1 -0
  9. package/dist/components/DateField.d.ts +14 -0
  10. package/dist/components/DateField.d.ts.map +1 -1
  11. package/dist/components/DateField.js +1 -1
  12. package/dist/components/DateField.js.map +1 -1
  13. package/dist/components/Kbd.d.ts +45 -0
  14. package/dist/components/Kbd.d.ts.map +1 -0
  15. package/dist/components/Kbd.js +45 -0
  16. package/dist/components/Kbd.js.map +1 -0
  17. package/dist/components/Table.d.ts +18 -0
  18. package/dist/components/Table.d.ts.map +1 -1
  19. package/dist/components/Table.js +14 -3
  20. package/dist/components/Table.js.map +1 -1
  21. package/dist/components/TagGroup.d.ts +51 -0
  22. package/dist/components/TagGroup.d.ts.map +1 -0
  23. package/dist/components/TagGroup.js +84 -0
  24. package/dist/components/TagGroup.js.map +1 -0
  25. package/dist/components/TimeField.d.ts +72 -0
  26. package/dist/components/TimeField.d.ts.map +1 -0
  27. package/dist/components/TimeField.js +102 -0
  28. package/dist/components/TimeField.js.map +1 -0
  29. package/dist/components/index.d.ts +14 -1
  30. package/dist/components/index.d.ts.map +1 -1
  31. package/dist/components/index.js +6 -0
  32. package/dist/components/index.js.map +1 -1
  33. package/dist/components/iso-time.d.ts +23 -0
  34. package/dist/components/iso-time.d.ts.map +1 -0
  35. package/dist/components/iso-time.js +34 -0
  36. package/dist/components/iso-time.js.map +1 -0
  37. package/dist/components/shortcut.d.ts +19 -0
  38. package/dist/components/shortcut.d.ts.map +1 -0
  39. package/dist/components/shortcut.js +122 -0
  40. package/dist/components/shortcut.js.map +1 -0
  41. package/dist/components/use-table-sort.d.ts +38 -0
  42. package/dist/components/use-table-sort.d.ts.map +1 -0
  43. package/dist/components/use-table-sort.js +33 -0
  44. package/dist/components/use-table-sort.js.map +1 -0
  45. package/dist/figma-descriptions.json +103 -68
  46. package/dist/figma-map.json +343 -2
  47. package/dist/meta/Card.json +153 -0
  48. package/dist/meta/Checkbox.json +2 -2
  49. package/dist/meta/CommandPalette.json +178 -0
  50. package/dist/meta/DatePicker.json +2 -2
  51. package/dist/meta/Kbd.json +97 -0
  52. package/dist/meta/Radio.json +2 -2
  53. package/dist/meta/RadioGroup.json +2 -2
  54. package/dist/meta/Table.json +4 -1
  55. package/dist/meta/TableBody.json +3 -0
  56. package/dist/meta/TableCell.json +49 -5
  57. package/dist/meta/TableHead.json +3 -0
  58. package/dist/meta/TableRow.json +3 -0
  59. package/dist/meta/Tag.json +54 -0
  60. package/dist/meta/TagGroup.json +211 -0
  61. package/dist/meta/TimeField.json +224 -0
  62. package/dist/meta/Toggle.json +2 -2
  63. package/dist/meta/components.json +1010 -36
  64. package/dist/meta/contrast.json +1133 -6
  65. package/dist/meta/index.json +86 -12
  66. package/dist/meta/patterns/DataTable.json +27 -3
  67. package/dist/meta/patterns/PageShell.json +15 -2
  68. package/dist/meta/patterns/index.json +7 -3
  69. package/dist/styles/agent-activity.css +4 -2
  70. package/dist/styles/approval-gate.css +4 -3
  71. package/dist/styles/card.css +75 -0
  72. package/dist/styles/citation.css +4 -2
  73. package/dist/styles/command-palette.css +250 -0
  74. package/dist/styles/confidence-indicator.css +2 -2
  75. package/dist/styles/index.css +5 -0
  76. package/dist/styles/kbd.css +47 -0
  77. package/dist/styles/streaming-text.css +3 -2
  78. package/dist/styles/table.css +51 -0
  79. package/dist/styles/tag-group.css +172 -0
  80. package/dist/styles/time-field.css +31 -0
  81. package/llms.txt +2 -2
  82. package/package.json +1 -1
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "package": "ionbase-ui",
3
- "version": "0.76.0",
3
+ "version": "0.81.0",
4
4
  "usage": "Pick a component here, then read dist/meta/<Name>.json for its full contract.",
5
5
  "hooks": [
6
6
  "useAgentRun",
7
+ "useTableSort",
7
8
  "useToast"
8
9
  ],
9
10
  "helpers": [
@@ -248,6 +249,18 @@
248
249
  "detail": "dist/meta/Button.json",
249
250
  "hasIntent": true
250
251
  },
252
+ "Card": {
253
+ "summary": "A bordered surface that groups one part of a page, with an optional title row. A titled card is a named region; an untitled one is a plain surface.",
254
+ "status": "stable",
255
+ "variants": {
256
+ "intent": [
257
+ "default",
258
+ "danger"
259
+ ]
260
+ },
261
+ "detail": "dist/meta/Card.json",
262
+ "hasIntent": true
263
+ },
251
264
  "ChartLegend": {
252
265
  "summary": "Which colour is which series, for a chart drawn with visx. The swatch reads the same token as the series.",
253
266
  "status": "stable",
@@ -278,8 +291,8 @@
278
291
  ],
279
292
  "intent": [
280
293
  "neutral",
281
- "brand",
282
- "danger"
294
+ "danger",
295
+ "brand"
283
296
  ]
284
297
  },
285
298
  "detail": "dist/meta/Checkbox.json",
@@ -324,6 +337,13 @@
324
337
  "detail": "dist/meta/Combobox.json",
325
338
  "hasIntent": true
326
339
  },
340
+ "CommandPalette": {
341
+ "summary": "A searchable list of every command in the product, opened with ⌘K / Ctrl+K from anywhere, that runs the chosen command and closes.",
342
+ "status": "stable",
343
+ "variants": {},
344
+ "detail": "dist/meta/CommandPalette.json",
345
+ "hasIntent": true
346
+ },
327
347
  "ConfidenceIndicator": {
328
348
  "summary": "How much to trust the thing next to it — three levels with a required basis, never a percentage.",
329
349
  "status": "stable",
@@ -477,6 +497,18 @@
477
497
  "detail": "dist/meta/Input.json",
478
498
  "hasIntent": true
479
499
  },
500
+ "Kbd": {
501
+ "summary": "A key, or a keyboard shortcut, drawn as keycaps in the reader's platform's own symbols and read aloud by name.",
502
+ "status": "stable",
503
+ "variants": {
504
+ "platform": [
505
+ "mac",
506
+ "other"
507
+ ]
508
+ },
509
+ "detail": "dist/meta/Kbd.json",
510
+ "hasIntent": true
511
+ },
480
512
  "Link": {
481
513
  "summary": "Navigation to somewhere else. Renders an <a> when given `href`, a <button> otherwise.",
482
514
  "status": "stable",
@@ -667,8 +699,8 @@
667
699
  ],
668
700
  "intent": [
669
701
  "neutral",
670
- "brand",
671
- "danger"
702
+ "danger",
703
+ "brand"
672
704
  ]
673
705
  },
674
706
  "detail": "dist/meta/Radio.json",
@@ -685,8 +717,8 @@
685
717
  ],
686
718
  "intent": [
687
719
  "neutral",
688
- "brand",
689
- "danger"
720
+ "danger",
721
+ "brand"
690
722
  ]
691
723
  },
692
724
  "detail": "dist/meta/RadioGroup.json",
@@ -859,8 +891,8 @@
859
891
  "status": "stable",
860
892
  "variants": {
861
893
  "density": [
862
- "compact",
863
894
  "default",
895
+ "compact",
864
896
  "relaxed"
865
897
  ]
866
898
  },
@@ -890,8 +922,13 @@
890
922
  "trailing"
891
923
  ],
892
924
  "variant": [
893
- "link",
894
- "default"
925
+ "default",
926
+ "link"
927
+ ],
928
+ "sortDirection": [
929
+ "none",
930
+ "ascending",
931
+ "descending"
895
932
  ]
896
933
  },
897
934
  "detail": "dist/meta/TableCell.json",
@@ -932,6 +969,25 @@
932
969
  "detail": "dist/meta/Tabs.json",
933
970
  "hasIntent": true
934
971
  },
972
+ "Tag": {
973
+ "summary": "One tag in a TagGroup. Re-exported from react-stately's collection Item — `key` identifies it, children are its label.",
974
+ "status": "stable",
975
+ "variants": {},
976
+ "detail": "dist/meta/Tag.json",
977
+ "hasIntent": true
978
+ },
979
+ "TagGroup": {
980
+ "summary": "Labels a person applied and can take away: active filters, the labels on a record, recipients. Neutral and removable, unlike Badge.",
981
+ "status": "stable",
982
+ "variants": {
983
+ "size": [
984
+ "sm",
985
+ "md"
986
+ ]
987
+ },
988
+ "detail": "dist/meta/TagGroup.json",
989
+ "hasIntent": true
990
+ },
935
991
  "Textarea": {
936
992
  "summary": "A multi-line text field. Every size and state is Input's; it wraps and it resizes vertically.",
937
993
  "status": "stable",
@@ -953,6 +1009,24 @@
953
1009
  "detail": "dist/meta/Textarea.json",
954
1010
  "hasIntent": true
955
1011
  },
1012
+ "TimeField": {
1013
+ "summary": "A time of day typed into hour, minute and AM/PM segments in the reader's own format, taking and returning a 24-hour HH:MM string.",
1014
+ "status": "stable",
1015
+ "variants": {
1016
+ "size": [
1017
+ "sm",
1018
+ "md",
1019
+ "lg"
1020
+ ],
1021
+ "granularity": [
1022
+ "hour",
1023
+ "minute",
1024
+ "second"
1025
+ ]
1026
+ },
1027
+ "detail": "dist/meta/TimeField.json",
1028
+ "hasIntent": true
1029
+ },
956
1030
  "Toast": {
957
1031
  "summary": "A transient notification on neutral chrome. Intent is carried by the icon, not by a tinted panel.",
958
1032
  "status": "stable",
@@ -994,8 +1068,8 @@
994
1068
  ],
995
1069
  "intent": [
996
1070
  "neutral",
997
- "brand",
998
- "danger"
1071
+ "danger",
1072
+ "brand"
999
1073
  ]
1000
1074
  },
1001
1075
  "detail": "dist/meta/Toggle.json",
@@ -15,14 +15,21 @@
15
15
  "Button",
16
16
  "Badge",
17
17
  "Alert",
18
- "Input"
18
+ "Input",
19
+ "TagGroup"
19
20
  ],
20
21
  "structure": [
21
22
  "A toolbar above the table: a search Input, filter controls, and the bulk-action Buttons that appear once something is selected.",
23
+ "Under the toolbar, once any filter is applied: a TagGroup of the active filters, each removable on its own, beside a Clear all Button. When the last one goes the row goes with it, so move focus to the search Input.",
22
24
  "Table with a TableHead whose first cell carries the select-all Checkbox, and a TableBody whose rows each carry their own.",
25
+ "Sortable columns: every header that sorts gets `sortDirection` — the current direction on the sorted column, `none` on the others — so each shows it can be sorted before anyone tries.",
23
26
  "One region below the table for pagination or a load-more Button.",
24
27
  "The table region is replaced — not overlaid — by the empty, loading and error states."
25
28
  ],
29
+ "drivenBy": {
30
+ "hook": "useTableSort",
31
+ "note": "It holds which column the rows are sorted by and which way, and `sortProps(column)` gives each sortable header TableCell its `sortDirection` and `onSort`. Choosing a new column sorts it in its `firstDirection` (ascending unless set — pass `descending` for dates and amounts); choosing it again reverses it. It does not reorder rows: sort them yourself from `sort`, or send it to the server when the table is paged. Only the sorted column gets `aria-sort`."
32
+ },
26
33
  "states": {
27
34
  "loading": {
28
35
  "must": "Render the table's header row and a fixed number of skeleton rows, keeping the column widths.",
@@ -56,7 +63,9 @@
56
63
  "TableCell": [
57
64
  "scope",
58
65
  "align",
59
- "variant"
66
+ "variant",
67
+ "sortDirection",
68
+ "onSort"
60
69
  ],
61
70
  "Checkbox": [
62
71
  "isIndeterminate",
@@ -73,6 +82,11 @@
73
82
  ],
74
83
  "Input": [
75
84
  "leadingIcon"
85
+ ],
86
+ "TagGroup": [
87
+ "label",
88
+ "items",
89
+ "onRemove"
76
90
  ]
77
91
  },
78
92
  "variantsUsed": {
@@ -99,7 +113,8 @@
99
113
  "`aria-label` on Table — it names the scroll region, which is keyboard reachable",
100
114
  "`scope=\"col\"` on every header cell",
101
115
  "an `aria-label` on every row-selection Checkbox, and `isIndeterminate` on the select-all when the selection is partial",
102
- "a live region announcing the row count after a filter or search changes it — otherwise a screen-reader user has no idea the table changed"
116
+ "a live region announcing the row count after a filter or search changes it — otherwise a screen-reader user has no idea the table changed",
117
+ "a sortable header's label is its accessible name — the button inside the <th> reads the column name, and `aria-sort` on the sorted <th> says the direction"
103
118
  ],
104
119
  "notes": [
105
120
  "Bulk-action Buttons appearing on selection is a change of available actions, not just of styling. Announce it."
@@ -128,6 +143,15 @@
128
143
  {
129
144
  "dont": "wrapping Table in your own scroll container",
130
145
  "why": "Table already owns one and makes it keyboard reachable; a second is unreachable"
146
+ },
147
+ {
148
+ "dont": "a sortable column with no indicator until it is clicked",
149
+ "do": "`sortDirection=\"none\"` on every sortable header, so the neutral arrows show which columns sort",
150
+ "why": "a header that sorts but looks like plain text is only found by accident"
151
+ },
152
+ {
153
+ "dont": "sorting only the current page of a paged table",
154
+ "why": "the reader sees \"A–Z\" and takes it as the whole list; send the sort to the server, which knows every row"
131
155
  }
132
156
  ]
133
157
  }
@@ -16,14 +16,17 @@
16
16
  "Avatar",
17
17
  "Button",
18
18
  "Divider",
19
- "ToastProvider"
19
+ "ToastProvider",
20
+ "CommandPalette",
21
+ "Kbd"
20
22
  ],
21
23
  "structure": [
22
24
  "Header at the top with the brand in `brand`, primary navigation in `center` and account actions in `end`.",
23
25
  "One <main> content region, named, holding the page's own heading as its h1.",
24
26
  "ToastProvider mounted once around the whole shell, not per route.",
25
27
  "Breadcrumbs, when present, sit inside <main> above the h1 — they describe where the content is, not where the chrome is.",
26
- "When navigation is hierarchical or longer than a header row holds, a Sidebar replaces the NavItems in Header's `center`: Header keeps the brand and account actions, and the Sidebar sits beside <main> as the primary navigation. Below the tablet breakpoint the same Sidebar moves into a Drawer opened by Header's own toggle with `menuType=\"dialog\"` — one copy, not two, and one menu button."
28
+ "When navigation is hierarchical or longer than a header row holds, a Sidebar replaces the NavItems in Header's `center`: Header keeps the brand and account actions, and the Sidebar sits beside <main> as the primary navigation. Below the tablet breakpoint the same Sidebar moves into a Drawer opened by Header's own toggle with `menuType=\"dialog\"` — one copy, not two, and one menu button.",
29
+ "A CommandPalette, when the product has one, is mounted once inside the shell beside ToastProvider, not per route. Header's `end` carries a Search button showing <Kbd shortcut=\"mod+k\" /> — the visible door for anyone who does not know the shortcut, and the only one on a phone. Everything in the palette is also reachable from the navigation."
27
30
  ],
28
31
  "states": {
29
32
  "loading": {
@@ -89,6 +92,16 @@
89
92
  "LogoMark": [
90
93
  "size",
91
94
  "label"
95
+ ],
96
+ "CommandPalette": [
97
+ "commands",
98
+ "onAction",
99
+ "isOpen",
100
+ "onOpenChange",
101
+ "label"
102
+ ],
103
+ "Kbd": [
104
+ "shortcut"
92
105
  ]
93
106
  },
94
107
  "variantsUsed": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "ionbase-ui",
3
- "version": "0.76.0",
3
+ "version": "0.81.0",
4
4
  "usage": "A pattern is a documented composition of components, not a component. Read one here, then read the contracts of what it composes.",
5
5
  "patterns": {
6
6
  "AgentRun": {
@@ -55,8 +55,10 @@
55
55
  "Button",
56
56
  "Badge",
57
57
  "Alert",
58
- "Input"
58
+ "Input",
59
+ "TagGroup"
59
60
  ],
61
+ "drivenBy": "useTableSort",
60
62
  "states": [
61
63
  "loading",
62
64
  "empty",
@@ -137,7 +139,9 @@
137
139
  "Avatar",
138
140
  "Button",
139
141
  "Divider",
140
- "ToastProvider"
142
+ "ToastProvider",
143
+ "CommandPalette",
144
+ "Kbd"
141
145
  ],
142
146
  "states": [
143
147
  "loading",
@@ -1,8 +1,10 @@
1
1
  /*
2
2
  * AgentActivity
3
3
  *
4
- * NOT DRAWN IN FIGMA. The rail-and-body layout is Alert's, one step down in
5
- * scale; colours are the status roles used everywhere else in the system.
4
+ * Drawn in Figma as `Agent Activity` (1162:246) and `Agent Activity Step`
5
+ * (1161:287), Status × Show Detail. The rail-and-body layout is Alert's, one
6
+ * step down in scale; colours are the status roles used everywhere else in
7
+ * the system.
6
8
  */
7
9
  .ion-agent-activity {
8
10
  display: flex;
@@ -1,9 +1,10 @@
1
1
  /*
2
2
  * ApprovalGate
3
3
  *
4
- * NOT DRAWN IN FIGMA. Structure is Alert's — icon rail, body, one variable per
5
- * colour slot — so that when Figma draws it, this file needs values changed
6
- * rather than rewriting. The action row borrows Button's ramp.
4
+ * Drawn in Figma as `Approval Gate` (1160:423), Risk × Status. Structure is
5
+ * Alert's — icon rail, body, one variable per colour slot — which is why
6
+ * drawing it needed values checked here rather than a rewrite. The action row
7
+ * borrows Button's ramp.
7
8
  *
8
9
  * RISK CHANGES EMPHASIS, NOT MEANING
9
10
  *
@@ -0,0 +1,75 @@
1
+ /*
2
+ * Card
3
+ *
4
+ * Promoted from the demo app's `.demo-panel`, which nine screens used
5
+ * unchanged — the measurements are the ones StatTile already settled: a
6
+ * `surface/default` card with a `border/subtle` rule and the `lg` radius,
7
+ * 16 of padding, 12 between the parts.
8
+ *
9
+ * title the H6 text style, `text/default`
10
+ * description body-sm, `text/secondary`, 2 under the title
11
+ * action end of the title row; wraps under it below ~12rem of title
12
+ * danger the rule turns `border/error-subtle`; nothing else changes,
13
+ * so the warning stays in the card's words and not its colour
14
+ *
15
+ * The block carries its own background, so the contrast gate measures the
16
+ * title and description against the card rather than the page behind it.
17
+ */
18
+ .ion-card {
19
+ display: grid;
20
+ align-content: start;
21
+ gap: var(--spacing-12);
22
+ min-width: 0;
23
+ padding: var(--spacing-16);
24
+ border: var(--border-width-default) solid var(--border-subtle);
25
+ border-radius: var(--radius-lg);
26
+ background-color: var(--surface-default);
27
+ color: var(--text-default);
28
+ font-family: var(--font-family-sans);
29
+ }
30
+
31
+ .ion-card--danger {
32
+ border-color: var(--border-error-subtle);
33
+ }
34
+
35
+ .ion-card__header {
36
+ display: flex;
37
+ flex-wrap: wrap;
38
+ align-items: flex-start;
39
+ justify-content: space-between;
40
+ gap: var(--spacing-8) var(--spacing-12);
41
+ }
42
+
43
+ /*
44
+ * A 12rem basis, not the title's own width: a long title shrinks and wraps
45
+ * beside the action, the way Figma's FILL heading does, and the action only
46
+ * drops to its own line when the card is too narrow for both.
47
+ */
48
+ .ion-card__heading {
49
+ display: grid;
50
+ flex: 1 1 12rem;
51
+ gap: var(--spacing-2);
52
+ min-width: 0;
53
+ }
54
+
55
+ .ion-card__title {
56
+ margin: 0;
57
+ color: var(--text-default);
58
+ font-size: var(--type-h6);
59
+ line-height: var(--type-h6-line-height);
60
+ font-weight: var(--font-weight-semibold);
61
+ }
62
+
63
+ .ion-card__description {
64
+ margin: 0;
65
+ color: var(--text-secondary);
66
+ font-size: var(--type-body-sm);
67
+ line-height: var(--type-body-sm-line-height);
68
+ }
69
+
70
+ .ion-card__action {
71
+ display: flex;
72
+ flex-shrink: 0;
73
+ align-items: center;
74
+ gap: var(--spacing-8);
75
+ }
@@ -1,8 +1,10 @@
1
1
  /*
2
2
  * Citation
3
3
  *
4
- * NOT DRAWN IN FIGMA. The inline marker borrows Badge's pill geometry at the
5
- * smallest size; the footer list borrows the Table's row rhythm.
4
+ * Drawn in Figma as `Citation` (1167:247), `Citation List` (1168:246) and
5
+ * `Citation List Item` (1168:241). The inline marker borrows Badge's pill
6
+ * geometry at the smallest size; the footer list borrows the Table's row
7
+ * rhythm.
6
8
  */
7
9
  .ion-citation {
8
10
  display: inline-flex;
@@ -0,0 +1,250 @@
1
+ /*
2
+ * CommandPalette
3
+ *
4
+ * Drawn in Figma as `Command Palette` on its own page, with `Kbd`.
5
+ *
6
+ * panel 640 wide, radius/xl, surface/overlay, 1px border/subtle,
7
+ * shadow-xl — Modal's panel, without Modal's padding
8
+ * search 52 tall, 16 across, icon/secondary search glyph, a
9
+ * border/subtle rule below
10
+ * list 8 around, capped at 22rem, then it scrolls
11
+ * row 40 tall, 8 / 12, gap 12, radius/sm; the active row is
12
+ * surface/hover — the same highlight Combobox uses
13
+ * heading caption, text/tertiary, 8 / 12
14
+ * footer caption, text/tertiary, a border/subtle rule above
15
+ *
16
+ * TOP-ANCHORED, NOT CENTRED
17
+ *
18
+ * Modal centres its panel. A palette's panel changes height with every
19
+ * keystroke, and a centred one would jump up and down under the reader's eyes
20
+ * while they type. Pinned 96 from the top, only its bottom edge moves.
21
+ */
22
+ .ion-command-palette__scrim {
23
+ position: fixed;
24
+ inset: 0;
25
+ z-index: 1000;
26
+ display: flex;
27
+ align-items: flex-start;
28
+ justify-content: center;
29
+ padding: var(--spacing-96) var(--spacing-16) var(--spacing-16);
30
+ background-color: var(--surface-scrim);
31
+ }
32
+
33
+ .ion-command-palette {
34
+ display: flex;
35
+ flex-direction: column;
36
+ width: 640px;
37
+ max-width: 100%;
38
+ max-height: 100%;
39
+ overflow: hidden;
40
+ background-color: var(--surface-overlay);
41
+ border: var(--border-width-default) solid var(--border-subtle);
42
+ border-radius: var(--radius-xl);
43
+ box-shadow: var(--ion-shadow-shadow-xl);
44
+ font-family: var(--font-family-sans);
45
+ }
46
+
47
+ .ion-command-palette:focus {
48
+ outline: none;
49
+ }
50
+
51
+ .ion-command-palette__search {
52
+ display: flex;
53
+ flex-shrink: 0;
54
+ align-items: center;
55
+ gap: var(--spacing-12);
56
+ height: calc(var(--spacing-48) + var(--spacing-4));
57
+ padding-inline: var(--spacing-16);
58
+ border-bottom: var(--border-width-default) solid var(--border-subtle);
59
+ }
60
+
61
+ .ion-command-palette__search-icon {
62
+ display: inline-flex;
63
+ flex-shrink: 0;
64
+ color: var(--icon-secondary);
65
+ }
66
+
67
+ .ion-command-palette__search-icon svg {
68
+ width: var(--icon-size-md);
69
+ height: var(--icon-size-md);
70
+ }
71
+
72
+ /*
73
+ * No outline on the field: it is the only thing in the panel that ever has
74
+ * focus, focus lands there on open, and the caret already says so. A ring
75
+ * would sit inside a panel that is itself the focus context.
76
+ */
77
+ .ion-command-palette__input {
78
+ flex: 1 1 auto;
79
+ min-width: 0;
80
+ height: 100%;
81
+ margin: 0;
82
+ padding: 0;
83
+ border: none;
84
+ background: none;
85
+ color: var(--text-default);
86
+ font-family: inherit;
87
+ font-size: var(--type-body-lg);
88
+ line-height: var(--type-body-lg-line-height);
89
+ outline: none;
90
+ }
91
+
92
+ .ion-command-palette__input::placeholder {
93
+ color: var(--text-tertiary);
94
+ }
95
+
96
+ .ion-command-palette__list {
97
+ display: flex;
98
+ flex-direction: column;
99
+ gap: var(--spacing-2);
100
+ max-height: 22rem;
101
+ padding: var(--spacing-8);
102
+ overflow-y: auto;
103
+ overscroll-behavior: contain;
104
+ }
105
+
106
+ /* An empty listbox still pads; collapse it so the empty message sits alone. */
107
+ .ion-command-palette__list:empty {
108
+ display: none;
109
+ }
110
+
111
+ .ion-command-palette__section {
112
+ display: flex;
113
+ flex-direction: column;
114
+ gap: var(--spacing-2);
115
+ }
116
+
117
+ .ion-command-palette__section + .ion-command-palette__section,
118
+ .ion-command-palette__option + .ion-command-palette__section {
119
+ margin-top: var(--spacing-8);
120
+ }
121
+
122
+ .ion-command-palette__heading {
123
+ padding: var(--spacing-8) var(--spacing-12) var(--spacing-4);
124
+ color: var(--text-tertiary);
125
+ font-size: var(--type-caption);
126
+ line-height: var(--type-caption-line-height);
127
+ font-weight: var(--font-weight-medium);
128
+ }
129
+
130
+ /*
131
+ * The highlight is `data-focused` — Combobox's name for the same virtual
132
+ * focus — never :hover or :focus — focus stays in the
133
+ * field and the row is only ever the active descendant. Pointer movement sets
134
+ * the same attribute, so the mouse and the keyboard cannot highlight two rows.
135
+ */
136
+ .ion-command-palette__option {
137
+ display: flex;
138
+ align-items: center;
139
+ gap: var(--spacing-12);
140
+ min-height: var(--spacing-40);
141
+ padding: var(--spacing-8) var(--spacing-12);
142
+ border-radius: var(--radius-sm);
143
+ background-color: var(--surface-overlay);
144
+ color: var(--text-default);
145
+ font-size: var(--type-body);
146
+ line-height: var(--type-body-line-height);
147
+ cursor: pointer;
148
+ user-select: none;
149
+ }
150
+
151
+ .ion-command-palette__option[data-focused='true'] {
152
+ background-color: var(--surface-hover);
153
+ }
154
+
155
+ .ion-command-palette__option[data-disabled='true'] {
156
+ color: var(--text-disabled);
157
+ cursor: default;
158
+ }
159
+
160
+ .ion-command-palette__icon {
161
+ display: inline-flex;
162
+ flex-shrink: 0;
163
+ color: var(--icon-secondary);
164
+ }
165
+
166
+ .ion-command-palette__icon svg {
167
+ width: var(--icon-size-sm);
168
+ height: var(--icon-size-sm);
169
+ }
170
+
171
+ .ion-command-palette__option[data-disabled='true'] .ion-command-palette__icon {
172
+ color: var(--icon-disabled);
173
+ }
174
+
175
+ .ion-command-palette__text {
176
+ display: flex;
177
+ flex: 1 1 auto;
178
+ flex-direction: column;
179
+ min-width: 0;
180
+ }
181
+
182
+ .ion-command-palette__label,
183
+ .ion-command-palette__description {
184
+ overflow: hidden;
185
+ text-overflow: ellipsis;
186
+ white-space: nowrap;
187
+ }
188
+
189
+ .ion-command-palette__description {
190
+ color: var(--text-tertiary);
191
+ font-size: var(--type-body-sm);
192
+ line-height: var(--type-body-sm-line-height);
193
+ }
194
+
195
+ .ion-command-palette__option[data-disabled='true']
196
+ .ion-command-palette__description {
197
+ color: var(--text-disabled);
198
+ }
199
+
200
+ .ion-command-palette__shortcut {
201
+ flex-shrink: 0;
202
+ }
203
+
204
+ .ion-command-palette__empty {
205
+ padding: var(--spacing-32) var(--spacing-16);
206
+ color: var(--text-tertiary);
207
+ font-size: var(--type-body);
208
+ line-height: var(--type-body-line-height);
209
+ text-align: center;
210
+ }
211
+
212
+ .ion-command-palette__footer {
213
+ display: flex;
214
+ flex-shrink: 0;
215
+ flex-wrap: wrap;
216
+ align-items: center;
217
+ gap: var(--spacing-16);
218
+ padding: var(--spacing-8) var(--spacing-16);
219
+ border-top: var(--border-width-default) solid var(--border-subtle);
220
+ color: var(--text-tertiary);
221
+ font-size: var(--type-caption);
222
+ line-height: var(--type-caption-line-height);
223
+ }
224
+
225
+ .ion-command-palette__hint {
226
+ display: inline-flex;
227
+ align-items: center;
228
+ gap: var(--spacing-4);
229
+ }
230
+
231
+ /*
232
+ * A phone has no ⌘K and usually no arrow keys, so the hints are for nobody;
233
+ * the list gets the room instead.
234
+ */
235
+ @media (width <= 40rem) {
236
+ .ion-command-palette__scrim {
237
+ padding-top: var(--spacing-16);
238
+ }
239
+
240
+ .ion-command-palette__footer {
241
+ display: none;
242
+ }
243
+ }
244
+
245
+ @media (forced-colors: active) {
246
+ .ion-command-palette__option[data-focused='true'] {
247
+ outline: var(--border-width-thick) solid Highlight;
248
+ outline-offset: calc(-1 * var(--border-width-thick));
249
+ }
250
+ }