mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-11-10 13:18:25 +00:00
Compare commits
97
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a92b9ba26 | ||
|
|
66d8f9fc39 | ||
|
|
e042fec3b5 | ||
|
|
c870b3c54a | ||
|
|
f53e5bfff0 | ||
|
|
f06ac67e33 | ||
|
|
b0b1a4f8ab | ||
|
|
3a37bfa5cd | ||
|
|
16fb55f52c | ||
|
|
951813ce76 | ||
|
|
ba6751ac45 | ||
|
|
99c1037550 | ||
|
|
e32a4a2c10 | ||
|
|
5d9cc1580a | ||
|
|
d9f72b2525 | ||
|
|
0d0408c43b | ||
|
|
55d30bda04 | ||
|
|
f90f10a24a | ||
|
|
f3e36fb0d7 | ||
|
|
4faa47c0b0 | ||
|
|
e593a44ad2 | ||
|
|
dba8fb7932 | ||
|
|
a87cecebe3 | ||
|
|
3b9ea6d087 | ||
|
|
076b52527f | ||
|
|
58ef6830c6 | ||
|
|
f032bed22e | ||
|
|
266fd1c717 | ||
|
|
357a50175e | ||
|
|
a50c54cfff | ||
|
|
bd95db1af8 | ||
|
|
6b474e16d4 | ||
|
|
566032a95d | ||
|
|
042bf77119 | ||
|
|
5b8975c9a3 | ||
|
|
7eb8afe2f8 | ||
|
|
08b2fd2455 | ||
|
|
a79b2d131d | ||
|
|
b998e2892b | ||
|
|
5da3ee357d | ||
|
|
6bbe5ddb07 | ||
|
|
4a26fa58e0 | ||
|
|
5d592a96aa | ||
|
|
bdf795a8e3 | ||
|
|
e4be16acd5 | ||
|
|
ea1242d384 | ||
|
|
7cf805a408 | ||
|
|
c2c28580b3 | ||
|
|
858b662775 | ||
|
|
c17f928577 | ||
|
|
d32ef89ef9 | ||
|
|
5fb5967884 | ||
|
|
1cb69f6f9c | ||
|
|
6d4501f4d3 | ||
|
|
ba642ebf42 | ||
|
|
2f7d975022 | ||
|
|
b115c04286 | ||
|
|
5d0f64034b | ||
|
|
8a225b9fa7 | ||
|
|
95989f4535 | ||
|
|
073ffe0448 | ||
|
|
30f2eda422 | ||
|
|
a68cc8d52f | ||
|
|
249fe3a120 | ||
|
|
07ed920096 | ||
|
|
cef2020c7a | ||
|
|
8970c2c4f9 | ||
|
|
26993aba5a | ||
|
|
a53bd66a03 | ||
|
|
3bcf71f7cf | ||
|
|
9e5f0607cd | ||
|
|
75955ec47b | ||
|
|
58fd3ccd59 | ||
|
|
22e6094bb5 | ||
|
|
ba95e4c988 | ||
|
|
e8db514793 | ||
|
|
ab000be04f | ||
|
|
a537cb6eb3 | ||
|
|
3859eb20fa | ||
|
|
2bfb9bb1ba | ||
|
|
94e4cb9d9d | ||
|
|
0196353e5c | ||
|
|
99d25bdee2 | ||
|
|
07a2c792ef | ||
|
|
93a1d72465 | ||
|
|
83ba4986c8 | ||
|
|
65271a11e7 | ||
|
|
90567c4ce0 | ||
|
|
4dc5b7ebae | ||
|
|
a87bc9b15b | ||
|
|
f96d09cf3a | ||
|
|
713b1fcd25 | ||
|
|
bca7e75a81 | ||
|
|
7f30808e30 | ||
|
|
6f56b8f1f6 | ||
|
|
b1a94838ac | ||
|
|
ecccc7b963 |
@@ -10,3 +10,4 @@ touchpad
|
||||
ungroup
|
||||
toc
|
||||
sunlounger
|
||||
monstera
|
||||
|
||||
@@ -57,6 +57,16 @@ When a request covers several icons at once, assign each use-case only to the ic
|
||||
|
||||
Related icons should get **variant-specific** wording, not one shared blurb. `battery-low` → "Indicating a low battery charge level"; `battery-full` → "…full…". `square-arrow-right-enter` → sign in / join; `-exit` → sign out / export. Resist copying the same line across a family.
|
||||
|
||||
### Deriving from source material
|
||||
|
||||
When mining descriptions, comments, or linked issues:
|
||||
|
||||
- Prefer the **stated purpose** over the backstory. Extract the function, discard the anecdote.
|
||||
- If a source lists genuinely distinct uses, keep them as separate entries; if it repeats one idea five ways, collapse to one.
|
||||
- Merge an intro line with its bullets ("To represent:" + "pottery" → "Representing pottery").
|
||||
- If the only signal is noise (a name restatement, a link, "thanks!"), leave the icon without a use-case rather than inventing one.
|
||||
- Never fabricate a use-case that isn't supported by the icon's meaning or the source.
|
||||
|
||||
## Examples
|
||||
|
||||
Good:
|
||||
|
||||
@@ -0,0 +1,587 @@
|
||||
---
|
||||
applyTo: "docs/*.md"
|
||||
---
|
||||
# UX writing guidelines
|
||||
|
||||
Follow these guidelines when writing or editing Lucide documentation, especially Markdown files.
|
||||
|
||||
Write for people who are trying to understand a concept, follow a specification, or complete a task. Prioritize clarity, precision, and scannability over personality or decorative prose.
|
||||
|
||||
## Be direct and concise
|
||||
|
||||
State the important information first. Remove words that do not add meaning.
|
||||
|
||||
Prefer short sentences and paragraphs, but do not sacrifice precision just to make text shorter.
|
||||
|
||||
**Do:**
|
||||
|
||||
> Icons must use a 24 × 24 pixel canvas.
|
||||
|
||||
**Don't:**
|
||||
|
||||
> When creating a new icon, you should make sure that the canvas you're working with has dimensions of 24 × 24 pixels.
|
||||
|
||||
Avoid unnecessary introductions such as:
|
||||
|
||||
- "It's important to note that…"
|
||||
- "Keep in mind that…"
|
||||
- "As you can see…"
|
||||
- "In order to…"
|
||||
- "It should be noted that…"
|
||||
|
||||
State the information directly instead.
|
||||
|
||||
## Put the rule before the explanation
|
||||
|
||||
When documenting a requirement or recommendation, state it before explaining why it exists.
|
||||
|
||||
**Do:**
|
||||
|
||||
> Keep at least 1 pixel of space between unrelated elements. This prevents shapes from visually merging at small sizes.
|
||||
|
||||
**Don't:**
|
||||
|
||||
> Because shapes can visually merge at small sizes, it's important to make sure that unrelated elements have enough space between them.
|
||||
|
||||
This makes documentation easier to scan and makes normative requirements easier to identify.
|
||||
|
||||
## Explain the reason when it adds value
|
||||
|
||||
Explain *why* a rule exists when the reasoning helps contributors apply it to situations that are not explicitly documented.
|
||||
|
||||
Do not merely restate the rule.
|
||||
|
||||
**Do:**
|
||||
|
||||
> Avoid simulated fills. They don't scale consistently with the rest of the icon set.
|
||||
|
||||
**Don't:**
|
||||
|
||||
> Avoid simulated fills because simulated fills should not be used.
|
||||
|
||||
Reasoning is particularly useful for design rules where contributors may need to evaluate unusual cases.
|
||||
|
||||
## Use scannable structure
|
||||
|
||||
Assume readers will scan the page before reading it in detail.
|
||||
|
||||
Use:
|
||||
|
||||
- descriptive headings
|
||||
- short paragraphs
|
||||
- lists for sets of related items
|
||||
- examples for rules that benefit from demonstration
|
||||
- **bold** for important terms or short emphasis
|
||||
- code formatting for filenames, properties, commands, values, and identifiers
|
||||
|
||||
Avoid long uninterrupted blocks of text.
|
||||
|
||||
Headings should communicate the subject or rule without requiring the surrounding text for context.
|
||||
|
||||
**Do:**
|
||||
|
||||
> ## Keep 1 pixel of safe space
|
||||
|
||||
**Don't:**
|
||||
|
||||
> ## Spacing
|
||||
|
||||
## Use sentence case
|
||||
|
||||
Use sentence case for:
|
||||
|
||||
- page titles
|
||||
- headings
|
||||
- table headings
|
||||
- labels
|
||||
- example captions
|
||||
|
||||
Capitalize proper nouns and branded terms normally.
|
||||
|
||||
**Do:**
|
||||
|
||||
> ## Use consistent corner radii
|
||||
|
||||
**Don't:**
|
||||
|
||||
> ## Use Consistent Corner Radii
|
||||
|
||||
## Address the reader directly
|
||||
|
||||
Use **you** and **your** when referring to something the reader does.
|
||||
|
||||
**Do:**
|
||||
|
||||
> If your icon contains overlapping shapes, simplify the hidden geometry.
|
||||
|
||||
Avoid referring to the reader as "the user," "the contributor," or other third-person terms when direct language is more natural.
|
||||
|
||||
However, prefer an imperative when the subject is obvious.
|
||||
|
||||
**Prefer:**
|
||||
|
||||
> Keep related elements visually grouped.
|
||||
|
||||
**Over:**
|
||||
|
||||
> You should keep related elements visually grouped.
|
||||
|
||||
## Use "we" only when it means the Lucide project or maintainers
|
||||
|
||||
First-person plural is acceptable when describing an intentional project decision, convention, or policy.
|
||||
|
||||
**Do:**
|
||||
|
||||
> We use `kebab-case` for icon names.
|
||||
|
||||
> We generally avoid adding brand logos to Lucide.
|
||||
|
||||
Do not use "we" merely to make documentation conversational.
|
||||
|
||||
**Prefer:**
|
||||
|
||||
> Icons use a 2 pixel stroke.
|
||||
|
||||
**Over:**
|
||||
|
||||
> We use a 2 pixel stroke for our icons.
|
||||
|
||||
## Use normative language consistently
|
||||
|
||||
Use specific language to communicate how strictly a rule applies.
|
||||
|
||||
- **must** — required; violating this is not acceptable
|
||||
- **must not** — prohibited
|
||||
- **should** — recommended; exceptions may be appropriate
|
||||
- **should not** — discouraged; exceptions may be appropriate
|
||||
- **can** — possible or permitted
|
||||
- **may** — optional or dependent on context
|
||||
|
||||
Do not use **should** when something is actually required.
|
||||
|
||||
**Do:**
|
||||
|
||||
> Icons must use a 24 × 24 pixel canvas.
|
||||
|
||||
**Don't:**
|
||||
|
||||
> Icons should use a 24 × 24 pixel canvas.
|
||||
|
||||
Avoid vague substitutes such as "ideally," "normally," or "preferably" when a more precise normative term applies.
|
||||
|
||||
## Prefer active voice
|
||||
|
||||
Use active voice unless passive voice makes the subject clearer or avoids unnecessary repetition.
|
||||
|
||||
**Do:**
|
||||
|
||||
> Align nodes to the pixel grid.
|
||||
|
||||
**Don't:**
|
||||
|
||||
> Nodes should be aligned to the pixel grid.
|
||||
|
||||
Passive voice is acceptable when the actor is irrelevant.
|
||||
|
||||
> Deprecated icons are removed in major releases.
|
||||
|
||||
## Use positive instructions when possible
|
||||
|
||||
Tell readers what to do rather than only what not to do.
|
||||
|
||||
**Do:**
|
||||
|
||||
> Use a single continuous path when the elements form one continuous shape.
|
||||
|
||||
**Less useful:**
|
||||
|
||||
> Don't split continuous shapes into unnecessary paths.
|
||||
|
||||
Use negative instructions when the prohibition itself is important.
|
||||
|
||||
> Don't use simulated fills.
|
||||
|
||||
## Be precise about consequences
|
||||
|
||||
When explaining why something should or should not be done, describe the actual consequence rather than using vague warnings.
|
||||
|
||||
**Do:**
|
||||
|
||||
> Keep unrelated elements at least 2 pixels apart. Smaller gaps can disappear when the icon is rendered at small sizes.
|
||||
|
||||
**Don't:**
|
||||
|
||||
> Be careful when placing elements too close together.
|
||||
|
||||
Avoid language that exaggerates consequences or attempts to influence a contributor's decision emotionally.
|
||||
|
||||
## Prefer concrete language
|
||||
|
||||
Use specific terms instead of vague references.
|
||||
|
||||
**Do:**
|
||||
|
||||
> The two endpoints must remain inside the 1 pixel safe zone.
|
||||
|
||||
**Don't:**
|
||||
|
||||
> These should stay within the appropriate area.
|
||||
|
||||
Avoid ambiguous references such as:
|
||||
|
||||
- this
|
||||
- that
|
||||
- it
|
||||
- things
|
||||
- stuff
|
||||
- appropriate
|
||||
- proper
|
||||
- correct
|
||||
|
||||
when the intended subject can be named directly.
|
||||
|
||||
## Introduce terminology before relying on it
|
||||
|
||||
Define Lucide-specific or technical terms when readers may not already know them.
|
||||
|
||||
Once defined, use the same term consistently.
|
||||
|
||||
**Do:**
|
||||
|
||||
> The **safe zone** is the 1 pixel area between the icon artwork and the canvas boundary.
|
||||
|
||||
Do not alternate between different terms for the same concept unless the distinction is intentional.
|
||||
|
||||
For example, don't alternate between "safe zone," "padding," and "margin" if they refer to the same rule.
|
||||
|
||||
## Use abbreviations sparingly
|
||||
|
||||
Spell out uncommon abbreviations on first use.
|
||||
|
||||
> Scalable Vector Graphics (SVG)
|
||||
|
||||
Common technical abbreviations such as HTML, CSS, SVG, API, and URL do not need to be expanded when the intended audience can reasonably be expected to know them.
|
||||
|
||||
Avoid Latin abbreviations such as:
|
||||
|
||||
- e.g.
|
||||
- i.e.
|
||||
- etc.
|
||||
|
||||
Prefer:
|
||||
|
||||
- for example
|
||||
- that is
|
||||
- and so on
|
||||
|
||||
## Use examples deliberately
|
||||
|
||||
Use examples when they clarify how to apply a rule.
|
||||
|
||||
Keep examples focused on the rule being demonstrated. Avoid introducing unrelated differences that make the comparison harder to understand.
|
||||
|
||||
When contrasting examples, use **Do** and **Don't** consistently.
|
||||
|
||||
**Do:**
|
||||
|
||||
> Use one path for a continuous outline.
|
||||
|
||||
**Don't:**
|
||||
|
||||
> Split a continuous outline into several overlapping paths.
|
||||
|
||||
The explanatory text should describe the principle, not merely narrate what is visible in the example.
|
||||
|
||||
## Use links descriptively
|
||||
|
||||
Link text should describe the destination or concept.
|
||||
|
||||
**Do:**
|
||||
|
||||
> See the [Icon Design Specification](./specification) for exact dimensions and spacing requirements.
|
||||
|
||||
**Don't:**
|
||||
|
||||
> For more information, [click here](./specification).
|
||||
|
||||
Avoid generic link text such as:
|
||||
|
||||
- click here
|
||||
- here
|
||||
- this page
|
||||
- learn more
|
||||
|
||||
when a descriptive label is possible.
|
||||
|
||||
## Avoid unnecessary repetition
|
||||
|
||||
Do not repeat information already established by a heading, example, or immediately preceding sentence.
|
||||
|
||||
**Prefer:**
|
||||
|
||||
> ## Keep 1 pixel of safe space
|
||||
>
|
||||
> Keep all artwork within the 22 × 22 pixel live area.
|
||||
|
||||
**Over:**
|
||||
|
||||
> ## Keep 1 pixel of safe space
|
||||
>
|
||||
> When designing an icon, you should keep 1 pixel of safe space around the icon.
|
||||
|
||||
Cross-reference another section instead of duplicating detailed rules that already have a canonical explanation.
|
||||
|
||||
## Use contractions in prose
|
||||
|
||||
Use natural contractions such as:
|
||||
|
||||
- don't
|
||||
- can't
|
||||
- isn't
|
||||
- it's
|
||||
- you'll
|
||||
|
||||
They make explanatory prose less formal and easier to scan.
|
||||
|
||||
Use uncontracted forms when deliberate emphasis is useful.
|
||||
|
||||
> Do not change existing icon geometry solely to reduce the number of SVG nodes.
|
||||
|
||||
Normative statements using **must not** should not be contracted.
|
||||
|
||||
## Use the serial comma
|
||||
|
||||
Use the serial (Oxford) comma in lists of three or more items.
|
||||
|
||||
**Do:**
|
||||
|
||||
> Check the icon's size, stroke width, and spacing.
|
||||
|
||||
**Don't:**
|
||||
|
||||
> Check the icon's size, stroke width and spacing.
|
||||
|
||||
## Use punctuation normally in prose
|
||||
|
||||
Use periods for complete sentences.
|
||||
|
||||
Periods may be omitted from:
|
||||
|
||||
- headings
|
||||
- short labels
|
||||
- short list items that are not complete sentences
|
||||
- captions consisting only of a short phrase
|
||||
|
||||
Be consistent within a list.
|
||||
|
||||
If list items are complete sentences, end them with periods. If they are fragments, generally omit periods.
|
||||
|
||||
## Skip colons in headings
|
||||
|
||||
Do not end headings with colons.
|
||||
|
||||
**Do:**
|
||||
|
||||
> ## Naming conventions
|
||||
|
||||
**Don't:**
|
||||
|
||||
> ## Naming conventions:
|
||||
|
||||
Use a colon normally when introducing a list or explanation in body text.
|
||||
|
||||
## Use exclamation points sparingly
|
||||
|
||||
Avoid exclamation points in technical documentation unless the context genuinely calls for excitement or celebration.
|
||||
|
||||
**Prefer:**
|
||||
|
||||
> Your first icon is ready.
|
||||
|
||||
**Over:**
|
||||
|
||||
> Your first icon is ready!
|
||||
|
||||
Do not use exclamation points to make instructions sound friendlier.
|
||||
|
||||
## Use ellipses only when they have meaning
|
||||
|
||||
Use an ellipsis (`…`) to represent omitted content or an intentionally incomplete statement.
|
||||
|
||||
Do not use ellipses decoratively or to make prose sound conversational.
|
||||
|
||||
When documenting literal UI text, preserve the punctuation used by the interface.
|
||||
|
||||
## Use parentheses for definitions and compact clarification
|
||||
|
||||
Use parentheses when they make a definition, abbreviation, or short clarification easier to understand.
|
||||
|
||||
> Scalable Vector Graphics (SVG)
|
||||
|
||||
Avoid using parentheses for long side notes. Rewrite important information as part of the sentence or as a separate sentence.
|
||||
|
||||
## Prefer "and" over ampersands
|
||||
|
||||
Use **and** in normal prose.
|
||||
|
||||
**Do:**
|
||||
|
||||
> Naming and metadata
|
||||
|
||||
**Avoid:**
|
||||
|
||||
> Naming & metadata
|
||||
|
||||
Use an ampersand only when it is part of an official name, literal UI text, code, or another established term.
|
||||
|
||||
## Avoid em dashes
|
||||
|
||||
Prefer commas, periods, colons, or parentheses when they produce a simpler sentence.
|
||||
|
||||
**Prefer:**
|
||||
|
||||
> This rule has one exception: existing legacy icons.
|
||||
|
||||
**Avoid:**
|
||||
|
||||
> This rule has one exception — existing legacy icons.
|
||||
|
||||
Use an en dash (`–`) without surrounding spaces for ranges.
|
||||
|
||||
> 1–3 pixels
|
||||
|
||||
> A–Z
|
||||
|
||||
Do not substitute a hyphen for an en dash in prose ranges unless required by a technical syntax.
|
||||
|
||||
## Use hyphens correctly
|
||||
|
||||
Hyphenate compound modifiers when necessary for clarity.
|
||||
|
||||
> 24 × 24 pixel canvas
|
||||
|
||||
> 2-pixel-wide gap
|
||||
|
||||
> pixel-aligned element
|
||||
|
||||
Do not hyphenate a phrase when it functions as a verb.
|
||||
|
||||
> Sign off on the design.
|
||||
|
||||
> Complete the sign-off.
|
||||
|
||||
Do not add hyphens mechanically when the meaning is already clear.
|
||||
|
||||
## Use code formatting consistently
|
||||
|
||||
Use backticks for literal technical values and identifiers, including:
|
||||
|
||||
- filenames and paths
|
||||
- commands
|
||||
- package names
|
||||
- properties and attributes
|
||||
- icon names
|
||||
- code values
|
||||
- SVG elements and attributes
|
||||
|
||||
**Do:**
|
||||
|
||||
> Add `camera-off` as an alias.
|
||||
|
||||
> Set `stroke-width` to `2`.
|
||||
|
||||
> Edit `icons/camera.svg`.
|
||||
|
||||
Do not use code formatting merely for emphasis.
|
||||
|
||||
## Use bold for semantic emphasis
|
||||
|
||||
Use **bold** sparingly to highlight a term or distinction that helps readers scan the page.
|
||||
|
||||
Do not bold entire sentences or paragraphs.
|
||||
|
||||
Prefer bold over italics for general emphasis.
|
||||
|
||||
Use *italics* only when conventional, such as when referring to a term as a term or when a specific distinction benefits from it.
|
||||
|
||||
## Don't use all caps for emphasis
|
||||
|
||||
Do not use blocks of uppercase text for emphasis.
|
||||
|
||||
**Don't:**
|
||||
|
||||
> NEVER place nodes outside the canvas.
|
||||
|
||||
**Do:**
|
||||
|
||||
> **Never** place nodes outside the canvas.
|
||||
|
||||
Preserve capitalization when documenting literal identifiers or established technical terms.
|
||||
|
||||
## Keep lists parallel
|
||||
|
||||
Items in the same list should use the same grammatical structure.
|
||||
|
||||
**Do:**
|
||||
|
||||
- Use a 24 × 24 pixel canvas
|
||||
- Keep 1 pixel of safe space
|
||||
- Use a 2 pixel stroke
|
||||
- Align nodes to the pixel grid
|
||||
|
||||
**Don't:**
|
||||
|
||||
- Use a 24 × 24 pixel canvas
|
||||
- Safe space should be 1 pixel
|
||||
- A 2 pixel stroke
|
||||
- Nodes are aligned to the pixel grid
|
||||
|
||||
Parallel structure makes rules easier to scan and compare.
|
||||
|
||||
## Avoid unnecessary filler and enthusiasm
|
||||
|
||||
Lucide documentation should sound helpful and confident, not promotional.
|
||||
|
||||
Avoid filler such as:
|
||||
|
||||
- "Simply…"
|
||||
- "Just…"
|
||||
- "Obviously…"
|
||||
- "Of course…"
|
||||
- "Easy…"
|
||||
- "Luckily…"
|
||||
- "Don't worry…"
|
||||
|
||||
These words can be inaccurate or dismissive when the task is not simple for the reader.
|
||||
|
||||
Also avoid marketing language such as:
|
||||
|
||||
- "powerful"
|
||||
- "seamless"
|
||||
- "effortless"
|
||||
- "amazing"
|
||||
- "best-in-class"
|
||||
|
||||
unless the word communicates a specific, verifiable distinction.
|
||||
|
||||
## Prefer timeless documentation
|
||||
|
||||
Avoid wording tied unnecessarily to the current state of development.
|
||||
|
||||
**Prefer:**
|
||||
|
||||
> Lucide supports custom icon aliases.
|
||||
|
||||
**Avoid:**
|
||||
|
||||
> Lucide now supports custom icon aliases.
|
||||
|
||||
Use time-sensitive language only when the timing itself matters, such as migration notes, deprecations, or release documentation.
|
||||
|
||||
## Preserve established Lucide terminology
|
||||
|
||||
When editing existing documentation, preserve established Lucide terminology and naming conventions unless there is a clear reason to change them.
|
||||
|
||||
Do not invent synonyms merely to avoid repetition. Consistency is more important than stylistic variety in technical documentation.
|
||||
|
||||
When terminology differs between the Lucide codebase, API, UI, and documentation, prefer the term used by the relevant canonical specification.
|
||||
@@ -30,6 +30,11 @@
|
||||
- any-glob-to-any-file:
|
||||
- 'icons/*.svg'
|
||||
|
||||
🔬 Lab icon:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'lab/*.svg'
|
||||
|
||||
# For changes in the lucide package
|
||||
🧳 lucide package:
|
||||
- changed-files:
|
||||
|
||||
@@ -64,6 +64,7 @@ jobs:
|
||||
contents: write # Required to create the release and tag
|
||||
outputs:
|
||||
VERSION: ${{ steps.new-version.outputs.NEW_VERSION }}
|
||||
RELEASE_URL: ${{ steps.create-release.outputs.url }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
@@ -102,6 +103,7 @@ jobs:
|
||||
echo '${{ steps.change-log.outputs.CHANGE_LOG }}'
|
||||
|
||||
- name: Create Release
|
||||
id: create-release
|
||||
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||||
with:
|
||||
tag_name: ${{ steps.new-version.outputs.NEW_VERSION }}
|
||||
@@ -129,3 +131,30 @@ jobs:
|
||||
with:
|
||||
version: ${{ inputs.version }}
|
||||
tag: ${{ inputs.tag }}
|
||||
|
||||
post-release:
|
||||
name: Trigger post-release automation
|
||||
if: >-
|
||||
always() &&
|
||||
github.repository == 'lucide-icons/lucide' &&
|
||||
(needs.start-release.result == 'success' || needs.dispatch-release.result == 'success')
|
||||
needs: [create-release, start-release, dispatch-release]
|
||||
runs-on: ubuntu-latest
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Dispatch post_release event
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.POST_RELEASE_TOKEN }}
|
||||
RELEASE_TAG: ${{ needs.create-release.outputs.VERSION || inputs.version }}
|
||||
RELEASE_URL: ${{ needs.create-release.outputs.RELEASE_URL }}
|
||||
run: |
|
||||
if [ -z "$RELEASE_URL" ]; then
|
||||
RELEASE_URL="https://github.com/lucide-icons/lucide/releases/tag/$RELEASE_TAG"
|
||||
fi
|
||||
|
||||
gh api \
|
||||
--method POST \
|
||||
repos/lucide-icons/post-release/dispatches \
|
||||
--field event_type=post_release \
|
||||
--field "client_payload[release_tag]=$RELEASE_TAG" \
|
||||
--field "client_payload[release_url]=$RELEASE_URL"
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Docs Checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- docs/**
|
||||
- .github/workflows/docs.yml
|
||||
- package.json
|
||||
- pnpm-workspace.yaml
|
||||
- pnpm-lock.yaml
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
# Release metadata is generated by diffing every release tag
|
||||
fetch-depth: 0
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build docs
|
||||
run: pnpm --filter @lucide/docs build
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
# Release metadata is generated by diffing every release tag
|
||||
fetch-depth: 0
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Generate docs data
|
||||
run: pnpm --filter @lucide/docs run "/^prebuild:.*/"
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter @lucide/docs typecheck
|
||||
@@ -46,3 +46,19 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter @lucide/astro test
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: 'package.json'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter @lucide/astro typecheck
|
||||
|
||||
@@ -37,3 +37,19 @@ jobs:
|
||||
with:
|
||||
name: lucide-font
|
||||
path: lucide-font
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter build-font typecheck
|
||||
|
||||
@@ -3,6 +3,7 @@ name: Lucide Lab checks
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- lab/**
|
||||
- packages/lab/**
|
||||
- packages/shared/**
|
||||
- tools/build-icons/**
|
||||
@@ -28,3 +29,19 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter @lucide/lab build
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter @lucide/lab typecheck
|
||||
|
||||
@@ -31,3 +31,19 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-preact test
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter lucide-preact typecheck
|
||||
|
||||
@@ -47,3 +47,19 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-react-native test
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter lucide-react-native typecheck
|
||||
|
||||
@@ -7,10 +7,13 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- packages/lucide-react/**
|
||||
- integrations/lucide-react/**
|
||||
- packages/shared/**
|
||||
- tools/build-icons/**
|
||||
- tools/rollup-plugins/**
|
||||
- scripts/generateNextJSAliases.mjs
|
||||
- package.json
|
||||
- pnpm-workspace.yaml
|
||||
- pnpm-lock.yaml
|
||||
|
||||
permissions:
|
||||
@@ -48,3 +51,38 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-react test
|
||||
|
||||
integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Chromium
|
||||
run: pnpm --filter @lucide/integration-react-vite exec playwright install --with-deps chromium
|
||||
|
||||
- name: Run integration checks
|
||||
run: pnpm test:integrations:react
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter lucide-react typecheck
|
||||
|
||||
@@ -27,4 +27,20 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-react test
|
||||
run: pnpm --filter @lucide/shared test
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter @lucide/shared typecheck
|
||||
|
||||
@@ -47,3 +47,19 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide-solid test
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter lucide-solid typecheck
|
||||
|
||||
@@ -26,3 +26,19 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter lucide-static build
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter lucide-static typecheck
|
||||
|
||||
@@ -61,5 +61,5 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Test
|
||||
- name: Typecheck
|
||||
run: pnpm --filter @lucide/svelte typecheck
|
||||
|
||||
@@ -47,3 +47,19 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter @lucide/vue test
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter @lucide/vue typecheck
|
||||
|
||||
@@ -46,3 +46,19 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: pnpm --filter lucide test
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm --filter lucide typecheck
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
name: Release @lucide/lab
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- lab/**/*.svg
|
||||
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release-type:
|
||||
description: Release type
|
||||
required: true
|
||||
default: 'minor'
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
if: github.repository == 'lucide-icons/lucide'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # Required to create the release and tag
|
||||
outputs:
|
||||
VERSION: ${{ steps.new-version.outputs.NEW_VERSION }}
|
||||
SHOULD_RELEASE: ${{ steps.release-notes.outputs.SHOULD_RELEASE }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0 # Required to diff the lab icons against the previous tag
|
||||
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Get latest tag
|
||||
id: latest-tag
|
||||
run: |
|
||||
LATEST_TAG=$(git tag --list '@lucide/lab@*' --sort=-v:refname | head -n 1)
|
||||
|
||||
if [ -z "$LATEST_TAG" ]; then
|
||||
echo "::error::No @lucide/lab@* tag found to release from."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Latest tag: $LATEST_TAG"
|
||||
echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create new version
|
||||
id: new-version
|
||||
run: |
|
||||
NEW_VERSION=$(pnpm semver "${LATEST_TAG#@lucide/lab@}" -i "$RELEASE_TYPE")
|
||||
|
||||
echo "New version: $NEW_VERSION"
|
||||
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
LATEST_TAG: ${{ steps.latest-tag.outputs.LATEST_TAG }}
|
||||
RELEASE_TYPE: ${{ inputs.release-type || 'minor' }}
|
||||
|
||||
- name: Generate release notes
|
||||
id: release-notes
|
||||
run: |
|
||||
NEW_TAG="@lucide/lab@$NEW_VERSION"
|
||||
|
||||
ADDED=$(git diff --name-only --diff-filter=A "$LATEST_TAG" HEAD -- 'lab/*.svg' \
|
||||
| xargs -r -n1 basename | sed 's/\.svg$//' | sort)
|
||||
REMOVED=$(git diff --name-only --diff-filter=D "$LATEST_TAG" HEAD -- 'lab/*.svg' \
|
||||
| xargs -r -n1 basename | sed 's/\.svg$//' | sort)
|
||||
|
||||
if [ -z "$ADDED" ] && [ "$EVENT_NAME" != 'workflow_dispatch' ]; then
|
||||
echo "::notice::No icons were added to ./lab since $LATEST_TAG, skipping release."
|
||||
echo "SHOULD_RELEASE=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
{
|
||||
if [ -n "$ADDED" ]; then
|
||||
printf '## New icons\n\n'
|
||||
printf '%s\n' "$ADDED" | sed 's|.*|- `&`|'
|
||||
printf '\n'
|
||||
fi
|
||||
|
||||
if [ -n "$REMOVED" ]; then
|
||||
printf '## Removed icons\n\nThese graduated to the core icon set.\n\n'
|
||||
printf '%s\n' "$REMOVED" | sed 's|.*|- `&`|'
|
||||
printf '\n'
|
||||
fi
|
||||
|
||||
printf '**Full changelog**: https://github.com/lucide-icons/lucide/compare/%s...%s\n' "$LATEST_TAG" "$NEW_TAG"
|
||||
} > lab-release-notes.md
|
||||
|
||||
cat lab-release-notes.md
|
||||
echo "SHOULD_RELEASE=true" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
LATEST_TAG: ${{ steps.latest-tag.outputs.LATEST_TAG }}
|
||||
NEW_VERSION: ${{ steps.new-version.outputs.NEW_VERSION }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
|
||||
- name: Create Release
|
||||
if: steps.release-notes.outputs.SHOULD_RELEASE == 'true'
|
||||
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||||
with:
|
||||
tag_name: '@lucide/lab@${{ steps.new-version.outputs.NEW_VERSION }}'
|
||||
name: '@lucide/lab@${{ steps.new-version.outputs.NEW_VERSION }}'
|
||||
body_path: lab-release-notes.md
|
||||
make_latest: 'false' # Never displace the core icon release as "Latest"
|
||||
|
||||
publish:
|
||||
if: github.repository == 'lucide-icons/lucide' && needs.create-release.outputs.SHOULD_RELEASE == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-release
|
||||
permissions:
|
||||
id-token: write # Required for OIDC (npm provenance)
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
cache: 'pnpm'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Set new version
|
||||
run: pnpm version ${{ needs.create-release.outputs.VERSION }} --filter @lucide/lab --fail-if-no-match --no-git-tag-version
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter @lucide/lab build
|
||||
|
||||
- name: Publish
|
||||
run: pnpm --filter @lucide/lab publish --access public --no-git-checks --ignore-scripts --tag latest
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: true
|
||||
+10
-1
@@ -1,11 +1,13 @@
|
||||
.DS_Store
|
||||
.idea
|
||||
.next
|
||||
.astro
|
||||
.obsidian
|
||||
.now
|
||||
.idea
|
||||
.cache
|
||||
.env
|
||||
node_modules
|
||||
.pnpm-store
|
||||
dist
|
||||
build
|
||||
sandbox
|
||||
@@ -13,6 +15,10 @@ stash
|
||||
coverage
|
||||
stats
|
||||
*.log
|
||||
*.tsbuildinfo
|
||||
integrations/**/.react-router
|
||||
integrations/**/.vitest-attachments
|
||||
integrations/**/__screenshots__
|
||||
outlined
|
||||
lucide-font
|
||||
packages/**/src/icons/*.js
|
||||
@@ -23,6 +29,9 @@ packages/**/src/aliases.ts
|
||||
!packages/**/src/aliases/index.ts
|
||||
packages/**/src/utils/*.ts
|
||||
!packages/shared/src/utils/*.ts
|
||||
!packages/shared/src/build/
|
||||
!packages/shared/src/build/*.ts
|
||||
packages/shared/src/build/types.d.ts
|
||||
packages/**/src/dynamicIconImports.ts
|
||||
packages/**/DynamicIcon.d.ts
|
||||
packages/**/dynamicIconImports.js
|
||||
|
||||
@@ -1,5 +1,2 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
pnpm lint-staged
|
||||
pnpm checkIcons
|
||||
|
||||
@@ -19,6 +19,13 @@ packages/lucide-static/icon-nodes.json
|
||||
packages/lucide-static/font
|
||||
packages/svelte/.svelte-kit
|
||||
|
||||
# integration test artifacts and generated routes
|
||||
integrations/**/.react-router
|
||||
integrations/**/next-env.d.ts
|
||||
integrations/**/routeTree.gen.ts
|
||||
integrations/**/.vitest-attachments
|
||||
integrations/**/__screenshots__
|
||||
|
||||
# lucide-svelte
|
||||
packages/svelte/src/icons/*.svelte
|
||||
packages/svelte/.svelte-kit
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { eventHandler, setResponseHeader } from 'h3';
|
||||
import iconNodes from '../../data/iconNodes/index.ts';
|
||||
import { IconNodeWithKeys } from '../../theme/types';
|
||||
import iconMetaData from '../../data/iconMetaData';
|
||||
import releaseMeta from '../../data/releaseMetaData.json';
|
||||
import categories from '../../data/categoriesData.json';
|
||||
|
||||
const dataResponse = {
|
||||
icons: Object.entries(iconNodes).reduce((acc, [name, iconNode]) => {
|
||||
const newIconNode = (iconNode as IconNodeWithKeys).map(([name, { key, ...attrs }]) => {
|
||||
const newIconNode = iconNode.map(([name, { key, ...attrs }]) => {
|
||||
return [name, attrs];
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { eventHandler, getQuery, setResponseHeader } from 'h3';
|
||||
import iconNodes from '../../data/iconNodes';
|
||||
import { IconNodeWithKeys } from '../../theme/types';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const query = getQuery(event);
|
||||
@@ -19,7 +18,7 @@ export default eventHandler((event) => {
|
||||
return [name, iconNode];
|
||||
}
|
||||
|
||||
const newIconNode = (iconNode as IconNodeWithKeys).map(([name, { key, ...attrs }]) => {
|
||||
const newIconNode = iconNode.map(([name, { key, ...attrs }]) => {
|
||||
return [name, attrs];
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { groupIconMdPlugin, groupIconVitePlugin } from 'vitepress-plugin-group-i
|
||||
import sidebar from './sidebar';
|
||||
import snackPlayer from './markdown/snackPlayer';
|
||||
import sandpackPlugin from './markdown/sandpack';
|
||||
import examplePlugin from './markdown/example';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { resourcesSidebar } from './sidebar/resources';
|
||||
import llmstxt from 'vitepress-plugin-llms';
|
||||
@@ -31,6 +32,7 @@ export default defineConfig({
|
||||
config(md) {
|
||||
md.use(groupIconMdPlugin);
|
||||
md.use(snackPlayer);
|
||||
md.use(examplePlugin);
|
||||
md.use(sandpackPlugin, {
|
||||
defaultFiles: {
|
||||
'/styles.css': {
|
||||
@@ -95,8 +97,9 @@ export default defineConfig({
|
||||
{
|
||||
text: 'Resources',
|
||||
items: [
|
||||
...resourcesSidebar[0].items,
|
||||
{ text: 'Design icons', link: '/contribute/icon-design-guide' },
|
||||
...resourcesSidebar[0].items.map(({ text, link }) => ({ text, link })),
|
||||
{ text: 'How to use icons', link: '/how-to/' },
|
||||
{ text: 'Contributing icons', link: '/contribute/icons/' },
|
||||
],
|
||||
},
|
||||
{ text: 'Packages', link: '/packages' },
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { SVGProps } from 'react';
|
||||
import { getCommands } from './utils';
|
||||
|
||||
export type Point = { x: number; y: number };
|
||||
@@ -15,8 +14,8 @@ export type Path = {
|
||||
};
|
||||
|
||||
export type PathProps<
|
||||
RequiredProps extends keyof SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
|
||||
NeverProps extends keyof SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
|
||||
RequiredProps extends keyof React.SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
|
||||
NeverProps extends keyof React.SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
|
||||
> = Required<Pick<React.SVGProps<SVGElement & SVGRectElement & SVGCircleElement>, RequiredProps>> &
|
||||
Omit<
|
||||
React.SVGProps<SVGPathElement & SVGRectElement & SVGCircleElement>,
|
||||
|
||||
@@ -53,10 +53,7 @@ export const getCommands = (src: string) =>
|
||||
getNodes(src)
|
||||
.map(convertToPathNode)
|
||||
.flatMap(({ d, name }, idx) =>
|
||||
new SVGPathData(d)
|
||||
.toAbs()
|
||||
// @ts-expect-error `commands` is typed as a union of command types svgson does not narrow
|
||||
.commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name })),
|
||||
new SVGPathData(d).toAbs().commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name })),
|
||||
);
|
||||
|
||||
const getPaths = (src: string) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Category, IconEntity } from '../theme/types';
|
||||
|
||||
const directory = path.join(process.cwd(), '../categories');
|
||||
|
||||
export function getAllCategoryFiles(): Category[] {
|
||||
export function getAllCategoryFiles(): Omit<Category, 'iconCount'>[] {
|
||||
const fileNames = fs.readdirSync(directory).filter((file) => path.extname(file) === '.json');
|
||||
|
||||
return fileNames.map((fileName) => {
|
||||
@@ -21,9 +21,9 @@ export function getAllCategoryFiles(): Category[] {
|
||||
}
|
||||
|
||||
export function mapCategoryIconCount(
|
||||
categories: Category[],
|
||||
categories: Omit<Category, 'iconCount'>[],
|
||||
icons: { categories: IconEntity['categories'] }[],
|
||||
) {
|
||||
): Category[] {
|
||||
return categories.map((category) => ({
|
||||
...category,
|
||||
iconCount: icons.reduce(
|
||||
|
||||
@@ -119,8 +119,7 @@ const highlighter = await createHighlighter({
|
||||
});
|
||||
|
||||
export type ThemeOptions =
|
||||
| ThemeRegistration
|
||||
| { light: ThemeRegistration; dark: ThemeRegistration };
|
||||
ThemeRegistration | { light: ThemeRegistration; dark: ThemeRegistration };
|
||||
|
||||
export const highLightCode = async (code: string, lang: string, active?: boolean) => {
|
||||
const highlightedCode = highlighter
|
||||
|
||||
@@ -2,8 +2,7 @@ import { bundledLanguages, type ThemeRegistration } from 'shiki';
|
||||
import { createHighlighter } from 'shiki';
|
||||
|
||||
export type ThemeOptions =
|
||||
| ThemeRegistration
|
||||
| { light: ThemeRegistration; dark: ThemeRegistration };
|
||||
ThemeRegistration | { light: ThemeRegistration; dark: ThemeRegistration };
|
||||
|
||||
const highLightCode = async (code: string, lang: string, active?: boolean) => {
|
||||
const highlighter = await createHighlighter({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createLucideIcon } from 'lucide-react/src/lucide-react';
|
||||
import React from 'react';
|
||||
import createLucideIcon from 'lucide-react/src/createLucideIcon';
|
||||
import { type LucideProps, type IconNode } from 'lucide-react/src/types';
|
||||
import { IconEntity } from '../theme/types';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
|
||||
@@ -20,6 +20,8 @@ export async function getData(name: string) {
|
||||
const iconNode = iconNodes[name];
|
||||
|
||||
const releaseData = releaseMeta?.[name] ?? {
|
||||
fromFork: false,
|
||||
awaitingRelease: false,
|
||||
createdRelease: {
|
||||
version: '0.0.0',
|
||||
date: DATE_OF_FORK,
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, isAbsolute, resolve } from 'node:path';
|
||||
import type { MarkdownIt, RenderRule } from './types';
|
||||
import container from 'markdown-it-container';
|
||||
|
||||
type ContainerArgs = [typeof container, string, { render: RenderRule }];
|
||||
type GuidanceType = 'do' | 'dont' | 'caution';
|
||||
|
||||
const isGuidanceOpenToken = (type: string) =>
|
||||
type === 'container_do_open' ||
|
||||
type === 'container_dont_open' ||
|
||||
type === 'container_caution_open';
|
||||
|
||||
const isGuidanceCloseToken = (type: string) =>
|
||||
type === 'container_do_close' ||
|
||||
type === 'container_dont_close' ||
|
||||
type === 'container_caution_close';
|
||||
|
||||
const parseInfo = (info: string, marker: string) => info.trim().slice(marker.length).trim();
|
||||
|
||||
const stripWrappingQuotes = (value: string) => {
|
||||
const quote = value[0];
|
||||
|
||||
if ((quote === '"' || quote === "'") && value.at(-1) === quote) {
|
||||
return value.slice(1, -1);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const isRawSvgUrl = (url: string) => {
|
||||
const [filePath, query = ''] = url.split('?');
|
||||
const params = new URLSearchParams(query);
|
||||
|
||||
return filePath.endsWith('.svg') && params.get('raw') === 'true';
|
||||
};
|
||||
|
||||
const isImageUrl = (value: string) => {
|
||||
const [filePath] = value.split('?');
|
||||
|
||||
return /\.(avif|gif|jpe?g|png|svg|webp)$/i.test(filePath);
|
||||
};
|
||||
|
||||
const resolveMarkdownPath = (env: Record<string, unknown>) =>
|
||||
(env.path as string | undefined) ??
|
||||
(env.file as string | undefined) ??
|
||||
(env.realPath as string | undefined);
|
||||
|
||||
const renderImage = (
|
||||
url: string,
|
||||
env: Record<string, unknown>,
|
||||
escapeAttr: (value: string) => string,
|
||||
) => {
|
||||
if (!isRawSvgUrl(url)) {
|
||||
return `<img class="example-guidance__image" src="${escapeAttr(url)}" alt="" loading="lazy">`;
|
||||
}
|
||||
|
||||
const [filePath] = url.split('?');
|
||||
const markdownPath = resolveMarkdownPath(env);
|
||||
const resolvedPath = isAbsolute(filePath)
|
||||
? resolve(process.cwd(), `.${filePath}`)
|
||||
: resolve(markdownPath ? dirname(markdownPath) : process.cwd(), filePath);
|
||||
const svg = readFileSync(resolvedPath, 'utf8');
|
||||
const replacedSvg = svg
|
||||
.replaceAll(/(#000(000)?|black)/g, 'currentColor')
|
||||
.replaceAll(' stroke="#D8D8D9"', 'style="stroke: currentColor; stroke-opacity: .25;"');
|
||||
|
||||
return `<div class="example-guidance__image example-guidance__image--raw">${replacedSvg}</div>`;
|
||||
};
|
||||
|
||||
const renderPreview = (
|
||||
value: string,
|
||||
env: Record<string, unknown>,
|
||||
escapeHtml: (value: string) => string,
|
||||
renderInline: (value: string) => string,
|
||||
) => {
|
||||
if (isImageUrl(value)) {
|
||||
return renderImage(value, env, escapeHtml);
|
||||
}
|
||||
|
||||
return `<div class="example-guidance__text">${renderInline(value)}</div>`;
|
||||
};
|
||||
|
||||
const hasTopLevelContent = (tokens: Parameters<RenderRule>[0], index: number) => {
|
||||
let guidanceDepth = 0;
|
||||
|
||||
for (let i = index + 1; i < tokens.length; i += 1) {
|
||||
const token = tokens[i];
|
||||
|
||||
if (token.type === 'container_example_close') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isGuidanceOpenToken(token.type)) {
|
||||
guidanceDepth += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isGuidanceCloseToken(token.type)) {
|
||||
guidanceDepth -= 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (guidanceDepth === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const hasGuidance = (tokens: Parameters<RenderRule>[0], index: number) => {
|
||||
for (let i = index + 1; i < tokens.length; i += 1) {
|
||||
const token = tokens[i];
|
||||
|
||||
if (token.type === 'container_example_close') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isGuidanceOpenToken(token.type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const isFirstGuidance = (tokens: Parameters<RenderRule>[0], index: number) => {
|
||||
for (let i = index - 1; i >= 0; i -= 1) {
|
||||
const token = tokens[i];
|
||||
|
||||
if (token.type === 'container_example_open') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isGuidanceOpenToken(token.type)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const getExampleOpenIndex = (tokens: Parameters<RenderRule>[0], index: number) => {
|
||||
for (let i = index; i >= 0; i -= 1) {
|
||||
if (tokens[i].type === 'container_example_open') {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
};
|
||||
|
||||
const labels: Record<GuidanceType, string> = {
|
||||
do: `<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" class="lucide lucide-check-icon lucide-check"><path d="M20 6 9 17l-5-5"/></svg> Do`,
|
||||
dont: `<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" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> Don't`,
|
||||
caution: `<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" class="lucide lucide-exclamation-point"><path d="M12 20 L12 20" /><path d="M12 4 L12 15" /></svg> Caution`,
|
||||
};
|
||||
|
||||
export default function examplePlugin(md: MarkdownIt) {
|
||||
const escapeHtml = md.utils.escapeHtml;
|
||||
|
||||
const createExampleContainer = (): ContainerArgs => [
|
||||
container,
|
||||
'example',
|
||||
{
|
||||
render(tokens, idx) {
|
||||
const token = tokens[idx];
|
||||
|
||||
if (token.nesting === 1) {
|
||||
const hasContent = hasTopLevelContent(tokens, idx);
|
||||
const guidanceOnlyClass = hasContent ? '' : ' example-block--guidance-only';
|
||||
const contentOpen = hasContent ? '<div class="example-block__content">\n' : '';
|
||||
|
||||
return `<section class="example-block${guidanceOnlyClass}">\n${contentOpen}`;
|
||||
}
|
||||
|
||||
const exampleOpenIndex = getExampleOpenIndex(tokens, idx);
|
||||
const contentClose =
|
||||
hasTopLevelContent(tokens, exampleOpenIndex) && !hasGuidance(tokens, exampleOpenIndex)
|
||||
? '</div>\n'
|
||||
: '';
|
||||
|
||||
return `${contentClose}</section>\n`;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const createGuidanceContainer = (type: GuidanceType): ContainerArgs => [
|
||||
container,
|
||||
type,
|
||||
{
|
||||
render(tokens, idx, _options, env) {
|
||||
const token = tokens[idx];
|
||||
|
||||
if (token.nesting === 1) {
|
||||
const exampleOpenIndex = getExampleOpenIndex(tokens, idx);
|
||||
const preview = stripWrappingQuotes(parseInfo(token.info, type));
|
||||
const media = renderPreview(preview, env, escapeHtml, md.renderInline.bind(md));
|
||||
const contentClose =
|
||||
hasTopLevelContent(tokens, exampleOpenIndex) && isFirstGuidance(tokens, idx)
|
||||
? '</div>\n'
|
||||
: '';
|
||||
const label = labels[type];
|
||||
|
||||
return `${contentClose}<figure class="example-guidance example-guidance--${type}">${media}<figcaption class="example-guidance__caption"><div class="example-guidance__label">${label}</div>\n`;
|
||||
}
|
||||
|
||||
return '</figcaption></figure>\n';
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
md.use(...createExampleContainer());
|
||||
md.use(...createGuidanceContainer('do'));
|
||||
md.use(...createGuidanceContainer('dont'));
|
||||
md.use(...createGuidanceContainer('caution'));
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import type MarkdownIt from 'markdown-it';
|
||||
import type { RenderRule } from 'markdown-it/lib/renderer.mjs';
|
||||
import type Token from 'markdown-it/lib/token.mjs';
|
||||
import type { MarkdownIt, RenderRule, Token } from './types';
|
||||
import container from 'markdown-it-container';
|
||||
import sandpackTheme from '../theme/sandpackTheme.json';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* />
|
||||
*/
|
||||
|
||||
import type MarkdownIt from 'markdown-it';
|
||||
import type { MarkdownIt } from './types';
|
||||
|
||||
type SnackParams = Record<string, string>;
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { MarkdownRenderer } from 'vitepress';
|
||||
|
||||
// VitePress 1.x renders with markdown-it 14, so derive the plugin types from its renderer
|
||||
// instead of the markdown-it 15 package installed in docs.
|
||||
export type MarkdownIt = MarkdownRenderer;
|
||||
export type RenderRule = NonNullable<MarkdownIt['renderer']['rules'][string]>;
|
||||
export type Token = Parameters<RenderRule>[0][number];
|
||||
@@ -94,7 +94,7 @@ export const angularSidebar = [
|
||||
items: [
|
||||
{
|
||||
text: 'Accessibility in depth',
|
||||
link: '/guide/accessibility',
|
||||
link: '/how-to/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -69,7 +69,7 @@ export const astroSidebar = [
|
||||
items: [
|
||||
{
|
||||
text: 'Accessibility in depth',
|
||||
link: '/guide/accessibility',
|
||||
link: '/how-to/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -36,6 +36,7 @@ const sidebar: Sidebar = {
|
||||
'/guide/react-native/': reactNativeSidebar,
|
||||
'/guide/angular/': angularSidebar,
|
||||
'/guide/static/': lucideStaticSidebar,
|
||||
'/how-to': resourcesSidebar,
|
||||
'/community': resourcesSidebar,
|
||||
'/contribute': resourcesSidebar,
|
||||
'/license': resourcesSidebar,
|
||||
|
||||
@@ -79,7 +79,7 @@ export const lucideSidebar = [
|
||||
items: [
|
||||
{
|
||||
text: 'Accessibility in depth',
|
||||
link: '/guide/accessibility',
|
||||
link: '/how-to/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -89,7 +89,7 @@ export const preactSidebar = [
|
||||
items: [
|
||||
{
|
||||
text: 'Accessibility in depth',
|
||||
link: '/guide/accessibility',
|
||||
link: '/how-to/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -100,7 +100,7 @@ export const reactSidebar = [
|
||||
items: [
|
||||
{
|
||||
text: 'Accessibility in depth',
|
||||
link: '/guide/accessibility',
|
||||
link: '/how-to/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,41 +11,110 @@ export const resourcesSidebar = [
|
||||
link: '/community',
|
||||
},
|
||||
{
|
||||
text: 'Code of Conduct',
|
||||
text: 'Code of conduct',
|
||||
link: '/code-of-conduct',
|
||||
},
|
||||
{
|
||||
text: 'Brand logo statement',
|
||||
link: '/brand-logo-statement',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Contribute',
|
||||
items: [
|
||||
{
|
||||
text: 'Contributing',
|
||||
link: '/contribute',
|
||||
text: 'Contribution guide',
|
||||
link: '/contribute/',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Designing icons',
|
||||
text: 'How to use Lucide icons',
|
||||
items: [
|
||||
{
|
||||
text: 'Icon Design Principles',
|
||||
link: '/contribute/icon-design-guide',
|
||||
text: 'Getting started',
|
||||
link: '/how-to/',
|
||||
},
|
||||
{
|
||||
text: 'Designing in Illustrator',
|
||||
link: '/contribute/illustrator-guide',
|
||||
text: 'Accessibility guide',
|
||||
link: '/how-to/accessibility',
|
||||
desc: 'Learn how to make icons readable, understandable, and usable with assistive technology.',
|
||||
},
|
||||
{
|
||||
text: 'Designing in Inkscape',
|
||||
link: '/contribute/inkscape-guide',
|
||||
text: 'How to pick the right icon',
|
||||
link: '/how-to/icon-guide',
|
||||
desc: 'Learn how to decide when icons belong in an interface and how to pick symbols people are likely to recognize.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Contributing icons',
|
||||
items: [
|
||||
{
|
||||
text: 'Getting started',
|
||||
link: '/contribute/icons/',
|
||||
},
|
||||
{
|
||||
text: 'Designing in Figma',
|
||||
link: '/contribute/figma-guide',
|
||||
text: 'Design language',
|
||||
link: '/contribute/icons/design-principles',
|
||||
desc: 'Learn the visual principles that shape Lucide icons with side-by-side examples.',
|
||||
},
|
||||
{
|
||||
text: 'Designing in Affinity Designer',
|
||||
link: '/contribute/affinity-designer-guide',
|
||||
text: 'Design specification',
|
||||
link: '/contribute/icons/specification',
|
||||
desc: 'Review the technical requirements every Lucide icon must meet.',
|
||||
},
|
||||
{
|
||||
text: 'Naming conventions',
|
||||
link: '/contribute/icons/naming-conventions',
|
||||
desc: 'Learn how to choose clear, consistent, and predictable names for Lucide icons.',
|
||||
},
|
||||
{
|
||||
text: 'Metadata conventions',
|
||||
link: '/contribute/icons/metadata-conventions',
|
||||
desc: 'Learn how to write use cases, tags, categories, aliases, and other icon metadata.',
|
||||
items: [
|
||||
{
|
||||
text: 'Use case guide',
|
||||
link: '/contribute/icons/metadata-conventions/use-case-guide',
|
||||
desc: '',
|
||||
},
|
||||
{
|
||||
text: 'Tag guide',
|
||||
link: '/contribute/icons/metadata-conventions/tag-guide',
|
||||
desc: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'SVG conventions',
|
||||
link: '/contribute/icons/code-conventions',
|
||||
desc: 'Learn how to structure and write Lucide SVG files.',
|
||||
},
|
||||
{
|
||||
text: 'Design software guides',
|
||||
collapsed: false,
|
||||
desc: 'Learn how to design and export Lucide icons with common vector tools.',
|
||||
items: [
|
||||
{
|
||||
text: 'Adobe Illustrator',
|
||||
link: '/contribute/icons/illustrator-guide',
|
||||
desc: '',
|
||||
},
|
||||
{
|
||||
text: 'Inkscape',
|
||||
link: '/contribute/icons/inkscape-guide',
|
||||
},
|
||||
{
|
||||
text: 'Figma',
|
||||
link: '/contribute/icons/figma-guide',
|
||||
},
|
||||
{
|
||||
text: 'Affinity Designer',
|
||||
link: '/contribute/icons/affinity-designer-guide',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -90,7 +90,7 @@ export const solidSidebar = [
|
||||
items: [
|
||||
{
|
||||
text: 'Accessibility in depth',
|
||||
link: '/guide/accessibility',
|
||||
link: '/how-to/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -75,7 +75,7 @@ export const lucideStaticSidebar = [
|
||||
items: [
|
||||
{
|
||||
text: 'Accessibility in depth',
|
||||
link: '/guide/accessibility',
|
||||
link: '/how-to/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -79,7 +79,7 @@ export const svelteSidebar = [
|
||||
items: [
|
||||
{
|
||||
text: 'Accessibility in depth',
|
||||
link: '/guide/accessibility',
|
||||
link: '/how-to/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -85,7 +85,7 @@ export const vueSidebar = [
|
||||
items: [
|
||||
{
|
||||
text: 'Accessibility in depth',
|
||||
link: '/guide/accessibility',
|
||||
link: '/how-to/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
import IconButton from './base/IconButton.vue';
|
||||
import Button from './base/Button.vue';
|
||||
import { x } from '../../data/iconNodes';
|
||||
import Icon from '@lucide/vue/src/Icon';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
@@ -65,16 +65,17 @@ function hidePopup() {
|
||||
}"
|
||||
class="ask-overlay"
|
||||
>
|
||||
<IconButton
|
||||
<Button
|
||||
@click="hidePopup"
|
||||
class="hide-button"
|
||||
>
|
||||
<Icon
|
||||
:iconNode="x"
|
||||
name="x"
|
||||
:size="20"
|
||||
absoluteStrokeWidth
|
||||
/>
|
||||
</IconButton>
|
||||
</Button>
|
||||
|
||||
<h2 class="title">
|
||||
We value your feedback!
|
||||
|
||||
+43
-3
@@ -1,7 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
href?: string;
|
||||
}>()
|
||||
|
||||
const isExternal = computed(() => {
|
||||
return props.href?.startsWith('http') ?? false
|
||||
})
|
||||
|
||||
const component = computed(() => {
|
||||
return props.href ? 'a' : 'button'
|
||||
})
|
||||
|
||||
const target = computed(() => {
|
||||
return isExternal.value ? '_blank' : undefined
|
||||
})
|
||||
|
||||
const rel = computed(() => {
|
||||
return isExternal.value ? 'noreferrer noopener' : undefined
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button v-bind="$attrs" class="icon-button">
|
||||
<component
|
||||
:is="component"
|
||||
v-bind="$attrs"
|
||||
:href="href"
|
||||
:target="target"
|
||||
:rel="rel"
|
||||
class="icon-button"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -16,12 +47,21 @@
|
||||
transition: color 0.25s, border-color 0.25s, background-color 0.25s;
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
/* width: 56px;
|
||||
height: 56px; */
|
||||
font-size: 24px;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: color 0.1s, border-color 0.1s, background-color 0.1s;
|
||||
}
|
||||
|
||||
.icon-button span {
|
||||
padding: 0 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
border-color: var(--vp-button-alt-hover-border);
|
||||
color: var(--vp-button-alt-hover-text);
|
||||
@@ -14,7 +14,7 @@ export interface InputProps {
|
||||
import { ref, onMounted, nextTick, watch } from 'vue';
|
||||
import Icon from '@lucide/vue/src/Icon';
|
||||
import { x } from '../../../data/iconNodes';
|
||||
import IconButton from './IconButton.vue';
|
||||
import Button from './Button.vue';
|
||||
|
||||
const props = withDefaults(defineProps<InputProps>(), {
|
||||
type: 'text',
|
||||
@@ -68,9 +68,9 @@ defineExpose({
|
||||
ref="input"
|
||||
:value="modelValue"
|
||||
v-bind="$attrs"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<IconButton
|
||||
<Button
|
||||
@click="onClear"
|
||||
v-if="type === 'search' && modelValue"
|
||||
class="clear-button"
|
||||
@@ -79,9 +79,10 @@ defineExpose({
|
||||
>
|
||||
<Icon
|
||||
:iconNode="x"
|
||||
name="x"
|
||||
:size="20"
|
||||
/>
|
||||
</IconButton>
|
||||
</Button>
|
||||
<kbd
|
||||
v-if="shortcut"
|
||||
class="shortcut"
|
||||
|
||||
@@ -46,6 +46,7 @@ const value = computed({
|
||||
<template #icon>
|
||||
<Icon
|
||||
:iconNode="search"
|
||||
name="search"
|
||||
class="search-icon"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,8 @@ import RocketDuotoneIcon from '../home/RocketDuotoneIcon.ts';
|
||||
<p>
|
||||
<Icon
|
||||
:iconNode="RocketDuotoneIcon"
|
||||
size="18"
|
||||
name="rocket-duotone"
|
||||
:size="18"
|
||||
class="rocket-icon"
|
||||
/>
|
||||
<VPLink
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue'
|
||||
|
||||
const props = defineProps<{
|
||||
tabs: string[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabGroup>
|
||||
<TabList class="tab-list">
|
||||
<template v-for="(tab, index) in props.tabs" :key="index">
|
||||
<Tab :id="`tab-${index}`" class="tab">{{ tab }}</Tab>
|
||||
</template>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<template v-for="(_, name) in $slots">
|
||||
<TabPanel>
|
||||
<slot :name="name" />
|
||||
</TabPanel>
|
||||
</template>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.tab-list {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 32px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: block;
|
||||
line-height: 32px;
|
||||
font-size: 16px;
|
||||
padding: 10px 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: color 0.5s;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.tab[aria-selected="true"] {
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
border-bottom: 2px solid var(--vp-c-brand);
|
||||
}
|
||||
|
||||
@media (min-width: 860px) {
|
||||
.tab-list {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.tab {
|
||||
font-size: 18px;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.tab {
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1152px) {
|
||||
.tab {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -34,7 +34,7 @@ const percentage = computed<string>(() => `${((Number(props.modelValue) - props.
|
||||
type="range"
|
||||
v-bind="$attrs"
|
||||
v-bind:value="modelValue"
|
||||
v-on:input="$emit('update:modelValue', Number($event.target.value))"
|
||||
v-on:input="$emit('update:modelValue', Number(($event.target as HTMLInputElement).value))"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { rotateCw } from '../../../data/iconNodes';
|
||||
import Icon from '@lucide/vue/src/Icon';
|
||||
import IconButton from './IconButton.vue';
|
||||
import Button from './Button.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconButton class="reset-button">
|
||||
<Button class="reset-button">
|
||||
<Icon
|
||||
:size="20"
|
||||
:iconNode="rotateCw"
|
||||
name="rotate-cw"
|
||||
/>
|
||||
</IconButton>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -27,6 +27,7 @@ const selected = defineModel<{ name: string; value: string; icon?: string; iconD
|
||||
<slot name="end-icon">
|
||||
<Icon
|
||||
:iconNode="chevronsUpDown"
|
||||
name="chevrons-up-down"
|
||||
class="chevron-icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -57,6 +58,7 @@ const selected = defineModel<{ name: string; value: string; icon?: string; iconD
|
||||
>
|
||||
<Icon
|
||||
:iconNode="check"
|
||||
name="check"
|
||||
class="check"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { type FunctionalComponent } from 'vue';
|
||||
import { type LucideProps } from '@lucide/vue';
|
||||
import { type LucideProps } from '@lucide/vue/src/types';
|
||||
import { Sandpack } from 'sandpack-vue3';
|
||||
|
||||
defineProps<FunctionalComponent<LucideProps>>();
|
||||
|
||||
@@ -10,7 +10,14 @@ import { useLocalStorage } from '@vueuse/core';
|
||||
const { page } = useData();
|
||||
const router = useRouter();
|
||||
|
||||
const frameworks = [
|
||||
interface Framework {
|
||||
name: string;
|
||||
icon: string;
|
||||
iconDark?: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const frameworks: Framework[] = [
|
||||
{ name: 'Vanilla', icon: '/framework-logos/js.svg', value: '/guide/lucide/' },
|
||||
{ name: 'React', icon: '/framework-logos/react.svg', value: '/guide/react/' },
|
||||
{ name: 'Vue', icon: '/framework-logos/vue.svg', value: '/guide/vue/' },
|
||||
@@ -48,7 +55,7 @@ const selected = computed(() => {
|
||||
return current || (fallbackFramework.value?.value ? fallbackFramework.value : frameworks[0]);
|
||||
});
|
||||
|
||||
function onSelectFramework(item: { name: string; icon: string; iconDark?: string; value: string }) {
|
||||
function onSelectFramework(item: Framework) {
|
||||
fallbackFramework.value = item;
|
||||
if (item.value !== router.route.path) {
|
||||
const likeRoute = router.route.path.replace(selected.value.value, item.value);
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import { data } from './HomeHeroIconsCard.data';
|
||||
import { useRouter } from 'vitepress';
|
||||
import { motion, Variants, useScroll, useTransform } from 'motion-v';
|
||||
import { motion, useScroll, useTransform, type Options as MotionOptions } from 'motion-v';
|
||||
import LucideIcon from '../base/LucideIcon.vue';
|
||||
|
||||
// motion-v re-exports framer-motion's `Variants`, which doesn't match the `variants` prop of its components
|
||||
type Variants = MotionOptions['variants'];
|
||||
|
||||
const emit = defineEmits(['animation-complete']);
|
||||
|
||||
const MotionLucideIcon = motion.create(LucideIcon);
|
||||
@@ -129,7 +132,7 @@ const onAnimationComplete = (item) => {
|
||||
|
||||
const randomIndex = ref(Math.floor(Math.random() * 64));
|
||||
|
||||
const iconAnimationVariants = {
|
||||
const iconAnimationVariants: Variants = {
|
||||
initial: {
|
||||
animationName: 'end',
|
||||
opacity: 0,
|
||||
@@ -149,7 +152,7 @@ const iconAnimationVariants = {
|
||||
|
||||
const shrinkIconAnimation = ref('initial');
|
||||
|
||||
const shrinkIconVariants = {
|
||||
const shrinkIconVariants: Variants = {
|
||||
initial: { strokeWidth: 2 },
|
||||
shrinkIcons: (index) => ({
|
||||
animationName: 'shrinkIcons',
|
||||
|
||||
@@ -18,7 +18,7 @@ const pickIcons = [
|
||||
'clapperboard',
|
||||
'tool-case',
|
||||
'printer',
|
||||
'album',
|
||||
'square-bookmark',
|
||||
'pencil-ruler',
|
||||
'calendar',
|
||||
'brain',
|
||||
@@ -53,7 +53,7 @@ const pickIcons = [
|
||||
'terminal',
|
||||
'thermometer',
|
||||
'thumbs-up',
|
||||
'trash-2',
|
||||
'trash',
|
||||
'umbrella',
|
||||
'watch',
|
||||
'sparkles',
|
||||
@@ -69,7 +69,7 @@ const pickIcons = [
|
||||
'stethoscope',
|
||||
];
|
||||
|
||||
export default {
|
||||
const loader = {
|
||||
async load() {
|
||||
const icons = Object.entries(iconNodes).map(([name, iconNode]) => ({ name, iconNode }));
|
||||
|
||||
@@ -96,3 +96,8 @@ export default {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
declare const data: Awaited<ReturnType<typeof loader.load>>;
|
||||
|
||||
export { data };
|
||||
export default loader;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default {
|
||||
const loader = {
|
||||
async load() {
|
||||
const version = await fetch('https://api.github.com/repos/lucide-icons/lucide/releases/latest')
|
||||
.then((res) => {
|
||||
@@ -16,3 +16,8 @@ export default {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
declare const data: Awaited<ReturnType<typeof loader.load>>;
|
||||
|
||||
export { data };
|
||||
export default loader;
|
||||
|
||||
@@ -15,10 +15,11 @@ import RocketDuotoneIcon from './RocketDuotoneIcon.ts';
|
||||
class="badge-special confetti-button animate"
|
||||
href="/guide/version-1"
|
||||
>
|
||||
<Icon :iconNode="RocketDuotoneIcon" />
|
||||
<Icon :iconNode="RocketDuotoneIcon" name="rocket-duotone" />
|
||||
Version 1
|
||||
<Icon
|
||||
:iconNode="moveRight"
|
||||
name="move-right"
|
||||
class=""
|
||||
/>
|
||||
</Badge>
|
||||
|
||||
@@ -41,8 +41,8 @@ const sizeCssVar = useCssVar(
|
||||
)
|
||||
|
||||
syncRef(color, colorCssVar)
|
||||
syncRef(strokeWidth, strokeWidthCssVar)
|
||||
syncRef(size, sizeCssVar)
|
||||
syncRef(strokeWidth, strokeWidthCssVar, { transform: { ltr: String, rtl: Number } })
|
||||
syncRef(size, sizeCssVar, { transform: { ltr: String, rtl: Number } })
|
||||
|
||||
function resetStyle () {
|
||||
color.value = 'currentColor'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default {
|
||||
const loader = {
|
||||
async load() {
|
||||
return {
|
||||
packages: [
|
||||
@@ -61,3 +61,8 @@ export default {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
declare const data: Awaited<ReturnType<typeof loader.load>>;
|
||||
|
||||
export { data };
|
||||
export default loader;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
headingLevel: 1 | 2 | 3 | 4 | 5 | 6,
|
||||
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6,
|
||||
}>()
|
||||
|
||||
const headingElement = computed(() => `h${props.headingLevel ?? 2}`)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const RocketDuotoneIcon = [
|
||||
import type { LucideIconNode } from '@lucide/vue/src/types';
|
||||
|
||||
const RocketDuotoneIcon: LucideIconNode[] = [
|
||||
[
|
||||
'path',
|
||||
{
|
||||
@@ -16,6 +18,6 @@ const RocketDuotoneIcon = [
|
||||
'clip-rule': 'evenodd',
|
||||
},
|
||||
],
|
||||
] as const;
|
||||
];
|
||||
|
||||
export default RocketDuotoneIcon;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress';
|
||||
import { useSessionStorage } from '@vueuse/core';
|
||||
import IconButton from '../base/IconButton.vue';
|
||||
import Button from '../base/Button.vue';
|
||||
import VPDocAsideCarbonAds from 'vitepress/dist/client/theme-default/components/VPDocAsideCarbonAds.vue';
|
||||
import { x } from '../../../data/iconNodes';
|
||||
import Icon from '@lucide/vue/src/Icon';
|
||||
@@ -40,16 +40,17 @@ onMounted(() => {
|
||||
class="floating-ad"
|
||||
v-if="theme.carbonAds"
|
||||
>
|
||||
<IconButton
|
||||
<Button
|
||||
@click="hideAd"
|
||||
class="hide-button"
|
||||
>
|
||||
<Icon
|
||||
:iconNode="x"
|
||||
name="x"
|
||||
:size="20"
|
||||
absoluteStrokeWidth
|
||||
/>
|
||||
</IconButton>
|
||||
</Button>
|
||||
<VPDocAsideCarbonAds :carbon-ads="theme.carbonAds" overlay />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { getAllCategoryFiles, mapCategoryIconCount } from '../../../lib/categories';
|
||||
import iconsMetaData from '../../../data/iconMetaData';
|
||||
|
||||
export default {
|
||||
const loader = {
|
||||
async load() {
|
||||
let categories = getAllCategoryFiles();
|
||||
|
||||
categories = mapCategoryIconCount(categories, Object.values(iconsMetaData));
|
||||
const categories = mapCategoryIconCount(getAllCategoryFiles(), Object.values(iconsMetaData));
|
||||
|
||||
return {
|
||||
categories,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
declare const data: Awaited<ReturnType<typeof loader.load>>;
|
||||
|
||||
export { data };
|
||||
export default loader;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
|
||||
import { isActive } from 'vitepress/dist/client/shared'
|
||||
import { isActive } from 'vitepress/dist/client/shared.js'
|
||||
import { useActiveAnchor } from '../../composables/useActiveAnchor'
|
||||
import { data } from './CategoryList.data'
|
||||
import CategoryListItem from './CategoryListItem.vue'
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { pencilSparkles } from '../../../data/iconNodes';
|
||||
import { IconEntity } from '../../types';
|
||||
import getStudioLink from '../../utils/getStudioLink';
|
||||
import Button from '../base/Button.vue';
|
||||
import Icon from '@lucide/vue/src/Icon.ts';
|
||||
|
||||
defineProps<{
|
||||
icon: IconEntity
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
:href="getStudioLink(icon.name, icon.iconNode, 'icon-info')"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon :iconNode="pencilSparkles" name="pencil-sparkles" :size="24" />
|
||||
<span class="text">Edit in studio</span>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.text {
|
||||
margin: 0 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -30,10 +30,15 @@ const props = defineProps<{
|
||||
.contributors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-items: start;
|
||||
/* justify-content: flex-end; */
|
||||
gap: 16px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.contributors .label {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.avatar-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -16,19 +16,17 @@ const tags = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <PageContainer class="overview"> -->
|
||||
<IconPreview
|
||||
:name="$params.name"
|
||||
:iconNode="$params.iconNode"
|
||||
class="preview"
|
||||
customizable
|
||||
/>
|
||||
<div class="details">
|
||||
<h1 class="title">
|
||||
{{ $params.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<!-- </PageContainer> -->
|
||||
<IconPreview
|
||||
:name="$params.name"
|
||||
:iconNode="$params.iconNode"
|
||||
class="preview"
|
||||
customizable
|
||||
/>
|
||||
<div class="details">
|
||||
<h1 class="title">
|
||||
{{ $params.name }}
|
||||
</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -6,7 +6,11 @@ import useConfetti from '../../composables/useConfetti';
|
||||
const { animate, confetti } = useConfetti();
|
||||
const slots = useSlots();
|
||||
|
||||
const copiedText = computed(() => slots.default?.()[0].children);
|
||||
const copiedText = computed(() => {
|
||||
const children = slots.default?.()[0].children;
|
||||
|
||||
return typeof children === 'string' ? children : '';
|
||||
});
|
||||
|
||||
function copyText() {
|
||||
navigator.clipboard.writeText(copiedText.value);
|
||||
@@ -25,6 +29,7 @@ function copyText() {
|
||||
<slot />
|
||||
<Icon
|
||||
:iconNode="copy"
|
||||
name="copy"
|
||||
:size="20"
|
||||
class="copy-icon"
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { IconEntity } from '../../types';
|
||||
import { computed } from 'vue';
|
||||
import createLucideIcon from '@lucide/vue/src/createLucideIcon';
|
||||
import IconButton from '../base/IconButton.vue';
|
||||
import Button from '../base/Button.vue';
|
||||
import IconContributors from './IconContributors.vue';
|
||||
import IconPreview from './IconPreview.vue';
|
||||
import { x, expand } from '../../../data/iconNodes';
|
||||
@@ -12,6 +12,7 @@ import Badge from '../base/Badge.vue';
|
||||
import { computedAsync } from '@vueuse/core';
|
||||
import { satisfies } from 'semver';
|
||||
import { useExternalLibs } from '../../composables/useExternalLibs';
|
||||
import EditInStudioButton from './EditInStudioButton.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
iconName: string | null;
|
||||
@@ -70,12 +71,19 @@ const Expand = createLucideIcon('Expand', expand);
|
||||
<div class="overlay-panel">
|
||||
<nav class="overlay-menu">
|
||||
<Badge
|
||||
v-if="icon.createdRelease"
|
||||
v-if="icon.awaitingRelease"
|
||||
class="version awaiting-release"
|
||||
>Unreleased</Badge
|
||||
>
|
||||
<Badge
|
||||
v-else-if="icon.createdRelease"
|
||||
class="version"
|
||||
:href="releaseTagLink(icon.createdRelease.version)"
|
||||
>v{{ icon.createdRelease.version }}</Badge
|
||||
>
|
||||
<IconButton
|
||||
{{ icon.createdRelease.version }}
|
||||
</Badge>
|
||||
<EditInStudioButton :icon="icon"/>
|
||||
<Button
|
||||
@click="
|
||||
go(
|
||||
icon.externalLibrary
|
||||
@@ -85,10 +93,10 @@ const Expand = createLucideIcon('Expand', expand);
|
||||
"
|
||||
>
|
||||
<component :is="Expand" />
|
||||
</IconButton>
|
||||
<IconButton @click="onClose">
|
||||
</Button>
|
||||
<Button @click="onClose">
|
||||
<component :is="CloseIcon" />
|
||||
</IconButton>
|
||||
</Button>
|
||||
</nav>
|
||||
<IconPreview
|
||||
id="previewer"
|
||||
@@ -200,7 +208,14 @@ const Expand = createLucideIcon('Expand', expand);
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.version.awaiting-release {
|
||||
color: var(--vp-c-danger-3);
|
||||
}
|
||||
|
||||
.contributors {
|
||||
justify-content: flex-end;
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,30 +10,36 @@ import { computed } from 'vue';
|
||||
import createLucideIcon from '@lucide/vue/src/createLucideIcon';
|
||||
import { diamond } from '../../../data/iconNodes';
|
||||
import deprecationReasonTemplate from '../../../../../tools/build-icons/utils/deprecationReasonTemplate.ts';
|
||||
import type { IconDeprecationReason } from '../../../../../tools/build-icons/types.ts';
|
||||
import IconTags from "./IconTags.vue"
|
||||
import getStudioLink from '../../utils/getStudioLink';
|
||||
|
||||
const props = defineProps<{
|
||||
icon: IconEntity;
|
||||
popoverPosition?: 'top' | 'bottom';
|
||||
showEditButton?: boolean;
|
||||
}>();
|
||||
|
||||
const { go } = useRouter();
|
||||
const { page } = useData();
|
||||
|
||||
const tags = computed(() => {
|
||||
if (!props.icon || !props?.icon?.tags) return [];
|
||||
return props.icon.tags.join(' • ');
|
||||
});
|
||||
|
||||
const DiamondIcon = createLucideIcon('Diamond', diamond);
|
||||
|
||||
const deprecatedTitle = computed(() => {
|
||||
if (!props.icon.deprecationReason) return '';
|
||||
return deprecationReasonTemplate(props.icon.deprecationReason, {
|
||||
return deprecationReasonTemplate(props.icon.deprecationReason as IconDeprecationReason, {
|
||||
componentName: props.icon.name,
|
||||
iconName: props.icon.name,
|
||||
toBeRemovedInVersion: props.icon.toBeRemovedInVersion,
|
||||
});
|
||||
});
|
||||
|
||||
const iconPath = computed(() => {
|
||||
if (props.icon.externalLibrary) {
|
||||
return `icons/${props.icon.externalLibrary}/${props.icon.name}`;
|
||||
}
|
||||
|
||||
return `icons/${props.icon.name}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -60,14 +66,7 @@ const deprecatedTitle = computed(() => {
|
||||
Deprecated
|
||||
</Badge>
|
||||
</div>
|
||||
<div
|
||||
class="tags-scroller"
|
||||
v-if="tags.length"
|
||||
>
|
||||
<p class="icon-tags horizontal-scroller">
|
||||
{{ tags }}
|
||||
</p>
|
||||
</div>
|
||||
<IconTags :tags="props.icon.tags" />
|
||||
<div class="group">
|
||||
<Badge
|
||||
v-for="category in icon.categories"
|
||||
@@ -80,26 +79,10 @@ const deprecatedTitle = computed(() => {
|
||||
|
||||
<div class="group buttons">
|
||||
<VPButton
|
||||
v-if="
|
||||
!page?.relativePath?.startsWith?.(
|
||||
icon.externalLibrary
|
||||
? `icons/${icon.externalLibrary}/${icon.name}`
|
||||
: `icons/${icon.name}`,
|
||||
)
|
||||
"
|
||||
:href="
|
||||
icon.externalLibrary
|
||||
? `/icons/${icon.externalLibrary}/${icon.name}`
|
||||
: `/icons/${icon.name}`
|
||||
"
|
||||
v-if="!page?.relativePath?.startsWith?.(iconPath)"
|
||||
:href="iconPath"
|
||||
text="See in action"
|
||||
@click="
|
||||
go(
|
||||
icon.externalLibrary
|
||||
? `/icons/${icon.externalLibrary}/${icon.name}`
|
||||
: `/icons/${icon.name}`,
|
||||
)
|
||||
"
|
||||
@click="go(iconPath)"
|
||||
/>
|
||||
<CopySVGButton
|
||||
:name="icon.name"
|
||||
@@ -115,6 +98,38 @@ const deprecatedTitle = computed(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.studio-edit-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 0 14px;
|
||||
min-height: 40px;
|
||||
border: 1px solid var(--vp-button-alt-border);
|
||||
border-radius: 999px;
|
||||
background-color: var(--vp-button-alt-bg);
|
||||
color: var(--vp-button-alt-text);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
color 0.2s,
|
||||
background-color 0.2s,
|
||||
transform 0.2s;
|
||||
margin-left: 0;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.studio-edit-button:hover,
|
||||
.studio-edit-button:focus-visible {
|
||||
border-color: var(--vp-button-alt-hover-border);
|
||||
color: var(--vp-button-alt-hover-text);
|
||||
background-color: var(--vp-button-alt-hover-bg);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -156,66 +171,6 @@ const deprecatedTitle = computed(() => {
|
||||
background-color: var(--vp-c-brand-2);
|
||||
}
|
||||
|
||||
.icon-tags {
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tags-scroller {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
padding: 8px 0 16px;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 8px;
|
||||
align-items: center;
|
||||
|
||||
--gradient-background: var(--tags-gradient-background, var(--vp-c-bg-elv));
|
||||
}
|
||||
.horizontal-scroller {
|
||||
overflow-x: scroll;
|
||||
/* Hide Scrollbar */
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
scrollbar-width: thin; /* can also be normal, or none, to not render scrollbar */
|
||||
scrollbar-color: var(--vp-c-text-4) transparent; /* foreground background */
|
||||
}
|
||||
.horizontal-scroller::-webkit-scrollbar {
|
||||
width: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.horizontal-scroller::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.horizontal-scroller::-webkit-scrollbar-thumb {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.tags-scroller::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 32px;
|
||||
height: 100%;
|
||||
/* Background Gradient left to right */
|
||||
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, var(--gradient-background) 100%);
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { IconEntity } from '../../types';
|
||||
import { computed, ref } from 'vue';
|
||||
import createLucideIcon from '@lucide/vue/src/createLucideIcon';
|
||||
import { useIconStyleContext } from '../../composables/useIconStyle';
|
||||
import { squarePen } from '../../../data/iconNodes';
|
||||
|
||||
const props = defineProps<{
|
||||
name: IconEntity['name'];
|
||||
@@ -11,6 +10,8 @@ const props = defineProps<{
|
||||
customizable?: boolean;
|
||||
}>();
|
||||
|
||||
const activeSize = ref('1000%');
|
||||
|
||||
const { size, color, strokeWidth, absoluteStrokeWidth } = useIconStyleContext();
|
||||
const previewIcon = ref();
|
||||
|
||||
@@ -20,31 +21,6 @@ const iconComponent = computed(() => {
|
||||
if (!props.name || !props.iconNode) return null;
|
||||
return createLucideIcon(props.name, props.iconNode);
|
||||
});
|
||||
|
||||
const SquarePenIcon = createLucideIcon('SquarePen', squarePen);
|
||||
|
||||
const innerSvg = computed(() => {
|
||||
if (!props.iconNode) return '';
|
||||
|
||||
const string = props.iconNode
|
||||
.map(([tag, attrs]) => {
|
||||
const attributes = Object.entries(attrs)
|
||||
.map(([key, value]) => `${key}="${value}"`)
|
||||
.join(' ');
|
||||
return `<${tag} ${attributes}/>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
return string
|
||||
.replace(/>[\r\n ]+</g, '><')
|
||||
.replace(/(<.*?>)|\s+/g, (m, $1) => $1 || ' ')
|
||||
.trim();
|
||||
});
|
||||
|
||||
const base64InnerSvg = computed(() => {
|
||||
if (typeof window === 'undefined') return '';
|
||||
return btoa(innerSvg.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -55,6 +31,8 @@ const base64InnerSvg = computed(() => {
|
||||
:size="size"
|
||||
:color="color"
|
||||
:strokeWidth="absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth"
|
||||
class="preview-icon"
|
||||
:data-size="activeSize"
|
||||
/>
|
||||
<svg
|
||||
class="icon-grid"
|
||||
@@ -62,6 +40,7 @@ const base64InnerSvg = computed(() => {
|
||||
fill="none"
|
||||
stroke-width="0.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:class="{ show: activeSize === '1000%' }"
|
||||
>
|
||||
<g
|
||||
:key="`grid-${i}`"
|
||||
@@ -83,16 +62,17 @@ const base64InnerSvg = computed(() => {
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<a
|
||||
:href="`https://studio.lucide.dev/edit?value=${encodeURIComponent(base64InnerSvg)}&name=${name}&utm_source=lucide.dev&utm_medium=icon-detail-preview`"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="studio-button"
|
||||
title="Open in Lucide Studio"
|
||||
aria-label="Open in Lucide Studio"
|
||||
>
|
||||
<SquarePenIcon :size="14" />
|
||||
</a>
|
||||
<div class="size-buttons" :class="{ show: activeSize !== '1000%' }">
|
||||
<button class="size-button" @click="activeSize = '1000%'" :class="{ active: activeSize === '1000%' }">
|
||||
1000%
|
||||
</button>
|
||||
<button class="size-button" @click="activeSize = '200%'" :class="{ active: activeSize === '200%' }">
|
||||
200%
|
||||
</button>
|
||||
<button class="size-button" @click="activeSize = '100%'" :class="{ active: activeSize === '100%' }">
|
||||
100%
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -104,56 +84,87 @@ const base64InnerSvg = computed(() => {
|
||||
top: 0;
|
||||
left: 0;
|
||||
stroke: var(--vp-c-divider);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.icon-grid.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
height: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
position: relative;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.icon-container > :deep(svg:not(.icon-grid)) {
|
||||
|
||||
.icon-container:hover .size-buttons {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.icon-container > :deep(svg.preview-icon) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: var(--vp-c-neutral);
|
||||
opacity: 0.8;
|
||||
transition: all 0.3s;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.studio-button {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--vp-c-text-2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s, color 0.2s, background-color 0.2s;
|
||||
z-index: 2;
|
||||
.icon-container > :deep(svg.preview-icon[data-size='1000%']) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.icon-container:hover .studio-button,
|
||||
.studio-button:focus-visible {
|
||||
opacity: 1;
|
||||
.icon-container > :deep(svg.preview-icon[data-size='200%']) {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.studio-button:hover,
|
||||
.studio-button:focus-visible {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
.icon-container > :deep(svg.preview-icon[data-size='100%']) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.icon-component.customizable {
|
||||
will-change: width, height, stroke-width, stroke;
|
||||
/* color: var(--customize-color, currentColor);
|
||||
stroke-width: var(--customize-strokeWidth, 2); */
|
||||
}
|
||||
|
||||
.size-buttons {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 50%;
|
||||
padding: 4px;
|
||||
border-radius: 12px;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
z-index: 99;
|
||||
background: var(--vp-code-tab-bg);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.size-buttons.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.size-button {
|
||||
color: var(--vp-code-tab-text-color);
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.size-button.active {
|
||||
background: var(--vp-c-neutral-inverse);
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
border-radius: 4px;
|
||||
color: var(--vp-code-tab-active-text-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -70,7 +70,6 @@ const prettyName = props.name
|
||||
class="showcase"
|
||||
v-if="iconComponent"
|
||||
>
|
||||
<h2 class="title">See this icon in action</h2>
|
||||
<div class="showcase-grid">
|
||||
<div class="showcase-item column">
|
||||
<div class="placeholder"></div>
|
||||
@@ -236,18 +235,17 @@ const prettyName = props.name
|
||||
.showcase-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 48px;
|
||||
margin-inline-start: 24px;
|
||||
margin-block-start: 48px;
|
||||
gap: 32px;
|
||||
background: var(--vp-code-block-bg);
|
||||
padding: 32px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.showcase-item {
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg);
|
||||
box-shadow:
|
||||
var(--vp-shadow-4),
|
||||
-24px -24px 0 var(--vp-c-bg-soft);
|
||||
box-shadow: var(--vp-shadow-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
const MAX_VISIBLE_TAGS = 5;
|
||||
|
||||
const props = defineProps<{
|
||||
tags?: string[];
|
||||
}>();
|
||||
|
||||
const tags = computed(() => props.tags ?? []);
|
||||
const expanded = ref(false);
|
||||
const hiddenCount = computed(() => Math.max(tags.value.length - MAX_VISIBLE_TAGS, 0));
|
||||
const showAll = computed(() => expanded.value || hiddenCount.value === 0);
|
||||
|
||||
// The overlay reuses this component when switching icons, so collapse again
|
||||
watch(tags, () => {
|
||||
expanded.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul
|
||||
v-if="tags.length"
|
||||
class="icon-tags"
|
||||
>
|
||||
<!-- v-show keeps hidden tags in the rendered HTML so crawlers can index them -->
|
||||
<li
|
||||
v-for="(tag, index) in tags"
|
||||
v-show="showAll || index < MAX_VISIBLE_TAGS"
|
||||
:key="tag"
|
||||
class="tag"
|
||||
>
|
||||
{{ tag }}
|
||||
</li>
|
||||
<li
|
||||
v-if="!showAll"
|
||||
class="more"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="more-button"
|
||||
:aria-label="`Show ${hiddenCount} more tags`"
|
||||
@click="expanded = true"
|
||||
>
|
||||
+{{ hiddenCount }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon-tags {
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
list-style-type: disc;
|
||||
gap: 4px 8px;
|
||||
/* Clip bullets hanging before tags that start a wrapped line, padding leaves room for the focus ring */
|
||||
overflow: hidden;
|
||||
padding: 4px;
|
||||
margin: 4px -4px 12px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin-top: 0;
|
||||
padding-left: 0px;
|
||||
padding-right: 12px;
|
||||
list-style-position: outside;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tag:first-child {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.more {
|
||||
margin-top: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.more-button {
|
||||
font: inherit;
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 12px;
|
||||
padding: 0 8px;
|
||||
margin-left: -4px;
|
||||
border-radius: 16px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
transition:
|
||||
color 0.25s,
|
||||
background-color 0.25s;
|
||||
}
|
||||
|
||||
.more-button:hover,
|
||||
.more-button:focus-visible {
|
||||
color: var(--vp-c-text-1);
|
||||
background-color: var(--vp-c-default-soft);
|
||||
}
|
||||
|
||||
.more-button:focus-visible {
|
||||
outline: 2px solid var(--vp-c-brand-1);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -122,7 +122,7 @@ const categoriesList = computed(() => {
|
||||
});
|
||||
const searchPlaceholder = useSearchPlaceholder(searchQuery, searchResults);
|
||||
const isSearchMetadataLoading = computed(
|
||||
() => searchQuery.value.length > 0 && (tags.value == null || categoriesMap.value == null),
|
||||
() => searchQuery.value.length > 0 && !isFetchingTags && !isFetchingCategories,
|
||||
);
|
||||
|
||||
const { list, containerProps, wrapperProps, scrollTo } = useVirtualList(categoriesList, {
|
||||
@@ -163,7 +163,7 @@ const IconDetailOverlay = defineAsyncComponent(() => import('./IconDetailOverlay
|
||||
function handleCloseDrawer() {
|
||||
setActiveIconName('');
|
||||
|
||||
const url = new URL(window.location);
|
||||
const url = new URL(window.location.href);
|
||||
url.pathname = '/icons/categories';
|
||||
|
||||
if (searchQueryDebounced.value) {
|
||||
|
||||
@@ -35,7 +35,6 @@ const SORTING = [
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
const initialGridItems = computed(() => {
|
||||
if (containerWidth.value === 0) return 120;
|
||||
|
||||
@@ -72,6 +71,8 @@ const sortedIcons = computed(() => {
|
||||
return [...props.icons].sort((a, b) => (b.popularity || 0) - (a.popularity || 0));
|
||||
case 'release-date':
|
||||
return [...props.icons].sort((a, b) => {
|
||||
if (a.awaitingRelease !== b.awaitingRelease) return a.awaitingRelease ? -1 : 1;
|
||||
|
||||
const aDate = a.createdRelease?.date ? new Date(a.createdRelease.date).getTime() : 0;
|
||||
const bDate = b.createdRelease?.date ? new Date(b.createdRelease.date).getTime() : 0;
|
||||
return bDate - aDate;
|
||||
@@ -88,9 +89,13 @@ const mappedIcons = computed(() => {
|
||||
return sortedIcons.value;
|
||||
}
|
||||
|
||||
if (categories.value == null) {
|
||||
return sortedIcons.value;
|
||||
}
|
||||
|
||||
return sortedIcons.value.map((icon) => {
|
||||
const iconTags = tags.value[icon.name];
|
||||
const iconCategories = categories.value?.[icon.name] ?? [];
|
||||
const iconCategories = categories.value[icon.name] ?? [];
|
||||
|
||||
return {
|
||||
...icon,
|
||||
@@ -115,7 +120,7 @@ const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
|
||||
|
||||
const searchPlaceholder = useSearchPlaceholder(searchQuery, searchResults);
|
||||
const isSearchMetadataLoading = computed(
|
||||
() => searchQuery.value.length > 0 && (tags.value == null || categories.value == null),
|
||||
() => searchQuery.value.length > 0 && !isFetchingTags && !isFetchingCategories,
|
||||
);
|
||||
|
||||
const chunkedIcons = computed(() => {
|
||||
@@ -168,7 +173,7 @@ watch(searchQueryDebounced, () => {
|
||||
function handleCloseDrawer() {
|
||||
setActiveIconName('');
|
||||
|
||||
const url = new URL(window.location);
|
||||
const url = new URL(window.location.href);
|
||||
url.pathname = '/icons/';
|
||||
|
||||
if (searchQueryDebounced.value) {
|
||||
@@ -203,6 +208,7 @@ function handleCloseDrawer() {
|
||||
<template #start-icon>
|
||||
<Icon
|
||||
:iconNode="listSortDescending"
|
||||
name="list-sort-descending"
|
||||
class="chevron-icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
@@ -39,7 +39,7 @@ interface Placeholder {
|
||||
finePrint?: string;
|
||||
}
|
||||
|
||||
const brandPlaceholders: Placeholder[] = shallowReadonly([
|
||||
const brandPlaceholders: readonly Placeholder[] = shallowReadonly([
|
||||
{
|
||||
title: 'Boooo! What a scary brand logo!',
|
||||
message:
|
||||
@@ -103,7 +103,7 @@ const brandPlaceholders: Placeholder[] = shallowReadonly([
|
||||
},
|
||||
]);
|
||||
|
||||
const notFoundPlaceholders: Omit<Placeholder, 'title'>[] = shallowReadonly([
|
||||
const notFoundPlaceholders: readonly Omit<Placeholder, 'title'>[] = shallowReadonly([
|
||||
{
|
||||
message: 'We’ve looked for this icon for a bird’s eye view, but could not find it.',
|
||||
icon: markRaw(bird),
|
||||
@@ -130,7 +130,7 @@ const notFoundPlaceholders: Omit<Placeholder, 'title'>[] = shallowReadonly([
|
||||
},
|
||||
]);
|
||||
|
||||
function randomItem<T>(arr: T[]): T {
|
||||
function randomItem<T>(arr: readonly T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ defineProps<{
|
||||
|
||||
<template>
|
||||
<section class="related-icons">
|
||||
<h2 class="title">More icons like this</h2>
|
||||
<IconGrid :icons="icons" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -34,8 +34,8 @@ const sizeCssVar = useCssVar('--customize-size', props.rootEl?.value ?? document
|
||||
});
|
||||
|
||||
syncRef(color, colorCssVar, { direction: 'ltr' });
|
||||
syncRef(strokeWidth, strokeWidthCssVar, { direction: 'ltr' });
|
||||
syncRef(size, sizeCssVar, { direction: 'ltr' });
|
||||
syncRef(strokeWidth, strokeWidthCssVar, { direction: 'ltr', transform: { ltr: String } });
|
||||
syncRef(size, sizeCssVar, { direction: 'ltr', transform: { ltr: String } });
|
||||
|
||||
function resetStyle() {
|
||||
color.value = STYLE_DEFAULTS.color;
|
||||
|
||||
@@ -61,6 +61,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar :deep(.select-button) {
|
||||
padding: 11px 14px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-2);
|
||||
flex-shrink: 0;
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-bar :deep(.select-button):hover,
|
||||
.search-bar :deep(.select-button):focus {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.search-bar :deep(.select-options) {
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.search-bar :deep(.select-option) {
|
||||
padding-right: 36px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.search-bar :deep(.select-icon) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-bar :deep(.select-button) .select-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.search-bar :deep(.select-button) {
|
||||
padding: 11px 16px 11px 16px;
|
||||
}
|
||||
|
||||
.search-bar :deep(.select-button) .select-text {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.search-bar {
|
||||
padding-top: 32px;
|
||||
@@ -68,4 +112,7 @@
|
||||
top: calc(var(--vp-layout-top-height, 0px) + 64px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
color: rgb(var(--text-color) / 1);
|
||||
display: none;
|
||||
top: -85%;
|
||||
background-image: radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
background-image:
|
||||
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
radial-gradient(circle, transparent 20%, var(--confetti-color) 20%, transparent 30%),
|
||||
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
@@ -56,7 +57,8 @@
|
||||
.confetti-button:after {
|
||||
display: none;
|
||||
bottom: -75%;
|
||||
background-image: radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
background-image:
|
||||
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
radial-gradient(circle, transparent 10%, var(--confetti-color) 15%, transparent 20%),
|
||||
radial-gradient(circle, var(--confetti-color) 20%, transparent 20%),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import VPHero from 'vitepress/dist/client/theme-default/components/VPHero.vue'
|
||||
import { useData } from 'vitepress/dist/client/theme-default/composables/data'
|
||||
import { useData } from 'vitepress/dist/client/theme-default/composables/data.js'
|
||||
import FakeInput from '../base/FakeInput.vue';
|
||||
import { useRouter } from 'vitepress';
|
||||
import { data } from '../home/HomeHeroIconsCard.data'
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
sparkles,
|
||||
} from '../../../data/iconNodes';
|
||||
import createElement from 'lucide/src/createElement';
|
||||
import { IconNode } from 'lucide';
|
||||
import { IconNode } from 'lucide/src/types';
|
||||
|
||||
const icons: Component[] = [
|
||||
heart, star, zap, code, feather, cloud, sun, moon, camera, music,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress';
|
||||
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar';
|
||||
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar.js';
|
||||
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue';
|
||||
import { computed } from 'vue';
|
||||
import SurveyAskOverlay from '../SurveyAskOverlay.vue';
|
||||
|
||||
@@ -2,7 +2,7 @@ import packageDataList from '../../../data/packageData.json';
|
||||
import thirdPartyPackages from '../../../data/packageData.thirdParty.json';
|
||||
import fetchPackages from '../../../lib/fetchPackages';
|
||||
|
||||
export default {
|
||||
const loader = {
|
||||
async load() {
|
||||
const packageJsonList = await fetchPackages();
|
||||
|
||||
@@ -29,3 +29,8 @@ export default {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
declare const data: Awaited<ReturnType<typeof loader.load>>;
|
||||
|
||||
export { data };
|
||||
export default loader;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { onMounted, onUpdated, onUnmounted } from 'vue';
|
||||
import { throttleAndDebounce } from 'vitepress/dist/client/theme-default/support/utils';
|
||||
import { throttleAndDebounce } from 'vitepress/dist/client/theme-default/support/utils.js';
|
||||
|
||||
/*
|
||||
* This file is compied and adjusted from vitepress/dist/client/theme-default/composables/useActiveAnchor.ts
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import Fuse from 'fuse.js';
|
||||
import Fuse, { type FuseOptionKeyObject } from 'fuse.js';
|
||||
import { shallowRef, computed, Ref } from 'vue';
|
||||
|
||||
const useSearch = <T>(
|
||||
query: Ref<string>,
|
||||
collection: Ref<T[]>,
|
||||
keys: Fuse.FuseOptionKeyObject<T>[] = [],
|
||||
keys: FuseOptionKeyObject<T>[] = [],
|
||||
) => {
|
||||
const index = shallowRef(
|
||||
new Fuse(collection.value, {
|
||||
threshold: 0.2,
|
||||
useExtendedSearch: true,
|
||||
keys,
|
||||
}),
|
||||
);
|
||||
@@ -17,7 +18,9 @@ const useSearch = <T>(
|
||||
index.value.setCollection(collection.value);
|
||||
|
||||
if (query.value) {
|
||||
return index.value.search(query.value).map((result) => result.item);
|
||||
return index.value
|
||||
.search({ $and: query.value.split(' ').filter((t) => !!t) })
|
||||
.map((result) => result.item);
|
||||
}
|
||||
|
||||
return collection.value;
|
||||
|
||||
@@ -152,6 +152,10 @@ html {
|
||||
.VPHomeHero .container {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.VPDoc:has(.vp-doc[class*=' _icons_']) {
|
||||
padding: 32px 32px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.VPNavBarHamburger .container > span {
|
||||
@@ -183,3 +187,144 @@ html {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.custom-block.success {
|
||||
border-color: transparent;
|
||||
color: var(--vp-c-text-1);
|
||||
background-color: var(--vp-c-success-soft);
|
||||
}
|
||||
/* Example guidance blocks */
|
||||
.example-block {
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
.example-block__title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 20px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.example-block > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.example-block > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.example-block__content > :first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.example-block__content > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.example-guidance__image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
border-bottom: 4px solid var(--vp-c-divider);
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.example-guidance__image--raw svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.example-guidance__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 160px;
|
||||
padding: 24px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 20px;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.example-guidance__text svg.lucide {
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
vertical-align: -6px;
|
||||
}
|
||||
|
||||
.example-guidance__caption {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.example-guidance__caption > p {
|
||||
margin: 0;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.example-guidance__label {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
margin: 0 0 16px;
|
||||
background-color: var(--example-guidance-label-bg);
|
||||
color: var(--vp-c-neutral-inverse);
|
||||
border-bottom-left-radius: 16px;
|
||||
border-bottom-right-radius: 16px;
|
||||
}
|
||||
|
||||
.example-guidance--do {
|
||||
--example-guidance-label-bg: var(--vp-c-success-1);
|
||||
}
|
||||
|
||||
.example-guidance--dont {
|
||||
--example-guidance-label-bg: var(--vp-c-danger-1);
|
||||
}
|
||||
|
||||
.example-guidance--caution {
|
||||
--example-guidance-label-bg: var(--vp-c-warning-1);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.example-block {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.example-block__content {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.example-guidance {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.example-block {
|
||||
grid-template-columns: minmax(0, 0.9fr) repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.example-block--guidance-only {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.example-block__content {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.example-guidance {
|
||||
grid-column: auto;
|
||||
grid-row: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
export type IconNode = [elementName: string, attrs: Record<string, string>][];
|
||||
export type IconNodeWithKeys = [elementName: string, attrs: Record<string, string>, key: string][];
|
||||
|
||||
export interface Alias {
|
||||
name: string;
|
||||
deprecationReason: string;
|
||||
deprecated: boolean;
|
||||
}
|
||||
|
||||
export interface IconMetaData {
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
contributors: string[];
|
||||
aliases?: string[];
|
||||
aliases?: Alias[];
|
||||
deprecated?: boolean;
|
||||
deprecationReason?: string;
|
||||
toBeRemovedInVersion?: string;
|
||||
@@ -18,6 +24,8 @@ export interface IconEntity extends IconMetaData {
|
||||
externalLibrary?: ExternalLibs;
|
||||
createdRelease?: Release;
|
||||
changedRelease?: Release;
|
||||
fromFork?: boolean;
|
||||
awaitingRelease?: boolean;
|
||||
popularity?: number;
|
||||
}
|
||||
|
||||
@@ -43,7 +51,8 @@ export interface PackageItem {
|
||||
packageDirname?: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
iconDark: string;
|
||||
iconDark?: string;
|
||||
iconClass?: string;
|
||||
shields: Shield[];
|
||||
source: string;
|
||||
documentation: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const chunkArray = <ArrayType>(stream: ArrayType, size: number) => {
|
||||
return stream.reduce<ArrayType[][]>(
|
||||
const chunkArray = <ItemType>(stream: ItemType[], size: number) => {
|
||||
return stream.reduce<ItemType[][]>(
|
||||
(chunks, item, idx, arr) =>
|
||||
idx % size == 0 ? [...chunks, arr.slice(idx, idx + size)] : chunks,
|
||||
[],
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { IconNode } from '../types';
|
||||
|
||||
function getInnerSvg(iconNode: IconNode) {
|
||||
return iconNode
|
||||
.map(([tag, attrs]) => {
|
||||
const attributes = Object.entries(attrs)
|
||||
.map(([key, value]) => `${key}="${value}"`)
|
||||
.join(' ');
|
||||
return `<${tag} ${attributes}/>`;
|
||||
})
|
||||
.join('')
|
||||
.replace(/>[\r\n ]+</g, '><')
|
||||
.replace(/(<.*?>)|\s+/g, (m, $1) => $1 || ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export default function getStudioLink(
|
||||
name: string,
|
||||
iconNode: IconNode,
|
||||
utmMedium = 'icon-detail-preview',
|
||||
) {
|
||||
const base64InnerSvg = btoa(getInnerSvg(iconNode ?? []));
|
||||
|
||||
return `https://studio.lucide.dev/edit?value=${encodeURIComponent(base64InnerSvg)}&name=${name}&utm_source=lucide.dev&utm_medium=${utmMedium}`;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export default function useSearchPlaceholder(
|
||||
}
|
||||
}
|
||||
state.value = {
|
||||
isNoResults: query in BRAND_STOPWORDS && searchResults.length === 0 && query !== '',
|
||||
isNoResults: query !== '' && searchResults.length === 0,
|
||||
isBrand: query in BRAND_STOPWORDS,
|
||||
query: BRAND_STOPWORDS[query] ?? query,
|
||||
};
|
||||
|
||||
Vendored
+5
@@ -1,3 +1,4 @@
|
||||
/// <reference types="vitepress/client" />
|
||||
import { type IconNode } from '@lucide/vue/src/types';
|
||||
import Vue from 'vue';
|
||||
|
||||
@@ -31,7 +32,11 @@ declare module '*.node.json' {
|
||||
}
|
||||
|
||||
declare global {
|
||||
// Defined by VitePress at build time, used by its VPDocAsideCarbonAds component
|
||||
const __CARBON__: boolean;
|
||||
|
||||
interface Window {
|
||||
_carbonads?: unknown;
|
||||
ExpoSnack?: {
|
||||
/**
|
||||
* Initialize all snack players on the page
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ Contribution in form of code or new icons is always welcome, but you can also he
|
||||
|
||||
### Design New Icons
|
||||
|
||||
If you have design skills, consider contributing new icons to the Lucide library. Check out our [icon design guide](/contribute/icon-design-guide.md) for tips and guidelines on creating icons that fit the Lucide style.
|
||||
If you have design skills, consider contributing new icons to the Lucide library. Check out our [icon design guide](/contribute/icons/design-principles.md) for tips and guidelines on creating icons that fit the Lucide style.
|
||||
|
||||
### Contribute Code
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
title: Affinity Designer Template Guide
|
||||
---
|
||||
|
||||
# Affinity Designer Template Guide
|
||||
|
||||
This guide describes how to use the Affinity Designer template for Lucide.
|
||||
|
||||
## General Workflow
|
||||
|
||||
>Attention: By default, Affinity Designer sets the unit for stroke to points. Make sure that it is set to pixel. To do this, open `Preferences > User Interface`. Under `Decimal Places for Unit Types`, uncheck `Show Lines in points`.
|
||||
|
||||
1. Download and open the [Affinity Designer template](https://github.com/lucide-icons/lucide/blob/main/docs/public/templates/affinity_designer.aftemplate).
|
||||
2. Follow the [Icon Design Principles](icon-design-guide.md) while you use the template (to ensure integrity with the Lucide icon pack).
|
||||
3. Export the file as SVG (`File > Export`). Make sure that _Rastering_ is set to _Nothing_, _Export text as curves_ is checked (hopefully, you won't need this), _Use hex colors_ is checked, and _Flatten transforms_ is checked.
|
||||
|
||||

|
||||
4. Optimize the exported SVG file further with [SVGOMG](https://jakearchibald.github.io/svgomg/) or [`svgo`](https://github.com/svg/svgo) (using `svgo --multipass exported_icon.svg`).
|
||||
@@ -1,51 +0,0 @@
|
||||
---
|
||||
title: Figma Template Guide
|
||||
---
|
||||
|
||||
# Figma Template Guide
|
||||
|
||||
This guide shows the steps to setup Figma for creating icons that conform to the Featherity design guidelines.
|
||||
|
||||
## Setting Up The Frame
|
||||
When you create a new document in Figma, the document. Each individual icon you want to create, has to be created in a separate frame.
|
||||
|
||||
To do this, create a frame of 24x24 pixels.
|
||||
|
||||
1. Click the frame button (or press `F`)
|
||||
2. Draw a 24x24 frame (or edit it afterwards from the design window)
|
||||
|
||||
In this newly created frame, you will create your icon. If you want, you can change the name of your frame to the name of the icon you are going to create. Then it will be exported as `FRAME-NAME.svg`.
|
||||
|
||||
## Create Your icon
|
||||
To design your icon in the style of Feather Icons, you need to adjust a few settings in Figma.
|
||||
|
||||
Draw in your new frame with the pen tool. You can open it with the window at the top, or with the shortcut `P`. Once you click in your frame, you can adjust the settings for the pen tool in the design-window on the right.
|
||||
|
||||
Set the following:
|
||||
1. Vector
|
||||
1. Corner radius: 2px
|
||||
2. Stroke
|
||||
1. Stroke width: 2px
|
||||
2. Stroke alignment: center
|
||||
|
||||

|
||||
|
||||
## Export Or Copy Your Icon
|
||||
Once you have completed your icon, you can export it.
|
||||
|
||||
1. Select the frame
|
||||
2. Open the *Export* tab on the right
|
||||
3. Set the file type as SVG
|
||||
4. Press export
|
||||
|
||||
Or you can also copy its source as SVG.
|
||||
|
||||
1. Select the frame
|
||||
2. Right click it
|
||||
3. Click on *Copy/Paste as*
|
||||
4. Click on *Copy as SVG*
|
||||
|
||||
That's it. You just made your first icon. Congratulations!
|
||||
|
||||
## Figma Tips
|
||||
1. The [Icon Design Guidelines](icon-design-guide.md) dictate that you keep 2px spacing between detached elements. In Figma, you can easily check this with: `⌥` Option (MacOS) or `Alt` (Windows).
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user