Repository navigation
Feat/free widget canvas - #454
Merged
Merged
Conversation
… redesign tour - unify homepage to custom canvas mode and remove obsolete `supportedModes` - add `simplify` variants for WigiPad and Tools in widget registry - update Simple mode migration coordinates and adjust widget dimensions to 2x4 - overhaul onboarding tour with custom RTL `TourTooltip`, refreshed Persian copy, and updated UX step priorities - consolidate VIP/Pro settings tabs and clean up appearance settings
… add-widget modal
Feat/habit tracker share card
generatePushCandidates only rejected candidate positions that overlapped a widget in fixedIds, i.e. the widget the user is actively dragging. In a cascading collision, where an already-pushed widget lands on a third widget that is not itself fixed, the widget's own current still-colliding position passed that check and was emitted as a candidate with dx = dy = 0. Its cost is lower than that of every real move, so solve() tried it first, reproduced the identical collision and looped until maxDepth was exhausted, rejecting drags for which a valid arrangement existed. Candidates must now always escape the blocker that triggered the push, which closes the self-referential loop. Also thread an optional widget registry through resolveLayoutChange into validateLayout. The allowedSizes check already existed in validation.ts, but the registry argument was never passed at any call site, so a widget's min/max size was never actually enforced at the engine level. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drag interaction: - Widget selection was dead code. setSelectedInstanceId was never called with a real id anywhere, so isSelected could never be true and isWiggling collapsed to "every widget wiggles at once" with no way to pick one. Pointer down now selects the widget it targets. - Escape mid-drag froze the widget visually but still committed the move: handlePointerMove bailed out once canvasMode flipped, while handlePointerUp only checked isDragActiveRef and went on to call moveWidget with the frozen offset. Escape now broadcasts cancelWidgetDrag, which aborts the gesture without committing it. - A second pointer overwrote the anchor of an in-progress drag, because handlePointerDown reset pointerStartRef on every event with no pointerId filtering. The gesture's pointer is now tracked and other pointers ignored, and onLostPointerCapture recovers state if capture is revoked. - Live drag offset moved from left/top to a transform, batched through requestAnimationFrame, so a drag no longer forces a reflow per pointer event. - Dropping a widget made it snap back to its origin and then slide to the destination: transform, position transition and the newly committed left/top all changed in one commit, so the browser animated left/top from the original cell while the transform vanished instantly. The transition is now suppressed for that single frame, letting the widget land where it was dropped. Grid geometry: Widgets render 12px shorter than their grid rect, but that inset lived as a magic number in canvas-widget-outer only, so the grid overlay cells were still drawn at full cellHeight and no longer lined up with the widgets on top of them. The inset is now a shared WIDGET_VERTICAL_INSET constant used by both, and the overlay adopts the shared widget radius. Verified that widget and cell edges match for every row/height combination. Edit mode is now left only through the toolbar's "پایان" button; clicking the canvas or pressing Escape just clears the selection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
While the canvas is in layout-edit mode the bottom navbar now slides away, reusing its existing hidden state, and the canvas edit toolbar moves down from bottom-20 into the slot the navbar occupied. The pull-tab that reopens a collapsed navbar is hidden too, so it cannot appear behind the toolbar. This keeps the bottom of the screen showing a single control bar at a time and gives the edit toolbar the prominence it needs while arranging widgets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
triggerServerSync captures the layout in a closure and fires 1s later, while the axios interceptor attaches whatever auth token is in storage at send time. Nothing cancelled that timer on an auth change, so logging out and back in as another user within the debounce window POSTed the previous user's layout under the new user's token. hasFetchedServerRef was likewise never reset, so after an in-session account switch the new user's server layout was never fetched and the stale in-memory layout could be synced onto their account. Both are now handled by an effect keyed on the auth token, which cancels any pending sync, allows the server fetch to run again and reloads the layout from storage. Further correctness fixes: - The initial server fetch overwrote savedLayout and runtimeLayout with no regard for edits made while it was in flight, so a slow GET silently reverted a move the user had just made. Because the sync response was only .catch()ed, nothing ever restored the correct layout and the UI stayed reverted for the rest of the session. The overwrite is now skipped once a local edit exists. - The debounced sync discarded its response, so a server-assigned instanceId was never written back for a widget created with a fallback local id. Every later edit re-sent instanceId: undefined, risking a duplicate server record each time. Returned ids are now reconciled back into local state. - Two open tabs each held their own snapshot with no listener for external storage changes, so whichever saved last silently discarded the other's edits. A watchStorage helper now keeps other tabs in step. This is a last-write-wins mitigation, not a merge; truly simultaneous edits can still race. - validateLayout and resolveLayoutChange are now called with the widget registry, so a widget's allowedSizes are enforced at the state layer instead of relying on the UI only ever offering pre-filtered size chips. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 4x1 search widget overflowed its cell, so the selection ring cut across the content instead of hugging it. Its content was built for the pre-inset 96px cell and adds up to exactly that: 4px outer padding, a 52px form row (py-2 plus h-9) and a 40px bookmark row (py-1, py-1, h-6). Once widgets started rendering 12px shorter than their grid rect the available height dropped to 84px, leaving the content 12px too tall, spilling past the ring top and bottom. Reclaim those 12px across the vertical rhythm rather than in one place, so nothing looks cramped: the form row goes to py-1 and both bookmark rows to py-0.5. Content is now 80px in an 84px box, with the remainder split evenly by the existing justify-center. The 2x1 variant was never affected: it carries no bookmark row and uses smaller controls, so it always fit. Also make the search surfaces fill the cell and share the widget radius while on the canvas. The visible box was auto-height and centred, so it was shorter than the cell the ring is drawn on, and its nested surfaces mixed a 16px inner curve with the 24px outer one only 2px apart. Off-canvas rendering keeps its original radius and intrinsic height. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The button rendered for an already-placed, non-duplicable widget called onSave, but handleSave had no removal path: the only removal logic lived in the else branch guarded by the module-level `const isCustom = true`, so it was unreachable. Execution instead fell to `if (!canAddCustom || !addWidget) return` and, since canAddCustom is always false for an active non-duplicable widget, the handler returned silently and the button did nothing at all. Removal now has its own handleRemove passed to the actions as onRemove, rather than overloading onSave and mirroring its trigger condition across two files, where any reordering of the action branches could silently break it again. The modal stays open afterwards so several widgets can be managed in one session and the button visibly flips back to "افزودن به صفحه". The widget limit also gated the remove button behind the upgrade prompt, so a free user who had filled every slot could not free one up, which is exactly when they need to. When a placed widget is blocked by a pro gate, whether the limit or the duplicate restriction, both actions are now offered side by side: upgrade to pro, or remove the widget. The repeated upgrade and remove buttons are extracted so the same markup is not copied across four branches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t options" This reverts commit e4058ea.
Restores widgets and grid overlay cells to their full cellHeight, removing the WIDGET_VERTICAL_INSET constant introduced to keep the two in sync while widgets rendered 12px shorter. Both sides go back together, so they stay aligned. Also removes the purple selection ring drawn around the widget being edited. The selectedInstanceId state itself is kept, since it still drives which widget stops wiggling in edit mode. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…viewports The two-column body was locked to a fixed 550px height. daisyUI caps .modal-box at calc(100vh - 5em) and the box clips its overflow, so on a short window the body was silently cut off with no way to scroll to what was hidden. The height is now viewport-aware: unchanged on a normal screen, shrinking to fit on a short one, which lets the panels size themselves and their scroll areas engage. On a narrow window the layout stacks, and there the previous two independent scroll areas split the available height between them, squeezing the preview panel down to nothing. Both panels now flow at their natural height and the body itself is the single scroll container, so the list and the preview read as one continuous scroll instead of two competing ones. The two-column behaviour with independent scroll areas is unchanged from md upwards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drops the comments added alongside the recent canvas, sync, search and widget-manager fixes, per project preference. No behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The layout engine only ran on pointer up, so nothing moved until the widget was released and the whole rearrangement then happened at once. Dragging now drives a live preview: crossing into a new cell re-runs the engine and updates the runtime layout, letting the surrounding widgets animate out of the way while the drag is still in progress. The preview is not persisted or synced; the drop commits it, and a cancelled drag restores the layout captured at drag start. Each preview resolves from that captured layout rather than from the previous preview, so pushes cannot accumulate and returning to the original cell restores the original arrangement. The engine only re-runs on a cell change, not per pointer event. The dragged widget now stays anchored to its starting cell and follows the pointer purely through a transform, which is also added to the item transition. On drop, position and transform animate together so it glides from where it was released to its cell, instead of snapping back to the origin first — this removes the need for the previous one-frame transition suppression. Fixes found while going over the drag flow: - The pointer-down guard latched on a ref that only a completed drag cleared, so a single missed pointerup left the widget permanently undraggable. It now keys off an active drag, which still ignores extra pointers mid-gesture but lets stale pending state heal on the next press. - setPointerCapture failures were swallowed, and without capture the widget never receives pointerup. A window-level pointerup/pointercancel listener now guarantees a drag always terminates; the element handler runs first and clears the tracked pointer, so the fallback cannot double-commit. - finishDrag now resets in a finally block, so a throw cannot leave a drag latched. - The drop row was unbounded, so a fast downward drag could land a widget far below the canvas. That inflates both the edit-mode grid overlay and the push engine's scan range, making the UI crawl until edit mode was left. The target row is now clamped to the visible grid. Clicking empty canvas leaves edit mode again. It only fires when the press both started and ended on the background, so releasing a widget drag over empty space no longer exits by accident. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two token faults and one API gap, all in the same area.
Every theme set a content token to its own surface colour. In light,
dark and glass:
--color-primary: var(--brand-primary);
--color-primary-content: rgba(var(--brand-primary-rgb), 0.9);
so any foreground on a primary surface rendered as that surface.
zarna, icy and esteghlal carried the same shape on primary, warning
and info content. Every pair is now scored against its surface and
set to a real contrasting value.
Call sites had papered over it with literals: 42 text-white and 35
indigo-500 classes stood in for tokens the themes could not supply.
Those now point at tokens, and the indigo ones at --color-vip.
Button carried one flat variant list, so every colour needed its own
entry and danger alone had four:
danger, danger-soft, danger-muted, danger-ghost
Split into two axes: variant (solid, outline, ghost, text) and color
(base, brand, primary, secondary, danger, success, warning, info,
vip). One danger, four appearances. 147 call sites moved over.
Three classes the button asked for compiled to nothing. rounded-card,
transition-ui and focus-ring were registered as class groups in cn.ts
but defined in no stylesheet, so every button shipped with no radius,
no transition and no focus ring. They are now @Utility rules in
index.css, which is also what lets focus-visible: and hover: prefix
them.
Also gone: btn-soft on pagination, inert without a .btn class;
btn-block in favour of the fullWidth prop; and a second cva that
duplicated the confirmation modal's confirm button colours.
Four buttons wanted a subdued look and got the filled default,
then dimmed the label by hand:
className="text-muted"
className="... text-muted hover:text-content ... border"
That pair is exactly what ghost/base now renders, so the classes
were restating the variant. The raw `border` in the friend picker
and `flex items-center` in the widget modal were restating the
button's own base classes as well.
All four now pass variant="ghost" and keep only what is genuinely
local: padding, sizing, and the hover:text-primary accent on the
widget settings button. The friend picker's bare `border` becomes
`border-content`, the semantic name for it.
They lose their filled background, which is the point: these read
as secondary actions beside a primary one.
The colour and theming section recorded its debt as fixed numbers:
- **253** opacity modifiers on surface tokens.
- **~267** hardcoded colour occurrences across roughly 68 files
Both went stale the moment the previous commit landed, and a reader
has no way to tell a stale number from a current one. The same held
for "there are currently zero in `src`" on the OS-keyed variants, and
for debt described in terms of the themes that happen to ship today.
Each is now stated as a shape of debt with the grep that measures it,
plus a note that the grep over-counts because opacity modifiers on
content tokens are the prescribed way to tint. The theme-stylesheet
entry becomes the general rule behind it: a theme file outlives the
markup it was written against, so a selector living there is not
evidence the class is still used.
…nvas branch # Conflicts: # src/layouts/widgets/calendar/variants/calendar-1x1.tsx
…rsor
Dragging a bookmark reverted on drop, and the dragged item trailed the
pointer before lunging after it. Two separate causes.
handleDragEnd looked the dragged bookmark up in a different list than
the one on screen. Render:
getCurrentFolderItems(currentFolderId, instanceId, isPrimary)
Drag end:
getCurrentFolderItems(currentFolderId, instanceId)
That third argument decides which root bookmarks the widget owns. Omitted,
the context falls back to `!widgetId || widgetId === 'bookmarks-default'`,
so a widget whose instanceId is anything else gets only bookmarks stamped
with that exact id. Reordering requires auth, an authenticated user's
layout comes from the server with server-assigned instance ids, and
existing bookmarks carry widgetId null or 'bookmarks-default'. So the list
on screen had bookmarks and the drag-end list was empty, sourceIndex came
back -1, and the handler returned with no state change and no toast.
bookmark.test.ts already documents the contract: pass true and a widget
with a server id gets its legacy bookmarks, omit it and it gets none.
The lag was a class overriding dnd-kit's own guard. sortable-bookmark-item
applied both the transition from useSortable and a Tailwind class:
className={`w-full h-full transition-transform duration-200 ...`}
dnd-kit strips the transition from the drag source so it tracks the pointer
exactly: with no DragOverlay, shouldDisplaceDragSource is true and
getTransition() returns undefined. React then omits the inline property and
the class is left standing, so every pointer move set a transform the
browser eased over 200ms. Non-dragged items get the same 200ms shuffle
inline from that function, so removing the class costs nothing.
Alongside, a behaviour-preserving pass on the same path. During a pointer
move BookmarkGrid does not re-render, so the props reaching each item keep
their identity for the whole drag, which makes memo effective without
touching a single callback:
- memo on BookmarkItem and FolderBookmarkItem, so only the dragged item
re-renders instead of every tile rebuilding its favicon url, title hash,
initials and sticker style array
- the folder tile's getCurrentFolderItems call, a find plus filter plus
copying sort over the whole bookmark array per folder per render, is now
memoised on [bookmarks, bookmark.id]
- stable items array for SortableContext and a hoisted pointer sensor
options object, both of which were new identities every render
Measured: background.js is byte identical, and the newtab chunk moves by
121 bytes for the memo wrappers and dependency arrays. The lazy
habit-share-modal chunk keeps its exact size; only its hash moves, because
it imports the newtab chunk by content-hashed filename.
In the glass theme, moving the mouse across the page intermittently drew
sharp, un-blurred page content over the widgets: wallpaper text crossing a
bookmark tile, or a dark slab landing on a neighbouring widget. It never
happened in dark or light.
That last detail is the whole diagnosis. .bg-glass only adds a filter in
two themes:
[data-theme="glass"] .widget-wrapper,
[data-theme="glass"] .bg-glass,
[data-theme="glass"] .tooltip {
backdrop-filter: blur(16px) saturate(150%);
}
dark and light ship no such rule, which is why they were clean.
Every widget on the canvas is positioned by canvas-widget-outer with
transform: baseTransform // translate3d(...), always present
A transformed ancestor gets its own compositing layer, and a
backdrop-filter inside that layer cannot reach the wallpaper, which is
painted on the body outside it. Given nothing valid to sample, Chromium
falls back to the wrong surface and composites raw page content on top.
The bookmark widget makes it easy to hit: ten tiles, each its own
backdrop-filter layer, each running three animations on hover.
So backdrop-filter cannot work inside the canvas, and the fix is to stop
asking it to:
[data-theme="glass"] .widget-outer .bg-glass {
backdrop-filter: none;
}
Scoped to widgets only. Modals, popovers and the search portals render
through createPortal, so they are not descendants of .widget-outer and keep
their blur, as do the navbar and tooltips.
The tiles stay at #171717b3, so they are now flat translucent rather than
blurred inside widgets. Bump that value if they read as too see-through.
…t-and-lag fix: bookmark reorder reverting, drag lag, and glass theme artifacts
…-and-gallery-cleanup Feat/combo widget settings and gallery cleanup
…AvatarComponent with isPro prop; update navbar layout and remove deprecated SettingsDropdown
…Trigger for improved responsiveness and VIP status handling
…to canvas context menu
…et modal handling
…e into NavbarLayout; remove widget management option from ProfileDropdownMenu
…mproved UI consistency
…erBookmarkItem for improved UI alignment; add page reload on logout confirmation
…ved navigation clarity
…dget refactor(widgets): remove wigipad widget and update default layouts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.