mirror of
https://github.com/lucide-icons/lucide.git
synced 2026-11-10 13:18:25 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e37a6c4ec |
@@ -10,4 +10,3 @@ touchpad
|
||||
ungroup
|
||||
toc
|
||||
sunlounger
|
||||
monstera
|
||||
|
||||
@@ -57,16 +57,6 @@ 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:
|
||||
|
||||
@@ -1,587 +0,0 @@
|
||||
---
|
||||
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,11 +30,6 @@
|
||||
- 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,7 +64,6 @@ 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
|
||||
@@ -103,7 +102,6 @@ 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 }}
|
||||
@@ -131,30 +129,3 @@ 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"
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
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,19 +46,3 @@ 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,19 +37,3 @@ 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,7 +3,6 @@ name: Lucide Lab checks
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- lab/**
|
||||
- packages/lab/**
|
||||
- packages/shared/**
|
||||
- tools/build-icons/**
|
||||
@@ -29,19 +28,3 @@ 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,19 +31,3 @@ 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,19 +47,3 @@ 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,13 +7,10 @@ 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:
|
||||
@@ -51,38 +48,3 @@ 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,20 +27,4 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: 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
|
||||
run: pnpm --filter lucide-react test
|
||||
|
||||
@@ -47,19 +47,3 @@ 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,19 +26,3 @@ 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: Typecheck
|
||||
- name: Test
|
||||
run: pnpm --filter @lucide/svelte typecheck
|
||||
|
||||
@@ -47,19 +47,3 @@ 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,19 +46,3 @@ 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,82 @@
|
||||
name: Pull request missing issue
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
paths:
|
||||
- 'icons/*.svg'
|
||||
|
||||
permissions: {}
|
||||
|
||||
# Two events landing close together (a push right after opening) would
|
||||
# otherwise both find no existing comment and post one each.
|
||||
concurrency:
|
||||
group: missing-issue-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pull-request-missing-issue:
|
||||
name: Pull Request Missing Issue
|
||||
if: github.repository == 'lucide-icons/lucide' && github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write # Required to post and update the comment on the pull request
|
||||
|
||||
steps:
|
||||
# We checkout the main branch of main repository for security reasons.
|
||||
# The script never runs code from the pull request, it only reads the
|
||||
# pull request's icons through the GitHub API.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: lucide-icons/lucide
|
||||
- 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 comment markup
|
||||
id: markup
|
||||
run: node ./scripts/generateMissingIssueComment.mts
|
||||
env:
|
||||
PULL_REQUEST_NUMBER: ${{ github.event.number }}
|
||||
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Find comment
|
||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
||||
id: pr-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: lucide-missing-issue-comment
|
||||
|
||||
- name: Post missing issue comment
|
||||
if: steps.markup.outputs.action == 'nag'
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
with:
|
||||
comment-id: ${{ steps.pr-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.number }}
|
||||
body-path: missing-issue-comment.md
|
||||
edit-mode: replace
|
||||
|
||||
# Linking an issue from the pull request's Development sidebar fires no
|
||||
# event, so the comment resolves on the next edit or push instead.
|
||||
- name: Resolve missing issue comment
|
||||
if: steps.markup.outputs.action == 'resolve' && steps.pr-comment.outputs.comment-id != ''
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
with:
|
||||
comment-id: ${{ steps.pr-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.number }}
|
||||
body: |
|
||||
<!-- lucide-missing-issue-comment -->
|
||||
Thanks for linking an issue for this icon design! ✅
|
||||
edit-mode: replace
|
||||
@@ -1,146 +0,0 @@
|
||||
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
|
||||
+1
-10
@@ -1,13 +1,11 @@
|
||||
.DS_Store
|
||||
.idea
|
||||
.next
|
||||
.astro
|
||||
.obsidian
|
||||
.now
|
||||
.cache
|
||||
.idea
|
||||
.env
|
||||
node_modules
|
||||
.pnpm-store
|
||||
dist
|
||||
build
|
||||
sandbox
|
||||
@@ -15,10 +13,6 @@ stash
|
||||
coverage
|
||||
stats
|
||||
*.log
|
||||
*.tsbuildinfo
|
||||
integrations/**/.react-router
|
||||
integrations/**/.vitest-attachments
|
||||
integrations/**/__screenshots__
|
||||
outlined
|
||||
lucide-font
|
||||
packages/**/src/icons/*.js
|
||||
@@ -29,9 +23,6 @@ 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,2 +1,5 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
pnpm lint-staged
|
||||
pnpm checkIcons
|
||||
|
||||
@@ -19,13 +19,6 @@ 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,12 +1,13 @@
|
||||
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.map(([name, { key, ...attrs }]) => {
|
||||
const newIconNode = (iconNode as IconNodeWithKeys).map(([name, { key, ...attrs }]) => {
|
||||
return [name, attrs];
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { eventHandler, getQuery, setResponseHeader } from 'h3';
|
||||
import iconNodes from '../../data/iconNodes';
|
||||
import { IconNodeWithKeys } from '../../theme/types';
|
||||
|
||||
export default eventHandler((event) => {
|
||||
const query = getQuery(event);
|
||||
@@ -18,7 +19,7 @@ export default eventHandler((event) => {
|
||||
return [name, iconNode];
|
||||
}
|
||||
|
||||
const newIconNode = iconNode.map(([name, { key, ...attrs }]) => {
|
||||
const newIconNode = (iconNode as IconNodeWithKeys).map(([name, { key, ...attrs }]) => {
|
||||
return [name, attrs];
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ 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';
|
||||
@@ -32,7 +31,6 @@ export default defineConfig({
|
||||
config(md) {
|
||||
md.use(groupIconMdPlugin);
|
||||
md.use(snackPlayer);
|
||||
md.use(examplePlugin);
|
||||
md.use(sandpackPlugin, {
|
||||
defaultFiles: {
|
||||
'/styles.css': {
|
||||
@@ -97,9 +95,8 @@ export default defineConfig({
|
||||
{
|
||||
text: 'Resources',
|
||||
items: [
|
||||
...resourcesSidebar[0].items.map(({ text, link }) => ({ text, link })),
|
||||
{ text: 'How to use icons', link: '/how-to/' },
|
||||
{ text: 'Contributing icons', link: '/contribute/icons/' },
|
||||
...resourcesSidebar[0].items,
|
||||
{ text: 'Design icons', link: '/contribute/icon-design-guide' },
|
||||
],
|
||||
},
|
||||
{ text: 'Packages', link: '/packages' },
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SVGProps } from 'react';
|
||||
import { getCommands } from './utils';
|
||||
|
||||
export type Point = { x: number; y: number };
|
||||
@@ -14,8 +15,8 @@ export type Path = {
|
||||
};
|
||||
|
||||
export type PathProps<
|
||||
RequiredProps extends keyof React.SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
|
||||
NeverProps extends keyof React.SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
|
||||
RequiredProps extends keyof SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
|
||||
NeverProps extends keyof SVGProps<SVGPathElement | SVGRectElement | SVGCircleElement>,
|
||||
> = Required<Pick<React.SVGProps<SVGElement & SVGRectElement & SVGCircleElement>, RequiredProps>> &
|
||||
Omit<
|
||||
React.SVGProps<SVGPathElement & SVGRectElement & SVGCircleElement>,
|
||||
|
||||
@@ -53,7 +53,10 @@ export const getCommands = (src: string) =>
|
||||
getNodes(src)
|
||||
.map(convertToPathNode)
|
||||
.flatMap(({ d, name }, idx) =>
|
||||
new SVGPathData(d).toAbs().commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name })),
|
||||
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 })),
|
||||
);
|
||||
|
||||
const getPaths = (src: string) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Category, IconEntity } from '../theme/types';
|
||||
|
||||
const directory = path.join(process.cwd(), '../categories');
|
||||
|
||||
export function getAllCategoryFiles(): Omit<Category, 'iconCount'>[] {
|
||||
export function getAllCategoryFiles(): Category[] {
|
||||
const fileNames = fs.readdirSync(directory).filter((file) => path.extname(file) === '.json');
|
||||
|
||||
return fileNames.map((fileName) => {
|
||||
@@ -21,9 +21,9 @@ export function getAllCategoryFiles(): Omit<Category, 'iconCount'>[] {
|
||||
}
|
||||
|
||||
export function mapCategoryIconCount(
|
||||
categories: Omit<Category, 'iconCount'>[],
|
||||
categories: Category[],
|
||||
icons: { categories: IconEntity['categories'] }[],
|
||||
): Category[] {
|
||||
) {
|
||||
return categories.map((category) => ({
|
||||
...category,
|
||||
iconCount: icons.reduce(
|
||||
|
||||
@@ -119,7 +119,8 @@ 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,7 +2,8 @@ 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,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import createLucideIcon from 'lucide-react/src/createLucideIcon';
|
||||
import { createLucideIcon } from 'lucide-react/src/lucide-react';
|
||||
import { type LucideProps, type IconNode } from 'lucide-react/src/types';
|
||||
import { IconEntity } from '../theme/types';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
|
||||
@@ -20,8 +20,6 @@ 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,
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
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,4 +1,6 @@
|
||||
import type { MarkdownIt, RenderRule, Token } from './types';
|
||||
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 container from 'markdown-it-container';
|
||||
import sandpackTheme from '../theme/sandpackTheme.json';
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* />
|
||||
*/
|
||||
|
||||
import type { MarkdownIt } from './types';
|
||||
import type MarkdownIt from 'markdown-it';
|
||||
|
||||
type SnackParams = Record<string, string>;
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
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: '/how-to/accessibility',
|
||||
link: '/guide/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -69,7 +69,7 @@ export const astroSidebar = [
|
||||
items: [
|
||||
{
|
||||
text: 'Accessibility in depth',
|
||||
link: '/how-to/accessibility',
|
||||
link: '/guide/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -36,7 +36,6 @@ 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: '/how-to/accessibility',
|
||||
link: '/guide/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -89,7 +89,7 @@ export const preactSidebar = [
|
||||
items: [
|
||||
{
|
||||
text: 'Accessibility in depth',
|
||||
link: '/how-to/accessibility',
|
||||
link: '/guide/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -100,7 +100,7 @@ export const reactSidebar = [
|
||||
items: [
|
||||
{
|
||||
text: 'Accessibility in depth',
|
||||
link: '/how-to/accessibility',
|
||||
link: '/guide/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,110 +11,41 @@ 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: 'Contribution guide',
|
||||
link: '/contribute/',
|
||||
text: 'Contributing',
|
||||
link: '/contribute',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'How to use Lucide icons',
|
||||
text: 'Designing icons',
|
||||
items: [
|
||||
{
|
||||
text: 'Getting started',
|
||||
link: '/how-to/',
|
||||
text: 'Icon Design Principles',
|
||||
link: '/contribute/icon-design-guide',
|
||||
},
|
||||
{
|
||||
text: 'Accessibility guide',
|
||||
link: '/how-to/accessibility',
|
||||
desc: 'Learn how to make icons readable, understandable, and usable with assistive technology.',
|
||||
text: 'Designing in Illustrator',
|
||||
link: '/contribute/illustrator-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 Inkscape',
|
||||
link: '/contribute/inkscape-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 Figma',
|
||||
link: '/contribute/figma-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',
|
||||
},
|
||||
],
|
||||
text: 'Designing in Affinity Designer',
|
||||
link: '/contribute/affinity-designer-guide',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -90,7 +90,7 @@ export const solidSidebar = [
|
||||
items: [
|
||||
{
|
||||
text: 'Accessibility in depth',
|
||||
link: '/how-to/accessibility',
|
||||
link: '/guide/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -75,7 +75,7 @@ export const lucideStaticSidebar = [
|
||||
items: [
|
||||
{
|
||||
text: 'Accessibility in depth',
|
||||
link: '/how-to/accessibility',
|
||||
link: '/guide/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -79,7 +79,7 @@ export const svelteSidebar = [
|
||||
items: [
|
||||
{
|
||||
text: 'Accessibility in depth',
|
||||
link: '/how-to/accessibility',
|
||||
link: '/guide/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -85,7 +85,7 @@ export const vueSidebar = [
|
||||
items: [
|
||||
{
|
||||
text: 'Accessibility in depth',
|
||||
link: '/how-to/accessibility',
|
||||
link: '/guide/accessibility',
|
||||
desc: 'Accessibility best practices',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
import Button from './base/Button.vue';
|
||||
import IconButton from './base/IconButton.vue';
|
||||
import { x } from '../../data/iconNodes';
|
||||
import Icon from '@lucide/vue/src/Icon';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
@@ -65,17 +65,16 @@ function hidePopup() {
|
||||
}"
|
||||
class="ask-overlay"
|
||||
>
|
||||
<Button
|
||||
<IconButton
|
||||
@click="hidePopup"
|
||||
class="hide-button"
|
||||
>
|
||||
<Icon
|
||||
:iconNode="x"
|
||||
name="x"
|
||||
:size="20"
|
||||
absoluteStrokeWidth
|
||||
/>
|
||||
</Button>
|
||||
</IconButton>
|
||||
|
||||
<h2 class="title">
|
||||
We value your feedback!
|
||||
|
||||
+3
-43
@@ -1,38 +1,7 @@
|
||||
<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>
|
||||
<component
|
||||
:is="component"
|
||||
v-bind="$attrs"
|
||||
:href="href"
|
||||
:target="target"
|
||||
:rel="rel"
|
||||
class="icon-button"
|
||||
>
|
||||
<button v-bind="$attrs" class="icon-button">
|
||||
<slot />
|
||||
</component>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -47,21 +16,12 @@ const rel = computed(() => {
|
||||
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; */
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
font-size: 24px;
|
||||
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 Button from './Button.vue';
|
||||
import IconButton from './IconButton.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 as HTMLInputElement).value)"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
<Button
|
||||
<IconButton
|
||||
@click="onClear"
|
||||
v-if="type === 'search' && modelValue"
|
||||
class="clear-button"
|
||||
@@ -79,10 +79,9 @@ defineExpose({
|
||||
>
|
||||
<Icon
|
||||
:iconNode="x"
|
||||
name="x"
|
||||
:size="20"
|
||||
/>
|
||||
</Button>
|
||||
</IconButton>
|
||||
<kbd
|
||||
v-if="shortcut"
|
||||
class="shortcut"
|
||||
|
||||
@@ -46,7 +46,6 @@ const value = computed({
|
||||
<template #icon>
|
||||
<Icon
|
||||
:iconNode="search"
|
||||
name="search"
|
||||
class="search-icon"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -9,8 +9,7 @@ import RocketDuotoneIcon from '../home/RocketDuotoneIcon.ts';
|
||||
<p>
|
||||
<Icon
|
||||
:iconNode="RocketDuotoneIcon"
|
||||
name="rocket-duotone"
|
||||
:size="18"
|
||||
size="18"
|
||||
class="rocket-icon"
|
||||
/>
|
||||
<VPLink
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
<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 as HTMLInputElement).value))"
|
||||
v-on:input="$emit('update:modelValue', Number($event.target.value))"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { rotateCw } from '../../../data/iconNodes';
|
||||
import Icon from '@lucide/vue/src/Icon';
|
||||
import Button from './Button.vue';
|
||||
import IconButton from './IconButton.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button class="reset-button">
|
||||
<IconButton class="reset-button">
|
||||
<Icon
|
||||
:size="20"
|
||||
:iconNode="rotateCw"
|
||||
name="rotate-cw"
|
||||
/>
|
||||
</Button>
|
||||
</IconButton>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -27,7 +27,6 @@ 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"
|
||||
/>
|
||||
@@ -58,7 +57,6 @@ 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/src/types';
|
||||
import { type LucideProps } from '@lucide/vue';
|
||||
import { Sandpack } from 'sandpack-vue3';
|
||||
|
||||
defineProps<FunctionalComponent<LucideProps>>();
|
||||
|
||||
@@ -10,14 +10,7 @@ import { useLocalStorage } from '@vueuse/core';
|
||||
const { page } = useData();
|
||||
const router = useRouter();
|
||||
|
||||
interface Framework {
|
||||
name: string;
|
||||
icon: string;
|
||||
iconDark?: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const frameworks: Framework[] = [
|
||||
const frameworks = [
|
||||
{ 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/' },
|
||||
@@ -55,7 +48,7 @@ const selected = computed(() => {
|
||||
return current || (fallbackFramework.value?.value ? fallbackFramework.value : frameworks[0]);
|
||||
});
|
||||
|
||||
function onSelectFramework(item: Framework) {
|
||||
function onSelectFramework(item: { name: string; icon: string; iconDark?: string; value: string }) {
|
||||
fallbackFramework.value = item;
|
||||
if (item.value !== router.route.path) {
|
||||
const likeRoute = router.route.path.replace(selected.value.value, item.value);
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
import { ref, shallowRef } from 'vue';
|
||||
import { data } from './HomeHeroIconsCard.data';
|
||||
import { useRouter } from 'vitepress';
|
||||
import { motion, useScroll, useTransform, type Options as MotionOptions } from 'motion-v';
|
||||
import { motion, Variants, useScroll, useTransform } 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);
|
||||
@@ -132,7 +129,7 @@ const onAnimationComplete = (item) => {
|
||||
|
||||
const randomIndex = ref(Math.floor(Math.random() * 64));
|
||||
|
||||
const iconAnimationVariants: Variants = {
|
||||
const iconAnimationVariants = {
|
||||
initial: {
|
||||
animationName: 'end',
|
||||
opacity: 0,
|
||||
@@ -152,7 +149,7 @@ const iconAnimationVariants: Variants = {
|
||||
|
||||
const shrinkIconAnimation = ref('initial');
|
||||
|
||||
const shrinkIconVariants: Variants = {
|
||||
const shrinkIconVariants = {
|
||||
initial: { strokeWidth: 2 },
|
||||
shrinkIcons: (index) => ({
|
||||
animationName: 'shrinkIcons',
|
||||
|
||||
@@ -18,7 +18,7 @@ const pickIcons = [
|
||||
'clapperboard',
|
||||
'tool-case',
|
||||
'printer',
|
||||
'square-bookmark',
|
||||
'album',
|
||||
'pencil-ruler',
|
||||
'calendar',
|
||||
'brain',
|
||||
@@ -53,7 +53,7 @@ const pickIcons = [
|
||||
'terminal',
|
||||
'thermometer',
|
||||
'thumbs-up',
|
||||
'trash',
|
||||
'trash-2',
|
||||
'umbrella',
|
||||
'watch',
|
||||
'sparkles',
|
||||
@@ -69,7 +69,7 @@ const pickIcons = [
|
||||
'stethoscope',
|
||||
];
|
||||
|
||||
const loader = {
|
||||
export default {
|
||||
async load() {
|
||||
const icons = Object.entries(iconNodes).map(([name, iconNode]) => ({ name, iconNode }));
|
||||
|
||||
@@ -96,8 +96,3 @@ const loader = {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
declare const data: Awaited<ReturnType<typeof loader.load>>;
|
||||
|
||||
export { data };
|
||||
export default loader;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const loader = {
|
||||
export default {
|
||||
async load() {
|
||||
const version = await fetch('https://api.github.com/repos/lucide-icons/lucide/releases/latest')
|
||||
.then((res) => {
|
||||
@@ -16,8 +16,3 @@ const loader = {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
declare const data: Awaited<ReturnType<typeof loader.load>>;
|
||||
|
||||
export { data };
|
||||
export default loader;
|
||||
|
||||
@@ -15,11 +15,10 @@ import RocketDuotoneIcon from './RocketDuotoneIcon.ts';
|
||||
class="badge-special confetti-button animate"
|
||||
href="/guide/version-1"
|
||||
>
|
||||
<Icon :iconNode="RocketDuotoneIcon" name="rocket-duotone" />
|
||||
<Icon :iconNode="RocketDuotoneIcon" />
|
||||
Version 1
|
||||
<Icon
|
||||
:iconNode="moveRight"
|
||||
name="move-right"
|
||||
class=""
|
||||
/>
|
||||
</Badge>
|
||||
|
||||
@@ -41,8 +41,8 @@ const sizeCssVar = useCssVar(
|
||||
)
|
||||
|
||||
syncRef(color, colorCssVar)
|
||||
syncRef(strokeWidth, strokeWidthCssVar, { transform: { ltr: String, rtl: Number } })
|
||||
syncRef(size, sizeCssVar, { transform: { ltr: String, rtl: Number } })
|
||||
syncRef(strokeWidth, strokeWidthCssVar)
|
||||
syncRef(size, sizeCssVar)
|
||||
|
||||
function resetStyle () {
|
||||
color.value = 'currentColor'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const loader = {
|
||||
export default {
|
||||
async load() {
|
||||
return {
|
||||
packages: [
|
||||
@@ -61,8 +61,3 @@ const loader = {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
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,6 +1,4 @@
|
||||
import type { LucideIconNode } from '@lucide/vue/src/types';
|
||||
|
||||
const RocketDuotoneIcon: LucideIconNode[] = [
|
||||
const RocketDuotoneIcon = [
|
||||
[
|
||||
'path',
|
||||
{
|
||||
@@ -18,6 +16,6 @@ const RocketDuotoneIcon: LucideIconNode[] = [
|
||||
'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 Button from '../base/Button.vue';
|
||||
import IconButton from '../base/IconButton.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,17 +40,16 @@ onMounted(() => {
|
||||
class="floating-ad"
|
||||
v-if="theme.carbonAds"
|
||||
>
|
||||
<Button
|
||||
<IconButton
|
||||
@click="hideAd"
|
||||
class="hide-button"
|
||||
>
|
||||
<Icon
|
||||
:iconNode="x"
|
||||
name="x"
|
||||
:size="20"
|
||||
absoluteStrokeWidth
|
||||
/>
|
||||
</Button>
|
||||
</IconButton>
|
||||
<VPDocAsideCarbonAds :carbon-ads="theme.carbonAds" overlay />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { getAllCategoryFiles, mapCategoryIconCount } from '../../../lib/categories';
|
||||
import iconsMetaData from '../../../data/iconMetaData';
|
||||
|
||||
const loader = {
|
||||
export default {
|
||||
async load() {
|
||||
const categories = mapCategoryIconCount(getAllCategoryFiles(), Object.values(iconsMetaData));
|
||||
let categories = getAllCategoryFiles();
|
||||
|
||||
categories = mapCategoryIconCount(categories, 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.js'
|
||||
import { isActive } from 'vitepress/dist/client/shared'
|
||||
import { useActiveAnchor } from '../../composables/useActiveAnchor'
|
||||
import { data } from './CategoryList.data'
|
||||
import CategoryListItem from './CategoryListItem.vue'
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<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,15 +30,10 @@ const props = defineProps<{
|
||||
.contributors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: start;
|
||||
align-items: center;
|
||||
/* justify-content: flex-end; */
|
||||
gap: 2px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.contributors .label {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.avatar-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -16,17 +16,19 @@ const tags = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IconPreview
|
||||
:name="$params.name"
|
||||
:iconNode="$params.iconNode"
|
||||
class="preview"
|
||||
customizable
|
||||
/>
|
||||
<div class="details">
|
||||
<h1 class="title">
|
||||
{{ $params.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<!-- <PageContainer class="overview"> -->
|
||||
<IconPreview
|
||||
:name="$params.name"
|
||||
:iconNode="$params.iconNode"
|
||||
class="preview"
|
||||
customizable
|
||||
/>
|
||||
<div class="details">
|
||||
<h1 class="title">
|
||||
{{ $params.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<!-- </PageContainer> -->
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -6,11 +6,7 @@ import useConfetti from '../../composables/useConfetti';
|
||||
const { animate, confetti } = useConfetti();
|
||||
const slots = useSlots();
|
||||
|
||||
const copiedText = computed(() => {
|
||||
const children = slots.default?.()[0].children;
|
||||
|
||||
return typeof children === 'string' ? children : '';
|
||||
});
|
||||
const copiedText = computed(() => slots.default?.()[0].children);
|
||||
|
||||
function copyText() {
|
||||
navigator.clipboard.writeText(copiedText.value);
|
||||
@@ -29,7 +25,6 @@ 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 Button from '../base/Button.vue';
|
||||
import IconButton from '../base/IconButton.vue';
|
||||
import IconContributors from './IconContributors.vue';
|
||||
import IconPreview from './IconPreview.vue';
|
||||
import { x, expand } from '../../../data/iconNodes';
|
||||
@@ -12,7 +12,6 @@ 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;
|
||||
@@ -71,19 +70,12 @@ const Expand = createLucideIcon('Expand', expand);
|
||||
<div class="overlay-panel">
|
||||
<nav class="overlay-menu">
|
||||
<Badge
|
||||
v-if="icon.awaitingRelease"
|
||||
class="version awaiting-release"
|
||||
>Unreleased</Badge
|
||||
>
|
||||
<Badge
|
||||
v-else-if="icon.createdRelease"
|
||||
v-if="icon.createdRelease"
|
||||
class="version"
|
||||
:href="releaseTagLink(icon.createdRelease.version)"
|
||||
>v{{ icon.createdRelease.version }}</Badge
|
||||
>
|
||||
{{ icon.createdRelease.version }}
|
||||
</Badge>
|
||||
<EditInStudioButton :icon="icon"/>
|
||||
<Button
|
||||
<IconButton
|
||||
@click="
|
||||
go(
|
||||
icon.externalLibrary
|
||||
@@ -93,10 +85,10 @@ const Expand = createLucideIcon('Expand', expand);
|
||||
"
|
||||
>
|
||||
<component :is="Expand" />
|
||||
</Button>
|
||||
<Button @click="onClose">
|
||||
</IconButton>
|
||||
<IconButton @click="onClose">
|
||||
<component :is="CloseIcon" />
|
||||
</Button>
|
||||
</IconButton>
|
||||
</nav>
|
||||
<IconPreview
|
||||
id="previewer"
|
||||
@@ -208,14 +200,7 @@ 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,36 +10,30 @@ 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 as IconDeprecationReason, {
|
||||
return deprecationReasonTemplate(props.icon.deprecationReason, {
|
||||
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>
|
||||
@@ -66,7 +60,14 @@ const iconPath = computed(() => {
|
||||
Deprecated
|
||||
</Badge>
|
||||
</div>
|
||||
<IconTags :tags="props.icon.tags" />
|
||||
<div
|
||||
class="tags-scroller"
|
||||
v-if="tags.length"
|
||||
>
|
||||
<p class="icon-tags horizontal-scroller">
|
||||
{{ tags }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="group">
|
||||
<Badge
|
||||
v-for="category in icon.categories"
|
||||
@@ -79,10 +80,26 @@ const iconPath = computed(() => {
|
||||
|
||||
<div class="group buttons">
|
||||
<VPButton
|
||||
v-if="!page?.relativePath?.startsWith?.(iconPath)"
|
||||
:href="iconPath"
|
||||
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}`
|
||||
"
|
||||
text="See in action"
|
||||
@click="go(iconPath)"
|
||||
@click="
|
||||
go(
|
||||
icon.externalLibrary
|
||||
? `/icons/${icon.externalLibrary}/${icon.name}`
|
||||
: `/icons/${icon.name}`,
|
||||
)
|
||||
"
|
||||
/>
|
||||
<CopySVGButton
|
||||
:name="icon.name"
|
||||
@@ -98,38 +115,6 @@ const iconPath = 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;
|
||||
@@ -171,6 +156,66 @@ const iconPath = 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,6 +3,7 @@ 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'];
|
||||
@@ -10,8 +11,6 @@ const props = defineProps<{
|
||||
customizable?: boolean;
|
||||
}>();
|
||||
|
||||
const activeSize = ref('1000%');
|
||||
|
||||
const { size, color, strokeWidth, absoluteStrokeWidth } = useIconStyleContext();
|
||||
const previewIcon = ref();
|
||||
|
||||
@@ -21,6 +20,31 @@ 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>
|
||||
@@ -31,8 +55,6 @@ const iconComponent = computed(() => {
|
||||
:size="size"
|
||||
:color="color"
|
||||
:strokeWidth="absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth"
|
||||
class="preview-icon"
|
||||
:data-size="activeSize"
|
||||
/>
|
||||
<svg
|
||||
class="icon-grid"
|
||||
@@ -40,7 +62,6 @@ const iconComponent = computed(() => {
|
||||
fill="none"
|
||||
stroke-width="0.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:class="{ show: activeSize === '1000%' }"
|
||||
>
|
||||
<g
|
||||
:key="`grid-${i}`"
|
||||
@@ -62,17 +83,16 @@ const iconComponent = computed(() => {
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<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>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@@ -84,87 +104,56 @@ const iconComponent = 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:hover .size-buttons {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.icon-container > :deep(svg.preview-icon) {
|
||||
.icon-container > :deep(svg:not(.icon-grid)) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: var(--vp-c-neutral);
|
||||
opacity: 0.8;
|
||||
transition: all 0.3s;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.icon-container > :deep(svg.preview-icon[data-size='1000%']) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.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='200%']) {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
.icon-container:hover .studio-button,
|
||||
.studio-button:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.icon-container > :deep(svg.preview-icon[data-size='100%']) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
.studio-button:hover,
|
||||
.studio-button:focus-visible {
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.icon-component.customizable {
|
||||
will-change: width, height, stroke-width, stroke;
|
||||
}
|
||||
|
||||
.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;
|
||||
/* color: var(--customize-color, currentColor);
|
||||
stroke-width: var(--customize-strokeWidth, 2); */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -70,6 +70,7 @@ 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>
|
||||
@@ -235,17 +236,18 @@ const prettyName = props.name
|
||||
.showcase-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 32px;
|
||||
background: var(--vp-code-block-bg);
|
||||
padding: 32px;
|
||||
border-radius: 8px;
|
||||
gap: 48px;
|
||||
margin-inline-start: 24px;
|
||||
margin-block-start: 48px;
|
||||
}
|
||||
|
||||
.showcase-item {
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg);
|
||||
box-shadow: var(--vp-shadow-2);
|
||||
box-shadow:
|
||||
var(--vp-shadow-4),
|
||||
-24px -24px 0 var(--vp-c-bg-soft);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
<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 && !isFetchingTags && !isFetchingCategories,
|
||||
() => searchQuery.value.length > 0 && (tags.value == null || categoriesMap.value == null),
|
||||
);
|
||||
|
||||
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.href);
|
||||
const url = new URL(window.location);
|
||||
url.pathname = '/icons/categories';
|
||||
|
||||
if (searchQueryDebounced.value) {
|
||||
|
||||
@@ -35,6 +35,7 @@ const SORTING = [
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
const initialGridItems = computed(() => {
|
||||
if (containerWidth.value === 0) return 120;
|
||||
|
||||
@@ -71,8 +72,6 @@ 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;
|
||||
@@ -89,13 +88,9 @@ 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,
|
||||
@@ -120,7 +115,7 @@ const searchResults = useSearch(searchQueryDebounced, mappedIcons, [
|
||||
|
||||
const searchPlaceholder = useSearchPlaceholder(searchQuery, searchResults);
|
||||
const isSearchMetadataLoading = computed(
|
||||
() => searchQuery.value.length > 0 && !isFetchingTags && !isFetchingCategories,
|
||||
() => searchQuery.value.length > 0 && (tags.value == null || categories.value == null),
|
||||
);
|
||||
|
||||
const chunkedIcons = computed(() => {
|
||||
@@ -173,7 +168,7 @@ watch(searchQueryDebounced, () => {
|
||||
function handleCloseDrawer() {
|
||||
setActiveIconName('');
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
const url = new URL(window.location);
|
||||
url.pathname = '/icons/';
|
||||
|
||||
if (searchQueryDebounced.value) {
|
||||
@@ -208,7 +203,6 @@ 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: readonly Placeholder[] = shallowReadonly([
|
||||
const brandPlaceholders: Placeholder[] = shallowReadonly([
|
||||
{
|
||||
title: 'Boooo! What a scary brand logo!',
|
||||
message:
|
||||
@@ -103,7 +103,7 @@ const brandPlaceholders: readonly Placeholder[] = shallowReadonly([
|
||||
},
|
||||
]);
|
||||
|
||||
const notFoundPlaceholders: readonly Omit<Placeholder, 'title'>[] = shallowReadonly([
|
||||
const notFoundPlaceholders: 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: readonly Omit<Placeholder, 'title'>[] = shallowReado
|
||||
},
|
||||
]);
|
||||
|
||||
function randomItem<T>(arr: readonly T[]): T {
|
||||
function randomItem<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ 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', transform: { ltr: String } });
|
||||
syncRef(size, sizeCssVar, { direction: 'ltr', transform: { ltr: String } });
|
||||
syncRef(strokeWidth, strokeWidthCssVar, { direction: 'ltr' });
|
||||
syncRef(size, sizeCssVar, { direction: 'ltr' });
|
||||
|
||||
function resetStyle() {
|
||||
color.value = STYLE_DEFAULTS.color;
|
||||
|
||||
@@ -61,50 +61,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -112,7 +68,4 @@
|
||||
top: calc(var(--vp-layout-top-height, 0px) + 64px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@@ -32,8 +32,7 @@
|
||||
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%),
|
||||
@@ -57,8 +56,7 @@
|
||||
.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.js'
|
||||
import { useData } from 'vitepress/dist/client/theme-default/composables/data'
|
||||
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/src/types';
|
||||
import { IconNode } from 'lucide';
|
||||
|
||||
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.js';
|
||||
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar';
|
||||
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';
|
||||
|
||||
const loader = {
|
||||
export default {
|
||||
async load() {
|
||||
const packageJsonList = await fetchPackages();
|
||||
|
||||
@@ -29,8 +29,3 @@ const loader = {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
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.js';
|
||||
import { throttleAndDebounce } from 'vitepress/dist/client/theme-default/support/utils';
|
||||
|
||||
/*
|
||||
* This file is compied and adjusted from vitepress/dist/client/theme-default/composables/useActiveAnchor.ts
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import Fuse, { type FuseOptionKeyObject } from 'fuse.js';
|
||||
import Fuse from 'fuse.js';
|
||||
import { shallowRef, computed, Ref } from 'vue';
|
||||
|
||||
const useSearch = <T>(
|
||||
query: Ref<string>,
|
||||
collection: Ref<T[]>,
|
||||
keys: FuseOptionKeyObject<T>[] = [],
|
||||
keys: Fuse.FuseOptionKeyObject<T>[] = [],
|
||||
) => {
|
||||
const index = shallowRef(
|
||||
new Fuse(collection.value, {
|
||||
threshold: 0.2,
|
||||
useExtendedSearch: true,
|
||||
keys,
|
||||
}),
|
||||
);
|
||||
@@ -18,9 +17,7 @@ const useSearch = <T>(
|
||||
index.value.setCollection(collection.value);
|
||||
|
||||
if (query.value) {
|
||||
return index.value
|
||||
.search({ $and: query.value.split(' ').filter((t) => !!t) })
|
||||
.map((result) => result.item);
|
||||
return index.value.search(query.value).map((result) => result.item);
|
||||
}
|
||||
|
||||
return collection.value;
|
||||
|
||||
@@ -152,10 +152,6 @@ html {
|
||||
.VPHomeHero .container {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.VPDoc:has(.vp-doc[class*=' _icons_']) {
|
||||
padding: 32px 32px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.VPNavBarHamburger .container > span {
|
||||
@@ -187,144 +183,3 @@ 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,17 +1,11 @@
|
||||
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?: Alias[];
|
||||
aliases?: string[];
|
||||
deprecated?: boolean;
|
||||
deprecationReason?: string;
|
||||
toBeRemovedInVersion?: string;
|
||||
@@ -24,8 +18,6 @@ export interface IconEntity extends IconMetaData {
|
||||
externalLibrary?: ExternalLibs;
|
||||
createdRelease?: Release;
|
||||
changedRelease?: Release;
|
||||
fromFork?: boolean;
|
||||
awaitingRelease?: boolean;
|
||||
popularity?: number;
|
||||
}
|
||||
|
||||
@@ -51,8 +43,7 @@ export interface PackageItem {
|
||||
packageDirname?: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
iconDark?: string;
|
||||
iconClass?: string;
|
||||
iconDark: string;
|
||||
shields: Shield[];
|
||||
source: string;
|
||||
documentation: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const chunkArray = <ItemType>(stream: ItemType[], size: number) => {
|
||||
return stream.reduce<ItemType[][]>(
|
||||
const chunkArray = <ArrayType>(stream: ArrayType, size: number) => {
|
||||
return stream.reduce<ArrayType[][]>(
|
||||
(chunks, item, idx, arr) =>
|
||||
idx % size == 0 ? [...chunks, arr.slice(idx, idx + size)] : chunks,
|
||||
[],
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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 !== '' && searchResults.length === 0,
|
||||
isNoResults: query in BRAND_STOPWORDS && searchResults.length === 0 && query !== '',
|
||||
isBrand: query in BRAND_STOPWORDS,
|
||||
query: BRAND_STOPWORDS[query] ?? query,
|
||||
};
|
||||
|
||||
Vendored
-5
@@ -1,4 +1,3 @@
|
||||
/// <reference types="vitepress/client" />
|
||||
import { type IconNode } from '@lucide/vue/src/types';
|
||||
import Vue from 'vue';
|
||||
|
||||
@@ -32,11 +31,7 @@ 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/icons/design-principles.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/icon-design-guide.md) for tips and guidelines on creating icons that fit the Lucide style.
|
||||
|
||||
### Contribute Code
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
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`).
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user