UIKitV3-Automator / bridge /src /Mapper.php
KyklosIO's picture
Claude Opus 5
Import a built site folder as a YOOtheme child theme
080e771
Raw History Blame Contribute Delete
8.94 kB
<?php
/**
* Token key -> semantic role -> YOOtheme/UIkit Less variables.
*
* This table is the product. Everything else is plumbing: it is what keeps an
* imported design editable inside YOOtheme's style panel instead of a CSS dump.
*/
namespace Fyoo\FigmaBridge;
class Mapper
{
/** role => [less variables it drives] */
public const ROLES = [
'color.primary' => ['@global-primary-background'],
'color.secondary' => ['@global-secondary-background'],
'color.background' => ['@global-background'],
'color.muted' => ['@global-muted-background'],
'color.text' => ['@global-color'],
'color.emphasis' => ['@global-emphasis-color'],
'color.text-muted' => ['@global-muted-color'],
'color.inverse' => ['@global-inverse-color'],
'color.link' => ['@global-link-color'],
'color.danger' => ['@global-danger-background'],
'color.success' => ['@global-success-background'],
'color.warning' => ['@global-warning-background'],
'color.border' => ['@global-border'],
'type.body' => ['@global-font-family', '@global-font-size', '@global-line-height'],
'type.small' => ['@global-small-font-size'],
'type.lead' => ['@global-medium-font-size'],
'type.h1' => ['@base-h1-font-size'],
'type.h2' => ['@base-h2-font-size'],
'type.h3' => ['@base-h3-font-size'],
'type.h4' => ['@base-h4-font-size'],
'type.h5' => ['@base-h5-font-size'],
'type.h6' => ['@base-h6-font-size'],
'space.xsmall' => ['@global-small-margin'],
'space.small' => ['@global-margin'],
'space.medium' => ['@global-medium-margin'],
'space.large' => ['@global-large-margin'],
'space.xlarge' => ['@global-xlarge-margin'],
'space.gutter' => ['@global-gutter'],
'space.section' => ['@section-padding-vertical'],
'space.container' => ['@container-max-width'],
'radius.default' => ['@border-rounded-border-radius'],
'shadow.small' => ['@global-small-box-shadow'],
'shadow.medium' => ['@global-medium-box-shadow'],
'shadow.large' => ['@global-large-box-shadow'],
'shadow.xlarge' => ['@global-xlarge-box-shadow'],
];
/**
* Ordered: first pattern that matches a token key wins. Order matters —
* "text-muted" must be tested before "text", "muted" before "background".
*/
public const RULES = [
'color' => [
'~(^|-)(on-primary|inverse|contrast)(-|$)~' => 'color.inverse',
'~(^|-)(text|foreground|fg|ink|content)-(muted|secondary|subtle|tertiary)(-|$)~' => 'color.text-muted',
'~(^|-)(muted|subtle)-(text|foreground|fg)(-|$)~' => 'color.text-muted',
'~(^|-)(heading|title|emphasis|display)(-|$)~' => 'color.emphasis',
'~(^|-)(text|foreground|fg|ink|body)(-|$)~' => 'color.text',
'~(^|-)link(-|$)~' => 'color.link',
'~(^|-)(danger|error|destructive|critical|negative)(-|$)~' => 'color.danger',
'~(^|-)(success|positive|ok)(-|$)~' => 'color.success',
'~(^|-)(warning|caution|alert)(-|$)~' => 'color.warning',
'~(^|-)(border|divider|outline|stroke|separator)(-|$)~' => 'color.border',
'~(^|-)(secondary|brand-2|accent-2)(-|$)~' => 'color.secondary',
'~(^|-)(brand-)?(primary|accent|brand)(-|$)~' => 'color.primary',
'~(^|-)(muted|subtle|surface-2|neutral-alt)(-|$)~' => 'color.muted',
'~(^|-)(surface|background|bg|canvas|base|paper)(-|$)~' => 'color.background',
],
'typography' => [
'~(^|-)(h1|heading-(xl|1|hero|display))(-|$)~' => 'type.h1',
'~(^|-)(h2|heading-(lg|2|large))(-|$)~' => 'type.h2',
'~(^|-)(h3|heading-(md|3|medium))(-|$)~' => 'type.h3',
'~(^|-)(h4|heading-(sm|4|small))(-|$)~' => 'type.h4',
'~(^|-)(h5|heading-(xs|5))(-|$)~' => 'type.h5',
'~(^|-)(h6|heading-(2xs|6)|overline|eyebrow)(-|$)~' => 'type.h6',
'~(^|-)(lead|intro|subtitle|body-large)(-|$)~' => 'type.lead',
'~(^|-)(small|caption|meta|footnote)(-|$)~' => 'type.small',
'~(^|-)(body|base|paragraph|text|default)(-|$)~' => 'type.body',
],
'spacing' => [
'~(^|-)(container|max-width|content|content-width|wrapper|page-width)(-|$)~' => 'space.container',
'~(^|-)(section|block|stack-xl)(-|$)~' => 'space.section',
'~(^|-)(gutter|grid-gap|column-gap|gap)(-|$)~' => 'space.gutter',
'~(^|-)(3xl|2xl|xxl|xlarge|xl)(-|$)~' => 'space.xlarge',
'~(^|-)(large|lg)(-|$)~' => 'space.large',
'~(^|-)(md|medium)(-|$)~' => 'space.medium',
'~(^|-)(xs|xxs|2xs|xsmall)(-|$)~' => 'space.xsmall',
'~(^|-)(sm|small|base|default)(-|$)~' => 'space.small',
],
'radius' => [
'~(^|-)(default|base|md|medium|card|button|control)(-|$)~' => 'radius.default',
],
'shadow' => [
'~(^|-)(3xl|2xl|xxl|xlarge|xl)(-|$)~' => 'shadow.xlarge',
'~(^|-)(lg|large)(-|$)~' => 'shadow.large',
'~(^|-)(md|medium|default|base|card)(-|$)~' => 'shadow.medium',
'~(^|-)(sm|xs|small)(-|$)~' => 'shadow.small',
],
];
/** Figma component name -> semantic role -> YOOtheme element target. */
public const COMPONENTS = [
'~^button\s*/\s*primary~i' => ['action.primary', 'button-primary'],
'~^button\s*/\s*secondary~i' => ['action.secondary', 'button-secondary'],
'~^button~i' => ['action.default', 'button-default'],
'~^card~i' => ['content.card', 'card-default'],
'~^(nav|navbar|header)~i' => ['navigation.primary', 'navbar'],
'~^(input|field|form)~i' => ['form.control', 'form'],
'~^(alert|notice|banner)~i' => ['feedback.alert', 'alert'],
'~^(section|hero)~i' => ['layout.section', 'section'],
];
private array $overrides;
/** @param array $overrides token key => role, saved from the admin mapping table */
public function __construct(array $overrides = [])
{
$this->overrides = $overrides;
}
public function role(string $group, string $key): ?string
{
if (isset($this->overrides[$key])) {
return $this->overrides[$key] === '-' ? null : $this->overrides[$key];
}
foreach (self::RULES[$group] ?? [] as $pattern => $role) {
if (preg_match($pattern, "-$key-")) {
return $role;
}
}
return null;
}
public function variables(string $role): array
{
return self::ROLES[$role] ?? [];
}
public function component(string $name): array
{
foreach (self::COMPONENTS as $pattern => $mapping) {
if (preg_match($pattern, $name)) {
return ['role' => $mapping[0], 'target' => $mapping[1]];
}
}
return ['role' => '', 'target' => ''];
}
/**
* Full resolution table for one token graph: what maps where, and what did not
* map at all (the admin UI shows these so a human can assign them).
*/
public function resolve(Tokens $tokens): array
{
$mapped = [];
$unmapped = [];
$claimed = [];
foreach (Tokens::GROUPS as $group) {
foreach ($tokens->group($group) as $key => $value) {
$role = $this->role($group === 'typography' ? 'typography' : $group, $key);
// First token to claim a role wins; later ones stay available as raw vars.
if ($role === null || isset($claimed[$role])) {
$unmapped[] = ['group' => $group, 'key' => $key, 'value' => $value, 'reason' => $role ? "role $role already taken" : 'no rule matched'];
continue;
}
$claimed[$role] = $key;
$mapped[] = [
'group' => $group,
'key' => $key,
'value' => $value,
'role' => $role,
'variables' => $this->variables($role),
];
}
}
return ['mapped' => $mapped, 'unmapped' => $unmapped];
}
}