Repository: ionic-team/ionicons Branch: main Commit: 2a8e43aff06a Files: 62 Total size: 287.4 KB Directory structure: gitextract_m0sxkfw_/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── feature_request.yml │ │ ├── icon_request.yml │ │ └── incorrect_icon.yml │ ├── ionic-issue-bot.yml │ └── workflows/ │ ├── actions/ │ │ ├── build-core/ │ │ │ └── action.yml │ │ ├── download-archive/ │ │ │ └── action.yml │ │ ├── publish-npm/ │ │ │ └── action.yml │ │ ├── test-e2e/ │ │ │ └── action.yml │ │ ├── test-spec/ │ │ │ └── action.yml │ │ ├── update-reference-screenshots/ │ │ │ └── action.yml │ │ └── upload-archive/ │ │ └── action.yml │ ├── dev-release.yml │ ├── production-release.yml │ ├── release-orchestrator.yml │ ├── update-screenshots.yml │ └── validation.yml ├── .gitignore ├── .prettierrc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── jest.config.mjs ├── package.json ├── playwright.config.ts ├── readme.md ├── scripts/ │ ├── build.ts │ ├── cheatsheet-template.html │ ├── collection-copy.ts │ ├── constants.ts │ ├── plugins.ts │ ├── readme.md │ └── types.ts ├── src/ │ ├── components/ │ │ ├── icon/ │ │ │ ├── icon.css │ │ │ ├── icon.tsx │ │ │ ├── readme.md │ │ │ ├── request.ts │ │ │ ├── svg/ │ │ │ │ └── .gitignore │ │ │ ├── test/ │ │ │ │ ├── icon.e2e.ts │ │ │ │ ├── icon.spec.ts │ │ │ │ ├── utils.spec.ts │ │ │ │ └── validate.spec.ts │ │ │ ├── utils.ts │ │ │ └── validate.ts │ │ └── test/ │ │ ├── csp/ │ │ │ ├── icon.e2e.ts │ │ │ └── index.html │ │ └── dynamic-type/ │ │ ├── icon.e2e.ts │ │ └── index.html │ ├── components.d.ts │ ├── data.json │ ├── index.html │ ├── index.ts │ ├── ionicons.web-types.json │ └── utils/ │ └── test/ │ └── playwright/ │ ├── index.ts │ ├── page/ │ │ └── utils/ │ │ ├── goto.ts │ │ └── index.ts │ ├── playwright-declarations.ts │ └── playwright-page.ts ├── stencil.config.ts ├── tsconfig.json └── tsconfig.spec.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: 🐛 Bug Report description: Report a bug in the ion-icon component. Incorrect icons should use the "Incorrect Icon" template instead. labels: ['triage'] title: 'bug: ' body: - type: textarea attributes: label: Current Behavior description: A clear description of what the bug is and how it manifests. validations: required: true - type: textarea attributes: label: Expected Behavior description: A clear description of what you expected to happen. validations: required: true - type: textarea attributes: label: Steps to Reproduce description: Please explain the steps required to duplicate this issue. validations: required: true - type: input attributes: label: Code Reproduction URL description: Please provide an application with the minimum code required to reproduce the issue. This is the best way to ensure this issue is triaged quickly. Issues without a code reproduction may be closed if the Ionic Team cannot reproduce the issue you are reporting. placeholder: https://github.com/... - type: textarea attributes: label: Additional Information description: List any other information that is relevant to your issue. Stack traces, related issues, suggestions on how to fix, Stack Overflow links, forum links, etc. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: 💡 Feature Request description: Suggest a new feature for the ion-icon component. Icon requests should use the "Icon Request" template instead. labels: ['triage'] title: 'feat: ' body: - type: textarea attributes: label: Describe Problem description: A clear and concise description of what the problem is. Ex. I am always frustrated when [...] validations: required: true - type: textarea attributes: label: Describe Preferred Solution description: A clear and concise description of what you want to happen. - type: textarea attributes: label: Describe Alternatives description: A clear and concise description of any alternative solutions or features you have considered. - type: textarea attributes: label: Additional Information description: Add any other context or screenshots about the feature request here. ================================================ FILE: .github/ISSUE_TEMPLATE/icon_request.yml ================================================ name: 🚀 Icon Request description: Request a new icon labels: ['triage'] title: 'icon request: ' body: - type: textarea attributes: label: Describe the Icon description: A clear and concise description of the icon you would like added. validations: required: true - type: textarea attributes: label: Links to Examples description: Add links to any examples of the icon in other libraries that you like. - type: textarea attributes: label: Additional Information description: Add any other context about the icon request here. ================================================ FILE: .github/ISSUE_TEMPLATE/incorrect_icon.yml ================================================ name: 😒 Incorrect Icon description: Report an incorrect icon labels: ['triage'] title: 'incorrect icon: ' body: - type: textarea attributes: label: Describe the Issue description: A clear description of what is wrong with the icon. Please include any relevant screenshots. validations: required: true - type: textarea attributes: label: Expected Behavior description: A clear description of what the icon should look like. Please include any relevant screenshots. validations: required: true ================================================ FILE: .github/ionic-issue-bot.yml ================================================ triage: label: triage dryRun: false closeAndLock: labels: - label: 'ionitron: support' message: > Thanks for the issue! This issue appears to be a support request. We use this issue tracker exclusively for bug reports and feature requests. Please use our [forum](https://forum.ionicframework.com/) for help or questions about Ionicons. Thank you for using Ionicons! - label: 'ionitron: missing template' message: > Thanks for the issue! It appears that you have not filled out the provided issue template. We use this issue template in order to gather more information and further assist you. Please create a new issue and ensure the template is fully filled out. Thank you for using Ionicons! close: true lock: true dryRun: false comment: labels: - label: 'help wanted' message: > This issue has been labeled as `help wanted`. This label is added to issues that we believe would be good for contributors. If you'd like to work on this issue, please comment here letting us know that you would like to submit a pull request for it. This helps us to keep track of the pull request and make sure there isn't duplicated effort. For a guide on how to create a pull request and test this project locally to see your changes, see our [contributing documentation](https://github.com/ionic-team/ionicons/blob/main/.github/CONTRIBUTING.md#creating-a-pull-request). Thank you! - label: 'ionitron: needs reproduction' message: > Thanks for the issue! This issue has been labeled as `needs reproduction`. This label is added to issues that need a code reproduction. Please reproduce this issue and provide a way for us to access it (GitHub repo, StackBlitz, etc). Without a reliable code reproduction, it is unlikely we will be able to resolve the issue, leading to it being closed. If you have already provided a code snippet and are seeing this message, it is likely that the code snippet was not enough for our team to reproduce the issue. - label: 'community feedback wanted' message: > This issue has been labeled as `community feedback wanted`. This label is added to issues that we would like to hear from the community on before moving forward with any final decision on the feature request. If the requested feature is something you would find useful for your applications, please react to the original post with 👍 (`+1`). If you would like to provide an additional use case for the feature, please post a comment. The team will review this feedback and make a final decision. Any decision will be posted on this thread, but please note that we may ultimately decide not to pursue this feature. Thank you! dryRun: false noReply: days: 14 maxIssuesPerRun: 100 label: 'needs: reply' responseLabel: triage exemptProjects: true exemptMilestones: true message: > Thanks for the issue! This issue is being closed due to the lack of a reply. If this is still an issue with the latest version of Ionicons, please create a new issue and ensure the template is fully filled out. Thank you for using Ionicons! close: true lock: true dryRun: false noReproduction: days: 14 maxIssuesPerRun: 100 label: 'ionitron: needs reproduction' responseLabel: triage exemptProjects: true exemptMilestones: true message: > Thanks for the issue! This issue is being closed due to the lack of a code reproduction. If this is still an issue with the latest version of Ionicons, please create a new issue and ensure the template is fully filled out. Thank you for using Ionicons! close: true lock: true dryRun: false ================================================ FILE: .github/workflows/actions/build-core/action.yml ================================================ name: 'Build Ionicons' description: 'Build Ionicons' runs: using: 'composite' steps: - uses: actions/checkout@v4 with: # Checkout the latest commit in this branch ref: ${{ github.event.pull_request.head.sha }} - uses: actions/setup-node@v4 - name: Cache Node Modules uses: actions/cache@v4 env: cache-name: node-modules with: path: ./node_modules key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./package-lock.json') }}-v1 - name: Install Dependencies run: npm install shell: bash - name: Build run: npm run build shell: bash - uses: ./.github/workflows/actions/upload-archive with: name: ionicons-build output: IoniconsBuild.zip paths: dist components icons www - uses: ./.github/workflows/actions/upload-archive with: name: ionicons-src output: IoniconsSrc.zip paths: src ================================================ FILE: .github/workflows/actions/download-archive/action.yml ================================================ name: 'Archive Download' description: 'Downloads and decompresses an archive from a previous job' inputs: path: description: 'Input archive name' filename: description: 'Input file name' name: description: 'Archive name' runs: using: 'composite' steps: - uses: actions/download-artifact@v4 with: name: ${{ inputs.name }} path: ${{ inputs.path }} - name: Exract Archive run: unzip -q -o ${{ inputs.path }}/${{ inputs.filename }} shell: bash ================================================ FILE: .github/workflows/actions/publish-npm/action.yml ================================================ name: 'Release' description: 'Releases a package' inputs: version: description: 'The type of version to release.' tag: description: 'The tag to publish to on NPM.' working-directory: description: 'The directory of the package.' folder: default: './' description: 'A folder containing a package.json file.' createRelease: description: 'Create a release on GitHub.' default: 'false' ghToken: description: 'The GitHub authentication token required to create a release.' runs: using: 'composite' steps: - uses: actions/setup-node@v4 with: node-version: 22.x - name: Install latest npm run: npm install -g npm@latest shell: bash - name: Install Dependencies run: npm ci shell: bash working-directory: ${{ inputs.working-directory }} - name: Set Git User run: | git config user.name ionitron git config user.email hi@ionicframework.com shell: bash - name: Update Version id: update_version run: | npm version ${{ inputs.version }} echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV shell: bash working-directory: ${{ inputs.working-directory }} - name: Run Build run: npm run build shell: bash working-directory: ${{ inputs.working-directory }} - name: Publish to NPM run: npm publish ${{ inputs.folder }} --tag ${{ inputs.tag }} --provenance shell: bash working-directory: ${{ inputs.working-directory }} - name: Create Release if: ${{ inputs.createRelease == 'true' }} run: | git remote set-url origin https://${GH_TOKEN}:x-oauth-basic@github.com/ionic-team/ionicons.git git remote -v git push origin main --tags gh release create "v$VERSION" --title "v$VERSION" --generate-notes shell: bash env: GH_TOKEN: ${{ inputs.ghToken }} VERSION: ${{ env.VERSION }} ================================================ FILE: .github/workflows/actions/test-e2e/action.yml ================================================ name: 'Test E2E' description: 'Test E2E' inputs: shard: description: 'Playwright Test Shard (ex: 2)' totalShards: description: 'Playwright total number of test shards (ex: 4)' update: description: 'Whether or not to update the reference snapshots' runs: using: 'composite' steps: - uses: actions/setup-node@v4 - name: Cache Node Modules uses: actions/cache@v4 env: cache-name: node-modules with: path: ./node_modules key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./package-lock.json') }}-v1 - uses: ./.github/workflows/actions/download-archive with: name: ionicons-build path: . filename: IoniconsBuild.zip - uses: ./.github/workflows/actions/download-archive with: name: ionicons-src path: . filename: IoniconsSrc.zip - name: Install Playwright Dependencies run: npx playwright install && npx playwright install-deps shell: bash - name: Test if: inputs.update != 'true' run: npx playwright test --shard=${{ inputs.shard }}/${{ inputs.totalShards }} shell: bash - name: Test and Update id: test-and-update if: inputs.update == 'true' # Keep track of the files that were # changed so they can be correctly restored # in the combine step. # To do this, we move only the changed files # to a separate directory, while preserving the # directory structure of the source. # When, we create and archive of these results # so that the combine step can simply # unzip and move the changed files into place. # We have extra logic added so that job runners # that do not have any new screenshots do not create # an unnecessary .zip. run: | npx playwright test --shard=${{ inputs.shard }}/${{ inputs.totalShards }} --update-snapshots git add src/\*.png --force mkdir updated-screenshots rsync -R --progress $(git diff --name-only --cached) updated-screenshots if [ "$(ls -A updated-screenshots)" ]; then echo "hasUpdatedScreenshots=$(echo 'true')" >> $GITHUB_OUTPUT cd updated-screenshots ls zip -q -r ../UpdatedScreenshots-${{ inputs.shard }}-${{ inputs.totalShards }}.zip ./ fi shell: bash - name: Archive Updated Screenshots if: inputs.update == 'true' && steps.test-and-update.outputs.hasUpdatedScreenshots == 'true' uses: actions/upload-artifact@v4 with: name: updated-screenshots-${{ inputs.shard }}-${{ inputs.totalShards }} path: UpdatedScreenshots-${{ inputs.shard }}-${{ inputs.totalShards }}.zip - name: Archive Test Results # The always() ensures that this step # runs even if the previous step fails. # We want the test results to be archived # even if the test fails in the previous # step, otherwise there would be no way # to debug these tests. if: always() uses: ./.github/workflows/actions/upload-archive with: name: test-results-${{ inputs.shard }}-${{ inputs.totalShards }} output: TestResults-${{ inputs.shard }}-${{ inputs.totalShards }}.zip paths: playwright-report ================================================ FILE: .github/workflows/actions/test-spec/action.yml ================================================ name: 'Test Spec' description: 'Test Spec' runs: using: 'composite' steps: - uses: actions/setup-node@v4 - name: Cache Node Modules uses: actions/cache@v4 env: cache-name: node-modules with: path: ./node_modules key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./package-lock.json') }}-v1 - uses: ./.github/workflows/actions/download-archive with: name: ionicons-build path: . filename: IoniconsBuild.zip - name: Test run: npm run test.spec -- --ci shell: bash ================================================ FILE: .github/workflows/actions/update-reference-screenshots/action.yml ================================================ name: 'Update Reference Screenshots' description: 'Update Reference Screenshots' on: workflow_dispatch: runs: using: 'composite' steps: - uses: actions/setup-node@v4 - uses: actions/download-artifact@v4 with: path: ./artifacts - name: Extract Archives # This finds all .zip files in the ./artifacts # directory, including nested directories. # It then unzips every .zip to the root directory run: | find . -type f -name 'UpdatedScreenshots-*.zip' -exec unzip -q -o -d ../ {} \; shell: bash working-directory: ./artifacts - name: Push Screenshots # Configure user as Ionitron # and push only the changed .png snapshots # to the remote branch. # Screenshots are in .gitignore # to prevent local screenshots from getting # pushed to Git. As a result, we need --force # here so that CI generated screenshots can # get added to git. Screenshot ground truths # should only be added via this CI process. run: | git config user.name ionitron git config user.email hi@ionicframework.com git add src/\*.png --force git commit -m "chore(): add updated snapshots" git push shell: bash ================================================ FILE: .github/workflows/actions/upload-archive/action.yml ================================================ name: 'Archive Upload' description: 'Compresses and uploads an archive to be reused across jobs' inputs: paths: description: 'Paths to files or directories to archive' output: description: 'Output file name' name: description: 'Archive name' runs: using: 'composite' steps: - name: Create Archive run: zip -q -r ${{ inputs.output }} ${{ inputs.paths }} shell: bash - uses: actions/upload-artifact@v4 with: name: ${{ inputs.name }} path: ${{ inputs.output }} ================================================ FILE: .github/workflows/dev-release.yml ================================================ name: 'Dev Release' on: workflow_call: jobs: create-dev-hash: runs-on: ubuntu-latest outputs: dev-hash: ${{ steps.create-dev-hash.outputs.DEV_HASH }} steps: - uses: actions/checkout@v4 - name: Install Dependencies run: npm ci --no-package-lock shell: bash - id: create-dev-hash name: Create Dev Hash run: | echo "DEV_HASH=$(node ./scripts/bump-version.js)-dev.1$(date +%s).1$(git log -1 --format=%H | cut -c 1-7)" >> $GITHUB_OUTPUT shell: bash release-ionicons: runs-on: ubuntu-latest needs: [create-dev-hash] permissions: id-token: write steps: - name: Checkout uses: actions/checkout@v4 - name: Publish to NPM uses: ./.github/workflows/actions/publish-npm with: tag: dev version: ${{ needs.create-dev-hash.outputs.dev-hash }} working-directory: './' createRelease: 'false' get-build: name: Get your dev build! runs-on: ubuntu-latest needs: [create-dev-hash, release-ionicons] steps: - run: echo ${{ needs.create-dev-hash.outputs.dev-hash }} ================================================ FILE: .github/workflows/production-release.yml ================================================ name: 'Production Release' on: workflow_call: inputs: version: required: false type: string description: npm version (major, minor, or patch) jobs: release-ionicons: runs-on: ubuntu-latest permissions: id-token: write steps: - name: Checkout uses: actions/checkout@v4 with: token: ${{ secrets.IONITRON_TOKEN }} - name: Publish to NPM uses: ./.github/workflows/actions/publish-npm with: tag: latest version: ${{ inputs.version }} working-directory: './' createRelease: true ghToken: ${{ secrets.IONITRON_TOKEN }} ================================================ FILE: .github/workflows/release-orchestrator.yml ================================================ name: 'Release Orchestrator' on: workflow_dispatch: inputs: release-type: description: 'Release type' required: true type: choice options: - dev - production version: description: 'Version for production releases' required: false type: choice options: - patch - minor - major permissions: id-token: write contents: read jobs: run-dev: if: ${{ inputs.release-type == 'dev' }} uses: ./.github/workflows/dev-release.yml secrets: inherit run-production: if: ${{ inputs.release-type == 'production' }} uses: ./.github/workflows/production-release.yml secrets: inherit with: version: ${{ inputs.version }} ================================================ FILE: .github/workflows/update-screenshots.yml ================================================ name: 'Update Reference Screenshots' on: workflow_dispatch: jobs: build-core: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/workflows/actions/build-core test-e2e: strategy: # This ensures that all screenshot shard # failures are reported so the dev can # review everything at once. fail-fast: false matrix: # Divide the tests into n buckets # and run those buckets in parallel. # To increase the number of shards, # add new items to the shard array # and change the value of totalShards # to be the length of the shard array. shard: [1, 2, 3] totalShards: [3] needs: [build-core] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/workflows/actions/test-e2e with: shard: ${{ matrix.shard }} totalShards: ${{ matrix.totalShards }} update: true update-reference-screenshots: runs-on: ubuntu-latest needs: [test-e2e] steps: - uses: actions/checkout@v4 # Normally, we could just push with the # default GITHUB_TOKEN, but that will # not cause the build workflow # to re-run. We use Ionitron's # Personal Access Token instead # to allow for this build workflow # to run when the screenshots are pushed. with: token: ${{ secrets.IONITRON_TOKEN }} - uses: ./.github/workflows/actions/update-reference-screenshots ================================================ FILE: .github/workflows/validation.yml ================================================ name: 'Validation' on: pull_request: branches: ['**'] push: branches: ['main'] # When pushing a new commit we should # cancel the previous test run to not # consume more runners than we need to. concurrency: group: ${{ github.ref }} cancel-in-progress: true jobs: build-core: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: # Checkout the latest commit in this branch ref: ${{ github.event.pull_request.head.sha }} - uses: ./.github/workflows/actions/build-core test-spec: needs: [build-core] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/workflows/actions/test-spec test-e2e: strategy: # This ensures that all screenshot shard # failures are reported so the dev can # review everything at once. fail-fast: false matrix: # Divide the tests into n buckets # and run those buckets in parallel. # To increase the number of shards, # add new items to the shard array # and change the value of totalShards # to be the length of the shard array. shard: [1, 2, 3] totalShards: [3] needs: [build-core] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/workflows/actions/test-e2e with: shard: ${{ matrix.shard }} totalShards: ${{ matrix.totalShards }} # Screenshots are required to pass # in order for the branch to be merge # eligible. However, the screenshot tests # are run on n runners where n can change # over time. The verify-screenshots step allows # us to have a required status check for screenshot # results without having to manually add each # matrix run in the branch protection rules # Source: https://github.community/t/status-check-for-a-matrix-jobs/127354 verify-e2e: if: ${{ always() }} needs: test-e2e runs-on: ubuntu-latest steps: - name: Check build matrix status if: ${{ needs.test-e2e.result != 'success' }} run: exit 1 ================================================ FILE: .gitignore ================================================ *~ *.sw[mnpcod] *.log *.lock *.tmp *.tmp.* log.txt *.sublime-project *.sublime-workspace .idea/ .versions/ .vscode/ /components/ node_modules/ tmp/ dist/ icons/ .DS_Store scripts/*.js !scripts/install-loader.js www/ /test-results/ /playwright-report/ /playwright/.cache/ /src/**/*-snapshots *.tgz ================================================ FILE: .prettierrc.json ================================================ { "arrowParens": "always", "bracketSpacing": true, "jsxBracketSameLine": false, "jsxSingleQuote": false, "quoteProps": "consistent", "printWidth": 120, "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": "all", "useTabs": false } ================================================ FILE: CHANGELOG.md ================================================ # Change Log Please see https://github.com/ionic-team/ionicons/releases ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Code of Conduct As contributors and maintainers of the Ionicons project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities. Communication through any of Ionic's channels (GitHub, Slack, Forum, IRC, mailing lists, Twitter, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the Ionicons project to do the same. If any member of the community violates this code of conduct, the maintainers of the Ionicons project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate. If you are subject to or witness unacceptable behavior, or have any other concerns, please email us at [hi@ionicframework.com](mailto:hi@ionicframework.com). ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Ionicons Thank you for your interest in contributing to Ionicons! :tada: This document outlines the guidelines and processes for contributing to this project. ## Table of Contents - [Contributing to Ionicons](#contributing-to-ionicons) - [Table of Contents](#table-of-contents) - [Code of Conduct](#code-of-conduct) - [Getting Started](#getting-started) - [Prerequisites](#prerequisites) - [Environment Setup](#environment-setup) - [Development Workflow](#development-workflow) - [Branch Strategy](#branch-strategy) - [Component Modifications](#component-modifications) - [Testing Changes](#testing-changes) - [Code Style](#code-style) - [Building](#building) - [Submitting Issues](#submitting-issues) - [Submitting Pull Requests](#submitting-pull-requests) ## Code of Conduct Please read our [Contributor Code of Conduct](./CODE_OF_CONDUCT.md) for information on our rules of conduct. By contributing to Ionicons, you agree to abide by its terms. ## Getting Started ### Prerequisites - [Node.js](https://nodejs.org/) (LTS version recommended) - [npm](https://www.npmjs.com/) (comes with Node.js) - Git ### Environment Setup 1. We recommend using [nvm](https://github.com/nvm-sh/nvm) (Node Version Manager) to manage Node.js versions: - For macOS/Linux: `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash` - For Windows: Use [nvm-windows](https://github.com/coreybutler/nvm-windows) - Install and use the LTS version: `nvm install --lts && nvm use --lts` Alternatively, you can [download the installer](https://nodejs.org/) for the LTS version of Node.js directly. 2. Fork the repository on GitHub 3. Clone your fork locally: ```bash git clone https://github.com/YOUR-USERNAME/ionicons.git cd ionicons ``` 4. Add the original repository as an upstream remote: ```bash git remote add upstream https://github.com/ionic-team/ionicons.git ``` 5. Create a new branch from `main` for your change: ```bash git checkout -b your-feature-branch ``` 6. Install dependencies: ```bash npm install ``` 7. Run the initial build: ```bash npm run build ``` 8. If desired, [modify the Icon Component](#component-modifications) 9. Or, modify and preview the site ## Development Workflow ### Branch Strategy - Always create a new branch from `main` for your changes: ```bash git checkout main git pull upstream main git checkout -b your-feature-branch ``` ### Component Modifications If you're modifying the `ion-icon` component: 1. Navigate to `src/components/` directory and open the `icon` component to modify 2. Make your changes to the component code 3. Test your changes (see [Testing Changes](#testing-changes)) ### Testing Changes To preview component changes: 1. Run: ```bash npm start ``` This will start a local version of the icon test with a test page 2. Modify the test page in `index.html` as needed to test your changes 3. If you are modifying icons, you can run `npm run build.files` to re-run the SVG optimization script to verify there are no changes after optimizing the SVG ### Code Style - This project uses Prettier for code formatting - Run `npm run prettier` to format your code before submitting ### Building - Run `npm run build` to build the complete package - Run `npm run build.files` to rebuild only the SVG icon files ## Submitting Issues Please submit issues for: - Bug reports - Feature requests - General questions about the project When creating issues: 1. If you have a question about using Ionicons, please ask on the [Ionic Forum](http://forum.ionicframework.com/) or in the [Ionic Discord](https://ionic.link/discord). 2. It is required that you clearly describe the steps necessary to reproduce the issue you are running into. Although we would love to help our users as much as possible, diagnosing issues without clear reproduction steps is extremely time-consuming and simply not sustainable. 3. The issue list of this repository is exclusively for bug reports and feature requests. Non-conforming issues will be closed immediately. 4. Check if a similar issue already exists by searching through [existing issues](https://github.com/ionic-team/ionicons/issues?utf8=%E2%9C%93&q=is%3Aissue). You can search through existing issues to see if there is a similar one reported. Include closed issues as it may have been closed with a solution. 5. Use the provided issue templates and clearly describe: - Expected behavior - Actual behavior - Steps to reproduce (for bugs) - Browser/device information when relevant 6. [Create a new issue](https://github.com/ionic-team/ionicons/issues/new/choose) that thoroughly explains the problem. ## Submitting Pull Requests 1. Before submitting a Pull Request (PR), please: - Create an issue first to discuss the proposed changes - Comment on an existing issue mentioning you'd like to work on it - Look for issues labeled with [`help wanted`](https://github.com/ionic-team/ionicons/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) if you're new to the project 2. Creating the PR: - Update your fork to the latest upstream main - Make your changes in a new git branch - Follow the code style of the project - Include relevant tests - Make sure all tests pass: `npm test` - Update documentation if needed - [Create a new pull request](https://github.com/ionic-team/ionicons/compare) with the `main` branch as the base. ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015-present Ionic (http://ionic.io/) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: jest.config.mjs ================================================ import path from 'path'; import url from 'url'; import { createJestStencilPreset } from 'jest-stencil-runner'; const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); export default createJestStencilPreset({ rootDir: __dirname, // Add any additional Jest configuration here collectCoverageFrom: [ 'src/**/*.{ts,tsx}', '!src/**/*.d.ts', ], testMatch: [ '**/__tests__/**/*.(ts|tsx|js)', '**/*.(test|spec).(ts|tsx|js)' ] }); ================================================ FILE: package.json ================================================ { "name": "ionicons", "version": "8.0.13", "description": "Premium icons for Ionic.", "files": [ "components/", "dist/", "icons/" ], "main": "./dist/index.cjs.js", "module": "./dist/index.js", "types": "dist/types/index.d.ts", "unpkg": "dist/ionicons/ionicons.esm.js", "jsdelivr": "dist/ionicons/ionicons.esm.js", "collection": "dist/collection/collection-manifest.json", "collection:main": "dist/collection/index.js", "exports": { ".": { "types": "./dist/types/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs.js" }, "./loader": { "types": "./dist/loader/index.d.ts", "import": "./dist/loader/index.js", "require": "./dist/loader/index.cjs.js" }, "./components": { "types": "./components/index.d.ts", "import": "./components/index.js" }, "./components/ion-icon.js": { "types": "./components/ion-icon.d.ts", "import": "./components/ion-icon.js" }, "./icons": { "types": "./icons/index.d.ts", "import": "./icons/index.mjs", "require": "./icons/index.js" }, "./dist/ionicons.json": { "default": "./dist/ionicons.json" } }, "scripts": { "build": "run-s clean build.* prettier", "build.files": "tsx scripts/build.ts", "build.component": "stencil build", "build.collection": "tsx scripts/collection-copy.ts", "clean": "rimraf dist components icons www", "prettier": "npm run prettier.base -- --write", "prettier.base": "prettier --cache \"./({bin,scripts,src,test}/**/*.{ts,tsx,js,jsx})|bin/stencil|.github/(**/)?*.(yml|yaml)|*.js\"", "prettier.dry-run": "npm run prettier.base -- --list-different", "start": "run-s build.files start.stencil", "start.stencil": "stencil build --dev --watch --serve", "test": "run-s test.spec", "test.spec": "jest", "test.e2e": "playwright test", "test.e2e.install": "playwright install && playwright install-deps" }, "dependencies": { "@stencil/core": "^4.35.3" }, "devDependencies": { "@playwright/test": "^1.53.2", "@types/fs-extra": "^11.0.4", "@types/jest": "^30.0.0", "@types/node": "^22.15.3", "@types/svgo": "^3.0.0", "fs-extra": "^11.3.0", "jest": "^30.0.4", "jest-cli": "^30.0.4", "jest-stencil-runner": "^0.0.8", "npm-run-all2": "^8.0.4", "prettier": "^3.6.2", "puppeteer": "^24.12.0", "rimraf": "^6.0.1", "semver": "^7.7.2", "serve": "^14.2.4", "svgo": "4.0.0", "tsx": "^4.20.3", "typescript": "^5.8.3" }, "keywords": [ "icon pack", "ionic", "icon", "svg", "mobile", "web component", "component", "custom element", "material design", "ios" ], "homepage": "http://ionicons.com/", "author": { "name": "Ben Sperry", "web": "https://twitter.com/benjsperry" }, "contributors": [ { "name": "Adam Bradley", "web": "http://twitter.com/adamdbradley" } ], "repository": { "type": "git", "url": "https://github.com/ionic-team/ionicons.git" }, "bugs": { "url": "https://github.com/ionic-team/ionicons/issues" }, "license": "MIT", "sideEffects": [ "icons/imports/" ], "web-types": "dist/ionic.web-types.json" } ================================================ FILE: playwright.config.ts ================================================ import type { PlaywrightTestConfig } from '@playwright/test'; import { devices, expect } from '@playwright/test'; const projects: PlaywrightTestConfig['projects'] = [ { /** * This is really just desktop Firefox * but with a mobile viewport. */ name: 'Mobile Firefox', use: { browserName: 'firefox', /** * This is the Pixel 5 configuration. * We can't use devices['Pixel 5'] * because the "isMobile" option is * not supported on Firefox. */ viewport: { width: 393, height: 727 } }, }, { name: 'Mobile Chrome', use: { browserName: 'chromium', ...devices['Pixel 5'] } }, { name: 'Mobile Safari', use: { browserName: 'webkit', ...devices['iPhone 12'] } } ]; /** * See https://playwright.dev/docs/test-configuration. */ const config: PlaywrightTestConfig = { testMatch: '*.e2e.ts', expect: { /** * Maximum time expect() should wait for the condition to be met. * For example in `await expect(locator).toHaveText();` */ timeout: 5000, /** * Configure screenshot comparison settings * to be more tolerant of minor visual differences */ toHaveScreenshot: { // Increase the threshold to allow for small font rendering differences // This sets the maximum allowed ratio of pixels that can be different maxDiffPixelRatio: 0.02, // Allow up to 2% of pixels to be different // Alternatively, you can use absolute pixel count // maxDiffPixels: 100, // Allow up to 100 pixels to be different // Add a threshold for per-pixel difference to handle anti-aliasing threshold: 0.2, // Default is 0.1 } }, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ actionTimeout: 0, /* Base URL to use in actions like `await page.goto('/')`. */ // baseURL: 'http://localhost:3000', /** * All failed tests should create * a trace file for easier debugging. * * See https://playwright.dev/docs/trace-viewer */ trace: 'retain-on-failure', baseURL: 'http://localhost:3333', }, /* Configure projects for major browsers */ projects, webServer: { command: 'serve www -p 3333', port: 3333, reuseExistingServer: !process.env.CI } }; export default config; ================================================ FILE: readme.md ================================================ # Ionicons [Ionicons](http://ionicons.com/) is a completely open-source icon set with 1,300 icons crafted for web, iOS, Android, and desktop apps. Ionicons was built for [Ionic Framework](https://ionicframework.com/), so icons have both Material Design and iOS versions. Note: All brand icons are trademarks of their respective owners. The use of these trademarks does not indicate endorsement of the trademark holder by Ionic, nor vice versa. We intend for this icon pack to be used with [Ionic](http://ionicframework.com/), but it’s by no means limited to it. Use them wherever you see fit, personal or commercial. They are free to use and licensed under [MIT](http://opensource.org/licenses/MIT). ## Contributing Thanks for your interest in contributing! Read up on our guidelines for [contributing](https://github.com/ionic-team/ionicons/blob/main/CONTRIBUTING.md) and then look through our issues with a [help wanted](https://github.com/ionic-team/ionicons/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) label. ## Using the Web Component The Ionicons Web Component is an easy and performant way to use Ionicons in your app. The component will dynamically load an SVG for each icon, so your app is only requesting the icons that you need. Also note that only visible icons are loaded, and icons that are "below the fold" and hidden from the user's view do not make fetch requests for the svg resource. ### Installation If you're using [Ionic Framework](https://ionicframework.com/), Ionicons is packaged by default, so no installation is necessary. Want to use Ionicons without Ionic Framework? Place the following ` ``` you can replace `latest` to pick any version of Ionicon, e.g.: ```html ``` ### Basic usage To use a built-in icon from the Ionicons package, populate the `name` attribute on the ion-icon component: ```html ``` ### Custom icons To use a custom SVG, provide its url in the `src` attribute to request the external SVG file. The `src` attribute works the same as `` in that the url must be accessible from the webpage that's making a request for the image. Additionally, the external file can only be a valid svg and does not allow scripts or events within the svg element. ```html ``` #### Custom Asset Path If you have a different set of icons you would like to load or if the Ionicon icons are hosted on a different page or path, you can set the asset url from which Ionicons pulls the icons via: ```ts import { setAssetPath, addIcons } from 'ionicons'; import { add, logoIonic, save } from 'ionicons/icons'; // set root path for loading icons to "/public/svg" setAssetPath(`${window.location.origin}/public/svg/`); // only load specific icons addIcons({ add, logoIonic, save }); ``` This allows the use of named icons like this: ```html ``` ## Variants Each app icon in Ionicons has a `filled`, `outline` and `sharp` variant. These different variants are provided to make your app feel native to a variety of platforms. The filled variant uses the default name without a suffix. Note: Logo icons do not have outline or sharp variants. ```html ``` ### Platform specificity When using icons in Ionic Framework you can specify different icons per platform. Use the `md` and `ios` attributes and provide the platform-specific icon/variant name. ```html ``` ## Size To specify the icon size, you can use the size attribute for our pre-defined font sizes. ```html ``` Or you can set a specific size by applying the `font-size` CSS property on the `ion-icon` component. It's recommended to use pixel sizes that are a multiple of 8 (8, 16, 32, 64, etc.) ```css ion-icon { font-size: 64px; } ``` ## Color Specify the icon color by applying the `color` CSS property on the `ion-icon` component. ```css ion-icon { color: blue; } ``` ## Stroke width When using an `outline` icon variant it is possible to adjust the stroke width, for improved visual balance relative to the icon's size or relative to the width of adjacent text. You can set a specific size by applying the `--ionicon-stroke-width` CSS custom property to the `ion-icon` component. The default value is 32px. ```html ``` ```css ion-icon { --ionicon-stroke-width: 16px; } ``` ## Migrating from v4 See the [5.0 release notes](https://github.com/ionic-team/ionicons/releases/tag/5.0.0) for a list of icon deletions/renames. ## License Ionicons is licensed under the [MIT license](http://opensource.org/licenses/MIT). ## Related * [Ionicons Homepage](http://ionicons.com/) * [Ionic Framework](https://ionicframework.com/) * [Ionic Discord](https://ionic.link/discord) * [Ionic Forum](https://forum.ionicframework.com/) * [Stencil](https://stenciljs.com/) * [Capacitor](https://capacitorjs.com/) ================================================ FILE: scripts/build.ts ================================================ import path from 'node:path'; import fs from 'fs-extra'; import { optimize } from 'svgo'; import { webComponentPassPlugins, sourcePassPlugins } from './plugins'; import { reservedKeywords } from './constants'; import pkgData from '../package.json' with { type: 'json' }; import type { SvgData } from './types'; async function build(rootDir: string) { try { const srcDir = path.join(rootDir, 'src'); const srcSvgDir = path.join(srcDir, 'svg'); const iconDir = path.join(rootDir, 'icons'); const distDir = path.join(rootDir, 'dist'); const distIoniconsDir = path.join(distDir, 'ionicons'); const distSvgDir = path.join(distDir, 'svg'); const optimizedSvgDir = path.join(distIoniconsDir, 'svg'); /** * Create the directories first, then empty them * This ensures they exist before we try to write files to them */ await Promise.all([ fs.ensureDir(iconDir), fs.ensureDir(distDir), fs.ensureDir(distSvgDir), fs.ensureDir(optimizedSvgDir), fs.ensureDir(distIoniconsDir), ]); /** * Empty the directories */ await Promise.all([ fs.emptyDir(iconDir), fs.emptyDir(distDir), fs.emptyDir(distSvgDir), fs.emptyDir(optimizedSvgDir), fs.emptyDir(distIoniconsDir), ]); const version = pkgData.version as string; const srcSvgData = await getSvgs(srcSvgDir, distSvgDir, optimizedSvgDir); await optimizeSvgs(srcSvgData); await Promise.all([ createDataJson(version, srcDir, distDir, srcSvgData), createIconPackage(version, iconDir, srcSvgData), ]); const svgSymbolsContent = await createSvgSymbols(version, distDir, srcSvgData); await createCheatsheet(version, rootDir, distDir, svgSymbolsContent, srcSvgData); await createWebTypes(version, rootDir, distDir, srcSvgData); await copyToTesting(rootDir, distDir, srcSvgData); } catch (e) { console.error(e); process.exit(1); } } async function optimizeSvgs(srcSvgData: SvgData[]) { await Promise.all( srcSvgData.map(async (svgData) => { return optimizeSvg(svgData); }), ); } async function optimizeSvg(svgData: SvgData) { const srcSvgContent = await fs.readFile(svgData.srcFilePath, 'utf8'); const optimizedSvg = await optimize(srcSvgContent, { path: svgData.srcFilePath }); const webComponentSvg = await optimize(optimizedSvg.data, { path: svgData.srcFilePath, plugins: webComponentPassPlugins, }); const sourceSvg = await optimize(optimizedSvg.data, { path: svgData.srcFilePath, plugins: sourcePassPlugins, }); // Ensure directories exist before writing files await Promise.all([ fs.ensureDir(path.dirname(svgData.optimizedFilePath)), fs.ensureDir(path.dirname(svgData.distSvgFilePath)), ]); svgData.optimizedSvgContent = webComponentSvg.data; await Promise.all([ fs.writeFile(svgData.optimizedFilePath, svgData.optimizedSvgContent), fs.writeFile(svgData.distSvgFilePath, sourceSvg.data), ]); } async function copyToTesting(rootDir: string, distDir: string, srcSvgData: SvgData[]) { const testDir = path.join(rootDir, 'www'); const testBuildDir = path.join(testDir, 'build'); const testSvgDir = path.join(testBuildDir, 'svg'); // Ensure all directories exist await Promise.all([fs.ensureDir(testDir), fs.ensureDir(testBuildDir), fs.ensureDir(testSvgDir)]); await Promise.all( srcSvgData .filter((svgData): svgData is SvgData & { optimizedSvgContent: string } => Boolean(svgData.optimizedSvgContent)) .map(async (svgData) => { const testSvgFilePath = path.join(testSvgDir, svgData.fileName); await fs.writeFile(testSvgFilePath, svgData.optimizedSvgContent); }), ); const distCheatsheetFilePath = path.join(distDir, 'cheatsheet.html'); const testCheatsheetFilePath = path.join(testDir, 'cheatsheet.html'); await fs.copyFile(distCheatsheetFilePath, testCheatsheetFilePath); } async function createSvgSymbols(version: string, distDir: string, srcSvgData: SvgData[]) { srcSvgData = srcSvgData.sort((a, b) => { if (a.iconName < b.iconName) return -1; if (a.iconName > b.iconName) return 1; return 0; }); const symbolsSvgFilePath = path.join(distDir, 'ionicons.symbols.svg'); const lines = [ ``, ``, ]; srcSvgData .filter((svgData): svgData is SvgData & { optimizedSvgContent: string } => Boolean(svgData.optimizedSvgContent)) .forEach((svgData) => { const svg = svgData.optimizedSvgContent .replace(``, ``); lines.push(svg); }); lines.push(``, ``); const content = lines.join('\n'); await fs.writeFile(symbolsSvgFilePath, content); return content; } async function createCheatsheet( version: string, rootDir: string, distDir: string, svgSymbolsContent: string, srcSvgData: SvgData[], ) { const CheatsheetTmpFilePath = path.join(rootDir, 'scripts', 'cheatsheet-template.html'); const distCheatsheetFilePath = path.join(distDir, 'cheatsheet.html'); const c = srcSvgData.map( (svgData) => ``, ); c.push(svgSymbolsContent); const html = (await fs.readFile(CheatsheetTmpFilePath, 'utf8')) .replace(/{{version}}/g, version) .replace(/{{count}}/g, srcSvgData.length.toString()) .replace(/{{content}}/g, c.join('\n')); await fs.writeFile(distCheatsheetFilePath, html); } async function createWebTypes(version: string, rootDir: string, distDir: string, srcSvgData: SvgData[]) { const srcWebTypeFilePath = path.join(rootDir, 'src/ionicons.web-types.json'); const distWebTypesFilePath = path.join(distDir, 'ionicons.web-types.json'); const webTypes = JSON.parse(await fs.readFile(srcWebTypeFilePath, 'utf8')); webTypes.version = version; const icons = webTypes.contributions.html.ionicons; for (let data of srcSvgData) { icons.push({ name: data.iconName, icon: 'dist/svg/' + data.fileName, }); } const jsonStr = JSON.stringify(webTypes, null, 2) + '\n'; await fs.writeFile(distWebTypesFilePath, jsonStr); } async function getSvgs(srcSvgDir: string, distSvgDir: string, optimizedSvgDir: string): Promise { const svgFiles = (await fs.readdir(srcSvgDir)).filter((fileName) => { return !fileName.startsWith('.') && fileName.endsWith('.svg'); }); const svgData = await Promise.all( svgFiles.map(async (fileName) => { // fileName: airplane-outline.svg if (fileName.toLowerCase() !== fileName) { throw new Error(`svg filename "${fileName}" must be all lowercase`); } // srcFilePath: /src/svg/airplane-outline.svg const srcFilePath = path.join(srcSvgDir, fileName); // srcFilePath: /src/svg/airplane-outline.svg const distSvgFilePath = path.join(distSvgDir, fileName); // optimizedFilePath: /dist/ionicons/svg/airplane-outline.svg const optimizedFilePath = path.join(optimizedSvgDir, fileName); const dotSplit = fileName.split('.'); if (dotSplit.length > 2) { throw new Error(`svg filename "${fileName}" cannot contain more than one period`); } // iconName: airplane-outline const iconName = dotSplit[0]; if (reservedKeywords.has(iconName)) { throw new Error(`svg icon name "${iconName}" is a reserved JavaScript keyword`); } // fileNameMjs: airplane-outline.mjs const fileNameMjs = iconName + '.mjs'; // fileNameCjs: airplane-outline.mjs const fileNameCjs = iconName + '.js'; // exportName: airplaneOutline const exportName = camelize(iconName); const title = toTitleCase( fileName.replace('.svg', '').replace('-outline', '').replace('-sharp', '').replace(/-/g, ' '), ); return { fileName, title, srcFilePath, distSvgFilePath, srcSvgContent: await fs.readFile(srcFilePath, 'utf8'), optimizedFilePath, iconName, fileNameMjs, fileNameCjs, exportName, }; }), ); return svgData.sort((a, b) => { if (a.exportName < b.exportName) return -1; if (a.exportName > b.exportName) return 1; return 0; }); } async function createIconPackage(version: string, iconDir: string, srcSvgData: SvgData[]) { await Promise.all([ createEsmIcons(version, iconDir, srcSvgData), createCjsIcons(version, iconDir, srcSvgData), createDtsIcons(version, iconDir, srcSvgData), ]); } async function createEsmIcons(version: string, iconDir: string, srcSvgData: SvgData[]) { const iconEsmFilePath = path.join(iconDir, 'index.mjs'); const o = [`/* Ionicons v${version}, ES Modules */`, ``]; srcSvgData.forEach((svgData) => { o.push(`export const ${svgData.exportName} = ${getDataUrl(svgData)}`); }); await fs.writeFile(iconEsmFilePath, o.join('\n') + '\n'); } async function createCjsIcons(version: string, iconDir: string, srcSvgData: SvgData[]) { const iconCjsFilePath = path.join(iconDir, 'index.js'); const o = [`/* Ionicons v${version}, CommonJS */`, ``]; srcSvgData.forEach((svgData) => { o.push(`exports.${svgData.exportName} = ${getDataUrl(svgData)}`); }); await fs.writeFile(iconCjsFilePath, o.join('\n') + '\n'); } async function createDtsIcons(version: string, iconDir: string, srcSvgData: SvgData[]) { const iconDtsFilePath = path.join(iconDir, 'index.d.ts'); const o = [`/* Ionicons v${version}, Types */`, ``]; srcSvgData.forEach((svgData) => { o.push(`export declare var ${svgData.exportName}: string;`); }); await fs.writeFile(iconDtsFilePath, o.join('\n') + '\n'); } function getDataUrl(svgData: SvgData) { let svg = svgData.optimizedSvgContent; if (!svg) { throw new Error(`oh no! no optimized SVG content! ${svgData.fileName}`); } if (svg.includes(`'`)) { throw new Error(`oh no! no single quotes allowed! ${svgData.fileName}`); } if (svg.includes(`\n`) || svg.includes(`\r`)) { throw new Error(`oh no! no new lines allowed! ${svgData.fileName}`); } svg = svg.replace(/"/g, "'"); return `"data:image/svg+xml;utf8,${svg}"`; } async function createDataJson(version: string, srcDir: string, distDir: string, srcSvgData: SvgData[]) { const srcDataJsonPath = path.join(srcDir, 'data.json'); const distDataJsonPath = path.join(distDir, 'ionicons.json'); const data = await fs.readJson(srcDataJsonPath).catch(() => ({})); data.icons = data.icons || []; // add new icons srcSvgData.forEach((svgData) => { if (!data.icons.some((i) => i.name === svgData.iconName)) { data.icons.push({ name: svgData.iconName, }); } }); // remove dead icons data.icons = data.icons.filter((dataIcon) => { return srcSvgData.some((svgData) => dataIcon.name === svgData.iconName); }); // sort data.icons = data.icons.sort((a, b) => { if (a.name < b.name) return -1; if (a.name > b.name) return 1; return 0; }); data.icons.forEach((icon) => { icon.tags = icon.tags || icon.name.split('-'); icon.tags = icon.tags.sort(); }); const srcJsonStr = JSON.stringify(data, null, 2) + '\n'; await fs.writeFile(srcDataJsonPath, srcJsonStr); const distJsonData = { name: 'ionicons', version, icons: data.icons, }; const distJsonStr = JSON.stringify(distJsonData, null, 2) + '\n'; await fs.writeFile(distDataJsonPath, distJsonStr); } function camelize(text: string) { let words = text.split(/[-_]/g); // ok one simple regexp. return words[0].toLowerCase() + words.slice(1).map(upFirst).join(''); } function upFirst(word: string) { return word[0].toUpperCase() + word.toLowerCase().slice(1); } function toTitleCase(str: string) { const s = str.trim().toLowerCase().split(' '); for (var i = 0; i < s.length; i++) { s[i] = s[i].charAt(0).toUpperCase() + s[i].slice(1); } return s.join(' '); } // let's do this build(path.join(__dirname, '..')); ================================================ FILE: scripts/cheatsheet-template.html ================================================ Ionicons {{version}} Cheatsheet

Ionicons {{version}} Cheatsheet

{{count}} icons

{{content}} ================================================ FILE: scripts/collection-copy.ts ================================================ import fs from 'fs-extra'; import { join } from 'path'; async function collectionCopy(rootDir: string) { // move optimized svgs to correct collection location const optimizedSrc = join(rootDir, 'dist', 'ionicons', 'svg'); const collectionDest = join(rootDir, 'dist', 'collection', 'components', 'icon', 'svg'); await fs.copy(optimizedSrc, collectionDest); // we don't to copy the src svgs to collection await fs.remove(join(rootDir, 'dist', 'collection', 'svg')); // We don't want to copy the test svg assets to collection await fs.remove(join(rootDir, 'dist', 'collection', 'components', 'test')); const cePackageDir = join(rootDir, 'components'); const cePackageJsonPath = join(cePackageDir, 'package.json'); const ceCjsPath = join(cePackageDir, 'index.cjs.js'); const emptyCjs = `/*empty cjs*/`; await fs.writeFile(ceCjsPath, emptyCjs); const cePackageJson = { name: 'ionicons/components', description: 'Ionicons custom element.', main: './index.cjs.js', module: './index.js', types: './index.d.ts', private: true, }; await fs.writeFile(cePackageJsonPath, JSON.stringify(cePackageJson, null, 2)); // this is temporary!!!! // removing the `type` from the d.ts export // to make it easier for users migrating between // of older versions of angular and typescript // to the newer verisons, where the `type` keyword // is used. This is a megahack, no doubt. const typesDist = join(rootDir, 'dist', 'types', 'index.d.ts'); let types = await fs.readFile(typesDist, 'utf8'); types = types.replace('export type', 'export'); await fs.writeFile(typesDist, types); } collectionCopy(join(__dirname, '..')); ================================================ FILE: scripts/constants.ts ================================================ // https://mathiasbynens.be/notes/reserved-keywords export const reservedKeywords = new Set([ 'do', 'if', 'in', 'for', 'let', 'new', 'try', 'var', 'case', 'else', 'enum', 'eval', 'null', 'this', 'true', 'void', 'with', 'await', 'break', 'catch', 'class', 'const', 'false', 'super', 'throw', 'while', 'yield', 'delete', 'export', 'import', 'public', 'return', 'static', 'switch', 'typeof', 'default', 'extends', 'finally', 'package', 'private', 'continue', 'debugger', 'function', 'arguments', 'interface', 'protected', 'implements', 'instanceof', 'constructor', ]); ================================================ FILE: scripts/plugins.ts ================================================ import { PluginConfig } from 'svgo'; const setRootIoniconClass: PluginConfig = { name: 'addClassesToSVGElement', params: { className: 'ionicon', }, }; const addFillNoneCss: PluginConfig = { name: 'addFillNoneCss', fn: () => ({ element: { enter: (element) => { if (element.attributes.fill) { if (element.attributes.fill === 'none') { element.attributes.class = [...(element.attributes.class?.split(' ') || []), 'ionicon-fill-none'].join(' '); } delete element.attributes.fill; } if (element.attributes.stroke) { delete element.attributes.stroke; } if ( element.attributes['stroke-width'] && (element.attributes['stroke-width'] === '32px' || element.attributes['stroke-width'] === '32') ) { delete element.attributes['stroke-width']; element.attributes.class = [...(element.attributes.class?.split(' ') || []), 'ionicon-stroke-width'].join( ' ', ); } }, }, }), }; const forceCurrentColor: PluginConfig = { name: 'forceCurrentColor', fn: () => ({ element: { enter: (element) => { const attr = element.attributes.stroke || element.attributes.fill; const attrName = element.attributes.stroke ? 'stroke' : element.attributes.fill ? 'fill' : undefined; if (attrName) { if (attr === '#000' || attr === 'currentColor') { element.attributes[attrName] = 'currentColor'; } else if (attr !== 'none') { throw new Error(`invalid "${attrName}" value: ${element.attributes[attrName]}`); } } }, }, }), }; const validatePlugin: PluginConfig = { name: 'validate', fn: () => ({ element: { enter: (element) => { if (element.attributes.style) { console.warn( `Inline style attributed detected: <${element.name} style="${element.attributes.style}">...`, ); } if (element.name === 'style') { console.warn('Inline style element detected'); } }, }, }), }; const basePlugins: PluginConfig[] = [ 'removeStyleElement', 'convertStyleToAttrs', 'removeScripts', 'removeDimensions', setRootIoniconClass, validatePlugin, ]; export const webComponentPassPlugins: PluginConfig[] = [...basePlugins, addFillNoneCss]; export const sourcePassPlugins: PluginConfig[] = [...basePlugins, forceCurrentColor]; ================================================ FILE: scripts/readme.md ================================================ # Development and Build Scripts ## Updating Icons The `src/svg` directory is the single source of truth for svgs. They should not already be optimized and can be the original svg export straight out of an svg editor. A build step before releasing will optimize the source svgs (remove comments, reduce the size, etc) and ensure they'll work within `ion-icon`. ## Build Locally After an svg has been updated, added or deleted from the `src/svg` directory, run: ```sh npm run build ``` The build command will optimize all of the icons and generate the files to be distributed. After the build command, all of the optimized svgs are saved in `dist/ionicons/svg`. Additionally the `dist` directory contains the distribution files for the `ion-icon` web component. ## Svg Symbols Cheatsheet After a build, a new `www/cheatsheet.html` file will be created. This version uses svg symbols rather than `ion-icon`. ## ion-icon Component Preview To see the `ion-icon` component in action, run: ```sh npm start ``` ## Release Steps The release script will ask what version to use. After the script completes, double check the `www/cheatsheet.html` to ensure everything is good to go. Next, update `CHANGELOG.md`, then commit and push your changes Github. ```sh npm run release ``` Triple check the version number is correct, and choose which tag this should be released as. If it's a pre-release, it should be `dev`. ================================================ FILE: scripts/types.ts ================================================ import { PluginConfig } from 'svgo'; export interface SvgData { /** * airplane-outline.svg */ fileName: string; /** * airplane */ title: string; /** * /src/svg/airplane-outline.svg */ srcFilePath: string; /** * /dist/ionicons/svg/airplane-outline.svg */ optimizedFilePath: string; /** * /dist/svg/airplane-outline.svg */ distSvgFilePath: string; /** * optimized svg content */ optimizedSvgContent?: string; /** * airplane-outline */ iconName: string; /** * airplane-outline.mjs */ fileNameMjs: string; /** * airplane-outline.js */ fileNameCjs: string; /** * airplaneOutline */ exportName: string; } ================================================ FILE: src/components/icon/icon.css ================================================ :host { display: inline-block; width: 1em; height: 1em; contain: strict; fill: currentColor; box-sizing: content-box !important; } :host .ionicon { stroke: currentColor; } .ionicon-fill-none { fill: none; } .ionicon-stroke-width { stroke-width: var(--ionicon-stroke-width, 32px); } .icon-inner, .ionicon, svg { display: block; height: 100%; width: 100%; } /* Icon RTL * ----------------------------------------------------------- */ /** * Safari <16.4 incorrectly reports * that it supports :dir(rtl) when it does not. * This @supports statement lets us target * WebKit browsers to apply the RTL fallback. * -webkit-named-image only exists on WebKit. * For WebKit browsers that do support :dir(rtl) * (i.e. Safari >= 16.4) then the :dir(rtl) * code farther down on the page will take * effect and override this fallback. */ @supports (background: -webkit-named-image(i)) { :host(.icon-rtl) .icon-inner { transform: scaleX(-1); } } /** * Fallback for browsers that support * neither :host-context nor :dir. * The icon will not react to dir * changes, but it will at least * respect the dir on component load. */ @supports not selector(:dir(rtl)) and selector(:host-context([dir='rtl'])) { :host(.icon-rtl) .icon-inner { transform: scaleX(-1); } } /* :host-context is supported in chromium; :dir is supported in safari & firefox */ :host(.flip-rtl):host-context([dir='rtl']) .icon-inner { transform: scaleX(-1); } @supports selector(:dir(rtl)) { :host(.flip-rtl:dir(rtl)) .icon-inner { transform: scaleX(-1); } /** * This is needed for WebKit otherwise the fallback * will always cause the icon to be flipped if the document * loads in RTL. */ :host(.flip-rtl:dir(ltr)) .icon-inner { transform: scaleX(1); } } /* Icon Sizes * ----------------------------------------------------------- */ :host(.icon-small) { font-size: 1.125rem !important; } :host(.icon-large) { font-size: 2rem !important; } /* Icon Colors * ----------------------------------------------------------- */ :host(.ion-color) { color: var(--ion-color-base) !important; } :host(.ion-color-primary) { --ion-color-base: var(--ion-color-primary, #3880ff); } :host(.ion-color-secondary) { --ion-color-base: var(--ion-color-secondary, #0cd1e8); } :host(.ion-color-tertiary) { --ion-color-base: var(--ion-color-tertiary, #f4a942); } :host(.ion-color-success) { --ion-color-base: var(--ion-color-success, #10dc60); } :host(.ion-color-warning) { --ion-color-base: var(--ion-color-warning, #ffce00); } :host(.ion-color-danger) { --ion-color-base: var(--ion-color-danger, #f14141); } :host(.ion-color-light) { --ion-color-base: var(--ion-color-light, #f4f5f8); } :host(.ion-color-medium) { --ion-color-base: var(--ion-color-medium, #989aa2); } :host(.ion-color-dark) { --ion-color-base: var(--ion-color-dark, #222428); } ================================================ FILE: src/components/icon/icon.tsx ================================================ import { Build, Component, Element, Host, Prop, State, Watch, h } from '@stencil/core'; import { getSvgContent, ioniconContent } from './request'; import { getName, getUrl, inheritAttributes, isRTL } from './utils'; @Component({ tag: 'ion-icon', assetsDirs: ['svg'], styleUrl: 'icon.css', shadow: true, }) export class Icon { private io?: IntersectionObserver; private iconName: string | null = null; private inheritedAttributes: { [k: string]: any } = {}; private didLoadIcon = false; @Element() el!: HTMLElement; @State() private svgContent?: string; @State() private isVisible = false; /** * The mode determines which platform styles to use. */ @Prop({ mutable: true }) mode = getIonMode(); /** * The color to use for the background of the item. */ @Prop() color?: string; /** * Specifies which icon to use on `ios` mode. */ @Prop() ios?: string; /** * Specifies which icon to use on `md` mode. */ @Prop() md?: string; /** * Specifies whether the icon should horizontally flip when `dir` is `"rtl"`. */ @Prop() flipRtl?: boolean; /** * Specifies which icon to use from the built-in set of icons. */ @Prop({ reflect: true }) name?: string; /** * Specifies the exact `src` of an SVG file to use. */ @Prop() src?: string; /** * A combination of both `name` and `src`. If a `src` url is detected * it will set the `src` property. Otherwise it assumes it's a built-in named * SVG and set the `name` property. */ @Prop() icon?: any; /** * The size of the icon. * Available options are: `"small"` and `"large"`. */ @Prop() size?: string; /** * If enabled, ion-icon will be loaded lazily when it's visible in the viewport. * Default, `false`. */ @Prop() lazy = false; /** * When set to `false`, SVG content that is HTTP fetched will not be checked * if the response SVG content has any `

Ionicons - Test

Default

================================================ FILE: src/components/test/dynamic-type/icon.e2e.ts ================================================ import { expect } from '@playwright/test'; import { test } from '../../../utils/test/playwright'; test.describe('icon: dynamic type', () => { test.beforeEach(async ({ page }) => { await page.goto('/test/dynamic-type'); }); test('should scale text on larger font sizes', async ({ page }) => { // Wait for all SVGs to be lazily loaded before taking screenshots await page.waitForLoadState('networkidle'); const icons = page.locator('#icons'); await expect(icons).toHaveScreenshot(`icon-dynamic-type-diff.png`); }); }); ================================================ FILE: src/components/test/dynamic-type/index.html ================================================ IonIcon - Dynamic Type
================================================ FILE: src/components.d.ts ================================================ /* eslint-disable */ /* tslint:disable */ /** * This is an autogenerated file created by the Stencil compiler. * It contains typing information for all components that exist in this project. */ import { HTMLStencilElement, JSXBase } from '@stencil/core/internal'; export namespace Components { interface IonIcon { /** * The color to use for the background of the item. */ color?: string; /** * Specifies whether the icon should horizontally flip when `dir` is `"rtl"`. */ flipRtl?: boolean; /** * A combination of both `name` and `src`. If a `src` url is detected it will set the `src` property. Otherwise it assumes it's a built-in named SVG and set the `name` property. */ icon?: any; /** * Specifies which icon to use on `ios` mode. */ ios?: string; /** * If enabled, ion-icon will be loaded lazily when it's visible in the viewport. Default, `false`. * @default false */ lazy: boolean; /** * Specifies which icon to use on `md` mode. */ md?: string; /** * The mode determines which platform styles to use. * @default getIonMode() */ mode: string; /** * Specifies which icon to use from the built-in set of icons. */ name?: string; /** * When set to `false`, SVG content that is HTTP fetched will not be checked if the response SVG content has any `

Ionicons

Default

Mode from html ios mode attribute

Mode set on icon

Colors

Stroke width

Font size

Custom SVGs

Custom SVGs: colors

Custom CSS

Aria

RTL

Default: Non-arrows

Flip: Non-arrows

Auto Flip: arrows

Un-flip: arrows

Auto Flip: chevrons

Un-flip: chevrons

Auto Flip, RTL on components

Sanitized (shouldn't show)

Not Sanitized (should show)

Base64 url

Cheatsheet

================================================ FILE: src/index.ts ================================================ export { setAssetPath } from '@stencil/core'; export { addIcons } from './components/icon/utils'; export type { Components, JSX } from './components'; ================================================ FILE: src/ionicons.web-types.json ================================================ { "$schema": "http://json.schemastore.org/web-types", "name": "ionicons", "version": "0.0.0", "description-markup": "markdown", "contributions": { "html": { "elements": [ { "name": "ion-icon", "description": "The Ionicons Web Component is an easy and performant way to use Ionicons in your app. The component will dynamically load an SVG for each icon, so your app is only requesting the icons that you need.", "doc-url": "https://ionic.io/ionicons/usage", "attributes": [ { "name": "name", "description": "The name of the built-in icon from the Ionicons package.", "priority": "high", "doc-url": "https://ionic.io/ionicons/usage#basic-usage", "value": { "type": "symbol" }, "values": [ { "name": "The ion-icon", "pattern": { "items": "ionicons" } } ] }, { "name": "src", "priority": "high", "description": "Provide url of a custom SVG icon. The `src` attribute works the same as `` in that the url must be accessible from the webpage that's making a request for the image. Additionally, the external file can only be a valid `svg` and does not allow scripts or events within the `svg` element.", "doc-url": "https://ionic.io/ionicons/usage#basic-usage" }, { "name": "md", "description": "Provide icon variant specific to MD platform", "priority": "high", "doc-url": "https://ionic.io/ionicons/usage#basic-usage", "value": { "type": "symbol" }, "values": [ { "name": "The ion-icon", "pattern": { "items": "ionicons" } } ] }, { "name": "ios", "description": "Provide icon variant specific to iOS platform", "priority": "high", "doc-url": "https://ionic.io/ionicons/usage#basic-usage", "value": { "type": "symbol" }, "values": [ { "name": "The ion-icon", "pattern": { "items": "ionicons" } } ] }, { "name": "size", "description": "Specify the icon size using one of the pre-defined font sizes.\n\nAlternatively you can set a specific size by applying the `font-size` CSS property on the `ion-icon` component. It's recommended to use pixel sizes that are a multiple of 8 (8, 16, 32, 64, etc.)", "doc-url": "https://ionic.io/ionicons/usage#basic-usage", "priority": "high", "value": { "type": "enum" }, "values": [ { "name": "small" }, { "name": "large" } ] } ] } ], "ionicons": [ ] } } } ================================================ FILE: src/utils/test/playwright/index.ts ================================================ export * from './playwright-page'; export * from './playwright-declarations'; ================================================ FILE: src/utils/test/playwright/page/utils/goto.ts ================================================ import type { Page, TestInfo } from '@playwright/test'; /** * This is an extended version of Playwright's * page.goto method. In addition to performing * the normal page.goto work, this code also * automatically waits for the Stencil components * to be hydrated before proceeding with the test. */ export const goto = async (page: Page, url: string, options: any, _: TestInfo, originalFn: typeof page.goto) => { const result = await Promise.all([ page.waitForFunction(() => (window as any).testAppLoaded === true, { timeout: 4750 }), originalFn(url, options), ]); return result[1]; }; ================================================ FILE: src/utils/test/playwright/page/utils/index.ts ================================================ export * from './goto'; ================================================ FILE: src/utils/test/playwright/playwright-declarations.ts ================================================ import type { Page, Response } from '@playwright/test'; export interface E2EPage extends Page { /** * Returns the main resource response. In case of multiple redirects, the navigation will resolve with the response of the * last redirect. * * The method will throw an error if: * - there's an SSL error (e.g. in case of self-signed certificates). * - target URL is invalid. * - the `timeout` is exceeded during navigation. * - the remote server does not respond or is unreachable. * - the main resource failed to load. * * The method will not throw an error when any valid HTTP status code is returned by the remote server, including 404 "Not * Found" and 500 "Internal Server Error". The status code for such responses can be retrieved by calling * [response.status()](https://playwright.dev/docs/api/class-response#response-status). * * > NOTE: The method either throws an error or returns a main resource response. The only exceptions are navigation to * `about:blank` or navigation to the same URL with a different hash, which would succeed and return `null`. * > NOTE: Headless mode doesn't support navigation to a PDF document. See the * [upstream issue](https://bugs.chromium.org/p/chromium/issues/detail?id=761295). * * Shortcut for main frame's [frame.goto(url[, options])](https://playwright.dev/docs/api/class-frame#frame-goto) * @param url URL to navigate page to. The url should include scheme, e.g. `https://`. When a `baseURL` via the context options was provided and the passed URL is a path, it gets merged via the * [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. */ goto: ( url: string, options?: { /** * Referer header value. If provided it will take preference over the referer header value set by * [page.setExtraHTTPHeaders(headers)](https://playwright.dev/docs/api/class-page#page-set-extra-http-headers). */ referer?: string; /** * Maximum operation time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be * changed by using the * [browserContext.setDefaultNavigationTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-navigation-timeout), * [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout), * [page.setDefaultNavigationTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout) * or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods. */ timeout?: number; /** * When to consider operation succeeded, defaults to `load`. Events can be either: * - `'domcontentloaded'` - consider operation to be finished when the `DOMContentLoaded` event is fired. * - `'load'` - consider operation to be finished when the `load` event is fired. * - `'networkidle'` - consider operation to be finished when there are no network connections for at least `500` ms. * - `'commit'` - consider operation to be finished when network response is received and the document started loading. */ waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit'; }, ) => Promise; } ================================================ FILE: src/utils/test/playwright/playwright-page.ts ================================================ import type { PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestInfo, } from '@playwright/test'; import { test as base } from '@playwright/test'; import { goto as goToPage } from './page/utils'; import type { E2EPage } from './playwright-declarations'; type CustomTestArgs = PlaywrightTestArgs & PlaywrightTestOptions & PlaywrightWorkerArgs & PlaywrightWorkerOptions & { page: E2EPage; }; type CustomFixtures = { page: E2EPage; }; /** * Extends the base `page` test figure within Playwright. * @param page The page to extend. * @param testInfo The test info. * @returns The modified playwright page with extended functionality. */ export async function extendPageFixture(page: E2EPage, testInfo: TestInfo) { const originalGoto = page.goto.bind(page); /** * Adds a global flag on the window that the test suite * can use to determine when it is safe to execute tests * on hydrated Stencil components. */ await page.addInitScript(` (function() { window.addEventListener('appload', () => { window.testAppLoaded = true; }); })();`); // Overridden Playwright methods page.goto = (url: string, options) => goToPage(page, url, options, testInfo, originalGoto); return page; } export const test = base.extend({ page: async ({ page }: CustomTestArgs, use: (r: E2EPage) => Promise, testInfo: TestInfo) => { page = await extendPageFixture(page, testInfo); await use(page); }, }); ================================================ FILE: stencil.config.ts ================================================ import { Config } from '@stencil/core'; export const config: Config = { namespace: 'ionicons', sourceMap: false, outputTargets: [ { type: 'dist', collectionDir: './collection', empty: false, }, { type: 'dist-custom-elements', externalRuntime: false, dir: './components', }, { type: 'docs-readme', }, { type: 'www', copy: [ { src: './components/test/*.svg', dest: './assets/', }, { src: './components/test/', dest: './test/', }, { src: './svg/*.svg', dest: './svg/', }, ], empty: false, serviceWorker: false, }, ], }; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "baseUrl": ".", "strict": true, "allowSyntheticDefaultImports": true, "allowUnreachableCode": false, "declaration": false, "experimentalDecorators": true, "lib": ["dom", "es2017"], "moduleResolution": "bundler", "module": "esnext", "target": "es2017", "noUnusedLocals": true, "noUnusedParameters": true, "jsx": "react", "jsxFactory": "h", "paths": { "@utils/*": ["src/utils/*"] } }, "include": ["src"], "exclude": ["node_modules", "src/utils/test"] } ================================================ FILE: tsconfig.spec.json ================================================ { "extends": "tsconfig.json", "exclude": ["node_modules"] }