option('output') ?: resource_path('css/material-scheme.css'); $data = preg_replace('/\.css$/', '', $stylesheet).'.json'; if (filled($this->argument('seed'))) { $scheme = $this->generate((string) $this->argument('seed'), (string) $this->option('variant'), (float) $this->option('contrast')); if ($scheme === null) { return self::FAILURE; } return $this->write($files, $stylesheet, $data, $this->stylesheet($scheme), $scheme); } $profiles = config('livewire-material.profiles'); if (! is_array($profiles) || $profiles === []) { $this->components->error('Give a seed colour (php artisan material:scheme "#4f46e5"), or list colour profiles in livewire-material.profiles.'); return self::FAILURE; } $generated = []; foreach ($profiles as $name => $profile) { if (! is_string($name) || preg_match('/^[a-z0-9-]+$/', $name) !== 1) { $this->components->error("A profile's name is lowercase letters, digits and dashes; \"{$name}\" is not."); return self::FAILURE; } $scheme = $this->generate((string) ($profile['seed'] ?? ''), (string) ($profile['variant'] ?? 'tonal-spot'), (float) ($profile['contrast'] ?? 0), "Profile \"{$name}\": "); if ($scheme === null) { return self::FAILURE; } $generated[$name] = ['label' => (string) ($profile['label'] ?? Str::headline($name)), ...$scheme]; } $configured = config('livewire-material.profile'); $default = is_string($configured) && isset($generated[$configured]) ? $configured : array_key_first($generated); return $this->write($files, $stylesheet, $data, $this->profilesStylesheet($generated, $default), [ ...collect($generated[$default])->except('label')->all(), 'default' => $default, 'profiles' => $generated, ]); } /** * One scheme from Google's colour utilities, or null once the reason has been shown. * * @return array{seed: string, variant: string, spec: string, contrast: float, light: array, dark: array}|null */ protected function generate(string $seed, string $variant, float $contrast, string $context = ''): ?array { $result = Process::run([ config('livewire-material.node', 'node'), __DIR__.'/../../resources/node/scheme.mjs', json_encode([ 'seed' => $seed, 'variant' => $variant, 'contrast' => $contrast, 'success' => $this->option('success'), 'warning' => $this->option('warning'), 'info' => $this->option('info'), ]), ]); if ($result->failed()) { $this->components->error($context.(trim($result->errorOutput()) ?: 'Node could not run the scheme generator. Is `node` installed and on the PATH?')); return null; } try { return json_decode($result->output(), true, flags: JSON_THROW_ON_ERROR); } catch (JsonException) { $this->components->error($context.'The scheme generator answered with something other than JSON.'); return null; } } /** * @param array $scheme */ protected function write(Filesystem $files, string $stylesheet, string $data, string $css, array $scheme): int { $files->ensureDirectoryExists(dirname($stylesheet)); $files->put($stylesheet, $css); $files->put($data, json_encode($scheme, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n"); $this->components->info("Wrote {$stylesheet} and {$data}."); return self::SUCCESS; } /** * @param array{seed: string, variant: string, spec: string, contrast: float, light: array, dark: array} $scheme */ protected function stylesheet(array $scheme): string { $command = sprintf( 'php artisan material:scheme "%s" --variant=%s%s', $scheme['seed'], $scheme['variant'], $scheme['contrast'] != 0 ? ' --contrast='.$scheme['contrast'] : '', ); $blocks = $this->blocks([':root', "[data-theme='light']"], ["[data-theme='dark']"], $scheme); return <<, dark: array}> $profiles */ protected function profilesStylesheet(array $profiles, string $default): string { $list = collect($profiles) ->map(fn (array $profile, string $name): string => sprintf( ' * %-12s %s, %s%s', $name, $profile['seed'], $profile['variant'], $profile['contrast'] != 0 ? ', contrast '.$profile['contrast'] : '', )) ->implode("\n"); $css = <<blocks([':root', "[data-theme='light']"], ["[data-theme='dark']"], $profiles[$default]); foreach ($profiles as $name => $profile) { $css .= "\n".$this->blocks( ["[data-scheme='{$name}']", "[data-scheme='{$name}'][data-theme='light']", "[data-scheme='{$name}'] [data-theme='light']"], ["[data-scheme='{$name}'][data-theme='dark']", "[data-scheme='{$name}'] [data-theme='dark']"], $profile, ); } return $css; } /** * A light and a dark block of roles under the given selectors. * * @param list $light * @param list $dark * @param array{light: array, dark: array} $scheme */ protected function blocks(array $light, array $dark, array $scheme): string { $roles = fn (array $roles): string => collect($roles) ->map(fn (string $hex, string $role): string => " --md-sys-color-{$role}: {$hex};") ->implode("\n"); return implode(",\n", $light)." {\n color-scheme: light;\n\n".$roles($scheme['light'])."\n}\n\n" .implode(",\n", $dark)." {\n color-scheme: dark;\n\n".$roles($scheme['dark'])."\n}\n"; } }