Format
This commit is contained in:
parent
07674d1919
commit
cc7f055dfc
|
|
@ -1,6 +1,6 @@
|
|||
import { computeAccurateBounds } from '../vector/bezier'
|
||||
import { PEN_HANDLE_RADIUS, PEN_VERTEX_RADIUS } from '../constants'
|
||||
import { vectorNetworkToPath } from '../vector'
|
||||
import { computeAccurateBounds } from '../vector/bezier'
|
||||
|
||||
import type { VectorVertex, VectorSegment, VectorRegion, SceneGraph } from '../scene-graph'
|
||||
import type { Vector } from '../types'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { converter, toGamut } from 'culori'
|
||||
|
||||
import { normalizeColor } from './'
|
||||
import { copyFill, copyStroke } from '../scene-graph/copy'
|
||||
import { normalizeColor } from './'
|
||||
|
||||
import type { SceneNode } from '../scene-graph'
|
||||
import type { Color } from '../types'
|
||||
|
|
@ -111,7 +111,11 @@ export function parseOkHCLPayload(value: string): OkHCLPayload | null {
|
|||
}
|
||||
}
|
||||
|
||||
function createOkHCLPayload(kind: 'fill' | 'stroke', index: number, color: OkHCLColor): OkHCLPayload {
|
||||
function createOkHCLPayload(
|
||||
kind: 'fill' | 'stroke',
|
||||
index: number,
|
||||
color: OkHCLColor
|
||||
): OkHCLPayload {
|
||||
return {
|
||||
version: 1,
|
||||
kind,
|
||||
|
|
@ -120,7 +124,11 @@ function createOkHCLPayload(kind: 'fill' | 'stroke', index: number, color: OkHCL
|
|||
}
|
||||
}
|
||||
|
||||
function filterOkHCLPayloads(entries: string[], kind?: 'fill' | 'stroke', index?: number): string[] {
|
||||
function filterOkHCLPayloads(
|
||||
entries: string[],
|
||||
kind?: 'fill' | 'stroke',
|
||||
index?: number
|
||||
): string[] {
|
||||
return entries.filter((entry) => {
|
||||
const payload = parseOkHCLPayload(entry)
|
||||
if (!payload) return true
|
||||
|
|
@ -129,7 +137,11 @@ function filterOkHCLPayloads(entries: string[], kind?: 'fill' | 'stroke', index?
|
|||
})
|
||||
}
|
||||
|
||||
export function setNodeFillOkHCL(node: SceneNode, index: number, color: OkHCLColor): Partial<SceneNode> {
|
||||
export function setNodeFillOkHCL(
|
||||
node: SceneNode,
|
||||
index: number,
|
||||
color: OkHCLColor
|
||||
): Partial<SceneNode> {
|
||||
const fills = node.fills.map(copyFill)
|
||||
if (index < 0 || index >= fills.length) throw new Error(`Fill ${index} not found`)
|
||||
const fill = fills[index]
|
||||
|
|
@ -140,7 +152,11 @@ export function setNodeFillOkHCL(node: SceneNode, index: number, color: OkHCLCol
|
|||
opacity: rgba.a
|
||||
}
|
||||
|
||||
const payloads = filterOkHCLPayloads(node.pluginData.map((entry) => entry.value), 'fill', index)
|
||||
const payloads = filterOkHCLPayloads(
|
||||
node.pluginData.map((entry) => entry.value),
|
||||
'fill',
|
||||
index
|
||||
)
|
||||
payloads.push(serializeOkHCLPayload(createOkHCLPayload('fill', index, color)))
|
||||
|
||||
return {
|
||||
|
|
@ -149,7 +165,11 @@ export function setNodeFillOkHCL(node: SceneNode, index: number, color: OkHCLCol
|
|||
}
|
||||
}
|
||||
|
||||
export function setNodeStrokeOkHCL(node: SceneNode, index: number, color: OkHCLColor): Partial<SceneNode> {
|
||||
export function setNodeStrokeOkHCL(
|
||||
node: SceneNode,
|
||||
index: number,
|
||||
color: OkHCLColor
|
||||
): Partial<SceneNode> {
|
||||
const strokes = node.strokes.map(copyStroke)
|
||||
if (index < 0 || index >= strokes.length) throw new Error(`Stroke ${index} not found`)
|
||||
const stroke = strokes[index]
|
||||
|
|
@ -160,7 +180,11 @@ export function setNodeStrokeOkHCL(node: SceneNode, index: number, color: OkHCLC
|
|||
opacity: rgba.a
|
||||
}
|
||||
|
||||
const payloads = filterOkHCLPayloads(node.pluginData.map((entry) => entry.value), 'stroke', index)
|
||||
const payloads = filterOkHCLPayloads(
|
||||
node.pluginData.map((entry) => entry.value),
|
||||
'stroke',
|
||||
index
|
||||
)
|
||||
payloads.push(serializeOkHCLPayload(createOkHCLPayload('stroke', index, color)))
|
||||
|
||||
return {
|
||||
|
|
@ -170,16 +194,32 @@ export function setNodeStrokeOkHCL(node: SceneNode, index: number, color: OkHCLC
|
|||
}
|
||||
|
||||
export function clearNodeFillOkHCL(node: SceneNode, index: number): Partial<SceneNode> {
|
||||
const okhclValues = filterOkHCLPayloads(node.pluginData.map((entry) => entry.value), 'fill', index)
|
||||
const okhclValues = filterOkHCLPayloads(
|
||||
node.pluginData.map((entry) => entry.value),
|
||||
'fill',
|
||||
index
|
||||
)
|
||||
return {
|
||||
pluginData: okhclValues.map((value) => ({ pluginId: 'open-pencil', key: OKHCL_PLUGIN_KEY, value }))
|
||||
pluginData: okhclValues.map((value) => ({
|
||||
pluginId: 'open-pencil',
|
||||
key: OKHCL_PLUGIN_KEY,
|
||||
value
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export function clearNodeStrokeOkHCL(node: SceneNode, index: number): Partial<SceneNode> {
|
||||
const okhclValues = filterOkHCLPayloads(node.pluginData.map((entry) => entry.value), 'stroke', index)
|
||||
const okhclValues = filterOkHCLPayloads(
|
||||
node.pluginData.map((entry) => entry.value),
|
||||
'stroke',
|
||||
index
|
||||
)
|
||||
return {
|
||||
pluginData: okhclValues.map((value) => ({ pluginId: 'open-pencil', key: OKHCL_PLUGIN_KEY, value }))
|
||||
pluginData: okhclValues.map((value) => ({
|
||||
pluginId: 'open-pencil',
|
||||
key: OKHCL_PLUGIN_KEY,
|
||||
value
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -191,9 +231,17 @@ export function getNodeOkHCLPayloads(node: SceneNode): OkHCLPayload[] {
|
|||
}
|
||||
|
||||
export function getFillOkHCL(node: SceneNode, index: number): OkHCLPayload | null {
|
||||
return getNodeOkHCLPayloads(node).find((payload) => payload.kind === 'fill' && payload.index === index) ?? null
|
||||
return (
|
||||
getNodeOkHCLPayloads(node).find(
|
||||
(payload) => payload.kind === 'fill' && payload.index === index
|
||||
) ?? null
|
||||
)
|
||||
}
|
||||
|
||||
export function getStrokeOkHCL(node: SceneNode, index: number): OkHCLPayload | null {
|
||||
return getNodeOkHCLPayloads(node).find((payload) => payload.kind === 'stroke' && payload.index === index) ?? null
|
||||
return (
|
||||
getNodeOkHCLPayloads(node).find(
|
||||
(payload) => payload.kind === 'stroke' && payload.index === index
|
||||
) ?? null
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { parseColor, colorToFill } from '../color'
|
||||
import { TRANSPARENT } from '../constants'
|
||||
import { createIconFromPaths } from '../icons/render'
|
||||
import { fetchIcons } from '../icons'
|
||||
import { createIconFromPaths } from '../icons/render'
|
||||
import { computeAllLayouts } from '../layout'
|
||||
import { isTreeNode } from './tree'
|
||||
|
||||
|
|
@ -405,7 +405,8 @@ function applyLayoutOverrides(
|
|||
applyAutoLayoutSizing(o, props, w, h)
|
||||
}
|
||||
|
||||
o.layoutDirection = parseDirection(props.flow ?? (!isText ? props.dir : undefined)) ?? o.layoutDirection
|
||||
o.layoutDirection =
|
||||
parseDirection(props.flow ?? (!isText ? props.dir : undefined)) ?? o.layoutDirection
|
||||
|
||||
if (props.gap !== undefined) o.itemSpacing = props.gap as number
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { buildFigmaClipboardHTML, importClipboardNodes, parseFigmaClipboard } from '../clipboard'
|
||||
import { selectionToJSX } from '../design-jsx'
|
||||
import { computeImageHash } from '../figma-api'
|
||||
import { collectFontKeys } from '../text/fonts'
|
||||
import { computeBounds } from '../geometry'
|
||||
import { renderNodesToSVG } from '../io/formats/svg'
|
||||
import { computeAllLayouts } from '../layout'
|
||||
import { selectionToJSX } from '../design-jsx'
|
||||
import { collectFontKeys } from '../text/fonts'
|
||||
|
||||
import type { Fill, SceneGraph, SceneNode } from '../scene-graph'
|
||||
import type { Vector } from '../types'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { copyEffects, copyFill, copyStyleRuns, copyStroke } from '../scene-graph/copy'
|
||||
import { resolveOkHCLForPreview } from '../color/management'
|
||||
import { rgbaToOkHCL } from '../color/okhcl'
|
||||
import { copyEffects, copyFill, copyStyleRuns, copyStroke } from '../scene-graph/copy'
|
||||
|
||||
import type { DocumentColorSpace, SceneNode } from '../scene-graph'
|
||||
import type { EditorContext } from './types'
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { prefetchFigmaSchema } from '../clipboard'
|
||||
import { CANVAS_BG_COLOR, IS_BROWSER } from '../constants'
|
||||
import { loadFont as defaultLoadFont } from '../text/fonts'
|
||||
import { computeAllLayouts, computeLayout, setTextMeasurer } from '../layout'
|
||||
import { SceneGraph } from '../scene-graph'
|
||||
import { TextEditor } from '../text/editor'
|
||||
import { UndoManager } from '../scene-graph/undo'
|
||||
import { TextEditor } from '../text/editor'
|
||||
import { loadFont as defaultLoadFont } from '../text/fonts'
|
||||
import { createAlignmentActions } from './alignment'
|
||||
import { createClipboardActions } from './clipboard'
|
||||
import { createColorSpaceActions } from './color-space'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { CANVAS_BG_COLOR } from '../constants'
|
||||
import { collectFontKeys } from '../text/fonts'
|
||||
import { computeAllLayouts } from '../layout'
|
||||
import { collectFontKeys } from '../text/fonts'
|
||||
|
||||
import type { Color } from '../types'
|
||||
import type { EditorContext } from './types'
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import type { SkiaRenderer } from '../canvas/renderer'
|
||||
import type { SceneGraph, VectorSegment, VectorVertex } from '../scene-graph'
|
||||
import type { SnapGuide } from '../scene-graph/snap'
|
||||
import type { UndoManager } from '../scene-graph/undo'
|
||||
import type { TextEditor } from '../text/editor'
|
||||
import type { Color, Rect, Vector } from '../types'
|
||||
import type { UndoManager } from '../scene-graph/undo'
|
||||
import type { CanvasKit } from 'canvaskit-wasm'
|
||||
|
||||
export type Tool =
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { computeAllLayouts } from '../layout'
|
||||
|
||||
import type { SceneNode } from '../scene-graph'
|
||||
import type { Rect, Vector } from '../types'
|
||||
import type { UndoEntry } from '../scene-graph/undo'
|
||||
import type { Rect, Vector } from '../types'
|
||||
import type { EditorContext } from './types'
|
||||
|
||||
export function createUndoActions(ctx: EditorContext) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
import { IS_BROWSER } from '../constants'
|
||||
import { copyFills, copyStrokes, copyEffects } from '../scene-graph/copy'
|
||||
import {
|
||||
FigmaNodeProxy,
|
||||
INTERNAL_ID,
|
||||
MIXED,
|
||||
type FigmaFontName,
|
||||
type NodeProxyHost
|
||||
} from './proxy'
|
||||
import { computeBounds } from '../geometry'
|
||||
import { copyFills, copyStrokes, copyEffects } from '../scene-graph/copy'
|
||||
import { FigmaNodeProxy, INTERNAL_ID, MIXED, type FigmaFontName, type NodeProxyHost } from './proxy'
|
||||
|
||||
import type { RasterExportFormat } from '../io/formats/raster'
|
||||
import type {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { normalizeColor } from '../color'
|
||||
import { getFillOkHCL, getStrokeOkHCL, setNodeFillOkHCL, setNodeStrokeOkHCL } from '../color/okhcl'
|
||||
import { copyFills, copyStrokes, copyEffects } from '../scene-graph/copy'
|
||||
import { FONT_WEIGHT_NAMES } from '../text/fonts'
|
||||
import { getFillOkHCL, getStrokeOkHCL, setNodeFillOkHCL, setNodeStrokeOkHCL } from '../color/okhcl'
|
||||
|
||||
import type { OkHCLColor, OkHCLPayload } from '../color/okhcl'
|
||||
/* eslint-disable max-lines -- Figma Plugin API proxy; FigmaAPI already in separate file */
|
||||
import type {
|
||||
SceneGraph,
|
||||
|
|
@ -14,7 +15,6 @@ import type {
|
|||
LayoutMode
|
||||
} from '../scene-graph'
|
||||
import type { Rect } from '../types'
|
||||
import type { OkHCLColor, OkHCLPayload } from '../color/okhcl'
|
||||
|
||||
const MIXED = Symbol('mixed')
|
||||
|
||||
|
|
@ -961,9 +961,11 @@ export class FigmaNodeProxy {
|
|||
// --- Plugin data ---
|
||||
|
||||
getPluginData(key: string): string {
|
||||
return this._raw().pluginData.find(
|
||||
(entry) => entry.pluginId === OPEN_PENCIL_PLUGIN_DATA_NAMESPACE && entry.key === key
|
||||
)?.value ?? ''
|
||||
return (
|
||||
this._raw().pluginData.find(
|
||||
(entry) => entry.pluginId === OPEN_PENCIL_PLUGIN_DATA_NAMESPACE && entry.key === key
|
||||
)?.value ?? ''
|
||||
)
|
||||
}
|
||||
|
||||
setPluginData(key: string, value: string): void {
|
||||
|
|
@ -978,15 +980,17 @@ export class FigmaNodeProxy {
|
|||
}
|
||||
|
||||
getPluginDataKeys(): string[] {
|
||||
return this._raw().pluginData
|
||||
.filter((entry) => entry.pluginId === OPEN_PENCIL_PLUGIN_DATA_NAMESPACE)
|
||||
return this._raw()
|
||||
.pluginData.filter((entry) => entry.pluginId === OPEN_PENCIL_PLUGIN_DATA_NAMESPACE)
|
||||
.map((entry) => entry.key)
|
||||
}
|
||||
|
||||
getSharedPluginData(namespace: string, key: string): string {
|
||||
return this._raw().sharedPluginData.find(
|
||||
(entry) => entry.namespace === namespace && entry.key === key
|
||||
)?.value ?? ''
|
||||
return (
|
||||
this._raw().sharedPluginData.find(
|
||||
(entry) => entry.namespace === namespace && entry.key === key
|
||||
)?.value ?? ''
|
||||
)
|
||||
}
|
||||
|
||||
setSharedPluginData(namespace: string, key: string, value: string): void {
|
||||
|
|
@ -1001,8 +1005,8 @@ export class FigmaNodeProxy {
|
|||
}
|
||||
|
||||
getSharedPluginDataKeys(namespace: string): string[] {
|
||||
return this._raw().sharedPluginData
|
||||
.filter((entry) => entry.namespace === namespace)
|
||||
return this._raw()
|
||||
.sharedPluginData.filter((entry) => entry.namespace === namespace)
|
||||
.map((entry) => entry.key)
|
||||
}
|
||||
|
||||
|
|
@ -1019,7 +1023,10 @@ export class FigmaNodeProxy {
|
|||
}
|
||||
|
||||
setStrokeOkHCL(color: OkHCLColor, index = 0): void {
|
||||
this[INTERNAL_GRAPH].updateNode(this[INTERNAL_ID], setNodeStrokeOkHCL(this._raw(), index, color))
|
||||
this[INTERNAL_GRAPH].updateNode(
|
||||
this[INTERNAL_ID],
|
||||
setNodeStrokeOkHCL(this._raw(), index, color)
|
||||
)
|
||||
}
|
||||
|
||||
// --- Serialization ---
|
||||
|
|
|
|||
|
|
@ -102,7 +102,11 @@ function effectOverflow(effects?: Effect[]) {
|
|||
|
||||
for (const effect of effects ?? []) {
|
||||
if (!effect.visible) continue
|
||||
if (effect.type !== 'DROP_SHADOW' && effect.type !== 'LAYER_BLUR' && effect.type !== 'FOREGROUND_BLUR') {
|
||||
if (
|
||||
effect.type !== 'DROP_SHADOW' &&
|
||||
effect.type !== 'LAYER_BLUR' &&
|
||||
effect.type !== 'FOREGROUND_BLUR'
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const blurSpread = effect.radius + effect.spread
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { parseColor } from '../color'
|
||||
|
||||
import type { IconData } from './'
|
||||
import type { SceneGraph, SceneNode, Stroke } from '../scene-graph'
|
||||
import type { Color } from '../types'
|
||||
import type { IconData } from './'
|
||||
|
||||
const STROKE_CAP_MAP: Record<string, SceneNode['strokeCap']> = {
|
||||
butt: 'NONE',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { sceneNodeToJSX, selectionToJSX } from '../design-jsx'
|
||||
|
||||
import { exportFigFile, parseFigFile } from './formats/fig'
|
||||
import { parsePenFile } from './formats/pen'
|
||||
import { headlessRenderNodes, renderNodesToImage, type RasterExportFormat } from './formats/raster'
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { deflateSync } from 'fflate'
|
||||
|
||||
import { CANVAS_BG_COLOR, IS_BROWSER, IS_TAURI } from '../../../constants'
|
||||
import { compressFigDataSync } from './compress'
|
||||
import { renderThumbnail } from '../raster'
|
||||
import { initCodec, getCompiledSchema, getSchemaBytes } from '../../../kiwi/codec'
|
||||
import { stringToGuid } from '../../../kiwi/convert'
|
||||
import {
|
||||
|
|
@ -13,9 +11,11 @@ import {
|
|||
makeDocumentNodeChange,
|
||||
makeCanvasNodeChange
|
||||
} from '../../../kiwi/serialize'
|
||||
import { renderThumbnail } from '../raster'
|
||||
import { compressFigDataSync } from './compress'
|
||||
|
||||
import type { NodeChange } from '../../../kiwi/codec'
|
||||
import type { SkiaRenderer } from '../../../canvas'
|
||||
import type { NodeChange } from '../../../kiwi/codec'
|
||||
import type { SceneGraph, VariableValue } from '../../../scene-graph'
|
||||
import type { GUID } from '../../../types'
|
||||
import type { CanvasKit } from 'canvaskit-wasm'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
/* eslint-disable max-lines -- JSX export formats share helpers and node walking logic */
|
||||
import { colorToHex8, colorToCSSCompact } from '../../../color'
|
||||
import { DEFAULT_FONT_FAMILY } from '../../../constants'
|
||||
import { resolveNodeTextDirection } from '../../../text/direction'
|
||||
import {
|
||||
pxToSpacing,
|
||||
colorToTwClass,
|
||||
|
|
@ -10,6 +9,7 @@ import {
|
|||
borderRadiusToTw,
|
||||
opacityToTw
|
||||
} from '../../../design-jsx/tailwind'
|
||||
import { resolveNodeTextDirection } from '../../../text/direction'
|
||||
|
||||
import type {
|
||||
SceneGraph,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { SceneGraph } from '../../../scene-graph'
|
||||
import { copyEffects, copyFills, copyStrokes } from '../../../scene-graph/copy'
|
||||
import { populateInstanceChildren } from '../../../scene-graph/instances'
|
||||
import { parseSVGPath } from '../svg/parse-path'
|
||||
import {
|
||||
applyCornerRadius,
|
||||
applyPadding,
|
||||
|
|
@ -20,16 +23,8 @@ import {
|
|||
type PenNode,
|
||||
type VarContext
|
||||
} from './convert'
|
||||
import { parseSVGPath } from '../svg/parse-path'
|
||||
import { SceneGraph } from '../../../scene-graph'
|
||||
import { populateInstanceChildren } from '../../../scene-graph/instances'
|
||||
|
||||
import type {
|
||||
LayoutMode,
|
||||
LayoutSizing,
|
||||
SceneNode,
|
||||
VectorNetwork
|
||||
} from '../../../scene-graph'
|
||||
import type { LayoutMode, LayoutSizing, SceneNode, VectorNetwork } from '../../../scene-graph'
|
||||
|
||||
function scaleVectorNetwork(vn: VectorNetwork, targetW: number, targetH: number): void {
|
||||
if (vn.vertices.length === 0) return
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { SkiaRenderer } from '../../../canvas'
|
||||
|
||||
import { renderNodesToImage, renderThumbnail, type ExportFormat } from './render'
|
||||
|
||||
import type { SceneGraph } from '../../../scene-graph'
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { computeVisualBounds } from '../../../geometry'
|
||||
import { extractExportGraph } from '../../subgraph'
|
||||
|
||||
import type { RenderColorSpace } from '../../../color/management'
|
||||
import type { SkiaRenderer } from '../../../canvas'
|
||||
import type { RenderColorSpace } from '../../../color/management'
|
||||
import type { SceneGraph } from '../../../scene-graph'
|
||||
import type { CanvasKit, Canvas } from 'canvaskit-wasm'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { colorToHex } from '../../../color'
|
||||
import { colorToDisplayCss, getDefaultRenderColorSpace } from '../../../color/management'
|
||||
|
||||
import { svg, type SVGNode } from './node'
|
||||
import { round } from './paths'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { computeContentBounds } from '../raster'
|
||||
|
||||
import { resolveNodeTextDirection } from '../../../text/direction'
|
||||
import { computeContentBounds } from '../raster'
|
||||
import {
|
||||
nextDefId,
|
||||
formatColor,
|
||||
|
|
@ -24,8 +23,6 @@ export { geometryBlobToSVGPath, vectorNetworkToSVGPaths } from './paths'
|
|||
|
||||
import { svg, renderSVGNode } from './node'
|
||||
|
||||
import type { SVGExportContext } from './defs'
|
||||
import type { SVGNode } from './node'
|
||||
import type {
|
||||
SceneGraph,
|
||||
SceneNode,
|
||||
|
|
@ -33,6 +30,8 @@ import type {
|
|||
Stroke,
|
||||
CharacterStyleOverride
|
||||
} from '../../../scene-graph'
|
||||
import type { SVGExportContext } from './defs'
|
||||
import type { SVGNode } from './node'
|
||||
|
||||
// --- Node rendering ---
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,4 @@
|
|||
import type {
|
||||
SceneNode,
|
||||
VectorNetwork,
|
||||
VectorSegment,
|
||||
VectorVertex
|
||||
} from '../../../scene-graph'
|
||||
import type { SceneNode, VectorNetwork, VectorSegment, VectorVertex } from '../../../scene-graph'
|
||||
|
||||
const CMD_CLOSE = 0
|
||||
const CMD_MOVE_TO = 1
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { SkiaRenderer } from '../canvas'
|
||||
import type { RenderColorSpace } from '../color/management'
|
||||
import type { JSXFormat } from '../design-jsx'
|
||||
import type { SkiaRenderer } from '../canvas'
|
||||
import type { SceneGraph } from '../scene-graph'
|
||||
import type { RasterExportFormat } from './formats/raster'
|
||||
import type { CanvasKit } from 'canvaskit-wasm'
|
||||
|
|
|
|||
|
|
@ -620,8 +620,9 @@ function convertTextProps(
|
|||
styleRuns: importStyleRuns(nc),
|
||||
textTruncation: (nc.textTruncation as string) === 'ENDING' ? 'ENDING' : 'DISABLED',
|
||||
textDirection:
|
||||
(getOpenPencilPluginValue(nc, TEXT_DIRECTION_PLUGIN_KEY) as SceneNode['textDirection'] | null) ||
|
||||
'AUTO'
|
||||
(getOpenPencilPluginValue(nc, TEXT_DIRECTION_PLUGIN_KEY) as
|
||||
| SceneNode['textDirection']
|
||||
| null) || 'AUTO'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -680,8 +681,7 @@ function convertLayoutProps(
|
|||
layoutDirection:
|
||||
(getOpenPencilPluginValue(nc, LAYOUT_DIRECTION_PLUGIN_KEY) as
|
||||
| SceneNode['layoutDirection']
|
||||
| null) ||
|
||||
'AUTO'
|
||||
| null) || 'AUTO'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,11 @@ function createStrokePaints(context: SceneNodeToKiwiContext, node: SceneNode): P
|
|||
}))
|
||||
}
|
||||
|
||||
function applyNodeVisualProps(context: SceneNodeToKiwiContext, node: SceneNode, nc: KiwiNodeChange): void {
|
||||
function applyNodeVisualProps(
|
||||
context: SceneNodeToKiwiContext,
|
||||
node: SceneNode,
|
||||
nc: KiwiNodeChange
|
||||
): void {
|
||||
if (node.independentStrokeWeights) {
|
||||
nc.borderStrokeWeightsIndependent = true
|
||||
nc.borderTopWeight = node.borderTopWeight
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
import { defineRule } from '../rule'
|
||||
|
||||
export default defineRule({
|
||||
meta: { id: 'effect-style-required', category: 'design-tokens', description: 'Effects should use shared effect presets or tokens' },
|
||||
meta: {
|
||||
id: 'effect-style-required',
|
||||
category: 'design-tokens',
|
||||
description: 'Effects should use shared effect presets or tokens'
|
||||
},
|
||||
check(node, context) {
|
||||
const visibleEffects = node.effects.filter((effect) => effect.visible)
|
||||
if (visibleEffects.length === 0) return
|
||||
context.report({ node, message: `Effect without shared style: ${visibleEffects.map((effect) => `${effect.type} ${effect.radius}px`).join(', ')}`, suggest: 'Extract reusable shadows and blurs into shared presets or variables' })
|
||||
context.report({
|
||||
node,
|
||||
message: `Effect without shared style: ${visibleEffects.map((effect) => `${effect.type} ${effect.radius}px`).join(', ')}`,
|
||||
suggest: 'Extract reusable shadows and blurs into shared presets or variables'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,20 @@
|
|||
import { defineRule } from '../rule'
|
||||
|
||||
export default defineRule({
|
||||
meta: { id: 'min-text-size', category: 'accessibility', description: 'Text should be large enough to be readable (minimum 12px)' },
|
||||
meta: {
|
||||
id: 'min-text-size',
|
||||
category: 'accessibility',
|
||||
description: 'Text should be large enough to be readable (minimum 12px)'
|
||||
},
|
||||
match: ['TEXT'],
|
||||
check(node, context) {
|
||||
const config = context.getConfig() as { minSize?: number } | undefined
|
||||
const minSize = config?.minSize ?? 12
|
||||
if (node.fontSize < minSize) context.report({ node, message: `Text size ${node.fontSize}px is below minimum ${minSize}px`, suggest: `Increase to at least ${minSize}px for readability` })
|
||||
if (node.fontSize < minSize)
|
||||
context.report({
|
||||
node,
|
||||
message: `Text size ${node.fontSize}px is below minimum ${minSize}px`,
|
||||
suggest: `Increase to at least ${minSize}px for readability`
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,13 +1,33 @@
|
|||
import { defineRule } from '../rule'
|
||||
|
||||
export default defineRule({
|
||||
meta: { id: 'no-hardcoded-colors', category: 'design-tokens', description: 'Colors should use variables instead of hardcoded values' },
|
||||
match: ['RECTANGLE','ELLIPSE','FRAME','TEXT','VECTOR','LINE','POLYGON','STAR','COMPONENT','INSTANCE'],
|
||||
meta: {
|
||||
id: 'no-hardcoded-colors',
|
||||
category: 'design-tokens',
|
||||
description: 'Colors should use variables instead of hardcoded values'
|
||||
},
|
||||
match: [
|
||||
'RECTANGLE',
|
||||
'ELLIPSE',
|
||||
'FRAME',
|
||||
'TEXT',
|
||||
'VECTOR',
|
||||
'LINE',
|
||||
'POLYGON',
|
||||
'STAR',
|
||||
'COMPONENT',
|
||||
'INSTANCE'
|
||||
],
|
||||
check(node, context) {
|
||||
const checkPaints = (paints: typeof node.fills, field: 'fills' | 'strokes') => {
|
||||
for (const paint of paints) {
|
||||
if (paint.type !== 'SOLID' || !paint.visible || !paint.color || node.boundVariables[field]) continue
|
||||
context.report({ node, message: `Hardcoded ${field === 'fills' ? 'fill' : 'stroke'} color detected`, suggest: 'Bind this color to a design variable for consistency' })
|
||||
if (paint.type !== 'SOLID' || !paint.visible || !paint.color || node.boundVariables[field])
|
||||
continue
|
||||
context.report({
|
||||
node,
|
||||
message: `Hardcoded ${field === 'fills' ? 'fill' : 'stroke'} color detected`,
|
||||
suggest: 'Bind this color to a design variable for consistency'
|
||||
})
|
||||
}
|
||||
}
|
||||
checkPaints(node.fills, 'fills')
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
import { defineRule } from '../rule'
|
||||
|
||||
export default defineRule({
|
||||
meta: { id: 'no-mixed-styles', category: 'typography', description: 'Text layers should not mix multiple styles in one node' },
|
||||
meta: {
|
||||
id: 'no-mixed-styles',
|
||||
category: 'typography',
|
||||
description: 'Text layers should not mix multiple styles in one node'
|
||||
},
|
||||
match: ['TEXT'],
|
||||
check(node, context) {
|
||||
if (node.text.length > 1 && node.styleRunCount > 0) {
|
||||
context.report({ node, message: 'Text layer has mixed font styles', suggest: 'Split into separate text layers or unify the text style' })
|
||||
context.report({
|
||||
node,
|
||||
message: 'Text layer has mixed font styles',
|
||||
suggest: 'Split into separate text layers or unify the text style'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
import { defineRule } from '../rule'
|
||||
|
||||
export default defineRule({
|
||||
meta: { id: 'text-style-required', category: 'typography', description: 'Text layers should use shared typography tokens or styles' },
|
||||
meta: {
|
||||
id: 'text-style-required',
|
||||
category: 'typography',
|
||||
description: 'Text layers should use shared typography tokens or styles'
|
||||
},
|
||||
match: ['TEXT'],
|
||||
check(node, context) {
|
||||
if (node.text.length <= 2) return
|
||||
if (node.boundVariables.fontSize || node.boundVariables.fontFamily) return
|
||||
context.report({ node, message: 'Text layer without typography variable bindings', suggest: 'Bind font size or font family to a shared text token when possible' })
|
||||
context.report({
|
||||
node,
|
||||
message: 'Text layer without typography variable bindings',
|
||||
suggest: 'Bind font size or font family to a shared text token when possible'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { rotatedBBox } from '../geometry'
|
||||
|
||||
import type { SceneNode } from './'
|
||||
import type { Rect } from '../types'
|
||||
import type { SceneNode } from './'
|
||||
|
||||
const SNAP_THRESHOLD = 5
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { LayoutDirection, SceneNode, TextDirection } from '../scene-graph'
|
||||
|
||||
const RTL_CHAR_RE = /\p{Script=Arabic}|\p{Script=Hebrew}|\p{Script=Syriac}|\p{Script=Thaana}|\p{Script=Nko}|\p{Script=Adlam}/u
|
||||
const RTL_CHAR_RE =
|
||||
/\p{Script=Arabic}|\p{Script=Hebrew}|\p{Script=Syriac}|\p{Script=Thaana}|\p{Script=Nko}|\p{Script=Adlam}/u
|
||||
const LTR_CHAR_RE = /\p{Script=Latin}|\p{Script=Cyrillic}|\p{Script=Greek}/u
|
||||
|
||||
export function detectTextDirection(text: string): Exclude<TextDirection, 'AUTO'> {
|
||||
|
|
@ -18,7 +19,9 @@ export function resolveTextDirection(
|
|||
return direction === 'AUTO' ? detectTextDirection(text) : direction
|
||||
}
|
||||
|
||||
export function resolveNodeTextDirection(node: Pick<SceneNode, 'textDirection' | 'text'>): 'LTR' | 'RTL' {
|
||||
export function resolveNodeTextDirection(
|
||||
node: Pick<SceneNode, 'textDirection' | 'text'>
|
||||
): 'LTR' | 'RTL' {
|
||||
return resolveTextDirection(node.textDirection, node.text)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { SkiaRenderer } from '../canvas'
|
||||
import { resolveNodeTextDirection } from './direction'
|
||||
|
||||
import type { SkiaRenderer } from '../canvas'
|
||||
import type { SceneNode } from '../scene-graph'
|
||||
import type { Rect } from '../types'
|
||||
import type { CanvasKit, Paragraph } from 'canvaskit-wasm'
|
||||
|
|
@ -258,8 +259,7 @@ export class TextEditor {
|
|||
if (lineNum < 0) return
|
||||
const metrics = s.paragraph.getLineMetricsAt(lineNum)
|
||||
if (!metrics) return
|
||||
s.cursor =
|
||||
s.textDirection === 'RTL' ? metrics.endExcludingWhitespaces : metrics.startIndex
|
||||
s.cursor = s.textDirection === 'RTL' ? metrics.endExcludingWhitespaces : metrics.startIndex
|
||||
}
|
||||
|
||||
moveToLineEnd(extend = false): void {
|
||||
|
|
@ -270,8 +270,7 @@ export class TextEditor {
|
|||
if (lineNum < 0) return
|
||||
const metrics = s.paragraph.getLineMetricsAt(lineNum)
|
||||
if (!metrics) return
|
||||
s.cursor =
|
||||
s.textDirection === 'RTL' ? metrics.startIndex : metrics.endExcludingWhitespaces
|
||||
s.cursor = s.textDirection === 'RTL' ? metrics.startIndex : metrics.endExcludingWhitespaces
|
||||
}
|
||||
|
||||
moveWordLeft(extend = false): void {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { parseColor } from '../color'
|
||||
import { createIconFromPaths } from '../icons/render'
|
||||
import { fetchIcons, searchIconsBatch } from '../icons'
|
||||
import { createIconFromPaths } from '../icons/render'
|
||||
import { defineTool, nodeSummary } from './schema'
|
||||
|
||||
import type { FigmaNodeProxy } from '../figma-api'
|
||||
|
|
|
|||
|
|
@ -91,7 +91,11 @@ export function updateRGBChannel(color: Color, channel: 'r' | 'g' | 'b', value25
|
|||
}
|
||||
}
|
||||
|
||||
export function updateHSLChannel(model: ColorPickerModel, channel: 'h' | 's' | 'l', value: number): Color {
|
||||
export function updateHSLChannel(
|
||||
model: ColorPickerModel,
|
||||
channel: 'h' | 's' | 'l',
|
||||
value: number
|
||||
): Color {
|
||||
const next = {
|
||||
...model.hsl,
|
||||
[channel]: channel === 'h' ? value : clampPercent(value)
|
||||
|
|
@ -106,7 +110,11 @@ export function updateHSLChannel(model: ColorPickerModel, channel: 'h' | 's' | '
|
|||
return rekaToAppColor(next)
|
||||
}
|
||||
|
||||
export function updateHSBChannel(model: ColorPickerModel, channel: 'h' | 's' | 'b', value: number): Color {
|
||||
export function updateHSBChannel(
|
||||
model: ColorPickerModel,
|
||||
channel: 'h' | 's' | 'b',
|
||||
value: number
|
||||
): Color {
|
||||
return rekaToAppColor({
|
||||
...model.hsb,
|
||||
[channel]: channel === 'h' ? value : clampPercent(value)
|
||||
|
|
@ -132,10 +140,7 @@ export function createOkHCLSliderPreviewModel(color: OkHCLColor): OkHCLSliderPre
|
|||
okhclHue: okhclToRGBA({
|
||||
...color,
|
||||
c: Math.max(color.c, OKHCL_HUE_PREVIEW_MIN_CHROMA),
|
||||
l:
|
||||
color.l <= 0 || color.l >= 1
|
||||
? OKHCL_HUE_PREVIEW_FALLBACK_LIGHTNESS
|
||||
: color.l
|
||||
l: color.l <= 0 || color.l >= 1 ? OKHCL_HUE_PREVIEW_FALLBACK_LIGHTNESS : color.l
|
||||
}),
|
||||
okhclChroma: okhclToRGBA(color),
|
||||
okhclLightness: okhclToRGBA(color)
|
||||
|
|
|
|||
|
|
@ -51,7 +51,9 @@ export function useExport() {
|
|||
hasSelection.value ? 'selection' : 'page'
|
||||
)
|
||||
const activeName = computed(() =>
|
||||
activeTarget.value === 'selection' ? (selectedNodeName.value ?? 'Export') : currentPageName.value
|
||||
activeTarget.value === 'selection'
|
||||
? (selectedNodeName.value ?? 'Export')
|
||||
: currentPageName.value
|
||||
)
|
||||
const activeSettings = computed(() =>
|
||||
activeTarget.value === 'selection' ? selectionSettings.value : pageSettings.value
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useEditor } from '@open-pencil/vue/context/editorContext'
|
||||
import { useSceneComputed } from '@open-pencil/vue/internal/useSceneComputed'
|
||||
import { useI18n } from '@open-pencil/vue/i18n'
|
||||
import { useSceneComputed } from '@open-pencil/vue/internal/useSceneComputed'
|
||||
|
||||
import type { SceneNode, LayoutSizing, LayoutAlign, LayoutCounterAlign, GridTrack } from '@open-pencil/core'
|
||||
import type {
|
||||
SceneNode,
|
||||
LayoutSizing,
|
||||
LayoutAlign,
|
||||
LayoutCounterAlign,
|
||||
GridTrack
|
||||
} from '@open-pencil/core'
|
||||
|
||||
type AlignCell = { primary: LayoutAlign; counter: LayoutCounterAlign }
|
||||
|
||||
|
|
@ -51,7 +57,9 @@ export function useLayout() {
|
|||
const { panels } = useI18n()
|
||||
|
||||
const node = useSceneComputed<SceneNode | null>(() => editor.getSelectedNode() ?? null)
|
||||
const layoutDirection = computed<SceneNode['layoutDirection']>(() => node.value?.layoutDirection ?? 'AUTO')
|
||||
const layoutDirection = computed<SceneNode['layoutDirection']>(
|
||||
() => node.value?.layoutDirection ?? 'AUTO'
|
||||
)
|
||||
|
||||
const isInAutoLayout = computed(() => {
|
||||
const n = node.value
|
||||
|
|
@ -87,7 +95,8 @@ export function useLayout() {
|
|||
{ value: 'FIXED', label: panels.value.sizingFixed }
|
||||
]
|
||||
if (isFlex.value) options.push({ value: 'HUG', label: panels.value.sizingHug })
|
||||
if (isInAutoLayout.value || isFlex.value) options.push({ value: 'FILL', label: panels.value.sizingFill })
|
||||
if (isInAutoLayout.value || isFlex.value)
|
||||
options.push({ value: 'FILL', label: panels.value.sizingFill })
|
||||
return options
|
||||
})
|
||||
|
||||
|
|
@ -96,7 +105,8 @@ export function useLayout() {
|
|||
{ value: 'FIXED', label: panels.value.sizingFixed }
|
||||
]
|
||||
if (isFlex.value) options.push({ value: 'HUG', label: panels.value.sizingHug })
|
||||
if (isInAutoLayout.value || isFlex.value) options.push({ value: 'FILL', label: panels.value.sizingFill })
|
||||
if (isInAutoLayout.value || isFlex.value)
|
||||
options.push({ value: 'FILL', label: panels.value.sizingFill })
|
||||
return options
|
||||
})
|
||||
|
||||
|
|
@ -184,7 +194,11 @@ export function useLayout() {
|
|||
|
||||
function setLayoutDirection(direction: SceneNode['layoutDirection']) {
|
||||
if (!node.value) return
|
||||
editor.updateNodeWithUndo(node.value.id, { layoutDirection: direction }, 'Change layout direction')
|
||||
editor.updateNodeWithUndo(
|
||||
node.value.id,
|
||||
{ layoutDirection: direction },
|
||||
'Change layout direction'
|
||||
)
|
||||
}
|
||||
|
||||
function updateGridTrack(
|
||||
|
|
|
|||
|
|
@ -22,18 +22,22 @@ export function useOkHCL() {
|
|||
}
|
||||
|
||||
function getFillOkHCLColor(node: SceneNode | null, index: number): OkHCLColor | null {
|
||||
return node ? getFillOkHCL(node, index)?.color ?? null : null
|
||||
return node ? (getFillOkHCL(node, index)?.color ?? null) : null
|
||||
}
|
||||
|
||||
function getStrokeOkHCLColor(node: SceneNode | null, index: number): OkHCLColor | null {
|
||||
return node ? getStrokeOkHCL(node, index)?.color ?? null : null
|
||||
return node ? (getStrokeOkHCL(node, index)?.color ?? null) : null
|
||||
}
|
||||
|
||||
function ensureFillOkHCL(node: SceneNode, index: number) {
|
||||
const color =
|
||||
getFillOkHCLColor(node, index) ??
|
||||
rgbaToOkHCL(node.fills[index]?.color ?? { r: 0, g: 0, b: 0, a: 1 })
|
||||
editor.updateNodeWithUndo(node.id, setNodeFillOkHCL(node, index, color), 'Update fill color model')
|
||||
editor.updateNodeWithUndo(
|
||||
node.id,
|
||||
setNodeFillOkHCL(node, index, color),
|
||||
'Update fill color model'
|
||||
)
|
||||
}
|
||||
|
||||
function ensureStrokeOkHCL(node: SceneNode, index: number) {
|
||||
|
|
|
|||
|
|
@ -199,4 +199,4 @@
|
|||
"noResults": "无结果",
|
||||
"share": "分享"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import ColorPicker from './ColorPicker.vue'
|
|||
import type { Color } from '@open-pencil/core'
|
||||
import type { OkHCLControls } from '@open-pencil/vue/ColorPicker/types'
|
||||
|
||||
const { editable = false, color, okhcl = null } = defineProps<{
|
||||
const {
|
||||
editable = false,
|
||||
color,
|
||||
okhcl = null
|
||||
} = defineProps<{
|
||||
color: Color
|
||||
editable?: boolean
|
||||
okhcl?: OkHCLControls | null
|
||||
|
|
@ -15,8 +19,15 @@ const emit = defineEmits<{ update: [color: Color] }>()
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<ColorInputRoot :color="color" :editable="editable" :okhcl="okhcl" @update="emit('update', $event)">
|
||||
<template #default="{ editable: isEditable, hex, updateFromHex, updateColor, okhcl: okhclControls }">
|
||||
<ColorInputRoot
|
||||
:color="color"
|
||||
:editable="editable"
|
||||
:okhcl="okhcl"
|
||||
@update="emit('update', $event)"
|
||||
>
|
||||
<template
|
||||
#default="{ editable: isEditable, hex, updateFromHex, updateColor, okhcl: okhclControls }"
|
||||
>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<ColorPicker :color="color" :okhcl="okhclControls" @update="updateColor($event)" />
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -48,12 +48,13 @@ const okhclSliderPreview = computed(() =>
|
|||
const okhclSliderGradient = computed(() =>
|
||||
okhcl?.okhcl ? createOkHCLSliderGradientModel(okhcl.okhcl) : null
|
||||
)
|
||||
const fieldOptions = computed(() =>
|
||||
okhcl?.fieldOptions ?? [
|
||||
{ value: 'rgb', label: panels.value.colorFormatRgb },
|
||||
{ value: 'hsl', label: panels.value.colorFormatHsl },
|
||||
{ value: 'hsb', label: panels.value.colorFormatHsb }
|
||||
]
|
||||
const fieldOptions = computed(
|
||||
() =>
|
||||
okhcl?.fieldOptions ?? [
|
||||
{ value: 'rgb', label: panels.value.colorFormatRgb },
|
||||
{ value: 'hsl', label: panels.value.colorFormatHsl },
|
||||
{ value: 'hsb', label: panels.value.colorFormatHsb }
|
||||
]
|
||||
)
|
||||
const fieldFormat = computed(() => okhcl?.fieldFormat ?? 'rgb')
|
||||
const isOkHCLFormat = computed(() => fieldFormat.value === 'okhcl' && okhcl)
|
||||
|
|
@ -150,17 +151,64 @@ function updateOkHCLChannel(channel: 'h' | 'c' | 'l' | 'a', value: number) {
|
|||
/>
|
||||
|
||||
<div class="min-w-0 flex flex-col gap-2">
|
||||
<div v-if="fieldFormat === 'rgb'" class="grid grid-cols-[repeat(3,minmax(0,1fr))] gap-px overflow-hidden rounded border border-border bg-border">
|
||||
<input type="number" class="bg-input px-2 py-1 text-xs text-surface outline-none" :value="Math.round(rgbColor.r)" min="0" max="255" @change="updateRGBChannelValue('r', +($event.target as HTMLInputElement).value)" />
|
||||
<input type="number" class="bg-input px-2 py-1 text-xs text-surface outline-none" :value="Math.round(rgbColor.g)" min="0" max="255" @change="updateRGBChannelValue('g', +($event.target as HTMLInputElement).value)" />
|
||||
<input type="number" class="bg-input px-2 py-1 text-xs text-surface outline-none" :value="Math.round(rgbColor.b)" min="0" max="255" @change="updateRGBChannelValue('b', +($event.target as HTMLInputElement).value)" />
|
||||
<div
|
||||
v-if="fieldFormat === 'rgb'"
|
||||
class="grid grid-cols-[repeat(3,minmax(0,1fr))] gap-px overflow-hidden rounded border border-border bg-border"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
class="bg-input px-2 py-1 text-xs text-surface outline-none"
|
||||
:value="Math.round(rgbColor.r)"
|
||||
min="0"
|
||||
max="255"
|
||||
@change="updateRGBChannelValue('r', +($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
class="bg-input px-2 py-1 text-xs text-surface outline-none"
|
||||
:value="Math.round(rgbColor.g)"
|
||||
min="0"
|
||||
max="255"
|
||||
@change="updateRGBChannelValue('g', +($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
class="bg-input px-2 py-1 text-xs text-surface outline-none"
|
||||
:value="Math.round(rgbColor.b)"
|
||||
min="0"
|
||||
max="255"
|
||||
@change="updateRGBChannelValue('b', +($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-else-if="fieldFormat === 'hsl'">
|
||||
<div class="grid grid-cols-[repeat(3,minmax(0,1fr))] gap-px overflow-hidden rounded border border-border bg-border">
|
||||
<input type="number" class="bg-input px-2 py-1 text-xs text-surface outline-none" :value="Math.round(hslColor.h ?? 0)" min="0" max="360" @change="updateHSLChannelValue('h', +($event.target as HTMLInputElement).value)" />
|
||||
<input type="number" class="bg-input px-2 py-1 text-xs text-surface outline-none" :value="Math.round(hslColor.s ?? 0)" min="0" max="100" @change="updateHSLChannelValue('s', +($event.target as HTMLInputElement).value)" />
|
||||
<input type="number" class="bg-input px-2 py-1 text-xs text-surface outline-none" :value="Math.round(hslColor.l ?? 0)" min="0" max="100" @change="updateHSLChannelValue('l', +($event.target as HTMLInputElement).value)" />
|
||||
<div
|
||||
class="grid grid-cols-[repeat(3,minmax(0,1fr))] gap-px overflow-hidden rounded border border-border bg-border"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
class="bg-input px-2 py-1 text-xs text-surface outline-none"
|
||||
:value="Math.round(hslColor.h ?? 0)"
|
||||
min="0"
|
||||
max="360"
|
||||
@change="updateHSLChannelValue('h', +($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
class="bg-input px-2 py-1 text-xs text-surface outline-none"
|
||||
:value="Math.round(hslColor.s ?? 0)"
|
||||
min="0"
|
||||
max="100"
|
||||
@change="updateHSLChannelValue('s', +($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
class="bg-input px-2 py-1 text-xs text-surface outline-none"
|
||||
:value="Math.round(hslColor.l ?? 0)"
|
||||
min="0"
|
||||
max="100"
|
||||
@change="updateHSLChannelValue('l', +($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PickerSlider
|
||||
|
|
@ -199,10 +247,33 @@ function updateOkHCLChannel(channel: 'h' | 'c' | 'l' | 'a', value: number) {
|
|||
</template>
|
||||
|
||||
<template v-else-if="fieldFormat === 'hsb'">
|
||||
<div class="grid grid-cols-[repeat(3,minmax(0,1fr))] gap-px overflow-hidden rounded border border-border bg-border">
|
||||
<input type="number" class="bg-input px-2 py-1 text-xs text-surface outline-none" :value="Math.round(hsbColor.h)" min="0" max="360" @change="updateHSBChannelValue('h', +($event.target as HTMLInputElement).value)" />
|
||||
<input type="number" class="bg-input px-2 py-1 text-xs text-surface outline-none" :value="Math.round(hsbColor.s)" min="0" max="100" @change="updateHSBChannelValue('s', +($event.target as HTMLInputElement).value)" />
|
||||
<input type="number" class="bg-input px-2 py-1 text-xs text-surface outline-none" :value="Math.round(hsbColor.b)" min="0" max="100" @change="updateHSBChannelValue('b', +($event.target as HTMLInputElement).value)" />
|
||||
<div
|
||||
class="grid grid-cols-[repeat(3,minmax(0,1fr))] gap-px overflow-hidden rounded border border-border bg-border"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
class="bg-input px-2 py-1 text-xs text-surface outline-none"
|
||||
:value="Math.round(hsbColor.h)"
|
||||
min="0"
|
||||
max="360"
|
||||
@change="updateHSBChannelValue('h', +($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
class="bg-input px-2 py-1 text-xs text-surface outline-none"
|
||||
:value="Math.round(hsbColor.s)"
|
||||
min="0"
|
||||
max="100"
|
||||
@change="updateHSBChannelValue('s', +($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
class="bg-input px-2 py-1 text-xs text-surface outline-none"
|
||||
:value="Math.round(hsbColor.b)"
|
||||
min="0"
|
||||
max="100"
|
||||
@change="updateHSBChannelValue('b', +($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PickerSlider
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ const {
|
|||
checkerboard?: boolean
|
||||
thumbFill?: string
|
||||
testId?: string
|
||||
ui?: Partial<Record<'root' | 'label' | 'track' | 'gradient' | 'range' | 'thumb' | 'input', string>>
|
||||
ui?: Partial<
|
||||
Record<'root' | 'label' | 'track' | 'gradient' | 'range' | 'thumb' | 'input', string>
|
||||
>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -74,10 +76,7 @@ function thumbLeft(): string {
|
|||
:value="modelValue"
|
||||
@input="emit('update:modelValue', +($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<div
|
||||
:class="cls.thumb"
|
||||
:style="{ left: thumbLeft(), background: thumbFill }"
|
||||
/>
|
||||
<div :class="cls.thumb" :style="{ left: thumbLeft(), background: thumbFill }" />
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
|
|
|
|||
|
|
@ -142,10 +142,7 @@ watch(open, (v) => {
|
|||
:class="itemCls"
|
||||
@select="store.zoomToLevel(preset.level)"
|
||||
>
|
||||
<icon-lucide-check
|
||||
v-if="isActivePreset(preset.level)"
|
||||
class="absolute left-2 size-3.5"
|
||||
/>
|
||||
<icon-lucide-check v-if="isActivePreset(preset.level)" class="absolute left-2 size-3.5" />
|
||||
<span class="flex-1">{{ preset.label }}</span>
|
||||
<span v-if="preset.shortcut" class="text-[11px] text-muted">{{ preset.shortcut }}</span>
|
||||
</DropdownMenuItem>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,12 @@ interface SelectUi {
|
|||
indicator?: string
|
||||
}
|
||||
|
||||
const { options, placeholder, ui, testId = 'app-select-trigger' } = defineProps<{
|
||||
const {
|
||||
options,
|
||||
placeholder,
|
||||
ui,
|
||||
testId = 'app-select-trigger'
|
||||
} = defineProps<{
|
||||
options: { value: T; label: string }[]
|
||||
placeholder?: string
|
||||
ui?: SelectUi
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ const pickerSlider = tv({
|
|||
track: 'relative flex h-3 flex-1 items-center rounded-md',
|
||||
gradient: 'absolute inset-0 rounded-md',
|
||||
range: 'absolute inset-0 z-10 h-full w-full cursor-pointer appearance-none opacity-0',
|
||||
thumb: 'pointer-events-none absolute top-1/2 size-3.5 -translate-y-1/2 rounded-full border-2 border-white shadow-sm',
|
||||
thumb:
|
||||
'pointer-events-none absolute top-1/2 size-3.5 -translate-y-1/2 rounded-full border-2 border-white shadow-sm',
|
||||
input: 'w-14 rounded border border-border bg-input px-1 py-0.5 text-right text-xs text-surface'
|
||||
},
|
||||
variants: {
|
||||
|
|
@ -25,12 +26,11 @@ const pickerSlider = tv({
|
|||
}
|
||||
})
|
||||
|
||||
export type PickerSliderUi = Partial<Record<'root' | 'label' | 'track' | 'gradient' | 'range' | 'thumb' | 'input', string>>
|
||||
export type PickerSliderUi = Partial<
|
||||
Record<'root' | 'label' | 'track' | 'gradient' | 'range' | 'thumb' | 'input', string>
|
||||
>
|
||||
|
||||
export function usePickerSliderUI(options?: {
|
||||
checkerboard?: boolean
|
||||
ui?: PickerSliderUi
|
||||
}) {
|
||||
export function usePickerSliderUI(options?: { checkerboard?: boolean; ui?: PickerSliderUi }) {
|
||||
const slots = pickerSlider({ checkerboard: options?.checkerboard })
|
||||
return {
|
||||
root: twMerge(slots.root(), options?.ui?.root),
|
||||
|
|
|
|||
|
|
@ -44,9 +44,7 @@ export const DEFAULT_COLLAB_STATE: CollabState = {
|
|||
|
||||
export function useCollab(storeOrGetter: EditorStore | (() => EditorStore)) {
|
||||
const getStore = () =>
|
||||
typeof storeOrGetter === 'function'
|
||||
? (storeOrGetter as () => EditorStore)()
|
||||
: storeOrGetter
|
||||
typeof storeOrGetter === 'function' ? (storeOrGetter as () => EditorStore)() : storeOrGetter
|
||||
const storedName = useLocalStorage('op-collab-name', '')
|
||||
const state = ref<CollabState>({
|
||||
connected: false,
|
||||
|
|
|
|||
|
|
@ -44,4 +44,12 @@ function setupGlobalErrorHandler() {
|
|||
})
|
||||
}
|
||||
|
||||
export const toast = { info, warning, error, remove, toasts, setupGlobalErrorHandler, TOAST_DURATION }
|
||||
export const toast = {
|
||||
info,
|
||||
warning,
|
||||
error,
|
||||
remove,
|
||||
toasts,
|
||||
setupGlobalErrorHandler,
|
||||
TOAST_DURATION
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue