+
{icon}
)}
diff --git a/src/components/atoms/index.ts b/src/components/atoms/index.ts
index 8dedb1b..500a917 100644
--- a/src/components/atoms/index.ts
+++ b/src/components/atoms/index.ts
@@ -19,4 +19,8 @@ export { List } from './List'
export { Grid } from './Grid'
export { DataSourceBadge } from './DataSourceBadge'
export { BindingIndicator } from './BindingIndicator'
+export { StatCard } from './StatCard'
+export { LoadingState } from './LoadingState'
+export { EmptyState } from './EmptyState'
+
diff --git a/src/config/pages/dashboard.json b/src/config/pages/dashboard.json
index 59ddce7..6f1db13 100644
--- a/src/config/pages/dashboard.json
+++ b/src/config/pages/dashboard.json
@@ -1,49 +1,120 @@
{
- "id": "dashboard-json",
- "name": "Dashboard (JSON)",
- "description": "JSON-driven dashboard page",
- "icon": "ChartBar",
- "layout": {
- "type": "single"
- },
- "dataSources": [
+ "dashboardCards": [
{
- "id": "files",
- "type": "kv",
- "key": "project-files",
- "defaultValue": []
+ "id": "completion",
+ "type": "gradient-card",
+ "title": "Project Completeness",
+ "icon": "CheckCircle",
+ "gradient": "from-primary/10 to-accent/10",
+ "dataSource": {
+ "type": "computed",
+ "compute": "calculateCompletionScore"
+ },
+ "components": [
+ {
+ "type": "metric",
+ "binding": "completionScore",
+ "format": "percentage",
+ "size": "large"
+ },
+ {
+ "type": "badge",
+ "binding": "completionStatus",
+ "variants": {
+ "ready": { "label": "Ready to Export", "variant": "default" },
+ "inProgress": { "label": "In Progress", "variant": "secondary" }
+ }
+ },
+ {
+ "type": "progress",
+ "binding": "completionScore"
+ },
+ {
+ "type": "text",
+ "binding": "completionMessage",
+ "className": "text-sm text-muted-foreground"
+ }
+ ]
},
{
- "id": "models",
- "type": "kv",
- "key": "project-models",
- "defaultValue": []
+ "id": "build-status",
+ "type": "card",
+ "title": "GitHub Build Status",
+ "icon": "GitBranch",
+ "component": "GitHubBuildStatus",
+ "props": {}
+ }
+ ],
+ "statCards": [
+ {
+ "id": "code-files",
+ "icon": "Code",
+ "title": "Code Files",
+ "dataBinding": "files.length",
+ "description": "files in your project",
+ "color": "text-blue-500"
+ },
+ {
+ "id": "database-models",
+ "icon": "Database",
+ "title": "Database Models",
+ "dataBinding": "models.length",
+ "description": "Prisma schemas",
+ "color": "text-purple-500"
},
{
"id": "components",
- "type": "kv",
- "key": "project-components",
- "defaultValue": []
- }
- ],
- "components": [
- {
- "id": "dashboard-root",
- "type": "ProjectDashboard",
- "props": {},
- "dataBinding": "files"
- }
- ],
- "actions": [
- {
- "id": "navigate-to-code",
- "type": "navigate",
- "target": "code"
+ "icon": "Cube",
+ "title": "Components",
+ "dataBinding": "components.length",
+ "description": "React components",
+ "color": "text-green-500"
},
{
- "id": "create-file",
- "type": "create",
- "target": "files"
+ "id": "workflows",
+ "icon": "GitBranch",
+ "title": "Workflows",
+ "dataBinding": "workflows.length",
+ "description": "automation flows",
+ "color": "text-orange-500"
+ },
+ {
+ "id": "flask-endpoints",
+ "icon": "Flask",
+ "title": "API Endpoints",
+ "dataBinding": "flaskConfig.blueprints.reduce((acc, bp) => acc + bp.endpoints.length, 0)",
+ "description": "Flask routes",
+ "color": "text-pink-500"
+ },
+ {
+ "id": "test-suites",
+ "icon": "TestTube",
+ "title": "Test Suites",
+ "dataBinding": "playwrightTests.length + storybookStories.length + unitTests.length",
+ "description": "automated tests",
+ "color": "text-cyan-500"
}
- ]
+ ],
+ "layout": {
+ "type": "vertical",
+ "spacing": "6",
+ "sections": [
+ {
+ "type": "header",
+ "title": "Project Dashboard",
+ "description": "Overview of your CodeForge project"
+ },
+ {
+ "type": "cards",
+ "items": "dashboardCards",
+ "spacing": "6"
+ },
+ {
+ "type": "grid",
+ "items": "statCards",
+ "columns": { "sm": 1, "md": 2, "lg": 3 },
+ "gap": "4"
+ }
+ ]
+ }
}
diff --git a/src/hooks/data/index.ts b/src/hooks/data/index.ts
index 8ccdffc..5814ebe 100644
--- a/src/hooks/data/index.ts
+++ b/src/hooks/data/index.ts
@@ -1,13 +1,14 @@
-export { useJSONData } from './use-json-data'
-export { useDataSources } from './use-data-sources'
+export { useKVDataSource, useComputedDataSource, useStaticDataSource, useMultipleDataSources } from './use-data-source'
export { useCRUD } from './use-crud'
-export { useSearch } from './use-search'
+export { useSearchFilter } from './use-search-filter'
export { useSort } from './use-sort'
-export { useFilter } from './use-filter'
-export { useLocalStorage } from './use-local-storage'
export { usePagination } from './use-pagination'
-export type { UseJSONDataOptions } from './use-json-data'
-export type { UseCRUDOptions } from './use-crud'
-export type { UseSearchOptions } from './use-search'
-export type { UseSortOptions, SortDirection } from './use-sort'
-export type { FilterConfig, UseFilterOptions } from './use-filter'
+export { useSelection } from './use-selection'
+export { useSeedData } from './use-seed-data'
+
+export type { DataSourceConfig, DataSourceType } from './use-data-source'
+export type { CRUDOperations, CRUDConfig } from './use-crud'
+export type { SearchFilterConfig } from './use-search-filter'
+export type { SortConfig, SortDirection } from './use-sort'
+export type { PaginationConfig } from './use-pagination'
+export type { SelectionConfig } from './use-selection'
diff --git a/src/hooks/data/use-crud.ts b/src/hooks/data/use-crud.ts
index 909a42d..b3d8c45 100644
--- a/src/hooks/data/use-crud.ts
+++ b/src/hooks/data/use-crud.ts
@@ -1,55 +1,51 @@
-import { useState, useCallback } from 'react'
-import { useKV } from '@github/spark/hooks'
+import { useCallback } from 'react'
-export interface UseCRUDOptions
{
- key: string
- defaultValue?: T[]
- persist?: boolean
- getId?: (item: T) => string | number
+export interface CRUDOperations {
+ create: (item: T) => void
+ read: (id: string | number) => T | undefined
+ update: (id: string | number, updates: Partial) => void
+ delete: (id: string | number) => void
+ list: () => T[]
}
-export function useCRUD(options: UseCRUDOptions) {
- const { key, defaultValue = [], persist = true, getId = (item: any) => item.id } = options
-
- const [persistedItems, setPersistedItems] = useKV(key, defaultValue)
- const [localItems, setLocalItems] = useState(defaultValue)
-
- const items = persist ? persistedItems : localItems
- const setItems = persist ? setPersistedItems : setLocalItems
+export interface CRUDConfig {
+ items: T[]
+ setItems: (updater: (items: T[]) => T[]) => void
+ idField?: keyof T
+}
+export function useCRUD>({
+ items,
+ setItems,
+ idField = 'id' as keyof T,
+}: CRUDConfig): CRUDOperations {
const create = useCallback((item: T) => {
- setItems((current: T[]) => [...current, item])
+ setItems(current => [...current, item])
}, [setItems])
- const read = useCallback((id: string | number): T | undefined => {
- return items.find(item => getId(item) === id)
- }, [items, getId])
+ const read = useCallback((id: string | number) => {
+ return items.find(item => item[idField] === id)
+ }, [items, idField])
const update = useCallback((id: string | number, updates: Partial) => {
- setItems((current: T[]) =>
+ setItems(current =>
current.map(item =>
- getId(item) === id ? { ...item, ...updates } : item
+ item[idField] === id ? { ...item, ...updates } : item
)
)
- }, [setItems, getId])
+ }, [setItems, idField])
- const remove = useCallback((id: string | number) => {
- setItems((current: T[]) =>
- current.filter(item => getId(item) !== id)
- )
- }, [setItems, getId])
+ const deleteItem = useCallback((id: string | number) => {
+ setItems(current => current.filter(item => item[idField] !== id))
+ }, [setItems, idField])
- const clear = useCallback(() => {
- setItems([])
- }, [setItems])
+ const list = useCallback(() => items, [items])
return {
- items,
create,
read,
update,
- remove,
- clear,
- setItems,
+ delete: deleteItem,
+ list,
}
}
diff --git a/src/hooks/data/use-data-source.ts b/src/hooks/data/use-data-source.ts
new file mode 100644
index 0000000..fd2dcb6
--- /dev/null
+++ b/src/hooks/data/use-data-source.ts
@@ -0,0 +1,81 @@
+import { useState, useEffect, useCallback } from 'react'
+import { useKV } from '@github/spark/hooks'
+
+export type DataSourceType = 'kv' | 'static' | 'computed'
+
+export interface DataSourceConfig {
+ id: string
+ type: DataSourceType
+ key?: string
+ defaultValue?: T
+ compute?: (allData: Record) => T
+ dependencies?: string[]
+}
+
+export function useKVDataSource(key: string, defaultValue: T) {
+ const [value, setValue, deleteValue] = useKV(key, defaultValue)
+
+ return {
+ data: value,
+ setData: setValue,
+ deleteData: deleteValue,
+ isLoading: false,
+ error: null,
+ }
+}
+
+export function useComputedDataSource(
+ compute: (allData: Record) => T,
+ allData: Record,
+ dependencies: string[],
+ defaultValue?: T
+) {
+ const [computed, setComputed] = useState(defaultValue as T)
+
+ useEffect(() => {
+ try {
+ const newValue = compute(allData)
+ setComputed(newValue)
+ } catch (error) {
+ console.error('Error computing data source:', error)
+ }
+ }, dependencies.map(dep => allData[dep]))
+
+ return {
+ data: computed,
+ setData: () => {},
+ deleteData: () => {},
+ isLoading: false,
+ error: null,
+ }
+}
+
+export function useStaticDataSource(value: T) {
+ return {
+ data: value,
+ setData: () => {},
+ deleteData: () => {},
+ isLoading: false,
+ error: null,
+ }
+}
+
+export function useMultipleDataSources(
+ configs: DataSourceConfig[],
+ onUpdate?: (data: Record) => void
+) {
+ const [allData, setAllData] = useState>({})
+
+ const updateData = useCallback((id: string, value: any) => {
+ setAllData(prev => {
+ const next = { ...prev, [id]: value }
+ onUpdate?.(next)
+ return next
+ })
+ }, [onUpdate])
+
+ return {
+ allData,
+ updateData,
+ }
+}
diff --git a/src/hooks/data/use-pagination.ts b/src/hooks/data/use-pagination.ts
index 9097fc8..557de71 100644
--- a/src/hooks/data/use-pagination.ts
+++ b/src/hooks/data/use-pagination.ts
@@ -1,52 +1,55 @@
-import { useState, useCallback, useMemo } from 'react'
+import { useState, useMemo, useCallback } from 'react'
export interface PaginationConfig {
- page: number
- pageSize: number
- total: number
+ items: any[]
+ pageSize?: number
+ initialPage?: number
}
-export function usePagination(items: T[], initialPageSize: number = 10) {
- const [page, setPage] = useState(1)
- const [pageSize, setPageSize] = useState(initialPageSize)
+export function usePagination({
+ items,
+ pageSize = 10,
+ initialPage = 1,
+}: PaginationConfig) {
+ const [currentPage, setCurrentPage] = useState(initialPage)
- const total = items.length
- const totalPages = Math.ceil(total / pageSize)
+ const totalPages = Math.ceil(items.length / pageSize)
const paginatedItems = useMemo(() => {
- const start = (page - 1) * pageSize
+ const start = (currentPage - 1) * pageSize
const end = start + pageSize
return items.slice(start, end)
- }, [items, page, pageSize])
+ }, [items, currentPage, pageSize])
- const goToPage = useCallback((newPage: number) => {
- setPage(Math.max(1, Math.min(newPage, totalPages)))
+ const goToPage = useCallback((page: number) => {
+ setCurrentPage(Math.max(1, Math.min(page, totalPages)))
}, [totalPages])
const nextPage = useCallback(() => {
- goToPage(page + 1)
- }, [page, goToPage])
+ goToPage(currentPage + 1)
+ }, [currentPage, goToPage])
const prevPage = useCallback(() => {
- goToPage(page - 1)
- }, [page, goToPage])
+ goToPage(currentPage - 1)
+ }, [currentPage, goToPage])
- const changePageSize = useCallback((newSize: number) => {
- setPageSize(newSize)
- setPage(1)
- }, [])
+ const reset = useCallback(() => {
+ setCurrentPage(initialPage)
+ }, [initialPage])
return {
items: paginatedItems,
- page,
- pageSize,
- total,
+ currentPage,
totalPages,
+ pageSize,
goToPage,
nextPage,
prevPage,
- changePageSize,
- hasNext: page < totalPages,
- hasPrev: page > 1,
+ reset,
+ hasNext: currentPage < totalPages,
+ hasPrev: currentPage > 1,
+ startIndex: (currentPage - 1) * pageSize + 1,
+ endIndex: Math.min(currentPage * pageSize, items.length),
+ totalItems: items.length,
}
}
diff --git a/src/hooks/data/use-search-filter.ts b/src/hooks/data/use-search-filter.ts
new file mode 100644
index 0000000..4325067
--- /dev/null
+++ b/src/hooks/data/use-search-filter.ts
@@ -0,0 +1,56 @@
+import { useState, useMemo, useCallback } from 'react'
+
+export interface SearchFilterConfig {
+ items: T[]
+ searchFields?: (keyof T)[]
+ filterFn?: (item: T, filters: Record) => boolean
+}
+
+export function useSearchFilter>({
+ items,
+ searchFields = [],
+ filterFn,
+}: SearchFilterConfig) {
+ const [searchQuery, setSearchQuery] = useState('')
+ const [filters, setFilters] = useState>({})
+
+ const filtered = useMemo(() => {
+ let result = items
+
+ if (searchQuery && searchFields.length > 0) {
+ const query = searchQuery.toLowerCase()
+ result = result.filter(item =>
+ searchFields.some(field => {
+ const value = item[field]
+ return String(value).toLowerCase().includes(query)
+ })
+ )
+ }
+
+ if (filterFn && Object.keys(filters).length > 0) {
+ result = result.filter(item => filterFn(item, filters))
+ }
+
+ return result
+ }, [items, searchQuery, searchFields, filters, filterFn])
+
+ const setFilter = useCallback((key: string, value: any) => {
+ setFilters(prev => ({ ...prev, [key]: value }))
+ }, [])
+
+ const clearFilters = useCallback(() => {
+ setSearchQuery('')
+ setFilters({})
+ }, [])
+
+ return {
+ searchQuery,
+ setSearchQuery,
+ filters,
+ setFilter,
+ clearFilters,
+ filtered,
+ count: filtered.length,
+ total: items.length,
+ }
+}
diff --git a/src/hooks/data/use-selection.ts b/src/hooks/data/use-selection.ts
new file mode 100644
index 0000000..5b2d48a
--- /dev/null
+++ b/src/hooks/data/use-selection.ts
@@ -0,0 +1,77 @@
+import { useState, useCallback } from 'react'
+
+export interface SelectionConfig {
+ items: T[]
+ multiple?: boolean
+ idField?: keyof T
+}
+
+export function useSelection>({
+ items,
+ multiple = false,
+ idField = 'id' as keyof T,
+}: SelectionConfig) {
+ const [selected, setSelected] = useState>(new Set())
+
+ const toggle = useCallback((id: string | number) => {
+ setSelected(prev => {
+ const next = new Set(prev)
+ if (next.has(id)) {
+ next.delete(id)
+ } else {
+ if (!multiple) {
+ next.clear()
+ }
+ next.add(id)
+ }
+ return next
+ })
+ }, [multiple])
+
+ const select = useCallback((id: string | number) => {
+ setSelected(prev => {
+ const next: Set = multiple ? new Set(prev) : new Set()
+ next.add(id)
+ return next
+ })
+ }, [multiple])
+
+ const deselect = useCallback((id: string | number) => {
+ setSelected(prev => {
+ const next = new Set(prev)
+ next.delete(id)
+ return next
+ })
+ }, [])
+
+ const selectAll = useCallback(() => {
+ if (multiple) {
+ setSelected(new Set(items.map(item => item[idField])))
+ }
+ }, [items, idField, multiple])
+
+ const deselectAll = useCallback(() => {
+ setSelected(new Set())
+ }, [])
+
+ const isSelected = useCallback((id: string | number) => {
+ return selected.has(id)
+ }, [selected])
+
+ const getSelected = useCallback(() => {
+ return items.filter(item => selected.has(item[idField]))
+ }, [items, selected, idField])
+
+ return {
+ selected,
+ toggle,
+ select,
+ deselect,
+ selectAll,
+ deselectAll,
+ isSelected,
+ getSelected,
+ count: selected.size,
+ hasSelection: selected.size > 0,
+ }
+}
diff --git a/src/hooks/data/use-sort.ts b/src/hooks/data/use-sort.ts
index d3fbc6f..6b7a31c 100644
--- a/src/hooks/data/use-sort.ts
+++ b/src/hooks/data/use-sort.ts
@@ -1,61 +1,54 @@
-import { useState, useMemo } from 'react'
+import { useState, useMemo, useCallback } from 'react'
-export type SortDirection = 'asc' | 'desc' | null
+export type SortDirection = 'asc' | 'desc'
-export interface UseSortOptions {
+export interface SortConfig {
items: T[]
- initialField?: keyof T
- initialDirection?: SortDirection
+ defaultField?: keyof T
+ defaultDirection?: SortDirection
}
-export function useSort(options: UseSortOptions) {
- const { items, initialField, initialDirection = 'asc' } = options
- const [field, setField] = useState(initialField || null)
- const [direction, setDirection] = useState(initialDirection)
+export function useSort>({
+ items,
+ defaultField,
+ defaultDirection = 'asc',
+}: SortConfig) {
+ const [sortField, setSortField] = useState(defaultField)
+ const [sortDirection, setSortDirection] = useState(defaultDirection)
const sorted = useMemo(() => {
- if (!field || !direction) return items
+ if (!sortField) return items
return [...items].sort((a, b) => {
- const aVal = a[field]
- const bVal = b[field]
+ const aVal = a[sortField]
+ const bVal = b[sortField]
- if (aVal == null && bVal == null) return 0
- if (aVal == null) return 1
- if (bVal == null) return -1
+ if (aVal === bVal) return 0
- if (aVal < bVal) return direction === 'asc' ? -1 : 1
- if (aVal > bVal) return direction === 'asc' ? 1 : -1
- return 0
+ const comparison = aVal < bVal ? -1 : 1
+ return sortDirection === 'asc' ? comparison : -comparison
})
- }, [items, field, direction])
+ }, [items, sortField, sortDirection])
- const toggleSort = (newField: keyof T) => {
- if (field === newField) {
- setDirection(prev =>
- prev === 'asc' ? 'desc' : prev === 'desc' ? null : 'asc'
- )
- if (direction === 'desc') {
- setField(null)
- }
+ const toggleSort = useCallback((field: keyof T) => {
+ if (sortField === field) {
+ setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc')
} else {
- setField(newField)
- setDirection('asc')
+ setSortField(field)
+ setSortDirection('asc')
}
- }
+ }, [sortField])
- const reset = () => {
- setField(null)
- setDirection(null)
- }
+ const resetSort = useCallback(() => {
+ setSortField(defaultField)
+ setSortDirection(defaultDirection)
+ }, [defaultField, defaultDirection])
return {
sorted,
- field,
- direction,
+ sortField,
+ sortDirection,
toggleSort,
- setField,
- setDirection,
- reset,
+ resetSort,
}
}
diff --git a/src/hooks/forms/index.ts b/src/hooks/forms/index.ts
index fec39f8..4c3fb31 100644
--- a/src/hooks/forms/index.ts
+++ b/src/hooks/forms/index.ts
@@ -1,2 +1,2 @@
-export * from './use-form'
-export * from './use-form-field'
+export { useFormField, useForm } from './use-form-field'
+export type { ValidationRule, FieldConfig, FormConfig } from './use-form-field'
diff --git a/src/hooks/forms/use-form-field.ts b/src/hooks/forms/use-form-field.ts
index 684e465..c6fcc2f 100644
--- a/src/hooks/forms/use-form-field.ts
+++ b/src/hooks/forms/use-form-field.ts
@@ -1,33 +1,40 @@
import { useState, useCallback } from 'react'
-export type ValidationRule = {
+export interface ValidationRule {
validate: (value: T) => boolean
message: string
}
-export function useFormField(
- initialValue: T,
- rules: ValidationRule[] = []
-) {
- const [value, setValue] = useState(initialValue)
+export interface FieldConfig {
+ name: string
+ defaultValue?: T
+ rules?: ValidationRule[]
+}
+
+export function useFormField(config: FieldConfig) {
+ const [value, setValue] = useState(config.defaultValue)
const [error, setError] = useState(null)
const [touched, setTouched] = useState(false)
const validate = useCallback(() => {
- for (const rule of rules) {
- if (!rule.validate(value)) {
+ if (!config.rules || !touched) return true
+
+ for (const rule of config.rules) {
+ if (!rule.validate(value as T)) {
setError(rule.message)
return false
}
}
setError(null)
return true
- }, [value, rules])
+ }, [value, config.rules, touched])
const onChange = useCallback((newValue: T) => {
setValue(newValue)
- setTouched(true)
- }, [])
+ if (touched) {
+ setError(null)
+ }
+ }, [touched])
const onBlur = useCallback(() => {
setTouched(true)
@@ -35,20 +42,43 @@ export function useFormField(
}, [validate])
const reset = useCallback(() => {
- setValue(initialValue)
+ setValue(config.defaultValue)
setError(null)
setTouched(false)
- }, [initialValue])
+ }, [config.defaultValue])
return {
value,
- setValue,
- onChange,
- onBlur,
error,
touched,
- isValid: error === null && touched,
- validate,
+ onChange,
+ onBlur,
reset,
+ validate,
+ isValid: error === null,
+ isDirty: value !== config.defaultValue,
+ }
+}
+
+export interface FormConfig {
+ fields: Record
+ onSubmit?: (values: Record) => void | Promise
+}
+
+export function useForm(config: FormConfig) {
+ const [isSubmitting, setIsSubmitting] = useState(false)
+
+ const submit = useCallback(async (values: Record) => {
+ setIsSubmitting(true)
+ try {
+ await config.onSubmit?.(values)
+ } finally {
+ setIsSubmitting(false)
+ }
+ }, [config])
+
+ return {
+ submit,
+ isSubmitting,
}
}
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index 4e63281..283c77d 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -15,5 +15,13 @@ export * from './ai/use-ai-generation'
export * from './data/use-seed-data'
export * from './data/use-seed-templates'
+export { useKVDataSource, useComputedDataSource, useStaticDataSource, useMultipleDataSources } from './data/use-data-source'
+export { useCRUD } from './data/use-crud'
+export { useSearchFilter } from './data/use-search-filter'
+export { useSort } from './data/use-sort'
+export { usePagination } from './data/use-pagination'
+export { useSelection as useDataSelection } from './data/use-selection'
+
+export { useFormField, useForm } from './forms/use-form-field'
export * from './use-route-preload'