Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
adfff96
deps: bump vite to 7.3.5, wrangler to 4.98.0; npm audit clean
devin-ai-integration[bot] Jun 11, 2026
95088ab
chore: upgrade toolchain majors (TypeScript 6, Vite 8, svgr 5, tsconf…
devin-ai-integration[bot] Jun 11, 2026
d0394da
perf: lazy-load term catalogs and split vendor chunks
devin-ai-integration[bot] Jun 11, 2026
06f49ee
fix: no-op workspace mutations while term catalog is loading
devin-ai-integration[bot] Jun 11, 2026
00a6cc3
fix: log error when term catalog fails to load
devin-ai-integration[bot] Jun 11, 2026
b88d8f0
fix: sync catalog state before paint when the term changes
devin-ai-integration[bot] Jun 11, 2026
21339b8
build: use function-form manualChunks for rolldown compatibility
devin-ai-integration[bot] Jun 11, 2026
8e8bdfb
chore: upgrade MUI to v9.1.0 (HelpOutline -> HelpOutlined icon rename)
devin-ai-integration[bot] Jun 11, 2026
ecff48c
fix: use HelpOutlineOutlined to preserve original HelpOutline glyph
devin-ai-integration[bot] Jun 11, 2026
0fc9466
fix: re-sync search options after async catalog load
devin-ai-integration[bot] Jun 11, 2026
9e6810e
fix: key search option re-sync on catalog identity
devin-ai-integration[bot] Jun 11, 2026
e6c7bf1
chore: upgrade toolchain majors (TypeScript 6, Vite 8, svgr 5, tsconf…
devin-ai-integration[bot] Jun 11, 2026
83e84cf
perf: lazy-load term catalogs + vendor code splitting (8.2 MB → ~1.0 …
devin-ai-integration[bot] Jun 11, 2026
39cfb1b
chore: upgrade MUI to v9.1.0 (#163)
devin-ai-integration[bot] Jun 11, 2026
56fba89
Replace flatpickr and react-select with native/MUI inputs, bump auto-…
devin-ai-integration[bot] Jun 11, 2026
2daf2d9
Merge remote-tracking branch 'origin/devin/1781144816-toolchain-major…
devin-ai-integration[bot] Jun 11, 2026
97a90fe
Merge remote-tracking branch 'origin/devin/1781145070-lazy-term-catal…
devin-ai-integration[bot] Jun 11, 2026
c24b0d9
Merge branch 'devin/1781144786-mui-9-upgrade' into devin/1781144811-a…
devin-ai-integration[bot] Jun 11, 2026
08bdb19
build: pin wrangler to exact 4.98.0 (4.99+ wrangler dev assets 404 re…
devin-ai-integration[bot] Jun 11, 2026
d6f4411
chore: gitignore .wrangler local dev cache
devin-ai-integration[bot] Jun 11, 2026
ffa1806
review: restore Icon gitignore CRs, drop stale lightningcss errorReco…
devin-ai-integration[bot] Jun 11, 2026
015eb5e
fix: alert instead of silent no-op for Import Workspace / Default Sch…
devin-ai-integration[bot] Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ dist
# SvelteKit build / generate output
.svelte-kit

# Wrangler local dev cache
.wrangler/

### OSX ###
# General
.DS_Store
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ Made with ❤️ by [Rahul](https://github.com/rchalamala/), [Eric](https://gith
In addition, thanks to [Armeet](https://github.com/armeetjatyani/) and others for suggestions/contributions!

Favicon art by Audrey Wong.

## Dependency notes

- `preact` and `@preact/signals` are never imported in `src/` directly — they are runtime peer dependencies of the Schedule-X calendar (`@schedule-x/*`) and must not be removed.
3,026 changes: 1,531 additions & 1,495 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 8 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,22 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@formkit/auto-animate": "^0.8.2",
"@formkit/auto-animate": "^0.9.0",
"@hello-pangea/dnd": "^18.0.1",
"@mui/icons-material": "^7.3.11",
"@mui/material": "^7.3.11",
"@mui/icons-material": "^9.1.0",
"@mui/material": "^9.1.0",
"@preact/signals": "^2.9.1",
"@schedule-x/calendar": "^4.6.0",
"@schedule-x/events-service": "^4.6.0",
"@schedule-x/react": "^4.1.0",
"@schedule-x/theme-default": "^4.6.0",
"flatpickr": "^4.6.13",
"fzf": "^0.5.1",
"ics": "^3.12.0",
"lz-string": "^1.5.0",
"motion": "^12.40.0",
"preact": "^10.29.2",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-flatpickr": "^4.0.11",
"react-select": "^5.9.0",
"temporal-polyfill": "^0.3.0",
"usehooks-ts": "^3.1.1"
},
Expand All @@ -46,11 +43,11 @@
"oxlint": "^1.69.0",
"prettier": "^3.8.4",
"tailwindcss": "^4.3.0",
"typescript": "^5.7.2",
"vite": "^7.2.4",
"vite-plugin-svgr": "^4.5.0",
"vite-tsconfig-paths": "^5.1.4",
"wrangler": "^4.49.1"
"typescript": "^6.0.3",
"vite": "^8.0.16",
"vite-plugin-svgr": "^5.2.0",
"vite-tsconfig-paths": "^6.1.1",
"wrangler": "4.98.0"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Wrangler version pinned without caret, unlike other dependencies

In package.json, wrangler changed from ^4.49.1 (allows minor/patch updates) to 4.98.0 (exact version). All other dependencies use ^ ranges. This inconsistency might be intentional (to pin a known-good version) but could lead to the project falling behind on wrangler patches. Worth confirming this was deliberate.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberate: wrangler 4.99.0 (latest at time of writing) has a regression where wrangler dev returns 404 for all static assets (bisected — 4.98.0 and earlier work). The exact pin prevents ^ from pulling in the broken version; it can be relaxed back to ^ once a fixed wrangler release ships.

},
"homepage": "."
}
4 changes: 2 additions & 2 deletions src/HelpButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from "react";
import { m } from "motion/react";
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
import HelpOutlineOutlinedIcon from "@mui/icons-material/HelpOutlineOutlined";
import Modal from "./Modal";
import Hyperlink from "./Hyperlink";

Expand All @@ -16,7 +16,7 @@ export default function HelpButton() {
className="h-8 w-8 rounded-full border-none bg-white p-0"
onClick={() => setModalOpen(true)}
>
<HelpOutlineIcon
<HelpOutlineOutlinedIcon
className="text-orange-500 bg-transparent"
style={{ width: "auto", height: "auto" }}
/>
Expand Down
56 changes: 38 additions & 18 deletions src/Planner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,45 @@ import { createViewWeek, CalendarConfig } from "@schedule-x/calendar";
import { createEventsServicePlugin } from "@schedule-x/events-service";
import { useCalendarApp, ScheduleXCalendar } from "@schedule-x/react";
import { Temporal } from "temporal-polyfill";
import Flatpickr from "react-flatpickr";

import "@schedule-x/theme-default/dist/index.css";
import "flatpickr/dist/themes/airbnb.css";

import "./css/planner.css";

const hasWeekendCourse = false;

function formatTime(date: Date): string {
return `${String(date.getHours()).padStart(2, "0")}:${String(
date.getMinutes(),
).padStart(2, "0")}`;
}

function TimeInput({
value,
onChange,
label,
}: {
value: Date;
onChange: (day: Date) => void;
label: string;
}) {
return (
<input
type="time"
className="planner-time-input"
aria-label={label}
value={formatTime(value)}
onChange={(e) => {
if (!e.target.value) return;
const [hours, minutes] = e.target.value.split(":").map(Number);
const day = new Date(value);
day.setHours(hours, minutes, 0, 0);
onChange(day);
}}
/>
);
}

function CourseToDates(courses: CourseStorage[]): DateData[] {
const dates: DateData[] = [];

Expand Down Expand Up @@ -162,27 +192,17 @@ function Planner() {
className={`flex min-w-0 flex-col items-center gap-y-2 px-0.5 ${idx === 0 ? "col-start-2" : ""}`}
key={idx}
>
<Flatpickr
data-enable-time
options={{
dateFormat: "H:i",
enableTime: true,
noCalendar: true,
}}
<TimeInput
label={`Day ${idx + 1} available start time`}
value={state.availableTimes[idx][0]}
onChange={([day]) => {
onChange={(day) => {
state.updateAvailableTimes(idx, true, day);
}}
/>
<Flatpickr
data-enable-time
options={{
dateFormat: "H:i",
enableTime: true,
noCalendar: true,
}}
<TimeInput
label={`Day ${idx + 1} available end time`}
value={state.availableTimes[idx][1]}
onChange={([day]) => {
onChange={(day) => {
state.updateAvailableTimes(idx, false, day);
}}
/>
Expand Down
90 changes: 46 additions & 44 deletions src/Workspace.tsx
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { use, useState } from "react";
import Modal, { useModal } from "./Modal";
import Select from "react-select";
import { SingleValue } from "react-select";
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
import { Fzf } from "fzf";
import { createEvents } from "ics";
Expand All @@ -22,7 +20,13 @@ import {
decompressFromEncodedURIComponent,
} from "lz-string";

import { Collapse, IconButton, Switch } from "@mui/material";
import {
Autocomplete,
Collapse,
IconButton,
Switch,
TextField,
} from "@mui/material";
import { UnfoldLess, UnfoldMore } from "@mui/icons-material";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import TERM_START_DATES from "./data/term_start_dates.json";
Expand Down Expand Up @@ -193,9 +197,9 @@ function SectionDropdown(props: { course: CourseStorage }) {
const course = props.course;
const state = use(AppState);

const onChange = (newSection: SingleValue<Maybe<SectionData>>) => {
const onChange = (newSection: Maybe<SectionData>) => {
course.sectionId =
newSection !== null
newSection != null
? course.courseData.sections.findIndex(
(s) => s.number === newSection.number,
)
Comment on lines +200 to 205

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: SectionDropdown.onChange directly mutates course.sectionId before calling addCourse

At src/Workspace.tsx:201, course.sectionId is mutated in-place (a direct state mutation) before state.addCourse(course) is called. This is a pre-existing pattern (existed before this PR), not introduced by the migration. It works because addCourse subsequently triggers a state update that persists the change. However, if addCourse returns early (e.g., due to the catalogReady guard), the mutation would persist on the object without a corresponding React state update, which could cause subtle inconsistencies. In practice this shouldn't happen since sections are only shown when courses are loaded.

(Refers to lines 200-208)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — pre-existing mutation pattern, not introduced by this PR. As noted, SectionDropdown only renders for courses already resolved from the catalog, so the catalogReady early-return path is effectively unreachable here. Leaving as-is; an immutable-update refactor could be a separate cleanup.

Expand All @@ -206,27 +210,24 @@ function SectionDropdown(props: { course: CourseStorage }) {

return (
<div className="workspace-entry-section">
<Select
isClearable
placeholder=""
<Autocomplete
size="small"
value={
course.sectionId !== null
? course.courseData.sections.find(
? (course.courseData.sections.find(
(c) =>
c.number ===
course.courseData.sections[course.sectionId!].number,
)
) ?? null)
: null
Comment on lines 215 to 222

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: SectionDropdown value lookup is redundant but necessary for MUI Autocomplete referential identity

The value computation at lines 215-222 uses sections.find(...) to look up the section object by number, even though sections[course.sectionId!] would directly return it. This is intentional: MUI Autocomplete requires the value to be referentially identical to one of the options array elements. Since find searches the same sections array used as options, it guarantees the correct reference. The ?? null fallback handles the unlikely case where findIndex returns -1.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
onChange={onChange}
onChange={(_event, newSection) => onChange(newSection)}
options={course.courseData.sections}
getOptionLabel={(section) => `${section.number}`}
isOptionSelected={(section) =>
course.sectionId !== null
? section.number ===
course.courseData.sections[course.sectionId].number
: false
isOptionEqualToValue={(option, selected) =>
option.number === selected.number
}
renderInput={(params) => <TextField {...params} aria-label="Section" />}
/>
</div>
);
Expand Down Expand Up @@ -409,20 +410,10 @@ function WorkspaceSearch() {
const indexedCourses = use(AllCourses);
const courses = Object.values(indexedCourses);

// For some reason, options = [] on the second render, even though
// courses = [...] by then and options should equal courses.
// I came up with this hacky solution to get around that...
// The dropdown options should re-render properly
let [options, setOptions] = useState<CourseData[]>(courses);
const [firstLoad, setFirstLoad] = useState(true);
if (firstLoad && options.length === 0) {
options = courses;
}

const [selectedCourse, setCourse] = useState<Maybe<CourseData>>(null);

const handleSelect = (courseData: SingleValue<CourseData>) => {
setCourse(courseData as CourseData);
const handleSelect = (courseData: Maybe<CourseData>) => {
setCourse(courseData);
if (courseData) {
state.addCourse({
courseData: courseData,
Expand All @@ -438,25 +429,26 @@ function WorkspaceSearch() {
selector: (item) => `${item.number} ${item.name}`,
});
Comment on lines 429 to 430

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Fzf instance recreated on every render in WorkspaceSearch

The Fzf instance at src/Workspace.tsx:428-430 is created on every render of WorkspaceSearch, rebuilding its index over the full course list each time. This includes during every keystroke in the Autocomplete input. While functionally correct (each render gets a fresh fzf matching the current courses), wrapping it in useMemo(() => new Fzf(courses, {...}), [courses]) would avoid redundant index construction, especially since the courses array only changes when the catalog loads or the term switches.

(Refers to lines 428-430)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — correct that useMemo(() => new Fzf(courses, ...), [courses]) would avoid rebuilding the index per keystroke. Functionally fine today; noted as a possible perf follow-up rather than expanding this PR further.


const sortCourses = (input: string) => {
setOptions(fzf.find(input).map((item) => item.item));
setFirstLoad(false);
};

return (
<Select
isClearable
<Autocomplete
size="small"
className="my-3"
placeholder="Add a course..."
options={options}
options={courses}
value={selectedCourse}
getOptionLabel={(course) => `${course?.number} - ${course?.name}`}
onChange={handleSelect}
isOptionSelected={(course) => course.id === selectedCourse?.id}
onInputChange={sortCourses}
filterOption={() => {
return true;
}}
getOptionLabel={(course) => `${course.number} - ${course.name}`}
isOptionEqualToValue={(option, selected) => option.id === selected.id}
onChange={(_event, courseData) => handleSelect(courseData)}
filterOptions={(allOptions, { inputValue }) =>
inputValue ? fzf.find(inputValue).map((item) => item.item) : allOptions
}
renderInput={(params) => (
<TextField
{...params}
placeholder={
courses.length === 0 ? "Loading courses..." : "Add a course..."
}
/>
)}
/>
);
}
Expand Down Expand Up @@ -603,6 +595,10 @@ export default function Workspace({ term }: { term: string }) {
});

const importWorkspace = () => {
if (Object.keys(indexedCourses).length === 0) {
alert("Course data is still loading. Please try again in a moment.");
return;
}
const code = prompt("Copy in the workspace code.") || "";
if (code === "") {
return;
Expand Down Expand Up @@ -697,6 +693,12 @@ export default function Workspace({ term }: { term: string }) {
<ControlButton
text="Default Schedule"
onClick={() => {
if (Object.keys(indexedCourses).length === 0) {
alert(
"Course data is still loading. Please try again in a moment.",
);
return;
}
state.setCourses(
// Change based on term
(DEFAULT_COURSES[term.substring(0, 2)] ?? []).flatMap((name) => {
Expand Down
64 changes: 35 additions & 29 deletions src/courseData.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,39 @@
import DATA_FA2023 from "./data/IndexedTotalFA2022-23.json";
import DATA_WI2023 from "./data/IndexedTotalWI2022-23.json";
import DATA_SP2023 from "./data/IndexedTotalSP2022-23.json";
import DATA_FA2024 from "./data/IndexedTotalFA2023-24.json";
import DATA_WI2024 from "./data/IndexedTotalWI2023-24.json";
import DATA_SP2024 from "./data/IndexedTotalSP2023-24.json";
import DATA_FA2025 from "./data/IndexedTotalFA2024-25.json";
import DATA_WI2025 from "./data/IndexedTotalWI2024-25.json";
import DATA_SP2025 from "./data/IndexedTotalSP2024-25.json";
import DATA_FA2026 from "./data/IndexedTotalFA2025-26.json";
import DATA_WI2026 from "./data/IndexedTotalWI2025-26.json";
import DATA_SP2026 from "./data/IndexedTotalSP2025-26.json";
import DATA_FA2027 from "./data/IndexedTotalFA2026-27.json";

export const CURRENT_TERM = "/fa2027";

export const courseDataSources: {
[key: string]: { [key: string]: CourseData };
const courseDataLoaders: {
[key: string]: () => Promise<{ default: CourseIndex }>;
} = {
"/fa2023": DATA_FA2023,
"/wi2023": DATA_WI2023,
"/sp2023": DATA_SP2023,
"/fa2024": DATA_FA2024,
"/wi2024": DATA_WI2024,
"/sp2024": DATA_SP2024,
"/fa2025": DATA_FA2025,
"/wi2025": DATA_WI2025,
"/sp2025": DATA_SP2025,
"/fa2026": DATA_FA2026,
"/wi2026": DATA_WI2026,
"/sp2026": DATA_SP2026,
"/fa2027": DATA_FA2027,
"/fa2023": () => import("./data/IndexedTotalFA2022-23.json"),
"/wi2023": () => import("./data/IndexedTotalWI2022-23.json"),
"/sp2023": () => import("./data/IndexedTotalSP2022-23.json"),
"/fa2024": () => import("./data/IndexedTotalFA2023-24.json"),
"/wi2024": () => import("./data/IndexedTotalWI2023-24.json"),
"/sp2024": () => import("./data/IndexedTotalSP2023-24.json"),
"/fa2025": () => import("./data/IndexedTotalFA2024-25.json"),
"/wi2025": () => import("./data/IndexedTotalWI2024-25.json"),
"/sp2025": () => import("./data/IndexedTotalSP2024-25.json"),
"/fa2026": () => import("./data/IndexedTotalFA2025-26.json"),
"/wi2026": () => import("./data/IndexedTotalWI2025-26.json"),
"/sp2026": () => import("./data/IndexedTotalSP2025-26.json"),
"/fa2027": () => import("./data/IndexedTotalFA2026-27.json"),
};

const courseIndexCache: { [key: string]: CourseIndex } = {};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Module-level courseIndexCache is a process-global singleton

The courseIndexCache at src/courseData.ts:21 is a module-level mutable object that persists across the entire application lifetime. This is fine for a client-side SPA, but if this module were ever imported in an SSR context, the cache would be shared across requests. Given this is a Vite + Cloudflare Workers (wrangler) project, verify that this module is only used on the client side. Currently it appears to be client-only based on the usage in useAppState.ts (a React hook), so this is safe.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


export function getCachedCourseIndex(term: string): CourseIndex | undefined {
return courseIndexCache[term];
}

export async function loadCourseIndex(term: string): Promise<CourseIndex> {
const cached = courseIndexCache[term];
if (cached) {
return cached;
}
const loader = courseDataLoaders[term];
if (!loader) {
return {};
Comment on lines +33 to +34

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: loadCourseIndex does not cache empty results for unknown terms

When loadCourseIndex is called with an unknown term (no matching loader), it returns {} without caching the result (src/courseData.ts:33-34). This means each call for an unknown term creates a new Promise and returns a new {} object. In the current code, useLayoutEffect only calls loadCourseIndex once per realPath change, so this isn't a performance issue. However, repeated navigation to an invalid term would repeatedly invoke the function. This is minor since there's no actual I/O for unknown terms.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
const module = await loader();
courseIndexCache[term] = module.default;
return module.default;
Comment on lines +27 to +38

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: loadCourseIndex has benign duplicate-call race but no deduplication

In src/courseData.ts:27-38, if loadCourseIndex is called concurrently for the same term (e.g., via React strict mode double-effects), both calls will miss the cache and independently call loader(). Both will resolve to the same module.default and both write it to the cache. This is harmless since the data is identical, but a pending-promise cache (storing the promise itself) would avoid the duplicate network/parse work. The useLayoutEffect in src/useAppState.ts:69-89 has a proper cancelled flag that handles this scenario correctly on the consumer side.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — the duplicate-call race is benign (same module record resolves for both callers; dynamic import() is deduped by the module loader, so there's no double network/parse in practice). A pending-promise cache would be a fine micro-optimization but isn't needed. Leaving as-is.

}
1 change: 1 addition & 0 deletions src/css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module "*.css";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: New css.d.ts declaration covers all CSS imports needed by the project

The new src/css.d.ts file declares *.css as a module. This is likely needed because Vite 8 (or TypeScript 6) changed how CSS imports are typed. The project imports CSS in four places (src/index.tsx, src/Planner.tsx × 3), all as side-effect-only imports (import "./foo.css"). The broad declare module "*.css" declaration covers all of these. This is a standard pattern for Vite projects.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — informational only. The declare module "*.css" shim covers the project's four side-effect CSS imports under the TypeScript 6 / Vite 8 toolchain from commit e6c7bf1, and npm run verify (tsc included) passes. No action needed.

Loading