///|
/// Validation policy for component contracts.
pub struct ComponentPolicy {
min_anatomy_parts : Int
min_states : Int
min_tokens : Int
require_purpose : Bool
require_focus_state : Bool
require_disabled_state : Bool
} derive(Debug, Eq)
///|
pub fn ComponentPolicy::default() -> ComponentPolicy {
{
min_anatomy_parts: 1,
min_states: 1,
min_tokens: 1,
require_purpose: true,
require_focus_state: false,
require_disabled_state: false,
}
}
///|
pub fn ComponentPolicy::strict() -> ComponentPolicy {
{
min_anatomy_parts: 2,
min_states: 2,
min_tokens: 1,
require_purpose: true,
require_focus_state: false,
require_disabled_state: false,
}
}
///|
pub fn ComponentPolicy::with_minimums(
self : ComponentPolicy,
anatomy : Int,
states : Int,
tokens : Int,
) -> ComponentPolicy {
{
..self,
min_anatomy_parts: if anatomy < 0 {
0
} else {
anatomy
},
min_states: if states < 0 {
0
} else {
states
},
min_tokens: if tokens < 0 {
0
} else {
tokens
},
}
}
///|
pub fn ComponentPolicy::requiring_focus(
self : ComponentPolicy,
required : Bool,
) -> ComponentPolicy {
{ ..self, require_focus_state: required }
}
///|
pub fn ComponentPolicy::requiring_disabled(
self : ComponentPolicy,
required : Bool,
) -> ComponentPolicy {
{ ..self, require_disabled_state: required }
}
///|
pub fn ComponentSpec::has_state(self : ComponentSpec, state : String) -> Bool {
let target = normalize_identifier(state)
self.states.any(fn(value) { normalize_identifier(value) == target })
}
///|
pub fn ComponentSpec::has_token(self : ComponentSpec, token : String) -> Bool {
let target = normalize_identifier(token)
self.tokens.any(fn(value) { normalize_identifier(value) == target })
}
///|
pub fn ComponentSpec::state_names(self : ComponentSpec) -> Array[String] {
self.states
}
///|
pub fn ComponentSpec::token_names(self : ComponentSpec) -> Array[String] {
self.tokens
}
///|
pub fn ComponentSpec::anatomy_names(self : ComponentSpec) -> Array[String] {
self.anatomy
}
///|
pub fn ComponentSpec::slug(self : ComponentSpec) -> String {
normalize_identifier(self.name)
}
///|
pub fn ComponentSpec::is_documented(self : ComponentSpec) -> Bool {
self.name.trim().to_owned() != "" && self.purpose.trim().to_owned() != ""
}
///|
pub fn ComponentSpec::quality_score(self : ComponentSpec) -> Int {
let mut score = 0
if self.name.trim().to_owned() != "" {
score += 20
}
if self.purpose.trim().to_owned() != "" {
score += 20
}
if self.anatomy.length() >= 2 {
score += 20
} else if self.anatomy.length() == 1 {
score += 10
}
if self.tokens.length() >= 1 {
score += 20
}
if self.states.length() >= 2 {
score += 20
} else if self.states.length() == 1 {
score += 10
}
score
}
///|
pub fn ComponentSpec::audit(
self : ComponentSpec,
policy : ComponentPolicy,
path : String,
) -> AuditReport {
let findings : Array[Finding] = []
if self.name.trim().to_owned() == "" {
findings.push(Finding::{
severity: Error,
path: path + ".name",
message: "component name cannot be empty",
})
}
if policy.require_purpose && self.purpose.trim().to_owned() == "" {
findings.push(Finding::{
severity: Error,
path: path + ".purpose",
message: "component purpose is required",
})
}
if self.anatomy.length() < policy.min_anatomy_parts {
findings.push(Finding::{
severity: Error,
path: path + ".anatomy",
message: "component anatomy does not describe enough parts",
})
}
if self.states.length() < policy.min_states {
findings.push(Finding::{
severity: Error,
path: path + ".states",
message: "component must document enough interaction states",
})
}
if self.tokens.length() < policy.min_tokens {
findings.push(Finding::{
severity: Error,
path: path + ".tokens",
message: "component must reference at least one design token",
})
}
if policy.require_focus_state &&
!self.has_state("focus") &&
!self.has_state("focus-visible") {
findings.push(Finding::{
severity: Error,
path: path + ".states",
message: "interactive component must define focus behavior",
})
}
if policy.require_disabled_state && !self.has_state("disabled") {
findings.push(Finding::{
severity: Error,
path: path + ".states",
message: "interactive component must define disabled behavior",
})
}
AuditReport::from_findings(findings)
}
///|
pub fn ComponentSpec::contract_markdown(self : ComponentSpec) -> String {
let lines : Array[String] = [
"## Contract: " + self.name,
"",
self.purpose,
"",
"### Anatomy",
]
for part in self.anatomy {
lines.push("- " + part)
}
lines.push("")
lines.push("### Tokens")
for token in self.tokens {
lines.push("- `" + token + "`")
}
lines.push("")
lines.push("### States")
for state in self.states {
lines.push("- " + state)
}
lines.join("\n")
}
///|
pub fn ComponentSpec::contract_json(self : ComponentSpec) -> String {
"{\"name\":\"" +
json_escape(self.name) +
"\",\"purpose\":\"" +
json_escape(self.purpose) +
"\",\"anatomy\":[" +
string_array_json(self.anatomy) +
"],\"tokens\":[" +
string_array_json(self.tokens) +
"],\"states\":[" +
string_array_json(self.states) +
"]}"
}
///|
pub fn ComponentSpec::state_matrix(self : ComponentSpec) -> Array[String] {
self.states.map(fn(state) { self.name + " / " + state })
}
///|
pub fn ComponentSpec::token_usage(
self : ComponentSpec,
token : String,
) -> String {
if self.has_token(token) {
"required by " + self.name
} else {
"not referenced by " + self.name
}
}
///|
pub fn BrandBook::audit_components(
self : BrandBook,
policy : ComponentPolicy,
) -> AuditReport {
let findings : Array[Finding] = []
if self.components.length() == 0 {
findings.push(Finding::{
severity: Error,
path: "components",
message: "at least one component contract is required",
})
}
for index, component in self.components {
let report = component.audit(
policy,
"components[" + index.to_string() + "]",
)
for finding in report.findings {
findings.push(finding)
}
}
for left_index in 0.. Int {
if self.components.length() == 0 {
0
} else {
self.components.fold(init=0, fn(total, component) {
total + component.quality_score()
}) /
self.components.length()
}
}
///|
pub fn BrandBook::component_contracts_markdown(self : BrandBook) -> String {
let sections : Array[String] = ["# Component contracts", ""]
for component in self.components {
sections.push(component.contract_markdown())
sections.push("")
}
sections.join("\n")
}
///|
pub fn BrandBook::component_state_names(self : BrandBook) -> Array[String] {
let result : Array[String] = []
for component in self.components {
for state in component.states {
if !result.contains(state) {
result.push(state)
}
}
}
result
}
///|
pub fn BrandBook::component_token_names(self : BrandBook) -> Array[String] {
let result : Array[String] = []
for component in self.components {
for token in component.tokens {
if !result.contains(token) {
result.push(token)
}
}
}
result
}
///|
pub fn BrandBook::find_components_by_token(
self : BrandBook,
token : String,
) -> Array[ComponentSpec] {
self.components.filter(fn(component) { component.has_token(token) })
}
///|
pub fn BrandBook::find_components_by_state(
self : BrandBook,
state : String,
) -> Array[ComponentSpec] {
self.components.filter(fn(component) { component.has_state(state) })
}
///|
fn string_array_json(values : Array[String]) -> String {
values.map(fn(value) { "\"" + json_escape(value) + "\"" }).join(",")
}