Commit graph

402 commits

Author SHA1 Message Date
Paulo Castellano
da5066e43e fix(posts): merge the editor status into the mobile switcher bar
On mobile the header was a whole bar showing only the Draft/Saving/Saved status.
Move that status into the switcher bar (right side) and hide the header on mobile
(the scheduled banner still shows), reclaiming a row. The switcher clears the
floating hamburger when it's the top bar.
2026-07-18 09:20:51 -03:00
Paulo Castellano
b7351667e2 fix(posts): tidy the post-view header on mobile
Stack it into a clean back-button row and a date + status row (date left, badge
right) instead of cramming everything into one line where the date wrapped and
the badge floated.
2026-07-18 09:06:56 -03:00
Paulo Castellano
fd557fe559 fix(ui): align the mobile sidebar trigger with the header buttons
Move the floating hamburger from left-2/top-2 to left-4/top-3 so it lines up
with the header's content padding (and the calendar's nav buttons); bump the
non-header page top reserve to pt-14 to clear the taller trigger.
2026-07-18 08:59:55 -03:00
Paulo Castellano
13e5bdb761 fix(ui): match the mobile sidebar trigger to the icon button size
The floating hamburger was size-9 (36px) next to size-10 (40px) icon buttons, so
it read as too small. Bump it to size-10 to match.
2026-07-18 08:55:49 -03:00
Paulo Castellano
0716ea0bd5 fix(posts): mobile calendar header — nav + compact date on row 1, full-width New post below
Move Today up with prev/next, keep the date picker compact on the right instead
of full-width, and give New post its own full-width row.
2026-07-18 08:38:09 -03:00
Paulo Castellano
6e0d663fc7 fix(posts): redesign the mobile calendar header
Split the header into a dedicated mobile layout (< lg) and the desktop grid.
Mobile is two clean rows: prev / date picker (fills) / next, then Today +
full-width New post. Fixes the stretched, styleless Today button. Each header
owns its own controls, so the date picker and view switcher no longer need
lg:hidden / hidden lg:block toggles.
2026-07-18 08:33:17 -03:00
Paulo Castellano
3de62ec2e3 fix(posts): stack the posts index toolbar into full-width rows on mobile
Search, label filter, and New post each take their own full-width row below sm
(inline on desktop). The label filter trigger goes full-width to match.
2026-07-17 21:20:52 -03:00
Paulo Castellano
6ead4eee84 fix(analytics): full-width account selector on mobile
Matches the date-range picker below it instead of sitting content-width.
2026-07-17 21:16:43 -03:00
Paulo Castellano
07c2e7cbf3 fix(posts): tidy the mobile calendar header
- Show/hide the date picker and view switcher with Tailwind (lg:hidden /
  hidden lg:block) instead of the isMobile ref; isMobile now only drives
  effectiveView (which changes navigation behavior, not just visibility)
- Full-width buttons on mobile: prev / Today (fills) / next, and the date
  picker fills the row next to New post
2026-07-17 21:16:43 -03:00
Paulo Castellano
fe721d490b refactor(posts): render PostEditorActions once in the editor header
The scheduled and editable states each rendered their own copy; the actions
component already decides internally what to show, so it only needs one instance
pushed right with ml-auto.
2026-07-17 21:16:43 -03:00
Paulo Castellano
d3738a2ec1 fix(ui): stop the scrollable tabs list from clipping button borders
overflow-x-auto forces overflow-y to auto too, so the tab triggers' border and
shadow were clipped at the list edges (visible on the editor Preview/Channels/
Comments tabs). Drop the fixed h-10 and pad the list (p-1) so the scroll
container has room around the buttons on all sides.
2026-07-17 20:58:53 -03:00
Paulo Castellano
f9a1731073 fix(auth): stop AuthSplitLayout overflowing on mobile with wide content
The mobile grid column was implicit (auto), so it sized to content — a long
workspace name (truncate = nowrap, wide intrinsic size) stretched the form
column to its max-w-lg (512px) and overflowed a 375px viewport by ~31px on the
workspaces list. Make the column an explicit minmax(0,1fr) track and give the
grid item min-w-0 so it stays within the viewport and the name truncates.

Found by a runtime horizontal-overflow check; adds MobileOverflowTest guarding
it across the layout variants (AuthLayout, default, full-width editor, detail).
2026-07-17 20:00:13 -03:00
Paulo Castellano
2013808312 fix(ui): third-pass mobile fixes
- AutomationHeader reserves space for the floating hamburger on mobile (its full-width pages had the back button under the trigger)
- AlertDialogContent caps height + scrolls like DialogContent
- AiGenerateDialog preview wraps long tokens (break-words)
- Enlarge notification panel action buttons on the full-width mobile panel (max-sm:size-9)
- min-w-0 so the billing processing title-bar label can truncate
2026-07-17 19:41:52 -03:00
Paulo Castellano
3d33d0d380 fix(posts): enlarge the platform logo badge in the preview switcher on mobile 2026-07-17 19:27:09 -03:00
Paulo Castellano
e31116c4b7 fix(posts): scale the preview phone mockup down on mobile for side padding
The 380px mockup frame is wider than a phone viewport, so it sat edge-to-edge. Scale it to 90% below lg (origin top-center keeps it centered) so there's clean symmetric padding to the screen edges. Reverts the now-redundant px-2 container tweak back to px-4.
2026-07-17 19:24:55 -03:00
Paulo Castellano
ca42a9f0ef fix(posts): tighten preview padding to px-2 on mobile so the phone mockup fits pixel-perfect 2026-07-17 19:18:00 -03:00
Paulo Castellano
f5a9e40bc1 refactor(ui): mobile polish and dead-state cleanup from review
- Restore desktop tab spacing (lg:gap-6) on the automation tabs
- break-words / viewport width caps on the AI review chips, lightbox alt overlay, and pick-time popover
- Remove the now-unused hoveredCommentId state after the comment toolbar became touch-visible
2026-07-17 19:15:58 -03:00
Paulo Castellano
a4ed3bcdad fix(ui): mobile fixes from second-pass review
- NotificationBell panel was fixed at left-[17rem] w-[22rem] and ran off-screen on a phone; now full-width-with-margins below sm
- Reserve top space for the floating sidebar trigger so page headings no longer sit under the hamburger on mobile
- automations/Index header stacks on mobile (was the one index page the first pass missed)
- Contain the preview phone mockup horizontally so it never breaks the page on narrow screens
- Comment bodies wrap long unbroken tokens (break-words)
2026-07-17 19:15:58 -03:00
Paulo Castellano
15eef28161 fix(ui): mobile polish — responsive headings, menu placement, truncation
PageHeader downscales on phones; workspace switcher opens within the viewport; OAuth page-selects go icon-only + truncate; invite info rows truncate.
2026-07-17 18:38:21 -03:00
Paulo Castellano
5118640a9c feat(automations): mobile-safe builder with desktop notice
The node builder is gated to lg+ (its fixed side panels no longer overflow a phone); below lg it shows a 'works best on a larger screen' notice. Automation tabs scroll instead of cutting off.
2026-07-17 18:38:20 -03:00
Paulo Castellano
49282d2acb fix(ui): stack page headers and toolbars on mobile
Non-wrapping justify-between headers now stack below sm; fixed-width searches go full-width. Calendar header wraps and seeds isMobile at setup to avoid the month-grid flash.
2026-07-17 18:38:20 -03:00
Paulo Castellano
ccf888258d feat(assets): touch-friendly gallery card actions on mobile
Uploads/Unsplash/Giphy card actions are visible on touch instead of hover-only, without the dark scrim on small screens.
2026-07-17 18:38:20 -03:00
Paulo Castellano
b7b3809790 feat(posts): make the post editor usable on mobile
- Top switcher (Compose / Channels / Preview / Comments) reveals the panel that was hidden below lg
- Sticky bottom action bar puts schedule/publish/delete within thumb reach
- Composer media actions and comment toolbar are visible on touch (no hover needed); media grid is 3-up
- Clamp media picker and emoji picker widths to the viewport
2026-07-17 18:38:20 -03:00
Paulo Castellano
a92a87ac46 fix(ui): make tabs scrollable and clamp dialogs/trigger for mobile
- TabsList scrolls horizontally instead of overflowing the page
- DialogContent caps height with internal scroll; DialogScrollContent gets the mobile width gutter
- SidebarTrigger tap target bumped to 36px
2026-07-17 18:38:20 -03:00
Paulo Castellano
925fd85b95 refactor(posts): return the card display domain from the backend
Compute the bare display host once in LinkCardMetadata (via Laravel's Uri::host + Str::chopStart) and return it as card.domain, so the LinkCard component renders it directly instead of parsing the URL client-side. The component is now purely presentational.
2026-07-17 16:24:23 -03:00
Paulo Castellano
7c79b331f6 refactor(posts): drop the frontend URL trim, rely on the backend's clean uri
The link-preview endpoint re-runs UrlDetector on whatever it receives and returns the exact, trimmed URL as card.uri, so the composer only needs a rough match to detect and dedup a link. Removes the firstUrl helper's punctuation/paren trimming that mirrored the backend tokenizer, leaving one trivial regex.
2026-07-17 16:17:06 -03:00
Paulo Castellano
ec3f251401 refactor(posts): simplify the link-preview composable and card
Replace the hand-rolled debounce + watch + lastAttemptedUrl bookkeeping in useLinkCard with vueuse's watchDebounced over a computed URL — the watch only fires when the detected URL changes, so dedup and unmount cleanup come for free. Make firstUrl declarative (regex match + a single trim helper) and drop the www-strip regex in LinkCard for a plain startsWith/slice. Behavior unchanged: URL detection still mirrors the backend UrlDetector (verified against the same cases).
2026-07-17 15:57:51 -03:00
Paulo Castellano
f84c6f818a feat(posts): show link preview card in the X and Threads editors
X and Threads auto-generate link cards server-side from the URL's meta tags, so no publish-side work is needed. Reuse the shared useLinkCard composable to render the card in their editor previews, gated on no attached media.
2026-07-17 14:32:40 -03:00
Paulo Castellano
8648ed9720 feat(bluesky): add link preview cards for posts
Bluesky does not hydrate link cards server-side, so build the app.bsky.embed.external embed at publish time: detect the first URL, scrape its OpenGraph metadata, and re-upload the og:image as the card thumb. Works for web, API and MCP. Adds a posts/link-preview endpoint so the editor renders the card live. The thumb download is SSRF-guarded and does not follow redirects.
2026-07-17 14:32:40 -03:00
Paulo Castellano
e7f93f4aef refactor(ai-create): clarify prompt-rule naming and label the counter
- Rename AiPromptRules::promptRule() to wizardPromptRule() so the asymmetry is
  explicit: only the create wizard carries a minimum; the editor's generation
  reuses just the shared maximum.
- Add aria-live and a data-testid to the prompt counter so the over-limit state
  is announced to assistive tech and reachable from browser tests.
2026-07-17 11:26:15 -03:00
Paulo Castellano
3dbb8e6f7e fix(ai-create): sync the prompt limit across front and back
The counter added earlier drifted from the backend in two ways: it counted
UTF-16 code units over the raw (untrimmed) value, while the backend measures
Unicode characters (mb_strlen) over the trimmed value that is actually sent —
so emoji or trailing whitespace could falsely turn the counter red and block
the button. The 2000 limit was also copied into three places, and the wizard's
frontend `>= 3` minimum had no backend counterpart.

- Add App\Support\AiPromptRules as the single source of truth for the prompt
  bounds; both StartPostCreationRequest and GeneratePostContentRequest use it.
- Add min:3 to the create wizard endpoint so front and back agree (the editor's
  generate-content flow keeps `required` — it has no counter to mirror).
- Count code points over the trimmed value in AiPostWizard so the counter and
  the submit gate match what the backend validates, matching AltTextDialog.
- Cover min/max/boundary in PostAiCreateTest.
2026-07-17 11:03:41 -03:00
Paulo Castellano
a9628b7e8d
Merge branch 'main' into feat/ai-prompt-char-counter 2026-07-17 10:46:44 -03:00
Paulo Castellano
af9736642c Render the story background as a soft, lightened blur
- Rewrite fitToCanvas to build the blurred story background with Imagick: scale the image to fill the width, heavily gaussian-blur it so shapes dissolve into a colour wash, gamma-lighten it, and mirror the top half onto the bottom for a symmetric background; the foreground is contained (fills the width, never cropped). Falls back to a GD downscale-blur on hosts without ext-imagick.
- Clean up the fit temp file if the blur/encode step throws.
- Update the editor preview (VerticalMediaCanvas) to a matching mirrored, lightened blur so it tracks the publish output.
- Cover the lightened image-derived background, the vertical mirror, and the GD fallback path with unit tests.
2026-07-17 10:31:35 -03:00
Paulo Castellano
e1acdab2e3 Revert Facebook and YouTube preview refactors (video-only, no feature effect)
These previews are video-only, so routing them through VerticalMediaCanvas changed nothing; keep the shared canvas on Instagram (Story) and TikTok (Photo) where it actually fits off-ratio images.
2026-07-16 20:51:07 -03:00
Paulo Castellano
2722effba9
Merge branch 'main' into feat/story-blurred-background 2026-07-16 20:34:00 -03:00
Paulo Castellano
90ce205763 Harden Instagram story image fitting and cover it end to end
- Guard MediaOptimizer::fitToCanvas and cropToAspectRatio against huge-dimension sources (getimagesize budget check) so they fail cleanly instead of exhausting GD memory.
- Fit and crop now translate decode/process failures into a clean InstagramPublishException and remove their temp files via finally; the two paths are symmetric.
- publishStory reads the story canvas dimensions via data_get.
- Previews: restore the IG empty-state background, align the autoFitsImage suppression predicate to isImage, drop the explanatory comment, and use single-quote imports.
- Tests: real end-to-end story fit, undecodable/download/container failures, the memory guard (fit and crop), temp-file cleanup, blurred-background pixel assertions, and the aspect-ratio warning suppression.
2026-07-16 20:09:43 -03:00
Paulo Castellano
cf045f2cdb Harden per-image alt text across publishers, validation, and attach paths
Publishing:
- Only send alt text for images (isImage guards on LinkedIn, X, Discord, Mastodon); never inject altText into video/document payloads.
- X sets alt via a best-effort media/metadata call so a metadata failure no longer blocks the tweet.

Validation:
- Validate media alt_text with a closure on media.*.meta so width/height/duration/slide_* survive a post update (Laravel's excludeUnvalidatedArrayKeys was stripping them).
- Add ALT_TEXT_MAX_LENGTH constant, a proper string-type error, and a localized attribute name.

Media attach (REST + MCP):
- Support per-image alt on attach-media-from-url via structured urls: [{url, alt?}] and on the MCP upload tool via an optional alt; alt is stored only for images.
- Carry submitted meta onto hosted external-URL media so alt is no longer dropped.

Composer:
- Alt-text dialog disables Save and reddens the counter over the limit, counting code points of the trimmed value to match the backend.
- Autosave shows 'Saved' only on a successful response; the lightbox alt overlay renders for images only.

Adds unit, feature, MCP, and browser tests covering every path above.
2026-07-16 13:55:33 -03:00
Paulo Castellano
a1f2fea1bc Merge the alt-text setter into the save handler 2026-07-13 08:54:25 -03:00
Paulo Castellano
b5dfc37022 Show alt text inside the image lightbox (opt-in) 2026-07-11 13:09:34 -03:00
Paulo Castellano
e61d12ea94 Show only the alt text on the thumbnail, without the icon 2026-07-11 13:01:02 -03:00
Paulo Castellano
797750a1c8 Show the alt text on the thumbnail and drop the file-size badge 2026-07-11 12:49:56 -03:00
Paulo Castellano
3397b829c2 Show the alt-text length limit in the editor dialog 2026-07-11 10:49:31 -03:00
Paulo Castellano
8d7948cb2e Add per-image alt text editor to the post composer 2026-07-11 09:45:52 -03:00
Paulo Castellano
c74170a25a
Merge branch 'main' into feat/story-blurred-background 2026-07-07 11:13:59 -03:00
Paulo Castellano
44127c4890 Remove the Vitest frontend unit-test layer
The project's test strategy is backend + e2e only, so drop the Vitest setup that had been added for the crop math: the imageCrop.test.ts suite, vitest.config.ts, the dependency and test:unit script, the CI step, and the test:all reference. The crop math is exercised through the Pest browser test.
2026-07-04 10:53:08 -03:00
Paulo Castellano
ef81ea74a8 Add frontend unit tests and harden the cropper's test coverage
Add Vitest (test:unit) with exact-value tests for the crop math (containScale, defaultSelection, clampSelection, all resizeSelection corners, and mime/filename resolution), and wire it plus the Pest browser test into CI. The browser test now samples output pixels against a four-quadrant fixture, so it detects a blank, flipped, or wrong crop rather than only asserting a 512x512 canvas. Add composer test:all to run PHP + Vitest + browser tests locally.
2026-07-04 09:56:38 -03:00
Paulo Castellano
36ccc570e0 Rewrite the image cropper as a movable/resizable selection box
Replace the move-image-behind-a-fixed-frame model with a selection box over the fully visible (contain-fit) image: drag to move, corner handles to resize, locked to 1:1 for the square avatar/logo output. The default selection is inset so the crop outline is always visible, handles sit inside the frame so they are never clipped, and wheel/pinch zoom is swallowed. Reset drag state on open/close, guard pointer capture and multi-touch, and fail-safe the canvas encode. Update the crop copy in all 15 locales to match the selection model.
2026-07-04 09:56:31 -03:00
Paulo Castellano
2549a82e9b Address review: cropper mime/zoom/error fixes, harden browser test
- Output a canvas-encodable mime (jpeg/png/webp, else png) and keep the File's
  name/extension in sync, so non-encodable input (gif/svg/heic) no longer ships
  PNG bytes mislabeled as the original type.
- Clamp zoom to a maximum (8x cover) so scrolling in can't collapse the crop to
  a sub-pixel region.
- Handle undecodable/zero-dimension images (@error + naturalWidth guard) with a
  crop_error message instead of a permanently-disabled Save.
- Replace the str_contains(static::class) Vite heuristic with a dedicated
  BrowserTestCase ($fakesVite = false).
- The browser test now decodes the dispatched blob and asserts a 512x512 image,
  and uses route(..., absolute: false) instead of a hardcoded path.
- Remove tests/Browser/ProbeTest.php (committed debug scratch).
2026-07-03 22:07:23 -03:00
Paulo Castellano
c6369a6ddb Crop the avatar/logo before upload with a dependency-free cropper
Selecting an avatar or workspace logo now opens a crop dialog (drag + zoom)
before uploading, so the image is framed the way it renders. The crop is
performed client-side and the resized 512x512 result is what gets uploaded.

This reworks the idea from #131 without its cropper dependency: vue-advanced-cropper
was last released ~2 years ago and we did not want an unmaintained package for
something this load-bearing. What we need is narrow (fixed 1:1, a circle/square
mask, fixed-size output), so a small canvas-based cropper covers it:

- imageCrop.ts: pure transform math (cover-fit, clamp, zoom, viewport->source).
- ImageCropperDialog.vue: CSS-transform preview, pointer drag, wheel/button zoom,
  a ResizeObserver to measure the modal (no requestAnimationFrame timing hacks),
  and a canvas toBlob only on save.
- PhotoUpload.vue: opens the cropper on file select; the mask shape follows the
  display shape (round avatar / square logo) instead of always being round.
- crop_* strings added to all 15 locales.

Also installs Pest browser testing (pest-plugin-browser + Playwright) and adds a
browser test for the crop flow. TestCase only calls withoutVite() for non-browser
tests, since browser tests need the real Vite assets to boot the SPA. The Pest
browser server does not parse multipart uploads, so the test asserts the crop
dispatches the correct upload request; endpoint persistence stays covered by
ProfileUpdateTest.
2026-07-03 21:46:19 -03:00
Paulo Castellano
b7773849ec Save the autofilled site logo as the workspace logo on brand settings update
Brand autofill on the workspace settings page already captured the site logo
and rendered a preview beneath the URL, but the update flow never persisted it.
The store flow attached it via LogoAttacher; the update flow was missing all
three legs: the form field, the request rule, and the controller attach.

- BrandTab: add logo_url to the useForm payload so autofill can set it and the
  form submits it.
- UpdateWorkspaceRequest: validate logo_url (nullable url) — FormRequest strips
  any unvalidated key, so without a rule it was silently dropped.
- WorkspaceController::updateSettings: pull logo_url out of the validated data
  (it is not a column) and attach it through LogoAttacher, mirroring store.
2026-07-03 20:05:54 -03:00