use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Deserialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum Shell {
Standard,
Document,
Deck,
Hub,
}
impl Shell {
pub fn class(&self) -> &'static str {
match self {
Shell::Standard => "shell-standard",
Shell::Document => "shell-document",
Shell::Deck => "shell-deck",
Shell::Hub => "shell-hub",
}
}
}
#[derive(Deserialize, Clone)]
pub struct HubConfig {
pub name: String,
#[serde(default)]
pub eyebrow: Option<String>,
#[serde(default)]
pub status: Option<String>,
#[serde(default)]
pub status_color: Option<SemColor>,
#[serde(default)]
pub pages: Vec<HubLink>,
}
#[derive(Deserialize, Clone)]
pub struct HubLink {
pub label: String,
pub href: String,
}
#[derive(Deserialize)]
pub struct PackMeta {
#[serde(default)]
pub targets: Vec<String>,
#[serde(default)]
pub hooks: Vec<PackHook>,
}
#[derive(Deserialize, Serialize, Clone)]
pub struct HookMatch {
pub tool: String,
}
#[derive(Deserialize, Serialize, Clone, Copy, Default, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum MatchMode {
#[default]
Substring,
Word,
Regex,
}
#[derive(Deserialize, Serialize, Clone, Copy, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum InjectEvent {
SessionStart,
UserPromptSubmit,
}
#[derive(Deserialize, Serialize, Clone)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum PackHook {
BlockOnMatch {
on: HookMatch,
#[serde(default)]
mode: MatchMode,
#[serde(default)]
field: Option<String>,
patterns: Vec<String>,
message: String,
},
BlockUnlessMatch {
on: HookMatch,
require: String,
message: String,
},
Allowlist {
on: HookMatch,
field: String,
allow: Vec<String>,
message: String,
},
Inject { event: InjectEvent, text: String },
ReviewPrompt {
on: HookMatch,
prompt: String,
#[serde(default)]
model_tier: Option<String>,
},
}
#[derive(Deserialize)]
pub struct Page {
pub title: String,
pub shell: Shell,
pub eyebrow: Option<String>,
pub subtitle: Option<String>,
pub components: Option<Vec<Component>>,
pub slides: Option<Vec<Slide>>,
#[serde(default)]
pub unlisted: bool,
#[serde(default)]
pub texture: Option<Texture>,
#[serde(default)]
pub glow: Option<Glow>,
#[serde(default)]
pub print_flow: Option<PrintFlow>,
#[serde(default)]
pub hub: Option<HubConfig>,
#[serde(default)]
pub search_terms: Vec<String>,
#[serde(default)]
pub freshness: Option<FreshnessValue>,
#[serde(default)]
pub owner: Option<String>,
#[serde(default)]
pub pack: Option<PackMeta>,
#[serde(default)]
pub references: Vec<Reference>,
#[serde(default)]
pub personas: Vec<String>,
#[serde(default)]
pub archived: bool,
#[serde(default)]
pub draft: bool,
#[serde(default)]
pub nav_layout: Option<NavLayout>,
#[serde(default)]
pub nav: Option<Vec<NavLink>>,
}
impl Page {
pub fn is_archived(&self, today: &str) -> bool {
if self.archived {
return true;
}
let freshness = self.freshness.as_ref().and_then(|fv| fv.as_full());
if crate::freshness::is_expired(freshness, today) {
return true;
}
if self.draft {
return crate::freshness::is_stale_draft(freshness, today);
}
false
}
}
#[derive(Deserialize, Clone)]
#[serde(untagged)]
pub enum FreshnessValue {
Never(FreshnessNever),
Full(Freshness),
}
impl FreshnessValue {
pub fn as_full(&self) -> Option<&Freshness> {
match self {
FreshnessValue::Full(f) => Some(f),
FreshnessValue::Never(_) => None,
}
}
pub fn is_never(&self) -> bool {
matches!(self, FreshnessValue::Never(_))
}
}
#[derive(Deserialize, Clone)]
pub enum FreshnessNever {
#[serde(rename = "never")]
Never,
}
#[derive(Deserialize, Clone)]
pub struct Reference {
pub url: String,
#[serde(default)]
pub note: Option<String>,
}
#[derive(Deserialize, Clone)]
pub struct Freshness {
pub updated: Option<String>,
pub review_every: Option<String>,
pub owner: Option<String>,
#[serde(default)]
pub sources_of_truth: Option<Vec<SourceOfTruth>>,
#[serde(default)]
pub expires: Option<String>,
#[serde(default)]
pub refresh: Option<RefreshValue>,
}
#[derive(Deserialize, Clone)]
#[serde(untagged)]
pub enum SourceOfTruth {
Simple(String),
Full { label: String, href: String },
}
impl SourceOfTruth {
pub fn href(&self) -> &str {
match self {
SourceOfTruth::Simple(h) => h,
SourceOfTruth::Full { href, .. } => href,
}
}
pub fn label(&self) -> &str {
match self {
SourceOfTruth::Simple(h) => h,
SourceOfTruth::Full { label, .. } => label,
}
}
}
#[derive(Debug, Deserialize, Clone)]
#[serde(untagged)]
pub enum RefreshValue {
Prompt(String),
Full(RefreshConfig),
}
#[derive(Debug, Deserialize, Clone)]
pub struct RefreshConfig {
#[serde(default)]
pub mode: RefreshMode,
#[serde(default)]
pub steps: Vec<RefreshStep>,
}
#[derive(Debug, Deserialize, Clone, Default)]
#[serde(rename_all = "lowercase")]
pub enum RefreshMode {
Human,
Auto,
#[default]
Assisted,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "lowercase")]
pub enum RefreshStep {
Run(String),
Prompt(String),
Review(String),
}
#[derive(Deserialize, Clone, Copy, Default)]
#[serde(rename_all = "snake_case")]
pub enum PrintFlow {
#[default]
Slides,
Continuous,
Square,
}
#[derive(Deserialize)]
pub struct Slide {
pub label: String,
pub components: Vec<Component>,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub eyebrow: Option<String>,
#[serde(default)]
pub subtitle: Option<String>,
#[serde(default)]
pub align: Option<String>,
#[serde(default)]
pub valign: Option<String>,
#[serde(default)]
pub cover: bool,
#[serde(default)]
pub hide_label: bool,
}
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Component {
Header {
title: String,
subtitle: Option<String>,
eyebrow: Option<String>,
#[serde(default)]
align: Align,
#[serde(default)]
id: Option<String>,
},
HeroBanner {
title: String,
eyebrow: Option<String>,
subtitle: Option<String>,
buttons: Option<Vec<ButtonConfig>>,
},
Meta {
fields: Vec<MetaField>,
},
CardGrid {
cards: Vec<Card>,
#[serde(default)]
min_width: Option<u32>,
#[serde(default)]
connector: Connector,
},
SelectableGrid {
cards: Vec<SelectableCard>,
#[serde(default)]
interaction: Interaction,
#[serde(default)]
connector: Connector,
},
Timeline {
items: Vec<TimelineItem>,
},
StatGrid {
stats: Vec<Stat>,
#[serde(default = "default_stat_columns")]
columns: u32,
},
BeforeAfter {
items: Vec<BeforeAfterItem>,
#[serde(default)]
before_label: Option<String>,
#[serde(default)]
after_label: Option<String>,
},
SplitCompare {
left: ComparePanel,
right: ComparePanel,
},
Steps {
items: Vec<Step>,
#[serde(default = "default_true")]
numbered: bool,
},
Markdown {
body: String,
},
Table {
columns: Vec<TableColumn>,
rows: Vec<HashMap<String, serde_yaml::Value>>,
#[serde(default)]
filterable: bool,
#[serde(default)]
summary: Option<TableSummary>,
},
Callout {
#[serde(default)]
variant: CalloutVariant,
title: Option<String>,
body: String,
links: Option<Vec<ButtonConfig>>,
},
Code {
language: Option<String>,
code: String,
},
Tabs {
tabs: Vec<Tab>,
},
Section {
heading: Option<String>,
eyebrow: Option<String>,
components: Vec<Component>,
#[serde(default)]
align: Align,
#[serde(default)]
id: Option<String>,
},
Columns {
columns: Vec<Vec<Component>>,
#[serde(default)]
equal_heights: bool,
},
Accordion {
items: Vec<AccordionItem>,
},
EventTimeline {
events: Vec<EventItem>,
#[serde(default)]
default_filter: EventFilter,
#[serde(default)]
show_filter_toggle: bool,
#[serde(default)]
limit: Option<u32>,
#[serde(default)]
filter_by: Vec<String>,
#[serde(default)]
group_by: Option<EventGroupBy>,
},
Tree {
nodes: Vec<TreeNode>,
#[serde(default)]
default_filter: TreeFilter,
#[serde(default)]
show_filter_toggle: bool,
#[serde(default)]
default_collapsed: bool,
#[serde(default)]
default_depth: Option<u32>,
#[serde(default)]
show_counts: bool,
#[serde(default)]
show_summary: bool,
#[serde(default)]
default_view: TreeDefaultView,
},
PriorityQueue {
items: Vec<QueueItem>,
#[serde(default)]
group_by: QueueGroup,
#[serde(default = "default_true")]
show_dates: bool,
#[serde(default = "default_true")]
show_counts: bool,
#[serde(default)]
filterable: bool,
#[serde(default)]
title: Option<String>,
},
Venn {
sets: Vec<VennSet>,
#[serde(default)]
overlaps: Vec<VennOverlap>,
#[serde(default)]
title: Option<String>,
},
Image {
src: String,
alt: Option<String>,
caption: Option<String>,
max_width: Option<u32>,
#[serde(default)]
align: Align,
},
Embed {
src: String,
title: Option<String>,
aspect: Option<String>,
},
Resources {
items: Vec<ResourceItem>,
},
Badge {
label: String,
#[serde(default)]
color: SemColor,
},
Tag {
label: String,
#[serde(default)]
color: SemColor,
},
Divider {
label: Option<String>,
},
Kbd {
keys: Vec<String>,
},
Status {
label: String,
#[serde(default)]
color: SemColor,
},
Breadcrumb {
items: Vec<BreadcrumbItem>,
},
ButtonGroup {
buttons: Vec<ButtonConfig>,
},
DefinitionList {
items: Vec<DefinitionItem>,
},
Blockquote {
body: String,
attribution: Option<String>,
},
Avatar {
name: String,
src: Option<String>,
#[serde(default)]
size: AvatarSize,
subtitle: Option<String>,
},
AvatarGroup {
avatars: Vec<AvatarConfig>,
#[serde(default)]
size: AvatarSize,
#[serde(default = "default_avatar_max")]
max: usize,
},
ProgressBar {
value: u8,
label: Option<String>,
#[serde(default)]
color: SemColor,
detail: Option<String>,
#[serde(default)]
target: Option<u8>,
#[serde(default)]
thresholds: HashMap<String, SemColor>,
},
EmptyState {
title: String,
body: Option<String>,
action: Option<EmptyStateAction>,
#[serde(default)]
icon: Option<String>,
},
Icon {
name: String,
#[serde(default)]
size: IconSize,
#[serde(default)]
color: SemColor,
},
Chart {
kind: ChartKind,
title: Option<String>,
#[serde(default)]
height: Option<u32>,
#[serde(default)]
x_label: Option<String>,
#[serde(default)]
y_label: Option<String>,
#[serde(default)]
orientation: ChartOrientation,
#[serde(default)]
data: Option<Vec<ChartPoint>>,
#[serde(default)]
series: Option<Vec<ChartSeries>>,
},
RoleMap {
#[serde(default)]
title: Option<String>,
},
Sankey {
title: Option<String>,
#[serde(default)]
height: Option<u32>,
flows: Vec<SankeyFlow>,
#[serde(default)]
colors: HashMap<String, SemColor>,
},
Radar {
title: Option<String>,
#[serde(default)]
height: Option<u32>,
axes: Vec<String>,
curves: Vec<RadarCurve>,
#[serde(default)]
max: Option<f64>,
},
Quadrant {
title: Option<String>,
#[serde(default)]
height: Option<u32>,
x_axis: String,
y_axis: String,
quadrants: Vec<String>,
points: Vec<QuadrantPoint>,
},
Architecture {
title: Option<String>,
#[serde(default)]
height: Option<u32>,
#[serde(default)]
direction: ArchDirection,
nodes: Vec<ArchNode>,
connections: Vec<ArchConnection>,
},
Pipeline {
title: Option<String>,
#[serde(default)]
height: Option<u32>,
inputs: Vec<PipelineItem>,
stages: Vec<PipelineStage>,
outputs: Vec<PipelineItem>,
#[serde(default)]
context: Vec<PipelineItem>,
},
Graph {
title: Option<String>,
#[serde(default)]
height: Option<u32>,
#[serde(default)]
direction: ArchDirection,
nodes: Vec<GraphNode>,
#[serde(default)]
edges: Vec<GraphEdge>,
#[serde(default)]
groups: Vec<GraphGroup>,
},
OrgChart {
title: Option<String>,
people: Vec<OrgPerson>,
#[serde(default)]
default_open_depth: Option<u32>,
},
Aside {
body: String,
},
RuleList {
items: Vec<RuleItem>,
},
Gauge {
items: Vec<GaugeItem>,
#[serde(default)]
title: Option<String>,
#[serde(default = "default_gauge_columns")]
columns: u32,
#[serde(default = "default_gauge_max")]
max: f64,
},
}
#[derive(Deserialize)]
pub struct MetaField {
pub key: String,
pub value: String,
}
#[derive(Deserialize)]
pub struct Card {
pub title: String,
pub badge: Option<Badge>,
pub description: Option<String>,
pub links: Option<Vec<Link>>,
pub href: Option<String>,
#[serde(default)]
pub color: SemColor,
}
#[derive(Deserialize)]
pub struct Badge {
pub label: String,
#[serde(default)]
pub color: SemColor,
}
#[derive(Deserialize, Default, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum SemColor {
#[default]
Default,
Green,
Yellow,
Red,
Teal,
}
impl SemColor {
pub fn class_suffix(&self) -> &'static str {
match self {
SemColor::Default => "default",
SemColor::Green => "green",
SemColor::Yellow => "yellow",
SemColor::Red => "red",
SemColor::Teal => "teal",
}
}
pub fn hex(&self) -> &'static str {
match self {
SemColor::Default => "#3CCECE",
SemColor::Green => "#34D399",
SemColor::Yellow => "#FBBF24",
SemColor::Red => "#F87171",
SemColor::Teal => "#3CCECE",
}
}
}
#[derive(Deserialize)]
pub struct Link {
pub label: String,
pub href: String,
}
#[derive(Deserialize)]
pub struct SelectableCard {
pub title: String,
pub eyebrow: Option<String>,
pub bullets: Option<Vec<String>>,
pub body: Option<String>,
#[serde(default)]
pub color: SemColor,
}
#[derive(Deserialize, Default, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum Interaction {
#[default]
SingleSelect,
MultiSelect,
None,
}
#[derive(Deserialize, Default, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum Connector {
#[default]
None,
DotsLine,
Arrow,
}
#[derive(Deserialize)]
pub struct TimelineItem {
pub name: String,
pub status: TimelineStatus,
}
#[derive(Deserialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum TimelineStatus {
Completed,
Active,
Upcoming,
}
#[derive(Deserialize)]
pub struct Stat {
pub label: String,
pub value: String,
pub detail: Option<String>,
#[serde(default)]
pub color: SemColor,
#[serde(default)]
pub trend: Option<Trend>,
#[serde(default)]
pub previous: Option<String>,
#[serde(default)]
pub history: Option<Vec<f64>>,
}
#[derive(Deserialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum Trend {
Up,
Down,
Flat,
}
impl Trend {
pub fn class(&self) -> &'static str {
match self {
Trend::Up => "trend-up",
Trend::Down => "trend-down",
Trend::Flat => "trend-flat",
}
}
pub fn arrow(&self) -> &'static str {
match self {
Trend::Up => "↑",
Trend::Down => "↓",
Trend::Flat => "→",
}
}
}
#[derive(Deserialize)]
pub struct BeforeAfterItem {
pub title: String,
pub before: String,
pub after: String,
pub after_context: Option<String>,
}
#[derive(Deserialize, Clone)]
pub struct ComparePanel {
#[serde(default)]
pub eyebrow: Option<String>,
pub title: String,
pub stats: Vec<CompareStat>,
}
#[derive(Deserialize, Clone)]
pub struct CompareStat {
pub label: String,
pub value: String,
#[serde(default)]
pub color: SemColor,
}
#[derive(Deserialize)]
pub struct Step {
pub title: String,
pub detail: Option<String>,
}
#[derive(Deserialize)]
pub struct TableColumn {
pub key: String,
pub label: String,
#[serde(default)]
pub sortable: bool,
#[serde(default)]
pub align: Align,
#[serde(default)]
pub color_map: HashMap<String, SemColor>,
}
#[derive(Deserialize, Default, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum Align {
#[default]
Left,
Right,
Center,
}
impl Align {
pub fn class(&self) -> &'static str {
match self {
Align::Left => "align-left",
Align::Right => "align-right",
Align::Center => "align-center",
}
}
}
#[derive(Deserialize, Default, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum CalloutVariant {
#[default]
Info,
Warn,
Success,
Danger,
}
impl CalloutVariant {
pub fn class(&self) -> &'static str {
match self {
CalloutVariant::Info => "c-callout-info",
CalloutVariant::Warn => "c-callout-warn",
CalloutVariant::Success => "c-callout-success",
CalloutVariant::Danger => "c-callout-danger",
}
}
}
#[derive(Deserialize)]
pub struct Tab {
pub label: String,
pub components: Vec<Component>,
}
#[derive(Deserialize)]
pub struct AccordionItem {
pub title: String,
pub components: Vec<Component>,
}
#[derive(Deserialize)]
pub struct EventItem {
pub date: String,
pub title: String,
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub severity: EventSeverity,
#[serde(default)]
pub source: Option<String>,
#[serde(default)]
pub link: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Deserialize, Default, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum EventSeverity {
Major,
#[default]
Minor,
Info,
}
impl EventSeverity {
pub fn class(&self) -> &'static str {
match self {
EventSeverity::Major => "severity-major",
EventSeverity::Minor => "severity-minor",
EventSeverity::Info => "severity-info",
}
}
pub fn label(&self) -> &'static str {
match self {
EventSeverity::Major => "major",
EventSeverity::Minor => "minor",
EventSeverity::Info => "info",
}
}
}
#[derive(Deserialize, Default, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum EventFilter {
#[default]
All,
Major,
}
impl EventFilter {
pub fn class(&self) -> &'static str {
match self {
EventFilter::All => "filter-all",
EventFilter::Major => "filter-major",
}
}
pub fn label(&self) -> &'static str {
match self {
EventFilter::All => "all",
EventFilter::Major => "major",
}
}
}
#[derive(Deserialize, Default, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum EventGroupBy {
#[default]
Month,
Quarter,
Source,
}
#[derive(Deserialize, Clone)]
pub struct TreeNode {
pub label: String,
#[serde(default)]
pub status: TreeStatus,
#[serde(default)]
pub note: Option<String>,
#[serde(default)]
pub children: Vec<TreeNode>,
#[serde(default)]
pub owner: Option<String>,
#[serde(default)]
pub due: Option<String>,
#[serde(default)]
pub original_due: Option<String>,
}
#[derive(Deserialize, Default, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum TreeStatus {
#[default]
Default,
Completed,
Active,
Blocked,
Priority,
Upcoming,
}
#[derive(Deserialize, Default, Clone, Copy, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum TreeFilter {
#[default]
All,
Incomplete,
Blocked,
Priority,
Overdue,
}
#[derive(Deserialize, Serialize, Default, Clone, Copy, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum TreeDefaultView {
#[default]
Tree,
Summary,
}
#[derive(Deserialize, Default, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum QueueGroup {
#[default]
Urgency,
Horizon,
Owner,
Status,
None,
}
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum QueueHorizon {
Now,
Next,
Later,
}
#[derive(Deserialize, Clone)]
pub struct QueueTag {
pub label: String,
#[serde(default)]
pub color: SemColor,
#[serde(default)]
pub emphasis: bool,
}
#[derive(Deserialize, Clone)]
pub struct QueueItem {
pub label: String,
#[serde(default)]
pub detail: Option<String>,
#[serde(default)]
pub due: Option<String>,
#[serde(default)]
pub original_due: Option<String>,
#[serde(default)]
pub owner: Option<String>,
#[serde(default)]
pub status: TreeStatus,
#[serde(default)]
pub tags: Vec<QueueTag>,
#[serde(default)]
pub href: Option<String>,
#[serde(default)]
pub horizon: Option<QueueHorizon>,
}
impl TreeFilter {
pub fn class(&self) -> &'static str {
match self {
TreeFilter::All => "filter-all",
TreeFilter::Incomplete => "filter-incomplete",
TreeFilter::Blocked => "filter-blocked",
TreeFilter::Priority => "filter-priority",
TreeFilter::Overdue => "filter-overdue",
}
}
pub fn label(&self) -> &'static str {
match self {
TreeFilter::All => "all",
TreeFilter::Incomplete => "incomplete",
TreeFilter::Blocked => "blocked",
TreeFilter::Priority => "priority",
TreeFilter::Overdue => "overdue",
}
}
}
impl TreeStatus {
pub fn class(&self) -> &'static str {
match self {
TreeStatus::Default => "status-default",
TreeStatus::Completed => "status-completed",
TreeStatus::Active => "status-active",
TreeStatus::Blocked => "status-blocked",
TreeStatus::Priority => "status-priority",
TreeStatus::Upcoming => "status-upcoming",
}
}
pub fn glyph(&self) -> &'static str {
match self {
TreeStatus::Default => "•",
TreeStatus::Completed => "✓",
TreeStatus::Active => "▸",
TreeStatus::Blocked => "⚠",
TreeStatus::Priority => "★",
TreeStatus::Upcoming => "○",
}
}
pub fn label(&self) -> &'static str {
match self {
TreeStatus::Default => "default",
TreeStatus::Completed => "completed",
TreeStatus::Active => "active",
TreeStatus::Blocked => "blocked",
TreeStatus::Priority => "priority",
TreeStatus::Upcoming => "upcoming",
}
}
}
#[derive(Deserialize)]
pub struct TableSummary {
pub group_by: String,
#[serde(default)]
pub colors: HashMap<String, SemColor>,
}
#[derive(Deserialize)]
pub struct GaugeItem {
pub label: String,
pub value: f64,
#[serde(default)]
pub color: SemColor,
}
#[derive(Deserialize)]
pub struct RuleItem {
pub label: String,
pub body: String,
#[serde(default)]
pub color: SemColor,
}
#[derive(Deserialize)]
pub struct VennSet {
pub label: String,
#[serde(default)]
pub color: SemColor,
}
#[derive(Deserialize)]
pub struct VennOverlap {
pub sets: Vec<usize>,
#[serde(default)]
pub label: Option<String>,
}
#[derive(Deserialize)]
pub struct BreadcrumbItem {
pub label: String,
pub href: Option<String>,
}
#[derive(Deserialize, Default, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum ButtonVariant {
#[default]
Primary,
Secondary,
Ghost,
}
#[derive(Deserialize)]
pub struct ButtonConfig {
pub label: String,
pub href: String,
#[serde(default)]
pub variant: ButtonVariant,
#[serde(default)]
pub external: bool,
pub icon: Option<String>,
}
#[derive(Deserialize, Default, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum IconSize {
Xs,
Sm,
#[default]
Md,
Lg,
Xl,
}
impl IconSize {
pub fn pixels(&self) -> u32 {
match self {
IconSize::Xs => 12,
IconSize::Sm => 14,
IconSize::Md => 16,
IconSize::Lg => 20,
IconSize::Xl => 24,
}
}
}
#[derive(Deserialize)]
pub struct DefinitionItem {
pub term: String,
pub definition: String,
}
#[derive(Deserialize)]
pub struct AvatarConfig {
pub name: String,
pub src: Option<String>,
}
#[derive(Deserialize, Default, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum AvatarSize {
Sm,
#[default]
Md,
Lg,
Xl,
}
impl AvatarSize {
pub fn class_suffix(&self) -> &'static str {
match self {
AvatarSize::Sm => "sm",
AvatarSize::Md => "md",
AvatarSize::Lg => "lg",
AvatarSize::Xl => "xl",
}
}
}
#[derive(Deserialize)]
pub struct EmptyStateAction {
pub label: String,
pub href: String,
}
#[derive(Deserialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum ChartKind {
Pie,
Bar,
Timeseries,
}
#[derive(Deserialize, Default, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ChartOrientation {
#[default]
Vertical,
Horizontal,
}
#[derive(Deserialize)]
pub struct ChartPoint {
pub label: String,
pub value: f64,
#[serde(default)]
pub color: Option<SemColor>,
}
#[derive(Deserialize)]
pub struct ChartSeries {
pub label: String,
#[serde(default)]
pub color: Option<SemColor>,
pub points: Vec<ChartPoint>,
}
#[derive(Deserialize)]
pub struct SankeyFlow {
pub source: String,
pub target: String,
pub value: f64,
}
#[derive(Deserialize)]
pub struct RadarCurve {
pub label: String,
pub values: Vec<f64>,
#[serde(default)]
pub color: Option<SemColor>,
}
#[derive(Deserialize)]
pub struct QuadrantPoint {
pub label: String,
pub x: f64,
pub y: f64,
#[serde(default)]
pub color: Option<SemColor>,
}
#[derive(Deserialize, Default, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum ArchDirection {
#[default]
LeftToRight,
TopToBottom,
}
#[derive(Deserialize)]
pub struct ArchNode {
pub id: String,
pub label: String,
#[serde(default)]
pub detail: Option<String>,
#[allow(dead_code)]
#[serde(default)]
pub icon: Option<String>,
#[serde(default)]
pub color: SemColor,
}
#[derive(Deserialize)]
pub struct ArchConnection {
pub from: String,
pub to: String,
#[serde(default)]
pub label: Option<String>,
}
#[derive(Deserialize)]
pub struct PipelineItem {
pub label: String,
#[serde(default)]
pub detail: Option<String>,
#[serde(default)]
pub color: SemColor,
#[serde(default)]
pub dim: bool,
}
#[derive(Deserialize)]
pub struct PipelineStage {
pub label: String,
#[serde(default)]
pub detail: Option<String>,
#[serde(default)]
pub capabilities: Vec<PipelineCapability>,
}
#[derive(Deserialize)]
pub struct PipelineCapability {
pub label: String,
#[serde(default)]
pub detail: Option<String>,
#[serde(default)]
pub dim: bool,
}
#[derive(Deserialize, Default, Clone, Copy, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum GraphShape {
#[default]
Box,
Diamond,
Pill,
}
#[derive(Deserialize, Default, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum GraphEdgeStyle {
#[default]
Solid,
Dashed,
}
#[derive(Deserialize, Clone)]
pub struct OrgPerson {
#[serde(default)]
#[allow(dead_code)]
pub id: Option<String>,
pub name: String,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub color: SemColor,
#[serde(default)]
pub email: Option<String>,
#[serde(default)]
pub linkedin: Option<String>,
#[serde(default)]
pub tags: Vec<OrgTag>,
#[serde(default)]
pub reports: Vec<OrgPerson>,
}
#[derive(Deserialize, Clone)]
pub struct OrgTag {
pub label: String,
#[serde(default)]
pub color: SemColor,
}
#[derive(Deserialize)]
pub struct GraphNode {
pub id: String,
pub label: String,
#[serde(default)]
pub detail: Option<String>,
#[serde(default)]
pub color: SemColor,
#[serde(default)]
pub shape: GraphShape,
#[serde(default)]
pub group: Option<String>,
#[serde(default)]
pub width: Option<u32>,
#[serde(default)]
pub height: Option<u32>,
#[serde(default)]
pub ports: Vec<PortLabel>,
}
#[derive(Deserialize)]
pub struct GraphEdge {
pub from: String,
pub to: String,
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub style: GraphEdgeStyle,
#[serde(default)]
pub color: Option<SemColor>,
}
#[derive(Deserialize)]
pub struct GraphGroup {
pub id: String,
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub parent: Option<String>,
#[serde(default)]
pub color: Option<SemColor>,
}
#[derive(Deserialize, Default, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum PortSide {
#[default]
Right,
Left,
Top,
Bottom,
}
#[derive(Deserialize)]
pub struct PortLabel {
pub side: PortSide,
pub label: String,
}
#[derive(Deserialize)]
pub struct NavLink {
pub label: String,
pub href: Option<String>,
#[serde(default)]
pub children: Option<Vec<NavLink>>,
#[serde(default)]
pub personas: Vec<String>,
#[serde(default)]
pub collapsed: bool,
}
impl NavLink {
pub fn normalize_hrefs(&mut self) {
if let Some(ref mut h) = self.href {
if !h.starts_with('/') && !h.starts_with("http") {
*h = format!("/{h}");
}
}
if let Some(ref mut kids) = self.children {
for child in kids.iter_mut() {
child.normalize_hrefs();
}
}
}
}
#[derive(Deserialize, Default, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NavLayout {
#[default]
Top,
Sidebar,
}
#[derive(Deserialize, Default, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Mode {
#[default]
Dark,
Light,
}
#[derive(Deserialize)]
pub struct SiteConfig {
pub name: String,
pub theme: Option<String>,
#[serde(default)]
pub colors: std::collections::HashMap<String, String>,
pub nav: Option<Vec<NavLink>>,
pub favicon: Option<Favicon>,
#[serde(default)]
pub logo: Option<Logo>,
#[serde(default)]
pub view_source: Option<bool>,
#[serde(default)]
pub texture: Texture,
#[serde(default)]
pub glow: Glow,
#[serde(default)]
pub nav_layout: NavLayout,
#[serde(default)]
pub mode: Mode,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub edit_url: Option<String>,
#[serde(default)]
pub og_image: Option<String>,
#[serde(default)]
pub voice: Option<Voice>,
#[serde(default)]
pub roles: Vec<Role>,
#[serde(default)]
pub drift: Option<DriftConfig>,
}
#[derive(Deserialize, Clone, Default)]
pub struct Voice {
#[serde(default)]
pub tone: Option<String>,
#[serde(default)]
pub reading_level: Option<String>,
#[serde(default)]
pub terminology: Option<Terminology>,
}
#[derive(Deserialize, Clone, Default)]
pub struct Terminology {
#[serde(default)]
pub prefer: std::collections::HashMap<String, String>,
#[serde(default)]
pub avoid: Vec<String>,
}
#[derive(Deserialize, Clone, Default)]
pub struct DriftConfig {
#[serde(default)]
pub repos: Vec<DriftRepo>,
}
#[derive(Deserialize, Clone)]
pub struct DriftRepo {
pub prefix: String,
pub local: String,
}
#[derive(Deserialize, Clone)]
pub struct Role {
pub id: String,
pub label: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub icon: Option<String>,
#[serde(default)]
pub href: Option<String>,
}
#[derive(Deserialize)]
pub struct ResourceItem {
pub title: String,
pub href: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub owner: Option<String>,
}
#[derive(Deserialize, Default, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Texture {
#[default]
None,
Dots,
Grid,
Grain,
Topography,
Diagonal,
}
#[derive(Deserialize, Default, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Glow {
#[default]
None,
Accent,
Corner,
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum Logo {
Simple(String),
Full {
src: String,
#[serde(default)]
height: Option<u32>,
#[serde(default)]
alt: Option<String>,
},
}
impl Logo {
pub fn src(&self) -> &str {
match self {
Logo::Simple(p) => p,
Logo::Full { src, .. } => src,
}
}
pub fn height(&self) -> Option<u32> {
match self {
Logo::Simple(_) => None,
Logo::Full { height, .. } => *height,
}
}
pub fn alt<'a>(&'a self, site_name: &'a str) -> &'a str {
match self {
Logo::Simple(_) => site_name,
Logo::Full { alt, .. } => alt.as_deref().unwrap_or(site_name),
}
}
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum Favicon {
Simple(String),
Full {
svg: Option<String>,
png: Option<String>,
ico: Option<String>,
apple_touch_icon: Option<String>,
},
}
impl Favicon {
pub fn render(&self, base: &str) -> String {
let resolve = |p: &str| crate::render::resolve_href(p, base);
match self {
Favicon::Simple(path) => {
let mime = mime_for(path);
format!(
r#"<link rel="icon" type="{}" href="{}">"#,
mime,
resolve(path)
)
}
Favicon::Full {
svg,
png,
ico,
apple_touch_icon,
} => {
let mut out = String::new();
if let Some(p) = svg {
out.push_str(&format!(
r#"<link rel="icon" type="image/svg+xml" href="{}">"#,
resolve(p)
));
}
if let Some(p) = png {
out.push_str(&format!(
r#"<link rel="icon" type="image/png" href="{}">"#,
resolve(p)
));
}
if let Some(p) = ico {
out.push_str(&format!(
r#"<link rel="icon" type="image/x-icon" href="{}">"#,
resolve(p)
));
}
if let Some(p) = apple_touch_icon {
out.push_str(&format!(
r#"<link rel="apple-touch-icon" href="{}">"#,
resolve(p)
));
}
out
}
}
}
}
fn mime_for(path: &str) -> &'static str {
let lower = path.to_lowercase();
if lower.ends_with(".svg") {
"image/svg+xml"
} else if lower.ends_with(".png") {
"image/png"
} else if lower.ends_with(".ico") {
"image/x-icon"
} else if lower.ends_with(".jpg") || lower.ends_with(".jpeg") {
"image/jpeg"
} else {
"image/png"
}
}
impl SiteConfig {
pub fn resolved_theme(&self) -> crate::theme::Theme {
let base = self.theme.as_deref().unwrap_or("dark");
crate::theme::Theme::named(base, self.mode).with_overrides(&self.colors)
}
}
impl Default for SiteConfig {
fn default() -> Self {
SiteConfig {
name: String::from("My Site"),
theme: None,
colors: std::collections::HashMap::new(),
nav: None,
favicon: None,
logo: None,
view_source: None,
texture: Texture::None,
glow: Glow::None,
nav_layout: NavLayout::Top,
mode: Mode::Dark,
description: None,
url: None,
edit_url: None,
og_image: None,
voice: None,
roles: Vec::new(),
drift: None,
}
}
}
fn default_stat_columns() -> u32 {
3
}
fn default_true() -> bool {
true
}
fn default_avatar_max() -> usize {
5
}
fn default_gauge_columns() -> u32 {
3
}
fn default_gauge_max() -> f64 {
100.0
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Annotation {
pub id: String,
pub text: String,
pub author: String,
#[serde(default)]
pub section: Option<String>,
pub added: String,
#[serde(default)]
pub status: AnnotationStatus,
#[serde(default)]
pub source: AnnotationSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum AnnotationStatus {
#[default]
Pending,
Incorporated,
Ignored,
Stale,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum AnnotationSource {
#[default]
Cli,
Agent,
Web,
}
pub fn value_to_string(v: &serde_yaml::Value) -> String {
match v {
serde_yaml::Value::Null => String::new(),
serde_yaml::Value::Bool(b) => b.to_string(),
serde_yaml::Value::Number(n) => n.to_string(),
serde_yaml::Value::String(s) => s.clone(),
serde_yaml::Value::Sequence(_) | serde_yaml::Value::Mapping(_) => {
serde_yaml::to_string(v).unwrap_or_default()
}
serde_yaml::Value::Tagged(t) => value_to_string(&t.value),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn value_to_string_handles_all_scalar_types() {
use serde_yaml::Value;
assert_eq!(value_to_string(&Value::Null), "");
assert_eq!(value_to_string(&Value::Bool(true)), "true");
assert_eq!(value_to_string(&Value::Number(42.into())), "42");
assert_eq!(value_to_string(&Value::String("hi".into())), "hi");
}
#[test]
fn sem_color_class_suffix() {
assert_eq!(SemColor::Default.class_suffix(), "default");
assert_eq!(SemColor::Green.class_suffix(), "green");
assert_eq!(SemColor::Yellow.class_suffix(), "yellow");
assert_eq!(SemColor::Red.class_suffix(), "red");
assert_eq!(SemColor::Teal.class_suffix(), "teal");
}
#[test]
fn sem_color_hex_values() {
assert_eq!(SemColor::Green.hex(), "#34D399");
assert_eq!(SemColor::Red.hex(), "#F87171");
assert_eq!(SemColor::Default.hex(), "#3CCECE");
}
}