style updates
This commit is contained in:
@@ -104,8 +104,8 @@ final class ManifestValidator
|
||||
* Compact lists such as ["light", "dark"] are accepted and converted to
|
||||
* keyed empty definitions. Object definitions remain unchanged.
|
||||
*
|
||||
* @param array<int|string, mixed> $value
|
||||
* @param array<int, string> $errors
|
||||
* @param array<int|string, mixed> $value
|
||||
* @param array<int, string> $errors
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
private function normaliseDefinitions(array $value, string $field, array &$errors, bool $allowCompact = true): array
|
||||
@@ -125,10 +125,12 @@ final class ManifestValidator
|
||||
foreach ($value as $name) {
|
||||
if (! is_string($name) || ! preg_match('/^[a-z0-9][a-z0-9_-]*$/', $name)) {
|
||||
$errors[] = "Every {$field} entry must be a valid lowercase key.";
|
||||
|
||||
continue;
|
||||
}
|
||||
$normalised[$name] = [];
|
||||
}
|
||||
|
||||
return $normalised;
|
||||
}
|
||||
|
||||
@@ -136,6 +138,7 @@ final class ManifestValidator
|
||||
foreach ($value as $name => $definition) {
|
||||
if (! is_string($name) || ! preg_match('/^[a-z0-9][a-z0-9_-]*$/', $name) || ! is_array($definition)) {
|
||||
$errors[] = "Every {$field} definition must use a valid lowercase key and an object value.";
|
||||
|
||||
continue;
|
||||
}
|
||||
$normalised[$name] = $definition;
|
||||
@@ -145,8 +148,8 @@ final class ManifestValidator
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string, mixed> $value
|
||||
* @param array<int, string> $errors
|
||||
* @param array<int|string, mixed> $value
|
||||
* @param array<int, string> $errors
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
private function normaliseCapabilities(array $value, array &$errors): array
|
||||
@@ -156,6 +159,7 @@ final class ManifestValidator
|
||||
foreach ($value as $name => $enabled) {
|
||||
if (! is_string($name) || ! preg_match('/^[a-z0-9][a-z0-9_-]*$/', $name) || ! is_bool($enabled)) {
|
||||
$errors[] = 'Every capability must use a valid lowercase key and a boolean value.';
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,13 @@ final class Stylist
|
||||
public function current(): Theme
|
||||
{
|
||||
$requested = $this->resolver->resolve();
|
||||
try { return $this->loader->find($requested); }
|
||||
catch (ThemeNotFoundException) {
|
||||
try {
|
||||
return $this->loader->find($requested);
|
||||
} catch (ThemeNotFoundException) {
|
||||
$fallback = (string) config('stylist.default_theme', 'default');
|
||||
if ($requested !== $fallback) return $this->loader->find($fallback);
|
||||
if ($requested !== $fallback) {
|
||||
return $this->loader->find($fallback);
|
||||
}
|
||||
throw ThemeNotFoundException::forKey($requested);
|
||||
}
|
||||
}
|
||||
@@ -44,21 +47,45 @@ final class Stylist
|
||||
);
|
||||
}
|
||||
|
||||
public function settings(): array { return $this->settings->get($this->current()); }
|
||||
public function variant(): string { return $this->settings->variant($this->current()); }
|
||||
public function appearance(): string { return $this->settings->appearance($this->current()); }
|
||||
public function accent(): string { return $this->settings->accent($this->current()); }
|
||||
public function all() { return $this->loader->all(); }
|
||||
public function settings(): array
|
||||
{
|
||||
return $this->settings->get($this->current());
|
||||
}
|
||||
|
||||
public function variant(): string
|
||||
{
|
||||
return $this->settings->variant($this->current());
|
||||
}
|
||||
|
||||
public function appearance(): string
|
||||
{
|
||||
return $this->settings->appearance($this->current());
|
||||
}
|
||||
|
||||
public function accent(): string
|
||||
{
|
||||
return $this->settings->accent($this->current());
|
||||
}
|
||||
|
||||
public function all()
|
||||
{
|
||||
return $this->loader->all();
|
||||
}
|
||||
|
||||
private function inheritedVariables(Theme $theme, array $seen = []): array
|
||||
{
|
||||
if (in_array($theme->key(), $seen, true)) return [];
|
||||
if (in_array($theme->key(), $seen, true)) {
|
||||
return [];
|
||||
}
|
||||
$seen[] = $theme->key();
|
||||
$parent = [];
|
||||
if ($theme->parent()) {
|
||||
try { $parent = $this->inheritedVariables($this->loader->find($theme->parent()), $seen); }
|
||||
catch (ThemeNotFoundException) {}
|
||||
try {
|
||||
$parent = $this->inheritedVariables($this->loader->find($theme->parent()), $seen);
|
||||
} catch (ThemeNotFoundException) {
|
||||
}
|
||||
}
|
||||
|
||||
return array_replace($parent, $theme->variables());
|
||||
}
|
||||
}
|
||||
|
||||
+137
-27
@@ -14,33 +14,143 @@ final class Theme
|
||||
private readonly array $meta = [],
|
||||
) {}
|
||||
|
||||
public function key(): string { return $this->key; }
|
||||
public function name(): string { return $this->name; }
|
||||
public function path(): string { return $this->path; }
|
||||
public function stylesheet(): string { return $this->stylesheet; }
|
||||
public function script(): string { return $this->script; }
|
||||
public function variables(): array { return $this->variables; }
|
||||
public function meta(): array { return $this->meta; }
|
||||
public function manifestVersion(): int { return (int) ($this->meta['manifest_version'] ?? 1); }
|
||||
public function version(): string { return (string) ($this->meta['version'] ?? '1.0.0'); }
|
||||
public function description(): string { return (string) ($this->meta['description'] ?? ''); }
|
||||
public function author(): string { return (string) ($this->meta['author'] ?? ''); }
|
||||
public function parent(): ?string { $parent = $this->meta['extends'] ?? null; return is_string($parent) && $parent !== '' ? $parent : null; }
|
||||
public function preview(): ?string { $preview = $this->meta['preview'] ?? null; return is_string($preview) && $preview !== '' ? $preview : null; }
|
||||
public function variants(): array { return (array) ($this->meta['variants'] ?? []); }
|
||||
public function appearances(): array { return (array) ($this->meta['appearances'] ?? []); }
|
||||
public function accents(): array { return (array) ($this->meta['accents'] ?? []); }
|
||||
public function defaultAppearance(): string { return (string) ($this->meta['default_appearance'] ?? array_key_first($this->appearances()) ?? ''); }
|
||||
public function defaultAccent(): string { return (string) ($this->meta['default_accent'] ?? array_key_first($this->accents()) ?? ''); }
|
||||
public function settingsSchema(): array { return (array) ($this->meta['settings'] ?? []); }
|
||||
public function seo(): array { return (array) ($this->meta['seo'] ?? []); }
|
||||
public function entryView(): string { return (string) ($this->meta['entry_view'] ?? 'home'); }
|
||||
public function capabilities(): array { return (array) ($this->meta['capabilities'] ?? []); }
|
||||
public function hasCapability(string $feature): bool { return (bool) ($this->capabilities()[$feature] ?? false); }
|
||||
public function supports(string $feature): bool { return (bool) data_get($this->meta, 'supports.'.$feature, $this->hasCapability($feature)); }
|
||||
public function has(string $relativePath): bool { return is_file($this->path.DIRECTORY_SEPARATOR.ltrim($relativePath, '/')); }
|
||||
public function absolutePath(string $relativePath): string { return $this->path.DIRECTORY_SEPARATOR.ltrim($relativePath, '/'); }
|
||||
public function key(): string
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function path(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
public function stylesheet(): string
|
||||
{
|
||||
return $this->stylesheet;
|
||||
}
|
||||
|
||||
public function script(): string
|
||||
{
|
||||
return $this->script;
|
||||
}
|
||||
|
||||
public function variables(): array
|
||||
{
|
||||
return $this->variables;
|
||||
}
|
||||
|
||||
public function meta(): array
|
||||
{
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
public function manifestVersion(): int
|
||||
{
|
||||
return (int) ($this->meta['manifest_version'] ?? 1);
|
||||
}
|
||||
|
||||
public function version(): string
|
||||
{
|
||||
return (string) ($this->meta['version'] ?? '1.0.0');
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return (string) ($this->meta['description'] ?? '');
|
||||
}
|
||||
|
||||
public function author(): string
|
||||
{
|
||||
return (string) ($this->meta['author'] ?? '');
|
||||
}
|
||||
|
||||
public function parent(): ?string
|
||||
{
|
||||
$parent = $this->meta['extends'] ?? null;
|
||||
|
||||
return is_string($parent) && $parent !== '' ? $parent : null;
|
||||
}
|
||||
|
||||
public function preview(): ?string
|
||||
{
|
||||
$preview = $this->meta['preview'] ?? null;
|
||||
|
||||
return is_string($preview) && $preview !== '' ? $preview : null;
|
||||
}
|
||||
|
||||
public function variants(): array
|
||||
{
|
||||
return (array) ($this->meta['variants'] ?? []);
|
||||
}
|
||||
|
||||
public function appearances(): array
|
||||
{
|
||||
return (array) ($this->meta['appearances'] ?? []);
|
||||
}
|
||||
|
||||
public function accents(): array
|
||||
{
|
||||
return (array) ($this->meta['accents'] ?? []);
|
||||
}
|
||||
|
||||
public function defaultAppearance(): string
|
||||
{
|
||||
return (string) ($this->meta['default_appearance'] ?? array_key_first($this->appearances()) ?? '');
|
||||
}
|
||||
|
||||
public function defaultAccent(): string
|
||||
{
|
||||
return (string) ($this->meta['default_accent'] ?? array_key_first($this->accents()) ?? '');
|
||||
}
|
||||
|
||||
public function settingsSchema(): array
|
||||
{
|
||||
return (array) ($this->meta['settings'] ?? []);
|
||||
}
|
||||
|
||||
public function seo(): array
|
||||
{
|
||||
return (array) ($this->meta['seo'] ?? []);
|
||||
}
|
||||
|
||||
public function entryView(): string
|
||||
{
|
||||
return (string) ($this->meta['entry_view'] ?? 'home');
|
||||
}
|
||||
|
||||
public function capabilities(): array
|
||||
{
|
||||
return (array) ($this->meta['capabilities'] ?? []);
|
||||
}
|
||||
|
||||
public function hasCapability(string $feature): bool
|
||||
{
|
||||
return (bool) ($this->capabilities()[$feature] ?? false);
|
||||
}
|
||||
|
||||
public function supports(string $feature): bool
|
||||
{
|
||||
return (bool) data_get($this->meta, 'supports.'.$feature, $this->hasCapability($feature));
|
||||
}
|
||||
|
||||
public function has(string $relativePath): bool
|
||||
{
|
||||
return is_file($this->path.DIRECTORY_SEPARATOR.ltrim($relativePath, '/'));
|
||||
}
|
||||
|
||||
public function absolutePath(string $relativePath): string
|
||||
{
|
||||
return $this->path.DIRECTORY_SEPARATOR.ltrim($relativePath, '/');
|
||||
}
|
||||
|
||||
/** @deprecated Theme assets are served through UrlGenerator. */
|
||||
public function assetsPath(): string { return 'stylist/theme-assets/'.$this->key; }
|
||||
public function assetsPath(): string
|
||||
{
|
||||
return 'stylist/theme-assets/'.$this->key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,16 @@ namespace Modules\Stylist\Theme;
|
||||
final class UrlGenerator
|
||||
{
|
||||
public function __construct(private readonly Stylist $stylist) {}
|
||||
public function asset(string $path): string { return $this->forTheme($this->stylist->current(), $path); }
|
||||
|
||||
public function asset(string $path): string
|
||||
{
|
||||
return $this->forTheme($this->stylist->current(), $path);
|
||||
}
|
||||
|
||||
public function forTheme(Theme|string $theme, string $path): string
|
||||
{
|
||||
$key = $theme instanceof Theme ? $theme->key() : $theme;
|
||||
|
||||
return route('stylist.theme.asset', ['theme' => $key, 'path' => ltrim($path, '/')]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user