diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index f2517f0a6..7418171ad 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -10,3 +10,4 @@ touchpad ungroup toc sunlounger +monstera diff --git a/.github/instructions/metadata.instructions.md b/.github/instructions/metadata.instructions.md index 05aacbae1..ea5753d61 100644 --- a/.github/instructions/metadata.instructions.md +++ b/.github/instructions/metadata.instructions.md @@ -13,8 +13,72 @@ The `contributors` property is a required array of GitHub usernames for the peop The `tags` property is an array of strings that describe the icon and can be used for searching. Validate the tags against the `icon.schema.json` to ensure they are correctly formatted and adhere to the defined structure. -Provide tag suggestions based on the name of the icon and the use cases provided in the PR description. Use the existing tags in the repository as a reference for consistency and to avoid duplicates. Don't suggest words like: 'icon' and preferably use single words. Tags should always be in lowercase, and may also contain spaces (e.g. `magnifying glass`). The name of icon should not be included in the tags, as it is already specified. +Provide tag suggestions based on the name of the icon and the use cases provided in the PR description. Use the existing tags in the repository as a reference for consistency and to avoid duplicates. Don't suggest words like: 'icon' and preferably use single words. Tags should always be in lowercase, and may also contain spaces (e.g. `magnifying glass`). Don't include the full icon name, its space-separated form, or any individual part of its kebab-case name because those terms are already searchable. For example, don't suggest `mail-search`, `mail search`, `mail`, or `search` for the `mail-search` icon. -# Categories +## Categories The `categories` property is an array of strings that specify the categories to which the icon belongs, such as "devices", "interface", "media", etc. See the `categories` property in the `icon.schema.json` for more details on the allowed values. The categories should be chosen based on the use cases provided in the PR description and the existing categories in the repository. The categories should be relevant to the icon and should help users find the icon when searching for specific types of icons. The name of icon should not be included in the categories, as it is already specified. Suggest categories based on the name of the icon and the use cases provided in the PR description. Use the existing categories in the repository as a reference for consistency and to avoid duplicates. Categories should always be in lowercase and should not contain spaces. + +## Use Cases + +The `use-cases` property is an array of strings that describe the specific scenarios in which the icon would be used. Each use case should be concise and clearly convey the context in which the icon is applicable. Use the existing use cases in the repository as a reference for consistency and to avoid duplicates. + +### Writing icon use-cases + +A use-case tells a reader **where and why an icon would be used in a real interface**. Each entry should let someone who has never seen the icon understand what it's for. + +### Format + +- Write each use-case as a short phrase, not a sentence. Start with a present participle verb: *Representing…*, *Indicating…*, *Marking…*, *Showing…*, *Confirming…*, *Toggling…*, *Categorizing…*, *Searching…*. +- Keep it to roughly 4–12 words. One clear idea per entry. +- No trailing period. No leading bullet, number, or dash (the surrounding format supplies that). +- Give each icon 1–4 use-cases, ordered most-common first. Prefer fewer strong entries over many weak or repetitive ones. + +### Voice + +- Write from the interface's perspective, describing the function: "Indicating a low battery charge level" — not the requester's story ("I need an icon that shows…"). +- Name the concrete context where useful: *in smart home apps*, *in navigation apps*, *in text editors*, *on maps*. Context is what makes a use-case self-explanatory. +- Be neutral and factual. No first person, no marketing adjectives ("perfect for", "ideal", "beautiful"), no emoji. + +### What to exclude + +- **The requester's personal narrative.** "I'm building a SvelteKit UI and need…" → drop the framing, keep the function. +- **References to other issues, PRs, or icons by ID.** No "As provided in #1865", "complementary to git-branch-plus", "same as above", "see #2055". +- **Design/implementation chatter.** Bezier offsets, grid alignment, path cuts, "the icon works perfectly" — none of that is a use-case. +- **Restating the icon's name.** For `microchip`, "microchip" is not a use-case; describe what it represents ("Representing processors, chips, or embedded hardware"). +- **Links, image tags, emoji, and markdown artifacts.** +- **Meta-commentary.** "Self-explanatory", "lots and lots of use cases", "obvious :)". + +### Cross-contamination + +When a request covers several icons at once, assign each use-case only to the icon it actually describes. A bullet prefixed with another icon's name belongs to that icon, not this one. Don't let `briefcase-medical`'s meanings leak into `briefcase-business`. + +### Variants + +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: + +- `ambulance` → "Directing users to nearby emergency services in navigation apps" +- `calendar-sync` → "Representing recurring events, promotions, or limited-time offers" +- `globe-x` → "Indicating a device or application is offline or unreachable" +- `heading-2` → "Applying a level-2 heading in text editors" + +Poor → fixed: + +- ~~"I need a firewall icon :-)"~~ → "Representing firewalls and firewall rules on cybersecurity platforms" +- ~~"As above"~~ → "Signifying a deal, agreement, or partnership" +- ~~"microchip"~~ → "Representing processors, chips, or embedded hardware" +- ~~"Same use cases as the other currency symbols, except Turkish lira"~~ → "Displaying prices or amounts in Turkish lira" diff --git a/.github/instructions/ux-writing.instructions.md b/.github/instructions/ux-writing.instructions.md new file mode 100644 index 000000000..0e76c8b6b --- /dev/null +++ b/.github/instructions/ux-writing.instructions.md @@ -0,0 +1,587 @@ +--- +applyTo: "docs/*.md" +--- +# UX writing guidelines + +Follow these guidelines when writing or editing Lucide documentation, especially Markdown files. + +Write for people who are trying to understand a concept, follow a specification, or complete a task. Prioritize clarity, precision, and scannability over personality or decorative prose. + +## Be direct and concise + +State the important information first. Remove words that do not add meaning. + +Prefer short sentences and paragraphs, but do not sacrifice precision just to make text shorter. + +**Do:** + +> Icons must use a 24 × 24 pixel canvas. + +**Don't:** + +> When creating a new icon, you should make sure that the canvas you're working with has dimensions of 24 × 24 pixels. + +Avoid unnecessary introductions such as: + +- "It's important to note that…" +- "Keep in mind that…" +- "As you can see…" +- "In order to…" +- "It should be noted that…" + +State the information directly instead. + +## Put the rule before the explanation + +When documenting a requirement or recommendation, state it before explaining why it exists. + +**Do:** + +> Keep at least 1 pixel of space between unrelated elements. This prevents shapes from visually merging at small sizes. + +**Don't:** + +> Because shapes can visually merge at small sizes, it's important to make sure that unrelated elements have enough space between them. + +This makes documentation easier to scan and makes normative requirements easier to identify. + +## Explain the reason when it adds value + +Explain *why* a rule exists when the reasoning helps contributors apply it to situations that are not explicitly documented. + +Do not merely restate the rule. + +**Do:** + +> Avoid simulated fills. They don't scale consistently with the rest of the icon set. + +**Don't:** + +> Avoid simulated fills because simulated fills should not be used. + +Reasoning is particularly useful for design rules where contributors may need to evaluate unusual cases. + +## Use scannable structure + +Assume readers will scan the page before reading it in detail. + +Use: + +- descriptive headings +- short paragraphs +- lists for sets of related items +- examples for rules that benefit from demonstration +- **bold** for important terms or short emphasis +- code formatting for filenames, properties, commands, values, and identifiers + +Avoid long uninterrupted blocks of text. + +Headings should communicate the subject or rule without requiring the surrounding text for context. + +**Do:** + +> ## Keep 1 pixel of safe space + +**Don't:** + +> ## Spacing + +## Use sentence case + +Use sentence case for: + +- page titles +- headings +- table headings +- labels +- example captions + +Capitalize proper nouns and branded terms normally. + +**Do:** + +> ## Use consistent corner radii + +**Don't:** + +> ## Use Consistent Corner Radii + +## Address the reader directly + +Use **you** and **your** when referring to something the reader does. + +**Do:** + +> If your icon contains overlapping shapes, simplify the hidden geometry. + +Avoid referring to the reader as "the user," "the contributor," or other third-person terms when direct language is more natural. + +However, prefer an imperative when the subject is obvious. + +**Prefer:** + +> Keep related elements visually grouped. + +**Over:** + +> You should keep related elements visually grouped. + +## Use "we" only when it means the Lucide project or maintainers + +First-person plural is acceptable when describing an intentional project decision, convention, or policy. + +**Do:** + +> We use `kebab-case` for icon names. + +> We generally avoid adding brand logos to Lucide. + +Do not use "we" merely to make documentation conversational. + +**Prefer:** + +> Icons use a 2 pixel stroke. + +**Over:** + +> We use a 2 pixel stroke for our icons. + +## Use normative language consistently + +Use specific language to communicate how strictly a rule applies. + +- **must** — required; violating this is not acceptable +- **must not** — prohibited +- **should** — recommended; exceptions may be appropriate +- **should not** — discouraged; exceptions may be appropriate +- **can** — possible or permitted +- **may** — optional or dependent on context + +Do not use **should** when something is actually required. + +**Do:** + +> Icons must use a 24 × 24 pixel canvas. + +**Don't:** + +> Icons should use a 24 × 24 pixel canvas. + +Avoid vague substitutes such as "ideally," "normally," or "preferably" when a more precise normative term applies. + +## Prefer active voice + +Use active voice unless passive voice makes the subject clearer or avoids unnecessary repetition. + +**Do:** + +> Align nodes to the pixel grid. + +**Don't:** + +> Nodes should be aligned to the pixel grid. + +Passive voice is acceptable when the actor is irrelevant. + +> Deprecated icons are removed in major releases. + +## Use positive instructions when possible + +Tell readers what to do rather than only what not to do. + +**Do:** + +> Use a single continuous path when the elements form one continuous shape. + +**Less useful:** + +> Don't split continuous shapes into unnecessary paths. + +Use negative instructions when the prohibition itself is important. + +> Don't use simulated fills. + +## Be precise about consequences + +When explaining why something should or should not be done, describe the actual consequence rather than using vague warnings. + +**Do:** + +> Keep unrelated elements at least 2 pixels apart. Smaller gaps can disappear when the icon is rendered at small sizes. + +**Don't:** + +> Be careful when placing elements too close together. + +Avoid language that exaggerates consequences or attempts to influence a contributor's decision emotionally. + +## Prefer concrete language + +Use specific terms instead of vague references. + +**Do:** + +> The two endpoints must remain inside the 1 pixel safe zone. + +**Don't:** + +> These should stay within the appropriate area. + +Avoid ambiguous references such as: + +- this +- that +- it +- things +- stuff +- appropriate +- proper +- correct + +when the intended subject can be named directly. + +## Introduce terminology before relying on it + +Define Lucide-specific or technical terms when readers may not already know them. + +Once defined, use the same term consistently. + +**Do:** + +> The **safe zone** is the 1 pixel area between the icon artwork and the canvas boundary. + +Do not alternate between different terms for the same concept unless the distinction is intentional. + +For example, don't alternate between "safe zone," "padding," and "margin" if they refer to the same rule. + +## Use abbreviations sparingly + +Spell out uncommon abbreviations on first use. + +> Scalable Vector Graphics (SVG) + +Common technical abbreviations such as HTML, CSS, SVG, API, and URL do not need to be expanded when the intended audience can reasonably be expected to know them. + +Avoid Latin abbreviations such as: + +- e.g. +- i.e. +- etc. + +Prefer: + +- for example +- that is +- and so on + +## Use examples deliberately + +Use examples when they clarify how to apply a rule. + +Keep examples focused on the rule being demonstrated. Avoid introducing unrelated differences that make the comparison harder to understand. + +When contrasting examples, use **Do** and **Don't** consistently. + +**Do:** + +> Use one path for a continuous outline. + +**Don't:** + +> Split a continuous outline into several overlapping paths. + +The explanatory text should describe the principle, not merely narrate what is visible in the example. + +## Use links descriptively + +Link text should describe the destination or concept. + +**Do:** + +> See the [Icon Design Specification](./specification) for exact dimensions and spacing requirements. + +**Don't:** + +> For more information, [click here](./specification). + +Avoid generic link text such as: + +- click here +- here +- this page +- learn more + +when a descriptive label is possible. + +## Avoid unnecessary repetition + +Do not repeat information already established by a heading, example, or immediately preceding sentence. + +**Prefer:** + +> ## Keep 1 pixel of safe space +> +> Keep all artwork within the 22 × 22 pixel live area. + +**Over:** + +> ## Keep 1 pixel of safe space +> +> When designing an icon, you should keep 1 pixel of safe space around the icon. + +Cross-reference another section instead of duplicating detailed rules that already have a canonical explanation. + +## Use contractions in prose + +Use natural contractions such as: + +- don't +- can't +- isn't +- it's +- you'll + +They make explanatory prose less formal and easier to scan. + +Use uncontracted forms when deliberate emphasis is useful. + +> Do not change existing icon geometry solely to reduce the number of SVG nodes. + +Normative statements using **must not** should not be contracted. + +## Use the serial comma + +Use the serial (Oxford) comma in lists of three or more items. + +**Do:** + +> Check the icon's size, stroke width, and spacing. + +**Don't:** + +> Check the icon's size, stroke width and spacing. + +## Use punctuation normally in prose + +Use periods for complete sentences. + +Periods may be omitted from: + +- headings +- short labels +- short list items that are not complete sentences +- captions consisting only of a short phrase + +Be consistent within a list. + +If list items are complete sentences, end them with periods. If they are fragments, generally omit periods. + +## Skip colons in headings + +Do not end headings with colons. + +**Do:** + +> ## Naming conventions + +**Don't:** + +> ## Naming conventions: + +Use a colon normally when introducing a list or explanation in body text. + +## Use exclamation points sparingly + +Avoid exclamation points in technical documentation unless the context genuinely calls for excitement or celebration. + +**Prefer:** + +> Your first icon is ready. + +**Over:** + +> Your first icon is ready! + +Do not use exclamation points to make instructions sound friendlier. + +## Use ellipses only when they have meaning + +Use an ellipsis (`…`) to represent omitted content or an intentionally incomplete statement. + +Do not use ellipses decoratively or to make prose sound conversational. + +When documenting literal UI text, preserve the punctuation used by the interface. + +## Use parentheses for definitions and compact clarification + +Use parentheses when they make a definition, abbreviation, or short clarification easier to understand. + +> Scalable Vector Graphics (SVG) + +Avoid using parentheses for long side notes. Rewrite important information as part of the sentence or as a separate sentence. + +## Prefer "and" over ampersands + +Use **and** in normal prose. + +**Do:** + +> Naming and metadata + +**Avoid:** + +> Naming & metadata + +Use an ampersand only when it is part of an official name, literal UI text, code, or another established term. + +## Avoid em dashes + +Prefer commas, periods, colons, or parentheses when they produce a simpler sentence. + +**Prefer:** + +> This rule has one exception: existing legacy icons. + +**Avoid:** + +> This rule has one exception — existing legacy icons. + +Use an en dash (`–`) without surrounding spaces for ranges. + +> 1–3 pixels + +> A–Z + +Do not substitute a hyphen for an en dash in prose ranges unless required by a technical syntax. + +## Use hyphens correctly + +Hyphenate compound modifiers when necessary for clarity. + +> 24 × 24 pixel canvas + +> 2-pixel-wide gap + +> pixel-aligned element + +Do not hyphenate a phrase when it functions as a verb. + +> Sign off on the design. + +> Complete the sign-off. + +Do not add hyphens mechanically when the meaning is already clear. + +## Use code formatting consistently + +Use backticks for literal technical values and identifiers, including: + +- filenames and paths +- commands +- package names +- properties and attributes +- icon names +- code values +- SVG elements and attributes + +**Do:** + +> Add `camera-off` as an alias. + +> Set `stroke-width` to `2`. + +> Edit `icons/camera.svg`. + +Do not use code formatting merely for emphasis. + +## Use bold for semantic emphasis + +Use **bold** sparingly to highlight a term or distinction that helps readers scan the page. + +Do not bold entire sentences or paragraphs. + +Prefer bold over italics for general emphasis. + +Use *italics* only when conventional, such as when referring to a term as a term or when a specific distinction benefits from it. + +## Don't use all caps for emphasis + +Do not use blocks of uppercase text for emphasis. + +**Don't:** + +> NEVER place nodes outside the canvas. + +**Do:** + +> **Never** place nodes outside the canvas. + +Preserve capitalization when documenting literal identifiers or established technical terms. + +## Keep lists parallel + +Items in the same list should use the same grammatical structure. + +**Do:** + +- Use a 24 × 24 pixel canvas +- Keep 1 pixel of safe space +- Use a 2 pixel stroke +- Align nodes to the pixel grid + +**Don't:** + +- Use a 24 × 24 pixel canvas +- Safe space should be 1 pixel +- A 2 pixel stroke +- Nodes are aligned to the pixel grid + +Parallel structure makes rules easier to scan and compare. + +## Avoid unnecessary filler and enthusiasm + +Lucide documentation should sound helpful and confident, not promotional. + +Avoid filler such as: + +- "Simply…" +- "Just…" +- "Obviously…" +- "Of course…" +- "Easy…" +- "Luckily…" +- "Don't worry…" + +These words can be inaccurate or dismissive when the task is not simple for the reader. + +Also avoid marketing language such as: + +- "powerful" +- "seamless" +- "effortless" +- "amazing" +- "best-in-class" + +unless the word communicates a specific, verifiable distinction. + +## Prefer timeless documentation + +Avoid wording tied unnecessarily to the current state of development. + +**Prefer:** + +> Lucide supports custom icon aliases. + +**Avoid:** + +> Lucide now supports custom icon aliases. + +Use time-sensitive language only when the timing itself matters, such as migration notes, deprecations, or release documentation. + +## Preserve established Lucide terminology + +When editing existing documentation, preserve established Lucide terminology and naming conventions unless there is a clear reason to change them. + +Do not invent synonyms merely to avoid repetition. Consistency is more important than stylistic variety in technical documentation. + +When terminology differs between the Lucide codebase, API, UI, and documentation, prefer the term used by the relevant canonical specification. diff --git a/.github/labeler.yml b/.github/labeler.yml index aeb274b50..6029f5e00 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -30,6 +30,11 @@ - any-glob-to-any-file: - 'icons/*.svg' +🔬 Lab icon: + - changed-files: + - any-glob-to-any-file: + - 'lab/*.svg' + # For changes in the lucide package 🧳 lucide package: - changed-files: @@ -77,6 +82,7 @@ - changed-files: - any-glob-to-any-file: - 'packages/lucide-solid/*' + - 'packages/solid/*' # For changes in the Lucide astro package 🚀 astro package: diff --git a/.github/workflows/angular.yml b/.github/workflows/angular.yml index 764d077ec..80896cdc7 100644 --- a/.github/workflows/angular.yml +++ b/.github/workflows/angular.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c410bd739..3da94beb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,10 @@ on: branches: - main paths: - - icons/**/*.svg + - icons/*.svg + + schedule: + - cron: '0 6 * * *' workflow_dispatch: inputs: @@ -26,47 +29,72 @@ on: permissions: {} jobs: - check-dispatch-gate: - if: github.repository == 'lucide-icons/lucide' && - startsWith(github.event.head_commit.message, 'feat(icons)') && - github.event_name != 'workflow_dispatch' + release-gate: + concurrency: + group: ci-${{ github.event_name == 'schedule' && github.ref || 'release-gated' }} + cancel-in-progress: true + if: github.repository == 'lucide-icons/lucide' && github.event_name != 'workflow_dispatch' + environment: ${{ github.event_name == 'schedule' && 'Icon Release' || 'Icon Release Gated' }} + runs-on: ubuntu-latest + steps: + - name: Approve release + run: echo 'Release approved' + + prepare-release: + needs: release-gate runs-on: ubuntu-latest permissions: - contents: read + contents: read # Required to check if a new release is needed outputs: - RELEASE_ENVIRONMENT: ${{ steps.environment.outputs.RELEASE_ENVIRONMENT }} + SHOULD_RELEASE: ${{ steps.changes.outputs.SHOULD_RELEASE }} + LATEST_TAG: ${{ steps.latest-tag.outputs.LATEST_TAG }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 - - name: Fetch tags - run: git fetch --all --tags + - name: Get latest tag + id: latest-tag + run: echo "LATEST_TAG=$(git tag --sort=-v:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)" >> "$GITHUB_OUTPUT" - - name: Check if latest tag was created today - id: environment + - name: Log latest tag + run: echo '${{ steps.latest-tag.outputs.LATEST_TAG }}' + + - name: Check for releasable changes + id: changes + env: + LATEST_TAG: ${{ steps.latest-tag.outputs.LATEST_TAG }} run: | - LATEST_TAG=$(git tag --sort=-v:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1) - TAG_DATE=$(git log -1 --format=%cs "$LATEST_TAG") - TODAY=$(date +%Y-%m-%d) - - if [ "$TAG_DATE" == "$TODAY" ]; then - echo "::warning::A release tag already exists for today ($TODAY): $LATEST_TAG. Manual approval will be required to proceed." - echo "RELEASE_ENVIRONMENT=Icon Release Gated" >> $GITHUB_OUTPUT - else - echo "RELEASE_ENVIRONMENT=Icon Release" >> $GITHUB_OUTPUT + if [ -z "$LATEST_TAG" ]; then + echo "::error::No version tag found to release from." + exit 1 fi + echo "Latest version tag: $LATEST_TAG" + + CHANGES=$(git diff --name-only "$LATEST_TAG" HEAD -- ':(glob)icons/*.svg') + + if [ -z "$CHANGES" ]; then + echo "::notice::No icon SVGs changed since $LATEST_TAG. Skipping release." + echo "SHOULD_RELEASE=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "$CHANGES" + echo "SHOULD_RELEASE=true" >> "$GITHUB_OUTPUT" + create-release: - if: github.repository == 'lucide-icons/lucide' - needs: check-dispatch-gate - environment: ${{ needs.check-dispatch-gate.outputs.RELEASE_ENVIRONMENT }} + if: github.repository == 'lucide-icons/lucide' && needs.prepare-release.outputs.SHOULD_RELEASE == 'true' + needs: prepare-release runs-on: ubuntu-latest permissions: 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 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -75,36 +103,26 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Fetch tags - run: git fetch --all --tags - - - name: Get latest tag - id: latest-tag - run: echo "LATEST_TAG=$(git tag --sort=-v:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)" >> $GITHUB_OUTPUT - - - name: Log latest tag - run: echo '${{ steps.latest-tag.outputs.LATEST_TAG }}' - - name: Check if we can patch - run: pnpm semver $LATEST_TAG -i minor + run: pnpm semver "$LATEST_TAG" -i minor env: - LATEST_TAG: ${{ steps.latest-tag.outputs.LATEST_TAG }} + LATEST_TAG: ${{ needs.prepare-release.outputs.LATEST_TAG }} - name: Create new version id: new-version - run: echo "NEW_VERSION=$(pnpm semver $LATEST_TAG -i minor)" >> $GITHUB_OUTPUT + run: echo "NEW_VERSION=$(pnpm semver "$LATEST_TAG" -i minor)" >> "$GITHUB_OUTPUT" env: - LATEST_TAG: ${{ steps.latest-tag.outputs.LATEST_TAG }} + LATEST_TAG: ${{ needs.prepare-release.outputs.LATEST_TAG }} - name: Check output - run: | - echo '${{ steps.new-version.outputs.NEW_VERSION }}' - echo '${{ steps.change-log.outputs.CHANGE_LOG }}' + run: echo '${{ steps.new-version.outputs.NEW_VERSION }}' - name: Create Release - uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 + id: create-release + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 with: tag_name: ${{ steps.new-version.outputs.NEW_VERSION }} + target_commitish: ${{ github.sha }} name: Version ${{ steps.new-version.outputs.NEW_VERSION }} generate_release_notes: true @@ -129,3 +147,30 @@ jobs: with: version: ${{ inputs.version }} tag: ${{ inputs.tag }} + + post-release: + name: Trigger post-release automation + if: >- + !cancelled() && + 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" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..6be2453ac --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,55 @@ +name: Docs Checks + +on: + pull_request: + paths: + - docs/** + - .github/workflows/docs.yml + - package.json + - pnpm-workspace.yaml + - pnpm-lock.yaml + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Release metadata is generated by diffing every release tag + fetch-depth: 0 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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 diff --git a/.github/workflows/icons.yml b/.github/workflows/icons.yml index e4d53a6f9..1982039d2 100644 --- a/.github/workflows/icons.yml +++ b/.github/workflows/icons.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' diff --git a/.github/workflows/lint-code.yml b/.github/workflows/lint-code.yml index edaec6611..ccbb3ad30 100644 --- a/.github/workflows/lint-code.yml +++ b/.github/workflows/lint-code.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' diff --git a/.github/workflows/linting-icons.yml b/.github/workflows/linting-icons.yml index 0f69ba490..5a6c2f213 100644 --- a/.github/workflows/linting-icons.yml +++ b/.github/workflows/linting-icons.yml @@ -20,7 +20,7 @@ jobs: contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -49,7 +49,7 @@ jobs: contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' diff --git a/.github/workflows/lucide-astro.yml b/.github/workflows/lucide-astro.yml index 47a1fc8dd..8a265ba5c 100644 --- a/.github/workflows/lucide-astro.yml +++ b/.github/workflows/lucide-astro.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: 'package.json' @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: 'package.json' @@ -46,3 +46,19 @@ jobs: - name: Test run: pnpm --filter @lucide/astro test + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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 diff --git a/.github/workflows/lucide-font.yml b/.github/workflows/lucide-font.yml index 7548723cb..4a99742bc 100644 --- a/.github/workflows/lucide-font.yml +++ b/.github/workflows/lucide-font.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -37,3 +37,19 @@ jobs: with: name: lucide-font path: lucide-font + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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 diff --git a/.github/workflows/lucide-lab.yml b/.github/workflows/lucide-lab.yml index 323cb3595..a8f9321dc 100644 --- a/.github/workflows/lucide-lab.yml +++ b/.github/workflows/lucide-lab.yml @@ -3,6 +3,7 @@ name: Lucide Lab checks on: pull_request: paths: + - lab/** - packages/lab/** - packages/shared/** - tools/build-icons/** @@ -17,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -28,3 +29,19 @@ jobs: - name: Build run: pnpm --filter @lucide/lab build + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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 diff --git a/.github/workflows/lucide-preact.yml b/.github/workflows/lucide-preact.yml index f842c1b47..5e7cccfe2 100644 --- a/.github/workflows/lucide-preact.yml +++ b/.github/workflows/lucide-preact.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -31,3 +31,19 @@ jobs: - name: Test run: pnpm --filter lucide-preact test + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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 diff --git a/.github/workflows/lucide-react-native.yml b/.github/workflows/lucide-react-native.yml index 6f8d83079..160f6e2cf 100644 --- a/.github/workflows/lucide-react-native.yml +++ b/.github/workflows/lucide-react-native.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -47,3 +47,19 @@ jobs: - name: Test run: pnpm --filter lucide-react-native test + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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 diff --git a/.github/workflows/lucide-react.yml b/.github/workflows/lucide-react.yml index 7d00a1ba5..02a9939ba 100644 --- a/.github/workflows/lucide-react.yml +++ b/.github/workflows/lucide-react.yml @@ -7,10 +7,13 @@ on: pull_request: paths: - packages/lucide-react/** + - integrations/lucide-react/** - packages/shared/** - tools/build-icons/** - tools/rollup-plugins/** - scripts/generateNextJSAliases.mjs + - package.json + - pnpm-workspace.yaml - pnpm-lock.yaml permissions: @@ -21,7 +24,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -37,7 +40,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -48,3 +51,38 @@ jobs: - name: Test run: pnpm --filter lucide-react test + + integration: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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 diff --git a/.github/workflows/lucide-shared.yml b/.github/workflows/lucide-shared.yml index efea0dc77..9ecc72556 100644 --- a/.github/workflows/lucide-shared.yml +++ b/.github/workflows/lucide-shared.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -27,4 +27,20 @@ jobs: run: pnpm install --frozen-lockfile - name: Test - run: pnpm --filter lucide-react test + run: pnpm --filter @lucide/shared test + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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 diff --git a/.github/workflows/lucide-solid.yml b/.github/workflows/lucide-solid.yml index d78602a35..8b5d3d658 100644 --- a/.github/workflows/lucide-solid.yml +++ b/.github/workflows/lucide-solid.yml @@ -7,9 +7,12 @@ on: pull_request: paths: - packages/lucide-solid/** + - integrations/lucide-solid/** - packages/shared/** - tools/build-icons/** - tools/rollup-plugins/** + - package.json + - pnpm-workspace.yaml - pnpm-lock.yaml permissions: @@ -20,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -36,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -47,3 +50,38 @@ 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@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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 + + integration: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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-solid-vite exec playwright install --with-deps chromium + + - name: Run integration checks + run: pnpm test:integrations:solid diff --git a/.github/workflows/lucide-static.yml b/.github/workflows/lucide-static.yml index fee24bf72..692882522 100644 --- a/.github/workflows/lucide-static.yml +++ b/.github/workflows/lucide-static.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -26,3 +26,19 @@ jobs: - name: Build run: pnpm --filter lucide-static build + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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 diff --git a/.github/workflows/lucide-svelte-5.yml b/.github/workflows/lucide-svelte-5.yml index 4f48fcf19..57c809d29 100644 --- a/.github/workflows/lucide-svelte-5.yml +++ b/.github/workflows/lucide-svelte-5.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -52,7 +52,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -61,5 +61,5 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Test + - name: Typecheck run: pnpm --filter @lucide/svelte typecheck diff --git a/.github/workflows/lucide-vue.yml b/.github/workflows/lucide-vue.yml index e4e4ee9b6..a5d5bee20 100644 --- a/.github/workflows/lucide-vue.yml +++ b/.github/workflows/lucide-vue.yml @@ -10,6 +10,9 @@ on: - packages/shared/** - tools/build-icons/** - tools/rollup-plugins/** + - integrations/lucide-vue/** + - package.json + - pnpm-workspace.yaml - pnpm-lock.yaml permissions: @@ -20,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -36,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -47,3 +50,38 @@ 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@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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 + + integration: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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-vue-vite exec playwright install --with-deps chromium + + - name: Run integration checks + run: pnpm test:integrations:vue diff --git a/.github/workflows/lucide.yml b/.github/workflows/lucide.yml index 844d2647f..6e7a3a190 100644 --- a/.github/workflows/lucide.yml +++ b/.github/workflows/lucide.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -46,3 +46,19 @@ jobs: - name: Test run: pnpm --filter lucide test + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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 diff --git a/.github/workflows/pull-request-metadata-suggestions.yml b/.github/workflows/pull-request-metadata-suggestions.yml index cc1586da7..fc7341353 100644 --- a/.github/workflows/pull-request-metadata-suggestions.yml +++ b/.github/workflows/pull-request-metadata-suggestions.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: lucide-icons/lucide - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' diff --git a/.github/workflows/release-lab.yml b/.github/workflows/release-lab.yml new file mode 100644 index 000000000..d8c15c1c9 --- /dev/null +++ b/.github/workflows/release-lab.yml @@ -0,0 +1,146 @@ +name: Release @lucide/lab + +on: + push: + branches: + - main + paths: + - lab/**/*.svg + + workflow_dispatch: + inputs: + release-type: + description: Release type + required: true + default: 'minor' + type: choice + options: + - patch + - minor + - major + +permissions: {} + +jobs: + create-release: + if: github.repository == 'lucide-icons/lucide' + runs-on: ubuntu-latest + permissions: + contents: write # Required to create the release and tag + outputs: + VERSION: ${{ steps.new-version.outputs.NEW_VERSION }} + SHOULD_RELEASE: ${{ steps.release-notes.outputs.SHOULD_RELEASE }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 # Required to diff the lab icons against the previous tag + + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e200da21f..01955f2f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,12 +53,13 @@ jobs: '@lucide/angular', '@lucide/astro', '@lucide/icons', + '@lucide/solid', '@lucide/svelte', '@lucide/vue', ] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -96,7 +97,7 @@ jobs: name: lucide-font path: lucide-font - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -127,7 +128,7 @@ jobs: contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: cache: 'pnpm' @@ -169,7 +170,7 @@ jobs: zip -r lucide-icons-${{ needs.prepare.outputs.VERSION }}.zip icons - name: Release zip and fonts - uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 with: tag_name: ${{ needs.prepare.outputs.VERSION }} diff --git a/.github/workflows/solid-2.yml b/.github/workflows/solid-2.yml new file mode 100644 index 000000000..f65ffa092 --- /dev/null +++ b/.github/workflows/solid-2.yml @@ -0,0 +1,71 @@ +name: Lucide Solid 2 Checks + +on: + push: + branches: + - next + pull_request: + paths: + - packages/solid/** + - packages/shared/** + - tools/build-icons/** + - tools/rollup-plugins/** + - integrations/lucide-solid-2/** + - 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 + - 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 + run: pnpm --filter @lucide/solid build + + test: + 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/solid 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-solid-2-vite exec playwright install --with-deps chromium + + - name: Run integration checks + run: pnpm test:integrations:solid-2 diff --git a/.gitignore b/.gitignore index 67343f5ec..1e94ab0f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,13 @@ .DS_Store .idea .next +.astro .obsidian .now .cache .env node_modules +.pnpm-store dist build sandbox @@ -13,6 +15,13 @@ stash coverage stats *.log +*.tsbuildinfo +integrations/**/.react-router +integrations/**/.vitest-attachments +integrations/**/__screenshots__ +integrations/**/.output +integrations/**/.nitro +integrations/**/.nuxt outlined lucide-font packages/**/src/icons/*.js @@ -23,6 +32,9 @@ packages/**/src/aliases.ts !packages/**/src/aliases/index.ts packages/**/src/utils/*.ts !packages/shared/src/utils/*.ts +!packages/shared/src/build/ +!packages/shared/src/build/*.ts +packages/shared/src/build/types.d.ts packages/**/src/dynamicIconImports.ts packages/**/DynamicIcon.d.ts packages/**/dynamicIconImports.js diff --git a/.husky/pre-commit b/.husky/pre-commit index 0675e2802..719ca7939 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,5 +1,2 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - pnpm lint-staged pnpm checkIcons diff --git a/.prettierignore b/.prettierignore index 435726b81..e8a4863bb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -19,6 +19,17 @@ 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/**/file-routes.d.ts +integrations/**/.vitest-attachments +integrations/**/__screenshots__ +integrations/**/.output +integrations/**/.nitro +integrations/**/.nuxt + # lucide-svelte packages/svelte/src/icons/*.svelte packages/svelte/.svelte-kit diff --git a/categories/accessibility.json b/categories/accessibility.json index a668d3ee3..e6a4cc76a 100644 --- a/categories/accessibility.json +++ b/categories/accessibility.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Accessibility", + "description": "Icons for accessibility, visibility, hearing, speech, and inclusive UI affordances.", "icon": "accessibility" } diff --git a/categories/account.json b/categories/account.json index 95a80ad40..cc903fddd 100644 --- a/categories/account.json +++ b/categories/account.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Accounts & access", + "description": "Icons for user profiles, identity, settings, memberships, and personal account actions.", "icon": "user" } diff --git a/categories/animals.json b/categories/animals.json index 85fafda5f..f1cff48ec 100644 --- a/categories/animals.json +++ b/categories/animals.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Animals", + "description": "Icons for animals, wildlife, pets, insects, and other living creatures.", "icon": "dog" } diff --git a/categories/arrows.json b/categories/arrows.json index 3258378f9..b42d13038 100644 --- a/categories/arrows.json +++ b/categories/arrows.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Arrows", + "description": "Icons for directional arrows, movement, sorting, transfer, and navigation flows.", "icon": "arrow-left-right" } diff --git a/categories/buildings.json b/categories/buildings.json index 13f24b08f..cd6d424c7 100644 --- a/categories/buildings.json +++ b/categories/buildings.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Buildings", + "description": "Icons for buildings, landmarks, houses, infrastructure, and built environments.", "icon": "building" } diff --git a/categories/charts.json b/categories/charts.json index e9bdc1882..8d37c9e26 100644 --- a/categories/charts.json +++ b/categories/charts.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Charts", + "description": "Icons for charts, graphs, trends, analytics, and data visualization.", "icon": "chart-pie" } diff --git a/categories/communication.json b/categories/communication.json index 3b9a899d7..cc22a29d4 100644 --- a/categories/communication.json +++ b/categories/communication.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Communication", + "description": "Icons for speaking, listening, calling, messaging, broadcasting, and communication devices.", "icon": "message-circle" } diff --git a/categories/connectivity.json b/categories/connectivity.json index b566a84ec..1568ff2da 100644 --- a/categories/connectivity.json +++ b/categories/connectivity.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Connectivity", + "description": "Icons for wireless signals, wired connections, networks, device pairing, and data transfer.", "icon": "wifi" } diff --git a/categories/cursors.json b/categories/cursors.json index 9522faa16..5996828a7 100644 --- a/categories/cursors.json +++ b/categories/cursors.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Cursors", + "description": "Icons for cursors, pointers, selection tools, and loading states.", "icon": "mouse-pointer-2" } diff --git a/categories/design.json b/categories/design.json index e32592560..bddae399e 100644 --- a/categories/design.json +++ b/categories/design.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Design", + "description": "Icons for design tools, layout controls, creative editing, and visual composition.", "icon": "palette" } diff --git a/categories/development.json b/categories/development.json index 113fd1cc2..beb75b795 100644 --- a/categories/development.json +++ b/categories/development.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Coding & development", + "description": "Icons for code, logic, data structures, automation, and software development workflows.", "icon": "code-xml" } diff --git a/categories/devices.json b/categories/devices.json index 3bb54f180..d6131a952 100644 --- a/categories/devices.json +++ b/categories/devices.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Devices", + "description": "Icons for hardware, electronics, computers, peripherals, and connected devices.", "icon": "smartphone" } diff --git a/categories/emoji.json b/categories/emoji.json index 2bd590877..78e838828 100644 --- a/categories/emoji.json +++ b/categories/emoji.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Emoji", + "description": "Icons for expressive symbols, faces, gestures, hearts, stars, and zodiac signs.", "icon": "face-slightly-smiling" } diff --git a/categories/files.json b/categories/files.json index 073a09beb..40b4265e8 100644 --- a/categories/files.json +++ b/categories/files.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "File icons", + "description": "Icons for files, folders, storage, import and export, and document-related actions.", "icon": "panels-top-left" } diff --git a/categories/finance.json b/categories/finance.json index 267643671..b89e9c126 100644 --- a/categories/finance.json +++ b/categories/finance.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Finance", + "description": "Icons for money, pricing, payments, currencies, banking, and financial trends.", "icon": "piggy-bank" } diff --git a/categories/food-beverage.json b/categories/food-beverage.json index 7456558b6..f643fcbb3 100644 --- a/categories/food-beverage.json +++ b/categories/food-beverage.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Food & beverage", + "description": "Icons for food, drinks, cooking, ingredients, dining, and kitchen items.", "icon": "coffee" } diff --git a/categories/gaming.json b/categories/gaming.json index 2c90f4c9d..c9f5e47f5 100644 --- a/categories/gaming.json +++ b/categories/gaming.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Gaming", + "description": "Icons for games, fantasy items, strategy pieces, achievements, and play-related objects.", "icon": "gamepad-2" } diff --git a/categories/home.json b/categories/home.json index 20a89330d..d15228e3d 100644 --- a/categories/home.json +++ b/categories/home.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Home", + "description": "Icons for household spaces, furniture, appliances, utilities, cleaning, and home safety.", "icon": "house" } diff --git a/categories/layout.json b/categories/layout.json index 22a163f95..ba9ffbe34 100644 --- a/categories/layout.json +++ b/categories/layout.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Layout", + "description": "Icons for alignment, spacing, resizing, grouping, and interface layout structure.", "icon": "panels-top-left" } diff --git a/categories/mail.json b/categories/mail.json index 3dd1d3026..329a55ed1 100644 --- a/categories/mail.json +++ b/categories/mail.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Mail", + "description": "Icons for mailboxes, inboxes, sending, replies, attachments, and email management.", "icon": "mail" } diff --git a/categories/math.json b/categories/math.json index 1ae72ad9e..19d8fe569 100644 --- a/categories/math.json +++ b/categories/math.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Mathematics", + "description": "Icons for math symbols, geometry, measurement, arithmetic, and calculation.", "icon": "divide" } diff --git a/categories/medical.json b/categories/medical.json index e745df697..fcc92c78d 100644 --- a/categories/medical.json +++ b/categories/medical.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Medical", + "description": "Icons for healthcare, anatomy, treatment, diagnostics, wellness, and medical facilities.", "icon": "heart" } diff --git a/categories/multimedia.json b/categories/multimedia.json index 00897c3e4..80907bdf8 100644 --- a/categories/multimedia.json +++ b/categories/multimedia.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Multimedia", + "description": "Icons for audio, video, playback, recording, broadcasting, and media libraries.", "icon": "circle-play" } diff --git a/categories/nature.json b/categories/nature.json index aa41fd8dd..5f83e3778 100644 --- a/categories/nature.json +++ b/categories/nature.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Nature", + "description": "Icons for plants, trees, landscapes, camping, and the natural world.", "icon": "sprout" } diff --git a/categories/navigation.json b/categories/navigation.json index 0c46693cb..4542bfdd7 100644 --- a/categories/navigation.json +++ b/categories/navigation.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Navigation & Places", + "description": "Icons for maps, locations, landmarks, routes, travel directions, and wayfinding.", "icon": "compass" } diff --git a/categories/notifications.json b/categories/notifications.json index c2a729bec..366c4efbe 100644 --- a/categories/notifications.json +++ b/categories/notifications.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", - "title": "Notification", + "title": "Notifications", + "description": "Icons for alerts, status updates, reminders, approvals, and attention states.", "icon": "triangle-alert" } diff --git a/categories/people.json b/categories/people.json index be028e4b5..a8378b7f4 100644 --- a/categories/people.json +++ b/categories/people.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "People", + "description": "Icons for people, groups, identities, roles, and person-centered actions.", "icon": "person-standing" } diff --git a/categories/photography.json b/categories/photography.json index a4ad5bff1..fcaa4f42f 100644 --- a/categories/photography.json +++ b/categories/photography.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Photography", + "description": "Icons for cameras, images, framing, viewing, editing, and photo workflows.", "icon": "camera" } diff --git a/categories/science.json b/categories/science.json index 720983fb3..27988158d 100644 --- a/categories/science.json +++ b/categories/science.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Science", + "description": "Icons for science, research, experiments, instruments, formulas, and discovery.", "icon": "flask-conical" } diff --git a/categories/seasons.json b/categories/seasons.json index 0c4fff3cc..43ab65678 100644 --- a/categories/seasons.json +++ b/categories/seasons.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Seasons", + "description": "Icons for seasonal weather, blooming plants, sunlight, and changing times of year.", "icon": "leaf" } diff --git a/categories/security.json b/categories/security.json index 8372554fe..f8fc17cfc 100644 --- a/categories/security.json +++ b/categories/security.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Security", + "description": "Icons for locks, keys, identity checks, surveillance, protection, and secure access.", "icon": "shield" } diff --git a/categories/shapes.json b/categories/shapes.json index c968b222b..d282fcd2a 100644 --- a/categories/shapes.json +++ b/categories/shapes.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Shapes", + "description": "Icons for basic geometric shapes and symbols.", "icon": "triangle" } diff --git a/categories/shopping.json b/categories/shopping.json index 5e6b05ff5..b9341196c 100644 --- a/categories/shopping.json +++ b/categories/shopping.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Shopping", + "description": "Icons for retail, carts, bags, checkout, barcodes, and purchasing.", "icon": "shopping-bag" } diff --git a/categories/social.json b/categories/social.json index ee87f8574..ce0210600 100644 --- a/categories/social.json +++ b/categories/social.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Social", + "description": "Icons for social interaction, reactions, sharing, community, and relationship signals.", "icon": "thumbs-up" } diff --git a/categories/sports.json b/categories/sports.json index 25440ad6f..aff648ab4 100644 --- a/categories/sports.json +++ b/categories/sports.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Sports", - "icon": "type" + "description": "Icons for sports, fitness, competition, equipment, and athletic achievement.", + "icon": "trophy" } diff --git a/categories/sustainability.json b/categories/sustainability.json index a0730f6fa..c18463168 100644 --- a/categories/sustainability.json +++ b/categories/sustainability.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Sustainability", + "description": "Icons for sustainability, clean energy, recycling, nature, and environmental stewardship.", "icon": "recycle" } diff --git a/categories/text.json b/categories/text.json index 74419487a..49d19130d 100644 --- a/categories/text.json +++ b/categories/text.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Text formatting", + "description": "Icons for letters, typography, formatting, reading, writing, and text structure.", "icon": "type" } diff --git a/categories/time.json b/categories/time.json index 2b85210de..703402f79 100644 --- a/categories/time.json +++ b/categories/time.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Time & calendar", + "description": "Icons for clocks, calendars, schedules, history, timers, and time tracking.", "icon": "calendar" } diff --git a/categories/tools.json b/categories/tools.json index 48cf0a96a..18722dd6e 100644 --- a/categories/tools.json +++ b/categories/tools.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Tools", + "description": "Icons for hand tools, equipment, maintenance, repair, and making things.", "icon": "hammer" } diff --git a/categories/transportation.json b/categories/transportation.json index 984da071c..768fa6a81 100644 --- a/categories/transportation.json +++ b/categories/transportation.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Transportation", + "description": "Icons for vehicles, transit, cargo, parking, fuel, and moving people or goods.", "icon": "train-front" } diff --git a/categories/travel.json b/categories/travel.json index 3d78c06e3..cf7081470 100644 --- a/categories/travel.json +++ b/categories/travel.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Travel", + "description": "Icons for travel, lodging, luggage, destinations, trip planning, and tourism.", "icon": "backpack" } diff --git a/categories/weather.json b/categories/weather.json index a08cc5d66..72c9b0338 100644 --- a/categories/weather.json +++ b/categories/weather.json @@ -1,5 +1,6 @@ { "$schema": "../category.schema.json", "title": "Weather", + "description": "Icons for clouds, sun, rain, wind, temperature, and weather conditions.", "icon": "cloud-sun" } diff --git a/category.schema.json b/category.schema.json index 7229f210e..385d1f14c 100644 --- a/category.schema.json +++ b/category.schema.json @@ -13,7 +13,7 @@ "title": "Lucide Icons category schema", "type": "object", "additionalProperties": false, - "required": ["$schema", "icon", "title"], + "required": ["$schema", "description", "icon", "title"], "properties": { "$schema": { "type": "string" diff --git a/docs/.vitepress/api/figma/data.ts b/docs/.vitepress/api/figma/data.ts index cde37f140..fad04b166 100644 --- a/docs/.vitepress/api/figma/data.ts +++ b/docs/.vitepress/api/figma/data.ts @@ -1,13 +1,12 @@ import { eventHandler, setResponseHeader } from 'h3'; import iconNodes from '../../data/iconNodes/index.ts'; -import { IconNodeWithKeys } from '../../theme/types'; import iconMetaData from '../../data/iconMetaData'; import releaseMeta from '../../data/releaseMetaData.json'; import categories from '../../data/categoriesData.json'; const dataResponse = { icons: Object.entries(iconNodes).reduce((acc, [name, iconNode]) => { - const newIconNode = (iconNode as IconNodeWithKeys).map(([name, { key, ...attrs }]) => { + const newIconNode = iconNode.map(([name, { key, ...attrs }]) => { return [name, attrs]; }); diff --git a/docs/.vitepress/api/gh-icon/diff/[...data].get.tsx b/docs/.vitepress/api/gh-icon/diff/[...data].get.tsx index 34d15b684..900c4f11c 100644 --- a/docs/.vitepress/api/gh-icon/diff/[...data].get.tsx +++ b/docs/.vitepress/api/gh-icon/diff/[...data].get.tsx @@ -41,7 +41,6 @@ export default eventHandler((event) => { diff --git a/docs/.vitepress/api/gh-icon/symmetry/[...data].get.tsx b/docs/.vitepress/api/gh-icon/symmetry/[...data].get.tsx index 08033bd4d..441704f8e 100644 --- a/docs/.vitepress/api/gh-icon/symmetry/[...data].get.tsx +++ b/docs/.vitepress/api/gh-icon/symmetry/[...data].get.tsx @@ -48,7 +48,6 @@ export default eventHandler((event) => { diff --git a/docs/.vitepress/api/icon-nodes/index.get.ts b/docs/.vitepress/api/icon-nodes/index.get.ts index 8ab634c78..dcdfe98fd 100644 --- a/docs/.vitepress/api/icon-nodes/index.get.ts +++ b/docs/.vitepress/api/icon-nodes/index.get.ts @@ -1,6 +1,5 @@ import { eventHandler, getQuery, setResponseHeader } from 'h3'; import iconNodes from '../../data/iconNodes'; -import { IconNodeWithKeys } from '../../theme/types'; export default eventHandler((event) => { const query = getQuery(event); @@ -19,7 +18,7 @@ export default eventHandler((event) => { return [name, iconNode]; } - const newIconNode = (iconNode as IconNodeWithKeys).map(([name, { key, ...attrs }]) => { + const newIconNode = iconNode.map(([name, { key, ...attrs }]) => { return [name, attrs]; }); diff --git a/docs/.vitepress/api/icons/[iconName].get.ts b/docs/.vitepress/api/icons/[iconName].get.ts index 733eeb5ea..ec8ab0b83 100644 --- a/docs/.vitepress/api/icons/[iconName].get.ts +++ b/docs/.vitepress/api/icons/[iconName].get.ts @@ -18,11 +18,12 @@ export default eventHandler((event) => { return sendError(event, error); } - const width = getQuery(event).width || undefined; - const height = getQuery(event).height || undefined; - const color = getQuery(event).color || undefined; - const strokeWidth = getQuery(event).strokeWidth || undefined; - const background = getQuery(event).background || undefined; + const query = getQuery>(event); + const width = query.width || undefined; + const height = query.height || undefined; + const color = query.color || undefined; + const strokeWidth = query.strokeWidth || undefined; + const background = query.background || undefined; const LucideIcon = createLucideIcon(params.iconName, iconNode); diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 86ec55b0b..547a14faa 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -4,6 +4,7 @@ import { groupIconMdPlugin, groupIconVitePlugin } from 'vitepress-plugin-group-i import sidebar from './sidebar'; import snackPlayer from './markdown/snackPlayer'; import sandpackPlugin from './markdown/sandpack'; +import examplePlugin from './markdown/example'; import { readFile } from 'node:fs/promises'; import { resourcesSidebar } from './sidebar/resources'; import llmstxt from 'vitepress-plugin-llms'; @@ -14,6 +15,10 @@ const defaultSandpackCSS = await readFile( fileURLToPath(new URL('./theme/sandpack-default.css', import.meta.url)), 'utf-8', ); +const iconStyleHeadScript = await readFile( + fileURLToPath(new URL('./theme/iconStyleHead.js', import.meta.url)), + 'utf-8', +); const title = 'Lucide'; const socialTitle = 'Lucide Icons'; @@ -31,6 +36,7 @@ export default defineConfig({ config(md) { md.use(groupIconMdPlugin); md.use(snackPlayer); + md.use(examplePlugin); md.use(sandpackPlugin, { defaultFiles: { '/styles.css': { @@ -82,7 +88,10 @@ export default defineConfig({ }) as unknown as UserConfig['vite']['plugins'][0], ], }, - head: getHeadConfig({ title, description, socialTitle }), + head: [ + ...getHeadConfig({ title, description, socialTitle }), + ['script', {}, iconStyleHeadScript], + ], transformPageData, themeConfig: { logo: { @@ -95,8 +104,9 @@ export default defineConfig({ { text: 'Resources', items: [ - ...resourcesSidebar[0].items, - { text: 'Design icons', link: '/contribute/icon-design-guide' }, + ...resourcesSidebar[0].items.map(({ text, link }) => ({ text, link })), + { text: 'How to use icons', link: '/how-to/' }, + { text: 'Contributing icons', link: '/contribute/icons/' }, ], }, { text: 'Packages', link: '/packages' }, diff --git a/docs/.vitepress/data/packageData.json b/docs/.vitepress/data/packageData.json index 9dd193f59..6009b6f2e 100644 --- a/docs/.vitepress/data/packageData.json +++ b/docs/.vitepress/data/packageData.json @@ -85,8 +85,26 @@ } ] }, - "lucide-react-native": { + "@lucide/solid": { "order": 5, + "icon": "solid", + "docsAlias": "solid", + "packageDirname": "solid", + "shields": [ + { + "alt": "npm", + "src": "https://img.shields.io/npm/v/@lucide/solid", + "href": "https://www.npmjs.com/package/@lucide/solid" + }, + { + "alt": "npm", + "src": "https://img.shields.io/npm/dw/@lucide/solid", + "href": "https://www.npmjs.com/package/@lucide/solid" + } + ] + }, + "lucide-react-native": { + "order": 6, "icon": "react-native", "docsAlias": "react-native", "shields": [ @@ -103,7 +121,7 @@ ] }, "@lucide/angular": { - "order": 6, + "order": 7, "icon": "angular", "docsAlias": "angular", "packageDirname": "angular", diff --git a/docs/.vitepress/lib/SvgPreview/Backdrop.tsx b/docs/.vitepress/lib/SvgPreview/Backdrop.tsx index 05d43f68d..24efe1c0c 100644 --- a/docs/.vitepress/lib/SvgPreview/Backdrop.tsx +++ b/docs/.vitepress/lib/SvgPreview/Backdrop.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { type JSX } from 'react'; interface BackdropProps { src: string; diff --git a/docs/.vitepress/lib/SvgPreview/index.tsx b/docs/.vitepress/lib/SvgPreview/index.tsx index 60643fa87..f7449c5e8 100644 --- a/docs/.vitepress/lib/SvgPreview/index.tsx +++ b/docs/.vitepress/lib/SvgPreview/index.tsx @@ -397,10 +397,14 @@ interface SvgPreviewProps extends React.SVGProps { children?: React.ReactNode; } -const SvgPreview = ( - { src, children, height = 24, width = 24, showGrid = false, ...props }: SvgPreviewProps, - ref, -) => { +const SvgPreview = ({ + src, + children, + height = 24, + width = 24, + showGrid = false, + ...props +}: SvgPreviewProps) => { const subGridSize = Math.max(height, width) % 3 === 0 ? Math.max(height, width) > 24 @@ -413,7 +417,6 @@ const SvgPreview = ( return ( , - NeverProps extends keyof SVGProps, + RequiredProps extends keyof React.SVGProps, + NeverProps extends keyof React.SVGProps, > = Required, RequiredProps>> & Omit< React.SVGProps, diff --git a/docs/.vitepress/lib/SvgPreview/utils.ts b/docs/.vitepress/lib/SvgPreview/utils.ts index 04ba47f5c..993cbfe6f 100644 --- a/docs/.vitepress/lib/SvgPreview/utils.ts +++ b/docs/.vitepress/lib/SvgPreview/utils.ts @@ -53,10 +53,7 @@ export const getCommands = (src: string) => getNodes(src) .map(convertToPathNode) .flatMap(({ d, name }, idx) => - new SVGPathData(d) - .toAbs() - // @ts-expect-error `commands` is typed as a union of command types svgson does not narrow - .commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name })), + new SVGPathData(d).toAbs().commands.map((c, cIdx) => ({ ...c, id: idx, idx: cIdx, name })), ); const getPaths = (src: string) => { diff --git a/docs/.vitepress/lib/categories.ts b/docs/.vitepress/lib/categories.ts index dfc0268b6..10f5daf3a 100644 --- a/docs/.vitepress/lib/categories.ts +++ b/docs/.vitepress/lib/categories.ts @@ -4,7 +4,7 @@ import { Category, IconEntity } from '../theme/types'; const directory = path.join(process.cwd(), '../categories'); -export function getAllCategoryFiles(): Category[] { +export function getAllCategoryFiles(): Omit[] { const fileNames = fs.readdirSync(directory).filter((file) => path.extname(file) === '.json'); return fileNames.map((fileName) => { @@ -16,14 +16,15 @@ export function getAllCategoryFiles(): Category[] { return { name, title: parsedFileContent.title, + description: parsedFileContent.description, }; }); } export function mapCategoryIconCount( - categories: Category[], + categories: Omit[], icons: { categories: IconEntity['categories'] }[], -) { +): Category[] { return categories.map((category) => ({ ...category, iconCount: icons.reduce( diff --git a/docs/.vitepress/lib/codeExamples/createCodeExamples.ts b/docs/.vitepress/lib/codeExamples/createCodeExamples.ts index ceafb02ca..976b79c4b 100644 --- a/docs/.vitepress/lib/codeExamples/createCodeExamples.ts +++ b/docs/.vitepress/lib/codeExamples/createCodeExamples.ts @@ -119,8 +119,7 @@ const highlighter = await createHighlighter({ }); export type ThemeOptions = - | ThemeRegistration - | { light: ThemeRegistration; dark: ThemeRegistration }; + ThemeRegistration | { light: ThemeRegistration; dark: ThemeRegistration }; export const highLightCode = async (code: string, lang: string, active?: boolean) => { const highlightedCode = highlighter diff --git a/docs/.vitepress/lib/codeExamples/highLightCode.ts b/docs/.vitepress/lib/codeExamples/highLightCode.ts index 2376d1432..42d7b5954 100644 --- a/docs/.vitepress/lib/codeExamples/highLightCode.ts +++ b/docs/.vitepress/lib/codeExamples/highLightCode.ts @@ -2,8 +2,7 @@ import { bundledLanguages, type ThemeRegistration } from 'shiki'; import { createHighlighter } from 'shiki'; export type ThemeOptions = - | ThemeRegistration - | { light: ThemeRegistration; dark: ThemeRegistration }; + ThemeRegistration | { light: ThemeRegistration; dark: ThemeRegistration }; const highLightCode = async (code: string, lang: string, active?: boolean) => { const highlighter = await createHighlighter({ diff --git a/docs/.vitepress/lib/getFallbackZip.tsx b/docs/.vitepress/lib/getFallbackZip.tsx index 6f776c47b..1c176430b 100644 --- a/docs/.vitepress/lib/getFallbackZip.tsx +++ b/docs/.vitepress/lib/getFallbackZip.tsx @@ -1,4 +1,5 @@ -import { createLucideIcon } from 'lucide-react/src/lucide-react'; +import React from 'react'; +import createLucideIcon from 'lucide-react/src/createLucideIcon'; import { type LucideProps, type IconNode } from 'lucide-react/src/types'; import { IconEntity } from '../theme/types'; import { renderToStaticMarkup } from 'react-dom/server'; diff --git a/docs/.vitepress/lib/icons.ts b/docs/.vitepress/lib/icons.ts index ece3f9bfd..05a78bbb0 100644 --- a/docs/.vitepress/lib/icons.ts +++ b/docs/.vitepress/lib/icons.ts @@ -20,6 +20,8 @@ export async function getData(name: string) { const iconNode = iconNodes[name]; const releaseData = releaseMeta?.[name] ?? { + fromFork: false, + awaitingRelease: false, createdRelease: { version: '0.0.0', date: DATE_OF_FORK, diff --git a/docs/.vitepress/markdown/example.ts b/docs/.vitepress/markdown/example.ts new file mode 100644 index 000000000..f38b949c1 --- /dev/null +++ b/docs/.vitepress/markdown/example.ts @@ -0,0 +1,218 @@ +import { readFileSync } from 'node:fs'; +import { dirname, isAbsolute, resolve } from 'node:path'; +import type { MarkdownIt, RenderRule } from './types'; +import container from 'markdown-it-container'; + +type ContainerArgs = [typeof container, string, { render: RenderRule }]; +type GuidanceType = 'do' | 'dont' | 'caution'; + +const isGuidanceOpenToken = (type: string) => + type === 'container_do_open' || + type === 'container_dont_open' || + type === 'container_caution_open'; + +const isGuidanceCloseToken = (type: string) => + type === 'container_do_close' || + type === 'container_dont_close' || + type === 'container_caution_close'; + +const parseInfo = (info: string, marker: string) => info.trim().slice(marker.length).trim(); + +const stripWrappingQuotes = (value: string) => { + const quote = value[0]; + + if ((quote === '"' || quote === "'") && value.at(-1) === quote) { + return value.slice(1, -1); + } + + return value; +}; + +const isRawSvgUrl = (url: string) => { + const [filePath, query = ''] = url.split('?'); + const params = new URLSearchParams(query); + + return filePath.endsWith('.svg') && params.get('raw') === 'true'; +}; + +const isImageUrl = (value: string) => { + const [filePath] = value.split('?'); + + return /\.(avif|gif|jpe?g|png|svg|webp)$/i.test(filePath); +}; + +const resolveMarkdownPath = (env: Record) => + (env.path as string | undefined) ?? + (env.file as string | undefined) ?? + (env.realPath as string | undefined); + +const renderImage = ( + url: string, + env: Record, + escapeAttr: (value: string) => string, +) => { + if (!isRawSvgUrl(url)) { + return ``; + } + + 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 `
${replacedSvg}
`; +}; + +const renderPreview = ( + value: string, + env: Record, + escapeHtml: (value: string) => string, + renderInline: (value: string) => string, +) => { + if (isImageUrl(value)) { + return renderImage(value, env, escapeHtml); + } + + return `
${renderInline(value)}
`; +}; + +const hasTopLevelContent = (tokens: Parameters[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[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[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[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 = { + do: ` Do`, + dont: ` Don't`, + caution: ` 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 ? '
\n' : ''; + + return `
\n${contentOpen}`; + } + + const exampleOpenIndex = getExampleOpenIndex(tokens, idx); + const contentClose = + hasTopLevelContent(tokens, exampleOpenIndex) && !hasGuidance(tokens, exampleOpenIndex) + ? '
\n' + : ''; + + return `${contentClose}\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) + ? '\n' + : ''; + const label = labels[type]; + + return `${contentClose}
${media}
${label}
\n`; + } + + return '
\n'; + }, + }, + ]; + + md.use(...createExampleContainer()); + md.use(...createGuidanceContainer('do')); + md.use(...createGuidanceContainer('dont')); + md.use(...createGuidanceContainer('caution')); +} diff --git a/docs/.vitepress/markdown/sandpack.ts b/docs/.vitepress/markdown/sandpack.ts index 94a137306..983a2ef90 100644 --- a/docs/.vitepress/markdown/sandpack.ts +++ b/docs/.vitepress/markdown/sandpack.ts @@ -1,6 +1,4 @@ -import type MarkdownIt from 'markdown-it'; -import type { RenderRule } from 'markdown-it/lib/renderer.mjs'; -import type Token from 'markdown-it/lib/token.mjs'; +import type { MarkdownIt, RenderRule, Token } from './types'; import container from 'markdown-it-container'; import sandpackTheme from '../theme/sandpackTheme.json'; diff --git a/docs/.vitepress/markdown/snackPlayer.ts b/docs/.vitepress/markdown/snackPlayer.ts index cb848d4ef..3005ed208 100644 --- a/docs/.vitepress/markdown/snackPlayer.ts +++ b/docs/.vitepress/markdown/snackPlayer.ts @@ -16,7 +16,7 @@ * /> */ -import type MarkdownIt from 'markdown-it'; +import type { MarkdownIt } from './types'; type SnackParams = Record; diff --git a/docs/.vitepress/markdown/types.ts b/docs/.vitepress/markdown/types.ts new file mode 100644 index 000000000..a71f15fa2 --- /dev/null +++ b/docs/.vitepress/markdown/types.ts @@ -0,0 +1,7 @@ +import type { MarkdownRenderer } from 'vitepress'; + +// VitePress 1.x renders with markdown-it 14, so derive the plugin types from its renderer +// instead of the markdown-it 15 package installed in docs. +export type MarkdownIt = MarkdownRenderer; +export type RenderRule = NonNullable; +export type Token = Parameters[0][number]; diff --git a/docs/.vitepress/sidebar/angular.ts b/docs/.vitepress/sidebar/angular.ts index df244e363..a0be89660 100644 --- a/docs/.vitepress/sidebar/angular.ts +++ b/docs/.vitepress/sidebar/angular.ts @@ -94,7 +94,7 @@ export const angularSidebar = [ items: [ { text: 'Accessibility in depth', - link: '/guide/accessibility', + link: '/how-to/accessibility', desc: 'Accessibility best practices', }, { diff --git a/docs/.vitepress/sidebar/astro.ts b/docs/.vitepress/sidebar/astro.ts index 4aaa77c69..e5b1e22e4 100644 --- a/docs/.vitepress/sidebar/astro.ts +++ b/docs/.vitepress/sidebar/astro.ts @@ -69,7 +69,7 @@ export const astroSidebar = [ items: [ { text: 'Accessibility in depth', - link: '/guide/accessibility', + link: '/how-to/accessibility', desc: 'Accessibility best practices', }, { diff --git a/docs/.vitepress/sidebar/index.ts b/docs/.vitepress/sidebar/index.ts index aaa1905f5..62fa9d001 100644 --- a/docs/.vitepress/sidebar/index.ts +++ b/docs/.vitepress/sidebar/index.ts @@ -36,6 +36,7 @@ const sidebar: Sidebar = { '/guide/react-native/': reactNativeSidebar, '/guide/angular/': angularSidebar, '/guide/static/': lucideStaticSidebar, + '/how-to': resourcesSidebar, '/community': resourcesSidebar, '/contribute': resourcesSidebar, '/license': resourcesSidebar, diff --git a/docs/.vitepress/sidebar/lucide.ts b/docs/.vitepress/sidebar/lucide.ts index b61c2d6f1..9bc14d3a5 100644 --- a/docs/.vitepress/sidebar/lucide.ts +++ b/docs/.vitepress/sidebar/lucide.ts @@ -79,7 +79,7 @@ export const lucideSidebar = [ items: [ { text: 'Accessibility in depth', - link: '/guide/accessibility', + link: '/how-to/accessibility', desc: 'Accessibility best practices', }, ], diff --git a/docs/.vitepress/sidebar/preact.ts b/docs/.vitepress/sidebar/preact.ts index 7ecf24fc6..dc3936e1a 100644 --- a/docs/.vitepress/sidebar/preact.ts +++ b/docs/.vitepress/sidebar/preact.ts @@ -89,7 +89,7 @@ export const preactSidebar = [ items: [ { text: 'Accessibility in depth', - link: '/guide/accessibility', + link: '/how-to/accessibility', desc: 'Accessibility best practices', }, { diff --git a/docs/.vitepress/sidebar/react.ts b/docs/.vitepress/sidebar/react.ts index 5d85386ea..97987f46f 100644 --- a/docs/.vitepress/sidebar/react.ts +++ b/docs/.vitepress/sidebar/react.ts @@ -100,7 +100,7 @@ export const reactSidebar = [ items: [ { text: 'Accessibility in depth', - link: '/guide/accessibility', + link: '/how-to/accessibility', desc: 'Accessibility best practices', }, { diff --git a/docs/.vitepress/sidebar/resources.ts b/docs/.vitepress/sidebar/resources.ts index 649815478..4706233f3 100644 --- a/docs/.vitepress/sidebar/resources.ts +++ b/docs/.vitepress/sidebar/resources.ts @@ -11,41 +11,110 @@ export const resourcesSidebar = [ link: '/community', }, { - text: 'Code of Conduct', + text: 'Code of conduct', link: '/code-of-conduct', }, { text: 'Brand logo statement', link: '/brand-logo-statement', }, + ], + }, + { + text: 'Contribute', + items: [ { - text: 'Contributing', - link: '/contribute', + text: 'Contribution guide', + link: '/contribute/', }, ], }, { - text: 'Designing icons', + text: 'How to use Lucide icons', items: [ { - text: 'Icon Design Principles', - link: '/contribute/icon-design-guide', + text: 'Getting started', + link: '/how-to/', }, { - text: 'Designing in Illustrator', - link: '/contribute/illustrator-guide', + text: 'Accessibility guide', + link: '/how-to/accessibility', + desc: 'Learn how to make icons readable, understandable, and usable with assistive technology.', }, { - text: 'Designing in Inkscape', - link: '/contribute/inkscape-guide', + text: 'How to pick the right icon', + link: '/how-to/icon-guide', + desc: 'Learn how to decide when icons belong in an interface and how to pick symbols people are likely to recognize.', + }, + ], + }, + { + text: 'Contributing icons', + items: [ + { + text: 'Getting started', + link: '/contribute/icons/', }, { - text: 'Designing in Figma', - link: '/contribute/figma-guide', + text: 'Design language', + link: '/contribute/icons/design-principles', + desc: 'Learn the visual principles that shape Lucide icons with side-by-side examples.', }, { - text: 'Designing in Affinity Designer', - link: '/contribute/affinity-designer-guide', + text: 'Design specification', + link: '/contribute/icons/specification', + desc: 'Review the technical requirements every Lucide icon must meet.', + }, + { + text: 'Naming conventions', + link: '/contribute/icons/naming-conventions', + desc: 'Learn how to choose clear, consistent, and predictable names for Lucide icons.', + }, + { + text: 'Metadata conventions', + link: '/contribute/icons/metadata-conventions', + desc: 'Learn how to write use cases, tags, categories, aliases, and other icon metadata.', + items: [ + { + text: 'Use case guide', + link: '/contribute/icons/metadata-conventions/use-case-guide', + desc: '', + }, + { + text: 'Tag guide', + link: '/contribute/icons/metadata-conventions/tag-guide', + desc: '', + }, + ], + }, + { + text: 'SVG conventions', + link: '/contribute/icons/code-conventions', + desc: 'Learn how to structure and write Lucide SVG files.', + }, + { + text: 'Design software guides', + collapsed: false, + desc: 'Learn how to design and export Lucide icons with common vector tools.', + items: [ + { + text: 'Adobe Illustrator', + link: '/contribute/icons/illustrator-guide', + desc: '', + }, + { + text: 'Inkscape', + link: '/contribute/icons/inkscape-guide', + }, + { + text: 'Figma', + link: '/contribute/icons/figma-guide', + }, + { + text: 'Affinity Designer', + link: '/contribute/icons/affinity-designer-guide', + }, + ], }, ], }, diff --git a/docs/.vitepress/sidebar/solid.ts b/docs/.vitepress/sidebar/solid.ts index 78f1855a2..6943891ab 100644 --- a/docs/.vitepress/sidebar/solid.ts +++ b/docs/.vitepress/sidebar/solid.ts @@ -90,7 +90,7 @@ export const solidSidebar = [ items: [ { text: 'Accessibility in depth', - link: '/guide/accessibility', + link: '/how-to/accessibility', desc: 'Accessibility best practices', }, { diff --git a/docs/.vitepress/sidebar/static.ts b/docs/.vitepress/sidebar/static.ts index 6628623a6..d21a30e2a 100644 --- a/docs/.vitepress/sidebar/static.ts +++ b/docs/.vitepress/sidebar/static.ts @@ -75,7 +75,7 @@ export const lucideStaticSidebar = [ items: [ { text: 'Accessibility in depth', - link: '/guide/accessibility', + link: '/how-to/accessibility', desc: 'Accessibility best practices', }, { diff --git a/docs/.vitepress/sidebar/svelte.ts b/docs/.vitepress/sidebar/svelte.ts index 80dbf365f..a12fca40d 100644 --- a/docs/.vitepress/sidebar/svelte.ts +++ b/docs/.vitepress/sidebar/svelte.ts @@ -79,7 +79,7 @@ export const svelteSidebar = [ items: [ { text: 'Accessibility in depth', - link: '/guide/accessibility', + link: '/how-to/accessibility', desc: 'Accessibility best practices', }, { diff --git a/docs/.vitepress/sidebar/vue.ts b/docs/.vitepress/sidebar/vue.ts index 3840ef7bd..00452d6c7 100644 --- a/docs/.vitepress/sidebar/vue.ts +++ b/docs/.vitepress/sidebar/vue.ts @@ -85,7 +85,7 @@ export const vueSidebar = [ items: [ { text: 'Accessibility in depth', - link: '/guide/accessibility', + link: '/how-to/accessibility', desc: 'Accessibility best practices', }, { diff --git a/docs/.vitepress/theme/components/SurveyAskOverlay.vue b/docs/.vitepress/theme/components/SurveyAskOverlay.vue index a8e42e81f..e1e7947a7 100644 --- a/docs/.vitepress/theme/components/SurveyAskOverlay.vue +++ b/docs/.vitepress/theme/components/SurveyAskOverlay.vue @@ -1,6 +1,6 @@ + diff --git a/docs/.vitepress/theme/components/base/RangeSlider.vue b/docs/.vitepress/theme/components/base/RangeSlider.vue index bfef6d091..871452650 100644 --- a/docs/.vitepress/theme/components/base/RangeSlider.vue +++ b/docs/.vitepress/theme/components/base/RangeSlider.vue @@ -34,7 +34,7 @@ const percentage = computed(() => `${((Number(props.modelValue) - props. type="range" v-bind="$attrs" v-bind:value="modelValue" - v-on:input="$emit('update:modelValue', Number($event.target.value))" + v-on:input="$emit('update:modelValue', Number(($event.target as HTMLInputElement).value))" :min="min" :max="max" :step="step" diff --git a/docs/.vitepress/theme/components/base/ResetButton.vue b/docs/.vitepress/theme/components/base/ResetButton.vue index ff205f796..8d04d8037 100644 --- a/docs/.vitepress/theme/components/base/ResetButton.vue +++ b/docs/.vitepress/theme/components/base/ResetButton.vue @@ -1,20 +1,22 @@ diff --git a/docs/.vitepress/theme/components/home/HomeIconCustomizerIcons.vue b/docs/.vitepress/theme/components/home/HomeIconCustomizerIcons.vue index 631490344..bfd558982 100644 --- a/docs/.vitepress/theme/components/home/HomeIconCustomizerIcons.vue +++ b/docs/.vitepress/theme/components/home/HomeIconCustomizerIcons.vue @@ -1,29 +1,35 @@ - ``` ::: @@ -86,7 +88,7 @@ You can also use the `text` SVG element to add text to your icon. ```vue src/App.vue [active] - ``` ::: diff --git a/docs/guide/vue/advanced/global-styling.md b/docs/guide/vue/advanced/global-styling.md index eb7ce6ed5..25070059f 100644 --- a/docs/guide/vue/advanced/global-styling.md +++ b/docs/guide/vue/advanced/global-styling.md @@ -2,6 +2,7 @@ title: Global Styling - Vue description: Learn how to style all Lucide icons globally in your Vue application using CSS or the Lucide context provider. --- + @@ -74,9 +75,9 @@ import { Sandwich, Wine, Dessert, -} from "@lucide/vue"; +} from '@lucide/vue'; -import "./icon.css"; +import './icon.css'; ``` + ::: -### Absolute stroke width +### Non-scaling strokes -For global absolute stroke width styling the `vector-effect: non-scaling-stroke` CSS property can be applied to the children. This will keep the stroke-width the same size no matter the size of the icon. See [absolute-stroke-width](../basics/stroke-width.md#absolute-stroke-width) for more info. +For global non-scaling stroke width styling the `vector-effect: non-scaling-stroke` CSS property can be applied to the children. This will keep the stroke-width the same size no matter the size of the icon. See [non-scaling strokes](../basics/stroke-width.md#non-scaling-strokes) for more info. ::: sandpack {template=vue editorHeight=300 editorWidthPercentage=55 dependencies="@lucide/vue"} @@ -132,9 +134,9 @@ import { Map, CloudMoon, Sparkles, -} from "@lucide/vue"; +} from '@lucide/vue'; -import "./icon.css"; +import './icon.css'; - ``` + ::: diff --git a/docs/guide/vue/advanced/typescript.md b/docs/guide/vue/advanced/typescript.md index 4114c1247..096cb0ade 100644 --- a/docs/guide/vue/advanced/typescript.md +++ b/docs/guide/vue/advanced/typescript.md @@ -2,6 +2,7 @@ title: Typescript - Vue description: Learn about the different types exported by the `@lucide/vue` package and how to use them in your Vue application. --- + # TypeScript Support List of exported types from the `@lucide/vue` package. @@ -16,15 +17,21 @@ interface LucideProps { size?: number | string; color?: string; strokeWidth?: number; + nonScalingStroke?: boolean; + /** + * @deprecated + */ absoluteStrokeWidth?: boolean; [key: string]: any; // Any other SVG attributes } ``` ### Using `LucideProps` + You can use the `LucideProps` interface to type props for your custom icon components. ::: code-group + ```vue [IconWrapper.vue] ``` + ::: diff --git a/docs/guide/vue/basics/stroke-width.md b/docs/guide/vue/basics/stroke-width.md index c41ac174f..693f9db08 100644 --- a/docs/guide/vue/basics/stroke-width.md +++ b/docs/guide/vue/basics/stroke-width.md @@ -1,7 +1,8 @@ --- title: Stroke width - Vue -description: Learn how to adjust the stroke width of icons in your Vue application using the `strokeWidth` prop or adjust the strokeWidth appearance using the `absoluteStrokeWidth` prop. +description: Learn how to adjust the stroke width of icons in your Vue application using the `strokeWidth` prop or adjust the strokeWidth appearance using the `nonScalingStroke` prop. --- + @@ -19,42 +20,43 @@ The `strokeWidth` can be adjusted to create a different look of the icons. ```vue src/App.vue [active] ``` + ::: -## Absolute stroke width +## Non-scaling strokes -When adjusting the `size` prop the size of the stroke width will be relative to the size of the icon, this is the default SVG behavior. The `absoluteStrokeWidth` prop is introduced to adjust this behavior to make the stroke width constant no matter the size of the icon. +When adjusting the `size` prop the size of the stroke width will be relative to the size of the icon, this is the default SVG behavior. The `nonScalingStroke` prop is introduced to adjust this behavior to make the stroke width constant no matter the size of the icon. -This means that when `absoluteStrokeWidth` is enabled and the `size` of the icons is set to `48px` the `strokeWidth` will still be `2px` on the screen. +This means that when `nonScalingStroke` is enabled and the `size` of the icons is set to `48px` the `strokeWidth` will still be `2px` on the screen. Note `2px` is the default stroke width for a Lucide icon, this can be adjusted to all sizes. -![Absolute stroke width comparison](../../../images/absolute-stroke-width-compare.png?raw=true "Absolute stroke width comparison") + -### Adjusting stroke width with `absoluteStrokeWidth` prop +### Adjusting stroke width with `nonScalingStroke` prop -Setting `absoluteStrokeWidth` to `true` will make the stroke width absolute. +Setting `nonScalingStroke` to `true` will make the stroke width non-scaling. ::: sandpack {template=vue showTabs=false editorHeight=320 editorWidthPercentage=60 dependencies="@lucide/vue"} ```vue src/App.vue [active] - ``` + ::: diff --git a/docs/guide/vue/getting-started.md b/docs/guide/vue/getting-started.md index 277071d51..ce22268b9 100644 --- a/docs/guide/vue/getting-started.md +++ b/docs/guide/vue/getting-started.md @@ -2,6 +2,7 @@ title: Getting started - Vue description: This guide will help you get started with Lucide in your Vue project. --- + + +# Accessibility in depth + +Icons are pictures that convey meaning without using words. + +They can help people scan an interface, recognize actions, and understand status quickly. + +Not everyone understands icons easily. Use the following accessibility rules when you add icons to an interface. + +::: tip +Lucide icons are hidden from screen readers by default with `aria-hidden="true"`. + +Add accessible labels when icons carry meaning or act as controls. +::: + +## Provide visible labels + +Icons can improve scanning, but they aren't a replacement for text. + +Text helps people understand actions and navigation more reliably than icons alone. + +:::: example +::: do +Provide a written description of your interactive elements. +::: +::: dont +Don't rely on icons alone to communicate what elements do. +::: +:::: + +## Provide enough contrast + +Use enough contrast between an icon and its background. Low contrast makes icons harder to see for people with low vision or color vision deficiencies. + +Follow [WCAG 2.1 SC 1.4.3](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html) when contrast requirements apply. + +:::: example +::: do ../images/a11y/contrast_do.svg?raw=true +Use a contrast ratio of at least 4.5:1. +::: +::: dont ../images/a11y/contrast_dont.svg?raw=true +Don't use low contrast. +::: +:::: + +## Do not rely on color alone + +Use non-color cues like shape, shading, or text to mark state, status, and validity. Color can support meaning, but it must not be the only cue. + +:::: example +::: do ../images/a11y/state_do.svg?raw=true +Mark state with non-color visual cues too. +::: +::: dont ../images/a11y/state_dont.svg?raw=true +Don't mark state only with color. +::: +:::: + +## Make interactive icons focusable + +Wrap interactive icons in semantic elements. Icons that perform actions need keyboard access and clear feedback when activated. + +:::: example +::: do ../images/a11y/interactive_icons_do.svg?raw=true +Wrap interactive icons in semantic elements, such as buttons, so they are focusable. +::: +::: dont ../images/a11y/interactive_icons_dont.svg?raw=true +Don't leave interactive icons unable to be focused. +::: +:::: + +## Use a large enough target size + +Use a large enough target size for interactive icon controls. Small targets can be difficult to click or tap. + +:::: example +::: do ../images/a11y/target_size_do.svg?raw=true +Keep the target size at a minimum of 44 by 44 pixels. +::: +::: dont ../images/a11y/target_size_dont.svg?raw=true +Don't make the target size smaller than 44 by 44 pixels. +::: +:::: + +The icon itself does not need to be 44 by 44 pixels. The interactive wrapper needs to meet the target size. + +## Choose universal symbols when possible + +Choose icons that are widely recognized for your use case. Avoid icons that only make sense to a narrow audience. + +:::: example +::: do +Pick icons that are universal symbols for your use case. +::: +::: dont +Don't pick icons that are not universally recognizable for your use case. +::: +:::: + +## Avoid cultural or linguistic shortcuts + +Choose icons with a clear connection to the concept. Puns, idioms, and culture-specific references can be difficult to understand or translate. + +:::: example +::: do +Pick icons with a clear connection to the concept you want to represent. +::: +::: dont +Don't pick icons based on puns or other cultural or linguistic cues. +::: +:::: + +## Use different icons for different meanings + +Use different icons for different meanings. Reusing one icon for unrelated concepts makes the interface harder to learn. + +:::: example +::: do ../images/a11y/distinct_use_case_do.svg?raw=true +Use different icons for distinct purposes or meanings. +::: +::: dont ../images/a11y/distinct_use_case_dont.svg?raw=true +Don't use the same icon for multiple distinct purposes or meanings, especially in the same context. +::: +:::: + +## Use the same icon for the same meaning + +Use the same icon for the same meaning. Consistent icon use helps people recognize familiar actions across an interface. + +:::: example +::: do ../images/a11y/same_use_case_do.svg?raw=true +Use the same icon for the same purpose or function. +::: +::: dont ../images/a11y/same_use_case_dont.svg?raw=true +Don't use different icons for the same purpose or function. +::: +:::: + +## Hide decorative icons from assistive technology + +Keep decorative icons hidden from screen readers. Decorative icons repeat nearby text or add visual styling without adding meaning. + +:::: example +::: do ../images/a11y/decorative_icons_do.svg?raw=true +Omit `aria-label` on decorative icons. +::: +::: dont ../images/a11y/decorative_icons_dont.svg?raw=true +Don't provide accessible labels to decorative icons. +::: +:::: + +## Label functional standalone icons carefully + +Avoid standalone functional icons when possible. If an icon carries meaning without nearby text or an interactive wrapper, give that icon an accessible label. + +:::: example +::: do ../images/a11y/functional_icons_do.svg?raw=true +Provide an accessible label for functional icons, but avoid standalone functional icons when possible. +::: +::: dont ../images/a11y/functional_icons_dont.svg?raw=true +Don't provide an accessible label for purely decorative icons. +::: +:::: + +In many cases, add visible text, place the icon inside a badge or button, or add a tooltip to the meaningful wrapper instead. + +## Label icon buttons on the button + +Give icon buttons an accessible name. Put the name on the button or in visually hidden text inside the button, not on the icon. + +:::: example +::: do ../images/a11y/icon_button_do.svg?raw=true +Provide the accessible label for icon buttons themselves. +::: +::: dont ../images/a11y/icon_button_dont.svg?raw=true +Don't provide the accessible label for icons on icon buttons. +::: +:::: + +::: details Code examples + +```tsx +// Don't do this + + +// Don't do this either + + +// This works, but visually hidden text is usually more reliable + + +// Prefer this + +``` + +::: + +## A note on `aria-label` + +You can provide accessible labels with `aria-label`, but visually hidden text is often more reliable. + +Read [why `aria-label` might not be the best solution](https://gomakethings.com/revisting-aria-label-versus-a-visually-hidden-class/) for more context. + +### Radix UI + +Use [Radix UI's built-in accessible icon utility component](https://www.radix-ui.com/primitives/docs/utilities/accessible-icon). + +```tsx +import { ArrowRightIcon } from 'lucide-react'; +import { AccessibleIcon } from '@radix-ui/react-accessible-icon'; + + + +; +``` + +### Bootstrap + +```html +
+ + Phone number +
+``` + +### Tailwind CSS + +```html +
+ + Phone number +
+``` + +If you are not sure which hiding technique to use, read [how to hide content](https://www.a11yproject.com/posts/how-to-hide-content/). + +## Further resources + +Use these accessibility resources: + +- [Web Content Accessibility Guidelines (WCAG) 2.1](https://www.w3.org/TR/WCAG21/) +- [Web Accessibility Initiative (WAI)](https://www.w3.org/WAI/) +- [Learn accessibility on web.dev](https://web.dev/learn/accessibility) +- [Inclusive Components](https://inclusive-components.design/) +- [A11yTalks](https://www.a11ytalks.com/) +- [A11y automation tracker](https://a11y-automation.dev/) +- [The A11Y Project](https://www.a11yproject.com/) diff --git a/docs/how-to/icon-guide.md b/docs/how-to/icon-guide.md new file mode 100644 index 000000000..bc6f48a26 --- /dev/null +++ b/docs/how-to/icon-guide.md @@ -0,0 +1,190 @@ +--- +title: How to pick the right icon +description: Learn when to use icons, when to avoid them, and how to choose icons that make interfaces easier to understand. +--- + + + +# How to pick the right icon + +Icons can make an interface easier to scan. They work best when people already understand the symbol or when the icon appears with a clear text label. + +Use text instead of an icon when the meaning is complex, abstract, or easy to misunderstand. + +## Match the interface style + +Use icons with the same visual language in the same interface. Mixed styles can make related controls look unrelated. + +:::: example +::: do +Use one icon style for related controls. +::: +::: dont +Don't mix unrelated icon styles in the same interface. +::: +:::: + +## Use familiar icons for familiar actions + +Icons work well for common actions and destinations that people already recognize from other interfaces. + +:::: example +::: do +Use icons for common actions with widely understood symbols. +::: +::: dont "strategy", "optimize", "synergy", "innovation" +Don't use icons alone for abstract concepts with no common symbol. +::: +:::: + +## Use icons as visual anchors + +Icons can help people scan lists, menus, cards, and repeated controls. The icon needs to support the text, not compete with it. + +:::: example +::: do ../images/how-to/visual_anchor_do.svg?raw=true +Use icons to make associated options easier to scan. +::: +::: dont ../images/how-to/visual_anchor_dont.svg?raw=true +Don't add icons to every line when they only create visual clutter. +::: +:::: + +## Use icons in compact interfaces + +Icons can help in tight spaces, such as mobile navigation, toolbars, tables, and compact controls. Keep labels when the icon is not obvious. + +:::: example +::: do ../images/how-to/compact_interfaces_labels_do.svg?raw=true +Keep labels in all layout variants when space allows. +::: +::: caution ../images/how-to/compact_interfaces_labels_dont.svg?raw=true +Use icon-only controls only when a visible label does not fit. + +In these cases, make sure that the label is accessible via hover, focus, or tap. +::: +:::: + +:::: example +::: do ../images/how-to/compact_interfaces_do.svg?raw=true +Keep text labels next to icons in compact layouts, even when space is sparse. +::: +::: dont ../images/how-to/compact_interfaces_dont.svg?raw=true +Don't remove labels in compact layouts when the meaning without them is unclear. +::: +:::: + +:::: example +::: caution ../images/how-to/compact_interfaces_caution_1.svg?raw=true +Use icon-only controls only when the icon represents a widely understood action. +::: +::: caution ../images/how-to/compact_interfaces_caution_2.svg?raw=true +Use icon-only controls only when the label is available on hover, focus, or tap. +::: +:::: + +## Choose the clearest symbol + +Use a familiar symbol when one exists. Novel symbols make people stop and interpret the interface. + +:::: example +::: do `trash`
for deleting an image
+Use a familiar symbol for a familiar action. +::: +::: dont `image-off`
for deleting an image
+Don't make people learn a new symbol for a common action. +::: +:::: + +## Match the context, not every detail + +Choose the icon that makes the action clearest in context. A more specific icon is better only when the extra detail helps people understand the action. + +:::: example +::: do ../images/how-to/appropriate_context_do.svg?raw=true +Use clear action icons when the surrounding interface already provides the object context. +::: +::: dont ../images/how-to/appropriate_context_dont.svg?raw=true +Don't add object details when the context already makes the object clear. +::: +:::: + +## Keep icon meaning consistent + +Use the same icon for the same meaning, and use different icons for different meanings. + +:::: example +::: do ../images/how-to/consistent_icon_meaning_do.svg?raw=true +Use different icons for distinct purposes or meanings. +::: +::: dont ../images/how-to/consistent_icon_meaning_dont.svg?raw=true +Don't use the same icon for multiple distinct purposes or meanings. +::: +:::: + +:::: example +::: do ../images/how-to/consistent_icon_function_do.svg?raw=true +Use the same icon for the same purpose or function. +::: +::: dont ../images/how-to/consistent_icon_function_dont.svg?raw=true +Don't use different icons for the same purpose or function. +::: +:::: + +## Do not iconize repeated status decoration + +Avoid adding an icon to every repeated status marker when color, weight, text, or layout already communicates the state. Repeated status icons can turn useful list structure into visual noise. + +:::: example +::: do ../images/how-to/repeated_status_do.svg?raw=true +Use the lightest cue that makes the status clear. +::: +::: dont ../images/how-to/repeated_status_dont.svg?raw=true +Don't add repeated status icons when they only restate what the list already shows. +::: +:::: + +## Do not replace built-in control cues + +Do not add icons to controls that already show people how they work. Native controls are easier to recognize when they behave and look familiar. + +:::: example +::: do ../images/how-to/native_ui_elements_do.svg?raw=true +Use the built-in visual cue when the control already communicates its behavior. +::: +::: dont ../images/how-to/native_ui_elements_dont.svg?raw=true +Don't replace native controls with similarly themed interactive icons. +::: +:::: + + diff --git a/docs/how-to/index.md b/docs/how-to/index.md new file mode 100644 index 000000000..176eb27a8 --- /dev/null +++ b/docs/how-to/index.md @@ -0,0 +1,29 @@ +--- +title: How to use Lucide icons +description: Learn how to use icons in an accessible, user-friendly way +--- + + + +# How to use Lucide icons + +Icons can make an interface easier to scan, but they only help when their meaning is clear. These guides explain how to choose icons that support the interface, when to pair icons with text, and when the best icon to use is no icon at all. + +Use the accessibility guide to make icons readable, understandable, and usable with assistive technology. Use the icon guide to decide when icons belong in an interface and how to pick symbols people are likely to recognize. + +## What this guide covers + + + + + +## Further resources + +- [The no-bullshit guide to icon design and usage](https://erik-engheim.medium.com/the-no-bullshit-guide-to-icon-design-and-usage-40948878dbbb) by Erik Engheim +- [On icons](https://ia.net/topics/on-icons/) by iA +- [How to use icons in UI and UX design](https://blog.thenounproject.com/how-to-use-icons-in-ui-and-ux-design-best-practices/) by The Noun Project +- [UX Myth: Icons enhance usability](https://uxmyths.com/post/715009009/myth-icons-enhance-usability) by UX Myths diff --git a/docs/icons/[name].md b/docs/icons/[name].md index f238e4811..9f88fe0e4 100644 --- a/docs/icons/[name].md +++ b/docs/icons/[name].md @@ -12,6 +12,7 @@ import { computed } from 'vue' import { useData } from 'vitepress' import IconPreview from '~/.vitepress/theme/components/icons/IconPreview.vue' import IconPreviewSmall from '~/.vitepress/theme/components/icons/IconPreviewSmall.vue' +import EditInStudioButton from '~/.vitepress/theme/components/icons/EditInStudioButton.vue' import IconInfo from '~/.vitepress/theme/components/icons/IconInfo.vue' import IconContributors from '~/.vitepress/theme/components/icons/IconContributors.vue' import IconShowcase from '~/.vitepress/theme/components/icons/IconShowcase.vue' @@ -19,9 +20,11 @@ import RelatedIcons from '~/.vitepress/theme/components/icons/RelatedIcons.vue' import CodeGroup from '~/.vitepress/theme/components/base/CodeGroup.vue' import Badge from '~/.vitepress/theme/components/base/Badge.vue' import Label from '~/.vitepress/theme/components/base/Label.vue' +import PageTabs from '~/.vitepress/theme/components/base/PageTabs.vue' import { data } from './codeExamples.data' import { toCamelCase, toPascalCase } from '@lucide/shared' import { satisfies } from 'semver' +import CarbonAdOverlay from '~/.vitepress/theme/components/icons/CarbonAdOverlay.vue'; const { params } = useData() @@ -49,75 +52,92 @@ function releaseTagLink(version) { } -
-
+
+
+ +
+
-
-
-
+ +
+
-
+
+ + Unreleased +
+
- v{{params.createdRelease.version}} + {{params.createdRelease.version}}
- v{{params.changedRelease.version}} + {{params.changedRelease.version}}
- +
+
+
+ + + + + -
- + + - -
+ - diff --git a/docs/icons/icons.data.ts b/docs/icons/icons.data.ts index 0284b270a..18c1cf703 100644 --- a/docs/icons/icons.data.ts +++ b/docs/icons/icons.data.ts @@ -3,17 +3,28 @@ import * as iconDetails from '../.vitepress/data/iconDetails'; export default { async load() { return { - icons: Object.entries(iconDetails).map( - ([, { name, iconNode, popularity, createdRelease, aliases = [] }]) => ({ - name, - iconNode, - popularity: popularity?.count ?? 0, - aliases: aliases - .map((alias) => (typeof alias === 'string' ? alias : alias?.name)) - .filter((alias): alias is string => Boolean(alias)), - createdRelease, - }), - ), + icons: Object.entries(iconDetails) + .map( + ([ + , + { name, iconNode, popularity, createdRelease, awaitingRelease, aliases = [], fromFork }, + ]) => ({ + name, + iconNode, + popularity: popularity?.count ?? 0, + aliases: aliases + .map((alias) => (typeof alias === 'string' ? alias : alias?.name)) + .filter((alias): alias is string => Boolean(alias)), + createdRelease, + awaitingRelease, + fromFork, + }), + // We hide icons that are not released yet, we make exceptions for preview environments + ) + .filter( + ({ awaitingRelease }) => + !awaitingRelease || process.env.VITE_SHOW_AWAITING_RELEASE === 'true', + ), }; }, }; diff --git a/docs/icons/lab/[name].paths.ts b/docs/icons/lab/[name].paths.ts index 6954ee935..be0528c10 100644 --- a/docs/icons/lab/[name].paths.ts +++ b/docs/icons/lab/[name].paths.ts @@ -1,11 +1,10 @@ import iconNodes from '../../.vitepress/data/lab/iconNodes'; import iconPopularity from '../../.vitepress/data/lab/iconPopularity'; import iconMetaData from '../../.vitepress/data/lab/iconMetaData'; -import { type IconEntity } from '../../.vitepress/theme/types'; export default { paths: async () => { - return (Object.entries(iconNodes) as unknown as IconEntity[]).map(([name, iconNode]) => { + return Object.entries(iconNodes).map(([name, iconNode]) => { const iconDetails = iconMetaData[name]; delete iconDetails['$schema']; diff --git a/docs/images/1px-border-radius.svg b/docs/images/1px-border-radius.svg index c9dc80e3b..273a5386d 100644 --- a/docs/images/1px-border-radius.svg +++ b/docs/images/1px-border-radius.svg @@ -1,92 +1,85 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/docs/images/1px-element-spacing.svg b/docs/images/1px-element-spacing.svg new file mode 100644 index 000000000..6a5a976f0 --- /dev/null +++ b/docs/images/1px-element-spacing.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/1px-inner-gap.svg b/docs/images/1px-inner-gap.svg new file mode 100644 index 000000000..f1ee88b60 --- /dev/null +++ b/docs/images/1px-inner-gap.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/images/1px-inner-spacing.svg b/docs/images/1px-inner-spacing.svg new file mode 100644 index 000000000..4863e30db --- /dev/null +++ b/docs/images/1px-inner-spacing.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/1px-padding-violation.svg b/docs/images/1px-padding-violation.svg new file mode 100644 index 000000000..d9538fac6 --- /dev/null +++ b/docs/images/1px-padding-violation.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/1px-padding.svg b/docs/images/1px-padding.svg index 2f95201c6..313a8c5a3 100644 --- a/docs/images/1px-padding.svg +++ b/docs/images/1px-padding.svg @@ -1,67 +1,61 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + diff --git a/docs/images/24px-24px-violation.svg b/docs/images/24px-24px-violation.svg new file mode 100644 index 000000000..cfd8b0aab --- /dev/null +++ b/docs/images/24px-24px-violation.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/docs/images/24px-24px.svg b/docs/images/24px-24px.svg index 5ed0597b5..549204db1 100644 --- a/docs/images/24px-24px.svg +++ b/docs/images/24px-24px.svg @@ -1,74 +1,4 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/docs/images/2px-border-radius.svg b/docs/images/2px-border-radius.svg index 72a1ffb90..9105ebfa3 100644 --- a/docs/images/2px-border-radius.svg +++ b/docs/images/2px-border-radius.svg @@ -1,72 +1,67 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + - - + + diff --git a/docs/images/2px-element-spacing-abrupt-cut.svg b/docs/images/2px-element-spacing-abrupt-cut.svg index f04e2d326..183e66bc0 100644 --- a/docs/images/2px-element-spacing-abrupt-cut.svg +++ b/docs/images/2px-element-spacing-abrupt-cut.svg @@ -1 +1,75 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/2px-element-spacing-connected.svg b/docs/images/2px-element-spacing-connected.svg index 45c60ac26..a12017164 100644 --- a/docs/images/2px-element-spacing-connected.svg +++ b/docs/images/2px-element-spacing-connected.svg @@ -1 +1,67 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/2px-element-spacing.svg b/docs/images/2px-element-spacing.svg index 1666b93f1..20852a263 100644 --- a/docs/images/2px-element-spacing.svg +++ b/docs/images/2px-element-spacing.svg @@ -1,76 +1,69 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/2px-inner-gap.svg b/docs/images/2px-inner-gap.svg new file mode 100644 index 000000000..f83b17fda --- /dev/null +++ b/docs/images/2px-inner-gap.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/images/2px-inner-spacing.svg b/docs/images/2px-inner-spacing.svg new file mode 100644 index 000000000..ca418d7c9 --- /dev/null +++ b/docs/images/2px-inner-spacing.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/2px-stroke-violation.svg b/docs/images/2px-stroke-violation.svg new file mode 100644 index 000000000..7e2d935e3 --- /dev/null +++ b/docs/images/2px-stroke-violation.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/2px-stroke.svg b/docs/images/2px-stroke.svg index eaa09caba..966b2aced 100644 --- a/docs/images/2px-stroke.svg +++ b/docs/images/2px-stroke.svg @@ -1,71 +1,66 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/docs/images/a11y/alttext-buttons.svg b/docs/images/a11y/alttext-buttons.svg deleted file mode 100644 index 51874f8b3..000000000 --- a/docs/images/a11y/alttext-buttons.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - Don't provide accessible labels to - decorative icons. - - - - - - - - - - - - - - - Do omit aria-label on decorative icons. - - - - - - - - diff --git a/docs/images/a11y/alttext-iconbuttons.svg b/docs/images/a11y/alttext-iconbuttons.svg deleted file mode 100644 index f715e9842..000000000 --- a/docs/images/a11y/alttext-iconbuttons.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - Don't provide accessible labels for - icons on icon buttons. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/a11y/alttext-standalone.svg b/docs/images/a11y/alttext-standalone.svg deleted file mode 100644 index 23e90bbf9..000000000 --- a/docs/images/a11y/alttext-standalone.svg +++ /dev/null @@ -1,92 +0,0 @@ - - In short: provide accessible label for semantic icons, but not for decorative icons. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/a11y/consistency.svg b/docs/images/a11y/consistency.svg deleted file mode 100644 index db303455d..000000000 --- a/docs/images/a11y/consistency.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - For example: Don’t use the same icon for multiple distinct purposes or meanings. Don’t use different icons for the same purpose or function. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/a11y/contrast.svg b/docs/images/a11y/contrast.svg deleted file mode 100644 index b361a03ec..000000000 --- a/docs/images/a11y/contrast.svg +++ /dev/null @@ -1,20 +0,0 @@ - - In short: use a contrast ratio of at least 4.5:1 - - - - - - - - Don't use low contrast. - - - - - - - - Do use a contrast ratio of at least 4.5:1. - - diff --git a/docs/images/a11y/contrast_do.svg b/docs/images/a11y/contrast_do.svg new file mode 100644 index 000000000..052e7368a --- /dev/null +++ b/docs/images/a11y/contrast_do.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/images/a11y/contrast_dont.svg b/docs/images/a11y/contrast_dont.svg new file mode 100644 index 000000000..1a9326c5e --- /dev/null +++ b/docs/images/a11y/contrast_dont.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/images/a11y/decorative_icons_do.svg b/docs/images/a11y/decorative_icons_do.svg new file mode 100644 index 000000000..e290fc74a --- /dev/null +++ b/docs/images/a11y/decorative_icons_do.svg @@ -0,0 +1,14 @@ + + + + +
+ + + + + + + + +
diff --git a/docs/images/a11y/decorative_icons_dont.svg b/docs/images/a11y/decorative_icons_dont.svg new file mode 100644 index 000000000..6be329360 --- /dev/null +++ b/docs/images/a11y/decorative_icons_dont.svg @@ -0,0 +1,14 @@ + + + + +
+ + + + + + + + +
diff --git a/docs/images/a11y/distinct_use_case_do.svg b/docs/images/a11y/distinct_use_case_do.svg new file mode 100644 index 000000000..1680ede4f --- /dev/null +++ b/docs/images/a11y/distinct_use_case_do.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/docs/images/a11y/distinct_use_case_dont.svg b/docs/images/a11y/distinct_use_case_dont.svg new file mode 100644 index 000000000..f2577e5ac --- /dev/null +++ b/docs/images/a11y/distinct_use_case_dont.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/docs/images/a11y/functional_icons_do.svg b/docs/images/a11y/functional_icons_do.svg new file mode 100644 index 000000000..0e7df67a7 --- /dev/null +++ b/docs/images/a11y/functional_icons_do.svg @@ -0,0 +1,35 @@ + + +
+ + + + + + + + + + +
+ + + + + + + + + +
+ + + + + + + + + + +
diff --git a/docs/images/a11y/functional_icons_dont.svg b/docs/images/a11y/functional_icons_dont.svg new file mode 100644 index 000000000..6f42c2505 --- /dev/null +++ b/docs/images/a11y/functional_icons_dont.svg @@ -0,0 +1,39 @@ + + + + +
+ + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + +
diff --git a/docs/images/a11y/icon_button_do.svg b/docs/images/a11y/icon_button_do.svg new file mode 100644 index 000000000..7720a4665 --- /dev/null +++ b/docs/images/a11y/icon_button_do.svg @@ -0,0 +1,14 @@ + + + + + +
+ + + + + + + +
diff --git a/docs/images/a11y/icon_button_dont.svg b/docs/images/a11y/icon_button_dont.svg new file mode 100644 index 000000000..faa496837 --- /dev/null +++ b/docs/images/a11y/icon_button_dont.svg @@ -0,0 +1,14 @@ + + +
+ + + + + + + + + + +
diff --git a/docs/images/a11y/interactive_icons_do.svg b/docs/images/a11y/interactive_icons_do.svg new file mode 100644 index 000000000..3cb98c59b --- /dev/null +++ b/docs/images/a11y/interactive_icons_do.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/docs/images/a11y/interactive_icons_dont.svg b/docs/images/a11y/interactive_icons_dont.svg new file mode 100644 index 000000000..e12f1248b --- /dev/null +++ b/docs/images/a11y/interactive_icons_dont.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/images/a11y/interactivity.svg b/docs/images/a11y/interactivity.svg deleted file mode 100644 index bf338f345..000000000 --- a/docs/images/a11y/interactivity.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/docs/images/a11y/meaningfulness.svg b/docs/images/a11y/meaningfulness.svg deleted file mode 100644 index 647d3c487..000000000 --- a/docs/images/a11y/meaningfulness.svg +++ /dev/null @@ -1,94 +0,0 @@ - - For example: Use universally understandable symbols and don't base your choice of icon on puns. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/a11y/same_use_case_do.svg b/docs/images/a11y/same_use_case_do.svg new file mode 100644 index 000000000..c81443da4 --- /dev/null +++ b/docs/images/a11y/same_use_case_do.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/docs/images/a11y/same_use_case_dont.svg b/docs/images/a11y/same_use_case_dont.svg new file mode 100644 index 000000000..f2c56ee94 --- /dev/null +++ b/docs/images/a11y/same_use_case_dont.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/docs/images/a11y/state_do.svg b/docs/images/a11y/state_do.svg new file mode 100644 index 000000000..e28747e22 --- /dev/null +++ b/docs/images/a11y/state_do.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/a11y/state_dont.svg b/docs/images/a11y/state_dont.svg new file mode 100644 index 000000000..0c8825b0b --- /dev/null +++ b/docs/images/a11y/state_dont.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/a11y/target-size.svg b/docs/images/a11y/target-size.svg deleted file mode 100644 index e5a5951d4..000000000 --- a/docs/images/a11y/target-size.svg +++ /dev/null @@ -1,49 +0,0 @@ - - Example of using target size correctly - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/a11y/target_size_do.svg b/docs/images/a11y/target_size_do.svg new file mode 100644 index 000000000..51329578a --- /dev/null +++ b/docs/images/a11y/target_size_do.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/a11y/target_size_dont.svg b/docs/images/a11y/target_size_dont.svg new file mode 100644 index 000000000..b9d95dd87 --- /dev/null +++ b/docs/images/a11y/target_size_dont.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/a11y/use-of-color.svg b/docs/images/a11y/use-of-color.svg deleted file mode 100644 index 573c44d65..000000000 --- a/docs/images/a11y/use-of-color.svg +++ /dev/null @@ -1,191 +0,0 @@ - - For example: Don’t mark state with color, mark it with distinct visuals. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/a11y/visible-labels.svg b/docs/images/a11y/visible-labels.svg deleted file mode 100644 index fbbf54d19..000000000 --- a/docs/images/a11y/visible-labels.svg +++ /dev/null @@ -1,23 +0,0 @@ - - In short: Don’t rely on communicating the function of elements by icons alone. Do also provide a written description of the your interactive elements. For example: write out "On this page" on your on-page navigation element. - - - - - - - - Don't rely on communicating the - function of elements by icons alone. - - - - - - - - - Do also provide a written description - of your interacive elements. - - diff --git a/docs/images/border-radius-90deg.svg b/docs/images/border-radius-90deg.svg new file mode 100644 index 000000000..9369df245 --- /dev/null +++ b/docs/images/border-radius-90deg.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/border-radius-arbitrary.svg b/docs/images/border-radius-arbitrary.svg new file mode 100644 index 000000000..f42485646 --- /dev/null +++ b/docs/images/border-radius-arbitrary.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/border-radius-correct.svg b/docs/images/border-radius-correct.svg new file mode 100644 index 000000000..069a31a3a --- /dev/null +++ b/docs/images/border-radius-correct.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/border-radius-line-join-violation.svg b/docs/images/border-radius-line-join-violation.svg new file mode 100644 index 000000000..7c1a6dc3d --- /dev/null +++ b/docs/images/border-radius-line-join-violation.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/border-radius-line-join.svg b/docs/images/border-radius-line-join.svg new file mode 100644 index 000000000..86a960ebe --- /dev/null +++ b/docs/images/border-radius-line-join.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/border-radius-violation.svg b/docs/images/border-radius-violation.svg new file mode 100644 index 000000000..71a8b70e8 --- /dev/null +++ b/docs/images/border-radius-violation.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/centered-strokes-violation.svg b/docs/images/centered-strokes-violation.svg new file mode 100644 index 000000000..8975f9f98 --- /dev/null +++ b/docs/images/centered-strokes-violation.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/centered-strokes.svg b/docs/images/centered-strokes.svg index 452262261..10737a906 100644 --- a/docs/images/centered-strokes.svg +++ b/docs/images/centered-strokes.svg @@ -1,78 +1,69 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/curvature-smooth.svg b/docs/images/curvature-smooth.svg index b4d977e49..ee39cb389 100644 --- a/docs/images/curvature-smooth.svg +++ b/docs/images/curvature-smooth.svg @@ -1,71 +1,61 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + diff --git a/docs/images/curvature-uneven.svg b/docs/images/curvature-uneven.svg index 0a1e0866b..d54e74dd9 100644 --- a/docs/images/curvature-uneven.svg +++ b/docs/images/curvature-uneven.svg @@ -1,68 +1,90 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + diff --git a/docs/images/density-optimal.svg b/docs/images/density-optimal.svg new file mode 100644 index 000000000..0dfe69e14 --- /dev/null +++ b/docs/images/density-optimal.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/density-too-dense.svg b/docs/images/density-too-dense.svg new file mode 100644 index 000000000..5e98a70e0 --- /dev/null +++ b/docs/images/density-too-dense.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/established-shapes-base-do.svg b/docs/images/established-shapes-base-do.svg new file mode 100644 index 000000000..acdf7036c --- /dev/null +++ b/docs/images/established-shapes-base-do.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/docs/images/established-shapes-base-dont.svg b/docs/images/established-shapes-base-dont.svg new file mode 100644 index 000000000..1aca9b55e --- /dev/null +++ b/docs/images/established-shapes-base-dont.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/docs/images/established-shapes-consistency-do.svg b/docs/images/established-shapes-consistency-do.svg new file mode 100644 index 000000000..20d3d2521 --- /dev/null +++ b/docs/images/established-shapes-consistency-do.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/docs/images/established-shapes-consistency-dont.svg b/docs/images/established-shapes-consistency-dont.svg new file mode 100644 index 000000000..7082dc666 --- /dev/null +++ b/docs/images/established-shapes-consistency-dont.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/docs/images/established-shapes-sub-do.svg b/docs/images/established-shapes-sub-do.svg new file mode 100644 index 000000000..a934d3295 --- /dev/null +++ b/docs/images/established-shapes-sub-do.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docs/images/established-shapes-sub-dont.svg b/docs/images/established-shapes-sub-dont.svg new file mode 100644 index 000000000..e3234bf42 --- /dev/null +++ b/docs/images/established-shapes-sub-dont.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/docs/images/how-to/appropriate_context_do.svg b/docs/images/how-to/appropriate_context_do.svg new file mode 100644 index 000000000..68b5b16f8 --- /dev/null +++ b/docs/images/how-to/appropriate_context_do.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/docs/images/how-to/appropriate_context_dont.svg b/docs/images/how-to/appropriate_context_dont.svg new file mode 100644 index 000000000..84c6267c3 --- /dev/null +++ b/docs/images/how-to/appropriate_context_dont.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/docs/images/how-to/compact_interfaces_caution_1.svg b/docs/images/how-to/compact_interfaces_caution_1.svg new file mode 100644 index 000000000..a93edbe3c --- /dev/null +++ b/docs/images/how-to/compact_interfaces_caution_1.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/how-to/compact_interfaces_caution_2.svg b/docs/images/how-to/compact_interfaces_caution_2.svg new file mode 100644 index 000000000..086a3332c --- /dev/null +++ b/docs/images/how-to/compact_interfaces_caution_2.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/how-to/compact_interfaces_do.svg b/docs/images/how-to/compact_interfaces_do.svg new file mode 100644 index 000000000..da01c7c1d --- /dev/null +++ b/docs/images/how-to/compact_interfaces_do.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/how-to/compact_interfaces_dont.svg b/docs/images/how-to/compact_interfaces_dont.svg new file mode 100644 index 000000000..7559fd14e --- /dev/null +++ b/docs/images/how-to/compact_interfaces_dont.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/how-to/compact_interfaces_labels_do.svg b/docs/images/how-to/compact_interfaces_labels_do.svg new file mode 100644 index 000000000..53641c505 --- /dev/null +++ b/docs/images/how-to/compact_interfaces_labels_do.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/how-to/compact_interfaces_labels_dont.svg b/docs/images/how-to/compact_interfaces_labels_dont.svg new file mode 100644 index 000000000..6b2e2c7b5 --- /dev/null +++ b/docs/images/how-to/compact_interfaces_labels_dont.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/how-to/consistent_icon_function_do.svg b/docs/images/how-to/consistent_icon_function_do.svg new file mode 100644 index 000000000..2a5bb7009 --- /dev/null +++ b/docs/images/how-to/consistent_icon_function_do.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/how-to/consistent_icon_function_dont.svg b/docs/images/how-to/consistent_icon_function_dont.svg new file mode 100644 index 000000000..a72d9e961 --- /dev/null +++ b/docs/images/how-to/consistent_icon_function_dont.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/how-to/consistent_icon_meaning_do.svg b/docs/images/how-to/consistent_icon_meaning_do.svg new file mode 100644 index 000000000..70cbfa1cc --- /dev/null +++ b/docs/images/how-to/consistent_icon_meaning_do.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/how-to/consistent_icon_meaning_dont.svg b/docs/images/how-to/consistent_icon_meaning_dont.svg new file mode 100644 index 000000000..80a25908d --- /dev/null +++ b/docs/images/how-to/consistent_icon_meaning_dont.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/how-to/native_ui_elements_do.svg b/docs/images/how-to/native_ui_elements_do.svg new file mode 100644 index 000000000..f2e5ab9d4 --- /dev/null +++ b/docs/images/how-to/native_ui_elements_do.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/how-to/native_ui_elements_dont.svg b/docs/images/how-to/native_ui_elements_dont.svg new file mode 100644 index 000000000..4e01e9dda --- /dev/null +++ b/docs/images/how-to/native_ui_elements_dont.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/docs/images/how-to/repeated_status_do.svg b/docs/images/how-to/repeated_status_do.svg new file mode 100644 index 000000000..cddd1e7e2 --- /dev/null +++ b/docs/images/how-to/repeated_status_do.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/docs/images/how-to/repeated_status_dont.svg b/docs/images/how-to/repeated_status_dont.svg new file mode 100644 index 000000000..4d1402dc4 --- /dev/null +++ b/docs/images/how-to/repeated_status_dont.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/docs/images/how-to/state_do.svg b/docs/images/how-to/state_do.svg new file mode 100644 index 000000000..5d605ff96 --- /dev/null +++ b/docs/images/how-to/state_do.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/how-to/visual_anchor_do.svg b/docs/images/how-to/visual_anchor_do.svg new file mode 100644 index 000000000..9253d0f63 --- /dev/null +++ b/docs/images/how-to/visual_anchor_do.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/docs/images/how-to/visual_anchor_dont.svg b/docs/images/how-to/visual_anchor_dont.svg new file mode 100644 index 000000000..f0cc50c9e --- /dev/null +++ b/docs/images/how-to/visual_anchor_dont.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/non-scaling-stroke-compare.svg b/docs/images/non-scaling-stroke-compare.svg new file mode 100644 index 000000000..56b3a5ad8 --- /dev/null +++ b/docs/images/non-scaling-stroke-compare.svg @@ -0,0 +1,59 @@ + diff --git a/docs/images/pixel-perfection-bad.svg b/docs/images/pixel-perfection-bad.svg index efb35aece..bdd6d68a6 100644 --- a/docs/images/pixel-perfection-bad.svg +++ b/docs/images/pixel-perfection-bad.svg @@ -1,67 +1,72 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + - - - + + + + - - + + + + + + + diff --git a/docs/images/pixel-perfection-diagonals-do.svg b/docs/images/pixel-perfection-diagonals-do.svg new file mode 100644 index 000000000..538436ea1 --- /dev/null +++ b/docs/images/pixel-perfection-diagonals-do.svg @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/pixel-perfection-diagonals-dont.svg b/docs/images/pixel-perfection-diagonals-dont.svg new file mode 100644 index 000000000..230b44575 --- /dev/null +++ b/docs/images/pixel-perfection-diagonals-dont.svg @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/pixel-perfection-ideal.svg b/docs/images/pixel-perfection-ideal.svg index 7a1271955..40c26c3f4 100644 --- a/docs/images/pixel-perfection-ideal.svg +++ b/docs/images/pixel-perfection-ideal.svg @@ -1,62 +1,67 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + diff --git a/docs/images/pixel-perfection-subelement-do.svg b/docs/images/pixel-perfection-subelement-do.svg new file mode 100644 index 000000000..62335edf7 --- /dev/null +++ b/docs/images/pixel-perfection-subelement-do.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/pixel-perfection-subelement-dont.svg b/docs/images/pixel-perfection-subelement-dont.svg new file mode 100644 index 000000000..9aebb8c21 --- /dev/null +++ b/docs/images/pixel-perfection-subelement-dont.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/round-caps-violation.svg b/docs/images/round-caps-violation.svg new file mode 100644 index 000000000..03a4fe65e --- /dev/null +++ b/docs/images/round-caps-violation.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/round-caps.svg b/docs/images/round-caps.svg index d4affc793..c41269b2e 100644 --- a/docs/images/round-caps.svg +++ b/docs/images/round-caps.svg @@ -1,74 +1,69 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/round-joints-violation.svg b/docs/images/round-joints-violation.svg new file mode 100644 index 000000000..7964757da --- /dev/null +++ b/docs/images/round-joints-violation.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/round-joints.svg b/docs/images/round-joints.svg index 72b7174a1..cbac76bce 100644 --- a/docs/images/round-joints.svg +++ b/docs/images/round-joints.svg @@ -1,72 +1,69 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/visual-weight-consistent.svg b/docs/images/visual-weight-consistent.svg new file mode 100644 index 000000000..d0a32544d --- /dev/null +++ b/docs/images/visual-weight-consistent.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/docs/images/visual-weight-uneven.svg b/docs/images/visual-weight-uneven.svg new file mode 100644 index 000000000..668bcba7a --- /dev/null +++ b/docs/images/visual-weight-uneven.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/visually-centered-bad.svg b/docs/images/visually-centered-bad.svg deleted file mode 100644 index 6ed8757ba..000000000 --- a/docs/images/visually-centered-bad.svg +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/images/visually-centered-do.svg b/docs/images/visually-centered-do.svg new file mode 100644 index 000000000..e36a51638 --- /dev/null +++ b/docs/images/visually-centered-do.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/visually-centered-dont.svg b/docs/images/visually-centered-dont.svg new file mode 100644 index 000000000..fcd350033 --- /dev/null +++ b/docs/images/visually-centered-dont.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/visually-centered-symmetrical-do.svg b/docs/images/visually-centered-symmetrical-do.svg new file mode 100644 index 000000000..71e77d3fd --- /dev/null +++ b/docs/images/visually-centered-symmetrical-do.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/visually-centered-symmetrical-dont.svg b/docs/images/visually-centered-symmetrical-dont.svg new file mode 100644 index 000000000..882fbdd63 --- /dev/null +++ b/docs/images/visually-centered-symmetrical-dont.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/visually-centered.svg b/docs/images/visually-centered.svg deleted file mode 100644 index c0e235307..000000000 --- a/docs/images/visually-centered.svg +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/package.json b/docs/package.json index ef283ebfa..9e43447ba 100644 --- a/docs/package.json +++ b/docs/package.json @@ -15,7 +15,7 @@ "prebuild:categoriesJson": "node ./scripts/writeCategoriesMetadata.mts", "prebuild:relatedIcons": "node ./scripts/writeIconRelatedIcons.mts", "prebuild:iconDetails": "node ./scripts/writeIconDetails.mts", - "prebuild:iconPopularity": "node ./scripts/writeiconPopularity.mts && node ./scripts/writeiconPopularityIndex.mts", + "prebuild:iconPopularity": "node --env-file-if-exists=.env ./scripts/writeiconPopularity.mts && node ./scripts/writeiconPopularityIndex.mts", "prebuild:brandStopwords": "node ./scripts/writeBrandStopwords.mts", "prebuild:writeLabIconNodes": "node ./scripts/writeLabIconNodes.mts", "prebuild:writeLabIconMetaIndex": "node ./scripts/writeLabIconMetaIndex.mts", @@ -24,7 +24,8 @@ "prebuild:api": "nitropack prepare", "build:api": "NODE_OPTIONS=--max-old-space-size=8192 nitropack build", "build": "pnpm run /^prebuild:.*/ && pnpm run build:api && pnpm run build:docs && pnpm postbuild:vercelJson", - "preview": "node .output/server/index.mjs" + "preview": "node .output/server/index.mjs", + "typecheck": "vue-tsc --noEmit" }, "author": "Eric Fennis", "license": "ISC", @@ -33,41 +34,45 @@ "@lucide/helpers": "workspace:*", "@lucide/shared": "workspace:*", "@rollup/plugin-replace": "^6.0.3", - "@types/semver": "^7.7.1", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "@types/semver": "^7.8.0", "h3": "^1.15.11", - "markdown-it": "^14.3.0", + "markdown-it": "^15.0.1", "markdown-it-container": "^4.0.0", "nitropack": "^2.13.4", "rollup-plugin-copy": "^3.5.0", "semver": "^7.8.5", - "sharp": "^0.34.5", - "shiki": "^3.23.0", - "simple-git": "^3.36.0", - "sitemap": "^7.1.3", - "svg-path-commander": "^2.2.1", - "svg-pathdata": "^6.0.3", + "sharp": "^0.35.4", + "shiki": "^4.4.3", + "simple-git": "^4.0.1", + "sitemap": "^9.0.1", + "svg-path-commander": "^2.3.3", + "svg-pathdata": "^9.0.0", "svgson": "^5.3.1", + "typescript": "^6.0.3", "vitepress": "^1.6.4", - "vitepress-plugin-group-icons": "^1.7.5", - "vitepress-plugin-llms": "^1.13.3" + "vitepress-plugin-group-icons": "^1.7.6", + "vitepress-plugin-llms": "^1.13.5", + "vue-tsc": "^3.3.11" }, "dependencies": { - "@floating-ui/vue": "^1.1.11", + "@floating-ui/vue": "^2.0.1", "@headlessui/vue": "^1.7.23", "@lucide/vue": "workspace:*", - "@vueuse/components": "^14.3.0", - "@vueuse/core": "^14.3.0", - "@vueuse/integrations": "^14.3.0", + "@vueuse/components": "^14.4.0", + "@vueuse/core": "^14.4.0", + "@vueuse/integrations": "^14.4.0", "element-to-path": "^1.2.1", "fuse.js": "^7.5.0", "jszip": "^3.10.1", "lodash-es": "^4.18.1", "lucide": "workspace:*", "lucide-react": "workspace:*", - "motion-v": "1.10.3", - "react": "18.2.0", - "react-dom": "18.2.0", + "motion-v": "2.4.0", + "react": "19.3.0", + "react-dom": "19.3.0", "sandpack-vue3": "3.1.12", - "vue": "^3.5.41" + "vue": "^3.5.43" } } diff --git a/docs/packages.md b/docs/packages.md index 6dd9e22ed..113573a26 100644 --- a/docs/packages.md +++ b/docs/packages.md @@ -6,8 +6,8 @@ outline: 2 outlineTitle: Packages ---
diff --git a/docs/public/templates/illustrator_template.ai b/docs/public/templates/illustrator_template.ai index fc7472e63..28ab9dea7 100644 --- a/docs/public/templates/illustrator_template.ai +++ b/docs/public/templates/illustrator_template.ai @@ -1,111 +1,113 @@ %PDF-1.6 %âãÏÓ -1 0 obj <>/OCGs[18 0 R 19 0 R 20 0 R 21 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream +1 0 obj <>/OCGs[6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream - - - - application/pdf - - - illustrator_template - - - 2023-11-18T19:30:35Z - 2023-11-18T19:30:35Z - 2023-11-18T19:30:35Z - Adobe Illustrator 25.2 (Macintosh) - - - - 256 - 256 - JPEG - /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYqlvmPzHovlvRbrWtauks9Ns05zzv9wVQN2ZjsqjcnYYq+P/AMzv+cu/OGt3Etl5 NB0HSASq3ZCvfTL4ljyWEeyfEP5sVeG6v5h1/WZjPrGpXWozE1Ml3NJO1fnIzeOKu0jzDr+jTCfR 9SutOmBqJLSaSBq/ONl8MVe5flj/AM5d+cNEuIrLzkDr2kEhWuwFS+hXxDDisw9n+I/zYq+wPLnm PRfMmi2utaLdJeabeJzgnT7irA7qynZlO4OxxVMsVdirsVdirsVdirsVdirsVdirsVdirsVdirsV dirsVdirsVdirsVdirsVdirsVdir4R/5ya/Ny586ecptGsJz/hrQpXgtkQ/BPcIeMtw1Nm3qsf8A k7j7RxV4zirsVdirsVezf84y/m5deS/OcOjX05/w1rsqQXUbn4ILh/giuFrsu9FkP8u5+yMVfd2K uxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KsY/M/X5fL/5deZNZ gbhc2en3D2ziu0xjKxHb/LIxV+aZJJqdycVaxV2KuxV2KtgkGo2IxV+ln5Ya/L5g/Lry3rM7c7m8 0+3e5c13mEYWU7/5YOKsnxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Ku xV2KsA/P7/yTXmz/AJgW/wCJLir86sVdirsVdirsVdir9DP+ccXd/wAk/KpcliLeVQT4LcSgD6AM VekYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FWAfn9/5JrzZ/zA t/xJcVfnVirsVdirsVdirsVfoV/zjd/5JLyt/wAYJv8AqJlxV6VirsVdirsVdirsVdirsVdirsVd irsVdirsVdirsVdirsVdirsVdirsVdirsVYB+f3/AJJrzZ/zAt/xJcVfnVirsVdirsVdirsVfoV/ zjd/5JLyt/xgm/6iZcVelYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FWAfn9/5JrzZ/zAt/xJcVfnVirsVdirsVdirsVfoV/wA43f8AkkvK3/GCb/qJlxV6VirsVdir sVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVYB+f3/kmvNn/ADAt/wASXFX5 1Yq7FXYq7FXYq7FX6Ff843f+SS8rf8YJv+omXFXpWKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2 KuxV2KuxV2KuxV2KuxV2KuxVgH5/f+Sa82f8wLf8SXFX51Yq7FXYq7FXYq7FX6Ff843f+SS8rf8A GCb/AKiZcVelYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FWAfn9 /wCSa82f8wLf8SXFX51Yq7FXYq7FXYq7FX6Ff843f+SS8rf8YJv+omXFXpWKuxV2KuxV2KuxV2Ku xV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVgH5/f+Sa82f8wLf8SXFX51Yq7FXYq7FXYq 7FX6Ff8AON3/AJJLyt/xgm/6iZcVelYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FWAfn9/5JrzZ/wAwLf8AElxV+dWKuxV2KuxV2KuxV+hX/ON3/kkvK3/GCb/qJlxV 6VirsVdirsVdirsVdirsVdirsVdirsVdirsVYxr/AOZ/5deX5Wg1nzJp9ncps9s9xGZhvTeJSX/D FUstfz1/J+5mEUfm7TVZuhlmES/S8nBR9+Ks0s72zvbZLqynjuraQVjnhdZI2HirKSDiqtirsVdi rsVdirsVdirAPz+/8k15s/5gW/4kuKvzqxV2KuxV2KuxV2Kv0K/5xu/8kl5W/wCME3/UTLir0rFX Yq7FXYq7FXYq7FVG8vLOytnubyeO2tohWSeZ1jRR4szEAYqwu6/PT8n7WZoZfNunF16mKYSr/wAH HyX8cVTPQPzP/LrzBKsGjeZNPvLl9ktkuIxMd6bRMQ/4YqyfFXYqlvmPzHovlvRbrWtauks9Ns05 zzv9wVQN2ZjsqjcnYYq+I/zc/wCcmvOfnS6nsdGml0Ly1UpHawPwuJ06criVN/iHWNTx7Hl1xV40 SSancnFWsVZH5L/MLzj5L1Bb7y5qc1i/INLArcoJadpYTVHHzFfDFX2z+Rv5+aN+ZNibK6VNO802 qcrrTw3wTIKVmt+W5Wv2l3K+43xV6xirsVdirsVdirsVYB+f3/kmvNn/ADAt/wASXFX51Yq7FXYq 7FXYq7FX6Ff843f+SS8rf8YJv+omXFXpWKuxV2KuxV2KuxV5P+eX5+aN+W1iLK1VNR803ScrXTy3 wQoa0muOO4Wv2V2Lew3xV8TedPzC84+dNQa+8x6nNfPyLRQM3GCKvaKEURB8hXxxVjmKtgkGo2Ix V7L+Uf8Azk15z8l3UFjrM0uu+WqhJLWd+dxAnTlbyvv8I6RsePYceuKvtzy55j0XzJotrrWi3SXm m3ic4J0+4qwO6sp2ZTuDscVfH/8Azl3+Z1xrfnAeTbKUjSNBIN2qn4Zr51qxPtCrcB4NyxV8+4q7 FXYq7FUZo+sapo2qW2q6VcyWeo2cgltrmI0dHHcfqIOxGxxV92/kN+fOl/mNpYsL8x2fm2zjreWY 2SdBsZ4Ae38y9VPtQ4q9bxV2KuxV2KuxVgH5/f8AkmvNn/MC3/ElxV+dWKuxV2KuxV2KuxV+hX/O N3/kkvK3/GCb/qJlxV6VirsVdirsVdiryT8+fz50v8udLNhYGO8823kdbOzO6QIdhPOB2/lXqx9q nFXwlrGsaprOqXOq6rcyXmo3khlubmU1d3Pc/qAGwGwxVB4q7FXYq7FX0F/ziJ+Z1xonnA+Tb2Un SNeJNorH4Yb5FqpHtMq8D4txxV4b5h1ebWdf1LWJyTNqN1NdyE9eU8jSHx/mxVL8VdirsVdirsVR mj6xqmjapbarpVzJZ6jZyCW2uYjR0cdx+og7EbHFX3b+Q3586X+Y2liwvzHZ+bbOOt5ZjZJ0Gxng B7fzL1U+1Dir1vFXYq7FXYqwD8/v/JNebP8AmBb/AIkuKvzqxV2KuxV2KuxV2Kv0K/5xu/8AJJeV v+ME3/UTLir0rFXYq7FXYq8k/Pn8+dL/AC50s2FgY7zzbeR1s7M7pAh2E84Hb+VerH2qcVfCWsax qms6pc6rqtzJeajeSGW5uZTV3c9z+oAbAbDFUHirsVdirsVdiqYeXtXm0bX9N1iAkTaddQ3cZHXl BIsg8P5cVd5h0ibRtf1LR5wRNp11NaSA9eUEjRnw/lxVL8VdirsVdirsVdiqM0fWNU0bVLbVdKuZ LPUbOQS21zEaOjjuP1EHYjY4q+7fyG/PnS/zG0sWF+Y7PzbZx1vLMbJOg2M8APb+Zeqn2ocVet4q 7FXYqwD8/v8AyTXmz/mBb/iS4q/OrFXYq7FXYq7FXYq/Qr/nG7/ySXlb/jBN/wBRMuKvSsVdirsV eSfnz+fOl/lzpZsLAx3nm28jrZ2Z3SBDsJ5wO38q9WPtU4q+EtY1jVNZ1S51XVbmS81G8kMtzcym ru57n9QA2A2GKoPFXYq7FXYq7FXYqmHl7SJtZ1/TdHgBM2o3UNpGB15TyLGPH+bFXuX/ADl3+WNx onnAecrKInSNeIF2yj4Yb5FowPtMq8x4tyxV8+4q7FXYq7FXYq7FXYqjNH1jVNG1S21XSrmSz1Gz kEttcxGjo47j9RB2I2OKvu38hvz50v8AMbSxYX5js/NtnHW8sxsk6DYzwA9v5l6qfahxV63irsVY B+f3/kmvNn/MC3/ElxV+dWKuxV2KuxV2KuxV+hX/ADjd/wCSS8rf8YJv+omXFXpWKuxV5J+fP586 X+XOlmwsDHeebbyOtnZndIEOwnnA7fyr1Y+1Tir4S1jWNU1nVLnVdVuZLzUbyQy3NzKau7nuf1AD YDYYqg8VdirsVdirsVdirsVfQX/OIn5Y3Gt+cD5yvYiNI0EkWjMPhmvnWige0KtzPg3HFX2B5j8u aL5k0W60XWrVLzTbxOE8D/eGUjdWU7qw3B3GKvgr87vyR1r8tdaqOd55avHP6M1OnTv6E9NllUfQ w3HcBV5lirsVdirsVdirsVdiqM0fWNU0bVLbVdKuZLPUbOQS21zEaOjjuP1EHYjY4q+7fyG/PnS/ zG0sWF+Y7PzbZx1vLMbJOg2M8APb+Zeqn2ocVet4qwD8/v8AyTXmz/mBb/iS4q/OrFXYq7FXYq7F XYq/Qr/nG7/ySXlb/jBN/wBRMuKvSsVeSfnz+fOl/lzpZsLAx3nm28jrZ2Z3SBDsJ5wO38q9WPtU 4q+EtY1jVNZ1S51XVbmS81G8kMtzcymru57n9QA2A2GKoPFXYq7FXYq7FXYq7FXpv5I/kjrX5la1 U87Py1ZuP0nqdOvf0IK7NKw+hRuewKr718ueXNF8t6La6LotqlnptmnCCBPvLMTuzMd2Y7k7nFUy xVLfMflzRfMmi3Wi61apeabeJwngf7wykbqyndWG4O4xV8Ffnd+SOtflrrVRzvPLV45/Rmp06d/Q npssqj6GG47gKvMsVdirsVdirsVdirsVRmj6xqmjapbarpVzJZ6jZyCW2uYjR0cdx+og7EbHFX3b +Q3586X+Y2liwvzHZ+bbOOt5ZjZJ0GxngB7fzL1U+1Diqefn9/5JrzZ/zAt/xJcVfnVirsVdirsV dirsVfoV/wA43f8AkkvK3/GCb/qJlxVLvz5/PnS/y50s2FgY7zzbeR1s7M7pAh2E84Hb+VerH2qc VfCWsaxqms6pc6rqtzJeajeSGW5uZTV3c9z+oAbAbDFUHirsVdirsVdirsVdir038kfyR1r8ytaq edn5as3H6T1OnXv6EFdmlYfQo3PYFV96+XPLmi+W9FtdF0W1Sz02zThBAn3lmJ3ZmO7MdydziqZY q7FXYqlvmPy5ovmTRbrRdatUvNNvE4TwP94ZSN1ZTurDcHcYq+Cvzu/JHWvy11qo53nlq8c/ozU6 dO/oT02WVR9DDcdwFXmWKuxV2KuxV2KuxV2KozR9Y1TRtUttV0q5ks9Rs5BLbXMRo6OO4/UQdiNj ir6tl/PnS/zG/IfzXYX5js/NtnprG8sxsk6BlBngB7fzL1U+1Dir5GxV2KuxV2KuxV2Kvq7SPz50 v8uf+cfvK9hYGO8823lpMbOzO6QIbmUCecDt/KvVj7VOKvl3WNY1TWdUudV1W5kvNRvJDLc3Mpq7 ue5/UANgNhiqDxV2KuxV2KuxV2KuxV6b+SP5I61+ZWtVPOz8tWbj9J6nTr39CCuzSsPoUbnsCq+9 fLnlzRfLei2ui6LapZ6bZpwggT7yzE7szHdmO5O5xVMsVdirsVdirsVS3zH5c0XzJot1outWqXmm 3icJ4H+8MpG6sp3VhuDuMVfBX53fkjrX5a61Uc7zy1eOf0ZqdOnf0J6bLKo+hhuO4CrzLFXYq7FX Yq7FXYq7FVyO6GqMVJBUkGhowoRt4g0OKrcVdirsVdirsVdirZZmNSSTQCp8AKDFWsVdirsVdirs VdirsVem/kj+SOtfmVrVTzs/LVm4/Sep069/Qgrs0rD6FG57AqvvXy55c0Xy3otroui2qWem2acI IE+8sxO7Mx3ZjuTucVTLFXYq7FXYq7FXYq7FUt8x+XNF8yaLdaLrVql5pt4nCeB/vDKRurKd1Ybg 7jFXwV+d35I61+WutVHO88tXjn9GanTp39CemyyqPoYbjuAq8yxV2KuxV2KuxV2KuxV2KuxV2Kux V2KuxV2KuxV2KuxV2KuxV2KvTfyR/JHWvzK1qp52flqzcfpPU6de/oQV2aVh9Cjc9gVX3r5c8uaL 5b0W10XRbVLPTbNOEECfeWYndmY7sx3J3OKplirsVdirsVdirsVdirsVdiqW+Y/Lmi+ZNFutF1q1 S8028ThPA/3hlI3VlO6sNwdxir4K/O78kda/LXWqjneeWrxz+jNTp07+hPTZZVH0MNx3AVeZYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXpv5I/kjrX5la1U87Py1ZuP0nqdOvf0IK7NK w+hRuewKr718ueXNF8t6La6LotqlnptmnCCBPvLMTuzMd2Y7k7nFUyxV2KuxV2KuxV2KuxV2KuxV 2KuxVLfMflzRfMmi3Wi61apeabeJwngf7wykbqyndWG4O4xV8Ffnd+SOtflrrVRzvPLV45/Rmp06 d/Qnpssqj6GG47gKvMsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir038kfyR1r8ytaqedn5 as3H6T1OnXv6EFdmlYfQo3PYFV96+XPLmi+W9FtdF0W1Sz02zThBAn3lmJ3ZmO7MdydziqZYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYqlvmPy5ovmTRbrRdatUvNNvE4TwP8AeGUjdWU7qw3B3GKvgr87 vyR1r8tdaqOd55avHP6M1OnTv6E9NllUfQw3HcBV5lirsVdirsVdirsVdirsVdirsVdirsVdirsV em/kj+SOtfmVrVTzs/LVm4/Sep069/Qgrs0rD6FG57AqvvXy55c0Xy3otroui2qWem2acIIE+8sx O7Mx3ZjuTucVTLFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FUt8x+XNF8yaLdaLrVql5pt4nCe B/vDKRurKd1Ybg7jFXwV+d35I61+WutVHO88tXjn9GanTp39CemyyqPoYbjuAq8yxV2KuxV2KuxV 2KuxV2KuxV2KuxV2KvTfyR/JHWvzK1qp52flqzcfpPU6de/oQV2aVh9Cjc9gVX3r5c8uaL5b0W10 XRbVLPTbNOEECfeWYndmY7sx3J3OKplirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdiqW+Y /Lmi+ZNFutF1q1S8028ThPA/3hlI3VlO6sNwdxir4K/O78kda/LXWqjneeWrxz+jNTp07+hPTZZV H0MNx3AVeZYq7FXYq7FXYq7FXYq7FXYq7FXpv5I/kjrX5la1U87Py1ZuP0nqdOvf0IK7NKw+hRue wKr718ueXNF8t6La6LotqlnptmnCCBPvLMTuzMd2Y7k7nFUyxV2KuxV2KuxV2KuxV2KuxV2KuxV2 KuxV2KuxV2KuxV2KuxVLfMflzRfMmi3Wi61apeabeJwngf7wykbqyndWG4O4xV8Ffnd+SOtflrrV RzvPLV45/Rmp06d/Qnpssqj6GG47gKvMsVdirsVdirsVdirsVdir038kfyR1r8ytaqedn5as3H6T 1OnXv6EFdmlYfQo3PYFV96+XPLmi+W9FtdF0W1Sz02zThBAn3lmJ3ZmO7MdydziqZYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqlvmPy5ovmTRbrRdatUvNNvE4TwP94ZSN1ZT urDcHcYq+Cvzu/JHWvy11qo53nlq8c/ozU6dO/oT02WVR9DDcdwFXmWKuxV2KuxV2KuxV6b+SP5I 61+ZWtVPOz8tWbj9J6nTr39CCuzSsPoUbnsCq+9fLnlzRfLei2ui6LapZ6bZpwggT7yzE7szHdmO 5O5xVMsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVS3zH5c0XzJot1 outWqXmm3icJ4H+8MpG6sp3VhuDuMVfBH53fkrrH5aa8Fq955cvmY6VqRG+25gmpssqD6GG47hVX mmKuxV2KuxV6X+SP5K6x+ZevFavZ+XLFlOq6kBvvuIIa7NK4+hRuewZV97+XPLmi+W9FtdF0W1Sz 02zThBAn3lmJ3ZmO7MdydziqZYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYqlXmnytoXmnQrrQ9ctVvNOvF4yxN1B/ZdG6q6ndWG4OKvjr8zf+cSfO/l+ea88phvM Wi/E6xJxW/iUfsvF8Il60BiqT/IuKvEdT0fVtKuntNUsriwu4zSS3uonhkU+BRwrDFXaZpGrardJ aaXZT393IaR29rE80jHwCIGY4q9u/LL/AJxJ87+YJ4bzzYG8u6L8LtE/Fr+VT+ykXxCLpQmWhH8j Yq+xfK3lbQvK2hWuh6HarZ6dZrxiiXqT+07t1Z2O7MdycVTXFXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FX/9k= - - - - uuid:8821779b-762c-6743-9c68-7f3a5e3bebda - xmp.did:779ae009-2b38-eb48-94c6-923993797922 - uuid:5D20892493BFDB11914A8590D31508C8 - proof:pdf - - xmp.iid:a8cac32b-00c6-cf4b-a912-99935168cf83 - xmp.did:a8cac32b-00c6-cf4b-a912-99935168cf83 - uuid:5D20892493BFDB11914A8590D31508C8 - default - - - - - saved - xmp.iid:e743d930-1240-8f4e-9990-55c7f8311d80 - 2021-06-19T17:54:42+02:00 - Adobe Illustrator 25.3 (Windows) - / - - - saved - xmp.iid:779ae009-2b38-eb48-94c6-923993797922 - 2021-06-19T18:02:38+02:00 - Adobe Illustrator 25.3 (Windows) - / - - - - Document - Print - AIRobin - False - True - 1 - - 24.000000 - 24.000000 - Pixels - - - - Cyan - Magenta - Yellow - Black - - - - - - Default Swatch Group - 0 - - - - #F56565 - SPOT - 100.000000 - RGB - 245 - 101 - 101 - - - - - - - Adobe PDF library 15.00 + + + + + + illustrator_template + + + + + + + 256 + 256 + JPEG + /9j/4AAQSkZJRgABAgEAAAAAAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAAAAAAAAEA AQAAAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYqlvmPzHovlvRbrWtauks9Ns05zzv9wVQN2ZjsqjcnYYq+P/AMzv+cu/OGt3Etl5 NB0HSASq3ZCvfTL4ljyWEeyfEP5sVeG6v5h1/WZjPrGpXWozE1Ml3NJO1fnIzeOKu0jzDr+jTCfR 9SutOmBqJLSaSBq/ONl8MVe5flj/AM5d+cNEuIrLzkDr2kEhWuwFS+hXxDDisw9n+I/zYq+wPLnm PRfMmi2utaLdJeabeJzgnT7irA7qynZlO4OxxVMsVdirsVdirsVdirsVdirsVdirsVdirsVdirsV dirsVdirsVdirsVdirsVdirsVdir4R/5ya/Ny586ecptGsJz/hrQpXgtkQ/BPcIeMtw1Nm3qsf8A k7j7RxV4zirsVdirsVezf84y/m5deS/OcOjX05/w1rsqQXUbn4ILh/giuFrsu9FkP8u5+yMVfd2K uxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KsY/M/X5fL/5deZNZ gbhc2en3D2ziu0xjKxHb/LIxV+aZJJqdycVaxV2KuxV2KtgkGo2IxV+ln5Ya/L5g/Lry3rM7c7m8 0+3e5c13mEYWU7/5YOKsnxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Ku xV2KsA/P7/yTXmz/AJgW/wCJLir86sVdirsVdirsVdir9DP+ccXd/wAk/KpcliLeVQT4LcSgD6AM VekYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FWAfn9/5JrzZ/zA t/xJcVfnVirsVdirsVdirsVfoV/zjd/5JLyt/wAYJv8AqJlxV6VirsVdirsVdirsVdirsVdirsVd irsVdirsVdirsVdirsVdirsVdirsVdirsVYB+f3/AJJrzZ/zAt/xJcVfnVirsVdirsVdirsVfoV/ zjd/5JLyt/xgm/6iZcVelYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FWAfn9/5JrzZ/zAt/xJcVfnVirsVdirsVdirsVfoV/wA43f8AkkvK3/GCb/qJlxV6VirsVdir sVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVYB+f3/kmvNn/ADAt/wASXFX5 1Yq7FXYq7FXYq7FX6Ff843f+SS8rf8YJv+omXFXpWKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2 KuxV2KuxV2KuxV2KuxV2KuxVgH5/f+Sa82f8wLf8SXFX51Yq7FXYq7FXYq7FX6Ff843f+SS8rf8A GCb/AKiZcVelYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FWAfn9 /wCSa82f8wLf8SXFX51Yq7FXYq7FXYq7FX6Ff843f+SS8rf8YJv+omXFXpWKuxV2KuxV2KuxV2Ku xV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVgH5/f+Sa82f8wLf8SXFX51Yq7FXYq7FXYq 7FX6Ff8AON3/AJJLyt/xgm/6iZcVelYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FWAfn9/5JrzZ/wAwLf8AElxV+dWKuxV2KuxV2KuxV+hX/ON3/kkvK3/GCb/qJlxV 6VirsVdirsVdirsVdirsVdirsVdirsVdirsVYxr/AOZ/5deX5Wg1nzJp9ncps9s9xGZhvTeJSX/D FUstfz1/J+5mEUfm7TVZuhlmES/S8nBR9+Ks0s72zvbZLqynjuraQVjnhdZI2HirKSDiqtirsVdi rsVdirsVdirAPz+/8k15s/5gW/4kuKvzqxV2KuxV2KuxV2Kv0K/5xu/8kl5W/wCME3/UTLir0rFX Yq7FXYq7FXYq7FVG8vbOytnur2eO1toxWSeZ1jjUeLMxAGKsLuvz0/J+1maGXzbpxdepimEq/wDB x8l/HFUz0D8z/wAuvMEqwaN5k0+8uX2S2S4jEx3ptExD/hirJ8VdiqW+Y/Mei+W9Futa1q6Sz02z TnPO/wBwVQN2ZjsqjcnYYq+I/wA3P+cmvOfnS6nsdGml0Ly1UpHawPwuJ06criVN/iHWNTx7Hl1x V40SSancnFWsVZH5L/MLzj5L1Bb7y5qc1i/INLArcoJadpYTVHHzFfDFX2z+Rv5+aN+ZNibK6VNO 802qcrrTw3wTIKVmt+W5Wv2l3K+43xV6xirsVdirsVdirsVYB+f3/kmvNn/MC3/ElxV+dWKuxV2K uxV2KuxV+hX/ADjd/wCSS8rf8YJv+omXFXpWKuxV2KuxV2KuxV5P+eX5+aN+W1iLK1VNR803ScrX Ty3wQoa0muOO4Wv2V2Lew3xV8TedPzC84+dNQa+8x6nNfPyLRQM3GCKvaKEURB8hXxxVjmKtgkGo 2IxV7L+Uf/OTXnPyXdQWOszS675aqEktZ353ECdOVvK+/wAI6RsePYceuKvtzy55j0XzJotrrWi3 SXmm3ic4J0+4qwO6sp2ZTuDscVfH/wDzl3+Z1xrfnAeTbKUjSNBIN2qn4Zr51qxPtCrcB4NyxV8+ 4q7FXYq7FUZo+sapo2qW2q6VcyWeo2cgltrmI0dHHcfqIOxGxxV92/kN+fOl/mNpYsL8x2fm2zjr eWY2SdBsZ4Ae38y9VPtQ4q9bxV2KuxV2KuxVgH5/f+Sa82f8wLf8SXFX51Yq7FXYq7FXYq7FX6Ff 843f+SS8rf8AGCb/AKiZcVelYq7FXYq7FXYq8k/Pn8+dL/LnSzYWBjvPNt5HWzszukCHYTzgdv5V 6sfapxV8JaxrGqazqlzquq3Ml5qN5IZbm5lNXdz3P6gBsBsMVQeKuxV2KuxV9Bf84ifmdcaJ5wPk 29lJ0jXiTaKx+GG+RaqR7TKvA+LccVeG+YdXm1nX9S1ickzajdTXchPXlPI0h8f5sVS/FXYq7FXY q7FUZo+sapo2qW2q6VcyWeo2cgltrmI0dHHcfqIOxGxxV92/kN+fOl/mNpYsL8x2fm2zjreWY2Sd BsZ4Ae38y9VPtQ4q9bxV2KuxV2KsA/P7/wAk15s/5gW/4kuKvzqxV2KuxV2KuxV2Kv0K/wCcbv8A ySXlb/jBN/1Ey4q9KxV2KuxV2KvJPz5/PnS/y50s2FgY7zzbeR1s7M7pAh2E84Hb+VerH2qcVfCW saxqms6pc6rqtzJeajeSGW5uZTV3c9z+oAbAbDFUHirsVdirsVdiqYeXtXm0bX9N1iAkTaddQ3cZ HXlBIsg8P5cVd5h0ibRtf1LR5wRNp11NaSA9eUEjRnw/lxVL8VdirsVdirsVdiqM0fWNU0bVLbVd KuZLPUbOQS21zEaOjjuP1EHYjY4q+7fyG/PnS/zG0sWF+Y7PzbZx1vLMbJOg2M8APb+Zeqn2ocVe t4q7FXYqwD8/v/JNebP+YFv+JLir86sVdirsVdirsVdir9Cv+cbv/JJeVv8AjBN/1Ey4q9KxV2Ku xV5J+fP586X+XOlmwsDHeebbyOtnZndIEOwnnA7fyr1Y+1Tir4S1jWNU1nVLnVdVuZLzUbyQy3Nz Kau7nuf1ADYDYYqg8VdirsVdirsVdiqYeXtIm1nX9N0eAEzajdQ2kYHXlPIsY8f5sVe5f85d/ljc aJ5wHnKyiJ0jXiBdso+GG+RaMD7TKvMeLcsVfPuKuxV2KuxV2KuxV2KozR9Y1TRtUttV0q5ks9Rs 5BLbXMRo6OO4/UQdiNjir7t/Ib8+dL/MbSxYX5js/NtnHW8sxsk6DYzwA9v5l6qfahxV63irsVYB +f3/AJJrzZ/zAt/xJcVfnVirsVdirsVdirsVfoV/zjd/5JLyt/xgm/6iZcVelYq7FXkn58/nzpf5 c6WbCwMd55tvI62dmd0gQ7CecDt/KvVj7VOKvhLWNY1TWdUudV1W5kvNRvJDLc3Mpq7ue5/UANgN hiqDxV2KuxV2KuxV2KuxV9Bf84ifljca35wPnK9iI0jQSRaMw+Ga+daKB7Qq3M+DccVfYHmPy5ov mTRbrRdatUvNNvE4TwP94ZSN1ZTurDcHcYq+Cvzu/JHWvy11qo53nlq8c/ozU6dO/oT02WVR9DDc dwFXmWKuxV2KuxV2KuxV2KozR9Y1TRtUttV0q5ks9Rs5BLbXMRo6OO4/UQdiNjir7t/Ib8+dL/Mb SxYX5js/NtnHW8sxsk6DYzwA9v5l6qfahxV63irAPz+/8k15s/5gW/4kuKvzqxV2KuxV2KuxV2Kv 0K/5xu/8kl5W/wCME3/UTLir0rFXkn58/nzpf5c6WbCwMd55tvI62dmd0gQ7CecDt/KvVj7VOKvh LWNY1TWdUudV1W5kvNRvJDLc3Mpq7ue5/UANgNhiqDxV2KuxV2KuxV2KuxV6b+SP5I61+ZWtVPOz 8tWbj9J6nTr39CCuzSsPoUbnsCq+9fLnlzRfLei2ui6LapZ6bZpwggT7yzE7szHdmO5O5xVMsVS3 zH5c0XzJot1outWqXmm3icJ4H+8MpG6sp3VhuDuMVfBX53fkjrX5a61Uc7zy1eOf0ZqdOnf0J6bL Ko+hhuO4CrzLFXYq7FXYq7FXYq7FUZo+sapo2qW2q6VcyWeo2cgltrmI0dHHcfqIOxGxxV92/kN+ fOl/mNpYsL8x2fm2zjreWY2SdBsZ4Ae38y9VPtQ4qnn5/f8AkmvNn/MC3/ElxV+dWKuxV2KuxV2K uxV+hX/ON3/kkvK3/GCb/qJlxVLvz5/PnS/y50s2FgY7zzbeR1s7M7pAh2E84Hb+VerH2qcVfCWs axqms6pc6rqtzJeajeSGW5uZTV3c9z+oAbAbDFUHirsVdirsVdirsVdir038kfyR1r8ytaqedn5a s3H6T1OnXv6EFdmlYfQo3PYFV96+XPLmi+W9FtdF0W1Sz02zThBAn3lmJ3ZmO7MdydziqZYq7FXY qlvmPy5ovmTRbrRdatUvNNvE4TwP94ZSN1ZTurDcHcYq+Cvzu/JHWvy11qo53nlq8c/ozU6dO/oT 02WVR9DDcdwFXmWKuxV2KuxV2KuxV2KozR9Y1TRtUttV0q5ks9Rs5BLbXMRo6OO4/UQdiNjir6tl /PnS/wAxvyH812F+Y7PzbZ6axvLMbJOgZQZ4Ae38y9VPtQ4q+RsVdirsVdirsVdir6u0j8+dL/Ln /nH7yvYWBjvPNt5aTGzszukCG5lAnnA7fyr1Y+1Tir5d1jWNU1nVLnVdVuZLzUbyQy3NzKau7nuf 1ADYDYYqg8VdirsVdirsVdirsVem/kj+SOtfmVrVTzs/LVm4/Sep069/Qgrs0rD6FG57AqvvXy55 c0Xy3otroui2qWem2acIIE+8sxO7Mx3ZjuTucVTLFXYq7FXYq7FUt8x+XNF8yaLdaLrVql5pt4nC eB/vDKRurKd1Ybg7jFXwV+d35I61+WutVHO88tXjn9GanTp39CemyyqPoYbjuAq8yxV2KuxV2Kux V2KuxVcjuhqjFSQVJBoaMKEbeINDiq3FXYq7FXYq7FXYq2WZjUkk0AqfACgxVrFXYq7FXYq7FXYq 7FXpv5I/kjrX5la1U87Py1ZuP0nqdOvf0IK7NKw+hRuewKr718ueXNF8t6La6LotqlnptmnCCBPv LMTuzMd2Y7k7nFUyxV2KuxV2KuxV2KuxVLfMflzRfMmi3Wi61apeabeJwngf7wykbqyndWG4O4xV 8Ffnd+SOtflrrVRzvPLV45/Rmp06d/Qnpssqj6GG47gKvMsVdirsVdirsVdirsVdirsVdirsVdir sVdirsVdirsVdirsVdir038kfyR1r8ytaqedn5as3H6T1OnXv6EFdmlYfQo3PYFV96+XPLmi+W9F tdF0W1Sz02zThBAn3lmJ3ZmO7MdydziqZYq7FXYq7FXYq7FXYq7FXYqlvmPy5ovmTRbrRdatUvNN vE4TwP8AeGUjdWU7qw3B3GKvgr87vyR1r8tdaqOd55avHP6M1OnTv6E9NllUfQw3HcBV5lirsVdi rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVem/kj+SOtfmVrVTzs/LVm4/Sep069/Qgrs0rD6 FG57AqvvXy55c0Xy3otroui2qWem2acIIE+8sxO7Mx3ZjuTucVTLFXYq7FXYq7FXYq7FXYq7FXYq 7FUt8x+XNF8yaLdaLrVql5pt4nCeB/vDKRurKd1Ybg7jFXwV+d35I61+WutVHO88tXjn9GanTp39 CemyyqPoYbjuAq8yxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KvTfyR/JHWvzK1qp52flqz cfpPU6de/oQV2aVh9Cjc9gVX3r5c8uaL5b0W10XRbVLPTbNOEECfeWYndmY7sx3J3OKplirsVdir sVdirsVdirsVdirsVdirsVdiqW+Y/Lmi+ZNFutF1q1S8028ThPA/3hlI3VlO6sNwdxir4K/O78kd a/LXWqjneeWrxz+jNTp07+hPTZZVH0MNx3AVeZYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXpv5 I/kjrX5la1U87Py1ZuP0nqdOvf0IK7NKw+hRuewKr718ueXNF8t6La6LotqlnptmnCCBPvLMTuzM d2Y7k7nFUyxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxVLfMflzRfMmi3Wi61apeabeJwngf7w ykbqyndWG4O4xV8Ffnd+SOtflrrVRzvPLV45/Rmp06d/Qnpssqj6GG47gKvMsVdirsVdirsVdirs VdirsVdirsVdir038kfyR1r8ytaqedn5as3H6T1OnXv6EFdmlYfQo3PYFV96+XPLmi+W9FtdF0W1 Sz02zThBAn3lmJ3ZmO7MdydziqZYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqlvmPy5o vmTRbrRdatUvNNvE4TwP94ZSN1ZTurDcHcYq+Cvzu/JHWvy11qo53nlq8c/ozU6dO/oT02WVR9DD cdwFXmWKuxV2KuxV2KuxV2KuxV2KuxV6b+SP5I61+ZWtVPOz8tWbj9J6nTr39CCuzSsPoUbnsCq+ 9fLnlzRfLei2ui6LapZ6bZpwggT7yzE7szHdmO5O5xVMsVdirsVdirsVdirsVdirsVdirsVdirsV dirsVdirsVdirsVS3zH5c0XzJot1outWqXmm3icJ4H+8MpG6sp3VhuDuMVfBX53fkjrX5a61Uc7z y1eOf0ZqdOnf0J6bLKo+hhuO4CrzLFXYq7FXYq7FXYq7FXYq9N/JH8kda/MrWqnnZ+WrNx+k9Tp1 7+hBXZpWH0KNz2BVfevlzy5ovlvRbXRdFtUs9Ns04QQJ95Zid2ZjuzHcnc4qmWKuxV2KuxV2KuxV 2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Kpb5j8uaL5k0W60XWrVLzTbxOE8D/eGUjdWU7qw3 B3GKvgr87vyR1r8tdaqOd55avHP6M1OnTv6E9NllUfQw3HcBV5lirsVdirsVdirsVem/kj+SOtfm VrVTzs/LVm4/Sep069/Qgrs0rD6FG57AqvvXy55c0Xy3otroui2qWem2acIIE+8sxO7Mx3ZjuTuc VTLFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FUt8x+XNF8yaLdaLrV ql5pt4nCeB/vDKRurKd1Ybg7jFXwR+d35K6x+WmvBaveeXL5mOlakRvtuYJqbLKg+hhuO4VV5pir sVdirsVel/kj+SusfmXrxWr2flyxZTqupAb77iCGuzSuPoUbnsGVfe/lzy5ovlvRbXRdFtUs9Ns0 4QQJ95Zid2ZjuzHcnc4qmWKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV 2KuxV2KpV5p8raF5p0K60PXLVbzTrxeMsTdQf2XRuqup3VhuDir46/M3/nEnzv5fnmvPKYbzFovx OsScVv4lH7LxfCJetAYqk/yLirxHU9H1bSrp7TVLK4sLuM0kt7qJ4ZFPgUcKwxV2maRq2q3SWml2 U9/dyGkdvaxPNIx8AiBmOKvbvyy/5xJ87+YJ4bzzYG8u6L8LtE/Fr+VT+ykXxCLpQmWhH8jYq+xf K3lbQvK2hWuh6HarZ6dZrxiiXqT+07t1Z2O7MdycVTXFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX/ 2Q== + + 0 + + + + + + + + + + + + + + Cyan + Magenta + Yellow + Black + + + + + + + + + + + - + + + + + @@ -127,183 +129,3921 @@ - - endstream endobj 3 0 obj <> endobj 5 0 obj <>/Resources<>/Properties<>>>/Thumb 25 0 R/TrimBox[0.0 0.0 24.0 24.0]/Type/Page>> endobj 23 0 obj <>stream -H‰Ò÷wVÐ÷u6PprqVàrõú!CL!#L!cˆ��¹s)”+˜(ø**d±P�{°�Bz1W!� Rc¨`h¤`h¦�œ Ö"u-r¸‚¹‘ÔX€TÁ•˜(èš•X9…`Û;$ú endstream endobj 25 0 obj <>stream -8;Xp,rVCX@L?f9"! endstream endobj 26 0 obj [/Indexed/DeviceRGB 255 27 0 R] endobj 27 0 obj <>stream -8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 -b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` -E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn -6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( -l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 18 0 obj <> endobj 19 0 obj <> endobj 20 0 obj <> endobj 21 0 obj <> endobj 34 0 obj [/View/Design] endobj 35 0 obj <>>> endobj 32 0 obj [/View/Design] endobj 33 0 obj <>>> endobj 30 0 obj [/View/Design] endobj 31 0 obj <>>> endobj 28 0 obj [/View/Design] endobj 29 0 obj <>>> endobj 24 0 obj <> endobj 7 0 obj <> endobj 14 0 obj <> endobj 15 0 obj <>stream -%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 24.0 %%AI8_CreatorVersion: 25.2.3 %%For: (Daniel Bayley) () %%Title: (illustrator_template.ai) %%CreationDate: 18/11/2023 7:30 pm %%Canvassize: 16383 %%BoundingBox: 7 -17 17 -7 %%HiResBoundingBox: 7 -17 17 -7 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 14.0 %AI12_BuildNumber: 259 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBCustomColor: 0.960784316062927 0.396078437566757 0.396078437566757 (#F56565) %%RGBProcessColor: 0 0 0 ([Passermarken]) %AI3_Cropmarks: 0 -24 24 0 %AI3_TemplateBox: 12.5 -12.5 12.5 -12.5 %AI3_TileBox: -294 -408 318 384 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI24_LargeCanvasScale: 1 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 4 %AI9_OpenToView: -11.8017206063087 5.30848013109426 24.41 1524 934 18 0 0 6 20 1 1 0 1 1 0 1 1 0 0 %AI5_OpenViewLayers: 7333 %%PageOrigin:-294 -408 %AI7_GridSettings: 1 1 1 1 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 16 0 obj <>stream -%AI24_ZStandard_Data(µ/ýX´È: p8 C   �u¡F)’¤L…6œRUüaóc£Sþpl6gK¶»$C,hW¹B ÈÄÀ:ŒZÍ×­—šþ2)ŽÓ@DI|}¸ß1ÄèPúTE� Ë#)E�#’>ÉÈ‚аŠ*1$§*Mp‚’аbĸxÀ÷;ÌòKçÿfç-TÛ³XÃþKžqÚÉÎL²¯a áá4ѯ·“Vþ’gI„‡ÓL~é]o‚üÖk€0Š*;?þâ¿$¾b>ª ¯"äŒYra«ÃÂ%†Œä‘ôH.ú¥#òä:œ Ü�ȓ˯õ¤,ð@¿Ò$MÒ$€G¢¦I¢ tJZr‚S„¢Š‚“Å€~qxk/9hpœú¥/·îý•¿L£Nã×_¦N€A§±$éWþŒó4[=&#Â#úž\Æ“·u‚I§‰ ÓB\[û øÜ÷/SÁïü"~éŒ-ŽhýgʾÏyKô7Ô§¡NE§âÂ)¿2r¹U»†8NUÐýú‰ªõÄÜqœ†ëBå�„²c£Š~ýž’‹«¢NÆïi;î7V¿Þîêà8�âqÅ÷ö›·_ï”<~eÃÚcQ¡àÂC¡U¹m­J80ŠÏo^¸äWˆ1‚EDDìç¶]ó<ØÕ~‡¥Ôoù‹»ŒA|ö�[_‘žNKöô&qel1]Ø·ßÖýNOȉÆEB(-ö­˜²·KMÈÓzô»J&óiŸ†_[, o‰pݳb^¢m}ëÜ:ûxš=\¹µÀM�+ôo/«yï)³g[OÎóâºi•ÜÏš­uxrgÕÚ›ÿÍœ�îÛû"4É¥Cé�Jök§íg*åÒ æ>WÛéèÑ¥KÑJ‡P:¤èP:¢Ç[-¯Ý*Ùó»Ï1Ê�· -bt)¥‘RZQJ饥RöM ;óÜúóÂgLÝUÓ&Þxs8OügKµ\|=ΣtοÕ*P;)¼|&=Wï”Ña”5dö+öï�[ä{ ì‰ÝTØ÷¹§ÓvºJä…Yt³ìfŒøyùõä&,. ’díãs�ä·@DˆC§ôz—ÙÜ ôz‹¾3;ìû—²g!‡#‹äô7óAÍ{*À#„1j�ÇÕ‰9aÞ~•¼ö,–*yKDksÓ—¯oW:;ìŽ:m¦al-šŒÛ Ö>S–ÿn´þÄFë#�£ú�¼y0Mù«†³AþiÂîßí%�Wµ™§­ÚwÁo]Æ -‘®xÁÎMg¶DßÜ: aÄd@zßsàóäõ̲ìí -¾¶:lt×­õcæ¦ œwé}ëeÙsp¸å%�ïÓ*¶ÑÊ® -¿ï<à÷F§µü%{-ã¶|‡÷ÆÙ'É/iç74|’´Ú(aÑs>‚bk¶¿’'†J‡Îƒé+Àƒ¹ôµ\fî)yÛqS_˜w`ŽÞ_y›Íš'&Î=µ¹ŸÃ s0G àÁ̹¼�9ÍÜSbE2’[×¥¡”&QŠ"AÖ#)YÖ4Yš&Jiš ˆŒò{¿�íÄ Ñ00°­7-/Y NP” ))ŠÐb@·&ɪ$M“Á¨Š¢IŠ”Œ¬hQº4Y‘QS%eEÃv\­L2r‰<º½ õèg �¦1ÜǼN›W@këÄÚ:ÙÕÓ.ŽH’wž”†7wGS`1†(¾ª/ÂÓ¸âñÆ ¡ǦÂa£9d`Wp]£7àÐ pØ|›„I’‚"+ J%§j’,)Q�¢*(+ˆ’�Ä€nYP¤*ÊŠ¢(VÅ€›‡~5š×^˘~ÃÆ Ï V6Þ°X l�4ü‰þ8ï -­zB"èÜ›U] -¿un‘§¹¼ñh4þÜ\h¸‚ Q «P’ îó‹ïãi.¿øÐhôÛoWv~X0:„2z|ÅÓh×€áŠÆF=ìà E~-3xþ˼SÁ«†m¨c»!S )3#@c00,« ¦“MRtÀD£à`ˆBƒp €@ ƒ��P‘Æ1 !,³V“A]6“‡v‡a ‚‰³%’eÜÓäÀÜTÆ�ȵoûÔGÓ_G–e0£èK=Þ&t P„£‰©WÑç�E�€¢æ­³¹£ø"”1|‚¡±¿"¹z‰¼qåñ˜5 -æ8¿@Š�¡_ð¥ï4€Î$=ö¶n¯#À4µ  ‚öœZfU¥u[ ž{=›½®“dÍ%¶@ɼ¡@Ó—Fõ˼<æ%è,rÃi0¨ˆ7´jþ=�A%ß5¼XYJˆã¯Šý0{·LÔ²3è:üQ€ñþZï[1NwQz–q¥_1;©KDÇÁò›¦^ Öâ¶:Ÿ’l…(‡°r£_å²Â–Ï#{j?¹·Êù¦ÃQèžÞõŽý\kÊlÈ ú 1Ë;mトóŸ�G*xÔÿêhÆa½;H9‹.Âû /;é½&¤ò ²¨î7fC‹$¨½®Á�À´“‚†Ö%©ÛÚ�aדõ9ÈŽ¬Á²3­¬5į¯7¯ºç+¬ï°:õÀ»Ú.=Ð;¼O2ón§¦"ksÓóÛæ;å>Â4*—¶ðUÁ�Ü�¡ß?$àn|X6O,Ú‰<Ît‚ÄÔ úʆ°2Zµ‰ÓŠ˜ä·AµÍæA"ÎaËŽÁ dOÓkfÐÙ¢-„Vjëg¦ö?é[ËüâÌo©Ùã•‚$`mç~Š"YM¶ü³ZíÉBâõ ºfq _½… --´cœôgª»‰Ô±Žkx:Ÿ¹ãÔšhdø‹gÓQ^Â0au½ÏJìqû ÌZ`‡ÖÝáŠ$÷yó#Æ:Ü´¹¸«ÆOSVò|ßÉZíuòª?É�ØmÒÕÀè3§ÖÞÀlgLÇÄkð¹Ó§ ¡×�|¨Ð»Ejw¬é% XŒ/›N$ßåtœ…ŸæäF~ƒVMÛ=ÈU¼º[Ä%ù6‘m~—{¸¤»¾ÎŸÈÖX²Cg2€pq­6ê«1Ô«Ìí@Ž_“}ºáøa×ë^¡Óq8ºyG£ 5ºñ£ÎŠÖz�^0ƒ±Ã1&Õ4¸± Ÿ-X\Š +endstream endobj 3 0 obj <> endobj 5 0 obj <>/Properties<>>>/TrimBox[0.0 0.0 24.0 24.0]/Type/Page/PieceInfo<>>> endobj 14 0 obj <>stream +H‰Ò÷wVÐ÷u6PprqVàrõú!CL!#L!cˆ��¹s)”+˜(ø**d±P�{°�Bz1W!� Rc¨`h¤`h¦�œ Ö"u-r¸‚¹‘ÔX€TÁ•˜(èš•X9…`ÿÿÿÿÛ;$ú +endstream endobj 15 0 obj <> endobj 17 0 obj <> endobj 18 0 obj <> endobj 19 0 obj <>stream +%!PS-Adobe-3.0 +%%Creator: Adobe Illustrator(R) 24.0 +%%AI8_CreatorVersion: 30.6.0 +%%Title: (~ai-114c9dc2-27ad-46af-99f5-a98294587d8a_.tmp) +%%CreationDate: 8/18/2026 11:59 AM +%%Canvassize: 16383 +%%BoundingBox: 7 -17 17 -7 +%%HiResBoundingBox: 7 -17 17 -7 +%%DocumentProcessColors: Cyan Magenta Yellow Black +%AI5_FileFormat 14.0 +%AI12_BuildNumber: 109 +%AI3_ColorUsage: Color +%AI7_ImageSettings: 0 +%%RGBCustomColor: 0.960784316062927 0.396078437566757 0.396078437566757 (#F56565) +%%RGBProcessColor: 0 0 0 ([Passermarken]) +%AI3_Cropmarks: 0 -24 24 0 +%AI3_TemplateBox: 12.5 -12.5 12.5 -12.5 +%AI3_TileBox: -294 -408 318 384 +%AI3_DocumentPreview: None +%AI5_ArtSize: 14400 14400 +%AI5_RulerUnits: 6 +%AI24_LargeCanvasScale: 1 +%AI9_ColorModel: 1 +%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 +%AI5_TargetResolution: 800 +%AI5_NumLayers: 4 +%AI17_Begin_Content_if_version_gt:24 4 +%AI10_OpenToVie: -29 5.96330275229411 36.3333333333333 0 8187.94036697248 8190.23853211009 2765 1260 18 0 0 78 127 1 1 0 1 1 0 1 1 0 0 +%AI17_Alternate_Content +%AI9_OpenToView: -29 5.96330275229411 36.3333333333333 2765 1260 18 0 0 78 127 1 1 0 1 1 0 1 1 0 0 +%AI17_End_Versioned_Content +%AI5_OpenViewLayers: 7333 +%AI17_Begin_Content_if_version_gt:24 4 +%AI17_Alternate_Content +%AI17_End_Versioned_Content +%%PageOrigin:-294 -408 +%AI7_GridSettings: 1 1 1 1 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 +%AI9_Flatten: 1 +%AI12_CMSettings: 00.MS +%%EndComments + +endstream endobj 20 0 obj <>stream +%AI24_ZStandard_Data(µ/ýX|¸:’à90) ¹ †0¨CEÜ´È2‰ÐÅ¥ˆ¥"²ÇÇ$¦¡ÌñÁ-¢ÔŒl›ì�×�S h€£œœPc4Å÷¸5ëìå¢Nvë¾5_.Vˆ¸n?&ÃçË­Ùë}Û0…^ƒÅBá`,�÷,”+Âû{K[¹ªA*‚ÒTU±z±‚ð=êñüAZóÁ5Y·f5/|&/‹çy&‰¯ý‹^‰ê·¼ù·•<}ŠÐ©‰‚þ¤=’¦èÕ{Æ'¢žøx¤œ‚âTIÒó ê1Q�òçÏCzÂØFJX™åÖŸ_ö¼ñžøÏX�Š"è�ñ@'-ûþöSZN×�Ä>Æþ½R£lžçE1ÂÌ&> ÕDŽW^ËÌGÈ=¶îÎû¶Ëänc�Ô8û"Ž}„b…˜å·<÷oÚ®Yìjáƒâ�Á€Ð�ÁçpŒ·Æ„}+¾<'â„&r¼÷ #óþbNcý”Xîô•[_LÉÒvó9Ý-Ìç´&aelñtd·n-«–ñv³õÂc°î׆Jéäʪu'ÂuËñtë[¥ÆYÉÏÜaÌ­�Í0¶íôçˆÁ¶–Õ×4R„\‡n€Ã.›]Â|�DáÞÎòq…q‹ñdSõE`0\á¸Â¡þ*†õ×nñ]¡;àÏ ð§p6þŽä¡‚¢8=ðyTõ"hŠ%EUŒ¦7MÔ‹Ô3Å'z<¯ªNÔTA©hz< +‡ÁŸ[ôiÆó6V€x^°¶V fð¶V ΀þ8ïYµœ@L¹16Suu€4üV©Q ’ߤÄèÉ™*ÀT0±müâ+a(˯¾@Ž~»Ù˜�%Õó÷ ¿¡C†Âi0õðD›npH#?Í ž?“WZœ¢×àzA²ˆ¨§Éng?“«D~‡÷xqΩë–CQÒ£‰„¢&Â!|Ï~†�Š®Ì8B?Gö[\#"8«ôÖÈw6¹ÿ9‹l"/ú‹‘8i¼{zkÁ–Ï}®–Ó +:Œ¼èôÈ{Ñß‹‘ú‹è0oŸ#ƒýÊiûšz�™çœpÎ9ç¤æœsR[½òsDÝ•R)ÿVk™V΋o·us*DtÃ9…é¨#+šRL¡™‘$IÒ£@0H$&IÅsQVu€@ÄÂð@0„¡0 ‚‚‚„¡€(‚Á�€@–Á ¶²�m"ˆMs&Ó.&„*ãüOJöeÛ«œä·¿ +¡ŠáLM¬IdJ%S,ûëÈmÆl8ÉÍe%ò.‡”˜ýTÁ¦ö®@ê+Á`\ nÄù Qbüo�ëˆWâ| 0 [§Û%,ahCõàçð÷þ`ϲœ½àøxÀ• sñq¿„3kŽÄµ½–äPõâSϧc×oRÕ6æã "¯›y*àØÃE�ò¸X ï¡“J¶ß#ìã=º#a’Â5;!u7õKu‡ìÓ;oÁ(=—l(ŸPÀÒûxáB(tkžj¨o Ý=+7àþŠÙ�¹&L¿¢ÕÁpÀç…hú‰ r8{Õ¼Ðon–¨º1(¢%F� ÆÝ‰8sè9Œ°„(ö Aðê’P5inB®úôž’¢NýBùÕ oó¥ë�„=·ó»rœ;§äòŽV.§Žlùêºrm‘3µ_ZeaÛ�Ó*°¥i_#ÜÔ‹pßVÒŽî?§¬ÁÀ½FÄÉ=ºáeæ-m‚¯¯e”�ðйY +”þl©›†€ó>M±ºšø]‹vÑ ˜¤Ak€é^ï@/^~Dmu�ʉDjýà×p8è,<ö1N|ÇGPŒ9F›Þ/ }³ j dZ7mA(Ðê ¨!q–� +°çÛ±çÉÿ;Ò­F‚�烾jÑÖR0™0k@¶r ßo~|ˆ>Ç,x�l�%%ýìíAÐÎÄQÇB éÀƒÜö!²ít\a4)%ƒ¹-¸·ö4ÛƒÇ)×ê2ue±µk}¦™¦6.‡T<Ò"ºm†€¢£Æ˜B–$b‰KÆýîí”ÝÓäÇ„c&áÉ;–ÁƒD§[�o… )KF«ò�iÑï£G-3.Ó;Ó¬ÕÛH_´‘:àžÜ]B�.ê)´–œgÒ³cÙ!#Á#3׿1AvþZÉ &6¯Hc ²Jç*r ïì¨*úïLyL}°dMì=A´ùДåÂwt~+Èsîö–¡€'èÎŽh6d=Òm5—ô£ËÇd1hÓé×Vˆw^6†mýVPv‰äör´äáXàœogAhouVëÌ6ç_¼2úË}pˆŸÞRgÌŽŠRˆÝ +K$ñ£ÇÑ T02;Éж*Åꜟ3+È3ba¯„ëû šxéÈsÝžß0�£Zb/‡‡t¯ñ§jgøÂˆaSÁ:£ÁÄL@)q¦L6ÂoDCL�jODŸ��$/Ž"­RúØ”kâùÄcGyaV�•R£X–0‡ü·´ +x1% %Q9€*"ín±Y‚AFK‚Ôš ÛZP…§�Ù¼1áG4¦ô²V‹«ÎõýrQù Vm^cJ")zÛm'EÝ`é•Ã9Ôop÷nA'rx:H ‘ììÒ“ÒÀ“k²y¹a`퇩*ŸK·`c@cÖjº5o*÷ë +-芯O` Ö‹QÑS©�"uƒ4šà‚Çøýõ‰Qé¼ ­ô(ëxcW‚,V{ !Î>s¯1’.ÇPrþòv½|ñ`¿Ãy‚ãñô1ŽõúƒQ'ª¶kÿx¤€Ë¿*ª2Á&�æ(‚IϬí”H8Ñ ½m„]À +k 4­úà "¼ºú‘Dx$CÔsÜê˜À|Œ²• oÅõDz÷$ø@ÞZ?0ó¦rüÁdjUxC§ý÷€Á«›{V2Z ¾jñû�èg95³ƒ¸žåwlæ›™ª§‚C" ¾¡ÕÔ2úëY-Ûí‹Í*m’ÿØ“¼þéÝdY¡ré„Ú‚ãY¸"ò°¤¦!KÚµ¨ví� ´õ¤I3€‰/žQ[1ÅŠ1K¿ µ)Š¿])r¡áÝ{P×…¢4É èÛ”ÒaeÜñÒh¿Í+ŒŸÁNç6M®9roSí¦ß‹~SÙ¦¹E½ ¿DË4)ji�«8v,×^»Nõ7ýÚÓâ¦Ò93ÓK,ìª@Ü̽µÐ ø¥fÞ´�QØIX®L#�Yšºp6N.[(¸Œ9Û½÷=Nn~KÄ‹1h¼ÁrkìáéFe»p§d‘ ½¡¿ªsÎ[L6øÕ‡+Ê`Kïg¿²ìë”P L–Û~oõ†EñÌsÈ…¦¾b¾×ÿ´©§—Ìÿ=q#•Hë¢=ŽDYÕÙýÀ„»nº–!_ Á ¹k)nò¨©9Z|þýtwZ€‡Úô¹„ƒžÂ{êÛ@%P-ÿŠÀÔü´›* ÉvH|¢ ŒÚ‹¢WE†p ƺ +éQËèãŽSwÓÎ4ˆÀz=¤®¸§,ix¢Ø5¥ZvyL¨×*´DVùW#×ÉD g‘6ÃÖ’D ¸Þ;Û“ü4µ�#E2ú´�ßð£q9Ôe›}… ˤ•d±¬$0ä§ig‚çñà¶Z¼w0N¹× ÷ÄfJãTZèdÀ…M´°vÓø‹êx +âi®Ö7„·¼ã³*à5«ïi;ÏÔð$jw—b²í�ëuå7 Êa1›\œMÞ <ØÚ«)€P[µûvš²Ái +é¯{ _ÐÅÜ(�‹p°_0(p³¢5“šˆKë7Ëk½L�cBÙÞ$ÖÒýêâ´“ðÒŠÛ:"öêjÐéFã½þ¨ˆ¹L0µs®—:ðXàˆ–éR|dDõZ}îË:êsÜLÕAm�R µäÕL!Bá®¶Z1:Õ�Ø]hÖìÕ½°:F;F¿ Ö‰¹¬FqD}t‡Üô˜¾ûœþULÐ0X¿5É÷9•m”C*‡äñª9ïî·ùpBc7œšµUò’“:1þ¤²Ât%UUÖÑøsß"ñvûN­z®b*Íyä�}’×ý¸á}Ò{ó�}öJy’Ò\Ü�=Qu`d‘!·/YqÛ s;à Ð/M„+&P®Z}"´' a RHëZ pâG¿‰v�ÓLgŒÌ”›�:mI å]=¹Ã¼"‚Uàè‰ÜÖ sÜA4ûšÊÀ¢([l†BTgèJsÚGØ+‡}UÞÖhªÌPÙ+IéÛn%¯[cæÓ´AÐCPæ®D�>+„Øe,È~™ :Ðð J�&œ"Š0UsoÇMH}6Ǭ4¡–?'ð§CÞÙ¹¾˜'•¤�íÚ%ºÇFµ¥€ +Ž…õ±Ì +¶;q4aG.PÎ è·½ÙÆ�ÎwRY fO0¼DÅ8J]oZÏ3Sn¤ý2ðc /˯®þòÄé 'Ö™¯Ó+èÓÓÑ]ªPp‰�•”WÌF߃QÍÒ!òÖB40ˆýĤ Ëú…è°ý1öjåëü] þã•sºˆý[(«3D(âÁîE˪Ð0P?kËñë^>káö/1F Fm«Ât>‚ q.Àã`"àØ!ò+3çÀ\^`Ð"ñøÉ¨1N…Ýñ€Rí,=�—X ÕɰqšûSÏí´¢DÐsñŽmÁ²ý¾®¨4^¿"VAu© ,בzo:¢šç,¤[«ÒÖ¢ž£¢¯3" )µ)ÊtÒHœ] �¯j¤/P%ÆÒ�²ô¿V¬„q}ÖUÙµGƒ»h7¨h&�óOÜ0ýAæ*Ë–w œåã±ߪ´àƒ¡dŒ‚Yý’+]ª…šõ®ÕÙ˜M}[Q»8~Ï5ÆŽV†sö�>a^‰ž]Mœ°�‹f/C7°dH�d@-!�½(lWfã)�~Ž»1á`oŸ÷,§¿÷)›xÆ’E¸á™®3�X¼B<4Éóá��#qØc“1‰²6 ¦4TX0þú ò›h’%)0±ºxX³vÒTÖÆrJXš3bZÇ` �£W­¯TÁÄò{eu"y*˜l1zÛ"I^ˆJ¥Jļ ¶ø·†zžáö +»þU¼üªŸüÆg‰×õàˆâþ<–%ãõ3Nðÿ ,˜D¾{ÖËC“¡‡Ç`+§XÇK„²Q˜k�‚=І¼A\e—Ö vÄõ.ÎWy·lu®¶-×gåÑ +÷ʽ2dL"³ÉŠ‹®]Â}©f…ï�‰ÐCWJa�ÂÈó+•´§úù]cöW�çUúS£{]øcauô1Å7/{—�dõ ¬è®·»Oc‚dö£ÉÏYI=Tõ†¤¯æxbXSšÏ؆ÍTkΊ?ת¶ôµ¤!Å‘xæv: HAªîmàKaû̱"¥éfß“–f +ÆØ¯Dž… µ3ù¹æ”saÁ]i§#Jb\ú0ÆÀ W^¾OþÙi~¶óŒ ˆWö°y{ÔÙ¢:Á0r©@£pÿBk \ óe(„j8ßÐyÄ=‚X3Y¼8�¾Ãõ.~œÝœ-•Cøåkõœ»�–(\Ë-–Ð]W‚§ãìðôëj‰¯ n¬<;äwª¢l ã‘0ÍS°á뜳•44×·–1¥ÒVçÎË�1�Oíÿ<¦ +ç ï×LÍ6dBZ°×þo32á”ÕqÅÈi©£ðÃ0W>bý-”òmÕ2#ðw®’Qm¿ �ƒ16?{JCw +ZÖ÷œoµ¹uAï.ò%Ô�ÉQßwJÜ/9ÞTDÐßø…ð:Ýí[Õâ�Ï!Ù["yÆ÷mÈ[`ÌX®Ë`òYpNhÿŠê6\t�ܺ~ xòJÙäÎù,Gëë«ñý:žÙœ»lQ¬,ƒ×³¿®rÞ€Žõ¯ø~)It>ç3Öh<Ø $=L ÅmœSMg:8§�bÆ'ŸÂ™ãW¥HÌùÑ©–gA@ÝSƒ—9Rý2׬líN(qØ`vËün«ÌÜu¯C²s‘â.è¯Ï�^æÄ×FÔ ‹ ÐÏĬüQÒßvd5€‹¤ˆ àvòÑÿçG–bÅ–°PÑÌ®7.ƒè°‹@RØa$ ÂÑi©z‹4s¯˜#šp1DÑ,éšåôba¹ñ Ý97Bím\‘¹*_ +fÆœ³{Â÷Òà‰!ŠCo�}ybQæÀ+^Ù=Ô’ÝE´Ÿm0ÇŠL‹a¡ó(C]±aŽ·ÜE뺎\º`ˆçÅÚ$ž�ˆg +³Æ@ôaÃñºŠÔýºX`)ÝD. w4J +<¿Êà”,8Eê68`ôŠ$ÉI"uCUv§È?Ï;…¢ß½ú¯KŽ3ðÚöWÂÔø½c~'@P.—…Ïš�“]ú»£uz�åÒøå±žµf2ƒ4.©Ã‡ßº#Îã;Õm2³nªæ:ø½ÿúæË1™œ¯ñÉùz~뎼zÑÌŠÙd²gF¥gø6Íë²¥KÛQµ8Û‰¿¶^õùfÜÑÜø8Ûbº4Þ„ß׸ó~36]¸XBÓ…2Lß;ƒ4îŸWãüË­7vÝ>5næ±=NŽóØšÔw+~ú†"“ãñè÷�¼ßv&½\øô7G­¸šâ|.ù=e+îÂ`�šµ3bðfѦnÌ–²÷ȃa¿_‡ã¿å‹â;öÈäœ~ëŽV$î!ìg¿Üò¹ ?‰ßº#òÞܱ·|wO·$Z)s“©š‘_£ýDæJ[i+m¥­4Noü±ß¦È\iŽÇi§ò=uFD|´B)�äÓgZió÷;Á —.½äVùÍÛoþ“T×üå†�[Ã^þ’0ÕOw×Üõí–{mûŸ›ðã4\Çaæ/ù/æ&ü(½Ü,ÎûmÞOqÖ9Én·ø»�›ðÃ\0ïãŒEû1ù™_ã¦æÍè×ßnšµ†ß ÷û½=ÍýËo†Ÿå˜·1œíJË{ìþæ~ÜÌPêoÝQlÿ¿E�ª±�á*ÙÞ“ez܈³/Ðòµ@Ësö[ðf³þ ã ã`äjÅbä_T·e¥]ظ3÷/êfÑân3<Çw›±òñ:Ç48û-iÿ�Õ•¶ÒÈ�ÈJÛ¬´ƒiYiއýÜt\Ç}�G“ã1š�K¦^ºšé.�vf³ñ¼ wñ¼Ñäx8Pˆ¦ûÿÍÖDêOµ?—¶uAÕW½KÅÐMÍ`ºJ7�³Ñ`¸Îó6í4]çaj†ë6u“ñ6:1Þ¦Ût¯«\fÓe¶Ìc•Ÿ ÛC˜ÓÖ›Ô}+î>~Us£-~×ecá†í!Ž»1[ +q }É�ÔÛ“‚õ]à÷/¶;;è¼±Ý_ãÖ›´�¦ZïâìË<êpM\ ¿©Šh}}ËW.�36é{<š0ŽÛ÷¦qÛÁ›9þ¹$Ý ‹Êø5Á­Fí–ÅÝ�óm½.»¯ï7ãöMÜ9Ûªm9†ùMÜ4U½o⯿~Ó«á÷½é‹IÛC±“wj<·ž›zš7sœ=�i ¿ÉÕð#µÕx„z˜(Óg~ókÌyÑWçüÎ~E|{j¾Æ^‚‘žGºuÎ7XÌ%–~ûºµ1fÞyÞ<¿dü7²��s=VWß}#û(nŒ�k`qóܺ®Þ<uÙ¢\Åó6Õó|ÂÙ �üXóæ9Ýã×·_Ò¾üæy�m±ñ~�·ØÈqœÇz}Iò½q ªµ-�/¹c$ç7ç=’Ý’Ú~”j“¡¯­s~]6†·Î9W½~ó÷œ“[þ¾[ñë‹;—ÙÄ�‘@ß/Žsk#ûøñ7vÝÞºY»°¿ä&n ‘Û6ö±èí&n ÜŠlÞó¸[_—å[Ïãkã6÷›±7²�WãܺäFö±üµÝ¸Æ¸ÁëKòÞ,ÖÝê<“Û×­,Z}7ßɾïëï-6zõ·¹õ¯ß<¯ý2nžw×ï7Cï‘o¿q�­ÕÚ,æâ˼5CËälüíɽAÿz±DÕØö¸4&ïcŸ¯7ˆ’WŒ}ð·<¶= Ðt3nínz�ø5áûý7f»5…ìcòó»�¡æýø51½¿�ó535ü>ªgàoó=a*“æ|=—Öa>*ç~°�n]�ˆô÷-âÊíÉùzjÆÓ¼\øÒi2^†ë4�#KÃ…‡z?Íñ¨.Õh18©FKŸt›‰ê5YÃOD‹asÜ/§ >\$´0 \%6Á¢Þ!õ…_”lcQ’d0…Iv*°T²Ñ¥b€ÀâP–±ÒÔ)óùr%�¡d¡ áU `�‹æxxT G!B  +?Á¢Ud²à4ÇCR‘`¡¸ó%Y†aI†!W”ì€%Ó´X’ µ�“LSÑT² JÖõ.‰ Q¸¯ÄB�\ ñG€óë�P(Y<鿘‡R»Ï+¢Äâ.<’ç|!ñd˜Ë-LP'Õh¡¤j´ø­ÛLRˆP§1~0�¨9¡½u<Ÿ’34Aœ˜Ø TÈ,Ý� ¨Ô¹OçÀaÓ©áC'® µ # à¤Äê Ø<n€hA„ñ–�£0 +“HE‚…r$ZÉ4Ǥ Q2ÔA9ɶK’M ™ˆ‚DÉ: Ž’¡@%S‘+J¦mã$a¼(�/È^Þ)aÂ@ tib¡N†úN  ’rÅñ,ºÔáÂ:Y2äU5Ç#Ä–ÎãÀhX:Ðß�0ñX… I/™ bU,bž…§¹h: ÑF�!©ëa¡ƒ�…Iè °µ€4±P‹(±0ɰ0 +�Üã1š0ô >Ë<˜ŽÇ׸©ZÃ�Ãéû5j»"q¶«~%@ Éu=GqXT`¿´å›ð£\¿&L ?Ï�¦ÈäLÜŒS.u}—ê6NxTÃë²&ډអÕã¸�þ‚›.°5L—ÓãÖž×—3[ êpz;_céçˆ3çvþroØ<-žsÿIÞ«g¹7úž<5¶®;õžÚf>˽q½�˜4i;½�7ãÖÔvº4ΰ~êû*0ýœ÷õçív^_©Kˆwâ{ ¢°nIýÒŒ­cå+u¿íWßþþ‚ç¶§¸þUX·$×=an=.�?&˜‰B^ƒ ŒàdÀÒ�ž!é*V á ªe°è$`#”©TTt2#úòN <!))cÈ,€ 4&î»%Cx`pÝÍñ(•”JÂÓ8$ÀŸD zQÁé(…ÏD­Áƒ�$�UöRDæL*$�EÕÜ‚ÔážTz >%Šª �šã1À¡%¯AÄ+Ûï.: ª³H€]õ?ƒú²HôËA¬`w’jbÊÃEiŽGE…(ù©è8ˆhl�¨‰sÑ �€¯¢¦d!10àyË6â&šã1àD F6VÙù&(ð°OÛt¢:¡ñ\Åž<Ññ \tV‡] 0! èE\è +!©Á' ³˜„4ÞsWTg¡$PrÑ)<§w?+ $Õ�Â(LDVEÉ…4Þ²A²�¤Ԍ.[§²zÉïNFa¡ìz÷£‰…d"-ê;p %‹çU?†Ð¥à)T²hŽG$Ô¼ê‚õ]6§Ì‡JÅ$œ„:,4tN6Ö€Q˜p W€€`>µ À¤ 9O†‰EX"�ª€¤ƒŠgApù` +$£. É( � ¢2N²Ô6N2ˆËWɶq’y` +“ìÓƒ‚‰*1É,0^”lÓƒ‚Ìâ™@2ƒŒÖ²ž ²Æ‹’¡*šJVÚÆI–}®÷……‚VDDÕë0…‰'[µh‡ô„*(#°¶&ÙftÙ*ŠE4�K¬ ™¯„)Ü7Qi—ÀÉ’jiÆú"ǺpQj©k ‡„‰Çb!xHͰ`%g±ªÑÂÚ4 …"¨óØaš&•sH ‰ÈH’$ÓH@ˆD:¤�VQqÁˆ¤¡™H�£8 ƒ(£0„ˆBÊ(3C23Ò4�ðˆhÿÀ—‚Q~BÉUö´tC=Y}kœ½½7WÏ`óÆ%¾\“‰ƒ¹óyέÓϦ6 +仼À-ì ¼�_Иb£†ü¸ƒB®b˜f&/sžOå»�Щž•´Ñ�3 ´÷QuÜ¢ªíû­…üã¶að�ÓÜB8¢ìžfÒÊ„òJAÅÜÑÞ<ŠÆ T„ð šB#Rã­T[²r=úò)¥>MÞ–X>æ’gxx\òA ßPƒCê—ää>gµ<;�BçK.fT„ÁqøÌ�kï}…Ëæ8R GKsá‘[w\XdÄë 8akb Í +þ9fí¦"�¾ÕR7ëÌ'¡:‹–ËtC Ÿ}`(]9£ŒYÂåóSæ÷çO‚n»fU±)÷r˜Ä¹Èm8Þ+6^l@vî-‹¢Y”8§ØM,Sk6<@…8[.nÞý¢3# H� Obæbð¯‘V"„ols òFà�dH.çlm%#}vAs]N>9%ÇŸñ�nìa·­¼¦LÅæ—H‚Í#Û)¥–£ÈQ™>Û&\§Ã©M#-9¹÷0•èÒà Xž†K¿JlBQ#? Ñ÷xÕÞý)ÝgŸœq(”¼œàDÓç©VpVëmyrÍ Z¡ò{NšÇ¤²˜¦RÁŠß¶ÇQ½ q@ÿ]‰xTÒ;ã)Ê~ +žaÕÆwSîÀÒœúÈñÆ€Ó¹­�ÒŒ=^ŒpH†”Z—˜6„À +ýéœÙ¸«¸™v8‚lE—á" ;oWyðÛWéS€[W¬tŶ˜£0g<ÖŸè©«EM‰• ø²D&¤+ͰĄGP°ãˆ• ÎÒD0úPŸk¹,íG8ÿfqmi\Òý‡›Lו.}Èì5_jÈ+X É=#ÎxzàêÊý XŽÄÙ¦<š ¨×ýþu³­1‘”ó¤�"AúþvÑù‘ä©Ð3Ûì6ÁÎl¶àjPã´ÎC2’*8êþ¸T©kšW(ôØPl�óñ‘ó7µ�3ñj(¸ë©�¤¹NØXç ;�‰„ .„¤IÛ1’䌭D°ïB$]¹c¼ªØ¸¬@Fˆ¡ÍnáQ.Ë;…Ô/øŸ8;¦1™•a�̪a¢/ óÿªz�/kðÒ±÷c õ’‹ +hз2€ËèI2: ã²ó+9ýÝB�gÅ jÙݳS�–Ù¯Ã�ƒjƒB�H]£lš9üó©ÀÄgË^2"Š�R'ÖøI{Ú©dˆ÷�†€�fŸOõ.´}Uö…{—zew¦q,ˆÓ^^– +Æ0×·0luæÙäJw¼_¥m·ê\•mÕá ÿ»+,÷0h‰;3BP§üL;s?åÃÊ቞ ±6J%}ö£«Ž¬i.ƒÆÿz_Ÿé¬{«÷fʘjƒÿ2/–YÁk""z‘ºÂ•,74.+rß9çé[¤{ˆo~¨Š8ªáRiŠøˆÞnï‹æŒ—âõ#ಠüaxY5à¶jëc¥Úòû‘Ÿ7øpÃþäð_vâ¾xÚ'~­QP~:%§ø›„h5NpèÒ� Àj ß²Ú}”SÎ�x7K2 0Ð=€­´Kð¸"ÃpnaC@Æ~C(ŸŠ{—EC;🥻ÆÓÓX! |‘2–‚IÈ1wsÆËqgÂŽc$+(C@H6E;ÉŸucr‘làÊXt‚?'–_veFÒó!)ÙKÅûjpÄ»À[ �µá Xžv; Z½…1‚œ& ¬³kkK ûöˆ*¤tØAùî%ÐN¦…ß°{Î~gt­ É»ß �z#9i��ƒhL1xñEýÔpQós$ÀåUNVÐlSAÑ(r�`Ûíñpé|0ŸÑm<)e]?Æ{a—bD[˜Å†d$C-<ö{e :—/4¡(ÌtCÅ7;`�€ä/úâ~F˨áJÕŠûº0º, W‘¢?}X0®¤¨�‘i7c€¥Ð|·½PKeݰª½�Ëdߢùý2?~%Æ®ô½gO¦Ew`¡„:5M°MºÊ¾ÿ÷¡` £Ó6VböWëoÙă“aMÏŸi&%!Ò»Mˆ«Û?Á¸Kt Ù°ó Ò �gT�0¯•E7µ'¤ÿ˜ªx´Hc +둽{:7Ä5]h¤1éË*[�1JÄÄ•Íw3R™>ÓU4WÒEö� J0{ï“8òô¥0�á@ß"`Çe�…Q¢Ü lá™9)+‚ù&ÚcC⟈HüTøÚ¡Uñ=„?QðN2 >ÖÈqN›´±¸UàÝÜß-jÜGÜ5|×�iêyÇÁýÎ ;ÓÔ-âÀûy êó@DÎÑ8Dó@4ð,Ìò”Vd á�Å™~²8“Í�5´†ÖÐ�ëYgê‹3u…—º\�ÈQd¨Ÿ/Qx©¯4ðJ£ÉÐ+Sý¼wP?ÏiCchÏÅÐ Å�ý:Ï×¢ROQàNQàóB³øjåʬb¢Ú…A ZV1µ ƒØž#ƒ–[Û >ƒ–ûÓ.,Ó2h™SL”¬E¡,[²_idÖ¢Pjî9c4hÉy³Pz9µ ÌBYÊ©1o;Ôcyà|üþ¬ŸÏÛe=íÂ80ÿhö xrüó^¦Â?®ˆ#õ¼8Eí¼r„Ÿ9Ëü]S"(õ±Ž@…2ð n‡‚ñE¯?.G'ÿX^œ¢×Ï:óÐЙ&˜D³8S‡IðâÌhJ™ZTê„ÊÁýJ^¬¤nä ³*æ +"“P9ôƒû·Å2Ø•IgdM gG·†¬?rϲ¶>ÿuáìà>ת Ô ¾øjeý‘íñ|ad&/õô®Û —vÈÙ ðRwA[ŠŸ ;ÙFJ‰ r"�‚®Æãæ!góB°È9A×9«Q»°Õ"Š{g¨Ÿç ¼©6> Cý¼va«¯Uϵd”V%tÃGG¹¯]¡-�H­-�È_­¬úùÕ"ªÊÕjîY^ d‡ñ�ëÛ¼8µ-wJ^œ¨öå('Øâ«µÌO#Ç©i¹¯6Ê V,ÙU`ÈöØìÐ }&ôiy1`H�}®QAë=Ý Áé°æ +GCÀK„ ©%Qh ’.k¼§WÀ á ÔþXÕésÿor€æVxæÏ­]Xa…/è”°"l½ + Î@µ††ýÚ…ÁŠGà¹3Çd€‹>úÁ]Çÿ@QàÚ…�YÃÓB°±¢ Tà¾aQ@-ûíc0«·kVÚÐOY +eeż}p×1I"Ì ‡Sw +ì A»°oI1\äàN¥˜IíÂ0â'͸’p‚¥$SÀEŠƒû„m#�¡ÖíÔÏ{#GÃDñ ÷Ý¢…‹�ì¡I !÷vprŸôzè—år2¥À Ú¶¶LT`. Ú^ä¡ÝÈäÊzT‹qhsÀ[jƒÙÔ)gm¦ƒ7´•qªÍªk®fò•ÆOP…2i…b±¨œÿ|F%òqƒåY�¥„l²å>žªÕû&SkeòX¿ã¾Ç®«¤e¥vÜ7*É: ©²Ñîôé¦Òd�¿ºô&Â�ãÓ" «î|ʇ£(¡Ä+õI¼LÐtkœàd‘x¥ç xÕÙƒA°uGÄuà.l¾5’n¬¦kœnG”Ù‘.z$ütÐÎó…¨I˜2ÀA®u¦’à`ë‚‘O‡àùaä�^q7“4Û €Ó+ò<>ÄŸIâV•N…°Oñào¤î o¡Å#õ¶°jYø¾œ@ÁEþäD­ôn: QcZã‚^¸‘ú]×?ð"0©ƒ½?D@Ç«_€ûù‚ÖÛÛNÛá¾›•n”LmKZQlƒFü;[’ޝuÿìÌà¨Dg¬N…€�݈Ƨ)s€#ÍçfÌ œÁˆœh¬ø¤spF¢«¤%¬ˆœ-Ï Þ¯ÏàŒ’³è>¬øªù…zÁR:pº"ñN‰®ÄˆiH”v‘˜ÞþS�šQª +„èàûܑۤ¸Ý›¡Ö])è �w:”ó$€¢êÖ½‘rd-m˜½¨Ì¢Üït5“݃œ½L!%TIk‚êñ¸~ÅGkHެ©ã#8§×ÏrÊìïú®a–ç‹âÀ›Xx³¡7–�èEy\–öÀšM£¡ �ëÙ’Å™Xœ‰3q`Öãz6CW¶‹úù�ÂKÝ“(¼ÔWXóÄÏ«E%®•:ÄÖ2ˆ ñ ‰ ZVLB–-Ù£rªd- +¥œê ¥œ’;³PÊ©–ìÑÜá1DLýAà�JÂ?ÜÖåŸß¡oàvèc៨éõÇ5½þøãgM¯µˆâ‹^¿§)$šŸ šŸ9@gÂ$‡I¯è³CŽ âx\/r\¼8Ýïô¸žñ}C»0ïë*æ{ääe‰$ôeW²•«ø2þìKßÅ´7efÒÃ¥ßêÐYÕ.,¬_²ƒrdMâÈšz9v¦Iãà>;v¦©Ó4MßugšhÚÛhÆòlF‘gë�ˆˆ~C‡^Èö­Çõ�l=nëq}ëq=Øz\ÎÔK›csl|jÈq¥�ÏÏCá_‰ào¤Î²© 7RçJ¯•¼ZTêúB_è1w6E�)ð©eÐ2«˜¢•�™AK®b¢0ƒ–�žcÐRN�²âQ_­ ¶�f¡,@<*�'üCžðÏÿyÛ¡¸É27RgcüÌUøãX �~è{Ðý‰^¿›Á'ß +Ô­@ýûÃ$íÂÒ“ƒûƒ*98ª³LàŒƒû¶uZBÈú£Ð”£WTº ¯í�mçk;Ÿ‡¿_«^Íáo;Ý ’½õ¦:Õ£~~í¤ +¬S»°¸ì$…‘5̬f�ø{Эha>ÂiáM àLÎ2(`Åg¸*+þåTnOœ±0ÿÀð%òÿˆ„͵}ùx +^ñ|²†h˜h±¢ÄA¿Í“8Ò©Ù2Ø€¿Çj%�€ 4 o÷ï€C@Ì_ûw„Z·…cXe í;ÞŒ;úPë¦=Ã逓cJ^”3‰Z9ÎC )¤: & !bÕKW¡*@´}%•5ív¨J8ëÆý}\Y;Â�¨àæS©�‹'Qï^†~Œu¿ ±šÉ‹d¥ùdP jkvB‡³Ví“� &i>hVËÐá»…¡–…¶t"K-ƒ–ˆÏá ,÷^oåªÍ¡W»°y jŒÓû^M~0ädgYÜÿ$/ÊÉÞ¥ö”ŒtNvAQ°N^ĉ5y ‘Bi¬9ô!¯~ë2ýQIB�”û#êI£È.ZPR)‘}õ4ò�Ù½Gdä;="3#q2Z䢸‹ "¡.ðþÇxšCvңŔ¸ô…‹ŠBö´ cU‘_D*4¼8 nãÍp*skˆÈV@œ "ŠpÈž7R ¡¸„1‡,Q—™,;²ç©E¡ô +ÙÃúyrB!û=ˆ¼"òqƒbä ×’Çõ!÷ð¥�½þR +Ù[Y-ÁƒúyDûÔF9Á.9�’à vÓ†½0‚`¯](D`ïIN�‚y €õ‹¬va¶ 'Ù=)`žýüg³�¨^â${êÄ…þñˆ»·¥…-ØãÃI¾#{ ûÝ¡×:d·‘硸‰ÙU õó�çÆÙóúp‚c—“ýæÎ�ŠûX@9ÁÐfSo6U»0<²J©wa¤ Žƒì\úŽ¥9œ ëBX¼ˆ¶-æ»+»™CkÝèÜnL�?t©Åjˆsa#¼¿â@é$þÜ¿Úh€Ÿ!jýQs5“.�=ò¨�1õ-¤z\´…UËP¥B×(… B»°ÍÂÞ1o ÃëÆ›ï´v\—Ÿ“¤!kp7˜·Ÿhû ¾ ]h2g‹4y;d€@i‹>uÌ8œOæ <ÏÓ.  ÞWã½zeañFÔ -pÝ—ÐéW=ÖÆLSó3“òÜJÇ„"÷”]G*~s¢ä¾=P#Ô5Ëê*ñQwzÌ?¨É§·#tÓ ²­é,Ñô“ŠÓtÒˆ ÓW´¾ôǸôÿæЉôΚˆtK+„{üù¼=uo/m‰Û¹$¦·ãW³€Îóg•ËêË´ {»Ée†„JË:Î'sf›uãÊ¡‘÷ J*ª•´@^i+ÓÁ>5®fâ†F~À8Ýß9²NaûPÜç_ Õ Yœ‰EO Aσ@ìÎÌ %(ËÿñÈ?ÿs;4À?ÿn;´;ÐëGó´|"{+P +T·@�`…S >Lz_ÄíÐ×u,T€[Àq�]åÛ¶õT`(îÚ…i–“n2Õ.ŒbBÄj@zÞwf@% n…:“B*’­„E™™šL +cîg8òR_]VTÚ»KAÊ V"ïúÃsÖ¯š5½&FÖTŽXqdMìèÑ9Ša¢p…—@^Zi2°DPꪋ‚Ÿ±µoè'üóÞvèö xXòšÿX þ±€¡ü¾“kÕ�rÄRrówßNkËïÓ@$Š_Ò$Š´ ‡Xå$PÌ«]Ø™ ´”Pè[ï jƾ?t@Jbö +§1€™l¶,‡¸ }€|è#Iè¯Ü)ôj4ò¨yE¥¬ˆ€…´|†µðº[©]ØG»°D$Ôºµ kÃùtb^F)ÀH›˜BÚ…aB—ˆš� µ¢&X©²��E¶•véÒþ –)¶ƒý„КiÊ�»¦vaíÂ6•�¡]Ø1е Ó.L»°GŠA$v�&ÌZ\n³�îuÔ“‡H6oäiÿD$KX²¢Ò”7Íä$vÕrÃ9T‚ªãCObMÝþ€¼©kJh†¿àIç%˨ÔßÖtkœÀdoo°qÜ·:QÈv½aA}n—µ ´ £4†t&Yvê1`©fN IìCº*„¼�h†Ô2©)i;¬þ¢à5û'dhæù,èÆ5+²ù¸C®2:u`•>‰›—8�ƒè5£Ñ.ž‡×ÉØŽÓ.Ld‰VŽŒ@T ¾_JÃ�ÔYšÃéî;«_²´A£z> Š Lè9ÌÆN¡“¼?ÌÖ1ÌÒ1Dä¿e¿rŸ-'JzI= +RÓ‡òd! 2ÀŸ_ªþ)»¥•…!Q[+Ù"B¶]Â[ZÙQè}“]þ�-üÚ…U‹�ì_é~h«]˜BãQÆdô¹]†�1¥Ÿüh93Õ� úÖ”¾E4HT•¶G^þ:ZÉJW3I(ðÈ ,=*%ô»ÿ|‡ÔÙ§va™Ð7@°÷´ Ó.LÔ.ìo"Pèµ ¨va›‡Ú±÷µ û(ê³>€™važŠJÌ΂/‰ +1 ×ÏË8 YÌÊ�Í +³bj˜�À’àSï«Ûì#3eßö1nÏdŽPŒ3�ß"½ š5uï&°¶ükönȵì"©ë¤$N‚Œ©(D•Èå_»°‘Êabµ »C–±­OFúռȮ]X¹£–-M‚½™ÚJ EPˆ½vaMv.”…Yxß©TÅì,U±€:Álb³Â¬�±¹L»°‰¨3Æ ~NaÆu`&“ê0{ `–v`&áH1û–6„dšÐãðö6À¤o—|+ ,ÒW ‘!E…@3i;`™cB¥"zýì'²Ú…P´ SÍemê)™b¢÷à±µnÙÒô°\3-´ ;bûÁß1!}ái šÌ‡‡öý¤”ɪjñÞµ s¿:sl�/Ã¹ÄØzTjeY›€h·“�ž+: h?ÀÁ¹ãUÊ¢9Mzoï›Æß]/­üaïì«4'ÓÈeݸXÔj¹ØhH’к6±š"±º˜)3!€rùA+9’¢–˜ú»¨+AB w+s¦R, {Æ ”{ÎHÉÝ}À"KDvKhÐ.l ÍnµÄ>íÂpBªÒþÎ! øn€"ûÅãú”F.ïøiFäãÍ®˜è}·*Í‚WLmÁÐbTíÂ" �‰JˆÃú=M!7Ãäõûp@4iIƒ÷®(xý$eL]ã8¼¿� Ô‚’©ÿäõ»iŒcû7'×PËo4õ0ºÚ…áB(î f&SòáÏ,Uÿ›CÖ/ÙÙÏJ; HîȉDnij§¬uQQ÷¢Ä`•-FE9`¸õ´ãÌ¶Ö ‘r”àÿ¾s‹aõ틤­Jª¬ÔPb·Òu¨u äš©%b^ý’]Lh¥va©~^±¥ Ö篛n•v"cÖø¢�tе.ªŒè<ÚŽ€�Q9N“j"Yªå†DnšcÐ’±J=®„ìàódãdÚ;‡XõКøTE t»a€Â]h—¹¨t˜4IŒ#D3 óŽ æ+|€&@:d(4&42�Å�qH0 Ãáp(Fa L¢TÒ"_!ŽžhèÀðìDѤ‰×rôgÀ—ÂIUÿè‰;hÔ0Ï"”0ü»×#QF3wΛ�D5—ço a6ÁÄâ&9ñ’Å-—žnšœ¬)Ô¥©Z‰Ds®rÖjBÈ?#‰#°8®ë…¨ePÁØÄ›ìÏ!AÝ)š…©!þ²(”ùÄs…Ð)4×ï5ñ·–þuâIô›`_õÙ#±3 ŠœøìèVoýzÓ¬LT÷o ^c¢^·aç/ý÷@E5÷¿½ÿµ¬þ@ýáÿ($EX�Rÿ}®˜â± ?Ï 1wâðîL¼‚[:z}½fˆÉkÓž­J‹Î÷ÔZØ¢oÚ2À¯ÀP_æ¿ß:fÄÿ=Êe`Vàb!´÷Ýîeùo &¿¡ÍϦÎ(×ñþ[¾Ä4Ãô#¢¼H,ð`wTàu±Ý'ü›˜ M™ˆU)È>¬*'ÿê‘ÜÎÅîX!?·{! +ŽK0«L_ÓÔô¹vÏB„4—P$Ú ­¶JWÉ÷^`­Jù +®…•%§L�TDüÉY&B¦ÕFzŸAùrI¢w§¿ƒ‰#&yÅ6þÖs$½ìY“ìŽ÷׎�¤X6³ÁÑö-ÿSº´¤m»–OÊ!.˜tÇë,Ãî‰`˜é +‚ߊTVçÊûÛ¸ÄÞŸéj!|¸[¦ÄÐö;MEx©¤w¿Ý†�ùüœwÓT +D/B´Pà ÞóVDŽÒ¸©…SH«JˆÏ?•~«iÒ!ïâTàò6‘’pÌý!|[»‘¨\$8„oU²8ú�dÅÿ¯+¡§²"ñ–+ ÓvÀ*Wù­ O“€rÚZØË=„9÷h€Ü^m{‚9 pv\p‘FD™°Ú Š”tE³'r…nãCw”©”w7†Oä CBs¹¬ÔOÝêVŠ\™lÓml“^ºÁ錀ÓHÓ<}™ÛÛ¢Õ³}?Ü6_(ɸÿs,v[§s‹Oï‘J£k‘~iµ„3¤,UJNðÊæuÄ™Mjç/UðÅÒGÂ.Â�ÝvèÛà@£ïVµø2'5¿”^ŸE9šìß» +@qˆä��3˜ÅªÊØØ +µÀqG Õ1ÝE:žΔcK¿oîç Kê€Þä±SçLVß`0iRQôjuŒUÌqœ½du%®NXëIÊ-Áõzq-õùÅ«ÉñŽ´¼oû4AïØoï‹¥ ë}m0²‰Ç—1z „€l–ÚmY»¡Ѩ0ŒÁyÝ0,¹î ¾ðMë4Ù˜#~ÈÛ¶o°¯?ͪâ豬WöêvؾÿÁe²zãµáÑD]2}/ +‡¦’÷\Mß éu‹¾�šDdáÑLôqÐ}÷Âáé +<<YACå +·„ï¯/+Ö>Ñ ?œ·4OÉ�-u·cp½qav›pÅ„ ?7P)tG ^”‹6¥®ì‘¢�ð¡ñT0É;Ôz.Ѳ‹� q¡»Ë‡KaƒbëÏŸ&?Ì[¥ò~²°çbݯR•Û}Pœäîh¿~¶öNâÙN$RGwºËÜëâ=Êk×{N鬌°ý¹­cwr7~ªðLqý‚”wÜåÜf7rR?»pdDà^2ÿË +Ú’¤Z‡«:úàý—Jé¶Ä°3�\ç§ÄhâÉ…«"ªÊ5øÚ&ï®´¢Ä´ŸÛAh9Ì:ˆòsʛѣ|HYÕTöYe.Z6½Bph0-~ìèÇ~ÛÙûª£¦¤þƒ¡Ò5W††²þ ÛW1³*³ÔvÖ¬�ºÕ6Þ¢d·>%wŒìäA•_vìð̓+wªÒVóK q�ØôË�Q„ƒµú®sjߌb ‡0¢Ç8©:ãÖ >'»}Gâ­¸4�”�®ËU[šáB&’äºÈÁl{QCÙ±*9-+UyØ5â …SŽþýÛÊ Ÿî].8 ¦©ŠÉâY:"íŸýÞd[GƒBþkƒ¶åçã_-<•ã{´ ³x•cT¥Ý�½N“"Z¿xð).�©HªNÝúÖä(’«²:h^Cp¬åEÖÕá¡B`9”™ðXØ“µHƒ®YñÂsRŠ+¦�;œ®R)Û¾ó;šHæóñÅÆ‚09ÄqÅF˜¸“Ó87LÙ¤»ÔŒNé°XÊdY±´ë×§–m ¦?�^(=u, ;ñšJ?ÐY›�Ïí�XO¦}$UÈ‚Öü²ÌP”—Ùùc„”@@øh|ùãBÚ žàÒXIï˜�5‹]+�C¬z@è^¾m¶¬�˜²ûµÉÞŠ>ا_‰Äm‘»¯dÔ´–ÛeõzuLYû$�•5Sto[ÞŠ4g˜–ë¦ÝÆþ¯k¦¤Î¿½Qª>öx@Ñ^~„Hè'ïÃåKÜ‘.¥ÕWm£˜UY_:%¯MnÿXppÝ�“ÊýG¦àõ(�=Ô¢Mž¦Òj!;†ûOX׳g–°Æ�ëG{úˆøHW—ýôb}â·XÍtÕŽMŽ'0á«òبyĦž–…‡Lk�p–6ÌØfºj5·ÊŽÂ£:aùz\c…:EHïÿÙ´!ž¬e„·ì‰8A1 {ÿB¨ÄÅùé xþåǹi)=ã“ÍÄ«ï/Äb�Üu©Óc3׸»'tmz̓¸n0pŽïþ�m»ôԯѿåô�`…ú ÙêùgÆ„�õ�¥j_H@�Q}øðI0)iÛBGCj¥5½ºàGS ö,§ßd…}Í­Dÿü¶ŠÒî¬ULáthon ›KÐoïzÕ¿ÇkoÝBUØá¯öóè ©í>×x�•0¡í÷ų4>º[37[é£F;꘶´´½jN“Õµ\9Õ»Xá{g®µ )A¦·�o¼9€FÓ7æ~»,~€Â€ÙÚÞÌ´Ý[ô$\_ÀBú6WÔ£�sÔî;¬Ÿy  p -Ì/¢6£4¦£ª?øFnp¨‹khÖ(ÞLn¸ÊÏ¿I4´(P¢“m h™ÎRtÄ+·‚ùª–œɼH�I&÷yŠΈü­F´bpŒà`—ýµk,LÓ—1Ã’Å×8L¼ÒÞEš~k}ìÌÅsô\Šÿ„%\’™*±à¸@ŒÂÂK.¾W|4�ÆDÁp›�ˆS¼ôçÚ'I�ø?ç÷¥¯_N#�³¯h//}f +ø\0&œÛ­/™~ù6:N/ýåLG3 Ò,?5flÔò˜¢ý¯¼AÌÅÃñGŽ­Ôè5Kýej¶0çr”Y¥ÒdQ[Np¤)¤¨6xñNpüO5´¬žîtÉqWÎN¬°÷ �W}Œ»kªö¾ù„Á_h@ž[÷pk¬Ž(²ð¾%.ñ¿&Ì©áüôÆðÈV.™/^·eÜäM–·+ ëÇ>+îc‹Ö*Ê9å!ÁÑ´õzô—ró÷ kˆ™æüæÃªü£&3Нÿ×ð  {/D´äÒÕ“F€¯%?Q†{ølD“=K>Õ +à/\aÅ›ä· �–ü<8 ù‰t‘® ±Gà:‰€?¸ÇÑ)Fnr|\ZÛ«ÕrÏb+yípFÄ=c÷ Y”�3dè À·ÏXI öñr,Þü£¿B7g+7ÐÛ“á†|3Šn´ùèËÆ'ÓV�¬Iùæoy¶,zö ô4w²yÍjk9ÉŽ8.pxWÆÈ.ºéë$êCÈvY Šn†™Õ’òÌÿo×#›ròÌÍB*ölLŠ“Ç‹—�ã�û}èKb˜ÆAœÚrH껕×?p~"Àz�IÿZÕÐÌ¢!Ðá;ôUth4%»�¤Êÿ¸·�ø¿Ô$­SÚJ~¦÷Ö¢ÌÌ$»ì çðw®ìDˆÆOêã&«("†ÀQƒ¯ Šóºb„0Îq†ÀµòøD‰Ü†@•õØKœ´¹A”èñ”ì€ÇWů޵ôâ|'òíÔ,�Uqþ„²+ç³ÁŒâLÊæÕ*$KMq>¥Ñë,‰%à~¯ùZy4Zd] ¸Töbª–”ŸGCˆŠÕ¨Žc«\`GS¦xlU�Û'Öv4ǧÒùÑSð3³âëQŽ¥Æ]è�g'†ÕEá‹ÀÐËê„ mA;mƒÿ÷Óª!N«’æG(°I¬“™s°ˆ¤¹}£\‚¼ ŽqršÛ«C ÿð:c‡Kš¹Ý¸?inñ +úDfÄr'žin湃 +E³š�Ï’,-H—#“‹áç³3 ¡õä²òAÁJi«°`V„úréÒ@N¿Lêå4Ä% ì\‹r¨[`\sÃ-ñª’ŠÂPjhÀ•€6{åNûFv›ãH·õ¡ yM8_ðHL0"F 9…Ï¥—ÜÝdÿë˜àâ3MÀV­£n²<‘‰v†ªŒO…ÞÝ�1­„ØÖ‘$E +¿ü@”ÐçÇò’¢X,+ê`†íšŠ‰TÔÔ[\@õ–³fôÆ/²ÿS%�œêÌ~2ð/ní«:¥2®á¥ÿœ)5d?ù.O~P4òÀlmðyfzªg}~¬€HøÀ¿‹O‹º¸~ÆgF½\I¼f~anÔW‹!FÅλ¤Ê°ÎÒ²?×-MÊY@…¤*+íà"UUê´#®…ë-8eKú -C&²ä -ŒÇ§®n‹íÀîÕ©Ã�Áw¯�MÝʬȡ¹ þJ‰¡¢¿#n•ÜM˜ÒGŸ¶LN.ol.£D{›Ï„: BDI’ïôi«BŒ„`à0p±Æšj @¶6mo¨EÆUÏ\€”;Ì„b9ú? -‡k"Ö$dðËŽ�' -)€œÓjú·Œ®ƒ'C$}�M-H–3¡tÅ¢ÉZpþPë¨y—c€ßœa0-…•°e4Ú  -¨oÂTž är¬–ð‘ƒr­DiÁ -*ðúyЊ& ^‘ÔU[Ù¿oWŒL_qŤ1ýغ˜‚ûNÜ_ÅiÑ$—€¦~�éÂ¥Å%[ òËl�éñ>E³bH"s*éóAÀ¬1–å¨Júcq²x£¡m¿ýÏèù6—JÙ÷ºÆ.~?XQ™8Óe~äïôR _J&œQú ß’B -«0vÚs_ä240q´æ¾63)GŸ©P»=JÏ]oßÝSžîV3âë£sC‹×) -æÒÑ“t9 H.m©ð [!0FŒ8-C2Xl¬*ÒÀõ(9ÖnmDÙèÖö´L5~û°�‡ÅJÆ+±ÞhØ*¿kŒ^èÜÖ_Y$åž?Á#M)yLJè¯ .,É6U¾œ}`ñ@ 1(¶Šfá™(Ëø;:‡Ÿ‰NŠ- ÇÞ^CÄü æ:ÚÄ\?g†z�“­';=ø${×Û}-ÉE&�ÓƒféÕp°[´aYÌ1‡ø‡/vxné!}ˆÀš¶²³"5Úf[‰éílâ+¼^¢u]‹ÁÉè–OÈ?«—¢À!í“Eö‰¡ó[Ó›ÊR4žñÊ-ibOÐJÈØÞ^a…ÑÇ�%¥®î7 ¯ k= uE@Ï™yjL”²RÀDÏ¿Pë›¶ùk'\m%žÄÊ(èèþ£òŒÝHª$ĨŠ0«ƒlcTפ­ËÑ]È;Ñ kSÎP'.·¶½ÚS03#>RÚ$ÔÔú#5Òkþ\Û$ž s-ß¼5Üœ›óS%ãÛØ|¢Í;ýCï6$Ç¡6ÇU•‡,¶3æÄf-‡œAÞ·m=輡#ªÍpö‰ã*kkCϺÖä0 -5¤{¨AšrxC?OÈYf~ Êãð¥C)¼ãâW›-Ö¬ -‚©/¶àÙgù*,Æ1AA«à¯ä�æÁO÷«¨Bdç¤A¢—K4EÏ'²UÅí­†SVa -[û(‡Xæf�["ðšëÚ¨ÈÑ„ž®&έ>“gΤ† læOùI©»ò XDe�glI<^/¤I)õA}˜3(›µDˆ|y˜Tè²?P^‚‡—î�¬pyÆE£#Y)Tºƒj§ÔeLêUOsñôÀÓl´©Æì‡4‘þR`1t›åTøtÿáãEœE~öîX6'M ٰخ‘«Î÷‹4ˆm×–®T#TÊ�ŒxúaκÅ|2.¨k+]öt9snAf$¯0[p …éX¾£‡QÝÅ€�yüe>žfh†âŒ 2bBAÙ¹ ‡ô‚õ¥ã3€k6w ‰9™GáI{þ[¿À2V#LìÏ Õ¥\phÖºèC5ÇíÅ[ð*58Þ*O£EäMb¬€;|‹Pb±"7DU‹™ =¨�N“©D³Gç ´ l©0k˜ési*xX² Txx�U¬2®"Ä“ùžu2º¶¡æãÆ�xÞÁ~€Q56e&ˆ_Ššá4 T`nƒêFÅxRÎQ²¢È¬‚sY!àzb’?:ý.øN*{�ØeÄhM;)~+1ÕI'|iOb(,^�2ñeº%ÏYSeJ’…çøÒCXváñZU?�àÀÁ&ΓS*³˜îbªÀð)�Ki˜¯mñÓÒo-±‡WФ¹¢©ŒÆY]á·ÿ¿2¿Ýö¶¼Šö4+7#‹D: Û\sÂʪR¢ÌíNÁ¥ÂPÉœ%8U¾ M0‰Õ:óK%ƒ³[�­qö‰R¥]68éÄÖ~¼—Ñ;Àï|åUýãlqÁ�þQyAÈ_@­Þ– kíK+B¶Uˈ´† G÷¬ÊagmºÔ‚®Ð­'‰Y*kÆáú¤'Hl°ör¤Èg‰1epzÙ~—TgÊ�m®òj6íèLÊ€ƒÓ¥\·¸qö°§tôwRi bóX G`ÞÀ£] ·]Oý·g=ûx"ŽÅû—ñ£¹‚‚õûàâ +°;€Hi¿:;%-ØvÜ=–èÔ|;ÞV•@שFÞ}ü ,XðÍ-@“ØPJ,/ì s3èk´“.(&Ib¡‡ZÎÒÍ^qŽnN-–Rÿ/â§€Ëã;»~ÆY®Î…Ao™6´ü¢‚ˆäpóƒsÐDvˆúø)ëµ­w íiclÃXªì�h5§¸Ô�c¦|1ñ�¡c¥¡èIžV�%‡c!ExEÁ„�a #‚éC Qý’‚Ò5t�ñÇég1�bµyBßi�µ ý>€{³î:e½˜Ug8\¥Ñ5‰kÚ -û§Ñ@ôËv]óÂ(�èr<%êqF7ùegg¼“ -ë…^"ª¹…�QC0ò,C¾‹¢�\]N®Šà ãjuR]ýÄÖIÄG¹R™™‘éKÖå½ÊÒÝÀgT(gðv¹�懔7Pëh& â¢ÂH��h¾ÙK-ô»ÊË=Zé˜p1¦«+¿ D�äêcFôš},PÝ -«Øð)çŠC‚´`<+b^ðÆ»ý,¹8„€uØ&�°TY™­1K@B±³/[#ëiÂ)ìhø_Ÿµ„ûõŽyðeD°ø<{BVS£¹a³PŒÔå Õ6ƒébjþL‹ù°\CDe D„ŒflO¶LEk6ì)SR1v -ꢟ1À{‚ -гҖýðƒ[̼‹pÃ’6Ð_mds KóX½28�÷$3=¾K!v¡‘˜�ex]Ž'ÝTÉ…YU€“Úé¿x‘¼yƒÐp‘± o£3ZpL"§–½�e�#•Ø -e±S p™,Cñˆ£€Ã3g‚—t¥|sªQ¦F¤g¯|�ö¿§g9Ð3ªD€GÙi‹˜9§ ËñËk<3_­8Æ=Y§ýeõ «=¤YEå¨!âq–Q€ä~#w±û³`ïíŸf¤<&Åà¹ÅÁnMÑ F¥£ƒfØô³·èm:ïaËmv™ -�’­+Xà?ñAa]‰<½Ö0¦¥‹³r\SÕh~„ -Ý[é¢Ö´O‚–ô Êš\„ñ­‚é~U“Äìï·B_*zÚ`ë²ìý�q¨N[/)Ÿ5tÒ�tÕv9l$1l64Uý�ÿ3Œ+Ž^s~´X�Ko¼|^à;Ž>Ü�‘lr›h¡ OëUªNý!u\ÁKçIá\_c+Wsôþéå2�Ò¸ÉÕC¦GOb,¿ÁI�š¤r¯ÞÂ�ŬùHרjË9ùñ�S=× RÃ3yöu°9&ˆHAó[Ù›D÷ü_à«ß?ŒäüÛl##Wi$º€9ùŠ»cº0¨QG˱Ë�Ë×hdŸ%'wO‰1ËïK˜Œ52#§�{Žîzp½h<ýuͪ¤1NÉÐRPX)Qœí?óÚ ó\ƾ”³Š�ÓNð¸ØÍ�Mn�Ž'›†¨ò@{aø±®¯ëŒËÕ¨Â3ó–¶T_êñ÷?än…å xD/ .½@ËÿÂpT?Ó—2–æ-žJ‚â€4ðÒ08_!]_!URùCþß·-l‰:-HßHÉî­Þ“àù‚Et(æˆUY¸êô I!Â@qVÞ"JÄ!Òºéô³XîmœüO‡R Ç^ÂZ(ó Þ6·¢6@T8¦ÎXÑ®ÁƒsvVy[b$Ú¥ª\õÁCRU4ªâ¢ˆ.I4i0xNÐÛ4ƒ\^£š…QŸèuå¯SY9•¾0PÔ£èm¬ fážAÝQP,\àTz›É  -Uã!` -Dp?‚غéY&ê‚BÓÞ6C½FÉö£ö‰¤pÓ•Q{ÛÈ®©è=#?¦ª©:½ð⇔àʨ½-ý¬/TŽ|£T´©~Ðt­QO…‡WóÝÖéö+cSûº,të Þ )–¨6EË è9µL½Í;>�r13�üUKš®5àÈ ‹Æ34G†sÆTÖÑÉú¹ -Z9ãwaÈzåÓBÉA´@C"ÚÁx&*ZPËò³Õ‘”4Ì 9Ì'iË@ˆD -®Ÿ%/|rð¾ÁAÉIëÈðJ-S@Ä ÅrÆ<Á„ ”¨¡d’Y„<§EAÉ@‰ê9LeM‹€Z«¸°y—‚Ï;–gÂ�t0ÞQ,8£È$3'ÆHôðFä™D@©ÛTÖ‚–@Ö¥·}igÆ Ñ[°…ª¨ðE•† -´EdtÉC&QÔÆÆ£r@bõ8}J©YE­kÖ£8SÍ”`Z,÷\QêIa—õQ—ò¨æ�% ’HçE½í0�¤%]?/!À�Ù¯œPÔçç‰ÃÔ6°³µS)¶HB>>¤ã)ý9iË$à¸G>º$ ¸h™'èm!÷_²ë«&ãrICèÈ9¸Íà‹z¿ sÈÇÿ}µôúŠÛøÕð¸�¹�ýšþŽ3ûË�ež_¾7_@º ´,sÅm’Ùí̊ůy·®Ð€–V?üÊJs‡ìúÕp³øâížÅoj[TÏÇ=Þ¹M+¾‹xÉî -»ueÚ…övÅJÙE¿q9–7å {íø¿.¯XNav|©SË/@~E -Ó ¥×7ói2-sɱ¼é²‹þXK»ÄdÕû–=¯]Õ÷-°i”&Ó–Ýnè¿mìÒn™èÛ-ýã¿öù¥]õWÅx€Ï⛫_˜Ìýúæà6Vã W·œÌuלë…ç¹åì[@ síËq²Í½þ�¹[-sáËÅo‹Å¯ý¾>Ë\ÜÂ3wËøu=âD‹ê‰[®n¹¿0.n¹Xü -hk.þ²\Íq_fsŸn¹_™è+ü—äþ’]·ŠèÕôyn½ÆÛ4Úi/-]~¬õÌèvû –Eõô~A9‡údX|qe gÇ/ ·]tå–]¢+üXÛÊ¿¦¿€O‹®<0‹[Ø�ßY-ºòçâ—ãä˜kp(gÇïCtå�Ú%«¿¾ELî†e׋n_qk‡èoÇ/ ÿ—L{Í3l×ó Û5=Ï/‘žç®¬¥½"=Ͼ± çùsôwßO‹8™‹¾U‘žç2‘žg‘žç8öÖÿ%ºò€ë[šýºu+ºò€+а<×" Ûñi9´,›_£åýá.xæ±ËUï-¾h—L€8·0ýÈâ‹v‰3ZŒÛ¯íÊ?~a؉œv‰3ZL†Åï�µ3ìÃoìÓüõ?ìÒâ<ݺߎ_‘暀æ²íz¦¿Ì–é˜8`¶¯·â<€ £gøeÓ_@,»w-Ú1l~a—8p¿ì_Ó" &‹3Zlþµ«Ã³\‹ó°XÏä,Ò\¢O(�®H+޲‚êOÿW+«Åz†Ý6þ"ÍE·ñëŠõÌã¸ýd×,‹ó®ø¦Á/ÑŽaýÛô‹nc?®l·0L¬çqTp‘Šv¿¯¿A}”£pc¨Ã…<•^·/Œr¯£Üå n€Ï/œr”óvÝjFîa»^µ¶ßWŦ[Z¨Ã}û••¸ùq‹XvýP_,fYÔXÈoìÓÜži²‹›_Ø·.' AÓ^»…?ÈLp¼¶]£´<Pm׫~ÜC~íTÛ±l±íÞ¡^Ó¯e€ÿŠ/fc‹_YýÆ-‰ë £¶ã—žé/@žaø§ŠN2¸N– –·…vȆ} g[T±ø® Pv£Ü§?lÛïõÉê¿1;gß³}w¸Æåä,{¸kïXÜj8þ¾†€ýÂíýábñË5.¨ÌõÊJößæ°¯a4fûÂ-÷Úq«èl,à¥eÖ´a_Ó½Âןžir -Ã?%kîÇ/lß2GÇûwÅâwÀIå‚a–”×ÿ0å–Sþé�`_#‚} èÛT&_) Å“p,F¿«µØ°¯);´,Ã;÷å‚ÎD.–ý{½ôÚË’ÁõFg¼v;ÓäÔ¢z~æÃ/L¦=¯ïÒ*5ýíOkéÖsAg¶<‡X¾Û®Wý°ÛŠÔ ýíøRZË�[—¬qɪݲð}ÜÃ~á×mí¾ˆûè_ÚóÖýçvæÔåºI𦢿^/ãàŠ?ÝÎuÜÒ6׸ 4ç9®å”áKo²¼-Œqö-Ï\r<û°Ë}»…åúå¸Þ/èÀK»nAœlß(wA?îQ¿p+¾ókÁòÚ™dy°ì*my®guKk?�Î2�¾~}á‹Îð¥gyÜ—¹P™ëýõ°¼X~í·–=Çò6�,‚?H½ŒF/º•]›Fy‚sþÍèv3Ë"^Ã÷üApëÊðjÊb/ˆdþæõ?Lá,o 㤷ÉlHô�ü¾R xd mÆCùþþ$t4(dȲŒ¼vO‚Å‹)Œ›b -ãb -ó¶Ó(¦°„˜Â¸ø9ÄÏA³L&ÍZùç8´æÎGQMUÔT¥§ŸOói>‘LaÍCÔ °Ñƒ³ú�)â�"z:«I™´Ê¤udBÌêϳúóñì4ý‹6àÈÉ«ýä�Õ~Nó¬g(dò¶�Il±<… ·ECÔµ�$1_¦K0µL’�é ‰¡‰¥ì2pØ|’ËÀžx›­~’ÚQŸ¶¼©O›Ç¡õYocclŒm­|Ë÷•o f&Š—™(~c'Å{šfÌNðCÒ ¿¾VAcvÞŒõµŽ"?3f5ïnm�îiikÞ�{¥¢†•Ue�ð¶ -)UéPžNpMœN°RY(¢?Ÿy¨ˆ›…°<'¨Ê›PE}·åCS(¢SdfÇTYÕbÚ½¢½ÒËh|Æfô¶™*J¨&Ý@Ñ�£š6h,±ƒÈß7�ZáZÖÁiåi Â@Áá ÷‘ÖMŸ%�P;IЗ -…WˆÂaÔ¯Sébaza q`Þ,‚ZàT¢ày4 a>1bAi8¼ ¤½^ÓBCCŠ%�v)H ;Ëì;oda'�…Ý©�Itéì»J“èV$šè*+Rر>q¢‹Õ¾x0œÒMTo ©/ªž »®@£wAeÝó µ ›è*¾ŽÏ j0d" ·}X&ŠOÇÚÕdº<™N¦“ 7%ìb -«ÝÚ¥Yë)~§YÞf"Ì"ŸzÛ@1x7bð)Kº'èm&šeJx[©–ª¥j©Zª4pm7 ÚžŽ‚«¥j©õI¤TªÒSÒ× °šÆçÓJ2�‡Ö<2}:Þ&“d’L’I#SçAýÖ¤‰Y-“dÒz‚qñ>çàóí|þý�Ï>ͳÒÉeàP«Užj/;V«|Š2¼8­~PX‚îz‚,ËDñÞ¶uoc™(~±ÚÏ Û6Q›�uû?÷ËDñ•Öðâl,$}ç$RÄà) ©1;Á¨³Ö2e"øâf­÷*bð6cvzËÑ×—g´F@Y6z›ßHwÙŸO¼�f­'h X‚¶S@m¡[ÏÐ1xÇÛ»ÞfWA‹iP|Í1n"%œÃ“ ÃxeR¿l,̧õÜjªÒ ™•œCÕT8´6‡$q"3”[å+«/ÜD Þp³.�Ù V -¢çsoƒW: ¨vP}ö°�¾Õyz•ód „¶{›ŠZç1P¿lôÔ/)µL}m‚"º·‰^ß|zž‰á“Ð\𜈛År¯Ðb¹g´6În=Aµ–)EôOŠH¤a%<Áφ •#¿p‰Á§¦µq|yŠTlÙzê@´8OÉ›#ˆ;z"Oð–Q¥BÊp SIÓÞÖnÞŽÇ2Ÿ•+«€uj©œÒ(<�Œ…·AímªC £$ßAf’taa¼éaB塃ÀkLt/†�b$0X¹·‘³EP§NªhA(¼mƒàÞ­›†À4¤h:”4}›"#7ÄŒÍxB¥ŽÓÛXF=ŠrpzN¤A�ðD ^@ƒ$F] ˆ~p#ï80 Z,W§Á¿,)“%5Øãij‚õ.t †(JÔæÔf¤¤}¦Š"ìeã²�ž`ãž“å˜,­ÉN¼ ZqÈ›Ñ~ÑâÖhòñ€@¨·�6µÊk|êi±|€À6Îù�væ¨T]Mc¡4=Ò(ŸÎ…â#4-~ßgB~Xš–pº�ôú”<ÁeÀ SqØïl Çô-‹Pñ¦ÖÛH5 -¡ò¨N‚ãÏr�ÆñcMÒL`…UÈ@œÄ�øÂ‹_>¶IPhp܃rÍ5€h4# )x”kЇä yiÚÛnKáÅ -šE¢�Z7=b"ìJŒÃVyCÇ‚v…·…•OÒ�S5]O -/nQ R�¨±L¬¦Eö; ~à Þ&3�„hё‹³h÷êðt`‰*Ý´¤ƒxÛj Z ç”:–¡H TÒEчIG©}XÜÛ,¦è¡}D[…€¨¦�X‡#¨½í ê¨x™*Ù(‹…p%%´UZ‘¼í ™�3‰gàu\Gr×�¸·1^n,t4 “�ÄS -†Mq¼í!a…ìð¿2©D¦þŽ? Ð”5› /�Ø(>ƒÙ¢®±‚aVP>(-³T¥Ã+i¦ )Š/q:Q>/ -/¾é& ÂòA„Ê‘®ßA�°5•>éx)ÛÌÿ [Mž<œœLAŸã¢8"nýµ  s b�_](›^ÆÍ�‘J^T3çÆËz-1„«^?«â¡-ô1*G ¥žrÕoÅ£?Ò­ mŽ‹0kÐŽÖ'‹wjÎmß^äÊ +ŒÁù8‘uÄ<»‚é³�ǸÖušÏШ&@ý>i¹�HÑòU¿5¬o’¨ß‘¼X/»†­Ýoh—ÆÄ¸Ó¦×í÷ƒ8v/êrø~CCeˆ�ðwšYÏÛƒÐl쌄±<‹|—04V4�íD“R÷åÀ-qYùç)–‰Sk=o^Y®›Fy^,Þ£¸Òk v<¤¼‘—%ÀÀ¿†/lÏ %ÒuTÏÃÙ& N1%·©ò¼(ö~Êõ¼šEëÝ ‚9E4˸Ôó‚%Ô/¡g–ç!ìRÕé† åy©.Áû‘J�8fjJ­£äbàÏK�G³¯ŠÙ8¶8ÿ xå½ÀôP§Ñ¨c86eë=@’ߥ»yxÅ@¨Tüªó¶“ ³¿k1 w‘[õîÅÅ€®hìv,& Ò§½ Ø8é5¼C•ba'uŽ>Œ¬o£¾[d4‘¶(ÕF£˜lH¶”# ;³wlG¾è·ô’ÖÈûÑ`èÂ_ò ÷°QÒpþ€%wû¶qf¢ -~×ÎS‹£‹ÆdL\Æ 3 /2pwyCä¢ëü�y@hó‘XO_åÂi€�ÿª(/ ¢Î°zßFšÿ®EæNšÕ €ä¥"|-OiְЦ½l5èê:NÒ©ª9>ü•¥ò•Œÿ ’Ú‹¢¹`z¯J7ŸÃÚ°}Váõõ�6‹\Û×ÊÓ¢úö#3hßk3;,ñóø¤TÓ{%áp…Ù�cÜôîÂÈGÒ·£¢�mèsšœü%&¼•—Ð(-¸$>÷_S›CÜö\ +ÜtG.ÁŃ<¤—»" ­ÝixÕ[÷î2Íóœë¤ ©|�Çj7e7 ‰@oÝLÝÄ›¥æ®ú%ˆÍ'ýí§s5ÜõEÓT. /ƒ¿qK‹ÃûØ+žº/ZwWÈþðõýä€çMí­G.-�ù y ܇þíD¨žtI!°�:ßfIÆ€ìÔ÷7Ì&üìYFø¡JÛ�.bsŒ aH.?»„Ù¥ùd¦æ0ž©’”�ÈÛ\^Y´ùkŸúÂx-Z3þÚ‡«G—`€íÄí×Þ:e «Ö:ŽÄ]˜9϶’¹=ò&Îë½^ ËJ/Ð +\‘X€‹æmÀ³úµ ¸¦ 26é]¸¸�£q–S¿HN�1XçÎ|êǪ´X„ê�ÊÖåªç_� µ­KN ênt~Â@-…ü~ÆtP#ÚÙ®Wúw‰]åQ`3ðÁüäÅÇ;Ÿöõ“ÃÁ¢Ô.#Â…CfCp.´�[!Gçrúµ€¼8ß.NÜOÎÐë“p.Tï4^ዹÀßjHôGI +È®ôAEä^ù)‚„ ¢ ÅžÏW(0²·ÒǸC�Àv1``›‡M­ê ãò7Æ�¼§´ýx`€4+'ÞXÛÌý"’xî#Šñ~jÐNð?æþ£W(|îyJó¥1ûå¢} +sYbZÔ¥.“/#2ì·Ÿu£~GM0ìIæ4ëÞf­¸*9�cž\Stò�¤’üNLðQ6¤¶^8l¯_,Ü›/½rùî[XØ4�%ÉOž�>èÕ¸›g�…Q, ¢cdÐÏÑ,”—I8§I-;© JqÇ€)µ°ýˆpœEM Ñ +>ª^_éD™B3+ðÁN*ðh ¨°yŠxH6~„ËIús®{§J‘HpT& J?ª éM©­rÄϱ=„¹ì>í�§Ð=D<#55sž™§!臭ÛéµåÅŒj¿™IšÈ*ÔÉ…Šþº¤¾âÄöö€CåŸýÚ\ªôÿÈFª�ºÂ€"¿Œ “!öm™,a¾ß67GVeJèñº—&bâÚ`+{¼Â!W®ú±8'ÎF�þpÅ„ÆE\Hy_õˆã_¨ÅÛ ¤üí¦Râkƒ1LÇ3Ì“ò4¦ŒE¼Uñ�bŽŽ̸Ì)/ +a/*Ï&å‹ñŒã5Txk*?³b²½fÎu!BWyámrœÎa1©4ï+¼ÉÖy»?/4ºÏoöVºôPù›3ؿֻÄM|$í…7! Ô J&2± û˜í!U¥q©ˆ€ì¨QÍÍyVÄT÷Q3hÙZîq©aõñÁxèF„{ð�eá>¬xõètVÝÄw:äK»¶ƒ6jIea>¨¾aÙ¶÷VfIf¬\0EâqŽ@ÑðãØt¡�Ó‡ €�{ϥ݊jMäã)¡=úg´p΋þ—/°=¼Ó@5b2§³ê+HÉ1À£KAÇ@kU¥R§EÉŠÄõÝÍò ˆ”ë=@Ú„òSÙO4ö‰<�ÚLÑ¢A²<’ ñ�ô Ëy†`�$Çx±ìpUØ)@a…L 0,öõà­w®+d¨ùŽVyIG3nÂ/A¡ù1ƒ*SŸNÅ›�U×Ngnfø{M@i°D,·¡*éšá¹w†¯pJfx¶°¸Ò9‚Ò)PA׺n +’fhL½…†(®+á1~`xáÕuE>Ã,ÞN•r]ql`9^{w]‘×ð„ñ_]¡—¹®´ „ Ã6t]ùæø\]¹³º®´Ô*€á��V®+*.ŸVWœz×ʸ¾<naxÛî”ëʳ=Ôt_`x¤ וÊF+•'ž�ÌðOe"x»¿—S‰ëŠ ùп^øKîÃâíU-0p¬tØùáÇÞ�#àu4¢Ýš²¥L[Þùãç}/¦¡zR(»`—€²†‘ÆîÝÄ %—R-à\ôES}~p°WÓ‡—Þ¼ÛƒÐÝ4G”C}�8à–œµÁGm@E‹ø2ógÊ‘©ïCGYooŠsÀ€\GéDæÛÙ� œßÆqŒ°r;.?1¤ªM¹ñRÀÛ¦Õ·gÌÕ/Y&ӷ䤼Ç=mñÕÝhé¢].@ˆWÁÈRÜßÈ[G$ê¶.ÒH©±M¯M{L»äHÝ6}uOŸ•ñä@=é ¼‹{YAá«d_w0Œ¹gd›„cAaúʧé¥Oí�Ý8ô7‡·nÁ¬õæéÏ,ÂΡK Éf—0 aw5÷¦ ØÛq˜Öˆdxo-Ô[Ä vá±+¾öv]†á'd+M« ¼Þ|L¿§kÎ7ÀÙ�Þ½�ã,3óp]Š!Ó[Ûc%(�ü1\ÚüÄý>“oxÇßÙI �)(é +Ó=‹Æ¿ÔrÇoúTgú@fEN9¦_:Kñä+CûdØÀö½ü0„Ž×|(´ �Êj0¶EbRC }C»mç‘�À…�„±BáM‡6Ocu _äs*á­œ^»:V�E/�'ÈÁ•ÀêYvnf0ÛV/mž¾jàè‘£�ª²Õ³!å« ˆŸg°2ÛVúyéS^¿=c«O#š�°€´œ³mOHe;¦­8ÓT:=°½dST×´áÁ«uù¨öQñIbåv£°ìw~çÁÅ!G̽¨ ŽQ̃gQ›)RAްh×âúdn%žƒe.ù”þØ¿=|àÁ騥©¦ /N ©‚i9ˆ$ŒŒ5˜=B{ç¶ÆÕÆþ !ð�¿‹®€—5\ú�7^v:*©�O®�IÀ˲»ÆCkàÊÝ.€wu¼ %†u AÂËDÕú¤3·›�sq3ò;Ñì�ê_}~ÆÐ„asI¨Î>•B“Ø=®3ÔPÈïâ¶šu¡_(dØ“l&.�&P²ÅƒI¹Ñe®é�'YºfI€ã)â0H²Õ¬O‹=¯Ïµ¤óa&³÷r˜í,X$�’ùÜ'O²•;0[Aekíö#Q•.®—¿¹úzù_™Š¥SAà%À¶ôEHØ"+s�tätÌÔL± D©’J�©ò[sʬª^¾Œ�Z}Ö·1{çJ:¤z‹]P3få‰~ÈÊãK`¾¨cO•Ýã®w8öG¦¢³©� †<ŸG7�‘œUk\µâ.µý¦{‰_ò�ž²Q† NÇÉìM4 Is8`ì1.‡RÉvTYUÂ5ÎoÀ TÕñ¿ZúA©³èg¬,ù‹ô/rÐM¨»|ø‹Üwç½`Ç{ÕÍ¡ן™†…1¹Ø^<ã=ÖÚ.ĺ¯3žà“VɱșpWãÒð¶8iËšÎ83«�Sö¶^ÕjÚª ¢3žY.ôþ”½ ~ÆË®4‘Í%ÆŠ‡cÙ4,#hÓl¤ZtÍ% çú“!åŸóš…n"Äð?þ®üç®™“宿ÁH”ÝD¦á¦”*„¶Á|¸Dí(*‚¬:ÔÍ1…ˆÀæçR_=N¤û�ã aò.n€CLJÀ‚@¶‚µÌ*©´”·‰¤¿ÔÙ–LwºFüÖ�yf^oX¯n¢u~¨{8ÑJÖɤTÌS"uù[N)•0Rƒ?™xoë?_IôC�z5 Ú ŸkϨ\€Î¦að„5KÞ¹«Y®Z)ož?¤05]4Ü‹dˆ!¦eðÏ›¤õ‹TÜ +u�6'#•~[>ûZqûKÃsgŸð :‡êmøðý¨YÜYk+IaME/c‡Czwôш‡“iN#–ú»`³‚ÉO΂UNž¼¶‚AÆ“u(ëÐ$y ƒ�)3¬T|6ëÐ锌ÓÁO¨)T~y0™) lP‹�L†š˜TÙ`ã�ÕÿY0„P‘A[Á¬*™ é/â_«­Lü3ϨÂ}Š;a¶‚ùB3 ÑäX==×u?õn½Z·J~—ìE…qÊf]äˆm'\g�—…í¨B£�`7ßÉwÚvVª +V3Ëà“ù§µóqN¶ïº˜t>EÆ 8·ý½k4œF+»tí5åx¢êV:Em"ZˆÙd&„½­¥=˜|fûšQ¥�‹ï…wrE&ÿ†Î•ÓTW©PòIïA„ÐgÆDIƒ†I˜‰ê•ˆE +zºãù_ú>•zÚÏ¥pC#ÁHàg�”� +oŽnšˆ0h?×~}ëúÚµˆ¿¸£0™dÃ{"‚¸‘9% GÿlL<›”+È.xÚÛ¤ôiÙ@óRa}¶X<6‡À Ü ×ãm'ç]ÿ ò àŽó^ˆ=Þ•F÷_9/ÐyßШ¿°�µOóxo²Ö‡¯àƲ»\9ÍcìÅñ1hÇj¤°ÂMù�+`gß³Œ´Æ1À˜ÄÄ\¦æ¢àkV¿À3‹fì4·¥J°ÆÁ*ˆäsªò<¬’V„Jc9Û»FŽþ+º&³/J§„gþ/‹×`¢”!§9OÉrì4ÿs-Öƒu‰#˜Ø÷`8râöô}c–=F �%yàbË`9Õ’Â*¬ÎŸ…ÐpZV ´ks“iøC‘Ó—Lc81^Æ|À*§nhPðB‘½ÐÒ¿1©`�Pp´úZ9 Ó +©Ô*EC;9ö^í&\×PŸÐ¡�dCkÈ5�\i(:}Y0³/ÜT¹Ð<Üú®¾\ú»éwj*ÂI)N JâëˆÒé¥ +€S“ÂÚ)—Ý«rÛŠìL#ÕMÆm–hPºGrÒÿLfÉ®xžb³q©¡oý0i»“J~ÔIðÓmœÝcG±GÑ=xƒTŒj•<É—ºÊ{¹Qßåóná`Éœé¨Y$i±ŠOÿþÝÎùrƒÇ—tIü^Êã‚!sF|ü^Ûü†+ªú�¯àˆZÊñ{�À$ŠÈv/A_Çï¥lh૪ü?*~¯é+xæ¿Ý@Ä_FÆŒ`ð{õªÒïÆ¿âÀc„×€]ü^{ø]ø½¨gŘÁÚÍø½·èÞÀ— èÀŽß«<4ðy$£Gx…&ß+ðU—ø½xÌ®Ì #¼,ù�hçÓ1¤|õø½ +¾B»ÃÆhƒö<‹û+ ŒÃ�=àµ/i1 +Ô7û7�ªS¢Äì90fÎR‚8)yn&ŽÇÑR¼E’pI +k.!ŽNT–0I#d¥à†€–—œÓ%©¥]È I¿~Œ +-èƒ3g‚4@Bq;Pw~–š,WxP›ø£Ýj¨Äµ*K3Èø¥–.bÎyiPÀ»7<^nE;%KMjLùÉ2 Î,õ\Åýwštâ™k„—Z¾§`©Ÿ©Œ5¶Ÿ¥v%ªDN;"z–Zð’±sœ™ öR”‰N�N6�âYÓ,~óat Ò>ÂøD$ÇCmózö5¿~ Dö[ø±=:;4ua#2hüL�»GÕ�kk8fûR'Ï’¸gÆ}uMš/j”÷/XoT<Eð’/îùAº­}1…¸a¡ H=¹;•È?sÔÁÍÜ ÅTýý߯²nYÓ_ôŒ ¯S�(Úâ!©ŠÉ¼3›{E’­Éˆ™ ‘&`‚þÓ¿Pà2µ1U麉Êг›N›|Î55lQMV>Úd BN#Bʉ.Ò‘—Ø�xòêÑ*< ôÉîa4ÐY�‚žmˆ—‰¾Ãõø{ºö¯û,|Ë­VÜ@S¸âyOfCœ!Î'ž¥Ø™ä&D)˜ò# 7×cÈÎéQ¨@rƒåÉžÎz!Ébx¯m$Ò!Р 4AäÎ “¶i9òm2SãÕ•ÿ1Ùi_ù”:¦~'Ã@7ÞšÛ›’Vhj„‹)Ÿù=û1u©¼””'í*IÉŒS/ú©Î¿ŒYãSTÕÖ}œnLSŸ” +EÅÔý6Ð)i 4<=Âk+b©¤©µ5œÈ´y`L „ = ¼ôH>Ê 96ˆ”Áhêä¸Ý`0<ÇÔ/â�`¨Œ˜"\£4ü±}Í»¡†RÞÀ »d5ì÷0¦ ¹ª-SÉ,¼Îï ­Ö´É^,uZV|ïÿ@•_±x*à>,%8^ÿhÀÁšƒ2nWy’Gm½ÊÉM-ŽáîC3U;¥%g×£¼eþÍ£'T*ù¹&ã>Œ +“D­/&ã4FbuË©�ŵ ºqXÕÆ%´�u8 ºï»wñúLVØÁ |òðF�yuqJw�¹íøŸ³fš‚�1Óô2Ir0Il›™ ‡ñOI�a�x›Q6»œÍ²÷oy)^ÖÜGh×&”‰³cÖ8ÀJlÑ)°Rî…3ö¿á`Ïf7b3œ½õ´áÄËúuŠ�MÓuÉ<ð7í5Èí5X4c(·«;}šSWGqõÓ-Ž‹Ý%Sµúäun{Íe¼ +ãH!Ž”d*ô“õȬ�™õ¤ŒŠüt¼Jõö3¿É€qíċʩ†—k³ê™¨€ÁŸœ]š�µž®ÏˆFùŸ©@l›Þ1 P„ž%+Ü‘V×íÆJ`šp¬/š¨Äeš“ †ý`÷M.Öú2AÀ”:틈•S\ÜaœóÀA]ƒÅôoš#9’užJNˆFî +¼M¯ òþ0š°–±²U¤.÷áÉå@ýň,? ·|ˆ˜Óò³ðÄL\šmIò÷%P(Ê´*v ·µFm-¶ªÊgj,ÍŽ£Ö¸"5oÔÎ*œq^žA@0^†|šF•é³WHèÚLc\k&„‚¬L'„ĬÅÖa£?‹’é¡åxMט)ezà�¶nÓ>±M™xo;؆.à¸5ÓŒNЀQ¦¯,.ã(Žçô‘݈$”FÒ;ï/Zsåë#”.KDN¨ hü`JªÂN|,ó\Œ�žE/ZõüÏ\ñÀ�`åÚ3dd¹øv3Vj‚ª¶—¢{Ë΋Îêfž ¡)£á§f¤7-j‚ΓÑa.››±Ç‹³{¤“ÑIêhs +ƒØÌÇ•Ñÿþ–ö­K"dwH‘�—ÑàªóEs¾ŒÖúW?}ÑtH}&£®æL{«È-Ò2ú.MöE¿et¶.a‚•¡díræ¾h4Êè¿€¾èT=Œï2úb@ò8ϾhÚ•Ñ +:~цWret•Ì-ùH¦ÑX¥ø›2:ÝÀÿƒ+à‹Êh‘–¯}.ÐM]F;'° 1¹/ú�et^*{ñE§]wÈhP*[öy_4×Ñ[*«ë­äetœµ4á×@ £{^4í*MÅmÙú\nU²^´Œ}ⓌNµ0U/i½ dt?¾niõ¢�ø”toþLèüEÃà×ÜÌ!£Ã}’÷¢‘¿‰SFû‘…ämÙɈÊ^´–*•{£2˜㉀S@Ð'MÈ£ÐÑ%`‚ºnØåR Þ(p8ó;¦×9Õ0¬•qx¯ßNäl%CÛ솓„ü"_øÕŠÚ�ü‹8}3üéCi[�ßù‰õ±¿Q«¢=< M#Ýq8ÓÜIi­WGZ‹ DS¤õ¨ÆäŒL*…B¿…àN*ÛÜUÕ?%^V‘“Á�e›¿€Á› ´Ô·XD\\~+„§sAV>€ñ‘³INdhÕH5ÁI”'mÑuç¢ Œ7q� pvû‚*[D1P­DÚœˆØ¶N�aU¾&'z$8Å�¹õ]gµ¥ÅϰÒÈ•”»›qÈÑt8jAHã `CýÉ“Q8ý +ÿzôj¼åš6šÙ•ß¶ 0Å�DÈß…ì"î*ÂîV†• +_º%EC ­÷ex“ùãħí©&�³ãÖ‘†¢l2O7Úà*)�ŽìbÄ�(çJŽÞ°G*v“ ð�„VÛ&`‚Ù3f¡®ð,-´U0ûF‡v +˜„hK¡^­Vo4Ž�õœW8Ò£ÑîÎÖ�6,úK£Á9Z«æ=-¶_æùÎ(óJÇ¥Ùsб�F£3boHÿီo"¢2 ×ÂÝgäYo´'ÙtL¢¯û?‰AZÚýæ:G-N¨æ8‹7ûj›Ó¸>©ºèS7ŽFLç9èÖGëÁtøJLãu–I+[®ƒ!JòŽ)›a|ÅuêÁr™L0(‰ê™dðWsÙ:iÁE“…Fº _÷iy–¤‚S¶©x¨QF”À÷‰¹*E¼×aþÉÑÏ ƒmÀ¯$­rúe:Ë«èÁŽ£¦)[¤¶ËPQå“‹gj®J�( ÀâÁ“‘¬�³?&WÉãö,àc€èa¤BûÁf6òYq«¾ 5ÔŒi›©™!v3ŸÝ”‡ÉÚPrm·ÕI¯åÀÈùW¸ØÔ"ÛU<1†=”#nÖS6`ê(0ÍKÔÏ x¼$²í/öMLp°Š­é'�#s˜åûj ±£ ++6˜èùÜ73˜¯}—p4葟R–cå˜XsÅj- ƒ +§œíxe ò±'}º62ÓÁ1ƒcÈÒ¦BŽæ"�Ô4 ⇿_n¡�~ô4?m…dµÍrWPMŒ 2«zþ¤!Ûe�^ók*]q€SUG”… @[â³+Cðá�¢A£ˆsÊ~�uq²ÝͰ¿Qiàp„N¾[iw0…¨Xí²dûõûW@õ’Õ§s¤>—_ jÉÇ¡ôW†O¿\·gC§ Ø’>E2ñéA Æ¢Â� +=~dôþTFE+Ê÷ ”Â9V— v¥§&•œ—�X%íxËFM/ïlì‹ Ry“áøÑ‘dþ|›­Ù2E<�«ô=ßP¥ï~xö½ñµ´áx Ñvo¡Ù� F‹ßFX #óî 2Ø�ï•ÔîÆ�áTìÛ»…%ý";ñá_ì>«°W ì]w.†h¬D¹\ɉWäñEɆ÷Ç…>…K$ïyzg r'¢87“¾`É�TžÔÂ{pÆG +î¿ó³+¢C¬ž9…Á°á¹gDnk¨€1šÇð^W{•9pÖvð%xj�ÍÊ&ä«+»+‘‰?z±÷´»&)]x/úÒ�ÁÑ¥Åu»‡²Ü†ó¼Ê~ …í:nËI”æw²€ª–׌IÉlîG×£î;›à£ +ÔšX×lQÊãfè 6_Ú�¯E¦¥Ò‹+'rg ¼=¥аYä”|ÄR{Çj5õºÇÇVæ¹”ww æ2VÄîªÄ åRëub³GÏ¥wéÚA‘��-§e5yé˜_�Ø¥99§†¹)±±é®ÛDiºÌ¡IÙ¡¢p˽Ä裗òCˆ•§ôD³Áør¡gãsi#;¡É&& `0žà™Ò¼Ž‹8]>:¼]Õ²�]ð9„-[æËÃÍd@‹+@·/.áQ¦¶–*(J¯Ò2×*8Ï‹64WãÇÀÒn|ä¯ÂŸ\\&E°b?ù±/å…[’ ‡í�PÈ}:´Í@çGºÐ:®èF~]#^‰ö]ŽäYà`×Ó„u +­.ˆ\³¶ñžŽ%™ðQâq o7xÆ~Sù9ˆœè¶ºQζÀ ÆÎØ×^ôÖo×ðÍ+þip³ß�¯ÒÍ‚ååRni·¨Ó{’ˆÒ£ÍÅê¾­î`„ +{ ãjæ¯Ò¶~¡ý^óöÿ¢éO( §VlÄÚe²»ñTÔ3Y¨Ã–§Øa�4_ö�A(޶p`ùC)í£zæ Ë#€ˆÏ6*2=ðd µ÷²÷S‚%"a<~¶s=å“wÐ< 2†'EÓƒ ‰òD/©‰v/0¾GZCA¾·1œ¨ØZ1ˆlª’Ü,OOléã8:X0A(¢#½õVQú3KËÈü´CëézŠma¯>Ø5UL‰ÞÁ–½ŒÄ΃ŒÐ#ÏãCz¤fS�3óÕð2àÈ)8rˆ‰ÜȱÇÝ9‰P,î„‚ü¹PÙ߆Ž\hÛÖAc�œ¯×#·ˆxMiP�> €.Ùܪ.„™–(¹â5Œ¬JeøÔ6y¨Î¼" "J“98…–å9½2^’n¨’B!×;œm²¯½¾§+J}–¿£ùg/Ì%kš’Ff*ºxYðþÛ€qvñMª@ÑûMÏ€-c]oå$e•İ âÕõµûÌž´º,t­¢ŸÜúCó]Ì:h!ÚÉ<ØAbtÖ’— +9m±vvù¢F²¹åƒ”9ÚŠ6†±¿ +¶˜š™ÂÌeþŽcHEGõ:K¼Q…Ñ-†h  &¥±ln_ŸˆR}86µ³óG:Ÿk¤‹bÔ†¿'à#œdÄ Óôjé�ðOü&ÂÍœB.°Â›#Jqìî�Ü¡X#¡v{“ ·ºÐ–ù p@¨Ë¾}(G¯¨à*ÛÀpÀ ÜÙiêgaãä(Xd¸ ò¿­Ö(>VñËøxvÊã\7Fe²ÒñÕ‰7$ƒâc6fÙ¿$t‚mé�P¡^g¯Ú¼ÔC–FK–ÊBo?6ÎTm(À µ"‹øoÑ>*s¶Ìa +þD"h[·ç| … �Uòf÷`ëOT¥EŒ²-Û#Ÿyˆ>ð™QØ ULㄯ}ª'$ÍI<Ê�UâUA¹QB§Ì¥/¹^À=Ë*#$z! â%Ûéw¸w¡„Ï5èèãÚ?ºÇé|¥.sõáJü—g’/°·á A´èû}m¥b—+æ@ù"]<`s$-Ç3¿•¾¡B0+ðùžøתB|D>±1O÷È�‹Ä>€ZgïûDídFò| ŸéG|o”�—©jÁúT%�ëKÛ[;ܸP3ºY�ë!PCŽ= l>Ö@¥ (qyÚ£F7œ1d_Z/�®F%È%h§ÓD ùyG´c’æDQQl 9&é—n`7¸þ÷^ênöðf‰‡gÁ×Ìk\�Û°Oãcw +ÝÂ*UÇæì,bmûQNÂmÞà³ÝŒá4 �þá4B|RCgêQ[�??AÈ�Óœ™òp´ÍK÷É^…½Ñ61µvk#ãXÎ{žQvVÁæ`îG+:u,ûQ<ÊÉÔË6fàšÙI€÷xµê©¢æáüyèŸãñ�çTK +èÞP-8 z]¬ýÊs.wèýÓj`øÊ‰ÌJü—/°×†žµU*áÔ6ƒD|WÀMLF ‘ò÷¸äMƒ ÎD醸—ª¾å0ŠOÂãKé�iºß÷H¶½ápü¾:mÈw[ T·|”“EÙK ËÅztÔ£çœð§�pfAjEˆUo´5p ˆž·��Zøh»øÃUK¢<ºid¾ÑàØFÛ6Ѳúçï1 ŒbmÛ8µ-†Ü☠ª£¨“ ŠFèPÛÁ]ii_†fÉ'OŸÅ5õ��hÊÅH{€áwà˜©ÜžHt´^ÅõIo„¾®õÌÊNF*¦ò ‹ðÃÌJ“ñYBk¦½ +dSÓÅ~Å[1õ[ôÓÇðÀ4UôCÚÙ¸ÍL­ï'¡–{½ƒš +Íf6Öa…ÀãÂÎè;Ð2Šè8+ŽЩ\u‚|œvÚ¼(ëm¯€&‚#.i—ÄØòNàfˆ�¯Ï.N‡X4Kð‰Ëg Ì( �Áù¹i#è#ÁÂ’HB�_à^O!0F�z3cù <2Sžš‹£gÅ`†ŒðÈ'óu"ºáT©µD«uüh·ò�´ø¯©µÕu5¬ÄÜK·dÇ��-s˜ÞœB¨h%L¸%~Ê‚+ +\Ž®y>»+ÈÛ¼«­õn¿‘OÇG6°tÐ*­^™(âp½f²BÎ9äÂjàVüÃ'è%�yg—\£É’™¸\ •p5@•ª'H•I·ïû_5øKåæ¶þ5ç¬_Y“ÏÏC¼`µ>¤<“Jú«@“VöÌ@¹?9¯X!WÃu0V¨ªHzEŠÈ9¯ù”m²Å®)‡¦ ó|ì|§lŽóÉ,Šàa3G4vf±Ñ¨Nï„ï$„KõWÉ7�ˆ™Ò…ôç1…"ˆÕ§{±:Ú|+‡Ë,ìÏ=;-Ž‹D¸ÁýMm·Å +»R1dRXt_wˆ¬ƒ·æ­H¦m¦0“ã@_0ñVRàÈÃâ'¥3w¯Q ÈJˆ¤œ”�†„mð½òíèý‰.¨¾´� Ô epüh⪘øÁÉXR(p Q¯$V‚4z%Ì_>D`ÿx"²XíOQ‹<ðÂ6ÊkMœl~ŽŽ@×gIàZ[<Ö¡P]8L/*ø2  äùÀSÏYõC?4OújNû5›J÷B‹—&Ç£z¼„"¥Œ%ÆÝ–_ +9¹€ê˜FüUk“Õ`©MÖ‚,6ÉÈL�üê?Ä­+c¥Âã×J†¬QÖY]‚”–uÂZ7iý× JF¡lHìÓi@ äØäL¹€À¯0±û7î Ô„ê á߸+PK¬P&T_´.TOø7îhëR4L¨®0߸×ì¸Áò¿M'�Ü�YP@â Ëq;s,jà VŒ°¨ždWÄ¢V~KµGgijÞÄ„çO²7ÚË£/ŽG{ª±ôkíÔ˜Ï÷åË÷SÜP ô°0Åï£cáû,è³j.µ¯ÀµwDµ'ÛCídžX‹ˆBzp®2�mbhFš {2µ?Æ{ùCD˜T ðˆÊ^ÑT½>é—{šw “ Uˆ Crùƒuoeè*=š²‡j¡¸€€pøfìëg¥  ~·�ÄÞÊBOa”8׆ž @š"ô$ªñ(xT5­Á(‚%Û�nsÛj¿A9æB.LÁä‘&icp¥ú¹¨>ý™¶Müæ äW k»dlë|ŸiÛT…ì“Jíkß¶]D¹Å,gú€q…\Ž�M!¡l%‰Á(”Va‘¶g¶IÑ�Eºš”EÂ&¬;O²]M8LDFìMËíѶM’¨;ñþ¶}Ë÷I»…cÄ�¯¹Ô—3å!W™ÀË™òã€}„r¦ÜCDØC`ƒÛòtØÿEÞ¯}?�r¦¼¢º0ÌÃŒ}}ÄK$¦³wù´ºH-¼A¤œ”œ)½&4qmèA ÖC?Ki)÷$]{ÇX Ÿ)ÿŒôëÇ™¢Pªð™ò¾é°?Â=ÉS?óÏ{ôzg°È‘»:d�Y¾„a6IŽÄ©Ì>,³4R•aT¢Š!SÁ+õ^ �T73–ãš?Ö¡Á…Jú]²¼� ÇÃ@Õu…~æ ~ÄQ2U‚‡�ªKŒ’˜•È`yk*wÐf&iÒq4ñw•­\P +ãÛ:�5CQÄâªgBuƒº‘>g/�òe×ÜÓ: Cr Líc¦ÐèG<[öÒú}�Iæ‚ÏÓà‚5àôi?8asþ¶–7Q¯b sx®;£{jgÚ.ÏÚNfY±,ÆîMtîÅ�vråË�Ä�¹•ŽKχ™¥ <¦j4E „Ãß�Âsa`‘ +2yLчA|JWˆ´Hs „6Âá¢ú ÞE . X�.-†òG‚;—�lÛÖ#ÛÒHud¸Ÿ‡áÈpÞ± ¹¨:ÏÛ¼Ò !…,|²S­ù·+c}ŽüWå} ¹AUíC;Gm�7€�KÂî†Û060Ä% y X’?œ˜n îà„#$Ð2”pc„^Yà®mÛ«¦w'ØÓpJ‚»,ÛÞ69~pÂþÐv y·gº2¥@“ª“æ¡ô½°m€“·“† +–…Èþ0î.²9ƒÄ+doîEúÅdº@g•X™RL«I��’¸1}º‚^VyTŒN!.€Q‡¦;¦G‡u±4N*‰ú!~:¢~†X93h <Í‘èLøy¬`Nk'TÈ:DNŸ€LoU8]™²ÊÛ Ÿ‚ͱIs7èMè�!4›8:e¢¬ û¦?@¤Ìa2æzP‰°"Ö]Í›‘é²6“çÎA®vî\@‚BÙl””2Ý_aÔ«2ÌI푵â wös�œä¾~ÌñD¸nT¬`%"o^¬Ê]ÎN€‘ˆ„ÊcNL9æÆöŽÖu¶p‘äQ¤‰‚ü’i& Ü1Ëí�ù�J¡±åÂæW¬‘r%~Û�îÔ3£Øw³Ö¨æ-XÛ&g�zd«Õ¶[TMkÿ£™`èßh¥•ÿÛ‹–ªð¸}îïÅŸa³k½;âLrßµD4,q‡É®î¥«¤ã•_Í0v@U$˜<Þá~2(ù؆ÏÞWJ8Ð,9ü¿òûóC�ûâ•ø„7'Ä +:˜”uðaHúä“`ËšÅ`Q#ÛlBN$¥¤Ó8ñʇ _ù&äz1¿ZKAÛS¨…®Œrg:~g䂆†ûíg�){̳UcÏË2ò®aƒ¼ Œ;ïHŸj¤Ó¾cet¾ãwŠŒœ&^œÒW“¥�ºà"O�ϳ›„¦åM«±÷â4”-ÅQzêÏî2 W!QjOÓ6¨¹¹ª8°×JýÈÛÐØƒ@r‚QÇ �íóÅ2¶vŽy‚�ØR2¾ÑÚ…ß´:’ò…ÎZ©·±¤Å òÌbvK,€)3·VùoÐby"=G>_mWEΑ«‹sôúæ³ z~†C’�˜Êƒ@‚Žð ¶é/�[åC1…qU71xQ>þÀçµÓ>Ak³Ì?{ä_éùœ>™'˜¨µsÈüÐT¥Ÿ"ðÊšdaW* DMm»ËAzQ måvM‡ä™l\ƒ êm¶¡Ùzv»¦~ytfº¸anˆÞ”¨‘³ Ñøº,MnÅ��P—V-*QÚAi$òmÕ‰zHB9òrTýËí‘”Bê.UsyPDL„) ´+µÜR)ƒ²&R,^�–�™­ºY\³¼ìÝÌ',ïmfÉU¸Ä�yó}v;­à9AðÓz+·sÌ4=�MÑygGÍ:roKX ŽU¥¯À‰ãû^ü0h±üàX)3IBjΑGba÷6ñÕG¡ë¥a¿\ØÏr{¡ -"2­˜‡=ƒ:°,¦‹ËrU©”Ak�LCùŹ·5j3WÅvŒ`¥&‘ø kž X3À¬y‚ˆ?ð±ÚÏGñãܦ5¤(˜�ÜÛj$óÏX,ìŒ�¯XèNó˜°X–‘÷±}ÓGO� ~Ù8“#'£ÖŽœÔ�FSÂ`‡]`çmÁì*¢ ì, •ÐuDØ©³ìH†1ѕȡ+ØÞá{ºZ¹Ý» Z^m ªÇ4^ÐxéDW?_iØ)²@ -,¯¦† ({Ì|}÷D" Úµ8åÞ•:ÎW÷‚wLfÖ£èø)¹D d÷árAÁ<Åú‘„Ž@ÒtÁ^°j²’bÕ:›‚·±†éMZwm@¾ÚT‡ÑLP„×[·7\hƒ¥x¤R,ßN”Ì4k�Sç½¼VŸ¯#òf™Àï½�×&Þf‹· 5‚¶ƒ6oÇU/Ý%.AmS•^I}Ö 3ò9 ‹L¸0"Ãj¼�Ã*}Á2Ò1á �žoh¿…÷(£MgPœ9†èd -PF™5ÊvÜô¥ ×´B…Ö´!Á¡éà -ÒâÑâqâÂâ&ÑVñ̬£âª+)qo#`f¯Å¹#N‘°BÜ4ͧõ$™§T¥§i:JHÆ¡8´†ÖÐvOZ,o?o?§yÖ“öµØLé�$df>DF23gúHBfæ�CrOj ycºCóµ�¼1]‚1dº„ÄÐ0™.ñ¶Qgmœ—Y­º �2Yå“T>I\«üEtkÕi{¶ÃÆüiŸ`…'ÈÆ<«O›^´J1!{wòW¹à9e¹à9U…0x¼ŠRU¢VðœrÁƒ-–÷åä(<‡R“*Ž:ŸSÉ2V§búpÿ%ûš‚H$Ó,‚u* -ÚнuÓŸà¡‘™ÊtÔt#±¶>Ñø2�äâ!d¦ßt\Ðãø :ËàÀÞ2OÌŒ”4u‰”#R°è„®¨¢“E@=-$"‹·y]Bƒê½A-´ÕÚa3ÌfÔˆ æ$ÎÄ_I \=îëçái²AN�N¼ñ"ËDñ‹Ú­ÝµvÅƽ-uM`Z"|AÛÆ]ÐaµÔš¼Í× °°f€Í'’‰ô‘èKª2ŸæÓêÁ œÕäFD’ŒL#Ó– Í“†Lkã<,¼‘„$»�CB’ÝÇ!É|’Ìœ|’ÎIHÛã�`ÌZƒpc:††i04Cw2C3ÎR!«Ul¬ìl¬> [b³±Î²,éE¯1[YjAëm²w+«JeåmdeUé¤*�¬¬Â“˜*50ÎU”Šz€Ñ¿ÄÉPð¨(í@�Vðx3ÖMw‘Žà:¯;ÑGaÐ,¶p G¾¶'osIãÌã±S9#J„ÌäŒà8ù0êŠ@ÕAR·¢6 Ín]�X2& ‰•E�:”3Áýdæ, |ô|d¦ÎäÈ š¬ŒçÅ -;*Bt¢+ìB+RØ•@jØõGѱŒD§™�`�…Ý‚ú:›¡!Å×È:†ÃiyL…S›¥Sñ<ÏÛ~¬�Ȳšû´lןŽq?ÖÎßn¹/ ­cýá·¥EŠÎ ·’‰ã¬• -ùîg> ¥UÞ;Ëcy,�å±<–Çðžädoi´Ìù¸'}wQFÀùzsq�ë>w¿¹83Áuß=G&6˜#]™àLŽL$8rƒ93Á‘›‹kôGr]¡h4ʆÂ,y¹Op$ƒ3H’s““¥Ù`ˆäs½3¸ÎYËcyŠW\±éÖý×#ƒìørªç†+|çkï¹ÍÅ%œß\¤¢Ñhtnz³/¥¨Ø\œ÷ ŸàÈÎ9£Q.J†÷¦¸è Î{¯¨ùb{™J&®Ç,�¦rœåm5Ê•,o›Q®³<úï¸eŽ/àÕ<|K¿Úû·¯ýÆOr’wþ˜¹d˜,™ýcZ>ÑߎÚŽ?@‚[ñås8>‹[ÛõAÇÛ~Üh+™¸ÃÇ)¶—‰ãÛh!6˜ßü¦rݲi4\ï$éÛŽ¹Ýˆ{û(Çcñûzµž®á¼wïI²,MS¡X,ŒFotÎ{÷Þ“é½÷Þ{ï{ï½÷Þ{ï½÷Þ;ï¼7 Æb¡P˜fY’¤÷ wà|›†ë ®ô­F9¿¡ß˜8¾¡\ßXÞöÙ4Ê✃S/#ÍÆ4Ú”ã[¬á¯—^¿ÍÕpœã&×yçÌÒs=ÁqŽs}£?ZÍ+®°zßN >æ ’ Û»%ÁœL/ë!]׌™‘·}óA¬yz#kž €¸Z»XkÈ kž '™(>²æ ž¶ƒóPš±.xo½g¦‘Ëžƒ!o*óMpÅðEÍ*�Þ¦eÙ¤Åã< ,$Û,l׫·öÈ¿I_;_Ù.“­ Í¼Ú¦å]ò5sûjÆQ3Ólä Ь|‘­;rÙ_§_¹#ï(w䇋ÆíÜäŽ|GnÇmËvÀ‚¢n:�üuQEß0±¬ŒÆ‡ám˜C ÚÞÒœÀÁ28A�Dy#ÏÈ 5eìxFNwmgh|]2ÜÈ;",ž‘³ÏÈïn$é uä÷@­ò‡”‰âaáÅÅ˺ -ÓuYG{«ÞAc�ñrªÒÓÛnËYj`¼cÚ„�f®…�éÅ -»�úºÆi϶mŸ ·Õ5›Žmo„ !|€ p7€oÛ®Õ1=›Õd3ÂÁUp ®ô·[síØ�¹²ï -¸nHF2t©Ú+Áím½ŠOçìtÖ²“ÀŠþÞVsO¦Žuºg2�íʧ˜ÔÁ¦”ýãÐ)u:]§ Ëi'íYu²G®¥¢þž˳JyÎ[¶ž ›2¥N*SJÔNÛÛ>1¿ª•­ÕK =¸$�Bx⤢}ñãôg�¼ -O�Ÿ£x¦*dÃLœ©J÷œð†PÉØÁím•¤Â1^`0ª X£S¦.œ I ýQ9¥7š‚ÏF¯¡j§·±Œ*}� ‹$b6Àާ½ŬRªHæzœ` ö8�”fTö8!Õ(¥Jéå‚ð�|:,s"‚t^Ô¢19c§°qLæTÁÕ/#¦f?÷`oØ_YÉÛ¼ÍÛ¼ÍÛ&«[å½í+OÖE1 CAGÌêÿ±f‚e¢xó1¤h„vEØ'Ș؟74¤Ø]`—úĉî„ßC·*Œ‰ÎC¢‰î1;Á#h_g*mÂÛ¢B˜.J®‰‹êAöR›ØàÊœÚ$ã e6y8BÎÈ©´Ü~QTÖþÍ6dÀZ‡Ne;ÔâÃÎýĉŽV{Qá,�´ 3'¢ñ¢Þ6‹ÀïgµaN$åúy*f6cm3<çå)&%ͬÜú«„·N�Ï#êZÓ=žÔF+gü¸JR3£<êm“#Ãi—oí%Õ-�òbµÿ§-ò+=¶�ÛN �©Q°�sÌ”d\“�šõ`YGN@TBõ¢@³8² !Ï2cž`Ô€Ón„‡Œy‚£Ìö‘HS“ åÁ8dáù4)ò¶t9cÀ€)‹B–«ÿ`� -)vNàå ž‹$¤É3òîÖ*ÏAž‘צˆÁ‹ÏÈïÄÅ3òxš(¢ƒ©§a·´Ì=ÁÎ1O�´$Ø>!°]ä”±•ó�Öîmó5ÊÚÓ9ì,²[‰–S#1øprªŽSõeb“Œ‡¿VÕ@ƒò$Ué†Ò¹a IHY­ò‰Z;òðŲ|bfŠÇÛ¾Òz‚¯ZåSsäƒF•jÖöVM,P¢–®¯›#�¤aÕÇB¢¦¯{¼Éuð�€Dó9-ld"9xˆCqð�2qð‘pð3•‚@GŸI‡uI.ט*\‘„«/®°Ï sÞÊí�‘h—lOÚƒ$lïÛ¹%Áz›è˜½-QÆVqµvo¿ÑÚÍÚÇH,«°{[jÂ~ˆù'3ðÉJ,5†•¨Tg„Á¬” )™‘#0ŠÃñ´–€=D*JLF,6$(#‘XŠc1Œƒ@ ‚0ǘ‚èªÈâý€y )[¬!ÝVh½j*©ø›õ‘íç÷5>ïÚù§YÌãŒxÃÁ™â"‹d$/96™)ÔŽ¡0phÄopu‰)¬ª„Ñýåú”dR/jƒ‰¨äÍçå&UfÁVN£,|th¼D*æàÙq&̇Ø@˜…ÜO="p¶%™ÀœH…MÊE*°+Ф«öº´E–"ØZÄ,!{Qm,Ê Ž.G~ì/l?'’ùsRÍñšPq(z=b}¡#zVžA°F¸ÌXF…yºEÌ®X'EÑšR"ÜG¬¢B>€äs6¬Ÿ¡h "xïêA�EìîVE€÷õO,µçì0ws-”,Áu¾„ñæTyst/Œ¤¿Ÿ¢·cÌ�ª(òVÉn$ŽS·xýÿßÕÇ£ïþ`» %lóìsÃMÝâúœ”nÕìÖøïÂQIþ3St´² bùÏQv½ ÉË´À†ÊFne5%¼)ˆÞl‹­ê1îÈ1{ûa‘<ÛÖp-º#°dêPÎ*¶î¡W±ú¸{Ì.x@Ñ«Ìùñ¿™¯;ÀVPsLG¤Xv¢ƒÉ~›UK« *Øv½­Ï¬)ÚJì?0áäR;æá«V¡Z»8Õ`g½—;ÇøÍ¶Kz6u:îI!U É XY§E ©¢Ò -[O½JGúÄG£Ãcæ¶T¹ºŠñiš“ÑÔ0g¡ ÷Þ‡O PàO@TQMÓHY–^GœÜFÿ0EqŽ'Õ`޳l\þTèJ»½…4‰ß¹éî,å¤Âìo ý…_#mw|8ï¨w‹µþÈVx¨AQ)R¡�³Ø·Â±ßl³›Án©*]×­²R“(:^¢'·Ö$úÑK”Ï88B›¯‰KIo2îÓõΩ/“!êÔ.Æk.ÈŸ´öüZ UyÙ 6�vXÛ™­$1^<Ó ÃÏxT|tAÒly+¨ªmƒ'aâX•ëaQ»žX•îÞ´ -±üI$Lù€l!ÝBM^ŽÐZ•ÓðÃÆ[&ø@Ø•9ÐA•]a4”®öÆšRvÿ�tg½By´‡ V"åX�ëü‰˜DOjèCìÉH¾»‚­Ùü†Pº™ç±lW.rgÎh$œ¢â,ûé{1ñ4ŠxTUETµxTäÜíº%ìª�G ÂInŽgIz„Ãöø›ØÅÒ̰4úÉW†;íba Æd·e'®NP®ù¦Ôs¨®×¼ D'¶/ 0+³�H ɘ[#ågçDúJ!´k 8 [ Œág¦ïTL`åÎ0¬æh�7¼yðl:“T?XÛ¾Ô[â°½½_÷ƒ*µ¼é&!Î0’28çÓ£"°«yÛ>Ÿ.?1–�s½¦ìdÝ6ì& Ñ f–íì¶í€X÷¥�›׫ôÓ‘âì¢ooû£EìNp_¡u»�qšñ‘zTA JPE{¤)íæÐËÕ—a\na0RvA?/s½`%Häïh]0‡`/QB�L[Há¾fœ†ØáO)$0s%ÈÅ;Ñ·…=û}ÏÈ�y.i`a¤yõC~Šf„I*&�ö$M‚ 7TÑ 7¤ Ÿ±q¢éÊÐ’æ­*p¡ÖðkôqšüÓ&@Yw˜ÉºâmÔ„÷}À|É»ÝÍ9Ø+G$t�Ÿ†–ƒÀpŽà†¹»Ó9">]íåÐ�säFNÞ¹ŽPðæ£ ˆsÒÊ!à9’žÓ«šÞ‹S Ó‘¢‹’+`_ŽfÄ~Ž`„! ->QĨšoÏgeS§ãƒá—ð¹3ÙÙ|Ö<½–¿cälÔÍRâ�¢®X -(^ó¢-ëw'sÿc¦>#^[%<¾J8š29BË6ÎuÂ+mÂ{ø·œA*éßWH:£Át!}C»¤S2�¦èàhDtH!8ÏEïÅU­¹${(ŽûßBôgxxºñ÷o)ú(â«„ë;aòJMdxÑÓi9ãìh¥¤B¬Zóa½`±>K÷Ä�Ýx2)@ãx§»NAÃU|íAè‹2Þ¬u%›bXNpÈÃ@;9:6ÈÅ”€%ÆCìwh6‚�½¼êQ]é‰�<Íš¢Î÷ÓS¯ düì'Cþàs WÄ1Ixtge&4D«TÂàßã<åîßTÝ#iaÑq«‡aR¾�ÏÒ&$§ÉɺÑå+ƒN®·_¿(±6yÜö£ ±¤�ÉUª>T:p5‘èù¡Cv)¤ÐÕÊE®x&˜Òï©•T±¥HÅŒt”fùBe¶ÎzÜÝJ óã×_=Œ‰DÝA Cz)³•¦”Z¤·à�rd0’Šä3‰ÛAUÓàz ~%Øí„|~ѦÆp‹b[ÜWW5éݘó óÞÙ­{jUÈÚÿäÕ0PJ¬ˆ[Áy§¨¹ù—�eéšÊªtßra-”Zÿ‘…B8DVå5Yh[dJ�¨GF鄺© ßÔQ,-ÄÞ�×yOÄÅý’ÖåÎ4BôgÒ)V»_!9-ày#Ým -Ö:ê}.ƒTVX¦(ˆ¡�²äCí² è ¶ÒuWìз÷ñÙÀ»f�{ ‚^ë fÈ"`… ΢A ûQS&§' +; /ï# À�?@ñtÏywKá<\ûœ-u®Ë)ü_vzi׿ç±ræ'+¤¢?;«À¼fm1ÈLââø�ØIK�³ie_5H€CÔf)É„ÅúKIu6@ä½áýšˆÔæ-jÈñËäçʤ.õúnžAtk�NÐÏpï=÷v*¸Â�© çÒĬÖVOô¼ù¦$ JwÙv­†aF,[5¹ Á—AfpÏFÕPgÕ –Éã3íF6[$B±áKì,ï•{ÁA ·?ô†Ÿ,Ow˜€5]⪆&,�ýÇ·¬TH‘(UVJ¶Y7ØB3©¼½…"Sek×j&z –±͹Q -ºÊEI„ÙÔ{6ƒÂùÇ&�AÕŸÐì4�G˜8[›˜'¤DDYãRÅ åÁÎëÅ]$¥œ”ž³�PKäSô<èõ5PQ™>a°¤kW‰ÀÔ¿5…kw4$Ãpµsuì‘…TEöKRØóUÏåÅé¡ ¢js“s )€N¥‡Á™,çÐøÆ98ŠŒ·tCƒ_Þ,õ6@1®Å¨Æf) -# ¤ÝD¶¾øYeùBÈð3)¨‰óã[:ÐLzÔÚ¤HJi)rBoX&�``xa†Îz!�¹%œV Ô¹‡dTãq\×  «‚%?HÃ÷2qI¸å|? /öN{I ŠC &<ãe­7í ð®•AË’ˆ‹p“` Ï9ó oð\[ÜT'øóþ}GäÒ½¾¶ŒÖ ¼A#  Æ2*YDù—{„ÉÿÒ/"Íða Æ š’ŠâW“/A4î®í¹‚b™š_W‰*†å{xÌ1û5 ªÏò-wÕñeU„‚ñ†SêÆNRDÑÀ­bULs�*X”—D¼Ý”tâl~ÄÚ©�Ĉ'Ž£Jsù\ñä Ža8c"’UÇý6Ýïýø?}(ÿˆƒ�jÀ#HL]áì…,E…CÚ€qÖÈI(²7 …{Ÿ™#_f¤.E�cD‰BƒïøIé#’•‡¶åâ û�Ï;rŸ±ëæHÜ¥·Ž|bŠ£_žÉêü²U‚’©}×uúJÝg˲o[dË Þå³—ÝdjO]{£þÍZÖ�³/E”²˜WDl#;³µ³*°�5çôqTÔêÈÞH,Æ4£†žHí¾ŒS×ö½ÙÈù]»¤&¯Éªÿ‚) ®ãµ¢.à穇3çÕJ›Ê‹YÖ¬Tfº3|ãÚ}ËǦF!³�ôcD4Ò|ä¶Îk� 44óôQw§ÛåVÀN9DPGjZfUäp.}‚þ Ñ�°M‹†ÙlªšK°2˜¼Í/gÓ³¾.F�‚ÕAq%Ö±ÙÞ¦Èd¹²jåÞanvf�Ý2»Æ3Û5ºÙj®±�q‡µ‹8Ó"E¤wè—&Ÿ*B ÊlIž �; ûCŸd³ °ÿD‹Å€²dPnÞÇ)ƒ“;ß’-�¸3N«$Jò[^ù#‚8k DOíüÖHiæí&íKnæ;wrm_û]“£w hÂJ»}Brx†ã§%¸쫽RýNÈ¢ó¥pd…Á‹»>ÌCŸœ:\×**‘ršŸš¢÷áF¸ÌþDŒÖɬv)Œ¯Ì¬Ð K_��ˆø=h]ˆŒ|§_Qô\- ߃Á«¯1‰Ò(4(ì»æ¶B[z©ÎõÜZøzÂ/7‘é�¹Ô†xÓ!BQ~±¶}u`{ AÇ\lŸ÷ˆµÃÌ�pÍûUI¶¬µèõG âС޳dOEà¡ùŨ7c7 ^6ÚÁCé¥Ä -êMþ¼;�z‰*­ZY£)C½mjšÐºÆ ¨—#µÈ*õÞq% 2ULŽiÈI§“[Ödµ¨7 Á^µD¨r­ˆ^)Žþºè0 €�¾œ¢™{ ¸Î.ÆU®mk)°ÁrL u\Åå—Ô7ö`ûÑ’×"¹ù²~ã"ù¯å¢Q‹èû›šq$bë2t’#—ß‹L£œR�'rF\u$a`Vò¢XfGGdæÂ–é_i,ˆšƒeDv´r÷fLGî‘ý»dlœ�n¡+²–/CñzÀK@=Ét+Î-R®;{ˆ ›b�8…È„˜ÕÔ0ŸF™Ê[WWì™Â\c+qk^nÅš§hÙˆQ­xuª×oJ, -Æê¸UÃ,úîë$x‹ u,‚/â ¯ÀµÀùi -òÄ“ÑÔ�pVPÅˆÌ ªþº�"k~˜$’l3^x‡6WŒ -£9Üyh̾�mJ"Š"4ääë8MØð -¸— ðÎ/<ÝCÜÍþãªÁæÝVß«÷£ÉÜèžÎÍ@ -{^½2ÿ#ªêÕ²I” �å–`źvÕKúùãÝÕ{æâ¨zE¸IœN$”í2sý¾Ë™È”KhÀ«zÔŠäˆÉ®ª—êñòÕÛ“tþõK‘mHDUo^†øê]⌞’ªwÝ:u|/þOU½Q¤ä{(>¨U½„f‰«gظÜS°�K+`ŸèŽ‹š6E4Zçk/Ö¥/(Š/9É*BÐXTp›ò� ÏÀ´=ïì|å^*�KtUÙkç½UÄÝôæ’åPy|œìUóJažÏ–”OõK&~#g6`áJäµ›ÎîÙšN¬Ã8CˆGÇtOΪ¥tÎ=Øê˜!�èçVîÿw ¹»þ&í,ìÀ§OœWwJ­�ðÞþX–MÀ…2tk|}ì |ù~#pfŠÃLG.ùjHàb[Æß¸¨åõxšH»<ì JL:UcÜ'„"ìä._qhX�!ÉÛq£Q½b4‚›ìÓwe1´b¥(¿DÞá¥dšªðâ}—.¾ ÞœT©=±Oþþ¤:ÿðãl_uv éÈäxWÊ`Vúî"É}pgâ�š”Àq†¢æyŽÐ¦€Í‘ÅÇ—ë<ì;CýþSY¿»n<ÚšÜÌÏØI,LrBvÎïŠ2›òÀ÷¼‡cš/–õ\2)#ß]ìiùÖ'ãÝx™«t8>óÓÅK«%ÞÝ•²r›£Ì”;Xæ´Wã©Ý±„}iûË»[Lôÿ Â‹w«—GŽ,¤¾¾RDCT»ŒMéþôäݼ»‘ºMd 3œÇª@Ûõ© -ƒ¦%ƒ›a‹ 9¬ÚÛC¾‡ �ræ8f4Tm/¦œ_¤ <º‡«@\ -µpßàÜM×¼à¬Ì7äæ�ª€€ê­YëŇð‘·ÌÁjy—k 8ÞMgrÔuÞÊ‘Då1  c8™z¹ÕHb £;­·ºµÒ׫»çÉ‚]Åüfâ?¹,ç0~x;ÁŒ%Œij±å;†s3ò�)4^¯¡á™i"\h_®Ñ¹f'S$e–1å1WÎ`½ ¿¸øÓù8²ôˆ­üYðH½%ð%ømYƒaþAAÿ,kBþôƓ톔¯¢Õ]İAèi€Ì1}¥%ו‰—^µ“›sx¦ÕëªIRÒe”Ñ5@$’D¨…°oÖKò’ IãDîP´ôhgE¼%SÖ¹�²À`êÿ‘ìÓÏŽ•:ùÛß8�Ç,쬃�Ðzó×$Éô¿¼I6ôEº Ç*Ü�tˆ…qÔ~±®½Žæ|”¶Õ•¨Çï’>=Ç!¥EíóÀ�bi w=v|ÖFóÞ±ªa­ÿüYÍæK¼ÓHÔÞûS»z"#ªàáSé˜JÖïÊášþ¶+¿rª5tΞ꘻LË7+`—ê•v¾GŠ“æ >óBÊ[� *Yú}MXSJ—w]Ø7¥ÙÄ ˆôš4ˆ‡Œà€20üzÒŽ‘ŽC - úŠRiA¸ÇÑa«j@z¡ìeÆ«€�5*¬%îî®EËnsˆÈ 1Iè(>Ðc#:Ó…dÆCööÆdkØ4Z†žE4Jth«±¬ë·>߈Ýðº!ýƪ‰¿u5¤w½fp­¬]†+Ûˆ‘[ñÔGW-øé ¸èžZK$ÏÑEfû´}Î IîèÊ'TQ~¬« ódâz8ºÑîRÑYÚÑ]®qE µØ(:"uH¾Íá«o«è~·ÊæŒw=ºÜK›LK?º‰êX";2û«¢«�ݶO·R©¾ÓVy&MS�*æ¯?ëh’Þì(^,x¯üqv›€Ôlñ‰a,œDR-2'>fôÍç¢�t5+Þr)¤Ôê¹3þŽ\!Ä®PŠ¿³ ⺘âÉ‹ZrÁÛð¢XKàfs'⥥Á…Tœ‰o+€yù4îj‡uÞã�J�vBM,„µÝH`AȤA”ËÛùÁÔ <ýȳ†ìaQÔ÷Êîªzž�ÚÑlÄ,+�ö¾Ô;"¾„\^ªUñ5᥈⾑Ì©l¡z¥ZG«üŽÞ+t„—HÐL‰tcXm -Ê68e"xùå±E§h¹Ó¶Z™¿¥ýüKúo=—÷¿Úc¢ª"p2S9˜8N›�)+&å.Ò‘�@&¢D6ÌgÄ:6a½6!é)¾ÝË{žÝr€Žâ”#cjE³µïPiû’€¿|&G+蘆æHÃ{­)®nêI멨H<¶UýYó"ºwÑ€¶°³‘Ċݰ§²#gº‹ëfÊ_2ã�éöObê®gÈ�1eöG¶e�à�é*�Þ”¾ùa‘—é"—XNJwæÂt�6¿ª!Œ0Ý•P›•ÿ¬tiFL”7*Ç0žwô0Ý…*R;ùKæ~3]÷2qåu‚‡ËFa™0[Y—pðÒmÅf[_ëWSи•ayp‚q?ÛÏj%ïG~Â×—€e)¯ ó5sV-ëC¡ˆž”’VmëÐvºç-©¤2Ü·•åq•Z‘9å-$É#þ5�©\Ú˜àÇfÔÎð¥ú•ìneX…ï¼7œJ –d™ÝWX!Y6è•åcXú\¯ÜÞ’çfqX �ª[fú1�Êê¾ìʃкÁØÂ'NÉ|EY.òî’lï@aÇd–‚‘™M ¨÷;â1½%nØžJ¹ô•‡lÝ•—Q’Ä �s§Z’ÔQzÊ9–ØÜ`Â\ï˜ð!¢e”8h§Á‡èH7r»HD[<Áï‘ÉYXCì³pÇ0yuÙzL˜@¬¿ÏTYIy£ÄýD³$8™Ð†ŸuYlð¦_…¬ ³¢#tÛ¸õ ŒH�çÈé˜RIà¤{hÕîAÅhñ(L ³ƒøCá‚XI�\ÈjdÒÄo)‚ÓEá¿5¼_ Á:Kjñ[=ò²å�ÊZt‘ pÆÒÊTJ¯AîÔL´…¦'úº[Ö³ás0* -0ó¬4óžã“>á)oo|ØŸx§ßÈÇŽÌqd ¾iÔaŸùfÅt´¥•¦;`M¨²q \Ѭ¯0‰^DYÔ¢–ŒX_é�^œBâX !ýÄMÉcqZxš¢œx2wsé(fq–S”Ï wòQªéܱÿ;YºuÀ`1�ëË�^ËùóùbÝ=M«Œ:+e ;­ñMšÙŒÉE�fïßç² -Uò„… ­‰qÝ–! B}.ÀšE³™]´V”k¤Š@¼�²³1µÿ¥.‹û -hëÉÂ`‰×¯”h&KSKuèwÑMeRŠÄ?­Uß��E¿Þ.9Þ\°bñ�ùU‡ã3³µ›‰Œ[7$kÚáè0Í{�)µs žÒ]…ˆ¶NQ‘Ð(n6½\5;W9Ê}Ÿ•w­U�ñ-†5ïâ§Î~œ`ûÒëRdßž¿ÇÏþŽ1«/%—ÍÝbèÄ‘ô�·$ØßQ!—<} ß•G�5k’º±š25#/»‡Äßn”°p~;Ö!¯o¤¿’Ó)‚p5ˆ•_3 ¿Ƿê"Ãïݸl1¹€FŽ�¿é(ÓXWTÅôm½èÉØ«™$CíöþÅS~5'DÇB?C!e8 ¿�Ǭ¾$ßΞSÁ8ü2ˆ¢ ¦˜òv¤Ø¯Ûó†zå^lý xßT±f)¯;} $-¡ñÕÃ0'`Wy»Ð‚ø�bwø)àzS@h:¥ìn5ò'*3Ë(RJÓlÙSDc+`tÚ´¿N"jé\n|g -ð@äˆáù×4e0#ãe1¦Z³êœd"nY‚á’\l‹œ©ÆL ¢`rk -{ͪc¿Ð 9¡–--}Kýx|bâ.†ÐtZb„*ÔMPƒo¯â¬+Øv}mC=Ü+�ú^ÆUTbÉ -æC‘Wµ)RÂÕ‘‡ž7ØÿOû¬¾[6ÇÐ6…�œ\H•¬J%¤·úe£“é%› = b±|¯^Z×2^~X“É%~gÄ•|ûN6v$.8bnþu{½-¨åÖnGüª+»ðDöñ‘ÌÉúø°Jóø_#…�XG‚·*~|\üV¿L QA¥Æ§r»³²^¶q@‹ñ’Oýáyä=õˆ9Öt2âg–óX´JL×9=`�“‰ÏG”“üرɶzÂ{ð2l0eÕ„GŒ�"òc-”OšÂ"R›Xw n>-ô!Åϯ¹ÃÃËp)#ÞsÜ8³Z/2ez‡ÖÞ£#K­;Ì¿Y¥×¿v3Õz1´}0 ?Î –Š{>lâŒÖ¡L¨ó¸¼§¨ìbË©qmV=ï7휙®†h5¼¶¶é¸ü(mH‹jôú¿ŽX 5Æ+kf›•jÄ ¡š“5~�¬ÂûÛ­c…¥k²W°cÅ Õ¼÷œ2–Û“ue¢šã|‡3÷¤mÓ¨Í�J�0‡À# ÛÞòe9±Ñ€žP‰{-ä—�óâ7VÖÓ¸…qa@!u„�³Óo?N\ߺä~À‰tnE8b0nIêàôÙÂÙð$ºXϳŒžGÉÉÛ[Eã›Ú&z+‡:*ÊØËFL(ÛÓ _�`H0Ùþ†/‚~FË Ý(.�o¿ÿÞ²)òj}ûåÝ‹X‘kµ šÍØ=ù¬·ìЍÈ_î=ï`í⥬œR¼Ï¹ŒíÕŒu[Ü߀¼~eͦyÁZW´‹� -¦èîL�Q˜yGŠa®Laϰ©(< AcÔ †G‰Åsf&ù1+k!ÞF ¦aBÇi,mP^»Âêµ÷äö¦/Å�|J7Û%+ܽûQÿ;«PƒéUÊs.5+ ¿ÈauG5?¢8h¸A^ý‚3à¼x_é5(f¡ÎE¬AÏ>Úbñ�‰W€‚-7ø øÎ/ó!J¢Œùó>�Âe~6̲™é.äUšŸGS~A\zÀÔÒD‹¦½¼[*DOYqŽ -�q޲åĈa%÷�Å/ÏCêŸÊyÛòZ�–d܉8•rþ‘õàl’¤š1­g€Ž"d€åÙþÄJ@ÐÚLÆÝës>ð(*¯Ç5ØMìQ$j@ÃÏß䑯à �Hµ=†wVY‘õ±pb°”‘¤!oê‡íuøŠÌihp¡C7ræe?cçââ€!6Ïw%¹Œ�”ঠ-z„]AÎP`½ø3™b'ªtB­bɪOÙ…ÏÙðé�VÚ4Ñ’ù�–OK ª¹©™(õÓWæ‘8p?77-ô¯ªe¯|²>Ñ™³âõy,¤%oŒX/½Òikü¥C{«n[¦'YI�.Ú3öhÇ4B¸`Þfvx¸ª´U%¶3e¬¨ÝO•:™ ³$§o·yë@ÝùÔÉ%°ò$Âü,ëÁâj”뤛€[~¾?uì´¤Ö¡þ¼VîØŒZ£Sw‚j¼‚ÖÇÀ‘tþ‚ËÔ®õŠŒ^ŒŸ�C�,åŠ6ýN§£PêòâŸK­hÕ¡©A›lòù¶¾‚\1ᩌêæìmà¾v‚WM4±¿zXRô´ž}·¾QÕ‘AT\('; FŽí÷õß!l7?*>:�”2†,*üz�I&Ré©`ðRª§DÍ8V³æÿ Šo¯akz–U°Jÿ3›–äÞ‘�– Ïüþé³³Çð{§m×d„®HË\ÃýµÔ�…Yö?ÐÁ=ú¸»W]P„»‹¯ñøioåÀK ‚´…L0Â&˜óU1jº±p—(ÅïË|q˜»A+�Ã.›®O�Ÿ¿Ü ŽÝš -¯¬b„^.æ�ÑÍIªU‡+Þ°œŸqW8ÙgliL >B`M1Õc9�¡ãC[¯Ö,˜Æö�x*1é¼úeÀÕµÔ� M�Z<ü?î(è~Ô5�ðCæëéïß ¨D)ü›�r™Ã€1ŠbéXX/G4Î5l4¢À“ߤ([ã‡ôùqœý‹fîjßô¦vʯÃ+Ã2“©†�ІÆ0¥3 -i¤á|{³ƒYj(m&K6ò¨š±àx°VˆÔ”˜žA‡¤Âñý1å­üÌ’­NG&�ÞHX_ÔëJkî�·d¹\Ÿš×¯ ç/!°¥÷êwfâýÍ{Št«wæ÷àÐÙM6€ÊÊÖI`?𵉂 à“V4VðÐo�1+bJ~ƒ'—û=*RÑ,x›Ô-'4êpÇ-ìcY¦ÝL�|ê­ W9*þ�ÀJ¿u"N¥úX|p±ªÔ l�Êô^X»Zë�çÅâ.Çåë³­â§êA+ÒyN¸½Í£ÝrÃMÊÐä!ëYÍh¿(C�rsJ�s„câËh+Ç ßórØ=¬Ò®»èW$z‹"Øi¸ŒwEŸÙ/dØ/÷¯ã5Ñö¢˜g ½ÁS�È&» †b‹±$‹r’GŠÝÌ^Зr—n™1–=Ÿn§„°b1dTÎ%Ý4ÕóI×^^÷;,á 'Ž3PÚS[:ÀÐ~a"3>{·óøú˜æHù�ÂyÖ;÷›�´– ÇqmÜøŽð�3åTPζþ‹–� ¯À�lÉ7Ú„mŠ1xÜâ¢Pæ~K3 -,•G]†i©‘¯ó®hmòcÚ‚Ì‹±5èð00(c™µGÕ8*ˆ¡Ñ @HJÇ‚-¹LD�Z‘•ßTp$�¯ Sà“Ÿ&÷¼´Û×»™ -%ˆÝpöçÜ'»?o+Z -ÉåóOíYxYŸ)&sªÓKi`6y>¡ÔJ´N^­jÑK'¢,Eö´�ô|‹ñ -ãÂFÌ6ù€iƒî.&‚Xx^Yeé�|ŸáCéÜjÔ¤Dð¿øã#…® ÏÖjÒpß„‘3¸HPg„ר˜ -®BÆeñÀu¥†sES$D*ËX¢Ïš!!†À¸È£š -1guHè2Í?°Ø ™Uú˜X¹ÊÙe–Z †c¿%ù�•›R‡ìV‘ UJ]Ñð¢p\ûÕûý¤®8Y¢u‡0¶‹é&S܃ZcÑ�Ò~“ço*!I’ÔïE$Ñ–�B¾–ojBÍìb ÔÃ=i+î#8b§2Àà&ùŒü(ý·Ý,¸”1.€AŒ»°‘9/ç°qq;PLäRºå$açÔRfœ ?î\G<¥e`qC@pèH“0ù`5èKx®¤ ìáAvL\!ñ¾Q÷¾µÛ~Úd…' *Ç"ؽ.;ŸH«á5J¥Ýž‚ϲ:]{lëB…-GlN›"N^SP¾¾&÷Oq¤6a}çINâ0ã݃ñBd H!•HžªJÎìò`ȃZ=£š8�;¤ðz³X´ sœÒZ´©Ò“˜ß§aP³­¸�Í·¯œ Õ �á -Ы¯ ×ß¶‚—�O²Z,[ ÌAÆÆ"ÕÆ²ŸiŸ#µf‰þ_@Õjgî 'ÓÈ€ ¦óž=×(rïÐËßzU -8—¿TõT.žÌGUî§Õ,¤Öþ0—&<‚4%!¾¯Õ™\+‡Þ^A ‹»|7‡¡› ÂK„àt’É­eXƒ>®veøØ]\˜Ëf[LõôÒ”‚Ø(qkA Jì�rCFþTÿbe—¨‡ 6§ñãBO¡æö E ­æ¥‡� -—“Ü_Áyªäq—«E‰®–{¬e¿Èû ý&Ç”LËzf‚^¯üÔ)2KÃñN°<äZ¡³è¬E©C�`=¶v;�_.˜¸øá1=¿®FvfwuïhQ²z!iãòä βÕ�µ€Ê=[t„s[ªb‰VžmTarÒþƲI[õEG%æAõjç-I¬'°RÚ¯^©‚/HÐkÊ­sEн6Ù†n¨Ð‰ŒC2ï¸â=P�Pí/h §"D`1(’°J¢V”€-"™æ"=�_í &¥—ˆD5C%ǽ"8)æÇ}*å˜~­$‡ ‘2¡j9¯ :Ï–™dïJ„XoОuí–}ZÏÐߣš Ü¤8~ë ®çp[laýbP Š$Z=êáR±º 9’~ ©R‹è¤«R© :©ºâ ß?Ž5„fAÃ[RT7«„¼ §w¡ –³’``%$e ý¤Ê¥AòÿDËL<\&5R ãÒ´ÑG^@èá.ñÏáõ“×FU‹{¬JÇ (VP‹lTnzíHí, ß¶©ïD°Ž›¦^c¾¬O� 쉢Ұv*ž²¼ÔŒÔ[OƒQ®Ôšî -µ»/·âù Iß°�˜ÙÌ–�A§2_ޕߛ©N‹!ÐxUt\§Ÿst'~+·¸,j�A¯c-Ã- òÔO£Ž ãn‰�˜5œ~£º=Q4›C:Qšëð­ô,쾿ï?1!È„üîÖ”­U9ÆrÖßHŠ•Oo…óð½<¢U~ Øî+¬î -E4LH1ÂbB º,˜¼;lw dTvµs�¥�„Yh”—È_˜4£·l¨­z˜çñÎ1ÎqvÏ·u˜dB • §È©ùd X[µ™ÔÛ’ý¾ì?˯è‚QdÃt�ߢdíaR`éL‘BB<ˆáüèŽêj -Þ¤nh¦·Dkw$Xòd7‰C:\¦'­²¨Ê x”í¹ÛÈß¹kj¥S`œ¿©rs¥Ët3×ÈUÍ”&ƒ ɲƒ­ÓþhRœ94@ºáChÔO‡g¬>šž7@#Ó?Wñ›óÚ¼­Í{Éá!jF<¿u­tëúûÊâ?[Ôôv­ :¬ðÿ‘‰4z˜’’ Øé¶!Q=·&zfŽ“®Ì (©+2ÿ -ÁÒWDácG�6'Ï~98+Î¥< 4!CŠd5´óïTÀl»TêaZ„yqŒddTÐÅb‚Y6Á,{k*Ž/»Xg$„ ;à ž\¦’ðÈ ¤´Á{7Ša2bl+q *ŽÇ£"€%ää%Ë!K5Ù )¥ÿ�JMó ž|k<2@[�ßý\™Fº”zuíkÏ ‡Ò^Iè¾Æ%“gŶo�Üaâ û£­ú˜C¨-²˜F„À³©r�Ùo F|è—é -âòQë?“¬· ÔÓ†VqÐ%Å-åõ¡NÏql endstream endobj 22 0 obj [21 0 R 20 0 R 19 0 R 18 0 R] endobj 36 0 obj <> endobj xref -0 37 -0000000004 65535 f +\�O,Ñ@KŒjöækö"ºm. ]Û¶/Þ„Ô �’Ôi%fÔS–"¨$[KRBBó‹„‚áX“ç€ H:n(8$.(Å�aH4Œƒa(ÈQIrÈaª-©'l¨üsbü3QC¶òcdßü•S�!¹:Uhk&I?22qŽó<Žøª"CÖÎz5>a5š€þ]I{næ÷<^™'¿1ÉÅš¢�×X®È`D?÷)`(ÝÃ`Ep·P’»rð™Z¹ýâV*”ší}ÜØoæãˆ>|¤k(ƒÖ†¶V¥Ç*‰‘=…;�F’öòæ$ð¹FnNNU)´�ÜJ‚æÜ`"1 'wP\i•;HÎb á§Ë=Hà ¹Ci¥¶CßÀj/,N¼C[þ8¤úÝÓ8ÈüÕSeôr…úž^ѽ°±–i Ãgµj¾o÷½ƒõ$–4š¡—@N¥ê}ëÌ®ç¼Sß@·³ˆ>àLõ�9¶)_£ä �s'ߺ¬�Ǧ¯`KzÌ|×HZ©¯ö'Jã@ˆ’Nú¦*‚™ØÛÔ€.yþ Í®[D(;ÁŒ’J^ª£æÃÞ¥ÀpÑœªÙ©3S䯣`ôÇ|áä&d¯¡¢±èYØ ÒÔ‡ºËáÐedÒþ%϶‘Þôã`û>dÔƒåá–£k>Ðù6ZÍ¿ê‹ws¼³«á ÷‡Ì'o º ÞT9d¨ä„­ñ#Z°Oª|ÂúøðaK[Ó$'·û¡£¼>6á|Ãÿ¬3œ–`¼� þ—kAmÞçkzC4ÄJ>´ù¿r…ëFVèLc�Ñ$ñ3€Î¼“DgÜz‹_U íÆ7êQ�'zv.á]×%‹W<>Š›cû¶;”{fSfIG~_(Š8M×ü½#Úí¼¾¡îõÍf’Œ–Œ¾tÇÜ=¶.ám4­`> +¹mPñûlèMI¿ã¬+(áU<ÃÁŸ¸EP3evBÆÛ„×zæ?Ñ`n¤ËçâÆJøDüésÐ)zVo}�ä•Yþý§9 ‘^ÁÐÖùëltÅ ;¡]0&à§4&lêÔNŠþèNiøk4®t„|zU´a T)cê:ABËæ=³RS†�‡��*e0òªSýJzbƒêAšãU>á ALþÌŒõþ¹!T:#›}ÕtÆÚÊ)§ù”#­EÞ§=º¢0רiY�?wRDQÐ >† +èjh»&1Ù]Áa52– ÍK�1¸ŸJ­§~{±s÷Í’8N(Àm,gppØàï7š~ÐíJÛÊk¯¶*R *’�«¯·w¨-í¡A +$¿�Ì|Íf«Îb¢R;[rB½U,^ú^É©*Ô )E/}›•û°ž¤juå²8TEÃÞ'ÏQ[r\Œƒ¬&9ã,õïcCêâè ÆY/�¶úž„I–HÇ—¾#Ž¥Þ1p‰gDö�¥BA/É0MW3TÝqšÉTM¶Æ�‹ö’É—Þ‘ãÚ*ùÒ„§ š�ÆIäÐÍ$Œ€6ò(üæ¼ÒÔó茜<ðñ!Õ"â�}‰†€xßìe•à?²W¸§®öö¾y°¸x}Ò@mœ ÛRráø[(ðøÙ’8-…Š—2"ÀoD¡C"¹Ôxà¶ìéïy-þXQ §Ë¿):Y¼�ÕãŒÀ¡¡�%VàŒA½´Ð¦Ã›®Ùqfd9wÝ(Í.Î !øE7jKP Ù›ÃAÑMú4—=`ä†Ð�¦�û�»E &ÕeÄ..`ÙÃ)ºqBâÙ¢4Œý;»{ò¨�†À + Î™¥wÊ-tÅ'¿ÌQ'ü&E€ëzh|Žk^_Nº/x±�µ†€xuó&€zsD”¸!0Ù÷Sgm@'bÑB÷ùõ´!pêD }"Àu‘Û(òY¡Iö_0˜ä[1E¨ðF`ÔÝ_Ú|™³"».  N:ªÅa£TýþܼÛú!ÜN¾€K6Vøu¡e‹É’<SÓ^b�çӤƗº†F$ Ðo늊Q¡þ q¶<ügÆÿt’迲°øÎOB‡8(¨Çd­ïããyÍø²‹Kží.Ù-óhþTrT«é0\BX9>ÞÅ3ëvçýFáXœ‚ÚÚJ訖Гr´¸•íy4áQ9ì`‚úuGÌ©“¯�”âpu y4t ƒ•0ãä y4‰`2µ�ˆÁÌÍ8Ó܈G±¦&XRNsÓT7¿³†-Š4ËhAœú§!‰O%Ѱj×¥Yº6„´Ë5N9ÛOðÓ®^›Aþÿ¡› 9û±ûY—cÜimsߺé‹�j†ìÒmÝ Š.¦ÑWŸ`4túC" ™~vCçÎÆÉãìhˆ¿9ƒL$øXß@:�i[È©OŠë¶¸ÆtérŽ62 öÝr <4búRŸ –y¼!åMq¡$O@KÉu»×JíbfÂS �aN¤BàŠ%?D¢22†ü@H‚B+ßV> ƒ²ÔÁQÄr'òNõò"É�ýÒœt fùƒ¤‰YÜ ì„ñ.[�Ï¿?WqØ™Vâ[Q=¾ 3,ƒ`ÔC€DgB¦OòMÏë.Ñ!Rð&R?1TªmÂÁÈF”Ëñ×#ßó5"¶ÿÀ~Éðó%Š5zIçxd9r'ó,©Ö¥´rTL³Ä»Šp\uån³W)�(Uó¡>E õ4D®ë½0ÂÛ�1-9Óm†F�†¨2™€à@ÐI¹©Ì¸]TEY'ɨ2°ã�…¡Ïl½'Á»öEó2eÿ ™…>YÊÒ<ñ j)6Bf/ð=òЬ4ž~ÌÚ08”Ì~R_yõ8Te@¡ý9Äò–�Y[À’%­ ¸ÿÇl}•S<tJ§°Ò(×hd[%õÙb_E_w¥Ï¦1ˆ¹#@õÙ 16 óï¯ê0QýNÛ¹6—BSÃ=5Æ"Ë~VÂÅ+Å#žÑF~{iOX-u‚ïmžæ„…å–°Iõ]Àà‹ËÁu”×Êð_"ñRF2RΛ�sØ +6Ô+f3êge­gܘN‹Žøðfu&]žç"]ø�¦Û$`‘Ë gÉû3äÆÉ鲉‡Ø¿8;.Œk›wHµOò¼ÚTñßÚ§èÞ÷ /¾¶Èѽ!oPž‡Tœ+Ž#«›ª�h6$µÔÂS,ñÉ)cÄÜìqHÀ!òÅÈÿ�=¶w\v3ú=k7 `tèÇÕ©—£³\¦/µ¦ƒ7ˆ£Æ¨U ùÏú³2—F¥½º­˜Ïî2:­v-×mìíj‰íŒôÐ8FFgÆ«ó²—bÌ +^—B5>eTqW?`%ð›mê+_ÈíñøÉ¡g¼ukÃ{b…((x�…™°�HÈPÔĦ9£@BíI{ä8Z¯§¹Ðú˜¿CEC<óÖ*$‡¾Mjm‰¿xéï„ ;$�úŒåí°¸±/udûG*XpªE²ƒ µÓpaÐGšÞ×(¤Ö†®›žgô� hÉ'¤Vé„„ÿõYE¢(L¦÷Žqµ¯‰þlJÞš†Có¹!µfmÄÚˆ³=j­Þšû—h56œúNGÔËônvÆêÝͳØKå½D1«ªÝ ‹$>’CÚé>™x¾ºÙ6Tjtj�Ô~fê*yç$é¬ÉúßA‰éC#x$F\hu¬h;v±¢ÿ]yØð5aøã4Ã"Ð7£þwÐ%€NA¨ÿÏE¾6‹šbàzη<$D]šoëNãѪ^óA@†3MîzÜXδy»j¼&ÝEÆ©»4‰yM(A_Bž†;ÿ ­qNxžÂ{™�²½UlØd'î +ì‘ë59¾�÷P²$æ¶ÛP£J‹‰Ž¨3}ˆ¾-êj"Tß�7ñþEçæ6uöî�T€Êáp”‹ÎÍžZo©ÈÞ äPÞ`âåˆÄQ`Ÿß “°wÉç’^EøÏ2pÌÐðâ¢cf8é€\޳±éÐbåzþ;p<¸0áÒšl¥²Ë+ès¸Ká�;-KsÔwùð‘+«˜ +xpv)å©uQîLÙE‹¥-¿è�üºÂ°•W(&d&,üžqãP–¹:—‘øîÅOÒ+ýÔxÜâ5£bÅÛïýP꫞ÃG²ßæM07ž`T­=úÔ–]s $uùLNv÷V8Ù×m�O3ð<ù£¼Ç3ipíd Ž�d›x—*WçѺmdD�”¬²$Fà¥:è ¾Uy‘œRü!Ñ0ŠAQ¥8<ÛVóy,™’sô³ â!òÈûlê¡®((ð‹ÄÇ{ +�íÇ1®+ppdÀVTk˜–Â,QÍ»Sh³µ6‚P!×–&H/¸á‘(KKøÂð  Ü܆ñ‘0]4ëæv)£s]ù�Å9Ãû‘êï Ÿ/Ï0|7p]�\ƒáå�ë +�Uñz�q]!a ~>Ž]W¨Õs:æ†?V®+Ú ,‚ΌëŠÌWÀ×À]¾Hq^‡¢ëŠ‚˜àƒíô6Æq]©ìò[Ãðó>"F*ס˜@Þþ®+Uá×–du%ñi®+¡3îæ‹j9•)óR×Vµ}Éw¬\}˜ëóJý€:�tP[~žB¢ªÝænŒ{La·¥‚lËSÒ±D­[èÕHïûòŒ–a‡òtÅ@¥Ýòd]R‘Û ²#ÕAzÄß�×\IPÈ1¯ÊãÀm•cº®ù6dQõ$ohqõ½”Ž?g¡Ê`”~§-æ5í•YìUþy¡ÿh2 _ÙÚѨ5®nè ×HŠ(¦0R!KÆÚá4šI +Ú Š¾óÊ-0;8�÷QUhk¡Þ‘�Ãæ¤,[}|2’µ M‘c×>Ô šˆüa U+’'ÐjgzOldø µ¥ïÜ[jµΰ§¥æ˜.o)µ>1Ó’þ3ùýƒ»Ù 3èì×e‘7 U{Y‹ãÞç�„£*üƒ£Õ1LK¶S é‚EóƒÃ°cr‰B¨%[jCl˜çÆd:ý•&ˆÞÚ.…£\^Î5È´•€ÓneáÁ%‚g“Àé­ÍƒËۘ͡vÁþcé�vqt'óìýö¬å2>8lOïö)OVîAØû%F“ §O{ËBs#33úAØ3°�ÅÐ\yUTž¹É +£u̧–dòŠx÷ Œ^«Ê«£P‘Q…¡k»6 e8ˆ°œ½úÎÞÝÛq¬jhY,î¯*ÌY»›ìiÇÚ¾øgËv8hÍÞ@›…ö¡ -*¨s¼/‹)·lXÎ�´oÞø±øÊ9˜…v�6`4ÉduèÛnouE¿N~ml°ú— $“Úê}�+K”û_Ê|«G•ÿC©ŒÒ2Îýâ´±øI$NM¬žmñ�"�ÇÈçf{jµ’qî°y«�K¬n�™ã޼!þ.-bõ5ì‰ñ‰^nôo[�ƒ’Ÿ@¦O¦jl覰ÈÈëàp¨8yăcŽk…ß½2ò_#·IÌiWÿâÁé( Š5•í:–q:9¸,×£`<øFÑ)‡ø;Œdd-!d”K„©Á¾6ZfÑ|¬ŒQ›!Ï2Våš2|½¾®ö‹;!äÙ�ó:ˆ½YˆõýZ'@§~µv`œí!HáÅ´GtÑ»thpÈ DÀÁU³]ñàeRæÎcÚìTþq Áé\cI6¬I€èõvªîxƪÚÍTºÎ ù xžˆ•Kè¤K——R­ËO]Æ8 ×�Œ'óOFå~( ñ1MÖþè@ Ã_®èc.À s�‡òT—á{²s#pxj²WcÞÑKçz(®ždŠärHßÑ<-Àyq}%[‘Eñ pþ“:¦>€•¾PË&'èIæ0ïVˆ{ˆvñ”$T»: +.’Õ.KIŠ� pÄö%Û¹DÈÁ_¬ɆúQ3Šm“2{‹üz�YêB¸Ð®£òQŽØk]³Èôî Ú.™ žÓèDFÔ¸T~›-¶ßy +C+¢ -WлþXºå܉ÞtKìéH‰ d¸ö^´æÀ™¬IBí#+¿Â§2>#p€€GEªx”Í‘UÍ” nÌŽq"ÃÒŸrÏÎOÛe;7³[ÑûÜfVÁßicé½eóqŽ)ôBA‘¹Ì¬}Án~„¢£êíï+àŒÓ\’‹“r±ØöOÏéV5‘ÖžT—«S!4‡ëG�zGRf´™^ p²[ô1”„ßgŸsUJ½”ºoÎUäBðIfêªÊÅ)ËÄkð�ëšg\(K£ñ¼Ò’+Å"fðô‰ù‘öV Ÿñp@zm-^Rçª:ÀöjùfÍ`>«…LK£Óyí»“kƒ¶høýœà ”w’ +މ¹Uv[‘ŽÙ€Óù ±GˆåGUq{§U{k–…O«š>ª’¶Rb’o5ú»å$U?;—9 ó£ªþæ @p=gn¼^ßóÈ46‡²ý¢l�ó6¥A`  +¶m,ª1Ä&Q§Kó�Ì=ªjä‹ÄI? +©ª/èwrÚ®­ßªGš€ø_ÍiÙA›M:vËÑeðLECÂ~ ðЗœcËoZºLLg@K¤^=aÍÓ—2ÔíQU3O-÷—~�‘Z:䎘ï©ê!¾äº€;ŒGU‘òKª¢Ò.Ã_z«0¾ä¦1�ž<ªbj®oJª’4€Û^'b÷?ÿ_ræw3sôߣª1w» fÆ—Ü¡-Í™'Xiìyn» ÝÊ—\_…|®q÷¨jöà ³Vª™Bä/;‚Û—ùo{ù¢e7­#“Zˆ�Áä}$> î‡½&˜±¤MYz…¬è³!•‚å¨ëàóBší˜@3Ýã€øq#2ÞmŸŒ/ ê�¦ ™ŒËïgM-ãb¹÷¶²í€•rña:‡{Šß øà".‚E;¼™!ËmIœªóK9‡Ç•ˆ©°FÏw?,íe +Nµ ¸_�@œªfŸb +ê#œJ•(jDJ%8•XO„‹^ëÊú¢p쨻‚}$[?Nø–²‡”¡&_©›•^A¹2GS R•�^9yQã½ ŠM$1üÉÝSsâT$gÔ÷BŽ÷i§j„µHq�¡$–F‚Ê¥ìHÅ×àTmï˜r È¡-�`Ñ°Úø"N@šL [pª8~¥QïÁµ@pª¸ÇHœJ¯ÍC N¥»®›OeœªwÚлev`œŠC¾àAÿT•È�§bs�àTy�Ä©¸öƒ®‰ÒÞ‚SÙå‘[U/Û[¢µ  0 §Â¾ q·C ÀÓ/¹sª„&qªø.JpkÞN¶`0-‹éH�‚ælÐâŒÞ‚]Ѫùç›87 «‘êàT›_”*êMº¬ú5-�ê”öjrÔajΗêÒœ/"„+h ’Ï3¢ÌébØ@ +32 Ô…Ü0~y¨¢ 8RÛ«Y1}Ü.ÄÚ° +XÚp®Ô¹¯íÁP÷N”®–vLó×öè9�6lóƒÚ^=�ɵ=:ó ê¢9|’–�P@­Þ²³H¼½™ú†ƒ«Éâ¢$¯H&ž±}÷¡ùov×}O—žDZ6Ç>©îˆŽ¹�5S¹®vxת°í5‰µlbjN˜#dÄ($lp`^õÏò&wŽn<‡lsGMFÊ¢ý†hnô ž‚<¯!J�'•úñ�ŧH͹Ÿ(Ç“1_ûMÝö Ú÷½ËƒÇÏè¹R6S9ùM“ì´¼žÖf�tbÐx’òáQ�eÓRFx×Û}Åî,É�sNÛVˆ%\€æ&ÌÆ\uνý¸¦XS0õA7kë  Úß-&ð¹R¡ˆŽ*KØoMãTD9=(r­•+ç¾”WßÖ1gÞ…°fòÔ ,tËŸ›)õ**­F½û-àöö)CE ÝÜ�CÚgå–h/ÙtÓ°yצ(¯«¯ l�÷Ç ›(á4 ä„㬠úÔ“D¸Zz¬¸QÍi·-—ÓeQÔ"^ƒÑ8 +sÙÚ¤_ÑÌWøÂ¬tX¬&O±¦„È ( +~'"Ô@ë2”“vb J5£”âëƉÝ̲($”œfnÝ«ž ÜµíVÿ^f¤"—'Æñ¤¶âª\\~aÆŽâ¾ä¹ßc¿J´-œ ü9§÷-_(G9uÒØ¥ÔÈÛ]СÆ8xí¶ìôÑ"2‚`µaàé(N$�Ÿ?›à …ÍE´.¹�X-ï÷ÒSd|"Œñ1fewýþ¹æ÷2gõ²ÁðÐÀWïÊ>ø½j]!!H8¿òû`.~/,Eà‹6„W§‚›~Ë]~¯n­–ÃïÕ¶øZœx­ +¿W÷Mî¿—ÑÀ%¨Cx+:+ðŶý^Nõi§äNyÝ/³„Äïõ®-–-ý^οL“Pµ;ñŒ)Âݧ–ܘƚ#ÆÌå…†?Þí½dI®Ì•ÿ¢_ƒsh+„LÏןÎ#ŽW‘*¿w»ˆ:ÄúÍO†ïvÞRQûE$² +~•ö?z‚@ÓRšlœ#39…}°¯Š6Ìw»"UV3Mx$YjÒŸ­'"Œ» –ÚKð�4 D™+æ øÅè?ßh–ÈP/8dÑXꪆÿØÐ Ë(½ôÀ9€,5Oz.ꆥ–Û°Ÿˆ%Y׉e©9*¯(3�#�L˜!jÛÑ€í¿º¾!w™ˆT…ÒÕéRæûmn¡±…6¤´0kîÞNXA]WID�÷öòíá¥8„üOúÖŒx†-„à½f:×Io6G€ûÉ jˆ8Ð.£+t—˜|ã§9ÇÏÝ4¹¦–ëR¸âÓ»n˨ôQéjz…�òß]rºhkºÕÎÍb¡Ó`Ÿå|'‚»a™á”2=ÜãžbÕ燂nuÓ ó ÷¼ÀP� ã« {•@íÍn˜¬(Å{ºW…{ýÄêFê¹PT3 ²ÓÛýÙKcX­"µZO›@²Õ™ö%ÑL Ý4”‹sû. –ª˜ÚŠs)$«O�ùu++Ì»ø[ŒAIûí­*öijöÿ1õÔ¶,+>¯D¡¦, Ÿ&&wÓî1µÖ&ôÒ€˜úSúYv#†5‹˜Ú@¨?%Œ©3œ—¦FýµHÅáaöê¬ÅäИ�ÁÇ‚ôo¹‘E¥û…¡Ê~•Wþ8©¬<Þ`ÐAoÿ HÄÁHù‚?¿Â¸ôBýWª:Uˆ(Ï A +\©âYñ¥¥rF篓>ÑY«^ïÊîI-x Q O | }ŽJà?\‹ÉE›Žb @rP\£¦£(ê…£ÔÍj:©Jž<ðÛt" ¨éLêZº•Š;Î ù.¹Ùó£-©zi ¹ºXŽùÇsé´ZÓÉg„уŸ”³rÃ_ÅýŸcÐËTä&)ä¦'ø–kÏÃ`³ÒË9;¼u)bq‰Aϵ<à�Ë=í';ˆ™r³d,@DxõÎÌÞ,Ì×!Å¿cëú.MÃW¯#|ÔkO>loùÄT [!õPÂÀÞ¤à•”cêå§áæüa…|P@¿¼dEX]dLÿs‘fõ9ôs§¶4¬Cô:‚FÃ}ù´acÂRa‹*ÃFS£{4aiÝΑy*²ÝûÇ1å~ùÄܽ 4¦æ¿µïDÞe:$zÙ&ß[ì òKáBi…›eÎ#L…ApšÀHÌm•2=ÍcbìŠÄ´Æy»Ò»KaÅŒú|ÕO(ÉÄÌn™ÙD·‹Ü4z°Þÿ…÷¬™ø,¢À{í"2÷@µëi°®@ïrÛݹÐa¯ÏÚN°Å*^,šipvÁÙ êð¼3°Ä¹̆2–6Pô*Þ´z–Z�U +>ÚT¦–(¸›í×LÚÐU¼Àñâ§_aópU¼OJ¸‚x;Å;þù±Ëh¯²�VãÅ�¼6´åh¶þRÌ#·<¿Úº�Ê\üÛ�„n-;㛤ô�€äâ« :ÃÆ9qôêŒÙšò"œ‰Õ×{!¥ ¹øhžâNl¢¸ÔÈ›‹/Ú‘;ÿQFw2ÿ0‹H±låÅA¥^¿“]¡_´ç³Ï(ÏEý�TPÄÙ�7,+²�Ò× Œ–_Á¥ôE“�DmF“!­phÞ¹HÛñh_©ÁXæ2"†ªÎ‹žã²HF;Óá�”=ýc&Ëhà)ŽÛ7Ä.OËfæå^DÉcc€Æ‹1° =›ç V¾hXé‚“ÑJŠƒÃòuqµ/2¡î?ÝÀå’£jäGxO1@T+ïÍØ ž“ÑJ¹/zŸ�îeô4Þœ›­¼Ò°¼£z½èáÜ„2º‹ƒîX/ú×›¿2:"¼m˜ó�=?Öÿ<M_¾hÓÔaÌRF»ì‘Ô´C+ï]e×ay³û+¯ŽÊ +�¯ä­¼}(£›œK”Ìšö ­•W�#ý¢•Ñ‘†ÒíOÇô¼*ó‘ÑžgÐÌ€÷—,ÕãªíÓèëÖ){½ºâ£Þƒû¢ÿa~–Ñm¢/Ó–`à 5dä¢ÎŠöp¿èpê7Wš@>{½M·4ƒ3¿ôçÃXb·×‹bïí�a~ù„/Ž™E+š÷zßM™ŽV�Õö†ïõÆ™¾„ê`½H,b{½:]h9º�e€vç6© U ûõÞ9&Ìi¯—_•ÂÜ%Ñ?µ£B\F#R}öz‹°7ŒÙÄ¢l꣘T„Û,ÌÃhš �Å»–tAÒI–¨1æÜÆ›ÛVjK|¢ób›‚õW]åCØëú¥Á¸t`²×‹Nm|”¸%%ž²§ 9LV¼�Éßë• W6`Iiî"özO€ÉoÞ:Ü•ÛëM–ë^ç,²×{áW¦ÏkaνQÖú£¦¯dNcîõBÎä0É!SÔ ªÿb2·“©zu8JM‚‚QñÐdÎE=b8TW(e{½6èÙVR1�…FŸ$6w9ƒöWH'úFãsÒhÓ(Nìq½Ñ´< ò½ÞªÓ4Ž "@£ õþ‘LèÍm¨“~È>�¶ØTþý·×Û…(‚EÍú„Ê^ï$ÿ&íE33¿SŸ;t¯-sÿÛô °�ãÙNš6XözGE§ `…öÚûÜjø{®æý›î™:Í®a_\Ÿ.‘:Ý¡éhD”¬QKŽ–šõ×Ë~4·oRc”j7ˆ´ó4Ö$ÛÃt ËHl¶;ý?é‘`{½HÄê6*b¯ ›ýϹç³_ÚëÝW×yÄÒ^ïÎ*°š‚Àxïìõ>²ÜŸUh°q²?„÷]Û‰i™Qx×®¿.²?Z~R;¶¹Y@„%ôÊé>öâÆ¸ˆ²{¯·Š…U•áú$ßawg¯>�*›¢åìõjnÓt†¦ñŸÿå^ïÙ.žP’ Ùë°,ŽsÓª„cÇ’ÕÆœìõöû)õS·t|p> à¨fµ/àh5q©fP­é.×íF˜xÙ¼½Â.µ�ŽŸz±h}çé5™••uæ§~´Õø ŸD�ô£Æôts ‡G‚ÆôøKšL‚ôî·—¯Î̪ù(´�çí]s9¦ãgP/e‡*�~Wõ-d†-ž&Ò¬ʬ(XT |C`O¢T8‰†åã0‹^1ÜFf¸ñ]Äe‡¼ç-ÿÑN‘õ“ñüUGú~®Ýšà«IBËçeeqA!A¹Ó³a‡UZ�'6 ¸Ri €¯wª´çs`_5O/ÒŸV|›šü&ÚJø–Á à{ˆ \Ý› +ù„0#�*ÉJI8°®hÊ8‡q¬‚ýp"­ˆŽ¯œ-¢z°A�@`ˆ§Ô“_ž3 ©¬4nÍsž]L‚u„9˜½�FM°Ð²m€Ò}~ÔgÞ5ƒaù˜4x�!j±ÚæÏ­á» £|YOWæh2Ó(gŸûÖÛY°ê_Çæ‹õíú‡ .Äý +J£\Aj +•aÛ;˜hÂÞ6‰qÿ¢t?ÀÙ 3°´Xw¤Ÿ€NÛ5ëÏ· +Z½‘öz,0¡U-�\Hyt¼ûØÅ «øNÝC”ègb””ðÏkZPIÄã6TM³ÏoÈâ÷Þ_O !Ýï5ú_„SW~Z”u·2^P«\Â<»vÅ>B  µ�XBÔEj2 ¼cj×wIÇbô«@Á½�?1 ·l÷Á…ϼ�V‡aÃõ%Ý •¦ã×!G]^,ÅèØ´ƒ¨ÚK¢§¿ ôÀ +¬ô,H­R×!\g +žU€6fú ÆÙͪ¢@AÒ.¿ÒOPgR U  ƒ™àºŸ`ÜUŸâ¤T ¢ú 65à¶@Aã?Qú î¤àÑE�‚Í7`O?Aó(PÐ4N]+¸~‚Âs +^’ûô� ´3лs^ë'(º’6PzSœì'hÐP7/ghåát¿Ÿài5P0ŠÝV‡°ŸÕªc(˜”ù€Ùü%Ée‚¼áä'X·±•›àŸˆ†Èµöå¶Æz"ŸhIî)º[�3úÿÉþ„»y{ËÖ§u…mŸiÕÂÞæu!šàÙv/Ò:Ù�‘.팅> â{+±„¢Í4¸¤:a潡ž@k±¶=p�ï@cìvœŠ>8˜óÅí§o”Jð§Á²â¨ú$š§ Ùf$²]×ÀÉ>#0 ^‰ÚEñ³Oƒ´ë8ˆ0Œ¾Žh¨h>–-†%ÕrIC7SÚS3ØÓÖ+¥`ïŸXÞÉ ÷Gžû453ÔáŠó­³ÚôŽ Í`…h½bm2.ß‘9/{¿´Âçè|?ž¬1}Ò"q¾+4§$(¦™-�7Î7ƒ¬=žÎr› !�nuéàþ.èYï`[ >,mý dx§çÖmmæ�óE5p/àºHï`{ �â[]þ*”CÅ�sÕ’IŠ¥DA�LàߦZYVEÞ’s°ŽÐ©pSc]8‚À´ù©µäÈY„K8¥Á&^ÊV ¸ B:çQ�@SÆšÓÈy4˜£ñÁ²ËÙIħCo½ù¢4Hú ÅÀ+$8»/×;X›‰Q©¡fE*mÄZóNkD�˜;Íú*;üÞ½’—sïüò0ÀÅÜ^`E_³¿w$Éö±¢'‚ ¶‡ª`Oa¤:‘�f$GSÎÆ±0^†!„ý²ûáôlmt4�Vgh”Ñ��L•Ôœ¨K�pN1à–žÁ'?ÿàò· ¾ë,þ‚ù[Úøø<߃åäp‘DAÑ–¼±Tî2n{ÈÌ sÝèCIž‡v[Ýjò2:5JË¿¾ræƒî–(kðhy r§åO�2MB£CY‚ªy¢z ŽÓèµ'J¿3_…�Q+jëoJÛz�[Ù%›/(O¦úŸº¢Õ£†#Á9;ÖÆè¶�±glú¶Éþ¦7ßÿ]6ÀEÛ;ÖÌ–M·¢çÜVŸo¾ˆƒž5ìï‚ó%»*$O64Ù|—Ýl‡Iê$Ri%þ™—Í·èËTwÑÛE&qãÍW÷&$>½m¡ü�ß‚ºÀ¬~_{¶ô„|ù+˜¡�,?‹RAôÂ@)´Ø6ÖLßEÐÑ Ç@ü+Ö…�^õ Œ�¯| ÜÉq†ÎãnM n7¾l™úž¯ß‹sNç’ŠƒÀRüþ¡ÃÒ¸pÄÔâßJ»„yrñI”hÆÅÄ2žÒP‹›j>»ƒ5ª¿¬ñS1é^”ûæ«Ø™f;9FEmÓºÔÐáŠæ¼†ð%8À ù¡(ÌÔ§Äp¾|EÂfÃ)MGOÇ#pø¾F5z&2²Äõ¸+åw9×A<œO⬫·@w4â¿ØA3ˆ~ˆó4ŽíûžWXÃU¨›Ä@YDÊ?ÚÚfg»388 +÷‡<„\µÏ²Ku• +Òü…ƒ +è�:Ù¬/.ó›`4Y~pÁ ’9ž¢’51qŽ� +*òƒt«ÂÙk²5‰:&€~ü�¢�@kÌYå9¡Ë~ªS"ÎñèŸü AšQз rR�ßIxÑ“¬/iRI”óøþÙNä +CT²µ‡qiùAq%~¬©±„¹/G�µ©€ò½º{EM4bóÔéšr ê>ŸždMÉ,®"ÁÀ¶ŸÔˆ°øô �ð¦>F8JÕ"°$U´$¶ú„ 1ñ+!;;dºa_=Lâ�ä¶¢V¬)?…i嘭Hd•KÏnŒ@©ÊŒŒe ~ù„ v�òMöM½ò¼â¹âבzµk�Á)ÈJ î%\¦ž›…üfò)^(ÛíóI‘øåÀÃX "K™ì#/ñ[4~óPÄÑŒò¦–HF¤³ËÔ¤—X{ÌA�`Ž!Ä *.[Ãï;9ÄÕj~bKkŽ`³o`μóW¶ä1-ù"Pî|61�‚VýÊ'·*±ƒÁH5ãg‹Ç¿’°êŸúè= ¶`ògSÜ0YG+5fGË'&œ�KNfò²‘ÆüªÔå‹ …](\È€Íêì©ã0tƒøA ›8öÕKùµ)—]a4¢$%_s¶B(Í}Ps¿£ï‰{´ßÜÊe+‹­ÍÖÔØèƒWƒÀ“²!È;¤Zxô—øTãdöo5á;ï£3nê‚`×mÑøOŸ7áß›äQîP‘iøMÖÕW)£ž'‚9Cð2lôÁ"‘ÿös�"�쀹iV\ÈÇþ*�Ãj ¥BZYÁØ„’öί¦[Ⱥä÷&^EX™Ù>°ÿ�¡JW/ÒD œñNË�£ +!D\´Ó÷ØFI"ØÍPž7HÇÊ, dQàÀiÑ \ñ(ï6ö-)û,=Êí0-†ÿ±¹"Ê•êÆ¢zx´eùù!ŠqÊIÅ*¦¬ï†Â{Ì •œÄaûž h¹4nß ¯÷m®\·I.›wù�˜ïÜâ ½HQKí¬´Yk­`{|T¦°ÒÕ– ©{ËŽòÃÍ(ïØ��¿“0ÃÛ/Èë�ŽdP“é¬éñë¡Ð³À�ÄŽ:1]A½[Éšl¬§�Çx—x6@`¯¦Ê.b$ô¦Úy„6HJ`_d¦:´Tr<?¤ëu74=—Z½Äp(üJ£3꬘*ÔíMð§�tÔÉm ‡Žb¤9ø8�Þ\·‹�­üº=mSà¥;`¨ÝÿÉÃk>¾•Ë\º¼§�éNèB;Á.’ÝTˆ€–“ñ¦è8oÃFMW’�W{¼—’aÕñ@܃“§¨J©Áë@tt‹ŒÄAt¯,¯ˆÆKë¢wÓ�dkªêß�(ÇJ႞ÅY»[3+î7l^˜ÿÃÒþkÐä.sè€E·Ò“óC_-hÈBĪš°,ΈË=ìpFEW¤]¡‰É)hZB†èÄ4?øä=óbªåŠ#°DÃñHÑü +“�5I1.qc…I½$ƒpÄŒHdĉ'2¨5À;ÞPYR™E.‚ÓÉÖVDÞñ?CgØ®ë¬-»àGIê£ܯì˜ M­9K>Ô–,:üòžÁ¡åæÆâIz•fL®gl)_4uûð +ÝÞ {a¤nvûX©N˜Ã £r€ �T›=!ÞÀ† «AÅApvYžô3±*<a *¼Ê +(è–í¨È²Ú Û�DôhÂ@P ú­0Ùï@Î�nVñ.œ^äÒª#ÔL=BW,NQ„# X×JÃtzëãÄ틲Âv±_Dpá4å0 ïYÕlk®õÊŽfÑöàRf¥\ýL€j#Y·:nY€F]?[�Q»Ü) Ș']ÃÄ.È#�8‚Œ«¸jý˜SƯ3ÿ|=¹Ï~¾D¸�T3âÇ4ƒ·£g#LÒ„†¦9ëªÚ„7“ŽÌ–œ}‡û›ù¿œ ZsÃËØßì„ÿGrv¢ëò›=9›JHî7C¹ ¦°®IÊÅhôš»„óÑ5¥é5;™Nœ™ÀuM×%èc¯ù¡]Uctͽæf"L×´‡5ïôšû‡Ëá)î5�® h¯É&©O×4±ÚL½¦·ìÿj]ó×f»^3�ä\Œ®9b¯Y˜{}N×Ü5íØkŠDÑ&ëšó˜ÍŠôš�wú?ºæ•½æäq©­¾‰B1œ@﻽„çßý»þ¼^\𯔥’?¢­:vlðûž’;A ë¦4ÃNdi%a�Io±«$4v2özZ?ZwRXä ̧^_A¤GZ!ߢ�òÿ#š˜€™2‘Ö +½É‹dÒ�=^þסyÃ/¼N9|r|Ó*É^7©±v"OÍpœiØ›ÈÈv”äÅ”ET;eÔCÎv€¹>75u‘ÊÁ9ñѵSúQ�K$ßÈB›ùiO7ëð™qØt(sŠ¢¸_C4è-×Å$ÑØaÃnS߆^ŸÄУ[™q¨àדÒ&|T”z{ÜG±2@*¶]…ðÌkC®y[ߤï6õàÛòaq%°oØ3QˆÈÐ,å=WÈ;†äw¨÷JÁÁAATPäÀƒ½h–€Ìoƒ“QpZóf4! œ“·Ê(Ñ*wV‚`EÐ.1?&ûŸ`È·©uõªa +Iy­æF}F¡äø›ù Ù©û¶`ÈÿcŒ�W×e 2“Ü·ˆ6ðäU� @£ÔÄ%¬†ƒ÷ +¾‹æN#öå­(­�%š'TÀx�]Ù·þXT;”¦~Jh¿†¼BJ¨¢ÅRQ5OÛŒKÇy~똨 Á~HJ�‘„‘_nö“Åjõô¸"GÙ "r ’]‹#B&ßÑñÁ«gN'ÔH=üé…•ýüÖNq瀒üA дÛÚ8ïóœXn&' ‡4òmÌ盦?óÂvº¯81ÖÔ�2äDWYk1+Åâ# äO“äã$Ž¥•Dºô™ký°¢þ…ˆ ëŸ3;„k(&ÿ%y¶a$QîzäÁR—÷ ô*nì\Ÿm2!µšŠø)�DˆùÀàŒÖùhK_̤®C� Õűm)•nP’D¾¿²ç†e®z9®á‘¹ÈèFâ…oÊ ·&^‹Á +5ã†bðý ËÔ@ê4Gòª+>XÔŽÐ?âŽ~ªuPNƒUr1 mX¶ØD<�ŪdUŠHÕ9DLËpyMœtÞµtÇ·LRÖ +“)»rYFoEȤ–=~JêŸÀt ‚(’>�"£ŸÒ¡&ržO#/jw’ÄÞbj‰Q°¼ªR;ÏÒží¿BÍX›#ªGV°¿ÈúCN�Ëbc‰�Bõ̉¯) ¼êB<^5œ?â+§ç Ju…žÌ5¹5qqÃ2�IÂÓ$6 + ŽÓ¦}5Ô\“jZÚ�ZEѰœ©›Òʆ—)¤2ñ ŠÄ`£>�Ñ ‹«p­c=d]L:/VH’O@‚,ãö:¤6Æð9òžÂÂdÚ…Žï"ô˜¢ð8I–¡� ˸DIŸV‘ÑOk5‘O¨y‡1HÍóŽSÁëëPt0bØÔ¨³6Ö«m‡:SڜۮÑ‚o ´è|Ç�Øü‚pΆe’W€¶k—°GLOä!(Hï*QHá­Þ²ŒQySˆ„áî9Ó°ôª�§mX–`°aÞè+ãLeÆ™¡ðÓðJ½îùjèºPy‰ó|ÂQ¢Aþ²×¨¹¥ÔHO�ÓLî4q¶Dëš^˜Yõ6"@U§J¨Pí ªetAF?Ï’ÑÏË2Š’Ý!a8�Ð×èóZ±F(-k¡�F#…æçùpð^mî„XT¦QOÜA?¿0ê„\oç%»Ÿå™G­ AS¹Ÿ:î´>RÇÂòUÇ0g:Z«µð(@xtœy3èç7, }´—ï€:Q •<�nôgåu@™ðˆŸ¹zÔ€èÇGýDúǂ袟H÷ýºƒ‚M°/Ì@ēдÁÏDS0`¹�ÛXb`$ðÞÍŒü{dPCå”8J‰ þw‚)±vSãdàò øi©Â+¾…‰wl‰~_ æ +–#31wi¼§r¦Hú´MAjìùú4Šb 5.ÁºP ö’€ÍÄcB˜ï{Oä]uKNY1aé+˜L»vÔŒˆ•$öôûy‹® Dì†eŠzÃ2ˆjm‰¾1��V"%Iþï}èíîÂaY»{¸I�e«ÝCZÝ~€»9–©»cÙ»a™@=«/ßç†L8­Å­g5¢nŽe¬vw¥Ô¼rX6j7ß¼Ü�÷]’~÷ps,›cà¶Äм-‹–y KúäÁØ®](Y¿t í:Ž +T²‰ø'°‡•ý¯¿J‰~_m‚}ÒUJôQ +°?¼ðñIC&þP89&>ÀÈÄ|FÑÏC¶&¦åwš¸†ë凫¬è×½’qEqĆYý ùE¢.*Ñ�t!ê1èzLl©Ä7Xѯ•(é ¤zS±+ÏâŸj‘ò‰tQU½.D,ê�6ѱNÁ•ò${8 ûj€»È‘ǰ<�Øïqx… p!ŸHF0ØŽDã’º�§`²Ô�êª0P‘@‚£>N΂s6,:#1�oÒ‰ˆÎ#X›‚ïDlÑ Ï2#ÛÌÈ_á$ó=2³¶ëÈC’uä¡/]»så- +%(˜¢Þyr\‰ƒ8EV|˜8[º‰{l‰~žLÇœ‚=ˆXN·ì(üg…€û$ÔHë˜w£˜¿Z#X&ÁD¶->Á®-½ã¹UÓñ|Úñ vÅ5¼c0}²9V¥ày³è�¿ïŠ Ñøî»ËÅÀ3„÷w{·w{·wkøß¹ÿ¾sx5·.\©�Nciwªzîî»ß?Ä2Õþá6¸ËýÃp—áþXFÚf÷ß}±0MÒ\”$ï"÷áþ ÞøŸó’²4âÇ2U™e�ÝßíÝÞ-+±"q*Ó:¨g¸ÖÄ©L‡v¥TªÈy¦à,°=íýÆ2ÒÃí±Œô¦iv^ú²3¤¸À2Òö>ôáþ�ó†I2Ȇ÷¥ÈèáöÞ/hÎHpwwYí»aY»‚lþn¤KÝýÝæXUbÁìyÆ.¥‰¤ÚóLÁs ~¨àT�bMüï<5@&µTÿ|çŽYsëqž± €Í«u ‚°&\B‡tç¸fÅ�óŒU'\“âƒóLÁÜNpwÇíË8–™<–yÈî,kûÿÆ”Xöò^X«›ŸX©R Îô¶wÎ{÷þ’,ËÅ‚Áh4ÌnvÎ{÷Þÿý½÷Þ{ï}ï½÷Þ{ï½÷Þ{ç�wÓl4ŒÅ¢ …Õ¨óirœ“hFD$IRè3@@( ˆCb™<Œ‘Ð@�ÈR�D‰„¡Æ@Æ1 ƒ e sÊ0d4Ût”¨÷ÜHºiYz«Åý¼&ŒŠûÈ/¯�Å~NÏ7—ri ¼w9Â)¶šÒÉ�ŠÔ:ô`èÐÎÝ´ª{èE8<¡\y¹8Ñ \½”Û¡’F‘¥ÊVçWÇýÚæÎ¸l§ žÃnÓÑ#/ršÒÌõ_€„ýOu¯ò¿2¡¤þÿJ+1¤V.Ä__ûGUÛ6ý�Ê­M.ƒ@’d@`ÙT2 +:Ä눂‡tÃÕ“ªÜå“§ ŒHʘ§:ã¶1xAgÙ=¢Ø¼:ÞíÉÞÆW¨ Î$þíBQK@Yli˜ji~\1SIê^·ÕOw ´þ$¦i»I´…�àÚˆ©¼è_ð“c-]Fh µüüt8Mýy¨ãþŽ‚ò¤fÊæ×}Nãâ�¹Å5kBÉ¡-m"/׫®H¾è4­@|“]LLT¬DN(tp / ta¼õBŸæ)|¦æ©ôºô–ÊOþ§ÓœÑ¡ž4UUÜ–m©0\äÁ>¾SokÑÅŒ^@qíæ_pq0ˉ®£¼=V� ãÃrw‚Tuô®¤(„@X9Œ0Mé‘ �&2=3šúþEÆ*HjÖ[ƒJ³hlFÛpž#é(Ua×O`D¨±±S`_:!ÜZdØf)S�°ëJ÷G.§ Œ£LHSë†ëšóÚ¤x±²š©GåûŽß±¶â„K¡Ó¸JlTùý‚¬‹&Ix¡Èû�ŒDMÏ4/%ñ«ôd•¯¬ˆ(®¾bVÅ?K½˜È�óg°{L6«I:ðŽ]KËíÕW´¤U¦CÛÞ““ZSÐÔT¥÷Üù«AXÖÀ�P¹KA Æ'µj|´‚do4õCt˜<^fƒ€ fj´®3 o%ºfU;“qž†J°”Ï-�>,u¦¬’Œ*® ØÆ£{j’ËEþ{ÚšzrStùˆíü6ùe ÿ8Q ïC¯NÊï ¼ÂAõF¦Sˆ‘vŽjaÆ]SɋĬH<)Þ¸—ë7ûj[s1f­þhWFNà¡è…�³Ç¬¥£àGŨ¹êWæ*Â]¤²µîç›ÔÕŠ«BÝ[“U'­�õ*T Ñåÿ‘nþÁ¦Ô{ɯLþM¾:ù~ò ””/R¾¦ü¢ò_å‹•ï•Ù3·l€C´ÀB/4`¥41 …+úÀC¡PôR7 + ¤õ}¥) G`Ÿ$>Þú{ Š C–/Ô¨ ŸùÐïúxÿ‡xdÇsu5Çç8>tãï[ãPg<“ñ9M8JúõÇÁÇ#VP©7^±‚NÍø‚w¸�ÙÝ9Ý•U‡‡é +þb¸lPp'K1ãáä’º;A5ŠÆAGºí.ÖÒzÙ«õwG³Ò´šõ[θ¡ÒŠ3¾¸�ƒò,œ+5—€ p¯Ïà×Eg§"n ø)]ÌH9P£ÍšÀEãˆqÑF+øÕ‰}“PJþl(8‚uI²¹Ì“OÈtT ÞQsã©7%ïm‰~‡ó>ëÆÌßÖ€‹û¤b^¥—ØýA¯šìÅF-löUíä8™5>[ToÃ�úCR¢³�Íur£`gÃðóôø©é%šƒá óžÔ"t– +Zî�QéÝŽW™¹†Dg¦N®8Q`fã®H镍:û·xÒö1± E¹j|4�fq‚nV£EE<šF�ÐE•3Š]Ÿ4#:Ñ‚hbí^!Æê/„fÂ�&Îϳª.œé6À¼’Ü_`º÷³Ñ°Þ]åÓ*óÂI}(¯uZ„–~DŽ⽑ꀦìê'&äV1VŸ¯šéÜ„q4šlbB­b3ÀwÀ0x¤Šä¶%u°ÍÐEuÑ”ÙÆD*(Tü™.C¥éˆL˜H7 Ó¡'h´.€ÊnCcð�1–`P™«+”BøHßa ÌX…ÁˆÁ3J—Ê7£ep­†±–[�˜ä¤i@mµ� µÆVã¼Ѹ:“Éʘ¦o—™ìçÁØà tƒH&\·¢Ø9^©4àéëN©�‰‡è ]cg¬¶3 í΀Î7£ÿÏ`ƒœ‘ÊÀûP�”qÑÇV˜;uÇ6ibšØ’Õƒ­{N’3A{ ÑF¹ƒøY’ +ƒ�÷…t€ÿ ìAbåozK{Ï~ÈYónæ!°DŒÌ9ˆS;k8Bsþm€QµÍII°h›w �4­CNQß ã7,&nô‚UÉÃ<K§yé@¼Ç”j„v-Á¡F숃C|†£. ®KÃóöbr¼*yaBD`¹ô(‡‘·"û�—>Ñ`GÌ·ÇIs%Jæ6�¢QùçÌ€ ëQG‡Âÿ»}V¹�õˆ*eS ¨ân/ê#ÌÞùJï–^úTOLž/§!·'kž@Ýqßϰ`Ó¦£øuU2±äZq;V¾ÜŸú3ÇÞUJÙÆ4 >0¾=�È`ð¤”I–U廑Aü½[Cd¾ÁAز‚4$ ˜í˜.²º…DíF_F�5Eb¼§Nô4ÔñÕÿ¤6cƒë͈rLxëW_ x͸B¢˜)ë�„PS`w]Ç=ÖV#ÀâÛ�±�eÒ qÆfFWß’“IÔRÁûK"A—Ù” > bT좠+’¯ê&.ê†îË/"êôTÍÈ×óßI©ß€ê¹b†îC4æ‰vNDÈ—”²ÀRœwwjвðÔS­ìd¡š(o³Å/™SÇ� 6³åJO’P«„£ö. › +ÀŸw�‹ ¦tó<Æ«WH©‰)�h}"ÌwBìn"‚ЖNp¤ÌpV +¬ðH4/¸€7L‚+ü8í¥Í´æç,†p’¹dqÕÇÿôPS¹T öÙMJÑþnÚ�¶ÞHŶ´S<`¥½ŽTp‡jø~<Œ$nŒAÔtõYŠ�¹íÊõ®ÛÛÐ}y–uŸRPÌ®½ñ­·dÝ£¦¹B2ÔgÚÑ’=ôw­öæŸ×Ó߸´oùÖ›:K\& ðo¢¤ÖŠo�@PŽ¡V¯•v•#¦äxE¾ B§ŸïÇ•N(‘’ý-²ç(«ò��ž„ÍÃÑAâÁÖºÿ±èÊKš6½F�^ô.$ö]9§;ºR‹ˆiku¦DG`eŸA§iLª!­Â@£7w’Ì¥Žz@8Biàû¸ÆKöK•D*ÑôÕu´¼†ÐÝÆŽÍìvS•lƒ€Ê(/ãY’ÿ¿‘ž€]çâÉÆÕšW×Y'âm$nf: &Â$%RØu*¤ÏÙ6û½a o¬3ÖŸxñªC…ÅßCÑQP'î£w‘�5ž¸ñ-ç?'ÞW¯ÈâHDeâË7†ÒW0Q+O›8>ñ-~a}•ø{Õ%P=²øaû¿d¹k´ 5Ã\D¤ªò&Î&�‚ëÿÄ£|`"“ºco‚´–Ou¹Å%é„T뉯ìåx«s°¸r‘ZÝêÈiü�쌱Øâsº8™^vgñÓG„0¯éÖÚoq ‚RT ‚ljSƒjñx�rìhÙÝÃ=7FJ·ˆ•Ø]Bªu¸(RC8]ò�¿00_ÓÕÔ»E/œÂë[°pèG¹[­µ²Hý.[°æ}Ü‹ÝÌò“ X ‘ÌòßñÜ8[i`´•ÂF§¥ã©·ŒÒ!»U´.t¶Û+…´j©»(ã­KÄ£æò�™˜Œá3·™E^>ë`‹ÔÚîÊÚp,¤†eÓ³òÁ ?|¾$À’)ÎÎV’¥„¥Ÿw,›?íÓ@iÙjøª¸ëu›•Bë“lWmâŸmë° +‡I65AjÐaE)m>XFw—ÀŒbC/+Ý»ÙaN‡¿{ ôùð%Àp)¼‹BLŽnWŽŸ¥‹À-%W"Q©šÞ€‚p£‹@”°È—¢Ö{ �ï EÙø�º€9ænk ¶†ü$”’Ô-ÚvÇgÁB’X±5d‘4_˜·§\œ’P!¿Óvèlƒ¬úLÛ-¬B’Œ]™`€"r˜àÐ4 þÇÈ|2œðƒÔöÔ3ìT£ÃÓGÅŸŸ ݨƕy­Â¢Ç§ÛeÁ€Á¾¶d€F ¿½²®Þš³°OvH`ý9ž)®î9ž`~!kбÕÜ[‘ˆ¥»ØC@)ã¥ö†8f� +’ƒ€?>/4¢p,²ða$ð8rzЈ¬* /H¹ÛEÔé\5Š*¯’yv«‰BJ%Mwë•¶�zñ‡Új¤†Æ6X'+WÍÅü(݆¯1ÍäOƒ€x|€÷Têãͺ±œMt’ÉêøŠy›’Ù°jÀ‡ä^ÍC ŽAÝ!>Û0UŽì¥Ü„ Àß.'!O~…¡·Ö�!¾ºní@ðî®káÜÂW­+‰ln’`'Á�1[…€Äzú~5™IŽÛ °ÞÖ¼=]�-qŒÊØYúD —¡ Ü oÏJú}x¹áûohè*0S߆wY]’Ç>L†5€Ñ™xÁ"hÈwhì ÛŠ_—&r >qÈŽü”yt ¯O™{æÔ G’ui¢â»jÀ\€Žv +¯Ì‰ùǃ z_‹ 0O”f8F…Â/ +�3¬X»×FFR�°‚ìˆ�@¥(t’?ŸTÒy}oòì_’€t�â?@½ÊvºJ"ÏÑýøÆ­æ›±† 0ÕÃó`ZþS��ý‡#”žm«˜ 8-¼A®)öÈÜøÔú½�åP¡Ù»½ŒŸ#ã­q›(HÒU~Äa§Õc0ôrrWR`¦iÁ;l �´¤‚ynw3þx¯’ÓØâm,¼­“û\k ÝLïÉéàfâxÄräàëÆ +endstream endobj 6 0 obj <> endobj 7 0 obj <> endobj 8 0 obj <> endobj 12 0 obj <> endobj 27 0 obj [/View/Design] endobj 28 0 obj <>>> endobj 25 0 obj [/View/Design] endobj 26 0 obj <>>> endobj 23 0 obj [/View/Design] endobj 24 0 obj <>>> endobj 21 0 obj [/View/Design] endobj 22 0 obj <>>> endobj 16 0 obj <> endobj 9 0 obj <> endobj 10 0 obj <> endobj 11 0 obj <> endobj 33 0 obj [/View/Design] endobj 34 0 obj <>>> endobj 31 0 obj [/View/Design] endobj 32 0 obj <>>> endobj 29 0 obj [/View/Design] endobj 30 0 obj <>>> endobj 13 0 obj [12 0 R 11 0 R 10 0 R 9 0 R 8 0 R 7 0 R 6 0 R] endobj 35 0 obj <> endobj xref +0 36 +0000000000 65535 f 0000000016 00000 n -0000000194 00000 n -0000017644 00000 n +0000000228 00000 n +0000380500 00000 n 0000000000 00000 f -0000017695 00000 n -0000000000 00000 f -0000019829 00000 n -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000000000 00000 f -0000019896 00000 n -0000020037 00000 n -0000021244 00000 n -0000000000 00000 f -0000018975 00000 n -0000019043 00000 n -0000019114 00000 n -0000019184 00000 n -0000049195 00000 n -0000018065 00000 n -0000019716 00000 n -0000018249 00000 n -0000018414 00000 n -0000018462 00000 n -0000019600 00000 n -0000019631 00000 n -0000019484 00000 n -0000019515 00000 n -0000019368 00000 n -0000019399 00000 n -0000019252 00000 n -0000019283 00000 n -0000049241 00000 n -trailer <<8DE991F14EB643839AD8BD996A30681D>]>> startxref 49429 %%EOF \ No newline at end of file +0000380551 00000 n +0000439254 00000 n +0000439321 00000 n +0000439391 00000 n +0000440105 00000 n +0000440172 00000 n +0000440243 00000 n +0000439460 00000 n +0000440661 00000 n +0000380925 00000 n +0000381119 00000 n +0000439992 00000 n +0000381184 00000 n +0000381258 00000 n +0000381399 00000 n +0000382960 00000 n +0000439876 00000 n +0000439907 00000 n +0000439760 00000 n +0000439791 00000 n +0000439644 00000 n +0000439675 00000 n +0000439528 00000 n +0000439559 00000 n +0000440545 00000 n +0000440576 00000 n +0000440429 00000 n +0000440460 00000 n +0000440313 00000 n +0000440344 00000 n +0000440724 00000 n +trailer +<<752461CC0A187547935702C929B19A73>]>> +startxref +440922 +%%EOF diff --git a/docs/scripts/writeIconDetails.mts b/docs/scripts/writeIconDetails.mts index 436b05e2e..d3f44a96a 100644 --- a/docs/scripts/writeIconDetails.mts +++ b/docs/scripts/writeIconDetails.mts @@ -27,12 +27,13 @@ import iconNode from '../iconNodes/${iconName}.node.json' with { type: 'json' }; import metaData from '../../../../icons/${iconName}.json' with { type: 'json' }; import releaseData from '../releaseMetadata/${iconName}.json' with { type: 'json' }; import popularity from '../iconPopularity/${iconName}.json' with { type: 'json' }; +import { IconMetaData, IconNode } from '~/.vitepress/theme/types'; -const { tags, categories, contributors, aliases, deprecated, deprecationReason, toBeRemovedInVersion } = metaData; +const { tags, categories, contributors, aliases, deprecated, deprecationReason, toBeRemovedInVersion }: IconMetaData = metaData; const iconDetails = { name: '${iconName}', - iconNode, + iconNode: iconNode as IconNode, contributors, tags, categories, diff --git a/docs/scripts/writeIconNodes.mts b/docs/scripts/writeIconNodes.mts index 4e8ea8316..f6261a90b 100644 --- a/docs/scripts/writeIconNodes.mts +++ b/docs/scripts/writeIconNodes.mts @@ -20,6 +20,7 @@ if (!fs.existsSync(iconNodesDirectory)) { const iconIndexFile = path.resolve(iconNodesDirectory, `index.ts`); const iconIndexFileImports = []; +const iconIndexFileNodes = []; const iconIndexFileExports = []; const iconIndexFileDefaultExports = []; @@ -32,7 +33,10 @@ const writeIconFiles = Object.entries(icons).map(async ([iconName, { children }] await fs.promises.writeFile(location, output, 'utf-8'); iconIndexFileImports.push( - `import ${toCamelCase(iconName)}Node from './${iconName}.node.json' with { type: "json" };`, + `import ${toCamelCase(iconName)}Json from './${iconName}.node.json' with { type: "json" };`, + ); + iconIndexFileNodes.push( + `const ${toCamelCase(iconName)}Node = ${toCamelCase(iconName)}Json as IconNode;`, ); iconIndexFileExports.push(` ${toCamelCase(iconName)}Node as ${toCamelCase(iconName)},`); iconIndexFileDefaultExports.push(` '${iconName}': ${toCamelCase(iconName)}Node,`); @@ -43,16 +47,19 @@ try { await fs.promises.writeFile( iconIndexFile, `\ +import type { IconNode } from '../../theme/types'; ${iconIndexFileImports.join('\n')} +${iconIndexFileNodes.join('\n')} + export { ${iconIndexFileExports.join('\n')} } export default { ${iconIndexFileDefaultExports.join('\n')} -} - `, +} satisfies Record; +`, 'utf-8', ); diff --git a/docs/scripts/writeLabIconNodes.mts b/docs/scripts/writeLabIconNodes.mts index 92c010e58..df8565d43 100644 --- a/docs/scripts/writeLabIconNodes.mts +++ b/docs/scripts/writeLabIconNodes.mts @@ -20,6 +20,7 @@ if (!fs.existsSync(iconNodesDirectory)) { const iconIndexFile = path.resolve(iconNodesDirectory, `index.ts`); const iconIndexFileImports: string[] = []; +const iconIndexFileNodes: string[] = []; const iconIndexFileExports: string[] = []; const iconIndexFileDefaultExports: string[] = []; @@ -32,7 +33,10 @@ const writeIconFiles = Object.entries(icons).map(async ([iconName, { children }] await fs.promises.writeFile(location, output, 'utf-8'); iconIndexFileImports.push( - `import ${toCamelCase(iconName)}Node from './${iconName}.node.json' with { type: "json" };`, + `import ${toCamelCase(iconName)}Json from './${iconName}.node.json' with { type: "json" };`, + ); + iconIndexFileNodes.push( + `const ${toCamelCase(iconName)}Node = ${toCamelCase(iconName)}Json as IconNode;`, ); iconIndexFileExports.push(` ${toCamelCase(iconName)}Node as ${toCamelCase(iconName)},`); iconIndexFileDefaultExports.push(` '${iconName}': ${toCamelCase(iconName)}Node,`); @@ -43,16 +47,19 @@ try { await fs.promises.writeFile( iconIndexFile, `\ +import type { IconNode } from '../../../theme/types'; ${iconIndexFileImports.join('\n')} +${iconIndexFileNodes.join('\n')} + export { ${iconIndexFileExports.join('\n')} } export default { ${iconIndexFileDefaultExports.join('\n')} -} - `, +} satisfies Record; +`, 'utf-8', ); diff --git a/docs/scripts/writeReleaseMetadata.mts b/docs/scripts/writeReleaseMetadata.mts index 36269e512..4c5a2a5c8 100644 --- a/docs/scripts/writeReleaseMetadata.mts +++ b/docs/scripts/writeReleaseMetadata.mts @@ -40,7 +40,21 @@ const fetchAllReleases = async () => { ); }; +const getIconNamesAtTag = async (tag: string) => { + // --full-tree resolves `icons/` from the repo root instead of the cwd (docs/) + const files = await git.raw(['ls-tree', '--full-tree', '--name-only', tag, 'icons/']); + + return new Set( + files + .split('\n') + .filter((file) => file.endsWith('.svg')) + .map((file) => path.basename(file, '.svg')), + ); +}; + const tags = await fetchAllReleases(); +const forkIconNames = await getIconNamesAtTag(tags[0]); +const releasedIconNames = await getIconNamesAtTag(tags[tags.length - 1]); const comparisonsPromises = tags.map(async (tag, index) => { const previousTag = tags[index - 1]; @@ -150,19 +164,26 @@ try { const iconName = path.basename(iconJsonFile, '.json'); const metaDir = path.resolve(releaseMetaDataDirectory, `${iconName}.json`); - if (!(iconName in newReleaseMetaData)) { + const metaData = await fs.promises.readFile(path.join(ICONS_DIR, iconJsonFile), 'utf-8'); + const iconMetaData = JSON.parse(metaData); + const aliases = iconMetaData.aliases ?? []; + + const fromFork = [iconName, ...aliases.map((alias) => alias.name)].some((name) => + forkIconNames.has(name), + ); + const awaitingRelease = !releasedIconNames.has(iconName); + + if (!(iconName in newReleaseMetaData) && !fromFork && !awaitingRelease) { console.error(`Could not find release metadata for icon '${iconName}'.`); } const contents = { + fromFork, + awaitingRelease, ...defaultReleaseMetaData, ...(newReleaseMetaData[iconName] ?? {}), }; - const metaData = await fs.promises.readFile(path.join(ICONS_DIR, iconJsonFile), 'utf-8'); - const iconMetaData = JSON.parse(metaData); - const aliases = iconMetaData.aliases ?? []; - if (aliases.length) { aliases.forEach((alias) => { if (!(alias.name in newReleaseMetaData)) { diff --git a/docs/showcase.md b/docs/showcase.md index c8884e6ff..3a7c2ea15 100644 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -5,8 +5,8 @@ outline: 2 outlineTitle: Showcase ---
diff --git a/docs/tsconfig.json b/docs/tsconfig.json index f786c0c16..432829d4a 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -7,8 +7,9 @@ "allowImportingTsExtensions": true, "allowSyntheticDefaultImports": true, "noEmit": true, + "skipLibCheck": true, "paths": { - "~/.vitepress/*": ["./.vitepress/*"], - }, - }, + "~/.vitepress/*": ["./.vitepress/*"] + } + } } diff --git a/docs/vercel.json b/docs/vercel.json index 0160d6dbd..268595d6c 100644 --- a/docs/vercel.json +++ b/docs/vercel.json @@ -13,7 +13,7 @@ }, { "source": "/docs/:slug(lucide-?.*)", - "destination": "/guide/packages/:slug", + "destination": "/guide/:slug", "permanent": true }, { @@ -42,9 +42,14 @@ "destination": "/guide/react/basics", "permanent": true }, + { + "source": "/guide/accessibility", + "destination": "/how-to/accessibility", + "permanent": true + }, { "source": "/guide/advanced/accessibility", - "destination": "/guide/accessibility", + "destination": "/how-to/accessibility", "permanent": true }, { @@ -58,54 +63,44 @@ "permanent": true }, { - "source": "/guide/packages/lucide-react", - "destination": "/guide/react", + "source": "/guide/packages/lucide-:package", + "destination": "/guide/:package", "permanent": true }, { - "source": "/guide/packages/lucide-vue-next", - "destination": "/guide/vue", - "permanent": true - }, - { - "source": "/guide/packages/lucide-vue", - "destination": "/guide/vue", - "permanent": true - }, - { - "source": "/guide/packages/lucide-svelte", - "destination": "/guide/svelte", - "permanent": true - }, - { - "source": "/guide/packages/lucide-solid", - "destination": "/guide/solid", - "permanent": true - }, - { - "source": "/guide/packages/lucide-angular", - "destination": "/guide/angular", - "permanent": true - }, - { - "source": "/guide/packages/lucide-preact", - "destination": "/guide/preact", - "permanent": true - }, - { - "source": "/guide/packages/lucide-astro", - "destination": "/guide/astro", - "permanent": true - }, - { - "source": "/guide/packages/lucide-static", - "destination": "/guide/static", + "source": "/guide/packages/:package", + "destination": "/guide/:package", "permanent": true }, { "source": "/contributing", "destination": "/contribute", "permanent": true + }, + { + "source": "/contribute/icon-design-guide", + "destination": "/contribute/icons/design-principles", + "permanent": true + }, + { + "source": "/contribute/illustrator-guide", + "destination": "/contribute/icons/illustrator-guide", + "permanent": true + }, + { + "source": "/contribute/inkscape-guide", + "destination": "/contribute/icons/inkscape-guide", + "permanent": true + }, + { + "source": "/contribute/figma-guide", + "destination": "/contribute/icons/figma-guide", + "permanent": true + }, + { + "source": "/contribute/affinity-designer-guide", + "destination": "/contribute/icons/affinity-designer-guide", + "permanent": true } ], "headers": [ diff --git a/eslint.config.js b/eslint.config.js index d96164b25..f88f795d6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -34,6 +34,7 @@ export default defineConfig([ 'docs/**/examples/', 'docs/.vitepress/theme/components/editors/preact/index.js', 'packages/svelte/.svelte-kit', + 'integrations/**/file-routes.d.ts', // Tracked in git despite matching a .gitignore pattern, so lint it. '!packages/lucide-react/dynamicIconImports.mjs', ], diff --git a/icon.schema.json b/icon.schema.json index 4bf4a0714..38ea133f7 100644 --- a/icon.schema.json +++ b/icon.schema.json @@ -141,7 +141,7 @@ "$defs": { "iconDeprecationReasons": { "type": "string", - "enum": ["icon.renamed"] + "enum": ["icon.design", "icon.use-case"] }, "aliasDeprecationReasons": { "type": "string", diff --git a/icons/activity.json b/icons/activity.json index ad8fd4a96..b69d29551 100644 --- a/icons/activity.json +++ b/icons/activity.json @@ -22,7 +22,7 @@ "hospital", "defibrillator", "earthquake", - "siesmic", + "seismic", "magnitude", "richter scale", "aftershock", diff --git a/icons/armenian-dram.json b/icons/armenian-dram.json new file mode 100644 index 000000000..80207d6d2 --- /dev/null +++ b/icons/armenian-dram.json @@ -0,0 +1,21 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "Elawphant" + ], + "use-cases": [ + "Representing money or prices in Armenian dram" + ], + "tags": [ + "finance", + "symbol", + "banking", + "economy", + "currency", + "money", + "payment" + ], + "categories": [ + "finance" + ] +} diff --git a/icons/album.svg b/icons/armenian-dram.svg similarity index 65% rename from icons/album.svg rename to icons/armenian-dram.svg index 68c07668c..035f41aad 100644 --- a/icons/album.svg +++ b/icons/armenian-dram.svg @@ -9,6 +9,7 @@ stroke-linecap="round" stroke-linejoin="round" > - - + + + diff --git a/icons/badge-swiss-franc.json b/icons/badge-swiss-franc.json index e3df7c78f..8cae15f70 100644 --- a/icons/badge-swiss-franc.json +++ b/icons/badge-swiss-franc.json @@ -3,6 +3,8 @@ "contributors": [ "danielbayley" ], + "deprecated": true, + "deprecationReason": "icon.use-case", "use-cases": [], "tags": [ "discount", diff --git a/icons/bangladeshi-taka.json b/icons/bangladeshi-taka.json new file mode 100644 index 000000000..3b19ab093 --- /dev/null +++ b/icons/bangladeshi-taka.json @@ -0,0 +1,31 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "TanvirMahin24", + "karsa-mistmere" + ], + "use-cases": [ + "Showing product prices in BDT", + "Selecting BDT in multi-currency interfaces", + "Showing BDT exchange rates in currency converters", + "Indicating BDT amounts in invoices and payments" + ], + "tags": [ + "currency", + "money", + "payment", + "bdt", + "৳", + "currencysymbol", + "cash", + "banknote", + "price", + "remittance", + "ecommerce" + ], + "categories": [ + "finance", + "shopping", + "travel" + ] +} diff --git a/icons/book-marked.svg b/icons/bangladeshi-taka.svg similarity index 60% rename from icons/book-marked.svg rename to icons/bangladeshi-taka.svg index 4ba30e643..74b5c9825 100644 --- a/icons/book-marked.svg +++ b/icons/bangladeshi-taka.svg @@ -9,6 +9,6 @@ stroke-linecap="round" stroke-linejoin="round" > - - + + diff --git a/icons/book-marked.json b/icons/book-bookmark.json similarity index 82% rename from icons/book-marked.json rename to icons/book-bookmark.json index c3eb07d50..a92736ab2 100644 --- a/icons/book-marked.json +++ b/icons/book-bookmark.json @@ -1,7 +1,8 @@ { "$schema": "../icon.schema.json", "contributors": [ - "danielbayley" + "danielbayley", + "karsa-mistmere" ], "use-cases": [], "tags": [ @@ -49,5 +50,12 @@ "text", "development", "gaming" + ], + "aliases": [ + { + "name": "book-marked", + "deprecated": true, + "deprecationReason": "alias.name" + } ] } diff --git a/icons/book-bookmark.svg b/icons/book-bookmark.svg new file mode 100644 index 000000000..1bbe99fd1 --- /dev/null +++ b/icons/book-bookmark.svg @@ -0,0 +1,14 @@ + + + + diff --git a/icons/book-heart.json b/icons/book-heart.json index ad4569669..97265e466 100644 --- a/icons/book-heart.json +++ b/icons/book-heart.json @@ -24,7 +24,7 @@ "teens", "teenager", "therapy", - "theraputic", + "therapeutic", "therapist", "planner", "organizer", diff --git a/icons/bridge.json b/icons/bridge.json new file mode 100644 index 000000000..12a98fd3d --- /dev/null +++ b/icons/bridge.json @@ -0,0 +1,36 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "Nykoula", + "jamiemlaw" + ], + "use-cases": [ + "Representing crossing infrastructure in navigation apps", + "Marking river crossings or connecting routes on maps", + "Showing clearance-limited transit zones in maritime dashboards" + ], + "tags": [ + "overpass", + "cross over", + "traverse", + "viaduct", + "suspension", + "infrastructure", + "transportation", + "crossing", + "road", + "river", + "gate", + "golden", + "travel", + "landmark", + "route", + "maritime", + "clearance" + ], + "categories": [ + "travel", + "transportation", + "buildings" + ] +} diff --git a/icons/bridge.svg b/icons/bridge.svg new file mode 100644 index 000000000..76ee1f838 --- /dev/null +++ b/icons/bridge.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/icons/briefcase-plus.json b/icons/briefcase-plus.json new file mode 100644 index 000000000..95a18060d --- /dev/null +++ b/icons/briefcase-plus.json @@ -0,0 +1,39 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "colebemis", + "csandman", + "ericfennis", + "tylerkade", + "jguddas" + ], + "use-cases": [ + "Indicating you're adding luggage", + "adding a new business or company profile", + "creating or saving a professional portfolio", + "adding a job or role to a user's profile", + "indicating add to briefcase or save for later in a product/service listing" + ], + "tags": [ + "work", + "bag", + "baggage", + "folder", + "new", + "add", + "create", + "increase", + "briefcase", + "portfolio", + "business", + "career", + "employment", + "professional", + "plus" + ], + "categories": [ + "transportation", + "files", + "account" + ] +} diff --git a/icons/briefcase-plus.svg b/icons/briefcase-plus.svg new file mode 100644 index 000000000..256f2eae1 --- /dev/null +++ b/icons/briefcase-plus.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/icons/broccoli.json b/icons/broccoli.json index 548817a95..2f92cb6a8 100644 --- a/icons/broccoli.json +++ b/icons/broccoli.json @@ -3,7 +3,8 @@ "contributors": [ "rrod497", "jamiemlaw", - "karsa-mistmere" + "karsa-mistmere", + "jguddas" ], "use-cases": [], "tags": [ diff --git a/icons/broccoli.svg b/icons/broccoli.svg index 2bdd23fc8..ca1efeb08 100644 --- a/icons/broccoli.svg +++ b/icons/broccoli.svg @@ -9,8 +9,8 @@ stroke-linecap="round" stroke-linejoin="round" > - - - - + + + + diff --git a/icons/building-complex-plus.json b/icons/building-complex-plus.json new file mode 100644 index 000000000..7a24b02c5 --- /dev/null +++ b/icons/building-complex-plus.json @@ -0,0 +1,40 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "maxim-s-barabash", + "ericfennis", + "karsa-mistmere", + "jguddas", + "tylerkade" + ], + "use-cases": [ + "Adding a company or business profile", + "Registering a company during onboarding", + "Creating office, branch, or headquarters entries", + "Marking new business or property locations on maps" + ], + "tags": [ + "business", + "company", + "enterprise", + "skyscraper", + "organisation", + "organization", + "city", + "new", + "add", + "create", + "increase", + "office", + "headquarters", + "startup", + "registration", + "onboarding", + "realestate", + "property" + ], + "categories": [ + "account", + "buildings" + ] +} diff --git a/icons/building-complex-plus.svg b/icons/building-complex-plus.svg new file mode 100644 index 000000000..b9657a181 --- /dev/null +++ b/icons/building-complex-plus.svg @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/icons/building-2.json b/icons/building-complex.json similarity index 73% rename from icons/building-2.json rename to icons/building-complex.json index 48212389b..3d3854466 100644 --- a/icons/building-2.json +++ b/icons/building-complex.json @@ -19,5 +19,12 @@ "categories": [ "account", "buildings" + ], + "aliases": [ + { + "name": "building-2", + "deprecated": true, + "deprecationReason": "alias.name" + } ] } diff --git a/icons/building-2.svg b/icons/building-complex.svg similarity index 100% rename from icons/building-2.svg rename to icons/building-complex.svg diff --git a/icons/calendar-chevrons-right.json b/icons/calendar-chevrons-right.json new file mode 100644 index 000000000..6f33edac7 --- /dev/null +++ b/icons/calendar-chevrons-right.json @@ -0,0 +1,37 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "karsa-mistmere", + "ericfennis", + "jguddas", + "colebemis", + "AlexandrePhilibert" + ], + "use-cases": [ + "Indicating upcoming events in calendars", + "Fast-forwarding through calendar views" + ], + "tags": [ + "navigation", + "arrow", + "right", + "chevron", + "direction", + "symbol", + "pointer", + "indicator", + "next", + "menu", + "date", + "day", + "month", + "year", + "events", + "chevrons" + ], + "categories": [ + "time", + "arrows", + "navigation" + ] +} diff --git a/icons/calendar-chevrons-right.svg b/icons/calendar-chevrons-right.svg new file mode 100644 index 000000000..d703bb8c9 --- /dev/null +++ b/icons/calendar-chevrons-right.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/icons/can-soda.json b/icons/can-soda.json new file mode 100644 index 000000000..6568e648f --- /dev/null +++ b/icons/can-soda.json @@ -0,0 +1,25 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "karsa-mistmere", + "jaynewey" + ], + "use-cases": [ + "representing canned drinks like soda or beer", + "representing canned foods like soup or legumes", + "representing litter/waste" + ], + "tags": [ + "beverage", + "drink", + "food", + "soda", + "tin", + "beer", + "alcohol", + "waste" + ], + "categories": [ + "food-beverage" + ] +} diff --git a/icons/can-soda.svg b/icons/can-soda.svg new file mode 100644 index 000000000..a28e13766 --- /dev/null +++ b/icons/can-soda.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/icons/can.json b/icons/can.json new file mode 100644 index 000000000..99253ebf8 --- /dev/null +++ b/icons/can.json @@ -0,0 +1,28 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "l0uisgrange", + "karsa-mistmere" + ], + "use-cases": [ + "Representing canned food in grocery or delivery apps", + "Labelling pantry items or nonperishable inventory", + "Indicating metal recycling or recyclable containers", + "Denoting canned ingredients in recipe or meal-planning interfaces" + ], + "tags": [ + "food", + "storage", + "groceries", + "tin", + "metal", + "recyclable", + "container", + "pantry", + "grocery", + "preserved" + ], + "categories": [ + "food-beverage" + ] +} diff --git a/icons/can.svg b/icons/can.svg new file mode 100644 index 000000000..3a984f3c8 --- /dev/null +++ b/icons/can.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/icons/card-sim.svg b/icons/card-sim.svg index 58e8cdbac..833be445d 100644 --- a/icons/card-sim.svg +++ b/icons/card-sim.svg @@ -12,5 +12,5 @@ - + diff --git a/icons/carton-off.json b/icons/carton-off.json new file mode 100644 index 000000000..2afd61f7f --- /dev/null +++ b/icons/carton-off.json @@ -0,0 +1,39 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "danielbayley", + "karsa-mistmere" + ], + "use-cases": [ + "Marking lactose-free or non-dairy beverages", + "Indicating unavailable packaged drinks in menus", + "Showing restricted dairy products in diet filters", + "Representing excluded milk products for allergies" + ], + "tags": [ + "milk", + "dairy", + "beverage", + "drink", + "diet", + "lactose free", + "non dairy", + "liquid", + "liter", + "litre", + "pint", + "pints", + "pasteurized", + "skim", + "skimmed", + "container", + "packaging", + "allergy", + "intolerance", + "vegan", + "unavailable" + ], + "categories": [ + "food-beverage" + ] +} diff --git a/icons/carton-off.svg b/icons/carton-off.svg new file mode 100644 index 000000000..de1ea8466 --- /dev/null +++ b/icons/carton-off.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/icons/carton.json b/icons/carton.json new file mode 100644 index 000000000..02b14de1c --- /dev/null +++ b/icons/carton.json @@ -0,0 +1,38 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "danielbayley", + "karsa-mistmere" + ], + "use-cases": [ + "Representing milk or dairy products", + "Showing packaged beverages in grocery apps", + "Marking liquid containers in inventory systems", + "Indicating packaged drink sizes in food catalogs" + ], + "tags": [ + "milk", + "dairy", + "beverage", + "drink", + "diet", + "lactose", + "liquid", + "liter", + "litre", + "pint", + "pints", + "pasteurized", + "skim", + "skimmed", + "container", + "packaging", + "grocery", + "juice", + "cream", + "cow" + ], + "categories": [ + "food-beverage" + ] +} diff --git a/lab/carton.svg b/icons/carton.svg similarity index 58% rename from lab/carton.svg rename to icons/carton.svg index 51be0a72b..f1d3fd123 100644 --- a/lab/carton.svg +++ b/icons/carton.svg @@ -9,7 +9,6 @@ stroke-linecap="round" stroke-linejoin="round" > - - + diff --git a/icons/circle-dashed-check.json b/icons/circle-dashed-check.json new file mode 100644 index 000000000..8966551fb --- /dev/null +++ b/icons/circle-dashed-check.json @@ -0,0 +1,38 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "colebemis", + "danielbayley", + "jguddas", + "karsa-mistmere" + ], + "use-cases": [ + "Indicating approved items with pending changes", + "Showing published schedules with unpublished updates", + "Marking requests that require re-approval", + "Confirming completion with revisions still pending" + ], + "tags": [ + "approved", + "pending", + "changes", + "revision", + "reapproval", + "published", + "schedule", + "assignment", + "request", + "review", + "progress", + "issue", + "draft", + "code", + "coding", + "version control" + ], + "categories": [ + "shapes", + "development", + "notifications" + ] +} diff --git a/icons/circle-dashed-check.svg b/icons/circle-dashed-check.svg new file mode 100644 index 000000000..38aa8695b --- /dev/null +++ b/icons/circle-dashed-check.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/icons/clef-alto.json b/icons/clef-alto.json new file mode 100644 index 000000000..52bea87f5 --- /dev/null +++ b/icons/clef-alto.json @@ -0,0 +1,30 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "rrod497" + ], + "use-cases": [ + "Labeling alto clef notation in sheet music editors", + "Representing viola parts in music composition or practice apps", + "Indicating C clef selection in notation software", + "Supporting clef choices in MIDI editors, instrument emulators, or music education tools" + ], + "tags": [ + "do", + "c", + "music", + "note", + "key", + "soprano", + "mezzo", + "alto", + "staff", + "pitch", + "baritone", + "major", + "scale" + ], + "categories": [ + "multimedia" + ] +} diff --git a/lab/houses.svg b/icons/clef-alto.svg similarity index 57% rename from lab/houses.svg rename to icons/clef-alto.svg index 95b34ebcf..36ca98d8b 100644 --- a/lab/houses.svg +++ b/icons/clef-alto.svg @@ -9,6 +9,7 @@ stroke-linecap="round" stroke-linejoin="round" > - - + + + diff --git a/icons/clef-bass.json b/icons/clef-bass.json new file mode 100644 index 000000000..7c543416d --- /dev/null +++ b/icons/clef-bass.json @@ -0,0 +1,29 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "rrod497" + ], + "use-cases": [ + "Labeling bass clef notation in sheet music editors", + "Representing bass, cello, trombone, tuba, or lower-pitched instrument parts", + "Indicating left-hand piano notation in music learning apps", + "Supporting clef choices in MIDI editors, instrument emulators, or music education tools" + ], + "tags": [ + "fa", + "f", + "music", + "note", + "key", + "baritone", + "staff", + "pitch", + "bass", + "subbass", + "major", + "scale" + ], + "categories": [ + "multimedia" + ] +} diff --git a/icons/clef-bass.svg b/icons/clef-bass.svg new file mode 100644 index 000000000..565548ae9 --- /dev/null +++ b/icons/clef-bass.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/icons/clef-treble.json b/icons/clef-treble.json new file mode 100644 index 000000000..38388c3e0 --- /dev/null +++ b/icons/clef-treble.json @@ -0,0 +1,29 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "rrod497" + ], + "use-cases": [ + "Representing music, songs, or audio content with a widely recognized symbol", + "Labeling sheet music, notation tools, or music education resources", + "Indicating melody, vocals, or higher-pitched instrument parts", + "Representing music apps, playlists, concerts, or performance features" + ], + "tags": [ + "sol", + "g", + "music", + "note", + "key", + "treble", + "staff", + "pitch", + "violin", + "solfege", + "major", + "scale" + ], + "categories": [ + "multimedia" + ] +} diff --git a/icons/clef-treble.svg b/icons/clef-treble.svg new file mode 100644 index 000000000..a96b7b767 --- /dev/null +++ b/icons/clef-treble.svg @@ -0,0 +1,13 @@ + + + diff --git a/icons/computer.json b/icons/computer.json index 3aa7e2173..ab75ca72f 100644 --- a/icons/computer.json +++ b/icons/computer.json @@ -1,7 +1,8 @@ { "$schema": "../icon.schema.json", "contributors": [ - "danielbayley" + "danielbayley", + "karsa-mistmere" ], "use-cases": [], "tags": [ diff --git a/icons/computer.svg b/icons/computer.svg index 707a77e3b..f86576fb4 100644 --- a/icons/computer.svg +++ b/icons/computer.svg @@ -9,8 +9,9 @@ stroke-linecap="round" stroke-linejoin="round" > - - - + + + + diff --git a/icons/cookie.json b/icons/cookie.json index fa1cf6adc..6499aa77f 100644 --- a/icons/cookie.json +++ b/icons/cookie.json @@ -2,14 +2,28 @@ "$schema": "../icon.schema.json", "contributors": [ "it-is-not", - "ericfennis" + "ericfennis", + "karsa-mistmere" + ], + "use-cases": [ + "Managing cookie consent or privacy preferences", + "Representing website cookies and browser storage", + "Indicating cookie settings in privacy or security interfaces", + "Representing cookies as food in recipes, menus or food-related interfaces" ], - "use-cases": [], "tags": [ "biscuit", + "snack", + "dessert", + "food", + "sweet", + "bakery", + "consent", "privacy", "legal", - "food" + "tracking", + "browser", + "website" ], "categories": [ "account", diff --git a/icons/cookie.svg b/icons/cookie.svg index 1bdedba5e..6abf7ca2d 100644 --- a/icons/cookie.svg +++ b/icons/cookie.svg @@ -9,10 +9,13 @@ stroke-linecap="round" stroke-linejoin="round" > - - - - - - + + + + + + + + + diff --git a/icons/credit-card-check.svg b/icons/credit-card-check.svg index 40490238b..fa53dd6b7 100644 --- a/icons/credit-card-check.svg +++ b/icons/credit-card-check.svg @@ -10,6 +10,7 @@ stroke-linejoin="round" > - + + diff --git a/icons/credit-card-minus.svg b/icons/credit-card-minus.svg index f5e258ef4..33884553b 100644 --- a/icons/credit-card-minus.svg +++ b/icons/credit-card-minus.svg @@ -9,7 +9,8 @@ stroke-linecap="round" stroke-linejoin="round" > - - + + + diff --git a/icons/credit-card-plus.svg b/icons/credit-card-plus.svg index 225512ee8..39f59d6ea 100644 --- a/icons/credit-card-plus.svg +++ b/icons/credit-card-plus.svg @@ -9,8 +9,9 @@ stroke-linecap="round" stroke-linejoin="round" > + + + - - diff --git a/icons/credit-card-reader.json b/icons/credit-card-reader.json new file mode 100644 index 000000000..1ba7815eb --- /dev/null +++ b/icons/credit-card-reader.json @@ -0,0 +1,38 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "jguddas", + "karsa-mistmere" + ], + "use-cases": [ + "Processing card payments at checkout terminals", + "Locating ATMs or payment kiosks on maps", + "Indicating card-present transactions in finance apps", + "Showing contactless or chip card payment options" + ], + "tags": [ + "bank", + "purchase", + "payment", + "cc", + "atm", + "terminal", + "checkout", + "kiosk", + "pos", + "point of sale", + "transaction", + "contactless", + "chip", + "swipe", + "tap", + "banking", + "finance", + "retail" + ], + "categories": [ + "finance", + "navigation", + "shopping" + ] +} diff --git a/icons/credit-card-reader.svg b/icons/credit-card-reader.svg new file mode 100644 index 000000000..e0110e30c --- /dev/null +++ b/icons/credit-card-reader.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/icons/credit-card-x.svg b/icons/credit-card-x.svg index d430706e0..a346dbd4f 100644 --- a/icons/credit-card-x.svg +++ b/icons/credit-card-x.svg @@ -10,7 +10,8 @@ stroke-linejoin="round" > - + + diff --git a/icons/credit-card.svg b/icons/credit-card.svg index f1476ca74..623f5c0eb 100644 --- a/icons/credit-card.svg +++ b/icons/credit-card.svg @@ -11,4 +11,5 @@ > + diff --git a/icons/cupcake.json b/icons/cupcake.json new file mode 100644 index 000000000..41cd43ab3 --- /dev/null +++ b/icons/cupcake.json @@ -0,0 +1,27 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "AnnaSasDev", + "briz123", + "ajokt123", + "rrod497", + "karsa-mistmere" + ], + "use-cases": [ + "Representing cupcakes in bakery product catalogs", + "Filtering dessert options in food ordering apps", + "Marking treats for parties or celebrations" + ], + "tags": [ + "cake", + "dessert", + "food", + "celebration", + "sweet", + "baking", + "cooking" + ], + "categories": [ + "food-beverage" + ] +} diff --git a/icons/cupcake.svg b/icons/cupcake.svg new file mode 100644 index 000000000..3680d26ba --- /dev/null +++ b/icons/cupcake.svg @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/icons/dome.json b/icons/dome.json new file mode 100644 index 000000000..941b1689b --- /dev/null +++ b/icons/dome.json @@ -0,0 +1,40 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "swastik7805", + "rrod497", + "karsa-mistmere" + ], + "use-cases": [ + "Marking domed monuments on historical maps", + "Identifying museums, observatories, or heritage sites", + "Representing domed Christian churches in architecture apps", + "Showing Byzantine or Orthodox landmarks on maps" + ], + "tags": [ + "roof", + "cupola", + "rotunda", + "shelter", + "building", + "architecture", + "structure", + "church", + "christian", + "orthodox", + "byzantine", + "cathedral", + "basilica", + "chapel", + "monument", + "landmark", + "museum", + "observatory", + "heritage" + ], + "categories": [ + "buildings", + "navigation", + "travel" + ] +} diff --git a/icons/dome.svg b/icons/dome.svg new file mode 100644 index 000000000..5ee1f7446 --- /dev/null +++ b/icons/dome.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/icons/door-closed-cog.json b/icons/door-closed-cog.json new file mode 100644 index 000000000..b95098840 --- /dev/null +++ b/icons/door-closed-cog.json @@ -0,0 +1,29 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "karsa-mistmere", + "lukedukeus", + "danielbayley", + "jguddas", + "UsamaKhan", + "ajaxjiang96" + ], + "use-cases": [ + "Opening room configuration in a smart home app", + "Setting automatic opening schedules for a building entrance", + "Managing visitor access to a shared entrance" + ], + "tags": [ + "room", + "entrance", + "entry", + "settings", + "gear", + "access", + "automation" + ], + "categories": [ + "home", + "security" + ] +} diff --git a/icons/door-closed-cog.svg b/icons/door-closed-cog.svg new file mode 100644 index 000000000..87d14dbf6 --- /dev/null +++ b/icons/door-closed-cog.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/icons/door-closed-locked.svg b/icons/door-closed-locked.svg index 9f7c04af0..3e3eeaf26 100644 --- a/icons/door-closed-locked.svg +++ b/icons/door-closed-locked.svg @@ -9,9 +9,9 @@ stroke-linecap="round" stroke-linejoin="round" > - - - - - + + + + + diff --git a/icons/door-closed-package.json b/icons/door-closed-package.json new file mode 100644 index 000000000..19c9093fa --- /dev/null +++ b/icons/door-closed-package.json @@ -0,0 +1,25 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "karsa-mistmere" + ], + "use-cases": [ + "Indicating package delivery to a home or residence", + "Representing packages left at a recipient's door", + "Tracking doorstep or unattended deliveries", + "Selecting a delivery location or drop-off point" + ], + "tags": [ + "delivery", + "parcel", + "doorstep", + "shipping", + "drop-off", + "courier" + ], + "categories": [ + "home", + "food-beverage", + "transportation" + ] +} diff --git a/icons/door-closed-package.svg b/icons/door-closed-package.svg new file mode 100644 index 000000000..a42607a4f --- /dev/null +++ b/icons/door-closed-package.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/icons/door-closed.svg b/icons/door-closed.svg index 68e73918d..b22e3f330 100644 --- a/icons/door-closed.svg +++ b/icons/door-closed.svg @@ -9,7 +9,7 @@ stroke-linecap="round" stroke-linejoin="round" > - - - + + + diff --git a/icons/door-open.svg b/icons/door-open.svg index 6538907e0..a9422997d 100644 --- a/icons/door-open.svg +++ b/icons/door-open.svg @@ -9,9 +9,9 @@ stroke-linecap="round" stroke-linejoin="round" > - - - + + - + + diff --git a/icons/door-stairwell.json b/icons/door-stairwell.json new file mode 100644 index 000000000..17d55fed5 --- /dev/null +++ b/icons/door-stairwell.json @@ -0,0 +1,40 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "jguddas", + "karsa-mistmere" + ], + "use-cases": [ + "Indicating stair access in building navigation", + "Showing floor levels in building directories", + "Marking where an item is located by floor", + "Directing users to take stairs instead of elevators" + ], + "tags": [ + "staircase", + "stairway", + "stairs", + "steps", + "ladder", + "transition", + "access", + "structure", + "spiral", + "building", + "vertical", + "movement", + "floor", + "level", + "entrance", + "entry", + "exit", + "egress", + "route", + "indoor", + "emergency exit" + ], + "categories": [ + "home", + "travel" + ] +} diff --git a/icons/door-stairwell.svg b/icons/door-stairwell.svg new file mode 100644 index 000000000..174c0ea9b --- /dev/null +++ b/icons/door-stairwell.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/icons/engine.json b/icons/engine.json new file mode 100644 index 000000000..373f86c4d --- /dev/null +++ b/icons/engine.json @@ -0,0 +1,34 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "benhaube", + "jamiemlaw" + ], + "use-cases": [ + "Indicating engine health or need of repair", + "Representing the engine category in a parts list", + "Representing automotive repair", + "showing a check engine warning on a dashboard", + "marking scheduled vehicle maintenance", + "filtering vehicle parts in an online catalog", + "labeling automotive diagnostic reports" + ], + "tags": [ + "vehicle", + "power", + "energy", + "drive", + "repair", + "car", + "motor", + "mechanical", + "diagnostics", + "maintenance", + "warning", + "dashboard", + "parts" + ], + "categories": [ + "transportation" + ] +} diff --git a/icons/engine.svg b/icons/engine.svg new file mode 100644 index 000000000..a76d6bf6e --- /dev/null +++ b/icons/engine.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/icons/equal-approximately-not.json b/icons/equal-approximately-not.json new file mode 100644 index 000000000..850b7c76f --- /dev/null +++ b/icons/equal-approximately-not.json @@ -0,0 +1,35 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "ksk3110", + "ryck" + ], + "use-cases": [ + "Representing the ≇ operator in formula editors", + "Flagging values outside tolerance in diff tools", + "Indicating failed approximate-equality assertions in test runners", + "Marking measurements outside acceptable variance" + ], + "tags": [ + "calculate", + "math", + "operator", + "not", + "approximately", + "unequal", + "code", + "≇", + "tolerance", + "threshold", + "mismatch", + "comparison", + "assertion", + "variance" + ], + "categories": [ + "math", + "development", + "charts", + "science" + ] +} diff --git a/icons/equal-approximately-not.svg b/icons/equal-approximately-not.svg new file mode 100644 index 000000000..c18346b35 --- /dev/null +++ b/icons/equal-approximately-not.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/icons/faucet.json b/icons/faucet.json new file mode 100644 index 000000000..76e39c06d --- /dev/null +++ b/icons/faucet.json @@ -0,0 +1,43 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "danielbayley", + "karsa-mistmere" + ], + "use-cases": [ + "Indicating water sources on maps", + "Controlling faucets in smart home apps", + "Representing kitchen or bathroom plumbing fixtures", + "Showing drinking water amenities in travel apps" + ], + "tags": [ + "tap", + "spigot", + "water", + "drinking", + "fresh", + "source", + "pipes", + "plumbing", + "plumber", + "sink", + "basin", + "kitchen", + "bathroom", + "washroom", + "fixture", + "valve", + "flow", + "drinkable", + "potable", + "amenities", + "utilities", + "hydration" + ], + "categories": [ + "food-beverage", + "home", + "navigation", + "travel" + ] +} diff --git a/icons/faucet.svg b/icons/faucet.svg new file mode 100644 index 000000000..c0e5ec8c1 --- /dev/null +++ b/icons/faucet.svg @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/icons/folder-bookmark.svg b/icons/folder-bookmark.svg index 8083f87f1..730ce045d 100644 --- a/icons/folder-bookmark.svg +++ b/icons/folder-bookmark.svg @@ -9,6 +9,6 @@ stroke-linecap="round" stroke-linejoin="round" > - - + + diff --git a/icons/gap-horizontal.json b/icons/gap-horizontal.json new file mode 100644 index 000000000..4d61cbff8 --- /dev/null +++ b/icons/gap-horizontal.json @@ -0,0 +1,31 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "samuelalake", + "karsa-mistmere" + ], + "use-cases": [ + "indicating horizontal gap setting in a design inspector", + "representing the column-gap control in grid or flex panels", + "adjusting horizontal spacing between components in a page builder", + "visualizing a spacing token in a design system or theme editor" + ], + "tags": [ + "gap", + "spacing", + "space", + "between", + "padding", + "margin", + "horizontal", + "auto layout", + "layout", + "distance", + "frame", + "distribute" + ], + "categories": [ + "design", + "layout" + ] +} diff --git a/icons/flip-horizontal-2.svg b/icons/gap-horizontal.svg similarity index 72% rename from icons/flip-horizontal-2.svg rename to icons/gap-horizontal.svg index 7e0459dab..d0eefc424 100644 --- a/icons/flip-horizontal-2.svg +++ b/icons/gap-horizontal.svg @@ -9,10 +9,10 @@ stroke-linecap="round" stroke-linejoin="round" > - - - - - + + + + + diff --git a/icons/gap-vertical.json b/icons/gap-vertical.json new file mode 100644 index 000000000..54b282e54 --- /dev/null +++ b/icons/gap-vertical.json @@ -0,0 +1,32 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "samuelalake", + "karsa-mistmere" + ], + "use-cases": [ + "displaying vertical spacing between stacked elements in a design inspector", + "representing the row-gap control in a css grid or flex panel", + "adjusting vertical gutter in a page builder or layout editor", + "visualizing space between rows in an auto-layout stack", + "indicating configurable vertical spacing in a ui component library" + ], + "tags": [ + "gap", + "spacing", + "space", + "between", + "padding", + "margin", + "vertical", + "auto layout", + "layout", + "distance", + "frame", + "distribute" + ], + "categories": [ + "design", + "layout" + ] +} diff --git a/icons/gap-vertical.svg b/icons/gap-vertical.svg new file mode 100644 index 000000000..78d81e097 --- /dev/null +++ b/icons/gap-vertical.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/icons/germ-off.json b/icons/germ-off.json new file mode 100644 index 000000000..7b8e6ec97 --- /dev/null +++ b/icons/germ-off.json @@ -0,0 +1,49 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "rrod497", + "karsa-mistmere" + ], + "use-cases": [ + "Indicating sanitized or disinfected surfaces", + "Marking bacteria-free products or environments", + "Showing infection prevention or contamination control", + "Representing antimicrobial treatment in medical interfaces" + ], + "tags": [ + "virus", + "microbe", + "bacterium", + "bacteria", + "germ", + "organism", + "cilia", + "pathogen", + "infection", + "disease", + "contagion", + "contamination", + "microbiology", + "health", + "medicine", + "biology", + "science", + "microorganism", + "pandemic", + "epidemic", + "outbreak", + "amoeba", + "flu", + "security", + "single cell", + "unicellular", + "vaccine", + "immunity", + "quarantine" + ], + "categories": [ + "medical", + "science", + "security" + ] +} diff --git a/icons/germ-off.svg b/icons/germ-off.svg new file mode 100644 index 000000000..1a1259372 --- /dev/null +++ b/icons/germ-off.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/icons/germ.json b/icons/germ.json new file mode 100644 index 000000000..2c717fd02 --- /dev/null +++ b/icons/germ.json @@ -0,0 +1,49 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "rrod497", + "karsa-mistmere" + ], + "use-cases": [ + "Representing bacteria or single-celled organisms", + "Indicating contamination or infectious disease", + "Marking microbiology research or laboratory samples", + "Showing microbes in hygiene and sanitation interfaces" + ], + "tags": [ + "virus", + "microbe", + "bacterium", + "bacteria", + "germ", + "organism", + "cilia", + "pathogen", + "infection", + "disease", + "contagion", + "contamination", + "microbiology", + "health", + "medicine", + "biology", + "science", + "microorganism", + "pandemic", + "epidemic", + "outbreak", + "amoeba", + "flu", + "security", + "single cell", + "unicellular", + "vaccine", + "immunity", + "quarantine" + ], + "categories": [ + "medical", + "science", + "security" + ] +} diff --git a/icons/germ.svg b/icons/germ.svg new file mode 100644 index 000000000..9bb2651e7 --- /dev/null +++ b/icons/germ.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/icons/globe-code.json b/icons/globe-code.json new file mode 100644 index 000000000..7fc05e66a --- /dev/null +++ b/icons/globe-code.json @@ -0,0 +1,32 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "colebemis", + "csandman", + "ericfennis", + "AleksejDix" + ], + "use-cases": [ + "Indicating global traffic in network monitoring dashboards", + "Representing web development in developer tools" + ], + "tags": [ + "website", + "internet", + "globe", + "connection", + "network", + "digital", + "access", + "global", + "link", + "world", + "browser", + "language", + "translate" + ], + "categories": [ + "navigation", + "development" + ] +} diff --git a/icons/globe-code.svg b/icons/globe-code.svg new file mode 100644 index 000000000..d2cf7dc62 --- /dev/null +++ b/icons/globe-code.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/icons/graduation-cap.json b/icons/graduation-cap.json index ea0b5b132..03d4db6ee 100644 --- a/icons/graduation-cap.json +++ b/icons/graduation-cap.json @@ -18,7 +18,7 @@ "academic", "hat", "diploma", - "bachlor's", + "bachelor's", "master's", "doctorate" ], diff --git a/icons/groceries.json b/icons/groceries.json new file mode 100644 index 000000000..8a424e3b5 --- /dev/null +++ b/icons/groceries.json @@ -0,0 +1,34 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "kemie", + "rrod497" + ], + "use-cases": [ + "Indicating pickup or delivery options in grocery shopping apps", + "Marking grocery categories in menus and filters", + "Categorizing grocery spending in personal finance apps", + "Representing food donations on community aid platforms" + ], + "tags": [ + "food", + "vegetables", + "health", + "grocery", + "market", + "farmer", + "produce", + "pantry", + "paper bag", + "shop", + "supermarket", + "retail", + "delivery", + "larder", + "provisions", + "takeout" + ], + "categories": [ + "food-beverage" + ] +} diff --git a/icons/groceries.svg b/icons/groceries.svg new file mode 100644 index 000000000..303e506ab --- /dev/null +++ b/icons/groceries.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/icons/hard-hat.json b/icons/hard-hat.json index 58632a25a..256780df3 100644 --- a/icons/hard-hat.json +++ b/icons/hard-hat.json @@ -8,8 +8,7 @@ "tags": [ "helmet", "construction", - "safety", - "savety" + "safety" ], "categories": [ "tools" diff --git a/icons/hiking-stick.json b/icons/hiking-stick.json new file mode 100644 index 000000000..eff0f8518 --- /dev/null +++ b/icons/hiking-stick.json @@ -0,0 +1,32 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "kamilasw", + "karsa-mistmere" + ], + "use-cases": [ + "Hiking routes", + "Hiking activities", + "marking trailheads on maps", + "denoting hiking gear in packing lists", + "labeling guided walking or trekking tours", + "indicating recommended equipment for a route" + ], + "tags": [ + "hiking", + "walking", + "stick", + "trekking", + "trail", + "pole", + "backpacking", + "outdoors", + "mountains" + ], + "categories": [ + "sports", + "travel", + "nature", + "navigation" + ] +} diff --git a/icons/hiking-stick.svg b/icons/hiking-stick.svg new file mode 100644 index 000000000..86aae41c8 --- /dev/null +++ b/icons/hiking-stick.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/icons/hourglass-cog.json b/icons/hourglass-cog.json new file mode 100644 index 000000000..cc4d16609 --- /dev/null +++ b/icons/hourglass-cog.json @@ -0,0 +1,29 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "rrod497" + ], + "use-cases": [ + "editing a timer or countdown", + "configuring session timeout", + "setting expiration for scheduled tasks" + ], + "tags": [ + "timer", + "time", + "sandglass", + "duration", + "settings", + "gear", + "cog", + "edit", + "configuration", + "countdown", + "timeout", + "expiration" + ], + "categories": [ + "time", + "tools" + ] +} diff --git a/icons/hourglass-cog.svg b/icons/hourglass-cog.svg new file mode 100644 index 000000000..3dc78e3a4 --- /dev/null +++ b/icons/hourglass-cog.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/icons/house-cog.json b/icons/house-cog.json new file mode 100644 index 000000000..94c9ef607 --- /dev/null +++ b/icons/house-cog.json @@ -0,0 +1,30 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "jguddas", + "karsa-mistmere", + "danielbayley", + "UsamaKhan", + "ajaxjiang96" + ], + "use-cases": [ + "Opening household settings in smart home applications", + "Managing property configuration in real estate dashboards", + "Configuring home automation rules in IoT device management interfaces", + "Accessing home security settings in residential alarm systems" + ], + "tags": [ + "home", + "building", + "residence", + "settings", + "gear", + "configuration", + "property", + "automation" + ], + "categories": [ + "buildings", + "home" + ] +} diff --git a/icons/house-cog.svg b/icons/house-cog.svg new file mode 100644 index 000000000..f6eb97533 --- /dev/null +++ b/icons/house-cog.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/icons/houses.json b/icons/houses.json new file mode 100644 index 000000000..3d0387f51 --- /dev/null +++ b/icons/houses.json @@ -0,0 +1,32 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "danielbayley", + "jguddas" + ], + "use-cases": [ + "Representing residential neighborhoods on maps", + "Indicating multiple homes or properties in real estate apps", + "Marking housing developments in planning tools" + ], + "tags": [ + "homes", + "housing", + "estate", + "living", + "buildings", + "development", + "residences", + "residential", + "village", + "neighbourhood", + "accommodation", + "community", + "dwellings", + "architecture" + ], + "categories": [ + "buildings", + "home" + ] +} diff --git a/icons/houses.svg b/icons/houses.svg new file mode 100644 index 000000000..b79041cd7 --- /dev/null +++ b/icons/houses.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/icons/id-card-lanyard.svg b/icons/id-card-lanyard.svg index 0cc2e7545..278a17b01 100644 --- a/icons/id-card-lanyard.svg +++ b/icons/id-card-lanyard.svg @@ -10,8 +10,8 @@ stroke-linejoin="round" > - - + + diff --git a/icons/id-card.svg b/icons/id-card.svg index 71a4eeaa7..fa04ca0aa 100644 --- a/icons/id-card.svg +++ b/icons/id-card.svg @@ -9,9 +9,9 @@ stroke-linecap="round" stroke-linejoin="round" > + - - + diff --git a/icons/iv-bag.json b/icons/iv-bag.json new file mode 100644 index 000000000..986a099c8 --- /dev/null +++ b/icons/iv-bag.json @@ -0,0 +1,35 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "karsa-mistmere" + ], + "use-cases": [ + "Representing intravenous therapy in healthcare interfaces", + "Showing infusion treatments or hydration services", + "Marking medication routes of administration", + "Identifying infusion clinics or patient education materials" + ], + "tags": [ + "intravenous", + "therapy", + "infusion", + "medicine", + "medication", + "treatment", + "healthcare", + "hydration", + "vitamin", + "supplement", + "blood", + "donation", + "chemotherapy", + "clinic", + "feeding tube", + "administration", + "health", + "regeneration" + ], + "categories": [ + "medical" + ] +} diff --git a/icons/iv-bag.svg b/icons/iv-bag.svg new file mode 100644 index 000000000..b932281ff --- /dev/null +++ b/icons/iv-bag.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/icons/kazakh-tenge.json b/icons/kazakh-tenge.json new file mode 100644 index 000000000..aec885e88 --- /dev/null +++ b/icons/kazakh-tenge.json @@ -0,0 +1,22 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "hkayrad" + ], + "use-cases": [ + "Showing the Kazakh Tenge currency symbol in financial applications or websites", + "Representing the Kazakh Tenge in currency conversion tools or financial reports" + ], + "tags": [ + "currency", + "money", + "payment", + "kazakhstan", + "currency-symbol", + "exchange", + "cash" + ], + "categories": [ + "finance" + ] +} diff --git a/icons/kazakh-tenge.svg b/icons/kazakh-tenge.svg new file mode 100644 index 000000000..b45c9a4b0 --- /dev/null +++ b/icons/kazakh-tenge.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/icons/lambda.json b/icons/lambda.json new file mode 100644 index 000000000..0c867c0bb --- /dev/null +++ b/icons/lambda.json @@ -0,0 +1,33 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "karsa-mistmere", + "kaleidosium", + "UbaidUllah9962" + ], + "use-cases": [ + "Representing anonymous functions in code editors", + "Representing serverless functions in cloud architecture diagrams", + "Representing lambda notation in math and science" + ], + "tags": [ + "greek", + "symbol", + "math", + "function", + "programming", + "serverless", + "anonymous", + "functional", + "cloud", + "wavelength", + "calculus", + "linear algebra" + ], + "categories": [ + "development", + "math", + "science", + "text" + ] +} diff --git a/icons/lambda.svg b/icons/lambda.svg new file mode 100644 index 000000000..77e9e8288 --- /dev/null +++ b/icons/lambda.svg @@ -0,0 +1,14 @@ + + + + diff --git a/icons/layout-arrow-down.json b/icons/layout-arrow-down.json new file mode 100644 index 000000000..3cca032fe --- /dev/null +++ b/icons/layout-arrow-down.json @@ -0,0 +1,34 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "samuelalake" + ], + "use-cases": [ + "Setting auto layout direction to vertical in design tools", + "Selecting flex-direction: column in devtools panels", + "Stacking content blocks top to bottom", + "Toggle between horizontal and vertical layouts in design tools", + "Switch to column view in file managers or galleries", + "Set flex-direction to column in css or style panels", + "Indicate vertical stacking mode in ui editors or prototyping tools", + "Choose vertical orientation for responsive layout controls" + ], + "tags": [ + "layout", + "direction", + "flex", + "flexbox", + "flex-direction", + "column", + "auto layout", + "vertical", + "arrange", + "order", + "flow", + "stack" + ], + "categories": [ + "design", + "layout" + ] +} diff --git a/lab/briefcase-plus.svg b/icons/layout-arrow-down.svg similarity index 56% rename from lab/briefcase-plus.svg rename to icons/layout-arrow-down.svg index 9cf51170b..69ae00c50 100644 --- a/lab/briefcase-plus.svg +++ b/icons/layout-arrow-down.svg @@ -9,8 +9,8 @@ stroke-linecap="round" stroke-linejoin="round" > - - - - + + + + diff --git a/icons/layout-arrow-right.json b/icons/layout-arrow-right.json new file mode 100644 index 000000000..2a6329973 --- /dev/null +++ b/icons/layout-arrow-right.json @@ -0,0 +1,34 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "samuelalake" + ], + "use-cases": [ + "Setting auto layout direction to horizontal in design tools", + "Selecting flex-direction: row in devtools panels", + "Switching sections from stacked to side-by-side", + "Switching the canvas to a horizontal layout", + "Choosing side-by-side or two-column display", + "Representing css flex-direction: row in developer tools", + "Toggling panel orientation in a ui", + "Selecting left-to-right flow in design editors" + ], + "tags": [ + "layout", + "direction", + "flex", + "flexbox", + "flex-direction", + "row", + "auto layout", + "horizontal", + "arrange", + "order", + "flow", + "stack" + ], + "categories": [ + "design", + "layout" + ] +} diff --git a/icons/layout-arrow-right.svg b/icons/layout-arrow-right.svg new file mode 100644 index 000000000..7bbd3c921 --- /dev/null +++ b/icons/layout-arrow-right.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/icons/layout-grid-circles.json b/icons/layout-grid-circles.json new file mode 100644 index 000000000..76213056a --- /dev/null +++ b/icons/layout-grid-circles.json @@ -0,0 +1,41 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "zenoamaro", + "ericfennis", + "csandman", + "mittalyashu", + "danielbayley", + "karsa-mistmere" + ], + "use-cases": [ + "Opening an app launcher or application grid", + "Switching to a tiled or grid-based layout", + "Displaying a dashboard of widgets or shortcuts", + "Representing a dot matrix or dot-matrix display" + ], + "tags": [ + "app", + "home", + "start", + "dot matrix", + "dots", + "menu", + "dashboard", + "collection", + "overview", + "tiles", + "grid", + "matrix", + "launcher", + "apps", + "widgets", + "ui", + "circles", + "shortcuts" + ], + "categories": [ + "design", + "layout" + ] +} diff --git a/icons/layout-grid-circles.svg b/icons/layout-grid-circles.svg new file mode 100644 index 000000000..b008e74e8 --- /dev/null +++ b/icons/layout-grid-circles.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/icons/leaf.svg b/icons/leaf.svg index f322ef022..8012589aa 100644 --- a/icons/leaf.svg +++ b/icons/leaf.svg @@ -9,6 +9,6 @@ stroke-linecap="round" stroke-linejoin="round" > - - + + diff --git a/icons/lectern.svg b/icons/lectern.svg index a414aee8e..fd8cc499b 100644 --- a/icons/lectern.svg +++ b/icons/lectern.svg @@ -9,7 +9,8 @@ stroke-linecap="round" stroke-linejoin="round" > - - - + + + + diff --git a/icons/lens.json b/icons/lens.json new file mode 100644 index 000000000..23c774717 --- /dev/null +++ b/icons/lens.json @@ -0,0 +1,42 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "rrod497" + ], + "use-cases": [ + "representing camera settings or photography mode", + "optical shops, eyeglasses or contact lens product categories", + "indicating visual effects like glassmorphism or lens reflections", + "generically representing shaders and visual effects, or scenic post-processing effects", + "educational apps (physics, optics, biology), scientific illustrations and documentation", + "indicating a specialized zoom / magnification tool for photography or camera apps", + "indicating aperture or depth-of-field controls in camera settings", + "toggling bokeh or portrait mode effects in photo editors", + "representing macro or close-up shooting modes", + "representing ar filters, virtual try-on or camera-lens selection features" + ], + "tags": [ + "optics", + "reflection", + "camera", + "photography", + "focus", + "effect", + "shader", + "post-process", + "visual", + "contact lens", + "glass", + "magnify", + "translucent", + "transparent", + "orb", + "bubble", + "zoom" + ], + "categories": [ + "photography", + "science", + "tools" + ] +} diff --git a/icons/lens.svg b/icons/lens.svg new file mode 100644 index 000000000..ff5e6d383 --- /dev/null +++ b/icons/lens.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/icons/letters.json b/icons/letters.json new file mode 100644 index 000000000..8a3350bcf --- /dev/null +++ b/icons/letters.json @@ -0,0 +1,26 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "karsa-mistmere" + ], + "use-cases": [ + "Representing letters, alphabets, or written characters", + "Indicating alphabetical sorting or organization", + "Representing language, literacy, or spelling", + "Representing text input fields or textual data" + ], + "tags": [ + "abc", + "alphabet", + "alphabetical", + "text", + "characters", + "language", + "typography", + "literacy", + "spelling" + ], + "categories": [ + "text" + ] +} diff --git a/icons/letters.svg b/icons/letters.svg new file mode 100644 index 000000000..a3a6fbf3b --- /dev/null +++ b/icons/letters.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/icons/lighthouse.json b/icons/lighthouse.json new file mode 100644 index 000000000..aad0294c8 --- /dev/null +++ b/icons/lighthouse.json @@ -0,0 +1,45 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "xougui", + "karsa-mistmere" + ], + "use-cases": [ + "Marking coastal landmarks or harbor entrances on maps", + "Indicating navigation beacons in maritime dashboards", + "Flagging maritime safety or beacon maintenance tasks", + "Representing guidance or guiding light in onboarding or help interfaces" + ], + "tags": [ + "lighthouse", + "beacon", + "coast", + "navigation", + "tower", + "sea", + "ocean", + "maritime", + "guide", + "safety", + "light", + "shine", + "port", + "marine", + "faro", + "signal", + "nautical", + "harbor", + "shore", + "lightbeam", + "wayfinding", + "guidance", + "onboarding", + "help", + "landmark" + ], + "categories": [ + "buildings", + "navigation", + "travel" + ] +} diff --git a/icons/lighthouse.svg b/icons/lighthouse.svg new file mode 100644 index 000000000..ebf682000 --- /dev/null +++ b/icons/lighthouse.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/icons/line-dot-bottom-vertical.json b/icons/line-dot-bottom-vertical.json new file mode 100644 index 000000000..11341208f --- /dev/null +++ b/icons/line-dot-bottom-vertical.json @@ -0,0 +1,23 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "nathan-de-pachtere" + ], + "use-cases": [ + "Marking the first commit in version-control graphs", + "Indicating an initial measurement point on a process timeline" + ], + "tags": [ + "code", + "version", + "control", + "station", + "waypoint", + "start", + "first" + ], + "categories": [ + "development", + "navigation" + ] +} diff --git a/icons/line-dot-bottom-vertical.svg b/icons/line-dot-bottom-vertical.svg new file mode 100644 index 000000000..ee36702c4 --- /dev/null +++ b/icons/line-dot-bottom-vertical.svg @@ -0,0 +1,14 @@ + + + + diff --git a/icons/line-dot-left-horizontal.json b/icons/line-dot-left-horizontal.json new file mode 100644 index 000000000..1d3381517 --- /dev/null +++ b/icons/line-dot-left-horizontal.json @@ -0,0 +1,32 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "nathan-de-pachtere" + ], + "use-cases": [ + "Marking a branch's first commit in version-control interfaces", + "Indicating measurements at a metal bar's starting point" + ], + "tags": [ + "line", + "dot", + "left", + "horizontal", + "shape", + "symbol", + "navigation", + "arrow", + "pointer", + "code", + "version control", + "waypoint", + "stop", + "start", + "first", + "station" + ], + "categories": [ + "development", + "navigation" + ] +} diff --git a/icons/line-dot-left-horizontal.svg b/icons/line-dot-left-horizontal.svg new file mode 100644 index 000000000..07fc26e34 --- /dev/null +++ b/icons/line-dot-left-horizontal.svg @@ -0,0 +1,14 @@ + + + + diff --git a/icons/line-dot-top-vertical.json b/icons/line-dot-top-vertical.json new file mode 100644 index 000000000..64e3213fa --- /dev/null +++ b/icons/line-dot-top-vertical.json @@ -0,0 +1,28 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "nathan-de-pachtere" + ], + "use-cases": [ + "Marking the last commit of a branch", + "Marking a measurement at the end of a bar" + ], + "tags": [ + "arrow", + "pointer", + "direction", + "indicator", + "code", + "version", + "control", + "waypoint", + "stop", + "station", + "last", + "end" + ], + "categories": [ + "development", + "navigation" + ] +} diff --git a/icons/line-dot-top-vertical.svg b/icons/line-dot-top-vertical.svg new file mode 100644 index 000000000..14c0aee50 --- /dev/null +++ b/icons/line-dot-top-vertical.svg @@ -0,0 +1,14 @@ + + + + diff --git a/icons/mail-pen.json b/icons/mail-pen.json new file mode 100644 index 000000000..d1a362430 --- /dev/null +++ b/icons/mail-pen.json @@ -0,0 +1,31 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "colebemis", + "karsa-mistmere", + "ericfennis", + "jguddas", + "jennieboops" + ], + "use-cases": [ + "Composing or editing email messages", + "Drafting letters in mail interfaces", + "Editing email addresses on settings pages" + ], + "tags": [ + "email", + "message", + "letter", + "pen", + "edit", + "compose", + "draft", + "write", + "writing", + "create", + "reply" + ], + "categories": [ + "mail" + ] +} diff --git a/icons/mail-pen.svg b/icons/mail-pen.svg new file mode 100644 index 000000000..04b301e51 --- /dev/null +++ b/icons/mail-pen.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/icons/map-pinned.json b/icons/map-pinned.json index e7a755820..cdb79b3aa 100644 --- a/icons/map-pinned.json +++ b/icons/map-pinned.json @@ -2,7 +2,8 @@ "$schema": "../icon.schema.json", "contributors": [ "danielbayley", - "karsa-mistmere" + "karsa-mistmere", + "jguddas" ], "use-cases": [], "tags": [ diff --git a/icons/map-pinned.svg b/icons/map-pinned.svg index 4ed4eb051..e6f921a7c 100644 --- a/icons/map-pinned.svg +++ b/icons/map-pinned.svg @@ -9,7 +9,7 @@ stroke-linecap="round" stroke-linejoin="round" > - + + - diff --git a/icons/messages-circle.json b/icons/messages-circle.json new file mode 100644 index 000000000..adee57e1c --- /dev/null +++ b/icons/messages-circle.json @@ -0,0 +1,40 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "colebemis", + "csandman", + "ericfennis", + "jguddas", + "karsa-mistmere", + "Mirazstudio-offical" + ], + "use-cases": [ + "Starting a group conversation in chat apps", + "Indicating multiple unread message threads", + "Opening discussions or comment sections in social apps" + ], + "tags": [ + "comment", + "chat", + "conversation", + "dialog", + "feedback", + "speech bubbles", + "copy", + "multiple", + "discussion", + "interview", + "debate", + "group", + "groupchat", + "threads", + "unread", + "bubble", + "messaging" + ], + "categories": [ + "social", + "communication", + "notifications" + ] +} diff --git a/icons/messages-circle.svg b/icons/messages-circle.svg new file mode 100644 index 000000000..842fb4f1d --- /dev/null +++ b/icons/messages-circle.svg @@ -0,0 +1,14 @@ + + + + diff --git a/icons/monitor-check.json b/icons/monitor-check.json index 7bd0d8eb2..b5bb0ced1 100644 --- a/icons/monitor-check.json +++ b/icons/monitor-check.json @@ -8,9 +8,15 @@ "use-cases": [], "tags": [ "tv", + "computer", "screen", "display", "desktop", + "external display", + "screen sharing", + "connected", + "success", + "verified", "running", "active", "virtual machine", diff --git a/icons/monitor-cloud.json b/icons/monitor-cloud.json index ff36ae17a..97f25571a 100644 --- a/icons/monitor-cloud.json +++ b/icons/monitor-cloud.json @@ -9,6 +9,12 @@ ], "use-cases": [], "tags": [ + "tv", + "computer", + "screen", + "display", + "desktop", + "external display", "virtual machine", "virtual desktop", "vm", @@ -20,10 +26,7 @@ "software as a service", "saas", "workstation", - "environment", - "tv", - "screen", - "display" + "environment" ], "categories": [ "connectivity", diff --git a/icons/monitor-cog.json b/icons/monitor-cog.json index 97c700b79..dd6224bac 100644 --- a/icons/monitor-cog.json +++ b/icons/monitor-cog.json @@ -8,13 +8,15 @@ "use-cases": [], "tags": [ "tv", + "computer", "screen", "display", + "desktop", + "external display", "virtual machine", "vm", "executable", "settings", - "cog", "edit", "gear", "configuration", diff --git a/icons/monitor-dot.json b/icons/monitor-dot.json index 7bd0d8eb2..fa7c19529 100644 --- a/icons/monitor-dot.json +++ b/icons/monitor-dot.json @@ -8,9 +8,15 @@ "use-cases": [], "tags": [ "tv", + "computer", "screen", "display", "desktop", + "external display", + "screen sharing", + "status", + "notification", + "indicator", "running", "active", "virtual machine", diff --git a/icons/monitor-down.json b/icons/monitor-down.json index d177cc67e..ffba9d252 100644 --- a/icons/monitor-down.json +++ b/icons/monitor-down.json @@ -8,10 +8,15 @@ "use-cases": [], "tags": [ "tv", + "computer", "screen", "display", "desktop", - "download" + "external display", + "screen sharing", + "download", + "remote", + "cast" ], "categories": [ "connectivity", diff --git a/icons/monitor-off.json b/icons/monitor-off.json index 3c49d7094..01f80bcd9 100644 --- a/icons/monitor-off.json +++ b/icons/monitor-off.json @@ -7,7 +7,17 @@ ], "use-cases": [], "tags": [ - "share" + "tv", + "computer", + "screen", + "display", + "desktop", + "external display", + "screen sharing", + "offline", + "disabled", + "disconnected", + "power" ], "categories": [ "connectivity", diff --git a/icons/monitor-pause.json b/icons/monitor-pause.json index 0b1b02d71..0257090ed 100644 --- a/icons/monitor-pause.json +++ b/icons/monitor-pause.json @@ -8,12 +8,16 @@ "use-cases": [], "tags": [ "tv", + "computer", "screen", "display", "desktop", + "external display", + "screen sharing", "video", "movie", "film", + "pause", "suspend", "hibernate", "boot", diff --git a/icons/monitor-pc.json b/icons/monitor-pc.json new file mode 100644 index 000000000..63aa8a400 --- /dev/null +++ b/icons/monitor-pc.json @@ -0,0 +1,33 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "colebemis", + "ericfennis", + "danielbayley", + "jguddas", + "karsa-mistmere" + ], + "use-cases": [ + "Representing desktop computer setups", + "Showing workstation or gaming PC devices", + "Identifying personal computers in device lists", + "Managing computer hardware in IT dashboards" + ], + "tags": [ + "personal computer", + "desktop", + "screen", + "display", + "workstation", + "tower", + "chassis", + "hardware", + "setup", + "gaming" + ], + "categories": [ + "devices", + "development", + "gaming" + ] +} diff --git a/icons/monitor-pc.svg b/icons/monitor-pc.svg new file mode 100644 index 000000000..4c76c14d9 --- /dev/null +++ b/icons/monitor-pc.svg @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/icons/monitor-play.json b/icons/monitor-play.json index 42916bebc..a3b1dc9df 100644 --- a/icons/monitor-play.json +++ b/icons/monitor-play.json @@ -8,12 +8,16 @@ "use-cases": [], "tags": [ "tv", + "computer", "screen", "display", "desktop", + "external display", + "screen sharing", "video", "movie", "film", + "play", "running", "start", "boot", diff --git a/icons/monitor-smartphone.json b/icons/monitor-smartphone.json index 7edcd1a0b..7f986c4ac 100644 --- a/icons/monitor-smartphone.json +++ b/icons/monitor-smartphone.json @@ -6,17 +6,25 @@ "jguddas", "ericfennis" ], - "use-cases": [], + "use-cases": [ + "Showing synchronization between desktop and mobile devices", + "Representing responsive previews across screen sizes", + "Indicating casting or screen sharing to a display" + ], "tags": [ - "smartphone", "phone", "cellphone", "device", "mobile", "desktop", - "monitor", + "screen", + "display", + "external display", + "screen sharing", "responsive", - "screens" + "screens", + "sync", + "cast" ], "categories": [ "connectivity", diff --git a/icons/monitor-speaker.json b/icons/monitor-speaker.json index 3085d95b0..08ed1f14b 100644 --- a/icons/monitor-speaker.json +++ b/icons/monitor-speaker.json @@ -6,9 +6,18 @@ ], "use-cases": [], "tags": [ - "devices", + "tv", + "computer", + "screen", + "display", + "desktop", + "external display", "connect", - "cast" + "cast", + "audio", + "sound", + "volume", + "presentation" ], "categories": [ "connectivity", diff --git a/icons/monitor-stop.json b/icons/monitor-stop.json index 4b8be56f0..447671cd6 100644 --- a/icons/monitor-stop.json +++ b/icons/monitor-stop.json @@ -8,9 +8,12 @@ "use-cases": [], "tags": [ "tv", + "computer", "screen", "display", "desktop", + "external display", + "screen sharing", "video", "movie", "film", diff --git a/icons/monitor-up.json b/icons/monitor-up.json index bbc53064e..0f0fddbd5 100644 --- a/icons/monitor-up.json +++ b/icons/monitor-up.json @@ -8,12 +8,16 @@ "use-cases": [], "tags": [ "tv", + "computer", "screen", "display", + "desktop", + "external display", "upload", "connect", "remote", - "screen share" + "screen sharing", + "cast" ], "categories": [ "connectivity", diff --git a/icons/monitor-x.json b/icons/monitor-x.json index b2be2194b..b769a2076 100644 --- a/icons/monitor-x.json +++ b/icons/monitor-x.json @@ -8,12 +8,18 @@ "use-cases": [], "tags": [ "tv", + "computer", "screen", "display", "desktop", + "external display", + "screen sharing", "virtual machine", "vm", "close", + "error", + "failed", + "disconnected", "stop", "suspend", "remove", diff --git a/icons/monitor.json b/icons/monitor.json index 89b4283b0..d1032d11b 100644 --- a/icons/monitor.json +++ b/icons/monitor.json @@ -7,11 +7,19 @@ "jguddas", "karsa-mistmere" ], - "use-cases": [], + "use-cases": [ + "Representing desktop displays and computer monitors", + "Showing screen-sharing or external display settings", + "Indicating virtual machines in infrastructure dashboards" + ], "tags": [ "tv", + "computer", + "desktop", "screen", "display", + "external display", + "screen sharing", "virtual machine", "vm" ], diff --git a/icons/mouth-off.json b/icons/mouth-off.json new file mode 100644 index 000000000..232f813cd --- /dev/null +++ b/icons/mouth-off.json @@ -0,0 +1,35 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "juliansolimando", + "karsa-mistmere" + ], + "use-cases": [ + "Indicating muted speech or disabled voice input", + "Showing silence in communication interfaces", + "Marking speech restrictions or unavailable talking features", + "Representing loss of taste or oral sensitivity" + ], + "tags": [ + "speech", + "voice", + "talk", + "mute", + "silence", + "speechless", + "taste loss", + "flavor loss", + "ageusia", + "tasteless", + "gustatory", + "oral", + "dental", + "communication", + "accessibility" + ], + "categories": [ + "medical", + "accessibility", + "communication" + ] +} diff --git a/icons/mouth-off.svg b/icons/mouth-off.svg new file mode 100644 index 000000000..67ee9ce56 --- /dev/null +++ b/icons/mouth-off.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/icons/mouth.json b/icons/mouth.json new file mode 100644 index 000000000..6bf5a9c4e --- /dev/null +++ b/icons/mouth.json @@ -0,0 +1,35 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "juliansolimando", + "karsa-mistmere" + ], + "use-cases": [ + "Representing speech or verbal expression", + "Indicating taste or oral sensory input", + "Marking dental or oral health information", + "Showing voice input or speaking controls" + ], + "tags": [ + "speech", + "voice", + "talk", + "speaking", + "taste", + "tasting", + "flavor", + "palate", + "tongue", + "gustatory", + "oral", + "dental", + "expression", + "communication", + "accessibility" + ], + "categories": [ + "medical", + "accessibility", + "communication" + ] +} diff --git a/icons/mouth.svg b/icons/mouth.svg new file mode 100644 index 000000000..3269a5d12 --- /dev/null +++ b/icons/mouth.svg @@ -0,0 +1,14 @@ + + + + diff --git a/icons/nepali-rupee.json b/icons/nepali-rupee.json new file mode 100644 index 000000000..1c60a40a8 --- /dev/null +++ b/icons/nepali-rupee.json @@ -0,0 +1,24 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "sarajdhakal" + ], + "use-cases": [ + "Displaying prices in Nepalese rupees (NPR)", + "Selecting Nepalese rupee in multi-currency selectors", + "Showing exchange rates for Nepalese rupee conversions", + "Indicating remittance transfers paid out in NPR" + ], + "tags": [ + "currency", + "money", + "nepal", + "rupee", + "npr" + ], + "categories": [ + "finance", + "shopping", + "travel" + ] +} diff --git a/icons/nepali-rupee.svg b/icons/nepali-rupee.svg new file mode 100644 index 000000000..ec4b7372f --- /dev/null +++ b/icons/nepali-rupee.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/icons/notebook-dot.json b/icons/notebook-dot.json new file mode 100644 index 000000000..35bada0e5 --- /dev/null +++ b/icons/notebook-dot.json @@ -0,0 +1,26 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "elenakovelskikh" + ], + "use-cases": [ + "Indicating newly updated shared notes in team workspaces", + "Marking shared documentation with new information" + ], + "tags": [ + "document", + "file", + "journal", + "page", + "paper", + "record", + "entry", + "notebook", + "notification", + "unread", + "note" + ], + "categories": [ + "communication" + ] +} diff --git a/icons/notebook-dot.svg b/icons/notebook-dot.svg new file mode 100644 index 000000000..8b620f8b0 --- /dev/null +++ b/icons/notebook-dot.svg @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/icons/nut-off.svg b/icons/nut-off.svg index 5e2561612..746d5fba0 100644 --- a/icons/nut-off.svg +++ b/icons/nut-off.svg @@ -9,9 +9,9 @@ stroke-linecap="round" stroke-linejoin="round" > - - - - - + + + + + diff --git a/icons/nut.svg b/icons/nut.svg index 9e232b529..ee2ca4a0d 100644 --- a/icons/nut.svg +++ b/icons/nut.svg @@ -9,7 +9,7 @@ stroke-linecap="round" stroke-linejoin="round" > - - - + + + diff --git a/icons/park.json b/icons/park.json new file mode 100644 index 000000000..b48002b6a --- /dev/null +++ b/icons/park.json @@ -0,0 +1,31 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "skajosborn", + "jguddas" + ], + "use-cases": [ + "Indicating public parks on maps and navigation apps", + "Marking outdoor recreation areas and picnic zones", + "Highlighting municipal park facilities and services" + ], + "tags": [ + "nature", + "green", + "trees", + "landscape", + "rest", + "walk", + "leisure", + "environment", + "tree", + "bench", + "outdoors", + "recreation" + ], + "categories": [ + "nature", + "navigation", + "travel" + ] +} diff --git a/icons/park.svg b/icons/park.svg new file mode 100644 index 000000000..30d6fb3a3 --- /dev/null +++ b/icons/park.svg @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/icons/pen-tool.svg b/icons/pen-tool.svg index 738c6feca..76b761218 100644 --- a/icons/pen-tool.svg +++ b/icons/pen-tool.svg @@ -9,8 +9,8 @@ stroke-linecap="round" stroke-linejoin="round" > - - - + + + diff --git a/icons/plant-pot.json b/icons/plant-pot.json new file mode 100644 index 000000000..6fe47100f --- /dev/null +++ b/icons/plant-pot.json @@ -0,0 +1,26 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "karsa-mistmere" + ], + "use-cases": [ + "Representing a potted plant in home decor catalogs", + "Indicating indoor plant care or watering reminders", + "Marking gardening supplies in shopping lists" + ], + "tags": [ + "plant", + "leaf", + "nature", + "pot", + "houseplant", + "gardening", + "planter", + "decor", + "indoor" + ], + "categories": [ + "home", + "nature" + ] +} diff --git a/icons/plant-pot.svg b/icons/plant-pot.svg new file mode 100644 index 000000000..b77cd394c --- /dev/null +++ b/icons/plant-pot.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/icons/printer-3d.json b/icons/printer-3d.json new file mode 100644 index 000000000..8da396a86 --- /dev/null +++ b/icons/printer-3d.json @@ -0,0 +1,40 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "Azuzula", + "karsa-mistmere", + "jguddas" + ], + "use-cases": [ + "indicating a 3D printer or fabrication machine", + "representing rapid prototyping and additive manufacturing", + "showing prototyping and model production", + "representing maker tools and digital fabrication", + "indicating a 3D printing job or workflow" + ], + "tags": [ + "model", + "hardware", + "technology", + "device", + "factory", + "manufacturing", + "art", + "extruder", + "stl", + "obj", + "step", + "additive", + "fabrication", + "rapid prototype", + "layer", + "filament", + "nozzle", + "maker", + "machine" + ], + "categories": [ + "devices", + "tools" + ] +} diff --git a/icons/printer-3d.svg b/icons/printer-3d.svg new file mode 100644 index 000000000..7f4747b5e --- /dev/null +++ b/icons/printer-3d.svg @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/icons/receipt-swiss-franc.json b/icons/receipt-swiss-franc.json index fc4aaf7cc..3cb6a0305 100644 --- a/icons/receipt-swiss-franc.json +++ b/icons/receipt-swiss-franc.json @@ -4,6 +4,8 @@ "danielbayley", "karsa-mistmere" ], + "deprecated": true, + "deprecationReason": "icon.use-case", "use-cases": [], "tags": [ "bill", diff --git a/icons/rotate-cw-clock.json b/icons/rotate-cw-clock.json new file mode 100644 index 000000000..5687bf9b8 --- /dev/null +++ b/icons/rotate-cw-clock.json @@ -0,0 +1,43 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "ericfennis", + "danielbayley", + "jguddas", + "karsa-mistmere", + "gkkconan" + ], + "use-cases": [ + "Updating content or syncing recent changes", + "Refreshing data in dashboards or reports", + "Renewing subscriptions or recurring services" + ], + "tags": [ + "modify", + "edit", + "refresh", + "sync", + "renew", + "revision", + "settings", + "adjust", + "upgrade", + "time", + "timeline", + "version", + "time machine", + "backup", + "clockwise", + "arrow", + "reload", + "rerun", + "synchronize", + "circular", + "cycle", + "update" + ], + "categories": [ + "arrows", + "time" + ] +} diff --git a/icons/rotate-cw-clock.svg b/icons/rotate-cw-clock.svg new file mode 100644 index 000000000..c4cb79a0e --- /dev/null +++ b/icons/rotate-cw-clock.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/icons/rugby-ball.json b/icons/rugby-ball.json new file mode 100644 index 000000000..2d6ec7dd4 --- /dev/null +++ b/icons/rugby-ball.json @@ -0,0 +1,21 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "dev-mralph", + "karsa-mistmere" + ], + "use-cases": [ + "Representing rugby matches and tournaments", + "Categorizing rugby-related content in sports apps" + ], + "tags": [ + "rugby", + "football", + "ball", + "goal", + "sport" + ], + "categories": [ + "sports" + ] +} diff --git a/icons/rugby-ball.svg b/icons/rugby-ball.svg new file mode 100644 index 000000000..f72737ba3 --- /dev/null +++ b/icons/rugby-ball.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/icons/salad.svg b/icons/salad.svg index ac16c2429..3c4b40228 100644 --- a/icons/salad.svg +++ b/icons/salad.svg @@ -9,9 +9,8 @@ stroke-linecap="round" stroke-linejoin="round" > + + - - - - + diff --git a/icons/satellite-dish.json b/icons/satellite-dish.json index 4ea7298c7..f88613d2c 100644 --- a/icons/satellite-dish.json +++ b/icons/satellite-dish.json @@ -4,12 +4,20 @@ "karsa-mistmere", "ericfennis" ], - "use-cases": [], + "use-cases": [ + "Configuring satellite communication or broadcast equipment", + "Indicating satellite television or radio reception", + "Representing ground stations and satellite tracking systems", + "Managing antennas, receivers or other wireless communication hardware" + ], "tags": [ "antenna", "receiver", "dish aerial", - "saucer" + "saucer", + "communication", + "broadcast", + "signal" ], "categories": [ "connectivity", diff --git a/icons/satellite-dish.svg b/icons/satellite-dish.svg index a122bc796..ef30b7fee 100644 --- a/icons/satellite-dish.svg +++ b/icons/satellite-dish.svg @@ -9,8 +9,8 @@ stroke-linecap="round" stroke-linejoin="round" > - - - - + + + + diff --git a/icons/save-off.svg b/icons/save-off.svg index ca6177ee2..19207db57 100644 --- a/icons/save-off.svg +++ b/icons/save-off.svg @@ -14,6 +14,5 @@ - diff --git a/icons/scratch-blocks.json b/icons/scratch-blocks.json new file mode 100644 index 000000000..5d8f471a5 --- /dev/null +++ b/icons/scratch-blocks.json @@ -0,0 +1,32 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "mc20000-01", + "karsa-mistmere" + ], + "use-cases": [ + "Representing a block-based code editor in educational apps", + "Indicating a project created with Scratch or similar block editors", + "Toggling a visual programming editor view in development tools", + "Showing integrations with block-based programming platforms" + ], + "tags": [ + "code", + "blocks", + "scratch", + "turbowarp", + "mistwarp", + "visual programming", + "block-based", + "drag-and-drop", + "editor", + "education", + "learning", + "programming", + "kids", + "integration" + ], + "categories": [ + "development" + ] +} diff --git a/icons/scratch-blocks.svg b/icons/scratch-blocks.svg new file mode 100644 index 000000000..fcf8e4a8a --- /dev/null +++ b/icons/scratch-blocks.svg @@ -0,0 +1,14 @@ + + + + diff --git a/icons/shield-alert.json b/icons/shield-alert.json index af0464476..e2ae07c22 100644 --- a/icons/shield-alert.json +++ b/icons/shield-alert.json @@ -45,7 +45,7 @@ "weakness", "infection", "infected", - "comprimised", + "compromised", "data leak", "audited", "admin", diff --git a/icons/shield-ban.json b/icons/shield-ban.json index 9ecfa7265..9c706952b 100644 --- a/icons/shield-ban.json +++ b/icons/shield-ban.json @@ -44,7 +44,7 @@ "weakness", "infection", "infected", - "comprimised", + "compromised", "data leak", "audited", "admin", diff --git a/icons/shield-off.json b/icons/shield-off.json index db17361a6..af52af6a1 100644 --- a/icons/shield-off.json +++ b/icons/shield-off.json @@ -37,7 +37,7 @@ "weakness", "infected", "infection", - "comprimised", + "compromised", "data leak", "unaudited", "admin", diff --git a/icons/shield-question-mark.json b/icons/shield-question-mark.json index 680db8f0b..49950f2b2 100644 --- a/icons/shield-question-mark.json +++ b/icons/shield-question-mark.json @@ -38,7 +38,7 @@ "vulnerable", "weakness", "infection", - "comprimised", + "compromised", "data leak", "audit", "admin", diff --git a/icons/shield-x.json b/icons/shield-x.json index ebd4d6a92..f2ab26afa 100644 --- a/icons/shield-x.json +++ b/icons/shield-x.json @@ -45,7 +45,7 @@ "weakness", "infection", "infected", - "comprimised", + "compromised", "data leak", "audited", "admin", diff --git a/icons/shopping-cart-minus.json b/icons/shopping-cart-minus.json new file mode 100644 index 000000000..a817ce6fe --- /dev/null +++ b/icons/shopping-cart-minus.json @@ -0,0 +1,35 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "ajay199210", + "jguddas", + "ericfennis", + "karsa-mistmere" + ], + "use-cases": [ + "Removing products from a shopping cart", + "Updating order contents in ecommerce apps", + "Indicating fewer items in a purchase" + ], + "tags": [ + "trolley", + "cart", + "basket", + "e-commerce", + "ecommerce", + "store", + "purchase", + "products", + "items", + "checkout", + "order", + "retail", + "remove", + "decrease", + "quantity", + "subtract" + ], + "categories": [ + "shopping" + ] +} diff --git a/icons/shopping-cart-minus.svg b/icons/shopping-cart-minus.svg new file mode 100644 index 000000000..0fd077750 --- /dev/null +++ b/icons/shopping-cart-minus.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/icons/shopping-cart-plus.json b/icons/shopping-cart-plus.json new file mode 100644 index 000000000..4425236b3 --- /dev/null +++ b/icons/shopping-cart-plus.json @@ -0,0 +1,35 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "ajay199210", + "jguddas", + "ericfennis", + "karsa-mistmere" + ], + "use-cases": [ + "Adding products to a shopping cart", + "Saving products for purchase in ecommerce apps", + "Confirming items were added to an order" + ], + "tags": [ + "trolley", + "cart", + "basket", + "e-commerce", + "ecommerce", + "store", + "purchase", + "products", + "items", + "checkout", + "order", + "retail", + "add", + "increase", + "quantity", + "buy" + ], + "categories": [ + "shopping" + ] +} diff --git a/icons/shopping-cart-plus.svg b/icons/shopping-cart-plus.svg new file mode 100644 index 000000000..e52a74750 --- /dev/null +++ b/icons/shopping-cart-plus.svg @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/icons/shrimp-off.json b/icons/shrimp-off.json new file mode 100644 index 000000000..06f8c7fb3 --- /dev/null +++ b/icons/shrimp-off.json @@ -0,0 +1,36 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "karsa-mistmere", + "jguddas" + ], + "use-cases": [ + "Indicating shellfish allergen information", + "Marking seafood-free items on a menu" + ], + "tags": [ + "marine", + "creature", + "fisherman", + "ocean", + "fresh", + "cuisine", + "marine-life", + "seafood", + "shellfish", + "crustacean", + "prawn", + "scallop", + "whelk", + "arthropod", + "littleneck", + "quahog", + "allergy", + "intolerance", + "diet" + ], + "categories": [ + "animals", + "food-beverage" + ] +} diff --git a/icons/shrimp-off.svg b/icons/shrimp-off.svg new file mode 100644 index 000000000..60c0d3aa5 --- /dev/null +++ b/icons/shrimp-off.svg @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/icons/shrimp.json b/icons/shrimp.json index 72f0e552f..ce4a0cdef 100644 --- a/icons/shrimp.json +++ b/icons/shrimp.json @@ -14,9 +14,16 @@ "arthropod", "littleneck", "quahog", - "cherrystone" + "marine", + "creature", + "fisherman", + "ocean", + "fresh", + "cuisine", + "marine-life" ], "categories": [ - "animals" + "animals", + "food-beverage" ] } diff --git a/icons/shrimp.svg b/icons/shrimp.svg index fed36840b..750a59647 100644 --- a/icons/shrimp.svg +++ b/icons/shrimp.svg @@ -9,9 +9,9 @@ stroke-linecap="round" stroke-linejoin="round" > - - - - - + + + + + diff --git a/icons/smartphone.json b/icons/smartphone.json index d3205dd41..e57da1911 100644 --- a/icons/smartphone.json +++ b/icons/smartphone.json @@ -5,12 +5,21 @@ "mittalyashu", "ericfennis" ], - "use-cases": [], + "use-cases": [ + "Representing mobile phones in device settings", + "Showing mobile access or app availability", + "Indicating phone-based communication channels" + ], "tags": [ "phone", "cellphone", "device", - "screen" + "mobile", + "screen", + "display", + "touchscreen", + "portable", + "responsive" ], "categories": [ "connectivity", diff --git a/icons/soup.svg b/icons/soup.svg index db73cb99b..77f8581ab 100644 --- a/icons/soup.svg +++ b/icons/soup.svg @@ -9,10 +9,10 @@ stroke-linecap="round" stroke-linejoin="round" > - - + + - - - + + + diff --git a/icons/square-activity.json b/icons/square-activity.json index d2926f331..adf6cf535 100644 --- a/icons/square-activity.json +++ b/icons/square-activity.json @@ -21,7 +21,7 @@ "hospital", "defibrillator", "earthquake", - "siesmic", + "seismic", "magnitude", "richter scale", "aftershock", diff --git a/icons/album.json b/icons/square-bookmark.json similarity index 57% rename from icons/album.json rename to icons/square-bookmark.json index b9eb7d231..9f2e2508c 100644 --- a/icons/album.json +++ b/icons/square-bookmark.json @@ -3,7 +3,8 @@ "contributors": [ "ericfennis", "csandman", - "mittalyashu" + "mittalyashu", + "karsa-mistmere" ], "use-cases": [], "tags": [ @@ -13,5 +14,12 @@ "categories": [ "photography", "multimedia" + ], + "aliases": [ + { + "name": "album", + "deprecated": true, + "deprecationReason": "alias.name" + } ] } diff --git a/icons/square-bookmark.svg b/icons/square-bookmark.svg new file mode 100644 index 000000000..208a2ea8a --- /dev/null +++ b/icons/square-bookmark.svg @@ -0,0 +1,14 @@ + + + + diff --git a/icons/square-dashed-plus.json b/icons/square-dashed-plus.json new file mode 100644 index 000000000..069b220d5 --- /dev/null +++ b/icons/square-dashed-plus.json @@ -0,0 +1,36 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "llaenowyd", + "mishkaio", + "ericfennis", + "karsa-mistmere", + "chessurisme", + "colebemis", + "psjdev" + ], + "use-cases": [ + "Prompting users to select content for an empty slot", + "Adding an area to an existing selection in design editors", + "Marking a placeholder where users can add a block" + ], + "tags": [ + "selection", + "select", + "add", + "new", + "placeholder", + "marquee", + "box", + "dashed", + "plus", + "insert", + "frame", + "empty" + ], + "categories": [ + "design", + "layout", + "shapes" + ] +} diff --git a/icons/square-dashed-plus.svg b/icons/square-dashed-plus.svg new file mode 100644 index 000000000..e9750c97e --- /dev/null +++ b/icons/square-dashed-plus.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/icons/square-dashed-x-corner.json b/icons/square-dashed-x-corner.json new file mode 100644 index 000000000..bacc6e833 --- /dev/null +++ b/icons/square-dashed-x-corner.json @@ -0,0 +1,41 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "llaenowyd", + "mishkaio", + "ericfennis", + "karsa-mistmere", + "jguddas", + "chessurisme", + "ethanhazel" + ], + "use-cases": [ + "Deselecting a selection in an image editor", + "Clearing a selected range in a spreadsheet", + "Removing selected files from a file manager selection", + "Toggling off marquee/selection mode in a design application" + ], + "tags": [ + "deselect", + "unselect", + "delete", + "select", + "selection", + "border", + "width", + "layout", + "style", + "design", + "rectangular", + "marquee", + "box", + "rectangle", + "aspect ratio", + "1:1" + ], + "categories": [ + "design", + "development", + "layout" + ] +} diff --git a/icons/square-dashed-x-corner.svg b/icons/square-dashed-x-corner.svg new file mode 100644 index 000000000..c04602d24 --- /dev/null +++ b/icons/square-dashed-x-corner.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/icons/square-dashed-x.json b/icons/square-dashed-x.json new file mode 100644 index 000000000..bacc6e833 --- /dev/null +++ b/icons/square-dashed-x.json @@ -0,0 +1,41 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "llaenowyd", + "mishkaio", + "ericfennis", + "karsa-mistmere", + "jguddas", + "chessurisme", + "ethanhazel" + ], + "use-cases": [ + "Deselecting a selection in an image editor", + "Clearing a selected range in a spreadsheet", + "Removing selected files from a file manager selection", + "Toggling off marquee/selection mode in a design application" + ], + "tags": [ + "deselect", + "unselect", + "delete", + "select", + "selection", + "border", + "width", + "layout", + "style", + "design", + "rectangular", + "marquee", + "box", + "rectangle", + "aspect ratio", + "1:1" + ], + "categories": [ + "design", + "development", + "layout" + ] +} diff --git a/icons/square-dashed-x.svg b/icons/square-dashed-x.svg new file mode 100644 index 000000000..c28de3b9d --- /dev/null +++ b/icons/square-dashed-x.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/icons/square-exclamation-point.json b/icons/square-exclamation-point.json new file mode 100644 index 000000000..70f983912 --- /dev/null +++ b/icons/square-exclamation-point.json @@ -0,0 +1,31 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "viralcodex", + "colebemis", + "ericfennis" + ], + "use-cases": [ + "Indicating application or system warnings or alerts", + "Highlighting important notifications that require user attention", + "Representing caution states for different components or elements", + "Marking validation errors, invalid input or configuration issues", + "Displaying diagnostic or troubleshooting information" + ], + "tags": [ + "warning", + "alert", + "danger", + "exclamation mark", + "rectangle", + "square", + "notification", + "attention", + "important", + "caution" + ], + "categories": [ + "notifications", + "shapes" + ] +} diff --git a/icons/square-exclamation-point.svg b/icons/square-exclamation-point.svg new file mode 100644 index 000000000..9055762fa --- /dev/null +++ b/icons/square-exclamation-point.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/icons/square-sparkles.json b/icons/square-sparkles.json new file mode 100644 index 000000000..488435ef6 --- /dev/null +++ b/icons/square-sparkles.json @@ -0,0 +1,24 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "colebemis", + "karsa-mistmere", + "ericfennis", + "nananecy" + ], + "use-cases": [ + "Indicating AI-assisted image generation features", + "Representing generative AI image creation tools" + ], + "tags": [ + "picture", + "photo", + "sparkles" + ], + "categories": [ + "photography", + "text", + "multimedia", + "files" + ] +} diff --git a/icons/square-sparkles.svg b/icons/square-sparkles.svg new file mode 100644 index 000000000..69685e3aa --- /dev/null +++ b/icons/square-sparkles.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/icons/swiss-franc.json b/icons/swiss-franc.json index caf59cb32..1313f7936 100644 --- a/icons/swiss-franc.json +++ b/icons/swiss-franc.json @@ -4,6 +4,8 @@ "ericfennis", "mittalyashu" ], + "deprecated": true, + "deprecationReason": "icon.use-case", "use-cases": [], "tags": [ "currency", diff --git a/icons/table-2.json b/icons/table-2.json index fbbc1d8f9..d5dabbbfd 100644 --- a/icons/table-2.json +++ b/icons/table-2.json @@ -3,7 +3,8 @@ "contributors": [ "abejenaru", "karsa-mistmere", - "ericfennis" + "ericfennis", + "jguddas" ], "use-cases": [], "tags": [ diff --git a/icons/table-2.svg b/icons/table-2.svg index a60705ca3..fa89be67f 100644 --- a/icons/table-2.svg +++ b/icons/table-2.svg @@ -9,5 +9,7 @@ stroke-linecap="round" stroke-linejoin="round" > - + + + diff --git a/icons/tablet-smartphone.json b/icons/tablet-smartphone.json index 0c2174fb6..f0aba491a 100644 --- a/icons/tablet-smartphone.json +++ b/icons/tablet-smartphone.json @@ -3,13 +3,24 @@ "contributors": [ "danielbayley" ], - "use-cases": [], + "use-cases": [ + "Previewing layouts across mobile device sizes", + "Testing responsive behavior on handheld screens", + "Representing mobile-first design workflows" + ], "tags": [ + "phone", + "cellphone", + "device", + "mobile", + "screen", + "display", + "touchscreen", + "portable", "responsive", "screens", "browser", - "testing", - "mobile" + "testing" ], "categories": [ "devices", diff --git a/icons/tablet.json b/icons/tablet.json index e4d631086..4462f9364 100644 --- a/icons/tablet.json +++ b/icons/tablet.json @@ -4,9 +4,18 @@ "colebemis", "ericfennis" ], - "use-cases": [], + "use-cases": [ + "Representing tablet devices in responsive layouts", + "Showing touch-based interfaces on larger mobile screens" + ], "tags": [ - "device" + "device", + "mobile", + "screen", + "display", + "touchscreen", + "portable", + "responsive" ], "categories": [ "devices" diff --git a/icons/text-align-justify-center.json b/icons/text-align-justify-center.json new file mode 100644 index 000000000..4d06b390a --- /dev/null +++ b/icons/text-align-justify-center.json @@ -0,0 +1,26 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "HectorZaGa", + "colebemis", + "ericfennis", + "karsa-mistmere" + ], + "use-cases": [ + "Aligning justified text with the final line centered", + "Formatting paragraphs with center-justified layout in document editors" + ], + "tags": [ + "paragraph", + "alignment", + "justified", + "center", + "middle", + "typography", + "editor", + "document" + ], + "categories": [ + "text" + ] +} diff --git a/icons/text-align-justify-center.svg b/icons/text-align-justify-center.svg new file mode 100644 index 000000000..a1ff0b2c1 --- /dev/null +++ b/icons/text-align-justify-center.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/icons/text-align-justify-end.json b/icons/text-align-justify-end.json new file mode 100644 index 000000000..a6dc27ffe --- /dev/null +++ b/icons/text-align-justify-end.json @@ -0,0 +1,26 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "HectorZaGa", + "colebemis", + "ericfennis", + "karsa-mistmere" + ], + "use-cases": [ + "Aligning justified text with the final line at the end", + "Formatting paragraphs with right-justified layout in document editors" + ], + "tags": [ + "paragraph", + "alignment", + "justified", + "right", + "end", + "typography", + "editor", + "document" + ], + "categories": [ + "text" + ] +} diff --git a/icons/text-align-justify-end.svg b/icons/text-align-justify-end.svg new file mode 100644 index 000000000..2da1ac63b --- /dev/null +++ b/icons/text-align-justify-end.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/icons/text-align-justify-start.json b/icons/text-align-justify-start.json new file mode 100644 index 000000000..fa8a971f2 --- /dev/null +++ b/icons/text-align-justify-start.json @@ -0,0 +1,26 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "HectorZaGa", + "colebemis", + "ericfennis", + "karsa-mistmere" + ], + "use-cases": [ + "Aligning justified text with the final line at the start", + "Formatting paragraphs with left-justified layout in document editors" + ], + "tags": [ + "paragraph", + "alignment", + "justified", + "left", + "start", + "typography", + "editor", + "document" + ], + "categories": [ + "text" + ] +} diff --git a/icons/text-align-justify-start.svg b/icons/text-align-justify-start.svg new file mode 100644 index 000000000..d910f7116 --- /dev/null +++ b/icons/text-align-justify-start.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/icons/tic-tac-toe.json b/icons/tic-tac-toe.json new file mode 100644 index 000000000..4e790640a --- /dev/null +++ b/icons/tic-tac-toe.json @@ -0,0 +1,24 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "karsa-mistmere" + ], + "use-cases": [ + "Representing turn-based board games", + "Showing game boards in gaming interfaces", + "Indicating a casual multiplayer match", + "Representing pencil-and-paper games in kids apps", + "Showing a simple puzzle or challenge in onboarding flows", + "Denoting a tie or draw outcome in game results" + ], + "tags": [ + "game", + "board", + "grid", + "strategy", + "noughts and crosses" + ], + "categories": [ + "gaming" + ] +} diff --git a/icons/tic-tac-toe.svg b/icons/tic-tac-toe.svg new file mode 100644 index 000000000..d5bf27375 --- /dev/null +++ b/icons/tic-tac-toe.svg @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/icons/tool-case.svg b/icons/tool-case.svg index d94c8a352..698758993 100644 --- a/icons/tool-case.svg +++ b/icons/tool-case.svg @@ -10,7 +10,7 @@ stroke-linejoin="round" > - - - + + + diff --git a/icons/toothbrush-sparkles.json b/icons/toothbrush-sparkles.json new file mode 100644 index 000000000..51d9d78d3 --- /dev/null +++ b/icons/toothbrush-sparkles.json @@ -0,0 +1,38 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "jamiemlaw", + "karsa-mistmere", + "jguddas" + ], + "use-cases": [ + "Representing improved oral hygiene or dental care", + "Indicating freshly cleaned teeth in health apps", + "Showing premium personal care amenities in travel apps" + ], + "tags": [ + "dental care", + "oral hygiene", + "brushing", + "teeth", + "dentist", + "bathroom", + "toiletries", + "personal care", + "cleaning", + "bristles", + "handle", + "plaque", + "toothpaste", + "selfcare", + "grooming", + "fresh", + "clean" + ], + "categories": [ + "home", + "tools", + "medical", + "travel" + ] +} diff --git a/icons/toothbrush-sparkles.svg b/icons/toothbrush-sparkles.svg new file mode 100644 index 000000000..eed727496 --- /dev/null +++ b/icons/toothbrush-sparkles.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/icons/toothbrush.json b/icons/toothbrush.json new file mode 100644 index 000000000..7bce524b5 --- /dev/null +++ b/icons/toothbrush.json @@ -0,0 +1,36 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "jamiemlaw", + "karsa-mistmere" + ], + "use-cases": [ + "Representing oral hygiene and dental care", + "Reminding users to brush their teeth in routine apps", + "Indicating dental health services in healthcare apps", + "Listing personal care amenities in travel apps" + ], + "tags": [ + "dental care", + "oral hygiene", + "brushing", + "teeth", + "dentist", + "bathroom", + "toiletries", + "personal care", + "cleaning", + "bristles", + "handle", + "plaque", + "toothpaste", + "selfcare", + "grooming" + ], + "categories": [ + "home", + "travel", + "tools", + "medical" + ] +} diff --git a/icons/toothbrush.svg b/icons/toothbrush.svg new file mode 100644 index 000000000..b1301efb2 --- /dev/null +++ b/icons/toothbrush.svg @@ -0,0 +1,14 @@ + + + + diff --git a/icons/trash-2.json b/icons/trash-2.json deleted file mode 100644 index c3c518011..000000000 --- a/icons/trash-2.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "../icon.schema.json", - "contributors": [ - "colebemis", - "csandman", - "ericfennis", - "karsa-mistmere" - ], - "use-cases": [], - "tags": [ - "garbage", - "delete", - "remove", - "bin" - ], - "categories": [ - "files", - "mail" - ] -} diff --git a/icons/trash-2.svg b/icons/trash-2.svg deleted file mode 100644 index e8cb0edf3..000000000 --- a/icons/trash-2.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/icons/trash-off.json b/icons/trash-off.json new file mode 100644 index 000000000..d6c35fbac --- /dev/null +++ b/icons/trash-off.json @@ -0,0 +1,42 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "colebemis", + "csandman", + "ericfennis", + "karsa-mistmere", + "drewcobleRH" + ], + "use-cases": [ + "Indicating a disabled delete action", + "Showing removal is unavailable or restricted", + "Preventing item deletion in locked states", + "Marking trash bins as full or out of service" + ], + "tags": [ + "empty", + "deletion", + "cleanup", + "junk", + "clear", + "garbage", + "delete", + "remove", + "bin", + "trash", + "waste", + "recycle", + "discard", + "rubbish", + "disabled", + "off", + "prevent", + "locked", + "unavailable", + "protected" + ], + "categories": [ + "files", + "mail" + ] +} diff --git a/icons/trash-off.svg b/icons/trash-off.svg new file mode 100644 index 000000000..d956d02d3 --- /dev/null +++ b/icons/trash-off.svg @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/icons/trash.json b/icons/trash.json index 83c6bfbc9..ad069cd50 100644 --- a/icons/trash.json +++ b/icons/trash.json @@ -26,5 +26,12 @@ "categories": [ "files", "mail" + ], + "aliases": [ + { + "name": "trash-2", + "deprecated": true, + "deprecationReason": "alias.duplicate" + } ] } diff --git a/icons/trash.svg b/icons/trash.svg index 690270a5f..e8cb0edf3 100644 --- a/icons/trash.svg +++ b/icons/trash.svg @@ -9,6 +9,8 @@ stroke-linecap="round" stroke-linejoin="round" > + + diff --git a/icons/flip-horizontal-2.json b/icons/triangles-centerline-dashed-horizontal.json similarity index 65% rename from icons/flip-horizontal-2.json rename to icons/triangles-centerline-dashed-horizontal.json index 395e2442d..99fef8349 100644 --- a/icons/flip-horizontal-2.json +++ b/icons/triangles-centerline-dashed-horizontal.json @@ -14,5 +14,12 @@ "categories": [ "design", "photography" + ], + "aliases": [ + { + "name": "flip-horizontal-2", + "deprecated": true, + "deprecationReason": "alias.name" + } ] } diff --git a/icons/flip-vertical-2.svg b/icons/triangles-centerline-dashed-horizontal.svg similarity index 61% rename from icons/flip-vertical-2.svg rename to icons/triangles-centerline-dashed-horizontal.svg index 26b0775a3..fdf121783 100644 --- a/icons/flip-vertical-2.svg +++ b/icons/triangles-centerline-dashed-horizontal.svg @@ -9,10 +9,10 @@ stroke-linecap="round" stroke-linejoin="round" > - - - + + + diff --git a/icons/flip-vertical-2.json b/icons/triangles-centerline-dashed-vertical.json similarity index 67% rename from icons/flip-vertical-2.json rename to icons/triangles-centerline-dashed-vertical.json index 8b708b3a5..bb5e805fc 100644 --- a/icons/flip-vertical-2.json +++ b/icons/triangles-centerline-dashed-vertical.json @@ -15,5 +15,12 @@ "categories": [ "design", "photography" + ], + "aliases": [ + { + "name": "flip-vertical-2", + "deprecated": true, + "deprecationReason": "alias.name" + } ] } diff --git a/icons/triangles-centerline-dashed-vertical.svg b/icons/triangles-centerline-dashed-vertical.svg new file mode 100644 index 000000000..15a65dc72 --- /dev/null +++ b/icons/triangles-centerline-dashed-vertical.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/icons/tube-lotion.json b/icons/tube-lotion.json new file mode 100644 index 000000000..08dac706d --- /dev/null +++ b/icons/tube-lotion.json @@ -0,0 +1,36 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "AlecRust", + "karsa-mistmere", + "rrod497", + "jguddas" + ], + "use-cases": [ + "Representing skincare and personal-care products in shopping catalogs", + "Indicating lotion or sunscreen application in skincare routines", + "Identifying toiletries in travel packing checklists" + ], + "tags": [ + "sunscreen", + "sunblock", + "spf", + "uv", + "cream", + "skincare", + "toothpaste", + "ointment", + "bottle", + "toiletries", + "bathroom", + "beach", + "summer" + ], + "categories": [ + "medical", + "home", + "shopping", + "travel", + "seasons" + ] +} diff --git a/icons/tube-lotion.svg b/icons/tube-lotion.svg new file mode 100644 index 000000000..08ecfb1b4 --- /dev/null +++ b/icons/tube-lotion.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/icons/user-group.json b/icons/user-group.json new file mode 100644 index 000000000..853fe9a21 --- /dev/null +++ b/icons/user-group.json @@ -0,0 +1,35 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "karsa-mistmere" + ], + "use-cases": [ + "Representing teams in account interfaces", + "Representing communities in social apps", + "Showing group membership status", + "Managing multiple users in admin interfaces" + ], + "tags": [ + "group", + "people", + "team", + "members", + "community", + "membership", + "collaboration", + "organization", + "contacts", + "directory", + "staff", + "family", + "ancestry", + "ancestors", + "lineage", + "parents" + ], + "categories": [ + "account", + "people", + "social" + ] +} diff --git a/icons/user-group.svg b/icons/user-group.svg new file mode 100644 index 000000000..6a8a59f99 --- /dev/null +++ b/icons/user-group.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/icons/user-round-group.json b/icons/user-round-group.json new file mode 100644 index 000000000..853fe9a21 --- /dev/null +++ b/icons/user-round-group.json @@ -0,0 +1,35 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "karsa-mistmere" + ], + "use-cases": [ + "Representing teams in account interfaces", + "Representing communities in social apps", + "Showing group membership status", + "Managing multiple users in admin interfaces" + ], + "tags": [ + "group", + "people", + "team", + "members", + "community", + "membership", + "collaboration", + "organization", + "contacts", + "directory", + "staff", + "family", + "ancestry", + "ancestors", + "lineage", + "parents" + ], + "categories": [ + "account", + "people", + "social" + ] +} diff --git a/icons/user-round-group.svg b/icons/user-round-group.svg new file mode 100644 index 000000000..a5c815f71 --- /dev/null +++ b/icons/user-round-group.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/icons/virus-off.json b/icons/virus-off.json new file mode 100644 index 000000000..453638a15 --- /dev/null +++ b/icons/virus-off.json @@ -0,0 +1,45 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "karsa-mistmere" + ], + "use-cases": [ + "Indicating no malware threats in security tools", + "Showing cleared infections in healthcare apps", + "Marking reduced infection risk in health dashboards", + "Representing blocked pathogens in science content" + ], + "tags": [ + "pathogen", + "infection", + "disease", + "illness", + "contagion", + "outbreak", + "pandemic", + "epidemic", + "covid", + "coronavirus", + "germ", + "microbe", + "microorganism", + "biology", + "medical", + "healthcare", + "malware", + "cybersecurity", + "threat", + "antivirus", + "blocked", + "removed", + "clean", + "safe", + "prevention", + "quarantine" + ], + "categories": [ + "medical", + "science", + "security" + ] +} diff --git a/icons/virus-off.svg b/icons/virus-off.svg new file mode 100644 index 000000000..d15c88563 --- /dev/null +++ b/icons/virus-off.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/virus.json b/icons/virus.json new file mode 100644 index 000000000..70037a884 --- /dev/null +++ b/icons/virus.json @@ -0,0 +1,38 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "karsa-mistmere" + ], + "use-cases": [ + "Representing viral infections in healthcare apps", + "Indicating malware threats in security tools", + "Showing disease outbreaks in public health dashboards", + "Labeling pathogens in biology or science content" + ], + "tags": [ + "pathogen", + "infection", + "disease", + "illness", + "contagion", + "outbreak", + "pandemic", + "epidemic", + "covid", + "coronavirus", + "germ", + "microbe", + "microorganism", + "biology", + "medical", + "healthcare", + "malware", + "cybersecurity", + "threat" + ], + "categories": [ + "medical", + "science", + "security" + ] +} diff --git a/icons/virus.svg b/icons/virus.svg new file mode 100644 index 000000000..401edaf71 --- /dev/null +++ b/icons/virus.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/whistle.json b/icons/whistle.json new file mode 100644 index 000000000..bb99afcb5 --- /dev/null +++ b/icons/whistle.json @@ -0,0 +1,41 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "briz123", + "UsamaKhan", + "ajokt123", + "timmy471", + "jguddas", + "karsa-mistmere" + ], + "use-cases": [ + "Representing coaches or referees in sports apps", + "Signaling alerts in training interfaces", + "Indicating traffic control or public safety roles", + "Marking lifeguard equipment in safety systems" + ], + "tags": [ + "alert", + "signal", + "sound", + "blow", + "noise", + "coach", + "trainer", + "referee", + "gym", + "game", + "training", + "sports", + "traffic cops", + "police", + "lifeguard", + "safety", + "rescue", + "emergency" + ], + "categories": [ + "sports", + "notifications" + ] +} diff --git a/icons/whistle.svg b/icons/whistle.svg new file mode 100644 index 000000000..3edb8a50c --- /dev/null +++ b/icons/whistle.svg @@ -0,0 +1,14 @@ + + + + diff --git a/icons/wifi-cog.svg b/icons/wifi-cog.svg index bebbe997d..221eb40ae 100644 --- a/icons/wifi-cog.svg +++ b/icons/wifi-cog.svg @@ -15,10 +15,10 @@ - + - - + + diff --git a/icons/wind-arrow-up.json b/icons/wind-arrow-up.json new file mode 100644 index 000000000..abdf076a3 --- /dev/null +++ b/icons/wind-arrow-up.json @@ -0,0 +1,30 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "colebemis", + "csandman", + "ericfennis", + "jamiemlaw", + "jamesgpearce" + ], + "use-cases": [ + "Indicating maximum wind speeds or gusts", + "Sorting wind strengths in weather tables" + ], + "tags": [ + "weather", + "air", + "pressure", + "blow", + "gust", + "windy", + "sort", + "increase" + ], + "categories": [ + "weather", + "sustainability", + "arrows", + "navigation" + ] +} diff --git a/icons/wind-arrow-up.svg b/icons/wind-arrow-up.svg new file mode 100644 index 000000000..1e6f6f335 --- /dev/null +++ b/icons/wind-arrow-up.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/integrations/lucide-react/README.md b/integrations/lucide-react/README.md new file mode 100644 index 000000000..f6fa56e8a --- /dev/null +++ b/integrations/lucide-react/README.md @@ -0,0 +1,18 @@ +# Lucide React integrations + +These fixtures exercise the built `lucide-react` package in real consumer frameworks. + +Every fixture runs three checks: + +- a framework production build; +- TypeScript with `noEmit`; +- Vitest Browser Mode in Chromium. + +From the repository root, install Chromium once and run the complete suite: + +```sh +pnpm --filter @lucide/integration-react-vite exec playwright install chromium +pnpm test:integrations:react +``` + +The fixtures depend on `lucide-react` through `workspace:*`, but import only its public package entry points. The root command builds `lucide-react` before running any consumer checks. diff --git a/integrations/lucide-react/nextjs/app/IconShowcase.tsx b/integrations/lucide-react/nextjs/app/IconShowcase.tsx new file mode 100644 index 000000000..c22829e11 --- /dev/null +++ b/integrations/lucide-react/nextjs/app/IconShowcase.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from 'lucide-react'; +import { DynamicIcon } from 'lucide-react/dynamic'; + +export default function IconShowcase() { + return ( +
+ + + + + + + + +
+ ); +} diff --git a/integrations/lucide-react/nextjs/app/layout.tsx b/integrations/lucide-react/nextjs/app/layout.tsx new file mode 100644 index 000000000..bb6b7ea87 --- /dev/null +++ b/integrations/lucide-react/nextjs/app/layout.tsx @@ -0,0 +1,9 @@ +import type { ReactNode } from 'react'; + +export default function RootLayout({ children }: Readonly<{ children: ReactNode }>) { + return ( + + {children} + + ); +} diff --git a/integrations/lucide-react/nextjs/app/page.tsx b/integrations/lucide-react/nextjs/app/page.tsx new file mode 100644 index 000000000..5d204e163 --- /dev/null +++ b/integrations/lucide-react/nextjs/app/page.tsx @@ -0,0 +1,5 @@ +import IconShowcase from './IconShowcase'; + +export default function Page() { + return ; +} diff --git a/integrations/lucide-react/nextjs/next-env.d.ts b/integrations/lucide-react/nextjs/next-env.d.ts new file mode 100644 index 000000000..ce4e94a6b --- /dev/null +++ b/integrations/lucide-react/nextjs/next-env.d.ts @@ -0,0 +1,7 @@ +/// +/// +import "./.next/types/routes.d.ts"; +import "./.next/types/root-params.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/integrations/lucide-react/nextjs/package.json b/integrations/lucide-react/nextjs/package.json new file mode 100644 index 000000000..232edb67a --- /dev/null +++ b/integrations/lucide-react/nextjs/package.json @@ -0,0 +1,28 @@ +{ + "name": "@lucide/integration-react-nextjs", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "build": "next build", + "typecheck": "next typegen && tsc --noEmit", + "test:browser": "vitest run", + "check": "pnpm build && pnpm typecheck && pnpm test:browser" + }, + "dependencies": { + "lucide-react": "workspace:*", + "next": "16.3.6", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "^6.1.1", + "@vitest/browser-playwright": "5.0.3", + "typescript": "^6.0.3", + "vite": "^8.3.2", + "vitest": "5.0.3", + "vitest-browser-react": "2.3.0" + } +} diff --git a/integrations/lucide-react/nextjs/tests/icons.browser.spec.tsx b/integrations/lucide-react/nextjs/tests/icons.browser.spec.tsx new file mode 100644 index 000000000..1a3b946ed --- /dev/null +++ b/integrations/lucide-react/nextjs/tests/icons.browser.spec.tsx @@ -0,0 +1,38 @@ +import { expect, test } from 'vitest'; +import { render } from 'vitest-browser-react'; +import Page from '../app/page'; + +test('renders Lucide public APIs in a real browser', async () => { + const screen = await render(); + + const staticIcon = screen.getByTestId('static-icon'); + await expect.element(staticIcon).toBeVisible(); + await expect.element(staticIcon).toHaveAttribute('width', '24'); + await expect.element(staticIcon).toHaveAttribute('height', '24'); + await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24'); + await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true'); + + const customIcon = screen.getByTestId('custom-icon'); + await expect.element(customIcon).toBeVisible(); + await expect.element(customIcon).toHaveAttribute('width', '48'); + await expect.element(customIcon).toHaveAttribute('height', '48'); + await expect.element(customIcon).toHaveAttribute('stroke', 'red'); + await expect.element(customIcon).toHaveAttribute('stroke-width', '2'); + await expect.element(customIcon).toHaveClass('consumer-icon'); + + const providerIcon = screen.getByTestId('provider-icon'); + await expect.element(providerIcon).toHaveAttribute('width', '32'); + await expect.element(providerIcon).toHaveAttribute('stroke', 'purple'); + await expect.element(providerIcon).toHaveAttribute('stroke-width', '3'); + + const aliasIcon = screen.getByTestId('alias-icon'); + const canonicalIcon = screen.getByTestId('canonical-icon'); + await expect.element(aliasIcon).toBeVisible(); + await expect.element(canonicalIcon).toBeVisible(); + expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML); + + const dynamicIcon = screen.getByTestId('dynamic-icon'); + await expect.element(dynamicIcon).toBeVisible(); + await expect.element(dynamicIcon).toHaveAttribute('aria-label', 'Dynamic circle'); + await expect.element(dynamicIcon).not.toHaveAttribute('aria-hidden'); +}); diff --git a/integrations/lucide-react/nextjs/tsconfig.json b/integrations/lucide-react/nextjs/tsconfig.json new file mode 100644 index 000000000..a0fa7d262 --- /dev/null +++ b/integrations/lucide-react/nextjs/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [{ "name": "next" }], + "types": ["vitest/globals"] + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.ts", + "**/*.tsx", + "**/*.mts" + ], + "exclude": ["node_modules"] +} diff --git a/integrations/lucide-react/nextjs/vitest.config.ts b/integrations/lucide-react/nextjs/vitest.config.ts new file mode 100644 index 000000000..f30d4b90d --- /dev/null +++ b/integrations/lucide-react/nextjs/vitest.config.ts @@ -0,0 +1,15 @@ +import react from '@vitejs/plugin-react'; +import { playwright } from '@vitest/browser-playwright'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [react()], + test: { + browser: { + enabled: true, + headless: true, + provider: playwright(), + instances: [{ browser: 'chromium' }], + }, + }, +}); diff --git a/integrations/lucide-react/react-router/app/IconShowcase.tsx b/integrations/lucide-react/react-router/app/IconShowcase.tsx new file mode 100644 index 000000000..f9b1da045 --- /dev/null +++ b/integrations/lucide-react/react-router/app/IconShowcase.tsx @@ -0,0 +1,32 @@ +import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from 'lucide-react'; +import { DynamicIcon } from 'lucide-react/dynamic'; + +export default function IconShowcase() { + return ( +
+ + + + + + + + +
+ ); +} diff --git a/integrations/lucide-react/react-router/app/root.tsx b/integrations/lucide-react/react-router/app/root.tsx new file mode 100644 index 000000000..2fdee3767 --- /dev/null +++ b/integrations/lucide-react/react-router/app/root.tsx @@ -0,0 +1,27 @@ +import type { ReactNode } from 'react'; +import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'; + +export function Layout({ children }: Readonly<{ children: ReactNode }>) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} diff --git a/integrations/lucide-react/react-router/app/routes.ts b/integrations/lucide-react/react-router/app/routes.ts new file mode 100644 index 000000000..ae841a80e --- /dev/null +++ b/integrations/lucide-react/react-router/app/routes.ts @@ -0,0 +1,6 @@ +import { index, route, type RouteConfig } from '@react-router/dev/routes'; + +export default [ + index('./routes/home.tsx'), + route('about', './routes/about.tsx'), +] satisfies RouteConfig; diff --git a/integrations/lucide-react/react-router/app/routes/about.tsx b/integrations/lucide-react/react-router/app/routes/about.tsx new file mode 100644 index 000000000..6cf9425e9 --- /dev/null +++ b/integrations/lucide-react/react-router/app/routes/about.tsx @@ -0,0 +1,11 @@ +import { ArrowLeft } from 'lucide-react'; +import { Link } from 'react-router'; + +export default function About() { + return ( +
+ + Home +
+ ); +} diff --git a/integrations/lucide-react/react-router/app/routes/home.tsx b/integrations/lucide-react/react-router/app/routes/home.tsx new file mode 100644 index 000000000..979950453 --- /dev/null +++ b/integrations/lucide-react/react-router/app/routes/home.tsx @@ -0,0 +1,11 @@ +import { Link } from 'react-router'; +import IconShowcase from '../IconShowcase'; + +export default function Home() { + return ( + <> + + About + + ); +} diff --git a/integrations/lucide-react/react-router/package.json b/integrations/lucide-react/react-router/package.json new file mode 100644 index 000000000..d103af2d4 --- /dev/null +++ b/integrations/lucide-react/react-router/package.json @@ -0,0 +1,33 @@ +{ + "name": "@lucide/integration-react-react-router", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "build": "react-router build", + "typecheck": "react-router typegen && tsc --noEmit", + "test:browser": "vitest run", + "check": "pnpm build && pnpm typecheck && pnpm test:browser" + }, + "dependencies": { + "@react-router/node": "8.3.1", + "@react-router/serve": "8.3.1", + "isbot": "5.2.2", + "lucide-react": "workspace:*", + "react": "19.3.0", + "react-dom": "19.3.0", + "react-router": "8.3.1" + }, + "devDependencies": { + "@react-router/dev": "8.3.1", + "@types/node": "^26.1.1", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "^6.1.1", + "@vitest/browser-playwright": "5.0.3", + "typescript": "^6.0.3", + "vite": "^8.3.2", + "vitest": "5.0.3", + "vitest-browser-react": "2.3.0" + } +} diff --git a/integrations/lucide-react/react-router/react-router.config.ts b/integrations/lucide-react/react-router/react-router.config.ts new file mode 100644 index 000000000..51e896777 --- /dev/null +++ b/integrations/lucide-react/react-router/react-router.config.ts @@ -0,0 +1,5 @@ +import type { Config } from '@react-router/dev/config'; + +export default { + ssr: true, +} satisfies Config; diff --git a/integrations/lucide-react/react-router/tests/icons.browser.spec.tsx b/integrations/lucide-react/react-router/tests/icons.browser.spec.tsx new file mode 100644 index 000000000..bbddd9236 --- /dev/null +++ b/integrations/lucide-react/react-router/tests/icons.browser.spec.tsx @@ -0,0 +1,52 @@ +import { expect, test } from 'vitest'; +import { render } from 'vitest-browser-react'; +import { MemoryRouter, Route, Routes } from 'react-router'; +import About from '../app/routes/about'; +import Home from '../app/routes/home'; + +test('renders Lucide public APIs and survives router navigation', async () => { + const screen = await render( + + + } + path="/" + /> + } + path="/about" + /> + + , + ); + + const staticIcon = screen.getByTestId('static-icon'); + await expect.element(staticIcon).toBeVisible(); + await expect.element(staticIcon).toHaveAttribute('width', '24'); + await expect.element(staticIcon).toHaveAttribute('height', '24'); + await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24'); + await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true'); + + const customIcon = screen.getByTestId('custom-icon'); + await expect.element(customIcon).toBeVisible(); + await expect.element(customIcon).toHaveAttribute('width', '48'); + const aliasIcon = screen.getByTestId('alias-icon'); + const canonicalIcon = screen.getByTestId('canonical-icon'); + await expect.element(aliasIcon).toBeVisible(); + await expect.element(canonicalIcon).toBeVisible(); + expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML); + await expect.element(customIcon).toHaveClass('consumer-icon'); + + const providerIcon = screen.getByTestId('provider-icon'); + await expect.element(providerIcon).toHaveAttribute('width', '32'); + await expect.element(providerIcon).toHaveAttribute('stroke', 'purple'); + await expect.element(providerIcon).toHaveAttribute('stroke-width', '3'); + + const dynamicIcon = screen.getByTestId('dynamic-icon'); + await expect.element(dynamicIcon).toBeVisible(); + await expect.element(dynamicIcon).toHaveAttribute('aria-label', 'Dynamic circle'); + await expect.element(dynamicIcon).not.toHaveAttribute('aria-hidden'); + + await screen.getByRole('link', { name: 'About' }).click(); + await expect.element(screen.getByTestId('route-icon')).toBeVisible(); +}); diff --git a/integrations/lucide-react/react-router/tsconfig.json b/integrations/lucide-react/react-router/tsconfig.json new file mode 100644 index 000000000..91398e158 --- /dev/null +++ b/integrations/lucide-react/react-router/tsconfig.json @@ -0,0 +1,18 @@ +{ + "include": ["**/*", ".react-router/types/**/*"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["node", "vite/client", "vitest/globals"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], + "verbatimModuleSyntax": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true + } +} diff --git a/integrations/lucide-react/react-router/vite.config.ts b/integrations/lucide-react/react-router/vite.config.ts new file mode 100644 index 000000000..68ba30d69 --- /dev/null +++ b/integrations/lucide-react/react-router/vite.config.ts @@ -0,0 +1,6 @@ +import { reactRouter } from '@react-router/dev/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [reactRouter()], +}); diff --git a/integrations/lucide-react/react-router/vitest.config.ts b/integrations/lucide-react/react-router/vitest.config.ts new file mode 100644 index 000000000..f30d4b90d --- /dev/null +++ b/integrations/lucide-react/react-router/vitest.config.ts @@ -0,0 +1,15 @@ +import react from '@vitejs/plugin-react'; +import { playwright } from '@vitest/browser-playwright'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [react()], + test: { + browser: { + enabled: true, + headless: true, + provider: playwright(), + instances: [{ browser: 'chromium' }], + }, + }, +}); diff --git a/integrations/lucide-react/tanstack-router/index.html b/integrations/lucide-react/tanstack-router/index.html new file mode 100644 index 000000000..84e6a0a0a --- /dev/null +++ b/integrations/lucide-react/tanstack-router/index.html @@ -0,0 +1,18 @@ + + + + + + Lucide React TanStack Router integration + + +
+ + + diff --git a/integrations/lucide-react/tanstack-router/package.json b/integrations/lucide-react/tanstack-router/package.json new file mode 100644 index 000000000..faa9eb4da --- /dev/null +++ b/integrations/lucide-react/tanstack-router/package.json @@ -0,0 +1,29 @@ +{ + "name": "@lucide/integration-react-tanstack-router", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "build": "vite build", + "typecheck": "tsc --noEmit", + "test:browser": "vitest run", + "check": "pnpm build && pnpm typecheck && pnpm test:browser" + }, + "dependencies": { + "@tanstack/react-router": "1.170.32", + "lucide-react": "workspace:*", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@tanstack/router-plugin": "1.168.35", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "^6.1.1", + "@vitest/browser-playwright": "5.0.3", + "typescript": "^6.0.3", + "vite": "^8.3.2", + "vitest": "5.0.3", + "vitest-browser-react": "2.3.0" + } +} diff --git a/integrations/lucide-react/tanstack-router/src/IconShowcase.tsx b/integrations/lucide-react/tanstack-router/src/IconShowcase.tsx new file mode 100644 index 000000000..f9b1da045 --- /dev/null +++ b/integrations/lucide-react/tanstack-router/src/IconShowcase.tsx @@ -0,0 +1,32 @@ +import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from 'lucide-react'; +import { DynamicIcon } from 'lucide-react/dynamic'; + +export default function IconShowcase() { + return ( +
+ + + + + + + + +
+ ); +} diff --git a/integrations/lucide-react/tanstack-router/src/main.tsx b/integrations/lucide-react/tanstack-router/src/main.tsx new file mode 100644 index 000000000..131b80238 --- /dev/null +++ b/integrations/lucide-react/tanstack-router/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { RouterProvider } from '@tanstack/react-router'; +import { router } from './router'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/integrations/lucide-react/tanstack-router/src/routeTree.gen.ts b/integrations/lucide-react/tanstack-router/src/routeTree.gen.ts new file mode 100644 index 000000000..12bc916ae --- /dev/null +++ b/integrations/lucide-react/tanstack-router/src/routeTree.gen.ts @@ -0,0 +1,77 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as AboutRouteImport } from './routes/about' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/about' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/about' + id: '__root__' | '/' | '/about' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/integrations/lucide-react/tanstack-router/src/router.tsx b/integrations/lucide-react/tanstack-router/src/router.tsx new file mode 100644 index 000000000..c1b56e93e --- /dev/null +++ b/integrations/lucide-react/tanstack-router/src/router.tsx @@ -0,0 +1,10 @@ +import { createRouter } from '@tanstack/react-router'; +import { routeTree } from './routeTree.gen'; + +export const router = createRouter({ routeTree }); + +declare module '@tanstack/react-router' { + interface Register { + router: typeof router; + } +} diff --git a/integrations/lucide-react/tanstack-router/src/routes/__root.tsx b/integrations/lucide-react/tanstack-router/src/routes/__root.tsx new file mode 100644 index 000000000..863a18b92 --- /dev/null +++ b/integrations/lucide-react/tanstack-router/src/routes/__root.tsx @@ -0,0 +1,5 @@ +import { Outlet, createRootRoute } from '@tanstack/react-router'; + +export const Route = createRootRoute({ + component: () => , +}); diff --git a/integrations/lucide-react/tanstack-router/src/routes/about.tsx b/integrations/lucide-react/tanstack-router/src/routes/about.tsx new file mode 100644 index 000000000..d7ed14255 --- /dev/null +++ b/integrations/lucide-react/tanstack-router/src/routes/about.tsx @@ -0,0 +1,15 @@ +import { Link, createFileRoute } from '@tanstack/react-router'; +import { ArrowLeft } from 'lucide-react'; + +export const Route = createFileRoute('/about')({ + component: About, +}); + +function About() { + return ( +
+ + Home +
+ ); +} diff --git a/integrations/lucide-react/tanstack-router/src/routes/index.tsx b/integrations/lucide-react/tanstack-router/src/routes/index.tsx new file mode 100644 index 000000000..db58dd3ae --- /dev/null +++ b/integrations/lucide-react/tanstack-router/src/routes/index.tsx @@ -0,0 +1,15 @@ +import { Link, createFileRoute } from '@tanstack/react-router'; +import IconShowcase from '../IconShowcase'; + +export const Route = createFileRoute('/')({ + component: Home, +}); + +function Home() { + return ( + <> + + About + + ); +} diff --git a/integrations/lucide-react/tanstack-router/tests/icons.browser.spec.tsx b/integrations/lucide-react/tanstack-router/tests/icons.browser.spec.tsx new file mode 100644 index 000000000..285afb697 --- /dev/null +++ b/integrations/lucide-react/tanstack-router/tests/icons.browser.spec.tsx @@ -0,0 +1,46 @@ +import { createMemoryHistory, createRouter, RouterProvider } from '@tanstack/react-router'; +import { expect, test } from 'vitest'; +import { render } from 'vitest-browser-react'; +import { routeTree } from '../src/routeTree.gen'; + +test('renders Lucide public APIs and survives router navigation', async () => { + const router = createRouter({ + history: createMemoryHistory({ initialEntries: ['/'] }), + routeTree, + }); + const screen = await render(); + + const staticIcon = screen.getByTestId('static-icon'); + await expect.element(staticIcon).toBeVisible(); + await expect.element(staticIcon).toHaveAttribute('width', '24'); + await expect.element(staticIcon).toHaveAttribute('height', '24'); + await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24'); + await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true'); + + const customIcon = screen.getByTestId('custom-icon'); + await expect.element(customIcon).toBeVisible(); + await expect.element(customIcon).toHaveAttribute('width', '48'); + await expect.element(customIcon).toHaveAttribute('height', '48'); + await expect.element(customIcon).toHaveAttribute('stroke', 'red'); + await expect.element(customIcon).toHaveAttribute('stroke-width', '2'); + await expect.element(customIcon).toHaveClass('consumer-icon'); + + const providerIcon = screen.getByTestId('provider-icon'); + await expect.element(providerIcon).toHaveAttribute('width', '32'); + await expect.element(providerIcon).toHaveAttribute('stroke', 'purple'); + await expect.element(providerIcon).toHaveAttribute('stroke-width', '3'); + + const aliasIcon = screen.getByTestId('alias-icon'); + const canonicalIcon = screen.getByTestId('canonical-icon'); + await expect.element(aliasIcon).toBeVisible(); + await expect.element(canonicalIcon).toBeVisible(); + expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML); + + const dynamicIcon = screen.getByTestId('dynamic-icon'); + await expect.element(dynamicIcon).toBeVisible(); + await expect.element(dynamicIcon).toHaveAttribute('aria-label', 'Dynamic circle'); + await expect.element(dynamicIcon).not.toHaveAttribute('aria-hidden'); + + await screen.getByRole('link', { name: 'About' }).click(); + await expect.element(screen.getByTestId('route-icon')).toBeVisible(); +}); diff --git a/integrations/lucide-react/tanstack-router/tsconfig.json b/integrations/lucide-react/tanstack-router/tsconfig.json new file mode 100644 index 000000000..47b2dc739 --- /dev/null +++ b/integrations/lucide-react/tanstack-router/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["vite/client", "vitest/globals"] + }, + "include": ["src", "tests", "vite.config.ts", "vitest.config.ts"] +} diff --git a/integrations/lucide-react/tanstack-router/vite.config.ts b/integrations/lucide-react/tanstack-router/vite.config.ts new file mode 100644 index 000000000..34e94dc44 --- /dev/null +++ b/integrations/lucide-react/tanstack-router/vite.config.ts @@ -0,0 +1,13 @@ +import react from '@vitejs/plugin-react'; +import { tanstackRouter } from '@tanstack/router-plugin/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + tanstackRouter({ + target: 'react', + autoCodeSplitting: true, + }), + react(), + ], +}); diff --git a/integrations/lucide-react/tanstack-router/vitest.config.ts b/integrations/lucide-react/tanstack-router/vitest.config.ts new file mode 100644 index 000000000..0aef508c8 --- /dev/null +++ b/integrations/lucide-react/tanstack-router/vitest.config.ts @@ -0,0 +1,17 @@ +import { playwright } from '@vitest/browser-playwright'; +import { defineConfig, mergeConfig } from 'vitest/config'; +import viteConfig from './vite.config'; + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + browser: { + enabled: true, + headless: true, + provider: playwright(), + instances: [{ browser: 'chromium' }], + }, + }, + }), +); diff --git a/integrations/lucide-react/tanstack-start/package.json b/integrations/lucide-react/tanstack-start/package.json new file mode 100644 index 000000000..c93322c25 --- /dev/null +++ b/integrations/lucide-react/tanstack-start/package.json @@ -0,0 +1,30 @@ +{ + "name": "@lucide/integration-react-tanstack-start", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "build": "vite build", + "typecheck": "tsc --noEmit", + "test:browser": "vitest run", + "check": "pnpm build && pnpm typecheck && pnpm test:browser" + }, + "dependencies": { + "@tanstack/react-router": "1.170.41", + "@tanstack/react-start": "1.168.60", + "lucide-react": "workspace:*", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@types/node": "^26.1.1", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "^6.1.1", + "@vitest/browser-playwright": "5.0.3", + "typescript": "^6.0.3", + "vite": "^8.3.2", + "vitest": "5.0.3", + "vitest-browser-react": "2.3.0" + } +} diff --git a/integrations/lucide-react/tanstack-start/src/IconShowcase.tsx b/integrations/lucide-react/tanstack-start/src/IconShowcase.tsx new file mode 100644 index 000000000..f9b1da045 --- /dev/null +++ b/integrations/lucide-react/tanstack-start/src/IconShowcase.tsx @@ -0,0 +1,32 @@ +import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from 'lucide-react'; +import { DynamicIcon } from 'lucide-react/dynamic'; + +export default function IconShowcase() { + return ( +
+ + + + + + + + +
+ ); +} diff --git a/integrations/lucide-react/tanstack-start/src/routeTree.gen.ts b/integrations/lucide-react/tanstack-start/src/routeTree.gen.ts new file mode 100644 index 000000000..d89758e68 --- /dev/null +++ b/integrations/lucide-react/tanstack-start/src/routeTree.gen.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as IndexRouteImport } from './routes/index' +import { Route as AboutRouteImport } from './routes/about' + +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const AboutRoute = AboutRouteImport.update({ + id: '/about', + path: '/about', + getParentRoute: () => rootRouteImport, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/about': typeof AboutRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/about': typeof AboutRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/about': typeof AboutRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/about' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/about' + id: '__root__' | '/' | '/about' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AboutRoute: typeof AboutRoute +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/about': { + id: '/about' + path: '/about' + fullPath: '/about' + preLoaderRoute: typeof AboutRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AboutRoute: AboutRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/integrations/lucide-react/tanstack-start/src/router.tsx b/integrations/lucide-react/tanstack-start/src/router.tsx new file mode 100644 index 000000000..e474d9811 --- /dev/null +++ b/integrations/lucide-react/tanstack-start/src/router.tsx @@ -0,0 +1,15 @@ +import { createRouter } from '@tanstack/react-router'; +import { routeTree } from './routeTree.gen'; + +export function getRouter() { + return createRouter({ + routeTree, + scrollRestoration: true, + }); +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType; + } +} diff --git a/integrations/lucide-react/tanstack-start/src/routes/__root.tsx b/integrations/lucide-react/tanstack-start/src/routes/__root.tsx new file mode 100644 index 000000000..85ce34b68 --- /dev/null +++ b/integrations/lucide-react/tanstack-start/src/routes/__root.tsx @@ -0,0 +1,35 @@ +import type { ReactNode } from 'react'; +import { HeadContent, Outlet, Scripts, createRootRoute } from '@tanstack/react-router'; + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' }, + { title: 'Lucide React TanStack Start integration' }, + ], + }), + component: RootComponent, +}); + +function RootComponent() { + return ( + + + + ); +} + +function RootDocument({ children }: Readonly<{ children: ReactNode }>) { + return ( + + + + + + {children} + + + + ); +} diff --git a/integrations/lucide-react/tanstack-start/src/routes/about.tsx b/integrations/lucide-react/tanstack-start/src/routes/about.tsx new file mode 100644 index 000000000..d7ed14255 --- /dev/null +++ b/integrations/lucide-react/tanstack-start/src/routes/about.tsx @@ -0,0 +1,15 @@ +import { Link, createFileRoute } from '@tanstack/react-router'; +import { ArrowLeft } from 'lucide-react'; + +export const Route = createFileRoute('/about')({ + component: About, +}); + +function About() { + return ( +
+ + Home +
+ ); +} diff --git a/integrations/lucide-react/tanstack-start/src/routes/index.tsx b/integrations/lucide-react/tanstack-start/src/routes/index.tsx new file mode 100644 index 000000000..db58dd3ae --- /dev/null +++ b/integrations/lucide-react/tanstack-start/src/routes/index.tsx @@ -0,0 +1,15 @@ +import { Link, createFileRoute } from '@tanstack/react-router'; +import IconShowcase from '../IconShowcase'; + +export const Route = createFileRoute('/')({ + component: Home, +}); + +function Home() { + return ( + <> + + About + + ); +} diff --git a/integrations/lucide-react/tanstack-start/tests/icons.browser.spec.tsx b/integrations/lucide-react/tanstack-start/tests/icons.browser.spec.tsx new file mode 100644 index 000000000..3d34789f6 --- /dev/null +++ b/integrations/lucide-react/tanstack-start/tests/icons.browser.spec.tsx @@ -0,0 +1,38 @@ +import { expect, test } from 'vitest'; +import { render } from 'vitest-browser-react'; +import IconShowcase from '../src/IconShowcase'; + +test('renders Lucide public APIs in a real browser', async () => { + const screen = await render(); + + const staticIcon = screen.getByTestId('static-icon'); + await expect.element(staticIcon).toBeVisible(); + await expect.element(staticIcon).toHaveAttribute('width', '24'); + await expect.element(staticIcon).toHaveAttribute('height', '24'); + await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24'); + await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true'); + + const customIcon = screen.getByTestId('custom-icon'); + await expect.element(customIcon).toBeVisible(); + await expect.element(customIcon).toHaveAttribute('width', '48'); + await expect.element(customIcon).toHaveAttribute('height', '48'); + await expect.element(customIcon).toHaveAttribute('stroke', 'red'); + await expect.element(customIcon).toHaveAttribute('stroke-width', '2'); + await expect.element(customIcon).toHaveClass('consumer-icon'); + + const providerIcon = screen.getByTestId('provider-icon'); + await expect.element(providerIcon).toHaveAttribute('width', '32'); + await expect.element(providerIcon).toHaveAttribute('stroke', 'purple'); + await expect.element(providerIcon).toHaveAttribute('stroke-width', '3'); + + const aliasIcon = screen.getByTestId('alias-icon'); + const canonicalIcon = screen.getByTestId('canonical-icon'); + await expect.element(aliasIcon).toBeVisible(); + await expect.element(canonicalIcon).toBeVisible(); + expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML); + + const dynamicIcon = screen.getByTestId('dynamic-icon'); + await expect.element(dynamicIcon).toBeVisible(); + await expect.element(dynamicIcon).toHaveAttribute('aria-label', 'Dynamic circle'); + await expect.element(dynamicIcon).not.toHaveAttribute('aria-hidden'); +}); diff --git a/integrations/lucide-react/tanstack-start/tsconfig.json b/integrations/lucide-react/tanstack-start/tsconfig.json new file mode 100644 index 000000000..8de805f01 --- /dev/null +++ b/integrations/lucide-react/tanstack-start/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "types": ["node", "vite/client", "vitest/globals"] + }, + "include": ["src", "tests", "vite.config.ts", "vitest.config.ts"] +} diff --git a/integrations/lucide-react/tanstack-start/vite.config.ts b/integrations/lucide-react/tanstack-start/vite.config.ts new file mode 100644 index 000000000..d8177653f --- /dev/null +++ b/integrations/lucide-react/tanstack-start/vite.config.ts @@ -0,0 +1,7 @@ +import react from '@vitejs/plugin-react'; +import { tanstackStart } from '@tanstack/react-start/plugin/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [tanstackStart(), react()], +}); diff --git a/integrations/lucide-react/tanstack-start/vitest.config.ts b/integrations/lucide-react/tanstack-start/vitest.config.ts new file mode 100644 index 000000000..f30d4b90d --- /dev/null +++ b/integrations/lucide-react/tanstack-start/vitest.config.ts @@ -0,0 +1,15 @@ +import react from '@vitejs/plugin-react'; +import { playwright } from '@vitest/browser-playwright'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [react()], + test: { + browser: { + enabled: true, + headless: true, + provider: playwright(), + instances: [{ browser: 'chromium' }], + }, + }, +}); diff --git a/integrations/lucide-react/vite/index.html b/integrations/lucide-react/vite/index.html new file mode 100644 index 000000000..37234e0dc --- /dev/null +++ b/integrations/lucide-react/vite/index.html @@ -0,0 +1,18 @@ + + + + + + Lucide React Vite integration + + +
+ + + diff --git a/integrations/lucide-react/vite/package.json b/integrations/lucide-react/vite/package.json new file mode 100644 index 000000000..726b9947b --- /dev/null +++ b/integrations/lucide-react/vite/package.json @@ -0,0 +1,28 @@ +{ + "name": "@lucide/integration-react-vite", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "build": "vite build", + "typecheck": "tsc --noEmit", + "test:browser": "vitest run", + "check": "pnpm build && pnpm typecheck && pnpm test:browser" + }, + "dependencies": { + "lucide-react": "workspace:*", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "^6.1.1", + "@vitest/browser-playwright": "5.0.3", + "playwright": "1.58.2", + "typescript": "^6.0.3", + "vite": "^8.3.2", + "vitest": "5.0.3", + "vitest-browser-react": "2.3.0" + } +} diff --git a/integrations/lucide-react/vite/src/App.tsx b/integrations/lucide-react/vite/src/App.tsx new file mode 100644 index 000000000..28c729c09 --- /dev/null +++ b/integrations/lucide-react/vite/src/App.tsx @@ -0,0 +1,5 @@ +import IconShowcase from './IconShowcase'; + +export default function App() { + return ; +} diff --git a/integrations/lucide-react/vite/src/IconShowcase.tsx b/integrations/lucide-react/vite/src/IconShowcase.tsx new file mode 100644 index 000000000..f9b1da045 --- /dev/null +++ b/integrations/lucide-react/vite/src/IconShowcase.tsx @@ -0,0 +1,32 @@ +import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from 'lucide-react'; +import { DynamicIcon } from 'lucide-react/dynamic'; + +export default function IconShowcase() { + return ( +
+ + + + + + + + +
+ ); +} diff --git a/integrations/lucide-react/vite/src/main.tsx b/integrations/lucide-react/vite/src/main.tsx new file mode 100644 index 000000000..d9736adc9 --- /dev/null +++ b/integrations/lucide-react/vite/src/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/integrations/lucide-react/vite/tests/icons.browser.spec.tsx b/integrations/lucide-react/vite/tests/icons.browser.spec.tsx new file mode 100644 index 000000000..a2f9982ab --- /dev/null +++ b/integrations/lucide-react/vite/tests/icons.browser.spec.tsx @@ -0,0 +1,38 @@ +import { expect, test } from 'vitest'; +import { render } from 'vitest-browser-react'; +import App from '../src/App'; + +test('renders Lucide public APIs in a real browser', async () => { + const screen = await render(); + + const staticIcon = screen.getByTestId('static-icon'); + await expect.element(staticIcon).toBeVisible(); + await expect.element(staticIcon).toHaveAttribute('width', '24'); + await expect.element(staticIcon).toHaveAttribute('height', '24'); + await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24'); + await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true'); + + const customIcon = screen.getByTestId('custom-icon'); + await expect.element(customIcon).toBeVisible(); + await expect.element(customIcon).toHaveAttribute('width', '48'); + await expect.element(customIcon).toHaveAttribute('height', '48'); + await expect.element(customIcon).toHaveAttribute('stroke', 'red'); + await expect.element(customIcon).toHaveAttribute('stroke-width', '2'); + await expect.element(customIcon).toHaveClass('consumer-icon'); + + const providerIcon = screen.getByTestId('provider-icon'); + await expect.element(providerIcon).toHaveAttribute('width', '32'); + await expect.element(providerIcon).toHaveAttribute('stroke', 'purple'); + await expect.element(providerIcon).toHaveAttribute('stroke-width', '3'); + + const aliasIcon = screen.getByTestId('alias-icon'); + const canonicalIcon = screen.getByTestId('canonical-icon'); + await expect.element(aliasIcon).toBeVisible(); + await expect.element(canonicalIcon).toBeVisible(); + expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML); + + const dynamicIcon = screen.getByTestId('dynamic-icon'); + await expect.element(dynamicIcon).toBeVisible(); + await expect.element(dynamicIcon).toHaveAttribute('aria-label', 'Dynamic circle'); + await expect.element(dynamicIcon).not.toHaveAttribute('aria-hidden'); +}); diff --git a/integrations/lucide-react/vite/tsconfig.json b/integrations/lucide-react/vite/tsconfig.json new file mode 100644 index 000000000..4c75ccce2 --- /dev/null +++ b/integrations/lucide-react/vite/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["vitest/globals"] + }, + "include": ["src", "tests", "vite.config.ts"] +} diff --git a/integrations/lucide-react/vite/vite.config.ts b/integrations/lucide-react/vite/vite.config.ts new file mode 100644 index 000000000..f30d4b90d --- /dev/null +++ b/integrations/lucide-react/vite/vite.config.ts @@ -0,0 +1,15 @@ +import react from '@vitejs/plugin-react'; +import { playwright } from '@vitest/browser-playwright'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [react()], + test: { + browser: { + enabled: true, + headless: true, + provider: playwright(), + instances: [{ browser: 'chromium' }], + }, + }, +}); diff --git a/integrations/lucide-solid-2/README.md b/integrations/lucide-solid-2/README.md new file mode 100644 index 000000000..f6d7961cb --- /dev/null +++ b/integrations/lucide-solid-2/README.md @@ -0,0 +1,36 @@ +# Lucide Solid 2 integrations + +These fixtures exercise the built `@lucide/solid` package in a real Solid 2 (release candidate) consumer app. + +The `vite` fixture was scaffolded with the Solid 2.0 `basic` template: + +```sh +npm create solid@latest -- --solid -t basic --ts +``` + +It keeps the template's turnkey client mode (`@solidjs/vite-plugin` with `start: true`, `filesystem-routing` and `@solidjs/router`), with the demo pages replaced by the icon showcase. The template's tooling versions are aligned with the workspace: TypeScript 6 and the workspace's pinned Vitest 4. + +There is no SolidStart fixture yet, because SolidStart still targets Solid 1. The Solid 1 fixtures for `lucide-solid` live in [`../lucide-solid`](../lucide-solid). + +Every fixture runs three checks: + +- a framework production build; +- TypeScript with `noEmit`; +- Vitest Browser Mode in Chromium. + +From the repository root, install Chromium once and run the complete suite: + +```sh +pnpm --filter @lucide/integration-solid-2-vite exec playwright install chromium +pnpm test:integrations:solid-2 +``` + +The fixtures depend on `@lucide/solid` through `workspace:*`, but import only its public package entry points. The root command builds `@lucide/solid` before running any consumer checks. + +## Two TypeScript projects per fixture + +`tsconfig.json` typechecks the fixture's own sources with `skipLibCheck: true`. + +`tsconfig.declarations.json` compiles only `src/IconShowcase.tsx` (the file that imports the public entry points) with `skipLibCheck: false` and no ambient `types`. This makes `tsc` read the declarations shipped in `dist/types`, so a `.d.ts` that points at a file the build never emitted fails the check. Both run under `pnpm typecheck`. + +`file-routes.d.ts` is generated by `filesystem-routing` on every build and is committed so `tsc` can resolve `virtual:file-routes` before a build has run. diff --git a/integrations/lucide-solid-2/vite/file-routes.d.ts b/integrations/lucide-solid-2/vite/file-routes.d.ts new file mode 100644 index 000000000..341e8e5f6 --- /dev/null +++ b/integrations/lucide-solid-2/vite/file-routes.d.ts @@ -0,0 +1,71 @@ +// Generated by filesystem-routing. Do not edit. +// Regenerated whenever a route file is added, changed or removed. + +declare module "virtual:file-routes" { + /** A code-split ref: the delivery adapter emits it as a dynamic import. */ + export interface FileRouteLazyRef> { + src: string; + import(): Promise; + } + + /** An eager ref: its picked exports are imported statically. */ + export interface FileRouteEagerRef> { + /** The source module path — present when the convention emitted the ref lazy and `codeSplitting: false` delivered it eagerly. */ + src?: string; + require(): M; + } + + /** A flat entry of the route manifest. */ + export interface FileRouteEntry { + path: string; + page?: boolean; + /** The page component is a server function; its `$component` is delivered eagerly. */ + server?: boolean; + $component?: FileRouteLazyRef | FileRouteEagerRef; + $$route?: FileRouteEagerRef; + [key: string]: unknown; + } + + /** An entry of the nested `pageRoutes` view. */ + export interface FileRouteTreeEntry extends FileRouteEntry { + id: string; + children?: readonly FileRouteTreeEntry[]; + } + + /** The flat route manifest, in scan order. */ + const routes: readonly [ + { + path: "/about"; + page: true; + $component: FileRouteLazyRef; + $$route?: undefined; + }, + { + path: "/"; + page: true; + $component: FileRouteLazyRef; + $$route?: undefined; + } + ]; + export default routes; + + /** The page entries, nested by path with grouping segments stripped. */ + export const pageRoutes: readonly [ + { + path: "/"; + id: "/"; + page: true; + $component: FileRouteLazyRef; + $$route?: undefined; + children?: undefined; + }, + { + path: "/about"; + id: "/about"; + page: true; + $component: FileRouteLazyRef; + $$route?: undefined; + children?: undefined; + } + ]; +} diff --git a/integrations/lucide-solid-2/vite/package.json b/integrations/lucide-solid-2/vite/package.json new file mode 100644 index 000000000..dcbb03ea2 --- /dev/null +++ b/integrations/lucide-solid-2/vite/package.json @@ -0,0 +1,29 @@ +{ + "name": "@lucide/integration-solid-2-vite", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "build": "vite build", + "typecheck": "tsc --noEmit && tsc --noEmit --project tsconfig.declarations.json", + "test:browser": "vitest run", + "check": "pnpm build && pnpm typecheck && pnpm test:browser" + }, + "dependencies": { + "@lucide/solid": "workspace:*", + "@solidjs/meta": "1.0.0-next.2", + "@solidjs/router": "2.0.0-next.34", + "@solidjs/web": "2.0.0-rc.13", + "solid-js": "2.0.0-rc.13" + }, + "devDependencies": { + "@solidjs/testing-library": "1.0.0-beta.3", + "@solidjs/vite-plugin": "3.0.0-next.47", + "@vitest/browser-playwright": "5.0.3", + "filesystem-routing": "0.4.0", + "playwright": "1.58.2", + "typescript": "^6.0.3", + "vite": "^8.3.2", + "vitest": "5.0.3" + } +} diff --git a/integrations/lucide-solid-2/vite/src/App.tsx b/integrations/lucide-solid-2/vite/src/App.tsx new file mode 100644 index 000000000..702627104 --- /dev/null +++ b/integrations/lucide-solid-2/vite/src/App.tsx @@ -0,0 +1,6 @@ +import { Loading } from 'solid-js'; +import { Router } from './router'; + +export default function App() { + return {(props) => {props.children}}; +} diff --git a/integrations/lucide-solid-2/vite/src/Document.tsx b/integrations/lucide-solid-2/vite/src/Document.tsx new file mode 100644 index 000000000..d9dd818d3 --- /dev/null +++ b/integrations/lucide-solid-2/vite/src/Document.tsx @@ -0,0 +1,19 @@ +import { HydrationScript } from '@solidjs/web'; +import type { ParentProps } from 'solid-js'; + +export default function Document(props: ParentProps) { + return ( + + + + + Lucide Solid 2 Vite integration + + + {props.children} + + ); +} diff --git a/integrations/lucide-solid-2/vite/src/IconShowcase.tsx b/integrations/lucide-solid-2/vite/src/IconShowcase.tsx new file mode 100644 index 000000000..bd7eefc75 --- /dev/null +++ b/integrations/lucide-solid-2/vite/src/IconShowcase.tsx @@ -0,0 +1,31 @@ +import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from '@lucide/solid'; +import Circle from '@lucide/solid/icons/circle'; + +export default function IconShowcase() { + return ( +
+ + + + + + + + +
+ ); +} diff --git a/integrations/lucide-solid-2/vite/src/router.ts b/integrations/lucide-solid-2/vite/src/router.ts new file mode 100644 index 000000000..006d87ed2 --- /dev/null +++ b/integrations/lucide-solid-2/vite/src/router.ts @@ -0,0 +1,7 @@ +import { createRouter } from '@solidjs/router'; +import { fileRoutes } from '@solidjs/router/fs'; +import { pageRoutes } from 'virtual:file-routes'; + +export const routes = fileRoutes(pageRoutes); + +export const Router = createRouter({ routes }); diff --git a/integrations/lucide-solid-2/vite/src/routes/about.tsx b/integrations/lucide-solid-2/vite/src/routes/about.tsx new file mode 100644 index 000000000..16a3ab3e8 --- /dev/null +++ b/integrations/lucide-solid-2/vite/src/routes/about.tsx @@ -0,0 +1,10 @@ +import { ArrowLeft } from '@lucide/solid'; + +export default function About() { + return ( +
+ + Home +
+ ); +} diff --git a/integrations/lucide-solid-2/vite/src/routes/index.tsx b/integrations/lucide-solid-2/vite/src/routes/index.tsx new file mode 100644 index 000000000..5a572403e --- /dev/null +++ b/integrations/lucide-solid-2/vite/src/routes/index.tsx @@ -0,0 +1,10 @@ +import IconShowcase from '../IconShowcase'; + +export default function Home() { + return ( + <> + + About + + ); +} diff --git a/integrations/lucide-solid-2/vite/src/vite-env.d.ts b/integrations/lucide-solid-2/vite/src/vite-env.d.ts new file mode 100644 index 000000000..56c779452 --- /dev/null +++ b/integrations/lucide-solid-2/vite/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/integrations/lucide-solid-2/vite/tests/icons.browser.spec.tsx b/integrations/lucide-solid-2/vite/tests/icons.browser.spec.tsx new file mode 100644 index 000000000..f767d2261 --- /dev/null +++ b/integrations/lucide-solid-2/vite/tests/icons.browser.spec.tsx @@ -0,0 +1,46 @@ +import { createRouter, memoryHistory } from '@solidjs/router'; +import { render } from '@solidjs/testing-library'; +import { Loading } from 'solid-js'; +import { expect, test } from 'vitest'; +import { page } from 'vitest/browser'; +import { routes } from '../src/router'; + +test('renders Lucide public APIs and survives router navigation', async () => { + const Router = createRouter({ routes, history: memoryHistory('/') }); + + render(() => {(props) => {props.children}}); + + const staticIcon = page.getByTestId('static-icon'); + await expect.element(staticIcon).toBeVisible(); + await expect.element(staticIcon).toHaveAttribute('width', '24'); + await expect.element(staticIcon).toHaveAttribute('height', '24'); + await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24'); + await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true'); + + const customIcon = page.getByTestId('custom-icon'); + await expect.element(customIcon).toBeVisible(); + await expect.element(customIcon).toHaveAttribute('width', '48'); + await expect.element(customIcon).toHaveAttribute('height', '48'); + await expect.element(customIcon).toHaveAttribute('stroke', 'red'); + await expect.element(customIcon).toHaveAttribute('stroke-width', '2'); + await expect.element(customIcon).toHaveClass('consumer-icon'); + + const providerIcon = page.getByTestId('provider-icon'); + await expect.element(providerIcon).toHaveAttribute('width', '32'); + await expect.element(providerIcon).toHaveAttribute('stroke', 'purple'); + await expect.element(providerIcon).toHaveAttribute('stroke-width', '3'); + + const aliasIcon = page.getByTestId('alias-icon'); + const canonicalIcon = page.getByTestId('canonical-icon'); + await expect.element(aliasIcon).toBeVisible(); + await expect.element(canonicalIcon).toBeVisible(); + expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML); + + const deepImportIcon = page.getByTestId('deep-import-icon'); + await expect.element(deepImportIcon).toBeVisible(); + await expect.element(deepImportIcon).toHaveAttribute('aria-label', 'Deep import circle'); + await expect.element(deepImportIcon).not.toHaveAttribute('aria-hidden'); + + await page.getByRole('link', { name: 'About' }).click(); + await expect.element(page.getByTestId('route-icon')).toBeVisible(); +}); diff --git a/integrations/lucide-solid-2/vite/tsconfig.declarations.json b/integrations/lucide-solid-2/vite/tsconfig.declarations.json new file mode 100644 index 000000000..3b3bdf14e --- /dev/null +++ b/integrations/lucide-solid-2/vite/tsconfig.declarations.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "skipLibCheck": false, + "types": [] + }, + "include": ["src/IconShowcase.tsx"] +} diff --git a/integrations/lucide-solid-2/vite/tsconfig.json b/integrations/lucide-solid-2/vite/tsconfig.json new file mode 100644 index 000000000..afb79d079 --- /dev/null +++ b/integrations/lucide-solid-2/vite/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "isolatedModules": true, + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "types": ["vite/client"] + }, + "include": ["src", "tests", "file-routes.d.ts", "vite.config.ts"] +} diff --git a/integrations/lucide-solid-2/vite/vite.config.ts b/integrations/lucide-solid-2/vite/vite.config.ts new file mode 100644 index 000000000..65025e63d --- /dev/null +++ b/integrations/lucide-solid-2/vite/vite.config.ts @@ -0,0 +1,22 @@ +import solid from '@solidjs/vite-plugin'; +import { playwright } from '@vitest/browser-playwright'; +import { fileRoutes } from 'filesystem-routing/vite'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + // Scaffolded with `npm create solid@latest -- --solid -t basic`: turnkey client + // mode generates the entries around src/App.tsx (wrapped in src/Document.tsx) + // and `vite build` prerenders the shell into dist/client. + plugins: [solid({ start: true, extensions: ['.jsx', '.tsx'] }), fileRoutes({ types: true })], + test: { + browser: { + enabled: true, + headless: true, + provider: playwright(), + instances: [{ browser: 'chromium' }], + }, + }, + build: { + target: 'esnext', + }, +}); diff --git a/integrations/lucide-solid/README.md b/integrations/lucide-solid/README.md new file mode 100644 index 000000000..c06b7ce7f --- /dev/null +++ b/integrations/lucide-solid/README.md @@ -0,0 +1,26 @@ +# Lucide Solid integrations + +These fixtures exercise the built `lucide-solid` package in real consumer frameworks. + +Every fixture runs three checks: + +- a framework production build; +- TypeScript with `noEmit`; +- Vitest Browser Mode in Chromium. + +From the repository root, install Chromium once and run the complete suite: + +```sh +pnpm --filter @lucide/integration-solid-vite exec playwright install chromium +pnpm test:integrations:solid +``` + +The fixtures depend on `lucide-solid` through `workspace:*`, but import only its public package entry points. The root command builds `lucide-solid` before running any consumer checks. + +## Two TypeScript projects per fixture + +`tsconfig.json` typechecks the fixture's own sources with `skipLibCheck: true`, like the React fixtures do, because the third-party declarations a Vite app pulls in are not clean under TypeScript 6. + +`tsconfig.declarations.json` compiles only `src/IconShowcase.tsx` — the file that imports the public entry points — with `skipLibCheck: false` and no ambient `types`. That makes `tsc` read the declarations shipped in `dist/types` instead of skipping them, so a `.d.ts` that points at a file the build never emitted fails the check. Both run under `pnpm typecheck`. + +Note that this does not reproduce the bug fixed in #4846, where the published declarations imported `@lucide/shared/types`: inside the workspace that specifier still resolves through `packages/lucide-solid/node_modules`. Catching that needs a check against the published tarball rather than the workspace link. diff --git a/integrations/lucide-solid/solid-start/package.json b/integrations/lucide-solid/solid-start/package.json new file mode 100644 index 000000000..26be0f602 --- /dev/null +++ b/integrations/lucide-solid/solid-start/package.json @@ -0,0 +1,31 @@ +{ + "name": "@lucide/integration-solid-solid-start", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "build": "vite build", + "typecheck": "tsc --noEmit && tsc --noEmit --project tsconfig.declarations.json", + "test:browser": "vitest run", + "check": "pnpm build && pnpm typecheck && pnpm test:browser" + }, + "dependencies": { + "@solidjs/meta": "0.29.4", + "@solidjs/router": "1.0.0", + "@solidjs/start": "2.0.5", + "lucide-solid": "workspace:*", + "nitro": "3.0.260903-beta", + "solid-js": "1.9.15" + }, + "devDependencies": { + "@vitest/browser-playwright": "5.0.3", + "typescript": "^6.0.3", + "vite": "^8.3.2", + "vite-plugin-solid": "^2.11.14", + "vitest": "5.0.3", + "vitest-browser-solid": "1.0.1" + }, + "engines": { + "node": ">=24" + } +} diff --git a/integrations/lucide-solid/solid-start/src/IconShowcase.tsx b/integrations/lucide-solid/solid-start/src/IconShowcase.tsx new file mode 100644 index 000000000..b98bbd0e1 --- /dev/null +++ b/integrations/lucide-solid/solid-start/src/IconShowcase.tsx @@ -0,0 +1,31 @@ +import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from 'lucide-solid'; +import Circle from 'lucide-solid/icons/circle'; + +export default function IconShowcase() { + return ( +
+ + + + + + + + +
+ ); +} diff --git a/integrations/lucide-solid/solid-start/src/app.tsx b/integrations/lucide-solid/solid-start/src/app.tsx new file mode 100644 index 000000000..817ac296e --- /dev/null +++ b/integrations/lucide-solid/solid-start/src/app.tsx @@ -0,0 +1,19 @@ +import { MetaProvider, Title } from '@solidjs/meta'; +import { Router } from '@solidjs/router'; +import { FileRoutes } from '@solidjs/start/router'; +import { Suspense } from 'solid-js'; + +export default function App() { + return ( + ( + + Lucide Solid SolidStart integration + {props.children} + + )} + > + + + ); +} diff --git a/integrations/lucide-solid/solid-start/src/entry-client.tsx b/integrations/lucide-solid/solid-start/src/entry-client.tsx new file mode 100644 index 000000000..7dac7a59f --- /dev/null +++ b/integrations/lucide-solid/solid-start/src/entry-client.tsx @@ -0,0 +1,4 @@ +// @refresh reload +import { mount, StartClient } from '@solidjs/start/client'; + +mount(() => , document.getElementById('app')!); diff --git a/integrations/lucide-solid/solid-start/src/entry-server.tsx b/integrations/lucide-solid/solid-start/src/entry-server.tsx new file mode 100644 index 000000000..1585007bb --- /dev/null +++ b/integrations/lucide-solid/solid-start/src/entry-server.tsx @@ -0,0 +1,23 @@ +// @refresh reload +import { createHandler, StartServer } from '@solidjs/start/server'; + +export default createHandler(() => ( + ( + + + + + {assets} + + +
{children}
+ {scripts} + + + )} + /> +)); diff --git a/integrations/lucide-solid/solid-start/src/global.d.ts b/integrations/lucide-solid/solid-start/src/global.d.ts new file mode 100644 index 000000000..dc6f10c22 --- /dev/null +++ b/integrations/lucide-solid/solid-start/src/global.d.ts @@ -0,0 +1 @@ +/// diff --git a/integrations/lucide-solid/solid-start/src/routes/about.tsx b/integrations/lucide-solid/solid-start/src/routes/about.tsx new file mode 100644 index 000000000..e3fe76520 --- /dev/null +++ b/integrations/lucide-solid/solid-start/src/routes/about.tsx @@ -0,0 +1,11 @@ +import { A } from '@solidjs/router'; +import { ArrowLeft } from 'lucide-solid'; + +export default function About() { + return ( +
+ + Home +
+ ); +} diff --git a/integrations/lucide-solid/solid-start/src/routes/index.tsx b/integrations/lucide-solid/solid-start/src/routes/index.tsx new file mode 100644 index 000000000..5151668a7 --- /dev/null +++ b/integrations/lucide-solid/solid-start/src/routes/index.tsx @@ -0,0 +1,11 @@ +import { A } from '@solidjs/router'; +import IconShowcase from '../IconShowcase'; + +export default function Home() { + return ( + <> + + About + + ); +} diff --git a/integrations/lucide-solid/solid-start/tests/icons.browser.spec.tsx b/integrations/lucide-solid/solid-start/tests/icons.browser.spec.tsx new file mode 100644 index 000000000..47733f485 --- /dev/null +++ b/integrations/lucide-solid/solid-start/tests/icons.browser.spec.tsx @@ -0,0 +1,54 @@ +import { MemoryRouter, Route } from '@solidjs/router'; +import { expect, test } from 'vitest'; +import { render } from 'vitest-browser-solid'; +import About from '../src/routes/about'; +import Home from '../src/routes/index'; + +test('renders Lucide public APIs and survives router navigation', async () => { + const screen = render(() => ( + + + + + )); + + const staticIcon = screen.getByTestId('static-icon'); + await expect.element(staticIcon).toBeVisible(); + await expect.element(staticIcon).toHaveAttribute('width', '24'); + await expect.element(staticIcon).toHaveAttribute('height', '24'); + await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24'); + await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true'); + + const customIcon = screen.getByTestId('custom-icon'); + await expect.element(customIcon).toBeVisible(); + await expect.element(customIcon).toHaveAttribute('width', '48'); + await expect.element(customIcon).toHaveAttribute('height', '48'); + await expect.element(customIcon).toHaveAttribute('stroke', 'red'); + await expect.element(customIcon).toHaveAttribute('stroke-width', '2'); + await expect.element(customIcon).toHaveClass('consumer-icon'); + + const providerIcon = screen.getByTestId('provider-icon'); + await expect.element(providerIcon).toHaveAttribute('width', '32'); + await expect.element(providerIcon).toHaveAttribute('stroke', 'purple'); + await expect.element(providerIcon).toHaveAttribute('stroke-width', '3'); + + const aliasIcon = screen.getByTestId('alias-icon'); + const canonicalIcon = screen.getByTestId('canonical-icon'); + await expect.element(aliasIcon).toBeVisible(); + await expect.element(canonicalIcon).toBeVisible(); + expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML); + + const deepImportIcon = screen.getByTestId('deep-import-icon'); + await expect.element(deepImportIcon).toBeVisible(); + await expect.element(deepImportIcon).toHaveAttribute('aria-label', 'Deep import circle'); + await expect.element(deepImportIcon).not.toHaveAttribute('aria-hidden'); + + await screen.getByRole('link', { name: 'About' }).click(); + await expect.element(screen.getByTestId('route-icon')).toBeVisible(); +}); diff --git a/integrations/lucide-solid/solid-start/tsconfig.declarations.json b/integrations/lucide-solid/solid-start/tsconfig.declarations.json new file mode 100644 index 000000000..3b3bdf14e --- /dev/null +++ b/integrations/lucide-solid/solid-start/tsconfig.declarations.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "skipLibCheck": false, + "types": [] + }, + "include": ["src/IconShowcase.tsx"] +} diff --git a/integrations/lucide-solid/solid-start/tsconfig.json b/integrations/lucide-solid/solid-start/tsconfig.json new file mode 100644 index 000000000..6cf5dfc78 --- /dev/null +++ b/integrations/lucide-solid/solid-start/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "isolatedModules": true, + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "types": ["vite/client", "vitest/globals"] + }, + "include": ["src", "tests", "vite.config.ts", "vitest.config.ts"] +} diff --git a/integrations/lucide-solid/solid-start/vite.config.ts b/integrations/lucide-solid/solid-start/vite.config.ts new file mode 100644 index 000000000..a2f7aa266 --- /dev/null +++ b/integrations/lucide-solid/solid-start/vite.config.ts @@ -0,0 +1,7 @@ +import { solidStart } from '@solidjs/start/config'; +import { nitro } from 'nitro/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [solidStart(), nitro()], +}); diff --git a/integrations/lucide-solid/solid-start/vitest.config.ts b/integrations/lucide-solid/solid-start/vitest.config.ts new file mode 100644 index 000000000..3aa61320c --- /dev/null +++ b/integrations/lucide-solid/solid-start/vitest.config.ts @@ -0,0 +1,15 @@ +import { playwright } from '@vitest/browser-playwright'; +import solid from 'vite-plugin-solid'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [solid()], + test: { + browser: { + enabled: true, + headless: true, + provider: playwright(), + instances: [{ browser: 'chromium' }], + }, + }, +}); diff --git a/integrations/lucide-solid/vite/index.html b/integrations/lucide-solid/vite/index.html new file mode 100644 index 000000000..943101241 --- /dev/null +++ b/integrations/lucide-solid/vite/index.html @@ -0,0 +1,18 @@ + + + + + + Lucide Solid Vite integration + + +
+ + + diff --git a/integrations/lucide-solid/vite/package.json b/integrations/lucide-solid/vite/package.json new file mode 100644 index 000000000..1377fb303 --- /dev/null +++ b/integrations/lucide-solid/vite/package.json @@ -0,0 +1,25 @@ +{ + "name": "@lucide/integration-solid-vite", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "build": "vite build", + "typecheck": "tsc --noEmit && tsc --noEmit --project tsconfig.declarations.json", + "test:browser": "vitest run", + "check": "pnpm build && pnpm typecheck && pnpm test:browser" + }, + "dependencies": { + "lucide-solid": "workspace:*", + "solid-js": "1.9.15" + }, + "devDependencies": { + "@vitest/browser-playwright": "5.0.3", + "playwright": "1.58.2", + "typescript": "^6.0.3", + "vite": "^7.3.6", + "vite-plugin-solid": "^2.11.14", + "vitest": "5.0.3", + "vitest-browser-solid": "1.0.1" + } +} diff --git a/integrations/lucide-solid/vite/src/App.tsx b/integrations/lucide-solid/vite/src/App.tsx new file mode 100644 index 000000000..28c729c09 --- /dev/null +++ b/integrations/lucide-solid/vite/src/App.tsx @@ -0,0 +1,5 @@ +import IconShowcase from './IconShowcase'; + +export default function App() { + return ; +} diff --git a/integrations/lucide-solid/vite/src/IconShowcase.tsx b/integrations/lucide-solid/vite/src/IconShowcase.tsx new file mode 100644 index 000000000..b98bbd0e1 --- /dev/null +++ b/integrations/lucide-solid/vite/src/IconShowcase.tsx @@ -0,0 +1,31 @@ +import { Camera, Droplet, Edit2, House, LucideProvider, Pen } from 'lucide-solid'; +import Circle from 'lucide-solid/icons/circle'; + +export default function IconShowcase() { + return ( +
+ + + + + + + + +
+ ); +} diff --git a/integrations/lucide-solid/vite/src/index.tsx b/integrations/lucide-solid/vite/src/index.tsx new file mode 100644 index 000000000..d12d354b8 --- /dev/null +++ b/integrations/lucide-solid/vite/src/index.tsx @@ -0,0 +1,4 @@ +import { render } from 'solid-js/web'; +import App from './App'; + +render(() => , document.getElementById('root')!); diff --git a/integrations/lucide-solid/vite/tests/icons.browser.spec.tsx b/integrations/lucide-solid/vite/tests/icons.browser.spec.tsx new file mode 100644 index 000000000..4208bfd66 --- /dev/null +++ b/integrations/lucide-solid/vite/tests/icons.browser.spec.tsx @@ -0,0 +1,38 @@ +import { expect, test } from 'vitest'; +import { render } from 'vitest-browser-solid'; +import App from '../src/App'; + +test('renders Lucide public APIs in a real browser', async () => { + const screen = render(() => ); + + const staticIcon = screen.getByTestId('static-icon'); + await expect.element(staticIcon).toBeVisible(); + await expect.element(staticIcon).toHaveAttribute('width', '24'); + await expect.element(staticIcon).toHaveAttribute('height', '24'); + await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24'); + await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true'); + + const customIcon = screen.getByTestId('custom-icon'); + await expect.element(customIcon).toBeVisible(); + await expect.element(customIcon).toHaveAttribute('width', '48'); + await expect.element(customIcon).toHaveAttribute('height', '48'); + await expect.element(customIcon).toHaveAttribute('stroke', 'red'); + await expect.element(customIcon).toHaveAttribute('stroke-width', '2'); + await expect.element(customIcon).toHaveClass('consumer-icon'); + + const providerIcon = screen.getByTestId('provider-icon'); + await expect.element(providerIcon).toHaveAttribute('width', '32'); + await expect.element(providerIcon).toHaveAttribute('stroke', 'purple'); + await expect.element(providerIcon).toHaveAttribute('stroke-width', '3'); + + const aliasIcon = screen.getByTestId('alias-icon'); + const canonicalIcon = screen.getByTestId('canonical-icon'); + await expect.element(aliasIcon).toBeVisible(); + await expect.element(canonicalIcon).toBeVisible(); + expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML); + + const deepImportIcon = screen.getByTestId('deep-import-icon'); + await expect.element(deepImportIcon).toBeVisible(); + await expect.element(deepImportIcon).toHaveAttribute('aria-label', 'Deep import circle'); + await expect.element(deepImportIcon).not.toHaveAttribute('aria-hidden'); +}); diff --git a/integrations/lucide-solid/vite/tsconfig.declarations.json b/integrations/lucide-solid/vite/tsconfig.declarations.json new file mode 100644 index 000000000..3b3bdf14e --- /dev/null +++ b/integrations/lucide-solid/vite/tsconfig.declarations.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "skipLibCheck": false, + "types": [] + }, + "include": ["src/IconShowcase.tsx"] +} diff --git a/integrations/lucide-solid/vite/tsconfig.json b/integrations/lucide-solid/vite/tsconfig.json new file mode 100644 index 000000000..8447449d8 --- /dev/null +++ b/integrations/lucide-solid/vite/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "isolatedModules": true, + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "types": ["vite/client", "vitest/globals"] + }, + "include": ["src", "tests", "vite.config.ts"] +} diff --git a/integrations/lucide-solid/vite/vite.config.ts b/integrations/lucide-solid/vite/vite.config.ts new file mode 100644 index 000000000..3aa61320c --- /dev/null +++ b/integrations/lucide-solid/vite/vite.config.ts @@ -0,0 +1,15 @@ +import { playwright } from '@vitest/browser-playwright'; +import solid from 'vite-plugin-solid'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [solid()], + test: { + browser: { + enabled: true, + headless: true, + provider: playwright(), + instances: [{ browser: 'chromium' }], + }, + }, +}); diff --git a/integrations/lucide-vue/README.md b/integrations/lucide-vue/README.md new file mode 100644 index 000000000..55068295c --- /dev/null +++ b/integrations/lucide-vue/README.md @@ -0,0 +1,18 @@ +# Lucide Vue integrations + +These fixtures exercise the built `@lucide/vue` package in real consumer frameworks. + +Every fixture runs three checks: + +- a framework production build; +- TypeScript with `vue-tsc` (`noEmit`); +- Vitest Browser Mode in Chromium. + +From the repository root, install Chromium once and run the complete suite: + +```sh +pnpm --filter @lucide/integration-vue-vite exec playwright install chromium +pnpm test:integrations:vue +``` + +The fixtures depend on `@lucide/vue` through `workspace:*`, but import only its public package entry point. The root command builds `@lucide/vue` before running any consumer checks. diff --git a/integrations/lucide-vue/nuxt/app/app.vue b/integrations/lucide-vue/nuxt/app/app.vue new file mode 100644 index 000000000..7635eb4bf --- /dev/null +++ b/integrations/lucide-vue/nuxt/app/app.vue @@ -0,0 +1,7 @@ + + + diff --git a/integrations/lucide-vue/nuxt/app/components/IconShowcase.vue b/integrations/lucide-vue/nuxt/app/components/IconShowcase.vue new file mode 100644 index 000000000..376e04446 --- /dev/null +++ b/integrations/lucide-vue/nuxt/app/components/IconShowcase.vue @@ -0,0 +1,23 @@ + + + diff --git a/integrations/lucide-vue/nuxt/app/components/LucideProvider.vue b/integrations/lucide-vue/nuxt/app/components/LucideProvider.vue new file mode 100644 index 000000000..c6111dee4 --- /dev/null +++ b/integrations/lucide-vue/nuxt/app/components/LucideProvider.vue @@ -0,0 +1,9 @@ + + + diff --git a/integrations/lucide-vue/nuxt/nuxt.config.ts b/integrations/lucide-vue/nuxt/nuxt.config.ts new file mode 100644 index 000000000..16731b1a3 --- /dev/null +++ b/integrations/lucide-vue/nuxt/nuxt.config.ts @@ -0,0 +1,11 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + compatibilityDate: '2025-07-15', + devtools: { enabled: true }, + typescript: { + // `nuxt typecheck` only covers `app/**` and `tests/nuxt/**` by default; opt the browser spec + // and the vitest config in so they are type-checked too. + tsConfig: { include: ['../tests/**/*'] }, + nodeTsConfig: { include: ['../vitest.config.*'] }, + }, +}); diff --git a/integrations/lucide-vue/nuxt/package.json b/integrations/lucide-vue/nuxt/package.json new file mode 100644 index 000000000..a98ecb8cd --- /dev/null +++ b/integrations/lucide-vue/nuxt/package.json @@ -0,0 +1,27 @@ +{ + "name": "@lucide/integration-vue-nuxt", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "build": "nuxt build", + "typecheck": "nuxt typecheck", + "test:browser": "vitest run", + "check": "pnpm build && pnpm typecheck && pnpm test:browser" + }, + "dependencies": { + "@lucide/vue": "workspace:*", + "nuxt": "4.5.1", + "vue": "3.5.43", + "vue-router": "5.3.1" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.8", + "@vitest/browser-playwright": "5.0.3", + "typescript": "^6.0.3", + "vite": "^8.3.2", + "vitest": "5.0.3", + "vitest-browser-vue": "3.1.0", + "vue-tsc": "^3.3.11" + } +} diff --git a/integrations/lucide-vue/nuxt/tests/icons.browser.spec.ts b/integrations/lucide-vue/nuxt/tests/icons.browser.spec.ts new file mode 100644 index 000000000..eb3beb854 --- /dev/null +++ b/integrations/lucide-vue/nuxt/tests/icons.browser.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from 'vitest'; +import { render } from 'vitest-browser-vue'; +import App from '../app/app.vue'; + +test('renders Lucide public APIs in a real browser', async () => { + const screen = await render(App); + + const staticIcon = screen.getByTestId('static-icon'); + await expect.element(staticIcon).toBeVisible(); + await expect.element(staticIcon).toHaveAttribute('width', '24'); + await expect.element(staticIcon).toHaveAttribute('height', '24'); + await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24'); + await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true'); + + const customIcon = screen.getByTestId('custom-icon'); + await expect.element(customIcon).toBeVisible(); + await expect.element(customIcon).toHaveAttribute('width', '48'); + await expect.element(customIcon).toHaveAttribute('height', '48'); + await expect.element(customIcon).toHaveAttribute('stroke', 'red'); + await expect.element(customIcon).toHaveAttribute('stroke-width', '2'); + await expect.element(customIcon).toHaveClass('consumer-icon'); + + const providerIcon = screen.getByTestId('provider-icon'); + await expect.element(providerIcon).toHaveAttribute('width', '32'); + await expect.element(providerIcon).toHaveAttribute('stroke', 'purple'); + await expect.element(providerIcon).toHaveAttribute('stroke-width', '3'); + + const aliasIcon = screen.getByTestId('alias-icon'); + const canonicalIcon = screen.getByTestId('canonical-icon'); + await expect.element(aliasIcon).toBeVisible(); + await expect.element(canonicalIcon).toBeVisible(); + expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML); +}); diff --git a/integrations/lucide-vue/nuxt/tsconfig.json b/integrations/lucide-vue/nuxt/tsconfig.json new file mode 100644 index 000000000..307b2134b --- /dev/null +++ b/integrations/lucide-vue/nuxt/tsconfig.json @@ -0,0 +1,18 @@ +{ + // https://nuxt.com/docs/guide/concepts/typescript + "files": [], + "references": [ + { + "path": "./.nuxt/tsconfig.app.json" + }, + { + "path": "./.nuxt/tsconfig.server.json" + }, + { + "path": "./.nuxt/tsconfig.shared.json" + }, + { + "path": "./.nuxt/tsconfig.node.json" + } + ] +} diff --git a/integrations/lucide-vue/nuxt/vitest.config.ts b/integrations/lucide-vue/nuxt/vitest.config.ts new file mode 100644 index 000000000..c8d00c712 --- /dev/null +++ b/integrations/lucide-vue/nuxt/vitest.config.ts @@ -0,0 +1,15 @@ +import vue from '@vitejs/plugin-vue'; +import { playwright } from '@vitest/browser-playwright'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [vue()], + test: { + browser: { + enabled: true, + headless: true, + provider: playwright(), + instances: [{ browser: 'chromium' }], + }, + }, +}); diff --git a/integrations/lucide-vue/vike/components/IconShowcase.vue b/integrations/lucide-vue/vike/components/IconShowcase.vue new file mode 100644 index 000000000..376e04446 --- /dev/null +++ b/integrations/lucide-vue/vike/components/IconShowcase.vue @@ -0,0 +1,23 @@ + + + diff --git a/integrations/lucide-vue/vike/components/LucideProvider.vue b/integrations/lucide-vue/vike/components/LucideProvider.vue new file mode 100644 index 000000000..c6111dee4 --- /dev/null +++ b/integrations/lucide-vue/vike/components/LucideProvider.vue @@ -0,0 +1,9 @@ + + + diff --git a/integrations/lucide-vue/vike/package.json b/integrations/lucide-vue/vike/package.json new file mode 100644 index 000000000..30ae4e7ed --- /dev/null +++ b/integrations/lucide-vue/vike/package.json @@ -0,0 +1,27 @@ +{ + "name": "@lucide/integration-vue-vike", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "build": "vike build", + "typecheck": "vue-tsc --noEmit", + "test:browser": "vitest run", + "check": "pnpm build && pnpm typecheck && pnpm test:browser" + }, + "dependencies": { + "@lucide/vue": "workspace:*", + "vike": "0.4.266", + "vike-vue": "0.9.13", + "vue": "3.5.43" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.8", + "@vitest/browser-playwright": "5.0.3", + "typescript": "^6.0.3", + "vite": "^8.3.2", + "vitest": "5.0.3", + "vitest-browser-vue": "3.1.0", + "vue-tsc": "^3.3.11" + } +} diff --git a/integrations/lucide-vue/vike/pages/+config.ts b/integrations/lucide-vue/vike/pages/+config.ts new file mode 100644 index 000000000..771c3bdcd --- /dev/null +++ b/integrations/lucide-vue/vike/pages/+config.ts @@ -0,0 +1,14 @@ +import type { Config } from 'vike/types'; +import vikeVue from 'vike-vue/config'; + +// Default config (can be overridden by pages) +// https://vike.dev/config + +const config: Config = { + // https://vike.dev/head-tags + title: 'Lucide Vue Vike integration', + + extends: [vikeVue], +}; + +export default config; diff --git a/integrations/lucide-vue/vike/pages/index/+Page.vue b/integrations/lucide-vue/vike/pages/index/+Page.vue new file mode 100644 index 000000000..618fd2a16 --- /dev/null +++ b/integrations/lucide-vue/vike/pages/index/+Page.vue @@ -0,0 +1,7 @@ + + + diff --git a/integrations/lucide-vue/vike/tests/icons.browser.spec.ts b/integrations/lucide-vue/vike/tests/icons.browser.spec.ts new file mode 100644 index 000000000..a6cfcca43 --- /dev/null +++ b/integrations/lucide-vue/vike/tests/icons.browser.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from 'vitest'; +import { render } from 'vitest-browser-vue'; +import App from '../pages/index/+Page.vue'; + +test('renders Lucide public APIs in a real browser', async () => { + const screen = await render(App); + + const staticIcon = screen.getByTestId('static-icon'); + await expect.element(staticIcon).toBeVisible(); + await expect.element(staticIcon).toHaveAttribute('width', '24'); + await expect.element(staticIcon).toHaveAttribute('height', '24'); + await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24'); + await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true'); + + const customIcon = screen.getByTestId('custom-icon'); + await expect.element(customIcon).toBeVisible(); + await expect.element(customIcon).toHaveAttribute('width', '48'); + await expect.element(customIcon).toHaveAttribute('height', '48'); + await expect.element(customIcon).toHaveAttribute('stroke', 'red'); + await expect.element(customIcon).toHaveAttribute('stroke-width', '2'); + await expect.element(customIcon).toHaveClass('consumer-icon'); + + const providerIcon = screen.getByTestId('provider-icon'); + await expect.element(providerIcon).toHaveAttribute('width', '32'); + await expect.element(providerIcon).toHaveAttribute('stroke', 'purple'); + await expect.element(providerIcon).toHaveAttribute('stroke-width', '3'); + + const aliasIcon = screen.getByTestId('alias-icon'); + const canonicalIcon = screen.getByTestId('canonical-icon'); + await expect.element(aliasIcon).toBeVisible(); + await expect.element(canonicalIcon).toBeVisible(); + expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML); +}); diff --git a/integrations/lucide-vue/vike/tsconfig.json b/integrations/lucide-vue/vike/tsconfig.json new file mode 100644 index 000000000..10faac175 --- /dev/null +++ b/integrations/lucide-vue/vike/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "types": ["vite/client"], + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "exclude": ["dist"] +} diff --git a/integrations/lucide-vue/vike/vite.config.ts b/integrations/lucide-vue/vike/vite.config.ts new file mode 100644 index 000000000..a56c4dd0e --- /dev/null +++ b/integrations/lucide-vue/vike/vite.config.ts @@ -0,0 +1,7 @@ +import vike from 'vike/plugin'; +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; + +export default defineConfig({ + plugins: [vike(), vue()], +}); diff --git a/integrations/lucide-vue/vike/vitest.config.ts b/integrations/lucide-vue/vike/vitest.config.ts new file mode 100644 index 000000000..c8d00c712 --- /dev/null +++ b/integrations/lucide-vue/vike/vitest.config.ts @@ -0,0 +1,15 @@ +import vue from '@vitejs/plugin-vue'; +import { playwright } from '@vitest/browser-playwright'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [vue()], + test: { + browser: { + enabled: true, + headless: true, + provider: playwright(), + instances: [{ browser: 'chromium' }], + }, + }, +}); diff --git a/integrations/lucide-vue/vite/env.d.ts b/integrations/lucide-vue/vite/env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/integrations/lucide-vue/vite/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/integrations/lucide-vue/vite/index.html b/integrations/lucide-vue/vite/index.html new file mode 100644 index 000000000..b4ab36677 --- /dev/null +++ b/integrations/lucide-vue/vite/index.html @@ -0,0 +1,18 @@ + + + + + + Lucide Vue Vite integration + + +
+ + + diff --git a/integrations/lucide-vue/vite/package.json b/integrations/lucide-vue/vite/package.json new file mode 100644 index 000000000..e0af540ef --- /dev/null +++ b/integrations/lucide-vue/vite/package.json @@ -0,0 +1,29 @@ +{ + "name": "@lucide/integration-vue-vite", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "build": "vite build", + "typecheck": "vue-tsc --build", + "test:browser": "vitest run", + "check": "pnpm build && pnpm typecheck && pnpm test:browser" + }, + "dependencies": { + "@lucide/vue": "workspace:*", + "vue": "3.5.43" + }, + "devDependencies": { + "@tsconfig/node24": "^24.0.5", + "@types/node": "^26.1.1", + "@vitejs/plugin-vue": "^6.0.8", + "@vitest/browser-playwright": "5.0.3", + "@vue/tsconfig": "^0.9.1", + "playwright": "1.58.2", + "typescript": "^6.0.3", + "vite": "^8.3.2", + "vitest": "5.0.3", + "vitest-browser-vue": "3.1.0", + "vue-tsc": "^3.3.11" + } +} diff --git a/integrations/lucide-vue/vite/src/App.vue b/integrations/lucide-vue/vite/src/App.vue new file mode 100644 index 000000000..e518feb23 --- /dev/null +++ b/integrations/lucide-vue/vite/src/App.vue @@ -0,0 +1,7 @@ + + + diff --git a/integrations/lucide-vue/vite/src/IconShowcase.vue b/integrations/lucide-vue/vite/src/IconShowcase.vue new file mode 100644 index 000000000..376e04446 --- /dev/null +++ b/integrations/lucide-vue/vite/src/IconShowcase.vue @@ -0,0 +1,23 @@ + + + diff --git a/integrations/lucide-vue/vite/src/LucideProvider.vue b/integrations/lucide-vue/vite/src/LucideProvider.vue new file mode 100644 index 000000000..c6111dee4 --- /dev/null +++ b/integrations/lucide-vue/vite/src/LucideProvider.vue @@ -0,0 +1,9 @@ + + + diff --git a/integrations/lucide-vue/vite/src/main.ts b/integrations/lucide-vue/vite/src/main.ts new file mode 100644 index 000000000..684d04215 --- /dev/null +++ b/integrations/lucide-vue/vite/src/main.ts @@ -0,0 +1,4 @@ +import { createApp } from 'vue'; +import App from './App.vue'; + +createApp(App).mount('#app'); diff --git a/integrations/lucide-vue/vite/tests/icons.browser.spec.ts b/integrations/lucide-vue/vite/tests/icons.browser.spec.ts new file mode 100644 index 000000000..7dc2a6f62 --- /dev/null +++ b/integrations/lucide-vue/vite/tests/icons.browser.spec.ts @@ -0,0 +1,33 @@ +import { expect, test } from 'vitest'; +import { render } from 'vitest-browser-vue'; +import App from '../src/App.vue'; + +test('renders Lucide public APIs in a real browser', async () => { + const screen = await render(App); + + const staticIcon = screen.getByTestId('static-icon'); + await expect.element(staticIcon).toBeVisible(); + await expect.element(staticIcon).toHaveAttribute('width', '24'); + await expect.element(staticIcon).toHaveAttribute('height', '24'); + await expect.element(staticIcon).toHaveAttribute('viewBox', '0 0 24 24'); + await expect.element(staticIcon).toHaveAttribute('aria-hidden', 'true'); + + const customIcon = screen.getByTestId('custom-icon'); + await expect.element(customIcon).toBeVisible(); + await expect.element(customIcon).toHaveAttribute('width', '48'); + await expect.element(customIcon).toHaveAttribute('height', '48'); + await expect.element(customIcon).toHaveAttribute('stroke', 'red'); + await expect.element(customIcon).toHaveAttribute('stroke-width', '2'); + await expect.element(customIcon).toHaveClass('consumer-icon'); + + const providerIcon = screen.getByTestId('provider-icon'); + await expect.element(providerIcon).toHaveAttribute('width', '32'); + await expect.element(providerIcon).toHaveAttribute('stroke', 'purple'); + await expect.element(providerIcon).toHaveAttribute('stroke-width', '3'); + + const aliasIcon = screen.getByTestId('alias-icon'); + const canonicalIcon = screen.getByTestId('canonical-icon'); + await expect.element(aliasIcon).toBeVisible(); + await expect.element(canonicalIcon).toBeVisible(); + expect(aliasIcon.element().innerHTML).toBe(canonicalIcon.element().innerHTML); +}); diff --git a/integrations/lucide-vue/vite/tsconfig.app.json b/integrations/lucide-vue/vite/tsconfig.app.json new file mode 100644 index 000000000..72db1a2c1 --- /dev/null +++ b/integrations/lucide-vue/vite/tsconfig.app.json @@ -0,0 +1,18 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "tests/**/*"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + // Extra safety for array and object lookups, but may have false positives. + "noUncheckedIndexedAccess": true, + + // Path mapping for cleaner imports. + "paths": { + "@/*": ["./src/*"] + }, + + // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. + // Specified here to keep it out of the root directory. + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo" + } +} diff --git a/integrations/lucide-vue/vite/tsconfig.json b/integrations/lucide-vue/vite/tsconfig.json new file mode 100644 index 000000000..66b5e5703 --- /dev/null +++ b/integrations/lucide-vue/vite/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/integrations/lucide-vue/vite/tsconfig.node.json b/integrations/lucide-vue/vite/tsconfig.node.json new file mode 100644 index 000000000..7274bd4c4 --- /dev/null +++ b/integrations/lucide-vue/vite/tsconfig.node.json @@ -0,0 +1,28 @@ +// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping. +{ + "extends": "@tsconfig/node24/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "playwright.config.*", + "eslint.config.*" + ], + "compilerOptions": { + // Most tools use transpilation instead of Node.js's native type-stripping. + // Bundler mode provides a smoother developer experience. + "module": "preserve", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + + // Include Node.js types and avoid accidentally including other `@types/*` packages. + "types": ["node"], + + // Disable emitting output during `vue-tsc --build`, which is used for type-checking only. + "noEmit": true, + + // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. + // Specified here to keep it out of the root directory. + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo" + } +} diff --git a/integrations/lucide-vue/vite/vite.config.ts b/integrations/lucide-vue/vite/vite.config.ts new file mode 100644 index 000000000..58f57c57f --- /dev/null +++ b/integrations/lucide-vue/vite/vite.config.ts @@ -0,0 +1,14 @@ +import { fileURLToPath, URL } from 'node:url'; + +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, +}); diff --git a/integrations/lucide-vue/vite/vitest.config.ts b/integrations/lucide-vue/vite/vitest.config.ts new file mode 100644 index 000000000..0aef508c8 --- /dev/null +++ b/integrations/lucide-vue/vite/vitest.config.ts @@ -0,0 +1,17 @@ +import { playwright } from '@vitest/browser-playwright'; +import { defineConfig, mergeConfig } from 'vitest/config'; +import viteConfig from './vite.config'; + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + browser: { + enabled: true, + headless: true, + provider: playwright(), + instances: [{ browser: 'chromium' }], + }, + }, + }), +); diff --git a/lab/bottle-toothbrush-comb.svg b/lab/bottle-toothbrush-comb.svg index dfed37c74..c6728842f 100644 --- a/lab/bottle-toothbrush-comb.svg +++ b/lab/bottle-toothbrush-comb.svg @@ -9,13 +9,13 @@ stroke-linecap="round" stroke-linejoin="round" > - - - - - + + - + + + + diff --git a/lab/briefcase-plus.json b/lab/briefcase-plus.json deleted file mode 100644 index b1e6b0b6b..000000000 --- a/lab/briefcase-plus.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "../icon.schema.json", - "tags": [ - "healthcare", - "first aid kit", - "paramedic", - "medkit", - "medikit", - "medpack", - "medicine", - "pharmacy", - "pharmaceutical", - "add", - "cross", - "box" - ], - "categories": [ - "medical", - "gaming" - ] -} diff --git a/lab/carton-off.json b/lab/carton-off.json deleted file mode 100644 index 52ea1140e..000000000 --- a/lab/carton-off.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../icon.schema.json", - "contributors": [ - "danielbayley" - ], - "tags": [ - "milk", - "dairy", - "drink", - "diet", - "lactose free", - "liquid", - "litres", - "pints", - "pasturised", - "skimmed", - "container", - "vegan" - ], - "categories": [ - "food-beverage" - ] -} diff --git a/lab/carton-off.svg b/lab/carton-off.svg deleted file mode 100644 index 373fa0498..000000000 --- a/lab/carton-off.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/lab/carton.json b/lab/carton.json deleted file mode 100644 index abdb25a33..000000000 --- a/lab/carton.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../icon.schema.json", - "contributors": [ - "danielbayley" - ], - "tags": [ - "milk", - "dairy", - "drink", - "diet", - "lactose", - "liquid", - "litres", - "pints", - "pasturised", - "skimmed", - "container", - "cow" - ], - "categories": [ - "food-beverage" - ] -} diff --git a/lab/check-x.json b/lab/check-x.json new file mode 100644 index 000000000..24ad23333 --- /dev/null +++ b/lab/check-x.json @@ -0,0 +1,23 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "ishanbagra18" + ], + "tags": [ + "uncheck", + "cancel", + "remove", + "unread", + "cross", + "mark", + "deselect", + "task" + ], + "categories": [ + "notifications" + ], + "use-cases": [ + "Unmarking / toggling read status (mark unread / uncheck)", + "Task & checklist management (cancel/reject a checked item)" + ] +} diff --git a/lab/check-x.svg b/lab/check-x.svg new file mode 100644 index 000000000..9a3bc73c0 --- /dev/null +++ b/lab/check-x.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/lab/faucet.json b/lab/faucet.json deleted file mode 100644 index dcf87a692..000000000 --- a/lab/faucet.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "../icon.schema.json", - "contributors": [ - "danielbayley" - ], - "tags": [ - "tap", - "spigot", - "water", - "drinking", - "fresh", - "source", - "pipes", - "plumbing", - "plumber" - ], - "categories": [ - "food-beverage", - "home", - "maps" - ] -} diff --git a/lab/faucet.svg b/lab/faucet.svg deleted file mode 100644 index fb75be81b..000000000 --- a/lab/faucet.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - diff --git a/lab/hand-gear.json b/lab/hand-gear.json new file mode 100644 index 000000000..c764502e7 --- /dev/null +++ b/lab/hand-gear.json @@ -0,0 +1,41 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "karsa-mistmere", + "danielbayley" + ], + "use-cases": [ + "indicate maintenance or repair tasks", + "represent manual configuration or settings adjustment", + "show hands-on technical support", + "denote mechanical engineering or machine servicing tasks", + "illustrate workshop or manufacturing processes", + "signal equipment calibration or tuning", + "open advanced hardware settings from a control panel" + ], + "tags": [ + "automation", + "innovation", + "skills", + "technical support", + "engineering", + "craftsmanship", + "industrial", + "problem solving", + "manufacturing", + "repair", + "maintenance", + "service", + "mechanic", + "settings", + "configuration", + "calibration", + "hand", + "gear", + "workshop" + ], + "categories": [ + "development", + "devices" + ] +} diff --git a/lab/hand-gear.svg b/lab/hand-gear.svg new file mode 100644 index 000000000..e1e04e72a --- /dev/null +++ b/lab/hand-gear.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/lab/houses.json b/lab/houses.json deleted file mode 100644 index e03779370..000000000 --- a/lab/houses.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "../icon.schema.json", - "contributors": [ - "danielbayley" - ], - "tags": [ - "homes", - "property", - "real estate", - "dwellings", - "living", - "rooms", - "door" - ], - "categories": [ - "buildings" - ] -} diff --git a/lab/monstera-leaf.json b/lab/monstera-leaf.json new file mode 100644 index 000000000..1d89466b3 --- /dev/null +++ b/lab/monstera-leaf.json @@ -0,0 +1,25 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "alx-xo" + ], + "tags": [ + "botanical", + "green", + "foliage", + "vegetation", + "exotic", + "decor", + "nature", + "plant", + "leaf", + "monstera", + "tropical", + "growth" + ], + "categories": [ + "nature", + "sustainability", + "travel" + ] +} diff --git a/lab/monstera-leaf.svg b/lab/monstera-leaf.svg new file mode 100644 index 000000000..81dee51db --- /dev/null +++ b/lab/monstera-leaf.svg @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/lab/peace-sign.json b/lab/peace-sign.json new file mode 100644 index 000000000..2109acf03 --- /dev/null +++ b/lab/peace-sign.json @@ -0,0 +1,20 @@ +{ + "$schema": "../icon.schema.json", + "contributors": [ + "ajokt123", + "briz123", + "jamiemlaw", + "jguddas" + ], + "tags": [ + "peace", + "emotion", + "positivity", + "chill" + ], + "categories": [ + "communication", + "emoji", + "social" + ] +} diff --git a/lab/peace-sign.svg b/lab/peace-sign.svg new file mode 100644 index 000000000..a875ba237 --- /dev/null +++ b/lab/peace-sign.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/lab/swiss-franc-circle.json b/lab/swiss-franc-circle.json index ea7e5e0a0..24383c81d 100644 --- a/lab/swiss-franc-circle.json +++ b/lab/swiss-franc-circle.json @@ -3,6 +3,8 @@ "contributors": [ "danielbayley" ], + "deprecated": true, + "deprecationReason": "icon.use-case", "tags": [ "monetization", "marketing", diff --git a/lab/swiss-franc-square.json b/lab/swiss-franc-square.json index 43d553d29..7c6b21fde 100644 --- a/lab/swiss-franc-square.json +++ b/lab/swiss-franc-square.json @@ -3,6 +3,8 @@ "contributors": [ "danielbayley" ], + "deprecated": true, + "deprecationReason": "icon.use-case", "tags": [ "monetization", "marketing", diff --git a/package.json b/package.json index 5035e9c59..6ad1092a7 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,10 @@ "build": "pnpm -r --filter './packages/**' build", "test": "pnpm -r --filter './packages/**' test", "test:update": "pnpm -r --filter './packages/**' --filter !'./packages/lucide-angular' test -- -u", + "test:integrations:react": "pnpm --filter lucide-react build && pnpm -r --workspace-concurrency=1 --filter './integrations/lucide-react/**' check", + "test:integrations:solid": "pnpm --filter lucide-solid build && pnpm -r --workspace-concurrency=1 --filter './integrations/lucide-solid/**' check", + "test:integrations:solid-2": "pnpm --filter @lucide/solid build && pnpm -r --workspace-concurrency=1 --filter './integrations/lucide-solid-2/**' check", + "test:integrations:vue": "pnpm --filter @lucide/vue build && pnpm -r --workspace-concurrency=1 --filter './integrations/lucide-vue/**' check", "test:font": "pnpm --filter build-font test", "lucide": "pnpm --filter lucide", "lucide-angular": "pnpm --filter lucide-angular", @@ -28,8 +32,9 @@ "generate:nextJSAliases": "node ./scripts/generateNextJSAliases.mts", "icon-usage": "node ./scripts/iconUsage.mts", "suggest:metadata": "node ./scripts/suggestMetaData.mts", + "suggest:metadata:debug": "node --env-file .env ./scripts/suggestMetaData.mts --debug", "suggest:metadata:watch": "node --env-file .env --watch ./scripts/suggestMetaData.mts", - "postinstall": "husky install", + "postinstall": "husky", "lint:es": "eslint .", "lint:format": "prettier \"**/*.{js,mjs,ts,jsx,tsx,html,css,scss,json,yml,yaml}\" --check", "lint:format-fix": "prettier \"**/*.{js,mjs,ts,jsx,tsx,html,css,scss,json,yml,yaml}\" --write", @@ -38,9 +43,10 @@ "lint:json:icons": "ajv --spec=draft2020 -s icon.schema.json -d 'icons/*.json' > /dev/null", "lint:json:categories": "ajv --spec=draft2020 -s category.schema.json -d 'categories/*.json' > /dev/null", "lint:json": "pnpm run lint:json:icons && pnpm run lint:json:categories", + "lint:icons:all": "pnpm run lint:icons && pnpm run lint:json:icons", "lint": "pnpm lint:es && pnpm lint:format && pnpm lint:json && pnpm lint:icons", "format": "prettier \"**/*.{js,mjs,ts,jsx,tsx,html,css,scss,json,yml,yaml}\" --write", - "prepare": "husky install", + "prepare": "husky", "gi": "node ./scripts/generate/generateIcons.mts", "rename": "node ./scripts/rename/renameIcon.mts", "renamePattern": "node ./scripts/rename/renamePattern.mts" @@ -50,37 +56,38 @@ "@actions/github": "^9.1.1", "@eslint/compat": "^2.1.0", "@eslint/js": "^10.0.1", - "@html-eslint/eslint-plugin": "^0.60.0", - "@html-eslint/parser": "^0.60.0", - "@octokit/rest": "^19.0.13", + "@html-eslint/eslint-plugin": "^0.65.0", + "@html-eslint/parser": "^0.65.0", + "@octokit/rest": "^22.0.1", "@types/yargs": "^17.0.35", - "@typescript-eslint/eslint-plugin": "^8.59.3", - "@typescript-eslint/parser": "^8.59.3", - "@vitest/coverage-v8": "4.1.10", - "@vitest/ui": "4.1.10", + "@typescript-eslint/eslint-plugin": "^8.69.0", + "@typescript-eslint/parser": "^8.69.0", + "@vitest/coverage-v8": "5.0.3", + "@vitest/ui": "5.0.3", "ajv-cli": "^5.0.0", - "cspell": "^10.0.1", + "cpy-cli": "^7.0.0", + "cspell": "^10.2.0", "dotenv": "^17.4.2", - "eslint": "^10.8.0", + "eslint": "^10.9.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import-x": "^4.17.1", - "husky": "^8.0.3", - "jsdom": "^27.4.0", - "lint-staged": "^17.0.8", + "husky": "^9.1.7", + "jsdom": "^30.0.1", + "lint-staged": "^17.4.1", "minimist": "^1.2.8", - "openai": "^5.23.2", - "p-memoize": "^7.1.1", - "prettier": "3.2.4", + "openai": "^7.9.0", + "p-memoize": "^8.0.0", + "prettier": "3.9.6", "prettier-plugin-astro": "^0.14.1", "semver": "^7.8.5", - "simple-git": "^3.36.0", - "svgo": "^3.3.4", + "simple-git": "^4.0.1", + "svgo": "^4.1.0", "svgson": "^5.3.1", "typescript": "^6.0.3", - "typescript-eslint": "^8.59.4", - "vitest": "4.1.10", - "yargs": "^17.7.3", - "zod": "^3.25.76" + "typescript-eslint": "^8.69.0", + "vitest": "5.0.3", + "yargs": "^18.1.0", + "zod": "^4.5.4" }, "engines": { "node": ">=24.11.1" diff --git a/packages/angular/package.json b/packages/angular/package.json index 05688d51c..4eb9cd5e4 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -17,14 +17,15 @@ "scripts": { "ng": "ng", "watch": "ng build --watch --configuration development", - "prebuild": "pnpm clean && pnpm copy:license && pnpm build:icons", + "prebuild": "pnpm clean && pnpm copy:license && pnpm copy:utils && pnpm build:icons", "build": "pnpm prebuild && pnpm build:ng", "copy:license": "cp ../../LICENSE ./LICENSE", + "copy:utils": "cpy ../../packages/shared/src/build/{buildLucideIconNode.ts,defaultAttributes.ts,types.ts} ../../packages/shared/src/utils/mergeClasses.ts ./src/utils/", "clean": "rm -rf dist && rm -rf ./src/icons/*.ts", "build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --iconFileExtension=.ts --exportFileName=lucide-angular.ts --useDefaultExports=0", "build:ng": "ng build --configuration production", - "test": "pnpm build:icons && ng test --no-watch", - "test:watch": "ng test", + "test": "pnpm copy:utils && pnpm build:icons && ng test --no-watch", + "test:watch": "pnpm copy:utils && ng test", "lint": "eslint 'src/**/*.{js,jsx,ts,tsx,html,css,scss}' --quiet --fix", "e2e": "ng e2e" }, @@ -41,26 +42,26 @@ ] }, "devDependencies": { - "@angular-eslint/builder": "~21.4.0", - "@angular-eslint/schematics": "~21.4.0", - "@angular/build": "^21.2.19", - "@angular/cli": "^21.2.19", - "@angular/common": "^21.2.19", - "@angular/compiler": "^21.2.19", - "@angular/compiler-cli": "^21.2.19", - "@angular/core": "^21.2.19", - "@angular/forms": "^21.2.19", - "@angular/platform-browser": "^21.2.19", - "@angular/platform-server": "^21.2.19", - "@angular/router": "^21.2.19", + "@angular-eslint/builder": "~22.5.0", + "@angular-eslint/schematics": "~22.5.0", + "@angular/build": "^22.2.1", + "@angular/cli": "^22.2.1", + "@angular/common": "^22.2.1", + "@angular/compiler": "^22.2.1", + "@angular/compiler-cli": "^22.2.1", + "@angular/core": "^22.2.1", + "@angular/forms": "^22.2.1", + "@angular/platform-browser": "^22.2.1", + "@angular/platform-server": "^22.2.1", + "@angular/router": "^22.2.1", "@lucide/build-icons": "workspace:*", "@lucide/helpers": "workspace:*", - "@vitest/browser-playwright": "^4.1.10", - "angular-eslint": "21.4.0", - "ng-packagr": "^21.2.5", + "@vitest/browser-playwright": "^5.0.3", + "angular-eslint": "22.5.0", + "ng-packagr": "^22.2.4", "rxjs": "~7.8.2", "tslib": "^2.8.1", - "typescript": "~5.9.3" + "typescript": "^6.0.3" }, "peerDependencies": { "@angular/common": ">=17.0.0", diff --git a/packages/angular/src/lucide-config.ts b/packages/angular/src/lucide-config.ts index 8a4953127..3944638f1 100644 --- a/packages/angular/src/lucide-config.ts +++ b/packages/angular/src/lucide-config.ts @@ -19,6 +19,11 @@ export interface LucideConfig { * @default 2 */ strokeWidth: number; + /** + * Whether to use absolute stroke width. + * @deprecated Use `nonScalingStroke` instead. + */ + absoluteStrokeWidth: boolean; /** * Whether stroke width should be scaled to appear uniform regardless of icon size. * @default false @@ -31,7 +36,7 @@ export interface LucideConfig { * } * ``` */ - absoluteStrokeWidth: boolean; + nonScalingStroke: boolean; } /** @@ -42,6 +47,7 @@ export const lucideDefaultConfig: LucideConfig = { size: 24, strokeWidth: 2, absoluteStrokeWidth: false, + nonScalingStroke: false, }; /** diff --git a/packages/angular/src/lucide-hydration.spec.ts b/packages/angular/src/lucide-hydration.spec.ts index 8521fa8e0..9b4757c85 100644 --- a/packages/angular/src/lucide-hydration.spec.ts +++ b/packages/angular/src/lucide-hydration.spec.ts @@ -8,7 +8,7 @@ import { LucideBadgeAlert } from './icons/badge-alert'; @Component({ // eslint-disable-next-line @angular-eslint/component-selector selector: 'lucide-static-hydration-root', - template: ``, + template: ``, imports: [LucideBadgeAlert], }) class StaticHydrationRootComponent {} diff --git a/packages/angular/src/lucide-icon-base.spec.ts b/packages/angular/src/lucide-icon-base.spec.ts index f90f91596..f5df57065 100644 --- a/packages/angular/src/lucide-icon-base.spec.ts +++ b/packages/angular/src/lucide-icon-base.spec.ts @@ -47,6 +47,7 @@ describe('LucideIconBase', () => { let size: WritableSignal; let strokeWidth: WritableSignal; let absoluteStrokeWidth: WritableSignal; + let nonScalingStroke: WritableSignal; const getSvgAttribute = (attr: string) => fixture.nativeElement.getAttribute(attr); const expectSvgClasses = (classes: string[], svg: SVGElement = fixture.nativeElement) => { for (const cssClass of classes) { @@ -63,6 +64,7 @@ describe('LucideIconBase', () => { inputBinding('size', size), inputBinding('strokeWidth', strokeWidth), inputBinding('absoluteStrokeWidth', absoluteStrokeWidth), + inputBinding('nonScalingStroke', nonScalingStroke), ], }); } @@ -72,6 +74,7 @@ describe('LucideIconBase', () => { size = signal(undefined); strokeWidth = signal(undefined); absoluteStrokeWidth = signal(undefined); + nonScalingStroke = signal(undefined); fixture = createComponent(); component = fixture.componentInstance; }); @@ -135,6 +138,11 @@ describe('LucideIconBase', () => { expect(getSvgAttribute('width')).toBe('18'); expect(getSvgAttribute('height')).toBe('18'); }); + it('should set viewBox', () => { + size.set('18'); + fixture.detectChanges(); + expect(getSvgAttribute('viewBox')).toBe('0 0 24 24'); + }); }); describe('strokeWidth', () => { @@ -155,22 +163,38 @@ describe('LucideIconBase', () => { }); describe('absoluteStrokeWidth', () => { + it('should adjust stroke width', () => { + strokeWidth.set(2); + size.set(32); + absoluteStrokeWidth.set(true); + fixture.detectChanges(); + expect(getSvgAttribute('stroke-width')).toBe('1.5'); + }); + it('should not set vector-effect on children', () => { + absoluteStrokeWidth.set(true); + for (const child of fixture.nativeElement.children) { + expect(child.getAttribute('vector-effect')).toBeNull(); + } + }); + }); + + describe('nonScalingStroke', () => { it('should not adjust stroke width', () => { strokeWidth.set(2); size.set(12); - absoluteStrokeWidth.set(true); + nonScalingStroke.set(false); fixture.detectChanges(); expect(getSvgAttribute('stroke-width')).toBe('2'); }); it('should not set vector-effect on children', () => { - absoluteStrokeWidth.set(false); + nonScalingStroke.set(false); fixture.detectChanges(); for (const child of getRenderedChildren()) { expect(child.getAttribute('vector-effect')).toBeNull(); } }); it('should set vector-effect on children', () => { - absoluteStrokeWidth.set(true); + nonScalingStroke.set(true); fixture.detectChanges(); for (const child of getRenderedChildren()) { expect(child.getAttribute('vector-effect')).toBe('non-scaling-stroke'); @@ -207,6 +231,7 @@ describe('LucideIconBase', () => { color: 'red', strokeWidth: 1, size: 12, + nonScalingStroke: true, absoluteStrokeWidth: true, }), ], @@ -229,12 +254,12 @@ describe('LucideIconBase', () => { describe('strokeWidth', () => { it('should use stroke width from config', () => { fixture.detectChanges(); - expect(getSvgAttribute('stroke-width')).toBe('1'); + expect(getSvgAttribute('stroke-width')).toBe('2'); }); it('should use override stroke width from config', () => { strokeWidth.set(3); fixture.detectChanges(); - expect(getSvgAttribute('stroke-width')).toBe('3'); + expect(getSvgAttribute('stroke-width')).toBe('6'); }); }); describe('size', () => { @@ -253,14 +278,23 @@ describe('LucideIconBase', () => { describe('absoluteStrokeWidth', () => { it('should use absoluteStrokeWidth from config', () => { fixture.detectChanges(); - for (const child of getRenderedChildren()) { - expect(child.getAttribute('vector-effect')).toBe('non-scaling-stroke'); - } + expect(getSvgAttribute('stroke-width')).toBe('2'); }); it('should override absoluteStrokeWidth', () => { absoluteStrokeWidth.set(false); fixture.detectChanges(); - for (const child of getRenderedChildren()) { + expect(getSvgAttribute('stroke-width')).toBe('1'); + }); + }); + describe('nonScalingStroke', () => { + it('should use nonScalingStroke from config', () => { + for (const child of fixture.nativeElement.children) { + expect(child.getAttribute('vector-effect')).toBe('non-scaling-stroke'); + } + }); + it('should override nonScalingStroke', () => { + nonScalingStroke.set(false); + for (const child of fixture.nativeElement.children) { expect(child.getAttribute('vector-effect')).toBeNull(); } }); diff --git a/packages/angular/src/lucide-icon-base.ts b/packages/angular/src/lucide-icon-base.ts index 5708baab6..624307076 100644 --- a/packages/angular/src/lucide-icon-base.ts +++ b/packages/angular/src/lucide-icon-base.ts @@ -1,8 +1,9 @@ import { Component, computed, inject, input, Signal } from '@angular/core'; import { LUCIDE_CONFIG } from './lucide-config'; -import { LucideIconData, Nullable } from './types'; +import { LucideIconData, LucideIconNode, Nullable } from './types'; import defaultAttributes from './default-attributes'; import { lucideIconTemplate } from './lucide-icon-template'; +import buildLucideIconNode from './utils/buildLucideIconNode'; /** * @internal @@ -12,38 +13,17 @@ import { lucideIconTemplate } from './lucide-icon-template'; template: lucideIconTemplate, host: { ...defaultAttributes, - class: 'lucide', - '[class]': 'iconClasses()', - '[attr.width]': 'size()', - '[attr.height]': 'size()', - '[attr.stroke]': 'color()', - '[attr.stroke-width]': 'strokeWidth()', - '[attr.aria-hidden]': '!title()', + '[attr.class]': 'computedClass()', + '[attr.width]': 'computedWidth()', + '[attr.height]': 'computedHeight()', + '[attr.viewBox]': 'computedViewbox()', + '[attr.stroke]': 'computedStroke()', + '[attr.stroke-width]': 'computedStrokeWidth()', + '[attr.aria-hidden]': 'computedAriaHidden()', }, }) export abstract class LucideIconBase { protected abstract readonly icon: Signal>; - protected readonly iconNodes = computed(() => { - const node = this.icon()?.node ?? []; - if (!this.absoluteStrokeWidth()) { - return node; - } - return node.map(([name, attrs]) => [ - name, - { - 'vector-effect': 'non-scaling-stroke', - ...attrs, - }, - ]); - }); - protected readonly iconClasses = computed(() => { - const icon = this.icon(); - if (!icon) { - return ''; - } - const { name, aliases = [] } = icon; - return [name, ...aliases].map((item) => `lucide-${item}`).join(' '); - }); protected readonly iconConfig = inject(LUCIDE_CONFIG); /** * An optional accessible label for the icon. @@ -58,6 +38,10 @@ export abstract class LucideIconBase { * - If your icon is functional (e.g. used in place of a label), feel free to use this property. */ readonly title = input>(); + /** + * Optional CSS classes for the icon. + */ + readonly class = input>(); /** * Width and height. * @default 24 @@ -65,6 +49,16 @@ export abstract class LucideIconBase { readonly size = input(this.iconConfig.size, { transform: (value: Nullable) => value ?? this.iconConfig.size, }); + /** + * Icon width. + * @default 24 + */ + readonly width = input(null); + /** + * Icon height. + * @default 24 + */ + readonly height = input(null); /** * Stroke color. * @default currentColor @@ -80,9 +74,95 @@ export abstract class LucideIconBase { transform: (value: Nullable) => value ?? this.iconConfig.strokeWidth, }); /** - * If set to true, it adds [`vector-effect="non-scaling-stroke"`](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/vector-effect) to child elements. + * Whether to use absolute stroke width. */ readonly absoluteStrokeWidth = input(this.iconConfig.absoluteStrokeWidth, { transform: (value: Nullable) => value ?? this.iconConfig.absoluteStrokeWidth, }); + /** + * If set to true, it adds [`vector-effect="non-scaling-stroke"`](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/vector-effect) to child elements. + */ + readonly nonScalingStroke = input(this.iconConfig.nonScalingStroke, { + transform: (value: Nullable) => value ?? this.iconConfig.nonScalingStroke, + }); + + /** + * @internal + */ + protected readonly computedIcon = computed(() => { + const icon = this.icon(); + return icon + ? buildLucideIconNode(icon, { + width: this.width() ?? this.size(), + height: this.height() ?? this.size(), + color: this.color(), + strokeWidth: this.strokeWidth(), + absoluteStrokeWidth: this.absoluteStrokeWidth(), + nonScalingStroke: this.nonScalingStroke(), + className: this.class() ?? undefined, + hasA11yProp: this.title() != null, + }) + : null; + }); + + /** + * @internal + */ + protected readonly computedAttributes = computed( + () => this.computedIcon()?.[1], + ); + + /** + * @internal + */ + protected readonly computedIconNode = computed(() => this.computedIcon()?.[2] ?? []); + + /** + * @internal + */ + protected readonly computedWidth = computed( + () => this.computedAttributes()?.['width'] ?? this.iconConfig.size, + ); + + /** + * @internal + */ + protected readonly computedHeight = computed( + () => this.computedAttributes()?.['height'] ?? this.iconConfig.size, + ); + + /** + * @internal + */ + protected readonly computedStroke = computed( + () => this.computedAttributes()?.['stroke'] ?? this.iconConfig.color, + ); + + /** + * @internal + */ + protected readonly computedStrokeWidth = computed( + () => this.computedAttributes()?.['stroke-width'] ?? this.iconConfig.strokeWidth, + ); + + /** + * @internal + */ + protected readonly computedAriaHidden = computed( + () => this.computedAttributes()?.['aria-hidden'] === 'true', + ); + + /** + * @internal + */ + protected readonly computedViewbox = computed( + () => this.computedAttributes()?.['viewBox'] ?? '0 0 24 24', + ); + + /** + * @internal + */ + protected readonly computedClass = computed( + () => this.computedAttributes()?.['class'] ?? 'lucide', + ); } diff --git a/packages/angular/src/lucide-icon-template.ts b/packages/angular/src/lucide-icon-template.ts index ec95dc55f..5a6cde461 100644 --- a/packages/angular/src/lucide-icon-template.ts +++ b/packages/angular/src/lucide-icon-template.ts @@ -6,7 +6,7 @@ export const lucideIconTemplate = `@if (title(); as titleValue) { {{ titleValue }} } -@for (child of iconNodes(); track child[1]['key'] ?? $index) { +@for (child of computedIconNode(); track child[1]['key'] ?? $index) { @let attrs = child[1]; @switch (child[0]) { @case ('path') { diff --git a/packages/angular/src/types.ts b/packages/angular/src/types.ts index bc75de19e..c234ab405 100644 --- a/packages/angular/src/types.ts +++ b/packages/angular/src/types.ts @@ -1,18 +1,16 @@ import { Signal, Type } from '@angular/core'; +import { + LucideIconNode as SharedLucideIconNode, + LucideIconData as SharedLucideIconData, +} from './utils/types'; -type HtmlAttributes = { [key: string]: string | number }; -export type LucideIconNode = readonly [string, HtmlAttributes]; +export type LucideIconNode = SharedLucideIconNode; export type LucideIcons = { [key: string]: LucideIconData }; /** * A Lucide icon object that fully describes an icon to be displayed. */ -export type LucideIconData = { - name: string; - size?: number; - node: LucideIconNode[]; - aliases?: string[]; -}; +export type LucideIconData = SharedLucideIconData & { name: string }; /** * Input signal map of Lucide icon components. @@ -22,7 +20,11 @@ interface LucideIconProps { size: Signal>; color: Signal>; strokeWidth: Signal>; + /** + * @deprecated Use `nonScalingStroke` instead. + */ absoluteStrokeWidth: Signal>; + nonScalingStroke: Signal>; } /** diff --git a/packages/angular/tsconfig.json b/packages/angular/tsconfig.json index 65b5c1a3d..36c4351f7 100644 --- a/packages/angular/tsconfig.json +++ b/packages/angular/tsconfig.json @@ -4,7 +4,7 @@ "compileOnSave": false, "compilerOptions": { "paths": { - "@lucide/angular": ["./dist"], + "@lucide/angular": ["./dist"] }, "strict": true, "noImplicitOverride": true, @@ -14,25 +14,24 @@ "skipLibCheck": true, "isolatedModules": true, "declaration": false, - "downlevelIteration": true, "experimentalDecorators": true, "importHelpers": true, "target": "ES2022", - "module": "preserve", + "module": "preserve" }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, - "strictTemplates": true, + "strictTemplates": true }, "files": [], "references": [ { - "path": "./tsconfig.lib.json", + "path": "./tsconfig.lib.json" }, { - "path": "./tsconfig.spec.json", - }, - ], + "path": "./tsconfig.spec.json" + } + ] } diff --git a/packages/astro/package.json b/packages/astro/package.json index f55794442..e04ac9c8a 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -43,22 +43,24 @@ "build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --withAliases --aliasesFileExtension=.ts --iconFileExtension=.ts --exportFileName=index.ts --pretty=false", "clean": "rm -rf dist && rm -rf stats && rm -rf ./src/icons/*.ts", "copy:license": "cp ../../LICENSE ./LICENSE", - "copy:utils": "mkdir -p ./src/utils && for f in hasA11yProp toKebabCase mergeClasses; do cp -f ../../packages/shared/src/utils/$f.ts ./src/utils/; done", + "copy:utils": "cpy ../../packages/shared/src/build/{buildLucideIconNode.ts,defaultAttributes.ts,types.ts} ../../packages/shared/src/utils/{hasA11yProp.ts,toKebabCase.ts,mergeClasses.ts} ./src/utils/", "test": "pnpm copy:utils && pnpm build:icons && vitest run", - "test:watch": "pnpm build:icons && vitest run --watch" + "test:watch": "pnpm build:icons && vitest run --watch", + "typecheck": "pnpm copy:utils && pnpm build:icons && astro check" }, "devDependencies": { - "@astrojs/ts-plugin": "^1.10.10", + "@astrojs/check": "^0.9.10", + "@astrojs/ts-plugin": "^1.10.11", "@lucide/build-icons": "workspace:*", "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.9.1", - "astro": "^7.1.0", + "@testing-library/jest-dom": "^7.0.1", + "astro": "^7.3.2", "jest-serializer-html": "^7.1.0", "linkedom": "^0.18.13", - "prettier": "^3.9.5", - "typescript": "^5.9.3", - "vite": "^7.3.6", - "vitest": "4.1.10" + "prettier": "3.9.6", + "typescript": "^6.0.3", + "vite": "^8.3.2", + "vitest": "5.0.3" }, "peerDependencies": { "astro": "^4 || ^5 || ^6 || ^7" diff --git a/packages/astro/scripts/exportTemplate.mts b/packages/astro/scripts/exportTemplate.mts index c8a41ed3a..1c43db3e4 100644 --- a/packages/astro/scripts/exportTemplate.mts +++ b/packages/astro/scripts/exportTemplate.mts @@ -1,28 +1,27 @@ import base64SVG from '@lucide/build-icons/utils/base64SVG'; import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate'; -export default defineExportTemplate(async ({ - componentName, - iconName, - children, - getSvg, - deprecated, - deprecationReason, -}) => { - const svgContents = await getSvg(); - const svgBase64 = base64SVG(svgContents); +export default defineExportTemplate( + async ({ componentName, iconName, iconData, getSvg, deprecated, deprecationReason }) => { + const svgContents = await getSvg(); + const svgBase64 = base64SVG(svgContents); - // Astro doesn't need keyed children in loops - const keylessChildren = children.map((c) => { - const [element, { key, ...otherAttrs }] = c; - return [element, otherAttrs]; - }); + // Astro doesn't need keyed children in loops + const keylessIconData = { + ...iconData, + node: iconData.node.map((child) => { + const [element, { key, ...otherAttrs }] = child; + return [element, otherAttrs]; + }), + }; - // TODO: build-icons' `pretty` is set to false as the prettier - // formatter uses babel which I'm not sure it supports typescript - return ` + // TODO: build-icons' `pretty` is set to false as the prettier + // formatter uses babel which I'm not sure it supports typescript + return ` import createLucideIcon from '../createLucideIcon'; -import type { AstroComponent } from '../types' +import type { AstroComponent, LucideIconData } from '../types' + +const iconData: LucideIconData = ${JSON.stringify(keylessIconData)}; /** * @component @name ${componentName} @@ -35,8 +34,9 @@ import type { AstroComponent } from '../types' * @returns {any} Astro Component * ${deprecated ? `@deprecated ${deprecationReason}` : ''} */ -const ${componentName} = createLucideIcon('${iconName}', ${JSON.stringify(keylessChildren)}) as AstroComponent; +const ${componentName} = createLucideIcon(iconData) as AstroComponent; export default ${componentName}; `; -}); + }, +); diff --git a/packages/astro/src/Icon.astro b/packages/astro/src/Icon.astro index eb504dbad..a2bacca75 100644 --- a/packages/astro/src/Icon.astro +++ b/packages/astro/src/Icon.astro @@ -1,32 +1,44 @@ --- -import defaultAttributes from './defaultAttributes'; +import buildLucideIconNode from './utils/buildLucideIconNode'; import type { IconProps as Props } from './types'; import { hasA11yProp } from './utils/hasA11yProp'; - const { color = 'currentColor', size = 24, + width, + height, 'stroke-width': strokeWidth = 2, absoluteStrokeWidth = false, + nonScalingStroke = false, iconNode = [], + icon = { + node: iconNode, + aliases: [], + size: 24, + }, class: className, ...rest } = Astro.props; + +const [, builtIconAttributes, builtIconNode = []] = buildLucideIconNode(icon, { + color, + width: width ?? size, + height: height ?? size, + strokeWidth, + absoluteStrokeWidth, + nonScalingStroke, + className, + hasA11yProp: hasA11yProp(rest), +}); + +const iconAttributes = { + ...builtIconAttributes, + ...rest, +}; --- - - {iconNode.map(([Tag, attrs]) => )} + + {builtIconNode.map(([Tag, attrs]) => )} diff --git a/packages/astro/src/createLucideIcon.ts b/packages/astro/src/createLucideIcon.ts index 36c1c99e7..7d5b2a594 100644 --- a/packages/astro/src/createLucideIcon.ts +++ b/packages/astro/src/createLucideIcon.ts @@ -1,11 +1,43 @@ import type { AstroComponentFactory } from 'astro/runtime/server/render/astro/factory.js'; -import type { IconNode } from './types'; +import type { LucideIconData, LucideIconNode } from './types'; import { render, renderSlot, createComponent, renderComponent } from 'astro/compiler-runtime'; import Icon from './Icon.astro'; -import { mergeClasses } from './utils/mergeClasses'; -import { toKebabCase } from './utils/toKebabCase'; -export default (iconName: string, iconNode: IconNode): AstroComponentFactory => { +/** + * Creates a Lucide icon component from icon data. + * + * @param icon Icon data object. + * @returns Lucide icon component. + */ +function createLucideIcon(icon: LucideIconData): AstroComponentFactory; + +/** + * Creates a Lucide icon component from the legacy icon arguments. + * + * @param iconName Icon name. + * @param iconNode Icon node. + * @param aliases Optional icon aliases. + * @returns Lucide icon component. + */ +function createLucideIcon( + iconName: string, + iconNode: LucideIconNode[], + aliases?: string[], +): AstroComponentFactory; + +function createLucideIcon( + iconOrIconName: string | LucideIconData, + iconNode: LucideIconNode[] = [], + aliases: string[] = [], +): AstroComponentFactory { + const icon = + typeof iconOrIconName === 'string' + ? { + name: iconOrIconName, + node: iconNode, + aliases, + } + : iconOrIconName; const Component = createComponent( ($$result, $$props: Record, $$slots) => { const { class: className, ...restProps } = $$props; @@ -14,11 +46,8 @@ export default (iconName: string, iconNode: IconNode): AstroComponentFactory => 'Icon', Icon, { - class: mergeClasses( - Boolean(iconName) && `lucide-${toKebabCase(iconName)}`, - Boolean(className) && className, - ), - iconNode, + class: className, + icon, ...restProps, }, { default: () => render`${renderSlot($$result, $$slots['default'])}` }, @@ -28,4 +57,6 @@ export default (iconName: string, iconNode: IconNode): AstroComponentFactory => 'none', ); return Component; -}; +} + +export default createLucideIcon; diff --git a/packages/astro/src/defaultAttributes.ts b/packages/astro/src/defaultAttributes.ts deleted file mode 100644 index 5b1fddd06..000000000 --- a/packages/astro/src/defaultAttributes.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { SVGAttributes } from './types'; - -const defaultAttributes: SVGAttributes = { - 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', -}; - -export default defaultAttributes; diff --git a/packages/astro/src/lucide-astro.ts b/packages/astro/src/lucide-astro.ts index adc0c1003..19d041177 100644 --- a/packages/astro/src/lucide-astro.ts +++ b/packages/astro/src/lucide-astro.ts @@ -3,6 +3,6 @@ export * as icons from './icons/index'; export * from './aliases'; export * from './types'; -export { default as defaultAttributes } from './defaultAttributes'; +export { default as defaultAttributes } from './utils/defaultAttributes'; export { default as createLucideIcon } from './createLucideIcon'; export { default as Icon } from './Icon.astro'; diff --git a/packages/astro/src/types.ts b/packages/astro/src/types.ts index 451e3d181..aff54253b 100644 --- a/packages/astro/src/types.ts +++ b/packages/astro/src/types.ts @@ -1,23 +1,47 @@ import type { HTMLAttributes } from 'astro/types'; +import type { + LucideIconData as SharedLucideIconData, + LucideIconNode as SharedLucideIconNode, +} from './utils/types'; // Type that the Astro language server needs to infer component props in Astro files -export type AstroComponent = (_props: IconProps) => unknown; +export type AstroComponent = (_props: LucideProps) => unknown; -export interface IconProps extends SVGAttributes { +export type LucideProps = SVGAttributes & { color?: string; size?: number | string; 'stroke-width'?: number | string; + /** + * @deprecated Use `nonScalingStroke` instead. + */ absoluteStrokeWidth?: boolean; + nonScalingStroke?: boolean; class?: string; - iconNode?: IconNode; title?: string; -} +}; + +export type IconProps = LucideProps & + ( + | { + icon: LucideIconData; + iconNode?: never; + } + | { + icon?: never; + iconNode: LucideIconNode[]; + } + ); export type SVGAttributes = HTMLAttributes<'svg'>; -export type IconNode = IconNodeChild[]; +export type LucideIconNode = SharedLucideIconNode; -type IconNodeChild = [elementName: SVGElements, attrs: SVGAttributes]; +export type LucideIconData = SharedLucideIconData; + +/** + * @deprecated Use LucideIconNode instead. + */ +export type IconNode = LucideIconNode[]; // All possible svg elements according to the Astro definitions type SVGElements = diff --git a/packages/astro/tests/__snapshots__/Icon.spec.ts.snap b/packages/astro/tests/__snapshots__/Icon.spec.ts.snap index a550ccb86..f3bc1ec4d 100644 --- a/packages/astro/tests/__snapshots__/Icon.spec.ts.snap +++ b/packages/astro/tests/__snapshots__/Icon.spec.ts.snap @@ -10,8 +10,8 @@ exports[`Using Icon Component > should render icon and match snapshot 1`] = ` stroke-width="1" stroke-linecap="round" stroke-linejoin="round" - aria-hidden="true" class="lucide" + aria-hidden="true" > diff --git a/packages/astro/tests/__snapshots__/createLucideIcon.spec.ts.snap b/packages/astro/tests/__snapshots__/createLucideIcon.spec.ts.snap index 13a285420..e53acc2bb 100644 --- a/packages/astro/tests/__snapshots__/createLucideIcon.spec.ts.snap +++ b/packages/astro/tests/__snapshots__/createLucideIcon.spec.ts.snap @@ -10,8 +10,8 @@ exports[`Using createLucideIcon > should create a component from an iconNode 1`] stroke-width="2" stroke-linecap="round" stroke-linejoin="round" - aria-hidden="true" class="lucide lucide-air-vent" + aria-hidden="true" > diff --git a/packages/astro/tests/__snapshots__/lucide-astro.spec.ts.snap b/packages/astro/tests/__snapshots__/lucide-astro.spec.ts.snap index 35f95548d..34154c0eb 100644 --- a/packages/astro/tests/__snapshots__/lucide-astro.spec.ts.snap +++ b/packages/astro/tests/__snapshots__/lucide-astro.spec.ts.snap @@ -10,9 +10,9 @@ exports[`Using lucide icon components > should add a non-default attribute to th stroke-width="2" stroke-linecap="round" stroke-linejoin="round" + class="lucide lucide-face-slightly-smiling lucide-smile" aria-hidden="true" style="position: absolute" - class="lucide lucide-face-slightly-smiling" > @@ -38,8 +38,8 @@ exports[`Using lucide icon components > should adjust the size, stroke color and stroke-width="4" stroke-linecap="round" stroke-linejoin="round" + class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3" aria-hidden="true" - class="lucide lucide-grid-3x3" > should apply all classes to the element stroke-width="2" stroke-linecap="round" stroke-linejoin="round" + class="lucide lucide-droplet lucide-drop my-icon" aria-hidden="true" - class="lucide lucide-droplet my-icon" > `; -exports[`Using lucide icon components > should not scale the strokeWidth when absoluteStrokeWidth is set 1`] = ` +exports[`Using lucide icon components > should apply vector-effect when nonScalingStroke is set 1`] = ` `; @@ -118,8 +127,8 @@ exports[`Using lucide icon components > should pass children to the icon slot 1` stroke-width="2" stroke-linecap="round" stroke-linejoin="round" + class="lucide lucide-face-slightly-smiling lucide-smile" aria-hidden="true" - class="lucide lucide-face-slightly-smiling" > @@ -148,8 +157,8 @@ exports[`Using lucide icon components > should render a component 1`] = ` stroke-width="2" stroke-linecap="round" stroke-linejoin="round" + class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3" aria-hidden="true" - class="lucide lucide-grid-3x3" > should render the icon with default attr stroke-width="2" stroke-linecap="round" stroke-linejoin="round" + class="lucide lucide-grid-3x3 lucide-grid lucide-grid-3-x-3" + aria-hidden="true" +> + + + + + + + + + + + +`; + +exports[`Using lucide icon components > should scale the strokeWidth when absoluteStrokeWidth is set 1`] = ` + `; + +exports[`Using lucide icon components > should scale the strokeWidth when absoluteStrokeWidth is set 1`] = ` + +`; diff --git a/packages/lucide-solid/tests/context.spec.tsx b/packages/lucide-solid/tests/context.spec.tsx index 6ae047a6d..4b591293a 100644 --- a/packages/lucide-solid/tests/context.spec.tsx +++ b/packages/lucide-solid/tests/context.spec.tsx @@ -1,4 +1,5 @@ import { render } from '@solidjs/testing-library'; +import { createSignal } from 'solid-js'; import { describe, expect, it } from 'vitest'; import { House, LucideProvider } from '../src/lucide-solid'; @@ -46,6 +47,23 @@ describe('Using LucideProvider', () => { expect(IconComponent).toHaveAttribute('stroke-width', '4'); }); + it('should update when reactive LucideProvider props change', () => { + const [size, setSize] = createSignal(24); + const { container } = render(() => ( + + + + )); + + expect(container.firstElementChild).toHaveAttribute('width', '24'); + expect(container.firstElementChild).toHaveAttribute('height', '24'); + + setSize(48); + + expect(container.firstElementChild).toHaveAttribute('width', '48'); + expect(container.firstElementChild).toHaveAttribute('height', '48'); + }); + it('should render the icon with LucideProvider and custom absoluteStrokeWidth', () => { const { container } = render(() => ( { expect(IconComponent).toHaveAttribute( 'class', - 'lucide lucide-icon provider-class lucide-house icon-class', + 'lucide lucide-house lucide-home lucide-icon provider-class icon-class', ); }); }); diff --git a/packages/lucide-solid/tests/lucide-solid.spec.tsx b/packages/lucide-solid/tests/lucide-solid.spec.tsx index 3a01b8ac7..1421dd034 100644 --- a/packages/lucide-solid/tests/lucide-solid.spec.tsx +++ b/packages/lucide-solid/tests/lucide-solid.spec.tsx @@ -57,7 +57,7 @@ describe('Using lucide icon components', () => { expect(PenIconRenderedHTML).toBe(Edit2Container.innerHTML); }); - it('should not scale the strokeWidth when absoluteStrokeWidth is set', () => { + it('should scale the strokeWidth when absoluteStrokeWidth is set', () => { const testId = 'grid-icon'; const { container, getByTestId } = render(() => ( { + .reduce>((acc, iconName) => { acc[iconName] = icons[iconName].tags; return acc; }, {}); diff --git a/packages/lucide-static/tsconfig.json b/packages/lucide-static/tsconfig.json index 6e7572cfe..4ae8ddc18 100644 --- a/packages/lucide-static/tsconfig.json +++ b/packages/lucide-static/tsconfig.json @@ -4,7 +4,7 @@ "declaration": true, "noEmitOnError": true, "noFallthroughCasesInSwitch": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "module": "ESNext", "target": "ESNext", "esModuleInterop": true, @@ -13,6 +13,7 @@ "resolveJsonModule": true, "allowImportingTsExtensions": true, "sourceMap": true, - "outDir": "./dist", + "outDir": "./dist" }, + "include": ["src", "scripts"] } diff --git a/packages/lucide/package.json b/packages/lucide/package.json index e5067211e..1657a70a7 100644 --- a/packages/lucide/package.json +++ b/packages/lucide/package.json @@ -39,18 +39,20 @@ "build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --iconFileExtension=.ts --withAliases --aliasNamesOnly --aliasesFileExtension=.ts --exportFileName=index.ts", "build:bundle": "rollup -c rollup.config.mjs", "test": "pnpm build:icons && vitest run", - "test:watch": "vitest watch" + "test:watch": "vitest watch", + "typecheck": "pnpm build:icons && tsc --noEmit" }, "devDependencies": { "@lucide/build-icons": "workspace:*", "@lucide/rollup-plugins": "workspace:*", "@lucide/shared": "workspace:*", "@rollup/plugin-replace": "^6.0.3", - "@testing-library/jest-dom": "^6.9.1", + "@testing-library/jest-dom": "^7.0.1", + "@types/node": "^25.9.1", "jest-serializer-html": "^7.1.0", - "rollup": "^4.62.2", - "rollup-plugin-dts": "^6.4.1", - "typescript": "^5.9.3", - "vite": "^7.3.6" + "rollup": "^4.63.1", + "rollup-plugin-dts": "^6.5.1", + "typescript": "^6.0.3", + "vite": "^8.3.2" } } diff --git a/packages/lucide/src/attributeTypes.ts b/packages/lucide/src/attributeTypes.ts index 2346a3fdc..e82032b1c 100644 --- a/packages/lucide/src/attributeTypes.ts +++ b/packages/lucide/src/attributeTypes.ts @@ -11,15 +11,7 @@ export interface AriaAttributes { 'aria-colspan'?: number | undefined; 'aria-controls'?: string | undefined; 'aria-current'?: - | boolean - | 'false' - | 'true' - | 'page' - | 'step' - | 'location' - | 'date' - | 'time' - | undefined; + boolean | 'false' | 'true' | 'page' | 'step' | 'location' | 'date' | 'time' | undefined; 'aria-describedby'?: string | undefined; 'aria-details'?: string | undefined; 'aria-disabled'?: Booleanish | undefined; @@ -29,15 +21,7 @@ export interface AriaAttributes { 'aria-flowto'?: string | undefined; 'aria-grabbed'?: Booleanish | undefined; 'aria-haspopup'?: - | boolean - | 'false' - | 'true' - | 'menu' - | 'listbox' - | 'tree' - | 'grid' - | 'dialog' - | undefined; + boolean | 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | undefined; 'aria-hidden'?: Booleanish | undefined; 'aria-invalid'?: boolean | 'false' | 'true' | 'grammar' | 'spelling' | undefined; 'aria-keyshortcuts'?: string | undefined; diff --git a/packages/lucide/src/replaceElement.ts b/packages/lucide/src/replaceElement.ts index 306df5bee..e3dacfc4f 100644 --- a/packages/lucide/src/replaceElement.ts +++ b/packages/lucide/src/replaceElement.ts @@ -22,7 +22,7 @@ export const getAttrs = (element: Element): Record => * @returns {Array} */ export const getClassNames = ( - attrs: Record | string, + attrs: Record | string, ): string | string[] => { if (typeof attrs === 'string') return attrs; if (!attrs || !attrs.class) return ''; diff --git a/packages/lucide/tsconfig.json b/packages/lucide/tsconfig.json index 7fdaedbf6..184ea4b08 100644 --- a/packages/lucide/tsconfig.json +++ b/packages/lucide/tsconfig.json @@ -4,14 +4,18 @@ "declaration": true, "noEmitOnError": true, "noFallthroughCasesInSwitch": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "module": "ESNext", "target": "ESNext", "esModuleInterop": true, "allowJs": true, "lib": ["esnext", "dom"], "resolveJsonModule": true, + "skipLibCheck": true, "sourceMap": true, "outDir": "./dist", + "types": ["@testing-library/jest-dom/vitest", "node"] }, + "exclude": ["**/node_modules"], + "include": ["src", "tests"] } diff --git a/packages/shared/package.json b/packages/shared/package.json index befa84965..b8f74780c 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -10,9 +10,18 @@ "license": "ISC", "scripts": { "test": "vitest run", - "test:watch": "vitest watch" + "test:watch": "vitest watch", + "typecheck": "tsc" + }, + "exports": { + ".": "./src/index.ts", + "./types": { + "types": "./src/build/types.ts", + "default": "./src/build/types.ts" + } }, "devDependencies": { - "vite": "^7.3.6" + "@types/node": "^26.4.1", + "vite": "^8.3.2" } } diff --git a/packages/shared/src/build/buildLucideDataUri.ts b/packages/shared/src/build/buildLucideDataUri.ts new file mode 100644 index 000000000..285403350 --- /dev/null +++ b/packages/shared/src/build/buildLucideDataUri.ts @@ -0,0 +1,30 @@ +import type { LucideBuildParams, LucideIconData } from './types'; +import buildLucideSvg from './buildLucideSvg'; + +/** + * Creates a base64 encoded data URI from a Lucide icon object. + * + * @param icon The icon to build. + * @param params Additional build parameters. + */ +function buildLucideDataUri(icon: LucideIconData, params: LucideBuildParams = {}) { + const svg = buildLucideSvg(icon, params); + + // Browser + if (typeof btoa === 'function') { + // Ensure proper UTF-8 handling before base64 + const utf8Bytes = new TextEncoder().encode(svg); + let binary = ''; + for (const element of utf8Bytes) binary += String.fromCodePoint(element); + return `data:image/svg+xml;base64,${btoa(binary)}`; + } + + // Node.js (and other JS runtimes with Buffer) + if (typeof Buffer !== 'undefined') { + return `data:image/svg+xml;base64,${Buffer.from(svg, 'utf8').toString('base64')}`; + } + + throw new Error('No base64 encoder available in this environment.'); +} + +export default buildLucideDataUri; diff --git a/packages/shared/src/build/buildLucideIconElement.ts b/packages/shared/src/build/buildLucideIconElement.ts new file mode 100644 index 000000000..0ac863fa6 --- /dev/null +++ b/packages/shared/src/build/buildLucideIconElement.ts @@ -0,0 +1,36 @@ +import buildLucideIconNode from './buildLucideIconNode'; +import type { LucideBuildParams, LucideIconData, LucideIconNode } from './types'; + +const buildDomElement = ( + document: Document, + [tagName, attributes, children = []]: LucideIconNode, +): Element => { + const element = document.createElementNS('http://www.w3.org/2000/svg', tagName); + for (const [attrName, value] of Object.entries(attributes)) { + if (value !== undefined && value !== null) { + element.setAttribute(attrName, String(value)); + } + } + for (const node of children) { + const childNode = buildDomElement(document, node); + element.appendChild(childNode); + } + return element; +}; + +/** + * Creates an SvgElement from a Lucide icon object. + * + * @param document The document to create the Element in. + * @param icon The icon to build. + * @param params Additional build parameters. + */ +function buildLucideIconElement( + document: Document, + icon: LucideIconData, + params: LucideBuildParams = {}, +) { + return buildDomElement(document, buildLucideIconNode(icon, params)); +} + +export default buildLucideIconElement; diff --git a/packages/shared/src/build/buildLucideIconForReact.ts b/packages/shared/src/build/buildLucideIconForReact.ts new file mode 100644 index 000000000..66b56f291 --- /dev/null +++ b/packages/shared/src/build/buildLucideIconForReact.ts @@ -0,0 +1,27 @@ +import buildLucideIconNode from './buildLucideIconNode'; +import type { LucideBuildParams, LucideIconData, LucideIconNode } from './types'; + +/** + * Creates a React-compatible Lucide icon node (an svgson-like format) from a Lucide icon object. + * + * @param icon The icon to build. + * @param params Additional build parameters. + */ +function buildLucideIconForReact( + icon: LucideIconData, + params: LucideBuildParams = {}, +): LucideIconNode { + return buildLucideIconNode(icon, { + ...params, + attributeNames: { + ...params.attributeNames, + class: 'className', + 'stroke-width': 'strokeWidth', + 'stroke-linecap': 'strokeLinecap', + 'stroke-linejoin': 'strokeLinejoin', + 'vector-effect': 'vectorEffect', + }, + }); +} + +export default buildLucideIconForReact; diff --git a/packages/shared/src/build/buildLucideIconNode.ts b/packages/shared/src/build/buildLucideIconNode.ts new file mode 100644 index 000000000..a88f323e7 --- /dev/null +++ b/packages/shared/src/build/buildLucideIconNode.ts @@ -0,0 +1,88 @@ +import type { LucideBuildParams, LucideIconData, LucideIconNode, SVGProps } from './types'; +import defaultAttributes from './defaultAttributes'; +import { mergeClasses } from '../utils/mergeClasses'; + +function isDefined(value: T | null | undefined): value is T { + return value !== null && value !== undefined; +} + +/** + * Creates a Lucide icon node (an svgson-like format) from a Lucide icon object. + * + * @param icon The icon to build. + * @param params Additional build parameters. + */ +function buildLucideIconNode(icon: LucideIconData, params: LucideBuildParams = {}): LucideIconNode { + const attributeNames = params.attributeNames ?? {}; + const getAttributeName = (attributeName: string) => + attributeNames[attributeName] ?? attributeName; + + const viewBoxWidth = icon.size ?? icon.width ?? defaultAttributes['width']; + const viewBoxHeight = icon.size ?? icon.height ?? defaultAttributes['height']; + const aliasClassNames = + icon.aliases + ?.filter((alias: string): alias is string => typeof alias === 'string' && alias.trim() !== '') + .map((alias: string) => `lucide-${alias}`) ?? []; + const iconClassNames = [...(icon.name ? [`lucide-${icon.name}`] : []), ...aliasClassNames]; + const classNamesFromClassName = params.className?.split(' ').filter(Boolean) ?? []; + const className = + params.includeDefaultClasses === false + ? mergeClasses(...classNamesFromClassName) + : mergeClasses('lucide', ...iconClassNames, ...classNamesFromClassName); + + const calculatedStrokeWidth = params.absoluteStrokeWidth + ? (Number(params.strokeWidth ?? defaultAttributes['stroke-width']) * + Number(icon.size ?? icon.width ?? defaultAttributes['width'])) / + Number(params.size ?? params.width ?? defaultAttributes['width']) + : (params.strokeWidth ?? defaultAttributes['stroke-width']); + + const attributes = { + ...Object.entries(defaultAttributes).reduce((attrs, [attrName, value]) => { + attrs[getAttributeName(attrName)] = value; + return attrs; + }, {} as SVGProps), + ...('color' in params && + params.color && { + [getAttributeName('stroke')]: params.color, + }), + ...('size' in params && + isDefined(params.size) && { + [getAttributeName('width')]: params.size, + [getAttributeName('height')]: params.size, + }), + ...('width' in params && + isDefined(params.width) && { + [getAttributeName('width')]: params.width, + }), + ...('height' in params && + isDefined(params.height) && { + [getAttributeName('height')]: params.height, + }), + [getAttributeName('stroke-width')]: calculatedStrokeWidth, + ...(className && { + [getAttributeName('class')]: className, + }), + [getAttributeName('viewBox')]: `0 0 ${viewBoxWidth} ${viewBoxHeight}`, + ...(params.hasA11yProp === false + ? { + [getAttributeName('aria-hidden')]: 'true', + } + : {}), + ...('attributes' in params && params.attributes), + }; + + return [ + 'svg', + attributes, + icon.node.map((child): LucideIconNode => { + const [name, attrs, children] = child; + const nextAttrs = params.nonScalingStroke + ? { [getAttributeName('vector-effect')]: 'non-scaling-stroke', ...attrs } + : attrs; + + return children ? [name, nextAttrs, children] : [name, nextAttrs]; + }), + ]; +} + +export default buildLucideIconNode; diff --git a/packages/shared/src/build/buildLucideSvg.ts b/packages/shared/src/build/buildLucideSvg.ts new file mode 100644 index 000000000..fa19192ca --- /dev/null +++ b/packages/shared/src/build/buildLucideSvg.ts @@ -0,0 +1,31 @@ +import buildLucideIconNode from './buildLucideIconNode'; +import type { LucideBuildParams, LucideIconData, LucideIconNode } from './types'; + +const entities = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', +} as const; + +const escapeAttribute = (value: string) => + value.replace(/[&<>"']/g, (char) => entities[char as keyof typeof entities]); + +const buildDomNode = ([tagName, attributes, children = []]: LucideIconNode): string => + `<${tagName} ${Object.entries(attributes) + .filter(([, value]) => value !== undefined && value !== null) + .map(([attrName, value]) => `${attrName}="${escapeAttribute(String(value))}"`) + .join(' ')}>${children?.map((child) => buildDomNode(child)).join('')}`; + +/** + * Creates an SVG string from a Lucide icon object. + * + * @param icon The icon to build. + * @param params Additional build parameters. + */ +function buildLucideSvg(icon: LucideIconData, params: LucideBuildParams = {}) { + return buildDomNode(buildLucideIconNode(icon, params)); +} + +export default buildLucideSvg; diff --git a/packages/svelte/src/defaultAttributes.ts b/packages/shared/src/build/defaultAttributes.ts similarity index 75% rename from packages/svelte/src/defaultAttributes.ts rename to packages/shared/src/build/defaultAttributes.ts index 61d05a1d6..2400ca251 100644 --- a/packages/svelte/src/defaultAttributes.ts +++ b/packages/shared/src/build/defaultAttributes.ts @@ -1,6 +1,4 @@ -import type { Attrs } from './types.js'; - -const defaultAttributes: Attrs = { +const defaultAttributes = { xmlns: 'http://www.w3.org/2000/svg', width: 24, height: 24, @@ -10,6 +8,6 @@ const defaultAttributes: Attrs = { 'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round', -}; +} as const; export default defaultAttributes; diff --git a/packages/shared/src/build/defaultReactAttributes.ts b/packages/shared/src/build/defaultReactAttributes.ts new file mode 100644 index 000000000..c8bca2e56 --- /dev/null +++ b/packages/shared/src/build/defaultReactAttributes.ts @@ -0,0 +1,13 @@ +const defaultReactAttributes = { + xmlns: 'http://www.w3.org/2000/svg', + width: 24, + height: 24, + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + strokeWidth: 2, + strokeLinecap: 'round', + strokeLinejoin: 'round', +} as const; + +export default defaultReactAttributes; diff --git a/packages/shared/src/build/types.ts b/packages/shared/src/build/types.ts new file mode 100644 index 000000000..39a99662f --- /dev/null +++ b/packages/shared/src/build/types.ts @@ -0,0 +1,76 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export type SVGProps = Record; + +/** + * A Lucide icon node (an svgson-like internal format) + */ +export type LucideIconNode< + TName extends string = string, + TProps extends Record = SVGProps, +> = + | [name: TName, attributes: TProps] + | [name: TName, attributes: TProps, children: LucideIconNode[]]; + +/** + * A Lucide icon object that fully describes an icon to be displayed. + */ +export type LucideIconData< + TName extends string = string, + TProps extends Record = SVGProps, +> = { + name?: string; + node: LucideIconNode[]; + aliases?: string[]; +} & ( + | { size?: number; width?: never; height?: never } + | { size?: never; width?: number; height?: number } +); + +/** + * Build parameters for creating a Lucide icon instance for display. + */ +export type LucideBuildParams = SVGProps> = { + /** + * The color of the icon. + */ + color?: string; + /** + * The stroke width. + */ + strokeWidth?: string | number; + /** + * Whether to use absolute stroke width. + * @deprecated Use `nonScalingStroke` instead. + */ + absoluteStrokeWidth?: boolean; + /** + * Adds [`vector-effect="non-scaling-stroke"`](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/vector-effect) to child elements. + */ + nonScalingStroke?: boolean; + /** + * Extra CSS class names to pass to the SVG element. + */ + className?: string; + /** + * Include default Lucide classes on the SVG element. + * + * @default true + */ + includeDefaultClasses?: boolean; + /** + * Whether the rendered icon already has accessibility props. + * If false, `aria-hidden="true"` is added automatically. + */ + hasA11yProp?: boolean; + /** + * Any extra attributes to pass to the SVG element. + */ + attributes?: TProps; + /** + * The attribute names to use for various HTML attributes. + */ + attributeNames?: Record; +} & ( + | { size?: string | number | null; width?: never; height?: never } + | { size?: never; width?: string | number | null; height?: string | number | null } +); diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index ca8a3aa23..e0bd3a2f1 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,7 +1,16 @@ +export { default as buildLucideDataUri } from './build/buildLucideDataUri'; +export { default as buildLucideIconElement } from './build/buildLucideIconElement'; +export { default as buildLucideIconNode } from './build/buildLucideIconNode'; +export { default as buildLucideIconForReact } from './build/buildLucideIconForReact'; +export { default as buildLucideSvg } from './build/buildLucideSvg'; +export { default as defaultAttributes } from './build/defaultAttributes'; +export { default as defaultReactAttributes } from './build/defaultReactAttributes'; +export type { LucideBuildParams, LucideIconData, LucideIconNode, SVGProps } from './build/types'; export * from './utils/hasA11yProp'; export * from './utils/isEmptyString'; export * from './utils/mergeClasses'; export * from './utils/toCamelCase'; export * from './utils/toKebabCase'; +export * from './utils/toLucideIconData'; export * from './utils/toPascalCase'; export * from './utility-types'; diff --git a/packages/shared/src/utils/toCamelCase.ts b/packages/shared/src/utils/toCamelCase.ts index 52ade7dbd..6faf1194b 100644 --- a/packages/shared/src/utils/toCamelCase.ts +++ b/packages/shared/src/utils/toCamelCase.ts @@ -4,7 +4,24 @@ * @param {string} string * @returns {string} A camelized string */ -export const toCamelCase = (string: T) => - string.replace(/^([A-Z])|[\s-_]+(\w)/g, (match, p1, p2) => - p2 ? p2.toUpperCase() : p1.toLowerCase(), - ); +export const toCamelCase = (string: T) => { + let out = ''; + let upperNext = false; + + for (const ch of string) { + if (ch === '-' || ch === '_' || ch <= ' ') { + upperNext = out.length > 0; + continue; + } + + if (out.length === 0) { + out += ch.toLowerCase(); + } else { + out += upperNext ? ch.toUpperCase() : ch; + } + + upperNext = false; + } + + return out as string; +}; diff --git a/packages/shared/src/utils/toKebabCase.ts b/packages/shared/src/utils/toKebabCase.ts index 4db12960a..02e09970e 100644 --- a/packages/shared/src/utils/toKebabCase.ts +++ b/packages/shared/src/utils/toKebabCase.ts @@ -5,4 +5,4 @@ * @returns {string} A kebabized string */ export const toKebabCase = (string: string) => - string.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); + string?.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); diff --git a/packages/shared/src/utils/toLucideIconData.ts b/packages/shared/src/utils/toLucideIconData.ts new file mode 100644 index 000000000..13d450f95 --- /dev/null +++ b/packages/shared/src/utils/toLucideIconData.ts @@ -0,0 +1,22 @@ +import { toKebabCase } from './toKebabCase'; +import type { LucideIconData, LucideIconNode } from '../build/types'; + +/** + * Converts legacy `(name, iconNode, aliases?)` icon arguments into icon data object format. + */ +export function toLucideIconData( + iconName: string, + iconNode: LucideIconNode[], + aliases: string[] = [], +): LucideIconData { + if (iconNode == null) { + throw new Error('[lucide]: iconNode is required when icon name is used'); + } + + return { + name: toKebabCase(iconName), + size: 24, + node: iconNode, + ...(aliases.length > 0 ? { aliases } : {}), + }; +} diff --git a/packages/shared/tests/buildLucideIconForReact.spec.ts b/packages/shared/tests/buildLucideIconForReact.spec.ts new file mode 100644 index 000000000..8207c5ec3 --- /dev/null +++ b/packages/shared/tests/buildLucideIconForReact.spec.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from 'vitest'; +import buildLucideIconForReact from '../src/build/buildLucideIconForReact'; +import defaultReactAttributes from '../src/build/defaultReactAttributes'; +import type { LucideIconData, LucideIconNode, SVGProps } from '../src/build/types'; + +const icon: LucideIconData = { + name: 'house', + size: 24, + node: [['path', { d: 'M0 0h1' }]], +}; + +const childrenOf = (node: LucideIconNode) => (node.at(2) ?? []) as LucideIconNode[]; + +describe('buildLucideIconForReact', () => { + it('uses the React spelling for the hyphenated SVG attributes', () => { + const [, attributes] = buildLucideIconForReact(icon); + + expect(attributes).toMatchObject({ + strokeWidth: defaultReactAttributes.strokeWidth, + strokeLinecap: defaultReactAttributes.strokeLinecap, + strokeLinejoin: defaultReactAttributes.strokeLinejoin, + className: 'lucide lucide-house', + }); + + expect(attributes).not.toHaveProperty('stroke-width'); + expect(attributes).not.toHaveProperty('stroke-linecap'); + expect(attributes).not.toHaveProperty('stroke-linejoin'); + expect(attributes).not.toHaveProperty('class'); + }); + + it('leaves the attributes React already accepts untouched', () => { + const [, attributes] = buildLucideIconForReact(icon); + + expect(attributes).toMatchObject({ + xmlns: defaultReactAttributes.xmlns, + width: defaultReactAttributes.width, + height: defaultReactAttributes.height, + viewBox: defaultReactAttributes.viewBox, + fill: defaultReactAttributes.fill, + stroke: defaultReactAttributes.stroke, + }); + }); + + it('keeps aria-hidden hyphenated, which is how React expects it', () => { + const [, attributes] = buildLucideIconForReact(icon, { hasA11yProp: false }); + + expect(attributes['aria-hidden']).toBe('true'); + expect(attributes).not.toHaveProperty('ariaHidden'); + }); + + it('renames vector-effect on the child nodes for a non-scaling stroke', () => { + const children = childrenOf(buildLucideIconForReact(icon, { nonScalingStroke: true })); + const childAttributes = (children[0]?.[1] ?? {}) as SVGProps; + + expect(childAttributes.vectorEffect).toBe('non-scaling-stroke'); + expect(childAttributes).not.toHaveProperty('vector-effect'); + }); + + it('merges caller attribute names but keeps its own React spellings', () => { + const [, attributes] = buildLucideIconForReact(icon, { + attributeNames: { fill: 'fillColor', class: 'cssClass' }, + }); + + expect(attributes.fillColor).toBe(defaultReactAttributes.fill); + expect(attributes.className).toBe('lucide lucide-house'); + expect(attributes).not.toHaveProperty('cssClass'); + }); +}); diff --git a/packages/shared/tests/buildLucideSvg.spec.ts b/packages/shared/tests/buildLucideSvg.spec.ts new file mode 100644 index 000000000..896a7464e --- /dev/null +++ b/packages/shared/tests/buildLucideSvg.spec.ts @@ -0,0 +1,14 @@ +import { describe, it, expect } from 'vitest'; +import buildLucideSvg from '../src/build/buildLucideSvg'; + +const icon = { name: 'dot', size: 24, node: [['circle', { cx: 12, cy: 12, r: 1 }]] } as const; + +describe('buildLucideSvg', () => { + it('should escape special characters in attribute values', () => { + const svg = buildLucideSvg(icon as any, { + attributes: { 'data-label': 'a\'s "quoted" & c' }, + }); + + expect(svg).toContain('data-label="a's "quoted" <b> & c"'); + }); +}); diff --git a/packages/shared/tests/toLucideIconData.spec.ts b/packages/shared/tests/toLucideIconData.spec.ts new file mode 100644 index 000000000..bbb321b93 --- /dev/null +++ b/packages/shared/tests/toLucideIconData.spec.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest'; +import { toLucideIconData } from '../src/utils/toLucideIconData'; +import type { LucideIconNode } from '../src/build/types'; + +describe('toLucideIconData', () => { + it('converts legacy name and node to icon data object', () => { + const iconNode: LucideIconNode[] = [['path', { d: 'M0 0h1' }]]; + + expect(toLucideIconData('AirVent', iconNode)).toEqual({ + name: 'air-vent', + size: 24, + node: iconNode, + }); + }); + + it('adds aliases when provided for legacy input', () => { + const iconNode: LucideIconNode[] = [['path', { d: 'M0 0h1' }]]; + + expect(toLucideIconData('AirVent', iconNode, ['vent'])).toEqual({ + name: 'air-vent', + size: 24, + node: iconNode, + aliases: ['vent'], + }); + }); + + it('throws when icon node is missing for legacy string input', () => { + expect(() => toLucideIconData('AirVent', undefined as never)).toThrowError( + '[lucide]: iconNode is required when icon name is used', + ); + }); +}); diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 000000000..e6e47d48c --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src", "tests"] +} diff --git a/packages/solid/README.md b/packages/solid/README.md new file mode 100644 index 000000000..242f974b8 --- /dev/null +++ b/packages/solid/README.md @@ -0,0 +1,89 @@ +

+ + Lucide icon library for Solid 2 applications. + +

+ +

+Lucide icon library for Solid 2 applications. +

+ +
+ + [![npm](https://img.shields.io/npm/v/%40lucide%2Fsolid?color=blue)](https://www.npmjs.com/package/@lucide/solid) + ![NPM Downloads](https://img.shields.io/npm/dw/%40lucide%2Fsolid) + [![License](https://img.shields.io/badge/license-ISC-green)](https://lucide.dev/license) +
+ +

+ About + · + Icons + · + Documentation + · + License +

+ +# Lucide Solid + +Implementation of the Lucide icon library for Solid 2 applications. + +## Installation + +```sh +pnpm add @lucide/solid +``` + +```sh +npm install @lucide/solid +``` + +```sh +yarn add @lucide/solid +``` + +```sh +bun add @lucide/solid +``` + +> `@lucide/solid` is only for Solid 2, for Solid 1 use the [`lucide-solid`](https://www.npmjs.com/package/lucide-solid) package. Solid 2 is still a release candidate. + +## Documentation + +For full documentation, visit [lucide.dev](https://lucide.dev/guide/solid) + +## Community + +Join the [Discord server](https://discord.gg/EH6nSts) to chat with the maintainers and other users. + +## License + +Lucide is licensed under the ISC license. See [LICENSE](https://lucide.dev/license). + +[//]: <> (Sponsors) + +## Sponsors + + + Powered by Vercel + + +DigitalOcean Referral Badge + +### Hero backers 🦸 + +Zephyr Cloud – From idea to prod: fast micro-frontend delivery! + +### Awesome backers � + +pdfme – Open-source PDF generation library built with TypeScript and React. +Pax Historia – An alternate history sandbox game + +### Backers ☕ + +Fina Money – Modular Finance Tracker + +### Other contributors 💸 + +You can find all our past and non-recurring financial contributors at [our Open Collective page](https://opencollective.com/lucide-icons). diff --git a/packages/solid/package.json b/packages/solid/package.json new file mode 100644 index 000000000..44e6dcd0c --- /dev/null +++ b/packages/solid/package.json @@ -0,0 +1,94 @@ +{ + "name": "@lucide/solid", + "description": "A Lucide icon library package for Solid 2 applications.", + "version": "0.0.1", + "license": "ISC", + "homepage": "https://lucide.dev", + "bugs": "https://github.com/lucide-icons/lucide/issues", + "repository": { + "type": "git", + "url": "https://github.com/lucide-icons/lucide.git", + "directory": "packages/solid" + }, + "keywords": [ + "Lucide", + "Solid", + "SolidJS", + "Feather", + "Icons", + "Icon", + "SVG", + "Feather Icons", + "Fontawesome", + "Font Awesome" + ], + "author": "Eric Fennis", + "source": "src/lucide-solid.ts", + "main": "dist/cjs/lucide-solid.js", + "module": "dist/esm/lucide-solid.mjs", + "types": "dist/types/lucide-solid.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/types/lucide-solid.d.ts", + "solid": "./dist/source/lucide-solid.jsx", + "import": "./dist/esm/lucide-solid.mjs", + "browser": "./dist/esm/lucide-solid.mjs", + "require": "./dist/cjs/lucide-solid.js" + }, + "./icons": { + "types": "./dist/types/lucide-solid.d.ts", + "solid": "./dist/source/lucide-solid.jsx", + "import": "./dist/esm/lucide-solid.mjs", + "browser": "./dist/esm/lucide-solid.mjs", + "require": "./dist/cjs/lucide-solid.js" + }, + "./icons/*": { + "types": "./dist/types/icons/*.d.ts", + "solid": "./dist/source/icons/*.jsx", + "import": "./dist/esm/icons/*.mjs", + "browser": "./dist/esm/icons/*.mjs", + "require": "./dist/cjs/icons/*.js" + } + }, + "sideEffects": false, + "scripts": { + "build": "pnpm clean && pnpm copy:license && pnpm build:icons && pnpm build:bundle", + "copy:license": "cp ../../LICENSE ./LICENSE", + "clean": "rm -rf dist && rm -rf stats && rm -rf ./src/icons/*.ts", + "build:transpile": "tsc --jsx preserve -t es2020 --rootDir src --outDir dist --noEmit false", + "build:version": "node ./scripts/replaceVersion.mjs", + "build:bundle": "rollup -c rollup.config.mjs", + "build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --withAliases --separateAliasesFile --separateAliasesFileIgnore=fingerprint --aliasesFileExtension=.ts --iconFileExtension=.tsx --exportFileName=index.ts", + "typecheck": "tsc", + "typecheck:watch": "tsc -w", + "test": "pnpm build:icons && vitest run", + "test:watch": "vitest watch" + }, + "devDependencies": { + "@babel/core": "^7.29.7", + "@babel/preset-env": "^7.29.7", + "@babel/preset-typescript": "^7.29.7", + "@lucide/build-icons": "workspace:*", + "@lucide/rollup-plugins": "workspace:*", + "@lucide/shared": "workspace:*", + "@rollup/plugin-babel": "^6.1.0", + "@solidjs/testing-library": "next", + "@solidjs/vite-plugin": "next", + "@solidjs/web": "next", + "@testing-library/jest-dom": "^7.0.1", + "babel-preset-solid": "next", + "esbuild": "^0.28.2", + "jest-serializer-html": "^7.1.0", + "rollup-plugin-dts": "^6.5.1", + "rollup": "^4.63.1", + "solid-js": "next", + "typescript": "^6.0.3", + "vite": "^7.3.6" + }, + "peerDependencies": { + "solid-js": "^2.0.0-rc.0" + } +} diff --git a/packages/solid/rollup.config.mjs b/packages/solid/rollup.config.mjs new file mode 100644 index 000000000..1e82f7cee --- /dev/null +++ b/packages/solid/rollup.config.mjs @@ -0,0 +1,137 @@ +import path from 'path'; +import { babel } from '@rollup/plugin-babel'; +import esbuild from 'esbuild'; +import plugins from '@lucide/rollup-plugins'; +import dts from 'rollup-plugin-dts'; + +import pkg from './package.json' with { type: 'json' }; + +const dtsOptions = { + includeExternal: ['@lucide/shared/types'], + tsconfig: './tsconfig.json', +}; + +const packageName = '@lucide/solid'; +const outputFileName = 'lucide-solid'; +const outputDir = 'dist'; +const inputs = ['src/lucide-solid.ts']; + +const bundles = [ + { + format: 'cjs', + inputs, + outputDir, + preserveModules: true, + }, + { + format: 'esm', + inputs, + outputDir, + preserveModules: true, + extension: 'mjs', + }, +]; + +const configs = bundles + .map(({ inputs, outputDir, format, preserveModules, extension = 'js' }) => + inputs.map((input) => ({ + input, + plugins: [ + babel({ + extensions: ['.ts', '.tsx', '.js', '.jsx'], + babelHelpers: 'bundled', + presets: [ + 'babel-preset-solid', + '@babel/preset-typescript', + ['@babel/preset-env', { bugfixes: true, targets: 'last 2 years' }], + ], + }), + ...plugins({ + pkg, + withEsbuild: false, + }), + format === 'esm' + ? { + name: 'ts', + buildEnd() { + // Transpile typescript to './dist/source' + esbuild.build({ + entryPoints: ['./src/**/*.tsx', './src/**/*.ts'], + outdir: './dist/source', + outExtension: { + '.js': '.jsx', + }, + loader: { + '.js': 'jsx', + }, + jsx: 'preserve', + jsxImportSource: '@solidjs/web', + bundle: true, + format: 'esm', + sourcemap: true, + target: ['esnext'], + banner: { + js: `/** +* @license ${pkg.name} v${pkg.version} - ${pkg.license} +* +* This source code is licensed under the ${pkg.license} license. +* See the LICENSE file in the root directory of this source tree. +*/`, + }, + plugins: [ + { + name: 'externalize-everything-except-own-dependencies', + setup(build) { + build.onResolve({ filter: /(.*)/ }, (args) => { + const modulePath = path.join(args.resolveDir, args.path); + if ( + args.kind === 'import-statement' && + args.path !== '@lucide/shared' && + !modulePath.includes('packages/shared') + ) { + return { path: args.path, external: true }; + } + }); + }, + }, + ], + external: ['solid-js', '@solidjs/web'], + }); + }, + } + : null, + ], + external: ['solid-js', '@solidjs/web'], + output: { + name: packageName, + ...(preserveModules + ? { + dir: `${outputDir}/${format}`, + exports: 'auto', + entryFileNames: `[name].${extension}`, + } + : { + file: `${outputDir}/${format}/${outputFileName}.${extension}`, + }), + format: format === 'source' ? 'esm' : format, + preserveModules, + preserveModulesRoot: 'src', + sourcemap: true, + }, + })), + ) + .flat(); + +const typesConfig = { + input: inputs[0], + output: { + dir: `${outputDir}/types`, + format: 'es', + preserveModules: true, + preserveModulesRoot: 'src', + entryFileNames: '[name].d.ts', + }, + plugins: [dts(dtsOptions)], +}; + +export default [...configs, typesConfig]; diff --git a/packages/solid/scripts/exportTemplate.mts b/packages/solid/scripts/exportTemplate.mts new file mode 100644 index 000000000..c32878174 --- /dev/null +++ b/packages/solid/scripts/exportTemplate.mts @@ -0,0 +1,33 @@ +import base64SVG from '@lucide/build-icons/utils/base64SVG'; +import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate'; + +export default defineExportTemplate( + async ({ componentName, iconName, iconData, getSvg, deprecated, deprecationReason }) => { + const svgContents = await getSvg(); + const svgBase64 = base64SVG(svgContents); + + return ` +import Icon from '../Icon'; +import type { LucideIconData, LucideProps } from '../types'; + +const iconData: LucideIconData = ${JSON.stringify(iconData)}; + +/** + * @component @name ${componentName} + * @description Lucide SVG icon component, renders SVG Element with children. + * + * @preview ![img](data:image/svg+xml;base64,${svgBase64}) - https://lucide.dev/icons/${iconName} + * @see https://lucide.dev/guide/solid - Documentation + * + * @param {Object} props - Lucide icons props and any valid SVG attribute + * @returns {JSX.Element} JSX Element + * ${deprecated ? `@deprecated ${deprecationReason}` : ''} + */ +const ${componentName} = (props: LucideProps) => ( + +) + +export default ${componentName}; +`; + }, +); diff --git a/packages/solid/src/Icon.tsx b/packages/solid/src/Icon.tsx new file mode 100644 index 000000000..dd4019c69 --- /dev/null +++ b/packages/solid/src/Icon.tsx @@ -0,0 +1,75 @@ +import { createMemo, For, omit, useContext } from 'solid-js'; +import { Dynamic } from '@solidjs/web'; +import { buildLucideIconNode, hasA11yProp, mergeClasses } from '@lucide/shared'; +import { LucideIconData, LucideIconNode, LucideProps } from './types'; +import { LucideContext } from './context'; + +type IconProps = + | { + icon: LucideIconData; + iconNode?: never; + } + | { + icon?: never; + iconNode?: LucideIconNode[]; + }; + +const LOCAL_PROP_KEYS = [ + 'color', + 'size', + 'width', + 'height', + 'strokeWidth', + 'children', + 'class', + 'icon', + 'iconNode', + 'absoluteStrokeWidth', + 'nonScalingStroke', +] as const; + +const Icon = (props: LucideProps & IconProps) => { + const globalProps = useContext(LucideContext); + + const rest = () => omit(props, ...LOCAL_PROP_KEYS); + + const icon = createMemo( + () => + props.icon ?? { + node: props.iconNode ?? ([] as LucideIconNode[]), + size: 24, + aliases: [], + }, + ); + + const builtIcon = createMemo(() => + buildLucideIconNode(icon(), { + color: props.color ?? globalProps.color, + width: props.width ?? props.size ?? globalProps.size, + height: props.height ?? props.size ?? globalProps.size, + strokeWidth: props.strokeWidth ?? globalProps.strokeWidth, + absoluteStrokeWidth: props.absoluteStrokeWidth ?? globalProps.absoluteStrokeWidth, + nonScalingStroke: props.nonScalingStroke ?? globalProps.nonScalingStroke, + className: mergeClasses('lucide-icon', globalProps.class, props.class), + hasA11yProp: Boolean(props.children) || hasA11yProp(rest()), + attributes: rest(), + }), + ); + + return ( + + + {([elementName, attrs]) => { + return ( + + ); + }} + + + ); +}; + +export default Icon; diff --git a/packages/solid/src/aliases/index.ts b/packages/solid/src/aliases/index.ts new file mode 100644 index 000000000..bbe66d0a2 --- /dev/null +++ b/packages/solid/src/aliases/index.ts @@ -0,0 +1,3 @@ +export * from './aliases'; +export * from './prefixed'; +export * from './suffixed'; diff --git a/packages/solid/src/context.tsx b/packages/solid/src/context.tsx new file mode 100644 index 000000000..fd9064f5b --- /dev/null +++ b/packages/solid/src/context.tsx @@ -0,0 +1,37 @@ +import { createContext, omit, type Element } from 'solid-js'; + +export const LucideContext = createContext<{ + size?: number; + color?: string; + strokeWidth?: number; + /** + * @deprecated Use `nonScalingStroke` instead. + */ + absoluteStrokeWidth?: boolean; + nonScalingStroke?: boolean; + class?: string; +}>({ + size: 24, + color: 'currentColor', + strokeWidth: 2, + absoluteStrokeWidth: false, + nonScalingStroke: false, + class: '', +}); + +interface LucideProviderProps { + children: Element; + size?: number; + color?: string; + strokeWidth?: number; + /** + * @deprecated Use `nonScalingStroke` instead. + */ + absoluteStrokeWidth?: boolean; + nonScalingStroke?: boolean; + class?: string; +} + +export function LucideProvider(props: LucideProviderProps) { + return {props.children}; +} diff --git a/packages/solid/src/defaultAttributes.ts b/packages/solid/src/defaultAttributes.ts new file mode 100644 index 000000000..cc4036fbd --- /dev/null +++ b/packages/solid/src/defaultAttributes.ts @@ -0,0 +1 @@ +export { defaultAttributes as default } from '@lucide/shared'; diff --git a/packages/solid/src/icons/.gitkeep b/packages/solid/src/icons/.gitkeep new file mode 100644 index 000000000..e3d85f774 --- /dev/null +++ b/packages/solid/src/icons/.gitkeep @@ -0,0 +1 @@ +Folder for generated icons diff --git a/packages/solid/src/lucide-solid.ts b/packages/solid/src/lucide-solid.ts new file mode 100644 index 000000000..6242889b9 --- /dev/null +++ b/packages/solid/src/lucide-solid.ts @@ -0,0 +1,7 @@ +export * from './icons'; +export * as icons from './icons'; +export * from './aliases'; +export * from './types'; +export * from './context'; + +export { default as Icon } from './Icon'; diff --git a/packages/solid/src/types.ts b/packages/solid/src/types.ts new file mode 100644 index 000000000..e99220569 --- /dev/null +++ b/packages/solid/src/types.ts @@ -0,0 +1,33 @@ +import type { JSX } from '@solidjs/web'; +import type { + LucideIconData as SharedLucideIconData, + LucideIconNode as SharedLucideIconNode, +} from '@lucide/shared/types'; + +export type SVGAttributes = Partial>; + +export type LucideIconNode = SharedLucideIconNode; + +export type LucideIconData = SharedLucideIconData; + +/** + * @deprecated Use LucideIconNode instead. + */ +export type IconNode = LucideIconNode[]; + +export interface LucideProps extends SVGAttributes { + key?: string | number; + class?: string; + size?: string | number; + width?: string | number; + height?: string | number; + color?: string; + strokeWidth?: string | number; + /** + * @deprecated Use `nonScalingStroke` instead. + */ + absoluteStrokeWidth?: boolean; + nonScalingStroke?: boolean; +} + +export type LucideIcon = (props: LucideProps) => JSX.Element; diff --git a/packages/solid/tests/Icon.spec.tsx b/packages/solid/tests/Icon.spec.tsx new file mode 100644 index 000000000..500bcff23 --- /dev/null +++ b/packages/solid/tests/Icon.spec.tsx @@ -0,0 +1,154 @@ +import { describe, it, expect } from 'vitest'; +import { render } from '@solidjs/testing-library'; +import { createSignal, flush } from 'solid-js'; + +import { airVent } from './testIconNodes'; +import { Icon } from '../src/lucide-solid'; + +describe('Using Icon Component', () => { + const airVentIcon = { name: 'air-vent', node: airVent }; + + it('should render icon based on a iconNode', async () => { + const { container } = render(() => ( + + )); + + expect(container.firstChild).toBeDefined(); + }); + + it('should render icon and match snapshot', async () => { + const { container } = render(() => ( + + )); + + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should render icon based on icon data', async () => { + const { container } = render(() => ( + + )); + + expect(container.firstChild).toBeDefined(); + }); + + it('should support nonScalingStroke', async () => { + const { container } = render(() => ( + + )); + + expect(container.firstChild?.firstChild).toHaveAttribute('vector-effect', 'non-scaling-stroke'); + }); + + it('should update when reactive props change', async () => { + const [size, setSize] = createSignal(24); + const { container } = render(() => ( + + )); + + expect(container.firstChild).toHaveAttribute('width', '24'); + expect(container.firstChild).toHaveAttribute('height', '24'); + + setSize(48); + flush(); + + expect(container.firstChild).toHaveAttribute('width', '48'); + expect(container.firstChild).toHaveAttribute('height', '48'); + }); +}); + +describe('Icon Component Accessibility', () => { + it('should have aria-hidden prop when no aria prop is present', async () => { + const { container } = render(() => ( + + )); + + expect(container.firstChild).toHaveAttribute('aria-hidden', 'true'); + }); + + it('should not have aria-hidden prop when aria prop is present', async () => { + const { container } = render(() => ( + + )); + + expect(container.firstChild).not.toHaveAttribute('aria-hidden'); + }); + + it('should not have aria-hidden prop when title prop is present', async () => { + const { container } = render(() => ( + + )); + + expect(container.firstChild).not.toHaveAttribute('aria-hidden'); + }); + + it('should not have aria-hidden prop when there are children that could be a element', async () => { + const { container } = render(() => ( + <Icon + iconNode={airVent} + size={48} + stroke="red" + absoluteStrokeWidth + > + <title>Some title + + )); + + expect(container.firstChild).not.toHaveAttribute('aria-hidden'); + }); + + it('should never override aria-hidden prop', async () => { + const { container } = render(() => ( + + )); + + expect(container.firstChild).toHaveAttribute('aria-hidden', 'false'); + }); +}); diff --git a/packages/solid/tests/__snapshots__/Icon.spec.tsx.snap b/packages/solid/tests/__snapshots__/Icon.spec.tsx.snap new file mode 100644 index 000000000..5a783f6e1 --- /dev/null +++ b/packages/solid/tests/__snapshots__/Icon.spec.tsx.snap @@ -0,0 +1,34 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Using Icon Component > should render icon and match snapshot 1`] = ` + +`; diff --git a/packages/solid/tests/__snapshots__/context.spec.tsx.snap b/packages/solid/tests/__snapshots__/context.spec.tsx.snap new file mode 100644 index 000000000..723407f5f --- /dev/null +++ b/packages/solid/tests/__snapshots__/context.spec.tsx.snap @@ -0,0 +1,26 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Using LucideProvider > should render the icon with LucideProvider 1`] = ` + +`; diff --git a/packages/solid/tests/__snapshots__/lucide-solid.spec.tsx.snap b/packages/solid/tests/__snapshots__/lucide-solid.spec.tsx.snap new file mode 100644 index 000000000..22743b33f --- /dev/null +++ b/packages/solid/tests/__snapshots__/lucide-solid.spec.tsx.snap @@ -0,0 +1,123 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Using lucide icon components > should adjust the size, stroke color and stroke width 1`] = ` + +`; + +exports[`Using lucide icon components > should render a component 1`] = ` + +`; + +exports[`Using lucide icon components > should scale the strokeWidth when absoluteStrokeWidth is set 1`] = ` + +`; diff --git a/packages/solid/tests/context.spec.tsx b/packages/solid/tests/context.spec.tsx new file mode 100644 index 000000000..74991a9c4 --- /dev/null +++ b/packages/solid/tests/context.spec.tsx @@ -0,0 +1,121 @@ +import { render } from '@solidjs/testing-library'; +import { createSignal, flush } from 'solid-js'; +import { describe, expect, it } from 'vitest'; +import { House, LucideProvider } from '../src/lucide-solid'; + +describe('Using LucideProvider', () => { + it('should render the icon with LucideProvider', () => { + const { container } = render(() => ( + + + + )); + + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should render the icon with default props when no provider is used', () => { + const { container } = render(() => ); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('width', '24'); + expect(IconComponent).toHaveAttribute('height', '24'); + expect(IconComponent).toHaveAttribute('stroke', 'currentColor'); + expect(IconComponent).toHaveAttribute('stroke-width', '2'); + }); + + it('should render the icon with LucideProvider and custom strokeWidth', () => { + const { container } = render(() => ( + + + + )); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('width', '48'); + expect(IconComponent).toHaveAttribute('height', '48'); + expect(IconComponent).toHaveAttribute('stroke', 'red'); + expect(IconComponent).toHaveAttribute('stroke-width', '4'); + }); + + it('should update when reactive LucideProvider props change', () => { + const [size, setSize] = createSignal(24); + const { container } = render(() => ( + + + + )); + + expect(container.firstElementChild).toHaveAttribute('width', '24'); + expect(container.firstElementChild).toHaveAttribute('height', '24'); + + setSize(48); + flush(); + + expect(container.firstElementChild).toHaveAttribute('width', '48'); + expect(container.firstElementChild).toHaveAttribute('height', '48'); + }); + + it('should render the icon with LucideProvider and custom absoluteStrokeWidth', () => { + const { container } = render(() => ( + + + + )); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('stroke-width', '1'); + }); + + it("should override the provider's global props when passing props to the icon", () => { + const { container } = render(() => ( + + + + )); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('width', '24'); + expect(IconComponent).toHaveAttribute('height', '24'); + expect(IconComponent).toHaveAttribute('stroke', 'blue'); + expect(IconComponent).toHaveAttribute('stroke-width', '2'); + }); + + it('should merge className from provider and icon', () => { + const { container } = render(() => ( + + + + )); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute( + 'class', + 'lucide lucide-house lucide-home lucide-icon provider-class icon-class', + ); + }); +}); diff --git a/packages/solid/tests/lucide-solid.spec.tsx b/packages/solid/tests/lucide-solid.spec.tsx new file mode 100644 index 000000000..1421dd034 --- /dev/null +++ b/packages/solid/tests/lucide-solid.spec.tsx @@ -0,0 +1,90 @@ +import { describe, it, expect } from 'vitest'; +import { render, cleanup } from '@solidjs/testing-library'; +import { Edit2, Grid, Pen, Droplet } from '../src/lucide-solid'; + +describe('Using lucide icon components', () => { + it('should render a component', () => { + const { container } = render(() => ); + + expect(container.innerHTML).toMatchSnapshot(); + }); + + it('should adjust the size, stroke color and stroke width', async () => { + const testId = 'grid-icon'; + const { container, getByTestId } = render(() => ( + + )); + + const { attributes } = (await getByTestId(testId)) as unknown as { + attributes: Record; + }; + expect(attributes.stroke.value).toBe('red'); + expect(attributes.width.value).toBe('48'); + expect(attributes.height.value).toBe('48'); + expect(attributes['stroke-width'].value).toBe('4'); + expect(container.innerHTML).toMatchSnapshot(); + }); + + it('should render the alias icon', () => { + const testId = 'pen-icon'; + const { container } = render(() => ( + + )); + + const PenIconRenderedHTML = container.innerHTML; + + cleanup(); + + const { container: Edit2Container } = render(() => ( + + )); + + expect(PenIconRenderedHTML).toBe(Edit2Container.innerHTML); + }); + + it('should scale the strokeWidth when absoluteStrokeWidth is set', () => { + const testId = 'grid-icon'; + const { container, getByTestId } = render(() => ( + + )); + + const { attributes } = getByTestId(testId) as unknown as { + attributes: Record; + }; + expect(attributes.stroke.value).toBe('red'); + expect(attributes.width.value).toBe('48'); + expect(attributes.height.value).toBe('48'); + expect(attributes['stroke-width'].value).toBe('1'); + + expect(container.innerHTML).toMatchSnapshot(); + }); + + it('should add all classes to the element', () => { + const testClass = 'my-class'; + const { container } = render(() => ); + + expect(container.firstChild).toHaveClass(testClass); + expect(container.firstChild).toHaveClass('lucide'); + expect(container.firstChild).toHaveClass('lucide-droplet'); + }); +}); diff --git a/packages/solid/tests/setupVitest.js b/packages/solid/tests/setupVitest.js new file mode 100644 index 000000000..eee9d60f6 --- /dev/null +++ b/packages/solid/tests/setupVitest.js @@ -0,0 +1,10 @@ +import { expect, afterEach } from 'vitest'; +import { cleanup } from '@solidjs/testing-library'; +import '@testing-library/jest-dom/vitest'; +import htmlSerializer from 'jest-serializer-html'; + +expect.addSnapshotSerializer(htmlSerializer); + +afterEach(() => { + cleanup(); +}); diff --git a/packages/solid/tests/testIconNodes.ts b/packages/solid/tests/testIconNodes.ts new file mode 100644 index 000000000..c93721961 --- /dev/null +++ b/packages/solid/tests/testIconNodes.ts @@ -0,0 +1,22 @@ +import { IconNode } from '../src/types'; + +export const airVent: IconNode = [ + [ + 'path', + { + d: 'M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2', + key: 'larmp2', + }, + ], + ['path', { d: 'M6 8h12', key: '6g4wlu' }], + ['path', { d: 'M18.3 17.7a2.5 2.5 0 0 1-3.16 3.83 2.53 2.53 0 0 1-1.14-2V12', key: '1bo8pg' }], + ['path', { d: 'M6.6 15.6A2 2 0 1 0 10 17v-5', key: 't9h90c' }], +]; + +export const coffee: IconNode = [ + ['path', { d: 'M17 8h1a4 4 0 1 1 0 8h-1', key: 'jx4kbh' }], + ['path', { d: 'M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z', key: '1bxrl0' }], + ['line', { x1: '6', x2: '6', y1: '2', y2: '4', key: '1cr9l3' }], + ['line', { x1: '10', x2: '10', y1: '2', y2: '4', key: '170wym' }], + ['line', { x1: '14', x2: '14', y1: '2', y2: '4', key: '1c5f70' }], +]; diff --git a/packages/solid/tsconfig.json b/packages/solid/tsconfig.json new file mode 100644 index 000000000..4b177df3b --- /dev/null +++ b/packages/solid/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "paths": { + "@lucide/shared": ["../shared/src/index.ts"], + "@lucide/shared/types": ["../shared/src/build/types.ts"] + }, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "types": ["vite/client", "@testing-library/jest-dom/vitest"], + "noEmit": true, + "isolatedModules": true, + "outDir": "./dist", + "declarationDir": "./dist", + "declaration": true + } +} diff --git a/packages/solid/vitest.config.mts b/packages/solid/vitest.config.mts new file mode 100644 index 000000000..bc9e80315 --- /dev/null +++ b/packages/solid/vitest.config.mts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config' +import solidPlugin from '@solidjs/vite-plugin'; + +export default defineConfig({ + plugins: [solidPlugin()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: './tests/setupVitest.js', + }, +}); diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 352b254b8..987de0e91 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -30,6 +30,11 @@ "svelte": "./dist/lucide-svelte.js", "default": "./dist/lucide-svelte.js" }, + "./context": { + "types": "./dist/context.d.ts", + "svelte": "./dist/context.js", + "default": "./dist/context.js" + }, "./icons": { "types": "./dist/lucide-svelte.d.ts", "svelte": "./dist/lucide-svelte.js" @@ -48,7 +53,7 @@ "scripts": { "build": "pnpm clean && pnpm copy:license && pnpm copy:utils && pnpm build:icons && pnpm build:package && pnpm build:license", "copy:license": "cp ../../LICENSE ./LICENSE", - "copy:utils": "mkdir -p ./src/utils && cp -rf ../../packages/shared/src/utils/hasA11yProp.ts ./src/utils/", + "copy:utils": "cpy ../../packages/shared/src/build/{buildLucideIconNode.ts,defaultAttributes.ts,types.ts} ../../packages/shared/src/utils/{hasA11yProp.ts,mergeClasses.ts} ./src/utils/ && node ./scripts/patchCopiedUtils.mts", "clean": "rm -rf dist stats ./src/icons/*.{ts,svelte} ./src/aliases/{aliases,prefixed,suffixed}.ts", "build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --exportFileName=index.ts --iconFileExtension=.svelte --importImportFileExtension=.svelte --separateIconFileExport --separateIconFileExportExtension=.ts --withAliases --aliasesFileExtension=.ts --separateAliasesFile --separateAliasesFileExtension=.ts --aliasImportFileExtension=.js --pretty=false", "build:package": "svelte-package --input ./src", @@ -61,17 +66,17 @@ "@lucide/build-icons": "workspace:*", "@lucide/helpers": "workspace:*", "@sveltejs/package": "^2.5.8", - "@sveltejs/vite-plugin-svelte": "^7.3.0", - "@testing-library/jest-dom": "^6.9.1", + "@sveltejs/vite-plugin-svelte": "^7.3.1", + "@testing-library/jest-dom": "^7.0.1", "@testing-library/svelte": "^5.4.2", "@tsconfig/svelte": "^5.0.8", "jest-serializer-html": "^7.1.0", - "jsdom": "^27.4.0", - "svelte": "^5.56.9", + "jsdom": "^30.0.1", + "svelte": "^5.57.1", "svelte-check": "^4.7.6", "svelte-preprocess": "^6.0.5", - "typescript": "^5.9.3", - "vite": "^7.3.6" + "typescript": "^6.0.3", + "vite": "^8.3.2" }, "peerDependencies": { "svelte": "^5" diff --git a/packages/svelte/scripts/exportTemplate.mts b/packages/svelte/scripts/exportTemplate.mts index a01d30286..ef046a584 100644 --- a/packages/svelte/scripts/exportTemplate.mts +++ b/packages/svelte/scripts/exportTemplate.mts @@ -3,7 +3,7 @@ import { getHTMLBanner } from './license.mts'; import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate'; export default defineExportTemplate( - async ({ iconName, children, getSvg, deprecated, deprecationReason }) => { + async ({ iconName, iconData, getSvg, deprecated, deprecationReason }) => { const svgContents = await getSvg(); const svgBase64 = base64SVG(svgContents); @@ -11,11 +11,12 @@ export default defineExportTemplate( ${getHTMLBanner()} - + `; }, ); diff --git a/packages/svelte/scripts/patchCopiedUtils.mts b/packages/svelte/scripts/patchCopiedUtils.mts new file mode 100644 index 000000000..34e2250c4 --- /dev/null +++ b/packages/svelte/scripts/patchCopiedUtils.mts @@ -0,0 +1,14 @@ +import { readFile, writeFile } from 'node:fs/promises'; + +const filePath = new URL('../src/utils/buildLucideIconNode.ts', import.meta.url); + +const source = await readFile(filePath, 'utf8'); + +const patched = source + .replace("from './types';", "from './types.js';") + .replace("from './defaultAttributes';", "from './defaultAttributes.js';") + .replace("from '../utils/mergeClasses';", "from '../utils/mergeClasses.js';"); + +if (patched !== source) { + await writeFile(filePath, patched); +} diff --git a/packages/svelte/src/Icon.svelte b/packages/svelte/src/Icon.svelte index cb05e43dc..724ec0c50 100644 --- a/packages/svelte/src/Icon.svelte +++ b/packages/svelte/src/Icon.svelte @@ -1,38 +1,61 @@ - - {#each iconNode as [tag, attrs]} + + {#each builtIconNode as [tag, attrs]} ; +export type Attrs = Record & SVGAttributes; type IconNodeElements = - | 'circle' - | 'ellipse' - | 'g' - | 'line' - | 'path' - | 'polygon' - | 'polyline' - | 'rect'; + 'circle' | 'ellipse' | 'g' | 'line' | 'path' | 'polygon' | 'polyline' | 'rect'; -export type IconNode = [elementName: IconNodeElements, attrs: Attrs][]; +export type LucideIconNode = SharedLucideIconNode; -export interface LucideProps extends Attrs { - name?: string; +export type LucideIconData = SharedLucideIconData; + +/** + * @deprecated Use LucideIconNode instead. + */ +export type IconNode = LucideIconNode[]; + +export type LucideProps = Attrs & { color?: string; size?: number | string; strokeWidth?: number | string; + /** + * @deprecated Use `nonScalingStroke` instead. + */ absoluteStrokeWidth?: boolean; - iconNode?: IconNode; + nonScalingStroke?: boolean; children?: Snippet; title?: string; -} +}; -export type IconProps = LucideProps; +export type IconProps = LucideProps & + ( + | { + icon: LucideIconData; + iconNode?: never; + } + | { + icon?: never; + iconNode: LucideIconNode[]; + } + ); export type LucideIcon = Component; diff --git a/packages/svelte/tests/__snapshots__/Icon.spec.ts.snap b/packages/svelte/tests/__snapshots__/Icon.spec.ts.snap index 659763fc7..a22c113f1 100644 --- a/packages/svelte/tests/__snapshots__/Icon.spec.ts.snap +++ b/packages/svelte/tests/__snapshots__/Icon.spec.ts.snap @@ -3,7 +3,7 @@ exports[`Using Icon Component > should render icon and match snapshot 1`] = `
`; -exports[`Using lucide icon components > should not scale the strokeWidth when absoluteStrokeWidth is set 1`] = ` +exports[`Using lucide icon components > should apply vector-effect when nonScalingStroke is set 1`] = `
should render an icon slot 1`] = `
`; + +exports[`Using lucide icon components > should scale the strokeWidth when absoluteStrokeWidth is set 1`] = ` + +`; diff --git a/packages/svelte/tests/lucide-svelte.spec.ts b/packages/svelte/tests/lucide-svelte.spec.ts index da19e096b..4713a05bf 100644 --- a/packages/svelte/tests/lucide-svelte.spec.ts +++ b/packages/svelte/tests/lucide-svelte.spec.ts @@ -66,7 +66,7 @@ describe('Using lucide icon components', () => { expect(PenIconRenderedHTML).toBe(Edit2Container.innerHTML); }); - it('should not scale the strokeWidth when absoluteStrokeWidth is set', () => { + it('should scale the strokeWidth when absoluteStrokeWidth is set', () => { const { container } = render(Smile, { color: 'red', size: 48, @@ -83,6 +83,24 @@ describe('Using lucide icon components', () => { expect(container.innerHTML).toMatchSnapshot(); }); + it('should apply vector-effect when nonScalingStroke is set', () => { + const { container } = render(Smile, { + color: 'red', + size: 48, + nonScalingStroke: true, + }); + + const IconComponent = container.firstElementChild; + + expect(IconComponent).toHaveAttribute('width', '48'); + expect(IconComponent).toHaveAttribute('height', '48'); + expect(IconComponent).toHaveAttribute('stroke', 'red'); + expect(IconComponent).toHaveAttribute('stroke-width', '2'); + expect(IconComponent?.firstElementChild).toHaveAttribute('vector-effect', 'non-scaling-stroke'); + + expect(container.innerHTML).toMatchSnapshot(); + }); + it('should use context values from the global set properties', () => { const { container } = render(ContextWrapper); diff --git a/packages/svelte/tests/setupVitest.ts b/packages/svelte/tests/setupVitest.ts index 03a0244af..01bf442b9 100644 --- a/packages/svelte/tests/setupVitest.ts +++ b/packages/svelte/tests/setupVitest.ts @@ -1,5 +1,6 @@ import { expect } from 'vitest'; -import '@testing-library/jest-dom/vitest'; +import * as matchers from '@testing-library/jest-dom/matchers'; import htmlSerializer from 'jest-serializer-html'; +expect.extend(matchers); expect.addSnapshotSerializer(htmlSerializer); diff --git a/packages/svelte/tsconfig.json b/packages/svelte/tsconfig.json index 3ef7dda35..1a10edad5 100644 --- a/packages/svelte/tsconfig.json +++ b/packages/svelte/tsconfig.json @@ -6,11 +6,15 @@ "module": "NodeNext", "moduleResolution": "NodeNext", "resolveJsonModule": true, + "paths": { + "@lucide/shared": ["../shared/src/index.ts"], + "@lucide/shared/types": ["../shared/src/build/types.ts"] + }, "allowJs": true, "checkJs": true, "isolatedModules": true, - "types": ["@testing-library/jest-dom"], + "types": ["@testing-library/jest-dom/vitest"] }, "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte", "tests/**/*.ts"], - "exclude": ["tests/setupVitest.ts"], + "exclude": ["tests/setupVitest.ts"] } diff --git a/packages/vue/package.json b/packages/vue/package.json index dd513b015..d4d39cee2 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -35,7 +35,7 @@ "clean": "rm -rf dist && rm -rf ./src/icons/*.ts", "build:icons": "build-icons --output=./src --templateSrc=./scripts/exportTemplate.mts --renderUniqueKey --withAliases --aliasesFileExtension=.ts --iconFileExtension=.ts --exportFileName=index.ts", "build:bundles": "rollup -c ./rollup.config.mjs", - "typecheck": "tsc", + "typecheck": "pnpm build:icons && tsc", "typecheck:watch": "tsc -w", "test": "pnpm build:icons && vitest run", "test:watch": "vitest watch" @@ -44,15 +44,15 @@ "@lucide/build-icons": "workspace:*", "@lucide/rollup-plugins": "workspace:*", "@lucide/shared": "workspace:*", - "@testing-library/jest-dom": "^6.9.1", + "@testing-library/jest-dom": "^7.0.1", "@testing-library/vue": "^8.1.0", - "@vitejs/plugin-vue": "^6.0.8", - "@vue/test-utils": "2.4.11", - "rollup": "^4.62.2", - "rollup-plugin-dts": "^6.4.1", - "typescript": "^5.9.3", - "vite": "^7.3.6", - "vue": "^3.5.41" + "@vitejs/plugin-vue": "^6.0.9", + "@vue/test-utils": "2.5.1", + "rollup": "^4.63.1", + "rollup-plugin-dts": "^6.5.1", + "typescript": "^6.0.3", + "vite": "^8.3.2", + "vue": "^3.5.43" }, "peerDependencies": { "vue": ">=3.0.1" diff --git a/packages/vue/rollup.config.mjs b/packages/vue/rollup.config.mjs index 94bba42f8..b69ab424a 100644 --- a/packages/vue/rollup.config.mjs +++ b/packages/vue/rollup.config.mjs @@ -2,6 +2,11 @@ import plugins from '@lucide/rollup-plugins'; import pkg from './package.json' with { type: 'json' }; import dts from 'rollup-plugin-dts'; +const dtsOptions = { + includeExternal: ['@lucide/shared/types'], + tsconfig: './tsconfig.json', +}; + const packageName = '@lucide/vue'; const outputFileName = 'lucide-vue'; const outputDir = 'dist'; @@ -58,13 +63,7 @@ export default [ format: 'es', }, ], - plugins: [ - dts({ - compilerOptions: { - preserveSymlinks: false, - }, - }), - ], + plugins: [dts(dtsOptions)], }, { input: `src/${outputFileName}.suffixed.ts`, @@ -74,13 +73,7 @@ export default [ format: 'es', }, ], - plugins: [ - dts({ - compilerOptions: { - preserveSymlinks: false, - }, - }), - ], + plugins: [dts(dtsOptions)], }, { input: `src/${outputFileName}.prefixed.ts`, @@ -90,13 +83,7 @@ export default [ format: 'es', }, ], - plugins: [ - dts({ - compilerOptions: { - preserveSymlinks: false, - }, - }), - ], + plugins: [dts(dtsOptions)], }, ...configs, ]; diff --git a/packages/vue/scripts/exportTemplate.mts b/packages/vue/scripts/exportTemplate.mts index d42df2a61..ea83a0235 100644 --- a/packages/vue/scripts/exportTemplate.mts +++ b/packages/vue/scripts/exportTemplate.mts @@ -1,22 +1,16 @@ import base64SVG from '@lucide/build-icons/utils/base64SVG'; import defineExportTemplate from '@lucide/build-icons/utils/defineExportTemplate'; -export default defineExportTemplate(async({ - componentName, - iconName, - children, - getSvg, - deprecated, - deprecationReason, -}) => { - const svgContents = await getSvg(); - const svgBase64 = base64SVG(svgContents); +export default defineExportTemplate( + async ({ componentName, iconName, iconData, getSvg, deprecated, deprecationReason }) => { + const svgContents = await getSvg(); + const svgBase64 = base64SVG(svgContents); - return ` + return ` import createLucideIcon from '../createLucideIcon'; -import { IconNode } from '../types'; +import type { LucideIcon, LucideIconData } from '../types'; -export const __iconNode: IconNode = ${JSON.stringify(children)} +export const __iconData: LucideIconData = ${JSON.stringify(iconData)} /** * @component @name ${componentName} @@ -29,8 +23,9 @@ export const __iconNode: IconNode = ${JSON.stringify(children)} * @returns {FunctionalComponent} Vue component * ${deprecated ? `@deprecated ${deprecationReason}` : ''} */ -const ${componentName} = createLucideIcon('${iconName}', __iconNode); +const ${componentName}: LucideIcon = createLucideIcon(__iconData); export default ${componentName}; `; -}); + }, +); diff --git a/packages/vue/src/Icon.ts b/packages/vue/src/Icon.ts index 8d4254460..f691755e7 100644 --- a/packages/vue/src/Icon.ts +++ b/packages/vue/src/Icon.ts @@ -1,24 +1,47 @@ -import { computed, type FunctionalComponent, h } from 'vue'; -import { isEmptyString, mergeClasses, toKebabCase, toPascalCase } from '@lucide/shared'; -import defaultAttributes from './defaultAttributes'; -import { IconNode, LucideProps } from './types'; +import { type FunctionalComponent, h } from 'vue'; +import { buildLucideIconNode, hasA11yProp, isEmptyString, toKebabCase } from '@lucide/shared'; +import { LucideIconNode, LucideIconData, LucideProps } from './types'; import { useLucideProps } from './context'; -type IconProps = { name: string } & ( - | { iconNode: IconNode; 'icon-node'?: never } - | { 'icon-node': IconNode; iconNode?: never } -); +type IconProps = + | { + icon: LucideIconData; + iconNode?: never; + 'icon-node'?: never; + } + | { + icon?: never; + iconNode: LucideIconNode[]; + 'icon-node'?: never; + name: string; + } + | { + icon?: never; + iconNode?: never; + 'icon-node': LucideIconNode[]; + name: string; + }; const Icon: FunctionalComponent = ( { name, iconNode, 'icon-node': iconNodeKebabCase, + icon = { + name: name && toKebabCase(name), + node: iconNode ?? iconNodeKebabCase ?? [], + size: 24, + aliases: [], + }, absoluteStrokeWidth, 'absolute-stroke-width': absoluteStrokeWidthKebabCase, + nonScalingStroke, + 'non-scaling-stroke': nonScalingStrokeKebabCase, strokeWidth, 'stroke-width': strokeWidthKebabCase, size, + width = size, + height = size, color, ...props }, @@ -29,54 +52,44 @@ const Icon: FunctionalComponent = ( color: contextColor, strokeWidth: contextStrokeWidth = 2, absoluteStrokeWidth: contextAbsoluteStrokeWidth = false, + nonScalingStroke: contextNonScalingStroke = false, class: contextClass = '', } = useLucideProps(); - const calculatedStrokeWidth = computed(() => { - const isAbsoluteStrokeWidth = - isEmptyString(absoluteStrokeWidth) || - isEmptyString(absoluteStrokeWidthKebabCase) || - absoluteStrokeWidth === true || - absoluteStrokeWidthKebabCase === true || - contextAbsoluteStrokeWidth === true; + const isAbsoluteStrokeWidth = + isEmptyString(absoluteStrokeWidth) || + isEmptyString(absoluteStrokeWidthKebabCase) || + absoluteStrokeWidth === true || + absoluteStrokeWidthKebabCase === true || + contextAbsoluteStrokeWidth === true; - const strokeWidthValue = - strokeWidth || - strokeWidthKebabCase || - contextStrokeWidth || - defaultAttributes['stroke-width']; + const isNonScalingStroke = + isEmptyString(nonScalingStroke) || + isEmptyString(nonScalingStrokeKebabCase) || + nonScalingStroke === true || + nonScalingStrokeKebabCase === true || + contextNonScalingStroke === true; - if (isAbsoluteStrokeWidth) { - return ( - (Number(strokeWidthValue) * 24) / Number(size ?? contextSize ?? defaultAttributes.width) - ); - } + delete props.class; - return strokeWidthValue; + const defaultSlot = slots.default?.(); + + const [, svgAttributes, builtIconNode = []] = buildLucideIconNode(icon, { + color: color ?? contextColor, + width: width ?? size ?? contextSize, + height: height ?? size ?? contextSize, + strokeWidth: strokeWidth ?? strokeWidthKebabCase ?? contextStrokeWidth, + absoluteStrokeWidth: isAbsoluteStrokeWidth, + nonScalingStroke: isNonScalingStroke, + className: contextClass, + hasA11yProp: (defaultSlot != null && defaultSlot.length > 0) || hasA11yProp(props), + attributes: props, }); - return h( - 'svg', - { - ...defaultAttributes, - ...props, - width: size ?? contextSize ?? defaultAttributes.width, - height: size ?? contextSize ?? defaultAttributes.height, - stroke: color ?? contextColor ?? defaultAttributes.stroke, - 'stroke-width': calculatedStrokeWidth.value, - class: mergeClasses( - 'lucide', - contextClass, - ...(name - ? [`lucide-${toKebabCase(toPascalCase(name))}-icon`, `lucide-${toKebabCase(name)}`] - : ['lucide-icon']), - ), - }, - [ - ...(iconNode ?? iconNodeKebabCase ?? []).map((child) => h(...child)), - ...(slots.default ? [slots.default()] : []), - ], - ); + return h('svg', svgAttributes, [ + ...builtIconNode.map(([tag, attrs]) => h(tag, attrs)), + ...(defaultSlot ?? []), + ]); }; export default Icon; diff --git a/packages/vue/src/context.ts b/packages/vue/src/context.ts index 7af325ec7..0435ed55c 100644 --- a/packages/vue/src/context.ts +++ b/packages/vue/src/context.ts @@ -6,7 +6,11 @@ interface LucideIconsContext { size?: number; color?: string; strokeWidth?: number; + /** + * @deprecated Use `nonScalingStroke` instead. + */ absoluteStrokeWidth?: boolean; + nonScalingStroke?: boolean; class?: string; } diff --git a/packages/vue/src/createLucideIcon.ts b/packages/vue/src/createLucideIcon.ts index 85919b106..5981b8153 100644 --- a/packages/vue/src/createLucideIcon.ts +++ b/packages/vue/src/createLucideIcon.ts @@ -1,25 +1,44 @@ import { h } from 'vue'; -import { IconNode, LucideIcon } from './types'; +import { LucideIcon, LucideIconData, LucideIconNode } from './types'; import Icon from './Icon'; /** - * Create a Lucide icon component - * @param {string} iconName - * @param {array} iconNode - * @returns {FunctionalComponent} LucideIcon + * Creates a Lucide icon component from icon data. + * + * @param icon Icon data object. + * @returns Lucide icon component. */ -const createLucideIcon = - (iconName: string, iconNode: IconNode): LucideIcon => - (props, { slots, attrs }) => +function createLucideIcon(icon: LucideIconData): LucideIcon; + +/** + * Creates a Lucide icon component from the legacy icon arguments. + * + * @param iconName Icon name. + * @param iconNode Icon node. + * @returns Lucide icon component. + */ +function createLucideIcon(iconName: string, iconNode: LucideIconNode[]): LucideIcon; + +function createLucideIcon( + iconDataOrName: LucideIconData | string, + iconNode: LucideIconNode[] = [], +): LucideIcon { + const icon: LucideIconData = + typeof iconDataOrName === 'string' + ? { + name: iconDataOrName, + node: iconNode, + } + : iconDataOrName; + return (props, { slots }) => h( Icon, { - ...attrs, ...props, - iconNode, - name: iconName, + icon, }, slots.default ? { default: slots.default } : undefined, ); +} export default createLucideIcon; diff --git a/packages/vue/src/defaultAttributes.ts b/packages/vue/src/defaultAttributes.ts index 8ae7d46ea..cc4036fbd 100644 --- a/packages/vue/src/defaultAttributes.ts +++ b/packages/vue/src/defaultAttributes.ts @@ -1,11 +1 @@ -export default { - 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', -}; +export { defaultAttributes as default } from '@lucide/shared'; diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts index a8b865db6..f292be26a 100644 --- a/packages/vue/src/types.ts +++ b/packages/vue/src/types.ts @@ -1,11 +1,31 @@ import type { FunctionalComponent, SVGAttributes } from 'vue'; +import type { + LucideIconData as SharedLucideIconData, + LucideIconNode as SharedLucideIconNode, +} from '@lucide/shared/types'; export interface LucideProps extends Partial { size?: 24 | number; strokeWidth?: number | string; + /** + * @deprecated Use `nonScalingStroke` instead. + */ absoluteStrokeWidth?: boolean; + /** + * @deprecated Use `nonScalingStroke` instead. + */ 'absolute-stroke-width'?: boolean; + nonScalingStroke?: boolean; + 'non-scaling-stroke'?: boolean; } -export type IconNode = [elementName: string, attrs: Record][]; +export type LucideIconNode = SharedLucideIconNode; + +export type LucideIconData = SharedLucideIconData; + +/** + * @deprecated Use LucideIconNode instead. + */ +export type IconNode = LucideIconNode[]; + export type LucideIcon = FunctionalComponent; diff --git a/packages/vue/tests/Icon.spec.ts b/packages/vue/tests/Icon.spec.ts index d8ec174a8..134e34c8c 100644 --- a/packages/vue/tests/Icon.spec.ts +++ b/packages/vue/tests/Icon.spec.ts @@ -1,11 +1,14 @@ import { describe, it, expect } from 'vitest'; import { render } from '@testing-library/vue'; +import { defineComponent } from 'vue'; import { airVent } from './testIconNodes'; import { Icon } from '../src/lucide-vue'; describe('Using Icon Component', () => { - it('should render icon based on a iconNode', async () => { + const airVentIcon = { name: 'air-vent', node: airVent, size: 24 }; + + it('should render icon based on an icon node', async () => { const { container } = render(Icon, { props: { iconNode: airVent, @@ -16,7 +19,7 @@ describe('Using Icon Component', () => { }, }); - expect(container.firstChild).toBeDefined(); + expect(container.firstChild).toMatchSnapshot(); }); it('should render when iconNode is passed as the kebab-case icon-node prop', async () => { @@ -34,8 +37,7 @@ describe('Using Icon Component', () => { it('should render icon and match snapshot', async () => { const { container } = render(Icon, { props: { - iconNode: airVent, - name: 'AirVent', + icon: airVentIcon, size: 48, color: 'red', absoluteStrokeWidth: true, @@ -44,4 +46,27 @@ describe('Using Icon Component', () => { expect(container.firstChild).toMatchSnapshot(); }); + + it('should update when reactive props change', async () => { + const wrapper = defineComponent({ + components: { Icon }, + props: { + size: Number, + }, + setup() { + return { airVentIcon }; + }, + template: '', + }); + + const { container, rerender } = render(wrapper, { props: { size: 24 } }); + + expect(container.firstChild).toHaveAttribute('width', '24'); + expect(container.firstChild).toHaveAttribute('height', '24'); + + await rerender({ size: 48 }); + + expect(container.firstChild).toHaveAttribute('width', '48'); + expect(container.firstChild).toHaveAttribute('height', '48'); + }); }); diff --git a/packages/vue/tests/__snapshots__/Icon.spec.ts.snap b/packages/vue/tests/__snapshots__/Icon.spec.ts.snap index 4b742c143..9257657d8 100644 --- a/packages/vue/tests/__snapshots__/Icon.spec.ts.snap +++ b/packages/vue/tests/__snapshots__/Icon.spec.ts.snap @@ -2,7 +2,37 @@ exports[`Using Icon Component > should render icon and match snapshot 1`] = ` +`; + +exports[`Using Icon Component > should render icon based on an icon node 1`] = ` +
should pass children to the icon slot 1` cy="12" r="10" /> - Hello World -
`; @@ -137,7 +138,8 @@ exports[`Using lucide icon components > should pass children to the icon slot 1` exports[`Using lucide icon components > should render an component 1`] = `