@nbtca/prompt 1.3.2 → 1.4.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.
Files changed (65) hide show
  1. package/README.md +44 -0
  2. package/SECURITY.md +47 -0
  3. package/dist/app/app.js +202 -0
  4. package/dist/app/chrome.js +104 -0
  5. package/dist/app/fields/list-field.js +174 -0
  6. package/dist/app/fields/text-field.js +38 -0
  7. package/dist/app/frame.js +48 -0
  8. package/dist/app/keys.js +20 -0
  9. package/dist/app/tabs.js +11 -0
  10. package/dist/app/view.js +1 -0
  11. package/dist/app/views/docs-render.js +82 -0
  12. package/dist/app/views/docs.js +457 -0
  13. package/dist/app/views/events-render.js +111 -0
  14. package/dist/app/views/events.js +228 -0
  15. package/dist/app/views/home.js +236 -0
  16. package/dist/app/views/schedule-grid-cursor.js +52 -0
  17. package/dist/app/views/schedule-render.js +317 -0
  18. package/dist/app/views/schedule.js +472 -0
  19. package/dist/app/views/settings-render.js +53 -0
  20. package/dist/app/views/settings.js +153 -0
  21. package/dist/auth/cookie-transport.js +222 -0
  22. package/dist/auth/errors.js +18 -0
  23. package/dist/auth/nbt-auth.js +239 -0
  24. package/dist/auth/session-store.js +118 -0
  25. package/dist/config/paths.js +22 -2
  26. package/dist/core/canvas.js +23 -0
  27. package/dist/core/capabilities.js +42 -0
  28. package/dist/core/components/confirm.js +75 -0
  29. package/dist/core/components/input-session.js +24 -0
  30. package/dist/core/components/menu.js +122 -0
  31. package/dist/core/components/messages.js +16 -0
  32. package/dist/core/components/note.js +18 -0
  33. package/dist/core/components/painter.js +26 -0
  34. package/dist/core/components/screen.js +18 -0
  35. package/dist/core/components/spinner.js +47 -0
  36. package/dist/core/components/text-input.js +98 -0
  37. package/dist/core/logo.js +40 -15
  38. package/dist/core/menu.js +24 -6
  39. package/dist/core/motion.js +86 -0
  40. package/dist/core/text.js +127 -5
  41. package/dist/core/theme.js +61 -0
  42. package/dist/core/transitions.js +19 -0
  43. package/dist/core/ui.js +5 -29
  44. package/dist/features/calendar-heatmap.js +29 -27
  45. package/dist/features/calendar-query.js +50 -0
  46. package/dist/features/calendar.js +192 -98
  47. package/dist/features/docs.js +258 -55
  48. package/dist/features/links.js +7 -4
  49. package/dist/features/schedule-query.js +47 -0
  50. package/dist/features/schedule-render.js +574 -0
  51. package/dist/features/schedule-store.js +73 -0
  52. package/dist/features/schedule-view.js +260 -0
  53. package/dist/features/settings.js +41 -30
  54. package/dist/features/status.js +37 -13
  55. package/dist/features/student-timetable.js +346 -0
  56. package/dist/features/update.js +16 -8
  57. package/dist/i18n/locales/en.json +149 -6
  58. package/dist/i18n/locales/zh.json +149 -6
  59. package/dist/index.js +59 -5
  60. package/dist/logo/ca-dotmatrix-large.txt +26 -0
  61. package/dist/logo/ca-dotmatrix-small.txt +12 -0
  62. package/dist/logo/ca-dotmatrix.txt +18 -16
  63. package/dist/logo/ca-logo.png +0 -0
  64. package/dist/main.js +33 -13
  65. package/package.json +10 -7
@@ -0,0 +1,317 @@
1
+ import { c, type, space, glyph } from '../../core/theme.js';
2
+ import { pickIcon } from '../../core/icons.js';
3
+ import { t, fmt } from '../../i18n/index.js';
4
+ import { renderListFieldWithContext } from '../fields/list-field.js';
5
+ import { currentWeekNumber, campusWeekday, meetingsOnDay, nextMeeting } from '../../features/schedule-query.js';
6
+ import { renderNextClassBanner, renderWeekGrid, renderUnresolvedItems, renderTodayTimeline, weekdayShortLabel, renderTermDensity, renderMeetingDetail, renderDayTimeline, renderDaySwitcher, } from '../../features/schedule-render.js';
7
+ import { renderEventBrief } from '../../features/calendar.js';
8
+ import { visualWidth, wrapAnsiToVisualWidth } from '../../core/text.js';
9
+ function heading(label) {
10
+ return `${space.indent}${type.heading(label)}`;
11
+ }
12
+ function hint(label) {
13
+ return `${space.indent}${type.hint(label)}`;
14
+ }
15
+ function wrappedIndentedLines(label, cols, style) {
16
+ const width = Number.isFinite(cols) ? Math.max(1, Math.floor(cols)) : Number.POSITIVE_INFINITY;
17
+ const indent = visualWidth(space.indent) < width ? space.indent : '';
18
+ const contentWidth = Math.max(1, width - visualWidth(indent));
19
+ return wrapAnsiToVisualWidth(style(label), contentWidth).map((part) => `${indent}${part}`);
20
+ }
21
+ function headingLines(label, cols) {
22
+ return wrappedIndentedLines(label, cols, type.heading);
23
+ }
24
+ function hintLines(label, cols) {
25
+ return wrappedIndentedLines(label, cols, type.hint);
26
+ }
27
+ function wrappedRenderedLine(line, cols) {
28
+ const content = line.startsWith(space.indent) ? line.slice(space.indent.length) : line;
29
+ return wrappedIndentedLines(content, cols, (value) => value);
30
+ }
31
+ export function hubShortcuts(tt) {
32
+ const trans = t();
33
+ const shortcuts = [
34
+ { key: 'w', label: trans.timetable.hubFullGrid },
35
+ { key: 't', label: trans.timetable.hubTermDensity },
36
+ { key: 's', label: trans.timetable.hubSwitchTerm },
37
+ { key: 'e', label: trans.timetable.hubExport },
38
+ ];
39
+ if (tt.unresolvedItems.length > 0) {
40
+ shortcuts.push({
41
+ key: 'u',
42
+ label: `${pickIcon('⚠', '!')} ${tt.unresolvedItems.length}`,
43
+ showKey: false,
44
+ warn: true,
45
+ });
46
+ }
47
+ shortcuts.push({ key: 'x', label: trans.timetable.hubLogout });
48
+ return shortcuts;
49
+ }
50
+ function renderShortcutLines(shortcuts, cols, compact = false) {
51
+ const available = Math.max(1, cols - visualWidth(space.indent));
52
+ const parts = shortcuts.map((shortcut) => {
53
+ const text = compact
54
+ ? shortcut.showKey === false ? `[${shortcut.key}] ${shortcut.label}` : `[${shortcut.key}]`
55
+ : shortcut.showKey === false ? `[${shortcut.label}]` : `[${shortcut.key}] ${shortcut.label}`;
56
+ return shortcut.warn ? c.warn(text) : type.hint(text);
57
+ });
58
+ const lines = [];
59
+ let current = '';
60
+ for (const part of parts) {
61
+ const next = current ? `${current} ${part}` : part;
62
+ if (current && visualWidth(next) > available) {
63
+ lines.push(`${space.indent}${current}`);
64
+ current = part;
65
+ }
66
+ else {
67
+ current = next;
68
+ }
69
+ }
70
+ if (current)
71
+ lines.push(`${space.indent}${current}`);
72
+ return lines;
73
+ }
74
+ function hubPreGridLines(state, now, cols) {
75
+ const trans = t();
76
+ const tt = state.timetable;
77
+ if (!tt || !state.weekOne)
78
+ return null;
79
+ const week = currentWeekNumber(state.weekOne, now);
80
+ const lines = [];
81
+ const banner = renderNextClassBanner(nextMeeting(tt.meetings, tt.periods, state.weekOne, now), now, cols);
82
+ lines.push(banner || hint(trans.timetable.noNextClass));
83
+ lines.push('');
84
+ const todayWd = campusWeekday(now);
85
+ if (week < 1) {
86
+ // weekOne can be a *future* date -- auto-inferred while on break, it
87
+ // deliberately points at the upcoming term (see academic-calendar.ts)
88
+ // so it's ready the moment classes start. There is no "today" to show
89
+ // yet, but the timetable's real week-1 data is already fetched -- show
90
+ // it as an explicit preview rather than showing no grid at all
91
+ // regardless of terminal height. The "Week 1 preview" heading keeps it
92
+ // unambiguous that this isn't "happening right now".
93
+ lines.push(heading(trans.timetable.termNotStarted));
94
+ lines.push(hint(fmt(trans.timetable.termStartsIn, {
95
+ date: state.weekOne,
96
+ days: String(daysBetween(now, new Date(`${state.weekOne}T00:00:00`))),
97
+ })));
98
+ lines.push('');
99
+ lines.push(heading(trans.timetable.termPreviewWeek));
100
+ return { inlineLines: lines, fallbackLines: [...lines], week: 1, tt };
101
+ }
102
+ const today = meetingsOnDay(tt.meetings, todayWd, week);
103
+ const weekHeading = heading(trans.timetable.hubWeek);
104
+ const inlineLines = [
105
+ ...lines,
106
+ heading(fmt(trans.timetable.todayHeading, { weekday: weekdayShortLabel(todayWd), week: String(week) })),
107
+ ...renderTodayTimeline(today, tt.periods, now, cols).split('\n'),
108
+ weekHeading,
109
+ ];
110
+ return { inlineLines, fallbackLines: [...lines, weekHeading], week, tt };
111
+ }
112
+ // Below this width, even an all-empty grid's own per-column floor (3, plus
113
+ // row-head and separator overhead) leaves each of the 7 columns too cramped
114
+ // to show real content -- a technically-fitting but practically unreadable
115
+ // grid isn't better than the single-day view, so width gates the decision
116
+ // just as much as height does.
117
+ const MIN_GRID_COLS = 100;
118
+ /** The one place that decides "does the real week grid fit inline (plus a
119
+ * floor reserved for the shortcut bar), or does the hub fall back to the
120
+ * single-day view." Both branches represent the exact same gridCursor, just
121
+ * rendered differently, so unlike the old non-interactive strip fallback
122
+ * this decision no longer needs to be exposed to key handling -- arrow
123
+ * keys/Enter are always meaningful in hub mode regardless of which branch is
124
+ * currently on screen. */
125
+ function gridFitsInline(precedingLineCount, tt, week, now, bodyRows, cols, cursor, reservedRows) {
126
+ if (cols < MIN_GRID_COLS)
127
+ return false;
128
+ const gridLines = renderWeekGrid(tt.meetings, tt.periods, week, now, cols, cursor).split('\n');
129
+ return precedingLineCount + gridLines.length <= bodyRows - reservedRows;
130
+ }
131
+ /** Renders the full weekday x period grid if it (plus a floor reserved for
132
+ * the shortcut bar) fits within bodyRows and cols, otherwise a single day's
133
+ * detailed timeline -- the day the cursor's own weekday points at (today, by
134
+ * default). A large terminal genuinely has no excuse to not show the whole
135
+ * week; a small one is better served by one day shown properly than seven
136
+ * days crammed into unreadable slivers. Shared by the "this week" and "term
137
+ * hasn't started yet, preview week 1" branches of renderHubBody -- the same
138
+ * measure-and-fallback decision, just against a different week number. */
139
+ function renderAdaptiveWeekGrid(inlineLines, fallbackLines, tt, week, todayWd, now, bodyRows, cols, cursor, reservedRows) {
140
+ if (gridFitsInline(inlineLines.length, tt, week, now, bodyRows, cols, cursor, reservedRows)) {
141
+ return [...inlineLines, ...renderWeekGrid(tt.meetings, tt.periods, week, now, cols, cursor).split('\n')];
142
+ }
143
+ const selectedWd = cursor?.weekday ?? todayWd;
144
+ const dayMeetings = meetingsOnDay(tt.meetings, selectedWd, week);
145
+ return [
146
+ ...fallbackLines,
147
+ renderDaySwitcher(selectedWd, todayWd, cols),
148
+ ...renderDayTimeline(dayMeetings, tt.periods, now, selectedWd === todayWd, cursor?.period, cols).split('\n'),
149
+ ];
150
+ }
151
+ function renderHubBody(state, now, bodyRows, cols) {
152
+ const tt = state.timetable;
153
+ const pre = hubPreGridLines(state, now, cols);
154
+ const shortcuts = tt ? hubShortcuts(tt) : [];
155
+ const rows = Math.max(0, Math.floor(bodyRows));
156
+ const build = (shortcutLines) => {
157
+ const tail = [];
158
+ if (pre && (state.statusMessage || shortcutLines.length > 0))
159
+ tail.push('');
160
+ if (state.statusMessage) {
161
+ tail.push(...hintLines(state.statusMessage, cols));
162
+ if (shortcutLines.length > 0)
163
+ tail.push('');
164
+ }
165
+ tail.push(...shortcutLines);
166
+ const content = pre
167
+ ? renderAdaptiveWeekGrid(pre.inlineLines, pre.fallbackLines, pre.tt, pre.week, campusWeekday(now), now, rows, cols, state.gridCursor, tail.length)
168
+ : [];
169
+ return { content, tail };
170
+ };
171
+ const full = build(renderShortcutLines(shortcuts, cols));
172
+ if (full.content.length + full.tail.length <= rows)
173
+ return [...full.content, ...full.tail];
174
+ const compact = build(renderShortcutLines(shortcuts, cols, true));
175
+ if (compact.tail.length >= rows)
176
+ return rows > 0 ? compact.tail.slice(-rows) : [];
177
+ return [
178
+ ...compact.content.slice(0, rows - compact.tail.length),
179
+ ...compact.tail,
180
+ ];
181
+ }
182
+ const TERM_PROGRESS_WIDTH = 20;
183
+ function renderTermProgressBar(w, now, cols) {
184
+ if (!w.nextBreakStart)
185
+ return null;
186
+ const weekOneMs = new Date(`${w.weekOneMonday}T00:00:00`).getTime();
187
+ const nextBreakMs = new Date(`${w.nextBreakStart}T00:00:00`).getTime();
188
+ const totalWeeks = Math.max(1, Math.round((nextBreakMs - weekOneMs) / (7 * 86400000)));
189
+ const currentWeek = currentWeekNumber(w.weekOneMonday, now);
190
+ const labelText = fmt(t().timetable.weekLabel2, { week: `${currentWeek}/${totalWeeks}` });
191
+ const label = type.hint(labelText);
192
+ const width = Number.isFinite(cols) ? Math.max(1, Math.floor(cols)) : Number.POSITIVE_INFINITY;
193
+ const indent = visualWidth(space.indent) < width ? space.indent : '';
194
+ const contentWidth = Math.max(1, width - visualWidth(indent));
195
+ const inlineBarWidth = Math.min(TERM_PROGRESS_WIDTH, contentWidth - visualWidth(label) - 2);
196
+ const barWidth = inlineBarWidth >= 1 ? inlineBarWidth : Math.min(TERM_PROGRESS_WIDTH, contentWidth);
197
+ const filledCols = Math.max(0, Math.min(barWidth, Math.round((currentWeek / totalWeeks) * barWidth)));
198
+ const filledChar = glyph.barFilled();
199
+ const emptyChar = glyph.barEmpty();
200
+ const bar = type.body(filledChar.repeat(filledCols) + emptyChar.repeat(barWidth - filledCols));
201
+ return inlineBarWidth >= 1
202
+ ? [`${indent}${bar} ${label}`]
203
+ : [`${indent}${bar}`, ...hintLines(labelText, cols)];
204
+ }
205
+ function daysBetween(a, b) {
206
+ return Math.max(0, Math.ceil((b.getTime() - a.getTime()) / 86400000));
207
+ }
208
+ function renderPublicBody(state, now, bodyRows, cols) {
209
+ const trans = t();
210
+ const lines = [];
211
+ const w = state.publicWindow;
212
+ if (w === undefined) {
213
+ lines.push(...hintLines(trans.common.loading, cols));
214
+ }
215
+ else if (w === null) {
216
+ lines.push(...hintLines(trans.timetable.publicUnavailable, cols));
217
+ }
218
+ else if (w.status === 'onBreak') {
219
+ lines.push(...headingLines(fmt(trans.timetable.onBreak, { title: w.breakTitle }), cols));
220
+ }
221
+ else {
222
+ const semesterLabel = w.semester === '1' ? trans.timetable.semester1 : trans.timetable.semester2;
223
+ lines.push(...headingLines(`${fmt(trans.timetable.academicYearSuffix, { year: w.academicYear })} · ${semesterLabel} · ${fmt(trans.timetable.weekLabel2, { week: String(w.currentWeek) })}`, cols));
224
+ const bar = renderTermProgressBar(w, now, cols);
225
+ if (bar)
226
+ lines.push(...bar);
227
+ if (w.nextBreakStart && w.nextBreakTitle) {
228
+ lines.push(...hintLines(fmt(trans.timetable.daysUntilBreak, {
229
+ title: w.nextBreakTitle,
230
+ days: String(daysBetween(now, new Date(`${w.nextBreakStart}T00:00:00`))),
231
+ }), cols));
232
+ }
233
+ }
234
+ lines.push('');
235
+ const loginLines = [...hintLines(trans.timetable.publicLoginHint, cols), ''];
236
+ const rows = Number.isFinite(bodyRows)
237
+ ? Math.max(0, Math.floor(bodyRows))
238
+ : Number.POSITIVE_INFINITY;
239
+ const fieldRows = state.publicField
240
+ ? Math.min(3, rows, state.publicField.render(Number.POSITIVE_INFINITY, cols).length)
241
+ : 0;
242
+ if (state.publicUpcoming && state.publicUpcoming.length > 0) {
243
+ const activityHeading = headingLines(trans.calendar.recentActivity, cols);
244
+ const eventBudget = Math.max(0, rows - lines.length - activityHeading.length - 1 - loginLines.length - fieldRows);
245
+ const eventLines = [];
246
+ for (const event of state.publicUpcoming) {
247
+ const wrapped = wrappedRenderedLine(renderEventBrief(event, now), cols);
248
+ if (eventLines.length + wrapped.length > eventBudget)
249
+ break;
250
+ eventLines.push(...wrapped);
251
+ }
252
+ if (eventLines.length > 0)
253
+ lines.push(...activityHeading, ...eventLines, '');
254
+ }
255
+ lines.push(...loginLines);
256
+ return state.publicField
257
+ ? renderListFieldWithContext(lines, state.publicField, bodyRows, cols)
258
+ : lines;
259
+ }
260
+ export function renderSchedule(state, now, bodyRows = 100, cols = 80) {
261
+ const trans = t();
262
+ switch (state.mode) {
263
+ case 'loading':
264
+ return hintLines(trans.common.loading, cols);
265
+ case 'public':
266
+ return renderPublicBody(state, now, bodyRows, cols);
267
+ case 'needsLoginId':
268
+ return [
269
+ ...(state.errorMessage ? [...hintLines(state.errorMessage, cols), ''] : []),
270
+ ...(state.idField?.render(cols) ?? []),
271
+ ];
272
+ case 'needsLoginPassword':
273
+ return state.passwordField?.render(cols) ?? [];
274
+ case 'authenticating':
275
+ return hintLines(state.statusMessage ?? trans.common.loading, cols);
276
+ case 'needsWeekOne':
277
+ return [
278
+ ...(state.errorMessage ? [...hintLines(state.errorMessage, cols), ''] : []),
279
+ ...(state.weekOneField?.render(cols) ?? []),
280
+ ];
281
+ case 'hub':
282
+ return renderHubBody(state, now, bodyRows, cols);
283
+ case 'week': {
284
+ if (!state.timetable || !state.weekOne)
285
+ return [hint(trans.timetable.genericError)];
286
+ const week = currentWeekNumber(state.weekOne, now);
287
+ const weekLines = [heading(trans.timetable.hubWeek), ''];
288
+ // Standalone week mode gets the *whole* bodyRows to itself (no
289
+ // banner/today-section eating into it first, unlike the hub's own
290
+ // inline area) -- reached via the `w` shortcut specifically so a
291
+ // marginal terminal that couldn't fit the grid inline the hub still
292
+ // gets a real shot at it here, falling back to the single-day view
293
+ // only if even the full screen isn't enough.
294
+ return renderAdaptiveWeekGrid(weekLines, weekLines, state.timetable, week, campusWeekday(now), now, bodyRows, cols, state.gridCursor, 2);
295
+ }
296
+ case 'termDensity':
297
+ return state.timetable && state.weekOne
298
+ ? renderTermDensity(state.timetable.meetings, state.weekOne, currentWeekNumber(state.weekOne, now), cols).split('\n')
299
+ : [hint(trans.timetable.genericError)];
300
+ case 'termPicker':
301
+ return state.termField?.render(bodyRows, cols) ?? [];
302
+ case 'unresolved':
303
+ return [
304
+ heading(trans.timetable.unresolvedTitle),
305
+ '',
306
+ ...renderUnresolvedItems(state.timetable?.unresolvedItems ?? [], cols).split('\n'),
307
+ ];
308
+ case 'meetingDetail':
309
+ return state.detailMeeting && state.timetable
310
+ ? renderMeetingDetail(state.detailMeeting, state.timetable.periods, cols).split('\n')
311
+ : [hint(trans.timetable.genericError)];
312
+ case 'error':
313
+ return hintLines(state.errorMessage ?? trans.timetable.genericError, cols);
314
+ default:
315
+ return [];
316
+ }
317
+ }