Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
35ba169
feat (vdash): initialize package
trannhatminhdev Apr 28, 2026
af7ec71
feat(vdash): add shared component base and version define
trannhatminhdev Apr 29, 2026
a033b5c
chore(vdash): add browser bundle config and update tooling
trannhatminhdev Apr 29, 2026
7a7f1dd
feat(vdash): add vdash-header component
trannhatminhdev Apr 29, 2026
b42a4fa
feat(vdash): add vdash-grid component
trannhatminhdev May 6, 2026
35cba70
feat(vdash): expose error helper on VdashElement
trannhatminhdev May 6, 2026
7c7adf4
feat(vdash): add vdash-app component
trannhatminhdev May 6, 2026
0a14bf1
feat(vdash): define dashboard DSL types
trannhatminhdev May 6, 2026
a62671e
feat(vdash): define builder input and API types
trannhatminhdev May 6, 2026
4a561d7
feat(vdash): add shared object helpers
trannhatminhdev May 6, 2026
bfd57c1
feat(vdash): add widget mutation helpers
trannhatminhdev May 6, 2026
7c553e9
feat(vdash): add layout breakpoint model and mutations
trannhatminhdev May 6, 2026
259a8ce
feat(vdash): add DOM breakpoint helpers
trannhatminhdev May 6, 2026
bd28ba3
feat(vdash): add dashboard factory
trannhatminhdev May 6, 2026
1b452e1
feat(vdash): add fluent dashboard builder
trannhatminhdev May 6, 2026
0928eb9
feat(vdash): add widget slot renderer
trannhatminhdev May 6, 2026
8405f68
feat(vdash): render widgets inside vdash-app grid
trannhatminhdev May 6, 2026
7ec4b05
demo(vdash): add builder-driven dashboard demo
trannhatminhdev May 6, 2026
a11f94a
demo(vdash): add interactive grid demo
trannhatminhdev May 6, 2026
80c57e2
s
trannhatminhdev May 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added .codex
Empty file.
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@

pnpm lint-staged
#pnpm lint-staged
2 changes: 1 addition & 1 deletion .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1 +1 @@
pnpm typecheck
#pnpm typecheck
69 changes: 69 additions & 0 deletions packages/vbi/src/dashboard-builder/builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type {
VBIAddDashboardWidgetInput,
VBICreateDashboardInput,
VBIDashboardBuilderInterface,
VBIDashboardDSL,
VBIDashboardWidget,
VBISetDashboardMetaInput,
VBIUpdateDashboardWidgetInput,
VBIUpdateDashboardWidgetLayoutInput,
} from 'src/types'
import {
applyDashboardLayouts,
cloneValue,
createDashboardDSL,
patchDashboardWidget,
pushDashboardWidget,
removeDashboardWidgetById,
removeDashboardWidgetLayouts,
stripUndefined,
} from './modules'

export class VBIDashboardBuilder implements VBIDashboardBuilderInterface {
private dsl: VBIDashboardDSL

constructor(dsl: VBIDashboardDSL) {
this.dsl = cloneValue(dsl)
}

public setMeta = (patch: VBISetDashboardMetaInput): this => {
this.dsl.meta = {
...this.dsl.meta,
...stripUndefined(patch),
}
return this
}

public addWidget = (input: VBIAddDashboardWidgetInput): this => {
const { layouts, ...widgetFields } = input
pushDashboardWidget(this.dsl, stripUndefined(widgetFields) as VBIDashboardWidget)
applyDashboardLayouts(this.dsl, input.id, layouts)
return this
}

public updateWidget = (widgetId: string, patch: VBIUpdateDashboardWidgetInput): this => {
patchDashboardWidget(this.dsl, widgetId, patch)
return this
}

public updateWidgetLayout = (widgetId: string, layouts: VBIUpdateDashboardWidgetLayoutInput): this => {
applyDashboardLayouts(this.dsl, widgetId, layouts)
return this
}

public removeWidget = (widgetId: string): this => {
removeDashboardWidgetById(this.dsl, widgetId)
removeDashboardWidgetLayouts(this.dsl, widgetId)
return this
}

public getDashboard = (): VBIDashboardDSL => cloneValue(this.dsl)

static create(input: VBICreateDashboardInput): VBIDashboardBuilder {
return new VBIDashboardBuilder(createDashboardDSL(input))
}

static fromDSL(dsl: VBIDashboardDSL): VBIDashboardBuilder {
return new VBIDashboardBuilder(dsl)
}
}
1 change: 1 addition & 0 deletions packages/vbi/src/dashboard-builder/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { VBIDashboardBuilder } from './builder'
17 changes: 17 additions & 0 deletions packages/vbi/src/dashboard-builder/modules/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { VBICreateDashboardInput, VBIDashboardDSL } from 'src/types'
import { zVBIDashboardDSL } from 'src/types/dashboardDSL/dashboard'
import { dashboardBreakpoints } from './layout'

export const createDashboardDSL = (input: VBICreateDashboardInput): VBIDashboardDSL => {
return zVBIDashboardDSL.parse({
version: 1,
type: 'dashboard',
uuid: input.uuid,
meta: input.meta,
widgets: [],
layout: {
breakpoints: dashboardBreakpoints,
layouts: { lg: [] },
},
})
}
4 changes: 4 additions & 0 deletions packages/vbi/src/dashboard-builder/modules/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { cloneValue, stripUndefined } from './utils'
export { dashboardBreakpoints, dashboardBreakpointKeys, applyDashboardLayouts, removeDashboardWidgetLayouts } from './layout'
export { createDashboardDSL } from './factory'
export { patchDashboardWidget, pushDashboardWidget, removeDashboardWidgetById } from './widget'
53 changes: 53 additions & 0 deletions packages/vbi/src/dashboard-builder/modules/layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { VBIDashboardBreakpoint, VBIDashboardDSL, VBIDashboardGridItemInput, VBIDashboardGridItemLayout } from 'src/types'

export const dashboardBreakpoints: Record<VBIDashboardBreakpoint, number> = {
xxl: 1600,
xl: 1200,
lg: 992,
md: 768,
sm: 576,
xs: 0,
}

export const dashboardBreakpointKeys: VBIDashboardBreakpoint[] = ['xxl', 'xl', 'lg', 'md', 'sm', 'xs']

export const upsertDashboardLayout = (
dsl: VBIDashboardDSL,
breakpoint: VBIDashboardBreakpoint,
item: VBIDashboardGridItemLayout,
): void => {
const items = dsl.layout.layouts[breakpoint] ?? []
const normalizedItem: VBIDashboardGridItemLayout = {
...item,
static: item.static ?? true,
}
const index = items.findIndex((layout) => layout.widgetId === item.widgetId)
if (index >= 0) {
items[index] = normalizedItem
} else {
items.push(normalizedItem)
}
dsl.layout.layouts[breakpoint] = items
}

export const applyDashboardLayouts = (
dsl: VBIDashboardDSL,
widgetId: string,
layouts: Partial<Record<VBIDashboardBreakpoint, VBIDashboardGridItemInput>>,
): void => {
for (const breakpoint of dashboardBreakpointKeys) {
const layout = layouts[breakpoint]
if (layout) {
upsertDashboardLayout(dsl, breakpoint, { ...layout, widgetId })
}
}
}

export const removeDashboardWidgetLayouts = (dsl: VBIDashboardDSL, widgetId: string): void => {
for (const breakpoint of dashboardBreakpointKeys) {
const items = dsl.layout.layouts[breakpoint]
if (items) {
dsl.layout.layouts[breakpoint] = items.filter((item) => item.widgetId !== widgetId)
}
}
}
14 changes: 14 additions & 0 deletions packages/vbi/src/dashboard-builder/modules/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const cloneValue = <T>(value: T): T => {
if (typeof structuredClone === 'function') return structuredClone(value)
return JSON.parse(JSON.stringify(value)) as T
}

export const stripUndefined = <T extends object>(input: T): Partial<T> => {
const result: Partial<T> = {}
for (const [key, value] of Object.entries(input) as [keyof T, T[keyof T]][]) {
if (value !== undefined) {
result[key] = value
}
}
return result
}
32 changes: 32 additions & 0 deletions packages/vbi/src/dashboard-builder/modules/widget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { VBIDashboardDSL, VBIDashboardWidget, VBIUpdateDashboardWidgetInput } from 'src/types'
import { stripUndefined } from './utils'

export const findDashboardWidget = (dsl: VBIDashboardDSL, widgetId: string): VBIDashboardWidget | undefined => {
return dsl.widgets.find((widget) => widget.id === widgetId)
}

export const ensureDashboardWidgetIdUnique = (dsl: VBIDashboardDSL, widgetId: string): void => {
if (findDashboardWidget(dsl, widgetId)) {
throw new Error(`Dashboard widget id "${widgetId}" already exists`)
}
}

export const pushDashboardWidget = (dsl: VBIDashboardDSL, widget: VBIDashboardWidget): void => {
ensureDashboardWidgetIdUnique(dsl, widget.id)
dsl.widgets.push(widget)
}

export const patchDashboardWidget = (
dsl: VBIDashboardDSL,
widgetId: string,
patch: VBIUpdateDashboardWidgetInput,
): void => {
const widget = findDashboardWidget(dsl, widgetId)
if (widget) {
Object.assign(widget, stripUndefined(patch))
}
}

export const removeDashboardWidgetById = (dsl: VBIDashboardDSL, widgetId: string): void => {
dsl.widgets = dsl.widgets.filter((widget) => widget.id !== widgetId)
}
1 change: 1 addition & 0 deletions packages/vbi/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export {
UndoManager,
} from './chart-builder'
export { VBIReportBuilder, ReportPageBuilder, ReportPageCollectionBuilder } from './report-builder'
export { VBIDashboardBuilder } from './dashboard-builder'
export { defaultVBIChartBuilderAdapters, resolveVBIChartBuilderAdapters } from './chart-builder/adapters'
export * from './types'
export {
Expand Down
40 changes: 40 additions & 0 deletions packages/vbi/src/types/builder/dashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type {
VBIDashboardBreakpoint,
VBIDashboardDSL,
VBIDashboardGridItemLayout,
VBIDashboardMeta,
VBIDashboardWidget,
} from '../dashboardDSL'

export type VBIDashboardGridItemInput = Omit<VBIDashboardGridItemLayout, 'widgetId'>

export type VBIDashboardWidgetLayoutMap = Partial<Record<VBIDashboardBreakpoint, VBIDashboardGridItemInput>> & {
lg: VBIDashboardGridItemInput
}

export type VBICreateDashboardInput = {
uuid: string
meta: VBIDashboardMeta
}

export type VBIAddDashboardWidgetInput = Omit<VBIDashboardWidget, 'id'> & {
id: string
layouts: VBIDashboardWidgetLayoutMap
}

export type VBIUpdateDashboardWidgetInput = Partial<Omit<VBIDashboardWidget, 'id' | 'type'>>

export type VBIUpdateDashboardWidgetLayoutInput = Partial<
Record<VBIDashboardBreakpoint, VBIDashboardGridItemInput>
>

export type VBISetDashboardMetaInput = Partial<VBIDashboardMeta>

export interface VBIDashboardBuilderInterface {
addWidget: (input: VBIAddDashboardWidgetInput) => this
updateWidget: (widgetId: string, patch: VBIUpdateDashboardWidgetInput) => this
updateWidgetLayout: (widgetId: string, layouts: VBIUpdateDashboardWidgetLayoutInput) => this
removeWidget: (widgetId: string) => this
setMeta: (patch: VBISetDashboardMetaInput) => this
getDashboard: () => VBIDashboardDSL
}
10 changes: 10 additions & 0 deletions packages/vbi/src/types/builder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,14 @@ export type {
} from './adapter'
export type { VBIReportBuilderInterface, VBIReportBuilderOptions } from './report'
export type { VBIInsightBuilderInterface } from './insight'
export type {
VBICreateDashboardInput,
VBIAddDashboardWidgetInput,
VBIUpdateDashboardWidgetInput,
VBIUpdateDashboardWidgetLayoutInput,
VBISetDashboardMetaInput,
VBIDashboardGridItemInput,
VBIDashboardWidgetLayoutMap,
VBIDashboardBuilderInterface,
} from './dashboard'
export type { ObserveCallback, ObserveDeepCallback } from './observe'
82 changes: 82 additions & 0 deletions packages/vbi/src/types/dashboardDSL/dashboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { z } from 'zod'

export const zVBIDashboardBreakpoint = z.enum(['xxl', 'xl', 'lg', 'md', 'sm', 'xs'])

export const zVBIDashboardMeta = z.object({
title: z.string(),
description: z.string().optional(),
mode: z.enum(['edit', 'view']).optional(),
theme: z.string().optional(),
})

export const zVBIDashboardGridItemLayout = z.object({
id: z.string(),
widgetId: z.string(),
x: z.number().int(),
y: z.number().int(),
w: z.number().int().positive(),
h: z.number().int().positive(),
static: z.boolean().optional(),
})

const zVBIDashboardWidgetBase = z.object({
id: z.string(),
type: z.string(),
title: z.string().optional(),
description: z.string().optional(),
})

export const zVBIDashboardChartWidget = zVBIDashboardWidgetBase.extend({
type: z.literal('chart'),
chartId: z.string(),
})

export const zVBIDashboardInsightWidget = zVBIDashboardWidgetBase.extend({
type: z.literal('insight'),
insightId: z.string(),
})

export const zVBIDashboardCustomWidget = zVBIDashboardWidgetBase.passthrough()

export const zVBIDashboardWidget = z.union([
zVBIDashboardChartWidget,
zVBIDashboardInsightWidget,
zVBIDashboardCustomWidget,
])

export const zVBIDashboardLayoutMap = z.object({
xxl: z.array(zVBIDashboardGridItemLayout).optional(),
xl: z.array(zVBIDashboardGridItemLayout).optional(),
lg: z.array(zVBIDashboardGridItemLayout).optional().default([]),
md: z.array(zVBIDashboardGridItemLayout).optional(),
sm: z.array(zVBIDashboardGridItemLayout).optional(),
xs: z.array(zVBIDashboardGridItemLayout).optional(),
})

export const zVBIDashboardLayout = z.object({
breakpoints: z.record(zVBIDashboardBreakpoint, z.number().int()),
cellHeight: z.number().int().positive().optional(),
layouts: zVBIDashboardLayoutMap,
})

export const zVBIDashboardDSL = z.object({
version: z.number().int().min(1).optional().default(1),
type: z.literal('dashboard').optional().default('dashboard'),
uuid: z.string().optional().default(''),
meta: zVBIDashboardMeta,
widgets: z.array(zVBIDashboardWidget).optional().default([]),
layout: zVBIDashboardLayout,
state: z.record(z.string(), z.unknown()).optional(),
})

export type VBIDashboardBreakpoint = z.output<typeof zVBIDashboardBreakpoint>
export type VBIDashboardMeta = z.output<typeof zVBIDashboardMeta>
export type VBIDashboardGridItemLayout = z.output<typeof zVBIDashboardGridItemLayout>
export type VBIDashboardChartWidget = z.output<typeof zVBIDashboardChartWidget>
export type VBIDashboardInsightWidget = z.output<typeof zVBIDashboardInsightWidget>
export type VBIDashboardCustomWidget = z.output<typeof zVBIDashboardCustomWidget>
export type VBIDashboardWidget = z.output<typeof zVBIDashboardWidget>
export type VBIDashboardLayoutMap = z.output<typeof zVBIDashboardLayoutMap>
export type VBIDashboardLayout = z.output<typeof zVBIDashboardLayout>
export type VBIDashboardDSLInput = z.input<typeof zVBIDashboardDSL>
export type VBIDashboardDSL = z.output<typeof zVBIDashboardDSL>
13 changes: 13 additions & 0 deletions packages/vbi/src/types/dashboardDSL/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type {
VBIDashboardBreakpoint,
VBIDashboardMeta,
VBIDashboardGridItemLayout,
VBIDashboardChartWidget,
VBIDashboardInsightWidget,
VBIDashboardCustomWidget,
VBIDashboardWidget,
VBIDashboardLayoutMap,
VBIDashboardLayout,
VBIDashboardDSL,
VBIDashboardDSLInput,
} from './dashboard'
1 change: 1 addition & 0 deletions packages/vbi/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './chartDSL'
export * from './insightDSL'
export * from './reportDSL'
export * from './dashboardDSL'
export * from './builder'
export * from './connector'
14 changes: 14 additions & 0 deletions packages/vdash/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Local
.DS_Store
*.local
*.log*

# Dist
node_modules
dist/
storybook-static

# IDE
.vscode/*
!.vscode/extensions.json
.idea
1 change: 1 addition & 0 deletions packages/vdash/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @visactor/vdash
Loading