You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TL;DR: This app has no composables yet — stateful logic that doesn't render anything (dialog state, view-mode persistence, scroll-arrow tracking) currently lives inline in <script setup> blocks alongside markup-driving state. Introduce the pattern with one small, real extraction first, establish the conventions it needs, then let it spread opportunistically per AGENTS.md's existing "a component that keeps absorbing new responsibilities... extract the next one that lands" guidance — not a big-bang refactor.
Detail
Why now
Found while investigating a recurring test-coverage gap across #126/#127/#130 (tracked separately in #133): the same file keeps absorbing new interactive elements and derived state without ever being split up. Composables are the piece "extract it" doesn't cover on its own — component extraction splits render output, composables split stateful logic with no direct template dependency. This project is small (5 Vue files, ~1000 lines total) and young, so establishing the convention now is cheap; it gets more expensive the more code lands on the current shape.
First target: TemplateSection.vue's scroll-arrows cluster
list (element ref) + canScrollLeft/canScrollRight + updateArrows() + scrollByStep() + the onMounted/onUnmounted wiring is a self-contained "scrollable region with arrow buttons" state machine — reusable, no dependency on other in-flight work, and the same region already has an open a11y gap (#59: no aria-label identifying it as a scrollable region) worth closing while in there.
Check @vueuse/core before hand-rolling this one. Verified directly against its shipped type declarations: useScroll(element) already returns arrivedState.{left,right,top,bottom} (the inverse of canScrollLeft/canScrollRight) plus reactive x/y, and handles the listener attach/detach and resize-observation internally. The extraction should be a thin wrapper over useScroll, not new hand-rolled logic — this is the same "reuse before you write" ladder AGENTS.md already applies to @nextcloud/* packages and @nextcloud/vue components, extended to general-purpose Vue utilities. @vueuse/core@14.4.0's only peer dependency is vue@^3.5.0, which this app already satisfies.
Other candidates identified, for later extractions (not in scope for the first PR)
Each is its own extraction, its own commit, done opportunistically the next time that region is touched — not bundled into one refactor PR.
Conventions to establish with the first extraction, not invent per-composable later
Check @vueuse/core first. Before hand-rolling a composable, check whether it already exists there (as with useScroll above). Prefer it the same way an existing @nextcloud/vue component or design token is preferred over a hand-rolled equivalent.
Return individual refs, not a reactive() object. A composable that returns reactive({...}) breaks reactivity silently if the caller destructures it. Return individual refs (or toRefs() on an internal reactive object) so destructuring is always safe.
A shared test helper for composables using lifecycle hooks.onMounted/onUnmounted throw outside an active component instance, so testing needs a small reusable helper — a trivial host component whose setup() just returns the composable — living in src/test-utils/, written once with the first such composable rather than reinvented slightly differently by the second and third.
Same new-unit-plus-sibling-spec rule as src/utils//src/components/ today.src/composables/useScrollArrows.ts lands with src/composables/useScrollArrows.spec.ts in the same commit — this is also where the src/composables/ directory convention itself gets established.
TL;DR: This app has no composables yet — stateful logic that doesn't render anything (dialog state, view-mode persistence, scroll-arrow tracking) currently lives inline in
<script setup>blocks alongside markup-driving state. Introduce the pattern with one small, real extraction first, establish the conventions it needs, then let it spread opportunistically per AGENTS.md's existing "a component that keeps absorbing new responsibilities... extract the next one that lands" guidance — not a big-bang refactor.Detail
Why now
Found while investigating a recurring test-coverage gap across #126/#127/#130 (tracked separately in #133): the same file keeps absorbing new interactive elements and derived state without ever being split up. Composables are the piece "extract it" doesn't cover on its own — component extraction splits render output, composables split stateful logic with no direct template dependency. This project is small (5 Vue files, ~1000 lines total) and young, so establishing the convention now is cheap; it gets more expensive the more code lands on the current shape.
First target:
TemplateSection.vue's scroll-arrows clusterlist(element ref) +canScrollLeft/canScrollRight+updateArrows()+scrollByStep()+ theonMounted/onUnmountedwiring is a self-contained "scrollable region with arrow buttons" state machine — reusable, no dependency on other in-flight work, and the same region already has an open a11y gap (#59: noaria-labelidentifying it as a scrollable region) worth closing while in there.Check
@vueuse/corebefore hand-rolling this one. Verified directly against its shipped type declarations:useScroll(element)already returnsarrivedState.{left,right,top,bottom}(the inverse ofcanScrollLeft/canScrollRight) plus reactivex/y, and handles the listener attach/detach and resize-observation internally. The extraction should be a thin wrapper overuseScroll, not new hand-rolled logic — this is the same "reuse before you write" ladder AGENTS.md already applies to@nextcloud/*packages and@nextcloud/vuecomponents, extended to general-purpose Vue utilities.@vueuse/core@14.4.0's only peer dependency isvue@^3.5.0, which this app already satisfies.Other candidates identified, for later extractions (not in scope for the first PR)
OfficeOverview.vue: the create-from-template dialog cluster (showCreateDialog,newFileName,pendingCreator,pendingTemplate,creating,createError,createInput)OfficeOverview.vue: the view-mode toggle + persistence cluster (viewMode,toggleViewMode(),getOverviewGridView/setOverviewGridView)OfficeOverview.vue: the active-creator/routing-sync cluster landing via feat: add navigation actions for office #130 (activeCreator,routeCreatorId, the URL-syncwatch)Each is its own extraction, its own commit, done opportunistically the next time that region is touched — not bundled into one refactor PR.
Conventions to establish with the first extraction, not invent per-composable later
@vueuse/corefirst. Before hand-rolling a composable, check whether it already exists there (as withuseScrollabove). Prefer it the same way an existing@nextcloud/vuecomponent or design token is preferred over a hand-rolled equivalent.reactive()object. A composable that returnsreactive({...})breaks reactivity silently if the caller destructures it. Return individualrefs (ortoRefs()on an internal reactive object) so destructuring is always safe.onMounted/onUnmountedthrow outside an active component instance, so testing needs a small reusable helper — a trivial host component whosesetup()just returns the composable — living insrc/test-utils/, written once with the first such composable rather than reinvented slightly differently by the second and third.src/utils//src/components/today.src/composables/useScrollArrows.tslands withsrc/composables/useScrollArrows.spec.tsin the same commit — this is also where thesrc/composables/directory convention itself gets established.