├── .changeset ├── README.md └── config.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1-documentation_change.yml │ ├── 2-feature_request.yml │ ├── 3-bug_report.yml │ └── config.yml ├── reproduire │ └── needs-reproduction.md └── workflows │ ├── build-preview.yml │ ├── ci.yml │ ├── deploy-docs.yml │ ├── deploy-preview.yml │ ├── deploy-prod.yml │ ├── preview-release.yml │ ├── release.yml │ ├── reproduire-close.yml │ └── reproduire.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── README.md ├── docs ├── .gitignore ├── .npmrc ├── README.md ├── mdsx.config.js ├── package.json ├── scripts │ ├── build-search-data.js │ └── update-velite-output.js ├── src │ ├── app.css │ ├── app.d.ts │ ├── app.html │ ├── content │ │ ├── components │ │ │ ├── barqode-dropzone.md │ │ │ └── barqode-stream.md │ │ ├── demos │ │ │ └── full-demo.md │ │ ├── getting-started.md │ │ └── index.md │ ├── lib │ │ ├── components │ │ │ ├── blueprint.svelte │ │ │ ├── demos │ │ │ │ ├── barqode-dropzone.svelte │ │ │ │ ├── barqode-stream.svelte │ │ │ │ └── full-demo.svelte │ │ │ └── logos │ │ │ │ ├── logo-dark.svelte │ │ │ │ └── logo-light.svelte │ │ ├── index.ts │ │ ├── navigation.ts │ │ ├── site-config.ts │ │ └── utils.ts │ └── routes │ │ ├── (docs) │ │ ├── +layout.svelte │ │ └── docs │ │ │ ├── +page.svelte │ │ │ ├── +page.ts │ │ │ └── [...slug] │ │ │ ├── +page.svelte │ │ │ └── +page.ts │ │ ├── (landing) │ │ ├── +page.svelte │ │ └── +page.ts │ │ ├── +layout.svelte │ │ └── api │ │ └── search.json │ │ ├── +server.ts │ │ └── search.json ├── static │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── og.png │ └── site.webmanifest ├── svelte.config.js ├── tsconfig.json ├── velite.config.js └── vite.config.ts ├── eslint.config.js ├── package.json ├── packages └── barqode │ ├── .gitignore │ ├── .npmrc │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── app.d.ts │ ├── app.html │ └── lib │ │ ├── components │ │ ├── barqode-dropzone.svelte │ │ ├── barqode-stream.svelte │ │ └── types.ts │ │ ├── index.ts │ │ └── internal │ │ ├── callforth.ts │ │ ├── camera.ts │ │ ├── errors.ts │ │ ├── global.d.ts │ │ ├── scanner.ts │ │ ├── shim-get-user-media.ts │ │ └── util.ts │ ├── svelte.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── tsconfig.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": [ 4 | "@svitejs/changesets-changelog-github-compact", 5 | { "repo": "svecosystem/barqode" } 6 | ], 7 | "commit": false, 8 | "fixed": [], 9 | "linked": [], 10 | "access": "public", 11 | "baseBranch": "main", 12 | "updateInternalDependencies": "patch", 13 | "ignore": ["@barqode/docs"] 14 | } 15 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [huntabyte] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: ollema 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-documentation_change.yml: -------------------------------------------------------------------------------- 1 | name: Report Docs Issue 2 | description: Suggest an addition or modification to the documentation 3 | labels: ["documentation"] 4 | body: 5 | - type: dropdown 6 | attributes: 7 | label: Change Type 8 | description: What type of change are you proposing? 9 | options: 10 | - Addition 11 | - Correction 12 | - Removal 13 | - Cleanup (formatting, typos, etc.) 14 | validations: 15 | required: true 16 | 17 | - type: textarea 18 | attributes: 19 | label: Proposed Changes 20 | description: Describe the proposed changes and why they are necessary 21 | validations: 22 | required: true 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🛠️ Request New Feature 2 | description: Let us know what you would like to see added. 3 | labels: ["enhancement"] 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Describe the feature in detail (code, mocks, or screenshots encouraged) 9 | validations: 10 | required: true 11 | - type: dropdown 12 | id: category 13 | attributes: 14 | label: What type of pull request would this be? 15 | options: 16 | - "New Feature" 17 | - "Enhancement" 18 | - "Guide" 19 | - "Docs" 20 | - "Other" 21 | - type: textarea 22 | id: references 23 | attributes: 24 | label: Provide relevant links or additional information. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "🐛 Bug report" 2 | description: Report an issue with Barqode 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | ### Thanks for taking the time to create an issue! Please search open/closed issues before submitting, as the issue may have already been reported/addressed. 8 | - type: markdown 9 | attributes: 10 | value: | 11 | #### If you aren't sure if something is a bug or not, please do not create an issue, instead ask in one of the following channels: 12 | - [Discussions](https://github.com/svecosystem/barqode/discussions/new?category=help) 13 | - [Discord](https://discord.gg/FKR4YhFbvB) 14 | - type: textarea 15 | id: bug-description 16 | attributes: 17 | label: Describe the bug 18 | description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us how in the description. Thanks! 19 | placeholder: Bug description 20 | validations: 21 | required: true 22 | - type: textarea 23 | id: reproduction 24 | attributes: 25 | label: Reproduction 26 | description: | 27 | Please provide a link to a repo or StackBlitz that can reproduce the problem you ran into. If a report is vague (e.g. just a generic error message) and has no reproduction, it will receive a "needs reproduction" label. If no reproduction is provided within a reasonable time-frame, the issue will be closed. 28 | placeholder: Reproduction 29 | validations: 30 | required: true 31 | - type: textarea 32 | id: logs 33 | attributes: 34 | label: Logs 35 | description: "Please include browser console and server logs around the time this bug occurred. Optional if provided reproduction. Please try not to insert an image but copy paste the log text." 36 | render: bash 37 | - type: textarea 38 | id: system-info 39 | attributes: 40 | label: System Info 41 | description: Output of `npx envinfo --system --npmPackages svelte,barqode,@sveltejs/kit --binaries --browsers` 42 | render: bash 43 | placeholder: System, Binaries, Browsers 44 | validations: 45 | required: true 46 | - type: dropdown 47 | id: severity 48 | attributes: 49 | label: Severity 50 | description: Select the severity of this issue 51 | options: 52 | - annoyance 53 | - blocking an upgrade 54 | - blocking all usage of Barqode 55 | validations: 56 | required: true 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Get Help 4 | url: https://github.com/svecosystem/barqode/discussions/new?category=help 5 | about: If you can't get something to work the way you expect, open a question in our discussion forums. 6 | - name: Discord 7 | url: https://discord.gg/WMa8MjNyCz 8 | about: If you need to have a back-and-forth conversation, join the Discord server. 9 | -------------------------------------------------------------------------------- /.github/reproduire/needs-reproduction.md: -------------------------------------------------------------------------------- 1 | Please provide a reproduction. 2 | 3 |
4 | More info 5 | 6 | ### Why do I need to provide a reproduction? 7 | 8 | This project is maintained by a very small team, and we simply don't have the bandwidth to investigate issues that we can't easily replicate. Reproductions enable us to fix issues faster and more efficiently. If you care about getting your issue resolved, providing a reproduction is the best way to do that. 9 | 10 | ### I've provided a reproduction - what happens now? 11 | 12 | Once a reproduction is provided, we'll remove the `needs reproduction` label and review the issue to determine how to resolve it. If we can confirm it's a bug, we'll label it as such and prioritize it based on its severity. 13 | 14 | If `needs reproduction` labeled issues don't receive any activity (e.g., a comment with a reproduction link), they'll be closed. Feel free to comment with a reproduction at any time and the issue will be reopened. 15 | 16 | ### How can I create a reproduction? 17 | 18 | You can use [this template](https://bits-ui.com/repro) to create a minimal reproduction. You can also link to a GitHub repository with the reproduction. 19 | 20 | Please ensure that the reproduction is as **minimal** as possible. If there is a ton of custom logic in your reproduction, it is difficult to determine if the issue is with your code or with the library. The more minimal the reproduction, the more likely it is that we'll be able to assist. 21 | 22 | You might also find these other articles interesting and/or helpful: 23 | 24 | - [The Importance of Reproductions](https://antfu.me/posts/why-reproductions-are-required) 25 | - [How to Generate a Minimal, Complete, and Verifiable Example](https://stackoverflow.com/help/mcve) 26 | 27 |
28 | -------------------------------------------------------------------------------- /.github/workflows/build-preview.yml: -------------------------------------------------------------------------------- 1 | name: Build Preview Deployment 2 | 3 | # cancel in-progress runs on new commits to same PR (github.event.number) 4 | concurrency: 5 | group: ${{ github.workflow }}-${{ github.event.number || github.sha }} 6 | cancel-in-progress: true 7 | 8 | on: 9 | pull_request: 10 | types: [opened, synchronize] 11 | 12 | jobs: 13 | build-preview: 14 | runs-on: macos-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: pnpm/action-setup@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | cache: pnpm 22 | 23 | - name: Install dependencies 24 | run: pnpm install 25 | 26 | - name: Build site 27 | run: pnpm build 28 | 29 | - name: Upload build artifact 30 | uses: actions/upload-artifact@v4 31 | with: 32 | name: preview-build 33 | path: docs/.svelte-kit/cloudflare 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.number || github.sha }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | check: 15 | name: Run svelte-check 16 | runs-on: macos-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: pnpm/action-setup@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 20 23 | cache: pnpm 24 | 25 | - name: Install dependencies 26 | run: pnpm install 27 | 28 | - name: Build 29 | run: pnpm build:packages 30 | 31 | - name: Sync 32 | run: pnpm sync 33 | 34 | - name: Run svelte-check 35 | run: pnpm check 36 | 37 | test: 38 | runs-on: macos-latest 39 | name: Test 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: pnpm/action-setup@v4 43 | - uses: actions/setup-node@v4 44 | with: 45 | node-version: 20 46 | cache: pnpm 47 | 48 | - name: Install dependencies 49 | run: pnpm install 50 | 51 | - run: pnpm test 52 | 53 | lint: 54 | runs-on: macos-latest 55 | name: Lint 56 | steps: 57 | - uses: actions/checkout@v4 58 | - uses: pnpm/action-setup@v4 59 | - uses: actions/setup-node@v4 60 | with: 61 | node-version: 20 62 | cache: pnpm 63 | 64 | - name: Install dependencies 65 | run: pnpm install 66 | 67 | - run: pnpm lint 68 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | deploy-production: 7 | runs-on: macos-latest 8 | permissions: 9 | contents: read 10 | deployments: write 11 | name: Manual Docs Deployment 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: pnpm/action-setup@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | cache: pnpm 19 | 20 | - name: Install dependencies 21 | run: pnpm install 22 | 23 | - name: Build package & site 24 | run: pnpm build 25 | 26 | - name: Deploy to Cloudflare Pages 27 | uses: AdrianGonz97/refined-cf-pages-action@v1 28 | with: 29 | apiToken: ${{ secrets.CF_API_TOKEN }} 30 | accountId: ${{ secrets.CF_ACCOUNT_ID }} 31 | githubToken: ${{ secrets.GITHUB_TOKEN }} 32 | projectName: barqode 33 | directory: ./.svelte-kit/cloudflare 34 | workingDirectory: docs 35 | deploymentName: Production 36 | -------------------------------------------------------------------------------- /.github/workflows/deploy-preview.yml: -------------------------------------------------------------------------------- 1 | name: Upload Preview Deployment 2 | 3 | on: 4 | workflow_run: 5 | workflows: ['Build Preview Deployment'] 6 | types: 7 | - completed 8 | 9 | permissions: 10 | actions: read 11 | deployments: write 12 | contents: read 13 | pull-requests: write 14 | 15 | jobs: 16 | deploy-preview: 17 | runs-on: macos-latest 18 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 19 | steps: 20 | - name: Download build artifact 21 | uses: actions/download-artifact@v4 22 | id: preview-build-artifact 23 | with: 24 | name: preview-build 25 | path: build 26 | github-token: ${{ secrets.GITHUB_TOKEN }} 27 | run-id: ${{ github.event.workflow_run.id }} 28 | 29 | - name: Deploy to Cloudflare Pages 30 | uses: AdrianGonz97/refined-cf-pages-action@v1 31 | with: 32 | apiToken: ${{ secrets.CF_API_TOKEN }} 33 | accountId: ${{ secrets.CF_ACCOUNT_ID }} 34 | githubToken: ${{ secrets.GITHUB_TOKEN }} 35 | projectName: barqode 36 | deploymentName: Preview 37 | directory: ${{ steps.preview-build-artifact.outputs.download-path }} 38 | -------------------------------------------------------------------------------- /.github/workflows/deploy-prod.yml: -------------------------------------------------------------------------------- 1 | name: Production Deployment 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - docs/** 8 | - packages/barqode/** 9 | workflow_dispatch: 10 | 11 | 12 | jobs: 13 | deploy-production: 14 | runs-on: macos-latest 15 | permissions: 16 | contents: read 17 | deployments: write 18 | name: Deploy Production Site to Cloudflare Pages 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: pnpm/action-setup@v4 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: 20 25 | cache: pnpm 26 | 27 | - name: Install dependencies 28 | run: pnpm install 29 | 30 | - name: Build site 31 | run: pnpm build 32 | 33 | - name: Deploy to Cloudflare Pages 34 | uses: AdrianGonz97/refined-cf-pages-action@v1 35 | with: 36 | apiToken: ${{ secrets.CF_API_TOKEN }} 37 | accountId: ${{ secrets.CF_ACCOUNT_ID }} 38 | githubToken: ${{ secrets.GITHUB_TOKEN }} 39 | projectName: barqode 40 | directory: ./.svelte-kit/cloudflare 41 | workingDirectory: docs 42 | deploymentName: Production 43 | -------------------------------------------------------------------------------- /.github/workflows/preview-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Preview Release 2 | on: 3 | pull_request: 4 | types: [ready_for_review, synchronize, opened, labeled] 5 | paths: [packages/**] 6 | 7 | jobs: 8 | preview-release: 9 | if: github.repository == 'svecosystem/barqode' && contains(github.event.pull_request.labels.*.name, 'publish:preview') 10 | timeout-minutes: 5 11 | runs-on: macos-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: pnpm/action-setup@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | cache: pnpm 19 | 20 | - name: install dependencies 21 | run: pnpm install 22 | 23 | - name: build 24 | run: pnpm build:packages 25 | 26 | - name: publish preview 27 | run: | 28 | pnpx pkg-pr-new@0.0 publish --pnpm --compact './packages/*' 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | permissions: 13 | contents: write # to create release (changesets/action) 14 | pull-requests: write # to create pull request (changesets/action) 15 | deployments: write 16 | name: Release 17 | runs-on: macos-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 22 | fetch-depth: 0 23 | 24 | - uses: pnpm/action-setup@v4 25 | 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | cache: pnpm 30 | 31 | - name: Install dependencies 32 | run: pnpm install 33 | 34 | - name: Create Release Pull Request or Publish to npm 35 | id: changesets 36 | uses: changesets/action@v1 37 | with: 38 | commit: "chore(release): version package" 39 | title: "chore(release): version package" 40 | publish: pnpm ci:publish 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | 45 | - name: Build site 46 | if: steps.changesets.outputs.published == 'true' 47 | run: pnpm -F docs build 48 | 49 | - name: Deploy to Cloudflare Pages 50 | if: steps.changesets.outputs.published == 'true' 51 | uses: AdrianGonz97/refined-cf-pages-action@v1 52 | with: 53 | apiToken: ${{ secrets.CF_API_TOKEN }} 54 | accountId: ${{ secrets.CF_ACCOUNT_ID }} 55 | githubToken: ${{ secrets.GITHUB_TOKEN }} 56 | projectName: barqode 57 | directory: ./.svelte-kit/cloudflare 58 | workingDirectory: docs 59 | deploymentName: Production 60 | -------------------------------------------------------------------------------- /.github/workflows/reproduire-close.yml: -------------------------------------------------------------------------------- 1 | name: Close incomplete issues 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "30 1 * * *" # run every day 6 | 7 | permissions: 8 | issues: write 9 | 10 | jobs: 11 | stale: 12 | runs-on: macos-latest 13 | steps: 14 | - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 15 | with: 16 | days-before-stale: -1 # Issues and PR will never be flagged stale automatically. 17 | stale-issue-label: "needs reproduction" # Label that flags an issue as stale. 18 | only-labels: "needs reproduction" # Only process these issues 19 | days-before-issue-close: 7 20 | ignore-updates: true 21 | remove-stale-when-updated: false 22 | close-issue-message: This issue was closed because it was open for 7 days without a reproduction. 23 | close-issue-label: closed-by-bot 24 | -------------------------------------------------------------------------------- /.github/workflows/reproduire.yml: -------------------------------------------------------------------------------- 1 | name: Reproduire 2 | on: 3 | issues: 4 | types: [labeled] 5 | 6 | permissions: 7 | issues: write 8 | 9 | jobs: 10 | reproduire: 11 | runs-on: macos-latest 12 | steps: 13 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 14 | - uses: Hebilicious/reproduire@4b686ae9cbb72dad60f001d278b6e3b2ce40a9ac # v0.0.9-mp 15 | with: 16 | label: needs reproduction 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | .turbo 12 | 13 | **/.svelte-kit/**/* 14 | 15 | # Logs 16 | logs 17 | *.log 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | lerna-debug.log* 22 | .pnpm-debug.log* 23 | 24 | # Diagnostic reports (https://nodejs.org/api/report.html) 25 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 26 | 27 | # Runtime data 28 | pids 29 | *.pid 30 | *.seed 31 | *.pid.lock 32 | 33 | # Directory for instrumented libs generated by jscoverage/JSCover 34 | lib-cov 35 | 36 | # Coverage directory used by tools like istanbul 37 | coverage 38 | *.lcov 39 | 40 | # nyc test coverage 41 | .nyc_output 42 | 43 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 44 | .grunt 45 | 46 | # Bower dependency directory (https://bower.io/) 47 | bower_components 48 | 49 | # node-waf configuration 50 | .lock-wscript 51 | 52 | # Compiled binary addons (https://nodejs.org/api/addons.html) 53 | build/Release 54 | 55 | # Dependency directories 56 | node_modules/ 57 | jspm_packages/ 58 | 59 | # Snowpack dependency directory (https://snowpack.dev/) 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | *.tsbuildinfo 64 | 65 | # Optional npm cache directory 66 | .npm 67 | 68 | # Optional eslint cache 69 | .eslintcache 70 | 71 | # Optional stylelint cache 72 | .stylelintcache 73 | 74 | # Microbundle cache 75 | .rpt2_cache/ 76 | .rts2_cache_cjs/ 77 | .rts2_cache_es/ 78 | .rts2_cache_umd/ 79 | 80 | # Optional REPL history 81 | .node_repl_history 82 | 83 | # Output of 'npm pack' 84 | *.tgz 85 | 86 | # Yarn Integrity file 87 | .yarn-integrity 88 | 89 | # dotenv environment variable files 90 | .env 91 | .env.development.local 92 | .env.test.local 93 | .env.production.local 94 | .env.local 95 | 96 | 97 | # Stores VSCode versions used for testing VSCode extensions 98 | .vscode-test 99 | 100 | # yarn v2 101 | .yarn/cache 102 | .yarn/unplugged 103 | .yarn/build-state.yml 104 | .yarn/install-state.gz 105 | .pnp.* 106 | packages/cli/dist 107 | dist/ 108 | dist 109 | generated-assets 110 | 111 | # JetBrains IDEs 112 | .idea/ 113 | 114 | .velite 115 | docs/public 116 | docs/.wrangler 117 | start/.wrangler 118 | start/public -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.15.1 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | .github 6 | .github/ 7 | docs/static/**/* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "tabWidth": 4, 4 | "singleQuote": false, 5 | "trailingComma": "es5", 6 | "printWidth": 100, 7 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 8 | "overrides": [ 9 | { 10 | "files": "*.svelte", 11 | "options": { 12 | "parser": "svelte" 13 | } 14 | } 15 | ], 16 | "tailwindFunctions": ["clsx", "cn", "tv"] 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Barqode 2 | 3 | 4 | 5 | [![npm version](https://flat.badgen.net/npm/v/barqode?color=green)](https://npmjs.com/package/barqode) 6 | [![npm downloads](https://flat.badgen.net/npm/dm/barqode?color=green)](https://npmjs.com/package/barqode) 7 | [![license](https://flat.badgen.net/github/license/svecosystem/barqode?color=green)](https://github.com/svecosystem/barqode/blob/main/LICENSE) 8 | 9 | 10 | 11 | Barqode does things. 12 | 13 | ## Installation 14 | 15 | ```bash 16 | npm install barqode 17 | ``` 18 | 19 | Check out the [documentation](https://barqode.sveco.dev) for more information. 20 | 21 | ## Sponsors 22 | 23 | This project is supported by the following beautiful people/organizations: 24 | 25 |

26 | 27 | Logos from Sponsors 28 | 29 |

30 | 31 | ## License 32 | 33 | 34 | 35 | Published under the [MIT](https://github.com/svecosystem/barqode/blob/main/LICENSE) license. Made by 36 | [@ollema](https://github.com/ollema), [@huntabyte](https://github.com/huntabyte) and 37 | [community](https://github.com/svecosystem/barqode/graphs/contributors) 💛

38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | /.svelte-kit 7 | /build 8 | 9 | # OS 10 | .DS_Store 11 | Thumbs.db 12 | 13 | # Env 14 | .env 15 | .env.* 16 | !.env.example 17 | !.env.test 18 | 19 | # Vite 20 | vite.config.js.timestamp-* 21 | vite.config.ts.timestamp-* 22 | -------------------------------------------------------------------------------- /docs/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # sv 2 | 3 | Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npx sv create 12 | 13 | # create a new project in my-app 14 | npx sv create my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /docs/mdsx.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "mdsx"; 2 | import { baseRemarkPlugins, baseRehypePlugins } from "@svecodocs/kit/mdsxConfig"; 3 | import { resolve } from "node:path"; 4 | import { fileURLToPath } from "node:url"; 5 | 6 | const __dirname = fileURLToPath(new URL(".", import.meta.url)); 7 | 8 | export default defineConfig({ 9 | remarkPlugins: [...baseRemarkPlugins], 10 | // @ts-expect-error shh 11 | rehypePlugins: [...baseRehypePlugins], 12 | blueprints: { 13 | default: { 14 | path: resolve(__dirname, "./src/lib/components/blueprint.svelte"), 15 | }, 16 | }, 17 | extensions: [".md"], 18 | }); 19 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@barqode/docs", 3 | "description": "Barqode documentation.", 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "pnpm \"/dev:/\"", 8 | "dev:content": "velite dev --watch", 9 | "dev:svelte": "vite dev", 10 | "build": "velite && node ./scripts/update-velite-output.js && pnpm build:search && vite build", 11 | "build:search": "node ./scripts/build-search-data.js", 12 | "preview": "vite preview", 13 | "check": "velite && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 14 | "check:watch": "pnpm build:content && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" 15 | }, 16 | "devDependencies": { 17 | "@svecodocs/kit": "^0.1.1", 18 | "@sveltejs/adapter-cloudflare": "^4.8.0", 19 | "@sveltejs/kit": "^2.9.0", 20 | "@sveltejs/vite-plugin-svelte": "^4.0.2", 21 | "@tailwindcss/vite": "4.0.0-beta.4", 22 | "barqode": "workspace:^", 23 | "mdsx": "^0.0.6", 24 | "phosphor-svelte": "^3.0.0", 25 | "svelte": "^5.4.0", 26 | "svelte-check": "^4.1.0", 27 | "svelte-preprocess": "^6.0.3", 28 | "tailwindcss": "4.0.0-beta.4", 29 | "typescript": "^5.7.2", 30 | "velite": "^0.2.1", 31 | "vite": "^5.4.11" 32 | }, 33 | "private": true 34 | } 35 | -------------------------------------------------------------------------------- /docs/scripts/build-search-data.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from "node:url"; 2 | import { writeFileSync } from "node:fs"; 3 | import { resolve } from "node:path"; 4 | import { docs } from "../.velite/index.js"; 5 | import { cleanMarkdown } from "../node_modules/@svecodocs/kit/dist/utils.js"; 6 | 7 | const __dirname = fileURLToPath(new URL(".", import.meta.url)); 8 | 9 | export function buildDocsSearchIndex() { 10 | return docs.map((doc) => ({ 11 | title: doc.title, 12 | href: `/docs/${doc.slug}`, 13 | description: doc.description, 14 | content: cleanMarkdown(doc.raw), 15 | })); 16 | } 17 | 18 | const searchData = buildDocsSearchIndex(); 19 | 20 | writeFileSync( 21 | resolve(__dirname, "../src/routes/api/search.json/search.json"), 22 | JSON.stringify(searchData), 23 | { flag: "w" } 24 | ); 25 | -------------------------------------------------------------------------------- /docs/scripts/update-velite-output.js: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from "node:fs/promises"; 2 | import { join } from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | const __dirname = fileURLToPath(new URL(".", import.meta.url)); 6 | const dtsPath = join(__dirname, "../.velite/index.d.ts"); 7 | const indexPath = join(__dirname, "../.velite/index.js"); 8 | 9 | async function replaceContents() { 10 | const data = await readFile(dtsPath, "utf8").catch((err) => { 11 | console.error("Error reading file:", err); 12 | }); 13 | if (!data) return; 14 | 15 | const updatedContent = data.replace("'../velite.config'", "'../velite.config.js'"); 16 | if (updatedContent === data) return; 17 | 18 | await writeFile(dtsPath, updatedContent, "utf8").catch((err) => { 19 | console.error("Error writing file:", err); 20 | }); 21 | } 22 | 23 | async function replaceIndexContents() { 24 | const data = await readFile(indexPath, "utf8").catch((err) => { 25 | console.error("Error reading file:", err); 26 | }); 27 | if (!data) return; 28 | 29 | const updatedContent = data.replaceAll(".json'", ".json' with { type: 'json' }"); 30 | if (updatedContent === data) return; 31 | 32 | await writeFile(indexPath, updatedContent, "utf8").catch((err) => { 33 | console.error("Error writing file:", err); 34 | }); 35 | } 36 | 37 | await replaceContents(); 38 | await replaceIndexContents(); 39 | -------------------------------------------------------------------------------- /docs/src/app.css: -------------------------------------------------------------------------------- 1 | @import "@svecodocs/kit/theme-blue.css"; 2 | @import "@svecodocs/kit/globals.css"; 3 | -------------------------------------------------------------------------------- /docs/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /docs/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/src/content/components/barqode-dropzone.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: BarqodeDropzone 3 | description: Click to upload images, drag & drop or use your camera to scan. 4 | section: Components 5 | --- 6 | 7 | 10 | 11 | This component functions as a file input with the `capture` attribute set to `environment` which allows users to take a picture with their camera. You can also drag-and-drop image files from your desktop or images embedded into other web pages anywhere in the area the component occupies. The images are directly scanned and positive results are indicated by the `onDetect` callback. 12 | 13 | ## Demo 14 | 15 | 16 | 17 | ## Usage 18 | 19 | ```svelte 20 | 34 | 35 |
36 | 37 |

Click to upload or drop an image here

38 |
39 |
40 | 41 | Last detected: {result} 42 | 43 | 48 | ``` 49 | 50 | ## Props 51 | 52 | ### `formats` 53 | 54 | Type: [`BarcodeFormat[]`](https://github.com/Sec-ant/barcode-detector?tab=readme-ov-file#barcode-detector) 55 | 56 | Default: `["qr_code"]` 57 | 58 | Configure the barcode formats to detect. By default, only QR codes are detected. 59 | 60 | If you want to detect multiple formats, pass an array of formats: 61 | 62 | ```svelte 63 | 64 | ``` 65 | 66 | Under the hood, the standard [BarcodeDetector API](https://developer.mozilla.org/en-US/docs/Web/API/BarcodeDetector) is used. Support varies across devices, operating systems and browsers. All components will prefer to use the native implementation if available and otherwise falls back to a polyfill implementation. 67 | 68 | Note that even if the native implementation is available, the component still might use the polyfill. For example, if the native implementation only supports the format `'qr_code'` but the you select the formats `['qr_code', 'aztec']`. 69 | 70 | ### `onDetect` 71 | 72 | Type: `(detectedCodes: DetectedBarcode[]) => void` 73 | 74 | Callback function that is called when a barcode is detected. 75 | 76 | It receives an array of [detected barcodes](https://developer.mozilla.org/en-US/docs/Web/API/BarcodeDetector/detect#return_value), one callback per image. 77 | 78 | If not barcode is detected, the array will be empty. 79 | 80 | ### `onDragover` 81 | 82 | Type: `(isDraggingOver: boolean) => void` 83 | 84 | Callback function that is called when a file is dragged over the drop zone. 85 | 86 | ### `onError` 87 | 88 | Type: `(error: Error) => void` 89 | 90 | Callback function that is called when an error occurs. 91 | 92 | TODO: insert link to errors. 93 | 94 | ### Other props 95 | 96 | The `BarqodeDropzone` component accepts all attributes that a standard `input` element accepts. 97 | 98 | By default, the following attributes are set: 99 | 100 | - `type="file"`. This is required to make the input a file input. You should not change this. 101 | - `name="image"`. This is the name of the file input. 102 | - `accept="image/*"`. This restricts the file types that can be uploaded to images. 103 | - `capture="environment"`. This tells the browser to open the camera when the input is clicked on mobile devices. You can choose between `user` and `environment`, which opens the front and back camera respectively. You can also disable this functionality by setting it to `null`. 104 | - `multiple`. This allows the user to upload multiple files at once. You can disable this by settings this to `false`. 105 | 106 | ## Browser Support 107 | 108 | This component depends on the [File Reader API](https://developer.mozilla.org/en-US/docs/Web/API/FileReader) which is widely supported in modern browsers. 109 | -------------------------------------------------------------------------------- /docs/src/content/components/barqode-stream.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: BarqodeStream 3 | description: Continuously scans frames from a camera stream. 4 | section: Components 5 | --- 6 | 7 | 11 | 12 | The `BarqodeStream` component continuously scans frames from a camera stream and detects barcodes in real-time. 13 | 14 | ## Demo 15 | 16 | 17 | 18 | ## Usage 19 | 20 | ```svelte 21 | 54 | 55 |
56 | 57 | {#if loading} 58 |
Loading...
59 | {/if} 60 |
61 |
62 | 63 | Last detected: {result} 64 | ``` 65 | 66 | ## Props 67 | 68 | ### `constraints` 69 | 70 | Type: [`MediaTrackConstraints`](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints) 71 | 72 | Default: `{ video: { facingMode: "environment" } }` 73 | 74 | Configure the the various camera options, for example whether to use front or rear camera. 75 | 76 | The object must be of type `MediaTrackConstraints`. 77 | 78 | The object is passed as-is to `getUserMedia`, which is the API call for requesting a camera stream: 79 | 80 | ```js 81 | navigator.mediaDevices.getUserMedia({ 82 | audio: false, 83 | video: the_constraint_object_you_provide, 84 | }); 85 | ``` 86 | 87 | When `constraints` is updated, a new camera stream is requested which triggers the `onCameraOn` callback again. You can catch errors with the `onError` callback. An error can occur when you try to use the front camera on a device that doesn't have one for example. 88 | 89 | ### `formats` 90 | 91 | Type: [`BarcodeFormat[]`](https://github.com/Sec-ant/barcode-detector?tab=readme-ov-file#barcode-detector) 92 | 93 | Default: `["qr_code"]` 94 | 95 | Configure the barcode formats to detect. By default, only QR codes are detected. 96 | 97 | If you want to detect multiple formats, pass an array of formats: 98 | 99 | ```svelte 100 | 101 | ``` 102 | 103 | 104 | 105 | Don't select more barcode formats than needed. 106 | 107 | Scanning becomes more expensive the more formats you select. 108 | 109 | 110 | 111 | Under the hood, the standard [BarcodeDetector API](https://developer.mozilla.org/en-US/docs/Web/API/BarcodeDetector) is used. Support varies across devices, operating systems and browsers. All components will prefer to use the native implementation if available and otherwise falls back to a polyfill implementation. 112 | 113 | Note that even if the native implementation is available, the component still might use the polyfill. For example, if the native implementation only supports the format `'qr_code'` but the you select the formats `['qr_code', 'aztec']`. 114 | 115 | ### `paused` 116 | 117 | Type: `boolean` (bindable) 118 | 119 | Default: `false` 120 | 121 | Whether the camera stream is or should be paused. Bindable, which means that you can pause/unpause the stream from outside the component. 122 | 123 | Pausing the stream by setting `paused` to `true` is useful if you want to show some microinteraction after successful scans. When the you set it to `false`, the camera stream will be restarted and the `onCameraOn` callback function will be triggered again. 124 | 125 | ### `torch` 126 | 127 | Type: `boolean` 128 | 129 | Default: `false` 130 | 131 | Turn the camera flashlight on or off. 132 | 133 | This is not consistently supported by all devices and browsers. Support can even vary on the same device with the same browser. For example the rear camera often has a flashlight but the front camera does not. 134 | 135 | We can only tell if flashlight control is supported once the camera is loaded and the `onCameraOn` callback has been called. At the moment, enabling the torch may silently fail on unsupported devices, but in the `onCameraOn` callback payload you can access the `MediaTrackCapabilities` object, from which you can determine if the torch is supported. 136 | 137 | The camera stream must be reloaded when turning the torch on or off. That means the `onCameraOn` event will be emitted again. 138 | 139 | ### `track` 140 | 141 | Type: `(detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) => void` 142 | 143 | Callback function that can be used to visually highlight detected barcodes. 144 | 145 | A transparent canvas is overlaid on top of the camera stream. The `track` function is used to draw on this canvas. 146 | 147 | It receives an array of [detected barcodes](https://developer.mozilla.org/en-US/docs/Web/API/BarcodeDetector/detect#return_value) and a [CanvasRenderingContext2D](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) as the second argument. 148 | 149 | Note that when `track` is set the scanning frequency has to be increased. So if you want to go easy on your target device you might not want to enable tracking. 150 | 151 | 152 | 153 | The `track` function is called for every frame. It is important to keep the function as performant as possible. 154 | 155 | This can lead to performance issues on low-end devices and memory leaks if not handled correctly. 156 | 157 | 158 | 159 | ### `onCameraOn` 160 | 161 | Type: `(capabilities: MediaTrackCapabilities) => void` 162 | 163 | Callback function that is called when the camera stream is successfully loaded. 164 | 165 | It might take a while before the component is ready and the scanning process starts. The user has to be asked for camera access permission first and the camera stream has to be loaded. 166 | 167 | If you want to show a loading indicator, you can wait for this callback to be called. It is called as soon as the camera start streaming. 168 | 169 | The callback receives the a promise which resolves with the cameras [`MediaTrackCapabilities`](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackCapabilities) when everything is ready. 170 | 171 | ### `onError` 172 | 173 | Type: `(error: Error) => void` 174 | 175 | Callback function that is called when an error occurs. 176 | 177 | TODO: insert link to errors. 178 | 179 | ### `onCameraOff` 180 | 181 | Type: `() => void` 182 | 183 | Callback function that is called when the camera stream is stopped. 184 | 185 | This can happen when the camera constraints are modified, for example when switching between front and rear camera or when turning the torch on or off. 186 | 187 | ### `onDetect` 188 | 189 | Type: `(detectedCodes: DetectedBarcode[]) => void` 190 | 191 | Callback function that is called when a barcode is detected. 192 | 193 | It receives an array of [detected barcodes](https://developer.mozilla.org/en-US/docs/Web/API/BarcodeDetector/detect#return_value). 194 | 195 | 196 | 197 | If you scan the same barcode multiple times in a row, `onDetect` is still only called once. When you hold a barcode in the camera, frames are actually decoded multiple times a second but you don't want to be flooded with callbacks that often. That's why the last decoded QR code is always cached and only new results are propagated. However, changing the value of `paused` resets this internal cache. 198 | 199 | 200 | 201 | ## Browser Support 202 | 203 | This component depends on the [Media Capture and Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API) which is widely supported in modern browsers. 204 | -------------------------------------------------------------------------------- /docs/src/content/demos/full-demo.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Full demo 3 | description: Kitchen sink demo of the BarqodeStream component. 4 | section: Demos 5 | --- 6 | 7 | 10 | 11 | Modern mobile phones often have a variety of different cameras installed (e.g. front, rear, 12 | wide-angle, infrared, desk-view). The one picked by default is sometimes not the best 13 | choice. For more fine-grained control, you can select a camera by device constraints or by 14 | the device ID. 15 | 16 | Detected codes are visually highlighted in real-time. In this demo you can use the track function dropdown to change the flavor. 17 | 18 | By default only QR-codes are detected but a variety of other barcode formats are also supported. 19 | You can select one or multiple but the more you select the more expensive scanning becomes. 20 | 21 | ## Demo 22 | 23 | 24 | 25 | ## Usage 26 | 27 | ```svelte 28 | 209 | 210 | 211 | 218 | 219 | 220 | 227 | 228 | 229 | {#each Object.keys(barcodeFormats) as option} 230 | {@const barcodeOption = option as BarcodeFormat} 231 |
232 | 233 | 234 |
235 | {/each} 236 | 237 | {#if error} 238 | {error} 239 | {/if} 240 | 241 |
242 | 250 |
251 | 252 | Last result: {result} 253 | ``` 254 | -------------------------------------------------------------------------------- /docs/src/content/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | description: A quick guide to get started using barqode 4 | section: Overview 5 | --- 6 | 7 | The following guide will walk you through installing and setting up the barqode components in your Svelte project. 8 | 9 | ## Installation 10 | 11 | Install the package using your preferred package manager: 12 | 13 | ```bash 14 | npm install barqode 15 | ``` 16 | 17 | ## Basic Usage 18 | 19 | The simplest way to use the library is with the `BarqodeStream` component for live scanning: 20 | 21 | ```svelte 22 | 29 | 30 |
31 | 32 |
33 | 34 | 41 | ``` 42 | 43 | For detailed information about each component's capabilities and options, refer to their respective API documentation pages. 44 | -------------------------------------------------------------------------------- /docs/src/content/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: What is Barqode? 4 | section: Overview 5 | --- 6 | 7 | Barqode provides a set of Svelte components for detecting and decoding QR codes and various other barcode formats right in the browser. It's a comprehensive solution that supports both camera streams and static image processing. 8 | 9 | Barqode started out as a port of [`vue-qrcode-reader`](https://github.com/gruhn/vue-qrcode-reader). 10 | 11 | ## Components 12 | 13 | - **`BarqodeStream`** - continuously scans frames from a camera stream. 14 | - **`BarqodeDropzone`** - drag & drop, click to upload or capture images from camera. 15 | 16 | ## Features 17 | 18 | - **Real-time scanning**. Detect codes from live camera stream. 19 | - **Multiple formats**. Support for QR codes and various barcode standards. 20 | - **Visual feedback**. Customizable canvas based tracking of detected codes. 21 | - **Cross-browser**. Works across modern browsers with a [WebAssembly-based polyfill](https://github.com/Sec-ant/barcode-detector) if needed. 22 | - **Camera selection**. Choose between front/rear cameras. 23 | - **Torch control**. Control device flashlight where supported. 24 | - **Responsive**. Components adapt to fill available space. 25 | - **Error handling**. Comprehensive error handling for camera/detection issues. 26 | 27 | ## Usage Example 28 | 29 | ```svelte 30 | 37 | 38 |
39 | 40 |
41 | 42 | 49 | ``` 50 | 51 | ## Browser Support 52 | 53 | The components rely primarily on the [Barcode Detection API](https://developer.mozilla.org/en-US/docs/Web/API/Barcode_Detection_API) and [Media Capture and Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API), with fallbacks where possible. 54 | 55 | While the core scanning functionality uses the native `BarcodeDetector` where available, it falls back to a [WebAssembly-based polyfill](https://github.com/Sec-ant/barcode-detector) to ensure consistent behavior across browsers. 56 | 57 | For a detailed compatibility overview, check each component's documentation. 58 | -------------------------------------------------------------------------------- /docs/src/lib/components/blueprint.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 10 | {@render children?.()} 11 | -------------------------------------------------------------------------------- /docs/src/lib/components/demos/barqode-dropzone.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 |
Detecting QR-codes
19 | 20 |
26 | 27 |
28 |

Click to upload or drop an image here

29 |
30 |
31 |
32 | 33 |
34 | Last detected: {result} 35 |
36 |
37 | -------------------------------------------------------------------------------- /docs/src/lib/components/demos/barqode-stream.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | 38 |
Detecting QR-codes
39 | 40 |
41 | 42 | {#if loading} 43 |
44 | 45 |
46 | {/if} 47 |
48 |
49 | 50 |
51 | Last detected: {result} 52 |
53 |
54 | -------------------------------------------------------------------------------- /docs/src/lib/components/demos/full-demo.svelte: -------------------------------------------------------------------------------- 1 | 185 | 186 | 187 |
188 | 189 | 194 | {#each constraintOptions as option} 195 | 198 | {/each} 199 | 200 |
201 | 202 |
203 | 204 | 209 | {#each trackFunctionOptions as option} 210 | 213 | {/each} 214 | 215 |
216 | 217 |
218 | Barcode formats: 219 |
220 | {#each Object.keys(barcodeFormats) as option} 221 | {@const barcodeOption = option as BarcodeFormat} 222 |
223 | 224 | 225 |
226 | {/each} 227 |
228 |
229 | 230 | {#if error} 231 |
{error}
232 | {/if} 233 | 234 |
235 | 243 |
244 | 245 |
246 | Last detected: {result} 247 |
248 |
249 | -------------------------------------------------------------------------------- /docs/src/lib/components/logos/logo-dark.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 14 | 18 | 22 | 26 | 30 | 34 | 38 | 42 | 48 | 49 | -------------------------------------------------------------------------------- /docs/src/lib/components/logos/logo-light.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 14 | 18 | 22 | 26 | 30 | 34 | 38 | 42 | 48 | 49 | -------------------------------------------------------------------------------- /docs/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /docs/src/lib/navigation.ts: -------------------------------------------------------------------------------- 1 | import { defineNavigation } from "@svecodocs/kit"; 2 | import ChalkboardTeacher from "phosphor-svelte/lib/ChalkboardTeacher"; 3 | import RocketLaunch from "phosphor-svelte/lib/RocketLaunch"; 4 | import Tag from "phosphor-svelte/lib/Tag"; 5 | import { getAllDocs } from "./utils.js"; 6 | 7 | const allDocs = getAllDocs(); 8 | 9 | const components = allDocs 10 | .filter((doc) => doc.section === "Components") 11 | .map((doc) => ({ 12 | title: doc.title, 13 | href: `/docs/${doc.slug}`, 14 | })); 15 | 16 | const demos = allDocs 17 | .filter((doc) => doc.section === "Demos") 18 | .map((doc) => ({ 19 | title: doc.title, 20 | href: `/docs/${doc.slug}`, 21 | })); 22 | 23 | export const navigation = defineNavigation({ 24 | anchors: [ 25 | { 26 | title: "Introduction", 27 | href: "/docs", 28 | icon: ChalkboardTeacher, 29 | }, 30 | { 31 | title: "Getting Started", 32 | href: "/docs/getting-started", 33 | icon: RocketLaunch, 34 | }, 35 | { 36 | title: "Releases", 37 | href: "https://github.com/svecosystem/barqode/releases", 38 | icon: Tag, 39 | }, 40 | ], 41 | sections: [ 42 | { 43 | title: "Components", 44 | items: components.sort((a, b) => { 45 | if (a.title === "BarqodeStream") return -1; 46 | if (b.title === "BarqodeStream") return 1; 47 | return a.title.localeCompare(b.title); 48 | }), 49 | }, 50 | { 51 | title: "Demos", 52 | items: demos, 53 | }, 54 | ], 55 | }); 56 | -------------------------------------------------------------------------------- /docs/src/lib/site-config.ts: -------------------------------------------------------------------------------- 1 | import { defineSiteConfig } from "@svecodocs/kit"; 2 | 3 | export const siteConfig = defineSiteConfig({ 4 | name: "Barqode", 5 | url: "https://barqode.sveco.dev", 6 | ogImage: { 7 | url: "https://barqode.sveco.dev/og.png", 8 | height: "630", 9 | width: "1200", 10 | }, 11 | description: "QR and barcode detection for Svelte.", 12 | author: "ollema", 13 | keywords: [ 14 | "svelte qr code reader", 15 | "svelte barcodes", 16 | "svelte qr", 17 | "svelte qr scanner", 18 | "svelte qr code scanner", 19 | "svelte qr codes", 20 | "svecosystem", 21 | "sveltekit", 22 | "documentation", 23 | "docs", 24 | ], 25 | license: { 26 | name: "MIT", 27 | url: "https://github.com/svecosystem/barqode/blob/main/LICENSE", 28 | }, 29 | links: { 30 | github: "https://github.com/svecosystem/barqode", 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /docs/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { docs, type Doc } from "$content/index.js"; 2 | import { error } from "@sveltejs/kit"; 3 | import type { Component } from "svelte"; 4 | 5 | export function getDocMetadata(slug: string = "index") { 6 | return docs.find((doc) => doc.slug === slug); 7 | } 8 | 9 | export function getAllDocs() { 10 | return docs; 11 | } 12 | 13 | function slugFromPath(path: string) { 14 | return path.replace("/src/content/", "").replace(".md", ""); 15 | } 16 | 17 | export type DocResolver = () => Promise<{ default: Component; metadata: Doc }>; 18 | 19 | export async function getDoc(slug: string = "index") { 20 | const modules = import.meta.glob("/src/content/**/*.md"); 21 | 22 | let match: { path?: string; resolver?: DocResolver } = {}; 23 | 24 | for (const [path, resolver] of Object.entries(modules)) { 25 | if (slugFromPath(path) === slug) { 26 | match = { path, resolver: resolver as unknown as DocResolver }; 27 | break; 28 | } 29 | } 30 | const doc = await match?.resolver?.(); 31 | const metadata = getDocMetadata(slug); 32 | if (!doc || !metadata) { 33 | error(404, "Could not find the document."); 34 | } 35 | 36 | return { 37 | component: doc.default, 38 | metadata, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /docs/src/routes/(docs)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | {#snippet logo()} 12 | 19 | -------------------------------------------------------------------------------- /docs/src/routes/(docs)/docs/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/src/routes/(docs)/docs/+page.ts: -------------------------------------------------------------------------------- 1 | import { getDoc } from "$lib/utils"; 2 | 3 | export async function load() { 4 | return getDoc(); 5 | } 6 | -------------------------------------------------------------------------------- /docs/src/routes/(docs)/docs/[...slug]/+page.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/src/routes/(docs)/docs/[...slug]/+page.ts: -------------------------------------------------------------------------------- 1 | import { getDoc } from "$lib/utils"; 2 | 3 | export async function load({ params }) { 4 | return getDoc(params.slug); 5 | } 6 | -------------------------------------------------------------------------------- /docs/src/routes/(landing)/+page.svelte: -------------------------------------------------------------------------------- 1 |

Welcome to SvelteKit

2 |

Visit svelte.dev/docs/kit to read the documentation

3 | -------------------------------------------------------------------------------- /docs/src/routes/(landing)/+page.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from "@sveltejs/kit"; 2 | 3 | export function load() { 4 | redirect(302, "/docs"); 5 | } 6 | -------------------------------------------------------------------------------- /docs/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | {#if !dev} 14 | 19 | {/if} 20 | 21 | 22 | {@render children?.()} 23 | -------------------------------------------------------------------------------- /docs/src/routes/api/search.json/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from "@sveltejs/kit"; 2 | import search from "./search.json" assert { type: "json" }; 3 | 4 | export const prerender = true; 5 | 6 | export const GET: RequestHandler = () => { 7 | return Response.json(search); 8 | }; 9 | -------------------------------------------------------------------------------- /docs/src/routes/api/search.json/search.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "title": "Getting Started", 4 | "href": "/docs/getting-started", 5 | "description": "A quick guide to get started using barqode", 6 | "content": "The following guide will walk you through installing and setting up the barqode components in your Svelte project. Installation Install the package using your preferred package manager: npm install barqode Basic Usage The simplest way to use the library is with the BarqodeStream component for live scanning: import { BarqodeStream, type DetectedBarcode } from \"barqode\"; function onDetect(detectedCodes: DetectedBarcode[]) { console.log(detectedCodes.map((detectedCode) => detectedCode.rawValue)); } .barqode { width: 100%; max-width: 600px; aspect-ratio: 4/3; } For detailed information about each component's capabilities and options, refer to their respective API documentation pages." 7 | }, 8 | { 9 | "title": "Introduction", 10 | "href": "/docs/index", 11 | "description": "What is Barqode?", 12 | "content": "Barqode provides a set of Svelte components for detecting and decoding QR codes and various other barcode formats right in the browser. It's a comprehensive solution that supports both camera streams and static image processing. Barqode started out as a port of $2. Components BarqodeStream** - continuously scans frames from a camera stream. BarqodeDropzone** - drag & drop, click to upload or capture images from camera. Features Real-time scanning**. Detect codes from live camera stream. Multiple formats**. Support for QR codes and various barcode standards. Visual feedback**. Customizable canvas based tracking of detected codes. Cross-browser**. Works across modern browsers with a $2 if needed. Camera selection**. Choose between front/rear cameras. Torch control**. Control device flashlight where supported. Responsive**. Components adapt to fill available space. Error handling**. Comprehensive error handling for camera/detection issues. Usage Example import { BarqodeStream, type DetectedBarcode } from \"barqode\"; function onDetect(detectedCodes: DetectedBarcode[]) { console.log(detectedCodes.map((detectedCode) => detectedCode.rawValue)); } .barqode { width: 100%; max-width: 600px; aspect-ratio: 4/3; } Browser Support The components rely primarily on the $2 and $2, with fallbacks where possible. While the core scanning functionality uses the native BarcodeDetector where available, it falls back to a $2 to ensure consistent behavior across browsers. For a detailed compatibility overview, check each component's documentation." 13 | }, 14 | { 15 | "title": "BarqodeDropzone", 16 | "href": "/docs/components/barqode-dropzone", 17 | "description": "Click to upload images, drag & drop or use your camera to scan.", 18 | "content": " import Demo from '$lib/components/demos/barqode-dropzone.svelte'; This component functions as a file input with the capture attribute set to environment which allows users to take a picture with their camera. You can also drag-and-drop image files from your desktop or images embedded into other web pages anywhere in the area the component occupies. The images are directly scanned and positive results are indicated by the onDetect callback. Demo Usage import { BarqodeDropzone, type DetectedBarcode } from \"barqode\"; let result = $state(\"\"); let dragover = $state(false); function onDetect(detectedCodes: DetectedBarcode[]) { result = detectedCodes.map((detectedCode) => detectedCode.rawValue).join(\", \"); } function onDragover(isDraggingOver: boolean) { dragover = isDraggingOver; } Click to upload or drop an image here Last detected: {result} .dragover { border-color: white; } Props formats Type: [BarcodeFormat[]](https://github.com/Sec-ant/barcode-detector?tab=readme-ov-file#barcode-detector) Default: [\"qr_code\"] Configure the barcode formats to detect. By default, only QR codes are detected. If you want to detect multiple formats, pass an array of formats: Under the hood, the standard $2 is used. Support varies across devices, operating systems and browsers. All components will prefer to use the native implementation if available and otherwise falls back to a polyfill implementation. Note that even if the native implementation is available, the component still might use the polyfill. For example, if the native implementation only supports the format 'qr_code' but the you select the formats ['qr_code', 'aztec']. onDetect Type: (detectedCodes: DetectedBarcode[]) => void Callback function that is called when a barcode is detected. It receives an array of $2, one callback per image. If not barcode is detected, the array will be empty. onDragover Type: (isDraggingOver: boolean) => void Callback function that is called when a file is dragged over the drop zone. onError Type: (error: Error) => void Callback function that is called when an error occurs. TODO: insert link to errors. Other props The BarqodeDropzone component accepts all attributes that a standard input element accepts. By default, the following attributes are set: type=\"file\". This is required to make the input a file input. You should not change this. name=\"image\". This is the name of the file input. accept=\"image/*\". This restricts the file types that can be uploaded to images. capture=\"environment\". This tells the browser to open the camera when the input is clicked on mobile devices. You can choose between user and environment, which opens the front and back camera respectively. You can also disable this functionality by setting it to null. multiple. This allows the user to upload multiple files at once. You can disable this by settings this to false. Browser Support This component depends on the $2 which is widely supported in modern browsers." 19 | }, 20 | { 21 | "title": "BarqodeStream", 22 | "href": "/docs/components/barqode-stream", 23 | "description": "Continuously scans frames from a camera stream.", 24 | "content": " import Demo from '$lib/components/demos/barqode-stream.svelte'; import { Callout } from '@svecodocs/kit'; The BarqodeStream component continuously scans frames from a camera stream and detects barcodes in real-time. Demo Usage import { BarqodeStream, type DetectedBarcode } from \"barqode\"; let loading = $state(true); let result: string | null = $state(null); function onCameraOn() { loading = false; } function onDetect(detectedCodes: DetectedBarcode[]) { result = detectedCode.map((code) => detectedCode.rawValue).join(\", \"); } function track(detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) { for (const detectedCode of detectedCodes) { const [firstPoint, ...otherPoints] = detectedCode.cornerPoints; ctx.strokeStyle = \"#2563eb\"; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(firstPoint.x, firstPoint.y); for (const { x, y } of otherPoints) { ctx.lineTo(x, y); } ctx.lineTo(firstPoint.x, firstPoint.y); ctx.closePath(); ctx.stroke(); } } {#if loading} Loading... {/if} Last detected: {result} Props constraints Type: $2 Default: { video: { facingMode: \"environment\" } } Configure the the various camera options, for example whether to use front or rear camera. The object must be of type MediaTrackConstraints. The object is passed as-is to getUserMedia, which is the API call for requesting a camera stream: navigator.mediaDevices.getUserMedia({ audio: false, video: the_constraint_object_you_provide, }); When constraints is updated, a new camera stream is requested which triggers the onCameraOn callback again. You can catch errors with the onError callback. An error can occur when you try to use the front camera on a device that doesn't have one for example. formats Type: [BarcodeFormat[]](https://github.com/Sec-ant/barcode-detector?tab=readme-ov-file#barcode-detector) Default: [\"qr_code\"] Configure the barcode formats to detect. By default, only QR codes are detected. If you want to detect multiple formats, pass an array of formats: Don't select more barcode formats than needed. Scanning becomes more expensive the more formats you select. Under the hood, the standard $2 is used. Support varies across devices, operating systems and browsers. All components will prefer to use the native implementation if available and otherwise falls back to a polyfill implementation. Note that even if the native implementation is available, the component still might use the polyfill. For example, if the native implementation only supports the format 'qr_code' but the you select the formats ['qr_code', 'aztec']. paused Type: boolean (bindable) Default: false Whether the camera stream is or should be paused. Bindable, which means that you can pause/unpause the stream from outside the component. Pausing the stream by setting paused to true is useful if you want to show some microinteraction after successful scans. When the you set it to false, the camera stream will be restarted and the onCameraOn callback function will be triggered again. torch Type: boolean Default: false Turn the camera flashlight on or off. This is not consistently supported by all devices and browsers. Support can even vary on the same device with the same browser. For example the rear camera often has a flashlight but the front camera does not. We can only tell if flashlight control is supported once the camera is loaded and the onCameraOn callback has been called. At the moment, enabling the torch may silently fail on unsupported devices, but in the onCameraOn callback payload you can access the MediaTrackCapabilities object, from which you can determine if the torch is supported. The camera stream must be reloaded when turning the torch on or off. That means the onCameraOn event will be emitted again. track Type: (detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) => void Callback function that can be used to visually highlight detected barcodes. A transparent canvas is overlaid on top of the camera stream. The track function is used to draw on this canvas. It receives an array of $2 and a $2 as the second argument. Note that when track is set the scanning frequency has to be increased. So if you want to go easy on your target device you might not want to enable tracking. The track function is called for every frame. It is important to keep the function as performant as possible. This can lead to performance issues on low-end devices and memory leaks if not handled correctly. onCameraOn Type: (capabilities: MediaTrackCapabilities) => void Callback function that is called when the camera stream is successfully loaded. It might take a while before the component is ready and the scanning process starts. The user has to be asked for camera access permission first and the camera stream has to be loaded. If you want to show a loading indicator, you can wait for this callback to be called. It is called as soon as the camera start streaming. The callback receives the a promise which resolves with the cameras $2 when everything is ready. onError Type: (error: Error) => void Callback function that is called when an error occurs. TODO: insert link to errors. onCameraOff Type: () => void Callback function that is called when the camera stream is stopped. This can happen when the camera constraints are modified, for example when switching between front and rear camera or when turning the torch on or off. onDetect Type: (detectedCodes: DetectedBarcode[]) => void Callback function that is called when a barcode is detected. It receives an array of $2. If you scan the same barcode multiple times in a row, onDetect is still only called once. When you hold a barcode in the camera, frames are actually decoded multiple times a second but you don't want to be flooded with callbacks that often. That's why the last decoded QR code is always cached and only new results are propagated. However, changing the value of paused resets this internal cache. Browser Support This component depends on the $2 which is widely supported in modern browsers." 25 | }, 26 | { 27 | "title": "Full demo", 28 | "href": "/docs/demos/full-demo", 29 | "description": "Kitchen sink demo of the BarqodeStream component.", 30 | "content": " import Demo from '$lib/components/demos/full-demo.svelte'; Modern mobile phones often have a variety of different cameras installed (e.g. front, rear, wide-angle, infrared, desk-view). The one picked by default is sometimes not the best choice. For more fine-grained control, you can select a camera by device constraints or by the device ID. Detected codes are visually highlighted in real-time. In this demo you can use the track function dropdown to change the flavor. By default only QR-codes are detected but a variety of other barcode formats are also supported. You can select one or multiple but the more you select the more expensive scanning becomes. Demo Usage import { BarqodeStream, type BarcodeFormat, type DetectedBarcode } from \"barqode\"; let result = $state(\"\"); let error = $state(\"\"); let selectedConstraints = $state({ facingMode: \"environment\" }); let barcodeFormats: { [key in BarcodeFormat]: boolean; } = $state({ aztec: false, code_128: false, code_39: false, code_93: false, codabar: false, databar: false, databar_expanded: false, databar_limited: false, data_matrix: false, dx_film_edge: false, ean_13: false, ean_8: false, itf: false, maxi_code: false, micro_qr_code: false, pdf417: false, qr_code: true, rm_qr_code: false, upc_a: false, upc_e: false, linear_codes: false, matrix_codes: false, unknown: false, }); // computed value for selected formats let selectedBarcodeFormats: BarcodeFormat[] = $derived( Object.keys(barcodeFormats).filter( (format: string) => barcodeFormats[format] ) as BarcodeFormat[] ); // track function options const trackFunctionOptions = [ { text: \"nothing (default)\", value: undefined }, { text: \"outline\", value: paintOutline }, { text: \"centered text\", value: paintCenterText }, { text: \"bounding box\", value: paintBoundingBox }, ]; let trackFunctionSelected = $state(trackFunctionOptions[1]); // camera constraint options const defaultConstraintOptions: { label: string; constraints: MediaTrackConstraints }[] = [ { label: \"rear camera\", constraints: { facingMode: \"environment\" } }, { label: \"front camera\", constraints: { facingMode: \"user\" } }, ]; let constraintOptions = $state(defaultConstraintOptions); async function onCameraOn() { try { const devices = await navigator.mediaDevices.enumerateDevices(); const videoDevices = devices.filter(({ kind }) => kind === \"videoinput\"); constraintOptions = [ ...defaultConstraintOptions, ...videoDevices.map(({ deviceId, label }) => ({ label: ${label}, constraints: { deviceId }, })), ]; error = \"\"; } catch (e) { console.error(e); } } function onError(err: { name: string; message: string }) { error = [${err.name}]: ; if (err.name === \"NotAllowedError\") { error += \"you need to grant camera access permission\"; } else if (err.name === \"NotFoundError\") { error += \"no camera on this device\"; } else if (err.name === \"NotSupportedError\") { error += \"secure context required (HTTPS, localhost)\"; } else if (err.name === \"NotReadableError\") { error += \"is the camera already in use?\"; } else if (err.name === \"OverconstrainedError\") { error += \"installed cameras are not suitable\"; } else if (err.name === \"StreamApiNotSupportedError\") { error += \"Stream API is not supported in this browser\"; } else { error += err.message; } } function onDetect(detectedCodes: DetectedBarcode[]) { console.log(detectedCodes); result = JSON.stringify(detectedCodes.map((code) => code.rawValue)); } // track functions function paintOutline( detectedCodes: { cornerPoints: { x: number; y: number }[]; boundingBox: DOMRectReadOnly; rawValue: string; format: Exclude; }[], ctx: CanvasRenderingContext2D ) { for (const detectedCode of detectedCodes) { const [firstPoint, ...otherPoints] = detectedCode.cornerPoints; ctx.strokeStyle = \"red\"; ctx.beginPath(); ctx.moveTo(firstPoint.x, firstPoint.y); for (const { x, y } of otherPoints) { ctx.lineTo(x, y); } ctx.lineTo(firstPoint.x, firstPoint.y); ctx.closePath(); ctx.stroke(); } } function paintBoundingBox( detectedCodes: { cornerPoints: { x: number; y: number }[]; boundingBox: DOMRectReadOnly; rawValue: string; format: Exclude; }[], ctx: CanvasRenderingContext2D ) { for (const detectedCode of detectedCodes) { const { boundingBox: { x, y, width, height }, } = detectedCode; ctx.lineWidth = 2; ctx.strokeStyle = \"#007bff\"; ctx.strokeRect(x, y, width, height); } } function paintCenterText( detectedCodes: { cornerPoints: { x: number; y: number }[]; boundingBox: DOMRectReadOnly; rawValue: string; format: Exclude; }[], ctx: CanvasRenderingContext2D ) { for (const detectedCode of detectedCodes) { const { boundingBox, rawValue } = detectedCode; const centerX = boundingBox.x + boundingBox.width / 2; const centerY = boundingBox.y + boundingBox.height / 2; const fontSize = Math.max(12, (50 * boundingBox.width) / ctx.canvas.width); ctx.font = bold ${fontSize}px sans-serif; ctx.textAlign = \"center\"; ctx.lineWidth = 3; ctx.strokeStyle = \"#35495e\"; ctx.strokeText(detectedCode.rawValue, centerX, centerY); ctx.fillStyle = \"#5cb984\"; ctx.fillText(rawValue, centerX, centerY); } } Camera constraints: {#each constraintOptions as option} {option.label} {/each} Track function: {#each trackFunctionOptions as option} {option.text} {/each} Barcode formats: {#each Object.keys(barcodeFormats) as option} {@const barcodeOption = option as BarcodeFormat} {option} {/each} {#if error} {error} {/if} Last result:{result} `" 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /docs/static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svecosystem/barqode/129d781e31c7b88016e618d4c5ad3635c2770f8a/docs/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svecosystem/barqode/129d781e31c7b88016e618d4c5ad3635c2770f8a/docs/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svecosystem/barqode/129d781e31c7b88016e618d4c5ad3635c2770f8a/docs/static/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svecosystem/barqode/129d781e31c7b88016e618d4c5ad3635c2770f8a/docs/static/favicon-16x16.png -------------------------------------------------------------------------------- /docs/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svecosystem/barqode/129d781e31c7b88016e618d4c5ad3635c2770f8a/docs/static/favicon-32x32.png -------------------------------------------------------------------------------- /docs/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svecosystem/barqode/129d781e31c7b88016e618d4c5ad3635c2770f8a/docs/static/favicon.ico -------------------------------------------------------------------------------- /docs/static/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svecosystem/barqode/129d781e31c7b88016e618d4c5ad3635c2770f8a/docs/static/og.png -------------------------------------------------------------------------------- /docs/static/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /docs/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; 2 | import { mdsx } from "mdsx"; 3 | import mdsxConfig from "./mdsx.config.js"; 4 | import adapter from "@sveltejs/adapter-cloudflare"; 5 | 6 | /** @type {import('@sveltejs/kit').Config} */ 7 | const config = { 8 | preprocess: [mdsx(mdsxConfig), vitePreprocess()], 9 | kit: { 10 | alias: { 11 | "$content/*": ".velite/*", 12 | }, 13 | adapter: adapter(), 14 | }, 15 | extensions: [".svelte", ".md"], 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/velite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig, s } from "velite"; 2 | 3 | const baseSchema = s.object({ 4 | title: s.string(), 5 | description: s.string(), 6 | path: s.path(), 7 | content: s.markdown(), 8 | navLabel: s.string().optional(), 9 | raw: s.raw(), 10 | toc: s.toc(), 11 | section: s.enum(["Overview", "Components", "Demos"]), 12 | }); 13 | 14 | const docSchema = baseSchema.transform((data) => { 15 | return { 16 | ...data, 17 | slug: data.path, 18 | slugFull: `/${data.path}`, 19 | }; 20 | }); 21 | 22 | export default defineConfig({ 23 | root: "./src/content", 24 | collections: { 25 | docs: { 26 | name: "Doc", 27 | pattern: "./**/*.md", 28 | schema: docSchema, 29 | }, 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /docs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from "@sveltejs/kit/vite"; 2 | import { defineConfig } from "vite"; 3 | import tailwindcss from "@tailwindcss/vite"; 4 | import { resolve } from "node:path"; 5 | 6 | const __dirname = new URL(".", import.meta.url).pathname; 7 | 8 | export default defineConfig({ 9 | plugins: [sveltekit(), tailwindcss()], 10 | optimizeDeps: { 11 | exclude: ["barqode"], 12 | }, 13 | server: { 14 | fs: { 15 | allow: [resolve(__dirname, "./.velite")], 16 | }, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js"; 2 | import prettier from "eslint-config-prettier"; 3 | import svelte from "eslint-plugin-svelte"; 4 | import globals from "globals"; 5 | import tseslint from "typescript-eslint"; 6 | 7 | export default tseslint.config( 8 | eslint.configs.recommended, 9 | ...tseslint.configs.recommended, 10 | ...svelte.configs["flat/recommended"], 11 | prettier, 12 | ...svelte.configs["flat/prettier"], 13 | { 14 | languageOptions: { 15 | globals: { 16 | ...globals.browser, 17 | ...globals.node, 18 | }, 19 | }, 20 | }, 21 | { 22 | files: ["**/*.svelte"], 23 | languageOptions: { 24 | parserOptions: { 25 | parser: tseslint.parser, 26 | }, 27 | }, 28 | }, 29 | { 30 | rules: { 31 | "@typescript-eslint/no-unused-vars": [ 32 | "error", 33 | { 34 | argsIgnorePattern: "^_", 35 | varsIgnorePattern: "^_", 36 | }, 37 | ], 38 | "@typescript-eslint/no-unused-expressions": "off", 39 | }, 40 | }, 41 | { 42 | ignores: [ 43 | "build/", 44 | ".svelte-kit/", 45 | "dist/", 46 | ".svelte-kit/**/*", 47 | "docs/.svelte-kit/**/*", 48 | "docs/static/**/*", 49 | "docs/build/**/*", 50 | ".svelte-kit", 51 | "packages/barqode/dist/**/*", 52 | "packages/barqode/.svelte-kit/**/*", 53 | ], 54 | } 55 | ); 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "description": "Monorepo for Barqode", 4 | "private": true, 5 | "version": "0.0.0", 6 | "author": "Hunter Johnston ", 7 | "license": "MIT", 8 | "type": "module", 9 | "scripts": { 10 | "build": "pnpm build:packages && pnpm build:docs", 11 | "build:packages": "pnpm -F \"./packages/**\" --parallel build", 12 | "build:docs": "pnpm -F \"./docs/**\" build", 13 | "check": "pnpm build:packages && pnpm -r check", 14 | "ci:publish": "pnpm build:packages && changeset publish", 15 | "dev": "pnpm -F \"./packages/**\" svelte-kit sync && pnpm -r --parallel --reporter append-only --color dev", 16 | "test": "pnpm -r test", 17 | "format": "prettier --write .", 18 | "lint": "prettier --check . && eslint ." 19 | }, 20 | "engines": { 21 | "pnpm": ">=9.0.0", 22 | "node": ">=20" 23 | }, 24 | "packageManager": "pnpm@9.14.4", 25 | "devDependencies": { 26 | "@changesets/cli": "^2.27.10", 27 | "@eslint/js": "^9.16.0", 28 | "@svitejs/changesets-changelog-github-compact": "^1.2.0", 29 | "@types/node": "^22.10.1", 30 | "@typescript-eslint/eslint-plugin": "^8.17.0", 31 | "@typescript-eslint/scope-manager": "^8.17.0", 32 | "@typescript-eslint/utils": "^8.17.0", 33 | "eslint": "^9.16.0", 34 | "eslint-config-prettier": "^9.1.0", 35 | "eslint-plugin-svelte": "^2.46.1", 36 | "globals": "^15.13.0", 37 | "prettier": "^3.4.1", 38 | "prettier-plugin-svelte": "^3.3.2", 39 | "prettier-plugin-tailwindcss": "^0.6.9", 40 | "svelte": "^5.4.0", 41 | "svelte-eslint-parser": "^0.43.0", 42 | "typescript": "^5.7.2", 43 | "typescript-eslint": "^8.17.0", 44 | "wrangler": "^3.91.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/barqode/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | /.svelte-kit 7 | /build 8 | /dist 9 | 10 | # OS 11 | .DS_Store 12 | Thumbs.db 13 | 14 | # Env 15 | .env 16 | .env.* 17 | !.env.example 18 | !.env.test 19 | 20 | # Vite 21 | vite.config.js.timestamp-* 22 | vite.config.ts.timestamp-* 23 | -------------------------------------------------------------------------------- /packages/barqode/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /packages/barqode/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # barqode 2 | 3 | ## 0.0.2 4 | 5 | ### Patch Changes 6 | 7 | - expose `*ref` props for access to the underlying elements ([#5](https://github.com/svecosystem/barqode/pull/5)) 8 | 9 | ## 0.0.1 10 | 11 | ### Patch Changes 12 | 13 | - Initial release ([#3](https://github.com/svecosystem/barqode/pull/3)) 14 | -------------------------------------------------------------------------------- /packages/barqode/README.md: -------------------------------------------------------------------------------- 1 | # Barqode 2 | -------------------------------------------------------------------------------- /packages/barqode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "barqode", 3 | "version": "0.0.2", 4 | "scripts": { 5 | "build": "pnpm package", 6 | "dev": "svelte-package --watch", 7 | "dev:svelte": "vite dev", 8 | "package": "svelte-kit sync && svelte-package && publint", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json" 10 | }, 11 | "files": [ 12 | "dist", 13 | "!dist/**/*.test.*", 14 | "!dist/**/*.spec.*" 15 | ], 16 | "sideEffects": [ 17 | "**/*.css" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/svecosystem/barqode.git", 22 | "directory": "packages/barqode" 23 | }, 24 | "funding": [ 25 | "https://github.com/sponsors/huntabyte", 26 | "https://ko-fi.com/ollema" 27 | ], 28 | "svelte": "./dist/index.js", 29 | "types": "./dist/index.d.ts", 30 | "type": "module", 31 | "exports": { 32 | ".": { 33 | "types": "./dist/index.d.ts", 34 | "svelte": "./dist/index.js" 35 | } 36 | }, 37 | "peerDependencies": { 38 | "svelte": "^5.0.0" 39 | }, 40 | "devDependencies": { 41 | "@sveltejs/adapter-auto": "^3.3.1", 42 | "@sveltejs/kit": "^2.9.0", 43 | "@sveltejs/package": "^2.3.7", 44 | "@sveltejs/vite-plugin-svelte": "^4.0.2", 45 | "publint": "^0.2.12", 46 | "svelte": "^5.4.0", 47 | "svelte-check": "^4.1.0", 48 | "typescript": "^5.7.2", 49 | "vite": "^5.4.11" 50 | }, 51 | "license": "MIT", 52 | "contributors": [ 53 | { 54 | "name": "Olle", 55 | "url": "https://github.com/ollema" 56 | }, 57 | { 58 | "name": "Hunter Johnston", 59 | "url": "https://github.com/huntabyte" 60 | } 61 | ], 62 | "dependencies": { 63 | "barcode-detector": "^2.3.1", 64 | "runed": "^0.16.1", 65 | "webrtc-adapter": "^9.0.1" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/barqode/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /packages/barqode/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/barqode/src/lib/components/barqode-dropzone.svelte: -------------------------------------------------------------------------------- 1 | 82 | 83 |
96 | 107 | 108 | {@render children?.()} 109 |
110 | 111 | 124 | -------------------------------------------------------------------------------- /packages/barqode/src/lib/components/barqode-stream.svelte: -------------------------------------------------------------------------------- 1 | 193 | 194 |
195 | 204 | 205 | 211 | 212 | 213 | 214 | 215 |
216 | {@render children?.()} 217 |
218 |
219 | 220 | 247 | -------------------------------------------------------------------------------- /packages/barqode/src/lib/components/types.ts: -------------------------------------------------------------------------------- 1 | import type { BarcodeFormat, DetectedBarcode, Point2D } from "barcode-detector/pure"; 2 | import type { Snippet } from "svelte"; 3 | import type { HTMLInputAttributes } from "svelte/elements"; 4 | 5 | export type { DetectedBarcode, BarcodeFormat, Point2D }; 6 | 7 | export type DropzoneProps = { 8 | /** 9 | * The formats of the barcodes to detect. 10 | * 11 | * @default ['qr_code'] 12 | * 13 | * @see https://developer.mozilla.org/en-US/docs/Web/API/Barcode_Detection_API#supported_barcode_formats 14 | */ 15 | formats?: BarcodeFormat[]; 16 | 17 | /** 18 | * A callback function called when the user drops an image file. 19 | * 20 | * @param codes - The detected barcodes. 21 | */ 22 | onDetect?: (detectedCodes: DetectedBarcode[]) => void; 23 | 24 | /** 25 | * A callback function called when the user drags an image file over the drop zone. 26 | * 27 | * @param isDraggingOver - Whether the user is dragging an image file over the drop zone. 28 | */ 29 | onDragover?: (isDraggingOver: boolean) => void; 30 | 31 | /** 32 | * A callback function called when the user drags an image file out of the drop zone. 33 | * 34 | * @param error - The error that occurred. 35 | */ 36 | onError?: (error: Error) => void; 37 | 38 | /** 39 | * Optional prop for content to overlay on top of the drop zone. 40 | */ 41 | children?: Snippet; 42 | 43 | /** 44 | * A reference to the underlying `input` element. 45 | * 46 | * @bindable 47 | * 48 | */ 49 | inputRef?: HTMLInputElement | null; 50 | 51 | /** 52 | * A reference to the wrapping `div` element. 53 | * 54 | * @bindable 55 | */ 56 | wrapperRef?: HTMLElement | null; 57 | } & HTMLInputAttributes; 58 | 59 | export type StreamProps = { 60 | /** 61 | * The MediaTrackConstraints specifying the desired media types and their constraints. 62 | * 63 | * @default { video: { facingMode: 'environment' } } 64 | * 65 | * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints 66 | */ 67 | constraints?: MediaTrackConstraints; 68 | 69 | /** 70 | * The formats of the barcodes to detect. 71 | * 72 | * @default ['qr_code'] 73 | * 74 | * @see https://developer.mozilla.org/en-US/docs/Web/API/Barcode_Detection_API#supported_barcode_formats 75 | */ 76 | formats?: BarcodeFormat[]; 77 | 78 | /** 79 | * Whether the camera stream is paused. Bindable. 80 | * 81 | * @default false 82 | * 83 | * Can also be set to `true` to pause the camera stream, which is useful when you want to show 84 | * some microinteraction after successful scans. When the you set it to `false`, the camera 85 | * stream will be restarted and the `onCameraOn` callback function will be triggered again. 86 | */ 87 | paused?: boolean; 88 | 89 | /** 90 | * Whether the torch is enabled. 91 | * 92 | * @default false 93 | * 94 | * Not consistently supported across devices. 95 | */ 96 | torch?: boolean; 97 | 98 | /** 99 | * Function to visually highlight the detected barcodes. 100 | * 101 | * @param codes - The detected barcodes with their adjusted corner points. 102 | * @param ctx - The canvas rendering context. 103 | */ 104 | track?: (detectedCodes: DetectedBarcode[], ctx: CanvasRenderingContext2D) => void; 105 | 106 | /** 107 | * A callback function called when the camera stream is turned on. 108 | * 109 | * @param capabilities - The MediaTrackCapabilities of the camera stream. 110 | */ 111 | onCameraOn?: (capabilities: MediaTrackCapabilities) => void; 112 | 113 | /** 114 | * A callback function called when the camera stream encounters an error. 115 | * 116 | * @param error - The error that occurred. 117 | */ 118 | onError?: (error: Error) => void; 119 | 120 | /** 121 | * A callback function called when the camera stream is turned off. 122 | */ 123 | onCameraOff?: () => void; 124 | 125 | /** 126 | * A callback function called a detection is made. 127 | * 128 | * Note: if you scan the same barcode code multiple times in a row, onDetect is still called once. 129 | * When you hold a barcode in the camera, frames are actually decoded multiple times a second 130 | * but you don't want to be flooded with onDetect callbacks that often. 131 | * That's why the last decoded barcode is always cached and only new results are propagated. 132 | * However changing the value of `paused` resets this internal cache. 133 | */ 134 | onDetect?: (detectedCodes: DetectedBarcode[]) => void; 135 | 136 | /** 137 | * Optional prop for content to overlay on top of the camera stream. 138 | */ 139 | children?: Snippet; 140 | 141 | /** 142 | * A reference to the underlying `video` element. 143 | * 144 | * @bindable 145 | * 146 | */ 147 | videoRef?: HTMLVideoElement | null; 148 | 149 | /** 150 | * A reference to the wrapping `div` element. 151 | * 152 | * @bindable 153 | */ 154 | wrapperRef?: HTMLElement | null; 155 | 156 | /** 157 | * A reference to the pause frame `canvas` element. 158 | * 159 | * @bindable 160 | */ 161 | pauseRef?: HTMLCanvasElement | null; 162 | 163 | /** 164 | * A reference to the tracking frame `canvas` element. 165 | * 166 | * @bindable 167 | */ 168 | trackingRef?: HTMLCanvasElement | null; 169 | }; 170 | -------------------------------------------------------------------------------- /packages/barqode/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { default as BarqodeDropzone } from "./components/barqode-dropzone.svelte"; 2 | export { default as BarqodeStream } from "./components/barqode-stream.svelte"; 3 | 4 | export type { 5 | DetectedBarcode, 6 | BarcodeFormat, 7 | DropzoneProps, 8 | StreamProps, 9 | } from "./components/types.js"; 10 | -------------------------------------------------------------------------------- /packages/barqode/src/lib/internal/callforth.ts: -------------------------------------------------------------------------------- 1 | // based on vue-qrcode-reader by Niklas Gruhn 2 | // see https://github.com/gruhn/vue-qrcode-reader 3 | 4 | /** 5 | * Creates a Promise that resolves when a specified event is triggered on the given EventTarget. 6 | * 7 | * @param eventTarget - The target to listen for events on. 8 | * @param successEvent - The name of the event that will resolve the Promise. 9 | * @param errorEvent - The name of the event that will reject the Promise. Defaults to 'error'. 10 | * 11 | * @returns A Promise that resolves with the event object when the successEvent is 12 | * triggered, or rejects with the event object when the errorEvent is triggered. 13 | */ 14 | export function eventOn( 15 | eventTarget: EventTarget, 16 | successEvent: string, 17 | errorEvent = "error" 18 | ): Promise { 19 | let $resolve: (value: Event) => void; 20 | let $reject: (reason?: Event) => void; 21 | 22 | const promise = new Promise( 23 | (resolve: (value: Event) => void, reject: (reason?: Event) => void) => { 24 | $resolve = resolve; 25 | $reject = reject; 26 | 27 | eventTarget.addEventListener(successEvent, $resolve); 28 | eventTarget.addEventListener(errorEvent, $reject); 29 | } 30 | ); 31 | 32 | promise.finally(() => { 33 | eventTarget.removeEventListener(successEvent, $resolve); 34 | eventTarget.removeEventListener(errorEvent, $reject); 35 | }); 36 | 37 | return promise; 38 | } 39 | 40 | /** 41 | * Creates a promise that resolves after a specified number of milliseconds. 42 | * 43 | * @param ms - The number of milliseconds to wait before the promise resolves. 44 | * 45 | * @returns A promise that resolves after the specified delay. 46 | */ 47 | export function sleep(ms: number) { 48 | return new Promise((resolve: (value: unknown) => void) => setTimeout(resolve, ms)); 49 | } 50 | -------------------------------------------------------------------------------- /packages/barqode/src/lib/internal/camera.ts: -------------------------------------------------------------------------------- 1 | // based on vue-qrcode-reader by Niklas Gruhn 2 | // see https://github.com/gruhn/vue-qrcode-reader 3 | 4 | import { 5 | StreamApiNotSupportedError, 6 | InsecureContextError, 7 | StreamLoadTimeoutError, 8 | } from "./errors.js"; 9 | import { eventOn, sleep } from "./callforth.js"; 10 | import shimGetUserMedia from "./shim-get-user-media.js"; 11 | import { assertNever } from "./util.js"; 12 | 13 | type StartTaskResult = { 14 | type: "start"; 15 | data: { 16 | video: HTMLVideoElement; 17 | stream: MediaStream; 18 | capabilities: Partial; 19 | constraints: MediaTrackConstraints; 20 | isTorchOn: boolean; 21 | }; 22 | }; 23 | 24 | type StopTaskResult = { 25 | type: "stop"; 26 | data: null; 27 | }; 28 | 29 | type FailedTask = { 30 | type: "failed"; 31 | error: Error; 32 | }; 33 | 34 | type TaskResult = StartTaskResult | StopTaskResult | FailedTask; 35 | 36 | let taskQueue: Promise = Promise.resolve({ type: "stop", data: null }); 37 | 38 | type CreateObjectURLCompat = (obj: MediaSource | Blob | MediaStream) => string; 39 | 40 | /** 41 | * Starts the camera with the given constraints and attaches the stream to the provided video element. 42 | * 43 | * @param video - The HTML video element to which the camera stream will be attached. 44 | * @param constraints - The media track constraints to apply when starting the camera. 45 | * @param torch - A boolean indicating whether the torch (flashlight) should be enabled if supported. 46 | * 47 | * @returns A promise that resolves to a `StartTaskResult` object containing details about the started camera stream. 48 | * 49 | * @throws InsecureContextError - If the page is not loaded in a secure context (HTTPS). 50 | * @throws StreamApiNotSupportedError - If the Stream API is not supported by the browser. 51 | * @throws StreamLoadTimeoutError - If the video element fails to load the camera stream within a 6-second timeout. 52 | */ 53 | async function runStartTask( 54 | video: HTMLVideoElement, 55 | constraints: MediaTrackConstraints, 56 | torch: boolean 57 | ): Promise { 58 | console.debug("[barqode] starting camera with constraints: ", JSON.stringify(constraints)); 59 | 60 | // at least in Chrome `navigator.mediaDevices` is undefined when the page is 61 | // loaded using HTTP rather than HTTPS. thus `STREAM_API_NOT_SUPPORTED` is 62 | // initialized with `false` although the API might actually be supported. 63 | // so although `getUserMedia` already should have a built-in mechanism to 64 | // detect insecure context (by throwing `NotAllowedError`), we have to do a 65 | // manual check before even calling `getUserMedia`. 66 | if (window.isSecureContext !== true) { 67 | throw new InsecureContextError(); 68 | } 69 | 70 | if (navigator?.mediaDevices?.getUserMedia === undefined) { 71 | throw new StreamApiNotSupportedError(); 72 | } 73 | 74 | // this is a browser API only shim. tt patches the global window object which 75 | // is not available during SSR. So we lazily apply this shim at runtime. 76 | shimGetUserMedia(); 77 | 78 | console.debug("[barqode] calling getUserMedia"); 79 | const stream = await navigator.mediaDevices.getUserMedia({ 80 | audio: false, 81 | video: constraints, 82 | }); 83 | 84 | if (video.srcObject !== undefined) { 85 | video.srcObject = stream; 86 | } else if (video.mozSrcObject !== undefined) { 87 | video.mozSrcObject = stream; 88 | } else if (window.URL.createObjectURL) { 89 | video.src = (window.URL.createObjectURL as CreateObjectURLCompat)(stream); 90 | } else if (window.webkitURL) { 91 | video.src = (window.webkitURL.createObjectURL as CreateObjectURLCompat)(stream); 92 | } else { 93 | video.src = stream.id; 94 | } 95 | 96 | // in the WeChat browser on iOS, 'loadeddata' event won't get fired unless video is explicitly triggered by play() 97 | video.play(); 98 | 99 | console.debug("[barqode] waiting for video element to load"); 100 | await Promise.race([ 101 | eventOn(video, "loadeddata"), 102 | 103 | // on iOS devices in PWA mode, BarqodeStream works initially, but after killing and restarting the PWA, 104 | // all video elements fail to load camera streams and never emit the `loadeddata` event. 105 | // looks like this is related to a WebKit issue (see #298). no workarounds at the moment. 106 | // to at least detect this situation, we throw an error if the event has not been emitted after a 6 second timeout. 107 | sleep(6_000).then(() => { 108 | throw new StreamLoadTimeoutError(); 109 | }), 110 | ]); 111 | console.debug("[barqode] video element loaded"); 112 | 113 | // according to: https://oberhofer.co/mediastreamtrack-and-its-capabilities/#queryingcapabilities 114 | // on some devices, getCapabilities only returns a non-empty object after some delay. 115 | // there is no appropriate event so we have to add a constant timeout 116 | await sleep(500); 117 | 118 | const [track] = stream.getVideoTracks(); 119 | 120 | const capabilities: Partial = track?.getCapabilities?.() ?? {}; 121 | 122 | let isTorchOn = false; 123 | // @ts-expect-error torch is not in the MediaTrackConstraints type but it should be? 124 | if (torch && capabilities.torch) { 125 | // @ts-expect-error torch is not in the MediaTrackConstraints type but it should be? 126 | await track.applyConstraints({ advanced: [{ torch: true }] }); 127 | isTorchOn = true; 128 | } 129 | 130 | console.debug("[barqode] camera ready"); 131 | 132 | return { 133 | type: "start", 134 | data: { 135 | video, 136 | stream, 137 | capabilities, 138 | constraints, 139 | isTorchOn, 140 | }, 141 | }; 142 | } 143 | 144 | /** 145 | * Starts the camera with the given video element and settings. 146 | * 147 | * @param video - The HTML video element to which the camera stream will be attached. 148 | * @param options.constraints - The media track constraints to apply when starting the camera. 149 | * @param options.torch - A boolean indicating whether the torch (flashlight) should be enabled if supported. 150 | * @param options.restart - A boolean indicating whether to restart the camera even if no settings changed. Defaults to false. 151 | * 152 | * @returns A promise that resolves to a `MediaTrackCapabilities` object containing the camera capabilities. 153 | * 154 | * @throws Error - If something goes wrong with the camera task queue. 155 | */ 156 | export async function start( 157 | video: HTMLVideoElement, 158 | { 159 | constraints, 160 | torch, 161 | restart = false, 162 | }: { 163 | constraints: MediaTrackConstraints; 164 | torch: boolean; 165 | restart?: boolean; 166 | } 167 | ): Promise> { 168 | // update the task queue synchronously 169 | taskQueue = taskQueue 170 | .then((prevTaskResult) => { 171 | if (prevTaskResult.type === "start") { 172 | // previous task is a start task 173 | // we'll check if we can reuse the previous result 174 | const { 175 | data: { 176 | video: prevVideo, 177 | stream: prevStream, 178 | constraints: prevConstraints, 179 | isTorchOn: prevIsTorchOn, 180 | }, 181 | } = prevTaskResult; 182 | // TODO: should we keep this object comparison 183 | // this code only checks object sameness not equality 184 | // deep comparison requires snapshots and value by value check 185 | // which seem too much 186 | if ( 187 | !restart && 188 | video === prevVideo && 189 | constraints === prevConstraints && 190 | torch === prevIsTorchOn 191 | ) { 192 | // things didn't change, reuse the previous result 193 | return prevTaskResult; 194 | } 195 | // something changed, restart (stop then start) 196 | return runStopTask(prevVideo, prevStream, prevIsTorchOn).then(() => 197 | runStartTask(video, constraints, torch) 198 | ); 199 | } else if (prevTaskResult.type === "stop" || prevTaskResult.type === "failed") { 200 | // previous task is a stop/error task 201 | // we can safely start 202 | return runStartTask(video, constraints, torch); 203 | } 204 | 205 | assertNever(prevTaskResult); 206 | }) 207 | .catch((error: Error) => { 208 | console.debug(`[barqode] starting camera failed with "${error}"`); 209 | return { type: "failed", error }; 210 | }); 211 | 212 | // await the task queue asynchronously 213 | const taskResult = await taskQueue; 214 | 215 | if (taskResult.type === "stop") { 216 | // we just synchronously updated the task above 217 | // to make the latest task a start task 218 | // so this case shouldn't happen 219 | throw new Error("Something went wrong with the camera task queue (start task)."); 220 | } else if (taskResult.type === "failed") { 221 | throw taskResult.error; 222 | } else if (taskResult.type === "start") { 223 | // return the data we want 224 | return taskResult.data.capabilities; 225 | } 226 | 227 | assertNever(taskResult); 228 | } 229 | 230 | /** 231 | * Stops the camera stream and cleans up associated resources. 232 | * 233 | * @param video - The HTML video element displaying the camera stream. 234 | * @param stream - The MediaStream object representing the active camera stream. 235 | * @param isTorchOn - A boolean indicating whether the torch is currently enabled. 236 | * 237 | * @returns A promise that resolves to a `StopTaskResult` when the camera is fully stopped. 238 | */ 239 | async function runStopTask( 240 | video: HTMLVideoElement, 241 | stream: MediaStream, 242 | isTorchOn: boolean 243 | ): Promise { 244 | console.debug("[barqode] stopping camera"); 245 | 246 | video.src = ""; 247 | video.srcObject = null; 248 | video.load(); 249 | 250 | // wait for load() to emit error 251 | // because src and srcObject are empty 252 | await eventOn(video, "error"); 253 | 254 | for (const track of stream.getTracks()) { 255 | // @ts-expect-error torch is not in the MediaTrackConstraints type but it should be? 256 | isTorchOn ?? (await track.applyConstraints({ advanced: [{ torch: false }] })); 257 | stream.removeTrack(track); 258 | track.stop(); 259 | } 260 | 261 | return { 262 | type: "stop", 263 | data: null, 264 | }; 265 | } 266 | 267 | /** 268 | * Stops any active camera stream and ensures proper cleanup. 269 | * 270 | * @returns A promise that resolves when the camera is fully stopped. 271 | * 272 | * @throws Error - If something goes wrong with the camera task queue. 273 | */ 274 | export async function stop() { 275 | // update the task queue synchronously 276 | taskQueue = taskQueue.then((prevTaskResult) => { 277 | if (prevTaskResult.type === "stop" || prevTaskResult.type === "failed") { 278 | // previous task is a stop task 279 | // no need to stop again 280 | return prevTaskResult; 281 | } 282 | const { 283 | data: { video, stream, isTorchOn }, 284 | } = prevTaskResult; 285 | return runStopTask(video, stream, isTorchOn); 286 | }); 287 | // await the task queue asynchronously 288 | const taskResult = await taskQueue; 289 | if (taskResult.type === "start") { 290 | // we just synchronously updated the task above 291 | // to make the latest task a stop task 292 | // so this case shouldn't happen 293 | throw new Error("Something went wrong with the camera task queue (stop task)."); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /packages/barqode/src/lib/internal/errors.ts: -------------------------------------------------------------------------------- 1 | export class DropImageFetchError extends Error { 2 | constructor() { 3 | super("can't process cross-origin image"); 4 | 5 | this.name = "DropImageFetchError"; 6 | } 7 | } 8 | 9 | export class StreamApiNotSupportedError extends Error { 10 | constructor() { 11 | super("this browser has no Stream API support"); 12 | 13 | this.name = "StreamApiNotSupportedError"; 14 | } 15 | } 16 | 17 | export class InsecureContextError extends Error { 18 | constructor() { 19 | super( 20 | "camera access is only permitted in secure context. Use HTTPS or localhost rather than HTTP." 21 | ); 22 | 23 | this.name = "InsecureContextError"; 24 | } 25 | } 26 | 27 | export class StreamLoadTimeoutError extends Error { 28 | constructor() { 29 | super( 30 | "loading camera stream timed out after 6 seconds. If you are on iOS in PWA mode, this is a known issue (see https://github.com/gruhn/vue-qrcode-reader/issues/298)" 31 | ); 32 | 33 | this.name = "StreamLoadTimeoutError"; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/barqode/src/lib/internal/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface HTMLVideoElement { 4 | mozSrcObject?: HTMLVideoElement["srcObject"]; 5 | } 6 | -------------------------------------------------------------------------------- /packages/barqode/src/lib/internal/scanner.ts: -------------------------------------------------------------------------------- 1 | // based on vue-qrcode-reader by Niklas Gruhn 2 | // see https://github.com/gruhn/vue-qrcode-reader 3 | 4 | import { type DetectedBarcode, type BarcodeFormat, BarcodeDetector } from "barcode-detector/pure"; 5 | import { eventOn } from "./callforth.js"; 6 | import { DropImageFetchError } from "./errors.js"; 7 | 8 | declare global { 9 | interface Window { 10 | BarcodeDetector?: typeof BarcodeDetector; 11 | } 12 | } 13 | 14 | /** 15 | * Singleton `BarcodeDetector` instance used by `BarqodeStream`. This is firstly to avoid 16 | * the overhead of creating a new instances for scanning each frame. And secondly, the 17 | * instances can seamlessly be replaced in the middle of the scanning process, if the 18 | * `formats` prop of `BarqodeStream` is changed. 19 | * 20 | * This instance is not used by `BarqodeCapture` and `BarqodeDropzone`, because it may not 21 | * have the right `formats` configured. For these components we create one-off `BarcodeDetector` 22 | * instances because it does not happen so frequently anyway (see: `processFile`/`processUrl`). 23 | */ 24 | let barcodeDetector: BarcodeDetector; 25 | 26 | /** 27 | * Constructs a `BarcodeDetector` instance, given a list of targeted barcode formats. 28 | * Preferably, we want to use the native `BarcodeDetector` implementation if supported. 29 | * Otherwise, we fall back to the polyfill implementation. 30 | * 31 | * Note, that we can't just monkey patch the polyfill on load, i.e. 32 | * 33 | * window.BarcodeDetector ??= BarcodeDetector 34 | * 35 | * for two reasons. Firstly, this is not SSR compatible, because `window` is not available 36 | * during SSR. Secondly, even if the native implementation is available, we still might 37 | * want to use the polyfill. For example, if the native implementation only supports the 38 | * format `"qr_code"` but the user wants to scan `["qr_code", "aztec"]` (see #450). 39 | */ 40 | async function createBarcodeDetector(formats: BarcodeFormat[]): Promise { 41 | if (window.BarcodeDetector === undefined) { 42 | console.debug("[barqode] native BarcodeDetector not supported. Will use polyfill."); 43 | return new BarcodeDetector({ formats }); 44 | } 45 | 46 | const allSupportedFormats = await window.BarcodeDetector.getSupportedFormats(); 47 | const unsupportedFormats = formats.filter((format) => !allSupportedFormats.includes(format)); 48 | 49 | if (unsupportedFormats.length > 0) { 50 | console.debug( 51 | `[barqode] native BarcodeDetector does not support formats ${JSON.stringify(unsupportedFormats)}. Will use polyfill.` 52 | ); 53 | return new BarcodeDetector({ formats }); 54 | } 55 | 56 | console.debug("[barqode] will use native BarcodeDetector."); 57 | return new window.BarcodeDetector({ formats }); 58 | } 59 | 60 | /** 61 | * Update the set of targeted barcode formats. In particular, this function 62 | * can be called during scanning and the camera stream doesn't have to be 63 | * interrupted. 64 | */ 65 | export async function setScanningFormats(formats: BarcodeFormat[]) { 66 | barcodeDetector = await createBarcodeDetector(formats); 67 | } 68 | 69 | type ScanHandler = (_: DetectedBarcode[]) => void; 70 | 71 | type KeepScanningOptions = { 72 | detectHandler: ScanHandler; 73 | locateHandler: ScanHandler; 74 | minDelay: number; 75 | formats: BarcodeFormat[]; 76 | }; 77 | 78 | /** 79 | * Continuously extracts frames from camera stream and tries to read 80 | * potentially pictured QR codes. 81 | */ 82 | export async function keepScanning( 83 | videoElement: HTMLVideoElement, 84 | { detectHandler, locateHandler, minDelay, formats }: KeepScanningOptions 85 | ) { 86 | console.debug("[barqode] start scanning"); 87 | await setScanningFormats(formats); 88 | 89 | const processFrame = 90 | (state: { lastScanned: number; contentBefore: string[]; lastScanHadContent: boolean }) => 91 | async (timeNow: number) => { 92 | if (videoElement.readyState === 0) { 93 | console.debug("[barqode] stop scanning: video element readyState is 0"); 94 | } else { 95 | const { lastScanned, contentBefore, lastScanHadContent } = state; 96 | 97 | // Scanning is expensive and we don't need to scan camera frames with 98 | // the maximum possible frequency. In particular when visual tracking 99 | // is disabled. So we skip scanning a frame if `minDelay` has not passed 100 | // yet. Notice that this approach is different from doing a `setTimeout` 101 | // after each scan. With `setTimeout`, delay and scanning are sequential: 102 | // 103 | // |-- scan --|---- minDelay ----|-- scan --|---- minDelay ----| 104 | // 105 | // Instead we do it concurrently: 106 | // 107 | // |---- minDelay ----|---- minDelay ----|---- minDelay ----| 108 | // |-- scan --| |-- scan --| |-- scan --| 109 | // 110 | // Let's say `minDelay` is 40ms, then we scan every 40ms as long as 111 | // scanning itself does not take more than 40ms. In particular when 112 | // visual tracking is enabled, that means we can repaint the tracking 113 | // canvas every 40ms. So we paint 114 | // 115 | // 1000ms / 40ms = 25fps (frames per second) 116 | // 117 | // 24fps is the minimum frame-rate that is perceived as a continuous 118 | // animation. We target 25fps just because 24 doesn't divide 1000ms 119 | // evenly. 120 | if (timeNow - lastScanned < minDelay) { 121 | window.requestAnimationFrame(processFrame(state)); 122 | } else { 123 | const detectedCodes = await barcodeDetector.detect(videoElement); 124 | 125 | // Only emit a detect event, if at least one of the detected codes has 126 | // not been seen before. Otherwise we spam tons of detect events while 127 | // a QR code is in view of the camera. To avoid that we store the previous 128 | // detection in `contentBefore`. 129 | // 130 | // Implicitly we also don't emit a `detect` event if `detectedCodes` is an 131 | // empty array. 132 | const anyNewCodesDetected = detectedCodes.some((code) => { 133 | return !contentBefore.includes(code.rawValue); 134 | }); 135 | 136 | if (anyNewCodesDetected) { 137 | detectHandler(detectedCodes); 138 | } 139 | 140 | const currentScanHasContent = detectedCodes.length > 0; 141 | 142 | // In contrast to the QR code content, the location changes all the time. 143 | // So we call the locate handler on every detection to repaint the tracking 144 | // canvas. 145 | if (currentScanHasContent) { 146 | locateHandler(detectedCodes); 147 | } 148 | 149 | // Additionally, we need to clear the tracking canvas once when no QR code 150 | // is in view of the camera anymore. Technically this can be merged with the 151 | // previous if-statement but this way it's more explicit. 152 | if (!currentScanHasContent && lastScanHadContent) { 153 | locateHandler(detectedCodes); 154 | } 155 | 156 | const newState = { 157 | lastScanned: timeNow, 158 | lastScanHadContent: currentScanHasContent, 159 | 160 | // It can happen that a QR code is constantly in view of the camera but 161 | // maybe a scanned frame is a bit blurry and we detect nothing but in the 162 | // next frame we detect the code again. We also want to avoid emitting 163 | // a `detect` event in such a case. So we don't reset `contentBefore`, 164 | // if we detect nothing, only if we detect something new. 165 | contentBefore: anyNewCodesDetected 166 | ? detectedCodes.map((code) => code.rawValue) 167 | : contentBefore, 168 | }; 169 | 170 | window.requestAnimationFrame(processFrame(newState)); 171 | } 172 | } 173 | }; 174 | 175 | processFrame({ 176 | lastScanned: performance.now(), 177 | contentBefore: [], 178 | lastScanHadContent: false, 179 | })(performance.now()); 180 | } 181 | 182 | async function imageElementFromUrl(url: string) { 183 | if (url.startsWith("http") && url.includes(location.host) === false) { 184 | throw new DropImageFetchError(); 185 | } 186 | 187 | const image = document.createElement("img"); 188 | image.src = url; 189 | 190 | await eventOn(image, "load"); 191 | 192 | return image; 193 | } 194 | 195 | export async function processFile( 196 | file: File, 197 | formats: BarcodeFormat[] = ["qr_code"] 198 | ): Promise { 199 | // To scan files/urls we use one-off `BarcodeDetector` instances, 200 | // since we don't scan as often as camera frames. Note, that we 201 | // always use the polyfill. This is because (at the time of writing) 202 | // some browser/OS combinations don't support `Blob`/`File` inputs 203 | // into the `detect` function. 204 | const barcodeDetector = new BarcodeDetector({ formats }); 205 | 206 | return await barcodeDetector.detect(file); 207 | } 208 | 209 | export async function processUrl( 210 | url: string, 211 | formats: BarcodeFormat[] = ["qr_code"] 212 | ): Promise { 213 | // To scan files/urls we use one-off `BarcodeDetector` instances, 214 | // since we don't scan as often as camera frames. Note, that we 215 | // always use the polyfill. This is because (at the time of writing) 216 | // some browser/OS combinations don't support `Blob`/`File` inputs 217 | // into the `detect` function. 218 | const barcodeDetector = new BarcodeDetector({ formats }); 219 | 220 | const image = await imageElementFromUrl(url); 221 | 222 | return await barcodeDetector.detect(image); 223 | } 224 | -------------------------------------------------------------------------------- /packages/barqode/src/lib/internal/shim-get-user-media.ts: -------------------------------------------------------------------------------- 1 | // based on vue-qrcode-reader by Niklas Gruhn 2 | // see https://github.com/gruhn/vue-qrcode-reader 3 | 4 | // @ts-expect-error no types available 5 | import { shimGetUserMedia as chromeShim } from "webrtc-adapter/dist/chrome/getusermedia"; 6 | // @ts-expect-error no types available 7 | import { shimGetUserMedia as firefoxShim } from "webrtc-adapter/dist/firefox/getusermedia"; 8 | // @ts-expect-error no types available 9 | import { shimGetUserMedia as safariShim } from "webrtc-adapter/dist/safari/safari_shim"; 10 | // @ts-expect-error no types available 11 | import { detectBrowser } from "webrtc-adapter/dist/utils"; 12 | 13 | import { StreamApiNotSupportedError } from "./errors.js"; 14 | import { idempotent } from "./util.js"; 15 | 16 | export default idempotent(() => { 17 | const browserDetails = detectBrowser(window); 18 | 19 | switch (browserDetails.browser) { 20 | case "chrome": 21 | chromeShim(window, browserDetails); 22 | break; 23 | case "firefox": 24 | firefoxShim(window, browserDetails); 25 | break; 26 | case "safari": 27 | safariShim(window, browserDetails); 28 | break; 29 | default: 30 | throw new StreamApiNotSupportedError(); 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /packages/barqode/src/lib/internal/util.ts: -------------------------------------------------------------------------------- 1 | // based on vue-qrcode-reader by Niklas Gruhn 2 | // see https://github.com/gruhn/vue-qrcode-reader 3 | 4 | /** 5 | * Creates a function that ensures the given action is only executed once. 6 | * Subsequent calls to the returned function will return the result of the first call. 7 | * 8 | * @template T - The return type of the action. 9 | * @template U - The type of the arguments passed to the action. 10 | * @param {function(U[]): T} action - The action to be executed idempotently. 11 | * @returns {function(...U[]): T | undefined} A function that, when called, will execute the action only once and return the result. 12 | */ 13 | export const idempotent = (action: (x: U[]) => T) => { 14 | let called = false; 15 | let result: T | undefined = undefined; 16 | 17 | return (...args: U[]) => { 18 | if (called) { 19 | return result; 20 | } else { 21 | result = action(args); 22 | called = true; 23 | 24 | return result; 25 | } 26 | }; 27 | }; 28 | 29 | /** 30 | * Asserts that a given value is of type `never`, indicating that this code path should be unreachable. 31 | * Throws an error if called, signaling a logic error in the code. 32 | * 33 | * @param _witness - The value that should be of type `never`. 34 | * @throws {Error} Always throws an error to indicate unreachable code. 35 | */ 36 | export function assertNever(_witness: never): never { 37 | throw new Error("this code should be unreachable"); 38 | } 39 | -------------------------------------------------------------------------------- /packages/barqode/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from "@sveltejs/adapter-auto"; 2 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://svelte.dev/docs/kit/integrations 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 13 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 14 | adapter: adapter(), 15 | }, 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /packages/barqode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "module": "NodeNext", 13 | "moduleResolution": "NodeNext" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/barqode/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from "@sveltejs/kit/vite"; 2 | import { defineConfig } from "vite"; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | }); 7 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "docs" 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./docs/tsconfig.json" 3 | } 4 | --------------------------------------------------------------------------------