diff --git a/.claude/hooks/protect-files.sh b/.claude/hooks/protect-files.sh index 662bf266..48c55579 100755 --- a/.claude/hooks/protect-files.sh +++ b/.claude/hooks/protect-files.sh @@ -36,8 +36,8 @@ if echo "$path" | grep -Eq '(^|/)apps/admin/'; then block "apps/admin/ was deleted in WS-3 and must not return" "Use apps/app/ (Organizer SPA, includes Platform Admin under /platform/*)" fi -# .claude/ tooling self-modification -if echo "$path" | grep -Eq '(^|/)\.claude/'; then +# .claude/ tooling self-modification (allow ephemeral agent worktrees under .claude/worktrees/) +if echo "$path" | grep -Eq '(^|/)\.claude/' && ! echo "$path" | grep -Eq '(^|/)\.claude/worktrees/'; then block "tooling self-modification — Bert reviews .claude/ changes by hand" "Open the file in an editor outside Claude Code, or ask Bert to authorize the change explicitly" fi diff --git a/CLAUDE.md b/CLAUDE.md index ea5bf617..e6445e33 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -403,7 +403,8 @@ Rules: 3. One commit per logical unit of work (one feature, one bugfix, one refactor) 4. Never bundle unrelated changes in a single commit 5. Never commit with failing tests -6. Do NOT push automatically — only commit locally. The developer will push manually. +6. Push, open PRs, and merge are AUTHORISED for Claude (granted by Bert + 2026-06-03), gated on a green merge gate — see "Push & Merge Authority". Commit message format: ``` @@ -424,3 +425,26 @@ Examples: - `feat: person tags system with org-level skills and sync endpoint` - `fix: auth race condition on page refresh` - `docs: update SCHEMA.md with person_identity_matches table` + +### Push & Merge Authority + +Claude may push feature branches, open Gitea PRs, and merge them **without +a separate human approval click**, provided ALL of the following hold: + +1. **Green merge gate** — crewli-reviewer `REVIEW VERDICT: PASS` (no MUST + FIX), all applicable tests passing, lint + typecheck clean, and for + backend changes Larastan clean + the multi-tenancy 403 test present. + The gate — not a human click — is the safety mechanism. If any signal + is red, Claude does NOT merge; it returns the work to the implementer. +2. **Never push directly to `main`.** Integrate only via a `--no-ff` merge + of a reviewed feature branch (a merged Gitea PR with `merge_style: merge`, + or a local `--no-ff` merge then push). +3. **Never force-push** to `main` or any shared branch. +4. **Post-merge:** verify the merge landed on `main`, then delete the + feature branch locally and remotely (per the rule above). +5. Still **present the merge gate** for visibility before self-merging, but + Claude may proceed to merge once the gate is green rather than blocking + on an explicit `merge` reply. + +This supersedes the earlier "developer pushes manually" rule and the +build-module skill's HUMAN GATE 2 (CLAUDE.md takes precedence over skills). diff --git a/apps/app/src/components-v2/layout/AppSidebar.stories.ts b/apps/app/src/components-v2/layout/AppSidebar.stories.ts index 1800e2e1..f4eb9e83 100644 --- a/apps/app/src/components-v2/layout/AppSidebar.stories.ts +++ b/apps/app/src/components-v2/layout/AppSidebar.stories.ts @@ -68,3 +68,52 @@ export const Collapsed: Story = { `, }), } + +/** + * Mobile (< lg) drawer, open — MOBILE-SHELL-PARITY visual verification target. + * + * Verify against the desktop sidebar: + * - defect 2: a SINGLE close control (X, "Sluit menu") at the top-right of + * the brand row, NOT overlapping the logo. PrimeVue's default header X is + * force-hidden (`header: '!hidden'`), so there must be no second X. + * - defect 1: the brand row renders EXPANDED (logo + "Crewli" wordmark + + * Beta pill) regardless of the desktop collapse state. + * - defect 3: the WorkspaceSwitcher is anchored at the BOTTOM of the drawer, + * fully visible (not clipped). + * + * The Drawer mounts only when isMobile (viewport < 1024px). This repo's + * Storybook has no viewport addon, so narrow the browser window to < 1024px + * (≈ 375px) to render the drawer. The parameters.viewport hint below takes + * effect if @storybook/addon-viewport is later added to .storybook/main.ts. + */ +export const MobileDrawer: Story = { + parameters: { + viewport: { + viewports: { + mobile375: { name: 'Mobile 375', styles: { width: '375px', height: '720px' } }, + }, + defaultViewport: 'mobile375', + }, + }, + decorators: [ + withPinia(() => { + const auth = useAuthStore() + + auth.user = userFixture + auth.organisations = [orgA] + + const shellUi = useShellUiStore() + + shellUi.sidebarCollapsed = false + shellUi.mobileOpen = true + }), + ], + render: () => ({ + components: { AppSidebar }, + template: ` +
+ +
+ `, + }), +} diff --git a/apps/app/src/components-v2/layout/AppSidebar.vue b/apps/app/src/components-v2/layout/AppSidebar.vue index 03a7d061..36cdddcc 100644 --- a/apps/app/src/components-v2/layout/AppSidebar.vue +++ b/apps/app/src/components-v2/layout/AppSidebar.vue @@ -114,6 +114,43 @@ const mobileVisible = computed({ The children are intentionally repeated (not factored into a slot or render-fn) — the 3-component block is short and the duplication is preferable to the indirection of a dynamic component or slot wiring. + + Close control (MOBILE-SHELL-PARITY defect 2): the mobile close affordance + lives in SidebarHeader's brand row, which renders an X ("Close menu") on + mobile — a single, non-overlapping control in the same top-right slot the + desktop sidebar uses for its collapse chevron. + + PrimeVue's OWN default header close-X is suppressed two ways, by design: + - `:show-close-icon="false"` stops PrimeVue mounting the default close + button at all. This is required for a11y, not just visuals: PrimeVue's + Drawer.focus() (no [autofocus] in our slots) falls back to focusing + `this.closeButton`; if that button is merely display:none (the earlier + `header: 'hidden'` approach), focus() is a no-op and keyboard focus is + stranded behind the overlay. With showCloseIcon=false the button is + never created, focus() no-ops cleanly, and v-focustrap focuses the + first focusable element — the visible brand-row X. + - `header: '!hidden'` then collapses the now-empty header band so it adds + no space above the content. The important variant beats PrimeVue's base + `.p-drawer-header` display (plain `hidden` lost to it in stylesheet + order — the original leak that let the default X overlap the brand row). + No duplicate control, no overlap, no stranded focus. + + Bottom anchoring (MOBILE-SHELL-PARITY defect 3 — WorkspaceSwitcher): + the content pt is a full-height flex COLUMN (`flex flex-col h-full + min-h-0`). PrimeVue's @primeuix base styles already give + `.p-drawer-left .p-drawer-content { height: 100% }` + `flex-grow: 1`, + and `.p-drawer-left .p-drawer { height: 100% }` inside the `height:100%` + mask — so the content fills the panel's full viewport height. With the + flex column, SidebarNav (`flex-1 min-h-0 overflow-y-auto`) claims the + slack and WorkspaceSwitcher (`flex-shrink-0`) anchors to the true bottom, + exactly as the desktop