Skip to content

Commit b373feb

Browse files
committed
Sync: auto-height mode, maxHeight prop, visibility metadata
1 parent 93fd369 commit b373feb

14 files changed

Lines changed: 320 additions & 28 deletions

File tree

BROWSER.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function App() {
2828
{ Name: 'Bob', Age: 25, Active: false },
2929
]}
3030
onChange={(rows) => console.log('Updated:', rows)}
31-
height={400}
31+
height="auto"
3232
/>
3333
);
3434
}
@@ -75,11 +75,12 @@ function App() {
7575

7676
| Prop | Type | Default | Description |
7777
|---|---|---|---|
78-
| `height` | `number \| string` | `'100%'` | Container height (number = pixels) |
78+
| `height` | `'auto' \| number \| string` | `'100%'` | `'auto'` fits content, number = fixed pixels (ghost rows on), string = CSS |
79+
| `maxHeight` | `number` || Max height in px. Caps `'auto'` growth or fluid containers |
7980
| `rowHeight` | `RowHeightOption` | `'medium'` | `'short'` \| `'medium'` \| `'tall'` \| `'extra-tall'` \| `'fit'` |
8081
| `showRowNumbers` | `boolean` | `false` | Show row number column |
8182
| `compactMode` | `boolean` | `false` | Denser layout — smaller fonts, tighter padding |
82-
| `ghostGrid` | `boolean \| { rows?: number, columns?: number }` || Show faint placeholder cells to fill the viewport |
83+
| `ghostGrid` | `boolean \| { rows?: number, columns?: number }` || Show faint placeholder cells to fill the viewport. Defaults to `true` when `height` is a number |
8384

8485
### Permissions
8586

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ function App() {
3333
{ Name: 'Beta', Score: 7.2, Status: 'done', Due: '2026-04-01', Approved: false },
3434
]}
3535
onChange={(rows) => console.log('Updated:', rows)}
36-
height={500}
36+
height="auto"
3737
/>
3838
);
3939
}

docs/guides/getting-started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function App() {
2828
{ Name: 'Bob', Age: 25, Active: false },
2929
]}
3030
onChange={(rows) => console.log('Updated:', rows)}
31-
height={400}
31+
height="auto"
3232
/>
3333
);
3434
}

examples/browser-standalone/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
.tab:hover { background: #f3f4f6; color: #374151; }
1616
.tab-active { background: #111827; color: white; border-color: #111827; }
1717
.tab-active:hover { background: #1f2937; }
18+
.tab-private { border-color: #dc2626 !important; color: #dc2626 !important; }
19+
.tab-private.tab-active { background: #dc2626 !important; border-color: #dc2626 !important; color: white !important; }
1820
.controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
1921
.tab-panel { }
2022
.example { background: white; border-radius: 8px; border: 1px solid #e5e7eb; padding: 24px; margin-bottom: 32px; }

examples/browser-standalone/main.tsx

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -425,16 +425,153 @@ function EmptyTableExample({ locale, language, ghostGrid, compactMode }: { local
425425
);
426426
}
427427

428-
const TABS = [
428+
// ---------------------------------------------------------------------------
429+
// Example 7: Height modes comparison
430+
// ---------------------------------------------------------------------------
431+
432+
const HEIGHT_COLUMNS = [
433+
{ id: 'Organization' },
434+
{ id: 'Slug' },
435+
{ id: 'Status', type: 'SingleSelect' as const, options: { options: [
436+
{ value: 'Synced', label: 'Synced', color: '#dcfce7' },
437+
{ value: 'Pending', label: 'Pending', color: '#fef3c7' },
438+
{ value: 'Error', label: 'Error', color: '#fecaca' },
439+
]}},
440+
{ id: 'Filter' },
441+
{ id: 'Link', type: 'URL' as const },
442+
];
443+
444+
const HEIGHT_ROWS_3 = [
445+
{ Organization: 'test', Slug: 'test', Status: 'Synced', Filter: "filter(org_slug == 'test')", Link: '/test/db/news-articles' },
446+
{ Organization: 'testorg', Slug: 'testorg', Status: 'Synced', Filter: "filter(org_slug == 'testorg')", Link: '/testorg/db/news-articles' },
447+
{ Organization: 'acme', Slug: 'acme', Status: 'Pending', Filter: "filter(org_slug == 'acme')", Link: '/acme/db/news-articles' },
448+
];
449+
450+
const HEIGHT_ROWS_1 = [HEIGHT_ROWS_3[0]];
451+
452+
type HeightMode = 'auto' | 'auto-capped' | 'fixed-400' | 'fixed-400-no-ghost' | 'fill';
453+
454+
const HEIGHT_MODES: { id: HeightMode; label: string; code: string }[] = [
455+
{ id: 'auto', label: 'auto', code: 'height="auto"' },
456+
{ id: 'auto-capped', label: 'auto + maxHeight', code: 'height="auto" maxHeight={300}' },
457+
{ id: 'fixed-400', label: '400px (ghost on)', code: 'height={400}' },
458+
{ id: 'fixed-400-no-ghost', label: '400px (ghost off)', code: 'height={400} ghostGrid={false}' },
459+
{ id: 'fill', label: '100% (fill parent)', code: 'height="100%"' },
460+
];
461+
462+
function HeightModeTable({ rows, mode, locale, language, compactMode }: {
463+
rows: Array<Record<string, Value>>;
464+
mode: HeightMode;
465+
locale: string;
466+
language: string;
467+
compactMode: boolean;
468+
}) {
469+
const heightProps = (() => {
470+
switch (mode) {
471+
case 'auto': return { height: 'auto' as const };
472+
case 'auto-capped': return { height: 'auto' as const, maxHeight: 300 };
473+
case 'fixed-400': return { height: 400 };
474+
case 'fixed-400-no-ghost': return { height: 400, ghostGrid: false as const };
475+
case 'fill': return { height: '100%' };
476+
}
477+
})();
478+
479+
const needsContainer = mode === 'fill';
480+
481+
const table = (
482+
<MonkeyTable
483+
columns={HEIGHT_COLUMNS}
484+
rows={rows}
485+
editable={false}
486+
showToolbar={false}
487+
compactMode={compactMode}
488+
locale={locale}
489+
language={language}
490+
{...heightProps}
491+
/>
492+
);
493+
494+
if (needsContainer) {
495+
return <div style={{ height: '300px', border: '1px dashed #d1d5db', borderRadius: '6px' }}>{table}</div>;
496+
}
497+
return table;
498+
}
499+
500+
function HeightModesExample({ locale, language, compactMode }: { locale: string; language: string; compactMode: boolean }) {
501+
const [mode, setMode] = useState<HeightMode>('auto');
502+
503+
const currentMode = HEIGHT_MODES.find((m) => m.id === mode)!;
504+
505+
return (
506+
<div className="example">
507+
<h2>Height Modes</h2>
508+
<div className="desc" style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap', marginBottom: '12px' }}>
509+
<span>Compare how different <code>height</code> modes handle 0, 1, and 3 rows.</span>
510+
<div style={{ display: 'flex', gap: '4px' }}>
511+
{HEIGHT_MODES.map((m) => (
512+
<button
513+
key={m.id}
514+
onClick={() => setMode(m.id)}
515+
style={{
516+
padding: '3px 10px',
517+
fontSize: '12px',
518+
fontWeight: mode === m.id ? 600 : 400,
519+
borderRadius: '4px',
520+
border: '1px solid',
521+
borderColor: mode === m.id ? '#111827' : '#d1d5db',
522+
background: mode === m.id ? '#111827' : 'white',
523+
color: mode === m.id ? 'white' : '#374151',
524+
cursor: 'pointer',
525+
}}
526+
>
527+
{m.label}
528+
</button>
529+
))}
530+
</div>
531+
</div>
532+
<div style={{ marginBottom: '16px', padding: '6px 12px', background: '#f9fafb', borderRadius: '4px', fontFamily: 'monospace', fontSize: '13px', color: '#6b7280' }}>
533+
{'<MonkeyTable '}{currentMode.code}{' />'}
534+
{mode === 'fill' && <span style={{ color: '#9ca3af' }}> (inside a 300px container)</span>}
535+
</div>
536+
537+
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
538+
{([
539+
{ label: '0 rows (empty)', rows: [] as Array<Record<string, Value>> },
540+
{ label: '1 row', rows: HEIGHT_ROWS_1 },
541+
{ label: '3 rows', rows: HEIGHT_ROWS_3 },
542+
]).map((scenario) => (
543+
<div key={scenario.label}>
544+
<div style={{ fontSize: '13px', fontWeight: 600, color: '#374151', marginBottom: '6px' }}>
545+
{scenario.label}
546+
</div>
547+
<div style={{ border: '1px solid #e5e7eb', borderRadius: '6px', overflow: 'hidden' }}>
548+
<HeightModeTable
549+
rows={scenario.rows}
550+
mode={mode}
551+
locale={locale}
552+
language={language}
553+
compactMode={compactMode}
554+
/>
555+
</div>
556+
</div>
557+
))}
558+
</div>
559+
</div>
560+
);
561+
}
562+
563+
564+
const TABS: Array<{ id: string; label: string; private?: boolean }> = [
429565
{ id: 'editable', label: 'Editable' },
566+
{ id: 'height', label: 'Height Modes' },
430567
{ id: 'columns', label: 'Column Options' },
431568
{ id: 'empty', label: 'Empty Table' },
432569
{ id: 'readonly', label: 'Read-Only' },
433570
{ id: 'large', label: 'Large Dataset' },
434571
{ id: 'pagination', label: 'Pagination' },
435-
] as const;
572+
];
436573

437-
type TabId = typeof TABS[number]['id'];
574+
type TabId = string;
438575

439576
function App() {
440577
const [tab, setTab] = useState<TabId>('editable');
@@ -467,7 +604,7 @@ function App() {
467604
{TABS.map((t) => (
468605
<button
469606
key={t.id}
470-
className={`tab ${tab === t.id ? 'tab-active' : ''}`}
607+
className={`tab ${tab === t.id ? 'tab-active' : ''}${t.private ? ' tab-private' : ''}`}
471608
onClick={() => setTab(t.id)}
472609
>
473610
{t.label}
@@ -550,6 +687,10 @@ function App() {
550687
</div>
551688
)}
552689

690+
{tab === 'height' && (
691+
<HeightModesExample locale={locale} language={language} compactMode={compactMode} />
692+
)}
693+
553694
{tab === 'columns' && (
554695
<div className="example" style={{ display: 'flex', flexDirection: 'column', minHeight: 'calc(100vh - 200px)' }}>
555696
<h2>Column Options</h2>
@@ -602,6 +743,7 @@ function App() {
602743
{tab === 'pagination' && (
603744
<PaginationExample locale={locale} language={language} />
604745
)}
746+
605747
</div>
606748
</div>
607749
);

src/browser/MonkeyTable.tsx

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,14 @@ export interface MonkeyTableProps {
111111
/** Show faint ghost rows/columns to fill the viewport, spreadsheet-style.
112112
* `true` auto-fills the visible area. Pass `{ rows, columns }` for explicit counts. */
113113
ghostGrid?: boolean | { rows?: number; columns?: number };
114-
/** CSS height for the container (number = pixels, string = CSS value) */
115-
height?: string | number;
114+
/** Container height.
115+
* - `'auto'`: fits content exactly — no scrollbar for small tables. Pair with `maxHeight` to cap.
116+
* - `number`: fixed pixel height. `ghostGrid` defaults to `true` to fill empty space.
117+
* - CSS string (`'100%'`, `'50vh'`): fills parent (default `'100%'`). */
118+
height?: 'auto' | number | string;
119+
/** Maximum height in pixels. Useful with `height="auto"` to cap growth, or with
120+
* `height="100%"` to limit fluid containers. Ignored when `height` is a fixed number. */
121+
maxHeight?: number;
116122
/** Row height preset (default: 'medium') */
117123
rowHeight?: RowHeightOption;
118124
/** Show row number column (default: false) */
@@ -190,6 +196,14 @@ export interface MonkeyTableProps {
190196
const BASE_ID = 'base-1';
191197
const TABLE_ID = 'table-1';
192198

199+
// Chrome heights for auto-height calculation (must match Grid/TableView layout)
200+
const AUTO_HEIGHT_HEADER = 44;
201+
const AUTO_HEIGHT_TOOLBAR = 40;
202+
const AUTO_HEIGHT_FOOTER = 37;
203+
const AUTO_HEIGHT_CHROME = 2;
204+
const AUTO_ROW_HEIGHTS: Record<string, number> = { short: 32, medium: 44, tall: 64, 'extra-tall': 88 };
205+
const AUTO_COMPACT_ROW_HEIGHTS: Record<string, number> = { short: 24, medium: 28, tall: 40, 'extra-tall': 60 };
206+
193207
export function MonkeyTable({
194208
// Data
195209
columns,
@@ -214,6 +228,7 @@ export function MonkeyTable({
214228
// Layout
215229
ghostGrid,
216230
height = '100%',
231+
maxHeight,
217232
rowHeight,
218233
showRowNumbers,
219234
compactMode,
@@ -635,8 +650,33 @@ export function MonkeyTable({
635650
);
636651
}
637652

653+
// ── Height mode resolution ───────────────────────────────────────────
654+
const isAutoHeight = height === 'auto';
655+
const isFixedHeight = typeof height === 'number';
656+
657+
let resolvedHeight: string | number = height;
658+
if (isAutoHeight) {
659+
const rowHeightKey = rowHeight ?? 'medium';
660+
const cellH = rowHeightKey === 'fit'
661+
? (compactMode ? 28 : 44)
662+
: (compactMode ? AUTO_COMPACT_ROW_HEIGHTS : AUTO_ROW_HEIGHTS)[rowHeightKey] ?? 44;
663+
const rowCount = inputRows?.length ?? 0;
664+
const computed = AUTO_HEIGHT_HEADER
665+
+ (showToolbar !== false ? AUTO_HEIGHT_TOOLBAR : 0)
666+
+ rowCount * cellH
667+
+ AUTO_HEIGHT_FOOTER
668+
+ AUTO_HEIGHT_CHROME;
669+
resolvedHeight = maxHeight ? Math.min(computed, maxHeight) : computed;
670+
}
671+
672+
// Ghost grid: default to true for fixed-pixel heights so empty space is filled
673+
const resolvedGhostGrid = ghostGrid !== undefined
674+
? ghostGrid
675+
: isFixedHeight ? true : undefined;
676+
638677
const containerStyle: React.CSSProperties = {
639-
height: typeof height === 'number' ? `${height}px` : height,
678+
height: typeof resolvedHeight === 'number' ? `${resolvedHeight}px` : resolvedHeight,
679+
...(maxHeight && !isFixedHeight && !isAutoHeight && { maxHeight: `${maxHeight}px` }),
640680
display: 'flex',
641681
flexDirection: 'column',
642682
overflow: 'hidden',
@@ -671,7 +711,7 @@ export function MonkeyTable({
671711
page={page}
672712
pageSize={pageSize}
673713
onPageChange={onPageChange}
674-
ghostGrid={ghostGrid}
714+
ghostGrid={resolvedGhostGrid}
675715
paginationMode={paginationMode}
676716
paginationLoading={paginationLoading}
677717
/>

src/browser/embed/EmbedMonkeyTable.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ export function EmbedMonkeyTable({
209209
rows={allRows}
210210
editable={false}
211211
height={config.height ?? '100%'}
212+
maxHeight={config.maxHeight}
212213
rowHeight={config.rowHeight}
213214
showRowNumbers={config.rowNumbers}
214215
compactMode={config.compact}

src/browser/embed/TableViewConfig.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ export interface TableViewConfig {
3737
rowNumbers?: boolean;
3838
/** Show ghost grid to fill viewport */
3939
ghostGrid?: boolean;
40-
/** Container height in pixels (omit for auto/100%) */
41-
height?: number;
40+
/** Container height: 'auto' to fit content, or pixels. Omit for 100%. */
41+
height?: 'auto' | number;
42+
/** Maximum height in pixels (useful with height='auto') */
43+
maxHeight?: number;
4244

4345
// ── Toolbar & chrome ─────────────────────────────────────────────────────
4446
/** Show the toolbar (default: true for pages, false for embeds) */

src/browser/embed/configParams.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
* compact=true
1313
* rowNumbers=true
1414
* ghostGrid=true
15-
* height=500
15+
* height=500 (or height=auto)
16+
* maxHeight=600
1617
* toolbar=false
1718
* showSearch=false
1819
* showFilters=false
@@ -59,6 +60,7 @@ export function configToParams(config: TableViewConfig): URLSearchParams {
5960
if (config.rowNumbers !== undefined) p.set('rowNumbers', String(config.rowNumbers));
6061
if (config.ghostGrid !== undefined) p.set('ghostGrid', String(config.ghostGrid));
6162
if (config.height) p.set('height', String(config.height));
63+
if (config.maxHeight) p.set('maxHeight', String(config.maxHeight));
6264

6365
if (config.toolbar !== undefined) p.set('toolbar', String(config.toolbar));
6466
if (config.showSearch !== undefined) p.set('showSearch', String(config.showSearch));
@@ -124,8 +126,18 @@ export function paramsToConfig(params: URLSearchParams): TableViewConfig {
124126

125127
const height = params.get('height');
126128
if (height) {
127-
const n = parseInt(height, 10);
128-
if (n > 0) config.height = n;
129+
if (height === 'auto') {
130+
config.height = 'auto';
131+
} else {
132+
const n = parseInt(height, 10);
133+
if (n > 0) config.height = n;
134+
}
135+
}
136+
137+
const maxHeight = params.get('maxHeight');
138+
if (maxHeight) {
139+
const n = parseInt(maxHeight, 10);
140+
if (n > 0) config.maxHeight = n;
129141
}
130142

131143
const locale = params.get('locale');

0 commit comments

Comments
 (0)