refactor(core): use es-toolkit utilities
This commit is contained in:
parent
f3649550f2
commit
955aa4d1e9
|
|
@ -292,6 +292,7 @@ Self-review checklist:
|
|||
- Files should stay under ~600 lines — split by domain when they grow (see `packages/core/src/tools/` for the pattern)
|
||||
- `structuredClone` for deep copies, never shallow spread when mutating nested objects
|
||||
- Don't hand-roll what a dependency already does. Check existing deps first (`package.json`, `packages/*/package.json`). If none covers it, find a quality library instead of inlining an implementation — e.g. use `diff` for unified diffs, not a custom line-by-line loop; use `culori` for color math, not manual RGB parsing
|
||||
- `es-toolkit` is available in core for small, focused utility helpers when it clearly improves readability. Prefer subpath imports such as `es-toolkit/object`, `es-toolkit/array`, and `es-toolkit/predicate`; good fits include `omit` / `pick` for object key selection, `uniq` for dedupe, and `isNotNil` for typed nullish filtering. Do not replace clear native JavaScript just for consistency, and avoid `es-toolkit/compat` unless deliberately migrating lodash-compatible behavior.
|
||||
- Check Reka UI for existing components (Dialog, Popover, DropdownMenu, Select, Tooltip, Toast, etc.) before building custom ones — especially dropdowns, popovers, and modals
|
||||
|
||||
|
||||
|
|
|
|||
3
bun.lock
3
bun.lock
|
|
@ -121,6 +121,7 @@
|
|||
"canvaskit-wasm": "^0.40.0",
|
||||
"culori": "^4.0.2",
|
||||
"diff": "^8.0.3",
|
||||
"es-toolkit": "^1.46.1",
|
||||
"expr-eval": "^2.0.2",
|
||||
"fflate": "^0.8.2",
|
||||
"fontoxpath": "^3.34.0",
|
||||
|
|
@ -1525,6 +1526,8 @@
|
|||
|
||||
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
||||
|
||||
"es-toolkit": ["es-toolkit@1.46.1", "", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="],
|
||||
|
||||
"esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
|
|
|||
|
|
@ -319,6 +319,7 @@
|
|||
"canvaskit-wasm": "^0.40.0",
|
||||
"culori": "^4.0.2",
|
||||
"diff": "^8.0.3",
|
||||
"es-toolkit": "^1.46.1",
|
||||
"expr-eval": "^2.0.2",
|
||||
"fflate": "^0.8.2",
|
||||
"fontoxpath": "^3.34.0",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { CanvasKit, FontWeight, Paragraph, TypefaceFontProvider } from 'canvaskit-wasm'
|
||||
import { uniq } from 'es-toolkit/array'
|
||||
|
||||
import { getCanvasKit } from '#core/canvaskit'
|
||||
import { resolveRGBAForPreview } from '#core/color/management'
|
||||
|
|
@ -133,7 +134,7 @@ function resolveParagraphFontFamilies(
|
|||
if (primary !== DEFAULT_FONT_FAMILY) families.push(DEFAULT_FONT_FAMILY)
|
||||
families.push(...arabicFallbacks, ...cjkFallbacks)
|
||||
|
||||
const resolved = [...new Set(families)]
|
||||
const resolved = uniq(families)
|
||||
fontFamilyCache.set(key, resolved)
|
||||
if (fontFamilyCache.size > FONT_FAMILY_CACHE_LIMIT) {
|
||||
const oldestKey = fontFamilyCache.keys().next().value
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { isNotNil } from 'es-toolkit/predicate'
|
||||
|
||||
import type { EditorContext } from '#core/editor/types'
|
||||
import { computeBounds } from '#core/geometry'
|
||||
|
||||
export function createClipboardPlacementActions(ctx: EditorContext) {
|
||||
function centerNodesAt(nodeIds: string[], cx: number, cy: number) {
|
||||
const items = nodeIds.map((id) => ctx.graph.getNode(id)).filter((node) => node != null)
|
||||
const items = nodeIds.map((id) => ctx.graph.getNode(id)).filter(isNotNil)
|
||||
const bounds = computeBounds(items)
|
||||
if (bounds.width === 0 && bounds.height === 0 && items.length === 0) return
|
||||
const dx = cx - (bounds.x + bounds.width / 2)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { omit } from 'es-toolkit/object'
|
||||
|
||||
import type { EditorContext } from '#core/editor/types'
|
||||
import { randomHex } from '#core/random'
|
||||
import { buildVariantName, parseVariantName } from '#core/scene-graph/variant-name'
|
||||
|
|
@ -77,9 +79,7 @@ export function createVariantActions(ctx: EditorContext) {
|
|||
for (const childId of node.childIds) {
|
||||
const child = ctx.graph.getNode(childId)
|
||||
if (!child) continue
|
||||
const values = Object.fromEntries(
|
||||
Object.entries(child.componentPropertyValues).filter(([key]) => key !== def.name)
|
||||
)
|
||||
const values = omit(child.componentPropertyValues, [def.name])
|
||||
ctx.graph.updateNode(childId, { componentPropertyValues: values })
|
||||
}
|
||||
ctx.undo.push({
|
||||
|
|
@ -95,9 +95,7 @@ export function createVariantActions(ctx: EditorContext) {
|
|||
for (const cid of n.childIds) {
|
||||
const c = ctx.graph.getNode(cid)
|
||||
if (!c) continue
|
||||
const v = Object.fromEntries(
|
||||
Object.entries(c.componentPropertyValues).filter(([key]) => key !== def.name)
|
||||
)
|
||||
const v = omit(c.componentPropertyValues, [def.name])
|
||||
ctx.graph.updateNode(cid, { componentPropertyValues: v })
|
||||
}
|
||||
}
|
||||
|
|
@ -128,9 +126,7 @@ export function createVariantActions(ctx: EditorContext) {
|
|||
if (!child) continue
|
||||
const values = { ...child.componentPropertyValues }
|
||||
if (prevName in values) {
|
||||
const nextValues = Object.fromEntries(
|
||||
Object.entries(values).filter(([key]) => key !== prevName)
|
||||
)
|
||||
const nextValues: Record<string, string> = omit(values, [prevName])
|
||||
nextValues[newName] = values[prevName]
|
||||
ctx.graph.updateNode(childId, { componentPropertyValues: nextValues })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { pick } from 'es-toolkit/object'
|
||||
|
||||
import { computeLayout } from '#core/layout'
|
||||
import type { LayoutMode, SceneNode } from '#core/scene-graph'
|
||||
|
||||
|
|
@ -58,7 +60,7 @@ function captureLayoutState(node: SceneNode): Partial<SceneNode> {
|
|||
}
|
||||
|
||||
function pickState(node: SceneNode, keys: (keyof SceneNode)[]): Partial<SceneNode> {
|
||||
return Object.fromEntries(keys.map((key) => [key, node[key]])) as Partial<SceneNode>
|
||||
return pick(node, keys) as Partial<SceneNode>
|
||||
}
|
||||
|
||||
function layoutModeUpdates(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { pick } from 'es-toolkit/object'
|
||||
|
||||
import type { SceneNode } from '#core/scene-graph'
|
||||
|
||||
import { createLayoutModeActions } from './layout-mode'
|
||||
|
|
@ -18,9 +20,7 @@ export function createNodeActions(ctx: EditorContext) {
|
|||
function updateNodeWithUndo(id: string, changes: Partial<SceneNode>, label = 'Update') {
|
||||
const node = ctx.graph.getNode(id)
|
||||
if (!node) return
|
||||
const previous = Object.fromEntries(
|
||||
(Object.keys(changes) as (keyof SceneNode)[]).map((key) => [key, node[key]])
|
||||
) as Partial<SceneNode>
|
||||
const previous = pick(node, Object.keys(changes) as (keyof SceneNode)[]) as Partial<SceneNode>
|
||||
ctx.graph.updateNode(id, changes)
|
||||
ctx.runLayoutForNode(id)
|
||||
ctx.undo.push({
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { pick } from 'es-toolkit/object'
|
||||
|
||||
import type { SceneNode } from '#core/scene-graph'
|
||||
import type { UndoEntry } from '#core/scene-graph/undo'
|
||||
import type { Rect, Vector } from '#core/types'
|
||||
|
|
@ -9,6 +11,7 @@ import {
|
|||
snapshotPage as createPageSnapshot,
|
||||
type PageSnapshot
|
||||
} from './history/snapshot'
|
||||
|
||||
import type { EditorContext } from './types'
|
||||
|
||||
export function createUndoActions(ctx: EditorContext) {
|
||||
|
|
@ -110,9 +113,7 @@ export function createUndoActions(ctx: EditorContext) {
|
|||
function commitNodeUpdate(nodeId: string, previous: Partial<SceneNode>, label = 'Update') {
|
||||
const node = ctx.graph.getNode(nodeId)
|
||||
if (!node) return
|
||||
const current = Object.fromEntries(
|
||||
(Object.keys(previous) as (keyof SceneNode)[]).map((key) => [key, node[key]])
|
||||
) as Partial<SceneNode>
|
||||
const current = pick(node, Object.keys(previous) as (keyof SceneNode)[]) as Partial<SceneNode>
|
||||
ctx.undo.push({
|
||||
label,
|
||||
forward: () => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { isNotNil } from 'es-toolkit/predicate'
|
||||
|
||||
import { BLACK } from '#core/constants'
|
||||
import type { NodeChange, VariableDataValuesEntry, Color, GUID } from '#core/kiwi/binary/codec'
|
||||
import { populateAndApplyOverrides } from '#core/kiwi/instance-overrides'
|
||||
|
|
@ -424,7 +426,7 @@ export function importNodeChanges(
|
|||
}
|
||||
const activeRootIds =
|
||||
options.populate === 'first-page'
|
||||
? [firstPageId, ...componentPageIds].filter(Boolean)
|
||||
? [firstPageId, ...componentPageIds].filter(isNotNil)
|
||||
: undefined
|
||||
|
||||
populateAndApplyOverrides(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { orderBy, sortBy } from 'es-toolkit/array'
|
||||
|
||||
import { colorToHex, colorDistance as colorDist } from '#core/color'
|
||||
import type { SceneGraph, SceneNode } from '#core/scene-graph'
|
||||
import type { Color } from '#core/types'
|
||||
|
|
@ -27,7 +29,7 @@ interface ColorCluster {
|
|||
function clusterColors(colors: ColorInfo[], threshold: number): ColorCluster[] {
|
||||
const clusters: ColorCluster[] = []
|
||||
const used = new Set<string>()
|
||||
const sorted = [...colors].sort((a, b) => b.count - a.count)
|
||||
const sorted = orderBy(colors, ['count'], ['desc'])
|
||||
|
||||
for (const color of sorted) {
|
||||
if (used.has(color.hex)) continue
|
||||
|
|
@ -50,7 +52,7 @@ function clusterColors(colors: ColorInfo[], threshold: number): ColorCluster[] {
|
|||
if (cluster.colors.length > 1) clusters.push(cluster)
|
||||
}
|
||||
|
||||
return clusters.sort((a, b) => b.colors.length - a.colors.length)
|
||||
return orderBy(clusters, [(cluster) => cluster.colors.length], ['desc'])
|
||||
}
|
||||
|
||||
function collectColors(graph: SceneGraph): { colors: ColorInfo[]; totalNodes: number } {
|
||||
|
|
@ -118,7 +120,7 @@ export const analyzeColorsCommand: RpcCommand<AnalyzeColorsArgs, AnalyzeColorsRe
|
|||
args.threshold ?? 15
|
||||
)
|
||||
: []
|
||||
return { colors: colors.sort((a, b) => b.count - a.count), totalNodes, clusters }
|
||||
return { colors: orderBy(colors, ['count'], ['desc']), totalNodes, clusters }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,7 +168,7 @@ export const analyzeTypographyCommand: RpcCommand<AnalyzeTypographyArgs, Analyze
|
|||
}
|
||||
}
|
||||
|
||||
return { styles: [...styleMap.values()].sort((a, b) => b.count - a.count), totalTextNodes }
|
||||
return { styles: orderBy([...styleMap.values()], ['count'], ['desc']), totalTextNodes }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -210,9 +212,11 @@ export const analyzeSpacingCommand: RpcCommand<void, AnalyzeSpacingResult> = {
|
|||
}
|
||||
|
||||
const toValues = (map: Map<number, number>) =>
|
||||
[...map.entries()]
|
||||
.map(([value, count]) => ({ value, count }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
orderBy(
|
||||
[...map.entries()].map(([value, count]) => ({ value, count })),
|
||||
['count'],
|
||||
['desc']
|
||||
)
|
||||
|
||||
return { gaps: toValues(gapMap), paddings: toValues(paddingMap), totalNodes }
|
||||
}
|
||||
|
|
@ -250,9 +254,8 @@ function buildSignature(graph: SceneGraph, node: SceneNode): string {
|
|||
if (!child) continue
|
||||
childTypes.set(child.type, (childTypes.get(child.type) ?? 0) + 1)
|
||||
}
|
||||
const childPart = [...childTypes.entries()]
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([t, c]) => `${t}:${c}`)
|
||||
const childPart = sortBy([...childTypes.entries()], [([type]) => type])
|
||||
.map(([type, count]) => `${type}:${count}`)
|
||||
.join(',')
|
||||
const w = Math.round(node.width / 10) * 10
|
||||
const h = Math.round(node.height / 10) * 10
|
||||
|
|
@ -287,11 +290,13 @@ export const analyzeClustersCommand: RpcCommand<AnalyzeClustersArgs, AnalyzeClus
|
|||
sigMap.set(sig, arr)
|
||||
}
|
||||
|
||||
const clusters = [...sigMap.entries()]
|
||||
.filter(([, nodes]) => nodes.length >= minCount)
|
||||
.map(([signature, nodes]) => ({ signature, nodes }))
|
||||
.sort((a, b) => b.nodes.length - a.nodes.length)
|
||||
.slice(0, limit)
|
||||
const clusters = orderBy(
|
||||
[...sigMap.entries()]
|
||||
.filter(([, nodes]) => nodes.length >= minCount)
|
||||
.map(([signature, nodes]) => ({ signature, nodes })),
|
||||
[(cluster) => cluster.nodes.length],
|
||||
['desc']
|
||||
).slice(0, limit)
|
||||
|
||||
return { clusters, totalNodes }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { omit, omitBy } from 'es-toolkit/object'
|
||||
|
||||
import { BLACK } from '#core/constants'
|
||||
import type { Color } from '#core/types'
|
||||
|
||||
|
|
@ -21,9 +23,10 @@ export function removeVariable(graph: SceneGraph, id: string): void {
|
|||
collection.variableIds = collection.variableIds.filter((vid) => vid !== id)
|
||||
}
|
||||
for (const node of graph.nodes.values()) {
|
||||
node.boundVariables = Object.fromEntries(
|
||||
Object.entries(node.boundVariables).filter(([, varId]) => varId !== id)
|
||||
)
|
||||
node.boundVariables = omitBy(node.boundVariables, (varId) => varId === id) as Record<
|
||||
string,
|
||||
string
|
||||
>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -140,11 +143,7 @@ export function removeMode(graph: SceneGraph, collectionId: string, modeId: stri
|
|||
}
|
||||
for (const varId of collection.variableIds) {
|
||||
const variable = graph.variables.get(varId)
|
||||
if (variable) {
|
||||
variable.valuesByMode = Object.fromEntries(
|
||||
Object.entries(variable.valuesByMode).filter(([id]) => id !== modeId)
|
||||
)
|
||||
}
|
||||
if (variable) variable.valuesByMode = omit(variable.valuesByMode, [modeId])
|
||||
}
|
||||
if (graph.activeMode.get(collectionId) === modeId) {
|
||||
graph.activeMode.set(collectionId, collection.defaultModeId)
|
||||
|
|
@ -236,9 +235,5 @@ export function bindVariable(
|
|||
|
||||
export function unbindVariable(graph: SceneGraph, nodeId: string, field: string): void {
|
||||
const node = graph.nodes.get(nodeId)
|
||||
if (node) {
|
||||
node.boundVariables = Object.fromEntries(
|
||||
Object.entries(node.boundVariables).filter(([key]) => key !== field)
|
||||
)
|
||||
}
|
||||
if (node) node.boundVariables = omit(node.boundVariables, [field])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { CanvasKit, TypefaceFontProvider } from 'canvaskit-wasm'
|
||||
import { uniq } from 'es-toolkit/array'
|
||||
|
||||
import { DEFAULT_FONT_FAMILY, IS_BROWSER, GOOGLE_FONTS_API_KEY } from '#core/constants'
|
||||
import type { SceneGraph } from '#core/scene-graph'
|
||||
|
|
@ -204,7 +205,7 @@ export class FontManager {
|
|||
|
||||
async listFamilies(): Promise<string[]> {
|
||||
const fonts = this.localFonts ?? (await this.requestLocalFontAccess())
|
||||
return [...new Set(fonts.map((f) => f.family))].sort()
|
||||
return uniq(fonts.map((f) => f.family)).sort()
|
||||
}
|
||||
|
||||
async fetchBundledFont(url: string): Promise<ArrayBuffer | null> {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { omit } from 'es-toolkit/object'
|
||||
|
||||
import type { CharacterStyleOverride, StyleRun, TextDecoration } from '#core/scene-graph'
|
||||
|
||||
export function getStyleAt(runs: StyleRun[], index: number): CharacterStyleOverride {
|
||||
|
|
@ -51,11 +53,7 @@ export function removeStyleFromRange(
|
|||
if (chars[i]) {
|
||||
const current = chars[i]
|
||||
if (!current) continue
|
||||
const copy = Object.fromEntries(
|
||||
Object.entries(current).filter(
|
||||
([key]) => !keys.includes(key as keyof CharacterStyleOverride)
|
||||
)
|
||||
) as CharacterStyleOverride
|
||||
const copy = omit(current, keys) as CharacterStyleOverride
|
||||
chars[i] = Object.keys(copy).length > 0 ? copy : null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { orderBy, sortBy } from 'es-toolkit/array'
|
||||
|
||||
import { defineTool } from '#core/tools/schema'
|
||||
|
||||
interface SizedItem {
|
||||
|
|
@ -64,8 +66,7 @@ export const analyzeClusters = defineTool({
|
|||
|
||||
const width = Math.round(raw.width / 10) * 10
|
||||
const height = Math.round(raw.height / 10) * 10
|
||||
const childSignature = [...childTypes.entries()]
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
const childSignature = sortBy([...childTypes.entries()], [([type]) => type])
|
||||
.map(([type, count]) => `${type}:${count}`)
|
||||
.join(',')
|
||||
const signature = `${raw.type}:${width}x${height}|${childSignature}`
|
||||
|
|
@ -83,9 +84,10 @@ export const analyzeClusters = defineTool({
|
|||
return false
|
||||
})
|
||||
|
||||
const clusters = [...signatureMap.entries()]
|
||||
.filter(([, nodes]) => nodes.length >= minCount)
|
||||
.map(([signature, nodes]) => {
|
||||
const clusters = orderBy(
|
||||
[...signatureMap.entries()]
|
||||
.filter(([, nodes]) => nodes.length >= minCount)
|
||||
.map(([signature, nodes]) => {
|
||||
const avgWidth = nodes.reduce((sum, node) => sum + node.width, 0) / nodes.length
|
||||
const avgHeight = nodes.reduce((sum, node) => sum + node.height, 0) / nodes.length
|
||||
const widths = nodes.map((node) => node.width)
|
||||
|
|
@ -94,19 +96,20 @@ export const analyzeClusters = defineTool({
|
|||
const heightRange = Math.max(...heights) - Math.min(...heights)
|
||||
const confidence = calcClusterConfidence(nodes)
|
||||
|
||||
return {
|
||||
signature,
|
||||
count: nodes.length,
|
||||
avgWidth: Math.round(avgWidth),
|
||||
avgHeight: Math.round(avgHeight),
|
||||
widthRange: Math.round(widthRange),
|
||||
heightRange: Math.round(heightRange),
|
||||
confidence,
|
||||
examples: nodes.slice(0, 3).map((node) => ({ id: node.id, name: node.name }))
|
||||
}
|
||||
})
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, limit)
|
||||
return {
|
||||
signature,
|
||||
count: nodes.length,
|
||||
avgWidth: Math.round(avgWidth),
|
||||
avgHeight: Math.round(avgHeight),
|
||||
widthRange: Math.round(widthRange),
|
||||
heightRange: Math.round(heightRange),
|
||||
confidence,
|
||||
examples: nodes.slice(0, 3).map((node) => ({ id: node.id, name: node.name }))
|
||||
}
|
||||
}),
|
||||
['count'],
|
||||
['desc']
|
||||
).slice(0, limit)
|
||||
|
||||
return { totalNodes, clusters }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { orderBy } from 'es-toolkit/array'
|
||||
|
||||
import { colorDistance, colorToHex } from '#core/color'
|
||||
import { defineTool } from '#core/tools/schema'
|
||||
import type { Color } from '#core/types'
|
||||
|
|
@ -67,7 +69,7 @@ export const analyzeColors = defineTool({
|
|||
return false
|
||||
})
|
||||
|
||||
const colors = [...colorMap.values()].sort((a, b) => b.count - a.count).slice(0, limit)
|
||||
const colors = orderBy([...colorMap.values()], ['count'], ['desc']).slice(0, limit)
|
||||
|
||||
const result: Record<string, unknown> = {
|
||||
totalNodes,
|
||||
|
|
@ -76,9 +78,11 @@ export const analyzeColors = defineTool({
|
|||
}
|
||||
|
||||
if (args.show_similar) {
|
||||
const hardcoded = [...colorMap.values()]
|
||||
.filter((c) => !c.variableName)
|
||||
.sort((a, b) => b.count - a.count)
|
||||
const hardcoded = orderBy(
|
||||
[...colorMap.values()].filter((c) => !c.variableName),
|
||||
['count'],
|
||||
['desc']
|
||||
)
|
||||
const used = new Set<string>()
|
||||
const clusters: { colors: string[]; totalCount: number; suggestedHex: string }[] = []
|
||||
|
||||
|
|
@ -102,7 +106,7 @@ export const analyzeColors = defineTool({
|
|||
}
|
||||
}
|
||||
|
||||
result.similarClusters = clusters.sort((a, b) => b.colors.length - a.colors.length)
|
||||
result.similarClusters = orderBy(clusters, [(cluster) => cluster.colors.length], ['desc'])
|
||||
}
|
||||
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { orderBy } from 'es-toolkit/array'
|
||||
|
||||
import { defineTool } from '#core/tools/schema'
|
||||
|
||||
export const analyzeSpacing = defineTool({
|
||||
|
|
@ -36,13 +38,13 @@ export const analyzeSpacing = defineTool({
|
|||
return false
|
||||
})
|
||||
|
||||
const gaps = [...gapMap.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([value, count]) => ({ value, count, onGrid: value % gridSize === 0 }))
|
||||
const gaps = orderBy([...gapMap.entries()], [(entry) => entry[1]], ['desc']).map(
|
||||
([value, count]) => ({ value, count, onGrid: value % gridSize === 0 })
|
||||
)
|
||||
|
||||
const paddings = [...paddingMap.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([value, count]) => ({ value, count, onGrid: value % gridSize === 0 }))
|
||||
const paddings = orderBy([...paddingMap.entries()], [(entry) => entry[1]], ['desc']).map(
|
||||
([value, count]) => ({ value, count, onGrid: value % gridSize === 0 })
|
||||
)
|
||||
|
||||
const offGridGaps = gaps.filter((gap) => !gap.onGrid)
|
||||
const offGridPaddings = paddings.filter((padding) => !padding.onGrid)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { orderBy, sortBy } from 'es-toolkit/array'
|
||||
|
||||
import { defineTool } from '#core/tools/schema'
|
||||
|
||||
export const analyzeTypography = defineTool({
|
||||
|
|
@ -43,16 +45,16 @@ export const analyzeTypography = defineTool({
|
|||
return false
|
||||
})
|
||||
|
||||
const styles = [...styleMap.values()].sort((a, b) => b.count - a.count)
|
||||
const styles = orderBy([...styleMap.values()], ['count'], ['desc'])
|
||||
|
||||
if (args.group_by === 'family') {
|
||||
const byFamily = new Map<string, number>()
|
||||
for (const s of styles) byFamily.set(s.family, (byFamily.get(s.family) ?? 0) + s.count)
|
||||
return {
|
||||
totalTextNodes,
|
||||
groups: [...byFamily.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([family, count]) => ({ family, count }))
|
||||
groups: orderBy([...byFamily.entries()], [(entry) => entry[1]], ['desc']).map(
|
||||
([family, count]) => ({ family, count })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,9 +63,10 @@ export const analyzeTypography = defineTool({
|
|||
for (const s of styles) bySize.set(s.size, (bySize.get(s.size) ?? 0) + s.count)
|
||||
return {
|
||||
totalTextNodes,
|
||||
groups: [...bySize.entries()]
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.map(([size, count]) => ({ size, count }))
|
||||
groups: sortBy([...bySize.entries()], [(entry) => entry[0]]).map(([size, count]) => ({
|
||||
size,
|
||||
count
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,9 +75,9 @@ export const analyzeTypography = defineTool({
|
|||
for (const s of styles) byWeight.set(s.weight, (byWeight.get(s.weight) ?? 0) + s.count)
|
||||
return {
|
||||
totalTextNodes,
|
||||
groups: [...byWeight.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([weight, count]) => ({ weight, count }))
|
||||
groups: orderBy([...byWeight.entries()], [(entry) => entry[1]], ['desc']).map(
|
||||
([weight, count]) => ({ weight, count })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { uniq } from 'es-toolkit/array'
|
||||
|
||||
import { defineTool } from '#core/tools/schema'
|
||||
|
||||
export const listFonts = defineTool({
|
||||
|
|
@ -43,7 +45,7 @@ export const listAvailableFonts = defineTool({
|
|||
},
|
||||
execute: async (figma, args) => {
|
||||
const fonts = await figma.listAvailableFontsAsync()
|
||||
let families = Array.from(new Set(fonts.map((font) => font.fontName.family)))
|
||||
let families = uniq(fonts.map((font) => font.fontName.family))
|
||||
if (args.family) {
|
||||
const q = args.family.toLowerCase()
|
||||
families = families.filter((family) => family.toLowerCase().includes(q))
|
||||
|
|
|
|||
Loading…
Reference in a new issue