github-issue-tower-defence-management 1.101.4 → 1.101.5

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.
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>TDPM Console</title>
7
- <script type="module" crossorigin src="/assets/index-DP5DGKUj.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-ES6SLB1Y.css">
7
+ <script type="module" crossorigin src="/assets/index-DX0mapkS.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-D4zghbcI.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-issue-tower-defence-management",
3
- "version": "1.101.4",
3
+ "version": "1.101.5",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "scripts": {
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from 'react';
1
+ import type { ReactNode, Ref } from 'react';
2
2
  import type { ImageProxyUrlBuilder } from '../../lib/imageProxy';
3
3
  import { colorFromEnum } from '../../logic/colors';
4
4
  import {
@@ -55,6 +55,7 @@ export type ConsoleItemDetailProps = {
55
55
  now: number;
56
56
  commentComposer: ReactNode;
57
57
  operationBar: ReactNode;
58
+ scrollRef?: Ref<HTMLElement>;
58
59
  buildImageProxyUrl?: ImageProxyUrlBuilder;
59
60
  };
60
61
 
@@ -80,6 +81,7 @@ export const ConsoleItemDetail = ({
80
81
  now,
81
82
  commentComposer,
82
83
  operationBar,
84
+ scrollRef,
83
85
  buildImageProxyUrl,
84
86
  }: ConsoleItemDetailProps) => {
85
87
  const resolvedState = state?.state ?? 'open';
@@ -98,166 +100,170 @@ export const ConsoleItemDetail = ({
98
100
  commitsAreLoading || commitsError !== null ? null : commits.length;
99
101
 
100
102
  return (
101
- <article className="console-detail">
102
- {storyName !== null && (
103
- <div className="console-detail-story">
104
- <span className="console-storytag">
105
- <span
106
- className="console-story-dot"
107
- style={{ backgroundColor: storyPalette.dot }}
108
- />
109
- {storyName}
110
- </span>
111
- </div>
112
- )}
113
-
114
- {overlayStatus !== null && statusPalette !== null && (
115
- <span
116
- className="console-detail-status-chip"
117
- style={{
118
- color: statusPalette.fg,
119
- borderColor: statusPalette.border,
120
- backgroundColor: statusPalette.bg,
121
- }}
122
- >
123
- {overlayStatus.name}
124
- </span>
125
- )}
103
+ <>
104
+ <article className="console-detail" ref={scrollRef}>
105
+ {storyName !== null && (
106
+ <div className="console-detail-story">
107
+ <span className="console-storytag">
108
+ <span
109
+ className="console-story-dot"
110
+ style={{ backgroundColor: storyPalette.dot }}
111
+ />
112
+ {storyName}
113
+ </span>
114
+ </div>
115
+ )}
126
116
 
127
- <h2 className="console-detail-title">
128
- <ConsoleItemIcon
129
- isPr={item.isPr}
130
- state={resolvedState}
131
- merged={merged}
132
- isDraft={false}
133
- stateReason=""
134
- />
135
- <span className="console-detail-title-text">{item.title}</span>
136
- <span className="console-detail-number">
137
- {item.isPr ? `PR #${item.number}` : `#${item.number}`}
138
- </span>
139
- {closedStateLabel !== null && (
140
- <span className="console-detail-closed-label">
141
- {closedStateLabel}
117
+ {overlayStatus !== null && statusPalette !== null && (
118
+ <span
119
+ className="console-detail-status-chip"
120
+ style={{
121
+ color: statusPalette.fg,
122
+ borderColor: statusPalette.border,
123
+ backgroundColor: statusPalette.bg,
124
+ }}
125
+ >
126
+ {overlayStatus.name}
142
127
  </span>
143
128
  )}
144
- </h2>
145
-
146
- <div className="console-detail-subbar">
147
- <a
148
- href={item.url}
149
- className="console-detail-link"
150
- target="_blank"
151
- rel="noopener noreferrer"
152
- >
153
- {item.isPr ? `PR #${item.number}` : `Issue #${item.number}`}
154
- </a>
155
- <span className="console-detail-repo">{item.repo}</span>
156
- <span className="console-detail-pill">
157
- {item.isPr ? 'PR' : 'Issue'}
158
- </span>
159
- </div>
160
129
 
161
- {item.labels.length > 0 && (
162
- <div className="console-detail-labels">
163
- {item.labels.map((label) => (
164
- <span key={label} className="console-label-chip">
165
- {label}
130
+ <h2 className="console-detail-title">
131
+ <ConsoleItemIcon
132
+ isPr={item.isPr}
133
+ state={resolvedState}
134
+ merged={merged}
135
+ isDraft={false}
136
+ stateReason=""
137
+ />
138
+ <span className="console-detail-title-text">{item.title}</span>
139
+ <span className="console-detail-number">
140
+ {item.isPr ? `PR #${item.number}` : `#${item.number}`}
141
+ </span>
142
+ {closedStateLabel !== null && (
143
+ <span className="console-detail-closed-label">
144
+ {closedStateLabel}
166
145
  </span>
167
- ))}
168
- </div>
169
- )}
170
-
171
- <div
172
- className="console-detail-createdat"
173
- title={formatFullTimestamp(item.createdAt)}
174
- >
175
- opened {formatRelativeTime(item.createdAt, now)}
176
- </div>
146
+ )}
147
+ </h2>
177
148
 
178
- <ConsolePanel
179
- title="Description"
180
- headerAction={
149
+ <div className="console-detail-subbar">
181
150
  <a
182
151
  href={item.url}
183
- className="console-panel-open-link"
152
+ className="console-detail-link"
184
153
  target="_blank"
185
154
  rel="noopener noreferrer"
186
155
  >
187
- open
156
+ {item.isPr ? `PR #${item.number}` : `Issue #${item.number}`}
188
157
  </a>
189
- }
190
- >
191
- {bodyError !== null ? (
192
- <p role="alert" className="console-detail-body-error">
193
- Failed to load description: {bodyError}
194
- </p>
195
- ) : bodyIsLoading ? (
196
- <p className="console-detail-body-loading">Loading description...</p>
197
- ) : (
198
- <ConsoleMarkdownContent
199
- body={body}
200
- buildImageProxyUrl={buildImageProxyUrl}
201
- />
158
+ <span className="console-detail-repo">{item.repo}</span>
159
+ <span className="console-detail-pill">
160
+ {item.isPr ? 'PR' : 'Issue'}
161
+ </span>
162
+ </div>
163
+
164
+ {item.labels.length > 0 && (
165
+ <div className="console-detail-labels">
166
+ {item.labels.map((label) => (
167
+ <span key={label} className="console-label-chip">
168
+ {label}
169
+ </span>
170
+ ))}
171
+ </div>
202
172
  )}
203
- </ConsolePanel>
204
173
 
205
- {item.isPr && (
206
- <ConsolePanel title="Changed files" count={filesCount}>
207
- <ConsoleChangedFileList
208
- files={files}
209
- isLoading={filesAreLoading}
210
- error={filesError}
211
- />
174
+ <div
175
+ className="console-detail-createdat"
176
+ title={formatFullTimestamp(item.createdAt)}
177
+ >
178
+ opened {formatRelativeTime(item.createdAt, now)}
179
+ </div>
180
+
181
+ <ConsolePanel
182
+ title="Description"
183
+ headerAction={
184
+ <a
185
+ href={item.url}
186
+ className="console-panel-open-link"
187
+ target="_blank"
188
+ rel="noopener noreferrer"
189
+ >
190
+ open
191
+ </a>
192
+ }
193
+ >
194
+ {bodyError !== null ? (
195
+ <p role="alert" className="console-detail-body-error">
196
+ Failed to load description: {bodyError}
197
+ </p>
198
+ ) : bodyIsLoading ? (
199
+ <p className="console-detail-body-loading">
200
+ Loading description...
201
+ </p>
202
+ ) : (
203
+ <ConsoleMarkdownContent
204
+ body={body}
205
+ buildImageProxyUrl={buildImageProxyUrl}
206
+ />
207
+ )}
212
208
  </ConsolePanel>
213
- )}
214
209
 
215
- <ConsolePanel
216
- title="Comments"
217
- count={commentsCount}
218
- defaultCollapsed={item.isPr}
219
- >
220
- <ConsoleCommentList
221
- comments={comments}
222
- isLoading={commentsAreLoading}
223
- error={commentsError}
224
- now={now}
225
- buildImageProxyUrl={buildImageProxyUrl}
226
- />
227
- </ConsolePanel>
210
+ {item.isPr && (
211
+ <ConsolePanel title="Changed files" count={filesCount}>
212
+ <ConsoleChangedFileList
213
+ files={files}
214
+ isLoading={filesAreLoading}
215
+ error={filesError}
216
+ />
217
+ </ConsolePanel>
218
+ )}
228
219
 
229
- {item.isPr && (
230
- <ConsolePanel title="Commits" count={commitsCount} defaultCollapsed>
231
- <ConsoleCommitList
232
- commits={commits}
233
- isLoading={commitsAreLoading}
234
- error={commitsError}
220
+ <ConsolePanel
221
+ title="Comments"
222
+ count={commentsCount}
223
+ defaultCollapsed={item.isPr}
224
+ >
225
+ <ConsoleCommentList
226
+ comments={comments}
227
+ isLoading={commentsAreLoading}
228
+ error={commentsError}
235
229
  now={now}
230
+ buildImageProxyUrl={buildImageProxyUrl}
236
231
  />
237
232
  </ConsolePanel>
238
- )}
239
233
 
240
- {!item.isPr &&
241
- relatedPullRequests.map((related) => (
242
- <ConsolePullRequestDetail
243
- key={related.pullRequest.url}
244
- pullRequest={related.pullRequest}
245
- body={related.pullRequest.summary?.body ?? ''}
246
- bodyIsLoading={false}
247
- files={related.files}
248
- filesAreLoading={related.filesAreLoading}
249
- filesError={related.filesError}
250
- commits={related.commits}
251
- commitsAreLoading={related.commitsAreLoading}
252
- commitsError={related.commitsError}
253
- now={now}
254
- buildImageProxyUrl={buildImageProxyUrl}
255
- />
256
- ))}
234
+ {item.isPr && (
235
+ <ConsolePanel title="Commits" count={commitsCount} defaultCollapsed>
236
+ <ConsoleCommitList
237
+ commits={commits}
238
+ isLoading={commitsAreLoading}
239
+ error={commitsError}
240
+ now={now}
241
+ />
242
+ </ConsolePanel>
243
+ )}
244
+
245
+ {!item.isPr &&
246
+ relatedPullRequests.map((related) => (
247
+ <ConsolePullRequestDetail
248
+ key={related.pullRequest.url}
249
+ pullRequest={related.pullRequest}
250
+ body={related.pullRequest.summary?.body ?? ''}
251
+ bodyIsLoading={false}
252
+ files={related.files}
253
+ filesAreLoading={related.filesAreLoading}
254
+ filesError={related.filesError}
255
+ commits={related.commits}
256
+ commitsAreLoading={related.commitsAreLoading}
257
+ commitsError={related.commitsError}
258
+ now={now}
259
+ buildImageProxyUrl={buildImageProxyUrl}
260
+ />
261
+ ))}
257
262
 
258
- {commentComposer}
263
+ {commentComposer}
264
+ </article>
259
265
 
260
- <div className="console-actionbar">{operationBar}</div>
261
- </article>
266
+ <footer className="console-actionbar">{operationBar}</footer>
267
+ </>
262
268
  );
263
269
  };
@@ -1,4 +1,4 @@
1
- import { useCallback } from 'react';
1
+ import { type Ref, useCallback } from 'react';
2
2
  import { ConsoleCommentComposer } from '../components/detail/ConsoleCommentComposer';
3
3
  import { ConsoleItemDetail } from '../components/detail/ConsoleItemDetail';
4
4
  import { ConsoleOperationMenu } from '../components/operations/ConsoleOperationMenu';
@@ -37,6 +37,7 @@ export type ConsoleItemDetailContainerProps = {
37
37
  overlayStatus: ConsoleOverlayStatus | null;
38
38
  now: number;
39
39
  onQueueAction: (input: ConsoleQueueActionInput) => void;
40
+ scrollRef?: Ref<HTMLElement>;
40
41
  };
41
42
 
42
43
  export const ConsoleItemDetailContainer = ({
@@ -51,6 +52,7 @@ export const ConsoleItemDetailContainer = ({
51
52
  overlayStatus,
52
53
  now,
53
54
  onQueueAction,
55
+ scrollRef,
54
56
  }: ConsoleItemDetailContainerProps) => {
55
57
  const detail = useConsoleItemDetailData(caches, item);
56
58
  const { token } = useConsoleToken();
@@ -148,6 +150,7 @@ export const ConsoleItemDetailContainer = ({
148
150
  commitsError={detail.commitsError}
149
151
  relatedPullRequests={detail.relatedPullRequests}
150
152
  now={now}
153
+ scrollRef={scrollRef}
151
154
  buildImageProxyUrl={resolveImageProxyUrl}
152
155
  commentComposer={
153
156
  <ConsoleCommentComposer
@@ -391,6 +391,103 @@ describe('ConsolePage swipe navigation', () => {
391
391
  });
392
392
  });
393
393
 
394
+ describe('ConsolePage detail scroll reset', () => {
395
+ beforeEach(() => {
396
+ localStorage.clear();
397
+ window.history.replaceState({}, '', '/projects/umino/prs?k=token');
398
+ const fetchMock = jest.fn(async (url: string) => {
399
+ const listMatch = url.match(/\/projects\/[^/]+\/([^/]+)\/list\.json/);
400
+ if (listMatch !== null) {
401
+ return {
402
+ ok: true,
403
+ status: 200,
404
+ json: async () =>
405
+ listMatch[1] === 'prs'
406
+ ? twoItemPrPayload()
407
+ : { ...twoItemPrPayload(), items: [] },
408
+ };
409
+ }
410
+ return { ok: true, status: 200, json: async () => ({ body: '# body' }) };
411
+ });
412
+ global.fetch = fetchMock as unknown as typeof fetch;
413
+ });
414
+
415
+ it('resets the detail scroll container to the top when navigating to the next item', async () => {
416
+ const { container, getByText, findByText } = render(<ConsolePage />);
417
+ await waitFor(() => {
418
+ expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
419
+ });
420
+ fireEvent.click(getByText('Add serveConsole subcommand'));
421
+ expect(await findByText('Approve')).toBeInTheDocument();
422
+
423
+ const scrollContainer = container.querySelector('.console-detail');
424
+ expect(scrollContainer).not.toBeNull();
425
+ (scrollContainer as HTMLElement).scrollTop = 240;
426
+ expect((scrollContainer as HTMLElement).scrollTop).toBe(240);
427
+
428
+ const detailScreen = container.querySelector('.console-detail-screen');
429
+ swipeDetailScreen(
430
+ detailScreen as HTMLElement,
431
+ { clientX: 240, clientY: 100 },
432
+ { clientX: 40, clientY: 110 },
433
+ );
434
+
435
+ await waitFor(() => {
436
+ expect(window.location.hash).toBe('#item/PVTI_2');
437
+ });
438
+ await waitFor(() => {
439
+ expect((scrollContainer as HTMLElement).scrollTop).toBe(0);
440
+ });
441
+ });
442
+ });
443
+
444
+ describe('ConsolePage next item prefetch', () => {
445
+ beforeEach(() => {
446
+ localStorage.clear();
447
+ window.history.replaceState({}, '', '/projects/umino/prs?k=token');
448
+ });
449
+
450
+ it('warms the next pending item resources while the current item is open', async () => {
451
+ const fetchMock = jest.fn(async (url: string) => {
452
+ const listMatch = url.match(/\/projects\/[^/]+\/([^/]+)\/list\.json/);
453
+ if (listMatch !== null) {
454
+ return {
455
+ ok: true,
456
+ status: 200,
457
+ json: async () =>
458
+ listMatch[1] === 'prs'
459
+ ? twoItemPrPayload()
460
+ : { ...twoItemPrPayload(), items: [] },
461
+ };
462
+ }
463
+ return { ok: true, status: 200, json: async () => ({ body: '# body' }) };
464
+ });
465
+ global.fetch = fetchMock as unknown as typeof fetch;
466
+
467
+ const { getByText, findByText, queryByText } = render(<ConsolePage />);
468
+ await waitFor(() => {
469
+ expect(getByText('Add serveConsole subcommand')).toBeInTheDocument();
470
+ });
471
+ fireEvent.click(getByText('Add serveConsole subcommand'));
472
+ expect(await findByText('Approve')).toBeInTheDocument();
473
+ expect(window.location.hash).toBe('#item/PVTI_1');
474
+ expect(queryByText('Add server-side console API handlers')).toBeNull();
475
+
476
+ const nextItemUrlFragment = encodeURIComponent(
477
+ 'https://github.com/o/r/pull/852',
478
+ );
479
+ await waitFor(() => {
480
+ const calledNextItemBody = fetchMock.mock.calls.some(
481
+ ([url]) =>
482
+ typeof url === 'string' &&
483
+ url.includes('/api/itembody') &&
484
+ url.includes(nextItemUrlFragment),
485
+ );
486
+ expect(calledNextItemBody).toBe(true);
487
+ });
488
+ });
489
+ });
490
+
394
491
  describe('ConsolePage auto-advance', () => {
395
492
  beforeEach(() => {
396
493
  localStorage.clear();
@@ -191,6 +191,44 @@ export const ConsolePage = () => {
191
191
  );
192
192
 
193
193
  const detailScreenRef = useConsoleSwipeNavigation(handleSwipe);
194
+ const detailScrollElementRef = useRef<HTMLElement | null>(null);
195
+
196
+ useEffect(() => {
197
+ if (selectedItemKey === null) {
198
+ return;
199
+ }
200
+ const element = detailScrollElementRef.current;
201
+ if (element !== null) {
202
+ element.scrollTop = 0;
203
+ }
204
+ }, [selectedItemKey]);
205
+
206
+ const nextPendingItem = useMemo<ConsoleListItem | null>(() => {
207
+ if (selectedItemKey === null) {
208
+ return null;
209
+ }
210
+ const nextKey = nextPendingKeyBrowse(orderedPendingKeys, selectedItemKey);
211
+ if (nextKey === null) {
212
+ return null;
213
+ }
214
+ return (
215
+ pendingItems.find((item) => overlayKeyForItem(item) === nextKey) ?? null
216
+ );
217
+ }, [selectedItemKey, orderedPendingKeys, pendingItems]);
218
+
219
+ useEffect(() => {
220
+ if (nextPendingItem === null) {
221
+ return;
222
+ }
223
+ const key = `${nextPendingItem.repo}#${nextPendingItem.number}`;
224
+ const url = nextPendingItem.url;
225
+ void caches.body.load(key, url);
226
+ void caches.state.load(key, url);
227
+ if (nextPendingItem.isPr) {
228
+ void caches.files.load(key, url);
229
+ void caches.commits.load(key, url);
230
+ }
231
+ }, [nextPendingItem, caches]);
194
232
 
195
233
  return (
196
234
  <main className="console-app">
@@ -235,6 +273,7 @@ export const ConsolePage = () => {
235
273
  overlayStatus={overlayStatusForSelected}
236
274
  now={now}
237
275
  onQueueAction={handleQueueAction}
276
+ scrollRef={detailScrollElementRef}
238
277
  />
239
278
  </div>
240
279
  )}
@@ -33,8 +33,10 @@ body {
33
33
  .console-app {
34
34
  max-width: 920px;
35
35
  margin: 0 auto;
36
+ height: 100dvh;
36
37
  display: flex;
37
38
  flex-direction: column;
39
+ min-height: 0;
38
40
  }
39
41
 
40
42
  .console-tabbar {
@@ -114,6 +116,10 @@ body {
114
116
  list-style: none;
115
117
  margin: 0;
116
118
  padding: 12px 18px 18px;
119
+ flex: 1;
120
+ min-height: 0;
121
+ overflow-y: auto;
122
+ -webkit-overflow-scrolling: touch;
117
123
  }
118
124
 
119
125
  .console-list-group {
@@ -258,6 +264,10 @@ body {
258
264
  }
259
265
 
260
266
  .console-detail {
267
+ flex: 1;
268
+ min-height: 0;
269
+ overflow-y: auto;
270
+ -webkit-overflow-scrolling: touch;
261
271
  display: flex;
262
272
  flex-direction: column;
263
273
  gap: 12px;
@@ -463,14 +473,11 @@ body {
463
473
  }
464
474
 
465
475
  .console-actionbar {
466
- position: fixed;
467
- bottom: 0;
468
- left: 0;
469
- right: 0;
470
- z-index: 100;
476
+ flex-shrink: 0;
471
477
  background: #161b22;
472
478
  border-top: 2px solid #30363d;
473
479
  padding: 10px 16px;
480
+ padding-bottom: calc(10px + env(safe-area-inset-bottom));
474
481
  }
475
482
 
476
483
  .console-operation-bar {
@@ -561,7 +568,10 @@ body {
561
568
  }
562
569
 
563
570
  .console-detail-screen {
564
- padding-bottom: 140px;
571
+ flex: 1;
572
+ min-height: 0;
573
+ display: flex;
574
+ flex-direction: column;
565
575
  }
566
576
 
567
577
  .console-panel-open-link {
@@ -0,0 +1 @@
1
+ /*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-outline-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--font-weight-semibold:600;--radius-md:.375rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-input:#e5e5e5;--color-ring:#0a0a0a;--color-background:#fff;--color-foreground:#0a0a0a;--color-primary:#171717;--color-primary-foreground:#fafafa;--color-secondary:#f5f5f5;--color-secondary-foreground:#171717;--color-accent:#f5f5f5;--color-accent-foreground:#171717}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.relative{position:relative}.static{position:static}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.hidden{display:none}.inline-flex{display:inline-flex}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-input{border-color:var(--color-input)}.border-transparent{border-color:#0000}.bg-background{background-color:var(--color-background)}.bg-primary{background-color:var(--color-primary)}.bg-secondary{background-color:var(--color-secondary)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-2{padding-block:calc(var(--spacing) * 2)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-foreground{color:var(--color-foreground)}.text-primary-foreground{color:var(--color-primary-foreground)}.text-secondary-foreground{color:var(--color-secondary-foreground)}.lowercase{text-transform:lowercase}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.hover\:bg-accent:hover{background-color:var(--color-accent)}.hover\:bg-primary\/90:hover{background-color:#171717e6}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--color-primary) 90%,transparent)}}.hover\:bg-secondary\/80:hover{background-color:#f5f5f5cc}@supports (color:color-mix(in lab,red,red)){.hover\:bg-secondary\/80:hover{background-color:color-mix(in oklab,var(--color-secondary) 80%,transparent)}}.hover\:text-accent-foreground:hover{color:var(--color-accent-foreground)}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color:var(--color-ring)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-50:disabled{opacity:.5}}body{color:#e6edf3;background-color:#0d1117;margin:0;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif}.console-app{flex-direction:column;max-width:920px;height:100dvh;min-height:0;margin:0 auto;display:flex}.console-tabbar{-webkit-overflow-scrolling:touch;background:#161b22;border-bottom:2px solid #30363d;flex-wrap:nowrap;align-items:stretch;gap:0;min-height:42px;padding:0 8px;display:flex;overflow-x:auto}.console-tab{color:#8b949e;white-space:nowrap;cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;align-items:center;gap:6px;margin-bottom:-2px;padding:10px 14px;font-size:13px;font-weight:500;line-height:1.2;text-decoration:none;display:inline-flex}.console-tab:hover{color:#e6edf3}.console-tab[data-active=true]{color:#e6edf3;border-bottom-color:#2f81f7;font-weight:700}.console-tab-badge{text-align:center;color:#e6edf3;background:#484f58;border-radius:20px;min-width:20px;padding:1px 7px;font-size:11px;font-weight:700;line-height:1.5}.console-tab-badge[data-zero=true]{color:#8b949e;background:#30363d}.console-tab-pjname{color:#8b949e;align-self:center;margin-left:auto;padding:0 8px;font-size:11.5px}.console-tab-geninfo{color:#8b949e;align-self:center;padding:0 4px;font-size:11px}.console-list{-webkit-overflow-scrolling:touch;flex:1;min-height:0;margin:0;padding:12px 18px 18px;list-style:none;overflow-y:auto}.console-list-group{list-style:none}.console-item-row .console-item-icon{flex:none;margin-top:3px}.console-group-header{background:#0b0f14;border-bottom:1px solid #21262d;justify-content:space-between;align-items:center;padding:6px 12px;display:flex}.console-storytag{align-items:center;gap:8px;font-size:13px;font-weight:700;display:inline-flex}.console-story-dot{border-radius:999px;width:10px;height:10px;display:inline-block}.console-group-count{color:#8b949e;font-size:12px}.console-item-row{color:#e6edf3;text-align:left;cursor:pointer;background:#161b22;border:1px solid #30363d;border-radius:8px;align-items:flex-start;gap:14px;width:100%;margin-bottom:10px;padding:12px 16px;display:flex}.console-item-row:hover{background:#1a2029;border-color:#484f58}.console-item-row[data-active=true]{background:#1a2029;border-color:#4493f8}.console-item-meta{flex:1;min-width:0}.console-item-title{font-size:14.5px;font-weight:600;display:block}.console-item-sub{color:#8b949e;margin-top:3px;font-size:12.5px;display:block}.console-item-pill{color:#8b949e;border:1px solid #30363d;border-radius:20px;margin-right:6px;padding:1px 8px;font-size:11px;display:inline-block}.console-item-createdat{color:#8b949e;cursor:help}.console-item-fields{color:#adbac7;flex-wrap:wrap;gap:4px 10px;margin-top:4px;font-size:11.5px;display:flex}.console-item-field{word-break:break-all;align-items:baseline;gap:4px;min-width:0;display:inline-flex}.console-item-field-label{color:#6e7681;text-transform:uppercase;letter-spacing:.03em;font-size:10px}.console-list-message{color:#8b949e;padding:16px;font-size:14px}.console-list-empty{text-align:center;color:#8b949e;padding:40px;font-size:14px}.console-list-error,.console-comment-error,.console-files-error,.console-commits-error,.console-detail-body-error{color:#f85149}.console-detail{-webkit-overflow-scrolling:touch;flex-direction:column;flex:1;gap:12px;min-height:0;padding:16px;display:flex;overflow-y:auto}.console-detail-title{align-items:center;gap:8px;margin:0;font-size:20px;display:flex}.console-detail-title-text{flex:1}.console-detail-number{color:#8b949e;font-weight:400}.console-detail-closed-label{color:#a371f7;font-size:13px}.console-detail-subbar{align-items:center;gap:12px;font-size:13px;display:flex}.console-detail-link{color:#4493f8}.console-detail-repo{color:#8b949e;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-detail-pill,.console-label-chip,.console-detail-status-chip{border:1px solid #30363d;border-radius:999px;padding:2px 8px;font-size:12px;display:inline-block}.console-detail-labels{flex-wrap:wrap;gap:6px;display:flex}.console-detail-createdat{color:#8b949e;font-size:12px}.console-panel{border:1px solid #30363d;border-radius:8px;overflow:hidden}.console-panel-header{background:#161b22;justify-content:space-between;align-items:center;padding:6px 12px;display:flex}.console-panel-toggle{color:#e6edf3;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;font-size:14px;font-weight:600;display:inline-flex}.console-panel-body{padding:12px}.console-markdown{word-break:break-word;font-size:14px;line-height:1.5}.console-markdown ul{margin:.5em 0;padding-left:1.5em;list-style:outside}.console-markdown ol{margin:.5em 0;padding-left:1.5em;list-style:decimal}.console-markdown li{margin:.25em 0}.console-markdown table{border-collapse:collapse;margin:.5em 0}.console-markdown th,.console-markdown td{border:1px solid #30363d;padding:4px 8px}.console-markdown th{background:#161b22}.console-mermaid-error{color:#f85149;font-size:13px}.console-comment{border-top:1px solid #21262d;padding-top:8px}.console-comment-header{color:#8b949e;gap:8px;font-size:12px;display:flex}.console-comment-author{color:#e6edf3;font-weight:600}.console-files,.console-commits{margin:0;padding:0;list-style:none}.console-commit{align-items:center;gap:8px;padding:4px 0;font-size:13px;display:flex}.console-file{flex-direction:column;font-size:13px;display:flex}.console-file-badge{text-align:center;border:1px solid;border-radius:4px;width:18px;font-size:11px}.console-file-path,.console-commit-message{flex:1;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-file-add,.console-pr-add{color:#3fb950}.console-file-del,.console-pr-del{color:#f85149}.console-commit-sha{color:#8b949e;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.console-actionbar{padding:10px 16px;padding-bottom:calc(10px + env(safe-area-inset-bottom));background:#161b22;border-top:2px solid #30363d;flex-shrink:0}.console-operation-bar{flex-direction:column;gap:8px;max-width:920px;margin:0 auto;display:flex}.console-op-group{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.console-op-group-review{gap:10px}.console-op-group-stories{max-height:50vh;overflow-y:auto}.console-op-button{color:#e6edf3;white-space:nowrap;cursor:pointer;background:#21262d;border:1px solid #30363d;border-radius:6px;padding:7px 16px;font-size:13px;font-weight:600}.console-op-button:hover{border-color:#484f58}.console-op-button-approve{color:#fff;background:#238636;border-color:#2ea043}.console-op-button-reject{color:#ffd166;background:#7d5000;border-color:#a06800}.console-op-button-wrong{color:#f85149;background:#3a1518;border-color:#f85149}.console-op-button-unneeded{color:#aab0b8;background:#2a2d31;border-color:#6e7681}.console-op-button-snooze{color:#79c0ff;background:#1c2b4a;border-color:#4493f8}.console-pr-section{border:1px solid #30363d;border-radius:8px;flex-direction:column;gap:10px;padding:12px;display:flex}.console-pr-statbar{color:#8b949e;gap:12px;font-size:12px;display:flex}.console-detail-screen{flex-direction:column;flex:1;min-height:0;display:flex}.console-panel-open-link{color:#4493f8;font-size:13px;font-weight:400}.console-composer{margin-top:4px}.console-composer-toggle{color:#8b949e;cursor:pointer;background:0 0;border:none;padding:2px 0;font-size:12.5px}.console-composer-toggle:hover{color:#e6edf3}.console-composer-posted{flex-direction:column;gap:8px;margin-top:8px;display:flex}.console-composer-form{margin-top:8px}.console-composer-input{box-sizing:border-box;color:#e6edf3;width:100%;font:inherit;resize:vertical;background:#21262d;border:1px solid #30363d;border-radius:6px;padding:8px;font-size:14px}.console-composer-row{align-items:center;gap:8px;margin-top:6px;display:flex}.console-composer-submit{color:#fff;cursor:pointer;background:#238636;border:1px solid #2ea043;border-radius:6px;padding:7px 16px;font-size:13px;font-weight:600}.console-composer-submit:disabled{opacity:.6;cursor:default}.console-composer-status{color:#8b949e;font-size:12px}.console-composer-error{color:#f85149}.console-undo-toast{z-index:9998;white-space:nowrap;border-radius:10px;align-items:center;gap:12px;max-width:92vw;padding:12px 18px;font-size:13.5px;display:flex;position:fixed;top:12px;left:50%;transform:translate(-50%);box-shadow:0 4px 20px #0009}.console-undo-toast-green{color:#3fb950;background:#0d3320;border:1px solid #2ea043}.console-undo-toast-amber{color:#ffd166;background:#3a2800;border:1px solid #a06800}.console-undo-toast-red{color:#f85149;background:#3a1518;border:1px solid #da3633}.console-undo-toast-gray{color:#aab0b8;background:#2a2d31;border:1px solid #6e7681}.console-undo-toast-blue{color:#79c0ff;background:#0d1f40;border:1px solid #4493f8}.console-undo-toast-error{color:#f85149;background:#3a1518;border:1px solid #f85149}.console-undo-toast-message{text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.console-undo-toast-undo{cursor:pointer;color:inherit;background:#ffffff1f;border:none;border-radius:6px;flex:none;padding:4px 12px;font-size:13px;font-weight:700}.console-undo-toast-undo:hover{background:#ffffff38}.console-undo-toast-countdown{opacity:.7;flex:none;font-size:11px}.console-undo-toast-bar{opacity:.5;background:currentColor;border-radius:0 0 10px 10px;height:3px;transition:width .1s linear;position:absolute;bottom:0;left:0}.console-file-row{width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;padding:4px 0;font-size:13px;display:flex}.console-file-row:hover{background:#1a2029}.console-file-caret{color:#8b949e;flex:none;width:1em}.console-file-diff{border-collapse:collapse;width:100%;margin:4px 0 8px;font:13px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace}.console-file-diff td{white-space:pre-wrap;word-break:break-word;vertical-align:top;padding:0 10px}.console-diff-ln{text-align:right;color:#6e7681;-webkit-user-select:none;user-select:none;white-space:nowrap;border-right:1px solid #30363d;width:1%;padding:0 8px}.console-file-diff td.console-diff-ln{white-space:nowrap;word-break:normal;overflow-wrap:normal}.console-diff-add td{background:#2ea04326}.console-diff-add .console-diff-code{color:#aff5b4}.console-diff-del td{background:#f8514926}.console-diff-del .console-diff-code{color:#ffdcd7}.console-diff-hunk td{color:#79c0ff;background:#161b22}.console-diff-ctx .console-diff-code{color:#c9d1d9}.console-file-diff-empty{color:#8b949e;padding:8px 14px;font-size:12px}.console-pr-header{flex-wrap:wrap;align-items:center;gap:10px;padding-top:4px;display:flex}.console-pr-section-title{color:#e6edf3;font-size:15px;font-weight:600}.console-pr-section-state{color:#8b949e;border:1px solid #6e7681;border-radius:999px;padding:1px 8px;font-size:11px}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}