0
Fork 0
mirror of https://github.com/penpot/penpot-export.git synced 2025-02-12 18:18:01 -05:00

feat(cli): allow exporting colors CSS vars

This commit is contained in:
Roberto Redradix 2023-08-23 21:05:39 +02:00 committed by Roberto RedRadix
parent c5288352ab
commit e2812c99ea
7 changed files with 105 additions and 7 deletions

View file

@ -5,6 +5,12 @@ require('dotenv').config()
*/
const config = {
accessToken: process.env.PENPOT_ACCESS_TOKEN,
colors: [
{
output: 'src/styles/colors.css', // 👈🏻 Path where your css should be generated.
fileId: '4a499800-872e-80e1-8002-fc0b585dc061'
},
],
typographies: [
{
output: 'src/styles/typographies.css', // 👈🏻 Path where your css should be generated.

View file

@ -15,6 +15,7 @@ import type {
PenpotTypography,
PenpotGetFileOptions,
PenpotFile,
PenpotColor,
} from './types'
class PenpotApiError extends Error {
@ -145,4 +146,18 @@ export class Penpot {
fontFamily: `"${typography.fontFamily}"`,
}
}
async getFileColors(
options: PenpotGetFileOptions,
): Promise<{ fileName: string; colors: PenpotColor[] }> {
const file = await this.fetcher<PenpotFile>({
command: 'get-file',
body: {
id: options.fileId,
},
})
const colors = Object.values(file.data.colors)
return { fileName: file.name, colors }
}
}

View file

@ -25,6 +25,18 @@ export interface PenpotObject {
shapes?: string[]
}
type PenpotAsset = {
id: string
name: string
modifiedAt: string
path: string
}
export interface PenpotColor extends PenpotAsset {
color: string
opacity: number
}
type CssTextProperty =
| 'lineHeight'
| 'fontStyle'
@ -34,11 +46,7 @@ type CssTextProperty =
| 'letterSpacing'
| 'fontFamily'
export type PenpotTypography = {
id: string
name: string
modifiedAt: string
path: string
export type PenpotTypography = PenpotAsset & {
fontId: string
fontVariantId: string
} & Record<CssTextProperty, string>
@ -56,6 +64,7 @@ export interface PenpotFile {
data: {
id: string
version: number
colors: PenpotColor[]
typographies: PenpotTypography[]
pages: PenpotPage[]
}

View file

@ -1,6 +1,6 @@
import { Config, PagesConfig } from './types'
const BASE_CONFIG: Omit<Config, 'pages' | 'typographies'> = {
const BASE_CONFIG: Omit<Config, 'pages' | 'typographies' | 'colors'> = {
accessToken: '',
}
@ -44,6 +44,24 @@ export function validateAndNormalizePenpotExportConfig(config: Config): Config {
...config,
pages: [],
typographies: [],
colors: [],
}
for (const [index, colorsConfig] of config.colors.entries()) {
const { output, fileId } = colorsConfig
if (!output) {
throw new MissingOutputPathError(index)
}
if (!fileId) {
throw new MissingFileIdError(index)
}
normalizedConfig.colors.push({
output,
fileId,
})
}
for (const [index, typographiesConfig] of config.typographies.entries()) {

View file

@ -61,6 +61,24 @@ export function textToCssClassSelector(str: string) {
return '.' + ident
}
/** From: https://www.w3.org/TR/css-variables-1/#custom-property
* A custom property is any property whose name starts with two dashes (U+002D HYPHEN-MINUS), like --foo. The
* <custom-property-name> production corresponds to this: its defined as any <dashed-ident> (a valid identifier that
* starts with two dashes), except -- itself, which is reserved for future use by CSS.
*
* The <dashed-ident> production is a <custom-ident>, with all the case-sensitivity that implies, with the additional
* restriction that it must start with two dashes (U+002D HYPHEN-MINUS).
*
* This generic data type is denoted by <custom-ident>, and represents any valid CSS identifier that would not be
* misinterpreted as a pre-defined keyword in that propertys value definition. Such identifiers are fully
* case-sensitive (meaning theyre compared using the "identical to" operation), even in the ASCII range (e.g. example
* and EXAMPLE are two different, unrelated user-defined identifiers).
*/
export function textToCssCustomProperyName(str: string) {
const unescapedDashedIdentifier = '--' + str.trimStart()
return textToCssIdentToken(unescapedDashedIdentifier)
}
export function cssClassDefinitionToCSS(
cssClassDefinition: CSSClassDefinition,
): string {

View file

@ -1,7 +1,11 @@
import Penpot from '../lib/api'
import { validateAndNormalizePenpotExportConfig } from './config'
import { CSSClassDefinition, Config } from './types'
import { textToCssClassSelector, writeCssFile } from './css'
import {
textToCssClassSelector,
textToCssCustomProperyName,
writeCssFile,
} from './css'
import path from 'path'
export async function generateCssFromConfig(
@ -11,6 +15,28 @@ export async function generateCssFromConfig(
const validatedConfig = validateAndNormalizePenpotExportConfig(config)
const penpot = new Penpot({ accessToken: config.accessToken })
for (const colorsConfig of validatedConfig.colors) {
const cssClassDefinition: CSSClassDefinition = {
selector: ':root',
cssProps: {},
}
const { colors } = await penpot.getFileColors({
fileId: colorsConfig.fileId,
})
for (const color of colors) {
const objectClassname = textToCssCustomProperyName(color.name)
cssClassDefinition.cssProps[objectClassname] = color.color // FIXME Add opacity with rgba()
}
const cssPath = path.resolve(rootProjectPath, colorsConfig.output)
writeCssFile(cssPath, [cssClassDefinition])
console.log('✅ Colors: %s', colorsConfig.output)
}
for (const typographiesConfig of validatedConfig.typographies) {
const cssClassDefinitions: CSSClassDefinition[] = []

View file

@ -9,8 +9,14 @@ export interface TypographiesConfig {
fileId: string
}
export interface ColorsConfig {
output: string
fileId: string
}
export interface Config {
accessToken: string
colors: ColorsConfig[]
typographies: TypographiesConfig[]
pages: PagesConfig[]
}