Spaces:
Sleeping
Sleeping
Download bridge/src/Compiler.php from acecalisto3/UIKitV3-Automator: direct link, hf CLI and curl.
- Browser
- Download file 42.5 kB
-
https://huggingface.co/spaces/acecalisto3/UIKitV3-Automator/resolve/main/bridge/src/Compiler.php
- Command line
-
hf download hf://spaces/acecalisto3/UIKitV3-Automator/bridge/src/Compiler.php
-
curl -L -o Compiler.php https://huggingface.co/spaces/acecalisto3/UIKitV3-Automator/resolve/main/bridge/src/Compiler.php
42.5 kB
| /** | |
| * Compiles a token graph into a YOOtheme Pro child theme. | |
| * | |
| * Child themes are YOOtheme's documented extension point for custom Less, so the | |
| * import lands as real style variables the customizer keeps honouring, not as | |
| * overriding CSS bolted on afterwards. | |
| */ | |
| namespace Fyoo\FigmaBridge; | |
| class Compiler | |
| { | |
| private Tokens $tokens; | |
| private Mapper $mapper; | |
| private array $resolved; | |
| private array $assetFiles = []; | |
| private string $stylesheet = ''; | |
| public function __construct(Tokens $tokens, ?Mapper $mapper = null) | |
| { | |
| $this->tokens = $tokens; | |
| $this->mapper = $mapper ?: new Mapper(); | |
| $this->resolved = $this->mapper->resolve($tokens); | |
| } | |
| /** name => source path, for imports whose assets are real files rather than inline data. */ | |
| public function withAssetFiles(array $files): self | |
| { | |
| $this->assetFiles = $files; | |
| return $this; | |
| } | |
| /** | |
| * Ships a stylesheet as the child theme's css/theme.css. Variables carry the | |
| * design system; this carries the layout and component CSS they cannot express. | |
| */ | |
| public function withStylesheet(string $css): self | |
| { | |
| $this->stylesheet = trim($css); | |
| return $this; | |
| } | |
| public function resolution(): array | |
| { | |
| return $this->resolved; | |
| } | |
| public function slug(): string | |
| { | |
| $slug = strtolower(preg_replace('~[^A-Za-z0-9]+~', '_', $this->tokens->data['name'])); | |
| return trim($slug, '_') ?: 'figma_import'; | |
| } | |
| public function less(): string | |
| { | |
| $name = $this->tokens->data['name']; | |
| $out = [ | |
| '//', | |
| '// ' . $name . ' — generated by Figma → YOOtheme Style Bridge', | |
| '// Source: ' . $this->tokens->data['source'] . ' · ' . $this->tokens->data['generatedAt'], | |
| '// Edit the tokens in Figma and re-import; local edits below are overwritten.', | |
| '//', | |
| '', | |
| '// ---------- Design tokens ----------', | |
| ]; | |
| foreach (Tokens::GROUPS as $group) { | |
| $lines = []; | |
| foreach ($this->tokens->group($group) as $key => $value) { | |
| if ($group === 'typography') { | |
| foreach ($value as $prop => $propValue) { | |
| $lines[] = sprintf('@fyoo-%s-%s-%s: %s;', $group, $key, $this->dash($prop), $this->quote($prop, $propValue)); | |
| } | |
| } else { | |
| $lines[] = sprintf('@fyoo-%s-%s: %s;', $group, $key, $value); | |
| } | |
| } | |
| if ($lines) { | |
| $out[] = ''; | |
| $out[] = '// ' . $group; | |
| $out = array_merge($out, $lines); | |
| } | |
| } | |
| $out[] = ''; | |
| $out[] = '// ---------- YOOtheme global variables ----------'; | |
| $rules = []; | |
| foreach ($this->resolved['mapped'] as $item) { | |
| $out = array_merge($out, $this->emit($item, $rules)); | |
| } | |
| if ($rules) { | |
| $out[] = ''; | |
| $out[] = '// ---------- Component rules (no matching Less variable upstream) ----------'; | |
| $out = array_merge($out, $rules); | |
| } | |
| $out[] = ''; | |
| return implode("\n", $out) . "\n"; | |
| } | |
| /** @param array $rules collected by reference: CSS for design intent with no variable to carry it */ | |
| private function emit(array $item, array &$rules): array | |
| { | |
| $ref = sprintf('@fyoo-%s-%s', $item['group'], $item['key']); | |
| if ($item['group'] !== 'typography') { | |
| $lines = ["// {$item['role']} ← {$item['key']}"]; | |
| foreach ($item['variables'] as $variable) { | |
| $lines[] = sprintf('%s: %s;', $variable, $ref); | |
| } | |
| if ($item['role'] === 'radius.default') { | |
| $rules[] = sprintf('.uk-card, .uk-button, .uk-input, .uk-textarea, .uk-select { border-radius: %s; }', $ref); | |
| } | |
| return $lines; | |
| } | |
| $style = $item['value']; | |
| $lines = ["// {$item['role']} ← {$item['key']}"]; | |
| $has = fn(string $p) => isset($style[$p]); | |
| if ($item['role'] === 'type.body') { | |
| if ($has('fontFamily')) { | |
| $lines[] = sprintf('@global-font-family: %s-font-family;', $ref); | |
| $lines[] = sprintf('@base-body-font-family: %s-font-family;', $ref); | |
| } | |
| if ($has('fontSize')) { | |
| $lines[] = sprintf('@global-font-size: %s-font-size;', $ref); | |
| } | |
| if ($has('lineHeight')) { | |
| $lines[] = sprintf('@global-line-height: %s-line-height;', $ref); | |
| } | |
| return $lines; | |
| } | |
| foreach ($item['variables'] as $variable) { | |
| if ($has('fontSize')) { | |
| $lines[] = sprintf('%s: %s-font-size;', $variable, $ref); | |
| } | |
| } | |
| // h1..h6: UIkit only exposes a size variable per level, the rest is a rule. | |
| if (preg_match('~^type\.h([1-6])$~', $item['role'], $m)) { | |
| $decls = []; | |
| foreach (['fontWeight' => 'font-weight', 'lineHeight' => 'line-height', 'letterSpacing' => 'letter-spacing', 'textTransform' => 'text-transform'] as $prop => $css) { | |
| if ($has($prop)) { | |
| $decls[] = sprintf('%s: %s-%s;', $css, $ref, $this->dash($prop)); | |
| } | |
| } | |
| if ($decls) { | |
| $rules[] = sprintf('h%1$s, .uk-h%1$s { %2$s }', $m[1], implode(' ', $decls)); | |
| } | |
| } | |
| return $lines; | |
| } | |
| public function manifest(): array | |
| { | |
| return [ | |
| 'name' => $this->tokens->data['name'], | |
| 'slug' => $this->slug(), | |
| 'source' => $this->tokens->data['source'], | |
| 'fileKey' => $this->tokens->data['fileKey'], | |
| 'generatedAt' => $this->tokens->data['generatedAt'], | |
| 'compiledAt' => gmdate('c'), | |
| 'bridge' => '0.1.0', | |
| 'target' => 'yootheme-pro-child-theme', | |
| 'tokenCount' => $this->tokens->count(), | |
| 'mapped' => count($this->resolved['mapped']), | |
| 'unmapped' => count($this->resolved['unmapped']), | |
| ]; | |
| } | |
| public function mappings(): array | |
| { | |
| $components = []; | |
| foreach ($this->tokens->data['components'] as $name => $def) { | |
| $auto = $this->mapper->component($name); | |
| $components[$name] = [ | |
| 'role' => $def['role'] ?: $auto['role'], | |
| 'target' => $def['target'] ?: $auto['target'], | |
| 'overrides' => $def['overrides'], | |
| ]; | |
| } | |
| return ['tokens' => $this->resolved, 'components' => $components]; | |
| } | |
| public function templateDetails(): string | |
| { | |
| $slug = $this->slug(); | |
| $name = htmlspecialchars($this->tokens->data['name'], ENT_XML1); | |
| return <<<XML | |
| <?xml version="1.0" encoding="utf-8"?> | |
| <extension type="template" client="site" method="upgrade"> | |
| <name>yootheme_child_{$slug}</name> | |
| <version>1.0.0</version> | |
| <creationDate>{$this->today()}</creationDate> | |
| <author>Figma → YOOtheme Style Bridge</author> | |
| <description>{$name} design system, imported from Figma.</description> | |
| <inheritable>0</inheritable> | |
| <parent>yootheme</parent> | |
| <files> | |
| <folder>less</folder> | |
| <folder>css</folder> | |
| <folder>assets</folder> | |
| </files> | |
| </extension> | |
| XML; | |
| } | |
| /** Resolved value for a semantic role, or null when nothing claimed it. */ | |
| public function valueForRole(string $role, ?string $property = null) | |
| { | |
| foreach ($this->resolved['mapped'] as $item) { | |
| if ($item['role'] === $role) { | |
| return $property === null ? $item['value'] : ($item['value'][$property] ?? null); | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * CSS that approximates the compiled Less against the UIkit CDN, so the design | |
| * can be judged on real components before anything is written to the site. | |
| */ | |
| public function previewCss(string $scope = '.uk-preview'): string | |
| { | |
| $v = fn(string $role, ?string $prop = null) => $this->valueForRole($role, $prop); | |
| $decl = function (array $pairs): string { | |
| $out = ''; | |
| foreach ($pairs as $prop => $value) { | |
| if ($value !== null && $value !== '') { | |
| $out .= sprintf('%s:%s;', $prop, $value); | |
| } | |
| } | |
| return $out; | |
| }; | |
| $radius = $v('radius.default'); | |
| $primary = $v('color.primary'); | |
| $rules = []; | |
| $rules[''] = $decl([ | |
| 'background' => $v('color.background'), | |
| 'color' => $v('color.text'), | |
| 'font-family' => $v('type.body', 'fontFamily'), | |
| 'font-size' => $v('type.body', 'fontSize'), | |
| 'line-height' => $v('type.body', 'lineHeight'), | |
| ]); | |
| $rules['h1,h2,h3,h4,h5,h6,.uk-h1,.uk-h2,.uk-h3'] = $decl(['color' => $v('color.emphasis')]); | |
| foreach (range(1, 6) as $level) { | |
| $style = $v("type.h$level"); | |
| if (is_array($style)) { | |
| $rules["h$level, .uk-h$level"] = $decl([ | |
| 'font-family' => $style['fontFamily'] ?? null, | |
| 'font-size' => $style['fontSize'] ?? null, | |
| 'font-weight' => $style['fontWeight'] ?? null, | |
| 'line-height' => $style['lineHeight'] ?? null, | |
| 'letter-spacing' => $style['letterSpacing'] ?? null, | |
| ]); | |
| } | |
| } | |
| $rules['.uk-button'] = $decl(['border-radius' => $radius]); | |
| $rules['.uk-button-primary'] = $decl(['background' => $primary, 'color' => $v('color.inverse')]); | |
| $rules['.uk-button-secondary'] = $decl(['background' => $v('color.secondary'), 'color' => $v('color.inverse')]); | |
| $rules['.uk-button-default'] = $decl(['border-color' => $v('color.border'), 'color' => $v('color.text')]); | |
| $rules['.uk-card-default'] = $decl([ | |
| 'background' => $v('color.background'), | |
| 'color' => $v('color.text'), | |
| 'border-radius' => $radius, | |
| 'box-shadow' => $v('shadow.medium'), | |
| ]); | |
| $rules['.uk-card-primary'] = $decl(['background' => $primary, 'color' => $v('color.inverse'), 'border-radius' => $radius]); | |
| $rules['.uk-input, .uk-textarea, .uk-select'] = $decl(['border-radius' => $radius, 'border-color' => $v('color.border')]); | |
| $rules['.uk-text-muted'] = $decl(['color' => $v('color.text-muted')]); | |
| $rules['.uk-link, a'] = $decl(['color' => $v('color.link') ?: $primary]); | |
| $rules['.uk-label'] = $decl(['background' => $primary, 'color' => $v('color.inverse')]); | |
| $rules['.uk-alert-danger'] = $decl(['background' => $v('color.danger'), 'color' => $v('color.inverse'), 'border-radius' => $radius]); | |
| $rules['.uk-alert-success'] = $decl(['background' => $v('color.success'), 'color' => $v('color.inverse'), 'border-radius' => $radius]); | |
| $rules['.uk-section'] = $decl(['padding-top' => $v('space.section'), 'padding-bottom' => $v('space.section')]); | |
| $rules['.uk-container'] = $decl(['max-width' => $v('space.container')]); | |
| // Section styles are how a layout refers to the palette without naming colours. | |
| $rules['.uk-section-muted, .uk-tile-muted'] = $decl(['background' => $v('color.muted')]); | |
| $rules['.uk-section-primary, .uk-tile-primary, .uk-background-primary, .uk-overlay-primary'] = $decl([ | |
| 'background' => $primary, | |
| 'color' => $v('color.inverse'), | |
| ]); | |
| $rules['.uk-section-secondary, .uk-tile-secondary, .uk-background-secondary'] = $decl([ | |
| 'background' => $v('color.secondary'), | |
| 'color' => $v('color.inverse'), | |
| ]); | |
| // Navigation | |
| $rules['.uk-navbar-container:not(.uk-navbar-transparent)'] = $decl([ | |
| 'background' => $v('color.background'), | |
| 'border-radius' => $radius, | |
| 'box-shadow' => $v('shadow.medium'), | |
| ]); | |
| $rules['.uk-navbar-nav>li>a, .uk-navbar-item, .uk-navbar-toggle'] = $decl([ | |
| 'color' => $v('color.text'), | |
| 'font-family' => $v('type.body', 'fontFamily'), | |
| ]); | |
| $rules['.uk-navbar-nav>li.uk-active>a, .uk-navbar-nav>li:hover>a'] = $decl(['color' => $primary]); | |
| $rules['.uk-dropdown, .uk-navbar-dropdown, .uk-dropbar, .uk-modal-dialog'] = $decl([ | |
| 'background' => $v('color.background'), | |
| 'color' => $v('color.text'), | |
| 'border-radius' => $radius, | |
| 'box-shadow' => $v('shadow.large'), | |
| ]); | |
| $rules['.uk-dropdown-nav>li.uk-active>a, .uk-nav-default>li.uk-active>a, .uk-nav-primary>li.uk-active>a'] = $decl(['color' => $primary]); | |
| $rules['.uk-nav-header'] = $decl(['color' => $v('color.emphasis')]); | |
| $rules['.uk-nav-divider'] = $decl(['border-top-color' => $v('color.border')]); | |
| $rules['.uk-subnav-pill>.uk-active>a'] = $decl([ | |
| 'background' => $primary, | |
| 'color' => $v('color.inverse'), | |
| 'border-radius' => $radius, | |
| ]); | |
| $rules['.uk-tab>.uk-active>a'] = $decl(['border-color' => $primary, 'color' => $v('color.emphasis')]); | |
| $rules['.uk-breadcrumb>.uk-active>*, .uk-pagination>.uk-active>*'] = $decl(['color' => $primary]); | |
| $rules['.uk-offcanvas-bar'] = $decl(['background' => $v('color.secondary')]); | |
| // Indicators | |
| $rules['.uk-badge, .uk-marker'] = $decl([ | |
| 'background' => $primary, | |
| 'color' => $v('color.inverse'), | |
| ]); | |
| $rules['.uk-label-success'] = $decl(['background' => $v('color.success')]); | |
| $rules['.uk-label-warning'] = $decl(['background' => $v('color.warning')]); | |
| $rules['.uk-label-danger'] = $decl(['background' => $v('color.danger')]); | |
| $rules['.uk-progress'] = $decl(['background' => $v('color.muted'), 'border-radius' => $radius]); | |
| $rules['.uk-progress::-webkit-progress-value'] = $decl(['background' => $primary, 'border-radius' => $radius]); | |
| $rules['.uk-progress::-moz-progress-bar'] = $decl(['background' => $primary, 'border-radius' => $radius]); | |
| $rules['.uk-text-primary'] = $decl(['color' => $primary]); | |
| $rules['.uk-text-emphasis'] = $decl(['color' => $v('color.emphasis')]); | |
| $rules['.uk-text-danger'] = $decl(['color' => $v('color.danger')]); | |
| $rules['.uk-text-success'] = $decl(['color' => $v('color.success')]); | |
| $rules['.uk-alert-primary'] = $decl(['background' => $v('color.muted'), 'color' => $primary, 'border-radius' => $radius]); | |
| $rules['.uk-alert-warning'] = $decl(['background' => $v('color.warning'), 'color' => $v('color.inverse'), 'border-radius' => $radius]); | |
| // Structure | |
| $rules['.uk-card-secondary'] = $decl([ | |
| 'background' => $v('color.secondary'), | |
| 'color' => $v('color.inverse'), | |
| 'border-radius' => $radius, | |
| ]); | |
| $rules['.uk-card-hover:hover'] = $decl(['box-shadow' => $v('shadow.large')]); | |
| $rules['.uk-table th'] = $decl(['color' => $v('color.emphasis')]); | |
| $rules['.uk-table-divider>tr:not(:first-child), .uk-table-divider>:not(:first-child)>tr, .uk-table-divider>:first-child>tr:not(:first-child), .uk-table-striped>tr:nth-of-type(odd), .uk-table-striped tbody tr:nth-of-type(odd)'] = $decl([ | |
| 'border-top-color' => $v('color.border'), | |
| ]); | |
| $rules['.uk-accordion-title, .uk-h1, .uk-h2, .uk-h3, .uk-h4'] = $decl(['color' => $v('color.emphasis')]); | |
| $rules['hr, .uk-hr, .uk-divider-icon::before, .uk-heading-divider'] = $decl(['border-color' => $v('color.border')]); | |
| $rules['.uk-search-input, .uk-search-default .uk-search-input'] = $decl(['border-radius' => $radius]); | |
| $rules['blockquote'] = $decl(['color' => $v('color.emphasis'), 'border-left-color' => $primary]); | |
| $css = ''; | |
| foreach ($rules as $selector => $body) { | |
| if ($body === '') { | |
| continue; | |
| } | |
| // Every selector in the list gets the scope, not just the first one, | |
| // or the demo styles leak out and restyle the page around it. | |
| $scoped = array_map( | |
| fn(string $part) => trim($scope . ' ' . trim($part)), | |
| explode(',', $selector) ?: [''] | |
| ); | |
| $css .= implode(',', $scoped) . '{' . $body . "}\n"; | |
| } | |
| return $css; | |
| } | |
| public function preview(): string | |
| { | |
| $colors = $this->tokens->group('color'); | |
| $type = $this->tokens->group('typography'); | |
| $shadow = $this->tokens->group('shadow'); | |
| $roles = []; | |
| foreach ($this->resolved['mapped'] as $item) { | |
| $roles[$item['group'] . '.' . $item['key']] = $item['role']; | |
| } | |
| // UIkit first, then the page's own chrome, then the token overrides scoped | |
| // to .uk-preview so the demo cannot restyle the inspector around it. | |
| $html = '<!doctype html><meta charset="utf-8"><title>' . htmlspecialchars($this->tokens->data['name']) | |
| . '</title><meta name="viewport" content="width=device-width, initial-scale=1">' | |
| . '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3/dist/css/uikit.min.css">' | |
| . '<script src="https://cdn.jsdelivr.net/npm/uikit@3/dist/js/uikit.min.js"></script>' | |
| . '<script src="https://cdn.jsdelivr.net/npm/uikit@3/dist/js/uikit-icons.min.js"></script>' | |
| . '<style>body{font:14px/1.5 system-ui,sans-serif;margin:24px;color:#111;background:#fff}' | |
| . 'h2{font-size:13px;text-transform:uppercase;letter-spacing:.08em;color:#666;margin:28px 0 10px}' | |
| . '.g{display:flex;flex-wrap:wrap;gap:10px}.s{width:132px}.c{height:56px;border-radius:6px;border:1px solid #0001}' | |
| . '.k{font:11px ui-monospace,monospace;margin-top:4px}.r{font:10px ui-monospace,monospace;color:#0a7}' | |
| . '.b{padding:16px;border:1px solid #eee;border-radius:8px;margin-bottom:10px}' | |
| . '.uk-preview{padding:20px;border:1px solid #eee;border-radius:8px}' | |
| . '.demo-group{padding:18px 0;border-top:1px solid #0000000f}' | |
| . '.demo-group:first-child{border-top:0}' | |
| . '.demo-label{position:sticky;top:0;z-index:5;margin:0 0 12px;padding:4px 8px;' | |
| . 'font:600 10px/1.6 ui-monospace,monospace;letter-spacing:.12em;text-transform:uppercase;' | |
| . 'color:#666;background:#f6f6f6;border-radius:4px;display:inline-block}' | |
| . '.demo-scale{display:flex;flex-wrap:wrap;gap:12px}' | |
| . '.demo-chip{padding:12px 16px;font:11px ui-monospace,monospace;border-radius:6px}</style>' | |
| . '<style>' . $this->previewCss() . '</style>'; | |
| $html .= '<h1>' . htmlspecialchars($this->tokens->data['name']) . '</h1>'; | |
| $html .= $this->previewComponents(); | |
| if ($colors) { | |
| $html .= '<h2>Color</h2><div class="g">'; | |
| foreach ($colors as $key => $value) { | |
| $role = $roles['color.' . $key] ?? ''; | |
| $html .= '<div class="s"><div class="c" style="background:' . htmlspecialchars($value) . '"></div>' | |
| . '<div class="k">' . htmlspecialchars($key) . '<br>' . htmlspecialchars($value) . '</div>' | |
| . '<div class="r">' . htmlspecialchars($role) . '</div></div>'; | |
| } | |
| $html .= '</div>'; | |
| } | |
| if ($type) { | |
| $html .= '<h2>Typography</h2>'; | |
| foreach ($type as $key => $style) { | |
| $css = ''; | |
| foreach (['fontFamily' => 'font-family', 'fontSize' => 'font-size', 'fontWeight' => 'font-weight', 'lineHeight' => 'line-height', 'letterSpacing' => 'letter-spacing'] as $prop => $name) { | |
| if (isset($style[$prop])) { | |
| $css .= $name . ':' . htmlspecialchars($style[$prop], ENT_QUOTES) . ';'; | |
| } | |
| } | |
| $html .= '<div class="b"><div style="' . $css . '">' . htmlspecialchars($key) . ' — The quick brown fox</div>' | |
| . '<div class="r">' . htmlspecialchars($roles['typography.' . $key] ?? '') . '</div></div>'; | |
| } | |
| } | |
| if ($shadow) { | |
| $html .= '<h2>Shadow</h2><div class="g">'; | |
| foreach ($shadow as $key => $value) { | |
| $html .= '<div class="s"><div class="c" style="background:#fff;box-shadow:' . htmlspecialchars($value, ENT_QUOTES) . '"></div>' | |
| . '<div class="k">' . htmlspecialchars($key) . '</div></div>'; | |
| } | |
| $html .= '</div>'; | |
| } | |
| if ($this->resolved['unmapped']) { | |
| $html .= '<h2>Unmapped (' . count($this->resolved['unmapped']) . ')</h2><div class="k">'; | |
| foreach ($this->resolved['unmapped'] as $item) { | |
| $html .= htmlspecialchars($item['group'] . '.' . $item['key'] . ' — ' . $item['reason']) . '<br>'; | |
| } | |
| $html .= '</div>'; | |
| } | |
| return $html; | |
| } | |
| /** | |
| * The full UIkit component set, rendered against the imported style so every | |
| * surface can be judged before anything is installed — not a token swatch | |
| * sheet, an actual theme proof. Static markup plus UIkit's own JS, so | |
| * dropdowns, tabs, accordions and modals really open. | |
| */ | |
| private function previewComponents(): string | |
| { | |
| $sections = [ | |
| 'Typography' => $this->demoTypography(), | |
| 'Buttons' => $this->demoButtons(), | |
| 'Navigation' => $this->demoNavigation(), | |
| 'Dropdowns' => $this->demoDropdowns(), | |
| 'Cards & tiles' => $this->demoCards(), | |
| 'Forms' => $this->demoForms(), | |
| 'Tables & lists' => $this->demoTables(), | |
| 'Feedback' => $this->demoFeedback(), | |
| 'Interactive' => $this->demoInteractive(), | |
| 'Section styles' => $this->demoSections(), | |
| 'Tokens applied' => $this->demoScales(), | |
| ]; | |
| $out = '<div class="uk-preview">'; | |
| foreach ($sections as $title => $markup) { | |
| $out .= '<div class="demo-group"><div class="demo-label">' . htmlspecialchars($title) . '</div>' . $markup . '</div>'; | |
| } | |
| return $out . '</div>'; | |
| } | |
| private function demoTypography(): string | |
| { | |
| return <<<'HTML' | |
| <h1>Heading one</h1> | |
| <h2>Heading two</h2> | |
| <h3>Heading three</h3> | |
| <h4>Heading four</h4> | |
| <h5>Heading five</h5> | |
| <h6>Heading six</h6> | |
| <p class="uk-text-lead">Lead paragraph: the type scale carried over from the design.</p> | |
| <p>Body copy with a <a href="#">text link</a>, <strong>bold</strong>, <em>italic</em>, | |
| <code>inline code</code> and <span class="uk-text-muted">muted text</span>.</p> | |
| <p class="uk-text-meta">Meta text — captions, bylines, timestamps.</p> | |
| <blockquote><p>A pull quote, styled from the emphasis colour and the brand accent.</p> | |
| <footer>Someone, <cite>somewhere</cite></footer></blockquote> | |
| <ul class="uk-list uk-list-bullet"><li>Bulleted list item</li><li>Second item</li></ul> | |
| <hr> | |
| <h2 class="uk-heading-divider">Heading with divider</h2> | |
| <p><span class="uk-text-primary">Primary</span> · <span class="uk-text-emphasis">Emphasis</span> · | |
| <span class="uk-text-success">Success</span> · <span class="uk-text-danger">Danger</span></p> | |
| HTML; | |
| } | |
| private function demoButtons(): string | |
| { | |
| return <<<'HTML' | |
| <p> | |
| <button class="uk-button uk-button-primary" type="button">Primary</button> | |
| <button class="uk-button uk-button-secondary" type="button">Secondary</button> | |
| <button class="uk-button uk-button-default" type="button">Default</button> | |
| <button class="uk-button uk-button-danger" type="button">Danger</button> | |
| <button class="uk-button uk-button-text" type="button">Text</button> | |
| <button class="uk-button uk-button-link" type="button">Link</button> | |
| </p> | |
| <p> | |
| <button class="uk-button uk-button-primary uk-button-small" type="button">Small</button> | |
| <button class="uk-button uk-button-primary" type="button">Default size</button> | |
| <button class="uk-button uk-button-primary uk-button-large" type="button">Large</button> | |
| <button class="uk-button uk-button-primary" type="button" disabled>Disabled</button> | |
| </p> | |
| <p> | |
| <div class="uk-button-group"> | |
| <button class="uk-button uk-button-secondary" type="button">Group</button> | |
| <button class="uk-button uk-button-secondary" type="button">of</button> | |
| <button class="uk-button uk-button-secondary" type="button">buttons</button> | |
| </div> | |
| </p> | |
| <p> | |
| <a class="uk-icon-button" href="#" uk-icon="heart"></a> | |
| <a class="uk-icon-button" href="#" uk-icon="search"></a> | |
| <button class="uk-button uk-button-primary" type="button"><span uk-icon="check"></span> With icon</button> | |
| </p> | |
| HTML; | |
| } | |
| private function demoNavigation(): string | |
| { | |
| return <<<'HTML' | |
| <div class="uk-navbar-container uk-margin-bottom"> | |
| <div class="uk-container"> | |
| <nav class="uk-navbar" uk-navbar> | |
| <div class="uk-navbar-left"> | |
| <a class="uk-navbar-item uk-logo" href="#">Brand</a> | |
| <ul class="uk-navbar-nav"> | |
| <li class="uk-active"><a href="#">Active</a></li> | |
| <li><a href="#">Parent <span uk-navbar-parent-icon></span></a> | |
| <div class="uk-navbar-dropdown"> | |
| <ul class="uk-nav uk-navbar-dropdown-nav"> | |
| <li class="uk-active"><a href="#">Active item</a></li> | |
| <li><a href="#">Second item</a></li> | |
| <li class="uk-nav-header">Header</li> | |
| <li><a href="#">Third item</a></li> | |
| <li class="uk-nav-divider"></li> | |
| <li><a href="#">After divider</a></li> | |
| </ul> | |
| </div> | |
| </li> | |
| <li><a href="#">Item</a></li> | |
| </ul> | |
| </div> | |
| <div class="uk-navbar-right"> | |
| <a class="uk-navbar-toggle" href="#" uk-search-icon></a> | |
| <a class="uk-button uk-button-primary uk-margin-small-left" href="#">Call to action</a> | |
| </div> | |
| </nav> | |
| </div> | |
| </div> | |
| <div class="uk-child-width-1-2@m uk-grid-small" uk-grid> | |
| <div> | |
| <ul class="uk-nav uk-nav-default"> | |
| <li class="uk-active"><a href="#">Nav default active</a></li> | |
| <li class="uk-parent"><a href="#">Parent</a> | |
| <ul class="uk-nav-sub"><li><a href="#">Sub item</a></li><li><a href="#">Sub item</a></li></ul> | |
| </li> | |
| <li class="uk-nav-header">Header</li> | |
| <li><a href="#">Item</a></li> | |
| <li class="uk-nav-divider"></li> | |
| <li><a href="#">Item</a></li> | |
| </ul> | |
| </div> | |
| <div> | |
| <ul class="uk-nav uk-nav-primary"> | |
| <li class="uk-active"><a href="#">Nav primary</a></li> | |
| <li><a href="#">Second</a></li> | |
| <li><a href="#">Third</a></li> | |
| </ul> | |
| </div> | |
| </div> | |
| <ul class="uk-subnav" uk-margin><li class="uk-active"><a href="#">Subnav</a></li><li><a href="#">Item</a></li><li><a href="#">Item</a></li></ul> | |
| <ul class="uk-subnav uk-subnav-divider"><li class="uk-active"><a href="#">Divider</a></li><li><a href="#">Item</a></li><li><a href="#">Item</a></li></ul> | |
| <ul class="uk-subnav uk-subnav-pill"><li class="uk-active"><a href="#">Pill</a></li><li><a href="#">Item</a></li><li><a href="#">Item</a></li></ul> | |
| <ul class="uk-breadcrumb"><li><a href="#">Home</a></li><li><a href="#">Section</a></li><li class="uk-active"><span>Current page</span></li></ul> | |
| <ul class="uk-pagination"> | |
| <li><a href="#"><span uk-pagination-previous></span></a></li> | |
| <li><a href="#">1</a></li><li class="uk-active"><span>2</span></li><li><a href="#">3</a></li> | |
| <li class="uk-pagination-next"><a href="#"><span uk-pagination-next></span></a></li> | |
| </ul> | |
| HTML; | |
| } | |
| private function demoDropdowns(): string | |
| { | |
| return <<<'HTML' | |
| <div class="uk-grid-small uk-flex-middle" uk-grid> | |
| <div class="uk-width-auto"> | |
| <button class="uk-button uk-button-default" type="button">Dropdown</button> | |
| <div uk-dropdown="mode: click"> | |
| <ul class="uk-nav uk-dropdown-nav"> | |
| <li class="uk-active"><a href="#">Active</a></li> | |
| <li><a href="#">Item</a></li> | |
| <li class="uk-nav-header">Header</li> | |
| <li><a href="#">Item</a></li> | |
| <li class="uk-nav-divider"></li> | |
| <li><a href="#">Item</a></li> | |
| </ul> | |
| </div> | |
| </div> | |
| <div class="uk-width-auto"> | |
| <button class="uk-button uk-button-primary" type="button">Primary dropdown</button> | |
| <div uk-dropdown="mode: click; pos: bottom-right"> | |
| <ul class="uk-nav uk-dropdown-nav"><li><a href="#">Profile</a></li><li><a href="#">Settings</a></li><li><a href="#">Sign out</a></li></ul> | |
| </div> | |
| </div> | |
| <div class="uk-width-auto"> | |
| <span uk-icon="more-vertical" class="uk-icon-button"></span> | |
| <div uk-dropdown="mode: click"><p class="uk-margin-remove">Free-form dropdown content.</p></div> | |
| </div> | |
| <div class="uk-width-auto"> | |
| <select class="uk-select" style="width:200px"><option>Native select</option><option>Second option</option></select> | |
| </div> | |
| </div> | |
| HTML; | |
| } | |
| private function demoCards(): string | |
| { | |
| return <<<'HTML' | |
| <div class="uk-child-width-1-3@m uk-grid-match" uk-grid> | |
| <div> | |
| <div class="uk-card uk-card-default uk-card-body"> | |
| <h3 class="uk-card-title">Default card</h3> | |
| <p>Surface, radius, shadow and card typography from the imported style.</p> | |
| <a href="#" class="uk-button uk-button-text">Read more</a> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="uk-card uk-card-primary uk-card-body"> | |
| <h3 class="uk-card-title">Primary card</h3> | |
| <p>Brand background with the contrast colour resolved for legibility.</p> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="uk-card uk-card-secondary uk-card-body"> | |
| <h3 class="uk-card-title">Secondary card</h3> | |
| <p>Secondary background.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="uk-child-width-1-2@m uk-margin" uk-grid> | |
| <div> | |
| <div class="uk-card uk-card-default"> | |
| <div class="uk-card-header"><h4 class="uk-margin-remove">Header</h4></div> | |
| <div class="uk-card-body"><p>Card with header and footer.</p></div> | |
| <div class="uk-card-footer"><a href="#" class="uk-button uk-button-text">Footer action</a></div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="uk-card uk-card-default uk-card-body uk-card-hover"> | |
| <div class="uk-card-badge uk-label">Badge</div> | |
| <h4>Hover card</h4><p>Hover for the large shadow token.</p> | |
| </div> | |
| </div> | |
| </div> | |
| HTML; | |
| } | |
| private function demoForms(): string | |
| { | |
| return <<<'HTML' | |
| <form class="uk-form-stacked"> | |
| <div class="uk-child-width-1-2@m uk-grid-small" uk-grid> | |
| <div> | |
| <label class="uk-form-label" for="d-in">Text input</label> | |
| <div class="uk-form-controls"><input class="uk-input" id="d-in" type="text" placeholder="Placeholder"></div> | |
| </div> | |
| <div> | |
| <label class="uk-form-label" for="d-sel">Select</label> | |
| <div class="uk-form-controls"><select class="uk-select" id="d-sel"><option>Option one</option><option>Option two</option></select></div> | |
| </div> | |
| <div> | |
| <label class="uk-form-label" for="d-ta">Textarea</label> | |
| <div class="uk-form-controls"><textarea class="uk-textarea" id="d-ta" rows="3" placeholder="Message"></textarea></div> | |
| </div> | |
| <div> | |
| <label class="uk-form-label" for="d-search">Search</label> | |
| <div class="uk-form-controls"><div class="uk-search uk-search-default uk-width-1-1"><span uk-search-icon></span><input class="uk-search-input" id="d-search" type="search" placeholder="Search"></div></div> | |
| </div> | |
| </div> | |
| <div class="uk-margin"> | |
| <label><input class="uk-checkbox" type="checkbox" checked> Checkbox</label> | |
| <label class="uk-margin-left"><input class="uk-radio" type="radio" name="d-r" checked> Radio one</label> | |
| <label><input class="uk-radio" type="radio" name="d-r"> Radio two</label> | |
| </div> | |
| <div class="uk-margin"> | |
| <input class="uk-range" type="range" value="60" min="0" max="100"> | |
| </div> | |
| <div class="uk-margin uk-grid-small uk-child-width-1-3@s" uk-grid> | |
| <div><input class="uk-input uk-form-small" type="text" placeholder="Small"></div> | |
| <div><input class="uk-input uk-form-danger" type="text" value="Danger state"></div> | |
| <div><input class="uk-input uk-form-success" type="text" value="Success state"></div> | |
| </div> | |
| <p><button class="uk-button uk-button-primary" type="button">Submit</button> | |
| <button class="uk-button uk-button-default" type="button">Cancel</button></p> | |
| </form> | |
| HTML; | |
| } | |
| private function demoTables(): string | |
| { | |
| return <<<'HTML' | |
| <table class="uk-table uk-table-divider uk-table-hover uk-table-small"> | |
| <caption>Divider, hover, small</caption> | |
| <thead><tr><th>Column</th><th>Column</th><th>Column</th></tr></thead> | |
| <tbody> | |
| <tr><td>Row one</td><td>Value</td><td><span class="uk-label">Label</span></td></tr> | |
| <tr><td>Row two</td><td>Value</td><td><span class="uk-label uk-label-success">Success</span></td></tr> | |
| <tr><td>Row three</td><td>Value</td><td><span class="uk-label uk-label-danger">Danger</span></td></tr> | |
| </tbody> | |
| </table> | |
| <div class="uk-child-width-1-3@m uk-grid-small" uk-grid> | |
| <div><ul class="uk-list uk-list-divider"><li>List divider</li><li>Second</li><li>Third</li></ul></div> | |
| <div><ul class="uk-list uk-list-striped"><li>List striped</li><li>Second</li><li>Third</li></ul></div> | |
| <div> | |
| <dl class="uk-description-list uk-description-list-divider"> | |
| <dt>Description term</dt><dd>Description details.</dd> | |
| <dt>Second term</dt><dd>More details.</dd> | |
| </dl> | |
| </div> | |
| </div> | |
| HTML; | |
| } | |
| private function demoFeedback(): string | |
| { | |
| return <<<'HTML' | |
| <div class="uk-alert uk-alert-primary" uk-alert><a class="uk-alert-close" uk-close></a><p>Primary alert.</p></div> | |
| <div class="uk-alert uk-alert-success" uk-alert><a class="uk-alert-close" uk-close></a><p>Success alert.</p></div> | |
| <div class="uk-alert uk-alert-warning" uk-alert><a class="uk-alert-close" uk-close></a><p>Warning alert.</p></div> | |
| <div class="uk-alert uk-alert-danger" uk-alert><a class="uk-alert-close" uk-close></a><p>Danger alert.</p></div> | |
| <p> | |
| <span class="uk-badge">3</span> | |
| <span class="uk-label">Label</span> | |
| <span class="uk-label uk-label-success">Success</span> | |
| <span class="uk-label uk-label-warning">Warning</span> | |
| <span class="uk-label uk-label-danger">Danger</span> | |
| <span class="uk-marker" uk-marker></span> | |
| <span uk-spinner></span> | |
| </p> | |
| <progress class="uk-progress" value="65" max="100"></progress> | |
| <p><a href="#" title="Tooltip text" uk-tooltip>Hover for a tooltip</a></p> | |
| HTML; | |
| } | |
| private function demoInteractive(): string | |
| { | |
| return <<<'HTML' | |
| <ul class="uk-tab" uk-tab> | |
| <li class="uk-active"><a href="#">Tab one</a></li> | |
| <li><a href="#">Tab two</a></li> | |
| <li><a href="#">Tab three</a></li> | |
| </ul> | |
| <ul class="uk-switcher uk-margin"> | |
| <li>Content for the first tab.</li><li>Content for the second tab.</li><li>Content for the third tab.</li> | |
| </ul> | |
| <ul uk-accordion> | |
| <li class="uk-open"><a class="uk-accordion-title" href="#">Accordion — open</a> | |
| <div class="uk-accordion-content"><p>Panel content in body copy.</p></div></li> | |
| <li><a class="uk-accordion-title" href="#">Accordion — closed</a> | |
| <div class="uk-accordion-content"><p>Second panel.</p></div></li> | |
| </ul> | |
| <p> | |
| <button class="uk-button uk-button-default" type="button" uk-toggle="target: #demo-modal">Open modal</button> | |
| <button class="uk-button uk-button-default" type="button" uk-toggle="target: #demo-offcanvas">Open offcanvas</button> | |
| </p> | |
| <div id="demo-modal" uk-modal> | |
| <div class="uk-modal-dialog uk-modal-body"> | |
| <h2 class="uk-modal-title">Modal title</h2> | |
| <p>Dialog surface, radius and shadow.</p> | |
| <p class="uk-text-right"><button class="uk-button uk-button-default uk-modal-close" type="button">Cancel</button> | |
| <button class="uk-button uk-button-primary" type="button">Save</button></p> | |
| </div> | |
| </div> | |
| <div id="demo-offcanvas" uk-offcanvas> | |
| <div class="uk-offcanvas-bar"> | |
| <button class="uk-offcanvas-close" type="button" uk-close></button> | |
| <ul class="uk-nav uk-nav-primary"><li class="uk-active"><a href="#">Menu item</a></li><li><a href="#">Second</a></li></ul> | |
| </div> | |
| </div> | |
| HTML; | |
| } | |
| private function demoSections(): string | |
| { | |
| return <<<'HTML' | |
| <div class="uk-section uk-section-default uk-padding-small"><div class="uk-container"><h4 class="uk-margin-remove">Section default</h4><p class="uk-margin-remove">Page surface.</p></div></div> | |
| <div class="uk-section uk-section-muted uk-padding-small"><div class="uk-container"><h4 class="uk-margin-remove">Section muted</h4><p class="uk-margin-remove">Muted surface, for alternating bands.</p></div></div> | |
| <div class="uk-section uk-section-primary uk-padding-small"><div class="uk-container"><h4 class="uk-margin-remove">Section primary</h4><p class="uk-margin-remove">Brand band — what an imported layout refers to as <code>"style": "primary"</code>.</p></div></div> | |
| <div class="uk-section uk-section-secondary uk-padding-small"><div class="uk-container"><h4 class="uk-margin-remove">Section secondary</h4><p class="uk-margin-remove">Secondary band.</p></div></div> | |
| HTML; | |
| } | |
| /** The raw scales, so a value can be checked directly against its component. */ | |
| private function demoScales(): string | |
| { | |
| $out = '<div class="demo-scale">'; | |
| foreach ($this->tokens->group('shadow') as $key => $value) { | |
| $out .= '<div class="demo-chip" style="box-shadow:' . htmlspecialchars($value, ENT_QUOTES) . '">' | |
| . htmlspecialchars($key) . '</div>'; | |
| } | |
| foreach ($this->tokens->group('radius') as $key => $value) { | |
| $out .= '<div class="demo-chip" style="border:1px solid currentColor;border-radius:' | |
| . htmlspecialchars($value, ENT_QUOTES) . '">' . htmlspecialchars($key) . '</div>'; | |
| } | |
| foreach ($this->tokens->group('spacing') as $key => $value) { | |
| $out .= '<div class="demo-chip" style="padding-left:' . htmlspecialchars($value, ENT_QUOTES) | |
| . ';max-width:100%">' . htmlspecialchars($key . ' ' . $value) . '</div>'; | |
| } | |
| return $out . '</div>'; | |
| } | |
| /** Writes the package tree. Returns the absolute path of the package root. */ | |
| public function write(string $dir): string | |
| { | |
| $child = $dir . '/yootheme_child_' . $this->slug(); | |
| foreach ([$dir, $dir . '/preview', $child, $child . '/less', $child . '/css', $child . '/assets'] as $path) { | |
| if (!is_dir($path) && !mkdir($path, 0755, true) && !is_dir($path)) { | |
| throw new \RuntimeException("Cannot create directory: $path"); | |
| } | |
| } | |
| file_put_contents($dir . '/manifest.json', json_encode($this->manifest(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); | |
| file_put_contents($dir . '/tokens.json', $this->tokens->toJson()); | |
| file_put_contents($dir . '/mappings.json', json_encode($this->mappings(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); | |
| file_put_contents($dir . '/preview/index.html', $this->preview()); | |
| file_put_contents($child . '/templateDetails.xml', $this->templateDetails()); | |
| file_put_contents($child . '/less/theme.less', $this->less()); | |
| // Keeps declared folders in the zip even when empty — templateDetails.xml lists them. | |
| file_put_contents($child . '/assets/index.html', '<!doctype html><title></title>'); | |
| file_put_contents($child . '/css/index.html', '<!doctype html><title></title>'); | |
| if ($this->stylesheet !== '') { | |
| file_put_contents( | |
| $child . '/css/theme.css', | |
| "/* Imported stylesheet — layout and component CSS the Less variables cannot express. */\n" | |
| . $this->rewriteAssetUrls($this->stylesheet) . "\n" | |
| ); | |
| } | |
| foreach ($this->assetFiles as $name => $source) { | |
| $name = preg_replace('/[^A-Za-z0-9_.\-]/', '', (string) $name); | |
| if ($name !== '' && is_file($source)) { | |
| copy($source, $child . '/assets/' . $name); | |
| } | |
| } | |
| foreach ($this->tokens->data['assets'] as $asset) { | |
| // An entry with no inline data is a manifest row for a file copied above; | |
| // writing it here would truncate that copy. | |
| if ($asset['data'] === '') { | |
| continue; | |
| } | |
| $data = $asset['type'] === 'svg' ? $asset['data'] : base64_decode($asset['data'], true); | |
| if ($data !== false && $asset['name'] !== '') { | |
| file_put_contents($child . '/assets/' . $asset['name'], $data); | |
| } | |
| } | |
| return $child; | |
| } | |
| public function zip(string $dir, string $zipPath): string | |
| { | |
| $child = $this->write($dir); | |
| $zip = new \ZipArchive(); | |
| if ($zip->open($zipPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { | |
| throw new \RuntimeException("Cannot create zip: $zipPath"); | |
| } | |
| $base = strlen($child) + 1; | |
| $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($child, \FilesystemIterator::SKIP_DOTS)); | |
| foreach ($files as $file) { | |
| $zip->addFile($file->getPathname(), substr($file->getPathname(), $base)); | |
| } | |
| $zip->close(); | |
| return $zipPath; | |
| } | |
| /** Source trees nest images; the child theme flattens them into assets/. */ | |
| private function rewriteAssetUrls(string $css): string | |
| { | |
| return (string) preg_replace_callback( | |
| '~url\(\s*([\'"]?)([^)\'"]+)\1\s*\)~i', | |
| function (array $m): string { | |
| $file = basename(explode('?', $m[2])[0]); | |
| if (preg_match('~^(data:|https?:|//)~i', $m[2]) || !isset($this->assetFiles[$file])) { | |
| return $m[0]; | |
| } | |
| return 'url("../assets/' . $file . '")'; | |
| }, | |
| $css | |
| ); | |
| } | |
| private function dash(string $prop): string | |
| { | |
| return strtolower(preg_replace('~(?<!^)[A-Z]~', '-$0', $prop)); | |
| } | |
| private function quote(string $prop, string $value): string | |
| { | |
| return $prop === 'fontFamily' && !preg_match('~^["\']~', $value) ? '"' . $value . '"' : $value; | |
| } | |
| private function today(): string | |
| { | |
| return gmdate('F Y'); | |
| } | |
| } | |