names * them. `standard` is the scheme's own level and has no attribute. * * @var list */ public const LEVELS = ['standard', 'medium', 'high']; /** * @var (Closure(): ?string)|null */ protected static ?Closure $profileResolver = null; /** * Name the active colour profile with the given closure, asked each time a colour is drawn. * * @param (Closure(): ?string)|null $resolver */ public static function resolveProfileUsing(?Closure $resolver): void { static::$profileResolver = $resolver; } /** * The roles to draw: the given profile's, the active profile's, or the single scheme's, at * the given contrast level (`standard`, `medium` or `high`; anything else is standard). * * @return array{light: array, dark: array} */ public static function load(?string $path = null, ?string $profile = null, string $contrast = 'standard'): array { $data = static::data($path); $profiles = static::profilesFrom($data, $contrast); if ($profiles !== []) { $name = $profile !== null && isset($profiles[$profile]) ? $profile : static::activeFrom($data, $profiles); return ['light' => $profiles[$name]['light'], 'dark' => $profiles[$name]['dark']]; } return static::scheme($data, $contrast); } /** * The light roles: what a mail wears. * * @return array */ public static function light(?string $path = null, ?string $profile = null, string $contrast = 'standard'): array { return static::load($path, $profile, $contrast)['light']; } /** * The generated colour profiles, in the order they were configured; empty for a single scheme. * * @return array, dark: array}> */ public static function profiles(?string $path = null, string $contrast = 'standard'): array { return static::profilesFrom(static::data($path), $contrast); } /** * The active profile's name, or null for a single scheme. */ public static function profile(?string $path = null): ?string { $data = static::data($path); $profiles = static::profilesFrom($data); return $profiles === [] ? null : static::activeFrom($data, $profiles); } /** * @return array */ protected static function data(?string $path): array { $path ??= (string) config('livewire-material.scheme'); $data = is_file($path) ? json_decode((string) file_get_contents($path), true) : null; return is_array($data) ? $data : []; } /** * @param array $data * @return array, dark: array}> */ protected static function profilesFrom(array $data, string $contrast = 'standard'): array { if (! is_array($data['profiles'] ?? null)) { return []; } $profiles = []; foreach ($data['profiles'] as $name => $profile) { if (! is_string($name) || preg_match('/^[a-z0-9-]+$/', $name) !== 1 || ! is_array($profile)) { continue; } $profiles[$name] = [ 'label' => is_string($profile['label'] ?? null) ? $profile['label'] : Str::headline($name), ...static::scheme($profile, $contrast), ]; } return $profiles; } /** * One scheme's roles at one contrast level: the level's own, over the standard ones it does * not name, over the package's default. A scheme file written before the levels existed has * only the standard roles, and draws them at every level rather than nothing. * * @param array $data * @return array{light: array, dark: array} */ protected static function scheme(array $data, string $contrast): array { $default = static::defaultScheme($contrast); $level = is_array($data['contrast'][$contrast] ?? null) ? $data['contrast'][$contrast] : []; return [ 'light' => [...$default['light'], ...static::roles($data['light'] ?? null), ...static::roles($level['light'] ?? null)], 'dark' => [...$default['dark'], ...static::roles($data['dark'] ?? null), ...static::roles($level['dark'] ?? null)], ]; } /** * @param array $data * @param non-empty-array $profiles */ protected static function activeFrom(array $data, array $profiles): string { try { $resolved = static::$profileResolver ? (static::$profileResolver)() : null; } catch (Throwable) { $resolved = null; } foreach ([$resolved, $data['default'] ?? null] as $candidate) { if (is_string($candidate) && isset($profiles[$candidate])) { return $candidate; } } return (string) array_key_first($profiles); } /** * The package's own scheme at one contrast level, which fills any role a file lacks. * * @return array{light: array, dark: array} */ protected static function defaultScheme(string $contrast = 'standard'): array { $data = json_decode((string) file_get_contents(dirname(__DIR__, 2).'/resources/css/tokens/scheme.json'), true); $level = is_array($data['contrast'][$contrast] ?? null) ? $data['contrast'][$contrast] : []; return [ 'light' => [...static::roles($data['light'] ?? null), ...static::roles($level['light'] ?? null)], 'dark' => [...static::roles($data['dark'] ?? null), ...static::roles($level['dark'] ?? null)], ]; } /** * @return array */ protected static function roles(mixed $roles): array { if (! is_array($roles)) { return []; } return array_filter( $roles, fn (mixed $hex, mixed $role): bool => is_string($role) && preg_match('/^[a-z-]+$/', $role) === 1 && is_string($hex) && preg_match('/^#[0-9a-fA-F]{6}$/', $hex) === 1, ARRAY_FILTER_USE_BOTH, ); } }