build(lint): enforce shared black color constants
This commit is contained in:
parent
2eab8f9a50
commit
92328a21d8
|
|
@ -533,6 +533,44 @@ const noMathRandom = {
|
|||
}
|
||||
}
|
||||
|
||||
function isNumericLiteral(node, value) {
|
||||
return node?.type === 'Literal' && node.value === value
|
||||
}
|
||||
|
||||
function colorObjectLiteral(node, color) {
|
||||
if (node?.type !== 'ObjectExpression') return false
|
||||
const props = new Map()
|
||||
for (const prop of node.properties ?? []) {
|
||||
if (prop.type !== 'Property') return false
|
||||
const key = prop.key.type === 'Identifier' ? prop.key.name : prop.key.value
|
||||
props.set(key, prop.value)
|
||||
}
|
||||
return Object.entries(color).every(([key, value]) => isNumericLiteral(props.get(key), value))
|
||||
}
|
||||
|
||||
const noHardcodedColorConstants = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Use named color constants instead of inline Color object literals for shared colors'
|
||||
}
|
||||
},
|
||||
create(context) {
|
||||
const file = normalizedFilename(context)
|
||||
if (file.includes('/tests/') || file.endsWith('/packages/core/src/constants.ts')) return {}
|
||||
|
||||
return {
|
||||
ObjectExpression(node) {
|
||||
if (colorObjectLiteral(node, { r: 0, g: 0, b: 0, a: 1 })) {
|
||||
context.report({ node, message: 'Use BLACK from constants instead of an inline black Color literal.' })
|
||||
}
|
||||
if (colorObjectLiteral(node, { r: 0, g: 0, b: 0, a: 0 })) {
|
||||
context.report({ node, message: 'Use TRANSPARENT from constants instead of an inline transparent Color literal.' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const noHandRolledColor = {
|
||||
meta: {
|
||||
docs: {
|
||||
|
|
@ -1553,6 +1591,7 @@ const plugin = {
|
|||
'no-document-query-selector-in-vue': noDocumentQuerySelectorInVue,
|
||||
'no-direct-selection-tool-state-mutation': noDirectSelectionToolStateMutation,
|
||||
'no-math-random': noMathRandom,
|
||||
'no-hardcoded-color-constants': noHardcodedColorConstants,
|
||||
'no-hand-rolled-color': noHandRolledColor,
|
||||
'no-raw-console-format': noRawConsoleFormat,
|
||||
'no-silent-catch': noSilentCatch,
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@
|
|||
"open-pencil/no-document-query-selector-in-vue": "error",
|
||||
"open-pencil/no-direct-selection-tool-state-mutation": "error",
|
||||
"open-pencil/no-math-random": "error",
|
||||
"open-pencil/no-hardcoded-color-constants": "error",
|
||||
"open-pencil/no-hand-rolled-color": "error",
|
||||
"open-pencil/no-raw-console-format": "off",
|
||||
"open-pencil/no-silent-catch": "error",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { TRANSPARENT } from '#core/constants'
|
||||
import { resolvePasteTarget } from '#core/editor/clipboard/paste-target'
|
||||
import type { EditorContext } from '#core/editor/types'
|
||||
import { computeImageHash } from '#core/figma-api'
|
||||
|
|
@ -44,7 +45,7 @@ export function createClipboardImageActions(ctx: EditorContext) {
|
|||
type: 'IMAGE',
|
||||
imageHash: hash,
|
||||
imageScaleMode: 'FILL',
|
||||
color: { r: 0, g: 0, b: 0, a: 0 },
|
||||
color: TRANSPARENT,
|
||||
opacity: 1,
|
||||
visible: true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
BLACK,
|
||||
DEFAULT_FRAME_FILL,
|
||||
DEFAULT_SHAPE_FILL,
|
||||
SECTION_DEFAULT_FILL,
|
||||
|
|
@ -13,7 +14,7 @@ export type { PenDragOptions } from './shapes/pen'
|
|||
|
||||
const BLACK_FILL: Fill = {
|
||||
type: 'SOLID',
|
||||
color: { r: 0, g: 0, b: 0, a: 1 },
|
||||
color: BLACK,
|
||||
opacity: 1,
|
||||
visible: true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { BLACK } from '#core/constants'
|
||||
import type { EditorContext } from '#core/editor/types'
|
||||
import type { SceneNode, VectorNetwork, VectorRegion, VectorSegment } from '#core/scene-graph'
|
||||
import type { Vector } from '#core/types'
|
||||
|
|
@ -19,7 +20,7 @@ type CreateShape = (
|
|||
) => string
|
||||
|
||||
const PEN_DEFAULT_STROKE: SceneNode['strokes'][number] = {
|
||||
color: { r: 0, g: 0, b: 0, a: 1 },
|
||||
color: BLACK,
|
||||
weight: 2,
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { parseColor } from '#core/color'
|
||||
import { BLACK } from '#core/constants'
|
||||
import { generateId } from '#core/scene-graph'
|
||||
import type {
|
||||
Color,
|
||||
|
|
@ -136,7 +137,7 @@ function penValueToSceneValue(raw: string | number, type: VariableType): Variabl
|
|||
}
|
||||
|
||||
function defaultForType(type: VariableType): VariableValue {
|
||||
if (type === 'COLOR') return { r: 0, g: 0, b: 0, a: 1 }
|
||||
if (type === 'COLOR') return { ...BLACK }
|
||||
if (type === 'FLOAT') return 0
|
||||
if (type === 'BOOLEAN') return false
|
||||
return ''
|
||||
|
|
@ -252,7 +253,7 @@ export function buildVarContext(
|
|||
if (val === undefined) return parseColor(ref)
|
||||
if (typeof val === 'object' && 'r' in val) return val
|
||||
if (typeof val === 'string') return parseColor(val)
|
||||
return { r: 0, g: 0, b: 0, a: 1 }
|
||||
return { ...BLACK }
|
||||
},
|
||||
resolveNumber(ref: string): number {
|
||||
const val = resolveVal(ref)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { BLACK } from '#core/constants'
|
||||
import type { NodeChange, VariableDataValuesEntry, Color, GUID } from '#core/kiwi/binary/codec'
|
||||
import { populateAndApplyOverrides } from '#core/kiwi/instance-overrides'
|
||||
import type { InstanceNodeChange } from '#core/kiwi/instance-overrides'
|
||||
|
|
@ -164,7 +165,7 @@ function resolveVariableValue(
|
|||
function resolveDefaultValue(type: VariableType): VariableValue {
|
||||
if (type === 'BOOLEAN') return false
|
||||
if (type === 'STRING') return ''
|
||||
if (type === 'COLOR') return { r: 0, g: 0, b: 0, a: 1 }
|
||||
if (type === 'COLOR') return { ...BLACK }
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { DEFAULT_FONT_FAMILY, DEFAULT_STROKE_MITER_LIMIT } from '#core/constants'
|
||||
import { BLACK, DEFAULT_FONT_FAMILY, DEFAULT_STROKE_MITER_LIMIT } from '#core/constants'
|
||||
|
||||
import type { NodeType, SceneNode } from './types'
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ export function createDefaultNode(
|
|||
figmaDerivedLayout: null,
|
||||
fills:
|
||||
type === 'TEXT'
|
||||
? [{ type: 'SOLID' as const, color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }]
|
||||
? [{ type: 'SOLID' as const, color: BLACK, opacity: 1, visible: true }]
|
||||
: [],
|
||||
strokes: [],
|
||||
effects: [],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { parseColor } from '#core/color'
|
||||
import { DEFAULT_SHADOW_COLOR } from '#core/constants'
|
||||
import { DEFAULT_SHADOW_COLOR, TRANSPARENT } from '#core/constants'
|
||||
import type { Effect } from '#core/scene-graph'
|
||||
import { defineTool, nodeNotFound } from '#core/tools/schema'
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ export const setEffects = defineTool({
|
|||
|
||||
const isBlur = args.type === 'FOREGROUND_BLUR' || args.type === 'BACKGROUND_BLUR'
|
||||
let color = { ...DEFAULT_SHADOW_COLOR }
|
||||
if (isBlur) color = { r: 0, g: 0, b: 0, a: 0 }
|
||||
if (isBlur) color = { ...TRANSPARENT }
|
||||
else if (args.color) color = parseColor(args.color)
|
||||
const effect: Effect = {
|
||||
type: args.type as Effect['type'],
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { parseColor } from '#core/color'
|
||||
import { BLACK } from '#core/constants'
|
||||
import { defineTool } from '#core/tools/schema'
|
||||
import type { Matrix } from '#core/types'
|
||||
|
||||
|
|
@ -116,7 +117,7 @@ export const setImageFill = defineTool({
|
|||
node.fills = [
|
||||
{
|
||||
type: 'IMAGE',
|
||||
color: { r: 0, g: 0, b: 0, a: 1 },
|
||||
color: BLACK,
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
imageHash: image.hash,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Ref } from 'vue'
|
||||
|
||||
import { BLACK } from '@open-pencil/core/constants'
|
||||
import {
|
||||
getFillOkHCL,
|
||||
getStrokeOkHCL,
|
||||
|
|
@ -31,14 +32,14 @@ export function getStrokeOkHCLColor(node: SceneNode | null, index: number): OkHC
|
|||
function fallbackFillOkHCL(node: SceneNode, index: number) {
|
||||
return (
|
||||
getFillOkHCLColor(node, index) ??
|
||||
rgbaToOkHCL(node.fills[index]?.color ?? { r: 0, g: 0, b: 0, a: 1 })
|
||||
rgbaToOkHCL(node.fills[index]?.color ?? BLACK)
|
||||
)
|
||||
}
|
||||
|
||||
function fallbackStrokeOkHCL(node: SceneNode, index: number) {
|
||||
return (
|
||||
getStrokeOkHCLColor(node, index) ??
|
||||
rgbaToOkHCL(node.strokes[index]?.color ?? { r: 0, g: 0, b: 0, a: 1 })
|
||||
rgbaToOkHCL(node.strokes[index]?.color ?? BLACK)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Ref } from 'vue'
|
||||
|
||||
import { BLACK } from '@open-pencil/core/constants'
|
||||
import type { Editor } from '@open-pencil/core/editor'
|
||||
import type { SceneNode, Stroke } from '@open-pencil/core/scene-graph'
|
||||
|
||||
|
|
@ -16,7 +17,7 @@ export const SIDE_OPTIONS: { value: StrokeSides; label: string }[] = [
|
|||
|
||||
export const BORDER_SIDES = ['top', 'right', 'bottom', 'left'] as const
|
||||
export const DEFAULT_STROKE: Stroke = {
|
||||
color: { r: 0, g: 0, b: 0, a: 1 },
|
||||
color: BLACK,
|
||||
weight: 1,
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Ref } from 'vue'
|
||||
|
||||
import { BLACK } from '@open-pencil/core/constants'
|
||||
import { colorToHexRaw, parseColor } from '@open-pencil/core/color'
|
||||
import type { Editor } from '@open-pencil/core/editor'
|
||||
import { randomHex } from '@open-pencil/core/random'
|
||||
|
|
@ -93,7 +94,7 @@ export function createVariableValueActions(
|
|||
getActiveCollection: () => VariableCollection | null
|
||||
) {
|
||||
function defaultVariableValue(type: VariableType): VariableValue {
|
||||
if (type === 'COLOR') return { r: 0, g: 0, b: 0, a: 1 }
|
||||
if (type === 'COLOR') return { ...BLACK }
|
||||
if (type === 'FLOAT') return 0
|
||||
if (type === 'BOOLEAN') return false
|
||||
return ''
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { BLACK } from '@open-pencil/core/constants'
|
||||
import type { Color, Fill, GradientStop, Stroke } from '@open-pencil/core/scene-graph'
|
||||
|
||||
export { BLACK }
|
||||
|
||||
export const WHITE: Color = { r: 1, g: 1, b: 1, a: 1 }
|
||||
export const BLACK: Color = { r: 0, g: 0, b: 0, a: 1 }
|
||||
export const GRAY_50: Color = { r: 0.98, g: 0.98, b: 0.98, a: 1 }
|
||||
export const GRAY_100: Color = { r: 0.96, g: 0.96, b: 0.97, a: 1 }
|
||||
export const GRAY_200: Color = { r: 0.9, g: 0.9, b: 0.92, a: 1 }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { TRANSPARENT } from '@open-pencil/core/constants'
|
||||
import type { Color, Effect } from '@open-pencil/core/scene-graph'
|
||||
|
||||
export function dropShadow(
|
||||
|
|
@ -37,7 +38,7 @@ export function innerShadow(
|
|||
export function blurEffect(type: Effect['type'], radius: number): Effect {
|
||||
return {
|
||||
type,
|
||||
color: { r: 0, g: 0, b: 0, a: 0 },
|
||||
color: TRANSPARENT,
|
||||
offset: { x: 0, y: 0 },
|
||||
radius,
|
||||
spread: 0,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { initCanvasKit } from '#cli/headless'
|
||||
import { BLACK } from '#core/constants'
|
||||
import { makeBooleanOperationPath } from '#core/canvas/boolean'
|
||||
import { makeNodeShapePath, makePolygonPath, makeRRect } from '#core/canvas/shapes'
|
||||
import type { SkiaRenderer } from '#core/canvas/renderer'
|
||||
|
|
@ -144,8 +145,8 @@ describe('boolean operation paths', () => {
|
|||
expect(firstNode).toBeDefined()
|
||||
expect(secondNode).toBeDefined()
|
||||
if (!firstNode || !secondNode) return
|
||||
firstNode.strokes = [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true, weight: 12, align: 'CENTER' }]
|
||||
secondNode.strokes = [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true, weight: 12, align: 'CENTER' }]
|
||||
firstNode.strokes = [{ type: 'SOLID', color: BLACK, opacity: 1, visible: true, weight: 12, align: 'CENTER' }]
|
||||
secondNode.strokes = [{ type: 'SOLID', color: BLACK, opacity: 1, visible: true, weight: 12, align: 'CENTER' }]
|
||||
|
||||
const booleanNode = api.union([first, second], api.currentPage)
|
||||
const node = api.graph.getNode(booleanNode.id)
|
||||
|
|
|
|||
Loading…
Reference in a new issue