Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c31ab914a | ||
|
|
f591c86aba | ||
|
|
8cab99a2ee | ||
|
|
ac25cdca38 | ||
|
|
dca5f0f5a6 | ||
|
|
dc988cdf45 | ||
|
|
326267521d | ||
|
|
e6253d2455 | ||
|
|
fa55b04fa0 | ||
|
|
eded6e0db2 | ||
|
|
1f9a3b6a8d |
@@ -10,5 +10,5 @@
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="17 18 11 12 17 6" />
|
||||
<path d="M7 6V18" />
|
||||
<path d="M7 6v12" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 272 B After Width: | Height: | Size: 272 B |
@@ -10,5 +10,5 @@
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="7 18 13 12 7 6" />
|
||||
<path d="M17 6V18" />
|
||||
<path d="M17 6v12" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 271 B After Width: | Height: | Size: 271 B |
13
icons/folder-open.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M6 17l2-5h14l-3 8a2 2 0 01-2 1H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h7a2 2 0 012 2v4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 305 B |
16
icons/timer-off.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M10 2h4" />
|
||||
<path d="M7.43 7.433A8 8 0 0118.566 18.57M4.582 11A8 8 0 0015 21.419" />
|
||||
<path d="M2 2l20 20" />
|
||||
<path d="M12 12v-2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 357 B |
@@ -13,9 +13,9 @@
|
||||
<path d="M15 16v-2" />
|
||||
<path d="M8 9h2" />
|
||||
<path d="M20 9h2" />
|
||||
<path d="M17.8 11.8 19 13" />
|
||||
<path d="M17.8 11.8L19 13" />
|
||||
<path d="M15 9h0" />
|
||||
<path d="M17.8 6.2 19 5" />
|
||||
<path d="M3 21 12 12" />
|
||||
<path d="M12.2 6.2 11 5" />
|
||||
<path d="M17.8 6.2L19 5" />
|
||||
<path d="M3 21l9-9" />
|
||||
<path d="M12.2 6.2L11 5" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 443 B After Width: | Height: | Size: 441 B |
16
icons/wrap-text.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<path d="M3 12h15a3 3 0 110 6h-4" />
|
||||
<polyline points="16 16 14 18 16 20" />
|
||||
<line x1="3" y1="18" x2="10" y2="18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 371 B |
5
packages/lucide-angular/.gitignore
vendored
@@ -46,6 +46,9 @@ testem.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
#npm-yarn
|
||||
# npm-yarn
|
||||
package-lock.json
|
||||
src/createElement.js
|
||||
|
||||
# angular cache
|
||||
.angular/cache
|
||||
@@ -23,7 +23,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "yarn clean && yarn build:icons && yarn build:ng",
|
||||
"clean": "rm -rf dist && rm -rf ./src/icons/*.ts",
|
||||
"clean": "npx shx rm -rf dist && npx shx rm -rf ./src/icons/*.ts",
|
||||
"build:icons": "yarn --cwd ../../ build:icons --output=../packages/lucide-angular/src --templateSrc=../packages/lucide-angular/scripts/exportTemplate --iconFileExtention=.ts --exportFileName=index.ts",
|
||||
"build:ng": "ng build --prod",
|
||||
"test:headless": "ng test --no-watch --no-progress --browsers=ChromeHeadlessCI",
|
||||
@@ -33,35 +33,31 @@
|
||||
"postinstall": "ngcc"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^11.2.6",
|
||||
"@angular/core": "^11.2.6"
|
||||
"tslib": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.1102.5",
|
||||
"@angular/cli": "~11.2.5",
|
||||
"@angular/common": "~11.2.6",
|
||||
"@angular/compiler": "~11.2.6",
|
||||
"@angular/compiler-cli": "~11.2.6",
|
||||
"@angular/core": "~11.2.6",
|
||||
"@angular/platform-browser": "~11.2.6",
|
||||
"@angular/platform-browser-dynamic": "~11.2.6",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/node": "^12.11.1",
|
||||
"codelyzer": "^6.0.0",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "~6.1.0",
|
||||
"@angular/cli": "~11.2.15",
|
||||
"@angular/common": "~11.2.14",
|
||||
"@angular/compiler": "~11.2.14",
|
||||
"@angular/compiler-cli": "~11.2.14",
|
||||
"@angular/core": "~11.2.14",
|
||||
"@angular/platform-browser": "~11.2.14",
|
||||
"@angular/platform-browser-dynamic": "~11.2.14",
|
||||
"ng-packagr": "^11.2.4",
|
||||
"@types/jasmine": "~3.10.2",
|
||||
"@types/node": "^16.11.7",
|
||||
"codelyzer": "^6.0.2",
|
||||
"jasmine-core": "~3.10.1",
|
||||
"jasmine-spec-reporter": "~7.0.0",
|
||||
"karma": "~6.3.8",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.0.3",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"ng-packagr": "^11.0.0",
|
||||
"karma-jasmine": "~4.0.1",
|
||||
"karma-jasmine-html-reporter": "^1.7.0",
|
||||
"protractor": "~7.0.0",
|
||||
"puppeteer": "^8.0.0",
|
||||
"ts-node": "~8.3.0",
|
||||
"ts-node": "~10.4.0",
|
||||
"tslint": "~6.1.0",
|
||||
"typescript": "~4.1.5"
|
||||
}
|
||||
|
||||
675
packages/lucide-figma/figma.d.ts
vendored
@@ -1,675 +0,0 @@
|
||||
// Global variable with Figma's plugin API.
|
||||
declare const figma: PluginAPI
|
||||
declare const __html__: string
|
||||
|
||||
interface PluginAPI {
|
||||
readonly currentPage: PageNode
|
||||
|
||||
// Root of the current Figma document.
|
||||
readonly root: DocumentNode
|
||||
|
||||
// API for accessing viewport information.
|
||||
readonly viewport: ViewportAPI
|
||||
|
||||
// call this once your plugin is finished executing.
|
||||
closePlugin(): void
|
||||
|
||||
// Command that the user chose through menu when launching the plugin.
|
||||
readonly command: string
|
||||
|
||||
// Finds a node by its id. If not found, returns null.
|
||||
getNodeById(id: string): BaseNode | null
|
||||
|
||||
// Finds a style by its id. If not found, returns null.
|
||||
getStyleById(id: string): BaseStyle | null
|
||||
|
||||
// Access browser APIs and/or show UI to the user.
|
||||
showUI(html: string, options?: ShowUIOptions): void
|
||||
readonly ui: UIAPI
|
||||
|
||||
// Lets you store persistent data on the user's local machine
|
||||
readonly clientStorage: ClientStorageAPI
|
||||
|
||||
// This value is returned when a property is in a "mixed" state.
|
||||
// In order to check if a property is in a mixed state, always
|
||||
// compare directly to this value. I.e.
|
||||
// `if (node.cornerRadius === figma.mixed) { ... }`
|
||||
mixed: symbol
|
||||
|
||||
// Creates new nodes. Nodes will start off inserted
|
||||
// into the current page.
|
||||
// To move them elsewhere use `appendChild` or `insertChild`
|
||||
createRectangle(): RectangleNode
|
||||
createLine(): LineNode
|
||||
createEllipse(): EllipseNode
|
||||
createPolygon(): PolygonNode
|
||||
createStar(): StarNode
|
||||
createVector(): VectorNode
|
||||
createText(): TextNode
|
||||
createBooleanOperation(): BooleanOperationNode
|
||||
createFrame(): FrameNode
|
||||
createComponent(): ComponentNode
|
||||
createPage(): PageNode
|
||||
createSlice(): SliceNode
|
||||
|
||||
// Creates styles. A style's id can be assigned to
|
||||
// node properties like textStyleId, fillStyleId, etc.
|
||||
createPaintStyle(): PaintStyle
|
||||
createTextStyle(): TextStyle
|
||||
createEffectStyle(): EffectStyle
|
||||
createGridStyle(): GridStyle
|
||||
|
||||
// These let you insert stuff from the team library if you have the key
|
||||
importComponentByKeyAsync(key: string): Promise<ComponentNode>
|
||||
importStyleByKeyAsync(key: string): Promise<BaseStyle>
|
||||
|
||||
// Return all fonts currently supported for use with the "fontName" property
|
||||
listAvailableFontsAsync(): Promise<Font[]>
|
||||
|
||||
// You must await the promise returned here before being able to use "fontName"
|
||||
loadFontAsync(fontName: FontName): Promise<void>
|
||||
|
||||
// Creates node from an SVG string.
|
||||
createNodeFromSvg(svg: string): FrameNode
|
||||
|
||||
// Creates an Image object using the provided file contents.
|
||||
createImage(data: Uint8Array): Image
|
||||
|
||||
// Groups every node in `nodes` under a new group.
|
||||
group(nodes: ReadonlyArray<BaseNode>, parent: BaseNode & ChildrenMixin, index?: number): FrameNode
|
||||
|
||||
// Flattens every node in `nodes` into a single vector network.
|
||||
flatten(nodes: ReadonlyArray<BaseNode>, parent?: BaseNode & ChildrenMixin, index?: number): VectorNode
|
||||
}
|
||||
|
||||
interface ClientStorageAPI {
|
||||
// This stores information in the browser, not on the server. It's similar to localStorage, but is
|
||||
// asynchronous, and allows storing objects, arrays, strings, numbers, booleans, null, undefined and Uint8Arrays.
|
||||
getAsync(key: string): Promise<any | undefined>
|
||||
setAsync(key: string, value: any): Promise<void>
|
||||
}
|
||||
|
||||
type ShowUIOptions = {
|
||||
visible?: boolean, // defaults to true
|
||||
width?: number, // defaults to 300
|
||||
height?: number, // defaults to 200
|
||||
}
|
||||
|
||||
interface UIAPI {
|
||||
show(): void
|
||||
hide(): void
|
||||
resize(width: number, height: number): void
|
||||
close(): void
|
||||
|
||||
// Sends a message to the iframe.
|
||||
postMessage(pluginMessage: any): void
|
||||
|
||||
// Registers a callback for messages sent by the iframe.
|
||||
onmessage: ((pluginMessage: any) => void) | undefined
|
||||
}
|
||||
|
||||
interface ViewportAPI {
|
||||
center: { x: number, y: number }
|
||||
|
||||
// 1.0 means 100% zoom, 0.5 means 50% zoom.
|
||||
zoom: number
|
||||
|
||||
// Adjust the viewport such that it shows the provided nodes.
|
||||
scrollAndZoomIntoView(nodes: ReadonlyArray<BaseNode>)
|
||||
}
|
||||
|
||||
// manifest.json format
|
||||
interface ManifestJson {
|
||||
// Name of the plugin.
|
||||
name: string
|
||||
|
||||
// Version of the runtime that the plugin uses, e.g. '0.5.0'.
|
||||
version: string
|
||||
|
||||
// The file name that contains the plugin code.
|
||||
script: string
|
||||
|
||||
// The file name that contains the html code made available in script.
|
||||
html?: string
|
||||
|
||||
// Shell command to be executed before the contents of the `html` and `script` files are read.
|
||||
build?: string
|
||||
|
||||
// Menu items to show up in UI.
|
||||
menu?: ManifestMenuItem[]
|
||||
}
|
||||
|
||||
type ManifestMenuItem =
|
||||
// Clickable menu item.
|
||||
{ name: string, command: string } |
|
||||
// Separator
|
||||
{ separator: true } |
|
||||
// Submenu
|
||||
{ name: string, menu: ManifestMenuItem[] }
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Values
|
||||
|
||||
// These are the top two rows of a 3x3 matrix. This is enough to represent
|
||||
// translation, rotation, and skew.
|
||||
type Transform = [
|
||||
[number, number, number],
|
||||
[number, number, number]
|
||||
]
|
||||
|
||||
interface Vector {
|
||||
readonly x: number
|
||||
readonly y: number
|
||||
}
|
||||
|
||||
interface RGB {
|
||||
readonly r: number
|
||||
readonly g: number
|
||||
readonly b: number
|
||||
}
|
||||
|
||||
interface RGBA {
|
||||
readonly r: number
|
||||
readonly g: number
|
||||
readonly b: number
|
||||
readonly a: number
|
||||
}
|
||||
|
||||
interface FontName {
|
||||
readonly family: string
|
||||
readonly style: string
|
||||
}
|
||||
|
||||
interface ArcData {
|
||||
readonly startingAngle: number
|
||||
readonly endingAngle: number
|
||||
readonly innerRadius: number
|
||||
}
|
||||
|
||||
interface ShadowEffect {
|
||||
readonly type: "DROP_SHADOW" | "INNER_SHADOW"
|
||||
readonly color: RGBA
|
||||
readonly offset: Vector
|
||||
readonly radius: number
|
||||
readonly visible: boolean
|
||||
readonly blendMode: BlendMode
|
||||
}
|
||||
|
||||
interface BlurEffect {
|
||||
readonly type: "LAYER_BLUR" | "BACKGROUND_BLUR"
|
||||
readonly radius: number
|
||||
readonly visible: boolean
|
||||
}
|
||||
|
||||
type Effect = ShadowEffect | BlurEffect
|
||||
|
||||
type ConstraintType = "MIN" | "CENTER" | "MAX" | "STRETCH" | "SCALE"
|
||||
|
||||
interface Constraints {
|
||||
readonly horizontal: ConstraintType
|
||||
readonly vertical: ConstraintType
|
||||
}
|
||||
|
||||
interface ColorStop {
|
||||
readonly position: number
|
||||
readonly color: RGBA
|
||||
}
|
||||
|
||||
interface SolidPaint {
|
||||
readonly type: "SOLID"
|
||||
readonly color: RGB
|
||||
|
||||
readonly visible?: boolean
|
||||
readonly opacity?: number
|
||||
}
|
||||
|
||||
interface GradientPaint {
|
||||
readonly type: "GRADIENT_LINEAR" | "GRADIENT_RADIAL" | "GRADIENT_ANGULAR" | "GRADIENT_DIAMOND"
|
||||
readonly gradientTransform: Transform
|
||||
readonly gradientStops: ReadonlyArray<ColorStop>
|
||||
|
||||
readonly visible?: boolean
|
||||
readonly opacity?: number
|
||||
}
|
||||
|
||||
interface ImagePaint {
|
||||
readonly type: "IMAGE"
|
||||
readonly scaleMode: "FILL" | "FIT" | "CROP" | "TILE"
|
||||
readonly image: Image | null
|
||||
readonly imageTransform?: Transform // setting for "CROP"
|
||||
readonly scalingFactor?: number // setting for "TILE"
|
||||
|
||||
readonly visible?: boolean
|
||||
readonly opacity?: number
|
||||
}
|
||||
|
||||
type Paint = SolidPaint | GradientPaint | ImagePaint
|
||||
|
||||
interface Guide {
|
||||
readonly axis: "X" | "Y"
|
||||
readonly offset: number
|
||||
}
|
||||
|
||||
interface RowsColsLayoutGrid {
|
||||
readonly pattern: "ROWS" | "COLUMNS"
|
||||
readonly alignment: "MIN" | "STRETCH" | "CENTER"
|
||||
readonly gutterSize: number
|
||||
|
||||
readonly count: number // Infinity when "Auto" is set in the UI
|
||||
readonly sectionSize?: number // Not set for alignment: "STRETCH"
|
||||
readonly offset?: number // Not set for alignment: "CENTER"
|
||||
|
||||
readonly visible?: boolean
|
||||
readonly color?: RGBA
|
||||
}
|
||||
|
||||
interface GridLayoutGrid {
|
||||
readonly pattern: "GRID"
|
||||
readonly sectionSize: number
|
||||
|
||||
readonly visible?: boolean
|
||||
readonly color?: RGBA
|
||||
}
|
||||
|
||||
type LayoutGrid = RowsColsLayoutGrid | GridLayoutGrid
|
||||
|
||||
interface ExportSettingsImage {
|
||||
format: "JPG" | "PNG"
|
||||
contentsOnly?: boolean // defaults to true
|
||||
suffix?: string
|
||||
constraint?: { // defaults to unscaled ({ type: "SCALE", value: 1 })
|
||||
type: "SCALE" | "WIDTH" | "HEIGHT"
|
||||
value: number
|
||||
}
|
||||
}
|
||||
|
||||
interface ExportSettingsSVG {
|
||||
format: "SVG"
|
||||
contentsOnly?: boolean // defaults to true
|
||||
suffix?: string
|
||||
svgOutlineText?: boolean // defaults to true
|
||||
svgIdAttribute?: boolean // defaults to false
|
||||
svgSimplifyStroke?: boolean // defaults to true
|
||||
}
|
||||
|
||||
interface ExportSettingsPDF {
|
||||
format: "PDF"
|
||||
contentsOnly?: boolean // defaults to true
|
||||
suffix?: string
|
||||
}
|
||||
|
||||
type ExportSettings = ExportSettingsImage | ExportSettingsSVG | ExportSettingsPDF
|
||||
|
||||
type WindingRule = "NONZERO" | "EVENODD"
|
||||
|
||||
interface VectorVertex {
|
||||
readonly x: number
|
||||
readonly y: number
|
||||
readonly strokeCap?: StrokeCap
|
||||
readonly strokeJoin?: StrokeJoin
|
||||
readonly cornerRadius?: number
|
||||
readonly handleMirroring?: HandleMirroring
|
||||
}
|
||||
|
||||
interface VectorSegment {
|
||||
readonly start: number
|
||||
readonly end: number
|
||||
readonly tangentStart?: Vector // Defaults to { x: 0, y: 0 }
|
||||
readonly tangentEnd?: Vector // Defaults to { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
interface VectorRegion {
|
||||
readonly windingRule: WindingRule
|
||||
readonly loops: ReadonlyArray<ReadonlyArray<number>>
|
||||
}
|
||||
|
||||
interface VectorNetwork {
|
||||
readonly vertices: ReadonlyArray<VectorVertex>
|
||||
readonly segments: ReadonlyArray<VectorSegment>
|
||||
readonly regions?: ReadonlyArray<VectorRegion> // Defaults to []
|
||||
}
|
||||
|
||||
interface VectorPath {
|
||||
// Similar to the svg fill-rule
|
||||
// "NONE" means an open path won't have a fill
|
||||
readonly windingRule: WindingRule | "NONE"
|
||||
readonly data: string
|
||||
}
|
||||
|
||||
type VectorPaths = ReadonlyArray<VectorPath>
|
||||
|
||||
interface NumberWithUnits {
|
||||
readonly value: number
|
||||
readonly units: "PIXELS" | "PERCENT"
|
||||
}
|
||||
|
||||
type BlendMode =
|
||||
"PASS_THROUGH" |
|
||||
"NORMAL" |
|
||||
"DARKEN" |
|
||||
"MULTIPLY" |
|
||||
"LINEAR_BURN" |
|
||||
"COLOR_BURN" |
|
||||
"LIGHTEN" |
|
||||
"SCREEN" |
|
||||
"LINEAR_DODGE" |
|
||||
"COLOR_DODGE" |
|
||||
"OVERLAY" |
|
||||
"SOFT_LIGHT" |
|
||||
"HARD_LIGHT" |
|
||||
"DIFFERENCE" |
|
||||
"EXCLUSION" |
|
||||
"HUE" |
|
||||
"SATURATION" |
|
||||
"COLOR" |
|
||||
"LUMINOSITY"
|
||||
|
||||
interface Font {
|
||||
fontName: FontName
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Mixins
|
||||
|
||||
interface BaseNodeMixin {
|
||||
readonly id: string
|
||||
readonly parent: (BaseNode & ChildrenMixin) | null
|
||||
name: string
|
||||
visible: boolean
|
||||
locked: boolean
|
||||
removed: boolean
|
||||
toString(): string
|
||||
remove(): void
|
||||
|
||||
// Attach custom data to a node. Only your plugin will be able to read this.
|
||||
getPluginData(key: string): string
|
||||
setPluginData(key: string, value: string): void
|
||||
|
||||
// Attach custom data to a node. All plugins will be able to read this.
|
||||
// Namespace is a string that must be at least 3 alphanumeric characters, and should
|
||||
// be a name related to your plugin. This is a mandatory argument to avoid multiple
|
||||
// multiple plugins adding keys like "data" and colliding with each other. Other
|
||||
// plugins will still be able to read shared plugin data as long as they know the
|
||||
// namespace you use.
|
||||
getSharedPluginData(namespace: string, key: string): string
|
||||
setSharedPluginData(namespace: string, key: string, value: string): void
|
||||
}
|
||||
|
||||
interface ChildrenMixin {
|
||||
// Sorted back-to-front. I.e. the top-most child is last in this array.
|
||||
readonly children: ReadonlyArray<BaseNode>
|
||||
|
||||
// Adds to the end of the .children array. I.e. visually on top of all other
|
||||
// children.
|
||||
appendChild(child: BaseNode): void
|
||||
|
||||
insertChild(index: number, child: BaseNode): void
|
||||
findAll(callback?: (node: BaseNode) => boolean): ReadonlyArray<BaseNode>
|
||||
findOne(callback: (node: BaseNode) => boolean): BaseNode | null
|
||||
}
|
||||
|
||||
interface LayoutMixin {
|
||||
readonly absoluteTransform: Transform
|
||||
relativeTransform: Transform
|
||||
x: number // The same as "relativeTransform[0][2]"
|
||||
y: number // The same as "relativeTransform[1][2]"
|
||||
rotation: number // The angle of the x axis of "relativeTransform" in degrees. Returns values from -180 to 180.
|
||||
|
||||
readonly size: Vector
|
||||
readonly width: number // The same as "size.x"
|
||||
readonly height: number // The same as "size.y"
|
||||
|
||||
// Resizes the node. If children of the node has constraints, it applies those constraints
|
||||
// width and height must be >= 0.01
|
||||
resize(width: number, height: number): void
|
||||
|
||||
// Resizes the node. Children of the node are never resized, even if those children have
|
||||
// constraints. width and height must be >= 0.01
|
||||
resizeWithoutConstraints(width: number, height: number): void
|
||||
|
||||
constraints: Constraints
|
||||
}
|
||||
|
||||
interface BlendMixin {
|
||||
opacity: number
|
||||
blendMode: BlendMode
|
||||
isMask: boolean
|
||||
effects: ReadonlyArray<Effect>
|
||||
effectStyleId: string
|
||||
}
|
||||
|
||||
interface FrameMixin {
|
||||
backgrounds: ReadonlyArray<Paint>
|
||||
layoutGrids: ReadonlyArray<LayoutGrid>
|
||||
clipsContent: boolean
|
||||
guides: ReadonlyArray<Guide>
|
||||
gridStyleId: string
|
||||
backgroundStyleId: string
|
||||
}
|
||||
|
||||
type StrokeCap = "NONE" | "ROUND" | "SQUARE" | "ARROW_LINES" | "ARROW_EQUILATERAL"
|
||||
type StrokeJoin = "MITER" | "BEVEL" | "ROUND"
|
||||
type HandleMirroring = "NONE" | "ANGLE" | "ANGLE_AND_LENGTH"
|
||||
|
||||
interface GeometryMixin {
|
||||
fills: ReadonlyArray<Paint> | symbol // This can return figma.mixed on TEXT nodes
|
||||
strokes: ReadonlyArray<Paint>
|
||||
strokeWeight: number
|
||||
strokeAlign: "CENTER" | "INSIDE" | "OUTSIDE"
|
||||
strokeCap: StrokeCap | symbol // This can return figma.mixed on VECTOR nodes if vertices have different strokeCap values
|
||||
strokeJoin: StrokeJoin | symbol // This can return figma.mixed on VECTOR nodes if vertices have different strokeJoin values
|
||||
dashPattern: ReadonlyArray<number>
|
||||
fillStyleId: string | symbol // This can return figma.mixed on TEXT nodes
|
||||
strokeStyleId: string
|
||||
}
|
||||
|
||||
interface CornerMixin {
|
||||
// This can return figma.mixed on VECTOR nodes if vertices have different cornerRadius values,
|
||||
// and on RECTANGLE nodes if node.topLeftRadius etc has different values
|
||||
cornerRadius: number | symbol
|
||||
|
||||
cornerSmoothing: number
|
||||
}
|
||||
|
||||
interface ExportMixin {
|
||||
exportSettings: ExportSettings[]
|
||||
exportAsync(settings?: ExportSettings): Promise<Uint8Array> // Defaults to PNG format
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Nodes
|
||||
|
||||
interface DocumentNode extends BaseNodeMixin, ChildrenMixin {
|
||||
readonly type: "DOCUMENT"
|
||||
clone(): DocumentNode // Note: this always throws an error
|
||||
}
|
||||
|
||||
interface PageNode extends BaseNodeMixin, ChildrenMixin, ExportMixin {
|
||||
readonly type: "PAGE"
|
||||
clone(): PageNode // cloned node starts off inserted into current page
|
||||
|
||||
guides: ReadonlyArray<Guide>
|
||||
selection: ReadonlyArray<BaseNode>
|
||||
}
|
||||
|
||||
interface FrameNode extends BaseNodeMixin, BlendMixin, ChildrenMixin, FrameMixin, LayoutMixin, ExportMixin {
|
||||
readonly type: "FRAME" | "GROUP"
|
||||
clone(): FrameNode // cloned node starts off inserted into current page
|
||||
}
|
||||
|
||||
interface SliceNode extends BaseNodeMixin, LayoutMixin, ExportMixin {
|
||||
readonly type: "SLICE"
|
||||
clone(): SliceNode // cloned node starts off inserted into current page
|
||||
}
|
||||
|
||||
interface RectangleNode extends BaseNodeMixin, BlendMixin, CornerMixin, GeometryMixin, LayoutMixin, ExportMixin {
|
||||
readonly type: "RECTANGLE"
|
||||
clone(): RectangleNode // cloned node starts off inserted into current page
|
||||
topLeftRadius: number
|
||||
topRightRadius: number
|
||||
bottomLeftRadius: number
|
||||
bottomRightRadius: number
|
||||
}
|
||||
|
||||
interface LineNode extends BaseNodeMixin, BlendMixin, GeometryMixin, LayoutMixin, ExportMixin {
|
||||
readonly type: "LINE"
|
||||
clone(): LineNode // cloned node starts off inserted into current page
|
||||
}
|
||||
|
||||
interface EllipseNode extends BaseNodeMixin, BlendMixin, CornerMixin, GeometryMixin, LayoutMixin, ExportMixin {
|
||||
readonly type: "ELLIPSE"
|
||||
clone(): EllipseNode // cloned node starts off inserted into current page
|
||||
arcData: ArcData
|
||||
}
|
||||
|
||||
interface PolygonNode extends BaseNodeMixin, BlendMixin, CornerMixin, GeometryMixin, LayoutMixin, ExportMixin {
|
||||
readonly type: "POLYGON"
|
||||
clone(): PolygonNode // cloned node starts off inserted into current page
|
||||
pointCount: number
|
||||
}
|
||||
|
||||
interface StarNode extends BaseNodeMixin, BlendMixin, CornerMixin, GeometryMixin, LayoutMixin, ExportMixin {
|
||||
readonly type: "STAR"
|
||||
clone(): StarNode // cloned node starts off inserted into current page
|
||||
pointCount: number
|
||||
|
||||
// This is a percentage value from 0 to 1
|
||||
innerRadius: number
|
||||
}
|
||||
|
||||
interface VectorNode extends BaseNodeMixin, BlendMixin, CornerMixin, GeometryMixin, LayoutMixin, ExportMixin {
|
||||
readonly type: "VECTOR"
|
||||
clone(): VectorNode // cloned node starts off inserted into current page
|
||||
vectorNetwork: VectorNetwork
|
||||
vectorPaths: VectorPaths
|
||||
handleMirroring: HandleMirroring | symbol // This can return figma.mixed if vertices have different handleMirroring values
|
||||
}
|
||||
|
||||
interface TextNode extends BaseNodeMixin, BlendMixin, GeometryMixin, LayoutMixin, ExportMixin {
|
||||
readonly type: "TEXT"
|
||||
clone(): TextNode // cloned node starts off inserted into current page
|
||||
characters: string
|
||||
textAlignHorizontal: "LEFT" | "CENTER" | "RIGHT" | "JUSTIFIED"
|
||||
textAlignVertical: "TOP" | "CENTER" | "BOTTOM"
|
||||
textAutoResize: "NONE" | "WIDTH_AND_HEIGHT" | "HEIGHT"
|
||||
paragraphIndent: number
|
||||
paragraphSpacing: number
|
||||
autoRename: boolean
|
||||
|
||||
// These properties can all return figma.mixed if the text has multiple values for the property
|
||||
textStyleId: string | symbol
|
||||
fontSize: number | symbol
|
||||
fontName: FontName | symbol
|
||||
textCase: "ORIGINAL" | "UPPER" | "LOWER" | "TITLE" | symbol
|
||||
textDecoration: "NONE" | "UNDERLINE" | "STRIKETHROUGH" | symbol
|
||||
letterSpacing: NumberWithUnits | symbol
|
||||
lineHeight: NumberWithUnits | symbol
|
||||
}
|
||||
|
||||
interface ComponentNode extends BaseNodeMixin, BlendMixin, ChildrenMixin, FrameMixin, LayoutMixin, ExportMixin {
|
||||
readonly type: "COMPONENT"
|
||||
clone(): ComponentNode // cloned node starts off inserted into current page
|
||||
|
||||
createInstance(): InstanceNode // instance starts off inserted into current page
|
||||
description: string
|
||||
readonly remote: boolean
|
||||
readonly key: string // The key to use with "importComponentByKeyAsync"
|
||||
}
|
||||
|
||||
interface InstanceNode extends BaseNodeMixin, BlendMixin, ChildrenMixin, FrameMixin, LayoutMixin, ExportMixin {
|
||||
readonly type: "INSTANCE"
|
||||
clone(): InstanceNode // cloned node starts off inserted into current page
|
||||
masterComponent: ComponentNode
|
||||
}
|
||||
|
||||
interface BooleanOperationNode extends BaseNodeMixin, BlendMixin, ChildrenMixin, CornerMixin, GeometryMixin, LayoutMixin, ExportMixin {
|
||||
readonly type: "BOOLEAN_OPERATION"
|
||||
clone(): BooleanOperationNode // cloned node starts off inserted into current page
|
||||
booleanOperation: "UNION" | "INTERSECT" | "SUBTRACT" | "EXCLUDE"
|
||||
}
|
||||
|
||||
type BaseNode =
|
||||
DocumentNode |
|
||||
PageNode |
|
||||
SliceNode |
|
||||
FrameNode |
|
||||
ComponentNode |
|
||||
InstanceNode |
|
||||
BooleanOperationNode |
|
||||
VectorNode |
|
||||
StarNode |
|
||||
LineNode |
|
||||
EllipseNode |
|
||||
PolygonNode |
|
||||
RectangleNode |
|
||||
TextNode
|
||||
|
||||
type NodeType =
|
||||
"DOCUMENT" |
|
||||
"PAGE" |
|
||||
"SLICE" |
|
||||
"FRAME" |
|
||||
"GROUP" |
|
||||
"COMPONENT" |
|
||||
"INSTANCE" |
|
||||
"BOOLEAN_OPERATION" |
|
||||
"VECTOR" |
|
||||
"STAR" |
|
||||
"LINE" |
|
||||
"ELLIPSE" |
|
||||
"POLYGON" |
|
||||
"RECTANGLE" |
|
||||
"TEXT"
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Styles
|
||||
type StyleType = "PAINT" | "TEXT" | "EFFECT" | "GRID"
|
||||
|
||||
interface BaseStyle {
|
||||
// The string to uniquely identify a style by
|
||||
readonly id: string
|
||||
readonly type: StyleType
|
||||
name: string // Note: setting this also sets "autoRename" to false on TextNodes
|
||||
description: string
|
||||
remote: boolean
|
||||
readonly key: string // The key to use with "importStyleByKeyAsync"
|
||||
remove(): void
|
||||
}
|
||||
|
||||
interface PaintStyle extends BaseStyle {
|
||||
type: "PAINT"
|
||||
paints: ReadonlyArray<Paint>
|
||||
}
|
||||
|
||||
interface TextStyle extends BaseStyle {
|
||||
type: "TEXT"
|
||||
fontSize: number
|
||||
textDecoration: "NONE" | "UNDERLINE" | "STRIKETHROUGH"
|
||||
fontName: FontName
|
||||
letterSpacing: NumberWithUnits
|
||||
lineHeight: NumberWithUnits
|
||||
paragraphIndent: number
|
||||
paragraphSpacing: number
|
||||
textCase: "ORIGINAL" | "UPPER" | "LOWER" | "TITLE"
|
||||
}
|
||||
|
||||
interface EffectStyle extends BaseStyle {
|
||||
type: "EFFECT"
|
||||
effects: ReadonlyArray<Paint>
|
||||
}
|
||||
|
||||
interface GridStyle extends BaseStyle {
|
||||
type: "GRID"
|
||||
layoutGrids: ReadonlyArray<LayoutGrid>
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Other
|
||||
|
||||
interface Image {
|
||||
// Returns a unique hash for the image
|
||||
readonly hash: string
|
||||
|
||||
// The contents of the image file
|
||||
getBytesAsync(): Promise<Uint8Array>
|
||||
}
|
||||
@@ -2,7 +2,16 @@
|
||||
"name": "Lucide Icons",
|
||||
"id": "939567362549682242",
|
||||
"api": "1.0.0",
|
||||
"ui": "build/ui.html",
|
||||
"main": "build/main.js",
|
||||
"editorType": ["figma"]
|
||||
"editorType": ["figma"],
|
||||
"main": "dist/assets/main.js",
|
||||
"ui": {
|
||||
"interface": "dist/src/interface/interface.html",
|
||||
"worker": "dist/src/worker/worker.html"
|
||||
},
|
||||
"parameterOnly": false,
|
||||
"parameters": [{
|
||||
"name": "Icon",
|
||||
"key": "icon-name",
|
||||
"description": "Enter the name of the icon you want to insert."
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
{
|
||||
"name": "lucide-figma",
|
||||
"version": "0.15.11",
|
||||
"license": "ISC",
|
||||
"private": true,
|
||||
"main": "build/ui.js",
|
||||
"license": "ISC",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
"build": "webpack --mode=production",
|
||||
"watch": "webpack --mode=development --watch"
|
||||
"dev": "vite",
|
||||
"build": "tsc && rm -rf dist && yarn build:main && yarn build:worker && yarn build:interface",
|
||||
"build:main": "INPUT=main vite build",
|
||||
"build:worker": "INPUT=worker vite build",
|
||||
"build:interface": "INPUT=interface vite build",
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/core": "^10.0.14",
|
||||
"@types/react": "^16.8.23",
|
||||
"@types/react-dom": "^16.8.4",
|
||||
"css-loader": "^3.0.0",
|
||||
"html-webpack-inline-source-plugin": "^0.0.10",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"lucide-react": "0.16.0",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"style-loader": "^0.23.1",
|
||||
"ts-loader": "^6.0.4",
|
||||
"typescript": "^3.5.2",
|
||||
"webpack": "^4.35.2",
|
||||
"webpack-cli": "^3.3.5"
|
||||
"minimist": "^1.2.5",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@figma/plugin-typings": "^1.36.0",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@vitejs/plugin-react": "^1.0.0",
|
||||
"typescript": "^4.3.2",
|
||||
"vite": "^2.6.4",
|
||||
"vite-plugin-singlefile": "^0.5.1"
|
||||
}
|
||||
}
|
||||
|
||||
68
packages/lucide-figma/src/api/fetchIcons.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import iconNodeToSvg from "../helpers/iconNodeToSvg"
|
||||
|
||||
export type IconNode = any[]
|
||||
export type IconName = string
|
||||
|
||||
export type Tag = string[]
|
||||
export interface Tags {
|
||||
[key:string]: Tag
|
||||
}
|
||||
|
||||
export interface LucideIcons {
|
||||
version: string
|
||||
iconNodes: { [key: IconName]: IconNode }
|
||||
tags: Tags,
|
||||
svgs: { [key: IconName]: string }
|
||||
}
|
||||
|
||||
export const fetchIcons = async (cachedIcons? : LucideIcons): Promise<LucideIcons> => {
|
||||
const response = await fetch('https://unpkg.com/lucide-static@latest/package.json')
|
||||
const packageJson = await response.json();
|
||||
|
||||
if(cachedIcons && cachedIcons?.version === packageJson.version) {
|
||||
return cachedIcons
|
||||
}
|
||||
|
||||
const iconNodesResponse = await fetch(`https://unpkg.com/lucide-static@${packageJson.version}/icon-nodes.json`)
|
||||
const tagsResponse = await fetch('https://unpkg.com/lucide-static@latest/tags.json')
|
||||
|
||||
const iconNodes = await iconNodesResponse.json();
|
||||
const tags = await tagsResponse.json();
|
||||
const svgs = Object.keys(iconNodes).reduce((acc : { [key:string]: string}, iconName) => {
|
||||
acc[iconName] = iconNodeToSvg(iconName, iconNodes[iconName])
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const lucideIcons: LucideIcons = {
|
||||
version: packageJson.version,
|
||||
tags,
|
||||
iconNodes,
|
||||
svgs
|
||||
}
|
||||
|
||||
parent.postMessage({
|
||||
pluginMessage: {
|
||||
type: "setCachedIcons",
|
||||
lucideIcons
|
||||
}
|
||||
}, "*")
|
||||
|
||||
return lucideIcons
|
||||
}
|
||||
|
||||
export const getIcons = () => new Promise<LucideIcons>(async (resolve, reject)=> {
|
||||
|
||||
parent.postMessage({
|
||||
pluginMessage: {
|
||||
type: "getCachedIcons",
|
||||
}
|
||||
}, "*")
|
||||
|
||||
window.onmessage = async (event) => {
|
||||
if (event.type === 'message' && event?.data?.pluginMessage.type === 'cachedIcons') {
|
||||
|
||||
const lucideIcons = await fetchIcons(event?.data?.pluginMessage?.cachedIcons)
|
||||
resolve(lucideIcons)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
.icon-button {
|
||||
padding: 8px;
|
||||
color: var(--color-black);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
appearance: none;
|
||||
outline: 0;
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
&:focus,
|
||||
&:active {
|
||||
box-shadow: inset 0 0 0 2px var(--color-blue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { renderToString } from 'react-dom/server'
|
||||
import { FC } from 'react';
|
||||
import './IconButton.scss'
|
||||
|
||||
interface IconButtonProps {
|
||||
name: string,
|
||||
component: FC,
|
||||
}
|
||||
|
||||
function IconButton({ name, component: IconComponent }: IconButtonProps) {
|
||||
const onIconClick = () => {
|
||||
const svg = renderToString(<IconComponent/>);
|
||||
|
||||
parent.postMessage({ pluginMessage: {
|
||||
type: 'drawIcon',
|
||||
icon: { name, svg }
|
||||
}}, '*')
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={name}
|
||||
aria-label={name}
|
||||
onClick={onIconClick}
|
||||
className='icon-button'
|
||||
>
|
||||
<IconComponent />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconButton
|
||||
1
packages/lucide-figma/src/components/IconButton/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './IconButton'
|
||||
@@ -0,0 +1,17 @@
|
||||
.search-input {
|
||||
.icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 4px;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 16px 0 36px;
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import "./SearchInput.scss"
|
||||
import { ChangeEvent } from "react"
|
||||
import SearchIcon from "../icons/SearchIcon"
|
||||
|
||||
interface SearchInputProps extends React.HTMLProps<HTMLDivElement> {
|
||||
value: string,
|
||||
iconCount: number,
|
||||
onChange: (event: ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
function SearchInput({ value, onChange, iconCount, className, ...props }: SearchInputProps) {
|
||||
return (
|
||||
<div
|
||||
className="search-input"
|
||||
{...props}
|
||||
>
|
||||
<SearchIcon className='icon'/>
|
||||
<input
|
||||
autoFocus
|
||||
type="search"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={`Search ${iconCount} icons`}
|
||||
className="input__field"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchInput
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './SearchInput'
|
||||
@@ -1,44 +0,0 @@
|
||||
import { jsx } from '@emotion/core'
|
||||
import theme from '../theme'
|
||||
import { renderToString } from 'react-dom/server'
|
||||
import { FC } from 'react';
|
||||
|
||||
interface IconButtonProps {
|
||||
name: string,
|
||||
component: FC,
|
||||
}
|
||||
|
||||
function IconButton({ name, component: IconComponent }: IconButtonProps) {
|
||||
const onIconclick = () => {
|
||||
const svg = renderToString(<IconComponent/>);
|
||||
|
||||
parent.postMessage({ pluginMessage: { name, svg }}, '*')
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={name}
|
||||
aria-label={name}
|
||||
onClick={onIconclick}
|
||||
css={{
|
||||
padding: theme.space[2],
|
||||
color: '#333',
|
||||
background: 'transparent',
|
||||
border: 0,
|
||||
borderRadius: theme.radii[1],
|
||||
appearance: 'none',
|
||||
outline: 0,
|
||||
'&:hover': {
|
||||
background: 'rgba(0, 0, 0, 0.06)',
|
||||
},
|
||||
'&:focus, &:active': {
|
||||
boxShadow: `inset 0 0 0 2px ${theme.colors.blue}`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IconComponent />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconButton
|
||||
@@ -1,43 +0,0 @@
|
||||
import { jsx } from '@emotion/core'
|
||||
import theme from '../theme'
|
||||
import SearchIcon from './search-icon'
|
||||
interface SearchInputProps extends React.HTMLProps<HTMLDivElement> {
|
||||
value: string,
|
||||
iconCount: number,
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
function SearchInput({ value, onChange, iconCount, ...props }: SearchInputProps) {
|
||||
return (
|
||||
<div css={{ position: 'relative' }} {...props}>
|
||||
<div
|
||||
css={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
padding: theme.space[1],
|
||||
}}
|
||||
>
|
||||
<SearchIcon fill="#333" />
|
||||
</div>
|
||||
<input
|
||||
autoFocus
|
||||
type="search"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={`Search ${iconCount} icons`}
|
||||
css={{
|
||||
width: '100%',
|
||||
height: 40,
|
||||
padding: `0 ${theme.space[4]} 0 36px`,
|
||||
fontFamily: 'inherit',
|
||||
fontSize: theme.fontSizes[0],
|
||||
border: 0,
|
||||
outline: 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchInput
|
||||
13
packages/lucide-figma/src/helpers/filterIcons.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Tags } from "../api/fetchIcons";
|
||||
import { Icon } from "../hooks/useSearch";
|
||||
|
||||
export default (icons: Icon[], tags: Tags ,query:string) =>
|
||||
icons.filter(([name]: Icon) => {
|
||||
const iconTags = tags && tags[name] ? tags[name] : []
|
||||
|
||||
return [name, ...iconTags].some(
|
||||
(item:string) => item
|
||||
.toLowerCase()
|
||||
.includes(query)
|
||||
)
|
||||
})
|
||||
11
packages/lucide-figma/src/helpers/iconNodeToSvg.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createReactComponent } from "lucide-react";
|
||||
import { createElement } from "react";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { IconNode } from "../api/fetchIcons";
|
||||
|
||||
const iconNodeToSvg = (iconName: string, iconNode : IconNode) => {
|
||||
const IconComponent = createReactComponent(iconName, iconNode)
|
||||
return renderToString(createElement(IconComponent));
|
||||
}
|
||||
|
||||
export default iconNodeToSvg
|
||||
17
packages/lucide-figma/src/hooks/useSearch.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { IconName, IconNode, Tags } from "../api/fetchIcons";
|
||||
import filterIcons from "../helpers/filterIcons";
|
||||
|
||||
export type Icon = [
|
||||
name: IconName,
|
||||
iconNode: IconNode
|
||||
]
|
||||
|
||||
function useSearch(icons: Icon[], tags: Tags ,query: string) {
|
||||
if(!query) return icons;
|
||||
|
||||
const searchString = query.toLowerCase()
|
||||
|
||||
return filterIcons(icons, tags, searchString);
|
||||
}
|
||||
|
||||
export default useSearch;
|
||||
1
packages/lucide-figma/src/icons/search-large.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="m18.8744 19.5815c-1.0453.8849-2.3975 1.4185-3.8744 1.4185-3.3137 0-6-2.6863-6-6s2.6863-6 6-6 6 2.6863 6 6c0 1.4769-.5336 2.8291-1.4185 3.8744l4.2721 4.272-.7072.7072zm1.1256-4.5815c0 2.7614-2.2386 5-5 5s-5-2.2386-5-5 2.2386-5 5-5 5 2.2386 5 5z" fill="#000" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 408 B |
1
packages/lucide-figma/src/icons/search.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="none" height="32" viewBox="0 0 32 32" width="32" xmlns="http://www.w3.org/2000/svg"><path clip-rule="evenodd" d="m18.3972 18.6046c-.7793.625-1.7687.9988-2.8455.9988-2.5138 0-4.5517-2.0378-4.5517-4.5517 0-2.5138 2.0379-4.5517 4.5517-4.5517 2.5139 0 4.5517 2.0379 4.5517 4.5517 0 1.0769-.3739 2.0664-.999 2.8458l3.2491 3.2492-.7071.7071zm.7062-3.5529c0 1.9616-1.5901 3.5517-3.5517 3.5517-1.9615 0-3.5517-1.5901-3.5517-3.5517 0-1.9615 1.5902-3.5517 3.5517-3.5517 1.9616 0 3.5517 1.5902 3.5517 3.5517z" fill="#000" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 549 B |
3
packages/lucide-figma/src/interface/interface.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<script type="module" src="./interface.tsx"></script>
|
||||
|
||||
<div id="root"></div>
|
||||
49
packages/lucide-figma/src/interface/interface.scss
Normal file
@@ -0,0 +1,49 @@
|
||||
@font-face {
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('https://rsms.me/inter/font-files/Inter-Regular.woff2?v=3.9')
|
||||
format('woff2'),
|
||||
url('https://rsms.me/inter/font-files/Inter-Regular.woff?v=3.9')
|
||||
format('woff');
|
||||
}
|
||||
|
||||
:root {
|
||||
--color-blue: #18a0fb;
|
||||
--color-black: #333;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Inter, sans-serif;
|
||||
font-size: 11px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
main {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
grid-gap: 8px;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
.footer-link {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
67
packages/lucide-figma/src/interface/interface.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { createReactComponent } from 'lucide-react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
import IconButton from '../components/IconButton'
|
||||
import SearchInput from '../components/SearchInput'
|
||||
import useSearch, { Icon } from '../hooks/useSearch'
|
||||
|
||||
import { getIcons } from '../api/fetchIcons'
|
||||
import './interface.scss'
|
||||
|
||||
function App() {
|
||||
const [query, setQuery] = useState('')
|
||||
const [icons, setIcons] = useState<Icon[]>([])
|
||||
const [tags, setTags] = useState({})
|
||||
const [version, setVersion ] = useState('')
|
||||
|
||||
const searchResults = useMemo(() => useSearch(icons, tags, query), [icons, query])
|
||||
|
||||
const getLatestIcons = async () => {
|
||||
const lucideIcons = await getIcons()
|
||||
|
||||
setIcons(Object.entries(lucideIcons.iconNodes))
|
||||
setTags(lucideIcons.tags)
|
||||
setVersion(lucideIcons.version)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getLatestIcons()
|
||||
}, [])
|
||||
|
||||
if(!icons.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SearchInput
|
||||
value={query}
|
||||
iconCount={icons.length}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
<main>
|
||||
<div className='icon-grid'>
|
||||
{searchResults.map(([name, iconNode] :any) => (
|
||||
<IconButton
|
||||
name={name}
|
||||
key={name}
|
||||
component={createReactComponent(name, iconNode)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<footer>
|
||||
<a
|
||||
href="https://lucide.dev"
|
||||
target="_blank"
|
||||
className='footer-link'
|
||||
>
|
||||
Lucide v{version}
|
||||
</a>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'))
|
||||
@@ -1,9 +1,97 @@
|
||||
figma.showUI(__html__, { width: 300, height: 400 })
|
||||
import type { LucideIcons } from "./api/fetchIcons";
|
||||
import filterIcons from "./helpers/filterIcons";
|
||||
|
||||
figma.ui.onmessage = ({name, svg}) => {
|
||||
const icon = figma.createNodeFromSvg(svg)
|
||||
icon.name = name
|
||||
icon.x = figma.viewport.center.x
|
||||
icon.y = figma.viewport.center.y
|
||||
figma.currentPage.selection = [icon]
|
||||
figma.showUI(__uiFiles__.worker, { visible: false })
|
||||
|
||||
let cachedIcons: LucideIcons
|
||||
|
||||
const setResults = ({result, query, lucideIcons} : { result: SuggestionResults, query: string, lucideIcons: LucideIcons }) => {
|
||||
const icons = Object.entries(lucideIcons.iconNodes);
|
||||
|
||||
const suggestions = filterIcons(icons, lucideIcons.tags, query.toLowerCase()).map(([name]) => ({
|
||||
name,
|
||||
icon: lucideIcons.svgs[name]
|
||||
}))
|
||||
|
||||
result.setSuggestions(suggestions)
|
||||
}
|
||||
|
||||
figma.parameters.on('input', async ({ parameters, key, query, result }) => {
|
||||
if (key === 'icon-name') {
|
||||
console.log('typ tpy', query);
|
||||
cachedIcons = await figma.clientStorage.getAsync(`lucide-icons`)
|
||||
console.log('cachedIcons', cachedIcons);
|
||||
|
||||
if(cachedIcons && cachedIcons.iconNodes && cachedIcons.tags) {
|
||||
setResults({result, query, lucideIcons: cachedIcons})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const drawIcon = ({icon: {name, svg}}: any) => {
|
||||
const min = 0
|
||||
const max = 100
|
||||
const randomPosition = () => Math.floor(Math.random() * (max - min + 1) + min)
|
||||
const icon = figma.createNodeFromSvg(svg)
|
||||
|
||||
icon.name = name
|
||||
icon.x = Math.round(figma.viewport.center.x + randomPosition())
|
||||
icon.y = Math.round(figma.viewport.center.y + randomPosition())
|
||||
figma.currentPage.selection = [icon]
|
||||
|
||||
// lock children
|
||||
icon.children.forEach((vectorNode, key) => {
|
||||
icon.children[key].locked = true
|
||||
});
|
||||
}
|
||||
|
||||
const setCachedIcons = async (pluginMessage: any) => {
|
||||
if(pluginMessage.lucideIcons) {
|
||||
await figma.clientStorage.setAsync(`lucide-icons`, pluginMessage.lucideIcons)
|
||||
}
|
||||
}
|
||||
|
||||
const getCachedIcons = async () => {
|
||||
cachedIcons = await figma.clientStorage.getAsync(`lucide-icons`)
|
||||
|
||||
const response = { type: 'cachedIcons' }
|
||||
|
||||
if(cachedIcons) {
|
||||
Object.assign(response, { cachedIcons })
|
||||
}
|
||||
|
||||
figma.ui.postMessage(response)
|
||||
}
|
||||
|
||||
getCachedIcons()
|
||||
|
||||
figma.ui.onmessage = (event) => {
|
||||
console.log(event, 'main');
|
||||
switch (event.type) {
|
||||
case "drawIcon":
|
||||
drawIcon(event)
|
||||
break;
|
||||
case "getCachedIcons":
|
||||
getCachedIcons()
|
||||
break;
|
||||
|
||||
case "setCachedIcons":
|
||||
setCachedIcons(event)
|
||||
break;
|
||||
|
||||
case "close":
|
||||
figma.closePlugin()
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
figma.on('run', event => {
|
||||
if(event.parameters) {
|
||||
figma.ui.postMessage({ type: 'getSvg', iconName: event.parameters['icon-name'], cachedIcons })
|
||||
} else {
|
||||
figma.showUI(__uiFiles__.interface, { width: 300, height: 400 })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
declare module 'lucide-react';
|
||||
declare module 'lucide';
|
||||
declare module 'lucide/icons';
|
||||
declare module 'lucide/build/icons';
|
||||
@@ -1,10 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('https://rsms.me/inter/font-files/Inter-Regular.woff2?v=3.9')
|
||||
format('woff2'),
|
||||
url('https://rsms.me/inter/font-files/Inter-Regular.woff?v=3.9')
|
||||
format('woff');
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<div id="root"></div>
|
||||
@@ -1,82 +0,0 @@
|
||||
import { Global, jsx } from '@emotion/core'
|
||||
import { version } from '../package.json'
|
||||
import React, { useMemo } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import IconButton from './components/icon-button'
|
||||
import SearchInput from './components/search-input'
|
||||
import theme from './theme'
|
||||
import './ui.css'
|
||||
import tags from '../../../tags.json'
|
||||
import * as iconComponents from 'lucide-react'
|
||||
import { toPascalCase } from './helpers/naming';
|
||||
import useSearch from '../../../site/src/lib/useSearch';
|
||||
|
||||
declare var ICONS: [];
|
||||
|
||||
function App() {
|
||||
const [query, setQuery] = React.useState('')
|
||||
const icons = ICONS.map(name => {
|
||||
const componentName = toPascalCase(name);
|
||||
return {
|
||||
name,
|
||||
tags: tags[name] || [],
|
||||
component: iconComponents[componentName] || null
|
||||
}
|
||||
}).filter(({component}) => !!component)
|
||||
|
||||
const searchResults = useMemo(() => useSearch(icons, query), [icons, query])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Global
|
||||
styles={{ body: { margin: 0, fontFamily: 'Inter, sans-serif' } }}
|
||||
/>
|
||||
<SearchInput
|
||||
value={query}
|
||||
iconCount={icons.length}
|
||||
onChange={event => setQuery(event.target.value)}
|
||||
css={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
borderBottom: '1px solid #e5e5e5',
|
||||
backfaceVisibility: 'hidden'
|
||||
}}
|
||||
/>
|
||||
<div css={{ padding: theme.space[2] }}>
|
||||
<div
|
||||
css={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(6, 1fr)',
|
||||
gridGap: theme.space[1],
|
||||
}}
|
||||
>
|
||||
{searchResults.map(({name, component: Icon} :any) => (
|
||||
<IconButton
|
||||
name={name}
|
||||
key={name}
|
||||
component={Icon}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
marginTop: theme.space[2],
|
||||
padding: theme.space[2],
|
||||
fontSize: theme.fontSizes[0],
|
||||
color: 'rgba(0, 0, 0, 0.5)',
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href="https://lucide.dev"
|
||||
target="_blank"
|
||||
css={{ color: 'inherit' }}
|
||||
>
|
||||
Lucide v{version}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'))
|
||||
1
packages/lucide-figma/src/worker/worker.html
Normal file
@@ -0,0 +1 @@
|
||||
<script type="module" src="./worker.ts"></script>
|
||||
60
packages/lucide-figma/src/worker/worker.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { fetchIcons, LucideIcons } from "../api/fetchIcons"
|
||||
import { createReactComponent } from 'lucide-react'
|
||||
import { renderToString } from 'react-dom/server'
|
||||
import { createElement } from "react"
|
||||
|
||||
const getLatestIcons = async ({ cachedIcons }: any) => {
|
||||
const lucideIcons = await fetchIcons(cachedIcons)
|
||||
|
||||
parent.postMessage({
|
||||
pluginMessage: {
|
||||
type: "latestIcons",
|
||||
lucideIcons,
|
||||
}
|
||||
}, "*")
|
||||
}
|
||||
|
||||
const getSvg = async ({ cachedIcons, iconName }: { cachedIcons: LucideIcons, iconName: string }) => {
|
||||
if (!cachedIcons) {
|
||||
return;
|
||||
}
|
||||
|
||||
const iconNode = cachedIcons.iconNodes[iconName];
|
||||
|
||||
if (iconNode) {
|
||||
const IconComponent = createReactComponent(iconName, iconNode)
|
||||
const svg = renderToString(createElement(IconComponent));
|
||||
|
||||
parent.postMessage({ pluginMessage: {
|
||||
type: 'drawIcon',
|
||||
icon: { name, svg }
|
||||
}}, '*')
|
||||
|
||||
parent.postMessage({ pluginMessage: {
|
||||
type: 'close',
|
||||
}}, '*')
|
||||
}
|
||||
}
|
||||
|
||||
window.onmessage = async (event) => {
|
||||
if (!event?.data?.pluginMessage) {
|
||||
return
|
||||
}
|
||||
|
||||
const { pluginMessage } = event.data
|
||||
|
||||
switch (pluginMessage.type) {
|
||||
case "getLatestIcons":
|
||||
getLatestIcons(pluginMessage)
|
||||
break;
|
||||
|
||||
case "getSvg":
|
||||
getSvg(pluginMessage)
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Hello world!')
|
||||
@@ -1,12 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"lib": ["dom", "esnext"],
|
||||
"jsx": "react",
|
||||
"jsxFactory": "jsx",
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": false,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"../../node_modules/@figma"
|
||||
],
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
40
packages/lucide-figma/vite.config.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { viteSingleFile } from "vite-plugin-singlefile"
|
||||
import { resolve } from 'path'
|
||||
|
||||
const entries = {
|
||||
main: resolve(__dirname, 'src/main.ts'),
|
||||
interface: resolve(__dirname, './src/interface/interface.html'),
|
||||
worker: resolve(__dirname, './src/worker/worker.html'),
|
||||
}
|
||||
|
||||
const input = {};
|
||||
|
||||
if(process.env['INPUT']) {
|
||||
const entry = process.env['INPUT'];
|
||||
input[entry] = entries[entry]
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), viteSingleFile()],
|
||||
build: {
|
||||
target: "esnext",
|
||||
assetsInlineLimit: 100000000,
|
||||
chunkSizeWarningLimit: 100000000,
|
||||
cssCodeSplit: false,
|
||||
brotliSize: false,
|
||||
emptyOutDir: false,
|
||||
rollupOptions: {
|
||||
input,
|
||||
inlineDynamicImports: true,
|
||||
output: {
|
||||
manualChunks: (chunk) => "all.js",
|
||||
entryFileNames: `assets/[name].js`,
|
||||
chunkFileNames: `assets/[name].js`,
|
||||
assetFileNames: `assets/[name].[ext]`
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,48 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = (env, argv) => ({
|
||||
// This is necessary because Figma's 'eval' works differently than normal eval
|
||||
devtool: argv.mode === 'production' ? false : 'inline-source-map',
|
||||
entry: {
|
||||
ui: './src/ui.tsx',
|
||||
main: './src/main.ts',
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, 'build'),
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
ICONS: JSON.stringify(
|
||||
fs.readdirSync(path.join(process.cwd(), '../../icons')).map(name => name.split('.')[0]),
|
||||
),
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './src/ui.html',
|
||||
filename: 'ui.html',
|
||||
inlineSource: '.(js)$',
|
||||
chunks: ['ui'],
|
||||
}),
|
||||
new HtmlWebpackInlineSourcePlugin(),
|
||||
],
|
||||
});
|
||||
@@ -1,10 +1,16 @@
|
||||
import path from "path";
|
||||
import {appendFile, readSvgDirectory, resetFile, toPascalCase, writeFile} from "../../../scripts/helpers";
|
||||
import path from 'path';
|
||||
import {
|
||||
appendFile,
|
||||
readSvgDirectory,
|
||||
resetFile,
|
||||
toPascalCase,
|
||||
writeFile,
|
||||
} from '../../../scripts/helpers';
|
||||
|
||||
const srcDirectory=path.join(__dirname, "../dist");
|
||||
const srcDirectory = path.join(__dirname, '../dist');
|
||||
|
||||
// Declare type definitions
|
||||
const typeDefinitions=`\
|
||||
const typeDefinitions = `\
|
||||
/// <reference types="react" />
|
||||
import { SVGAttributes } from 'react'
|
||||
|
||||
@@ -15,6 +21,8 @@ export interface LucideProps extends Partial<React.SVGProps<SVGSVGElement>> {
|
||||
size?: string | number
|
||||
}
|
||||
|
||||
export declare const createReactComponent: (iconName: string, iconNode: any[]) => (props: LucideProps) => JSX.Element;
|
||||
|
||||
export type Icon = React.FC<LucideProps>;
|
||||
|
||||
// Generated icons
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './icons';
|
||||
export { default as createReactComponent } from './createReactComponent';
|
||||
|
||||
@@ -10,9 +10,11 @@ const TYPES_FILE_NAME = 'lucide.d.ts';
|
||||
// Generates header of d.ts file include some types and functions
|
||||
const typeDefinitions = `\
|
||||
declare module 'lucide'
|
||||
|
||||
export interface SVGProps extends Partial<SVGElement> ${JSON.stringify(defaultAttributes, null, 2)}
|
||||
|
||||
export declare type IconNode = readonly [tag: string, attrs:SVGProps, children?:IconNode];
|
||||
export declare type IconNodeChild = readonly [string, object];
|
||||
export declare type IconNode = readonly [tag: string, attrs: SVGProps, children?: IconNodeChild[]];
|
||||
export declare type CustomAttrs = { [attr:string]: any }
|
||||
export type Icons = { [key: string]: IconNode }
|
||||
|
||||
|
||||
@@ -2074,6 +2074,12 @@
|
||||
"air",
|
||||
"blow"
|
||||
],
|
||||
"wrap-text": [
|
||||
"words",
|
||||
"lines",
|
||||
"break",
|
||||
"paragraph"
|
||||
],
|
||||
"wrench": [
|
||||
"tool",
|
||||
"settings",
|
||||
|
||||