Skip to content

Commit cc3c4e4

Browse files
committed
feat: add mobile sidebar trigger to AppHeader and update build process for CSS
1 parent 94eb4a4 commit cc3c4e4

5 files changed

Lines changed: 202 additions & 2 deletions

File tree

packages/app-shell/src/layout/AppHeader.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import { useLocation, useParams, Link, useNavigate } from 'react-router-dom';
2121
import {
22+
SidebarTrigger,
2223
Button,
2324
DropdownMenu,
2425
DropdownMenuTrigger,
@@ -244,6 +245,9 @@ export function AppHeader({
244245

245246
{isApp && (
246247
<>
248+
{/* Mobile sidebar trigger */}
249+
<SidebarTrigger className="md:hidden shrink-0 ml-1" />
250+
247251
{/* App dropdown */}
248252
{activeAppName && onAppChange ? (
249253
<>

packages/components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
},
2828
"scripts": {
2929
"test:compliance": "vitest run src/__tests__/compliance.test.tsx",
30-
"build": "vite build",
30+
"build": "vite build && node scripts/build-css.mjs",
3131
"prebuild": "pnpm --filter @object-ui/types build && pnpm --filter @object-ui/core build && pnpm --filter @object-ui/react build",
3232
"pretest": "pnpm run prebuild",
3333
"test": "vitest run",
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*
8+
* Builds `dist/index.css` from `src/index.css`.
9+
*
10+
* Why a standalone script (instead of `import './index.css'` in `src/index.ts`):
11+
* In a pnpm monorepo dev server, that import would inject a SECOND
12+
* Tailwind stylesheet (its `@source` only scans this package's src),
13+
* which would override responsive utilities (`md:hidden`, `!top-14`, …)
14+
* declared in sibling packages. See the comment in `src/index.ts`.
15+
*
16+
* This script runs only during `pnpm build` so consumers can still
17+
* `@import '@object-ui/components/style.css'` from their own CSS entry.
18+
*
19+
* The build pipeline:
20+
* 1. `vite build` extracts `import './sidebar-fixes.css'` from `src/index.ts`
21+
* and writes it to `dist/index.css` (~2 kB of plain CSS overrides).
22+
* 2. This script runs AFTER vite, reads the Vite-emitted file, then
23+
* prepends the full Tailwind compilation of `src/index.css`. Order
24+
* matters — Tailwind utilities first, sidebar overrides last so they
25+
* win on equal specificity.
26+
*/
27+
import { mkdir, readFile, writeFile, access } from 'node:fs/promises';
28+
import { dirname, resolve } from 'node:path';
29+
import { fileURLToPath } from 'node:url';
30+
import postcss from 'postcss';
31+
import tailwindPostcss from '@tailwindcss/postcss';
32+
33+
const here = dirname(fileURLToPath(import.meta.url));
34+
const root = resolve(here, '..');
35+
const input = resolve(root, 'src/index.css');
36+
const output = resolve(root, 'dist/index.css');
37+
38+
const css = await readFile(input, 'utf8');
39+
const result = await postcss([tailwindPostcss()]).process(css, {
40+
from: input,
41+
to: output,
42+
});
43+
44+
let viteEmittedOverrides = '';
45+
try {
46+
await access(output);
47+
viteEmittedOverrides = await readFile(output, 'utf8');
48+
} catch {
49+
// Vite did not emit dist/index.css (e.g. fresh build). That's fine — the
50+
// sidebar-fixes.css source will be missing from the published bundle, but
51+
// the same overrides also load at runtime from the JS-side
52+
// `import './sidebar-fixes.css'`. This branch is mainly defensive.
53+
}
54+
55+
const merged = viteEmittedOverrides
56+
? `${result.css}\n\n/* === sidebar-fixes.css (extracted by Vite) === */\n${viteEmittedOverrides}`
57+
: result.css;
58+
59+
await mkdir(dirname(output), { recursive: true });
60+
await writeFile(output, merged, 'utf8');
61+
if (result.map) await writeFile(`${output}.map`, result.map.toString(), 'utf8');
62+
63+
const sizeKb = (Buffer.byteLength(merged, 'utf8') / 1024).toFixed(2);
64+
console.log(`✓ built dist/index.css (${sizeKb} kB)`);
65+

packages/components/src/index.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,31 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
import './index.css';
9+
// NOTE: Do NOT `import './index.css'` here.
10+
//
11+
// `index.css` is a standalone Tailwind v4 entry (`@import 'tailwindcss'`).
12+
// In Vite library `build`, that import is extracted to `dist/index.css` and
13+
// dropped from the JS bundle automatically — consumers explicitly load it
14+
// via `@import '@object-ui/components/style.css'` (see README).
15+
//
16+
// In a pnpm monorepo dev server, however, the workspace package is resolved
17+
// to its `src/`, so the import would be evaluated and Vite would inject a
18+
// SECOND, partial Tailwind stylesheet (its `@source` only scans
19+
// `packages/components/src`, missing every other package's classes). That
20+
// second sheet is appended after the app's stylesheet, so any base utility
21+
// it redeclares (e.g. `.inline-flex`) overrides media-query variants from
22+
// the first sheet (e.g. `md:hidden`, `!top-14`) defined elsewhere — silently
23+
// breaking responsive utilities and overrides used by `@object-ui/app-shell`,
24+
// `@object-ui/auth`, etc.
25+
//
26+
// Apps must own the single Tailwind entrypoint and `@source`-scan every
27+
// workspace package they consume (see `apps/console-starter/src/index.css`).
28+
//
29+
// We DO import `sidebar-fixes.css` below: it contains plain CSS overrides
30+
// (no Tailwind directives) for shadcn sidebar utility classes that Tailwind
31+
// v4 cannot compile correctly. Plain CSS is safe to inject from a workspace
32+
// package because it doesn't trigger a second Tailwind build.
33+
import './sidebar-fixes.css';
1034

1135
// Register all ObjectUI renderers (side-effects)
1236
import './renderers';
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
* ObjectUI — Shadcn Sidebar compatibility overrides for Tailwind v4.
3+
*
4+
* These rules are plain CSS (no `@import 'tailwindcss'` / `@theme`), so they
5+
* can be safely imported from `src/index.ts` without re-injecting a second
6+
* Tailwind stylesheet (which would override responsive utilities like
7+
* `md:hidden`/`!top-14` declared in sibling packages — see comment in
8+
* `src/index.ts`).
9+
*
10+
* Background:
11+
* Shadcn's `sidebar.tsx` was authored for Tailwind v3 and uses arbitrary
12+
* value classes like `w-[--sidebar-width]` and
13+
* `group-data-[collapsible=icon]:w-[--sidebar-width-icon]`. Tailwind v4
14+
* accepts the selector but emits an empty rule (the bare `--xxx` is no
15+
* longer interpreted as `var(--xxx)` for arbitrary values), so the
16+
* sidebar collapses to its content width and overlays the main area.
17+
*
18+
* The rules below restore the intended behaviour by writing the equivalent
19+
* `width`, `opacity`, `display`, etc. declarations explicitly, scoped to
20+
* the data-attribute combinations the shadcn component actually toggles.
21+
*/
22+
23+
/* Sidebar explicit rules for Tailwind 4 compatibility */
24+
25+
/* Icon mode: collapsed state = icon width, expanded state = full width */
26+
.group[data-collapsible="icon"][data-state="collapsed"] .group-data-\[collapsible\=icon\]\:w-\[--sidebar-width-icon\] {
27+
width: var(--sidebar-width-icon);
28+
}
29+
30+
.group[data-collapsible="icon"][data-state="expanded"] .group-data-\[collapsible\=icon\]\:w-\[--sidebar-width-icon\] {
31+
width: var(--sidebar-width);
32+
}
33+
34+
.group[data-collapsible="icon"][data-state="collapsed"] .group-data-\[collapsible\=icon\]\:w-\[calc\(var\(--sidebar-width-icon\)_\+_theme\(spacing\.4\)\)\] {
35+
width: calc(var(--sidebar-width-icon) + 1rem);
36+
}
37+
38+
.group[data-collapsible="icon"][data-state="expanded"] .group-data-\[collapsible\=icon\]\:w-\[calc\(var\(--sidebar-width-icon\)_\+_theme\(spacing\.4\)\)\] {
39+
width: var(--sidebar-width);
40+
}
41+
42+
.group[data-collapsible="icon"][data-state="collapsed"] .group-data-\[collapsible\=icon\]\:w-\[calc\(var\(--sidebar-width-icon\)_\+_theme\(spacing\.4\)_\+2px\)\] {
43+
width: calc(var(--sidebar-width-icon) + 1rem + 2px);
44+
}
45+
46+
.group[data-collapsible="icon"][data-state="expanded"] .group-data-\[collapsible\=icon\]\:w-\[calc\(var\(--sidebar-width-icon\)_\+_theme\(spacing\.4\)_\+2px\)\] {
47+
width: var(--sidebar-width);
48+
}
49+
50+
/* Labels visibility - only hide when BOTH icon mode AND collapsed */
51+
.group[data-collapsible="icon"][data-state="collapsed"] .group-data-\[collapsible\=icon\]\:opacity-0 {
52+
opacity: 0;
53+
}
54+
55+
.group[data-collapsible="icon"][data-state="expanded"] .group-data-\[collapsible\=icon\]\:opacity-0 {
56+
opacity: 1;
57+
}
58+
59+
.group[data-collapsible="icon"][data-state="collapsed"] .group-data-\[collapsible\=icon\]\:-mt-8 {
60+
margin-top: -2rem;
61+
}
62+
63+
.group[data-collapsible="icon"][data-state="expanded"] .group-data-\[collapsible\=icon\]\:-mt-8 {
64+
margin-top: 0;
65+
}
66+
67+
.group[data-collapsible="icon"][data-state="collapsed"] .group-data-\[collapsible\=icon\]\:hidden {
68+
display: none;
69+
}
70+
71+
.group[data-collapsible="icon"][data-state="expanded"] .group-data-\[collapsible\=icon\]\:hidden {
72+
display: block;
73+
}
74+
75+
.group[data-collapsible="icon"][data-state="collapsed"] .group-data-\[collapsible\=icon\]\:overflow-hidden {
76+
overflow: hidden;
77+
}
78+
79+
.group[data-collapsible="icon"][data-state="expanded"] .group-data-\[collapsible\=icon\]\:overflow-hidden {
80+
overflow: auto;
81+
}
82+
83+
/* Menu button behavior - collapsed = icon only, expanded = full width */
84+
.group[data-collapsible="icon"][data-state="collapsed"] .sidebar-menu-button-icon-mode {
85+
width: 2rem !important;
86+
height: 2rem !important;
87+
padding: 0.5rem !important;
88+
}
89+
90+
.group[data-collapsible="icon"][data-state="expanded"] .sidebar-menu-button-icon-mode {
91+
width: 100%;
92+
height: auto;
93+
padding: 0.5rem;
94+
}
95+
96+
.group[data-collapsible="icon"][data-state="collapsed"] .sidebar-menu-button-icon-mode-lg {
97+
padding: 0 !important;
98+
}
99+
100+
.group[data-collapsible="icon"][data-state="expanded"] .sidebar-menu-button-icon-mode-lg {
101+
padding: 0.5rem;
102+
}
103+
104+
/* Offcanvas mode: always collapsed = 0 width */
105+
.group[data-collapsible="offcanvas"] .group-data-\[collapsible\=offcanvas\]\:w-0 {
106+
width: 0;
107+
}

0 commit comments

Comments
 (0)