From 955aa4d1e9a3a017bc67573fb23aa4bf465af66f Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 18 May 2026 19:10:46 +0300 Subject: [PATCH] refactor(core): use es-toolkit utilities --- AGENTS.md | 1 + bun.lock | 3 ++ packages/core/package.json | 1 + packages/core/src/canvas/text.ts | 3 +- .../core/src/editor/clipboard/placement.ts | 4 +- .../core/src/editor/components/variants.ts | 14 +++---- packages/core/src/editor/layout-mode.ts | 4 +- packages/core/src/editor/nodes.ts | 6 +-- packages/core/src/editor/undo.ts | 7 ++-- packages/core/src/kiwi/fig/import.ts | 4 +- packages/core/src/rpc/analyze-commands.ts | 35 ++++++++++------- packages/core/src/scene-graph/variables.ts | 21 ++++------ packages/core/src/text/fonts.ts | 3 +- packages/core/src/text/style-runs.ts | 8 ++-- packages/core/src/tools/analyze/clusters.ts | 39 ++++++++++--------- packages/core/src/tools/analyze/colors.ts | 14 ++++--- packages/core/src/tools/analyze/spacing.ts | 14 ++++--- packages/core/src/tools/analyze/typography.ts | 23 ++++++----- packages/core/src/tools/read/fonts.ts | 4 +- 19 files changed, 115 insertions(+), 93 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 189e185d0..94314f781 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/bun.lock b/bun.lock index 7f5f1413e..e45e12911 100644 --- a/bun.lock +++ b/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=="], diff --git a/packages/core/package.json b/packages/core/package.json index de4dfaebb..8102ff8d6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/core/src/canvas/text.ts b/packages/core/src/canvas/text.ts index c138cdc0c..19d8a708d 100644 --- a/packages/core/src/canvas/text.ts +++ b/packages/core/src/canvas/text.ts @@ -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 diff --git a/packages/core/src/editor/clipboard/placement.ts b/packages/core/src/editor/clipboard/placement.ts index cd5d2c72a..c10b97ba9 100644 --- a/packages/core/src/editor/clipboard/placement.ts +++ b/packages/core/src/editor/clipboard/placement.ts @@ -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) diff --git a/packages/core/src/editor/components/variants.ts b/packages/core/src/editor/components/variants.ts index 0ff79de42..dfae8f747 100644 --- a/packages/core/src/editor/components/variants.ts +++ b/packages/core/src/editor/components/variants.ts @@ -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 = omit(values, [prevName]) nextValues[newName] = values[prevName] ctx.graph.updateNode(childId, { componentPropertyValues: nextValues }) } diff --git a/packages/core/src/editor/layout-mode.ts b/packages/core/src/editor/layout-mode.ts index 13e2fadf7..8a15028ce 100644 --- a/packages/core/src/editor/layout-mode.ts +++ b/packages/core/src/editor/layout-mode.ts @@ -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 { } function pickState(node: SceneNode, keys: (keyof SceneNode)[]): Partial { - return Object.fromEntries(keys.map((key) => [key, node[key]])) as Partial + return pick(node, keys) as Partial } function layoutModeUpdates( diff --git a/packages/core/src/editor/nodes.ts b/packages/core/src/editor/nodes.ts index 9d899fcc1..b5b769473 100644 --- a/packages/core/src/editor/nodes.ts +++ b/packages/core/src/editor/nodes.ts @@ -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, 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 + const previous = pick(node, Object.keys(changes) as (keyof SceneNode)[]) as Partial ctx.graph.updateNode(id, changes) ctx.runLayoutForNode(id) ctx.undo.push({ diff --git a/packages/core/src/editor/undo.ts b/packages/core/src/editor/undo.ts index 4021dd79a..d396d1d00 100644 --- a/packages/core/src/editor/undo.ts +++ b/packages/core/src/editor/undo.ts @@ -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, 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 + const current = pick(node, Object.keys(previous) as (keyof SceneNode)[]) as Partial ctx.undo.push({ label, forward: () => { diff --git a/packages/core/src/kiwi/fig/import.ts b/packages/core/src/kiwi/fig/import.ts index ed767b2f7..a0ff63191 100644 --- a/packages/core/src/kiwi/fig/import.ts +++ b/packages/core/src/kiwi/fig/import.ts @@ -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( diff --git a/packages/core/src/rpc/analyze-commands.ts b/packages/core/src/rpc/analyze-commands.ts index 6396226b8..96d66f937 100644 --- a/packages/core/src/rpc/analyze-commands.ts +++ b/packages/core/src/rpc/analyze-commands.ts @@ -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() - 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 b.count - a.count), totalNodes, clusters } + return { colors: orderBy(colors, ['count'], ['desc']), totalNodes, clusters } } } @@ -166,7 +168,7 @@ export const analyzeTypographyCommand: RpcCommand b.count - a.count), totalTextNodes } + return { styles: orderBy([...styleMap.values()], ['count'], ['desc']), totalTextNodes } } } @@ -210,9 +212,11 @@ export const analyzeSpacingCommand: RpcCommand = { } const toValues = (map: Map) => - [...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 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 } } diff --git a/packages/core/src/scene-graph/variables.ts b/packages/core/src/scene-graph/variables.ts index 0215b6b6c..4c061a67b 100644 --- a/packages/core/src/scene-graph/variables.ts +++ b/packages/core/src/scene-graph/variables.ts @@ -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]) } diff --git a/packages/core/src/text/fonts.ts b/packages/core/src/text/fonts.ts index 0f83e8547..6277584b5 100644 --- a/packages/core/src/text/fonts.ts +++ b/packages/core/src/text/fonts.ts @@ -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 { 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 { diff --git a/packages/core/src/text/style-runs.ts b/packages/core/src/text/style-runs.ts index a7fe3f21e..975d62605 100644 --- a/packages/core/src/text/style-runs.ts +++ b/packages/core/src/text/style-runs.ts @@ -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 } } diff --git a/packages/core/src/tools/analyze/clusters.ts b/packages/core/src/tools/analyze/clusters.ts index 4957d45d0..ae2af8d9d 100644 --- a/packages/core/src/tools/analyze/clusters.ts +++ b/packages/core/src/tools/analyze/clusters.ts @@ -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 } } diff --git a/packages/core/src/tools/analyze/colors.ts b/packages/core/src/tools/analyze/colors.ts index a445ae5fd..ece44a358 100644 --- a/packages/core/src/tools/analyze/colors.ts +++ b/packages/core/src/tools/analyze/colors.ts @@ -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 = { 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() 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 diff --git a/packages/core/src/tools/analyze/spacing.ts b/packages/core/src/tools/analyze/spacing.ts index 9916906d7..332c65d56 100644 --- a/packages/core/src/tools/analyze/spacing.ts +++ b/packages/core/src/tools/analyze/spacing.ts @@ -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) diff --git a/packages/core/src/tools/analyze/typography.ts b/packages/core/src/tools/analyze/typography.ts index ff8c26ab3..33604fbe0 100644 --- a/packages/core/src/tools/analyze/typography.ts +++ b/packages/core/src/tools/analyze/typography.ts @@ -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() 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 }) + ) } } diff --git a/packages/core/src/tools/read/fonts.ts b/packages/core/src/tools/read/fonts.ts index 6837656d8..4a9446c34 100644 --- a/packages/core/src/tools/read/fonts.ts +++ b/packages/core/src/tools/read/fonts.ts @@ -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))