docs: add agent rules for dev server management and browser verification
- Section 6: Never start dev servers from the agent (prevents orphan processes and broken tmux panes) - Section 7: Check tmux panes for compile errors (renumbered) - Section 8: Verify compiled output in browser before committing (catches squint's silent empty-file failures)
This commit is contained in:
@@ -369,6 +369,89 @@
|
||||
[:span {:class "sidebar-collapsible-chevron" :aria-hidden "true"}]]
|
||||
(into [:div {:class "sidebar-collapsible-content"}] children)])))
|
||||
|
||||
;; ── Sidebar Mobile Toggle ────────────────────────────────────────────
|
||||
|
||||
(defn sidebar-mobile-toggle-class-list [_opts] ["sidebar-mobile-toggle"])
|
||||
(defn sidebar-mobile-toggle-classes [opts] (str/join " " (sidebar-mobile-toggle-class-list opts)))
|
||||
|
||||
(defn sidebar-mobile-toggle
|
||||
"Render a hamburger/close toggle button for mobile sidebar.
|
||||
Hidden on desktop via CSS. On click, toggles `data-sidebar-open`
|
||||
on the nearest `.sidebar-layout` ancestor.
|
||||
|
||||
Props:
|
||||
:on-click - click handler (cljs/squint only)
|
||||
:class - additional CSS classes
|
||||
:attrs - additional HTML attributes"
|
||||
[{:keys [on-click class attrs] :as _props}]
|
||||
#?(:squint
|
||||
(let [classes (cond-> (sidebar-mobile-toggle-classes {}) class (str " " class))
|
||||
base-attrs (cond-> (merge {:class classes
|
||||
:type "button"
|
||||
:aria-label "Toggle sidebar"} attrs)
|
||||
on-click (assoc :on-click on-click))]
|
||||
[:button base-attrs
|
||||
[:span {:class "sidebar-toggle-icon-open" :aria-hidden "true"}
|
||||
(icon/icon {:icon-name :menu :size :sm})]
|
||||
[:span {:class "sidebar-toggle-icon-close" :aria-hidden "true"}
|
||||
(icon/icon {:icon-name :x :size :sm})]])
|
||||
|
||||
:cljs
|
||||
(let [cls (sidebar-mobile-toggle-class-list {})
|
||||
classes (cond-> cls class (conj class))
|
||||
base-attrs (cond-> (merge {:class classes
|
||||
:type "button"
|
||||
:aria-label "Toggle sidebar"} attrs)
|
||||
on-click (assoc-in [:on :click] on-click))]
|
||||
[:button base-attrs
|
||||
[:span {:class ["sidebar-toggle-icon-open"] :aria-hidden "true"}
|
||||
(icon/icon {:icon-name :menu :size :sm})]
|
||||
[:span {:class ["sidebar-toggle-icon-close"] :aria-hidden "true"}
|
||||
(icon/icon {:icon-name :x :size :sm})]])
|
||||
|
||||
:clj
|
||||
(let [classes (cond-> (sidebar-mobile-toggle-classes {}) class (str " " class))
|
||||
base-attrs (merge {:class classes
|
||||
:type "button"
|
||||
:aria-label "Toggle sidebar"
|
||||
:onclick "this.closest('.sidebar-layout').toggleAttribute('data-sidebar-open')"} attrs)]
|
||||
[:button base-attrs
|
||||
[:span {:class "sidebar-toggle-icon-open" :aria-hidden "true"}
|
||||
(icon/icon {:icon-name :menu :size :sm})]
|
||||
[:span {:class "sidebar-toggle-icon-close" :aria-hidden "true"}
|
||||
(icon/icon {:icon-name :x :size :sm})]])))
|
||||
|
||||
;; ── Sidebar Overlay ─────────────────────────────────────────────────
|
||||
|
||||
(defn sidebar-overlay
|
||||
"Render the backdrop overlay for mobile sidebar.
|
||||
Clicking it closes the sidebar. Place inside sidebar-layout,
|
||||
as a sibling of the sidebar.
|
||||
|
||||
Props:
|
||||
:on-click - click handler (cljs/squint only)
|
||||
:class - additional CSS classes
|
||||
:attrs - additional HTML attributes"
|
||||
[{:keys [on-click class attrs] :as _props}]
|
||||
#?(:squint
|
||||
(let [classes (cond-> "sidebar-overlay" class (str " " class))
|
||||
base-attrs (cond-> (merge {:class classes :aria-hidden "true"} attrs)
|
||||
on-click (assoc :on-click on-click))]
|
||||
[:div base-attrs])
|
||||
|
||||
:cljs
|
||||
(let [classes (cond-> ["sidebar-overlay"] class (conj class))
|
||||
base-attrs (cond-> (merge {:class classes :aria-hidden "true"} attrs)
|
||||
on-click (assoc-in [:on :click] on-click))]
|
||||
[:div base-attrs])
|
||||
|
||||
:clj
|
||||
(let [classes (cond-> "sidebar-overlay" class (str " " class))
|
||||
base-attrs (merge {:class classes
|
||||
:aria-hidden "true"
|
||||
:onclick "this.closest('.sidebar-layout').removeAttribute('data-sidebar-open')"} attrs)]
|
||||
[:div base-attrs])))
|
||||
|
||||
;; ── Sidebar Separator ───────────────────────────────────────────────
|
||||
|
||||
(defn sidebar-separator
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-layout-main {
|
||||
@@ -144,7 +145,9 @@
|
||||
.sidebar-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: var(--size-2) var(--size-3);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
@@ -196,6 +199,11 @@
|
||||
|
||||
/* ── Sidebar Menu Item ─────────────────────────────────────────── */
|
||||
|
||||
.sidebar-menu > li {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -211,8 +219,12 @@
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
a.sidebar-menu-item {
|
||||
@@ -387,3 +399,97 @@ a.sidebar-menu-item {
|
||||
border-left: var(--border-0);
|
||||
padding-left: var(--size-2);
|
||||
}
|
||||
|
||||
/* ── Sidebar Mobile Toggle ─────────────────────────────────────── */
|
||||
|
||||
.sidebar-mobile-toggle {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--size-10);
|
||||
height: var(--size-10);
|
||||
padding: 0;
|
||||
border: var(--border-0);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-0);
|
||||
color: var(--fg-1);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: background-color 150ms ease, color 150ms ease;
|
||||
}
|
||||
|
||||
.sidebar-mobile-toggle:hover {
|
||||
background: var(--bg-1);
|
||||
color: var(--fg-0);
|
||||
}
|
||||
|
||||
.sidebar-mobile-toggle:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Show open icon by default, hide close icon */
|
||||
.sidebar-mobile-toggle .sidebar-toggle-icon-open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sidebar-mobile-toggle .sidebar-toggle-icon-close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── Sidebar Overlay ───────────────────────────────────────────── */
|
||||
|
||||
.sidebar-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 40;
|
||||
opacity: 0;
|
||||
transition: opacity 200ms ease;
|
||||
}
|
||||
|
||||
/* ── Mobile Responsive ─────────────────────────────────────────── */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar-mobile-toggle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sidebar-layout > .sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 50;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 250ms ease;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sidebar-layout > .sidebar-overlay {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Open state — toggled via data-sidebar-open on .sidebar-layout */
|
||||
.sidebar-layout[data-sidebar-open] > .sidebar {
|
||||
transform: translateX(0);
|
||||
box-shadow: var(--shadow-3);
|
||||
}
|
||||
|
||||
.sidebar-layout[data-sidebar-open] > .sidebar-overlay {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Swap icons when open */
|
||||
.sidebar-layout[data-sidebar-open] .sidebar-mobile-toggle .sidebar-toggle-icon-open {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-layout[data-sidebar-open] .sidebar-mobile-toggle .sidebar-toggle-icon-close {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user