Skip to content

[ui] Composition API migration + mobile responsive pass#2011

Open
frankrousseau wants to merge 35 commits into
cgwire:mainfrom
frankrousseau:main
Open

[ui] Composition API migration + mobile responsive pass#2011
frankrousseau wants to merge 35 commits into
cgwire:mainfrom
frankrousseau:main

Conversation

@frankrousseau
Copy link
Copy Markdown
Contributor

Problems

  • A large block of admin pages still uses Options API mixins, making it hard to share helpers with newer <script setup> components and forcing legacy $tc / head() patterns
  • Most admin pages (asset library, asset types, departments, task statuses, task types, hardware items, software licenses, news feed, salary scale, entity search) are unusable on tablet/mobile: fixed-width columns, side panels eating the screen, missing breakpoints
  • The news feed timeline shows a plain spinner and a bare "No news" text — no visible loading rhythm, no hint when filters are too narrow, no real-time feedback when a news:new socket event arrives
  • TaskStatus / TaskTypes drag-to-reorder triggers on touch even though there is no good UX for it on mobile
  • Profile / Settings / Notifications and the SharePlaylist / ChangeAvatar modals share inconsistent card markup, save-button placement, and loading flags — clicking one action loads every section at once
  • Avatar uploads went straight to the server at full size with no client-side framing, and the production picture upload had its own separate cropping flow
  • vue-date-picker's default dark border (#2d2d2d) is barely visible against its #212121 background
  • Two small bugs in adjacent code: annotations were wiped before the save succeeded (lost on network failure), and the shared-playlist preview download URL was hard-coded to .mp4

Solutions

  • Migrate to <script setup> with proper section layout, useStore getters, useHead, and pipe pluralization: AssetLibrary, AssetTypes, Departments, TaskStatus, TaskTypes, HardwareItems, SoftwareLicenses, SalaryScale, EntitySearch, ProductionNewsFeed, Profile, Settings, Notifications, plus their tightly-coupled list components
  • Apply the standard 768 px responsive parity block on every migrated admin page; convert list tables to stacked cards on mobile (data-label attributes, hidden <thead>, hidden actions, last-card background fix). Add a 1024 px breakpoint for the news task panel that becomes a slide-in drawer with backdrop + sticky close button + Escape dismissal
  • News feed polish: extract NewsRow, NewsSkeleton, NewsFilters from the page, add an illustrated empty state, sticky day headers (position: sticky with the compound .timeline-entry.day-sticky selector to outrank .timeline-entry { position: relative }), and a slide-down + light-green pulse animation for entries arriving via the news:new socket
  • Disable <draggable> on mobile (:disabled="isMobile" + window.resize tracking) for TaskStatus and TaskTypes lists
  • Profile / Settings / Notifications redesigned around a shared Card widget owning its own save button, error and success state; per-action loading instead of a single page-wide flag
  • Introduce an ImageCropper widget that crops on the client before upload (circle shape for avatars, rounded for production pictures); reused by both ChangeAvatarModal and EditProductionModal, with the avatar preview circle becoming the centerpiece of the modal
  • Replace spinners with staggered skeleton loaders driven by a new useSkeletonCycle composable; ship GridLoadingSkeleton, ListLoadingSkeleton, KanbanLoadingSkeleton, NewsSkeleton
  • Override --dp-border-color on .dp__theme_dark to #3a3a3a with !important (the lib CSS is imported after App.vue in main.js)
  • Keep annotation edits in memory until the save call resolves, retry on error; correctly resolve the preview download URL for non-mp4 files in shared playlists
  • Small cleanups: align budget icon gray with other section icons, render plugin icons at the right size in nav, inset the toggle knob inside its track

frankrousseau and others added 30 commits May 13, 2026 12:58
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the spinner shown by TableInfo while a list loads with
Slack-style skeleton rows that appear one by one and restart in a
continuous wave. Three variants are dispatched through TableInfo:

- list (default): thumbnail + name + N cell bars + actions; entity
  lists (asset, shot, sequence, edit, episode) opt into big-cells
  for validation-shaped blocks
- kanban: 4 columns of stacked card placeholders, used by KanbanBoard
- grid: rows of name + day-cells + actions, used by PeopleTimesheetList

Each list passes the props that match its actual column shape
(cells, with-thumbnail, with-actions). The shared cycle/restart and
fade-in/out logic lives in a new useSkeletonCycle composable. A
--skeleton-rgb variable was added so the placeholder color stays
visible against both light and dark backgrounds.

Also: source the asset page filter list directly from userFilters so
the filter pills stay visible during asset list loading instead of
being briefly cleared by LOAD_ASSETS_START.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each two-factor method (TOTP, email OTP, FIDO) is now displayed in its
own card with an icon, status badge and inline configuration flow.
Clicking enable expands the QR code, OTP input or device name input
inside the card itself instead of stacking them above three full-width
buttons.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The toggle knob was sized exactly half the track width (1.3rem of 2.6rem)
and placed flush against the left/right edges, so the track only showed
as a thin halo top/bottom and disappeared horizontally. Shrunk the knob
slightly (1.2rem) and offset it 0.15rem inside the track on both sides
so the off / on positions read correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Migrate the page from Options API to <script setup>.
- Replace the green header band + negative-offset avatar trick with a
  centered avatar, display name and role label that adapts to the
  current theme.
- Split content into three cards (Information, Notifications, Change
  password / 2FA) with right-aligned Save buttons scoped to each card.
- Switch the timezone and language pickers from raw <select> to the
  shared Combobox, keep the English language order, append the native
  name in parentheses.
- Replace ComboboxBoolean notification dropdowns with the toggle
  Checkbox; in-channel user-id text fields appear only when the channel
  is enabled.
- Use a two-column grid for first/last name and email/phone above
  768px.
- Theme cleanup: page on var(--background-page), cards on
  var(--background) / var(--background-alt), explicit color: var(--text)
  inside .card and on form labels so they no longer fall back to the
  legacy grey color.
- Add profile.notifications_title locale key in en/fr.
- store/api/people: accept booleans for notifications_* flags via a
  small toBool helper so the new boolean-typed form keeps working
  alongside the legacy 'true'/'false' strings used by EditPersonModal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous Change Avatar modal sent the original file as-is. Now the
modal lets the user reposition and zoom the picture inside a circular
preview frame; on confirm the visible region is drawn into an offscreen
400x400 canvas, exported as a JPEG blob and packed into a fresh
FormData that is forwarded to the upload action.

- ChangeAvatarModal: migrated to <script setup>, drives drag (mouse +
  touch) and a zoom slider against a fixed preview frame, with scale
  bounds that keep the image always covering the circle. Object URLs
  are released on reset / unmount.
- Profile: forwards the cropped FormData from `confirm` to the
  `uploadAvatar` dispatch instead of ignoring the payload.
- store/modules/user.uploadAvatar: accepts an optional formData
  argument and falls back to state.avatarFormData so existing call
  sites stay working.
- Locales: tighten profile.avatar.preview_hint to describe the new
  drag + zoom interaction (en + fr).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The plugin entries used Lucide's <icon /> without a size override, so
the SVG kept its default 24x24 attribute pair. The .nav-icon /
.section-icon classes only constrain width to 20px, leaving the height
at 24 and stretching the icon vertically next to the other 20x20
sidebar / topbar entries. Pass :size="20" on the plugin <icon /> in
the sidebar and in both topbar section-list render sites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Convert Notifications.vue to <script setup>; replace mapGetters /
  mapActions with useStore-backed computeds and direct dispatches.
- Inline the parametersMixin logic (URL query persistence + local
  preferences) since the mixin only had this page as a consumer.
- Replace the legacy `socket: { events: ... }` option with explicit
  on/off registration against the global $socket grabbed via
  getCurrentInstance in onMounted / onBeforeUnmount.
- Group the five filter watchers into one watcher on the relevant
  parameter keys.
- Use useHead for the page title.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add a @media (max-width: 768px) block that wraps the filter row
  selectors across multiple lines (50% each), resets the hardcoded
  width on ComboboxTaskType / ComboboxStatus / ComboboxStyled so they
  stretch with the flex layout, tightens page / filter-bar padding,
  and on each notification row hides the date, entity thumbnail and
  read toggle while bumping padding for breathing room.
- Filter bar: add a 1em top margin so the card sits below the topbar
  edge instead of flush against it.
- Notification rows: drop the border from 4px to 3px, lighten the box
  shadow (0 1px 2px rgba(0,0,0,0.06) light / 0.25 dark), scale the
  entity thumbnail down to 30px to match the avatar so the inline
  metadata aligns on a single baseline, swap the `flexrow-item`
  margin-soup for a single `gap: 0.6em` on `.notification-header`,
  and move the validation tag from after the entity link to right
  after the type icon so the task status reads first.
- Push the date to the right of the row (after the filler, before
  the read toggle).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace the spinner with a card-shaped skeleton loader (six rounded
  blocks, staggered fade-in / fade-out via the shared
  useSkeletonCycle composable). Lower the dark-mode opacity from 0.45
  to 0.15 so the blocks don't glow on the dark page background.
- Add a notificationAuthor() helper that returns a placeholder
  (initials "?", grey background, t('main.unknown') name) when the
  notification's author isn't in personMap so the avatar slot never
  silently disappears. Add main.unknown locale key (en + fr).
- Add a project avatar (ProductionName with only-avatar) next to the
  task-type chip; tooltip carries the project name.
- Two-row notification body:
    Row 1 — type icon, project avatar, task-type chip, entity
            thumbnail, entity link, then filler / date / read toggle.
    Row 2 — actor avatar, actor name, verb, optional "to" + status
            (validation-tag). Only renders when the notification is
            expanded (showComments or selected).
- Move the verb chain out of the expanded comment area onto the actor
  row so the action description sits next to the avatar.
- Append "to {status}" inline when the notification carries a status
  change, with a new notifications.to_status locale key (en / fr).
- Gate the .comment-content wrapper on showComments / isSelected so
  the bottom comment area no longer leaves an empty padding strip.
- Mobile (<= 768px): hide .date, .thumbnail-wrapper, .has-text-right,
  .actor-name, .verb; the row collapses to icon + project + task-type
  + entity link.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous Change Avatar modal pinned a generic file-upload button at
the top with a separate preview circle below, so the empty state and
the cropping interaction lived in two disconnected zones. The strings
were also leaking from the CSV import flow (`main.csv.select_file` /
`main.csv.upload_file`).

- Drop the FileUpload widget in favour of a hidden <input type="file">
  triggered by clicking the preview circle. The same circle accepts
  drag-and-drop files and is keyboard reachable (Enter / Space).
- When no photo is loaded the circle renders as a dashed dropzone with
  a Lucide UploadIcon + "Click or drop a photo here"; hover, focus and
  drag-over paint the border green for affordance.
- Once a file is selected the circle hosts the preview as before (drag
  to reposition, zoom slider unchanged) with a small "Choose another
  photo" text link and the drag/zoom hint stacked under it.
- Bump the preview from 140px to 180px so drag/zoom precision is
  comfortable; output canvas stays at 400px.
- Replace the CSV-leakage copy with three new locale keys —
  profile.avatar.intro, profile.avatar.drop_or_click and
  profile.avatar.replace — in en.js and fr.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The crop / drag / zoom logic that lived inside ChangeAvatarModal now
lives in a standalone widget consumed by both ChangeAvatarModal (avatar)
and EditProductionModal (production picture).

- Add src/components/widgets/ImageCropper.vue. Props: `shape` ('circle'
  or 'rounded'), `size` (display, default 180px), `outputSize` (canvas,
  default 400px), `outputType` / `outputQuality`. Emits `fileselected`
  with the raw FormData when an image is picked or dropped. Exposes
  `cropToFormData()` and `reset()` via defineExpose so parents can
  request the framed FormData on confirm.
- Slim ChangeAvatarModal down to a thin wrapper that forwards
  `fileselected` and calls `cropToFormData()` from its confirm handler.
- Replace the FileUpload + label block in EditProductionModal's
  picture field with the cropper (shape="rounded"); make
  `runConfirmation` async so the framed FormData is re-emitted via
  `fileselected` before the existing `confirm` payload reaches the
  parent. The existing storeProductionPicture + uploadProductionAvatar
  flow keeps working since it consumes whatever was last stored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Generate button buried in the modal footer left users guessing how
to create a link, the active link rows blended into the form, and the
revoke X was a single click away from cutting off external reviewers.
Refresh the layout so each section has a clear visual home.

- Wrap each active share link in its own card (background + border +
  rounded corners) so the list reads as a stack of items instead of
  bare rows. Cards swap surface in dark mode so the contained input
  still pops.
- Tighten the per-row action buttons: input takes the remaining space
  via flex: 1, the three icon buttons sit packed at the right with a
  0.2em gap, and the revoke button is always visible (was opacity 0
  until hover).
- Revoke now requires confirmation: clicking X swaps the row for a red
  inline confirm strip ("Revoke this link? Anyone using it will lose
  access." + Cancel / red Revoke).
- Hide the create form behind an "Add a new link" button. The button
  expands the form, the form auto-closes on successful create.
- Wrap the form in a card with an uppercase "Create a new link" title.
  Allow comments toggle is now a Checkbox toggle (was ComboboxBoolean,
  string 'true'/'false') and sits above the expiration date. Both
  fields are centered, label on top.
- Replace the modal footer's two-button row with a plain right-aligned
  Close button.
- Add an empty-state placeholder when no share link exists yet.

Locale: add playlists.share_modal.revoke_confirm, .no_links and
.add_link (en).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Settings page reuses ChangeAvatarModal for the studio / organisation
logo, where a circular crop doesn't match how the logo is displayed.
Expose a `shape` prop on the modal (default 'circle') that is forwarded
to ImageCropper, and pass shape="rounded" from Settings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apply the Profile page's card-on-page rhythm so the studio settings
read as grouped sections instead of one long form in a single card.

- Page background uses var(--background-page); content column at
  720px max with a centered studio header (rounded logo frame + studio
  name) and three cards stacked below.
- Header logo: 120x120 rounded frame (matches the new shape="rounded"
  cropper) with inner padding so the image is inset. Falls back to a
  dashed frame showing the studio's first letter when no logo is set.
  Text-link actions ("Set / Change logo", "Remove logo") sit below.
- Cards: Studio info (name + hours-by-day), Preferences (five
  Checkbox toggle widgets, was ComboboxBoolean with 'true' / 'false'
  strings), Integrations (Slack / Discord / Mattermost tokens with
  the webhook error inline). Each card ends in a right-aligned green
  Save button.
- Form values are now real booleans; the watcher coerces with
  Boolean(...) when hydrating from the store and saveSettings spreads
  the form directly rather than mapping each '=== "true"' field.
- Theme tokens replace the hardcoded white / $dark-grey-lighter card
  backgrounds. Cards stack with 1.5rem margin-bottom between
  siblings.
- New locale key: settings.preferences_title (en + fr).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Profile and Settings duplicated the same card + save button + error
markup three times each, the save buttons shared a single loading
flag so clicking one spun all three, and the TOTP widget had the same
issue across nine actions.

- Add src/components/widgets/Card.vue. Props: title, error, success,
  saveLabel, loading, disabled. Renders the card surface, uppercase
  title, the slotted body, and (optionally) a right-aligned green save
  button that emits @save. Error / success messages render
  automatically when the corresponding prop is set.
- Profile / Settings now use <card> directly: local per-section
  loading + errors refs (loading.info / notifications in Profile,
  loading.studio / preferences / integrations in Settings) replace the
  shared Vuex flag, so each card spins independently. The
  duplicated .card / .card-title / .card-actions / .save-button CSS
  is gone from both pages.
- user.saveProfile now returns the promise it kicks off and rethrows
  on error so the page-level handlers can await it.
- TwoFactorAuthenticationSetup: replace the single twoFA.isLoading
  boolean (shared by every button in the widget) with
  twoFA.loadingAction (string identifying which action is in flight:
  enableTOTP, preEnableTOTP, disableTOTP, enableEmailOTP,
  preEnableEmailOTP, disableEmailOTP, registerFIDO, unregisterFIDO,
  newRecoveryCodes, or null). Each button checks its own action name
  so clicking on one TOTP action no longer spins the rest. Buttons
  that only open a validation flow (disable TOTP / disable email OTP
  / generate new recovery codes / pre-register FIDO) drop the loading
  state entirely — they never actually waited for a request.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Convert AssetLibrary.vue to <script setup>; replace mapGetters /
  mapActions with useStore-backed computeds and direct dispatches.
- Drop the searchMixin dependency: this page only relied on the
  inherited setSearchInUrl helper (it overrides onSearchChange and
  doesn't use the route watcher), so inline a small setSearchInUrl
  that pushes the search query into the URL.
- Group imports per CLAUDE.md (third-party / @/lib / components by
  folder: layouts → sides → widgets).
- Use useHead for the page title.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Hide the "Add to library" side panel under 768px by listening to a
  matchMedia query in setup and gating PageLayout's `:side` prop.
- Wrap the filter row on mobile: each filter stretches to 100% width
  and stacks vertically with a clean 0.75em gap. Reset the global
  .field margin-bottom (only ComboboxProduction and the sort Combobox
  carry it; SearchField doesn't) so the rhythm doesn't read as tight
  above one filter and loose below the next.
- Tighten the page padding (2.5em → 4.5em on top so the title clears
  the topbar, narrower side margins) and explicit margin-bottom on
  the header and the filter row.
- Reset the global `ul { margin-left: 1em }` on `.items` so
  justify-content: center actually centers the asset cards on mobile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Page: convert AssetTypes.vue to <script setup>; replace mapGetters /
  mapActions with useStore-backed computeds + dispatches. Drop the
  carried-over `choices: []` data field that was never read. Rebuild
  the confirm handlers around try / await / catch instead of nested
  .then chains, with a `{ ...form, id }` spread so the incoming form
  is never mutated. Switch the export builder from
  `[headers].concat(rows)` to `[headers, ...rows]`. Make `tabs` a
  computed so the labels follow locale changes.
- List: convert AssetTypeList.vue to <script setup>; switch the
  deprecated `$tc('asset_types.number', n)` call to the equivalent
  `$t('asset_types.number', n)` (vue-i18n 9 pipe pluralization).
  Rename the `sortTaskTypes` method to `sortedTaskTypes` to avoid
  shadowing the imported helper of the same name.
- Both: apply the Studios.vue responsive recipe — `.asset-types`
  gets tighter side padding under 768px; the list collapses the
  fixed-width `.name` column and tightens table padding at the same
  breakpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Under 768px transform each table row of AssetTypeList into a self
contained card so the page no longer relies on horizontal scrolling.

- Tag every <td> with a data-label so it can carry its own caption.
- Drop <thead>, switch every table element to display: block, and
  render each <tr> as a rounded card (var(--background) light /
  var(--background-alt) dark, 1px border, 12px radius, 0.75em gap).
- A `::before { content: attr(data-label) }` pseudo-element prints
  the small uppercase muted caption above every cell value. The name
  cell doubles as the card title, so its label is hidden and its
  weight is bumped.
- Hide the action buttons on mobile (`.datatable-row .actions { display
  : none }`) — the edit / delete flow stays available on desktop.
- Hide empty short-name cells via a data-label-less placeholder <td>
  plus a `:not([data-label])` selector that keeps the desktop column
  layout intact.
- Keep the wrapper's vertical overflow scrolling (only override
  `overflow-x: visible`) so the list still scrolls inside .fixed-page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Page: convert Departments.vue to <script setup>; replace mapGetters /
  mapActions with useStore-backed computeds and dispatches. Rewrite the
  edit confirm handler around try / await with `{ ...form, id }` spread
  so the incoming form isn't mutated. Switch the export builder from
  `[headers].concat(rows)` to `[headers, ...rows]`. Make `tabs` a
  computed so labels follow locale changes.
- List: convert DepartmentList.vue to <script setup> and switch the
  deprecated `$tc('departments.number', n)` to the pipe-pluralization
  form `$t('departments.number', n)`.
- Both: apply the Studios.vue responsive recipe — page gets tighter
  side padding under 768px and the list folds rows into mobile cards
  (data-label captions, hidden head/actions) like AssetTypeList.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Under 768px the linked-hardware / linked-software tabs collapse the
three-column layout into a vertical stack, and the "Available items"
column is hidden — linking new items stays a desktop-only flow. The
list (departments + items linked to the selected department) is what
mobile actually needs.

Also reset the .color cell's hard-coded 20x20 sizing inside the mobile
DepartmentList card so the color row gets its own line with the label
above and the colored circle below.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Page: convert TaskStatus.vue to <script setup>; replace mapGetters /
  mapActions with useStore-backed computeds and dispatches. Rebuild the
  edit confirm handler around try / await with a `{ ...form, id }`
  spread so the incoming form isn't mutated. Switch the export builder
  from `[headers].concat(rows)` to `[headers, ...rows]`. Make `tabs`
  and `entityTabs` computed so the labels follow locale changes. Move
  `deleteText` from a method to a computed for cheap reactivity.
- List: convert TaskStatusList.vue to <script setup>; drop the unused
  formatListMixin (no format helper is actually used in the template).
  Replace deprecated `$tc('task_status.number', n)` with the
  pipe-pluralization form `$t('task_status.number', n)`. Initialise
  the draggable copy with a fresh array spread so reordering doesn't
  mutate the parent's prop.
- Both: apply the Studios.vue responsive recipe — page gets tighter
  side padding under 768px and the list folds rows into mobile cards
  (data-label captions, hidden head/actions) like AssetTypeList.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the short-name (status chip) column before the name column in
both the header and the row template, and drop the hardcoded
class="name" from TaskStatusCell so it no longer steals the parent
list's .name styling.

Add class="datatable-row-footer" on the row actions so the edit /
delete buttons stay hidden on desktop and slide in sticky-right on
row hover, matching the People page behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert HardwareItems.vue and HardwareItemList.vue to <script setup>:
swap mapGetters/mapActions for useStore-backed computeds and dispatches,
make tabs a computed so labels react to locale changes, replace head()
with useHead, and switch the deprecated $tc('hardware_items.number') for
$t with pipe pluralization. Stop mutating the form arg in the edit
confirm handler by spreading { ...form, id }, and rewrite the
usedAmounts / remainingHardwareItems accumulators with reduce.

Add the standard 768px responsive parity block on the page and the
shared mobile card layout on the list (hide thead, stack cells as a
labelled card via data-label attrs, hide the actions column).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert SalaryScale.vue to <script setup>: data() refs, mapGetters
becomes a useStore-backed computed, mapActions becomes inline
store.dispatch calls, mounted runs the setSalaryScale loader.

Add a parallel mobile rendering that loops departments x positions x
seniorities and stacks each department as a card with position blocks
and labelled inputs styled to match the desktop input-editor. Hide the
table under 768px and force the desktop table to 100% width so it no
longer sits narrow on the left. Disable the empty side column via
:side="false" so the page fills the full width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrate SoftwareLicenses.vue and SoftwareLicenseList.vue to
<script setup>: mapGetters/mapActions becomes useStore-backed computeds
and dispatches, tabs become a computed so labels track locale changes,
head() becomes useHead, and the deprecated $tc('software_licenses.number')
is replaced with $t pipe pluralization. The usedAmounts /
remainingSoftwareLicenses accumulators become reduce, and the edit
confirm handler stops mutating its form arg. Add the standard 768px
responsive parity block on the page and the shared mobile card layout
on the list, including the datatable-row-footer class so action
buttons fly in on hover like on the People page.

Fix the mobile card background bug on both SoftwareLicenseList and
HardwareItemList: the global App.vue rule
`.datatable-row:last-child:nth-child(even) td { background-color:
var(--background-alt) }` was painting the last row's cells a different
color from the row itself, breaking the uniform card look. Force the
tr to var(--background) (or var(--background-alt) in dark mode) with
!important and neutralize the td background-color so the card color
shows through cleanly for every entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert TaskTypes.vue and TaskTypeList.vue to <script setup>:
mapGetters/mapActions become useStore-backed computeds and dispatches,
tabs and entityTabs become computeds so labels track locale changes,
head() becomes useHead, $tc('task_types.number') is replaced with $t
pipe pluralization. The export builder uses [headers, ...rows], the
edit confirm handler stops mutating its form arg, and updatePriority
rewrites the forEach+push accumulator as a single .map. The savePriorities
throttling state moves from implicit `this` properties to plain `let`
variables in the module scope.

Add the standard 768px responsive parity block on the list (mobile card
layout with data-label attributes, last-card background override, td
background neutralization) plus the datatable-row-footer class on
row-actions so edit/delete buttons fly in on hover. Disable drag-and-drop
on mobile via an isMobile ref bound to a window.resize listener, and
reset the grab cursor under 768px. Drop two dead CSS selectors
(.priority and .color) that did not match any rendered element.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Annotations were silently lost when the save round-trip failed: the
mixin wiped additions/updates/deletions before the emit and the Vuex
action swallowed errors behind a misleading alert. Now the mixin
swaps active arrays into a pendingSave buffer, and the parent's
async handler confirms (drops buffer) or restores (merges back +
schedules retry) based on the action's outcome. The Vuex action
captures failures to Sentry with payload context and rethrows.
Guard against an undefined annotations payload in
UPDATE_PREVIEW_ANNOTATION mutation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
frankrousseau and others added 5 commits May 13, 2026 12:58
Bind `<draggable :disabled="isMobile">` and track viewport width with
an isMobile ref attached to window.resize (registered in onMounted,
torn down in onBeforeUnmount). Reset the cursor: grab to default under
768px so the row no longer suggests a draggable interaction it can't
fulfill there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert EntitySearch.vue to <script setup>: mapGetters/mapActions
become useStore-backed computeds and dispatches, data() splits into
refs for scalars and reactive blocks for results / searchFilter, the
template ref is renamed to searchField and bound directly, the keydown
listener moves to onMounted/onBeforeUnmount and head() becomes
useHead. The string-keyed `this[isLoadingMore + capitalize(name)]`
branching is replaced with a direct ref ternary, so the stringHelpers
import is no longer needed. Drop the dead personPath method and
getPersonPath import that were not referenced from the template.

Center the result cards under 768px so the wrapped grid no longer
left-aligns with empty space on the right. In ComboboxProduction,
force ProductionName's .avatar-name back on inside the combobox under
768px - the global rule hides it to save space everywhere else, but
inside a production picker the name is the whole point.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… loading

Convert ProductionNewsFeed.vue to <script setup>: mapGetters/mapActions
become useStore-backed computeds and dispatches, the timeMixin becomes
useTime(), the socket events: { ... } block becomes socket.on/off in
onMounted/onBeforeUnmount (socket pulled via
getCurrentInstance().appContext.config.globalProperties.$socket, same
pattern as Notifications.vue), the array-style :ref="`news-${id}`"
becomes a setNewsRef function-ref backed by a plain Map, head() becomes
useHead, and watchers split into one watch() per source. Drop dead code:
isStatsDisplayed, toggleStats, renderedStats, statMax, personName,
getPreviewPath, getPreviewDlPath and the orphan .stats CSS block.

Extract three page-scoped components under src/components/pages/news/:
NewsRow owns a single feed entry (comments + previews modes, own
helpers and store getters, emits @select), NewsSkeleton wraps the
useSkeletonCycle loading placeholder, NewsFilters owns the entire
filter strip (episode/status/task-type/person/from/to/preview-mode)
plus its own toggleFilters state and option-list computeds. The parent
talks to them via multiple defineModel v-models.

Replace the spinner with a skeleton-cards loader while news are
fetching. Fade the timeline rail's blue left-border to transparent
when loading or empty (isTimelineBlank computed + .timeline-blank
class with a 0.2s transition) so the rail doesn't run alongside an
empty section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mobile drawer for the task panel: under 1024px the .side-column
switches to position: fixed and slides in from the right when a news
is selected. A `.drawer-backdrop` sibling catches outside taps to
close, and a sticky close button at the top right of the drawer
(plus Escape) provides explicit dismissal. Drawer sits at z-index
250 above the topbar (204) so it covers the full viewport, and uses
`min(100vw, 420px)` for the width — full-screen on phones, capped at
420px on tablets. Overrides App.vue's global `.side-column` width
and margin-top with !important to win the cascade.

NewsRow gets a single root `<div ref="root" class="news-entry">` and
exposes the ref via defineExpose so the parent can call
getBoundingClientRect on a real DOM node instead of a Vue component
proxy.

Mobile timeline cleanup under 768px:
- Hide the blue rail (`.timeline { border-left: 0 }`) and big-dots in
  the day headers — chronology still reads via the sticky day labels.
- Hide the per-row dot, reset task-type/validation/date min-widths so
  short tags hug their content, wrap `.comment-content` to a second
  line with `flex-basis: 100%` and a padding-left aligning the entity
  thumbnail with the avatar above.
- Hide the filter toggle, episode filter and person filter from
  NewsFilters; only task-status and task-type stay.
- Add `:active` next to `:hover` so the border highlight shows on
  touch taps the same way it does on mouse hover.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PictureViewer.getDimensions() started from the image's natural width and
only ever shrunk it to fit the container, so small images stayed at their
natural size in fullscreen instead of filling the viewport. Mirror the
VideoViewer behaviour in fullscreen mode: scale to the container width and
clamp by defaultHeight while preserving the aspect ratio.

Fixes cgwire#1990

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant