Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/clever-foxes-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@eventcatalog/core": patch
---

Add `doc` resource reference type support for custom documentation pages, extract badge styling into shared utility, and fix theming to use CSS variables
4 changes: 2 additions & 2 deletions examples/default/docs/guides/event-storming/01-index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,5 @@ To conduct an effective Event Storming session, you'll need:
## Next Steps

Continue reading to learn:
- [How to Facilitate an Event Storming Session](/docs/guides/event-storming/02-facilitation)
- [From Event Storming to Implementation](/docs/guides/event-storming/03-implementation)
- [[doc|guides/event-storming/02-facilitation]]
- [[doc|guides/event-storming/03-implementation]]
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ Browse the runbooks in this section to find procedures for:
- **Payment Service** - Payment gateway and transaction failures
- **Shipping Service** - Shipping provider integrations and tracking

## Quick Links

Use inline custom doc references to jump directly to each runbook:

- [[doc|operations-and-support/runbooks/inventory-service-runbook]]
- [[doc|operations-and-support/runbooks/orders-service-runbook]]
- [[doc|operations-and-support/runbooks/payment-service-runbook]]
- [[doc|operations-and-support/runbooks/shipping-service-runbook]]

## Using Runbooks

1. Identify the affected service
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ interface Props {
| 'diagram'
| 'container'
| 'user'
| 'team';
| 'team'
| 'doc';
version?: string;
}

Expand All @@ -38,6 +39,20 @@ const { type = 'entity', version } = Astro.props;
const slotContent = await Astro.slots.render('default');
const resourceId = slotContent.trim();

const normalizeCustomDocPath = (value: string): string => {
const normalized = value
.trim()
.replace(/^https?:\/\/[^/]+/i, '')
.replace(/\\/g, '/');
return normalized
.replace(/^\/+/, '')
.replace(/^docs\/custom\//i, '')
.replace(/^docs\//i, '')
.replace(/\/+$/, '');
};

const normalizedResourceId = normalizeCustomDocPath(resourceId).toLowerCase();

// Map type to collection name
const collection = resourceToCollectionMap[type as keyof typeof resourceToCollectionMap];

Expand All @@ -56,6 +71,7 @@ const typeStyles: Record<string, { borderColor: string; label: string }> = {
container: { borderColor: 'var(--ec-badge-default-text)', label: 'Container' }, // gray
user: { borderColor: 'var(--ec-badge-default-text)', label: 'User' }, // gray
team: { borderColor: 'var(--ec-badge-default-text)', label: 'Team' }, // gray
doc: { borderColor: 'var(--ec-badge-default-text)', label: 'Doc' }, // gray
};

// SVG icons for each type (from heroicons outline)
Expand All @@ -81,6 +97,7 @@ const typeIcons: Record<string, string> = {
'<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />', // Database
user: '<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />', // User
team: '<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" />', // UserGroup
doc: '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-8.25a2.25 2.25 0 00-2.25-2.25h-10.5A2.25 2.25 0 004.5 6v12a2.25 2.25 0 002.25 2.25h5.25M16.5 18.75h6m-3-3v6" />', // Document with plus
};

const style = typeStyles[type] || typeStyles.entity;
Expand All @@ -92,23 +109,51 @@ let hasError = false;
let errorMessage = '';

try {
if (!collection) {
throw new Error(`Unknown resource type: ${type}`);
}
if (type === 'doc') {
const docs = (await getCollection('customPages')) as {
id: string;
data: { title?: string; summary?: string; slug?: string; owners?: any[] };
}[];

const resourcesCollection = (await getCollection(collection as any)) as { data: { id: string; version: string } }[];
const resources = getItemsFromCollectionByIdAndSemverOrLatest(resourcesCollection, resourceId, version);
const doc = docs.find((entry) => {
const normalizedId = normalizeCustomDocPath(entry.id).toLowerCase();
const normalizedSlug = normalizeCustomDocPath(entry.data.slug || '').toLowerCase();

if (resources.length === 0) {
throw new Error(`Resource not found: ${resourceId}`);
}
return normalizedId === normalizedResourceId || normalizedSlug === normalizedResourceId;
});

if (!doc) {
throw new Error(`Document not found: ${resourceId}`);
}

const docSlug = normalizeCustomDocPath(doc.data.slug || doc.id);
href = buildUrl(`/docs/custom/${docSlug}`);
resource = {
...doc,
data: {
...doc.data,
name: doc.data.title || docSlug,
},
};
} else {
if (!collection) {
throw new Error(`Unknown resource type: ${type}`);
}

const resourcesCollection = (await getCollection(collection as any)) as { data: { id: string; version: string } }[];
const resources = getItemsFromCollectionByIdAndSemverOrLatest(resourcesCollection, resourceId, version);

resource = resources[0];
// Diagrams use /diagrams/ path, other resources use /docs/{collection}/
href =
type === 'diagram'
? buildUrl(`/diagrams/${resourceId}/${resource.data.version}`)
: buildUrl(`/docs/${collection}/${resourceId}/${resource.data.version}`);
if (resources.length === 0) {
throw new Error(`Resource not found: ${resourceId}`);
}

resource = resources[0];
// Diagrams use /diagrams/ path, other resources use /docs/{collection}/
href =
type === 'diagram'
? buildUrl(`/diagrams/${resourceId}/${resource.data.version}`)
: buildUrl(`/docs/${collection}/${resourceId}/${resource.data.version}`);
}
} catch (error) {
hasError = true;
errorMessage = error instanceof Error ? error.message : 'Unknown error';
Expand All @@ -119,6 +164,8 @@ const maxSummaryLength = 120;
const summary = resource?.data?.summary || '';
const truncatedSummary = summary.length > maxSummaryLength ? summary.slice(0, maxSummaryLength) + '...' : summary;

const isVersionedResource = type !== 'doc';

// Only these types have visualizers
const hasVisualizer = ['domain', 'service', 'event', 'query', 'command', 'container'].includes(type);

Expand Down Expand Up @@ -192,7 +239,7 @@ const tooltipId = `ref-tooltip-${Math.random().toString(36).slice(2, 9)}`;
hasError ? (
<span
class="text-[rgb(var(--ec-page-text-muted))] underline decoration-wavy decoration-red-400/50 underline-offset-2 cursor-help"
title={`Resource not found: ${resourceId}`}
title={`Reference not found: ${resourceId}`}
>
{resourceId}
</span>
Expand Down Expand Up @@ -240,9 +287,11 @@ const tooltipId = `ref-tooltip-${Math.random().toString(36).slice(2, 9)}`;
</span>
<span class="text-[0.65rem] text-[rgb(var(--ec-page-text-muted))] uppercase tracking-wide">{style.label}</span>
</span>
<span class="text-[0.7rem] font-mono text-[rgb(var(--ec-page-text-muted))] mt-0.5">
v{resource?.data?.version}
</span>
{isVersionedResource && (
<span class="text-[0.7rem] font-mono text-[rgb(var(--ec-page-text-muted))] mt-0.5">
v{resource?.data?.version}
</span>
)}
</span>
<svg
class="w-5 h-5 flex-shrink-0"
Expand Down Expand Up @@ -392,7 +441,7 @@ const tooltipId = `ref-tooltip-${Math.random().toString(36).slice(2, 9)}`;
)}
</span>
<a href={href} class="text-[rgb(var(--ec-accent))] hover:underline font-medium">
{type === 'diagram' ? 'View diagram' : 'View docs'}
{type === 'diagram' ? 'View diagram' : type === 'doc' ? 'View doc' : 'View docs'}
</a>
</span>
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { buildUrl } from '@utils/url-builder';
import type { SidebarItem } from '../types';
import { ExternalLinkIcon } from 'lucide-react';
import { getCustomDocsSidebarBadgeClasses } from '@enterprise/custom-documentation/utils/badge-styles';

interface NestedItemProps {
item: SidebarItem;
Expand Down Expand Up @@ -29,6 +30,8 @@ const NestedItem: React.FC<NestedItemProps> = ({
const folderPath = folderHasLink ? buildUrl(`/docs/custom/${item.slug}`) : undefined;
const isFolderActive = folderPath && (currentPath === folderPath || currentPath.endsWith(`/${item.slug}`));

const badgeClassName = getCustomDocsSidebarBadgeClasses(item.badge?.color);

return (
<div className="py-1">
<div className="flex items-center">
Expand Down Expand Up @@ -66,17 +69,7 @@ const NestedItem: React.FC<NestedItemProps> = ({
data-active={isFolderActive}
>
<span className="truncate">{item.label}</span>
{item.badge && item?.badge?.text && (
<span
className={
item.badge.color
? `text-${item.badge.color}-600 dark:text-${item.badge.color}-400 ml-2 text-[10px] font-medium bg-${item.badge.color}-50 dark:bg-${item.badge.color}-500/20 px-2 py-0.5 rounded uppercase`
: `text-[rgb(var(--ec-accent))] ml-2 text-[10px] font-medium bg-[rgb(var(--ec-accent-subtle))] px-2 py-0.5 rounded uppercase`
}
>
{item.badge.text}
</span>
)}
{item.badge && item?.badge?.text && <span className={badgeClassName}>{item.badge.text}</span>}
</a>
) : (
// Folder without index file - render as toggle button
Expand All @@ -88,17 +81,7 @@ const NestedItem: React.FC<NestedItemProps> = ({
}}
>
<span className="truncate">{item.label}</span>
{item.badge && item?.badge?.text && (
<span
className={
item.badge.color
? `text-${item.badge.color}-600 dark:text-${item.badge.color}-400 ml-2 text-[10px] font-medium bg-${item.badge.color}-50 dark:bg-${item.badge.color}-500/20 px-2 py-0.5 rounded uppercase`
: `text-[rgb(var(--ec-accent))] ml-2 text-[10px] font-medium bg-[rgb(var(--ec-accent-subtle))] px-2 py-0.5 rounded uppercase`
}
>
{item.badge.text}
</span>
)}
{item.badge && item?.badge?.text && <span className={badgeClassName}>{item.badge.text}</span>}
</button>
)}
</div>
Expand Down Expand Up @@ -153,6 +136,8 @@ const NestedItem: React.FC<NestedItemProps> = ({
itemPath = item.slug;
}

const badgeClassName = getCustomDocsSidebarBadgeClasses(item.badge?.color);

return (
<a
href={itemPath}
Expand All @@ -165,17 +150,7 @@ const NestedItem: React.FC<NestedItemProps> = ({
{item.label}
{isExternalLink && <ExternalLinkIcon className="w-3 -mt-0.5 h-3" />}
</span>
{item.badge && item?.badge?.text && (
<span
className={
item.badge.color
? `text-${item.badge.color}-600 dark:text-${item.badge.color}-400 ml-2 text-[10px] font-medium bg-${item.badge.color}-50 dark:bg-${item.badge.color}-500/20 px-2 py-0.5 rounded uppercase`
: `text-[rgb(var(--ec-accent))] ml-2 text-[10px] font-medium bg-[rgb(var(--ec-accent-subtle))] px-2 py-0.5 rounded uppercase`
}
>
{item.badge.text}
</span>
)}
{item.badge && item?.badge?.text && <span className={badgeClassName}>{item.badge.text}</span>}
</a>
);
};
Expand Down
Loading