first commit

This commit is contained in:
Matt Kane
2026-04-01 10:44:22 +01:00
commit 43fcb9a131
1789 changed files with 395041 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
import type { WidgetComponentDef } from "./types.js";
/**
* Core widget components registry
* These are built-in widgets that ship with EmDash
*/
export const coreWidgetComponents: WidgetComponentDef[] = [
{
id: "core:recent-posts",
label: "Recent Posts",
description: "Display a list of recent posts",
props: {
count: {
type: "number",
label: "Number of posts",
default: 5,
},
showThumbnails: {
type: "boolean",
label: "Show thumbnails",
default: false,
},
showDate: {
type: "boolean",
label: "Show date",
default: true,
},
},
},
{
id: "core:categories",
label: "Categories",
description: "Display category list",
props: {
showCount: {
type: "boolean",
label: "Show post count",
default: true,
},
hierarchical: {
type: "boolean",
label: "Show hierarchy",
default: true,
},
},
},
{
id: "core:tags",
label: "Tags",
description: "Display tag cloud",
props: {
showCount: {
type: "boolean",
label: "Show count",
default: false,
},
limit: {
type: "number",
label: "Maximum tags",
default: 20,
},
},
},
{
id: "core:search",
label: "Search",
description: "Search form",
props: {
placeholder: {
type: "string",
label: "Placeholder text",
default: "Search...",
},
},
},
{
id: "core:archives",
label: "Archives",
description: "Monthly/yearly archives",
props: {
type: {
type: "select",
label: "Group by",
default: "monthly",
options: [
{ value: "monthly", label: "Monthly" },
{ value: "yearly", label: "Yearly" },
],
},
limit: {
type: "number",
label: "Limit",
default: 12,
},
},
},
];
/**
* Get all widget component definitions (core + plugin-registered)
* For now, only returns core components. Plugin widgets will be added later.
*/
export function getWidgetComponents(): WidgetComponentDef[] {
return [...coreWidgetComponents];
}

View File

@@ -0,0 +1,131 @@
import { getDb } from "../loader.js";
import { getWidgetComponents as getComponentRegistry } from "./components.js";
import type { Widget, WidgetArea, WidgetRow, WidgetComponentDef } from "./types.js";
export type {
Widget,
WidgetArea,
WidgetType,
WidgetComponentDef,
PropDef,
CreateWidgetAreaInput,
CreateWidgetInput,
UpdateWidgetInput,
ReorderWidgetsInput,
} from "./types.js";
/**
* Get a widget area by name, with all its widgets
*/
export async function getWidgetArea(name: string): Promise<WidgetArea | null> {
const db = await getDb();
// Get the area
const areaRow = await db
.selectFrom("_emdash_widget_areas")
.selectAll()
.where("name", "=", name)
.executeTakeFirst();
if (!areaRow) {
return null;
}
// Get widgets for this area, ordered by sort_order
const widgetRows = await db
.selectFrom("_emdash_widgets")
.selectAll()
.$castTo<WidgetRow>()
.where("area_id", "=", areaRow.id)
.orderBy("sort_order", "asc")
.execute();
// Map to API types
const widgets: Widget[] = widgetRows.map((row) => rowToWidget(row));
return {
id: areaRow.id,
name: areaRow.name,
label: areaRow.label,
description: areaRow.description ?? undefined,
widgets,
};
}
/**
* Get all widget areas with their widgets
*/
export async function getWidgetAreas(): Promise<WidgetArea[]> {
const db = await getDb();
// Get all areas
const areaRows = await db.selectFrom("_emdash_widget_areas").selectAll().execute();
// Get all widgets
const widgetRows = await db
.selectFrom("_emdash_widgets")
.selectAll()
.$castTo<WidgetRow>()
.orderBy("sort_order", "asc")
.execute();
// Group widgets by area
const widgetsByArea = new Map<string, Widget[]>();
for (const row of widgetRows) {
if (!widgetsByArea.has(row.area_id)) {
widgetsByArea.set(row.area_id, []);
}
widgetsByArea.get(row.area_id)!.push(rowToWidget(row));
}
// Combine
return areaRows.map((areaRow) => ({
id: areaRow.id,
name: areaRow.name,
label: areaRow.label,
description: areaRow.description ?? undefined,
widgets: widgetsByArea.get(areaRow.id) || [],
}));
}
/**
* Get available widget components (for admin UI)
*/
export function getWidgetComponents(): WidgetComponentDef[] {
return getComponentRegistry();
}
/**
* Convert a widget row to the API type
*/
function rowToWidget(row: WidgetRow): Widget {
const widget: Widget = {
id: row.id,
type: row.type,
title: row.title ?? undefined,
};
// Type-specific fields
if (row.type === "content" && row.content) {
try {
widget.content = JSON.parse(row.content);
} catch {
// Invalid JSON, ignore
}
}
if (row.type === "menu" && row.menu_name) {
widget.menuName = row.menu_name;
}
if (row.type === "component" && row.component_id) {
widget.componentId = row.component_id;
if (row.component_props) {
try {
widget.componentProps = JSON.parse(row.component_props);
} catch {
// Invalid JSON, ignore
}
}
}
return widget;
}

View File

@@ -0,0 +1,81 @@
import type { PortableTextBlock } from "../fields/index.js";
export type WidgetType = "content" | "menu" | "component";
export interface Widget {
id: string;
type: WidgetType;
title?: string;
// Type-specific fields
content?: PortableTextBlock[]; // For content type
menuName?: string; // For menu type
componentId?: string; // For component type
componentProps?: Record<string, unknown>;
}
export interface WidgetArea {
id: string;
name: string;
label: string;
description?: string;
widgets: Widget[];
}
// For DB layer
export interface WidgetRow {
id: string;
area_id: string;
sort_order: number;
type: WidgetType;
title: string | null;
content: string | null; // JSON string
menu_name: string | null;
component_id: string | null;
component_props: string | null; // JSON string
created_at: string;
}
export interface WidgetAreaRow {
id: string;
name: string;
label: string;
description: string | null;
created_at: string;
}
// Component registration
export interface WidgetComponentDef {
id: string; // 'core:recent-posts'
label: string; // 'Recent Posts'
description?: string;
props: Record<string, PropDef>;
}
export interface PropDef {
type: "string" | "number" | "boolean" | "select";
label: string;
default?: unknown;
options?: Array<{ value: string; label: string }>; // For select
}
// Admin API types
export interface CreateWidgetAreaInput {
name: string;
label: string;
description?: string;
}
export interface CreateWidgetInput {
type: WidgetType;
title?: string;
content?: PortableTextBlock[];
menuName?: string;
componentId?: string;
componentProps?: Record<string, unknown>;
}
export interface UpdateWidgetInput extends Partial<CreateWidgetInput> {}
export interface ReorderWidgetsInput {
widgetIds: string[];
}