From 8294d84cf2334ebf717ec3a83fe82189f6f1bbe4 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 11 Aug 2026 14:46:03 +0530 Subject: [PATCH 01/34] docs(spec): plan the frappe-ui beta.25 -> beta.45 upgrade Inventory of the 696-commit delta, restricted to what touches wiki: four loud breaks, thirteen silent ones, two non-idempotent codemods, and a phased order that puts the useFileUpload privacy audit first. Co-Authored-By: Claude Opus 5 (1M context) --- specs/frappe_ui_beta45_upgrade.md | 140 ++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 specs/frappe_ui_beta45_upgrade.md diff --git a/specs/frappe_ui_beta45_upgrade.md b/specs/frappe_ui_beta45_upgrade.md new file mode 100644 index 000000000..7f2d3389e --- /dev/null +++ b/specs/frappe_ui_beta45_upgrade.md @@ -0,0 +1,140 @@ +# Frappe UI `beta.25` → `beta.45` Upgrade + +Date: 2026-08-11 +Status: **Planned — not started.** +Research base: frappe-ui `v1.0.0-beta.25` (2026-07-20, what we ship) vs `v1.0.0-beta.45` (2026-08-10, npm `beta`). 696 upstream commits. Reference upgrade: [frappe/gameplan#543](https://github.com/frappe/gameplan/pull/543) (beta.36 → beta.43). + +Sources: `docs/content/docs/migration.md` and `docs/content/docs/changelog.md` at `v1.0.0-beta.45` (the changelog moved out of `v1-release/changelog.md` in #1031), diffed against the same files at `v1.0.0-beta.25`. 76 new changelog entries in the delta. + +## Goal + +Move `frontend/package.json` from `frappe-ui@1.0.0-beta.25` to `1.0.0-beta.45`, absorb every break that touches wiki, and keep the public reader's vendored token CSS in sync. + +## Delta inventory + +Everything below is new since `beta.25`. **Hit** = wiki has call sites. **Loud** = build/import fails. **Silent** = renders wrong with no error. + +### A. Loud breaks that hit us + +| # | Change | Wiki call sites | +|---|---|---| +| A1 | `frappe-ui/list-style.css` and `frappe-ui/editor-style.css` exports removed — the `frappe-ui/list` and `frappe-ui/editor` barrels are side-effectful now and ship their own CSS | `frontend/src/index.css:7,9` — delete both `@import`s | +| A2 | `Autocomplete` removed — split into `Combobox` (single) and `MultiSelect` (multiple) | `components/AssignDialog.vue` (uses `multiple` → `MultiSelect`). `components/SpaceList.vue` uses our *local* `components/Autocomplete.vue`, which is hand-rolled and unaffected | +| A3 | `pageMetaPlugin` removed | `main.js:20,45` — delete the import and `app.use`. `usePageMeta` (already used in `pages/Spaces.vue`) is the replacement | +| A4 | Node floor `>=20.19.0` via `engines` | local is v24.15.0 ✓; check CI images | + +### B. Silent breaks that hit us + +| # | Change | Wiki call sites | +|---|---|---| +| B1 | **Radius aliases removed.** `rounded`/`-sm`/`-md`/`-lg`/`-xl`/`-2xl` emit no CSS (preset replaces Tailwind's `borderRadius`). Map: `rounded→4`, `sm→1`, `md→5`, `lg→6`, `xl→7`, `2xl→8`. `rounded-none`/`-full` kept. `var(--radius-sm\|md\|lg\|xl\|2xl)` also dies | ~398 bare `rounded`, 114 `-lg`, 95 `-md`, 30 `-sm`, 13 `-xl`, 1 `-2xl`, plus directional `rounded-t/r/l/b/tl/tr`. Codemod handles most; bare `rounded` inside multi-line template literals is missed | +| B2 | **Chromatic ink scales shift one level.** New `ink-red-1` is old `ink-red-2`, all 11 chromatic families, scales end at `-9`. `ink-gray` does **not** shift. Old `-1` was white → manual, usually `text-white` | ~130 `text-ink-{red,green,amber,blue,violet,orange}-N` sites; `-1` sites exist (`ink-violet-1`, `ink-red-1`, `ink-green-1`, `ink-blue-1`, `ink-amber-1`) and need manual fixes | +| B3 | `Dropdown`/`ContextMenu`: `placement` prop ignored (falls back to `align="start"`); `{group, items}` → `{group, options}`; `component:` rows → `slots: { item }` | `components/MobileAppMenu.vue:2` (`placement="right"` → `align="end"`), `components/tiptap-extensions/CalloutBlockView.vue:335` (`placement="bottom-end"` → `align="end"`) | +| B4 | `PageHeaderMobile` `#left`/`#right` → `#prefix`/`#suffix`; `PageHeaderMobileTitle` `#icon` → `#prefix` | `components/SpaceList.vue:7` (`#right`), `pages/SpaceDetails.vue:9` (`#left`), `:15` (`#icon`), `pages/Contributions.vue:5` (`#right`) | +| B5 | **`Tabs` replaced by a composed family** `Tabs`/`TabList`/`TabTrigger`/`TabPanel`. Model is the trigger `value`, never an index. Layout defaults (`flex flex-1 overflow-hidden`, panel `overflow-auto`) are gone | `pages/Contributions.vue:30` — `v-model="activeTabIndex"` + `:tabs` + `#tab-panel`. Index model must become the tab key; check panel scrolling | +| B6 | **`useFileUpload()` / `FileUploadHandler` default to private.** An upload with no stated `private`/`is_private` now uploads `is_private=1` | `components/PageSettings.vue:175`, `components/WikiEditor.vue:142`, `components/SpaceSettings/GeneralPanel.vue:123`. **Audit each** — editor images and space/page cover images are served to the public reader with no session; a flip to private returns 403 | +| B7 | `Combobox`: `reset()` on a template ref → `clear()` | `components/SpaceSettings/PermissionsPanel.vue:263` | +| B8 | `Sidebar` no longer wraps the middle list in a scroll container or applies padding (app-owned); `SidebarHeader` `#logo` → `#prefix` | `components/Sidebar.vue` already composes and owns its own padding/scroll — verify visually, likely no change | +| B9 | `FrappeUI` plugin: `$resources` Options-API mixin no longer installs by default (`app.use(FrappeUI, { resources: true })`); `config`/`call`/`socketio` options removed | `main.js:44` installs `resourcesPlugin` directly, which still works. No component declares an Options-API `resources` block. Confirm with a grep before dropping | +| B10 | `Tooltip`: `placement` → `side`, `arrowClass` → `offset`/`[data-slot="arrow"]`, `#body` → `#content` | No hits — `components/AssigneeAvatars.vue` uses `text` only ✓ | +| B11 | Unused tokens removed: `text-tiny`, `text-13xl`–`text-16xl`, `shadow-status`, `--elevation-status`, `surface-alert-button-*`, `ink-alert-button-*`, `surface-alpha-gray-2-overlay` | No hits in `frontend/src` (only in built assets) ✓ | +| B12 | `Select`: `displayValue` trigger slot prop → `selectedOption.label` | `components/SpaceSettings/PermissionsPanel.vue:44` — check the trigger slot | +| B13 | Editor: media captions moved off `alt` to a `caption` attribute (`data-caption`); the editor no longer edits `alt` | Wiki ships its own `components/tiptap-extensions/image-extension.js` with caption support. Verify no collision with the frappe-ui editor's image handling | + +### C. Moved to subpaths (loud, no wiki hits) + +- `ListView` family → `frappe-ui/experimental`. **Our `frappe-ui/list` imports (`SpaceList.vue`, `ContributionsPanel.vue`) are the new List family and stay put.** ✓ +- `Calendar` family → `frappe-ui/experimental`. +- Sprite `Icon`/`IconPicker`/`spritePlugin`: `frappe-ui/icons` → `frappe-ui/experimental`. Named SFC icons stay on `frappe-ui/icons`. Wiki's `components/IconPicker.vue` is our own ✓. +- v0 `TextEditor` family removed from root, parked in `frappe-ui/experimental`. Wiki is already on `frappe-ui/editor` ✓. +- `frappe-ui/frappe` and `frappe-ui/drive` subpaths deleted. +- `code-editor` subpath folded into `experimental`. +- `./hljs-theme.css` export removed — wiki owns its highlighter already ✓. + +### D. Removed components (no wiki hits) + +`Input`, `FeatherIcon`, `Card`, `ListItem`, standalone ``, `MonthPicker`, `CircularProgressBar`, `GridLayout`, `NestedPopover`, `ListFilter`, `SearchComplete`. `FormControl type="autocomplete"` removed (silent — falls through to a text input). + +### E. Redesigns / renames worth knowing (no current hits) + +- **`Alert` redesigned for espresso 2.0**: stateless (no `v-model`, `v-if` + `@dismiss`), `theme="yellow"`→`"amber"`, default theme `gray`, `dismissible` defaults `false`, `variant` gone, `#icon`→`#prefix`, `#footer`→`primaryAction`/`secondaryAction`/`#actions`. Wiki registers `Alert` globally in `main.js` but has **zero `` call sites** — drop it from the global registration. +- `useTheme` → `useColorScheme` (`currentTheme`→`colorScheme`, read-only; `setTheme`→`setColorScheme`). Wiki's `composables/useTheme.js` is entirely local, no frappe-ui dependency ✓. beta.44/45 added a built-in transition mute across scheme swaps — potential future replacement for our `.no-transition` hack in `index.css`. +- Nine scroll members → `shellScrollContainer` + `useShellScrolled`; `useIsMobile`/`useScreenSize` un-exported. Wiki's `composables/useMobile.js` is local ✓. +- `PageHeaderBackButton`: `to` is now a fallback used only when there is no in-app history. +- `Popover` v0 API removed (`#target`→`#trigger` with reka wiring, `placement`→`side`+`align`, `show`→`open`, `#body`→`#default`+`bare`). Wiki uses reka-ui `PopoverRoot` directly in `IconPicker.vue` ✓. +- `TabButtons`: `type`→`variant`, `buttons`→`options`, `value` required per option, new `fluid`. Sliding indicator animation on both `Tabs` and `TabButtons`. +- `CommandPalette`: `show`→`open`. `KeyboardShortcut`/`useShortcut` surface trimmed. +- `DatePicker`/`TimePicker`/`DateRangePicker`: footer removed, `#actions` sidebar slot, `DateRangePicker` emit shape, deprecated aliases gone. +- Data fetching v2 (`useCall`/`useDoc`/`useList`/`useNewDoc`): one request per submit, `submit()` rejects on failure, `data` no longer cleared on failure, `error` no longer cleared on start, `isLoading(id)` replaces `params`-sniffing, `useFrappeFetch` un-exported, `FrappeResponseError` exported, a throwing `beforeSubmit` now cancels the submit. **Wiki is entirely on `createResource`/`createListResource`/`createDocumentResource`, which are unchanged** ✓. +- `frappeRequest` fixes: `onError` fired twice per failure; method names starting with `http` skipped the `/api/method/` prefix; `login` returned only `message` under `requestBaseUrl`. +- `createListResource`: `hasPreviousPage` was stale after `reload()`. +- `Dialog`: sibling-mount host dedup; the stack survives duplicate package copies (beta.43). May interact with our `dialog-overlay { animation: none }` workaround — retest the stuck-overlay class before keeping it. +- `Button`: solid red label contrast raised. +- Portal target: `usePortalTarget`/`providePortalTarget`/`portalTargetKey` for embedded hosts. +- `SidebarCard` — new promotional card component. +- `Charts` — new family at `frappe-ui/charts` with its own `--chart-*` tokens. + +### F. Build / tooling + +| Change | Action | +|---|---| +| `frappe-ui/tailwind` now exports `content` — the authoritative glob list | `frontend/tailwind.config.js` hand-maintains `src/components/**` + `src/molecules/**`, which **already drops classes the editor and list molecules emit**. Spread the export instead | +| `frappe-ui/tailwind` `tokens.js` export removed | Not imported directly ✓, but `scripts/generate-public-theme.mjs` reads `frontend/node_modules/frappe-ui/tailwind` — verify it still resolves | +| `wiki/public/css/frappe-ui-tokens.css` is generated from the installed frappe-ui | Regenerate via `yarn tailwind:build` after the bump. Header still says `beta.25`; the radius aliases it emits go away | +| `@tiptap/markdown` | Gameplan hit this: beta.43+ pulls `@tiptap/markdown@3.28.0`, which demands an exact `3.28.0` core and breaks against existing pins. Wiki has `^3.26.0` on every tiptap package — pin `@tiptap/markdown` to `3.26.0` or move the whole tiptap set to 3.28.0 together | +| `reka-ui ^2.10.1` resolution + prosemirror resolutions in `package.json` | Load-bearing today. Re-check whether beta.45 makes them redundant; do not drop them blind | + +## Codemods + +Two separate, **non-idempotent** runs. Both must land in the same commit as the version bump — the codemod without the upgrade renders wrong, and so does the upgrade without the codemod. + +```sh +cd frontend +npx --package frappe-ui@beta tokens-v2 --dry-run . # radius renames +npx --package frappe-ui@beta tokens-v2 . + +npx --package frappe-ui@beta tokens-v2 --ink-shift --dry-run . +npx --package frappe-ui@beta tokens-v2 --ink-shift . +``` + +`--ink-shift` writes a `.tokens-v2-ink-shift` marker in each target directory and refuses to run again while it exists. **Commit the marker** — on a fresh clone without it the guard is gone and a re-run double-shifts. Run it on `frontend/` (and separately on any other real package root); do not let it walk into `node_modules`. + +Neither codemod touches `wiki/public/**` or `wiki/www/**` — the public reader's hand-written CSS and Jinja templates need a manual radius/ink sweep. + +## Phases + +Tracer-bullet order: get the build green first, then fix silent breaks by blast radius, then verify. + +**Phase 0 — Branch and baseline.** `git fetch upstream develop`, branch `feat/frappe-ui-beta45` off `upstream/develop`. Commit this spec first. Record a `yarn build` baseline and screenshots of the sidebar, editor, settings dialog, contributions tabs, and public reader. + +**Phase 1 — Bump and make the build compile.** Bump to `1.0.0-beta.45`, pin `@tiptap/markdown` as needed, delete the two CSS `@import`s (A1), delete `pageMetaPlugin` (A3), drop `Alert` from the global component registration (E), spread the preset's `content` in `tailwind.config.js` (F). Build must pass. Confirm editor and list CSS still land in the output (grep the built CSS for ProseMirror and list rules, as gameplan did). + +**Phase 2 — Tokens.** Run both codemods on `frontend/`, commit the marker, grep for leftovers: +```sh +grep -rnE "rounded(-(sm|md|lg|xl|2xl))?\b" frontend/src # bare rounded in template literals +grep -rn -- "--radius-\(sm\|md\|lg\|xl\|2xl\)" frontend/src wiki/public wiki/www +grep -rnE "ink-(red|green|amber|blue|violet|orange|teal|pink|purple|cyan|yellow)-1\b" frontend/src +``` +Manual radius/ink sweep of `wiki/public/css/*.css` and `wiki/www/**`. Regenerate `frappe-ui-tokens.css`. Visual diff against the Phase 0 screenshots — B1 and B2 are the only changes in this phase, and both are pure-visual. + +**Phase 3 — Component API breaks.** One commit per item, ordered by blast radius: +1. B6 `useFileUpload` privacy audit — pass `private: false` explicitly wherever the file is served to the public reader. **Do this before anything cosmetic; it is the one break that can silently 403 published content.** +2. A2 `Autocomplete` → `MultiSelect` in `AssignDialog.vue` (v-model payload inverts to a value array). +3. B5 `Tabs` composed family in `pages/Contributions.vue` (index model → key model, restore the layout defaults the panel relied on). +4. B3 `Dropdown` `placement` → `align` (2 sites). +5. B4 `PageHeaderMobile` slot renames (4 sites). +6. B7 `Combobox.reset()` → `clear()`, B12 `Select` trigger slot. +7. B8/B13 verification passes. + +**Phase 4 — Verify.** `yarn build`, `yarn test`, Playwright e2e with `BASE_URL=http://wiki.localhost:8000`. Manual pass on: sidebar collapse, editor (image upload, captions, toolbar, bubble menu), settings dialogs, contributions tabs, mobile shell headers, public reader in both themes. Re-test the stuck-dialog-overlay class (memory: `project_sidebar_spec_flake`) to see whether beta.43's dialog-host fixes let us drop the `animation: none` override. + +**Phase 5 — PR.** Against `frappe/wiki` `develop`. Sanity-check `git diff --stat upstream/develop..feat/frappe-ui-beta45` is only our files. + +## Decisions (2026-08-11) + +1. **Go to `beta.45` now**, not wait for `1.0.0` final. The delta only grows, and gameplan is already past beta.43. +2. **Bump the whole tiptap set**, do not pin `@tiptap/markdown` back. `@tiptap/markdown` declares *exact* peers on `@tiptap/core` and `@tiptap/pm` at its own version, so the set has to move together. Target `^3.29.2` (latest). frappe-ui `beta.45` declares `^3.26.0` on every tiptap package, so 3.29.2 satisfies it; add `resolutions` for `@tiptap/core` and `@tiptap/pm` if yarn ends up with two copies. + +## Open question + +- Does `--ink-shift` need to run over `wiki/public` and `wiki/www` too, or is a manual sweep safer given the marker's one-shot semantics? From f177792ba92e715e3b15ff00051e391d963c3e36 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 11 Aug 2026 14:52:43 +0530 Subject: [PATCH 02/34] chore(deps): upgrade frappe-ui to 1.0.0-beta.45 Clears every break in the delta that fails the build. - tiptap moves to 3.29.2 as a set. @tiptap/markdown declares exact peers on @tiptap/core and @tiptap/pm at its own version, so a partial bump leaves two cores in the tree. yarn v1 ignores a "@tiptap/**" glob resolution, so all 43 packages are pinned explicitly. - frappe-ui/editor-style.css and frappe-ui/list-style.css are gone. The editor and list barrels self-import their CSS and are marked side-effectful, so the stylesheets ship with the subpath imports. The built output still carries 119 ProseMirror and 36 list-slot rules. - pageMetaPlugin is removed. Nothing declares the pageMeta() option; Spaces.vue already uses usePageMeta. - Alert is redesigned and stateless. It was registered globally with no call sites, so the registration goes rather than the migration. - Autocomplete is removed. AssignDialog used it with `multiple`, so it becomes MultiSelect, whose model is a value array rather than option objects. - tailwind.config.js spreads the preset's own `content` export. The hand-written globs it replaces covered src/components and src/molecules only, dropping every class the editor and list molecules emit. Co-Authored-By: Claude Opus 5 (1M context) --- frontend/package.json | 65 ++- frontend/src/components/AssignDialog.vue | 7 +- frontend/src/index.css | 11 +- frontend/src/main.js | 4 - frontend/tailwind.config.js | 9 +- frontend/yarn.lock | 633 ++++++++++------------- 6 files changed, 354 insertions(+), 375 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 5a4a4797e..5fc9d4b60 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,20 +14,20 @@ "dependencies": { "@floating-ui/dom": "^1.6.0", "@pierre/diffs": "^1.2.11", - "@tiptap/core": "^3.26.0", - "@tiptap/extension-image": "^3.26.0", - "@tiptap/extension-list": "^3.26.0", - "@tiptap/extension-table": "^3.26.0", - "@tiptap/extensions": "^3.26.0", - "@tiptap/markdown": "^3.26.0", - "@tiptap/pm": "^3.26.0", - "@tiptap/starter-kit": "^3.26.0", - "@tiptap/suggestion": "^3.26.0", - "@tiptap/vue-3": "^3.26.0", + "@tiptap/core": "^3.29.2", + "@tiptap/extension-image": "^3.29.2", + "@tiptap/extension-list": "^3.29.2", + "@tiptap/extension-table": "^3.29.2", + "@tiptap/extensions": "^3.29.2", + "@tiptap/markdown": "^3.29.2", + "@tiptap/pm": "^3.29.2", + "@tiptap/starter-kit": "^3.29.2", + "@tiptap/suggestion": "^3.29.2", + "@tiptap/vue-3": "^3.29.2", "@vueuse/core": "^14.1.0", "@vueuse/integrations": "^14.3.0", "@vueuse/router": "^14.2.1", - "frappe-ui": "1.0.0-beta.25", + "frappe-ui": "1.0.0-beta.45", "fuzzysort": "^3.1.0", "highlight.js": "~11.11.0", "idb-keyval": "^6.2.0", @@ -49,6 +49,49 @@ "vite": "^5.4.10" }, "resolutions": { + "@tiptap/core": "3.29.2", + "@tiptap/extension-blockquote": "3.29.2", + "@tiptap/extension-bold": "3.29.2", + "@tiptap/extension-bubble-menu": "3.29.2", + "@tiptap/extension-bullet-list": "3.29.2", + "@tiptap/extension-code": "3.29.2", + "@tiptap/extension-code-block": "3.29.2", + "@tiptap/extension-code-block-lowlight": "3.29.2", + "@tiptap/extension-color": "3.29.2", + "@tiptap/extension-document": "3.29.2", + "@tiptap/extension-dropcursor": "3.29.2", + "@tiptap/extension-floating-menu": "3.29.2", + "@tiptap/extension-gapcursor": "3.29.2", + "@tiptap/extension-hard-break": "3.29.2", + "@tiptap/extension-heading": "3.29.2", + "@tiptap/extension-highlight": "3.29.2", + "@tiptap/extension-horizontal-rule": "3.29.2", + "@tiptap/extension-image": "3.29.2", + "@tiptap/extension-italic": "3.29.2", + "@tiptap/extension-link": "3.29.2", + "@tiptap/extension-list": "3.29.2", + "@tiptap/extension-list-item": "3.29.2", + "@tiptap/extension-list-keymap": "3.29.2", + "@tiptap/extension-mention": "3.29.2", + "@tiptap/extension-node-range": "3.29.2", + "@tiptap/extension-ordered-list": "3.29.2", + "@tiptap/extension-paragraph": "3.29.2", + "@tiptap/extension-placeholder": "3.29.2", + "@tiptap/extension-strike": "3.29.2", + "@tiptap/extension-table": "3.29.2", + "@tiptap/extension-task-item": "3.29.2", + "@tiptap/extension-task-list": "3.29.2", + "@tiptap/extension-text": "3.29.2", + "@tiptap/extension-text-align": "3.29.2", + "@tiptap/extension-text-style": "3.29.2", + "@tiptap/extension-typography": "3.29.2", + "@tiptap/extension-underline": "3.29.2", + "@tiptap/extensions": "3.29.2", + "@tiptap/markdown": "3.29.2", + "@tiptap/pm": "3.29.2", + "@tiptap/starter-kit": "3.29.2", + "@tiptap/suggestion": "3.29.2", + "@tiptap/vue-3": "3.29.2", "shiki": "^3.23.0", "@shikijs/themes": "^3.23.0", "prosemirror-model": "^1.25.9", diff --git a/frontend/src/components/AssignDialog.vue b/frontend/src/components/AssignDialog.vue index b5f37080e..75c6bbef2 100644 --- a/frontend/src/components/AssignDialog.vue +++ b/frontend/src/components/AssignDialog.vue @@ -8,11 +8,10 @@

{{ __('Assign this change request to a reviewer. They will be notified and it will appear in their "Assigned to me" list.') }}

- @@ -34,9 +33,9 @@ diff --git a/frontend/src/composables/useTheme.js b/frontend/src/composables/useTheme.js index 545d91c54..1f4a90b90 100644 --- a/frontend/src/composables/useTheme.js +++ b/frontend/src/composables/useTheme.js @@ -1,38 +1,61 @@ -import { useStorage } from '@vueuse/core'; -import { computed } from 'vue'; - -// Module-level so desktop Sidebar and mobile top nav share one theme value. -const userTheme = useStorage('wiki-theme', 'dark'); - -// Suppress transitions for the swap itself: without this, every element with a -// colour transition animates independently and the page flashes on its way to -// the new theme. Two rAFs so the class survives the style + paint of the swap. -function applyTheme(theme) { - const root = document.documentElement; - root.classList.add('no-transition'); - root.setAttribute('data-theme', theme); - requestAnimationFrame(() => { - requestAnimationFrame(() => { - root.classList.remove('no-transition'); - }); +import { resolvedColorScheme, useColorScheme } from 'frappe-ui'; +import { computed, ref } from 'vue'; + +// Light/dark now comes from frappe-ui's useColorScheme: it owns the +// `data-theme` attribute, the `theme` localStorage key, following the OS while +// the preference is `system`, and muting transitions across a swap so the page +// doesn't flash. That last part used to live here as a hand-rolled two-rAF +// dance plus a `.no-transition` rule in index.css; frappe-ui ships both, and its +// version also cancels a pending unmute so back-to-back swaps can't uncover a +// repaint. + +// Carry a preference saved under the old key over to frappe-ui's, once. Both +// surfaces used `wiki-theme` before; without this, everyone who had ever picked +// a theme would silently land back on `system` after the upgrade. Runs before +// the first useColorScheme() call so the restore below sees the migrated value. +if (typeof localStorage !== 'undefined') { + const legacy = localStorage.getItem('wiki-theme'); + if (legacy && !localStorage.getItem('theme')) { + localStorage.setItem('theme', legacy); + } +} + +// The painted scheme, which is what a consumer picking a light/dark asset +// actually needs. It is not derivable from the preference alone: `system` +// resolves against the OS, and an OS flip repaints without changing the +// preference, so there is nothing reactive to watch. Track the attribute +// frappe-ui writes instead. Module-level so every caller shares one observer. +const resolvedTheme = ref(resolvedColorScheme()); + +if (typeof document !== 'undefined') { + new MutationObserver(() => { + resolvedTheme.value = resolvedColorScheme(); + }).observe(document.documentElement, { + attributes: true, + attributeFilter: ['data-theme'], }); } export function useTheme() { + const { colorScheme, setColorScheme } = useColorScheme(); + const themeIcon = computed(() => - userTheme.value === 'dark' ? 'lucide-sun' : 'lucide-moon', + resolvedTheme.value === 'dark' ? 'lucide-sun' : 'lucide-moon', ); + // Not frappe-ui's toggleColorScheme: that one branches on the preference, so + // the first click while the preference is `system` picks `dark` — which is + // what a system-dark page is already painted in, and the click looks dead. + // Flipping the painted scheme always changes something. function toggleTheme() { - const next = userTheme.value === 'dark' ? 'light' : 'dark'; - applyTheme(next); - userTheme.value = next; - } - - // Reflect the stored theme onto once the mounting shell appears. - function initTheme() { - applyTheme(userTheme.value); + setColorScheme(resolvedTheme.value === 'dark' ? 'light' : 'dark'); } - return { userTheme, themeIcon, toggleTheme, initTheme }; + return { + colorScheme, + resolvedTheme, + themeIcon, + setTheme: setColorScheme, + toggleTheme, + }; } diff --git a/frontend/src/index.css b/frontend/src/index.css index e8df01827..18a623bbc 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -16,14 +16,9 @@ html[data-theme="light"] { color-scheme: light; } -/* Added by the theme switcher for the duration of one swap — many frappe-ui - components transition their colours, and letting them all animate at once - flashes the page. */ -.no-transition *, -.no-transition *::before, -.no-transition *::after { - transition: none !important; -} +/* The `.no-transition` rule that used to sit here now comes from + frappe-ui/style.css, and useColorScheme applies the class across a swap + itself. */ /* Dialog open/close animations are disabled: reka-ui's Presence unmounts the overlay only after the leave animation's end event, and rapid open/close diff --git a/frontend/src/layouts/MainLayout.vue b/frontend/src/layouts/MainLayout.vue index 2360b40f5..6c44772a3 100644 --- a/frontend/src/layouts/MainLayout.vue +++ b/frontend/src/layouts/MainLayout.vue @@ -79,7 +79,7 @@ From 01a03e3291414b479bbca6ec8a2e102fe8f21c1f Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Thu, 27 Aug 2026 13:07:37 +0530 Subject: [PATCH 31/34] style(callouts): match Alert on the public page and in print MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server-rendered callout gets the same treatment as the editor: a header row (icon + title) over a full-width body, one neutral surface, and the Alert status glyph in the type's colour. The `.callout-body` wrapper and the two-column grid are gone — the body is no longer indented beside the title. Public icons were also a shade off: they used the -2 inks where the SPA used -5. Print resolves the tokens to hex and floats the icon, since that pipeline has no CSS variables and QtWebKit parses neither oklch() nor flexbox. Drops the callout rules from wiki-rendered.css, whose only consumer — the standalone CR preview page — was removed in afb6808. The rest of that file is dead for the same reason but is left alone here. Co-Authored-By: Claude Opus 5 (1M context) --- frontend/src/wiki-rendered.css | 65 ----------------- .../standard_wiki_document.html | 47 +++++-------- wiki/public/css/main.css | 69 +++++++------------ wiki/wiki/markdown.py | 21 ++++-- wiki/wiki/test_markdown.py | 25 ++++++- 5 files changed, 82 insertions(+), 145 deletions(-) diff --git a/frontend/src/wiki-rendered.css b/frontend/src/wiki-rendered.css index dc653c567..16d004236 100644 --- a/frontend/src/wiki-rendered.css +++ b/frontend/src/wiki-rendered.css @@ -66,71 +66,6 @@ font-weight: 600; } -/* Callouts */ -.wiki-rendered .callout { - margin: 1rem 0; - padding: 0.875rem 1rem; - border-radius: 0.375rem; - display: grid; - grid-template-columns: auto 1fr; - gap: 0.75rem; - align-items: start; -} -.wiki-rendered .callout-icon { - display: flex; - align-items: center; - padding-top: 0.125rem; -} -.wiki-rendered .callout-icon svg { - width: 1rem; - height: 1rem; -} -.wiki-rendered .callout-body { - display: flex; - flex-direction: column; - gap: 0.5rem; -} -.wiki-rendered .callout-title { - font-weight: 500; - font-size: 0.875rem; - color: var(--ink-gray-9); -} -.wiki-rendered .callout-content { - font-size: 0.875rem; - line-height: 1.5; - color: var(--ink-gray-7); -} -.wiki-rendered .callout-content > *:first-child { - margin-top: 0; -} -.wiki-rendered .callout-content > *:last-child { - margin-bottom: 0; -} -.wiki-rendered .callout-note { - background-color: var(--surface-blue-2); -} -.wiki-rendered .callout-note .callout-icon { - color: var(--ink-blue-5); -} -.wiki-rendered .callout-tip { - background-color: var(--surface-green-2); -} -.wiki-rendered .callout-tip .callout-icon { - color: var(--ink-green-5); -} -.wiki-rendered .callout-caution { - background-color: var(--surface-amber-2); -} -.wiki-rendered .callout-caution .callout-icon { - color: var(--ink-amber-5); -} -.wiki-rendered .callout-danger { - background-color: var(--surface-red-2); -} -.wiki-rendered .callout-danger .callout-icon { - color: var(--ink-red-5); -} - /* PDF embed card (static; interactive viewer JS is reader-only) */ .wiki-rendered .wiki-pdf-embed { margin: 1.5rem 0; diff --git a/wiki/frappe_wiki/print_format/standard_wiki_document/standard_wiki_document.html b/wiki/frappe_wiki/print_format/standard_wiki_document/standard_wiki_document.html index fef647c4b..c954f189e 100644 --- a/wiki/frappe_wiki/print_format/standard_wiki_document/standard_wiki_document.html +++ b/wiki/frappe_wiki/print_format/standard_wiki_document/standard_wiki_document.html @@ -94,20 +94,24 @@ content: close-quote; } - /* Callouts — mirror the public-page treatment. Public CSS uses display:grid; - wkhtmltopdf's QtWebKit predates Grid, so use a float for icon-left layout. */ + /* Callouts — mirror the public-page treatment: the frappe-ui Alert banner, + one neutral surface with the type's colour only on the icon. Tokens are + resolved to hex here; the print pipeline has no CSS variables, and + wkhtmltopdf's QtWebKit cannot parse oklch() either way. */ .wiki-document-content .callout { margin: 1rem 0; - padding: 0.875rem 1rem; - border-radius: 6px; + padding: 0.75rem; + border-radius: 12px; + background-color: #F8F8F8; overflow: hidden; } + /* QtWebKit predates flexbox, so the header row is a float. */ .wiki-document-content .callout-icon { float: left; width: 1rem; - margin-right: 0.75rem; - padding-top: 0.125rem; + height: 1rem; + margin-right: 0.375rem; line-height: 0; } @@ -116,20 +120,21 @@ height: 1rem; } - .wiki-document-content .callout-body { + .wiki-document-content .callout-header { overflow: hidden; } .wiki-document-content .callout-title { display: block; - margin-bottom: 0.5rem; + overflow: hidden; font-size: 0.875rem; font-weight: 500; - line-height: 1.4; + line-height: 1.15; color: #171717; } .wiki-document-content .callout-content { + margin-top: 0.25rem; font-size: 0.875rem; line-height: 1.5; color: #525252; @@ -143,36 +148,20 @@ margin-bottom: 0; } - .wiki-document-content .callout-note { - background-color: #E6F4FF; - } - .wiki-document-content .callout-note .callout-icon { - color: #007BE0; - } - - .wiki-document-content .callout-tip { - background-color: #E4FAEB; + color: #0C8EF8; } .wiki-document-content .callout-tip .callout-icon { - color: #278F5E; - } - - .wiki-document-content .callout-caution { - background-color: #FFF7D3; + color: #43AC79; } .wiki-document-content .callout-caution .callout-icon { - color: #DB7706; - } - - .wiki-document-content .callout-danger { - background-color: #FFE7E7; + color: #DF9310; } .wiki-document-content .callout-danger .callout-icon { - color: #E03636; + color: #E03434; } diff --git a/wiki/public/css/main.css b/wiki/public/css/main.css index 52344d482..975b0c439 100644 --- a/wiki/public/css/main.css +++ b/wiki/public/css/main.css @@ -262,23 +262,28 @@ body { color: var(--ink-gray-6); } -/* Callout/Aside Styles - Frappe UI Alert-inspired */ +/* Callout/Aside Styles — the frappe-ui Alert banner: one neutral surface, with + the type's colour carried only by the icon. */ .callout { margin: 1rem 0; - padding: 0.875rem 1rem; - border-radius: 0.375rem; - position: relative; - display: grid; - grid-template-columns: auto 1fr; - gap: 0.75rem; - align-items: start; + padding: 0.75rem; + border-radius: var(--radius-6); + background-color: var(--surface-gray-1); +} + +.callout-header { + display: flex; + align-items: center; + gap: 0.375rem; } .callout-icon { flex-shrink: 0; display: flex; align-items: center; - padding-top: 0.125rem; + justify-content: center; + width: 1rem; + height: 1rem; } .callout-icon svg { @@ -286,23 +291,21 @@ body { height: 1rem; } -.callout-body { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - .callout-title { - font-weight: 500; + min-width: 0; font-size: 0.875rem; - line-height: 1.4; - color: var(--ink-gray-9); + font-weight: 500; + line-height: 1.15; + letter-spacing: 0.02em; + color: var(--ink-gray-8); } .callout-content { + margin-top: 0.25rem; font-size: 0.875rem; line-height: 1.5; - color: var(--ink-gray-7); + letter-spacing: 0.02em; + color: var(--ink-gray-6); } .callout-content > *:first-child { @@ -344,40 +347,20 @@ body { margin: 0.25rem 0; } -/* Note callout - Blue (info style) */ -.callout-note { - background-color: var(--surface-blue-2); -} - .callout-note .callout-icon { - color: var(--ink-blue-2); -} - -/* Tip callout - Green (success style) */ -.callout-tip { - background-color: var(--surface-green-2); + color: var(--ink-blue-5); } .callout-tip .callout-icon { - color: var(--ink-green-2); -} - -/* Caution callout - Amber/Yellow (warning style) */ -.callout-caution { - background-color: var(--surface-amber-2); + color: var(--ink-green-5); } .callout-caution .callout-icon { - color: var(--ink-amber-2); -} - -/* Danger callout - Red (error style) */ -.callout-danger { - background-color: var(--surface-red-2); + color: var(--ink-amber-5); } .callout-danger .callout-icon { - color: var(--ink-red-2); + color: var(--ink-red-5); } /* Image lightbox active state */ diff --git a/wiki/wiki/markdown.py b/wiki/wiki/markdown.py index 407202712..a38c679b0 100644 --- a/wiki/wiki/markdown.py +++ b/wiki/wiki/markdown.py @@ -56,12 +56,17 @@ def slugify(text: str) -> str: "warning": "Caution", # warning is alias for caution } -# SVG icons for each callout type +# SVG icons for each callout type. +# +# The frappe-ui Alert status glyphs (icon/solid/* in Figma) — the same paths the +# editor's node view inlines, kept in sync by hand: the SFCs behind +# `solidStatusIcons` are not exported from frappe-ui/icons, and this renderer +# has no build step that could import them. CALLOUT_ICONS = { - "note": '', - "tip": '', - "caution": '', - "danger": '', + "note": '', + "tip": '', + "caution": '', + "danger": '', } # Pattern to match callout blocks @@ -85,13 +90,15 @@ def _generate_callout_html(callout_type, title, inner_html): icon = CALLOUT_ICONS.get(callout_type, CALLOUT_ICONS["note"]) + # Header row, then a full-width body — the layout frappe-ui's Alert uses for + # its banner form. The editor's node view builds the same three elements. return ( f'" ) diff --git a/wiki/wiki/test_markdown.py b/wiki/wiki/test_markdown.py index f46c5b75a..f20173799 100644 --- a/wiki/wiki/test_markdown.py +++ b/wiki/wiki/test_markdown.py @@ -3,7 +3,7 @@ import unittest -from wiki.wiki.markdown import render_markdown, render_markdown_with_toc +from wiki.wiki.markdown import CALLOUT_ICONS, render_markdown, render_markdown_with_toc class TestMarkdownRenderer(unittest.TestCase): @@ -325,6 +325,29 @@ def test_callout_with_inline_markdown(self): self.assertIn('href="https://example.com"', result) self.assertIn("a link", result) + def test_callout_html_is_the_alert_banner_structure(self): + """Header row (icon + title), then a full-width body. + + The editor's node view builds the same three elements, so the public page + and the editor agree — the old markup nested the body beside the title in + a `.callout-body` grid cell. + """ + result = render_markdown(":::tip[Careful]\nBody text\n:::\n") + self.assertIn('