@cantera/project-typestypes
Generic project-context types for cantera components: hubs, projects, folders, items, versions, model translations, and sheet version sets. The lingua franca adapters translate into.
npx shadcn@latest add @cantera/project-typesThe lingua franca for project context — hubs, projects, browsable folders and items, immutable versions, model translations, and sheet version sets. Components take these shapes as props and never fetch; adapters translate provider payloads into them.
import type { FolderEntry, Hub, ItemVersion, Project } from '@/lib/project-types'
const hub: Hub = { id: 'b.ridgeline-us', name: 'Ridgeline Builders', region: 'US' }
const projects: Project[] = [
{ id: 'b.summit-tower', name: 'Summit Tower', hubId: hub.id },
{ id: 'b.cedar-mill', name: 'Cedar Mill Campus', hubId: hub.id },
]
const entries: FolderEntry[] = [{ id: 'folder-1', name: 'Project Files', type: 'folder' }]
const versions: ItemVersion[] = []| Export | Type | Description |
|---|---|---|
| Hub | interface | An account-level container of projects — an ACC hub, a Procore company: id, name, optional region. |
| Project | interface | One project: id, name, and the hubId pickers group by when present. |
| BrowsePathSegment | interface | One controlled breadcrumb level: id, name, and type 'hub' | 'project' | 'folder'. |
| Folder / Item / FolderEntry | interface / union | Folder-like navigation rows and file-like item rows. FolderEntry is their rendering union; Hub and Project are structurally compatible with Folder. |
| ItemVersion | interface | An immutable file version: id, version number, display name, creator/time, storage size, and nullable derivative URN. |
| isItem | (entry: FolderEntry) => entry is Item | Narrows a browser row to its file-like Item shape. |
| normalizeSearchText | (value: string) => string | Case-folds and strips diacritics for consistent client and server search. |
| ModelTranslationStatus | type | 'pending' | 'inprogress' | 'success' | 'failed' | 'timeout'. |
| ModelTranslation | interface | The translation state of one design: urn, status, and optional name, progress, outputs, error. |
| SheetVersionSet | interface | A named issuance of construction sheets: id, name, and when it was issued. |
| versionSetIssuance | (versionSet: SheetVersionSet) => Date | null | Normalizes issuanceDate (Date, string, or number) into a Date, or null when absent. |
| groupProjectsByHub | (hubs: Hub[], projects: Project[]) => { hub: Hub | null; projects: Project[] }[] | Projects grouped in hub catalog order; projects referencing no known hub land in a trailing hub: null group rather than being dropped. |
This is the exact code the CLI installs into your project — you own it from there.
/** An account-level container of projects — an ACC hub, a Procore company. */
export interface Hub {
id: string
name: string
region?: string
}
export interface Project {
id: string
name: string
/** Pickers group by it when present. */
hubId?: string
}
export interface BrowsePathSegment {
id: string
name: string
type: 'hub' | 'project' | 'folder'
}
export interface Folder {
id: string
name: string
/** APS folder adapters set this; hubs and projects can omit it. */
type?: 'folder'
lastModifiedTime?: Date | string | number
modifiedBy?: string
objectCount?: number
}
export interface ItemVersion {
id: string
versionNumber: number
displayName: string
createTime: Date | string | number
createdBy: string
storageSize: number
/** URL-safe Model Derivative URN, or null when this version is not translated. */
derivativeUrn: string | null
}
export interface Item {
id: string
name: string
type: 'item'
lastModifiedTime?: Date | string | number
modifiedBy?: string
tip?: ItemVersion
translationStatus?: ModelTranslationStatus
}
export type FolderEntry = Folder | Item
export function isItem(entry: FolderEntry): entry is Item {
return entry.type === 'item'
}
export function normalizeSearchText(value: string): string {
return value
.normalize('NFD')
.replace(/\p{M}+/gu, '')
.toLocaleLowerCase()
}
/** Mirrors the Model Derivative manifest vocabulary; adapters normalize into it. */
export type ModelTranslationStatus = 'pending' | 'inprogress' | 'success' | 'failed' | 'timeout'
export interface ModelTranslation {
/** The design URN the manifest describes (base64, as the API returns it). */
urn: string
name?: string
status: ModelTranslationStatus
progress?: string
outputs?: string[]
error?: string
}
/** A named issuance of construction sheets — an ACC Sheets version set. */
export interface SheetVersionSet {
id: string
name: string
issuanceDate?: Date | string | number
}
const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/
export function versionSetIssuance(versionSet: SheetVersionSet): Date | null {
if (versionSet.issuanceDate == null) return null
// A date-only string ("2026-03-12" — the shape ACC Sheets returns) names a
// calendar day, not an instant. `new Date(string)` would read it as UTC
// midnight, which formats a day early anywhere west of UTC — so build it in
// local time instead.
if (typeof versionSet.issuanceDate === 'string') {
const dateOnly = DATE_ONLY.exec(versionSet.issuanceDate)
if (dateOnly) {
const [, year, month, day] = dateOnly
return new Date(Number(year), Number(month) - 1, Number(day))
}
}
const date = new Date(versionSet.issuanceDate)
return Number.isNaN(date.getTime()) ? null : date
}
/** Projects referencing no known hub land in a trailing `hub: null` group, so
* nothing is silently dropped. */
export function groupProjectsByHub(
hubs: Hub[],
projects: Project[],
): { hub: Hub | null; projects: Project[] }[] {
const byHub = new Map<string, Project[]>()
const orphans: Project[] = []
// Membership by Set, not `hubs.some()` per project, which would be quadratic.
const knownHubs = new Set(hubs.map((hub) => hub.id))
for (const project of projects) {
const hub = project.hubId != null && knownHubs.has(project.hubId)
if (!hub) {
orphans.push(project)
continue
}
const group = byHub.get(project.hubId as string)
if (group) group.push(project)
else byHub.set(project.hubId as string, [project])
}
const groups: { hub: Hub | null; projects: Project[] }[] = []
for (const hub of hubs) {
const grouped = byHub.get(hub.id)
if (grouped) groups.push({ hub, projects: grouped })
}
if (orphans.length > 0) groups.push({ hub: null, projects: orphans })
return groups
}