diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0cd6ee912..f24782a23f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: push: branches: [main] pull_request: - branches: [main] + branches: [main, feat/maic-editor-v0] concurrency: group: ci-${{ github.ref }} diff --git a/app/classroom/[id]/page.tsx b/app/classroom/[id]/page.tsx index 14a6f82e4b..6eabca6213 100644 --- a/app/classroom/[id]/page.tsx +++ b/app/classroom/[id]/page.tsx @@ -12,6 +12,8 @@ import { useWhiteboardHistoryStore } from '@/lib/store/whiteboard-history'; import { createLogger } from '@/lib/logger'; import { MediaStageProvider } from '@/lib/contexts/media-stage-context'; import { generateMediaForOutlines } from '@/lib/media/media-orchestrator'; +import { migrateScene } from '@/lib/edit/slide-schema'; +import type { Scene } from '@/lib/types/stage'; const log = createLogger('Classroom'); @@ -46,9 +48,18 @@ export default function ClassroomDetailPage() { if (json.success && json.classroom) { const { stage, scenes } = json.classroom; useStageStore.getState().setStage(stage); + // Normalize legacy slide content (missing schemaVersion) on the + // way in, same as the store's setScenes/loadFromStorage paths — + // server snapshots predate the schema field. + const migrated = (scenes as Scene[]).map(migrateScene); useStageStore.setState({ - scenes, - currentSceneId: scenes[0]?.id ?? null, + scenes: migrated, + currentSceneId: migrated[0]?.id ?? null, + // Match `loadFromStorage` semantics: mode is transient UI + // state, not persisted with the stage. Reset on every + // classroom load so SPA navigation doesn't carry Pro + // mode across. + mode: 'playback', }); log.info('Loaded from server-side storage:', classroomId); diff --git a/app/editor-fonts.ts b/app/editor-fonts.ts new file mode 100644 index 0000000000..27d9db3e4d --- /dev/null +++ b/app/editor-fonts.ts @@ -0,0 +1,39 @@ +/** + * Loads the web fonts offered in the slide editor's text-format picker. + * + * `@fontsource` ships the font files via npm (no binaries committed to the + * repo) and `unicode-range`-subsets the CJK faces, so a CJK font downloads + * lazily — only the glyph-range chunks a slide actually uses. Imported once + * from the root layout. + * + * The picker list lives in `configs/font.ts`; each entry's `value` must match + * the `@font-face` family name of a package imported here. Inter is loaded + * separately via `next/font` in `app/layout.tsx`. + */ + +// Latin +import '@fontsource/roboto/400.css'; +import '@fontsource/roboto/700.css'; +import '@fontsource/open-sans/400.css'; +import '@fontsource/open-sans/700.css'; +import '@fontsource/montserrat/400.css'; +import '@fontsource/montserrat/700.css'; +import '@fontsource/source-sans-3/400.css'; +import '@fontsource/source-sans-3/700.css'; +import '@fontsource/merriweather/400.css'; +import '@fontsource/merriweather/700.css'; +import '@fontsource/literata/400.css'; +import '@fontsource/literata/700.css'; +import '@fontsource/source-serif-4/400.css'; +import '@fontsource/source-serif-4/700.css'; +import '@fontsource/jetbrains-mono/400.css'; +import '@fontsource/jetbrains-mono/700.css'; + +// Chinese — @fontsource unicode-range-subsets these, so each loads lazily. +import '@fontsource/noto-sans-sc/400.css'; +import '@fontsource/noto-sans-sc/700.css'; +import '@fontsource/noto-serif-sc/400.css'; +import '@fontsource/noto-serif-sc/700.css'; +import '@fontsource/lxgw-wenkai/500.css'; +import '@fontsource/lxgw-wenkai/700.css'; +import '@fontsource/zcool-kuaile/400.css'; diff --git a/app/globals.css b/app/globals.css index d21d190f54..b9499f3ad4 100644 --- a/app/globals.css +++ b/app/globals.css @@ -132,12 +132,86 @@ body { @apply bg-background text-foreground; } + /* Radix Select / Popover wrap with `react-remove-scroll`, which adds a + compensation `padding-right` to when they open. Our + already reserves the scrollbar gutter (above), so that compensation + creates a visible layout shift on every dropdown open. Scope the + override to editor mode — the SlideCanvas sets `data-maic-editor` on + the body while mounted — so Radix's compensation still works on the + rest of the app (modals, sheets, etc. on non-editor pages). */ + body[data-maic-editor='true'] { + padding-right: 0 !important; + } } /* ProseMirror Editor Styles */ .prosemirror-editor { cursor: text; } +/* The slide editor draws a text element's frame via the renderer's Operate + layer. The focused contenteditable must not also paint a UA focus ring on + top of it (the base `* { @apply outline-ring/50 }` rule gives every focused + element an outline). `.prosemirror-editor` is an editor-only class — + playback's BaseTextElement never carries it, so playback is unaffected. */ +.prosemirror-editor :focus, +.prosemirror-editor :focus-visible { + outline: none; +} + +/* Tailwind's preflight resets `list-style: none` and `padding: 0` on + `