- Move 12 markdown files from root to docs/ subdirectories - Organize documentation by category: • docs/troubleshooting/ (1 file) - Technical troubleshooting guides • docs/deployment/ (4 files) - Deployment and security documentation • docs/guides/ (3 files) - Feature-specific guides • docs/planning/ (4 files) - Planning and improvement proposals Root directory cleanup: - Reduced from 16 to 4 markdown files in root - Only essential project files remain: • CLAUDE.md (AI instructions) • README.md (Main project readme) • CLEANUP_PLAN.md (Current cleanup plan) • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements) This improves: ✅ Documentation discoverability ✅ Logical organization by purpose ✅ Clean root directory ✅ Better maintainability
174 lines
5.4 KiB
PHP
174 lines
5.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\View\Components;
|
|
|
|
use App\Framework\View\Attributes\ComponentName;
|
|
use App\Framework\View\ValueObjects\HtmlAttributes;
|
|
use App\Framework\View\ValueObjects\HtmlElement;
|
|
use App\Framework\View\ValueObjects\HtmlTag;
|
|
use App\Framework\View\ValueObjects\StandardHtmlElement;
|
|
use App\Framework\View\ValueObjects\TagName;
|
|
|
|
#[ComponentName('card')]
|
|
final readonly class Card implements HtmlElement
|
|
{
|
|
public HtmlTag $tag;
|
|
public HtmlAttributes $attributes;
|
|
public string $content;
|
|
|
|
public function __construct(
|
|
string $bodyContent,
|
|
public ?string $title = null,
|
|
public ?string $subtitle = null,
|
|
public ?string $footer = null,
|
|
public ?string $imageSrc = null,
|
|
public ?string $imageAlt = null,
|
|
public string $variant = 'default',
|
|
public HtmlAttributes $additionalAttributes = new HtmlAttributes()
|
|
) {
|
|
$this->tag = new HtmlTag(TagName::DIV);
|
|
|
|
$classes = ['card', "card--{$this->variant}"];
|
|
$this->attributes = HtmlAttributes::empty()
|
|
->withClass(implode(' ', $classes));
|
|
|
|
foreach ($this->additionalAttributes->attributes as $name => $value) {
|
|
$this->attributes = $this->attributes->with($name, $value);
|
|
}
|
|
|
|
$this->content = $this->buildContent($bodyContent);
|
|
}
|
|
|
|
public static function create(string $bodyContent): self
|
|
{
|
|
return new self(bodyContent: $bodyContent);
|
|
}
|
|
|
|
public static function withTitle(string $title, string $bodyContent): self
|
|
{
|
|
return new self(bodyContent: $bodyContent, title: $title);
|
|
}
|
|
|
|
public static function withImage(string $imageSrc, string $bodyContent, ?string $imageAlt = null): self
|
|
{
|
|
return new self(bodyContent: $bodyContent, imageSrc: $imageSrc, imageAlt: $imageAlt);
|
|
}
|
|
|
|
public function withSubtitle(string $subtitle): self
|
|
{
|
|
// Note: Can't access bodyContent from constructor, need to rebuild from content
|
|
return new self(
|
|
bodyContent: '', // Will be rebuilt in buildContent
|
|
title: $this->title,
|
|
subtitle: $subtitle,
|
|
footer: $this->footer,
|
|
imageSrc: $this->imageSrc,
|
|
imageAlt: $this->imageAlt,
|
|
variant: $this->variant,
|
|
additionalAttributes: $this->additionalAttributes
|
|
);
|
|
}
|
|
|
|
public function withFooter(string|HtmlElement $footer): self
|
|
{
|
|
return new self(
|
|
bodyContent: '', // Will be rebuilt in buildContent
|
|
title: $this->title,
|
|
subtitle: $this->subtitle,
|
|
footer: (string) $footer,
|
|
imageSrc: $this->imageSrc,
|
|
imageAlt: $this->imageAlt,
|
|
variant: $this->variant,
|
|
additionalAttributes: $this->additionalAttributes
|
|
);
|
|
}
|
|
|
|
public function withVariant(string $variant): self
|
|
{
|
|
return new self(
|
|
bodyContent: '', // Will be rebuilt in buildContent
|
|
title: $this->title,
|
|
subtitle: $this->subtitle,
|
|
footer: $this->footer,
|
|
imageSrc: $this->imageSrc,
|
|
imageAlt: $this->imageAlt,
|
|
variant: $variant,
|
|
additionalAttributes: $this->additionalAttributes
|
|
);
|
|
}
|
|
|
|
private function buildContent(string $bodyContent): string
|
|
{
|
|
$elements = [];
|
|
|
|
// Image
|
|
if ($this->imageSrc !== null) {
|
|
$imgAttrs = HtmlAttributes::empty()
|
|
->with('src', $this->imageSrc)
|
|
->withClass('card__image');
|
|
|
|
if ($this->imageAlt !== null) {
|
|
$imgAttrs = $imgAttrs->with('alt', $this->imageAlt);
|
|
}
|
|
|
|
$elements[] = StandardHtmlElement::create(TagName::IMG, $imgAttrs);
|
|
}
|
|
|
|
// Header (title + subtitle)
|
|
if ($this->title !== null || $this->subtitle !== null) {
|
|
$headerElements = [];
|
|
|
|
if ($this->title !== null) {
|
|
$headerElements[] = StandardHtmlElement::create(
|
|
TagName::H3,
|
|
HtmlAttributes::empty()->withClass('card__title'),
|
|
$this->title
|
|
);
|
|
}
|
|
|
|
if ($this->subtitle !== null) {
|
|
$headerElements[] = StandardHtmlElement::create(
|
|
TagName::P,
|
|
HtmlAttributes::empty()->withClass('card__subtitle'),
|
|
$this->subtitle
|
|
);
|
|
}
|
|
|
|
$elements[] = StandardHtmlElement::create(
|
|
TagName::DIV,
|
|
HtmlAttributes::empty()->withClass('card__header'),
|
|
implode('', array_map('strval', $headerElements))
|
|
);
|
|
}
|
|
|
|
// Body
|
|
$elements[] = StandardHtmlElement::create(
|
|
TagName::DIV,
|
|
HtmlAttributes::empty()->withClass('card__body'),
|
|
$bodyContent
|
|
);
|
|
|
|
// Footer
|
|
if ($this->footer !== null) {
|
|
$elements[] = StandardHtmlElement::create(
|
|
TagName::DIV,
|
|
HtmlAttributes::empty()->withClass('card__footer'),
|
|
$this->footer
|
|
);
|
|
}
|
|
|
|
return implode('', array_map('strval', $elements));
|
|
}
|
|
|
|
public function __toString(): string
|
|
{
|
|
return (string) StandardHtmlElement::create(
|
|
$this->tag->name,
|
|
$this->attributes,
|
|
$this->content
|
|
);
|
|
}
|
|
}
|