Skip to content

Commit 3766733

Browse files
authored
Merge pull request #9 from objectstack-ai/copilot/enhance-sdk-rendering-engine
2 parents 9c72c9c + bb99ab3 commit 3766733

8 files changed

Lines changed: 800 additions & 44 deletions

File tree

components/renderers/DashboardViewRenderer.tsx

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from "react";
2-
import { View, Text, ScrollView, ActivityIndicator } from "react-native";
2+
import { View, Text, ScrollView, ActivityIndicator, useWindowDimensions } from "react-native";
33
import {
44
TrendingUp,
55
TrendingDown,
@@ -252,12 +252,20 @@ function renderWidget(
252252
/* Main Component */
253253
/* ------------------------------------------------------------------ */
254254

255+
/** Breakpoint (dp) at which the dashboard switches to 2-column grid */
256+
const GRID_BREAKPOINT = 600;
257+
/** Gap between grid cells in pixels */
258+
const GRID_GAP = 12;
259+
255260
export function DashboardViewRenderer({
256261
dashboard,
257262
widgetData = {},
258263
isLoading = false,
259264
onWidgetPress,
260265
}: DashboardViewRendererProps) {
266+
const { width: screenWidth } = useWindowDimensions();
267+
const numColumns = screenWidth >= GRID_BREAKPOINT ? 2 : 1;
268+
261269
if (isLoading) {
262270
return (
263271
<View className="flex-1 items-center justify-center">
@@ -278,10 +286,41 @@ export function DashboardViewRenderer({
278286
);
279287
}
280288

289+
/* ---- Responsive grid layout ---- */
290+
const contentPadding = 16;
291+
const availableWidth = screenWidth - contentPadding * 2;
292+
const columnWidth =
293+
numColumns === 1
294+
? availableWidth
295+
: (availableWidth - GRID_GAP) / numColumns;
296+
297+
/** Build rows of widgets respecting span hints */
298+
const rows: DashboardWidgetMeta[][] = [];
299+
let currentRow: DashboardWidgetMeta[] = [];
300+
let currentSpan = 0;
301+
302+
for (const widget of dashboard.widgets) {
303+
// A widget can declare span: 2 to take the full width
304+
const span = Math.min(widget.span ?? 1, numColumns);
305+
306+
if (currentSpan + span > numColumns && currentRow.length > 0) {
307+
rows.push(currentRow);
308+
currentRow = [];
309+
currentSpan = 0;
310+
}
311+
currentRow.push(widget);
312+
currentSpan += span;
313+
}
314+
if (currentRow.length > 0) rows.push(currentRow);
315+
281316
return (
282317
<ScrollView
283318
className="flex-1"
284-
contentContainerClassName="px-4 pb-8 pt-4"
319+
contentContainerStyle={{
320+
paddingHorizontal: contentPadding,
321+
paddingBottom: 32,
322+
paddingTop: 16,
323+
}}
285324
showsVerticalScrollIndicator={false}
286325
>
287326
{/* Dashboard header */}
@@ -296,10 +335,29 @@ export function DashboardViewRenderer({
296335
</View>
297336
)}
298337

299-
{/* Widgets */}
300-
{dashboard.widgets.map((widget) => (
301-
<View key={widget.name}>
302-
{renderWidget(widget, widgetData[widget.name])}
338+
{/* Widget grid */}
339+
{rows.map((row, rowIdx) => (
340+
<View
341+
key={`row-${rowIdx}`}
342+
style={{
343+
flexDirection: "row",
344+
marginBottom: GRID_GAP,
345+
gap: GRID_GAP,
346+
}}
347+
>
348+
{row.map((widget) => {
349+
const span = Math.min(widget.span ?? 1, numColumns);
350+
const widgetWidth =
351+
numColumns === 1
352+
? availableWidth
353+
: columnWidth * span + GRID_GAP * (span - 1);
354+
355+
return (
356+
<View key={widget.name} style={{ width: widgetWidth }}>
357+
{renderWidget(widget, widgetData[widget.name])}
358+
</View>
359+
);
360+
})}
303361
</View>
304362
))}
305363
</ScrollView>

components/renderers/DetailViewRenderer.tsx

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useMemo } from "react";
22
import { View, Text, ScrollView, Pressable, ActivityIndicator } from "react-native";
3-
import { Edit, Trash2, MoreHorizontal } from "lucide-react-native";
3+
import { Edit, Trash2, MoreHorizontal, ChevronLeft, ChevronRight } from "lucide-react-native";
44
import { cn } from "~/lib/utils";
55
import { FieldRenderer } from "./fields/FieldRenderer";
66
import type { FieldDefinition, FormViewMeta, FormSection, ActionMeta } from "./types";
@@ -34,6 +34,16 @@ export interface DetailViewRendererProps {
3434
relatedLists?: RelatedListConfig[];
3535
/** Handler when a related record is pressed */
3636
onRelatedRecordPress?: (objectName: string, record: Record<string, unknown>) => void;
37+
/** Navigate to the previous record */
38+
onPrevious?: () => void;
39+
/** Navigate to the next record */
40+
onNext?: () => void;
41+
/** Whether there is a previous record available */
42+
hasPrevious?: boolean;
43+
/** Whether there is a next record available */
44+
hasNext?: boolean;
45+
/** Label indicating position, e.g. "3 of 50" */
46+
positionLabel?: string;
3747
}
3848

3949
export interface RelatedListConfig {
@@ -136,6 +146,69 @@ function RelatedListSection({
136146
);
137147
}
138148

149+
/* ------------------------------------------------------------------ */
150+
/* Record Navigator */
151+
/* ------------------------------------------------------------------ */
152+
153+
function RecordNavigator({
154+
onPrevious,
155+
onNext,
156+
hasPrevious = false,
157+
hasNext = false,
158+
positionLabel,
159+
}: Pick<
160+
DetailViewRendererProps,
161+
"onPrevious" | "onNext" | "hasPrevious" | "hasNext" | "positionLabel"
162+
>) {
163+
if (!onPrevious && !onNext) return null;
164+
165+
return (
166+
<View className="flex-row items-center justify-between border-b border-border bg-card px-4 py-2">
167+
<Pressable
168+
className={cn(
169+
"flex-row items-center rounded-lg px-3 py-2",
170+
hasPrevious ? "active:bg-muted" : "opacity-40",
171+
)}
172+
onPress={hasPrevious ? onPrevious : undefined}
173+
disabled={!hasPrevious}
174+
>
175+
<ChevronLeft size={16} color={hasPrevious ? "#1e40af" : "#94a3b8"} />
176+
<Text
177+
className={cn(
178+
"ml-1 text-sm font-medium",
179+
hasPrevious ? "text-primary" : "text-muted-foreground",
180+
)}
181+
>
182+
Previous
183+
</Text>
184+
</Pressable>
185+
186+
{positionLabel && (
187+
<Text className="text-xs text-muted-foreground">{positionLabel}</Text>
188+
)}
189+
190+
<Pressable
191+
className={cn(
192+
"flex-row items-center rounded-lg px-3 py-2",
193+
hasNext ? "active:bg-muted" : "opacity-40",
194+
)}
195+
onPress={hasNext ? onNext : undefined}
196+
disabled={!hasNext}
197+
>
198+
<Text
199+
className={cn(
200+
"mr-1 text-sm font-medium",
201+
hasNext ? "text-primary" : "text-muted-foreground",
202+
)}
203+
>
204+
Next
205+
</Text>
206+
<ChevronRight size={16} color={hasNext ? "#1e40af" : "#94a3b8"} />
207+
</Pressable>
208+
</View>
209+
);
210+
}
211+
139212
/* ------------------------------------------------------------------ */
140213
/* Main Component */
141214
/* ------------------------------------------------------------------ */
@@ -153,6 +226,11 @@ export function DetailViewRenderer({
153226
actions,
154227
relatedLists,
155228
onRelatedRecordPress,
229+
onPrevious,
230+
onNext,
231+
hasPrevious,
232+
hasNext,
233+
positionLabel,
156234
}: DetailViewRendererProps) {
157235
/* ---- Build sections ---- */
158236
const sections: FormSection[] = useMemo(() => {
@@ -224,6 +302,15 @@ export function DetailViewRenderer({
224302
onAction={onAction}
225303
/>
226304

305+
{/* Record navigation (previous / next) */}
306+
<RecordNavigator
307+
onPrevious={onPrevious}
308+
onNext={onNext}
309+
hasPrevious={hasPrevious}
310+
hasNext={hasNext}
311+
positionLabel={positionLabel}
312+
/>
313+
227314
<ScrollView
228315
className="flex-1"
229316
contentContainerClassName="px-4 pb-8 pt-4"

0 commit comments

Comments
 (0)