@skillcdn/console 0.3.1 → 0.4.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 (179) hide show
  1. package/README.md +4 -4
  2. package/dist/api.d.ts +4 -4
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +4 -4
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts +4 -0
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +201 -25
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +15 -11
  12. package/dist/cli/format.js.map +1 -1
  13. package/dist/cli/help.d.ts.map +1 -1
  14. package/dist/cli/help.js +59 -26
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/cli/main.js +1 -0
  17. package/dist/cli/main.js.map +1 -1
  18. package/dist/cli/pack.d.ts +7 -2
  19. package/dist/cli/pack.d.ts.map +1 -1
  20. package/dist/cli/pack.js +16 -2
  21. package/dist/cli/pack.js.map +1 -1
  22. package/dist/client.d.ts +20 -10
  23. package/dist/client.d.ts.map +1 -1
  24. package/dist/client.js +17 -3
  25. package/dist/client.js.map +1 -1
  26. package/dist/compat.d.ts +28 -6
  27. package/dist/compat.d.ts.map +1 -1
  28. package/dist/compat.js +55 -14
  29. package/dist/compat.js.map +1 -1
  30. package/dist/components/board.d.ts +12 -1
  31. package/dist/components/board.d.ts.map +1 -1
  32. package/dist/components/board.js +57 -7
  33. package/dist/components/board.js.map +1 -1
  34. package/dist/components/calendar.d.ts +17 -7
  35. package/dist/components/calendar.d.ts.map +1 -1
  36. package/dist/components/calendar.js +89 -38
  37. package/dist/components/calendar.js.map +1 -1
  38. package/dist/components/decision-form.d.ts +12 -4
  39. package/dist/components/decision-form.d.ts.map +1 -1
  40. package/dist/components/decision-form.js +24 -6
  41. package/dist/components/decision-form.js.map +1 -1
  42. package/dist/components/decision-list.d.ts +4 -0
  43. package/dist/components/decision-list.d.ts.map +1 -1
  44. package/dist/components/decision-list.js +9 -6
  45. package/dist/components/decision-list.js.map +1 -1
  46. package/dist/components/documents.d.ts.map +1 -1
  47. package/dist/components/documents.js +3 -3
  48. package/dist/components/documents.js.map +1 -1
  49. package/dist/components/event-feed.d.ts.map +1 -1
  50. package/dist/components/event-feed.js +63 -8
  51. package/dist/components/event-feed.js.map +1 -1
  52. package/dist/components/my-decisions.d.ts +20 -0
  53. package/dist/components/my-decisions.d.ts.map +1 -0
  54. package/dist/components/my-decisions.js +36 -0
  55. package/dist/components/my-decisions.js.map +1 -0
  56. package/dist/components/people.d.ts.map +1 -1
  57. package/dist/components/people.js +1 -1
  58. package/dist/components/people.js.map +1 -1
  59. package/dist/components/picker.d.ts +39 -0
  60. package/dist/components/picker.d.ts.map +1 -0
  61. package/dist/components/picker.js +155 -0
  62. package/dist/components/picker.js.map +1 -0
  63. package/dist/components/projects.d.ts.map +1 -1
  64. package/dist/components/projects.js +3 -3
  65. package/dist/components/projects.js.map +1 -1
  66. package/dist/components/roadmap.js +1 -1
  67. package/dist/components/roadmap.js.map +1 -1
  68. package/dist/components/runs.d.ts.map +1 -1
  69. package/dist/components/runs.js +1 -1
  70. package/dist/components/runs.js.map +1 -1
  71. package/dist/components/shell.d.ts.map +1 -1
  72. package/dist/components/shell.js +36 -1
  73. package/dist/components/shell.js.map +1 -1
  74. package/dist/components/task-form.d.ts +18 -4
  75. package/dist/components/task-form.d.ts.map +1 -1
  76. package/dist/components/task-form.js +56 -13
  77. package/dist/components/task-form.js.map +1 -1
  78. package/dist/components/task-list.js +2 -2
  79. package/dist/components/task-list.js.map +1 -1
  80. package/dist/components/task-view.d.ts +7 -1
  81. package/dist/components/task-view.d.ts.map +1 -1
  82. package/dist/components/task-view.js +7 -6
  83. package/dist/components/task-view.js.map +1 -1
  84. package/dist/components/ui.d.ts +17 -0
  85. package/dist/components/ui.d.ts.map +1 -1
  86. package/dist/components/ui.js +26 -11
  87. package/dist/components/ui.js.map +1 -1
  88. package/dist/components/work.d.ts.map +1 -1
  89. package/dist/components/work.js +5 -3
  90. package/dist/components/work.js.map +1 -1
  91. package/dist/console.css +1794 -433
  92. package/dist/console.d.ts.map +1 -1
  93. package/dist/console.js +93 -10
  94. package/dist/console.js.map +1 -1
  95. package/dist/data.d.ts +21 -1
  96. package/dist/data.d.ts.map +1 -1
  97. package/dist/data.js +24 -1
  98. package/dist/data.js.map +1 -1
  99. package/dist/i18n/en.d.ts +45 -3
  100. package/dist/i18n/en.d.ts.map +1 -1
  101. package/dist/i18n/en.js +52 -10
  102. package/dist/i18n/en.js.map +1 -1
  103. package/dist/i18n/ko.d.ts.map +1 -1
  104. package/dist/i18n/ko.js +54 -12
  105. package/dist/i18n/ko.js.map +1 -1
  106. package/dist/index.d.ts +5 -3
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +5 -3
  109. package/dist/index.js.map +1 -1
  110. package/dist/limits.d.ts +2 -0
  111. package/dist/limits.d.ts.map +1 -1
  112. package/dist/limits.js +2 -0
  113. package/dist/limits.js.map +1 -1
  114. package/dist/router.d.ts +9 -1
  115. package/dist/router.d.ts.map +1 -1
  116. package/dist/router.js +12 -0
  117. package/dist/router.js.map +1 -1
  118. package/dist/routes.d.ts +3 -1
  119. package/dist/routes.d.ts.map +1 -1
  120. package/dist/routes.js +3 -1
  121. package/dist/routes.js.map +1 -1
  122. package/dist/rules.js +1 -1
  123. package/dist/rules.js.map +1 -1
  124. package/dist/schemas.d.ts +252 -12
  125. package/dist/schemas.d.ts.map +1 -1
  126. package/dist/schemas.js +27 -10
  127. package/dist/schemas.js.map +1 -1
  128. package/dist/vocabulary.d.ts +2 -0
  129. package/dist/vocabulary.d.ts.map +1 -1
  130. package/dist/vocabulary.js +2 -0
  131. package/dist/vocabulary.js.map +1 -1
  132. package/docs/specs/cli.md +14 -13
  133. package/docs/specs/rest.md +24 -19
  134. package/package.json +2 -2
  135. package/skills/building-a-console/SKILL.md +4 -4
  136. package/skills/building-a-console/references/the-default-console.md +4 -4
  137. package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
  138. package/skills/deploying-a-console/SKILL.md +2 -2
  139. package/skills/deploying-a-console/references/resources.md +1 -1
  140. package/skills/working-the-board/SKILL.md +8 -6
  141. package/skills/working-the-board/references/terms.md +3 -3
  142. package/src/api.ts +7 -0
  143. package/src/cli/cli.ts +254 -25
  144. package/src/cli/format.ts +22 -11
  145. package/src/cli/help.ts +63 -26
  146. package/src/cli/main.ts +1 -0
  147. package/src/cli/pack.ts +17 -1
  148. package/src/client.ts +44 -11
  149. package/src/compat.ts +63 -14
  150. package/src/components/board.tsx +124 -25
  151. package/src/components/calendar.tsx +258 -117
  152. package/src/components/decision-form.tsx +71 -23
  153. package/src/components/decision-list.tsx +30 -3
  154. package/src/components/documents.tsx +27 -11
  155. package/src/components/event-feed.tsx +116 -33
  156. package/src/components/my-decisions.tsx +138 -0
  157. package/src/components/people.tsx +3 -1
  158. package/src/components/picker.tsx +297 -0
  159. package/src/components/projects.tsx +7 -5
  160. package/src/components/roadmap.tsx +1 -1
  161. package/src/components/runs.tsx +5 -1
  162. package/src/components/shell.tsx +36 -1
  163. package/src/components/task-form.tsx +149 -63
  164. package/src/components/task-list.tsx +3 -3
  165. package/src/components/task-view.tsx +71 -56
  166. package/src/components/ui.tsx +89 -11
  167. package/src/components/work.tsx +6 -4
  168. package/src/console.tsx +157 -29
  169. package/src/data.ts +61 -1
  170. package/src/i18n/en.ts +54 -10
  171. package/src/i18n/ko.ts +56 -12
  172. package/src/index.ts +19 -1
  173. package/src/limits.ts +2 -0
  174. package/src/router.ts +18 -1
  175. package/src/routes.ts +3 -1
  176. package/src/rules.ts +1 -1
  177. package/src/schemas.ts +29 -9
  178. package/src/styles/console.css +1794 -433
  179. package/src/vocabulary.ts +3 -0
@@ -0,0 +1,297 @@
1
+ import {
2
+ type KeyboardEvent,
3
+ type ReactNode,
4
+ useEffect,
5
+ useId,
6
+ useLayoutEffect,
7
+ useRef,
8
+ useState,
9
+ } from "react";
10
+ import { useWords } from "../i18n/index.js";
11
+ import { cx } from "./ui.js";
12
+
13
+ // Choosing one thing out of a listing too long to offer whole (ADR-0037): the task a new one is
14
+ // part of, the goal it works towards. The picker asks for what the words typed find, a page at a
15
+ // time, and never holds the listing: the first page as soon as the field is entered, the words'
16
+ // page once the typing rests, the next page on a press. What is chosen stands in the field's
17
+ // place, and a press clears it. Takes a search and nothing else from the network.
18
+
19
+ /** How many items one page of a picker holds: enough to find the one meant, few enough to read at a glance. */
20
+ export const PICKER_PAGE_LIMIT = 10;
21
+ /** How long the typing rests before the words are searched. */
22
+ const SEARCH_DELAY_MS = 200;
23
+
24
+ export interface PickerPage<T> {
25
+ readonly items: readonly T[];
26
+ readonly more: boolean;
27
+ }
28
+
29
+ /** What the words find, from `offset` on; the first page of everything while the words are empty. */
30
+ export type PickerSearch<T> = (
31
+ query: string,
32
+ page: { readonly limit: number; readonly offset: number },
33
+ signal: AbortSignal,
34
+ ) => Promise<PickerPage<T>>;
35
+
36
+ /** How far the list stands from the field, in pixels, as the stylesheet has it. */
37
+ const LIST_GAP = 4;
38
+
39
+ /**
40
+ * Whether the list opens above the field: only where the window has no room for it below the
41
+ * field and more room above than below; else below, as a list does.
42
+ */
43
+ export function opensAbove(
44
+ field: { readonly top: number; readonly bottom: number },
45
+ listHeight: number,
46
+ windowHeight: number,
47
+ ): boolean {
48
+ const below = windowHeight - field.bottom - LIST_GAP;
49
+ const over = field.top - LIST_GAP;
50
+ return listHeight > below && over > below;
51
+ }
52
+
53
+ export interface PickerProps<T> {
54
+ /** What is chosen, if anything. */
55
+ readonly value: T | undefined;
56
+ /** What stands for a choice made but not read back yet: shown in the chip's place, and cleared as one. */
57
+ readonly held?: ReactNode | undefined;
58
+ readonly search: PickerSearch<T>;
59
+ readonly keyOf: (item: T) => string;
60
+ /** How an item reads, in the list and once chosen. */
61
+ readonly render: (item: T) => ReactNode;
62
+ readonly onChange: (item: T | undefined) => void;
63
+ /** The field's name, for the input and the list. */
64
+ readonly label: string;
65
+ readonly placeholder: string;
66
+ readonly disabled?: boolean | undefined;
67
+ /** What the search may answer with but cannot be chosen: a task, as its own parent. */
68
+ readonly exclude?: ((item: T) => boolean) | undefined;
69
+ }
70
+
71
+ export function Picker<T>(props: PickerProps<T>) {
72
+ const words = useWords().common;
73
+ const listId = useId();
74
+ const [query, setQuery] = useState("");
75
+ const [open, setOpen] = useState(false);
76
+ const [items, setItems] = useState<readonly T[]>([]);
77
+ const [more, setMore] = useState(false);
78
+ const [loading, setLoading] = useState(false);
79
+ const [active, setActive] = useState(-1);
80
+ const [above, setAbove] = useState(false);
81
+ const field = useRef<HTMLInputElement>(null);
82
+ const list = useRef<HTMLDivElement>(null);
83
+ // The search and the exclusion as the page last gave them, read at the moment of a request,
84
+ // so that a page rendering again starts no search over.
85
+ const search = useRef(props.search);
86
+ search.current = props.search;
87
+ const exclude = useRef(props.exclude);
88
+ exclude.current = props.exclude;
89
+ const latest = useRef(query);
90
+ latest.current = query;
91
+
92
+ // The list opens below the field, and above it only where the window has no room below and
93
+ // more above: measured before anything is painted, once it is open and again whenever what it
94
+ // holds changes its height, so that it never shows below for a frame on its way up.
95
+ // biome-ignore lint/correctness/useExhaustiveDependencies: the items, the loading and the more are what changes the list's height
96
+ useLayoutEffect(() => {
97
+ if (!open || field.current === null || list.current === null) {
98
+ return;
99
+ }
100
+ setAbove(
101
+ opensAbove(
102
+ field.current.getBoundingClientRect(),
103
+ list.current.offsetHeight,
104
+ window.innerHeight,
105
+ ),
106
+ );
107
+ }, [open, items, loading, more]);
108
+
109
+ // The words' page once the typing rests; the first page of everything at once on entering.
110
+ useEffect(() => {
111
+ if (!open) {
112
+ return;
113
+ }
114
+ const controller = new AbortController();
115
+ const timer = setTimeout(
116
+ () => {
117
+ setLoading(true);
118
+ search
119
+ .current(query, { limit: PICKER_PAGE_LIMIT, offset: 0 }, controller.signal)
120
+ .then((page) => {
121
+ if (!controller.signal.aborted) {
122
+ setItems(page.items.filter((item) => exclude.current?.(item) !== true));
123
+ setMore(page.more);
124
+ setActive(-1);
125
+ setLoading(false);
126
+ }
127
+ })
128
+ .catch(() => {
129
+ if (!controller.signal.aborted) {
130
+ setItems([]);
131
+ setMore(false);
132
+ setLoading(false);
133
+ }
134
+ });
135
+ },
136
+ query === "" ? 0 : SEARCH_DELAY_MS,
137
+ );
138
+ return () => {
139
+ clearTimeout(timer);
140
+ controller.abort();
141
+ };
142
+ }, [open, query]);
143
+
144
+ /** The next page of the same words, put under the ones shown; dropped when the words moved on. */
145
+ const loadMore = () => {
146
+ const wanted = query;
147
+ setLoading(true);
148
+ search
149
+ .current(
150
+ wanted,
151
+ { limit: PICKER_PAGE_LIMIT, offset: items.length },
152
+ new AbortController().signal,
153
+ )
154
+ .then((page) => {
155
+ if (wanted !== latest.current) {
156
+ return;
157
+ }
158
+ setItems((current) => [
159
+ ...current,
160
+ ...page.items.filter((item) => exclude.current?.(item) !== true),
161
+ ]);
162
+ setMore(page.more);
163
+ setLoading(false);
164
+ })
165
+ .catch(() => {
166
+ if (wanted === latest.current) {
167
+ setMore(false);
168
+ setLoading(false);
169
+ }
170
+ });
171
+ };
172
+
173
+ const choose = (item: T) => {
174
+ props.onChange(item);
175
+ setQuery("");
176
+ setOpen(false);
177
+ };
178
+
179
+ const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
180
+ if (event.key === "ArrowDown") {
181
+ event.preventDefault();
182
+ if (!open) {
183
+ setOpen(true);
184
+ return;
185
+ }
186
+ setActive((current) => (items.length === 0 ? -1 : (current + 1) % items.length));
187
+ } else if (event.key === "ArrowUp") {
188
+ event.preventDefault();
189
+ setActive((current) =>
190
+ items.length === 0 ? -1 : current <= 0 ? items.length - 1 : current - 1,
191
+ );
192
+ } else if (event.key === "Enter") {
193
+ // A choice, or nothing: the form around the field is never sent by this key.
194
+ event.preventDefault();
195
+ const chosen = active >= 0 ? items[active] : undefined;
196
+ if (chosen !== undefined) {
197
+ choose(chosen);
198
+ }
199
+ } else if (event.key === "Escape" && open) {
200
+ event.preventDefault();
201
+ setOpen(false);
202
+ }
203
+ };
204
+
205
+ if (props.value !== undefined || props.held !== undefined) {
206
+ return (
207
+ <div className="sc-picker sc-picker-chosen">
208
+ <span className="sc-picker-value">
209
+ {props.value === undefined ? props.held : props.render(props.value)}
210
+ </span>
211
+ <button
212
+ type="button"
213
+ className="sc-picker-clear"
214
+ aria-label={words.remove}
215
+ disabled={props.disabled}
216
+ onClick={() => props.onChange(undefined)}
217
+ >
218
+ <span aria-hidden="true">&times;</span>
219
+ </button>
220
+ </div>
221
+ );
222
+ }
223
+
224
+ return (
225
+ <div className="sc-picker">
226
+ <input
227
+ ref={field}
228
+ className="sc-input"
229
+ type="text"
230
+ role="combobox"
231
+ aria-label={props.label}
232
+ aria-expanded={open}
233
+ aria-controls={listId}
234
+ aria-autocomplete="list"
235
+ aria-activedescendant={active >= 0 ? `${listId}-${active}` : undefined}
236
+ autoComplete="off"
237
+ placeholder={props.placeholder}
238
+ value={query}
239
+ disabled={props.disabled}
240
+ onChange={(event) => {
241
+ setQuery(event.target.value);
242
+ setOpen(true);
243
+ }}
244
+ onFocus={() => setOpen(true)}
245
+ onBlur={() => setOpen(false)}
246
+ onKeyDown={onKeyDown}
247
+ />
248
+ {open && (
249
+ // The list of a combobox: read and chosen through the input, which keeps the focus; an
250
+ // option is chosen on the press, before the input loses its focus and the list closes.
251
+ <div
252
+ ref={list}
253
+ className={cx("sc-picker-list", above && "sc-picker-list-above")}
254
+ id={listId}
255
+ role="listbox"
256
+ aria-label={props.label}
257
+ >
258
+ {items.map((item, index) => (
259
+ <div
260
+ key={props.keyOf(item)}
261
+ id={`${listId}-${index}`}
262
+ role="option"
263
+ tabIndex={-1}
264
+ aria-selected={index === active}
265
+ className={cx("sc-picker-option", index === active && "sc-picker-option-active")}
266
+ onMouseDown={(event) => {
267
+ event.preventDefault();
268
+ choose(item);
269
+ }}
270
+ onMouseEnter={() => setActive(index)}
271
+ >
272
+ {props.render(item)}
273
+ </div>
274
+ ))}
275
+ {!loading && items.length === 0 && (
276
+ <div className="sc-picker-note sc-muted">{words.picker.nothingFound}</div>
277
+ )}
278
+ {loading && <div className="sc-picker-note sc-muted">{words.picker.searching}</div>}
279
+ {more && !loading && (
280
+ <div className="sc-picker-note">
281
+ <button
282
+ type="button"
283
+ className="sc-button sc-button-ghost sc-button-sm"
284
+ onMouseDown={(event) => {
285
+ event.preventDefault();
286
+ loadMore();
287
+ }}
288
+ >
289
+ {words.picker.more}
290
+ </button>
291
+ </div>
292
+ )}
293
+ </div>
294
+ )}
295
+ </div>
296
+ );
297
+ }
@@ -93,7 +93,7 @@ export const VISIBILITY_LABELS: Readonly<Record<ProjectVisibility, string>> = {
93
93
  export function ProjectRoleBadge(props: { readonly role: ProjectRole }) {
94
94
  const words = useWords();
95
95
  return (
96
- <Badge tone={props.role === "owner" ? "point" : "neutral"}>
96
+ <Badge tone={props.role === "owner" ? "accent" : "neutral"}>
97
97
  {words.vocabulary.projectRole[props.role]}
98
98
  </Badge>
99
99
  );
@@ -143,7 +143,9 @@ export function ProjectList(props: ProjectListProps) {
143
143
  )}
144
144
  {project.openDecisions > 0 && (
145
145
  <p className="sc-project-meta">
146
- <Badge tone="warning">{words.decisionsWaiting(project.openDecisions)}</Badge>
146
+ <Badge tone="warning" kind="count">
147
+ {words.decisionsWaiting(project.openDecisions)}
148
+ </Badge>
147
149
  </p>
148
150
  )}
149
151
  </li>
@@ -444,11 +446,11 @@ export function MemberList(props: MemberListProps) {
444
446
  {props.members.map((member) => (
445
447
  <li key={member.person.id} className="sc-person-row">
446
448
  <PersonChip person={member.person} size="md" />
449
+ {props.me?.id === member.person.id && (
450
+ <span className="sc-person-you">{words.common.you}</span>
451
+ )}
447
452
  <span className="sc-person-name">
448
453
  {member.person.name ?? ""}
449
- {props.me?.id === member.person.id && (
450
- <span className="sc-muted"> {words.common.you}</span>
451
- )}
452
454
  <span className="sc-muted">
453
455
  {" "}
454
456
  {members.since} <Time iso={member.addedAt} />
@@ -157,7 +157,7 @@ export function GoalTimeline(props: GoalTimelineProps) {
157
157
  return (
158
158
  <div key={goal.id} className={cx("sc-timeline-row", `sc-goal-${goal.state}`)}>
159
159
  <div className="sc-timeline-label">
160
- <a className="sc-timeline-goal" href={props.goalHref(goal)}>
160
+ <a className="sc-timeline-goal" href={props.goalHref(goal)} title={goal.title}>
161
161
  {goal.title}
162
162
  </a>
163
163
  <GoalStateBadge state={goal.state} />
@@ -32,7 +32,11 @@ export function RunStatusBadge(props: { readonly status: RunStatus }) {
32
32
  : props.status === "failed"
33
33
  ? "danger"
34
34
  : "neutral";
35
- return <Badge tone={tone}>{words.vocabulary.runStatus[props.status]}</Badge>;
35
+ return (
36
+ <Badge tone={tone} kind="state">
37
+ {words.vocabulary.runStatus[props.status]}
38
+ </Badge>
39
+ );
36
40
  }
37
41
 
38
42
  export interface RunCardProps {
@@ -186,6 +186,41 @@ function PersonMenu(props: {
186
186
 
187
187
  export function Shell(props: ShellProps) {
188
188
  const words = useWords();
189
+ // Where the tabs scroll sideways, the current one is brought into the bar's view.
190
+ const tabs = useRef<HTMLElement>(null);
191
+ const current = props.nav.find((item) => item.current)?.href;
192
+ useEffect(() => {
193
+ const bar = tabs.current;
194
+ if (current === undefined || bar === null) {
195
+ return;
196
+ }
197
+ const bring = () => {
198
+ const tab = bar.querySelector<HTMLElement>(".sc-tab-current");
199
+ if (tab === null) {
200
+ return;
201
+ }
202
+ const left = tab.offsetLeft - bar.offsetLeft;
203
+ // The last part of the bar fades where it scrolls (the stylesheet's mask): a tab there
204
+ // counts as out of view.
205
+ const seen = bar.scrollWidth > bar.clientWidth ? bar.clientWidth * 0.85 : bar.clientWidth;
206
+ if (left < bar.scrollLeft || left + tab.offsetWidth > bar.scrollLeft + seen) {
207
+ bar.scrollLeft = left - (bar.clientWidth - tab.offsetWidth) / 2;
208
+ }
209
+ };
210
+ bring();
211
+ // The tabs grow once the page's face has loaded, and the bar narrows with the window: either
212
+ // may put the current tab out of view again.
213
+ if (typeof ResizeObserver === "undefined") {
214
+ return;
215
+ }
216
+ const watch = new ResizeObserver(bring);
217
+ watch.observe(bar);
218
+ const inner = bar.firstElementChild;
219
+ if (inner !== null) {
220
+ watch.observe(inner);
221
+ }
222
+ return () => watch.disconnect();
223
+ }, [current]);
189
224
  const follow = (href: string) => (event: React.MouseEvent) => {
190
225
  if (isPlainClick(event)) {
191
226
  event.preventDefault();
@@ -259,7 +294,7 @@ export function Shell(props: ShellProps) {
259
294
  </div>
260
295
  </div>
261
296
  {props.nav.length > 0 && (
262
- <nav className="sc-tabs" aria-label={words.shell.pages}>
297
+ <nav ref={tabs} className="sc-tabs" aria-label={words.shell.pages}>
263
298
  <div className="sc-tabs-inner">
264
299
  {props.nav.map((item) => (
265
300
  <a